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,114 @@
|
|
|
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
|
+
// 「和 g ai 对话时固定注入的两块提示词」的拼装与取数口径。
|
|
16
|
+
//
|
|
17
|
+
// 这个能力是**纯 prompt 注入**的:没有按钮、没有状态、界面上看不出对错。
|
|
18
|
+
// 改 agentChat.js / runExternalTurn.js 的人(哪怕只是把一段挪个位置)会无声地把它弄掉,
|
|
19
|
+
// 而用户只会觉得"模型今天不太听话"。所以口径在这一层逐条钉住,注入点那条链路由
|
|
20
|
+
// agentChatShared.test.js 跑真实 runAgentTurn 验(见那里的"用户固定提示词"用例)。
|
|
21
|
+
//
|
|
22
|
+
// 注意:必须在 import 之前把 USERPROFILE/HOME 指到沙箱 —— chatPrompts.js 会连带
|
|
23
|
+
// 加载 configManager 与 orchestratorStore,它们按 home 定位 ~/.zen-gitsync。
|
|
24
|
+
// 本文件里的用例全部走注入口(不读真实配置),沙箱只是防"哪天有人加了真实读取"。
|
|
25
|
+
import { test } from 'node:test'
|
|
26
|
+
import assert from 'node:assert/strict'
|
|
27
|
+
import { mkdtempSync } from 'node:fs'
|
|
28
|
+
import os from 'node:os'
|
|
29
|
+
import path from 'node:path'
|
|
30
|
+
|
|
31
|
+
const SANDBOX = mkdtempSync(path.join(os.tmpdir(), 'zen-chat-prompts-'))
|
|
32
|
+
process.env.USERPROFILE = SANDBOX
|
|
33
|
+
process.env.HOME = SANDBOX
|
|
34
|
+
delete process.env.HOMEDRIVE
|
|
35
|
+
delete process.env.HOMEPATH
|
|
36
|
+
|
|
37
|
+
const { buildChatPromptSection, resolveChatPromptSection } = await import('./chatPrompts.js')
|
|
38
|
+
|
|
39
|
+
// ── 拼装(纯函数)─────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
test('两块都没配 → 空串(调用方据此完全跳过注入,连一节标题都不出现)', () => {
|
|
42
|
+
assert.equal(buildChatPromptSection({}), '')
|
|
43
|
+
assert.equal(buildChatPromptSection({ gaiPrompt: '', dispatchPrompt: '' }), '')
|
|
44
|
+
// 只有空白也算没配 —— 用户清空后留下一堆换行,不该在提示词里留一节空标题
|
|
45
|
+
assert.equal(buildChatPromptSection({ gaiPrompt: ' \n\n ', dispatchPrompt: '\t' }), '')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('两块都在 → 各带自己的小标题,g ai 那段在前', () => {
|
|
49
|
+
const out = buildChatPromptSection({ gaiPrompt: '回答用中文', dispatchPrompt: '改完就提交' })
|
|
50
|
+
assert.match(out, /### g ai 默认提示词\n回答用中文/)
|
|
51
|
+
assert.match(out, /### 派发任务的提示词(与工作台派发任务时附加的是同一份)\n改完就提交/)
|
|
52
|
+
assert.ok(out.indexOf('回答用中文') < out.indexOf('改完就提交'), 'g ai 那段必须在前')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('只配一块 → 只出那一块(另一块不留空标题)', () => {
|
|
56
|
+
const onlyGai = buildChatPromptSection({ gaiPrompt: '回答用中文' })
|
|
57
|
+
assert.match(onlyGai, /### g ai 默认提示词/)
|
|
58
|
+
assert.doesNotMatch(onlyGai, /### 派发任务的提示词/)
|
|
59
|
+
|
|
60
|
+
const onlyDispatch = buildChatPromptSection({ dispatchPrompt: '改完就提交' })
|
|
61
|
+
assert.match(onlyDispatch, /### 派发任务的提示词/)
|
|
62
|
+
assert.doesNotMatch(onlyDispatch, /### g ai 默认提示词/)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('正文前后空白被裁掉,中间原样保留(用户排版不被破坏)', () => {
|
|
66
|
+
const out = buildChatPromptSection({ gaiPrompt: '\n\n第一段\n\n第二段\n\n' })
|
|
67
|
+
assert.match(out, /### g ai 默认提示词\n第一段\n\n第二段$/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('英文界面走英文标题(与系统提示词同一套 locale 判定)', () => {
|
|
71
|
+
const out = buildChatPromptSection({ gaiPrompt: 'answer in Chinese', locale: 'en-US' })
|
|
72
|
+
assert.match(out, /### g ai default prompt/)
|
|
73
|
+
assert.match(out, /Standing instructions|Settings/)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('非字符串入参不炸(手改配置塞了数字 / 对象)', () => {
|
|
77
|
+
assert.equal(buildChatPromptSection({ gaiPrompt: 42, dispatchPrompt: {} }), '')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// ── 取数 ────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
test('两块分别来自两处:g ai 默认提示词取 config,派发提示词取 orchestrator', async () => {
|
|
83
|
+
const seenCwd = []
|
|
84
|
+
const out = await resolveChatPromptSection({
|
|
85
|
+
cwd: 'D:\\ws\\alpha',
|
|
86
|
+
loadAgentPrompt: async () => '回答用中文',
|
|
87
|
+
loadDispatchPrompt: async (cwd) => { seenCwd.push(cwd); return '改完就提交' },
|
|
88
|
+
})
|
|
89
|
+
assert.match(out, /回答用中文/)
|
|
90
|
+
assert.match(out, /改完就提交/)
|
|
91
|
+
assert.deepEqual(seenCwd, ['D:\\ws\\alpha'], '派发提示词必须按本轮 cwd 解析(项目级那条靠它生效)')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('取数抛错一律当"没配",绝不把整轮对话打挂', async () => {
|
|
95
|
+
const both = await resolveChatPromptSection({
|
|
96
|
+
loadAgentPrompt: async () => { throw new Error('config.json 坏了') },
|
|
97
|
+
loadDispatchPrompt: async () => { throw new Error('orchestrator.json 坏了') },
|
|
98
|
+
})
|
|
99
|
+
assert.equal(both, '', '两边都挂 = 不注入')
|
|
100
|
+
|
|
101
|
+
// 一边挂另一边照常:坏的那份不该连累好的那份
|
|
102
|
+
const half = await resolveChatPromptSection({
|
|
103
|
+
loadAgentPrompt: async () => '回答用中文',
|
|
104
|
+
loadDispatchPrompt: async () => { throw new Error('orchestrator.json 坏了') },
|
|
105
|
+
})
|
|
106
|
+
assert.match(half, /回答用中文/)
|
|
107
|
+
assert.doesNotMatch(half, /### 派发任务的提示词/)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('返回值是字符串而不是 undefined(调用方直接拼进 system,undefined 会印出字面量)', async () => {
|
|
111
|
+
const out = await resolveChatPromptSection({ loadAgentPrompt: async () => '', loadDispatchPrompt: async () => '' })
|
|
112
|
+
assert.equal(typeof out, 'string')
|
|
113
|
+
assert.equal(out, '')
|
|
114
|
+
})
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
//
|
|
26
26
|
// 这里把「读三个配置文件」这件事收口成一个模块,让前端能在选择器里直接显示。
|
|
27
27
|
//
|
|
28
|
-
// ──
|
|
28
|
+
// ── 四家配置文件的形状(这是本模块存在的全部理由)────────────────────────
|
|
29
29
|
//
|
|
30
30
|
// claude ~/.claude/settings.json JSON。模型在 env 里,且有**别名两层**:
|
|
31
31
|
// ANTHROPIC_DEFAULT_SONNET_MODEL 是 CLI 认识的名字
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
// variant(截图里那个蓝色的 max)在同文件的顶层 variant 表里。
|
|
49
49
|
// 两处都没有才算"没指定",返回值里用 source 区分(config / state)。
|
|
50
50
|
//
|
|
51
|
+
// codebuddy ~/.codebuddy/settings.json JSON。顶层 `model` 字符串
|
|
52
|
+
// (--settings '{"model":"..."}' 的落盘处;没写过就是没配,
|
|
53
|
+
// CLI 用自己的内置默认 —— 那种情况下这里如实报 null)。
|
|
54
|
+
//
|
|
51
55
|
// ── 三条必须守住的底线 ──────────────────────────────────────────────────────
|
|
52
56
|
//
|
|
53
57
|
// 1. **纯只读**:只 fs.readFile,一个字节都不写。改用户 CLI 的配置是绝对禁区。
|
|
@@ -257,25 +261,42 @@ function detectOpencode(cfgJson, stateJson) {
|
|
|
257
261
|
return detectOpencodeConfig(cfgJson) || detectOpencodeState(stateJson);
|
|
258
262
|
}
|
|
259
263
|
|
|
264
|
+
/**
|
|
265
|
+
* codebuddy:~/.codebuddy/settings.json 的顶层 model。
|
|
266
|
+
*
|
|
267
|
+
* 与另三家一个重要差别:codebuddy **没有"第二处"**—— TUI 里选的模型会写回
|
|
268
|
+
* settings.json(`codebuddy config set -g model <id>`),不存在 opencode 那种
|
|
269
|
+
* "选择躺在 state 里、配置是空的"分裂。所以读一个文件就够。
|
|
270
|
+
* 没配 → null:CLI 有自己的内置默认(init 事件里能看到,如 fast-model),
|
|
271
|
+
* 但那是 CLI 的默认而不是用户的设置,按本模块的底线不猜、不报。
|
|
272
|
+
*/
|
|
273
|
+
function detectCodebuddy(json) {
|
|
274
|
+
const raw = json && json.model;
|
|
275
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
276
|
+
return { model: raw.trim(), display: null, provider: null, source: 'config' };
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
260
281
|
function escapeRe(s) {
|
|
261
282
|
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
262
283
|
}
|
|
263
284
|
|
|
264
285
|
/**
|
|
265
|
-
*
|
|
286
|
+
* 探测四个执行器当前配置的模型。
|
|
266
287
|
*
|
|
267
288
|
* @param {object} [opts]
|
|
268
289
|
* @param {string} [opts.homeDir] 测试沙箱用(默认 os.homedir())
|
|
269
|
-
* @returns {Promise<{ claude: object|null, codex: object|null, opencode: object|null }>}
|
|
290
|
+
* @returns {Promise<{ claude: object|null, codex: object|null, opencode: object|null, codebuddy: object|null }>}
|
|
270
291
|
* 每项 `{ model, display, provider, source }`;该执行器没配模型时为 null。
|
|
271
292
|
* source = 'config'(配置文件里写死的)| 'state'(CLI 自己记的"上次用的")。
|
|
272
293
|
*
|
|
273
|
-
*
|
|
294
|
+
* 四路并发、互不阻塞:任何一个配置文件读失败都只让**它自己**变成 null。
|
|
274
295
|
*/
|
|
275
296
|
export async function detectExecutorModels({ homeDir } = {}) {
|
|
276
297
|
const home = homeDir || os.homedir();
|
|
277
298
|
|
|
278
|
-
const [claudeRaw, codexRaw, opencodeJsonRaw, opencodeJsoncRaw, opencodeStateRaw] = await Promise.all([
|
|
299
|
+
const [claudeRaw, codexRaw, opencodeJsonRaw, opencodeJsoncRaw, opencodeStateRaw, codebuddyRaw] = await Promise.all([
|
|
279
300
|
readTextOrNull(path.join(home, '.claude', 'settings.json')),
|
|
280
301
|
readTextOrNull(path.join(home, '.codex', 'config.toml')),
|
|
281
302
|
// .json 优先(标准名),.jsonc 兜底。opencode 两个都认。
|
|
@@ -283,6 +304,8 @@ export async function detectExecutorModels({ homeDir } = {}) {
|
|
|
283
304
|
readTextOrNull(path.join(home, '.config', 'opencode', 'opencode.jsonc')),
|
|
284
305
|
// TUI 的"最近使用"(不写回配置文件,见 detectOpencodeState 的注释)
|
|
285
306
|
readTextOrNull(path.join(home, '.local', 'state', 'opencode', 'model.json')),
|
|
307
|
+
// CodeBuddy Code CLI(与 WorkBuddy 桌面版共用的配置目录)
|
|
308
|
+
readTextOrNull(path.join(home, '.codebuddy', 'settings.json')),
|
|
286
309
|
]);
|
|
287
310
|
|
|
288
311
|
const pickJson = (raw) => {
|
|
@@ -295,6 +318,7 @@ export async function detectExecutorModels({ homeDir } = {}) {
|
|
|
295
318
|
// 于是注释里写的"jsonc 兜底"是死代码 —— 只写了 .jsonc 的机器永远显示未配置。
|
|
296
319
|
const opencodeCfg = pickJson(opencodeJsonRaw) || pickJson(opencodeJsoncRaw);
|
|
297
320
|
const opencodeState = pickJson(opencodeStateRaw);
|
|
321
|
+
const codebuddyCfg = pickJson(codebuddyRaw);
|
|
298
322
|
|
|
299
323
|
return {
|
|
300
324
|
claude: claudeCfg && typeof claudeCfg.env === 'object' && claudeCfg.env
|
|
@@ -302,6 +326,7 @@ export async function detectExecutorModels({ homeDir } = {}) {
|
|
|
302
326
|
: null,
|
|
303
327
|
codex: codexRaw ? detectCodex(codexRaw) : null,
|
|
304
328
|
opencode: detectOpencode(opencodeCfg, opencodeState),
|
|
329
|
+
codebuddy: detectCodebuddy(codebuddyCfg),
|
|
305
330
|
};
|
|
306
331
|
}
|
|
307
332
|
|
|
@@ -121,12 +121,14 @@ const CODEX_TOML = [
|
|
|
121
121
|
'model = "inner-model"',
|
|
122
122
|
].join('\n')
|
|
123
123
|
const OPENCODE_JSONC = '{\n // 注释\n "model": "anthropic/claude-x",\n}'
|
|
124
|
+
const CODEBUDDY_JSON = JSON.stringify({ model: 'deepseek-v4-1-flash-260910' })
|
|
124
125
|
|
|
125
|
-
test('detectExecutorModels:
|
|
126
|
+
test('detectExecutorModels: 四家配置齐全时各自读出模型', async () => {
|
|
126
127
|
const home = await makeHome({
|
|
127
128
|
'.claude/settings.json': CLAUDE_JSON,
|
|
128
129
|
'.codex/config.toml': CODEX_TOML,
|
|
129
130
|
'.config/opencode/opencode.jsonc': OPENCODE_JSONC,
|
|
131
|
+
'.codebuddy/settings.json': CODEBUDDY_JSON,
|
|
130
132
|
})
|
|
131
133
|
const out = await detectExecutorModels({ homeDir: home })
|
|
132
134
|
|
|
@@ -141,6 +143,15 @@ test('detectExecutorModels: 三家配置齐全时各自读出模型', async () =
|
|
|
141
143
|
assert.deepEqual(out.opencode, {
|
|
142
144
|
model: 'anthropic/claude-x', display: null, provider: null, source: 'config',
|
|
143
145
|
})
|
|
146
|
+
// codebuddy:一个文件一处键,没有"第二来源"(TUI 选择会写回 settings.json)
|
|
147
|
+
assert.deepEqual(out.codebuddy, {
|
|
148
|
+
model: 'deepseek-v4-1-flash-260910', display: null, provider: null, source: 'config',
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('detectExecutorModels: codebuddy 没配 model 时为 null(CLI 内置默认不冒名)', async () => {
|
|
153
|
+
const home = await makeHome({ '.codebuddy/settings.json': '{"theme":"dark"}' })
|
|
154
|
+
assert.equal((await detectExecutorModels({ homeDir: home })).codebuddy, null)
|
|
144
155
|
})
|
|
145
156
|
|
|
146
157
|
test('detectExecutorModels: 只有 .jsonc 时 opencode 仍读得到 [回归]', async () => {
|
|
@@ -254,10 +265,10 @@ test('detectExecutorModels: claude 显式 ANTHROPIC_MODEL 优先于档位别名'
|
|
|
254
265
|
})
|
|
255
266
|
})
|
|
256
267
|
|
|
257
|
-
test('detectExecutorModels:
|
|
268
|
+
test('detectExecutorModels: 一个配置文件都没有时四项全 null 且不抛', async () => {
|
|
258
269
|
const home = await makeHome()
|
|
259
270
|
const out = await detectExecutorModels({ homeDir: home })
|
|
260
|
-
assert.deepEqual(out, { claude: null, codex: null, opencode: null })
|
|
271
|
+
assert.deepEqual(out, { claude: null, codex: null, opencode: null, codebuddy: null })
|
|
261
272
|
})
|
|
262
273
|
|
|
263
274
|
test('detectExecutorModels: 配置残缺/是目录/内容不是 JSON 时只让自己变 null', async () => {
|
|
@@ -265,9 +276,10 @@ test('detectExecutorModels: 配置残缺/是目录/内容不是 JSON 时只让
|
|
|
265
276
|
'.claude/settings.json': '{ 这不是 JSON', // 解析失败 → null
|
|
266
277
|
'.codex/config.toml': '# 只有注释,没有 model\n', // 无 model → null
|
|
267
278
|
'.config/opencode/opencode.json': '{"model":""}', // 空串 → null
|
|
279
|
+
'.codebuddy/settings.json': '{"model":123}', // 非字符串 → null
|
|
268
280
|
})
|
|
269
281
|
const out = await detectExecutorModels({ homeDir: home })
|
|
270
|
-
assert.deepEqual(out, { claude: null, codex: null, opencode: null })
|
|
282
|
+
assert.deepEqual(out, { claude: null, codex: null, opencode: null, codebuddy: null })
|
|
271
283
|
})
|
|
272
284
|
|
|
273
285
|
test('detectExecutorModels: 返回值里绝不出现任何密钥', async () => {
|
|
@@ -1660,7 +1660,7 @@ export function registerWorkbenchRoutes({
|
|
|
1660
1660
|
* 无参数、服务端不算缓存:三个文件都很小,每次实读比让前端踩脏缓存划算;
|
|
1661
1661
|
* 前端那边(stores/toolsStore)自己按分钟级 TTL + 并发去重压请求频率。
|
|
1662
1662
|
* 返回值里某项为 null = 那个 CLI 没在配置里写模型,前端照实显示「未在配置中指定」。
|
|
1663
|
-
* key 恒为 claude / codex / opencode
|
|
1663
|
+
* key 恒为 claude / codex / opencode / codebuddy 四个,与 TASK_EXECUTOR_OPTIONS 对齐。
|
|
1664
1664
|
*/
|
|
1665
1665
|
app.get('/api/workbench/executor-models', asyncRoute(async (_req, res) => {
|
|
1666
1666
|
const detected = await detectExecutorModels();
|
|
@@ -216,7 +216,7 @@ export function jobDurationMs(job) {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
|
-
* 本轮跑的是哪个执行器(`claude` | `opencode` | `codex`)。
|
|
219
|
+
* 本轮跑的是哪个执行器(`claude` | `opencode` | `codex` | `codebuddy`)。
|
|
220
220
|
*
|
|
221
221
|
* 只在**认出是已知执行器**时才回值:job.agent 来自 jobs.json(会被手工编辑、也读过
|
|
222
222
|
* 别实例写的旧记录),回一个前端不认识的字符串只会让卡片上的图标位空着或显示错品牌。
|
|
@@ -224,10 +224,10 @@ export function jobDurationMs(job) {
|
|
|
224
224
|
* 猜错的执行器比不显示更糟。
|
|
225
225
|
*
|
|
226
226
|
* 这里**刻意不 import taskRunner 的 TASK_EXECUTORS**:taskRunner 拉起进程、依赖一堆
|
|
227
|
-
* 运行时模块,而本模块被 projectRegistry(5s
|
|
227
|
+
* 运行时模块,而本模块被 projectRegistry(5s 轮询的看板)直接引用,没必要为了四个
|
|
228
228
|
* 字符串把整条执行链拖进来。新增执行器时这里和 taskRunner 一起改。
|
|
229
229
|
*/
|
|
230
|
-
const KNOWN_EXECUTORS = ['claude', 'opencode', 'codex'];
|
|
230
|
+
const KNOWN_EXECUTORS = ['claude', 'opencode', 'codex', 'codebuddy'];
|
|
231
231
|
|
|
232
232
|
export function jobAgent(job) {
|
|
233
233
|
const v = job && typeof job.agent === 'string' ? job.agent.trim().toLowerCase() : '';
|
|
@@ -181,6 +181,7 @@ test('jobAgent:认识的执行器原样返回', () => {
|
|
|
181
181
|
assert.equal(jobAgent({ agent: 'claude' }), 'claude');
|
|
182
182
|
assert.equal(jobAgent({ agent: 'opencode' }), 'opencode');
|
|
183
183
|
assert.equal(jobAgent({ agent: 'codex' }), 'codex');
|
|
184
|
+
assert.equal(jobAgent({ agent: 'codebuddy' }), 'codebuddy');
|
|
184
185
|
});
|
|
185
186
|
|
|
186
187
|
test('jobAgent:大小写与首尾空白归一后再认(jobs.json 会被手工编辑)', () => {
|
|
@@ -23,18 +23,22 @@
|
|
|
23
23
|
//
|
|
24
24
|
// ⚠️ 两条不重复实现的红线(本仓库 agentParity.test.js 就是为这类分叉立的规矩):
|
|
25
25
|
// 1. **spawn 与协议解析一律复用 taskRunner.js**,绝不在这里再写一份。
|
|
26
|
-
// `createClaudeEventHandler` / `createOpencodeEventHandler` /
|
|
26
|
+
// `createClaudeEventHandler` / `createOpencodeEventHandler` /
|
|
27
|
+
// `createCodexEventHandler` / `createCodebuddyEventHandler`
|
|
27
28
|
// 的签名是 `(job, appendOutput, appendThinking, toolCalls)` —— 回调早已抽象好,
|
|
28
29
|
// 这里只是换一组回调(推 SSE 而不是推工作台面板),所以适配层能很薄。
|
|
29
30
|
// 2. **权限档不在这里决定**。taskRunner 的 launcher 已经带死了各自的档位
|
|
30
31
|
// (claude: --permission-mode bypassPermissions --dangerously-skip-permissions;
|
|
31
|
-
// opencode: --auto;codex: --dangerously-bypass-approvals-and-sandbox
|
|
32
|
-
//
|
|
32
|
+
// opencode: --auto;codex: --dangerously-bypass-approvals-and-sandbox;
|
|
33
|
+
// codebuddy: -y),与工作台任务执行保持一致。要改档位请改 launcher,别在这儿加参数。
|
|
33
34
|
//
|
|
34
|
-
// 关于 headless 权限(2026-09-28
|
|
35
|
+
// 关于 headless 权限(2026-09-28 实测,三家在非交互下的真实行为;codebuddy 2026-10-12 补测):
|
|
35
36
|
// claude 默认档也能跑,但不放开会挡住写操作;工作台那套是 bypassPermissions。
|
|
36
37
|
// codex 默认档是只读沙箱(写文件被拦),所以要 --dangerously-bypass-...
|
|
37
38
|
// opencode **不给 --auto 会卡死**(在等一个永远不会有人回答的交互式批准)。
|
|
39
|
+
// codebuddy 不给 -y 时写操作被拦(init 事件回传 permissionMode 可验证);
|
|
40
|
+
// 另外它每次运行自带引擎服务器,默认端口 62443 会被 WorkBuddy
|
|
41
|
+
// 桌面版占着,所以 launcher 固定传 --port 0 让 OS 选空闲端口。
|
|
38
42
|
// 这正是 AgentView 里没有终端、没人能点"允许"所决定的 —— 必须预设档位。
|
|
39
43
|
|
|
40
44
|
import { spawn, execFile } from 'child_process';
|
|
@@ -42,27 +46,19 @@ import { logger } from './shared.js';
|
|
|
42
46
|
import { engineLabel } from './agentEngines.js';
|
|
43
47
|
import {
|
|
44
48
|
createToolCallTracker,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
createCodexEventHandler,
|
|
48
|
-
launchClaudeInNewWindow,
|
|
49
|
-
launchOpencodeRun,
|
|
50
|
-
launchCodexExec,
|
|
49
|
+
EXECUTOR_LAUNCHERS,
|
|
50
|
+
EXECUTOR_HANDLERS,
|
|
51
51
|
} from './taskRunner.js';
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
/**
|
|
54
|
+
* 引擎 → spawn 实现。直接复用 taskRunner 的运行时分发表(EXECUTOR_LAUNCHERS)——
|
|
55
|
+
* 不在这里另抄一张表:那张表已经是"执行器 → launcher"的唯一真相源,抄一份就等于
|
|
56
|
+
* 给未来某次"加了第五家只改了一处"埋雷。别名导出只为保持本模块对外的旧名字。
|
|
57
|
+
*/
|
|
58
|
+
export const ENGINE_LAUNCHERS = EXECUTOR_LAUNCHERS;
|
|
59
59
|
|
|
60
|
-
/** 引擎 → NDJSON
|
|
61
|
-
export const ENGINE_HANDLERS =
|
|
62
|
-
claude: createClaudeEventHandler,
|
|
63
|
-
opencode: createOpencodeEventHandler,
|
|
64
|
-
codex: createCodexEventHandler,
|
|
65
|
-
};
|
|
60
|
+
/** 引擎 → NDJSON 事件处理器工厂。同上,复用 taskRunner 的 EXECUTOR_HANDLERS。 */
|
|
61
|
+
export const ENGINE_HANDLERS = EXECUTOR_HANDLERS;
|
|
66
62
|
|
|
67
63
|
/** 输出片段最长保留多少字符(与前端 MAX_LOG_DISPLAY 的口径一致,防止会话文件膨胀) */
|
|
68
64
|
const MAX_ACCUM = 400_000;
|
|
@@ -90,7 +86,7 @@ function killChildTree(child) {
|
|
|
90
86
|
*
|
|
91
87
|
* @param {object} args
|
|
92
88
|
* @param {object} args.session 会话记录(会被就地更新:messages / engineSessionId)
|
|
93
|
-
* @param {string} args.engine 'claude' | 'opencode' | 'codex'
|
|
89
|
+
* @param {string} args.engine 'claude' | 'opencode' | 'codex' | 'codebuddy'
|
|
94
90
|
* @param {string} args.userMessage 用户这一轮说的话
|
|
95
91
|
* @param {string} args.cwd 工作目录
|
|
96
92
|
* @param {string} [args.promptPrefix] 拼在用户消息**前面**的工作区状态块(见 agentRoutes)
|
|
@@ -275,8 +271,9 @@ export async function runExternalTurn({
|
|
|
275
271
|
});
|
|
276
272
|
}
|
|
277
273
|
|
|
278
|
-
//
|
|
279
|
-
//
|
|
274
|
+
// 续聊标识:各家字段名不同(claude session_id / opencode sessionID / codex thread_id /
|
|
275
|
+
// codebuddy session_id),但 taskRunner 的 handler 已经统一落到 job.claudeSessionId 上,
|
|
276
|
+
// 这里只做搬运。
|
|
280
277
|
if (job.claudeSessionId) session.engineSessionId = job.claudeSessionId;
|
|
281
278
|
|
|
282
279
|
send({ type: 'done', content: aborted ? `${text}\n\n[已停止]` : text });
|
|
@@ -127,6 +127,40 @@ test('codex:exec --json → 同一组事件(item.* 分派)', async () => {
|
|
|
127
127
|
assert.equal(session.engineSessionId, 'th-1');
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
test('codebuddy:stream-json → 同一组事件(协议与 claude 对齐,特有事件被忽略)', async () => {
|
|
131
|
+
// 样例取自 2026-10-12 实测(CLI 2.164.0):init 带 session_id / apiKeySource,
|
|
132
|
+
// 工具调用是"tool_use(running) → user(tool_result)"两条,另有 system/status、
|
|
133
|
+
// ai-title 两个 cbc 特有事件必须被无视。
|
|
134
|
+
const lines = [
|
|
135
|
+
JSON.stringify({ type: 'system', subtype: 'init', session_id: 'cbc-sess-1', apiKeySource: 'www.workbuddy.cn', model: 'fast-model', permissionMode: 'bypassPermissions' }),
|
|
136
|
+
JSON.stringify({ type: 'system', subtype: 'status', status: 'compacting', session_id: 'cbc-sess-1' }),
|
|
137
|
+
JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '读一下' }] } }),
|
|
138
|
+
JSON.stringify({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'tu-1', name: 'Read', input: { file_path: 'a.txt' } }] } }),
|
|
139
|
+
JSON.stringify({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'tu-1', content: [{ type: 'text', text: 'hello' }] }] } }),
|
|
140
|
+
JSON.stringify({ type: 'ai-title', title: '读文件' }),
|
|
141
|
+
JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '完了' }] } }),
|
|
142
|
+
JSON.stringify({ type: 'result', subtype: 'success', is_error: false, session_id: 'cbc-sess-1' }),
|
|
143
|
+
];
|
|
144
|
+
const session = baseSession();
|
|
145
|
+
const c = collector();
|
|
146
|
+
await runExternalTurn({
|
|
147
|
+
session, engine: 'codebuddy', userMessage: '读 a.txt', cwd: 'D:/ws',
|
|
148
|
+
send: c.send,
|
|
149
|
+
launchers: { codebuddy: async () => ({ pid: 4242, child: fakeChild(lines) }) },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
assert.equal(c.of('content').map(e => e.delta).join(''), '读一下完了');
|
|
153
|
+
const starts = c.of('tool_call_start');
|
|
154
|
+
assert.equal(starts.length, 1);
|
|
155
|
+
assert.equal(starts[0].toolCallId, 'tu-1');
|
|
156
|
+
assert.equal(starts[0].name, 'Read');
|
|
157
|
+
assert.match(String(c.of('tool_result')[0].result), /hello/);
|
|
158
|
+
assert.equal(c.of('error').length, 0);
|
|
159
|
+
assert.equal(c.of('done').length, 1);
|
|
160
|
+
// 续聊标识搬运:cbc 的 session_id → session.engineSessionId(--resume 用)
|
|
161
|
+
assert.equal(session.engineSessionId, 'cbc-sess-1');
|
|
162
|
+
});
|
|
163
|
+
|
|
130
164
|
test('非 JSON 行(CLI 的告警)原样贴出来,不当成事件丢掉', async () => {
|
|
131
165
|
const session = baseSession();
|
|
132
166
|
const c = collector();
|