zen-gitsync 2.17.10 → 2.17.11

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 (131) hide show
  1. package/package.json +2 -1
  2. package/src/ui/public/assets/AgentView-DJVwX8Er.js +5 -0
  3. package/src/ui/public/assets/AgentView-vvVhbIQ5.css +1 -0
  4. package/src/ui/public/assets/{AppVersionBadge-D5tJzJfa.css → AppVersionBadge-CJrDr7GV.css} +1 -1
  5. package/src/ui/public/assets/AppVersionBadge-rNwRlkEK.js +2 -0
  6. package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +1 -0
  7. package/src/ui/public/assets/BranchSelector-D2JJINuw.js +1 -0
  8. package/src/ui/public/assets/CommitForm-CQ0K8Yy2.css +1 -0
  9. package/src/ui/public/assets/CommitForm-CayXdf3-.js +6 -0
  10. package/src/ui/public/assets/CommonDialog-DF5Vzvi8.js +1 -0
  11. package/src/ui/public/assets/CommonDialog-jP0E9mco.css +1 -0
  12. package/src/ui/public/assets/EditorView-Bv-3PWwF.js +1 -0
  13. package/src/ui/public/assets/EditorView-DZMLvQds.css +1 -0
  14. package/src/ui/public/assets/FileDiffViewer-B9EkuKdF.css +1 -0
  15. package/src/ui/public/assets/FileDiffViewer-CUsML3pW.js +15 -0
  16. package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +1 -0
  17. package/src/ui/public/assets/FlowExecutionViewer-n07dZgnE.js +1 -0
  18. package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +1 -0
  19. package/src/ui/public/assets/FlowOrchestrationWorkspace-CdG8HRux.js +21 -0
  20. package/src/ui/public/assets/IconButton-GxK-nCF7.js +1 -0
  21. package/src/ui/public/assets/IconButton-tjkOjzkC.css +1 -0
  22. package/src/ui/public/assets/LogList-BlJplIi1.css +1 -0
  23. package/src/ui/public/assets/LogList-CpcpXaIZ.js +7 -0
  24. package/src/ui/public/assets/MindmapView-BHVJ6OzY.js +2 -0
  25. package/src/ui/public/assets/MindmapView-BQTqx842.css +1 -0
  26. package/src/ui/public/assets/MonitorView-DetlFFXq.css +1 -0
  27. package/src/ui/public/assets/MonitorView-IpfJRJaK.js +1 -0
  28. package/src/ui/public/assets/ProjectStartupButton-DM2gYw48.js +1 -0
  29. package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +1 -0
  30. package/src/ui/public/assets/RemoteManagerDialog-BauAeH2w.css +1 -0
  31. package/src/ui/public/assets/RemoteManagerDialog-DLPnI8Tr.js +4 -0
  32. package/src/ui/public/assets/RemoteRepoCard-CG-jARTe.css +1 -0
  33. package/src/ui/public/assets/RemoteRepoCard-D804CRwk.js +7 -0
  34. package/src/ui/public/assets/SourceMapView-CN4Hkyp4.css +1 -0
  35. package/src/ui/public/assets/SourceMapView-Ct8J5WA4.js +3 -0
  36. package/src/ui/public/assets/{SvgIcon-BQ6bR7l0.css → SvgIcon-CDNsk4-Q.css} +1 -1
  37. package/src/ui/public/assets/SvgIcon-DRYmw37Q.js +1 -0
  38. package/src/ui/public/assets/TemplateManager-BGEG9EkP.js +1 -0
  39. package/src/ui/public/assets/TemplateManager-DMuG4i3G.css +1 -0
  40. package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +1 -0
  41. package/src/ui/public/assets/UserInputNode-DO4QsmCx.js +1 -0
  42. package/src/ui/public/assets/WorkbenchView-B1BJhWHi.css +1 -0
  43. package/src/ui/public/assets/WorkbenchView-D4herpOg.js +51 -0
  44. package/src/ui/public/assets/_plugin-vue_export-helper-uGHPMZWt.js +19 -0
  45. package/src/ui/public/assets/configStore-DAZtECRW.js +1 -0
  46. package/src/ui/public/assets/element-plus-BJ5_9fFh.js +48 -0
  47. package/src/ui/public/assets/element-plus-C1PloHQu.css +1 -0
  48. package/src/ui/public/assets/flow-mindmap-9rLOb4W_.js +139 -0
  49. package/src/ui/public/assets/{gitStore-DMEDMZMk.js → gitStore-BQhrNfOL.js} +4 -4
  50. package/src/ui/public/assets/{index-CW3xs-af.css → index-B-c23Usy.css} +1 -1
  51. package/src/ui/public/assets/index-BM9Kz0P0.js +87 -0
  52. package/src/ui/public/assets/office-docx-BrfnjjUo.js +32 -0
  53. package/src/ui/public/assets/{office-excel-D-uYLKaA.js → office-excel-C08VoWqf.js} +1 -1
  54. package/src/ui/public/assets/office-pptx-Cs69NVuQ.js +74 -0
  55. package/src/ui/public/assets/{previewSandbox-B6Ud9g6a.css → previewSandbox-CgdwPcuW.css} +1 -1
  56. package/src/ui/public/assets/previewSandbox-D2nklUXY.js +0 -0
  57. package/src/ui/public/assets/socket-io-BFqQXqAp.js +1 -0
  58. package/src/ui/public/assets/{toolsStore-CpImRUhI.js → toolsStore-DX4sCU-U.js} +1 -1
  59. package/src/ui/public/assets/{useThemeObserver-CG8k8U18.js → useThemeObserver-chduV3hr.js} +1 -1
  60. package/src/ui/public/assets/{vendor-BIgZEtkx.js → vendor-xs0vOPEn.js} +46 -39
  61. package/src/ui/public/assets/vue-flow-DvidrbDz.js +39 -0
  62. package/src/ui/public/index.html +25 -25
  63. package/src/ui/server/routes/remoteRepos.js +73 -19
  64. package/src/ui/server/routes/remoteRepos.test.js +111 -1
  65. package/src/ui/server/routes/workbench/agentSessionStore.js +2 -2
  66. package/src/ui/server/routes/workbench/envContext.test.js +8 -8
  67. package/src/ui/server/routes/workbench/index.js +32 -736
  68. package/src/ui/server/routes/workbench/instructionStore.js +3 -113
  69. package/src/ui/server/routes/workbench/jobStore.js +2 -4
  70. package/src/ui/server/routes/workbench/jobStore.test.js +1 -1
  71. package/src/ui/server/routes/workbench/jsonParse.js +6 -143
  72. package/src/ui/server/routes/workbench/orchestratorStore.js +1 -8
  73. package/src/ui/server/routes/workbench/projectRegistry.js +6 -29
  74. package/src/ui/server/routes/workbench/projectRegistry.test.js +33 -56
  75. package/src/ui/server/routes/workbench/projectTool.js +0 -1
  76. package/src/ui/server/routes/workbench/projectTool.test.js +2 -3
  77. package/src/ui/server/routes/workbench/shared.js +2 -7
  78. package/src/ui/server/routes/workbench/taskRunner.js +18 -180
  79. package/src/ui/server/routes/workbench.js +4 -5
  80. package/src/ui/public/assets/AgentView-BV15UtEF.css +0 -1
  81. package/src/ui/public/assets/AgentView-C-AJo7KS.js +0 -5
  82. package/src/ui/public/assets/AppVersionBadge-B6R6eN2N.js +0 -2
  83. package/src/ui/public/assets/BranchSelector-Cj3kMWON.js +0 -1
  84. package/src/ui/public/assets/BranchSelector-T_rJm-JY.css +0 -1
  85. package/src/ui/public/assets/CommitForm-DpXuFG-F.js +0 -6
  86. package/src/ui/public/assets/CommitForm-Oij8R6P-.css +0 -1
  87. package/src/ui/public/assets/CommonDialog-BLPHDsn0.js +0 -1
  88. package/src/ui/public/assets/CommonDialog-C_RjF9Mp.css +0 -1
  89. package/src/ui/public/assets/EditorView-B1OnoXXf.js +0 -1
  90. package/src/ui/public/assets/EditorView-BgA-_8WV.css +0 -1
  91. package/src/ui/public/assets/FileDiffViewer-B2_ghtbv.css +0 -1
  92. package/src/ui/public/assets/FileDiffViewer-BQl0JcWd.js +0 -14
  93. package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +0 -1
  94. package/src/ui/public/assets/FlowExecutionViewer-BgEt5_Hz.js +0 -1
  95. package/src/ui/public/assets/FlowOrchestrationWorkspace-BAQDU9Jd.css +0 -1
  96. package/src/ui/public/assets/FlowOrchestrationWorkspace-CbLmsSF8.js +0 -21
  97. package/src/ui/public/assets/IconButton-CTwyZrv2.css +0 -1
  98. package/src/ui/public/assets/IconButton-aM3vaSIN.js +0 -1
  99. package/src/ui/public/assets/LogList-DlWwSZet.css +0 -1
  100. package/src/ui/public/assets/LogList-UjoVFZ-H.js +0 -7
  101. package/src/ui/public/assets/MindmapView-CS2t30Ko.css +0 -1
  102. package/src/ui/public/assets/MindmapView-D_04v0n4.js +0 -1
  103. package/src/ui/public/assets/MonitorView-BD1_uzq7.js +0 -1
  104. package/src/ui/public/assets/MonitorView-CphDOlQu.css +0 -1
  105. package/src/ui/public/assets/ProjectStartupButton-B7zvqRj_.css +0 -1
  106. package/src/ui/public/assets/ProjectStartupButton-BTYlVvzh.js +0 -1
  107. package/src/ui/public/assets/RemoteManagerDialog-20mpQMwu.js +0 -1
  108. package/src/ui/public/assets/RemoteManagerDialog-bpwaVAvX.css +0 -1
  109. package/src/ui/public/assets/RemoteRepoCard-CDykPlPj.css +0 -1
  110. package/src/ui/public/assets/RemoteRepoCard-DOrm17Xd.js +0 -1
  111. package/src/ui/public/assets/SourceMapView-DjYKJZLj.css +0 -1
  112. package/src/ui/public/assets/SourceMapView-nUU9bYPh.js +0 -3
  113. package/src/ui/public/assets/SvgIcon-LUynX3Iq.js +0 -1
  114. package/src/ui/public/assets/TemplateManager-Bo3Q7J9Y.js +0 -1
  115. package/src/ui/public/assets/TemplateManager-C0rm8xfC.css +0 -1
  116. package/src/ui/public/assets/UserInputNode-D5tSXiJz.js +0 -1
  117. package/src/ui/public/assets/UserInputNode-DDtvv9Iq.css +0 -1
  118. package/src/ui/public/assets/WorkbenchView-C_Gmzvj1.css +0 -1
  119. package/src/ui/public/assets/WorkbenchView-UGC334JL.js +0 -13
  120. package/src/ui/public/assets/_plugin-vue_export-helper-D-D9qkRz.js +0 -19
  121. package/src/ui/public/assets/configStore-BLRcYJdf.js +0 -1
  122. package/src/ui/public/assets/element-plus-BWbXh9mk.css +0 -1
  123. package/src/ui/public/assets/element-plus-DcZ-TVp6.js +0 -28
  124. package/src/ui/public/assets/flow-mindmap-D1VTkZh3.js +0 -139
  125. package/src/ui/public/assets/index-S0_gRyiO.js +0 -26
  126. package/src/ui/public/assets/office-docx-C0g5Oepv.js +0 -31
  127. package/src/ui/public/assets/office-pptx-De3fnc6M.js +0 -60
  128. package/src/ui/public/assets/previewSandbox-DXAxqDVu.js +0 -0
  129. package/src/ui/public/assets/socket-io-TBtw0msS.js +0 -1
  130. package/src/ui/public/assets/vue-flow-CPCFgenl.js +0 -39
  131. package/src/ui/server/routes/workbench/sessionStore.js +0 -126
