zen-gitsync 2.17.10 → 2.17.12

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 (93) hide show
  1. package/README.md +34 -0
  2. package/package.json +3 -1
  3. package/src/ui/public/assets/AgentView-BNH92bKu.js +5 -0
  4. package/src/ui/public/assets/AppVersionBadge-DXWTExsY.js +2 -0
  5. package/src/ui/public/assets/BranchSelector-CJI-I8JY.js +1 -0
  6. package/src/ui/public/assets/CommitForm-CTYhgpkj.js +6 -0
  7. package/src/ui/public/assets/CommonDialog-DNnN70HC.js +1 -0
  8. package/src/ui/public/assets/EditorView-C2AbXelw.js +1 -0
  9. package/src/ui/public/assets/FileDiffViewer-ZD705xXx.js +14 -0
  10. package/src/ui/public/assets/{FlowExecutionViewer-BgEt5_Hz.js → FlowExecutionViewer-BP9w_OQb.js} +1 -1
  11. package/src/ui/public/assets/FlowOrchestrationWorkspace-DeSI8Mlj.js +21 -0
  12. package/src/ui/public/assets/IconButton-BxRsG5E1.js +1 -0
  13. package/src/ui/public/assets/LogList-1sIFUx4e.js +7 -0
  14. package/src/ui/public/assets/MindmapView-6_fbvhh3.js +1 -0
  15. package/src/ui/public/assets/MonitorView-CURagJDe.js +1 -0
  16. package/src/ui/public/assets/ProjectStartupButton-COyb4Fbk.js +1 -0
  17. package/src/ui/public/assets/RemoteManagerDialog-DoS9EWQr.js +1 -0
  18. package/src/ui/public/assets/RemoteRepoCard-BwrdI5UH.js +1 -0
  19. package/src/ui/public/assets/SourceMapView-DADzBGfb.js +3 -0
  20. package/src/ui/public/assets/SvgIcon-VCh_kGGV.js +1 -0
  21. package/src/ui/public/assets/TemplateManager-C4go4RZu.js +1 -0
  22. package/src/ui/public/assets/UserInputNode-Cl5-bI42.js +1 -0
  23. package/src/ui/public/assets/WorkbenchView-B8QOp8nM.css +1 -0
  24. package/src/ui/public/assets/WorkbenchView-DtCyyW3b.js +6 -0
  25. package/src/ui/public/assets/_plugin-vue_export-helper-Dcylt9fO.js +19 -0
  26. package/src/ui/public/assets/{configStore-BLRcYJdf.js → configStore-BlqdtNUA.js} +1 -1
  27. package/src/ui/public/assets/element-plus-C1PloHQu.css +1 -0
  28. package/src/ui/public/assets/{element-plus-DcZ-TVp6.js → element-plus-IG-bwpR5.js} +6 -6
  29. package/src/ui/public/assets/flow-mindmap-C_w8GJXm.js +139 -0
  30. package/src/ui/public/assets/{gitStore-DMEDMZMk.js → gitStore-BKkZm2m3.js} +4 -4
  31. package/src/ui/public/assets/index-Bq7iCtof.css +1 -0
  32. package/src/ui/public/assets/index-DGHrUfrh.js +28 -0
  33. package/src/ui/public/assets/{office-docx-C0g5Oepv.js → office-docx-CkLOvV7H.js} +5 -5
  34. package/src/ui/public/assets/{office-excel-D-uYLKaA.js → office-excel-CDjN3Va1.js} +1 -1
  35. package/src/ui/public/assets/{office-pptx-De3fnc6M.js → office-pptx-CVu3fXsL.js} +1 -1
  36. package/src/ui/public/assets/previewSandbox-DYMhXJ91.js +0 -0
  37. package/src/ui/public/assets/toolsStore-BlRHNqXl.js +1 -0
  38. package/src/ui/public/assets/{useThemeObserver-CG8k8U18.js → useThemeObserver-DAWKXvWv.js} +1 -1
  39. package/src/ui/public/assets/{vendor-BIgZEtkx.js → vendor-DcK6TCKZ.js} +27 -27
  40. package/src/ui/public/assets/vue-flow-B3gZmkCI.js +39 -0
  41. package/src/ui/public/index.html +18 -18
  42. package/src/ui/server/index.js +8 -0
  43. package/src/ui/server/routes/recentDirectoriesAiSummary.js +288 -0
  44. package/src/ui/server/routes/recentDirectoriesAiSummary.test.js +208 -0
  45. package/src/ui/server/routes/remoteRepos.js +117 -21
  46. package/src/ui/server/routes/remoteRepos.test.js +167 -1
  47. package/src/ui/server/routes/workbench/agentChat.js +9 -3
  48. package/src/ui/server/routes/workbench/agentParity.test.js +16 -0
  49. package/src/ui/server/routes/workbench/agentSessionStore.js +2 -2
  50. package/src/ui/server/routes/workbench/envContext.test.js +8 -8
  51. package/src/ui/server/routes/workbench/index.js +32 -736
  52. package/src/ui/server/routes/workbench/instructionStore.js +3 -113
  53. package/src/ui/server/routes/workbench/jobStore.js +2 -4
  54. package/src/ui/server/routes/workbench/jobStore.test.js +1 -1
  55. package/src/ui/server/routes/workbench/jsonParse.js +6 -143
  56. package/src/ui/server/routes/workbench/orchestratorStore.js +1 -8
  57. package/src/ui/server/routes/workbench/projectRegistry.js +6 -29
  58. package/src/ui/server/routes/workbench/projectRegistry.test.js +33 -56
  59. package/src/ui/server/routes/workbench/projectTool.js +0 -1
  60. package/src/ui/server/routes/workbench/projectTool.test.js +2 -3
  61. package/src/ui/server/routes/workbench/shared.js +2 -7
  62. package/src/ui/server/routes/workbench/taskRunner.js +18 -180
  63. package/src/ui/server/routes/workbench.js +4 -5
  64. package/src/ui/public/assets/AgentView-C-AJo7KS.js +0 -5
  65. package/src/ui/public/assets/AppVersionBadge-B6R6eN2N.js +0 -2
  66. package/src/ui/public/assets/BranchSelector-Cj3kMWON.js +0 -1
  67. package/src/ui/public/assets/CommitForm-DpXuFG-F.js +0 -6
  68. package/src/ui/public/assets/CommonDialog-BLPHDsn0.js +0 -1
  69. package/src/ui/public/assets/EditorView-B1OnoXXf.js +0 -1
  70. package/src/ui/public/assets/FileDiffViewer-BQl0JcWd.js +0 -14
  71. package/src/ui/public/assets/FlowOrchestrationWorkspace-CbLmsSF8.js +0 -21
  72. package/src/ui/public/assets/IconButton-aM3vaSIN.js +0 -1
  73. package/src/ui/public/assets/LogList-UjoVFZ-H.js +0 -7
  74. package/src/ui/public/assets/MindmapView-D_04v0n4.js +0 -1
  75. package/src/ui/public/assets/MonitorView-BD1_uzq7.js +0 -1
  76. package/src/ui/public/assets/ProjectStartupButton-BTYlVvzh.js +0 -1
  77. package/src/ui/public/assets/RemoteManagerDialog-20mpQMwu.js +0 -1
  78. package/src/ui/public/assets/RemoteRepoCard-DOrm17Xd.js +0 -1
  79. package/src/ui/public/assets/SourceMapView-nUU9bYPh.js +0 -3
  80. package/src/ui/public/assets/SvgIcon-LUynX3Iq.js +0 -1
  81. package/src/ui/public/assets/TemplateManager-Bo3Q7J9Y.js +0 -1
  82. package/src/ui/public/assets/UserInputNode-D5tSXiJz.js +0 -1
  83. package/src/ui/public/assets/WorkbenchView-C_Gmzvj1.css +0 -1
  84. package/src/ui/public/assets/WorkbenchView-UGC334JL.js +0 -13
  85. package/src/ui/public/assets/_plugin-vue_export-helper-D-D9qkRz.js +0 -19
  86. package/src/ui/public/assets/element-plus-BWbXh9mk.css +0 -1
  87. package/src/ui/public/assets/flow-mindmap-D1VTkZh3.js +0 -139
  88. package/src/ui/public/assets/index-CW3xs-af.css +0 -1
  89. package/src/ui/public/assets/index-S0_gRyiO.js +0 -26
  90. package/src/ui/public/assets/previewSandbox-DXAxqDVu.js +0 -0
  91. package/src/ui/public/assets/toolsStore-CpImRUhI.js +0 -1
  92. package/src/ui/public/assets/vue-flow-CPCFgenl.js +0 -39
  93. package/src/ui/server/routes/workbench/sessionStore.js +0 -126
