diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4c8c68e7..254f3b32 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -36,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - image: [server, agent, web, mcp, browser] + image: [server, agent, web, mcp, browser, sparse] platform: [linux/amd64, linux/arm64] include: - image: server @@ -49,6 +49,8 @@ jobs: dockerfile: docker/Dockerfile.mcp - image: browser dockerfile: docker/Dockerfile.browser + - image: sparse + dockerfile: docker/Dockerfile.sparse - platform: linux/amd64 runner: ubuntu-latest - platform: linux/arm64 @@ -132,7 +134,7 @@ jobs: needs: build strategy: matrix: - image: [server, agent, web, mcp, browser] + image: [server, agent, web, mcp, browser, sparse] steps: - name: Download digests uses: actions/download-artifact@v4 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 99051eaf..04d4473a 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -15,13 +15,24 @@ git clone https://github.com/memohai/Memoh.git cd Memoh cp conf/app.docker.toml config.toml nano config.toml # Change passwords and JWT secret -sudo docker compose up -d ``` > On macOS or if your user is in the `docker` group, `sudo` is not required. > **Important**: You must create `config.toml` before starting. `docker-compose.yml` mounts `./config.toml` into the containers — running without it will fail. +### Standard startup (with Qdrant + Browser) + +```bash +sudo docker compose --profile qdrant --profile browser up -d +``` + +### Minimal startup (core only) + +```bash +sudo docker compose up -d +``` + Access: - Web UI: http://localhost:8082 - API: http://localhost:8080 @@ -29,6 +40,39 @@ Access: Default credentials: `admin` / `admin123` (change in `config.toml`) +## Docker Compose Profiles + +The base `docker-compose.yml` contains all services. Core services (postgres, server, agent, web) always start. Optional services are gated by profiles and only start when explicitly enabled: + +| Profile | Service | Description | +|---------|---------|-------------| +| `qdrant` | Qdrant | Vector database for memory semantic search | +| `browser` | Browser | Browser automation gateway (Playwright) | +| `openviking` | OpenViking | Self-hosted OpenViking memory provider | + +### Supported combinations + +```bash +# Core + Qdrant + Browser (recommended default) +docker compose --profile qdrant --profile browser up -d + +# Core + Qdrant + OpenViking (self-hosted) +docker compose --profile qdrant --profile openviking up -d +``` + +### SaaS / external providers + +For Mem0 or OpenViking SaaS, no profile is needed. Configure the provider directly in the Memoh admin UI with the external `base_url` and API key. + +### China Mainland Mirror + +Uncomment `registry = "memoh.cn"` in `config.toml` under `[mcp]`, then add the CN overlay: + +```bash +sudo docker compose -f docker-compose.yml -f docker/docker-compose.cn.yml \ + --profile qdrant --profile browser up -d +``` + ## Prerequisites - Docker (with Docker Compose v2) @@ -43,14 +87,6 @@ Recommended changes for production: - `auth.jwt_secret` — JWT secret (generate with `openssl rand -base64 32`) - `postgres.password` — Database password (also set `POSTGRES_PASSWORD` env var) -### China Mainland Mirror - -Uncomment `registry = "memoh.cn"` in `config.toml` under `[mcp]`, then use: - -```bash -sudo docker compose -f docker-compose.yml -f docker/docker-compose.cn.yml up -d -``` - ## Common Commands > Prefix with `sudo` on Linux if your user is not in the `docker` group. diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 7a6384a3..dbe9edd3 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -258,12 +258,38 @@ "deleteConfirm": "Are you sure you want to delete this memory provider? Bots using it will lose memory access.", "name": "Name", "namePlaceholder": "Enter provider name", - "memoryModel": "Memory Model", - "memoryModelDescription": "LLM model for memory extraction and decision", - "embeddingModel": "Embedding Model", - "embeddingModelDescription": "Embedding model for dense vector search", + "builtinMode": "Built-in Memory Mode", + "builtinModeDescription": "Choose how the built-in memory provider stores and retrieves memory for bots.", + "sparseSectionTitle": "Sparse Retrieval", + "denseSectionTitle": "Dense Retrieval", + "sparseInstallHint": "Sparse mode depends on the optional sparse service. Enable the sparse installation option when running the installer or start the sparse profile in Docker Compose.", + "denseBackend": "Dense Backend", + "denseBackendValue": "Embedding API + Qdrant", + "denseEmbeddingModel": "Dense Embedding Model", + "denseEmbeddingModelDescription": "Select the third-party embedding model used before local rerank.", + "denseQdrantHint": "Dense memory will use Qdrant as the storage backend after the backend runtime is connected.", + "qdrantCollection": "Qdrant Collection", + "sparseQdrantCollectionDescription": "Sparse mode writes to the Qdrant collection. Current/default collection: {collection}.", + "denseQdrantCollectionDescription": "Dense mode writes to the Qdrant collection. Current/default collection: {collection}.", + "collectionPoints": "Points in collection", + "collectionExists": "Collection exists", + "collectionMissing": "Collection not created yet", + "collectionHealthy": "Healthy", + "collectionUnavailable": "Unavailable", + "modeNames": { + "off": "Off", + "sparse": "Sparse", + "dense": "Dense" + }, + "modeDescriptions": { + "off": "Keep the current file-based built-in memory behavior without Qdrant retrieval.", + "sparse": "Use the local OpenSearch sparse model for memory indexing and retrieval with Qdrant.", + "dense": "Use the selected embedding model API to build dense vectors and store/search them directly in Qdrant." + }, "providerNames": { - "builtin": "Built-in" + "builtin": "Built-in", + "mem0": "Mem0", + "openviking": "OpenViking" } }, "ttsProvider": { @@ -683,12 +709,32 @@ }, "settings": { "chatModel": "Chat Model", - "memoryModel": "Memory Model", - "embeddingModel": "Embedding Model", "searchProvider": "Search Provider", "searchProviderPlaceholder": "Select search provider", "memoryProvider": "Memory Provider", "memoryProviderPlaceholder": "Select memory provider (disabled if empty)", + "memoryModePreview": "Selected built-in mode: {mode}", + "sparseStatusTitle": "Sparse Retrieval Status", + "sparseStatusHint": "Markdown files remain the only source of truth. Manual sync rebuilds Qdrant from entries under /data/memory.", + "denseStatusTitle": "Dense Retrieval Status", + "denseStatusHint": "Markdown files remain the only source of truth. Manual sync rebuilds the dense Qdrant index from entries under /data/memory.", + "mem0StatusTitle": "Mem0 Sync Status", + "mem0StatusHint": "Markdown files remain the only source of truth. Manual sync rebuilds managed Mem0 memories from entries under /data/memory.", + "indexedMemoryStatusPendingSave": "Save the selected memory provider before viewing indexed memory status or running a sync.", + "memorySyncAction": "Manual Sync", + "memorySyncSuccess": "Sync completed: {fsCount} source entries, {restoredCount} restored, {storageCount} indexed.", + "memorySyncFailed": "Manual sync failed", + "memorySourceDir": "Memory Directory", + "memoryOverviewPath": "Overview File", + "memoryMarkdownFiles": "Markdown Files", + "memorySourceEntries": "Source Entries", + "memoryIndexedEntries": "Indexed Entries", + "memoryQdrantCollection": "Qdrant Collection", + "memoryEncoderHealth": "Sparse Encoder", + "memoryDenseEmbeddingHealth": "Embedding Backend", + "memoryQdrantHealth": "Qdrant", + "memoryHealthOk": "Healthy", + "memoryHealthUnavailable": "Unavailable", "ttsModel": "TTS Model", "ttsModelPlaceholder": "Select TTS model", "maxContextLoadTime": "Max Context Load Time", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index 879a21ea..87d52b53 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -254,12 +254,38 @@ "deleteConfirm": "确定要删除该记忆提供方吗?使用它的 Bot 将失去记忆访问。", "name": "名称", "namePlaceholder": "输入提供方名称", - "memoryModel": "记忆模型", - "memoryModelDescription": "用于记忆提取和决策的 LLM 模型", - "embeddingModel": "向量化模型", - "embeddingModelDescription": "用于稠密向量搜索的embedding模型", + "builtinMode": "内置记忆模式", + "builtinModeDescription": "选择内置记忆提供方为 Bot 存储和检索记忆的方式。", + "sparseSectionTitle": "稀疏检索", + "denseSectionTitle": "稠密检索", + "sparseInstallHint": "Sparse 模式依赖可选的 sparse 服务。安装时请启用 sparse 选项,或在 Docker Compose 中启动 sparse profile。", + "denseBackend": "Dense 后端", + "denseBackendValue": "Embedding API + Qdrant", + "denseEmbeddingModel": "Dense 向量模型", + "denseEmbeddingModelDescription": "选择本地 rerank 之前使用的第三方 embedding 模型。", + "denseQdrantHint": "后端接通后,dense memory 会以 Qdrant 作为存储后端。", + "qdrantCollection": "Qdrant Collection", + "sparseQdrantCollectionDescription": "稀疏模式会写入对应的 Qdrant collection。当前/默认 collection:{collection}。", + "denseQdrantCollectionDescription": "稠密模式会写入对应的 Qdrant collection。当前/默认 collection:{collection}。", + "collectionPoints": "Collection 中的点数", + "collectionExists": "Collection 已存在", + "collectionMissing": "Collection 尚未创建", + "collectionHealthy": "正常", + "collectionUnavailable": "不可用", + "modeNames": { + "off": "关闭", + "sparse": "稀疏", + "dense": "稠密" + }, + "modeDescriptions": { + "off": "保持当前基于文件的内置记忆行为,不启用 Qdrant 检索。", + "sparse": "使用本地 OpenSearch 稀疏模型,并通过 Qdrant 进行记忆索引与检索。", + "dense": "使用所选 embedding 模型 API 生成 dense 向量,并直接写入/检索 Qdrant。" + }, "providerNames": { - "builtin": "内置" + "builtin": "内置", + "mem0": "Mem0", + "openviking": "OpenViking" } }, "ttsProvider": { @@ -679,12 +705,32 @@ }, "settings": { "chatModel": "对话模型", - "memoryModel": "记忆模型", - "embeddingModel": "向量模型", "searchProvider": "搜索提供方", "searchProviderPlaceholder": "选择搜索提供方", "memoryProvider": "记忆提供方", "memoryProviderPlaceholder": "选择记忆提供方(为空则禁用)", + "memoryModePreview": "当前内置模式:{mode}", + "sparseStatusTitle": "稀疏检索状态", + "sparseStatusHint": "Markdown 文件是唯一可信源;手动同步会把 /data/memory 下的条目重新写入 Qdrant。", + "denseStatusTitle": "稠密检索状态", + "denseStatusHint": "Markdown 文件是唯一可信源;手动同步会把 /data/memory 下的条目重新写入稠密 Qdrant 索引。", + "mem0StatusTitle": "Mem0 同步状态", + "mem0StatusHint": "Markdown 文件是唯一可信源;手动同步会把 /data/memory 下托管的记忆条目重新同步到 Mem0。", + "indexedMemoryStatusPendingSave": "请先保存当前记忆提供方选择,再查看索引状态或执行同步。", + "memorySyncAction": "手动同步", + "memorySyncSuccess": "同步完成:源条目 {fsCount} 条,修复 {restoredCount} 条,索引现有 {storageCount} 条。", + "memorySyncFailed": "手动同步失败", + "memorySourceDir": "记忆目录", + "memoryOverviewPath": "概览文件", + "memoryMarkdownFiles": "Markdown 文件数", + "memorySourceEntries": "源条目数", + "memoryIndexedEntries": "索引条目数", + "memoryQdrantCollection": "Qdrant Collection", + "memoryEncoderHealth": "Sparse Encoder", + "memoryDenseEmbeddingHealth": "Embedding 后端", + "memoryQdrantHealth": "Qdrant", + "memoryHealthOk": "正常", + "memoryHealthUnavailable": "暂不可用", "ttsModel": "语音合成模型", "ttsModelPlaceholder": "选择语音合成模型", "maxContextLoadTime": "最大上下文加载时间", diff --git a/apps/web/src/pages/bots/components/bot-memory.vue b/apps/web/src/pages/bots/components/bot-memory.vue index 2d5ed8fb..278e138c 100644 --- a/apps/web/src/pages/bots/components/bot-memory.vue +++ b/apps/web/src/pages/bots/components/bot-memory.vue @@ -182,147 +182,38 @@ -
+
Vector Manifold
- +

