feat: integrate AI candidate review and automatic publication

This commit is contained in:
2026-10-09 22:33:38 +08:00
parent 04dbeb2ac6
commit 86f1e7a897
46 changed files with 2460 additions and 2362 deletions
+5 -3
View File
@@ -1,10 +1,11 @@
import {useEffect,useState} from "react"
import {ApiError} from "@/lib/auth"
import {request,ApiError} from "@/lib/auth"
import {getProviders,saveProvider,type ProviderConfig,type ProviderKey} from "@/lib/providers"
import {Button} from "@/components/ui/button"
import {Input} from "@/components/ui/input"
import {Badge} from "@/components/ui/badge"
import {Card,CardContent,CardHeader,CardTitle,CardDescription} from "@/components/ui/card"
import {AIReviewPanel} from "./AIReviewPanel"
const titles:Record<ProviderKey,string>={model_primary:"主模型",model_backup:"备用模型",model_review:"审核模型",market_primary:"行情源"}
export function ProvidersPanel({kind,onSessionExpired}:{kind:"model"|"market";onSessionExpired:()=>void}){
const [configs,setConfigs]=useState<ProviderConfig[]>([])
@@ -16,7 +17,8 @@ export function ProvidersPanel({kind,onSessionExpired}:{kind:"model"|"market";on
const keys:ProviderKey[]=kind==="model"?["model_primary","model_review","model_backup"]:["market_primary"]
if(loading)return <p className="text-sm text-muted-foreground">正在读取配置…</p>
if(error)return <div role="alert" className="space-y-4"><p className="text-sm text-destructive">{error}</p><Button variant="outline" onClick={()=>setGeneration(v=>v+1)}>重新读取</Button></div>
return <div className="space-y-6">{notice&&<p role="status" className="text-sm">{notice}</p>}<p className="text-sm leading-7 text-muted-foreground">供应商信息由管理员填写,初始留空。启用表示允许后续业务使用该配置,不代表连接已验证。{kind==="model"&&"模型接口按 OpenAI 兼容协议配置。"}</p>{keys.map(key=><ProviderForm key={`${key}:${configs.find(c=>c.config_key===key)?.version??0}`} configKey={key} config={configs.find(c=>c.config_key===key)} onSessionExpired={onSessionExpired} onSaved={saved=>{setConfigs(prev=>[...prev.filter(c=>c.config_key!==key),saved]);setNotice(`${titles[key]}配置已保存。`)}} onReload={()=>setGeneration(v=>v+1)}/>)}</div>
async function copyReview(){setError("");try{const saved=await request<ProviderConfig>("/api/admin/v1/providers/model_review/copy-primary",{method:"POST",body:JSON.stringify({expected_version:configs.find(c=>c.config_key==="model_review")?.version??0})});setConfigs(prev=>[...prev.filter(c=>c.config_key!=="model_review"),saved]);setNotice("审核配置已复用主模型,仍执行独立复核调用。")}catch(e){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"复制失败")}}
return <div className="space-y-6">{kind==="model"&&<AIReviewPanel onSessionExpired={onSessionExpired}/>} {notice&&<p role="status" className="text-sm">{notice}</p>}<p className="text-sm leading-7 text-muted-foreground">供应商信息由管理员填写,初始留空。启用表示允许后续业务使用该配置,不代表连接已验证。{kind==="model"&&"模型接口按 OpenAI 兼容协议配置。"}</p>{kind==="model"&&<Button variant="outline" onClick={()=>void copyReview()}>审核模型复用主模型配置</Button>}{keys.map(key=><ProviderForm key={`${key}:${configs.find(c=>c.config_key===key)?.version??0}`} configKey={key} config={configs.find(c=>c.config_key===key)} onSessionExpired={onSessionExpired} onSaved={saved=>{setConfigs(prev=>[...prev.filter(c=>c.config_key!==key),saved]);setNotice(`${titles[key]}配置已保存。`)}} onReload={()=>setGeneration(v=>v+1)}/>)}</div>
}
function ProviderForm({configKey,config,onSaved,onReload,onSessionExpired}:{configKey:ProviderKey;config?:ProviderConfig;onSaved:(c:ProviderConfig)=>void;onReload:()=>void;onSessionExpired:()=>void}){
const [quoteURL,setQuoteURL]=useState(config?.quote_base_url??"")
@@ -36,5 +38,5 @@ function ProviderForm({configKey,config,onSaved,onReload,onSessionExpired}:{conf
const [error,setError]=useState("")
async function submit(event:React.FormEvent){event.preventDefault();setSaving(true);setError("");try{const result=await saveProvider(configKey,{quote_base_url:quoteURL,market_adapter:marketAdapter,history_base_url:historyURL,name,base_url:baseURL,model_name:modelName,wire_api:wireAPI,model_catalog_url:catalogURL,disable_response_storage:disableStorage,requires_openai_auth:requiresAuth,enabled,timeout_seconds:timeout,clear_api_key:clearKey,expected_version:config?.version??0,...(apiKey?{api_key:apiKey}:{})});setAPIKey("");setClearKey(false);onSaved(result)}catch(e){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof ApiError&&e.status===409?"配置已被其他管理员修改,请重新读取。":e instanceof Error?e.message:"保存失败")}finally{setSaving(false)}}
const fieldClass="space-y-2 text-sm"
return <Card><CardHeader><div className="flex items-center justify-between"><CardTitle>{titles[configKey]}</CardTitle><Badge variant="outline">{config?.enabled?"已启用 · 未验证":"未启用"}</Badge></div><CardDescription>{config?`版本 ${config.version} · 更新于 ${new Date(config.updated_at).toLocaleString("zh-CN")}`:"尚未配置"}</CardDescription></CardHeader><CardContent><form onSubmit={submit} className="space-y-5"><fieldset disabled={saving} className="grid gap-5 sm:grid-cols-2">{configKey==="market_primary"&&<><label className={fieldClass}>行情适配器<select className="flex h-10 w-full rounded-md border bg-background px-3 text-sm" value={marketAdapter} onChange={e=>setMarketAdapter(e.target.value)}><option value="">尚未选择</option><option value="sina_public">新浪公开数据(开发研究)</option><option value="eastmoney_public">东方财富公开数据(开发研究)</option></select></label><label className={fieldClass}>报价地址(HTTPS)<Input type="url" value={quoteURL} onChange={e=>setQuoteURL(e.target.value)}/></label><label className={fieldClass}>历史行情地址(HTTPS)<Input type="url" value={historyURL} onChange={e=>setHistoryURL(e.target.value)}/></label></>}<label className={fieldClass}>供应商名称<Input value={name} onChange={e=>setName(e.target.value)} maxLength={80}/></label><label className={fieldClass}>接口地址(HTTPS)<Input value={baseURL} onChange={e=>setBaseURL(e.target.value)} type="url" maxLength={2048}/></label>{configKey!=="market_primary"&&<label className={fieldClass}>模型名称<Input value={modelName} onChange={e=>setModelName(e.target.value)} maxLength={120}/></label>}{configKey!=="market_primary"&&<><label className={fieldClass}>API 协议<select aria-label="API 协议" value={wireAPI} onChange={e=>setWireAPI(e.target.value as "responses"|"chat_completions")} className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-sm"><option value="responses">Responses</option><option value="chat_completions">Chat Completions</option></select></label><label className={`${fieldClass} sm:col-span-2`}>模型目录地址(同源 HTTPS,可留空)<Input value={catalogURL} onChange={e=>setCatalogURL(e.target.value)} type="url" maxLength={2048}/></label><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={disableStorage} onChange={e=>setDisableStorage(e.target.checked)}/>禁止供应商存储响应</label><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={requiresAuth} onChange={e=>setRequiresAuth(e.target.checked)}/>使用 API 密钥认证</label></>}<label className={fieldClass}>请求超时(秒)<Input value={timeout} onChange={e=>setTimeoutValue(Number(e.target.value))} type="number" min={1} max={30} required/></label><label className={`${fieldClass} sm:col-span-2`}>API 密钥<Input type="password" autoComplete="new-password" value={apiKey} onChange={e=>{setAPIKey(e.target.value);setClearKey(false)}} maxLength={4096} placeholder={config?.has_api_key?"已保存密钥;留空保留原值":"尚未填写"}/><span className="block text-xs leading-6 text-muted-foreground">密钥加密保存。更换接口地址时请重新填写或清除已有密钥。</span></label><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={enabled} onChange={e=>setEnabled(e.target.checked)}/>启用此配置</label>{config?.has_api_key&&<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={clearKey} onChange={e=>{setClearKey(e.target.checked);if(e.target.checked)setAPIKey("")}}/>清除已保存密钥</label>}</fieldset>{error&&<div role="alert" className="space-y-3 text-sm text-destructive"><p>{error}</p>{error.includes("重新读取")&&<Button type="button" variant="outline" onClick={onReload}>重新读取</Button>}</div>}<Button type="submit" disabled={saving}>{saving?"正在保存…":"保存配置"}</Button></form></CardContent></Card>
return <Card><CardHeader><div className="flex items-center justify-between"><CardTitle>{titles[configKey]}</CardTitle><Badge variant="outline">{config?.enabled?"已启用 · 未验证":"未启用"}</Badge></div><CardDescription>{config?`版本 ${config.version} · 更新于 ${new Date(config.updated_at).toLocaleString("zh-CN")}`:"尚未配置"}</CardDescription></CardHeader><CardContent><form onSubmit={submit} className="space-y-5"><fieldset disabled={saving} className="grid gap-5 sm:grid-cols-2">{configKey==="market_primary"&&<><label className={fieldClass}>行情适配器<select className="flex h-10 w-full rounded-md border bg-background px-3 text-sm" value={marketAdapter} onChange={e=>setMarketAdapter(e.target.value)}><option value="">尚未选择</option><option value="sina_public">新浪公开数据(开发研究)</option><option value="eastmoney_public">东方财富公开数据(开发研究)</option></select></label><label className={fieldClass}>报价地址(HTTPS)<Input type="url" value={quoteURL} onChange={e=>setQuoteURL(e.target.value)}/></label><label className={fieldClass}>历史行情地址(HTTPS)<Input type="url" value={historyURL} onChange={e=>setHistoryURL(e.target.value)}/></label></>}<label className={fieldClass}>供应商名称<Input value={name} onChange={e=>setName(e.target.value)} maxLength={80}/></label><label className={fieldClass}>接口地址(HTTPS)<Input value={baseURL} onChange={e=>setBaseURL(e.target.value)} type="url" maxLength={2048}/></label>{configKey!=="market_primary"&&<label className={fieldClass}>模型名称<Input value={modelName} onChange={e=>setModelName(e.target.value)} maxLength={120}/></label>}{configKey!=="market_primary"&&<><label className={fieldClass}>API 协议<select aria-label="API 协议" value={wireAPI} onChange={e=>setWireAPI(e.target.value as "responses"|"chat_completions")} className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-sm"><option value="responses">Responses</option><option value="chat_completions">Chat Completions</option></select></label><label className={`${fieldClass} sm:col-span-2`}>模型目录地址(同源 HTTPS,可留空)<Input value={catalogURL} onChange={e=>setCatalogURL(e.target.value)} type="url" maxLength={2048}/></label><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={disableStorage} onChange={e=>setDisableStorage(e.target.checked)}/>禁止供应商存储响应</label><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={requiresAuth} onChange={e=>setRequiresAuth(e.target.checked)}/>使用 API 密钥认证</label></>}<label className={fieldClass}>请求超时(秒)<Input value={timeout} onChange={e=>setTimeoutValue(Number(e.target.value))} type="number" min={1} max={120} required/></label><label className={`${fieldClass} sm:col-span-2`}>API 密钥<Input type="password" autoComplete="new-password" value={apiKey} onChange={e=>{setAPIKey(e.target.value);setClearKey(false)}} maxLength={4096} placeholder={config?.has_api_key?"已保存密钥;留空保留原值":"尚未填写"}/><span className="block text-xs leading-6 text-muted-foreground">密钥加密保存。更换接口地址时请重新填写或清除已有密钥。</span></label><label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={enabled} onChange={e=>setEnabled(e.target.checked)}/>启用此配置</label>{config?.has_api_key&&<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={clearKey} onChange={e=>{setClearKey(e.target.checked);if(e.target.checked)setAPIKey("")}}/>清除已保存密钥</label>}</fieldset>{error&&<div role="alert" className="space-y-3 text-sm text-destructive"><p>{error}</p>{error.includes("重新读取")&&<Button type="button" variant="outline" onClick={onReload}>重新读取</Button>}</div>}<Button type="submit" disabled={saving}>{saving?"正在保存…":"保存配置"}</Button></form></CardContent></Card>
}