zen-gitsync 2.18.23 → 2.18.24

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 (46) hide show
  1. package/package.json +5 -2
  2. package/src/cli/ai/tools.js +113 -0
  3. package/src/cli/ai/webSearch.js +260 -0
  4. package/src/config.js +71 -1
  5. package/src/ui/public/assets/{AgentView-DQOp7_6G.js → AgentView-CiWEJ_2v.js} +1 -1
  6. package/src/ui/public/assets/{AppVersionBadge-BBVS3HeA.js → AppVersionBadge-D3IEpIR2.js} +2 -2
  7. package/src/ui/public/assets/{BranchSelector-BwmMgaSS.js → BranchSelector-C0nB2LEE.js} +1 -1
  8. package/src/ui/public/assets/{ChatSilenceBadge-BnaLbCwp.js → ChatSilenceBadge-hb_fJMMg.js} +1 -1
  9. package/src/ui/public/assets/{CommitForm-BtRxJodG.js → CommitForm-CvOOlBZI.js} +1 -1
  10. package/src/ui/public/assets/{CommonDialog-Qg_owAyQ.js → CommonDialog-C0zTwqdL.js} +1 -1
  11. package/src/ui/public/assets/{EditorView-DL3Lj4ho.js → EditorView-BWVtGf7z.js} +1 -1
  12. package/src/ui/public/assets/{FlowExecutionViewer-BsxAtCqd.js → FlowExecutionViewer-ai4_NArk.js} +1 -1
  13. package/src/ui/public/assets/{FlowOrchestrationWorkspace-lcqnsQE-.js → FlowOrchestrationWorkspace-Lhxo8V4t.js} +1 -1
  14. package/src/ui/public/assets/{LogList-CW3-fShz.js → LogList-CX8R6MyZ.js} +1 -1
  15. package/src/ui/public/assets/{MindmapView-ymN6XI7n.js → MindmapView-Cp2j7oyO.js} +1 -1
  16. package/src/ui/public/assets/{MonitorView-C4fFNPb2.js → MonitorView-CdjqaM-T.js} +1 -1
  17. package/src/ui/public/assets/{ProjectManagerView-ph_imDtI.js → ProjectManagerView-wlBbHwVQ.js} +1 -1
  18. package/src/ui/public/assets/{ProjectStartupButton-C2dqnR-6.js → ProjectStartupButton-DlZSyZx4.js} +1 -1
  19. package/src/ui/public/assets/{RemoteManagerDialog-DFkRW2O4.js → RemoteManagerDialog-CcO2xc2Y.js} +1 -1
  20. package/src/ui/public/assets/{RemoteRepoCard-BBnMjldy.js → RemoteRepoCard-DB0iLLQa.js} +1 -1
  21. package/src/ui/public/assets/{SourceMapView-CQcK1acs.js → SourceMapView-DpS3HjJy.js} +1 -1
  22. package/src/ui/public/assets/{SvgIcon-XBkgNKCj.js → SvgIcon-Coxq3TGz.js} +1 -1
  23. package/src/ui/public/assets/{TaskExecutorPicker-B6p57RJY.js → TaskExecutorPicker-CJZD3YVi.js} +1 -1
  24. package/src/ui/public/assets/{UserInputNode-CkXLP8y4.js → UserInputNode-C_znv4Pn.js} +1 -1
  25. package/src/ui/public/assets/WorkbenchView-B0DU80Kl.js +6 -0
  26. package/src/ui/public/assets/{WorkbenchView-MUfM9DER.css → WorkbenchView-SonUoHZE.css} +1 -1
  27. package/src/ui/public/assets/{_plugin-vue_export-helper-B3dZvHQf.js → _plugin-vue_export-helper-PJOlbt_0.js} +4 -4
  28. package/src/ui/public/assets/configStore-ByUSd24J.js +1 -0
  29. package/src/ui/public/assets/index-BTEGKhWc.js +59 -0
  30. package/src/ui/public/assets/index-Q5dnmWKq.css +1 -0
  31. package/src/ui/public/assets/{relativeTime-Ce-gekf_.js → relativeTime-De2wWXk0.js} +1 -1
  32. package/src/ui/public/assets/{taskExecutionExport-Hfww5P9_.js → taskExecutionExport-Bb0HWZiH.js} +1 -1
  33. package/src/ui/public/assets/{toolsStore-oZEAoHnG.js → toolsStore-ChBM3BeQ.js} +1 -1
  34. package/src/ui/public/assets/{useWorkbenchProjects-BifctigH.js → useWorkbenchProjects-CrDCUDV3.js} +1 -1
  35. package/src/ui/public/index.html +7 -7
  36. package/src/ui/server/routes/config.js +14 -0
  37. package/src/ui/server/routes/workbench/dispatchInstruction.js +9 -0
  38. package/src/ui/server/routes/workbench/imageDescribe.js +183 -0
  39. package/src/ui/server/routes/workbench/imageDescribe.test.js +149 -0
  40. package/src/ui/server/routes/workbench/index.js +2 -1
  41. package/src/ui/server/routes/workbench/pdfText.js +22 -5
  42. package/src/ui/server/routes/workbench/taskRunner.js +30 -1
  43. package/src/ui/public/assets/WorkbenchView-cucUmAwl.js +0 -6
  44. package/src/ui/public/assets/configStore-HDlssep1.js +0 -1
  45. package/src/ui/public/assets/index-CAWXBaPa.js +0 -59
  46. package/src/ui/public/assets/index-CigPOqV6.css +0 -1
