zen-gitsync 2.17.23 → 2.17.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 (49) hide show
  1. package/README.md +4 -4
  2. package/package.json +5 -1
  3. package/src/config.js +34 -3
  4. package/src/ui/public/assets/{AgentEngineSelector-DLJ8He83.js → AgentEngineSelector-CvIoGCLD.js} +1 -1
  5. package/src/ui/public/assets/{AgentView-CQ18nw02.js → AgentView-BrqS78Rt.js} +1 -1
  6. package/src/ui/public/assets/{AppVersionBadge-1pyUzRer.js → AppVersionBadge-BHxZm7-h.js} +2 -2
  7. package/src/ui/public/assets/{BranchSelector-DrsrZiWJ.js → BranchSelector-w5ybmWVv.js} +1 -1
  8. package/src/ui/public/assets/{CommitForm-B5p-7P7m.js → CommitForm-pRCnAfz6.js} +1 -1
  9. package/src/ui/public/assets/{CommonDialog-BEm2cK1X.js → CommonDialog-qXAsFhNS.js} +1 -1
  10. package/src/ui/public/assets/{EditorView-fuMrS9P7.js → EditorView-DnuGnX3k.js} +1 -1
  11. package/src/ui/public/assets/{FileDiffViewer-CYxcLw10.js → FileDiffViewer-Bc8j9cQn.js} +1 -1
  12. package/src/ui/public/assets/{FlowExecutionViewer-DTmlBLXQ.js → FlowExecutionViewer-CF4HkGu6.js} +1 -1
  13. package/src/ui/public/assets/{FlowOrchestrationWorkspace-Da_UmTRT.js → FlowOrchestrationWorkspace-a3Zk4BMK.js} +1 -1
  14. package/src/ui/public/assets/{IconButton-BShiC2AP.js → IconButton-BjIHmE4p.js} +1 -1
  15. package/src/ui/public/assets/{LogList-5M5JBhlB.js → LogList-CqpSAW5T.js} +1 -1
  16. package/src/ui/public/assets/{MindmapView-BUV_w14M.js → MindmapView-CgptM54l.js} +1 -1
  17. package/src/ui/public/assets/{MonitorView-D9N4QH5Y.js → MonitorView-qb0EPvQW.js} +1 -1
  18. package/src/ui/public/assets/{ProjectStartupButton-DYYQ-O7P.js → ProjectStartupButton-CmankQtk.js} +1 -1
  19. package/src/ui/public/assets/{RecentDirectoriesChat-BtWa_HMY.js → RecentDirectoriesChat-CsHHckPP.js} +1 -1
  20. package/src/ui/public/assets/{RemoteManagerDialog-C4QIJK_F.js → RemoteManagerDialog-Cpg4g5KW.js} +1 -1
  21. package/src/ui/public/assets/{RemoteRepoCard-BmDh538v.js → RemoteRepoCard-DCc-adLb.js} +1 -1
  22. package/src/ui/public/assets/{SourceMapView-Ceamya-N.js → SourceMapView-I41XvvZr.js} +1 -1
  23. package/src/ui/public/assets/{SvgIcon-xtz4vvc9.js → SvgIcon-BB80ERhO.js} +1 -1
  24. package/src/ui/public/assets/{TemplateManager-DhKTZ4EM.js → TemplateManager-BB_bEiBB.js} +1 -1
  25. package/src/ui/public/assets/{UserInputNode-DfeCGlEO.js → UserInputNode-y9gz0EKH.js} +1 -1
  26. package/src/ui/public/assets/WorkbenchView-Bkzbu5Sp.css +1 -0
  27. package/src/ui/public/assets/WorkbenchView-DKqriLBP.js +6 -0
  28. package/src/ui/public/assets/{_plugin-vue_export-helper-C_bQVzeb.js → _plugin-vue_export-helper-Dd_YmKEO.js} +4 -4
  29. package/src/ui/public/assets/{agentConversations-BUPj8vzH.js → agentConversations-CT9E8eQr.js} +1 -1
  30. package/src/ui/public/assets/configStore-BR8vEJCp.js +1 -0
  31. package/src/ui/public/assets/{gitStore-ByP9lpuT.js → gitStore-Bz77W-S0.js} +1 -1
  32. package/src/ui/public/assets/index-CsLyZsXy.js +29 -0
  33. package/src/ui/public/assets/index-D2rf9QKB.css +1 -0
  34. package/src/ui/public/assets/{previewSandbox-DSzZ_Rky.js → previewSandbox-luPexzF4.js} +1 -1
  35. package/src/ui/public/assets/toolsStore-CeI7jM5o.js +1 -0
  36. package/src/ui/public/index.html +12 -12
  37. package/src/ui/server/routes/config.js +11 -2
  38. package/src/ui/server/routes/fileOpen.js +35 -6
  39. package/src/ui/server/routes/workbench/executorModels.js +329 -0
  40. package/src/ui/server/routes/workbench/executorModels.test.js +326 -0
  41. package/src/ui/server/routes/workbench/index.js +26 -0
  42. package/src/ui/server/routes/workbench/jobActivity.js +6 -0
  43. package/src/ui/server/routes/workbench/orchestratorStore.js +4 -1
  44. package/src/ui/public/assets/WorkbenchView-D9l3i4Qj.css +0 -1
  45. package/src/ui/public/assets/WorkbenchView-DX2epPD9.js +0 -6
  46. package/src/ui/public/assets/configStore-DJH7OlB_.js +0 -1
  47. package/src/ui/public/assets/index-CFX8sK4w.js +0 -29
  48. package/src/ui/public/assets/index-DVJITdth.css +0 -1
  49. package/src/ui/public/assets/toolsStore-B49mCaQ_.js +0 -1