@@ -33,7 +33,6 @@ import {
33
33
  } from './projectRegistry.js';
34
34
  import { normalizeOrchestrator, buildActivityFeed, buildRunningAgents } from './orchestratorStore.js';
35
35
 
36
- const sub = (id, status) => ({ id, title: id, desc: '', status, promptOverride: '' });
37
36
  const job = (id, taskId, status, startedAt, endedAt = null) => ({
38
37
  id, taskId, subId: `${taskId}__${id}`, title: '', status, startedAt, endedAt,
39
38
  });
@@ -84,28 +83,25 @@ test('resolveTaskRepoPath 优先用任务自己的项目,空则回退当前项
84
83
  // ── 看板列推导 ──────────────────────────────────────────────────────
85
84
 
86
85
  test('deriveTaskColumn: 从没动过的任务在待处理', () => {
87
- assert.equal(deriveTaskColumn({ subtasks: [] }, []), 'todo');
88
- assert.equal(deriveTaskColumn({ subtasks: [sub('s1', 'todo')] }, []), 'todo');
86
+ assert.equal(deriveTaskColumn({}, []), 'todo');
89
87
  });
90
88
 
91
- test('deriveTaskColumn: 有 job 在跑或子任务 running 就是进行中', () => {
92
- assert.equal(deriveTaskColumn({ subtasks: [] }, [job('j1', 't1', 'running', '2026-01-01T00:00:00Z')]), 'doing');
93
- assert.equal(deriveTaskColumn({ subtasks: [] }, [job('j1', 't1', 'pending', '2026-01-01T00:00:00Z')]), 'doing');
94
- assert.equal(deriveTaskColumn({ subtasks: [sub('s1', 'running')] }, []), 'doing');
89
+ test('deriveTaskColumn: 有 job 在跑就是进行中', () => {
90
+ assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'running', '2026-01-01T00:00:00Z')]), 'doing');
91
+ assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'pending', '2026-01-01T00:00:00Z')]), 'doing');
95
92
  });
96
93
 
97
- test('deriveTaskColumn: 子任务全部完成才算已完成', () => {
98
- assert.equal(deriveTaskColumn({ subtasks: [sub('s1', 'done'), sub('s2', 'done')] }, []), 'done');
99
- // 一半完成 → 仍待处理,错误与进度由卡片标记展示
100
- assert.equal(deriveTaskColumn({ subtasks: [sub('s1', 'done'), sub('s2', 'todo')] }, []), 'todo');
101
- // 有子任务报错也算"需要人看一眼"
102
- assert.equal(deriveTaskColumn({ subtasks: [sub('s1', 'error'), sub('s2', 'todo')] }, []), 'todo');
103
- });
104
-
105
- test('deriveTaskColumn: 无子任务的任务只能看执行记录', () => {
106
- assert.equal(deriveTaskColumn({ subtasks: [], type: 'simple' }, [job('j1', 't1', 'done', '2026-01-01T00:00:00Z')]), 'done');
107
- assert.equal(deriveTaskColumn({ subtasks: [], type: 'simple' }, [job('j1', 't1', 'cancelled', '2026-01-01T00:00:00Z')]), 'todo');
108
- assert.equal(deriveTaskColumn({ subtasks: [], type: 'simple' }, [job('j1', 't1', 'error', '2026-01-01T00:00:00Z')]), 'todo');
94
+ test('deriveTaskColumn: 看执行记录 —— 最近一条 done 才算已完成', () => {
95
+ assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'done', '2026-01-01T00:00:00Z')]), 'done');
96
+ // 错误与取消都退回待处理:列表示任务是否仍待处理,错误是这一轮执行的结果
97
+ assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'cancelled', '2026-01-01T00:00:00Z')]), 'todo');
98
+ assert.equal(deriveTaskColumn({}, [job('j1', 't1', 'error', '2026-01-01T00:00:00Z')]), 'todo');
99
+ // 最新一条失败、前面成功过 → 仍然待处理
100
+ const jobs = [
101
+ job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:01:00Z'),
102
+ job('j2', 't1', 'error', '2026-01-02T00:00:00Z', '2026-01-02T00:01:00Z'),
103
+ ];
104
+ assert.equal(deriveTaskColumn({}, jobs), 'todo');
109
105
  });
