feat: implement configurable Xingbang risk agent on mini-openclaw core

This commit is contained in:
NathanAlex7
2026-09-08 01:08:34 +08:00
parent f2439ee5a3
commit 58fb2854dc
26 changed files with 1990 additions and 208 deletions
+15
View File
@@ -0,0 +1,15 @@
import { NextRequest, NextResponse } from "next/server";
async function proxy(request: NextRequest, {params}: {params:{path:string[]}}) {
const base=process.env.RISK_AGENT_BACKEND_URL || "http://127.0.0.1:8002";
try {
const response=await fetch(`${base}/api/xingbang/risk-agent/${params.path.map(encodeURIComponent).join("/")}`,{
method:request.method,headers:{"Content-Type":"application/json", "Authorization":request.headers.get("authorization")||"", "X-Xingbang-Client-Host":request.headers.get("host")||""},
body:["GET","HEAD"].includes(request.method)?undefined:await request.text(),cache:"no-store",
});
return new NextResponse(await response.text(),{status:response.status,headers:{"Content-Type":"application/json"}});
} catch {
return NextResponse.json({detail:"风险 Agent 服务未连接,请检查后端运行状态"},{status:502});
}
}
export {proxy as GET,proxy as POST,proxy as PUT,proxy as DELETE};
+31
View File
@@ -0,0 +1,31 @@
"use client";
import {useState} from "react";
import {Download, ShieldCheck, Plus, Trash2, X} from "lucide-react";
type Fields={item:any;field:(key:string,value:unknown)=>void};
function Field({label,children}:{label:string;children:React.ReactNode}){return <label className="ra-field"><span>{label}</span>{children}</label>}
export function ConnectionFields({item,field}:Fields){return <>
<Field label="连接类型"><select value={item.type} onChange={e=>field("type",e.target.value)}>{["demo","SQLite","PostgreSQL","MySQL"].map(t=><option key={t} value={t}>{t==="demo"?"本地演示数据库":t}</option>)}</select></Field>
{item.type!=="demo"&&<>
{item.type!=="SQLite"&&<><div className="ra-two"><Field label="主机地址"><input value={item.host||""} onChange={e=>field("host",e.target.value)}/></Field><Field label="端口"><input type="number" value={item.port||""} onChange={e=>field("port",e.target.value)}/></Field></div><Field label="只读账号"><input value={item.username||""} onChange={e=>field("username",e.target.value)}/></Field><Field label="服务器密码引用"><input placeholder="XINGBANG_DB_ERP_PASSWORD" value={item.password_env||""} onChange={e=>field("password_env",e.target.value)}/></Field></>}
<Field label={item.type==="SQLite"?"服务器 SQLite 文件完整路径":"数据库名称"}><input value={item.database||""} onChange={e=>field("database",e.target.value)}/></Field>
<Field label="允许访问的表或视图 · 每行一个"><textarea value={(item.allowed_tables||[]).join("\n")} onChange={e=>field("allowed_tables",e.target.value.split("\n").filter(Boolean))}/></Field>
<div className="ra-two"><Field label="超时(秒)"><input type="number" min="1" max="60" value={item.timeout||15} onChange={e=>field("timeout",Number(e.target.value))}/></Field><Field label="最大返回行数"><input type="number" min="1" max="10000" value={item.max_rows||1000} onChange={e=>field("max_rows",Number(e.target.value))}/></Field></div>
<p className="ra-note">密码由服务器密钥或环境配置提供,不写入数据卡。数据库账号必须具有只读权限。SQLite 文件须位于服务器允许的目录内。</p>
</>}
</>}
export function AlignmentFields({item,field}:Fields){const a=item.alignment||{};const set=(key:string,value:string)=>field("alignment",{...a,[key]:value});return <section className="ra-alignment"><h3>多卡数据对齐</h3><Field label="项目标识源字段"><input value={a.project_field??"project_id"} onChange={e=>set("project_field",e.target.value)}/></Field><div className="ra-two"><Field label="期间源字段(可选)"><input placeholder="period" value={a.period_field||""} onChange={e=>set("period_field",e.target.value)}/></Field><Field label="口径源字段(可选)"><input placeholder="scope" value={a.scope_field||""} onChange={e=>set("scope_field",e.target.value)}/></Field></div><Field label="记录选择方式"><select value={a.selection||"single"} onChange={e=>set("selection",e.target.value)}><option value="single">唯一记录</option><option value="latest">最新一期</option><option value="period">指定期间</option><option value="all">保留多期供综合研判</option></select></Field><div className="ra-two"><Field label="目标口径(可选)"><input placeholder="合并" value={a.target_scope||""} onChange={e=>set("target_scope",e.target.value)}/></Field>{a.selection==="period"&&<Field label="目标期间"><input placeholder="2025" value={a.target_period||""} onChange={e=>set("target_period",e.target.value)}/></Field>}</div><p className="ra-note">不同数据卡平级取数。金额换算保留原值证据,期间、口径及重复记录存在冲突时不自动计算。</p></section>}
export function RuleFields({item,field,json,setJson}:{item:any;field:Fields["field"];json:string;setJson:(value:string)=>void}){
let conditions:any[]=[];let invalid=false;try{conditions=JSON.parse(json);if(!Array.isArray(conditions))invalid=true}catch{invalid=true}
const change=(i:number,k:string,v:unknown)=>setJson(JSON.stringify(conditions.map((r,j)=>j===i?{...r,[k]:v}:r),null,2));
return <><Field label="人为自然语言规则"><textarea placeholder="例如:结合利润与经营现金流的变化识别疑点,逐项列明证据不足之处,只给核查建议。" value={item.natural_language||""} onChange={e=>field("natural_language",e.target.value)}/></Field><p className="ra-note">所选规则随平级数据卡送入本地模型;模型不可修改规则或作出审批决定。模型未配置时明确显示该项未完成。</p><h3>确定性检查条件</h3>{!invalid&&conditions.map((c,i)=><div className="ra-rule-row" key={i}><Field label="检查名称"><input value={c.title||""} onChange={e=>change(i,"title",e.target.value)}/></Field><div className="ra-two"><Field label="标准字段"><input value={c.field||""} onChange={e=>change(i,"field",e.target.value)}/></Field><Field label="判断条件"><select value={c.operator||"gt"} onChange={e=>change(i,"operator",e.target.value)}><option value="gt">大于</option><option value="lt">小于</option><option value="eq">等于</option></select></Field></div><Field label="阈值"><input type="number" step="any" value={c.threshold??0} onChange={e=>change(i,"threshold",Number(e.target.value))}/></Field><Field label="核查建议"><input value={c.suggestion||""} onChange={e=>change(i,"suggestion",e.target.value)}/></Field><button className="ra-button" title="删除条件" onClick={()=>setJson(JSON.stringify(conditions.filter((_,j)=>j!==i),null,2))}><Trash2 size={15}/></button></div>)}<button className="ra-button" disabled={invalid} onClick={()=>setJson(JSON.stringify([...conditions,{title:"新检查",field:"metrics.debt_ratio",operator:"gt",threshold:0.7,suggestion:"请核实"}],null,2))}><Plus size={15}/>添加条件</button><details className="ra-note"><summary>高级 JSON 配置</summary><Field label="条件 JSON"><textarea className="ra-code" value={json} onChange={e=>setJson(e.target.value)}/></Field></details></>
}
export function AccessDialog({close,connected}:{close:()=>void;connected:()=>void}){const [token,setToken]=useState("");return <div className="ra-overlay"><section className="ra-confirm" role="dialog" aria-modal="true"><button style={{float:"right"}} title="关闭身份窗口" onClick={close}><X size={18}/></button><ShieldCheck size={24}/><h2>访问身份</h2><Field label="访问令牌"><input type="password" autoComplete="off" value={token} onChange={e=>setToken(e.target.value)}/></Field><p>令牌由系统管理员分配。身份与项目权限在服务器校验。</p><div><button className="ra-button" onClick={()=>{sessionStorage.removeItem("xingbang-access-token");connected()}}>退出身份</button><button className="ra-button primary" onClick={()=>{sessionStorage.setItem("xingbang-access-token",token);connected()}}>连接</button></div></section></div>}
export function Reviews({items,busy,review}:{items:any[];busy:boolean;review:(id:string,approve:boolean,note:string)=>void}){const [notes,setNotes]=useState<Record<string,string>>({});return <section>{items.map(r=><article className="ra-review-row" key={r.id}><h3>{r.snapshot.name} · {r.snapshot.version}</h3><p className="ra-note">提交人 {r.submitted_by} · {new Date(r.time).toLocaleString()} · {r.status}</p><details className="ra-note"><summary>查看本次提交内容</summary><pre className="ra-result-json">{JSON.stringify(r.snapshot,null,2)}</pre></details>{r.status==="pending"?<><Field label="审核意见"><input value={notes[r.id]||""} onChange={e=>setNotes({...notes,[r.id]:e.target.value})}/></Field><button className="ra-button" disabled={busy||!notes[r.id]?.trim()} onClick={()=>review(r.id,false,notes[r.id])}>驳回</button> <button className="ra-button primary" disabled={busy||!notes[r.id]?.trim()} onClick={()=>review(r.id,true,notes[r.id])}>审核并发布</button></>:<p className="ra-note">{r.reviewed_by}:{r.note}</p>}</article>)}{!items.length&&<p className="ra-empty">暂无审核记录</p>}</section>}
export function ResultExtras({result,previous}:{result:any;previous:any}){const exportResult=()=>{const blob=new Blob([JSON.stringify(result,null,2)],{type:"application/json"});const url=URL.createObjectURL(blob);const a=document.createElement("a");a.href=url;a.download=`risk-${result.project_id}-${result.id.slice(0,8)}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)};const changes=previous&&previous.project_id===result.project_id?Object.keys({...previous.values,...result.values}).filter(k=>previous.values?.[k]!==result.values?.[k]):[];return <div><button className="ra-button" onClick={exportResult}><Download size={15}/>导出建议与证据</button>{result.summary?.incomplete&&<p className="ra-alert error">本次存在信息缺失或未完成检查,请补充或核查后重新研判。</p>}{previous&&previous.project_id===result.project_id&&<details className="ra-note"><summary>与上次运行比较:{changes.length} 项数据变化</summary>{changes.map(k=><p key={k}>{k}:{String(previous.values?.[k]??"缺失")} → {String(result.values?.[k]??"缺失")}</p>)}</details>}</div>}
+77
View File
@@ -0,0 +1,77 @@
"use client";
import { useEffect, useState } from "react";
import { Activity, ArrowLeft, ArrowRight, Boxes, Check, CheckCircle2, ChevronRight, CircleDashed, Copy, Database, FileCode2, History, Layers3, Loader2, Pencil, Play, Plus, Search, ShieldCheck, Sparkles, Trash2, Workflow, X } from "lucide-react";
import "./workspace.css";
import {ConnectionFields, AlignmentFields, RuleFields, AccessDialog, Reviews, ResultExtras} from "./Enhancements";
type Item = { id: string; name: string; description?: string; status: string; version: string; [key: string]: any };
type Kind = "connections" | "cards" | "skills" | "rules" | "plans";
type Catalog = Record<Kind, Item[]>;
const EMPTY: Catalog = {connections:[],cards:[],skills:[],rules:[],plans:[]};
const META = {connections:{label:"数据连接",icon:Database},cards:{label:"数据卡",icon:Layers3},skills:{label:"Skills 能力库",icon:Sparkles},rules:{label:"规则集",icon:ShieldCheck},plans:{label:"研判方案",icon:Workflow}};
const STATUS: Record<string,string> = {published:"已发布",draft:"草稿",archived:"已归档"};
const API = "/api/risk-agent";
async function api(path: string, method="GET", body?: unknown) {
const r=await fetch(API+path,{method,headers:{"Content-Type":"application/json",...(typeof window!=="undefined"&&sessionStorage.getItem("xingbang-access-token")?{"Authorization":"Bearer "+sessionStorage.getItem("xingbang-access-token")}:{})},body:body===undefined?undefined:JSON.stringify(body)});
const data=await r.json();
if(!r.ok) throw Error(typeof data.detail==="string"?data.detail:JSON.stringify(data.detail||data));
return data;
}
const defaults = (kind:Kind): Record<string,unknown> => ({connections:{type:"demo",host:"",database:""},cards:{connection:"conn-erp",sql:"SELECT * FROM finance WHERE project_id = :project_id",mapping:{"financial.total_assets":"assets","financial.total_liabilities":"liabilities"},skills:[],grain:"客户 + 期间 + 口径",unit:"万元",period:"2025",scope:"合并"},skills:{engine:"quality",inputs:["*"],output:"quality.findings",instructions:"识别空值与缺失字段"},rules:{conditions:[]},plans:{cards:[],skills:[],rules:[]}}[kind]);
export default function RiskAgentConsole() {
const [catalog,setCatalog]=useState<Catalog>(EMPTY),[view,setView]=useState<Kind|"run"|"history"|"reviews">("cards");
const [search,setSearch]=useState(""),[status,setStatus]=useState("active"),[loading,setLoading]=useState(true),[busy,setBusy]=useState(false),[error,setError]=useState(""),[notice,setNotice]=useState("");
const [editor,setEditor]=useState<{kind:Kind;item:Item;fresh:boolean}|null>(null),[advanced,setAdvanced]=useState(false),[json,setJson]=useState(""),[confirmDelete,setConfirmDelete]=useState<{kind:Kind;item:Item}|null>(null);
const [project,setProject]=useState("XB-2026-001"),[plan,setPlan]=useState("plan-default"),[result,setResult]=useState<any>(null),[history,setHistory]=useState<any[]>([]),[test,setTest]=useState<any>(null);
const [nativeSkills,setNativeSkills]=useState<any[]>([]);
async function reload() { setCatalog(await api("/catalog")); }
const [access,setAccess]=useState<any>({role:"admin",preview:true}),[accessOpen,setAccessOpen]=useState(false),[reviewItems,setReviewItems]=useState<any[]>([]),[previous,setPrevious]=useState<any>(null);
const canManage=access.role!=="manager", canEdit=access.role==="admin";
async function connectIdentity(){try{const user=await api("/access/context");setAccess(user);setResult(null);setHistory([]);setEditor(null);setView(user.role==="manager"?"run":"cards");await reload();if(user.role!=="manager")setNativeSkills(await api("/native-skills/list"));setAccessOpen(false);setError("")}catch(e:any){setError(e.message);setCatalog(EMPTY);setAccessOpen(true)}finally{setLoading(false)}}
useEffect(()=>{connectIdentity()},[]);
async function perform(fn:()=>Promise<void>){setBusy(true);setError("");setNotice("");try{await fn()}catch(e:any){setError(e.message)}finally{setBusy(false)}}
function open(kind:Kind,item?:Item) {
const value=item?structuredClone(item):{...defaults(kind),id:"",name:"",description:"",status:"draft",version:"v1"} as Item;
setEditor({kind,item:value,fresh:!item});setAdvanced(false);setTest(null);
setJson(JSON.stringify(kind==="cards"?value.mapping||{}:kind==="rules"?value.conditions||[]:{},null,2));
}
function field(key:string,value:unknown){setEditor(e=>e?{...e,item:{...e.item,[key]:value}}:e)}
function toggle(key:string,id:string){if(editor)field(key,(editor.item[key]||[]).includes(id)?editor.item[key].filter((x:string)=>x!==id):[...(editor.item[key]||[]),id])}
async function save(publish=false){if(!editor)return;await perform(async()=>{
const data={...editor.item};if(editor.kind==="cards")data.mapping=JSON.parse(json);if(editor.kind==="rules")data.conditions=JSON.parse(json);
data.status=publish&&access.preview?"published":"draft";
const saved=await api(`/${editor.kind}${editor.fresh?"":"/"+data.id}`,editor.fresh?"POST":"PUT",{name:data.name,data});
if(editor.fresh&&publish&&access.preview)await api(`/${editor.kind}/${saved.id}`,"PUT",{name:saved.name,data:{...saved,status:"published"}});
if(publish&&!access.preview)await api(`/${editor.kind}/${saved.id}/submit`,"POST");
await reload();setEditor(null);setNotice(publish?(access.preview?"配置已发布,后续运行使用新版本。":"已提交审核,独立审核员批准后生效。"):"草稿已保存。");
})}
const active=(kind:Kind)=>catalog[kind].filter(x=>x.status!=="archived");
const title=view==="reviews"?"发布审核":view==="run"?"项目提交前自查":view==="history"?"研判运行记录":META[view as Kind].label;
const subtitle=view==="cards"?"把业务数据封装成可复用的数据卡,连接每一次风险研判。":view==="skills"?"组合专业检查、确定性计算与语义分析能力。":view==="plans"?"选择数据、能力与规则,形成客户经理的一键自查方案。":view==="run"?"项目事实、专业检查与制度规则在同一条证据链上汇合。":view==="rules"?"管理检查条件与核查建议,保留每一个制度版本。":view==="connections"?"统一维护连接配置,由数据卡复用。":view==="reviews"?"核对提交版本,由独立审核员发布或驳回。":"回看数据快照、配置版本、执行结果与证据。";
const items=view in META?catalog[view as Kind].filter(x=>(status==="all"||x.status!=="archived")&&(x.name+" "+x.description).toLowerCase().includes(search.toLowerCase())):[];
return <div className="ra-app">
<aside className="ra-sidebar"><a href="/" className="ra-brand"><div className="ra-brand-mark"><Activity size={22}/></div><div>兴邦金租<small>RISK INTELLIGENCE</small></div></a><div className="ra-side-label">客户经理</div><button className={view==="run"?"active":""} onClick={()=>setView("run")}><ShieldCheck size={18}/>项目风险自查</button><button className={view==="history"?"active":""} onClick={()=>{setView("history");perform(async()=>setHistory(await api("/runs/history")))}}><History size={18}/>运行记录</button><div className="ra-side-label">配置管理</div>{(canManage?Object.keys(META) as Kind[]:[]).map(k=>{const Icon=META[k].icon;return <button key={k} className={view===k?"active":""} onClick={()=>{setView(k);setSearch("")}}><Icon size={18}/>{META[k].label}<span>{active(k).length}</span></button>})}{canManage&&<button className={view==="reviews"?"active":""} onClick={()=>{setView("reviews");perform(async()=>setReviewItems(await api("/reviews/list")))}}><ShieldCheck size={18}/>发布审核</button>}<div className="ra-sidebar-bottom"><div className="ra-env-dot"/>本地业务验证环境<small>示例数据 · 演示规则</small><a href="/xingbang-unified"><ArrowLeft size={14}/>返回评审工作台</a></div></aside>
<div className="ra-body"><header className="ra-top"><div>风险研判 Agent <ChevronRight size={14}/><b>{title}</b></div><span className="ra-badge">业务验证版</span><button className="ra-button" onClick={()=>setAccessOpen(true)}>{access.preview?"本机预览":({admin:"配置管理员",manager:"客户经理",reviewer:"发布审核员"} as Record<string,string>)[access.role]}</button></header>
<main className="ra-main"><div className="ra-heading"><div><div className="ra-eyebrow">RISK AGENT / {view.toUpperCase()}</div><h1>{title}</h1><p>{subtitle}</p></div>{canEdit&&view in META&&<button className="ra-button primary" onClick={()=>open(view as Kind)}><Plus size={16}/>新建{META[view as Kind].label}</button>}</div>
{error&&<div role="alert" className="ra-alert error">{error}<button title="关闭错误" onClick={()=>setError("")}><X size={16}/></button></div>}{notice&&<div role="status" className="ra-alert">{notice}<button title="关闭提示" onClick={()=>setNotice("")}><X size={16}/></button></div>}
<div className="ra-pipeline">{[{label:"业务数据",sub:`${active("cards").length} 张数据卡`,icon:Database},{label:"专业能力",sub:`${active("skills").length} 项 Skills`,icon:Sparkles},{label:"制度规则",sub:`${active("rules").length} 套规则集`,icon:ShieldCheck},{label:"研判方案",sub:`${active("plans").length} 个方案`,icon:Workflow}].map((x,i)=><div key={x.label} className="ra-pipeline-step"><span className={`ra-stage-icon color-${i}`}><x.icon size={19}/></span><div><b>{x.label}</b><small>{x.sub}</small></div>{i<3&&<ArrowRight className="ra-pipeline-arrow" size={18}/>}</div>)}</div>
{loading?<div className="ra-empty"><Loader2 className="ra-spin"/>正在读取配置…</div>:view in META?<><div className="ra-toolbar"><div className="ra-search"><Search size={16}/><input aria-label="搜索配置" placeholder="搜索名称或业务描述…" value={search} onChange={e=>setSearch(e.target.value)}/></div><select aria-label="状态筛选" value={status} onChange={e=>setStatus(e.target.value)}><option value="active">有效配置</option><option value="all">包含归档</option></select><span>{items.length} 个配置</span></div><div className="ra-card-grid">{items.map(item=>{const kind=view as Kind,Icon=META[kind].icon;return <article className="ra-card" key={item.id}><div className="ra-card-top"><div className={`ra-object-icon ${kind}`}><Icon size={23}/></div><span className={`ra-status ${item.status}`}><i/>{STATUS[item.status]}</span></div><h2>{item.name}</h2><p>{item.description||"尚未填写业务描述"}</p>{kind==="cards"?<><div className="ra-chips"><span>{catalog.connections.find(c=>c.id===item.connection)?.name||"未选择连接"}</span><span>{Object.keys(item.mapping||{}).length} 个字段</span></div><div className="ra-card-detail"><span>记录粒度</span><b>{item.grain||"未定义"}</b></div><div className="ra-card-detail"><span>关联 Skills</span><b>{(item.skills||[]).length} 项</b></div></>:kind==="skills"?<><div className="ra-chips"><span>{{quality:"质量检查",ratio:"确定性计算",overdue:"征信检查",model:"本地模型"}[item.engine as string]||item.engine}</span></div><div className="ra-card-detail"><span>输入依赖</span><b>{(item.inputs||[]).length} 个字段</b></div></>:kind==="plans"?<><div className="ra-plan-counts">{["cards","skills","rules"].map((k,i)=><div key={k}><b>{(item[k]||[]).length}</b><small>{["数据卡","Skills","规则集"][i]}</small></div>)}</div></>:kind==="rules"?<div className="ra-card-detail"><span>配置条件</span><b>{(item.conditions||[]).length} 条</b></div>:<><div className="ra-chips"><span>{item.type==="demo"?"演示数据库":item.type}</span></div><div className="ra-card-detail"><span>连接状态</span><b>{item.type==="demo"?"本地样例可运行":"连接器待接入"}</b></div></>}
<footer><span>{item.version}</span><div><button disabled={!canEdit} title="复制为草稿" onClick={()=>{open(kind,{...item,id:"",name:item.name+" 副本",status:"draft"});setEditor(e=>e?{...e,fresh:true}:e)}}><Copy size={15}/></button><button disabled={!canEdit} title="归档配置" onClick={()=>setConfirmDelete({kind,item})}><Trash2 size={15}/></button><button disabled={!canEdit} className="ra-edit" onClick={()=>open(kind,item)}><Pencil size={14}/>配置</button></div></footer></article>})}<button disabled={!canEdit} className="ra-add-card" onClick={()=>open(view as Kind)}><Plus size={27}/><b>添加{META[view as Kind].label}</b><span>扩展你的研判能力</span></button></div></>:view==="run"?<><section className="ra-run-form"><label>项目编号<input value={project} onChange={e=>setProject(e.target.value)}/></label><label>研判方案<select value={plan} onChange={e=>setPlan(e.target.value)}>{active("plans").map(p=><option key={p.id} value={p.id}>{p.name} · {STATUS[p.status]}</option>)}</select></label><button disabled={busy} className="ra-button primary" onClick={()=>perform(async()=>{setPrevious(result);setResult(await api(`/plans/${plan}/run`,"POST",{project_id:project}))})}>{busy?<Loader2 className="ra-spin" size={16}/>:<Play size={16}/>}开始风险自查</button></section><p className="ra-note">示例项目 XB-2026-001。数据库查询与指标计算真实执行;尚未接入兴邦 ERP 与正式制度,本地模型需配置内网端点。</p>{result?<><ResultExtras result={result} previous={previous}/><Results result={result}/></>:<div className="ra-empty"><ShieldCheck size={36}/><h2>从项目事实开始研判</h2><p>选择已发布方案,查看检查结果与来源证据。</p></div>}</>:view==="reviews"?<Reviews items={reviewItems} busy={busy} review={(id,approve,note)=>perform(async()=>{await api(`/reviews/${id}/review`,"POST",{approve,note});setReviewItems(await api("/reviews/list"));await reload();setNotice(approve?"已审核发布":"已驳回")})}/>:<><div className="ra-history">{history.map(r=><button key={r.id} onClick={()=>{setResult(r);setView("run")}}><History size={18}/><div><b>{r.project_id} · {r.plan.name}</b><small>{new Date(r.time).toLocaleString()} · {r.mode}</small></div><span>{r.findings.length} 项关注</span><ChevronRight size={16}/></button>)}</div>{!history.length&&<div className="ra-empty">暂无运行记录</div>}</>}
</main></div>
{editor&&<div className="ra-overlay"><section className="ra-drawer" role="dialog" aria-modal="true" aria-label="配置编辑"><header><div><small>{META[editor.kind].label} / {editor.fresh?"新建":"编辑"}</small><h2>{editor.item.name||"新配置"}</h2></div><button title="关闭编辑" onClick={()=>setEditor(null)}><X size={22}/></button></header><div className="ra-drawer-body">{error&&<div className="ra-alert error">{error}</div>}<div className="ra-two"><Field label="名称"><input value={editor.item.name} onChange={e=>field("name",e.target.value)}/></Field><Field label="版本 / 状态"><input readOnly value={`${editor.item.version} · ${STATUS[editor.item.status]}`}/></Field></div><Field label="业务描述"><textarea value={editor.item.description||""} onChange={e=>field("description",e.target.value)}/></Field>
{editor.kind==="connections"&&<ConnectionFields item={editor.item} field={field}/>}
{editor.kind==="cards"&&<><Field label="复用数据连接"><select value={editor.item.connection||""} onChange={e=>field("connection",e.target.value)}><option value="">请选择</option>{active("connections").map(c=><option key={c.id} value={c.id}>{c.name}</option>)}</select></Field><Field label="记录粒度"><input value={editor.item.grain||""} onChange={e=>field("grain",e.target.value)}/></Field><div className="ra-two"><Field label="金额单位"><input value={editor.item.unit||""} onChange={e=>field("unit",e.target.value)}/></Field><Field label="报表期间"><input value={editor.item.period||""} onChange={e=>field("period",e.target.value)}/></Field></div><Field label="财务口径"><input value={editor.item.scope||""} onChange={e=>field("scope",e.target.value)}/></Field><AlignmentFields item={editor.item} field={field}/><CheckList title="关联 Skills" items={active("skills")} checked={editor.item.skills||[]} onToggle={id=>toggle("skills",id)}/><button className="ra-button" onClick={()=>setAdvanced(!advanced)}><FileCode2 size={15}/>{advanced?"收起":"展开"} SQL 与字段映射</button>{advanced&&<><Field label="参数化 SQL · :project_id"><textarea className="ra-code" value={editor.item.sql||""} onChange={e=>field("sql",e.target.value)}/></Field><Field label="字段映射 · 标准字段 → 源字段 (JSON)"><textarea className="ra-code" value={json} onChange={e=>setJson(e.target.value)}/></Field></>}<button disabled={busy||editor.fresh} className="ra-button" onClick={()=>perform(async()=>setTest(await api(`/cards/${editor.item.id}/test`,"POST",{project_id:project})))}><Play size={15}/>测试已保存版本</button>{test&&<pre className="ra-result-json">{JSON.stringify(test,null,2)}</pre>}</>}
{editor.kind==="skills"&&<><Field label="执行器"><select value={editor.item.engine} onChange={e=>field("engine",e.target.value)}><option value="quality">数据质量检查</option><option value="ratio">比率计算(Python)</option><option value="overdue">征信输入检查</option><option value="model">本地模型</option></select></Field><Field label="所需标准字段 · 每行一个"><textarea value={(editor.item.inputs||[]).join("\n")} onChange={e=>field("inputs",e.target.value.split("\n"))}/></Field>{editor.item.engine==="ratio"&&<div className="ra-two"><Field label="分子字段"><input value={editor.item.numerator||""} onChange={e=>field("numerator",e.target.value)}/></Field><Field label="分母字段"><input value={editor.item.denominator||""} onChange={e=>field("denominator",e.target.value)}/></Field></div>}<Field label="输出字段"><input value={editor.item.output||""} onChange={e=>field("output",e.target.value)}/></Field>{editor.item.engine==="model"&&<CheckList title="mini-openclaw 原生 Skills" items={nativeSkills.map(s=>({...s,status:"published",version:"SKILL.md"}))} checked={editor.item.native_skills||[]} onToggle={id=>toggle("native_skills",id)}/>}<Field label="业务说明 / 模型指令"><textarea value={editor.item.instructions||""} onChange={e=>field("instructions",e.target.value)}/></Field><p className="ra-note">确定性执行器依据结构化字段运行;模型指令在配置本地模型端点后执行。当前不支持上传任意 Python 脚本。</p></>}
{editor.kind==="rules"&&<RuleFields item={editor.item} field={field} json={json} setJson={setJson}/>}
{editor.kind==="plans"&&<><CheckList title="选择数据卡" items={active("cards")} checked={editor.item.cards||[]} onToggle={id=>toggle("cards",id)}/><CheckList title="选择 Skills" items={active("skills")} checked={editor.item.skills||[]} onToggle={id=>toggle("skills",id)}/><CheckList title="选择规则集" items={active("rules")} checked={editor.item.rules||[]} onToggle={id=>toggle("rules",id)}/><Dependency plan={editor.item} catalog={catalog}/></>}
</div><footer><button className="ra-button" onClick={()=>setEditor(null)}>取消</button><button disabled={busy||!editor.item.name.trim()} className="ra-button" onClick={()=>save()}>保存草稿</button><button disabled={busy||!editor.item.name.trim()} className="ra-button primary" onClick={()=>save(true)}><Check size={16}/>{access.preview?"保存并发布":"保存并提交审核"}</button></footer></section></div>}
{confirmDelete&&<div className="ra-overlay"><section className="ra-confirm" role="dialog" aria-modal="true"><Trash2 size={24}/><h2>归档「{confirmDelete.item.name}」?</h2><p>配置将停止使用,历史运行快照保留。被其他有效配置引用时需要先解除关联。</p><div><button className="ra-button" onClick={()=>setConfirmDelete(null)}>取消</button><button disabled={busy} className="ra-button primary" onClick={()=>perform(async()=>{await api(`/${confirmDelete.kind}/${confirmDelete.item.id}`,"DELETE");await reload();setConfirmDelete(null);setNotice("配置已归档")})}>确认归档</button></div>{error&&<p role="alert">{error}</p>}</section></div>}
{accessOpen&&<AccessDialog close={()=>setAccessOpen(false)} connected={connectIdentity}/>}
</div>;
}
function Field({label,children}:{label:string;children:React.ReactNode}){return <label className="ra-field"><span>{label}</span>{children}</label>}
function CheckList({title,items,checked,onToggle}:{title:string;items:Item[];checked:string[];onToggle:(id:string)=>void}){return <section className="ra-checks"><h3>{title}<span>{checked.length} 已选</span></h3>{items.map(item=><label key={item.id}><input type="checkbox" checked={checked.includes(item.id)} onChange={()=>onToggle(item.id)}/><div><b>{item.name}</b><small>{item.description}</small></div><span className={`ra-status ${item.status}`}>{STATUS[item.status]}</span></label>)}</section>}
function Dependency({plan,catalog}:{plan:Item;catalog:Catalog}){const fields=new Set(catalog.cards.filter(c=>(plan.cards||[]).includes(c.id)).flatMap(c=>Object.keys(c.mapping||{})));const issues=catalog.skills.filter(s=>(plan.skills||[]).includes(s.id)).flatMap(s=>(s.inputs||[]).filter((f:string)=>f!=="*"&&!fields.has(f)).map((f:string)=>s.name+" 缺少 "+f));return <div className={"ra-dependency "+(issues.length?"warning":"")}><h3>输入依赖检查</h3>{issues.length?issues.map((x:string)=><p key={x}>{x}</p>):<p>所选 Skills 的声明字段均有数据卡提供。</p>}</div>}
function Results({result}:{result:any}){return <div className="ra-results"><section className="ra-findings"><div className="ra-section-heading"><h2>研判发现 <span>{result.findings.length}</span></h2><span className="ra-badge">{result.mode}</span></div>{result.findings.map((f:any,i:number)=><article key={i}><div className="ra-finding-title"><span className={"ra-finding-dot "+(f.category==="信息缺失"?"missing":"")}/><h3>{f.title}</h3><span>{f.category}</span></div><p>{f.suggestion}</p>{f.value!==undefined&&<p>计算值:<b>{Number(f.value).toFixed(4)}</b></p>}<details><summary>证据与规则 <ChevronRight size={13}/></summary><pre>{JSON.stringify({来源:f.evidence,规则:f.rule,条件:f.condition},null,2)}</pre></details></article>)}{result.advisories?.map((a:any,i:number)=><article key={`model-${i}`}><h3>{a.skill}</h3><span className="ra-badge">{a.status}</span><p style={{whiteSpace:"pre-wrap"}}>{a.text}</p></article>)}{!result.findings.length&&<p className="ra-note">当前检查未发现异常;不代表项目无风险。</p>}</section><aside className="ra-trace"><h2>执行轨迹</h2>{result.agent_traces?.map((trace:any,i:number)=><details key={`core-${i}`}><summary><Workflow size={17}/><span><b>mini-openclaw Agent 内核</b><small>{trace.skill} · 工具调用轨迹</small></span></summary>{trace.messages.filter((m:any)=>m.type!=="human").map((m:any,j:number)=><div key={j}><b>{m.type} {m.data.name||""}</b><pre>{JSON.stringify({content:m.data.content,tool_calls:m.data.tool_calls},null,2)}</pre></div>)}</details>)}<p className="ra-note">{new Date(result.time).toLocaleString()}</p>{result.steps.map((s:any,i:number)=><details key={i}><summary>{s.status==="完成"?<CheckCircle2 size={17}/>:<CircleDashed size={17}/>}<span><b>{s.name}</b><small>{s.status} · {s.version}</small></span></summary><p>{s.detail}</p>{s.rows&&<pre>{JSON.stringify(s.rows,null,2)}</pre>}</details>)}<p className="ra-note">运行 ID:{result.id}</p></aside></div>}
File diff suppressed because one or more lines are too long
+40 -182
View File
@@ -1,186 +1,44 @@
'use client'
import React from 'react'
import { Shield, FileSearch, TrendingUp, CheckCircle } from 'lucide-react'
const AGENTS = [
{
id: 1,
name: '风险研判助手',
icon: '🎯',
description: '双轨驱动:规则引擎 + 历史数据分析',
features: ['智能风险识别', '历史案例匹配', '智能决策推荐'],
status: 'active',
route: '/risk-assess',
color: 'from-blue-600 to-cyan-600'
},
{
id: 2,
name: '报告助手',
icon: '📝',
description: '智能报告生成与模板管理',
features: ['报告模板设计', '一键生成评审报告', '多模板快速切换'],
status: 'active',
route: '/report-assistant',
color: 'from-purple-600 to-pink-600'
},
{
id: 3,
name: '评审调配助手',
icon: '🎯',
description: '智能评审员分配 + 关键指标测算',
features: ['智能评审员匹配', '工作负荷平衡', '财务指标测算'],
status: 'active',
route: '/review-allocation',
color: 'from-orange-600 to-red-600'
},
{
id: 4,
name: '保单条款智能核验',
icon: '🛡️',
description: 'OCR识别 + 条款解析 + 合规核验',
features: ['保单PDF智能识别', '除外责任风险分析', '多模板核验报告'],
status: 'active',
route: '/policy-verification',
color: 'from-green-600 to-teal-600'
"use client";
import { useEffect, useState } from "react";
const API = `http://${typeof window === 'undefined' ? 'localhost' : window.location.hostname}:8002/api/xingbang/workspace`;
const labels: Record<string,string> = {name:'项目名称',client:'客户名称',amount:'授信金额(万元)',assets:'资产总额(万元)',liabilities:'负债总额(万元)',lease_value:'租赁物价值(万元)',insurance:'保额(万元)'};
export default function Page() {
const [projects,setProjects]=useState<any[]>([]), [project,setProject]=useState<any>(null);
const [error,setError]=useState(''), [busy,setBusy]=useState(false), [tab,setTab]=useState('风险研判');
const [note,setNote]=useState(''), [actor,setActor]=useState('演示主审员'), [amount,setAmount]=useState('');
const [form,setForm]=useState<Record<string,string>>({name:'演示光伏项目',client:'示例客户',amount:'5000',assets:'18000',liabilities:'13200',lease_value:'5400',insurance:''});
async function request(path:string, body?:any) {
const r=await fetch(API+path,body?{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}:undefined);
const data=await r.json(); if(!r.ok) throw new Error(typeof data.detail==='string'?data.detail:JSON.stringify(data.detail)); return data;
}
]
export default function XingbangUnifiedPage() {
const handleAgentClick = (agent: typeof AGENTS[0]) => {
if (agent.route) {
window.location.href = agent.route
}
useEffect(()=>{request('/projects').then(data=>{setProjects(data);setProject(data[0]||null)}).catch(e=>setError(e.message));},[]);
async function run(kind:string,target='') {
setBusy(true);setError('');
try {
let data: any;
if(kind==='create') {
const body:Record<string,any>={...form};
Object.keys(body).filter(k=>!['name','client'].includes(k)).forEach(k=>body[k]=form[k]===''?null:Number(form[k]));
data=await request('/projects',body);setProjects(xs=>[data,...xs]);
} else {data=await request(`/projects/${project.id}/actions`,{kind,target,note,actor,amount:amount?Number(amount):null});setProjects(xs=>xs.map(x=>x.id===data.id?data:x));}
setProject(data);setNote('');
}catch(e:any){setError(e.message)}finally{setBusy(false)}
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
{/* 导航栏 */}
<nav className="sticky top-0 z-50 backdrop-blur-md bg-slate-900/80 border-b border-slate-700">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-600 to-cyan-600 flex items-center justify-center shadow-lg">
<span className="text-white text-lg font-bold">兴</span>
</div>
<div>
<h1 className="text-lg font-bold text-white">兴邦金租智能评审工作台</h1>
<p className="text-xs text-slate-400">AI-Powered Risk Assessment Platform</p>
</div>
</div>
<a
href="/"
className="px-4 py-2 text-sm text-slate-400 hover:text-white hover:bg-slate-800 rounded-lg transition-colors"
>
返回主页
</a>
</div>
</nav>
{/* 主内容区 */}
<div className="max-w-7xl mx-auto px-6 py-12">
{/* 页面标题 */}
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-white mb-4">
四大智能助手
</h2>
<p className="text-xl text-slate-400">
AI驱动的金融风控决策系统
</p>
</div>
{/* Agent 卡片网格 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{AGENTS.map(agent => (
<div
key={agent.id}
onClick={() => handleAgentClick(agent)}
className={`group relative overflow-hidden rounded-2xl border border-slate-700 bg-slate-800/50 backdrop-blur-sm transition-all duration-300 ${
agent.status === 'active'
? 'hover:scale-[1.02] hover:shadow-2xl hover:border-cyan-500 cursor-pointer'
: 'opacity-60 cursor-not-allowed'
}`}
>
{/* 渐变背景装饰 */}
<div className={`absolute top-0 right-0 w-64 h-64 bg-gradient-to-br ${agent.color} opacity-10 blur-3xl group-hover:opacity-20 transition-opacity`} />
<div className="relative p-8">
{/* 图标和状态 */}
<div className="flex items-start justify-between mb-6">
<div className="flex items-center gap-4">
<div className={`w-16 h-16 rounded-xl bg-gradient-to-br ${agent.color} flex items-center justify-center text-3xl shadow-lg`}>
{agent.icon}
</div>
<div>
<h3 className="text-2xl font-bold text-white mb-1">
{agent.name}
</h3>
<p className="text-sm text-slate-400">
{agent.description}
</p>
</div>
</div>
{agent.status === 'active' && (
<div className="px-3 py-1 bg-green-500/20 border border-green-500 rounded-full text-xs font-bold text-green-400">
已启用
</div>
)}
{agent.status === 'planned' && (
<div className="px-3 py-1 bg-slate-700 border border-slate-600 rounded-full text-xs font-bold text-slate-400">
规划中
</div>
)}
</div>
{/* 功能特性 */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-slate-400 uppercase tracking-wider">核心功能</h4>
<ul className="space-y-2">
{agent.features.map((feature, idx) => (
<li key={idx} className="flex items-center gap-2 text-slate-300">
<CheckCircle className="w-4 h-4 text-cyan-400 flex-shrink-0" />
<span className="text-sm">{feature}</span>
</li>
))}
</ul>
</div>
{/* 操作按钮 */}
{agent.status === 'active' ? (
<div className="mt-6 pt-6 border-t border-slate-700">
<button className={`w-full py-3 rounded-lg font-bold text-white bg-gradient-to-r ${agent.color} hover:shadow-lg transition-all`}>
进入助手 →
</button>
</div>
) : (
<div className="mt-6 pt-6 border-t border-slate-700">
<div className="w-full py-3 rounded-lg font-bold text-center text-slate-500 bg-slate-700/50">
即将推出
</div>
</div>
)}
</div>
</div>
))}
</div>
{/* 底部说明 */}
<div className="mt-12 p-6 rounded-xl bg-slate-800/30 border border-slate-700">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-blue-600/20 flex items-center justify-center flex-shrink-0">
<Shield className="w-5 h-5 text-blue-400" />
</div>
<div>
<h3 className="text-lg font-bold text-white mb-2">关于智能评审工作台</h3>
<p className="text-sm text-slate-400 leading-relaxed">
兴邦金租智能评审工作台由四大AI助手组成,覆盖从风险研判到决策支持的完整流程。
每个助手都采用深度学习和规则引擎相结合的方式,确保决策的准确性和可解释性。
Agent 1 风险研判助手已正式上线,其他助手正在紧张开发中。
</p>
</div>
</div>
</div>
</div>
const button=(text:string,kind:string,target='')=><button disabled={busy} className="border rounded px-3 py-2 text-sm bg-white hover:bg-gray-100 disabled:opacity-40" onClick={()=>run(kind,target)}>{text}</button>;
const snapshot=project?.snapshots?.at(-1);
return <main className="min-h-screen bg-slate-100 text-slate-900">
<header className="bg-white border-b p-5"><h1 className="text-xl font-bold">兴邦金租 · 智能评审工作台</h1><p className="text-sm text-amber-800 mt-1">业务验证原型 · 人工录入 / 演示阈值 · 未接入 ERP、本地模型及正式权限体系</p></header>
<div className="grid lg:grid-cols-[260px_minmax(0,1fr)] gap-5 p-5">
<aside className="bg-white border rounded p-4"><h2 className="font-bold mb-3">项目列表</h2>{projects.map(x=><button key={x.id} className={`block text-left w-full p-2 rounded ${project?.id===x.id?'bg-blue-50 text-blue-800':''}`} onClick={()=>{setProject(x);setError('')}}>{x.name}</button>)}<h3 className="font-bold mt-6">新建验证项目</h3>{Object.keys(form).map(k=><label key={k} className="block text-xs mt-3">{labels[k]}<input className="border rounded p-2 w-full mt-1" value={form[k]} onChange={e=>setForm({...form,[k]:e.target.value})}/></label>)}<p className="text-xs text-gray-500 my-3">数值留空表示缺失,不作为零处理。</p>{button('建立项目','create')}</aside>
<section className="min-w-0">{error&&<p role="alert" className="bg-red-50 border border-red-300 p-3 mb-4">{error}</p>}{project?<>
<div className="bg-white border rounded p-5 mb-4"><h2 className="text-xl font-semibold">{project.name}</h2><p>{project.client} · {project.stale?'当前快照待研判':'已研判'}</p><div className="text-xs text-gray-500 mt-3 break-all">快照 {snapshot.id} · {snapshot.time} · {snapshot.source}</div><details className="text-xs mt-2"><summary>查看原始数据及版本</summary><pre className="whitespace-pre-wrap">{JSON.stringify(snapshot,null,2)}</pre></details></div>
<nav className="flex flex-wrap gap-2 mb-4">{['风险研判','评审协同','租赁物与增信','报告与经验'].map(t=><button key={t} className={`px-4 py-3 rounded ${tab===t?'bg-blue-900 text-white':'bg-white border'}`} onClick={()=>setTab(t)}>{t}</button>)}</nav>
<div className="bg-white border p-4 mb-4 grid sm:grid-cols-2 gap-3"><label className="text-sm">演示操作人<input className="border p-2 w-full" value={actor} onChange={e=>setActor(e.target.value)}/></label><label className="text-sm">判断依据 / 处理说明<input className="border p-2 w-full" value={note} onChange={e=>setNote(e.target.value)}/></label></div>
{(tab==='风险研判'||tab==='租赁物与增信')&&<>{button('基于当前快照研判','assess')}{project.risks.filter((r:any)=>tab==='风险研判'||r.id!=='debt').map((r:any)=><article key={r.id} className="bg-white border rounded p-4 mt-3"><h3 className="font-bold">{r.title}</h3><p>{r.category} · {r.value===null?'数据缺失':r.value}</p><p className="text-sm mt-2">{r.status} {r.note}</p><details className="text-xs my-3"><summary>查看来源、计算公式和演示规则</summary><pre className="whitespace-pre-wrap">{JSON.stringify(r.evidence,null,2)}</pre></details><div className="flex gap-2">{button('确认','confirm',r.id)}{button('驳回','reject',r.id)}{button('转补件','task',r.id)}</div></article>)}{tab==='租赁物与增信'&&<p className="text-sm p-4">本原型仅测算价值和保额比例;权属、保险条款、担保登记和法律可执行性尚未核验。</p>}</>}
{tab==='评审协同'&&<><div className="bg-white border p-4 mb-3"><label>新授信额度(万元)<input className="border p-2 mx-2" value={amount} onChange={e=>setAmount(e.target.value)}/></label>{button('保存方案变更','change')}<ul className="mt-3">{project.impacts?.map((s:string)=><li key={s}>需复核:{s}</li>)}</ul></div>{project.tasks.map((t:any)=><article key={t.id} className="bg-white border p-4 mb-3"><b>{t.risk}</b><p>{t.requirement}</p><p className="text-sm my-2">{t.owner} · {t.status} · {t.note}</p>{t.status==='待提交'&&button('登记材料已提交','submit_task',t.id)}{t.status==='已提交待确认'&&button('确认问题已解决','close_task',t.id)}</article>)}</>}
{tab==='报告与经验'&&<>{button('生成报告草稿','report')}{project.reports.map((r:any)=><article key={r.id} className="bg-white border p-4 my-3"><b>{r.status} · {r.stale?'旧快照,需更新':'当前版本'}</b><pre className="whitespace-pre-wrap text-sm my-3">{r.body}</pre>{!r.stale&&r.status==='草稿'&&button('确认定稿并提取经验卡','finalize',r.id)}</article>)}<h3 className="font-bold mt-5">经验卡</h3>{project.cards.map((c:any)=><article key={c.id} className="bg-white border p-4 mt-3"><p className="mb-2">{c.status} · 来源报告 {c.report.slice(0,8)}</p><details><summary>查看经验事实与审核记录</summary><pre className="whitespace-pre-wrap text-xs">{JSON.stringify(c,null,2)}</pre></details>{c.status!=='已过期/归档'&&button('记录审核意见并进入下一状态','card',c.id)}</article>)}</>}
<details className="mt-5 text-xs"><summary>审计记录({project.audit.length})</summary><pre className="whitespace-pre-wrap">{JSON.stringify(project.audit,null,2)}</pre></details>
</>:<p>从左侧建立项目开始验证。</p>}</section>
</div>
)
</main>;
}
+1 -1
View File
@@ -107,7 +107,7 @@ export default function Sidebar() {
</p>
<div className="space-y-0.5">
<a
href="/risk-assess"
href="/data-access"
className="w-full flex items-center gap-2 px-3 py-2 text-[13px] text-gray-600 hover:bg-white/40 hover:text-gray-800 rounded-lg transition-all"
>
<Shield className="w-3.5 h-3.5" />