zen-gitsync 2.17.19 → 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.
Files changed (135) hide show
  1. package/README.md +10 -8
  2. package/package.json +3 -1
  3. package/src/config.js +3 -0
  4. package/src/ui/public/assets/AgentEngineSelector-DR3r5eQE.css +1 -0
  5. package/src/ui/public/assets/AgentEngineSelector-D_8VedA0.js +1 -0
  6. package/src/ui/public/assets/AgentView-CpKIfQLq.js +1 -0
  7. package/src/ui/public/assets/AgentView-DnN-vCMi.css +1 -0
  8. package/src/ui/public/assets/{AppVersionBadge-DDYQqoy7.css → AppVersionBadge--5If494j.css} +1 -1
  9. package/src/ui/public/assets/AppVersionBadge-kyk4Mmvf.js +2 -0
  10. package/src/ui/public/assets/{BranchSelector-F4ibldMN.css → BranchSelector-BTCMYwG0.css} +1 -1
  11. package/src/ui/public/assets/BranchSelector-BUPiKcdW.js +1 -0
  12. package/src/ui/public/assets/CommitForm-C-cL8_vz.css +1 -0
  13. package/src/ui/public/assets/CommitForm-CMe1gjY-.js +6 -0
  14. package/src/ui/public/assets/CommonDialog-Bzi5ISqT.css +1 -0
  15. package/src/ui/public/assets/CommonDialog-DMWtstZq.js +1 -0
  16. package/src/ui/public/assets/EditorView-C6msf9YP.js +1 -0
  17. package/src/ui/public/assets/EditorView-mUmFBb7H.css +1 -0
  18. package/src/ui/public/assets/FileDiffViewer-B6zy6NiA.css +1 -0
  19. package/src/ui/public/assets/FileDiffViewer-CvN5b1ZC.js +14 -0
  20. package/src/ui/public/assets/FlowExecutionViewer-B5kh7VxS.css +1 -0
  21. package/src/ui/public/assets/FlowExecutionViewer-Ynr4q8qE.js +1 -0
  22. package/src/ui/public/assets/FlowOrchestrationWorkspace-CM2_9TW3.css +1 -0
  23. package/src/ui/public/assets/FlowOrchestrationWorkspace-DPDZc9uf.js +21 -0
  24. package/src/ui/public/assets/IconButton-CFJkDLVG.js +1 -0
  25. package/src/ui/public/assets/IconButton-W2X09OKH.css +1 -0
  26. package/src/ui/public/assets/LogList-C7q0Y4J_.css +1 -0
  27. package/src/ui/public/assets/LogList-CbfsCVsy.js +7 -0
  28. package/src/ui/public/assets/MindmapView-C4xg3TwX.css +1 -0
  29. package/src/ui/public/assets/MindmapView-D8A1V-Fd.js +1 -0
  30. package/src/ui/public/assets/MonitorView-BWOQfPtI.css +1 -0
  31. package/src/ui/public/assets/MonitorView-Bm_vUP8Y.js +1 -0
  32. package/src/ui/public/assets/ProjectStartupButton-BBND5XcZ.js +1 -0
  33. package/src/ui/public/assets/ProjectStartupButton-VoQqFee4.css +1 -0
  34. package/src/ui/public/assets/RecentDirectoriesChat-BQbncY1L.css +1 -0
  35. package/src/ui/public/assets/RecentDirectoriesChat-D9fMFh8r.js +1 -0
  36. package/src/ui/public/assets/RemoteManagerDialog-BSOph2h2.css +1 -0
  37. package/src/ui/public/assets/RemoteManagerDialog-BVQeIr8U.js +1 -0
  38. package/src/ui/public/assets/RemoteRepoCard-CG9WPcA_.css +1 -0
  39. package/src/ui/public/assets/RemoteRepoCard-vuCu_cpA.js +1 -0
  40. package/src/ui/public/assets/SourceMapView-BZjgxQM8.js +3 -0
  41. package/src/ui/public/assets/SourceMapView-CO_BZJKu.css +1 -0
  42. package/src/ui/public/assets/{SvgIcon-CDNsk4-Q.css → SvgIcon-BQ6bR7l0.css} +1 -1
  43. package/src/ui/public/assets/SvgIcon-D0hK-p1U.js +1 -0
  44. package/src/ui/public/assets/TemplateManager-DiM3vrbf.css +1 -0
  45. package/src/ui/public/assets/TemplateManager-XoPZv_i6.js +1 -0
  46. package/src/ui/public/assets/UserInputNode-CZarmsrv.js +1 -0
  47. package/src/ui/public/assets/UserInputNode-d-oRXM_J.css +1 -0
  48. package/src/ui/public/assets/WorkbenchView-BLyXsdYw.css +1 -0
  49. package/src/ui/public/assets/WorkbenchView-CPur8WEj.js +6 -0
  50. package/src/ui/public/assets/{_plugin-vue_export-helper-Ut942ckG.js → _plugin-vue_export-helper-N8Vc5eEs.js} +5 -5
  51. package/src/ui/public/assets/{agentConversations-C2864Cya.js → agentConversations-Da7zLPfP.js} +2 -2
  52. package/src/ui/public/assets/configStore-BFb0I8eP.js +1 -0
  53. package/src/ui/public/assets/element-plus-Bx3j6vzG.js +28 -0
  54. package/src/ui/public/assets/{flow-mindmap-DnzM9SlK.css → flow-mindmap-BMpmHdqh.css} +1 -1
  55. package/src/ui/public/assets/flow-mindmap-OPE_D4da.js +257 -0
  56. package/src/ui/public/assets/{gitStore-DyaXgTSR.js → gitStore-grKwgyHH.js} +1 -1
  57. package/src/ui/public/assets/index-BHYAwndE.js +29 -0
  58. package/src/ui/public/assets/index-DVJITdth.css +1 -0
  59. package/src/ui/public/assets/office-docx-B0sJQnr-.js +31 -0
  60. package/src/ui/public/assets/{office-excel-BOjWK-1x.js → office-excel-DMRySOdK.js} +1 -1
  61. package/src/ui/public/assets/office-pptx-Do2blxao.js +60 -0
  62. package/src/ui/public/assets/previewSandbox-D4455rKO.js +9 -0
  63. package/src/ui/public/assets/previewSandbox-SqNQvuHv.css +1 -0
  64. package/src/ui/public/assets/socket-io-TBtw0msS.js +1 -0
  65. package/src/ui/public/assets/{toolsStore-DrPVMxvn.js → toolsStore-B49mCaQ_.js} +1 -1
  66. package/src/ui/public/assets/{useThemeObserver-CiUpR4zj.js → useThemeObserver-DYxFoHh1.js} +1 -1
  67. package/src/ui/public/assets/{vendor-B5GNSK12.js → vendor-CyWsJd6S.js} +252 -109
  68. package/src/ui/public/assets/vue-flow-fVVGZspa.js +39 -0
  69. package/src/ui/public/index.html +25 -25
  70. package/src/ui/server/index.js +8 -0
  71. package/src/ui/server/routes/fs.js +98 -0
  72. package/src/ui/server/routes/workbench/index.js +156 -7
  73. package/src/ui/server/routes/workbench/jobStore.js +141 -48
  74. package/src/ui/server/routes/workbench/jobStore.test.js +139 -2
  75. package/src/ui/server/routes/workbench/liveJobs.js +226 -0
  76. package/src/ui/server/routes/workbench/liveJobs.test.js +215 -0
  77. package/src/ui/server/routes/workbench/orchestratorStore.js +113 -2
  78. package/src/ui/server/routes/workbench/progressReport.js +293 -0
  79. package/src/ui/server/routes/workbench/progressReport.test.js +344 -0
  80. package/src/ui/server/routes/workbench/shared.js +28 -0
  81. package/src/ui/server/routes/workbench/taskRunner.js +8 -0
  82. package/src/ui/public/assets/AgentEngineSelector-DUf0TfaU.css +0 -1
  83. package/src/ui/public/assets/AgentEngineSelector-j7LYtcCD.js +0 -2
  84. package/src/ui/public/assets/AgentView-B8zgTjEM.js +0 -10
  85. package/src/ui/public/assets/AgentView-phgvR10P.css +0 -1
  86. package/src/ui/public/assets/AppVersionBadge-eo7ROtED.js +0 -2
  87. package/src/ui/public/assets/BranchSelector-BwrAEUNg.js +0 -1
  88. package/src/ui/public/assets/CommitForm-4kI2csgs.css +0 -1
  89. package/src/ui/public/assets/CommitForm-BIx30QyK.js +0 -6
  90. package/src/ui/public/assets/CommonDialog-CXmqa6q4.js +0 -1
  91. package/src/ui/public/assets/CommonDialog-w6EmBEXv.css +0 -1
  92. package/src/ui/public/assets/EditorView-Bo176FOY.js +0 -31
  93. package/src/ui/public/assets/EditorView-D10uW5AV.css +0 -1
  94. package/src/ui/public/assets/FileDiffViewer-DXnoEMNi.js +0 -15
  95. package/src/ui/public/assets/FileDiffViewer-n5ea9qMe.css +0 -1
  96. package/src/ui/public/assets/FlowExecutionViewer-D_pSWT0R.js +0 -1
  97. package/src/ui/public/assets/FlowExecutionViewer-jNHI5zdm.css +0 -1
  98. package/src/ui/public/assets/FlowOrchestrationWorkspace-CcG6dBBg.js +0 -21
  99. package/src/ui/public/assets/FlowOrchestrationWorkspace-DwuSSxJ1.css +0 -1
  100. package/src/ui/public/assets/IconButton-RlXqKOeu.css +0 -1
  101. package/src/ui/public/assets/IconButton-jqB5gLxI.js +0 -1
  102. package/src/ui/public/assets/LogList-CtrKeYUA.js +0 -7
  103. package/src/ui/public/assets/LogList-XspUhx-C.css +0 -1
  104. package/src/ui/public/assets/MindmapView-DUWUbAgc.js +0 -2
  105. package/src/ui/public/assets/MindmapView-Diq2Cu4O.css +0 -1
  106. package/src/ui/public/assets/MonitorView-CLW2-HBy.js +0 -1
  107. package/src/ui/public/assets/MonitorView-DwUwJXnY.css +0 -1
  108. package/src/ui/public/assets/ProjectStartupButton-CB8IOg1E.css +0 -1
  109. package/src/ui/public/assets/ProjectStartupButton-CErZT4zR.js +0 -1
  110. package/src/ui/public/assets/RecentDirectoriesChat-CO7G6tRt.css +0 -1
  111. package/src/ui/public/assets/RecentDirectoriesChat-cVsO1elA.js +0 -1
  112. package/src/ui/public/assets/RemoteManagerDialog-D4WphMtO.css +0 -1
  113. package/src/ui/public/assets/RemoteManagerDialog-sksn2MmQ.js +0 -4
  114. package/src/ui/public/assets/RemoteRepoCard-Cz_aoLZb.js +0 -7
  115. package/src/ui/public/assets/RemoteRepoCard-o01IrbVz.css +0 -1
  116. package/src/ui/public/assets/SourceMapView-CfDydtsu.css +0 -1
  117. package/src/ui/public/assets/SourceMapView-D5qkdXhn.js +0 -3
  118. package/src/ui/public/assets/SvgIcon-D54dZy2f.js +0 -1
  119. package/src/ui/public/assets/TemplateManager-DO7EPfj8.css +0 -1
  120. package/src/ui/public/assets/TemplateManager-DRHRpB64.js +0 -1
  121. package/src/ui/public/assets/UserInputNode-C4NV2vaL.js +0 -1
  122. package/src/ui/public/assets/UserInputNode-DDNSarag.css +0 -1
  123. package/src/ui/public/assets/WorkbenchView-B3VcBlk7.js +0 -70
  124. package/src/ui/public/assets/WorkbenchView-ZDvzGafG.css +0 -1
  125. package/src/ui/public/assets/configStore-DG0VBR3h.js +0 -1
  126. package/src/ui/public/assets/element-plus-AaVXaSqv.js +0 -48
  127. package/src/ui/public/assets/flow-mindmap-j-zhcNPe.js +0 -139
  128. package/src/ui/public/assets/index-B5MKbT-v.css +0 -1
  129. package/src/ui/public/assets/index-Dq6OyCrE.js +0 -127
  130. package/src/ui/public/assets/office-docx-CJ6JSCs6.js +0 -32
  131. package/src/ui/public/assets/office-pptx-CF43ZwXE.js +0 -74
  132. package/src/ui/public/assets/previewSandbox-CxLaNg4X.css +0 -1
  133. package/src/ui/public/assets/previewSandbox-D2ioUBVm.js +0 -0
  134. package/src/ui/public/assets/socket-io-BFqQXqAp.js +0 -1
  135. package/src/ui/public/assets/vue-flow-DwYB6Myr.js +0 -39