110
106
 
111
107
  test('latestJob 按 startedAt 取最新的一条', () => {
@@ -152,7 +148,7 @@ test('buildProjectEntries: 目录段大小写不同也是同一个项目(旧
152
148
  assert.equal(entries[0].name, 'xuze3');
153
149
  // 项目行只认这个 key 的任务,key 分叉会连带把任务计数拆到两行上去
154
150
  const stats = summarizeProjectTasks(
155
- [{ id: 't1', projectPath: 'C:\\Users\\xuze3', subtasks: [] }],
151
+ [{ id: 't1', projectPath: 'C:\\Users\\xuze3' }],
156
152
  [],
157
153
  );
158
154
  assert.equal(stats.get('c:\\users\\xuze3').total, 1);
@@ -172,11 +168,11 @@ test('buildProjectEntries: 目录段大小写不同也是同一个项目(旧
172
168
  test('summarizeProjectTasks: 按项目汇总列数量、进度、活跃执行与最后活跃时间', () => {
173
169
  const tasks = [
174
170
  // A 项目:1 个已完成
175
- { id: 'a1', projectPath: 'D:\\a', subtasks: [sub('a1s', 'done')], updatedAt: '2026-01-01T00:00:00Z' },
171
+ { id: 'a1', projectPath: 'D:\\a', updatedAt: '2026-01-01T00:00:00Z' },
176
172
  // A 项目:1 个在跑(job running)
177
- { id: 'a2', projectPath: 'D:\\a', subtasks: [sub('a2s', 'running')], updatedAt: '2026-01-02T00:00:00Z' },
173
+ { id: 'a2', projectPath: 'D:\\a', updatedAt: '2026-01-02T00:00:00Z' },
178
174
  // B 项目:1 个待处理
179
- { id: 'b1', projectPath: 'D:\\b', subtasks: [sub('b1s', 'todo')], updatedAt: '2026-01-03T00:00:00Z' },
175
+ { id: 'b1', projectPath: 'D:\\b', updatedAt: '2026-01-03T00:00:00Z' },
180
176
  ];
181
177
  const jobs = [
182
178
  job('j1', 'a2', 'running', '2026-01-04T00:00:00Z'),
@@ -204,15 +200,10 @@ test('summarizeProjectTasks: 按项目汇总列数量、进度、活跃执行与
204
200
  test('decorateTaskForBoard: 只回卡片需要的字段', () => {
205
201
  const card = decorateTaskForBoard({
206
202
  id: 't1', title: '标题', desc: '描述', projectPath: 'D:\\a',
207
- subtasks: [sub('s1', 'done'), sub('s2', 'error')],
208
203
  attachments: [{ id: 'at1' }],
209
204
  createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-02T00:00:00Z',
210
205
  }, []);
211
206
  assert.equal(card.column, 'todo');
212
- assert.equal(card.type, 'complex');
213
- assert.equal(card.subtaskCount, 2);
214
- assert.equal(card.subtaskDoneCount, 1);
215
- assert.equal(card.subtaskErrorCount, 1);
216
207
  assert.equal(card.attachmentCount, 1);
217
208
  assert.equal(card.runningJobs, 0);
218
209
  // 没跑过就没有完成时间——看板「已完成」列的排序靠它,空值必须显式是 null
@@ -226,7 +217,7 @@ test('decorateTaskForBoard: lastJobEndedAt 取最近一条 job 的结束时间
226
217
  job('j2', 't1', 'done', '2026-01-03T00:00:00Z', '2026-01-03T00:20:00Z'),
227
218
  job('j3', 't1', 'error', '2026-01-02T00:00:00Z', '2026-01-02T00:05:00Z'),
228
219
  ];
229
- const base = { id: 't1', title: '', desc: '', subtasks: [], createdAt: '2025-12-31T00:00:00Z' };
220
+ const base = { id: 't1', title: '', desc: '', createdAt: '2025-12-31T00:00:00Z' };
230
221
 
231
222
  // 只有 j2 是"最近"的,即使它的结束时间排在 startedAt 序里也是最后
232
223
  assert.equal(decorateTaskForBoard(base, jobs).lastJobEndedAt, '2026-01-03T00:20:00Z');
@@ -261,11 +252,7 @@ test('buildActivityFeed: job 起止合成两条记录,与人类指令一起按
261
252
  id: 'j1', taskId: 't1', subId: 's1', status: 'done', pid: 123,
262
253
  startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z', exitCode: 0,
263
254
  }];
264
- const tasks = [{
265
- id: 't1', projectPath: 'D:\\ws\\proj-a',
266
- subtasks: [{ id: 's1', title: '子任务一', status: 'done' }],
267
- title: '任务一',
268
- }];
255
+ const tasks = [{ id: 't1', projectPath: 'D:\\ws\\proj-a', title: '任务一' }];
269
256
  const instructions = [{
270
257
  id: 'i1', text: '把这些重构一下', projectPath: 'D:\\ws\\proj-a',
271
258
  at: '2026-01-01T00:10:00Z', status: 'accepted',
@@ -275,7 +262,6 @@ test('buildActivityFeed: job 起止合成两条记录,与人类指令一起按
275
262
  assert.equal(feed.length, 3);
276
263
  assert.deepEqual(feed.map(r => r.kind), ['user', 'done', 'dispatch']);
277
264
  assert.equal(feed[1].taskTitle, '任务一');
278
- assert.equal(feed[1].subTitle, '子任务一');
279
265
  assert.equal(feed[1].projectName, 'proj-a');
280
266
  assert.equal(feed[2].pid, 123);
281
267
  assert.equal(feed[0].instructionStatus, 'accepted');
@@ -287,7 +273,7 @@ test('buildActivityFeed: 指令带起来的起跑并进指令行,不再重复
287
273
  startedAt: '2026-01-01T00:00:01Z', endedAt: '2026-01-01T00:05:00Z',
288
274
  }];
289
275
  // 任务标题就是指令首行,所以派发行和指令行本来长得一模一样
290
- const tasks = [{ id: 't1', title: '把这些重构一下', projectPath: 'D:\\ws\\proj-a', subtasks: [] }];
276
+ const tasks = [{ id: 't1', title: '把这些重构一下', projectPath: 'D:\\ws\\proj-a' }];
291
277
  const instructions = [{
292
278
  id: 'i1', text: '把这些重构一下', taskId: 't1', projectPath: 'D:\\ws\\proj-a',
293
279
  at: '2026-01-01T00:00:00Z', status: 'accepted',
@@ -304,7 +290,7 @@ test('buildActivityFeed: 只吸收指令后的首次起跑,手动重跑仍保
304
290
  { id: 'j1', taskId: 't1', status: 'done', startedAt: '2026-01-01T00:00:01Z', endedAt: '2026-01-01T00:05:00Z' },
305
291
  { id: 'j2', taskId: 't1', status: 'done', startedAt: '2026-01-01T00:10:00Z', endedAt: '2026-01-01T00:15:00Z' },
306
292
  ];
307
- const tasks = [{ id: 't1', title: '跑一下', projectPath: 'D:\\ws\\proj-a', subtasks: [] }];
293
+ const tasks = [{ id: 't1', title: '跑一下', projectPath: 'D:\\ws\\proj-a' }];
308
294
  const instructions = [{
309
295
  id: 'i1', text: '跑一下', taskId: 't1', projectPath: 'D:\\ws\\proj-a',
310
296
  at: '2026-01-01T00:00:00Z', status: 'accepted',
@@ -318,7 +304,7 @@ test('buildActivityFeed: 只吸收指令后的首次起跑,手动重跑仍保
318
304
  test('buildActivityFeed: 指令还没跑起来时,起跑行照旧保留给别的 job', () => {
319
305
  // 指令建了任务但没执行(调度暂停)——此时该任务的任何起跑都不是它带起来的
320
306
  const jobs = [{ id: 'j1', taskId: 't1', status: 'done', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z' }];
321
- const tasks = [{ id: 't1', title: '手跑的', projectPath: 'D:\\a', subtasks: [] }];
307
+ const tasks = [{ id: 't1', title: '手跑的', projectPath: 'D:\\a' }];
322
308
  const instructions = [{
323
309
  id: 'i1', text: '手跑的', taskId: 't1', projectPath: 'D:\\a',
324
310
  at: '2026-01-01T00:20:00Z', status: 'created',
@@ -333,7 +319,7 @@ test('buildActivityFeed: 失败与取消分别落成 error / cancelled', () => {
333
319
  { id: 'j1', taskId: 't1', subId: 's1', status: 'error', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:01:00Z', error: 'boom' },
334
320
  { id: 'j2', taskId: 't1', subId: 's2', status: 'cancelled', startedAt: '2026-01-01T00:02:00Z', endedAt: '2026-01-01T00:03:00Z' },
335
321
  ];
336
- const feed = buildActivityFeed({ jobs, tasks: [{ id: 't1', projectPath: 'D:\\a', subtasks: [] }] });
322
+ const feed = buildActivityFeed({ jobs, tasks: [{ id: 't1', projectPath: 'D:\\a' }] });
337
323
  const kinds = feed.map(r => r.kind);
338
324
  assert.ok(kinds.includes('error'));
339
325
  assert.ok(kinds.includes('cancelled'));
@@ -348,23 +334,18 @@ test('buildActivityFeed: 任务已被删除时仍能出记录(不留孤儿空
348
334
  assert.equal(feed[0].projectName, '');
349
335
  });
350
336
 
351
- test('buildRunningAgents: 只列活跃 job,到 job 粒度而不是任务粒度', () => {
352
- const tasks = [{
353
- id: 't1', projectPath: 'D:\\ws\\proj-a', subtasks: [
354
- { id: 's1', title: '子任务一', status: 'running' },
355
- { id: 's2', title: '子任务二', status: 'running' },
356
- ],
357
- }];
337
+ test('buildRunningAgents: 只列活跃 job,按 startedAt 倒序', () => {
338
+ const tasks = [{ id: 't1', title: '任务一', projectPath: 'D:\\ws\\proj-a' }];
358
339
  const jobs = [
359
- { id: 'j1', taskId: 't1', subId: 's1', status: 'running', pid: 111, startedAt: '2026-01-01T00:02:00Z' },
360
- { id: 'j2', taskId: 't1', subId: 's2', status: 'pending', pid: 222, startedAt: '2026-01-01T00:01:00Z' },
361
- { id: 'j3', taskId: 't1', subId: 's3', status: 'done', pid: 333, startedAt: '2026-01-01T00:00:00Z' },
340
+ { id: 'j1', taskId: 't1', subId: 't1__simple', status: 'running', pid: 111, startedAt: '2026-01-01T00:02:00Z' },
341
+ { id: 'j2', taskId: 't1', subId: 't1__simple__r1', status: 'pending', pid: 222, startedAt: '2026-01-01T00:01:00Z' },
342
+ { id: 'j3', taskId: 't1', subId: 't1__simple__r2', status: 'done', pid: 333, startedAt: '2026-01-01T00:00:00Z' },
362
343
  ];
363
344
  const running = buildRunningAgents({ jobs, tasks });
364
345
  assert.equal(running.length, 2);
365
346
  // 最新的排在前面
366
347
  assert.equal(running[0].jobId, 'j1');
367
- assert.equal(running[0].subTitle, '子任务一');
348
+ assert.equal(running[0].taskTitle, '任务一');
368
349
  assert.equal(running[0].projectName, 'proj-a');
369
350
  assert.equal(running[1].status, 'pending');
370
351
  });
@@ -416,11 +397,7 @@ test('buildTaskDetail: 任务缺失返回 null(交路由转 404)', () => {
416
397
  });
417
398
 
418
399
  test('buildTaskDetail: lastJob 取最新一条,recentJobs 倒序且被限制条数', () => {
419
- const task = {
420
- id: 't1', title: '任务', desc: '', type: 'complex',
421
- subtasks: [sub('s1', 'done'), sub('s2', 'running')],
422
- attachments: [{ id: 'a1' }],
423
- };
400
+ const task = { id: 't1', title: '任务', desc: '', attachments: [{ id: 'a1' }] };
424
401
  const jobs = [
425
402
  job('j1', 't1', 'done', '2026-01-01T00:00:00Z', '2026-01-01T00:01:00Z'),
426
403
  job('j3', 't1', 'running', '2026-01-01T00:02:00Z'),
@@ -439,7 +416,7 @@ test('buildTaskDetail: lastJob 取最新一条,recentJobs 倒序且被限制
439
416
  });
440
417
 
441
418
  test('buildTaskDetail: recentJobLimit 生效;无 job 时 lastJob 为 null 而不是抛错', () => {
442
- const task = { id: 't2', title: '', desc: '', type: 'simple', subtasks: [] };
419
+ const task = { id: 't2', title: '', desc: '' };
443
420
  const jobs = [1, 2, 3, 4, 5, 6].map(i => job('j' + i, 't2', 'done', `2026-01-0${i}T00:00:00Z`));
444
421
  const d = buildTaskDetail(task, jobs, { recentJobLimit: 2 });
445
422
  assert.equal(d.recentJobs.length, 2);
@@ -79,7 +79,6 @@ function taskLineOf(p) {
79
79
  if (!s.total) return '任务: 无';
80
80
  const parts = [`待处理 ${s.todo || 0}`, `进行中 ${s.doing || 0}`, `已完成 ${s.done || 0}`];
81
81
  if (s.runningJobs > 0) parts.push(`正在执行 ${s.runningJobs}`);
82
- if (s.errorSubtasks > 0) parts.push(`子任务报错 ${s.errorSubtasks}`);
83
82
  return `任务: ${parts.join(' / ')}`;
84
83
  }
85
84
 
@@ -42,7 +42,7 @@ function project(overrides = {}) {
42
42
  unstaged: 0,
43
43
  untracked: 0,
44
44
  };
45
- const baseStats = { total: 0, todo: 0, doing: 0, done: 0, progress: 0, runningJobs: 0, errorSubtasks: 0 };
45
+ const baseStats = { total: 0, todo: 0, doing: 0, done: 0, progress: 0, runningJobs: 0 };
46
46
  const merged = {
47
47
  path: 'D:\\ws\\proj',
48
48
  key: 'd:\\ws\\proj',
@@ -112,10 +112,9 @@ test('非仓库 / 目录不存在 / 探测超时都能区分,不谎报成"干
112
112
  });
113
113
 
114
114
  test('任务进度按看板三列报出,无任务时说"无"', () => {
115
- const withTasks = formatProjectList([project({ stats: { total: 9, todo: 1, doing: 2, done: 6, runningJobs: 1, errorSubtasks: 1 } })]);
115
+ const withTasks = formatProjectList([project({ stats: { total: 9, todo: 1, doing: 2, done: 6, runningJobs: 1 } })]);
116
116
  assert.match(withTasks, /待处理 1 \/ 进行中 2 \/ 已完成 6/);
117
117
  assert.match(withTasks, /正在执行 1/);
118
- assert.match(withTasks, /子任务报错 1/);
119
118
 
120
119
  assert.match(formatProjectList([project()]), /任务: 无/);
121
120
  });
@@ -38,11 +38,6 @@ export const TASKS_FILE = path.join(DATA_DIR, 'tasks.json');
38
38
  export const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
39
39
  export const IMAGES_DIR = path.join(DATA_DIR, 'workbench-images');
40
40
  export const INSTRUCTION_FILE = path.join(DATA_DIR, 'ai-instruction.json');
41
- export const SUBTASK_INSTRUCTION_FILE = path.join(DATA_DIR, 'ai-subtask-instruction.json');
42
- // AI 对话拆分会话存档:每个 sessionId 一个文件,重启可恢复多轮上下文
43
- export const SESSIONS_DIR = path.join(DATA_DIR, 'ai-split-sessions');
44
- export const MAX_SESSIONS = 100; // 软上限,触发清理
45
- export const SESSIONS_KEEP = 50; // 超过上限时保留最新 N 个
46
41
  // 执行日志持久化:jobs.json 是历史档案,jobs-config.json 是保留策略
47
42
  export const JOBS_FILE = path.join(DATA_DIR, 'jobs.json');
48
43
  export const JOBS_CONFIG_FILE = path.join(DATA_DIR, 'jobs-config.json');
@@ -82,7 +77,7 @@ export const MANIFEST_FILES = [
82
77
  // 真正需要小体积的是图片,由前端上传前压缩保证(见 useWorkbenchAttachments.ts),
83
78
  // 这里只做最后一道兜底,避免超大 body 把内存吃光。
84
79
  export const MAX_IMAGE_BYTES = 20 * 1024 * 1024; // 单个附件最大 20MB
85
- export const MAX_ATTACHMENTS_PER_SUBTASK = 9; // 一个子任务最多挂 9 个附件
80
+ export const MAX_ATTACHMENTS_PER_TASK = 9; // 一个任务最多挂 9 个附件
86
81
 
87
82
  // ── 时间 / ID 工具 ─────────────────────────────────────────
88
83
  export function nowIso() {
@@ -109,7 +104,7 @@ export async function readJson(file, fallback) {
109
104
  }
110
105
  }
111
106
 
112
- // 原子写:tmp + rename。避免半写状态被读到(与 sessionStore 一致)
107
+ // 原子写:tmp + rename。避免半写状态被读到(与 jobStore 一致)
113
108
  export async function writeJson(file, data) {
114
109
  await ensureDataDir();
115
110
  const tmp = `${file}.tmp`;
@@ -23,12 +23,12 @@
23
23
  // - launchClaudeInNewWindow(cwd, prompt, resumeSessionId) spawn claude,返回 {pid, child}
24
24
  // - launchOpencodeRun(cwd, prompt, resumeSessionId) spawn opencode,返回 {pid, child}
25
25
  // - normalizeTaskExecutor(value) 非法值回落 'claude'
26
- // - runTaskQueue(task, repoPath, branch, opts) 顺序执行 task.subtasks(opts.executor 透传)
27
- // - runSingleSubtask(task, sub, repoPath, branch, priorOutputs, opts) 跑单个 sub
28
- // - syncSubToCancelled(job) cancel 路径专用
29
- // - persistTaskAfterRun(task) 把 sub.status 落回 tasks.json
30
- // - collectPriorOutputs(task, targetSub) 收集前序 done sub 的输出
26
+ // - runSingleSubtask(task, sub, repoPath, branch, opts) 跑一次任务(sub 是运行载体,见下)
31
27
  // - waitProcessExit(pid) polling 等进程退出
28
+ //
29
+ // 关于 `sub`:任务不再有子任务,但执行器仍以一个「运行载体」对象作为入参 ——
30
+ // 它携带本次要跑的 title / desc / promptOverride / attachments。路由层构造它,
31
+ // 续聊时同一任务会造出新的载体(`{taskId}__simple__r1` 等),job.subId 记的就是它。
32
32
 
33
33
  import fs from 'fs';
34
34
  import path from 'path';
@@ -36,9 +36,7 @@ import { spawn, execFileSync } from 'child_process';
36
36
  import {
37
37
  logger,
38
38
  PROMPTS_FILE,
39
- TASKS_FILE,
40
39
  readJson,
41
- writeJson,
42
40
  nowIso,
43
41
  genId,
44
42
  interpolate,
@@ -49,8 +47,6 @@ import {
49
47
  jobs,
50
48
  cancelledJobs,
51
49
  publish,
52
- snapshotJobs,
53
- refreshJobsFromDisk,
54
50
  flushJobsSaveNow,
55
51
  } from './jobStore.js';
56
52
 
@@ -590,55 +586,13 @@ export function createOpencodeEventHandler(job, appendOutput, appendThinking, to
590
586
  }
591
587
 
592
588
  /**
593
- * 顺序执行一个任务下所有子任务;上一个结束再启动下一个。
594
- * 入参 fromIndex 指定从哪个 sub 开始(0-based,默认 0),用于"从此处开始"入口;
595
- * fromIndex>0 时,把 [0, fromIndex) 区间内已 done 的 sub 输出预填到 priorOutputs,
596
- * 这样后续 sub 仍能拿到前序上下文(跟单 sub 执行的语义保持一致)。
597
- *
598
- * 连续模式(默认):任意 sub 终态非 done(cancelled / error)→ 整批停,后续 sub 保持 todo。
599
- * AI 拆出来的 sub 一般前后强依赖(前一步产出是后一步输入),出错就停下来让用户决策。
600
- * 关闭后回退旧行为:单个 sub 失败不影响后续 sub 继续跑。
601
- */
602
- export async function runTaskQueue(task, repoPath, branch, opts) {
603
- // 前序上下文:跑完一个 sub 后把它"完成态"输出存到这里,下一个 sub 启动时
604
- // 拼到 prompt 头部,让 Claude 知道前面做了什么、产出了什么。
605
- // 现在 LLM 都是百万 token 上下文窗口,完整透传 raw output,不做截断——
606
- // 关键产物(生成的代码块、JSON、结论)在中间被砍掉反而会让后续 sub 失去依据。
607
- const requested = Number(opts && opts.fromIndex);
608
- const fromIndex = Number.isInteger(requested) && requested >= 0 && requested < task.subtasks.length
609
- ? requested
610
- : 0;
611
- // 从 fromIndex 开始时,把前面已 done 的 sub 输出预填进 priorOutputs,
612
- // 否则"从中间开始"会丢失前序上下文。
613
- const priorOutputs = fromIndex > 0
614
- ? await collectPriorOutputsUpTo(task, fromIndex)
615
- : [];
616
- const sequential = task.sequential !== false;
617
- for (let i = fromIndex; i < task.subtasks.length; i++) {
618
- const sub = task.subtasks[i];
619
- if (sub.status === 'done') continue;
620
- const outcome = await runSingleSubtask(task, sub, repoPath, branch, priorOutputs, { executor: opts.executor });
621
- // 逐 sub 落盘:之前只在队列跑完才 persistTaskAfterRun,中途崩溃会丢已完成
622
- // sub 的状态。runSingleSubtask 已经把 sub.status 改完,这里补一次落盘。
623
- await persistTaskAfterRun(task);
624
- if (sequential && outcome !== 'done') {
625
- // cancelled / error → 后续 sub 全部保持 todo,不再继续
626
- break;
627
- }
628
- }
629
- }
630
-
631
- /**
632
- * 执行单个子任务。被 runTaskQueue(整批)和"单 sub 执行"endpoint 共用。
589
+ * 执行一次任务。任务一次只跑一个进程,本函数是唯一的执行入口
590
+ * (首次执行、手动重跑、续聊都走它)。
633
591
  *
634
- * @param {object} task 主任务对象
635
- * @param {object} sub 要跑的子任务
592
+ * @param {object} task 任务对象
593
+ * @param {object} sub 本次运行载体:{ id, title, desc, promptOverride, attachments }
636
594
  * @param {string} repoPath 仓库路径
637
595
  * @param {string} branch 分支名(可空)
638
- * @param {Array<{title:string,output:string}>} priorOutputs
639
- * 前序 done 子任务的输出摘要(in-place 追加)。用于把同一任务下
640
- * 前面已完成的 sub 产物拼到当前 sub 的 prompt 头部,让 Claude
641
- * 知道上下文。单独跑一个 sub 时,这个数组里只会有"前面 done 的 sub"。
642
596
  * @param {object} [options]
643
597
  * @param {string|null} [options.resumeSessionId]
644
598
  * 续接历史会话:claude 走 --resume <id>,opencode 走 --session <id>。
@@ -646,9 +600,9 @@ export async function runTaskQueue(task, repoPath, branch, opts) {
646
600
  * @param {string} [options.executor]
647
601
  * 执行器 'claude' | 'opencode'。缺省回落 'claude';路由层负责把
648
602
  * "body 指定 > 配置默认"先解析好再传进来。
649
- * @returns {Promise<'done'|'cancelled'|'error'>} sub 的终态
603
+ * @returns {Promise<'done'|'cancelled'|'error'>} 本次运行的终态
650
604
  */
651
- export async function runSingleSubtask(task, sub, repoPath, branch, priorOutputs, options) {
605
+ export async function runSingleSubtask(task, sub, repoPath, branch, options) {
652
606
  const opts = options || {};
653
607
  const resumeSessionId = opts.resumeSessionId || null;
654
608
  const executor = normalizeTaskExecutor(opts.executor);
@@ -662,28 +616,13 @@ export async function runSingleSubtask(task, sub, repoPath, branch, priorOutputs
662
616
  branch: branch || ''
663
617
  };
664
618
  const interpolated = interpolate(promptTemplate, ctx);
665
- // 标题与描述可能逐字相同(派发建的简单任务就是),去重规则见 promptParts.js
619
+ // 标题与描述可能逐字相同(派发建的任务就是),去重规则见 promptParts.js
666
620
  let prompt = composePromptBody(interpolated, sub.title, sub.desc);
667
621
 
668
- // ── 前序上下文:把前几个 done 子任务的输出完整拼到 prompt 头部 ──
669
- if (priorOutputs && priorOutputs.length > 0) {
670
- const prevBlock = priorOutputs.map((p, i) => {
671
- return `### [${i + 1}] ${p.title}\n${p.output || ''}`;
672
- }).join('\n\n');
673
- prompt = `以下是同一任务下已经完成的前序子任务输出(仅作上下文参考,请基于这些结论继续当前子任务,无需重复执行它们):
674
-
675
- ${prevBlock}
676
-
677
- ---
678
-
679
- ${prompt}`;
680
- }
681
-
682
622
  // ── 附件:合并 sub.attachments + task.attachments 后拼到 prompt 末尾 ──
683
623
  // PDF 附件在服务端预提取全文(Claude CLI v2.1.x 的 pdfParse 有 bug),
684
624
  // 图片和其他文件仍列路径让 Claude CLI 直接读取。
685
- // 主任务附件对所有 sub 都可见;子任务自己的附件只对该 sub 可见。
686
- // 注意:run-simple 路径下 virtualSub.attachments 就是 task.attachments 的同一引用,
625
+ // 注意:首次执行时 sub.attachments 就是 task.attachments 的同一引用,
687
626
  // 不去重会把同一张图在 prompt 里列两遍。按 absolutePath 去重。
688
627
  const taskAtts = Array.isArray(task.attachments) ? task.attachments : [];
689
628
  const subAtts = Array.isArray(sub.attachments) ? sub.attachments : [];
@@ -703,7 +642,7 @@ ${prompt}`;
703
642
  }
704
643
 
705
644
  // ── 运行环境上下文:项目清单 + 看板概览 + 真相源文件路径 ──
706
- // 拼在**最前面**、任务正文压尾,与上面 priorOutputs 的位置口径一致:
645
+ // 拼在**最前面**、任务正文压尾:
707
646
  // 越靠后离模型的注意力中心越近,用户真正要办的那句话必须在最后一屏。
708
647
  // task.envContext === false 可以单任务关掉(默认开 —— 老任务不迁移也一并受益)。
709
648
  if (task.envContext !== false) {
@@ -716,7 +655,8 @@ ${prompt}`;
716
655
  id: jobId,
717
656
  taskId: task.id,
718
657
  subId: sub.id,
719
- title: `${task.title} / ${sub.title}`,
658
+ // 一条任务 = 一次会话:日志标题就用任务标题(sub 只是运行载体,标题同源)
659
+ title: task.title,
720
660
  status: 'pending',
721
661
  prompt,
722
662
  // 本轮用的执行器。前端日志详情按它显示助手名;简单任务续聊时
@@ -731,8 +671,6 @@ ${prompt}`;
731
671
  toolCalls: []
732
672
  };
733
673
  jobs.set(jobId, job);
734
- sub.status = 'running';
735
- publish('sub:update', { taskId: task.id, sub });
736
674
  publish('job:update', job);
737
675
 
738
676
  // 声明在 try 之外:启动器就抛错时 finally 也要能安全收口(见 toolTracker?.seal)
@@ -836,43 +774,28 @@ ${prompt}`;
836
774
  job.exitCode = 130; // 128 + SIGINT(2),约定俗成的"用户取消"退出码
837
775
  job.status = 'cancelled';
838
776
  job.error = '用户已停止执行';
839
- // 同步把闭包里的 sub 也置 cancelled(cancel 接口的 syncSubToCancelled 改了磁盘 task
840
- // 引用,但 runSingleSubtask 形参里的 sub 是另一份内存引用)。否则 finally publish sub:update
841
- // 会用 status='running' 覆盖掉前端已渲染的 cancelled 状态,导致 UI 反复跳回 running。
842
- sub.status = 'cancelled';
843
- if (!sub.error) sub.error = '用户已停止执行';
844
777
  } else if (job.agentError) {
845
778
  // 执行器在协议层报了 error(如模型 5xx、配置缺失)。CLI 退出码可能是 0,
846
- // 但不能标 done —— 那会把失败伪装成完成,后续 sub 还会拿空输出当下文继续跑。
779
+ // 但不能标 done —— 那会把失败伪装成完成。
847
780
  job.exitCode = 1;
848
781
  job.status = 'error';
849
782
  job.error = job.agentError;
850
- sub.status = 'error';
851
- sub.error = job.agentError;
852
- sub.errorAt = nowIso();
853
783
  appendOutput(`\n> [${executor}] ${job.agentError}\n`);
854
784
  } else {
855
785
  job.exitCode = 0;
856
786
  job.status = 'done';
857
- sub.status = 'done';
858
- // 把这个 sub 的输出累积到前序上下文,喂给下一个 sub
859
- if (priorOutputs) priorOutputs.push({ title: sub.title, output: job.output || '' });
860
787
  }
861
788
  } catch (err) {
862
789
  const errMsg = err && err.message ? err.message : String(err);
863
790
  job.error = errMsg;
864
791
  job.status = 'error';
865
- sub.status = 'error';
866
- sub.error = errMsg;
867
- sub.errorAt = nowIso();
868
792
  } finally {
869
793
  // 工具调用收口:进程都退了,还挂在 running 的调用不会再有下文。先标终态再推 job:update,
870
- // 否则前端那些工具块会一直转圈(跟 sub 终态没写回时 UI 一直 running 同一类问题)。
794
+ // 否则前端那些工具块会一直转圈。
871
795
  toolTracker?.seal(job.status);
872
796
  // 移除 child 引用——避免后续被 SSE 序列化到前端
873
797
  delete job.child;
874
798
  publish('job:update', job);
875
- publish('sub:update', { taskId: task.id, sub });
876
799
  // 终态:await 同步落盘,确保 done/cancelled/error 全部立即归档。
877
800
  try {
878
801
  await flushJobsSaveNow();
@@ -880,94 +803,9 @@ ${prompt}`;
880
803
  logger.warn('[workbench] flushJobsSaveNow failed (job id=' + job.id + ', status=' + job.status + '):', err && err.message || err);
881
804
  }
882
805
  }
883
- // 把 sub 的终态返回给 runTaskQueue,用于「连续模式」判断要不要 break 整批队列
884
806
  return job.status; // 'done' | 'cancelled' | 'error'
885
807
  }
886
808
 
887
- /**
888
- * 把被取消的 sub 同步置 'cancelled' 并落盘。
889
- * cancelJob 路径专用:前端 taskIsRunning/sub.is-running 都看 sub.status,不改就会出现
890
- * "主任务黄点 + sub running 动效 + 右侧执行完成"三处不一致。
891
- *
892
- * 简单任务的虚拟 subId 不在 tasks.json 里(task.subtasks 是 complex 才有),所以这里
893
- * 找不到 sub 时静默返回;简单任务的 running 状态由 job 数组单独维护(见 taskIsRunning)。
894
- *
895
- * @returns {{ taskId: string, sub: object } | null} 找到并更新时返回新 sub,否则 null
896
- */
897
- export async function syncSubToCancelled(job) {
898
- if (!job || !job.taskId || !job.subId) return null;
899
- const data = await readJson(TASKS_FILE, { tasks: [] });
900
- const task = (data.tasks || []).find(x => x.id === job.taskId);
901
- if (!task || !Array.isArray(task.subtasks)) return null;
902
- const sub = task.subtasks.find(s => s && s.id === job.subId);
903
- if (!sub) return null;
904
- if (sub.status === 'cancelled') return { taskId: task.id, sub }; // 已置过,幂等返回
905
- sub.status = 'cancelled';
906
- sub.error = '用户已停止执行';
907
- sub.errorAt = nowIso();
908
- task.updatedAt = nowIso();
909
- await writeJson(TASKS_FILE, data);
910
- publish('sub:update', { taskId: task.id, sub });
911
- publish('task:update', task);
912
- return { taskId: task.id, sub };
913
- }
914
-
915
- /** 把 task.subtasks 写回 tasks.json,并广播 task:update。runTaskQueue 和"单 sub 执行"共用。 */
916
- export async function persistTaskAfterRun(task) {
917
- const data = await readJson(TASKS_FILE, { tasks: [] });
918
- const t = data.tasks.find(x => x.id === task.id);
919
- if (t) {
920
- // 仅同步 status 之外的 error/errorAt 字段,避免覆盖用户编辑过的 title/desc 等。
921
- const newMap = new Map(task.subtasks.map(s => [s.id, s]));
922
- t.subtasks = (t.subtasks || []).map(old => {
923
- const fresh = newMap.get(old.id);
924
- if (!fresh) return old;
925
- return {
926
- ...old,
927
- status: fresh.status ?? old.status,
928
- error: fresh.error ?? old.error,
929
- errorAt: fresh.errorAt ?? old.errorAt,
930
- };
931
- });
932
- t.updatedAt = nowIso();
933
- await writeJson(TASKS_FILE, data);
934
- publish('task:update', t);
935
- }
936
- }
937
-
938
- /**
939
- * 构建单 sub 执行时的 priorOutputs:把同一 task 下"排在当前 sub 之前"且已 done 的
940
- * 子任务输出摘要收集起来。这样单独跑一个 sub 时,它也能拿到前序上下文。
941
- */
942
- export async function collectPriorOutputs(task, targetSub) {
943
- const targetIdx = task.subtasks.findIndex(s => s.id === targetSub.id);
944
- if (targetIdx < 0) return [];
945
- return collectPriorOutputsUpTo(task, targetIdx);
946
- }
947
-
948
- /**
949
- * 收集 [0, endIdx) 区间内已 done 的 sub 输出摘要,作为队列内 sub 的前序上下文。
950
- * runTaskQueue 在 fromIndex>0 时调这个,让"从此处开始"也能拼上前序 done sub 的结论。
951
- */
952
- export async function collectPriorOutputsUpTo(task, endIdx) {
953
- // 前序 sub 可能是**另一个 g ui 实例**跑完的(或者本进程重启过),只翻内存会漏,
954
- // 于是"前序结论"整段丢了。刷新有 mtime 短路,队列里每个 sub 调一次也只是个 stat。
955
- await refreshJobsFromDisk();
956
- const prior = [];
957
- for (let i = 0; i < endIdx; i++) {
958
- const s = task.subtasks[i];
959
- if (s.status !== 'done') continue;
960
- // 从 jobs 列表里找最近一个属于这个 sub 且 status=done 的 job,
961
- // 取其 output 作为"前序上下文"。完整透传,不做字符截断。
962
- const job = snapshotJobs()
963
- .filter(j => j.subId === s.id && j.status === 'done')
964
- .sort((a, b) => (b.endedAt || '').localeCompare(a.endedAt || ''))[0];
965
- if (!job) continue;
966
- prior.push({ title: s.title, output: job.output || '' });
967
- }
968
- return prior;
969
- }
970
-
971
809
  // polling 等进程退出:信号 0 探测存活;30 分钟超时兜底
972
810
  export function waitProcessExit(pid) {
973
811
  return new Promise(resolve => {
@@ -15,17 +15,16 @@
15
15
  // 工作台后端路由入口。
16
16
  //
17
17
  // 本文件原本是 3539 行的巨型文件(含所有顶层工具函数 + 45 个路由端点)。
18
- // 2026-06-29 已按业务域拆分到 ./workbench/ 子目录的 9 个模块:
18
+ // 2026-06-29 已按业务域拆分到 ./workbench/ 子目录的多个模块:
19
19
  // - shared.js 常量 + 通用工具 (nowIso, genId, readJson, writeJson, interpolate)
20
- // - jsonParse.js JSON 解析降级链 (parseSubtaskJson 等)
20
+ // - jsonParse.js 从 LLM 输出里抠 JSON 的纯函数
21
21
  // - llmClient.js LLM 客户端 (callLlmJson, callLlmStream)
22
22
  // - projectScan.js 子项目识别 (findSubProjects, detectProjectManifest)
23
23
  // - attachmentUtils.js 附件白名单 (sanitizeExt, resolveExt, MIME_TO_EXT)
24
- // - sessionStore.js AI 对话拆分会话持久化
25
24
  // - instructionStore.js AI 指令读写
26
25
  // - jobStore.js jobs Map + bus + 持久化 + retention
27
- // - taskRunner.js 任务执行引擎 (runTaskQueue, runSingleSubtask)
28
- // - index.js registerWorkbenchRoutes 入口聚合(45 个路由)
26
+ // - taskRunner.js 任务执行引擎 (runSingleSubtask)
27
+ // - index.js registerWorkbenchRoutes 入口聚合
29
28
  //
30
29
  // 此处保留 re-export 是为了让 src/ui/server/index.js 的 import 路径不变。
31
30