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
|
@@ -17,16 +17,15 @@
|
|
|
17
17
|
// 历史背景:本文件曾是 3539 行的巨型文件,包含所有顶层工具函数 + 45 个路由端点。
|
|
18
18
|
// 2026-06-29 按业务域拆分为 workbench/ 子目录的 9 个模块:
|
|
19
19
|
// - shared.js 常量 + 通用工具 (nowIso, genId, readJson, writeJson, interpolate)
|
|
20
|
-
// - jsonParse.js
|
|
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 任务执行引擎 (
|
|
26
|
+
// - taskRunner.js 任务执行引擎 (runSingleSubtask —— 一次任务 = 一次会话)
|
|
28
27
|
//
|
|
29
|
-
// 本文件(index.js)只做 registerWorkbenchRoutes
|
|
28
|
+
// 本文件(index.js)只做 registerWorkbenchRoutes 入口聚合,按业务域分节注册路由。
|
|
30
29
|
// 错误处理:
|
|
31
30
|
// - 非 SSE 路由统一用 asyncRoute 包装,handler 内部不再写 try/catch
|
|
32
31
|
// - 异常自动走 asyncRoute → 全局 errorHandler 中间件(server/index.js 注册)
|
|
@@ -48,16 +47,14 @@ import {
|
|
|
48
47
|
JOBS_FILE,
|
|
49
48
|
ORCHESTRATOR_FILE,
|
|
50
49
|
IMAGES_DIR,
|
|
51
|
-
SUBTASK_INSTRUCTION_FILE,
|
|
52
50
|
MAX_IMAGE_BYTES,
|
|
53
|
-
|
|
51
|
+
MAX_ATTACHMENTS_PER_TASK,
|
|
54
52
|
MAX_DEFAULT_PROMPT_CHARS,
|
|
55
53
|
readJson,
|
|
56
54
|
writeJson,
|
|
57
55
|
nowIso,
|
|
58
56
|
genId,
|
|
59
57
|
} from './shared.js';
|
|
60
|
-
import { parseSubtaskJson } from './jsonParse.js';
|
|
61
58
|
import { callLlmJson, callLlmJsonWithRetry, callLlmStream } from './llmClient.js';
|
|
62
59
|
import {
|
|
63
60
|
findSubProjects,
|
|
@@ -78,15 +75,10 @@ import {
|
|
|
78
75
|
findStagingFile,
|
|
79
76
|
cleanupDispatchStaging,
|
|
80
77
|
} from './attachmentUtils.js';
|
|
81
|
-
import { sessionStore } from './sessionStore.js';
|
|
82
78
|
import {
|
|
83
79
|
DEFAULT_INSTRUCTION,
|
|
84
|
-
DEFAULT_SUBTASK_INSTRUCTION_ZH,
|
|
85
|
-
pickDefaultSubtaskInstruction,
|
|
86
80
|
readInstruction,
|
|
87
81
|
writeInstruction,
|
|
88
|
-
readSubtaskInstruction,
|
|
89
|
-
writeSubtaskInstruction,
|
|
90
82
|
} from './instructionStore.js';
|
|
91
83
|
import {
|
|
92
84
|
jobs,
|
|
@@ -104,15 +96,8 @@ import {
|
|
|
104
96
|
mergedJobs,
|
|
105
97
|
} from './jobStore.js';
|
|
106
98
|
import {
|
|
107
|
-
launchClaudeInNewWindow,
|
|
108
99
|
normalizeTaskExecutor,
|
|
109
|
-
runTaskQueue,
|
|
110
100
|
runSingleSubtask,
|
|
111
|
-
syncSubToCancelled,
|
|
112
|
-
persistTaskAfterRun,
|
|
113
|
-
collectPriorOutputs,
|
|
114
|
-
collectPriorOutputsUpTo,
|
|
115
|
-
waitProcessExit,
|
|
116
101
|
setEnvContextProvider,
|
|
117
102
|
} from './taskRunner.js';
|
|
118
103
|
import {
|
|
@@ -137,8 +122,6 @@ import {
|
|
|
137
122
|
buildRunningAgents,
|
|
138
123
|
} from './orchestratorStore.js';
|
|
139
124
|
|
|
140
|
-
const { genSessionId, read: readSessionFile, write: writeSessionFile, delete: deleteSessionFile, listMeta: listSessionsMeta, enforceRetention: enforceSessionsRetention } = sessionStore;
|
|
141
|
-
|
|
142
125
|
/**
|
|
143
126
|
* 解析本次执行用哪个执行器:body.executor 显式指定 > 配置里的全局默认 > 'claude'。
|
|
144
127
|
* 配置读失败一律回落 'claude' —— 执行器选不出来不该挡住任务本身能跑。
|
|
@@ -341,463 +324,7 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
341
324
|
}));
|
|
342
325
|
|
|
343
326
|
// ════════════════════════════════════════════════════════════════════════
|
|
344
|
-
// §3.
|
|
345
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
346
|
-
app.get('/api/workbench/tasks/ai-subtask-instruction', asyncRoute(async (req, res) => {
|
|
347
|
-
const def = pickDefaultSubtaskInstruction(req);
|
|
348
|
-
const instruction = await readSubtaskInstruction(req);
|
|
349
|
-
res.json({ success: true, instruction, isDefault: instruction === def });
|
|
350
|
-
}));
|
|
351
|
-
|
|
352
|
-
app.put('/api/workbench/tasks/ai-subtask-instruction', asyncRoute(async (req, res) => {
|
|
353
|
-
const text = req.body && typeof req.body.instruction === 'string'
|
|
354
|
-
? req.body.instruction.trim()
|
|
355
|
-
: '';
|
|
356
|
-
if (!text) throw new HttpError(400, '指令不能为空');
|
|
357
|
-
if (text.length > 500000) throw new HttpError(413, '指令过长(最多 500000 字符)');
|
|
358
|
-
// 如果保存的文本正好等于当前 locale 的默认——不写文件,保持 fallback 行为
|
|
359
|
-
const def = pickDefaultSubtaskInstruction(req);
|
|
360
|
-
if (text === def) {
|
|
361
|
-
try { await fsp.unlink(SUBTASK_INSTRUCTION_FILE); } catch { /* 已不存在 */ }
|
|
362
|
-
return res.json({ success: true, isDefault: true });
|
|
363
|
-
}
|
|
364
|
-
await writeSubtaskInstruction(text);
|
|
365
|
-
res.json({ success: true, isDefault: false });
|
|
366
|
-
}));
|
|
367
|
-
|
|
368
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
369
|
-
// §4. AI 拆分子任务(SSE 流式)— 保留 try/catch,错误以 SSE 帧发出
|
|
370
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
371
|
-
app.post('/api/workbench/tasks/ai-split-subtasks', async (req, res) => {
|
|
372
|
-
const title = String(req.body?.title || '').trim();
|
|
373
|
-
const desc = String(req.body?.desc || '').trim();
|
|
374
|
-
const taskId = String(req.body?.taskId || '').trim();
|
|
375
|
-
const promptId = String(req.body?.promptId || '').trim();
|
|
376
|
-
const customUserBlock = typeof req.body?.customUserBlock === 'string' ? req.body.customUserBlock : '';
|
|
377
|
-
if (!title) {
|
|
378
|
-
return res.status(400).json({ success: false, error: '任务标题不能为空' });
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// 建立 SSE
|
|
382
|
-
res.set({
|
|
383
|
-
'Content-Type': 'text/event-stream',
|
|
384
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
385
|
-
'Connection': 'keep-alive',
|
|
386
|
-
'X-Accel-Buffering': 'no'
|
|
387
|
-
});
|
|
388
|
-
res.flushHeaders?.();
|
|
389
|
-
const send = (obj) => {
|
|
390
|
-
try { res.write(`data: ${JSON.stringify(obj)}\n\n`); } catch {}
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
const abortController = new AbortController();
|
|
394
|
-
let finished = false;
|
|
395
|
-
// 客户端真实断开:监听 socket close
|
|
396
|
-
const onSocketClose = () => {
|
|
397
|
-
if (!finished) abortController.abort();
|
|
398
|
-
};
|
|
399
|
-
if (req.socket) req.socket.once('close', onSocketClose);
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
let model;
|
|
403
|
-
try {
|
|
404
|
-
if (!configManager) throw new Error('configManager 不可用');
|
|
405
|
-
const rawConfig = await configManager.readRawConfigFile();
|
|
406
|
-
const models = Array.isArray(rawConfig.models) ? rawConfig.models : [];
|
|
407
|
-
model = models.find(m => m.isDefault) || models[0];
|
|
408
|
-
} catch (err) {
|
|
409
|
-
send({ type: 'error', error: '读取 AI 配置失败: ' + err.message });
|
|
410
|
-
finished = true;
|
|
411
|
-
return res.end();
|
|
412
|
-
}
|
|
413
|
-
if (!model) {
|
|
414
|
-
send({ type: 'error', error: '未配置 AI 模型,请先在通用设置中添加模型' });
|
|
415
|
-
finished = true;
|
|
416
|
-
return res.end();
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const userInstruction = await readSubtaskInstruction(req);
|
|
420
|
-
const projectPath = typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '';
|
|
421
|
-
const projectName = projectPath ? path.basename(projectPath) : '(未指定项目)';
|
|
422
|
-
const manifestHint = await detectProjectManifest(projectPath);
|
|
423
|
-
|
|
424
|
-
// 取绑定的预置模板(promptId)
|
|
425
|
-
let templateBlock = '';
|
|
426
|
-
if (promptId) {
|
|
427
|
-
try {
|
|
428
|
-
const promptData = await readJson(PROMPTS_FILE, { prompts: [] });
|
|
429
|
-
const p = (promptData.prompts || []).find(x => x.id === promptId);
|
|
430
|
-
if (p && p.content) {
|
|
431
|
-
templateBlock = `\n\n## 子任务执行模板(每个拆出的子任务最终会被这套模板包裹后送给 claude 执行;拆分时请确保子任务能让模板里的 {{sub.title}} / {{sub.desc}} 等变量填得有意义)\n模板名:${p.name || '(未命名)'}\n---\n${p.content}\n---`;
|
|
432
|
-
}
|
|
433
|
-
} catch { /* 模板读取失败不影响拆分 */ }
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// 取任务附件
|
|
437
|
-
let attachmentBlock = '';
|
|
438
|
-
const imageDataUrls = [];
|
|
439
|
-
if (taskId) {
|
|
440
|
-
try {
|
|
441
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
442
|
-
const task = (data.tasks || []).find(t => t.id === taskId);
|
|
443
|
-
const atts = Array.isArray(task?.attachments) ? task.attachments : [];
|
|
444
|
-
if (atts.length > 0) {
|
|
445
|
-
const lines = [];
|
|
446
|
-
for (let i = 0; i < atts.length; i++) {
|
|
447
|
-
const a = atts[i];
|
|
448
|
-
if (!a || !a.absolutePath) continue;
|
|
449
|
-
lines.push(` ${i + 1}. [${a.mimeType || 'application/octet-stream'}] ${a.absolutePath}`);
|
|
450
|
-
if (isImageExt(a.ext)) {
|
|
451
|
-
try {
|
|
452
|
-
const buf = await fsp.readFile(a.absolutePath);
|
|
453
|
-
const mime = a.mimeType || 'image/png';
|
|
454
|
-
imageDataUrls.push(`data:${mime};base64,${buf.toString('base64')}`);
|
|
455
|
-
} catch { /* 文件丢失就跳过这张图 */ }
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
if (lines.length > 0) {
|
|
459
|
-
const imgNote = imageDataUrls.length > 0
|
|
460
|
-
? `(其中 ${imageDataUrls.length} 张图片已随消息一并发送,请直接基于图片内容拆分)`
|
|
461
|
-
: '';
|
|
462
|
-
attachmentBlock = `\n\n## 任务附件${imgNote}\n${lines.join('\n')}`;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
} catch { /* 没拿到附件不影响拆分 */ }
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const userBlock = customUserBlock.trim() ? customUserBlock : `${userInstruction}
|
|
469
|
-
|
|
470
|
-
---
|
|
471
|
-
|
|
472
|
-
## 待拆分的任务
|
|
473
|
-
标题:${title}
|
|
474
|
-
${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateBlock}
|
|
475
|
-
|
|
476
|
-
## 项目上下文(仅供参考,便于拆分时考虑项目特性)
|
|
477
|
-
- 项目名称:${projectName}
|
|
478
|
-
- 项目根目录:${projectPath || '(未指定)'}
|
|
479
|
-
- 主要 manifest:${manifestHint || '(未识别到)'}
|
|
480
|
-
|
|
481
|
-
请先仔细分析(可以放在 reasoning 中或直接写出来)。仔细分析指:列出 5 个维度——任务真实目标 / 关键技术栈与边界 / 自然执行顺序 / 风险点与可能失败步骤 / 是否需要前置调研——不要简短一两句话就过;分析过后再给出 JSON。JSON 用 \`\`\`json ... \`\`\` 包裹:
|
|
482
|
-
{
|
|
483
|
-
"subtasks": [
|
|
484
|
-
{ "title": "子任务标题", "desc": "具体描述" }
|
|
485
|
-
]
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
**JSON 输出严格要求**(不遵守会导致解析失败、用户无法入库):
|
|
489
|
-
1. title 和 desc 内如需引用术语 / 页面名 / 状态名,**必须使用中文引号「」或『』**,禁止使用 ASCII 双引号、单引号或反引号,否则会破坏外层 JSON 结构。
|
|
490
|
-
2. JSON 中不允许尾随逗号(最后一个元素后面不能跟逗号)。
|
|
491
|
-
3. JSON 中不允许写注释。
|
|
492
|
-
4. 所有字符串字段必须用 ASCII 双引号包裹,字符串内部如有换行用 \\n 转义。`;
|
|
493
|
-
|
|
494
|
-
// 先把 prompt 元信息推给前端
|
|
495
|
-
send({ type: 'meta', prompt: { system: userInstruction, user: userBlock } });
|
|
496
|
-
|
|
497
|
-
// 流式调用 LLM
|
|
498
|
-
const { content, aborted } = await callLlmStream(
|
|
499
|
-
model,
|
|
500
|
-
userBlock,
|
|
501
|
-
(delta) => {
|
|
502
|
-
if (delta.thinking) send({ type: 'thinking', delta: delta.thinking });
|
|
503
|
-
if (delta.content) send({ type: 'content', delta: delta.content });
|
|
504
|
-
},
|
|
505
|
-
{ maxTokens: 32000, timeoutMs: 600000, images: imageDataUrls, signal: abortController.signal }
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
if (aborted) {
|
|
509
|
-
send({ type: 'error', error: '已取消' });
|
|
510
|
-
finished = true;
|
|
511
|
-
return res.end();
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const { parsed, parseError, parseStage } = parseSubtaskJson(content);
|
|
515
|
-
const list = Array.isArray(parsed?.subtasks) ? parsed.subtasks : [];
|
|
516
|
-
const subtasks = list
|
|
517
|
-
.map(s => ({
|
|
518
|
-
title: String(s?.title || '').trim(),
|
|
519
|
-
desc: String(s?.desc || '').trim()
|
|
520
|
-
}))
|
|
521
|
-
.filter(s => s.title);
|
|
522
|
-
|
|
523
|
-
send({ type: 'done', subtasks, raw: content, parseError, parseStage });
|
|
524
|
-
finished = true;
|
|
525
|
-
res.end();
|
|
526
|
-
} catch (err) {
|
|
527
|
-
send({ type: 'error', error: 'AI 拆分失败: ' + (err?.message || String(err)) });
|
|
528
|
-
finished = true;
|
|
529
|
-
res.end();
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
534
|
-
// §5. AI 拆分预览(不调 LLM,只返回拼好的 prompt)
|
|
535
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
536
|
-
app.post('/api/workbench/tasks/ai-split-preview', asyncRoute(async (req, res) => {
|
|
537
|
-
const title = String(req.body?.title || '').trim();
|
|
538
|
-
const desc = String(req.body?.desc || '').trim();
|
|
539
|
-
const taskId = String(req.body?.taskId || '').trim();
|
|
540
|
-
const promptId = String(req.body?.promptId || '').trim();
|
|
541
|
-
if (!title) throw new HttpError(400, '任务标题不能为空');
|
|
542
|
-
|
|
543
|
-
const userInstruction = await readSubtaskInstruction(req);
|
|
544
|
-
const projectPath = typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '';
|
|
545
|
-
const projectName = projectPath ? path.basename(projectPath) : '(未指定项目)';
|
|
546
|
-
const manifestHint = await detectProjectManifest(projectPath);
|
|
547
|
-
|
|
548
|
-
let templateBlock = '';
|
|
549
|
-
if (promptId) {
|
|
550
|
-
try {
|
|
551
|
-
const promptData = await readJson(PROMPTS_FILE, { prompts: [] });
|
|
552
|
-
const p = (promptData.prompts || []).find(x => x.id === promptId);
|
|
553
|
-
if (p && p.content) {
|
|
554
|
-
templateBlock = `\n\n## 子任务执行模板(每个拆出的子任务最终会被这套模板包裹后送给 claude 执行;拆分时请确保子任务能让模板里的 {{sub.title}} / {{sub.desc}} 等变量填得有意义)\n模板名:${p.name || '(未命名)'}\n---\n${p.content}\n---`;
|
|
555
|
-
}
|
|
556
|
-
} catch { /* 模板读取失败不影响预览 */ }
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// 附件:PDF 预提取全文,图片只计数(preview 阶段不需要 image_data_url)
|
|
560
|
-
let attachmentBlock = '';
|
|
561
|
-
let imageCount = 0;
|
|
562
|
-
if (taskId) {
|
|
563
|
-
try {
|
|
564
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
565
|
-
const task = (data.tasks || []).find(t => t.id === taskId);
|
|
566
|
-
const atts = Array.isArray(task?.attachments) ? task.attachments : [];
|
|
567
|
-
if (atts.length > 0) {
|
|
568
|
-
imageCount = atts.filter(a => a && isImageExt(a.ext)).length;
|
|
569
|
-
const result = await buildAttachmentBlock(atts, { withImageData: false });
|
|
570
|
-
attachmentBlock = result.block;
|
|
571
|
-
}
|
|
572
|
-
} catch { /* 没拿到附件不影响预览 */ }
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const userBlock = `${userInstruction}
|
|
576
|
-
|
|
577
|
-
---
|
|
578
|
-
|
|
579
|
-
## 待拆分的任务
|
|
580
|
-
标题:${title}
|
|
581
|
-
${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateBlock}
|
|
582
|
-
|
|
583
|
-
## 项目上下文(仅供参考,便于拆分时考虑项目特性)
|
|
584
|
-
- 项目名称:${projectName}
|
|
585
|
-
- 项目根目录:${projectPath || '(未指定)'}
|
|
586
|
-
- 主要 manifest:${manifestHint || '(未识别到)'}
|
|
587
|
-
|
|
588
|
-
请先仔细分析(可以放在 reasoning 中或直接写出来)。仔细分析指:列出 5 个维度——任务真实目标 / 关键技术栈与边界 / 自然执行顺序 / 风险点与可能失败步骤 / 是否需要前置调研——不要简短一两句话就过;分析过后再给出 JSON。JSON 用 \`\`\`json ... \`\`\` 包裹:
|
|
589
|
-
{
|
|
590
|
-
"subtasks": [
|
|
591
|
-
{ "title": "子任务标题", "desc": "具体描述" }
|
|
592
|
-
]
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
**JSON 输出严格要求**(不遵守会导致解析失败、用户无法入库):
|
|
596
|
-
1. title 和 desc 内如需引用术语 / 页面名 / 状态名,**必须使用中文引号「」或『』**,禁止使用 ASCII 双引号、单引号或反引号,否则会破坏外层 JSON 结构。
|
|
597
|
-
2. JSON 中不允许尾随逗号(最后一个元素后面不能跟逗号)。
|
|
598
|
-
3. JSON 中不允许写注释。
|
|
599
|
-
4. 所有字符串字段必须用 ASCII 双引号包裹,字符串内部如有换行用 \\n 转义。`;
|
|
600
|
-
|
|
601
|
-
res.json({
|
|
602
|
-
success: true,
|
|
603
|
-
system: userInstruction,
|
|
604
|
-
user: userBlock,
|
|
605
|
-
hasImages: imageCount > 0,
|
|
606
|
-
imageCount
|
|
607
|
-
});
|
|
608
|
-
}));
|
|
609
|
-
|
|
610
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
611
|
-
// §6. 解析子任务 JSON(前端用户手改后调)
|
|
612
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
613
|
-
app.post('/api/workbench/tasks/parse-subtasks', asyncRoute(async (req, res) => {
|
|
614
|
-
const raw = String(req.body?.raw || '');
|
|
615
|
-
const { parsed, parseError, parseStage } = parseSubtaskJson(raw);
|
|
616
|
-
const list = Array.isArray(parsed?.subtasks) ? parsed.subtasks : [];
|
|
617
|
-
const subtasks = list
|
|
618
|
-
.map(s => ({
|
|
619
|
-
title: String(s?.title || '').trim(),
|
|
620
|
-
desc: String(s?.desc || '').trim()
|
|
621
|
-
}))
|
|
622
|
-
.filter(s => s.title);
|
|
623
|
-
res.json({ success: true, subtasks, parseError, parseStage });
|
|
624
|
-
}));
|
|
625
|
-
|
|
626
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
627
|
-
// §7. AI 对话拆分(多轮 SSE)— 保留 try/catch
|
|
628
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
629
|
-
app.post('/api/workbench/tasks/ai-chat-split', async (req, res) => {
|
|
630
|
-
const userMessage = String(req.body?.userMessage || '').trim();
|
|
631
|
-
const sessionIdInput = String(req.body?.sessionId || '').trim();
|
|
632
|
-
const title = String(req.body?.title || '').trim();
|
|
633
|
-
const desc = String(req.body?.desc || '').trim();
|
|
634
|
-
const taskId = String(req.body?.taskId || '').trim();
|
|
635
|
-
const promptId = String(req.body?.promptId || '').trim();
|
|
636
|
-
|
|
637
|
-
// SSE 头
|
|
638
|
-
res.set({
|
|
639
|
-
'Content-Type': 'text/event-stream',
|
|
640
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
641
|
-
'Connection': 'keep-alive',
|
|
642
|
-
'X-Accel-Buffering': 'no'
|
|
643
|
-
});
|
|
644
|
-
res.flushHeaders?.();
|
|
645
|
-
const send = (obj) => {
|
|
646
|
-
try { res.write(`data: ${JSON.stringify(obj)}\n\n`); } catch {}
|
|
647
|
-
};
|
|
648
|
-
const abortController = new AbortController();
|
|
649
|
-
let finished = false;
|
|
650
|
-
if (req.socket) req.socket.once('close', () => { if (!finished) abortController.abort(); });
|
|
651
|
-
|
|
652
|
-
try {
|
|
653
|
-
// 加载或新建 session
|
|
654
|
-
let session, isNew = false;
|
|
655
|
-
if (sessionIdInput) {
|
|
656
|
-
try {
|
|
657
|
-
session = await readSessionFile(sessionIdInput);
|
|
658
|
-
} catch (err) {
|
|
659
|
-
if (err.statusCode === 404) {
|
|
660
|
-
send({ type: 'error', error: '会话不存在' });
|
|
661
|
-
return res.end();
|
|
662
|
-
}
|
|
663
|
-
throw err;
|
|
664
|
-
}
|
|
665
|
-
} else {
|
|
666
|
-
if (!title) {
|
|
667
|
-
send({ type: 'error', error: '新建会话时必须提供 title' });
|
|
668
|
-
return res.end();
|
|
669
|
-
}
|
|
670
|
-
session = {
|
|
671
|
-
version: 1,
|
|
672
|
-
sessionId: genSessionId(),
|
|
673
|
-
title, desc, taskId, promptId,
|
|
674
|
-
createdAt: nowIso(),
|
|
675
|
-
updatedAt: nowIso(),
|
|
676
|
-
messages: [],
|
|
677
|
-
latestSubtasks: [],
|
|
678
|
-
latestRaw: '',
|
|
679
|
-
latestParseStage: ''
|
|
680
|
-
};
|
|
681
|
-
isNew = true;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
if (!userMessage) {
|
|
685
|
-
send({ type: 'error', error: '消息内容不能为空' });
|
|
686
|
-
return res.end();
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// 拼 system message(只在第一轮)
|
|
690
|
-
if (session.messages.length === 0) {
|
|
691
|
-
const userInstruction = await readSubtaskInstruction(req);
|
|
692
|
-
const ctxLines = [];
|
|
693
|
-
if (title) ctxLines.push(`【任务标题】${title}`);
|
|
694
|
-
if (desc) ctxLines.push(`【任务描述】${desc}`);
|
|
695
|
-
const sysContent = ctxLines.length
|
|
696
|
-
? `${userInstruction}\n\n${ctxLines.join('\n')}`
|
|
697
|
-
: userInstruction;
|
|
698
|
-
session.messages.push({ role: 'system', content: sysContent });
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// 追加 user message
|
|
702
|
-
session.messages.push({ role: 'user', content: userMessage });
|
|
703
|
-
|
|
704
|
-
// 推 meta + user_echo
|
|
705
|
-
send({
|
|
706
|
-
type: 'meta',
|
|
707
|
-
sessionId: session.sessionId,
|
|
708
|
-
isNew,
|
|
709
|
-
prompt: { system: session.messages[0].content, user: userMessage }
|
|
710
|
-
});
|
|
711
|
-
send({ type: 'user_echo', userMessage });
|
|
712
|
-
|
|
713
|
-
// 加载 model
|
|
714
|
-
let model;
|
|
715
|
-
try {
|
|
716
|
-
if (!configManager) throw new Error('configManager 不可用');
|
|
717
|
-
const rawConfig = await configManager.readRawConfigFile();
|
|
718
|
-
const models = Array.isArray(rawConfig.models) ? rawConfig.models : [];
|
|
719
|
-
model = models.find(m => m.isDefault) || models[0];
|
|
720
|
-
} catch (err) {
|
|
721
|
-
send({ type: 'error', error: '读取 AI 配置失败: ' + err.message });
|
|
722
|
-
finished = true;
|
|
723
|
-
return res.end();
|
|
724
|
-
}
|
|
725
|
-
if (!model) {
|
|
726
|
-
send({ type: 'error', error: '未配置 AI 模型' });
|
|
727
|
-
finished = true;
|
|
728
|
-
return res.end();
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// 流式调用(messages 模式)
|
|
732
|
-
const { content, aborted } = await callLlmStream(
|
|
733
|
-
model,
|
|
734
|
-
session.messages,
|
|
735
|
-
(delta) => {
|
|
736
|
-
if (delta.thinking) send({ type: 'thinking', delta: delta.thinking });
|
|
737
|
-
if (delta.content) send({ type: 'content', delta: delta.content });
|
|
738
|
-
},
|
|
739
|
-
{ maxTokens: 32000, timeoutMs: 600000, signal: abortController.signal }
|
|
740
|
-
);
|
|
741
|
-
|
|
742
|
-
if (aborted) {
|
|
743
|
-
session.updatedAt = nowIso();
|
|
744
|
-
await writeSessionFile(session.sessionId, session).catch(() => {});
|
|
745
|
-
enforceSessionsRetention().catch(() => {});
|
|
746
|
-
send({ type: 'error', error: '已取消' });
|
|
747
|
-
finished = true;
|
|
748
|
-
return res.end();
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// 解析 + 写盘
|
|
752
|
-
const { parsed, parseError, parseStage } = parseSubtaskJson(content);
|
|
753
|
-
const subtasks = Array.isArray(parsed?.subtasks)
|
|
754
|
-
? parsed.subtasks
|
|
755
|
-
.map(s => ({
|
|
756
|
-
title: String(s?.title || '').trim(),
|
|
757
|
-
desc: String(s?.desc || '').trim()
|
|
758
|
-
}))
|
|
759
|
-
.filter(s => s.title)
|
|
760
|
-
: [];
|
|
761
|
-
|
|
762
|
-
session.messages.push({ role: 'assistant', content });
|
|
763
|
-
session.latestSubtasks = subtasks;
|
|
764
|
-
session.latestRaw = content;
|
|
765
|
-
session.latestParseStage = parseStage;
|
|
766
|
-
session.updatedAt = nowIso();
|
|
767
|
-
await writeSessionFile(session.sessionId, session);
|
|
768
|
-
enforceSessionsRetention().catch(() => {});
|
|
769
|
-
|
|
770
|
-
send({ type: 'done', subtasks, raw: content, parseError, parseStage });
|
|
771
|
-
finished = true;
|
|
772
|
-
res.end();
|
|
773
|
-
} catch (err) {
|
|
774
|
-
send({ type: 'error', error: '对话拆分失败: ' + (err?.message || String(err)) });
|
|
775
|
-
finished = true;
|
|
776
|
-
res.end();
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
781
|
-
// §8. AI 对话拆分会话:列表 / 详情 / 删除
|
|
782
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
783
|
-
app.get('/api/workbench/tasks/ai-chat-sessions', asyncRoute(async (_req, res) => {
|
|
784
|
-
const sessions = await listSessionsMeta();
|
|
785
|
-
res.json({ success: true, sessions });
|
|
786
|
-
}));
|
|
787
|
-
|
|
788
|
-
app.get('/api/workbench/tasks/ai-chat-sessions/:sessionId', asyncRoute(async (req, res) => {
|
|
789
|
-
// readSessionFile 抛带 statusCode 的 Error(400/404),让 asyncRoute 透传
|
|
790
|
-
const session = await readSessionFile(req.params.sessionId);
|
|
791
|
-
res.json({ success: true, session });
|
|
792
|
-
}));
|
|
793
|
-
|
|
794
|
-
app.delete('/api/workbench/tasks/ai-chat-sessions/:sessionId', asyncRoute(async (req, res) => {
|
|
795
|
-
await deleteSessionFile(req.params.sessionId);
|
|
796
|
-
res.json({ success: true });
|
|
797
|
-
}));
|
|
798
|
-
|
|
799
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
800
|
-
// §9. SSE 事件流(订阅 job/sub/task 更新)
|
|
327
|
+
// §3. SSE 事件流(订阅 job/sub/task 更新)
|
|
801
328
|
// ════════════════════════════════════════════════════════════════════════
|
|
802
329
|
app.get('/api/workbench/events', asyncRoute(async (req, res) => {
|
|
803
330
|
res.set({
|
|
@@ -954,10 +481,8 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
954
481
|
}));
|
|
955
482
|
|
|
956
483
|
app.post('/api/workbench/tasks', asyncRoute(async (req, res) => {
|
|
957
|
-
const { id, title, desc, promptId,
|
|
484
|
+
const { id, title, desc, promptId, simpleOverride } = req.body || {};
|
|
958
485
|
const safeTitle = typeof title === 'string' ? title.trim() : '';
|
|
959
|
-
const taskType = rawType === 'simple' ? 'simple' : 'complex';
|
|
960
|
-
const sequential = rawSequential === false ? false : true;
|
|
961
486
|
const safeOverride = typeof simpleOverride === 'string' ? simpleOverride.slice(0, 8000) : '';
|
|
962
487
|
// 显式指定的归属项目(多项目编排台传选中项目);不传则沿用当前项目。
|
|
963
488
|
// 只影响**新建**,更新分支一律保留任务原有的 projectPath(否则编辑一次就会把任务挪到当前项目去)。
|
|
@@ -974,28 +499,7 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
974
499
|
title: safeTitle,
|
|
975
500
|
desc: desc || '',
|
|
976
501
|
promptId: promptId || null,
|
|
977
|
-
|
|
978
|
-
sequential: taskType === 'complex' ? sequential : true,
|
|
979
|
-
simpleOverride: taskType === 'simple' ? safeOverride : '',
|
|
980
|
-
subtasks: Array.isArray(subtasks) ? subtasks.map(s => ({
|
|
981
|
-
id: s.id || genId(),
|
|
982
|
-
title: s.title || '',
|
|
983
|
-
desc: s.desc || '',
|
|
984
|
-
status: s.status || 'todo',
|
|
985
|
-
promptOverride: s.promptOverride || '',
|
|
986
|
-
error: typeof s.error === 'string' ? s.error : undefined,
|
|
987
|
-
errorAt: typeof s.errorAt === 'string' ? s.errorAt : undefined,
|
|
988
|
-
attachments: Array.isArray(s.attachments) ? s.attachments.map(a => ({
|
|
989
|
-
id: a.id,
|
|
990
|
-
originalName: a.originalName,
|
|
991
|
-
mimeType: a.mimeType,
|
|
992
|
-
size: a.size,
|
|
993
|
-
ext: a.ext,
|
|
994
|
-
storedName: a.storedName,
|
|
995
|
-
absolutePath: a.absolutePath,
|
|
996
|
-
createdAt: a.createdAt
|
|
997
|
-
})) : (tasks[i].subtasks.find(x => x.id === s.id)?.attachments || [])
|
|
998
|
-
})) : tasks[i].subtasks,
|
|
502
|
+
simpleOverride: safeOverride,
|
|
999
503
|
updatedAt: now
|
|
1000
504
|
};
|
|
1001
505
|
await writeJson(TASKS_FILE, { tasks });
|
|
@@ -1006,20 +510,8 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1006
510
|
title: safeTitle,
|
|
1007
511
|
desc: desc || '',
|
|
1008
512
|
promptId: promptId || null,
|
|
1009
|
-
|
|
1010
|
-
sequential: taskType === 'complex' ? sequential : true,
|
|
1011
|
-
simpleOverride: taskType === 'simple' ? safeOverride : '',
|
|
513
|
+
simpleOverride: safeOverride,
|
|
1012
514
|
projectPath: bodyProjectPath || currentProjectPath || '',
|
|
1013
|
-
subtasks: Array.isArray(subtasks) ? subtasks.map(s => ({
|
|
1014
|
-
id: s.id || genId(),
|
|
1015
|
-
title: s.title || '',
|
|
1016
|
-
desc: s.desc || '',
|
|
1017
|
-
status: s.status || 'todo',
|
|
1018
|
-
promptOverride: s.promptOverride || '',
|
|
1019
|
-
error: typeof s.error === 'string' ? s.error : undefined,
|
|
1020
|
-
errorAt: typeof s.errorAt === 'string' ? s.errorAt : undefined,
|
|
1021
|
-
attachments: Array.isArray(s.attachments) ? s.attachments : []
|
|
1022
|
-
})) : [],
|
|
1023
515
|
status: 'todo',
|
|
1024
516
|
createdAt: now,
|
|
1025
517
|
updatedAt: now
|
|
@@ -1052,16 +544,6 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1052
544
|
} catch (e) {
|
|
1053
545
|
logger.warn(`[workbench] failed to remove task image dir for ${taskId}: ${e.message}`);
|
|
1054
546
|
}
|
|
1055
|
-
if (Array.isArray(task.subtasks)) {
|
|
1056
|
-
for (const sub of task.subtasks) {
|
|
1057
|
-
if (!sub || !sub.id) continue;
|
|
1058
|
-
try {
|
|
1059
|
-
await fsp.rm(path.join(IMAGES_DIR, sub.id), { recursive: true, force: true });
|
|
1060
|
-
} catch (e) {
|
|
1061
|
-
logger.warn(`[workbench] failed to remove sub image dir for ${sub.id}: ${e.message}`);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
547
|
|
|
1066
548
|
const tasks = allTasks.filter(t => t.id !== taskId);
|
|
1067
549
|
await writeJson(TASKS_FILE, { tasks });
|
|
@@ -1069,50 +551,17 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1069
551
|
}));
|
|
1070
552
|
|
|
1071
553
|
// ════════════════════════════════════════════════════════════════════════
|
|
1072
|
-
// §12.
|
|
554
|
+
// §12. 执行任务(每个任务都是一次会话)
|
|
1073
555
|
// ════════════════════════════════════════════════════════════════════════
|
|
1074
556
|
app.post('/api/workbench/tasks/:id/run', asyncRoute(async (req, res) => {
|
|
1075
557
|
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1076
558
|
const task = (data.tasks || []).find(t => t.id === req.params.id);
|
|
1077
559
|
if (!task) throw new HttpError(404, '任务不存在');
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
// 异步执行,立即返回
|
|
1084
|
-
res.json({ success: true, message: '已开始执行' });
|
|
1085
|
-
runTaskQueue(task, repoPath, '', { executor }).catch(err => {
|
|
1086
|
-
publish('task:error', { taskId: task.id, error: err.message });
|
|
1087
|
-
});
|
|
1088
|
-
}));
|
|
1089
|
-
|
|
1090
|
-
app.post('/api/workbench/tasks/:id/run-from', asyncRoute(async (req, res) => {
|
|
1091
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1092
|
-
const task = (data.tasks || []).find(t => t.id === req.params.id);
|
|
1093
|
-
if (!task) throw new HttpError(404, '任务不存在');
|
|
1094
|
-
if (!task.subtasks || task.subtasks.length === 0) {
|
|
1095
|
-
throw new HttpError(400, '任务没有子任务');
|
|
1096
|
-
}
|
|
1097
|
-
const startSubIndex = Number(req.body?.startSubIndex);
|
|
1098
|
-
if (!Number.isInteger(startSubIndex) || startSubIndex < 0 || startSubIndex >= task.subtasks.length) {
|
|
1099
|
-
throw new HttpError(400, 'startSubIndex 越界');
|
|
1100
|
-
}
|
|
1101
|
-
const repoPath = resolveTaskRepoPath(task, typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '');
|
|
1102
|
-
const executor = await resolveExecutor(req.body?.executor);
|
|
1103
|
-
res.json({ success: true, message: `已从第 ${startSubIndex + 1} 个子任务开始执行` });
|
|
1104
|
-
runTaskQueue(task, repoPath, '', { fromIndex: startSubIndex, executor }).catch(err => {
|
|
1105
|
-
publish('task:error', { taskId: task.id, error: err.message });
|
|
1106
|
-
});
|
|
1107
|
-
}));
|
|
1108
|
-
|
|
1109
|
-
app.post('/api/workbench/tasks/:id/run-simple', asyncRoute(async (req, res) => {
|
|
1110
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1111
|
-
const task = (data.tasks || []).find(t => t.id === req.params.id);
|
|
1112
|
-
if (!task) throw new HttpError(404, '任务不存在');
|
|
1113
|
-
if (task.type !== 'simple') {
|
|
1114
|
-
throw new HttpError(400, '该任务不是简单任务,请使用普通执行接口');
|
|
1115
|
-
}
|
|
560
|
+
// 兜底:磁盘上没有 running 状态,但还有 job 在跑(可能是别的 g ui 起的),也拦一下。
|
|
561
|
+
// 先刷磁盘再取快照,否则另一个实例正在跑的这个任务会被重复起一份。
|
|
562
|
+
await refreshJobsFromDisk();
|
|
563
|
+
const liveJob = snapshotJobs().find(j => j.taskId === task.id && (j.status === 'running' || j.status === 'pending'));
|
|
564
|
+
if (liveJob) throw new HttpError(400, '该任务已有正在执行的 job');
|
|
1116
565
|
const repoPath = resolveTaskRepoPath(task, typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '');
|
|
1117
566
|
const executor = await resolveExecutor(req.body?.executor);
|
|
1118
567
|
const virtualSub = {
|
|
@@ -1123,43 +572,12 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1123
572
|
promptOverride: task.simpleOverride || '',
|
|
1124
573
|
attachments: Array.isArray(task.attachments) ? task.attachments : []
|
|
1125
574
|
};
|
|
1126
|
-
res.json({ success: true, message: '
|
|
1127
|
-
runSingleSubtask(task, virtualSub, repoPath, '',
|
|
575
|
+
res.json({ success: true, message: '已开始执行任务' });
|
|
576
|
+
runSingleSubtask(task, virtualSub, repoPath, '', { executor }).catch(err => {
|
|
1128
577
|
publish('task:error', { taskId: task.id, error: err.message });
|
|
1129
578
|
});
|
|
1130
579
|
}));
|
|
1131
580
|
|
|
1132
|
-
app.post('/api/workbench/subtasks/:id/run', asyncRoute(async (req, res) => {
|
|
1133
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1134
|
-
const subId = req.params.id;
|
|
1135
|
-
let foundTask = null;
|
|
1136
|
-
let foundSub = null;
|
|
1137
|
-
for (const t of (data.tasks || [])) {
|
|
1138
|
-
if (!Array.isArray(t.subtasks)) continue;
|
|
1139
|
-
const s = t.subtasks.find(x => x.id === subId);
|
|
1140
|
-
if (s) { foundTask = t; foundSub = s; break; }
|
|
1141
|
-
}
|
|
1142
|
-
if (!foundTask || !foundSub) throw new HttpError(404, '子任务不存在');
|
|
1143
|
-
if (foundSub.status === 'running') throw new HttpError(400, '该子任务正在执行中');
|
|
1144
|
-
// 兜底:即便磁盘状态是 todo,如果还有 job 在跑(可能是别的 g ui 起的),也拦一下。
|
|
1145
|
-
// 先刷磁盘再取快照,否则另一个实例正在跑的这个 sub 会被重复起一份。
|
|
1146
|
-
await refreshJobsFromDisk();
|
|
1147
|
-
const liveJob = snapshotJobs().find(j => j.subId === subId && (j.status === 'running' || j.status === 'pending'));
|
|
1148
|
-
if (liveJob) throw new HttpError(400, '该子任务已有正在执行的 job');
|
|
1149
|
-
const repoPath = resolveTaskRepoPath(foundTask, typeof getCurrentProjectPath === 'function' ? getCurrentProjectPath() : '');
|
|
1150
|
-
const executor = await resolveExecutor(req.body?.executor);
|
|
1151
|
-
res.json({ success: true, message: `已开始执行子任务:${foundSub.title || subId}` });
|
|
1152
|
-
(async () => {
|
|
1153
|
-
try {
|
|
1154
|
-
const priorOutputs = await collectPriorOutputs(foundTask, foundSub);
|
|
1155
|
-
await runSingleSubtask(foundTask, foundSub, repoPath, '', priorOutputs, { executor });
|
|
1156
|
-
await persistTaskAfterRun(foundTask);
|
|
1157
|
-
} catch (err) {
|
|
1158
|
-
publish('task:error', { taskId: foundTask.id, error: err.message });
|
|
1159
|
-
}
|
|
1160
|
-
})();
|
|
1161
|
-
}));
|
|
1162
|
-
|
|
1163
581
|
// ════════════════════════════════════════════════════════════════════════
|
|
1164
582
|
// §13. Job 查询(兜底,SSE 断了也能拉)
|
|
1165
583
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -1193,8 +611,6 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1193
611
|
job.error = '用户已停止执行';
|
|
1194
612
|
job.endedAt = nowIso();
|
|
1195
613
|
publish('job:update', { ...job }); // 浅拷贝避免序列化 child 引用
|
|
1196
|
-
// 同步把 sub 也置 cancelled
|
|
1197
|
-
syncSubToCancelled(job).catch(err => logger.warn('[workbench] syncSubToCancelled failed:', err.message));
|
|
1198
614
|
// 终态:fire-and-forget 同步落盘
|
|
1199
615
|
flushJobsSaveNow().catch(err => logger.warn('[workbench] jobs save failed:', err.message));
|
|
1200
616
|
const child = job.child;
|
|
@@ -1236,7 +652,6 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1236
652
|
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1237
653
|
const task = (data.tasks || []).find(t => t.id === prevJob.taskId);
|
|
1238
654
|
if (!task) throw new HttpError(404, '所属任务不存在');
|
|
1239
|
-
if (task.type !== 'simple') throw new HttpError(400, '仅支持简单任务的续接');
|
|
1240
655
|
// 计算续接轮次号
|
|
1241
656
|
const subIdPrefix = `${task.id}__simple`;
|
|
1242
657
|
let maxRound = 0;
|
|
@@ -1266,7 +681,7 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1266
681
|
res.json({ success: true, message: '已加入续接队列' });
|
|
1267
682
|
// 执行器必须沿用上一轮 job 的 agent:claude 的 --resume 和 opencode 的 --session
|
|
1268
683
|
// 互不认对方的会话 id,串了执行器续接必然失败。
|
|
1269
|
-
runSingleSubtask(task, virtualSub, repoPath, '',
|
|
684
|
+
runSingleSubtask(task, virtualSub, repoPath, '', {
|
|
1270
685
|
resumeSessionId: prevJob.claudeSessionId,
|
|
1271
686
|
executor: normalizeTaskExecutor(prevJob.agent)
|
|
1272
687
|
})
|
|
@@ -1295,7 +710,7 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1295
710
|
if (status && j.status !== status) return false;
|
|
1296
711
|
if (taskId && j.taskId !== taskId) return false;
|
|
1297
712
|
if (term) {
|
|
1298
|
-
const hay = `${j.title || ''} ${j.taskTitle || ''}
|
|
713
|
+
const hay = `${j.title || ''} ${j.taskTitle || ''}`.toLowerCase();
|
|
1299
714
|
if (!hay.includes(term)) return false;
|
|
1300
715
|
}
|
|
1301
716
|
return true;
|
|
@@ -1419,31 +834,11 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1419
834
|
jobsData.jobs = jobsData.jobs.filter(j => j.taskId !== taskId);
|
|
1420
835
|
if (jobsData.jobs.length !== before) await writeJson(JOBS_FILE, jobsData);
|
|
1421
836
|
}
|
|
1422
|
-
// 2)
|
|
1423
|
-
const tasksData = await readJson(TASKS_FILE, { tasks: [] });
|
|
1424
|
-
const task = (tasksData.tasks || []).find(t => t.id === taskId);
|
|
1425
|
-
if (!task) {
|
|
1426
|
-
return res.json({ success: true, removedJobs: removedJobIds.length, resetSubs: 0, message: '任务不存在,仅清空 job' });
|
|
1427
|
-
}
|
|
1428
|
-
let resetSubs = 0;
|
|
1429
|
-
if (Array.isArray(task.subtasks)) {
|
|
1430
|
-
for (const s of task.subtasks) {
|
|
1431
|
-
if (s.status !== 'todo') {
|
|
1432
|
-
s.status = 'todo';
|
|
1433
|
-
resetSubs++;
|
|
1434
|
-
if (s.error) delete s.error;
|
|
1435
|
-
publish('sub:update', { taskId, sub: s });
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
task.updatedAt = nowIso();
|
|
1439
|
-
await writeJson(TASKS_FILE, tasksData);
|
|
1440
|
-
publish('task:update', task);
|
|
1441
|
-
}
|
|
837
|
+
// 2) 不用再改任务本体 —— 看板列完全由 job 推导,job 没了自然回到「待执行」
|
|
1442
838
|
res.json({
|
|
1443
839
|
success: true,
|
|
1444
840
|
removedJobs: removedJobIds.length,
|
|
1445
|
-
|
|
1446
|
-
message: `已清空 ${removedJobIds.length} 条执行记录,${resetSubs} 个子任务重置为待执行`
|
|
841
|
+
message: `已清空 ${removedJobIds.length} 条执行记录`
|
|
1447
842
|
});
|
|
1448
843
|
}));
|
|
1449
844
|
|
|
@@ -1475,7 +870,6 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1475
870
|
if (!task) {
|
|
1476
871
|
return res.json({ success: true, removedJobs: removedJobIds.length, message: '任务不存在,仅清空 job' });
|
|
1477
872
|
}
|
|
1478
|
-
const removedSubCount = Array.isArray(task.subtasks) ? task.subtasks.length : 0;
|
|
1479
873
|
const removedAttCount = Array.isArray(task.attachments) ? task.attachments.length : 0;
|
|
1480
874
|
const hadDesc = !!(task.desc && task.desc.length > 0);
|
|
1481
875
|
const hadPrompt = !!task.promptId;
|
|
@@ -1485,7 +879,6 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1485
879
|
const newTask = { id: task.id, title: task.title, status: preservedStatus };
|
|
1486
880
|
if (preservedProjectPath) newTask.projectPath = preservedProjectPath;
|
|
1487
881
|
if (preservedCreatedAt) newTask.createdAt = preservedCreatedAt;
|
|
1488
|
-
newTask.subtasks = [];
|
|
1489
882
|
newTask.attachments = [];
|
|
1490
883
|
newTask.updatedAt = nowIso();
|
|
1491
884
|
for (const k of Object.keys(task)) delete task[k];
|
|
@@ -1495,57 +888,13 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1495
888
|
res.json({
|
|
1496
889
|
success: true,
|
|
1497
890
|
removedJobs: removedJobIds.length,
|
|
1498
|
-
removedSubs: removedSubCount,
|
|
1499
891
|
removedAttachments: removedAttCount,
|
|
1500
892
|
clearedDesc: hadDesc,
|
|
1501
893
|
clearedPrompt: hadPrompt,
|
|
1502
|
-
message:
|
|
894
|
+
message: `已清空:${removedAttCount ? '删除 ' + removedAttCount + ' 个附件' : '无附件'}${hadDesc ? '、任务描述' : ''},任务标题保留`
|
|
1503
895
|
});
|
|
1504
896
|
}));
|
|
1505
897
|
|
|
1506
|
-
app.post('/api/workbench/tasks/:id/clear-subtasks', asyncRoute(async (req, res) => {
|
|
1507
|
-
const taskId = req.params.id;
|
|
1508
|
-
if (!taskId) throw new HttpError(400, '缺少 taskId');
|
|
1509
|
-
const live = [];
|
|
1510
|
-
for (const j of jobs.values()) {
|
|
1511
|
-
if (j.taskId !== taskId) continue;
|
|
1512
|
-
if (j.status === 'running' || j.status === 'pending') live.push(j.id);
|
|
1513
|
-
}
|
|
1514
|
-
if (live.length > 0) {
|
|
1515
|
-
throw new HttpError(400, `有 ${live.length} 个 job 正在执行,请先停止`);
|
|
1516
|
-
}
|
|
1517
|
-
const removedJobIds = [];
|
|
1518
|
-
for (const j of jobs.values()) {
|
|
1519
|
-
if (j.taskId !== taskId) continue;
|
|
1520
|
-
jobs.delete(j.id);
|
|
1521
|
-
removedJobIds.push(j.id);
|
|
1522
|
-
}
|
|
1523
|
-
const jobsData = await readJson(JOBS_FILE, { version: 1, jobs: [] });
|
|
1524
|
-
if (jobsData && Array.isArray(jobsData.jobs)) {
|
|
1525
|
-
const before = jobsData.jobs.length;
|
|
1526
|
-
jobsData.jobs = jobsData.jobs.filter(j => j.taskId !== taskId);
|
|
1527
|
-
if (jobsData.jobs.length !== before) await writeJson(JOBS_FILE, jobsData);
|
|
1528
|
-
}
|
|
1529
|
-
const tasksData = await readJson(TASKS_FILE, { tasks: [] });
|
|
1530
|
-
const task = (tasksData.tasks || []).find(t => t.id === taskId);
|
|
1531
|
-
if (!task) {
|
|
1532
|
-
return res.json({ success: true, removedJobs: removedJobIds.length, removedSubs: 0, message: '任务不存在,仅清空 job' });
|
|
1533
|
-
}
|
|
1534
|
-
const removedSubCount = Array.isArray(task.subtasks) ? task.subtasks.length : 0;
|
|
1535
|
-
if (removedSubCount === 0) {
|
|
1536
|
-
return res.json({ success: true, removedJobs: removedJobIds.length, removedSubs: 0, message: '没有子任务需要清空' });
|
|
1537
|
-
}
|
|
1538
|
-
task.subtasks = [];
|
|
1539
|
-
task.updatedAt = nowIso();
|
|
1540
|
-
await writeJson(TASKS_FILE, tasksData);
|
|
1541
|
-
publish('task:update', task);
|
|
1542
|
-
res.json({
|
|
1543
|
-
success: true,
|
|
1544
|
-
removedJobs: removedJobIds.length,
|
|
1545
|
-
removedSubs: removedSubCount,
|
|
1546
|
-
message: `已清空 ${removedSubCount} 个子任务,任务描述和附件保留`
|
|
1547
|
-
});
|
|
1548
|
-
}));
|
|
1549
898
|
|
|
1550
899
|
// GET /api/workbench/jobs/:id (放在所有字面路径之后)
|
|
1551
900
|
app.get('/api/workbench/jobs/:id', asyncRoute(async (req, res) => {
|
|
@@ -1600,19 +949,10 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1600
949
|
return { owner: 'task', task: t, list, att, storageDir: path.join(IMAGES_DIR, '_task-' + t.id) };
|
|
1601
950
|
}
|
|
1602
951
|
}
|
|
1603
|
-
for (const t of data.tasks || []) {
|
|
1604
|
-
for (const s of t.subtasks || []) {
|
|
1605
|
-
const list = Array.isArray(s.attachments) ? s.attachments : [];
|
|
1606
|
-
const att = list.find(x => x.id === attId);
|
|
1607
|
-
if (att) {
|
|
1608
|
-
return { owner: 'sub', task: t, sub: s, list, att, storageDir: path.join(IMAGES_DIR, s.id) };
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
952
|
return null;
|
|
1613
953
|
}
|
|
1614
954
|
|
|
1615
|
-
// 共享 helper
|
|
955
|
+
// 共享 helper:写入新附件
|
|
1616
956
|
async function writeAttachmentTo({ req, target, maxCount }) {
|
|
1617
957
|
if (!req.body || !(req.body instanceof Buffer) || req.body.length === 0) {
|
|
1618
958
|
throw new HttpError(400, '请求体为空');
|
|
@@ -1653,54 +993,14 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1653
993
|
}
|
|
1654
994
|
|
|
1655
995
|
// 子任务附件
|
|
1656
|
-
|
|
1657
|
-
const { subId } = req.params;
|
|
1658
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1659
|
-
let foundSub = null;
|
|
1660
|
-
for (const t of data.tasks || []) {
|
|
1661
|
-
const s = (t.subtasks || []).find(x => x.id === subId);
|
|
1662
|
-
if (s) { foundSub = s; break; }
|
|
1663
|
-
}
|
|
1664
|
-
if (!foundSub) throw new HttpError(404, '子任务不存在');
|
|
1665
|
-
const target = { ...foundSub, storageDir: path.join(IMAGES_DIR, subId) };
|
|
1666
|
-
const att = await writeAttachmentTo({ req, target, maxCount: MAX_ATTACHMENTS_PER_SUBTASK });
|
|
1667
|
-
// target 是 spread 出来的浅拷贝,data 引用里的 foundSub 没改;显式 push 回去
|
|
1668
|
-
foundSub.attachments = Array.isArray(foundSub.attachments) ? foundSub.attachments : [];
|
|
1669
|
-
foundSub.attachments.push(att);
|
|
1670
|
-
foundSub.updatedAt = nowIso();
|
|
1671
|
-
await writeJson(TASKS_FILE, data);
|
|
1672
|
-
res.json({ success: true, attachment: att });
|
|
1673
|
-
}));
|
|
1674
|
-
|
|
1675
|
-
app.delete('/api/workbench/subtasks/:subId/attachments/:attId', asyncRoute(async (req, res) => {
|
|
1676
|
-
const { subId, attId } = req.params;
|
|
1677
|
-
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1678
|
-
let foundSub = null;
|
|
1679
|
-
for (const t of data.tasks || []) {
|
|
1680
|
-
const s = (t.subtasks || []).find(x => x.id === subId);
|
|
1681
|
-
if (s) { foundSub = s; break; }
|
|
1682
|
-
}
|
|
1683
|
-
if (!foundSub) throw new HttpError(404, '子任务不存在');
|
|
1684
|
-
const list = Array.isArray(foundSub.attachments) ? foundSub.attachments : [];
|
|
1685
|
-
const i = list.findIndex(a => a.id === attId);
|
|
1686
|
-
if (i < 0) throw new HttpError(404, '附件不存在');
|
|
1687
|
-
const [removed] = list.splice(i, 1);
|
|
1688
|
-
try {
|
|
1689
|
-
await fsp.unlink(path.join(IMAGES_DIR, subId, removed.storedName));
|
|
1690
|
-
} catch { /* 文件可能已不存在 */ }
|
|
1691
|
-
foundSub.updatedAt = nowIso();
|
|
1692
|
-
await writeJson(TASKS_FILE, data);
|
|
1693
|
-
res.json({ success: true });
|
|
1694
|
-
}));
|
|
1695
|
-
|
|
1696
|
-
// 主任务附件
|
|
996
|
+
// 任务附件
|
|
1697
997
|
app.post('/api/workbench/tasks/:taskId/attachments', rawAttachment, asyncRoute(async (req, res) => {
|
|
1698
998
|
const { taskId } = req.params;
|
|
1699
999
|
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1700
1000
|
const task = (data.tasks || []).find(t => t.id === taskId);
|
|
1701
1001
|
if (!task) throw new HttpError(404, '任务不存在');
|
|
1702
1002
|
const target = { ...task, storageDir: path.join(IMAGES_DIR, '_task-' + taskId) };
|
|
1703
|
-
const att = await writeAttachmentTo({ req, target, maxCount:
|
|
1003
|
+
const att = await writeAttachmentTo({ req, target, maxCount: MAX_ATTACHMENTS_PER_TASK });
|
|
1704
1004
|
task.attachments = Array.isArray(task.attachments) ? task.attachments : [];
|
|
1705
1005
|
task.attachments.push(att);
|
|
1706
1006
|
task.updatedAt = nowIso();
|
|
@@ -1858,7 +1158,7 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1858
1158
|
const task = (data.tasks || []).find(t => t && t.id === id);
|
|
1859
1159
|
if (!task) throw new HttpError(404, '任务不存在');
|
|
1860
1160
|
|
|
1861
|
-
// loadAllJobs 已经把「文件里归档的 + 内存里还活着的」合并过(含 taskTitle
|
|
1161
|
+
// loadAllJobs 已经把「文件里归档的 + 内存里还活着的」合并过(含 taskTitle)
|
|
1862
1162
|
const allJobs = await loadAllJobs();
|
|
1863
1163
|
const detail = buildTaskDetail(task, allJobs.filter(j => j && j.taskId === id));
|
|
1864
1164
|
if (!detail) throw new HttpError(404, '任务不存在');
|
|
@@ -1954,8 +1254,7 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
1954
1254
|
* > 主 Agent 判断 > 应用当前项目)。用户不必先选项目,落点会如实记进指令流水
|
|
1955
1255
|
* - autoRun 默认 true;调度暂停时只建任务不执行(指令记录里会写明原因)
|
|
1956
1256
|
*
|
|
1957
|
-
*
|
|
1958
|
-
* 而不是造一个没有子任务的复杂任务 —— 后者连执行入口都没有,只会变成看板上的死卡。
|
|
1257
|
+
* 一条指令落成目标项目下的一个任务:整段指令就是那次会话的 prompt。
|
|
1959
1258
|
*/
|
|
1960
1259
|
app.post('/api/workbench/orchestrator/dispatch', asyncRoute(async (req, res) => {
|
|
1961
1260
|
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : '';
|
|
@@ -2035,8 +1334,8 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
2035
1334
|
// 所以不存在"前端指定任意路径"这回事 —— 比"信任 absolutePath 再校验前缀"干净。
|
|
2036
1335
|
// 数量与文件存在性在这里统一校验:上传时服务端是无状态的,压根不知道前端一共攒了几个。
|
|
2037
1336
|
const rawAttachments = Array.isArray(req.body?.attachments) ? req.body.attachments : [];
|
|
2038
|
-
if (rawAttachments.length >
|
|
2039
|
-
throw new HttpError(400, `附件最多 ${
|
|
1337
|
+
if (rawAttachments.length > MAX_ATTACHMENTS_PER_TASK) {
|
|
1338
|
+
throw new HttpError(400, `附件最多 ${MAX_ATTACHMENTS_PER_TASK} 个`);
|
|
2040
1339
|
}
|
|
2041
1340
|
const staged = [];
|
|
2042
1341
|
for (const item of rawAttachments) {
|
|
@@ -2097,15 +1396,12 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
2097
1396
|
title: text.split('\n')[0].slice(0, 120),
|
|
2098
1397
|
desc: text,
|
|
2099
1398
|
promptId: null,
|
|
2100
|
-
type: 'simple',
|
|
2101
|
-
sequential: true,
|
|
2102
1399
|
// 默认提示词在派发这一刻抄进任务(这次生效的是什么,任务自己记着)。
|
|
2103
1400
|
// 之后用户改设置不会回头改写它 —— 一条已存在的任务,"它当时是被怎么派出去的"
|
|
2104
1401
|
// 是既成事实,不是当前配置的投影。
|
|
2105
1402
|
simpleOverride: dispatchPrompt.text,
|
|
2106
1403
|
projectPath: targetPath,
|
|
2107
1404
|
attachments,
|
|
2108
|
-
subtasks: [],
|
|
2109
1405
|
status: 'todo',
|
|
2110
1406
|
createdAt: now,
|
|
2111
1407
|
updatedAt: now,
|
|
@@ -2130,21 +1426,21 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
2130
1426
|
publish('task:created', { task });
|
|
2131
1427
|
publish('orchestrator:instruction', record);
|
|
2132
1428
|
|
|
2133
|
-
// 与 POST /tasks/:id/run
|
|
1429
|
+
// 与 POST /tasks/:id/run 走同一条执行路径,避免出现第二套执行入口
|
|
2134
1430
|
if (willRun) {
|
|
2135
1431
|
const virtualSub = {
|
|
2136
1432
|
id: `${task.id}__simple`,
|
|
2137
1433
|
title: task.title,
|
|
2138
1434
|
desc: task.desc || '',
|
|
2139
1435
|
status: 'todo',
|
|
2140
|
-
//
|
|
1436
|
+
// 提示词取自 task.simpleOverride(派发时已把
|
|
2141
1437
|
// 全局/项目默认提示词抄进去),而不是在这里再解析一次配置
|
|
2142
1438
|
promptOverride: task.simpleOverride || '',
|
|
2143
1439
|
attachments: [],
|
|
2144
1440
|
};
|
|
2145
1441
|
// 派发控制台可以显式指定执行器;不传则用配置里的全局默认(resolveExecutor 走回落链)
|
|
2146
1442
|
const executor = await resolveExecutor(req.body?.executor);
|
|
2147
|
-
runSingleSubtask(task, virtualSub, targetPath, '',
|
|
1443
|
+
runSingleSubtask(task, virtualSub, targetPath, '', { executor }).catch(err => {
|
|
2148
1444
|
publish('task:error', { taskId: task.id, error: err.message });
|
|
2149
1445
|
});
|
|
2150
1446
|
}
|
|
@@ -2163,7 +1459,7 @@ ${desc ? `描述:${desc}` : '描述:(无)'}${attachmentBlock}${templateB
|
|
|
2163
1459
|
app.post('/api/workbench/orchestrator/attachments', rawAttachment, asyncRoute(async (req, res) => {
|
|
2164
1460
|
await fsp.mkdir(DISPATCH_STAGING_DIR, { recursive: true });
|
|
2165
1461
|
const target = { attachments: [], storageDir: DISPATCH_STAGING_DIR };
|
|
2166
|
-
const att = await writeAttachmentTo({ req, target, maxCount:
|
|
1462
|
+
const att = await writeAttachmentTo({ req, target, maxCount: MAX_ATTACHMENTS_PER_TASK });
|
|
2167
1463
|
res.json({ success: true, attachment: att });
|
|
2168
1464
|
}));
|
|
2169
1465
|
|