@@ -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{p as j}from"./vendor-CyWsJd6S.js";import{t as M}from"./_plugin-vue_export-helper-C_bQVzeb.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(){
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{p as j}from"./vendor-CyWsJd6S.js";import{t as M}from"./_plugin-vue_export-helper-Dd_YmKEO.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});
@@ -0,0 +1 @@
1
+ import{br as e,pr as t}from"./element-plus-Bx3j6vzG.js";import{v as n}from"./vendor-CyWsJd6S.js";import{n as r}from"./_plugin-vue_export-helper-Dd_YmKEO.js";import{i,o as a}from"./configStore-BR8vEJCp.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};
@@ -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-CFX8sK4w.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-CsLyZsXy.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-CyWsJd6S.js">
18
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-C_bQVzeb.js">
19
- <link rel="modulepreload" crossorigin href="/assets/SvgIcon-xtz4vvc9.js">
20
- <link rel="modulepreload" crossorigin href="/assets/IconButton-BShiC2AP.js">
21
- <link rel="modulepreload" crossorigin href="/assets/configStore-DJH7OlB_.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-Dd_YmKEO.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/SvgIcon-BB80ERhO.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/IconButton-BjIHmE4p.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/configStore-BR8vEJCp.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
23
- <link rel="modulepreload" crossorigin href="/assets/gitStore-ByP9lpuT.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/gitStore-Bz77W-S0.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-DSzZ_Rky.js">
26
+ <link rel="modulepreload" crossorigin href="/assets/previewSandbox-luPexzF4.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/useThemeObserver-DYxFoHh1.js">
28
- <link rel="modulepreload" crossorigin href="/assets/CommonDialog-BEm2cK1X.js">
29
- <link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-CYxcLw10.js">
30
- <link rel="modulepreload" crossorigin href="/assets/toolsStore-B49mCaQ_.js">
31
- <link rel="modulepreload" crossorigin href="/assets/TemplateManager-DhKTZ4EM.js">
28
+ <link rel="modulepreload" crossorigin href="/assets/CommonDialog-qXAsFhNS.js">
29
+ <link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-Bc8j9cQn.js">
30
+ <link rel="modulepreload" crossorigin href="/assets/toolsStore-CeI7jM5o.js">
31
+ <link rel="modulepreload" crossorigin href="/assets/TemplateManager-BB_bEiBB.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">
@@ -41,7 +41,7 @@
41
41
  <link rel="stylesheet" crossorigin href="/assets/FileDiffViewer-B6zy6NiA.css">
42
42
  <link rel="stylesheet" crossorigin href="/assets/TemplateManager-DiM3vrbf.css">
