zen-gitsync 2.17.31 → 2.17.32

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 (42) hide show
  1. package/README.md +6 -6
  2. package/package.json +3 -1
  3. package/scripts/release.js +66 -29
  4. package/src/ui/public/assets/{AgentEngineSelector-bQmJlH1b.js → AgentEngineSelector-CPS9WawU.js} +1 -1
  5. package/src/ui/public/assets/{AgentView-DpNj6VFj.js → AgentView-CHj8Esf5.js} +1 -1
  6. package/src/ui/public/assets/{AppVersionBadge-2etZBF2E.js → AppVersionBadge-CbYQ0KFm.js} +2 -2
  7. package/src/ui/public/assets/{BranchSelector-BvbMlZW6.js → BranchSelector-i_DZoGr4.js} +1 -1
  8. package/src/ui/public/assets/{CommitForm-Bp41Q0Oo.js → CommitForm-DrAhqXPt.js} +1 -1
  9. package/src/ui/public/assets/{CommonDialog-UrQUmb6G.js → CommonDialog-Nwwi4-_9.js} +1 -1
  10. package/src/ui/public/assets/{EditorView-DIu2cIai.js → EditorView-Cj7r5oIY.js} +1 -1
  11. package/src/ui/public/assets/{FileDiffViewer-BlDGBxKd.js → FileDiffViewer-2vvvRZ_R.js} +1 -1
  12. package/src/ui/public/assets/{FlowExecutionViewer-MPq4Fh6G.js → FlowExecutionViewer-STZphigj.js} +1 -1
  13. package/src/ui/public/assets/{FlowOrchestrationWorkspace-ByBJ4B3G.js → FlowOrchestrationWorkspace-BdW1MijZ.js} +1 -1
  14. package/src/ui/public/assets/{IconButton-BvbRjQ_D.js → IconButton-BuiJBsJ3.js} +1 -1
  15. package/src/ui/public/assets/{LogList-Xs52JPC1.js → LogList-C14b7EUJ.js} +1 -1
  16. package/src/ui/public/assets/{MindmapView-Br7Fj7Ga.js → MindmapView-dcm1kcrO.js} +1 -1
  17. package/src/ui/public/assets/{MonitorView-DSV5_Q-6.js → MonitorView-7SOXc-I2.js} +1 -1
  18. package/src/ui/public/assets/{ProjectStartupButton-BCS2YQdX.js → ProjectStartupButton-BM2RrBbL.js} +1 -1
  19. package/src/ui/public/assets/{RecentDirectoriesChat-Dv47YPKL.js → RecentDirectoriesChat-CXxGKkqI.js} +1 -1
  20. package/src/ui/public/assets/{RemoteManagerDialog-CIHTYzr7.js → RemoteManagerDialog-BTtVeges.js} +1 -1
  21. package/src/ui/public/assets/{RemoteRepoCard-DCYTEmuq.js → RemoteRepoCard-B0n8k5Go.js} +1 -1
  22. package/src/ui/public/assets/{SourceMapView-BANSmwrh.js → SourceMapView-CT2C3tAc.js} +1 -1
  23. package/src/ui/public/assets/{SvgIcon-b50XyCO5.js → SvgIcon-U-AI95UV.js} +1 -1
  24. package/src/ui/public/assets/{TemplateManager-D5-gC33Z.js → TemplateManager-BIMeoTp0.js} +1 -1
  25. package/src/ui/public/assets/{UserInputNode-BFmTQYZP.js → UserInputNode-D_V8dhh9.js} +1 -1
  26. package/src/ui/public/assets/WorkbenchView-BeC5sg_V.css +1 -0
  27. package/src/ui/public/assets/WorkbenchView-C74ciC03.js +20 -0
  28. package/src/ui/public/assets/{_plugin-vue_export-helper-MIPci_U9.js → _plugin-vue_export-helper-DkeTtFKF.js} +2 -2
  29. package/src/ui/public/assets/{agentConversations-D8mdZaNY.js → agentConversations-B2aTliJe.js} +1 -1
  30. package/src/ui/public/assets/{configStore-BMixvVAA.js → configStore-Y1biBqmb.js} +1 -1
  31. package/src/ui/public/assets/{gitStore-DriJnutC.js → gitStore-CiBz68_Z.js} +1 -1
  32. package/src/ui/public/assets/{index-CEEQaAF4.js → index-BoZNUwmd.js} +5 -5
  33. package/src/ui/public/assets/{previewSandbox-D86LH88s.js → previewSandbox-D7HcNin7.js} +1 -1
  34. package/src/ui/public/assets/{toolsStore-B1Lx5gWu.js → toolsStore-Dhfr-VDL.js} +1 -1
  35. package/src/ui/public/index.html +11 -11
  36. package/src/ui/server/routes/workbench/llmClient.js +17 -2
  37. package/src/ui/server/routes/workbench/progressReport.js +135 -50
  38. package/src/ui/server/routes/workbench/progressReport.test.js +101 -4
  39. package/src/utils/desktopNotify.js +199 -41
  40. package/src/utils/desktopNotify.test.js +84 -29
  41. package/src/ui/public/assets/WorkbenchView-Dc7bMMfe.css +0 -1
  42. package/src/ui/public/assets/WorkbenchView-DsPII7uQ.js +0 -20
