From 04859defeb95da10eca44a6cef1aab82c963548e Mon Sep 17 00:00:00 2001 From: john Date: Thu, 23 Jul 2026 21:38:41 +0800 Subject: [PATCH 1/5] feat: manage pluggable search services --- server/app.mjs | 9 ++ src/admin/pages/MindSearchPage.tsx | 185 +++++++++++++++++++++++++---- src/api/client.ts | 5 + src/index.css | 38 ++++++ src/types.ts | 33 +++++ 5 files changed, 250 insertions(+), 20 deletions(-) diff --git a/server/app.mjs b/server/app.mjs index 6ea46dc..32dd843 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -401,6 +401,15 @@ export function createAdminApp(services) { if (!mindSearchConfigService?.getRuntimeState) return res.status(503).json({ message: 'MindSearch 配置未启用' }); res.json(await mindSearchConfigService.getRuntimeState()); }); + adminApi.post('/mindsearch/services/:serviceId/test', requireAdmin, async (req, res) => { + if (!mindSearchConfigService?.testService) return res.status(503).json({ message: 'MindSearch 服务测试未启用' }); + try { + return res.json(await mindSearchConfigService.testService(req.params.serviceId)); + } catch (error) { + if (error?.code === 'SEARCH_SERVICE_NOT_FOUND') return res.status(404).json({ message: error.message }); + throw error; + } + }); adminApi.get('/memory-v2/status', requireAdmin, async (_req, res) => { const portalBaseUrl = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`; diff --git a/src/admin/pages/MindSearchPage.tsx b/src/admin/pages/MindSearchPage.tsx index 71c5d28..6cb219c 100644 --- a/src/admin/pages/MindSearchPage.tsx +++ b/src/admin/pages/MindSearchPage.tsx @@ -1,14 +1,40 @@ import { useEffect, useState } from 'react'; -import { getMindSearchConfig, updateMindSearchConfig } from '../../api/client'; -import type { MindSearchConfig } from '../../types'; +import { getMindSearchConfig, testMindSearchService, updateMindSearchConfig } from '../../api/client'; +import type { MindSearchConfig, MindSearchRoutes, MindSearchService } from '../../types'; + +const builtInServices: MindSearchService[] = [ + { id: 'searxng', name: 'SearXNG', kind: 'provider', adapter: 'searxng', capabilities: ['search.web', 'search.news'], endpoint: '', healthPath: '/config', enabled: false, timeoutMs: 8000, priority: 100 }, + { id: 'github', name: 'GitHub Code', kind: 'provider', adapter: 'github', capabilities: ['search.code'], endpoint: 'https://api.github.com/search/code', healthPath: '', enabled: false, timeoutMs: 8000, priority: 90 }, + { id: 'reader', name: 'Safe Reader', kind: 'reader', adapter: 'reader', capabilities: ['search.read'], endpoint: '', healthPath: '', enabled: false, timeoutMs: 8000, priority: 80 }, +]; const initial: MindSearchConfig = { enabled: false, mode: 'off', providers: { searxng: false, github: false, reader: false }, settings: { searxngEndpoint: '', maxResults: 10, timeoutMs: 8000, readerMaxChars: 12000 }, + services: builtInServices, + routes: { web: 'searxng', news: 'searxng', code: 'github', read: 'reader', research: '' }, }; +const routeLabels: Record = { + web: '普通网页搜索', + news: '新闻搜索', + code: '代码搜索', + read: '网页读取', + research: '深度研究', +}; + +function mergeConfig(config: MindSearchConfig): MindSearchConfig { + return { + ...initial, + ...config, + settings: { ...initial.settings, ...config.settings }, + services: config.services?.length ? config.services : builtInServices, + routes: { ...initial.routes, ...config.routes }, + }; +} + export function MindSearchPage() { const [config, setConfig] = useState(initial); const [meta, setMeta] = useState<{ source?: string; updatedAt?: number | null; updatedBy?: string | null }>({}); @@ -17,7 +43,7 @@ export function MindSearchPage() { useEffect(() => { getMindSearchConfig() .then((result) => { - setConfig({ ...initial, ...result.config, settings: { ...initial.settings, ...result.config.settings } }); + setConfig(mergeConfig(result.config)); setMeta(result); setMessage(''); }) @@ -28,7 +54,7 @@ export function MindSearchPage() { setMessage('保存中…'); try { const result = await updateMindSearchConfig(config); - setConfig({ ...initial, ...result.config, settings: { ...initial.settings, ...result.config.settings } }); + setConfig(mergeConfig(result.config)); setMeta(result); setMessage('已保存到数据库;新会话生效,旧会话不受影响'); } catch (error) { @@ -36,30 +62,149 @@ export function MindSearchPage() { } }; - const toggleProvider = (key: keyof MindSearchConfig['providers']) => - setConfig((current) => ({ ...current, providers: { ...current.providers, [key]: !current.providers[key] } })); const updateSetting = (key: keyof MindSearchConfig['settings'], value: string | number) => setConfig((current) => ({ ...current, settings: { ...current.settings, [key]: value } })); + const updateService = (id: string, patch: Partial) => + setConfig((current) => ({ + ...current, + services: current.services.map((service) => service.id === id ? { ...service, ...patch } : service), + })); + const addService = () => { + const existingIds = new Set(config.services.map((service) => service.id)); + let suffix = config.services.length + 1; + while (existingIds.has(`research-${suffix}`)) suffix += 1; + const id = `research-${suffix}`; + setConfig((current) => ({ + ...current, + services: [...current.services, { + id, + name: 'Research Engine', + kind: 'orchestrator', + adapter: 'research-http', + capabilities: ['search.web', 'research.plan', 'research.execute'], + endpoint: 'http://127.0.0.1:20100', + healthPath: '/health', + enabled: false, + timeoutMs: 30000, + priority: 100, + }], + })); + }; + const removeService = (id: string) => + setConfig((current) => ({ + ...current, + services: current.services.filter((service) => service.id !== id), + routes: Object.fromEntries( + Object.entries(current.routes).map(([key, value]) => [key, value === id ? '' : value]), + ) as unknown as MindSearchRoutes, + })); + const runServiceTest = async (id: string) => { + setMessage(`测试 ${id} 中…`); + try { + const result = await testMindSearchService(id); + setMessage(`${result.ok ? '通过' : '失败'}:${id};${result.message};${result.latencyMs}ms${result.resultCount == null ? '' : `;${result.resultCount} 条结果`}`); + } catch (error) { + setMessage(error instanceof Error ? error.message : '服务测试失败'); + } + }; return (
-

MindSearch

可插拔外部搜索增强。关闭时完全保持原有搜索、记忆和上下文链路。

+
+
+

MindSearch

+

可插拔搜索与研究服务。关闭时保持原有搜索、记忆和上下文链路。

+
+

总开关与模式

- - -

Provider

-
可用 Provider - - - -
+
+ + +
+ +

服务注册表

+

服务独立部署;后台只管理连接、路由和健康检查,不直接操作 Docker 或生产进程。测试使用已保存配置。

+
+ {config.services.map((service) => ( +
+ {service.name}({service.id}) + +
+ + + + + + + + +
+
+ + {!['searxng', 'github', 'reader'].includes(service.id) && } +
+
+ ))} +
+ + +

能力路由

+
+ {(Object.keys(routeLabels) as Array).map((route) => ( + + ))} +
+

运行参数

- - - - - +
+ + + +
+ {message &&

{message}

}

配置来源:{meta.source === 'admin' ? '数据库' : '环境变量默认值'};最后更新:{meta.updatedAt ? new Date(meta.updatedAt).toLocaleString() : '尚未保存'}{meta.updatedBy ? `;操作人:${meta.updatedBy}` : ''}

diff --git a/src/api/client.ts b/src/api/client.ts index 04fe530..f77a8ef 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -49,6 +49,7 @@ import type { WechatMessage, WechatWebNotification, MindSearchConfig, + MindSearchServiceTestResult, } from '../types'; export class ApiError extends Error { @@ -669,6 +670,10 @@ export async function updateMindSearchConfig(config: Partial): return portalFetch('/admin-api/mindsearch/config', { method: 'PATCH', body: JSON.stringify(config) }); } +export async function testMindSearchService(serviceId: string): Promise { + return portalFetch(`/admin-api/mindsearch/services/${encodeURIComponent(serviceId)}/test`, { method: 'POST' }); +} + // ── Policies ────────────────────────────────────────── export async function listPolicyCatalog(): Promise { diff --git a/src/index.css b/src/index.css index 8b09409..f193005 100644 --- a/src/index.css +++ b/src/index.css @@ -577,6 +577,44 @@ body, justify-self: stretch; } +.search-service-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); + gap: 14px; + margin: 14px 0; +} + +.search-service-card { + min-width: 0; + padding: 14px; + border: 1px solid var(--color-border-input); + border-radius: var(--radius-md); +} + +.search-service-card legend { + padding: 0 6px; + font-weight: 600; +} + +.search-service-toggle { + display: flex !important; + align-items: center; + gap: 8px; + margin-bottom: 12px; + color: var(--color-text-primary) !important; +} + +.search-service-toggle input { + width: auto; + margin: 0; +} + +.search-service-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + .form-span-all { grid-column: 1 / -1; } diff --git a/src/types.ts b/src/types.ts index b8801e1..0da7d6b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -625,4 +625,37 @@ export type MindSearchConfig = { mode: 'off' | 'shadow' | 'assist'; providers: { searxng: boolean; github: boolean; reader: boolean }; settings: { searxngEndpoint: string; maxResults: number; timeoutMs: number; readerMaxChars: number }; + services: MindSearchService[]; + routes: MindSearchRoutes; +}; + +export type MindSearchService = { + id: string; + name: string; + kind: 'provider' | 'reader' | 'orchestrator'; + adapter: 'searxng' | 'github' | 'reader' | 'research-http'; + capabilities: string[]; + endpoint: string; + healthPath: string; + enabled: boolean; + timeoutMs: number; + priority: number; +}; + +export type MindSearchRoutes = { + web: string; + news: string; + code: string; + read: string; + research: string; +}; + +export type MindSearchServiceTestResult = { + ok: boolean; + serviceId: string; + adapter: MindSearchService['adapter']; + status: number | null; + latencyMs: number; + resultCount?: number | null; + message: string; }; -- 2.52.0 From ce8247743e424e0faf509f9398675761515444e6 Mon Sep 17 00:00:00 2001 From: john Date: Thu, 23 Jul 2026 22:27:11 +0800 Subject: [PATCH 2/5] feat: register local deep search service --- src/admin/pages/MindSearchPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/admin/pages/MindSearchPage.tsx b/src/admin/pages/MindSearchPage.tsx index 6cb219c..4b3df2c 100644 --- a/src/admin/pages/MindSearchPage.tsx +++ b/src/admin/pages/MindSearchPage.tsx @@ -6,6 +6,7 @@ const builtInServices: MindSearchService[] = [ { id: 'searxng', name: 'SearXNG', kind: 'provider', adapter: 'searxng', capabilities: ['search.web', 'search.news'], endpoint: '', healthPath: '/config', enabled: false, timeoutMs: 8000, priority: 100 }, { id: 'github', name: 'GitHub Code', kind: 'provider', adapter: 'github', capabilities: ['search.code'], endpoint: 'https://api.github.com/search/code', healthPath: '', enabled: false, timeoutMs: 8000, priority: 90 }, { id: 'reader', name: 'Safe Reader', kind: 'reader', adapter: 'reader', capabilities: ['search.read'], endpoint: '', healthPath: '', enabled: false, timeoutMs: 8000, priority: 80 }, + { id: 'deep-search', name: 'TKMind Deep Search', kind: 'orchestrator', adapter: 'research-http', capabilities: ['search.web', 'research.plan', 'research.execute'], endpoint: 'http://127.0.0.1:20100', healthPath: '/health', enabled: false, timeoutMs: 120000, priority: 100 }, ]; const initial: MindSearchConfig = { @@ -14,7 +15,7 @@ const initial: MindSearchConfig = { providers: { searxng: false, github: false, reader: false }, settings: { searxngEndpoint: '', maxResults: 10, timeoutMs: 8000, readerMaxChars: 12000 }, services: builtInServices, - routes: { web: 'searxng', news: 'searxng', code: 'github', read: 'reader', research: '' }, + routes: { web: 'searxng', news: 'searxng', code: 'github', read: 'reader', research: 'deep-search' }, }; const routeLabels: Record = { -- 2.52.0 From 6977f9fc24a805b7b50a72eceff87788292e73bb Mon Sep 17 00:00:00 2001 From: john Date: Thu, 23 Jul 2026 22:46:14 +0800 Subject: [PATCH 3/5] feat: select deep search llm in admin --- src/admin/pages/MindSearchPage.tsx | 90 +++++++++++++++++++++++++++--- src/index.css | 7 +++ src/types.ts | 2 + 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/src/admin/pages/MindSearchPage.tsx b/src/admin/pages/MindSearchPage.tsx index 4b3df2c..8fe356a 100644 --- a/src/admin/pages/MindSearchPage.tsx +++ b/src/admin/pages/MindSearchPage.tsx @@ -1,12 +1,23 @@ import { useEffect, useState } from 'react'; -import { getMindSearchConfig, testMindSearchService, updateMindSearchConfig } from '../../api/client'; -import type { MindSearchConfig, MindSearchRoutes, MindSearchService } from '../../types'; +import { + getMindSearchConfig, + listLlmProviderKeys, + testLlmProviderKey, + testMindSearchService, + updateMindSearchConfig, +} from '../../api/client'; +import type { + LlmProviderKeyRow, + MindSearchConfig, + MindSearchRoutes, + MindSearchService, +} from '../../types'; const builtInServices: MindSearchService[] = [ { id: 'searxng', name: 'SearXNG', kind: 'provider', adapter: 'searxng', capabilities: ['search.web', 'search.news'], endpoint: '', healthPath: '/config', enabled: false, timeoutMs: 8000, priority: 100 }, { id: 'github', name: 'GitHub Code', kind: 'provider', adapter: 'github', capabilities: ['search.code'], endpoint: 'https://api.github.com/search/code', healthPath: '', enabled: false, timeoutMs: 8000, priority: 90 }, { id: 'reader', name: 'Safe Reader', kind: 'reader', adapter: 'reader', capabilities: ['search.read'], endpoint: '', healthPath: '', enabled: false, timeoutMs: 8000, priority: 80 }, - { id: 'deep-search', name: 'TKMind Deep Search', kind: 'orchestrator', adapter: 'research-http', capabilities: ['search.web', 'research.plan', 'research.execute'], endpoint: 'http://127.0.0.1:20100', healthPath: '/health', enabled: false, timeoutMs: 120000, priority: 100 }, + { id: 'deep-search', name: 'TKMind Deep Search', kind: 'orchestrator', adapter: 'research-http', capabilities: ['search.web', 'research.plan', 'research.execute'], endpoint: 'http://127.0.0.1:20100', healthPath: '/health', enabled: false, timeoutMs: 120000, priority: 100, llmProviderKeyId: '', llmModel: '' }, ]; const initial: MindSearchConfig = { @@ -38,13 +49,18 @@ function mergeConfig(config: MindSearchConfig): MindSearchConfig { export function MindSearchPage() { const [config, setConfig] = useState(initial); + const [llmKeys, setLlmKeys] = useState([]); const [meta, setMeta] = useState<{ source?: string; updatedAt?: number | null; updatedBy?: string | null }>({}); const [message, setMessage] = useState('加载配置中…'); useEffect(() => { - getMindSearchConfig() - .then((result) => { + Promise.all([ + getMindSearchConfig(), + listLlmProviderKeys().catch(() => [] as LlmProviderKeyRow[]), + ]) + .then(([result, keys]) => { setConfig(mergeConfig(result.config)); + setLlmKeys(keys.filter((key) => key.status === 'active')); setMeta(result); setMessage(''); }) @@ -88,6 +104,8 @@ export function MindSearchPage() { enabled: false, timeoutMs: 30000, priority: 100, + llmProviderKeyId: '', + llmModel: '', }], })); }; @@ -108,6 +126,21 @@ export function MindSearchPage() { setMessage(error instanceof Error ? error.message : '服务测试失败'); } }; + const runLlmTest = async (service: MindSearchService) => { + if (!service.llmProviderKeyId || !service.llmModel) { + setMessage('请先选择 Deep Search 的 LLM 配置和模型'); + return; + } + setMessage(`测试 ${service.llmModel} 中…`); + try { + const result = await testLlmProviderKey(service.llmProviderKeyId, service.llmModel); + setMessage(result.ok + ? `LLM 通过:${result.model ?? service.llmModel}${result.latencyMs == null ? '' : `;${result.latencyMs}ms`}` + : `LLM 失败:${result.message ?? '模型连接失败'}`); + } catch (error) { + setMessage(error instanceof Error ? error.message : 'LLM 测试失败'); + } + }; return (
@@ -145,7 +178,9 @@ export function MindSearchPage() {

服务注册表

服务独立部署;后台只管理连接、路由和健康检查,不直接操作 Docker 或生产进程。测试使用已保存配置。

- {config.services.map((service) => ( + {config.services.map((service) => { + const selectedLlmKey = llmKeys.find((key) => key.id === service.llmProviderKeyId); + return (
{service.name}({service.id}) @@ -173,13 +208,52 @@ export function MindSearchPage() { + {service.adapter === 'research-http' && <> + + +

密钥复用“统一模型中心”,不会保存到 MindSearch 配置或下发给 Deep Search 服务。

+ }
- {!['searxng', 'github', 'reader'].includes(service.id) && } + {service.adapter === 'research-http' && service.llmProviderKeyId && service.llmModel + && } + {!['searxng', 'github', 'reader', 'deep-search'].includes(service.id) && }
- ))} + ); + })} diff --git a/src/index.css b/src/index.css index f193005..fc2614d 100644 --- a/src/index.css +++ b/src/index.css @@ -615,6 +615,13 @@ body, margin-top: 12px; } +.search-llm-hint { + margin: -2px 0 0; + color: var(--color-text-muted); + font-size: 12px; + line-height: 1.55; +} + .form-span-all { grid-column: 1 / -1; } diff --git a/src/types.ts b/src/types.ts index 0da7d6b..5faacbf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -640,6 +640,8 @@ export type MindSearchService = { enabled: boolean; timeoutMs: number; priority: number; + llmProviderKeyId?: string; + llmModel?: string; }; export type MindSearchRoutes = { -- 2.52.0 From 1b5bbdf467b73e877431e2f27510d104a3184bf2 Mon Sep 17 00:00:00 2001 From: john Date: Thu, 23 Jul 2026 22:51:44 +0800 Subject: [PATCH 4/5] fix: expose new built-in search services --- src/admin/pages/MindSearchPage.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/admin/pages/MindSearchPage.tsx b/src/admin/pages/MindSearchPage.tsx index 8fe356a..1297449 100644 --- a/src/admin/pages/MindSearchPage.tsx +++ b/src/admin/pages/MindSearchPage.tsx @@ -38,11 +38,20 @@ const routeLabels: Record = { }; function mergeConfig(config: MindSearchConfig): MindSearchConfig { + const configuredServices = config.services ?? []; + const configuredServiceIds = new Set(configuredServices.map((service) => service.id)); + const services = configuredServices.length + ? [ + ...configuredServices, + ...builtInServices.filter((service) => !configuredServiceIds.has(service.id)), + ] + : builtInServices; + return { ...initial, ...config, settings: { ...initial.settings, ...config.settings }, - services: config.services?.length ? config.services : builtInServices, + services, routes: { ...initial.routes, ...config.routes }, }; } -- 2.52.0 From 4d7bcbe9482883b655181ae63eebe3cef8a19732 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 24 Jul 2026 09:53:06 +0800 Subject: [PATCH 5/5] fix: harden admin production source gates --- .adm-api.pid | 1 - .adm-preview.pid | 1 - .gitignore | 1 + scripts/release-prod.sh | 33 ++++++++++++++++++++++++++++++--- 4 files changed, 31 insertions(+), 5 deletions(-) delete mode 100644 .adm-api.pid delete mode 100644 .adm-preview.pid diff --git a/.adm-api.pid b/.adm-api.pid deleted file mode 100644 index e7579bb..0000000 --- a/.adm-api.pid +++ /dev/null @@ -1 +0,0 @@ -26747 diff --git a/.adm-preview.pid b/.adm-preview.pid deleted file mode 100644 index 1b2a2f9..0000000 --- a/.adm-preview.pid +++ /dev/null @@ -1 +0,0 @@ -25645 diff --git a/.gitignore b/.gitignore index 5608d60..d8fda2c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ dist-ssr/ memind-lib/ .vite/ *.log +*.pid logs/ diff --git a/scripts/release-prod.sh b/scripts/release-prod.sh index ca0298e..0db1fe8 100755 --- a/scripts/release-prod.sh +++ b/scripts/release-prod.sh @@ -88,12 +88,24 @@ need_cmd shasum need_cmd node if [[ -x "${ROOT}/scripts/check-release-ready.sh" ]]; then - bash "${ROOT}/scripts/check-release-ready.sh" + ALLOW_MAIN_RELEASE=1 bash "${ROOT}/scripts/check-release-ready.sh" else echo "缺少发布前闸门: ${ROOT}/scripts/check-release-ready.sh" >&2 exit 1 fi +ROOT_GIT_BRANCH="$(git -C "${ROOT}" branch --show-current)" +ROOT_GIT_HEAD="$(git -C "${ROOT}" rev-parse HEAD)" +ROOT_ORIGIN_HEAD="$(git -C "${ROOT}" rev-parse origin/main)" +[[ "${ROOT_GIT_BRANCH}" == "main" ]] || { + echo "memindadm 生产发布必须从 main 执行,当前分支: ${ROOT_GIT_BRANCH:-detached}" >&2 + exit 1 +} +[[ "${ROOT_GIT_HEAD}" == "${ROOT_ORIGIN_HEAD}" ]] || { + echo "memindadm main 必须与 origin/main 完全一致" >&2 + exit 1 +} + [[ -f "${IGNORE_FILE}" ]] || { echo "缺少忽略文件: ${IGNORE_FILE}" >&2 exit 1 @@ -127,8 +139,23 @@ say "验证共享模块图像运行依赖" exit 1 } -MEMIND_GIT_HEAD="$(git -C "${MEMIND_SRC}" rev-parse HEAD 2>/dev/null || echo unknown)" -MEMIND_GIT_BRANCH="$(git -C "${MEMIND_SRC}" branch --show-current 2>/dev/null || echo detached)" +git -C "${MEMIND_SRC}" fetch origin main --quiet +MEMIND_GIT_HEAD="$(git -C "${MEMIND_SRC}" rev-parse HEAD)" +MEMIND_ORIGIN_HEAD="$(git -C "${MEMIND_SRC}" rev-parse origin/main)" +MEMIND_GIT_BRANCH="$(git -C "${MEMIND_SRC}" branch --show-current)" +[[ "${MEMIND_GIT_BRANCH}" == "main" ]] || { + echo "Memind 共享模块必须从 main 打包,当前分支: ${MEMIND_GIT_BRANCH:-detached}" >&2 + exit 1 +} +[[ "${MEMIND_GIT_HEAD}" == "${MEMIND_ORIGIN_HEAD}" ]] || { + echo "Memind main 必须与 origin/main 完全一致" >&2 + exit 1 +} +[[ -z "$(git -C "${MEMIND_SRC}" status --porcelain=v1 --untracked-files=all)" ]] || { + echo "Memind 共享模块工作区必须干净" >&2 + git -C "${MEMIND_SRC}" status --short --untracked-files=all >&2 + exit 1 +} say "打包 Memind 共享模块到 memind-lib" rm -rf "${MEMIND_LIB}" -- 2.52.0