@@ -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 < keyof MindSearchRoutes , string > = {
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 < MindSearchService > ) = >
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 (
< section className = "admin-page" >
< div className = "admin-page-header" > < div > < h1 > MindSearch < / h1 > < p > 可 插 拔 外 部 搜 索 增 强 。 关 闭 时 完 全 保 持 原 有 搜 索 、 记 忆 和 上 下 文 链 路 。 < / p > < / div > < / div >
< div className = "admin-page-header" >
< div >
< h1 > MindSearch < / h1 >
< p > 可 插 拔 搜 索 与 研 究 服 务 。 关 闭 时 保 持 原 有 搜 索 、 记 忆 和 上 下 文 链 路 。 < / p >
< / div >
< / div >
< div className = "admin-card" >
< h2 > 总 开 关 与 模 式 < / h2 >
< label > < input type = "checkbox" checked = { config . enabled } onChange = { ( event ) = > setConfig ( { . . . config , enabled : event.target.checked , mode : event.target.checked ? ( config . mode === 'off' ? 'shadow' : config . mode ) : 'off' } ) } / > 启 用 MindSearch < / label >
< label > 运 行 模 式 < select value = { config . mode } onChange = { ( event ) = > setConfig ( { . . . config , mode : event.target.value as MindSearchConfig [ 'mode' ] } ) } > < option value = "off" > 关 闭 < / option > < option value = "shadow" > Shadow ( 只 记 录 , 不 注 入 ) < / option > < option value = "assist" > Assist ( 补 充 工 具 ) < / option > < / select > < / label >
< h2 > Provider < / h2 >
< fieldset > < legend > 可 用 Provider < / legend >
< label > < input type = "checkbox" checked = { config . providers . searxng } onChange = { ( ) = > toggleProvider ( 'searxng' ) } / > SearXNG Web / News < / label >
< label > < input type = "checkbox" checked = { config . providers . github } onChange = { ( ) = > toggleProvider ( 'github' ) } / > GitHub Code ( Token 通 过 环 境 变 量 配 置 , 不 在 页 面 回 显 ) < / label >
< label > < input type = "checkbox" checked = { config . providers . reader } onChange = { ( ) = > toggleProvider ( 'reader' ) } / > Reader ( 阻 止 localhost / 内 网 地 址 ) < / label >
< / fieldset >
< div className = "admin-form" >
< label className = "search-service-toggle" >
< input
type = "checkbox"
checked = { config . enabled }
onChange = { ( event ) = > setConfig ( {
. . . config ,
enabled : event.target.checked ,
mode : event.target.checked ? ( config . mode === 'off' ? 'shadow' : config . mode ) : 'off' ,
} ) }
/ >
< span > 启 用 MindSearch < / span >
< / label >
< label >
< span > 运 行 模 式 < / span >
< select value = { config . mode } onChange = { ( event ) = > setConfig ( { . . . config , mode : event.target.value as MindSearchConfig [ 'mode' ] } ) } >
< option value = "off" > 关 闭 < / option >
< option value = "shadow" > Shadow ( 只 记 录 , 不 注 入 ) < / option >
< option value = "assist" > Assist ( 补 充 工 具 ) < / option >
< / select >
< / label >
< / div >
< h2 > 服 务 注 册 表 < / h2 >
< p > 服 务 独 立 部 署 ; 后 台 只 管 理 连 接 、 路 由 和 健 康 检 查 , 不 直 接 操 作 Docker 或 生 产 进 程 。 测 试 使 用 已 保 存 配 置 。 < / p >
< div className = "search-service-grid" >
{ config . services . map ( ( service ) = > (
< 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 >
< div className = "admin-form" >
< label > < span > 名 称 < / span > < input value = { service . name } onChange = { ( event ) = > updateService ( service . id , { name : event.target.value } ) } / > < / label >
< label >
< span > 类 型 < / span >
< select value = { service . kind } onChange = { ( event ) = > updateService ( service . id , { kind : event.target.value as MindSearchService [ 'kind' ] } ) } >
< option value = "provider" > Provider < / option >
< option value = "reader" > Reader < / option >
< option value = "orchestrator" > Orchestrator < / option >
< / select >
< / label >
< label >
< span > 适 配 器 < / span >
< select value = { service . adapter } onChange = { ( event ) = > updateService ( service . id , { adapter : event.target.value as MindSearchService [ 'adapter' ] } ) } >
< option value = "searxng" > SearXNG < / option >
< option value = "github" > GitHub < / option >
< option value = "reader" > Reader < / option >
< option value = "research-http" > Research HTTP < / option >
< / select >
< / label >
< label > < span > 健 康 检 查 路 径 < / span > < input value = { service . healthPath } placeholder = "/health" onChange = { ( event ) = > updateService ( service . id , { healthPath : event.target.value } ) } / > < / label >
< label className = "form-span-all" > < span > Endpoint < / span > < input value = { service . endpoint } placeholder = "http://127.0.0.1:20100" onChange = { ( event ) = > updateService ( service . id , { endpoint : event.target.value } ) } / > < / 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 = { 1 } max = { 1000 } value = { service . priority } onChange = { ( event ) = > updateService ( service . id , { priority : Number ( event . target . value ) } ) } / > < / label >
< / 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 > }
< / div >
< / fieldset >
) ) }
< / div >
< button className = "send-btn" type = "button" onClick = { addService } > 添 加 Research Service < / button >
< h2 > 能 力 路 由 < / h2 >
< div className = "admin-form" >
{ ( Object . keys ( routeLabels ) as Array < keyof MindSearchRoutes > ) . map ( ( route ) = > (
< label key = { route } >
< span > { routeLabels [ route ] } < / span >
< select
value = { config . routes [ route ] }
onChange = { ( event ) = > setConfig ( ( current ) = > ( { . . . current , routes : { . . . current . routes , [ route ] : event . target . value } } ) ) }
>
< option value = "" > 未 配 置 < / option >
{ config . services . map ( ( service ) = > < option key = { service . id } value = { service . id } > { service . name } ( { service . id } ) < / option > ) }
< / select >
< / label >
) ) }
< / div >
< h2 > 运 行 参 数 < / h2 >
< label > SearXNG 地 址 < input value = { config . settings . searxngEndpoint } placeholder = "http://127.0.0.1:8080/search" onChange = { ( event ) = > updateSetting ( 'searxngEndpoint' , event . target . value ) } / > < / label >
< label > 最 大 结 果 数 < input type = "number" min = { 1 } max = { 20 } value = { config . settings . maxResults } onChange = { ( event ) = > updateSetting ( 'maxResults' , Number ( event . target . value ) ) } / > < / label >
< label > Provider 超 时 ( 毫 秒 ) < input type = "number" min = { 1000 } max = { 30000 } value = { config . settings . timeoutMs } onChange = { ( event ) = > updateSetting ( 'timeoutMs' , Number ( event . target . value ) ) } / > < / label >
< label > Reader 最 大 字 符 数 < input type = "number" min = { 1000 } max = { 50000 } value = { config . settings . readerMaxChars } onChange = { ( event ) = > updateSetting ( 'readerMaxChars' , Number ( event . target . value ) ) } / > < / label >
< button type = "button" onClick = { save } > 保 存 配 置 < / button >
< div className = "admin-form" >
< label > < span > 最 大 结 果 数 < / span > < input type = "number" min = { 1 } max = { 20 } value = { config . settings . maxResults } onChange = { ( event ) = > updateSetting ( 'maxResults' , Number ( event . target . value ) ) } / > < / label >
< label > < span > 默 认 Provider 超 时 ( 毫 秒 ) < / span > < input type = "number" min = { 1000 } max = { 30000 } value = { config . settings . timeoutMs } onChange = { ( event ) = > updateSetting ( 'timeoutMs' , Number ( event . target . value ) ) } / > < / label >
< label > < span > Reader 最 大 字 符 数 < / span > < input type = "number" min = { 1000 } max = { 50000 } value = { config . settings . readerMaxChars } onChange = { ( event ) = > updateSetting ( 'readerMaxChars' , Number ( event . target . value ) ) } / > < / label >
< / div >
< button className = "send-btn" type = "button" onClick = { save } > 保 存 配 置 < / button >
{ message && < p role = "status" > { message } < / p > }
< p > 配 置 来 源 : { meta . source === 'admin' ? '数据库' : '环境变量默认值' } ; 最 后 更 新 : { meta . updatedAt ? new Date ( meta . updatedAt ) . toLocaleString ( ) : '尚未保存' } { meta . updatedBy ? ` ;操作人: ${ meta . updatedBy } ` : '' } < / p >
< / div >