zen-gitsync 2.18.8 → 2.18.10

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 (75) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -1
  3. package/src/cli/ai/agent.js +7 -0
  4. package/src/cli/ai/runtime.test.js +22 -1
  5. package/src/cli/ai/tools.js +153 -2
  6. package/src/cli/ai/tools.test.js +71 -2
  7. package/src/cli/ai/transport.js +8 -3
  8. package/src/config.js +7 -5
  9. package/src/mcp/tools/workbench.js +1 -1
  10. package/src/ui/public/assets/{AgentView-DZx3n-So.css → AgentView-BnEgu3NT.css} +1 -1
  11. package/src/ui/public/assets/AgentView-DQQe_ivW.js +1 -0
  12. package/src/ui/public/assets/{AppVersionBadge-Bg5lrtKb.js → AppVersionBadge-D7PiBP0p.js} +2 -2
  13. package/src/ui/public/assets/{BranchSelector-CxjTqAkl.js → BranchSelector-BzHoMBOc.js} +1 -1
  14. package/src/ui/public/assets/{CommitForm-BWWrOvj5.js → CommitForm-CNTjLYTR.js} +2 -2
  15. package/src/ui/public/assets/{CommonDialog-x5jfL4Lq.js → CommonDialog-j_53ORvj.js} +1 -1
  16. package/src/ui/public/assets/CopySessionButton-Dy5t8Mjy.js +20 -0
  17. package/src/ui/public/assets/EditorView-CwcdA0Ei.js +1 -0
  18. package/src/ui/public/assets/{EditorView-BUiVsBv7.css → EditorView-Ims2BUK_.css} +1 -1
  19. package/src/ui/public/assets/{FlowExecutionViewer-BkZ20jxN.js → FlowExecutionViewer-CQ-TPhwF.js} +1 -1
  20. package/src/ui/public/assets/{FlowOrchestrationWorkspace-aZBYu29g.js → FlowOrchestrationWorkspace-CJHpEBCw.js} +1 -1
  21. package/src/ui/public/assets/{LogList-CSIbmUMK.js → LogList-KUePBjAt.js} +4 -4
  22. package/src/ui/public/assets/{MindmapView-JYLp185X.js → MindmapView-CwdvsuEK.js} +1 -1
  23. package/src/ui/public/assets/{MonitorView-BS2Mh-I7.js → MonitorView-CjEnnrTL.js} +1 -1
  24. package/src/ui/public/assets/{ProjectStartupButton-DAEP6ZU5.js → ProjectStartupButton-B36lRb6-.js} +1 -1
  25. package/src/ui/public/assets/RecentDirectoriesChat-7fzSkSmc.css +1 -0
  26. package/src/ui/public/assets/RecentDirectoriesChat-DNiji9Pr.js +1 -0
  27. package/src/ui/public/assets/{RemoteManagerDialog-aaNmsKpy.js → RemoteManagerDialog-Dj_GpA-c.js} +1 -1
  28. package/src/ui/public/assets/{RemoteRepoCard-DndI8rNZ.js → RemoteRepoCard-C8u9M6dn.js} +1 -1
  29. package/src/ui/public/assets/{SourceMapView-BTD9-R8t.js → SourceMapView-eng0B9U2.js} +1 -1
  30. package/src/ui/public/assets/{SvgIcon-0n6uuKFm.js → SvgIcon-5AlG3IfR.js} +1 -1
  31. package/src/ui/public/assets/{UserInputNode-B-mDg9Wy.js → UserInputNode-BKP869D6.js} +1 -1
  32. package/src/ui/public/assets/WorkbenchView-CZO18THH.js +6 -0
  33. package/src/ui/public/assets/WorkbenchView-DogERhHQ.css +1 -0
  34. package/src/ui/public/assets/{_plugin-vue_export-helper-C3o7kvRL.js → _plugin-vue_export-helper-Csn085FF.js} +4 -4
  35. package/src/ui/public/assets/agentConversations-De3wKTsN.js +10 -0
  36. package/src/ui/public/assets/configStore-DOaPZ6Mq.js +1 -0
  37. package/src/ui/public/assets/index-DQ82h1aq.js +57 -0
  38. package/src/ui/public/assets/index-NYzaTfbS.css +1 -0
  39. package/src/ui/public/assets/{useOrchestrator-CS9sn7nV.js → useOrchestrator-BjC7oHpt.js} +1 -1
  40. package/src/ui/public/index.html +6 -6
  41. package/src/ui/server/index.js +8 -0
  42. package/src/ui/server/routes/fileOpen.js +57 -1
  43. package/src/ui/server/routes/fileOpen.test.js +1 -1
  44. package/src/ui/server/routes/git/clone.js +5 -13
  45. package/src/ui/server/routes/workbench/agentChat.js +75 -10
  46. package/src/ui/server/routes/workbench/agentChatExtensions.test.js +290 -0
  47. package/src/ui/server/routes/workbench/agentEngines.js +8 -4
  48. package/src/ui/server/routes/workbench/agentExtensions.js +157 -0
  49. package/src/ui/server/routes/workbench/agentMarketplace.js +12 -0
  50. package/src/ui/server/routes/workbench/agentParity.test.js +16 -0
  51. package/src/ui/server/routes/workbench/agentRoutes.js +99 -7
  52. package/src/ui/server/routes/workbench/agentRoutes.test.js +141 -1
  53. package/src/ui/server/routes/workbench/executorModels.js +30 -5
  54. package/src/ui/server/routes/workbench/executorModels.test.js +16 -4
  55. package/src/ui/server/routes/workbench/index.js +17 -5
  56. package/src/ui/server/routes/workbench/jobActivity.js +3 -3
  57. package/src/ui/server/routes/workbench/jobActivity.test.js +1 -0
  58. package/src/ui/server/routes/workbench/runExternalTurn.js +22 -25
  59. package/src/ui/server/routes/workbench/runExternalTurn.test.js +34 -0
  60. package/src/ui/server/routes/workbench/scheduler.js +3 -0
  61. package/src/ui/server/routes/workbench/taskRunner.codebuddy.test.js +275 -0
  62. package/src/ui/server/routes/workbench/taskRunner.js +311 -24
  63. package/src/ui/server/routes/workbench/taskRunner.opencode.test.js +2 -1
  64. package/src/utils/gitRepoName.js +33 -0
  65. package/src/ui/public/assets/AgentView-BRAUZCtA.js +0 -1
  66. package/src/ui/public/assets/CopySessionButton-D0wFpk8a.js +0 -20
  67. package/src/ui/public/assets/EditorView-DmaGiI-z.js +0 -1
  68. package/src/ui/public/assets/RecentDirectoriesChat-B19uAf8k.css +0 -1
  69. package/src/ui/public/assets/RecentDirectoriesChat-CVFTShrU.js +0 -1
  70. package/src/ui/public/assets/WorkbenchView-CfSrCIbp.js +0 -8
  71. package/src/ui/public/assets/WorkbenchView-D6NRqZ5W.css +0 -1
  72. package/src/ui/public/assets/agentConversations-DpiGplZB.js +0 -8
  73. package/src/ui/public/assets/configStore-BdL3uW2E.js +0 -1
  74. package/src/ui/public/assets/index-BS7BDFG4.js +0 -57
  75. package/src/ui/public/assets/index-DwJWiCiN.css +0 -1