@@ -12,17 +12,14 @@
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
  //
15
- // AI 指令文本持久化(生成指令 + 拆分子任务指令)。
16
- // 拆分自原 routes/workbench.js 57-158 行 + 958-998 行。
15
+ // AI 指令文本持久化(生成项目架构说明的指令)。
16
+ // 拆分自原 routes/workbench.js 57-158 行。
17
17
  //
18
- // 两个独立的指令文件:
19
- // - ai-instruction.json 生成项目架构说明的指令
20
- // - ai-subtask-instruction.json 拆分子任务的指令(支持 zh/en 多语言默认)
18
+ // 指令文件:ai-instruction.json
21
19
 
22
20
  import fsp from 'fs/promises';
23
21
  import {
24
22
  INSTRUCTION_FILE,
25
- SUBTASK_INSTRUCTION_FILE,
26
23
  ensureDataDir,
27
24
  nowIso,
28
25
  } from './shared.js';
@@ -47,91 +44,6 @@ export const DEFAULT_INSTRUCTION = `你是一名资深软件架构师。
47
44
  4. 语气专业、具体、面向接手这个项目的开发者。
48
45
  5. 只返回 JSON:{ "name": "项目名(建议:项目名+架构说明)", "summary": "架构说明正文" }。`;
49
46
 