@@ -1 +1 @@
1
- import{n as e}from"./_plugin-vue_export-helper-B3dZvHQf.js";var t=6e4,n=60*t,r=24*n;function i(i,a=Date.now()){if(!i)return``;let o=new Date(i).getTime();if(Number.isNaN(o))return``;let s=Math.max(0,a-o);return s<t?e(`@WORKBENCH:刚刚`):s<n?e(`@WORKBENCH:N 分钟前`,{n:Math.floor(s/t)}):s<r?e(`@WORKBENCH:N 小时前`,{n:Math.floor(s/n)}):s<30*r?e(`@WORKBENCH:N 天前`,{n:Math.floor(s/r)}):new Date(o).toLocaleDateString()}function a(t){if(typeof t!=`number`||!Number.isFinite(t)||t<0)return``;let n=Math.floor(t/1e3),r=Math.floor(n/3600),i=Math.floor(n%3600/60),a=n%60;return r>0?e(`@WORKBENCH:N 小时 M 分`,{n:r,m:i}):i>0?e(`@WORKBENCH:N 分 M 秒`,{n:i,m:a}):e(`@WORKBENCH:N 秒`,{n:a})}function o(e,t){return e.getFullYear()===t.getFullYear()&&e.getMonth()===t.getMonth()&&e.getDate()===t.getDate()}function s(t,n=Date.now()){if(!t)return``;let r=new Date(t);if(Number.isNaN(r.getTime()))return``;let i=e=>String(e).padStart(2,`0`),a=`${i(r.getHours())}:${i(r.getMinutes())}:${i(r.getSeconds())}`,s=new Date(n);if(o(r,s))return a;if(o(r,new Date(s.getFullYear(),s.getMonth(),s.getDate()-1)))return e(`@WORKBENCH:昨天 {time}`,{time:a});let c=`${r.getMonth()+1}/${r.getDate()}`;return r.getFullYear()===s.getFullYear()?`${c} ${a}`:`${r.getFullYear()}/${c} ${a}`}export{a as n,i as r,s as t};
1
+ import{n as e}from"./_plugin-vue_export-helper-PJOlbt_0.js";var t=6e4,n=60*t,r=24*n;function i(i,a=Date.now()){if(!i)return``;let o=new Date(i).getTime();if(Number.isNaN(o))return``;let s=Math.max(0,a-o);return s<t?e(`@WORKBENCH:刚刚`):s<n?e(`@WORKBENCH:N 分钟前`,{n:Math.floor(s/t)}):s<r?e(`@WORKBENCH:N 小时前`,{n:Math.floor(s/n)}):s<30*r?e(`@WORKBENCH:N 天前`,{n:Math.floor(s/r)}):new Date(o).toLocaleDateString()}function a(t){if(typeof t!=`number`||!Number.isFinite(t)||t<0)return``;let n=Math.floor(t/1e3),r=Math.floor(n/3600),i=Math.floor(n%3600/60),a=n%60;return r>0?e(`@WORKBENCH:N 小时 M 分`,{n:r,m:i}):i>0?e(`@WORKBENCH:N 分 M 秒`,{n:i,m:a}):e(`@WORKBENCH:N 秒`,{n:a})}function o(e,t){return e.getFullYear()===t.getFullYear()&&e.getMonth()===t.getMonth()&&e.getDate()===t.getDate()}function s(t,n=Date.now()){if(!t)return``;let r=new Date(t);if(Number.isNaN(r.getTime()))return``;let i=e=>String(e).padStart(2,`0`),a=`${i(r.getHours())}:${i(r.getMinutes())}:${i(r.getSeconds())}`,s=new Date(n);if(o(r,s))return a;if(o(r,new Date(s.getFullYear(),s.getMonth(),s.getDate()-1)))return e(`@WORKBENCH:昨天 {time}`,{time:a});let c=`${r.getMonth()+1}/${r.getDate()}`;return r.getFullYear()===s.getFullYear()?`${c} ${a}`:`${r.getFullYear()}/${c} ${a}`}export{a as n,i as r,s as t};
@@ -1,4 +1,4 @@
1
- import{In as e,xn as t}from"./element-plus-CN9J6t-d.js";import{n}from"./_plugin-vue_export-helper-B3dZvHQf.js";import{d as r,f as i,p as a,t as o}from"./configStore-HDlssep1.js";import{t as s}from"./toolsStore-oZEAoHnG.js";var c=[`png`,`jpg`,`jpeg`,`gif`,`webp`,`bmp`,`svg`],l=/!\[([^\]]*)\]\(\s*(?:<([^<>\n]+)>|([^()\s]+))(\s+["'][^"']*["'])?\s*\)/g,u=/^\s*(`{3,}|~{3,})/,d=/\.([a-z0-9]+)$/;function f(e){let t=e.trim().toLowerCase().match(d);return!!t&&c.includes(t[1])}function p(e){let t=String(e||``).trim();if(!t)return!1;if(/^[a-zA-Z]:[\\/]/.test(t))return f(t);let n=t.match(/^([a-z][a-z0-9+.-]+):/i);return n&&n[1].toLowerCase()!==`file`||t.startsWith(`//`)||t.startsWith(`/`)?!1:f(t)}function m(e,t){return`/api/workbench/jobs/${encodeURIComponent(e)}/image?path=${encodeURIComponent(t)}`}function h(e,t){return`/api/agent/sessions/${encodeURIComponent(e)}/image?path=${encodeURIComponent(t)}`}function g(e,t){return e.includes(`![`)?e.replace(l,(e,n,r,i,a)=>{let o=(r??i??``).trim();return p(o)?`![${n}](${t(o)}${a||``})`:e}):e}function _(e,t){let n=typeof e==`string`?e:``;if(!n||!t||!n.includes(`![`))return n;let r=n.split(`
1
+ import{In as e,xn as t}from"./element-plus-CN9J6t-d.js";import{n}from"./_plugin-vue_export-helper-PJOlbt_0.js";import{d as r,f as i,p as a,t as o}from"./configStore-ByUSd24J.js";import{t as s}from"./toolsStore-ChBM3BeQ.js";var c=[`png`,`jpg`,`jpeg`,`gif`,`webp`,`bmp`,`svg`],l=/!\[([^\]]*)\]\(\s*(?:<([^<>\n]+)>|([^()\s]+))(\s+["'][^"']*["'])?\s*\)/g,u=/^\s*(`{3,}|~{3,})/,d=/\.([a-z0-9]+)$/;function f(e){let t=e.trim().toLowerCase().match(d);return!!t&&c.includes(t[1])}function p(e){let t=String(e||``).trim();if(!t)return!1;if(/^[a-zA-Z]:[\\/]/.test(t))return f(t);let n=t.match(/^([a-z][a-z0-9+.-]+):/i);return n&&n[1].toLowerCase()!==`file`||t.startsWith(`//`)||t.startsWith(`/`)?!1:f(t)}function m(e,t){return`/api/workbench/jobs/${encodeURIComponent(e)}/image?path=${encodeURIComponent(t)}`}function h(e,t){return`/api/agent/sessions/${encodeURIComponent(e)}/image?path=${encodeURIComponent(t)}`}function g(e,t){return e.includes(`![`)?e.replace(l,(e,n,r,i,a)=>{let o=(r??i??``).trim();return p(o)?`![${n}](${t(o)}${a||``})`:e}):e}function _(e,t){let n=typeof e==`string`?e:``;if(!n||!t||!n.includes(`![`))return n;let r=n.split(`
2
2
  `),i=null,a=[];for(let e of r){let n=e.match(u);if(n){let t=n[1][0];i===null?i=t:i===t&&(i=null),a.push(e);continue}a.push(i===null?g(e,t):e)}return a.join(`
3
3
  `)}function v(e,t){return _(e,t?e=>m(t,e):null)}function y(e,t){return _(e,t?e=>h(t,e):null)}e();function b(){let e=o(),n=s(),a=t({get:()=>e.resolvedTaskExecutor,set:t=>{i(t)&&e.setLastTaskExecutor(t)}}),c=t(()=>({claude:n.claudeAvailable,opencode:n.opencodeAvailable,codex:n.codexAvailable,codebuddy:n.codebuddyAvailable,pi:n.piAvailable,codearts:n.codeartsAvailable})),l=t(()=>r.some(e=>c.value[e.id])),u=t(()=>c.value[a.value]?a.value:Object.keys(c.value).find(e=>c.value[e])??a.value);function d(e){c.value[e]&&(a.value=e)}function f(e){return n.executorModelText(e)}function p(e){return[f(e),n.executorModelDetail(e)].filter(Boolean).join(` · `)}return{selected:a,active:u,availability:c,hasAnyExecutor:l,choose:d,executorModelText:f,executorModelTitle:p}}var x=8192;function S(e,t){return e.length>t?`…(前文已截断)\n${e.slice(-t)}`:e}function C(e,t){let n=e||`done`;return n!==`pending`&&n!==`running`||t===`running`||t===`pending`?n:t===`cancelled`||t===`error`?`error`:`done`}function w(e){let t=e?.toolCalls;if(!Array.isArray(t)||t.length===0)return[];let n=e?.status||`done`,r=[];for(let e=0;e<t.length;e++){let i=t[e];if(!i||typeof i!=`object`)continue;let a=C(i.status,n),o=typeof i.result==`string`?i.result:``;r.push({id:typeof i.id==`string`&&i.id?i.id:`tool-${e}`,name:typeof i.name==`string`?i.name:``,argsPreview:typeof i.argsPreview==`string`?i.argsPreview:``,arguments:typeof i.arguments==`string`?i.arguments:``,result:o?S(o,x):``,status:a,error:typeof i.error==`string`&&i.error?i.error:void 0})}return r}var T=`
4
4
 
@@ -1 +1 @@
1
- import{gr as e,lr as t}from"./element-plus-CN9J6t-d.js";import{S as n}from"./vendor-Bs5KA16r.js";import{n as r}from"./_plugin-vue_export-helper-B3dZvHQf.js";import{d as i,f as a}from"./configStore-HDlssep1.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(!1),m=e(!1),h=e(!1),g=e(null),_=e(!1),v=e(``),y=e({}),b=e({}),x=e({}),S=e({}),C=e(!1),w=0,T=null;function E(e=!1){return!e&&Date.now()-w<3e5?Promise.resolve():T||(C.value=!0,T=(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`&&(S.value=e.latest,w=Date.now())}catch{}finally{C.value=!1,T=null}})(),T)}let D=null;function O(){return D||(_.value=!0,D=(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.codebuddy,d.value=!!e.pi,f.value=!!e.codearts,p.value=!!e.kimi,m.value=!!e.zcode,h.value=!!e.dsh,v.value=typeof e.platform==`string`?e.platform:``,y.value=e.installers&&typeof e.installers==`object`?e.installers:{},x.value=e.desktopApps&&typeof e.desktopApps==`object`?e.desktopApps:{},b.value=e.versions&&typeof e.versions==`object`?e.versions:{},g.value=Date.now())}catch{}finally{_.value=!1,D=null}})(),D)}function k(e){switch(e){case`vscode`:return t.value;case`claude`:return n.value;case`codex`:return c.value;case`opencode`:return l.value;case`codebuddy`:return u.value;case`pi`:return d.value;case`codearts`:return f.value;case`kimi`:return p.value;case`zcode`:return m.value;case`dsh`:return h.value}}function A(e){return x.value[e]}let j=e({}),M=e(!1),N=0,P=null;function F(e){if(!M.value)return{status:`unknown`};if(!a(e))return{status:`unset`};let t=j.value[e];return t?{status:`set`,info:t}:{status:`unset`}}function I(e=!1){return!e&&M.value&&Date.now()-N<6e5?Promise.resolve():P||(P=(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]);j.value=t,M.value=!0,N=Date.now()}}catch{}finally{P=null}})(),P)}function L(e){let t=F(e);return t.status===`unknown`?``:t.status===`set`?t.info.name:r(`@42BB9:未在配置中指定`)}function R(e){let t=F(e);return t.status===`set`?[t.info.source===`state`?r(`@42BB9:CLI 内最近使用`):``,t.info.detail,t.info.provider].filter(Boolean).join(` · `):``}let z=null;function B(){z||=(O(),I(),setInterval(()=>{O(),I()},o))}function V(){z&&=(clearInterval(z),null)}return{vscodeAvailable:t,claudeAvailable:n,codexAvailable:c,opencodeAvailable:l,codebuddyAvailable:u,piAvailable:d,codeartsAvailable:f,kimiAvailable:p,zcodeAvailable:m,dshAvailable:h,lastCheckedAt:g,isChecking:_,platform:v,installers:y,versions:b,latestVersions:S,isFetchingLatest:C,isToolAvailable:k,desktopApps:x,desktopAppOf:A,executorModels:j,executorModelsReady:M,executorModelState:F,executorModelText:L,executorModelDetail:R,fetchExecutorModels:I,checkTools:O,fetchLatestVersions:E,startPolling:B,stopPolling:V}});export{c as t};
1
+ import{gr as e,lr as t}from"./element-plus-CN9J6t-d.js";import{S as n}from"./vendor-Bs5KA16r.js";import{n as r}from"./_plugin-vue_export-helper-PJOlbt_0.js";import{d as i,f as a}from"./configStore-ByUSd24J.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(!1),m=e(!1),h=e(!1),g=e(null),_=e(!1),v=e(``),y=e({}),b=e({}),x=e({}),S=e({}),C=e(!1),w=0,T=null;function E(e=!1){return!e&&Date.now()-w<3e5?Promise.resolve():T||(C.value=!0,T=(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`&&(S.value=e.latest,w=Date.now())}catch{}finally{C.value=!1,T=null}})(),T)}let D=null;function O(){return D||(_.value=!0,D=(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.codebuddy,d.value=!!e.pi,f.value=!!e.codearts,p.value=!!e.kimi,m.value=!!e.zcode,h.value=!!e.dsh,v.value=typeof e.platform==`string`?e.platform:``,y.value=e.installers&&typeof e.installers==`object`?e.installers:{},x.value=e.desktopApps&&typeof e.desktopApps==`object`?e.desktopApps:{},b.value=e.versions&&typeof e.versions==`object`?e.versions:{},g.value=Date.now())}catch{}finally{_.value=!1,D=null}})(),D)}function k(e){switch(e){case`vscode`:return t.value;case`claude`:return n.value;case`codex`:return c.value;case`opencode`:return l.value;case`codebuddy`:return u.value;case`pi`:return d.value;case`codearts`:return f.value;case`kimi`:return p.value;case`zcode`:return m.value;case`dsh`:return h.value}}function A(e){return x.value[e]}let j=e({}),M=e(!1),N=0,P=null;function F(e){if(!M.value)return{status:`unknown`};if(!a(e))return{status:`unset`};let t=j.value[e];return t?{status:`set`,info:t}:{status:`unset`}}function I(e=!1){return!e&&M.value&&Date.now()-N<6e5?Promise.resolve():P||(P=(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]);j.value=t,M.value=!0,N=Date.now()}}catch{}finally{P=null}})(),P)}function L(e){let t=F(e);return t.status===`unknown`?``:t.status===`set`?t.info.name:r(`@42BB9:未在配置中指定`)}function R(e){let t=F(e);return t.status===`set`?[t.info.source===`state`?r(`@42BB9:CLI 内最近使用`):``,t.info.detail,t.info.provider].filter(Boolean).join(` · `):``}let z=null;function B(){z||=(O(),I(),setInterval(()=>{O(),I()},o))}function V(){z&&=(clearInterval(z),null)}return{vscodeAvailable:t,claudeAvailable:n,codexAvailable:c,opencodeAvailable:l,codebuddyAvailable:u,piAvailable:d,codeartsAvailable:f,kimiAvailable:p,zcodeAvailable:m,dshAvailable:h,lastCheckedAt:g,isChecking:_,platform:v,installers:y,versions:b,latestVersions:S,isFetchingLatest:C,isToolAvailable:k,desktopApps:x,desktopAppOf:A,executorModels:j,executorModelsReady:M,executorModelState:F,executorModelText:L,executorModelDetail:R,fetchExecutorModels:I,checkTools:O,fetchLatestVersions:E,startPolling:B,stopPolling:V}});export{c as t};
@@ -1 +1 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{An as e,Ar as t,Cn as n,In as r,J as i,Jr as a,Lt as o,Mn as s,Nt as ee,Sn as c,Sr as l,Tn as u,Ur as d,Yn as f,_n as p,gn as m,gr as h,hn as g,i as _,ir as v,lr as y,on as b,qn as x,st as S,wn as C,xn as te,xt as ne}from"./element-plus-CN9J6t-d.js";import{n as w,t as T}from"./_plugin-vue_export-helper-B3dZvHQf.js";import{t as E}from"./SvgIcon-XBkgNKCj.js";import{t as D}from"./toolsStore-oZEAoHnG.js";import{$ as re,G as O,J as ie,K as k,Q as ae,U as A,W as oe,X as se,Y as j,Z as ce,et as M,nt as le,q as ue,tt as N}from"./index-CAWXBaPa.js";function P(){let e=D();function t(t){return e.lastCheckedAt!==null&&!e.isToolAvailable(t)}async function n(t,n,r){if(!n)return`no-dir`;if(!await j())return _.warning(w(`@67CE7:工具检测失败,请稍后重试`)),`failed`;if(!e.isToolAvailable(t))return`install`;let i=await M(t,n,r);return i.success?(_.success(i.message||w(`@67CE7:已用 {tool} 打开目录`,{tool:k[t]})),`opened`):(_.error(`${w(`@67CE7:打开失败: `)}${i.error??``}`),`failed`)}return{toolMissing:t,tryOpenWithTool:n}}r(),y(),t(),b();var F=[`aria-label`],I=[`onKeydown`],L={class:`proj-menu__icon`},R={class:`proj-menu__label`},z=[`onKeydown`],B={class:`proj-menu__icon`},V={class:`proj-menu__label`},H=[`onKeydown`],U={class:`proj-menu__icon`},de={class:`proj-menu__label`},fe=[`onKeydown`],pe={class:`proj-menu__icon`},me={class:`proj-menu__label`},he=[`onKeydown`],ge={class:`proj-menu__icon`},_e={class:`proj-menu__label`},ve={class:`proj-menu__title`,role:`presentation`},ye=[`onClick`,`onKeydown`],be={class:`proj-menu__icon`},xe={class:`proj-menu__label`},Se={key:0,class:`proj-menu__hint`},Ce={class:`proj-menu__icon`},we=[`src`],Te={class:`proj-menu__label`},Ee={class:`proj-menu__hint`},De={class:`proj-menu__icon`},Oe=[`src`],ke={class:`proj-menu__label`},Ae={class:`proj-menu__icon`},je={class:`proj-menu__label`},Me={class:`proj-menu__icon`},Ne={class:`proj-menu__label`},Pe={key:0,class:`proj-menu__hint`},Fe={key:1,class:`proj-menu__hint`},Ie={class:`proj-menu__icon`},Le={class:`proj-menu__label`},Re={class:`proj-menu__icon`},ze={class:`proj-menu__label`},Be={class:`proj-menu__icon`},Ve={class:`proj-menu__label`},He={class:`proj-menu__title`,role:`presentation`},Ue=[`onClick`,`onKeydown`],W={class:`proj-menu__icon`},We=[`src`],Ge={class:`proj-menu__label`},Ke={class:`proj-menu__hint`},G=T(s({__name:`ProjectOpenMenuList`,props:{path:{},name:{}},emits:[`done`],setup(t,{emit:r}){let s=t,y=r,{toolMissing:b,tryOpenWithTool:T}=P(),j=D(),M=h(!1),G=h(null),K=te(()=>oe.filter(e=>j.desktopAppOf(e)).map(e=>({id:e,name:k[e],icon:O.find(t=>t.id===e)?.icon??``})));async function q(e){let t=j.desktopAppOf(e);if(!t)return;if(y(`done`),!t.installed){window.open(t.downloadUrl,`_blank`,`noopener,noreferrer`);return}let n=await ce(e);n.success?_.success(n.message||w(`@67CE7:已打开 {app}`,{app:t.name})):_.error(n.error||w(`@WORKBENCH:打开桌面端失败`))}async function J(){y(`done`);let e=await ae(s.path);e.success?_.success(e.message||w(`@WORKBENCH:已在文件管理器中打开文件夹`)):_.error(e.error||w(`@WORKBENCH:打开文件夹失败`))}async function Y(){y(`done`);let e=await re(s.path);e.success?_.success(e.message||w(`@67CE7:已在终端中打开目录`)):_.error(e.error||w(`@WORKBENCH:打开终端失败`))}async function X(){y(`done`);let e=await se(s.path);e.success?_.success(w(`@WORKBENCH:已在新标签页启动 g ui`)):_.error(e.error||w(`@WORKBENCH:启动 g ui 失败`))}async function Z(){if(y(`done`),s.name)try{await navigator.clipboard.writeText(s.name),_.success(w(`@67CE7:已复制文件夹名称`))}catch{_.error(w(`@67CE7:复制失败`))}}async function Q(){if(y(`done`),s.path)try{await navigator.clipboard.writeText(s.path),_.success(w(`@67CE7:已复制文件夹路径`))}catch{_.error(w(`@67CE7:复制失败`))}}async function $(e,t){if(await T(e,s.path,t)===`install`){G.value=e,M.value=!0;return}y(`done`)}return(t,r)=>{let s=i;return x(),u(p,null,[c(`ul`,{class:`proj-menu`,role:`menu`,"aria-label":l(w)(`@WORKBENCH:打开方式`)},[c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:J,onKeydown:[g(m(J,[`prevent`]),[`enter`]),g(m(J,[`prevent`]),[`space`])]},[c(`span`,L,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(ne))]),_:1})]),c(`span`,R,a(l(w)(`@WORKBENCH:在文件管理器中打开`)),1)],40,I),c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:Y,onKeydown:[g(m(Y,[`prevent`]),[`enter`]),g(m(Y,[`prevent`]),[`space`])]},[c(`span`,B,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(ee))]),_:1})]),c(`span`,V,a(l(w)(`@WORKBENCH:在终端中打开`)),1)],40,z),c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:X,onKeydown:[g(m(X,[`prevent`]),[`enter`]),g(m(X,[`prevent`]),[`space`])]},[c(`span`,U,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(o))]),_:1})]),c(`span`,de,a(l(w)(`@WORKBENCH:在新标签页启动 g ui`)),1)],40,H),c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:Z,onKeydown:[g(m(Z,[`prevent`]),[`enter`]),g(m(Z,[`prevent`]),[`space`])]},[c(`span`,pe,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(S))]),_:1})]),c(`span`,me,a(l(w)(`@67CE7:复制文件夹名称`)),1)],40,fe),c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:Q,onKeydown:[g(m(Q,[`prevent`]),[`enter`]),g(m(Q,[`prevent`]),[`space`])]},[c(`span`,ge,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(S))]),_:1})]),c(`span`,_e,a(l(w)(`@67CE7:复制文件夹路径`)),1)],40,he),r[23]||=c(`li`,{class:`proj-menu__sep`,role:`separator`},null,-1),c(`li`,ve,a(l(w)(`@WORKBENCH:用工具打开`)),1),(x(!0),u(p,null,f(l(O),t=>(x(),u(`li`,{key:t.id,class:d([`proj-menu__item`,{"is-missing":l(b)(t.id)}]),role:`menuitem`,tabindex:`-1`,onClick:e=>$(t.id),onKeydown:[g(m(e=>$(t.id),[`prevent`]),[`enter`]),g(m(e=>$(t.id),[`prevent`]),[`space`])]},[c(`span`,be,[e(E,{"icon-class":t.icon,class:d({"icon-invert-dark":t.id===`codex`,"icon-outline-dark":l(A).has(t.id)})},null,8,[`icon-class`,`class`])]),c(`span`,xe,a(l(w)(t.labelKey)),1),l(b)(t.id)?(x(),u(`span`,Se,a(l(w)(`@67CE7:未安装`)),1)):C(``,!0)],42,ye))),128)),c(`li`,{class:d([`proj-menu__item`,{"is-missing":l(b)(`claude`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[0]||=e=>$(`claude`),onKeydown:[r[1]||=g(m(e=>$(`claude`),[`prevent`]),[`enter`]),r[2]||=g(m(e=>$(`claude`),[`prevent`]),[`space`])]},[c(`span`,Ce,[c(`img`,{src:l(N),alt:``,class:`proj-menu__img`},null,8,we)]),c(`span`,Te,a(l(w)(`@67CE7:用 Claude Code 打开`)),1),c(`span`,Ee,a(l(b)(`claude`)?l(w)(`@67CE7:未安装`):l(w)(`@67CE7:默认权限`)),1)],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`claude`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[3]||=e=>$(`claude`,`bypassPermissions`),onKeydown:[r[4]||=g(m(e=>$(`claude`,`bypassPermissions`),[`prevent`]),[`enter`]),r[5]||=g(m(e=>$(`claude`,`bypassPermissions`),[`prevent`]),[`space`])]},[c(`span`,De,[c(`img`,{src:l(N),alt:``,class:`proj-menu__img`},null,8,Oe)]),c(`span`,ke,a(l(w)(`@67CE7:用 Claude Code 打开(完全批准)`)),1)],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`opencode`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[6]||=e=>$(`opencode`,`auto`),onKeydown:[r[7]||=g(m(e=>$(`opencode`,`auto`),[`prevent`]),[`enter`]),r[8]||=g(m(e=>$(`opencode`,`auto`),[`prevent`]),[`space`])]},[c(`span`,Ae,[e(E,{"icon-class":`opencode`})]),c(`span`,je,a(l(w)(`@67CE7:用 OpenCode 打开(完全批准)`)),1)],34),c(`li`,{class:d([`proj-menu__item`,{"is-missing":l(b)(`codex`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[9]||=e=>$(`codex`,`sandboxed`),onKeydown:[r[10]||=g(m(e=>$(`codex`,`sandboxed`),[`prevent`]),[`enter`]),r[11]||=g(m(e=>$(`codex`,`sandboxed`),[`prevent`]),[`space`])]},[c(`span`,Me,[e(E,{"icon-class":`codex`,class:`icon-invert-dark`})]),c(`span`,Ne,a(l(w)(`@67CE7:用 Codex 打开(自动批准)`)),1),l(b)(`codex`)?(x(),u(`span`,Fe,a(l(w)(`@67CE7:未安装`)),1)):(x(),u(`span`,Pe,a(l(w)(`@67CE7:沙箱内`)),1))],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`codex`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[12]||=e=>$(`codex`,`bypass`),onKeydown:[r[13]||=g(m(e=>$(`codex`,`bypass`),[`prevent`]),[`enter`]),r[14]||=g(m(e=>$(`codex`,`bypass`),[`prevent`]),[`space`])]},[c(`span`,Ie,[e(E,{"icon-class":`codex`,class:`icon-invert-dark`})]),c(`span`,Le,a(l(w)(`@67CE7:用 Codex 打开(完全批准)`)),1)],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`codebuddy`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[15]||=e=>$(`codebuddy`,`bypassPermissions`),onKeydown:[r[16]||=g(m(e=>$(`codebuddy`,`bypassPermissions`),[`prevent`]),[`enter`]),r[17]||=g(m(e=>$(`codebuddy`,`bypassPermissions`),[`prevent`]),[`space`])]},[c(`span`,Re,[e(E,{"icon-class":`codebuddy`})]),c(`span`,ze,a(l(w)(`@67CE7:用 CodeBuddy Code 打开(完全批准)`)),1)],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`kimi`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[18]||=e=>$(`kimi`,`auto`),onKeydown:[r[19]||=g(m(e=>$(`kimi`,`auto`),[`prevent`]),[`enter`]),r[20]||=g(m(e=>$(`kimi`,`auto`),[`prevent`]),[`space`])]},[c(`span`,Be,[e(E,{"icon-class":`kimi`})]),c(`span`,Ve,a(l(w)(`@67CE7:用 Kimi Code 打开(完全批准)`)),1)],34),K.value.length?(x(),u(p,{key:0},[r[22]||=c(`li`,{class:`proj-menu__sep`,role:`separator`},null,-1),c(`li`,He,a(l(w)(`@WORKBENCH:桌面端`)),1),(x(!0),u(p,null,f(K.value,e=>(x(),u(`li`,{key:`desktop-${e.id}`,class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:t=>q(e.id),onKeydown:[g(m(t=>q(e.id),[`prevent`]),[`enter`]),g(m(t=>q(e.id),[`prevent`]),[`space`])]},[c(`span`,W,[e.id===`claude`?(x(),u(`img`,{key:0,src:l(N),alt:``,class:`proj-menu__img`},null,8,We)):(x(),n(E,{key:1,"icon-class":e.icon,class:d({"icon-invert-dark":e.id===`codex`,"icon-outline-dark":l(A).has(e.id)})},null,8,[`icon-class`,`class`]))]),c(`span`,Ge,a(l(ie)(l(j).desktopAppOf(e.id))),1),c(`span`,Ke,a(l(ue)(l(j).desktopAppOf(e.id))),1)],40,Ue))),128))],64)):C(``,!0)],8,F),e(le,{modelValue:M.value,"onUpdate:modelValue":r[21]||=e=>M.value=e,tool:G.value},null,8,[`modelValue`,`tool`])],64)}}}),[[`__scopeId`,`data-v-80fde3fe`]]);y();function K(){let e=h([]),t=h([]),n=h(``),r=h(!1),i=h(!1);async function a(a=!1){a||(r.value=!0);try{let r=await fetch(`/api/workbench/projects`,{cache:`no-store`}).then(e=>e.json());return r?.success?(e.value=Array.isArray(r.projects)?r.projects:[],t.value=Array.isArray(r.tasks)?r.tasks:[],n.value=r.currentProjectPath||``,i.value=!0,!0):(a||_.error(r?.error||w(`@WORKBENCH:读取项目列表失败`)),!1)}catch(e){return a||_.error(w(`@WORKBENCH:网络错误: `)+(e?.message||e)),!1}finally{r.value=!1}}async function o(e){if(!e)return{ok:!1,keptTasks:0};try{let t=await fetch(`/api/workbench/projects/remove`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({path:e})}).then(e=>e.json()).catch(()=>null);return t?.success?{ok:!0,keptTasks:Number.isFinite(t.keptTasks)?t.keptTasks:0}:(_.error(t?.error||w(`@WORKBENCH:移除项目失败`)),{ok:!1,keptTasks:0})}catch(e){return _.error(w(`@WORKBENCH:网络错误: `)+(e?.message||e)),{ok:!1,keptTasks:0}}}async function s(e){if(!e)return{ok:!1,alreadyKnown:!1};try{let t=await fetch(`/api/workbench/projects/add`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({path:e})}).then(e=>e.json()).catch(()=>null);return t?.success?{ok:!0,alreadyKnown:!!t.alreadyKnown}:(_.error(t?.error||w(`@PROJM:添加项目失败`)),{ok:!1,alreadyKnown:!1})}catch(e){return _.error(w(`@WORKBENCH:网络错误: `)+(e?.message||e)),{ok:!1,alreadyKnown:!1}}}return{projects:e,boardTasks:t,currentProjectPath:n,loading:r,loaded:i,loadProjects:a,removeProject:o,addProject:s}}export{G as n,K as t};
1
+ import"./rolldown-runtime-C0FnF6B9.js";import{An as e,Ar as t,Cn as n,In as r,J as i,Jr as a,Lt as o,Mn as s,Nt as ee,Sn as c,Sr as l,Tn as u,Ur as d,Yn as f,_n as p,gn as m,gr as h,hn as g,i as _,ir as v,lr as y,on as b,qn as x,st as S,wn as C,xn as te,xt as ne}from"./element-plus-CN9J6t-d.js";import{n as w,t as T}from"./_plugin-vue_export-helper-PJOlbt_0.js";import{t as E}from"./SvgIcon-Coxq3TGz.js";import{t as D}from"./toolsStore-ChBM3BeQ.js";import{$ as re,G as O,J as ie,K as k,Q as ae,U as A,W as oe,X as se,Y as j,Z as ce,et as M,nt as le,q as ue,tt as N}from"./index-BTEGKhWc.js";function P(){let e=D();function t(t){return e.lastCheckedAt!==null&&!e.isToolAvailable(t)}async function n(t,n,r){if(!n)return`no-dir`;if(!await j())return _.warning(w(`@67CE7:工具检测失败,请稍后重试`)),`failed`;if(!e.isToolAvailable(t))return`install`;let i=await M(t,n,r);return i.success?(_.success(i.message||w(`@67CE7:已用 {tool} 打开目录`,{tool:k[t]})),`opened`):(_.error(`${w(`@67CE7:打开失败: `)}${i.error??``}`),`failed`)}return{toolMissing:t,tryOpenWithTool:n}}r(),y(),t(),b();var F=[`aria-label`],I=[`onKeydown`],L={class:`proj-menu__icon`},R={class:`proj-menu__label`},z=[`onKeydown`],B={class:`proj-menu__icon`},V={class:`proj-menu__label`},H=[`onKeydown`],U={class:`proj-menu__icon`},de={class:`proj-menu__label`},fe=[`onKeydown`],pe={class:`proj-menu__icon`},me={class:`proj-menu__label`},he=[`onKeydown`],ge={class:`proj-menu__icon`},_e={class:`proj-menu__label`},ve={class:`proj-menu__title`,role:`presentation`},ye=[`onClick`,`onKeydown`],be={class:`proj-menu__icon`},xe={class:`proj-menu__label`},Se={key:0,class:`proj-menu__hint`},Ce={class:`proj-menu__icon`},we=[`src`],Te={class:`proj-menu__label`},Ee={class:`proj-menu__hint`},De={class:`proj-menu__icon`},Oe=[`src`],ke={class:`proj-menu__label`},Ae={class:`proj-menu__icon`},je={class:`proj-menu__label`},Me={class:`proj-menu__icon`},Ne={class:`proj-menu__label`},Pe={key:0,class:`proj-menu__hint`},Fe={key:1,class:`proj-menu__hint`},Ie={class:`proj-menu__icon`},Le={class:`proj-menu__label`},Re={class:`proj-menu__icon`},ze={class:`proj-menu__label`},Be={class:`proj-menu__icon`},Ve={class:`proj-menu__label`},He={class:`proj-menu__title`,role:`presentation`},Ue=[`onClick`,`onKeydown`],W={class:`proj-menu__icon`},We=[`src`],Ge={class:`proj-menu__label`},Ke={class:`proj-menu__hint`},G=T(s({__name:`ProjectOpenMenuList`,props:{path:{},name:{}},emits:[`done`],setup(t,{emit:r}){let s=t,y=r,{toolMissing:b,tryOpenWithTool:T}=P(),j=D(),M=h(!1),G=h(null),K=te(()=>oe.filter(e=>j.desktopAppOf(e)).map(e=>({id:e,name:k[e],icon:O.find(t=>t.id===e)?.icon??``})));async function q(e){let t=j.desktopAppOf(e);if(!t)return;if(y(`done`),!t.installed){window.open(t.downloadUrl,`_blank`,`noopener,noreferrer`);return}let n=await ce(e);n.success?_.success(n.message||w(`@67CE7:已打开 {app}`,{app:t.name})):_.error(n.error||w(`@WORKBENCH:打开桌面端失败`))}async function J(){y(`done`);let e=await ae(s.path);e.success?_.success(e.message||w(`@WORKBENCH:已在文件管理器中打开文件夹`)):_.error(e.error||w(`@WORKBENCH:打开文件夹失败`))}async function Y(){y(`done`);let e=await re(s.path);e.success?_.success(e.message||w(`@67CE7:已在终端中打开目录`)):_.error(e.error||w(`@WORKBENCH:打开终端失败`))}async function X(){y(`done`);let e=await se(s.path);e.success?_.success(w(`@WORKBENCH:已在新标签页启动 g ui`)):_.error(e.error||w(`@WORKBENCH:启动 g ui 失败`))}async function Z(){if(y(`done`),s.name)try{await navigator.clipboard.writeText(s.name),_.success(w(`@67CE7:已复制文件夹名称`))}catch{_.error(w(`@67CE7:复制失败`))}}async function Q(){if(y(`done`),s.path)try{await navigator.clipboard.writeText(s.path),_.success(w(`@67CE7:已复制文件夹路径`))}catch{_.error(w(`@67CE7:复制失败`))}}async function $(e,t){if(await T(e,s.path,t)===`install`){G.value=e,M.value=!0;return}y(`done`)}return(t,r)=>{let s=i;return x(),u(p,null,[c(`ul`,{class:`proj-menu`,role:`menu`,"aria-label":l(w)(`@WORKBENCH:打开方式`)},[c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:J,onKeydown:[g(m(J,[`prevent`]),[`enter`]),g(m(J,[`prevent`]),[`space`])]},[c(`span`,L,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(ne))]),_:1})]),c(`span`,R,a(l(w)(`@WORKBENCH:在文件管理器中打开`)),1)],40,I),c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:Y,onKeydown:[g(m(Y,[`prevent`]),[`enter`]),g(m(Y,[`prevent`]),[`space`])]},[c(`span`,B,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(ee))]),_:1})]),c(`span`,V,a(l(w)(`@WORKBENCH:在终端中打开`)),1)],40,z),c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:X,onKeydown:[g(m(X,[`prevent`]),[`enter`]),g(m(X,[`prevent`]),[`space`])]},[c(`span`,U,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(o))]),_:1})]),c(`span`,de,a(l(w)(`@WORKBENCH:在新标签页启动 g ui`)),1)],40,H),c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:Z,onKeydown:[g(m(Z,[`prevent`]),[`enter`]),g(m(Z,[`prevent`]),[`space`])]},[c(`span`,pe,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(S))]),_:1})]),c(`span`,me,a(l(w)(`@67CE7:复制文件夹名称`)),1)],40,fe),c(`li`,{class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:Q,onKeydown:[g(m(Q,[`prevent`]),[`enter`]),g(m(Q,[`prevent`]),[`space`])]},[c(`span`,ge,[e(s,{"aria-hidden":`true`},{default:v(()=>[e(l(S))]),_:1})]),c(`span`,_e,a(l(w)(`@67CE7:复制文件夹路径`)),1)],40,he),r[23]||=c(`li`,{class:`proj-menu__sep`,role:`separator`},null,-1),c(`li`,ve,a(l(w)(`@WORKBENCH:用工具打开`)),1),(x(!0),u(p,null,f(l(O),t=>(x(),u(`li`,{key:t.id,class:d([`proj-menu__item`,{"is-missing":l(b)(t.id)}]),role:`menuitem`,tabindex:`-1`,onClick:e=>$(t.id),onKeydown:[g(m(e=>$(t.id),[`prevent`]),[`enter`]),g(m(e=>$(t.id),[`prevent`]),[`space`])]},[c(`span`,be,[e(E,{"icon-class":t.icon,class:d({"icon-invert-dark":t.id===`codex`,"icon-outline-dark":l(A).has(t.id)})},null,8,[`icon-class`,`class`])]),c(`span`,xe,a(l(w)(t.labelKey)),1),l(b)(t.id)?(x(),u(`span`,Se,a(l(w)(`@67CE7:未安装`)),1)):C(``,!0)],42,ye))),128)),c(`li`,{class:d([`proj-menu__item`,{"is-missing":l(b)(`claude`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[0]||=e=>$(`claude`),onKeydown:[r[1]||=g(m(e=>$(`claude`),[`prevent`]),[`enter`]),r[2]||=g(m(e=>$(`claude`),[`prevent`]),[`space`])]},[c(`span`,Ce,[c(`img`,{src:l(N),alt:``,class:`proj-menu__img`},null,8,we)]),c(`span`,Te,a(l(w)(`@67CE7:用 Claude Code 打开`)),1),c(`span`,Ee,a(l(b)(`claude`)?l(w)(`@67CE7:未安装`):l(w)(`@67CE7:默认权限`)),1)],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`claude`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[3]||=e=>$(`claude`,`bypassPermissions`),onKeydown:[r[4]||=g(m(e=>$(`claude`,`bypassPermissions`),[`prevent`]),[`enter`]),r[5]||=g(m(e=>$(`claude`,`bypassPermissions`),[`prevent`]),[`space`])]},[c(`span`,De,[c(`img`,{src:l(N),alt:``,class:`proj-menu__img`},null,8,Oe)]),c(`span`,ke,a(l(w)(`@67CE7:用 Claude Code 打开(完全批准)`)),1)],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`opencode`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[6]||=e=>$(`opencode`,`auto`),onKeydown:[r[7]||=g(m(e=>$(`opencode`,`auto`),[`prevent`]),[`enter`]),r[8]||=g(m(e=>$(`opencode`,`auto`),[`prevent`]),[`space`])]},[c(`span`,Ae,[e(E,{"icon-class":`opencode`})]),c(`span`,je,a(l(w)(`@67CE7:用 OpenCode 打开(完全批准)`)),1)],34),c(`li`,{class:d([`proj-menu__item`,{"is-missing":l(b)(`codex`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[9]||=e=>$(`codex`,`sandboxed`),onKeydown:[r[10]||=g(m(e=>$(`codex`,`sandboxed`),[`prevent`]),[`enter`]),r[11]||=g(m(e=>$(`codex`,`sandboxed`),[`prevent`]),[`space`])]},[c(`span`,Me,[e(E,{"icon-class":`codex`,class:`icon-invert-dark`})]),c(`span`,Ne,a(l(w)(`@67CE7:用 Codex 打开(自动批准)`)),1),l(b)(`codex`)?(x(),u(`span`,Fe,a(l(w)(`@67CE7:未安装`)),1)):(x(),u(`span`,Pe,a(l(w)(`@67CE7:沙箱内`)),1))],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`codex`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[12]||=e=>$(`codex`,`bypass`),onKeydown:[r[13]||=g(m(e=>$(`codex`,`bypass`),[`prevent`]),[`enter`]),r[14]||=g(m(e=>$(`codex`,`bypass`),[`prevent`]),[`space`])]},[c(`span`,Ie,[e(E,{"icon-class":`codex`,class:`icon-invert-dark`})]),c(`span`,Le,a(l(w)(`@67CE7:用 Codex 打开(完全批准)`)),1)],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`codebuddy`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[15]||=e=>$(`codebuddy`,`bypassPermissions`),onKeydown:[r[16]||=g(m(e=>$(`codebuddy`,`bypassPermissions`),[`prevent`]),[`enter`]),r[17]||=g(m(e=>$(`codebuddy`,`bypassPermissions`),[`prevent`]),[`space`])]},[c(`span`,Re,[e(E,{"icon-class":`codebuddy`})]),c(`span`,ze,a(l(w)(`@67CE7:用 CodeBuddy Code 打开(完全批准)`)),1)],34),c(`li`,{class:d([`proj-menu__item proj-menu__item--danger`,{"is-missing":l(b)(`kimi`)}]),role:`menuitem`,tabindex:`-1`,onClick:r[18]||=e=>$(`kimi`,`auto`),onKeydown:[r[19]||=g(m(e=>$(`kimi`,`auto`),[`prevent`]),[`enter`]),r[20]||=g(m(e=>$(`kimi`,`auto`),[`prevent`]),[`space`])]},[c(`span`,Be,[e(E,{"icon-class":`kimi`})]),c(`span`,Ve,a(l(w)(`@67CE7:用 Kimi Code 打开(完全批准)`)),1)],34),K.value.length?(x(),u(p,{key:0},[r[22]||=c(`li`,{class:`proj-menu__sep`,role:`separator`},null,-1),c(`li`,He,a(l(w)(`@WORKBENCH:桌面端`)),1),(x(!0),u(p,null,f(K.value,e=>(x(),u(`li`,{key:`desktop-${e.id}`,class:`proj-menu__item`,role:`menuitem`,tabindex:`-1`,onClick:t=>q(e.id),onKeydown:[g(m(t=>q(e.id),[`prevent`]),[`enter`]),g(m(t=>q(e.id),[`prevent`]),[`space`])]},[c(`span`,W,[e.id===`claude`?(x(),u(`img`,{key:0,src:l(N),alt:``,class:`proj-menu__img`},null,8,We)):(x(),n(E,{key:1,"icon-class":e.icon,class:d({"icon-invert-dark":e.id===`codex`,"icon-outline-dark":l(A).has(e.id)})},null,8,[`icon-class`,`class`]))]),c(`span`,Ge,a(l(ie)(l(j).desktopAppOf(e.id))),1),c(`span`,Ke,a(l(ue)(l(j).desktopAppOf(e.id))),1)],40,Ue))),128))],64)):C(``,!0)],8,F),e(le,{modelValue:M.value,"onUpdate:modelValue":r[21]||=e=>M.value=e,tool:G.value},null,8,[`modelValue`,`tool`])],64)}}}),[[`__scopeId`,`data-v-80fde3fe`]]);y();function K(){let e=h([]),t=h([]),n=h(``),r=h(!1),i=h(!1);async function a(a=!1){a||(r.value=!0);try{let r=await fetch(`/api/workbench/projects`,{cache:`no-store`}).then(e=>e.json());return r?.success?(e.value=Array.isArray(r.projects)?r.projects:[],t.value=Array.isArray(r.tasks)?r.tasks:[],n.value=r.currentProjectPath||``,i.value=!0,!0):(a||_.error(r?.error||w(`@WORKBENCH:读取项目列表失败`)),!1)}catch(e){return a||_.error(w(`@WORKBENCH:网络错误: `)+(e?.message||e)),!1}finally{r.value=!1}}async function o(e){if(!e)return{ok:!1,keptTasks:0};try{let t=await fetch(`/api/workbench/projects/remove`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({path:e})}).then(e=>e.json()).catch(()=>null);return t?.success?{ok:!0,keptTasks:Number.isFinite(t.keptTasks)?t.keptTasks:0}:(_.error(t?.error||w(`@WORKBENCH:移除项目失败`)),{ok:!1,keptTasks:0})}catch(e){return _.error(w(`@WORKBENCH:网络错误: `)+(e?.message||e)),{ok:!1,keptTasks:0}}}async function s(e){if(!e)return{ok:!1,alreadyKnown:!1};try{let t=await fetch(`/api/workbench/projects/add`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({path:e})}).then(e=>e.json()).catch(()=>null);return t?.success?{ok:!0,alreadyKnown:!!t.alreadyKnown}:(_.error(t?.error||w(`@PROJM:添加项目失败`)),{ok:!1,alreadyKnown:!1})}catch(e){return _.error(w(`@WORKBENCH:网络错误: `)+(e?.message||e)),{ok:!1,alreadyKnown:!1}}}return{projects:e,boardTasks:t,currentProjectPath:n,loading:r,loaded:i,loadProjects:a,removeProject:o,addProject:s}}export{G as n,K as t};
@@ -10,19 +10,19 @@
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-CAWXBaPa.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-BTEGKhWc.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/dagre-JTV5zPzI.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/element-plus-CN9J6t-d.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/vendor-Bs5KA16r.js">
18
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-B3dZvHQf.js">
19
- <link rel="modulepreload" crossorigin href="/assets/SvgIcon-XBkgNKCj.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-PJOlbt_0.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/SvgIcon-Coxq3TGz.js">
20
20
  <link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
