zen-gitsync 2.17.25 → 2.17.26
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/package.json +1 -1
- package/scripts/release.js +8 -10
- package/src/cli/ai/tools.js +2 -1
- package/src/ui/public/assets/{AgentEngineSelector-BArUIELz.js → AgentEngineSelector-CEayBStS.js} +1 -1
- package/src/ui/public/assets/{AgentView-LpT8A9Fo.js → AgentView-BhLlAvng.js} +1 -1
- package/src/ui/public/assets/{AppVersionBadge-DNXQm_Si.js → AppVersionBadge-btxeyJpH.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-DZPTz5Ql.js → BranchSelector-ERybqfLW.js} +1 -1
- package/src/ui/public/assets/{CommitForm-6avyu1Kv.js → CommitForm-DcXdYwKn.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-CRXyMlp5.js → CommonDialog-5bwugMhs.js} +1 -1
- package/src/ui/public/assets/{EditorView-BIHL8I9g.js → EditorView-BzxNDG8A.js} +1 -1
- package/src/ui/public/assets/{FileDiffViewer-BEYcCYs5.js → FileDiffViewer-BBOtL3bR.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-DKRHDqbG.js → FlowExecutionViewer-BPMDn3Md.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-BnCsF4z4.js → FlowOrchestrationWorkspace-PtCM4LB1.js} +1 -1
- package/src/ui/public/assets/{IconButton-D4df4t9A.js → IconButton-BELiW6cx.js} +1 -1
- package/src/ui/public/assets/{LogList-DXqB4JJv.js → LogList-BAD5dV_N.js} +1 -1
- package/src/ui/public/assets/{MindmapView-Ddc55wzp.js → MindmapView-BAwb7Gs1.js} +1 -1
- package/src/ui/public/assets/{MonitorView-DeERavq-.js → MonitorView-DIqKQfiM.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-DaLU4iS4.js → ProjectStartupButton-DnQyvZOG.js} +1 -1
- package/src/ui/public/assets/{RecentDirectoriesChat-DVtsf7lW.js → RecentDirectoriesChat-BMZA0DgP.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-C3cfDsZi.js → RemoteManagerDialog-BmdcIfwA.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-D1Hso5Fq.js → RemoteRepoCard-DqKIIcGh.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-Dwn5wy1o.js → SourceMapView-Sd_871DD.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-diKLLWK9.js → SvgIcon-DyQoHzNw.js} +1 -1
- package/src/ui/public/assets/{TemplateManager-D9gyZbHP.js → TemplateManager-BI1EXn7d.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-JNtVWcbE.js → UserInputNode-3qtC_swg.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-CJCxYK-K.css → WorkbenchView-BNxybEqF.css} +1 -1
- package/src/ui/public/assets/WorkbenchView-wAdnj8Kl.js +6 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-C7IvbEC3.js → _plugin-vue_export-helper-CGArUtKL.js} +2 -2
- package/src/ui/public/assets/{agentConversations-nN-yDz8q.js → agentConversations-cmS8iux5.js} +1 -1
- package/src/ui/public/assets/{configStore-C-htWfWm.js → configStore-Vh8LZWlL.js} +1 -1
- package/src/ui/public/assets/{gitStore-DYpd8vZK.js → gitStore-B9gZQf2q.js} +1 -1
- package/src/ui/public/assets/{index-eq1OUcdk.js → index-DE-31lcL.js} +5 -5
- package/src/ui/public/assets/{previewSandbox-Z707CZD9.js → previewSandbox-DmecmT9g.js} +1 -1
- package/src/ui/public/assets/{toolsStore-DFu4Alwg.js → toolsStore-DgCBDdhm.js} +1 -1
- package/src/ui/public/index.html +11 -11
- package/src/ui/server/routes/workbench/dispatchInstruction.js +8 -6
- package/src/ui/server/routes/workbench/dispatchInstruction.test.js +29 -2
- package/src/ui/server/routes/workbench/index.js +22 -15
- package/src/ui/server/routes/workbench/orchestratorPrompt.test.js +47 -2
- package/src/ui/server/routes/workbench/orchestratorStore.js +29 -1
- package/src/ui/server/routes/workbench/shared.js +28 -2
- package/src/utils/selfUpdatePolicy.js +26 -4
- package/src/utils/selfUpdatePolicy.test.js +21 -2
- package/src/ui/public/assets/WorkbenchView-DxaAjZAA.js +0 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/office-docx-B0sJQnr-.js","assets/rolldown-runtime-C0FnF6B9.js","assets/element-plus-Bx3j6vzG.js","assets/element-plus-Qexo5hiM.css","assets/office-excel-DMRySOdK.js","assets/office-pptx-Do2blxao.js"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{Bt as t,Cn as n,Dn as r,En as i,Er as a,H as o,Jn as s,K as c,Kn as l,Mn as u,Nn as d,Pn as f,Pr as p,Qn as m,Qr as h,Rn as g,St as _,Tn as v,Xn as y,Yr as b,an as x,br as S,cr as C,on as w,or as T,pr as E,qr as D,tr as O,wn as k,yn as A}from"./element-plus-Bx3j6vzG.js";import{h as j}from"./vendor-BjomZKSS.js";import{t as M}from"./_plugin-vue_export-helper-
|
|
2
|
+
import{o as e}from"./rolldown-runtime-C0FnF6B9.js";import{Bt as t,Cn as n,Dn as r,En as i,Er as a,H as o,Jn as s,K as c,Kn as l,Mn as u,Nn as d,Pn as f,Pr as p,Qn as m,Qr as h,Rn as g,St as _,Tn as v,Xn as y,Yr as b,an as x,br as S,cr as C,on as w,or as T,pr as E,qr as D,tr as O,wn as k,yn as A}from"./element-plus-Bx3j6vzG.js";import{h as j}from"./vendor-BjomZKSS.js";import{t as M}from"./_plugin-vue_export-helper-CGArUtKL.js";import{i as N}from"./monaco-DkJuNcCr.js";import{n as P,r as F}from"./flow-mindmap-OPE_D4da.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()}`}g(),p(),E();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(f({__name:`ImagePreview`,props:{filePath:{},fileName:{}},setup(e){let i=e,d=S(null),f=S(1),p=S(0),m=S(0),g=S(!1),v=S({x:0,y:0}),E=S(null),O=S(!1),A=S(null),j=()=>{if(!d.value||!E.value)return;let e=d.value.clientWidth-32,t=d.value.clientHeight-32;if(e<=0||t<=0)return;let n=Math.min(e/E.value.w,t/E.value.h,1);f.value=n,p.value=0,m.value=0},M=()=>{f.value=Math.min(f.value*1.2,8)},N=()=>{f.value=Math.max(f.value/1.2,.1)},P=()=>{f.value=1,p.value=0,m.value=0,j()},F=e=>{e.preventDefault(),e.deltaY<0?M():N()},I=e=>{e.button===0&&(g.value=!0,v.value={x:e.clientX-p.value,y:e.clientY-m.value})},L=e=>{g.value&&(p.value=e.clientX-v.value.x,m.value=e.clientY-v.value.y)},R=()=>{g.value=!1},z=e=>{let t=e.target;E.value={w:t.naturalWidth,h:t.naturalHeight},O.value=!1,requestAnimationFrame(()=>j())},B=()=>{O.value=!0},V=n(()=>i.fileName||i.filePath?.split(`/`).pop()||``),H=n(()=>ee(i.filePath));return s(()=>{window.addEventListener(`mousemove`,L),window.addEventListener(`mouseup`,R),window.addEventListener(`resize`,j)}),l(()=>{window.removeEventListener(`mousemove`,L),window.removeEventListener(`mouseup`,R),window.removeEventListener(`resize`,j)}),T(()=>i.filePath,()=>{E.value=null,O.value=!1,f.value=1,p.value=0,m.value=0}),(e,n)=>{let s=o;return y(),r(`div`,{class:`image-preview`,ref_key:`containerRef`,ref:d,onWheel:F},[k(`div`,te,[k(`span`,{class:`image-preview-name`,title:i.filePath},h(V.value),9,U),E.value?(y(),r(`span`,W,h(E.value.w)+` × `+h(E.value.h)+` px`,1)):(y(),r(`span`,G,h(Math.round(f.value*100))+`%`,1)),n[0]||=k(`div`,{class:`image-preview-spacer`},null,-1),u(s,{content:`放大`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:M},[u(a(c),null,{default:C(()=>[u(a(x))]),_:1})])]),_:1}),u(s,{content:`缩小`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:N},[u(a(c),null,{default:C(()=>[u(a(w))]),_:1})])]),_:1}),u(s,{content:`适应窗口`,placement:`top`},{default:C(()=>[k(`button`,{class:`image-preview-btn`,onClick:P},[u(a(c),null,{default:C(()=>[u(a(t))]),_:1})])]),_:1})]),k(`div`,{class:D([`image-preview-stage`,{"is-dragging":g.value}]),onMousedown:I},[O.value?(y(),r(`div`,K,[u(a(c),{size:`32`,color:`var(--color-danger)`},{default:C(()=>[u(a(_))]),_:1}),n[1]||=k(`p`,null,`图片加载失败`,-1),k(`p`,q,h(i.filePath),1)])):(y(),r(`img`,{key:1,ref_key:`imgRef`,ref:A,src:H.value,alt:V.value,class:`image-preview-img`,style:b({transform:`translate(${p.value}px, ${m.value}px) scale(${f.value})`}),onLoad:z,onError:B,draggable:`false`},null,44,J))],34),k(`div`,Y,[n[2]||=k(`span`,null,`滚轮缩放 · 拖拽平移 · 双击重置`,-1),k(`span`,X,h(Math.round(f.value*100))+`%`,1)])],544)}}}),[[`__scopeId`,`data-v-28b3ff72`]]);g(),E();var ne={class:`md-preview`},re=[`innerHTML`],ie={key:1,class:`md-mindmap`},ae=M(f({__name:`MarkdownPreview`,props:{content:{},allowHtml:{type:Boolean,default:!0}},setup(e){let t=e;function i(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=n(()=>{let e=t.content||``,n=[],r=`@@FLOWDASH_MINDMAP_BLOCK_`,a=e.replace(/```[ \t]*mindmap[ \t]*\n([\s\S]*?)```/gi,(e,t)=>{let i=n.length;return n.push({id:i,md:t}),`${r}${i}@@`}),o=j(a,{className:!1,html:t.allowHtml}),s=t.allowHtml?o:i(o),c=RegExp(`(?:<p>)?\\s*${r}(\\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:n[e].md,data:P(n[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,t)=>(y(),r(`div`,ne,[(y(!0),r(A,null,m(o.value,(e,n)=>(y(),r(A,{key:n},[e.type===`html`?(y(),r(`div`,{key:0,class:`md-segment`,innerHTML:e.html},null,8,re)):(y(),r(`div`,ie,[t[0]||=k(`div`,{class:`md-mindmap-title`},`四维导图`,-1),u(a(F),{data:e.data,"preview-mode":``,class:`md-mindmap-canvas`},null,8,[`data`])]))],64))),128))]))}}),[[`__scopeId`,`data-v-9514c256`]]);g(),p(),E();var oe={class:`office-preview`},se={class:`office-preview-toolbar`},ce={class:`office-preview-badge`},le={key:0,class:`office-preview-side`},ue={key:0,class:`office-preview-state`},Q={key:1,class:`office-preview-state office-preview-error`},de=[`src`],fe=M(f({__name:`OfficePreview`,props:{filePath:{},source:{default:`worktree`},rev:{default:``},side:{default:`new`}},setup(t){let a=t,o=d(()=>N(()=>import(`./office-docx-B0sJQnr-.js`).then(t=>e(t.t(),1)),__vite__mapDeps([0,1,2,3]))),s=d(()=>N(()=>import(`./office-excel-DMRySOdK.js`).then(t=>e(t.t(),1)),__vite__mapDeps([4,1,0,2,3]))),c=d(()=>N(()=>import(`./office-pptx-Do2blxao.js`).then(t=>e(t.t(),1)),__vite__mapDeps([5,1,0,2,3]))),u=n(()=>z(a.filePath)),f=n(()=>u.value===`docx`?o:u.value===`xls`||u.value===`xlsx`?s:u.value===`pptx`?c:null),p=n(()=>H(a.filePath,{source:a.source,rev:a.rev,side:a.side})),m=n(()=>V(a.filePath,{source:a.source,rev:a.rev,side:a.side})),g=S(null),_=S(``),b=S(!1),x=S(!1),C=S(!1),w=0;function E(){_.value&&URL.revokeObjectURL(_.value),_.value=``}async function D(e){try{let t=await fetch(m.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==w)return;_.value=URL.createObjectURL(await t.blob()),C.value=!0,x.value=!1}catch{e===w&&(x.value=!0)}}async function A(){let e=++w;if(b.value=!0,x.value=!1,C.value=!1,g.value=null,E(),!f.value){await D(e),e===w&&(b.value=!1);return}try{let t=await fetch(p.value);if(!t.ok)throw Error(`HTTP ${t.status}`);if(e!==w)return;g.value=await t.arrayBuffer()}catch{await D(e)}finally{e===w&&(b.value=!1)}}function j(){if(!C.value){let e=w;g.value=null,b.value=!0,D(e).finally(()=>{e===w&&(b.value=!1)})}}function M(){A()}return T(p,()=>{A()},{immediate:!0}),l(()=>{w++,E()}),(e,n)=>(y(),r(`div`,oe,[k(`div`,se,[n[0]||=k(`span`,{class:`office-preview-title`},`Office 预览`,-1),k(`span`,ce,h(u.value.toUpperCase()),1),t.source===`git`?(y(),r(`span`,le,h(t.side===`old`?`旧版本`:`新版本`),1)):i(``,!0),k(`button`,{class:`office-preview-reload`,type:`button`,onClick:M},`刷新`)]),b.value?(y(),r(`div`,ue,`正在生成预览…`)):x.value?(y(),r(`div`,Q,[...n[1]||=[k(`p`,null,`无法预览此 Office 文件。`,-1),k(`p`,{class:`office-preview-hint`},`支持 docx、xls/xlsx、pptx;老格式需要安装 LibreOffice 才能兜底预览。`,-1)]])):C.value?(y(),r(`iframe`,{key:2,class:`office-preview-frame`,src:_.value,title:`Office 文件 PDF 预览`},null,8,de)):f.value&&g.value?(y(),v(O(f.value),{key:3,class:`office-preview-document`,src:g.value,onError:j},null,40,[`src`])):i(``,!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-Bx3j6vzG.js";import{b as n}from"./vendor-BjomZKSS.js";import{n as r}from"./_plugin-vue_export-helper-
|
|
1
|
+
import{br as e,pr as t}from"./element-plus-Bx3j6vzG.js";import{b as n}from"./vendor-BjomZKSS.js";import{n as r}from"./_plugin-vue_export-helper-CGArUtKL.js";import{i,o as a}from"./configStore-Vh8LZWlL.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(!a(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 i)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,25 +10,25 @@
|
|
|
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-DE-31lcL.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/element-plus-Bx3j6vzG.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/monaco-DkJuNcCr.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/vendor-BjomZKSS.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-CGArUtKL.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-DyQoHzNw.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/IconButton-BELiW6cx.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/configStore-Vh8LZWlL.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-B9gZQf2q.js">
|
|
24
24
|
<link rel="modulepreload" crossorigin href="/assets/fileIcon-B5p5pGN9.js">
|
|
25
25
|
<link rel="modulepreload" crossorigin href="/assets/flow-mindmap-OPE_D4da.js">
|
|
26
|
-
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-
|
|
26
|
+
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-DmecmT9g.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/useThemeObserver-DYxFoHh1.js">
|
|
28
|
-
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-
|
|
29
|
-
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/toolsStore-
|
|
31
|
-
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-
|
|
28
|
+
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-5bwugMhs.js">
|
|
29
|
+
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-BBOtL3bR.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/toolsStore-DgCBDdhm.js">
|
|
31
|
+
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-BI1EXn7d.js">
|
|
32
32
|
<link rel="modulepreload" crossorigin href="/assets/vue-flow-fVVGZspa.js">
|
|
33
33
|
<link rel="stylesheet" crossorigin href="/assets/element-plus-Qexo5hiM.css">
|
|
34
34
|
<link rel="stylesheet" crossorigin href="/assets/monaco-B2lPvkrc.css">
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
genId,
|
|
39
39
|
TASKS_FILE,
|
|
40
40
|
IMAGES_DIR,
|
|
41
|
-
|
|
41
|
+
MAX_INSTRUCTION_CHARS,
|
|
42
42
|
} from './shared.js';
|
|
43
43
|
import { stagingPath, mimeForExt } from './attachmentUtils.js';
|
|
44
44
|
import { readOrchestrator, resolveDispatchPrompt, appendInstruction } from './orchestratorStore.js';
|
|
@@ -109,7 +109,11 @@ export function createDispatcher({ configManager, getCurrentProjectPath, runTask
|
|
|
109
109
|
async function dispatchInstruction(payload = {}) {
|
|
110
110
|
const text = typeof payload.text === 'string' ? payload.text.trim() : '';
|
|
111
111
|
if (!text) throw new HttpError(400, '指令内容不能为空');
|
|
112
|
-
|
|
112
|
+
// 上限来自 shared.js,不在这里写死数字:写死过一次(4000),前端提示、
|
|
113
|
+
// 工具描述、这条校验各说各的,改的时候漏一处就变成"界面说能发、后端说不能"。
|
|
114
|
+
if (text.length > MAX_INSTRUCTION_CHARS) {
|
|
115
|
+
throw new HttpError(400, `指令过长(上限 ${MAX_INSTRUCTION_CHARS} 字)`);
|
|
116
|
+
}
|
|
113
117
|
|
|
114
118
|
const fallbackPath = typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '';
|
|
115
119
|
const bodyPath = typeof payload.projectPath === 'string' ? payload.projectPath.trim() : '';
|
|
@@ -182,11 +186,9 @@ export function createDispatcher({ configManager, getCurrentProjectPath, runTask
|
|
|
182
186
|
// ── 附件:调用方只回传 { id, ext, originalName },服务端按 id 回暂存区找文件 ──
|
|
183
187
|
// 路径完全由服务端拼(stagingPath 会同时校验 id 形状与 ext 白名单),
|
|
184
188
|
// 所以不存在"调用方指定任意路径"这回事 —— 比"信任 absolutePath 再校验前缀"干净。
|
|
185
|
-
//
|
|
189
|
+
// 这里只校验**每个附件自己**(形状 + 文件是否还在);数量不设上限,
|
|
190
|
+
// 真正的卡点只有下游读得动读不动。
|
|
186
191
|
const rawAttachments = Array.isArray(payload.attachments) ? payload.attachments : [];
|
|
187
|
-
if (rawAttachments.length > MAX_ATTACHMENTS_PER_TASK) {
|
|
188
|
-
throw new HttpError(400, `附件最多 ${MAX_ATTACHMENTS_PER_TASK} 个`);
|
|
189
|
-
}
|
|
190
192
|
const staged = [];
|
|
191
193
|
for (const item of rawAttachments) {
|
|
192
194
|
const attId = typeof item?.id === 'string' ? item.id : '';
|
|
@@ -55,7 +55,7 @@ delete process.env.HOMEPATH;
|
|
|
55
55
|
const load = p => import(pathToFileURL(path.join(projectRoot, p)).href);
|
|
56
56
|
const { createDispatcher, resolveExecutor } = await load('src/ui/server/routes/workbench/dispatchInstruction.js');
|
|
57
57
|
const { readOrchestrator, setOrchestratorActive, setDefaultPrompt } = await load('src/ui/server/routes/workbench/orchestratorStore.js');
|
|
58
|
-
const { readJson, TASKS_FILE } = await load('src/ui/server/routes/workbench/shared.js');
|
|
58
|
+
const { readJson, TASKS_FILE, MAX_INSTRUCTION_CHARS } = await load('src/ui/server/routes/workbench/shared.js');
|
|
59
59
|
|
|
60
60
|
// 两个项目目录:proj 是"选中/当前"的落点,other 用来证明显式指定优先于默认
|
|
61
61
|
const proj = path.join(fakeHome, 'proj-alpha');
|
|
@@ -201,10 +201,37 @@ test('空指令 / 超长指令一律 400,不留下任务', async () => {
|
|
|
201
201
|
const { dispatchInstruction } = makeDispatcher();
|
|
202
202
|
const before = (await readTasks()).length;
|
|
203
203
|
await assert.rejects(() => dispatchInstruction({ text: ' ' }), err => err.statusCode === 400 && /指令内容不能为空/.test(err.message));
|
|
204
|
-
|
|
204
|
+
// 用常量而不是写死数字:上限放宽过(4000 → 100000),钉死的用例会在放宽那天
|
|
205
|
+
// 悄悄变成"检查 4001 字"——它照样通过,却再也不测真正那道闸门了。
|
|
206
|
+
await assert.rejects(
|
|
207
|
+
() => dispatchInstruction({ text: 'x'.repeat(MAX_INSTRUCTION_CHARS + 1) }),
|
|
208
|
+
err => err.statusCode === 400 && /指令过长/.test(err.message),
|
|
209
|
+
);
|
|
205
210
|
assert.equal((await readTasks()).length, before);
|
|
206
211
|
});
|
|
207
212
|
|
|
213
|
+
test('刚好卡在上限的指令放行(闸门是 >,不是 >=)', async () => {
|
|
214
|
+
const { dispatchInstruction } = makeDispatcher();
|
|
215
|
+
const result = await dispatchInstruction({
|
|
216
|
+
text: 'x'.repeat(MAX_INSTRUCTION_CHARS),
|
|
217
|
+
autoRun: false,
|
|
218
|
+
});
|
|
219
|
+
// 整段正文原样落进任务:截断发生在"下发预览"那一层,不在派发这一层
|
|
220
|
+
assert.equal(result.task.desc.length, MAX_INSTRUCTION_CHARS);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('指令正文原样进任务,不被悄悄截断', async () => {
|
|
224
|
+
const { dispatchInstruction } = makeDispatcher();
|
|
225
|
+
// 2026-09-29:用户粘一整段报错日志被 400 挡回来。除了放宽上限,还要钉住
|
|
226
|
+
// "整段进 desc" —— 因为 dispatchInstruction 是唯一拿得到原文的地方,
|
|
227
|
+
// 一旦这里截了,任务执行时模型看到的就是半截需求,而界面毫无异样。
|
|
228
|
+
const long = Array.from({ length: 3000 }, (_, i) => `第 ${i} 行:原始需求内容`).join('\n');
|
|
229
|
+
assert.ok(long.length > 20000 && long.length < MAX_INSTRUCTION_CHARS);
|
|
230
|
+
const result = await dispatchInstruction({ text: long, autoRun: false });
|
|
231
|
+
assert.equal(result.task.desc, long);
|
|
232
|
+
assert.equal(result.instruction.text, long, '指令流水那份也存完整原文(截断只发生在 5s 轮询下发时)');
|
|
233
|
+
});
|
|
234
|
+
|
|
208
235
|
test('目标目录不存在:记一条 rejected 流水再抛 400', async () => {
|
|
209
236
|
const { dispatchInstruction } = makeDispatcher();
|
|
210
237
|
const missing = path.join(fakeHome, 'not-created-yet');
|
|
@@ -47,8 +47,9 @@ import {
|
|
|
47
47
|
TRUTH_FILES,
|
|
48
48
|
IMAGES_DIR,
|
|
49
49
|
MAX_IMAGE_BYTES,
|
|
50
|
-
MAX_ATTACHMENTS_PER_TASK,
|
|
51
50
|
MAX_DEFAULT_PROMPT_CHARS,
|
|
51
|
+
MAX_INSTRUCTION_CHARS,
|
|
52
|
+
INSTRUCTION_PREVIEW_CHARS,
|
|
52
53
|
readJson,
|
|
53
54
|
writeJson,
|
|
54
55
|
nowIso,
|
|
@@ -125,6 +126,7 @@ import {
|
|
|
125
126
|
claimReportSlot,
|
|
126
127
|
readReports,
|
|
127
128
|
appendReport,
|
|
129
|
+
withInstructionPreview,
|
|
128
130
|
} from './orchestratorStore.js';
|
|
129
131
|
import {
|
|
130
132
|
buildRunningFacts,
|
|
@@ -967,7 +969,12 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
967
969
|
}
|
|
968
970
|
|
|
969
971
|
// 共享 helper:写入新附件
|
|
970
|
-
|
|
972
|
+
//
|
|
973
|
+
// 只负责把字节落盘 + 造出附件记录,**不碰 target.attachments** —— 由调用方决定挂到哪。
|
|
974
|
+
// 之前这里既 push 了一次、调用方又 push 一次,而两处指向同一个数组(target 是浅拷贝),
|
|
975
|
+
// 于是每次上传都在 tasks.json 里留下一条 id 完全相同的幽灵附件:附件数翻倍、
|
|
976
|
+
// 缩略图列表出现重复项,删一条另一条还在。
|
|
977
|
+
async function writeAttachmentTo({ req, target }) {
|
|
971
978
|
if (!req.body || !(req.body instanceof Buffer) || req.body.length === 0) {
|
|
972
979
|
throw new HttpError(400, '请求体为空');
|
|
973
980
|
}
|
|
@@ -980,10 +987,6 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
980
987
|
if (!ext) {
|
|
981
988
|
throw new HttpError(400, `不支持的文件类型(仅允许 ${[...ALLOWED_EXTS].join(', ')})`);
|
|
982
989
|
}
|
|
983
|
-
if (!Array.isArray(target.attachments)) target.attachments = [];
|
|
984
|
-
if (target.attachments.length >= maxCount) {
|
|
985
|
-
throw new HttpError(400, `附件已达上限 ${maxCount} 个`);
|
|
986
|
-
}
|
|
987
990
|
|
|
988
991
|
const attId = genId();
|
|
989
992
|
await fsp.mkdir(target.storageDir, { recursive: true });
|
|
@@ -991,7 +994,7 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
991
994
|
const storedPath = path.join(target.storageDir, storedName);
|
|
992
995
|
await fsp.writeFile(storedPath, req.body);
|
|
993
996
|
|
|
994
|
-
|
|
997
|
+
return {
|
|
995
998
|
id: attId,
|
|
996
999
|
originalName,
|
|
997
1000
|
mimeType,
|
|
@@ -1001,9 +1004,6 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
1001
1004
|
absolutePath: storedPath,
|
|
1002
1005
|
createdAt: nowIso()
|
|
1003
1006
|
};
|
|
1004
|
-
target.attachments.push(attachment);
|
|
1005
|
-
target.updatedAt = nowIso();
|
|
1006
|
-
return attachment;
|
|
1007
1007
|
}
|
|
1008
1008
|
|
|
1009
1009
|
// 子任务附件
|
|
@@ -1014,7 +1014,7 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
1014
1014
|
const task = (data.tasks || []).find(t => t.id === taskId);
|
|
1015
1015
|
if (!task) throw new HttpError(404, '任务不存在');
|
|
1016
1016
|
const target = { ...task, storageDir: path.join(IMAGES_DIR, '_task-' + taskId) };
|
|
1017
|
-
const att = await writeAttachmentTo({ req, target
|
|
1017
|
+
const att = await writeAttachmentTo({ req, target });
|
|
1018
1018
|
task.attachments = Array.isArray(task.attachments) ? task.attachments : [];
|
|
1019
1019
|
task.attachments.push(att);
|
|
1020
1020
|
task.updatedAt = nowIso();
|
|
@@ -1232,7 +1232,14 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
1232
1232
|
success: true,
|
|
1233
1233
|
active: state.active,
|
|
1234
1234
|
updatedAt: state.updatedAt,
|
|
1235
|
-
|
|
1235
|
+
// ⚠️ 这个接口是 5s 轮询的。指令正文在**下发这一份**里被截到
|
|
1236
|
+
// INSTRUCTION_PREVIEW_CHARS(落盘那份不截,见 shared.js 的注释)。
|
|
1237
|
+
// 指令上限放宽到十万字之后不截就是 200 × 100000 = 20MB 一轮。
|
|
1238
|
+
instructions: state.instructions.map(withInstructionPreview),
|
|
1239
|
+
// 指令正文上限跟着状态一起下发:前端要显示"还剩多少字"就得知道这个数,
|
|
1240
|
+
// 而它归服务端定(派发校验也在这儿)。前端不再自己写一份数字 ——
|
|
1241
|
+
// 之前 maxlength 那种"界面说能发、后端 400"的分叉就是这么来的。
|
|
1242
|
+
maxInstructionChars: MAX_INSTRUCTION_CHARS,
|
|
1236
1243
|
// 默认提示词跟着这份状态一起下发:控制台要拿它显示"当前会附带什么",
|
|
1237
1244
|
// 设置弹窗打开时也不必再单独取一次(单条上限 8000 字,体积可控)
|
|
1238
1245
|
defaultPrompt: state.defaultPrompt,
|
|
@@ -1480,12 +1487,12 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
1480
1487
|
* 为什么需要这一层:§17 的上传端点都要求 taskId / subId 当挂载点,而派发这一刻
|
|
1481
1488
|
* 任务还不存在。所以先写进 `_dispatch/`,**不写任何 JSON** —— 附件记录回给前端持有,
|
|
1482
1489
|
* 派发时再按 id 搬进任务目录。
|
|
1483
|
-
*
|
|
1490
|
+
* 代价是服务端在这里无状态:它不知道前端一共攒了几个(数量本来也不设上限,
|
|
1491
|
+
* 所以这份无状态不再有代价 —— 真要卡也只能卡在派发那一刻,而那时已经晚了)。
|
|
1484
1492
|
*/
|
|
1485
1493
|
app.post('/api/workbench/orchestrator/attachments', rawAttachment, asyncRoute(async (req, res) => {
|
|
1486
1494
|
await fsp.mkdir(DISPATCH_STAGING_DIR, { recursive: true });
|
|
1487
|
-
const
|
|
1488
|
-
const att = await writeAttachmentTo({ req, target, maxCount: MAX_ATTACHMENTS_PER_TASK });
|
|
1495
|
+
const att = await writeAttachmentTo({ req, target: { storageDir: DISPATCH_STAGING_DIR } });
|
|
1489
1496
|
res.json({ success: true, attachment: att });
|
|
1490
1497
|
}));
|
|
1491
1498
|
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
import { test } from 'node:test';
|
|
25
25
|
import assert from 'node:assert/strict';
|
|
26
26
|
|
|
27
|
-
import { resolveDispatchPrompt, normalizeOrchestrator } from './orchestratorStore.js';
|
|
28
|
-
import { MAX_DEFAULT_PROMPT_CHARS } from './shared.js';
|
|
27
|
+
import { resolveDispatchPrompt, normalizeOrchestrator, withInstructionPreview } from './orchestratorStore.js';
|
|
28
|
+
import { MAX_DEFAULT_PROMPT_CHARS, INSTRUCTION_PREVIEW_CHARS } from './shared.js';
|
|
29
29
|
|
|
30
30
|
/** 造一份最小可用的编排状态(字段与 normalizeOrchestrator 的输出对齐) */
|
|
31
31
|
function stateWith({ defaultPrompt = '', projectPrompts = {} } = {}) {
|
|
@@ -157,3 +157,48 @@ test('normalizeOrchestrator:指令的 promptSource 只认白名单,脏值归
|
|
|
157
157
|
});
|
|
158
158
|
assert.deepEqual(s.instructions.map(i => i.promptSource), ['both', 'global', '', '']);
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
// ── 指令流水的「下发预览」截断(2026-09-29)────────────────────────────
|
|
162
|
+
//
|
|
163
|
+
// 指令上限从 4000 放宽到 100000 之后,唯一会先崩的是**5s 轮询**:
|
|
164
|
+
// /api/workbench/orchestrator 每轮下发 state.instructions 整份(200 条上限),
|
|
165
|
+
// 不截就是 200 × 100000 = 20MB 一轮。
|
|
166
|
+
//
|
|
167
|
+
// 关键在"截哪一份":落盘那份(orchestrator.json)与任务正文(task.desc)必须完整 ——
|
|
168
|
+
// 指令流水的意义就是"我说过什么"能被翻到,而 Agent 侧的真相源清单指着那个文件。
|
|
169
|
+
// 所以这里守的是"只截下发、且如实标出被截过"。
|
|
170
|
+
|
|
171
|
+
test('withInstructionPreview:超长正文截到预览上限并标出已截断', () => {
|
|
172
|
+
const long = 'x'.repeat(INSTRUCTION_PREVIEW_CHARS + 500);
|
|
173
|
+
const r = withInstructionPreview({ id: 'a', text: long, status: 'accepted' });
|
|
174
|
+
assert.equal(r.text.length, INSTRUCTION_PREVIEW_CHARS);
|
|
175
|
+
assert.equal(r.textTruncated, true);
|
|
176
|
+
// 其余字段一个都不能丢:前端要靠 status / projectPath / targetSource 渲染这行
|
|
177
|
+
assert.equal(r.id, 'a');
|
|
178
|
+
assert.equal(r.status, 'accepted');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('withInstructionPreview:没超长时原样返回,且不误标截断', () => {
|
|
182
|
+
const short = '一句话指令';
|
|
183
|
+
const r = withInstructionPreview({ id: 'a', text: short });
|
|
184
|
+
assert.equal(r.text, short);
|
|
185
|
+
assert.equal(r.textTruncated, false);
|
|
186
|
+
// 恰好等于上限不算超(截断口是 >)
|
|
187
|
+
const atLimit = withInstructionPreview({ id: 'b', text: 'y'.repeat(INSTRUCTION_PREVIEW_CHARS) });
|
|
188
|
+
assert.equal(atLimit.textTruncated, false);
|
|
189
|
+
assert.equal(atLimit.text.length, INSTRUCTION_PREVIEW_CHARS);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('withInstructionPreview:不改传入的记录(落盘那份必须还是完整的)', () => {
|
|
193
|
+
const long = 'z'.repeat(INSTRUCTION_PREVIEW_CHARS + 10);
|
|
194
|
+
const original = { id: 'a', text: long };
|
|
195
|
+
withInstructionPreview(original);
|
|
196
|
+
assert.equal(original.text.length, INSTRUCTION_PREVIEW_CHARS + 10, '截断只发生在返回的新对象上');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('withInstructionPreview:脏输入不炸(缺 text / null / 非对象)', () => {
|
|
200
|
+
assert.deepEqual(withInstructionPreview({ id: 'a' }), { id: 'a', text: '', textTruncated: false });
|
|
201
|
+
assert.equal(withInstructionPreview(null).text, '');
|
|
202
|
+
assert.equal(withInstructionPreview(undefined).text, '');
|
|
203
|
+
assert.equal(withInstructionPreview({ text: 123 }).text, '');
|
|
204
|
+
});
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
ORCHESTRATOR_REPORTS_FILE,
|
|
40
40
|
MAX_ORCHESTRATOR_INSTRUCTIONS,
|
|
41
41
|
MAX_DEFAULT_PROMPT_CHARS,
|
|
42
|
+
INSTRUCTION_PREVIEW_CHARS,
|
|
42
43
|
MAX_PROGRESS_REPORTS,
|
|
43
44
|
PROGRESS_REPORT_INTERVALS_MS,
|
|
44
45
|
DEFAULT_PROGRESS_REPORT_INTERVAL_MS,
|
|
@@ -245,6 +246,27 @@ export function resolveDispatchPrompt(state, projectPath) {
|
|
|
245
246
|
return { text, source };
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
/**
|
|
250
|
+
* 把一条指令流水压成「可以每 5 秒下发」的形状:正文只留开头 INSTRUCTION_PREVIEW_CHARS 字。
|
|
251
|
+
*
|
|
252
|
+
* 为什么需要它:指令正文上限已放宽到十万字(MAX_INSTRUCTION_CHARS),而
|
|
253
|
+
* /api/workbench/orchestrator 是 5s 轮询、下发 state.instructions 整份(200 条)。
|
|
254
|
+
* 不截就是 200 × 100000 = 20MB 一轮。
|
|
255
|
+
*
|
|
256
|
+
* 只压**下发**:落盘那份(orchestrator.json)与任务正文(task.desc)都保持完整,
|
|
257
|
+
* 指令流水的意义就是"我说过什么"能被翻到。附 textTruncated 让前端能如实说"已截断",
|
|
258
|
+
* 而不是让用户以为当初就只写了这么多。
|
|
259
|
+
*
|
|
260
|
+
* @param {object} it orchestratorStore 的指令记录
|
|
261
|
+
* @returns {object} 新对象(不改原记录 —— 落盘那份必须还是完整的)
|
|
262
|
+
*/
|
|
263
|
+
export function withInstructionPreview(it) {
|
|
264
|
+
const src = it && typeof it === 'object' ? it : {};
|
|
265
|
+
const text = typeof src.text === 'string' ? src.text : '';
|
|
266
|
+
const truncated = text.length > INSTRUCTION_PREVIEW_CHARS;
|
|
267
|
+
return { ...src, text: truncated ? text.slice(0, INSTRUCTION_PREVIEW_CHARS) : text, textTruncated: truncated };
|
|
268
|
+
}
|
|
269
|
+
|
|
248
270
|
/** 追加一条人类干预指令记录,返回写入后的那条记录 */
|
|
249
271
|
export async function appendInstruction(entry) {
|
|
250
272
|
return serialize(async () => {
|
|
@@ -508,12 +530,18 @@ export function buildActivityFeed({ jobs = [], tasks = [], instructions = [] } =
|
|
|
508
530
|
|
|
509
531
|
for (const it of Array.isArray(instructions) ? instructions : []) {
|
|
510
532
|
if (!it || !it.id) continue;
|
|
533
|
+
// 正文只带**开头一段**进活动流:这条流跟着 5s 轮询下发(截断口径见
|
|
534
|
+
// withInstructionPreview)。落盘那份不截 —— 完整原文在 orchestrator.json 与
|
|
535
|
+
// task.desc 里,要全文的人去那两个地方拿。
|
|
536
|
+
const preview = withInstructionPreview(it);
|
|
511
537
|
rows.push({
|
|
512
538
|
id: `ins:${it.id}`,
|
|
513
539
|
kind: 'user',
|
|
514
540
|
at: it.at,
|
|
515
541
|
instructionId: it.id,
|
|
516
|
-
text:
|
|
542
|
+
text: preview.text,
|
|
543
|
+
// 前端据此显示"已截断",而不是让人以为指令本来就那么短
|
|
544
|
+
textTruncated: preview.textTruncated,
|
|
517
545
|
taskId: it.taskId || null,
|
|
518
546
|
instructionStatus: it.status || 'created',
|
|
519
547
|
reason: it.reason || '',
|
|
@@ -102,6 +102,31 @@ export const TRUTH_FILES = {
|
|
|
102
102
|
// 就不会出现"派发时写进去了、回头在编辑器里一保存又被悄悄截掉"。
|
|
103
103
|
export const MAX_DEFAULT_PROMPT_CHARS = 4000;
|
|
104
104
|
|
|
105
|
+
// 派发指令正文长度上限。
|
|
106
|
+
//
|
|
107
|
+
// 2026-09-29 从 4000 放宽到 100000:执行侧走的是 claude / opencode / codex,
|
|
108
|
+
// 上下文窗口早已是百万级,而 prompt 是**经 stdin 喂进去**的(见 taskRunner 的
|
|
109
|
+
// launchXxxRun),不受 Windows 命令行 32K 上限约束。4000 是模型上下文还小的时候
|
|
110
|
+
// 定的,现在它卡住的不是模型,是"粘一份完整报错日志 / 一整段需求"这种最普通的用法。
|
|
111
|
+
//
|
|
112
|
+
// 十万字不是"随便多大都行",它同时要落在三份存储里(见下方各自的注释):
|
|
113
|
+
// · tasks.json 的 task.desc —— 一次执行真正用的正文
|
|
114
|
+
// · jobs.json 的 job.prompt —— 保留策略 500 条 / 256MB 兜底
|
|
115
|
+
// · orchestrator.json 的指令流水 —— 200 条上限
|
|
116
|
+
// 真正会让界面卡住的不是这三份,而是**轮询下发**(INSTRUCTION_PREVIEW_CHARS 那道口子)。
|
|
117
|
+
export const MAX_INSTRUCTION_CHARS = 100000;
|
|
118
|
+
|
|
119
|
+
// 指令流水进 5s 轮询 / 活动流时的正文上限。
|
|
120
|
+
//
|
|
121
|
+
// 为什么单独一道:/api/workbench/orchestrator 每 5 秒把 state.instructions 与活动流
|
|
122
|
+
// 整份下发(MAX_ORCHESTRATOR_INSTRUCTIONS = 200 条)。指令放宽到十万字之后,
|
|
123
|
+
// 不截就是 200 × 100000 = 20MB 一轮 —— 界面先卡住,而"卡住"和"上限"毫无关系。
|
|
124
|
+
//
|
|
125
|
+
// 只截**下发**这一份,不截落盘:流水存在的意义就是"我说过什么"能被翻到,
|
|
126
|
+
// 落盘那份必须完整(Agent 侧的真相源清单也指着 orchestrator.json)。
|
|
127
|
+
// 任务正文本身更不截 —— 完整正文在 task.desc 里,流水这条只是同一句话的旁证。
|
|
128
|
+
export const INSTRUCTION_PREVIEW_CHARS = 2000;
|
|
129
|
+
|
|
105
130
|
// 子项目识别 / 文件扫描时需要跳过的目录
|
|
106
131
|
export const SKIP_DIRS = new Set([
|
|
107
132
|
'node_modules', 'dist', 'build', '.next', '.nuxt', '__pycache__',
|
|
@@ -117,12 +142,13 @@ export const MANIFEST_FILES = [
|
|
|
117
142
|
'Gemfile', 'pubspec.yaml'
|
|
118
143
|
];
|
|
119
144
|
|
|
120
|
-
//
|
|
145
|
+
// 附件大小限制(**数量不限**)
|
|
121
146
|
// 单文件 20MB:4K 屏截图(PNG)5–15MB 是常态,卡在 5MB 会让"随手截一张就超"。
|
|
122
147
|
// 真正需要小体积的是图片,由前端上传前压缩保证(见 useWorkbenchAttachments.ts),
|
|
123
148
|
// 这里只做最后一道兜底,避免超大 body 把内存吃光。
|
|
149
|
+
// 数量曾经按 9 个封顶,2026-09-29 按需求放开:一次要交十几张截图 / 一堆日志是常态,
|
|
150
|
+
// 封顶只会逼人分批建任务。数量放开后单文件上限成了唯一的内存关口,所以它必须留着。
|
|
124
151
|
export const MAX_IMAGE_BYTES = 20 * 1024 * 1024; // 单个附件最大 20MB
|
|
125
|
-
export const MAX_ATTACHMENTS_PER_TASK = 9; // 一个任务最多挂 9 个附件
|
|
126
152
|
|
|
127
153
|
// ── 时间 / ID 工具 ─────────────────────────────────────────
|
|
128
154
|
export function nowIso() {
|
|
@@ -105,19 +105,41 @@ export function nextPollIntervalMs({ elapsedMs, fastWindowMs, fastIntervalMs, sl
|
|
|
105
105
|
* 与"EPERM(文件被占)"分不清 —— 两者的处理方式完全相反(一个只能等,一个要杀实例)。
|
|
106
106
|
* 汇总成 `E404 ×8 / ETARGET ×2` 这样一行,下一次事故就不用再靠猜。
|
|
107
107
|
*
|
|
108
|
+
* v2.17.25 实测教训(优先级为什么长这样):registry 还没就绪(E404)的每一轮
|
|
109
|
+
* 输出里都附带 `npm warn cleanup Failed to remove ... EPERM` —— 那是 npm 删旧
|
|
110
|
+
* 全局目录没删干净的动作噪声,不是失败根因。按旧的"EPERM 子串优先"判法被
|
|
111
|
+
* 误判成文件占用,导致每轮反复停实例 + 强删全局目录。所以:
|
|
112
|
+
* ① npm 自己报了 `npm error code <CODE>` 行 → 以它为最终裁决;
|
|
113
|
+
* ② 没有 code 行 → 只在 **error 行**里找文件占用类(warn 行永远不算);
|
|
114
|
+
* ③ 还没有 → 才对全文兜底(兼容只截到 warn cleanup 的残缺输出)。
|
|
115
|
+
*
|
|
108
116
|
* @param {string} output - npm install 的 stdout+stderr
|
|
109
|
-
* @returns {string} E404 | ETARGET | EPERM |
|
|
117
|
+
* @returns {string} E404 | ETARGET | EPERM | ENOTFOUND | ETIMEDOUT | ENETUNREACH | 其他 | npm 原生未识别短码
|
|
110
118
|
*/
|
|
111
119
|
export function classifyInstallError(output) {
|
|
112
120
|
const s = String(output || '')
|
|
113
121
|
if (!s) return '其他'
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
if (
|
|
122
|
+
// ① npm 的裁决码行,如 `npm error code E404`
|
|
123
|
+
const codeLine = s.match(/^\s*npm (?:error|ERR!) code ([A-Z0-9_]+)\b/m)
|
|
124
|
+
if (codeLine) return normalizeErrCode(codeLine[1])
|
|
125
|
+
// ② error 行里的文件占用类(warn 行不算 —— 那是清理噪声)
|
|
126
|
+
const errorLines = s.split(/\r?\n/).filter((l) => /^\s*npm (?:error|ERR!)/.test(l))
|
|
127
|
+
if (errorLines.some((l) => /EPERM|EBUSY|operation not permitted|Failed to remove/i.test(l))) return 'EPERM'
|
|
128
|
+
// ③ 全文兜底:registry 类优先(它是自更新这一段的主题),文件占用类殿后
|
|
117
129
|
if (/\bETARGET\b/.test(s)) return 'ETARGET'
|
|
118
130
|
if (/\bE404\b|404 Not Found/i.test(s)) return 'E404'
|
|
119
131
|
if (/\bENOTFOUND\b/.test(s)) return 'ENOTFOUND'
|
|
120
132
|
if (/\bETIMEDOUT\b|timed out/i.test(s)) return 'ETIMEDOUT'
|
|
121
133
|
if (/\bENETUNREACH\b|ECONNRESET|ECONNREFUSED/i.test(s)) return 'ENETUNREACH'
|
|
134
|
+
if (/EPERM|EBUSY|operation not permitted|Failed to remove/i.test(s)) return 'EPERM'
|
|
122
135
|
return '其他'
|
|
123
136
|
}
|
|
137
|
+
|
|
138
|
+
// npm 原生错误码 → 本脚本的对账短码。EBUSY 与 EPERM 同属"文件被占"一类
|
|
139
|
+
// (处置完全相同:杀实例 + 清目录),合并进同一个桶方便 errorTally 汇总;
|
|
140
|
+
// 没见过的码原样返回,别硬塞 —— 归错桶比归不出来更误导。
|
|
141
|
+
function normalizeErrCode(code) {
|
|
142
|
+
if (code === 'EBUSY') return 'EPERM'
|
|
143
|
+
if (code === 'ECONNRESET' || code === 'ECONNREFUSED') return 'ENETUNREACH'
|
|
144
|
+
return code
|
|
145
|
+
}
|
|
@@ -189,6 +189,24 @@ test('classifyInstallError: EPERM / EBUSY → 文件被占(和"还没就绪"完
|
|
|
189
189
|
assert.equal(classifyInstallError('npm error EBUSY: resource busy or locked'), 'EPERM')
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
+
test('classifyInstallError: npm 的 code 行是最终裁决(v2.17.25 实测回归)', () => {
|
|
193
|
+
// v2.17.25 真实形态:registry 还没就绪(E404)的每一轮都附带 npm warn cleanup EPERM
|
|
194
|
+
// (删旧全局目录的噪声)。旧判法按"EPERM 子串优先"误判成文件占用,
|
|
195
|
+
// 导致每轮反复停实例 + 强删全局目录。有 code 行时必须以它为准。
|
|
196
|
+
const notReady = [
|
|
197
|
+
'npm warn cleanup Failed to remove some directories: EPERM',
|
|
198
|
+
'npm error code E404',
|
|
199
|
+
'npm error 404 Not Found - GET https://registry.npmjs.org/zen-gitsync/-/zen-gitsync-2.17.25.tgz - Not found',
|
|
200
|
+
].join('\n')
|
|
201
|
+
assert.equal(classifyInstallError(notReady), 'E404')
|
|
202
|
+
// npm 自己裁决成文件占用 → 才允许走"杀实例 + 清目录"
|
|
203
|
+
assert.equal(classifyInstallError('npm error code EPERM\nnpm error rename EPERM: operation not permitted'), 'EPERM')
|
|
204
|
+
// EBUSY 与 EPERM 同属"文件被占",归进同一个桶
|
|
205
|
+
assert.equal(classifyInstallError('npm error code EBUSY\nnpm error resource busy or locked'), 'EPERM')
|
|
206
|
+
// 没见过的码原样返回,不硬塞进既有短码
|
|
207
|
+
assert.equal(classifyInstallError('npm error code EAI_AGAIN\nnpm error getaddrinfo EAI_AGAIN'), 'EAI_AGAIN')
|
|
208
|
+
})
|
|
209
|
+
|
|
192
210
|
test('classifyInstallError: 网络类与空输入不炸,各有短码', () => {
|
|
193
211
|
assert.equal(classifyInstallError('npm error code ENOTFOUND registry.npmjs.org'), 'ENOTFOUND')
|
|
194
212
|
assert.equal(classifyInstallError('npm error code ETIMEDOUT'), 'ETIMEDOUT')
|
|
@@ -198,7 +216,8 @@ test('classifyInstallError: 网络类与空输入不炸,各有短码', () => {
|
|
|
198
216
|
assert.equal(classifyInstallError('some unknown failure'), '其他')
|
|
199
217
|
})
|
|
200
218
|
|
|
201
|
-
test('classifyInstallError:
|
|
202
|
-
|
|
219
|
+
test('classifyInstallError: 无 code 行时,error 行里的文件占用类仍优先于 registry 类', () => {
|
|
220
|
+
// 两条都是 error 行、又没有裁决 code 行 → 文件占用是唯一要"动手"的那类,优先报
|
|
221
|
+
const mixed = 'npm error E404 Not Found - GET https://...\nnpm error EPERM: operation not permitted'
|
|
203
222
|
assert.equal(classifyInstallError(mixed), 'EPERM')
|
|
204
223
|
})
|