chore(v2): 从兴邦预审v2白名单抽取独立可运行基座
按 docs/v2/05-implementation-plan.md §1 候选迁移表与 claude_use/T00 白名单, 从 55e2cdb 抽取 224 个文件(2.48 MB)到独立目录 F:/mini-OpenClaw-v2。 - 排除:凭据(.env/server-deploy.conf/旧DELIVERY.md)、运行数据(xingbang_data 等 4 个 SQLite)、依赖(venv/node_modules 符号链接)、归档(_backup*/test-evidence/ ori_file)、Office 临时文件 - 目标目录独立 git 仓库,初始恢复点;Gitea 远程与分支待 OQ-06 Q2 定案 - 未部署、未启动服务、未调用模型 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import {ChangeEvent,FormEvent,useEffect,useMemo,useState} from "react";
|
||||
import {ArrowUpRight,Bot,FilePlus2,Paperclip,Send,Sparkles,UserRound} from "lucide-react";
|
||||
import AgentNav from "./AgentNav";
|
||||
import IdentitySelector,{ROLE_LABEL} from "./IdentitySelector";
|
||||
import {newRequestId} from "@/lib/request-id";
|
||||
import "./business-assistant.css";
|
||||
|
||||
const ROOT="/api/risk-agent/project-assistant";
|
||||
type Message={id:string;side:"assistant"|"user";text:string;url?:string;objectId?:string};
|
||||
|
||||
function headers(json=true){
|
||||
const token=sessionStorage.getItem("xingbang-access-token"),role=sessionStorage.getItem("xingbang-demo-role");
|
||||
return {...(json?{"Content-Type":"application/json"}:{}),...(token?{Authorization:`Bearer ${token}`}:{}) ,...(role?{"X-Xingbang-Demo-Role":role}:{})};
|
||||
}
|
||||
|
||||
export default function BusinessAssistant(){
|
||||
const [projects,setProjects]=useState<any[]>([]),[identity,setIdentity]=useState<any>(null),[projectId,setProjectId]=useState("");
|
||||
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]);
|
||||
|
||||
async function load(){
|
||||
setError("");
|
||||
try{const response=await fetch(`${ROOT}/projects`,{headers:headers()});const body=await response.json();if(!response.ok)throw Error(body.detail||"身份或项目读取失败");setProjects(body.projects);setIdentity(body.identity);const requested=new URLSearchParams(location.search).get("project");setProjectId(current=>current||(requested&&body.projects.some((p:any)=>p.id===requested)?requested:body.projects[0]?.id||""));}
|
||||
catch(e:any){setError(e.message)}
|
||||
}
|
||||
useEffect(()=>{load()},[]);
|
||||
async function send(text:string){
|
||||
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();}
|
||||
catch(e:any){setError(e.message);setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:`未执行:${e.message}`}]);}
|
||||
finally{setBusy(false)}
|
||||
}
|
||||
async function attach(event:ChangeEvent<HTMLInputElement>){
|
||||
const file=event.target.files?.[0];event.target.value="";if(!file||!projectId||busy)return;
|
||||
setMessages(old=>[...old,{id:newRequestId(),side:"user",text:`把材料“${file.name}”补充到 ${projectId}`}]);setBusy(true);setError("");
|
||||
try{const form=new FormData();form.append("file",file);const response=await fetch(`${ROOT}/projects/${encodeURIComponent(projectId)}/documents`,{method:"POST",headers:headers(false),body:form});const body=await response.json();if(!response.ok)throw Error(body.detail||"材料上传失败");setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:`材料已归档并解析出 ${body.candidates?.length||0} 个候选字段。为避免误改事实,请到项目页确认来源。`,url:`/xingbang-workbench?project=${encodeURIComponent(projectId)}&tab=findings`,objectId:body.id}]);await load();}
|
||||
catch(e:any){setError(e.message);setMessages(old=>[...old,{id:newRequestId(),side:"assistant",text:`材料未补充:${e.message}`}]);}
|
||||
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>
|
||||
{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>;
|
||||
|
||||
return <div className="ba-shell"><AgentNav active="assistant" project={projectId} identity={identity}/><main className="ba-main">
|
||||
<header className="ba-heading"><div><p>XINGBANG BUSINESS ASSISTANT</p><h1>一句话,推进对公业务</h1><span>对话和页面按钮共用同一套 Skills、规则、权限与留痕。</span></div><IdentitySelector identity={identity}/></header>
|
||||
<section className="ba-context"><div><span>正在处理</span><select aria-label="选择业务项目" value={projectId} onChange={e=>setProjectId(e.target.value)}>{projects.map(p=><option key={p.id} value={p.id}>{p.id} · {p.customer}</option>)}</select></div>{project&&<><div><span>当前阶段</span><b>{project.stage}</b></div><div><span>下一步</span><b>{project.next_action}</b></div><a href={`/xingbang-workbench?project=${encodeURIComponent(projectId)}`}>进入项目<ArrowUpRight size={15}/></a></>}</section>
|
||||
<section className="ba-workspace"><aside><Sparkles size={18}/><h2>{ROLE_LABEL[role]}快捷操作</h2><p>{role==="reviewer"?"独立查看材料和历史,再形成评审意见。":role==="admin"?"配置能力与数据边界,不替业务人员作判断。":"补证、预审、成稿、提交,一条主线完成。"}</p>{quick.map(item=><button key={item} disabled={busy||!projectId} onClick={()=>send(item)}>{item}<ArrowUpRight size={14}/></button>)}<small><Bot size={13}/>每次执行都会生成对象编号,可回到项目中追溯。</small></aside>
|
||||
{conversation}
|
||||
</section>
|
||||
</main></div>;
|
||||
}
|
||||
Reference in New Issue
Block a user