- Top-K Bucket + {{ chartLeftTitle }}

-
-
- - -
- -
- {{ topKMaxValue.toFixed(4) }} - {{ topKMinValue.toFixed(4) }} -
-
+
- +

- Energy Gradient (CDF) + {{ chartRightTitle }}

-
- - - - - - - - - - - - - - - -
-
-
- - -
-

- K: {{ hoveredCdfPoint.k }} -

-

- P: {{ hoveredCdfPoint.cumulative.toFixed(6) }} -

-
- - -
- 1.0 - 0.0 -
-
- k=1 - k={{ selectedCdfLength }} -
-
+
@@ -554,6 +445,15 @@ diff --git a/apps/web/src/pages/bots/components/bot-settings.vue b/apps/web/src/pages/bots/components/bot-settings.vue index 8303047e..071d8053 100644 --- a/apps/web/src/pages/bots/components/bot-settings.vue +++ b/apps/web/src/pages/bots/components/bot-settings.vue @@ -20,6 +20,135 @@ :providers="memoryProviders" :placeholder="$t('bots.settings.memoryProviderPlaceholder')" /> +
+ {{ $t('bots.settings.memoryModePreview', { + mode: $t(`memoryProvider.modeNames.${selectedBuiltinMemoryMode}`), + }) }} +
+
+
+
+

