zen-gitsync 2.17.47 → 2.17.50

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 (64) hide show
  1. package/README.md +18 -2
  2. package/package.json +4 -2
  3. package/src/cli/ai/mcp.js +323 -27
  4. package/src/cli/ai/mcp.test.js +202 -2
  5. package/src/cli/ai/modelSetup.js +63 -2
  6. package/src/cli/ai/modelSetup.test.js +28 -2
  7. package/src/ui/public/assets/{AgentView-CRqQYAzh.css → AgentView-CAPlZLDq.css} +1 -1
  8. package/src/ui/public/assets/AgentView-D3EyOwN5.js +1 -0
  9. package/src/ui/public/assets/{AppVersionBadge-IWS9XBUV.js → AppVersionBadge-U0CyGRID.js} +2 -2
  10. package/src/ui/public/assets/{BranchSelector-C_mK2mkS.js → BranchSelector-CJQb9Z16.js} +1 -1
  11. package/src/ui/public/assets/{CommitForm-C6I9rQXn.js → CommitForm-D25Wj9NR.js} +1 -1
  12. package/src/ui/public/assets/{CommonDialog-2zVHBuqT.js → CommonDialog-33jIvgti.js} +1 -1
  13. package/src/ui/public/assets/{AgentEngineSelector-D7DmG6PU.css → CopySessionButton-BLUHPwDR.css} +1 -1
  14. package/src/ui/public/assets/CopySessionButton-Bpe-A3h0.js +20 -0
  15. package/src/ui/public/assets/EditorView-B3bNe9Ic.css +1 -0
  16. package/src/ui/public/assets/EditorView-Ce4rZawb.js +1 -0
  17. package/src/ui/public/assets/{FlowExecutionViewer-0Xw0oDen.js → FlowExecutionViewer-CAiAPsVH.js} +1 -1
  18. package/src/ui/public/assets/{FlowOrchestrationWorkspace-BKUspY-s.js → FlowOrchestrationWorkspace-CV84hp8i.js} +1 -1
  19. package/src/ui/public/assets/{LogList-pbwn5n2S.js → LogList-DMM6xsft.js} +1 -1
  20. package/src/ui/public/assets/{MindmapView-DYh6nuuF.js → MindmapView-FEdyCLvB.js} +1 -1
  21. package/src/ui/public/assets/{MonitorView-CsJyUL9v.js → MonitorView-DSngr6xg.js} +1 -1
  22. package/src/ui/public/assets/{ProjectStartupButton-BhFQfAX1.js → ProjectStartupButton-DzodaStk.js} +1 -1
  23. package/src/ui/public/assets/RecentDirectoriesChat-DvNSA3M_.js +1 -0
  24. package/src/ui/public/assets/RecentDirectoriesChat-THkqWEtO.css +1 -0
  25. package/src/ui/public/assets/{RemoteManagerDialog-CIZagKkC.js → RemoteManagerDialog-B8A7vQ0s.js} +1 -1
  26. package/src/ui/public/assets/{RemoteRepoCard-Dj-B-6WU.js → RemoteRepoCard-ChdaJMso.js} +1 -1
  27. package/src/ui/public/assets/{SourceMapView-pIlmQzYw.js → SourceMapView-DxBk0ftU.js} +1 -1
  28. package/src/ui/public/assets/{SvgIcon-CaefOO1F.js → SvgIcon-IjDm7vQ-.js} +1 -1
  29. package/src/ui/public/assets/{UserInputNode-1--I25vm.js → UserInputNode-DE8GEC3J.js} +1 -1
  30. package/src/ui/public/assets/WorkbenchView--CxNdUys.js +8 -0
  31. package/src/ui/public/assets/WorkbenchView-BaUAQnQR.css +1 -0
  32. package/src/ui/public/assets/{_plugin-vue_export-helper-UTW98kcD.js → _plugin-vue_export-helper-DcVJ6Vx3.js} +5 -5
  33. package/src/ui/public/assets/agentConversations-Dzt5oiO3.js +8 -0
  34. package/src/ui/public/assets/{configStore-H-n3ukZ_.js → configStore-jJrldIrT.js} +1 -1
  35. package/src/ui/public/assets/{dagre-Br4Eexe7.js → dagre-Dkbv4_Ri.js} +3 -3
  36. package/src/ui/public/assets/{element-plus-CZdvD4lq.js → element-plus-BFy5M3jt.js} +1 -1
  37. package/src/ui/public/assets/{flow-mindmap-4kZ-2J2Y.js → flow-mindmap-Qo4JDluG.js} +1 -1
  38. package/src/ui/public/assets/{index-CD7otmb_.js → index-Do0MeO4E.js} +7 -7
  39. package/src/ui/public/assets/{monaco-CoYCcK3Q.js → monaco-Eb1VRwsF.js} +1 -1
  40. package/src/ui/public/assets/{office-docx-DndLP6GO.js → office-docx-BeV9kLXP.js} +1 -1
  41. package/src/ui/public/assets/{office-excel-Jt5zzo6D.js → office-excel-CILIK4LB.js} +1 -1
  42. package/src/ui/public/assets/{office-pptx-J8tzRTjU.js → office-pptx-SxwVIVqF.js} +1 -1
  43. package/src/ui/public/assets/{vendor-BD09UDuP.js → vendor-BXLxNxh-.js} +502 -500
  44. package/src/ui/public/assets/vendor-aCkTc13z.css +1 -0
  45. package/src/ui/public/assets/{vue-flow-CkM05TWt.js → vue-flow--96Pa3sG.js} +1 -1
  46. package/src/ui/public/index.html +12 -12
  47. package/src/ui/server/routes/workbench/agentMarketplace.js +65 -30
  48. package/src/ui/server/routes/workbench/agentMarketplace.test.js +28 -4
  49. package/src/ui/server/routes/workbench/liveJobs.js +4 -0
  50. package/src/ui/server/routes/workbench/liveJobs.test.js +11 -0
  51. package/src/ui/server/routes/workbench/taskRunner.js +44 -7
  52. package/src/ui/server/routes/workbench/taskRunner.thinking.test.js +115 -0
  53. package/src/utils/aiEndpoint.js +48 -0
  54. package/src/utils/aiEndpoint.test.js +51 -1
  55. package/src/ui/public/assets/AgentEngineSelector-CKiVaqrY.js +0 -1
  56. package/src/ui/public/assets/AgentView-c1frCBc3.js +0 -1
  57. package/src/ui/public/assets/EditorView-DUJrWJgh.js +0 -1
  58. package/src/ui/public/assets/EditorView-DqagGedH.css +0 -1
  59. package/src/ui/public/assets/RecentDirectoriesChat-BxbpKrXc.css +0 -1
  60. package/src/ui/public/assets/RecentDirectoriesChat-DM6sRO9L.js +0 -1
  61. package/src/ui/public/assets/WorkbenchView-BLzcZqy0.css +0 -1
  62. package/src/ui/public/assets/WorkbenchView-bpzL6m84.js +0 -20
  63. package/src/ui/public/assets/agentConversations-B9LNft3v.js +0 -8
  64. package/src/ui/public/assets/vendor-DNiTBOIX.css +0 -1
