@marsken/dsh-studio 0.1.0 → 0.1.1

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/dist/client.js CHANGED
@@ -25,7 +25,7 @@ GET /api/points | 200 |`,onChange:m=>le({...Z,steps:m.target.value})}),e("div",{
25
25
  按边界值、异常流、并发与幂等设计用例,列出实际执行结果。`}];for(const K of O)await s("/experts/save",{expert:{name:`${I}${K.name}`,emoji:K.emoji,role:K.role,domain:I,description:`负责${I}业务线的${K.name}工作`,prompt:K.prompt,model:"",preset:"",enabled:!0}});L(""),await t()}catch(O){y(O instanceof Error?O.message:String(O))}finally{C(!1)}}},$=I=>{const O=String(I??"").trim();if(O==="")return"";const K=O.replace(/\s*\n\s*/g," / ");return K.length>96?`${K.slice(0,96)}…`:K},Y=k.map(I=>a("div",{className:"ds-listrow",key:I.id},a("div",{className:"ds-listrow-main"},a("div",{},`${I.emoji} ${I.name}`,I.domain?a(f,{className:"ds-chip"},I.domain):null,I.enabled===!1?a("span",{className:"ds-hint"}," (已停用)"):null),a("div",{className:"ds-hint"},`${I.model||"跟随默认模型"}${I.description?" · "+I.description:""}`),$(I.prompt)===""?a("div",{className:"ds-hint","data-empty":"1"},"提示词:还没有写,会退回内置角色说明(点「编辑」补上)"):a("div",{className:"ds-hint","data-prompt":"1",title:String(I.prompt??""),style:{cursor:"pointer"},onClick:()=>d({...I})},`提示词:${$(I.prompt)}`),(I.skills??[]).length>0?a("div",{className:"ds-hint"},`技能:${I.skills.join("、")}`):null),a("div",{className:"ds-listrow-actions"},l("编辑这位专家",a("button",{className:"ds-btn",disabled:S,onClick:()=>d({...I})},"编辑")),l("删除这位专家",a("button",{className:"ds-btn",disabled:S,onClick:()=>void R(I),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(r,{title:"专家(可在评论里 @ 他们)",hint:"每个专家有名字、业务领域、角色提示词和技能;评论里 @名字 就会把这条工作派给他。",rows:Y,onAdd:()=>d({id:"",name:"",emoji:"🧩",role:"",domain:"",description:"",prompt:"",skills:[],model:"",preset:"",sandbox:"",enabled:!0}),addLabel:"新增专家",render:I=>I,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:G,placeholder:"支付 / 风控 / 会员",onChange:I=>L(I.target.value)}),a("button",{className:"ds-btn",disabled:S||G.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:I=>A("name",I.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(I=>a("button",{key:I,type:"button",className:"ds-emoji","data-on":g.emoji===I?"1":"0",title:I,onClick:()=>A("emoji",I)},I))),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:I=>A("emoji",I.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:I=>A("description",I.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:I=>A("role",I.target.value)},a("option",{value:""},"不限"),["product","architect","dev","qa"].map(I=>a("option",{key:I,value:I},Aa[I]??I))),a("div",{className:"ds-hint"},"决定默认接哪一列")),a("div",{className:"ds-field",style:{flex:1}},c("业务领域","「按专长匹配」看这里:领域相同的卡优先派给它"),a(u,{className:"ds-input",value:g.domain??"",placeholder:"支付 / 风控 / 会员(选填)",onChange:I=>A("domain",I.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(I=>{const O=(g.skills??[]).includes(I.name),K=k.filter(Ne=>(Ne.skills??[]).includes(I.name)).length;return a("label",{key:I.name,className:"ds-skillopt","data-on":O?"1":"0",title:String(I.description??"")},a(v,{checked:O,onChange:()=>J(I.name)}),a("span",{className:"ds-skillname"},I.name),a("span",{className:"ds-skilldesc"},String(I.description??"").slice(0,46)),K>0?a("span",{className:"ds-skillusers"},`${K} 位在用`):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:q,placeholder:"技能库以外的技能名,例如 lark-doc、office-xlsx(回车或点「加上」)",onChange:I=>F(I.target.value),onKeyDown:I=>{I.key==="Enter"&&(I.preventDefault(),M())}}),a("button",{className:"ds-btn",onClick:()=>M()},"加上")),(g.skills??[]).filter(I=>!P.some(O=>O.name===I)).length>0?a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginTop:6,alignItems:"center"}},a("span",{className:"ds-hint"},"不在技能库(仍会写进提示词):"),(g.skills??[]).filter(I=>!P.some(O=>O.name===I)).map(I=>a(f,{key:I,className:"ds-chip","data-missing":"1"},I,a("button",{className:"ds-chip-x",title:"移除",onClick:()=>A("skills",(g.skills??[]).filter(O=>O!==I))},"×")))):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:I=>A("model",I.target.value)})),a("div",{className:"ds-field",style:{flex:1}},c("Agent 预设","预设决定这个专家能用哪些工具、以什么身份干活(如 standard、架构师)"),a(u,{className:"ds-input",value:g.preset,placeholder:"standard",onChange:I=>A("preset",I.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:I=>A("sandbox",I.target.value)},[["","跟随全局"],["read-only","只读"],["workspace-write","只写工作区"],["danger-full-access","无沙箱"]].map(([I,O])=>a("option",{key:I,value:I},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:`例如:
26
26
  你是支付域的产品经理。先读 docs/ 与支付相关的 ADR,再动手。
27
27
  需求必须写清:目标用户、主流程、异常与退款路径、对账口径。
28
- 不确定的地方去代码或历史工单里找证据,禁止编造接口字段。`,onChange:I=>A("prompt",I.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:I=>A("enabled",I.target.checked)}),a("span",{},"启用这个专家")))))}function jx({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}=ht(),k=e.teams??[],g=e.experts??[],[d,h]=n.useState(null),[y,S]=n.useState(null),[C,E]=n.useState(!1),[_,R]=n.useState({});n.useEffect(()=>{let q=!0;return s("/team/memory",{}).then(F=>{q&&R(F?.memory??{})}).catch(()=>{}),()=>{q=!1}},[e.teams]);const A=async q=>{if(await i("清空「"+q.name+"」的团队记忆?",{title:"清空团队记忆",danger:!0,okText:"清空"}))try{const F=await s("/team/memory/clear",{teamId:q.id});R(F?.memory??{}),o("已清空这个团的记忆")}catch(F){S(F instanceof Error?F.message:String(F))}},G=q=>{const F=g.find(M=>M.id===q);return F===void 0?q:`${F.emoji} ${F.name}`},L=async()=>{if(d!==null){E(!0),S(null);try{await s("/teams/save",{team:d}),h(null),await t()}catch(q){S(q instanceof Error?q.message:String(q))}finally{E(!1)}}},P=async q=>{if(await i(`删除专家团「${q.name}」?`,{title:"删除专家团",danger:!0,okText:"删除"})){E(!0),S(null);try{await s("/teams/delete",{id:q.id}),await t()}catch(F){S(F instanceof Error?F.message:String(F))}finally{E(!1)}}},W=(q,F)=>h({...d,[q]:F}),J=k.map(q=>a("div",{className:"ds-listrow",key:q.id},a("div",{className:"ds-listrow-main"},a("div",{},`👥 ${q.name}`),a("div",{className:"ds-hint"},`团长 ${G(q.lead)} · 成员 ${q.members.map(G).join("、")}`),a("div",{className:"ds-hint"},(_[q.id]??[]).length===0?"🧠 团队记忆:还没有(卡做完会自动记一条,派活时带进提示词)":`🧠 团队记忆 ${_[q.id].length} 条 · 最近:${_[q.id][0]?.outcome??""} ${_[q.id][0]?.title??""}`)),a("div",{className:"ds-listrow-actions"},l("清空这个团的记忆",a("button",{className:"ds-btn ds-btn-ghost",disabled:C||(_[q.id]??[]).length===0,onClick:()=>void A(q)},"清空记忆")),l("编辑这个专家团",a("button",{className:"ds-btn",disabled:C,onClick:()=>h({...q})},"编辑")),l("删除这个专家团",a("button",{className:"ds-btn",disabled:C,onClick:()=>void P(q),"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:q=>q,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 L()},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:q=>W("name",q.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"这个团负责什么"),a(u,{className:"ds-input",value:d.description,placeholder:"从需求澄清一路做到验收",onChange:q=>W("description",q.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(q=>a("label",{className:"ds-row",key:q.id,style:{alignItems:"center",gap:6,cursor:"pointer"}},a(v,{checked:(d.members??[]).includes(q.id),onChange:F=>W("members",F.target.checked?[...d.members??[],q.id]:(d.members??[]).filter(M=>M!==q.id))}),a("span",{},`${q.emoji} ${q.name}`))))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"团长(负责拆活和汇总)"),a(x,{className:"ds-input",value:d.lead??"",onChange:q=>W("lead",q.target.value)},a("option",{value:""},"自动(第一个成员)"),(d.members??[]).map(q=>a("option",{key:q,value:q},G(q)))))))}function Dx({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}=ht(),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)),[_,R]=n.useState(!1),[A,G]=n.useState(null),[L,P]=n.useState(null),W=async $=>{R(!0),G(null),P(null);try{await s("/config",{testing:{envs:$.envs??g,defaultEnv:$.defaultEnv!==void 0?$.defaultEnv:k.defaultEnv??"",timeoutMs:$.timeoutMs!==void 0?$.timeoutMs:k.timeoutMs??15e3,allowHosts:$.allowHosts!==void 0?$.allowHosts:Array.isArray(k.allowHosts)?k.allowHosts:[],concurrency:k.concurrency??4}}),P("已保存"),o("已保存"),await t()}catch(Y){G(Y instanceof Error?Y.message:String(Y))}finally{R(!1)}},J=async()=>{if(d===null)return;const $=String(d.name??"").trim(),Y=String(d.baseUrl??"").trim();if($===""){G("环境名不能为空");return}const I={id:d.id??"",name:$,baseUrl:Y},O=d.id===void 0||d.id===""?[...g,I]:g.map(K=>K.id===d.id?{...K,...I}:K);h(null),await W({envs:O,defaultEnv:k.defaultEnv===""?"":k.defaultEnv})},q=async $=>{if(!await i(`删除环境「${$.name}」?`,{title:"删除环境",danger:!0,okText:"删除"}))return;const Y=g.filter(I=>I.id!==$.id);await W({envs:Y,defaultEnv:k.defaultEnv===$.id?Y[0]?.id??"":k.defaultEnv})},F=async()=>{const $=y.split(/[,,\s]+/).map(I=>I.trim()).filter(I=>I!==""),Y=Number(C);await W({allowHosts:$,timeoutMs:Number.isFinite(Y)&&Y>0?Y:15e3})},M=$=>a("div",{key:$.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${$.name}${k.defaultEnv===$.id?" · 默认":""}`),a("div",{className:"ds-hint"},$.baseUrl===""?"相对地址会因为没有 baseUrl 而失败":$.baseUrl)),a("div",{className:"ds-listrow-actions"},k.defaultEnv===$.id?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>void W({defaultEnv:$.id})},"设为默认"),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>h({...$})},"编辑"),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>void q($),"data-danger":"1"},"删除"))),w=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:$=>h({...d,name:$.target.value})})),a("div",{className:"ds-field"},c("baseUrl","接口测试的根地址,用例里的 /path 会拼在它后面"),a(u,{className:"ds-input",value:d.baseUrl??"",placeholder:"https://test.example.com",onChange:$=>h({...d,baseUrl:$.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 J()},"保存环境"))));return a("div",{className:"ds-pane-stack"},w,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(M))),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:$=>S($.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"单条用例超时(毫秒)"),a(u,{className:"ds-input",value:C,onChange:$=>E($.target.value)})),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void F()},_?"保存中…":"保存约束")),L===null?null:a("div",{className:"ds-ok"},L),A===null?null:a("div",{className:"ds-err"},A))))}function Ox({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}=ht(),k=e.feishu??{},[g,d]=n.useState(""),[h,y]=n.useState(String(k.escalateAfter??3)),[S,C]=n.useState(!1),[E,_]=n.useState(null),[R,A]=n.useState(null),G=async()=>{C(!0),_(null),A(null);try{const L={};String(g??"").trim()!==""&&(L.escalateWebhook=String(g).trim()),L.escalateAfter=Number(h)||3,await s("/config",{feishu:L}),d(""),_("已保存:连续失败 "+(Number(h)||3)+" 次后往升级群发一条"),await t()}catch(L){A(L instanceof Error?L.message:String(L))}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:L=>d(L.target.value)})),a("div",{className:"ds-field"},c("连续失败几次后升级","同一通道连续失败到这个次数,就给升级群发消息"),a(u,{className:"ds-input",style:{width:90},value:h,onChange:L=>y(L.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 G()},S?"保存中…":"保存升级设置")),E===null?null:a("div",{className:"ds-ok"},E),R===null?null:a("div",{className:"ds-err"},R)))}function Lx({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}=ht(),[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),[_,R]=n.useState(null),A=async J=>{R(null);try{await s("/config",{releaseRules:J}),R("发布准入已更新"),await t()}catch(q){R(q instanceof Error?q.message:String(q))}},G=()=>g(J=>J.length>=12?J:[...J,{id:"",name:"",kind:"other",note:""}]),L=(J,q)=>g(F=>F.map((M,w)=>w===J?{...M,...q}:M)),P=J=>g(q=>q.filter((F,M)=>M!==J)),W=async()=>{h(!0),S(null),E(null);try{const J=k.map(q=>({...q,name:String(q.name??"").trim()})).filter(q=>q.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,q)=>a("div",{key:`env-${q}`,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=>L(q,{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=>L(q,{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=>L(q,{note:F.target.value})})),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>P(q),"data-danger":"1"},"删除"))),a("div",{className:"ds-row",style:{gap:8,marginTop:10}},a("button",{className:"ds-btn",onClick:G,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 W()},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 A({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 A({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 Mx({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}=ht(),f=e.ldap??{},[v,x]=p.useState({enabled:f.enabled===!0,url:f.url??"ldap://ldap.laiyifen.com: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([]),R=e.localAuth??{},[A,G]=p.useState(""),[L,P]=p.useState(""),[W,J]=p.useState(!1),[q,F]=p.useState(null),[M,w]=p.useState(null),$=async()=>{J(!0),F(null),w(null);try{await r("/auth/password",{username:R.username,currentPassword:A,newPassword:L}),G(""),P(""),F("密码已更新"),t()}catch(K){w(K instanceof Error?K.message:String(K))}finally{J(!1)}},Y=K=>x(Ne=>({...Ne,...K})),I=async()=>{d(!0),y(null),C(null),_([]);try{const K=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});_(K.steps??[]),y(K.message??"连接正常"),s(K.message??"连接正常",K.ok===!1?"error":"success")}catch(K){const Ne=K instanceof Error?K.message:String(K);C(Ne)}finally{d(!1)}},O=async()=>{k(!0),y(null),C(null);try{const K={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()!==""&&(K.ldap.bindPassword=v.bindPassword.trim()),await r("/config",K),Y({bindPassword:""}),y(v.bindPassword.trim()===""?"已保存(服务账号密码没改)":"已保存,服务账号密码已写入密钥文件"),await t()}catch(K){C(K instanceof Error?K.message:String(K))}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:K=>Y({enabled:K.target.checked})}),a("span",{className:"ds-check-text"},"启用 LDAP 登录")),a("label",{className:"ds-check",style:{marginTop:6}},a(c,{checked:v.requireLogin,onChange:K=>Y({requireLogin:K.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:K=>Y({url:K.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:K=>Y({timeoutMs:K.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:K=>Y({baseDN:K.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:K=>Y({bindDN:K.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:K=>Y({bindPassword:K.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:K=>Y({userFilter:K.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:K=>Y({nameAttr:K.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"邮箱属性"),a(l,{className:"ds-input",value:v.mailAttr,onChange:K=>Y({mailAttr:K.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"部门属性"),a(l,{className:"ds-input",value:v.deptAttr,onChange:K=>Y({deptAttr:K.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:10}},a("button",{className:"ds-btn",disabled:g,onClick:()=>void I()},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((K,Ne)=>a("div",{key:`ldap-step-${Ne}`,className:K.ok===!0?"ds-ok":"ds-error"},`${K.ok===!0?"✓":"✗"} ${K.name}:${K.message}${K.ms>0?`(${K.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}},`账号:${R.username??"admin"} · ${R.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:A,onChange:K=>G(K.target.value)})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"新密码(至少 8 位)"),a(l,{className:"ds-input",type:"password",value:L,onChange:K=>P(K.target.value)})),a("button",{className:"ds-btn",disabled:W||A===""||L.length<8,onClick:()=>void $()},W?"提交中…":"改密码")),q===null?null:a("div",{className:"ds-ok",style:{marginTop:6}},q),M===null?null:a("div",{className:"ds-error",style:{marginTop:6}},M)),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(K=>a("div",{key:K.id,className:"ds-kv"},a("div",null,K.name),a("div",null,`${K.id} · ${K.dept===""?"(无部门)":K.dept} · 登录 ${K.loginCount} 次 · 最近 ${Ye(K.lastLoginAt)}`))))))}function zx({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}=ht(),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),R=async()=>{y(!0),C(null),_(null);try{const A=Number(v),G=Number(b),L=Number(g);if(!Number.isFinite(A)||A<=0)throw new Error("预算次数要是正整数");if(!Number.isFinite(G)||G<=0)throw new Error("预算小时要是正整数");await r("/config",{costs:{budgetRuns:Math.round(A),budgetHours:Math.round(G),warnAt:Number.isFinite(L)?Math.min(100,Math.max(10,Math.round(L))):80}}),C("已保存:预算"),await t()}catch(A){_(A instanceof Error?A.message:String(A))}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:A=>x(A.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"每月机上时长上限(小时)"),a(l,{className:"ds-input",value:b,onChange:A=>k(A.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"警告阈值(百分比)"),a(l,{className:"ds-input",value:g,onChange:A=>d(A.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 R()},h?"保存中…":"保存预算")),S===null?null:a("div",{className:"ds-ok"},S),E===null?null:a("div",{className:"ds-err"},E)))}function Fx({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}=ht(),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)}}},R=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($){g($ instanceof Error?$.message:String($))}finally{S(!1)}}},A=async w=>{if(C.name.trim()!==""){S(!0),g(null);try{await r("/projects/iteration",{project:w.id,name:C.name.trim(),start:Pr(C.start),due:Pr(C.due),status:C.status}),E({name:"",start:"",due:"",status:"planned"}),await t()}catch($){g($ instanceof Error?$.message:String($))}finally{S(!1)}}},G=async(w,$)=>{S(!0),g(null);try{await r("/projects/iteration",{project:w.id,action:"remove",iteration:$.id}),await t()}catch(Y){g(Y instanceof Error?Y.message:String(Y))}finally{S(!1)}},L=(w,$)=>b({...x,[w]:$}),P=(w,$,Y)=>b({...x,services:(x.services??[]).map((I,O)=>O===w?{...I,[$]:Y}:I)}),W=()=>b({...x,services:[...x.services??[],{id:"",name:"",repoId:"",repoPath:"",baseBranch:x.baseBranch||"main",dependsOn:[]}]}),J=w=>b({...x,services:(x.services??[]).filter(($,Y)=>Y!==w)}),q=x===null?[]:x.services??[],F=(w,$)=>b({...x,services:(x.services??[]).map((Y,I)=>{if(I!==w)return Y;const O=Array.isArray(Y.dependsOn)?Y.dependsOn:[];return{...Y,dependsOn:O.includes($)?O.filter(K=>K!==$):[...O,$]}})}),M=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} · ${Wl[w.status]??w.status}${w.lead?` · 负责人 ${w.lead}`:""}`),a("div",{className:"ds-hint"},`${(e.projects??[]).find($=>$.id===w.id)?.iterations?.length??0} 个迭代 · ${(w.services??[]).length} 个服务${(()=>{const $=(w.services??[]).reduce((Y,I)=>Y+(I.dependsOn??[]).length,0);return $>0?` · ${$} 条依赖`:""})()}${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 R(w),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(n,{title:"业务项目(一个项目可以挂很多张卡和很多个迭代)",hint:"项目是看板的第一层分组:卡片挂项目,项目下面划迭代,看板可按项目和迭代筛选,项目页签看每个项目的体检数据。",rows:M,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=>L("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=>L("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=>L("status",w.target.value)},Object.keys(Wl).map(w=>a("option",{key:w,value:w},Wl[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=>L("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:"/home/lyf/repos/order-center",onChange:w=>L("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=>L("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"},`服务(${q.length})· 一个服务一个 git 路径,卡片可以指定改哪个`),q.length===0?a("div",{className:"ds-empty"},"还没有分服务。只有一个仓库时留空即可,走上面的默认代码目录。"):q.map((w,$)=>{const Y=q.filter((I,O)=>O!==$&&String(I.id??"").trim()!=="");return a("div",{key:`${w.id}-${$}`,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:I=>P($,"name",I.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:"/home/lyf/repos/order-service",onChange:I=>P($,"repoPath",I.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:I=>P($,"baseBranch",I.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:I=>P($,"repoId",I.target.value)},a("option",{value:""},"不关联"),v.map(I=>a("option",{key:I.id,value:I.id},I.name||I.project)))),a("button",{className:"ds-btn",type:"button",onClick:()=>J($),"data-danger":"1"},"删除")),Y.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}},"(勾上的服务要先完成,本服务的卡才能收口)"),Y.map(I=>a("span",{key:I.id,className:"ds-chip","data-on":(w.dependsOn??[]).includes(I.id)?"1":"0",title:`依赖 ${I.name||I.id}`,onClick:()=>F($,I.id)},I.name||I.id))))}),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn",type:"button",onClick:()=>W()},"+ 加一个服务"),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=>L("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} · ${Hl[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 G(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(Hl).map(w=>a("option",{key:w,value:w},Hl[w])))),a("button",{className:"ds-btn",disabled:y||C.name.trim()==="",onClick:()=>void A(x)},"+ 加迭代")),a("div",{className:"ds-hint"},"迭代改完会立刻反映到看板的迭代筛选。"))))}function Bx({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}=ht(),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(""),[_,R]=v.useState(!1),[A,G]=v.useState(""),L=async(w,$)=>{h(!0),S(null);try{await s(w,$),await a()}catch(Y){{const I=Y instanceof Error?Y.message:String(Y);S(I),o(I,"error")}}finally{h(!1)}},P=async()=>{const w=k.name.trim();w!==""&&(await L("/access/save",{member:{name:w,role:k.role}}),g({name:"",role:"member"}))},W=Array.from(new Set(b.map(w=>w.by))).sort(),J=A.trim().toLowerCase(),q=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=q.slice(0,120),M=()=>{const w=new URLSearchParams({by:C,q:A.trim(),failed:_?"1":"0",token:r()??""}),$=document.createElement("a");$.href=`${API}/audit/export.csv?${w.toString()}`,$.rel="noopener",document.body.appendChild($),$.click(),document.body.removeChild($),o(`正在导出 ${q.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 L("/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 L("/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($=>$.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:$=>void L("/access/save",{member:{name:w.name,role:$.target.value}})},ACCESS_ROLE_OPTIONS.map($=>n("option",{key:$.id,value:$.id},$.label))),n("button",{className:"ds-btn ds-btn-ghost",disabled:d,onClick:()=>void L("/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} / 命中 ${q.length} / 共 ${b.length} 条)`),n("div",{className:"ds-row",style:{gap:8,marginLeft:"auto"}},n(c,{className:"ds-input",placeholder:"搜动作 / 人 / 详情",value:A,style:{width:170},onChange:w=>G(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__"},"所有人"),W.map(w=>n("option",{key:w,value:w},w))),n("label",{className:"ds-check"},n(p,{checked:_,onChange:w=>R(w.target.checked)}),n("span",null,"只看失败")),n("button",{className:"ds-btn ds-btn-ghost",disabled:d,onClick:async()=>{await l("清空审计日志?清掉之后所有操作记录都没了。",{title:"清空审计日志",danger:!0,okText:"清空"})&&L("/audit/clear",{})}},"清空"),n("button",{className:"ds-btn ds-btn-ghost",disabled:q.length===0,onClick:M,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"},Ye(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 Ux({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}=ht(),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(()=>ef(v),[v]),_=F=>{const M=v.columns?.[F];return M==null?[]:Array.isArray(M.members)?M.members:M.kind==="none"?[]:[{kind:M.kind,id:M.id}]},R=F=>v.columns?.[F]?.strategy??"round_robin",A=F=>{if(F.kind==="role")return"🎯 "+(Aa[F.id]??F.id);if(F.kind==="team"){const w=(v.teams??[]).find($=>$.id===F.id);return w===void 0?`👥 ${F.id}`:`${w.emoji??"👥"} ${w.name}`}const M=(v.experts??[]).find(w=>w.id===F.id);return M===void 0?F.id:`${M.emoji} ${M.name}`},G=Array.isArray(v.design?.columns)?v.design.columns:["dev"],L=async F=>{g(!0),b(null);try{await r("/config",{design:{...v.design??{},...F}}),await t()}catch(M){b(M instanceof Error?M.message:String(M))}finally{g(!1)}},P=F=>{const M=G.includes(F)?G.filter(w=>w!==F):[...G,F];L({columns:M})},W=async(F,M,w)=>{g(!0),b(null);try{await r("/columns/save",{status:F,members:M,strategy:w}),await t()}catch($){b($ instanceof Error?$.message:String($))}finally{g(!1)}},J=(F,M)=>{const w=M.key.startsWith("t:")?"team":M.key.startsWith("r:")?"role":"expert",$=M.key.slice(2),Y=_(F);Y.some(I=>I.kind===w&&I.id===$)||(S(""),W(F,[...Y,{kind:w,id:$}],R(F)))},q=(F,M)=>void W(F,_(F).filter((w,$)=>$!==M),R(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 L({enabled:v.design?.enabled!==!0})},v.design?.enabled===!0?"设计闸门:已开启":"设计闸门:已关闭"),C.map(F=>a("button",{key:"gate-"+F.id,className:G.includes(F.id)?"ds-btn ds-btn-primary":"ds-btn",disabled:k,onClick:()=>P(F.id)},F.label??F.id))),C.map(F=>{const M=_(F.id),w=R(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),M.length===0?a("span",{className:"ds-hint"},"👤 等人认领(不自动派)"):M.map(($,Y)=>a(c,{className:"ds-chip ds-chip-owner",key:`${$.kind}:${$.id}`},A($),a("button",{className:"ds-chip-x",title:"从候选里去掉",disabled:k,onClick:()=>q(F.id,Y)},"×"))),a("button",{className:"ds-btn",style:{marginLeft:"auto"},disabled:k,onClick:()=>{S(""),h($=>$===F.id?null:F.id)}},d===F.id?"收起":"+ 加候选"),M.length>=2?a(p,{className:"ds-input",style:{width:240},value:w,disabled:k,onChange:$=>void W(F.id,M,$.target.value)},STRATEGIES.map($=>a("option",{key:$.id,value:$.id},$.label))):null),d===F.id?a("div",{className:"ds-colpick"},a(l,{className:"ds-mention-search",autoFocus:!0,value:y,placeholder:"搜专家或专家团,点一下加进候选",onChange:$=>S($.target.value)}),a("div",{className:"ds-colpick-list"},Fo(E,y).length===0?a("div",{className:"ds-mention-empty"},"没有匹配的专家或专家团"):Fo(E,y).map($=>a("button",{key:$.key,className:"ds-mention-item",onClick:()=>J(F.id,$)},a("span",null,$.emoji),a("span",null,$.name),a("span",{className:"ds-hint"},$.hint))))):null)}),x!==null?a("div",{className:"ds-error"},x):null))}function qx({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}=ht(),[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(A=>{x(A.storage??null),k(A.snapshots??[]),d(null)}).catch(A=>d(A instanceof Error?A.message:String(A)))},[]);f.useEffect(()=>{E()},[E,e.revision]);const _=async A=>{await i(`把看板回滚到快照 #${A}?当前的这一版会先另存一份,不会丢。`,{title:"回滚快照",okText:"回滚"})&&(C(!0),y(null),r("/snapshot/restore",{id:A}).then(()=>{y(`已回滚到快照 #${A}`),E(),t?.()}).catch(G=>d(G instanceof Error?G.message:String(G))).finally(()=>C(!1)))},R=A=>{const G=new Date(Number(A)||0);return`${G.getMonth()+1}-${String(G.getDate()).padStart(2,"0")} ${String(G.getHours()).padStart(2,"0")}:${String(G.getMinutes()).padStart(2,"0")}:${String(G.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(A=>a("div",{key:A.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`#${A.id} · rev ${A.revision}`),a("div",{className:"ds-hint"},`${R(A.at)} · ${(A.bytes/1024).toFixed(1)} KB`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:S,onClick:()=>_(A.id)},"回滚到这版")))))))}function Vx({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}=ht(),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(""),[R,A]=v.useState(!1),G=E.trim().toLowerCase(),L=(y.log??[]).filter(w=>{const $=String(w?.line??"");return R&&!/失败|错误|异常|拦|拒绝/.test($)?!1:G===""||$.toLowerCase().includes(G)}),P=L.slice(0,12),W=()=>{const w=new URLSearchParams({q:E.trim(),failed:R?"1":"0",token:r()??""}),$=document.createElement("a");$.href=`${API}/export/dispatch.csv?${w.toString()}`,$.rel="noopener",document.body.appendChild($),$.click(),document.body.removeChild($),o(`正在导出 ${L.length} 条派单日志`)},J=async w=>{k(w),d(null);try{if(w==="gitlab"){const $=await s("/gitlab/sync",{});d($.report===void 0?"已触发 GitLab 同步":`已同步:${($.report.projects??[]).map(Y=>`${Y.name} +${Y.created}`).join(",")||"无新卡"}`)}else if(w==="drive")if(h===void 0)d("还没有带云盘文件夹的需求,先建一条需求并归档一次。");else{const $=await s("/requirement/sync",{id:h.id});d(`已归档 ${$.report?.written??0} / ${$.report?.total??0} 件`),o(`已归档 ${$.report?.written??0} / ${$.report?.total??0} 件`)}else if(w==="feishu"){const $=await s("/feishu/test",{});d($.ok===!0?"测试消息已发出":`发送失败:${$.result?.error??"没配置机器人"}`)}else d("代码隔离没有独立的重试入口,下一张卡派单时会自动重试。");await a()}catch($){{const Y=$ instanceof Error?$.message:String($);d(Y),o(Y,"error")}}finally{k("")}},q=w=>{const $=Number(w)||0;if($===0)return"—";const Y=new Date($);return`${Y.getMonth()+1}-${String(Y.getDate()).padStart(2,"0")} ${String(Y.getHours()).padStart(2,"0")}:${String(Y.getMinutes()).padStart(2,"0")}`},F={ok:"正常",error:"异常",idle:"还没用过"},M={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:R,onChange:w=>A(w.target.checked)}),n("span",null,"只看失败")),n("button",{className:"ds-btn ds-btn-ghost",disabled:L.length===0,onClick:W,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",{},q(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,$)=>n("div",{key:String($),className:"ds-log-line"},`${q(w.at)} · ${String(w.line??"")}`))),(y.log??[]).length>0&&L.length>P.length?n("div",{className:"ds-hint"},`只显示最近 ${P.length} / 命中 ${L.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:M[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"},`最近一次:${q(w.at)} · 成功 ${q(w.lastOkAt)} · 失败 ${q(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 Wx({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}=ht(),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(W){d(W instanceof Error?W.message:String(W))}finally{C(!1)}}},R=async P=>{C(!0),d(null),y(null);try{const W=await r("/repos/test",{id:P.id});y(`连通:${P.project}(示例 Issue ${W.sample} 条)`)}catch(W){d(W instanceof Error?W.message:String(W))}finally{C(!1)}},A=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(W){d(W instanceof Error?W.message:String(W))}finally{C(!1)}},G=(P,W)=>k({...b,[P]:W}),L=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 A(P)},"同步"),a("button",{className:"ds-btn",disabled:S,onClick:()=>void R(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:L,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=>G("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=>G("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=>G("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=>G("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=>G("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=>G("syncEnabled",P.target.checked)}),a("span",{},"每分钟自动把这个项目的新 Issue 拉成卡片")))))}const sf=[{key:"change",label:"卡片状态变化"},{key:"sla",label:"缺陷 SLA 超时"},{key:"bug",label:"自动开缺陷"},{key:"release",label:"版本发布"}],Hx=e=>{const t=e.events!==null&&typeof e.events=="object"?e.events:null;return t===null?e.notifyOnChange===!1?[]:["change","sla","bug","release"]:sf.filter(a=>t[a.key]!==!1).map(a=>a.label)};function Gx({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}=ht(),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??""),[R,A]=v.useState(e.feishu?.autoSyncArtifacts!==!1),[G,L]=v.useState(!1),[P,W]=v.useState(null),J=async()=>{L(!0),W(null);try{await r("/config",{feishu:{driveFolder:E.trim(),autoSyncArtifacts:R}}),W("已保存"),await t()}catch(O){W(O instanceof Error?O.message:String(O))}finally{L(!1)}},q=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(K){d(K instanceof Error?K.message:String(K))}finally{C(!1)}}},M=async O=>{C(!0),d(null),y(null);try{const Ne=((await r("/feishu/test",{id:O.id})).results??[])[0];y(Ne!==void 0&&Ne.ok===!0?`「${O.name}」测试消息已发送`:`「${O.name}」发送失败:${Ne?.error??"检查 Webhook"}`)}catch(K){d(K instanceof Error?K.message:String(K))}finally{C(!1)}},w=(O,K)=>k({...b,[O]:K}),$=O=>{const K=b.events!==null&&typeof b.events=="object"?b.events:null;return K===null?O==="change"?b.notifyOnChange!==!1:!0:K[O]!==!1},Y=(O,K)=>{const fe={...b.events!==null&&typeof b.events=="object"?b.events:{},[O]:K};k({...b,events:fe,notifyOnChange:fe.change!==!1})},I=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:""} · 收:${Hx(O).join(" / ")||"什么都不收"}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn",disabled:S,onClick:()=>void M(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:I,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:R,onChange:O=>A(O.target.checked)}),a("span",{},"专家登记交付物时自动归档(关掉就只存平台里)")),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:G,onClick:()=>void J()},G?"保存中…":"保存"),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 q()},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"}},...sf.map(O=>a("label",{key:O.key,className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:$(O.key),onChange:K=>Y(O.key,K.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 Kx({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}=ht(),[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),[_,R]=v.useState(!1),[A,G]=v.useState(!1),L=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(()=>{L()},[L]);const P=x?.skills??[],W=x?.onDisk??[],J=O=>(e.experts??[]).filter(K=>(K.skills??[]).includes(O)),q=(O,K)=>g({...k,[O]:K}),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 L(),await t()}catch(O){h(O instanceof Error?O.message:String(O))}finally{E(!1)}}},M=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 L(),await t()}catch(K){h(K instanceof Error?K.message:String(K))}finally{E(!1)}}},w=async()=>{E(!0),h(null),S(null);try{const O=await r("/skills/sync",{});S(`已重新写入 ${(O.written??[]).length} 条技能`),await L()}catch(O){h(O instanceof Error?O.message:String(O))}finally{E(!1)}},$=async O=>{if(O!=null){R(!0),h(null),S(null);try{const K=new Uint8Array(await O.arrayBuffer());let Ne="";const fe=32768;for(let Se=0;Se<K.length;Se+=fe)Ne+=String.fromCharCode.apply(null,K.subarray(Se,Se+fe));const xe=await r("/skills/upload",{fileName:O.name,dataBase64:window.btoa(Ne),overwrite:A}),He=xe.overwritten===!0?"已覆盖":"已上传";S(`${He}技能「${xe.skill.name}」${xe.file?`,写入 ${xe.file}`:""}`),g(null),await L(),await t()}catch(K){h(K instanceof Error?K.message:String(K))}finally{R(!1)}}},Y=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(K=>a(c,{className:"ds-chip",key:K},K)),O.enabled===!1?a("span",{className:"ds-hint"}," (已停用)"):null,W.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(K=>`${K.emoji}${K.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 M(O),"data-danger":"1"},"删除")))),I=W.filter(O=>!P.some(K=>K.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:Y,addLabel:"新建技能",onAdd:()=>{S(null),g({name:"",description:"",whenToUse:"",tags:"",content:`# 什么时候用
28
+ 不确定的地方去代码或历史工单里找证据,禁止编造接口字段。`,onChange:I=>A("prompt",I.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:I=>A("enabled",I.target.checked)}),a("span",{},"启用这个专家")))))}function jx({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}=ht(),k=e.teams??[],g=e.experts??[],[d,h]=n.useState(null),[y,S]=n.useState(null),[C,E]=n.useState(!1),[_,R]=n.useState({});n.useEffect(()=>{let q=!0;return s("/team/memory",{}).then(F=>{q&&R(F?.memory??{})}).catch(()=>{}),()=>{q=!1}},[e.teams]);const A=async q=>{if(await i("清空「"+q.name+"」的团队记忆?",{title:"清空团队记忆",danger:!0,okText:"清空"}))try{const F=await s("/team/memory/clear",{teamId:q.id});R(F?.memory??{}),o("已清空这个团的记忆")}catch(F){S(F instanceof Error?F.message:String(F))}},G=q=>{const F=g.find(M=>M.id===q);return F===void 0?q:`${F.emoji} ${F.name}`},L=async()=>{if(d!==null){E(!0),S(null);try{await s("/teams/save",{team:d}),h(null),await t()}catch(q){S(q instanceof Error?q.message:String(q))}finally{E(!1)}}},P=async q=>{if(await i(`删除专家团「${q.name}」?`,{title:"删除专家团",danger:!0,okText:"删除"})){E(!0),S(null);try{await s("/teams/delete",{id:q.id}),await t()}catch(F){S(F instanceof Error?F.message:String(F))}finally{E(!1)}}},W=(q,F)=>h({...d,[q]:F}),J=k.map(q=>a("div",{className:"ds-listrow",key:q.id},a("div",{className:"ds-listrow-main"},a("div",{},`👥 ${q.name}`),a("div",{className:"ds-hint"},`团长 ${G(q.lead)} · 成员 ${q.members.map(G).join("、")}`),a("div",{className:"ds-hint"},(_[q.id]??[]).length===0?"🧠 团队记忆:还没有(卡做完会自动记一条,派活时带进提示词)":`🧠 团队记忆 ${_[q.id].length} 条 · 最近:${_[q.id][0]?.outcome??""} ${_[q.id][0]?.title??""}`)),a("div",{className:"ds-listrow-actions"},l("清空这个团的记忆",a("button",{className:"ds-btn ds-btn-ghost",disabled:C||(_[q.id]??[]).length===0,onClick:()=>void A(q)},"清空记忆")),l("编辑这个专家团",a("button",{className:"ds-btn",disabled:C,onClick:()=>h({...q})},"编辑")),l("删除这个专家团",a("button",{className:"ds-btn",disabled:C,onClick:()=>void P(q),"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:q=>q,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 L()},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:q=>W("name",q.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"这个团负责什么"),a(u,{className:"ds-input",value:d.description,placeholder:"从需求澄清一路做到验收",onChange:q=>W("description",q.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(q=>a("label",{className:"ds-row",key:q.id,style:{alignItems:"center",gap:6,cursor:"pointer"}},a(v,{checked:(d.members??[]).includes(q.id),onChange:F=>W("members",F.target.checked?[...d.members??[],q.id]:(d.members??[]).filter(M=>M!==q.id))}),a("span",{},`${q.emoji} ${q.name}`))))),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"团长(负责拆活和汇总)"),a(x,{className:"ds-input",value:d.lead??"",onChange:q=>W("lead",q.target.value)},a("option",{value:""},"自动(第一个成员)"),(d.members??[]).map(q=>a("option",{key:q,value:q},G(q)))))))}function Dx({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}=ht(),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)),[_,R]=n.useState(!1),[A,G]=n.useState(null),[L,P]=n.useState(null),W=async $=>{R(!0),G(null),P(null);try{await s("/config",{testing:{envs:$.envs??g,defaultEnv:$.defaultEnv!==void 0?$.defaultEnv:k.defaultEnv??"",timeoutMs:$.timeoutMs!==void 0?$.timeoutMs:k.timeoutMs??15e3,allowHosts:$.allowHosts!==void 0?$.allowHosts:Array.isArray(k.allowHosts)?k.allowHosts:[],concurrency:k.concurrency??4}}),P("已保存"),o("已保存"),await t()}catch(Y){G(Y instanceof Error?Y.message:String(Y))}finally{R(!1)}},J=async()=>{if(d===null)return;const $=String(d.name??"").trim(),Y=String(d.baseUrl??"").trim();if($===""){G("环境名不能为空");return}const I={id:d.id??"",name:$,baseUrl:Y},O=d.id===void 0||d.id===""?[...g,I]:g.map(K=>K.id===d.id?{...K,...I}:K);h(null),await W({envs:O,defaultEnv:k.defaultEnv===""?"":k.defaultEnv})},q=async $=>{if(!await i(`删除环境「${$.name}」?`,{title:"删除环境",danger:!0,okText:"删除"}))return;const Y=g.filter(I=>I.id!==$.id);await W({envs:Y,defaultEnv:k.defaultEnv===$.id?Y[0]?.id??"":k.defaultEnv})},F=async()=>{const $=y.split(/[,,\s]+/).map(I=>I.trim()).filter(I=>I!==""),Y=Number(C);await W({allowHosts:$,timeoutMs:Number.isFinite(Y)&&Y>0?Y:15e3})},M=$=>a("div",{key:$.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${$.name}${k.defaultEnv===$.id?" · 默认":""}`),a("div",{className:"ds-hint"},$.baseUrl===""?"相对地址会因为没有 baseUrl 而失败":$.baseUrl)),a("div",{className:"ds-listrow-actions"},k.defaultEnv===$.id?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>void W({defaultEnv:$.id})},"设为默认"),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>h({...$})},"编辑"),a("button",{className:"ds-btn ds-btn-ghost",disabled:_,onClick:()=>void q($),"data-danger":"1"},"删除"))),w=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:$=>h({...d,name:$.target.value})})),a("div",{className:"ds-field"},c("baseUrl","接口测试的根地址,用例里的 /path 会拼在它后面"),a(u,{className:"ds-input",value:d.baseUrl??"",placeholder:"https://test.example.com",onChange:$=>h({...d,baseUrl:$.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 J()},"保存环境"))));return a("div",{className:"ds-pane-stack"},w,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(M))),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:$=>S($.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"单条用例超时(毫秒)"),a(u,{className:"ds-input",value:C,onChange:$=>E($.target.value)})),a("div",{className:"ds-row",style:{justifyContent:"flex-end"}},a("button",{className:"ds-btn ds-btn-primary",disabled:_,onClick:()=>void F()},_?"保存中…":"保存约束")),L===null?null:a("div",{className:"ds-ok"},L),A===null?null:a("div",{className:"ds-err"},A))))}function Ox({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}=ht(),k=e.feishu??{},[g,d]=n.useState(""),[h,y]=n.useState(String(k.escalateAfter??3)),[S,C]=n.useState(!1),[E,_]=n.useState(null),[R,A]=n.useState(null),G=async()=>{C(!0),_(null),A(null);try{const L={};String(g??"").trim()!==""&&(L.escalateWebhook=String(g).trim()),L.escalateAfter=Number(h)||3,await s("/config",{feishu:L}),d(""),_("已保存:连续失败 "+(Number(h)||3)+" 次后往升级群发一条"),await t()}catch(L){A(L instanceof Error?L.message:String(L))}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:L=>d(L.target.value)})),a("div",{className:"ds-field"},c("连续失败几次后升级","同一通道连续失败到这个次数,就给升级群发消息"),a(u,{className:"ds-input",style:{width:90},value:h,onChange:L=>y(L.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 G()},S?"保存中…":"保存升级设置")),E===null?null:a("div",{className:"ds-ok"},E),R===null?null:a("div",{className:"ds-err"},R)))}function Lx({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}=ht(),[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),[_,R]=n.useState(null),A=async J=>{R(null);try{await s("/config",{releaseRules:J}),R("发布准入已更新"),await t()}catch(q){R(q instanceof Error?q.message:String(q))}},G=()=>g(J=>J.length>=12?J:[...J,{id:"",name:"",kind:"other",note:""}]),L=(J,q)=>g(F=>F.map((M,w)=>w===J?{...M,...q}:M)),P=J=>g(q=>q.filter((F,M)=>M!==J)),W=async()=>{h(!0),S(null),E(null);try{const J=k.map(q=>({...q,name:String(q.name??"").trim()})).filter(q=>q.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,q)=>a("div",{key:`env-${q}`,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=>L(q,{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=>L(q,{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=>L(q,{note:F.target.value})})),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>P(q),"data-danger":"1"},"删除"))),a("div",{className:"ds-row",style:{gap:8,marginTop:10}},a("button",{className:"ds-btn",onClick:G,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 W()},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 A({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 A({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 Mx({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}=ht(),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([]),R=e.localAuth??{},[A,G]=p.useState(""),[L,P]=p.useState(""),[W,J]=p.useState(!1),[q,F]=p.useState(null),[M,w]=p.useState(null),$=async()=>{J(!0),F(null),w(null);try{await r("/auth/password",{username:R.username,currentPassword:A,newPassword:L}),G(""),P(""),F("密码已更新"),t()}catch(K){w(K instanceof Error?K.message:String(K))}finally{J(!1)}},Y=K=>x(Ne=>({...Ne,...K})),I=async()=>{d(!0),y(null),C(null),_([]);try{const K=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});_(K.steps??[]),y(K.message??"连接正常"),s(K.message??"连接正常",K.ok===!1?"error":"success")}catch(K){const Ne=K instanceof Error?K.message:String(K);C(Ne)}finally{d(!1)}},O=async()=>{k(!0),y(null),C(null);try{const K={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()!==""&&(K.ldap.bindPassword=v.bindPassword.trim()),await r("/config",K),Y({bindPassword:""}),y(v.bindPassword.trim()===""?"已保存(服务账号密码没改)":"已保存,服务账号密码已写入密钥文件"),await t()}catch(K){C(K instanceof Error?K.message:String(K))}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:K=>Y({enabled:K.target.checked})}),a("span",{className:"ds-check-text"},"启用 LDAP 登录")),a("label",{className:"ds-check",style:{marginTop:6}},a(c,{checked:v.requireLogin,onChange:K=>Y({requireLogin:K.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:K=>Y({url:K.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:K=>Y({timeoutMs:K.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:K=>Y({baseDN:K.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:K=>Y({bindDN:K.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:K=>Y({bindPassword:K.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:K=>Y({userFilter:K.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:K=>Y({nameAttr:K.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"邮箱属性"),a(l,{className:"ds-input",value:v.mailAttr,onChange:K=>Y({mailAttr:K.target.value})})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"部门属性"),a(l,{className:"ds-input",value:v.deptAttr,onChange:K=>Y({deptAttr:K.target.value})}))),a("div",{className:"ds-row",style:{gap:8,marginTop:10}},a("button",{className:"ds-btn",disabled:g,onClick:()=>void I()},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((K,Ne)=>a("div",{key:`ldap-step-${Ne}`,className:K.ok===!0?"ds-ok":"ds-error"},`${K.ok===!0?"✓":"✗"} ${K.name}:${K.message}${K.ms>0?`(${K.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}},`账号:${R.username??"admin"} · ${R.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:A,onChange:K=>G(K.target.value)})),a("div",{className:"ds-field",style:{flex:1}},a("div",{className:"ds-label"},"新密码(至少 8 位)"),a(l,{className:"ds-input",type:"password",value:L,onChange:K=>P(K.target.value)})),a("button",{className:"ds-btn",disabled:W||A===""||L.length<8,onClick:()=>void $()},W?"提交中…":"改密码")),q===null?null:a("div",{className:"ds-ok",style:{marginTop:6}},q),M===null?null:a("div",{className:"ds-error",style:{marginTop:6}},M)),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(K=>a("div",{key:K.id,className:"ds-kv"},a("div",null,K.name),a("div",null,`${K.id} · ${K.dept===""?"(无部门)":K.dept} · 登录 ${K.loginCount} 次 · 最近 ${Ye(K.lastLoginAt)}`))))))}function zx({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}=ht(),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),R=async()=>{y(!0),C(null),_(null);try{const A=Number(v),G=Number(b),L=Number(g);if(!Number.isFinite(A)||A<=0)throw new Error("预算次数要是正整数");if(!Number.isFinite(G)||G<=0)throw new Error("预算小时要是正整数");await r("/config",{costs:{budgetRuns:Math.round(A),budgetHours:Math.round(G),warnAt:Number.isFinite(L)?Math.min(100,Math.max(10,Math.round(L))):80}}),C("已保存:预算"),await t()}catch(A){_(A instanceof Error?A.message:String(A))}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:A=>x(A.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"每月机上时长上限(小时)"),a(l,{className:"ds-input",value:b,onChange:A=>k(A.target.value)})),a("div",{className:"ds-field"},a("div",{className:"ds-label"},"警告阈值(百分比)"),a(l,{className:"ds-input",value:g,onChange:A=>d(A.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 R()},h?"保存中…":"保存预算")),S===null?null:a("div",{className:"ds-ok"},S),E===null?null:a("div",{className:"ds-err"},E)))}function Fx({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}=ht(),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)}}},R=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($){g($ instanceof Error?$.message:String($))}finally{S(!1)}}},A=async w=>{if(C.name.trim()!==""){S(!0),g(null);try{await r("/projects/iteration",{project:w.id,name:C.name.trim(),start:Pr(C.start),due:Pr(C.due),status:C.status}),E({name:"",start:"",due:"",status:"planned"}),await t()}catch($){g($ instanceof Error?$.message:String($))}finally{S(!1)}}},G=async(w,$)=>{S(!0),g(null);try{await r("/projects/iteration",{project:w.id,action:"remove",iteration:$.id}),await t()}catch(Y){g(Y instanceof Error?Y.message:String(Y))}finally{S(!1)}},L=(w,$)=>b({...x,[w]:$}),P=(w,$,Y)=>b({...x,services:(x.services??[]).map((I,O)=>O===w?{...I,[$]:Y}:I)}),W=()=>b({...x,services:[...x.services??[],{id:"",name:"",repoId:"",repoPath:"",baseBranch:x.baseBranch||"main",dependsOn:[]}]}),J=w=>b({...x,services:(x.services??[]).filter(($,Y)=>Y!==w)}),q=x===null?[]:x.services??[],F=(w,$)=>b({...x,services:(x.services??[]).map((Y,I)=>{if(I!==w)return Y;const O=Array.isArray(Y.dependsOn)?Y.dependsOn:[];return{...Y,dependsOn:O.includes($)?O.filter(K=>K!==$):[...O,$]}})}),M=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} · ${Wl[w.status]??w.status}${w.lead?` · 负责人 ${w.lead}`:""}`),a("div",{className:"ds-hint"},`${(e.projects??[]).find($=>$.id===w.id)?.iterations?.length??0} 个迭代 · ${(w.services??[]).length} 个服务${(()=>{const $=(w.services??[]).reduce((Y,I)=>Y+(I.dependsOn??[]).length,0);return $>0?` · ${$} 条依赖`:""})()}${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 R(w),"data-danger":"1"},"删除")))));return a("div",{className:"ds-pane-stack"},a(n,{title:"业务项目(一个项目可以挂很多张卡和很多个迭代)",hint:"项目是看板的第一层分组:卡片挂项目,项目下面划迭代,看板可按项目和迭代筛选,项目页签看每个项目的体检数据。",rows:M,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=>L("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=>L("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=>L("status",w.target.value)},Object.keys(Wl).map(w=>a("option",{key:w,value:w},Wl[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=>L("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=>L("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=>L("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"},`服务(${q.length})· 一个服务一个 git 路径,卡片可以指定改哪个`),q.length===0?a("div",{className:"ds-empty"},"还没有分服务。只有一个仓库时留空即可,走上面的默认代码目录。"):q.map((w,$)=>{const Y=q.filter((I,O)=>O!==$&&String(I.id??"").trim()!=="");return a("div",{key:`${w.id}-${$}`,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:I=>P($,"name",I.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:I=>P($,"repoPath",I.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:I=>P($,"baseBranch",I.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:I=>P($,"repoId",I.target.value)},a("option",{value:""},"不关联"),v.map(I=>a("option",{key:I.id,value:I.id},I.name||I.project)))),a("button",{className:"ds-btn",type:"button",onClick:()=>J($),"data-danger":"1"},"删除")),Y.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}},"(勾上的服务要先完成,本服务的卡才能收口)"),Y.map(I=>a("span",{key:I.id,className:"ds-chip","data-on":(w.dependsOn??[]).includes(I.id)?"1":"0",title:`依赖 ${I.name||I.id}`,onClick:()=>F($,I.id)},I.name||I.id))))}),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn",type:"button",onClick:()=>W()},"+ 加一个服务"),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=>L("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} · ${Hl[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 G(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(Hl).map(w=>a("option",{key:w,value:w},Hl[w])))),a("button",{className:"ds-btn",disabled:y||C.name.trim()==="",onClick:()=>void A(x)},"+ 加迭代")),a("div",{className:"ds-hint"},"迭代改完会立刻反映到看板的迭代筛选。"))))}function Bx({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}=ht(),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(""),[_,R]=v.useState(!1),[A,G]=v.useState(""),L=async(w,$)=>{h(!0),S(null);try{await s(w,$),await a()}catch(Y){{const I=Y instanceof Error?Y.message:String(Y);S(I),o(I,"error")}}finally{h(!1)}},P=async()=>{const w=k.name.trim();w!==""&&(await L("/access/save",{member:{name:w,role:k.role}}),g({name:"",role:"member"}))},W=Array.from(new Set(b.map(w=>w.by))).sort(),J=A.trim().toLowerCase(),q=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=q.slice(0,120),M=()=>{const w=new URLSearchParams({by:C,q:A.trim(),failed:_?"1":"0",token:r()??""}),$=document.createElement("a");$.href=`${API}/audit/export.csv?${w.toString()}`,$.rel="noopener",document.body.appendChild($),$.click(),document.body.removeChild($),o(`正在导出 ${q.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 L("/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 L("/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($=>$.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:$=>void L("/access/save",{member:{name:w.name,role:$.target.value}})},ACCESS_ROLE_OPTIONS.map($=>n("option",{key:$.id,value:$.id},$.label))),n("button",{className:"ds-btn ds-btn-ghost",disabled:d,onClick:()=>void L("/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} / 命中 ${q.length} / 共 ${b.length} 条)`),n("div",{className:"ds-row",style:{gap:8,marginLeft:"auto"}},n(c,{className:"ds-input",placeholder:"搜动作 / 人 / 详情",value:A,style:{width:170},onChange:w=>G(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__"},"所有人"),W.map(w=>n("option",{key:w,value:w},w))),n("label",{className:"ds-check"},n(p,{checked:_,onChange:w=>R(w.target.checked)}),n("span",null,"只看失败")),n("button",{className:"ds-btn ds-btn-ghost",disabled:d,onClick:async()=>{await l("清空审计日志?清掉之后所有操作记录都没了。",{title:"清空审计日志",danger:!0,okText:"清空"})&&L("/audit/clear",{})}},"清空"),n("button",{className:"ds-btn ds-btn-ghost",disabled:q.length===0,onClick:M,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"},Ye(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 Ux({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}=ht(),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(()=>ef(v),[v]),_=F=>{const M=v.columns?.[F];return M==null?[]:Array.isArray(M.members)?M.members:M.kind==="none"?[]:[{kind:M.kind,id:M.id}]},R=F=>v.columns?.[F]?.strategy??"round_robin",A=F=>{if(F.kind==="role")return"🎯 "+(Aa[F.id]??F.id);if(F.kind==="team"){const w=(v.teams??[]).find($=>$.id===F.id);return w===void 0?`👥 ${F.id}`:`${w.emoji??"👥"} ${w.name}`}const M=(v.experts??[]).find(w=>w.id===F.id);return M===void 0?F.id:`${M.emoji} ${M.name}`},G=Array.isArray(v.design?.columns)?v.design.columns:["dev"],L=async F=>{g(!0),b(null);try{await r("/config",{design:{...v.design??{},...F}}),await t()}catch(M){b(M instanceof Error?M.message:String(M))}finally{g(!1)}},P=F=>{const M=G.includes(F)?G.filter(w=>w!==F):[...G,F];L({columns:M})},W=async(F,M,w)=>{g(!0),b(null);try{await r("/columns/save",{status:F,members:M,strategy:w}),await t()}catch($){b($ instanceof Error?$.message:String($))}finally{g(!1)}},J=(F,M)=>{const w=M.key.startsWith("t:")?"team":M.key.startsWith("r:")?"role":"expert",$=M.key.slice(2),Y=_(F);Y.some(I=>I.kind===w&&I.id===$)||(S(""),W(F,[...Y,{kind:w,id:$}],R(F)))},q=(F,M)=>void W(F,_(F).filter((w,$)=>$!==M),R(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 L({enabled:v.design?.enabled!==!0})},v.design?.enabled===!0?"设计闸门:已开启":"设计闸门:已关闭"),C.map(F=>a("button",{key:"gate-"+F.id,className:G.includes(F.id)?"ds-btn ds-btn-primary":"ds-btn",disabled:k,onClick:()=>P(F.id)},F.label??F.id))),C.map(F=>{const M=_(F.id),w=R(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),M.length===0?a("span",{className:"ds-hint"},"👤 等人认领(不自动派)"):M.map(($,Y)=>a(c,{className:"ds-chip ds-chip-owner",key:`${$.kind}:${$.id}`},A($),a("button",{className:"ds-chip-x",title:"从候选里去掉",disabled:k,onClick:()=>q(F.id,Y)},"×"))),a("button",{className:"ds-btn",style:{marginLeft:"auto"},disabled:k,onClick:()=>{S(""),h($=>$===F.id?null:F.id)}},d===F.id?"收起":"+ 加候选"),M.length>=2?a(p,{className:"ds-input",style:{width:240},value:w,disabled:k,onChange:$=>void W(F.id,M,$.target.value)},STRATEGIES.map($=>a("option",{key:$.id,value:$.id},$.label))):null),d===F.id?a("div",{className:"ds-colpick"},a(l,{className:"ds-mention-search",autoFocus:!0,value:y,placeholder:"搜专家或专家团,点一下加进候选",onChange:$=>S($.target.value)}),a("div",{className:"ds-colpick-list"},Fo(E,y).length===0?a("div",{className:"ds-mention-empty"},"没有匹配的专家或专家团"):Fo(E,y).map($=>a("button",{key:$.key,className:"ds-mention-item",onClick:()=>J(F.id,$)},a("span",null,$.emoji),a("span",null,$.name),a("span",{className:"ds-hint"},$.hint))))):null)}),x!==null?a("div",{className:"ds-error"},x):null))}function qx({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}=ht(),[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(A=>{x(A.storage??null),k(A.snapshots??[]),d(null)}).catch(A=>d(A instanceof Error?A.message:String(A)))},[]);f.useEffect(()=>{E()},[E,e.revision]);const _=async A=>{await i(`把看板回滚到快照 #${A}?当前的这一版会先另存一份,不会丢。`,{title:"回滚快照",okText:"回滚"})&&(C(!0),y(null),r("/snapshot/restore",{id:A}).then(()=>{y(`已回滚到快照 #${A}`),E(),t?.()}).catch(G=>d(G instanceof Error?G.message:String(G))).finally(()=>C(!1)))},R=A=>{const G=new Date(Number(A)||0);return`${G.getMonth()+1}-${String(G.getDate()).padStart(2,"0")} ${String(G.getHours()).padStart(2,"0")}:${String(G.getMinutes()).padStart(2,"0")}:${String(G.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(A=>a("div",{key:A.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`#${A.id} · rev ${A.revision}`),a("div",{className:"ds-hint"},`${R(A.at)} · ${(A.bytes/1024).toFixed(1)} KB`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:S,onClick:()=>_(A.id)},"回滚到这版")))))))}function Vx({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}=ht(),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(""),[R,A]=v.useState(!1),G=E.trim().toLowerCase(),L=(y.log??[]).filter(w=>{const $=String(w?.line??"");return R&&!/失败|错误|异常|拦|拒绝/.test($)?!1:G===""||$.toLowerCase().includes(G)}),P=L.slice(0,12),W=()=>{const w=new URLSearchParams({q:E.trim(),failed:R?"1":"0",token:r()??""}),$=document.createElement("a");$.href=`${API}/export/dispatch.csv?${w.toString()}`,$.rel="noopener",document.body.appendChild($),$.click(),document.body.removeChild($),o(`正在导出 ${L.length} 条派单日志`)},J=async w=>{k(w),d(null);try{if(w==="gitlab"){const $=await s("/gitlab/sync",{});d($.report===void 0?"已触发 GitLab 同步":`已同步:${($.report.projects??[]).map(Y=>`${Y.name} +${Y.created}`).join(",")||"无新卡"}`)}else if(w==="drive")if(h===void 0)d("还没有带云盘文件夹的需求,先建一条需求并归档一次。");else{const $=await s("/requirement/sync",{id:h.id});d(`已归档 ${$.report?.written??0} / ${$.report?.total??0} 件`),o(`已归档 ${$.report?.written??0} / ${$.report?.total??0} 件`)}else if(w==="feishu"){const $=await s("/feishu/test",{});d($.ok===!0?"测试消息已发出":`发送失败:${$.result?.error??"没配置机器人"}`)}else d("代码隔离没有独立的重试入口,下一张卡派单时会自动重试。");await a()}catch($){{const Y=$ instanceof Error?$.message:String($);d(Y),o(Y,"error")}}finally{k("")}},q=w=>{const $=Number(w)||0;if($===0)return"—";const Y=new Date($);return`${Y.getMonth()+1}-${String(Y.getDate()).padStart(2,"0")} ${String(Y.getHours()).padStart(2,"0")}:${String(Y.getMinutes()).padStart(2,"0")}`},F={ok:"正常",error:"异常",idle:"还没用过"},M={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:R,onChange:w=>A(w.target.checked)}),n("span",null,"只看失败")),n("button",{className:"ds-btn ds-btn-ghost",disabled:L.length===0,onClick:W,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",{},q(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,$)=>n("div",{key:String($),className:"ds-log-line"},`${q(w.at)} · ${String(w.line??"")}`))),(y.log??[]).length>0&&L.length>P.length?n("div",{className:"ds-hint"},`只显示最近 ${P.length} / 命中 ${L.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:M[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"},`最近一次:${q(w.at)} · 成功 ${q(w.lastOkAt)} · 失败 ${q(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 Wx({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}=ht(),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(W){d(W instanceof Error?W.message:String(W))}finally{C(!1)}}},R=async P=>{C(!0),d(null),y(null);try{const W=await r("/repos/test",{id:P.id});y(`连通:${P.project}(示例 Issue ${W.sample} 条)`)}catch(W){d(W instanceof Error?W.message:String(W))}finally{C(!1)}},A=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(W){d(W instanceof Error?W.message:String(W))}finally{C(!1)}},G=(P,W)=>k({...b,[P]:W}),L=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 A(P)},"同步"),a("button",{className:"ds-btn",disabled:S,onClick:()=>void R(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:L,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=>G("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=>G("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=>G("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=>G("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=>G("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=>G("syncEnabled",P.target.checked)}),a("span",{},"每分钟自动把这个项目的新 Issue 拉成卡片")))))}const sf=[{key:"change",label:"卡片状态变化"},{key:"sla",label:"缺陷 SLA 超时"},{key:"bug",label:"自动开缺陷"},{key:"release",label:"版本发布"}],Hx=e=>{const t=e.events!==null&&typeof e.events=="object"?e.events:null;return t===null?e.notifyOnChange===!1?[]:["change","sla","bug","release"]:sf.filter(a=>t[a.key]!==!1).map(a=>a.label)};function Gx({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}=ht(),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??""),[R,A]=v.useState(e.feishu?.autoSyncArtifacts!==!1),[G,L]=v.useState(!1),[P,W]=v.useState(null),J=async()=>{L(!0),W(null);try{await r("/config",{feishu:{driveFolder:E.trim(),autoSyncArtifacts:R}}),W("已保存"),await t()}catch(O){W(O instanceof Error?O.message:String(O))}finally{L(!1)}},q=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(K){d(K instanceof Error?K.message:String(K))}finally{C(!1)}}},M=async O=>{C(!0),d(null),y(null);try{const Ne=((await r("/feishu/test",{id:O.id})).results??[])[0];y(Ne!==void 0&&Ne.ok===!0?`「${O.name}」测试消息已发送`:`「${O.name}」发送失败:${Ne?.error??"检查 Webhook"}`)}catch(K){d(K instanceof Error?K.message:String(K))}finally{C(!1)}},w=(O,K)=>k({...b,[O]:K}),$=O=>{const K=b.events!==null&&typeof b.events=="object"?b.events:null;return K===null?O==="change"?b.notifyOnChange!==!1:!0:K[O]!==!1},Y=(O,K)=>{const fe={...b.events!==null&&typeof b.events=="object"?b.events:{},[O]:K};k({...b,events:fe,notifyOnChange:fe.change!==!1})},I=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:""} · 收:${Hx(O).join(" / ")||"什么都不收"}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn",disabled:S,onClick:()=>void M(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:I,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:R,onChange:O=>A(O.target.checked)}),a("span",{},"专家登记交付物时自动归档(关掉就只存平台里)")),a("div",{className:"ds-row",style:{gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:G,onClick:()=>void J()},G?"保存中…":"保存"),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 q()},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"}},...sf.map(O=>a("label",{key:O.key,className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:$(O.key),onChange:K=>Y(O.key,K.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 Kx({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}=ht(),[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),[_,R]=v.useState(!1),[A,G]=v.useState(!1),L=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(()=>{L()},[L]);const P=x?.skills??[],W=x?.onDisk??[],J=O=>(e.experts??[]).filter(K=>(K.skills??[]).includes(O)),q=(O,K)=>g({...k,[O]:K}),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 L(),await t()}catch(O){h(O instanceof Error?O.message:String(O))}finally{E(!1)}}},M=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 L(),await t()}catch(K){h(K instanceof Error?K.message:String(K))}finally{E(!1)}}},w=async()=>{E(!0),h(null),S(null);try{const O=await r("/skills/sync",{});S(`已重新写入 ${(O.written??[]).length} 条技能`),await L()}catch(O){h(O instanceof Error?O.message:String(O))}finally{E(!1)}},$=async O=>{if(O!=null){R(!0),h(null),S(null);try{const K=new Uint8Array(await O.arrayBuffer());let Ne="";const fe=32768;for(let Se=0;Se<K.length;Se+=fe)Ne+=String.fromCharCode.apply(null,K.subarray(Se,Se+fe));const xe=await r("/skills/upload",{fileName:O.name,dataBase64:window.btoa(Ne),overwrite:A}),He=xe.overwritten===!0?"已覆盖":"已上传";S(`${He}技能「${xe.skill.name}」${xe.file?`,写入 ${xe.file}`:""}`),g(null),await L(),await t()}catch(K){h(K instanceof Error?K.message:String(K))}finally{R(!1)}}},Y=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(K=>a(c,{className:"ds-chip",key:K},K)),O.enabled===!1?a("span",{className:"ds-hint"}," (已停用)"):null,W.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(K=>`${K.emoji}${K.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 M(O),"data-danger":"1"},"删除")))),I=W.filter(O=>!P.some(K=>K.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:Y,addLabel:"新建技能",onAdd:()=>{S(null),g({name:"",description:"",whenToUse:"",tags:"",content:`# 什么时候用
29
29
 
30
30
  # 步骤
31
31
 
@@ -41,7 +41,7 @@ GET /api/points | 200 |`,onChange:m=>le({...Z,steps:m.target.value})}),e("div",{
41
41
  - `,onChange:O=>q("content",O.target.value)})),a("div",{className:"ds-field"},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:k.enabled!==!1,onChange:O=>q("enabled",O.target.checked)}),a("span",{},"启用(停用则不写入磁盘)")))))}function Yx({state:e,onOpenTask:t,onOpenBoard:a,onOpenCases:n}){const{h:r,React:s,api:o,toast:i}=ht(),[l,c]=s.useState(null),[u,p]=s.useState(null),[f,v]=s.useState(null);if(s.useEffect(()=>{let S=!0;return(async()=>{try{const C=await o("/dashboard");S&&(c(C?.dashboard??null),p(C?.regression??null))}catch(C){if(S){const E=C instanceof Error?C.message:String(C);v(E),i(E,"error")}}})(),()=>{S=!1}},[e.revision]),l===null)return r("div",{className:"ds-body"},r("div",{className:"ds-hint"},f===null?"正在算今天该看什么…":`算不出来:${f}`));const x=u===null?null:(()=>{const S=(Number(u.failed)||0)+(Number(u.failedCards)||0)>0;return r("div",{className:"ds-reg-banner","data-bad":S?"1":"0"},r("span",null,`最近回归 · ${Ye(u.at)} · ${u.by||"人"}`),r("span",{className:"ds-hint"},`${u.cards} 张卡 / ${u.total} 条:通过 ${u.passed}${S?`,不通过 ${u.failed}`:""}${u.bugs>0?` · 自动开缺陷 ${u.bugs} 张`:""}`),n===void 0?null:r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>n()},"看用例"))})(),b=l.risks??{},k=l.flow??{},g=l.ai??{},d=(S,C,E,_)=>r("div",{className:"ds-kpi","data-warn":_===!0?"1":"0"},r("div",{className:"ds-kpi-value"},String(C??0)),r("div",{className:"ds-kpi-label"},S),r("div",{className:"ds-kpi-hint"},E)),h=S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.code||S.id} · ${S.title}`),r("div",{className:"ds-hint"},`${S.statusLabel}${S.dueAt>0?` · 截止 ${Wa(S.dueAt)}`:""}${S.sla?.state==="overdue"?" · 缺陷超时":""}`)),r("div",{className:"ds-listrow-actions"},r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(S.id)},"打开"))),y=(S,C,E,_)=>r("div",{className:"ds-panel"},r("div",{className:"ds-panel-head"},r("span",null,S),r("span",{className:"ds-hint"},_??"")),r("div",{className:"ds-panel-body"},C.length===0?r("div",{className:"ds-hint"},E):C));return r("div",{className:"ds-body"},x,r("div",{className:"ds-kpi-grid"},d("在办卡",l.scope.open,`共 ${l.scope.tasks} 张`),d("逾期",b.overdue,"过了期望完成时间还没完成",b.overdue>0),d("阻塞",b.blocked,"挂着阻塞标记等人决策",b.blocked>0),d("缺陷超时",b.slaOverdue,"按 P0-P3 时限算",b.slaOverdue>0),d("准出被拦",b.gateFailed,"待测试/待验收里过不了准出",b.gateFailed>0),d("流水线失败",b.pipelineFailed,"最近一次 CI 没通过",b.pipelineFailed>0),d("集成异常",b.integrationsBroken,"飞书/GitLab/云盘/隔离",b.integrationsBroken>0),d("AI 在跑",g.running,`今天派单 ${g.todayRuns} 次`)),r("div",{className:"ds-panel"},r("div",{className:"ds-panel-head"},r("span",null,"流程与产出"),r("span",{className:"ds-hint"},"需要人动手的地方")),r("div",{className:"ds-panel-body ds-grid4"},r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待评审"),r("div",null,String(k.review??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待测试"),r("div",null,String(k.test??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待验收"),r("div",null,String(k.accept??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"今天完成"),r("div",null,String(k.doneToday??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"近 7 天完成"),r("div",null,String(k.doneWeek??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"需求卡住"),r("div",null,String(b.requirementBlocked??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"版本卡住"),r("div",null,String(b.releaseBlocked??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},""),r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>a()},"去看板")))),y("逾期",(l.list?.overdue??[]).map(h),"没有逾期的卡。"),y("阻塞",(l.list?.blocked??[]).map(h),"没有卡在等人决策。"),y("缺陷超时",(l.list?.slaOverdue??[]).map(h),"缺陷都在时限内。"),y("准出被拦",(l.list?.gateFailed??[]).map(h),"没有卡被准出规则拦住。","清单没勾完 / 用例不通过 / 已签过字但被打回"),y("流水线失败",(l.list?.pipelineFailed??[]).map(h),"没有 CI 失败的卡。"),y("需求与版本卡住",[...(l.list?.requirementBlocked??[]).map(S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.code||S.id} · ${S.title}`),r("div",{className:"ds-hint"},`需求:${(S.blockers??[]).join(";")}`)))),...(l.list?.releaseBlocked??[]).map(S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.version||S.id} · ${S.name||""}`),r("div",{className:"ds-hint"},`版本:${(S.blockers??[]).join(";")}`))))],"需求与版本都还顺。"),r("div",{className:"ds-hint"},`统计时间 ${Ye(l.at)},数字随看板变化实时算,不落库。`))}function Qx({state:e,onOpenTask:t}){const{h:a,React:n,api:r,toast:s,confirmDialog:o,promptDialog:i,TextField:l,SelectField:c,ChipField:u,CheckField:p}=ht(),[f,v]=n.useState(null),[x,b]=n.useState(null),[k,g]=n.useState(""),[d,h]=n.useState("__all__"),[y,S]=n.useState("__all__"),[C,E]=n.useState(()=>new Set),[_,R]=n.useState(!1),[A,G]=n.useState([]),[L,P]=n.useState({}),[W,J]=n.useState({}),[q,F]=n.useState(null),[M,w]=n.useState(""),[$,Y]=n.useState([]),[I,O]=n.useState(null),K=n.useCallback(async()=>{try{const V=await r("/cases",{});v(Array.isArray(V.cases)?V.cases:[]),Y(Array.isArray(V.regressions)?V.regressions:[]),G(Array.isArray(V.suites)?V.suites:[]),P(Object.fromEntries((V.counts??[]).map(me=>[me.id,me.count]))),J(Object.fromEntries((V.counts??[]).map(me=>[me.id,me.keys??[]]))),b(null)}catch(V){b(V instanceof Error?V.message:String(V)),v([])}},[]),Ne=(e.tasks??[]).reduce((V,me)=>{const Ae=me.caseSummary??{total:0,pass:0,fail:0};return V+Ae.total*7+Ae.pass*13+Ae.fail*17},0);n.useEffect(()=>{K()},[K,Ne]);const fe=n.useMemo(()=>{const V=k.trim().toLowerCase(),me=q===null?null:new Set(W[q]??[]);return(f??[]).filter(Ae=>me!==null&&!me.has(Ae.key)||d!=="__all__"&&Ae.requirementId!==d||y!=="__all__"&&Ae.result!==y?!1:V===""?!0:`${Ae.id} ${Ae.title} ${Ae.taskTitle} ${Ae.requirementTitle} ${Ae.pointId}`.toLowerCase().includes(V))},[f,k,d,y,q,W]),xe=n.useMemo(()=>{const V=fe.filter(ze=>ze.result==="pass").length,me=fe.filter(ze=>ze.result==="fail").length,Ae=fe.filter(ze=>ze.result==="skip").length;return{pass:V,fail:me,skip:Ae,rest:fe.length-V-me-Ae}},[fe]),He=e.requirements??[],Se=fe.filter(V=>V.http),U=Se.filter(V=>C.has(V.key)),ae=V=>E(me=>{const Ae=new Set(me);return Ae.has(V)?Ae.delete(V):Ae.add(V),Ae}),Te=()=>E(new Set(Se.map(V=>V.key))),Z=()=>E(new Set),le=async V=>{if(V.length===0){w("先勾几条带接口定义的用例再跑(手工用例没有 http,跑不了)");return}R(!0),w("");try{const Ae=(await r("/cases/run",{keys:V.map(Je=>Je.key),by:"回归"}))?.results??[],ze=Je=>Ae.reduce((bt,ls)=>bt+(Number(ls[Je])||0),0),At=Ae.filter(Je=>Je.ok===!1),Vt=[`跑了 ${Ae.length} 张卡 / ${ze("total")} 条:通过 ${ze("passed")},不通过 ${ze("failed")}`];ze("bugs")>0&&Vt.push(`自动开缺陷 ${ze("bugs")} 张`),At.length>0&&Vt.push(`${At.length} 张卡报错:${At[0].error??""}`),w(Vt.join(",")),E(new Set),await K()}catch(me){w(me instanceof Error?me.message:String(me))}finally{R(!1)}},he=V=>{const me=V?.results??[],Ae=Vt=>me.reduce((Je,bt)=>Je+(Number(bt[Vt])||0),0),ze=me.filter(Vt=>Vt.ok===!1),At=[`跑了 ${me.length} 张卡 / ${Ae("total")} 条:通过 ${Ae("passed")},不通过 ${Ae("failed")}`];return Ae("bugs")>0&&At.push(`自动开缺陷 ${Ae("bugs")} 张`),ze.length>0&&At.push(`${ze.length} 张卡报错:${ze[0].error??""}`),At.join(",")},H=async V=>{R(!0),w("");try{const me=await r("/suite/run",{id:V.id});w(he(me)),await K()}catch(me){w(me instanceof Error?me.message:String(me))}finally{R(!1)}},Ce=async()=>{const V=await i("套件名字(跑的就是当前筛选里的用例)",{title:"新建回归套件",defaultValue:"默认回归"});if(!(V===null||String(V).trim()===""))try{await r("/suite/save",{suite:{name:String(V).trim().slice(0,40),rules:{requirementIds:d==="__all__"?[]:[d],httpOnly:!0},keys:[],isDefault:A.length===0},by:"人"}),s("已保存套件"),await K()}catch(me){s(me instanceof Error?me.message:String(me),"error")}},be=async V=>{try{await r("/suite/save",{suite:{...V,isDefault:!0},by:"人"}),s(`夜间回归改跑「${V.name}」`),await K()}catch(me){s(me instanceof Error?me.message:String(me),"error")}},_e=async V=>{if(await o(`删除套件「${V.name}」?用例本身不受影响。`,{title:"删除套件",danger:!0,okText:"删除"}))try{await r("/suite/delete",{id:V.id}),await K()}catch(me){s(me instanceof Error?me.message:String(me),"error")}},Ue=fe.filter(V=>V.http&&V.result==="fail"),qa=()=>le(U),Tn=()=>le(Ue);return a("div",{className:"ds-body"},a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},a("span",null,"用例总览"),a("span",{className:"ds-hint"},"全公司的用例按卡片汇总:点一行跳回那张卡;用例本身仍在卡片的「测试」里维护"),a(u,{className:"ds-chip",style:{marginLeft:"auto"}},`${fe.length} 条 · ✅ ${xe.pass} · ❌ ${xe.fail}${xe.skip>0?` · ⏭️ ${xe.skip}`:""}${xe.rest>0?` · ⬜ ${xe.rest}`:""}`),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void K()},"刷新")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-row"},a(l,{className:"ds-input",style:{width:240},value:k,placeholder:"搜用例 / 卡片 / 需求 / 功能点",onChange:V=>g(V.target.value)}),a(c,{className:"ds-select",style:{width:220},value:d,onChange:V=>h(V.target.value)},a("option",{value:"__all__"},"全部需求"),He.map(V=>a("option",{key:V.id,value:V.id},`${V.code||V.id} ${V.title}`))),a(c,{className:"ds-select",style:{width:140},value:y,onChange:V=>S(V.target.value)},a("option",{value:"__all__"},"全部结果"),Gp.map(V=>a("option",{key:V,value:V},Vl[V])))),a("div",{className:"ds-row",style:{marginTop:8}},a("button",{className:"ds-btn ds-btn-ghost",disabled:_||Se.length===0,onClick:Te},`全选可跑的 ${Se.length} 条`),a("button",{className:"ds-btn ds-btn-ghost",disabled:_||C.size===0,onClick:Z},"清空选择"),a("button",{className:"ds-btn ds-btn-primary",disabled:_||U.length===0,onClick:()=>void qa()},_?"跑着…":`▶ 跑选中的 ${U.length} 条`),a("button",{className:"ds-btn",disabled:_||Ue.length===0,title:"只补跑上次不通过、且有接口定义的用例",onClick:()=>void Tn()},`↻ 只跑上次失败的 ${Ue.length} 条`),a("span",{className:"ds-hint"},"按卡串行跑,每张卡一轮;失败的照旧自动开缺陷(最多一次 60 条)")),a("div",{className:"ds-row",style:{marginTop:8,flexWrap:"wrap",gap:8}},a("span",{className:"ds-hint"},"回归套件:"),A.length===0?a("span",{className:"ds-hint"},"还没有套件。把当前筛选存成套件,夜间就自动跑它。"):A.map(V=>a("span",{key:V.id,className:"ds-suite","data-default":V.isDefault?"1":"0"},a("span",{style:{cursor:"pointer"},title:"点一下只看这个套件里的用例,再点一次取消",onClick:()=>F(me=>me===V.id?null:V.id)},`📦 ${V.name}${V.isDefault?" · 默认":""} · ${L[V.id]??0} 条${q===V.id?"(只看中)":""}`),a("button",{className:"ds-btn ds-btn-ghost",disabled:_||(L[V.id]??0)===0,onClick:()=>void H(V)},"▶ 跑"),V.isDefault?null:a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void be(V)},"设为默认"),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void _e(V),"data-danger":"1"},"删除"))),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Ce()},"+ 存为套件")),M===""?null:a("div",{className:"ds-hint",style:{marginTop:6}},M),$.length===0?null:a("div",{className:"ds-reg-list"},a("span",{className:"ds-hint"},"最近回归:"),$.slice(0,4).map(V=>a("span",{key:String(V.at),className:"ds-reg-item","data-bad":(Number(V.failed)||0)+(Number(V.failedCards)||0)>0?"1":"0",title:`${new Date(V.at).toLocaleString("zh-CN")} · ${V.by||"人"}`},`${Ye(V.at)} ${V.by||""} · ${V.cards} 卡 / ${V.total} 条 · ✅${V.passed} ❌${V.failed}${V.bugs>0?` 🐞${V.bugs}`:""}`))),x!==null?a("div",{className:"ds-err"},x):null,f===null?a("div",{className:"ds-hint",style:{padding:"10px 2px"}},"载入用例…"):fe.length===0?a("div",{className:"ds-empty"},"没有符合条件的用例。用例登记在卡片详情的「测试」里。"):a("div",{className:"ds-case-lib"},fe.map(V=>a("div",{key:V.key,className:"ds-case-lib-wrap"},a("div",{className:"ds-case-lib-row","data-result":V.result,title:"打开所属卡片",onClick:()=>t(V.taskId)},a("span",{className:"ds-case-lib-pick",onClick:me=>me.stopPropagation()},a(p,{checked:C.has(V.key),disabled:_||!V.http,onChange:()=>ae(V.key)})),a("span",{className:"ds-case-lib-result"},Vl[V.result]??V.result),a("span",{className:"ds-case-lib-title"},V.title),a("span",{className:"ds-case-lib-meta"},`${V.taskCode||V.taskId} · ${V.requirementTitle||"未挂需求"}${V.pointId?" · "+V.pointId:""}`),a("span",{className:"ds-case-lib-meta"},V.http?"接口":"手工"),a("span",{className:"ds-case-lib-pri"},V.priority),a("span",{className:"ds-case-lib-at"},V.lastRunAt>0?Ye(V.lastRunAt):"没跑过"),a("button",{className:"ds-btn ds-btn-ghost ds-case-lib-more",title:"看最近一次执行的明细",onClick:me=>{me.stopPropagation(),O(I===V.key?null:V.key)}},I===V.key?"收起":"明细")),I===V.key?a("div",{className:"ds-case-lib-detail"},a("div",null,`期望:${V.expected||"—"}`),a("div",null,`最近一次:${V.note||(V.lastRunAt>0?"只有结果,没有记录":"还没跑过")}`),V.lastError===""?null:a("div",{className:"ds-err"},`错误:${V.lastError}`),V.lastPreview===""?null:a("pre",{className:"ds-case-lib-preview"},V.lastPreview),a("div",{className:"ds-hint"},`用例编号 ${V.id} · 所属卡片 ${V.taskCode||V.taskId}`)):null))))))}function Xx({value:e,tone:t}){const{h:a,kit:n}=ht(),r=n("ProgressBar"),s=Math.max(0,Math.min(100,Number(e)||0));if(r!==null)return a(r,{value:s,tone:t});const o=t==="over"?"#e5484d":t==="warn"?"#d97706":void 0;return a("div",{className:"ds-an-track"},a("div",{className:"ds-an-fill",style:{width:`${s}%`,background:o}}))}function Zx({series:e}){const{h:t,kit:a}=ht(),n=Array.isArray(e)?e:[];if(n.length===0)return t("div",{className:"ds-empty"},"还没有数据。");const r=n.map(u=>Math.max(0,Number(u.remaining)||0)),s=Math.max(1,...r),o=n.length===1?100:100/(n.length-1),i=n.map((u,p)=>({x:p*o,y:38-r[p]/s*32,remaining:r[p],day:String(u.day??"")})),l=i.map((u,p)=>`${p===0?"M":"L"}${u.x.toFixed(2)},${u.y.toFixed(2)}`).join(" "),c=`${l} L100,40 L0,40 Z`;return t("div",{className:"ds-burn"},t("svg",{viewBox:"0 0 100 40",preserveAspectRatio:"none",className:"ds-burn-svg",role:"img","aria-label":"燃尽趋势"},t("path",{d:c,className:"ds-burn-area"}),t("path",{d:l,className:"ds-burn-line",vectorEffect:"non-scaling-stroke"}),i.map(u=>t("circle",{key:u.day,cx:u.x,cy:u.y,r:1.1,className:"ds-burn-dot"},t("title",{},`${u.day}:还剩 ${u.remaining} 张`)))),t("div",{className:"ds-burn-axis"},t("span",{},i[0].day),t("span",{},i[i.length-1].day)))}const ed=800;function rf(e){const t=String(e??"").replace(/\r\n?/g,`
42
42
  `);if(t==="")return[];const a=t.split(`
43
43
  `);return a.length>0&&a[a.length-1]===""&&a.pop(),a}function Jx(e,t){const a=rf(e),n=rf(t);if(a.length>ed||n.length>ed)return[{kind:"del",text:`(原基线 ${a.length} 行,超过 ${ed} 行的对比上限)`},{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 ew(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 tw({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}=ht(),h=e.requirements??[],y=e.config.projects??[],[S,C]=o.useState(h.length>0?h[0].id:null),E=()=>{R({title:"",description:"",project:"",iteration:"",owner:"",priority:"normal",status:"draft",links:{}}),G(null)};o.useEffect(()=>{Number(n)>0&&E()},[n]),o.useEffect(()=>{r&&r.id&&C(r.id)},[r]);const[_,R]=o.useState(null),[A,G]=o.useState(null),[L,P]=o.useState(!1),[W,J]=o.useState(null),[q,F]=o.useState(null),[M,w]=o.useState(null),[$,Y]=o.useState(null),[I,O]=o.useState(null),[K,Ne]=o.useState(!1),[fe,xe]=o.useState(null),[He,Se]=o.useState(!1),[U,ae]=o.useState(null),[Te,Z]=o.useState(null),[le,he]=o.useState(null),[H,Ce]=o.useState(""),[be,_e]=o.useState(null),[Ue,qa]=o.useState("0"),[Tn,V]=o.useState([]),[me,Ae]=o.useState(null),ze=h.find(Q=>Q.id===S)??null;o.useEffect(()=>{qa(String(ze?.order??0)),V(Array.isArray(ze?.dependsOn)?ze.dependsOn:[]),Ae(null)},[S,ze?.updatedAt]);const At=async()=>{if(ze!==null)try{await i("/requirement/save",{requirement:{id:ze.id,title:ze.title,description:ze.description??"",project:ze.project??"",iteration:ze.iteration??"",owner:ze.owner??"",priority:ze.priority??"normal",status:ze.status??"draft",links:ze.links??{},order:Number(Ue)||0,dependsOn:Tn}}),Ae("已保存:排序与依赖"),l("排序与依赖已保存"),a!==void 0&&a()}catch(Q){Ae(Q instanceof Error?Q.message:String(Q))}},Vt=async Q=>{if(B===null)return;const z=String(H??"").trim();if(Q==="sign"&&z===""){_e("签字要写一句结论:交付了什么、遗留什么");return}try{await i("/requirement/signoff",{id:B.id,action:Q,note:z,by:c()}),_e(Q==="sign"?"已签字":"已取消签字"),l(Q==="sign"?"需求已签字":"已取消需求签字"),Ce(""),a!==void 0&&a()}catch(ie){_e(ie.message)}},Je=o.useCallback(async Q=>{if(Q==null||Q===""){ae(null);return}try{const z=await i("/requirement/matrix",{id:Q});ae(z.matrix)}catch(z){Z(z.message)}},[]),bt=async Q=>{if(B!==null)try{await i("/requirement/save",{requirement:{...B,links:B.links??{},points:Q}}),await Je(B.id),Z("功能点已保存"),he(null)}catch(z){Z(z.message)}},ls=()=>({title:"",description:"",priority:"normal",status:"draft",acceptance:[]}),wa=async()=>{if(le===null||B===null)return;if(String(le.title??"").trim()===""){Z("功能点要有标题");return}const Q=String(le.acceptanceText??"").split(`
44
- `).map(Le=>Le.trim()).filter(Le=>Le!==""),z={id:le.id??`p${(B.points??[]).length+1}`,title:le.title,description:le.description??"",priority:le.priority??"normal",status:le.status??"draft",acceptance:Q.map((Le,Ve)=>({id:`a${Ve+1}`,text:Le,done:!1}))},ie=(B.points??[]).filter(Le=>Le.id!==z.id);await bt([...ie,z])},Is=async Q=>{B!==null&&await u("删除这个功能点?挂在上面的卡会变成「未归功能点」。",{title:"删除功能点",danger:!0,okText:"删除"})&&await bt((B.points??[]).filter(z=>z.id!==Q))},X=async(Q,z)=>{try{await i("/task/update",{id:Q,pointId:z}),B!==null&&await Je(B.id)}catch(ie){Z(ie.message)}},B=h.find(Q=>Q.id===S)??null,ee=Q=>e.tasks.filter(z=>z.requirementId===Q),ve=Q=>e.statuses.find(z=>z.id===Q)?.label??Q;o.useEffect(()=>{ae(null),Z(null),B!==null&&Je(B.id)},[B===null?"":B.id,Je]);const te=async()=>{if(_!==null){if(String(_.title??"").trim()===""){G("需求标题不能为空");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),R(null),G(null),l(Q.created===!0?"需求已创建":"需求已保存")}catch(Q){G(Q.message)}finally{P(!1)}}},oe=async Q=>{if(B!==null)try{await i("/requirement/save",{requirement:{...B,links:B.links??{},status:Q}}),l("需求状态已更新")}catch(z){G(z.message)}},de=async Q=>{if(await u("删除这条需求?它下面的卡会保留,只是不再挂需求。",{title:"删除需求",danger:!0,okText:"删除"}))try{await i("/requirement/delete",{id:Q}),C(null)}catch(z){G(z.message)}},ge={pending:"待评审",approved:"评审通过",changes_requested:"评审打回"},[we,ye]=o.useState(!1),[Ee,tt]=o.useState(null),at=async(Q,z)=>{if(B!==null){ye(!0),tt(null);try{const ie=await i("/requirement/review",{id:B.id,decision:Q,note:z??"",by:c(),operator:c()});tt(`已${Q==="approve"?"通过":Q==="reject"?"打回":"提交"}评审:${ge[ie.review?.status]??""}`),typeof a=="function"&&a()}catch(ie){tt(ie.message)}finally{ye(!1)}}},ct=async()=>{const Q=await p("打回要给意见(会记在评审记录里):",{title:"评审打回",danger:!0,okText:"打回",placeholder:"写清楚哪里不满足,之后要据此改"});if(Q!==null){if(String(Q).trim()===""){tt("打回必须写意见");return}at("reject",String(Q).trim().slice(0,600))}},[qe,nt]=o.useState(!1),[et,$e]=o.useState(null),Pt=async()=>{if(B!==null){nt(!0),$e(null);try{const z=(await i("/requirement/split",{id:B.id})).created??[];$e(z.length===0?"没有可拆的功能点:要么都覆盖了,要么还没加功能点。":`已建 ${z.length} 张卡:${z.map(ie=>ie.code||ie.id).join("、")}`),typeof a=="function"&&a()}catch(Q){$e(Q.message)}finally{nt(!1)}}},Fe=()=>({kind:"prd",title:"",url:B?.links?.prd??"",version:"v1.0",content:"",status:"draft"}),yt=async Q=>{if(F(null),Q===null){J(Fe());return}try{const ie=((await i("/requirement/get",{id:B.id})).requirement?.docs??[]).find(Le=>Le.id===Q);J(ie===void 0?Fe():{...ie})}catch(z){G(z.message)}},jt=async Q=>{if(M!==null&&M.id===Q){w(null);return}w({id:Q,content:"",busy:!0});try{const ie=((await i("/requirement/get",{id:B.id})).requirement?.docs??[]).find(Le=>Le.id===Q);w({id:Q,content:String(ie?.content??""),busy:!1})}catch(z){G(z.message),w(null)}},Qt=async()=>{if($!==null){Y(null);return}try{const z=(await i("/requirement/get",{id:B.id})).requirement??{},ie=z.baseline??null,Ve=[...z.docs??[]].sort((_n,Do)=>(Number(Do.at)||0)-(Number(_n.at)||0)),Ge=Ve.find(_n=>_n.kind==="prd")??Ve[0]??null;if(ie===null){Y(null),G("这条需求还没有基线,先挑一份文档「设为基线」");return}if(Ge===null){Y(null),G("这条需求还没有文档可以对比");return}const We=Jx(ie.content??"",Ge.content??"");Y({rows:We,summary:ew(We),version:ie.version,docTitle:Ge.title})}catch(Q){Y(null),G(Q instanceof Error?Q.message:String(Q))}},re=async()=>{if(I!==null){O(null);return}try{const Q=await i("/requirement/get",{id:B.id}),z={};for(const ie of Q.requirement?.docs??[])z[ie.id]=String(ie.content??"");O(z)}catch(Q){G(Q.message)}},ke=async()=>{if(W!==null){if(String(W.title??"").trim()===""){F("文档要有标题");return}Ne(!0);try{const Q=await i("/requirement/doc",{id:B.id,action:W.id===void 0?"add":"update",docId:W.id,doc:{kind:W.kind,title:W.title,url:W.url,version:W.version,content:W.content,status:W.status}});J(null),F(null)}catch(Q){F(Q.message)}finally{Ne(!1)}}},pe=async()=>{Se(!0),xe(null);try{const Q=await i("/requirement/folder",{id:B.id});xe(Q.ok?`云盘文件夹已就绪:${Q.folder?.url??""}`:`创建失败:${Q.error||Q.folder?.error||"未知原因"}`)}catch(Q){xe(Q.message)}finally{Se(!1)}},Va=async()=>{Se(!0),xe(null);try{const z=(await i("/requirement/sync",{id:B.id})).report??{},ie=Number(z.failed??0);xe(`归档完成:共 ${z.total??0} 件,成功 ${z.written??0} 件${ie>0?`,失败 ${ie} 件(${z.errors?.[0]??""})`:"。"}`)}catch(Q){xe(Q.message)}finally{Se(!1)}},St=async Q=>{try{await i("/requirement/doc",{id:B.id,action:"baseline",docId:Q,by:"人"}),F("这一版已生效,之后新建的卡都会记下它。")}catch(z){F(z.message)}},la=async()=>{try{await i("/requirement/doc",{id:B.id,action:"clear-baseline"}),F("基线已取消,需求回到「评审中」。")}catch(Q){F(Q.message)}},Pn=async Q=>{if(await u("删除这份文档?",{title:"删除文档",danger:!0,okText:"删除"}))try{await i("/requirement/doc",{id:B.id,action:"remove",docId:Q})}catch(z){F(z.message)}},Ra=async()=>{Ne(!0),F(null);try{const Q=await i("/requirement/pull",{url:W?.url??""});if(Q.ok===!0){const z=String(Q.text??"");J(ie=>({...ie,title:String(ie?.title??"").trim()===""?String(Q.title??""):ie.title,content:z})),F(`已拉取 ${z.length} 字正文${Q.title?`,标题取自文档「${Q.title}」`:""}。`)}else F(Q.error??"拉取失败")}catch(Q){F(Q.message)}finally{Ne(!1)}},Dt=_===null?null:s("div",{className:"ds-panel ds-form-panel"},s("div",{className:"ds-panel-head"},_.id===void 0?"新建需求":`编辑 ${B?.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=>R({..._,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=>R({..._,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=>R({..._,status:Q.target.value})},Yp.map(Q=>s("option",{key:Q,value:Q},Tr[Q])))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务负责人"),s(v,{className:"ds-input",value:_.owner,onChange:Q=>R({..._,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=>R({..._,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=>R({..._,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=>R({..._,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=>R({..._,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=>R({..._,description:Q.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{R(null),G(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:L,onClick:()=>void te()},L?"保存中…":"保存需求")))),Ot=B===null?s("div",{className:"ds-empty"},"左边选一条需求,或先新建一条。"):(()=>{const Q=ee(B.id),z=Q.filter(j=>j.status==="done").length,ie=Q.filter(j=>j.blocked!==void 0&&j.blocked!==null).length,Le=Q.filter(j=>j.type==="bug"&&j.status!=="done").length,Ve=Q.reduce((j,ce)=>{const De=Kl(ce);return{done:j.done+De.done,total:j.total+De.total}},{done:0,total:0}),Ge=Q.reduce((j,ce)=>{const De=Gl(ce);return{pass:j.pass+De.pass,total:j.total+De.total}},{pass:0,total:0}),We=Q.flatMap(j=>(j.artifacts??[]).map(ce=>({...ce,task:j}))),_n=Q.filter(j=>(j.gitlabMR?.iid??0)>0),Do=Ge.pass,an=B.links??{},nn=B.folder??{},ds=(j,ce)=>ce===""?null:s("a",{className:"ds-link",href:ce,target:"_blank",rel:"noreferrer"},j);return s("div",{className:"ds-req-body"},s("div",{className:"ds-req-headline"},s("div",null,s("div",{className:"ds-req-code"},B.code||B.id),s("div",{className:"ds-req-name"},B.title)),s("div",{className:"ds-req-actions"},s(g,{className:"ds-select",value:B.status,onChange:j=>void oe(j.target.value)},Yp.map(j=>s("option",{key:j,value:j},Tr[j]))),s("button",{className:"ds-btn",onClick:()=>R({...B,...an})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void de(B.id),"data-danger":"1"},"删除"))),(()=>{const j=B.review??null,ce=j===null?"还没提交评审":ge[j.status]??j.status,De=j===null||j.status==="changes_requested",xt=j!==null&&j.status==="pending";return s("div",{className:"ds-panel","data-review":j===null?"none":j.status,style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求评审"),s(b,{className:"ds-chip","data-on":j?.status==="approved"?"1":"0",style:{marginLeft:8}},ce),j===null||j.at===0?null:s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ye(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},j!==null&&String(j.note??"")!==""?s("div",{className:"ds-hint",style:{marginBottom:8}},`评审意见:${j.note}`):null,s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},De?s("button",{className:"ds-btn ds-btn-primary",disabled:we,onClick:()=>void at("submit")},we?"提交中…":"提交评审"):null,xt?s("button",{className:"ds-btn ds-btn-primary",disabled:we,onClick:()=>void at("approve")},"评审通过"):null,xt?s("button",{className:"ds-btn ds-btn-ghost",disabled:we,onClick:ct,"data-danger":"1"},"打回"):null,j?.status==="approved"?s("span",{className:"ds-hint"},"评审已通过,可以拆卡和派活了。"):null),Ee===null?null:s("div",{className:"ds-hint",style:{marginTop:8}},Ee)))})(),ds("PRD",an.prd)||ds("原型",an.proto)||ds("验收标准",an.acceptance)?s("div",{className:"ds-req-links"},ds("PRD 文档",an.prd),ds("原型",an.proto),ds("验收标准",an.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 j=await p("想请产品补什么?",{title:"申请产品补 PRD",defaultValue:"需要产品输出 PRD 与原型"});if(j!==null)try{await i("/requirement/request-prd",{id:B.id,by:c(),note:j}),l("已申请产品补 PRD"),a()}catch(ce){l(ce.message??String(ce),"error")}}},"让产品补 PRD")]),B.description?s("div",{className:"ds-panel ds-req-desc-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求明细"),B.source==="business"?s("span",{className:"ds-hint","data-src":"business"},`业务提单 · ${B.requester?.name||"未署名"}${B.requester?.dept?"("+B.requester.dept+")":""} · ${Ye(B.requester?.at||B.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(B.description)))):s("div",{className:"ds-req-desc ds-hint"},"还没有写需求描述:产品背景、要解决的问题、验收口径都建议写在这里,点右侧「编辑」补上。"),(()=>{const j=(B.history??[]).slice().reverse();if(j.length===0||(B.change??null)!==null)return null;const ce={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"}},`最近 ${j.length} 条`)),s("div",{className:"ds-panel-body"},s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto"}},j.map((De,xt)=>s("div",{key:`${De.at}-${xt}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint",style:{flex:"0 0 auto",fontVariantNumeric:"tabular-nums"}},Ye(De.at)),s("span",{style:{flex:"0 0 auto"}},De.by===""?"人":De.by),s("span",{className:"ds-chip",style:{flex:"0 0 auto"}},ce[De.field]??De.field),s("span",{className:"ds-hint",style:{minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},`${De.from===""?"(空)":De.from} → ${De.to===""?"(空)":De.to}`))))))})(),(()=>{const j=B.pool??{reach:0,impact:0,confidence:0,effort:0,score:0},ce=async(xt,da,st)=>{const Ie=await p(da,{title:xt,defaultValue:String(st??0)});if(Ie===null)return null;const Ke=Number(String(Ie).trim());return Number.isFinite(Ke)&&Ke>=0?Ke:null},De=async()=>{const xt=await ce("影响面 Reach","影响多少用户或多少笔交易?估算依据写进正文",j.reach);if(xt===null)return;const da=await ce("影响度 Impact","3 显著 / 2 高 / 1 中 / 0.5 低",j.impact);if(da===null)return;const st=await ce("置信度 Confidence","100 有数据 / 80 有访谈 / 50 只有猜测(百分数)",j.confidence);if(st===null)return;const Ie=await ce("成本 Effort","人日(研发+测试+联调)",j.effort);if(Ie!==null)try{await i("/requirement/pool",{id:B.id,pool:{reach:xt,impact:da,confidence:st,effort:Ie}}),l("需求池评分已更新"),await a()}catch(Ke){l(Ke instanceof Error?Ke.message:String(Ke),"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"}},j.score>0?`得分 ${j.score}`:"还没打分:不打分就没法和别的需求比优先级"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void De()},j.score>0?"重新打分":"打分")),j.score>0?s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},s("span",{className:"ds-chip"},`影响面 ${j.reach}`),s("span",{className:"ds-chip"},`影响度 ${j.impact}`),s("span",{className:"ds-chip"},`置信度 ${j.confidence}%`),s("span",{className:"ds-chip"},`成本 ${j.effort} 人日`),s("span",{className:"ds-chip","data-on":"1"},`得分 ${j.score}`))):null)})(),(()=>{const j=B.questions??[],ce=j.filter(Ie=>Ie.status==="open").length,De=async(Ie,Ke)=>{try{await i("/requirement/questions",{id:B.id,...Ie}),Ke&&l(Ke),await a()}catch(sn){l(sn instanceof Error?sn.message:String(sn),"error")}},xt=async()=>{const Ie=await p("还没确认清楚的问题是什么?",{title:"加一个待确认问题",placeholder:"例如:退款失败时是否允许人工补发?"});Ie===null||Ie.trim()===""||await De({action:"add",text:Ie},"已加入待确认清单")},da=async Ie=>{const Ke=await p(Ie.text,{title:"回答这个问题",defaultValue:Ie.answer??"",placeholder:"写清结论与依据"});Ke===null||Ke.trim()===""||await De({action:"answer",questionId:Ie.id,answer:Ke},"已回答")},st=async Ie=>{await u("删掉这个问题?",{title:"删除待确认项",danger:!0,okText:"删除"})===!0&&await De({action:"delete",questionId:Ie.id},"已删除")};return s("div",{className:"ds-panel ds-question-panel","data-open":ce>0?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"待确认清单"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},ce>0?`${ce} 个没回答,回答完才能拆卡`:j.length===0?"澄清阶段把不确定的先记在这里":"全部已回答"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void xt()},"+ 加问题")),s("div",{className:"ds-panel-body"},j.length===0?s("div",{className:"ds-empty"},"还没有待确认的问题。产品澄清时把「需要业务拍板」的点写在这里,避免猜业务规则。"):s("div",{style:{display:"flex",flexDirection:"column",gap:8}},j.map(Ie=>s("div",{key:Ie.id,className:"ds-question-row","data-status":Ie.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":Ie.status==="answered"?"1":"0"},Ie.status==="answered"?"已回答":"待回答"),s("div",{style:{flex:"1 1 auto",minWidth:0}},s("div",{style:{fontSize:13}},Ie.text),Ie.answer===""||Ie.answer===void 0?null:s("div",{className:"ds-hint",style:{marginTop:4}},`答:${Ie.answer}`)),s("div",{style:{display:"flex",gap:6,flex:"0 0 auto"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void da(Ie)},Ie.status==="answered"?"改答案":"回答"),s("button",{className:"ds-btn ds-btn-ghost","data-danger":"1",onClick:()=>void st(Ie)},"删除")))))))})(),(()=>{const j=B.change??null;if(j===null)return null;const ce=j.affected??[],De=j.affectedCases??[],xt=new Map(Q.map(st=>[st.id,st])),da=st=>e.statuses.find(Ie=>Ie.id===st)?.label??st;return s("div",{className:"ds-panel ds-change-panel","data-invalidated":j.invalidated?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,j.invalidated?"变更影响(签字已作废)":"变更影响"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ye(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},function(){const st=(B.history??[]).slice().reverse();if(st.length===0)return null;const Ie={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}},`变更记录 · 最近 ${st.length} 条`),st.map((Ke,sn)=>s("div",{key:`${Ke.at}-${sn}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint"},Ye(Ke.at)),s("span",null,Ke.by===""?"人":Ke.by),s("span",{className:"ds-chip"},Ie[Ke.field]??Ke.field),s("span",{className:"ds-hint"},`${Ke.from===""?"(空)":Ke.from} → ${Ke.to===""?"(空)":Ke.to}`))))}(),s("div",{className:"ds-hint"},`改了:${(j.parts??[]).join("、")}。${ce.length===0?"当时没有已产出的卡受影响,不用返工。":`${ce.length} 张卡已经产出过东西${De.length===0?"":`、${De.length} 条用例要重跑`},要按新版复核:`}`),ce.length===0?null:s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8,marginTop:8}},ce.map(st=>s("span",{key:st.id,className:"ds-chip","data-pt":"open",style:{cursor:"pointer"},title:st.title,onClick:()=>{const Ie=xt.get(st.id);Ie!==void 0&&typeof t=="function"&&t(Ie)}},`${st.code||st.id} ${da(st.status)}`))),j.invalidated?s("div",{className:"ds-hint",style:{marginTop:8}},"验收签字已清空:这些卡是按旧版做的,复核完重新签一次。"):null))})(),(()=>{const j=B.deploys??[];return j.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}},j.map(ce=>s("span",{key:ce.id,className:"ds-chip","data-env":ce.kind,title:`${ce.releaseName??""} · ${Ye(ce.at)} · ${ce.by}`},`${ce.name} · ${Wa(ce.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:Ue,onChange:j=>qa(j.target.value)}),s("span",{className:"ds-hint"},"被依赖的需求没交付,这条需求的准出会被拦")),s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:10}},h.filter(j=>j.id!==B.id).map(j=>s("label",{key:j.id,className:"ds-check-row"},s(k,{checked:Tn.includes(j.id),onChange:ce=>V(De=>ce.target.checked?[...De,j.id]:De.filter(xt=>xt!==j.id))}),s("span",{className:"ds-check-text"},`${j.code||j.id} ${j.title}(${Tr[j.status]??j.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 At()},"保存排序与依赖"),me===null?null:s("span",{className:"ds-hint"},me)))),(()=>{const j=B.progress??null,ce=j?.gate??{ready:!1,blockers:["还没有加载出准出状态,稍后自动刷新"],warnings:[]},De=ce.stats??{},xt=B.signoff??null,da=j?.progress??(Q.length===0?0:Math.round(z/Q.length*100)),st=De.coveredPoints??0,Ie=De.points??(B.points??[]).length,Ke=De.pass??Do,sn=De.cases??Ge.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":ce.ready?"covered":"open",style:{marginLeft:"auto"}},ce.ready?"可以交付":`还差 ${ce.blockers.length} 项`)),s("div",{className:"ds-panel-body"},s("div",{className:"ds-check-progress"},s("div",{className:"ds-check-bar","data-full":da===100?"1":"0"},s("span",{style:{width:`${da}%`}})),s("div",{className:"ds-hint"},`${da}% · ${z}/${Q.length} 张卡完成 · 功能点覆盖 ${st}/${Ie} · 用例通过 ${Ke}/${sn}`)),(ce.blockers??[]).map(In=>s("div",{key:In,className:"ds-gate-item","data-kind":"block"},`⛔ ${In}`)),(ce.warnings??[]).map(In=>s("div",{key:In,className:"ds-gate-item","data-kind":"warn"},`⚠ ${In}`)),ce.ready?s("div",{className:"ds-hint"},"门禁已通过:签完字这条需求就算交付。"):null,xt===null?s("div",{className:"ds-gate-sign"},s(v,{className:"ds-input",style:{flex:1,minWidth:220},placeholder:"验收结论:交付了什么、遗留什么",value:H,onChange:In=>Ce(In.target.value)}),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void Vt("sign")},"需求验收签字")):s("div",{className:"ds-gate-sign"},s("div",{className:"ds-hint",style:{flex:1}},`✍ ${xt.by} 于 ${Ye(xt.at)} 签字:${xt.note}`),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Vt("clear")},"取消签字")),be===null?null:s("div",{className:"ds-hint"},be)))})(),s("div",{className:"ds-sec-head",style:{marginTop:16}},"交付物归档(飞书云盘)",s("div",{className:"ds-req-actions"},nn.url?s("a",{className:"ds-link",href:nn.url,target:"_blank",rel:"noreferrer"},"打开文件夹"):null,s("button",{className:"ds-btn ds-btn-ghost",disabled:He,onClick:()=>void pe()},nn.url?"校验文件夹":"创建文件夹"),s("button",{className:"ds-btn",disabled:He||We.length===0,onClick:()=>void Va()},He?"归档中…":`归档 ${We.length} 件交付物`))),s("div",{className:"ds-hint"},nn.url?`文件夹:${nn.name||nn.token} 已归档 ${We.filter(j=>j.doc?.url).length}/${We.length} 件交付物`:nn.error?`上次创建失败:${nn.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 yt(null)},"+ 添加文档")),B.baseline?s("div",{className:"ds-base-box"},s("div",{className:"ds-base-main"},s("b",null,`当前基线 ${B.baseline.version}`),s("span",null,`${B.baseline.title} · 生效于 ${Ye(B.baseline.at)} · ${B.baseline.by||"人"} · 正文 ${B.baseline.contentLength??0} 字`)),s("button",{className:"ds-btn ds-btn-ghost",title:"把当前文档和这份基线逐行比一遍(只读)",onClick:()=>void Qt()},$===null?"对比基线":"收起对比"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void la()},"取消基线")):s("div",{className:"ds-base-box","data-empty":"1"},s("div",{className:"ds-base-main"},s("b",null,"还没有基线"),s("span",null,"PRD 评审通过后点「设为基线」,之后建的卡都会记下用的是哪一版。"))),B.baseline!==null&&$!==null?s("div",{className:"ds-diff"},s("div",{className:"ds-diff-head"},s("b",null,`基线 ${$.version} → 当前:${$.docTitle}`),s("span",{className:"ds-hint"},$.summary.added+$.summary.removed===0?"正文没有变化":`+${$.summary.added} 行 / −${$.summary.removed} 行`)),s("div",{className:"ds-diff-body"},$.rows.slice(0,400).map((j,ce)=>s("div",{key:ce,className:"ds-diff-line","data-kind":j.kind},`${j.kind==="add"?"+":j.kind==="del"?"−":" "} ${j.text}`)),$.rows.length>400?s("div",{className:"ds-hint"},`(还有 ${$.rows.length-400} 行未显示)`):null)):null,(B.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 re()},I===null?"全部展开":"全部收起"),s("span",{className:"ds-hint"},`共 ${(B.docs??[]).length} 篇,展开后可直接读 Markdown 正文`)),s("div",{className:"ds-doc-list"},(B.docs??[]).map(j=>s("div",{key:j.id,className:"ds-doc-wrap"},s("div",{className:"ds-doc-row"},s(b,{className:"ds-chip","data-doc":j.kind},Qp[j.kind]??j.kind),s("div",{className:"ds-doc-main"},s("div",{className:"ds-doc-title"},j.url===""?j.title:s("a",{className:"ds-link",href:j.url,target:"_blank",rel:"noreferrer"},j.title),s("span",{className:"ds-chip-code"},j.version),s(b,{className:"ds-chip","data-docstatus":j.status},rx[j.status]??j.status)),s("div",{className:"ds-doc-meta"},`正文 ${j.contentLength??0} 字 · ${j.by||"—"} · ${j.at>0?Ye(j.at):"—"}`)),s("div",{className:"ds-listrow-actions"},j.status==="baseline"?null:s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void St(j.id)},"设为基线"),s("button",{className:"ds-btn ds-btn-ghost",title:"直接读正文(Markdown 渲染)",onClick:()=>void jt(j.id)},M!==null&&M.id===j.id?"收起":"阅读"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void yt(j.id)},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Pn(j.id),"data-danger":"1"},"删除"))),M!==null&&M.id===j.id?s("div",{className:"ds-md-preview ds-reader"},M.busy?s("span",{className:"ds-hint"},"读取中…"):M.content.trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(M.content)):I!==null&&I[j.id]!==void 0?s("div",{className:"ds-md-preview ds-reader"},I[j.id].trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(I[j.id])):null)))),q===null?null:s("div",{className:"ds-hint",style:{marginTop:6}},q),W===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},W.id===void 0?"添加文档":`编辑 ${W.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:W.kind,onChange:j=>J({...W,kind:j.target.value})},sx.map(j=>s("option",{key:j,value:j},Qp[j])))),s("div",{className:"ds-field",style:{flex:"1 1 220px"}},s("div",{className:"ds-label"},"标题"),s(v,{className:"ds-input",value:W.title,placeholder:"例如 会员积分抵扣 PRD",onChange:j=>J({...W,title:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 110px"}},s("div",{className:"ds-label"},"版本"),s(v,{className:"ds-input",value:W.version,placeholder:"v1.0",onChange:j=>J({...W,version:j.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:W.url,placeholder:"https://laiyifen.feishu.cn/wiki/…",onChange:j=>J({...W,url:j.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:K,onClick:()=>void Ra()},K?"拉取中…":"从链接拉正文"))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"文档正文(拉不到就自己粘,研发和测试都读这一份)"),s(d,{className:"ds-textarea",rows:8,value:W.content,onChange:j=>J({...W,content:j.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:K,onClick:()=>void ke()},"保存文档")))),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"功能点与可追溯矩阵"),s("div",{className:"ds-pane-desc"},"功能点是需求的骨架:一条功能点对若干张卡、若干条用例。卡和用例上都挂 pointId,这里就能看出哪条没人做、哪条没被验证。")),U===null?s("div",{className:"ds-hint",style:{padding:"6px 0"}},"还没有功能点。点「+ 加功能点」把需求拆成可交付、可验证的条目。"):s("div",{className:"ds-matrix"},s("div",{className:"ds-matrix-sum"},`功能点 ${U.summary.points} · 已覆盖 ${U.summary.coveredPoints}${U.summary.pointsWithoutCases>0?`(${U.summary.pointsWithoutCases} 条还没有用例,覆盖率 ${U.summary.coverageRate}%)`:""} · 卡 ${U.summary.cards}(未归点 ${U.summary.looseCards})· 用例 ${U.summary.cases}(通过 ${U.summary.pass}${U.summary.fail>0?`,失败 ${U.summary.fail}`:""})· 通过率 ${U.summary.passRate}%`),(U.points??[]).map(j=>s("div",{key:j.id,className:"ds-pt-row","data-covered":j.covered?"1":"0"},s("div",{className:"ds-pt-main"},s("div",{className:"ds-pt-title"},s("span",{className:"ds-chip-code"},j.id),s("b",null,j.title),s(b,{className:"ds-chip","data-pt":j.covered?"covered":"open"},j.covered?"已覆盖":"未覆盖")),String(j.description??"")===""?null:s("div",{className:"ds-req-desc ds-md"},f(j.description)),s("div",{className:"ds-pt-meta"},`卡 ${j.cards.length}${j.cards.length===0?"(还没人做)":":"+j.cards.map(ce=>ce.code||ce.id).join("、")}`,` · 用例 ${j.cases.total}(通过 ${j.cases.pass}${j.cases.fail>0?`,失败 ${j.cases.fail}`:""}${j.cases.pending>0?`,没跑 ${j.cases.pending}`:""})`,j.acceptance.length===0?"":` · 验收条件 ${j.acceptance.filter(ce=>ce.covered).length}/${j.acceptance.length}`)),s("div",{className:"ds-listrow-actions"},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>he({...j,acceptanceText:(j.acceptance??[]).map(ce=>ce.text).join(`
44
+ `).map(Le=>Le.trim()).filter(Le=>Le!==""),z={id:le.id??`p${(B.points??[]).length+1}`,title:le.title,description:le.description??"",priority:le.priority??"normal",status:le.status??"draft",acceptance:Q.map((Le,Ve)=>({id:`a${Ve+1}`,text:Le,done:!1}))},ie=(B.points??[]).filter(Le=>Le.id!==z.id);await bt([...ie,z])},Is=async Q=>{B!==null&&await u("删除这个功能点?挂在上面的卡会变成「未归功能点」。",{title:"删除功能点",danger:!0,okText:"删除"})&&await bt((B.points??[]).filter(z=>z.id!==Q))},X=async(Q,z)=>{try{await i("/task/update",{id:Q,pointId:z}),B!==null&&await Je(B.id)}catch(ie){Z(ie.message)}},B=h.find(Q=>Q.id===S)??null,ee=Q=>e.tasks.filter(z=>z.requirementId===Q),ve=Q=>e.statuses.find(z=>z.id===Q)?.label??Q;o.useEffect(()=>{ae(null),Z(null),B!==null&&Je(B.id)},[B===null?"":B.id,Je]);const te=async()=>{if(_!==null){if(String(_.title??"").trim()===""){G("需求标题不能为空");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),R(null),G(null),l(Q.created===!0?"需求已创建":"需求已保存")}catch(Q){G(Q.message)}finally{P(!1)}}},oe=async Q=>{if(B!==null)try{await i("/requirement/save",{requirement:{...B,links:B.links??{},status:Q}}),l("需求状态已更新")}catch(z){G(z.message)}},de=async Q=>{if(await u("删除这条需求?它下面的卡会保留,只是不再挂需求。",{title:"删除需求",danger:!0,okText:"删除"}))try{await i("/requirement/delete",{id:Q}),C(null)}catch(z){G(z.message)}},ge={pending:"待评审",approved:"评审通过",changes_requested:"评审打回"},[we,ye]=o.useState(!1),[Ee,tt]=o.useState(null),at=async(Q,z)=>{if(B!==null){ye(!0),tt(null);try{const ie=await i("/requirement/review",{id:B.id,decision:Q,note:z??"",by:c(),operator:c()});tt(`已${Q==="approve"?"通过":Q==="reject"?"打回":"提交"}评审:${ge[ie.review?.status]??""}`),typeof a=="function"&&a()}catch(ie){tt(ie.message)}finally{ye(!1)}}},ct=async()=>{const Q=await p("打回要给意见(会记在评审记录里):",{title:"评审打回",danger:!0,okText:"打回",placeholder:"写清楚哪里不满足,之后要据此改"});if(Q!==null){if(String(Q).trim()===""){tt("打回必须写意见");return}at("reject",String(Q).trim().slice(0,600))}},[qe,nt]=o.useState(!1),[et,$e]=o.useState(null),Pt=async()=>{if(B!==null){nt(!0),$e(null);try{const z=(await i("/requirement/split",{id:B.id})).created??[];$e(z.length===0?"没有可拆的功能点:要么都覆盖了,要么还没加功能点。":`已建 ${z.length} 张卡:${z.map(ie=>ie.code||ie.id).join("、")}`),typeof a=="function"&&a()}catch(Q){$e(Q.message)}finally{nt(!1)}}},Fe=()=>({kind:"prd",title:"",url:B?.links?.prd??"",version:"v1.0",content:"",status:"draft"}),yt=async Q=>{if(F(null),Q===null){J(Fe());return}try{const ie=((await i("/requirement/get",{id:B.id})).requirement?.docs??[]).find(Le=>Le.id===Q);J(ie===void 0?Fe():{...ie})}catch(z){G(z.message)}},jt=async Q=>{if(M!==null&&M.id===Q){w(null);return}w({id:Q,content:"",busy:!0});try{const ie=((await i("/requirement/get",{id:B.id})).requirement?.docs??[]).find(Le=>Le.id===Q);w({id:Q,content:String(ie?.content??""),busy:!1})}catch(z){G(z.message),w(null)}},Qt=async()=>{if($!==null){Y(null);return}try{const z=(await i("/requirement/get",{id:B.id})).requirement??{},ie=z.baseline??null,Ve=[...z.docs??[]].sort((_n,Do)=>(Number(Do.at)||0)-(Number(_n.at)||0)),Ge=Ve.find(_n=>_n.kind==="prd")??Ve[0]??null;if(ie===null){Y(null),G("这条需求还没有基线,先挑一份文档「设为基线」");return}if(Ge===null){Y(null),G("这条需求还没有文档可以对比");return}const We=Jx(ie.content??"",Ge.content??"");Y({rows:We,summary:ew(We),version:ie.version,docTitle:Ge.title})}catch(Q){Y(null),G(Q instanceof Error?Q.message:String(Q))}},re=async()=>{if(I!==null){O(null);return}try{const Q=await i("/requirement/get",{id:B.id}),z={};for(const ie of Q.requirement?.docs??[])z[ie.id]=String(ie.content??"");O(z)}catch(Q){G(Q.message)}},ke=async()=>{if(W!==null){if(String(W.title??"").trim()===""){F("文档要有标题");return}Ne(!0);try{const Q=await i("/requirement/doc",{id:B.id,action:W.id===void 0?"add":"update",docId:W.id,doc:{kind:W.kind,title:W.title,url:W.url,version:W.version,content:W.content,status:W.status}});J(null),F(null)}catch(Q){F(Q.message)}finally{Ne(!1)}}},pe=async()=>{Se(!0),xe(null);try{const Q=await i("/requirement/folder",{id:B.id});xe(Q.ok?`云盘文件夹已就绪:${Q.folder?.url??""}`:`创建失败:${Q.error||Q.folder?.error||"未知原因"}`)}catch(Q){xe(Q.message)}finally{Se(!1)}},Va=async()=>{Se(!0),xe(null);try{const z=(await i("/requirement/sync",{id:B.id})).report??{},ie=Number(z.failed??0);xe(`归档完成:共 ${z.total??0} 件,成功 ${z.written??0} 件${ie>0?`,失败 ${ie} 件(${z.errors?.[0]??""})`:"。"}`)}catch(Q){xe(Q.message)}finally{Se(!1)}},St=async Q=>{try{await i("/requirement/doc",{id:B.id,action:"baseline",docId:Q,by:"人"}),F("这一版已生效,之后新建的卡都会记下它。")}catch(z){F(z.message)}},la=async()=>{try{await i("/requirement/doc",{id:B.id,action:"clear-baseline"}),F("基线已取消,需求回到「评审中」。")}catch(Q){F(Q.message)}},Pn=async Q=>{if(await u("删除这份文档?",{title:"删除文档",danger:!0,okText:"删除"}))try{await i("/requirement/doc",{id:B.id,action:"remove",docId:Q})}catch(z){F(z.message)}},Ra=async()=>{Ne(!0),F(null);try{const Q=await i("/requirement/pull",{url:W?.url??""});if(Q.ok===!0){const z=String(Q.text??"");J(ie=>({...ie,title:String(ie?.title??"").trim()===""?String(Q.title??""):ie.title,content:z})),F(`已拉取 ${z.length} 字正文${Q.title?`,标题取自文档「${Q.title}」`:""}。`)}else F(Q.error??"拉取失败")}catch(Q){F(Q.message)}finally{Ne(!1)}},Dt=_===null?null:s("div",{className:"ds-panel ds-form-panel"},s("div",{className:"ds-panel-head"},_.id===void 0?"新建需求":`编辑 ${B?.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=>R({..._,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=>R({..._,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=>R({..._,status:Q.target.value})},Yp.map(Q=>s("option",{key:Q,value:Q},Tr[Q])))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务负责人"),s(v,{className:"ds-input",value:_.owner,onChange:Q=>R({..._,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=>R({..._,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=>R({..._,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=>R({..._,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=>R({..._,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=>R({..._,description:Q.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{R(null),G(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:L,onClick:()=>void te()},L?"保存中…":"保存需求")))),Ot=B===null?s("div",{className:"ds-empty"},"左边选一条需求,或先新建一条。"):(()=>{const Q=ee(B.id),z=Q.filter(j=>j.status==="done").length,ie=Q.filter(j=>j.blocked!==void 0&&j.blocked!==null).length,Le=Q.filter(j=>j.type==="bug"&&j.status!=="done").length,Ve=Q.reduce((j,ce)=>{const De=Kl(ce);return{done:j.done+De.done,total:j.total+De.total}},{done:0,total:0}),Ge=Q.reduce((j,ce)=>{const De=Gl(ce);return{pass:j.pass+De.pass,total:j.total+De.total}},{pass:0,total:0}),We=Q.flatMap(j=>(j.artifacts??[]).map(ce=>({...ce,task:j}))),_n=Q.filter(j=>(j.gitlabMR?.iid??0)>0),Do=Ge.pass,an=B.links??{},nn=B.folder??{},ds=(j,ce)=>ce===""?null:s("a",{className:"ds-link",href:ce,target:"_blank",rel:"noreferrer"},j);return s("div",{className:"ds-req-body"},s("div",{className:"ds-req-headline"},s("div",null,s("div",{className:"ds-req-code"},B.code||B.id),s("div",{className:"ds-req-name"},B.title)),s("div",{className:"ds-req-actions"},s(g,{className:"ds-select",value:B.status,onChange:j=>void oe(j.target.value)},Yp.map(j=>s("option",{key:j,value:j},Tr[j]))),s("button",{className:"ds-btn",onClick:()=>R({...B,...an})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void de(B.id),"data-danger":"1"},"删除"))),(()=>{const j=B.review??null,ce=j===null?"还没提交评审":ge[j.status]??j.status,De=j===null||j.status==="changes_requested",xt=j!==null&&j.status==="pending";return s("div",{className:"ds-panel","data-review":j===null?"none":j.status,style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求评审"),s(b,{className:"ds-chip","data-on":j?.status==="approved"?"1":"0",style:{marginLeft:8}},ce),j===null||j.at===0?null:s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ye(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},j!==null&&String(j.note??"")!==""?s("div",{className:"ds-hint",style:{marginBottom:8}},`评审意见:${j.note}`):null,s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},De?s("button",{className:"ds-btn ds-btn-primary",disabled:we,onClick:()=>void at("submit")},we?"提交中…":"提交评审"):null,xt?s("button",{className:"ds-btn ds-btn-primary",disabled:we,onClick:()=>void at("approve")},"评审通过"):null,xt?s("button",{className:"ds-btn ds-btn-ghost",disabled:we,onClick:ct,"data-danger":"1"},"打回"):null,j?.status==="approved"?s("span",{className:"ds-hint"},"评审已通过,可以拆卡和派活了。"):null),Ee===null?null:s("div",{className:"ds-hint",style:{marginTop:8}},Ee)))})(),ds("PRD",an.prd)||ds("原型",an.proto)||ds("验收标准",an.acceptance)?s("div",{className:"ds-req-links"},ds("PRD 文档",an.prd),ds("原型",an.proto),ds("验收标准",an.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 j=await p("想请产品补什么?",{title:"申请产品补 PRD",defaultValue:"需要产品输出 PRD 与原型"});if(j!==null)try{await i("/requirement/request-prd",{id:B.id,by:c(),note:j}),l("已申请产品补 PRD"),a()}catch(ce){l(ce.message??String(ce),"error")}}},"让产品补 PRD")]),B.description?s("div",{className:"ds-panel ds-req-desc-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求明细"),B.source==="business"?s("span",{className:"ds-hint","data-src":"business"},`业务提单 · ${B.requester?.name||"未署名"}${B.requester?.dept?"("+B.requester.dept+")":""} · ${Ye(B.requester?.at||B.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(B.description)))):s("div",{className:"ds-req-desc ds-hint"},"还没有写需求描述:产品背景、要解决的问题、验收口径都建议写在这里,点右侧「编辑」补上。"),(()=>{const j=(B.history??[]).slice().reverse();if(j.length===0||(B.change??null)!==null)return null;const ce={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"}},`最近 ${j.length} 条`)),s("div",{className:"ds-panel-body"},s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto"}},j.map((De,xt)=>s("div",{key:`${De.at}-${xt}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint",style:{flex:"0 0 auto",fontVariantNumeric:"tabular-nums"}},Ye(De.at)),s("span",{style:{flex:"0 0 auto"}},De.by===""?"人":De.by),s("span",{className:"ds-chip",style:{flex:"0 0 auto"}},ce[De.field]??De.field),s("span",{className:"ds-hint",style:{minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},`${De.from===""?"(空)":De.from} → ${De.to===""?"(空)":De.to}`))))))})(),(()=>{const j=B.pool??{reach:0,impact:0,confidence:0,effort:0,score:0},ce=async(xt,da,st)=>{const Ie=await p(da,{title:xt,defaultValue:String(st??0)});if(Ie===null)return null;const Ke=Number(String(Ie).trim());return Number.isFinite(Ke)&&Ke>=0?Ke:null},De=async()=>{const xt=await ce("影响面 Reach","影响多少用户或多少笔交易?估算依据写进正文",j.reach);if(xt===null)return;const da=await ce("影响度 Impact","3 显著 / 2 高 / 1 中 / 0.5 低",j.impact);if(da===null)return;const st=await ce("置信度 Confidence","100 有数据 / 80 有访谈 / 50 只有猜测(百分数)",j.confidence);if(st===null)return;const Ie=await ce("成本 Effort","人日(研发+测试+联调)",j.effort);if(Ie!==null)try{await i("/requirement/pool",{id:B.id,pool:{reach:xt,impact:da,confidence:st,effort:Ie}}),l("需求池评分已更新"),await a()}catch(Ke){l(Ke instanceof Error?Ke.message:String(Ke),"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"}},j.score>0?`得分 ${j.score}`:"还没打分:不打分就没法和别的需求比优先级"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void De()},j.score>0?"重新打分":"打分")),j.score>0?s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},s("span",{className:"ds-chip"},`影响面 ${j.reach}`),s("span",{className:"ds-chip"},`影响度 ${j.impact}`),s("span",{className:"ds-chip"},`置信度 ${j.confidence}%`),s("span",{className:"ds-chip"},`成本 ${j.effort} 人日`),s("span",{className:"ds-chip","data-on":"1"},`得分 ${j.score}`))):null)})(),(()=>{const j=B.questions??[],ce=j.filter(Ie=>Ie.status==="open").length,De=async(Ie,Ke)=>{try{await i("/requirement/questions",{id:B.id,...Ie}),Ke&&l(Ke),await a()}catch(sn){l(sn instanceof Error?sn.message:String(sn),"error")}},xt=async()=>{const Ie=await p("还没确认清楚的问题是什么?",{title:"加一个待确认问题",placeholder:"例如:退款失败时是否允许人工补发?"});Ie===null||Ie.trim()===""||await De({action:"add",text:Ie},"已加入待确认清单")},da=async Ie=>{const Ke=await p(Ie.text,{title:"回答这个问题",defaultValue:Ie.answer??"",placeholder:"写清结论与依据"});Ke===null||Ke.trim()===""||await De({action:"answer",questionId:Ie.id,answer:Ke},"已回答")},st=async Ie=>{await u("删掉这个问题?",{title:"删除待确认项",danger:!0,okText:"删除"})===!0&&await De({action:"delete",questionId:Ie.id},"已删除")};return s("div",{className:"ds-panel ds-question-panel","data-open":ce>0?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"待确认清单"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},ce>0?`${ce} 个没回答,回答完才能拆卡`:j.length===0?"澄清阶段把不确定的先记在这里":"全部已回答"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void xt()},"+ 加问题")),s("div",{className:"ds-panel-body"},j.length===0?s("div",{className:"ds-empty"},"还没有待确认的问题。产品澄清时把「需要业务拍板」的点写在这里,避免猜业务规则。"):s("div",{style:{display:"flex",flexDirection:"column",gap:8}},j.map(Ie=>s("div",{key:Ie.id,className:"ds-question-row","data-status":Ie.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":Ie.status==="answered"?"1":"0"},Ie.status==="answered"?"已回答":"待回答"),s("div",{style:{flex:"1 1 auto",minWidth:0}},s("div",{style:{fontSize:13}},Ie.text),Ie.answer===""||Ie.answer===void 0?null:s("div",{className:"ds-hint",style:{marginTop:4}},`答:${Ie.answer}`)),s("div",{style:{display:"flex",gap:6,flex:"0 0 auto"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void da(Ie)},Ie.status==="answered"?"改答案":"回答"),s("button",{className:"ds-btn ds-btn-ghost","data-danger":"1",onClick:()=>void st(Ie)},"删除")))))))})(),(()=>{const j=B.change??null;if(j===null)return null;const ce=j.affected??[],De=j.affectedCases??[],xt=new Map(Q.map(st=>[st.id,st])),da=st=>e.statuses.find(Ie=>Ie.id===st)?.label??st;return s("div",{className:"ds-panel ds-change-panel","data-invalidated":j.invalidated?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,j.invalidated?"变更影响(签字已作废)":"变更影响"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ye(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},function(){const st=(B.history??[]).slice().reverse();if(st.length===0)return null;const Ie={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}},`变更记录 · 最近 ${st.length} 条`),st.map((Ke,sn)=>s("div",{key:`${Ke.at}-${sn}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint"},Ye(Ke.at)),s("span",null,Ke.by===""?"人":Ke.by),s("span",{className:"ds-chip"},Ie[Ke.field]??Ke.field),s("span",{className:"ds-hint"},`${Ke.from===""?"(空)":Ke.from} → ${Ke.to===""?"(空)":Ke.to}`))))}(),s("div",{className:"ds-hint"},`改了:${(j.parts??[]).join("、")}。${ce.length===0?"当时没有已产出的卡受影响,不用返工。":`${ce.length} 张卡已经产出过东西${De.length===0?"":`、${De.length} 条用例要重跑`},要按新版复核:`}`),ce.length===0?null:s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8,marginTop:8}},ce.map(st=>s("span",{key:st.id,className:"ds-chip","data-pt":"open",style:{cursor:"pointer"},title:st.title,onClick:()=>{const Ie=xt.get(st.id);Ie!==void 0&&typeof t=="function"&&t(Ie)}},`${st.code||st.id} ${da(st.status)}`))),j.invalidated?s("div",{className:"ds-hint",style:{marginTop:8}},"验收签字已清空:这些卡是按旧版做的,复核完重新签一次。"):null))})(),(()=>{const j=B.deploys??[];return j.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}},j.map(ce=>s("span",{key:ce.id,className:"ds-chip","data-env":ce.kind,title:`${ce.releaseName??""} · ${Ye(ce.at)} · ${ce.by}`},`${ce.name} · ${Wa(ce.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:Ue,onChange:j=>qa(j.target.value)}),s("span",{className:"ds-hint"},"被依赖的需求没交付,这条需求的准出会被拦")),s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:10}},h.filter(j=>j.id!==B.id).map(j=>s("label",{key:j.id,className:"ds-check-row"},s(k,{checked:Tn.includes(j.id),onChange:ce=>V(De=>ce.target.checked?[...De,j.id]:De.filter(xt=>xt!==j.id))}),s("span",{className:"ds-check-text"},`${j.code||j.id} ${j.title}(${Tr[j.status]??j.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 At()},"保存排序与依赖"),me===null?null:s("span",{className:"ds-hint"},me)))),(()=>{const j=B.progress??null,ce=j?.gate??{ready:!1,blockers:["还没有加载出准出状态,稍后自动刷新"],warnings:[]},De=ce.stats??{},xt=B.signoff??null,da=j?.progress??(Q.length===0?0:Math.round(z/Q.length*100)),st=De.coveredPoints??0,Ie=De.points??(B.points??[]).length,Ke=De.pass??Do,sn=De.cases??Ge.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":ce.ready?"covered":"open",style:{marginLeft:"auto"}},ce.ready?"可以交付":`还差 ${ce.blockers.length} 项`)),s("div",{className:"ds-panel-body"},s("div",{className:"ds-check-progress"},s("div",{className:"ds-check-bar","data-full":da===100?"1":"0"},s("span",{style:{width:`${da}%`}})),s("div",{className:"ds-hint"},`${da}% · ${z}/${Q.length} 张卡完成 · 功能点覆盖 ${st}/${Ie} · 用例通过 ${Ke}/${sn}`)),(ce.blockers??[]).map(In=>s("div",{key:In,className:"ds-gate-item","data-kind":"block"},`⛔ ${In}`)),(ce.warnings??[]).map(In=>s("div",{key:In,className:"ds-gate-item","data-kind":"warn"},`⚠ ${In}`)),ce.ready?s("div",{className:"ds-hint"},"门禁已通过:签完字这条需求就算交付。"):null,xt===null?s("div",{className:"ds-gate-sign"},s(v,{className:"ds-input",style:{flex:1,minWidth:220},placeholder:"验收结论:交付了什么、遗留什么",value:H,onChange:In=>Ce(In.target.value)}),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void Vt("sign")},"需求验收签字")):s("div",{className:"ds-gate-sign"},s("div",{className:"ds-hint",style:{flex:1}},`✍ ${xt.by} 于 ${Ye(xt.at)} 签字:${xt.note}`),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Vt("clear")},"取消签字")),be===null?null:s("div",{className:"ds-hint"},be)))})(),s("div",{className:"ds-sec-head",style:{marginTop:16}},"交付物归档(飞书云盘)",s("div",{className:"ds-req-actions"},nn.url?s("a",{className:"ds-link",href:nn.url,target:"_blank",rel:"noreferrer"},"打开文件夹"):null,s("button",{className:"ds-btn ds-btn-ghost",disabled:He,onClick:()=>void pe()},nn.url?"校验文件夹":"创建文件夹"),s("button",{className:"ds-btn",disabled:He||We.length===0,onClick:()=>void Va()},He?"归档中…":`归档 ${We.length} 件交付物`))),s("div",{className:"ds-hint"},nn.url?`文件夹:${nn.name||nn.token} 已归档 ${We.filter(j=>j.doc?.url).length}/${We.length} 件交付物`:nn.error?`上次创建失败:${nn.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 yt(null)},"+ 添加文档")),B.baseline?s("div",{className:"ds-base-box"},s("div",{className:"ds-base-main"},s("b",null,`当前基线 ${B.baseline.version}`),s("span",null,`${B.baseline.title} · 生效于 ${Ye(B.baseline.at)} · ${B.baseline.by||"人"} · 正文 ${B.baseline.contentLength??0} 字`)),s("button",{className:"ds-btn ds-btn-ghost",title:"把当前文档和这份基线逐行比一遍(只读)",onClick:()=>void Qt()},$===null?"对比基线":"收起对比"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void la()},"取消基线")):s("div",{className:"ds-base-box","data-empty":"1"},s("div",{className:"ds-base-main"},s("b",null,"还没有基线"),s("span",null,"PRD 评审通过后点「设为基线」,之后建的卡都会记下用的是哪一版。"))),B.baseline!==null&&$!==null?s("div",{className:"ds-diff"},s("div",{className:"ds-diff-head"},s("b",null,`基线 ${$.version} → 当前:${$.docTitle}`),s("span",{className:"ds-hint"},$.summary.added+$.summary.removed===0?"正文没有变化":`+${$.summary.added} 行 / −${$.summary.removed} 行`)),s("div",{className:"ds-diff-body"},$.rows.slice(0,400).map((j,ce)=>s("div",{key:ce,className:"ds-diff-line","data-kind":j.kind},`${j.kind==="add"?"+":j.kind==="del"?"−":" "} ${j.text}`)),$.rows.length>400?s("div",{className:"ds-hint"},`(还有 ${$.rows.length-400} 行未显示)`):null)):null,(B.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 re()},I===null?"全部展开":"全部收起"),s("span",{className:"ds-hint"},`共 ${(B.docs??[]).length} 篇,展开后可直接读 Markdown 正文`)),s("div",{className:"ds-doc-list"},(B.docs??[]).map(j=>s("div",{key:j.id,className:"ds-doc-wrap"},s("div",{className:"ds-doc-row"},s(b,{className:"ds-chip","data-doc":j.kind},Qp[j.kind]??j.kind),s("div",{className:"ds-doc-main"},s("div",{className:"ds-doc-title"},j.url===""?j.title:s("a",{className:"ds-link",href:j.url,target:"_blank",rel:"noreferrer"},j.title),s("span",{className:"ds-chip-code"},j.version),s(b,{className:"ds-chip","data-docstatus":j.status},rx[j.status]??j.status)),s("div",{className:"ds-doc-meta"},`正文 ${j.contentLength??0} 字 · ${j.by||"—"} · ${j.at>0?Ye(j.at):"—"}`)),s("div",{className:"ds-listrow-actions"},j.status==="baseline"?null:s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void St(j.id)},"设为基线"),s("button",{className:"ds-btn ds-btn-ghost",title:"直接读正文(Markdown 渲染)",onClick:()=>void jt(j.id)},M!==null&&M.id===j.id?"收起":"阅读"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void yt(j.id)},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Pn(j.id),"data-danger":"1"},"删除"))),M!==null&&M.id===j.id?s("div",{className:"ds-md-preview ds-reader"},M.busy?s("span",{className:"ds-hint"},"读取中…"):M.content.trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(M.content)):I!==null&&I[j.id]!==void 0?s("div",{className:"ds-md-preview ds-reader"},I[j.id].trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(I[j.id])):null)))),q===null?null:s("div",{className:"ds-hint",style:{marginTop:6}},q),W===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},W.id===void 0?"添加文档":`编辑 ${W.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:W.kind,onChange:j=>J({...W,kind:j.target.value})},sx.map(j=>s("option",{key:j,value:j},Qp[j])))),s("div",{className:"ds-field",style:{flex:"1 1 220px"}},s("div",{className:"ds-label"},"标题"),s(v,{className:"ds-input",value:W.title,placeholder:"例如 会员积分抵扣 PRD",onChange:j=>J({...W,title:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 110px"}},s("div",{className:"ds-label"},"版本"),s(v,{className:"ds-input",value:W.version,placeholder:"v1.0",onChange:j=>J({...W,version:j.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:W.url,placeholder:"https://<租户>.feishu.cn/wiki/…",onChange:j=>J({...W,url:j.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:K,onClick:()=>void Ra()},K?"拉取中…":"从链接拉正文"))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"文档正文(拉不到就自己粘,研发和测试都读这一份)"),s(d,{className:"ds-textarea",rows:8,value:W.content,onChange:j=>J({...W,content:j.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:K,onClick:()=>void ke()},"保存文档")))),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"功能点与可追溯矩阵"),s("div",{className:"ds-pane-desc"},"功能点是需求的骨架:一条功能点对若干张卡、若干条用例。卡和用例上都挂 pointId,这里就能看出哪条没人做、哪条没被验证。")),U===null?s("div",{className:"ds-hint",style:{padding:"6px 0"}},"还没有功能点。点「+ 加功能点」把需求拆成可交付、可验证的条目。"):s("div",{className:"ds-matrix"},s("div",{className:"ds-matrix-sum"},`功能点 ${U.summary.points} · 已覆盖 ${U.summary.coveredPoints}${U.summary.pointsWithoutCases>0?`(${U.summary.pointsWithoutCases} 条还没有用例,覆盖率 ${U.summary.coverageRate}%)`:""} · 卡 ${U.summary.cards}(未归点 ${U.summary.looseCards})· 用例 ${U.summary.cases}(通过 ${U.summary.pass}${U.summary.fail>0?`,失败 ${U.summary.fail}`:""})· 通过率 ${U.summary.passRate}%`),(U.points??[]).map(j=>s("div",{key:j.id,className:"ds-pt-row","data-covered":j.covered?"1":"0"},s("div",{className:"ds-pt-main"},s("div",{className:"ds-pt-title"},s("span",{className:"ds-chip-code"},j.id),s("b",null,j.title),s(b,{className:"ds-chip","data-pt":j.covered?"covered":"open"},j.covered?"已覆盖":"未覆盖")),String(j.description??"")===""?null:s("div",{className:"ds-req-desc ds-md"},f(j.description)),s("div",{className:"ds-pt-meta"},`卡 ${j.cards.length}${j.cards.length===0?"(还没人做)":":"+j.cards.map(ce=>ce.code||ce.id).join("、")}`,` · 用例 ${j.cases.total}(通过 ${j.cases.pass}${j.cases.fail>0?`,失败 ${j.cases.fail}`:""}${j.cases.pending>0?`,没跑 ${j.cases.pending}`:""})`,j.acceptance.length===0?"":` · 验收条件 ${j.acceptance.filter(ce=>ce.covered).length}/${j.acceptance.length}`)),s("div",{className:"ds-listrow-actions"},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>he({...j,acceptanceText:(j.acceptance??[]).map(ce=>ce.text).join(`
45
45
  `)})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Is(j.id),"data-danger":"1"},"删除")))),(U.loose??[]).length===0?null:s("div",{className:"ds-pt-loose"},s("div",{className:"ds-hint",style:{marginBottom:6}},`还有 ${U.loose.length} 张卡没归到功能点,选一个挂上去,矩阵才算完整:`),U.loose.map(j=>s("div",{key:j.id,className:"ds-pt-loose-row"},s("span",{className:"ds-chip-code"},j.code||j.id),s("span",{className:"ds-pt-loose-title"},j.title),s(g,{className:"ds-select",style:{width:220},value:"",onChange:ce=>{ce.target.value!==""&&X(j.id,ce.target.value)}},s("option",{value:""},"挂到…"),(U.points??[]).map(ce=>s("option",{key:ce.id,value:ce.id},`${ce.id} ${ce.title}`))))))),s("div",{className:"ds-row",style:{gap:8,marginTop:10,alignItems:"center"}},s("button",{className:"ds-btn",onClick:()=>he(ls())},"+ 加功能点"),s("button",{className:"ds-btn ds-btn-primary",disabled:qe,onClick:()=>void Pt()},qe?"拆卡中…":"按未覆盖的功能点拆卡"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>{B!==null&&Je(B.id)}},"刷新矩阵"),et===null?null:s("span",{className:"ds-hint"},et),Te===null?null:s("span",{className:"ds-hint"},Te)),le===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},le.id===void 0?"新增功能点":`编辑功能点 ${le.id}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"功能点标题"),s(v,{className:"ds-input",value:le.title,placeholder:"例如 结算页展示可用积分",onChange:j=>he({...le,title:j.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"说明"),s(d,{className:"ds-textarea",rows:3,value:le.description??"",onChange:j=>he({...le,description:j.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"验收条件(一行一条,做成什么样算完成)"),s(x,{className:"ds-textarea",rows:4,value:le.acceptanceText??"",onChange:j=>he({...le,acceptanceText:j.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:le.priority,onChange:j=>he({...le,priority:j.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:le.status,onChange:j=>he({...le,status:j.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:()=>he(null)},"取消"),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void wa()},"保存功能点")))),s("div",{className:"ds-req-stats"},s("div",{className:"ds-stat-box"},s("b",null,`${z}/${Q.length}`),s("span",null,"卡已完成")),s("div",{className:"ds-stat-box","data-warn":ie>0?"1":"0"},s("b",null,String(ie)),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"},s("b",null,`${Ve.done}/${Ve.total}`),s("span",null,"验收清单")),s("div",{className:"ds-stat-box"},s("b",null,`${Ge.pass}/${Ge.total}`),s("span",null,"用例通过")),s("div",{className:"ds-stat-box"},s("b",null,String(We.length)),s("span",null,"交付物")),s("div",{className:"ds-stat-box"},s("b",null,String(_n.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(j=>s("div",{key:j.id,className:"ds-listrow ds-req-task",onClick:()=>t(j.id)},s("div",{className:"ds-listrow-main"},s("div",{className:"ds-req-task-title"},j.code?s(b,{className:"ds-chip ds-chip-code"},j.code):null,s("span",null,j.title)),s("div",{className:"ds-hint"},`${ve(j.status)}${(()=>{const ce=j.expert===""?"":(e.config.experts??[]).find(De=>De.id===j.expert)?.name??j.expert;return ce===""?"":` · ${ce}`})()}${j.dueAt>0?` · 截止 ${Wa(j.dueAt)}`:""}`)),s("div",{className:"ds-listrow-actions"},j.blocked!==void 0&&j.blocked!==null?s(b,{className:"ds-chip ds-chip-blocked"},"⛔"):null,j.type==="bug"?s(b,{className:"ds-chip ds-chip-bug","data-sev":j.severity??"p2"},"BUG"):null)))),_n.length>0?s("div",null,s("div",{className:"ds-sec-head",style:{marginTop:14}},"关联 MR"),_n.map(j=>s("div",{key:j.id,className:"ds-hint"},`${j.code||j.id} · !${j.gitlabMR.iid} ${j.gitlabMR.title??""}(${j.gitlabMR.state||"open"})`))):null)})();return s("div",{className:"ds-body"},A!==null?s("div",{className:"ds-err",style:{marginBottom:10}},A):null,Dt,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,z)=>(z.pool?.score??0)-(Q.pool?.score??0)):h).map(Q=>{const z=window.localStorage.getItem("dsh-studio:reqsrc")??"all";if(z!=="all"&&(Q.source==="business"?"business":"internal")!==z)return null;const ie=ee(Q.id),Le=ie.filter(We=>We.status==="done").length,Ve=Q.progress?.progress??(ie.length===0?0:Math.round(Le/ie.length*100)),Ge=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},Tr[Q.status]??Q.status),s("span",{className:"ds-hint"},`${Le}/${ie.length} 卡`),Ge===null?null:s("span",{className:"ds-hint",style:{color:Ge.ready?"#0f9d58":"#b26a00"}},Ge.ready?"准出通过":`还差 ${Ge.blockers.length} 项`)),s("div",{className:"ds-check-bar","data-full":Ve===100?"1":"0",style:{marginTop:2}},s("span",{style:{width:`${Ve}%`}})))})),s("div",{className:"ds-req-detail"},Ot)))}function aw({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}=ht(),[x,b]=n.useState([]),[k,g]=n.useState(null),[d,h]=n.useState(null),[y,S]=n.useState(null),[C,E]=n.useState(""),[_,R]=n.useState(null),[A,G]=n.useState(""),[L,P]=n.useState(!1),[W,J]=n.useState(""),[q,F]=n.useState(""),[M,w]=n.useState(""),[$,Y]=n.useState("10"),[I,O]=n.useState(""),[K,Ne]=n.useState(""),fe=n.useCallback(async H=>{const be=(await r("/releases")).releases??[];b(be);const _e=H??(be.some(qa=>qa.id===k)?k:be[0]?.id??null);if(g(_e),_e===null){h(null);return}const Ue=await r("/release/get",{id:_e});h(Ue),S({id:Ue.release.id,version:Ue.release.version,name:Ue.release.name,project:Ue.release.project,owner:Ue.release.owner,status:Ue.release.status,plannedAt:Ue.release.plannedAt,tagName:Ue.release.tagName??"",commitSha:Ue.release.commitSha??""}),E(Ue.release.notes??"")},[k]);n.useEffect(()=>{let H=!0;return fe().then(()=>{H&&R(null)}).catch(Ce=>{H&&R(Ce instanceof Error?Ce.message:String(Ce))}),()=>{H=!1}},[e.revision,fe]);async function xe(H){g(H),P(!0);try{const Ce=await r("/release/get",{id:H});h(Ce),S({id:Ce.release.id,version:Ce.release.version,name:Ce.release.name,project:Ce.release.project,owner:Ce.release.owner,status:Ce.release.status,plannedAt:Ce.release.plannedAt,tagName:Ce.release.tagName??"",commitSha:Ce.release.commitSha??""}),E(Ce.release.notes??""),G("")}catch(Ce){R(Ce instanceof Error?Ce.message:String(Ce))}finally{P(!1)}}function He(){g(null),h(null),S({id:"",version:"",name:"",project:"",owner:"",status:"draft",plannedAt:0,tagName:"",commitSha:""}),E(""),G("")}async function Se(H,Ce,be){P(!0),R(null);try{const _e=await r(H,Ce);G(be??"已保存"),s(be??"已保存"),s(be??"已保存"),await fe(_e.release?.id??k)}catch(_e){const Ue=_e instanceof Error?_e.message:String(_e);R(Ue),s(Ue,"error")}finally{P(!1)}}if(_!==null&&x.length===0)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},_));const U=d?.gate,ae=d?.release,Te=d?.tasks??[],Z=e.tasks.filter(H=>H.releaseId===""||H.releaseId===void 0),le=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(H=>a("div",{key:H.id,className:"ds-req-item","data-on":k===H.id?"1":"0",onClick:()=>void xe(H.id)},a("div",{className:"ds-req-item-title"},`${H.version||H.id} ${H.name}`.trim()),a("div",{className:"ds-req-item-meta"},a(u,{className:"ds-chip","data-kind":H.status},Xp[H.status]??H.status),a(u,{className:"ds-chip"},`${H.gate.stats.done}/${H.gate.stats.tasks} 卡`),H.gate.blockers.length===0?a(u,{className:"ds-chip",style:{color:"#0f9d58",borderColor:"#0f9d5855"}},"门禁通过"):a(u,{className:"ds-chip ds-chip-blocked"},`${H.gate.blockers.length} 项拦着`))))),he=y===null?a("div",{className:"ds-hint"},"左边选一个版本,或者新建一个。"):a("div",{className:"ds-req-detail"},(ae?.requirements??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginBottom:8}},a("span",{className:"ds-hint"},`这个版本涉及 ${ae.requirements.length} 条需求:`),ae.requirements.map(H=>a("span",{key:H.id,className:"ds-chip",title:H.title},`${H.code||H.id} ${H.title}(${Tr[H.derived]??H.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"},ae==null?"新版本:先填版本号与名字,保存后可以把卡挂进来":`创建于 ${Wa(ae.createdAt)}${ae.releasedAt>0?` · 发布于 ${Wa(ae.releasedAt)}`:""}${ae.approveBy!==""?` · 审批人 ${ae.approveBy}`:""}`)),a("div",{className:"ds-req-actions"},a(f,{className:"ds-select",style:{width:108},value:y.status,onChange:H=>S({...y,status:H.target.value})},ox.map(H=>a("option",{key:H,value:H},Xp[H]))))),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:ae?.status==="released",onChange:H=>S({...y,version:H.target.value})})),a("label",{className:"ds-field"},a("span",{},"版本名"),a(c,{className:"ds-input",value:y.name,placeholder:"积分抵扣首个版本",onChange:H=>S({...y,name:H.target.value})})),a("label",{className:"ds-field"},a("span",{},"业务项目"),a(f,{className:"ds-select",value:y.project,onChange:H=>S({...y,project:H.target.value})},a("option",{value:""},"不属于任何项目"),(e.config.projects??[]).map(H=>a("option",{key:H.id,value:H.id},H.name)))),a("label",{className:"ds-field"},a("span",{},"发布负责人"),a(c,{className:"ds-input",value:y.owner,onChange:H=>S({...y,owner:H.target.value})})),a("label",{className:"ds-field"},a("span",{},"计划发布时间"),a(c,{className:"ds-input",type:"date",value:Zp(y.plannedAt),onChange:H=>S({...y,plannedAt:Pr(H.target.value)})})),a("label",{className:"ds-field"},a("span",{},"发布标签(tag)"),a(c,{className:"ds-input",value:y.tagName??"",placeholder:"v1.0.0",onChange:H=>S({...y,tagName:H.target.value})})),a("label",{className:"ds-field"},a("span",{},"对应提交(sha,选填)"),a(c,{className:"ds-input",value:y.commitSha??"",placeholder:"a1b2c3d",onChange:H=>S({...y,commitSha:H.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:L,onClick:()=>void Se("/release/save",{release:y},"版本已保存")},"保存"),y.id===""?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:L,style:{color:"var(--destructive)"},onClick:async()=>{await o("删除这个版本?卡会保留,只是脱离版本。",{title:"删除版本",danger:!0,okText:"删除"})&&Se("/release/delete",{id:y.id},"版本已删除")}},"删除")))),ae==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(U?.stats.done??0)),a("span",{},"已完成卡")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(U?.stats.open??0)>0?"1":"0"},String(U?.stats.open??0)),a("span",{},"未完成")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(U?.stats.blocked??0)>0?"1":"0"},String(U?.stats.blocked??0)),a("span",{},"阻塞")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(U?.stats.bugs??0)>0?"1":"0"},String(U?.stats.bugs??0)),a("span",{},"未结缺陷")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(U?.stats.failing??0)>0?"1":"0"},String(U?.stats.failing??0)),a("span",{},"失败用例")),a("div",{className:"ds-stat-box"},a("b",{},String(U?.stats.artifacts??0)),a("span",{},"交付物"))),(U?.blockers??[]).map((H,Ce)=>a("div",{key:`b${Ce}`,className:"ds-error"},`⛔ ${H}`)),(U?.warnings??[]).map((H,Ce)=>a("div",{key:`w${Ce}`,className:"ds-hint"},`⚠ ${H}`)),a("div",{className:"ds-action-bar"},a("div",{className:"ds-action-bar-left"},a("button",{className:"ds-btn ds-btn-ghost",disabled:L,onClick:()=>void Se("/release/notes",{id:y.id},"发布说明已生成")},"生成发布说明"),a("button",{className:"ds-btn",disabled:L||!U?.ready,onClick:()=>void Se("/release/approve",{id:y.id,by:"人"},"已审批,可以发布了")},"审批通过")),a("div",{className:"ds-action-bar-right"},a("button",{className:"ds-btn ds-btn-primary",disabled:L||!U?.ready||ae.status==="released",onClick:()=>void Se("/release/publish",{id:y.id,by:"人"},"已发布")},ae.status==="released"?"已发布":"发布"),l("发布准入(预发部署 / 需求签字)没过时也要发:必须写原因,会记进部署日志",a("button",{className:"ds-btn ds-btn-ghost",disabled:L||!U?.ready||ae.status==="released","data-force":"1",onClick:async()=>{const H=await i("强制发布的原因(会记进部署日志,便于复盘):",{title:"强制发布",okText:"强制发布",placeholder:"为什么不等预发部署 / 需求签字?"});if(H!==null){if(String(H).trim()===""){G("强制发布必须写原因");return}Se("/release/publish",{id:y.id,by:"人",force:!0,forceNote:String(H).trim()},"已强制发布(原因已记入部署日志)")}}},"强制发布")),ae.status==="released"?a("button",{className:"ds-btn ds-btn-ghost",disabled:L,onClick:async()=>{const H=await i("回滚原因(要进复盘记录):",{title:"回滚这个版本",danger:!0,okText:"回滚"});H!==null&&H.trim()!==""&&Se("/release/rollback",{id:y.id,reason:H},"已回滚")}},"回滚"):null)),ae.rollbackReason===""?null:a("div",{className:"ds-error"},`回滚原因:${ae.rollbackReason}`))),ae==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布冒烟清单",a("span",{className:"ds-hint"},`${(ae.smoke??[]).filter(H=>H.done).length}/${(ae.smoke??[]).length} 项已过 · 发布前逐条点过,门禁才算过`)),a("div",{className:"ds-panel-body"},(ae.smoke??[]).length===0?a("div",{className:"ds-empty"},"还没有冒烟项:把发布前必验的几条写在这里,比如「能登录」「下单支付成功」「首页不报错」。"):null,(ae.smoke??[]).map(H=>a("div",{key:H.id,className:"ds-listrow"},a("label",{className:"ds-row",style:{gap:8,alignItems:"center",flex:1,cursor:"pointer"}},a(p,{checked:H.done===!0,onChange:()=>void Se("/release/smoke",{id:y.id,action:"toggle",itemId:H.id,done:H.done!==!0,by:"人"},H.done?"已取消":"冒烟项已过")}),a("span",{className:H.done?"ds-smoke-done":""},H.text)),H.done&&H.by!==""?a("span",{className:"ds-hint"},`${H.by} · ${Wa(H.at)}`):null,a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:L,onClick:()=>void Se("/release/smoke",{id:y.id,action:"remove",itemId:H.id},"已删除")},"×")))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:1},value:q,placeholder:"发布前必须验证什么?",onChange:H=>F(H.target.value)}),a("button",{className:"ds-btn",disabled:L||q.trim()==="",onClick:()=>{const H=q.trim();H!==""&&(F(""),Se("/release/smoke",{id:y.id,action:"add",text:H},"已加冒烟项"))}},"+ 加一条")))),ae==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"审批、灰度与部署记录",a("span",{className:"ds-hint"},`审批 ${(ae.approvals??[]).length} 条 · 灰度 ${(ae.grayBatches??[]).length} 批`)),a("div",{className:"ds-panel-body"},(ae.approvals??[]).length===0?a("div",{className:"ds-empty"},"还没有审批记录:点上面的「审批通过」,系统会把谁批的、什么结论记在这里。"):(ae.approvals??[]).map(H=>a("div",{key:H.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${H.by} 审批通过`),a("div",{className:"ds-hint"},H.note||"(没写结论)")),a("span",{className:"ds-hint"},Wa(H.at)))),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),(ae.grayBatches??[]).length===0?a("div",{className:"ds-hint"},"没有灰度批次:发布就是一次性全量。先加一批 10% 跑一轮再全量,出事好收。"):(ae.grayBatches??[]).map(H=>a("div",{key:H.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${H.name} · ${H.percent}%`),a("div",{className:"ds-hint"},`${{pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"}[H.status]??H.status}${H.note===""?"":` · ${H.note}`}`)),a("div",{className:"ds-listrow-actions"},H.status==="pending"?a("button",{className:"ds-btn ds-btn-ghost",disabled:L,onClick:()=>void Se("/release/gray",{id:y.id,action:"start",batchId:H.id,by:"人"},"已开始灰度")},"开始"):null,H.status==="running"?a("button",{className:"ds-btn ds-btn-ghost",disabled:L,onClick:()=>void Se("/release/gray",{id:y.id,action:"advance",batchId:H.id,by:"人"},"这一批已通过")},"通过"):null,H.status==="passed"?a(u,{className:"ds-chip","data-on":"1"},"已通过"):null,H.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:M,placeholder:"批次名,例如 内测用户",onChange:H=>w(H.target.value)}),a(c,{className:"ds-input",style:{flex:"0 0 88px"},type:"number",min:1,max:100,value:$,onChange:H=>Y(H.target.value)}),a("button",{className:"ds-btn",disabled:L||Number($)<=0,onClick:async()=>{await Se("/release/gray",{id:y.id,action:"add",name:M,percent:Number($),by:"人"},"已加一批灰度"),w(""),Y("10")}},"+ 加一批")),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),a("div",{className:"ds-hint"},"上过哪些环境(只登记,不触发部署)"),(ae.deploys??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginTop:6}},ae.deploys.map(H=>a("span",{key:H.id,className:"ds-chip","data-env":H.kind,"data-on":H.at===0?"0":"1",title:H.at===0?`还没登记部署到「${H.name}」`:`${H.name}:${Wa(H.at)} ${H.action} · ${H.by}`},H.at===0?`${H.name} —`:`${H.name} ✓`))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(f,{className:"ds-input",style:{flex:"0 0 148px"},value:I,onChange:H=>O(H.target.value)},a("option",{value:""},"选环境…"),(e.config?.environments??[]).map(H=>a("option",{key:H.id,value:H.id},H.name))),a(c,{className:"ds-input",style:{flex:1},value:K,placeholder:"备注:谁发的、发了什么(可留空)",onChange:H=>Ne(H.target.value)}),a("button",{className:"ds-btn",disabled:L||I==="",onClick:async()=>{await Se("/release/deploy",{id:y.id,env:I,note:K,by:"人"},"已登记部署(没有触发任何部署)"),Ne("")}},"登记部署")),a("div",{className:"ds-hint",style:{marginTop:10}},"部署与灰度记录(最近 8 条)"),(()=>{const H={pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"},Ce=[...(ae.deployLog??[]).map(be=>({at:be.at,kind:"deploy",by:be.by,text:`${be.action}${be.env===""?"":` · ${(e.config?.environments??[]).find(_e=>_e.id===be.env)?.name??be.env}`}`,note:be.note})),...(ae.grayBatches??[]).map(be=>({at:be.at??be.createdAt??0,kind:"gray",by:be.by??"人",text:`灰度 ${be.name} ${be.percent}%`,note:H[be.status]??be.status}))].filter(be=>be.at).sort((be,_e)=>be.at-_e.at).slice(-8).reverse();return Ce.length===0?a("div",{className:"ds-empty"},"还没有部署或灰度记录。"):Ce.map(be=>a("div",{key:`${be.at}-${be.kind}-${be.text}`,className:"ds-hint"},a("span",{className:"ds-chip",style:{marginRight:6}},be.kind==="gray"?"灰度":"部署"),`${Wa(be.at)} · ${be.text} · ${be.by}${be.note===""||be.note===void 0?"":`:${be.note}`}`))})())),ae==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"这个版本里的卡",a("span",{className:"ds-hint"},`${Te.length} 张`)),a("div",{className:"ds-panel-body"},Te.length===0?a("div",{className:"ds-empty"},"还没有卡。从下面挑,或者在卡片详情里选「所属版本」。"):Te.map(H=>a("div",{key:H.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${H.code?`${H.code} `:""}${H.title}`),a("div",{className:"ds-hint"},`${(e.statuses??[]).find(Ce=>Ce.id===H.status)?.label??H.status}${H.blocked!==null&&H.blocked!==void 0?" · 阻塞":""}${H.type==="bug"?" · 缺陷":""}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(H.id)},"打开"),a("button",{className:"ds-btn ds-btn-ghost",disabled:L,onClick:()=>void Se("/release/link",{id:y.id,taskId:H.id,action:"remove"},"已从版本移除")},"移除")))),a("div",{className:"ds-row",style:{marginTop:8}},a(f,{className:"ds-select",style:{flex:1},value:W,onChange:H=>J(H.target.value)},a("option",{value:""},`挑一张卡挂进来(${Z.length} 张可选)`),Z.map(H=>a("option",{key:H.id,value:H.id},`${H.code?`${H.code} `:""}${H.title}`))),a("button",{className:"ds-btn",disabled:L||W==="",onClick:async()=>{await Se("/release/link",{id:y.id,taskId:W,action:"add"},"已挂到这个版本"),J("")}},"挂进来")))),ae==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:L,onClick:()=>void navigator.clipboard?.writeText(C).then(()=>G("发布说明已复制"),()=>{})},"复制")),a("div",{className:"ds-panel-body"},a(v,{className:"ds-input",rows:12,value:C,placeholder:"点「生成发布说明」按类型自动汇总;之后可以在这里手动补充",onChange:H=>E(H.target.value),onBlur:()=>{C!==(ae.notes??"")&&Se("/release/save",{release:{...y,notes:C}},"发布说明已保存")}}))),_===null?null:a("div",{className:"ds-error",style:{marginTop:8}},_),A===""?null:a("div",{className:"ds-hint",style:{marginTop:8}},A));return a("div",{className:"ds-body"},a("div",{className:"ds-req-grid"},le,he))}function nw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,SelectField:s}=ht(),[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 $=`/analytics?days=${o}${l===""?"":`&project=${encodeURIComponent(l)}`}`;r($).then(I=>{w&&(p(I.analytics),v(null))}).catch(I=>{w&&v(I instanceof Error?I.message:String(I))});const Y=l===""?"":`&project=${encodeURIComponent(l)}`;return Promise.all([r("/workload"),r(`/burndown?days=14${Y}`),r("/costs"),r("/quality")]).then(([I,O,K,Ne])=>{w&&(b(I.workload??null),g(O.burndown??null),h({...K.costs??{},budget:K.budget??null}),S(Ne.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 $=Number(w)||0;return $<=0?"0":$*1440<60?`${Math.max(1,Math.round($*1440))} 分钟`:$<1?`${($*24).toFixed(1)} 小时`:`${$.toFixed(1)} 天`},_=(w,$,Y,I)=>a("div",{className:"ds-kpi","data-warn":I===!0?"1":"0"},a("div",{className:"ds-kpi-value"},$),a("div",{className:"ds-kpi-label"},w),a("div",{className:"ds-kpi-hint"},Y)),R=(u.cycle.buckets??[]).reduce((w,$)=>w+$.count,0),A=Math.max(1,...(u.throughput??[]).map(w=>w.count),...(u.cycle.buckets??[]).map(w=>w.count)),G=Math.max(.001,...(u.columns??[]).map(w=>w.avgDays)),L=(u.columns??[]).reduce((w,$)=>w===null||$.avgDays>w.avgDays?$:w,null),P=(u.throughput??[]).reduce((w,$)=>w+$.count,0),W=(x?.experts??[]).filter(w=>w.enabled),J=W.reduce((w,$)=>w+$.open,0),q=W.reduce((w,$)=>w+($.overdue??0),0),F=k?.series??[],M=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"}},`${R} 张已完成`)),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/A*100)}%`}})),a("span",{className:"ds-an-num"},`${w.count}`))),R===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,$)=>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(Y=>Y.count)))*100)}%`}}),a("span",{className:"ds-spark-day"},$%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/G*100)}%`}})),a("span",{className:"ds-an-num"},E(w.avgDays)))),a("div",{className:"ds-an-foot"},L===null?"还没有数据。":`最慢的是「${L.label}」:平均 ${E(L.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($=>$.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"},`${W.length} 位在岗,手上合计 ${J} 张${q>0?`,其中 ${q} 张逾期`:",没有逾期的"}`))),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(Zx,{series:k?.series??[]}),a("div",{className:"ds-an-foot"},M===null?"还没有数据。":`累计建 ${M.created} · 累计完成 ${M.done} · 现在还剩 ${M.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(Xx,{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($=>$.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 sw(e,t){const a="studio",n="/studio-api";let r=null;function s(X){r=X??null}function o(X){return r===null?null:r[X]??null}function i(X,B){if(B==null)return B;const ee=B.props??{},ve=ee.title!==void 0?B:t(B.type,{...ee,title:X}),te=o("Tip");return te===null?ve:t(te,{key:B.key,label:X},ve)}const{TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f}=ix({h:t,kit:o,React:e}),v=lx({h:t,kit:o}),x=Rx({h:t,React:e}),b=dx({h:t,ChipField:u}),k=cx({h:t,React:e,Card:b,TextField:l,SelectField:f,CheckField:p,toast:W});function g({rows:X=6,value:B,onChange:ee,placeholder:ve,className:te,...oe}){const[de,ge]=e.useState(!1),we=String(B??"");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:()=>ge(!de)},de?"回到编辑":"预览渲染")),de?t("div",{className:"ds-md-preview ds-req-desc ds-md"},we.trim()===""?t("span",{className:"ds-hint"},"还没写内容。"):Ce(we)):t(c,{className:te??"ds-textarea",rows:X,value:B,onChange:ee,placeholder:ve,...oe}))}function d(X,B){const ee=t("div",{className:"ds-label",style:{cursor:"help"}},X);return B==null||B===""?ee:i(B,ee)}let h=null,y=null;function S(X){h=X,typeof y=="function"&&y(X)}function C(X,B){const ee=B??{};return o("DialogBox")===null?Promise.resolve(window.confirm(String(X))):new Promise(te=>{S({kind:"confirm",title:ee.title??"确认操作",message:String(X??""),danger:ee.danger===!0,okText:ee.okText??"确定",cancelText:ee.cancelText??"取消",resolve:te})})}function E(X,B){const ee=B??{};return o("DialogBox")===null?Promise.resolve(window.prompt(String(X),ee.defaultValue??"")):new Promise(te=>{S({kind:"prompt",title:ee.title??"需要填写",message:String(X??""),value:String(ee.defaultValue??""),placeholder:ee.placeholder??"",okText:ee.okText??"确定",cancelText:"取消",resolve:te})})}function _(){const[X,B]=e.useState(null),[ee,ve]=e.useState("");e.useEffect(()=>{const de=ge=>{B(ge),ve(ge!==null&&ge.kind==="prompt"?ge.value:"")};return y=de,h!==null&&de(h),()=>{y===de&&(y=null)}},[]);const te=o("DialogBox");if(X===null||te===null)return null;const oe=de=>{const ge=X.resolve;B(null),h=null,ge(de)};return t(te,{open:!0,title:X.title,message:X.message,danger:X.danger===!0,okText:X.okText,cancelText:X.cancelText,input:X.kind==="prompt",value:ee,placeholder:X.placeholder,onChange:de=>ve(de),onOk:()=>oe(X.kind==="prompt"?ee.trim():!0),onCancel:()=>oe(X.kind==="prompt"?null:!1)})}let R=0,A=[],G=[],L={text:"",at:0};function P(){const X=A.slice();for(const B of G.slice())B(X)}function W(X,B="success",ee=3200){const ve=String(X??"").trim();if(ve==="")return null;const te=Date.now();if(L.text===ve&&te-L.at<600)return null;L={text:ve,at:te};const oe=++R;return A=A.concat([{id:oe,text:ve,tone:B}]),P(),window.setTimeout(()=>{J(oe)},ee),oe}function J(X){const B=A.filter(ee=>ee.id!==X);B.length!==A.length&&(A=B,P())}const q={success:"✓",error:"!",info:"i"};function F(){const[X,B]=e.useState(()=>A.slice());return e.useEffect(()=>(G.push(B),()=>{G=G.filter(ee=>ee!==B)}),[]),X.length===0?null:t("div",{className:"ds-toasts",role:"status","aria-live":"polite"},X.map(ee=>t("div",{key:ee.id,className:"ds-toast","data-tone":ee.tone},t("span",{className:"ds-toast-icon","aria-hidden":"true"},q[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 M="dsh-studio:user";function w(){try{const X=window.localStorage.getItem(M);if(typeof X=="string"&&X.trim()!=="")return X.trim().slice(0,40)}catch{}return"人"}function $(X){const B=String(X??"").trim().slice(0,40)||"人";try{window.localStorage.setItem(M,B)}catch{}return B}const Y="dsh-studio:theme",I=[["light","☀ 明亮"],["dark","☾ 黑暗"],["system","跟随系统"]];function O(){return typeof window.matchMedia=="function"?window.matchMedia("(prefers-color-scheme: dark)"):null}function K(){try{const X=window.getComputedStyle(document.body).backgroundColor,B=/rgba?\(([^)]+)\)/.exec(String(X??""));if(B===null)return null;const ee=B[1].split(",").map(ge=>Number(ge.trim())),[ve,te,oe,de]=ee;return![ve,te,oe].every(ge=>Number.isFinite(ge))||Number.isFinite(de)&&de<.2?null:.2126*ve+.7152*te+.0722*oe<128?"dark":"light"}catch{return null}}function Ne(){try{const X=window.localStorage.getItem(Y);if(X==="light"||X==="dark"||X==="system")return X}catch{}return"system"}function fe(X){const B=X==="light"||X==="dark"?X:"system";try{window.localStorage.setItem(Y,B)}catch{}return B}const xe="dsh-studio:session",He="dsh-studio:auth-user";function Se(){try{const X=window.localStorage.getItem(xe);if(typeof X=="string"&&X.trim()!=="")return X.trim()}catch{}return""}function U(X){try{X===""||X===null||X===void 0?window.localStorage.removeItem(xe):window.localStorage.setItem(xe,String(X))}catch{}}function ae(){try{const X=window.localStorage.getItem(He);if(typeof X=="string"&&X!==""){const B=JSON.parse(X);if(B!==null&&typeof B=="object")return B}}catch{}return null}function Te(X){try{X==null?window.localStorage.removeItem(He):window.localStorage.setItem(He,JSON.stringify(X))}catch{}return X??null}async function Z(X,B){const ee={"x-studio-user":encodeURIComponent(w())},ve=Se();ve!==""&&(ee["x-studio-session"]=ve),B!==void 0&&(ee["content-type"]="application/json");const te=B===void 0?{method:"GET",headers:ee}:{method:"POST",headers:ee,body:JSON.stringify(B)},oe=await fetch(n+X,te);let de={};try{de=await oe.json()}catch{}if(!oe.ok||de.ok===!1)throw new Error(de.error??`请求失败(HTTP ${oe.status})`);return de}const le=`
46
46
  html[data-dsh-theme="dark"] body { background: #0e1014 !important; color: #e6e8ee }
47
47
  html[data-dsh-theme="dark"] [class*="frame"],
@@ -823,7 +823,7 @@ html[data-dsh-theme="light"] [class*="rightbarCol"] { background: #fafafa !impor
823
823
  [data-dsh-studio] .ds-sec-head, [data-dsh-studio] .ds-an-sec { font-size:12px; font-weight:600; color:var(--muted-foreground); text-transform:none; letter-spacing:.01em; }
824
824
  [data-dsh-studio] .ds-panel-head .ds-hint, [data-dsh-studio] .ds-head .ds-sub { font-size:11.5px; }
825
825
  [data-dsh-studio] .ds-filters, [data-dsh-studio] .ds-check-bar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
826
- `;function H(){const[X,B]=e.useState(null),[ee,ve]=e.useState(null),te=e.useRef(""),oe=e.useCallback(async(de=!1)=>{if(!(typeof document<"u"&&document.hidden===!0))try{const ge=await Z("/state"),we={...ge.state??{},locked:ge.locked===!0},ye=[we.revision??"",(we.tasks??[]).length,(we.requirements??[]).length,(we.agents??[]).length,we.locked===!0?1:0].join("|");if(!de&&ye===te.current){ve(null);return}te.current=ye,B(we),ve(null)}catch(ge){ve(ge instanceof Error?ge.message:String(ge))}},[]);return e.useEffect(()=>{let de=!0;const ge=async()=>{de&&await oe()};ge();const we=setInterval(()=>{ge()},15e3),ye=()=>{document.hidden!==!0&&ge()};return document.addEventListener("visibilitychange",ye),()=>{de=!1,clearInterval(we),document.removeEventListener("visibilitychange",ye)}},[oe]),{state:X,error:ee,reload:oe}}const Ce=Qy(t),be=ux({h:t,TextField:l}),_e=mx({h:t,React:e,api:Z,kit:o,currentUser:w,renderMarkdown:Ce,tipOf:i,tipLabel:d,confirmDialog:C,promptDialog:E,TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f,MdField:g,MentionPicker:be}),Ue=_x({h:t,React:e});Ix({h:t,React:e,Editor:x,api:Z,kit:o,toast:W,sessionToken:Se,currentUser:w,confirmDialog:C,promptDialog:E,tipOf:i,tipLabel:d,renderMarkdown:Ce,SideDrawer:v,MentionPicker:be,Card:b,TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f,MdField:g});function qa({state:X,onChanged:B}){const[ee,ve]=e.useState(null),[te,oe]=e.useState(null),[de,ge]=e.useState(null),[we,ye]=e.useState(null),[Ee,tt]=e.useState({name:"",spec:"",description:""}),[at,ct]=e.useState(!1),[qe,nt]=e.useState(!1),et=e.useCallback(async()=>{try{ve(await Z("/market"))}catch(re){ge(re instanceof Error?re.message:String(re))}},[]);e.useEffect(()=>{et()},[et]);const $e=new Set(ee?.installed??[]),Pt=re=>re.replace(/^link:/,"").split("/").pop().replace(/^dsh-/,"dsh-"),Fe=async(re,ke)=>{oe(re.id),ge(null),ye(null);try{const pe=await Z(`/market/${ke}`,{id:re.id});if(pe.ok===!1)throw new Error((pe.result?.output??"").slice(-300)||"安装失败");ye(ke==="install"?"已安装,重启工作空间后生效":"已移除,重启工作空间后生效"),await et()}catch(pe){ge(pe instanceof Error?pe.message:String(pe))}finally{oe(null)}},yt=async()=>{if(!(Ee.name.trim()===""||Ee.spec.trim()==="")){nt(!0),ge(null);try{const re=[...X.config.market.items??[],{id:`m-${Math.random().toString(16).slice(2,10)}`,name:Ee.name.trim(),spec:Ee.spec.trim(),description:Ee.description.trim()}];await Z("/config",{market:{items:re}}),tt({name:"",spec:"",description:""}),ct(!1),await B(),await et()}catch(re){ge(re instanceof Error?re.message:String(re))}finally{nt(!1)}}},jt=async re=>{if(!await C(`从市场列表移除「${re.name}」?不会卸载已安装的插件。`,{title:"移除插件",danger:!0,okText:"移除"}))return;const ke=(X.config.market.items??[]).filter(pe=>pe.id!==re.id);await Z("/config",{market:{items:ke}}).catch(()=>{}),await B(),await et()},Qt=ee?.items??X.config.market.items??[];return t("div",null,t("div",{className:"ds-row",style:{marginBottom:12}},t("div",{style:{fontSize:13.5,fontWeight:650}},ee?.title??X.config.market.title),t("span",{className:"ds-stat"},`${Qt.length} 个条目 · profile ${X.profile??"—"}`),t("div",{className:"ds-grow"}),t("button",{className:"ds-btn",onClick:()=>ct(!at)},at?"收起":"+ 添加条目")),de!==null?t("div",{className:"ds-err",style:{padding:"0 0 8px"}},de):null,we!==null?t("div",{className:"ds-ok",style:{padding:"0 0 8px"}},we):null,Qt.length===0?t("div",{className:"ds-empty"},"还没有条目。把公司内部的插件包加进来,同事就能一键安装。"):t("div",{className:"ds-market-grid"},Qt.map(re=>{const ke=Pt(re.spec),pe=$e.has(ke)||$e.has(re.name);return t("div",{key:re.id,className:"ds-market-card"},t("div",{className:"ds-mc-head"},t("span",{className:"ds-mc-name ds-grow"},re.name),t(u,{className:"ds-chip","data-on":pe?"1":"0"},pe?"已安装":"未安装")),re.description!==""?t("div",{className:"ds-mc-desc"},re.description):null,t("code",null,re.spec),t("div",{className:"ds-row",style:{marginTop:"auto"}},t("button",{className:pe?"ds-btn":"ds-btn ds-btn-primary",disabled:te===re.id,onClick:async()=>{await C(pe?`从 profile 移除「${ke}」?`:`安装「${ke}」到工作空间?`,{title:pe?"移除插件":"安装插件",danger:pe,okText:pe?"移除":"安装"})&&Fe(re,pe?"uninstall":"install")}},te===re.id?"处理中…":pe?"移除":"安装"),t("button",{className:"ds-btn",style:{marginLeft:"auto",opacity:.7},onClick:()=>void jt(re)},"删条目")))})),at?t("div",{className:"ds-panel",style:{marginTop:14}},t("div",{className:"ds-panel-head"},"添加条目"),t("div",{className:"ds-panel-body"},t("div",{className:"ds-field"},t("div",{className:"ds-label"},"显示名"),t(l,{className:"ds-input",value:Ee.name,placeholder:"代码评审助手",onChange:re=>tt({...Ee,name:re.target.value})})),t("div",{className:"ds-field"},t("div",{className:"ds-label"},"包名或路径"),t(l,{className:"ds-input",value:Ee.spec,placeholder:"@company/dsh-reviewer 或 link:/home/lyf/pkgs/reviewer",onChange:re=>tt({...Ee,spec:re.target.value})})),t("div",{className:"ds-field"},t("div",{className:"ds-label"},"一句话说明"),t(l,{className:"ds-input",value:Ee.description,placeholder:"自动跑 GitLab MR 的评审清单",onChange:re=>tt({...Ee,description:re.target.value})}))),t("div",{style:{padding:"0 14px 14px"}},t("button",{className:"ds-btn ds-btn-primary",disabled:qe||Ee.name.trim()===""||Ee.spec.trim()==="",onClick:()=>void yt()},qe?"添加中…":"加入市场"))):null)}function Tn({state:X,onChanged:B,initialSection:ee,onSection:ve}){const te=X.config,[oe,de]=e.useState({boardTitle:te.board.title,gitlabUrl:te.gitlab.url,gitlabGroup:te.gitlab.group,gitlabProject:te.gitlab.project,gitlabToken:"",feishuAppId:te.feishu.appId,feishuAppSecret:"",feishuChatId:te.feishu.chatId,feishuBaseToken:te.feishu.baseToken,feishuTableId:te.feishu.tableId,feishuWebhook:"",feishuNotify:te.feishu.notifyOnChange!==!1,gitlabSyncEnabled:te.gitlab.syncEnabled===!0,gitlabSyncLabel:te.gitlab.syncLabel??"",roles:JSON.parse(JSON.stringify(te.roles)),agentsEnabled:te.agents?.enabled===!0,agentsMaxConcurrent:String(te.agents?.maxConcurrent??2),agentsPreset:te.agents?.agentPreset??"standard",agentsWorkspace:te.agents?.workspaceDir??"",agentsSandbox:te.agents?.sandbox??"workspace-write",agentsIsolation:te.agents?.isolation??"none",agentsWorktreeRoot:te.agents?.worktreeRoot??"",agentsCleanup:te.agents?.cleanupWorktree??"manual",agentsAuto:Array.isArray(te.agents?.autoStatuses)?te.agents.autoStatuses:["design","dev_ready","test"]}),[ge,we]=e.useState(null),[ye,Ee]=e.useState(null),[tt,at]=e.useState(!1),ct=async()=>{at(!0),Ee(null),we(null);try{await Z("/config",{board:{title:oe.boardTitle},roles:oe.roles,agents:{enabled:oe.agentsEnabled,maxConcurrent:Number(oe.agentsMaxConcurrent)||2,agentPreset:oe.agentsPreset,workspaceDir:oe.agentsWorkspace,sandbox:oe.agentsSandbox,isolation:oe.agentsIsolation,worktreeRoot:oe.agentsWorktreeRoot,cleanupWorktree:oe.agentsCleanup,autoStatuses:oe.agentsAuto}}),we("已保存"),await B()}catch(re){Ee(re instanceof Error?re.message:String(re))}finally{at(!1)}},qe=(re,ke,pe,Va)=>t("div",{className:"ds-field",key:re},t("div",{className:"ds-label"},ke),t(l,{className:"ds-input",type:"text",value:oe[re],placeholder:pe??"",onChange:St=>de({...oe,[re]:St.target.value})})),nt=Object.keys(oe.roles),[et,$e]=e.useState(ee??"experts");e.useEffect(()=>{typeof ee=="string"&&ee!==""&&$e(ee)},[ee]);const Pt=re=>{$e(re),ve?.(re)},Fe={projects:["项目与迭代","项目是看板的第一层分组:卡片挂项目、项目下面划迭代,之后按项目/迭代筛选和考核。"],experts:["专家","每个专家是一个能独立接活的工作单元。角色位决定它接哪一列,业务领域决定它懂哪块业务,角色提示词和技能决定它怎么做。"],teams:["专家团","团长负责拆活和汇总,成员各自干活;评论里 @团名 就交给团长。"],skills:["技能库","技能是一段可复用的做法。写进技能库后会落成 SKILL.md,专家在需要时自己加载,也可以自己新增。"],columns:["列的负责人","看板每一列默认派给谁。选「默认」用系统自带的那一位。"],gitlab:["代码仓库","一个仓库一条记录:地址、路径、令牌各自独立,同步时互不干扰。"],feishu:["飞书机器人","卡片状态变化时往群里发消息。可以按团队或项目建多个机器人。"],agents:["自动派单","卡片进入勾选的列时,自动把活派给该列的负责人;这里是全局开关与并发上限。"],models:["模型","每个角色位用哪个模型。单个专家还能在「专家」里单独覆盖。"],testing:["测试执行","接口用例跑在哪、能访问哪些域名、超时多少。执行器就跑在这台机器上(.218),不依赖外部 CI。"],costs:["成本与预算","一个月允许派多少次活、跑多少小时,超了在效能页亮黄亮红。"],environments:["部署环境","版本可以登记「上过哪个环境」,只留痕:平台不接 CD,不会触发任何部署。"],board:["看板外观","看板顶部显示的标题。"],access:["权限与审计","启用成员名单后,每个写操作都要认人:只读不能改,成员能推进卡,管理员才能改配置、删数据、发布版本。所有写操作都会进下面的审计日志。"],ldap:["统一登录(LDAP)","用公司账号登录平台:登录成功后服务端记住这个人,写操作和审计都挂在他的名字下。密码只存服务端密钥文件,不回显。"],storage:["数据与备份","台账存在哪、密钥存在哪,以及每次写操作留下的快照。改坏了可以回滚到上一版。"],health:["集成健康","飞书通知、GitLab、飞书云盘、代码隔离四条外部通道最近一次是成功还是失败。以前这些失败只写进日志,现在会在这里亮出来,坏了能看见、能补一次。"]},yt=t("div",{className:"ds-row",style:{gap:10,marginTop:2}},t("button",{className:"ds-btn ds-btn-primary",disabled:tt,onClick:()=>void ct()},tt?"保存中…":"保存"),ge!==null?t("span",{className:"ds-ok"},ge):null,ye!==null?t("span",{className:"ds-err"},ye):null),jt=[{group:"团队"},{id:"projects",label:"项目与迭代",count:(te.projects??[]).length},{id:"experts",label:"专家",count:(te.experts??[]).length},{id:"teams",label:"专家团",count:(te.teams??[]).length},{id:"skills",label:"技能库"},{id:"columns",label:"列的负责人"},{group:"接入"},{id:"gitlab",label:"代码仓库",count:(te.gitlab.projects??[]).length},{id:"feishu",label:"飞书机器人",count:(te.feishu.bots??[]).length},{id:"health",label:"集成健康",count:X.integrations?.broken??0},{group:"运行"},{id:"agents",label:"自动派单"},{id:"testing",label:"测试执行",count:(te.testing?.envs??[]).length},{id:"costs",label:"成本与预算"},{id:"environments",label:"部署环境",count:(te.environments??[]).length},{id:"board",label:"看板外观"},{group:"安全"},{id:"ldap",label:"统一登录(LDAP)",count:te.ldap?.enabled===!0?te.ldap?.requireLogin===!0?"必须":"开":0},{id:"access",label:"权限与审计",count:(te.access?.members??[]).length},{id:"storage",label:"数据与备份",count:X.storage?.snapshots??0}],Qt=re=>re==="projects"?t(Fx,{config:te,onChanged:B}):re==="experts"?t(Ax,{config:te,onChanged:B}):re==="teams"?t(jx,{config:te,onChanged:B}):re==="skills"?t(Kx,{config:te,onChanged:B}):re==="columns"?t(Ux,{state:X,onChanged:B}):re==="testing"?t(Dx,{config:te,onChanged:B}):re==="costs"?t(zx,{config:te,onChanged:B}):re==="environments"?t(Lx,{config:te,onChanged:B}):re==="access"?t(Bx,{state:X,config:te,onChanged:B}):re==="ldap"?t(Mx,{config:te,onChanged:B}):re==="storage"?t(qx,{state:X,onChanged:B}):re==="gitlab"?t(Wx,{config:te,onChanged:B}):re==="feishu"?t("div",null,t(Gx,{config:te,onChanged:B}),t(Ox,{config:te,onChanged:B})):re==="health"?t(Vx,{state:X,config:te,onChanged:B}):re==="board"?t("div",null,t("div",{className:"ds-panel ds-form-panel"},t("div",{className:"ds-panel-head"},"看板"),t("div",{className:"ds-panel-body"},qe("boardTitle","看板标题","需求&任务工作空间"))),yt):re==="models"?t("div",null,t("div",{className:"ds-panel ds-form-panel"},t("div",{className:"ds-panel-head"},"角色与模型"),t("div",{className:"ds-panel-body ds-grid2"},nt.map(ke=>t("div",{className:"ds-field",key:ke},t("div",{className:"ds-label"},`${Aa[ke]??ke}`),t(l,{className:"ds-input",value:oe.roles[ke].model,placeholder:"deepseek-v4.1-flash",onChange:pe=>de({...oe,roles:{...oe.roles,[ke]:{...oe.roles[ke],model:pe.target.value}}})}))))),t("div",{className:"ds-hint",style:{margin:"-6px 0 12px"}},"单个专家可以在「专家」里单独指定模型,优先级:专家模型 → 这里的角色模型 → 宿主默认。"),yt):t("div",null,t("div",{className:"ds-panel ds-form-panel"},t("div",{className:"ds-panel-head"},"自动派单(专家 agent)"),t("div",{className:"ds-panel-body"},t("div",{className:"ds-field",style:{gridColumn:"1 / -1"}},t("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},t(p,{checked:oe.agentsEnabled,onChange:ke=>de({...oe,agentsEnabled:ke.target.checked})}),t("span",{},"卡片进入勾选的列时,自动派给该列的负责人")),t("div",{className:"ds-hint"},"关闭后仍可在卡片抽屉里点「马上处理」手动派单;派单结果会写回评论并推进卡片。")),t("div",{className:"ds-field",style:{gridColumn:"1 / -1"}},t("div",{className:"ds-label"},"哪些列自动派单"),t("div",{className:"ds-row",style:{flexWrap:"wrap",gap:14}},[["design","待方案"],["dev_ready","待开发"],["dev","开发中"],["test","待测试"]].map(([ke,pe])=>t("label",{className:"ds-row",key:ke,style:{alignItems:"center",gap:6,cursor:"pointer"}},t(p,{checked:oe.agentsAuto.includes(ke),onChange:Va=>de({...oe,agentsAuto:Va.target.checked?[...oe.agentsAuto,ke]:oe.agentsAuto.filter(St=>St!==ke)})}),t("span",{},pe)))),t("div",{className:"ds-hint"},"勾选的列在卡片进入时自动派给该列对应的负责人;其余列只能手动「马上处理」。")),qe("agentsMaxConcurrent","同时最多几个 agent","2"),qe("agentsPreset","Agent 预设","standard"),qe("agentsWorkspace","工作区目录","/home/lyf/dsh-workspace"),t("div",{className:"ds-hint",style:{gridColumn:"1 / -1"}},"预设决定 agent 手里有哪些工具:standard 带 shell 与文件读写,minimal 只有基础工具。"),t("div",{className:"ds-field",key:"agentsSandbox"},t("div",{className:"ds-label"},"沙箱档位(这个会话能写哪、能跑什么)"),t(f,{className:"ds-input",value:oe.agentsSandbox,onChange:ke=>de({...oe,agentsSandbox:ke.target.value})},[["read-only","只读:能看不能改"],["workspace-write","只写工作区(推荐):能改自己的目录,改不到系统别处"],["danger-full-access","无沙箱:可以写任何地方(危险)"]].map(([ke,pe])=>t("option",{key:ke,value:ke},pe))),t("div",{className:"ds-hint"},"由 DSH 的 permission preset 执行:read-only / workspace-write 走 bwrap 或 Landlock 隔离。单个专家可以在「专家」里覆盖。")),t("div",{className:"ds-field",key:"agentsIsolation"},t("div",{className:"ds-label"},"代码隔离"),t(f,{className:"ds-input",value:oe.agentsIsolation,onChange:ke=>de({...oe,agentsIsolation:ke.target.value})},[["none","不隔离:所有卡共用一个工作区"],["worktree","一卡一个 git worktree(推荐给多卡并行)"]].map(([ke,pe])=>t("option",{key:ke,value:ke},pe))),t("div",{className:"ds-hint"},"开启后,卡要挂在配了「本机代码目录」的项目上,派单时会为它切一条 task/<编号> 分支。")),qe("agentsWorktreeRoot","worktree 根目录","留空 = 工作区同级的 dsh-worktrees"),t("div",{className:"ds-field",key:"agentsCleanup"},t("div",{className:"ds-label"},"卡完成时"),t(f,{className:"ds-input",value:oe.agentsCleanup,onChange:ke=>de({...oe,agentsCleanup:ke.target.value})},[["manual","保留 worktree 和分支,等人看 diff"],["on_done","自动删掉 worktree 和分支"]].map(([ke,pe])=>t("option",{key:ke,value:ke},pe)))))),yt);return t("div",{className:"ds-settings"},t("div",{className:"ds-settings-nav"},jt.map((re,ke)=>re.group!==void 0?t("div",{className:"ds-settings-nav-group",key:`group-${ke}`},re.group):i((Fe[re.id]??[re.label,""])[1]||re.label,t("button",{key:re.id,className:"ds-nav-item","data-active":et===re.id?"1":"0",onClick:()=>Pt(re.id)},t("span",{},re.label),re.count===void 0?null:t("span",{className:"ds-nav-count"},String(re.count)))))),t("div",{className:"ds-settings-pane"},t("div",{className:"ds-pane-head"},t("div",null,t("div",{className:"ds-pane-title"},(Fe[et]??["设置"])[0]),t("div",{className:"ds-pane-desc"},(Fe[et]??["",""])[1]))),Qt(et)))}function V({state:X,onClose:B,onCreated:ee}){const ve=X?.config?.projects??[],[te,oe]=e.useState(""),[de,ge]=e.useState(""),[we,ye]=e.useState("requirement"),[Ee,tt]=e.useState("normal"),[at,ct]=e.useState("p2"),[qe,nt]=e.useState(""),et=X?.requirements??[],[$e,Pt]=e.useState(""),[Fe,yt]=e.useState(ve.length===1?ve[0].id:""),[jt,Qt]=e.useState(""),[re,ke]=e.useState(""),[pe,Va]=e.useState(""),[St,la]=e.useState(null),[Pn,Ra]=e.useState(!1),Dt=we==="bug",Ot=ve.find(z=>z.id===Fe)?.iterations??[],Q=async()=>{Ra(!0),la(null);try{await Z("/task/create",{title:te,requirement:de,type:we,priority:Ee,domain:qe,severity:Dt?at:"",role:Dt?"dev":"product",status:Dt?"dev_ready":"clarify",project:Fe,iteration:Fe===""?"":jt,owner:re.trim(),dueAt:Pr(pe),requirementId:$e}),await ee(),B()}catch(z){la(z instanceof Error?z.message:String(z))}finally{Ra(!1)}};return t("div",{className:"ds-modal-wrap",onClick:z=>{z.target===z.currentTarget&&B()}},t("div",{className:"ds-modal"},t("h3",{},Dt?"登记缺陷":"新建任务"),t("div",{className:"ds-field"},t("div",{className:"ds-label"},"标题"),t(l,{className:"ds-input",autoFocus:!0,placeholder:Dt?"一句话说清什么坏了,例如「优惠券叠加后金额算错」":"一句话说清要做什么",value:te,onChange:z=>oe(z.target.value),onKeyDown:z=>{z.key==="Enter"&&!z.isComposing&&Q()}})),t("div",{className:"ds-field"},t("div",{className:"ds-label"},Dt?"复现步骤 / 现象":"需求 / 背景"),t(g,{className:"ds-textarea",rows:5,placeholder:Dt?`复现步骤:1… 2…
826
+ `;function H(){const[X,B]=e.useState(null),[ee,ve]=e.useState(null),te=e.useRef(""),oe=e.useCallback(async(de=!1)=>{if(!(typeof document<"u"&&document.hidden===!0))try{const ge=await Z("/state"),we={...ge.state??{},locked:ge.locked===!0},ye=[we.revision??"",(we.tasks??[]).length,(we.requirements??[]).length,(we.agents??[]).length,we.locked===!0?1:0].join("|");if(!de&&ye===te.current){ve(null);return}te.current=ye,B(we),ve(null)}catch(ge){ve(ge instanceof Error?ge.message:String(ge))}},[]);return e.useEffect(()=>{let de=!0;const ge=async()=>{de&&await oe()};ge();const we=setInterval(()=>{ge()},15e3),ye=()=>{document.hidden!==!0&&ge()};return document.addEventListener("visibilitychange",ye),()=>{de=!1,clearInterval(we),document.removeEventListener("visibilitychange",ye)}},[oe]),{state:X,error:ee,reload:oe}}const Ce=Qy(t),be=ux({h:t,TextField:l}),_e=mx({h:t,React:e,api:Z,kit:o,currentUser:w,renderMarkdown:Ce,tipOf:i,tipLabel:d,confirmDialog:C,promptDialog:E,TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f,MdField:g,MentionPicker:be}),Ue=_x({h:t,React:e});Ix({h:t,React:e,Editor:x,api:Z,kit:o,toast:W,sessionToken:Se,currentUser:w,confirmDialog:C,promptDialog:E,tipOf:i,tipLabel:d,renderMarkdown:Ce,SideDrawer:v,MentionPicker:be,Card:b,TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f,MdField:g});function qa({state:X,onChanged:B}){const[ee,ve]=e.useState(null),[te,oe]=e.useState(null),[de,ge]=e.useState(null),[we,ye]=e.useState(null),[Ee,tt]=e.useState({name:"",spec:"",description:""}),[at,ct]=e.useState(!1),[qe,nt]=e.useState(!1),et=e.useCallback(async()=>{try{ve(await Z("/market"))}catch(re){ge(re instanceof Error?re.message:String(re))}},[]);e.useEffect(()=>{et()},[et]);const $e=new Set(ee?.installed??[]),Pt=re=>re.replace(/^link:/,"").split("/").pop().replace(/^dsh-/,"dsh-"),Fe=async(re,ke)=>{oe(re.id),ge(null),ye(null);try{const pe=await Z(`/market/${ke}`,{id:re.id});if(pe.ok===!1)throw new Error((pe.result?.output??"").slice(-300)||"安装失败");ye(ke==="install"?"已安装,重启工作空间后生效":"已移除,重启工作空间后生效"),await et()}catch(pe){ge(pe instanceof Error?pe.message:String(pe))}finally{oe(null)}},yt=async()=>{if(!(Ee.name.trim()===""||Ee.spec.trim()==="")){nt(!0),ge(null);try{const re=[...X.config.market.items??[],{id:`m-${Math.random().toString(16).slice(2,10)}`,name:Ee.name.trim(),spec:Ee.spec.trim(),description:Ee.description.trim()}];await Z("/config",{market:{items:re}}),tt({name:"",spec:"",description:""}),ct(!1),await B(),await et()}catch(re){ge(re instanceof Error?re.message:String(re))}finally{nt(!1)}}},jt=async re=>{if(!await C(`从市场列表移除「${re.name}」?不会卸载已安装的插件。`,{title:"移除插件",danger:!0,okText:"移除"}))return;const ke=(X.config.market.items??[]).filter(pe=>pe.id!==re.id);await Z("/config",{market:{items:ke}}).catch(()=>{}),await B(),await et()},Qt=ee?.items??X.config.market.items??[];return t("div",null,t("div",{className:"ds-row",style:{marginBottom:12}},t("div",{style:{fontSize:13.5,fontWeight:650}},ee?.title??X.config.market.title),t("span",{className:"ds-stat"},`${Qt.length} 个条目 · profile ${X.profile??"—"}`),t("div",{className:"ds-grow"}),t("button",{className:"ds-btn",onClick:()=>ct(!at)},at?"收起":"+ 添加条目")),de!==null?t("div",{className:"ds-err",style:{padding:"0 0 8px"}},de):null,we!==null?t("div",{className:"ds-ok",style:{padding:"0 0 8px"}},we):null,Qt.length===0?t("div",{className:"ds-empty"},"还没有条目。把公司内部的插件包加进来,同事就能一键安装。"):t("div",{className:"ds-market-grid"},Qt.map(re=>{const ke=Pt(re.spec),pe=$e.has(ke)||$e.has(re.name);return t("div",{key:re.id,className:"ds-market-card"},t("div",{className:"ds-mc-head"},t("span",{className:"ds-mc-name ds-grow"},re.name),t(u,{className:"ds-chip","data-on":pe?"1":"0"},pe?"已安装":"未安装")),re.description!==""?t("div",{className:"ds-mc-desc"},re.description):null,t("code",null,re.spec),t("div",{className:"ds-row",style:{marginTop:"auto"}},t("button",{className:pe?"ds-btn":"ds-btn ds-btn-primary",disabled:te===re.id,onClick:async()=>{await C(pe?`从 profile 移除「${ke}」?`:`安装「${ke}」到工作空间?`,{title:pe?"移除插件":"安装插件",danger:pe,okText:pe?"移除":"安装"})&&Fe(re,pe?"uninstall":"install")}},te===re.id?"处理中…":pe?"移除":"安装"),t("button",{className:"ds-btn",style:{marginLeft:"auto",opacity:.7},onClick:()=>void jt(re)},"删条目")))})),at?t("div",{className:"ds-panel",style:{marginTop:14}},t("div",{className:"ds-panel-head"},"添加条目"),t("div",{className:"ds-panel-body"},t("div",{className:"ds-field"},t("div",{className:"ds-label"},"显示名"),t(l,{className:"ds-input",value:Ee.name,placeholder:"代码评审助手",onChange:re=>tt({...Ee,name:re.target.value})})),t("div",{className:"ds-field"},t("div",{className:"ds-label"},"包名或路径"),t(l,{className:"ds-input",value:Ee.spec,placeholder:"@company/dsh-reviewer 或 link:~/pkgs/reviewer",onChange:re=>tt({...Ee,spec:re.target.value})})),t("div",{className:"ds-field"},t("div",{className:"ds-label"},"一句话说明"),t(l,{className:"ds-input",value:Ee.description,placeholder:"自动跑 GitLab MR 的评审清单",onChange:re=>tt({...Ee,description:re.target.value})}))),t("div",{style:{padding:"0 14px 14px"}},t("button",{className:"ds-btn ds-btn-primary",disabled:qe||Ee.name.trim()===""||Ee.spec.trim()==="",onClick:()=>void yt()},qe?"添加中…":"加入市场"))):null)}function Tn({state:X,onChanged:B,initialSection:ee,onSection:ve}){const te=X.config,[oe,de]=e.useState({boardTitle:te.board.title,gitlabUrl:te.gitlab.url,gitlabGroup:te.gitlab.group,gitlabProject:te.gitlab.project,gitlabToken:"",feishuAppId:te.feishu.appId,feishuAppSecret:"",feishuChatId:te.feishu.chatId,feishuBaseToken:te.feishu.baseToken,feishuTableId:te.feishu.tableId,feishuWebhook:"",feishuNotify:te.feishu.notifyOnChange!==!1,gitlabSyncEnabled:te.gitlab.syncEnabled===!0,gitlabSyncLabel:te.gitlab.syncLabel??"",roles:JSON.parse(JSON.stringify(te.roles)),agentsEnabled:te.agents?.enabled===!0,agentsMaxConcurrent:String(te.agents?.maxConcurrent??2),agentsPreset:te.agents?.agentPreset??"standard",agentsWorkspace:te.agents?.workspaceDir??"",agentsSandbox:te.agents?.sandbox??"workspace-write",agentsIsolation:te.agents?.isolation??"none",agentsWorktreeRoot:te.agents?.worktreeRoot??"",agentsCleanup:te.agents?.cleanupWorktree??"manual",agentsAuto:Array.isArray(te.agents?.autoStatuses)?te.agents.autoStatuses:["design","dev_ready","test"]}),[ge,we]=e.useState(null),[ye,Ee]=e.useState(null),[tt,at]=e.useState(!1),ct=async()=>{at(!0),Ee(null),we(null);try{await Z("/config",{board:{title:oe.boardTitle},roles:oe.roles,agents:{enabled:oe.agentsEnabled,maxConcurrent:Number(oe.agentsMaxConcurrent)||2,agentPreset:oe.agentsPreset,workspaceDir:oe.agentsWorkspace,sandbox:oe.agentsSandbox,isolation:oe.agentsIsolation,worktreeRoot:oe.agentsWorktreeRoot,cleanupWorktree:oe.agentsCleanup,autoStatuses:oe.agentsAuto}}),we("已保存"),await B()}catch(re){Ee(re instanceof Error?re.message:String(re))}finally{at(!1)}},qe=(re,ke,pe,Va)=>t("div",{className:"ds-field",key:re},t("div",{className:"ds-label"},ke),t(l,{className:"ds-input",type:"text",value:oe[re],placeholder:pe??"",onChange:St=>de({...oe,[re]:St.target.value})})),nt=Object.keys(oe.roles),[et,$e]=e.useState(ee??"experts");e.useEffect(()=>{typeof ee=="string"&&ee!==""&&$e(ee)},[ee]);const Pt=re=>{$e(re),ve?.(re)},Fe={projects:["项目与迭代","项目是看板的第一层分组:卡片挂项目、项目下面划迭代,之后按项目/迭代筛选和考核。"],experts:["专家","每个专家是一个能独立接活的工作单元。角色位决定它接哪一列,业务领域决定它懂哪块业务,角色提示词和技能决定它怎么做。"],teams:["专家团","团长负责拆活和汇总,成员各自干活;评论里 @团名 就交给团长。"],skills:["技能库","技能是一段可复用的做法。写进技能库后会落成 SKILL.md,专家在需要时自己加载,也可以自己新增。"],columns:["列的负责人","看板每一列默认派给谁。选「默认」用系统自带的那一位。"],gitlab:["代码仓库","一个仓库一条记录:地址、路径、令牌各自独立,同步时互不干扰。"],feishu:["飞书机器人","卡片状态变化时往群里发消息。可以按团队或项目建多个机器人。"],agents:["自动派单","卡片进入勾选的列时,自动把活派给该列的负责人;这里是全局开关与并发上限。"],models:["模型","每个角色位用哪个模型。单个专家还能在「专家」里单独覆盖。"],testing:["测试执行","接口用例跑在哪、能访问哪些域名、超时多少。执行器就跑在这台机器上(.218),不依赖外部 CI。"],costs:["成本与预算","一个月允许派多少次活、跑多少小时,超了在效能页亮黄亮红。"],environments:["部署环境","版本可以登记「上过哪个环境」,只留痕:平台不接 CD,不会触发任何部署。"],board:["看板外观","看板顶部显示的标题。"],access:["权限与审计","启用成员名单后,每个写操作都要认人:只读不能改,成员能推进卡,管理员才能改配置、删数据、发布版本。所有写操作都会进下面的审计日志。"],ldap:["统一登录(LDAP)","用公司账号登录平台:登录成功后服务端记住这个人,写操作和审计都挂在他的名字下。密码只存服务端密钥文件,不回显。"],storage:["数据与备份","台账存在哪、密钥存在哪,以及每次写操作留下的快照。改坏了可以回滚到上一版。"],health:["集成健康","飞书通知、GitLab、飞书云盘、代码隔离四条外部通道最近一次是成功还是失败。以前这些失败只写进日志,现在会在这里亮出来,坏了能看见、能补一次。"]},yt=t("div",{className:"ds-row",style:{gap:10,marginTop:2}},t("button",{className:"ds-btn ds-btn-primary",disabled:tt,onClick:()=>void ct()},tt?"保存中…":"保存"),ge!==null?t("span",{className:"ds-ok"},ge):null,ye!==null?t("span",{className:"ds-err"},ye):null),jt=[{group:"团队"},{id:"projects",label:"项目与迭代",count:(te.projects??[]).length},{id:"experts",label:"专家",count:(te.experts??[]).length},{id:"teams",label:"专家团",count:(te.teams??[]).length},{id:"skills",label:"技能库"},{id:"columns",label:"列的负责人"},{group:"接入"},{id:"gitlab",label:"代码仓库",count:(te.gitlab.projects??[]).length},{id:"feishu",label:"飞书机器人",count:(te.feishu.bots??[]).length},{id:"health",label:"集成健康",count:X.integrations?.broken??0},{group:"运行"},{id:"agents",label:"自动派单"},{id:"testing",label:"测试执行",count:(te.testing?.envs??[]).length},{id:"costs",label:"成本与预算"},{id:"environments",label:"部署环境",count:(te.environments??[]).length},{id:"board",label:"看板外观"},{group:"安全"},{id:"ldap",label:"统一登录(LDAP)",count:te.ldap?.enabled===!0?te.ldap?.requireLogin===!0?"必须":"开":0},{id:"access",label:"权限与审计",count:(te.access?.members??[]).length},{id:"storage",label:"数据与备份",count:X.storage?.snapshots??0}],Qt=re=>re==="projects"?t(Fx,{config:te,onChanged:B}):re==="experts"?t(Ax,{config:te,onChanged:B}):re==="teams"?t(jx,{config:te,onChanged:B}):re==="skills"?t(Kx,{config:te,onChanged:B}):re==="columns"?t(Ux,{state:X,onChanged:B}):re==="testing"?t(Dx,{config:te,onChanged:B}):re==="costs"?t(zx,{config:te,onChanged:B}):re==="environments"?t(Lx,{config:te,onChanged:B}):re==="access"?t(Bx,{state:X,config:te,onChanged:B}):re==="ldap"?t(Mx,{config:te,onChanged:B}):re==="storage"?t(qx,{state:X,onChanged:B}):re==="gitlab"?t(Wx,{config:te,onChanged:B}):re==="feishu"?t("div",null,t(Gx,{config:te,onChanged:B}),t(Ox,{config:te,onChanged:B})):re==="health"?t(Vx,{state:X,config:te,onChanged:B}):re==="board"?t("div",null,t("div",{className:"ds-panel ds-form-panel"},t("div",{className:"ds-panel-head"},"看板"),t("div",{className:"ds-panel-body"},qe("boardTitle","看板标题","需求&任务工作空间"))),yt):re==="models"?t("div",null,t("div",{className:"ds-panel ds-form-panel"},t("div",{className:"ds-panel-head"},"角色与模型"),t("div",{className:"ds-panel-body ds-grid2"},nt.map(ke=>t("div",{className:"ds-field",key:ke},t("div",{className:"ds-label"},`${Aa[ke]??ke}`),t(l,{className:"ds-input",value:oe.roles[ke].model,placeholder:"deepseek-v4.1-flash",onChange:pe=>de({...oe,roles:{...oe.roles,[ke]:{...oe.roles[ke],model:pe.target.value}}})}))))),t("div",{className:"ds-hint",style:{margin:"-6px 0 12px"}},"单个专家可以在「专家」里单独指定模型,优先级:专家模型 → 这里的角色模型 → 宿主默认。"),yt):t("div",null,t("div",{className:"ds-panel ds-form-panel"},t("div",{className:"ds-panel-head"},"自动派单(专家 agent)"),t("div",{className:"ds-panel-body"},t("div",{className:"ds-field",style:{gridColumn:"1 / -1"}},t("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},t(p,{checked:oe.agentsEnabled,onChange:ke=>de({...oe,agentsEnabled:ke.target.checked})}),t("span",{},"卡片进入勾选的列时,自动派给该列的负责人")),t("div",{className:"ds-hint"},"关闭后仍可在卡片抽屉里点「马上处理」手动派单;派单结果会写回评论并推进卡片。")),t("div",{className:"ds-field",style:{gridColumn:"1 / -1"}},t("div",{className:"ds-label"},"哪些列自动派单"),t("div",{className:"ds-row",style:{flexWrap:"wrap",gap:14}},[["design","待方案"],["dev_ready","待开发"],["dev","开发中"],["test","待测试"]].map(([ke,pe])=>t("label",{className:"ds-row",key:ke,style:{alignItems:"center",gap:6,cursor:"pointer"}},t(p,{checked:oe.agentsAuto.includes(ke),onChange:Va=>de({...oe,agentsAuto:Va.target.checked?[...oe.agentsAuto,ke]:oe.agentsAuto.filter(St=>St!==ke)})}),t("span",{},pe)))),t("div",{className:"ds-hint"},"勾选的列在卡片进入时自动派给该列对应的负责人;其余列只能手动「马上处理」。")),qe("agentsMaxConcurrent","同时最多几个 agent","2"),qe("agentsPreset","Agent 预设","standard"),qe("agentsWorkspace","工作区目录","~/dsh-workspace"),t("div",{className:"ds-hint",style:{gridColumn:"1 / -1"}},"预设决定 agent 手里有哪些工具:standard 带 shell 与文件读写,minimal 只有基础工具。"),t("div",{className:"ds-field",key:"agentsSandbox"},t("div",{className:"ds-label"},"沙箱档位(这个会话能写哪、能跑什么)"),t(f,{className:"ds-input",value:oe.agentsSandbox,onChange:ke=>de({...oe,agentsSandbox:ke.target.value})},[["read-only","只读:能看不能改"],["workspace-write","只写工作区(推荐):能改自己的目录,改不到系统别处"],["danger-full-access","无沙箱:可以写任何地方(危险)"]].map(([ke,pe])=>t("option",{key:ke,value:ke},pe))),t("div",{className:"ds-hint"},"由 DSH 的 permission preset 执行:read-only / workspace-write 走 bwrap 或 Landlock 隔离。单个专家可以在「专家」里覆盖。")),t("div",{className:"ds-field",key:"agentsIsolation"},t("div",{className:"ds-label"},"代码隔离"),t(f,{className:"ds-input",value:oe.agentsIsolation,onChange:ke=>de({...oe,agentsIsolation:ke.target.value})},[["none","不隔离:所有卡共用一个工作区"],["worktree","一卡一个 git worktree(推荐给多卡并行)"]].map(([ke,pe])=>t("option",{key:ke,value:ke},pe))),t("div",{className:"ds-hint"},"开启后,卡要挂在配了「本机代码目录」的项目上,派单时会为它切一条 task/<编号> 分支。")),qe("agentsWorktreeRoot","worktree 根目录","留空 = 工作区同级的 dsh-worktrees"),t("div",{className:"ds-field",key:"agentsCleanup"},t("div",{className:"ds-label"},"卡完成时"),t(f,{className:"ds-input",value:oe.agentsCleanup,onChange:ke=>de({...oe,agentsCleanup:ke.target.value})},[["manual","保留 worktree 和分支,等人看 diff"],["on_done","自动删掉 worktree 和分支"]].map(([ke,pe])=>t("option",{key:ke,value:ke},pe)))))),yt);return t("div",{className:"ds-settings"},t("div",{className:"ds-settings-nav"},jt.map((re,ke)=>re.group!==void 0?t("div",{className:"ds-settings-nav-group",key:`group-${ke}`},re.group):i((Fe[re.id]??[re.label,""])[1]||re.label,t("button",{key:re.id,className:"ds-nav-item","data-active":et===re.id?"1":"0",onClick:()=>Pt(re.id)},t("span",{},re.label),re.count===void 0?null:t("span",{className:"ds-nav-count"},String(re.count)))))),t("div",{className:"ds-settings-pane"},t("div",{className:"ds-pane-head"},t("div",null,t("div",{className:"ds-pane-title"},(Fe[et]??["设置"])[0]),t("div",{className:"ds-pane-desc"},(Fe[et]??["",""])[1]))),Qt(et)))}function V({state:X,onClose:B,onCreated:ee}){const ve=X?.config?.projects??[],[te,oe]=e.useState(""),[de,ge]=e.useState(""),[we,ye]=e.useState("requirement"),[Ee,tt]=e.useState("normal"),[at,ct]=e.useState("p2"),[qe,nt]=e.useState(""),et=X?.requirements??[],[$e,Pt]=e.useState(""),[Fe,yt]=e.useState(ve.length===1?ve[0].id:""),[jt,Qt]=e.useState(""),[re,ke]=e.useState(""),[pe,Va]=e.useState(""),[St,la]=e.useState(null),[Pn,Ra]=e.useState(!1),Dt=we==="bug",Ot=ve.find(z=>z.id===Fe)?.iterations??[],Q=async()=>{Ra(!0),la(null);try{await Z("/task/create",{title:te,requirement:de,type:we,priority:Ee,domain:qe,severity:Dt?at:"",role:Dt?"dev":"product",status:Dt?"dev_ready":"clarify",project:Fe,iteration:Fe===""?"":jt,owner:re.trim(),dueAt:Pr(pe),requirementId:$e}),await ee(),B()}catch(z){la(z instanceof Error?z.message:String(z))}finally{Ra(!1)}};return t("div",{className:"ds-modal-wrap",onClick:z=>{z.target===z.currentTarget&&B()}},t("div",{className:"ds-modal"},t("h3",{},Dt?"登记缺陷":"新建任务"),t("div",{className:"ds-field"},t("div",{className:"ds-label"},"标题"),t(l,{className:"ds-input",autoFocus:!0,placeholder:Dt?"一句话说清什么坏了,例如「优惠券叠加后金额算错」":"一句话说清要做什么",value:te,onChange:z=>oe(z.target.value),onKeyDown:z=>{z.key==="Enter"&&!z.isComposing&&Q()}})),t("div",{className:"ds-field"},t("div",{className:"ds-label"},Dt?"复现步骤 / 现象":"需求 / 背景"),t(g,{className:"ds-textarea",rows:5,placeholder:Dt?`复现步骤:1… 2…
827
827
  实际结果:
828
828
  期望结果:
829
829
  影响范围 / 环境(版本、账号、数据条件):`:"背景、期望结果、验收口径。专家会照这段去开工。",value:de,onChange:z=>ge(z.target.value)})),t("div",{className:"ds-modal-grid"},t("div",{className:"ds-field"},t("div",{className:"ds-label"},"类型"),t(f,{className:"ds-select",value:we,onChange:z=>ye(z.target.value)},Object.keys(zo).map(z=>t("option",{key:z,value:z},zo[z])))),Dt?t("div",{className:"ds-field"},t("div",{className:"ds-label"},"严重级别"),t(f,{className:"ds-select",value:at,onChange:z=>ct(z.target.value)},Object.keys(ql).map(z=>t("option",{key:z,value:z},ql[z])))):t("div",{className:"ds-field"},t("div",{className:"ds-label"},"优先级"),t(f,{className:"ds-select",value:Ee,onChange:z=>tt(z.target.value)},Object.keys(As).map(z=>t("option",{key:z,value:z},As[z])))),Dt?t("div",{className:"ds-field"},t("div",{className:"ds-label"},"优先级"),t(f,{className:"ds-select",value:Ee,onChange:z=>tt(z.target.value)},Object.keys(As).map(z=>t("option",{key:z,value:z},As[z])))):null,t("div",{className:"ds-field",style:{gridColumn:"1 / -1"}},t("div",{className:"ds-label"},"挂在哪个需求下"),et.length===0?t("div",{className:"ds-hint",style:{fontSize:11.5}},"还没有需求。到「需求」页先建一条,这张卡就能拿到 DEV-001 这样的编号。"):t(f,{className:"ds-select",value:$e,onChange:z=>Pt(z.target.value)},t("option",{value:""},"暂不挂需求(编号留空)"),et.map(z=>t("option",{key:z.id,value:z.id},`${z.code||z.id} · ${z.title}`)))),t("div",{className:"ds-field",style:{gridColumn:"1 / -1"}},t("div",{className:"ds-label"},"业务领域(选填)"),t(l,{className:"ds-input",placeholder:"例如 支付 / 会员 / 履约,填了会自动派给同领域的专家",value:qe,onChange:z=>nt(z.target.value)})),ve.length===0?null:t("div",{className:"ds-field"},t("div",{className:"ds-label"},"业务项目"),t(f,{className:"ds-select",value:Fe,onChange:z=>{yt(z.target.value),Qt("")}},t("option",{value:""},"不属于任何项目"),ve.map(z=>t("option",{key:z.id,value:z.id},`${z.key?`${z.key} · `:""}${z.name}`)))),ve.length===0||Ot.length===0?null:t("div",{className:"ds-field"},t("div",{className:"ds-label"},"迭代"),t(f,{className:"ds-select",value:jt,onChange:z=>Qt(z.target.value)},t("option",{value:""},"不指定迭代"),Ot.map(z=>t("option",{key:z.id,value:z.id},z.name)))),t("div",{className:"ds-field"},t("div",{className:"ds-label"},"业务负责人(选填)"),t(l,{className:"ds-input",placeholder:"提需求的人,例如 张伟",value:re,onChange:z=>ke(z.target.value)})),t("div",{className:"ds-field"},t("div",{className:"ds-label"},"期望完成(选填)"),t(l,{className:"ds-input",type:"date",value:pe,onChange:z=>Va(z.target.value)}))),Dt?t("div",{className:"ds-hint",style:{fontSize:11.5}},"缺陷卡会直接落到「待开发」,由研发先复现、定位根因再修,修完自动交给测试验证。"):null,t("div",{className:"ds-modal-foot"},St!==null?t("span",{className:"ds-err"},St):null,t("div",{className:"ds-grow"}),t("button",{className:"ds-btn",onClick:B},"取消"),t("button",{className:"ds-btn ds-btn-primary",disabled:Pn||te.trim()==="",onClick:()=>void Q()},Pn?"创建中…":Dt?"登记缺陷":"创建"))))}function me({state:X,onOpenBoard:B}){const[ee,ve]=e.useState(null),[te,oe]=e.useState(null);if(e.useEffect(()=>{let ge=!0;return Z("/metrics").then(we=>{ge&&ve(we.metrics)}).catch(we=>{ge&&oe(we instanceof Error?we.message:String(we))}),()=>{ge=!1}},[X.revision]),te!==null)return t("div",{className:"ds-body"},t("div",{className:"ds-err"},te));if(ee===null)return t("div",{className:"ds-loading"},"正在统计…");const de=(ge,we,ye,Ee)=>t("div",{className:"ds-proj-card",key:Ee},t("div",{className:"ds-proj-head"},t("div",null,t("div",{className:"ds-proj-name"},ge),t("div",{className:"ds-hint"},we)),t("button",{className:"ds-btn",onClick:()=>B(Ee)},"看这些卡")),t("div",{className:"ds-proj-stats"},t("div",null,t("b",{},ye.inProgress),t("span",{},"进行中")),t("div",null,t("b",{"data-warn":ye.blocked>0?"1":"0"},ye.blocked),t("span",{},"阻塞")),t("div",null,t("b",{"data-warn":ye.overdue>0?"1":"0"},ye.overdue),t("span",{},"逾期")),t("div",null,t("b",{},ye.done),t("span",{},"已完成")),t("div",null,t("b",{},`${ye.checklistRate}%`),t("span",{},"清单完成")),t("div",null,t("b",{},ye.artifacts),t("span",{},"交付物"))),t("div",{className:"ds-proj-bar"},t("div",{className:"ds-check-bar","data-full":ye.checklistRate===100?"1":"0"},t("span",{style:{width:`${ye.checklistRate}%`}}))),ye.cycleDays>0?t("div",{className:"ds-hint"},`平均周期 ${ye.cycleDays} 天 · 近 7 天完成 ${ye.throughput7d} 张`):t("div",{className:"ds-hint"},`近 7 天完成 ${ye.throughput7d} 张`));return t("div",{className:"ds-body"},t("div",{className:"ds-proj-grid"},(ee.projects??[]).map(ge=>de(`${ge.key?`[${ge.key}] `:""}${ge.name}`,`${(ge.iterations??[]).length} 个迭代`,ge.metrics,ge.id)),ee.unassigned!==null&&ee.unassigned.total>0?de("未归属项目","还没挂到项目上的卡",ee.unassigned,""):null),(ee.projects??[]).length===0&&(ee.unassigned===null||ee.unassigned.total===0)?t("div",{className:"ds-empty"},"还没有卡片。先去「设置 → 项目与迭代」建一个项目,再新建任务。"):null)}const Ae={INPUT:1,TEXTAREA:1,SELECT:1,BUTTON:1,A:1,OPTION:1},ze='[data-on],[data-role],[role="button"],[tabindex],[class*="ds-"]';function At(X){if(X===null||X.nodeType!==1||Ae[X.tagName]===1||X.dataset.kb==="1"||typeof X.className=="string"&&X.className.indexOf("mask")>=0||X.getAttribute("contenteditable")==="true")return!1;let B="";try{B=window.getComputedStyle(X).cursor}catch{B=""}return B==="pointer"}function Vt(X){X.dataset.kb="1",X.getAttribute("tabindex")===null&&X.setAttribute("tabindex","0"),X.getAttribute("role")===null&&X.setAttribute("role","button");const B=X.getAttribute("data-on");if(B!==null&&X.setAttribute("aria-pressed",B==="1"?"true":"false"),X.getAttribute("aria-label")===null){const ee=String(X.getAttribute("title")??X.textContent??"").trim().replace(/\s+/g," ").slice(0,80);ee.length>0&&X.setAttribute("aria-label",ee)}X.addEventListener("keydown",ee=>{ee.key!=="Enter"&&ee.key!==" "||X.getAttribute("aria-disabled")!=="true"&&(ee.preventDefault(),X.click())})}function Je(){if(typeof document>"u")return()=>{};const X=B=>{const ee=B.target,ve=ee instanceof Element?ee.closest(".ds-board"):null;ve!==null&&(ve.scrollWidth<=ve.clientWidth+1||B.shiftKey||Math.abs(B.deltaY)<=Math.abs(B.deltaX)||(ve.scrollLeft+=B.deltaY,B.preventDefault()))};return document.addEventListener("wheel",X,{passive:!1}),()=>document.removeEventListener("wheel",X)}function bt(){if(typeof document>"u")return()=>{};const X=new WeakSet,B=[];let ee=!1;const ve=()=>typeof performance<"u"?performance.now():Date.now(),te=new WeakSet,oe=[];let de=null;const ge=()=>{ee=!1;const at=ve()+6,ct=[];for(;B.length>0&&ct.length<60&&ve()<at;)ct.push(B.pop());const qe=[],nt=[];for(const $e of ct)$e.isConnected!==!1&&(At($e)?qe.push($e):nt.push($e));for(const $e of qe)X.add($e),Vt($e);let et=!1;for(const $e of nt)te.has($e)||(te.add($e),$e.isConnected!==!1&&(oe.push($e),et=!0));et&&de===null&&(de=window.setTimeout(()=>{de=null,B.push(...oe.splice(0)),we()},400)),B.length>0&&we()},we=()=>{ee!==!0&&(ee=!0,typeof window.requestIdleCallback=="function"?window.requestIdleCallback(ge,{timeout:300}):window.setTimeout(ge,16))},ye=()=>{const at=document.querySelector("[data-dsh-studio]");if(at!==null){for(const ct of at.querySelectorAll(ze))X.has(ct)||te.has(ct)||ct.dataset.kb==="1"||B.push(ct);B.length>0&&we()}};ye();let Ee=null;const tt=new MutationObserver(()=>{Ee===null&&(Ee=window.setTimeout(()=>{Ee=null,ye()},200))});return tt.observe(document.body,{childList:!0,subtree:!0}),()=>tt.disconnect()}function ls({config:X,theme:B,message:ee,onDone:ve,onClose:te,overlay:oe}){const de=String(X?.localAuth?.username??"admin"),ge=X?.localAuth?.enabled!==!1,we=X?.ldap?.enabled===!0,[ye,Ee]=e.useState(we?"ldap":"local"),[tt,at]=e.useState(we?"":de),[ct,qe]=e.useState(""),[nt,et]=e.useState(!1),[$e,Pt]=e.useState(ee??""),Fe=pe=>{pe!==ye&&(Ee(pe),Pt(""),qe(""),at(pe==="local"?de:""))},yt=async()=>{if(!nt){if(tt.trim()===""||ct===""){Pt(ye==="local"?"本地账号和密码都要填":"域账号和密码都要填");return}et(!0),Pt("");try{const pe=await Z("/auth/login",{username:tt.trim(),password:ct,mode:ye});U(pe.token),Te(pe.user??null),$(String(pe.user?.name??pe.user?.id??tt.trim())),qe(""),ve()}catch(pe){Pt(pe instanceof Error?pe.message:String(pe))}finally{et(!1)}}},jt=t("div",{className:"ds-seg",style:{alignSelf:"flex-start"}},t("button",{className:"ds-seg-btn",type:"button","data-on":ye==="ldap"?"1":"0",disabled:!we,title:we?"用公司目录(LDAP)的账号登录":"LDAP 还没启用,先在设置里配好再用",onClick:()=>Fe("ldap")},"LDAP 账号"),t("button",{className:"ds-seg-btn",type:"button","data-on":ye==="local"?"1":"0",disabled:!ge,title:"用平台自带的超管账号登录",onClick:()=>Fe("local")},"本地超管")),Qt=ye==="ldap"?t("div",{className:"ds-login-hint"},"用公司账号登录(LDAP)。登录后会记下你的名字,写操作和审计都挂在这个名字下。"):t("div",{className:"ds-login-hint"},`用平台自带的超管账号登录。默认账号 ${de},默认口令 admin(改过之后就用新口令)。`),re=t("div",{className:"ds-login"},t("div",{className:"ds-login-brand"},"◈ ",X?.board?.title??"工作空间"),jt,Qt,t("label",{className:"ds-field"},t("span",null,ye==="local"?"超管账号":"域账号"),t("input",{value:tt,autoFocus:ye==="ldap",name:"studio-account",autoComplete:"off",spellCheck:!1,placeholder:ye==="local"?de:"域账号",onChange:pe=>at(pe.target.value),onKeyDown:pe=>{pe.key==="Enter"&&yt()}})),t("label",{className:"ds-field"},t("span",null,"密码"),t("input",{type:"password",value:ct,autoFocus:ye==="local",name:"studio-pass",autoComplete:"new-password",placeholder:ye==="local"?"默认口令 admin":"域密码",onChange:pe=>qe(pe.target.value),onKeyDown:pe=>{pe.key==="Enter"&&yt()}})),$e===""?null:t("div",{className:"ds-error"},$e),$e!==""&&/LDAP|域|目录|连不上/.test($e)?t("div",{className:"ds-login-note"},"连不上目录服务时,切到「本地超管」用平台账号进来;等网络打通再用域账号。"):null,t("div",{className:"ds-login-actions"},t("button",{className:"ds-btn ds-btn-primary",disabled:nt,onClick:()=>void yt()},nt?"登录中…":ye==="local"?"登录(本地超管)":"登录(LDAP)"),typeof te=="function"?t("button",{className:"ds-btn ds-btn-ghost",onClick:()=>te()},"先不登录"):null),t("div",{className:"ds-login-note"},ye==="ldap"?`${X?.ldap?.url??"—"} · BASE:${X?.ldap?.baseDN??"—"}`:"本地账号存在服务端密钥里(scrypt 哈希),改密码在「设置 → 统一登录(LDAP)」")),ke=t("div",{className:"ds-login-wrap","data-overlay":oe===!0?"1":"0"},re);return oe===!0?ke:t("div",{"data-dsh-studio":"","data-theme":B},t("style",null,he),ke)}function wa(X){const B=X?.loginGate??ls,{state:ee,error:ve,reload:te}=H(),[oe,de]=e.useState("dashboard"),[ge,we]=e.useState("experts"),[ye,Ee]=e.useState(null),[tt,at]=e.useState(!1);async function ct(z){const ie=await Tx(z,{api:Z,state:ee,confirm:C,prompt:E,notify:W});ie.refresh===!0&&await te(!0),ie.open!==void 0&&Ee(ie.open)}const[qe,nt]=e.useState(!1),[et,$e]=e.useState(0);e.useEffect(()=>{if(!qe)return;const z=()=>nt(!1);return document.addEventListener("click",z),()=>document.removeEventListener("click",z)},[qe]);const[Pt,Fe]=e.useState(Ne),[yt,jt]=e.useState(()=>K()??(O()?.matches?"dark":"light")),[Qt,re]=e.useState(!1),[ke,pe]=e.useState(!1),[Va,St]=e.useState(null),[la,Pn]=e.useState(0),[Ra,Dt]=e.useState(null);e.useEffect(()=>{const z=ie=>{const Le=String(ie.key).toLowerCase();if((ie.metaKey||ie.ctrlKey)&&!ie.shiftKey&&!ie.altKey&&Le==="k"){ie.preventDefault(),St(null),pe(Ge=>!Ge);return}const Ve=bx(ie);Ve!==null&&(ie.preventDefault(),St(Ve),Pn(Ge=>Ge+1),pe(!0))};return document.addEventListener("keydown",z),()=>document.removeEventListener("keydown",z)},[]),e.useEffect(()=>{const z=()=>{const Ge=K()??(O()?.matches?"dark":"light");jt(We=>We===Ge?We:Ge)};z();const ie=O(),Le=()=>z();ie!==null&&ie.addEventListener("change",Le);const Ve=window.setInterval(z,1500);return()=>{ie!==null&&ie.removeEventListener("change",Le),window.clearInterval(Ve)}},[]);const Ot=Pt==="system"?yt:Pt;if(e.useEffect(()=>{const z=document.documentElement;z.setAttribute("data-dsh-theme",Ot),z.style.colorScheme=Ot;let ie=document.getElementById("dsh-studio-global-theme");return ie===null&&(ie=document.createElement("style"),ie.id="dsh-studio-global-theme",ie.textContent=le,document.head.appendChild(ie)),()=>{z.removeAttribute("data-dsh-theme"),z.style.colorScheme=""}},[Ot]),e.useEffect(()=>bt(),[]),e.useEffect(()=>Je(),[]),ee===null)return t("div",{"data-dsh-studio":"","data-theme":Ot},t("style",null,he),t("div",{className:"ds-loading"},ve===null?"正在载入工作空间…":`载入失败:${ve}`));if(ee.locked===!0)return t(B,{config:ee.config,theme:Ot,message:"",onDone:()=>void te()});const Q=ye===null?null:ee.tasks.find(z=>z.id===ye)??null;return ee.tasks.reduce((z,ie)=>(z[ie.status]=(z[ie.status]??0)+1,z),{}),t("div",{"data-dsh-studio":"","data-theme":Ot},t("style",null,he),t(_,null),t(F,null),t("div",{className:"ds-head"},t("div",{className:"ds-head-top"},t("div",{className:"ds-brand"},t("span",{className:"ds-brand-mark"},"◈"),t("div",null,t("div",{className:"ds-title"},ee.config.board.title),t("div",{className:"ds-sub"},`${ee.tasks.length} 个任务 · rev ${ee.revision} · 更新于 ${Ye(ee.updatedAt)}`))),t("div",{className:"ds-head-actions"},ae()===null?t(u,{className:"ds-chip","data-on":"1",title:"没配 LDAP 时平台默认以超管身份使用,不需要登录;写操作与审计都记在它名下"},`👤 超管 ${ee.config.localAuth?.username??"admin"}`):t(u,{className:"ds-chip",title:`已登录:${ae()?.id??""}`},`👤 ${ae()?.name??ae()?.id}`),ae()===null?t("button",{className:"ds-btn ds-btn-ghost",title:"用域账号或本地超管账号登录",onClick:()=>re(!0)},"登录"):i("退出当前账号(退出后回到超管身份)",t("button",{className:"ds-btn ds-btn-ghost",onClick:async()=>{try{await Z("/auth/logout",{})}catch{}U(""),Te(null),re(!1),te()}},"退出登录")),(()=>{const z=o("SegGroup"),ie=I.map(([Le,Ve])=>({id:Le,label:Ve}));return z!==null?t(z,{value:Pt,onChange:Le=>Fe(fe(Le)),options:ie}):t("div",{className:"ds-seg",title:"显示模式;「跟随系统」会先跟宿主界面的深浅色,宿主没给出信号时再跟操作系统"},I.map(([Le,Ve])=>t("button",{key:Le,className:"ds-seg-btn","data-on":Pt===Le?"1":"0",onClick:()=>Fe(fe(Le))},Ve)))})(),t("button",{className:"ds-btn",onClick:()=>void te()},"刷新"),t("a",{className:"ds-btn ds-btn-ghost",style:{textDecoration:"none"},href:"/studio-api/export/tasks.csv",title:"把看板上的卡导出成 CSV(Excel 可直接打开)"},"导出卡"),t("a",{className:"ds-btn ds-btn-ghost",style:{textDecoration:"none"},href:"/studio-api/export/requirements.csv",title:"把需求与准出情况导出成 CSV"},"导出需求"),(ee.integrations?.broken??0)>0?t("button",{className:"ds-btn ds-btn-ghost",style:{color:"#e5484d",borderColor:"color-mix(in srgb, #e5484d 40%, var(--ds-line))"},title:(ee.integrations.channels??[]).filter(z=>z.state==="error").map(z=>`${z.label}:${z.lastError}`).join(`