+ {{ indexedMemoryStatusTitle }} +

+

+ {{ isSelectedMemoryProviderPersisted + ? indexedMemoryStatusHint + : $t('bots.settings.indexedMemoryStatusPendingSave') }} +

+
+ +
+ +
+ {{ $t('common.loading') }} +
+ +
+
+

+ {{ $t('bots.settings.memorySourceDir') }} +

+

+ {{ statusCardData.source_dir || '-' }} +

+
+
+

+ {{ $t('bots.settings.memoryOverviewPath') }} +

+

+ {{ statusCardData.overview_path || '-' }} +

+
+
+

+ {{ $t('bots.settings.memoryMarkdownFiles') }} +

+

+ {{ statusCardData.markdown_file_count ?? 0 }} +

+
+
+

+ {{ $t('bots.settings.memorySourceEntries') }} +

+

+ {{ statusCardData.source_count ?? 0 }} +

+
+
+

+ {{ $t('bots.settings.memoryIndexedEntries') }} +

+

+ {{ statusCardData.indexed_count ?? 0 }} +

+
+
+

+ {{ $t('bots.settings.memoryQdrantCollection') }} +

+

+ {{ statusCardData.qdrant_collection || '-' }} +

+
+
+

+ {{ encoderHealthLabel }} +

+

