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.
- package/package.json +2 -1
- package/src/ui/public/assets/AgentView-DJVwX8Er.js +5 -0
- package/src/ui/public/assets/AgentView-vvVhbIQ5.css +1 -0
- package/src/ui/public/assets/{AppVersionBadge-D5tJzJfa.css → AppVersionBadge-CJrDr7GV.css} +1 -1
- package/src/ui/public/assets/AppVersionBadge-rNwRlkEK.js +2 -0
- package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +1 -0
- package/src/ui/public/assets/BranchSelector-D2JJINuw.js +1 -0
- package/src/ui/public/assets/CommitForm-CQ0K8Yy2.css +1 -0
- package/src/ui/public/assets/CommitForm-CayXdf3-.js +6 -0
- package/src/ui/public/assets/CommonDialog-DF5Vzvi8.js +1 -0
- package/src/ui/public/assets/CommonDialog-jP0E9mco.css +1 -0
- package/src/ui/public/assets/EditorView-Bv-3PWwF.js +1 -0
- package/src/ui/public/assets/EditorView-DZMLvQds.css +1 -0
- package/src/ui/public/assets/FileDiffViewer-B9EkuKdF.css +1 -0
- package/src/ui/public/assets/FileDiffViewer-CUsML3pW.js +15 -0
- package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-n07dZgnE.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CdG8HRux.js +21 -0
- package/src/ui/public/assets/IconButton-GxK-nCF7.js +1 -0
- package/src/ui/public/assets/IconButton-tjkOjzkC.css +1 -0
- package/src/ui/public/assets/LogList-BlJplIi1.css +1 -0
- package/src/ui/public/assets/LogList-CpcpXaIZ.js +7 -0
- package/src/ui/public/assets/MindmapView-BHVJ6OzY.js +2 -0
- package/src/ui/public/assets/MindmapView-BQTqx842.css +1 -0
- package/src/ui/public/assets/MonitorView-DetlFFXq.css +1 -0
- package/src/ui/public/assets/MonitorView-IpfJRJaK.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-DM2gYw48.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-BauAeH2w.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-DLPnI8Tr.js +4 -0
- package/src/ui/public/assets/RemoteRepoCard-CG-jARTe.css +1 -0
- package/src/ui/public/assets/RemoteRepoCard-D804CRwk.js +7 -0
- package/src/ui/public/assets/SourceMapView-CN4Hkyp4.css +1 -0
- package/src/ui/public/assets/SourceMapView-Ct8J5WA4.js +3 -0
- package/src/ui/public/assets/{SvgIcon-BQ6bR7l0.css → SvgIcon-CDNsk4-Q.css} +1 -1
- package/src/ui/public/assets/SvgIcon-DRYmw37Q.js +1 -0
- package/src/ui/public/assets/TemplateManager-BGEG9EkP.js +1 -0
- package/src/ui/public/assets/TemplateManager-DMuG4i3G.css +1 -0
- package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +1 -0
- package/src/ui/public/assets/UserInputNode-DO4QsmCx.js +1 -0
- package/src/ui/public/assets/WorkbenchView-B1BJhWHi.css +1 -0
- package/src/ui/public/assets/WorkbenchView-D4herpOg.js +51 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-uGHPMZWt.js +19 -0
- package/src/ui/public/assets/configStore-DAZtECRW.js +1 -0
- package/src/ui/public/assets/element-plus-BJ5_9fFh.js +48 -0
- package/src/ui/public/assets/element-plus-C1PloHQu.css +1 -0
- package/src/ui/public/assets/flow-mindmap-9rLOb4W_.js +139 -0
- package/src/ui/public/assets/{gitStore-DMEDMZMk.js → gitStore-BQhrNfOL.js} +4 -4
- package/src/ui/public/assets/{index-CW3xs-af.css → index-B-c23Usy.css} +1 -1
- package/src/ui/public/assets/index-BM9Kz0P0.js +87 -0
- package/src/ui/public/assets/office-docx-BrfnjjUo.js +32 -0
- package/src/ui/public/assets/{office-excel-D-uYLKaA.js → office-excel-C08VoWqf.js} +1 -1
- package/src/ui/public/assets/office-pptx-Cs69NVuQ.js +74 -0
- package/src/ui/public/assets/{previewSandbox-B6Ud9g6a.css → previewSandbox-CgdwPcuW.css} +1 -1
- package/src/ui/public/assets/previewSandbox-D2nklUXY.js +0 -0
- package/src/ui/public/assets/socket-io-BFqQXqAp.js +1 -0
- package/src/ui/public/assets/{toolsStore-CpImRUhI.js → toolsStore-DX4sCU-U.js} +1 -1
- package/src/ui/public/assets/{useThemeObserver-CG8k8U18.js → useThemeObserver-chduV3hr.js} +1 -1
- package/src/ui/public/assets/{vendor-BIgZEtkx.js → vendor-xs0vOPEn.js} +46 -39
- package/src/ui/public/assets/vue-flow-DvidrbDz.js +39 -0
- package/src/ui/public/index.html +25 -25
- package/src/ui/server/routes/remoteRepos.js +73 -19
- package/src/ui/server/routes/remoteRepos.test.js +111 -1
- package/src/ui/server/routes/workbench/agentSessionStore.js +2 -2
- package/src/ui/server/routes/workbench/envContext.test.js +8 -8
- package/src/ui/server/routes/workbench/index.js +32 -736
- package/src/ui/server/routes/workbench/instructionStore.js +3 -113
- package/src/ui/server/routes/workbench/jobStore.js +2 -4
- package/src/ui/server/routes/workbench/jobStore.test.js +1 -1
- package/src/ui/server/routes/workbench/jsonParse.js +6 -143
- package/src/ui/server/routes/workbench/orchestratorStore.js +1 -8
- package/src/ui/server/routes/workbench/projectRegistry.js +6 -29
- package/src/ui/server/routes/workbench/projectRegistry.test.js +33 -56
- package/src/ui/server/routes/workbench/projectTool.js +0 -1
- package/src/ui/server/routes/workbench/projectTool.test.js +2 -3
- package/src/ui/server/routes/workbench/shared.js +2 -7
- package/src/ui/server/routes/workbench/taskRunner.js +18 -180
- package/src/ui/server/routes/workbench.js +4 -5
- package/src/ui/public/assets/AgentView-BV15UtEF.css +0 -1
- package/src/ui/public/assets/AgentView-C-AJo7KS.js +0 -5
- package/src/ui/public/assets/AppVersionBadge-B6R6eN2N.js +0 -2
- package/src/ui/public/assets/BranchSelector-Cj3kMWON.js +0 -1
- package/src/ui/public/assets/BranchSelector-T_rJm-JY.css +0 -1
- package/src/ui/public/assets/CommitForm-DpXuFG-F.js +0 -6
- package/src/ui/public/assets/CommitForm-Oij8R6P-.css +0 -1
- package/src/ui/public/assets/CommonDialog-BLPHDsn0.js +0 -1
- package/src/ui/public/assets/CommonDialog-C_RjF9Mp.css +0 -1
- package/src/ui/public/assets/EditorView-B1OnoXXf.js +0 -1
- package/src/ui/public/assets/EditorView-BgA-_8WV.css +0 -1
- package/src/ui/public/assets/FileDiffViewer-B2_ghtbv.css +0 -1
- package/src/ui/public/assets/FileDiffViewer-BQl0JcWd.js +0 -14
- package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-BgEt5_Hz.js +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BAQDU9Jd.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CbLmsSF8.js +0 -21
- package/src/ui/public/assets/IconButton-CTwyZrv2.css +0 -1
- package/src/ui/public/assets/IconButton-aM3vaSIN.js +0 -1
- package/src/ui/public/assets/LogList-DlWwSZet.css +0 -1
- package/src/ui/public/assets/LogList-UjoVFZ-H.js +0 -7
- package/src/ui/public/assets/MindmapView-CS2t30Ko.css +0 -1
- package/src/ui/public/assets/MindmapView-D_04v0n4.js +0 -1
- package/src/ui/public/assets/MonitorView-BD1_uzq7.js +0 -1
- package/src/ui/public/assets/MonitorView-CphDOlQu.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-B7zvqRj_.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-BTYlVvzh.js +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-20mpQMwu.js +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-bpwaVAvX.css +0 -1
- package/src/ui/public/assets/RemoteRepoCard-CDykPlPj.css +0 -1
- package/src/ui/public/assets/RemoteRepoCard-DOrm17Xd.js +0 -1
- package/src/ui/public/assets/SourceMapView-DjYKJZLj.css +0 -1
- package/src/ui/public/assets/SourceMapView-nUU9bYPh.js +0 -3
- package/src/ui/public/assets/SvgIcon-LUynX3Iq.js +0 -1
- package/src/ui/public/assets/TemplateManager-Bo3Q7J9Y.js +0 -1
- package/src/ui/public/assets/TemplateManager-C0rm8xfC.css +0 -1
- package/src/ui/public/assets/UserInputNode-D5tSXiJz.js +0 -1
- package/src/ui/public/assets/UserInputNode-DDtvv9Iq.css +0 -1
- package/src/ui/public/assets/WorkbenchView-C_Gmzvj1.css +0 -1
- package/src/ui/public/assets/WorkbenchView-UGC334JL.js +0 -13
- package/src/ui/public/assets/_plugin-vue_export-helper-D-D9qkRz.js +0 -19
- package/src/ui/public/assets/configStore-BLRcYJdf.js +0 -1
- package/src/ui/public/assets/element-plus-BWbXh9mk.css +0 -1
- package/src/ui/public/assets/element-plus-DcZ-TVp6.js +0 -28
- package/src/ui/public/assets/flow-mindmap-D1VTkZh3.js +0 -139
- package/src/ui/public/assets/index-S0_gRyiO.js +0 -26
- package/src/ui/public/assets/office-docx-C0g5Oepv.js +0 -31
- package/src/ui/public/assets/office-pptx-De3fnc6M.js +0 -60
- package/src/ui/public/assets/previewSandbox-DXAxqDVu.js +0 -0
- package/src/ui/public/assets/socket-io-TBtw0msS.js +0 -1
- package/src/ui/public/assets/vue-flow-CPCFgenl.js +0 -39
- 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
|
|
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
|
|
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
|
|
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',
|
|
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
|
-
//
|
|
16
|
-
// 拆分自原 routes/workbench.js 395-776 行。
|
|
15
|
+
// 从 LLM 输出里抠 JSON 的两个纯函数。
|
|
17
16
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
|
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
|
|
131
|
-
* 2.
|
|
132
|
-
* 3.
|
|
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,
|
|
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
|
-
//
|
|
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 完整任务(含
|
|
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
|