21
- <link rel="modulepreload" crossorigin href="/assets/configStore-HDlssep1.js">
22
- <link rel="modulepreload" crossorigin href="/assets/toolsStore-oZEAoHnG.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/configStore-ByUSd24J.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/toolsStore-ChBM3BeQ.js">
23
23
  <link rel="modulepreload" crossorigin href="/assets/monaco-irXl5Eu9.js">
24
24
  <link rel="modulepreload" crossorigin href="/assets/flow-mindmap-CyZrdnd4.js">
25
- <link rel="modulepreload" crossorigin href="/assets/CommonDialog-Qg_owAyQ.js">
25
+ <link rel="modulepreload" crossorigin href="/assets/CommonDialog-C0zTwqdL.js">
26
26
  <link rel="modulepreload" crossorigin href="/assets/vue-flow-NLvet82_.js">
27
27
  <link rel="stylesheet" crossorigin href="/assets/element-plus-D-6gla1z.css">
28
28
  <link rel="stylesheet" crossorigin href="/assets/vendor-CAhoE06w.css">
@@ -31,7 +31,7 @@
31
31
  <link rel="stylesheet" crossorigin href="/assets/flow-mindmap-BwYC4xxV.css">
32
32
  <link rel="stylesheet" crossorigin href="/assets/CommonDialog-Bzi5ISqT.css">
