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; };