@@ -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-DkeTtFKF.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-DkeTtFKF.js";import{a as i,i as a}from"./configStore-Y1biBqmb.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-BoZNUwmd.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-DkeTtFKF.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/SvgIcon-U-AI95UV.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/IconButton-BuiJBsJ3.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/configStore-Y1biBqmb.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-CiBz68_Z.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-D7HcNin7.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-Nwwi4-_9.js">
30
+ <link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-2vvvRZ_R.js">
31
+ <link rel="modulepreload" crossorigin href="/assets/toolsStore-Dhfr-VDL.js">
32
+ <link rel="modulepreload" crossorigin href="/assets/TemplateManager-BIMeoTp0.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">
@@ -108,7 +108,14 @@ export function cloneMsgContent(c) {
108
108
  * - reasoning_content / reasoning:部分模型(如 deepseek)放在 delta.reasoning_content
109
109
  * - reasoning / reasoning_text:openai o1 风格
110
110
  * - content:普通输出
111
- * 返回 { content: string, aborted: boolean }。
111
+ * 返回 { content, aborted, finishReason, usage }。
112
+ *
113
+ * finishReason / usage 是 2026-09-30 为进度报告补的:**推理模型的 max_tokens 是
114
+ * "思考 + 正文"共用的**,思考写满预算时正文一个字都没有,返回的却是正常的 HTTP 200。
115
+ * 上层只看到"content 为空",分不清是"被截断"还是"模型就是不说话" ——
116
+ * 截断(finish_reason='length')→ 调大预算重试就能拿到;
117
+ * 不吭声(finish_reason='stop')→ 重试没用。
118
+ * 值直接透传 provider 的原话;不支持 usage 的网关给 null,两个字段都只是"有则更好"。
112
119
  *
113
120
  * input 支持两种形态(判别 union):
114
121
  * - string: 旧模式,拼成单条 user message,支持 opts.images 多模态
@@ -169,6 +176,9 @@ export async function callLlmStream(model, input, onDelta, opts = {}) {
169
176
 
170
177
  let fullContent = '';
171
178
  let aborted = false;
179
+ // 结束原因与用量:见函数头。`stop` / `length` 是判"被预算截断"的唯一依据
180
+ let finishReason = '';
181
+ let usage = null;
172
182
  try {
173
183
  const resp = await fetch(url, { method: 'POST', headers, body, signal: controller.signal });
174
184
  if (!resp.ok || !resp.body) {
@@ -203,6 +213,11 @@ export async function callLlmStream(model, input, onDelta, opts = {}) {
203
213
  fullContent += contentChunk;
204
214
  onDelta({ content: contentChunk });
205
215
  }
216
+ // finish_reason 只在最后一帧出现;usage 多数网关放在最后一帧(有的放在
217
+ // 一个 choices 为空的独立帧里),两处都收 —— 收到哪个算哪个
218
+ const choice = evt.choices?.[0];
219
+ if (choice && choice.finish_reason) finishReason = String(choice.finish_reason);
220
+ if (evt.usage) usage = evt.usage;
206
221
  } catch { /* 跳过无法解析的行 */ }
207
222
  }
208
223
  }
@@ -217,5 +232,5 @@ export async function callLlmStream(model, input, onDelta, opts = {}) {
217
232
  clearTimeout(timer);
218
233
  if (signal) signal.removeEventListener('abort', onAbort);
219
234
  }
220
- return { content: fullContent, aborted };
235
+ return { content: fullContent, aborted, finishReason, usage };
221
236
  }
