@marsken/dsh-studio 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +11 -1
- package/client.js +2 -2
- package/dist/client.js +2 -2
- package/docs/compatibility.md +1 -1
- package/docs/publish.md +1 -0
- package/package.json +1 -1
- package/src/host/domain.js +4 -4
- package/src/host/routes/insights.js +5 -0
- package/src/host/session-usage.js +100 -0
- package/src/host/usage.js +124 -0
package/dist/client.js
CHANGED
|
@@ -26,7 +26,7 @@ GET /api/points | 200 |`,onChange:m=>ie({...X,steps:m.target.value})}),e("div",{
|
|
|
26
26
|
按边界值、异常流、并发与幂等设计用例,列出实际执行结果。`}];for(const G of O)await s("/experts/save",{expert:{name:`${R}${G.name}`,emoji:G.emoji,role:G.role,domain:R,description:`负责${R}业务线的${G.name}工作`,prompt:G.prompt,model:"",preset:"",enabled:!0}});M(""),await t()}catch(O){y(O instanceof Error?O.message:String(O))}finally{C(!1)}}},T=R=>{const O=String(R??"").trim();if(O==="")return"";const G=O.replace(/\s*\n\s*/g," / ");return G.length>96?`${G.slice(0,96)}…`:G},K=k.map(R=>a("div",{className:"ds-listrow",key:R.id},a("div",{className:"ds-listrow-main"},a("div",{},`${R.emoji} ${R.name}`,R.domain?a(f,{className:"ds-chip"},R.domain):null,R.enabled===!1?a("span",{className:"ds-hint"}," (已停用)"):null),a("div",{className:"ds-hint"},`${R.model||"跟随默认模型"}${R.description?" · "+R.description:""}`),T(R.prompt)===""?a("div",{className:"ds-hint","data-empty":"1"},"提示词:还没有写,会退回内置角色说明(点「编辑」补上)"):a("div",{className:"ds-hint","data-prompt":"1",title:String(R.prompt??""),style:{cursor:"pointer"},onClick:()=>d({...R})},`提示词:${T(R.prompt)}`),(R.skills??[]).length>0?a("div",{className:"ds-hint"},`技能:${R.skills.join("、")}`):null),a("div",{className:"ds-listrow-actions"},l("编辑这位专家",a("button",{className:"ds-btn",disabled:S,onClick:()=>d({...R})},"编辑")),l("删除这位专家",a("button",{className:"ds-btn",disabled:S,onClick:()=>void I(R),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(r,{title:"专家(可在评论里 @ 他们)",hint:"每个专家有名字、业务领域、角色提示词和技能;评论里 @名字 就会把这条工作派给他。",rows:K,onAdd:()=>d({id:"",name:"",emoji:"🧩",role:"",domain:"",description:"",prompt:"",skills:[],model:"",preset:"",sandbox:"",enabled:!0}),addLabel:"新增专家",render:R=>R,empty:"还没有专家,点「新增专家」建一个。"}),a("div",{className:"ds-row",style:{gap:8,alignItems:"center",padding:"6px 0 10px"}},a("span",{className:"ds-hint"},"按业务领域一次生成四位:"),a(u,{className:"ds-input",style:{maxWidth:180},value:W,placeholder:"支付 / 风控 / 会员",onChange:R=>M(R.target.value)}),a("button",{className:"ds-btn",disabled:S||W.trim().length===0,onClick:()=>void w()},"生成产品/架构/研发/测试"),h!==null?a("span",{className:"ds-error"},h):null),g===null?null:a(b,{title:g.id?`编辑专家:${g.name||g.id}`:"新增专家",hint:"技能与提示词会随下一次派活生效,改完点右下角「保存」。",onClose:()=>d(null),footer:[a("button",{key:"save",className:"ds-btn ds-btn-primary",disabled:S,onClick:()=>void _()},S?"保存中…":"保存"),a("button",{key:"cancel",className:"ds-btn",disabled:S,onClick:()=>d(null)},"取消"),h!==null?a("span",{key:"error",className:"ds-error ds-side-note"},h):null]},a("div",{className:"ds-row",style:{gap:12,alignItems:"flex-start"}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"名字(@ 用这个名字)"),a(u,{className:"ds-input",value:g.name,placeholder:"安全专家",onChange:R=>D("name",R.target.value)})),a("div",{className:"ds-field",style:{flex:"0 0 96px"}},a("div",{className:"ds-label"},"当前图标"),a("div",{className:"ds-emoji-preview"},g.emoji||"🧩"))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"图标(点一个换上,也可以自己填)"),a("div",{className:"ds-emojigrid"},E.map(R=>a("button",{key:R,type:"button",className:"ds-emoji","data-on":g.emoji===R?"1":"0",title:R,onClick:()=>D("emoji",R)},R))),a("div",{className:"ds-row",style:{gap:8,marginTop:8,alignItems:"center"}},a(u,{className:"ds-input",style:{flex:"0 0 120px"},value:g.emoji,maxLength:4,placeholder:"🧩",onChange:R=>D("emoji",R.target.value)}),a("span",{className:"ds-hint"},"也可以粘一个 emoji,或者留空用默认的 🧩。"))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"一句话定位"),a(u,{className:"ds-input",value:g.description,placeholder:"负责接口安全与权限模型评审",onChange:R=>D("description",R.target.value)})),a("div",{className:"ds-row",style:{gap:12,alignItems:"flex-start"}},a("div",{className:"ds-field",style:{flex:"0 0 132px"}},c("角色位","决定它对应看板上的哪一列;列负责人按角色位挑人时用得上"),a(x,{className:"ds-select",value:g.role??"",onChange:R=>D("role",R.target.value)},a("option",{value:""},"不限"),["product","architect","dev","qa"].map(R=>a("option",{key:R,value:R},za[R]??R))),a("div",{className:"ds-hint"},"决定默认接哪一列")),a("div",{className:"ds-field",style:{flex:1}},c("业务领域","「按专长匹配」看这里:领域相同的卡优先派给它"),a(u,{className:"ds-input",value:g.domain??"",placeholder:"支付 / 风控 / 会员(选填)",onChange:R=>D("domain",R.target.value)}),a("div",{className:"ds-hint"},"同一张卡会优先派给同领域的专家"))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},`技能(从技能库勾选 · 已选 ${(g.skills??[]).length} 项)`),P.length>0?a("div",{className:"ds-skillbox"},P.map(R=>{const O=(g.skills??[]).includes(R.name),G=k.filter(we=>(we.skills??[]).includes(R.name)).length;return a("label",{key:R.name,className:"ds-skillopt","data-on":O?"1":"0",title:String(R.description??"")},a(v,{checked:O,onChange:()=>J(R.name)}),a("span",{className:"ds-skillname"},R.name),a("span",{className:"ds-skilldesc"},String(R.description??"").slice(0,46)),G>0?a("span",{className:"ds-skillusers"},`${G} 位在用`):null)})):a("div",{className:"ds-hint"},"技能库还是空的,去「技能库」建一条,或者在下面直接写名字。"),a("div",{className:"ds-row",style:{gap:8,marginTop:8,alignItems:"center"}},a(u,{className:"ds-input",style:{flex:1},value:U,placeholder:"技能库以外的技能名,例如 lark-doc、office-xlsx(回车或点「加上」)",onChange:R=>F(R.target.value),onKeyDown:R=>{R.key==="Enter"&&(R.preventDefault(),L())}}),a("button",{className:"ds-btn",onClick:()=>L()},"加上")),(g.skills??[]).filter(R=>!P.some(O=>O.name===R)).length>0?a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginTop:6,alignItems:"center"}},a("span",{className:"ds-hint"},"不在技能库(仍会写进提示词):"),(g.skills??[]).filter(R=>!P.some(O=>O.name===R)).map(R=>a(f,{key:R,className:"ds-chip","data-missing":"1"},R,a("button",{className:"ds-chip-x",title:"移除",onClick:()=>D("skills",(g.skills??[]).filter(O=>O!==R))},"×")))):null),a("div",{className:"ds-row",style:{gap:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"模型"),a(u,{className:"ds-input",value:g.model,placeholder:"deepseek-v4.1-flash(留空跟随角色默认)",onChange:R=>D("model",R.target.value)})),a("div",{className:"ds-field",style:{flex:1}},c("Agent 预设","预设决定这个专家能用哪些工具、以什么身份干活(如 standard、架构师)"),a(u,{className:"ds-input",value:g.preset,placeholder:"standard",onChange:R=>D("preset",R.target.value)})),a("div",{className:"ds-field",style:{flex:1}},c("沙箱档位","read-only 只能看;workspace-write 能改工作区;danger-full-access 不做限制"),a(x,{className:"ds-input",value:g.sandbox??"",onChange:R=>D("sandbox",R.target.value)},[["","跟随全局"],["read-only","只读"],["workspace-write","只写工作区"],["danger-full-access","无沙箱"]].map(([R,O])=>a("option",{key:R,value:R},O))),a("div",{className:"ds-hint"},"研发/测试这类要真跑命令的专家建议「只写工作区」。"))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"角色提示词(每次派活都会作为系统提示发给这个专家)"),a(p,{className:"ds-input",rows:9,value:g.prompt,placeholder:`例如:
|
|
27
27
|
你是支付域的产品经理。先读 docs/ 与支付相关的 ADR,再动手。
|
|
28
28
|
需求必须写清:目标用户、主流程、异常与退款路径、对账口径。
|
|
29
|
-
不确定的地方去代码或历史工单里找证据,禁止编造接口字段。`,onChange:R=>D("prompt",R.target.value)})),a("div",{className:"ds-field"},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(v,{checked:g.enabled!==!1,onChange:R=>D("enabled",R.target.checked)}),a("span",{},"启用这个专家")))))}function zx({config:e,onChanged:t}){const{h:a,React:n,Editor:r,api:s,toast:o,confirmDialog:i,tipOf:l,tipLabel:c,TextField:u,TextAreaField:p,ChipField:f,CheckField:v,SelectField:x,SideDrawer:b}=vt(),k=e.teams??[],g=e.experts??[],[d,h]=n.useState(null),[y,S]=n.useState(null),[C,E]=n.useState(!1),[_,I]=n.useState({});n.useEffect(()=>{let U=!0;return s("/team/memory",{}).then(F=>{U&&I(F?.memory??{})}).catch(()=>{}),()=>{U=!1}},[e.teams]);const D=async U=>{if(await i("清空「"+U.name+"」的团队记忆?",{title:"清空团队记忆",danger:!0,okText:"清空"}))try{const F=await s("/team/memory/clear",{teamId:U.id});I(F?.memory??{}),o("已清空这个团的记忆")}catch(F){S(F instanceof Error?F.message:String(F))}},W=U=>{const F=g.find(L=>L.id===U);return F===void 0?U:`${F.emoji} ${F.name}`},M=async()=>{if(d!==null){E(!0),S(null);try{await s("/teams/save",{team:d}),h(null),await t()}catch(U){S(U instanceof Error?U.message:String(U))}finally{E(!1)}}},P=async U=>{if(await i(`删除专家团「${U.name}」?`,{title:"删除专家团",danger:!0,okText:"删除"})){E(!0),S(null);try{await s("/teams/delete",{id:U.id}),await t()}catch(F){S(F instanceof Error?F.message:String(F))}finally{E(!1)}}},H=(U,F)=>h({...d,[U]:F}),J=k.map(U=>a("div",{className:"ds-listrow",key:U.id},a("div",{className:"ds-listrow-main"},a("div",{},`👥 ${U.name}`),a("div",{className:"ds-hint"},`团长 ${W(U.lead)} · 成员 ${U.members.map(W).join("、")}`),a("div",{className:"ds-hint"},(_[U.id]??[]).length===0?"🧠 团队记忆:还没有(卡做完会自动记一条,派活时带进提示词)":`🧠 团队记忆 ${_[U.id].length} 条 · 最近:${_[U.id][0]?.outcome??""} ${_[U.id][0]?.title??""}`)),a("div",{className:"ds-listrow-actions"},l("清空这个团的记忆",a("button",{className:"ds-btn ds-btn-ghost",disabled:C||(_[U.id]??[]).length===0,onClick:()=>void D(U)},"清空记忆")),l("编辑这个专家团",a("button",{className:"ds-btn",disabled:C,onClick:()=>h({...U})},"编辑")),l("删除这个专家团",a("button",{className:"ds-btn",disabled:C,onClick:()=>void P(U),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(r,{title:"专家团(一起干活)",hint:"专家团有团长和成员。评论里 @团名,团长会拆活、给成员派活并汇总结论;成员之间可以继续 @ 接棒。",rows:J,onAdd:()=>h({id:"",name:"",description:"",members:[],lead:"",enabled:!0}),addLabel:"新增专家团",render:U=>U,empty:"还没有专家团。"}),d===null?null:a(b,{title:d.id?`编辑专家团:${d.name||d.id}`:"新增专家团",hint:"团长负责拆活,成员之间在评论里用 @名字 接棒;改完点右下角「保存」。",onClose:()=>h(null),footer:[a("button",{key:"save",className:"ds-btn ds-btn-primary",disabled:C,onClick:()=>void M()},C?"保存中…":"保存"),a("button",{key:"cancel",className:"ds-btn",disabled:C,onClick:()=>h(null)},"取消"),y!==null?a("span",{key:"error",className:"ds-error ds-side-note"},y):null]},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"团名(@ 用这个名字)"),a(u,{className:"ds-input",value:d.name,placeholder:"需求交付组",onChange:U=>H("name",U.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"这个团负责什么"),a(u,{className:"ds-input",value:d.description,placeholder:"从需求澄清一路做到验收",onChange:U=>H("description",U.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"成员"),g.length===0?a("div",{className:"ds-hint"},"先去上面建几个专家。"):a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:12}},g.map(U=>a("label",{className:"ds-row",key:U.id,style:{alignItems:"center",gap:6,cursor:"pointer"}},a(v,{checked:(d.members??[]).includes(U.id),onChange:F=>H("members",F.target.checked?[...d.members??[],U.id]:(d.members??[]).filter(L=>L!==U.id))}),a("span",{},`${U.emoji} ${U.name}`))))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"团长(负责拆活和汇总)"),a(x,{className:"ds-input",value:d.lead??"",onChange:U=>H("lead",U.target.value)},a("option",{value:""},"自动(第一个成员)"),(d.members??[]).map(U=>a("option",{key:U,value:U},W(U)))))))}function Fx({config:e,onChanged:t}){const{h:a,React:n,Editor:r,api:s,toast:o,confirmDialog:i,tipOf:l,tipLabel:c,TextField:u,TextAreaField:p,ChipField:f,CheckField:v,SelectField:x,SideDrawer:b}=vt(),k=e.testing??{},g=Array.isArray(k.envs)?k.envs:[],[d,h]=n.useState(null),[y,S]=n.useState((Array.isArray(k.allowHosts)?k.allowHosts:[]).join(", ")),[C,E]=n.useState(String(k.timeoutMs??15e3)),[_,I]=n.useState(!1),[D,W]=n.useState(null),[M,P]=n.useState(null),[H,J]=n.useState({allowShell:k.allowShell===!0,allowDb:k.allowDb===!0}),[U,F]=n.useState({casesPerTask:e.limits?.casesPerTask??60,runKeys:e.limits?.runKeys??60,artifactsPerTask:e.limits?.artifactsPerTask??40}),[L,w]=n.useState(()=>(Array.isArray(e.webhooks)?e.webhooks:[]).map(ae=>({...ae}))),T=async ae=>{J(ae),I(!0),P(null);try{await s("/config",{testing:{...k,allowShell:ae.allowShell,allowDb:ae.allowDb}}),P("已保存"),await t()}catch(z){W(z instanceof Error?z.message:String(z))}finally{I(!1)}},K=async()=>{I(!0),P(null);try{await s("/config",{limits:U}),P("上限已保存"),o("上限已保存"),await t()}catch(ae){W(ae instanceof Error?ae.message:String(ae))}finally{I(!1)}},R=async()=>{I(!0),P(null);try{await s("/config",{webhooks:L.filter(ae=>String(ae.url??"").trim()!=="")}),P("webhook 已保存"),o("webhook 已保存"),await t()}catch(ae){W(ae instanceof Error?ae.message:String(ae))}finally{I(!1)}},O=async ae=>{I(!0),W(null),P(null);try{await s("/config",{testing:{envs:ae.envs??g,defaultEnv:ae.defaultEnv!==void 0?ae.defaultEnv:k.defaultEnv??"",timeoutMs:ae.timeoutMs!==void 0?ae.timeoutMs:k.timeoutMs??15e3,allowHosts:ae.allowHosts!==void 0?ae.allowHosts:Array.isArray(k.allowHosts)?k.allowHosts:[],concurrency:k.concurrency??4}}),P("已保存"),o("已保存"),await t()}catch(z){W(z instanceof Error?z.message:String(z))}finally{I(!1)}},G=async()=>{if(d===null)return;const ae=String(d.name??"").trim(),z=String(d.baseUrl??"").trim();if(ae===""){W("环境名不能为空");return}const te={id:d.id??"",name:ae,baseUrl:z},xe=d.id===void 0||d.id===""?[...g,te]:g.map(X=>X.id===d.id?{...X,...te}:X);h(null),await O({envs:xe,defaultEnv:k.defaultEnv===""?"":k.defaultEnv})},we=async ae=>{if(!await i(`删除环境「${ae.name}」?`,{title:"删除环境",danger:!0,okText:"删除"}))return;const z=g.filter(te=>te.id!==ae.id);await O({envs:z,defaultEnv:k.defaultEnv===ae.id?z[0]?.id??"":k.defaultEnv})},fe=async()=>{const ae=y.split(/[,,\s]+/).map(te=>te.trim()).filter(te=>te!==""),z=Number(C);await O({allowHosts:ae,timeoutMs:Number.isFinite(z)&&z>0?z:15e3})},ye=ae=>a("div",{key:ae.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${ae.name}${k.defaultEnv===ae.id?" · 默认":""}`),a("div",{className:"ds-hint"},ae.baseUrl===""?"相对地址会因为没有 baseUrl 而失败":ae.baseUrl)),a("div",{className:"ds-listrow-actions"},k.defaultEnv===ae.id?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>void O({defaultEnv:ae.id})},"设为默认"),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>h({...ae})},"编辑"),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>void we(ae),"data-danger":"1"},"删除"))),He=d===null?null:a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},d.id===void 0||d.id===""?"新增环境":`编辑环境:${d.name}`),a("div",{className:"ds-panel-body"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"环境名"),a(u,{className:"ds-input",value:d.name??"",placeholder:"例如 测试环境 / 预发环境",onChange:ae=>h({...d,name:ae.target.value})})),a("div",{className:"ds-field"},c("baseUrl","接口测试的根地址,用例里的 /path 会拼在它后面"),a(u,{className:"ds-input",value:d.baseUrl??"",placeholder:"https://test.example.com",onChange:ae=>h({...d,baseUrl:ae.target.value})})),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>h(null)},"取消"),a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void G()},"保存环境"))));return a("div",{className:"ds-pane-stack"},He,a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"执行开关",a("span",{className:"ds-hint"},"默认都关:打开等于把宿主机的能力交给看板使用者")),a("div",{className:"ds-panel-body"},a("label",{className:"ds-check"},a(v,{checked:H.allowShell,disabled:_,onChange:ae=>void T({...H,allowShell:ae.target.checked})}),a("span",{className:"ds-check-text"},"允许 shell 用例(能在宿主机执行命令)")),a("label",{className:"ds-check",style:{marginTop:6}},a(v,{checked:H.allowDb,disabled:_,onChange:ae=>void T({...H,allowDb:ae.target.checked})}),a("span",{className:"ds-check-text"},"允许数据库用例(默认只读打开 SQLite)")))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"数量上限"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-row",style:{gap:8,flexWrap:"wrap"}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"每张卡用例上限"),a(u,{className:"ds-input",type:"number",value:String(U.casesPerTask),onChange:ae=>F({...U,casesPerTask:Number(ae.target.value)})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"一次批量跑上限"),a(u,{className:"ds-input",type:"number",value:String(U.runKeys),onChange:ae=>F({...U,runKeys:Number(ae.target.value)})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"交付物上限"),a(u,{className:"ds-input",type:"number",value:String(U.artifactsPerTask),onChange:ae=>F({...U,artifactsPerTask:Number(ae.target.value)})}))),a("div",{className:"ds-row",style:{justifyContent:"flex-end",marginTop:6}},a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void K()},"保存上限")))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"出站 webhook",a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>w(ae=>[...ae,{url:"",events:["task/moved"],enabled:!0}])},"+ 加一个")),a("div",{className:"ds-panel-body"},L.length===0?a("div",{className:"ds-empty"},"还没有 webhook。配一个就能把卡片流转、用例失败、门禁拦截推给 CI / IM / 工单系统。"):L.map((ae,z)=>a("div",{key:z,style:{marginBottom:8}},a("div",{className:"ds-row",style:{gap:6,alignItems:"center"}},a(u,{className:"ds-input",style:{flex:1},placeholder:"https://ci.example/hook",value:ae.url??"",onChange:te=>w(xe=>xe.map((X,ie)=>ie===z?{...X,url:te.target.value}:X))}),a(v,{checked:ae.enabled!==!1,onChange:te=>w(xe=>xe.map((X,ie)=>ie===z?{...X,enabled:te.target.checked}:X))}),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>w(te=>te.filter((xe,X)=>X!==z))},"×")),a("div",{className:"ds-row",style:{gap:10,flexWrap:"wrap",marginTop:2}},["task/moved","case/failed","gate/blocked","agent/failed","regression/finished"].map(te=>a("label",{className:"ds-check",key:te},a(v,{checked:(ae.events??[]).includes(te),onChange:xe=>w(X=>X.map((ie,ge)=>ge===z?{...ie,events:xe.target.checked?Array.from(new Set([...ie.events??[],te])):(ie.events??[]).filter(V=>V!==te)}:ie))}),a("span",{className:"ds-check-text",style:{fontSize:12}},te))),(ae.events??[]).length===0?a("span",{className:"ds-hint",style:{fontSize:12}},"一个都没勾 = 订阅全部事件"):null))),L.length===0?null:a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void R()},"保存 webhook")))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"测试环境",a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>h({name:"",baseUrl:""})},"+ 新增环境")),a("div",{className:"ds-panel-body"},g.length===0?a("div",{className:"ds-empty"},"还没有环境。接口用例里的相对地址会拼到这里配置的 baseUrl。"):g.map(ye))),a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"执行约束"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"允许访问的域名(逗号分隔,支持 *.example.com)"),a(u,{className:"ds-input",value:y,placeholder:"127.0.0.1, localhost, test.example.com",onChange:ae=>S(ae.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"单条用例超时(毫秒)"),a(u,{className:"ds-input",value:C,onChange:ae=>E(ae.target.value)})),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void fe()},_?"保存中…":"保存约束")),M===null?null:a("div",{className:"ds-ok"},M),D===null?null:a("div",{className:"ds-err"},D))))}function Bx({config:e,onChanged:t}){const{h:a,React:n,Editor:r,api:s,toast:o,confirmDialog:i,tipOf:l,tipLabel:c,TextField:u,TextAreaField:p,ChipField:f,CheckField:v,SelectField:x,SideDrawer:b}=vt(),k=e.feishu??{},[g,d]=n.useState(""),[h,y]=n.useState(String(k.escalateAfter??3)),[S,C]=n.useState(!1),[E,_]=n.useState(null),[I,D]=n.useState(null),W=async()=>{C(!0),_(null),D(null);try{const M={};String(g??"").trim()!==""&&(M.escalateWebhook=String(g).trim()),M.escalateAfter=Number(h)||3,await s("/config",{feishu:M}),d(""),_("已保存:连续失败 "+(Number(h)||3)+" 次后往升级群发一条"),await t()}catch(M){D(M instanceof Error?M.message:String(M))}finally{C(!1)}};return a("div",{className:"ds-panel ds-form-panel",style:{marginTop:14}},a("div",{className:"ds-panel-head"},"通知升级"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"升级群 Webhook"),a(u,{className:"ds-input",value:g,placeholder:k.escalateWebhookSet===!0?"已设置(留空表示不修改)":"https://open.feishu.cn/open-apis/bot/v2/hook/…",onChange:M=>d(M.target.value)})),a("div",{className:"ds-field"},c("连续失败几次后升级","同一通道连续失败到这个次数,就给升级群发消息"),a(u,{className:"ds-input",style:{width:90},value:h,onChange:M=>y(M.target.value)})),a("div",{className:"ds-hint"},"飞书通知、GitLab、云盘归档、代码隔离任一通道连续失败达到阈值时,只在跨过阈值那一次往升级群发一条,包含通道、失败次数与最近错误。"),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:S,onClick:()=>void W()},S?"保存中…":"保存升级设置")),E===null?null:a("div",{className:"ds-ok"},E),I===null?null:a("div",{className:"ds-err"},I)))}function Ux({config:e,onChanged:t}){const{h:a,React:n,Editor:r,api:s,toast:o,confirmDialog:i,tipOf:l,tipLabel:c,TextField:u,TextAreaField:p,ChipField:f,CheckField:v,SelectField:x,SideDrawer:b}=vt(),[k,g]=n.useState(()=>(e.environments??[]).map(J=>({id:J.id,name:J.name,kind:J.kind,note:J.note}))),[d,h]=n.useState(!1),[y,S]=n.useState(null),[C,E]=n.useState(null),[_,I]=n.useState(null),D=async J=>{I(null);try{await s("/config",{releaseRules:J}),I("发布准入已更新"),await t()}catch(U){I(U instanceof Error?U.message:String(U))}},W=()=>g(J=>J.length>=12?J:[...J,{id:"",name:"",kind:"other",note:""}]),M=(J,U)=>g(F=>F.map((L,w)=>w===J?{...L,...U}:L)),P=J=>g(U=>U.filter((F,L)=>L!==J)),H=async()=>{h(!0),S(null),E(null);try{const J=k.map(U=>({...U,name:String(U.name??"").trim()})).filter(U=>U.name!=="");if(J.length===0)throw new Error("至少留一个环境,否则部署记录没处挂");await s("/config",{environments:J}),S(`已保存 ${J.length} 个环境`),o(`已保存 ${J.length} 个环境`),await t()}catch(J){E(J instanceof Error?J.message:String(J))}finally{h(!1)}};return a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"部署环境",a("span",{className:"ds-hint"},"只登记,不触发")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-hint"},"这些环境只用来在版本上留痕:谁在什么时候把哪个版本放到了哪个环境。平台不会调用任何部署脚本、webhook 或 CI,部署仍然由人或你们自己的流水线完成。"),k.map((J,U)=>a("div",{key:`env-${U}`,className:"ds-row",style:{gap:8,marginTop:8,alignItems:"flex-end"}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"名字"),a(u,{className:"ds-input",value:J.name,placeholder:"例如 预发",onChange:F=>M(U,{name:F.target.value})})),a("div",{className:"ds-field",style:{flex:"0 0 120px"}},a("div",{className:"ds-label"},"类型"),a(x,{className:"ds-input",value:J.kind,onChange:F=>M(U,{kind:F.target.value})},["dev","staging","prod","other"].map(F=>a("option",{key:F,value:F},F)))),a("div",{className:"ds-field",style:{flex:2}},a("div",{className:"ds-label"},"说明"),a(u,{className:"ds-input",value:J.note,placeholder:"例如 上线前最后一关",onChange:F=>M(U,{note:F.target.value})})),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>P(U),"data-danger":"1"},"删除"))),a("div",{className:"ds-row",style:{gap:8,marginTop:10}},a("button",{className:"ds-btn",onClick:W,disabled:k.length>=12},"+ 加一个环境")),a("div",{className:"ds-hint"},"id 由名字生成一次就固定了:改名不会断开已有的部署记录;删掉的环境,历史记录里的名字会显示成 id。"),a("div",{className:"ds-row",style:{justifyContent:"flex-end",marginTop:10}},a("button",{className:"ds-btn ds-btn-primary",disabled:d,onClick:()=>void H()},d?"保存中…":"保存环境")),y===null?null:a("div",{className:"ds-ok"},y),C===null?null:a("div",{className:"ds-err"},C)),a("div",{className:"ds-panel-body",style:{borderTop:"1px solid var(--ds-line)"}},a("div",{className:"ds-label"},"发布准入(发布时会检查,不满足要「强制发布」并写原因)"),a("label",{className:"ds-check",style:{marginTop:8}},a(v,{checked:(e.releaseRules?.requireStaging??!1)===!0,onChange:J=>void D({requireStaging:J.target.checked})}),a("span",{className:"ds-check-text"},"发布前必须先在「预发」环境登记过一次部署")),a("label",{className:"ds-check",style:{marginTop:6}},a(v,{checked:(e.releaseRules?.requireRequirementSignoff??!1)===!0,onChange:J=>void D({requireRequirementSignoff:J.target.checked})}),a("span",{className:"ds-check-text"},"关联需求必须都签过验收字")),a("div",{className:"ds-hint",style:{marginTop:6}},"两条默认都不勾:不勾的时候发布只看门禁,不看你上没上过预发、需不需要签字。"),_===null?null:a("div",{className:"ds-ok"},_)))}function qx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,CheckField:c,SelectField:u,React:p}=vt(),f=e.ldap??{},[v,x]=p.useState({enabled:f.enabled===!0,url:f.url??"ldap://localhost:389",baseDN:f.baseDN??"DC=LYF,DC=COM",bindDN:f.bindDN??"cn=root",bindPassword:"",userFilter:f.userFilter??"(|(sAMAccountName={user})(uid={user})(mail={user}))",nameAttr:f.nameAttr??"displayName",mailAttr:f.mailAttr??"mail",deptAttr:f.deptAttr??"department",defaultRole:f.defaultRole??"member",requireLogin:f.requireLogin===!0,timeoutMs:String(f.timeoutMs??6e3)}),[b,k]=p.useState(!1),[g,d]=p.useState(!1),[h,y]=p.useState(null),[S,C]=p.useState(null),[E,_]=p.useState([]),I=e.localAuth??{},[D,W]=p.useState(""),[M,P]=p.useState(""),[H,J]=p.useState(!1),[U,F]=p.useState(null),[L,w]=p.useState(null),T=async()=>{J(!0),F(null),w(null);try{await r("/auth/password",{username:I.username,currentPassword:D,newPassword:M}),W(""),P(""),F("密码已更新"),t()}catch(G){w(G instanceof Error?G.message:String(G))}finally{J(!1)}},K=G=>x(we=>({...we,...G})),R=async()=>{d(!0),y(null),C(null),_([]);try{const G=await r("/auth/ldap/test",{ldap:{url:v.url,baseDN:v.baseDN,bindDN:v.bindDN,userFilter:v.userFilter,nameAttr:v.nameAttr,mailAttr:v.mailAttr,deptAttr:v.deptAttr,timeoutMs:Number(v.timeoutMs)},bindPassword:v.bindPassword});_(G.steps??[]),y(G.message??"连接正常"),s(G.message??"连接正常",G.ok===!1?"error":"success")}catch(G){const we=G instanceof Error?G.message:String(G);C(we)}finally{d(!1)}},O=async()=>{k(!0),y(null),C(null);try{const G={ldap:{enabled:v.enabled,url:v.url.trim(),baseDN:v.baseDN.trim(),bindDN:v.bindDN.trim(),userFilter:v.userFilter.trim(),nameAttr:v.nameAttr.trim(),mailAttr:v.mailAttr.trim(),deptAttr:v.deptAttr.trim(),defaultRole:v.defaultRole,requireLogin:v.requireLogin,timeoutMs:Number(v.timeoutMs)||6e3}};v.bindPassword.trim()!==""&&(G.ldap.bindPassword=v.bindPassword.trim()),await r("/config",G),K({bindPassword:""}),y(v.bindPassword.trim()===""?"已保存(服务账号密码没改)":"已保存,服务账号密码已写入密钥文件"),await t()}catch(G){C(G instanceof Error?G.message:String(G))}finally{k(!1)}};return a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"统一登录(LDAP)",a("span",{className:"ds-hint"},f.bindPasswordSet===!0?"服务账号密码已保存":"服务账号密码还没保存")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-hint"},"开启后,页头会出现登录框:用域账号登录,平台拿到的用户名会记进 config.users(首次登录建档、之后只更新最近登录),写操作和审计都挂在这个名字下。"),a("label",{className:"ds-check",style:{marginTop:8}},a(c,{checked:v.enabled,onChange:G=>K({enabled:G.target.checked})}),a("span",{className:"ds-check-text"},"启用 LDAP 登录")),a("label",{className:"ds-check",style:{marginTop:6}},a(c,{checked:v.requireLogin,onChange:G=>K({requireLogin:G.target.checked})}),a("span",{className:"ds-check-text"},"必须登录才能用(不勾也可以先配好再打开)")),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:2}},a("div",{className:"ds-label"},"服务器地址"),a(l,{className:"ds-input",value:v.url,placeholder:"ldap://host:389",onChange:G=>K({url:G.target.value})})),a("div",{className:"ds-field",style:{flex:"0 0 110px"}},a("div",{className:"ds-label"},"超时(毫秒)"),a(l,{className:"ds-input",value:v.timeoutMs,onChange:G=>K({timeoutMs:G.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"BASE DN"),a(l,{className:"ds-input",value:v.baseDN,placeholder:"DC=LYF,DC=COM",onChange:G=>K({baseDN:G.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"服务账号 DN"),a(l,{className:"ds-input",value:v.bindDN,placeholder:"cn=root",onChange:G=>K({bindDN:G.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},f.bindPasswordSet===!0?"服务账号密码(留空 = 不改)":"服务账号密码"),a(l,{className:"ds-input",type:"password",value:v.bindPassword,placeholder:f.bindPasswordSet===!0?"已保存,留空表示不修改":"写一次就进密钥文件",onChange:G=>K({bindPassword:G.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"用户过滤器({user} 会被替换)"),a(l,{className:"ds-input",value:v.userFilter,onChange:G=>K({userFilter:G.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"显示名属性"),a(l,{className:"ds-input",value:v.nameAttr,onChange:G=>K({nameAttr:G.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"邮箱属性"),a(l,{className:"ds-input",value:v.mailAttr,onChange:G=>K({mailAttr:G.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"部门属性"),a(l,{className:"ds-input",value:v.deptAttr,onChange:G=>K({deptAttr:G.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:10}},a("button",{className:"ds-btn",disabled:g,onClick:()=>void R()},g?"测试中…":"测试连接"),a("button",{className:"ds-btn ds-btn-primary",disabled:b,onClick:()=>void O()},b?"保存中…":"保存")),E.length===0?null:a("div",{style:{marginTop:8}},E.map((G,we)=>a("div",{key:`ldap-step-${we}`,className:G.ok===!0?"ds-ok":"ds-error"},`${G.ok===!0?"✓":"✗"} ${G.name}:${G.message}${G.ms>0?`(${G.ms}ms)`:""}`))),h===null?null:a("div",{className:"ds-ok",style:{marginTop:8}},h),S===null?null:a("div",{className:"ds-error",style:{marginTop:8}},S),a("div",{className:"ds-hint",style:{marginTop:10}},"依赖系统里的 ldapsearch(Ubuntu:apt-get install ldap-utils)。密码只写进 ~/.dsh/dsh-studio.secrets.json(0600),不会出现在状态、导出和日志里。")),a("div",{className:"ds-panel-body",style:{borderTop:"1px solid var(--ds-line)"}},a("div",{className:"ds-label"},"本地超管账号"),a("div",{className:"ds-hint",style:{marginTop:4}},`账号:${I.username??"admin"} · ${I.hasPassword===!0?"已设置自己的密码":"还在用默认口令 admin"}。没启用 LDAP 时平台默认以这个身份使用,不需要登录;启用 LDAP 后它仍可用来兜底登录。`),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"当前密码"),a(l,{className:"ds-input",type:"password",value:D,onChange:G=>W(G.target.value)})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"新密码(至少 8 位)"),a(l,{className:"ds-input",type:"password",value:M,onChange:G=>P(G.target.value)})),a("button",{className:"ds-btn",disabled:H||D===""||M.length<8,onClick:()=>void T()},H?"提交中…":"改密码")),U===null?null:a("div",{className:"ds-ok",style:{marginTop:6}},U),L===null?null:a("div",{className:"ds-error",style:{marginTop:6}},L)),a("div",{className:"ds-panel-body",style:{borderTop:"1px solid var(--ds-line)"}},a("div",{className:"ds-label"},`登录过的人(${(e.users??[]).length})`),(e.users??[]).length===0?a("div",{className:"ds-hint",style:{marginTop:6}},"还没有人用 LDAP 登录过。"):a("div",{style:{marginTop:6}},(e.users??[]).slice(0,20).map(G=>a("div",{key:G.id,className:"ds-kv"},a("div",null,G.name),a("div",null,`${G.id} · ${G.dept===""?"(无部门)":G.dept} · 登录 ${G.loginCount} 次 · 最近 ${Ke(G.lastLoginAt)}`))))))}function Vx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,CheckField:c,SelectField:u,React:p}=vt(),f=e.costs??{},[v,x]=p.useState(String(f.budgetRuns??500)),[b,k]=p.useState(String(f.budgetHours??80)),[g,d]=p.useState(String(f.warnAt??80)),[h,y]=p.useState(!1),[S,C]=p.useState(null),[E,_]=p.useState(null),I=async()=>{y(!0),C(null),_(null);try{const D=Number(v),W=Number(b),M=Number(g);if(!Number.isFinite(D)||D<=0)throw new Error("预算次数要是正整数");if(!Number.isFinite(W)||W<=0)throw new Error("预算小时要是正整数");await r("/config",{costs:{budgetRuns:Math.round(D),budgetHours:Math.round(W),warnAt:Number.isFinite(M)?Math.min(100,Math.max(10,Math.round(M))):80}}),C("已保存:预算"),await t()}catch(D){_(D instanceof Error?D.message:String(D))}finally{y(!1)}};return a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"成本预算"),a("div",{className:"ds-panel-body ds-grid2"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"每月派单次数上限"),a(l,{className:"ds-input",value:v,onChange:D=>x(D.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"每月机上时长上限(小时)"),a(l,{className:"ds-input",value:b,onChange:D=>k(D.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"警告阈值(百分比)"),a(l,{className:"ds-input",value:g,onChange:D=>d(D.target.value)})),a("div",{className:"ds-field ds-span"},a("div",{className:"ds-hint"},"效能页的「AI 执行成本」下方会出现预算条;口径是派单次数与机上时长,宿主没有回传 token 用量。")),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:h,onClick:()=>void I()},h?"保存中…":"保存预算")),S===null?null:a("div",{className:"ds-ok"},S),E===null?null:a("div",{className:"ds-err"},E)))}function Wx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,CheckField:c,SelectField:u,React:p}=vt(),f=e.projects??[],v=e.gitlab?.projects??[],[x,b]=p.useState(null),[k,g]=p.useState(null),[d,h]=p.useState(null),[y,S]=p.useState(!1),[C,E]=p.useState({name:"",start:"",due:"",status:"planned"}),_=async()=>{if(x!==null){S(!0),g(null),h(null);try{await r("/projects/save",{project:{...x,iterations:x.iterations??[]}}),h("已保存"),s("已保存"),await t()}catch(w){g(w instanceof Error?w.message:String(w))}finally{S(!1)}}},I=async w=>{if(await o(`删除项目「${w.name}」?卡片会保留,但不再归属任何项目。`,{title:"删除项目",danger:!0,okText:"删除"})){S(!0),g(null);try{await r("/projects/delete",{id:w.id}),b(null),await t()}catch(T){g(T instanceof Error?T.message:String(T))}finally{S(!1)}}},D=async w=>{if(C.name.trim()!==""){S(!0),g(null);try{await r("/projects/iteration",{project:w.id,name:C.name.trim(),start:jr(C.start),due:jr(C.due),status:C.status}),E({name:"",start:"",due:"",status:"planned"}),await t()}catch(T){g(T instanceof Error?T.message:String(T))}finally{S(!1)}}},W=async(w,T)=>{S(!0),g(null);try{await r("/projects/iteration",{project:w.id,action:"remove",iteration:T.id}),await t()}catch(K){g(K instanceof Error?K.message:String(K))}finally{S(!1)}},M=(w,T)=>b({...x,[w]:T}),P=(w,T,K)=>b({...x,services:(x.services??[]).map((R,O)=>O===w?{...R,[T]:K}:R)}),H=()=>b({...x,services:[...x.services??[],{id:"",name:"",repoId:"",repoPath:"",baseBranch:x.baseBranch||"main",dependsOn:[]}]}),J=w=>b({...x,services:(x.services??[]).filter((T,K)=>K!==w)}),U=x===null?[]:x.services??[],F=(w,T)=>b({...x,services:(x.services??[]).map((K,R)=>{if(R!==w)return K;const O=Array.isArray(K.dependsOn)?K.dependsOn:[];return{...K,dependsOn:O.includes(T)?O.filter(G=>G!==T):[...O,T]}})}),L=f.map(w=>a("div",{className:"ds-listrow",key:w.id},a("div",{className:"ds-listrow-main"},a("div",{},`${w.key?`[${w.key}] `:""}${w.name} · ${Xl[w.status]??w.status}${w.lead?` · 负责人 ${w.lead}`:""}`),a("div",{className:"ds-hint"},`${(e.projects??[]).find(T=>T.id===w.id)?.iterations?.length??0} 个迭代 · ${(w.services??[]).length} 个服务${(()=>{const T=(w.services??[]).reduce((K,R)=>K+(R.dependsOn??[]).length,0);return T>0?` · ${T} 条依赖`:""})()}${w.description?` · ${w.description}`:""}`)),a("div",{className:"ds-listrow-actions"},i("改项目信息、加迭代",a("button",{className:"ds-btn",disabled:y,onClick:()=>b({...w})},"编辑 / 迭代")),i("删除这个项目(卡不会删)",a("button",{className:"ds-btn",disabled:y,onClick:()=>void I(w),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(n,{title:"业务项目(一个项目可以挂很多张卡和很多个迭代)",hint:"项目是看板的第一层分组:卡片挂项目,项目下面划迭代,看板可按项目和迭代筛选,项目页签看每个项目的体检数据。",rows:L,onAdd:()=>b({id:"",name:"",key:"",description:"",lead:"",status:"active",repos:[],services:[],iterations:[]}),addLabel:"新增项目",render:w=>w,empty:"还没有项目。团队只跑一个产品也可以先不建,直接在看板上干活。"}),d!==null?a("div",{className:"ds-ok",style:{marginBottom:8}},d):null,x===null?null:a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},x.id?`编辑项目:${x.name||x.id}`:"新增项目"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-row",style:{gap:12}},a("div",{className:"ds-field",style:{flex:"0 0 110px"}},a("div",{className:"ds-label"},"代号"),a(l,{className:"ds-input",value:x.key??"",placeholder:"ORD",onChange:w=>M("key",w.target.value)})),a("div",{className:"ds-field",style:{flex:2}},a("div",{className:"ds-label"},"项目名"),a(l,{className:"ds-input",value:x.name??"",placeholder:"订单中心",onChange:w=>M("name",w.target.value)})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"状态"),a(u,{className:"ds-select",value:x.status??"active",onChange:w=>M("status",w.target.value)},Object.keys(Xl).map(w=>a("option",{key:w,value:w},Xl[w])))),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"业务负责人"),a(l,{className:"ds-input",value:x.lead??"",placeholder:"张伟",onChange:w=>M("lead",w.target.value)}))),a("div",{className:"ds-row",style:{gap:12}},a("div",{className:"ds-field",style:{flex:3}},a("div",{className:"ds-label"},"默认代码目录(没有分服务时用)"),a(l,{className:"ds-input",value:x.repoPath??"",placeholder:"~/repos/order-center",onChange:w=>M("repoPath",w.target.value)})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"基线分支"),a(l,{className:"ds-input",value:x.baseBranch??"",placeholder:"main",onChange:w=>M("baseBranch",w.target.value)}))),a("div",{className:"ds-hint",style:{margin:"-6px 0 4px"}},"配了代码目录且「代码隔离」选 worktree 时,每张卡会切一条 task/<编号> 分支,互不干扰。项目里有多个服务时,用下面的服务列表分开配。"),a("div",{className:"ds-field"},a("div",{className:"ds-label"},`服务(${U.length})· 一个服务一个 git 路径,卡片可以指定改哪个`),U.length===0?a("div",{className:"ds-empty"},"还没有分服务。只有一个仓库时留空即可,走上面的默认代码目录。"):U.map((w,T)=>{const K=U.filter((R,O)=>O!==T&&String(R.id??"").trim()!=="");return a("div",{key:`${w.id}-${T}`,style:{marginBottom:8}},a("div",{className:"ds-row",style:{gap:8,alignItems:"flex-end"}},a("div",{className:"ds-field",style:{flex:"0 0 150px"}},a("div",{className:"ds-label"},"服务名"),a(l,{className:"ds-input",value:w.name??"",placeholder:"order-service",onChange:R=>P(T,"name",R.target.value)})),a("div",{className:"ds-field",style:{flex:3}},a("div",{className:"ds-label"},"本机 git 路径"),a(l,{className:"ds-input",value:w.repoPath??"",placeholder:"~/repos/order-service",onChange:R=>P(T,"repoPath",R.target.value)})),a("div",{className:"ds-field",style:{flex:"0 0 110px"}},a("div",{className:"ds-label"},"基线分支"),a(l,{className:"ds-input",value:w.baseBranch??"",placeholder:"main",onChange:R=>P(T,"baseBranch",R.target.value)})),v.length===0?null:a("div",{className:"ds-field",style:{flex:"0 0 150px"}},a("div",{className:"ds-label"},"GitLab 仓库"),a(u,{className:"ds-select",value:w.repoId??"",onChange:R=>P(T,"repoId",R.target.value)},a("option",{value:""},"不关联"),v.map(R=>a("option",{key:R.id,value:R.id},R.name||R.project)))),a("button",{className:"ds-btn",type:"button",onClick:()=>J(T),"data-danger":"1"},"删除")),K.length===0?null:a("div",{className:"ds-row",style:{gap:6,alignItems:"center",marginTop:4,flexWrap:"wrap"}},a("span",{className:"ds-hint"},"依赖"),a("span",{className:"ds-hint",style:{marginRight:2}},"(勾上的服务要先完成,本服务的卡才能收口)"),K.map(R=>a("span",{key:R.id,className:"ds-chip","data-on":(w.dependsOn??[]).includes(R.id)?"1":"0",title:`依赖 ${R.name||R.id}`,onClick:()=>F(T,R.id)},R.name||R.id))))}),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn",type:"button",onClick:()=>H()},"+ 加一个服务"),a("span",{className:"ds-hint"},"服务名会被写进任务详情;worktree 从该服务的 git 路径切 task/<编号> 分支。"))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"项目说明"),a(l,{className:"ds-input",value:x.description??"",placeholder:"这个项目做什么、给谁用",onChange:w=>M("description",w.target.value)})),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:y,onClick:()=>void _()},y?"保存中…":"保存项目"),a("button",{className:"ds-btn",disabled:y,onClick:()=>b(null)},"取消")))),x===null||x.id===""?null:a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"迭代",a("span",{className:"ds-hint"},"迭代用来按时间切需求,看板可按迭代筛选")),a("div",{className:"ds-panel-body"},(x.iterations??[]).length===0?a("div",{className:"ds-empty"},"还没有迭代。可以先加一个「2026-11 迭代」。"):(x.iterations??[]).map(w=>a("div",{className:"ds-listrow",key:w.id},a("div",{className:"ds-listrow-main"},a("div",{},`${w.name} · ${Zl[w.status]??w.status}`),a("div",{className:"ds-hint"},`${w.start?new Date(w.start).toISOString().slice(0,10):"未定开始"} → ${w.due?new Date(w.due).toISOString().slice(0,10):"未定结束"}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:y,onClick:()=>void W(x,w)},"×")))),a("div",{className:"ds-row",style:{gap:8,alignItems:"flex-end"}},a("div",{className:"ds-field",style:{flex:2}},a("div",{className:"ds-label"},"迭代名"),a(l,{className:"ds-input",value:C.name,placeholder:"2026-11 迭代",onChange:w=>E({...C,name:w.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"开始"),a(l,{className:"ds-input",type:"date",value:C.start,onChange:w=>E({...C,start:w.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"结束"),a(l,{className:"ds-input",type:"date",value:C.due,onChange:w=>E({...C,due:w.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"状态"),a(u,{className:"ds-select",value:C.status,onChange:w=>E({...C,status:w.target.value})},Object.keys(Zl).map(w=>a("option",{key:w,value:w},Zl[w])))),a("button",{className:"ds-btn",disabled:y||C.name.trim()==="",onClick:()=>void D(x)},"+ 加迭代")),a("div",{className:"ds-hint"},"迭代改完会立刻反映到看板的迭代筛选。"))))}function Hx({state:e,config:t,onChanged:a}){const{h:n,sessionToken:r,api:s,toast:o,currentUser:i,confirmDialog:l,TextField:c,ChipField:u,CheckField:p,SelectField:f,React:v}=vt(),x=t.access??{enabled:!1,members:[]},b=e.audit??[],[k,g]=v.useState({name:"",role:"member"}),[d,h]=v.useState(!1),[y,S]=v.useState(null),[C,E]=v.useState(""),[_,I]=v.useState(!1),[D,W]=v.useState(""),M=async(w,T)=>{h(!0),S(null);try{await s(w,T),await a()}catch(K){{const R=K instanceof Error?K.message:String(K);S(R),o(R,"error")}}finally{h(!1)}},P=async()=>{const w=k.name.trim();w!==""&&(await M("/access/save",{member:{name:w,role:k.role}}),g({name:"",role:"member"}))},H=Array.from(new Set(b.map(w=>w.by))).sort(),J=D.trim().toLowerCase(),U=b.filter(w=>(C===""||w.by===C)&&(_===!1||w.ok===!1)&&(J===""||`${ROUTE_LABEL[w.route]??w.route} ${w.by} ${w.summary}`.toLowerCase().includes(J))),F=U.slice(0,120),L=()=>{const w=new URLSearchParams({by:C,q:D.trim(),failed:_?"1":"0",token:r()??""}),T=document.createElement("a");T.href=`${API}/audit/export.csv?${w.toString()}`,T.rel="noopener",document.body.appendChild(T),T.click(),document.body.removeChild(T),o(`正在导出 ${U.length} 条审计记录`)};return n("div",{className:"ds-pane-stack"},n("div",{className:"ds-panel ds-form-panel"},n("div",{className:"ds-panel-head"},n("span",null,"成员权限"),n(u,{className:"ds-chip"},`当前身份:${i()}`)),n("div",{className:"ds-panel-body"},n("div",{className:"ds-row",style:{gap:10,alignItems:"center",flexWrap:"wrap"}},n("label",{className:"ds-check",style:{flex:"0 0 auto"}},n(p,{checked:x.enabled===!0,disabled:d,onChange:w=>void M("/access/save",{enabled:w.target.checked})}),n("span",null,"启用成员权限")),n("span",{className:"ds-hint"},x.enabled===!0?"已启用:写操作要认人,删除/配置/发布要管理员。":"未启用:任何人都是管理员(单机用法)。")),x.enabled===!0&&(x.members??[]).length===0?n("div",{className:"ds-error"},"已启用但名单是空的:除了名单里的人,其他人都动不了。先把你自己加进去。"):null,x.enabled===!0?n("label",{className:"ds-check"},n(p,{checked:(x.agents??"inherit")==="inherit",disabled:d,onChange:w=>void M("/access/save",{agents:w.target.checked?"inherit":"bypass"})}),n("span",null,"AI 专家也受这份名单约束")):null,x.enabled===!0?n("div",{className:"ds-hint"},(x.agents??"inherit")==="inherit"?"专家调工具时按同一张名单判权限:只读专家只能看,成员能干活,管理员才能改配置。名单里没有的专家会被拦下,记得在下面加进去。":"已豁免:专家调用工具不受名单限制,但仍记审计。适合「全交给 AI」的团队。"):null,(x.members??[]).map(w=>n("div",{key:w.name,className:"ds-listrow"},n("div",{className:"ds-listrow-main"},n("div",null,w.name,w.name===i()?n(u,{className:"ds-chip ds-chip-soft"},"我"):null),n("div",{className:"ds-hint"},(ACCESS_ROLE_OPTIONS.find(T=>T.id===w.role)??ACCESS_ROLE_OPTIONS[1]).hint)),n("div",{className:"ds-listrow-actions"},n(f,{className:"ds-select",value:w.role,disabled:d,onChange:T=>void M("/access/save",{member:{name:w.name,role:T.target.value}})},ACCESS_ROLE_OPTIONS.map(T=>n("option",{key:T.id,value:T.id},T.label))),n("button",{className:"ds-btn ds-btn-ghost",disabled:d,onClick:()=>void M("/access/delete",{name:w.name})},"移除")))),(x.members??[]).length===0?n("div",{className:"ds-empty"},"还没有成员。"):null,n("div",{className:"ds-row",style:{gap:8,flexWrap:"wrap"}},n(c,{className:"ds-input",placeholder:"成员名字(和页头「👤 我是…」一致)",value:k.name,style:{flex:"1 1 200px"},onChange:w=>g({...k,name:w.target.value})}),n(f,{className:"ds-select",value:k.role,style:{flex:"0 0 130px"},onChange:w=>g({...k,role:w.target.value})},ACCESS_ROLE_OPTIONS.map(w=>n("option",{key:w.id,value:w.id},w.label))),n("button",{className:"ds-btn ds-btn-primary",disabled:d||k.name.trim()==="",onClick:()=>void P()},"加成员")),y!==null?n("div",{className:"ds-error"},y):null)),n("div",{className:"ds-panel"},n("div",{className:"ds-panel-head"},n("span",null,`审计日志(显示 ${F.length} / 命中 ${U.length} / 共 ${b.length} 条)`),n("div",{className:"ds-row",style:{gap:8,marginLeft:"auto"}},n(c,{className:"ds-input",placeholder:"搜动作 / 人 / 详情",value:D,style:{width:170},onChange:w=>W(w.target.value)}),n(f,{className:"ds-select",value:C===""?"__all__":C,style:{minWidth:150},onChange:w=>E(w.target.value==="__all__"?"":w.target.value)},n("option",{value:"__all__"},"所有人"),H.map(w=>n("option",{key:w,value:w},w))),n("label",{className:"ds-check"},n(p,{checked:_,onChange:w=>I(w.target.checked)}),n("span",null,"只看失败")),n("button",{className:"ds-btn ds-btn-ghost",disabled:d,onClick:async()=>{await l("清空审计日志?清掉之后所有操作记录都没了。",{title:"清空审计日志",danger:!0,okText:"清空"})&&M("/audit/clear",{})}},"清空"),n("button",{className:"ds-btn ds-btn-ghost",disabled:U.length===0,onClick:L,title:"把当前筛选结果导出为 CSV(Excel 可直接打开)"},"导出 CSV"))),n("div",{className:"ds-panel-body"},F.length===0?n("div",{className:"ds-empty"},b.length===0?"还没有记录。做一次写操作就会出现在这里。":"没有符合筛选条件的记录。把关键字清空或换个人试试。"):F.map(w=>n("div",{key:w.id,className:"ds-audit-row","data-ok":w.ok?"1":"0"},n("span",{className:"ds-audit-time"},Ke(w.at)),n("span",{className:"ds-audit-by"},w.by),n("span",{className:"ds-audit-what"},ROUTE_LABEL[w.route]??w.route),n("span",{className:"ds-audit-detail"},w.summary),n("span",{className:"ds-audit-status","data-ok":w.ok?"1":"0"},w.ok?"成功":`失败 ${w.status}`))))))}function Gx({state:e,onChanged:t}){const{h:a,sessionToken:n,api:r,toast:s,currentUser:o,confirmDialog:i,TextField:l,ChipField:c,CheckField:u,SelectField:p,React:f}=vt(),v=e.config,[x,b]=f.useState(null),[k,g]=f.useState(!1),[d,h]=f.useState(null),[y,S]=f.useState(""),C=e.statuses??[],E=f.useMemo(()=>rf(v),[v]),_=F=>{const L=v.columns?.[F];return L==null?[]:Array.isArray(L.members)?L.members:L.kind==="none"?[]:[{kind:L.kind,id:L.id}]},I=F=>v.columns?.[F]?.strategy??"round_robin",D=F=>{if(F.kind==="role")return"🎯 "+(za[F.id]??F.id);if(F.kind==="team"){const w=(v.teams??[]).find(T=>T.id===F.id);return w===void 0?`👥 ${F.id}`:`${w.emoji??"👥"} ${w.name}`}const L=(v.experts??[]).find(w=>w.id===F.id);return L===void 0?F.id:`${L.emoji} ${L.name}`},W=Array.isArray(v.design?.columns)?v.design.columns:["dev"],M=async F=>{g(!0),b(null);try{await r("/config",{design:{...v.design??{},...F}}),await t()}catch(L){b(L instanceof Error?L.message:String(L))}finally{g(!1)}},P=F=>{const L=W.includes(F)?W.filter(w=>w!==F):[...W,F];M({columns:L})},H=async(F,L,w)=>{g(!0),b(null);try{await r("/columns/save",{status:F,members:L,strategy:w}),await t()}catch(T){b(T instanceof Error?T.message:String(T))}finally{g(!1)}},J=(F,L)=>{const w=L.key.startsWith("t:")?"team":L.key.startsWith("r:")?"role":"expert",T=L.key.slice(2),K=_(F);K.some(R=>R.kind===w&&R.id===T)||(S(""),H(F,[...K,{kind:w,id:T}],I(F)))},U=(F,L)=>void H(F,_(F).filter((w,T)=>T!==L),I(F));return a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"每一列由谁负责"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-hint"},"推荐挂「角色位」:澄清/验收挂产品经理、开发挂研发工程师、测试挂测试工程师——派单时会按卡片的业务领域自动挑对应领域专家(如支付产品经理);也可以直接指定某位专家或专家团。一列可挂多人(候选池),一个都不挂就只把卡停在原地等人认领。"),a("div",{className:"ds-hint",style:{marginTop:14}},"设计闸门:打开后,卡片进下面选中的列之前设计必须评审通过(验收时可 override 强推)。"),a("div",{className:"ds-row",style:{gap:8,flexWrap:"wrap",marginTop:6}},a("button",{className:v.design?.enabled===!0?"ds-btn ds-btn-primary":"ds-btn",disabled:k,"data-design-gate":v.design?.enabled===!0?"on":"off",onClick:()=>void M({enabled:v.design?.enabled!==!0})},v.design?.enabled===!0?"设计闸门:已开启":"设计闸门:已关闭"),C.map(F=>a("button",{key:"gate-"+F.id,className:W.includes(F.id)?"ds-btn ds-btn-primary":"ds-btn",disabled:k,onClick:()=>P(F.id)},F.label??F.id))),C.map(F=>{const L=_(F.id),w=I(F.id);return a("div",{className:"ds-colrow",key:F.id},a("div",{className:"ds-colrow-head"},a("span",{className:"ds-colrow-name"},F.label??F.id),L.length===0?a("span",{className:"ds-hint"},"👤 等人认领(不自动派)"):L.map((T,K)=>a(c,{className:"ds-chip ds-chip-owner",key:`${T.kind}:${T.id}`},D(T),a("button",{className:"ds-chip-x",title:"从候选里去掉",disabled:k,onClick:()=>U(F.id,K)},"×"))),a("button",{className:"ds-btn",style:{marginLeft:"auto"},disabled:k,onClick:()=>{S(""),h(T=>T===F.id?null:F.id)}},d===F.id?"收起":"+ 加候选"),L.length>=2?a(p,{className:"ds-input",style:{width:240},value:w,disabled:k,onChange:T=>void H(F.id,L,T.target.value)},STRATEGIES.map(T=>a("option",{key:T.id,value:T.id},T.label))):null),d===F.id?a("div",{className:"ds-colpick"},a(l,{className:"ds-mention-search",autoFocus:!0,value:y,placeholder:"搜专家或专家团,点一下加进候选",onChange:T=>S(T.target.value)}),a("div",{className:"ds-colpick-list"},Ho(E,y).length===0?a("div",{className:"ds-mention-empty"},"没有匹配的专家或专家团"):Ho(E,y).map(T=>a("button",{key:T.key,className:"ds-mention-item",onClick:()=>J(F.id,T)},a("span",null,T.emoji),a("span",null,T.name),a("span",{className:"ds-hint"},T.hint))))):null)}),x!==null?a("div",{className:"ds-error"},x):null))}function Kx({state:e,onChanged:t}){const{h:a,sessionToken:n,api:r,toast:s,currentUser:o,confirmDialog:i,TextField:l,ChipField:c,CheckField:u,SelectField:p,React:f}=vt(),[v,x]=f.useState(e.storage??null),[b,k]=f.useState([]),[g,d]=f.useState(null),[h,y]=f.useState(null),[S,C]=f.useState(!1),E=f.useCallback(()=>{r("/snapshots").then(D=>{x(D.storage??null),k(D.snapshots??[]),d(null)}).catch(D=>d(D instanceof Error?D.message:String(D)))},[]);f.useEffect(()=>{E()},[E,e.revision]);const _=async D=>{await i(`把看板回滚到快照 #${D}?当前的这一版会先另存一份,不会丢。`,{title:"回滚快照",okText:"回滚"})&&(C(!0),y(null),r("/snapshot/restore",{id:D}).then(()=>{y(`已回滚到快照 #${D}`),E(),t?.()}).catch(W=>d(W instanceof Error?W.message:String(W))).finally(()=>C(!1)))},I=D=>{const W=new Date(Number(D)||0);return`${W.getMonth()+1}-${String(W.getDate()).padStart(2,"0")} ${String(W.getHours()).padStart(2,"0")}:${String(W.getMinutes()).padStart(2,"0")}:${String(W.getSeconds()).padStart(2,"0")}`};return a("div",{className:"ds-pane-stack"},a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"台账存储",a(c,{className:"ds-chip","data-on":v?.engine==="sqlite"?"1":"0"},v?.engine==="sqlite"?"SQLite":"单文件")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-an-kv"},a("span",{},"数据文件"),a("b",{},v?.db??"—")),a("div",{className:"ds-an-kv"},a("span",{},"密钥文件(600 权限,明文不进台账)"),a("b",{},v?.secretsFile??"—")),a("div",{className:"ds-an-kv"},a("span",{},"保留快照"),a("b",{},`${v?.snapshots??0} / ${v?.snapshotLimit??0}`)),a("div",{className:"ds-hint"},"每次写操作都会在同一个事务里先存一份快照,再更新台账;多进程同时写会排队(busy_timeout 5s),不会互相覆盖。"))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"快照",a("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:"auto"},onClick:E,disabled:S},"刷新")),a("div",{className:"ds-panel-body"},h!==null?a("div",{className:"ds-ok"},h):null,g!==null?a("div",{className:"ds-error"},g):null,b.length===0?a("div",{className:"ds-hint"},v?.engine==="sqlite"?"还没有快照。":"单文件模式没有快照(这台机器的 node 不支持 node:sqlite 时会退回单文件)。"):b.map(D=>a("div",{key:D.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`#${D.id} · rev ${D.revision}`),a("div",{className:"ds-hint"},`${I(D.at)} · ${(D.bytes/1024).toFixed(1)} KB`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:S,onClick:()=>_(D.id)},"回滚到这版")))))))}function Yx({state:e,config:t,onChanged:a}){const{h:n,sessionToken:r,api:s,toast:o,currentUser:i,confirmDialog:l,TextField:c,ChipField:u,CheckField:p,SelectField:f,React:v}=vt(),x=e.integrations??{channels:[],broken:0},[b,k]=v.useState(""),[g,d]=v.useState(null),h=(e.requirements??[]).find(w=>(w.folder?.token??"")!==""),y=e.agents??{},S=y.lock??null,C=S===null?0:Math.max(0,Date.now()-(Number(S.at)||0)),[E,_]=v.useState(""),[I,D]=v.useState(!1),W=E.trim().toLowerCase(),M=(y.log??[]).filter(w=>{const T=String(w?.line??"");return I&&!/失败|错误|异常|拦|拒绝/.test(T)?!1:W===""||T.toLowerCase().includes(W)}),P=M.slice(0,12),H=()=>{const w=new URLSearchParams({q:E.trim(),failed:I?"1":"0",token:r()??""}),T=document.createElement("a");T.href=`${API}/export/dispatch.csv?${w.toString()}`,T.rel="noopener",document.body.appendChild(T),T.click(),document.body.removeChild(T),o(`正在导出 ${M.length} 条派单日志`)},J=async w=>{k(w),d(null);try{if(w==="gitlab"){const T=await s("/gitlab/sync",{});d(T.report===void 0?"已触发 GitLab 同步":`已同步:${(T.report.projects??[]).map(K=>`${K.name} +${K.created}`).join(",")||"无新卡"}`)}else if(w==="drive")if(h===void 0)d("还没有带云盘文件夹的需求,先建一条需求并归档一次。");else{const T=await s("/requirement/sync",{id:h.id});d(`已归档 ${T.report?.written??0} / ${T.report?.total??0} 件`),o(`已归档 ${T.report?.written??0} / ${T.report?.total??0} 件`)}else if(w==="feishu"){const T=await s("/feishu/test",{});d(T.ok===!0?"测试消息已发出":`发送失败:${T.result?.error??"没配置机器人"}`)}else d("代码隔离没有独立的重试入口,下一张卡派单时会自动重试。");await a()}catch(T){{const K=T instanceof Error?T.message:String(T);d(K),o(K,"error")}}finally{k("")}},U=w=>{const T=Number(w)||0;if(T===0)return"—";const K=new Date(T);return`${K.getMonth()+1}-${String(K.getDate()).padStart(2,"0")} ${String(K.getHours()).padStart(2,"0")}:${String(K.getMinutes()).padStart(2,"0")}`},F={ok:"正常",error:"异常",idle:"还没用过"},L={ok:"#0f9d58",error:"#e5484d",idle:"#8a8f98"};return n("div",{className:"ds-pane-stack"},n("div",{className:"ds-panel"},n("div",{className:"ds-panel-head"},"派单运行状态",n(u,{className:"ds-chip","data-on":y.enabled===!0?"1":"0"},y.enabled===!0?"运行中":"已停用"),n("div",{className:"ds-row",style:{gap:8,marginLeft:"auto"}},n(c,{className:"ds-input",placeholder:"搜派单日志",value:E,style:{width:150},onChange:w=>_(w.target.value)}),n("label",{className:"ds-check"},n(p,{checked:I,onChange:w=>D(w.target.checked)}),n("span",null,"只看失败")),n("button",{className:"ds-btn ds-btn-ghost",disabled:M.length===0,onClick:H,title:"把当前筛选结果导出为 CSV"},"导出 CSV"))),n("div",{className:"ds-panel-body"},n("div",{className:"ds-kv"},n("dt",{},"实例"),n("dd",{},String(y.instance??"—")),n("dt",{},"派单锁"),n("dd",{},S===null?"还没抢到过(可能是内存模式)":`${S.mine===!0?"本进程持有":"其他实例持有"} · ${Math.round(C/1e3)} 秒前续租`),n("dt",{},"并发占用"),n("dd",{},`${(y.running??[]).length} / ${y.maxConcurrent??"—"}`),n("dt",{},"最近一轮"),n("dd",{},U(y.lastTickAt)),n("dt",{},"最近错误"),n("dd",{},String(y.lastError??"")===""?"无":String(y.lastError))),(y.log??[]).length===0?n("div",{className:"ds-empty"},"还没有派单日志。卡进入自动列后,这里会出现派单、空闲、失败记录。"):P.length===0?n("div",{className:"ds-empty"},"没有符合筛选条件的派单日志。"):n("div",{className:"ds-log"},P.map((w,T)=>n("div",{key:String(T),className:"ds-log-line"},`${U(w.at)} · ${String(w.line??"")}`))),(y.log??[]).length>0&&M.length>P.length?n("div",{className:"ds-hint"},`只显示最近 ${P.length} / 命中 ${M.length} 条,导出可看全量`):null)),n("div",{className:"ds-panel"},n("div",{className:"ds-panel-head"},"集成状态",n(u,{className:"ds-chip","data-on":x.broken===0?"1":"0"},x.broken===0?"全部正常":`${x.broken} 条异常`),n("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:"auto"},onClick:()=>void a()},"刷新")),n("div",{className:"ds-panel-body"},g!==null?n("div",{className:"ds-hint"},g):null,x.channels.length===0?n("div",{className:"ds-empty"},"还没有调用过任何外部集成。配好 GitLab / 飞书机器人 / 云盘文件夹后,第一次成功或失败就会出现在这里。"):x.channels.map(w=>n("div",{key:w.channel,className:"ds-listrow"},n("div",{className:"ds-listrow-main"},n("div",null,n("span",{style:{color:L[w.state],fontWeight:700}},"● "),w.label,n(u,{className:"ds-chip ds-chip-soft",style:{marginLeft:6}},F[w.state])),n("div",{className:"ds-hint"},w.hint),n("div",{className:"ds-hint"},`最近一次:${U(w.at)} · 成功 ${U(w.lastOkAt)} · 失败 ${U(w.lastErrorAt)}${w.attempts>0?` · 试着调了 ${w.attempts} 次,连挂 ${w.failures} 次`:""}`),w.lastError===""?null:n("div",{className:"ds-error"},w.lastError)),n("div",{className:"ds-listrow-actions"},n("button",{className:"ds-btn ds-btn-ghost",disabled:b!=="",onClick:()=>void J(w.channel)},b===w.channel?"重试中…":"再试一次")))))),n("div",{className:"ds-panel ds-form-panel"},n("div",{className:"ds-panel-head"},"失败了会怎样"),n("div",{className:"ds-panel-body"},n("div",{className:"ds-hint"},"· 飞书通知:同一条消息会自动重发一次,两次都失败才记异常(飞书 200 但 body 里 code≠0 也算失败)。"),n("div",{className:"ds-hint"},"· GitLab:拉 Issue、回写评论、对 MR 失败会记在这里;同步定时器仍会自动重试下一轮。"),n("div",{className:"ds-hint"},"· 飞书云盘:归档失败会挂在交付物自身的 doc.error 上,这里再汇总一份。"),n("div",{className:"ds-hint"},"· 代码隔离:worktree 建失败只影响这一张卡(退回普通工作区),不影响别的卡。"))))}function Qx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,ChipField:c,CheckField:u,MdField:p,SideDrawer:f,React:v}=vt(),x=e.gitlab?.projects??[],[b,k]=v.useState(null),[g,d]=v.useState(null),[h,y]=v.useState(null),[S,C]=v.useState(!1),E=async()=>{if(b!==null){C(!0),d(null),y(null);try{await r("/repos/save",{project:b}),k(null),y("已保存"),s("已保存"),await t()}catch(P){d(P instanceof Error?P.message:String(P))}finally{C(!1)}}},_=async P=>{if(await o(`删除 GitLab 项目「${P.name}」?已同步的卡片会保留。`,{title:"删除 GitLab 项目",danger:!0,okText:"删除"})){C(!0),d(null);try{await r("/repos/delete",{id:P.id}),await t()}catch(H){d(H instanceof Error?H.message:String(H))}finally{C(!1)}}},I=async P=>{C(!0),d(null),y(null);try{const H=await r("/repos/test",{id:P.id});y(`连通:${P.project}(示例 Issue ${H.sample} 条)`)}catch(H){d(H instanceof Error?H.message:String(H))}finally{C(!1)}},D=async P=>{C(!0),d(null),y(null);try{const J=((await r("/gitlab/sync",{project:P.id})).summary?.projects??[])[0];y(J===void 0?"没有可同步的项目":`${J.name}:拉取 ${J.fetched},新建 ${J.created}${J.error?",错误 "+J.error:""}`),await t()}catch(H){d(H instanceof Error?H.message:String(H))}finally{C(!1)}},W=(P,H)=>k({...b,[P]:H}),M=x.map(P=>a("div",{className:"ds-listrow",key:P.id},a("div",{className:"ds-listrow-main"},a("div",{},`${P.name||P.project}${P.syncEnabled?" · 自动同步":""}${P.tokenSet?"":" · 缺令牌"}`),a("div",{className:"ds-hint"},`${P.url||e.gitlab.url||"(未填地址)"} / ${P.project}${P.lastSyncAt?" · 上次 "+new Date(P.lastSyncAt).toLocaleString():""}`),P.lastSyncError?a("div",{className:"ds-error"},P.lastSyncError):null),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn",disabled:S,onClick:()=>void D(P)},"同步"),a("button",{className:"ds-btn",disabled:S,onClick:()=>void I(P)},"测试"),a("button",{className:"ds-btn",disabled:S,onClick:()=>k({...P,token:""})},"编辑"),i("删除这个项目(卡不会删)",a("button",{className:"ds-btn",disabled:S,onClick:()=>void _(P),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(n,{title:"代码仓库(GitLab,可以有多个)",hint:"每个项目单独填地址、项目路径和令牌;同步时按项目分开去重,两个仓库都有 #12 也不会串。",rows:M,onAdd:()=>k({id:"",name:"",url:e.gitlab?.url??"",project:"",token:"",syncEnabled:!1,syncLabel:""}),addLabel:"新增项目",render:P=>P,empty:"还没有项目,点「新增项目」。"}),h!==null?a("div",{className:"ds-ok",style:{marginBottom:8}},h):null,b===null?null:a(f,{title:b.id?`编辑仓库:${b.name||b.project}`:"新增仓库",hint:"一个仓库对应 GitLab 上的一个项目;令牌只写盘不回显,留空表示不修改。",onClose:()=>k(null),footer:[a("button",{key:"save",className:"ds-btn ds-btn-primary",disabled:S,onClick:()=>void E()},S?"保存中…":"保存"),a("button",{key:"cancel",className:"ds-btn",disabled:S,onClick:()=>k(null)},"取消"),g!==null?a("span",{key:"error",className:"ds-error ds-side-note"},g):null]},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"显示名"),a(l,{className:"ds-input",value:b.name,placeholder:"订单服务",onChange:P=>W("name",P.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"GitLab 地址"),a(l,{className:"ds-input",value:b.url,placeholder:"https://gitlab.example.com",onChange:P=>W("url",P.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"项目路径(含 namespace)"),a(l,{className:"ds-input",value:b.project,placeholder:"backend/order-service",onChange:P=>W("project",P.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},b.tokenSet?"Access Token(已设置,留空则不修改)":"Access Token"),a(l,{className:"ds-input",type:"password",value:b.token,placeholder:b.tokenSet?"••••••••":"glpat-…",onChange:P=>W("token",P.target.value)}),a("div",{className:"ds-hint"},"需要 api 权限;只写入磁盘,不回显。")),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"只同步带此标签的 Issue"),a(l,{className:"ds-input",value:b.syncLabel,placeholder:"留空则同步全部 open Issue",onChange:P=>W("syncLabel",P.target.value)})),a("div",{className:"ds-field"},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:b.syncEnabled===!0,onChange:P=>W("syncEnabled",P.target.checked)}),a("span",{},"每分钟自动把这个项目的新 Issue 拉成卡片")))))}const cf=[{key:"change",label:"卡片状态变化"},{key:"sla",label:"缺陷 SLA 超时"},{key:"bug",label:"自动开缺陷"},{key:"release",label:"版本发布"}],Xx=e=>{const t=e.events!==null&&typeof e.events=="object"?e.events:null;return t===null?e.notifyOnChange===!1?[]:["change","sla","bug","release"]:cf.filter(a=>t[a.key]!==!1).map(a=>a.label)};function Zx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,ChipField:c,CheckField:u,MdField:p,SideDrawer:f,React:v}=vt(),x=e.feishu?.bots??[],[b,k]=v.useState(null),[g,d]=v.useState(null),[h,y]=v.useState(null),[S,C]=v.useState(!1),[E,_]=v.useState(e.feishu?.driveFolder??""),[I,D]=v.useState(e.feishu?.autoSyncArtifacts!==!1),[W,M]=v.useState(!1),[P,H]=v.useState(null),J=async()=>{M(!0),H(null);try{await r("/config",{feishu:{driveFolder:E.trim(),autoSyncArtifacts:I}}),H("已保存"),await t()}catch(O){H(O instanceof Error?O.message:String(O))}finally{M(!1)}},U=async()=>{if(b!==null){C(!0),d(null),y(null);try{await r("/bots/save",{bot:b}),k(null),y("已保存"),s("已保存"),await t()}catch(O){d(O instanceof Error?O.message:String(O))}finally{C(!1)}}},F=async O=>{if(await o(`删除飞书机器人「${O.name}」?`,{title:"删除机器人",danger:!0,okText:"删除"})){C(!0),d(null);try{await r("/bots/delete",{id:O.id}),await t()}catch(G){d(G instanceof Error?G.message:String(G))}finally{C(!1)}}},L=async O=>{C(!0),d(null),y(null);try{const we=((await r("/feishu/test",{id:O.id})).results??[])[0];y(we!==void 0&&we.ok===!0?`「${O.name}」测试消息已发送`:`「${O.name}」发送失败:${we?.error??"检查 Webhook"}`)}catch(G){d(G instanceof Error?G.message:String(G))}finally{C(!1)}},w=(O,G)=>k({...b,[O]:G}),T=O=>{const G=b.events!==null&&typeof b.events=="object"?b.events:null;return G===null?O==="change"?b.notifyOnChange!==!1:!0:G[O]!==!1},K=(O,G)=>{const fe={...b.events!==null&&typeof b.events=="object"?b.events:{},[O]:G};k({...b,events:fe,notifyOnChange:fe.change!==!1})},R=x.map(O=>a("div",{className:"ds-listrow",key:O.id},a("div",{className:"ds-listrow-main"},a("div",{},`${O.name}${O.enabled===!1?" (已停用)":""}${O.webhookSet?"":" · 缺 Webhook"}`),a("div",{className:"ds-hint"},`Webhook ${O.webhookHint||"(未设置)"}${O.chatId?" · 群 "+O.chatId:""} · 收:${Xx(O).join(" / ")||"什么都不收"}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn",disabled:S,onClick:()=>void L(O)},"测试"),a("button",{className:"ds-btn",disabled:S,onClick:()=>k({...O,webhook:""})},"编辑"),a("button",{className:"ds-btn",disabled:S,onClick:()=>void F(O),"data-danger":"1"},"删除"))));return a("div",{className:"ds-pane-stack"},a(n,{title:"飞书机器人(可以有多个)",hint:"一个群建一个机器人,并在下面勾选它要收哪些事件;研发群只勾缺陷相关,产品群只勾版本发布。",rows:R,onAdd:()=>k({id:"",name:"",webhook:"",chatId:"",notifyOnChange:!0,events:{change:!0,sla:!0,bug:!0,release:!0},enabled:!0}),addLabel:"新增机器人",render:O=>O,empty:"还没有机器人。"}),h!==null?a("div",{className:"ds-ok",style:{marginBottom:8}},h):null,a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"云盘归档(产物文档)"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"云盘根文件夹 token 或链接"),a(l,{className:"ds-input",value:E,placeholder:"IuJPfTCtWlDrzwdCFmLc71aynbg 或文件夹链接",onChange:O=>_(O.target.value)}),a("div",{className:"ds-hint"},`每条需求会在它下面建一个「编号 + 标题」的同名文件夹,交付物写成文件夹里的一份 .md 文档。飞书授权:${e.feishu?.larkReady?"lark-cli 已就绪":"lark-cli 未就绪或未授权"}`)),a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:I,onChange:O=>D(O.target.checked)}),a("span",{},"专家登记交付物时自动归档(关掉就只存平台里)")),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:W,onClick:()=>void J()},W?"保存中…":"保存"),P!==null?a("div",{className:"ds-hint"},P):null))),b===null?null:a(f,{title:b.id?`编辑机器人:${b.name||b.id}`:"新增机器人",hint:"Webhook 只写盘不回显,留空表示不修改;一个群建一个机器人即可。",onClose:()=>k(null),footer:[a("button",{key:"save",className:"ds-btn ds-btn-primary",disabled:S,onClick:()=>void U()},S?"保存中…":"保存"),a("button",{key:"cancel",className:"ds-btn",disabled:S,onClick:()=>k(null)},"取消"),g!==null?a("span",{key:"error",className:"ds-error ds-side-note"},g):null]},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"名字"),a(l,{className:"ds-input",value:b.name,placeholder:"研发群通知",onChange:O=>w("name",O.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},b.webhookSet?"Webhook(已设置,留空则不修改)":"Webhook"),a(l,{className:"ds-input",type:"password",value:b.webhook,placeholder:"https://open.feishu.cn/open-apis/bot/v2/hook/…",onChange:O=>w("webhook",O.target.value)}),a("div",{className:"ds-hint"},"在飞书群里「添加机器人 → 自定义机器人」即可拿到,不需要自建应用。")),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"备注群 chat_id(可选)"),a(l,{className:"ds-input",value:b.chatId,placeholder:"oc_…",onChange:O=>w("chatId",O.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"收哪些事件"),a("div",{className:"ds-row",style:{gap:16,flexWrap:"wrap"}},...cf.map(O=>a("label",{key:O.key,className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:T(O.key),onChange:G=>K(O.key,G.target.checked)}),a("span",{},O.label)))),a("div",{className:"ds-hint"},"不勾任何一项就等于这个群静默(只保留测试消息)。")),a("div",{className:"ds-row",style:{gap:16}},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:b.enabled!==!1,onChange:O=>w("enabled",O.target.checked)}),a("span",{},"启用")))))}function Jx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,ChipField:c,CheckField:u,MdField:p,SideDrawer:f,React:v}=vt(),[x,b]=v.useState(null),[k,g]=v.useState(null),[d,h]=v.useState(null),[y,S]=v.useState(null),[C,E]=v.useState(!1),[_,I]=v.useState(!1),[D,W]=v.useState(!1),M=v.useCallback(async()=>{try{const O=await r("/skills");b({root:O.root,skills:O.skills??[],onDisk:O.onDisk??[]})}catch(O){h(O instanceof Error?O.message:String(O))}},[]);v.useEffect(()=>{M()},[M]);const P=x?.skills??[],H=x?.onDisk??[],J=O=>(e.experts??[]).filter(G=>(G.skills??[]).includes(O)),U=(O,G)=>g({...k,[O]:G}),F=async()=>{if(k!==null){E(!0),h(null),S(null);try{const O=await r("/skills/save",{skill:{name:k.name,description:k.description,whenToUse:k.whenToUse,tags:k.tags,content:k.content,enabled:k.enabled!==!1}});g(null),S(O.warning??(O.file?`已写入 ${O.file}`:"已保存")),await M(),await t()}catch(O){h(O instanceof Error?O.message:String(O))}finally{E(!1)}}},L=async O=>{if(await o(`删除技能「${O.name}」?磁盘上的 SKILL.md 也会一起删掉。`,{title:"删除技能",danger:!0,okText:"删除"})){E(!0),h(null),S(null);try{await r("/skills/delete",{name:O.name}),await M(),await t()}catch(G){h(G instanceof Error?G.message:String(G))}finally{E(!1)}}},w=async()=>{E(!0),h(null),S(null);try{const O=await r("/skills/sync",{});S(`已重新写入 ${(O.written??[]).length} 条技能`),await M()}catch(O){h(O instanceof Error?O.message:String(O))}finally{E(!1)}},T=async O=>{if(O!=null){I(!0),h(null),S(null);try{const G=new Uint8Array(await O.arrayBuffer());let we="";const fe=32768;for(let ae=0;ae<G.length;ae+=fe)we+=String.fromCharCode.apply(null,G.subarray(ae,ae+fe));const ye=await r("/skills/upload",{fileName:O.name,dataBase64:window.btoa(we),overwrite:D}),He=ye.overwritten===!0?"已覆盖":"已上传";S(`${He}技能「${ye.skill.name}」${ye.file?`,写入 ${ye.file}`:""}`),g(null),await M(),await t()}catch(G){h(G instanceof Error?G.message:String(G))}finally{I(!1)}}},K=P.map(O=>a("div",{className:"ds-listrow",key:O.name},a("div",{className:"ds-listrow-main"},a("div",{},a("code",{},O.name),(O.tags??[]).map(G=>a(c,{className:"ds-chip",key:G},G)),O.enabled===!1?a("span",{className:"ds-hint"}," (已停用)"):null,H.includes(O.name)?null:a("span",{className:"ds-err"}," 未落盘")),a("div",{className:"ds-hint"},O.description),O.whenToUse?a("div",{className:"ds-hint"},`何时用:${O.whenToUse}`):null,J(O.name).length>0?a("div",{className:"ds-hint"},`使用中的专家:${J(O.name).map(G=>`${G.emoji}${G.name}`).join("、")}`):a("div",{className:"ds-empty"},"还没有专家挂这个技能")),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn",disabled:C,onClick:()=>{S(null),g({...O,editing:!0,tags:(O.tags??[]).join(", ")})}},"编辑"),a("button",{className:"ds-btn",disabled:C,onClick:()=>void L(O),"data-danger":"1"},"删除")))),R=H.filter(O=>!P.some(G=>G.name===O));return a("div",{className:"ds-pane-stack"},a(n,{title:"技能库(skills-manager)",hint:x===null?"读取中…":`保存在 ${x.root}/<名字>/SKILL.md,所有专家都能用 skill 工具加载;专家也可以自己用 studio_skill_save 往这里加技能。支持上传 SKILL.md 或包含 SKILL.md 的 .zip 技能包。`,rows:K,addLabel:"新建技能",onAdd:()=>{S(null),g({name:"",description:"",whenToUse:"",tags:"",content:`# 什么时候用
|
|
29
|
+
不确定的地方去代码或历史工单里找证据,禁止编造接口字段。`,onChange:R=>D("prompt",R.target.value)})),a("div",{className:"ds-field"},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(v,{checked:g.enabled!==!1,onChange:R=>D("enabled",R.target.checked)}),a("span",{},"启用这个专家")))))}function zx({config:e,onChanged:t}){const{h:a,React:n,Editor:r,api:s,toast:o,confirmDialog:i,tipOf:l,tipLabel:c,TextField:u,TextAreaField:p,ChipField:f,CheckField:v,SelectField:x,SideDrawer:b}=vt(),k=e.teams??[],g=e.experts??[],[d,h]=n.useState(null),[y,S]=n.useState(null),[C,E]=n.useState(!1),[_,I]=n.useState({});n.useEffect(()=>{let U=!0;return s("/team/memory",{}).then(F=>{U&&I(F?.memory??{})}).catch(()=>{}),()=>{U=!1}},[e.teams]);const D=async U=>{if(await i("清空「"+U.name+"」的团队记忆?",{title:"清空团队记忆",danger:!0,okText:"清空"}))try{const F=await s("/team/memory/clear",{teamId:U.id});I(F?.memory??{}),o("已清空这个团的记忆")}catch(F){S(F instanceof Error?F.message:String(F))}},W=U=>{const F=g.find(L=>L.id===U);return F===void 0?U:`${F.emoji} ${F.name}`},M=async()=>{if(d!==null){E(!0),S(null);try{await s("/teams/save",{team:d}),h(null),await t()}catch(U){S(U instanceof Error?U.message:String(U))}finally{E(!1)}}},P=async U=>{if(await i(`删除专家团「${U.name}」?`,{title:"删除专家团",danger:!0,okText:"删除"})){E(!0),S(null);try{await s("/teams/delete",{id:U.id}),await t()}catch(F){S(F instanceof Error?F.message:String(F))}finally{E(!1)}}},H=(U,F)=>h({...d,[U]:F}),J=k.map(U=>a("div",{className:"ds-listrow",key:U.id},a("div",{className:"ds-listrow-main"},a("div",{},`👥 ${U.name}`),a("div",{className:"ds-hint"},`团长 ${W(U.lead)} · 成员 ${U.members.map(W).join("、")}`),a("div",{className:"ds-hint"},(_[U.id]??[]).length===0?"🧠 团队记忆:还没有(卡做完会自动记一条,派活时带进提示词)":`🧠 团队记忆 ${_[U.id].length} 条 · 最近:${_[U.id][0]?.outcome??""} ${_[U.id][0]?.title??""}`)),a("div",{className:"ds-listrow-actions"},l("清空这个团的记忆",a("button",{className:"ds-btn ds-btn-ghost",disabled:C||(_[U.id]??[]).length===0,onClick:()=>void D(U)},"清空记忆")),l("编辑这个专家团",a("button",{className:"ds-btn",disabled:C,onClick:()=>h({...U})},"编辑")),l("删除这个专家团",a("button",{className:"ds-btn",disabled:C,onClick:()=>void P(U),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(r,{title:"专家团(一起干活)",hint:"专家团有团长和成员。评论里 @团名,团长会拆活、给成员派活并汇总结论;成员之间可以继续 @ 接棒。",rows:J,onAdd:()=>h({id:"",name:"",description:"",members:[],lead:"",enabled:!0}),addLabel:"新增专家团",render:U=>U,empty:"还没有专家团。"}),d===null?null:a(b,{title:d.id?`编辑专家团:${d.name||d.id}`:"新增专家团",hint:"团长负责拆活,成员之间在评论里用 @名字 接棒;改完点右下角「保存」。",onClose:()=>h(null),footer:[a("button",{key:"save",className:"ds-btn ds-btn-primary",disabled:C,onClick:()=>void M()},C?"保存中…":"保存"),a("button",{key:"cancel",className:"ds-btn",disabled:C,onClick:()=>h(null)},"取消"),y!==null?a("span",{key:"error",className:"ds-error ds-side-note"},y):null]},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"团名(@ 用这个名字)"),a(u,{className:"ds-input",value:d.name,placeholder:"需求交付组",onChange:U=>H("name",U.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"这个团负责什么"),a(u,{className:"ds-input",value:d.description,placeholder:"从需求澄清一路做到验收",onChange:U=>H("description",U.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"成员"),g.length===0?a("div",{className:"ds-hint"},"先去上面建几个专家。"):a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:12}},g.map(U=>a("label",{className:"ds-row",key:U.id,style:{alignItems:"center",gap:6,cursor:"pointer"}},a(v,{checked:(d.members??[]).includes(U.id),onChange:F=>H("members",F.target.checked?[...d.members??[],U.id]:(d.members??[]).filter(L=>L!==U.id))}),a("span",{},`${U.emoji} ${U.name}`))))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"团长(负责拆活和汇总)"),a(x,{className:"ds-input",value:d.lead??"",onChange:U=>H("lead",U.target.value)},a("option",{value:""},"自动(第一个成员)"),(d.members??[]).map(U=>a("option",{key:U,value:U},W(U)))))))}function Fx({config:e,onChanged:t}){const{h:a,React:n,Editor:r,api:s,toast:o,confirmDialog:i,tipOf:l,tipLabel:c,TextField:u,TextAreaField:p,ChipField:f,CheckField:v,SelectField:x,SideDrawer:b}=vt(),k=e.testing??{},g=Array.isArray(k.envs)?k.envs:[],[d,h]=n.useState(null),[y,S]=n.useState((Array.isArray(k.allowHosts)?k.allowHosts:[]).join(", ")),[C,E]=n.useState(String(k.timeoutMs??15e3)),[_,I]=n.useState(!1),[D,W]=n.useState(null),[M,P]=n.useState(null),[H,J]=n.useState({allowShell:k.allowShell===!0,allowDb:k.allowDb===!0}),[U,F]=n.useState({casesPerTask:e.limits?.casesPerTask??60,runKeys:e.limits?.runKeys??60,artifactsPerTask:e.limits?.artifactsPerTask??40}),[L,w]=n.useState(()=>(Array.isArray(e.webhooks)?e.webhooks:[]).map(ae=>({...ae}))),T=async ae=>{J(ae),I(!0),P(null);try{await s("/config",{testing:{...k,allowShell:ae.allowShell,allowDb:ae.allowDb}}),P("已保存"),await t()}catch(z){W(z instanceof Error?z.message:String(z))}finally{I(!1)}},K=async()=>{I(!0),P(null);try{await s("/config",{limits:U}),P("上限已保存"),o("上限已保存"),await t()}catch(ae){W(ae instanceof Error?ae.message:String(ae))}finally{I(!1)}},R=async()=>{I(!0),P(null);try{await s("/config",{webhooks:L.filter(ae=>String(ae.url??"").trim()!=="")}),P("webhook 已保存"),o("webhook 已保存"),await t()}catch(ae){W(ae instanceof Error?ae.message:String(ae))}finally{I(!1)}},O=async ae=>{I(!0),W(null),P(null);try{await s("/config",{testing:{envs:ae.envs??g,defaultEnv:ae.defaultEnv!==void 0?ae.defaultEnv:k.defaultEnv??"",timeoutMs:ae.timeoutMs!==void 0?ae.timeoutMs:k.timeoutMs??15e3,allowHosts:ae.allowHosts!==void 0?ae.allowHosts:Array.isArray(k.allowHosts)?k.allowHosts:[],concurrency:k.concurrency??4}}),P("已保存"),o("已保存"),await t()}catch(z){W(z instanceof Error?z.message:String(z))}finally{I(!1)}},G=async()=>{if(d===null)return;const ae=String(d.name??"").trim(),z=String(d.baseUrl??"").trim();if(ae===""){W("环境名不能为空");return}const te={id:d.id??"",name:ae,baseUrl:z},xe=d.id===void 0||d.id===""?[...g,te]:g.map(X=>X.id===d.id?{...X,...te}:X);h(null),await O({envs:xe,defaultEnv:k.defaultEnv===""?"":k.defaultEnv})},we=async ae=>{if(!await i(`删除环境「${ae.name}」?`,{title:"删除环境",danger:!0,okText:"删除"}))return;const z=g.filter(te=>te.id!==ae.id);await O({envs:z,defaultEnv:k.defaultEnv===ae.id?z[0]?.id??"":k.defaultEnv})},fe=async()=>{const ae=y.split(/[,,\s]+/).map(te=>te.trim()).filter(te=>te!==""),z=Number(C);await O({allowHosts:ae,timeoutMs:Number.isFinite(z)&&z>0?z:15e3})},ye=ae=>a("div",{key:ae.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${ae.name}${k.defaultEnv===ae.id?" · 默认":""}`),a("div",{className:"ds-hint"},ae.baseUrl===""?"相对地址会因为没有 baseUrl 而失败":ae.baseUrl)),a("div",{className:"ds-listrow-actions"},k.defaultEnv===ae.id?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>void O({defaultEnv:ae.id})},"设为默认"),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>h({...ae})},"编辑"),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>void we(ae),"data-danger":"1"},"删除"))),He=d===null?null:a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},d.id===void 0||d.id===""?"新增环境":`编辑环境:${d.name}`),a("div",{className:"ds-panel-body"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"环境名"),a(u,{className:"ds-input",value:d.name??"",placeholder:"例如 测试环境 / 预发环境",onChange:ae=>h({...d,name:ae.target.value})})),a("div",{className:"ds-field"},c("baseUrl","接口测试的根地址,用例里的 /path 会拼在它后面"),a(u,{className:"ds-input",value:d.baseUrl??"",placeholder:"https://test.example.com",onChange:ae=>h({...d,baseUrl:ae.target.value})})),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>h(null)},"取消"),a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void G()},"保存环境"))));return a("div",{className:"ds-pane-stack"},He,a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"执行开关",a("span",{className:"ds-hint"},"默认都关:打开等于把宿主机的能力交给看板使用者")),a("div",{className:"ds-panel-body"},a("label",{className:"ds-check"},a(v,{checked:H.allowShell,disabled:_,onChange:ae=>void T({...H,allowShell:ae.target.checked})}),a("span",{className:"ds-check-text"},"允许 shell 用例(能在宿主机执行命令)")),a("label",{className:"ds-check",style:{marginTop:6}},a(v,{checked:H.allowDb,disabled:_,onChange:ae=>void T({...H,allowDb:ae.target.checked})}),a("span",{className:"ds-check-text"},"允许数据库用例(默认只读打开 SQLite)")))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"数量上限"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-row",style:{gap:8,flexWrap:"wrap"}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"每张卡用例上限"),a(u,{className:"ds-input",type:"number",value:String(U.casesPerTask),onChange:ae=>F({...U,casesPerTask:Number(ae.target.value)})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"一次批量跑上限"),a(u,{className:"ds-input",type:"number",value:String(U.runKeys),onChange:ae=>F({...U,runKeys:Number(ae.target.value)})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"交付物上限"),a(u,{className:"ds-input",type:"number",value:String(U.artifactsPerTask),onChange:ae=>F({...U,artifactsPerTask:Number(ae.target.value)})}))),a("div",{className:"ds-row",style:{justifyContent:"flex-end",marginTop:6}},a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void K()},"保存上限")))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"出站 webhook",a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>w(ae=>[...ae,{url:"",events:["task/moved"],enabled:!0}])},"+ 加一个")),a("div",{className:"ds-panel-body"},L.length===0?a("div",{className:"ds-empty"},"还没有 webhook。配一个就能把卡片流转、用例失败、门禁拦截推给 CI / IM / 工单系统。"):L.map((ae,z)=>a("div",{key:z,style:{marginBottom:8}},a("div",{className:"ds-row",style:{gap:6,alignItems:"center"}},a(u,{className:"ds-input",style:{flex:1},placeholder:"https://ci.example/hook",value:ae.url??"",onChange:te=>w(xe=>xe.map((X,ie)=>ie===z?{...X,url:te.target.value}:X))}),a(v,{checked:ae.enabled!==!1,onChange:te=>w(xe=>xe.map((X,ie)=>ie===z?{...X,enabled:te.target.checked}:X))}),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>w(te=>te.filter((xe,X)=>X!==z))},"×")),a("div",{className:"ds-row",style:{gap:10,flexWrap:"wrap",marginTop:2}},["task/moved","case/failed","gate/blocked","agent/failed","regression/finished"].map(te=>a("label",{className:"ds-check",key:te},a(v,{checked:(ae.events??[]).includes(te),onChange:xe=>w(X=>X.map((ie,ge)=>ge===z?{...ie,events:xe.target.checked?Array.from(new Set([...ie.events??[],te])):(ie.events??[]).filter(V=>V!==te)}:ie))}),a("span",{className:"ds-check-text",style:{fontSize:12}},te))),(ae.events??[]).length===0?a("span",{className:"ds-hint",style:{fontSize:12}},"一个都没勾 = 订阅全部事件"):null))),L.length===0?null:a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void R()},"保存 webhook")))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"测试环境",a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>h({name:"",baseUrl:""})},"+ 新增环境")),a("div",{className:"ds-panel-body"},g.length===0?a("div",{className:"ds-empty"},"还没有环境。接口用例里的相对地址会拼到这里配置的 baseUrl。"):g.map(ye))),a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"执行约束"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"允许访问的域名(逗号分隔,支持 *.example.com)"),a(u,{className:"ds-input",value:y,placeholder:"127.0.0.1, localhost, test.example.com",onChange:ae=>S(ae.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"单条用例超时(毫秒)"),a(u,{className:"ds-input",value:C,onChange:ae=>E(ae.target.value)})),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void fe()},_?"保存中…":"保存约束")),M===null?null:a("div",{className:"ds-ok"},M),D===null?null:a("div",{className:"ds-err"},D))))}function Bx({config:e,onChanged:t}){const{h:a,React:n,Editor:r,api:s,toast:o,confirmDialog:i,tipOf:l,tipLabel:c,TextField:u,TextAreaField:p,ChipField:f,CheckField:v,SelectField:x,SideDrawer:b}=vt(),k=e.feishu??{},[g,d]=n.useState(""),[h,y]=n.useState(String(k.escalateAfter??3)),[S,C]=n.useState(!1),[E,_]=n.useState(null),[I,D]=n.useState(null),W=async()=>{C(!0),_(null),D(null);try{const M={};String(g??"").trim()!==""&&(M.escalateWebhook=String(g).trim()),M.escalateAfter=Number(h)||3,await s("/config",{feishu:M}),d(""),_("已保存:连续失败 "+(Number(h)||3)+" 次后往升级群发一条"),await t()}catch(M){D(M instanceof Error?M.message:String(M))}finally{C(!1)}};return a("div",{className:"ds-panel ds-form-panel",style:{marginTop:14}},a("div",{className:"ds-panel-head"},"通知升级"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"升级群 Webhook"),a(u,{className:"ds-input",value:g,placeholder:k.escalateWebhookSet===!0?"已设置(留空表示不修改)":"https://open.feishu.cn/open-apis/bot/v2/hook/…",onChange:M=>d(M.target.value)})),a("div",{className:"ds-field"},c("连续失败几次后升级","同一通道连续失败到这个次数,就给升级群发消息"),a(u,{className:"ds-input",style:{width:90},value:h,onChange:M=>y(M.target.value)})),a("div",{className:"ds-hint"},"飞书通知、GitLab、云盘归档、代码隔离任一通道连续失败达到阈值时,只在跨过阈值那一次往升级群发一条,包含通道、失败次数与最近错误。"),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:S,onClick:()=>void W()},S?"保存中…":"保存升级设置")),E===null?null:a("div",{className:"ds-ok"},E),I===null?null:a("div",{className:"ds-err"},I)))}function Ux({config:e,onChanged:t}){const{h:a,React:n,Editor:r,api:s,toast:o,confirmDialog:i,tipOf:l,tipLabel:c,TextField:u,TextAreaField:p,ChipField:f,CheckField:v,SelectField:x,SideDrawer:b}=vt(),[k,g]=n.useState(()=>(e.environments??[]).map(J=>({id:J.id,name:J.name,kind:J.kind,note:J.note}))),[d,h]=n.useState(!1),[y,S]=n.useState(null),[C,E]=n.useState(null),[_,I]=n.useState(null),D=async J=>{I(null);try{await s("/config",{releaseRules:J}),I("发布准入已更新"),await t()}catch(U){I(U instanceof Error?U.message:String(U))}},W=()=>g(J=>J.length>=12?J:[...J,{id:"",name:"",kind:"other",note:""}]),M=(J,U)=>g(F=>F.map((L,w)=>w===J?{...L,...U}:L)),P=J=>g(U=>U.filter((F,L)=>L!==J)),H=async()=>{h(!0),S(null),E(null);try{const J=k.map(U=>({...U,name:String(U.name??"").trim()})).filter(U=>U.name!=="");if(J.length===0)throw new Error("至少留一个环境,否则部署记录没处挂");await s("/config",{environments:J}),S(`已保存 ${J.length} 个环境`),o(`已保存 ${J.length} 个环境`),await t()}catch(J){E(J instanceof Error?J.message:String(J))}finally{h(!1)}};return a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"部署环境",a("span",{className:"ds-hint"},"只登记,不触发")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-hint"},"这些环境只用来在版本上留痕:谁在什么时候把哪个版本放到了哪个环境。平台不会调用任何部署脚本、webhook 或 CI,部署仍然由人或你们自己的流水线完成。"),k.map((J,U)=>a("div",{key:`env-${U}`,className:"ds-row",style:{gap:8,marginTop:8,alignItems:"flex-end"}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"名字"),a(u,{className:"ds-input",value:J.name,placeholder:"例如 预发",onChange:F=>M(U,{name:F.target.value})})),a("div",{className:"ds-field",style:{flex:"0 0 120px"}},a("div",{className:"ds-label"},"类型"),a(x,{className:"ds-input",value:J.kind,onChange:F=>M(U,{kind:F.target.value})},["dev","staging","prod","other"].map(F=>a("option",{key:F,value:F},F)))),a("div",{className:"ds-field",style:{flex:2}},a("div",{className:"ds-label"},"说明"),a(u,{className:"ds-input",value:J.note,placeholder:"例如 上线前最后一关",onChange:F=>M(U,{note:F.target.value})})),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>P(U),"data-danger":"1"},"删除"))),a("div",{className:"ds-row",style:{gap:8,marginTop:10}},a("button",{className:"ds-btn",onClick:W,disabled:k.length>=12},"+ 加一个环境")),a("div",{className:"ds-hint"},"id 由名字生成一次就固定了:改名不会断开已有的部署记录;删掉的环境,历史记录里的名字会显示成 id。"),a("div",{className:"ds-row",style:{justifyContent:"flex-end",marginTop:10}},a("button",{className:"ds-btn ds-btn-primary",disabled:d,onClick:()=>void H()},d?"保存中…":"保存环境")),y===null?null:a("div",{className:"ds-ok"},y),C===null?null:a("div",{className:"ds-err"},C)),a("div",{className:"ds-panel-body",style:{borderTop:"1px solid var(--ds-line)"}},a("div",{className:"ds-label"},"发布准入(发布时会检查,不满足要「强制发布」并写原因)"),a("label",{className:"ds-check",style:{marginTop:8}},a(v,{checked:(e.releaseRules?.requireStaging??!1)===!0,onChange:J=>void D({requireStaging:J.target.checked})}),a("span",{className:"ds-check-text"},"发布前必须先在「预发」环境登记过一次部署")),a("label",{className:"ds-check",style:{marginTop:6}},a(v,{checked:(e.releaseRules?.requireRequirementSignoff??!1)===!0,onChange:J=>void D({requireRequirementSignoff:J.target.checked})}),a("span",{className:"ds-check-text"},"关联需求必须都签过验收字")),a("div",{className:"ds-hint",style:{marginTop:6}},"两条默认都不勾:不勾的时候发布只看门禁,不看你上没上过预发、需不需要签字。"),_===null?null:a("div",{className:"ds-ok"},_)))}function qx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,CheckField:c,SelectField:u,React:p}=vt(),f=e.ldap??{},[v,x]=p.useState({enabled:f.enabled===!0,url:f.url??"ldap://localhost:389",baseDN:f.baseDN??"DC=LYF,DC=COM",bindDN:f.bindDN??"cn=root",bindPassword:"",userFilter:f.userFilter??"(|(sAMAccountName={user})(uid={user})(mail={user}))",nameAttr:f.nameAttr??"displayName",mailAttr:f.mailAttr??"mail",deptAttr:f.deptAttr??"department",defaultRole:f.defaultRole??"member",requireLogin:f.requireLogin===!0,timeoutMs:String(f.timeoutMs??6e3)}),[b,k]=p.useState(!1),[g,d]=p.useState(!1),[h,y]=p.useState(null),[S,C]=p.useState(null),[E,_]=p.useState([]),I=e.localAuth??{},[D,W]=p.useState(""),[M,P]=p.useState(""),[H,J]=p.useState(!1),[U,F]=p.useState(null),[L,w]=p.useState(null),T=async()=>{J(!0),F(null),w(null);try{await r("/auth/password",{username:I.username,currentPassword:D,newPassword:M}),W(""),P(""),F("密码已更新"),t()}catch(G){w(G instanceof Error?G.message:String(G))}finally{J(!1)}},K=G=>x(we=>({...we,...G})),R=async()=>{d(!0),y(null),C(null),_([]);try{const G=await r("/auth/ldap/test",{ldap:{url:v.url,baseDN:v.baseDN,bindDN:v.bindDN,userFilter:v.userFilter,nameAttr:v.nameAttr,mailAttr:v.mailAttr,deptAttr:v.deptAttr,timeoutMs:Number(v.timeoutMs)},bindPassword:v.bindPassword});_(G.steps??[]),y(G.message??"连接正常"),s(G.message??"连接正常",G.ok===!1?"error":"success")}catch(G){const we=G instanceof Error?G.message:String(G);C(we)}finally{d(!1)}},O=async()=>{k(!0),y(null),C(null);try{const G={ldap:{enabled:v.enabled,url:v.url.trim(),baseDN:v.baseDN.trim(),bindDN:v.bindDN.trim(),userFilter:v.userFilter.trim(),nameAttr:v.nameAttr.trim(),mailAttr:v.mailAttr.trim(),deptAttr:v.deptAttr.trim(),defaultRole:v.defaultRole,requireLogin:v.requireLogin,timeoutMs:Number(v.timeoutMs)||6e3}};v.bindPassword.trim()!==""&&(G.ldap.bindPassword=v.bindPassword.trim()),await r("/config",G),K({bindPassword:""}),y(v.bindPassword.trim()===""?"已保存(服务账号密码没改)":"已保存,服务账号密码已写入密钥文件"),await t()}catch(G){C(G instanceof Error?G.message:String(G))}finally{k(!1)}};return a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"统一登录(LDAP)",a("span",{className:"ds-hint"},f.bindPasswordSet===!0?"服务账号密码已保存":"服务账号密码还没保存")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-hint"},"开启后,页头会出现登录框:用域账号登录,平台拿到的用户名会记进 config.users(首次登录建档、之后只更新最近登录),写操作和审计都挂在这个名字下。"),a("label",{className:"ds-check",style:{marginTop:8}},a(c,{checked:v.enabled,onChange:G=>K({enabled:G.target.checked})}),a("span",{className:"ds-check-text"},"启用 LDAP 登录")),a("label",{className:"ds-check",style:{marginTop:6}},a(c,{checked:v.requireLogin,onChange:G=>K({requireLogin:G.target.checked})}),a("span",{className:"ds-check-text"},"必须登录才能用(不勾也可以先配好再打开)")),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:2}},a("div",{className:"ds-label"},"服务器地址"),a(l,{className:"ds-input",value:v.url,placeholder:"ldap://host:389",onChange:G=>K({url:G.target.value})})),a("div",{className:"ds-field",style:{flex:"0 0 110px"}},a("div",{className:"ds-label"},"超时(毫秒)"),a(l,{className:"ds-input",value:v.timeoutMs,onChange:G=>K({timeoutMs:G.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"BASE DN"),a(l,{className:"ds-input",value:v.baseDN,placeholder:"DC=LYF,DC=COM",onChange:G=>K({baseDN:G.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"服务账号 DN"),a(l,{className:"ds-input",value:v.bindDN,placeholder:"cn=root",onChange:G=>K({bindDN:G.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},f.bindPasswordSet===!0?"服务账号密码(留空 = 不改)":"服务账号密码"),a(l,{className:"ds-input",type:"password",value:v.bindPassword,placeholder:f.bindPasswordSet===!0?"已保存,留空表示不修改":"写一次就进密钥文件",onChange:G=>K({bindPassword:G.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"用户过滤器({user} 会被替换)"),a(l,{className:"ds-input",value:v.userFilter,onChange:G=>K({userFilter:G.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"显示名属性"),a(l,{className:"ds-input",value:v.nameAttr,onChange:G=>K({nameAttr:G.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"邮箱属性"),a(l,{className:"ds-input",value:v.mailAttr,onChange:G=>K({mailAttr:G.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"部门属性"),a(l,{className:"ds-input",value:v.deptAttr,onChange:G=>K({deptAttr:G.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:10}},a("button",{className:"ds-btn",disabled:g,onClick:()=>void R()},g?"测试中…":"测试连接"),a("button",{className:"ds-btn ds-btn-primary",disabled:b,onClick:()=>void O()},b?"保存中…":"保存")),E.length===0?null:a("div",{style:{marginTop:8}},E.map((G,we)=>a("div",{key:`ldap-step-${we}`,className:G.ok===!0?"ds-ok":"ds-error"},`${G.ok===!0?"✓":"✗"} ${G.name}:${G.message}${G.ms>0?`(${G.ms}ms)`:""}`))),h===null?null:a("div",{className:"ds-ok",style:{marginTop:8}},h),S===null?null:a("div",{className:"ds-error",style:{marginTop:8}},S),a("div",{className:"ds-hint",style:{marginTop:10}},"依赖系统里的 ldapsearch(Ubuntu:apt-get install ldap-utils)。密码只写进 ~/.dsh/dsh-studio.secrets.json(0600),不会出现在状态、导出和日志里。")),a("div",{className:"ds-panel-body",style:{borderTop:"1px solid var(--ds-line)"}},a("div",{className:"ds-label"},"本地超管账号"),a("div",{className:"ds-hint",style:{marginTop:4}},`账号:${I.username??"admin"} · ${I.hasPassword===!0?"已设置自己的密码":"还在用默认口令 admin"}。没启用 LDAP 时平台默认以这个身份使用,不需要登录;启用 LDAP 后它仍可用来兜底登录。`),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"当前密码"),a(l,{className:"ds-input",type:"password",value:D,onChange:G=>W(G.target.value)})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"新密码(至少 8 位)"),a(l,{className:"ds-input",type:"password",value:M,onChange:G=>P(G.target.value)})),a("button",{className:"ds-btn",disabled:H||D===""||M.length<8,onClick:()=>void T()},H?"提交中…":"改密码")),U===null?null:a("div",{className:"ds-ok",style:{marginTop:6}},U),L===null?null:a("div",{className:"ds-error",style:{marginTop:6}},L)),a("div",{className:"ds-panel-body",style:{borderTop:"1px solid var(--ds-line)"}},a("div",{className:"ds-label"},`登录过的人(${(e.users??[]).length})`),(e.users??[]).length===0?a("div",{className:"ds-hint",style:{marginTop:6}},"还没有人用 LDAP 登录过。"):a("div",{style:{marginTop:6}},(e.users??[]).slice(0,20).map(G=>a("div",{key:G.id,className:"ds-kv"},a("div",null,G.name),a("div",null,`${G.id} · ${G.dept===""?"(无部门)":G.dept} · 登录 ${G.loginCount} 次 · 最近 ${Ke(G.lastLoginAt)}`))))))}function Vx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,CheckField:c,SelectField:u,React:p}=vt(),f=e.costs??{},[v,x]=p.useState(String(f.budgetRuns??500)),[b,k]=p.useState(String(f.budgetHours??80)),[g,d]=p.useState(String(f.warnAt??80)),[h,y]=p.useState(!1),[S,C]=p.useState(null),[E,_]=p.useState(null),I=async()=>{y(!0),C(null),_(null);try{const D=Number(v),W=Number(b),M=Number(g);if(!Number.isFinite(D)||D<=0)throw new Error("预算次数要是正整数");if(!Number.isFinite(W)||W<=0)throw new Error("预算小时要是正整数");await r("/config",{costs:{budgetRuns:Math.round(D),budgetHours:Math.round(W),warnAt:Number.isFinite(M)?Math.min(100,Math.max(10,Math.round(M))):80}}),C("已保存:预算"),await t()}catch(D){_(D instanceof Error?D.message:String(D))}finally{y(!1)}};return a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"成本预算"),a("div",{className:"ds-panel-body ds-grid2"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"每月派单次数上限"),a(l,{className:"ds-input",value:v,onChange:D=>x(D.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"每月机上时长上限(小时)"),a(l,{className:"ds-input",value:b,onChange:D=>k(D.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"警告阈值(百分比)"),a(l,{className:"ds-input",value:g,onChange:D=>d(D.target.value)})),a("div",{className:"ds-field ds-span"},a("div",{className:"ds-hint"},'效能页的「AI 执行成本」会显示真实 token 用量(读 studio-* 会话日志)。要显示钱就在 config.costs.prices 里配价目表:[{ "model": "deepseek-v4.1-flash", "input": 2, "output": 8, "cacheRead": 0.2 }],单位是元 / 百万 token;没配价目的调用记「未知」,不按 0 算。')),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:h,onClick:()=>void I()},h?"保存中…":"保存预算")),S===null?null:a("div",{className:"ds-ok"},S),E===null?null:a("div",{className:"ds-err"},E)))}function Wx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,CheckField:c,SelectField:u,React:p}=vt(),f=e.projects??[],v=e.gitlab?.projects??[],[x,b]=p.useState(null),[k,g]=p.useState(null),[d,h]=p.useState(null),[y,S]=p.useState(!1),[C,E]=p.useState({name:"",start:"",due:"",status:"planned"}),_=async()=>{if(x!==null){S(!0),g(null),h(null);try{await r("/projects/save",{project:{...x,iterations:x.iterations??[]}}),h("已保存"),s("已保存"),await t()}catch(w){g(w instanceof Error?w.message:String(w))}finally{S(!1)}}},I=async w=>{if(await o(`删除项目「${w.name}」?卡片会保留,但不再归属任何项目。`,{title:"删除项目",danger:!0,okText:"删除"})){S(!0),g(null);try{await r("/projects/delete",{id:w.id}),b(null),await t()}catch(T){g(T instanceof Error?T.message:String(T))}finally{S(!1)}}},D=async w=>{if(C.name.trim()!==""){S(!0),g(null);try{await r("/projects/iteration",{project:w.id,name:C.name.trim(),start:jr(C.start),due:jr(C.due),status:C.status}),E({name:"",start:"",due:"",status:"planned"}),await t()}catch(T){g(T instanceof Error?T.message:String(T))}finally{S(!1)}}},W=async(w,T)=>{S(!0),g(null);try{await r("/projects/iteration",{project:w.id,action:"remove",iteration:T.id}),await t()}catch(K){g(K instanceof Error?K.message:String(K))}finally{S(!1)}},M=(w,T)=>b({...x,[w]:T}),P=(w,T,K)=>b({...x,services:(x.services??[]).map((R,O)=>O===w?{...R,[T]:K}:R)}),H=()=>b({...x,services:[...x.services??[],{id:"",name:"",repoId:"",repoPath:"",baseBranch:x.baseBranch||"main",dependsOn:[]}]}),J=w=>b({...x,services:(x.services??[]).filter((T,K)=>K!==w)}),U=x===null?[]:x.services??[],F=(w,T)=>b({...x,services:(x.services??[]).map((K,R)=>{if(R!==w)return K;const O=Array.isArray(K.dependsOn)?K.dependsOn:[];return{...K,dependsOn:O.includes(T)?O.filter(G=>G!==T):[...O,T]}})}),L=f.map(w=>a("div",{className:"ds-listrow",key:w.id},a("div",{className:"ds-listrow-main"},a("div",{},`${w.key?`[${w.key}] `:""}${w.name} · ${Xl[w.status]??w.status}${w.lead?` · 负责人 ${w.lead}`:""}`),a("div",{className:"ds-hint"},`${(e.projects??[]).find(T=>T.id===w.id)?.iterations?.length??0} 个迭代 · ${(w.services??[]).length} 个服务${(()=>{const T=(w.services??[]).reduce((K,R)=>K+(R.dependsOn??[]).length,0);return T>0?` · ${T} 条依赖`:""})()}${w.description?` · ${w.description}`:""}`)),a("div",{className:"ds-listrow-actions"},i("改项目信息、加迭代",a("button",{className:"ds-btn",disabled:y,onClick:()=>b({...w})},"编辑 / 迭代")),i("删除这个项目(卡不会删)",a("button",{className:"ds-btn",disabled:y,onClick:()=>void I(w),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(n,{title:"业务项目(一个项目可以挂很多张卡和很多个迭代)",hint:"项目是看板的第一层分组:卡片挂项目,项目下面划迭代,看板可按项目和迭代筛选,项目页签看每个项目的体检数据。",rows:L,onAdd:()=>b({id:"",name:"",key:"",description:"",lead:"",status:"active",repos:[],services:[],iterations:[]}),addLabel:"新增项目",render:w=>w,empty:"还没有项目。团队只跑一个产品也可以先不建,直接在看板上干活。"}),d!==null?a("div",{className:"ds-ok",style:{marginBottom:8}},d):null,x===null?null:a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},x.id?`编辑项目:${x.name||x.id}`:"新增项目"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-row",style:{gap:12}},a("div",{className:"ds-field",style:{flex:"0 0 110px"}},a("div",{className:"ds-label"},"代号"),a(l,{className:"ds-input",value:x.key??"",placeholder:"ORD",onChange:w=>M("key",w.target.value)})),a("div",{className:"ds-field",style:{flex:2}},a("div",{className:"ds-label"},"项目名"),a(l,{className:"ds-input",value:x.name??"",placeholder:"订单中心",onChange:w=>M("name",w.target.value)})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"状态"),a(u,{className:"ds-select",value:x.status??"active",onChange:w=>M("status",w.target.value)},Object.keys(Xl).map(w=>a("option",{key:w,value:w},Xl[w])))),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"业务负责人"),a(l,{className:"ds-input",value:x.lead??"",placeholder:"张伟",onChange:w=>M("lead",w.target.value)}))),a("div",{className:"ds-row",style:{gap:12}},a("div",{className:"ds-field",style:{flex:3}},a("div",{className:"ds-label"},"默认代码目录(没有分服务时用)"),a(l,{className:"ds-input",value:x.repoPath??"",placeholder:"~/repos/order-center",onChange:w=>M("repoPath",w.target.value)})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"基线分支"),a(l,{className:"ds-input",value:x.baseBranch??"",placeholder:"main",onChange:w=>M("baseBranch",w.target.value)}))),a("div",{className:"ds-hint",style:{margin:"-6px 0 4px"}},"配了代码目录且「代码隔离」选 worktree 时,每张卡会切一条 task/<编号> 分支,互不干扰。项目里有多个服务时,用下面的服务列表分开配。"),a("div",{className:"ds-field"},a("div",{className:"ds-label"},`服务(${U.length})· 一个服务一个 git 路径,卡片可以指定改哪个`),U.length===0?a("div",{className:"ds-empty"},"还没有分服务。只有一个仓库时留空即可,走上面的默认代码目录。"):U.map((w,T)=>{const K=U.filter((R,O)=>O!==T&&String(R.id??"").trim()!=="");return a("div",{key:`${w.id}-${T}`,style:{marginBottom:8}},a("div",{className:"ds-row",style:{gap:8,alignItems:"flex-end"}},a("div",{className:"ds-field",style:{flex:"0 0 150px"}},a("div",{className:"ds-label"},"服务名"),a(l,{className:"ds-input",value:w.name??"",placeholder:"order-service",onChange:R=>P(T,"name",R.target.value)})),a("div",{className:"ds-field",style:{flex:3}},a("div",{className:"ds-label"},"本机 git 路径"),a(l,{className:"ds-input",value:w.repoPath??"",placeholder:"~/repos/order-service",onChange:R=>P(T,"repoPath",R.target.value)})),a("div",{className:"ds-field",style:{flex:"0 0 110px"}},a("div",{className:"ds-label"},"基线分支"),a(l,{className:"ds-input",value:w.baseBranch??"",placeholder:"main",onChange:R=>P(T,"baseBranch",R.target.value)})),v.length===0?null:a("div",{className:"ds-field",style:{flex:"0 0 150px"}},a("div",{className:"ds-label"},"GitLab 仓库"),a(u,{className:"ds-select",value:w.repoId??"",onChange:R=>P(T,"repoId",R.target.value)},a("option",{value:""},"不关联"),v.map(R=>a("option",{key:R.id,value:R.id},R.name||R.project)))),a("button",{className:"ds-btn",type:"button",onClick:()=>J(T),"data-danger":"1"},"删除")),K.length===0?null:a("div",{className:"ds-row",style:{gap:6,alignItems:"center",marginTop:4,flexWrap:"wrap"}},a("span",{className:"ds-hint"},"依赖"),a("span",{className:"ds-hint",style:{marginRight:2}},"(勾上的服务要先完成,本服务的卡才能收口)"),K.map(R=>a("span",{key:R.id,className:"ds-chip","data-on":(w.dependsOn??[]).includes(R.id)?"1":"0",title:`依赖 ${R.name||R.id}`,onClick:()=>F(T,R.id)},R.name||R.id))))}),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn",type:"button",onClick:()=>H()},"+ 加一个服务"),a("span",{className:"ds-hint"},"服务名会被写进任务详情;worktree 从该服务的 git 路径切 task/<编号> 分支。"))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"项目说明"),a(l,{className:"ds-input",value:x.description??"",placeholder:"这个项目做什么、给谁用",onChange:w=>M("description",w.target.value)})),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:y,onClick:()=>void _()},y?"保存中…":"保存项目"),a("button",{className:"ds-btn",disabled:y,onClick:()=>b(null)},"取消")))),x===null||x.id===""?null:a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"迭代",a("span",{className:"ds-hint"},"迭代用来按时间切需求,看板可按迭代筛选")),a("div",{className:"ds-panel-body"},(x.iterations??[]).length===0?a("div",{className:"ds-empty"},"还没有迭代。可以先加一个「2026-11 迭代」。"):(x.iterations??[]).map(w=>a("div",{className:"ds-listrow",key:w.id},a("div",{className:"ds-listrow-main"},a("div",{},`${w.name} · ${Zl[w.status]??w.status}`),a("div",{className:"ds-hint"},`${w.start?new Date(w.start).toISOString().slice(0,10):"未定开始"} → ${w.due?new Date(w.due).toISOString().slice(0,10):"未定结束"}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:y,onClick:()=>void W(x,w)},"×")))),a("div",{className:"ds-row",style:{gap:8,alignItems:"flex-end"}},a("div",{className:"ds-field",style:{flex:2}},a("div",{className:"ds-label"},"迭代名"),a(l,{className:"ds-input",value:C.name,placeholder:"2026-11 迭代",onChange:w=>E({...C,name:w.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"开始"),a(l,{className:"ds-input",type:"date",value:C.start,onChange:w=>E({...C,start:w.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"结束"),a(l,{className:"ds-input",type:"date",value:C.due,onChange:w=>E({...C,due:w.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"状态"),a(u,{className:"ds-select",value:C.status,onChange:w=>E({...C,status:w.target.value})},Object.keys(Zl).map(w=>a("option",{key:w,value:w},Zl[w])))),a("button",{className:"ds-btn",disabled:y||C.name.trim()==="",onClick:()=>void D(x)},"+ 加迭代")),a("div",{className:"ds-hint"},"迭代改完会立刻反映到看板的迭代筛选。"))))}function Hx({state:e,config:t,onChanged:a}){const{h:n,sessionToken:r,api:s,toast:o,currentUser:i,confirmDialog:l,TextField:c,ChipField:u,CheckField:p,SelectField:f,React:v}=vt(),x=t.access??{enabled:!1,members:[]},b=e.audit??[],[k,g]=v.useState({name:"",role:"member"}),[d,h]=v.useState(!1),[y,S]=v.useState(null),[C,E]=v.useState(""),[_,I]=v.useState(!1),[D,W]=v.useState(""),M=async(w,T)=>{h(!0),S(null);try{await s(w,T),await a()}catch(K){{const R=K instanceof Error?K.message:String(K);S(R),o(R,"error")}}finally{h(!1)}},P=async()=>{const w=k.name.trim();w!==""&&(await M("/access/save",{member:{name:w,role:k.role}}),g({name:"",role:"member"}))},H=Array.from(new Set(b.map(w=>w.by))).sort(),J=D.trim().toLowerCase(),U=b.filter(w=>(C===""||w.by===C)&&(_===!1||w.ok===!1)&&(J===""||`${ROUTE_LABEL[w.route]??w.route} ${w.by} ${w.summary}`.toLowerCase().includes(J))),F=U.slice(0,120),L=()=>{const w=new URLSearchParams({by:C,q:D.trim(),failed:_?"1":"0",token:r()??""}),T=document.createElement("a");T.href=`${API}/audit/export.csv?${w.toString()}`,T.rel="noopener",document.body.appendChild(T),T.click(),document.body.removeChild(T),o(`正在导出 ${U.length} 条审计记录`)};return n("div",{className:"ds-pane-stack"},n("div",{className:"ds-panel ds-form-panel"},n("div",{className:"ds-panel-head"},n("span",null,"成员权限"),n(u,{className:"ds-chip"},`当前身份:${i()}`)),n("div",{className:"ds-panel-body"},n("div",{className:"ds-row",style:{gap:10,alignItems:"center",flexWrap:"wrap"}},n("label",{className:"ds-check",style:{flex:"0 0 auto"}},n(p,{checked:x.enabled===!0,disabled:d,onChange:w=>void M("/access/save",{enabled:w.target.checked})}),n("span",null,"启用成员权限")),n("span",{className:"ds-hint"},x.enabled===!0?"已启用:写操作要认人,删除/配置/发布要管理员。":"未启用:任何人都是管理员(单机用法)。")),x.enabled===!0&&(x.members??[]).length===0?n("div",{className:"ds-error"},"已启用但名单是空的:除了名单里的人,其他人都动不了。先把你自己加进去。"):null,x.enabled===!0?n("label",{className:"ds-check"},n(p,{checked:(x.agents??"inherit")==="inherit",disabled:d,onChange:w=>void M("/access/save",{agents:w.target.checked?"inherit":"bypass"})}),n("span",null,"AI 专家也受这份名单约束")):null,x.enabled===!0?n("div",{className:"ds-hint"},(x.agents??"inherit")==="inherit"?"专家调工具时按同一张名单判权限:只读专家只能看,成员能干活,管理员才能改配置。名单里没有的专家会被拦下,记得在下面加进去。":"已豁免:专家调用工具不受名单限制,但仍记审计。适合「全交给 AI」的团队。"):null,(x.members??[]).map(w=>n("div",{key:w.name,className:"ds-listrow"},n("div",{className:"ds-listrow-main"},n("div",null,w.name,w.name===i()?n(u,{className:"ds-chip ds-chip-soft"},"我"):null),n("div",{className:"ds-hint"},(ACCESS_ROLE_OPTIONS.find(T=>T.id===w.role)??ACCESS_ROLE_OPTIONS[1]).hint)),n("div",{className:"ds-listrow-actions"},n(f,{className:"ds-select",value:w.role,disabled:d,onChange:T=>void M("/access/save",{member:{name:w.name,role:T.target.value}})},ACCESS_ROLE_OPTIONS.map(T=>n("option",{key:T.id,value:T.id},T.label))),n("button",{className:"ds-btn ds-btn-ghost",disabled:d,onClick:()=>void M("/access/delete",{name:w.name})},"移除")))),(x.members??[]).length===0?n("div",{className:"ds-empty"},"还没有成员。"):null,n("div",{className:"ds-row",style:{gap:8,flexWrap:"wrap"}},n(c,{className:"ds-input",placeholder:"成员名字(和页头「👤 我是…」一致)",value:k.name,style:{flex:"1 1 200px"},onChange:w=>g({...k,name:w.target.value})}),n(f,{className:"ds-select",value:k.role,style:{flex:"0 0 130px"},onChange:w=>g({...k,role:w.target.value})},ACCESS_ROLE_OPTIONS.map(w=>n("option",{key:w.id,value:w.id},w.label))),n("button",{className:"ds-btn ds-btn-primary",disabled:d||k.name.trim()==="",onClick:()=>void P()},"加成员")),y!==null?n("div",{className:"ds-error"},y):null)),n("div",{className:"ds-panel"},n("div",{className:"ds-panel-head"},n("span",null,`审计日志(显示 ${F.length} / 命中 ${U.length} / 共 ${b.length} 条)`),n("div",{className:"ds-row",style:{gap:8,marginLeft:"auto"}},n(c,{className:"ds-input",placeholder:"搜动作 / 人 / 详情",value:D,style:{width:170},onChange:w=>W(w.target.value)}),n(f,{className:"ds-select",value:C===""?"__all__":C,style:{minWidth:150},onChange:w=>E(w.target.value==="__all__"?"":w.target.value)},n("option",{value:"__all__"},"所有人"),H.map(w=>n("option",{key:w,value:w},w))),n("label",{className:"ds-check"},n(p,{checked:_,onChange:w=>I(w.target.checked)}),n("span",null,"只看失败")),n("button",{className:"ds-btn ds-btn-ghost",disabled:d,onClick:async()=>{await l("清空审计日志?清掉之后所有操作记录都没了。",{title:"清空审计日志",danger:!0,okText:"清空"})&&M("/audit/clear",{})}},"清空"),n("button",{className:"ds-btn ds-btn-ghost",disabled:U.length===0,onClick:L,title:"把当前筛选结果导出为 CSV(Excel 可直接打开)"},"导出 CSV"))),n("div",{className:"ds-panel-body"},F.length===0?n("div",{className:"ds-empty"},b.length===0?"还没有记录。做一次写操作就会出现在这里。":"没有符合筛选条件的记录。把关键字清空或换个人试试。"):F.map(w=>n("div",{key:w.id,className:"ds-audit-row","data-ok":w.ok?"1":"0"},n("span",{className:"ds-audit-time"},Ke(w.at)),n("span",{className:"ds-audit-by"},w.by),n("span",{className:"ds-audit-what"},ROUTE_LABEL[w.route]??w.route),n("span",{className:"ds-audit-detail"},w.summary),n("span",{className:"ds-audit-status","data-ok":w.ok?"1":"0"},w.ok?"成功":`失败 ${w.status}`))))))}function Gx({state:e,onChanged:t}){const{h:a,sessionToken:n,api:r,toast:s,currentUser:o,confirmDialog:i,TextField:l,ChipField:c,CheckField:u,SelectField:p,React:f}=vt(),v=e.config,[x,b]=f.useState(null),[k,g]=f.useState(!1),[d,h]=f.useState(null),[y,S]=f.useState(""),C=e.statuses??[],E=f.useMemo(()=>rf(v),[v]),_=F=>{const L=v.columns?.[F];return L==null?[]:Array.isArray(L.members)?L.members:L.kind==="none"?[]:[{kind:L.kind,id:L.id}]},I=F=>v.columns?.[F]?.strategy??"round_robin",D=F=>{if(F.kind==="role")return"🎯 "+(za[F.id]??F.id);if(F.kind==="team"){const w=(v.teams??[]).find(T=>T.id===F.id);return w===void 0?`👥 ${F.id}`:`${w.emoji??"👥"} ${w.name}`}const L=(v.experts??[]).find(w=>w.id===F.id);return L===void 0?F.id:`${L.emoji} ${L.name}`},W=Array.isArray(v.design?.columns)?v.design.columns:["dev"],M=async F=>{g(!0),b(null);try{await r("/config",{design:{...v.design??{},...F}}),await t()}catch(L){b(L instanceof Error?L.message:String(L))}finally{g(!1)}},P=F=>{const L=W.includes(F)?W.filter(w=>w!==F):[...W,F];M({columns:L})},H=async(F,L,w)=>{g(!0),b(null);try{await r("/columns/save",{status:F,members:L,strategy:w}),await t()}catch(T){b(T instanceof Error?T.message:String(T))}finally{g(!1)}},J=(F,L)=>{const w=L.key.startsWith("t:")?"team":L.key.startsWith("r:")?"role":"expert",T=L.key.slice(2),K=_(F);K.some(R=>R.kind===w&&R.id===T)||(S(""),H(F,[...K,{kind:w,id:T}],I(F)))},U=(F,L)=>void H(F,_(F).filter((w,T)=>T!==L),I(F));return a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"每一列由谁负责"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-hint"},"推荐挂「角色位」:澄清/验收挂产品经理、开发挂研发工程师、测试挂测试工程师——派单时会按卡片的业务领域自动挑对应领域专家(如支付产品经理);也可以直接指定某位专家或专家团。一列可挂多人(候选池),一个都不挂就只把卡停在原地等人认领。"),a("div",{className:"ds-hint",style:{marginTop:14}},"设计闸门:打开后,卡片进下面选中的列之前设计必须评审通过(验收时可 override 强推)。"),a("div",{className:"ds-row",style:{gap:8,flexWrap:"wrap",marginTop:6}},a("button",{className:v.design?.enabled===!0?"ds-btn ds-btn-primary":"ds-btn",disabled:k,"data-design-gate":v.design?.enabled===!0?"on":"off",onClick:()=>void M({enabled:v.design?.enabled!==!0})},v.design?.enabled===!0?"设计闸门:已开启":"设计闸门:已关闭"),C.map(F=>a("button",{key:"gate-"+F.id,className:W.includes(F.id)?"ds-btn ds-btn-primary":"ds-btn",disabled:k,onClick:()=>P(F.id)},F.label??F.id))),C.map(F=>{const L=_(F.id),w=I(F.id);return a("div",{className:"ds-colrow",key:F.id},a("div",{className:"ds-colrow-head"},a("span",{className:"ds-colrow-name"},F.label??F.id),L.length===0?a("span",{className:"ds-hint"},"👤 等人认领(不自动派)"):L.map((T,K)=>a(c,{className:"ds-chip ds-chip-owner",key:`${T.kind}:${T.id}`},D(T),a("button",{className:"ds-chip-x",title:"从候选里去掉",disabled:k,onClick:()=>U(F.id,K)},"×"))),a("button",{className:"ds-btn",style:{marginLeft:"auto"},disabled:k,onClick:()=>{S(""),h(T=>T===F.id?null:F.id)}},d===F.id?"收起":"+ 加候选"),L.length>=2?a(p,{className:"ds-input",style:{width:240},value:w,disabled:k,onChange:T=>void H(F.id,L,T.target.value)},STRATEGIES.map(T=>a("option",{key:T.id,value:T.id},T.label))):null),d===F.id?a("div",{className:"ds-colpick"},a(l,{className:"ds-mention-search",autoFocus:!0,value:y,placeholder:"搜专家或专家团,点一下加进候选",onChange:T=>S(T.target.value)}),a("div",{className:"ds-colpick-list"},Ho(E,y).length===0?a("div",{className:"ds-mention-empty"},"没有匹配的专家或专家团"):Ho(E,y).map(T=>a("button",{key:T.key,className:"ds-mention-item",onClick:()=>J(F.id,T)},a("span",null,T.emoji),a("span",null,T.name),a("span",{className:"ds-hint"},T.hint))))):null)}),x!==null?a("div",{className:"ds-error"},x):null))}function Kx({state:e,onChanged:t}){const{h:a,sessionToken:n,api:r,toast:s,currentUser:o,confirmDialog:i,TextField:l,ChipField:c,CheckField:u,SelectField:p,React:f}=vt(),[v,x]=f.useState(e.storage??null),[b,k]=f.useState([]),[g,d]=f.useState(null),[h,y]=f.useState(null),[S,C]=f.useState(!1),E=f.useCallback(()=>{r("/snapshots").then(D=>{x(D.storage??null),k(D.snapshots??[]),d(null)}).catch(D=>d(D instanceof Error?D.message:String(D)))},[]);f.useEffect(()=>{E()},[E,e.revision]);const _=async D=>{await i(`把看板回滚到快照 #${D}?当前的这一版会先另存一份,不会丢。`,{title:"回滚快照",okText:"回滚"})&&(C(!0),y(null),r("/snapshot/restore",{id:D}).then(()=>{y(`已回滚到快照 #${D}`),E(),t?.()}).catch(W=>d(W instanceof Error?W.message:String(W))).finally(()=>C(!1)))},I=D=>{const W=new Date(Number(D)||0);return`${W.getMonth()+1}-${String(W.getDate()).padStart(2,"0")} ${String(W.getHours()).padStart(2,"0")}:${String(W.getMinutes()).padStart(2,"0")}:${String(W.getSeconds()).padStart(2,"0")}`};return a("div",{className:"ds-pane-stack"},a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"台账存储",a(c,{className:"ds-chip","data-on":v?.engine==="sqlite"?"1":"0"},v?.engine==="sqlite"?"SQLite":"单文件")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-an-kv"},a("span",{},"数据文件"),a("b",{},v?.db??"—")),a("div",{className:"ds-an-kv"},a("span",{},"密钥文件(600 权限,明文不进台账)"),a("b",{},v?.secretsFile??"—")),a("div",{className:"ds-an-kv"},a("span",{},"保留快照"),a("b",{},`${v?.snapshots??0} / ${v?.snapshotLimit??0}`)),a("div",{className:"ds-hint"},"每次写操作都会在同一个事务里先存一份快照,再更新台账;多进程同时写会排队(busy_timeout 5s),不会互相覆盖。"))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"快照",a("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:"auto"},onClick:E,disabled:S},"刷新")),a("div",{className:"ds-panel-body"},h!==null?a("div",{className:"ds-ok"},h):null,g!==null?a("div",{className:"ds-error"},g):null,b.length===0?a("div",{className:"ds-hint"},v?.engine==="sqlite"?"还没有快照。":"单文件模式没有快照(这台机器的 node 不支持 node:sqlite 时会退回单文件)。"):b.map(D=>a("div",{key:D.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`#${D.id} · rev ${D.revision}`),a("div",{className:"ds-hint"},`${I(D.at)} · ${(D.bytes/1024).toFixed(1)} KB`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:S,onClick:()=>_(D.id)},"回滚到这版")))))))}function Yx({state:e,config:t,onChanged:a}){const{h:n,sessionToken:r,api:s,toast:o,currentUser:i,confirmDialog:l,TextField:c,ChipField:u,CheckField:p,SelectField:f,React:v}=vt(),x=e.integrations??{channels:[],broken:0},[b,k]=v.useState(""),[g,d]=v.useState(null),h=(e.requirements??[]).find(w=>(w.folder?.token??"")!==""),y=e.agents??{},S=y.lock??null,C=S===null?0:Math.max(0,Date.now()-(Number(S.at)||0)),[E,_]=v.useState(""),[I,D]=v.useState(!1),W=E.trim().toLowerCase(),M=(y.log??[]).filter(w=>{const T=String(w?.line??"");return I&&!/失败|错误|异常|拦|拒绝/.test(T)?!1:W===""||T.toLowerCase().includes(W)}),P=M.slice(0,12),H=()=>{const w=new URLSearchParams({q:E.trim(),failed:I?"1":"0",token:r()??""}),T=document.createElement("a");T.href=`${API}/export/dispatch.csv?${w.toString()}`,T.rel="noopener",document.body.appendChild(T),T.click(),document.body.removeChild(T),o(`正在导出 ${M.length} 条派单日志`)},J=async w=>{k(w),d(null);try{if(w==="gitlab"){const T=await s("/gitlab/sync",{});d(T.report===void 0?"已触发 GitLab 同步":`已同步:${(T.report.projects??[]).map(K=>`${K.name} +${K.created}`).join(",")||"无新卡"}`)}else if(w==="drive")if(h===void 0)d("还没有带云盘文件夹的需求,先建一条需求并归档一次。");else{const T=await s("/requirement/sync",{id:h.id});d(`已归档 ${T.report?.written??0} / ${T.report?.total??0} 件`),o(`已归档 ${T.report?.written??0} / ${T.report?.total??0} 件`)}else if(w==="feishu"){const T=await s("/feishu/test",{});d(T.ok===!0?"测试消息已发出":`发送失败:${T.result?.error??"没配置机器人"}`)}else d("代码隔离没有独立的重试入口,下一张卡派单时会自动重试。");await a()}catch(T){{const K=T instanceof Error?T.message:String(T);d(K),o(K,"error")}}finally{k("")}},U=w=>{const T=Number(w)||0;if(T===0)return"—";const K=new Date(T);return`${K.getMonth()+1}-${String(K.getDate()).padStart(2,"0")} ${String(K.getHours()).padStart(2,"0")}:${String(K.getMinutes()).padStart(2,"0")}`},F={ok:"正常",error:"异常",idle:"还没用过"},L={ok:"#0f9d58",error:"#e5484d",idle:"#8a8f98"};return n("div",{className:"ds-pane-stack"},n("div",{className:"ds-panel"},n("div",{className:"ds-panel-head"},"派单运行状态",n(u,{className:"ds-chip","data-on":y.enabled===!0?"1":"0"},y.enabled===!0?"运行中":"已停用"),n("div",{className:"ds-row",style:{gap:8,marginLeft:"auto"}},n(c,{className:"ds-input",placeholder:"搜派单日志",value:E,style:{width:150},onChange:w=>_(w.target.value)}),n("label",{className:"ds-check"},n(p,{checked:I,onChange:w=>D(w.target.checked)}),n("span",null,"只看失败")),n("button",{className:"ds-btn ds-btn-ghost",disabled:M.length===0,onClick:H,title:"把当前筛选结果导出为 CSV"},"导出 CSV"))),n("div",{className:"ds-panel-body"},n("div",{className:"ds-kv"},n("dt",{},"实例"),n("dd",{},String(y.instance??"—")),n("dt",{},"派单锁"),n("dd",{},S===null?"还没抢到过(可能是内存模式)":`${S.mine===!0?"本进程持有":"其他实例持有"} · ${Math.round(C/1e3)} 秒前续租`),n("dt",{},"并发占用"),n("dd",{},`${(y.running??[]).length} / ${y.maxConcurrent??"—"}`),n("dt",{},"最近一轮"),n("dd",{},U(y.lastTickAt)),n("dt",{},"最近错误"),n("dd",{},String(y.lastError??"")===""?"无":String(y.lastError))),(y.log??[]).length===0?n("div",{className:"ds-empty"},"还没有派单日志。卡进入自动列后,这里会出现派单、空闲、失败记录。"):P.length===0?n("div",{className:"ds-empty"},"没有符合筛选条件的派单日志。"):n("div",{className:"ds-log"},P.map((w,T)=>n("div",{key:String(T),className:"ds-log-line"},`${U(w.at)} · ${String(w.line??"")}`))),(y.log??[]).length>0&&M.length>P.length?n("div",{className:"ds-hint"},`只显示最近 ${P.length} / 命中 ${M.length} 条,导出可看全量`):null)),n("div",{className:"ds-panel"},n("div",{className:"ds-panel-head"},"集成状态",n(u,{className:"ds-chip","data-on":x.broken===0?"1":"0"},x.broken===0?"全部正常":`${x.broken} 条异常`),n("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:"auto"},onClick:()=>void a()},"刷新")),n("div",{className:"ds-panel-body"},g!==null?n("div",{className:"ds-hint"},g):null,x.channels.length===0?n("div",{className:"ds-empty"},"还没有调用过任何外部集成。配好 GitLab / 飞书机器人 / 云盘文件夹后,第一次成功或失败就会出现在这里。"):x.channels.map(w=>n("div",{key:w.channel,className:"ds-listrow"},n("div",{className:"ds-listrow-main"},n("div",null,n("span",{style:{color:L[w.state],fontWeight:700}},"● "),w.label,n(u,{className:"ds-chip ds-chip-soft",style:{marginLeft:6}},F[w.state])),n("div",{className:"ds-hint"},w.hint),n("div",{className:"ds-hint"},`最近一次:${U(w.at)} · 成功 ${U(w.lastOkAt)} · 失败 ${U(w.lastErrorAt)}${w.attempts>0?` · 试着调了 ${w.attempts} 次,连挂 ${w.failures} 次`:""}`),w.lastError===""?null:n("div",{className:"ds-error"},w.lastError)),n("div",{className:"ds-listrow-actions"},n("button",{className:"ds-btn ds-btn-ghost",disabled:b!=="",onClick:()=>void J(w.channel)},b===w.channel?"重试中…":"再试一次")))))),n("div",{className:"ds-panel ds-form-panel"},n("div",{className:"ds-panel-head"},"失败了会怎样"),n("div",{className:"ds-panel-body"},n("div",{className:"ds-hint"},"· 飞书通知:同一条消息会自动重发一次,两次都失败才记异常(飞书 200 但 body 里 code≠0 也算失败)。"),n("div",{className:"ds-hint"},"· GitLab:拉 Issue、回写评论、对 MR 失败会记在这里;同步定时器仍会自动重试下一轮。"),n("div",{className:"ds-hint"},"· 飞书云盘:归档失败会挂在交付物自身的 doc.error 上,这里再汇总一份。"),n("div",{className:"ds-hint"},"· 代码隔离:worktree 建失败只影响这一张卡(退回普通工作区),不影响别的卡。"))))}function Qx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,ChipField:c,CheckField:u,MdField:p,SideDrawer:f,React:v}=vt(),x=e.gitlab?.projects??[],[b,k]=v.useState(null),[g,d]=v.useState(null),[h,y]=v.useState(null),[S,C]=v.useState(!1),E=async()=>{if(b!==null){C(!0),d(null),y(null);try{await r("/repos/save",{project:b}),k(null),y("已保存"),s("已保存"),await t()}catch(P){d(P instanceof Error?P.message:String(P))}finally{C(!1)}}},_=async P=>{if(await o(`删除 GitLab 项目「${P.name}」?已同步的卡片会保留。`,{title:"删除 GitLab 项目",danger:!0,okText:"删除"})){C(!0),d(null);try{await r("/repos/delete",{id:P.id}),await t()}catch(H){d(H instanceof Error?H.message:String(H))}finally{C(!1)}}},I=async P=>{C(!0),d(null),y(null);try{const H=await r("/repos/test",{id:P.id});y(`连通:${P.project}(示例 Issue ${H.sample} 条)`)}catch(H){d(H instanceof Error?H.message:String(H))}finally{C(!1)}},D=async P=>{C(!0),d(null),y(null);try{const J=((await r("/gitlab/sync",{project:P.id})).summary?.projects??[])[0];y(J===void 0?"没有可同步的项目":`${J.name}:拉取 ${J.fetched},新建 ${J.created}${J.error?",错误 "+J.error:""}`),await t()}catch(H){d(H instanceof Error?H.message:String(H))}finally{C(!1)}},W=(P,H)=>k({...b,[P]:H}),M=x.map(P=>a("div",{className:"ds-listrow",key:P.id},a("div",{className:"ds-listrow-main"},a("div",{},`${P.name||P.project}${P.syncEnabled?" · 自动同步":""}${P.tokenSet?"":" · 缺令牌"}`),a("div",{className:"ds-hint"},`${P.url||e.gitlab.url||"(未填地址)"} / ${P.project}${P.lastSyncAt?" · 上次 "+new Date(P.lastSyncAt).toLocaleString():""}`),P.lastSyncError?a("div",{className:"ds-error"},P.lastSyncError):null),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn",disabled:S,onClick:()=>void D(P)},"同步"),a("button",{className:"ds-btn",disabled:S,onClick:()=>void I(P)},"测试"),a("button",{className:"ds-btn",disabled:S,onClick:()=>k({...P,token:""})},"编辑"),i("删除这个项目(卡不会删)",a("button",{className:"ds-btn",disabled:S,onClick:()=>void _(P),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(n,{title:"代码仓库(GitLab,可以有多个)",hint:"每个项目单独填地址、项目路径和令牌;同步时按项目分开去重,两个仓库都有 #12 也不会串。",rows:M,onAdd:()=>k({id:"",name:"",url:e.gitlab?.url??"",project:"",token:"",syncEnabled:!1,syncLabel:""}),addLabel:"新增项目",render:P=>P,empty:"还没有项目,点「新增项目」。"}),h!==null?a("div",{className:"ds-ok",style:{marginBottom:8}},h):null,b===null?null:a(f,{title:b.id?`编辑仓库:${b.name||b.project}`:"新增仓库",hint:"一个仓库对应 GitLab 上的一个项目;令牌只写盘不回显,留空表示不修改。",onClose:()=>k(null),footer:[a("button",{key:"save",className:"ds-btn ds-btn-primary",disabled:S,onClick:()=>void E()},S?"保存中…":"保存"),a("button",{key:"cancel",className:"ds-btn",disabled:S,onClick:()=>k(null)},"取消"),g!==null?a("span",{key:"error",className:"ds-error ds-side-note"},g):null]},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"显示名"),a(l,{className:"ds-input",value:b.name,placeholder:"订单服务",onChange:P=>W("name",P.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"GitLab 地址"),a(l,{className:"ds-input",value:b.url,placeholder:"https://gitlab.example.com",onChange:P=>W("url",P.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"项目路径(含 namespace)"),a(l,{className:"ds-input",value:b.project,placeholder:"backend/order-service",onChange:P=>W("project",P.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},b.tokenSet?"Access Token(已设置,留空则不修改)":"Access Token"),a(l,{className:"ds-input",type:"password",value:b.token,placeholder:b.tokenSet?"••••••••":"glpat-…",onChange:P=>W("token",P.target.value)}),a("div",{className:"ds-hint"},"需要 api 权限;只写入磁盘,不回显。")),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"只同步带此标签的 Issue"),a(l,{className:"ds-input",value:b.syncLabel,placeholder:"留空则同步全部 open Issue",onChange:P=>W("syncLabel",P.target.value)})),a("div",{className:"ds-field"},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:b.syncEnabled===!0,onChange:P=>W("syncEnabled",P.target.checked)}),a("span",{},"每分钟自动把这个项目的新 Issue 拉成卡片")))))}const cf=[{key:"change",label:"卡片状态变化"},{key:"sla",label:"缺陷 SLA 超时"},{key:"bug",label:"自动开缺陷"},{key:"release",label:"版本发布"}],Xx=e=>{const t=e.events!==null&&typeof e.events=="object"?e.events:null;return t===null?e.notifyOnChange===!1?[]:["change","sla","bug","release"]:cf.filter(a=>t[a.key]!==!1).map(a=>a.label)};function Zx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,ChipField:c,CheckField:u,MdField:p,SideDrawer:f,React:v}=vt(),x=e.feishu?.bots??[],[b,k]=v.useState(null),[g,d]=v.useState(null),[h,y]=v.useState(null),[S,C]=v.useState(!1),[E,_]=v.useState(e.feishu?.driveFolder??""),[I,D]=v.useState(e.feishu?.autoSyncArtifacts!==!1),[W,M]=v.useState(!1),[P,H]=v.useState(null),J=async()=>{M(!0),H(null);try{await r("/config",{feishu:{driveFolder:E.trim(),autoSyncArtifacts:I}}),H("已保存"),await t()}catch(O){H(O instanceof Error?O.message:String(O))}finally{M(!1)}},U=async()=>{if(b!==null){C(!0),d(null),y(null);try{await r("/bots/save",{bot:b}),k(null),y("已保存"),s("已保存"),await t()}catch(O){d(O instanceof Error?O.message:String(O))}finally{C(!1)}}},F=async O=>{if(await o(`删除飞书机器人「${O.name}」?`,{title:"删除机器人",danger:!0,okText:"删除"})){C(!0),d(null);try{await r("/bots/delete",{id:O.id}),await t()}catch(G){d(G instanceof Error?G.message:String(G))}finally{C(!1)}}},L=async O=>{C(!0),d(null),y(null);try{const we=((await r("/feishu/test",{id:O.id})).results??[])[0];y(we!==void 0&&we.ok===!0?`「${O.name}」测试消息已发送`:`「${O.name}」发送失败:${we?.error??"检查 Webhook"}`)}catch(G){d(G instanceof Error?G.message:String(G))}finally{C(!1)}},w=(O,G)=>k({...b,[O]:G}),T=O=>{const G=b.events!==null&&typeof b.events=="object"?b.events:null;return G===null?O==="change"?b.notifyOnChange!==!1:!0:G[O]!==!1},K=(O,G)=>{const fe={...b.events!==null&&typeof b.events=="object"?b.events:{},[O]:G};k({...b,events:fe,notifyOnChange:fe.change!==!1})},R=x.map(O=>a("div",{className:"ds-listrow",key:O.id},a("div",{className:"ds-listrow-main"},a("div",{},`${O.name}${O.enabled===!1?" (已停用)":""}${O.webhookSet?"":" · 缺 Webhook"}`),a("div",{className:"ds-hint"},`Webhook ${O.webhookHint||"(未设置)"}${O.chatId?" · 群 "+O.chatId:""} · 收:${Xx(O).join(" / ")||"什么都不收"}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn",disabled:S,onClick:()=>void L(O)},"测试"),a("button",{className:"ds-btn",disabled:S,onClick:()=>k({...O,webhook:""})},"编辑"),a("button",{className:"ds-btn",disabled:S,onClick:()=>void F(O),"data-danger":"1"},"删除"))));return a("div",{className:"ds-pane-stack"},a(n,{title:"飞书机器人(可以有多个)",hint:"一个群建一个机器人,并在下面勾选它要收哪些事件;研发群只勾缺陷相关,产品群只勾版本发布。",rows:R,onAdd:()=>k({id:"",name:"",webhook:"",chatId:"",notifyOnChange:!0,events:{change:!0,sla:!0,bug:!0,release:!0},enabled:!0}),addLabel:"新增机器人",render:O=>O,empty:"还没有机器人。"}),h!==null?a("div",{className:"ds-ok",style:{marginBottom:8}},h):null,a("div",{className:"ds-panel ds-form-panel"},a("div",{className:"ds-panel-head"},"云盘归档(产物文档)"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"云盘根文件夹 token 或链接"),a(l,{className:"ds-input",value:E,placeholder:"IuJPfTCtWlDrzwdCFmLc71aynbg 或文件夹链接",onChange:O=>_(O.target.value)}),a("div",{className:"ds-hint"},`每条需求会在它下面建一个「编号 + 标题」的同名文件夹,交付物写成文件夹里的一份 .md 文档。飞书授权:${e.feishu?.larkReady?"lark-cli 已就绪":"lark-cli 未就绪或未授权"}`)),a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:I,onChange:O=>D(O.target.checked)}),a("span",{},"专家登记交付物时自动归档(关掉就只存平台里)")),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:W,onClick:()=>void J()},W?"保存中…":"保存"),P!==null?a("div",{className:"ds-hint"},P):null))),b===null?null:a(f,{title:b.id?`编辑机器人:${b.name||b.id}`:"新增机器人",hint:"Webhook 只写盘不回显,留空表示不修改;一个群建一个机器人即可。",onClose:()=>k(null),footer:[a("button",{key:"save",className:"ds-btn ds-btn-primary",disabled:S,onClick:()=>void U()},S?"保存中…":"保存"),a("button",{key:"cancel",className:"ds-btn",disabled:S,onClick:()=>k(null)},"取消"),g!==null?a("span",{key:"error",className:"ds-error ds-side-note"},g):null]},a("div",{className:"ds-field"},a("div",{className:"ds-label"},"名字"),a(l,{className:"ds-input",value:b.name,placeholder:"研发群通知",onChange:O=>w("name",O.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},b.webhookSet?"Webhook(已设置,留空则不修改)":"Webhook"),a(l,{className:"ds-input",type:"password",value:b.webhook,placeholder:"https://open.feishu.cn/open-apis/bot/v2/hook/…",onChange:O=>w("webhook",O.target.value)}),a("div",{className:"ds-hint"},"在飞书群里「添加机器人 → 自定义机器人」即可拿到,不需要自建应用。")),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"备注群 chat_id(可选)"),a(l,{className:"ds-input",value:b.chatId,placeholder:"oc_…",onChange:O=>w("chatId",O.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"收哪些事件"),a("div",{className:"ds-row",style:{gap:16,flexWrap:"wrap"}},...cf.map(O=>a("label",{key:O.key,className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:T(O.key),onChange:G=>K(O.key,G.target.checked)}),a("span",{},O.label)))),a("div",{className:"ds-hint"},"不勾任何一项就等于这个群静默(只保留测试消息)。")),a("div",{className:"ds-row",style:{gap:16}},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:b.enabled!==!1,onChange:O=>w("enabled",O.target.checked)}),a("span",{},"启用")))))}function Jx({config:e,onChanged:t}){const{h:a,Editor:n,api:r,toast:s,confirmDialog:o,tipOf:i,TextField:l,ChipField:c,CheckField:u,MdField:p,SideDrawer:f,React:v}=vt(),[x,b]=v.useState(null),[k,g]=v.useState(null),[d,h]=v.useState(null),[y,S]=v.useState(null),[C,E]=v.useState(!1),[_,I]=v.useState(!1),[D,W]=v.useState(!1),M=v.useCallback(async()=>{try{const O=await r("/skills");b({root:O.root,skills:O.skills??[],onDisk:O.onDisk??[]})}catch(O){h(O instanceof Error?O.message:String(O))}},[]);v.useEffect(()=>{M()},[M]);const P=x?.skills??[],H=x?.onDisk??[],J=O=>(e.experts??[]).filter(G=>(G.skills??[]).includes(O)),U=(O,G)=>g({...k,[O]:G}),F=async()=>{if(k!==null){E(!0),h(null),S(null);try{const O=await r("/skills/save",{skill:{name:k.name,description:k.description,whenToUse:k.whenToUse,tags:k.tags,content:k.content,enabled:k.enabled!==!1}});g(null),S(O.warning??(O.file?`已写入 ${O.file}`:"已保存")),await M(),await t()}catch(O){h(O instanceof Error?O.message:String(O))}finally{E(!1)}}},L=async O=>{if(await o(`删除技能「${O.name}」?磁盘上的 SKILL.md 也会一起删掉。`,{title:"删除技能",danger:!0,okText:"删除"})){E(!0),h(null),S(null);try{await r("/skills/delete",{name:O.name}),await M(),await t()}catch(G){h(G instanceof Error?G.message:String(G))}finally{E(!1)}}},w=async()=>{E(!0),h(null),S(null);try{const O=await r("/skills/sync",{});S(`已重新写入 ${(O.written??[]).length} 条技能`),await M()}catch(O){h(O instanceof Error?O.message:String(O))}finally{E(!1)}},T=async O=>{if(O!=null){I(!0),h(null),S(null);try{const G=new Uint8Array(await O.arrayBuffer());let we="";const fe=32768;for(let ae=0;ae<G.length;ae+=fe)we+=String.fromCharCode.apply(null,G.subarray(ae,ae+fe));const ye=await r("/skills/upload",{fileName:O.name,dataBase64:window.btoa(we),overwrite:D}),He=ye.overwritten===!0?"已覆盖":"已上传";S(`${He}技能「${ye.skill.name}」${ye.file?`,写入 ${ye.file}`:""}`),g(null),await M(),await t()}catch(G){h(G instanceof Error?G.message:String(G))}finally{I(!1)}}},K=P.map(O=>a("div",{className:"ds-listrow",key:O.name},a("div",{className:"ds-listrow-main"},a("div",{},a("code",{},O.name),(O.tags??[]).map(G=>a(c,{className:"ds-chip",key:G},G)),O.enabled===!1?a("span",{className:"ds-hint"}," (已停用)"):null,H.includes(O.name)?null:a("span",{className:"ds-err"}," 未落盘")),a("div",{className:"ds-hint"},O.description),O.whenToUse?a("div",{className:"ds-hint"},`何时用:${O.whenToUse}`):null,J(O.name).length>0?a("div",{className:"ds-hint"},`使用中的专家:${J(O.name).map(G=>`${G.emoji}${G.name}`).join("、")}`):a("div",{className:"ds-empty"},"还没有专家挂这个技能")),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn",disabled:C,onClick:()=>{S(null),g({...O,editing:!0,tags:(O.tags??[]).join(", ")})}},"编辑"),a("button",{className:"ds-btn",disabled:C,onClick:()=>void L(O),"data-danger":"1"},"删除")))),R=H.filter(O=>!P.some(G=>G.name===O));return a("div",{className:"ds-pane-stack"},a(n,{title:"技能库(skills-manager)",hint:x===null?"读取中…":`保存在 ${x.root}/<名字>/SKILL.md,所有专家都能用 skill 工具加载;专家也可以自己用 studio_skill_save 往这里加技能。支持上传 SKILL.md 或包含 SKILL.md 的 .zip 技能包。`,rows:K,addLabel:"新建技能",onAdd:()=>{S(null),g({name:"",description:"",whenToUse:"",tags:"",content:`# 什么时候用
|
|
30
30
|
|
|
31
31
|
# 步骤
|
|
32
32
|
|
|
@@ -43,7 +43,7 @@ GET /api/points | 200 |`,onChange:m=>ie({...X,steps:m.target.value})}),e("div",{
|
|
|
43
43
|
`);if(t==="")return[];const a=t.split(`
|
|
44
44
|
`);return a.length>0&&a[a.length-1]===""&&a.pop(),a}function sw(e,t){const a=uf(e),n=uf(t);if(a.length>od||n.length>od)return[{kind:"del",text:`(原基线 ${a.length} 行,超过 ${od} 行的对比上限)`},{kind:"add",text:`(当前文档 ${n.length} 行)`}];const r=Array.from({length:a.length+1},()=>new Array(n.length+1).fill(0));for(let l=a.length-1;l>=0;l-=1)for(let c=n.length-1;c>=0;c-=1)r[l][c]=a[l]===n[c]?r[l+1][c+1]+1:Math.max(r[l+1][c],r[l][c+1]);const s=[];let o=0,i=0;for(;o<a.length&&i<n.length;){if(a[o]===n[i]){s.push({kind:"same",text:a[o]}),o+=1,i+=1;continue}if(r[o+1][i]>=r[o][i+1]){s.push({kind:"del",text:a[o]}),o+=1;continue}s.push({kind:"add",text:n[i]}),i+=1}for(;o<a.length;)s.push({kind:"del",text:a[o]}),o+=1;for(;i<n.length;)s.push({kind:"add",text:n[i]}),i+=1;return s}function rw(e){const t=Array.isArray(e)?e:[];return{added:t.filter(a=>a.kind==="add").length,removed:t.filter(a=>a.kind==="del").length,same:t.filter(a=>a.kind==="same").length}}function ow({state:e,onOpenTask:t,onChanged:a,createSignal:n,focusReq:r}){const{h:s,React:o,api:i,toast:l,currentUser:c,confirmDialog:u,promptDialog:p,renderMarkdown:f,TextField:v,TextAreaField:x,ChipField:b,CheckField:k,SelectField:g,MdField:d}=vt(),h=e.requirements??[],y=e.config.projects??[],[S,C]=o.useState(h.length>0?h[0].id:null),E=()=>{I({title:"",description:"",project:"",iteration:"",owner:"",priority:"normal",status:"draft",links:{}}),W(null)};o.useEffect(()=>{Number(n)>0&&E()},[n]),o.useEffect(()=>{r&&r.id&&C(r.id)},[r]);const[_,I]=o.useState(null),[D,W]=o.useState(null),[M,P]=o.useState(!1),[H,J]=o.useState(null),[U,F]=o.useState(null),[L,w]=o.useState(null),[T,K]=o.useState(null),[R,O]=o.useState(null),[G,we]=o.useState(!1),[fe,ye]=o.useState(null),[He,ae]=o.useState(!1),[z,te]=o.useState(null),[xe,X]=o.useState(null),[ie,ge]=o.useState(null),[V,Ee]=o.useState(""),[ke,Ae]=o.useState(null),[Ve,Ya]=o.useState("0"),[Rn,B]=o.useState([]),[he,je]=o.useState(null),[Ue,_t]=o.useState(null),[Gt,st]=o.useState(!1),Ct=async()=>{if(pt!==null){st(!0);try{const q=await i("/requirement/impact",{id:pt.id});_t({summary:q.summary,cards:q.cards??[],cases:q.cases??[]})}catch(q){je(q instanceof Error?q.message:String(q))}finally{st(!1)}}},pt=h.find(q=>q.id===S)??null;o.useEffect(()=>{Ya(String(pt?.order??0)),B(Array.isArray(pt?.dependsOn)?pt.dependsOn:[]),je(null)},[S,pt?.updatedAt]);const Ea=async()=>{if(pt!==null)try{await i("/requirement/save",{requirement:{id:pt.id,title:pt.title,description:pt.description??"",project:pt.project??"",iteration:pt.iteration??"",owner:pt.owner??"",priority:pt.priority??"normal",status:pt.status??"draft",links:pt.links??{},order:Number(Ve)||0,dependsOn:Rn}}),je("已保存:排序与依赖"),l("排序与依赖已保存"),a!==void 0&&a()}catch(q){je(q instanceof Error?q.message:String(q))}},hs=async q=>{if(Q===null)return;const ce=String(V??"").trim();if(q==="sign"&&ce===""){Ae("签字要写一句结论:交付了什么、遗留什么");return}try{await i("/requirement/signoff",{id:Q.id,action:q,note:ce,by:c()}),Ae(q==="sign"?"已签字":"已取消签字"),l(q==="sign"?"需求已签字":"已取消需求签字"),Ee(""),a!==void 0&&a()}catch(Le){Ae(Le.message)}},Y=o.useCallback(async q=>{if(q==null||q===""){te(null);return}try{const ce=await i("/requirement/matrix",{id:q});te(ce.matrix)}catch(ce){X(ce.message)}},[]),ne=async q=>{if(Q!==null)try{await i("/requirement/save",{requirement:{...Q,links:Q.links??{},points:q}}),await Y(Q.id),X("功能点已保存"),ge(null)}catch(ce){X(ce.message)}},ee=()=>({title:"",description:"",priority:"normal",status:"draft",acceptance:[]}),be=async()=>{if(ie===null||Q===null)return;if(String(ie.title??"").trim()===""){X("功能点要有标题");return}const q=String(ie.acceptanceText??"").split(`
|
|
45
45
|
`).map(At=>At.trim()).filter(At=>At!==""),ce={id:ie.id??`p${(Q.points??[]).length+1}`,title:ie.title,description:ie.description??"",priority:ie.priority??"normal",status:ie.status??"draft",acceptance:q.map((At,$a)=>({id:`a${$a+1}`,text:At,done:!1}))},Le=(Q.points??[]).filter(At=>At.id!==ce.id);await ne([...Le,ce])},se=async q=>{Q!==null&&await u("删除这个功能点?挂在上面的卡会变成「未归功能点」。",{title:"删除功能点",danger:!0,okText:"删除"})&&await ne((Q.points??[]).filter(ce=>ce.id!==q))},de=async(q,ce)=>{try{await i("/task/update",{id:q,pointId:ce}),Q!==null&&await Y(Q.id)}catch(Le){X(Le.message)}},Q=h.find(q=>q.id===S)??null,ve=q=>e.tasks.filter(ce=>ce.requirementId===q),Ce=q=>e.statuses.find(ce=>ce.id===q)?.label??q;o.useEffect(()=>{te(null),X(null),Q!==null&&Y(Q.id)},[Q===null?"":Q.id,Y]);const Se=async()=>{if(_!==null){if(String(_.title??"").trim()===""){W("需求标题不能为空");return}P(!0);try{const q=await i("/requirement/save",{requirement:{id:_.id,title:_.title,description:_.description,project:_.project,owner:_.owner,domain:_.domain??"",priority:_.priority,status:_.status,links:{prd:_.prd,proto:_.proto,acceptance:_.acceptance}}});C(q.requirement.id),I(null),W(null),l(q.created===!0?"需求已创建":"需求已保存")}catch(q){W(q.message)}finally{P(!1)}}},Pe=async q=>{if(Q!==null)try{await i("/requirement/save",{requirement:{...Q,links:Q.links??{},status:q}}),l("需求状态已更新")}catch(ce){W(ce.message)}},dt=async q=>{if(await u("删除这条需求?它下面的卡会保留,只是不再挂需求。",{title:"删除需求",danger:!0,okText:"删除"}))try{await i("/requirement/delete",{id:q}),C(null)}catch(ce){W(ce.message)}},rt={pending:"待评审",approved:"评审通过",changes_requested:"评审打回"},[Je,We]=o.useState(!1),[tt,Ge]=o.useState(null),Te=async(q,ce)=>{if(Q!==null){We(!0),Ge(null);try{const Le=await i("/requirement/review",{id:Q.id,decision:q,note:ce??"",by:c(),operator:c()});Ge(`已${q==="approve"?"通过":q==="reject"?"打回":"提交"}评审:${rt[Le.review?.status]??""}`),typeof a=="function"&&a()}catch(Le){Ge(Le.message)}finally{We(!1)}}},It=async()=>{const q=await p("打回要给意见(会记在评审记录里):",{title:"评审打回",danger:!0,okText:"打回",placeholder:"写清楚哪里不满足,之后要据此改"});if(q!==null){if(String(q).trim()===""){Ge("打回必须写意见");return}Te("reject",String(q).trim().slice(0,600))}},[Be,yt]=o.useState(!1),[Rt,Ut]=o.useState(null),le=async()=>{if(Q!==null){yt(!0),Ut(null);try{const ce=(await i("/requirement/split",{id:Q.id})).created??[];Ut(ce.length===0?"没有可拆的功能点:要么都覆盖了,要么还没加功能点。":`已建 ${ce.length} 张卡:${ce.map(Le=>Le.code||Le.id).join("、")}`),typeof a=="function"&&a()}catch(q){Ut(q.message)}finally{yt(!1)}}},Ne=()=>({kind:"prd",title:"",url:Q?.links?.prd??"",version:"v1.0",content:"",status:"draft"}),me=async q=>{if(F(null),q===null){J(Ne());return}try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===q);J(Le===void 0?Ne():{...Le})}catch(ce){W(ce.message)}},Qa=async q=>{if(L!==null&&L.id===q){w(null);return}w({id:q,content:"",busy:!0});try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===q);w({id:q,content:String(Le?.content??""),busy:!1})}catch(ce){W(ce.message),w(null)}},Et=async()=>{if(T!==null){K(null);return}try{const ce=(await i("/requirement/get",{id:Q.id})).requirement??{},Le=ce.baseline??null,$a=[...ce.docs??[]].sort((Dn,Fo)=>(Number(Fo.at)||0)-(Number(Dn.at)||0)),ra=$a.find(Dn=>Dn.kind==="prd")??$a[0]??null;if(Le===null){K(null),W("这条需求还没有基线,先挑一份文档「设为基线」");return}if(ra===null){K(null),W("这条需求还没有文档可以对比");return}const Ma=sw(Le.content??"",ra.content??"");K({rows:Ma,summary:rw(Ma),version:Le.version,docTitle:ra.title})}catch(q){K(null),W(q instanceof Error?q.message:String(q))}},fa=async()=>{if(R!==null){O(null);return}try{const q=await i("/requirement/get",{id:Q.id}),ce={};for(const Le of q.requirement?.docs??[])ce[Le.id]=String(Le.content??"");O(ce)}catch(q){W(q.message)}},An=async()=>{if(H!==null){if(String(H.title??"").trim()===""){F("文档要有标题");return}we(!0);try{const q=await i("/requirement/doc",{id:Q.id,action:H.id===void 0?"add":"update",docId:H.id,doc:{kind:H.kind,title:H.title,url:H.url,version:H.version,content:H.content,status:H.status}});J(null),F(null)}catch(q){F(q.message)}finally{we(!1)}}},La=async()=>{ae(!0),ye(null);try{const q=await i("/requirement/folder",{id:Q.id});ye(q.ok?`云盘文件夹已就绪:${q.folder?.url??""}`:`创建失败:${q.error||q.folder?.error||"未知原因"}`)}catch(q){ye(q.message)}finally{ae(!1)}},Lt=async()=>{ae(!0),ye(null);try{const ce=(await i("/requirement/sync",{id:Q.id})).report??{},Le=Number(ce.failed??0);ye(`归档完成:共 ${ce.total??0} 件,成功 ${ce.written??0} 件${Le>0?`,失败 ${Le} 件(${ce.errors?.[0]??""})`:"。"}`)}catch(q){ye(q.message)}finally{ae(!1)}},Mt=async q=>{try{await i("/requirement/doc",{id:Q.id,action:"baseline",docId:q,by:"人"}),F("这一版已生效,之后新建的卡都会记下它。")}catch(ce){F(ce.message)}},jn=async()=>{try{await i("/requirement/doc",{id:Q.id,action:"clear-baseline"}),F("基线已取消,需求回到「评审中」。")}catch(q){F(q.message)}},Z=async q=>{if(await u("删除这份文档?",{title:"删除文档",danger:!0,okText:"删除"}))try{await i("/requirement/doc",{id:Q.id,action:"remove",docId:q})}catch(ce){F(ce.message)}},_e=async()=>{we(!0),F(null);try{const q=await i("/requirement/pull",{url:H?.url??""});if(q.ok===!0){const ce=String(q.text??"");J(Le=>({...Le,title:String(Le?.title??"").trim()===""?String(q.title??""):Le.title,content:ce})),F(`已拉取 ${ce.length} 字正文${q.title?`,标题取自文档「${q.title}」`:""}。`)}else F(q.error??"拉取失败")}catch(q){F(q.message)}finally{we(!1)}},xt=_===null?null:s("div",{className:"ds-panel ds-form-panel"},s("div",{className:"ds-panel-head"},_.id===void 0?"新建需求":`编辑 ${Q?.code??""}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求标题"),s(v,{className:"ds-input",value:_.title,onChange:q=>I({..._,title:q.target.value})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务项目"),s(g,{className:"ds-select",value:_.project,onChange:q=>I({..._,project:q.target.value})},s("option",{value:""},"不指定"),y.map(q=>s("option",{key:q.id,value:q.id},q.name)))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:_.status,onChange:q=>I({..._,status:q.target.value})},ef.map(q=>s("option",{key:q,value:q},Ar[q])))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务负责人"),s(v,{className:"ds-input",value:_.owner,onChange:q=>I({..._,owner:q.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务领域"),s(v,{className:"ds-input",placeholder:"支付 / 风控 / 会员(决定派给哪位领域专家)",value:_.domain??"",onChange:q=>I({..._,domain:q.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"PRD 链接"),s(v,{className:"ds-input",placeholder:"https://…(飞书云文档)",value:_.prd,onChange:q=>I({..._,prd:q.target.value})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"原型链接"),s(v,{className:"ds-input",value:_.proto,onChange:q=>I({..._,proto:q.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"验收标准链接"),s(v,{className:"ds-input",value:_.acceptance,onChange:q=>I({..._,acceptance:q.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求正文 / PRD 摘要"),s(d,{className:"ds-textarea",rows:10,style:{minHeight:180},placeholder:"支持 Markdown:`# 背景`、`- 列表`、`**重点**`、`` `代码` ``、`| 表格 |`;保存后在「需求明细」里按 Markdown 渲染,不再截断。",value:_.description,onChange:q=>I({..._,description:q.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{I(null),W(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void Se()},M?"保存中…":"保存需求")))),ht=Q===null?s("div",{className:"ds-empty"},"左边选一条需求,或先新建一条。"):(()=>{const q=ve(Q.id),ce=q.filter(A=>A.status==="done").length,Le=q.filter(A=>A.blocked!==void 0&&A.blocked!==null).length,At=q.filter(A=>A.type==="bug"&&A.status!=="done").length,$a=q.reduce((A,ue)=>{const Fe=ed(ue);return{done:A.done+Fe.done,total:A.total+Fe.total}},{done:0,total:0}),ra=q.reduce((A,ue)=>{const Fe=Jl(ue);return{pass:A.pass+Fe.pass,total:A.total+Fe.total}},{pass:0,total:0}),Ma=q.flatMap(A=>(A.artifacts??[]).map(ue=>({...ue,task:A}))),Dn=q.filter(A=>(A.gitlabMR?.iid??0)>0),Fo=ra.pass,On=Q.links??{},dn=Q.folder??{},Ln=(A,ue)=>ue===""?null:s("a",{className:"ds-link",href:ue,target:"_blank",rel:"noreferrer"},A);return s("div",{className:"ds-req-body"},s("div",{className:"ds-req-headline"},s("div",null,s("div",{className:"ds-req-code"},Q.code||Q.id),s("div",{className:"ds-req-name"},Q.title)),s("div",{className:"ds-req-actions"},s(g,{className:"ds-select",value:Q.status,onChange:A=>void Pe(A.target.value)},ef.map(A=>s("option",{key:A,value:A},Ar[A]))),s("button",{className:"ds-btn",onClick:()=>I({...Q,...On})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void dt(Q.id),"data-danger":"1"},"删除"))),(()=>{const A=Q.review??null,ue=A===null?"还没提交评审":rt[A.status]??A.status,Fe=A===null||A.status==="changes_requested",wt=A!==null&&A.status==="pending";return s("div",{className:"ds-panel","data-review":A===null?"none":A.status,style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求评审"),s(b,{className:"ds-chip","data-on":A?.status==="approved"?"1":"0",style:{marginLeft:8}},ue),A===null||A.at===0?null:s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(A.at)}${A.by===""?"":` · ${A.by}`}`)),s("div",{className:"ds-panel-body"},A!==null&&String(A.note??"")!==""?s("div",{className:"ds-hint",style:{marginBottom:8}},`评审意见:${A.note}`):null,s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},Fe?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("submit")},Je?"提交中…":"提交评审"):null,wt?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("approve")},"评审通过"):null,wt?s("button",{className:"ds-btn ds-btn-ghost",disabled:Je,onClick:It,"data-danger":"1"},"打回"):null,A?.status==="approved"?s("span",{className:"ds-hint"},"评审已通过,可以拆卡和派活了。"):null),tt===null?null:s("div",{className:"ds-hint",style:{marginTop:8}},tt)))})(),Ln("PRD",On.prd)||Ln("原型",On.proto)||Ln("验收标准",On.acceptance)?s("div",{className:"ds-req-links"},Ln("PRD 文档",On.prd),Ln("原型",On.proto),Ln("验收标准",On.acceptance)):s("div",{className:"ds-empty"},[s("span",null,"还没有登记 PRD / 原型 / 验收标准链接;PRD 与原型由产品在澄清后输出,可在这里补,或点右边「让产品补 PRD」。"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:async()=>{const A=await p("想请产品补什么?",{title:"申请产品补 PRD",defaultValue:"需要产品输出 PRD 与原型"});if(A!==null)try{await i("/requirement/request-prd",{id:Q.id,by:c(),note:A}),l("已申请产品补 PRD"),a()}catch(ue){l(ue.message??String(ue),"error")}}},"让产品补 PRD")]),Q.description?s("div",{className:"ds-panel ds-req-desc-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求明细"),Q.source==="business"?s("span",{className:"ds-hint","data-src":"business"},`业务提单 · ${Q.requester?.name||"未署名"}${Q.requester?.dept?"("+Q.requester.dept+")":""} · ${Ke(Q.requester?.at||Q.createdAt)}`):null,s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"支持 Markdown")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-req-desc ds-md"},f(Q.description)))):s("div",{className:"ds-req-desc ds-hint"},"还没有写需求描述:产品背景、要解决的问题、验收口径都建议写在这里,点右侧「编辑」补上。"),(()=>{const A=(Q.history??[]).slice().reverse();if(A.length===0||(Q.change??null)!==null)return null;const ue={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{className:"ds-panel ds-history-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"变更记录"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`最近 ${A.length} 条`)),s("div",{className:"ds-panel-body"},s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto"}},A.map((Fe,wt)=>s("div",{key:`${Fe.at}-${wt}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint",style:{flex:"0 0 auto",fontVariantNumeric:"tabular-nums"}},Ke(Fe.at)),s("span",{style:{flex:"0 0 auto"}},Fe.by===""?"人":Fe.by),s("span",{className:"ds-chip",style:{flex:"0 0 auto"}},ue[Fe.field]??Fe.field),s("span",{className:"ds-hint",style:{minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},`${Fe.from===""?"(空)":Fe.from} → ${Fe.to===""?"(空)":Fe.to}`))))))})(),(()=>{const A=Q.pool??{reach:0,impact:0,confidence:0,effort:0,score:0},ue=async(wt,ma,et)=>{const De=await p(ma,{title:wt,defaultValue:String(et??0)});if(De===null)return null;const Ye=Number(String(De).trim());return Number.isFinite(Ye)&&Ye>=0?Ye:null},Fe=async()=>{const wt=await ue("影响面 Reach","影响多少用户或多少笔交易?估算依据写进正文",A.reach);if(wt===null)return;const ma=await ue("影响度 Impact","3 显著 / 2 高 / 1 中 / 0.5 低",A.impact);if(ma===null)return;const et=await ue("置信度 Confidence","100 有数据 / 80 有访谈 / 50 只有猜测(百分数)",A.confidence);if(et===null)return;const De=await ue("成本 Effort","人日(研发+测试+联调)",A.effort);if(De!==null)try{await i("/requirement/pool",{id:Q.id,pool:{reach:wt,impact:ma,confidence:et,effort:De}}),l("需求池评分已更新"),await a()}catch(Ye){l(Ye instanceof Error?Ye.message:String(Ye),"error")}};return s("div",{className:"ds-panel ds-pool-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求池评分(RICE)"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},A.score>0?`得分 ${A.score}`:"还没打分:不打分就没法和别的需求比优先级"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void Fe()},A.score>0?"重新打分":"打分")),A.score>0?s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},s("span",{className:"ds-chip"},`影响面 ${A.reach}`),s("span",{className:"ds-chip"},`影响度 ${A.impact}`),s("span",{className:"ds-chip"},`置信度 ${A.confidence}%`),s("span",{className:"ds-chip"},`成本 ${A.effort} 人日`),s("span",{className:"ds-chip","data-on":"1"},`得分 ${A.score}`))):null)})(),(()=>{const A=Q.questions??[],ue=A.filter(De=>De.status==="open").length,Fe=async(De,Ye)=>{try{await i("/requirement/questions",{id:Q.id,...De}),Ye&&l(Ye),await a()}catch(Mn){l(Mn instanceof Error?Mn.message:String(Mn),"error")}},wt=async()=>{const De=await p("还没确认清楚的问题是什么?",{title:"加一个待确认问题",placeholder:"例如:退款失败时是否允许人工补发?"});De===null||De.trim()===""||await Fe({action:"add",text:De},"已加入待确认清单")},ma=async De=>{const Ye=await p(De.text,{title:"回答这个问题",defaultValue:De.answer??"",placeholder:"写清结论与依据"});Ye===null||Ye.trim()===""||await Fe({action:"answer",questionId:De.id,answer:Ye},"已回答")},et=async De=>{await u("删掉这个问题?",{title:"删除待确认项",danger:!0,okText:"删除"})===!0&&await Fe({action:"delete",questionId:De.id},"已删除")};return s("div",{className:"ds-panel ds-question-panel","data-open":ue>0?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"待确认清单"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},ue>0?`${ue} 个没回答,回答完才能拆卡`:A.length===0?"澄清阶段把不确定的先记在这里":"全部已回答"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void wt()},"+ 加问题")),s("div",{className:"ds-panel-body"},A.length===0?s("div",{className:"ds-empty"},"还没有待确认的问题。产品澄清时把「需要业务拍板」的点写在这里,避免猜业务规则。"):s("div",{style:{display:"flex",flexDirection:"column",gap:8}},A.map(De=>s("div",{key:De.id,className:"ds-question-row","data-status":De.status,style:{display:"flex",gap:10,alignItems:"flex-start",padding:"8px 10px",border:"1px solid var(--border)",borderRadius:8,background:"var(--card)"}},s("span",{className:"ds-chip","data-on":De.status==="answered"?"1":"0"},De.status==="answered"?"已回答":"待回答"),s("div",{style:{flex:"1 1 auto",minWidth:0}},s("div",{style:{fontSize:13}},De.text),De.answer===""||De.answer===void 0?null:s("div",{className:"ds-hint",style:{marginTop:4}},`答:${De.answer}`)),s("div",{style:{display:"flex",gap:6,flex:"0 0 auto"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void ma(De)},De.status==="answered"?"改答案":"回答"),s("button",{className:"ds-btn ds-btn-ghost","data-danger":"1",onClick:()=>void et(De)},"删除")))))))})(),(()=>{const A=Q.change??null;if(A===null)return null;const ue=A.affected??[],Fe=A.affectedCases??[],wt=new Map(q.map(et=>[et.id,et])),ma=et=>e.statuses.find(De=>De.id===et)?.label??et;return s("div",{className:"ds-panel ds-change-panel","data-invalidated":A.invalidated?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,A.invalidated?"变更影响(签字已作废)":"变更影响"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(A.at)}${A.by===""?"":` · ${A.by}`}`)),s("div",{className:"ds-panel-body"},function(){const et=(Q.history??[]).slice().reverse();if(et.length===0)return null;const De={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:220,overflowY:"auto",marginBottom:8,paddingBottom:8,borderBottom:"1px dashed var(--border)"}},s("div",{className:"ds-hint",style:{fontWeight:600}},`变更记录 · 最近 ${et.length} 条`),et.map((Ye,Mn)=>s("div",{key:`${Ye.at}-${Mn}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint"},Ke(Ye.at)),s("span",null,Ye.by===""?"人":Ye.by),s("span",{className:"ds-chip"},De[Ye.field]??Ye.field),s("span",{className:"ds-hint"},`${Ye.from===""?"(空)":Ye.from} → ${Ye.to===""?"(空)":Ye.to}`))))}(),s("div",{className:"ds-hint"},`改了:${(A.parts??[]).join("、")}。${ue.length===0?"当时没有已产出的卡受影响,不用返工。":`${ue.length} 张卡已经产出过东西${Fe.length===0?"":`、${Fe.length} 条用例要重跑`},要按新版复核:`}`),ue.length===0?null:s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8,marginTop:8}},ue.map(et=>s("span",{key:et.id,className:"ds-chip","data-pt":"open",style:{cursor:"pointer"},title:et.title,onClick:()=>{const De=wt.get(et.id);De!==void 0&&typeof t=="function"&&t(De)}},`${et.code||et.id} ${ma(et.status)}`))),A.invalidated?s("div",{className:"ds-hint",style:{marginTop:8}},"验收签字已清空:这些卡是按旧版做的,复核完重新签一次。"):null))})(),(()=>{const A=Q.deploys??[];return A.length===0?null:s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"上线足迹"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"来自版本的部署记录,只留痕不触发")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},A.map(ue=>s("span",{key:ue.id,className:"ds-chip","data-env":ue.kind,title:`${ue.releaseName??""} · ${Ke(ue.at)} · ${ue.by}`},`${ue.name} · ${Xa(ue.at)}`)))))})(),s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"依赖与排序")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row"},s("span",{className:"ds-hint"},"排序号"),s(v,{className:"ds-input",style:{width:90},value:Ve,onChange:A=>Ya(A.target.value)}),s("span",{className:"ds-hint"},"被依赖的需求没交付,这条需求的准出会被拦")),s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:10}},h.filter(A=>A.id!==Q.id).map(A=>s("label",{key:A.id,className:"ds-check-row"},s(k,{checked:Rn.includes(A.id),onChange:ue=>B(Fe=>ue.target.checked?[...Fe,A.id]:Fe.filter(wt=>wt!==A.id))}),s("span",{className:"ds-check-text"},`${A.code||A.id} ${A.title}(${Ar[A.status]??A.status})`))),h.length<=1?s("span",{className:"ds-hint"},"只有这一条需求,暂时没有可依赖的对象。"):null),s("div",{className:"ds-row"},s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void Ea()},"保存排序与依赖"),he===null?null:s("span",{className:"ds-hint"},he)))),(()=>{const A=Q.progress??null,ue=A?.gate??{ready:!1,blockers:["还没有加载出准出状态,稍后自动刷新"],warnings:[]},Fe=ue.stats??{},wt=Q.signoff??null,ma=A?.progress??(q.length===0?0:Math.round(ce/q.length*100)),et=Fe.coveredPoints??0,De=Fe.points??(Q.points??[]).length,Ye=Fe.pass??Fo,Mn=Fe.cases??ra.total;return s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求准出与验收签字"),s(b,{className:"ds-chip","data-pt":ue.ready?"covered":"open",style:{marginLeft:"auto"}},ue.ready?"可以交付":`还差 ${ue.blockers.length} 项`)),s("div",{className:"ds-panel-body"},s("div",{className:"ds-check-progress"},s("div",{className:"ds-check-bar","data-full":ma===100?"1":"0"},s("span",{style:{width:`${ma}%`}})),s("div",{className:"ds-hint"},`${ma}% · ${ce}/${q.length} 张卡完成 · 功能点覆盖 ${et}/${De} · 用例通过 ${Ye}/${Mn}`)),(ue.blockers??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"block"},`⛔ ${zn}`)),(ue.warnings??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"warn"},`⚠ ${zn}`)),ue.ready?s("div",{className:"ds-hint"},"门禁已通过:签完字这条需求就算交付。"):null,wt===null?s("div",{className:"ds-gate-sign"},s(v,{className:"ds-input",style:{flex:1,minWidth:220},placeholder:"验收结论:交付了什么、遗留什么",value:V,onChange:zn=>Ee(zn.target.value)}),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void hs("sign")},"需求验收签字")):s("div",{className:"ds-gate-sign"},s("div",{className:"ds-hint",style:{flex:1}},`✍ ${wt.by} 于 ${Ke(wt.at)} 签字:${wt.note}`),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void hs("clear")},"取消签字")),ke===null?null:s("div",{className:"ds-hint"},ke)))})(),s("div",{className:"ds-sec-head",style:{marginTop:16}},"交付物归档(飞书云盘)",s("div",{className:"ds-req-actions"},dn.url?s("a",{className:"ds-link",href:dn.url,target:"_blank",rel:"noreferrer"},"打开文件夹"):null,s("button",{className:"ds-btn ds-btn-ghost",disabled:He,onClick:()=>void La()},dn.url?"校验文件夹":"创建文件夹"),s("button",{className:"ds-btn",disabled:He||Ma.length===0,onClick:()=>void Lt()},He?"归档中…":`归档 ${Ma.length} 件交付物`))),s("div",{className:"ds-hint"},dn.url?`文件夹:${dn.name||dn.token} 已归档 ${Ma.filter(A=>A.doc?.url).length}/${Ma.length} 件交付物`:dn.error?`上次创建失败:${dn.error}`:"还没有云盘文件夹;建好后,这条需求下每件交付物都会写成文件夹里的一份 Markdown 文档。"),fe?s("div",{className:"ds-hint"},fe):null,s("div",{className:"ds-sec-head",style:{marginTop:16}},"文档与基线",s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(null)},"+ 添加文档")),Q.baseline?s("div",{className:"ds-base-box"},s("div",{className:"ds-base-main"},s("b",null,`当前基线 ${Q.baseline.version}`),s("span",null,`${Q.baseline.title} · 生效于 ${Ke(Q.baseline.at)} · ${Q.baseline.by||"人"} · 正文 ${Q.baseline.contentLength??0} 字`)),s("button",{className:"ds-btn ds-btn-ghost",title:"把当前文档和这份基线逐行比一遍(只读)",onClick:()=>void Et()},T===null?"对比基线":"收起对比"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void jn()},"取消基线")):s("div",{className:"ds-base-box","data-empty":"1"},s("div",{className:"ds-base-main"},s("b",null,"还没有基线"),s("span",null,"PRD 评审通过后点「设为基线」,之后建的卡都会记下用的是哪一版。"))),Q.baseline!==null&&T!==null?s("div",{className:"ds-diff"},s("div",{className:"ds-diff-head"},s("b",null,`基线 ${T.version} → 当前:${T.docTitle}`),s("span",{className:"ds-hint"},T.summary.added+T.summary.removed===0?"正文没有变化":`+${T.summary.added} 行 / −${T.summary.removed} 行`)),s("div",{className:"ds-diff-body"},T.rows.slice(0,400).map((A,ue)=>s("div",{key:ue,className:"ds-diff-line","data-kind":A.kind},`${A.kind==="add"?"+":A.kind==="del"?"−":" "} ${A.text}`)),T.rows.length>400?s("div",{className:"ds-hint"},`(还有 ${T.rows.length-400} 行未显示)`):null)):null,(Q.docs??[]).length===0?s("div",{className:"ds-hint",style:{marginTop:8}},"还没有登记文档。可以先加 PRD 链接,把正文粘进来,评审通过后设为基线。"):s("div",null,s("div",{className:"ds-row",style:{marginTop:8,gap:8,alignItems:"center"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void fa()},R===null?"全部展开":"全部收起"),s("span",{className:"ds-hint"},`共 ${(Q.docs??[]).length} 篇,展开后可直接读 Markdown 正文`)),s("div",{className:"ds-doc-list"},(Q.docs??[]).map(A=>s("div",{key:A.id,className:"ds-doc-wrap"},s("div",{className:"ds-doc-row"},s(b,{className:"ds-chip","data-doc":A.kind},tf[A.kind]??A.kind),s("div",{className:"ds-doc-main"},s("div",{className:"ds-doc-title"},A.url===""?A.title:s("a",{className:"ds-link",href:A.url,target:"_blank",rel:"noreferrer"},A.title),s("span",{className:"ds-chip-code"},A.version),s(b,{className:"ds-chip","data-docstatus":A.status},cx[A.status]??A.status)),s("div",{className:"ds-doc-meta"},`正文 ${A.contentLength??0} 字 · ${A.by||"—"} · ${A.at>0?Ke(A.at):"—"}`)),s("div",{className:"ds-listrow-actions"},A.status==="baseline"?null:s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Mt(A.id)},"设为基线"),s("button",{className:"ds-btn ds-btn-ghost",title:"直接读正文(Markdown 渲染)",onClick:()=>void Qa(A.id)},L!==null&&L.id===A.id?"收起":"阅读"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(A.id)},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Z(A.id),"data-danger":"1"},"删除"))),L!==null&&L.id===A.id?s("div",{className:"ds-md-preview ds-reader"},L.busy?s("span",{className:"ds-hint"},"读取中…"):L.content.trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(L.content)):R!==null&&R[A.id]!==void 0?s("div",{className:"ds-md-preview ds-reader"},R[A.id].trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(R[A.id])):null)))),U===null?null:s("div",{className:"ds-hint",style:{marginTop:6}},U),H===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},H.id===void 0?"添加文档":`编辑 ${H.title}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"类型"),s(g,{className:"ds-select",value:H.kind,onChange:A=>J({...H,kind:A.target.value})},dx.map(A=>s("option",{key:A,value:A},tf[A])))),s("div",{className:"ds-field",style:{flex:"1 1 220px"}},s("div",{className:"ds-label"},"标题"),s(v,{className:"ds-input",value:H.title,placeholder:"例如 会员积分抵扣 PRD",onChange:A=>J({...H,title:A.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 110px"}},s("div",{className:"ds-label"},"版本"),s(v,{className:"ds-input",value:H.version,placeholder:"v1.0",onChange:A=>J({...H,version:A.target.value})}))),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"1 1 auto"}},s("div",{className:"ds-label"},"飞书文档链接"),s(v,{className:"ds-input",value:H.url,placeholder:"https://<租户>.feishu.cn/wiki/…",onChange:A=>J({...H,url:A.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 auto",justifyContent:"flex-end"}},s("div",{className:"ds-label"}," "),s("button",{className:"ds-btn",disabled:G,onClick:()=>void _e()},G?"拉取中…":"从链接拉正文"))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"文档正文(拉不到就自己粘,研发和测试都读这一份)"),s(d,{className:"ds-textarea",rows:8,value:H.content,onChange:A=>J({...H,content:A.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{J(null),F(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:G,onClick:()=>void An()},"保存文档")))),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"需求变更影响面"),s("div",{className:"ds-pane-desc"},"需求一改,下面这些卡和用例就要重看。点一下现算,不改任何数据。")),s("div",{className:"ds-row",style:{gap:8,alignItems:"center",marginBottom:6}},s("button",{className:"ds-btn",disabled:Gt||pt===null,onClick:()=>void Ct()},Gt?"计算中…":"算一下影响面"),Ue===null?s("span",{className:"ds-hint"},"还没算过"):s("span",{className:"ds-hint"},`卡 ${Ue.summary.cards} · 用例 ${Ue.summary.cases}(能自动跑 ${Ue.summary.runnable})· 失败 ${Ue.summary.failing}`)),Ue===null||Ue.cards.length===0?null:s("div",{className:"ds-sec-body",style:{maxHeight:160,overflow:"auto"}},Ue.cards.slice(0,12).map(A=>s("div",{key:A.id,className:"ds-art-row"},s("span",{className:"ds-art-title"},(A.code===""?"":A.code+" ")+A.title),s("span",{className:"ds-hint"},A.status))),Ue.cards.length>12?s("div",{className:"ds-hint"},`还有 ${Ue.cards.length-12} 张`):null),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"功能点与可追溯矩阵"),s("div",{className:"ds-pane-desc"},"功能点是需求的骨架:一条功能点对若干张卡、若干条用例。卡和用例上都挂 pointId,这里就能看出哪条没人做、哪条没被验证。")),z===null?s("div",{className:"ds-hint",style:{padding:"6px 0"}},"还没有功能点。点「+ 加功能点」把需求拆成可交付、可验证的条目。"):s("div",{className:"ds-matrix"},s("div",{className:"ds-matrix-sum"},`功能点 ${z.summary.points} · 已覆盖 ${z.summary.coveredPoints}${z.summary.pointsWithoutCases>0?`(${z.summary.pointsWithoutCases} 条还没有用例,覆盖率 ${z.summary.coverageRate}%)`:""} · 卡 ${z.summary.cards}(未归点 ${z.summary.looseCards})· 用例 ${z.summary.cases}(通过 ${z.summary.pass}${z.summary.fail>0?`,失败 ${z.summary.fail}`:""})· 通过率 ${z.summary.passRate}%`),(z.points??[]).map(A=>s("div",{key:A.id,className:"ds-pt-row","data-covered":A.covered?"1":"0"},s("div",{className:"ds-pt-main"},s("div",{className:"ds-pt-title"},s("span",{className:"ds-chip-code"},A.id),s("b",null,A.title),s(b,{className:"ds-chip","data-pt":A.covered?"covered":"open"},A.covered?"已覆盖":"未覆盖")),String(A.description??"")===""?null:s("div",{className:"ds-req-desc ds-md"},f(A.description)),s("div",{className:"ds-pt-meta"},`卡 ${A.cards.length}${A.cards.length===0?"(还没人做)":":"+A.cards.map(ue=>ue.code||ue.id).join("、")}`,` · 用例 ${A.cases.total}(通过 ${A.cases.pass}${A.cases.fail>0?`,失败 ${A.cases.fail}`:""}${A.cases.pending>0?`,没跑 ${A.cases.pending}`:""})`,A.acceptance.length===0?"":` · 验收条件 ${A.acceptance.filter(ue=>ue.covered).length}/${A.acceptance.length}`)),s("div",{className:"ds-listrow-actions"},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>ge({...A,acceptanceText:(A.acceptance??[]).map(ue=>ue.text).join(`
|
|
46
|
-
`)})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void se(A.id),"data-danger":"1"},"删除")))),(z.loose??[]).length===0?null:s("div",{className:"ds-pt-loose"},s("div",{className:"ds-hint",style:{marginBottom:6}},`还有 ${z.loose.length} 张卡没归到功能点,选一个挂上去,矩阵才算完整:`),z.loose.map(A=>s("div",{key:A.id,className:"ds-pt-loose-row"},s("span",{className:"ds-chip-code"},A.code||A.id),s("span",{className:"ds-pt-loose-title"},A.title),s(g,{className:"ds-select",style:{width:220},value:"",onChange:ue=>{ue.target.value!==""&&de(A.id,ue.target.value)}},s("option",{value:""},"挂到…"),(z.points??[]).map(ue=>s("option",{key:ue.id,value:ue.id},`${ue.id} ${ue.title}`))))))),s("div",{className:"ds-row",style:{gap:8,marginTop:10,alignItems:"center"}},s("button",{className:"ds-btn",onClick:()=>ge(ee())},"+ 加功能点"),s("button",{className:"ds-btn ds-btn-primary",disabled:Be,onClick:()=>void le()},Be?"拆卡中…":"按未覆盖的功能点拆卡"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>{Q!==null&&Y(Q.id)}},"刷新矩阵"),Rt===null?null:s("span",{className:"ds-hint"},Rt),xe===null?null:s("span",{className:"ds-hint"},xe)),ie===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},ie.id===void 0?"新增功能点":`编辑功能点 ${ie.id}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"功能点标题"),s(v,{className:"ds-input",value:ie.title,placeholder:"例如 结算页展示可用积分",onChange:A=>ge({...ie,title:A.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"说明"),s(d,{className:"ds-textarea",rows:3,value:ie.description??"",onChange:A=>ge({...ie,description:A.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"验收条件(一行一条,做成什么样算完成)"),s(x,{className:"ds-textarea",rows:4,value:ie.acceptanceText??"",onChange:A=>ge({...ie,acceptanceText:A.target.value})})),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"优先级"),s(g,{className:"ds-select",value:ie.priority,onChange:A=>ge({...ie,priority:A.target.value})},s("option",{value:"urgent"},"紧急"),s("option",{value:"normal"},"普通"),s("option",{value:"relaxed"},"可缓"))),s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:ie.status,onChange:A=>ge({...ie,status:A.target.value})},s("option",{value:"draft"},"草稿"),s("option",{value:"ready"},"就绪"),s("option",{value:"done"},"已完成")))),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>ge(null)},"取消"),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void be()},"保存功能点")))),s("div",{className:"ds-req-stats"},s("div",{className:"ds-stat-box"},s("b",null,`${ce}/${q.length}`),s("span",null,"卡已完成")),s("div",{className:"ds-stat-box","data-warn":Le>0?"1":"0"},s("b",null,String(Le)),s("span",null,"阻塞")),s("div",{className:"ds-stat-box","data-warn":At>0?"1":"0"},s("b",null,String(At)),s("span",null,"未结缺陷")),s("div",{className:"ds-stat-box"},s("b",null,`${$a.done}/${$a.total}`),s("span",null,"验收清单")),s("div",{className:"ds-stat-box"},s("b",null,`${ra.pass}/${ra.total}`),s("span",null,"用例通过")),s("div",{className:"ds-stat-box"},s("b",null,String(Ma.length)),s("span",null,"交付物")),s("div",{className:"ds-stat-box"},s("b",null,String(Dn.length)),s("span",null,"MR"))),s("div",{className:"ds-sec-head",style:{marginTop:14}},`这张需求下的 ${q.length} 张卡`),q.length===0?s("div",{className:"ds-empty"},"还没有挂上来的卡。建卡时选择这条需求,编号会自动分配。"):s("div",{className:"ds-req-tasks"},q.map(A=>s("div",{key:A.id,className:"ds-listrow ds-req-task",onClick:()=>t(A.id)},s("div",{className:"ds-listrow-main"},s("div",{className:"ds-req-task-title"},A.code?s(b,{className:"ds-chip ds-chip-code"},A.code):null,s("span",null,A.title)),s("div",{className:"ds-hint"},`${Ce(A.status)}${(()=>{const ue=A.expert===""?"":(e.config.experts??[]).find(Fe=>Fe.id===A.expert)?.name??A.expert;return ue===""?"":` · ${ue}`})()}${A.dueAt>0?` · 截止 ${Xa(A.dueAt)}`:""}`)),s("div",{className:"ds-listrow-actions"},A.blocked!==void 0&&A.blocked!==null?s(b,{className:"ds-chip ds-chip-blocked"},"⛔"):null,A.type==="bug"?s(b,{className:"ds-chip ds-chip-bug","data-sev":A.severity??"p2"},"BUG"):null)))),Dn.length>0?s("div",null,s("div",{className:"ds-sec-head",style:{marginTop:14}},"关联 MR"),Dn.map(A=>s("div",{key:A.id,className:"ds-hint"},`${A.code||A.id} · !${A.gitlabMR.iid} ${A.gitlabMR.title??""}(${A.gitlabMR.state||"open"})`))):null)})();return s("div",{className:"ds-body"},D!==null?s("div",{className:"ds-err",style:{marginBottom:10}},D):null,xt,s("div",{className:"ds-req-grid"},s("div",{className:"ds-intake-launch",style:{display:"flex",gap:8,padding:"0 0 8px"}},s("span",{className:"ds-hint"},"新建需求请用右上角「+ 新建」")),s("div",{className:"ds-req-list"},s("div",{className:"ds-req-list-head"},s("span",null,`需求 ${h.length}`),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},title:"按来源筛选:全部 / 业务提单 / 内部",onClick:()=>{const q=window.localStorage.getItem("dsh-studio:reqsrc")??"all";window.localStorage.setItem("dsh-studio:reqsrc",q==="all"?"business":q==="business"?"internal":"all"),window.dispatchEvent(new Event("resize"))}},(window.localStorage.getItem("dsh-studio:reqsrc")??"all")==="all"?"来源:全部":window.localStorage.getItem("dsh-studio:reqsrc")==="business"?"来源:业务提单":"来源:内部")),h.length===0?s("div",{className:"ds-empty"},"还没有需求。先建一条——后面所有的卡、用例、缺陷、MR 都会挂在它下面。"):((window.localStorage.getItem("dsh-studio:reqsort")??"score")==="score"?h.slice().sort((q,ce)=>(ce.pool?.score??0)-(q.pool?.score??0)):h).map(q=>{const ce=window.localStorage.getItem("dsh-studio:reqsrc")??"all";if(ce!=="all"&&(q.source==="business"?"business":"internal")!==ce)return null;const Le=ve(q.id),At=Le.filter(Ma=>Ma.status==="done").length,$a=q.progress?.progress??(Le.length===0?0:Math.round(At/Le.length*100)),ra=q.progress?.gate??null;return s("div",{key:q.id,className:"ds-req-item","data-on":q.id===S?"1":"0",onClick:()=>C(q.id)},s("div",{className:"ds-req-item-title"},q.title),s("div",{className:"ds-req-item-meta"},s(b,{className:"ds-chip ds-chip-code"},q.code||"未编号"),q.source==="business"?s(b,{className:"ds-chip","data-src":"business",title:[q.requester?.name,q.requester?.dept].filter(Boolean).join(" · ")||"业务提单"},"业务提单"):null,s(b,{className:"ds-chip","data-req":q.status},Ar[q.status]??q.status),s("span",{className:"ds-hint"},`${At}/${Le.length} 卡`),ra===null?null:s("span",{className:"ds-hint",style:{color:ra.ready?"#0f9d58":"#b26a00"}},ra.ready?"准出通过":`还差 ${ra.blockers.length} 项`)),s("div",{className:"ds-check-bar","data-full":$a===100?"1":"0",style:{marginTop:2}},s("span",{style:{width:`${$a}%`}})))})),s("div",{className:"ds-req-detail"},ht)))}function iw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,toast:s,confirmDialog:o,promptDialog:i,tipOf:l,TextField:c,ChipField:u,CheckField:p,SelectField:f,MdField:v}=vt(),[x,b]=n.useState([]),[k,g]=n.useState(null),[d,h]=n.useState(null),[y,S]=n.useState(null),[C,E]=n.useState(""),[_,I]=n.useState(null),[D,W]=n.useState(""),[M,P]=n.useState(!1),[H,J]=n.useState(""),[U,F]=n.useState(""),[L,w]=n.useState(""),[T,K]=n.useState("10"),[R,O]=n.useState(""),[G,we]=n.useState(""),fe=n.useCallback(async V=>{const ke=(await r("/releases")).releases??[];b(ke);const Ae=V??(ke.some(Ya=>Ya.id===k)?k:ke[0]?.id??null);if(g(Ae),Ae===null){h(null);return}const Ve=await r("/release/get",{id:Ae});h(Ve),S({id:Ve.release.id,version:Ve.release.version,name:Ve.release.name,project:Ve.release.project,owner:Ve.release.owner,status:Ve.release.status,plannedAt:Ve.release.plannedAt,tagName:Ve.release.tagName??"",commitSha:Ve.release.commitSha??""}),E(Ve.release.notes??"")},[k]);n.useEffect(()=>{let V=!0;return fe().then(()=>{V&&I(null)}).catch(Ee=>{V&&I(Ee instanceof Error?Ee.message:String(Ee))}),()=>{V=!1}},[e.revision,fe]);async function ye(V){g(V),P(!0);try{const Ee=await r("/release/get",{id:V});h(Ee),S({id:Ee.release.id,version:Ee.release.version,name:Ee.release.name,project:Ee.release.project,owner:Ee.release.owner,status:Ee.release.status,plannedAt:Ee.release.plannedAt,tagName:Ee.release.tagName??"",commitSha:Ee.release.commitSha??""}),E(Ee.release.notes??""),W("")}catch(Ee){I(Ee instanceof Error?Ee.message:String(Ee))}finally{P(!1)}}function He(){g(null),h(null),S({id:"",version:"",name:"",project:"",owner:"",status:"draft",plannedAt:0,tagName:"",commitSha:""}),E(""),W("")}async function ae(V,Ee,ke){P(!0),I(null);try{const Ae=await r(V,Ee);W(ke??"已保存"),s(ke??"已保存"),s(ke??"已保存"),await fe(Ae.release?.id??k)}catch(Ae){const Ve=Ae instanceof Error?Ae.message:String(Ae);I(Ve),s(Ve,"error")}finally{P(!1)}}if(_!==null&&x.length===0)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},_));const z=d?.gate,te=d?.release,xe=d?.tasks??[],X=e.tasks.filter(V=>V.releaseId===""||V.releaseId===void 0),ie=a("div",{className:"ds-req-list"},a("div",{className:"ds-row",style:{justifyContent:"space-between",marginBottom:6}},a("span",{className:"ds-hint"},`${x.length} 个版本`),a("button",{className:"ds-btn",onClick:He},"+ 新建版本")),x.length===0?a("div",{className:"ds-empty"},"还没有版本。先建一个版本,再把卡挂上来。"):x.map(V=>a("div",{key:V.id,className:"ds-req-item","data-on":k===V.id?"1":"0",onClick:()=>void ye(V.id)},a("div",{className:"ds-req-item-title"},`${V.version||V.id} ${V.name}`.trim()),a("div",{className:"ds-req-item-meta"},a(u,{className:"ds-chip","data-kind":V.status},af[V.status]??V.status),a(u,{className:"ds-chip"},`${V.gate.stats.done}/${V.gate.stats.tasks} 卡`),V.gate.blockers.length===0?a(u,{className:"ds-chip",style:{color:"#0f9d58",borderColor:"#0f9d5855"}},"门禁通过"):a(u,{className:"ds-chip ds-chip-blocked"},`${V.gate.blockers.length} 项拦着`))))),ge=y===null?a("div",{className:"ds-hint"},"左边选一个版本,或者新建一个。"):a("div",{className:"ds-req-detail"},(te?.requirements??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginBottom:8}},a("span",{className:"ds-hint"},`这个版本涉及 ${te.requirements.length} 条需求:`),te.requirements.map(V=>a("span",{key:V.id,className:"ds-chip",title:V.title},`${V.code||V.id} ${V.title}(${Ar[V.derived]??V.derived})`))),a("div",{className:"ds-req-headline"},a("div",null,a("div",{className:"ds-req-name"},`${y.version||"未编号"} ${y.name}`.trim()),a("div",{className:"ds-hint"},te==null?"新版本:先填版本号与名字,保存后可以把卡挂进来":`创建于 ${Xa(te.createdAt)}${te.releasedAt>0?` · 发布于 ${Xa(te.releasedAt)}`:""}${te.approveBy!==""?` · 审批人 ${te.approveBy}`:""}`)),a("div",{className:"ds-req-actions"},a(f,{className:"ds-select",style:{width:108},value:y.status,onChange:V=>S({...y,status:V.target.value})},ux.map(V=>a("option",{key:V,value:V},af[V]))))),a("div",{className:"ds-panel",style:{marginTop:4}},a("div",{className:"ds-panel-head"},"版本信息"),a("div",{className:"ds-panel-body ds-grid2"},a("label",{className:"ds-field"},a("span",{},"版本号"),a(c,{className:"ds-input",value:y.version,placeholder:"v1.3.0",disabled:te?.status==="released",onChange:V=>S({...y,version:V.target.value})})),a("label",{className:"ds-field"},a("span",{},"版本名"),a(c,{className:"ds-input",value:y.name,placeholder:"积分抵扣首个版本",onChange:V=>S({...y,name:V.target.value})})),a("label",{className:"ds-field"},a("span",{},"业务项目"),a(f,{className:"ds-select",value:y.project,onChange:V=>S({...y,project:V.target.value})},a("option",{value:""},"不属于任何项目"),(e.config.projects??[]).map(V=>a("option",{key:V.id,value:V.id},V.name)))),a("label",{className:"ds-field"},a("span",{},"发布负责人"),a(c,{className:"ds-input",value:y.owner,onChange:V=>S({...y,owner:V.target.value})})),a("label",{className:"ds-field"},a("span",{},"计划发布时间"),a(c,{className:"ds-input",type:"date",value:nf(y.plannedAt),onChange:V=>S({...y,plannedAt:jr(V.target.value)})})),a("label",{className:"ds-field"},a("span",{},"发布标签(tag)"),a(c,{className:"ds-input",value:y.tagName??"",placeholder:"v1.0.0",onChange:V=>S({...y,tagName:V.target.value})})),a("label",{className:"ds-field"},a("span",{},"对应提交(sha,选填)"),a(c,{className:"ds-input",value:y.commitSha??"",placeholder:"a1b2c3d",onChange:V=>S({...y,commitSha:V.target.value})})),a("div",{className:"ds-row",style:{alignItems:"flex-end",justifyContent:"flex-end",gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void ae("/release/save",{release:y},"版本已保存")},"保存"),y.id===""?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:M,style:{color:"var(--destructive)"},onClick:async()=>{await o("删除这个版本?卡会保留,只是脱离版本。",{title:"删除版本",danger:!0,okText:"删除"})&&ae("/release/delete",{id:y.id},"版本已删除")}},"删除")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布门禁",a("span",{className:"ds-hint"},"卡全做完、没阻塞、缺陷关闭、用例没有失败,才算能发")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-req-stats"},a("div",{className:"ds-stat-box"},a("b",{},String(z?.stats.done??0)),a("span",{},"已完成卡")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(z?.stats.open??0)>0?"1":"0"},String(z?.stats.open??0)),a("span",{},"未完成")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(z?.stats.blocked??0)>0?"1":"0"},String(z?.stats.blocked??0)),a("span",{},"阻塞")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(z?.stats.bugs??0)>0?"1":"0"},String(z?.stats.bugs??0)),a("span",{},"未结缺陷")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(z?.stats.failing??0)>0?"1":"0"},String(z?.stats.failing??0)),a("span",{},"失败用例")),a("div",{className:"ds-stat-box"},a("b",{},String(z?.stats.artifacts??0)),a("span",{},"交付物"))),(z?.blockers??[]).map((V,Ee)=>a("div",{key:`b${Ee}`,className:"ds-error"},`⛔ ${V}`)),(z?.warnings??[]).map((V,Ee)=>a("div",{key:`w${Ee}`,className:"ds-hint"},`⚠ ${V}`)),a("div",{className:"ds-action-bar"},a("div",{className:"ds-action-bar-left"},a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/notes",{id:y.id},"发布说明已生成")},"生成发布说明"),a("button",{className:"ds-btn",disabled:M||!z?.ready,onClick:()=>void ae("/release/approve",{id:y.id,by:"人"},"已审批,可以发布了")},"审批通过")),a("div",{className:"ds-action-bar-right"},a("button",{className:"ds-btn ds-btn-primary",disabled:M||!z?.ready||te.status==="released",onClick:()=>void ae("/release/publish",{id:y.id,by:"人"},"已发布")},te.status==="released"?"已发布":"发布"),l("发布准入(预发部署 / 需求签字)没过时也要发:必须写原因,会记进部署日志",a("button",{className:"ds-btn ds-btn-ghost",disabled:M||!z?.ready||te.status==="released","data-force":"1",onClick:async()=>{const V=await i("强制发布的原因(会记进部署日志,便于复盘):",{title:"强制发布",okText:"强制发布",placeholder:"为什么不等预发部署 / 需求签字?"});if(V!==null){if(String(V).trim()===""){W("强制发布必须写原因");return}ae("/release/publish",{id:y.id,by:"人",force:!0,forceNote:String(V).trim()},"已强制发布(原因已记入部署日志)")}}},"强制发布")),te.status==="released"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:async()=>{const V=await i("回滚原因(要进复盘记录):",{title:"回滚这个版本",danger:!0,okText:"回滚"});V!==null&&V.trim()!==""&&ae("/release/rollback",{id:y.id,reason:V},"已回滚")}},"回滚"):null)),te.rollbackReason===""?null:a("div",{className:"ds-error"},`回滚原因:${te.rollbackReason}`))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布冒烟清单",a("span",{className:"ds-hint"},`${(te.smoke??[]).filter(V=>V.done).length}/${(te.smoke??[]).length} 项已过 · 发布前逐条点过,门禁才算过`)),a("div",{className:"ds-panel-body"},(te.smoke??[]).length===0?a("div",{className:"ds-empty"},"还没有冒烟项:把发布前必验的几条写在这里,比如「能登录」「下单支付成功」「首页不报错」。"):null,(te.smoke??[]).map(V=>a("div",{key:V.id,className:"ds-listrow"},a("label",{className:"ds-row",style:{gap:8,alignItems:"center",flex:1,cursor:"pointer"}},a(p,{checked:V.done===!0,onChange:()=>void ae("/release/smoke",{id:y.id,action:"toggle",itemId:V.id,done:V.done!==!0,by:"人"},V.done?"已取消":"冒烟项已过")}),a("span",{className:V.done?"ds-smoke-done":""},V.text)),V.done&&V.by!==""?a("span",{className:"ds-hint"},`${V.by} · ${Xa(V.at)}`):null,a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/smoke",{id:y.id,action:"remove",itemId:V.id},"已删除")},"×")))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:1},value:U,placeholder:"发布前必须验证什么?",onChange:V=>F(V.target.value)}),a("button",{className:"ds-btn",disabled:M||U.trim()==="",onClick:()=>{const V=U.trim();V!==""&&(F(""),ae("/release/smoke",{id:y.id,action:"add",text:V},"已加冒烟项"))}},"+ 加一条")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"审批、灰度与部署记录",a("span",{className:"ds-hint"},`审批 ${(te.approvals??[]).length} 条 · 灰度 ${(te.grayBatches??[]).length} 批`)),a("div",{className:"ds-panel-body"},(te.approvals??[]).length===0?a("div",{className:"ds-empty"},"还没有审批记录:点上面的「审批通过」,系统会把谁批的、什么结论记在这里。"):(te.approvals??[]).map(V=>a("div",{key:V.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${V.by} 审批通过`),a("div",{className:"ds-hint"},V.note||"(没写结论)")),a("span",{className:"ds-hint"},Xa(V.at)))),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),(te.grayBatches??[]).length===0?a("div",{className:"ds-hint"},"没有灰度批次:发布就是一次性全量。先加一批 10% 跑一轮再全量,出事好收。"):(te.grayBatches??[]).map(V=>a("div",{key:V.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${V.name} · ${V.percent}%`),a("div",{className:"ds-hint"},`${{pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"}[V.status]??V.status}${V.note===""?"":` · ${V.note}`}`)),a("div",{className:"ds-listrow-actions"},V.status==="pending"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/gray",{id:y.id,action:"start",batchId:V.id,by:"人"},"已开始灰度")},"开始"):null,V.status==="running"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/gray",{id:y.id,action:"advance",batchId:V.id,by:"人"},"这一批已通过")},"通过"):null,V.status==="passed"?a(u,{className:"ds-chip","data-on":"1"},"已通过"):null,V.status==="rolled_back"?a(u,{className:"ds-chip","data-sla":"overdue"},"已回滚"):null))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:2},value:L,placeholder:"批次名,例如 内测用户",onChange:V=>w(V.target.value)}),a(c,{className:"ds-input",style:{flex:"0 0 88px"},type:"number",min:1,max:100,value:T,onChange:V=>K(V.target.value)}),a("button",{className:"ds-btn",disabled:M||Number(T)<=0,onClick:async()=>{await ae("/release/gray",{id:y.id,action:"add",name:L,percent:Number(T),by:"人"},"已加一批灰度"),w(""),K("10")}},"+ 加一批")),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),a("div",{className:"ds-hint"},"上过哪些环境(只登记,不触发部署)"),(te.deploys??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginTop:6}},te.deploys.map(V=>a("span",{key:V.id,className:"ds-chip","data-env":V.kind,"data-on":V.at===0?"0":"1",title:V.at===0?`还没登记部署到「${V.name}」`:`${V.name}:${Xa(V.at)} ${V.action} · ${V.by}`},V.at===0?`${V.name} —`:`${V.name} ✓`))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(f,{className:"ds-input",style:{flex:"0 0 148px"},value:R,onChange:V=>O(V.target.value)},a("option",{value:""},"选环境…"),(e.config?.environments??[]).map(V=>a("option",{key:V.id,value:V.id},V.name))),a(c,{className:"ds-input",style:{flex:1},value:G,placeholder:"备注:谁发的、发了什么(可留空)",onChange:V=>we(V.target.value)}),a("button",{className:"ds-btn",disabled:M||R==="",onClick:async()=>{await ae("/release/deploy",{id:y.id,env:R,note:G,by:"人"},"已登记部署(没有触发任何部署)"),we("")}},"登记部署")),a("div",{className:"ds-hint",style:{marginTop:10}},"部署与灰度记录(最近 8 条)"),(()=>{const V={pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"},Ee=[...(te.deployLog??[]).map(ke=>({at:ke.at,kind:"deploy",by:ke.by,text:`${ke.action}${ke.env===""?"":` · ${(e.config?.environments??[]).find(Ae=>Ae.id===ke.env)?.name??ke.env}`}`,note:ke.note})),...(te.grayBatches??[]).map(ke=>({at:ke.at??ke.createdAt??0,kind:"gray",by:ke.by??"人",text:`灰度 ${ke.name} ${ke.percent}%`,note:V[ke.status]??ke.status}))].filter(ke=>ke.at).sort((ke,Ae)=>ke.at-Ae.at).slice(-8).reverse();return Ee.length===0?a("div",{className:"ds-empty"},"还没有部署或灰度记录。"):Ee.map(ke=>a("div",{key:`${ke.at}-${ke.kind}-${ke.text}`,className:"ds-hint"},a("span",{className:"ds-chip",style:{marginRight:6}},ke.kind==="gray"?"灰度":"部署"),`${Xa(ke.at)} · ${ke.text} · ${ke.by}${ke.note===""||ke.note===void 0?"":`:${ke.note}`}`))})())),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"这个版本里的卡",a("span",{className:"ds-hint"},`${xe.length} 张`)),a("div",{className:"ds-panel-body"},xe.length===0?a("div",{className:"ds-empty"},"还没有卡。从下面挑,或者在卡片详情里选「所属版本」。"):xe.map(V=>a("div",{key:V.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${V.code?`${V.code} `:""}${V.title}`),a("div",{className:"ds-hint"},`${(e.statuses??[]).find(Ee=>Ee.id===V.status)?.label??V.status}${V.blocked!==null&&V.blocked!==void 0?" · 阻塞":""}${V.type==="bug"?" · 缺陷":""}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(V.id)},"打开"),a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/link",{id:y.id,taskId:V.id,action:"remove"},"已从版本移除")},"移除")))),a("div",{className:"ds-row",style:{marginTop:8}},a(f,{className:"ds-select",style:{flex:1},value:H,onChange:V=>J(V.target.value)},a("option",{value:""},`挑一张卡挂进来(${X.length} 张可选)`),X.map(V=>a("option",{key:V.id,value:V.id},`${V.code?`${V.code} `:""}${V.title}`))),a("button",{className:"ds-btn",disabled:M||H==="",onClick:async()=>{await ae("/release/link",{id:y.id,taskId:H,action:"add"},"已挂到这个版本"),J("")}},"挂进来")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布说明",a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void navigator.clipboard?.writeText(C).then(()=>W("发布说明已复制"),()=>{})},"复制")),a("div",{className:"ds-panel-body"},a(v,{className:"ds-input",rows:12,value:C,placeholder:"点「生成发布说明」按类型自动汇总;之后可以在这里手动补充",onChange:V=>E(V.target.value),onBlur:()=>{C!==(te.notes??"")&&ae("/release/save",{release:{...y,notes:C}},"发布说明已保存")}}))),_===null?null:a("div",{className:"ds-error",style:{marginTop:8}},_),D===""?null:a("div",{className:"ds-hint",style:{marginTop:8}},D));return a("div",{className:"ds-body"},a("div",{className:"ds-req-grid"},ie,ge))}function lw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,SelectField:s}=vt(),[o,i]=n.useState(30),[l,c]=n.useState(""),[u,p]=n.useState(null),[f,v]=n.useState(null),[x,b]=n.useState(null),[k,g]=n.useState(null),[d,h]=n.useState(null),[y,S]=n.useState(null);if(n.useEffect(()=>{let w=!0;const T=`/analytics?days=${o}${l===""?"":`&project=${encodeURIComponent(l)}`}`;r(T).then(R=>{w&&(p(R.analytics),v(null))}).catch(R=>{w&&v(R instanceof Error?R.message:String(R))});const K=l===""?"":`&project=${encodeURIComponent(l)}`;return Promise.all([r("/workload"),r(`/burndown?days=14${K}`),r("/costs"),r("/quality")]).then(([R,O,G,we])=>{w&&(b(R.workload??null),g(O.burndown??null),h({...G.costs??{},budget:G.budget??null}),S(we.quality??null))}).catch(()=>{}),()=>{w=!1}},[o,l,e.revision]),f!==null)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},f));if(u===null)return a("div",{className:"ds-loading"},"正在统计…");const C=w=>`${Math.round((w??0)*100)}%`,E=w=>{const T=Number(w)||0;return T<=0?"0":T*1440<60?`${Math.max(1,Math.round(T*1440))} 分钟`:T<1?`${(T*24).toFixed(1)} 小时`:`${T.toFixed(1)} 天`},_=(w,T,K,R)=>a("div",{className:"ds-kpi","data-warn":R===!0?"1":"0"},a("div",{className:"ds-kpi-value"},T),a("div",{className:"ds-kpi-label"},w),a("div",{className:"ds-kpi-hint"},K)),I=(u.cycle.buckets??[]).reduce((w,T)=>w+T.count,0),D=Math.max(1,...(u.throughput??[]).map(w=>w.count),...(u.cycle.buckets??[]).map(w=>w.count)),W=Math.max(.001,...(u.columns??[]).map(w=>w.avgDays)),M=(u.columns??[]).reduce((w,T)=>w===null||T.avgDays>w.avgDays?T:w,null),P=(u.throughput??[]).reduce((w,T)=>w+T.count,0),H=(x?.experts??[]).filter(w=>w.enabled),J=H.reduce((w,T)=>w+T.open,0),U=H.reduce((w,T)=>w+(T.overdue??0),0),F=k?.series??[],L=F.length===0?null:F[F.length-1];return a("div",{className:"ds-body"},a("div",{className:"ds-board-bar"},a("span",{className:"ds-hint"},`统计口径:最近 ${u.days} 天 · ${u.scope.tasks} 张卡(${u.scope.done} 张已完成)`),a(s,{className:"ds-select",value:l,style:{width:"auto",minWidth:130},onChange:w=>c(w.target.value)},a("option",{value:""},"全部项目"),(e.config.projects??[]).map(w=>a("option",{key:w.id,value:w.id},`${w.key?`[${w.key}] `:""}${w.name}`))),a(s,{className:"ds-select",value:String(o),style:{width:"auto",minWidth:96},onChange:w=>i(Number(w.target.value))},a("option",{value:"7"},"近 7 天"),a("option",{value:"30"},"近 30 天"),a("option",{value:"90"},"近 90 天"))),a("div",{className:"ds-an-sec"},"质量",a("span",{className:"ds-hint"},"先看这四个数:返工多不多、多久能做完、AI 交的东西有多少被人验收")),a("div",{className:"ds-kpi-grid"},_("一次通过率",C(u.rework.firstPassRate),"完成的卡里,从没被退回过的比例",u.rework.firstPassRate<.6),_("返工率",C(u.rework.rate),`${u.rework.reworked}/${u.rework.moved} 张卡倒退过`,u.rework.rate>.3),_("中位周期",E(u.cycle.medianDays),`平均 ${E(u.cycle.avgDays)} · P90 ${E(u.cycle.p90Days)}`),_("AI 交付物采纳率",C(u.ai.acceptedRate),`${u.ai.accepted}/${u.ai.artifactsAi} 件已被验收 · 占全部交付物 ${C(u.ai.aiShare)}`)),a("div",{className:"ds-an-sec"},"流程与产能",a("span",{className:"ds-hint"},"完成的卡各花了多久 · 时间都卡在哪一列 · 最近每天完成几张")),a("div",{className:"ds-an-grid"},a("div",{className:"ds-span-4",style:{display:"grid",gap:12,gridTemplateRows:"auto 1fr",alignContent:"stretch"}},a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"周期分布",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${I} 张已完成`)),a("div",{className:"ds-panel-body"},(u.cycle.buckets??[]).map(w=>a("div",{key:w.id,className:"ds-an-row"},a("span",{className:"ds-an-name"},w.label),a("div",{className:"ds-an-track","data-empty":w.count===0?"1":"0"},a("div",{className:"ds-an-fill",style:{width:`${Math.round(w.count/D*100)}%`}})),a("span",{className:"ds-an-num"},`${w.count}`))),I===0?a("div",{className:"ds-hint"},"这个区间里还没有完成的卡。"):null,a("div",{className:"ds-an-foot"},`平均 ${E(u.cycle.avgDays)} · 中位 ${E(u.cycle.medianDays)} · P90 ${E(u.cycle.p90Days)}`))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"近 14 天完成"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-spark"},(u.throughput??[]).map((w,T)=>a("div",{key:w.day,className:"ds-spark-col",title:`${w.day}:完成 ${w.count} 张`},a("div",{className:"ds-spark-bar",style:{height:`${Math.round(w.count/Math.max(1,Math.max(...u.throughput.map(K=>K.count)))*100)}%`}}),a("span",{className:"ds-spark-day"},T%2===0?w.day:"")))),a("div",{className:"ds-an-foot"},`近 14 天合计 ${P} 张 · 日均 ${(P/14).toFixed(1)} 张 · 空着的日子是没卡完成,不是没数据`)))),a("div",{className:"ds-panel ds-span-8"},a("div",{className:"ds-panel-head"},"每列停留(平均)"),a("div",{className:"ds-panel-body"},(u.columns??[]).map(w=>a("div",{key:w.status,className:"ds-an-row"},a("span",{className:"ds-an-name"},w.label),a("div",{className:"ds-an-track","data-empty":w.avgDays===0?"1":"0"},a("div",{className:"ds-an-fill ds-an-fill-warn",style:{width:`${Math.round(w.avgDays/W*100)}%`}})),a("span",{className:"ds-an-num"},E(w.avgDays)))),a("div",{className:"ds-an-foot"},M===null?"还没有数据。":`最慢的是「${M.label}」:平均 ${E(M.avgDays)},时间主要压在这一列。`)))),a("div",{className:"ds-an-sec"},"交付、成本与返工",a("span",{className:"ds-hint"},"AI 交的东西被验收多少 · 谁手上压得最多 · 派单花了多少 · 哪些卡来回折腾")),a("div",{className:"ds-an-grid"},a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"按专家看质量",a("span",{className:"ds-hint"},"派单量来自运行日志(内存最近 300 条)")),a("div",{className:"ds-panel-body"},y===null?a("div",{className:"ds-hint"},"还没算出来"):(y.experts??[]).length===0?a("div",{className:"ds-hint"},"还没有派出过活"):a("div",null,(y.experts??[]).slice(0,8).map(w=>a("div",{className:"ds-an-kv",key:w.expert},a("span",{},`${w.label} · ${w.dispatched} 次`),a("b",{"data-warn":w.reworkRate!==null&&w.reworkRate>=40?"1":"0"},`${w.done}/${w.cards} 完成 · 一次过 ${w.firstPassRate===null?"—":w.firstPassRate+"%"}`))),a("div",{className:"ds-hint",style:{marginTop:6}},`合计:派单 ${y.totals.dispatched} 次 · 平均耗时 ${y.totals.avgMs>0?Math.round(y.totals.avgMs/1e3)+" 秒":"—"} · 返工率 ${y.totals.reworkRate===null?"—":y.totals.reworkRate+"%"} · AI 失败 ${y.totals.failures} 次 · 缺陷重开 ${y.totals.reopens} 次`),a("div",{className:"ds-hint"},y.cost?.note??"")))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"AI 产出质量"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-an-kv"},a("span",{},"交付物由专家登记"),a("b",{},`${u.ai.artifactsAi} / ${u.ai.artifactsTotal}`)),a("div",{className:"ds-an-kv"},a("span",{},"其中被人验收"),a("b",{},`${u.ai.accepted}(${C(u.ai.acceptedRate)})`)),a("div",{className:"ds-an-kv"},a("span",{},"缺正文(只有标题)"),a("b",{"data-warn":u.ai.emptyBody>0?"1":"0"},`${u.ai.emptyBody}`)),a("div",{className:"ds-an-kv"},a("span",{},"写码卡有执行记录"),a("b",{},`${u.ai.codingTasks}`)),a("div",{className:"ds-an-kv"},a("span",{},"评审通过 / 要求修改"),a("b",{},`${u.ai.codingApproved} / ${u.ai.codingChanges}`)),a("div",{className:"ds-an-kv"},a("span",{},"单测通过"),a("b",{},`${u.ai.unitPassed}/${u.ai.unitTests}(${C(u.ai.unitPassRate)})`)),a("div",{className:"ds-an-kv"},a("span",{},"已合并"),a("b",{},`${u.ai.merged}`)))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"团队负载",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${x?.busy??0} 位在跑`)),a("div",{className:"ds-panel-body"},(x?.experts??[]).filter(w=>w.enabled).length===0?a("div",{className:"ds-empty"},"还没有启用的专家。"):(x?.experts??[]).filter(w=>w.enabled).map(w=>a("div",{key:w.id,className:"ds-an-row"},a("span",{className:"ds-an-name"},`${w.emoji} ${w.name}`),a("div",{className:"ds-an-track"},a("div",{className:"ds-an-fill",style:{width:`${Math.round(w.open/Math.max(1,...(x.experts??[]).map(T=>T.open))*100)}%`}})),a("span",{className:"ds-an-num"},`在办 ${w.open}${w.running>0?` · 跑 ${w.running}`:""}${w.overdue>0?` · 逾期 ${w.overdue}`:""}`))),a("div",{className:"ds-an-foot"},`${H.length} 位在岗,手上合计 ${J} 张${U>0?`,其中 ${U} 张逾期`:",没有逾期的"}`))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"燃尽(近 14 天剩余)",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`现在还剩 ${k?.now?.remaining??0} 张`)),a("div",{className:"ds-panel-body"},a(nw,{series:k?.series??[]}),a("div",{className:"ds-an-foot"},L===null?"还没有数据。":`累计建 ${L.created} · 累计完成 ${L.done} · 现在还剩 ${L.remaining} 张`))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"AI 执行成本",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${d?.total??0} 次派单`)),a("div",{className:"ds-panel-body"},a("div",{className:"ds-an-kv"},a("span",{},"平均执行时长"),a("b",{},`${Math.round((d?.avgMs??0)/1e3)} 秒`)),(d?.byModel??[]).slice(0,3).map(w=>a("div",{key:`m-${w.key}`,className:"ds-an-kv"},a("span",{},`模型 ${w.key}`),a("b",{},`${w.runs} 次`))),(d?.byPromptVersion??[]).slice(0,3).map(w=>a("div",{key:`p-${w.key}`,className:"ds-an-kv"},a("span",{},`提示词 ${w.key}`),a("b",{},`${w.runs} 次`))),a("div",{className:"ds-hint"},d?.note??""),d?.budget===null||d?.budget===void 0?null:a("div",{style:{marginTop:8}},a("div",{className:"ds-an-kv"},a("span",{},"预算使用"),a("b",{style:{color:d.budget.level==="over"?"#e5484d":d.budget.level==="warn"?"#d97706":void 0}},`${Math.max(d.budget.runRate,d.budget.hourRate)}%`)),a(aw,{value:Math.min(100,Math.max(d.budget.runRate,d.budget.hourRate)),tone:d.budget.level}),a("div",{className:"ds-hint"},`${d.budget.runs}/${d.budget.budgetRuns} 次 · ${d.budget.hours}/${d.budget.budgetHours} 小时 · ${d.budget.hint}`)))),a("div",{className:"ds-panel ds-span-8"},a("div",{className:"ds-panel-head"},"返工最多的卡",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`评审打回 ${u.rework.reviewRework} 次 · 测试打回 ${u.rework.testRework} 次`)),a("div",{className:"ds-panel-body"},(u.rework.worst??[]).length===0?a("div",{className:"ds-hint"},"没有卡倒退过,挺好。"):u.rework.worst.map(w=>a("div",{key:w.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${w.code===""?"":`${w.code} `}${w.title}`),a("div",{className:"ds-hint"},`倒退 ${w.backward} 次 · 现在在「${(e.statuses.find(T=>T.id===w.status)??{}).label??w.status}」`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(w.id)},"打开"))))))))}function dw(e,t){const a="studio",n="/studio-api";let r=null;function s(Y){r=Y??null}function o(Y){return r===null?null:r[Y]??null}function i(Y,ne){if(ne==null)return ne;const ee=ne.props??{},be=ee.title!==void 0?ne:t(ne.type,{...ee,title:Y}),se=o("Tip");return se===null?be:t(se,{key:ne.key,label:Y},be)}const{TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f}=px({h:t,kit:o,React:e}),v=fx({h:t,kit:o}),x=Lx({h:t,React:e}),b=mx({h:t,ChipField:u}),k=gx({h:t,React:e,Card:b,TextField:l,SelectField:f,CheckField:p,toast:H});function g({rows:Y=6,value:ne,onChange:ee,placeholder:be,className:se,...de}){const[Q,ve]=e.useState(!1),Ce=String(ne??"");return t("div",{className:"ds-md-field"},t("div",{className:"ds-md-bar"},t("span",{className:"ds-hint"},"支持 Markdown:标题 / 列表 / 粗体 / 代码块 / 表格"),t("button",{className:"ds-btn ds-btn-ghost",type:"button",onClick:()=>ve(!Q)},Q?"回到编辑":"预览渲染")),Q?t("div",{className:"ds-md-preview ds-req-desc ds-md"},Ce.trim()===""?t("span",{className:"ds-hint"},"还没写内容。"):Ee(Ce)):t(c,{className:se??"ds-textarea",rows:Y,value:ne,onChange:ee,placeholder:be,...de}))}function d(Y,ne){const ee=t("div",{className:"ds-label",style:{cursor:"help"}},Y);return ne==null||ne===""?ee:i(ne,ee)}let h=null,y=null;function S(Y){h=Y,typeof y=="function"&&y(Y)}function C(Y,ne){const ee=ne??{};return o("DialogBox")===null?Promise.resolve(window.confirm(String(Y))):new Promise(se=>{S({kind:"confirm",title:ee.title??"确认操作",message:String(Y??""),danger:ee.danger===!0,okText:ee.okText??"确定",cancelText:ee.cancelText??"取消",resolve:se})})}function E(Y,ne){const ee=ne??{};return o("DialogBox")===null?Promise.resolve(window.prompt(String(Y),ee.defaultValue??"")):new Promise(se=>{S({kind:"prompt",title:ee.title??"需要填写",message:String(Y??""),value:String(ee.defaultValue??""),placeholder:ee.placeholder??"",okText:ee.okText??"确定",cancelText:"取消",resolve:se})})}function _(){const[Y,ne]=e.useState(null),[ee,be]=e.useState("");e.useEffect(()=>{const Q=ve=>{ne(ve),be(ve!==null&&ve.kind==="prompt"?ve.value:"")};return y=Q,h!==null&&Q(h),()=>{y===Q&&(y=null)}},[]);const se=o("DialogBox");if(Y===null||se===null)return null;const de=Q=>{const ve=Y.resolve;ne(null),h=null,ve(Q)};return t(se,{open:!0,title:Y.title,message:Y.message,danger:Y.danger===!0,okText:Y.okText,cancelText:Y.cancelText,input:Y.kind==="prompt",value:ee,placeholder:Y.placeholder,onChange:Q=>be(Q),onOk:()=>de(Y.kind==="prompt"?ee.trim():!0),onCancel:()=>de(Y.kind==="prompt"?null:!1)})}let I=0,D=[],W=[],M={text:"",at:0};function P(){const Y=D.slice();for(const ne of W.slice())ne(Y)}function H(Y,ne="success",ee=3200){const be=String(Y??"").trim();if(be==="")return null;const se=Date.now();if(M.text===be&&se-M.at<600)return null;M={text:be,at:se};const de=++I;return D=D.concat([{id:de,text:be,tone:ne}]),P(),window.setTimeout(()=>{J(de)},ee),de}function J(Y){const ne=D.filter(ee=>ee.id!==Y);ne.length!==D.length&&(D=ne,P())}const U={success:"✓",error:"!",info:"i"};function F(){const[Y,ne]=e.useState(()=>D.slice());return e.useEffect(()=>(W.push(ne),()=>{W=W.filter(ee=>ee!==ne)}),[]),Y.length===0?null:t("div",{className:"ds-toasts",role:"status","aria-live":"polite"},Y.map(ee=>t("div",{key:ee.id,className:"ds-toast","data-tone":ee.tone},t("span",{className:"ds-toast-icon","aria-hidden":"true"},U[ee.tone]??"i"),t("span",{className:"ds-toast-text"},ee.text),t("button",{type:"button",className:"ds-toast-close","aria-label":"关闭提示",onClick:()=>J(ee.id)},"×"))))}const L="dsh-studio:user";function w(){try{const Y=window.localStorage.getItem(L);if(typeof Y=="string"&&Y.trim()!=="")return Y.trim().slice(0,40)}catch{}return"人"}function T(Y){const ne=String(Y??"").trim().slice(0,40)||"人";try{window.localStorage.setItem(L,ne)}catch{}return ne}const K="dsh-studio:theme",R=[["light","☀ 明亮"],["dark","☾ 黑暗"],["system","跟随系统"]];function O(){return typeof window.matchMedia=="function"?window.matchMedia("(prefers-color-scheme: dark)"):null}function G(){try{const Y=window.getComputedStyle(document.body).backgroundColor,ne=/rgba?\(([^)]+)\)/.exec(String(Y??""));if(ne===null)return null;const ee=ne[1].split(",").map(ve=>Number(ve.trim())),[be,se,de,Q]=ee;return![be,se,de].every(ve=>Number.isFinite(ve))||Number.isFinite(Q)&&Q<.2?null:.2126*be+.7152*se+.0722*de<128?"dark":"light"}catch{return null}}function we(){try{const Y=window.localStorage.getItem(K);if(Y==="light"||Y==="dark"||Y==="system")return Y}catch{}return"system"}function fe(Y){const ne=Y==="light"||Y==="dark"?Y:"system";try{window.localStorage.setItem(K,ne)}catch{}return ne}const ye="dsh-studio:session",He="dsh-studio:auth-user";function ae(){try{const Y=window.localStorage.getItem(ye);if(typeof Y=="string"&&Y.trim()!=="")return Y.trim()}catch{}return""}function z(Y){try{Y===""||Y===null||Y===void 0?window.localStorage.removeItem(ye):window.localStorage.setItem(ye,String(Y))}catch{}}function te(){try{const Y=window.localStorage.getItem(He);if(typeof Y=="string"&&Y!==""){const ne=JSON.parse(Y);if(ne!==null&&typeof ne=="object")return ne}}catch{}return null}function xe(Y){try{Y==null?window.localStorage.removeItem(He):window.localStorage.setItem(He,JSON.stringify(Y))}catch{}return Y??null}async function X(Y,ne){const ee={"x-studio-user":encodeURIComponent(w())},be=ae();be!==""&&(ee["x-studio-session"]=be),ne!==void 0&&(ee["content-type"]="application/json");const se=ne===void 0?{method:"GET",headers:ee}:{method:"POST",headers:ee,body:JSON.stringify(ne)},de=await fetch(n+Y,se);let Q={};try{Q=await de.json()}catch{}if(!de.ok||Q.ok===!1)throw new Error(Q.error??`请求失败(HTTP ${de.status})`);return Q}const ie=`
|
|
46
|
+
`)})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void se(A.id),"data-danger":"1"},"删除")))),(z.loose??[]).length===0?null:s("div",{className:"ds-pt-loose"},s("div",{className:"ds-hint",style:{marginBottom:6}},`还有 ${z.loose.length} 张卡没归到功能点,选一个挂上去,矩阵才算完整:`),z.loose.map(A=>s("div",{key:A.id,className:"ds-pt-loose-row"},s("span",{className:"ds-chip-code"},A.code||A.id),s("span",{className:"ds-pt-loose-title"},A.title),s(g,{className:"ds-select",style:{width:220},value:"",onChange:ue=>{ue.target.value!==""&&de(A.id,ue.target.value)}},s("option",{value:""},"挂到…"),(z.points??[]).map(ue=>s("option",{key:ue.id,value:ue.id},`${ue.id} ${ue.title}`))))))),s("div",{className:"ds-row",style:{gap:8,marginTop:10,alignItems:"center"}},s("button",{className:"ds-btn",onClick:()=>ge(ee())},"+ 加功能点"),s("button",{className:"ds-btn ds-btn-primary",disabled:Be,onClick:()=>void le()},Be?"拆卡中…":"按未覆盖的功能点拆卡"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>{Q!==null&&Y(Q.id)}},"刷新矩阵"),Rt===null?null:s("span",{className:"ds-hint"},Rt),xe===null?null:s("span",{className:"ds-hint"},xe)),ie===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},ie.id===void 0?"新增功能点":`编辑功能点 ${ie.id}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"功能点标题"),s(v,{className:"ds-input",value:ie.title,placeholder:"例如 结算页展示可用积分",onChange:A=>ge({...ie,title:A.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"说明"),s(d,{className:"ds-textarea",rows:3,value:ie.description??"",onChange:A=>ge({...ie,description:A.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"验收条件(一行一条,做成什么样算完成)"),s(x,{className:"ds-textarea",rows:4,value:ie.acceptanceText??"",onChange:A=>ge({...ie,acceptanceText:A.target.value})})),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"优先级"),s(g,{className:"ds-select",value:ie.priority,onChange:A=>ge({...ie,priority:A.target.value})},s("option",{value:"urgent"},"紧急"),s("option",{value:"normal"},"普通"),s("option",{value:"relaxed"},"可缓"))),s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:ie.status,onChange:A=>ge({...ie,status:A.target.value})},s("option",{value:"draft"},"草稿"),s("option",{value:"ready"},"就绪"),s("option",{value:"done"},"已完成")))),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>ge(null)},"取消"),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void be()},"保存功能点")))),s("div",{className:"ds-req-stats"},s("div",{className:"ds-stat-box"},s("b",null,`${ce}/${q.length}`),s("span",null,"卡已完成")),s("div",{className:"ds-stat-box","data-warn":Le>0?"1":"0"},s("b",null,String(Le)),s("span",null,"阻塞")),s("div",{className:"ds-stat-box","data-warn":At>0?"1":"0"},s("b",null,String(At)),s("span",null,"未结缺陷")),s("div",{className:"ds-stat-box"},s("b",null,`${$a.done}/${$a.total}`),s("span",null,"验收清单")),s("div",{className:"ds-stat-box"},s("b",null,`${ra.pass}/${ra.total}`),s("span",null,"用例通过")),s("div",{className:"ds-stat-box"},s("b",null,String(Ma.length)),s("span",null,"交付物")),s("div",{className:"ds-stat-box"},s("b",null,String(Dn.length)),s("span",null,"MR"))),s("div",{className:"ds-sec-head",style:{marginTop:14}},`这张需求下的 ${q.length} 张卡`),q.length===0?s("div",{className:"ds-empty"},"还没有挂上来的卡。建卡时选择这条需求,编号会自动分配。"):s("div",{className:"ds-req-tasks"},q.map(A=>s("div",{key:A.id,className:"ds-listrow ds-req-task",onClick:()=>t(A.id)},s("div",{className:"ds-listrow-main"},s("div",{className:"ds-req-task-title"},A.code?s(b,{className:"ds-chip ds-chip-code"},A.code):null,s("span",null,A.title)),s("div",{className:"ds-hint"},`${Ce(A.status)}${(()=>{const ue=A.expert===""?"":(e.config.experts??[]).find(Fe=>Fe.id===A.expert)?.name??A.expert;return ue===""?"":` · ${ue}`})()}${A.dueAt>0?` · 截止 ${Xa(A.dueAt)}`:""}`)),s("div",{className:"ds-listrow-actions"},A.blocked!==void 0&&A.blocked!==null?s(b,{className:"ds-chip ds-chip-blocked"},"⛔"):null,A.type==="bug"?s(b,{className:"ds-chip ds-chip-bug","data-sev":A.severity??"p2"},"BUG"):null)))),Dn.length>0?s("div",null,s("div",{className:"ds-sec-head",style:{marginTop:14}},"关联 MR"),Dn.map(A=>s("div",{key:A.id,className:"ds-hint"},`${A.code||A.id} · !${A.gitlabMR.iid} ${A.gitlabMR.title??""}(${A.gitlabMR.state||"open"})`))):null)})();return s("div",{className:"ds-body"},D!==null?s("div",{className:"ds-err",style:{marginBottom:10}},D):null,xt,s("div",{className:"ds-req-grid"},s("div",{className:"ds-intake-launch",style:{display:"flex",gap:8,padding:"0 0 8px"}},s("span",{className:"ds-hint"},"新建需求请用右上角「+ 新建」")),s("div",{className:"ds-req-list"},s("div",{className:"ds-req-list-head"},s("span",null,`需求 ${h.length}`),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},title:"按来源筛选:全部 / 业务提单 / 内部",onClick:()=>{const q=window.localStorage.getItem("dsh-studio:reqsrc")??"all";window.localStorage.setItem("dsh-studio:reqsrc",q==="all"?"business":q==="business"?"internal":"all"),window.dispatchEvent(new Event("resize"))}},(window.localStorage.getItem("dsh-studio:reqsrc")??"all")==="all"?"来源:全部":window.localStorage.getItem("dsh-studio:reqsrc")==="business"?"来源:业务提单":"来源:内部")),h.length===0?s("div",{className:"ds-empty"},"还没有需求。先建一条——后面所有的卡、用例、缺陷、MR 都会挂在它下面。"):((window.localStorage.getItem("dsh-studio:reqsort")??"score")==="score"?h.slice().sort((q,ce)=>(ce.pool?.score??0)-(q.pool?.score??0)):h).map(q=>{const ce=window.localStorage.getItem("dsh-studio:reqsrc")??"all";if(ce!=="all"&&(q.source==="business"?"business":"internal")!==ce)return null;const Le=ve(q.id),At=Le.filter(Ma=>Ma.status==="done").length,$a=q.progress?.progress??(Le.length===0?0:Math.round(At/Le.length*100)),ra=q.progress?.gate??null;return s("div",{key:q.id,className:"ds-req-item","data-on":q.id===S?"1":"0",onClick:()=>C(q.id)},s("div",{className:"ds-req-item-title"},q.title),s("div",{className:"ds-req-item-meta"},s(b,{className:"ds-chip ds-chip-code"},q.code||"未编号"),q.source==="business"?s(b,{className:"ds-chip","data-src":"business",title:[q.requester?.name,q.requester?.dept].filter(Boolean).join(" · ")||"业务提单"},"业务提单"):null,s(b,{className:"ds-chip","data-req":q.status},Ar[q.status]??q.status),s("span",{className:"ds-hint"},`${At}/${Le.length} 卡`),ra===null?null:s("span",{className:"ds-hint",style:{color:ra.ready?"#0f9d58":"#b26a00"}},ra.ready?"准出通过":`还差 ${ra.blockers.length} 项`)),s("div",{className:"ds-check-bar","data-full":$a===100?"1":"0",style:{marginTop:2}},s("span",{style:{width:`${$a}%`}})))})),s("div",{className:"ds-req-detail"},ht)))}function iw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,toast:s,confirmDialog:o,promptDialog:i,tipOf:l,TextField:c,ChipField:u,CheckField:p,SelectField:f,MdField:v}=vt(),[x,b]=n.useState([]),[k,g]=n.useState(null),[d,h]=n.useState(null),[y,S]=n.useState(null),[C,E]=n.useState(""),[_,I]=n.useState(null),[D,W]=n.useState(""),[M,P]=n.useState(!1),[H,J]=n.useState(""),[U,F]=n.useState(""),[L,w]=n.useState(""),[T,K]=n.useState("10"),[R,O]=n.useState(""),[G,we]=n.useState(""),fe=n.useCallback(async V=>{const ke=(await r("/releases")).releases??[];b(ke);const Ae=V??(ke.some(Ya=>Ya.id===k)?k:ke[0]?.id??null);if(g(Ae),Ae===null){h(null);return}const Ve=await r("/release/get",{id:Ae});h(Ve),S({id:Ve.release.id,version:Ve.release.version,name:Ve.release.name,project:Ve.release.project,owner:Ve.release.owner,status:Ve.release.status,plannedAt:Ve.release.plannedAt,tagName:Ve.release.tagName??"",commitSha:Ve.release.commitSha??""}),E(Ve.release.notes??"")},[k]);n.useEffect(()=>{let V=!0;return fe().then(()=>{V&&I(null)}).catch(Ee=>{V&&I(Ee instanceof Error?Ee.message:String(Ee))}),()=>{V=!1}},[e.revision,fe]);async function ye(V){g(V),P(!0);try{const Ee=await r("/release/get",{id:V});h(Ee),S({id:Ee.release.id,version:Ee.release.version,name:Ee.release.name,project:Ee.release.project,owner:Ee.release.owner,status:Ee.release.status,plannedAt:Ee.release.plannedAt,tagName:Ee.release.tagName??"",commitSha:Ee.release.commitSha??""}),E(Ee.release.notes??""),W("")}catch(Ee){I(Ee instanceof Error?Ee.message:String(Ee))}finally{P(!1)}}function He(){g(null),h(null),S({id:"",version:"",name:"",project:"",owner:"",status:"draft",plannedAt:0,tagName:"",commitSha:""}),E(""),W("")}async function ae(V,Ee,ke){P(!0),I(null);try{const Ae=await r(V,Ee);W(ke??"已保存"),s(ke??"已保存"),s(ke??"已保存"),await fe(Ae.release?.id??k)}catch(Ae){const Ve=Ae instanceof Error?Ae.message:String(Ae);I(Ve),s(Ve,"error")}finally{P(!1)}}if(_!==null&&x.length===0)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},_));const z=d?.gate,te=d?.release,xe=d?.tasks??[],X=e.tasks.filter(V=>V.releaseId===""||V.releaseId===void 0),ie=a("div",{className:"ds-req-list"},a("div",{className:"ds-row",style:{justifyContent:"space-between",marginBottom:6}},a("span",{className:"ds-hint"},`${x.length} 个版本`),a("button",{className:"ds-btn",onClick:He},"+ 新建版本")),x.length===0?a("div",{className:"ds-empty"},"还没有版本。先建一个版本,再把卡挂上来。"):x.map(V=>a("div",{key:V.id,className:"ds-req-item","data-on":k===V.id?"1":"0",onClick:()=>void ye(V.id)},a("div",{className:"ds-req-item-title"},`${V.version||V.id} ${V.name}`.trim()),a("div",{className:"ds-req-item-meta"},a(u,{className:"ds-chip","data-kind":V.status},af[V.status]??V.status),a(u,{className:"ds-chip"},`${V.gate.stats.done}/${V.gate.stats.tasks} 卡`),V.gate.blockers.length===0?a(u,{className:"ds-chip",style:{color:"#0f9d58",borderColor:"#0f9d5855"}},"门禁通过"):a(u,{className:"ds-chip ds-chip-blocked"},`${V.gate.blockers.length} 项拦着`))))),ge=y===null?a("div",{className:"ds-hint"},"左边选一个版本,或者新建一个。"):a("div",{className:"ds-req-detail"},(te?.requirements??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginBottom:8}},a("span",{className:"ds-hint"},`这个版本涉及 ${te.requirements.length} 条需求:`),te.requirements.map(V=>a("span",{key:V.id,className:"ds-chip",title:V.title},`${V.code||V.id} ${V.title}(${Ar[V.derived]??V.derived})`))),a("div",{className:"ds-req-headline"},a("div",null,a("div",{className:"ds-req-name"},`${y.version||"未编号"} ${y.name}`.trim()),a("div",{className:"ds-hint"},te==null?"新版本:先填版本号与名字,保存后可以把卡挂进来":`创建于 ${Xa(te.createdAt)}${te.releasedAt>0?` · 发布于 ${Xa(te.releasedAt)}`:""}${te.approveBy!==""?` · 审批人 ${te.approveBy}`:""}`)),a("div",{className:"ds-req-actions"},a(f,{className:"ds-select",style:{width:108},value:y.status,onChange:V=>S({...y,status:V.target.value})},ux.map(V=>a("option",{key:V,value:V},af[V]))))),a("div",{className:"ds-panel",style:{marginTop:4}},a("div",{className:"ds-panel-head"},"版本信息"),a("div",{className:"ds-panel-body ds-grid2"},a("label",{className:"ds-field"},a("span",{},"版本号"),a(c,{className:"ds-input",value:y.version,placeholder:"v1.3.0",disabled:te?.status==="released",onChange:V=>S({...y,version:V.target.value})})),a("label",{className:"ds-field"},a("span",{},"版本名"),a(c,{className:"ds-input",value:y.name,placeholder:"积分抵扣首个版本",onChange:V=>S({...y,name:V.target.value})})),a("label",{className:"ds-field"},a("span",{},"业务项目"),a(f,{className:"ds-select",value:y.project,onChange:V=>S({...y,project:V.target.value})},a("option",{value:""},"不属于任何项目"),(e.config.projects??[]).map(V=>a("option",{key:V.id,value:V.id},V.name)))),a("label",{className:"ds-field"},a("span",{},"发布负责人"),a(c,{className:"ds-input",value:y.owner,onChange:V=>S({...y,owner:V.target.value})})),a("label",{className:"ds-field"},a("span",{},"计划发布时间"),a(c,{className:"ds-input",type:"date",value:nf(y.plannedAt),onChange:V=>S({...y,plannedAt:jr(V.target.value)})})),a("label",{className:"ds-field"},a("span",{},"发布标签(tag)"),a(c,{className:"ds-input",value:y.tagName??"",placeholder:"v1.0.0",onChange:V=>S({...y,tagName:V.target.value})})),a("label",{className:"ds-field"},a("span",{},"对应提交(sha,选填)"),a(c,{className:"ds-input",value:y.commitSha??"",placeholder:"a1b2c3d",onChange:V=>S({...y,commitSha:V.target.value})})),a("div",{className:"ds-row",style:{alignItems:"flex-end",justifyContent:"flex-end",gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void ae("/release/save",{release:y},"版本已保存")},"保存"),y.id===""?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:M,style:{color:"var(--destructive)"},onClick:async()=>{await o("删除这个版本?卡会保留,只是脱离版本。",{title:"删除版本",danger:!0,okText:"删除"})&&ae("/release/delete",{id:y.id},"版本已删除")}},"删除")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布门禁",a("span",{className:"ds-hint"},"卡全做完、没阻塞、缺陷关闭、用例没有失败,才算能发")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-req-stats"},a("div",{className:"ds-stat-box"},a("b",{},String(z?.stats.done??0)),a("span",{},"已完成卡")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(z?.stats.open??0)>0?"1":"0"},String(z?.stats.open??0)),a("span",{},"未完成")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(z?.stats.blocked??0)>0?"1":"0"},String(z?.stats.blocked??0)),a("span",{},"阻塞")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(z?.stats.bugs??0)>0?"1":"0"},String(z?.stats.bugs??0)),a("span",{},"未结缺陷")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(z?.stats.failing??0)>0?"1":"0"},String(z?.stats.failing??0)),a("span",{},"失败用例")),a("div",{className:"ds-stat-box"},a("b",{},String(z?.stats.artifacts??0)),a("span",{},"交付物"))),(z?.blockers??[]).map((V,Ee)=>a("div",{key:`b${Ee}`,className:"ds-error"},`⛔ ${V}`)),(z?.warnings??[]).map((V,Ee)=>a("div",{key:`w${Ee}`,className:"ds-hint"},`⚠ ${V}`)),a("div",{className:"ds-action-bar"},a("div",{className:"ds-action-bar-left"},a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/notes",{id:y.id},"发布说明已生成")},"生成发布说明"),a("button",{className:"ds-btn",disabled:M||!z?.ready,onClick:()=>void ae("/release/approve",{id:y.id,by:"人"},"已审批,可以发布了")},"审批通过")),a("div",{className:"ds-action-bar-right"},a("button",{className:"ds-btn ds-btn-primary",disabled:M||!z?.ready||te.status==="released",onClick:()=>void ae("/release/publish",{id:y.id,by:"人"},"已发布")},te.status==="released"?"已发布":"发布"),l("发布准入(预发部署 / 需求签字)没过时也要发:必须写原因,会记进部署日志",a("button",{className:"ds-btn ds-btn-ghost",disabled:M||!z?.ready||te.status==="released","data-force":"1",onClick:async()=>{const V=await i("强制发布的原因(会记进部署日志,便于复盘):",{title:"强制发布",okText:"强制发布",placeholder:"为什么不等预发部署 / 需求签字?"});if(V!==null){if(String(V).trim()===""){W("强制发布必须写原因");return}ae("/release/publish",{id:y.id,by:"人",force:!0,forceNote:String(V).trim()},"已强制发布(原因已记入部署日志)")}}},"强制发布")),te.status==="released"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:async()=>{const V=await i("回滚原因(要进复盘记录):",{title:"回滚这个版本",danger:!0,okText:"回滚"});V!==null&&V.trim()!==""&&ae("/release/rollback",{id:y.id,reason:V},"已回滚")}},"回滚"):null)),te.rollbackReason===""?null:a("div",{className:"ds-error"},`回滚原因:${te.rollbackReason}`))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布冒烟清单",a("span",{className:"ds-hint"},`${(te.smoke??[]).filter(V=>V.done).length}/${(te.smoke??[]).length} 项已过 · 发布前逐条点过,门禁才算过`)),a("div",{className:"ds-panel-body"},(te.smoke??[]).length===0?a("div",{className:"ds-empty"},"还没有冒烟项:把发布前必验的几条写在这里,比如「能登录」「下单支付成功」「首页不报错」。"):null,(te.smoke??[]).map(V=>a("div",{key:V.id,className:"ds-listrow"},a("label",{className:"ds-row",style:{gap:8,alignItems:"center",flex:1,cursor:"pointer"}},a(p,{checked:V.done===!0,onChange:()=>void ae("/release/smoke",{id:y.id,action:"toggle",itemId:V.id,done:V.done!==!0,by:"人"},V.done?"已取消":"冒烟项已过")}),a("span",{className:V.done?"ds-smoke-done":""},V.text)),V.done&&V.by!==""?a("span",{className:"ds-hint"},`${V.by} · ${Xa(V.at)}`):null,a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/smoke",{id:y.id,action:"remove",itemId:V.id},"已删除")},"×")))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:1},value:U,placeholder:"发布前必须验证什么?",onChange:V=>F(V.target.value)}),a("button",{className:"ds-btn",disabled:M||U.trim()==="",onClick:()=>{const V=U.trim();V!==""&&(F(""),ae("/release/smoke",{id:y.id,action:"add",text:V},"已加冒烟项"))}},"+ 加一条")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"审批、灰度与部署记录",a("span",{className:"ds-hint"},`审批 ${(te.approvals??[]).length} 条 · 灰度 ${(te.grayBatches??[]).length} 批`)),a("div",{className:"ds-panel-body"},(te.approvals??[]).length===0?a("div",{className:"ds-empty"},"还没有审批记录:点上面的「审批通过」,系统会把谁批的、什么结论记在这里。"):(te.approvals??[]).map(V=>a("div",{key:V.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${V.by} 审批通过`),a("div",{className:"ds-hint"},V.note||"(没写结论)")),a("span",{className:"ds-hint"},Xa(V.at)))),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),(te.grayBatches??[]).length===0?a("div",{className:"ds-hint"},"没有灰度批次:发布就是一次性全量。先加一批 10% 跑一轮再全量,出事好收。"):(te.grayBatches??[]).map(V=>a("div",{key:V.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${V.name} · ${V.percent}%`),a("div",{className:"ds-hint"},`${{pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"}[V.status]??V.status}${V.note===""?"":` · ${V.note}`}`)),a("div",{className:"ds-listrow-actions"},V.status==="pending"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/gray",{id:y.id,action:"start",batchId:V.id,by:"人"},"已开始灰度")},"开始"):null,V.status==="running"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/gray",{id:y.id,action:"advance",batchId:V.id,by:"人"},"这一批已通过")},"通过"):null,V.status==="passed"?a(u,{className:"ds-chip","data-on":"1"},"已通过"):null,V.status==="rolled_back"?a(u,{className:"ds-chip","data-sla":"overdue"},"已回滚"):null))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:2},value:L,placeholder:"批次名,例如 内测用户",onChange:V=>w(V.target.value)}),a(c,{className:"ds-input",style:{flex:"0 0 88px"},type:"number",min:1,max:100,value:T,onChange:V=>K(V.target.value)}),a("button",{className:"ds-btn",disabled:M||Number(T)<=0,onClick:async()=>{await ae("/release/gray",{id:y.id,action:"add",name:L,percent:Number(T),by:"人"},"已加一批灰度"),w(""),K("10")}},"+ 加一批")),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),a("div",{className:"ds-hint"},"上过哪些环境(只登记,不触发部署)"),(te.deploys??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginTop:6}},te.deploys.map(V=>a("span",{key:V.id,className:"ds-chip","data-env":V.kind,"data-on":V.at===0?"0":"1",title:V.at===0?`还没登记部署到「${V.name}」`:`${V.name}:${Xa(V.at)} ${V.action} · ${V.by}`},V.at===0?`${V.name} —`:`${V.name} ✓`))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(f,{className:"ds-input",style:{flex:"0 0 148px"},value:R,onChange:V=>O(V.target.value)},a("option",{value:""},"选环境…"),(e.config?.environments??[]).map(V=>a("option",{key:V.id,value:V.id},V.name))),a(c,{className:"ds-input",style:{flex:1},value:G,placeholder:"备注:谁发的、发了什么(可留空)",onChange:V=>we(V.target.value)}),a("button",{className:"ds-btn",disabled:M||R==="",onClick:async()=>{await ae("/release/deploy",{id:y.id,env:R,note:G,by:"人"},"已登记部署(没有触发任何部署)"),we("")}},"登记部署")),a("div",{className:"ds-hint",style:{marginTop:10}},"部署与灰度记录(最近 8 条)"),(()=>{const V={pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"},Ee=[...(te.deployLog??[]).map(ke=>({at:ke.at,kind:"deploy",by:ke.by,text:`${ke.action}${ke.env===""?"":` · ${(e.config?.environments??[]).find(Ae=>Ae.id===ke.env)?.name??ke.env}`}`,note:ke.note})),...(te.grayBatches??[]).map(ke=>({at:ke.at??ke.createdAt??0,kind:"gray",by:ke.by??"人",text:`灰度 ${ke.name} ${ke.percent}%`,note:V[ke.status]??ke.status}))].filter(ke=>ke.at).sort((ke,Ae)=>ke.at-Ae.at).slice(-8).reverse();return Ee.length===0?a("div",{className:"ds-empty"},"还没有部署或灰度记录。"):Ee.map(ke=>a("div",{key:`${ke.at}-${ke.kind}-${ke.text}`,className:"ds-hint"},a("span",{className:"ds-chip",style:{marginRight:6}},ke.kind==="gray"?"灰度":"部署"),`${Xa(ke.at)} · ${ke.text} · ${ke.by}${ke.note===""||ke.note===void 0?"":`:${ke.note}`}`))})())),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"这个版本里的卡",a("span",{className:"ds-hint"},`${xe.length} 张`)),a("div",{className:"ds-panel-body"},xe.length===0?a("div",{className:"ds-empty"},"还没有卡。从下面挑,或者在卡片详情里选「所属版本」。"):xe.map(V=>a("div",{key:V.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${V.code?`${V.code} `:""}${V.title}`),a("div",{className:"ds-hint"},`${(e.statuses??[]).find(Ee=>Ee.id===V.status)?.label??V.status}${V.blocked!==null&&V.blocked!==void 0?" · 阻塞":""}${V.type==="bug"?" · 缺陷":""}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(V.id)},"打开"),a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/link",{id:y.id,taskId:V.id,action:"remove"},"已从版本移除")},"移除")))),a("div",{className:"ds-row",style:{marginTop:8}},a(f,{className:"ds-select",style:{flex:1},value:H,onChange:V=>J(V.target.value)},a("option",{value:""},`挑一张卡挂进来(${X.length} 张可选)`),X.map(V=>a("option",{key:V.id,value:V.id},`${V.code?`${V.code} `:""}${V.title}`))),a("button",{className:"ds-btn",disabled:M||H==="",onClick:async()=>{await ae("/release/link",{id:y.id,taskId:H,action:"add"},"已挂到这个版本"),J("")}},"挂进来")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布说明",a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void navigator.clipboard?.writeText(C).then(()=>W("发布说明已复制"),()=>{})},"复制")),a("div",{className:"ds-panel-body"},a(v,{className:"ds-input",rows:12,value:C,placeholder:"点「生成发布说明」按类型自动汇总;之后可以在这里手动补充",onChange:V=>E(V.target.value),onBlur:()=>{C!==(te.notes??"")&&ae("/release/save",{release:{...y,notes:C}},"发布说明已保存")}}))),_===null?null:a("div",{className:"ds-error",style:{marginTop:8}},_),D===""?null:a("div",{className:"ds-hint",style:{marginTop:8}},D));return a("div",{className:"ds-body"},a("div",{className:"ds-req-grid"},ie,ge))}function lw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,SelectField:s}=vt(),[o,i]=n.useState(30),[l,c]=n.useState(""),[u,p]=n.useState(null),[f,v]=n.useState(null),[x,b]=n.useState(null),[k,g]=n.useState(null),[d,h]=n.useState(null),[y,S]=n.useState(null);if(n.useEffect(()=>{let w=!0;const T=`/analytics?days=${o}${l===""?"":`&project=${encodeURIComponent(l)}`}`;r(T).then(R=>{w&&(p(R.analytics),v(null))}).catch(R=>{w&&v(R instanceof Error?R.message:String(R))});const K=l===""?"":`&project=${encodeURIComponent(l)}`;return Promise.all([r("/workload"),r(`/burndown?days=14${K}`),r("/costs"),r("/quality")]).then(([R,O,G,we])=>{w&&(b(R.workload??null),g(O.burndown??null),h({...G.costs??{},budget:G.budget??null,usage:G.usage??null}),S(we.quality??null))}).catch(()=>{}),()=>{w=!1}},[o,l,e.revision]),f!==null)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},f));if(u===null)return a("div",{className:"ds-loading"},"正在统计…");const C=w=>`${Math.round((w??0)*100)}%`,E=w=>{const T=Number(w)||0;return T<=0?"0":T*1440<60?`${Math.max(1,Math.round(T*1440))} 分钟`:T<1?`${(T*24).toFixed(1)} 小时`:`${T.toFixed(1)} 天`},_=(w,T,K,R)=>a("div",{className:"ds-kpi","data-warn":R===!0?"1":"0"},a("div",{className:"ds-kpi-value"},T),a("div",{className:"ds-kpi-label"},w),a("div",{className:"ds-kpi-hint"},K)),I=(u.cycle.buckets??[]).reduce((w,T)=>w+T.count,0),D=Math.max(1,...(u.throughput??[]).map(w=>w.count),...(u.cycle.buckets??[]).map(w=>w.count)),W=Math.max(.001,...(u.columns??[]).map(w=>w.avgDays)),M=(u.columns??[]).reduce((w,T)=>w===null||T.avgDays>w.avgDays?T:w,null),P=(u.throughput??[]).reduce((w,T)=>w+T.count,0),H=(x?.experts??[]).filter(w=>w.enabled),J=H.reduce((w,T)=>w+T.open,0),U=H.reduce((w,T)=>w+(T.overdue??0),0),F=k?.series??[],L=F.length===0?null:F[F.length-1];return a("div",{className:"ds-body"},a("div",{className:"ds-board-bar"},a("span",{className:"ds-hint"},`统计口径:最近 ${u.days} 天 · ${u.scope.tasks} 张卡(${u.scope.done} 张已完成)`),a(s,{className:"ds-select",value:l,style:{width:"auto",minWidth:130},onChange:w=>c(w.target.value)},a("option",{value:""},"全部项目"),(e.config.projects??[]).map(w=>a("option",{key:w.id,value:w.id},`${w.key?`[${w.key}] `:""}${w.name}`))),a(s,{className:"ds-select",value:String(o),style:{width:"auto",minWidth:96},onChange:w=>i(Number(w.target.value))},a("option",{value:"7"},"近 7 天"),a("option",{value:"30"},"近 30 天"),a("option",{value:"90"},"近 90 天"))),a("div",{className:"ds-an-sec"},"质量",a("span",{className:"ds-hint"},"先看这四个数:返工多不多、多久能做完、AI 交的东西有多少被人验收")),a("div",{className:"ds-kpi-grid"},_("一次通过率",C(u.rework.firstPassRate),"完成的卡里,从没被退回过的比例",u.rework.firstPassRate<.6),_("返工率",C(u.rework.rate),`${u.rework.reworked}/${u.rework.moved} 张卡倒退过`,u.rework.rate>.3),_("中位周期",E(u.cycle.medianDays),`平均 ${E(u.cycle.avgDays)} · P90 ${E(u.cycle.p90Days)}`),_("AI 交付物采纳率",C(u.ai.acceptedRate),`${u.ai.accepted}/${u.ai.artifactsAi} 件已被验收 · 占全部交付物 ${C(u.ai.aiShare)}`)),a("div",{className:"ds-an-sec"},"流程与产能",a("span",{className:"ds-hint"},"完成的卡各花了多久 · 时间都卡在哪一列 · 最近每天完成几张")),a("div",{className:"ds-an-grid"},a("div",{className:"ds-span-4",style:{display:"grid",gap:12,gridTemplateRows:"auto 1fr",alignContent:"stretch"}},a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"周期分布",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${I} 张已完成`)),a("div",{className:"ds-panel-body"},(u.cycle.buckets??[]).map(w=>a("div",{key:w.id,className:"ds-an-row"},a("span",{className:"ds-an-name"},w.label),a("div",{className:"ds-an-track","data-empty":w.count===0?"1":"0"},a("div",{className:"ds-an-fill",style:{width:`${Math.round(w.count/D*100)}%`}})),a("span",{className:"ds-an-num"},`${w.count}`))),I===0?a("div",{className:"ds-hint"},"这个区间里还没有完成的卡。"):null,a("div",{className:"ds-an-foot"},`平均 ${E(u.cycle.avgDays)} · 中位 ${E(u.cycle.medianDays)} · P90 ${E(u.cycle.p90Days)}`))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"近 14 天完成"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-spark"},(u.throughput??[]).map((w,T)=>a("div",{key:w.day,className:"ds-spark-col",title:`${w.day}:完成 ${w.count} 张`},a("div",{className:"ds-spark-bar",style:{height:`${Math.round(w.count/Math.max(1,Math.max(...u.throughput.map(K=>K.count)))*100)}%`}}),a("span",{className:"ds-spark-day"},T%2===0?w.day:"")))),a("div",{className:"ds-an-foot"},`近 14 天合计 ${P} 张 · 日均 ${(P/14).toFixed(1)} 张 · 空着的日子是没卡完成,不是没数据`)))),a("div",{className:"ds-panel ds-span-8"},a("div",{className:"ds-panel-head"},"每列停留(平均)"),a("div",{className:"ds-panel-body"},(u.columns??[]).map(w=>a("div",{key:w.status,className:"ds-an-row"},a("span",{className:"ds-an-name"},w.label),a("div",{className:"ds-an-track","data-empty":w.avgDays===0?"1":"0"},a("div",{className:"ds-an-fill ds-an-fill-warn",style:{width:`${Math.round(w.avgDays/W*100)}%`}})),a("span",{className:"ds-an-num"},E(w.avgDays)))),a("div",{className:"ds-an-foot"},M===null?"还没有数据。":`最慢的是「${M.label}」:平均 ${E(M.avgDays)},时间主要压在这一列。`)))),a("div",{className:"ds-an-sec"},"交付、成本与返工",a("span",{className:"ds-hint"},"AI 交的东西被验收多少 · 谁手上压得最多 · 派单花了多少 · 哪些卡来回折腾")),a("div",{className:"ds-an-grid"},a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"按专家看质量",a("span",{className:"ds-hint"},"派单量来自运行日志(内存最近 300 条)")),a("div",{className:"ds-panel-body"},y===null?a("div",{className:"ds-hint"},"还没算出来"):(y.experts??[]).length===0?a("div",{className:"ds-hint"},"还没有派出过活"):a("div",null,(y.experts??[]).slice(0,8).map(w=>a("div",{className:"ds-an-kv",key:w.expert},a("span",{},`${w.label} · ${w.dispatched} 次`),a("b",{"data-warn":w.reworkRate!==null&&w.reworkRate>=40?"1":"0"},`${w.done}/${w.cards} 完成 · 一次过 ${w.firstPassRate===null?"—":w.firstPassRate+"%"}`))),a("div",{className:"ds-hint",style:{marginTop:6}},`合计:派单 ${y.totals.dispatched} 次 · 平均耗时 ${y.totals.avgMs>0?Math.round(y.totals.avgMs/1e3)+" 秒":"—"} · 返工率 ${y.totals.reworkRate===null?"—":y.totals.reworkRate+"%"} · AI 失败 ${y.totals.failures} 次 · 缺陷重开 ${y.totals.reopens} 次`),a("div",{className:"ds-hint"},y.cost?.note??"")))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"AI 产出质量"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-an-kv"},a("span",{},"交付物由专家登记"),a("b",{},`${u.ai.artifactsAi} / ${u.ai.artifactsTotal}`)),a("div",{className:"ds-an-kv"},a("span",{},"其中被人验收"),a("b",{},`${u.ai.accepted}(${C(u.ai.acceptedRate)})`)),a("div",{className:"ds-an-kv"},a("span",{},"缺正文(只有标题)"),a("b",{"data-warn":u.ai.emptyBody>0?"1":"0"},`${u.ai.emptyBody}`)),a("div",{className:"ds-an-kv"},a("span",{},"写码卡有执行记录"),a("b",{},`${u.ai.codingTasks}`)),a("div",{className:"ds-an-kv"},a("span",{},"评审通过 / 要求修改"),a("b",{},`${u.ai.codingApproved} / ${u.ai.codingChanges}`)),a("div",{className:"ds-an-kv"},a("span",{},"单测通过"),a("b",{},`${u.ai.unitPassed}/${u.ai.unitTests}(${C(u.ai.unitPassRate)})`)),a("div",{className:"ds-an-kv"},a("span",{},"已合并"),a("b",{},`${u.ai.merged}`)))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"团队负载",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${x?.busy??0} 位在跑`)),a("div",{className:"ds-panel-body"},(x?.experts??[]).filter(w=>w.enabled).length===0?a("div",{className:"ds-empty"},"还没有启用的专家。"):(x?.experts??[]).filter(w=>w.enabled).map(w=>a("div",{key:w.id,className:"ds-an-row"},a("span",{className:"ds-an-name"},`${w.emoji} ${w.name}`),a("div",{className:"ds-an-track"},a("div",{className:"ds-an-fill",style:{width:`${Math.round(w.open/Math.max(1,...(x.experts??[]).map(T=>T.open))*100)}%`}})),a("span",{className:"ds-an-num"},`在办 ${w.open}${w.running>0?` · 跑 ${w.running}`:""}${w.overdue>0?` · 逾期 ${w.overdue}`:""}`))),a("div",{className:"ds-an-foot"},`${H.length} 位在岗,手上合计 ${J} 张${U>0?`,其中 ${U} 张逾期`:",没有逾期的"}`))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"燃尽(近 14 天剩余)",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`现在还剩 ${k?.now?.remaining??0} 张`)),a("div",{className:"ds-panel-body"},a(nw,{series:k?.series??[]}),a("div",{className:"ds-an-foot"},L===null?"还没有数据。":`累计建 ${L.created} · 累计完成 ${L.done} · 现在还剩 ${L.remaining} 张`))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"AI 执行成本",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${d?.total??0} 次派单`)),a("div",{className:"ds-panel-body"},d?.usage?.totals===null||d?.usage?.totals===void 0?null:a("div",{className:"ds-an-kv"},a("span",{},"token 用量"),a("b",{},(d.usage.totals.total/1e3).toFixed(1)+"k"),a("span",{className:"ds-hint",style:{marginLeft:8}},"入 "+(d.usage.totals.input/1e3).toFixed(1)+"k · 出 "+(d.usage.totals.output/1e3).toFixed(1)+"k · 缓存 "+(d.usage.totals.cacheRead/1e3).toFixed(1)+"k")),d?.usage?.totals===null||d?.usage?.totals===void 0?null:a("div",{className:"ds-an-kv"},a("span",{},"预估花费"),a("b",{},d.usage.totals.cost===null?"未知(价目表没配)":"¥"+Number(d.usage.totals.cost).toFixed(2)),(d.usage.totals.unknownCalls??0)>0?a("span",{className:"ds-hint",style:{marginLeft:8}},d.usage.totals.unknownCalls+" 次无价目"):null),a("div",{className:"ds-an-kv"},a("span",{},"平均执行时长"),a("b",{},`${Math.round((d?.avgMs??0)/1e3)} 秒`)),(d?.byModel??[]).slice(0,3).map(w=>a("div",{key:`m-${w.key}`,className:"ds-an-kv"},a("span",{},`模型 ${w.key}`),a("b",{},`${w.runs} 次`))),(d?.byPromptVersion??[]).slice(0,3).map(w=>a("div",{key:`p-${w.key}`,className:"ds-an-kv"},a("span",{},`提示词 ${w.key}`),a("b",{},`${w.runs} 次`))),a("div",{className:"ds-hint"},d?.note??""),d?.budget===null||d?.budget===void 0?null:a("div",{style:{marginTop:8}},a("div",{className:"ds-an-kv"},a("span",{},"预算使用"),a("b",{style:{color:d.budget.level==="over"?"#e5484d":d.budget.level==="warn"?"#d97706":void 0}},`${Math.max(d.budget.runRate,d.budget.hourRate)}%`)),a(aw,{value:Math.min(100,Math.max(d.budget.runRate,d.budget.hourRate)),tone:d.budget.level}),a("div",{className:"ds-hint"},`${d.budget.runs}/${d.budget.budgetRuns} 次 · ${d.budget.hours}/${d.budget.budgetHours} 小时 · ${d.budget.hint}`)))),a("div",{className:"ds-panel ds-span-8"},a("div",{className:"ds-panel-head"},"返工最多的卡",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`评审打回 ${u.rework.reviewRework} 次 · 测试打回 ${u.rework.testRework} 次`)),a("div",{className:"ds-panel-body"},(u.rework.worst??[]).length===0?a("div",{className:"ds-hint"},"没有卡倒退过,挺好。"):u.rework.worst.map(w=>a("div",{key:w.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${w.code===""?"":`${w.code} `}${w.title}`),a("div",{className:"ds-hint"},`倒退 ${w.backward} 次 · 现在在「${(e.statuses.find(T=>T.id===w.status)??{}).label??w.status}」`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(w.id)},"打开"))))))))}function dw(e,t){const a="studio",n="/studio-api";let r=null;function s(Y){r=Y??null}function o(Y){return r===null?null:r[Y]??null}function i(Y,ne){if(ne==null)return ne;const ee=ne.props??{},be=ee.title!==void 0?ne:t(ne.type,{...ee,title:Y}),se=o("Tip");return se===null?be:t(se,{key:ne.key,label:Y},be)}const{TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f}=px({h:t,kit:o,React:e}),v=fx({h:t,kit:o}),x=Lx({h:t,React:e}),b=mx({h:t,ChipField:u}),k=gx({h:t,React:e,Card:b,TextField:l,SelectField:f,CheckField:p,toast:H});function g({rows:Y=6,value:ne,onChange:ee,placeholder:be,className:se,...de}){const[Q,ve]=e.useState(!1),Ce=String(ne??"");return t("div",{className:"ds-md-field"},t("div",{className:"ds-md-bar"},t("span",{className:"ds-hint"},"支持 Markdown:标题 / 列表 / 粗体 / 代码块 / 表格"),t("button",{className:"ds-btn ds-btn-ghost",type:"button",onClick:()=>ve(!Q)},Q?"回到编辑":"预览渲染")),Q?t("div",{className:"ds-md-preview ds-req-desc ds-md"},Ce.trim()===""?t("span",{className:"ds-hint"},"还没写内容。"):Ee(Ce)):t(c,{className:se??"ds-textarea",rows:Y,value:ne,onChange:ee,placeholder:be,...de}))}function d(Y,ne){const ee=t("div",{className:"ds-label",style:{cursor:"help"}},Y);return ne==null||ne===""?ee:i(ne,ee)}let h=null,y=null;function S(Y){h=Y,typeof y=="function"&&y(Y)}function C(Y,ne){const ee=ne??{};return o("DialogBox")===null?Promise.resolve(window.confirm(String(Y))):new Promise(se=>{S({kind:"confirm",title:ee.title??"确认操作",message:String(Y??""),danger:ee.danger===!0,okText:ee.okText??"确定",cancelText:ee.cancelText??"取消",resolve:se})})}function E(Y,ne){const ee=ne??{};return o("DialogBox")===null?Promise.resolve(window.prompt(String(Y),ee.defaultValue??"")):new Promise(se=>{S({kind:"prompt",title:ee.title??"需要填写",message:String(Y??""),value:String(ee.defaultValue??""),placeholder:ee.placeholder??"",okText:ee.okText??"确定",cancelText:"取消",resolve:se})})}function _(){const[Y,ne]=e.useState(null),[ee,be]=e.useState("");e.useEffect(()=>{const Q=ve=>{ne(ve),be(ve!==null&&ve.kind==="prompt"?ve.value:"")};return y=Q,h!==null&&Q(h),()=>{y===Q&&(y=null)}},[]);const se=o("DialogBox");if(Y===null||se===null)return null;const de=Q=>{const ve=Y.resolve;ne(null),h=null,ve(Q)};return t(se,{open:!0,title:Y.title,message:Y.message,danger:Y.danger===!0,okText:Y.okText,cancelText:Y.cancelText,input:Y.kind==="prompt",value:ee,placeholder:Y.placeholder,onChange:Q=>be(Q),onOk:()=>de(Y.kind==="prompt"?ee.trim():!0),onCancel:()=>de(Y.kind==="prompt"?null:!1)})}let I=0,D=[],W=[],M={text:"",at:0};function P(){const Y=D.slice();for(const ne of W.slice())ne(Y)}function H(Y,ne="success",ee=3200){const be=String(Y??"").trim();if(be==="")return null;const se=Date.now();if(M.text===be&&se-M.at<600)return null;M={text:be,at:se};const de=++I;return D=D.concat([{id:de,text:be,tone:ne}]),P(),window.setTimeout(()=>{J(de)},ee),de}function J(Y){const ne=D.filter(ee=>ee.id!==Y);ne.length!==D.length&&(D=ne,P())}const U={success:"✓",error:"!",info:"i"};function F(){const[Y,ne]=e.useState(()=>D.slice());return e.useEffect(()=>(W.push(ne),()=>{W=W.filter(ee=>ee!==ne)}),[]),Y.length===0?null:t("div",{className:"ds-toasts",role:"status","aria-live":"polite"},Y.map(ee=>t("div",{key:ee.id,className:"ds-toast","data-tone":ee.tone},t("span",{className:"ds-toast-icon","aria-hidden":"true"},U[ee.tone]??"i"),t("span",{className:"ds-toast-text"},ee.text),t("button",{type:"button",className:"ds-toast-close","aria-label":"关闭提示",onClick:()=>J(ee.id)},"×"))))}const L="dsh-studio:user";function w(){try{const Y=window.localStorage.getItem(L);if(typeof Y=="string"&&Y.trim()!=="")return Y.trim().slice(0,40)}catch{}return"人"}function T(Y){const ne=String(Y??"").trim().slice(0,40)||"人";try{window.localStorage.setItem(L,ne)}catch{}return ne}const K="dsh-studio:theme",R=[["light","☀ 明亮"],["dark","☾ 黑暗"],["system","跟随系统"]];function O(){return typeof window.matchMedia=="function"?window.matchMedia("(prefers-color-scheme: dark)"):null}function G(){try{const Y=window.getComputedStyle(document.body).backgroundColor,ne=/rgba?\(([^)]+)\)/.exec(String(Y??""));if(ne===null)return null;const ee=ne[1].split(",").map(ve=>Number(ve.trim())),[be,se,de,Q]=ee;return![be,se,de].every(ve=>Number.isFinite(ve))||Number.isFinite(Q)&&Q<.2?null:.2126*be+.7152*se+.0722*de<128?"dark":"light"}catch{return null}}function we(){try{const Y=window.localStorage.getItem(K);if(Y==="light"||Y==="dark"||Y==="system")return Y}catch{}return"system"}function fe(Y){const ne=Y==="light"||Y==="dark"?Y:"system";try{window.localStorage.setItem(K,ne)}catch{}return ne}const ye="dsh-studio:session",He="dsh-studio:auth-user";function ae(){try{const Y=window.localStorage.getItem(ye);if(typeof Y=="string"&&Y.trim()!=="")return Y.trim()}catch{}return""}function z(Y){try{Y===""||Y===null||Y===void 0?window.localStorage.removeItem(ye):window.localStorage.setItem(ye,String(Y))}catch{}}function te(){try{const Y=window.localStorage.getItem(He);if(typeof Y=="string"&&Y!==""){const ne=JSON.parse(Y);if(ne!==null&&typeof ne=="object")return ne}}catch{}return null}function xe(Y){try{Y==null?window.localStorage.removeItem(He):window.localStorage.setItem(He,JSON.stringify(Y))}catch{}return Y??null}async function X(Y,ne){const ee={"x-studio-user":encodeURIComponent(w())},be=ae();be!==""&&(ee["x-studio-session"]=be),ne!==void 0&&(ee["content-type"]="application/json");const se=ne===void 0?{method:"GET",headers:ee}:{method:"POST",headers:ee,body:JSON.stringify(ne)},de=await fetch(n+Y,se);let Q={};try{Q=await de.json()}catch{}if(!de.ok||Q.ok===!1)throw new Error(Q.error??`请求失败(HTTP ${de.status})`);return Q}const ie=`
|
|
47
47
|
html[data-dsh-theme="dark"] body { background: #0e1014 !important; color: #e6e8ee }
|
|
48
48
|
html[data-dsh-theme="dark"] [class*="frame"],
|
|
49
49
|
html[data-dsh-theme="dark"] [class*="centerCol"],
|