zen-gitsync 2.17.20 → 2.17.21
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/README.md +10 -8
- package/package.json +3 -1
- package/src/ui/public/assets/AgentEngineSelector-DR3r5eQE.css +1 -0
- package/src/ui/public/assets/AgentEngineSelector-D_8VedA0.js +1 -0
- package/src/ui/public/assets/AgentView-CpKIfQLq.js +1 -0
- package/src/ui/public/assets/AgentView-DnN-vCMi.css +1 -0
- package/src/ui/public/assets/{AppVersionBadge-DDYQqoy7.css → AppVersionBadge--5If494j.css} +1 -1
- package/src/ui/public/assets/AppVersionBadge-kyk4Mmvf.js +2 -0
- package/src/ui/public/assets/{BranchSelector-F4ibldMN.css → BranchSelector-BTCMYwG0.css} +1 -1
- package/src/ui/public/assets/BranchSelector-BUPiKcdW.js +1 -0
- package/src/ui/public/assets/CommitForm-C-cL8_vz.css +1 -0
- package/src/ui/public/assets/CommitForm-CMe1gjY-.js +6 -0
- package/src/ui/public/assets/CommonDialog-Bzi5ISqT.css +1 -0
- package/src/ui/public/assets/CommonDialog-DMWtstZq.js +1 -0
- package/src/ui/public/assets/EditorView-C6msf9YP.js +1 -0
- package/src/ui/public/assets/{EditorView-BZjWMYYl.css → EditorView-mUmFBb7H.css} +1 -1
- package/src/ui/public/assets/FileDiffViewer-B6zy6NiA.css +1 -0
- package/src/ui/public/assets/FileDiffViewer-CvN5b1ZC.js +14 -0
- package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-Ynr4q8qE.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CM2_9TW3.css +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DPDZc9uf.js +21 -0
- package/src/ui/public/assets/IconButton-CFJkDLVG.js +1 -0
- package/src/ui/public/assets/IconButton-W2X09OKH.css +1 -0
- package/src/ui/public/assets/LogList-C7q0Y4J_.css +1 -0
- package/src/ui/public/assets/LogList-CbfsCVsy.js +7 -0
- package/src/ui/public/assets/MindmapView-C4xg3TwX.css +1 -0
- package/src/ui/public/assets/MindmapView-D8A1V-Fd.js +1 -0
- package/src/ui/public/assets/MonitorView-BWOQfPtI.css +1 -0
- package/src/ui/public/assets/MonitorView-Bm_vUP8Y.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-BBND5XcZ.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-VoQqFee4.css +1 -0
- package/src/ui/public/assets/RecentDirectoriesChat-BQbncY1L.css +1 -0
- package/src/ui/public/assets/RecentDirectoriesChat-D9fMFh8r.js +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-BSOph2h2.css +1 -0
- package/src/ui/public/assets/RemoteManagerDialog-BVQeIr8U.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-CG9WPcA_.css +1 -0
- package/src/ui/public/assets/RemoteRepoCard-vuCu_cpA.js +1 -0
- package/src/ui/public/assets/SourceMapView-BZjgxQM8.js +3 -0
- package/src/ui/public/assets/SourceMapView-CO_BZJKu.css +1 -0
- package/src/ui/public/assets/{SvgIcon-CDNsk4-Q.css → SvgIcon-BQ6bR7l0.css} +1 -1
- package/src/ui/public/assets/SvgIcon-D0hK-p1U.js +1 -0
- package/src/ui/public/assets/TemplateManager-DiM3vrbf.css +1 -0
- package/src/ui/public/assets/TemplateManager-XoPZv_i6.js +1 -0
- package/src/ui/public/assets/UserInputNode-CZarmsrv.js +1 -0
- package/src/ui/public/assets/UserInputNode-d-oRXM_J.css +1 -0
- package/src/ui/public/assets/WorkbenchView-BLyXsdYw.css +1 -0
- package/src/ui/public/assets/WorkbenchView-CPur8WEj.js +6 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-o8uPiAjs.js → _plugin-vue_export-helper-N8Vc5eEs.js} +5 -5
- package/src/ui/public/assets/{agentConversations-BmOPFH_t.js → agentConversations-Da7zLPfP.js} +1 -1
- package/src/ui/public/assets/{configStore-DrMJDegh.js → configStore-BFb0I8eP.js} +1 -1
- package/src/ui/public/assets/element-plus-Bx3j6vzG.js +28 -0
- package/src/ui/public/assets/{flow-mindmap-C22NSZc1.js → flow-mindmap-OPE_D4da.js} +34 -34
- package/src/ui/public/assets/{gitStore-BI_ujQ9r.js → gitStore-grKwgyHH.js} +1 -1
- package/src/ui/public/assets/index-BHYAwndE.js +29 -0
- package/src/ui/public/assets/index-DVJITdth.css +1 -0
- package/src/ui/public/assets/office-docx-B0sJQnr-.js +31 -0
- package/src/ui/public/assets/{office-excel-BOjWK-1x.js → office-excel-DMRySOdK.js} +1 -1
- package/src/ui/public/assets/office-pptx-Do2blxao.js +60 -0
- package/src/ui/public/assets/previewSandbox-D4455rKO.js +9 -0
- package/src/ui/public/assets/{previewSandbox-PYnM6Paz.css → previewSandbox-SqNQvuHv.css} +1 -1
- package/src/ui/public/assets/socket-io-TBtw0msS.js +1 -0
- package/src/ui/public/assets/{toolsStore-DgqU92bI.js → toolsStore-B49mCaQ_.js} +1 -1
- package/src/ui/public/assets/{useThemeObserver-CiUpR4zj.js → useThemeObserver-DYxFoHh1.js} +1 -1
- package/src/ui/public/assets/{vendor-CPpc-p6F.js → vendor-CyWsJd6S.js} +45 -52
- package/src/ui/public/assets/vue-flow-fVVGZspa.js +39 -0
- package/src/ui/public/index.html +24 -24
- package/src/ui/server/index.js +8 -0
- package/src/ui/server/routes/workbench/index.js +156 -7
- package/src/ui/server/routes/workbench/jobStore.js +141 -48
- package/src/ui/server/routes/workbench/jobStore.test.js +139 -2
- package/src/ui/server/routes/workbench/liveJobs.js +226 -0
- package/src/ui/server/routes/workbench/liveJobs.test.js +215 -0
- package/src/ui/server/routes/workbench/orchestratorStore.js +113 -2
- package/src/ui/server/routes/workbench/progressReport.js +293 -0
- package/src/ui/server/routes/workbench/progressReport.test.js +344 -0
- package/src/ui/server/routes/workbench/shared.js +28 -0
- package/src/ui/server/routes/workbench/taskRunner.js +8 -0
- package/src/ui/public/assets/AgentEngineSelector-DIn2efUS.js +0 -2
- package/src/ui/public/assets/AgentEngineSelector-DUf0TfaU.css +0 -1
- package/src/ui/public/assets/AgentView-CS-sFe-x.js +0 -10
- package/src/ui/public/assets/AgentView-CWCc_b0o.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-CeHsanRP.js +0 -2
- package/src/ui/public/assets/BranchSelector-CxEoHp2q.js +0 -1
- package/src/ui/public/assets/CommitForm-4kI2csgs.css +0 -1
- package/src/ui/public/assets/CommitForm-DOfYuDqr.js +0 -6
- package/src/ui/public/assets/CommonDialog-CZ5McwHT.js +0 -1
- package/src/ui/public/assets/CommonDialog-w6EmBEXv.css +0 -1
- package/src/ui/public/assets/EditorView-BxRrHGUj.js +0 -31
- package/src/ui/public/assets/FileDiffViewer-CKcSkMAd.js +0 -15
- package/src/ui/public/assets/FileDiffViewer-n5ea9qMe.css +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-DShoSVO7.js +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BkPZZ0-D.js +0 -21
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DwuSSxJ1.css +0 -1
- package/src/ui/public/assets/IconButton-D-maDWg7.js +0 -1
- package/src/ui/public/assets/IconButton-RlXqKOeu.css +0 -1
- package/src/ui/public/assets/LogList-Bv5ZqdMf.js +0 -7
- package/src/ui/public/assets/LogList-XspUhx-C.css +0 -1
- package/src/ui/public/assets/MindmapView-C27U_5_C.js +0 -2
- package/src/ui/public/assets/MindmapView-Diq2Cu4O.css +0 -1
- package/src/ui/public/assets/MonitorView-BudY0fTU.js +0 -1
- package/src/ui/public/assets/MonitorView-DwUwJXnY.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-CB8IOg1E.css +0 -1
- package/src/ui/public/assets/ProjectStartupButton-YJPcfS3t.js +0 -1
- package/src/ui/public/assets/RecentDirectoriesChat-Bqd_4eU5.js +0 -1
- package/src/ui/public/assets/RecentDirectoriesChat-CZD_IC5M.css +0 -1
- package/src/ui/public/assets/RemoteManagerDialog-BUNLKNem.js +0 -4
- package/src/ui/public/assets/RemoteManagerDialog-D4WphMtO.css +0 -1
- package/src/ui/public/assets/RemoteRepoCard-Bq1oLUnZ.js +0 -7
- package/src/ui/public/assets/RemoteRepoCard-o01IrbVz.css +0 -1
- package/src/ui/public/assets/SourceMapView-BCrE4CvT.js +0 -3
- package/src/ui/public/assets/SourceMapView-CfDydtsu.css +0 -1
- package/src/ui/public/assets/SvgIcon-z4ExzvGR.js +0 -1
- package/src/ui/public/assets/TemplateManager-BSq-dNvW.js +0 -1
- package/src/ui/public/assets/TemplateManager-DO7EPfj8.css +0 -1
- package/src/ui/public/assets/UserInputNode-BzbaR9lE.js +0 -1
- package/src/ui/public/assets/UserInputNode-DDNSarag.css +0 -1
- package/src/ui/public/assets/WorkbenchView-BJ7lMt3C.js +0 -70
- package/src/ui/public/assets/WorkbenchView-DGIn0UR4.css +0 -1
- package/src/ui/public/assets/element-plus-AaVXaSqv.js +0 -48
- package/src/ui/public/assets/index-Cp5WtCfY.css +0 -1
- package/src/ui/public/assets/index-D_Pwvey4.js +0 -130
- package/src/ui/public/assets/office-docx-CJ6JSCs6.js +0 -32
- package/src/ui/public/assets/office-pptx-CF43ZwXE.js +0 -74
- package/src/ui/public/assets/previewSandbox-B16xvZLS.js +0 -9
- package/src/ui/public/assets/socket-io-BFqQXqAp.js +0 -1
- package/src/ui/public/assets/vue-flow-DwYB6Myr.js +0 -39
package/src/ui/public/index.html
CHANGED
|
@@ -10,38 +10,38 @@
|
|
|
10
10
|
<meta name="theme-color" content="#141820" media="(prefers-color-scheme: dark)" />
|
|
11
11
|
<title>Zen GitSync</title>
|
|
12
12
|
<!-- 字体改为自托管(@font-face 见 src/main.css),不再依赖 Google Fonts 外链 -->
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-BHYAwndE.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/element-plus-
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/element-plus-Bx3j6vzG.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/monaco-DkJuNcCr.js">
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/IconButton-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/assets/configStore-
|
|
22
|
-
<link rel="modulepreload" crossorigin href="/assets/socket-io-
|
|
23
|
-
<link rel="modulepreload" crossorigin href="/assets/gitStore-
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-CyWsJd6S.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-N8Vc5eEs.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/SvgIcon-D0hK-p1U.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/IconButton-CFJkDLVG.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/configStore-BFb0I8eP.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/assets/socket-io-TBtw0msS.js">
|
|
23
|
+
<link rel="modulepreload" crossorigin href="/assets/gitStore-grKwgyHH.js">
|
|
24
24
|
<link rel="modulepreload" crossorigin href="/assets/fileIcon-B5p5pGN9.js">
|
|
25
|
-
<link rel="modulepreload" crossorigin href="/assets/flow-mindmap-
|
|
26
|
-
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-
|
|
27
|
-
<link rel="modulepreload" crossorigin href="/assets/useThemeObserver-
|
|
28
|
-
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-
|
|
29
|
-
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/toolsStore-
|
|
31
|
-
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-
|
|
32
|
-
<link rel="modulepreload" crossorigin href="/assets/vue-flow-
|
|
25
|
+
<link rel="modulepreload" crossorigin href="/assets/flow-mindmap-OPE_D4da.js">
|
|
26
|
+
<link rel="modulepreload" crossorigin href="/assets/previewSandbox-D4455rKO.js">
|
|
27
|
+
<link rel="modulepreload" crossorigin href="/assets/useThemeObserver-DYxFoHh1.js">
|
|
28
|
+
<link rel="modulepreload" crossorigin href="/assets/CommonDialog-DMWtstZq.js">
|
|
29
|
+
<link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-CvN5b1ZC.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/toolsStore-B49mCaQ_.js">
|
|
31
|
+
<link rel="modulepreload" crossorigin href="/assets/TemplateManager-XoPZv_i6.js">
|
|
32
|
+
<link rel="modulepreload" crossorigin href="/assets/vue-flow-fVVGZspa.js">
|
|
33
33
|
<link rel="stylesheet" crossorigin href="/assets/element-plus-Qexo5hiM.css">
|
|
34
34
|
<link rel="stylesheet" crossorigin href="/assets/monaco-B2lPvkrc.css">
|
|
35
35
|
<link rel="stylesheet" crossorigin href="/assets/vendor-BzMymUBi.css">
|
|
36
|
-
<link rel="stylesheet" crossorigin href="/assets/SvgIcon-
|
|
37
|
-
<link rel="stylesheet" crossorigin href="/assets/IconButton-
|
|
36
|
+
<link rel="stylesheet" crossorigin href="/assets/SvgIcon-BQ6bR7l0.css">
|
|
37
|
+
<link rel="stylesheet" crossorigin href="/assets/IconButton-W2X09OKH.css">
|
|
38
38
|
<link rel="stylesheet" crossorigin href="/assets/flow-mindmap-BMpmHdqh.css">
|
|
39
|
-
<link rel="stylesheet" crossorigin href="/assets/previewSandbox-
|
|
40
|
-
<link rel="stylesheet" crossorigin href="/assets/CommonDialog-
|
|
41
|
-
<link rel="stylesheet" crossorigin href="/assets/FileDiffViewer-
|
|
42
|
-
<link rel="stylesheet" crossorigin href="/assets/TemplateManager-
|
|
39
|
+
<link rel="stylesheet" crossorigin href="/assets/previewSandbox-SqNQvuHv.css">
|
|
40
|
+
<link rel="stylesheet" crossorigin href="/assets/CommonDialog-Bzi5ISqT.css">
|
|
41
|
+
<link rel="stylesheet" crossorigin href="/assets/FileDiffViewer-B6zy6NiA.css">
|
|
42
|
+
<link rel="stylesheet" crossorigin href="/assets/TemplateManager-DiM3vrbf.css">
|
|
43
43
|
<link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
|
|
44
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
44
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DVJITdth.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
|
47
47
|
<div id="app"></div>
|
package/src/ui/server/index.js
CHANGED
|
@@ -697,6 +697,14 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
697
697
|
// 2) 清心跳 + 反注册实例
|
|
698
698
|
try { clearInterval(heartbeatTimer); } catch (_) {}
|
|
699
699
|
try { await instanceRegistry.unregister(process.pid); } catch (_) {}
|
|
700
|
+
// 2b) 清掉自己那份「运行中 job」广播文件(live-jobs/<pid>.json)。
|
|
701
|
+
// 不清也不会留下幽灵「进行中」——别的实例读的时候发现 owner pid 没了会跳过并
|
|
702
|
+
// 顺手删掉,这里只是让 Ctrl+C 这种常见路径当场干净。动态 import:模块早就在
|
|
703
|
+
// 路由注册时加载过了,这里只是拿它的函数,不会往入口的静态 import 图里加东西。
|
|
704
|
+
try {
|
|
705
|
+
const { clearOwnLiveJobsFile } = await import('./routes/workbench/jobStore.js');
|
|
706
|
+
await clearOwnLiveJobsFile();
|
|
707
|
+
} catch (_) {}
|
|
700
708
|
console.log(chalk.gray(`[shutdown] 收到 ${signal},已清理本实例`));
|
|
701
709
|
// 3) 给 OS 100ms 刷盘日志
|
|
702
710
|
setTimeout(() => process.exit(0), 100);
|
|
@@ -83,8 +83,8 @@ import {
|
|
|
83
83
|
bus,
|
|
84
84
|
cancelledJobs,
|
|
85
85
|
serializeJob,
|
|
86
|
-
scheduleJobsSave,
|
|
87
86
|
flushJobsSaveNow,
|
|
87
|
+
clearOwnLiveJobsFile,
|
|
88
88
|
readJobsConfig,
|
|
89
89
|
writeJobsConfig,
|
|
90
90
|
enforceRetention,
|
|
@@ -120,7 +120,15 @@ import {
|
|
|
120
120
|
setProjectPrompt,
|
|
121
121
|
buildActivityFeed,
|
|
122
122
|
buildRunningAgents,
|
|
123
|
+
setReportIntervalMs,
|
|
124
|
+
claimReportSlot,
|
|
125
|
+
readReports,
|
|
126
|
+
appendReport,
|
|
123
127
|
} from './orchestratorStore.js';
|
|
128
|
+
import {
|
|
129
|
+
buildRunningFacts,
|
|
130
|
+
generateProgressReport,
|
|
131
|
+
} from './progressReport.js';
|
|
124
132
|
|
|
125
133
|
/**
|
|
126
134
|
* 「任务结束 → 刷快照」的监听器。存成模块级的,是为了在重复装配路由时
|
|
@@ -129,6 +137,12 @@ import {
|
|
|
129
137
|
*/
|
|
130
138
|
let jobSettledHandler = null;
|
|
131
139
|
|
|
140
|
+
/** 自动进度报告的定时器。同上:重复装配路由时先清掉上一个 */
|
|
141
|
+
let reportTimer = null;
|
|
142
|
+
|
|
143
|
+
/** 自动报告的检查粒度。它**不等于**报告间隔 —— 见 tickProgressReport 的注释 */
|
|
144
|
+
const REPORT_TICK_MS = 30 * 1000;
|
|
145
|
+
|
|
132
146
|
/**
|
|
133
147
|
* 注册所有 workbench 路由(共 45 个端点)。
|
|
134
148
|
* @param {Object} deps
|
|
@@ -897,16 +911,17 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
897
911
|
app.get('/api/workbench/jobs/:id', asyncRoute(async (req, res) => {
|
|
898
912
|
// 优先查内存(含活跃)
|
|
899
913
|
const live = jobs.get(req.params.id);
|
|
914
|
+
const tasksData = await readJson(TASKS_FILE, { tasks: [] });
|
|
915
|
+
const taskMap = new Map((tasksData.tasks || []).map(t => [t.id, t]));
|
|
900
916
|
if (live) {
|
|
901
|
-
const tasksData = await readJson(TASKS_FILE, { tasks: [] });
|
|
902
|
-
const taskMap = new Map((tasksData.tasks || []).map(t => [t.id, t]));
|
|
903
917
|
return res.json({ success: true, job: serializeJob(live, taskMap) });
|
|
904
918
|
}
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
|
|
919
|
+
// 退回「磁盘历史 ∪ 别的实例正在跑」的合并表 —— 光读 jobs.json 会让
|
|
920
|
+
// "看板上明明在转的那条"点开变成 404(运行中的记录不在历史档案里)。
|
|
921
|
+
await refreshJobsFromDisk();
|
|
922
|
+
const j = mergedJobs().get(req.params.id);
|
|
908
923
|
if (!j) throw new HttpError(404, 'job 不存在');
|
|
909
|
-
res.json({ success: true, job: j });
|
|
924
|
+
res.json({ success: true, job: serializeJob(j, taskMap) });
|
|
910
925
|
}));
|
|
911
926
|
|
|
912
927
|
app.delete('/api/workbench/jobs/:id', asyncRoute(async (req, res) => {
|
|
@@ -1204,11 +1219,145 @@ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summar
|
|
|
1204
1219
|
// 设置弹窗打开时也不必再单独取一次(单条上限 8000 字,体积可控)
|
|
1205
1220
|
defaultPrompt: state.defaultPrompt,
|
|
1206
1221
|
projectPrompts: state.projectPrompts,
|
|
1222
|
+
// 进度报告的**配置**(一个数字 + 一个时间戳)跟着轮询走,报告的**正文**走
|
|
1223
|
+
// /orchestrator/reports:报告历史一份几 KB,20 份就是几十 KB,
|
|
1224
|
+
// 塞进这个 5s 轮询的接口纯属浪费(与 /tasks/:id/detail 不进 /projects 同一个理由)。
|
|
1225
|
+
reportIntervalMs: state.reportIntervalMs,
|
|
1226
|
+
lastReportAt: state.lastReportAt,
|
|
1207
1227
|
activity: buildActivityFeed({ jobs: jobsSnap, tasks, instructions: state.instructions }),
|
|
1208
1228
|
running: buildRunningAgents({ jobs: jobsSnap, tasks }),
|
|
1209
1229
|
});
|
|
1210
1230
|
}));
|
|
1211
1231
|
|
|
1232
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1233
|
+
// §19.5 进度报告(右栏「进度报告」面板)
|
|
1234
|
+
//
|
|
1235
|
+
// 面板上那一段"现在跑到哪一步了"由模型写,但**什么时候写、写几次**由这一层管:
|
|
1236
|
+
// · 手动「立即报告」→ POST /orchestrator/report,永远可用;
|
|
1237
|
+
// · 自动报告 → 下面的定时器,间隔来自用户设置(0 = 关)。
|
|
1238
|
+
// 两份历史都存在 orchestrator-reports.json,读取走 /orchestrator/reports。
|
|
1239
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* 报告要用到的两份东西:默认模型 + 界面语言。
|
|
1243
|
+
* 一次 config 读出来 —— 分两次读会读到"两个瞬间"(改设置的同时正好在报告)。
|
|
1244
|
+
*/
|
|
1245
|
+
async function readReportContext() {
|
|
1246
|
+
const out = { model: null, locale: '' };
|
|
1247
|
+
if (!configManager || typeof configManager.readRawConfigFile !== 'function') return out;
|
|
1248
|
+
try {
|
|
1249
|
+
const raw = await configManager.readRawConfigFile();
|
|
1250
|
+
const models = Array.isArray(raw && raw.models) ? raw.models : [];
|
|
1251
|
+
out.model = models.find(m => m && m.isDefault) || models[0] || null;
|
|
1252
|
+
out.locale = typeof (raw && raw.locale) === 'string' ? raw.locale : '';
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
logger.warn('[workbench] 读取模型配置失败,本次报告只记事实:', (err && err.message) || err);
|
|
1255
|
+
}
|
|
1256
|
+
return out;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** 当前在跑的 job 拼成报告事实。先刷磁盘那一半,别的实例跑的任务也要算进来 */
|
|
1260
|
+
async function collectRunningFacts() {
|
|
1261
|
+
const data = await readJson(TASKS_FILE, { tasks: [] });
|
|
1262
|
+
await refreshJobsFromDisk();
|
|
1263
|
+
return buildRunningFacts({ jobs: Array.from(mergedJobs().values()), tasks: data.tasks || [] });
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* 生成并落盘一份进度报告。
|
|
1268
|
+
*
|
|
1269
|
+
* @param {'auto'|'manual'} trigger
|
|
1270
|
+
* @param {{ locale?: string, intervalMs?: number }} [opts]
|
|
1271
|
+
* locale 缺省时用配置里的界面语言;intervalMs 只在 auto 时用(抢占名额)
|
|
1272
|
+
* @returns {Promise<object|null>} null = 这次不该报(没任务在跑 / 名额已被占)
|
|
1273
|
+
*/
|
|
1274
|
+
async function runProgressReport(trigger, { locale = '', intervalMs = 0 } = {}) {
|
|
1275
|
+
const facts = await collectRunningFacts();
|
|
1276
|
+
|
|
1277
|
+
// 没任务在跑时自动报告**静默跳过**:一条"什么都没发生"的报告每 10 分钟占一格,
|
|
1278
|
+
// 只会把真正有用的那几条挤出历史(上限 20 条)。
|
|
1279
|
+
// 手动「立即报告」不受此限 —— 那时用户要的就是"现在确实没有东西在跑"这句实话。
|
|
1280
|
+
if (!facts.length && trigger === 'auto') return null;
|
|
1281
|
+
if (trigger === 'auto' && !(await claimReportSlot(intervalMs))) return null;
|
|
1282
|
+
|
|
1283
|
+
const ctx = await readReportContext();
|
|
1284
|
+
const report = await generateProgressReport({
|
|
1285
|
+
facts,
|
|
1286
|
+
locale: locale || ctx.locale,
|
|
1287
|
+
trigger,
|
|
1288
|
+
model: ctx.model,
|
|
1289
|
+
});
|
|
1290
|
+
const saved = await appendReport(report);
|
|
1291
|
+
publish('orchestrator:report', { id: saved.id, at: saved.at, trigger: saved.trigger });
|
|
1292
|
+
return saved;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* 同一时刻只跑一次生成。
|
|
1297
|
+
* 「立即报告」连点两下、或手动与自动撞在一起时,第二次应该**等第一份**,
|
|
1298
|
+
* 而不是再打一次模型换回一份几乎一样的报告。
|
|
1299
|
+
*/
|
|
1300
|
+
let reportInFlight = null;
|
|
1301
|
+
function runProgressReportOnce(trigger, opts) {
|
|
1302
|
+
if (reportInFlight) return reportInFlight;
|
|
1303
|
+
reportInFlight = runProgressReport(trigger, opts).finally(() => { reportInFlight = null; });
|
|
1304
|
+
return reportInFlight;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/** 取报告历史(新的在前)。面板打开时取一次、之后每次生成后取一次 */
|
|
1308
|
+
app.get('/api/workbench/orchestrator/reports', asyncRoute(async (_req, res) => {
|
|
1309
|
+
res.json({ success: true, reports: await readReports() });
|
|
1310
|
+
}));
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* 手动生成一份报告。body: { locale? }
|
|
1314
|
+
*
|
|
1315
|
+
* 与自动报告共用同一条链路,只在两处不同:跳过"必须有任务在跑"的静默跳过、
|
|
1316
|
+
* 不占用自动报告的间隔名额(否则用户点一下「立即报告」,下一次自动报告被推迟一整个间隔)。
|
|
1317
|
+
*/
|
|
1318
|
+
app.post('/api/workbench/orchestrator/report', asyncRoute(async (req, res) => {
|
|
1319
|
+
const locale = typeof req.body?.locale === 'string' ? req.body.locale : '';
|
|
1320
|
+
const report = await runProgressReportOnce('manual', { locale });
|
|
1321
|
+
res.json({ success: true, report });
|
|
1322
|
+
}));
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* 改自动报告间隔。body: { intervalMs }
|
|
1326
|
+
* 取值由 shared.js 的白名单卡死(0 / 5 / 10 / 15 / 30 / 60 分钟),非法值回落默认 ——
|
|
1327
|
+
* 这个数字直接决定每多久烧一次额度,不能让请求体里的任意值进来。
|
|
1328
|
+
*/
|
|
1329
|
+
app.post('/api/workbench/orchestrator/report-interval', asyncRoute(async (req, res) => {
|
|
1330
|
+
const reportIntervalMs = await setReportIntervalMs(req.body?.intervalMs);
|
|
1331
|
+
publish('orchestrator:report-interval', { reportIntervalMs });
|
|
1332
|
+
res.json({ success: true, reportIntervalMs });
|
|
1333
|
+
}));
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* 自动报告定时器。
|
|
1337
|
+
*
|
|
1338
|
+
* 30s 一跳,但每一跳**只是判断该不该报**:读 orchestrator.json 拿间隔 + 看内存里
|
|
1339
|
+
* 有没有在跑的 job。真正花额度的那一步被三重条件挡住 —— 开关打开、有任务在跑、
|
|
1340
|
+
* 距上次报告满一个间隔。所以"10 分钟报一次"的实际误差不超过 30s,
|
|
1341
|
+
* 而不是每 30 秒打一次模型。
|
|
1342
|
+
*
|
|
1343
|
+
* 为什么放服务端而不是前端定时器:报告是给**离开键盘的人**看的,
|
|
1344
|
+
* 前端定时器在标签页隐藏/关掉后就不跑了,而那正是最需要它的时刻。
|
|
1345
|
+
*/
|
|
1346
|
+
async function tickProgressReport() {
|
|
1347
|
+
const state = await readOrchestrator();
|
|
1348
|
+
if (!state.reportIntervalMs) return;
|
|
1349
|
+
await runProgressReportOnce('auto', { intervalMs: state.reportIntervalMs });
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (reportTimer) { clearInterval(reportTimer); reportTimer = null; }
|
|
1353
|
+
reportTimer = setInterval(() => {
|
|
1354
|
+
tickProgressReport().catch(err => {
|
|
1355
|
+
logger.warn('[workbench] 自动进度报告失败:', (err && err.message) || err);
|
|
1356
|
+
});
|
|
1357
|
+
}, REPORT_TICK_MS);
|
|
1358
|
+
// 定时器不该把进程吊着不退出
|
|
1359
|
+
reportTimer.unref?.();
|
|
1360
|
+
|
|
1212
1361
|
app.post('/api/workbench/orchestrator/state', asyncRoute(async (req, res) => {
|
|
1213
1362
|
const active = req.body?.active;
|
|
1214
1363
|
if (typeof active !== 'boolean') throw new HttpError(400, 'active 必须是布尔值');
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// 拆分自原 routes/workbench.js 814-955 行。
|
|
17
17
|
//
|
|
18
18
|
// 设计要点:
|
|
19
|
-
// -
|
|
19
|
+
// - 两条落盘路径:**jobs.json 只在终态写**;**运行中只写 live-jobs/<pid>.json**
|
|
20
|
+
// (1.5s 防抖)。后者是「别的 g ui 实例也能看到进行中」的依据,见 liveJobs.js
|
|
20
21
|
// - 永不持久化 child 引用(参考 cancel 路由的浅拷贝模式)
|
|
21
22
|
// - hydrate 时把 running/pending 降级为 error:原 child 进程已不存在
|
|
22
23
|
// - enforceRetention 在每次落盘后跑,按 endedAt desc FIFO 裁剪
|
|
@@ -38,6 +39,13 @@ import {
|
|
|
38
39
|
writeJson,
|
|
39
40
|
nowIso,
|
|
40
41
|
} from './shared.js';
|
|
42
|
+
import {
|
|
43
|
+
projectJob,
|
|
44
|
+
toLiveRecord,
|
|
45
|
+
writeLiveJobsFile,
|
|
46
|
+
removeLiveJobsFile,
|
|
47
|
+
scanLiveJobsDir,
|
|
48
|
+
} from './liveJobs.js';
|
|
41
49
|
|
|
42
50
|
// 进程表:记录每个子任务的运行状态
|
|
43
51
|
// jobId -> { id, taskId, subId, status, pid, startedAt, endedAt, exitCode, error, prompt, child, output, thinking, claudeSessionId, toolCalls }
|
|
@@ -59,12 +67,12 @@ export const cancelledJobs = new Set();
|
|
|
59
67
|
// 的 deriveTaskColumn),而数据其实一直在 jobs.json 里,只是没第二个进程去读它。
|
|
60
68
|
//
|
|
61
69
|
// 现在:凡是需要"反映别的 g ui 进程"的读取路径,先 await refreshJobsFromDisk(),
|
|
62
|
-
// 再用 mergedJobs()
|
|
63
|
-
//
|
|
70
|
+
// 再用 mergedJobs() 拿「内存 ∪ 运行中 ∪ 磁盘」。**内存优先** —— 本进程跑的 job 带着
|
|
71
|
+
// 最新的流式输出和终态,磁盘上的那一份还可能停在 1.5s debounce 之前的旧状态。
|
|
64
72
|
//
|
|
65
73
|
// 同步 vs 异步:refreshJobsFromDisk() 是异步的(要 stat + 读文件),mergedJobs() /
|
|
66
|
-
// snapshotJobs()
|
|
67
|
-
//
|
|
74
|
+
// snapshotJobs() 保持同步纯函数,读缓存的两半 —— 调用点"先 await 刷新、再同步取快照"
|
|
75
|
+
// 这个顺序别颠倒,否则拿到的是上一次的磁盘内容。
|
|
68
76
|
//
|
|
69
77
|
// 用户明确接受秒级延迟:看板本身 5s 轮询,不需要为跨实例推送再造一套 IPC/SSE。
|
|
70
78
|
//
|
|
@@ -72,6 +80,11 @@ export const cancelledJobs = new Set();
|
|
|
72
80
|
// 没变就是一个 stat。
|
|
73
81
|
let diskCache = { mtimeMs: -1, size: -1, jobs: [] };
|
|
74
82
|
|
|
83
|
+
// 「运行中」那一半:别的 g ui 进程写的 live-jobs/<pid>.json(见 liveJobs.js)。
|
|
84
|
+
// 这一半**只有 running/pending 的记录** —— 终态仍然只走 jobs.json,不存在两份口径。
|
|
85
|
+
// 同样用 signature(mtime+size 拼串)做短路。
|
|
86
|
+
let liveCache = { signature: null, jobs: [] };
|
|
87
|
+
|
|
75
88
|
/** 磁盘记录自称 running/pending,但进程已消失、文件也已 N 秒没被写过 → 判为孤儿 */
|
|
76
89
|
// 宽限期要大于 jobs 落盘的 1.5s debounce:job 刚结束时 child 已退出、终态还没 flush,
|
|
77
90
|
// 那个窗口里(pid 死 + 磁盘 running)不该被误判。5s 足够跨过它,又快到用户感知不到。
|
|
@@ -104,11 +117,25 @@ function reclaimOrphan(j, fileMtimeMs) {
|
|
|
104
117
|
};
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
/**
|
|
121
|
+
* 把 live-jobs/ 目录当前内容读进缓存(别的 g ui 进程正在跑的 job)。
|
|
122
|
+
* 目录整体没变(signature 一样)就直接返回 —— 与 jobs.json 的 mtime+size 短路同理。
|
|
123
|
+
*/
|
|
124
|
+
export async function refreshLiveJobs({ force = false } = {}) {
|
|
125
|
+
try {
|
|
126
|
+
const { signature, jobs: live } = await scanLiveJobsDir();
|
|
127
|
+
if (!force && signature === liveCache.signature) return;
|
|
128
|
+
liveCache = { signature, jobs: live };
|
|
129
|
+
} catch (err) {
|
|
130
|
+
logger.warn('[workbench] live-jobs 目录扫描失败,沿用上次缓存:', err?.message || err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
107
134
|
/**
|
|
108
135
|
* 把 jobs.json 当前内容读进缓存。没变( mtime + size 都没动)就直接返回。
|
|
109
136
|
* 读失败一律降级成"沿用上次缓存":看板请求不该因为一个坏文件而 500。
|
|
110
137
|
*/
|
|
111
|
-
|
|
138
|
+
async function refreshJobsFile({ force = false } = {}) {
|
|
112
139
|
let st;
|
|
113
140
|
try {
|
|
114
141
|
st = await fsp.stat(JOBS_FILE);
|
|
@@ -134,29 +161,108 @@ export async function refreshJobsFromDisk({ force = false } = {}) {
|
|
|
134
161
|
}
|
|
135
162
|
}
|
|
136
163
|
|
|
164
|
+
/**
|
|
165
|
+
* 刷新两半缓存:history(jobs.json) + live(live-jobs/)。
|
|
166
|
+
*
|
|
167
|
+
* **所有"要反映别的实例"的读取路径都走这里**,所以 live 的刷新被折进来而不是
|
|
168
|
+
* 另开一个函数让调用点各自记得调 —— 忘了调不会报错,只会让「进行中」又不同步,
|
|
169
|
+
* 那正是这个 bug 的形态。jobs.json 那边的 mtime 短路不影响这里:函数入口先把
|
|
170
|
+
* 两半都刷一遍,各自内部再短路。
|
|
171
|
+
*/
|
|
172
|
+
export async function refreshJobsFromDisk({ force = false } = {}) {
|
|
173
|
+
await refreshLiveJobs({ force });
|
|
174
|
+
await refreshJobsFile({ force });
|
|
175
|
+
}
|
|
176
|
+
|
|
137
177
|
/** 本进程自己写完盘后把缓存作废,免得下一次 refresh 被 mtime 短路在旧内容上 */
|
|
138
178
|
function invalidateDiskCache() {
|
|
139
179
|
diskCache = { mtimeMs: -1, size: -1, jobs: [] };
|
|
180
|
+
liveCache = { signature: null, jobs: [] };
|
|
140
181
|
}
|
|
141
182
|
|
|
142
183
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
184
|
+
* 「内存 ∪ 运行中(别的实例) ∪ 磁盘」的 job 表(id -> 记录)。
|
|
185
|
+
* 优先级:内存 > live > 磁盘 —— 内存是本进程跑的(最新),live 是别人的**在跑**记录
|
|
186
|
+
* (可能比它自己 jobs.json 里的旧终态新),磁盘是历史档案。
|
|
187
|
+
* 同步、无 IO —— 两半缓存由 refreshJobsFromDisk() 预先填好。
|
|
188
|
+
*
|
|
189
|
+
* includeLive=false 的用途:**写 jobs.json 时不要把别人的 live 记录写进去**。
|
|
190
|
+
* 那份是历史档案,而 live 记录的 output/thinking/toolCalls 是截尾的 —— 写进去等于
|
|
191
|
+
* 把一份残缺的日志永久归档,还会参与 size 计价的保留策略。别人的 job 由他自己落盘。
|
|
145
192
|
*/
|
|
146
|
-
export function mergedJobs() {
|
|
193
|
+
export function mergedJobs({ includeLive = true } = {}) {
|
|
147
194
|
const map = new Map();
|
|
148
195
|
for (const j of diskCache.jobs) {
|
|
149
196
|
if (!j || !j.id) continue;
|
|
150
197
|
if (jobs.has(j.id)) continue; // 本进程跑的,下面用内存里的覆盖
|
|
151
198
|
map.set(j.id, reclaimOrphan(j, diskCache.mtimeMs));
|
|
152
199
|
}
|
|
200
|
+
// 别的实例正在跑的:覆盖同 id 的历史记录(那条历史多半是上一次跑的终态)
|
|
201
|
+
if (includeLive) {
|
|
202
|
+
for (const j of liveCache.jobs) {
|
|
203
|
+
if (!j || !j.id) continue;
|
|
204
|
+
if (jobs.has(j.id)) continue;
|
|
205
|
+
map.set(j.id, j);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
153
208
|
for (const [id, j] of jobs) map.set(id, j);
|
|
154
209
|
return map;
|
|
155
210
|
}
|
|
156
211
|
|
|
157
212
|
// ── 持久化层 ─────────────────────────────────────────────────
|
|
158
|
-
//
|
|
159
|
-
|
|
213
|
+
// 两条落盘路径,分工明确:
|
|
214
|
+
// · jobs.json(历史档案)—— **只在终态写**:一整份 7.5MB 的东西不该为了同步
|
|
215
|
+
// "还在跑"这件事被反复重写
|
|
216
|
+
// · live-jobs/<pid>.json(运行中)—— 1.5s 防抖写,几十 KB 封顶
|
|
217
|
+
// jobs Map 承载当前进程产出的活跃 job(也可能是刚结束、等终态 flush 的)。
|
|
218
|
+
//
|
|
219
|
+
// 历史档案没有防抖定时器了:它只在终态写(以前那个 `jobsSaveTimer`/`scheduleJobsSave`
|
|
220
|
+
// 是给"流式阶段也写 jobs.json"准备的,但从来没有任何调用点,2026-09-28 被 live-jobs 取代)。
|
|
221
|
+
|
|
222
|
+
// 运行中那一份的防抖写。由 taskRunner 在 job 转 running/pending 与流式 chunk 时调用。
|
|
223
|
+
let activeSaveTimer = null;
|
|
224
|
+
// 进程内写串行化:防抖回调与终态 flush 可能挨在一起,别让两次写交错
|
|
225
|
+
let activeWriteChain = Promise.resolve();
|
|
226
|
+
|
|
227
|
+
/** 本进程当前**活跃**的 job(running/pending),投影 + 瘦身后用于跨实例广播 */
|
|
228
|
+
function activeJobRecords() {
|
|
229
|
+
const list = [];
|
|
230
|
+
for (const j of jobs.values()) {
|
|
231
|
+
if (j && (j.status === 'running' || j.status === 'pending')) list.push(toLiveRecord(j));
|
|
232
|
+
}
|
|
233
|
+
return list;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 防抖写 live-jobs。**这是运行中 job 唯一的落盘点** —— 少了它,「进行中」就只在
|
|
238
|
+
* 跑任务的那个实例可见(2026-09-28 修的正是这个)。
|
|
239
|
+
*/
|
|
240
|
+
export function scheduleActiveJobsSave() {
|
|
241
|
+
if (activeSaveTimer) return;
|
|
242
|
+
activeSaveTimer = setTimeout(() => {
|
|
243
|
+
activeSaveTimer = null;
|
|
244
|
+
flushActiveJobsSave().catch(err => logger.warn('[workbench] live-jobs save failed:', err?.message || err));
|
|
245
|
+
}, JOBS_SAVE_DEBOUNCE_MS);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 立即写 live-jobs:把本进程活跃 job 写进自己的文件,没有活跃 job 就删掉文件。
|
|
250
|
+
* 终态时(flushJobsSaveNow)必须走一次,否则别的实例的看板上那条会一直转圈,
|
|
251
|
+
* 直到 owner pid 消失才被回收。
|
|
252
|
+
*/
|
|
253
|
+
export function flushActiveJobsSave() {
|
|
254
|
+
if (activeSaveTimer) { clearTimeout(activeSaveTimer); activeSaveTimer = null; }
|
|
255
|
+
const run = () => writeLiveJobsFile(process.pid, activeJobRecords());
|
|
256
|
+
const next = activeWriteChain.then(run, run);
|
|
257
|
+
activeWriteChain = next.catch(() => {});
|
|
258
|
+
return next;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 本进程退出时清掉自己的运行中文件(进程内其它清理路径无需感知这件事) */
|
|
262
|
+
export async function clearOwnLiveJobsFile() {
|
|
263
|
+
if (activeSaveTimer) { clearTimeout(activeSaveTimer); activeSaveTimer = null; }
|
|
264
|
+
await removeLiveJobsFile(process.pid);
|
|
265
|
+
}
|
|
160
266
|
|
|
161
267
|
// 序列化 job 落盘:剥离 child 引用(ChildProcess 无法 JSON.stringify),
|
|
162
268
|
// 反范式 taskTitle 方便管理页直接读
|
|
@@ -181,18 +287,8 @@ export function serializeJob(j, taskMap) {
|
|
|
181
287
|
};
|
|
182
288
|
}
|
|
183
289
|
|
|
184
|
-
// 流式 chunk 阶段 debounce 写盘;终态时由 flushJobsSaveNow() 强制立即落盘
|
|
185
|
-
export function scheduleJobsSave() {
|
|
186
|
-
if (jobsSaveTimer) clearTimeout(jobsSaveTimer);
|
|
187
|
-
jobsSaveTimer = setTimeout(() => {
|
|
188
|
-
jobsSaveTimer = null;
|
|
189
|
-
flushJobsSaveNow().catch(err => logger.warn('[workbench] jobs save failed:', err.message));
|
|
190
|
-
}, JOBS_SAVE_DEBOUNCE_MS);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
290
|
// 立即落盘(终态调用):把 jobs Map 当前快照写到 jobs.json,然后跑 retention
|
|
194
291
|
export async function flushJobsSaveNow() {
|
|
195
|
-
if (jobsSaveTimer) { clearTimeout(jobsSaveTimer); jobsSaveTimer = null; }
|
|
196
292
|
// 读 tasks.json 给落盘 job 反范式 taskTitle——父任务被删后管理页仍可读
|
|
197
293
|
const tasksData = await readJson(TASKS_FILE, { tasks: [] });
|
|
198
294
|
const taskMap = new Map((tasksData.tasks || []).map(t => [t.id, t]));
|
|
@@ -201,9 +297,17 @@ export async function flushJobsSaveNow() {
|
|
|
201
297
|
await refreshJobsFromDisk();
|
|
202
298
|
const payload = {
|
|
203
299
|
version: 1,
|
|
204
|
-
|
|
300
|
+
// 只并「磁盘历史 ∪ 本进程内存」:别人的 live(运行中)记录不进历史档案,见 mergedJobs 注释
|
|
301
|
+
jobs: Array.from(mergedJobs({ includeLive: false }).values()).map(j => serializeJob(j, taskMap))
|
|
205
302
|
};
|
|
206
303
|
await writeJson(JOBS_FILE, payload);
|
|
304
|
+
// 运行中那一份同步收口:这个 job 已经不再 running/pending,别的实例的看板
|
|
305
|
+
// 必须立刻把它从「进行中」挪走,不能干等 owner pid 消失。
|
|
306
|
+
try {
|
|
307
|
+
await flushActiveJobsSave();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
logger.warn('[workbench] live-jobs flush failed:', err?.message || err);
|
|
310
|
+
}
|
|
207
311
|
invalidateDiskCache();
|
|
208
312
|
await enforceRetention();
|
|
209
313
|
}
|
|
@@ -290,6 +394,12 @@ export async function enforceRetention() {
|
|
|
290
394
|
invalidateDiskCache();
|
|
291
395
|
const keepIds = new Set(data.jobs.map(j => j.id));
|
|
292
396
|
for (const id of Array.from(jobs.keys())) {
|
|
397
|
+
const j = jobs.get(id);
|
|
398
|
+
// **运行中的 job 一律不许在这里被删掉**:它还没进过 jobs.json(或者进来的是上一轮的
|
|
399
|
+
// 终态),按 keepIds 一刀切会把它从内存里剜掉 —— 表现是任务跑到一半"消失",界面
|
|
400
|
+
// 也不再有它,而 child 进程还在。启动时 hydrateJobs 的 fire-and-forget 链路
|
|
401
|
+
// (读文件 → 跑 retention)刚好能和"启动后立刻点执行"撞上,实测能复现。
|
|
402
|
+
if (j && (j.status === 'running' || j.status === 'pending')) continue;
|
|
293
403
|
if (!keepIds.has(id)) jobs.delete(id);
|
|
294
404
|
}
|
|
295
405
|
}
|
|
@@ -304,35 +414,15 @@ export function publish(event, payload) {
|
|
|
304
414
|
bus.emit('event', { event, payload, ts: nowIso() });
|
|
305
415
|
}
|
|
306
416
|
|
|
307
|
-
// 给 SSE / 看板 / cancel / continue 路由用:返回「磁盘 ∪ 内存」的可序列化快照
|
|
417
|
+
// 给 SSE / 看板 / cancel / continue 路由用:返回「磁盘 ∪ 运行中 ∪ 内存」的可序列化快照
|
|
308
418
|
// 剥离 child 引用(不可序列化),只保留前端需要的字段。
|
|
309
419
|
//
|
|
310
|
-
//
|
|
420
|
+
// 同步函数,两半缓存读的是 refreshJobsFromDisk() 填好的 —— 想让别的 g ui
|
|
311
421
|
// 进程跑的 job 出现在结果里,调用前先 await 一次刷新(见文件头的说明)。
|
|
422
|
+
//
|
|
423
|
+
// 投影口径只有一份:liveJobs.projectJob(live-jobs 落盘也用同一个函数)。
|
|
312
424
|
export function snapshotJobs() {
|
|
313
|
-
return Array.from(mergedJobs().values()).map(
|
|
314
|
-
id: j.id,
|
|
315
|
-
taskId: j.taskId,
|
|
316
|
-
subId: j.subId,
|
|
317
|
-
title: j.title,
|
|
318
|
-
status: j.status,
|
|
319
|
-
prompt: j.prompt || '',
|
|
320
|
-
output: j.output || '',
|
|
321
|
-
thinking: j.thinking || '',
|
|
322
|
-
pid: j.pid || null,
|
|
323
|
-
startedAt: j.startedAt || null,
|
|
324
|
-
endedAt: j.endedAt || null,
|
|
325
|
-
exitCode: typeof j.exitCode === 'number' ? j.exitCode : null,
|
|
326
|
-
error: j.error || null,
|
|
327
|
-
// 本轮用的执行器(claude | opencode)。前端对话区助手名 / 日志详情按它显示;
|
|
328
|
-
// 加字段时记得同步这里 —— 白名单投影会把没列出的字段静默剥掉。
|
|
329
|
-
agent: j.agent || null,
|
|
330
|
-
// 工具调用流水({id,name,argsPreview,arguments,result,status,error}[])。
|
|
331
|
-
// 前端靠它画工具块;刷新页面 / 换实例后仍然可见,所以必须过白名单。
|
|
332
|
-
toolCalls: Array.isArray(j.toolCalls) ? j.toolCalls : [],
|
|
333
|
-
// 续接对话用:claude --output-format stream-json 的 system.init 事件捕获到的 session_id
|
|
334
|
-
claudeSessionId: j.claudeSessionId || null
|
|
335
|
-
}));
|
|
425
|
+
return Array.from(mergedJobs().values()).map(projectJob);
|
|
336
426
|
}
|
|
337
427
|
|
|
338
428
|
export const jobStore = {
|
|
@@ -340,7 +430,9 @@ export const jobStore = {
|
|
|
340
430
|
bus,
|
|
341
431
|
cancelledJobs,
|
|
342
432
|
serializeJob,
|
|
343
|
-
|
|
433
|
+
scheduleActiveJobsSave,
|
|
434
|
+
flushActiveJobsSave,
|
|
435
|
+
clearOwnLiveJobsFile,
|
|
344
436
|
flushJobsSaveNow,
|
|
345
437
|
readJobsConfig,
|
|
346
438
|
writeJobsConfig,
|
|
@@ -349,5 +441,6 @@ export const jobStore = {
|
|
|
349
441
|
publish,
|
|
350
442
|
snapshotJobs,
|
|
351
443
|
refreshJobsFromDisk,
|
|
444
|
+
refreshLiveJobs,
|
|
352
445
|
mergedJobs,
|
|
353
446
|
};
|