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:
2026-09-29 12:58:26 +08:00
co-authored by Claude Code
commit d56c699ce6
224 changed files with 33392 additions and 0 deletions
File diff suppressed because one or more lines are too long
+47
View File
@@ -0,0 +1,47 @@
"use client";
import {useEffect,useMemo,useState} from "react";
import {Activity,ArrowUpRight,Clock3,DatabaseZap,RefreshCw,TimerReset,UsersRound} from "lucide-react";
import AgentNav from "@/components/xingbang/AgentNav";
import IdentitySelector from "@/components/xingbang/IdentitySelector";
import "./operations.css";
const ROOT="/api/risk-agent/project-assistant";
const CATEGORY:Record<string,{label:string,color:string}>={active:{label:"人员处理",color:"#39c98a"},queue:{label:"排队等待",color:"#7d91ef"},external_wait:{label:"外部等待",color:"#f1a95b"},system:{label:"系统执行",color:"#55b9d6"}};
function headers(){const t=sessionStorage.getItem("xingbang-access-token"),r=sessionStorage.getItem("xingbang-demo-role");return {...(t?{Authorization:`Bearer ${t}`}:{}) ,...(r?{"X-Xingbang-Demo-Role":r}:{})};}
function hours(value:number){return value<1?`${Math.max(1,Math.round(value*60))}分钟`:value<24?`${value.toFixed(1)}小时`:`${(value/24).toFixed(1)}天`;}
function stamp(value:string){return new Date(value).toLocaleString("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"});}
export default function TimeDashboard(){
const [data,setData]=useState<any>(null),[error,setError]=useState(""),[busy,setBusy]=useState(false),[scope,setScope]=useState("all");
async function load(){setBusy(true);setError("");try{const response=await fetch(`${ROOT}/time-dashboard`,{headers:headers()});const body=await response.json();if(!response.ok)throw Error(body.detail||"时效数据读取失败");setData(body);}catch(e:any){setError(e.message)}finally{setBusy(false)}}
useEffect(()=>{load()},[]);
const filterRole=data?.identity?.role==="admin"?scope:data?.identity?.role||"all";
const projects=useMemo(()=>!data?[]:data.projects.map((p:any)=>({...p,events:filterRole==="all"?p.events:p.events.filter((e:any)=>e.role===filterRole||e.role==="system")})).filter((p:any)=>p.events.length),[data,filterRole]);
const view=useMemo(()=>{
const categories:Record<string,number>={},phaseMap:Record<string,any>={};
const totals=projects.map((project:any)=>project.events.reduce((sum:number,item:any)=>sum+item.duration_hours,0)).sort((a:number,b:number)=>a-b);
for(const project of projects)for(const item of project.events){categories[item.category]=(categories[item.category]||0)+item.duration_hours;const phase=phaseMap[item.phase_code]||(phaseMap[item.phase_code]={code:item.phase_code,name:item.phase_name,hours:0,count:0});phase.hours+=item.duration_hours;phase.count+=1;}
const phases=Object.values(phaseMap).map((item:any)=>({...item,average_hours:item.hours/item.count}));
return {categories,phases,summary:{project_count:projects.length,median_hours:totals.length?totals[Math.floor(totals.length/2)]:0,active_hours:categories.active||0,wait_hours:(categories.queue||0)+(categories.external_wait||0),system_hours:categories.system||0}};
},[projects]);
const maxProject=Math.max(1,...projects.map((p:any)=>p.events.reduce((n:number,e:any)=>n+e.duration_hours,0)));
return <div className="td-shell"><AgentNav active="time" identity={data?.identity}/><main className="td-main">
<header className="td-hero"><div><p>BUSINESS VELOCITY</p><h1>业务时效看板</h1><span>把对公业务的处理、等待与系统耗时分开,看清真正的提效空间。</span></div><div className="td-hero-actions"><IdentitySelector identity={data?.identity} dark/>{data?.identity?.role==="admin"&&<label className="td-scope"><span>统计范围</span><select aria-label="统计范围" value={scope} onChange={e=>setScope(e.target.value)}><option value="all">全部角色</option><option value="manager">客户经理</option><option value="reviewer">评审员</option></select></label>}<button onClick={load} disabled={busy}><RefreshCw size={16} className={busy?"spin":""}/>{busy?"更新中":"刷新数据"}</button></div></header>
{error&&<div role="alert" className="td-error">{error}</div>}
{!data?<div className="td-loading"><Activity/>正在读取SQLite时间事件…</div>:<>
<section className="td-kpis">
<Metric icon={UsersRound} label="纳入项目" value={`${view.summary.project_count}`} unit="个" note="仅统计当前身份授权范围"/>
<Metric icon={Clock3} label="中位业务周期" value={hours(view.summary.median_hours)} note="减少极端项目对平均值的干扰"/>
<Metric icon={TimerReset} label="人员处理" value={hours(view.summary.active_hours)} note="当前身份实际办理时间" accent/>
<Metric icon={DatabaseZap} label="排队与外部等待" value={hours(view.summary.wait_hours)} note={`系统执行仅 ${hours(view.summary.system_hours)}`}/>
</section>
<section className="td-insight"><div><p>本期关键观察</p><h2>时间主要消耗在等待与人工闭环,而不是AI运行。</h2></div><div className="td-donut" style={{background:`conic-gradient(#e2a354 0 ${Math.round(view.summary.wait_hours/Math.max(1,view.summary.active_hours+view.summary.wait_hours+view.summary.system_hours)*100)}%,#38c487 0 100%)`}}><strong>{Math.round(view.summary.wait_hours/Math.max(1,view.summary.active_hours+view.summary.wait_hours+view.summary.system_hours)*100)}%</strong><span>等待占比</span></div></section>
<section className="td-grid"><article className="td-panel"><header><div><p>STAGE DISTRIBUTION</p><h2>阶段耗时分布</h2></div><small>自动记录 · 无需人工打卡</small></header><div className="td-phases">{[...view.phases].sort((a:any,b:any)=>b.hours-a.hours).map((p:any)=>{const max=Math.max(...view.phases.map((x:any)=>x.hours),1);return <div className="td-phase" key={p.code}><div><b>{p.name}</b><span>{hours(p.hours)} · {p.count}次</span></div><div className="td-track"><i style={{width:`${Math.max(3,p.hours/max*100)}%`}}/></div><strong>{hours(p.average_hours)}</strong></div>})}</div></article>
<article className="td-panel td-legend"><header><div><p>TIME COMPOSITION</p><h2>时间口径</h2></div></header>{Object.entries(CATEGORY).map(([key,item])=><div key={key}><i style={{background:item.color}}/><span>{item.label}</span><strong>{hours(view.categories[key]||0)}</strong></div>)}<p>“业务端准备周期”和“正式评审周期”分开统计,外部补件等待不会计入人员实际处理时间。</p></article></section>
<section className="td-projects"><header><div><p>PROJECT TIMELINE</p><h2>项目时间轴</h2></div><span>点击项目进入完整业务记录</span></header>{projects.map((p:any)=>{const total=p.events.reduce((n:number,e:any)=>n+e.duration_hours,0);return <a className="td-project" href={`/xingbang-workbench?project=${encodeURIComponent(p.project_id)}&tab=time`} key={p.project_id}><div className="td-project-title"><span>{p.project_id}</span><h3>{p.customer}</h3><small>{p.industry} · {p.stage}</small></div><div className="td-bars">{p.events.map((e:any)=><div className="td-segment" key={e.id} style={{width:`${Math.max(4,e.duration_hours/maxProject*100)}%`,background:CATEGORY[e.category]?.color}} title={`${e.phase_name} ${hours(e.duration_hours)}`}><span>{e.duration_hours/maxProject>0.1?e.phase_name:""}</span></div>)}</div><div className="td-project-total"><small>累计周期</small><strong>{hours(total)}</strong><ArrowUpRight size={17}/></div><div className="td-project-events">{p.events.map((e:any)=><span key={e.id}><i style={{background:CATEGORY[e.category]?.color}}/>{e.phase_name}<b>{hours(e.duration_hours)}</b><small>{stamp(e.started_at)}{e.status==="running"?" · 进行中":""}</small></span>)}</div></a>})}</section>
</>}
</main></div>;
}
function Metric({icon:Icon,label,value,unit,note,accent}:{icon:any,label:string,value:string,unit?:string,note:string,accent?:boolean}){return <article className={accent?"accent":""}><div><span>{label}</span><Icon size={19}/></div><strong>{value}<small>{unit}</small></strong><p>{note}</p></article>}