zen-gitsync 2.18.7 → 2.18.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/src/cli/ai/agent.js +7 -0
- package/src/cli/ai/runtime.test.js +22 -1
- package/src/cli/ai/tools.js +1 -1
- package/src/cli/ai/tools.test.js +1 -1
- package/src/cli/ai/transport.js +8 -3
- package/src/config.js +48 -6
- package/src/mcp/tools/workbench.js +1 -1
- package/src/ui/public/assets/{AgentView-am93H7C2.css → AgentView-CV_8sYX6.css} +1 -1
- package/src/ui/public/assets/AgentView-DxmnqJxE.js +1 -0
- package/src/ui/public/assets/{AppVersionBadge-D7X53Bf1.js → AppVersionBadge-C4OhcUi8.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-yXxfIs57.js → BranchSelector-CrLieQyJ.js} +1 -1
- package/src/ui/public/assets/{CommitForm-C8Ez91_c.js → CommitForm-KGQisnPQ.js} +2 -2
- package/src/ui/public/assets/{CommonDialog-BUuLsgNS.js → CommonDialog-j_53ORvj.js} +1 -1
- package/src/ui/public/assets/CopySessionButton-y5Rzmact.js +20 -0
- package/src/ui/public/assets/EditorView-CkZb2kw5.js +1 -0
- package/src/ui/public/assets/{EditorView-BUiVsBv7.css → EditorView-Ims2BUK_.css} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-BuU5kQG8.js → FlowExecutionViewer-CQ-TPhwF.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-CTBEooj5.js → FlowOrchestrationWorkspace-BMQSVm3V.js} +1 -1
- package/src/ui/public/assets/{LogList-Bq5aLoA-.js → LogList-BxH6QNlK.js} +4 -4
- package/src/ui/public/assets/{MindmapView-CSysrruz.js → MindmapView-D9_qjJsR.js} +1 -1
- package/src/ui/public/assets/{MonitorView-Cgf5zRxT.js → MonitorView-z_fsnE8Y.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-CrLWTK20.js → ProjectStartupButton-CFB_JpYG.js} +1 -1
- package/src/ui/public/assets/RecentDirectoriesChat-7fzSkSmc.css +1 -0
- package/src/ui/public/assets/RecentDirectoriesChat-BYI8lkjQ.js +1 -0
- package/src/ui/public/assets/{RemoteManagerDialog-CJZFzQPc.js → RemoteManagerDialog-DXDcN8e7.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-aSx1ikZ8.js → RemoteRepoCard-C_TYGsXi.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-DEljf14g.js → SourceMapView-D6cSweDt.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-Du5MOe8A.js → SvgIcon-5AlG3IfR.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-oOQwaadU.js → UserInputNode-BKP869D6.js} +1 -1
- package/src/ui/public/assets/WorkbenchView-D4UDurhF.js +6 -0
- package/src/ui/public/assets/WorkbenchView-DogERhHQ.css +1 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-Csn085FF.js +19 -0
- package/src/ui/public/assets/agentConversations-DyzaxUXm.js +10 -0
- package/src/ui/public/assets/configStore-DOaPZ6Mq.js +1 -0
- package/src/ui/public/assets/index-C7Y9odvZ.js +57 -0
- package/src/ui/public/assets/index-NYzaTfbS.css +1 -0
- package/src/ui/public/assets/useOrchestrator-C236G5P5.js +1 -0
- package/src/ui/public/index.html +6 -6
- package/src/ui/server/index.js +8 -0
- package/src/ui/server/routes/config.js +13 -0
- package/src/ui/server/routes/fileOpen.js +57 -1
- package/src/ui/server/routes/fileOpen.test.js +1 -1
- package/src/ui/server/routes/workbench/agentChat.js +106 -12
- package/src/ui/server/routes/workbench/agentChatExtensions.test.js +290 -0
- package/src/ui/server/routes/workbench/agentChatShared.test.js +148 -57
- package/src/ui/server/routes/workbench/agentEngines.js +8 -4
- package/src/ui/server/routes/workbench/agentExtensions.js +157 -0
- package/src/ui/server/routes/workbench/agentMarketplace.js +12 -0
- package/src/ui/server/routes/workbench/agentParity.test.js +16 -0
- package/src/ui/server/routes/workbench/agentRoutes.js +105 -8
- package/src/ui/server/routes/workbench/agentRoutes.test.js +141 -1
- package/src/ui/server/routes/workbench/chatPrompts.js +115 -0
- package/src/ui/server/routes/workbench/chatPrompts.test.js +114 -0
- package/src/ui/server/routes/workbench/executorModels.js +30 -5
- package/src/ui/server/routes/workbench/executorModels.test.js +16 -4
- package/src/ui/server/routes/workbench/index.js +1 -1
- package/src/ui/server/routes/workbench/jobActivity.js +3 -3
- package/src/ui/server/routes/workbench/jobActivity.test.js +1 -0
- package/src/ui/server/routes/workbench/runExternalTurn.js +22 -25
- package/src/ui/server/routes/workbench/runExternalTurn.test.js +34 -0
- package/src/ui/server/routes/workbench/taskRunner.codebuddy.test.js +275 -0
- package/src/ui/server/routes/workbench/taskRunner.js +311 -24
- package/src/ui/server/routes/workbench/taskRunner.opencode.test.js +2 -1
- package/src/ui/public/assets/AgentView-BlG0PdkI.js +0 -1
- package/src/ui/public/assets/CopySessionButton-Do4vHJm6.js +0 -20
- package/src/ui/public/assets/EditorView-DK2dYRVb.js +0 -1
- package/src/ui/public/assets/RecentDirectoriesChat-B19uAf8k.css +0 -1
- package/src/ui/public/assets/RecentDirectoriesChat-BlIG_9G4.js +0 -1
- package/src/ui/public/assets/WorkbenchView-D6NRqZ5W.css +0 -1
- package/src/ui/public/assets/WorkbenchView-DXzxg5_z.js +0 -8
- package/src/ui/public/assets/_plugin-vue_export-helper-CDrhhAKC.js +0 -19
- package/src/ui/public/assets/agentConversations-BKjyYn-u.js +0 -8
- package/src/ui/public/assets/configStore-D0oycG71.js +0 -1
- package/src/ui/public/assets/index-DjaZpz97.js +0 -57
- package/src/ui/public/assets/index-DwJWiCiN.css +0 -1
|
@@ -0,0 +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-Csn085FF.js";import{t as i}from"./configStore-DOaPZ6Mq.js";import{m as a}from"./index-C7Y9odvZ.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};
|
package/src/ui/public/index.html
CHANGED
|
@@ -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-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-C7Y9odvZ.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-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-
|
|
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-
|
|
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-
|
|
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-
|
|
33
|
+
<link rel="stylesheet" crossorigin href="/assets/index-NYzaTfbS.css">
|
|
34
34
|
</head>
|
|
35
35
|
<body>
|
|
36
36
|
<div id="app"></div>
|
package/src/ui/server/index.js
CHANGED
|
@@ -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:模块早就在
|
|
@@ -1138,6 +1138,19 @@ export function registerConfigRoutes({
|
|
|
1138
1138
|
}
|
|
1139
1139
|
updates.agentPresetPrompts = normalized
|
|
1140
1140
|
}
|
|
1141
|
+
// g ai 智能体的默认提示词(全局配置):和 g ai 对话时每轮拼进系统提示词。
|
|
1142
|
+
// **空串是合法值**(= 清除),所以判空用 !== undefined;非字符串整体拒绝(400),
|
|
1143
|
+
// 与 normalizeAgentDefaultPrompt 的语义一致。
|
|
1144
|
+
if (body.agentDefaultPrompt !== undefined) {
|
|
1145
|
+
const normalized = configManager.normalizeAgentDefaultPrompt(body.agentDefaultPrompt)
|
|
1146
|
+
if (normalized === null) {
|
|
1147
|
+
return res.status(400).json({
|
|
1148
|
+
success: false,
|
|
1149
|
+
error: 'agentDefaultPrompt 非法: 需要字符串'
|
|
1150
|
+
})
|
|
1151
|
+
}
|
|
1152
|
+
updates.agentDefaultPrompt = normalized
|
|
1153
|
+
}
|
|
1141
1154
|
if (Object.keys(updates).length === 0) {
|
|
1142
1155
|
return res.status(400).json({ success: false, error: '缺少可保存的参数' })
|
|
1143
1156
|
}
|
|
@@ -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
|
-
|
|
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 缺失`)
|
|
@@ -59,6 +59,12 @@ import { streamChatOnce } from '../../../../cli/ai/transport.js';
|
|
|
59
59
|
import { checkDangerousCommand } from '../../../../cli/ai/safety.js';
|
|
60
60
|
import { guardCommand } from '../../../../cli/ai/platformGuard.js';
|
|
61
61
|
import configManager from '../../../../config.js';
|
|
62
|
+
// 用户固定提示词(g ai 默认提示词 + 派发提示词)的取数与拼装。与外部 CLI 引擎那条
|
|
63
|
+
// 链路共用同一份(见 runExternalTurn 的调用点),免得两块提示词在两处各拼一遍。
|
|
64
|
+
import { resolveChatPromptSection } from './chatPrompts.js';
|
|
65
|
+
// 扩展层(Skill 清单 + MCP 工具)按 cwd 的加载缓存。CLI 在 agent.js 启动时加载;
|
|
66
|
+
// Web 会话长命,没有"进程重启"这个自然边界,改成按 cwd 缓存 + 广场安装时失效。
|
|
67
|
+
import { getWebExtensions } from './agentExtensions.js';
|
|
62
68
|
|
|
63
69
|
// 单轮工具调用循环数的兜底值(防失控);实际值取全局配置 aiMaxToolIterations,
|
|
64
70
|
// 与 CLI 侧 src/cli/ai/agent.js 共用同一个配置项。
|
|
@@ -186,6 +192,9 @@ ${isWin ? `- 当前是 Windows,以下 Unix 命令**不存在**,用了必定报"
|
|
|
186
192
|
# 输出
|
|
187
193
|
- 你的文本输出直接显示在用户 Web 界面,用简体中文交流
|
|
188
194
|
- 界面按 Markdown 渲染:标题 / 列表 / 表格 / 引用 / 代码块都会排版后显示,别用 ASCII 画表格
|
|
195
|
+
- 想让用户**看到**图(截图/图表/示意图),就在回答正文里写 Markdown 图片 \`\` ——
|
|
196
|
+
本机绝对路径、或相对当前工作目录的路径都可以,界面会自动加载渲染;
|
|
197
|
+
只贴一条路径、或把路径放进代码块,用户看到的只是一行字
|
|
189
198
|
- 讲**流程 / 结构 / 时序 / 状态**这类"说出来不如画出来"的东西时,写 \`\`\`mermaid 代码块
|
|
190
199
|
(flowchart / sequenceDiagram / stateDiagram-v2 / erDiagram …),界面会把它渲染成真正的图。
|
|
191
200
|
不要用纯文本箭头、缩进或 ASCII 框线凑一张图 —— 那种"图"在界面上只是一段等宽文本
|
|
@@ -292,6 +301,10 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
|
|
|
292
301
|
# Output
|
|
293
302
|
- Your text output is displayed in the user's Web UI
|
|
294
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 \`\` — 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
|
|
295
308
|
- For **flows / structure / sequences / state machines**, prefer a \`\`\`mermaid block
|
|
296
309
|
(flowchart / sequenceDiagram / stateDiagram-v2 / erDiagram …): the UI renders it as a real diagram.
|
|
297
310
|
Never hand-draw one out of plain-text arrows, indentation or box characters — on screen that is just monospaced text
|
|
@@ -342,6 +355,10 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
|
|
|
342
355
|
// - requestBudget: { maxChars, maxMessages, maxUserChars } 每轮请求的上下文预算
|
|
343
356
|
// (解析自全局配置 aiMaxRequestChars,见 cli/ai/context.js 的 resolveRequestBudget)。
|
|
344
357
|
// 不传时 prepareRequestMessages 走默认值,行为与改造前一致。
|
|
358
|
+
// - promptSection: 用户固定提示词(g ai 默认提示词 + 派发提示词,见 chatPrompts.js)。
|
|
359
|
+
// **不传(undefined)= 自己现读**:这样定时任务那条链路(scheduler.js 直接调
|
|
360
|
+
// 本函数)不必重复接一次线。显式传 '' = 本轮确定不带(调用方已经解析过)。
|
|
361
|
+
// 只进请求副本,不落 session.messages —— 改设置立刻生效,历史会话不被腌进旧规则。
|
|
345
362
|
// - resume: true 时**不再压入 user 消息**,直接拿着会话现有的尾巴继续跑。
|
|
346
363
|
// 用于"重试上一轮":失败那轮留下的 user + 工具结果都还在,再发一遍 user
|
|
347
364
|
// 会让模型看到两条同样的请求 + 已经跑完的工具结果。
|
|
@@ -349,8 +366,12 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
|
|
|
349
366
|
// (单测要整体关掉自动重试,真等退避会让一个用例跑成分钟级),
|
|
350
367
|
// 生产调用点一律不传 —— 传了就等于换了一份传输实现。
|
|
351
368
|
//
|
|
369
|
+
// 扩展层(Skill + MCP)不在参数里:按 cwd 从 agentExtensions.js 的进程内缓存取,
|
|
370
|
+
// 整轮一次。三条路径都与 CLI turn.js 对齐 —— 工具表叠 extraTools、调用按 owns()
|
|
371
|
+
// 分流、摘要注进请求副本(agentParity.test.js 有守卫)。
|
|
372
|
+
//
|
|
352
373
|
// 返回: { aborted: boolean }
|
|
353
|
-
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 }) {
|
|
374
|
+
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 }) {
|
|
354
375
|
const ctx = { cwd, locale, onChild, askUser, listProjects, dispatchTask };
|
|
355
376
|
|
|
356
377
|
// 确保 session.messages 存在
|
|
@@ -400,20 +421,57 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
|
|
|
400
421
|
|
|
401
422
|
const maxIterations = await resolveMaxToolIterations();
|
|
402
423
|
|
|
424
|
+
// 用户固定提示词(g ai 默认提示词 + 派发提示词):整轮只解析一次 —— 和快照同理,
|
|
425
|
+
// 它在这一轮里不会变,而工具循环每轮都要重拼一次请求。
|
|
426
|
+
// 调用方已经解析过(agentRoutes 为了外部引擎那条链路先解了一次)时不重复读文件;
|
|
427
|
+
// 显式传 '' 同样尊重 —— 那是"确定没有",不是"没传"。
|
|
428
|
+
let userPromptSection = promptSection;
|
|
429
|
+
if (userPromptSection === undefined) {
|
|
430
|
+
try {
|
|
431
|
+
userPromptSection = await resolveChatPromptSection({ cwd, locale });
|
|
432
|
+
} catch (err) {
|
|
433
|
+
// 取不到就当没配:这是锦上添花,不能因为它挂掉就让用户这条消息发不出去
|
|
434
|
+
logger.warn(`[agentChat] 取用户固定提示词失败,本轮跳过注入: ${err?.message || err}`);
|
|
435
|
+
userPromptSection = '';
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
403
439
|
// 预算缺省时在**这里**解析一次,与 prepareRequestMessages 的派生口径一致
|
|
404
440
|
// (不能让它自己兜默认:那样 measureContextUsage 拿到的分母会和实际裁剪用的分母
|
|
405
441
|
// 可能不同,UI 上的进度条就会说谎)。
|
|
406
442
|
const budget = requestBudget || resolveRequestBudget();
|
|
407
443
|
|
|
444
|
+
// 扩展层(Skill 清单 + MCP 工具):按 cwd 进程内缓存,整轮只取一次。
|
|
445
|
+
// CLI 把 promptSuffix bake 进首条 system 消息(进程启动时);Web 会话是长命的
|
|
446
|
+
// (可以活好几天,期间用户随时可能在广场装新的),所以这里每轮从缓存取、
|
|
447
|
+
// 摘要注进**请求副本** —— 装完立刻生效、老会话也吃得到,也不把当时的清单腌进
|
|
448
|
+
// 磁盘历史(口径同工作区快照:只进副本,不落 session.messages)。
|
|
449
|
+
// 加载失败已在 agentExtensions.js 里降级成空扩展,这里不会抛。
|
|
450
|
+
const extensions = await getWebExtensions({ cwd, locale });
|
|
451
|
+
|
|
452
|
+
// MCP 工具按名字分流,口径与 CLI turn.js 逐字对齐:名字属于扩展就走扩展
|
|
453
|
+
// (McpManager.call 内部已把失败翻成错误字符串,不会抛进循环),扩展返回
|
|
454
|
+
// null/undefined(加载后被换掉等边角)时回落内置工具。没装扩展时 owns() 恒为
|
|
455
|
+
// false,等价于直接调内置工具,零开销。
|
|
456
|
+
const executeOne = extensions
|
|
457
|
+
? async (name, args, toolCtx) => {
|
|
458
|
+
if (extensions.owns?.(name)) {
|
|
459
|
+
const output = await extensions.execute(name, args, toolCtx);
|
|
460
|
+
if (output !== null && output !== undefined) return output;
|
|
461
|
+
}
|
|
462
|
+
return executeTool(name, args, toolCtx);
|
|
463
|
+
}
|
|
464
|
+
: executeTool;
|
|
465
|
+
|
|
408
466
|
for (let iter = 0; iter < maxIterations; iter++) {
|
|
409
467
|
// 每轮都从完整会话记录重新构建一次请求副本:条数/token 双预算 → 被丢掉的旧消息
|
|
410
468
|
// 摘录成一条梗概 → 旧图片降级 → provider 兼容消毒。
|
|
411
469
|
// 只作用于副本,session.messages 保持完整(与 CLI 的磁盘口径一致)。
|
|
412
470
|
// 预算来自 requestBudget(全局配置 aiMaxRequestTokens 的解析结果)。
|
|
413
471
|
const messages = prepareRequestMessages(session.messages, { locale, ...budget });
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
injectRequestContext(messages, { cwd, openFilePath, attachments, locale, workspaceBlock, dirStatusBlock });
|
|
472
|
+
// 请求级上下文:用户固定提示词 + 工作区状态快照 + 常用目录状态 + 当前打开的文档
|
|
473
|
+
// + 本轮附件路径 + 扩展层摘要(只改副本,不落 session.messages,下一轮不重复累积)
|
|
474
|
+
injectRequestContext(messages, { cwd, openFilePath, attachments, locale, workspaceBlock, dirStatusBlock, promptSection: userPromptSection, extensionSection: extensionPromptSuffix(extensions, locale) });
|
|
417
475
|
|
|
418
476
|
// 上下文占用:在**请求发出去之前**量一次,发给 UI 画圆环。
|
|
419
477
|
// 为什么必须在发之前:provider 的真实 usage(input_tokens)要等响应回来才有,
|
|
@@ -432,6 +490,9 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
|
|
|
432
490
|
model,
|
|
433
491
|
messages,
|
|
434
492
|
signal,
|
|
493
|
+
// 内置工具是底座;extraTools 是本轮 cwd 的 MCP 工具叠加(transport 里合并,
|
|
494
|
+
// 为空时回落纯内置表)。CLI turn.js 同一个参数,两边口径一致。
|
|
495
|
+
extraTools: extensions?.tools,
|
|
435
496
|
// 整轮对话(含后续工具调用产生的每一轮请求)复用同一个会话 ID
|
|
436
497
|
sessionId: session.sessionId,
|
|
437
498
|
onToken: ({ thinking, content }) => {
|
|
@@ -540,7 +601,7 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
|
|
|
540
601
|
signal,
|
|
541
602
|
});
|
|
542
603
|
}
|
|
543
|
-
const output = await
|
|
604
|
+
const output = await executeOne(name, args, toolCtx);
|
|
544
605
|
// read_image 会返回 { text, images }。前端只吃文本(result 是字符串,
|
|
545
606
|
// 直接把对象丢过去会渲染成 "[object Object]"),图片进会话历史里的
|
|
546
607
|
// 多模态 tool 消息 —— 模型看得见图,用户看到的是"已读取图片 xxx.png"。
|
|
@@ -556,6 +617,16 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
|
|
|
556
617
|
return { aborted: false };
|
|
557
618
|
}
|
|
558
619
|
|
|
620
|
+
// 扩展层摘要(Skill 清单 + MCP 连接状态)。取不到就当没有 —— 它是增量能力,
|
|
621
|
+
// 不该因为摘要拼不出来影响整轮对话。口径与 CLI 的 extensionSuffix 同一份文本。
|
|
622
|
+
function extensionPromptSuffix(extensions, locale) {
|
|
623
|
+
try {
|
|
624
|
+
return extensions?.promptSuffix?.(locale) || '';
|
|
625
|
+
} catch {
|
|
626
|
+
return '';
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
559
630
|
// 工具参数简短摘要(给前端展示)
|
|
560
631
|
function summarizeArgs(name, args) {
|
|
561
632
|
try {
|
|
@@ -584,21 +655,26 @@ function summarizeArgs(name, args) {
|
|
|
584
655
|
}
|
|
585
656
|
}
|
|
586
657
|
|
|
587
|
-
// ──
|
|
588
|
-
// 把"工作区各板块的状态摘要""常用目录那批目录的 Git 状态"
|
|
589
|
-
// "本轮附件的落盘路径"
|
|
590
|
-
// session.messages
|
|
658
|
+
// ── 请求级上下文注入(用户固定提示词 / 工作区状态 / 常用目录状态 / 文件空间对话 / 本轮附件 / 扩展层摘要) ──
|
|
659
|
+
// 把"用户配的两块固定提示词""工作区各板块的状态摘要""常用目录那批目录的 Git 状态"
|
|
660
|
+
// "用户当前打开的文件""本轮附件的落盘路径"与"扩展层摘要(Skill + MCP)"追加到
|
|
661
|
+
// **请求副本**的 system 消息末尾:只影响这一次请求,session.messages 与磁盘历史保持
|
|
662
|
+
// 原样,下一轮也不会重复累积。
|
|
591
663
|
//
|
|
592
664
|
// ⚠️ 工作区快照**必须走这条副本路径,不能塞进 session.messages 里那条 system 消息**。
|
|
593
665
|
// 那条只在首轮 push 一次(见上面 `session.messages.length === 0` 的判断)并会落盘,
|
|
594
666
|
// 快照进去就等于永久停在"会话创建那天"——git 分支、任务进度全会是过期的,
|
|
595
667
|
// 而且这种错不会报错、只会让模型理直气壮地给出错答案。有单测钉住这一点。
|
|
596
668
|
// 常用目录状态同理,而且更严重:它说的是"这一刻"的领先/落后,几小时后必然不同。
|
|
669
|
+
// 用户固定提示词同理,但理由换一条:**改设置要立刻生效**,而且它属于"每轮都在的规则",
|
|
670
|
+
// 腌进历史会话等于让用户回看时看到一份过期规则(详见 chatPrompts.js 文件头)。
|
|
671
|
+
// 扩展层摘要同理,而且它还多一条:Web 会话长命,bake 进去等于"这个会话再也吃不到
|
|
672
|
+
// 新装的 MCP/Skill"——CLI 每启动一个进程重算一次所以没这个问题,Web 必须每轮注副本。
|
|
597
673
|
//
|
|
598
674
|
// 附件为什么只给路径、不给内容:见 utils/agentAttachments.js 的头注释 —— 非图片附件
|
|
599
675
|
// 由服务端落盘,模型自己用 read / grep 按需取,比把几百 KB 文本内联进消息省得多。
|
|
600
676
|
// 快照块同理,只给摘要与目录路径,板块正文由模型按需读。
|
|
601
|
-
export function injectRequestContext(messages, { cwd, openFilePath, attachments = [], locale, workspaceBlock = '', dirStatusBlock = '' }) {
|
|
677
|
+
export function injectRequestContext(messages, { cwd, openFilePath, attachments = [], locale, workspaceBlock = '', dirStatusBlock = '', promptSection = '', extensionSection = '' }) {
|
|
602
678
|
if (!Array.isArray(messages)) return;
|
|
603
679
|
const en = String(locale || '').startsWith('en');
|
|
604
680
|
|
|
@@ -644,9 +720,27 @@ export function injectRequestContext(messages, { cwd, openFilePath, attachments
|
|
|
644
720
|
: `用户本轮附带了 ${files.length} 个文件,已保存到以下绝对路径:\n${lines.join('\n')}\n需要时用工具读取(内容没有内联在这里),不要臆测。`);
|
|
645
721
|
}
|
|
646
722
|
|
|
647
|
-
|
|
723
|
+
// ④ 扩展层摘要(Skill 清单 + MCP 连接状态)排在最后:与 CLI 一致 ——
|
|
724
|
+
// 扩展是"增量能力",不该插在权限/环境/规则中间打断阅读。
|
|
725
|
+
// promptSuffix 自带 `# 已安装的 Skill` / `# 已接入的 MCP 服务` 标题,
|
|
726
|
+
// 且明说"这些外部工具已挂到你的工具列表里",正好接住基础提示词里那句
|
|
727
|
+
// "可用工具只有上面列出的 N 个"(N=内置数)—— 两边不打架。
|
|
728
|
+
const hasExtensionSection = typeof extensionSection === 'string' && extensionSection.trim();
|
|
648
729
|
|
|
649
|
-
|
|
730
|
+
if (parts.length === 0 && !(typeof promptSection === 'string' && promptSection.trim()) && !hasExtensionSection) return;
|
|
731
|
+
|
|
732
|
+
// 用户固定提示词单独一节,且排在"当前上下文"**前面**:它是每轮都成立的规则,
|
|
733
|
+
// 而当前上下文是这一刻的快照 —— 规则在前、现场在后,读起来才是"先约束后事实"。
|
|
734
|
+
let note = '';
|
|
735
|
+
if (typeof promptSection === 'string' && promptSection.trim()) {
|
|
736
|
+
note += `\n\n# ${en ? 'Standing instructions' : '用户固定提示词'}\n${promptSection.trim()}`;
|
|
737
|
+
}
|
|
738
|
+
if (parts.length > 0) {
|
|
739
|
+
note += `\n\n# ${en ? 'Current context' : '当前上下文'}\n${parts.join('\n\n')}`;
|
|
740
|
+
}
|
|
741
|
+
if (hasExtensionSection) {
|
|
742
|
+
note += `\n\n${extensionSection.trim()}`;
|
|
743
|
+
}
|
|
650
744
|
const sys = messages.find(m => m && m.role === 'system' && typeof m.content === 'string');
|
|
651
745
|
if (sys) sys.content += note;
|
|
652
746
|
else messages.unshift({ role: 'system', content: note.trim() });
|