@@ -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 < LlmProviderKeyRow [ ] > ( [ ] ) ;
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 (
< section className = "admin-page" >
@@ -145,7 +178,9 @@ export function MindSearchPage() {
< h2 > 服 务 注 册 表 < / h2 >
< p > 服 务 独 立 部 署 ; 后 台 只 管 理 连 接 、 路 由 和 健 康 检 查 , 不 直 接 操 作 Docker 或 生 产 进 程 。 测 试 使 用 已 保 存 配 置 。 < / p >
< 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" >
< 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 >
@@ -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 > < 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 >
{ 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 className = "search-service-actions" >
< 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 >
< / fieldset >
) ) }
) ;
} ) }
< / div >
< button className = "send-btn" type = "button" onClick = { addService } > 添 加 Research Service < / button >