zen-gitsync 2.17.31 → 2.17.33

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.
Files changed (55) hide show
  1. package/README.md +14 -6
  2. package/package.json +10 -1
  3. package/scripts/release.js +66 -29
  4. package/src/ui/public/assets/{AgentEngineSelector-bQmJlH1b.js → AgentEngineSelector-CGrUh2kD.js} +1 -1
  5. package/src/ui/public/assets/{AgentView-DpNj6VFj.js → AgentView-BRC3kn-3.js} +1 -1
  6. package/src/ui/public/assets/{AppVersionBadge-2etZBF2E.js → AppVersionBadge-BR_ZX6Oq.js} +2 -2
  7. package/src/ui/public/assets/{BranchSelector-BvbMlZW6.js → BranchSelector-C77-aizC.js} +1 -1
  8. package/src/ui/public/assets/{CommitForm-C-cL8_vz.css → CommitForm-BbgIoF4h.css} +1 -1
  9. package/src/ui/public/assets/CommitForm-C6qTIW0T.js +6 -0
  10. package/src/ui/public/assets/{CommonDialog-UrQUmb6G.js → CommonDialog-kDck8ITZ.js} +1 -1
  11. package/src/ui/public/assets/{EditorView-DIu2cIai.js → EditorView-DNZXnJnc.js} +1 -1
  12. package/src/ui/public/assets/{FileDiffViewer-BlDGBxKd.js → FileDiffViewer-BxFkqvIA.js} +1 -1
  13. package/src/ui/public/assets/{FlowExecutionViewer-MPq4Fh6G.js → FlowExecutionViewer-DDkb05TW.js} +1 -1
  14. package/src/ui/public/assets/{FlowOrchestrationWorkspace-ByBJ4B3G.js → FlowOrchestrationWorkspace-BiHGQE0o.js} +1 -1
  15. package/src/ui/public/assets/{IconButton-BvbRjQ_D.js → IconButton-BflzboUD.js} +1 -1
  16. package/src/ui/public/assets/{LogList-Xs52JPC1.js → LogList-DHfr-rPz.js} +1 -1
  17. package/src/ui/public/assets/{MindmapView-Br7Fj7Ga.js → MindmapView-7u02tj5n.js} +1 -1
  18. package/src/ui/public/assets/{MonitorView-DSV5_Q-6.js → MonitorView-cdUkh4VE.js} +1 -1
  19. package/src/ui/public/assets/{ProjectStartupButton-BCS2YQdX.js → ProjectStartupButton-B9KAavAO.js} +1 -1
  20. package/src/ui/public/assets/{RecentDirectoriesChat-Dv47YPKL.js → RecentDirectoriesChat-DQ9phSSJ.js} +1 -1
  21. package/src/ui/public/assets/{RemoteManagerDialog-CIHTYzr7.js → RemoteManagerDialog-CgnM1ndZ.js} +1 -1
  22. package/src/ui/public/assets/{RemoteRepoCard-DCYTEmuq.js → RemoteRepoCard-BDAvJcFk.js} +1 -1
  23. package/src/ui/public/assets/{SourceMapView-BANSmwrh.js → SourceMapView-BEAC-dHC.js} +1 -1
  24. package/src/ui/public/assets/{SvgIcon-b50XyCO5.js → SvgIcon-B-2iTLW-.js} +1 -1
  25. package/src/ui/public/assets/{TemplateManager-D5-gC33Z.js → TemplateManager-CYy3G7rH.js} +1 -1
  26. package/src/ui/public/assets/{UserInputNode-BFmTQYZP.js → UserInputNode-DIX4shBD.js} +1 -1
  27. package/src/ui/public/assets/WorkbenchView-BAko4SjJ.js +20 -0
  28. package/src/ui/public/assets/WorkbenchView-DWQChnqF.css +1 -0
  29. package/src/ui/public/assets/{_plugin-vue_export-helper-MIPci_U9.js → _plugin-vue_export-helper-D8rwCQpe.js} +6 -6
  30. package/src/ui/public/assets/{agentConversations-D8mdZaNY.js → agentConversations-C0WoSH0y.js} +1 -1
  31. package/src/ui/public/assets/{configStore-BMixvVAA.js → configStore-BFGSfIBp.js} +1 -1
  32. package/src/ui/public/assets/{gitStore-DriJnutC.js → gitStore-BcrFZogC.js} +1 -1
  33. package/src/ui/public/assets/index-BCTkDpQv.css +1 -0
  34. package/src/ui/public/assets/{index-CEEQaAF4.js → index-aNSr4k87.js} +10 -10
  35. package/src/ui/public/assets/{previewSandbox-D86LH88s.js → previewSandbox-C_ZzCEpS.js} +1 -1
  36. package/src/ui/public/assets/{toolsStore-B1Lx5gWu.js → toolsStore-B3LQrSEp.js} +1 -1
  37. package/src/ui/public/index.html +12 -12
  38. package/src/ui/server/routes/workbench/index.js +182 -18
  39. package/src/ui/server/routes/workbench/jobActivity.js +18 -6
  40. package/src/ui/server/routes/workbench/liveJobs.js +7 -1
  41. package/src/ui/server/routes/workbench/llmClient.js +37 -6
  42. package/src/ui/server/routes/workbench/progressReport.js +141 -50
  43. package/src/ui/server/routes/workbench/progressReport.test.js +101 -4
  44. package/src/ui/server/routes/workbench/projectRegistry.js +58 -2
  45. package/src/ui/server/routes/workbench/projectRegistry.test.js +51 -0
  46. package/src/ui/server/routes/workbench/stallWatchdog.js +330 -0
  47. package/src/ui/server/routes/workbench/stallWatchdog.test.js +356 -0
  48. package/src/ui/server/utils/directoryGitState.js +41 -1
  49. package/src/ui/server/utils/directoryGitState.test.js +91 -0
  50. package/src/utils/desktopNotify.js +199 -41
  51. package/src/utils/desktopNotify.test.js +84 -29
  52. package/src/ui/public/assets/CommitForm-Bp41Q0Oo.js +0 -6
  53. package/src/ui/public/assets/WorkbenchView-Dc7bMMfe.css +0 -1
  54. package/src/ui/public/assets/WorkbenchView-DsPII7uQ.js +0 -20
  55. package/src/ui/public/assets/index-BrIl1iaG.css +0 -1
