zen-gitsync 2.17.5 → 2.17.7

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 (71) hide show
  1. package/README.md +35 -2
  2. package/package.json +1 -1
  3. package/scripts/release.js +139 -48
  4. package/src/aiCommit.js +6 -3
  5. package/src/cli/ai/agent.js +106 -311
  6. package/src/cli/ai/cli.integration.test.js +99 -0
  7. package/src/cli/ai/context.js +90 -0
  8. package/src/cli/ai/modelSetup.js +44 -8
  9. package/src/cli/ai/runtime.test.js +152 -0
  10. package/src/cli/ai/safety.js +27 -4
  11. package/src/cli/ai/safety.test.js +13 -0
  12. package/src/cli/ai/telemetry.js +43 -0
  13. package/src/cli/ai/termui.js +154 -41
  14. package/src/cli/ai/termui.test.js +63 -14
  15. package/src/cli/ai/tools.js +27 -5
  16. package/src/cli/ai/tools.test.js +23 -0
  17. package/src/cli/ai/transport.js +89 -0
  18. package/src/cli/ai/turn.js +126 -0
  19. package/src/config.js +88 -5
  20. package/src/ui/public/assets/{AgentView-DJOdvJEV.js → AgentView-CZc191Xm.js} +3 -3
  21. package/src/ui/public/assets/AgentView-CzUeyQfR.css +1 -0
  22. package/src/ui/public/assets/{AppVersionBadge-D2Li5_R-.js → AppVersionBadge-Gs5lUl7P.js} +2 -2
  23. package/src/ui/public/assets/{BranchSelector-CSd5uaVm.js → BranchSelector-IjaFk0S-.js} +1 -1
  24. package/src/ui/public/assets/{CommitForm-Ckg9-zay.js → CommitForm-C-sowOXH.js} +3 -3
  25. package/src/ui/public/assets/{CommonDialog-B8XOXtz_.js → CommonDialog-QyptFD1R.js} +1 -1
  26. package/src/ui/public/assets/{EditorView-Crj8S2TI.js → EditorView-CvvluFCY.js} +1 -1
  27. package/src/ui/public/assets/{FileDiffViewer-DL07IMz5.js → FileDiffViewer-BXVsdCxE.js} +1 -1
  28. package/src/ui/public/assets/{FlowExecutionViewer-DvVT19Qv.js → FlowExecutionViewer-CRxipXru.js} +1 -1
  29. package/src/ui/public/assets/{FlowOrchestrationWorkspace-kRhK9IyL.js → FlowOrchestrationWorkspace-AE1ZHTKF.js} +1 -1
  30. package/src/ui/public/assets/{IconButton-pqSYKRT8.js → IconButton-BmSueF_H.js} +1 -1
  31. package/src/ui/public/assets/{LogList-BrN6qtFb.js → LogList-iaVA-_aC.js} +1 -1
  32. package/src/ui/public/assets/{MindmapView-CSsT0NDZ.js → MindmapView-DcG4jpCh.js} +1 -1
  33. package/src/ui/public/assets/{MonitorView-edRyBybQ.js → MonitorView-DO59syvl.js} +1 -1
  34. package/src/ui/public/assets/{ProjectStartupButton-DZ8VgU79.js → ProjectStartupButton-DYppceyw.js} +1 -1
  35. package/src/ui/public/assets/{RemoteManagerDialog-pu9szuYB.js → RemoteManagerDialog-CDbxG6pZ.js} +1 -1
  36. package/src/ui/public/assets/{RemoteRepoCard-BSFehgY5.js → RemoteRepoCard-Cko2KB8w.js} +1 -1
  37. package/src/ui/public/assets/{SourceMapView-DbEydLSU.js → SourceMapView-CwWPVdz-.js} +1 -1
  38. package/src/ui/public/assets/{SvgIcon-BVCbrPws.js → SvgIcon-Cf7nOKc8.js} +1 -1
  39. package/src/ui/public/assets/{TemplateManager-DPyYaGCE.js → TemplateManager-DWPDjWxM.js} +1 -1
  40. package/src/ui/public/assets/{UserInputNode-DXHeCjzG.js → UserInputNode-YOziN-5A.js} +1 -1
  41. package/src/ui/public/assets/WorkbenchView-DNnEpIYy.js +13 -0
  42. package/src/ui/public/assets/WorkbenchView-v3QFn6Ln.css +1 -0
  43. package/src/ui/public/assets/_plugin-vue_export-helper-CQjg8MXm.js +19 -0
  44. package/src/ui/public/assets/configStore-C7HOx5S1.js +1 -0
  45. package/src/ui/public/assets/{gitStore-BJbNtjxA.js → gitStore-DIsBd6n4.js} +1 -1
  46. package/src/ui/public/assets/index-FOe1cHkh.css +1 -0
  47. package/src/ui/public/assets/index-MmcnT2Dn.js +24 -0
  48. package/src/ui/public/assets/{previewSandbox-BZV6Eqhv.js → previewSandbox-TKucqLxt.js} +0 -0
  49. package/src/ui/public/assets/{toolsStore-Bu8Ujw_A.js → toolsStore-BiC2mmC3.js} +1 -1
  50. package/src/ui/public/assets/{vendor-CwZs6S_i.js → vendor-92fKgtnz.js} +7 -7
  51. package/src/ui/public/assets/{vendor-_6pgW9Z2.css → vendor-B1UrVTTI.css} +1 -1
  52. package/src/ui/public/index.html +14 -14
  53. package/src/ui/server/routes/codeAnalysis.js +6 -3
  54. package/src/ui/server/routes/config.js +40 -9
  55. package/src/ui/server/routes/fileOpen.js +48 -2
  56. package/src/ui/server/routes/gAiLauncher.test.js +70 -0
  57. package/src/ui/server/routes/workbench/agentChat.js +34 -8
  58. package/src/ui/server/routes/workbench/index.js +37 -6
  59. package/src/ui/server/routes/workbench/jobStore.js +4 -0
  60. package/src/ui/server/routes/workbench/llmClient.js +15 -8
  61. package/src/ui/server/routes/workbench/taskRunner.js +276 -96
  62. package/src/ui/server/routes/workbench/taskRunner.opencode.test.js +126 -0
  63. package/src/utils/aiEndpoint.js +231 -0
  64. package/src/utils/aiEndpoint.test.js +175 -0
  65. package/src/ui/public/assets/AgentView-BsN9y4Kg.css +0 -1
  66. package/src/ui/public/assets/WorkbenchView-CKXtZAJn.css +0 -1
  67. package/src/ui/public/assets/WorkbenchView-DGcirla3.js +0 -13
  68. package/src/ui/public/assets/_plugin-vue_export-helper-C-YBnnP5.js +0 -19
  69. package/src/ui/public/assets/configStore-DoobmT7N.js +0 -1
  70. package/src/ui/public/assets/index-ClqvC4kJ.css +0 -1
  71. package/src/ui/public/assets/index-ndDaGoRO.js +0 -24