33
33
  <link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
34
- <link rel="stylesheet" crossorigin href="/assets/index-CigPOqV6.css">
34
+ <link rel="stylesheet" crossorigin href="/assets/index-Q5dnmWKq.css">
35
35
  </head>
36
36
  <body>
37
37
  <div id="app"></div>
@@ -1164,6 +1164,20 @@ export function registerConfigRoutes({
1164
1164
  }
1165
1165
  updates.agentDefaultPrompt = normalized
1166
1166
  }
1167
+ // 联网搜索配置(全局):{ enabled, provider, apiKeys: {provider: key}, proxy }。
1168
+ // 结构坏(非普通对象)整体拒绝(400),与 normalizeWebSearch 语义一致;
1169
+ // 单个字段坏由 normalizeWebSearch 内部各自回落,不整组拒绝。
1170
+ // 回执里 updates.webSearch 是归一化后的最终值,前端以回执为准。
1171
+ if (body.webSearch !== undefined) {
1172
+ const normalized = configManager.normalizeWebSearch(body.webSearch)
1173
+ if (normalized === null) {
1174
+ return res.status(400).json({
1175
+ success: false,
1176
+ error: 'webSearch 非法: 需要 { enabled, provider, apiKeys, proxy } 对象'
1177
+ })
1178
+ }
1179
+ updates.webSearch = normalized
1180
+ }
1167
1181
  if (Object.keys(updates).length === 0) {
1168
1182
  return res.status(400).json({ success: false, error: '缺少可保存的参数' })
1169
1183
  }
