简化业务助手为单一入口
verify / test-and-build (push) Canceled after 0s

This commit is contained in:
NathanAlex7
2026-09-21 13:33:00 +08:00
parent 4732cee85e
commit b10093f9d8
4 changed files with 17 additions and 22 deletions
+4 -2
View File
@@ -172,8 +172,10 @@ class OperationsCenterTests(unittest.TestCase):
self.assertNotIn("<p>{item.detail}</p>", workspace)
self.assertNotIn('"方案比较"', workspace)
self.assertNotIn('tab === "scenario"', workspace)
self.assertIn("mini-openclaw", assistant)
self.assertIn("<ChatPanel/>", assistant)
self.assertNotIn("mini-openclaw", assistant)
self.assertNotIn("<ChatPanel/>", assistant)
self.assertIn('className="ba-workspace"', assistant)
self.assertIn("当前身份只会调用本项目授权的 Skills、规则和业务工具", assistant)
self.assertIn("IdentitySelector", assistant)
self.assertIn("IdentitySelector", dashboard)
self.assertIn("统计范围", dashboard)
+6 -6
View File
@@ -11,13 +11,13 @@ const coreProxy = read("src/app/api/core/[...path]/route.ts");
assert.match(
assistant,
/setNativeAccess\(body\.identity\?\.role==="admin"&&!!sessionStorage\.getItem\("xingbang-access-token"\)\)/,
"只有带正式令牌的管理员才能进入原版通用工具工作台",
/<section className="ba-workspace">/,
"业务助手应保留单一、直接的项目操作入口",
);
assert.match(
assert.doesNotMatch(
assistant,
/nativeAccess\?<AppProvider><ChatPanel\/><\/AppProvider>:conversation/,
"客户经理和评审员应回落到受角色约束的项目会话,而不是请求管理员接口",
/mini-openclaw|ChatPanel|AppProvider|nativeAccess|ba-mode-tabs/,
"业务人员页面不应继续展示或加载重复的 mini-openclaw 入口",
);
assert.match(
assistant,
@@ -35,4 +35,4 @@ assert.match(
"修复不得放开原版终端、文件和网页工具的管理员边界",
);
console.log("Cloud mini-openclaw role access checks passed");
console.log("Cloud business-assistant single-entry checks passed");
+6 -11
View File
@@ -1,11 +1,9 @@
"use client";
import {ChangeEvent,FormEvent,useEffect,useMemo,useState} from "react";
import {ArrowUpRight,Bot,FilePlus2,MessagesSquare,Paperclip,Send,Sparkles,UserRound} from "lucide-react";
import {ArrowUpRight,Bot,FilePlus2,Paperclip,Send,Sparkles,UserRound} from "lucide-react";
import AgentNav from "./AgentNav";
import IdentitySelector,{ROLE_LABEL} from "./IdentitySelector";
import ChatPanel from "@/components/chat/ChatPanel";
import {AppProvider} from "@/lib/store";
import {newRequestId} from "@/lib/request-id";
import "./business-assistant.css";
@@ -20,15 +18,13 @@ function headers(json=true){
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 [mode,setMode]=useState<"business"|"openclaw">("business");
const [nativeAccess,setNativeAccess]=useState(false);
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);setNativeAccess(body.identity?.role==="admin"&&!!sessionStorage.getItem("xingbang-access-token"));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||""));}
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()},[]);
@@ -48,17 +44,16 @@ 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>正在通过 mini-openclaw 执行并记录…</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>{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>MINI-OPENCLAW BUSINESS ENTRY</p><h1>一句话,推进对公业务</h1><span>对话和页面按钮共用同一套 Skills、规则、权限与留痕。</span></div><IdentitySelector identity={identity}/></header>
<nav className="ba-mode-tabs" aria-label="助手模式"><button className={mode==="business"?"active":""} onClick={()=>setMode("business")}><Sparkles size={15}/>业务快捷助手</button><button className={mode==="openclaw"?"active":""} onClick={()=>setMode("openclaw")}><MessagesSquare size={15}/>mini-openclaw</button></nav>
<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>
{mode==="business"?<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>
<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>:<section className="ba-native"><header><div><Bot size={18}/><div><b>{nativeAccess?"mini-openclaw 原生管理工作台":`${ROLE_LABEL[role]} mini-openclaw 工作台`}</b><span>{nativeAccess?"管理员已认证,可使用原版通用工具":`已绑定 ${projectId};按当前身份调用受限 Skills、规则和项目工具`}</span></div></div><a href={`/xingbang-workbench?project=${encodeURIComponent(projectId)}`}>查看项目事实<ArrowUpRight size={14}/></a></header>{nativeAccess?<AppProvider><ChatPanel/></AppProvider>:conversation}</section>}
</section>
</main></div>;
}
@@ -1,4 +1,4 @@
.ba-shell{min-height:100vh;background:#f1f5f2;color:#173127;font-family:Arial,"Microsoft YaHei",sans-serif}.ba-shell *{box-sizing:border-box}.ba-main{max-width:1420px;margin:auto;padding:44px 38px 70px}.ba-heading{display:flex;justify-content:space-between;align-items:flex-end;gap:24px}.ba-heading>div>p{font-size:10px;letter-spacing:.16em;color:#378265;margin-bottom:10px}.ba-heading h1{font:500 36px/1.2 Georgia,"Microsoft YaHei",serif}.ba-heading>div>span{display:block;color:#74857d;font-size:12px;margin-top:12px}.ba-mode-tabs{display:flex;gap:5px;margin-top:28px}.ba-mode-tabs button{display:flex;align-items:center;gap:7px;border:0;background:transparent;color:#6f8178;padding:10px 13px;border-radius:9px;font-size:11px}.ba-mode-tabs button.active{background:#dcece3;color:#17633f;font-weight:700}.ba-context{display:grid;grid-template-columns:minmax(280px,1.4fr) .7fr 1fr auto;align-items:center;gap:18px;background:#0e2a1e;color:white;margin:10px 0 16px;padding:18px 22px;border-radius:15px;box-shadow:0 15px 42px #0d2d201c}.ba-context>div{min-width:0;border-right:1px solid #ffffff17}.ba-context span{display:block;font-size:9px;color:#7ba18e;margin-bottom:7px}.ba-context select{width:90%;border:0;background:transparent;color:#eaf7f0;font-size:13px;font-weight:700}.ba-context select option{color:#1a2a22}.ba-context b{display:block;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ba-context>a{display:flex;align-items:center;gap:6px;color:#73dfa9;font-size:11px}.ba-workspace{display:grid;grid-template-columns:290px minmax(0,1fr);min-height:610px;border:1px solid #dfe8e2;border-radius:18px;overflow:hidden;background:#fff;box-shadow:0 30px 80px #274d3a0e}.ba-workspace>aside{padding:30px 24px;background:linear-gradient(160deg,#f7fbf8,#edf5f0);border-right:1px solid #e1eae4}.ba-workspace>aside>svg{color:#2c9c6a}.ba-workspace aside h2{font-size:17px;margin:18px 0 10px}.ba-workspace aside p{color:#718179;font-size:11px;line-height:1.8;margin-bottom:24px}.ba-workspace aside button{width:100%;display:flex;justify-content:space-between;align-items:center;border:1px solid #dbe7df;background:#fff;color:#345a48;padding:12px 13px;border-radius:9px;margin-bottom:9px;font-size:11px;cursor:pointer}.ba-workspace aside button:hover{border-color:#62ad89;color:#17633f}.ba-workspace aside small{display:flex;align-items:flex-start;gap:7px;color:#8a9991;font-size:9px;line-height:1.7;margin-top:28px}.ba-chat{display:flex;flex-direction:column;min-width:0}.ba-messages{flex:1;padding:28px 34px;overflow:auto;max-height:560px}.ba-messages article{max-width:75%;margin-bottom:18px}.ba-messages article>div{display:flex;align-items:flex-start;gap:9px}.ba-messages article p{padding:13px 15px;border-radius:12px;background:#edf4f0;color:#385347;font-size:12px;line-height:1.8}.ba-messages article.user{margin-left:auto}.ba-messages article.user p{background:#17633f;color:#fff}.ba-messages article svg{color:#369367;margin-top:13px;flex-shrink:0}.ba-messages article small{display:block;color:#9aa6a0;font-size:9px;margin:7px 0 0 25px}.ba-messages article a{display:inline-flex;align-items:center;gap:4px;color:#24724f;font-size:10px;margin:6px 0 0 25px}.ba-messages .pending{opacity:.65}.ba-error{margin:0 28px 10px;color:#9c463b;background:#fff0ed;padding:10px 13px;border-radius:8px;font-size:11px}.ba-chat form{display:flex;align-items:center;gap:9px;margin:0 28px;padding:8px 9px;border:1px solid #d8e3dc;border-radius:13px;background:#fff;box-shadow:0 8px 25px #244a370d}.ba-chat form>input{flex:1;border:0;outline:0;padding:10px;font-size:12px}.ba-chat form>button,.ba-attach{width:38px;height:38px;display:grid;place-items:center;border:0;border-radius:9px;cursor:pointer}.ba-chat form>button{background:#17633f;color:#fff}.ba-chat form>button:disabled{opacity:.4}.ba-attach{color:#668078;background:#eff4f1}.ba-attach input{display:none}.ba-chat footer{display:flex;align-items:center;gap:6px;color:#93a199;font-size:9px;padding:10px 38px 20px}.ba-native{height:610px;display:grid;grid-template-rows:auto 1fr;overflow:hidden;border:1px solid #dfe8e2;border-radius:18px;background:#fff;box-shadow:0 30px 80px #274d3a0e}.ba-native>header{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-bottom:1px solid #e1e9e4;background:#f6faf7}.ba-native>header>div,.ba-native>header>a{display:flex;align-items:center;gap:9px}.ba-native>header b{font-size:12px}.ba-native>header span{display:block;color:#819087;font-size:9px;margin-top:4px}.ba-native>header>a{color:#21704d;font-size:10px}@media(max-width:820px){.ba-main{padding:26px 14px 50px}.ba-heading{align-items:flex-start;flex-direction:column}.ba-heading h1{font-size:29px}.ba-context{grid-template-columns:1fr 1fr}.ba-workspace{grid-template-columns:1fr}.ba-workspace>aside{border-right:0;border-bottom:1px solid #e1eae4}.ba-messages{padding:22px 18px}.ba-messages article{max-width:90%}.ba-chat form{margin:0 14px}.ba-chat footer{padding-left:22px}}
.ba-shell{min-height:100vh;background:#f1f5f2;color:#173127;font-family:Arial,"Microsoft YaHei",sans-serif}.ba-shell *{box-sizing:border-box}.ba-main{max-width:1420px;margin:auto;padding:44px 38px 70px}.ba-heading{display:flex;justify-content:space-between;align-items:flex-end;gap:24px}.ba-heading>div>p{font-size:10px;letter-spacing:.16em;color:#378265;margin-bottom:10px}.ba-heading h1{font:500 36px/1.2 Georgia,"Microsoft YaHei",serif}.ba-heading>div>span{display:block;color:#74857d;font-size:12px;margin-top:12px}.ba-context{display:grid;grid-template-columns:minmax(280px,1.4fr) .7fr 1fr auto;align-items:center;gap:18px;background:#0e2a1e;color:white;margin:28px 0 16px;padding:18px 22px;border-radius:15px;box-shadow:0 15px 42px #0d2d201c}.ba-context>div{min-width:0;border-right:1px solid #ffffff17}.ba-context span{display:block;font-size:9px;color:#7ba18e;margin-bottom:7px}.ba-context select{width:90%;border:0;background:transparent;color:#eaf7f0;font-size:13px;font-weight:700}.ba-context select option{color:#1a2a22}.ba-context b{display:block;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ba-context>a{display:flex;align-items:center;gap:6px;color:#73dfa9;font-size:11px}.ba-workspace{display:grid;grid-template-columns:290px minmax(0,1fr);min-height:610px;border:1px solid #dfe8e2;border-radius:18px;overflow:hidden;background:#fff;box-shadow:0 30px 80px #274d3a0e}.ba-workspace>aside{padding:30px 24px;background:linear-gradient(160deg,#f7fbf8,#edf5f0);border-right:1px solid #e1eae4}.ba-workspace>aside>svg{color:#2c9c6a}.ba-workspace aside h2{font-size:17px;margin:18px 0 10px}.ba-workspace aside p{color:#718179;font-size:11px;line-height:1.8;margin-bottom:24px}.ba-workspace aside button{width:100%;display:flex;justify-content:space-between;align-items:center;border:1px solid #dbe7df;background:#fff;color:#345a48;padding:12px 13px;border-radius:9px;margin-bottom:9px;font-size:11px;cursor:pointer}.ba-workspace aside button:hover{border-color:#62ad89;color:#17633f}.ba-workspace aside small{display:flex;align-items:flex-start;gap:7px;color:#8a9991;font-size:9px;line-height:1.7;margin-top:28px}.ba-chat{display:flex;flex-direction:column;min-width:0}.ba-messages{flex:1;padding:28px 34px;overflow:auto;max-height:560px}.ba-messages article{max-width:75%;margin-bottom:18px}.ba-messages article>div{display:flex;align-items:flex-start;gap:9px}.ba-messages article p{padding:13px 15px;border-radius:12px;background:#edf4f0;color:#385347;font-size:12px;line-height:1.8}.ba-messages article.user{margin-left:auto}.ba-messages article.user p{background:#17633f;color:#fff}.ba-messages article svg{color:#369367;margin-top:13px;flex-shrink:0}.ba-messages article small{display:block;color:#9aa6a0;font-size:9px;margin:7px 0 0 25px}.ba-messages article a{display:inline-flex;align-items:center;gap:4px;color:#24724f;font-size:10px;margin:6px 0 0 25px}.ba-messages .pending{opacity:.65}.ba-error{margin:0 28px 10px;color:#9c463b;background:#fff0ed;padding:10px 13px;border-radius:8px;font-size:11px}.ba-chat form{display:flex;align-items:center;gap:9px;margin:0 28px;padding:8px 9px;border:1px solid #d8e3dc;border-radius:13px;background:#fff;box-shadow:0 8px 25px #244a370d}.ba-chat form>input{flex:1;border:0;outline:0;padding:10px;font-size:12px}.ba-chat form>button,.ba-attach{width:38px;height:38px;display:grid;place-items:center;border:0;border-radius:9px;cursor:pointer}.ba-chat form>button{background:#17633f;color:#fff}.ba-chat form>button:disabled{opacity:.4}.ba-attach{color:#668078;background:#eff4f1}.ba-attach input{display:none}.ba-chat footer{display:flex;align-items:center;gap:6px;color:#93a199;font-size:9px;padding:10px 38px 20px}@media(max-width:820px){.ba-main{padding:26px 14px 50px}.ba-heading{align-items:flex-start;flex-direction:column}.ba-heading h1{font-size:29px}.ba-context{grid-template-columns:1fr 1fr}.ba-workspace{grid-template-columns:1fr}.ba-workspace>aside{border-right:0;border-bottom:1px solid #e1eae4}.ba-messages{padding:22px 18px}.ba-messages article{max-width:90%}.ba-chat form{margin:0 14px}.ba-chat footer{padding-left:22px}}
/* Larger business copy and clearer assistant actions. */
.ba-shell{font-family:"Microsoft YaHei","PingFang SC",sans-serif;font-size:15px}
@@ -10,8 +10,6 @@
.ba-workspace aside button:hover{background:#edf7f1;border-color:#3e8f68;box-shadow:0 4px 14px #244a3712}
.ba-workspace aside small{font-size:12px}.ba-messages article p{font-size:15px}.ba-messages article small{font-size:12px}.ba-messages article a{font-size:13px;font-weight:700}
.ba-error{font-size:14px}.ba-chat form{border-color:#bdd2c6;box-shadow:0 8px 28px #244a3715}.ba-chat form>input{font-size:15px}.ba-chat form>button,.ba-attach{width:44px;height:44px}.ba-chat footer{font-size:12px}
.ba-native>header b{font-size:14px}.ba-native>header span{font-size:12px}.ba-native>header>a{font-size:13px;font-weight:700}
.ba-native>.ba-chat{min-height:0}.ba-native>.ba-chat .ba-messages{max-height:none}
/* Give operator messages a full row so the bubble cannot drift into the middle. */
.ba-messages article.user{width:100%;max-width:100%;margin-left:0}