zen-gitsync 2.18.20 → 2.18.22

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 (88) hide show
  1. package/package.json +4 -1
  2. package/src/cli/ai/agent.js +9 -2
  3. package/src/cli/ai/extensions.js +1 -1
  4. package/src/cli/ai/runtime.test.js +57 -1
  5. package/src/cli/ai/termui.js +11 -2
  6. package/src/cli/ai/tools.js +266 -5
  7. package/src/cli/ai/tools.test.js +81 -0
  8. package/src/cli/ai/transport.js +1 -1
  9. package/src/cli/ai/turn.js +59 -3
  10. package/src/config.js +60 -4
  11. package/src/mcp/install.js +548 -529
  12. package/src/ui/public/assets/AgentView-DdiyBMDD.js +26 -0
  13. package/src/ui/public/assets/{AgentView-BE7GD8rc.css → AgentView-_DUYxFmp.css} +1 -1
  14. package/src/ui/public/assets/{AppVersionBadge-86tbKU20.js → AppVersionBadge-CSEM45L4.js} +2 -2
  15. package/src/ui/public/assets/{BranchSelector-CsrkmTCA.js → BranchSelector-cSx2ZxYM.js} +1 -1
  16. package/src/ui/public/assets/ChatSilenceBadge-Dtv23ZmV.js +16 -0
  17. package/src/ui/public/assets/{CommitForm-DBK0GXEh.js → CommitForm-CrAsbDkq.js} +2 -2
  18. package/src/ui/public/assets/{CommonDialog-BkuDIWro.js → CommonDialog-BtO3uKdw.js} +1 -1
  19. package/src/ui/public/assets/{EditorView-CnWsMdHg.css → EditorView-B-t-nuf0.css} +1 -1
  20. package/src/ui/public/assets/EditorView-BesiWTV2.js +10 -0
  21. package/src/ui/public/assets/{FlowExecutionViewer-ClUkAmA2.js → FlowExecutionViewer-CfU9DLFG.js} +1 -1
  22. package/src/ui/public/assets/{FlowOrchestrationWorkspace-B3sOK3fz.js → FlowOrchestrationWorkspace-D3CSRMvG.js} +1 -1
  23. package/src/ui/public/assets/{LogList-6aO0-2xh.js → LogList-CT-SRIS3.js} +4 -4
  24. package/src/ui/public/assets/MindmapView-BCGq2zdh.js +35 -0
  25. package/src/ui/public/assets/MindmapView-CoUbyadF.css +1 -0
  26. package/src/ui/public/assets/{MonitorView-fcNrgVaU.js → MonitorView-BuBJJVhR.js} +1 -1
  27. package/src/ui/public/assets/{ProjectManagerView-CZfrML6A.js → ProjectManagerView-DlSfRn9N.js} +2 -2
  28. package/src/ui/public/assets/{ProjectStartupButton-D5MREz6F.js → ProjectStartupButton-DKZttDb3.js} +1 -1
  29. package/src/ui/public/assets/RemoteManagerDialog-CVrhhUPg.js +4 -0
  30. package/src/ui/public/assets/{RemoteRepoCard-CTnmfv2y.js → RemoteRepoCard-CmdYDF24.js} +1 -1
  31. package/src/ui/public/assets/{SourceMapView-B4q5qiE8.js → SourceMapView-DClgTmda.js} +1 -1
  32. package/src/ui/public/assets/{SvgIcon-WpEiC1rI.js → SvgIcon-D5fhBiJr.js} +1 -1
  33. package/src/ui/public/assets/TaskExecutorPicker-CzvWdS_K.js +1 -0
  34. package/src/ui/public/assets/TaskExecutorPicker-Dn8SgrTZ.css +1 -0
  35. package/src/ui/public/assets/{UserInputNode-Deoc3UvQ.js → UserInputNode-CNuW_Dtu.js} +1 -1
  36. package/src/ui/public/assets/WorkbenchView-D61DxNnn.css +1 -0
  37. package/src/ui/public/assets/WorkbenchView-WKNPn-oz.js +341 -0
  38. package/src/ui/public/assets/_plugin-vue_export-helper-E2AaPXn7.js +19 -0
  39. package/src/ui/public/assets/configStore-JIX2MOKo.js +1 -0
  40. package/src/ui/public/assets/{dagre-CpKxpMNH.js → dagre-yiQDclZv.js} +3 -3
  41. package/src/ui/public/assets/{element-plus-Bz6lYur-.js → element-plus-BeZBQw4X.js} +1 -1
  42. package/src/ui/public/assets/{flow-mindmap-cTg78Krq.js → flow-mindmap-CGjPqCv0.js} +1 -1
  43. package/src/ui/public/assets/{index-cDiVh_ZL.js → index-Cjv2Z6nF.js} +134 -125
  44. package/src/ui/public/assets/index-IjYv2goK.css +1 -0
  45. package/src/ui/public/assets/{monaco-CvMddat7.js → monaco-CMQow_j-.js} +1 -1
  46. package/src/ui/public/assets/{office-docx-VOB-i4K2.js → office-docx-nuGPI15f.js} +1 -1
  47. package/src/ui/public/assets/{office-excel-CJ2JvfA8.js → office-excel-CK-HOYi8.js} +1 -1
  48. package/src/ui/public/assets/{office-pptx-Z88k9qnx.js → office-pptx-9onMeLux.js} +1 -1
  49. package/src/ui/public/assets/{relativeTime-CoXAs57j.js → relativeTime-DHhE9Ihn.js} +1 -1
  50. package/src/ui/public/assets/taskExecutionExport-DlBMELzX.js +15 -0
  51. package/src/ui/public/assets/toolsStore-h5iqPyST.js +1 -0
  52. package/src/ui/public/assets/{useWorkbenchProjects-BS59r5Rj.js → useWorkbenchProjects-DW3vBHpK.js} +1 -1
  53. package/src/ui/public/assets/{vendor-CSFGMcMB.css → vendor-CAhoE06w.css} +1 -1
  54. package/src/ui/public/assets/{vendor-C2k0GBzj.js → vendor-KxSgeiUX.js} +303 -303
  55. package/src/ui/public/assets/{vue-flow-Cnxq2qrs.js → vue-flow-BMMiguH1.js} +1 -1
  56. package/src/ui/public/index.html +14 -14
  57. package/src/ui/server/routes/config.js +13 -0
  58. package/src/ui/server/routes/fileOpen.js +1851 -1736
  59. package/src/ui/server/routes/fileOpen.test.js +39 -1
  60. package/src/ui/server/routes/workbench/agentChat.js +48 -8
  61. package/src/ui/server/routes/workbench/agentChatShared.test.js +584 -496
  62. package/src/ui/server/routes/workbench/agentEngines.js +7 -4
  63. package/src/ui/server/routes/workbench/executorModels.js +25 -4
  64. package/src/ui/server/routes/workbench/executorModels.test.js +557 -530
  65. package/src/ui/server/routes/workbench/jobActivity.js +2 -2
  66. package/src/ui/server/routes/workbench/taskRunner.codearts.test.js +252 -0
  67. package/src/ui/server/routes/workbench/taskRunner.codebuddy.test.js +317 -317
  68. package/src/ui/server/routes/workbench/taskRunner.js +216 -7
  69. package/src/ui/server/routes/workbench/taskRunner.opencode.test.js +130 -130
  70. package/src/ui/server/routes/workbench/taskRunner.pi.test.js +298 -298
  71. package/src/ui/public/assets/AgentView-Du9akUeZ.js +0 -12
  72. package/src/ui/public/assets/ChatSilenceBadge-BBoFeSqe.js +0 -21
  73. package/src/ui/public/assets/EditorAgentPanel-BMUphuG0.css +0 -1
  74. package/src/ui/public/assets/EditorAgentPanel-KiHbQPJP.js +0 -34
  75. package/src/ui/public/assets/EditorView-B18B4yNJ.js +0 -13
  76. package/src/ui/public/assets/MindmapView-BDI3D_1h.css +0 -1
  77. package/src/ui/public/assets/MindmapView-aJ9TTijE.js +0 -4
  78. package/src/ui/public/assets/RecentDirectoriesChat-DBGoUlkn.css +0 -1
  79. package/src/ui/public/assets/RecentDirectoriesChat-wfkSF6NU.js +0 -3
  80. package/src/ui/public/assets/RemoteManagerDialog-BBwNlK9J.js +0 -4
  81. package/src/ui/public/assets/WorkbenchView-CVqXb__l.css +0 -1
  82. package/src/ui/public/assets/WorkbenchView-CijzpDWN.js +0 -360
  83. package/src/ui/public/assets/_plugin-vue_export-helper-BhdsvhKo.js +0 -19
  84. package/src/ui/public/assets/agentConversations-DaAtF1v4.js +0 -10
  85. package/src/ui/public/assets/configStore-Drlw8KtR.js +0 -1
  86. package/src/ui/public/assets/index-DSQ6mp_N.css +0 -1
  87. package/src/ui/public/assets/toolsStore-CnX34C96.js +0 -1
  88. package/src/ui/public/assets/useOrchestrator-BXmxJsRl.js +0 -1