50
- // ── 拆分子任务指令:默认系统指令(用户在 GUI 可编辑覆盖) ───────
51
- // 国际化:根据请求 Accept-Language 在 zh / en 之间选默认。
52
- // 中英两份都内置——用户保存到 ~/.zen-gitsync/ai-subtask-instruction.json 后覆盖。
53
- export const DEFAULT_SUBTASK_INSTRUCTION_ZH = `你是一名任务拆分助手。
54
-
55
- 【绝对禁止】
56
- - 不要进入任何"plan 模式"、"plan 阶段"或"先写 plan 再执行"。
57
- - 不要输出"我要进入 plan / 准备 / 探索 / 调研"之类的元叙述。
58
- - 不要写"Plan 文件已写入 xxx"或"等用户批准后开始"这种"声明意图"的话。
59
- - 你只做一件事:把用户给的任务拆成可独立执行的子任务清单,直接输出 JSON。中间任何"我要做什么"的描述都属于错误输出。
60
- - 你的输出会被另一个程序读取,该程序只看最终 JSON,所有非 JSON 文字会被丢弃——所以不要把"执行步骤"写进 analysis 段,只写在 JSON 的 desc 字段里。
61
-
62
- 【思考过程】
63
- 在给出 JSON 之前,请先在内部仔细思考(如果模型支持,把思考放在 reasoning 中;否则可以先输出一段较长的仔细分析,再输出 JSON)。分析要覆盖足够维度,不要简短一两句话就过:
64
- - 任务的真实目标是什么?用户提供的描述/图片/上下文里有哪些关键信息和隐藏诉求?
65
- - 涉及哪些技术栈、模块、文件、约束?是否有易被忽略的边界条件、风险点?
66
- - 自然的执行顺序是什么?哪些步骤是前置依赖?哪些可以并行?
67
- - 哪些步骤可能失败、需要单独验证?验证方式是什么?
68
- - 是否需要先做调研/读现有代码的步骤,再做修改?
69
-
70
- 【拆分原则】
71
- 1. 单一职责:每个子任务只做一件事,避免"做 A 和 B"。
72
- 2. 粒度适中:单个子任务应当能在一次会话里完成(既不要"实现整个登录功能"这么大,也不要"打印 hello"这么琐碎)。
73
- 3. 顺序合理:子任务按依赖关系和执行顺序排列(先准备、后实现、最后验证)。
74
- 4. 可验证:每个子任务都有明确的完成标志("输出文件 xxx"、"通过测试 yyy"、"控制台打印 zzz")。
75
- 5. 数量:不设上限。任务很简单时 1-2 个就够,复杂时按需拆成 10 个、20 个甚至更多子任务都行,直到覆盖所有可独立验证的步骤。
76
- 6. 描述具体:desc 字段要写清楚"要做什么、参考什么、产出什么",不要只是把 title 改写一遍。
77
- 7. 如果任务里附带了图片,必须基于图片的实际内容拆分(例如指出图片中的哪个区域、哪个元素需要改),而不是泛泛而谈。
78
-
79
- 【输出要求】
80
- 最后必须输出 JSON,结构:
81
- {
82
- "subtasks": [
83
- { "title": "子任务标题", "desc": "子任务的具体描述,包含要做什么、输入是什么、输出/验证标志是什么" }
84
- ]
85
- }
86
-
87
- JSON 要用 \`\`\`json ... \`\`\` 代码块包裹,前面可以有分析文字,但 JSON 必须完整、合法、可解析。`;
88
-
89
- export const DEFAULT_SUBTASK_INSTRUCTION_EN = `You are a task breakdown assistant.
90
-
91
- [ABSOLUTE PROHIBITIONS]
92
- - Do NOT enter any "plan mode" / "planning phase" / "I'll write a plan first, then execute".
93
- - Do NOT output meta-narration like "I'm entering plan / preparing / exploring / researching".
94
- - Do NOT write "Plan file written to xxx" or "waiting for user approval" style intent-declaration text.
95
- - You only do one thing: split the user's task into an independently executable subtask list and output JSON directly. Any "what I'm going to do" description in the middle is an error.
96
- - Your output is consumed by another program that only reads the final JSON. All non-JSON text is discarded — so put execution steps in the desc field of JSON, not in the analysis section.
97
-
98
- [Thinking process]
99
- Before producing JSON, think carefully (put your thoughts in reasoning if the model supports it; otherwise output a longer careful analysis first, then JSON). Cover enough dimensions — don't settle for a one-or-two-sentence summary:
100
- - What is the real goal? What key info and hidden needs are in the description / images / context?
101
- - Which stack, modules, files, constraints are involved? Any easily missed edge cases or risks?
102
- - What is the natural execution order? What blocks what? What can run in parallel?
103
- - Which steps may fail and need separate verification? How will you verify?
104
- - Should a research / read-existing-code step come before the changes?
105
-
106
- [Breakdown principles]
107
- 1. Single responsibility: each subtask does only one thing, avoid bundling "do A and B".
108
- 2. Right granularity: a subtask should finish in one Claude session (not as big as "implement the whole login flow", not as trivial as "print hello").
109
- 3. Sensible order: arrange subtasks by dependency / execution order (prepare first, implement, then verify).
110
- 4. Verifiable: every subtask has a clear completion signal (e.g. "produce file xxx", "pass test yyy", "log zzz to console").
111
- 5. Quantity: no hard limit. Very simple tasks need only 1-2; complex tasks can be split into 10, 20 or more subtasks until every independently verifiable step is covered.
112
- 6. Concrete desc: write what to do, what to reference, what to produce — don't just paraphrase the title.
113
- 7. If the task includes images, the breakdown must reference the actual image content (which region, which element to change), not just generic talk.
114
-
115
- [Output requirements]
116
- End with JSON, structure:
117
- {
118
- "subtasks": [
119
- { "title": "subtask title", "desc": "concrete description: what to do, what the input is, what the output / verification signal is" }
120
- ]
121
- }
122
-
123
- Wrap JSON in a \`\`\`json ... \`\`\` code block. Preceding analysis text is allowed, but the JSON must be complete, valid, and parseable.`;
124
-
125
- // 兼容旧引用:未保存指令文件时回退到 ZH 版本
126
- export const DEFAULT_SUBTASK_INSTRUCTION = DEFAULT_SUBTASK_INSTRUCTION_ZH;
127
-
128
- // 根据请求 Accept-Language 选默认(zh / en)
129
- export function pickDefaultSubtaskInstruction(req) {
130
- const al = String(req?.headers?.['accept-language'] || '').toLowerCase();
131
- if (al.startsWith('en')) return DEFAULT_SUBTASK_INSTRUCTION_EN;
132
- return DEFAULT_SUBTASK_INSTRUCTION_ZH;
133
- }
134
-
135
47
  // ── 生成指令持久化(~/.zen-gitsync/ai-instruction.json) ───────