@@ -47,6 +47,17 @@
47
47
  // 界面不画那条进度条:宁可什么都不画,也不画一条我们自己编出来的进度。
48
48
  // · 解析fail-safe:认不出来的标记行**留在正文里**(不吞内容),只把真认下来的那两行
49
49
  // 从正文里摘掉。老模型 / 不听话的模型不过是少一条进度条,报告本身照常。
50
+ //
51
+ // 2026-09-30 晚修的「有时候生成失败」(用户报的,历史里三条连着空报告):
52
+ // · 失败形态:errorCode=LLM_FAILED、errorDetail **空**、text 空 —— 界面上只有一句
53
+ // "生成失败,只记录了任务事实",盘上查不出任何原因。
54
+ // · 根因:`max_tokens` 是**思考 + 正文共用**的,而 900 的预算被推理模型的思考写满了。
55
+ // 用本机默认模型(deepseek-v4.1-flash)复现:同样一份 prompt,一轮思考 2708 字符、
56
+ // content 0 字节,HTTP 却是 200 —— 前端看到的"有时候",就是模型这次想得多不多。
57
+ // · 三处改动:预算 900 → 3000(正文只要 200~400,其余是思考的余量);
58
+ // 被截断且正文为空时**加预算重试一次**(第二种预算见 MAX_REPORT_TOKENS_RETRY);
59
+ // 两轮都空记成 `LLM_EMPTY`,并把 finish_reason / token 用量写进 errorDetail,
60
+ // 下次不用再翻网关日志。
50
61
 
51
62
  import { callLlmStream } from './llmClient.js';
52
63
  import { createThinkFilter } from '../../../../cli/ai/streamFilter.js';
@@ -68,8 +79,23 @@ export const MAX_FACT_TASKS = 8;
68
79
  const MAX_TITLE_CHARS = 140;
69
80
  const MAX_REPORT_CHARS = 2000;
70
81
  const MAX_ERROR_CHARS = 300;
71
- /** 一次汇报只有几句话,给足余量(部分模型会把预算用在 thinking 上) */
72
- const MAX_REPORT_TOKENS = 900;
82
+ /**
83
+ * 生成预算。**注意这是"思考 + 正文"共用的**:推理模型(本机默认的
84
+ * deepseek-v4.1-flash 就是)在写正文之前会先想一遍,思考的 token 也从这里扣。
85
+ *
86
+ * 2026-09-30 从 900 提到 3000:900 的时候观察到的失败形态是**思考把预算写满、
87
+ * 正文一个字都没有**,HTTP 200、`finish_reason='length'`、content 空 —— 界面上
88
+ * 就是"生成失败,只记录了任务事实"(那三条失败报告全是这个,见下面的重试)。
89
+ * 正文本身只要 200~400(220 字的汇报),剩下的全是留给思考的余量。
90
+ */
91
+ export const MAX_REPORT_TOKENS = 3000;
92
+
93
+ /**
94
+ * 还是被截断时的重试预算。只在"第一次被截断且正文为空"时用一次 ——
95
+ * 思考的长度跟任务数量、日志长度一起涨,3000 也不是保险箱(实测 4 个任务时
96
+ * 思考已到 2708 字符),而重试的代价只是一次已经失败掉的调用。
97
+ */
98
+ export const MAX_REPORT_TOKENS_RETRY = 8000;
73
99
  /** 生成超时。它跑在服务端后台,卡住不返回会一直占着"正在生成"这个位子 */
74
100
  const REPORT_TIMEOUT_MS = 90000;
75
101
 