@@ -15,7 +15,7 @@
15
15
  // 任务执行引擎:spawn 本地 agent CLI 跑子任务,流式收集输出,更新 job 状态。
16
16
  // 拆分自原 routes/workbench.js 1108-1564 行。
17
17
  //
18
- // 支持五种执行器(executor):
18
+ // 支持六种执行器(executor):
19
19
  // - claude —— claude -p --output-format stream-json(默认,历史行为)
20
20
  // - opencode —— opencode run --format json --auto(sst/opencode CLI,跟随其自身默认模型)
21
21
  // - codex —— codex exec --json(OpenAI Codex CLI,跟随其自身配置;2026-09-27 加入)
@@ -25,12 +25,16 @@
25
25
  // - pi —— pi --mode json -a [--session <id>]
26
26
  // (earendil-works 开源终端编程 Agent,npm @earendil-works/pi-coding-agent;
27
27
  // 2026-10-13 加入,**自有 JSONL 协议**,见该段注释)
28
+ // - codearts —— codearts run --format json --auto [-s <ses_id>]
29
+ // (华为云码道 CLI,2026-10-25 加入;它就是 opencode 的 fork,
30
+ // 事件流与 opencode 逐字同构,handler 直接转调,见该段注释)
28
31
  //
29
32
  // 核心 API:
30
33
  // - launchClaudeInNewWindow(cwd, prompt, resumeSessionId) spawn claude,返回 {pid, child}
31
34
  // - launchOpencodeRun(cwd, prompt, resumeSessionId) spawn opencode,返回 {pid, child}
32
35
  // - launchCodexExec(cwd, prompt, resumeSessionId) spawn codex,返回 {pid, child}
