import { useCallback, useEffect, useState, type ChangeEvent } from 'react'; import { getMemoryV2AdminConfig, listMemoryV2ModelOptions, updateMemoryV2AdminConfig, } from '../../api/client'; import type { LlmGlobalSettings, LlmProviderKeyRow, MemoryV2AdminConfig, MemoryV2AdminConfigResponse, MemoryV2ModelApiType, } from '../../types'; type BackendKey = | 'pgvector' | 'qdrant' | 'weaviate' | 'mem0' | 'letta' | 'neo4j' | 'redisStreams' | 'langgraph'; type ModelBackendKey = 'mem0' | 'letta' | 'langgraph'; const BACKENDS: Array<{ key: BackendKey; label: string; fields: Array<{ key: string; label: string; secret?: boolean }>; }> = [ { key: 'pgvector', label: 'pgvector', fields: [ { key: 'databaseUrl', label: 'Database URL', secret: true }, { key: 'table', label: 'Table' }, { key: 'embeddingModule', label: 'Embedding Module' }, { key: 'poolMax', label: 'Pool Max' }, { key: 'limit', label: 'Recall Limit' }, ], }, { key: 'qdrant', label: 'Qdrant', fields: [ { key: 'url', label: 'Base URL' }, { key: 'collection', label: 'Collection' }, { key: 'embeddingModule', label: 'Embedding Module' }, { key: 'apiKey', label: 'API Key', secret: true }, { key: 'timeoutMs', label: 'Timeout Ms' }, { key: 'limit', label: 'Recall Limit' }, ], }, { key: 'weaviate', label: 'Weaviate', fields: [ { key: 'url', label: 'Base URL' }, { key: 'collection', label: 'Collection' }, { key: 'embeddingModule', label: 'Embedding Module' }, { key: 'apiKey', label: 'API Key', secret: true }, { key: 'timeoutMs', label: 'Timeout Ms' }, { key: 'limit', label: 'Recall Limit' }, ], }, { key: 'mem0', label: 'Mem0', fields: [ { key: 'apiKey', label: 'API Key', secret: true }, { key: 'projectId', label: 'Project ID' }, { key: 'baseUrl', label: 'Base URL' }, { key: 'writePath', label: 'Write Path' }, { key: 'compactPath', label: 'Compact Path' }, { key: 'timeoutMs', label: 'Timeout Ms' }, ], }, { key: 'letta', label: 'Letta', fields: [ { key: 'apiKey', label: 'API Key', secret: true }, { key: 'projectId', label: 'Project ID' }, { key: 'agentId', label: 'Agent ID' }, { key: 'baseUrl', label: 'Base URL' }, { key: 'resolvePath', label: 'Resolve Path' }, { key: 'writePath', label: 'Write Path' }, { key: 'compactPath', label: 'Compact Path' }, { key: 'timeoutMs', label: 'Timeout Ms' }, ], }, { key: 'neo4j', label: 'Neo4j', fields: [ { key: 'uri', label: 'Bolt URI' }, { key: 'httpUrl', label: 'HTTP URL' }, { key: 'user', label: 'Username' }, { key: 'password', label: 'Password', secret: true }, { key: 'database', label: 'Database' }, { key: 'timeoutMs', label: 'Timeout Ms' }, ], }, { key: 'redisStreams', label: 'Redis Streams', fields: [ { key: 'url', label: 'Redis URL' }, { key: 'stream', label: 'Stream' }, ], }, { key: 'langgraph', label: 'LangGraph', fields: [ { key: 'url', label: 'Base URL' }, { key: 'policyId', label: 'Policy ID' }, { key: 'apiKey', label: 'API Key', secret: true }, { key: 'resolvePath', label: 'Resolve Path' }, { key: 'timeoutMs', label: 'Timeout Ms' }, ], }, ]; const MODEL_BACKENDS: ModelBackendKey[] = ['mem0', 'letta', 'langgraph']; const MODEL_API_TYPES: Array<{ value: MemoryV2ModelApiType; label: string }> = [ { value: 'chat', label: 'Chat API' }, { value: 'response', label: 'Response API' }, ]; function defaultConfig(): MemoryV2AdminConfig { return { global: { enabled: false, backend: 'legacy', profileEnabled: false, eventLogEnabled: false, vectorEnabled: false, failOpen: true, }, chatIntentRouter: { enabled: false, modelProviderKeyId: '', model: '', modelApiType: 'chat', minConfidence: '0.65', memoryResolveEnabled: true, memoryResolveLimit: '8', timeoutMs: '1500', fallbackRoute: 'agent_orchestration', }, pgvector: {}, qdrant: {}, weaviate: {}, mem0: {}, letta: {}, neo4j: {}, redisStreams: {}, langgraph: {}, }; } function normalizeConfig(config?: Partial | null): MemoryV2AdminConfig { const base = defaultConfig(); return { global: { ...base.global, ...(config?.global ?? {}) }, chatIntentRouter: { ...base.chatIntentRouter, ...(config?.chatIntentRouter ?? {}) }, pgvector: { ...base.pgvector, ...(config?.pgvector ?? {}) }, qdrant: { ...base.qdrant, ...(config?.qdrant ?? {}) }, weaviate: { ...base.weaviate, ...(config?.weaviate ?? {}) }, mem0: { ...base.mem0, ...(config?.mem0 ?? {}) }, letta: { ...base.letta, ...(config?.letta ?? {}) }, neo4j: { ...base.neo4j, ...(config?.neo4j ?? {}) }, redisStreams: { ...base.redisStreams, ...(config?.redisStreams ?? {}) }, langgraph: { ...base.langgraph, ...(config?.langgraph ?? {}) }, }; } function isModelBackend(backend: BackendKey): backend is ModelBackendKey { return MODEL_BACKENDS.includes(backend as ModelBackendKey); } function dedupeStrings(values: Array): string[] { return [...new Set(values.map((value) => String(value ?? '').trim()).filter(Boolean))]; } function findProviderKey(keys: LlmProviderKeyRow[], keyId: string | null | undefined): LlmProviderKeyRow | null { const resolvedKeyId = String(keyId ?? '').trim(); if (!resolvedKeyId) return null; return keys.find((key) => key.id === resolvedKeyId) ?? null; } function providerLabel(key: LlmProviderKeyRow): string { return `${key.name} · ${key.providerLabel}`; } function modelOptionsForProvider( providerKey: LlmProviderKeyRow | null, globalSettings: LlmGlobalSettings | null, ): string[] { if (!providerKey) { return dedupeStrings([globalSettings?.globalModel, ...(globalSettings?.availableModels ?? [])]); } return dedupeStrings([providerKey.defaultModel, ...(providerKey.models ?? [])]); } function describeEffectiveModelSource( section: MemoryV2AdminConfig[ModelBackendKey], providerKeys: LlmProviderKeyRow[], globalSettings: LlmGlobalSettings | null, ): string { const providerKey = findProviderKey(providerKeys, section.modelProviderKeyId); const model = String(section.model ?? '').trim(); const apiType = String(section.modelApiType ?? '').trim(); const providerText = providerKey ? providerLabel(providerKey) : (globalSettings?.keyName ? `${globalSettings.keyName} · ${globalSettings.providerLabel ?? '统一模型中心'}` : '统一模型中心默认'); const modelText = model || providerKey?.defaultModel || globalSettings?.globalModel || 'provider 默认模型'; const apiText = apiType || '未指定 API 类型'; return `${providerText} / ${modelText} / ${apiText}`; } export function MemoryV2Page() { const [payload, setPayload] = useState(null); const [draft, setDraft] = useState(defaultConfig()); const [providerKeys, setProviderKeys] = useState([]); const [globalModelSettings, setGlobalModelSettings] = useState(null); const [supportedApiTypes, setSupportedApiTypes] = useState(['chat', 'response']); const [loading, setLoading] = useState(true); const [busyScope, setBusyScope] = useState<'global' | 'chatIntentRouter' | BackendKey | 'reload' | null>(null); const [error, setError] = useState(null); const [message, setMessage] = useState(null); const load = useCallback(async () => { setLoading(true); setBusyScope('reload'); setError(null); setMessage(null); try { const [configResult, modelOptions] = await Promise.all([ getMemoryV2AdminConfig(), listMemoryV2ModelOptions(), ]); setPayload(configResult); setDraft(normalizeConfig(configResult.config)); setProviderKeys(modelOptions.keys.filter((item) => item.status === 'active')); setGlobalModelSettings(modelOptions.global); setSupportedApiTypes(modelOptions.supportedApiTypes); } catch (err) { setError(err instanceof Error ? err.message : '加载 Memory V2 配置失败'); } finally { setLoading(false); setBusyScope((current) => (current === 'reload' ? null : current)); } }, []); useEffect(() => { void load(); }, [load]); const updateGlobalFlag = (key: keyof MemoryV2AdminConfig['global']) => (event: ChangeEvent) => { setDraft((current) => ({ ...current, global: { ...current.global, [key]: event.target.checked }, })); }; const updateGlobalText = (key: keyof MemoryV2AdminConfig['global']) => (event: ChangeEvent) => { setDraft((current) => ({ ...current, global: { ...current.global, [key]: event.target.value }, })); }; const updateRouterFlag = (key: string) => (event: ChangeEvent) => { setDraft((current) => ({ ...current, chatIntentRouter: { ...current.chatIntentRouter, [key]: event.target.checked }, })); }; const updateRouterValue = (key: string) => (event: ChangeEvent) => { setDraft((current) => ({ ...current, chatIntentRouter: { ...current.chatIntentRouter, [key]: event.target.value }, })); }; const updateBackendFlag = (backend: BackendKey) => (event: ChangeEvent) => { setDraft((current) => ({ ...current, [backend]: { ...current[backend], enabled: event.target.checked }, })); }; const updateBackendValue = (backend: BackendKey, field: string) => ( event: ChangeEvent, ) => { setDraft((current) => ({ ...current, [backend]: { ...current[backend], [field]: event.target.value }, })); }; const updateModelProvider = (backend: ModelBackendKey) => (event: ChangeEvent) => { const nextProviderKeyId = event.target.value; const nextProviderKey = findProviderKey(providerKeys, nextProviderKeyId); const nextOptions = modelOptionsForProvider(nextProviderKey, globalModelSettings); setDraft((current) => { const currentSection = current[backend]; const currentModel = String(currentSection.model ?? '').trim(); const nextModel = nextOptions.includes(currentModel) ? currentModel : String(nextProviderKey?.defaultModel ?? globalModelSettings?.globalModel ?? '').trim(); return { ...current, [backend]: { ...currentSection, modelProviderKeyId: nextProviderKeyId, model: nextModel, }, }; }); }; const updateRouterModelProvider = (event: ChangeEvent) => { const nextProviderKeyId = event.target.value; const nextProviderKey = findProviderKey(providerKeys, nextProviderKeyId); const nextOptions = modelOptionsForProvider(nextProviderKey, globalModelSettings); setDraft((current) => { const currentSection = current.chatIntentRouter; const currentModel = String(currentSection.model ?? '').trim(); const nextModel = nextOptions.includes(currentModel) ? currentModel : String(nextProviderKey?.defaultModel ?? globalModelSettings?.globalModel ?? '').trim(); return { ...current, chatIntentRouter: { ...currentSection, modelProviderKeyId: nextProviderKeyId, model: nextModel, }, }; }); }; const applySavedConfig = (result: MemoryV2AdminConfigResponse, successMessage: string) => { setPayload(result); setDraft(normalizeConfig(result.config)); setMessage(successMessage); }; const saveConfigSection = async ( patch: Partial, scope: 'global' | 'chatIntentRouter' | BackendKey, successMessage: string, ) => { setBusyScope(scope); setError(null); setMessage(null); try { const result = await updateMemoryV2AdminConfig(patch); applySavedConfig(result, successMessage); } catch (err) { setError(err instanceof Error ? err.message : '保存 Memory V2 配置失败'); } finally { setBusyScope((current) => (current === scope ? null : current)); } }; const saveGlobalConfig = async () => { await saveConfigSection( { global: draft.global }, 'global', '全局 Memory V2 开关已保存。主站运行时下次访问记忆 facade 时会按新配置自动刷新。', ); }; const saveRouterConfig = async () => { await saveConfigSection( { chatIntentRouter: draft.chatIntentRouter }, 'chatIntentRouter', '前置 LLM 意图路由配置已保存。关闭时主站会退回原有 Direct Chat / Agent 判定链路。', ); }; const saveBackendConfig = async (backend: BackendKey) => { await saveConfigSection( { [backend]: draft[backend] }, backend, `${BACKENDS.find((item) => item.key === backend)?.label ?? backend} 配置已保存。主站运行时下次访问记忆 facade 时会按新配置自动刷新。`, ); }; const current = payload?.config ? normalizeConfig(payload.config) : draft; const routerSection = draft.chatIntentRouter; const currentRouterSection = current.chatIntentRouter; const routerProviderKey = findProviderKey(providerKeys, String(routerSection.modelProviderKeyId ?? '')); const routerAvailableModels = modelOptionsForProvider(routerProviderKey, globalModelSettings); return (

Memory V2 配置

这里是 Memory V2 的统一后台控制面。`memind_adm` 负责改配置,主站会从共享数据库读取并自动切换。

{error &&

{error}

} {message &&

{message}

}

全局开关

更新时间:{payload?.updatedAt ? new Date(payload.updatedAt).toLocaleString('zh-CN', { hour12: false }) : '未保存过数据库配置'}

{!loading && (

当前数据库配置默认 backend:{String(current.global.backend ?? 'legacy')}

)}

前置 LLM 意图路由

关闭时主站完全退回原有 Direct Chat / Agent 判定;打开后才会调用轻量 LLM,并可读取 Memory V2 的轻量上下文。

当前数据库配置:{currentRouterSection.enabled ? '已启用' : '未启用'} {' · '} 生效模型链路:{describeEffectiveModelSource(routerSection, providerKeys, globalModelSettings)}

{!providerKeys.length && (

统一模型中心当前没有可用的激活 key;若启用路由,请先配置 Provider key 与模型列表。

)}
{BACKENDS.map((backend) => { const section = draft[backend.key]; const currentSection = current[backend.key]; const providerKey = isModelBackend(backend.key) ? findProviderKey(providerKeys, section.modelProviderKeyId) : null; const availableModels = isModelBackend(backend.key) ? modelOptionsForProvider(providerKey, globalModelSettings) : []; return (

{backend.label}

单独启用后,只有当全局默认 backend 指向它时才会成为主读写目标。

{backend.fields.map((field) => { const configured = Boolean(currentSection?.[`${field.key}Configured`]); const masked = String(currentSection?.[`${field.key}Masked`] ?? ''); return ( ); })}
{isModelBackend(backend.key) && ( <>

当前生效模型链路:{describeEffectiveModelSource(section, providerKeys, globalModelSettings)}

{!providerKeys.length && (

统一模型中心当前没有可用的激活 key,请先到“统一模型中心”配置 provider key 与模型列表。

)} )}
); })}
); }