@@ -358,6 +384,30 @@ Then write **one English progress report**:
358
384
  5. Output only the report itself`;
359
385
  }
360
386
 
387
+ /**
388
+ * 一次调用的元信息摘要 → 写进 errorDetail(界面上悬停可见的那个 tooltip)。
389
+ *
390
+ * 为什么值得单独写一行:界面上"生成失败"三个字背后是几种完全不同的处理办法,
391
+ * 而它们在盘上的记录里**长得一模一样**。2026-09-30 那三条失败报告就是
392
+ * `errorCode=LLM_FAILED` + `errorDetail=''` + `text=''`,光看 orchestrator-reports.json
393
+ * 什么都查不出来(得去翻网关日志才知道是被 max_tokens 截断)。
394
+ * 摘要里放 finish_reason 与 token 用量,下次一眼能分清"被预算截断"和"网关没回话"。
395
+ *
396
+ * 认不出来的字段一律不写(老网关不给 usage),整串截到 MAX_ERROR_CHARS。
397
+ */
398
+ export function describeCallResult(res, maxTokens) {
399
+ const r = res && typeof res === 'object' ? res : {};
400
+ const parts = [`max_tokens=${Number(maxTokens) || 0}`];
401
+ parts.push(`finish_reason=${r.finishReason || 'unknown'}`);
402
+ const u = r.usage && typeof r.usage === 'object' ? r.usage : null;
403
+ if (u && u.completion_tokens != null) parts.push(`completion_tokens=${u.completion_tokens}`);
404
+ // reasoning_tokens 是"预算里有多少花在思考上"的唯一直接证据
405
+ const detail = u && u.completion_tokens_details;
406
+ if (detail && detail.reasoning_tokens != null) parts.push(`reasoning_tokens=${detail.reasoning_tokens}`);
407
+ if (r.aborted) parts.push('aborted');
408
+ return parts.join(' ').slice(0, MAX_ERROR_CHARS);
409
+ }
410
+
361
411
  /**
362
412
  * 生成一份进度报告。**不落盘**(落盘由 orchestratorStore.appendReport 负责),
363
413
  * 返回的就是要存的那个记录。
@@ -365,6 +415,7 @@ Then write **one English progress report**:
365
415
  * 三种"没有正文"的情况都记成一条**带事实的报告**而不是抛错,理由是一样:
366
416
  * 面板上"有 2 个任务在跑,但没配模型"远比一个空白面板有用,
367
417
  * 而且失败会留在历史里 —— 用户能看出"是模型挂了"而不是"这个功能没做"。
418
+ * (第四种,"模型答了但没写正文",记成 LLM_EMPTY —— 成因与处理办法都不同。)
368
419
  *
369
420
  * @param {object} input
370
421
  * @param {object[]} input.facts buildRunningFacts 的输出
@@ -403,58 +454,92 @@ export async function generateProgressReport({
403
454
 
404
455
  const zh = !String(locale || '').startsWith('en');
405
456
  const prompt = buildReportPrompt(facts, locale);
406
- const controller = new AbortController();
407
- const timer = setTimeout(() => controller.abort(), REPORT_TIMEOUT_MS);
408
-
409
- // 部分模型把推理也塞进 content(与 recentDirectoriesAiSummary 同一处理):
410
- // 汇报里混进一段思考过程,用户会以为那是结论。
411
- const filter = createThinkFilter();
412
- let text = '';
413
- const forward = (segments) => {
414
- for (const seg of segments) if (seg.content) text += seg.content;
415
- };
416
457
 
417
- try {
418
- const { aborted } = await callStream(
419
- model,
420
- prompt,
421
- (delta) => { if (delta.content) forward(filter.feed(delta.content)); },
422
- {
423
- maxTokens: MAX_REPORT_TOKENS,
424
- systemPrompt: zh ? SYSTEM_PROMPT_ZH : SYSTEM_PROMPT_EN,
425
- signal: controller.signal,
426
- }
427
- );
428
- forward(filter.flush());
429
- if (aborted) return { ...base, errorCode: 'LLM_TIMEOUT' };
430
- } catch (err) {
431
- return {
432
- ...base,
433
- errorCode: 'LLM_FAILED',
434
- errorDetail: String((err && err.message) || err).slice(0, MAX_ERROR_CHARS),
458
+ /**
459
+ * 跑一次模型。每次调用**自带** AbortController 与过滤器:重试必须是干净的一次,
460
+ * 上一轮的超时定时器和半截 think 状态都不能带过来。
461
+ * 部分模型把推理也塞进 content(与 recentDirectoriesAiSummary 同一处理):
462
+ * 汇报里混进一段思考过程,用户会以为那是结论。
463
+ */
464
+ async function attempt(maxTokens) {
465
+ const controller = new AbortController();
466
+ const timer = setTimeout(() => controller.abort(), REPORT_TIMEOUT_MS);
467
+ const filter = createThinkFilter();
468
+ let raw = '';
469
+ const forward = (segments) => {
470
+ for (const seg of segments) if (seg.content) raw += seg.content;
435
471
  };
436
- } finally {
437
- clearTimeout(timer);
472
+ try {
473
+ const res = await callStream(
474
+ model,
475
+ prompt,
476
+ (delta) => { if (delta.content) forward(filter.feed(delta.content)); },
477
+ {
478
+ maxTokens,
479
+ systemPrompt: zh ? SYSTEM_PROMPT_ZH : SYSTEM_PROMPT_EN,
480
+ signal: controller.signal,
481
+ }
482
+ );
483
+ forward(filter.flush());
484
+ return {
485
+ raw,
486
+ aborted: !!(res && res.aborted),
487
+ // 老签名(单测注入的假流)没有这两个字段 —— 拿不到就当"不知道",
488
+ // 后果是退化成"不重试",不是重试到天荒地老
489
+ finishReason: String((res && res.finishReason) || ''),
490
+ meta: describeCallResult(res, maxTokens),
491
+ };
492
+ } finally {
493
+ clearTimeout(timer);
494
+ }
438
495
  }
439
496
 
440
- // 先摘掉头部那两行(PROGRESS / TASKS),剩下的才是给用户看的正文。
441
- // 顺序不能反:先截断再解析,免得 2000 字的正文把标记行挤出去(标记总在最前面,
442
- // 截断其实伤不到它,但"先解析"这件事本身更经得起以后改格式)
443
- const parsed = parseProgressHeader(text.trim().slice(0, MAX_REPORT_CHARS));
444
- const out = parsed.text;
445
- // 空正文按失败记:界面上一张"什么都没有"的报告卡片和"模型没返回内容"是两回事
446
- if (!out) return { ...base, errorCode: 'LLM_FAILED' };
447
- return {
448
- ...base,
449
- text: out,
450
- percent: parsed.percent,
451
- // 每个任务各自的百分比挂在事实快照上:报告卡片里那一行行事实本来就在,
452
- // 挂上去才不会"整体 62%,但看不出是哪两个任务拖的"
453
- tasks: facts.map((f, i) => ({
454
- ...f,
455
- percent: i < parsed.taskPercents.length ? parsed.taskPercents[i] : null,
456
- })),
457
- };
497
+ // 两轮:常规预算 → (只有**被截断且正文为空**才)加预算重试一次。
498
+ // 中途任何一次拿到正文就返回;一次都没拿到正文时,第二轮的元信息更值得留档
499
+ // (它才带着"两次都空"这个事实)。
500
+ let meta = '';
501
+ for (let round = 0; round < 2; round++) {
502
+ const budget = round === 0 ? MAX_REPORT_TOKENS : MAX_REPORT_TOKENS_RETRY;
503
+ let res;
504
+ try {
505
+ res = await attempt(budget);
506
+ } catch (err) {
507
+ return {
508
+ ...base,
509
+ errorCode: 'LLM_FAILED',
510
+ errorDetail: String((err && err.message) || err).slice(0, MAX_ERROR_CHARS),
511
+ };
512
+ }
513
+ // 超时不重试:90 秒没回来,再来一次大概率还是 90 秒
514
+ if (res.aborted) return { ...base, errorCode: 'LLM_TIMEOUT', errorDetail: res.meta };
515
+ meta = res.meta;
516
+
517
+ // 先摘掉头部那两行(PROGRESS / TASKS),剩下的才是给用户看的正文。
518
+ // 顺序不能反:先截断再解析,免得 2000 字的正文把标记行挤出去(标记总在最前面,
519
+ // 截断其实伤不到它,但"先解析"这件事本身更经得起以后改格式)
520
+ const parsed = parseProgressHeader(res.raw.trim().slice(0, MAX_REPORT_CHARS));
521
+ if (parsed.text) {
522
+ return {
523
+ ...base,
524
+ text: parsed.text,
525
+ percent: parsed.percent,
526
+ // 每个任务各自的百分比挂在事实快照上:报告卡片里那一行行事实本来就在,
527
+ // 挂上去才不会"整体 62%,但看不出是哪两个任务拖的"
528
+ tasks: facts.map((f, i) => ({
529
+ ...f,
530
+ percent: i < parsed.taskPercents.length ? parsed.taskPercents[i] : null,
531
+ })),
532
+ };
533
+ }
534
+ // 空正文但不是被预算截断(模型自己 stop 了、或只回了那两行标记):
535
+ // 加预算重试解决不了,直接按空正文记
536
+ if (res.finishReason !== 'length') break;
537
+ }
538
+
539
+ // 空正文按失败记:界面上一张"什么都没有"的报告卡片和"模型没返回内容"是两回事。
540
+ // 单独一个码是因为它的成因与前两个不同(模型答了,但没写正文),
541
+ // 处理办法也不同 —— LLM_EMPTY 要调的是生成预算,不是网络。
542
+ return { ...base, errorCode: 'LLM_EMPTY', errorDetail: meta };
458
543
  }
459
544
 
460
545
  export const __testables = {
@@ -28,11 +28,14 @@ import { readFile } from 'node:fs/promises';
28
28
  import {
29
29
  buildRunningFacts,
30
30
  buildReportPrompt,
31
+ describeCallResult,
31
32
  formatDuration,
32
33
  generateProgressReport,
33
34
  normalizePercent,
34
35
  parseProgressHeader,
35
36
  MAX_FACT_TASKS,
37
+ MAX_REPORT_TOKENS,
38
+ MAX_REPORT_TOKENS_RETRY,
36
39
  SYSTEM_PROMPT_ZH,
37
40
  SYSTEM_PROMPT_EN,
38
41
  } from './progressReport.js';
@@ -237,11 +240,11 @@ test('英文模板同样给思考 / 工具分布 / 静默', () => {
237
240
  // ── generateProgressReport ──────────────────────────────────────────────
238
241
 
239
242
  /** 假流式调用:按 delta 序列回调,再给出终态 */
240
- function fakeStream(deltas, { aborted = false, error = null } = {}) {
243
+ function fakeStream(deltas, { aborted = false, error = null, finishReason = '', usage = null } = {}) {
241
244
  return async (_model, _prompt, onDelta) => {
242
245
  if (error) throw error;
243
246
  for (const d of deltas) onDelta(d);
244
- return { content: deltas.map(d => d.content || '').join(''), aborted };
247
+ return { content: deltas.map(d => d.content || '').join(''), aborted, finishReason, usage };
245
248
  };
246
249
  }
247
250
 
@@ -300,7 +303,7 @@ test('thinking 段被滤掉,不混进正文(否则用户会以为那是结
300
303
  assert.equal(report.text, '一切正常。');
301
304
  });
302
305
 
303
- test('模型报错 → LLM_FAILED 并留住原因;空正文也按失败记', async () => {
306
+ test('模型报错 → LLM_FAILED 并留住原因;空正文记成 LLM_EMPTY', async () => {
304
307
  const facts = buildRunningFacts({ jobs: [runningJob()], tasks: TASKS, now: NOW });
305
308
  const failed = await generateProgressReport({
306
309
  facts,
@@ -310,13 +313,107 @@ test('模型报错 → LLM_FAILED 并留住原因;空正文也按失败记', a
310
313
  assert.equal(failed.errorCode, 'LLM_FAILED');
311
314
  assert.equal(failed.errorDetail, 'HTTP 401');
312
315
 
316
+ // 空正文与"报错"分开记:成因不同(这个是模型答了但没写正文),处理办法也不同
313
317
  const empty = await generateProgressReport({
314
318
  facts,
315
319
  model: MODEL,
316
320
  callStream: fakeStream([{ content: ' ' }]),
317
321
  });
318
- assert.equal(empty.errorCode, 'LLM_FAILED');
322
+ assert.equal(empty.errorCode, 'LLM_EMPTY');
319
323
  assert.equal(empty.text, '');
324
+ // detail 里必须留下可以查的东西 —— 老实现这里是空串,界面上只有一句"生成失败",
325
+ // 盘上也查不出原因(2026-09-30 那三条失败报告就是这么埋掉的)
326
+ assert.match(empty.errorDetail, /finish_reason=/);
327
+ });
328
+
329
+ // ── 空正文:预算被思考吃光(2026-09-30 用户报的"有时候生成失败")──────────
330
+ //
331
+ // 复现过的形态:推理模型的 `max_tokens` 是**思考 + 正文共用**的,思考写满预算时
332
+ // 正文一个字都没写出来,HTTP 还是 200。900 的老预算下实测思考 2708 字符 / content 0。
333
+ // 这三条守的是"预算够用 + 截断了会自己重试一次 + 重试也没用时不假装成功"。
334
+
335
+ test('生成预算够装下思考与正文(900 那个老值会被推理模型吃光)', () => {
336
+ assert.ok(MAX_REPORT_TOKENS >= 2000, `MAX_REPORT_TOKENS=${MAX_REPORT_TOKENS}`);
337
+ assert.ok(MAX_REPORT_TOKENS_RETRY > MAX_REPORT_TOKENS, '重试预算必须比常规预算大');
338
+ });
339
+
340
+ test('第一次被截断(finish_reason=length)且正文为空 → 加预算重试一次并拿到正文', async () => {
341
+ const facts = buildRunningFacts({ jobs: [runningJob()], tasks: TASKS, now: NOW });
342
+ const budgets = [];
343
+ const report = await generateProgressReport({
344
+ facts,
345
+ model: MODEL,
346
+ callStream: async (_m, _p, onDelta, opts) => {
347
+ budgets.push(opts.maxTokens);
348
+ if (budgets.length === 1) return { content: '', aborted: false, finishReason: 'length' };
349
+ onDelta({ content: 'PROGRESS: 40\nTASKS: 40\n第二次拿到了汇报正文。' });
350
+ return { content: '...', aborted: false, finishReason: 'stop' };
351
+ },
352
+ });
353
+ assert.equal(report.errorCode, '');
354
+ assert.equal(report.text, '第二次拿到了汇报正文。');
355
+ assert.equal(report.percent, 40);
356
+ assert.equal(budgets.length, 2, '必须恰好重试一次');
357
+ assert.ok(budgets[1] > budgets[0], `预算没加大:${budgets.join(' -> ')}`);
358
+ });
359
+
360
+ test('空正文但没被截断(模型自己 stop 了)→ 不重试,加预算也没用', async () => {
361
+ const facts = buildRunningFacts({ jobs: [runningJob()], tasks: TASKS, now: NOW });
362
+ let calls = 0;
363
+ const report = await generateProgressReport({
364
+ facts,
365
+ model: MODEL,
366
+ callStream: async () => { calls++; return { content: '', aborted: false, finishReason: 'stop' }; },
367
+ });
368
+ assert.equal(calls, 1);
369
+ assert.equal(report.errorCode, 'LLM_EMPTY');
370
+ assert.match(report.errorDetail, /finish_reason=stop/);
371
+ });
372
+
373
+ test('重试也没拿到正文时,errorDetail 里留下 finish_reason 与思考用量', async () => {
374
+ const facts = buildRunningFacts({ jobs: [runningJob()], tasks: TASKS, now: NOW });
375
+ let calls = 0;
376
+ const report = await generateProgressReport({
377
+ facts,
378
+ model: MODEL,
379
+ callStream: async () => {
380
+ calls++;
381
+ return {
382
+ content: '',
383
+ aborted: false,
384
+ finishReason: 'length',
385
+ usage: { completion_tokens: 8000, completion_tokens_details: { reasoning_tokens: 7980 } },
386
+ };
387
+ },
388
+ });
389
+ assert.equal(calls, 2, '被截断时两轮都该跑');
390
+ assert.equal(report.errorCode, 'LLM_EMPTY');
391
+ // 这一串就是"下次不用再翻网关日志"的全部意义
392
+ assert.match(report.errorDetail, /finish_reason=length/);
393
+ assert.match(report.errorDetail, /reasoning_tokens=7980/);
394
+ assert.match(report.errorDetail, /max_tokens=8000/);
395
+ });
396
+
397
+ test('超时不重试(90 秒没回来,再来一次大概率还是 90 秒)', async () => {
398
+ const facts = buildRunningFacts({ jobs: [runningJob()], tasks: TASKS, now: NOW });
399
+ let calls = 0;
400
+ const report = await generateProgressReport({
401
+ facts,
402
+ model: MODEL,
403
+ callStream: async () => { calls++; return { content: '', aborted: true, finishReason: 'length' }; },
404
+ });
405
+ assert.equal(calls, 1);
406
+ assert.equal(report.errorCode, 'LLM_TIMEOUT');
407
+ assert.match(report.errorDetail, /aborted/);
408
+ });
409
+
410
+ test('describeCallResult 认不出的字段不写,整串有上限', () => {
411
+ assert.equal(describeCallResult(null, 900), 'max_tokens=900 finish_reason=unknown');
412
+ assert.equal(
413
+ describeCallResult({ finishReason: 'stop' }, 3000),
414
+ 'max_tokens=3000 finish_reason=stop'
415
+ );
416
+ assert.ok(describeCallResult({ finishReason: 'x'.repeat(600) }, 1).length <= 300);
320
417
  });
321
418
 
322
419
  test('流被中断(超时)→ LLM_TIMEOUT,而不是当成一句空汇报', async () => {