zen-gitsync 2.17.32 → 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.
- package/README.md +8 -0
- package/package.json +8 -1
- package/src/ui/public/assets/{AgentEngineSelector-CPS9WawU.js → AgentEngineSelector-CGrUh2kD.js} +1 -1
- package/src/ui/public/assets/{AgentView-CHj8Esf5.js → AgentView-BRC3kn-3.js} +1 -1
- package/src/ui/public/assets/{AppVersionBadge-CbYQ0KFm.js → AppVersionBadge-BR_ZX6Oq.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-i_DZoGr4.js → BranchSelector-C77-aizC.js} +1 -1
- package/src/ui/public/assets/{CommitForm-C-cL8_vz.css → CommitForm-BbgIoF4h.css} +1 -1
- package/src/ui/public/assets/CommitForm-C6qTIW0T.js +6 -0
- package/src/ui/public/assets/{CommonDialog-Nwwi4-_9.js → CommonDialog-kDck8ITZ.js} +1 -1
- package/src/ui/public/assets/{EditorView-Cj7r5oIY.js → EditorView-DNZXnJnc.js} +1 -1
- package/src/ui/public/assets/{FileDiffViewer-2vvvRZ_R.js → FileDiffViewer-BxFkqvIA.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-STZphigj.js → FlowExecutionViewer-DDkb05TW.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-BdW1MijZ.js → FlowOrchestrationWorkspace-BiHGQE0o.js} +1 -1
- package/src/ui/public/assets/{IconButton-BuiJBsJ3.js → IconButton-BflzboUD.js} +1 -1
- package/src/ui/public/assets/{LogList-C14b7EUJ.js → LogList-DHfr-rPz.js} +1 -1
- package/src/ui/public/assets/{MindmapView-dcm1kcrO.js → MindmapView-7u02tj5n.js} +1 -1
- package/src/ui/public/assets/{MonitorView-7SOXc-I2.js → MonitorView-cdUkh4VE.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-BM2RrBbL.js → ProjectStartupButton-B9KAavAO.js} +1 -1
- package/src/ui/public/assets/{RecentDirectoriesChat-CXxGKkqI.js → RecentDirectoriesChat-DQ9phSSJ.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-BTtVeges.js → RemoteManagerDialog-CgnM1ndZ.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-B0n8k5Go.js → RemoteRepoCard-BDAvJcFk.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-CT2C3tAc.js → SourceMapView-BEAC-dHC.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-U-AI95UV.js → SvgIcon-B-2iTLW-.js} +1 -1
- package/src/ui/public/assets/{TemplateManager-BIMeoTp0.js → TemplateManager-CYy3G7rH.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-D_V8dhh9.js → UserInputNode-DIX4shBD.js} +1 -1
- package/src/ui/public/assets/WorkbenchView-BAko4SjJ.js +20 -0
- package/src/ui/public/assets/WorkbenchView-DWQChnqF.css +1 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-DkeTtFKF.js → _plugin-vue_export-helper-D8rwCQpe.js} +6 -6
- package/src/ui/public/assets/{agentConversations-B2aTliJe.js → agentConversations-C0WoSH0y.js} +1 -1
- package/src/ui/public/assets/{configStore-Y1biBqmb.js → configStore-BFGSfIBp.js} +1 -1
- package/src/ui/public/assets/{gitStore-CiBz68_Z.js → gitStore-BcrFZogC.js} +1 -1
- package/src/ui/public/assets/index-BCTkDpQv.css +1 -0
- package/src/ui/public/assets/{index-BoZNUwmd.js → index-aNSr4k87.js} +10 -10
- package/src/ui/public/assets/{previewSandbox-D7HcNin7.js → previewSandbox-C_ZzCEpS.js} +1 -1
- package/src/ui/public/assets/{toolsStore-Dhfr-VDL.js → toolsStore-B3LQrSEp.js} +1 -1
- package/src/ui/public/index.html +12 -12
- package/src/ui/server/routes/workbench/index.js +182 -18
- package/src/ui/server/routes/workbench/jobActivity.js +18 -6
- package/src/ui/server/routes/workbench/liveJobs.js +7 -1
- package/src/ui/server/routes/workbench/llmClient.js +20 -4
- package/src/ui/server/routes/workbench/progressReport.js +6 -0
- package/src/ui/server/routes/workbench/projectRegistry.js +58 -2
- package/src/ui/server/routes/workbench/projectRegistry.test.js +51 -0
- package/src/ui/server/routes/workbench/stallWatchdog.js +330 -0
- package/src/ui/server/routes/workbench/stallWatchdog.test.js +356 -0
- package/src/ui/server/utils/directoryGitState.js +41 -1
- package/src/ui/server/utils/directoryGitState.test.js +91 -0
- package/src/ui/public/assets/CommitForm-DrAhqXPt.js +0 -6
- package/src/ui/public/assets/WorkbenchView-BeC5sg_V.css +0 -1
- package/src/ui/public/assets/WorkbenchView-C74ciC03.js +0 -20
- 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-
|
|
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-
|
|
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};
|
package/src/ui/public/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/IconButton-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/assets/configStore-
|
|
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-
|
|
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-
|
|
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-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-
|
|
31
|
-
<link rel="modulepreload" crossorigin href="/assets/toolsStore-
|
|
32
|
-
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-
|
|
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-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
172
|
-
*
|
|
170
|
+
* 距最后一次产出(正文 / 思考 / 工具调用)的**原始**毫秒数,不设阈值。
|
|
171
|
+
*
|
|
172
|
+
* 两个调用方要的是不同的问题:卡片/报告问的是"静默到值得说出来了吗"(用下面的
|
|
173
|
+
* silentMsOf,60s 起),静默看门狗问的是"静默到该判一次了吗"(10 分钟起)。
|
|
174
|
+
* 阈值各留各的,但**计时口径必须只有一份** —— 各写一份 `now - lastActivityAt`
|
|
175
|
+
* 迟早会在"取不到时间戳算什么"上分叉(这里定了:null,不拿 startedAt 兜底,
|
|
176
|
+
* 也不给假的 0)。没有 `lastActivityAt`(老记录、别实例上的旧版进程)→ null。
|
|
173
177
|
*/
|
|
174
|
-
export function
|
|
178
|
+
export function silentMsRaw(job, now) {
|
|
175
179
|
const last = Date.parse((job && job.lastActivityAt) || '');
|
|
176
180
|
if (!Number.isFinite(last)) return null;
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
|
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);
|
|
@@ -130,6 +130,12 @@ export function buildRunningFacts({ jobs = [], tasks = [], now = Date.now() } =
|
|
|
130
130
|
const started = Date.parse(job.startedAt || '');
|
|
131
131
|
out.push({
|
|
132
132
|
taskId: job.taskId || null,
|
|
133
|
+
/**
|
|
134
|
+
* 这条事实对应哪次执行。报告本身用不到(一张卡片一条事实),
|
|
135
|
+
* 但静默看门狗要拿着判定结果**回到那条 job 上改状态** ——
|
|
136
|
+
* 同一个任务可能有好几条 job(重跑 / 并行子任务),按 taskId 改会改错人。
|
|
137
|
+
*/
|
|
138
|
+
jobId: job.id || null,
|
|
133
139
|
taskTitle: String((task && task.title) || '').slice(0, MAX_TITLE_CHARS),
|
|
134
140
|
projectName: projectName(projectPath),
|
|
135
141
|
startedAt: job.startedAt || null,
|
|
@@ -124,22 +124,53 @@ export function latestJob(jobsForTask) {
|
|
|
124
124
|
return best;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* 用户手动标的「已完成」(`task.manualDoneAt`,由 POST /tasks/:id/done 写)现在还成不成立。
|
|
129
|
+
*
|
|
130
|
+
* 为什么要这么个字段:列本来纯靠执行事实推导,但有**两件事只有人知道** ——
|
|
131
|
+
* 一条待在「待处理」里的任务其实早在别处干完了;一条「进行中」的任务模型已经不说话了,
|
|
132
|
+
* 用户比静默看门狗更早判断它干完了。这两件事推导不出来,只能由人落一个标记。
|
|
133
|
+
*
|
|
134
|
+
* 为什么写成"时间比较"而不是"起新一轮时把标记清掉":
|
|
135
|
+
* 起 job 的入口不止一处(看板「执行」、编辑器「执行任务」、续聊、主 Agent 自动派发),
|
|
136
|
+
* 每处都去清一遍,漏掉任何一处就会留下一张"明明刚跑完却还挂在已完成"的卡片 —— 而且不报错。
|
|
137
|
+
* 时间比较把判据收在一个纯函数里:**标记之后又跑过一轮,这次标记就作废**
|
|
138
|
+
* (那次执行才是最新的事实,用户标完成时看到的不是它)。
|
|
139
|
+
*
|
|
140
|
+
* jobsForTask 里没有 startedAt 的老记录不算数:宁可让标记继续成立,
|
|
141
|
+
* 也不能凭空认为"标记过期了"而把用户手动收掉的任务又弹回待处理。
|
|
142
|
+
*/
|
|
143
|
+
export function manualDoneHolds(task, jobsForTask = []) {
|
|
144
|
+
const at = task && typeof task.manualDoneAt === 'string' ? task.manualDoneAt : '';
|
|
145
|
+
if (!at) return false;
|
|
146
|
+
return !(Array.isArray(jobsForTask) ? jobsForTask : [])
|
|
147
|
+
.some(j => j && typeof j.startedAt === 'string' && j.startedAt > at);
|
|
148
|
+
}
|
|
149
|
+
|
|
127
150
|
/**
|
|
128
151
|
* 把一个任务推导到看板的四列之一。纯函数,单测覆盖。
|
|
129
152
|
*
|
|
130
153
|
* 口径(按优先级,逐条短路):
|
|
131
154
|
* 1. 有 job 处于 running/pending → `doing`
|
|
132
|
-
* 2.
|
|
133
|
-
* 3.
|
|
155
|
+
* 2. 有成立的手动「已完成」标记 → `done`
|
|
156
|
+
* 3. 从没执行过 → `todo`
|
|
157
|
+
* 4. 最近一条 job 是 done → `done`;其余终态(error/cancelled)→ `todo`
|
|
134
158
|
*
|
|
135
159
|
* error/cancelled 不再单列:列表示任务是否仍待处理,错误是这一轮执行的结果,
|
|
136
160
|
* 具体错误继续通过卡片标记和任务详情展示。
|
|
161
|
+
*
|
|
162
|
+
* 「在跑」为什么排在手写标记前面:手动完成一条**正在跑**的任务时,路由会先把这一轮停掉
|
|
163
|
+
* 再落标记(见 /tasks/:id/done)。停不掉的那种(任务跑在另一个 g ui 实例里)路由会直接
|
|
164
|
+
* 拒绝,所以正常情况下走不到这条 —— 真走到了,也宁可继续显示「进行中」:
|
|
165
|
+
* 卡片说"已完成"而进程还在写代码,是比列不准更难发现的一种撒谎。
|
|
137
166
|
*/
|
|
138
167
|
export function deriveTaskColumn(task, jobsForTask = []) {
|
|
139
168
|
const jobs = Array.isArray(jobsForTask) ? jobsForTask : [];
|
|
140
169
|
|
|
141
170
|
if (jobs.some(j => j && (j.status === 'running' || j.status === 'pending'))) return 'doing';
|
|
142
171
|
|
|
172
|
+
if (manualDoneHolds(task, jobs)) return 'done';
|
|
173
|
+
|
|
143
174
|
if (jobs.length === 0) return 'todo';
|
|
144
175
|
const last = latestJob(jobs);
|
|
145
176
|
return last && last.status === 'done' ? 'done' : 'todo';
|
|
@@ -364,6 +395,31 @@ export function decorateTaskForBoard(task, jobsForTask = [], { now = Date.now()
|
|
|
364
395
|
lastDurationMs: jobDurationMs(last),
|
|
365
396
|
/** 那条 job 的启动时刻,配合 lastJobEndedAt 给悬停提示里的「几点到几点」 */
|
|
366
397
|
lastJobStartedAt: last ? (last.startedAt || null) : null,
|
|
398
|
+
/**
|
|
399
|
+
* 「这条不是跑完的,是**判**完的」——静默看门狗给的 `{at, reason, silentMs}`,没判过是 null。
|
|
400
|
+
*
|
|
401
|
+
* 为什么要专门给卡片一个字段:自动收尾和"进程自己正常退出"在看板上长得一模一样
|
|
402
|
+
* (都是「已完成」+ 一段用时),但它俩的含义差很远 —— 前者是模型读了思考与最后那段话
|
|
403
|
+
* 后认为活已经干完,用户多半想扫一眼依据再决定要不要信。没有这个字段,卡片只会
|
|
404
|
+
* 不声不响地换个列,模型那句依据就永远躺在 jobs.json 里没人看得到。
|
|
405
|
+
*/
|
|
406
|
+
autoCompleted: (last && last.autoCompleted && typeof last.autoCompleted === 'object')
|
|
407
|
+
? {
|
|
408
|
+
at: last.autoCompleted.at || null,
|
|
409
|
+
reason: typeof last.autoCompleted.reason === 'string' ? last.autoCompleted.reason : '',
|
|
410
|
+
silentMs: Number.isFinite(last.autoCompleted.silentMs) ? last.autoCompleted.silentMs : null,
|
|
411
|
+
}
|
|
412
|
+
: null,
|
|
413
|
+
/**
|
|
414
|
+
* 用户手动标的「已完成」时刻(`POST /tasks/:id/done`),没标过 / 已经被新一轮执行作废 → null。
|
|
415
|
+
*
|
|
416
|
+
* 给的是**成立与否**(manualDoneHolds)而不是原始字段:卡片拿它做两件事 ——
|
|
417
|
+
* ① 决定右下角那颗按钮是「完成」还是「撤销」(撤销只该出现在"手动标的"那种卡上,
|
|
418
|
+
* 撤销一个自己跑完的任务什么都不会发生);② 当完成时刻显示。
|
|
419
|
+
* ② 是必须的:手动收掉的任务没有"跑完的时刻",最近一条 job 可能是三天前那次报错,
|
|
420
|
+
* 照旧取 lastJobEndedAt 的话,用户刚点完完成,卡片上写的是「3 天前」。
|
|
421
|
+
*/
|
|
422
|
+
manualDoneAt: manualDoneHolds(task, jobs) ? task.manualDoneAt : null,
|
|
367
423
|
createdAt: task.createdAt || null,
|
|
368
424
|
updatedAt: task.updatedAt || null,
|
|
369
425
|
};
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
projectName,
|
|
24
24
|
resolveTaskRepoPath,
|
|
25
25
|
deriveTaskColumn,
|
|
26
|
+
manualDoneHolds,
|
|
26
27
|
latestJob,
|
|
27
28
|
summarizeProjectTasks,
|
|
28
29
|
buildProjectEntries,
|
|
@@ -113,6 +114,56 @@ test('latestJob 按 startedAt 取最新的一条', () => {
|
|
|
113
114
|
assert.equal(latestJob([]), null);
|
|
114
115
|
});
|
|
115
116
|
|
|
117
|
+
// ── 手动「已完成」标记(看板卡片右下角的「完成」) ──────────────────
|
|
118
|
+
|
|
119
|
+
test('deriveTaskColumn: 手动标的已完成能盖住"从没跑过"与"上一轮报错"', () => {
|
|
120
|
+
const marked = { manualDoneAt: '2026-01-02T00:00:00Z' };
|
|
121
|
+
assert.equal(deriveTaskColumn(marked, []), 'done');
|
|
122
|
+
// 上一轮报错/被取消的任务,本来退回待处理 —— 手动标完就留在已完成
|
|
123
|
+
const err = [job('j1', 't1', 'error', '2026-01-01T00:00:00Z', '2026-01-01T00:01:00Z')];
|
|
124
|
+
assert.equal(deriveTaskColumn({ manualDoneAt: '2026-01-02T00:00:00Z' }, err), 'done');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('deriveTaskColumn: 标记之后又起过一轮,这次标记作废', () => {
|
|
128
|
+
// 用户标完成时看到的是"上一轮报错",之后又跑了一轮 —— 那次执行才是最新的事实。
|
|
129
|
+
// 用时间比较而不是"起 job 时清标记":起 job 的入口有四个,漏一处就会留下
|
|
130
|
+
// 一张"刚跑完却还挂在已完成"的卡片,而且不报错。
|
|
131
|
+
const jobs = [
|
|
132
|
+
job('j1', 't1', 'running', '2026-01-03T00:00:00Z'),
|
|
133
|
+
];
|
|
134
|
+
assert.equal(manualDoneHolds({ manualDoneAt: '2026-01-02T00:00:00Z' }, jobs), false);
|
|
135
|
+
// 新一轮跑挂了 → 回到待处理,不是已完成(那条错误正等着人看)
|
|
136
|
+
const later = [job('j2', 't1', 'error', '2026-01-03T00:00:00Z', '2026-01-03T00:01:00Z')];
|
|
137
|
+
assert.equal(deriveTaskColumn({ manualDoneAt: '2026-01-02T00:00:00Z' }, later), 'todo');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('deriveTaskColumn: 在跑永远压过手动标记(卡片不能说"已完成"而进程还在写代码)', () => {
|
|
141
|
+
const jobs = [job('j1', 't1', 'running', '2026-01-01T00:00:00Z')];
|
|
142
|
+
// 正常路径下走不到:路由会先把这一轮停掉再落标记,停不掉(别的实例在跑)直接拒绝。
|
|
143
|
+
assert.equal(deriveTaskColumn({ manualDoneAt: '2026-01-02T00:00:00Z' }, jobs), 'doing');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('manualDoneHolds: 缺 startedAt 的老记录不算"标记过期"', () => {
|
|
147
|
+
// 宁可让标记继续成立,也不能凭空认为它过期、把用户手动收掉的任务弹回待处理
|
|
148
|
+
const legacy = [{ id: 'j1', taskId: 't1', status: 'error', startedAt: '' }];
|
|
149
|
+
assert.equal(manualDoneHolds({ manualDoneAt: '2026-01-02T00:00:00Z' }, legacy), true);
|
|
150
|
+
// 从没标过 / 字段不是字符串 → 一律不成立
|
|
151
|
+
assert.equal(manualDoneHolds({}, []), false);
|
|
152
|
+
assert.equal(manualDoneHolds({ manualDoneAt: 12345 }, []), false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('decorateTaskForBoard: manualDoneAt 给的是"成立与否",不是原始字段', () => {
|
|
156
|
+
const now = Date.parse('2026-01-05T00:00:00Z');
|
|
157
|
+
const task = { id: 't1', manualDoneAt: '2026-01-02T00:00:00Z' };
|
|
158
|
+
// 标记之后没跑过 → 卡片拿到它,据此显示「撤销」并把完成时间显示成标记时刻
|
|
159
|
+
assert.equal(decorateTaskForBoard(task, [], { now }).manualDoneAt, '2026-01-02T00:00:00Z');
|
|
160
|
+
// 标记之后又跑了一轮 → 给 null:卡片上那颗按钮该变回「完成」,不能还挂着「撤销」
|
|
161
|
+
const later = [job('j2', 't1', 'done', '2026-01-03T00:00:00Z', '2026-01-03T00:01:00Z')];
|
|
162
|
+
assert.equal(decorateTaskForBoard(task, later, { now }).manualDoneAt, null);
|
|
163
|
+
// 没标过就是 null(不是 undefined —— 前端按 truthy 判断)
|
|
164
|
+
assert.equal(decorateTaskForBoard({ id: 't2' }, [], { now }).manualDoneAt, null);
|
|
165
|
+
});
|
|
166
|
+
|
|
116
167
|
// ── 项目合并去重 ────────────────────────────────────────────────────
|
|
117
168
|
|
|
118
169
|
test('buildProjectEntries: 最近目录与任务路径取并集去重,大小写与斜杠都归一', () => {
|