feat(v2): 完善配置中心与可替换轨迹质检并联动预审依据

This commit is contained in:
2026-10-01 17:13:02 +08:00
parent 1adbe16cb3
commit e604d177cb
42 changed files with 1285 additions and 83 deletions
+6 -4
View File
@@ -43,20 +43,22 @@ export default function AgentNav({active,project,identity:provided}:{active:stri
const identity=provided||loaded;
const role=identity?.role||roleHint;
const canConfigure=identity?.preview||role==="admin";
const canUsePool=role==="manager";
const canUsePool=role==="admin";
return <nav className="xb-agent-nav" aria-label="兴邦对公业务导航">
<a href={projectLink("/",project)} className="xb-brand" title="兴邦金租智审工作台">
<span className="xb-brand-mark" aria-hidden="true">兴</span>
<span><b>兴邦金租</b><small>智审工作台</small></span>
</a>
<span className="xb-nav-group">业务工作区</span>
<span className="xb-nav-group">{canUsePool?"能力配置与治理":"业务工作区"}</span>
<a href={projectLink("/prescreen-workbench",project)} className={active==="prescreen"?"selected":""}><ShieldCheck size={16}/>AI 预审</a>
<a href={projectLink("/experience-center",project)} className={active==="experience"?"selected":""}><History size={16}/>历史经验加工</a>
{canUsePool&&<a href={projectLink("/experience-center",project)} className={active==="experience"?"selected":""}><History size={16}/>历史经验加工</a>}
<a href={projectLink("/trajectory-center",project)} className={active==="trajectory"?"selected":""}><GitBranch size={16}/>轨迹复盘</a>
<details className="xb-extra-nav" open={["overview","time","risk"].includes(active)}><summary>项目与高级功能</summary>
<a href={projectLink("/xingbang-workbench",project)} className={active==="overview"?"selected":""}><LayoutDashboard size={16}/>项目工作台</a>
<a href={projectLink("/operations-center",project)} className={active==="time"?"selected":""}><BarChart3 size={16}/>业务时效</a>
{canConfigure&&<a href="/data-access" className={active==="risk"?"selected":""}><Settings2 size={16}/>配置治理</a>}
</details>
{canConfigure&&<a href={projectLink("/configuration-center",project)} className={active==="configuration"?"selected":""}><Settings2 size={16}/>配置中心</a>}
<a href="/assistant" className={active==="assistant"?"selected":""}><Bot size={16}/>业务助手</a>
<span className="xb-nav-group">miniOpenClaw · Skills / Rules<br/>受控工具 · 证据留痕</span>
<span className="xb-nav-spacer"/>
@@ -20,7 +20,7 @@ export default function BusinessAssistant(){
const [input,setInput]=useState(""),[busy,setBusy]=useState(false),[error,setError]=useState("");
const [messages,setMessages]=useState<Message[]>([{id:"hello",side:"assistant",text:"你好,我是兴邦对公业务助手。选择项目后,你可以直接告诉我查状态、发起AI预审、生成报告或补充材料。"}]);
const role=identity?.role||"manager",project=projects.find(p=>p.id===projectId);
const quick=useMemo(()=>role==="reviewer"?["查看项目状态","发起独立评审","生成评审报告"]:role==="admin"?["查看项目状态"]:["查看项目状态","发起AI预审","生成预审报告","提交正式评审"],[role]);
const quick=useMemo(()=>role==="reviewer"?["查看项目状态","发起独立评审","生成评审报告"]:role==="admin"?["查看项目状态"]:["查看项目状态","我有几个 Skills?","哪些金融相关 Skills?","发起AI预审","生成预审报告"],[role]);
async function load(){
setError("");
@@ -32,7 +32,7 @@ export default function BusinessAssistant(){
const message=text.trim();if(!message||!projectId||busy)return;
if(message.includes("提交")&&message.includes("评审")&&!window.confirm(`确认将 ${projectId} 的AI预审报告提交正式评审?提交后保留当前版本。`))return;
setMessages(old=>[...old,{id:newRequestId(),side:"user",text:message}]);setInput("");setBusy(true);setError("");
try{const response=await fetch(`${ROOT}/commands`,{method:"POST",headers:{...headers(),"Idempotency-Key":newRequestId()},body:JSON.stringify({project_id:projectId,message})});const body=await response.json();if(!response.ok)throw Error(body.detail||"业务指令执行失败");setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:body.message,url:body.url,objectId:body.object_id}]);await load();}
try{const catalogQuery=/skill|rules?|技能|规则|数据卡|模板/i.test(message);const response=await fetch(`${ROOT}/${catalogQuery?"library/inventory":"commands"}`,{method:"POST",headers:{...headers(),"Idempotency-Key":newRequestId()},body:JSON.stringify({project_id:projectId,message})});const body=await response.json();if(!response.ok)throw Error(body.detail||"业务指令执行失败");setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:body.message,url:body.url,objectId:body.object_id}]);await load();}
catch(e:any){setError(e.message);setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:`未执行:${e.message}`}]);}
finally{setBusy(false)}
}
@@ -44,7 +44,7 @@ export default function BusinessAssistant(){
finally{setBusy(false)}
}
function submit(event:FormEvent){event.preventDefault();send(input)}
const conversation=<div className="ba-chat"><div className="ba-messages">{messages.map(message=><article key={message.id} className={message.side}><div>{message.side==="assistant"&&<Bot size={16}/>}<p>{message.text}</p>{message.side==="user"&&<span className="ba-user-icon" aria-label="本人"><UserRound size={17}/></span>}</div>{message.objectId&&<small>操作凭证 · {message.objectId}</small>}{message.url&&<a href={message.url}>查看项目中的结果<ArrowUpRight size={14}/></a>}</article>)}{busy&&<article className="assistant pending"><div><Bot size={16}/><p>正在执行并生成可追溯记录…</p></div></article>}</div>
const conversation=<div className="ba-chat"><div className="ba-messages">{messages.map(message=><article key={message.id} className={message.side}><div>{message.side==="assistant"&&<Bot size={16}/>}<p style={{whiteSpace:"pre-line"}}>{message.text}</p>{message.side==="user"&&<span className="ba-user-icon" aria-label="本人"><UserRound size={17}/></span>}</div>{message.objectId&&<small>操作凭证 · {message.objectId}</small>}{message.url&&<a href={message.url}>查看项目中的结果<ArrowUpRight size={14}/></a>}</article>)}{busy&&<article className="assistant pending"><div><Bot size={16}/><p>正在执行并生成可追溯记录…</p></div></article>}</div>
{error&&<div role="alert" className="ba-error">{error}</div>}
<form onSubmit={submit}><label className="ba-attach" title="补充项目材料"><Paperclip size={18}/><input aria-label="补充项目材料" type="file" accept=".txt,.csv,.docx,.xlsx,.pdf,.png,.jpg,.jpeg" onChange={attach}/></label><input aria-label="业务指令" value={input} onChange={e=>setInput(e.target.value)} placeholder={`例如:帮我做一次 ${projectId||"当前项目"} 风险研判`}/><button aria-label="发送业务指令" disabled={busy||!input.trim()||!projectId}><Send size={18}/></button></form><footer><FilePlus2 size={13}/>当前身份只会调用本项目授权的 Skills、规则和业务工具,并保留操作凭证。</footer>
</div>;
@@ -0,0 +1,9 @@
"use client";
import {useEffect,useState} from 'react';
import {Blocks,ShieldCheck,ArrowRight} from 'lucide-react';
import {stageName} from '@/lib/prescreen-run';
export default function EvidenceInfluence({projectId,runId,request,scenarioStages}:any){
const [data,setData]=useState<any>(null),[error,setError]=useState('');
useEffect(()=>{let live=true;setData(null);setError('');request(`/library/projects/${encodeURIComponent(projectId)}/runs/${encodeURIComponent(runId)}/influences`).then((result:any)=>{if(live)setData(result);}).catch((e:any)=>{if(live)setError(e.message);});return()=>{live=false;};},[projectId,runId]);
return <section className="v2-influences"><div className="v2-section-heading"><div><span className="v2-kicker">TRACEABLE DECISION FACTORS</span><h3>哪些卡片影响这次判断</h3><p>从核查方法、规则命中到证据缺口,逐项对应本次冻结版本。</p></div></div>{error&&<p className="ra-alert error" role="alert">{error}</p>}{!data&&!error&&<p>正在读取冻结卡片依据…</p>}<div className="v2-influence-grid">{data?.items.map((card:any)=>{const overlay=scenarioStages.get(card.id);return <article key={card.id} className={`v2-influence ${card.state==='blocked'?'blocked':''}`}><div className="v2-card-top"><span>{card.kind==='skill'?<Blocks size={14}/>:<ShieldCheck size={14}/>} {card.kind==='skill'?'Skill':'Rule'} · v{card.version}</span><span>{card.effect==='hard'?'硬性约束':'核查依据'}</span></div><h4>{card.name}</h4><div className="v2-stage-change"><b>{stageName(card.stage)}</b>{overlay?.assumed&&<><ArrowRight size={14}/><b className="v2-scenario-stage">{stageName(overlay.scenario_stage)}(假设)</b></>}</div>{card.checks.length?card.checks.map((check:any,index:number)=><div className="v2-check-result" key={index}><span className={`v2-result result-${check.result}`}>{check.result==='pass'?'核查通过':check.result==='issue'?'待处理':check.result==='error'?'未完成':'待核实'}</span><p>{check.title}</p><small>{check.evidence_count} 项引用证据</small>{check.result!=='pass'&&<p className="v2-advice">建议:{check.suggestion}</p>}</div>):<p className="v2-muted">本卡片没有直接核查项;输出用于后续规则或仍需完成核查。</p>}<small>{card.id} · 动态雷达维度</small></article>;})}</div>{data&&<p className="v2-muted">{data.note}</p>}</section>;
}
+8 -7
View File
@@ -8,13 +8,13 @@ export default function KnowledgePools({project,identity,send,refreshKey=0}:any)
const [cards,setCards]=useState<any[]>([]),[fields,setFields]=useState<string[]>([]),[error,setError]=useState(''),[notice,setNotice]=useState('');
const [loading,setLoading]=useState(true),[busy,setBusy]=useState(false),[kind,setKind]=useState('skill'),[opened,setOpened]=useState<any>(null),[expanded,setExpanded]=useState('');
const [pending,setPending]=useState<File[]>([]),[requiredField,setRequiredField]=useState(''),[creating,setCreating]=useState(false),[title,setTitle]=useState(''),[field,setField]=useState(''),[threshold,setThreshold]=useState(''),[instruction,setInstruction]=useState('');
const [revision,setRevision]=useState(''),[samples,setSamples]=useState('');
const [revision,setRevision]=useState(''),[samples,setSamples]=useState(''),[configuration,setConfiguration]=useState(''),[sources,setSources]=useState<any[]>([]),[sourceId,setSourceId]=useState('');
const folder=useRef<HTMLInputElement>(null); const canEdit=identity?.role==='admin';
async function refresh(){try{const data=await send('GET',`/library/cards?project_id=${encodeURIComponent(project.id)}`);setCards(data.items);setFields(data.fields);}catch(e:any){setError(e.message);}finally{setLoading(false);}}
async function refresh(){try{const data=await send('GET',`/library/cards?project_id=${encodeURIComponent(project.id)}`);setCards(data.items);setFields(data.fields);if(canEdit){const result=await send('GET',`/library/configuration/sources?project_id=${encodeURIComponent(project.id)}`);setSources(result.items);setSourceId(old=>old||result.items[0]?.id||'');}}catch(e:any){setError(e.message);}finally{setLoading(false);}}
useEffect(()=>{setExpanded('');setPending([]);refresh();},[project.id,refreshKey]);
async function action(work:()=>Promise<any>){setBusy(true);setError('');setNotice('');try{await work();await refresh();}catch(e:any){setError(e.message);}finally{setBusy(false);}}
async function importFolder(){await action(async()=>{const files=await encodeFiles(pending);await send('POST','/library/skills/import',{project_id:project.id,files,required_field:requiredField||null});setPending([]);setNotice('标准 Skills 文件夹已保存,卡片以人工导入草稿进入池中。');});}
async function create(){await action(async()=>{const content=kind==='skill'?{engine:'required',inputs:[field],code:'manual.'+field,title,instructions:instruction,description:instruction}:{effect:'advisory',on_match:'supplement',description:instruction,conditions:[{code:'manual.'+field,field,operator:'lt',threshold,title,suggestion:instruction}]};
async function create(){await action(async()=>{if(kind==='template'||kind==='data_card'){const content=kind==='template'?{sections:configuration.split('\n').filter(Boolean).map((line,index)=>({id:['overview','findings','judgment'][index]||'section'+index,title:line,dependencies:['*']})),instructions:instruction}:{source_id:sourceId,...JSON.parse(configuration)};await send('POST','/library/configuration/cards',{project_id:project.id,kind,name:title,content});setCreating(false);setNotice('人工配置已保存为草稿,评测通过并独立审核后才能发布。');return;}const content=kind==='skill'?{engine:'required',inputs:[field],code:'manual.'+field,title,instructions:instruction,description:instruction}:{effect:'advisory',on_match:'supplement',description:instruction,conditions:[{code:'manual.'+field,field,operator:'lt',threshold,title,suggestion:instruction}]};
await send('POST',`/prescreen/projects/${encodeURIComponent(project.id)}/knowledge-candidates`,{kind,title,origin:'manual',version:'1.0.0',content});setCreating(false);setNotice('人工草稿已进入卡片池,评测与独立审核后才可用于预审。');});}
async function transition(card:any,step:string){await action(async()=>{const result=await send('POST',`/prescreen/knowledge-candidates/${encodeURIComponent(card.id)}/transitions`,{action:step});setNotice(step==='evaluate'?(result.passed?'当前版本隔离评测通过。':'评测未通过:'+result.checks.map((c:any)=>c.detail||c.case).join(';')):'卡片已发布。');});}
async function revise(card:any){await action(async()=>{const changes:any={};if(samples.trim())changes.test_cases=JSON.parse(samples);await send('PATCH',`/prescreen/knowledge-candidates/${encodeURIComponent(card.id)}`,{requirement:revision,changes,note:revision||'补充独立测试样本',expected_revision:card.revision});setNotice('已保存草稿修订,需要重新评测。');});}
@@ -23,13 +23,14 @@ export default function KnowledgePools({project,identity,send,refreshKey=0}:any)
<div className="v2-section-heading"><div><span className="v2-kicker">KNOWLEDGE ASSETS</span><h2>可复用的经验卡片池</h2><p>来源颜色与风险等级分别表达;只有已发布卡片能装配 Agent。</p></div>{canEdit&&<div className="xb-actions"><button className="ra-button" onClick={()=>{setKind('skill');folder.current?.click();}} disabled={busy}><FolderPlus size={16}/>从文件夹添加 Skills</button><button className="ra-button" onClick={()=>setCreating(!creating)} disabled={busy}><Plus size={16}/>人工创建</button></div>}</div>
<input ref={folder} type="file" multiple hidden {...{webkitdirectory:'',directory:''}} aria-label="选择标准 Skills 文件夹" onChange={e=>{setPending(Array.from(e.target.files||[]));e.currentTarget.value='';}}/>
{error&&<p role="alert" className="ra-alert error">{error}</p>}{notice&&<p role="status" className="ra-alert">{notice}</p>}
<div className="v2-tabs" role="tablist" aria-label="知识卡片类型">{[['skill','Skills'],['rule_set','Rules'],['data_card','数据卡片']].map(([key,label])=><button role="tab" aria-selected={kind===key} key={key} onClick={()=>{setKind(key);setCreating(false);}}>{label} <span>{cards.filter(c=>c.kind===key).length}</span></button>)}</div>
<div className="v2-tabs" role="tablist" aria-label="知识卡片类型">{[['skill','Skills'],['rule_set','Rules'],['data_card','数据卡片'],['template','模板语言卡']].map(([key,label])=><button role="tab" aria-selected={kind===key} key={key} onClick={()=>{setKind(key);setCreating(false);}}>{label} <span>{cards.filter(c=>c.kind===key).length}</span></button>)}</div>
{pending.length>0&&<div className="v2-import-preview"><h3>确认导入标准文件夹 · {pending.length} 个文件</h3><p>{pending[0]?.webkitRelativePath.split('/')[0]} · 总计 {(pending.reduce((n,f)=>n+f.size,0)/1024).toFixed(1)} KB</p><ul>{pending.slice(0,8).map(f=><li key={f.webkitRelativePath}>{f.webkitRelativePath}</li>)}</ul><label className="ra-field"><span>执行方式</span><select value={requiredField} onChange={e=>setRequiredField(e.target.value)}><option value="">原生模型方法(需模型语义评测,暂存草稿)</option>{fields.map(f=><option key={f} value={f}>受控缺件核查:{f}</option>)}</select></label><p className="v2-muted">可绑定一个标准字段作为缺件核查入口;文件内容保留,工具白名单由系统限定。</p><div className="xb-actions"><button className="ra-button primary" disabled={busy} onClick={importFolder}>{busy?'正在保存…':'确认导入'}</button><button className="ra-button" disabled={busy} onClick={()=>setPending([])}>取消</button></div></div>}
{creating&&kind!=='data_card'&&<div className="v2-import-preview"><h3>人工创建 {kind==='skill'?'Skill':'Rule'} 草稿</h3><div className="v2-form-grid"><label className="ra-field"><span>卡片名称</span><input value={title} onChange={e=>setTitle(e.target.value)}/></label><label className="ra-field"><span>核查字段</span><select value={field} onChange={e=>setField(e.target.value)}><option value="">选择标准字段</option>{fields.map(f=><option key={f}>{f}</option>)}</select></label>{kind==='rule_set'&&<label className="ra-field"><span>建议阈值(低于此值提示补件)</span><input type="number" value={threshold} onChange={e=>setThreshold(e.target.value)}/></label>}</div><label className="ra-field"><span>用自然语言描述核查方法、适用范围与例外(当前先按说明保存)</span><textarea value={instruction} onChange={e=>setInstruction(e.target.value)}/></label><button className="ra-button primary" disabled={busy||!field||!title||!instruction||(kind==='rule_set'&&!threshold)} onClick={create}>保存人工草稿</button></div>}
{creating&&['data_card','template'].includes(kind)&&<div className="v2-import-preview"><h3>人工创建{kind==='template'?'模板语言卡':'数据卡'}草稿</h3><label className="ra-field"><span>卡片名称</span><input value={title} onChange={e=>setTitle(e.target.value)}/></label>{kind==='data_card'&&<label className="ra-field"><span>已批准数据源</span><select value={sourceId} onChange={e=>setSourceId(e.target.value)}>{sources.map(source=><option key={source.id} value={source.id}>{source.name}</option>)}</select>{!sources.length&&<small>请先在高级数据接入中登记并发布数据源。</small>}</label>}<label className="ra-field"><span>{kind==='template'?'报告章节标题,每行一个(顺序:项目概况、风险依据、预审建议)':'只读查询与字段映射 JSON(高级配置)'}</span><textarea rows={5} value={configuration} placeholder={kind==='template'?'项目概况\n风险依据与补件\n预审建议':'{"sql":"SELECT * FROM finance WHERE project_id=:project_id","mapping":{"facility.amount":"amount"},"tables":["lease_project"]}'} onChange={e=>setConfiguration(e.target.value)}/></label><label className="ra-field"><span>模板说明 / 使用场景</span><textarea value={instruction} onChange={e=>setInstruction(e.target.value)}/></label><button className="ra-button primary" disabled={busy||!title||!configuration||(kind==='data_card'&&!sourceId)} onClick={create}>保存人工草稿</button></div>}
{creating&&['skill','rule_set'].includes(kind)&&<div className="v2-import-preview"><h3>人工创建 {kind==='skill'?'Skill':'Rule'} 草稿</h3><div className="v2-form-grid"><label className="ra-field"><span>卡片名称</span><input value={title} onChange={e=>setTitle(e.target.value)}/></label><label className="ra-field"><span>核查字段</span><select value={field} onChange={e=>setField(e.target.value)}><option value="">选择标准字段</option>{fields.map(f=><option key={f}>{f}</option>)}</select></label>{kind==='rule_set'&&<label className="ra-field"><span>建议阈值(低于此值提示补件)</span><input type="number" value={threshold} onChange={e=>setThreshold(e.target.value)}/></label>}</div><label className="ra-field"><span>用自然语言描述核查方法、适用范围与例外(当前先按说明保存)</span><textarea value={instruction} onChange={e=>setInstruction(e.target.value)}/></label><button className="ra-button primary" disabled={busy||!field||!title||!instruction||(kind==='rule_set'&&!threshold)} onClick={create}>保存人工草稿</button></div>}
{loading&&<p role="status">正在读取 SQLite 卡片版本…</p>}
{!loading&&!shown.length&&<div className="ra-empty"><h3>尚无{kind==='rule_set'?' Rules ':'此类'}卡片</h3><p>可以提炼历史经验或人工创建;空池不会自动补成已发布卡片。</p></div>}
<div className="v2-card-grid">{shown.map(card=>{const history=card.origin==='history_derived';const key=`${card.id}:${card.version}:${card.status}:${card.revision}`;return <article key={key} className={`v2-knowledge-card ${history?'from-ai':'from-manual'}`}><div className="v2-card-top"><span>{history?<Sparkles size={14}/>:<FileCheck2 size={14}/>} {history?'AI 提炼':card.import_method==='folder'?'文件夹导入':card.edited_by?'人工创建':'系统内置'}</span><span>{card.status==='published'?'已发布':'草稿'} · v{card.version}</span></div><h3>{card.name}</h3><p>{card.description||card.instructions||'查看卡片依据与适用条件'}</p><div className="v2-card-meta">{card.case_count?`来源 ${card.case_count} 条案例 · `:''}{card.engine==='model'?'模型方法':card.kind==='rule_set'?'建议规则':'受控核查'}{card.scope==='demo'?' · 模拟域':''}</div><div className="xb-actions">{card.kind==='skill'&&<button className="ra-button" onClick={()=>setOpened(card)}><FolderOpen size={14}/>标准文件夹</button>}<button className="ra-button" onClick={()=>{setExpanded(expanded===key?'':key);setRevision('');setSamples('');}}>查看依据与治理</button></div>
{expanded===key&&<div className="v2-card-details"><p>适用条件 {JSON.stringify(card.conditions||[])};例外 {card.exception_note||'未定义,须人工确认'}</p><p>来源 {(card.source_evidence_ids||[]).join('、')||'人工/内置配置'};版本哈希 {card.content_hash?.slice(0,16)}</p>{card.evaluation&&<p>隔离评测:{card.evaluation.passed?'通过':'未通过'}</p>}{canEdit&&card.status==='draft'&&<><label className="ra-field"><span>自然语言修订说明(需要真实模型)</span><textarea value={revision} onChange={e=>setRevision(e.target.value)}/></label><label className="ra-field"><span>独立测试样本 JSON(执行回归,不代表真实预测准确率)</span><textarea value={samples} placeholder='[{"name":"缺件样本","values":{},"expected_outputs":{},"expected_checks":{"核查编码":"unknown"}}]' onChange={e=>setSamples(e.target.value)}/></label><div className="xb-actions"><button className="ra-button" disabled={busy||(!revision&&!samples)} onClick={()=>revise(card)}>保存修订</button><button className="ra-button" disabled={busy} onClick={()=>transition(card,'evaluate')}>运行隔离评测</button>{card.edited_by!==identity?.id&&<button className="ra-button primary" disabled={busy||!card.evaluation?.passed} onClick={()=>transition(card,'publish')}>审核发布</button>}</div>{card.edited_by===identity?.id&&<p>发布需要独立审核人:另一位配置管理员。</p>}</>}</div>}
<div className="v2-card-grid">{shown.map(card=>{const history=card.origin==='history_derived';const key=`${card.id}:${card.version}:${card.status}:${card.revision}`;return <article key={key} className={`v2-knowledge-card ${history?'from-ai':'from-manual'}`}><div className="v2-card-top"><span>{history?<Sparkles size={14}/>:<FileCheck2 size={14}/>} {history?'AI 提炼':card.import_method==='folder'?'文件夹导入':card.edited_by?'人工创建':'系统内置'}</span><span>{card.hidden?'已隐藏':card.status==='published'?'已发布':'草稿'} · v{card.version}</span></div><h3>{card.name}</h3><p>{card.description||card.instructions||'查看卡片依据与适用条件'}</p><div className="v2-card-meta">{card.case_count?`来源 ${card.case_count} 条案例 · `:''}{card.engine==='model'?'模型方法':card.kind==='rule_set'?'建议规则':'受控核查'}{card.scope==='demo'?' · 模拟域':''}</div><div className="xb-actions">{canEdit&&card.status==='published'&&<button className="ra-button" disabled={busy} onClick={()=>action(async()=>{await send('PATCH',`/library/cards/${encodeURIComponent(card.component_id||card.id)}/visibility`,{hidden:!card.hidden});setNotice(card.hidden?'已恢复发布可见性。':'已隐藏,引用它的方案暂停新预审;历史仍保留。');})}>{card.hidden?'恢复发布':'隐藏卡片'}</button>}{card.kind==='skill'&&<button className="ra-button" onClick={()=>setOpened(card)}><FolderOpen size={14}/>标准文件夹</button>}<button className="ra-button" onClick={()=>{setExpanded(expanded===key?'':key);setRevision('');setSamples('');}}>查看依据与治理</button></div>
{expanded===key&&<div className="v2-card-details">{card.kind==='template'&&<p>章节:{card.sections?.map((section:any)=>section.title).join(' → ')}。模板影响报告结构,不影响核查与概率。</p>}{card.kind==='data_card'&&<p>数据源 {card.source_id} · 查询与映射 {JSON.stringify(card.mapping)}</p>}<p>适用条件 {JSON.stringify(card.conditions||[])};例外 {card.exception_note||'未定义,须人工确认'}</p><p>来源 {(card.source_evidence_ids||[]).join('、')||'人工/内置配置'};版本哈希 {card.content_hash?.slice(0,16)}</p>{card.evaluation&&<p>隔离评测:{card.evaluation.passed?'通过':'未通过'}</p>}{canEdit&&card.status==='draft'&&<>{['skill','rule_set'].includes(card.kind)&&<><label className="ra-field"><span>自然语言修订说明(需要真实模型)</span><textarea value={revision} onChange={e=>setRevision(e.target.value)}/></label><label className="ra-field"><span>独立测试样本 JSON(执行回归,不代表真实预测准确率)</span><textarea value={samples} placeholder='[{"name":"缺件样本","values":{},"expected_outputs":{},"expected_checks":{"核查编码":"unknown"}}]' onChange={e=>setSamples(e.target.value)}/></label><div className="xb-actions"><button className="ra-button" disabled={busy||(!revision&&!samples)} onClick={()=>revise(card)}>保存修订</button></div></>}<div className="xb-actions"><button className="ra-button" disabled={busy} onClick={()=>transition(card,'evaluate')}>运行隔离评测</button>{card.edited_by!==identity?.id&&<button className="ra-button primary" disabled={busy||!card.evaluation?.passed} onClick={()=>transition(card,'publish')}>审核发布</button>}</div>{card.edited_by===identity?.id&&<p>发布需要独立审核人:另一位配置管理员。</p>}</>}</div>}
</article>;})}</div>
{opened&&<SkillPackageDialog card={opened} send={send} onClose={()=>setOpened(null)}/>}
</section>;
+1 -1
View File
@@ -5,5 +5,5 @@ import {LeaseCase} from '@/lib/lease-library';
export default function LeaseCaseDialog({item,onClose}:{item:LeaseCase;onClose:()=>void}) {
const dialog=useRef<HTMLDialogElement>(null);
useEffect(()=>{dialog.current?.showModal();},[]);
return <dialog ref={dialog} className="v2-dialog" onCancel={onClose} aria-labelledby="case-detail-title"><header><div><span className="v2-kicker">CORPORATE FINANCIAL LEASE</span><h2 id="case-detail-title">{item.lessee}</h2><p>{item.id} · 模拟案例 · {item.year}年</p></div><button className="ra-button" aria-label="关闭案例详情" onClick={onClose}><X size={18}/></button></header><div className="v2-form-grid">{[['行业',item.industry],['租赁方式',item.mode],['融资金额',`${item.amount.toLocaleString()} 万元`],['租赁期限',`${item.term} 个月`],['评审会结果',item.outcome],['增信方式',item.guarantee]].map(([key,value])=><div key={key} className="v2-fact"><span>{key}</span><b>{value}</b></div>)}</div><h3>初始材料中的模拟财务数据</h3><div className="v2-form-grid">{[["经营现金流",item.operating_cashflow],["租赁物价值",item.asset_value],["营业收入",item.revenue],["总负债",item.total_liabilities],["总资产",item.total_assets]].map(([label,value])=><div key={label} className="v2-fact"><span>{label}</span><b>{Number(value).toLocaleString()} 万元</b></div>)}</div><details style={{marginTop:16}}><summary>查看初始材料时点快照</summary><pre className="v2-code-input" style={{whiteSpace:"pre-wrap"}}>{JSON.stringify(JSON.parse(item.initial_snapshot||"{}"),null,2)}</pre></details><h3>租赁物</h3><p>{item.asset}</p><h3>评审过程发现的问题</h3><p>{item.issue}</p><h3>补充材料记录</h3><p>{item.supplements}</p><h3>会议判断依据</h3><p>{item.meeting_reason}</p><p className="v2-muted">所有企业、设备交易与会议记录均为虚构。暂缓与未送审没有通过/否决标签,不能用作负样本。</p></dialog>;
return <dialog ref={dialog} className="v2-dialog" onCancel={onClose} aria-labelledby="case-detail-title"><header><div><span className="v2-kicker">CORPORATE FINANCIAL LEASE</span><h2 id="case-detail-title">{item.lessee}</h2><p>{item.id} · 模拟案例 · {item.year}年</p></div><button className="ra-button" aria-label="关闭案例详情" onClick={onClose}><X size={18}/></button></header><div className="v2-form-grid">{[['行业',item.industry],['租赁方式',item.mode],['融资金额',`${item.amount.toLocaleString()} 万元`],['租赁期限',`${item.term} 个月`],['评审会结果',item.outcome],['增信方式',item.guarantee]].map(([key,value])=><div key={key} className="v2-fact"><span>{key}</span><b>{value}</b></div>)}</div><h3>初始材料中的财务数据</h3><div className="v2-form-grid">{[["经营现金流",item.operating_cashflow],["租赁物价值",item.asset_value],["营业收入",item.revenue],["总负债",item.total_liabilities],["总资产",item.total_assets]].map(([label,value])=><div key={label} className="v2-fact"><span>{label}</span><b>{Number(value).toLocaleString()} 万元</b></div>)}</div><details style={{marginTop:16}}><summary>查看初始材料时点快照</summary><pre className="v2-code-input" style={{whiteSpace:"pre-wrap"}}>{JSON.stringify(JSON.parse(item.initial_snapshot||"{}"),null,2)}</pre></details><h3>租赁物</h3><p>{item.asset}</p><h3>评审过程发现的问题</h3><p>{item.issue}</p><h3>补充材料记录</h3><p>{item.supplements}</p><h3>会议判断依据</h3><p>{item.meeting_reason}</p><p className="v2-muted">所有企业、设备交易与会议记录均为虚构。暂缓与未送审没有通过/否决标签,不能用作负样本。</p></dialog>;
}
@@ -31,7 +31,7 @@ export default function LeaseLibraryWorkbench({project,identity,send}:any) {
<div className="xb-actions"><button className="ra-button primary" onClick={query} disabled={submitting||!canEdit}><Search size={15}/>{submitting?'处理中…':'预览筛选结果'}</button>{preview&&<span className="v2-muted">解析条件:{mode==='sql'?preview.sql:JSON.stringify(preview.filters)}</span>}{!canEdit&&<span>客户经理可查看历史案例;经验加工由配置管理员操作。</span>}</div>
{error&&<p role="alert" className="ra-alert error">{error}</p>}{notice&&<p role="status" className="v2-notice">{notice}</p>}
{state==='loading'&&<p role="status">正在读取 SQLite 案例库…</p>}{state==='empty'&&<div className="ra-empty">没有符合条件的案例,请修改筛选。</div>}
<div className="v2-table-wrap"><table className="v2-case-table"><thead><tr><th>选中</th><th>承租企业 / 案例</th><th>业务与租赁物</th><th>金额 / 期限</th><th>会议结果</th></tr></thead><tbody>{visible.map(item=><tr key={item.id}><td><input type="checkbox" aria-label={`选择 ${item.id}`} disabled={!preview||submitting||!canEdit} checked={useAll?Boolean(preview):picked.includes(item.id)} onChange={()=>toggle(item.id)}/></td><td><button className="v2-link" onClick={()=>setDetail(item)}>{item.lessee}</button><small>{item.id} · {item.year}年 · 模拟</small></td><td><b>{item.mode} · {item.industry}</b><small>{item.asset}</small></td><td><b>{item.amount.toLocaleString()} 万元</b><small>{item.term} 个月</small></td><td><span className={`v2-outcome outcome-${outcomes.indexOf(item.outcome)}`}>{item.outcome}</span></td></tr>)}</tbody></table></div>
<div className="v2-table-wrap"><table className="v2-case-table"><thead><tr><th>选中</th><th>承租企业 / 案例</th><th>业务与租赁物</th><th>金额 / 期限</th><th>会议结果</th></tr></thead><tbody>{visible.map(item=><tr key={item.id}><td><input type="checkbox" aria-label={`选择 ${item.id}`} disabled={!preview||submitting||!canEdit} checked={useAll?Boolean(preview):picked.includes(item.id)} onChange={()=>toggle(item.id)}/></td><td><button className="v2-link" onClick={()=>setDetail(item)}>{item.lessee}</button><small>{item.id} · {item.year}年</small></td><td><b>{item.mode} · {item.industry}</b><small>{item.asset}</small></td><td><b>{item.amount.toLocaleString()} 万元</b><small>{item.term} 个月</small></td><td><span className={`v2-outcome outcome-${outcomes.indexOf(item.outcome)}`}>{item.outcome}</span></td></tr>)}</tbody></table></div>
<div className="v2-pagination"><span>实际 {items.length} 条 · 每页20条 · 第 {page+1}/{pages} 页</span><div className="xb-actions"><button className="ra-button" aria-label="上一页" disabled={page===0||submitting} onClick={()=>{setPage(p=>p-1);setPicked([]);setSelection(null);}}><ChevronLeft size={16}/></button><button className="ra-button" aria-label="下一页" disabled={page>=pages-1||submitting} onClick={()=>{setPage(p=>p+1);setPicked([]);setSelection(null);}}><ChevronRight size={16}/></button></div></div><p className="v2-muted">{source}</p>
</section>
<aside className="v2-surface v2-extraction"><span className="v2-kicker">EXPERIENCE → AGENT</span><h2>把经验变成可复用能力</h2><ol className="v2-steps"><li className={preview?'done':''}><span>1</span><div><b>筛选历史案例</b><p>结果直接来自 SQLite</p></div></li><li className={selection?'done':''}><span>2</span><div><b>确认加工范围</b><p>选择快照保留实际案例编号</p></div></li><li><span>3</span><div><b>分别提炼 Skills / Rules</b><p>真实模型生成草稿,随后审核</p></div></li></ol>
@@ -2,16 +2,16 @@
import {useEffect,useRef,useState} from 'react';
import {Database,Blocks,ShieldCheck,Save} from 'lucide-react';
export default function PrescreenAssembly({project,identity,planId,request,onChanged}:any) {
const [frozen,setFrozen]=useState<any>(null),[available,setAvailable]=useState<any[]>([]),[plans,setPlans]=useState<any[]>([]),[selected,setSelected]=useState<Record<string,string[]>>({skill:[],rule_set:[],data_card:[]});
export default function PrescreenAssembly({project,identity,planId,request,onChanged,configuration=false}:any) {
const [frozen,setFrozen]=useState<any>(null),[available,setAvailable]=useState<any[]>([]),[plans,setPlans]=useState<any[]>([]),[selected,setSelected]=useState<Record<string,string[]>>({skill:[],rule_set:[],data_card:[],template:[]});
const [editing,setEditing]=useState(false),[busy,setBusy]=useState(false),[error,setError]=useState(''),[notice,setNotice]=useState(''),[name,setName]=useState(''); const sequence=useRef(0);
const canEdit=identity?.role==='admin';
async function load(){const ticket=++sequence.current;try{const [current,all,custom]=await Promise.all([request(`/library/plans/${encodeURIComponent(planId)}?project_id=${encodeURIComponent(project.id)}`),request(`/library/cards?project_id=${encodeURIComponent(project.id)}`),request(`/prescreen/projects/${encodeURIComponent(project.id)}/review-plans`)]);if(ticket!==sequence.current)return;setFrozen(current);setAvailable(all.items.filter((c:any)=>c.status==='published'));setPlans(custom.items||[]);setSelected({skill:current.skills.map((c:any)=>c.id),rule_set:current.rules.map((c:any)=>c.id),data_card:current.cards.filter((c:any)=>c.source_id).map((c:any)=>c.id)});}catch(e:any){if(ticket===sequence.current)setError(e.message);}}
const canEdit=identity?.role==='admin'&&configuration;
async function load(){const ticket=++sequence.current;try{const [current,all,custom]=await Promise.all([request(`/library/plans/${encodeURIComponent(planId)}?project_id=${encodeURIComponent(project.id)}`),request(`/library/cards?project_id=${encodeURIComponent(project.id)}`),request(`/prescreen/projects/${encodeURIComponent(project.id)}/review-plans`)]);if(ticket!==sequence.current)return;setFrozen(current);setAvailable(all.items.filter((c:any)=>c.status==='published'&&!c.hidden));setPlans(custom.items||[]);setSelected({skill:current.skills.map((c:any)=>c.id),rule_set:current.rules.map((c:any)=>c.id),data_card:current.cards.filter((c:any)=>c.source_id).map((c:any)=>c.id),template:current.template?[current.template.id]:[]});}catch(e:any){if(ticket===sequence.current)setError(e.message);}}
useEffect(()=>{setEditing(false);setFrozen(null);setError('');if(planId)load();return()=>{sequence.current++;};},[project.id,planId]);
function toggle(kind:string,id:string){setSelected(current=>({...current,[kind]:current[kind].includes(id)?current[kind].filter(k=>k!==id):[...current[kind],id]}));}
async function save(){setBusy(true);setError('');try{const result=await request(`/prescreen/projects/${encodeURIComponent(project.id)}/review-plans`,{name:name||'对公租赁预审装配方案',skills:selected.skill,rules:selected.rule_set,cards:selected.data_card,template:'template.ai-pre-review',audience:['manager','admin'],run_kind:'pre_review',version:'1.0.0'});setNotice(`装配草稿已保存 ${result.id},请完成评测与独立审核。`);setEditing(false);await load();}catch(e:any){setError(e.message);}finally{setBusy(false);}}
function toggle(kind:string,id:string){setSelected(current=>({...current,[kind]:kind==='template'?[id]:current[kind].includes(id)?current[kind].filter(k=>k!==id):[...current[kind],id]}));}
async function save(){setBusy(true);setError('');try{const result=await request(`/prescreen/projects/${encodeURIComponent(project.id)}/review-plans`,{name:name||'对公租赁预审装配方案',skills:selected.skill,rules:selected.rule_set,cards:selected.data_card,template:selected.template[0]||'template.ai-pre-review',audience:['manager','admin'],run_kind:'pre_review',version:'1.0.0'});setNotice(`装配草稿已保存 ${result.id},请完成评测与独立审核。`);setEditing(false);await load();}catch(e:any){setError(e.message);}finally{setBusy(false);}}
async function transition(plan:any,action:string){setBusy(true);setError('');try{const result=await request(`/prescreen/review-plans/${encodeURIComponent(plan.id)}/transitions`,{action});setNotice(action==='evaluate'?(result.passed?'方案隔离评测通过。':'方案评测未通过,请查看依赖与规则。'):'方案已发布,可选择后发起预审。');await load();await onChanged();}catch(e:any){setError(e.message);}finally{setBusy(false);}}
const groups=[{kind:'data_card',title:'02 · 数据卡片',icon:Database,items:frozen?.cards||[]},{kind:'skill',title:'03 · Skills 卡片',icon:Blocks,items:frozen?.skills||[]},{kind:'rule_set',title:'04 · Rules 卡片',icon:ShieldCheck,items:frozen?.rules||[]}];
const groups=[{kind:'data_card',title:'02 · 数据卡片',icon:Database,items:frozen?.cards||[]},{kind:'skill',title:'03 · Skills 卡片',icon:Blocks,items:frozen?.skills||[]},{kind:'rule_set',title:'04 · Rules 卡片',icon:ShieldCheck,items:frozen?.rules||[]},{kind:'template',title:'05 · 模板语言卡',icon:Blocks,items:frozen?.template?[frozen.template]:[]}];
return <section className="v2-surface v2-assembly"><div className="v2-section-heading"><div><span className="v2-kicker">ASSEMBLE THE PRESCREEN AGENT</span><h2>本次预审使用哪些卡片</h2><p>资料文件夹+数据+Skills+Rules+本次要求,共同组成冻结输入。</p></div>{canEdit&&<button className="ra-button" disabled={busy||!frozen} onClick={()=>setEditing(!editing)}>{editing?'返回已发布方案':'重新装配方案'}</button>}</div>
{error&&<p role="alert" className="ra-alert error">{error}</p>}{notice&&<p role="status" className="ra-alert">{notice}</p>}{!frozen&&!error&&<p role="status">正在读取方案真实组件…</p>}
<div className="v2-assembly-grid">{groups.map(group=>{const Icon=group.icon;const items=editing?available.filter(c=>c.kind===group.kind):group.items;return <div key={group.kind} className="v2-assembly-card"><h3><Icon size={16} style={{display:'inline',verticalAlign:'middle'}}/> {group.title}</h3>{!items.length&&<p className="v2-muted">{group.kind==='data_card'?'未额外绑定数据卡,使用当前项目材料与事实快照。':'本方案未选择此类卡片。'}</p>}{items.map((card:any)=><label key={card.id}><input type="checkbox" checked={editing?selected[group.kind].includes(card.id):true} disabled={!editing||busy} onChange={()=>toggle(group.kind,card.id)}/><span>{card.name}<small>{card.version||'已发布配置'} · {card.origin==='history_derived'?'AI提炼':card.import_method==='folder'?'文件夹导入':'内置/人工'}{card.effect==='hard'?' · 硬规则':''}</small></span></label>)}</div>;})}</div>
+8 -6
View File
@@ -1,4 +1,5 @@
"use client";
import EvidenceInfluence from "./EvidenceInfluence";
import ProbabilitySummary from "./ProbabilitySummary";
import {useEffect, useRef, useState} from "react";
import {AlertTriangle} from "lucide-react";
@@ -182,13 +183,14 @@ export default function PrescreenPanel({project, request, busy, setBusy, setErro
<div>
<b>主审意见:{RECOMMENDATION_LABELS[run.recommendation || ""] || "尚未形成意见"}</b>
<p>方案 {run.plan_ref.id} · {run.plan_ref.version} · 运行 {RUN_STATUS_LABELS[run.status] || run.status} · 输入快照 {run.snapshot_id}</p>
<p>{domain.text}({run.data_domain === "demo" ? "所有数值为人工演示,未连接真实数据与模型" : "真实数据域不显示任何未校准概率"})</p>
<p>{domain.text}({run.data_domain === "demo" ? "材料为模拟数据;概率为人工情景值,模型调用状态以本次执行记录为准" : "真实数据域不显示任何未校准概率"})</p>
{run.missing_inputs.length > 0 && <p className="xb-warning"><AlertTriangle size={13}/> 待补充 {run.missing_inputs.length} 项:{run.missing_inputs.join("、")}</p>}
{run.degradations.map((item, index) => <p className="xb-warning" key={index}>未完成项:{item.message}({item.code})</p>)}
</div>
</div>
<ProbabilitySummary baseline={baseline} scenario={scenario} status={scenarioState} reason={simulation?.reason}/>
<EvidenceInfluence projectId={project.id} runId={run.id} request={request} scenarioStages={scenarioStages}/>
<details className="v2-advanced"><summary>查看概率口径与情景计算依据</summary>
<div className="xb-finding" aria-live="polite">
<div>
@@ -224,7 +226,7 @@ export default function PrescreenPanel({project, request, busy, setBusy, setErro
<p>方案没有维度时不补占位轴,也不给出任何结论。</p></div>}
{shown.layout !== "empty" && <>
<p>{shown.reason}</p>
{shown.layout === "radar" && <RadarChart dimensions={dimensions} scenarioStages={scenarioStages} />}
{shown.layout === "radar" && <RadarChart dimensions={dimensions} scenarioStages={scenarioStages} labels={Object.fromEntries(((project.runs||[]).find((r:any)=>r.id===run.id)?.steps||[]).map((s:any)=>[s.skill?.id||s.step_id,s.skill?.name||s.name]))} />}
<div className="xb-finding">
<div>
{dimensions.map((item) => {
@@ -287,7 +289,7 @@ export default function PrescreenPanel({project, request, busy, setBusy, setErro
}
/** 六到八轴证据雷达。基线虚线、情景实线,旁边始终有等价文字列表。 */
function RadarChart({dimensions, scenarioStages}: {dimensions: any[]; scenarioStages: Map<string, any>}) {
function RadarChart({dimensions, scenarioStages, labels={}}: {dimensions: any[]; scenarioStages: Map<string, any>; labels?:Record<string,string>}) {
const radius = 90;
const count = dimensions.length;
const point = (index: number, stage: number | null) => {
@@ -304,9 +306,9 @@ function RadarChart({dimensions, scenarioStages}: {dimensions: any[]; scenarioSt
});
// 只有真的存在被假设的维度才画实线:没勾选时把基线复制成"所选情景"是误导
const hasScenario = Array.from(scenarioStages.values()).some((item) => item.assumed);
return <div>
return <div className="v2-radar">
<svg role="group" aria-label="证据核验阶段雷达,详细内容见下方维度按钮"
width={330} height={260} viewBox="-150 -130 300 270">
width={430} height={330} viewBox="-150 -130 300 270">
{[1, 2, 3, 4].map((step) => <polygon key={step} className="radar-grid" fill="none" stroke="currentColor" opacity={0.2}
points={polygon(Array(count).fill(step))} />)}
{dimensions.map((item, index) => {
@@ -320,7 +322,7 @@ function RadarChart({dimensions, scenarioStages}: {dimensions: any[]; scenarioSt
{dimensions.map((item, index) => {
const [x, y] = point(index, 4.7);
return <text key={`label-${item.id}`} x={x} y={y} fontSize={11} textAnchor="middle"
fill="currentColor">{item.id}</text>;
fill="currentColor">{(labels[item.id]||item.id).slice(0,8)}</text>;
})}
</svg>
<p>虚线为基线,实线为所选假设;两者都只是阶段,不是分数。</p>
@@ -0,0 +1,9 @@
"use client";
import {useEffect,useState} from 'react';
import {ShieldCheck,Server,CircleOff,PlugZap} from 'lucide-react';
export default function QualitySettings({send}:any){
const [value,setValue]=useState<any>(null),[key,setKey]=useState(''),[busy,setBusy]=useState(false),[message,setMessage]=useState(''),[error,setError]=useState('');
useEffect(()=>{send('GET','/library/quality/settings').then(setValue).catch((e:any)=>setError(e.message));},[]);
async function action(probe=false){setBusy(true);setError('');setMessage('');try{if(probe){const result=await send('POST','/library/quality/probe');setMessage(result.message);}else{const result=await send('PUT','/library/quality/settings',{provider:value.provider,endpoint:value.endpoint||'',model:value.model||'',allow_external:!!value.allow_external,...(key?{api_key:key}:{})});setValue(result);setKey('');setMessage('专项配置已保存,只影响后续质检。');}}catch(e:any){setError(e.message);}finally{setBusy(false);}}
return <section className="v2-surface"><div className="v2-section-heading"><div><span className="v2-kicker">JUDGMENT PROVIDER</span><h2>专项判断与轨迹质检</h2><p>主审由 miniOpenClaw 编排 Skills、Rules 和受控 Tools;这里检查编排过程是否完整、有据可查。</p></div><ShieldCheck size={28}/></div>{error&&<p role="alert" className="ra-alert error">{error}</p>}{message&&<p role="status" className="ra-alert">{message}</p>}{!value?<p>正在读取专项配置…</p>:<><div className="v2-provider-grid">{[['disabled','关闭专项模型',CircleOff],['local','本地兼容判断模型',Server],['jev','Jev · TypeSafe',ShieldCheck]].map(([id,label,Icon]:any)=><button className={`v2-provider ${value.provider===id?'selected':''}`} key={id} disabled={busy} aria-pressed={value.provider===id} onClick={()=>setValue({...value,provider:id,model:id==='jev'?'jev-latest':id==='local'?'qwen3:8b':'',endpoint:id==='jev'?'https://api.typesafe.ai/v1/systemone':id==='local'?'http://127.0.0.1:11434/v1':'',allow_external:false})}><Icon size={22}/><b>{label}</b><small>{id==='disabled'?'保留规则审计':id==='local'?'Ollama / vLLM · 无需 Jev Key':'窄问题判断 · 可替换'}</small></button>)}</div>{value.provider!=='disabled'&&<div className="v2-form-grid"><label className="ra-field"><span>模型地址(本地模式限本机或内网 IP)</span><input value={value.endpoint||''} disabled={busy||value.provider==='jev'} onChange={e=>setValue({...value,endpoint:e.target.value})}/></label><label className="ra-field"><span>模型名称</span><input value={value.model||''} disabled={busy} onChange={e=>setValue({...value,model:e.target.value})}/></label><label className="ra-field"><span>密钥 · {value.key_configured?'已设置,留空保留':'未设置'}</span><input type="password" autoComplete="new-password" value={key} disabled={busy} onChange={e=>setKey(e.target.value)}/></label></div>}{value.provider==='jev'&&<label className="ra-field"><span><input type="checkbox" checked={!!value.allow_external} disabled={busy} onChange={e=>setValue({...value,allow_external:e.target.checked})}/>允许向 TypeSafe 发送模拟域的步骤状态、核查码与引用计数(不含材料正文)</span></label>}<div className="xb-actions"><button className="ra-button primary" disabled={busy} onClick={()=>action()}>{busy?'处理中…':'保存专项配置'}</button><button className="ra-button" disabled={busy||value.provider==='disabled'} onClick={()=>action(true)}><PlugZap size={15}/>检查已保存连接</button><a className="ra-button" href="/trajectory-center?view=quality">前往轨迹质检</a></div><p className="v2-muted">配置修订 v{value.revision} · {value.rubric_version} · 影子质检。连接通过不代表任务质量验证通过;关闭或失败均不产生默认分值。专项置信度不等于评审会通过概率。</p></>}</section>;
}
+2 -2
View File
@@ -72,8 +72,8 @@ export default function TrajectoryPanel({project, identity, send, view="all"}: a
useEffect(() => {
if (runs.length) {
const first = runs[0].id;
setRunId(first);
load(first);
const query=new URLSearchParams(location.search),requested=query.get("run");const target=runs.some((r:any)=>r.id===requested)?requested!:first;setRunId(target);setOpenStep(query.get("step")||"");
load(target);
} else {setTrajectory(null); setLoaded(true);}
setForm(EMPTY_FORM); setNotice("");
loadOutcomes();
@@ -0,0 +1,11 @@
"use client";
import {useEffect,useRef,useState} from 'react';
import {ScanLine,CheckCircle2,ArrowUpRight} from 'lucide-react';
const labels:any={missing:'是否有未完成核查',unsupported:'是否有缺少证据引用的通过项',deviation:'是否执行了未冻结的组件'};
export default function TrajectoryQualityPanel({project,send}:any){
const [runId,setRunId]=useState(''),[items,setItems]=useState<any[]>([]),[busy,setBusy]=useState(false),[error,setError]=useState('');const ticket=useRef(0);
const runs=(project.runs||[]).filter((r:any)=>['completed','partial','failed','cancelled'].includes(r.status));
useEffect(()=>{const current=++ticket.current;setRunId(runs[0]?.id||'');setItems([]);setError('');send('GET',`/library/projects/${encodeURIComponent(project.id)}/quality`).then((data:any)=>{if(current===ticket.current)setItems(data.items);}).catch((e:any)=>{if(current===ticket.current)setError(e.message);});return()=>{ticket.current++;};},[project.id]);
async function inspect(){const current=ticket.current;setBusy(true);setError('');try{const row=await send('POST',`/library/projects/${encodeURIComponent(project.id)}/quality`,{run_id:runId});if(current===ticket.current)setItems(old=>[row,...old]);}catch(e:any){if(current===ticket.current)setError(e.message);}finally{setBusy(false);}}
return <section className="v2-surface v2-quality"><div className="v2-section-heading"><div><span className="v2-kicker">TRAJECTORY QUALITY</span><h2>这次核查过程,哪里需要改进</h2><p>选择一次已结束的预审,检查缺漏、证据引用和组件版本,再定位到对应步骤。</p></div><ScanLine size={30}/></div><div className="v2-quality-flow"><span>01 选择运行</span><i>→</i><span>02 核查轨迹</span><i>→</i><span>03 回看问题步骤</span><i>→</i><span>04 人工提改进候选</span></div>{error&&<p className="ra-alert error" role="alert">{error}</p>}{!runs.length?<div className="ra-empty">先完成一次 AI 预审,再检查它的调用轨迹。</div>:<><label className="ra-field"><span>本次质检对象</span><select value={runId} disabled={busy} onChange={e=>setRunId(e.target.value)}>{runs.map((r:any)=><option key={r.id} value={r.id}>{r.created_at?.slice(0,16)} · {r.plan_id} · {r.status} · {r.id.slice(-8)}</option>)}</select></label><button className="ra-button primary" disabled={busy||!runId} onClick={inspect}><ScanLine size={16}/>{busy?'正在专项核查…':'检查这次轨迹'}</button></>}{items.filter(r=>r.run_id===runId).map(row=><article className="v2-quality-report" key={row.id}><div className="v2-card-top"><b>{row.config.provider==='disabled'?'规则审计':row.config.provider==='jev'?'Jev 专项质检':'本地模型专项质检'} · 配置 v{row.config.revision}</b><span>{row.created_at.slice(0,16)}</span></div><p>{row.judgment.reason}</p>{row.checks.length?row.checks.map((c:any,index:number)=><div key={index} className="v2-quality-issue"><b>{c.message}</b><small>步骤 {c.step_id} · {c.code}</small><a href={`/trajectory-center?project=${encodeURIComponent(project.id)}&view=process&run=${encodeURIComponent(row.run_id)}&step=${encodeURIComponent(c.step_id)}`}>定位步骤<ArrowUpRight size={13}/></a></div>):<p><CheckCircle2 size={15}/>规则审计未发现上述结构问题;不代表材料真实性已经核验。</p>}{Object.entries(row.judgment.answers||{}).map(([k,v]:any)=><div className="v2-quality-answer" key={k}><span>{labels[k]}</span><b>{v.metric==='boolean'?(v.value?'是':'否'):`判断为是:${Math.round(v.value*100)}%`}</b></div>)}{row.judgment.status==='failed'&&<p className="ra-alert error">{row.judgment.code} · 未生成模型判断</p>}<small>快照摘要 {row.input_hash.slice(0,16)} · 主审意见与概率保持原值 · 质检只读编排元数据</small></article>)}{!items.length&&<p className="v2-muted">尚无质检记录。结果保留配置版本,可在后续会议结果对照时回看。</p>}</section>;
}
+2
View File
@@ -19,3 +19,5 @@
.xb-role-switch>span{font-size:12px}.xb-role-switch select{font-size:14px;cursor:pointer}
.xb-overview-heading h1{font-size:30px;line-height:1.35}.xb-overview-heading p{font-size:14px;line-height:1.75;color:#667970}
@media(max-width:760px){.xb-agent-nav a{font-size:12px;min-height:38px;padding:8px}.xb-brand b{font-size:14px}.xb-overview-heading h1{font-size:26px}}
.xb-extra-nav{margin:8px 0}.xb-extra-nav>summary{font-size:12px;cursor:pointer;padding:10px 12px;color:#B5C9BE}.xb-extra-nav>a{display:flex;align-items:center;gap:10px;padding:11px 12px;border-radius:8px;font-size:13px;color:#B5C9BE}.xb-extra-nav>a:hover,.xb-extra-nav>a.selected{background:#244D41;color:#FFF}
+8
View File
@@ -25,3 +25,11 @@
@media(max-width:900px){.xb-v2 .xb-agent-nav{width:190px;padding:22px 12px}.xb-v2 .ra-main{margin-left:190px;padding:24px 20px}.v2-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.v2-card-grid,.v2-assembly-grid{grid-template-columns:minmax(0,1fr)}.v2-section-heading{align-items:start;flex-wrap:wrap}}
@media(max-width:760px){.xb-v2 .xb-agent-nav{position:relative;width:100%;flex-direction:row;flex-wrap:wrap;align-items:center;padding:12px;gap:6px}.xb-v2 .xb-agent-nav .xb-brand{width:100%;padding:4px 6px 14px}.xb-v2 .xb-agent-nav a{font-size:12px;min-height:40px;padding:9px}.xb-v2 .xb-nav-group,.xb-v2 .xb-project-context,.xb-v2 .xb-nav-spacer{display:none}.xb-v2 .xb-agent-nav .xb-role-switch{margin:6px 0}.xb-v2 .xb-role-switch>span{width:auto}.xb-v2 .ra-main{margin:0;padding:24px 16px}.v2-metrics{gap:10px}.v2-metrics article{padding:14px}.v2-surface,.xb-v2 .xb-panel,.xb-v2 .xb-task{padding:18px}.v2-steps{display:block}.v2-steps li{margin-bottom:16px}.v2-form-grid,.v2-file-layout,.v2-comparison-grid{grid-template-columns:1fr}.v2-file-layout aside{border-right:0;border-bottom:1px solid var(--v2-line)}.v2-probability{padding:18px;gap:16px;flex-wrap:wrap}}
@media(prefers-reduced-motion:reduce){.xb-v2 *{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
.v2-environment{font-size:12px;color:var(--v2-muted);border:1px solid var(--v2-line);border-radius:999px;padding:7px 12px;white-space:nowrap;background:var(--v2-surface)}
.v2-provider-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin:22px 0}.v2-provider{display:flex;flex-direction:column;gap:12px;align-items:flex-start;border:1px solid var(--v2-line);background:var(--v2-surface);border-radius:12px;padding:22px;text-align:left;color:var(--v2-muted)}.v2-provider b{font-size:15px;color:var(--v2-text)}.v2-provider small{font-size:12px}.v2-provider.selected{border-color:var(--v2-primary);background:var(--v2-bg);box-shadow:inset 0 0 0 1px var(--v2-primary);color:var(--v2-primary)}
.v2-quality-flow{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:18px;border-radius:10px;background:var(--v2-bg);margin:20px 0;color:var(--v2-muted);font-size:13px}.v2-quality-flow i{font-style:normal}.v2-quality-report{border:1px solid var(--v2-line);border-radius:12px;padding:22px;margin-top:20px}.v2-quality-report>p{margin:16px 0;color:var(--v2-muted);font-size:13px}.v2-quality-report>small{display:block;margin-top:16px;color:var(--v2-muted);font-size:11px}.v2-quality-issue{display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:14px 0;border-bottom:1px solid var(--v2-line)}.v2-quality-issue b{font-size:13px;flex:1;min-width:250px}.v2-quality-issue small,.v2-quality-issue a{font-size:12px;color:var(--v2-muted)}.v2-quality-issue a{display:flex;gap:4px;color:var(--v2-primary)}.v2-quality-answer{display:flex;justify-content:space-between;gap:16px;font-size:13px;padding:12px;border-radius:7px;background:var(--v2-bg);margin-top:10px}
.v2-influences{margin:28px 0}.v2-influence-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.v2-influence{background:var(--v2-surface);border:1px solid var(--v2-line);border-radius:12px;padding:20px}.v2-influence.blocked{border-color:var(--v2-ai)}.v2-influence h4{margin:15px 0 12px;font-size:15px}.v2-influence>small{display:block;margin-top:14px;font-size:11px;color:var(--v2-muted)}.v2-stage-change{display:flex;gap:8px;align-items:center;font-size:12px;color:var(--v2-muted)}.v2-scenario-stage{color:var(--v2-primary)}.v2-check-result{font-size:13px;border-top:1px solid var(--v2-line);padding-top:12px;margin-top:12px}.v2-check-result p{margin:8px 0}.v2-check-result small{font-size:11px;color:var(--v2-muted)}.v2-result{font-size:11px;background:var(--v2-bg);border-radius:5px;padding:4px 8px}.v2-result.result-pass{color:var(--v2-primary)}.v2-result.result-issue,.v2-result.result-error{color:var(--v2-ai)}.v2-advice{color:var(--v2-muted);line-height:1.7}.v2-assistant-entry{display:block;border:1px solid var(--v2-line);border-radius:10px;padding:15px 20px;background:var(--v2-surface);margin:18px 0;color:var(--v2-primary);font-size:13px}.v2-assembly-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.xb-v2 button:focus-visible,.xb-v2 a:focus-visible,.xb-v2 input:focus-visible,.xb-v2 select:focus-visible{outline:2px solid var(--v2-primary);outline-offset:3px}
@media(max-width:1100px){.v2-assembly-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.v2-provider-grid,.v2-influence-grid,.v2-assembly-grid{grid-template-columns:1fr}.v2-quality-flow{gap:8px}}
.v2-radar{padding:24px;background:radial-gradient(ellipse at center,#EDF5F1 0%,var(--v2-surface) 72%);border:1px solid var(--v2-line);border-radius:12px;margin:18px 0;text-align:center}.v2-radar svg{max-width:100%;height:auto;color:var(--v2-primary)}.v2-radar polygon[data-series="baseline"]{stroke:var(--v2-muted);stroke-width:1.5;fill:#52675C0B}.v2-radar polygon[data-series="scenario"]{stroke:var(--v2-primary);stroke-width:2.5;fill:#17634C;fill-opacity:.18}.v2-radar p{font-size:12px;color:var(--v2-muted);margin-top:12px}