+ {{ healthLabel(statusCardData.encoder?.ok, statusCardData.encoder?.error) }} +

+
+
+

+ {{ $t('bots.settings.memoryQdrantHealth') }} +

+

+ {{ healthLabel(statusCardData.qdrant?.ok, statusCardData.qdrant?.error) }} +

+
+
+
@@ -213,7 +342,7 @@ import MemoryProviderSelect from './memory-provider-select.vue' import TtsModelSelect from './tts-model-select.vue' import BrowserContextSelect from './browser-context-select.vue' import { useQuery, useMutation, useQueryCache } from '@pinia/colada' -import { getBotsByBotIdSettings, putBotsByBotIdSettings, deleteBotsById, getModels, getProviders, getSearchProviders, getMemoryProviders, getTtsProviders, getBrowserContexts } from '@memoh/sdk' +import { getBotsByBotIdSettings, putBotsByBotIdSettings, deleteBotsById, getModels, getProviders, getSearchProviders, getMemoryProviders, getTtsProviders, getBrowserContexts, getBotsByBotIdMemoryStatus, postBotsByBotIdMemoryRebuild } from '@memoh/sdk' import type { SettingsSettings } from '@memoh/sdk' import type { Ref } from 'vue' import { resolveApiErrorMessage } from '@/utils/api-error' @@ -332,6 +461,53 @@ const memoryProviders = computed(() => memoryProviderData.value ?? []) const ttsProviders = computed(() => ttsProviderData.value ?? []) const ttsModels = computed(() => ttsModelData.value ?? []) const browserContexts = computed(() => browserContextData.value ?? []) +const selectedMemoryProvider = computed(() => + memoryProviders.value.find((provider) => provider.id === form.memory_provider_id), +) +const selectedMemoryProviderType = computed(() => + selectedMemoryProvider.value?.provider ?? '', +) +const selectedBuiltinMemoryProvider = computed(() => + selectedMemoryProvider.value?.provider === 'builtin' ? selectedMemoryProvider.value : null, +) +const selectedMem0MemoryProvider = computed(() => + selectedMemoryProvider.value?.provider === 'mem0' ? selectedMemoryProvider.value : null, +) +const selectedBuiltinMemoryMode = computed(() => + (selectedBuiltinMemoryProvider.value?.config as Record | undefined)?.memory_mode || 'off', +) +const persistedMemoryProviderID = computed(() => settings.value?.memory_provider_id ?? '') +const isSelectedMemoryProviderPersisted = computed(() => + !!form.memory_provider_id && form.memory_provider_id === persistedMemoryProviderID.value, +) +const showBuiltinIndexedMemoryStatus = computed(() => + selectedBuiltinMemoryMode.value === 'sparse' || selectedBuiltinMemoryMode.value === 'dense', +) +const showMem0MemoryStatus = computed(() => + !!selectedMem0MemoryProvider.value, +) +const showMemoryProviderStatusCard = computed(() => + showBuiltinIndexedMemoryStatus.value || showMem0MemoryStatus.value, +) +const shouldLoadMemoryStatus = computed(() => + !!botIdRef.value + && showMemoryProviderStatusCard.value + && isSelectedMemoryProviderPersisted.value, +) +const indexedMemoryStatusTitle = computed(() => + selectedMemoryProviderType.value === 'mem0' + ? t('bots.settings.mem0StatusTitle') + : selectedBuiltinMemoryMode.value === 'dense' + ? t('bots.settings.denseStatusTitle') + : t('bots.settings.sparseStatusTitle'), +) +const indexedMemoryStatusHint = computed(() => + selectedMemoryProviderType.value === 'mem0' + ? t('bots.settings.mem0StatusHint') + : selectedBuiltinMemoryMode.value === 'dense' + ? t('bots.settings.denseStatusHint') + : t('bots.settings.sparseStatusHint'), +) const chatModelSupportsReasoning = computed(() => { if (!form.chat_model_id) return false @@ -339,6 +515,48 @@ const chatModelSupportsReasoning = computed(() => { return !!m?.supports_reasoning }) +const { data: memoryStatusData, isLoading: isMemoryStatusLoading } = useQuery({ + key: () => ['bot-memory-status', botIdRef.value, persistedMemoryProviderID.value], + query: async () => { + const { data } = await getBotsByBotIdMemoryStatus({ + path: { bot_id: botIdRef.value }, + throwOnError: true, + }) + return data + }, + enabled: () => shouldLoadMemoryStatus.value, +}) + +const { mutateAsync: rebuildMemory, isLoading: isRebuilding } = useMutation({ + mutation: async () => { + const { data } = await postBotsByBotIdMemoryRebuild({ + path: { bot_id: botIdRef.value }, + throwOnError: true, + }) + return data + }, + onSettled: () => { + queryCache.invalidateQueries({ key: ['bot-memory-status', botIdRef.value, persistedMemoryProviderID.value] }) + }, +}) + +const memoryStatus = computed(() => memoryStatusData.value ?? null) +const statusCardData = computed(() => memoryStatus.value) +const showQdrantDetails = computed(() => + selectedBuiltinMemoryMode.value === 'sparse' || selectedBuiltinMemoryMode.value === 'dense', +) +const showEncoderHealth = computed(() => + selectedBuiltinMemoryMode.value === 'sparse' || selectedBuiltinMemoryMode.value === 'dense', +) +const showQdrantHealth = computed(() => + selectedBuiltinMemoryMode.value === 'sparse' || selectedBuiltinMemoryMode.value === 'dense', +) +const encoderHealthLabel = computed(() => + selectedBuiltinMemoryMode.value === 'dense' + ? t('bots.settings.memoryDenseEmbeddingHealth') + : t('bots.settings.memoryEncoderHealth'), +) + // ---- Form ---- const form = reactive({ chat_model_id: '', @@ -358,9 +576,9 @@ watch(settings, (val) => { if (val) { form.chat_model_id = val.chat_model_id ?? '' form.search_provider_id = val.search_provider_id ?? '' - form.memory_provider_id = (val as any).memory_provider_id ?? '' - form.tts_model_id = (val as any).tts_model_id ?? '' - form.browser_context_id = (val as any).browser_context_id ?? '' + form.memory_provider_id = val.memory_provider_id ?? '' + form.tts_model_id = val.tts_model_id ?? '' + form.browser_context_id = val.browser_context_id ?? '' form.max_context_load_time = val.max_context_load_time ?? 0 form.max_context_tokens = val.max_context_tokens ?? 0 form.language = val.language ?? '' @@ -372,7 +590,7 @@ watch(settings, (val) => { const hasChanges = computed(() => { if (!settings.value) return true - const s = settings.value as any + const s = settings.value let changed = form.chat_model_id !== (s.chat_model_id ?? '') || form.search_provider_id !== (s.search_provider_id ?? '') @@ -399,6 +617,33 @@ async function handleSave() { } } +function healthTextClass(ok: boolean | undefined) { + return ok ? 'text-foreground' : 'text-destructive' +} + +function healthLabel(ok: boolean | undefined, error?: string) { + if (ok) return t('bots.settings.memoryHealthOk') + if (error) return error + return t('bots.settings.memoryHealthUnavailable') +} + +async function handleMemorySync() { + if (!isSelectedMemoryProviderPersisted.value) { + toast.error(t('bots.settings.indexedMemoryStatusPendingSave')) + return + } + try { + const result = await rebuildMemory() + toast.success(t('bots.settings.memorySyncSuccess', { + fsCount: result?.fs_count ?? 0, + restoredCount: result?.restored_count ?? 0, + storageCount: result?.storage_count ?? 0, + })) + } catch (error) { + toast.error(resolveApiErrorMessage(error, t('bots.settings.memorySyncFailed'))) + } +} + async function handleDeleteBot() { try { await deleteBot() diff --git a/apps/web/src/pages/bots/components/memory-provider-select.vue b/apps/web/src/pages/bots/components/memory-provider-select.vue index b3168ff0..4f852dc7 100644 --- a/apps/web/src/pages/bots/components/memory-provider-select.vue +++ b/apps/web/src/pages/bots/components/memory-provider-select.vue @@ -62,6 +62,7 @@ interface MemoryProviderItem { id: string name: string provider: string + config?: Record } const props = defineProps<{ @@ -81,8 +82,14 @@ const options = computed(() => { const providerOptions = props.providers.map((provider) => ({ value: provider.id || '', label: provider.name || provider.id || '', - description: provider.provider, - keywords: [provider.name ?? '', provider.provider ?? ''], + description: provider.provider === 'builtin' + ? t(`memoryProvider.modeNames.${provider.config?.memory_mode || 'off'}`) + : provider.provider, + keywords: [ + provider.name ?? '', + provider.provider ?? '', + provider.config?.memory_mode ?? '', + ], })) return [noneOption, ...providerOptions] }) diff --git a/apps/web/src/pages/memory-providers/components/add-memory-provider.vue b/apps/web/src/pages/memory-providers/components/add-memory-provider.vue index 46a1c9ce..3eff83f1 100644 --- a/apps/web/src/pages/memory-providers/components/add-memory-provider.vue +++ b/apps/web/src/pages/memory-providers/components/add-memory-provider.vue @@ -35,6 +35,12 @@ {{ $t('memoryProvider.providerNames.builtin') }} + + {{ $t('memoryProvider.providerNames.mem0') }} + + + {{ $t('memoryProvider.providerNames.openviking') }} + diff --git a/apps/web/src/pages/memory-providers/components/provider-setting.vue b/apps/web/src/pages/memory-providers/components/provider-setting.vue index 73eca487..36ca7948 100644 --- a/apps/web/src/pages/memory-providers/components/provider-setting.vue +++ b/apps/web/src/pages/memory-providers/components/provider-setting.vue @@ -43,32 +43,166 @@ />
- + + + + @@ -90,9 +224,16 @@