@@ -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-Dq6OyCrE.js"></script>
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-AaVXaSqv.js">
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-B5GNSK12.js">
18
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-Ut942ckG.js">
19
- <link rel="modulepreload" crossorigin href="/assets/SvgIcon-D54dZy2f.js">
20
- <link rel="modulepreload" crossorigin href="/assets/IconButton-jqB5gLxI.js">
21
- <link rel="modulepreload" crossorigin href="/assets/configStore-DG0VBR3h.js">
22
- <link rel="modulepreload" crossorigin href="/assets/socket-io-BFqQXqAp.js">
23
- <link rel="modulepreload" crossorigin href="/assets/gitStore-DyaXgTSR.js">
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-j-zhcNPe.js">
26
- <link rel="modulepreload" crossorigin href="/assets/previewSandbox-D2ioUBVm.js">
27
- <link rel="modulepreload" crossorigin href="/assets/useThemeObserver-CiUpR4zj.js">
28
- <link rel="modulepreload" crossorigin href="/assets/CommonDialog-CXmqa6q4.js">
29
- <link rel="modulepreload" crossorigin href="/assets/FileDiffViewer-DXnoEMNi.js">
30
- <link rel="modulepreload" crossorigin href="/assets/toolsStore-DrPVMxvn.js">
31
- <link rel="modulepreload" crossorigin href="/assets/TemplateManager-DRHRpB64.js">
32
- <link rel="modulepreload" crossorigin href="/assets/vue-flow-DwYB6Myr.js">
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-CDNsk4-Q.css">
37
- <link rel="stylesheet" crossorigin href="/assets/IconButton-RlXqKOeu.css">
38
- <link rel="stylesheet" crossorigin href="/assets/flow-mindmap-DnzM9SlK.css">
39
- <link rel="stylesheet" crossorigin href="/assets/previewSandbox-CxLaNg4X.css">
40
- <link rel="stylesheet" crossorigin href="/assets/CommonDialog-w6EmBEXv.css">
41
- <link rel="stylesheet" crossorigin href="/assets/FileDiffViewer-n5ea9qMe.css">
42
- <link rel="stylesheet" crossorigin href="/assets/TemplateManager-DO7EPfj8.css">
36
+ <link rel="stylesheet" crossorigin href="/assets/SvgIcon-BQ6bR7l0.css">
37
+ <link rel="stylesheet" crossorigin href="/assets/IconButton-W2X09OKH.css">
38
+ <link rel="stylesheet" crossorigin href="/assets/flow-mindmap-BMpmHdqh.css">
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-B5MKbT-v.css">
44
+ <link rel="stylesheet" crossorigin href="/assets/index-DVJITdth.css">
45
45
  </head>