43
43
  <link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
44
- <link rel="stylesheet" crossorigin href="/assets/index-DVJITdth.css">
44
+ <link rel="stylesheet" crossorigin href="/assets/index-D2rf9QKB.css">
45
45
  </head>
46
46
  <body>
47
47
  <div id="app"></div>
@@ -989,7 +989,7 @@ export function registerConfigRoutes({
989
989
  // 保存通用设置(主题、语言、工作台任务执行器、任务完成提示)
990
990
  app.post('/api/config/save-general-settings', express.json(), async (req, res) => {
991
991
  try {
992
- const { theme, locale, taskExecutor, notifyOnTaskDone } = req.body
992
+ const { theme, locale, taskExecutor, notifyOnTaskDone, notifySoundOnTaskDone } = req.body
993
993
 
994
994
  // 读取原始配置以保留项目设置
995
995
  const rawConfig = await configManager.readRawConfigFile()
@@ -1015,13 +1015,22 @@ export function registerConfigRoutes({
1015
1015
  if (normalizedNotify !== null) {
1016
1016
  rawConfig.notifyOnTaskDone = normalizedNotify
1017
1017
  }
1018
+ // 任务完成提示音开关(全局,默认开)。同上:非布尔值静默忽略。
1019
+ // 它从属于 notifyOnTaskDone —— 这里不替前端做联动(关总开关时不顺带清掉这个),
1020
+ // 两个键各自保留用户的选择,联动只在消费端(useTaskNotifier)。这样用户
1021
+ // 临时关掉总开关再打开,提示音设置还在,不用重设一遍。
1022
+ const normalizedNotifySound = configManager.normalizeNotifySoundOnTaskDone(notifySoundOnTaskDone)
1023
+ if (normalizedNotifySound !== null) {
1024
+ rawConfig.notifySoundOnTaskDone = normalizedNotifySound
1025
+ }
1018
1026
 
1019
1027
  // 直接写入原始配置,避免覆盖项目设置
1020
1028
  await configManager.writeRawConfigFile(rawConfig)
1021
1029
  res.json({
1022
1030
  success: true,
1023
1031
  taskExecutor: normalizedExecutor || undefined,
1024
- notifyOnTaskDone: normalizedNotify ?? undefined
1032
+ notifyOnTaskDone: normalizedNotify ?? undefined,
1033
+ notifySoundOnTaskDone: normalizedNotifySound ?? undefined
1025
1034
  })
1026
1035
  } catch (error) {
1027
1036
  res.status(500).json({ success: false, error: error.message })
@@ -447,14 +447,40 @@ async function launchCodex(dirPath) {
447
447
  return launchInTerminal(dirPath, 'codex');
448
448
  }
449
449
 
450
- async function launchOpenCode(dirPath) {
450
+ /**
451
+ * opencode 的「完全批准」= `--auto`(官方文档里的 auto mode):把**未被显式拒绝**的
452
+ * 权限请求自动批准。
453
+ *
454
+ * 它不等于"什么都放行":配置里显式 `deny` 的仍然会被拦(opencode 默认就 deny 了
455
+ * `read` 的 `*.env` / `*.env.*`,TUI 里 `--auto` 也一样读不到),这与 claude 的
456
+ * bypassPermissions 受 `~/.claude/settings.json` deny 约束是同一个口径 —— 两边都是
457
+ * "除非你明确禁止,否则全放"。
458
+ *
459
+ * 档位 token:`auto` 是本工具的正式叫法;`bypassPermissions` 只是为了让调用方
460
+ * (顶栏菜单与编排台项目菜单)与 claude 那档共用同一个字符串。
461
+ */
462
+ const OPENCODE_AUTO_MODES = new Set(['auto', 'bypassPermissions']);
463
+
464
+ /**
465
+ * opencode 的启动参数(按权限档位)。
466
+ *
467
+ * 单独抽成纯函数是为了能**真的跑断言**而不用 spawn 一个终端窗口 —— 带 `--auto` 的
468
+ * TUI 会弹出来并停在那里等人输入,验证不能有这种副作用。回归 `test/opencode-permission.test.mjs`。
469
+ * 白名单式:非字符串 / 未知 token 一律返回空数组,不存在拼参数注入的面。
470
+ */
471
+ export function buildOpencodeArgs(permissionMode) {
472
+ return OPENCODE_AUTO_MODES.has(permissionMode) ? ['--auto'] : [];
473
+ }
474
+
475
+ async function launchOpenCode(dirPath, { permissionMode } = {}) {
451
476
  // opencode (sst/opencode) CLI - https://opencode.ai
477
+ const cliArgs = buildOpencodeArgs(permissionMode);
452
478
  if (process.platform === 'win32') {
453
- return spawnDetached('cmd.exe', ['/c', 'start', '""', 'opencode'], {
479
+ return spawnDetached('cmd.exe', ['/c', 'start', '""', 'opencode', ...cliArgs], {
454
480
  cwd: dirPath
455
481
  });
456
482
  }
457
- return launchInTerminal(dirPath, 'opencode');
483
+ return launchInTerminal(dirPath, 'opencode', cliArgs);
458
484
  }
459
485
 
460
486
  // Run the CLI shipped with this server, even when `g` is not in PATH or points
@@ -988,7 +1014,7 @@ export function registerFileOpenRoutes({
988
1014
  // 用 OpenCode 打开目录
989
1015
  app.post('/api/open-directory-with-opencode', asyncRoute(async (req, res) => {
990
1016
  try {
991
- const { path: dirPath } = req.body || {};
1017
+ const { path: dirPath, permissionMode } = req.body || {};
992
1018
  if (!dirPath) {
993
1019
  throw new HttpError(400, '目录路径不能为空');
994
1020
  }
@@ -1000,8 +1026,11 @@ export function registerFileOpenRoutes({
1000
1026
  }
1001
1027
 
1002
1028
  try {
1003
- await launchOpenCode(dirPath);
1004
- res.json({ success: true, message: '已用 OpenCode 打开目录' });
1029
+ await launchOpenCode(dirPath, { permissionMode });
1030
+ const message = OPENCODE_AUTO_MODES.has(permissionMode)
1031
+ ? '已用 OpenCode 打开目录(--auto:自动批准未被显式拒绝的权限)'
1032
+ : '已用 OpenCode 打开目录';
1033
+ res.json({ success: true, message });
1005
1034
  } catch (error) {
1006
1035
  res.status(400).json({
1007
1036
  success: false,
@@ -0,0 +1,329 @@
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
+ //
17
+ // ── 为什么要做这件事 ────────────────────────────────────────────────────────
18
+ //
19
+ // 工作台派任务时一律**不传模型参数**:claude / opencode / codex 的 spawn argv 里
20
+ // 都没有 --model(见 taskRunner.js 的注释与 README 的 argv 清单),模型跟随各自
21
+ // CLI 的配置。这条设计是对的 —— 但副作用是界面上**一个执行器的模型都看不到**:
22
+ // 底栏那个「默认模型 xxx」是**内置 g ai 引擎**用的 config.models,跟三个执行器
23
+ // 毫无关系(agentRoutes.js 里对外部引擎刻意把 session.model 置空也是同一个原因)。
24
+ // 于是用户想确认"这活到底是哪个模型跑的"只能去翻三个不同格式的配置文件。
25
+ //
26
+ // 这里把「读三个配置文件」这件事收口成一个模块,让前端能在选择器里直接显示。
27
+ //
28
+ // ── 三家配置文件的形状(这是本模块存在的全部理由)────────────────────────
29
+ //
30
+ // claude ~/.claude/settings.json JSON。模型在 env 里,且有**别名两层**:
31
+ // ANTHROPIC_DEFAULT_SONNET_MODEL 是 CLI 认识的名字
32
+ // (claude-sonnet-5[1M]),..._MODEL_NAME 才是它背后真正的模型
33
+ // (本机经代理指向 deepseek-v4.1-flash)。只报前者等于没报 ——
34
+ // 用户看到"claude-sonnet-5"会以为在用官方模型,实际是代理换过的。
35
+ // 所以两条都返回,display 优先取 _NAME。
36
+ //
37
+ // codex ~/.codex/config.toml TOML。顶层 `model = "..."`。
38
+ // ⚠️ 文件里还有 [projects.*] / [mcp_servers.*] 等几十个段,
39
+ // 段内也可能出现 model 键 —— 只认**任何 [表头] 之前**的顶层赋值。
40
+ // 同段的 model_providers.<id>.name 给出服务商名,一并带出。
41
+ //
42
+ // opencode ~/.config/opencode/opencode.json / .jsonc
43
+ // JSONC(带注释与尾逗号)。顶层 `model` 字段,可以是字符串
44
+ // "provider/model-id",也可以是对象 { model, variant }。
45
+ // ⚠️ **但它经常是空的** —— opencode 的 TUI 允许直接选模型,而选择存在
46
+ // 自己的 state 里、不回写配置。所以还要读第二处:
47
+ // ~/.local/state/opencode/model.json 的 recent[0](TUI 最近使用),
48
+ // variant(截图里那个蓝色的 max)在同文件的顶层 variant 表里。
49
+ // 两处都没有才算"没指定",返回值里用 source 区分(config / state)。
50
+ //
51
+ // ── 三条必须守住的底线 ──────────────────────────────────────────────────────
52
+ //
53
+ // 1. **纯只读**:只 fs.readFile,一个字节都不写。改用户 CLI 的配置是绝对禁区。
54
+ // 2. **不泄露密钥**:codex 的 config.toml 里有 experimental_bearer_token,
55
+ // claude 的 env 里有 ANTHROPIC_API_KEY —— 本模块只取模型名/服务商名,
56
+ // 任何 key 字段都不许出现在返回值里(readTomlTopLevel 只取显式白名单里的键)。
57
+ // 3. **读不到就是 null**:不猜、不回落成"看起来对"的字符串。前端照实显示
58
+ // 「未在配置中指定」,这比显示一个假模型名有用得多。
59
+
60
+ import fs from 'fs/promises';
61
+ import os from 'os';
62
+ import path from 'path';
63
+
64
+ /**
65
+ * 从 TOML 文本里取**顶层**(任何 [表头] 之前)的标量键。
66
+ *
67
+ * 为什么不用 TOML 解析库:codex 的 config.toml 结构简单(顶层几个标量 + 一堆段),
68
+ * 而为一个"读一行"引入解析器依赖不划算;本仓库也没有现成的 TOML 依赖。
69
+ * 代价是**只支持顶层标量**——这正是本模块需要的,段内的键一律不取。
70
+ *
71
+ * ⚠️ 刻意不解析行尾注释之外的任何语法:支持单/双引号值、数组(取不到就跳过)、
72
+ * 布尔与数字原样返回字符串。够用且不会把 `[projects.'c:\path']` 这种
73
+ * 引号里带方括号的段头误判成值。
74
+ *
75
+ * ⚠️ 注释行与空行必须 **continue 而不是 break**:真实的 codex config.toml 顶部
76
+ * 常带几行 `#` 说明、键与键之间也常空行;早先这三类一起 break,导致"文件第一行
77
+ * 是注释"时顶层 model 整个读空(返回 {}),表现为界面上 codex 永远显示未配置。
78
+ */
79
+ export function readTomlTopLevel(text, keys) {
80
+ const out = {};
81
+ for (const raw of String(text).split(/\r?\n/)) {
82
+ const line = raw.trim();
83
+ // 注释 / 空行在顶层键之间是合法的,跳过继续找
84
+ if (!line || line.startsWith('#')) continue;
85
+ // 只有段头才代表"顶层到此结束":后面的键都属于某个段
86
+ if (line.startsWith('[')) break;
87
+ const m = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
88
+ if (!m) continue;
89
+ if (!keys.includes(m[1])) continue;
90
+ const value = m[2].trim();
91
+ // 只收字符串值。数组/内联表一律不取 —— 本模块用不到,硬解只会解错。
92
+ const quoted = value.match(/^(["'])([\s\S]*?)\1\s*(?:#.*)?$/);
93
+ if (quoted) out[m[1]] = quoted[2];
94
+ }
95
+ return out;
96
+ }
97
+
98
+ /**
99
+ * 剥掉 JSONC 的注释与尾逗号,让 JSON.parse 能吃下 opencode.jsonc。
100
+ *
101
+ * 不引依赖的原因同上(只为读一个字段)。实现要点是**必须逐字符走**而不是
102
+ * 正则全局替换 —— 字符串字面量里的 `//`(比如 base_url 带 `https://`)会被误伤,
103
+ * 而本机的 codex/opencode 配置里 URL 是常见值。扫描时记住"在不在字符串里",
104
+ * 同时处理转义引号 `\"`。
105
+ */
106
+ export function stripJsonComments(text) {
107
+ let out = '';
108
+ let inString = false;
109
+ let escaped = false;
110
+ for (let i = 0; i < text.length; i += 1) {
111
+ const ch = text[i];
112
+ if (inString) {
113
+ out += ch;
114
+ if (escaped) escaped = false;
115
+ else if (ch === '\\') escaped = true;
116
+ else if (ch === '"') inString = false;
117
+ continue;
118
+ }
119
+ if (ch === '"') { inString = true; out += ch; continue; }
120
+ // 行注释
121
+ if (ch === '/' && text[i + 1] === '/') {
122
+ while (i < text.length && text[i] !== '\n') i += 1;
123
+ out += '\n';
124
+ continue;
125
+ }
126
+ // 块注释
127
+ if (ch === '/' && text[i + 1] === '*') {
128
+ i += 2;
129
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
130
+ i += 1;
131
+ continue;
132
+ }
133
+ out += ch;
134
+ }
135
+ // 尾逗号:`{"a":1,}` 与数组里的 `1,]`。这里只处理对象/数组结尾前的那一个逗号。
136
+ return out.replace(/,(\s*[}\]])/g, '$1');
137
+ }
138
+
139
+ async function readTextOrNull(filePath) {
140
+ try {
141
+ return await fs.readFile(filePath, 'utf8');
142
+ } catch {
143
+ // 文件不存在 / 无权限 / 是目录 —— 一律当"没配",不抛
144
+ return null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * claude:~/.claude/settings.json 的 env 段。
150
+ *
151
+ * 返回两条名字是有意的:
152
+ * model = ANTHROPIC_MODEL(若显式指定),否则取 sonnet 档的别名
153
+ * display= ..._MODEL_NAME,即别名背后真正的模型名
154
+ * 都取不到时返回 null。
155
+ *
156
+ * 为什么按 sonnet → opus → haiku 的顺序找:claude 在没有显式 ANTHROPIC_MODEL 时
157
+ * 会按档位选模型,日常跑任务默认落在 sonnet 档,所以它是最可能生效的那一档。
158
+ */
159
+ function detectClaude(env) {
160
+ const pick = (tier) => ({
161
+ alias: env[`ANTHROPIC_DEFAULT_${tier}_MODEL`],
162
+ display: env[`ANTHROPIC_DEFAULT_${tier}_MODEL_NAME`],
163
+ });
164
+
165
+ const explicit = env.ANTHROPIC_MODEL;
166
+ if (typeof explicit === 'string' && explicit.trim()) {
167
+ // 显式指定时没有"别名背后"的第二层,但 base_url 仍然要带出来 ——
168
+ // 用户看到 ANTHROPIC_MODEL 是官方名、实际请求打到自建代理,这才是关键信息。
169
+ return { model: explicit.trim(), display: null, provider: env.ANTHROPIC_BASE_URL || null };
170
+ }
171
+
172
+ for (const tier of ['SONNET', 'OPUS', 'HAIKU', 'FABLE']) {
173
+ const hit = pick(tier);
174
+ if (!hit.alias && !hit.display) continue;
175
+ return {
176
+ model: hit.alias || null,
177
+ // display 优先:别名背后那个才是用户真正在付钱/发请求的模型
178
+ display: hit.display || null,
179
+ provider: env.ANTHROPIC_BASE_URL || null,
180
+ };
181
+ }
182
+ return null;
183
+ }
184
+
185
+ /** codex:顶层 model + model_providers.<model_provider>.name 的服务商名 */
186
+ function detectCodex(toml) {
187
+ const top = readTomlTopLevel(toml, ['model', 'model_provider']);
188
+ if (!top.model) return null;
189
+ const providerId = top.model_provider;
190
+ // 服务商名要进 [model_providers.x] 段里找,段头格式:`[model_providers."custom"]`
191
+ // 或 `[model_providers.custom]`。这里只取「段内第一个 name = 」,够用且不误伤
192
+ // (其他段也有 name 键,但只认 model_providers.* 段)。
193
+ let provider = null;
194
+ if (providerId) {
195
+ const sectionRe = new RegExp(`^\\[model_providers\\.${escapeRe(providerId)}\\]\\s*$`, 'm');
196
+ const m = sectionRe.exec(toml);
197
+ if (m) {
198
+ const rest = toml.slice(m.index + m[0].length);
199
+ const end = rest.search(/^\s*\[/m);
200
+ const body = end >= 0 ? rest.slice(0, end) : rest;
201
+ const name = readTomlTopLevel(body, ['name']).name;
202
+ if (name) provider = name;
203
+ }
204
+ }
205
+ return { model: top.model, display: null, provider };
206
+ }
207
+
208
+ /** opencode:顶层 model,字符串或 { model, variant } 都吃;顺便记 variant */
209
+ function detectOpencodeConfig(json) {
210
+ const raw = json && json.model;
211
+ if (typeof raw === 'string' && raw.trim()) {
212
+ return { model: raw.trim(), display: null, provider: null, source: 'config' };
213
+ }
214
+ if (raw && typeof raw === 'object' && typeof raw.model === 'string' && raw.model.trim()) {
215
+ const variant = typeof raw.variant === 'string' && raw.variant.trim() ? ` (${raw.variant.trim()})` : '';
216
+ return { model: raw.model.trim() + variant, display: null, provider: null, source: 'config' };
217
+ }
218
+ return null;
219
+ }
220
+
221
+ /**
222
+ * opencode 当前用哪个模型:**配置文件优先,没有就退回 TUI 的「最近使用」**。
223
+ *
224
+ * 为什么要读两处 —— opencode 的 TUI 允许直接选模型,而它把选择存在**自己的 state 里
225
+ * 而不是回写配置文件**。本机就是活例子:`~/.config/opencode/opencode.jsonc` 里只有
226
+ * `$schema`(真的没配 model),但 statusline 明确在用 `opencode-go/space-bunny-free`;
227
+ * 那个选择躺在 `~/.local/state/opencode/model.json` 的 `recent[0]`。
228
+ * 只读配置文件的话界面上会显示「未在配置中指定」—— 对一个**正在跑的模型**说"没指定",
229
+ * 比不说还糟:用户会去翻配置,而配置里确实没有,于是白折腾一场。
230
+ *
231
+ * 两种来源语义不同,用 `source` 区分,前端会照实标出来(同一条模型名,来源不同含义不同):
232
+ * config —— 配置文件里写死了,这是"默认模型"
233
+ * state —— TUI 上次选的,opencode 下次启动默认还用它
234
+ *
235
+ * variant(截图里那个蓝色的 `max`)在 state 顶层 `variant` 表里,键是 `provider/model`;
236
+ * 也兼容某些版本直接写在 recent 项上的写法。
237
+ */
238
+ function detectOpencodeState(json) {
239
+ const recent = json && Array.isArray(json.recent) ? json.recent[0] : null;
240
+ if (!recent || typeof recent.providerID !== 'string' || typeof recent.modelID !== 'string') return null;
241
+ const id = `${recent.providerID}/${recent.modelID}`;
242
+ const own = typeof recent.variant === 'string' ? recent.variant.trim() : '';
243
+ const mapped = typeof json.variant?.[id] === 'string' ? json.variant[id].trim() : '';
244
+ const variant = own || mapped;
245
+ return {
246
+ model: variant ? `${id} (${variant})` : id,
247
+ display: null,
248
+ // provider 不再单列:模型名本身就是 `provider/model` 形态,再报一次只是噪音
249
+ // (`opencode-go/space-bunny-free (max) (… · opencode-go)`)
250
+ provider: null,
251
+ source: 'state',
252
+ };
253
+ }
254
+
255
+ /** 配置里有就用配置的,否则看 TUI 状态;两处都没有才是真的"没指定" */
256
+ function detectOpencode(cfgJson, stateJson) {
257
+ return detectOpencodeConfig(cfgJson) || detectOpencodeState(stateJson);
258
+ }
259
+
260
+ function escapeRe(s) {
261
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
262
+ }
263
+
264
+ /**
265
+ * 探测三个执行器当前配置的模型。
266
+ *
267
+ * @param {object} [opts]
268
+ * @param {string} [opts.homeDir] 测试沙箱用(默认 os.homedir())
269
+ * @returns {Promise<{ claude: object|null, codex: object|null, opencode: object|null }>}
270
+ * 每项 `{ model, display, provider, source }`;该执行器没配模型时为 null。
271
+ * source = 'config'(配置文件里写死的)| 'state'(CLI 自己记的"上次用的")。
272
+ *
273
+ * 三路并发、互不阻塞:任何一个配置文件读失败都只让**它自己**变成 null。
274
+ */
275
+ export async function detectExecutorModels({ homeDir } = {}) {
276
+ const home = homeDir || os.homedir();
277
+
278
+ const [claudeRaw, codexRaw, opencodeJsonRaw, opencodeJsoncRaw, opencodeStateRaw] = await Promise.all([
279
+ readTextOrNull(path.join(home, '.claude', 'settings.json')),
280
+ readTextOrNull(path.join(home, '.codex', 'config.toml')),
281
+ // .json 优先(标准名),.jsonc 兜底。opencode 两个都认。
282
+ readTextOrNull(path.join(home, '.config', 'opencode', 'opencode.json')),
283
+ readTextOrNull(path.join(home, '.config', 'opencode', 'opencode.jsonc')),
284
+ // TUI 的"最近使用"(不写回配置文件,见 detectOpencodeState 的注释)
285
+ readTextOrNull(path.join(home, '.local', 'state', 'opencode', 'model.json')),
286
+ ]);
287
+
288
+ const pickJson = (raw) => {
289
+ if (!raw) return null;
290
+ try { return JSON.parse(stripJsonComments(raw)); } catch { return null; }
291
+ };
292
+
293
+ const claudeCfg = pickJson(claudeRaw);
294
+ // ⚠️ 两个文件都要接住:早先这一行只吃了 .json(另一个 promise 被解构丢掉),
295
+ // 于是注释里写的"jsonc 兜底"是死代码 —— 只写了 .jsonc 的机器永远显示未配置。
296
+ const opencodeCfg = pickJson(opencodeJsonRaw) || pickJson(opencodeJsoncRaw);
297
+ const opencodeState = pickJson(opencodeStateRaw);
298
+
299
+ return {
300
+ claude: claudeCfg && typeof claudeCfg.env === 'object' && claudeCfg.env
301
+ ? detectClaude(claudeCfg.env)
302
+ : null,
303
+ codex: codexRaw ? detectCodex(codexRaw) : null,
304
+ opencode: detectOpencode(opencodeCfg, opencodeState),
305
+ };
306
+ }
307
+
308
+ /**
309
+ * 探测结果的展示格式化:给出**给人看**的那一个名字。
310
+ *
311
+ * 为什么 display 优先:claude 场景下 model 是 CLI 认识的别名(claude-sonnet-5[1M]),
312
+ * display 才是背后真实的模型(deepseek-v4.1-flash)。给用户看别名等于没回答问题。
313
+ * 两者都有且不同时用 `display` 并把别名放 detail(前端 title 用)。
314
+ */
315
+ export function formatExecutorModel(info) {
316
+ // 两个名字全空 = 没探测到,返回 null 让前端走"未在配置中指定"
317
+ if (!info || (!info.model && !info.display)) return null;
318
+ const name = info.display || info.model;
319
+ const detail = info.display && info.model && info.display !== info.model
320
+ ? info.model
321
+ : null;
322
+ return {
323
+ name,
324
+ detail: detail || null,
325
+ provider: info.provider || null,
326
+ // 来源照实带出去:同一个模型名,"配置里写死的"与"CLI 上次记下的"含义不同
327
+ source: info.source || null,
328
+ };
329
+ }