@@ -101,6 +101,9 @@ export function createDispatcher({ configManager, getCurrentProjectPath, runTask
101
101
  * @param {boolean} [payload.autoRun] 默认 true;false = 只建任务不执行
102
102
  * @param {boolean} [payload.useDefaultPrompt] 默认 true;false = 本次不附加预设提示词
103
103
  * @param {string} [payload.executor] claude | opencode | codex;缺省走配置默认
104
+ * @param {boolean} [payload.imageText] 本次派发是否把图片先转成文字描述。
105
+ * 不传 = 不记(执行时按执行器兜底:codearts 开、别家关,见 taskRunner 的
106
+ * shouldDescribeImages)。显式传 true/false 会记在任务上,重跑沿用。
104
107
  * @param {Array} [payload.attachments] 暂存区附件记录 [{ id, ext, originalName }]
105
108
  * @returns {Promise<{task: object, instruction: object, ran: boolean, target: object}>}
106
109
  */
@@ -207,6 +210,12 @@ export function createDispatcher({ configManager, getCurrentProjectPath, runTask
207
210
  if (dispatchPrompt.parts && (dispatchPrompt.parts.global || dispatchPrompt.parts.project)) {
208
211
  task.promptParts = dispatchPrompt.parts;
209
212
  }
213
+ // 「图片先转文字」的选择记在任务上:重跑不该让用户再勾一次。
214
+ // 不传就不写字段(= 执行时按执行器兜底),而不是写成 false —— 那会把
215
+ // "这次没选" 说成 "这次明确不要"。
216
+ if (typeof payload.imageText === 'boolean') {
217
+ task.imageText = payload.imageText;
218
+ }
210
219
  tasks.push(task);
211
220
  await writeJson(TASKS_FILE, { tasks });
212
221
 
@@ -0,0 +1,183 @@
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // 「图片 → 文字」预处理:派发前先用**我们自己的**多模态通路把截图描述成文本,
16
+ // 再把这段描述拼进 prompt。
17
+ //
18
+ // ── 为什么需要它 ──────────────────────────────────────────────────────────
19
+ //
20
+ // 附件是"列路径 + 让 Agent 自己去读",这条口径对能读图的执行器(Claude)是对的:
21
+ // 图片按原样进模型上下文,不丢信息。但 **codearts 执行器读不了图**(2026-10-26 实测):
22
+ // 它的 `read` 工具明明返回了 data URL 附件,发给模型的请求体里却一个 image_url 都没有 ——
23
+ // 原因是它把"哪些模型算多模态"写死在一份 `multimodalModels` 白名单里,默认只有两个
24
+ // Qwen-VL,`deepseek-v4.1-flash` 不在其中,于是图片被替换成一行
25
+ // `ERROR: Cannot read image (this model does not support image input)`。
26
+ // 它的另一条通道 `analyzeImage` 又要 PromptCenter 的 base url(只有 IDE/企业版才下发)。
27
+ //
28
+ // 与其等它,不如在派发这一步就把图"看"掉:用我们配置里的模型(`config.json` 的
29
+ // models[],与提交信息 / 目录解读同一口径)生成一段描述,执行器拿到的是文字 ——
30
+ // 换谁跑都能用。
31
+ //
32
+ // ── 设计取舍 ──────────────────────────────────────────────────────────────
33
+ //
34
+ // · **失败绝不阻断**:描述不出来就退回"只有路径"的老形态(执行器若自己会读图,
35
+ // 照样读得到)。预处理是锦上添花,不能把派发卡死。
36
+ // · **只描述图片**:PDF 已有全文提取(pdfText.js),其他附件没有可靠的"转文字"通路。
37
+ // · **单张大小上限**:附件上限是 20MB,把它 base64 塞进一次 LLM 请求既慢又容易
38
+ // 被网关拒;超过上限的跳过并说明原因,让执行器自己想办法。
39
+ // · **数量上限**:一次派发挂十几张截图是有的,逐张串行调用会把"点派发到真的开始跑"
40
+ // 拖到几分钟。超过上限的仍列路径,只是没有描述。
41
+
42
+ import fs from 'node:fs';
43
+ import path from 'node:path';
44
+ import { logger, readJson, CONFIG_FILE } from './shared.js';
45
+ import { isImageExt } from './attachmentUtils.js';
46
+ import { callLlmStream } from './llmClient.js';
47
+
48
+ /** 单张图片大小上限:超过就不送去描述(base64 之后还要再涨 1/3) */
49
+ export const MAX_DESCRIBE_IMAGE_BYTES = 6 * 1024 * 1024;
50
+
51
+ /** 一次派发最多描述几张图 */
52
+ export const MAX_DESCRIBE_IMAGES = 4;
53
+
54
+ /** 单张图片的描述字数上限(prompt 里还要放任务正文,别让描述把上下文吃满) */
55
+ export const MAX_DESCRIBE_CHARS = 1200;
56
+
57
+ /** 单张图片的调用超时 */
58
+ export const DESCRIBE_TIMEOUT_MS = 45_000;
59
+
60
+ const SYSTEM_PROMPT = [
61
+ '你是一个把图片转成文字的助手。用户会给你一张截图(可能是软件界面、报错、设计稿、图表)。',
62
+ '请用中文客观描述,只写你在图里真的看到的东西:',
63
+ '1. 这是什么界面/画面,整体布局;',
64
+ '2. 图上所有可见的关键文字(标题、按钮、菜单、报错原文、数值)——逐字抄,不要改写、不要补全;',
65
+ '3. 明显的状态(红框、选中态、报错图标、进度、缺失/空白区域);',
66
+ '4. 如果图里有用户高亮/圈出的位置,优先描述那一块。',
67
+ `不要猜测看不到的内容,不要评价好坏,不要写"可能"之类的推测。总长度不超过 ${MAX_DESCRIBE_CHARS} 字,用短句和列表。`,
68
+ ].join('\n');
69
+
70
+ /**
71
+ * 解析"用哪个模型来描述图片":`config.json` 里 models[] 的默认项(isDefault 优先,
72
+ * 否则第一个)—— 与提交信息生成 / 目录解读 / 落点判断同一口径,不另立第二份。
73
+ *
74
+ * @returns {Promise<object|null>} 形如 { model, apiKey, baseURL, ... };没配就 null
75
+ */
76
+ export async function resolveDescribeModel() {
77
+ try {
78
+ const raw = await readJson(CONFIG_FILE, {});
79
+ const models = Array.isArray(raw?.models) ? raw.models : [];
80
+ return models.find(m => m && m.isDefault) || models[0] || null;
81
+ } catch (err) {
82
+ logger.warn('[imageDescribe] 读取 AI 配置失败,跳过图片转文字:', err?.message || err);
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /** 附件里的图片(按 isImageExt 判,与附件白名单同一份口径) */
88
+ export function pickImageAttachments(attachments) {
89
+ if (!Array.isArray(attachments)) return [];
90
+ return attachments.filter(a => a && a.absolutePath && isImageExt(a.ext));
91
+ }
92
+
93
+ /**
94
+ * 描述一张图片。
95
+ *
96
+ * @param {string} filePath 本机绝对路径
97
+ * @param {object} model config.json 里的模型条目
98
+ * @param {object} [opts]
99
+ * @param {string} [opts.fileName] 给模型的文件名提示(不含目录,别泄漏本地路径)
100
+ * @returns {Promise<string|null>} 描述文本;失败/超限/空回答一律 null
101
+ */
102
+ export async function describeImage(filePath, model, opts = {}) {
103
+ if (!model || !filePath) return null;
104
+ let buf;
105
+ try {
106
+ const stat = fs.statSync(filePath);
107
+ if (!stat.isFile()) return null;
108
+ if (stat.size > MAX_DESCRIBE_IMAGE_BYTES) {
109
+ logger.info(`[imageDescribe] 跳过超大图片 ${path.basename(filePath)} (${stat.size} bytes)`);
110
+ return null;
111
+ }
112
+ buf = fs.readFileSync(filePath);
113
+ } catch (err) {
114
+ logger.warn('[imageDescribe] 读取图片失败:', err?.message || err);
115
+ return null;
116
+ }
117
+
118
+ const ext = String(path.extname(filePath) || '').replace(/^\./, '').toLowerCase() || 'png';
119
+ const dataUrl = `data:${ext === 'jpg' ? 'image/jpeg' : `image/${ext}`};base64,${buf.toString('base64')}`;
120
+ const name = opts.fileName || path.basename(filePath);
121
+
122
+ let text = '';
123
+ try {
124
+ const { content, aborted } = await callLlmStream(
125
+ model,
126
+ `这张图片的文件名是 ${name},请按系统提示把它转成文字描述。`,
127
+ (delta) => { if (delta?.content) text += delta.content; },
128
+ {
129
+ images: [dataUrl],
130
+ systemPrompt: SYSTEM_PROMPT,
131
+ maxTokens: MAX_DESCRIBE_CHARS * 2,
132
+ timeoutMs: DESCRIBE_TIMEOUT_MS,
133
+ },
134
+ );
135
+ if (aborted) return null;
136
+ const out = String(content || text || '').trim();
137
+ if (!out) return null;
138
+ return out.length > MAX_DESCRIBE_CHARS
139
+ ? out.slice(0, MAX_DESCRIBE_CHARS) + '…'
140
+ : out;
141
+ } catch (err) {
142
+ // 模型不支持图片 / 网关拒收 / 超时都会走到这里:记一行日志,退回"只给路径"
143
+ logger.warn(`[imageDescribe] 描述图片失败 ${name}:`, err?.message || err);
144
+ return null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 批量描述:给 buildAttachmentBlock 用的构造函数。
150
+ *
151
+ * 返回的 describer 每次调用返回 `{ text }` 或 null —— 不抛错(见文件头的"失败绝不阻断")。
152
+ * 模型只解析一次;一张图都没描述成功时后续图片直接跳过,不重复烧额度。
153
+ *
154
+ * @param {object} [opts]
155
+ * @param {object} [opts.model] 不给就自己从 config.json 解析
156
+ * @param {number} [opts.maxImages]
157
+ * @param {(filePath: string, model: object, extra?: object) => Promise<string|null>} [opts.describe]
158
+ * 单张描述的实现(默认 describeImage)。只在单测里替换,生产不传 ——
159
+ * 否则一次"验熔断/去重"的测试就得真连一次网关。
160
+ * @returns {Promise<(att: object) => Promise<string|null>>}
161
+ */
162
+ export async function createImageDescriber(opts = {}) {
163
+ const maxImages = Number.isFinite(opts.maxImages) ? opts.maxImages : MAX_DESCRIBE_IMAGES;
164
+ const model = opts.model || await resolveDescribeModel();
165
+ const runDescribe = typeof opts.describe === 'function' ? opts.describe : describeImage;
166
+ const cache = new Map();
167
+ let done = 0;
168
+ let anyFailed = false;
169
+
170
+ return async function describe(att) {
171
+ const filePath = att && att.absolutePath;
172
+ if (!filePath || !model) return null;
173
+ if (cache.has(filePath)) return cache.get(filePath);
174
+ if (done >= maxImages || anyFailed) return null;
175
+ done++;
176
+ const text = await runDescribe(filePath, model, { fileName: att.originalName || '' });
177
+ if (!text) anyFailed = true;
178
+ cache.set(filePath, text);
179
+ return text;
180
+ };
181
+ }
182
+
183
+ export default { describeImage, createImageDescriber, resolveDescribeModel, pickImageAttachments };
@@ -0,0 +1,149 @@
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // 「图片 → 文字」预处理单测(imageDescribe.js + pdfText 的 describeImage 分支)。
16
+ //
17
+ // 这里刻意**不联网**:描述本身是 `callLlmStream` 的事,测它等于测网关。
18
+ // 钉的是这一层的契约 ——
19
+ // · 只挑图片附件(别把 PDF / 日志也送去"看图");
20
+ // · 描述失败 / 文件不存在 / 超大 → 返回 null 且不抛(预处理不许掐断派发);
21
+ // · 一张图失败后不再继续烧额度;
22
+ // · buildAttachmentBlock 把描述**紧跟在该图路径之下**,多图时不串位。
23
+
24
+ import { test } from 'node:test';
25
+ import assert from 'node:assert/strict';
26
+ import fs from 'node:fs';
27
+ import os from 'node:os';
28
+ import path from 'node:path';
29
+
30
+ import {
31
+ pickImageAttachments,
32
+ createImageDescriber,
33
+ } from './imageDescribe.js';
34
+ import { buildAttachmentBlock } from './pdfText.js';
35
+ import { shouldDescribeImages } from './taskRunner.js';
36
+
37
+ function tmpFile(name, content) {
38
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'zg-imgdesc-'));
39
+ const file = path.join(dir, name);
40
+ fs.writeFileSync(file, content);
41
+ return file;
42
+ }
43
+
44
+ test('pickImageAttachments: 只挑图片,且要求有绝对路径', () => {
45
+ const list = [
46
+ { ext: 'png', absolutePath: '/a/x.png' },
47
+ { ext: 'JPG', absolutePath: '/a/y.JPG' },
48
+ { ext: 'pdf', absolutePath: '/a/doc.pdf' },
49
+ { ext: 'png' }, // 没有路径:跳过
50
+ null,
51
+ ];
52
+ const picked = pickImageAttachments(list);
53
+ assert.deepEqual(picked.map(a => a.ext), ['png', 'JPG']);
54
+ assert.deepEqual(pickImageAttachments(undefined), []);
55
+ assert.deepEqual(pickImageAttachments([]), []);
56
+ });
57
+
58
+ test('createImageDescriber: 没有模型 / 文件不存在时返回 null,不抛', async () => {
59
+ const noModel = await createImageDescriber({ model: null });
60
+ assert.equal(await noModel({ absolutePath: '/a/x.png' }), null);
61
+
62
+ const withModel = await createImageDescriber({ model: { model: 'stub' } });
63
+ // 文件不存在 → 读盘失败被吞掉,返回 null(退化成"只给路径")
64
+ assert.equal(await withModel({ absolutePath: '/definitely/not/here.png' }), null);
65
+ });
66
+
67
+ test('createImageDescriber: 同一张图只问一次,一张失败后熔断不再继续', async () => {
68
+ const calls = [];
69
+ const describe = await createImageDescriber({
70
+ model: { model: 'stub' },
71
+ // 第一张成功、第二张失败、第三张本应被跳过(熔断)
72
+ describe: async (filePath) => {
73
+ calls.push(filePath);
74
+ if (filePath.endsWith('bad.png')) return null;
75
+ return `描述 ${path.basename(filePath)}`;
76
+ },
77
+ });
78
+
79
+ assert.equal(await describe({ absolutePath: '/x/one.png' }), '描述 one.png');
80
+ assert.equal(await describe({ absolutePath: '/x/one.png' }), '描述 one.png'); // 命中缓存
81
+ assert.deepEqual(calls, ['/x/one.png'], '同一张图不该问第二遍');
82
+
83
+ assert.equal(await describe({ absolutePath: '/x/bad.png' }), null);
84
+ assert.deepEqual(calls, ['/x/one.png', '/x/bad.png']);
85
+
86
+ // 已经失败过 → 后面的图直接跳过,连"问一次"都不做(一次派发挂十张截图时
87
+ // 不该为了十张全失败各烧一次额度)
88
+ assert.equal(await describe({ absolutePath: '/x/later.png' }), null);
89
+ assert.deepEqual(calls, ['/x/one.png', '/x/bad.png']);
90
+ });
91
+
92
+ test('createImageDescriber: 超过 maxImages 的部分不描述', async () => {
93
+ const calls = [];
94
+ const describe = await createImageDescriber({
95
+ model: { model: 'stub' },
96
+ maxImages: 2,
97
+ describe: async (filePath) => { calls.push(filePath); return 'ok'; },
98
+ });
99
+ for (const n of ['a', 'b', 'c']) {
100
+ await describe({ absolutePath: `/x/${n}.png` });
101
+ }
102
+ assert.deepEqual(calls, ['/x/a.png', '/x/b.png']);
103
+ });
104
+
105
+ test('buildAttachmentBlock: 描述紧跟在该图路径之后,多图不串位', async () => {
106
+ const a = tmpFile('one.png', 'x');
107
+ const b = tmpFile('two.png', 'y');
108
+ const desc = { [a]: '第一张:红色方块', [b]: '第二张:蓝色方块' };
109
+ const { block, describedCount } = await buildAttachmentBlock(
110
+ [{ absolutePath: a, ext: 'png', mimeType: 'image/png' },
111
+ { absolutePath: b, ext: 'png', mimeType: 'image/png' }],
112
+ { describeImage: async (att) => desc[att.absolutePath] || null },
113
+ );
114
+
115
+ assert.equal(describedCount, 2);
116
+ const lines = block.split('\n');
117
+ const ai = lines.findIndex(l => l.includes(a));
118
+ const bi = lines.findIndex(l => l.includes(b));
119
+ // 描述块夹在"图 A 路径"与"图 B 路径"之间 → A 的描述紧跟 A
120
+ assert.ok(ai >= 0 && bi > ai);
121
+ const between = lines.slice(ai, bi).join('\n');
122
+ assert.ok(between.includes('第一张:红色方块'), 'A 的描述应紧跟在 A 之后');
123
+ assert.ok(!between.includes('第二张'), 'B 的描述不能跑到 A 后面');
124
+ assert.ok(block.includes('2 张图片已转成文字描述'), '汇总行要说明转了几张');
125
+ });
126
+
127
+ test('buildAttachmentBlock: 没给 describeImage 时行为不变(老形态)', async () => {
128
+ const a = tmpFile('one.png', 'x');
129
+ const { block, describedCount } = await buildAttachmentBlock([
130
+ { absolutePath: a, ext: 'png', mimeType: 'image/png' },
131
+ ]);
132
+ assert.equal(describedCount, 0);
133
+ assert.ok(!block.includes('<image_description>'));
134
+ assert.ok(block.includes(a));
135
+ });
136
+
137
+ test('shouldDescribeImages: 载体 > 任务 > 执行器兜底(codearts 默认开)', () => {
138
+ // 执行器兜底:只有 codearts 读不了图,别家能直接看
139
+ assert.equal(shouldDescribeImages({}, {}, 'codearts'), true);
140
+ assert.equal(shouldDescribeImages({}, {}, 'claude'), false);
141
+ assert.equal(shouldDescribeImages({}, {}, 'opencode'), false);
142
+
143
+ // 任务上记过的选择优先于执行器兜底(重跑沿用派发那一刻的勾选)
144
+ assert.equal(shouldDescribeImages({ imageText: false }, {}, 'codearts'), false);
145
+ assert.equal(shouldDescribeImages({ imageText: true }, {}, 'claude'), true);
146
+
147
+ // 载体上的值优先于任务(同一条任务重跑时换了选择)
148
+ assert.equal(shouldDescribeImages({ imageText: true }, { imageText: false }, 'codearts'), false);
149
+ });
@@ -1795,7 +1795,7 @@ export function registerWorkbenchRoutes({
1795
1795
 
1796
1796
  /**
1797
1797
  * 派发一条指令。
1798
- * body: { text, projectPath?, autoRun?, useDefaultPrompt?, executor?, attachments? }
1798
+ * body: { text, projectPath?, autoRun?, useDefaultPrompt?, executor?, imageText?, attachments? }
1799
1799
  * - projectPath 缺省 -> 交给 targetResolver 判断落到哪个项目(显式指定 > 指令里点名
1800
1800
  * > 主 Agent 判断 > 应用当前项目)。用户不必先选项目,落点会如实记进指令流水
1801
1801
  * - autoRun 默认 true;false = 只建任务不执行
@@ -1813,6 +1813,7 @@ export function registerWorkbenchRoutes({
1813
1813
  autoRun: req.body?.autoRun,
1814
1814
  useDefaultPrompt: req.body?.useDefaultPrompt,
1815
1815
  executor: req.body?.executor,
1816
+ imageText: req.body?.imageText,
1816
1817
  attachments: req.body?.attachments,
1817
1818
  });
1818
1819
  res.json({ success: true, ...result });
@@ -73,20 +73,25 @@ export async function extractPdfText(filePath) {
73
73
 
74
74
  /**
75
75
  * 构建附件信息块。
76
- * - 图片:列路径(Claude CLI 可直接读取)
76
+ * - 图片:列路径(Claude CLI 可直接读取);给了 describeImage 时额外内联一段文字描述
77
77
  * - PDF:预提取文本并内联,不列路径(绕过 Claude CLI 的 pdfParse bug)
78
78
  * - 其他:列路径
79
79
  *
80
80
  * @param {Array<{absolutePath?: string, ext?: string, mimeType?: string}>} attachments
81
81
  * @param {object} [opts]
82
82
  * @param {boolean} [opts.withImageData=false] 是否读取图片为 data URL(预览/拆分场景需要)
83
- * @returns {Promise<{ block: string, imageDataUrls: string[] }>}
83
+ * @param {(att: object) => Promise<string|null>} [opts.describeImage]
84
+ * 「图片 → 文字」的预处理函数(见 imageDescribe.js)。给看图能力缺失的执行器
85
+ * (codearts)兜底:描述不出来就退回纯路径,不抛错、不阻断。
86
+ * @returns {Promise<{ block: string, imageDataUrls: string[], describedCount: number }>}
84
87
  */
85
88
  export async function buildAttachmentBlock(attachments, opts) {
86
89
  const withImages = opts?.withImageData ?? false;
90
+ const describeImage = typeof opts?.describeImage === 'function' ? opts.describeImage : null;
87
91
  const imageDataUrls = [];
92
+ let describedCount = 0;
88
93
  if (!attachments || attachments.length === 0) {
89
- return { block: '', imageDataUrls };
94
+ return { block: '', imageDataUrls, describedCount };
90
95
  }
91
96
 
92
97
  const lines = [];
@@ -111,6 +116,15 @@ export async function buildAttachmentBlock(attachments, opts) {
111
116
  } else if (isImageExt(a.ext)) {
112
117
  // 图片:列路径,可选读 data URL
113
118
  lines.push(` ${i + 1}. [${a.mimeType || 'image'}] ${a.absolutePath}`);
119
+ if (describeImage) {
120
+ // 先文字描述、后路径:模型读 prompt 是线性的,"这张图长什么样"必须紧跟在这张图
121
+ // 的身份之后,否则多图时描述会跟路径错位(路径在第 3 行、描述在第 9 行)。
122
+ const desc = await describeImage(a);
123
+ if (desc) {
124
+ describedCount++;
125
+ lines.push(` <image_description>\n${desc}\n </image_description>`);
126
+ }
127
+ }
114
128
  if (withImages) {
115
129
  try {
116
130
  const buf = fs.readFileSync(a.absolutePath);
@@ -125,7 +139,7 @@ export async function buildAttachmentBlock(attachments, opts) {
125
139
  }
126
140
 
127
141
  if (lines.length === 0) {
128
- return { block: '', imageDataUrls };
142
+ return { block: '', imageDataUrls, describedCount };
129
143
  }
130
144
 
131
145
  const parts = [];
@@ -135,11 +149,14 @@ export async function buildAttachmentBlock(attachments, opts) {
135
149
  if (pdfFailedCount > 0) {
136
150
  parts.push(`${pdfFailedCount} 个 PDF 解析失败`);
137
151
  }
152
+ if (describedCount > 0) {
153
+ parts.push(`其中 ${describedCount} 张图片已转成文字描述`);
154
+ }
138
155
  const imgNote = withImages && imageDataUrls.length > 0
139
156
  ? `(其中 ${imageDataUrls.length} 张图片已随消息一并发送,请直接基于图片内容分析)`
140
157
  : '';
141
158
  const note = parts.length > 0 ? `(${parts.join(',')})` : imgNote;
142
159
 
143
160
  const block = `\n\n## 任务附件${note}\n${lines.join('\n')}`;
144
- return { block, imageDataUrls };
161
+ return { block, imageDataUrls, describedCount };
145
162
  }