46
46
  <body>
47
47
  <div id="app"></div>
@@ -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);
@@ -354,6 +354,104 @@ export function registerFsRoutes({
354
354
  }
355
355
  }));
356
356
 
357
+ // 一次性取回"根目录 + 已展开子目录"的整棵可见子树,供前端静默刷新文件树。
358
+ //
359
+ // 为什么不复用 GET /api/browse_directory 逐目录并发请求:树展开得越深,请求数越多
360
+ // (多级展开会有十几个到几十个),一轮刷新要等最慢的那个;中途还会出现"部分目录
361
+ // 已刷新、部分没刷"的中间态。这里一次往返把已展开的部分整棵读回来,前端按 path
362
+ // 对账(内容没变就完全不碰 DOM),所以刷新是静默的。
363
+ //
364
+ // 请求体:{ path: 根目录绝对路径, expand?: string[] 已展开目录的绝对路径 }
365
+ // 响应:{ success, items: [...] },只有 expand 里列出的目录才带 children
366
+ // —— 展开态由前端提供,后端不猜,避免把整盘目录读爆。
367
+ const MAX_TREE_EXPAND_DIRS = 200;
368
+
369
+ app.post('/api/browse_directory_tree', express.json(), asyncRoute(async (req, res) => {
370
+ const rawRoot = req.body?.path;
371
+ if (typeof rawRoot !== 'string' || !rawRoot.trim()) {
372
+ res.status(400).json({ success: false, error: '缺少 path 参数' });
373
+ return;
374
+ }
375
+ const rootPath = path.resolve(rawRoot);
376
+
377
+ // 只接受根目录之下的路径:既挡越权(前端传个 C:\Windows 进来),
378
+ // 也顺手挡掉路径拼错。Windows 下比较要忽略大小写。
379
+ const normalize = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
380
+ const normRoot = normalize(rootPath);
381
+ const rootPrefix = normRoot.endsWith(path.sep) ? normRoot : normRoot + path.sep;
382
+ const expandSet = new Set();
383
+ const rawExpand = Array.isArray(req.body?.expand) ? req.body.expand : [];
384
+ for (const p of rawExpand.slice(0, MAX_TREE_EXPAND_DIRS)) {
385
+ if (typeof p !== 'string' || !p.trim()) continue;
386
+ const resolved = path.resolve(p);
387
+ const normResolved = normalize(resolved);
388
+ if (normResolved === normRoot || normResolved.startsWith(rootPrefix)) {
389
+ expandSet.add(resolved);
390
+ }
391
+ }
392
+
393
+ // 读一层目录,排序口径与 GET /api/browse_directory 保持一致(目录在前,各自按名排)。
394
+ async function readDirEntries(dirPath) {
395
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
396
+ const directories = [];
397
+ const files = [];
398
+ for (const item of entries) {
399
+ const fullPath = path.join(dirPath, item.name);
400
+ if (item.isDirectory()) {
401
+ directories.push({ name: item.name, path: fullPath, type: 'directory' });
402
+ } else if (item.isFile()) {
403
+ files.push({ name: item.name, path: fullPath, type: 'file' });
404
+ }
405
+ }
406
+ directories.sort((a, b) => a.name.localeCompare(b.name));
407
+ files.sort((a, b) => a.name.localeCompare(b.name));
408
+ return [...directories, ...files];
409
+ }
410
+
411
+ try {
412
+ // 根目录读不到就老实报错,让前端保留旧树 —— 不能返回空树,
413
+ // 否则一次瞬时 IO 失败会把用户已展开的整棵树清空。
414
+ const rootEntries = await readDirEntries(rootPath);
415
+ const contents = new Map([[rootPath, rootEntries]]);
416
+
417
+ // 逐层(BFS)并行读取:frontier 是"已经拿到内容、还要继续往下读"的目录。
418
+ let frontier = [rootPath];
419
+ while (frontier.length > 0) {
420
+ await Promise.all(frontier.map(async (dir) => {
421
+ try {
422
+ contents.set(dir, await readDirEntries(dir));
423
+ } catch {
424
+ // 单个子目录读失败(权限 / 刚被删掉)—— 退化成空目录,不让整次刷新失败。
425
+ // 轮询刷新是后台行为,不该因为一个坏目录把用户已展开的树整个清掉。
426
+ contents.set(dir, []);
427
+ }
428
+ }));
429
+ const next = [];
430
+ for (const dir of frontier) {
431
+ for (const item of contents.get(dir)) {
432
+ if (item.type === 'directory' && expandSet.has(item.path) && !contents.has(item.path)) {
433
+ next.push(item.path);
434
+ }
435
+ }
436
+ }
437
+ frontier = next;
438
+ }
439
+
440
+ const build = (dir) => contents.get(dir).map((item) => (
441
+ item.type === 'directory' && contents.has(item.path)
442
+ ? { ...item, children: build(item.path) }
443
+ : item
444
+ ));
445
+
446
+ res.json({ success: true, currentPath: rootPath, items: build(rootPath) });
447
+ } catch (error) {
448
+ res.status(400).json({
449
+ success: false,
450
+ error: `无法读取目录 "${rootPath}": ${error.message}`
451
+ });
452
+ }
453
+ }));
454
+
357
455
  // 获取最近访问的目录列表
358
456
  app.get('/api/recent_directories', asyncRoute(async (req, res) => {
359
457
  try {
@@ -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
- const data = await readJson(JOBS_FILE, { version: 1, jobs: [] });
907
- const j = (data.jobs || []).find(x => x.id === req.params.id);
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 必须是布尔值');