@@ -704,7 +704,8 @@ async function installedMcpsIn(file, target) {
704
704
  name: clampText(server?.name || id, 80),
705
705
  description: clampText(server?.description || '', 400),
706
706
  package: server?.package || '',
707
- command: server?.command || '',
707
+ // 手写的 url 型(http 传输)没有 command,退回显示 url —— 否则列表里这一项看起来是空的
708
+ command: server?.command || server?.url || '',
708
709
  args: Array.isArray(server?.args) ? server.args : [],
709
710
  envKeys: Object.keys(server?.env || {}),
710
711
  file,
@@ -878,11 +879,10 @@ async function installSkill({ item, target, cwd }) {
878
879
  async function installMcp({ item, target, cwd }) {
879
880
  const packageName = String(item.package || '').trim();
880
881
  const isStdio = !!packageName;
881
- // 只有 remote 端点的 server:用 mcp-remote 桥接成 stdio,
882
- // 否则 g ai 的 stdio 客户端根本连不上。这是生态里的通行做法。
883
- const bridge = !isStdio && item.remoteUrl ? 'mcp-remote' : '';
882
+ const remoteUrl = String(item.remoteUrl || '').trim();
883
+ const isRemote = !isStdio && /^https?:\/\//i.test(remoteUrl);
884
884
 
885
- if (!isStdio && !bridge) {
885
+ if (!isStdio && !isRemote) {
886
886
  throw new HttpError(422, '该 MCP 服务没有提供 npm 包或远程端点,暂时无法一键安装');
887
887
  }
888
888
  if (isStdio && !SAFE_PACKAGE.test(packageName)) throw new HttpError(400, 'MCP npm 包名不合法');
@@ -896,6 +896,54 @@ async function installMcp({ item, target, cwd }) {
896
896
  throw new HttpError(409, `该 MCP 服务已安装在${paths.label}`);
897
897
  }
898
898
 
899
+ // 安装时顺手收集的键值(弹窗里用户填的)。只收合法的标识符键,
900
+ // 值为空的键跳过 —— 留给用户以后手改配置文件,而不是写一个空值进去让 server 起不来。
901
+ const filled = {};
902
+ if (item.env && typeof item.env === 'object') {
903
+ for (const [key, value] of Object.entries(item.env)) {
904
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(String(key))) continue;
905
+ const text = String(value ?? '').trim();
906
+ if (text) filled[key] = clampText(text, 2000);
907
+ }
908
+ }
909
+ // 还缺哪些必填键,记下来给 UI 提示"装好了但还差配置"
910
+ const missingKeys = (Array.isArray(item.envKeys) ? item.envKeys : []).filter(key => !filled[key]);
911
+ const name = item.name || id;
912
+ const description = clampText(item.description || '', 400);
913
+
914
+ // ── 远程端点:直接写 http 配置,g ai 自己的 Streamable HTTP 客户端直连 ──
915
+ //
916
+ // 早先这里是桥成 stdio(npx -y mcp-remote <url>)。桥本身没错,但**鉴权传不进去**:
917
+ // mcp-remote 认的是它自己的 --header 约定,不是我们收集的那组键值 ——
918
+ // 表现就是"装好了但用不了"。g ai 现已原生支持 Streamable HTTP,直接落 url + headers
919
+ // 更短、更可控,也少一层随包升级漂移的中间件。
920
+ if (isRemote) {
921
+ const headers = {};
922
+ if (item.headers && typeof item.headers === 'object') {
923
+ for (const [key, value] of Object.entries(item.headers)) {
924
+ if (!key || value === undefined || value === null) continue;
925
+ headers[String(key)] = clampText(String(value), 2000);
926
+ }
927
+ }
928
+ // 弹窗收集的键值对远程服务来说就是请求头(没有子进程,env 无处可落)
929
+ Object.assign(headers, filled);
930
+
931
+ config.mcpServers[id] = {
932
+ name,
933
+ description,
934
+ type: 'http',
935
+ url: remoteUrl,
936
+ headers,
937
+ transport: 'remote',
938
+ requiredEnv: missingKeys,
939
+ };
940
+ await fs.mkdir(path.dirname(paths.file), { recursive: true });
941
+ await fs.writeFile(paths.file, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
942
+ return { id, type: 'mcp', name, target, file: paths.file, envKeys: missingKeys, warning: '' };
943
+ }
944
+
945
+ // ── npm 包:stdio ──
946
+ //
899
947
  // 项目级顺手 npm install 一次:把版本钉进 package.json,启动也更快。
900
948
  // 失败不阻断 —— npx 运行时仍会按需下载,只是慢一点。
901
949
  //
@@ -903,49 +951,36 @@ async function installMcp({ item, target, cwd }) {
903
951
  // 装到 ~/.zen-gitsync/ai/node_modules 里 npx 根本解析不到,等于白装一份;
904
952
  // npx 自己的缓存本来就是按用户全局共享的,首次启动下载一次即可。
905
953
  let installWarning = '';
906
- const primary = isStdio ? packageName : bridge;
907
954
  if (target === 'project') {
908
955
  try {
909
- await execFileAsync(npmBinary(), ['install', '--save-dev', primary], { cwd, timeout: NPM_TIMEOUT_MS, windowsHide: true });
956
+ await execFileAsync(npmBinary(), ['install', '--save-dev', packageName], { cwd, timeout: NPM_TIMEOUT_MS, windowsHide: true });
910
957
  } catch (err) {
911
958
  installWarning = `npm install 未成功(${clampText(err.message, 200)}),已写入配置,首次启动时会由 npx 按需下载`;
912
959
  }
913
960
  }
914
961
 
915
- const args = isStdio
916
- ? (Array.isArray(item.args) && item.args.length ? item.args : ['-y', packageName])
917
- : ['-y', bridge, String(item.remoteUrl)];
962
+ const args = Array.isArray(item.args) && item.args.length ? item.args : ['-y', packageName];
918
963
  const replaceProject = value => String(value).replaceAll('${project}', cwd || process.cwd());
919
-
920
- // 安装时顺手收集环境变量(弹窗里用户填的)。只收合法的标识符键,
921
- // 值为空的键跳过 —— 留给用户以后手改配置文件,而不是写一个空值进去让 server 起不来。
922
- const env = {};
923
- if (item.env && typeof item.env === 'object') {
924
- for (const [key, value] of Object.entries(item.env)) {
925
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(key))) continue;
926
- const text = String(value ?? '').trim();
927
- if (text) env[key] = clampText(text, 2000);
928
- }
929
- }
930
- // 还缺哪些必填环境变量,记下来给 UI 提示"装好了但还差配置"
931
- const missingEnv = (Array.isArray(item.envKeys) ? item.envKeys : []).filter(key => !env[key]);
964
+ // 环境变量名比请求头名严(不接受连字符),stdio 这条路再筛一遍
965
+ const stdioEnv = Object.fromEntries(
966
+ Object.entries(filled).filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)),
967
+ );
932
968
 
933
969
  config.mcpServers[id] = {
934
- name: item.name || id,
935
- description: clampText(item.description || '', 400),
970
+ name,
971
+ description,
936
972
  command: 'npx',
937
973
  args: args.map(replaceProject),
938
- env,
974
+ env: stdioEnv,
939
975
  // 记下来给 UI 提示"这个 server 需要哪些环境变量",也便于卸载时回溯
940
- requiredEnv: missingEnv,
941
- package: isStdio ? packageName : bridge,
942
- ...(item.transport === 'remote' ? { transport: 'remote', remoteUrl: item.remoteUrl } : {}),
976
+ requiredEnv: missingKeys,
977
+ package: packageName,
943
978
  };
944
979
 
945
980
  await fs.mkdir(path.dirname(paths.file), { recursive: true });
946
981
  await fs.writeFile(paths.file, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
947
982
 
948
- return { id, type: 'mcp', name: item.name || id, target, file: paths.file, envKeys: missingEnv, warning: installWarning };
983
+ return { id, type: 'mcp', name, target, file: paths.file, envKeys: missingKeys, warning: installWarning };
949
984
  }
950
985
 
951
986
  /**
@@ -249,24 +249,48 @@ test('install mcp: 非法包名被拒绝,且不写出配置文件', async () =>
249
249
  if (!before) assert.equal(await exists(PROJECT_MCP), false);
250
250
  });
251
251
 
252
- test('install mcp: 只有远程端点时用 mcp-remote 桥接,而不是报错或装出一个无效配置', async () => {
252
+ test('install mcp: 只有远程端点时直接写 http 配置(url + headers),不再绕 mcp-remote', async () => {
253
253
  const { call } = harness();
254
254
  const { status } = await call('POST /api/agent/marketplace/install', {
255
255
  body: {
256
256
  type: 'mcp',
257
257
  target: 'global',
258
258
  cwd: sandboxProject,
259
- item: { id: 'remote-only', name: 'Remote', remoteUrl: 'https://example.com/mcp', transport: 'remote' },
259
+ item: {
260
+ id: 'remote-only',
261
+ name: 'Remote',
262
+ remoteUrl: 'https://example.com/mcp',
263
+ transport: 'remote',
264
+ env: { Authorization: 'Bearer t0ken' },
265
+ },
260
266
  },
261
267
  });
262
268
  assert.equal(status, 200);
263
269
  const config = JSON.parse(await fs.readFile(AI_MCP_FILE, 'utf8'));
264
270
  const server = config.mcpServers['remote-only'];
265
- assert.equal(server.command, 'npx');
266
- assert.deepEqual(server.args, ['-y', 'mcp-remote', 'https://example.com/mcp']);
271
+ assert.equal(server.type, 'http');
272
+ assert.equal(server.url, 'https://example.com/mcp');
273
+ // 弹窗里填的键值对远程服务就是请求头(g ai 的 http 客户端只认 headers)
274
+ assert.equal(server.headers.Authorization, 'Bearer t0ken');
275
+ assert.equal(server.command, undefined, 'http 型不该再落 command,否则会被当成 stdio 去 spawn');
267
276
  assert.equal(server.transport, 'remote');
268
277
  });
269
278
 
279
+ test('install mcp: 远程端点不是 http(s) 时拒绝,不写出半成品配置', async () => {
280
+ const { call } = harness();
281
+ const before = await exists(AI_MCP_FILE);
282
+ const { status } = await call('POST /api/agent/marketplace/install', {
283
+ body: {
284
+ type: 'mcp',
285
+ target: 'global',
286
+ cwd: sandboxProject,
287
+ item: { id: 'bad-remote', name: 'Bad', remoteUrl: 'ftp://example.com/mcp', transport: 'remote' },
288
+ },
289
+ });
290
+ assert.equal(status, 422);
291
+ if (!before) assert.equal(await exists(AI_MCP_FILE), false);
292
+ });
293
+
270
294
  test('install mcp: 既没有包也没有远程端点时拒绝', async () => {
271
295
  const { call } = harness();
272
296
  const { status } = await call('POST /api/agent/marketplace/install', {
@@ -91,6 +91,10 @@ export function projectJob(j) {
91
91
  // 最后一次产出的时刻。别的实例跑的任务,进度报告只能从这份文件上读它 ——
92
92
  // 不过白名单就会变成"本实例的报告说得出静默多久,跨实例的报告永远说不出来"。
93
93
  lastActivityAt: j.lastActivityAt || null,
94
+ // 思考段计时(第一个思考分片 → 最后一个分片)。前端折叠态显示「想了多久」靠它,
95
+ // 运行中的任务也要能看到 —— 不过白名单就变成"本实例看得见耗时、别的实例看不见"。
96
+ thinkingStartedAt: j.thinkingStartedAt || null,
97
+ thinkingEndedAt: j.thinkingEndedAt || null,
94
98
  endedAt: j.endedAt || null,
95
99
  exitCode: typeof j.exitCode === 'number' ? j.exitCode : null,
96
100
  error: j.error || null,
@@ -87,6 +87,8 @@ const runningJob = (id, extra = {}) => ({
87
87
  prompt: 'p',
88
88
  output: 'out',
89
89
  thinking: 'think',
90
+ thinkingStartedAt: '2026-09-28T14:56:52.000Z',
91
+ thinkingEndedAt: '2026-09-28T14:56:58.500Z',
90
92
  toolCalls: [],
91
93
  claudeSessionId: null,
92
94
  // 不该被广播出去的内部字段(白名单投影必须剥掉)
@@ -112,11 +114,20 @@ test('projectJob:白名单投影剥掉 child 等不可序列化字段,保留前
112
114
  assert.equal(rec.agent, 'claude');
113
115
  assert.equal(rec.claudeSessionId, null);
114
116
  assert.deepEqual(rec.toolCalls, []);
117
+ // 思考段计时必须过白名单:漏了它 = 本实例看得见「想了多久」、别的实例永远看不见
118
+ assert.equal(rec.thinkingStartedAt, '2026-09-28T14:56:52.000Z');
119
+ assert.equal(rec.thinkingEndedAt, '2026-09-28T14:56:58.500Z');
115
120
  assert.ok(!('child' in rec), 'child 必须先剥掉,否则 JSON.stringify 直接抛');
116
121
  // 投影结果要能真的序列化
117
122
  assert.doesNotThrow(() => JSON.stringify(rec));
118
123
  });
119
124
 
125
+ test('projectJob:老 job 没有计时字段时归成 null(不是 undefined,前端不用判两种空)', () => {
126
+ const rec = projectJob(runningJob('j-old', { thinkingStartedAt: undefined, thinkingEndedAt: undefined }));
127
+ assert.equal(rec.thinkingStartedAt, null);
128
+ assert.equal(rec.thinkingEndedAt, null);
129
+ });
130
+
120
131
  test('toLiveRecord:正文/思考截尾,工具流水只留最近若干条', () => {
121
132
  const toolCalls = Array.from({ length: LIVE_TOOL_CALL_LIMIT + 5 }, (_, i) => ({
122
133
  id: `c${i}`,
@@ -577,6 +577,40 @@ export function createToolCallTracker(job, onUpdates) {
577
577
  return { record, flush, seal };
578
578
  }
579
579
 
580
+ /**
581
+ * 思考文本的保底上限(尾部截断:模型偶尔会长时间闷头思考,尾巴才是最新的)。
582
+ * 与工具流水的截断同一口径,只是这里量级更大。
583
+ */
584
+ export const MAX_THINKING = 100 * 1024 * 1024;
585
+
586
+ /**
587
+ * 追加一段思考文本:截尾 + 记「思考段计时」。
588
+ *
589
+ * 计时口径与 zen-ai-chat-ui 的 ThinkingBlock 一致:**第一个分片记起点、之后每个
590
+ * 分片把终点往前推**(不是"思考状态转 done 为止"——那样会把"想完了但正文还没开始"
591
+ * 的空档也算进去)。这两个时间戳随 job 落盘(jobs.json)、随 live-jobs 跨实例可见,
592
+ * 前端折叠着也能显示「想了多久」;只在内存里算的话,刷新页面就没了。
593
+ *
594
+ * 导出仅为回归测试可见(taskRunner.thinking.test.js):跑整条 runSingleSubtask
595
+ * 去断言这两个字段,代价是起一个假 CLI 进程,不划算。
596
+ *
597
+ * @param {object} job 执行记录(thinking / lastActivityAt 与两个计时字段都记在它身上)
598
+ * @param {string} text 本批新增的思考文本
599
+ * @param {string} [at] 时间戳(ISO);默认取当前时刻,测试可注入
600
+ * @returns {string} 本次真正追加进 job.thinking 的增量(供批量推送用)
601
+ */
602
+ export function appendThinkingToJob(job, text, at = nowIso()) {
603
+ if (!text) return '';
604
+ job.lastActivityAt = at;
605
+ if (!job.thinkingStartedAt) job.thinkingStartedAt = at;
606
+ job.thinkingEndedAt = at;
607
+ const prev = job.thinking || '';
608
+ job.thinking = (prev + text).slice(-MAX_THINKING);
609
+ // 顶到上限之后这里返回 ''(增量不再往前端推)—— 100MB 的思考文本没有真实场景,
610
+ // 真到那一步终态快照仍带全文尾巴,不值得为它把增量算法复杂化。
611
+ return job.thinking.slice(prev.length);
612
+ }
613
+
580
614
  /**
581
615
  * claude stream-json 事件处理器。
582
616
  * system/init → session_id(--resume 续接用)
@@ -961,7 +995,12 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
961
995
  claudeSessionId: resumeSessionId,
962
996
  // 工具调用流水({id,name,argsPreview,arguments,result,status,error}[])。
963
997
  // 先给个空数组,前端拿到的 job:update 就有了稳定形态,不用到处判 undefined。
964
- toolCalls: []
998
+ toolCalls: [],
999
+ // 思考段计时(第一个思考分片 → 最后一个分片,ISO 字符串)。
1000
+ // 显式给 null 而不是不写:快照 / 落盘 / 跨实例的口径要一致,前端也不用判 undefined。
1001
+ // 记法与语义见 appendThinkingToJob。
1002
+ thinkingStartedAt: null,
1003
+ thinkingEndedAt: null
965
1004
  };
966
1005
  jobs.set(jobId, job);
967
1006
  publish('job:update', job);
@@ -998,7 +1037,7 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
998
1037
  // 工具调用 → job.toolCalls(前端工具块 + 落盘归档)
999
1038
  // 其他事件 → 忽略,避免噪声
1000
1039
  const MAX_OUTPUT = 100 * 1024 * 1024;
1001
- const MAX_THINKING = 100 * 1024 * 1024;
1040
+ // 思考的截断上限是模块级的 MAX_THINKING(appendThinkingToJob 要用)
1002
1041
  job.output = '';
1003
1042
  job.thinking = '';
1004
1043
  const lineBuf = { stdout: '', stderr: '' };
@@ -1022,11 +1061,9 @@ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
1022
1061
  }
1023
1062
  };
1024
1063
  const appendThinking = (text) => {
1025
- if (!text) return;
1026
- job.lastActivityAt = nowIso();
1027
- const prevLen = job.thinking.length;
1028
- job.thinking = (job.thinking + text).slice(-MAX_THINKING);
1029
- thinkingBatch += job.thinking.slice(prevLen);
1064
+ // 追加 / 截断 / 思考段计时都在 appendThinkingToJob 里(导出给回归测试的那份),
1065
+ // 这里只负责批量推送:一批 NDJSON 处理完统一 flush 一次
1066
+ thinkingBatch += appendThinkingToJob(job, text);
1030
1067
  };
1031
1068
 
1032
1069
  // 工具调用:写进 job.toolCalls(随 job 落盘),并按批推 job:toolcalls 给前端。
@@ -0,0 +1,115 @@
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
+ // 思考段计时的回归测试(2026-10-08)。
16
+ //
17
+ // 背景:任务执行弹窗里的思考块以前只有「思考」两个字,折叠着看不出它想了多久。
18
+ // zen-ai-chat-ui 的 ThinkingBlock 支持在标题右侧显示「第一个思考分片 → 最后一个
19
+ // 分片」的耗时(折叠也看得见),但时间戳得由宿主喂 —— 也就是这里测的
20
+ // appendThinkingToJob。口径拧了「单测全绿而数字是错的」(比如把起点记成 job 开始、
21
+ // 或每次追加都重置起点),所以把口径钉死在单测里。
22
+ //
23
+ // 钉住的口径:
24
+ // 1. 第一个分片记起点,之后每个分片把终点往前推(**不是**"到第一个正文为止")
25
+ // 2. 起点一旦记下就不再移动(中途隔着工具调用再想一段,预览窗口跟着往后推)
26
+ // 3. 空分片不产生任何副作用(连 lastActivityAt 都不碰)
27
+ // 4. 老 job(没有这两个字段)第一次追加也补得上
28
+ // 5. 返回值 = 本次真正追加的增量(批量推送 job:thinking-delta 用它)
29
+ //
30
+ // 隔离策略同 taskRunner.toolcalls.test.js:import taskRunner.js 之前把
31
+ // USERPROFILE/HOME 指向 mkdtemp 沙箱(taskRunner → jobStore import 时就会 hydrate)。
32
+
33
+ import { test } from 'node:test';
34
+ import assert from 'node:assert/strict';
35
+ import path from 'node:path';
36
+ import os from 'node:os';
37
+ import { mkdtempSync } from 'node:fs';
38
+ import { fileURLToPath, pathToFileURL } from 'node:url';
39
+
40
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
41
+ const projectRoot = path.resolve(__dirname, '../../../../..');
42
+
43
+ // ---- 环境隔离:必须在 import taskRunner.js 之前完成 ----
44
+ const sandbox = mkdtempSync(path.join(os.tmpdir(), 'zen-thinking-test-'));
45
+ process.env.USERPROFILE = sandbox;
46
+ process.env.HOME = sandbox;
47
+ delete process.env.HOMEDRIVE;
48
+ delete process.env.HOMEPATH;
49
+
50
+ const { appendThinkingToJob } = await import(
51
+ pathToFileURL(path.join(projectRoot, 'src/ui/server/routes/workbench/taskRunner.js')).href
52
+ );
53
+
54
+ /** 一个刚创建、还没产出任何思考的 job(字段形状照 runSingleSubtask 里那个字面量) */
55
+ const makeJob = (extra = {}) => ({
56
+ id: 'j1',
57
+ thinking: '',
58
+ lastActivityAt: null,
59
+ thinkingStartedAt: null,
60
+ thinkingEndedAt: null,
61
+ ...extra,
62
+ });
63
+
64
+ test('第一个分片记起点,之后每个分片把终点往前推', () => {
65
+ const job = makeJob();
66
+ const t1 = '2026-10-08T01:00:00.000Z';
67
+ const t2 = '2026-10-08T01:00:03.000Z';
68
+ const t3 = '2026-10-08T01:00:09.000Z';
69
+
70
+ appendThinkingToJob(job, '先看看目录', t1);
71
+ assert.equal(job.thinkingStartedAt, t1, '第一个分片就是起点');
72
+ assert.equal(job.thinkingEndedAt, t1, '只有一个分片时终点与起点相同');
73
+
74
+ appendThinkingToJob(job, ',再读文件', t2);
75
+ assert.equal(job.thinkingStartedAt, t1, '起点不随后续分片移动');
76
+ assert.equal(job.thinkingEndedAt, t2, '每来一个分片,终点往前推');
77
+
78
+ // 中间隔着工具调用再想一段 —— 窗口跟着推到最后一段(与 ThinkingBlock 的
79
+ // 「第一个思考分片 → 最后一个分片」同口径,不把中间干活的时间算进去)
80
+ appendThinkingToJob(job, ',最后收个尾', t3);
81
+ assert.equal(job.thinkingStartedAt, t1);
82
+ assert.equal(job.thinkingEndedAt, t3);
83
+ assert.equal(job.thinking, '先看看目录,再读文件,最后收个尾');
84
+ });
85
+
86
+ test('每次追加都把 lastActivityAt 记到同一时刻(静默看门狗的记账口径)', () => {
87
+ const job = makeJob();
88
+ appendThinkingToJob(job, '想', '2026-10-08T02:00:00.000Z');
89
+ assert.equal(job.lastActivityAt, '2026-10-08T02:00:00.000Z');
90
+ });
91
+
92
+ test('返回值 = 本次真正追加的增量(供批量推送 job:thinking-delta)', () => {
93
+ const job = makeJob();
94
+ assert.equal(appendThinkingToJob(job, '第一段', '2026-10-08T02:10:00.000Z'), '第一段');
95
+ assert.equal(appendThinkingToJob(job, '第二段', '2026-10-08T02:10:01.000Z'), '第二段');
96
+ assert.equal(job.thinking, '第一段第二段');
97
+ });
98
+
99
+ test('空分片:整条记录一个字都不动(连 lastActivityAt 都不碰)', () => {
100
+ const job = makeJob({ lastActivityAt: '2026-10-08T03:00:00.000Z' });
101
+ assert.equal(appendThinkingToJob(job, ''), '');
102
+ assert.equal(appendThinkingToJob(job, undefined), '');
103
+ assert.equal(job.thinking, '');
104
+ assert.equal(job.thinkingStartedAt, null, '空分片不该开出计时窗口');
105
+ assert.equal(job.thinkingEndedAt, null);
106
+ assert.equal(job.lastActivityAt, '2026-10-08T03:00:00.000Z', 'lastActivityAt 保持原值');
107
+ });
108
+
109
+ test('老 job(字段都不存在)第一次追加也补得上计时', () => {
110
+ const job = { id: 'old', thinking: '旧内容' };
111
+ appendThinkingToJob(job, '新的', '2026-10-08T04:00:00.000Z');
112
+ assert.equal(job.thinkingStartedAt, '2026-10-08T04:00:00.000Z');
113
+ assert.equal(job.thinkingEndedAt, '2026-10-08T04:00:00.000Z');
114
+ assert.equal(job.thinking, '旧内容新的');
115
+ });
@@ -33,6 +33,10 @@
33
33
  // 要**明确报错**(提示换模型),而不是把 OpenAI body 打到 Anthropic 路由上等一个
34
34
  // 看不懂的 4xx。
35
35
  //
36
+ // 目前有两家网关是这个形态:OpenCode Go(三族)与 Command Code(两族,
37
+ // claude-* 只能走 /messages)。两者的客户端身份要求不同——Command Code 只认
38
+ // Bearer,不需要额外的会话头。
39
+ //
36
40
  // 参考实现:ai-model-form 包的 server/middleware.js(同一套判断,那边只做连通性测试)。
37
41
  // ─────────────────────────────────────────────────────────────
38
42
 
@@ -55,6 +59,9 @@ export const AI_USER_AGENT = `zen-gitsync/${PKG_VERSION}`
55
59
  /** OpenCode 网关的 Go 订阅入口(ai-model-form 里叫 OPENCODE_GO_URL) */
56
60
  export const OPENCODE_GO_URL = 'https://opencode.ai/zen/go/v1'
57
61
 
62
+ /** Command Code 的 Provider API 入口(ai-model-form 里同名常量) */
63
+ export const COMMAND_CODE_URL = 'https://api.commandcode.ai/provider/v1'
64
+
58
65
  /**
59
66
  * OpenCode Go 上走 Anthropic /messages 协议的模型(鉴权是 x-api-key,不是 Bearer)。
60
67
  * 只列**非 chat** 的模型:没列到的一律按 chat 处理,这样网关以后新增 chat 模型
@@ -108,6 +115,38 @@ export function openCodeGoFamily(modelId) {
108
115
  return 'chat'
109
116
  }
110
117
 
118
+ /**
119
+ * 是否为 Command Code 网关(commandcode.ai 及其子域)。
120
+ * 与 isOpenCodeGateway 同样只看主机名后缀,防止 evil-commandcode.ai.attacker.com
121
+ * 这类伪装域名被当成官方入口。
122
+ * @param {string} baseURL
123
+ * @returns {boolean}
124
+ */
125
+ export function isCommandCodeGateway(baseURL) {
126
+ try {
127
+ const host = new URL(normalizeBaseURL(baseURL)).hostname
128
+ return /(^|\.)commandcode\.ai$/i.test(host)
129
+ } catch {
130
+ return false
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 判断模型在 Command Code 上走哪种协议族。
136
+ *
137
+ * 这里按 `claude-*` 前缀判断而不是维护一张模型清单:官方 /models 的
138
+ * supported_endpoints 字段里「不支持 /chat/completions」的恰好就是全部 claude-*
139
+ * (2026-10-08 核对:11 个,两边完全吻合)。网关侧对 Claude 系是硬约束——
140
+ * 打到 /chat/completions 会直接回 400「must be called via /provider/v1/messages」。
141
+ *
142
+ * @param {string} modelId
143
+ * @returns {'chat'|'messages'}
144
+ */
145
+ export function commandCodeFamily(modelId) {
146
+ const id = String(modelId || '').trim()
147
+ return /^claude-/i.test(id) ? 'messages' : 'chat'
148
+ }
149
+
111
150
  let _processSessionId = null
112
151
 
113
152
  /**
@@ -170,6 +209,15 @@ export function buildAiChatRequest({ baseURL, model, apiKey, sessionId, extraHea
170
209
  }
171
210
  }
172
211
 
212
+ if (isCommandCodeGateway(base) && commandCodeFamily(model) !== 'chat') {
213
+ throw new Error(
214
+ `模型「${model}」在 Command Code 网关上只能走 Anthropic 的 /messages 协议,` +
215
+ '本工具的 AI 调用只实现了 OpenAI 兼容的 /chat/completions。请在设置里换一个' +
216
+ '走 chat/completions 的模型(如 moonshotai/Kimi-K3、zai-org/GLM-5.3、' +
217
+ 'Qwen/Qwen3.8-Max、deepseek/deepseek-v4-flash)。'
218
+ )
219
+ }
220
+
173
221
  return {
174
222
  url: `${base}/chat/completions`,
175
223
  headers: {
@@ -18,15 +18,19 @@
18
18
  // 所以用测试把三件事钉死:
19
19
  // 1) 只对 OpenCode 网关加 x-opencode-session,别的 provider 不加(避免污染第三方请求)
20
20
  // 2) 请求头里必须有自己的客户端名字(User-Agent 不能是 node-fetch/undici)
21
- // 3) OpenCode 网关上非 chat 协议族的模型要**明确报错**,不能把 OpenAI body 打错路由
21
+ // 3) OpenCode / Command Code 网关上非 chat 协议族的模型要**明确报错**,不能把 OpenAI
22
+ // body 打错路由(两家网关的协议族识别与伪装域名都要各自钉死)
22
23
  import { test } from 'node:test'
23
24
  import assert from 'node:assert/strict'
24
25
  import {
25
26
  AI_USER_AGENT,
27
+ COMMAND_CODE_URL,
26
28
  OPENCODE_GO_URL,
27
29
  buildAiApiRequest,
28
30
  buildAiChatRequest,
31
+ commandCodeFamily,
29
32
  describeAiHttpError,
33
+ isCommandCodeGateway,
30
34
  isOpenCodeGateway,
31
35
  normalizeBaseURL,
32
36
  openCodeGoFamily,
@@ -34,6 +38,7 @@ import {
34
38
  } from './aiEndpoint.js'
35
39
 
36
40
  const GO = 'https://opencode.ai/zen/go/v1'
41
+ const CC = 'https://api.commandcode.ai/provider/v1'
37
42
 
38
43
  // ========== normalizeBaseURL ==========
39
44
 
@@ -73,6 +78,32 @@ test('openCodeGoFamily: 三种协议族与默认 chat', () => {
73
78
  assert.equal(openCodeGoFamily(''), 'chat')
74
79
  })
75
80
 
81
+ // ========== isCommandCodeGateway ==========
82
+
83
+ test('isCommandCodeGateway: 识别 commandcode.ai 及其子域', () => {
84
+ assert.equal(isCommandCodeGateway(CC), true)
85
+ assert.equal(isCommandCodeGateway('https://api.commandcode.ai/provider/v1/'), true)
86
+ assert.equal(isCommandCodeGateway('https://commandcode.ai/api'), true)
87
+ assert.equal(isCommandCodeGateway('https://api.deepseek.com/v1'), false)
88
+ // 伪装域名不能被当成官方入口
89
+ assert.equal(isCommandCodeGateway('https://evil-commandcode.ai.attacker.com/v1'), false)
90
+ assert.equal(isCommandCodeGateway('not-a-url'), false)
91
+ assert.equal(isCommandCodeGateway(''), false)
92
+ })
93
+
94
+ // ========== commandCodeFamily ==========
95
+
96
+ test('commandCodeFamily: claude-* 走 messages,其余默认 chat', () => {
97
+ assert.equal(commandCodeFamily('claude-sonnet-5'), 'messages')
98
+ assert.equal(commandCodeFamily('claude-opus-5-5'), 'messages')
99
+ // 大小写/空格容错
100
+ assert.equal(commandCodeFamily(' Claude-Haiku-5-5 '), 'messages')
101
+ assert.equal(commandCodeFamily('moonshotai/Kimi-K3'), 'chat')
102
+ assert.equal(commandCodeFamily('deepseek/deepseek-v4-flash'), 'chat')
103
+ assert.equal(commandCodeFamily('inclusionai/ling-3.1-flash:free'), 'chat')
104
+ assert.equal(commandCodeFamily(''), 'chat')
105
+ })
106
+
76
107
  // ========== buildAiChatRequest ==========
77
108
 
78
109
  test('buildAiChatRequest: 普通 provider 拼 url + Bearer,不带 opencode 会话头', () => {
@@ -129,6 +160,25 @@ test('buildAiChatRequest: OpenCode Go 上非 chat 协议族的模型直接给可
129
160
  )
130
161
  })
131
162
 
163
+ test('buildAiChatRequest: Command Code + chat 模型拼 url,且不该带 opencode 会话头', () => {
164
+ const { url, headers } = buildAiChatRequest({
165
+ baseURL: CC,
166
+ model: 'moonshotai/Kimi-K3',
167
+ apiKey: 'user_abc',
168
+ })
169
+ assert.equal(url, `${COMMAND_CODE_URL}/chat/completions`)
170
+ assert.equal(headers['Authorization'], 'Bearer user_abc')
171
+ // Command Code 只认 Bearer,别误判成 OpenCode 网关往上加会话头
172
+ assert.ok(!('x-opencode-session' in headers), 'Command Code 不需要 x-opencode-session')
173
+ })
174
+
175
+ test('buildAiChatRequest: Command Code 上 claude-* 只能走 /messages,直接给可读报错', () => {
176
+ assert.throws(
177
+ () => buildAiChatRequest({ baseURL: CC, model: 'claude-sonnet-5', apiKey: 'k' }),
178
+ /claude-sonnet-5.*Anthropic.*\/messages.*换一个/
179
+ )
180
+ })
181
+
132
182
  test('buildAiChatRequest: 无 apiKey 不发 Authorization,extraHeaders 可覆盖默认头', () => {
133
183
  const { headers } = buildAiChatRequest({
134
184
  baseURL: 'https://api.deepseek.com/v1',
@@ -1 +0,0 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{C as e,Cn as t,Dn as n,En as r,Gn as i,Gr as a,Hn as o,J as s,Mn as c,Mr as l,Pn as u,Rn as d,S as f,Tn as p,Xr as m,Y as h,Yn as g,Zn as _,dr as v,or as y,qn as b,tt as x,vr as S,w as C,wn as w,wr as T,yn as E}from"./element-plus-CZdvD4lq.js";import{n as D,t as O}from"./_plugin-vue_export-helper-UTW98kcD.js";import{t as k}from"./SvgIcon-CaefOO1F.js";import{T as A,m as j,z as M}from"./index-CD7otmb_.js";import{c as N,d as P,l as F,u as I}from"./agentConversations-B9LNft3v.js";d(),v();function L(e,n=680){let r=S(0),a=null;function s(){let t=e.value;return t?(r.value=t.clientWidth,typeof ResizeObserver>`u`||(a=new ResizeObserver(e=>{let t=e[0]?.contentRect?.width;typeof t==`number`&&(r.value=t)}),a.observe(t),!0)):!1}return b(()=>{s()||o(()=>s())}),i(()=>{a?.disconnect(),a=null}),{width:r,narrow:t(()=>r.value>0&&r.value<n)}}d(),v(),l();var R={class:`agent-engine`},z=[`disabled`,`title`,`aria-label`],B={class:`agent-engine__name`},V={class:`agent-engine__item`},H={class:`agent-engine__item-name`},U={key:3,class:`agent-engine__item-missing`},W=O(u({__name:`AgentEngineSelector`,props:{engine:{},locked:{type:Boolean}},emits:[`select`],setup(i,{emit:o}){let l=i,u=o,d=M(),v=t(()=>I(l.engine)),b=t(()=>({gai:!0,claude:d.claudeAvailable,opencode:d.opencodeAvailable,codex:d.codexAvailable})),O=S(!1),L=S(null);function W(e){if(!l.locked){if(!b.value[e]){if(!P(e))return;L.value=e,O.value=!0;return}u(`select`,e)}}return(t,o)=>(g(),n(`div`,R,[c(T(f),{trigger:`click`,onCommand:W},{dropdown:y(()=>[c(T(C),null,{default:y(()=>[(g(!0),n(E,null,_(T(N),t=>(g(),p(T(e),{key:t.id,command:t.id},{default:y(()=>[w(`span`,V,[T(P)(t.id)?(g(),p(j,{key:0,executor:t.id,class:`agent-engine__item-icon`},null,8,[`executor`])):(g(),p(k,{key:1,"icon-class":T(F),class:`agent-engine__item-icon`},null,8,[`icon-class`])),w(`span`,H,m(t.name),1),i.engine===t.id?(g(),p(T(s),{key:2,class:`agent-engine__item-check`},{default:y(()=>[c(T(x))]),_:1})):b.value[t.id]?r(``,!0):(g(),n(`span`,U,m(T(D)(`@42BB9:未安装`)),1))])]),_:2},1032,[`command`]))),128))]),_:1})]),default:y(()=>[w(`button`,{type:`button`,class:a([`agent-engine__btn`,{"is-locked":i.locked}]),disabled:i.locked,title:i.locked?T(D)(`@AGENT:本会话的引擎已锁定;要换引擎请新建会话`):T(D)(`@AGENT:新建会话使用的引擎`),"aria-label":T(D)(`@AGENT:智能体引擎`)},[T(P)(i.engine)?(g(),p(j,{key:0,executor:i.engine,class:`agent-engine__icon`},null,8,[`executor`])):(g(),p(k,{key:1,"icon-class":T(F),class:`agent-engine__icon`},null,8,[`icon-class`])),w(`span`,B,m(v.value),1),c(T(s),{class:`agent-engine__caret`},{default:y(()=>[c(T(h))]),_:1})],10,z)]),_:1}),c(A,{modelValue:O.value,"onUpdate:modelValue":o[0]||=e=>O.value=e,tool:L.value},null,8,[`modelValue`,`tool`])]))}}),[[`__scopeId`,`data-v-cc0a8973`]]);export{L as n,W as t};
@@ -1 +0,0 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{At as e,Cn as t,Ct as n,Dn as r,En as i,Gr as a,Hn as o,J as ee,Jt as te,M as s,Mn as c,Mr as l,Pn as u,Rn as d,Tn as f,Tt as p,Vt as ne,W as re,Wt as m,Xr as h,Yn as g,Zn as _,a as v,dr as y,er as b,et as x,gr as S,hn as C,i as w,ir as T,j as ie,jn as E,kt as ae,ln as D,lt as O,mt as k,or as A,qr as j,r as M,rn as oe,sr as N,st as P,tt as se,vr as F,wn as I,wr as L,yn as R}from"./element-plus-CZdvD4lq.js";import{a as z,l as B}from"./vendor-BD09UDuP.js";import{n as V,t as H}from"./_plugin-vue_export-helper-UTW98kcD.js";import{t as ce}from"./configStore-H-n3ukZ_.js";import{t as le}from"./CommonDialog-2zVHBuqT.js";import{a as U,i as ue,n as de,o as W,r as G,s as K,t as q}from"./agentConversations-B9LNft3v.js";import{n as J,t as fe}from"./AgentEngineSelector-CKiVaqrY.js";d(),y(),D(),l();var Y={class:`marketplace-panel`},pe={class:`mp-toolbar`},me={class:`mp-search`},he=[`placeholder`],ge={class:`mp-toolbar-sub`},_e={class:`mp-target-hint`},ve={class:`mp-source-filter`},X=[`onClick`],ye={class:`mp-body`},be={class:`mp-group-header`},xe={class:`mp-group-title`},Se={key:0,class:`mp-group-count`},Ce={key:1,class:`mp-group-error`},we=[`href`,`title`],Te={key:0,class:`mp-grid`},Ee={class:`mp-card-head`},Z=[`title`],De={key:0,class:`mp-card-metric`},Oe=[`title`],Q={class:`mp-card-meta`},$=[`title`],ke=[`title`],Ae={key:2,class:`mp-tag remote`},je={class:`mp-card-foot`},Me=[`href`],Ne={key:1,class:`mp-installed-chip`},Pe=[`disabled`,`onClick`],Fe={key:3,class:`mp-not-installable`},Ie={key:1,class:`mp-empty`},Le={key:0,class:`mp-group installed`},Re={class:`mp-group-header`},ze={class:`mp-group-title`},Be={class:`mp-group-count`},Ve={key:0,class:`mp-installed-block`},He={class:`mp-installed-target`},Ue={class:`mp-installed-list`},We=[`title`],Ge=[`title`],Ke={key:1,class:`mp-meta-pkg`},qe={key:2,class:`mp-tag warn`},Je=[`disabled`,`title`,`onClick`],Ye=[`disabled`,`onClick`],Xe={key:1,class:`mp-empty all`},Ze={key:0,class:`mkt-form`},Qe={class:`mkt-form-name`},$e=[`onUpdate:modelValue`,`placeholder`],et={key:0,class:`mkt-field`},tt={class:`mkt-hint`},nt=H(u({__name:`MarketplacePanel`,props:{type:{}},setup(e){let o=e,l=ce(),u=F([]),d=F([]),p=F(!1),y=F(``),b=F(``),x=F([]),D=F(`project`),j=F(new Set),P=F(new Set),z=F(new Set),B=t(()=>l.currentDirectory||``),H=null;async function U(){if(!p.value){p.value=!0;try{let e=new URLSearchParams({type:o.type,cwd:B.value});b.value&&e.set(`q`,b.value),x.value.length&&e.set(`sources`,x.value.join(`,`));let t=await(await fetch(`/api/agent/marketplace/catalog?${e}`)).json();t?.success?(u.value=t.groups||[],d.value=t.installed||[]):w.error(t?.error||V(`@MKT:加载失败`))}catch{w.error(V(`@MKT:加载失败`))}finally{p.value=!1}}}T(y,e=>{H&&clearTimeout(H),H=setTimeout(()=>{b.value=e.trim(),U()},400)}),T(x,()=>U()),T(()=>o.type,()=>{x.value=[],b.value=``,y.value=``,U()}),T(B,()=>U()),U();let ue=t(()=>{let e=B.value?B.value.split(/[\\/]/).filter(Boolean).pop():``;return D.value===`project`?e||V(`@MKT:当前项目`):V(`@MKT:g ai 智能体`)});function de(e){return e.stars?`${W(e.stars)} ★`:e.downloads?`${W(e.downloads)} ${V(`@MKT:周下载`)}`:e.uses?`${W(e.uses)} ${V(`@MKT:次使用`)}`:``}function W(e){return e>=1e3?`${(e/1e3).toFixed(e>=1e4?0:1)}k`:String(e)}function G(e){return`${e.id}`}function K(e){return j.value.has(G(e))}let q=t(()=>{let e={project:[],global:[]};for(let t of d.value)(e[t.target]||=[]).push(t);return e}),J=S({visible:!1,item:null,env:{},args:``});function fe(e){return o.type===`mcp`&&(!!e.envKeys?.length||!!e.args?.length)}function nt(e){J.item=e,J.env=Object.fromEntries((e.envKeys||[]).map(e=>[e,``])),J.args=(e.args||[]).join(` `),J.visible=!0}function rt(){let e=J.item;if(J.visible=!1,!e)return;let t=Object.fromEntries(Object.entries(J.env).filter(([,e])=>e.trim()!==``)),n=J.args.trim()?J.args.trim().match(/(?:[^\s"]+|"[^"]*")+/g)?.map(e=>e.replace(/^"|"$/g,``))??void 0:void 0;at(e,{env:t,...n?{args:n}:{}})}async function it(e){if(!e.installed||await ot(e)){if(fe(e)){nt(e);return}await at(e,{})}}async function at(e,t){let n=G(e);j.value.add(n);try{let n=await fetch(`/api/agent/marketplace/install`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({type:o.type,target:D.value,cwd:B.value,item:{...e,...t}})}),r=await n.json().catch(()=>null);if(!n.ok||!r?.success){w.error(r?.error||V(`@MKT:安装失败`));return}r.item?.warning&&w.warning(r.item.warning),r.item?.envKeys?.length?w.warning(`${V(`@MKT:还缺环境变量`)}: ${r.item.envKeys.join(`, `)}`):w.success(V(`@MKT:安装成功`)),await U()}catch{w.error(V(`@MKT:安装失败`))}finally{j.value.delete(n)}}async function ot(e){try{return await M.confirm(`${e.name} ${V(`@MKT:已安装,要重新安装吗?`)}`,V(`@MKT:重新安装`),{confirmButtonText:V(`@MKT:重装`),cancelButtonText:V(`@MKT:取消`),type:`warning`}),!0}catch{return!1}}async function st(e){let t=`${e.target}:${e.id}`;z.value.add(t);try{let t=await fetch(`/api/agent/marketplace/reveal`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({type:o.type,target:e.target,cwd:B.value,id:e.id})}),n=await t.json().catch(()=>null);(!t.ok||!n?.success)&&w.error(n?.error||V(`@MKT:打开失败`))}catch{w.error(V(`@MKT:打开失败`))}finally{z.value.delete(t)}}async function ct(e){try{await M.confirm(`${V(`@MKT:确定卸载`)} ${e.name}?`,V(`@MKT:卸载`),{confirmButtonText:V(`@MKT:卸载`),cancelButtonText:V(`@MKT:取消`),type:`warning`})}catch{return}let t=`${e.target}:${e.id}`;P.value.add(t);try{let t=new URLSearchParams({target:e.target,cwd:B.value}),n=await fetch(`/api/agent/marketplace/item/${o.type}/${encodeURIComponent(e.id)}?${t}`,{method:`DELETE`}),r=await n.json().catch(()=>null);if(!n.ok||!r?.success){w.error(r?.error||V(`@MKT:卸载失败`));return}w.success(V(`@MKT:已卸载`)),await U()}catch{w.error(V(`@MKT:卸载失败`))}finally{P.value.delete(t)}}return(t,o)=>{let l=ee,b=ie,S=s,w=re,T=v;return g(),r(`div`,Y,[I(`div`,pe,[I(`div`,me,[c(l,{class:`mp-search-icon`},{default:A(()=>[c(L(m))]),_:1}),N(I(`input`,{"onUpdate:modelValue":o[0]||=e=>y.value=e,class:`mp-search-input`,type:`text`,placeholder:e.type===`skill`?L(V)(`@MKT:搜索 Skill...`):L(V)(`@MKT:搜索 MCP 服务...`)},null,8,he),[[C,y.value]])]),c(S,{modelValue:D.value,"onUpdate:modelValue":o[1]||=e=>D.value=e,size:`small`,class:`mp-target`},{default:A(()=>[c(b,{value:`project`},{default:A(()=>[E(h(L(V)(`@MKT:当前项目`)),1)]),_:1}),c(b,{value:`global`},{default:A(()=>[E(h(L(V)(`@MKT:g ai 智能体`)),1)]),_:1})]),_:1},8,[`modelValue`]),c(w,{content:L(V)(`@MKT:刷新`),placement:`top`,"show-after":300},{default:A(()=>[I(`button`,{class:a([`mp-refresh`,{spinning:p.value}]),onClick:U},[c(l,null,{default:A(()=>[c(L(ne))]),_:1})],2)]),_:1},8,[`content`])]),I(`div`,ge,[I(`span`,_e,[E(h(L(V)(`@MKT:安装到`))+` `,1),I(`b`,null,h(ue.value),1)]),I(`div`,ve,[(g(!0),r(R,null,_(u.value,e=>(g(),r(`button`,{key:e.id,class:a([`mp-chip`,{active:x.value.includes(e.id)}]),onClick:()=>{let t=x.value.indexOf(e.id);t>=0?x.value.splice(t,1):x.value.push(e.id)}},h(e.label),11,X))),128)),x.value.length?(g(),r(`button`,{key:0,class:`mp-chip clear`,onClick:o[2]||=e=>x.value=[]},h(L(V)(`@MKT:全部来源`)),1)):i(``,!0)])]),N((g(),r(`div`,ye,[(g(!0),r(R,null,_(u.value,e=>(g(),r(`section`,{key:e.id,class:`mp-group`},[I(`header`,be,[I(`h3`,xe,h(e.label),1),e.status===`ok`?(g(),r(`span`,Se,h(e.items.length),1)):(g(),r(`span`,Ce,[c(l,null,{default:A(()=>[c(L(oe))]),_:1}),E(` `+h(e.error||L(V)(`@MKT:来源不可用`)),1)])),e.homepage?(g(),r(`a`,{key:2,class:`mp-group-link`,href:e.homepage,target:`_blank`,rel:`noopener noreferrer`,title:e.homepage},[c(l,null,{default:A(()=>[c(L(ae))]),_:1})],8,we)):i(``,!0)]),e.status===`ok`&&e.items.length?(g(),r(`div`,Te,[(g(!0),r(R,null,_(e.items,e=>(g(),r(`article`,{key:G(e),class:`mp-card`},[I(`div`,Ee,[I(`span`,{class:`mp-card-name`,title:e.name},h(e.name),9,Z),de(e)?(g(),r(`span`,De,[c(l,null,{default:A(()=>[e.stars?(g(),f(L(te),{key:0})):(g(),f(L(k),{key:1}))]),_:2},1024),E(` `+h(de(e)),1)])):i(``,!0)]),I(`p`,{class:`mp-card-desc`,title:e.description},h(e.description||L(V)(`@MKT:暂无描述`)),9,Oe),I(`div`,Q,[e.package?(g(),r(`code`,{key:0,class:`mp-meta-pkg`,title:e.package},h(e.package),9,$)):e.repository?(g(),r(`span`,{key:1,class:`mp-meta-repo`,title:e.repository+(e.subpath?` · `+e.subpath:``)},[E(h(e.repository),1),e.subpath?(g(),r(R,{key:0},[E(` · `+h(e.subpath),1)],64)):i(``,!0)],8,ke)):i(``,!0),e.transport===`remote`?(g(),r(`span`,Ae,h(L(V)(`@MKT:远程服务`)),1)):i(``,!0),(g(!0),r(R,null,_((e.tags||[]).slice(0,2),e=>(g(),r(`span`,{key:e,class:`mp-tag`},h(e),1))),128))]),I(`div`,je,[e.upstream?(g(),r(`a`,{key:0,class:`mp-card-link`,href:e.upstream,target:`_blank`,rel:`noopener noreferrer`},h(L(V)(`@MKT:查看来源`)),9,Me)):i(``,!0),o[6]||=I(`span`,{class:`mp-foot-spacer`},null,-1),e.installed?(g(),r(`span`,Ne,[c(l,null,{default:A(()=>[c(L(se))]),_:1}),E(h(L(V)(`@MKT:已安装`)),1)])):e.installable===!1?(g(),r(`span`,Fe,h(L(V)(`@MKT:暂不支持一键安装`)),1)):(g(),r(`button`,{key:2,class:`mp-install-btn`,disabled:K(e),onClick:t=>it(e)},h(K(e)?L(V)(`@MKT:安装中...`):L(V)(`@MKT:安装`)),9,Pe))])]))),128))])):e.status===`ok`?(g(),r(`div`,Ie,h(L(V)(`@MKT:该来源暂无结果`)),1)):i(``,!0)]))),128)),d.value.length?(g(),r(`section`,Le,[I(`header`,Re,[I(`h3`,ze,h(L(V)(`@MKT:已安装`)),1),I(`span`,Be,h(d.value.length),1)]),(g(),r(R,null,_([`project`,`global`],e=>I(`div`,{key:e},[q.value[e]?.length?(g(),r(`div`,Ve,[I(`div`,He,h(e===`project`?L(V)(`@MKT:当前项目`):L(V)(`@MKT:g ai 智能体`)),1),I(`div`,Ue,[(g(!0),r(R,null,_(q.value[e],t=>(g(),r(`div`,{key:e+t.id,class:`mp-installed-row`},[I(`span`,{class:`mp-installed-name`,title:t.name},h(t.name),9,We),t.id&&t.id!==t.name?(g(),r(`code`,{key:0,class:`mp-installed-id`,title:t.dir||t.file||t.id},h(t.id),9,Ge)):i(``,!0),t.package?(g(),r(`code`,Ke,h(t.package),1)):i(``,!0),t.requiredEnv?.length?(g(),r(`span`,qe,h(L(V)(`@MKT:待配置`))+`: `+h(t.requiredEnv.join(`, `)),1)):i(``,!0),o[7]||=I(`span`,{class:`mp-foot-spacer`},null,-1),I(`button`,{class:`mp-reveal-btn`,disabled:z.value.has(e+t.id),title:t.dir||t.file||``,onClick:e=>st(t)},[c(l,null,{default:A(()=>[c(L(n))]),_:1}),E(` `+h(L(V)(`@MKT:打开文件夹`)),1)],8,Je),I(`button`,{class:`mp-remove-btn`,disabled:P.value.has(e+t.id),onClick:e=>ct(t)},[c(l,null,{default:A(()=>[c(L(O))]),_:1}),E(` `+h(P.value.has(e+t.id)?L(V)(`@MKT:卸载中...`):L(V)(`@MKT:卸载`)),1)],8,Ye)]))),128))])])):i(``,!0)])),64))])):i(``,!0),!p.value&&!u.value.length?(g(),r(`div`,Xe,h(L(V)(`@MKT:加载失败`)),1)):i(``,!0)])),[[T,p.value]]),c(le,{modelValue:J.visible,"onUpdate:modelValue":o[5]||=e=>J.visible=e,title:L(V)(`@MKT:配置 MCP`),width:`460px`,type:`flex`,"close-on-click-modal":!1,"append-to-body":``},{footer:A(()=>[I(`button`,{class:`mkt-cancel`,onClick:o[4]||=e=>J.visible=!1},h(L(V)(`@MKT:取消`)),1),I(`button`,{class:`mkt-confirm`,onClick:rt},h(L(V)(`@MKT:安装`)),1)]),default:A(()=>[J.item?(g(),r(`div`,Ze,[I(`p`,Qe,h(J.item.name),1),(g(!0),r(R,null,_(Object.keys(J.env),e=>(g(),r(`div`,{key:e,class:`mkt-field`},[I(`label`,null,h(e),1),N(I(`input`,{"onUpdate:modelValue":t=>J.env[e]=t,type:`text`,autocomplete:`off`,placeholder:L(V)(`@MKT:环境变量值`),spellcheck:`false`},null,8,$e),[[C,J.env[e]]])]))),128)),J.item.args?.length?(g(),r(`div`,et,[I(`label`,null,h(L(V)(`@MKT:启动参数`)),1),N(I(`input`,{"onUpdate:modelValue":o[3]||=e=>J.args=e,type:`text`,autocomplete:`off`,spellcheck:`false`},null,512),[[C,J.args]]),I(`p`,tt,h(L(V)(`@MKT:参数提示`)),1)])):i(``,!0)])):i(``,!0)]),_:1},8,[`modelValue`,`title`])])}}}),[[`__scopeId`,`data-v-536d7952`]]);d(),y(),l();var rt={class:`agent-chat-area`},it={class:`agent-tabs`,role:`tablist`},at=[`aria-selected`,`onClick`],ot={class:`agent-tab-body`},st={key:0,class:`agent-chat-pane`},ct={key:0,class:`agent-list-page`},lt={key:0,class:`agent-page-bar`},ut=[`title`,`aria-label`],dt={class:`agent-page-title`},ft={key:0,class:`chat-loading`},pt=H(u({__name:`AgentView`,setup(n){let te=ce(),s=F(null),{narrow:l}=J(s),u=F(`chat`),d=F(`chat`),ne=t(()=>d.value===`mcp`?`mcp`:`skill`),re=t(()=>[{id:`chat`,label:V(`@AGENT:对话`),icon:x},{id:`skill`,label:V(`@AGENT:Skill 广场`),icon:p},{id:`mcp`,label:V(`@AGENT:MCP 广场`),icon:P}]),{sessions:m,sessionsLoading:v,currentSessionId:y,messages:S,isStreaming:C,sessionLoading:w,loadSessions:ie,loadSession:E,deleteSession:ae,renameSession:D,newSession:O,sendMessage:k,pendingQuestion:M,answeringQuestion:oe,answerQuestion:N,isSessionGenerating:se,currentEngine:H,pendingEngine:le,isEngineLocked:Y,pickEngine:pe,stop:me,inputContextUsage:he}=K(),ge=t(()=>Y.value?H.value:le.value);function _e(e){Y.value||pe(e)}let ve=t(()=>{let e=te.theme;return e===`dark`||e===`auto`&&window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}),X=t(()=>U(m.value,se)),ye=G(),be=t(()=>X.value.find(e=>e.id===y.value)?.title||V(`@AGENT:对话`)),xe={"--welcome-avatar":`url("${q}")`},Se=t(()=>[{id:`p1`,label:V(`@AGENT:查看项目结构`),prompt:V(`@AGENT:prompt_p1`)},{id:`p2`,label:V(`@AGENT:分析代码质量`),prompt:V(`@AGENT:prompt_p2`)},{id:`p3`,label:V(`@AGENT:帮我写测试`),prompt:V(`@AGENT:prompt_p3`)},{id:`p4`,label:V(`@AGENT:Git 状态检查`),prompt:V(`@AGENT:prompt_p4`)},{id:`p5`,label:V(`@AGENT:帮我启动项目`),prompt:V(`@AGENT:prompt_p5`)}]);async function Ce(e){await k(e.text,e.files),await o(),Z()}async function we(e){await k(e.prompt),await o(),Z()}let Te=ue(),Ee=F(null);function Z(e=!0){Ee.value?.scrollToBottom(e)}T(()=>S.value.length,()=>{o(()=>Z(!1))});function De(e){E(e),u.value=`chat`}function Oe(){O(),u.value=`chat`}let Q=F(280),$=F(!1);function ke(e){$.value=!0;let t=e.clientX,n=Q.value,r=e=>{let r=e.clientX-t,i=Math.max(200,Math.min(500,n+r));Q.value=i},i=()=>{$.value=!1,document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)}return T(l,e=>{e||(u.value=`chat`)}),T(()=>[te.currentDirectory,C.value],async([e,t],n)=>{let r=!n||e!==n[0],i=!!n&&n[1]===!0&&!t;(r||i)&&await ie(),y.value&&!C.value&&!m.value.some(e=>e.sessionId===y.value)&&O()},{immediate:!0}),(t,n)=>(g(),r(`div`,{ref_key:`rootRef`,ref:s,class:`agent-view`},[d.value===`chat`&&!L(l)?(g(),r(R,{key:0},[I(`aside`,{class:`agent-sidebar`,style:j({width:Q.value+`px`})},[c(L(B),{class:`agent-conversations`,items:X.value,"active-id":L(y),loading:L(v),labels:L(ye),onSelect:De,onNew:Oe,onRename:L(D),onDelete:L(ae)},null,8,[`items`,`active-id`,`loading`,`labels`,`onRename`,`onDelete`])],4),I(`div`,{class:a([`sidebar-resizer`,{active:$.value}]),onMousedown:ke},null,34)],64)):i(``,!0),I(`main`,rt,[I(`nav`,it,[(g(!0),r(R,null,_(re.value,e=>(g(),r(`button`,{key:e.id,type:`button`,class:a([`agent-tab`,{active:d.value===e.id}]),role:`tab`,"aria-selected":d.value===e.id,onClick:t=>d.value=e.id},[c(L(ee),null,{default:A(()=>[(g(),f(b(e.icon)))]),_:2},1024),I(`span`,null,h(e.label),1)],10,at))),128)),d.value===`chat`?(g(),f(fe,{key:0,class:`agent-engine`,engine:ge.value,locked:L(Y),onSelect:_e},null,8,[`engine`,`locked`])):i(``,!0)]),I(`div`,ot,[d.value===`chat`?(g(),r(`div`,st,[L(l)&&u.value===`list`?(g(),r(`div`,ct,[c(L(B),{class:`agent-conversations`,items:X.value,"active-id":L(y),loading:L(v),labels:L(ye),onSelect:De,onNew:Oe,onRename:L(D),onDelete:L(ae)},null,8,[`items`,`active-id`,`loading`,`labels`,`onRename`,`onDelete`])])):(g(),r(R,{key:1},[L(l)?(g(),r(`div`,lt,[I(`button`,{type:`button`,class:`agent-page-back`,title:L(V)(`@AGENT:返回会话列表`),"aria-label":L(V)(`@AGENT:返回会话列表`),onClick:n[0]||=e=>u.value=`list`},[...n[1]||=[I(`svg`,{viewBox:`0 0 24 24`,width:`14`,height:`14`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"stroke-linecap":`round`,"stroke-linejoin":`round`},[I(`line`,{x1:`19`,y1:`12`,x2:`5`,y2:`12`}),I(`polyline`,{points:`12 19 5 12 12 5`})],-1)]],8,ut),I(`span`,dt,h(be.value),1)])):i(``,!0),I(`div`,{class:`agent-chat-host`,style:xe},[L(w)?(g(),r(`div`,ft,[c(L(ee),{class:`is-loading`,size:32},{default:A(()=>[c(L(e))]),_:1}),I(`span`,null,h(L(V)(`@AGENT:加载会话中...`)),1)])):(g(),f(L(z),{key:1,ref_key:`chatContainerRef`,ref:Ee,messages:L(S),"preset-questions":Se.value,"welcome-title":L(V)(`@AGENT:智能体助手`),"welcome-description":L(V)(`@AGENT:我可以帮你阅读代码、执行命令、修改文件。选择下方话题或直接输入你的问题。`),"assistant-name":L(de),"assistant-avatar":L(q),theme:ve.value,disabled:L(C),generating:L(C),"upload-config":{accept:L(W)},"context-usage":L(he),placeholder:L(C)?L(V)(`@AGENT:正在生成中...`):L(V)(`@AGENT:输入消息,Enter 发送`),question:L(M),"question-submitting":L(oe),"question-labels":L(Te),"plan-config":{labels:{title:L(V)(`@AGENT:计划`),raw:L(V)(`@AGENT:原始参数`)}},onSend:Ce,onSelect:we,onStop:L(me),onAnswer:L(N)},null,8,[`messages`,`preset-questions`,`welcome-title`,`welcome-description`,`assistant-name`,`assistant-avatar`,`theme`,`disabled`,`generating`,`upload-config`,`context-usage`,`placeholder`,`question`,`question-submitting`,`question-labels`,`plan-config`,`onStop`,`onAnswer`]))])],64))])):(g(),f(nt,{key:1,type:ne.value},null,8,[`type`]))])])],512))}}),[[`__scopeId`,`data-v-794cbc00`]]);export{pt as default};