@@ -35,10 +35,27 @@ import { agentSessionStore } from './agentSessionStore.js';
35
35
  import { TOOL_DEFINITIONS, executeTool } from '../../../../cli/ai/tools.js';
36
36
  import { checkDangerousCommand } from '../../../../cli/ai/safety.js';
37
37
  import { guardCommand } from '../../../../cli/ai/platformGuard.js';
38
+ import configManager from '../../../../config.js';
39
+ import { buildAiChatRequest, describeAiHttpError } from '../../../../utils/aiEndpoint.js';
38
40
 
39
- const MAX_TOOL_ITERATIONS = 40;
41
+ // 单轮工具调用循环数的兜底值(防失控);实际值取全局配置 aiMaxToolIterations,
42
+ // 与 CLI 侧 src/cli/ai/agent.js 共用同一个配置项。
43
+ const DEFAULT_MAX_TOOL_ITERATIONS = 200;
40
44
  const LLM_TIMEOUT_MS = 300000; // 5 分钟
41
45
 
46
+ // 读取全局配置里的单轮工具调用上限。
47
+ // 读配置失败不该把整轮对话打挂 —— 退回默认值继续跑,比用户消息直接发不出去好。
48
+ async function resolveMaxToolIterations() {
49
+ try {
50
+ const cfg = await configManager.loadConfig();
51
+ const n = Number(cfg?.aiMaxToolIterations);
52
+ if (Number.isFinite(n) && n > 0) return Math.floor(n);
53
+ } catch (err) {
54
+ logger.warn(`[agentChat] 读取 aiMaxToolIterations 失败,回退默认值: ${err?.message || err}`);
55
+ }
56
+ return DEFAULT_MAX_TOOL_ITERATIONS;
57
+ }
58
+
42
59
  // ── 系统提示词构建 ──────────────────────────────────────────