@@ -1 +1 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{i as e,mr as t,xr as n}from"./element-plus-J45pKUrE.js";import{n as r}from"./_plugin-vue_export-helper-C3o7kvRL.js";import{t as i}from"./configStore-BdL3uW2E.js";import{m as a}from"./index-BS7BDFG4.js";var o=[0,5,10,15,30,60].map(e=>e*60*1e3),s=6e5;function c(e){let t=Number(e);return o.includes(t)?t:s}function l(e){return e?e>=36e5?`@WORKBENCH:每 {n} 小时报告`:`@WORKBENCH:每 {n} 分钟报告`:`@WORKBENCH:关闭自动报告`}function u(e){return e?{n:Math.round(e>=36e5?e/36e5:e/6e4)}:{}}function d(e){switch(e){case`NO_MODEL`:return`@WORKBENCH:没有可用的 AI 模型,只记录了任务事实`;case`LLM_TIMEOUT`:return`@WORKBENCH:生成超时,只记录了任务事实`;case`LLM_EMPTY`:return`@WORKBENCH:模型没有返回正文,只记录了任务事实`;case`LLM_FAILED`:return`@WORKBENCH:生成失败,只记录了任务事实`;default:return`@WORKBENCH:生成失败,只记录了任务事实`}}function f(e){if(e==null||e===``)return null;let t=Number(e);if(!Number.isFinite(t))return null;let n=Math.round(t);return n>=0&&n<=100?n:null}function p(e){return typeof e!=`number`||!Number.isFinite(e)||e<=0?null:Math.floor(e)}var m=`@WORKBENCH:AI 估计`,h=`@WORKBENCH:百分比是主 Agent 根据任务思考与工具调用推测的大致估计,不是精确进度`;t();var g=1e5;function _(){let t=n(!0),o=n([]),l=n([]),u=n(g),d=n([]),f=n(null),p=n(!1),m=n(!1),h=n(!1),_=n(``),v=n({}),y=n([]),b=n(!1),x=n(!1),S=n(s);async function C(n=!1){try{let i=await fetch(`/api/workbench/orchestrator`,{cache:`no-store`}).then(e=>e.json());return i?.success?(t.value=i.active!==!1,Number.isInteger(i.maxInstructionChars)&&i.maxInstructionChars>0&&(u.value=i.maxInstructionChars),o.value=Array.isArray(i.instructions)?i.instructions:[],l.value=Array.isArray(i.activity)?i.activity:[],d.value=Array.isArray(i.running)?i.running:[],a().setRunning(d.value.length),f.value=i.updatedAt||null,_.value=typeof i.defaultPrompt==`string`?i.defaultPrompt:``,v.value=i.projectPrompts&&typeof i.projectPrompts==`object`?i.projectPrompts:{},S.value=c(i.reportIntervalMs),p.value=!0,!0):(n||e.error(i?.error||r(`@WORKBENCH:读取编排状态失败`)),!1)}catch(t){return n||e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function w(t=!1){if(b.value)return!1;b.value=!0;try{let n=await fetch(`/api/workbench/orchestrator/reports`,{cache:`no-store`}).then(e=>e.json());return n?.success?(y.value=Array.isArray(n.reports)?n.reports:[],!0):(t||e.error(n?.error||r(`@WORKBENCH:读取进度报告失败`)),!1)}catch(n){return t||e.error(r(`@WORKBENCH:网络错误: `)+(n?.message||n)),!1}finally{b.value=!1}}async function T(){if(x.value)return null;x.value=!0;try{let t=String(i().locale||``).startsWith(`en`)?`en`:`zh`,n=await fetch(`/api/workbench/orchestrator/report`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({locale:t})}).then(e=>e.json());return n?.success?n.report?(y.value=[n.report,...y.value].slice(0,20),n.report):(e.info(r(`@WORKBENCH:当前没有正在执行的任务,没有可汇报的进度`)),null):(e.error(n?.error||r(`@WORKBENCH:生成进度报告失败`)),null)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),null}finally{x.value=!1}}async function E(t){try{let n=await fetch(`/api/workbench/orchestrator/report-interval`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({intervalMs:t})}).then(e=>e.json());return n?.success?(S.value=c(n.reportIntervalMs),!0):(e.error(n?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function D(t){try{let n=await fetch(`/api/workbench/orchestrator/default-prompt`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({prompt:t})}).then(e=>e.json());return n?.success?(_.value=typeof n.defaultPrompt==`string`?n.defaultPrompt:``,e.success(r(`@WORKBENCH:已保存全局预设提示词`)),!0):(e.error(n?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function O(t,n){try{let i=await fetch(`/api/workbench/orchestrator/project-prompt`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({projectPath:t,prompt:n})}).then(e=>e.json());return i?.success?(v.value=i.projectPrompts&&typeof i.projectPrompts==`object`?i.projectPrompts:{},e.success(r(`@WORKBENCH:已保存项目预设提示词`)),!0):(e.error(i?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function k(n){if(m.value)return!1;let i=t.value;t.value=n,m.value=!0;try{let a=await fetch(`/api/workbench/orchestrator/state`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({active:n})}).then(e=>e.json());return a?.success?(t.value=a.active!==!1,e.success(t.value?r(`@WORKBENCH:已恢复调度`):r(`@WORKBENCH:已暂停调度,派发将只建任务不执行`)),!0):(t.value=i,e.error(a?.error||r(`@WORKBENCH:切换调度状态失败`)),!1)}catch(n){return t.value=i,e.error(r(`@WORKBENCH:网络错误: `)+(n?.message||n)),!1}finally{m.value=!1}}async function A(t){if(h.value)return null;h.value=!0;try{let n=await fetch(`/api/workbench/orchestrator/dispatch`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({text:t.text,projectPath:t.projectPath||``,autoRun:t.autoRun!==!1,useDefaultPrompt:t.useDefaultPrompt!==!1,executor:t.executor,attachments:(t.attachments||[]).map(e=>({id:e.id,ext:e.ext,originalName:e.originalName}))})}).then(e=>e.json());return n?.success?(await C(!0),{task:n.task||null,ran:!!n.ran}):(e.error(n?.error||r(`@WORKBENCH:派发失败`)),null)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),null}finally{h.value=!1}}return{active:t,instructions:o,activity:l,running:d,updatedAt:f,loaded:p,togglingSchedule:m,dispatching:h,maxInstructionChars:u,defaultPrompt:_,projectPrompts:v,reports:y,loadingReports:b,generatingReport:x,reportIntervalMs:S,loadOrchestrator:C,setSchedulingActive:k,dispatch:A,saveDefaultPrompt:D,saveProjectPrompt:O,loadReports:w,generateReport:T,setReportInterval:E}}export{d as a,f as c,m as i,p as l,o as n,l as o,h as r,u as s,_ as t};
1
+ import"./rolldown-runtime-C0FnF6B9.js";import{i as e,mr as t,xr as n}from"./element-plus-J45pKUrE.js";import{n as r}from"./_plugin-vue_export-helper-Csn085FF.js";import{t as i}from"./configStore-DOaPZ6Mq.js";import{m as a}from"./index-DQ82h1aq.js";var o=[0,5,10,15,30,60].map(e=>e*60*1e3),s=6e5;function c(e){let t=Number(e);return o.includes(t)?t:s}function l(e){return e?e>=36e5?`@WORKBENCH:每 {n} 小时报告`:`@WORKBENCH:每 {n} 分钟报告`:`@WORKBENCH:关闭自动报告`}function u(e){return e?{n:Math.round(e>=36e5?e/36e5:e/6e4)}:{}}function d(e){switch(e){case`NO_MODEL`:return`@WORKBENCH:没有可用的 AI 模型,只记录了任务事实`;case`LLM_TIMEOUT`:return`@WORKBENCH:生成超时,只记录了任务事实`;case`LLM_EMPTY`:return`@WORKBENCH:模型没有返回正文,只记录了任务事实`;case`LLM_FAILED`:return`@WORKBENCH:生成失败,只记录了任务事实`;default:return`@WORKBENCH:生成失败,只记录了任务事实`}}function f(e){if(e==null||e===``)return null;let t=Number(e);if(!Number.isFinite(t))return null;let n=Math.round(t);return n>=0&&n<=100?n:null}function p(e){return typeof e!=`number`||!Number.isFinite(e)||e<=0?null:Math.floor(e)}var m=`@WORKBENCH:AI 估计`,h=`@WORKBENCH:百分比是主 Agent 根据任务思考与工具调用推测的大致估计,不是精确进度`;t();var g=1e5;function _(){let t=n(!0),o=n([]),l=n([]),u=n(g),d=n([]),f=n(null),p=n(!1),m=n(!1),h=n(!1),_=n(``),v=n({}),y=n([]),b=n(!1),x=n(!1),S=n(s);async function C(n=!1){try{let i=await fetch(`/api/workbench/orchestrator`,{cache:`no-store`}).then(e=>e.json());return i?.success?(t.value=i.active!==!1,Number.isInteger(i.maxInstructionChars)&&i.maxInstructionChars>0&&(u.value=i.maxInstructionChars),o.value=Array.isArray(i.instructions)?i.instructions:[],l.value=Array.isArray(i.activity)?i.activity:[],d.value=Array.isArray(i.running)?i.running:[],a().setRunning(d.value.length),f.value=i.updatedAt||null,_.value=typeof i.defaultPrompt==`string`?i.defaultPrompt:``,v.value=i.projectPrompts&&typeof i.projectPrompts==`object`?i.projectPrompts:{},S.value=c(i.reportIntervalMs),p.value=!0,!0):(n||e.error(i?.error||r(`@WORKBENCH:读取编排状态失败`)),!1)}catch(t){return n||e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function w(t=!1){if(b.value)return!1;b.value=!0;try{let n=await fetch(`/api/workbench/orchestrator/reports`,{cache:`no-store`}).then(e=>e.json());return n?.success?(y.value=Array.isArray(n.reports)?n.reports:[],!0):(t||e.error(n?.error||r(`@WORKBENCH:读取进度报告失败`)),!1)}catch(n){return t||e.error(r(`@WORKBENCH:网络错误: `)+(n?.message||n)),!1}finally{b.value=!1}}async function T(){if(x.value)return null;x.value=!0;try{let t=String(i().locale||``).startsWith(`en`)?`en`:`zh`,n=await fetch(`/api/workbench/orchestrator/report`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({locale:t})}).then(e=>e.json());return n?.success?n.report?(y.value=[n.report,...y.value].slice(0,20),n.report):(e.info(r(`@WORKBENCH:当前没有正在执行的任务,没有可汇报的进度`)),null):(e.error(n?.error||r(`@WORKBENCH:生成进度报告失败`)),null)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),null}finally{x.value=!1}}async function E(t){try{let n=await fetch(`/api/workbench/orchestrator/report-interval`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({intervalMs:t})}).then(e=>e.json());return n?.success?(S.value=c(n.reportIntervalMs),!0):(e.error(n?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function D(t){try{let n=await fetch(`/api/workbench/orchestrator/default-prompt`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({prompt:t})}).then(e=>e.json());return n?.success?(_.value=typeof n.defaultPrompt==`string`?n.defaultPrompt:``,e.success(r(`@WORKBENCH:已保存全局预设提示词`)),!0):(e.error(n?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function O(t,n){try{let i=await fetch(`/api/workbench/orchestrator/project-prompt`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({projectPath:t,prompt:n})}).then(e=>e.json());return i?.success?(v.value=i.projectPrompts&&typeof i.projectPrompts==`object`?i.projectPrompts:{},e.success(r(`@WORKBENCH:已保存项目预设提示词`)),!0):(e.error(i?.error||r(`@WORKBENCH:保存失败`)),!1)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),!1}}async function k(n){if(m.value)return!1;let i=t.value;t.value=n,m.value=!0;try{let a=await fetch(`/api/workbench/orchestrator/state`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({active:n})}).then(e=>e.json());return a?.success?(t.value=a.active!==!1,e.success(t.value?r(`@WORKBENCH:已恢复调度`):r(`@WORKBENCH:已暂停调度,派发将只建任务不执行`)),!0):(t.value=i,e.error(a?.error||r(`@WORKBENCH:切换调度状态失败`)),!1)}catch(n){return t.value=i,e.error(r(`@WORKBENCH:网络错误: `)+(n?.message||n)),!1}finally{m.value=!1}}async function A(t){if(h.value)return null;h.value=!0;try{let n=await fetch(`/api/workbench/orchestrator/dispatch`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({text:t.text,projectPath:t.projectPath||``,autoRun:t.autoRun!==!1,useDefaultPrompt:t.useDefaultPrompt!==!1,executor:t.executor,attachments:(t.attachments||[]).map(e=>({id:e.id,ext:e.ext,originalName:e.originalName}))})}).then(e=>e.json());return n?.success?(await C(!0),{task:n.task||null,ran:!!n.ran}):(e.error(n?.error||r(`@WORKBENCH:派发失败`)),null)}catch(t){return e.error(r(`@WORKBENCH:网络错误: `)+(t?.message||t)),null}finally{h.value=!1}}return{active:t,instructions:o,activity:l,running:d,updatedAt:f,loaded:p,togglingSchedule:m,dispatching:h,maxInstructionChars:u,defaultPrompt:_,projectPrompts:v,reports:y,loadingReports:b,generatingReport:x,reportIntervalMs:S,loadOrchestrator:C,setSchedulingActive:k,dispatch:A,saveDefaultPrompt:D,saveProjectPrompt:O,loadReports:w,generateReport:T,setReportInterval:E}}export{d as a,f as c,m as i,p as l,o as n,l as o,h as r,u as s,_ as t};
@@ -10,18 +10,18 @@
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-BS7BDFG4.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-DQ82h1aq.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/dagre-DPkLmI0C.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/element-plus-J45pKUrE.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/vendor-B3NyhlYR.js">
18
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-C3o7kvRL.js">
19
- <link rel="modulepreload" crossorigin href="/assets/SvgIcon-0n6uuKFm.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-Csn085FF.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/SvgIcon-5AlG3IfR.js">
20
20
  <link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
21
- <link rel="modulepreload" crossorigin href="/assets/configStore-BdL3uW2E.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/configStore-DOaPZ6Mq.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/monaco-ChZ0d0qB.js">
23
23
  <link rel="modulepreload" crossorigin href="/assets/flow-mindmap-DvB74LE1.js">
24
- <link rel="modulepreload" crossorigin href="/assets/CommonDialog-x5jfL4Lq.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/CommonDialog-j_53ORvj.js">
25
25
  <link rel="modulepreload" crossorigin href="/assets/vue-flow-8aIpM5CU.js">
26
26
  <link rel="stylesheet" crossorigin href="/assets/element-plus-D-6gla1z.css">
27
27
  <link rel="stylesheet" crossorigin href="/assets/vendor-Bd8DyOF9.css">
@@ -30,7 +30,7 @@
30
30
  <link rel="stylesheet" crossorigin href="/assets/flow-mindmap-DqCKZUlr.css">
31
31
  <link rel="stylesheet" crossorigin href="/assets/CommonDialog-Bzi5ISqT.css">
32
32
  <link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
33
- <link rel="stylesheet" crossorigin href="/assets/index-DwJWiCiN.css">
33
+ <link rel="stylesheet" crossorigin href="/assets/index-NYzaTfbS.css">
34
34
  </head>
35
35
  <body>
36
36
  <div id="app"></div>
@@ -763,6 +763,14 @@ async function startUIServer(noOpen = false, savePort = false) {
763
763
  // 2) 清心跳 + 反注册实例
764
764
  try { clearInterval(heartbeatTimer); } catch (_) {}
765
765
  try { await instanceRegistry.unregister(process.pid); } catch (_) {}
766
+ // 2a) 关掉智能体对话加载的 MCP 连接(stdio 子进程 + HTTP 会话)。
767
+ // 它们不在 runningProcesses 里(走的是 cli/cleanup.js 的 trackChild),
768
+ // 上面的 drain 兜不到 —— 不关的话 Ctrl+C 后会留下一堆 npx 孤儿子进程。
769
+ // 动态 import:模块只在用过扩展层的进程里才需要,别进静态 import 图。
770
+ try {
771
+ const { closeWebExtensions } = await import('./routes/workbench/agentExtensions.js');
772
+ await closeWebExtensions();
773
+ } catch (_) {}
766
774
  // 2b) 清掉自己那份「运行中 job」广播文件(live-jobs/<pid>.json)。
767
775
  // 不清也不会留下幽灵「进行中」——别的实例读的时候发现 owner pid 没了会跳过并
768
776
  // 顺手删掉,这里只是让 Ctrl+C 这种常见路径当场干净。动态 import:模块早就在
@@ -28,6 +28,7 @@ const TOOL_INSTALL_PACKAGES = Object.freeze({
28
28
  claude: '@anthropic-ai/claude-code',
29
29
  codex: '@openai/codex',
30
30
  opencode: 'opencode-ai',
31
+ codebuddy: '@tencent-ai/codebuddy-code',
31
32
  dsh: '@deepseek-ai/dsh',
32
33
  });
33
34
 
@@ -36,6 +37,7 @@ const TOOL_DOCS_URLS = Object.freeze({
36
37
  claude: 'https://docs.anthropic.com/en/docs/claude-code/getting-started',
37
38
  codex: 'https://help.openai.com/en/articles/11096431-openai-codex-cli-getting-started',
38
39
  opencode: 'https://opencode.ai/docs',
40
+ codebuddy: 'https://www.codebuddy.cn/docs/cli/installation',
39
41
  kimi: 'https://moonshotai.github.io/kimi-code/en/guides/getting-started',
40
42
  zcode: 'https://zcode.z.ai/',
41
43
  dsh: 'https://github.com/deepseek-ai/deepseek-harness',
@@ -515,6 +517,27 @@ async function launchOpenCode(dirPath, { permissionMode } = {}) {
515
517
  return launchInTerminal(dirPath, 'opencode', cliArgs);
516
518
  }
517
519
 
520
+ /**
521
+ * 用 CodeBuddy Code 打开目录(交互 TUI,2026-10-12 随执行器接入一起补)。
522
+ *
523
+ * `--port 0` 是**必须**的:CLI 每次运行都自带一个引擎服务器,默认端口写死 62443,
524
+ * 而 WorkBuddy 桌面版常驻占着它 —— 实测裸跑 `codebuddy`(交互也一样)直接
525
+ * EADDRINUSE 退出。0 = 让 OS 选空闲端口,与 headless 执行器
526
+ * (taskRunner.launchCodebuddyPrint)同一个口径。
527
+ *
528
+ * 登录态同样来自 ~/.codebuddy:没登录过的话 TUI 里会提示 /login 扫码。
529
+ */
530
+ async function launchCodebuddy(dirPath) {
531
+ // CodeBuddy Code CLI - https://www.codebuddy.cn/docs/cli
532
+ const cliArgs = ['--port', '0'];
533
+ if (process.platform === 'win32') {
534
+ return spawnDetached('cmd.exe', ['/c', 'start', '""', 'codebuddy', ...cliArgs], {
535
+ cwd: dirPath
536
+ });
537
+ }
538
+ return launchInTerminal(dirPath, 'codebuddy', cliArgs);
539
+ }
540
+
518
541
  // Run the CLI shipped with this server, even when `g` is not in PATH or points
519
542
  // to another install. Directory paths remain spawn options, never CMD source.
520
543
  export async function launchGai(dirPath, {
@@ -1078,6 +1101,34 @@ export function registerFileOpenRoutes({
1078
1101
  }
1079
1102
  }));
1080
1103
 
1104
+ // 用 CodeBuddy Code 打开目录
1105
+ app.post('/api/open-directory-with-codebuddy', asyncRoute(async (req, res) => {
1106
+ try {
1107
+ const { path: dirPath } = req.body || {};
1108
+ if (!dirPath) {
1109
+ throw new HttpError(400, '目录路径不能为空');
1110
+ }
1111
+
1112
+ try {
1113
+ await fs.access(dirPath);
1114
+ } catch (error) {
1115
+ throw new HttpError(400, `目录不存在或不可访问: ${dirPath}`);
1116
+ }
1117
+
1118
+ try {
1119
+ await launchCodebuddy(dirPath);
1120
+ res.json({ success: true, message: '已用 CodeBuddy Code 打开目录' });
1121
+ } catch (error) {
1122
+ res.status(400).json({
1123
+ success: false,
1124
+ error: error.message || '未检测到 CodeBuddy Code,请先安装并确保可以在终端中直接运行 codebuddy'
1125
+ });
1126
+ }
1127
+ } catch (error) {
1128
+ res.status(500).json({ success: false, error: error.message });
1129
+ }
1130
+ }));
1131
+
1081
1132
  // 用 Kimi Code 打开目录
1082
1133
  app.post('/api/open-directory-with-kimi', asyncRoute(async (req, res) => {
1083
1134
  const { path: dirPath } = req.body || {};
@@ -1243,11 +1294,14 @@ export function registerFileOpenRoutes({
1243
1294
 
1244
1295
  // dsh / kimi 的版本采集依赖 find 结果,用 then 链接保持全并行
1245
1296
  // (find 会跑两次——一次拿路径一次链版本——都是纯 fs/注册表查询,开销可忽略)
1246
- const [vscodeRes, claudeRes, codexRes, opencodeRes, kimiExecutable, zcodeExecutable, dshExecutable, kimiVersion, dshVersion] = await Promise.all([
1297
+ // ⚠️ codebuddy 的 --version 是安全的:实测它只打印版本号就退出,不起引擎服务器
1298
+ // (裸跑 codebuddy 才会占 62443,与 checkCmd 的 --version 用法无关)。
1299
+ const [vscodeRes, claudeRes, codexRes, opencodeRes, codebuddyRes, kimiExecutable, zcodeExecutable, dshExecutable, kimiVersion, dshVersion] = await Promise.all([
1247
1300
  checkCmd('code'),
1248
1301
  checkCmd('claude'),
1249
1302
  checkCmd('codex'),
1250
1303
  checkCmd('opencode'),
1304
+ checkCmd('codebuddy'),
1251
1305
  findKimiExecutable(),
1252
1306
  findZCodeExecutable(),
1253
1307
  findDshExecutable(),
@@ -1264,6 +1318,7 @@ export function registerFileOpenRoutes({
1264
1318
  claude: claudeRes.installed,
1265
1319
  codex: codexRes.installed,
1266
1320
  opencode: opencodeRes.installed,
1321
+ codebuddy: codebuddyRes.installed,
1267
1322
  kimi: !!kimiExecutable,
1268
1323
  zcode: !!zcodeExecutable,
1269
1324
  dsh: !!dshExecutable,
@@ -1272,6 +1327,7 @@ export function registerFileOpenRoutes({
1272
1327
  claude: claudeRes.version,
1273
1328
  codex: codexRes.version,
1274
1329
  opencode: opencodeRes.version,
1330
+ codebuddy: codebuddyRes.version,
1275
1331
  kimi: kimiExecutable ? kimiVersion : null,
1276
1332
  zcode: null, // 桌面应用,无 CLI 版本通道
1277
1333
  dsh: dshExecutable ? dshVersion : null,
@@ -64,7 +64,7 @@ test('findDshExecutable: Windows 上返回 string 或 null,且非 null 时路径
64
64
 
65
65
  test('getToolInstallers: npm 包工具的升级变体带 @latest', () => {
66
66
  const installers = getToolInstallers('win32', () => true)
67
- for (const tool of ['claude', 'codex', 'opencode', 'dsh']) {
67
+ for (const tool of ['claude', 'codex', 'opencode', 'codebuddy', 'dsh']) {
68
68
  const inst = installers[tool]
69
69
  assert.match(inst.updateCommand, /@latest$/, `${tool}.updateCommand 应带 @latest`)
70
70
  assert.ok(inst.updateExecutable, `${tool}.updateExecutable 缺失`)
@@ -33,6 +33,7 @@ import { HttpError } from '../../utils/asyncRoute.js'
33
33
  import { assertGitRemoteUrl } from '../../utils/gitArgs.js'
34
34
  import { rememberRepo } from '../../utils/localRepoScan.js'
35
35
  import { augmentEnvPath } from '../../../../utils/shellPath.js'
36
+ import { repoNameFromUrl } from '../../../../utils/gitRepoName.js'
36
37
 
37
38
  const execFileAsync = promisify(execFile)
38
39
 
@@ -40,19 +41,10 @@ const execFileAsync = promisify(execFile)
40
41
  const CLONE_TIMEOUT_MS = 180_000
41
42
  const MAX_BUFFER = 10 * 1024 * 1024
42
43
 
43
- /**
44
- * 从远程地址里取出仓库目录名(clone 出来的子目录就用它)。
45
- * git@github.com:owner/repo.git → repo
46
- * https://gitee.com/owner/repo.git → repo
47
- * https://github.com/owner/repo/ → repo
48
- * 取不出来返回空串,调用方兜底成 repository。
49
- */
50
- export function repoNameFromUrl(url) {
51
- const s = String(url || '').trim().replace(/\/+$/, '')
52
- if (!s) return ''
53
- const tail = s.split(/[/:]/).filter(Boolean).pop() || ''
54
- return tail.replace(/\.git$/i, '')
55
- }
44
+ // repoNameFromUrl 的实现已挪到 utils/gitRepoName.js —— g ai 的 run_command 后处理
45
+ // 也要用它推导 git clone 的落点,两份同规则实现迟早分叉。这里保留 re-export,
46
+ // 既有调用点(clone.test.js)不必改。
47
+ export { repoNameFromUrl }
56
48
 
57
49
  /**
58
50
  * 校验父目录:必须是**已存在的目录**的绝对路径。
@@ -62,6 +62,9 @@ import configManager from '../../../../config.js';
62
62
  // 用户固定提示词(g ai 默认提示词 + 派发提示词)的取数与拼装。与外部 CLI 引擎那条
63
63
  // 链路共用同一份(见 runExternalTurn 的调用点),免得两块提示词在两处各拼一遍。
64
64
  import { resolveChatPromptSection } from './chatPrompts.js';
65
+ // 扩展层(Skill 清单 + MCP 工具)按 cwd 的加载缓存。CLI 在 agent.js 启动时加载;
66
+ // Web 会话长命,没有"进程重启"这个自然边界,改成按 cwd 缓存 + 广场安装时失效。
67
+ import { getWebExtensions } from './agentExtensions.js';
65
68
 
66
69
  // 单轮工具调用循环数的兜底值(防失控);实际值取全局配置 aiMaxToolIterations,
67
70
  // 与 CLI 侧 src/cli/ai/agent.js 共用同一个配置项。
@@ -189,6 +192,9 @@ ${isWin ? `- 当前是 Windows,以下 Unix 命令**不存在**,用了必定报"
189
192
  # 输出
190
193
  - 你的文本输出直接显示在用户 Web 界面,用简体中文交流
191
194
  - 界面按 Markdown 渲染:标题 / 列表 / 表格 / 引用 / 代码块都会排版后显示,别用 ASCII 画表格
195
+ - 想让用户**看到**图(截图/图表/示意图),就在回答正文里写 Markdown 图片 \`![说明](图片路径)\` ——
196
+ 本机绝对路径、或相对当前工作目录的路径都可以,界面会自动加载渲染;
197
+ 只贴一条路径、或把路径放进代码块,用户看到的只是一行字
192
198
  - 讲**流程 / 结构 / 时序 / 状态**这类"说出来不如画出来"的东西时,写 \`\`\`mermaid 代码块
193
199
  (flowchart / sequenceDiagram / stateDiagram-v2 / erDiagram …),界面会把它渲染成真正的图。
194
200
  不要用纯文本箭头、缩进或 ASCII 框线凑一张图 —— 那种"图"在界面上只是一段等宽文本
@@ -295,6 +301,10 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
295
301
  # Output
296
302
  - Your text output is displayed in the user's Web UI
297
303
  - The UI renders Markdown: headings / lists / tables / quotes / code blocks are shown formatted — do not fake tables with ASCII art
304
+ - To **show** the user an image (screenshot / chart / diagram), embed it in your reply as a Markdown
305
+ image \`![caption](path)\` — a local absolute path, or a path relative to the working directory,
306
+ both work and the UI loads and renders it; pasting a bare path, or wrapping it in a code block,
307
+ only shows the user a line of text
298
308
  - For **flows / structure / sequences / state machines**, prefer a \`\`\`mermaid block
299
309
  (flowchart / sequenceDiagram / stateDiagram-v2 / erDiagram …): the UI renders it as a real diagram.
300
310
  Never hand-draw one out of plain-text arrows, indentation or box characters — on screen that is just monospaced text
@@ -337,6 +347,9 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
337
347
  // - askUser: (args, meta) => Promise<string> 等待用户回答
338
348
  // - listProjects: (args) => Promise<string> list_projects 工具的数据源
339
349
  // (由 agentRoutes 注入:最近目录/tasks.json/看板统计只有 GUI 侧拿得到)
350
+ // - registerProject: (dir) => Promise<void> register_project 工具的实现,以及
351
+ // run_command 里"建了仓库就顺手登记进常用目录"那一步的落点(同一个注入、同一个
352
+ // 效果)。不注入 = 没有登记能力(CLI 与测试环境),对应工具回一句可照做的说明。
340
353
  // - dispatchTask: (payload) => Promise<string> dispatch_task 工具的实现 —— 派发一条
341
354
  // 工作台任务。同样由 agentRoutes 注入,但**只在主 Agent 控制台发起的对话里**注入
342
355
  // (别的入口不注入 = 那个入口没有派发能力,工具会回一句可照做的 unavailable)
@@ -356,9 +369,13 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
356
369
  // (单测要整体关掉自动重试,真等退避会让一个用例跑成分钟级),
357
370
  // 生产调用点一律不传 —— 传了就等于换了一份传输实现。
358
371
  //
372
+ // 扩展层(Skill + MCP)不在参数里:按 cwd 从 agentExtensions.js 的进程内缓存取,
373
+ // 整轮一次。三条路径都与 CLI turn.js 对齐 —— 工具表叠 extraTools、调用按 owns()
374
+ // 分流、摘要注进请求副本(agentParity.test.js 有守卫)。
375
+ //
359
376
  // 返回: { aborted: boolean }
360
- export async function runAgentTurn({ session, model, userMessage, images = [], cwd, locale, openFilePath, attachments = [], dirStatusBlock = '', signal, send, onChild, askUser, listProjects, dispatchTask, getContextBlock, requestBudget = null, resume = false, streamFn = streamChatOnce, promptSection }) {
361
- const ctx = { cwd, locale, onChild, askUser, listProjects, dispatchTask };
377
+ export async function runAgentTurn({ session, model, userMessage, images = [], cwd, locale, openFilePath, attachments = [], dirStatusBlock = '', signal, send, onChild, askUser, listProjects, registerProject, dispatchTask, getContextBlock, requestBudget = null, resume = false, streamFn = streamChatOnce, promptSection }) {
378
+ const ctx = { cwd, locale, onChild, askUser, listProjects, registerProject, dispatchTask };
362
379
 
363
380
  // 确保 session.messages 存在
364
381
  if (!Array.isArray(session.messages)) session.messages = [];
@@ -427,6 +444,28 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
427
444
  // 可能不同,UI 上的进度条就会说谎)。
428
445
  const budget = requestBudget || resolveRequestBudget();
429
446
 
447
+ // 扩展层(Skill 清单 + MCP 工具):按 cwd 进程内缓存,整轮只取一次。
448
+ // CLI 把 promptSuffix bake 进首条 system 消息(进程启动时);Web 会话是长命的
449
+ // (可以活好几天,期间用户随时可能在广场装新的),所以这里每轮从缓存取、
450
+ // 摘要注进**请求副本** —— 装完立刻生效、老会话也吃得到,也不把当时的清单腌进
451
+ // 磁盘历史(口径同工作区快照:只进副本,不落 session.messages)。
452
+ // 加载失败已在 agentExtensions.js 里降级成空扩展,这里不会抛。
453
+ const extensions = await getWebExtensions({ cwd, locale });
454
+
455
+ // MCP 工具按名字分流,口径与 CLI turn.js 逐字对齐:名字属于扩展就走扩展
456
+ // (McpManager.call 内部已把失败翻成错误字符串,不会抛进循环),扩展返回
457
+ // null/undefined(加载后被换掉等边角)时回落内置工具。没装扩展时 owns() 恒为
458
+ // false,等价于直接调内置工具,零开销。
459
+ const executeOne = extensions
460
+ ? async (name, args, toolCtx) => {
461
+ if (extensions.owns?.(name)) {
462
+ const output = await extensions.execute(name, args, toolCtx);
463
+ if (output !== null && output !== undefined) return output;
464
+ }
465
+ return executeTool(name, args, toolCtx);
466
+ }
467
+ : executeTool;
468
+
430
469
  for (let iter = 0; iter < maxIterations; iter++) {
431
470
  // 每轮都从完整会话记录重新构建一次请求副本:条数/token 双预算 → 被丢掉的旧消息
432
471
  // 摘录成一条梗概 → 旧图片降级 → provider 兼容消毒。
@@ -434,8 +473,8 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
434
473
  // 预算来自 requestBudget(全局配置 aiMaxRequestTokens 的解析结果)。
435
474
  const messages = prepareRequestMessages(session.messages, { locale, ...budget });
436
475
  // 请求级上下文:用户固定提示词 + 工作区状态快照 + 常用目录状态 + 当前打开的文档
437
- // + 本轮附件路径(只改副本,不落 session.messages,下一轮不重复累积)
438
- injectRequestContext(messages, { cwd, openFilePath, attachments, locale, workspaceBlock, dirStatusBlock, promptSection: userPromptSection });
476
+ // + 本轮附件路径 + 扩展层摘要(只改副本,不落 session.messages,下一轮不重复累积)
477
+ injectRequestContext(messages, { cwd, openFilePath, attachments, locale, workspaceBlock, dirStatusBlock, promptSection: userPromptSection, extensionSection: extensionPromptSuffix(extensions, locale) });
439
478
 
440
479
  // 上下文占用:在**请求发出去之前**量一次,发给 UI 画圆环。
441
480
  // 为什么必须在发之前:provider 的真实 usage(input_tokens)要等响应回来才有,
@@ -454,6 +493,9 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
454
493
  model,
455
494
  messages,
456
495
  signal,
496
+ // 内置工具是底座;extraTools 是本轮 cwd 的 MCP 工具叠加(transport 里合并,
497
+ // 为空时回落纯内置表)。CLI turn.js 同一个参数,两边口径一致。
498
+ extraTools: extensions?.tools,
457
499
  // 整轮对话(含后续工具调用产生的每一轮请求)复用同一个会话 ID
458
500
  sessionId: session.sessionId,
459
501
  onToken: ({ thinking, content }) => {
@@ -562,7 +604,7 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
562
604
  signal,
563
605
  });
564
606
  }
565
- const output = await executeTool(name, args, toolCtx);
607
+ const output = await executeOne(name, args, toolCtx);
566
608
  // read_image 会返回 { text, images }。前端只吃文本(result 是字符串,
567
609
  // 直接把对象丢过去会渲染成 "[object Object]"),图片进会话历史里的
568
610
  // 多模态 tool 消息 —— 模型看得见图,用户看到的是"已读取图片 xxx.png"。
@@ -578,6 +620,16 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
578
620
  return { aborted: false };
579
621
  }
580
622
 
623
+ // 扩展层摘要(Skill 清单 + MCP 连接状态)。取不到就当没有 —— 它是增量能力,
624
+ // 不该因为摘要拼不出来影响整轮对话。口径与 CLI 的 extensionSuffix 同一份文本。
625
+ function extensionPromptSuffix(extensions, locale) {
626
+ try {
627
+ return extensions?.promptSuffix?.(locale) || '';
628
+ } catch {
629
+ return '';
630
+ }
631
+ }
632
+
581
633
  // 工具参数简短摘要(给前端展示)
582
634
  function summarizeArgs(name, args) {
583
635
  try {
@@ -606,10 +658,11 @@ function summarizeArgs(name, args) {
606
658
  }
607
659
  }
608
660
 
609
- // ── 请求级上下文注入(用户固定提示词 / 工作区状态 / 常用目录状态 / 文件空间对话 / 本轮附件) ──
661
+ // ── 请求级上下文注入(用户固定提示词 / 工作区状态 / 常用目录状态 / 文件空间对话 / 本轮附件 / 扩展层摘要) ──
610
662
  // 把"用户配的两块固定提示词""工作区各板块的状态摘要""常用目录那批目录的 Git 状态"
611
- // "用户当前打开的文件"与"本轮附件的落盘路径"追加到**请求副本**的 system 消息末尾:
612
- // 只影响这一次请求,session.messages 与磁盘历史保持原样,下一轮也不会重复累积。
663
+ // "用户当前打开的文件""本轮附件的落盘路径"与"扩展层摘要(Skill + MCP)"追加到
664
+ // **请求副本**的 system 消息末尾:只影响这一次请求,session.messages 与磁盘历史保持
665
+ // 原样,下一轮也不会重复累积。
613
666
  //
614
667
  // ⚠️ 工作区快照**必须走这条副本路径,不能塞进 session.messages 里那条 system 消息**。
615
668
  // 那条只在首轮 push 一次(见上面 `session.messages.length === 0` 的判断)并会落盘,
@@ -618,11 +671,13 @@ function summarizeArgs(name, args) {
618
671
  // 常用目录状态同理,而且更严重:它说的是"这一刻"的领先/落后,几小时后必然不同。
619
672
  // 用户固定提示词同理,但理由换一条:**改设置要立刻生效**,而且它属于"每轮都在的规则",
620
673
  // 腌进历史会话等于让用户回看时看到一份过期规则(详见 chatPrompts.js 文件头)。
674
+ // 扩展层摘要同理,而且它还多一条:Web 会话长命,bake 进去等于"这个会话再也吃不到
675
+ // 新装的 MCP/Skill"——CLI 每启动一个进程重算一次所以没这个问题,Web 必须每轮注副本。
621
676
  //
622
677
  // 附件为什么只给路径、不给内容:见 utils/agentAttachments.js 的头注释 —— 非图片附件
623
678
  // 由服务端落盘,模型自己用 read / grep 按需取,比把几百 KB 文本内联进消息省得多。
624
679
  // 快照块同理,只给摘要与目录路径,板块正文由模型按需读。
625
- export function injectRequestContext(messages, { cwd, openFilePath, attachments = [], locale, workspaceBlock = '', dirStatusBlock = '', promptSection = '' }) {
680
+ export function injectRequestContext(messages, { cwd, openFilePath, attachments = [], locale, workspaceBlock = '', dirStatusBlock = '', promptSection = '', extensionSection = '' }) {
626
681
  if (!Array.isArray(messages)) return;
627
682
  const en = String(locale || '').startsWith('en');
628
683
 
@@ -668,7 +723,14 @@ export function injectRequestContext(messages, { cwd, openFilePath, attachments
668
723
  : `用户本轮附带了 ${files.length} 个文件,已保存到以下绝对路径:\n${lines.join('\n')}\n需要时用工具读取(内容没有内联在这里),不要臆测。`);
669
724
  }
670
725
 
671
- if (parts.length === 0 && !(typeof promptSection === 'string' && promptSection.trim())) return;
726
+ // ④ 扩展层摘要(Skill 清单 + MCP 连接状态)排在最后:与 CLI 一致 ——
727
+ // 扩展是"增量能力",不该插在权限/环境/规则中间打断阅读。
728
+ // promptSuffix 自带 `# 已安装的 Skill` / `# 已接入的 MCP 服务` 标题,
729
+ // 且明说"这些外部工具已挂到你的工具列表里",正好接住基础提示词里那句
730
+ // "可用工具只有上面列出的 N 个"(N=内置数)—— 两边不打架。
731
+ const hasExtensionSection = typeof extensionSection === 'string' && extensionSection.trim();
732
+
733
+ if (parts.length === 0 && !(typeof promptSection === 'string' && promptSection.trim()) && !hasExtensionSection) return;
672
734
 
673
735
  // 用户固定提示词单独一节,且排在"当前上下文"**前面**:它是每轮都成立的规则,
674
736
  // 而当前上下文是这一刻的快照 —— 规则在前、现场在后,读起来才是"先约束后事实"。
@@ -679,6 +741,9 @@ export function injectRequestContext(messages, { cwd, openFilePath, attachments
679
741
  if (parts.length > 0) {
680
742
  note += `\n\n# ${en ? 'Current context' : '当前上下文'}\n${parts.join('\n\n')}`;
681
743
  }
744
+ if (hasExtensionSection) {
745
+ note += `\n\n${extensionSection.trim()}`;
746
+ }
682
747
  const sys = messages.find(m => m && m.role === 'system' && typeof m.content === 'string');
683
748
  if (sys) sys.content += note;
684
749
  else messages.unshift({ role: 'system', content: note.trim() });