136
48
  export async function readInstruction() {
137
49
  try {
@@ -151,25 +63,3 @@ export async function writeInstruction(instruction) {
151
63
  await fsp.writeFile(tmp, JSON.stringify({ instruction: text, updatedAt: nowIso() }, null, 2), 'utf-8');
152
64
  await fsp.rename(tmp, INSTRUCTION_FILE);
153
65
  }
154
-
155
- // ── 拆分子任务指令持久化 ─────────────────────────────────────
156
- // 支持传入 req:根据 Accept-Language 选默认(zh/en)
157
- export async function readSubtaskInstruction(req) {
158
- try {
159
- const buf = await fsp.readFile(SUBTASK_INSTRUCTION_FILE, 'utf-8');
160
- const obj = JSON.parse(buf);
161
- if (obj && typeof obj.instruction === 'string' && obj.instruction.trim()) {
162
- return obj.instruction;
163
- }
164
- } catch { /* 文件不存在或解析失败 */ }
165
- return pickDefaultSubtaskInstruction(req);
166
- }
167
-
168
- export async function writeSubtaskInstruction(instruction) {
169
- await ensureDataDir();
170
- // 写入时如果与当前 locale 默认一致,不写文件——这样前端"isDefault"判定永远准确
171
- const text = String(instruction || '').trim() || DEFAULT_SUBTASK_INSTRUCTION_ZH;
172
- const tmp = `${SUBTASK_INSTRUCTION_FILE}.tmp`;
173
- await fsp.writeFile(tmp, JSON.stringify({ instruction: text, updatedAt: nowIso() }, null, 2), 'utf-8');
174
- await fsp.rename(tmp, SUBTASK_INSTRUCTION_FILE);
175
- }
@@ -159,11 +159,10 @@ export function mergedJobs() {
159
159
  let jobsSaveTimer = null;
160
160
 
161
161
  // 序列化 job 落盘:剥离 child 引用(ChildProcess 无法 JSON.stringify),
162
- // 反范式 taskTitle/subTitle 方便管理页直接读
162
+ // 反范式 taskTitle 方便管理页直接读
163
163
  export function serializeJob(j, taskMap) {
164
164
  const { child, ...rest } = j;
165
165
  const t = taskMap ? taskMap.get(rest.taskId) : null;
166
- const sub = t && Array.isArray(t.subtasks) ? t.subtasks.find(s => s.id === rest.subId) : null;
167
166
  // size 是保留策略(maxSizeMB)的计价口径,工具调用也会占体积,必须算进去,
168
167
  // 否则一份全是工具流水的 job 会被当成"很小"而留下来。
169
168
  const toolCallsSize = Array.isArray(rest.toolCalls)
@@ -178,7 +177,6 @@ export function serializeJob(j, taskMap) {
178
177
  return {
179
178
  ...rest,
180
179
  taskTitle: t ? t.title : '',
181
- subTitle: sub ? sub.title : '',
182
180
  size
183
181
  };
184
182
  }
@@ -195,7 +193,7 @@ export function scheduleJobsSave() {
195
193
  // 立即落盘(终态调用):把 jobs Map 当前快照写到 jobs.json,然后跑 retention
196
194
  export async function flushJobsSaveNow() {
197
195
  if (jobsSaveTimer) { clearTimeout(jobsSaveTimer); jobsSaveTimer = null; }
198
- // 读 tasks.json 给落盘 job 反范式 taskTitle/subTitle——父任务被删后管理页仍可读
196
+ // 读 tasks.json 给落盘 job 反范式 taskTitle——父任务被删后管理页仍可读
199
197
  const tasksData = await readJson(TASKS_FILE, { tasks: [] });
200
198
  const taskMap = new Map((tasksData.tasks || []).map(t => [t.id, t]));
201
199
  // **必须并上磁盘上的历史 job 再写**:别的 g ui 进程跑的记录不在本进程内存里,
@@ -95,7 +95,7 @@ const diskJob = (id, taskId, status, extra = {}) => ({
95
95
  ...extra,
96
96
  });
97
97
 
98
- const simpleTask = (id) => ({ id, title: id, type: 'simple', subtasks: [], projectPath: 'c:\\ws\\a' });
98
+ const simpleTask = (id) => ({ id, title: id, type: 'simple', projectPath: 'c:\\ws\\a' });
99
99
 
100
100
  // ── 沙箱自检 ────────────────────────────────────────────────────────
101
101
  // 这条不是形式主义:DATA_DIR 一旦没落在沙箱里,下面每个用例都会去写用户真实的
@@ -12,19 +12,13 @@
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
  //
15
- // AI 拆分子任务 JSON 多级降级解析。
16
- // 拆分自原 routes/workbench.js 395-776 行。
15
+ // 从 LLM 输出里抠 JSON 的两个纯函数。
17
16
  //
18
- // 模型经常会犯几类格式错:在 desc 里用 ASCII 双引号引用术语 / 末尾留尾随逗号 /
19
- // 输出被 token 上限截断导致代码块未闭合。直接 JSON.parse 一旦失败就会让前端的
20
- // "确认入库" 按钮永远是 (0),用户看不到原因。这里按"越简单越优先"的顺序尝试:
21
- // ① ```json``` 代码块 / ```any``` 代码块 / 第一个完整 { ... } 范围
22
- // ② 把候选片段去掉尾随逗号 + 块/行注释 再 parse
23
- // ③ 用括号深度扫描,从开头找一个语法平衡的 { ... } 子串
24
- // ④ 启发式转义模型夹在字符串内部的未转义 ASCII 双引号
25
- // 任一步成功就返回 parsed,全部失败时返回最后一次 JSON.parse 的错误,
26
- // 用 parseStage 告知前端"模型输出哪一步崩了",并把原始 raw 一并回传。
27
-
17
+ // 模型经常把 JSON 包在 思考块、代码块或总结:{...}这类散文里,直接 JSON.parse 必崩。
18
+ // 这里提供两级:
19
+ // ① stripThinkingBlocks —— 先剥掉 思考块 与代码块围栏
20
+ // ② extractFirstJsonObject —— 在剩下的文本里定位第一个「平衡花括号对象」
21
+ // 上层的 llmClient 用它俩抠 JSON;抠不出来时由调用方决定怎么降级。
28
22
  // 剥离 LLM 输出的 思考块、```json``` 代码块围栏,以及首尾说明文字。
29
23
  // 原贪婪正则 /({[\s\S]*})/ 会把 思考块(内含未配对花括号或字符串)一起吞进 JSON.parse,
30
24
  // 在 DeepSeek 等会输出 推理链的模型上偶发 "Unterminated string in JSON"。
@@ -86,134 +80,3 @@ export function extractFirstJsonObject(content) {
86
80
  // 兜底:找不到平衡对象时返回原文(让上层 JSON.parse 报清晰错误)
87
81
  return s;
88
82
  }
89
-
90
- // 从字符串中提取首个语法平衡的 { ... } 子串。
91
- // 跟踪字符串字面量(含转义),避免把 desc 里的 } 当成结束。
92
- export function extractBalancedJson(text) {
93
- const s = String(text || '');
94
- const start = s.indexOf('{');
95
- if (start < 0) return '';
96
- let depth = 0;
97
- let inStr = false;
98
- let strCh = '';
99
- let esc = false;
100
- for (let i = start; i < s.length; i++) {
101
- const c = s[i];
102
- if (inStr) {
103
- if (esc) { esc = false; continue; }
104
- if (c === '\\') { esc = true; continue; }
105
- if (c === strCh) { inStr = false; }
106
- continue;
107
- }
108
- if (c === '"' || c === "'") { inStr = true; strCh = c; continue; }
109
- if (c === '{') depth++;
110
- else if (c === '}') {
111
- depth--;
112
- if (depth === 0) return s.slice(start, i + 1);
113
- }
114
- }
115
- return '';
116
- }
117
-
118
- // ④ 级降级:把字符串字面量内部出现的、未转义的 ASCII 双引号自动转义。
119
- // 实战中模型最常见的错误是 "desc": "...用户点击"保存"按钮..." 这种—
120
- // 中间的 "保存" 把外层字符串截断成两段,后面变成裸文本,JSON.parse 必崩。
121
- //
122
- // 启发式判断:扫描时若处于字符串中且遇到 ",往后看第一个非空白字符:
123
- // - 是 , } ] : 或文本结尾 → 这是真闭合,正常退出字符串
124
- // - 否则 → 是模型乱写的字面量引号,改写为 \" 并继续留在字符串里
125
- // 不依赖正则、不破坏已经转义的 \",对嵌套 / 多行字符串都安全。
126
- export function reescapeUnescapedQuotes(text) {
127
- const s = String(text || '');
128
- if (!s) return '';
129
- const out = [];
130
- let inStr = false;
131
- let strCh = '';
132
- let esc = false;
133
- for (let i = 0; i < s.length; i++) {
134
- const c = s[i];
135
- if (!inStr) {
136
- out.push(c);
137
- if (c === '"' || c === "'") { inStr = true; strCh = c; }
138
- continue;
139
- }
140
- // 字符串内部
141
- if (esc) { out.push(c); esc = false; continue; }
142
- if (c === '\\') { out.push(c); esc = true; continue; }
143
- if (c !== strCh) { out.push(c); continue; }
144
- // 遇到与开闭引号相同的字符——往后看下一个非空白
145
- let j = i + 1;
146
- while (j < s.length && (s[j] === ' ' || s[j] === '\t')) j++;
147
- const next = j < s.length ? s[j] : '';
148
- if (next === '' || next === ',' || next === '}' || next === ']'
149
- || next === ':' || next === '\n' || next === '\r') {
150
- // 真闭合
151
- out.push(c);
152
- inStr = false;
153
- strCh = '';
154
- } else {
155
- // 字面量裸引号——转义
156
- out.push('\\', c);
157
- }
158
- }
159
- return out.join('');
160
- }
161
-
162
- // AI 拆分子任务 JSON 多级降级解析主入口。
163
- // 返回 { parsed, parseError, parseStage }:
164
- // - parsed: 解析成功时的对象,失败时为 null
165
- // - parseError: 失败时的错误信息
166
- // - parseStage: 'empty' / '' (成功) / 'cleaned' / 'balanced' / 'reescaped' / 'failed'
167
- export function parseSubtaskJson(content) {
168
- let src = String(content || '');
169
- if (!src.trim()) {
170
- return { parsed: null, parseError: '模型未返回任何内容', parseStage: 'empty' };
171
- }
172
-
173
- // 预处理:剥离 deepseek-r1 / QwQ 等模型在 content 前输出的 <think>...</think> 思考块
174
- src = src.replace(/<think>[\s\S]*?<\/think>/gi, '').replace(/<think\s*\/>/gi, '').trim();
175
-
176
- const candidates = [];
177
- const fenced = src.match(/```json\s*([\s\S]*?)```/i) || src.match(/```\s*([\s\S]*?)```/);
178
- if (fenced) candidates.push(fenced[1]);
179
- // 平衡花括号扫描:避免贪婪正则把多段 JSON / 散落的 { } 全包进去
180
- const balancedFirst = extractBalancedJson(src);
181
- if (balancedFirst) candidates.push(balancedFirst);
182
- // 兜底:整段当 JSON 试
183
- candidates.push(src);
184
-
185
- let lastErr = null;
186
- for (const raw of candidates) {
187
- const txt = String(raw || '').trim();
188
- if (!txt) continue;
189
- // ① 直 parse
190
- try { return { parsed: JSON.parse(txt), parseError: '', parseStage: '' }; }
191
- catch (e) { lastErr = e; }
192
- // ② 清洗:去 //…/* */ 注释 + 尾随逗号
193
- const cleaned = txt
194
- .replace(/\/\*[\s\S]*?\*\//g, '')
195
- .replace(/(^|[^:"'\\])\/\/[^\n]*/g, '$1')
196
- .replace(/,(\s*[}\]])/g, '$1');
197
- try { return { parsed: JSON.parse(cleaned), parseError: '', parseStage: 'cleaned' }; }
198
- catch (e) { lastErr = e; }
199
- // ③ 平衡花括号扫描
200
- const balanced = extractBalancedJson(cleaned);
201
- if (balanced && balanced !== cleaned) {
202
- try { return { parsed: JSON.parse(balanced), parseError: '', parseStage: 'balanced' }; }
203
- catch (e) { lastErr = e; }
204
- }
205
- // ④ 启发式转义字符串内的未转义双引号
206
- const base = balanced || cleaned;
207
- const reescaped = reescapeUnescapedQuotes(base);
208
- if (reescaped && reescaped !== base) {
209
- try { return { parsed: JSON.parse(reescaped), parseError: '', parseStage: 'reescaped' }; }
210
- catch (e) { lastErr = e; }
211
- }
212
- }
213
-
214
- return {
215
- parsed: null,
216
- parseError: lastErr ? (lastErr.message || String(lastErr)) : '未能从模型输出中提取出 JSON',
217
- parseStage: 'failed'
218
- };
219
- }
@@ -261,8 +261,7 @@ export async function clearInstructions() {
261
261
  /**
262
262
  * 正在执行的执行体清单(看板左栏「执行监控」用)。
263
263
  *
264
- * 一行 = 一个活跃 job(不是任务):一个任务并行跑 3 个子任务就是三行,
265
- * 因为"谁在跑什么"只有到 job 粒度才说得清。job 只是执行进程,
264
+ * 一行 = 一个活跃 job(不是任务):job 只是执行进程,
266
265
  * 所以文案上叫「执行」而不是「Agent」—— 不要把正在跑的 claude 进程
267
266
  * 渲染成一个并不存在的"智能体集群"。
268
267
  *
@@ -274,15 +273,12 @@ export function buildRunningAgents({ jobs = [], tasks = [] } = {}) {
274
273
  .filter(j => j && (j.status === 'running' || j.status === 'pending'))
275
274
  .map(j => {
276
275
  const task = taskMap.get(j.taskId) || null;
277
- const subs = task && Array.isArray(task.subtasks) ? task.subtasks : [];
278
- const sub = subs.find(s => s && s.id === j.subId) || null;
279
276
  const projectPath = (task && task.projectPath) || '';
280
277
  return {
281
278
  jobId: j.id,
282
279
  taskId: j.taskId || null,
283
280
  subId: j.subId || null,
284
281
  taskTitle: (task && task.title) || '',
285
- subTitle: (sub && sub.title) || '',
286
282
  status: j.status,
287
283
  pid: j.pid || null,
288
284
  startedAt: j.startedAt || null,
@@ -317,14 +313,11 @@ export function buildActivityFeed({ jobs = [], tasks = [], instructions = [] } =
317
313
  for (const j of Array.isArray(jobs) ? jobs : []) {
318
314
  if (!j || !j.id) continue;
319
315
  const task = taskMap.get(j.taskId) || null;
320
- const subs = task && Array.isArray(task.subtasks) ? task.subtasks : [];
321
- const sub = subs.find(s => s && s.id === j.subId) || null;
322
316
  const base = {
323
317
  jobId: j.id,
324
318
  taskId: j.taskId || null,
325
319
  subId: j.subId || null,
326
320
  taskTitle: (task && task.title) || '',
327
- subTitle: (sub && sub.title) || '',
328
321
  jobStatus: j.status || '',
329
322
  pid: j.pid || null,
330
323
  ...projectOf(task),
@@ -127,30 +127,17 @@ export function latestJob(jobsForTask) {
127
127
  * 把一个任务推导到看板的四列之一。纯函数,单测覆盖。
128
128
  *
129
129
  * 口径(按优先级,逐条短路):
130
- * 1. 有 job 处于 running/pending,或子任务状态是 running → `doing`
131
- * 2. 有子任务且全部 done → `done`
132
- * 3. 有子任务但没全 done:
133
- * - 动过(有 done 的子任务 / 执行过 / 有 error 的子任务)→ `todo`
134
- * - 完全没动过 → `todo`
135
- * 4. 没子任务(简单任务,或还没拆分的复杂任务):
136
- * - 从没执行过 → `todo`
137
- * - 最近一条 job 是 done → `done`;其余终态(error/cancelled)→ `todo`
130
+ * 1. 有 job 处于 running/pending → `doing`
131
+ * 2. 从没执行过 → `todo`
132
+ * 3. 最近一条 job 是 done → `done`;其余终态(error/cancelled)→ `todo`
138
133
  *
139
134
  * error/cancelled 不再单列:列表示任务是否仍待处理,错误是这一轮执行的结果,
140
135
  * 具体错误继续通过卡片标记和任务详情展示。
141
136
  */
142
137
  export function deriveTaskColumn(task, jobsForTask = []) {
143
- const subs = Array.isArray(task && task.subtasks) ? task.subtasks : [];
144
138
  const jobs = Array.isArray(jobsForTask) ? jobsForTask : [];
145
139
 
146
140
  if (jobs.some(j => j && (j.status === 'running' || j.status === 'pending'))) return 'doing';
147
- if (subs.some(s => s && s.status === 'running')) return 'doing';
148
-
149
- if (subs.length > 0) {
150
- const doneCount = subs.filter(s => s && s.status === 'done').length;
151
- if (doneCount === subs.length) return 'done';
152
- return 'todo';
153
- }
154
141
 
155
142
  if (jobs.length === 0) return 'todo';
156
143
  const last = latestJob(jobs);
@@ -161,7 +148,7 @@ export function deriveTaskColumn(task, jobsForTask = []) {
161
148
  function emptyStats() {
162
149
  return {
163
150
  total: 0, todo: 0, doing: 0, done: 0,
164
- progress: 0, runningJobs: 0, errorSubtasks: 0, lastActiveAt: null,
151
+ progress: 0, runningJobs: 0, lastActiveAt: null,
165
152
  };
166
153
  }
167
154
 
@@ -191,9 +178,6 @@ export function summarizeProjectTasks(tasks, jobs) {
191
178
  const column = deriveTaskColumn(t, jobsForTask);
192
179
  agg.total += 1;
193
180
  agg[column] += 1;
194
- if (Array.isArray(t.subtasks)) {
195
- agg.errorSubtasks += t.subtasks.filter(s => s && s.status === 'error').length;
196
- }
197
181
  agg.lastActiveAt = laterOf(agg.lastActiveAt, t.updatedAt || t.createdAt);
198
182
  // 最后活跃时间也要把执行记录算进来:任务本身可能很久没改,但刚刚跑过
199
183
  for (const j of jobsForTask) {
@@ -316,26 +300,20 @@ export async function listProjects({
316
300
 
317
301
  /** 看板任务卡需要的精简字段(完整任务体在 /api/workbench/tasks,这里只给卡片用得到的) */
318
302
  export function decorateTaskForBoard(task, jobsForTask = []) {
319
- const subs = Array.isArray(task && task.subtasks) ? task.subtasks : [];
320
303
  const jobs = Array.isArray(jobsForTask) ? jobsForTask : [];
321
304
  const last = latestJob(jobs);
322
305
  return {
323
306
  id: task.id,
324
307
  title: task.title || '',
325
308
  desc: task.desc || '',
326
- type: task.type === 'simple' ? 'simple' : 'complex',
327
309
  projectPath: task.projectPath || '',
328
310
  column: deriveTaskColumn(task, jobs),
329
- subtaskCount: subs.length,
330
- subtaskDoneCount: subs.filter(s => s && s.status === 'done').length,
331
- subtaskErrorCount: subs.filter(s => s && s.status === 'error').length,
332
311
  attachmentCount: Array.isArray(task.attachments) ? task.attachments.length : 0,
333
312
  runningJobs: jobs.filter(j => j && (j.status === 'running' || j.status === 'pending')).length,
334
313
  lastJobStatus: last ? last.status : null,
335
314
  // 最近一条 job 的结束时间 = 这张卡片"跑完"的时刻。
336
315
  // 看板的「已完成」列要按完成时间倒序排(最新完成的在最上边),而 updatedAt 撑不起这个排序:
337
- // 简单任务的执行完成只写 jobs.json,tasks.json 里的 updatedAt 一直停在创建时间
338
- //(见 taskRunner.persistTaskAfterRun —— 它只有复杂任务才走),
316
+ // 执行完成只写 jobs.json,tasks.json 里的 updatedAt 一直停在创建/编辑时间,
339
317
  // 于是十几张卡片的时间会全是创建时刻,"最新完成的"根本排不出来。
340
318
  lastJobEndedAt: last ? (last.endedAt || last.startedAt || null) : null,
341
319
  createdAt: task.createdAt || null,
@@ -368,7 +346,6 @@ export function trimJobForDetail(job) {
368
346
  id: job.id || '',
369
347
  subId: job.subId || '',
370
348
  title: job.title || '',
371
- subTitle: job.subTitle || '',
372
349
  status: job.status || '',
373
350
  pid: typeof job.pid === 'number' ? job.pid : null,
374
351
  startedAt: job.startedAt || null,
@@ -384,7 +361,7 @@ export function trimJobForDetail(job) {
384
361
  /**
385
362
  * 组装单个任务的详情。纯函数,单测覆盖。
386
363
  *
387
- * @param {object} task 完整任务(含 subtasks / attachments)
364
+ * @param {object} task 完整任务(含 attachments)
388
365
  * @param {Array} jobsForTask 属于该任务的 job(顺序不限,内部会按时间排)
389
366
  * @param {number} recentJobLimit 最多回带几条 job 明细(默认 10)
390
367
  * @returns {object|null} task 缺失时返回 null,由路由转成 404