43
60
  // 与 CLI agent.js 的 buildSystemPrompt 保持一致,但标注来源为 Web 端
44
61
  function buildWebSystemPrompt({ cwd, locale }) {
@@ -138,10 +155,14 @@ ${isWin ? `- This is Windows. The following Unix commands do NOT exist here:
138
155
 
139
156
  // ── LLM 流式调用(OpenAI 兼容 + function calling) ──────────
140
157
  // 返回 { content, toolCalls, aborted }
141
- async function streamChatOnce({ model, messages, signal, onToken }) {
142
- const url = `${String(model.baseURL || '').replace(/\/$/, '')}/chat/completions`;
143
- const headers = { 'Content-Type': 'application/json' };
144
- if (model.apiKey) headers['Authorization'] = `Bearer ${model.apiKey}`;
158
+ async function streamChatOnce({ model, messages, signal, onToken, sessionId }) {
159
+ // sessionId = 对话 ID:OpenCode 网关靠它做路由与提示缓存,同一轮对话的所有请求要复用
160
+ const { url, headers } = buildAiChatRequest({
161
+ baseURL: model.baseURL,
162
+ model: model.model,
163
+ apiKey: model.apiKey,
164
+ sessionId,
165
+ });
145
166
 
146
167
  const body = JSON.stringify({
147
168
  model: model.model,
@@ -167,7 +188,8 @@ async function streamChatOnce({ model, messages, signal, onToken }) {
167
188
  const resp = await fetch(url, { method: 'POST', headers, body, signal: controller.signal });
168
189
  if (!resp.ok || !resp.body) {
169
190
  const errText = await resp.text().catch(() => '');
170
- const snippet = errText.length > 300 ? errText.slice(0, 300) + '…' : errText;
191
+ // 抽 error.message:网关/provider 的说明都在里面,整坨 JSON 甩到聊天窗没法看
192
+ const snippet = describeAiHttpError(errText, resp.status);
171
193
  if (resp.status === 400 && /tool|function/i.test(snippet)) {
172
194
  throw new Error(`HTTP 400: 当前模型可能不支持 function calling(${snippet})。请在设置中换用支持工具调用的模型。`);
173
195
  }
@@ -342,7 +364,9 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
342
364
  // 旧图片降级
343
365
  stripStaleImages(session.messages);
344
366
 
345
- for (let iter = 0; iter < MAX_TOOL_ITERATIONS; iter++) {
367
+ const maxIterations = await resolveMaxToolIterations();
368
+
369
+ for (let iter = 0; iter < maxIterations; iter++) {
346
370
  trimHistory(session.messages);
347
371
  sanitizeMessages(session.messages);
348
372
 
@@ -352,6 +376,8 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
352
376
  model,
353
377
  messages: session.messages,
354
378
  signal,
379
+ // 整轮对话(含后续工具调用产生的每一轮请求)复用同一个会话 ID
380
+ sessionId: session.sessionId,
355
381
  onToken: ({ thinking, content }) => {
356
382
  if (thinking) send({ type: 'thinking', delta: thinking });
357
383
  if (content) send({ type: 'content', delta: content });
@@ -418,7 +444,7 @@ export async function runAgentTurn({ session, model, userMessage, images = [], c
418
444
  }
419
445
 
420
446
  // 达到最大迭代次数
421
- send({ type: 'done', content: `已达单轮最大工具调用次数(${MAX_TOOL_ITERATIONS}),本轮结束。如需继续请再发一条消息。` });
447
+ send({ type: 'done', content: `已达单轮最大工具调用次数(${maxIterations}),本轮结束。如需继续请再发一条消息。` });
422
448
  return { aborted: false };
423
449
  }
424
450
 
@@ -105,6 +105,7 @@ import {
105
105
  } from './jobStore.js';
106
106
  import {
107
107
  launchClaudeInNewWindow,
108
+ normalizeTaskExecutor,
108
109
  runTaskQueue,
109
110
  runSingleSubtask,
110
111
  syncSubToCancelled,
@@ -138,6 +139,25 @@ import {
138
139
 
139
140
  const { genSessionId, read: readSessionFile, write: writeSessionFile, delete: deleteSessionFile, listMeta: listSessionsMeta, enforceRetention: enforceSessionsRetention } = sessionStore;
140
141
 
142
+ /**
143
+ * 解析本次执行用哪个执行器:body.executor 显式指定 > 配置里的全局默认 > 'claude'。
144
+ * 配置读失败一律回落 'claude' —— 执行器选不出来不该挡住任务本身能跑。
145
+ */
146
+ async function resolveExecutor(requested) {
147
+ const explicit = normalizeTaskExecutor(requested);
148
+ if (explicit) return explicit;
149
+ try {
150
+ if (configManager) {
151
+ const cfg = await configManager.loadConfig();
152
+ const fallback = normalizeTaskExecutor(cfg?.taskExecutor);
153
+ if (fallback) return fallback;
154
+ }
155
+ } catch (err) {
156
+ logger.warn('[workbench] 读取 taskExecutor 配置失败,回落 claude:', err && err.message || err);
157
+ }
158
+ return 'claude';
159
+ }
160
+
141
161
  /**
142
162
  * 注册所有 workbench 路由(共 45 个端点)。
143
163
  * @param {Object} deps
@@ -1059,9 +1079,10 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
1059
1079
  throw new HttpError(400, '任务没有子任务');
1060
1080
  }
1061
1081
  const repoPath = resolveTaskRepoPath(task, typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '');
1082
+ const executor = await resolveExecutor(req.body?.executor);
1062
1083
  // 异步执行,立即返回
1063
1084
  res.json({ success: true, message: '已开始执行' });
1064
- runTaskQueue(task, repoPath, '').catch(err => {
1085
+ runTaskQueue(task, repoPath, '', { executor }).catch(err => {
1065
1086
  publish('task:error', { taskId: task.id, error: err.message });
1066
1087
  });
1067
1088
  }));
@@ -1078,8 +1099,9 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
1078
1099
  throw new HttpError(400, 'startSubIndex 越界');
1079
1100
  }
1080
1101
  const repoPath = resolveTaskRepoPath(task, typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '');
1102
+ const executor = await resolveExecutor(req.body?.executor);
1081
1103
  res.json({ success: true, message: `已从第 ${startSubIndex + 1} 个子任务开始执行` });
1082
- runTaskQueue(task, repoPath, '', { fromIndex: startSubIndex }).catch(err => {
1104
+ runTaskQueue(task, repoPath, '', { fromIndex: startSubIndex, executor }).catch(err => {
1083
1105
  publish('task:error', { taskId: task.id, error: err.message });
1084
1106
  });
1085
1107
  }));
@@ -1092,6 +1114,7 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
1092
1114
  throw new HttpError(400, '该任务不是简单任务,请使用普通执行接口');
1093
1115
  }
1094
1116
  const repoPath = resolveTaskRepoPath(task, typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '');
1117
+ const executor = await resolveExecutor(req.body?.executor);
1095
1118
  const virtualSub = {
1096
1119
  id: `${task.id}__simple`,
1097
1120
  title: task.title,
@@ -1101,7 +1124,7 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
1101
1124
  attachments: Array.isArray(task.attachments) ? task.attachments : []
1102
1125
  };
1103
1126
  res.json({ success: true, message: '已开始执行简单任务' });
1104
- runSingleSubtask(task, virtualSub, repoPath, '', []).catch(err => {
1127
+ runSingleSubtask(task, virtualSub, repoPath, '', [], { executor }).catch(err => {
1105
1128
  publish('task:error', { taskId: task.id, error: err.message });
1106
1129
  });
1107
1130
  }));
@@ -1124,11 +1147,12 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
1124
1147
  const liveJob = snapshotJobs().find(j => j.subId === subId && (j.status === 'running' || j.status === 'pending'));
1125
1148
  if (liveJob) throw new HttpError(400, '该子任务已有正在执行的 job');
1126
1149
  const repoPath = resolveTaskRepoPath(foundTask, typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '');
1150
+ const executor = await resolveExecutor(req.body?.executor);
1127
1151
  res.json({ success: true, message: `已开始执行子任务:${foundSub.title || subId}` });
1128
1152
  (async () => {
1129
1153
  try {
1130
1154
  const priorOutputs = await collectPriorOutputs(foundTask, foundSub);
1131
- await runSingleSubtask(foundTask, foundSub, repoPath, '', priorOutputs);
1155
+ await runSingleSubtask(foundTask, foundSub, repoPath, '', priorOutputs, { executor });
1132
1156
  await persistTaskAfterRun(foundTask);
1133
1157
  } catch (err) {
1134
1158
  publish('task:error', { taskId: foundTask.id, error: err.message });
@@ -1240,7 +1264,12 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
1240
1264
  };
1241
1265
  const repoPath = typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '';
1242
1266
  res.json({ success: true, message: '已加入续接队列' });
1243
- runSingleSubtask(task, virtualSub, repoPath, '', [], { resumeSessionId: prevJob.claudeSessionId })
1267
+ // 执行器必须沿用上一轮 job 的 agent:claude 的 --resume 和 opencode 的 --session
1268
+ // 互不认对方的会话 id,串了执行器续接必然失败。
1269
+ runSingleSubtask(task, virtualSub, repoPath, '', [], {
1270
+ resumeSessionId: prevJob.claudeSessionId,
1271
+ executor: normalizeTaskExecutor(prevJob.agent)
1272
+ })
1244
1273
  .catch(err => {
1245
1274
  publish('task:error', { taskId: task.id, error: err.message });
1246
1275
  });
@@ -2113,7 +2142,9 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
2113
2142
  promptOverride: task.simpleOverride || '',
2114
2143
  attachments: [],
2115
2144
  };
2116
- runSingleSubtask(task, virtualSub, targetPath, '', []).catch(err => {
2145
+ // 派发控制台可以显式指定执行器;不传则用配置里的全局默认(resolveExecutor 走回落链)
2146
+ const executor = await resolveExecutor(req.body?.executor);
2147
+ runSingleSubtask(task, virtualSub, targetPath, '', [], { executor }).catch(err => {
2117
2148
  publish('task:error', { taskId: task.id, error: err.message });
2118
2149
  });
2119
2150
  }
@@ -309,11 +309,15 @@ export function snapshotJobs() {
309
309
  status: j.status,
310
310
  prompt: j.prompt || '',
311
311
  output: j.output || '',
312
+ thinking: j.thinking || '',
312
313
  pid: j.pid || null,
313
314
  startedAt: j.startedAt || null,
314
315
  endedAt: j.endedAt || null,
315
316
  exitCode: typeof j.exitCode === 'number' ? j.exitCode : null,
316
317
  error: j.error || null,
318
+ // 本轮用的执行器(claude | opencode)。前端对话区助手名 / 日志详情按它显示;
319
+ // 加字段时记得同步这里 —— 白名单投影会把没列出的字段静默剥掉。
320
+ agent: j.agent || null,
317
321
  // 续接对话用:claude --output-format stream-json 的 system.init 事件捕获到的 session_id
318
322
  claudeSessionId: j.claudeSessionId || null
319
323
  }));
@@ -23,6 +23,7 @@
23
23
  // - cloneMsgContent 深拷贝消息 content(避免外部 mutation 污染请求)
24
24
 
25
25
  import { stripThinkingBlocks, extractFirstJsonObject } from './jsonParse.js';
26
+ import { buildAiChatRequest, describeAiHttpError } from '../../../../utils/aiEndpoint.js';
26
27
 
27
28
  // 非流式调用 LLM,要求返回 JSON 对象。
28
29
  // 失败时抛错;成功时返回已 parse 的对象。
@@ -32,9 +33,11 @@ import { stripThinkingBlocks, extractFirstJsonObject } from './jsonParse.js';
32
33
  export async function callLlmJson(model, prompt, opts = {}) {
33
34
  const { timeoutMs = 60000, images = [] } = opts;
34
35
  const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
35
- const url = `${String(model.baseURL || '').replace(/\/$/, '')}/chat/completions`;
36
- const headers = { 'Content-Type': 'application/json' };
37
- if (model.apiKey) headers['Authorization'] = `Bearer ${model.apiKey}`;
36
+ const { url, headers } = buildAiChatRequest({
37
+ baseURL: model.baseURL,
38
+ model: model.model,
39
+ apiKey: model.apiKey,
40
+ });
38
41
 
39
42
  let userContent;
40
43
  if (Array.isArray(images) && images.length > 0) {
@@ -115,11 +118,14 @@ export function cloneMsgContent(c) {
115
118
  * 函数返回 { aborted: true },不抛错——上层决定怎么处理。
116
119
  */
117
120
  export async function callLlmStream(model, input, onDelta, opts = {}) {
118
- const { maxTokens = 2000, timeoutMs = 600000, signal, images = [], systemPrompt } = opts;
121
+ const { maxTokens = 2000, timeoutMs = 600000, signal, images = [], systemPrompt, sessionId } = opts;
119
122
  const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
120
- const url = `${String(model.baseURL || '').replace(/\/$/, '')}/chat/completions`;
121
- const headers = { 'Content-Type': 'application/json' };
122
- if (model.apiKey) headers['Authorization'] = `Bearer ${model.apiKey}`;
123
+ const { url, headers } = buildAiChatRequest({
124
+ baseURL: model.baseURL,
125
+ model: model.model,
126
+ apiKey: model.apiKey,
127
+ sessionId,
128
+ });
123
129
 
124
130
  // 判别 input: string(旧,直接拆分)或 messages 数组(新,对话拆分)
125
131
  let messages;
@@ -167,7 +173,8 @@ export async function callLlmStream(model, input, onDelta, opts = {}) {
167
173
  const resp = await fetch(url, { method: 'POST', headers, body, signal: controller.signal });
168
174
  if (!resp.ok || !resp.body) {
169
175
  const errText = await resp.text().catch(() => '');
170
- throw new Error(errText || `HTTP ${resp.status}`);
176
+ // 抽 error.message,别把整坨 JSON 甩给用户(网关的报错说明都在 message 里)
177
+ throw new Error(describeAiHttpError(errText, resp.status));
171
178
  }
172
179
 
173
180
  // SSE 格式:每行 "data: {...}",最后 "data: [DONE]"