33
36
  // - launchPiPrint(cwd, prompt, resumeSessionId) spawn pi,返回 {pid, child}
37
+ // - launchCodeArtsRun(cwd, prompt, resumeSessionId) spawn codearts,返回 {pid, child}
34
38
  // - normalizeTaskExecutor(value) 非法值回落 'claude'
35
39
  // - runSingleSubtask(task, sub, repoPath, branch, opts) 跑一次任务(sub 是运行载体,见下)
36
40
  // - waitProcessExit(pid) polling 等进程退出
@@ -40,6 +44,7 @@
40
44
  // 续聊时同一任务会造出新的载体(`{taskId}__simple__r1` 等),job.subId 记的就是它。
41
45
 
42
46
  import fs from 'fs';
47
+ import os from 'os';
43
48
  import path from 'path';
44
49
  import { spawn, execFileSync } from 'child_process';
45
50
  import {
@@ -224,16 +229,17 @@ function attachPromptStdin(child, promptText, resolve, reject) {
224
229
 
225
230
  // ── 执行器(executor) ──────────────────────────────────────────────────
226
231
  // 工作台任务默认由 claude CLI 执行;opencode(2026-09-20)、codex(2026-09-27)、
227
- // codebuddy(2026-10-12)、pi(2026-10-13)作为可选执行器。五者都用「stdin 喂 prompt +
228
- // stdout 结构化 JSON 事件流」的同构形态,差异只在协议:claude / codebuddy 是 stream-json,
229
- // opencode 是 --format json 的事件 NDJSON,codex 是 exec --json 的 thread/item 事件 NDJSON,
230
- // pi 是 --mode json 的自有 JSONL(session/turn/message/tool_execution,与另四家都不同)。
232
+ // codebuddy(2026-10-12)、pi(2026-10-13)、codearts(2026-10-25)作为可选执行器。
233
+ // 六者都用「stdin 喂 prompt + stdout 结构化 JSON 事件流」的同构形态,差异只在协议:
234
+ // claude / codebuddy 是 stream-json,opencode 是 --format json 的事件 NDJSON,
235
+ // codex 是 exec --json 的 thread/item 事件 NDJSON,pi 是 --mode json 的自有 JSONL,
236
+ // codearts 的 --format json 与 opencode 逐字同构(它是 opencode 的 fork)。
231
237
  //
232
238
  // ⚠️ 这份清单与 src/config.js 的 TASK_EXECUTORS 是**同一份口径的两处书写**——
233
239
  // 历史上为此吵过(见 agentEngines.js 文件头):config.js 那份是"配置/归一化"的
234
240
  // 真相源,本文件这份服务"运行时分发"。两边不一致的失效方式是 normalize 放行、
235
- // 这里查无此人 → 静默回落 claude。加第五家时两处都要改,taskRunner.*.test.js 有钉子。
236
- export const TASK_EXECUTORS = ['claude', 'opencode', 'codex', 'codebuddy', 'pi'];
241
+ // 这里查无此人 → 静默回落 claude。加第六家时两处都要改,taskRunner.*.test.js 有钉子。
242
+ export const TASK_EXECUTORS = ['claude', 'opencode', 'codex', 'codebuddy', 'pi', 'codearts'];
237
243
 
238
244
  /** 执行器 → launcher。runSingleSubtask 与 runExternalTurn 共用这一张表。 */
239
245
  export const EXECUTOR_LAUNCHERS = {
@@ -242,6 +248,7 @@ export const EXECUTOR_LAUNCHERS = {
242
248
  codex: launchCodexExec,
243
249
  codebuddy: launchCodebuddyPrint,
244
250
  pi: launchPiPrint,
251
+ codearts: launchCodeArtsRun,
245
252
  };
246
253
 
247
254
  /** 执行器 → NDJSON 事件处理器工厂。 */
@@ -251,6 +258,7 @@ export const EXECUTOR_HANDLERS = {
251
258
  codex: createCodexEventHandler,
252
259
  codebuddy: createCodebuddyEventHandler,
253
260
  pi: createPiEventHandler,
261
+ codearts: createCodeArtsEventHandler,
254
262
  };
255
263
 
256
264
  /** 非法/缺省的 executor 一律回落 'claude'(历史行为的兼容兜底)。
@@ -1305,6 +1313,207 @@ export function createOpencodeEventHandler(job, appendOutput, appendThinking, to
1305
1313
  };
1306
1314
  }
1307
1315
 
1316
+ // ── codearts 执行器(2026-10-25 加入) ────────────────────────────────────
1317
+ // 华为云码道代码智能体 CLI(CodeArts Agent CLI,华为云产品)。与另五家同构:
1318
+ // codearts run --format json --auto [-s <ses_id>]
1319
+ // - prompt 从 stdin 喂(无位置参数时读 stdin,与 opencode 同效)
1320
+ // - --auto 以 always_allow 模式跑 bash。**必须带**:默认档是沙箱
1321
+ // (安装器往 ~/.codeartsdoer/cli-data/storage/permission/config.json 写
1322
+ // "bash_mode": "sandbox"),沙箱里 git 读不到真实仓库状态(实测 git log /
1323
+ // diff / status 拿不到),而派发链要求 agent 自己 commit。
1324
+ // - -s <ses_id> 原地续接(sessionID 不变,实测记得上文)
1325
+ // - 不传 -m:模型跟随它自己的配置(与另五家同一口径)
1326
+ //
1327
+ // **它就是 opencode 的 fork**(2026-10-25 实测,四处实锤):配置首行写着
1328
+ // "$schema": "https://opencode.ai/config.json";`auth login --help` 的位置参数
1329
+ // 说明原文是 "opencode auth provider";入口 codearts.cmd 里全是 OPENCODE_* 环境
1330
+ // 变量;--attach 的示例端口就是 opencode 的 4096。所以 --format json 的事件流
1331
+ // (step_start / text / reasoning / tool_use / step_finish / error、顶层 sessionID、
1332
+ // part.state.{status,input,output})与 opencode 逐字一致,handler 直接转调。
1333
+ //
1334
+ // 三个必须记住的坑:
1335
+ // 1. **鉴权只认 AK/SK 环境变量**(CODEARTS_CLI_AK / CODEARTS_CLI_SK),没有
1336
+ // 浏览器登录那一出(TUI 会弹,run 不会)。缺了它们 CLI 打一行中文错误后
1337
+ // **退出码仍是 0** —— 与 codebuddy 同款坑,判失败不能只看 exit code。
1338
+ // 2. **不经 codearts.cmd 就必须自己补齐它那串环境变量**(见 buildCodeArtsEnv):
1339
+ // 少了 OPENCODE_CONFIG / KERNEL_DATA_DIR,它会去读用户自己那套 opencode 的
1340
+ // 配置和数据目录(~/.config/opencode、~/.local/share/opencode 的 db)。
1341
+ // 3. **它会往项目根写 .codeartsdoer/**(memory/experience 的落地目录,--pure
1342
+ // 也拦不住),里面 mem-exp-config.json 不在它自己的 .gitignore 里 → 每个被
1343
+ // 派发的仓库都会多出一条未跟踪文件。launchCodeArtsRun 会顺手写
1344
+ // .git/info/exclude(只改本地仓库元数据,不动工作区、不入库)。
1345
+
1346
+ /**
1347
+ * 解析 codearts 可执行文件。
1348
+ *
1349
+ * 安装器(官网 install.ps1)把 shim 放在 ~/.codeartsdoer/installers、真身放在
1350
+ * 同目录 bin/codearts.exe;PATH 上能查到的也是 shim。Node 23+ 拒绝 spawn .cmd
1351
+ * (EINVAL),所以固定走 bin 下的 exe —— 环境变量由 buildCodeArtsEnv 补齐。
1352
+ */
1353
+ export function resolveCodeArtsExe() {
1354
+ const exe = path.join(os.homedir(), '.codeartsdoer', 'installers', 'bin', 'codearts.exe');
1355
+ if (fs.existsSync(exe)) return { exe, prefixArgs: [] };
1356
+ // 兜底:装到别处时按 PATH 找原生 exe(非 Windows 上就是这个)
1357
+ try {
1358
+ const lines = execFileSync('where', ['codearts'], { encoding: 'utf8', windowsHide: true })
1359
+ .split(/\r?\n/).map(s => s.trim()).filter(Boolean);
1360
+ const directExe = lines.find(s => /\.exe$/i.test(s));
1361
+ if (directExe && fs.existsSync(directExe)) return { exe: directExe, prefixArgs: [] };
1362
+ } catch { /* fallback */ }
1363
+ return { exe: process.platform === 'win32' ? 'codearts.cmd' : 'codearts', prefixArgs: [] };
1364
+ }
1365
+
1366
+ /**
1367
+ * codearts 子进程环境:复刻 ~/.codeartsdoer/installers/codearts.cmd 里那串 SET。
1368
+ *
1369
+ * 为什么逐个抄而不是直接 spawn .cmd:Node 23+ 对 .cmd 直接 EINVAL,走 shell 会让
1370
+ * stdin 管道与杀进程树都变复杂。这些变量是它"认自己是谁"的全部依据(少了就走
1371
+ * opencode 的目录),照抄比猜依赖关系稳。
1372
+ *
1373
+ * 刻意**不抄** NODE_TLS_REJECT_UNAUTHORIZED=0:shim 里关掉了全局证书校验,抄进
1374
+ * 来会污染本进程派生的所有 TLS 请求。实测不设它也能正常连华为云。
1375
+ * (导出仅为回归测试可见:taskRunner.codearts.test.js)
1376
+ */
1377
+ export function buildCodeArtsEnv() {
1378
+ const root = path.join(os.homedir(), '.codeartsdoer');
1379
+ const env = { ...process.env };
1380
+ env.SCENARIO = 'codeartsdoer';
1381
+ env.KERNEL_DATA_DIR = path.join(root, 'cli-data');
1382
+ env.KERNEL_STATE_DIR = path.join(root, 'state');
1383
+ env.KERNEL_CONFIG_DIR = root;
1384
+ env.OPENCODE_CHANNEL = 'latest';
1385
+ env.OPENCODE_CONFIG = path.join(root, 'codearts_cli.json');
1386
+ env.OPENCODE_CONFIG_FILE = 'codearts_cli.json,codearts_cli.jsonc';
1387
+ env.OPENCODE_MODE = 'tui';
1388
+ env.PLUGIN_ENV = 'hc';
1389
+ env.OPENCODE_DISABLE_MODELS_FETCH = '1';
1390
+ env.OPENCODE_DISABLE_AUTOUPDATE = 'true';
1391
+ env.OPENCODE_ALWAYS_NOTIFY_UPDATE = 'false';
1392
+ env.CODEARTS_DISABLE_AUTO_UPDATE = 'true';
1393
+ env.CODEAGENT_USE_MESSAGE_FEEDBACK = 'false';
1394
+ env.OMO_SEND_ANONYMOUS_TELEMETRY = '0';
1395
+ env.LANG = 'zh_CN.UTF-8';
1396
+ // 凭据兜底:用户常常是「装完/配完环境变量,应用还开着」——`setx` 只对新进程生效,
1397
+ // 于是 env 里读不到 AK/SK,任务会以"认证失败 + 退出码 0"静默失败(最难查的那种)。
1398
+ // 缺的时候回读一次用户级环境变量(HKCU\Environment),与 PATH 那个坑同一性质。
1399
+ for (const key of ['CODEARTS_CLI_AK', 'CODEARTS_CLI_SK']) {
1400
+ if (!env[key]) {
1401
+ const fromUserEnv = readWindowsUserEnv(key);
1402
+ if (fromUserEnv) env[key] = fromUserEnv;
1403
+ }
1404
+ }
1405
+ return env;
1406
+ }
1407
+
1408
+ /**
1409
+ * 读 Windows 用户级环境变量(HKCU\Environment),只在 process.env 里没有时用。
1410
+ * 非 win32 或读不到一律返回空串 —— 调用方按"没配"处理。
1411
+ */
1412
+ function readWindowsUserEnv(name) {
1413
+ if (process.platform !== 'win32' || !name) return '';
1414
+ try {
1415
+ const out = execFileSync('reg', ['query', 'HKCU\\Environment', '/v', name], {
1416
+ encoding: 'utf8',
1417
+ windowsHide: true,
1418
+ });
1419
+ const line = out.split(/\r?\n/).find(row => new RegExp(`\\s${name}\\s+REG_`).test(row));
1420
+ if (!line) return '';
1421
+ // 形如 ` CODEARTS_CLI_AK REG_SZ HPUA...`
1422
+ const parts = line.trim().split(/\s{2,}/);
1423
+ return parts.length >= 3 ? parts.slice(2).join(' ').trim() : '';
1424
+ } catch {
1425
+ return '';
1426
+ }
1427
+ }
1428
+
1429
+ /** codearts 的 spawn argv(纯函数,理由同另几家的 buildXxxArgs)。 */
1430
+ export function buildCodeArtsArgs(resumeSessionId) {
1431
+ const args = ['run', '--format', 'json', '--auto'];
1432
+ if (resumeSessionId) {
1433
+ args.push('-s', String(resumeSessionId));
1434
+ }
1435
+ return args;
1436
+ }
1437
+
1438
+ /**
1439
+ * 保证 .codeartsdoer/ 不进版本库。
1440
+ *
1441
+ * codearts 每次运行都会在项目根生成 .codeartsdoer/mem-exp-config.json(经验沉淀的
1442
+ * 落地目录),它自己的 .codeartsdoer/.gitignore 不覆盖这个文件,--pure 也拦不住。
1443
+ * 本仓的派发链让 agent 自己 git add,看到未跟踪文件很可能顺手带上 —— 于是往
1444
+ * .git/info/exclude 补一行(只影响本地,不产生任何工作区改动)。
1445
+ *
1446
+ * 失败一律吞掉:没有 .git(非仓库目录)或没有写权限时,让任务照常跑。
1447
+ * (导出仅为回归测试可见:taskRunner.codearts.test.js)
1448
+ */
1449
+ export function ensureCodeArtsGitExclude(cwd) {
1450
+ try {
1451
+ const gitDir = path.join(cwd, '.git');
1452
+ if (!fs.existsSync(gitDir) || !fs.statSync(gitDir).isDirectory()) return;
1453
+ const excludeFile = path.join(gitDir, 'info', 'exclude');
1454
+ const marker = '.codeartsdoer/';
1455
+ let current = '';
1456
+ try { current = fs.readFileSync(excludeFile, 'utf8'); } catch { /* 首次没有这个文件 */ }
1457
+ if (current.split(/\r?\n/).some(line => line.trim() === marker)) return;
1458
+ fs.mkdirSync(path.dirname(excludeFile), { recursive: true });
1459
+ const next = current && !current.endsWith('\n') ? `${current}\n${marker}\n` : `${current}${marker}\n`;
1460
+ fs.writeFileSync(excludeFile, next, 'utf8');
1461
+ } catch { /* 忽略:排除失败不影响任务本身 */ }
1462
+ }
1463
+
1464
+ /** spawn codearts 跑一个 prompt(非交互),返回 { pid, child }。 */
1465
+ export async function launchCodeArtsRun(cwd, promptText, resumeSessionId) {
1466
+ ensureCodeArtsGitExclude(cwd);
1467
+ const args = buildCodeArtsArgs(resumeSessionId);
1468
+ const { exe, prefixArgs } = resolveCodeArtsExe();
1469
+ const spawnArgs = [...prefixArgs, ...args];
1470
+ return new Promise((resolve, reject) => {
1471
+ let child;
1472
+ if (process.platform === 'win32') {
1473
+ child = spawn(exe, spawnArgs, {
1474
+ cwd,
1475
+ stdio: ['pipe', 'pipe', 'pipe'],
1476
+ windowsHide: false,
1477
+ env: buildCodeArtsEnv()
1478
+ });
1479
+ } else {
1480
+ child = spawn(exe, spawnArgs, {
1481
+ cwd,
1482
+ detached: true,
1483
+ stdio: ['pipe', 'pipe', 'pipe'],
1484
+ env: buildCodeArtsEnv()
1485
+ });
1486
+ }
1487
+ attachPromptStdin(child, promptText, resolve, reject);
1488
+ });
1489
+ }
1490
+
1491
+ /**
1492
+ * codearts --format json 事件处理器:**转调 opencode 那一份**。
1493
+ *
1494
+ * 2026-10-25 用本机 26.10.9 实跑核对过事件形状(文本、工具调用、step_finish 的
1495
+ * tokens、顶层 sessionID 逐字段一致),因此不再抄第二份实现 —— 本仓库为"同一份
1496
+ * 口径写两遍"吃过太多亏(分叉时不报错,只是悄悄少一半数据)。
1497
+ *
1498
+ * 仍留独立入口而不是在表里直接写 createOpencodeEventHandler:华为是 fork 持有方,
1499
+ * 协议随时可能被它改(它已经改过鉴权:从浏览器登录改成 AK/SK),那时在这里加分支,
1500
+ * 不会顺带改坏 opencode 那条链路。
1501
+ *
1502
+ * 另外记住:它的 stdout 会混进非 JSON 行(免费套餐限流时的"模型排队"提示,实测
1503
+ * 一次问答刷 48 行)—— 解析循环的 try/catch 跳过即可,不要当成协议错误。
1504
+ * (导出仅为回归测试可见:taskRunner.codearts.test.js)
1505
+ */
1506
+ export function createCodeArtsEventHandler(job, appendOutput, appendThinking, toolCalls) {
1507
+ const inner = createOpencodeEventHandler(job, appendOutput, appendThinking, toolCalls);
1508
+ // opencode 那份假定入参一定是解析好的对象(它的调用方只在 JSON.parse 成功后才喂)。
1509
+ // 码道这边 stdout 会混进非 JSON 行,解析层多一层保险不打紧 —— 顺手把
1510
+ // `null` / 标量挡在门外(`JSON.parse('null')` 也会成功返回 null)。
1511
+ return (evt) => {
1512
+ if (!evt || typeof evt !== 'object') return;
1513
+ inner(evt);
1514
+ };
1515
+ }
1516
+
1308
1517
  /**
1309
1518
  * codex `exec --json` 事件处理器。
1310
1519
  *
@@ -1,130 +1,130 @@
1
- // Copyright 2026 xz333221
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
- //
15
-
16
- // opencode 执行器事件处理器的回归测试(2026-09-20)。
17
- //
18
- // 钉住 opencode `run --format json` 适配层的四条口径:
19
- // 1. text / reasoning 事件的 part.text 分别落到 output / thinking
20
- // 2. 顶层 sessionID 覆盖 job.claudeSessionId(续接 --session 的取值来源)
21
- // 3. 同一 part.id 重复出现时按"前缀延长"只追加增量,不把同一段文字拼两遍
22
- // 4. error 事件写入 job.agentError(进程退出码可能是 0,终态判定靠它)
23
- //
24
- // 隔离策略同 jobStore.test.js:import taskRunner.js 之前把 USERPROFILE/HOME
25
- // 指向 mkdtemp 沙箱(taskRunner → jobStore import 时就会 hydrate)。
26
-
27
- import { test } from 'node:test';
28
- import assert from 'node:assert/strict';
29
- import path from 'node:path';
30
- import os from 'node:os';
31
- import { mkdtempSync } from 'node:fs';
32
- import { fileURLToPath, pathToFileURL } from 'node:url';
33
-
34
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
35
- const projectRoot = path.resolve(__dirname, '../../../../..');
36
-
37
- // ---- 环境隔离:必须在 import taskRunner.js 之前完成 ----
38
- const sandbox = mkdtempSync(path.join(os.tmpdir(), 'zen-taskrunner-test-'));
39
- process.env.USERPROFILE = sandbox;
40
- process.env.HOME = sandbox;
41
- delete process.env.HOMEDRIVE;
42
- delete process.env.HOMEPATH;
43
-
44
- const { createOpencodeEventHandler, normalizeTaskExecutor, TASK_EXECUTORS } = await import(
45
- pathToFileURL(path.join(projectRoot, 'src/ui/server/routes/workbench/taskRunner.js')).href
46
- );
47
-
48
- /** 可断言的 appendOutput/appendThinking 收集器 */
49
- function makeSink() {
50
- const sink = { output: [], thinking: [] };
51
- sink.appendOutput = (t) => { if (t) sink.output.push(t); };
52
- sink.appendThinking = (t) => { if (t) sink.thinking.push(t); };
53
- return sink;
54
- }
55
-
56
- test('normalizeTaskExecutor:白名单 + 大小写 + 非法回落', () => {
57
- assert.deepEqual(TASK_EXECUTORS, ['claude', 'opencode', 'codex', 'codebuddy', 'pi']);
58
- assert.equal(normalizeTaskExecutor('opencode'), 'opencode');
59
- assert.equal(normalizeTaskExecutor('codex'), 'codex');
60
- assert.equal(normalizeTaskExecutor('codebuddy'), 'codebuddy');
61
- assert.equal(normalizeTaskExecutor('pi'), 'pi');
62
- // 大小写不敏感(存储前统一小写)
63
- assert.equal(normalizeTaskExecutor('OpenCode'), 'opencode');
64
- assert.equal(normalizeTaskExecutor('opencode '), 'opencode');
65
- assert.equal(normalizeTaskExecutor('Codex '), 'codex');
66
- assert.equal(normalizeTaskExecutor('gpt'), 'claude');
67
- assert.equal(normalizeTaskExecutor(undefined), 'claude');
68
- assert.equal(normalizeTaskExecutor(null), 'claude');
69
- });
70
-
71
- test('opencode 事件:text/reasoning 分别落 output/thinking,sessionID 被捕获', () => {
72
- const job = {};
73
- const sink = makeSink();
74
- const handle = createOpencodeEventHandler(job, sink.appendOutput, sink.appendThinking);
75
-
76
- handle({ type: 'step_start', sessionID: 'ses_a', part: { type: 'step-start' } });
77
- handle({ type: 'reasoning', sessionID: 'ses_a', part: { id: 'p1', type: 'reasoning', text: '想一下' } });
78
- handle({ type: 'text', sessionID: 'ses_a', part: { id: 'p2', type: 'text', text: '好的' } });
79
- handle({ type: 'step_finish', sessionID: 'ses_a', part: { type: 'step-finish', tokens: {} } });
80
-
81
- assert.deepEqual(sink.output, ['好的']);
82
- assert.deepEqual(sink.thinking, ['想一下']);
83
- assert.equal(job.claudeSessionId, 'ses_a');
84
- assert.equal(job.agentError, undefined);
85
- });
86
-
87
- test('opencode 事件:同一 part 累积推送只追加增量', () => {
88
- const job = {};
89
- const sink = makeSink();
90
- const handle = createOpencodeEventHandler(job, sink.appendOutput, sink.appendThinking);
91
-
92
- // 累积语义:第二次事件的 text 是第一次的前缀延长
93
- handle({ type: 'text', sessionID: 'ses_b', part: { id: 'p1', text: 'Hello' } });
94
- handle({ type: 'text', sessionID: 'ses_b', part: { id: 'p1', text: 'Hello, world' } });
95
- // 重复事件(完全相同):不产生任何输出
96
- handle({ type: 'text', sessionID: 'ses_b', part: { id: 'p1', text: 'Hello, world' } });
97
-
98
- assert.deepEqual(sink.output, ['Hello', ', world']);
99
-
100
- // 纯增量语义(text 不是前次的前缀延长):整段追加
101
- handle({ type: 'text', sessionID: 'ses_b', part: { id: 'p1', text: '!' } });
102
- assert.deepEqual(sink.output, ['Hello', ', world', '!']);
103
- });
104
-
105
- test('opencode 事件:error 事件写入 agentError,只记第一条', () => {
106
- const job = {};
107
- const sink = makeSink();
108
- const handle = createOpencodeEventHandler(job, sink.appendOutput, sink.appendThinking);
109
-
110
- handle({ type: 'error', sessionID: 'ses_c', error: { name: 'UnknownError', data: { message: 'Unexpected server error' } } });
111
- handle({ type: 'error', sessionID: 'ses_c', error: { name: 'UnknownError', data: { message: 'second one' } } });
112
-
113
- assert.equal(job.agentError, 'Unexpected server error');
114
- // error 前若有正文,正文保留(前端能看到已产出的部分)
115
- assert.deepEqual(sink.output, []);
116
- });
117
-
118
- test('opencode 事件:error 结构缺 data.message 时逐级回落', () => {
119
- const job = {};
120
- const sink = makeSink();
121
- const handle = createOpencodeEventHandler(job, sink.appendOutput, sink.appendThinking);
122
-
123
- handle({ type: 'error', sessionID: 'ses_d', error: { name: 'ProviderError' } });
124
- assert.equal(job.agentError, 'ProviderError');
125
-
126
- const job2 = {};
127
- const handle2 = createOpencodeEventHandler(job2, sink.appendOutput, sink.appendThinking);
128
- handle2({ type: 'error', sessionID: 'ses_e', error: {} });
129
- assert.equal(job2.agentError, 'opencode 执行出错');
130
- });
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ // opencode 执行器事件处理器的回归测试(2026-09-20)。
17
+ //
18
+ // 钉住 opencode `run --format json` 适配层的四条口径:
19
+ // 1. text / reasoning 事件的 part.text 分别落到 output / thinking
20
+ // 2. 顶层 sessionID 覆盖 job.claudeSessionId(续接 --session 的取值来源)
21
+ // 3. 同一 part.id 重复出现时按"前缀延长"只追加增量,不把同一段文字拼两遍
22
+ // 4. error 事件写入 job.agentError(进程退出码可能是 0,终态判定靠它)
23
+ //
24
+ // 隔离策略同 jobStore.test.js:import taskRunner.js 之前把 USERPROFILE/HOME
25
+ // 指向 mkdtemp 沙箱(taskRunner → jobStore import 时就会 hydrate)。
26
+
27
+ import { test } from 'node:test';
28
+ import assert from 'node:assert/strict';
29
+ import path from 'node:path';
30
+ import os from 'node:os';
31
+ import { mkdtempSync } from 'node:fs';
32
+ import { fileURLToPath, pathToFileURL } from 'node:url';
33
+
34
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
35
+ const projectRoot = path.resolve(__dirname, '../../../../..');
36
+
37
+ // ---- 环境隔离:必须在 import taskRunner.js 之前完成 ----
38
+ const sandbox = mkdtempSync(path.join(os.tmpdir(), 'zen-taskrunner-test-'));
39
+ process.env.USERPROFILE = sandbox;
40
+ process.env.HOME = sandbox;
41
+ delete process.env.HOMEDRIVE;
42
+ delete process.env.HOMEPATH;
43
+
44
+ const { createOpencodeEventHandler, normalizeTaskExecutor, TASK_EXECUTORS } = await import(
45
+ pathToFileURL(path.join(projectRoot, 'src/ui/server/routes/workbench/taskRunner.js')).href
46
+ );
47
+
48
+ /** 可断言的 appendOutput/appendThinking 收集器 */
49
+ function makeSink() {
50
+ const sink = { output: [], thinking: [] };
51
+ sink.appendOutput = (t) => { if (t) sink.output.push(t); };
52
+ sink.appendThinking = (t) => { if (t) sink.thinking.push(t); };
53
+ return sink;
54
+ }
55
+
56
+ test('normalizeTaskExecutor:白名单 + 大小写 + 非法回落', () => {
57
+ assert.deepEqual(TASK_EXECUTORS, ['claude', 'opencode', 'codex', 'codebuddy', 'pi', 'codearts']);
58
+ assert.equal(normalizeTaskExecutor('opencode'), 'opencode');
59
+ assert.equal(normalizeTaskExecutor('codex'), 'codex');
60
+ assert.equal(normalizeTaskExecutor('codebuddy'), 'codebuddy');
61
+ assert.equal(normalizeTaskExecutor('pi'), 'pi');
62
+ // 大小写不敏感(存储前统一小写)
63
+ assert.equal(normalizeTaskExecutor('OpenCode'), 'opencode');
64
+ assert.equal(normalizeTaskExecutor('opencode '), 'opencode');
65
+ assert.equal(normalizeTaskExecutor('Codex '), 'codex');
66
+ assert.equal(normalizeTaskExecutor('gpt'), 'claude');
67
+ assert.equal(normalizeTaskExecutor(undefined), 'claude');
68
+ assert.equal(normalizeTaskExecutor(null), 'claude');
69
+ });
70
+
71
+ test('opencode 事件:text/reasoning 分别落 output/thinking,sessionID 被捕获', () => {
72
+ const job = {};
73
+ const sink = makeSink();
74
+ const handle = createOpencodeEventHandler(job, sink.appendOutput, sink.appendThinking);
75
+
76
+ handle({ type: 'step_start', sessionID: 'ses_a', part: { type: 'step-start' } });
77
+ handle({ type: 'reasoning', sessionID: 'ses_a', part: { id: 'p1', type: 'reasoning', text: '想一下' } });
78
+ handle({ type: 'text', sessionID: 'ses_a', part: { id: 'p2', type: 'text', text: '好的' } });
79
+ handle({ type: 'step_finish', sessionID: 'ses_a', part: { type: 'step-finish', tokens: {} } });
80
+
81
+ assert.deepEqual(sink.output, ['好的']);
82
+ assert.deepEqual(sink.thinking, ['想一下']);
83
+ assert.equal(job.claudeSessionId, 'ses_a');
84
+ assert.equal(job.agentError, undefined);
85
+ });
86
+
87
+ test('opencode 事件:同一 part 累积推送只追加增量', () => {
88
+ const job = {};
89
+ const sink = makeSink();
90
+ const handle = createOpencodeEventHandler(job, sink.appendOutput, sink.appendThinking);
91
+
92
+ // 累积语义:第二次事件的 text 是第一次的前缀延长
93
+ handle({ type: 'text', sessionID: 'ses_b', part: { id: 'p1', text: 'Hello' } });
94
+ handle({ type: 'text', sessionID: 'ses_b', part: { id: 'p1', text: 'Hello, world' } });
95
+ // 重复事件(完全相同):不产生任何输出
96
+ handle({ type: 'text', sessionID: 'ses_b', part: { id: 'p1', text: 'Hello, world' } });
97
+
98
+ assert.deepEqual(sink.output, ['Hello', ', world']);
99
+
100
+ // 纯增量语义(text 不是前次的前缀延长):整段追加
101
+ handle({ type: 'text', sessionID: 'ses_b', part: { id: 'p1', text: '!' } });
102
+ assert.deepEqual(sink.output, ['Hello', ', world', '!']);
103
+ });
104
+
105
+ test('opencode 事件:error 事件写入 agentError,只记第一条', () => {
106
+ const job = {};
107
+ const sink = makeSink();
108
+ const handle = createOpencodeEventHandler(job, sink.appendOutput, sink.appendThinking);
109
+
110
+ handle({ type: 'error', sessionID: 'ses_c', error: { name: 'UnknownError', data: { message: 'Unexpected server error' } } });
111
+ handle({ type: 'error', sessionID: 'ses_c', error: { name: 'UnknownError', data: { message: 'second one' } } });
112
+
113
+ assert.equal(job.agentError, 'Unexpected server error');
114
+ // error 前若有正文,正文保留(前端能看到已产出的部分)
115
+ assert.deepEqual(sink.output, []);
116
+ });
117
+
118
+ test('opencode 事件:error 结构缺 data.message 时逐级回落', () => {
119
+ const job = {};
120
+ const sink = makeSink();
121
+ const handle = createOpencodeEventHandler(job, sink.appendOutput, sink.appendThinking);
122
+
123
+ handle({ type: 'error', sessionID: 'ses_d', error: { name: 'ProviderError' } });
124
+ assert.equal(job.agentError, 'ProviderError');
125
+
126
+ const job2 = {};
127
+ const handle2 = createOpencodeEventHandler(job2, sink.appendOutput, sink.appendThinking);
128
+ handle2({ type: 'error', sessionID: 'ses_e', error: {} });
129
+ assert.equal(job2.agentError, 'opencode 执行出错');
130
+ });