feat: select deep search llm in admin
This commit is contained in:
@@ -1,12 +1,23 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getMindSearchConfig, testMindSearchService, updateMindSearchConfig } from '../../api/client';
|
import {
|
||||||
import type { MindSearchConfig, MindSearchRoutes, MindSearchService } from '../../types';
|
getMindSearchConfig,
|
||||||
|
listLlmProviderKeys,
|
||||||
|
testLlmProviderKey,
|
||||||
|
testMindSearchService,
|
||||||
|
updateMindSearchConfig,
|
||||||
|
} from '../../api/client';
|
||||||
|
import type {
|
||||||
|
LlmProviderKeyRow,
|
||||||
|
MindSearchConfig,
|
||||||
|
MindSearchRoutes,
|
||||||
|
MindSearchService,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
const builtInServices: MindSearchService[] = [
|
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: '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: '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: '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 = {
|
const initial: MindSearchConfig = {
|
||||||
@@ -38,13 +49,18 @@ function mergeConfig(config: MindSearchConfig): MindSearchConfig {
|
|||||||
|
|
||||||
export function MindSearchPage() {
|
export function MindSearchPage() {
|
||||||
const [config, setConfig] = useState(initial);
|
const [config, setConfig] = useState(initial);
|
||||||
|
const [llmKeys, setLlmKeys] = useState<LlmProviderKeyRow[]>([]);
|
||||||
const [meta, setMeta] = useState<{ source?: string; updatedAt?: number | null; updatedBy?: string | null }>({});
|
const [meta, setMeta] = useState<{ source?: string; updatedAt?: number | null; updatedBy?: string | null }>({});
|
||||||
const [message, setMessage] = useState('加载配置中…');
|
const [message, setMessage] = useState('加载配置中…');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getMindSearchConfig()
|
Promise.all([
|
||||||
.then((result) => {
|
getMindSearchConfig(),
|
||||||
|
listLlmProviderKeys().catch(() => [] as LlmProviderKeyRow[]),
|
||||||
|
])
|
||||||
|
.then(([result, keys]) => {
|
||||||
setConfig(mergeConfig(result.config));
|
setConfig(mergeConfig(result.config));
|
||||||
|
setLlmKeys(keys.filter((key) => key.status === 'active'));
|
||||||
setMeta(result);
|
setMeta(result);
|
||||||
setMessage('');
|
setMessage('');
|
||||||
})
|
})
|
||||||
@@ -88,6 +104,8 @@ export function MindSearchPage() {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
timeoutMs: 30000,
|
timeoutMs: 30000,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
|
llmProviderKeyId: '',
|
||||||
|
llmModel: '',
|
||||||
}],
|
}],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -108,6 +126,21 @@ export function MindSearchPage() {
|
|||||||
setMessage(error instanceof Error ? error.message : '服务测试失败');
|
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 (
|
return (
|
||||||
<section className="admin-page">
|
<section className="admin-page">
|
||||||
@@ -145,7 +178,9 @@ export function MindSearchPage() {
|
|||||||
<h2>服务注册表</h2>
|
<h2>服务注册表</h2>
|
||||||
<p>服务独立部署;后台只管理连接、路由和健康检查,不直接操作 Docker 或生产进程。测试使用已保存配置。</p>
|
<p>服务独立部署;后台只管理连接、路由和健康检查,不直接操作 Docker 或生产进程。测试使用已保存配置。</p>
|
||||||
<div className="search-service-grid">
|
<div className="search-service-grid">
|
||||||
{config.services.map((service) => (
|
{config.services.map((service) => {
|
||||||
|
const selectedLlmKey = llmKeys.find((key) => key.id === service.llmProviderKeyId);
|
||||||
|
return (
|
||||||
<fieldset key={service.id} className="search-service-card">
|
<fieldset key={service.id} className="search-service-card">
|
||||||
<legend>{service.name}({service.id})</legend>
|
<legend>{service.name}({service.id})</legend>
|
||||||
<label className="search-service-toggle"><input type="checkbox" checked={service.enabled} onChange={(event) => updateService(service.id, { enabled: event.target.checked })} /><span>启用服务</span></label>
|
<label className="search-service-toggle"><input type="checkbox" checked={service.enabled} onChange={(event) => updateService(service.id, { enabled: event.target.checked })} /><span>启用服务</span></label>
|
||||||
@@ -173,13 +208,52 @@ export function MindSearchPage() {
|
|||||||
<label className="form-span-all"><span>Capabilities</span><input value={service.capabilities.join(', ')} onChange={(event) => updateService(service.id, { capabilities: event.target.value.split(',').map((item) => item.trim()).filter(Boolean) })} /></label>
|
<label className="form-span-all"><span>Capabilities</span><input value={service.capabilities.join(', ')} onChange={(event) => updateService(service.id, { capabilities: event.target.value.split(',').map((item) => item.trim()).filter(Boolean) })} /></label>
|
||||||
<label><span>超时(毫秒)</span><input type="number" min={1000} max={120000} value={service.timeoutMs} onChange={(event) => updateService(service.id, { timeoutMs: Number(event.target.value) })} /></label>
|
<label><span>超时(毫秒)</span><input type="number" min={1000} max={120000} value={service.timeoutMs} onChange={(event) => updateService(service.id, { timeoutMs: Number(event.target.value) })} /></label>
|
||||||
<label><span>优先级</span><input type="number" min={1} max={1000} value={service.priority} onChange={(event) => updateService(service.id, { priority: Number(event.target.value) })} /></label>
|
<label><span>优先级</span><input type="number" min={1} max={1000} value={service.priority} onChange={(event) => updateService(service.id, { priority: Number(event.target.value) })} /></label>
|
||||||
|
{service.adapter === 'research-http' && <>
|
||||||
|
<label className="form-span-all">
|
||||||
|
<span>规划与报告 LLM(可选)</span>
|
||||||
|
<select
|
||||||
|
value={service.llmProviderKeyId ?? ''}
|
||||||
|
onChange={(event) => {
|
||||||
|
const key = llmKeys.find((item) => item.id === event.target.value);
|
||||||
|
updateService(service.id, {
|
||||||
|
llmProviderKeyId: event.target.value,
|
||||||
|
llmModel: key?.defaultModel ?? '',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">不使用 LLM(确定性降级)</option>
|
||||||
|
{service.llmProviderKeyId && !selectedLlmKey
|
||||||
|
&& <option value={service.llmProviderKeyId}>原 LLM 配置已停用或删除</option>}
|
||||||
|
{llmKeys.map((key) => (
|
||||||
|
<option key={key.id} value={key.id}>{key.name} · {key.providerLabel}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="form-span-all">
|
||||||
|
<span>模型</span>
|
||||||
|
<select
|
||||||
|
value={service.llmModel ?? ''}
|
||||||
|
disabled={!selectedLlmKey}
|
||||||
|
onChange={(event) => updateService(service.id, { llmModel: event.target.value })}
|
||||||
|
>
|
||||||
|
{!selectedLlmKey && <option value="">请先选择 LLM 配置</option>}
|
||||||
|
{selectedLlmKey && service.llmModel && !selectedLlmKey.models.includes(service.llmModel)
|
||||||
|
&& <option value={service.llmModel}>原模型已不可用:{service.llmModel}</option>}
|
||||||
|
{selectedLlmKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<p className="search-llm-hint form-span-all">密钥复用“统一模型中心”,不会保存到 MindSearch 配置或下发给 Deep Search 服务。</p>
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
<div className="search-service-actions">
|
<div className="search-service-actions">
|
||||||
<button className="send-btn" type="button" onClick={() => runServiceTest(service.id)}>测试已保存配置</button>
|
<button className="send-btn" type="button" onClick={() => runServiceTest(service.id)}>测试已保存配置</button>
|
||||||
{!['searxng', 'github', 'reader'].includes(service.id) && <button type="button" onClick={() => removeService(service.id)}>移除</button>}
|
{service.adapter === 'research-http' && service.llmProviderKeyId && service.llmModel
|
||||||
|
&& <button type="button" onClick={() => runLlmTest(service)}>测试 LLM</button>}
|
||||||
|
{!['searxng', 'github', 'reader', 'deep-search'].includes(service.id) && <button type="button" onClick={() => removeService(service.id)}>移除</button>}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<button className="send-btn" type="button" onClick={addService}>添加 Research Service</button>
|
<button className="send-btn" type="button" onClick={addService}>添加 Research Service</button>
|
||||||
|
|
||||||
|
|||||||
@@ -615,6 +615,13 @@ body,
|
|||||||
margin-top: 12px;
|
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 {
|
.form-span-all {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -640,6 +640,8 @@ export type MindSearchService = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
llmProviderKeyId?: string;
|
||||||
|
llmModel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MindSearchRoutes = {
|
export type MindSearchRoutes = {
|
||||||
|
|||||||
Reference in New Issue
Block a user