@@ -1,5 +1,5 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/office-docx-mH6Ds72v.js","assets/rolldown-runtime-C0FnF6B9.js","assets/element-plus-CgEuJBlB.js","assets/dagre-Dfa6RS7s.js","assets/element-plus-Qexo5hiM.css","assets/office-excel-Doz15UyW.js","assets/office-pptx-C9I5OR_u.js"])))=>i.map(i=>d[i]);
2
- import{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{$n as t,Bn as n,Dn as r,En as i,Er as a,Fn as o,Ht as s,In as c,J as l,On as u,Pn as d,Pr as f,Qr as p,Tn as m,W as h,Yn as g,Yr as _,Zn as v,br as y,cn as b,cr as x,kn as S,nr as C,or as w,pr as T,qn as E,qr as D,sn as O,wt as k,xn as A}from"./element-plus-CgEuJBlB.js";import{_ as j}from"./vendor-CDZ64WVU.js";import{t as M}from"./_plugin-vue_export-helper-MIPci_U9.js";import{ir as N}from"./dagre-Dfa6RS7s.js";import{n as P,r as F}from"./flow-mindmap-CzytSMsF.js";var I=new Set([`png`,`jpg`,`jpeg`,`gif`,`webp`,`bmp`,`ico`,`svg`]);function L(e){if(!e)return!1;let t=e.split(`.`).pop()?.toLowerCase()??``;return I.has(t)}function ee(e){return`/api/editor/raw?path=${encodeURIComponent(e)}`}var R=new Set([`doc`,`docx`,`xls`,`xlsx`,`ppt`,`pptx`,`odt`,`ods`,`odp`]);function z(e){return e.split(/[\\/]/).pop()?.split(`.`).pop()?.toLowerCase()||``}function B(e){return R.has(z(e))}function V(e,t={}){let n=new URLSearchParams({source:t.source||`worktree`,file:e});return t.rev&&n.set(`rev`,t.rev),t.side&&n.set(`side`,t.side),`/api/office/preview?${n.toString()}`}function H(e,t={}){let n=new URLSearchParams({source:t.source||`worktree`,file:e});return t.rev&&n.set(`rev`,t.rev),t.side&&n.set(`side`,t.side),`/api/office/raw?${n.toString()}`}n(),f(),T();var te={class:`image-preview-toolbar`},U=[`title`],W={key:0,class:`image-preview-dim`},G={key:1,class:`image-preview-dim`},K={key:0,class:`image-preview-error`},q={class:`image-preview-error-path`},J=[`src`,`alt`],Y={class:`image-preview-footer`},X={class:`image-preview-zoom`},Z=M(c({__name:`ImagePreview`,props:{filePath:{},fileName:{}},setup(e){let t=e,n=y(null),r=y(1),o=y(0),c=y(0),u=y(!1),f=y({x:0,y:0}),C=y(null),T=y(!1),A=y(null),j=()=>{if(!n.value||!C.value)return;let e=n.value.clientWidth-32,t=n.value.clientHeight-32;if(e<=0||t<=0)return;let i=Math.min(e/C.value.w,t/C.value.h,1);r.value=i,o.value=0,c.value=0},M=()=>{r.value=Math.min(r.value*1.2,8)},N=()=>{r.value=Math.max(r.value/1.2,.1)},P=()=>{r.value=1,o.value=0,c.value=0,j()},F=e=>{e.preventDefault(),e.deltaY<0?M():N()},I=e=>{e.button===0&&(u.value=!0,f.value={x:e.clientX-o.value,y:e.clientY-c.value})},L=e=>{u.value&&(o.value=e.clientX-f.value.x,c.value=e.clientY-f.value.y)},R=()=>{u.value=!1},z=e=>{let t=e.target;C.value={w:t.naturalWidth,h:t.naturalHeight},T.value=!1,requestAnimationFrame(()=>j())},B=()=>{T.value=!0},V=m(()=>t.fileName||t.filePath?.split(`/`).pop()||``),H=m(()=>ee(t.filePath));return g(()=>{window.addEventListener(`mousemove`,L),window.addEventListener(`mouseup`,R),window.addEventListener(`resize`,j)}),E(()=>{window.removeEventListener(`mousemove`,L),window.removeEventListener(`mouseup`,R),window.removeEventListener(`resize`,j)}),w(()=>t.filePath,()=>{C.value=null,T.value=!1,r.value=1,o.value=0,c.value=0}),(e,f)=>{let m=h;return v(),S(`div`,{class:`image-preview`,ref_key:`containerRef`,ref:n,onWheel:F},[i(`div`,te,[i(`span`,{class:`image-preview-name`,title:t.filePath},p(V.value),9,U),C.value?(v(),S(`span`,W,p(C.value.w)+` × `+p(C.value.h)+` px`,1)):(v(),S(`span`,G,p(Math.round(r.value*100))+`%`,1)),f[0]||=i(`div`,{class:`image-preview-spacer`},null,-1),d(m,{content:`放大`,placement:`top`},{default:x(()=>[i(`button`,{class:`image-preview-btn`,onClick:M},[d(a(l),null,{default:x(()=>[d(a(O))]),_:1})])]),_:1}),d(m,{content:`缩小`,placement:`top`},{default:x(()=>[i(`button`,{class:`image-preview-btn`,onClick:N},[d(a(l),null,{default:x(()=>[d(a(b))]),_:1})])]),_:1}),d(m,{content:`适应窗口`,placement:`top`},{default:x(()=>[i(`button`,{class:`image-preview-btn`,onClick:P},[d(a(l),null,{default:x(()=>[d(a(s))]),_:1})])]),_:1})]),i(`div`,{class:D([`image-preview-stage`,{"is-dragging":u.value}]),onMousedown:I},[T.value?(v(),S(`div`,K,[d(a(l),{size:`32`,color:`var(--color-danger)`},{default:x(()=>[d(a(k))]),_:1}),f[1]||=i(`p`,null,`图片加载失败`,-1),i(`p`,q,p(t.filePath),1)])):(v(),S(`img`,{key:1,ref_key:`imgRef`,ref:A,src:H.value,alt:V.value,class:`image-preview-img`,style:_({transform:`translate(${o.value}px, ${c.value}px) scale(${r.value})`}),onLoad:z,onError:B,draggable:`false`},null,44,J))],34),i(`div`,Y,[f[2]||=i(`span`,null,`滚轮缩放 · 拖拽平移 · 双击重置`,-1),i(`span`,X,p(Math.round(r.value*100))+`%`,1)])],544)}}}),[[`__scopeId`,`data-v-28b3ff72`]]);n(),T();var ne={class:`md-preview`},re=[`innerHTML`],ie={key:1,class:`md-mindmap`},ae=M(c({__name:`MarkdownPreview`,props:{content:{},allowHtml:{type:Boolean,default:!0}},setup(e){let n=e;function r(e){if(typeof document>`u`)return e;let t=document.createElement(`template`);return t.innerHTML=e,t.content.querySelectorAll(`script, style, iframe, object, embed, form, input, button, textarea, select`).forEach(e=>e.remove()),t.content.querySelectorAll(`*`).forEach(e=>{for(let t of Array.from(e.attributes)){let n=t.name.toLowerCase();if(n.startsWith(`on`)||n===`style`||n===`srcdoc`){e.removeAttribute(t.name);continue}if(n===`href`||n===`src`||n===`xlink:href`){let r=t.value.trim().replace(/[\u0000-\u0020]+/g,``);/^(https?:|mailto:|tel:|#|\/|\.\/|\.\.\/)/i.test(r)||n===`src`&&/^data:image\/(?:png|gif|jpe?g|webp);base64,/i.test(r)||e.removeAttribute(t.name)}}e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`)}),t.innerHTML}let o=m(()=>{let e=n.content||``,t=[],i=`@@FLOWDASH_MINDMAP_BLOCK_`,a=e.replace(/```[ \t]*mindmap[ \t]*\n([\s\S]*?)```/gi,(e,n)=>{let r=t.length;return t.push({id:r,md:n}),`${i}${r}@@`}),o=j(a,{className:!1,html:n.allowHtml}),s=n.allowHtml?o:r(o),c=RegExp(`(?:<p>)?\\s*${i}(\\d+)@@\\s*(?:</p>)?`,`g`),l=[],u=0,d;for(;(d=c.exec(s))!==null;){d.index>u&&l.push({type:`html`,html:s.slice(u,d.index)});let e=Number(d[1]);l.push({type:`mindmap`,id:e,md:t[e].md,data:P(t[e].md)}),u=d.index+d[0].length}return u<s.length&&l.push({type:`html`,html:s.slice(u)}),l.length===0&&l.push({type:`html`,html:`<p class="md-empty">无内容</p>`}),l});return(e,n)=>(v(),S(`div`,ne,[(v(!0),S(A,null,t(o.value,(e,t)=>(v(),S(A,{key:t},[e.type===`html`?(v(),S(`div`,{key:0,class:`md-segment`,innerHTML:e.html},null,8,re)):(v(),S(`div`,ie,[n[0]||=i(`div`,{class:`md-mindmap-title`},`四维导图`,-1),d(a(F),{data:e.data,"preview-mode":``,class:`md-mindmap-canvas`},null,8,[`data`])]))],64))),128))]))}}),[[`__scopeId`,`data-v-9514c256`]]);n(),f(),T();var oe={class:`office-preview`},se={class:`office-preview-toolbar`},Q={class:`office-preview-badge`},ce={key:0,class:`office-preview-side`},le={key:0,class:`office-preview-state`},ue={key:1,class:`office-preview-state office-preview-error`},de=[`src`],fe=M(c({__name:`OfficePreview`,props:{filePath:{},source:{default:`worktree`},rev:{default:``},side:{default:`new`}},setup(t){let n=t,a=o(()=>N(()=>import(`./office-docx-mH6Ds72v.js`).then(t=>e(t.t(),1)),__vite__mapDeps([0,1,2,3,4]))),s=o(()=>N(()=>import(`./office-excel-Doz15UyW.js`).then(t=>e(t.t(),1)),__vite__mapDeps([5,1,0,2,3,4]))),c=o(()=>N(()=>import(`./office-pptx-C9I5OR_u.js`).then(t=>e(t.t(),1)),__vite__mapDeps([6,1,0,2,3,4]))),l=m(()=>z(n.filePath)),d=m(()=>l.value===`docx`?a:l.value===`xls`||l.value===`xlsx`?s:l.value===`pptx`?c:null),f=m(()=>H(n.filePath,{source:n.source,rev:n.rev,side:n.side})),h=m(()=>V(n.filePath,{source:n.source,rev:n.rev,side:n.side})),g=y(null),_=y(``),b=y(!1),x=y(!1),T=y(!1),D=0;function O(){_.value&&URL.revokeObjectURL(_.value),_.value=``}async function k(e){try{let t=await fetch(h.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==D)return;_.value=URL.createObjectURL(await t.blob()),T.value=!0,x.value=!1}catch{e===D&&(x.value=!0)}}async function A(){let e=++D;if(b.value=!0,x.value=!1,T.value=!1,g.value=null,O(),!d.value){await k(e),e===D&&(b.value=!1);return}try{let t=await fetch(f.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==D)return;g.value=await t.arrayBuffer()}catch{await k(e)}finally{e===D&&(b.value=!1)}}function j(){if(!T.value){let e=D;g.value=null,b.value=!0,k(e).finally(()=>{e===D&&(b.value=!1)})}}function M(){A()}return w(f,()=>{A()},{immediate:!0}),E(()=>{D++,O()}),(e,n)=>(v(),S(`div`,oe,[i(`div`,se,[n[0]||=i(`span`,{class:`office-preview-title`},`Office 预览`,-1),i(`span`,Q,p(l.value.toUpperCase()),1),t.source===`git`?(v(),S(`span`,ce,p(t.side===`old`?`旧版本`:`新版本`),1)):u(``,!0),i(`button`,{class:`office-preview-reload`,type:`button`,onClick:M},`刷新`)]),b.value?(v(),S(`div`,le,`正在生成预览…`)):x.value?(v(),S(`div`,ue,[...n[1]||=[i(`p`,null,`无法预览此 Office 文件。`,-1),i(`p`,{class:`office-preview-hint`},`支持 docx、xls/xlsx、pptx;老格式需要安装 LibreOffice 才能兜底预览。`,-1)]])):T.value?(v(),S(`iframe`,{key:2,class:`office-preview-frame`,src:_.value,title:`Office 文件 PDF 预览`},null,8,de)):d.value&&g.value?(v(),r(C(d.value),{key:3,class:`office-preview-document`,src:g.value,onError:j},null,40,[`src`])):u(``,!0)]))}}),[[`__scopeId`,`data-v-89cd4207`]]),pe=`allow-scripts allow-forms allow-popups allow-modals`,$=`<script>(function(){
2
+ import{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{$n as t,Bn as n,Dn as r,En as i,Er as a,Fn as o,Ht as s,In as c,J as l,On as u,Pn as d,Pr as f,Qr as p,Tn as m,W as h,Yn as g,Yr as _,Zn as v,br as y,cn as b,cr as x,kn as S,nr as C,or as w,pr as T,qn as E,qr as D,sn as O,wt as k,xn as A}from"./element-plus-CgEuJBlB.js";import{_ as j}from"./vendor-CDZ64WVU.js";import{t as M}from"./_plugin-vue_export-helper-D8rwCQpe.js";import{ir as N}from"./dagre-Dfa6RS7s.js";import{n as P,r as F}from"./flow-mindmap-CzytSMsF.js";var I=new Set([`png`,`jpg`,`jpeg`,`gif`,`webp`,`bmp`,`ico`,`svg`]);function L(e){if(!e)return!1;let t=e.split(`.`).pop()?.toLowerCase()??``;return I.has(t)}function ee(e){return`/api/editor/raw?path=${encodeURIComponent(e)}`}var R=new Set([`doc`,`docx`,`xls`,`xlsx`,`ppt`,`pptx`,`odt`,`ods`,`odp`]);function z(e){return e.split(/[\\/]/).pop()?.split(`.`).pop()?.toLowerCase()||``}function B(e){return R.has(z(e))}function V(e,t={}){let n=new URLSearchParams({source:t.source||`worktree`,file:e});return t.rev&&n.set(`rev`,t.rev),t.side&&n.set(`side`,t.side),`/api/office/preview?${n.toString()}`}function H(e,t={}){let n=new URLSearchParams({source:t.source||`worktree`,file:e});return t.rev&&n.set(`rev`,t.rev),t.side&&n.set(`side`,t.side),`/api/office/raw?${n.toString()}`}n(),f(),T();var te={class:`image-preview-toolbar`},U=[`title`],W={key:0,class:`image-preview-dim`},G={key:1,class:`image-preview-dim`},K={key:0,class:`image-preview-error`},q={class:`image-preview-error-path`},J=[`src`,`alt`],Y={class:`image-preview-footer`},X={class:`image-preview-zoom`},Z=M(c({__name:`ImagePreview`,props:{filePath:{},fileName:{}},setup(e){let t=e,n=y(null),r=y(1),o=y(0),c=y(0),u=y(!1),f=y({x:0,y:0}),C=y(null),T=y(!1),A=y(null),j=()=>{if(!n.value||!C.value)return;let e=n.value.clientWidth-32,t=n.value.clientHeight-32;if(e<=0||t<=0)return;let i=Math.min(e/C.value.w,t/C.value.h,1);r.value=i,o.value=0,c.value=0},M=()=>{r.value=Math.min(r.value*1.2,8)},N=()=>{r.value=Math.max(r.value/1.2,.1)},P=()=>{r.value=1,o.value=0,c.value=0,j()},F=e=>{e.preventDefault(),e.deltaY<0?M():N()},I=e=>{e.button===0&&(u.value=!0,f.value={x:e.clientX-o.value,y:e.clientY-c.value})},L=e=>{u.value&&(o.value=e.clientX-f.value.x,c.value=e.clientY-f.value.y)},R=()=>{u.value=!1},z=e=>{let t=e.target;C.value={w:t.naturalWidth,h:t.naturalHeight},T.value=!1,requestAnimationFrame(()=>j())},B=()=>{T.value=!0},V=m(()=>t.fileName||t.filePath?.split(`/`).pop()||``),H=m(()=>ee(t.filePath));return g(()=>{window.addEventListener(`mousemove`,L),window.addEventListener(`mouseup`,R),window.addEventListener(`resize`,j)}),E(()=>{window.removeEventListener(`mousemove`,L),window.removeEventListener(`mouseup`,R),window.removeEventListener(`resize`,j)}),w(()=>t.filePath,()=>{C.value=null,T.value=!1,r.value=1,o.value=0,c.value=0}),(e,f)=>{let m=h;return v(),S(`div`,{class:`image-preview`,ref_key:`containerRef`,ref:n,onWheel:F},[i(`div`,te,[i(`span`,{class:`image-preview-name`,title:t.filePath},p(V.value),9,U),C.value?(v(),S(`span`,W,p(C.value.w)+` × `+p(C.value.h)+` px`,1)):(v(),S(`span`,G,p(Math.round(r.value*100))+`%`,1)),f[0]||=i(`div`,{class:`image-preview-spacer`},null,-1),d(m,{content:`放大`,placement:`top`},{default:x(()=>[i(`button`,{class:`image-preview-btn`,onClick:M},[d(a(l),null,{default:x(()=>[d(a(O))]),_:1})])]),_:1}),d(m,{content:`缩小`,placement:`top`},{default:x(()=>[i(`button`,{class:`image-preview-btn`,onClick:N},[d(a(l),null,{default:x(()=>[d(a(b))]),_:1})])]),_:1}),d(m,{content:`适应窗口`,placement:`top`},{default:x(()=>[i(`button`,{class:`image-preview-btn`,onClick:P},[d(a(l),null,{default:x(()=>[d(a(s))]),_:1})])]),_:1})]),i(`div`,{class:D([`image-preview-stage`,{"is-dragging":u.value}]),onMousedown:I},[T.value?(v(),S(`div`,K,[d(a(l),{size:`32`,color:`var(--color-danger)`},{default:x(()=>[d(a(k))]),_:1}),f[1]||=i(`p`,null,`图片加载失败`,-1),i(`p`,q,p(t.filePath),1)])):(v(),S(`img`,{key:1,ref_key:`imgRef`,ref:A,src:H.value,alt:V.value,class:`image-preview-img`,style:_({transform:`translate(${o.value}px, ${c.value}px) scale(${r.value})`}),onLoad:z,onError:B,draggable:`false`},null,44,J))],34),i(`div`,Y,[f[2]||=i(`span`,null,`滚轮缩放 · 拖拽平移 · 双击重置`,-1),i(`span`,X,p(Math.round(r.value*100))+`%`,1)])],544)}}}),[[`__scopeId`,`data-v-28b3ff72`]]);n(),T();var ne={class:`md-preview`},re=[`innerHTML`],ie={key:1,class:`md-mindmap`},ae=M(c({__name:`MarkdownPreview`,props:{content:{},allowHtml:{type:Boolean,default:!0}},setup(e){let n=e;function r(e){if(typeof document>`u`)return e;let t=document.createElement(`template`);return t.innerHTML=e,t.content.querySelectorAll(`script, style, iframe, object, embed, form, input, button, textarea, select`).forEach(e=>e.remove()),t.content.querySelectorAll(`*`).forEach(e=>{for(let t of Array.from(e.attributes)){let n=t.name.toLowerCase();if(n.startsWith(`on`)||n===`style`||n===`srcdoc`){e.removeAttribute(t.name);continue}if(n===`href`||n===`src`||n===`xlink:href`){let r=t.value.trim().replace(/[\u0000-\u0020]+/g,``);/^(https?:|mailto:|tel:|#|\/|\.\/|\.\.\/)/i.test(r)||n===`src`&&/^data:image\/(?:png|gif|jpe?g|webp);base64,/i.test(r)||e.removeAttribute(t.name)}}e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`)}),t.innerHTML}let o=m(()=>{let e=n.content||``,t=[],i=`@@FLOWDASH_MINDMAP_BLOCK_`,a=e.replace(/```[ \t]*mindmap[ \t]*\n([\s\S]*?)```/gi,(e,n)=>{let r=t.length;return t.push({id:r,md:n}),`${i}${r}@@`}),o=j(a,{className:!1,html:n.allowHtml}),s=n.allowHtml?o:r(o),c=RegExp(`(?:<p>)?\\s*${i}(\\d+)@@\\s*(?:</p>)?`,`g`),l=[],u=0,d;for(;(d=c.exec(s))!==null;){d.index>u&&l.push({type:`html`,html:s.slice(u,d.index)});let e=Number(d[1]);l.push({type:`mindmap`,id:e,md:t[e].md,data:P(t[e].md)}),u=d.index+d[0].length}return u<s.length&&l.push({type:`html`,html:s.slice(u)}),l.length===0&&l.push({type:`html`,html:`<p class="md-empty">无内容</p>`}),l});return(e,n)=>(v(),S(`div`,ne,[(v(!0),S(A,null,t(o.value,(e,t)=>(v(),S(A,{key:t},[e.type===`html`?(v(),S(`div`,{key:0,class:`md-segment`,innerHTML:e.html},null,8,re)):(v(),S(`div`,ie,[n[0]||=i(`div`,{class:`md-mindmap-title`},`四维导图`,-1),d(a(F),{data:e.data,"preview-mode":``,class:`md-mindmap-canvas`},null,8,[`data`])]))],64))),128))]))}}),[[`__scopeId`,`data-v-9514c256`]]);n(),f(),T();var oe={class:`office-preview`},se={class:`office-preview-toolbar`},Q={class:`office-preview-badge`},ce={key:0,class:`office-preview-side`},le={key:0,class:`office-preview-state`},ue={key:1,class:`office-preview-state office-preview-error`},de=[`src`],fe=M(c({__name:`OfficePreview`,props:{filePath:{},source:{default:`worktree`},rev:{default:``},side:{default:`new`}},setup(t){let n=t,a=o(()=>N(()=>import(`./office-docx-mH6Ds72v.js`).then(t=>e(t.t(),1)),__vite__mapDeps([0,1,2,3,4]))),s=o(()=>N(()=>import(`./office-excel-Doz15UyW.js`).then(t=>e(t.t(),1)),__vite__mapDeps([5,1,0,2,3,4]))),c=o(()=>N(()=>import(`./office-pptx-C9I5OR_u.js`).then(t=>e(t.t(),1)),__vite__mapDeps([6,1,0,2,3,4]))),l=m(()=>z(n.filePath)),d=m(()=>l.value===`docx`?a:l.value===`xls`||l.value===`xlsx`?s:l.value===`pptx`?c:null),f=m(()=>H(n.filePath,{source:n.source,rev:n.rev,side:n.side})),h=m(()=>V(n.filePath,{source:n.source,rev:n.rev,side:n.side})),g=y(null),_=y(``),b=y(!1),x=y(!1),T=y(!1),D=0;function O(){_.value&&URL.revokeObjectURL(_.value),_.value=``}async function k(e){try{let t=await fetch(h.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==D)return;_.value=URL.createObjectURL(await t.blob()),T.value=!0,x.value=!1}catch{e===D&&(x.value=!0)}}async function A(){let e=++D;if(b.value=!0,x.value=!1,T.value=!1,g.value=null,O(),!d.value){await k(e),e===D&&(b.value=!1);return}try{let t=await fetch(f.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==D)return;g.value=await t.arrayBuffer()}catch{await k(e)}finally{e===D&&(b.value=!1)}}function j(){if(!T.value){let e=D;g.value=null,b.value=!0,k(e).finally(()=>{e===D&&(b.value=!1)})}}function M(){A()}return w(f,()=>{A()},{immediate:!0}),E(()=>{D++,O()}),(e,n)=>(v(),S(`div`,oe,[i(`div`,se,[n[0]||=i(`span`,{class:`office-preview-title`},`Office 预览`,-1),i(`span`,Q,p(l.value.toUpperCase()),1),t.source===`git`?(v(),S(`span`,ce,p(t.side===`old`?`旧版本`:`新版本`),1)):u(``,!0),i(`button`,{class:`office-preview-reload`,type:`button`,onClick:M},`刷新`)]),b.value?(v(),S(`div`,le,`正在生成预览…`)):x.value?(v(),S(`div`,ue,[...n[1]||=[i(`p`,null,`无法预览此 Office 文件。`,-1),i(`p`,{class:`office-preview-hint`},`支持 docx、xls/xlsx、pptx;老格式需要安装 LibreOffice 才能兜底预览。`,-1)]])):T.value?(v(),S(`iframe`,{key:2,class:`office-preview-frame`,src:_.value,title:`Office 文件 PDF 预览`},null,8,de)):d.value&&g.value?(v(),r(C(d.value),{key:3,class:`office-preview-document`,src:g.value,onError:j},null,40,[`src`])):u(``,!0)]))}}),[[`__scopeId`,`data-v-89cd4207`]]),pe=`allow-scripts allow-forms allow-popups allow-modals`,$=`<script>(function(){
3
3
  try{window.localStorage.getItem('__zk')}catch(e){
4
4
  var d={};var s={getItem:function(k){return Object.prototype.hasOwnProperty.call(d,k)?d[k]:null},setItem:function(k,v){d[k]=String(v)},removeItem:function(k){delete d[k]},clear:function(){d={}},key:function(i){var ks=Object.keys(d);return i<ks.length?ks[i]:null}};
5
5
  Object.defineProperty(window,'localStorage',{get:function(){return s},configurable:true});
@@ -1 +1 @@
1
- import{br as e,pr as t}from"./element-plus-CgEuJBlB.js";import{S as n}from"./vendor-CDZ64WVU.js";import{n as r}from"./_plugin-vue_export-helper-MIPci_U9.js";import{a as i,i as a}from"./configStore-BMixvVAA.js";t();var o=6e5;function s(e){if(!e||typeof e!=`object`)return null;let t=e;return typeof t.name!=`string`||!t.name?null:{name:t.name,detail:typeof t.detail==`string`&&t.detail?t.detail:null,provider:typeof t.provider==`string`&&t.provider?t.provider:null,source:t.source===`config`||t.source===`state`?t.source:null}}var c=n(`tools`,()=>{let t=e(!1),n=e(!1),c=e(!1),l=e(!1),u=e(!1),d=e(!1),f=e(!1),p=e(null),m=e(!1),h=e(``),g=e({}),_=e({}),v=e({}),y=e(!1),b=0,x=null;function S(e=!1){return!e&&Date.now()-b<3e5?Promise.resolve():x||(y.value=!0,x=(async()=>{try{let e=await(await fetch(`/api/latest-tool-versions`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})})).json();e.success&&e.latest&&typeof e.latest==`object`&&(v.value=e.latest,b=Date.now())}catch{}finally{y.value=!1,x=null}})(),x)}let C=null;function w(){return C||(m.value=!0,C=(async()=>{try{let e=await(await fetch(`/api/check-tools`)).json();e.success&&(t.value=!!e.vscode,n.value=!!e.claude,c.value=!!e.codex,l.value=!!e.opencode,u.value=!!e.kimi,d.value=!!e.zcode,f.value=!!e.dsh,h.value=typeof e.platform==`string`?e.platform:``,g.value=e.installers&&typeof e.installers==`object`?e.installers:{},_.value=e.versions&&typeof e.versions==`object`?e.versions:{},p.value=Date.now())}catch{}finally{m.value=!1,C=null}})(),C)}function T(e){switch(e){case`vscode`:return t.value;case`claude`:return n.value;case`codex`:return c.value;case`opencode`:return l.value;case`kimi`:return u.value;case`zcode`:return d.value;case`dsh`:return f.value}}let E=e({}),D=e(!1),O=0,k=null;function A(e){if(!D.value)return{status:`unknown`};if(!i(e))return{status:`unset`};let t=E.value[e];return t?{status:`set`,info:t}:{status:`unset`}}function j(e=!1){return!e&&D.value&&Date.now()-O<6e5?Promise.resolve():k||(k=(async()=>{try{let e=await(await fetch(`/api/workbench/executor-models`)).json();if(e?.success&&e.models&&typeof e.models==`object`){let t={};for(let n of a)t[n.id]=s(e.models[n.id]);E.value=t,D.value=!0,O=Date.now()}}catch{}finally{k=null}})(),k)}function M(e){let t=A(e);return t.status===`unknown`?``:t.status===`set`?t.info.name:r(`@42BB9:未在配置中指定`)}function N(e){let t=A(e);return t.status===`set`?[t.info.source===`state`?r(`@42BB9:CLI 内最近使用`):``,t.info.detail,t.info.provider].filter(Boolean).join(` · `):``}let P=null;function F(){P||=(w(),j(),setInterval(()=>{w(),j()},o))}function I(){P&&=(clearInterval(P),null)}return{vscodeAvailable:t,claudeAvailable:n,codexAvailable:c,opencodeAvailable:l,kimiAvailable:u,zcodeAvailable:d,dshAvailable:f,lastCheckedAt:p,isChecking:m,platform:h,installers:g,versions:_,latestVersions:v,isFetchingLatest:y,isToolAvailable:T,executorModels:E,executorModelsReady:D,executorModelState:A,executorModelText:M,executorModelDetail:N,fetchExecutorModels:j,checkTools:w,fetchLatestVersions:S,startPolling:F,stopPolling:I}});export{c as t};
1
+ import{br as e,pr as t}from"./element-plus-CgEuJBlB.js";import{S as n}from"./vendor-CDZ64WVU.js";import{n as r}from"./_plugin-vue_export-helper-D8rwCQpe.js";import{a as i,i as a}from"./configStore-BFGSfIBp.js";t();var o=6e5;function s(e){if(!e||typeof e!=`object`)return null;let t=e;return typeof t.name!=`string`||!t.name?null:{name:t.name,detail:typeof t.detail==`string`&&t.detail?t.detail:null,provider:typeof t.provider==`string`&&t.provider?t.provider:null,source:t.source===`config`||t.source===`state`?t.source:null}}var c=n(`tools`,()=>{let t=e(!1),n=e(!1),c=e(!1),l=e(!1),u=e(!1),d=e(!1),f=e(!1),p=e(null),m=e(!1),h=e(``),g=e({}),_=e({}),v=e({}),y=e(!1),b=0,x=null;function S(e=!1){return!e&&Date.now()-b<3e5?Promise.resolve():x||(y.value=!0,x=(async()=>{try{let e=await(await fetch(`/api/latest-tool-versions`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({})})).json();e.success&&e.latest&&typeof e.latest==`object`&&(v.value=e.latest,b=Date.now())}catch{}finally{y.value=!1,x=null}})(),x)}let C=null;function w(){return C||(m.value=!0,C=(async()=>{try{let e=await(await fetch(`/api/check-tools`)).json();e.success&&(t.value=!!e.vscode,n.value=!!e.claude,c.value=!!e.codex,l.value=!!e.opencode,u.value=!!e.kimi,d.value=!!e.zcode,f.value=!!e.dsh,h.value=typeof e.platform==`string`?e.platform:``,g.value=e.installers&&typeof e.installers==`object`?e.installers:{},_.value=e.versions&&typeof e.versions==`object`?e.versions:{},p.value=Date.now())}catch{}finally{m.value=!1,C=null}})(),C)}function T(e){switch(e){case`vscode`:return t.value;case`claude`:return n.value;case`codex`:return c.value;case`opencode`:return l.value;case`kimi`:return u.value;case`zcode`:return d.value;case`dsh`:return f.value}}let E=e({}),D=e(!1),O=0,k=null;function A(e){if(!D.value)return{status:`unknown`};if(!i(e))return{status:`unset`};let t=E.value[e];return t?{status:`set`,info:t}:{status:`unset`}}function j(e=!1){return!e&&D.value&&Date.now()-O<6e5?Promise.resolve():k||(k=(async()=>{try{let e=await(await fetch(`/api/workbench/executor-models`)).json();if(e?.success&&e.models&&typeof e.models==`object`){let t={};for(let n of a)t[n.id]=s(e.models[n.id]);E.value=t,D.value=!0,O=Date.now()}}catch{}finally{k=null}})(),k)}function M(e){let t=A(e);return t.status===`unknown`?``:t.status===`set`?t.info.name:r(`@42BB9:未在配置中指定`)}function N(e){let t=A(e);return t.status===`set`?[t.info.source===`state`?r(`@42BB9:CLI 内最近使用`):``,t.info.detail,t.info.provider].filter(Boolean).join(` · `):``}let P=null;function F(){P||=(w(),j(),setInterval(()=>{w(),j()},o))}function I(){P&&=(clearInterval(P),null)}return{vscodeAvailable:t,claudeAvailable:n,codexAvailable:c,opencodeAvailable:l,kimiAvailable:u,zcodeAvailable:d,dshAvailable:f,lastCheckedAt:p,isChecking:m,platform:h,installers:g,versions:_,latestVersions:v,isFetchingLatest:y,isToolAvailable:T,executorModels:E,executorModelsReady:D,executorModelState:A,executorModelText:M,executorModelDetail:N,fetchExecutorModels:j,checkTools:w,fetchLatestVersions:S,startPolling:F,stopPolling:I}});export{c as t};
@@ -10,26 +10,26 @@
10
10
  <meta name="theme-color" content="#141820" media="(prefers-color-scheme: dark)" />
11
11
  <title>Zen GitSync</title>
12
12
  <!-- 字体改为自托管(@font-face 见 src/main.css),不再依赖 Google Fonts 外链 -->
13
- <script type="module" crossorigin src="/assets/index-CEEQaAF4.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-aNSr4k87.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/dagre-Dfa6RS7s.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/element-plus-CgEuJBlB.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/vendor-CDZ64WVU.js">
18
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-MIPci_U9.js">
19
- <link rel="modulepreload" crossorigin href="/assets/SvgIcon-b50XyCO5.js">
20
- <link rel="modulepreload" crossorigin href="/assets/IconButton-BvbRjQ_D.js">
21
- <link rel="modulepreload" crossorigin href="/assets/configStore-BMixvVAA.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-D8rwCQpe.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/SvgIcon-B-2iTLW-.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/IconButton-BflzboUD.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/configStore-BFGSfIBp.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
23
- <link rel="modulepreload" crossorigin href="/assets/gitStore-DriJnutC.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/gitStore-BcrFZogC.js">
24
24
  <link rel="modulepreload" crossorigin href="/assets/fileIcon-B5p5pGN9.js">
25
25
  <link rel="modulepreload" crossorigin href="/assets/flow-mindmap-CzytSMsF.js">
26
- <link rel="modulepreload" crossorigin href="/assets/previewSandbox-D86LH88s.js">
26
+ <link rel="modulepreload" crossorigin href="/assets/previewSandbox-C_ZzCEpS.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/monaco-DF5LNTW7.js">
28
28
  <link rel="modulepreload" crossorigin href="/assets/useThemeObserver-DAyxqy9l.js">
29
- <link rel="modulepreload" crossorigin href="/assets/CommonDialog-UrQUmb6G.js">
30
- <link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-BlDGBxKd.js">
31
- <link rel="modulepreload" crossorigin href="/assets/toolsStore-B1Lx5gWu.js">
32
- <link rel="modulepreload" crossorigin href="/assets/TemplateManager-D5-gC33Z.js">
29
+ <link rel="modulepreload" crossorigin href="/assets/CommonDialog-kDck8ITZ.js">
30
+ <link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-BxFkqvIA.js">
31
+ <link rel="modulepreload" crossorigin href="/assets/toolsStore-B3LQrSEp.js">
32
+ <link rel="modulepreload" crossorigin href="/assets/TemplateManager-CYy3G7rH.js">
33
33
  <link rel="modulepreload" crossorigin href="/assets/vue-flow-tRJAB9jP.js">
34
34
  <link rel="stylesheet" crossorigin href="/assets/element-plus-Qexo5hiM.css">
35
35
  <link rel="stylesheet" crossorigin href="/assets/vendor-CP-WuGZG.css">
@@ -42,7 +42,7 @@
42
42
  <link rel="stylesheet" crossorigin href="/assets/FileDiffViewer-B6zy6NiA.css">
43
43
  <link rel="stylesheet" crossorigin href="/assets/TemplateManager-DiM3vrbf.css">
44
44
  <link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
45
- <link rel="stylesheet" crossorigin href="/assets/index-BrIl1iaG.css">
45
+ <link rel="stylesheet" crossorigin href="/assets/index-BCTkDpQv.css">
46
46
  </head>
47
47
  <body>
48
48
  <div id="app"></div>
@@ -138,6 +138,12 @@ import {
138
138
  buildRunningFacts,
139
139
  generateProgressReport,
140
140
  } from './progressReport.js';
141
+ import {
142
+ applyStallVerdict,
143
+ buildStallFact,
144
+ judgeStalledJob,
145
+ shouldCheckStall,
146
+ } from './stallWatchdog.js';
141
147
  import { detectExecutorModels, formatExecutorModel } from './executorModels.js';
142
148
 
143
149
  /**
@@ -150,6 +156,9 @@ let jobSettledHandler = null;
150
156
  /** 自动进度报告的定时器。同上:重复装配路由时先清掉上一个 */
151
157
  let reportTimer = null;
152
158
 
159
+ /** 静默看门狗的定时器(与报告同一个粒度,各自独立装配/清理) */
160
+ let stallTimer = null;
161
+
153
162
  /** 自动报告的检查粒度。它**不等于**报告间隔 —— 见 tickProgressReport 的注释 */
154
163
  const REPORT_TICK_MS = 30 * 1000;
155
164
 
@@ -599,6 +608,72 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
599
608
  });
600
609
  }));
601
610
 
611
+ // ════════════════════════════════════════════════════════════════════════
612
+ // §12.5 手动标记「已完成」/ 撤销这个标记
613
+ // POST /api/workbench/tasks/:id/done
614
+ // DELETE /api/workbench/tasks/:id/done
615
+ //
616
+ // 列本来是纯推导的(projectRegistry.deriveTaskColumn),但有两件事推不出来 ——
617
+ // 一条待在「待处理」里的任务其实早在别处干完了;一条「进行中」的任务模型已经不说话了、
618
+ // 用户比静默看门狗更早判断它干完了。这两件事只有人知道,所以给一个人工落点:
619
+ // 写 task.manualDoneAt,列由它推导(判据与失效条件都在 deriveTaskColumn 的注释里)。
620
+ //
621
+ // 为什么标记写在 task 上、而不是补一条 job:job 是「这轮执行发生了什么」的事实记录,
622
+ // 混进人工判断之后,"跑了多久 / 谁跑的 / 输出了什么"就再也分不清真假了。
623
+ // ════════════════════════════════════════════════════════════════════════
624
+
625
+ app.post('/api/workbench/tasks/:id/done', asyncRoute(async (req, res) => {
626
+ const taskId = req.params.id;
627
+ const data = await readJson(TASKS_FILE, { tasks: [] });
628
+ const task = (data.tasks || []).find(t => t && t.id === taskId);
629
+ if (!task) throw new HttpError(404, '任务不存在');
630
+
631
+ // 标「已完成」的同时把这一轮停掉:一张卡片不能既是"已完成"又还在跑
632
+ // (列的口径是"在跑 ⇔ 进行中",不先停就会当场翻回去,用户以为按钮坏了)。
633
+ // 先刷磁盘再取快照 —— 别的实例刚起的 job 也要看得到(与 /tasks/:id/run 同一口径)。
634
+ await refreshJobsFromDisk();
635
+ const liveJobs = [];
636
+ for (const j of mergedJobs().values()) {
637
+ if (!j || j.taskId !== taskId) continue;
638
+ if (j.status !== 'running' && j.status !== 'pending') continue;
639
+ const local = jobs.get(j.id);
640
+ // child 句柄只活在跑它的那个进程里,别的实例起的 job 这里停不掉 ——
641
+ // 那就别假装标成功。这句话与 §14 的取消路由**逐字一致**:
642
+ // 同一个事实在两个入口有两种说法,用户会以为是两件事。
643
+ if (!local) throw new HttpError(404, '这个任务正在另一个 g ui 实例里执行,请到那个窗口停止它');
644
+ liveJobs.push(local);
645
+ }
646
+ for (const j of liveJobs) {
647
+ try {
648
+ cancelRunningJob(j);
649
+ } catch (err) {
650
+ throw new HttpError(500, '停止这一轮执行失败: ' + err.message);
651
+ }
652
+ }
653
+
654
+ const now = nowIso();
655
+ task.manualDoneAt = now;
656
+ task.updatedAt = now;
657
+ await writeJson(TASKS_FILE, data);
658
+ publish('task:update', task);
659
+ res.json({ success: true, task, stoppedJobs: liveJobs.length });
660
+ }));
661
+
662
+ app.delete('/api/workbench/tasks/:id/done', asyncRoute(async (req, res) => {
663
+ const data = await readJson(TASKS_FILE, { tasks: [] });
664
+ const task = (data.tasks || []).find(t => t && t.id === req.params.id);
665
+ if (!task) throw new HttpError(404, '任务不存在');
666
+ // 幂等:没标过也回成功。撤销是"点错了要退回来"的动作,为它再弹一个错误没有意义。
667
+ // 撤销之后回到哪一列由执行事实说了算 —— 最近一条 job 是跑完的,它本来就该在「已完成」。
668
+ if (task.manualDoneAt) {
669
+ delete task.manualDoneAt;
670
+ task.updatedAt = nowIso();
671
+ await writeJson(TASKS_FILE, data);
672
+ publish('task:update', task);
673
+ }
674
+ res.json({ success: true, task });
675
+ }));
676
+
602
677
  // ════════════════════════════════════════════════════════════════════════
603
678
  // §13. Job 查询(兜底,SSE 断了也能拉)
604
679
  // ════════════════════════════════════════════════════════════════════════
@@ -612,20 +687,21 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
612
687
  // ════════════════════════════════════════════════════════════════════════
613
688
  // §14. 取消正在执行的 job
614
689
  // ════════════════════════════════════════════════════════════════════════
615
- app.post('/api/workbench/jobs/:id/cancel', asyncRoute(async (req, res) => {
616
- const job = jobs.get(req.params.id);
617
- if (!job) {
618
- // 看板现在能看到**别的 g ui 实例**正在跑的 job(执行记录合并了),但 child 句柄
619
- // 只活在跑它的那个进程里 —— 这里停不了。给个说得清的理由,别让用户对着
620
- // "job 不存在"发懵(他明明在卡片上看着它转)。
621
- await refreshJobsFromDisk();
622
- throw new HttpError(404, mergedJobs().has(req.params.id)
623
- ? '这个任务正在另一个 g ui 实例里执行,请到那个窗口停止它'
624
- : 'job 不存在');
625
- }
626
- if (job.status !== 'running' && job.status !== 'pending') {
627
- throw new HttpError(400, `当前状态 ${job.status} 不可取消`);
628
- }
690
+
691
+ /**
692
+ * 停掉**本进程**里正在跑的一个 job:先落终态 + 广播,再杀进程树。
693
+ *
694
+ * 抽出来是因为 §12.5 的「手动标记完成」也要用它(标完成时这一轮必须先停下来)——
695
+ * 两处各写一份杀进程的逻辑,早晚会分叉成"一个入口停得掉、另一个停不干净",
696
+ * 而且分叉了不报错,只有用户能看见:一张卡片停在「进行中」不动。
697
+ *
698
+ * **调用方负责判断这个 job 归不归本进程管**:child 句柄只活在跑它的那个进程里,
699
+ * 别的 g ui 实例起的 job 在这里只有一条磁盘记录(§14 的路由为这种情况单独给了 404
700
+ * 与一句明确的话;§12.5 则直接拒绝落标记)。
701
+ *
702
+ * @returns {boolean} 是否真的发了信号(false = 只是改了状态,进程句柄不在手上)
703
+ */
704
+ function cancelRunningJob(job) {
629
705
  cancelledJobs.add(job.id);
630
706
  // 立即给前端一个状态反馈(不等 child 真正退出)
631
707
  job.status = 'cancelled';
@@ -634,10 +710,9 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
634
710
  publish('job:update', { ...job }); // 浅拷贝避免序列化 child 引用
635
711
  // 终态:fire-and-forget 同步落盘
636
712
  flushJobsSaveNow().catch(err => logger.warn('[workbench] jobs save failed:', err.message));
713
+
637
714
  const child = job.child;
638
- if (!child) {
639
- return res.json({ success: true, message: '已标记取消,进程将尽快结束' });
640
- }
715
+ if (!child) return false;
641
716
  try {
642
717
  if (process.platform === 'win32') {
643
718
  // Windows: child.kill(SIGTERM) 经常无效,用 taskkill 杀进程树
@@ -649,11 +724,37 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
649
724
  } else {
650
725
  child.kill('SIGTERM');
651
726
  }
652
- res.json({ success: true, message: '已发送停止信号' });
727
+ return true;
653
728
  } catch (err) {
654
729
  cancelledJobs.delete(job.id);
730
+ throw err;
731
+ }
732
+ }
733
+
734
+ app.post('/api/workbench/jobs/:id/cancel', asyncRoute(async (req, res) => {
735
+ const job = jobs.get(req.params.id);
736
+ if (!job) {
737
+ // 看板现在能看到**别的 g ui 实例**正在跑的 job(执行记录合并了),但 child 句柄
738
+ // 只活在跑它的那个进程里 —— 这里停不了。给个说得清的理由,别让用户对着
739
+ // "job 不存在"发懵(他明明在卡片上看着它转)。
740
+ await refreshJobsFromDisk();
741
+ throw new HttpError(404, mergedJobs().has(req.params.id)
742
+ ? '这个任务正在另一个 g ui 实例里执行,请到那个窗口停止它'
743
+ : 'job 不存在');
744
+ }
745
+ if (job.status !== 'running' && job.status !== 'pending') {
746
+ throw new HttpError(400, `当前状态 ${job.status} 不可取消`);
747
+ }
748
+ let signalled;
749
+ try {
750
+ signalled = cancelRunningJob(job);
751
+ } catch (err) {
655
752
  throw new HttpError(500, '发送停止信号失败: ' + err.message);
656
753
  }
754
+ res.json({
755
+ success: true,
756
+ message: signalled ? '已发送停止信号' : '已标记取消,进程将尽快结束'
757
+ });
657
758
  }));
658
759
 
659
760
  // ════════════════════════════════════════════════════════════════════════
@@ -1483,6 +1584,69 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
1483
1584
  // 定时器不该把进程吊着不退出
1484
1585
  reportTimer.unref?.();
1485
1586
 
1587
+ /**
1588
+ * 静默看门狗的一跳:把**本进程**跑着的、已经静默够久的 job 挨个判一次,
1589
+ * 判成"已经做完了"就把这条执行落成终态(卡片随即从「进行中」挪到「已完成」)。
1590
+ *
1591
+ * 与自动报告的三点不同,都是有意的:
1592
+ * · **不吃报告开关**:报告间隔是"隔多久汇报一次"的偏好,不是"允不允许后台打模型"
1593
+ * 的总闸。用户关掉报告之后,卡了 54 分钟的任务仍然该被收掉。
1594
+ * · **只判本进程的 job**:别人的 job 只在 live-jobs 缓存里,终态由它的 owner 落盘 ——
1595
+ * 从这边改会被下一轮刷新盖回去(见 stallWatchdog 文件头的第 4 条)。
1596
+ * · **串行判**:一次模型往返几秒到几十秒,同时跑好几条就是同时打十几次模型。
1597
+ * 排队判的代价只是排在后面的那几条晚几十秒,而它们已经静默十分钟了。
1598
+ *
1599
+ * 判定的准入条件(该不该判)全在 stallWatchdog.shouldCheckStall 里,这里只负责
1600
+ * "把模型的话落到 job 上",免得同一套门槛出现两份。
1601
+ */
1602
+ let stallTickInFlight = false;
1603
+ async function tickStallWatchdog() {
1604
+ if (stallTickInFlight) return;
1605
+ stallTickInFlight = true;
1606
+ try {
1607
+ // 每判完一条都重新取一次时间:判定本身要花几秒,用 tick 开始时的 now
1608
+ // 会让"刚刚已经恢复产出"的任务在第二次静默检查里蒙混过关
1609
+ const pending = Array.from(jobs.values()).filter(j => shouldCheckStall(j, Date.now()));
1610
+ if (!pending.length) return;
1611
+
1612
+ const data = await readJson(TASKS_FILE, { tasks: [] });
1613
+ const taskMap = new Map((data.tasks || []).filter(t => t && t.id).map(t => [t.id, t]));
1614
+ const ctx = await readReportContext();
1615
+
1616
+ for (const job of pending) {
1617
+ if (!shouldCheckStall(job, Date.now())) continue;
1618
+ const fact = buildStallFact(job, taskMap.get(job.taskId), Date.now());
1619
+ const verdict = await judgeStalledJob({ fact, locale: ctx.locale, model: ctx.model });
1620
+ const settled = applyStallVerdict(job, { at: nowIso(), verdict });
1621
+ if (settled) {
1622
+ // 先告诉前端(SSE)再落盘:卡片该立刻动,而不是等下一次 5s 轮询
1623
+ publish('job:update', job);
1624
+ logger.info(`[workbench] 静默看门狗判定任务已完成,自动收尾 (job=${job.id}): ${job.autoCompleted.reason || '(模型未给依据)'}`);
1625
+ try {
1626
+ await flushJobsSaveNow();
1627
+ } catch (err) {
1628
+ logger.warn('[workbench] 自动收尾落盘失败:', (err && err.message) || err);
1629
+ }
1630
+ } else if (verdict.errorCode) {
1631
+ logger.info(`[workbench] 静默判定未采纳 (job=${job.id}): ${verdict.errorCode} ${verdict.errorDetail}`.trim());
1632
+ } else if (verdict.done) {
1633
+ // 判成完成但没落成终态 —— 判定期间它又动了(见 applyStallVerdict)
1634
+ logger.info(`[workbench] 静默判定为已完成,但期间任务又有产出,本次不改状态 (job=${job.id})`);
1635
+ }
1636
+ }
1637
+ } finally {
1638
+ stallTickInFlight = false;
1639
+ }
1640
+ }
1641
+
1642
+ if (stallTimer) { clearInterval(stallTimer); stallTimer = null; }
1643
+ stallTimer = setInterval(() => {
1644
+ tickStallWatchdog().catch(err => {
1645
+ logger.warn('[workbench] 静默看门狗失败:', (err && err.message) || err);
1646
+ });
1647
+ }, REPORT_TICK_MS);
1648
+ stallTimer.unref?.();
1649
+
1486
1650
  app.post('/api/workbench/orchestrator/state', asyncRoute(async (req, res) => {
1487
1651
  const active = req.body?.active;
1488
1652
  if (typeof active !== 'boolean') throw new HttpError(400, 'active 必须是布尔值');
@@ -167,15 +167,27 @@ export function describeToolMix(toolCalls) {
167
167
  }
168
168
 
169
169
  /**
170
- * 距最后一次产出(正文 / 思考 / 工具调用)多久。
171
- * 没有 `lastActivityAt`(老记录、别实例上的旧版进程)或还没到阈值 → null,
172
- * 调用方据此决定"这条不显示",而不是显示一个假的 0。
170
+ * 距最后一次产出(正文 / 思考 / 工具调用)的**原始**毫秒数,不设阈值。
171
+ *
172
+ * 两个调用方要的是不同的问题:卡片/报告问的是"静默到值得说出来了吗"(用下面的
173
+ * silentMsOf,60s 起),静默看门狗问的是"静默到该判一次了吗"(10 分钟起)。
174
+ * 阈值各留各的,但**计时口径必须只有一份** —— 各写一份 `now - lastActivityAt`
175
+ * 迟早会在"取不到时间戳算什么"上分叉(这里定了:null,不拿 startedAt 兜底,
176
+ * 也不给假的 0)。没有 `lastActivityAt`(老记录、别实例上的旧版进程)→ null。
173
177
  */
174
- export function silentMsOf(job, now) {
178
+ export function silentMsRaw(job, now) {
175
179
  const last = Date.parse((job && job.lastActivityAt) || '');
176
180
  if (!Number.isFinite(last)) return null;
177
- const ms = Math.max(0, now - last);
178
- return ms >= SILENT_NOTABLE_MS ? ms : null;
181
+ return Math.max(0, now - last);
182
+ }
183
+
184
+ /**
185
+ * 静默到**值得写进事实**的毫秒数(不到阈值就是 null)。
186
+ * 调用方据此决定"这条不显示",而不是显示一个假的 0。
187
+ */
188
+ export function silentMsOf(job, now) {
189
+ const ms = silentMsRaw(job, now);
190
+ return ms !== null && ms >= SILENT_NOTABLE_MS ? ms : null;
179
191
  }
180
192
 
181
193
  /** job 是否算"正在跑"(看板卡片只在此时显示活动摘要) */
@@ -100,7 +100,13 @@ export function projectJob(j) {
100
100
  // 前端靠它画工具块;刷新页面 / 换实例后仍然可见,所以必须过白名单。
101
101
  toolCalls: Array.isArray(j.toolCalls) ? j.toolCalls : [],
102
102
  // 续接对话用:claude --output-format stream-json 的 system.init 事件捕获到的 session_id
103
- claudeSessionId: j.claudeSessionId || null
103
+ claudeSessionId: j.claudeSessionId || null,
104
+ /**
105
+ * 「这条是我判成完成的」(静默看门狗写的,见 stallWatchdog.js)。`{at, reason, silentMs}`,
106
+ * 没判过就是 null。**必须过白名单**:看板卡片靠它显示"为什么它自己跳到已完成列了"——
107
+ * 没有这个字段,用户看到的就是一条任务不声不响换了列,模型给的依据也无处可看。
108
+ */
109
+ autoCompleted: (j.autoCompleted && typeof j.autoCompleted === 'object') ? j.autoCompleted : null
104
110
  };
105
111
  }
106
112
 
@@ -30,8 +30,16 @@ import { buildAiChatRequest, describeAiHttpError } from '../../../../utils/aiEnd
30
30
  //
31
31
  // 图片支持:opts.images 是 data URL 数组(如 'data:image/png;base64,...')。
32
32
  // 非多模态模型遇到 image_url 会忽略图片块,相当于退化成纯文本,不会报错。
33
+ //
34
+ // opts.maxTokens / opts.systemPrompt 是 2026-10-01 为静默看门狗补的(此前这个函数
35
+ // 既不带 system,也不限制输出长度)。两个都是**可选**:不传就与改前逐字一致,
36
+ // 现有调用点(targetResolver / aiContext 摘要 / 提示词生成)不受影响。
37
+ // · maxTokens:推理模型的 max_tokens 是"思考 + 正文"共用的,不设的话就等于把
38
+ // 上限交给网关默认值 —— 判定这种"只要一个布尔值"的调用不值得赌那个默认值。
39
+ // · systemPrompt:把判定口径(不可信数据 / 宁可不判)与用户消息分开,
40
+ // 免得跟任务正文混在同一段里被当成数据的一部分。
33
41
  export async function callLlmJson(model, prompt, opts = {}) {
34
- const { timeoutMs = 60000, images = [] } = opts;
42
+ const { timeoutMs = 60000, images = [], maxTokens, systemPrompt } = opts;
35
43
  const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
36
44
  const { url, headers } = buildAiChatRequest({
37
45
  baseURL: model.baseURL,
@@ -49,13 +57,21 @@ export async function callLlmJson(model, prompt, opts = {}) {
49
57
  userContent = prompt;
50
58
  }
51
59
 
52
- const body = JSON.stringify({
60
+ const messages = [];
61
+ if (systemPrompt) messages.push({ role: 'system', content: String(systemPrompt) });
62
+ messages.push({ role: 'user', content: userContent });
63
+
64
+ const payload = {
53
65
  model: model.model,
54
- messages: [{ role: 'user', content: userContent }],
66
+ messages,
55
67
  temperature: 0.4,
56
68
  response_format: { type: 'json_object' },
57
69
  stream: false,
58
- });
70
+ };
71
+ // 只在调用方真给了正整数时才写进 body:传 undefined 会序列化成 `"max_tokens": null`,
72
+ // 有的网关把它当 0 处理 → 直接返回空正文
73
+ if (Number.isFinite(maxTokens) && maxTokens > 0) payload.max_tokens = Math.floor(maxTokens);
74
+ const body = JSON.stringify(payload);
59
75
 
60
76
  const controller = new AbortController();
61
77
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -108,7 +124,14 @@ export function cloneMsgContent(c) {
108
124
  * - reasoning_content / reasoning:部分模型(如 deepseek)放在 delta.reasoning_content
109
125
  * - reasoning / reasoning_text:openai o1 风格
110
126
  * - content:普通输出
111
- * 返回 { content: string, aborted: boolean }。
127
+ * 返回 { content, aborted, finishReason, usage }。
128
+ *
129
+ * finishReason / usage 是 2026-09-30 为进度报告补的:**推理模型的 max_tokens 是
130
+ * "思考 + 正文"共用的**,思考写满预算时正文一个字都没有,返回的却是正常的 HTTP 200。
131
+ * 上层只看到"content 为空",分不清是"被截断"还是"模型就是不说话" ——
132
+ * 截断(finish_reason='length')→ 调大预算重试就能拿到;
133
+ * 不吭声(finish_reason='stop')→ 重试没用。
134
+ * 值直接透传 provider 的原话;不支持 usage 的网关给 null,两个字段都只是"有则更好"。
112
135
  *
113
136
  * input 支持两种形态(判别 union):
114
137
  * - string: 旧模式,拼成单条 user message,支持 opts.images 多模态
@@ -169,6 +192,9 @@ export async function callLlmStream(model, input, onDelta, opts = {}) {
169
192
 
170
193
  let fullContent = '';
171
194
  let aborted = false;
195
+ // 结束原因与用量:见函数头。`stop` / `length` 是判"被预算截断"的唯一依据
196
+ let finishReason = '';
197
+ let usage = null;
172
198
  try {
173
199
  const resp = await fetch(url, { method: 'POST', headers, body, signal: controller.signal });
174
200
  if (!resp.ok || !resp.body) {
@@ -203,6 +229,11 @@ export async function callLlmStream(model, input, onDelta, opts = {}) {
203
229
  fullContent += contentChunk;
204
230
  onDelta({ content: contentChunk });
205
231
  }
232
+ // finish_reason 只在最后一帧出现;usage 多数网关放在最后一帧(有的放在
233
+ // 一个 choices 为空的独立帧里),两处都收 —— 收到哪个算哪个
234
+ const choice = evt.choices?.[0];
235
+ if (choice && choice.finish_reason) finishReason = String(choice.finish_reason);
236
+ if (evt.usage) usage = evt.usage;
206
237
  } catch { /* 跳过无法解析的行 */ }
207
238
  }
208
239
  }
@@ -217,5 +248,5 @@ export async function callLlmStream(model, input, onDelta, opts = {}) {
217
248
  clearTimeout(timer);
218
249
  if (signal) signal.removeEventListener('abort', onAbort);
219
250
  }
220
- return { content: fullContent, aborted };
251
+ return { content: fullContent, aborted, finishReason, usage };
221
252
  }