@jingyi0605/codingns 0.8.5 → 0.9.0

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 (439) hide show
  1. package/bin/codingns.mjs +7 -156
  2. package/dist/public/assets/AdaptiveButlerPage-B17QiMyT.js +2 -0
  3. package/dist/public/assets/{App-BOHBGFOd.js → App-CFBwDUNA.js} +6 -6
  4. package/dist/public/assets/{BootstrapPage-BxHQT4nA.js → BootstrapPage-W5wU3BPh.js} +1 -1
  5. package/dist/public/assets/{ConversationPage-DWFsF6BB.js → ConversationPage-DQLX1bUh.js} +6 -6
  6. package/dist/public/assets/{DesktopDetachPreviewPage-DOgEjYEf.js → DesktopDetachPreviewPage-DTPeuAW-.js} +1 -1
  7. package/dist/public/assets/DesktopModal-6ii53_Y9.js +1 -0
  8. package/dist/public/assets/DesktopWindowPage-D0blSuKd.js +2 -0
  9. package/dist/public/assets/FileContextPanel-BrKO8Xt6.js +1 -0
  10. package/dist/public/assets/GitSidebar-BdwiDtOr.js +6 -0
  11. package/dist/public/assets/MobileCreateSessionSheet-Cx_dBiBb.js +1 -0
  12. package/dist/public/assets/MobileSheet-opTWyRe1.js +1 -0
  13. package/dist/public/assets/{MobileTopHeaderFrame-lcp2GscV.js → MobileTopHeaderFrame-BbNON3Y4.js} +1 -1
  14. package/dist/public/assets/MobileWorkspaceSwitcherHeader-BZEzPeMj.js +1 -0
  15. package/dist/public/assets/{PluginAccessOverview-DGcKAMQl.js → PluginAccessOverview-mQDmAljp.js} +1 -1
  16. package/dist/public/assets/PluginContainerPage-CcxUJpM4.js +1 -0
  17. package/dist/public/assets/{PluginDetailPage-CAJ7LFpD.js → PluginDetailPage-D5--ACIt.js} +1 -1
  18. package/dist/public/assets/{PluginsListPage-BxZG1NyT.js → PluginsListPage-D_oJxYXT.js} +1 -1
  19. package/dist/public/assets/{RelayConnectEntryPage-CfNO_TIl.js → RelayConnectEntryPage-DROxpnkv.js} +1 -1
  20. package/dist/public/assets/{ServerSettingsModal-by36Z_5k.js → ServerSettingsModal-CUUOPqSe.js} +1 -1
  21. package/dist/public/assets/SessionIndexPage-C2Jxh6Gp.js +1 -0
  22. package/dist/public/assets/SettingsPage-BlAZCHsy.js +2 -0
  23. package/dist/public/assets/{TerminalManagerPanel-NVZRxxmH.js → TerminalManagerPanel-CjzbiWjl.js} +1 -1
  24. package/dist/public/assets/{TerminalPage-C4LNoPBp.js → TerminalPage-CwWyFDj8.js} +2 -2
  25. package/dist/public/assets/TerminalRuntimeFallbackModal-CSVVbO8r.js +1 -0
  26. package/dist/public/assets/{ToolFilesPage-47zbdgTW.js → ToolFilesPage-QBEY8oCf.js} +1 -1
  27. package/dist/public/assets/{ToolGitPage-Fuk_b_jg.js → ToolGitPage-BKoZ2l9v.js} +1 -1
  28. package/dist/public/assets/{ToolProcessesPage-sWSMWD-9.js → ToolProcessesPage-BOH0ib4G.js} +1 -1
  29. package/dist/public/assets/{ToolsHomePage-R1mZlbZi.js → ToolsHomePage-BcMZ3BCQ.js} +1 -1
  30. package/dist/public/assets/{WorkbenchLandingPage-CqmiFH2u.js → WorkbenchLandingPage-B5zoppEl.js} +1 -1
  31. package/dist/public/assets/WorkbenchLayout-BksVkkFF.css +1 -0
  32. package/dist/public/assets/WorkbenchLayout-CikJBS62.js +1019 -0
  33. package/dist/public/assets/{WorkbenchModal-C7qoQElW.js → WorkbenchModal-NGmPgqaE.js} +1 -1
  34. package/dist/public/assets/WorkbenchShellRoute-BbbSOiZw.js +1 -0
  35. package/dist/public/assets/WorkbenchShellRoute-DT3VMjWD.css +1 -0
  36. package/dist/public/assets/WorkspaceDebugDetailPage-CVivdPx5.js +1 -0
  37. package/dist/public/assets/{WorkspaceDetailPage-BPsrFffw.js → WorkspaceDetailPage-DgOSjscR.js} +1 -1
  38. package/dist/public/assets/{WorkspaceHomePage-KAtqZOAb.js → WorkspaceHomePage-HPa7M_Vh.js} +1 -1
  39. package/dist/public/assets/{client-runtime-manager-wmCJZKYd.js → client-runtime-manager-DXbI9K1K.js} +1 -1
  40. package/dist/public/assets/index-BxJPQpFM.css +1 -0
  41. package/dist/public/assets/index-CeXGOT_T.js +50 -0
  42. package/dist/public/assets/{login-direct-candidate-resolver-BOAgTuUf.js → login-direct-candidate-resolver-DkKyFtQJ.js} +1 -1
  43. package/dist/public/assets/{plugin-permission-copy-Cq99cnzV.js → plugin-permission-copy-CzN269Bk.js} +1 -1
  44. package/dist/public/assets/{plugins-api-BQTV5DOp.js → plugins-api-Bv9DHpLF.js} +1 -1
  45. package/dist/public/assets/{preferences-service-DJxbEEeg.js → preferences-service-D2ISL2Zz.js} +1 -1
  46. package/dist/public/assets/{relay-entry-D-LfvdiX.js → relay-entry-Bg0OisQy.js} +1 -1
  47. package/dist/public/assets/{terminal-runtime-meta-BJmy8dyK.js → terminal-runtime-meta-C8t-CIDF.js} +1 -1
  48. package/dist/public/assets/{useRegisteredDebugTemplates-DQAWVdCo.js → useRegisteredDebugTemplates-Bol3NVfN.js} +1 -1
  49. package/dist/public/assets/{workbench-navigation-MEzCSmsK.js → workbench-navigation-B7IjRQd8.js} +1 -1
  50. package/dist/public/index.html +2 -2
  51. package/dist/server/middlewares/auth-guard.js +10 -5
  52. package/dist/server/middlewares/auth-guard.js.map +1 -1
  53. package/dist/server/modules/affairs-indexer/contracts/src/errors/app-error.d.ts +11 -0
  54. package/dist/server/modules/affairs-indexer/contracts/src/errors/app-error.js +22 -0
  55. package/dist/server/modules/affairs-indexer/contracts/src/errors/app-error.js.map +1 -0
  56. package/dist/server/modules/affairs-indexer/contracts/src/errors/error-codes.d.ts +23 -0
  57. package/dist/server/modules/affairs-indexer/contracts/src/errors/error-codes.js +23 -0
  58. package/dist/server/modules/affairs-indexer/contracts/src/errors/error-codes.js.map +1 -0
  59. package/dist/server/modules/affairs-indexer/contracts/src/index.d.ts +4 -0
  60. package/dist/server/modules/affairs-indexer/contracts/src/index.js +5 -0
  61. package/dist/server/modules/affairs-indexer/contracts/src/index.js.map +1 -0
  62. package/dist/server/modules/affairs-indexer/contracts/src/types/cli-command-context.d.ts +7 -0
  63. package/dist/server/modules/affairs-indexer/contracts/src/types/cli-command-context.js +2 -0
  64. package/dist/server/modules/affairs-indexer/contracts/src/types/cli-command-context.js.map +1 -0
  65. package/dist/server/modules/affairs-indexer/contracts/src/types/runtime-config.d.ts +16 -0
  66. package/dist/server/modules/affairs-indexer/contracts/src/types/runtime-config.js +2 -0
  67. package/dist/server/modules/affairs-indexer/contracts/src/types/runtime-config.js.map +1 -0
  68. package/dist/server/modules/affairs-indexer/core/src/config/load-runtime-config.d.ts +10 -0
  69. package/dist/server/modules/affairs-indexer/core/src/config/load-runtime-config.js +215 -0
  70. package/dist/server/modules/affairs-indexer/core/src/config/load-runtime-config.js.map +1 -0
  71. package/dist/server/modules/affairs-indexer/core/src/index.d.ts +31 -0
  72. package/dist/server/modules/affairs-indexer/core/src/index.js +32 -0
  73. package/dist/server/modules/affairs-indexer/core/src/index.js.map +1 -0
  74. package/dist/server/modules/affairs-indexer/core/src/logging/structured-logger.d.ts +20 -0
  75. package/dist/server/modules/affairs-indexer/core/src/logging/structured-logger.js +47 -0
  76. package/dist/server/modules/affairs-indexer/core/src/logging/structured-logger.js.map +1 -0
  77. package/dist/server/modules/affairs-indexer/core/src/parser/base-complex-parser-adapter.d.ts +17 -0
  78. package/dist/server/modules/affairs-indexer/core/src/parser/base-complex-parser-adapter.js +63 -0
  79. package/dist/server/modules/affairs-indexer/core/src/parser/base-complex-parser-adapter.js.map +1 -0
  80. package/dist/server/modules/affairs-indexer/core/src/parser/complex-document-skip-adapter.d.ts +12 -0
  81. package/dist/server/modules/affairs-indexer/core/src/parser/complex-document-skip-adapter.js +42 -0
  82. package/dist/server/modules/affairs-indexer/core/src/parser/complex-document-skip-adapter.js.map +1 -0
  83. package/dist/server/modules/affairs-indexer/core/src/parser/csv-parser-adapter.d.ts +7 -0
  84. package/dist/server/modules/affairs-indexer/core/src/parser/csv-parser-adapter.js +107 -0
  85. package/dist/server/modules/affairs-indexer/core/src/parser/csv-parser-adapter.js.map +1 -0
  86. package/dist/server/modules/affairs-indexer/core/src/parser/document-parser.d.ts +19 -0
  87. package/dist/server/modules/affairs-indexer/core/src/parser/document-parser.js +37 -0
  88. package/dist/server/modules/affairs-indexer/core/src/parser/document-parser.js.map +1 -0
  89. package/dist/server/modules/affairs-indexer/core/src/parser/docx-parser-adapter.d.ts +7 -0
  90. package/dist/server/modules/affairs-indexer/core/src/parser/docx-parser-adapter.js +123 -0
  91. package/dist/server/modules/affairs-indexer/core/src/parser/docx-parser-adapter.js.map +1 -0
  92. package/dist/server/modules/affairs-indexer/core/src/parser/openxml-utils.d.ts +8 -0
  93. package/dist/server/modules/affairs-indexer/core/src/parser/openxml-utils.js +111 -0
  94. package/dist/server/modules/affairs-indexer/core/src/parser/openxml-utils.js.map +1 -0
  95. package/dist/server/modules/affairs-indexer/core/src/parser/parser-adapter.d.ts +42 -0
  96. package/dist/server/modules/affairs-indexer/core/src/parser/parser-adapter.js +2 -0
  97. package/dist/server/modules/affairs-indexer/core/src/parser/parser-adapter.js.map +1 -0
  98. package/dist/server/modules/affairs-indexer/core/src/parser/parser-capability-registry.d.ts +18 -0
  99. package/dist/server/modules/affairs-indexer/core/src/parser/parser-capability-registry.js +91 -0
  100. package/dist/server/modules/affairs-indexer/core/src/parser/parser-capability-registry.js.map +1 -0
  101. package/dist/server/modules/affairs-indexer/core/src/parser/parser-router.d.ts +18 -0
  102. package/dist/server/modules/affairs-indexer/core/src/parser/parser-router.js +59 -0
  103. package/dist/server/modules/affairs-indexer/core/src/parser/parser-router.js.map +1 -0
  104. package/dist/server/modules/affairs-indexer/core/src/parser/parser-skip-repository.d.ts +48 -0
  105. package/dist/server/modules/affairs-indexer/core/src/parser/parser-skip-repository.js +193 -0
  106. package/dist/server/modules/affairs-indexer/core/src/parser/parser-skip-repository.js.map +1 -0
  107. package/dist/server/modules/affairs-indexer/core/src/parser/pdf-parser-adapter.d.ts +7 -0
  108. package/dist/server/modules/affairs-indexer/core/src/parser/pdf-parser-adapter.js +371 -0
  109. package/dist/server/modules/affairs-indexer/core/src/parser/pdf-parser-adapter.js.map +1 -0
  110. package/dist/server/modules/affairs-indexer/core/src/parser/plain-text-parser-adapter.d.ts +10 -0
  111. package/dist/server/modules/affairs-indexer/core/src/parser/plain-text-parser-adapter.js +55 -0
  112. package/dist/server/modules/affairs-indexer/core/src/parser/plain-text-parser-adapter.js.map +1 -0
  113. package/dist/server/modules/affairs-indexer/core/src/parser/plain-text-parser.d.ts +9 -0
  114. package/dist/server/modules/affairs-indexer/core/src/parser/plain-text-parser.js +2 -0
  115. package/dist/server/modules/affairs-indexer/core/src/parser/plain-text-parser.js.map +1 -0
  116. package/dist/server/modules/affairs-indexer/core/src/parser/pptx-parser-adapter.d.ts +7 -0
  117. package/dist/server/modules/affairs-indexer/core/src/parser/pptx-parser-adapter.js +130 -0
  118. package/dist/server/modules/affairs-indexer/core/src/parser/pptx-parser-adapter.js.map +1 -0
  119. package/dist/server/modules/affairs-indexer/core/src/parser/xlsx-parser-adapter.d.ts +7 -0
  120. package/dist/server/modules/affairs-indexer/core/src/parser/xlsx-parser-adapter.js +228 -0
  121. package/dist/server/modules/affairs-indexer/core/src/parser/xlsx-parser-adapter.js.map +1 -0
  122. package/dist/server/modules/affairs-indexer/core/src/repositories/catalog-repository.d.ts +205 -0
  123. package/dist/server/modules/affairs-indexer/core/src/repositories/catalog-repository.js +1471 -0
  124. package/dist/server/modules/affairs-indexer/core/src/repositories/catalog-repository.js.map +1 -0
  125. package/dist/server/modules/affairs-indexer/core/src/repositories/catalog-write-repository.d.ts +161 -0
  126. package/dist/server/modules/affairs-indexer/core/src/repositories/catalog-write-repository.js +1350 -0
  127. package/dist/server/modules/affairs-indexer/core/src/repositories/catalog-write-repository.js.map +1 -0
  128. package/dist/server/modules/affairs-indexer/core/src/scanner/file-scanner.d.ts +32 -0
  129. package/dist/server/modules/affairs-indexer/core/src/scanner/file-scanner.js +208 -0
  130. package/dist/server/modules/affairs-indexer/core/src/scanner/file-scanner.js.map +1 -0
  131. package/dist/server/modules/affairs-indexer/core/src/services/dirty/dirty-scope-resolver.d.ts +30 -0
  132. package/dist/server/modules/affairs-indexer/core/src/services/dirty/dirty-scope-resolver.js +66 -0
  133. package/dist/server/modules/affairs-indexer/core/src/services/dirty/dirty-scope-resolver.js.map +1 -0
  134. package/dist/server/modules/affairs-indexer/core/src/services/export/export-builder.d.ts +33 -0
  135. package/dist/server/modules/affairs-indexer/core/src/services/export/export-builder.js +705 -0
  136. package/dist/server/modules/affairs-indexer/core/src/services/export/export-builder.js.map +1 -0
  137. package/dist/server/modules/affairs-indexer/core/src/services/indexer/allowed-extensions-diff-service.d.ts +80 -0
  138. package/dist/server/modules/affairs-indexer/core/src/services/indexer/allowed-extensions-diff-service.js +193 -0
  139. package/dist/server/modules/affairs-indexer/core/src/services/indexer/allowed-extensions-diff-service.js.map +1 -0
  140. package/dist/server/modules/affairs-indexer/core/src/services/indexer/text-indexer.d.ts +77 -0
  141. package/dist/server/modules/affairs-indexer/core/src/services/indexer/text-indexer.js +467 -0
  142. package/dist/server/modules/affairs-indexer/core/src/services/indexer/text-indexer.js.map +1 -0
  143. package/dist/server/modules/affairs-indexer/core/src/services/mcp/mcp-stdio-server.d.ts +17 -0
  144. package/dist/server/modules/affairs-indexer/core/src/services/mcp/mcp-stdio-server.js +264 -0
  145. package/dist/server/modules/affairs-indexer/core/src/services/mcp/mcp-stdio-server.js.map +1 -0
  146. package/dist/server/modules/affairs-indexer/core/src/services/search/offline-search-service.d.ts +11 -0
  147. package/dist/server/modules/affairs-indexer/core/src/services/search/offline-search-service.js +76 -0
  148. package/dist/server/modules/affairs-indexer/core/src/services/search/offline-search-service.js.map +1 -0
  149. package/dist/server/modules/affairs-indexer/core/src/services/search/search-index-builder.d.ts +26 -0
  150. package/dist/server/modules/affairs-indexer/core/src/services/search/search-index-builder.js +305 -0
  151. package/dist/server/modules/affairs-indexer/core/src/services/search/search-index-builder.js.map +1 -0
  152. package/dist/server/modules/affairs-indexer/core/src/services/tagging/tag-recompute-service.d.ts +53 -0
  153. package/dist/server/modules/affairs-indexer/core/src/services/tagging/tag-recompute-service.js +566 -0
  154. package/dist/server/modules/affairs-indexer/core/src/services/tagging/tag-recompute-service.js.map +1 -0
  155. package/dist/server/modules/affairs-indexer/core/src/services/watch/watch-service.d.ts +47 -0
  156. package/dist/server/modules/affairs-indexer/core/src/services/watch/watch-service.js +227 -0
  157. package/dist/server/modules/affairs-indexer/core/src/services/watch/watch-service.js.map +1 -0
  158. package/dist/server/modules/affairs-indexer/core/src/sqlite/catalog-schema.d.ts +5 -0
  159. package/dist/server/modules/affairs-indexer/core/src/sqlite/catalog-schema.js +245 -0
  160. package/dist/server/modules/affairs-indexer/core/src/sqlite/catalog-schema.js.map +1 -0
  161. package/dist/server/modules/affairs-indexer/core/src/sqlite/detect-catalog-schema.d.ts +14 -0
  162. package/dist/server/modules/affairs-indexer/core/src/sqlite/detect-catalog-schema.js +87 -0
  163. package/dist/server/modules/affairs-indexer/core/src/sqlite/detect-catalog-schema.js.map +1 -0
  164. package/dist/server/modules/affairs-indexer/core/src/sqlite/init-catalog.d.ts +13 -0
  165. package/dist/server/modules/affairs-indexer/core/src/sqlite/init-catalog.js +16 -0
  166. package/dist/server/modules/affairs-indexer/core/src/sqlite/init-catalog.js.map +1 -0
  167. package/dist/server/modules/affairs-indexer/core/src/sqlite/migration-runner.d.ts +22 -0
  168. package/dist/server/modules/affairs-indexer/core/src/sqlite/migration-runner.js +430 -0
  169. package/dist/server/modules/affairs-indexer/core/src/sqlite/migration-runner.js.map +1 -0
  170. package/dist/server/modules/affairs-indexer/core/src/sqlite/open-database.d.ts +9 -0
  171. package/dist/server/modules/affairs-indexer/core/src/sqlite/open-database.js +19 -0
  172. package/dist/server/modules/affairs-indexer/core/src/sqlite/open-database.js.map +1 -0
  173. package/dist/server/modules/affairs-indexer/core/src/tagging/simple-tag-inference.d.ts +21 -0
  174. package/dist/server/modules/affairs-indexer/core/src/tagging/simple-tag-inference.js +94 -0
  175. package/dist/server/modules/affairs-indexer/core/src/tagging/simple-tag-inference.js.map +1 -0
  176. package/dist/server/modules/affairs-indexer/core/src/utils/abort.d.ts +2 -0
  177. package/dist/server/modules/affairs-indexer/core/src/utils/abort.js +13 -0
  178. package/dist/server/modules/affairs-indexer/core/src/utils/abort.js.map +1 -0
  179. package/dist/server/modules/affairs-indexer/core/src/utils/file-streaming.d.ts +9 -0
  180. package/dist/server/modules/affairs-indexer/core/src/utils/file-streaming.js +64 -0
  181. package/dist/server/modules/affairs-indexer/core/src/utils/file-streaming.js.map +1 -0
  182. package/dist/server/modules/affairs-indexer/core/src/utils/root-command-lock.d.ts +10 -0
  183. package/dist/server/modules/affairs-indexer/core/src/utils/root-command-lock.js +230 -0
  184. package/dist/server/modules/affairs-indexer/core/src/utils/root-command-lock.js.map +1 -0
  185. package/dist/server/modules/affairs-indexer/core/src/utils/rss-log.d.ts +2 -0
  186. package/dist/server/modules/affairs-indexer/core/src/utils/rss-log.js +19 -0
  187. package/dist/server/modules/affairs-indexer/core/src/utils/rss-log.js.map +1 -0
  188. package/dist/server/modules/affairs-indexer/internal-command-runner.d.ts +31 -0
  189. package/dist/server/modules/affairs-indexer/internal-command-runner.js +643 -0
  190. package/dist/server/modules/affairs-indexer/internal-command-runner.js.map +1 -0
  191. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +0 -49
  192. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +10 -56
  193. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  194. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +2 -46
  195. package/dist/server/modules/assistant-capability/assistant-capability-service.js +15 -158
  196. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  197. package/dist/server/modules/browser-runtime/opencli-bridge-browser-executor.d.ts +4 -2
  198. package/dist/server/modules/browser-runtime/opencli-bridge-browser-executor.js +62 -21
  199. package/dist/server/modules/browser-runtime/opencli-bridge-browser-executor.js.map +1 -1
  200. package/dist/server/modules/butler/butler-control-session-service.d.ts +3 -4
  201. package/dist/server/modules/butler/butler-control-session-service.js +39 -62
  202. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  203. package/dist/server/modules/butler/butler-controller.d.ts +11 -3
  204. package/dist/server/modules/butler/butler-controller.js +19 -4
  205. package/dist/server/modules/butler/butler-controller.js.map +1 -1
  206. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  207. package/dist/server/modules/butler/butler-profile-service.d.ts +1 -1
  208. package/dist/server/modules/butler/butler-profile-service.js +34 -63
  209. package/dist/server/modules/butler/butler-profile-service.js.map +1 -1
  210. package/dist/server/modules/butler/butler-project-service.d.ts +1 -3
  211. package/dist/server/modules/butler/butler-project-service.js +1 -7
  212. package/dist/server/modules/butler/butler-project-service.js.map +1 -1
  213. package/dist/server/modules/butler/butler-session-service.d.ts +1 -0
  214. package/dist/server/modules/butler/butler-session-service.js +109 -0
  215. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  216. package/dist/server/modules/butler/butler-session-summary-service.js +0 -2
  217. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -1
  218. package/dist/server/modules/butler/butler-workspace-context.d.ts +5 -1
  219. package/dist/server/modules/butler/butler-workspace-context.js +21 -12
  220. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  221. package/dist/server/modules/file/file-content-service.d.ts +11 -0
  222. package/dist/server/modules/file/file-content-service.js +55 -0
  223. package/dist/server/modules/file/file-content-service.js.map +1 -1
  224. package/dist/server/modules/file/file-controller.d.ts +26 -1
  225. package/dist/server/modules/file/file-controller.js +59 -4
  226. package/dist/server/modules/file/file-controller.js.map +1 -1
  227. package/dist/server/modules/file/file-preview-link-service.d.ts +1 -0
  228. package/dist/server/modules/file/file-preview-link-service.js +25 -0
  229. package/dist/server/modules/file/file-preview-link-service.js.map +1 -1
  230. package/dist/server/modules/file/file-preview-service.js +15 -4
  231. package/dist/server/modules/file/file-preview-service.js.map +1 -1
  232. package/dist/server/modules/file/file-preview-types.d.ts +9 -1
  233. package/dist/server/modules/file/file-preview-types.js +8 -1
  234. package/dist/server/modules/file/file-preview-types.js.map +1 -1
  235. package/dist/server/modules/file/recent-modified-file-service.d.ts +15 -0
  236. package/dist/server/modules/file/recent-modified-file-service.js +102 -0
  237. package/dist/server/modules/file/recent-modified-file-service.js.map +1 -0
  238. package/dist/server/modules/file/runtime/codingns-workspace-bridge.js +6 -0
  239. package/dist/server/modules/file/workspace-file-bridge-service.d.ts +11 -0
  240. package/dist/server/modules/file/workspace-file-bridge-service.js +19 -0
  241. package/dist/server/modules/file/workspace-file-bridge-service.js.map +1 -1
  242. package/dist/server/modules/file/workspace-index-apply-service.d.ts +25 -0
  243. package/dist/server/modules/file/workspace-index-apply-service.js +42 -0
  244. package/dist/server/modules/file/workspace-index-apply-service.js.map +1 -0
  245. package/dist/server/modules/office/office-controller.d.ts +15 -1
  246. package/dist/server/modules/office/office-controller.js +26 -1
  247. package/dist/server/modules/office/office-controller.js.map +1 -1
  248. package/dist/server/modules/office/onlyoffice-integration-service.d.ts +78 -0
  249. package/dist/server/modules/office/onlyoffice-integration-service.js +610 -0
  250. package/dist/server/modules/office/onlyoffice-integration-service.js.map +1 -0
  251. package/dist/server/modules/plugins/plugin-file-gateway-service.d.ts +12 -0
  252. package/dist/server/modules/plugins/plugin-file-gateway-service.js +13 -0
  253. package/dist/server/modules/plugins/plugin-file-gateway-service.js.map +1 -1
  254. package/dist/server/modules/preferences/profile-service.d.ts +1 -0
  255. package/dist/server/modules/preferences/profile-service.js +27 -3
  256. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  257. package/dist/server/modules/sessions/codex-app-server-helper-process.js +0 -8
  258. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  259. package/dist/server/modules/sessions/session-controller.d.ts +1 -0
  260. package/dist/server/modules/sessions/session-controller.js +3 -0
  261. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  262. package/dist/server/modules/sessions/session-history-service.d.ts +2 -0
  263. package/dist/server/modules/sessions/session-history-service.js +78 -3
  264. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  265. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +1 -0
  266. package/dist/server/modules/sessions/session-live-runtime-service.js +4 -0
  267. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  268. package/dist/server/modules/sessions/session-permission-request-service.js +0 -4
  269. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -1
  270. package/dist/server/modules/sessions/workspace-session-instruction-watch-service.d.ts +23 -0
  271. package/dist/server/modules/sessions/workspace-session-instruction-watch-service.js +122 -0
  272. package/dist/server/modules/sessions/workspace-session-instruction-watch-service.js.map +1 -0
  273. package/dist/server/modules/sessions/workspace-session-runtime-context-service.d.ts +15 -0
  274. package/dist/server/modules/sessions/workspace-session-runtime-context-service.js +93 -10
  275. package/dist/server/modules/sessions/workspace-session-runtime-context-service.js.map +1 -1
  276. package/dist/server/modules/skills/builtin-skills/codingns-assistant/SKILL.md +6 -7
  277. package/dist/server/modules/skills/builtin-skills/codingns-assistant/references/cli-workflow.md +2 -3
  278. package/dist/server/modules/system/host-resource-controller.d.ts +7 -0
  279. package/dist/server/modules/system/host-resource-controller.js +12 -0
  280. package/dist/server/modules/system/host-resource-controller.js.map +1 -0
  281. package/dist/server/modules/system/host-resource-service.d.ts +54 -0
  282. package/dist/server/modules/system/host-resource-service.js +162 -0
  283. package/dist/server/modules/system/host-resource-service.js.map +1 -0
  284. package/dist/server/modules/tasks/observability-service.d.ts +12 -2
  285. package/dist/server/modules/tasks/observability-service.js +13 -1
  286. package/dist/server/modules/tasks/observability-service.js.map +1 -1
  287. package/dist/server/modules/tasks/task-helper-client.d.ts +36 -2
  288. package/dist/server/modules/tasks/task-helper-client.js +201 -19
  289. package/dist/server/modules/tasks/task-helper-client.js.map +1 -1
  290. package/dist/server/modules/tasks/task-helper-pool.d.ts +37 -0
  291. package/dist/server/modules/tasks/task-helper-pool.js +173 -0
  292. package/dist/server/modules/tasks/task-helper-pool.js.map +1 -0
  293. package/dist/server/modules/tasks/task-helper-process-handlers.d.ts +27 -0
  294. package/dist/server/modules/tasks/task-helper-process-handlers.js +25 -1
  295. package/dist/server/modules/tasks/task-helper-process-handlers.js.map +1 -1
  296. package/dist/server/modules/tasks/task-helper-process.js +75 -26
  297. package/dist/server/modules/tasks/task-helper-process.js.map +1 -1
  298. package/dist/server/modules/tasks/task-helper-scheduling.d.ts +11 -0
  299. package/dist/server/modules/tasks/task-helper-scheduling.js +43 -0
  300. package/dist/server/modules/tasks/task-helper-scheduling.js.map +1 -0
  301. package/dist/server/modules/tasks/task-lane-executors.js +19 -3
  302. package/dist/server/modules/tasks/task-lane-executors.js.map +1 -1
  303. package/dist/server/modules/tasks/task-manager.d.ts +1 -0
  304. package/dist/server/modules/tasks/task-manager.js +3 -0
  305. package/dist/server/modules/tasks/task-manager.js.map +1 -1
  306. package/dist/server/modules/tasks/task-registry.d.ts +1 -0
  307. package/dist/server/modules/tasks/task-registry.js +3 -0
  308. package/dist/server/modules/tasks/task-registry.js.map +1 -1
  309. package/dist/server/modules/tasks/task-scheduler.d.ts +6 -0
  310. package/dist/server/modules/tasks/task-scheduler.js +162 -7
  311. package/dist/server/modules/tasks/task-scheduler.js.map +1 -1
  312. package/dist/server/modules/tasks/task-types.d.ts +28 -3
  313. package/dist/server/modules/tasks/task-types.js +14 -2
  314. package/dist/server/modules/tasks/task-types.js.map +1 -1
  315. package/dist/server/modules/workbench/affairs-assistant-session-snapshot-service.d.ts +68 -0
  316. package/dist/server/modules/workbench/affairs-assistant-session-snapshot-service.js +286 -0
  317. package/dist/server/modules/workbench/affairs-assistant-session-snapshot-service.js.map +1 -0
  318. package/dist/server/modules/workbench/workbench-controller.js +11 -1
  319. package/dist/server/modules/workbench/workbench-controller.js.map +1 -1
  320. package/dist/server/modules/workbench/workbench-service.d.ts +9 -2
  321. package/dist/server/modules/workbench/workbench-service.js +58 -18
  322. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  323. package/dist/server/modules/workspace/affairs-library-controller.d.ts +117 -0
  324. package/dist/server/modules/workspace/affairs-library-controller.js +164 -0
  325. package/dist/server/modules/workspace/affairs-library-controller.js.map +1 -0
  326. package/dist/server/modules/workspace/affairs-library-debug-log.d.ts +23 -0
  327. package/dist/server/modules/workspace/affairs-library-debug-log.js +107 -0
  328. package/dist/server/modules/workspace/affairs-library-debug-log.js.map +1 -0
  329. package/dist/server/modules/workspace/affairs-library-dirty-watch-service.d.ts +52 -0
  330. package/dist/server/modules/workspace/affairs-library-dirty-watch-service.js +555 -0
  331. package/dist/server/modules/workspace/affairs-library-dirty-watch-service.js.map +1 -0
  332. package/dist/server/modules/workspace/affairs-library-preview-link-service.d.ts +24 -0
  333. package/dist/server/modules/workspace/affairs-library-preview-link-service.js +157 -0
  334. package/dist/server/modules/workspace/affairs-library-preview-link-service.js.map +1 -0
  335. package/dist/server/modules/workspace/affairs-library-refresh-contract.d.ts +56 -0
  336. package/dist/server/modules/workspace/affairs-library-refresh-contract.js +48 -0
  337. package/dist/server/modules/workspace/affairs-library-refresh-contract.js.map +1 -0
  338. package/dist/server/modules/workspace/affairs-library-service.d.ts +344 -0
  339. package/dist/server/modules/workspace/affairs-library-service.js +3755 -0
  340. package/dist/server/modules/workspace/affairs-library-service.js.map +1 -0
  341. package/dist/server/modules/workspace/affairs-lightweight-session-controller.d.ts +78 -0
  342. package/dist/server/modules/workspace/affairs-lightweight-session-controller.js +146 -0
  343. package/dist/server/modules/workspace/affairs-lightweight-session-controller.js.map +1 -0
  344. package/dist/server/modules/workspace/affairs-lightweight-session-service.d.ts +133 -0
  345. package/dist/server/modules/workspace/affairs-lightweight-session-service.js +1447 -0
  346. package/dist/server/modules/workspace/affairs-lightweight-session-service.js.map +1 -0
  347. package/dist/server/modules/workspace/affairs-tag-controller.d.ts +107 -0
  348. package/dist/server/modules/workspace/affairs-tag-controller.js +97 -0
  349. package/dist/server/modules/workspace/affairs-tag-controller.js.map +1 -0
  350. package/dist/server/modules/workspace/affairs-tag-service.d.ts +153 -0
  351. package/dist/server/modules/workspace/affairs-tag-service.js +680 -0
  352. package/dist/server/modules/workspace/affairs-tag-service.js.map +1 -0
  353. package/dist/server/modules/workspace/workspace-controller.d.ts +2 -1
  354. package/dist/server/modules/workspace/workspace-controller.js +8 -2
  355. package/dist/server/modules/workspace/workspace-controller.js.map +1 -1
  356. package/dist/server/modules/workspace/workspace-service.js +60 -9
  357. package/dist/server/modules/workspace/workspace-service.js.map +1 -1
  358. package/dist/server/routes/affairs.d.ts +3 -0
  359. package/dist/server/routes/affairs.js +7 -0
  360. package/dist/server/routes/affairs.js.map +1 -0
  361. package/dist/server/routes/assistant.js +0 -5
  362. package/dist/server/routes/assistant.js.map +1 -1
  363. package/dist/server/routes/files.js +5 -0
  364. package/dist/server/routes/files.js.map +1 -1
  365. package/dist/server/routes/office.js +4 -0
  366. package/dist/server/routes/office.js.map +1 -1
  367. package/dist/server/routes/system.d.ts +2 -1
  368. package/dist/server/routes/system.js +2 -1
  369. package/dist/server/routes/system.js.map +1 -1
  370. package/dist/server/routes/workspaces.d.ts +4 -1
  371. package/dist/server/routes/workspaces.js +46 -1
  372. package/dist/server/routes/workspaces.js.map +1 -1
  373. package/dist/server/server/create-server.d.ts +6 -2
  374. package/dist/server/server/create-server.js +113 -32
  375. package/dist/server/server/create-server.js.map +1 -1
  376. package/dist/server/shared/http/error-handler.js +10 -0
  377. package/dist/server/shared/http/error-handler.js.map +1 -1
  378. package/dist/server/storage/repositories/affairs-assistant-session-snapshot-repository.d.ts +10 -0
  379. package/dist/server/storage/repositories/affairs-assistant-session-snapshot-repository.js +47 -0
  380. package/dist/server/storage/repositories/affairs-assistant-session-snapshot-repository.js.map +1 -0
  381. package/dist/server/storage/repositories/butler-profile-repository.js +7 -3
  382. package/dist/server/storage/repositories/butler-profile-repository.js.map +1 -1
  383. package/dist/server/storage/repositories/office-onlyoffice-setting-repository.d.ts +19 -0
  384. package/dist/server/storage/repositories/office-onlyoffice-setting-repository.js +55 -0
  385. package/dist/server/storage/repositories/office-onlyoffice-setting-repository.js.map +1 -0
  386. package/dist/server/storage/repositories/session-index-repository.js +9 -2
  387. package/dist/server/storage/repositories/session-index-repository.js.map +1 -1
  388. package/dist/server/storage/repositories/user-affairs-library-setting-repository.d.ts +10 -0
  389. package/dist/server/storage/repositories/user-affairs-library-setting-repository.js +69 -0
  390. package/dist/server/storage/repositories/user-affairs-library-setting-repository.js.map +1 -0
  391. package/dist/server/storage/repositories/user-preference-profile-repository.js +6 -3
  392. package/dist/server/storage/repositories/user-preference-profile-repository.js.map +1 -1
  393. package/dist/server/storage/repositories/workspace-navigation-state-repository.d.ts +3 -0
  394. package/dist/server/storage/repositories/workspace-navigation-state-repository.js +47 -4
  395. package/dist/server/storage/repositories/workspace-navigation-state-repository.js.map +1 -1
  396. package/dist/server/storage/sqlite/client.js +230 -123
  397. package/dist/server/storage/sqlite/client.js.map +1 -1
  398. package/dist/server/storage/sqlite/schema.sql +48 -25
  399. package/dist/server/types/domain.d.ts +27 -20
  400. package/dist/server/ws/workbench-ws-hub.js +2 -2
  401. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  402. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-permissions.js +0 -2
  403. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-permissions.js.map +1 -1
  404. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +0 -6
  405. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  406. package/package.json +1 -1
  407. package/dist/public/assets/AdaptiveButlerPage-B153lk5H.css +0 -1
  408. package/dist/public/assets/AdaptiveButlerPage-CJw8Ae62.js +0 -3
  409. package/dist/public/assets/DesktopModal-D_A8sgQU.js +0 -1
  410. package/dist/public/assets/DesktopWindowPage-DK7L7osV.js +0 -2
  411. package/dist/public/assets/FileContextPanel-BdCoubcJ.js +0 -1
  412. package/dist/public/assets/GitSidebar-BeZ0hj7A.js +0 -6
  413. package/dist/public/assets/MobileCreateSessionSheet-DfLMVu8q.js +0 -1
  414. package/dist/public/assets/MobileSheet-5kZ-w-gU.js +0 -1
  415. package/dist/public/assets/MobileWorkspaceSwitcherHeader-C6JMiOq_.js +0 -1
  416. package/dist/public/assets/PluginContainerPage-BlY-xJDh.js +0 -1
  417. package/dist/public/assets/SessionIndexPage-DkBp9Mqz.js +0 -1
  418. package/dist/public/assets/SettingsPage-C-ASmJAG.js +0 -2
  419. package/dist/public/assets/TerminalRuntimeFallbackModal-Bzum5nZ0.js +0 -1
  420. package/dist/public/assets/WorkbenchLayout-OFi6CWgH.js +0 -244
  421. package/dist/public/assets/WorkbenchShellRoute-B4XB8SwG.css +0 -1
  422. package/dist/public/assets/WorkbenchShellRoute-BAQe_E0O.js +0 -1
  423. package/dist/public/assets/WorkspaceDebugDetailPage-DhKa6e9y.js +0 -1
  424. package/dist/public/assets/file-tree-icon-Mg1DiBRX.js +0 -590
  425. package/dist/public/assets/index-C4t-vvqk.css +0 -1
  426. package/dist/public/assets/index-CL97fwWB.js +0 -42
  427. package/dist/public/assets/realtime-client-CLafKzzJ.js +0 -1
  428. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.d.ts +0 -32
  429. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.js +0 -93
  430. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.js.map +0 -1
  431. package/dist/server/modules/butler/assistant-sandbox-service.d.ts +0 -69
  432. package/dist/server/modules/butler/assistant-sandbox-service.js +0 -399
  433. package/dist/server/modules/butler/assistant-sandbox-service.js.map +0 -1
  434. package/dist/server/modules/channels/wechat-claw-client.d.ts +0 -51
  435. package/dist/server/modules/channels/wechat-claw-client.js +0 -245
  436. package/dist/server/modules/channels/wechat-claw-client.js.map +0 -1
  437. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.d.ts +0 -18
  438. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js +0 -191
  439. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js.map +0 -1
@@ -0,0 +1,3755 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { AppError } from "../../shared/errors/app-error.js";
4
+ import { nowIso } from "../../shared/utils/time.js";
5
+ import { MAX_TEXT_FILE_BYTES, MAX_PREVIEW_FILE_BYTES, MAX_RESOURCE_PREVIEW_FILE_BYTES } from "../file/file-constants.js";
6
+ import { buildPreviewCapabilities, detectPreviewKind, isResourcePreviewKind } from "../file/file-preview-types.js";
7
+ import { normalizeRelativePath } from "../file/path-normalizer.js";
8
+ import { hashContent } from "../../shared/utils/hash.js";
9
+ import { HOST_TASK_TYPES } from "../tasks/task-types.js";
10
+ import { runAffairsIndexerCommand } from "../affairs-indexer/internal-command-runner.js";
11
+ import { isIncludedHiddenPath, normalizeIncludedHiddenPaths, SUPPORTED_INDEX_EXTENSION_LIST } from "../affairs-indexer/core/src/scanner/file-scanner.js";
12
+ import { writeAffairsLibraryDebugLog } from "./affairs-library-debug-log.js";
13
+ import { AFFAIRS_LIBRARY_DEBUG_EVENTS, AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS, AFFAIRS_LIBRARY_RECONCILE_REASONS, AFFAIRS_LIBRARY_RECONCILE_SCOPES, AFFAIRS_LIBRARY_RECONCILE_STATUSES } from "./affairs-library-refresh-contract.js";
14
+ import { getSharedTaskHelperPool } from "../tasks/task-helper-pool.js";
15
+ const DEFAULT_CONFIG_RELATIVE_PATH = ".ai-index/doc-semantic-index.config.json";
16
+ const INDEX_DIR_RELATIVE_PATH = ".ai-index";
17
+ const EXPORT_DIR_RELATIVE_PATH = ".ai-index/exports";
18
+ const EXPORT_STATUS_RELATIVE_PATH = ".ai-index/exports/status.json";
19
+ const EXPORT_MANIFEST_RELATIVE_PATH = ".ai-index/exports/manifest.json";
20
+ const RUNTIME_STATUS_RELATIVE_PATH = ".ai-index/runtime-status.json";
21
+ const COMMAND_LOCK_DIR_RELATIVE_PATH = ".ai-index/runtime/command.lock";
22
+ const COMMAND_LOCK_OWNER_RELATIVE_PATH = ".ai-index/runtime/command.lock/owner.json";
23
+ const COMMAND_LOCK_HEARTBEAT_RELATIVE_PATH = ".ai-index/runtime/command.lock/heartbeat.json";
24
+ const DEFAULT_EXPORT_MODE = "v2";
25
+ const INDEX_TASK_TIMEOUT_MS = 15 * 60 * 1000;
26
+ const DIRECTORY_HINT_TASK_TIMEOUT_MS = 8_000;
27
+ const INDEX_TASK_QUEUE_WAIT_TIMEOUT_MS = 60_000;
28
+ const DIRECTORY_HINT_QUEUE_WAIT_TIMEOUT_MS = 15_000;
29
+ const INDEX_TASK_COOLDOWN_MS = 15_000;
30
+ const AUTO_TASK_QUIET_WINDOW_MS = 800;
31
+ const AUTO_TASK_RETRY_WINDOW_MS = 1_000;
32
+ const LIGHTWEIGHT_RECONCILE_INTERVAL_MS = 45_000;
33
+ const LIGHTWEIGHT_RECONCILE_DRIFT_TOLERANCE_MS = 1_500;
34
+ const COMMAND_LOCK_STALE_HEARTBEAT_MS = 3 * 60 * 1000;
35
+ const ORPHAN_TASK_RECONCILE_GRACE_MS = 15_000;
36
+ const SNAPSHOT_CACHE_FILE_NAME = "codingns-affairs-snapshot-cache.json";
37
+ const SNAPSHOT_CACHE_SCHEMA_VERSION = 2;
38
+ const HOT_DIRECTORY_CACHE_TTL_MS = 10 * 60 * 1000;
39
+ const HOT_DIRECTORY_MAX_PER_WORKSPACE = 3;
40
+ const LIVE_DIRECTORY_SYNC_SCAN_MAX_DOCUMENTS = 200;
41
+ export class AffairsLibraryService {
42
+ workspaceService;
43
+ workspaceNavigationStateRepository;
44
+ userAffairsLibrarySettingRepository;
45
+ taskManager;
46
+ logger;
47
+ exportCache = new Map();
48
+ autoTaskStateByWorkspace = new Map();
49
+ hotDirectoryCache = new Map();
50
+ lightweightReconcileTimers = new Map();
51
+ reconcileObservationStateByWorkspace = new Map();
52
+ constructor(workspaceService, workspaceNavigationStateRepository, userAffairsLibrarySettingRepository, taskManager, logger) {
53
+ this.workspaceService = workspaceService;
54
+ this.workspaceNavigationStateRepository = workspaceNavigationStateRepository;
55
+ this.userAffairsLibrarySettingRepository = userAffairsLibrarySettingRepository;
56
+ this.taskManager = taskManager;
57
+ this.logger = logger;
58
+ this.registerBackgroundTasks();
59
+ this.resumeEnabledBindings();
60
+ this.syncLightweightReconcileTimers();
61
+ }
62
+ getGlobalBinding(userId) {
63
+ const setting = this.resolveLibrarySetting(userId, null);
64
+ return this.buildBindingFromSetting(setting, null);
65
+ }
66
+ getBinding(workspaceId, userId) {
67
+ const setting = this.resolveLibrarySetting(userId, workspaceId);
68
+ return this.buildBindingFromSetting(setting, workspaceId);
69
+ }
70
+ saveGlobalBinding(userId, rootDir) {
71
+ const normalizedRootDir = this.normalizeAndValidateBindingRootDir(rootDir);
72
+ const timestamp = nowIso();
73
+ const currentSetting = this.resolveLibrarySetting(userId, null);
74
+ const workspaceId = this.resolvePreferredWorkspaceId(currentSetting?.lastWorkspaceId ?? null);
75
+ const nextSetting = this.upsertLibrarySetting({
76
+ userId,
77
+ rootDir: normalizedRootDir,
78
+ enabled: true,
79
+ favoritesJson: currentSetting?.favoritesJson ?? "[]",
80
+ lastWorkspaceId: workspaceId ?? currentSetting?.lastWorkspaceId ?? null,
81
+ createdAt: currentSetting?.createdAt ?? timestamp,
82
+ updatedAt: timestamp
83
+ });
84
+ this.syncLightweightReconcileTimers();
85
+ if (workspaceId) {
86
+ this.scheduleAutoRefresh(workspaceId, "binding_saved");
87
+ }
88
+ return this.buildBindingFromSetting(nextSetting, null);
89
+ }
90
+ setGlobalEnabled(userId, enabled) {
91
+ const currentSetting = this.resolveLibrarySetting(userId, null);
92
+ const rootDir = currentSetting?.rootDir?.trim() ?? "";
93
+ if (!rootDir) {
94
+ throw new AppError({
95
+ statusCode: 409,
96
+ errorCode: "AFFAIRS_LIBRARY_BINDING_REQUIRED",
97
+ detail: "当前用户还没有绑定文档库路径"
98
+ });
99
+ }
100
+ if (enabled) {
101
+ this.assertLibraryRootDir(rootDir);
102
+ }
103
+ const workspaceId = this.resolvePreferredWorkspaceId(currentSetting?.lastWorkspaceId ?? null);
104
+ const nextSetting = this.upsertLibrarySetting({
105
+ userId,
106
+ rootDir,
107
+ enabled,
108
+ favoritesJson: currentSetting?.favoritesJson ?? "[]",
109
+ lastWorkspaceId: workspaceId ?? currentSetting?.lastWorkspaceId ?? null,
110
+ createdAt: currentSetting?.createdAt ?? nowIso(),
111
+ updatedAt: nowIso()
112
+ });
113
+ this.syncLightweightReconcileTimers();
114
+ if (enabled && workspaceId) {
115
+ this.scheduleAutoRefresh(workspaceId, "library_enabled");
116
+ }
117
+ return this.buildBindingFromSetting(nextSetting, null);
118
+ }
119
+ updateGlobalFavorites(userId, favorites) {
120
+ const currentSetting = this.resolveLibrarySetting(userId, null);
121
+ const normalizedFavorites = this.normalizeFavorites(favorites);
122
+ const workspaceId = this.resolvePreferredWorkspaceId(currentSetting?.lastWorkspaceId ?? null);
123
+ const nextSetting = this.upsertLibrarySetting({
124
+ userId,
125
+ rootDir: currentSetting?.rootDir ?? null,
126
+ enabled: currentSetting?.enabled ?? false,
127
+ favoritesJson: JSON.stringify(normalizedFavorites),
128
+ lastWorkspaceId: workspaceId ?? currentSetting?.lastWorkspaceId ?? null,
129
+ createdAt: currentSetting?.createdAt ?? nowIso(),
130
+ updatedAt: nowIso()
131
+ });
132
+ return normalizedFavorites;
133
+ }
134
+ saveBinding(workspaceId, userId, rootDir) {
135
+ this.workspaceService.getWorkspaceOrThrow(workspaceId);
136
+ const normalizedRootDir = this.normalizeAndValidateBindingRootDir(rootDir);
137
+ const timestamp = nowIso();
138
+ const currentSetting = this.resolveLibrarySetting(userId, workspaceId);
139
+ const nextSetting = this.upsertLibrarySetting({
140
+ userId,
141
+ rootDir: normalizedRootDir,
142
+ enabled: true,
143
+ favoritesJson: currentSetting?.favoritesJson ?? "[]",
144
+ lastWorkspaceId: workspaceId,
145
+ createdAt: currentSetting?.createdAt ?? timestamp,
146
+ updatedAt: timestamp
147
+ });
148
+ this.syncLightweightReconcileTimers();
149
+ this.scheduleAutoRefresh(workspaceId, "binding_saved");
150
+ return this.buildBindingFromSetting(nextSetting, workspaceId);
151
+ }
152
+ setEnabled(workspaceId, userId, enabled) {
153
+ this.workspaceService.getWorkspaceOrThrow(workspaceId);
154
+ const currentSetting = this.resolveLibrarySetting(userId, workspaceId);
155
+ const rootDir = currentSetting?.rootDir?.trim() ?? "";
156
+ if (!rootDir) {
157
+ throw new AppError({
158
+ statusCode: 409,
159
+ errorCode: "AFFAIRS_LIBRARY_BINDING_REQUIRED",
160
+ detail: "当前工作区还没有绑定文档库路径"
161
+ });
162
+ }
163
+ if (enabled) {
164
+ this.assertLibraryRootDir(rootDir);
165
+ }
166
+ const nextSetting = this.upsertLibrarySetting({
167
+ userId,
168
+ rootDir,
169
+ enabled,
170
+ favoritesJson: currentSetting?.favoritesJson ?? "[]",
171
+ lastWorkspaceId: workspaceId,
172
+ createdAt: currentSetting?.createdAt ?? nowIso(),
173
+ updatedAt: nowIso()
174
+ });
175
+ this.syncLightweightReconcileTimers();
176
+ if (enabled) {
177
+ this.scheduleAutoRefresh(workspaceId, "library_enabled");
178
+ }
179
+ return this.buildBindingFromSetting(nextSetting, workspaceId);
180
+ }
181
+ getConfig(workspaceId, userId) {
182
+ const binding = this.getBinding(workspaceId, userId);
183
+ if (!binding) {
184
+ return {
185
+ binding: null,
186
+ mirrorRoot: null,
187
+ allowedExtensions: [],
188
+ includedHiddenPaths: [],
189
+ folderOpenBehavior: "double_click",
190
+ configRelativePath: DEFAULT_CONFIG_RELATIVE_PATH,
191
+ canWrite: false
192
+ };
193
+ }
194
+ const config = this.readConfig(binding.rootDir);
195
+ return {
196
+ binding,
197
+ mirrorRoot: config.mirrorRoot,
198
+ allowedExtensions: config.allowedExtensions,
199
+ includedHiddenPaths: config.includedHiddenPaths,
200
+ folderOpenBehavior: config.folderOpenBehavior,
201
+ configRelativePath: DEFAULT_CONFIG_RELATIVE_PATH,
202
+ canWrite: true
203
+ };
204
+ }
205
+ async saveConfig(workspaceId, userId, input) {
206
+ const binding = this.requireBinding(workspaceId, userId);
207
+ this.ensureLibraryEnabled(binding);
208
+ const configPath = path.join(binding.rootDir, DEFAULT_CONFIG_RELATIVE_PATH);
209
+ const current = this.readRawConfigFile(configPath);
210
+ const mirrorRoot = normalizeOptionalAbsolutePath(input.mirrorRoot);
211
+ const allowedExtensions = normalizeAllowedExtensions(input.allowedExtensions ?? current.allowedExtensions ?? []);
212
+ const includedHiddenPaths = normalizeIncludedHiddenPaths(input.includedHiddenPaths ?? current.includedHiddenPaths ?? []);
213
+ const folderOpenBehavior = normalizeFolderOpenBehavior(input.folderOpenBehavior ?? current.folderOpenBehavior);
214
+ const nextPayload = {
215
+ allowedExtensions,
216
+ includedHiddenPaths,
217
+ folderOpenBehavior,
218
+ };
219
+ if (mirrorRoot) {
220
+ nextPayload.mirrorRoot = mirrorRoot;
221
+ }
222
+ else {
223
+ delete nextPayload.mirrorRoot;
224
+ }
225
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
226
+ fs.writeFileSync(configPath, `${JSON.stringify(nextPayload, null, 2)}\n`, "utf8");
227
+ const handle = this.taskManager.enqueue(HOST_TASK_TYPES.affairsLibraryApplyConfig, {
228
+ key: workspaceId,
229
+ source: "affairs_library.apply_config_after_save",
230
+ input: {
231
+ workspaceId,
232
+ rootDir: binding.rootDir
233
+ }
234
+ });
235
+ await handle.promise;
236
+ const nextBinding = this.requireBinding(workspaceId, userId);
237
+ return {
238
+ binding: nextBinding,
239
+ mirrorRoot,
240
+ allowedExtensions,
241
+ includedHiddenPaths,
242
+ folderOpenBehavior,
243
+ configRelativePath: DEFAULT_CONFIG_RELATIVE_PATH,
244
+ canWrite: true,
245
+ applyConfigTaskId: handle.taskId,
246
+ applyConfigStatus: this.readIndexStatus(workspaceId, nextBinding)
247
+ };
248
+ }
249
+ getSnapshot(workspaceId, userId) {
250
+ const binding = this.getBinding(workspaceId, userId);
251
+ const status = this.readIndexStatus(workspaceId, binding);
252
+ const favorites = this.readFavorites(workspaceId, userId);
253
+ if (!binding) {
254
+ return {
255
+ binding: null,
256
+ status,
257
+ tags: [],
258
+ favorites,
259
+ folders: [],
260
+ documentCount: 0,
261
+ lastError: null
262
+ };
263
+ }
264
+ if (!binding.enabled) {
265
+ return {
266
+ binding,
267
+ status,
268
+ tags: [],
269
+ favorites,
270
+ folders: [],
271
+ documentCount: 0,
272
+ lastError: status.errorSummary
273
+ };
274
+ }
275
+ const exportData = this.readAvailableExportData(binding.rootDir);
276
+ if (!exportData) {
277
+ return {
278
+ binding,
279
+ status: status.state === "fresh"
280
+ ? {
281
+ ...status,
282
+ state: "stale",
283
+ errorSummary: "当前还没有可读取的文档库导出结果,先运行一次索引刷新。"
284
+ }
285
+ : status,
286
+ tags: [],
287
+ favorites,
288
+ folders: [],
289
+ documentCount: 0,
290
+ lastError: "当前还没有可读取的文档库导出结果,先运行一次索引刷新。"
291
+ };
292
+ }
293
+ return {
294
+ binding,
295
+ status: {
296
+ ...status,
297
+ lastCompletedAt: status.lastCompletedAt ?? exportData.generatedAt ?? status.lastCompletedAt
298
+ },
299
+ tags: exportData.tags,
300
+ favorites,
301
+ folders: exportData.folders,
302
+ documentCount: exportData.documents.length,
303
+ lastError: status.errorSummary
304
+ };
305
+ }
306
+ listDocuments(workspaceId, userId, input) {
307
+ const binding = this.getBinding(workspaceId, userId);
308
+ if (!binding || !binding.enabled) {
309
+ return {
310
+ total: 0,
311
+ offset: 0,
312
+ limit: normalizePositiveInt(input.limit, 120, 400),
313
+ items: [],
314
+ tagFacetCounts: {},
315
+ directoryStatus: null
316
+ };
317
+ }
318
+ const favorites = this.readFavorites(workspaceId, userId);
319
+ const exportData = this.readAvailableExportData(binding.rootDir);
320
+ const browseMode = input.browseMode === "tag" ? "tag" : "folder";
321
+ const offset = Math.max(0, normalizePositiveInt(input.offset, 0, Number.MAX_SAFE_INTEGER));
322
+ const limit = normalizePositiveInt(input.limit, 120, 400);
323
+ const indexStatus = this.readIndexStatus(workspaceId, binding);
324
+ const selectedFavorite = favorites.find((item) => buildFavoriteNodeId(item.kind, item.path) === (input.selectedFavoriteId?.trim() ?? "")) ?? null;
325
+ const normalizedSelectedTagPaths = normalizeSelectedTagPaths(input.selectedTagPaths);
326
+ if (browseMode === "folder") {
327
+ return this.listLiveFolderDocuments(workspaceId, binding.rootDir, favorites, exportData, selectedFavorite, {
328
+ selectedFolderPath: input.selectedFolderPath,
329
+ offset,
330
+ limit,
331
+ indexStatus
332
+ });
333
+ }
334
+ if (!exportData) {
335
+ return {
336
+ total: 0,
337
+ offset,
338
+ limit,
339
+ items: [],
340
+ tagFacetCounts: {},
341
+ directoryStatus: null
342
+ };
343
+ }
344
+ const filtered = exportData.documents.filter((document) => {
345
+ if (browseMode === "tag") {
346
+ const tagPaths = selectedFavorite?.kind === "tag"
347
+ ? [selectedFavorite.path]
348
+ : normalizedSelectedTagPaths.length > 0
349
+ ? normalizedSelectedTagPaths
350
+ : (input.selectedTagPath?.trim() ? [input.selectedTagPath.trim()] : []);
351
+ return tagPaths.length === 0 || tagPaths.every((tagPath) => matchesTagPath(document, tagPath));
352
+ }
353
+ const folderPath = selectedFavorite?.kind === "folder"
354
+ ? selectedFavorite.path
355
+ : (input.selectedFolderPath?.trim() ?? "");
356
+ return matchesDirectFolder(document.path, folderPath);
357
+ });
358
+ const items = filtered.slice(offset, offset + limit).map((document) => {
359
+ const fileStats = readAffairsLibraryStatsSafe(binding.rootDir, document.path);
360
+ return {
361
+ ...document,
362
+ createdAt: document.createdAt ?? toIsoOrNull(fileStats?.birthtime),
363
+ sizeBytes: document.sizeBytes ?? fileStats?.size ?? null,
364
+ isFavorite: favorites.some((favorite) => matchesFavorite(favorite, document.path, document.tags, document.derivedTags))
365
+ };
366
+ });
367
+ return {
368
+ total: filtered.length,
369
+ offset,
370
+ limit,
371
+ items,
372
+ tagFacetCounts: browseMode === "tag"
373
+ ? buildTagFacetCounts(exportData.documents, normalizedSelectedTagPaths, selectedFavorite?.kind === "tag" ? selectedFavorite.path : null)
374
+ : {},
375
+ directoryStatus: null
376
+ };
377
+ }
378
+ listLiveFolderDocuments(workspaceId, rootDir, favorites, exportData, selectedFavorite, input) {
379
+ const folderPath = selectedFavorite?.kind === "folder"
380
+ ? selectedFavorite.path
381
+ : (input.selectedFolderPath?.trim() ?? "");
382
+ const normalizedFolderPath = normalizeFolderPath(folderPath);
383
+ const normalizedDirectoryPath = normalizedFolderPath || ".";
384
+ const directoryStatus = this.readDirectoryStatus(workspaceId, rootDir, normalizedDirectoryPath, "snapshot");
385
+ const cacheKey = buildHotDirectoryCacheKey(workspaceId, normalizedDirectoryPath);
386
+ const cachedDirectoryEntry = this.hotDirectoryCache.get(cacheKey) ?? null;
387
+ const liveScanDecision = this.decideLiveDirectoryScan(input.indexStatus, directoryStatus, cachedDirectoryEntry, normalizedFolderPath, exportData);
388
+ const fallbackResult = liveScanDecision.avoidSyncScan
389
+ ? this.buildCachedFolderDocuments(workspaceId, rootDir, normalizedFolderPath, exportData, directoryStatus, liveScanDecision.staleReason)
390
+ : null;
391
+ const liveScanStartedAtMs = liveScanDecision.avoidSyncScan ? 0 : Date.now();
392
+ const directoryResult = fallbackResult ?? this.buildFreshFolderDocuments(rootDir, normalizedFolderPath, exportData);
393
+ const liveScanDurationMs = liveScanDecision.avoidSyncScan
394
+ ? null
395
+ : Math.max(0, Date.now() - liveScanStartedAtMs);
396
+ const itemsWithFavorites = directoryResult.items.map((item) => ({
397
+ ...item,
398
+ isFavorite: favorites.some((favorite) => matchesFavorite(favorite, item.path, item.tags, item.derivedTags))
399
+ }));
400
+ const items = [...itemsWithFavorites].sort((left, right) => {
401
+ const rightTime = Date.parse(right.updatedAt);
402
+ const leftTime = Date.parse(left.updatedAt);
403
+ if (Number.isFinite(rightTime) && Number.isFinite(leftTime) && rightTime !== leftTime) {
404
+ return rightTime - leftTime;
405
+ }
406
+ return left.path.localeCompare(right.path, "zh-Hans-CN");
407
+ });
408
+ const effectiveSource = liveScanDecision.avoidSyncScan && fallbackResult
409
+ ? fallbackResult.source
410
+ : directoryResult.source;
411
+ if (!liveScanDecision.avoidSyncScan) {
412
+ this.updateHotDirectoryCache(workspaceId, rootDir, normalizedDirectoryPath, items, directoryResult.source, {
413
+ preserveStatus: directoryStatus.state === "running" || directoryStatus.state === "queued"
414
+ || directoryStatus.state === "queue_timeout",
415
+ generatedAt: directoryResult.generatedAt,
416
+ filesystemObservedAt: directoryResult.filesystemObservedAt,
417
+ staleReason: null
418
+ });
419
+ writeAffairsLibraryDebugLog({
420
+ event: "directory_live_scan_sync",
421
+ processRole: "host",
422
+ workspaceId,
423
+ rootDir,
424
+ source: "affairs_library.folder_list",
425
+ targetPath: normalizedDirectoryPath,
426
+ status: directoryResult.source,
427
+ durationMs: liveScanDurationMs,
428
+ details: {
429
+ estimatedDocumentCount: liveScanDecision.estimatedDocumentCount,
430
+ itemCount: items.length,
431
+ generatedAt: directoryResult.generatedAt,
432
+ filesystemObservedAt: directoryResult.filesystemObservedAt
433
+ }
434
+ });
435
+ }
436
+ else {
437
+ const fallbackEntry = this.getOrCreateHotDirectoryEntry(workspaceId, rootDir, normalizedDirectoryPath);
438
+ fallbackEntry.items = directoryResult.items;
439
+ fallbackEntry.source = directoryResult.source;
440
+ fallbackEntry.generatedAt = directoryResult.generatedAt;
441
+ fallbackEntry.filesystemObservedAt = directoryResult.filesystemObservedAt;
442
+ fallbackEntry.staleReason = directoryResult.staleReason;
443
+ this.writeDirectoryFallbackDebugLog(workspaceId, rootDir, normalizedDirectoryPath, liveScanDecision, directoryResult);
444
+ this.ensureDirectoryWindow(workspaceId, rootDir, normalizedDirectoryPath);
445
+ }
446
+ this.ensureDirectoryWindow(workspaceId, rootDir, normalizedDirectoryPath);
447
+ if (!liveScanDecision.avoidSyncScan
448
+ && directoryStatus.state !== "running"
449
+ && directoryStatus.state !== "queued") {
450
+ this.scheduleDirectoryHintRefresh(workspaceId, normalizedDirectoryPath, "list_documents");
451
+ }
452
+ else if (liveScanDecision.avoidSyncScan
453
+ && liveScanDecision.staleReason?.startsWith("large_directory:")
454
+ && directoryStatus.state !== "running"
455
+ && directoryStatus.state !== "queued") {
456
+ this.scheduleDirectoryHintRefresh(workspaceId, normalizedDirectoryPath, "large_directory_live_scan");
457
+ }
458
+ const resultItems = items.slice(input.offset, input.offset + input.limit);
459
+ writeAffairsLibraryDebugLog({
460
+ event: "folder_list_served",
461
+ processRole: "host",
462
+ workspaceId,
463
+ rootDir,
464
+ source: "affairs_library.folder_list",
465
+ targetPath: normalizedDirectoryPath,
466
+ status: "served",
467
+ details: {
468
+ resultSource: effectiveSource,
469
+ usedCachedResult: liveScanDecision.avoidSyncScan,
470
+ indexState: input.indexStatus.state,
471
+ directoryState: directoryStatus.state,
472
+ staleReason: directoryResult.staleReason,
473
+ generatedAt: directoryResult.generatedAt,
474
+ filesystemObservedAt: directoryResult.filesystemObservedAt,
475
+ estimatedDocumentCount: liveScanDecision.estimatedDocumentCount,
476
+ total: items.length,
477
+ returned: resultItems.length,
478
+ offset: input.offset,
479
+ limit: input.limit,
480
+ cachedItemCount: cachedDirectoryEntry?.items.length ?? 0
481
+ }
482
+ });
483
+ return {
484
+ total: items.length,
485
+ offset: input.offset,
486
+ limit: input.limit,
487
+ items: resultItems,
488
+ tagFacetCounts: {},
489
+ directoryStatus: this.readDirectoryStatus(workspaceId, rootDir, normalizedDirectoryPath, effectiveSource)
490
+ };
491
+ }
492
+ listFiles(workspaceId, userId, requestedPath, limit = 200) {
493
+ const resolved = this.resolvePreviewFile(workspaceId, userId, requestedPath ?? "", {
494
+ mustExist: true,
495
+ kind: "directory",
496
+ allowRoot: true
497
+ });
498
+ const items = fs
499
+ .readdirSync(resolved.absolutePath, { withFileTypes: true })
500
+ .filter((entry) => !entry.isSymbolicLink())
501
+ .reduce((result, entry) => {
502
+ const childRelativePath = resolved.relativePath
503
+ ? `${resolved.relativePath}/${entry.name}`
504
+ : entry.name;
505
+ const normalizedChildPath = childRelativePath.replace(/\\/g, "/");
506
+ if (normalizedChildPath === ".ai-index" || normalizedChildPath.startsWith(".ai-index/")) {
507
+ return result;
508
+ }
509
+ const childAbsolutePath = path.join(resolved.absolutePath, entry.name);
510
+ const childStats = fs.statSync(childAbsolutePath);
511
+ result.push({
512
+ path: normalizedChildPath,
513
+ name: entry.name,
514
+ kind: entry.isDirectory() ? "directory" : "file",
515
+ size: entry.isDirectory() ? null : childStats.size,
516
+ updatedAt: childStats.mtime.toISOString()
517
+ });
518
+ return result;
519
+ }, []);
520
+ return items.slice(0, limit).sort((left, right) => {
521
+ if (left.kind !== right.kind) {
522
+ return left.kind === "directory" ? -1 : 1;
523
+ }
524
+ return left.name.localeCompare(right.name, "zh-Hans-CN");
525
+ });
526
+ }
527
+ decideLiveDirectoryScan(indexStatus, directoryStatus, cachedDirectoryEntry, normalizedFolderPath, exportData) {
528
+ const estimatedDocumentCount = estimateFolderDocumentCount(normalizedFolderPath, exportData, cachedDirectoryEntry);
529
+ if (typeof estimatedDocumentCount === "number"
530
+ && estimatedDocumentCount > LIVE_DIRECTORY_SYNC_SCAN_MAX_DOCUMENTS) {
531
+ return {
532
+ avoidSyncScan: true,
533
+ staleReason: `large_directory:${estimatedDocumentCount}`,
534
+ estimatedDocumentCount
535
+ };
536
+ }
537
+ if (!cachedDirectoryEntry || cachedDirectoryEntry.items.length === 0) {
538
+ return {
539
+ avoidSyncScan: false,
540
+ staleReason: null,
541
+ estimatedDocumentCount
542
+ };
543
+ }
544
+ if (cachedDirectoryEntry.dirty) {
545
+ return {
546
+ avoidSyncScan: false,
547
+ staleReason: null,
548
+ estimatedDocumentCount
549
+ };
550
+ }
551
+ if (indexStatus.state === "running") {
552
+ return {
553
+ avoidSyncScan: true,
554
+ staleReason: "index_running",
555
+ estimatedDocumentCount
556
+ };
557
+ }
558
+ if (!directoryStatus) {
559
+ return {
560
+ avoidSyncScan: false,
561
+ staleReason: null,
562
+ estimatedDocumentCount
563
+ };
564
+ }
565
+ return {
566
+ avoidSyncScan: directoryStatus.state === "running",
567
+ staleReason: directoryStatus.state === "running" ? "directory_hint_running" : null,
568
+ estimatedDocumentCount
569
+ };
570
+ }
571
+ buildCachedFolderDocuments(workspaceId, rootDir, normalizedFolderPath, exportData, directoryStatus, staleReason) {
572
+ const cacheKey = buildHotDirectoryCacheKey(workspaceId, normalizedFolderPath || ".");
573
+ const cached = this.hotDirectoryCache.get(cacheKey);
574
+ if (cached && cached.items.length > 0) {
575
+ return {
576
+ items: cached.items,
577
+ source: staleReason ? "stale_fallback" : cached.source,
578
+ generatedAt: cached.generatedAt,
579
+ filesystemObservedAt: cached.filesystemObservedAt,
580
+ staleReason
581
+ };
582
+ }
583
+ const snapshotResult = this.buildSnapshotFolderDocuments(rootDir, normalizedFolderPath, exportData, directoryStatus);
584
+ return staleReason
585
+ ? {
586
+ ...snapshotResult,
587
+ source: "stale_fallback",
588
+ staleReason
589
+ }
590
+ : snapshotResult;
591
+ }
592
+ buildSnapshotFolderDocuments(rootDir, normalizedFolderPath, exportData, directoryStatus) {
593
+ const items = (exportData?.documents ?? [])
594
+ .filter((document) => matchesDirectFolder(document.path, normalizedFolderPath))
595
+ .map((document) => ({
596
+ ...document,
597
+ isFavorite: false
598
+ }));
599
+ return {
600
+ items,
601
+ source: directoryStatus?.source ?? "snapshot",
602
+ generatedAt: exportData?.generatedAt ?? directoryStatus?.generatedAt ?? null,
603
+ filesystemObservedAt: directoryStatus?.filesystemObservedAt ?? null,
604
+ staleReason: directoryStatus?.staleReason ?? null
605
+ };
606
+ }
607
+ buildFreshFolderDocuments(rootDir, normalizedFolderPath, exportData) {
608
+ return buildAffairsFolderDocumentsFromFilesystem(rootDir, normalizedFolderPath, exportData, this.readConfig(rootDir));
609
+ }
610
+ buildLiveDirectoryDocuments(rootDir, normalizedFolderPath, exportData, configuredExtensions, supportedExtensions, includedHiddenPaths) {
611
+ return buildAffairsFolderDocumentsFromFilesystem(rootDir, normalizedFolderPath, exportData, {
612
+ mirrorRoot: null,
613
+ allowedExtensions: [...configuredExtensions],
614
+ includedHiddenPaths: [...includedHiddenPaths]
615
+ }, supportedExtensions);
616
+ }
617
+ writeDirectoryFallbackDebugLog(workspaceId, rootDir, directoryPath, decision, result) {
618
+ writeAffairsLibraryDebugLog({
619
+ event: "directory_live_scan_deferred",
620
+ processRole: "host",
621
+ workspaceId,
622
+ rootDir,
623
+ source: "affairs_library.folder_list",
624
+ targetPath: directoryPath,
625
+ status: result.source,
626
+ reason: decision.staleReason,
627
+ details: {
628
+ estimatedDocumentCount: decision.estimatedDocumentCount,
629
+ generatedAt: result.generatedAt,
630
+ filesystemObservedAt: result.filesystemObservedAt,
631
+ itemCount: result.items.length
632
+ }
633
+ });
634
+ }
635
+ updateFavorites(workspaceId, userId, favorites) {
636
+ this.workspaceService.getWorkspaceOrThrow(workspaceId);
637
+ const currentSetting = this.resolveLibrarySetting(userId, workspaceId);
638
+ const normalizedFavorites = this.normalizeFavorites(favorites);
639
+ const nextSetting = this.upsertLibrarySetting({
640
+ userId,
641
+ rootDir: currentSetting?.rootDir ?? null,
642
+ enabled: currentSetting?.enabled ?? false,
643
+ favoritesJson: JSON.stringify(normalizedFavorites),
644
+ lastWorkspaceId: workspaceId,
645
+ createdAt: currentSetting?.createdAt ?? nowIso(),
646
+ updatedAt: nowIso()
647
+ });
648
+ return normalizedFavorites;
649
+ }
650
+ previewDocument(workspaceId, userId, requestedPath) {
651
+ const resolved = this.resolvePreviewFile(workspaceId, userId, requestedPath, {
652
+ mustExist: true,
653
+ kind: "file"
654
+ });
655
+ const previewKind = detectPreviewKind(resolved.relativePath);
656
+ const fileSize = resolved.stats?.size ?? 0;
657
+ if (isResourcePreviewKind(previewKind)
658
+ && previewKind !== "office"
659
+ && fileSize > MAX_RESOURCE_PREVIEW_FILE_BYTES) {
660
+ return this.buildPreviewResult({
661
+ workspaceId,
662
+ path: resolved.relativePath,
663
+ supported: false,
664
+ kind: "unsupported",
665
+ reason: "文件过大,当前内置资源预览暂不处理这么大的文件",
666
+ content: null,
667
+ version: null,
668
+ size: fileSize,
669
+ updatedAt: resolved.stats?.mtime.toISOString() ?? null
670
+ });
671
+ }
672
+ if (!isResourcePreviewKind(previewKind) && fileSize > MAX_PREVIEW_FILE_BYTES) {
673
+ return this.buildPreviewResult({
674
+ workspaceId,
675
+ path: resolved.relativePath,
676
+ supported: false,
677
+ kind: "unsupported",
678
+ reason: "文件过大,本轮只提供轻量预览",
679
+ content: null,
680
+ version: null,
681
+ size: fileSize,
682
+ updatedAt: resolved.stats?.mtime.toISOString() ?? null
683
+ });
684
+ }
685
+ if (previewKind === "image" || previewKind === "pdf" || previewKind === "office") {
686
+ return this.buildPreviewResult({
687
+ workspaceId,
688
+ path: resolved.relativePath,
689
+ supported: true,
690
+ kind: previewKind,
691
+ reason: null,
692
+ content: null,
693
+ version: previewKind === "office"
694
+ ? buildOfficeDocumentVersion(fileSize, resolved.stats?.mtime.toISOString() ?? null)
695
+ : null,
696
+ size: fileSize,
697
+ updatedAt: resolved.stats?.mtime.toISOString() ?? null
698
+ });
699
+ }
700
+ const buffer = fs.readFileSync(resolved.absolutePath);
701
+ if (buffer.includes(0)) {
702
+ return this.buildPreviewResult({
703
+ workspaceId,
704
+ path: resolved.relativePath,
705
+ supported: false,
706
+ kind: "binary",
707
+ reason: "二进制文件暂不支持直接预览",
708
+ content: null,
709
+ version: null,
710
+ size: fileSize || buffer.byteLength,
711
+ updatedAt: resolved.stats?.mtime.toISOString() ?? null
712
+ });
713
+ }
714
+ return this.buildPreviewResult({
715
+ workspaceId,
716
+ path: resolved.relativePath,
717
+ supported: true,
718
+ kind: previewKind,
719
+ reason: null,
720
+ content: buffer.toString("utf8"),
721
+ version: shouldEnableAffairsLibraryInlineEditing(previewKind, fileSize || buffer.byteLength)
722
+ ? hashContent(buffer)
723
+ : null,
724
+ size: fileSize || buffer.byteLength,
725
+ updatedAt: resolved.stats?.mtime.toISOString() ?? null
726
+ });
727
+ }
728
+ downloadFile(workspaceId, userId, requestedPath) {
729
+ const resolved = this.resolvePreviewFile(workspaceId, userId, requestedPath, {
730
+ mustExist: true,
731
+ kind: "file"
732
+ });
733
+ this.ensureUserContentPath(resolved.relativePath);
734
+ const buffer = fs.readFileSync(resolved.absolutePath);
735
+ const stats = resolved.stats ?? fs.statSync(resolved.absolutePath);
736
+ return {
737
+ workspaceId,
738
+ path: resolved.relativePath,
739
+ fileName: path.basename(resolved.relativePath) || resolved.relativePath,
740
+ contentBase64: buffer.toString("base64"),
741
+ size: buffer.byteLength,
742
+ updatedAt: stats.mtime.toISOString()
743
+ };
744
+ }
745
+ operateFile(workspaceId, userId, input) {
746
+ const opType = input.opType;
747
+ if (opType !== "delete"
748
+ && opType !== "move"
749
+ && opType !== "copy"
750
+ && opType !== "create_directory"
751
+ && opType !== "create_file"
752
+ && opType !== "write") {
753
+ throw new AppError({
754
+ statusCode: 400,
755
+ errorCode: "INVALID_FILE_OPERATION",
756
+ detail: "不支持的文档库文件操作",
757
+ field: "opType"
758
+ });
759
+ }
760
+ if (opType === "create_directory" || opType === "create_file") {
761
+ const target = this.resolvePreviewFile(workspaceId, userId, input.dstPath ?? "", {
762
+ mustExist: false,
763
+ kind: opType === "create_directory" ? "directory" : "file"
764
+ });
765
+ this.ensureUserContentPath(target.relativePath);
766
+ if (target.exists) {
767
+ throw new AppError({
768
+ statusCode: 409,
769
+ errorCode: "FILE_ALREADY_EXISTS",
770
+ detail: "目标路径已存在",
771
+ field: "dstPath"
772
+ });
773
+ }
774
+ fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
775
+ if (opType === "create_directory") {
776
+ fs.mkdirSync(target.absolutePath);
777
+ }
778
+ else {
779
+ fs.writeFileSync(target.absolutePath, input.content ?? "", "utf8");
780
+ }
781
+ this.afterFileMutation(workspaceId, target.rootDir, `library_${opType}:${target.relativePath}`, target.relativePath);
782
+ return {
783
+ success: true,
784
+ opType,
785
+ sourcePath: target.relativePath,
786
+ targetPath: target.relativePath
787
+ };
788
+ }
789
+ const source = this.resolvePreviewFile(workspaceId, userId, input.srcPath ?? "", {
790
+ mustExist: true,
791
+ kind: "any"
792
+ });
793
+ this.ensureUserContentPath(source.relativePath);
794
+ if (opType === "write") {
795
+ if (!source.stats?.isFile()) {
796
+ throw new AppError({
797
+ statusCode: 400,
798
+ errorCode: "NOT_A_FILE",
799
+ detail: "指定路径不是文件",
800
+ field: "srcPath"
801
+ });
802
+ }
803
+ const currentBuffer = fs.readFileSync(source.absolutePath);
804
+ ensureEditableAffairsLibraryTextBuffer(currentBuffer);
805
+ const currentVersion = hashContent(currentBuffer);
806
+ const expectedVersion = input.expectedVersion?.trim() ?? "";
807
+ if (!expectedVersion) {
808
+ throw new AppError({
809
+ statusCode: 400,
810
+ errorCode: "INVALID_CONTENT",
811
+ detail: "保存文件必须提供 expectedVersion",
812
+ field: "expectedVersion"
813
+ });
814
+ }
815
+ if (expectedVersion !== currentVersion) {
816
+ throw new AppError({
817
+ statusCode: 409,
818
+ errorCode: "FILE_VERSION_CONFLICT",
819
+ detail: "文件已被其他修改覆盖,请先刷新再保存",
820
+ field: "expectedVersion"
821
+ });
822
+ }
823
+ const nextBuffer = Buffer.from(input.content ?? "", "utf8");
824
+ ensureWritableAffairsLibraryTextBuffer(nextBuffer);
825
+ fs.writeFileSync(source.absolutePath, nextBuffer);
826
+ this.afterFileMutation(workspaceId, source.rootDir, `library_write:${source.relativePath}`, source.relativePath);
827
+ return {
828
+ success: true,
829
+ opType,
830
+ sourcePath: source.relativePath,
831
+ targetPath: source.relativePath
832
+ };
833
+ }
834
+ if (opType === "delete") {
835
+ if (source.stats?.isDirectory()) {
836
+ fs.rmSync(source.absolutePath, { recursive: true, force: false });
837
+ }
838
+ else {
839
+ fs.rmSync(source.absolutePath, { force: false });
840
+ }
841
+ this.afterFileMutation(workspaceId, source.rootDir, `library_delete:${source.relativePath}`, source.relativePath);
842
+ return {
843
+ success: true,
844
+ opType,
845
+ sourcePath: source.relativePath,
846
+ targetPath: null
847
+ };
848
+ }
849
+ const target = this.resolvePreviewFile(workspaceId, userId, input.dstPath ?? "", {
850
+ mustExist: false,
851
+ kind: "any"
852
+ });
853
+ this.ensureUserContentPath(target.relativePath);
854
+ if (target.exists) {
855
+ throw new AppError({
856
+ statusCode: 409,
857
+ errorCode: "FILE_ALREADY_EXISTS",
858
+ detail: "目标路径已存在",
859
+ field: "dstPath"
860
+ });
861
+ }
862
+ if (source.relativePath === target.relativePath) {
863
+ throw new AppError({
864
+ statusCode: 400,
865
+ errorCode: "INVALID_FILE_OPERATION",
866
+ detail: "源路径和目标路径不能相同",
867
+ field: "dstPath"
868
+ });
869
+ }
870
+ if (source.stats?.isDirectory() && isSameOrDescendantRelativePath(source.relativePath, target.relativePath)) {
871
+ throw new AppError({
872
+ statusCode: 400,
873
+ errorCode: "INVALID_FILE_OPERATION",
874
+ detail: opType === "move" ? "目录不能移动到自己内部" : "目录不能复制到自己内部",
875
+ field: "dstPath"
876
+ });
877
+ }
878
+ if (opType === "move") {
879
+ fs.renameSync(source.absolutePath, target.absolutePath);
880
+ }
881
+ else {
882
+ fs.cpSync(source.absolutePath, target.absolutePath, {
883
+ recursive: source.stats?.isDirectory() ?? false,
884
+ errorOnExist: true,
885
+ force: false
886
+ });
887
+ }
888
+ this.afterFileMutation(workspaceId, source.rootDir, `library_${opType}:${source.relativePath}->${target.relativePath}`, target.relativePath);
889
+ return {
890
+ success: true,
891
+ opType,
892
+ sourcePath: source.relativePath,
893
+ targetPath: target.relativePath
894
+ };
895
+ }
896
+ resolvePreviewFile(workspaceId, userId, requestedPath, options = {}) {
897
+ const binding = this.requireBinding(workspaceId, userId);
898
+ this.ensureLibraryEnabled(binding);
899
+ this.assertLibraryRootDir(binding.rootDir);
900
+ const rootRealPath = fs.realpathSync.native(binding.rootDir);
901
+ const relativePath = normalizeRelativePath(requestedPath, options.allowRoot ?? false);
902
+ const absolutePath = path.resolve(binding.rootDir, relativePath);
903
+ const relativeToRoot = path.relative(binding.rootDir, absolutePath);
904
+ if (relativeToRoot === ".."
905
+ || relativeToRoot.startsWith(`..${path.sep}`)
906
+ || path.isAbsolute(relativeToRoot)) {
907
+ throw new AppError({
908
+ statusCode: 400,
909
+ errorCode: "PATH_OUT_OF_WORKSPACE",
910
+ detail: "文件路径超出事务资料库边界",
911
+ field: "path"
912
+ });
913
+ }
914
+ const exists = fs.existsSync(absolutePath);
915
+ let stats = null;
916
+ if (exists) {
917
+ const targetRealPath = fs.realpathSync.native(absolutePath);
918
+ const relativeToRealRoot = path.relative(rootRealPath, targetRealPath);
919
+ if (relativeToRealRoot === ".."
920
+ || relativeToRealRoot.startsWith(`..${path.sep}`)
921
+ || path.isAbsolute(relativeToRealRoot)) {
922
+ throw new AppError({
923
+ statusCode: 400,
924
+ errorCode: "PATH_OUT_OF_WORKSPACE",
925
+ detail: "文件路径超出事务资料库边界",
926
+ field: "path"
927
+ });
928
+ }
929
+ stats = fs.statSync(absolutePath);
930
+ }
931
+ else if (options.mustExist ?? true) {
932
+ throw new AppError({
933
+ statusCode: 404,
934
+ errorCode: "FILE_NOT_FOUND",
935
+ detail: "指定文件不存在",
936
+ field: "path"
937
+ });
938
+ }
939
+ if (stats && options.kind === "file" && !stats.isFile()) {
940
+ throw new AppError({
941
+ statusCode: 400,
942
+ errorCode: "NOT_A_FILE",
943
+ detail: "指定路径不是文件",
944
+ field: "path"
945
+ });
946
+ }
947
+ if (stats && options.kind === "directory" && !stats.isDirectory()) {
948
+ throw new AppError({
949
+ statusCode: 400,
950
+ errorCode: "NOT_A_DIRECTORY",
951
+ detail: "指定路径不是目录",
952
+ field: "path"
953
+ });
954
+ }
955
+ return {
956
+ workspaceId,
957
+ userId,
958
+ rootDir: binding.rootDir,
959
+ rootRealPath,
960
+ relativePath,
961
+ absolutePath,
962
+ exists,
963
+ stats
964
+ };
965
+ }
966
+ ensureUserContentPath(relativePath) {
967
+ const normalized = relativePath.trim().replace(/^\.\/+/, "");
968
+ if (!normalized || normalized === ".ai-index" || normalized.startsWith(".ai-index/")) {
969
+ throw new AppError({
970
+ statusCode: 400,
971
+ errorCode: "INVALID_FILE_OPERATION",
972
+ detail: "文档库内部索引文件不能在这里操作",
973
+ field: "path"
974
+ });
975
+ }
976
+ }
977
+ afterFileMutation(workspaceId, rootDir, reason, targetPath) {
978
+ this.invalidateExportCache(rootDir);
979
+ this.scheduleAutoRefresh(workspaceId, reason, normalizeMutationRefreshTarget(targetPath) ?? undefined);
980
+ }
981
+ requestRefresh(workspaceId, userId, reason) {
982
+ const binding = this.requireBinding(workspaceId, userId);
983
+ this.ensureLibraryEnabled(binding);
984
+ const normalizedReason = reason.trim() || "manual_refresh";
985
+ this.reconcileOrphanedRunningTasks(workspaceId, binding.rootDir, {
986
+ source: "affairs_library.refresh",
987
+ triggerReason: normalizedReason
988
+ });
989
+ const handle = this.taskManager.enqueue(HOST_TASK_TYPES.affairsLibraryIndex, {
990
+ key: workspaceId,
991
+ source: "affairs_library.refresh",
992
+ input: {
993
+ workspaceId,
994
+ rootDir: binding.rootDir,
995
+ reason: normalizedReason
996
+ }
997
+ });
998
+ writeAffairsLibraryDebugLog({
999
+ event: "manual_refresh_enqueued",
1000
+ processRole: "host",
1001
+ workspaceId,
1002
+ rootDir: binding.rootDir,
1003
+ taskType: handle.taskType,
1004
+ taskId: handle.taskId,
1005
+ source: "affairs_library.refresh",
1006
+ reason: normalizedReason,
1007
+ deduped: handle.deduped,
1008
+ status: "queued"
1009
+ });
1010
+ void handle.promise.then((result) => {
1011
+ this.invalidateExportCache(binding.rootDir);
1012
+ writeAffairsLibraryDebugLog({
1013
+ event: "manual_refresh_finished",
1014
+ processRole: "host",
1015
+ workspaceId,
1016
+ rootDir: binding.rootDir,
1017
+ taskType: handle.taskType,
1018
+ taskId: handle.taskId,
1019
+ source: "affairs_library.refresh",
1020
+ reason: normalizedReason,
1021
+ status: "finished",
1022
+ durationMs: result.durationMs,
1023
+ resultSummary: summarizeIndexerCommandResult(result.result)
1024
+ });
1025
+ }).catch((error) => {
1026
+ writeAffairsLibraryDebugLog({
1027
+ event: "manual_refresh_failed",
1028
+ processRole: "host",
1029
+ workspaceId,
1030
+ rootDir: binding.rootDir,
1031
+ taskType: handle.taskType,
1032
+ taskId: handle.taskId,
1033
+ source: "affairs_library.refresh",
1034
+ reason: normalizedReason,
1035
+ status: "failed",
1036
+ message: error instanceof Error ? error.message : String(error)
1037
+ });
1038
+ });
1039
+ return {
1040
+ taskId: handle.taskId,
1041
+ deduped: handle.deduped,
1042
+ status: this.readIndexStatus(workspaceId, binding)
1043
+ };
1044
+ }
1045
+ requestRefreshHint(workspaceId, userId, reason, targetPath) {
1046
+ const binding = this.requireBinding(workspaceId, userId);
1047
+ this.ensureLibraryEnabled(binding);
1048
+ const normalizedTargetPath = normalizeHintTargetPath(targetPath);
1049
+ const directoryPath = normalizeFolderPath(normalizedTargetPath) || ".";
1050
+ this.scheduleDirectoryHintRefresh(workspaceId, directoryPath, reason.trim() || "directory_hint");
1051
+ writeAffairsLibraryDebugLog({
1052
+ event: "directory_hint_received",
1053
+ processRole: "host",
1054
+ workspaceId,
1055
+ rootDir: binding.rootDir,
1056
+ source: "affairs_library.directory_hint",
1057
+ reason: reason.trim() || "directory_hint",
1058
+ targetPath: directoryPath,
1059
+ status: "scheduled"
1060
+ });
1061
+ return {
1062
+ scheduled: true,
1063
+ status: this.readIndexStatus(workspaceId, binding),
1064
+ directoryStatus: this.readDirectoryStatus(workspaceId, binding.rootDir, directoryPath, "mixed")
1065
+ };
1066
+ }
1067
+ notifyWorkspaceFileMutation(workspaceId, input) {
1068
+ const normalizedWorkspaceId = workspaceId.trim();
1069
+ const absolutePath = input.absolutePath.trim();
1070
+ if (!normalizedWorkspaceId || !absolutePath) {
1071
+ return;
1072
+ }
1073
+ const binding = this.findEnabledBindingByWorkspaceId(normalizedWorkspaceId);
1074
+ const rootDir = binding?.rootDir?.trim() ?? "";
1075
+ if (!rootDir || binding?.enabled !== true) {
1076
+ return;
1077
+ }
1078
+ const relativePath = resolveAffairsLibraryRelativePath(rootDir, absolutePath);
1079
+ if (!relativePath) {
1080
+ return;
1081
+ }
1082
+ if (relativePath === DEFAULT_CONFIG_RELATIVE_PATH) {
1083
+ this.scheduleAutoApplyConfig(normalizedWorkspaceId, `app_write:${relativePath}`);
1084
+ return;
1085
+ }
1086
+ if (relativePath === ".ai-index" || relativePath.startsWith(".ai-index/")) {
1087
+ return;
1088
+ }
1089
+ const targetPath = normalizeMutationRefreshTarget(relativePath);
1090
+ if (!targetPath) {
1091
+ return;
1092
+ }
1093
+ this.scheduleAutoRefresh(normalizedWorkspaceId, `app_${input.kind}:${targetPath}`, targetPath);
1094
+ }
1095
+ scheduleAutoRefresh(workspaceId, reason, targetPath) {
1096
+ const normalizedWorkspaceId = workspaceId.trim();
1097
+ if (!normalizedWorkspaceId) {
1098
+ return;
1099
+ }
1100
+ const normalizedTargetPath = targetPath?.trim().replace(/^\.\//, "") ?? undefined;
1101
+ const state = this.getOrCreateAutoTaskState(normalizedWorkspaceId);
1102
+ state.indexReasons.add(reason.trim() || "auto_refresh");
1103
+ if (normalizedTargetPath) {
1104
+ state.indexTargets.add(normalizedTargetPath);
1105
+ this.markHotDirectoryCacheDirty(normalizedWorkspaceId, deriveDirectoryPathFromDocumentTarget(normalizedTargetPath), reason.trim() || "auto_refresh");
1106
+ }
1107
+ writeAffairsLibraryDebugLog({
1108
+ event: "auto_refresh_marked_dirty",
1109
+ processRole: "host",
1110
+ workspaceId: normalizedWorkspaceId,
1111
+ source: "affairs_library.auto_refresh",
1112
+ reason: reason.trim() || "auto_refresh",
1113
+ targetPath: normalizedTargetPath ?? null,
1114
+ details: {
1115
+ pendingReasonCount: state.indexReasons.size,
1116
+ pendingTargetCount: state.indexTargets.size
1117
+ }
1118
+ });
1119
+ if (normalizedTargetPath) {
1120
+ this.scheduleDirectoryHintRefresh(normalizedWorkspaceId, deriveDirectoryPathFromDocumentTarget(normalizedTargetPath), `watch_hint:${normalizedTargetPath}`);
1121
+ }
1122
+ this.armAutoTaskTimer(normalizedWorkspaceId, AUTO_TASK_QUIET_WINDOW_MS);
1123
+ }
1124
+ scheduleAutoApplyConfig(workspaceId, reason) {
1125
+ const normalizedWorkspaceId = workspaceId.trim();
1126
+ if (!normalizedWorkspaceId) {
1127
+ return;
1128
+ }
1129
+ const state = this.getOrCreateAutoTaskState(normalizedWorkspaceId);
1130
+ state.applyConfigReasons.add(reason.trim() || `watch:${DEFAULT_CONFIG_RELATIVE_PATH}`);
1131
+ writeAffairsLibraryDebugLog({
1132
+ event: "auto_apply_config_marked_dirty",
1133
+ processRole: "host",
1134
+ workspaceId: normalizedWorkspaceId,
1135
+ source: "affairs_library.auto_apply_config",
1136
+ reason: reason.trim() || `watch:${DEFAULT_CONFIG_RELATIVE_PATH}`,
1137
+ details: {
1138
+ pendingReasonCount: state.applyConfigReasons.size
1139
+ }
1140
+ });
1141
+ this.armAutoTaskTimer(normalizedWorkspaceId, AUTO_TASK_QUIET_WINDOW_MS);
1142
+ }
1143
+ syncLightweightReconcileTimers() {
1144
+ const enabledSettings = this.listEnabledSettingsWithWorkspace();
1145
+ const activeWorkspaceIds = new Set();
1146
+ for (const setting of enabledSettings) {
1147
+ const rootDir = setting.rootDir?.trim() ?? "";
1148
+ const workspaceId = setting.lastWorkspaceId?.trim() ?? "";
1149
+ if (!workspaceId || !rootDir || setting.enabled !== true) {
1150
+ continue;
1151
+ }
1152
+ activeWorkspaceIds.add(workspaceId);
1153
+ this.ensureLightweightReconcileTimer(workspaceId);
1154
+ }
1155
+ for (const workspaceId of [...this.lightweightReconcileTimers.keys()]) {
1156
+ if (!activeWorkspaceIds.has(workspaceId)) {
1157
+ this.clearLightweightReconcileTimer(workspaceId);
1158
+ }
1159
+ }
1160
+ }
1161
+ ensureLightweightReconcileTimer(workspaceId) {
1162
+ if (this.lightweightReconcileTimers.has(workspaceId)) {
1163
+ return;
1164
+ }
1165
+ const timer = setInterval(() => {
1166
+ this.scheduleLightweightReconcile(workspaceId, AFFAIRS_LIBRARY_RECONCILE_REASONS.timer);
1167
+ }, LIGHTWEIGHT_RECONCILE_INTERVAL_MS);
1168
+ this.lightweightReconcileTimers.set(workspaceId, timer);
1169
+ }
1170
+ clearLightweightReconcileTimer(workspaceId) {
1171
+ const timer = this.lightweightReconcileTimers.get(workspaceId);
1172
+ if (!timer) {
1173
+ return;
1174
+ }
1175
+ clearInterval(timer);
1176
+ this.lightweightReconcileTimers.delete(workspaceId);
1177
+ }
1178
+ scheduleLightweightReconcile(workspaceId, triggerReason) {
1179
+ const binding = this.findEnabledBindingByWorkspaceId(workspaceId);
1180
+ const rootDir = binding?.rootDir?.trim() ?? "";
1181
+ if (!rootDir || binding?.enabled !== true) {
1182
+ this.clearLightweightReconcileTimer(workspaceId);
1183
+ return;
1184
+ }
1185
+ const activeTask = this.findBlockingAutoTask(workspaceId);
1186
+ if (activeTask) {
1187
+ writeAffairsLibraryDebugLog({
1188
+ event: AFFAIRS_LIBRARY_DEBUG_EVENTS.lightweightReconcileSkipped,
1189
+ processRole: "host",
1190
+ workspaceId,
1191
+ rootDir,
1192
+ taskType: activeTask.taskType,
1193
+ taskId: activeTask.taskId,
1194
+ source: "affairs_library.lightweight_reconcile",
1195
+ reason: triggerReason,
1196
+ status: activeTask.status,
1197
+ details: {
1198
+ skipReason: "blocking_task_active",
1199
+ triggerReason
1200
+ }
1201
+ });
1202
+ return;
1203
+ }
1204
+ const result = this.evaluateLightweightReconcile(workspaceId, rootDir);
1205
+ this.recordLightweightReconcileObservation(workspaceId, result);
1206
+ writeAffairsLibraryDebugLog({
1207
+ event: AFFAIRS_LIBRARY_DEBUG_EVENTS.lightweightReconcileTick,
1208
+ processRole: "host",
1209
+ workspaceId,
1210
+ rootDir,
1211
+ source: "affairs_library.lightweight_reconcile",
1212
+ reason: result.reason,
1213
+ status: result.status,
1214
+ details: {
1215
+ triggerReason,
1216
+ scope: result.scope,
1217
+ targetPaths: result.targetPaths
1218
+ }
1219
+ });
1220
+ if (result.status === AFFAIRS_LIBRARY_RECONCILE_STATUSES.healthy) {
1221
+ return;
1222
+ }
1223
+ if (result.recentDirectoryPath) {
1224
+ this.scheduleDirectoryHintRefresh(workspaceId, result.recentDirectoryPath, `${AFFAIRS_LIBRARY_RECONCILE_REASONS.recentDirectoryMtime}:${result.recentDirectoryPath}`);
1225
+ }
1226
+ writeAffairsLibraryDebugLog({
1227
+ event: AFFAIRS_LIBRARY_DEBUG_EVENTS.lightweightReconcileDriftDetected,
1228
+ processRole: "host",
1229
+ workspaceId,
1230
+ rootDir,
1231
+ source: "affairs_library.lightweight_reconcile",
1232
+ reason: result.reason,
1233
+ status: result.status,
1234
+ details: {
1235
+ scope: result.scope,
1236
+ targetPaths: result.targetPaths,
1237
+ observedAt: result.observedAt
1238
+ }
1239
+ });
1240
+ this.scheduleAutoRefresh(workspaceId, result.reason);
1241
+ writeAffairsLibraryDebugLog({
1242
+ event: AFFAIRS_LIBRARY_DEBUG_EVENTS.lightweightReconcileScheduledRefresh,
1243
+ processRole: "host",
1244
+ workspaceId,
1245
+ rootDir,
1246
+ source: "affairs_library.lightweight_reconcile",
1247
+ reason: result.reason,
1248
+ status: "queued",
1249
+ details: {
1250
+ scope: result.scope,
1251
+ targetPaths: result.targetPaths,
1252
+ observedAt: result.observedAt
1253
+ }
1254
+ });
1255
+ }
1256
+ schedulePeriodicAudit(workspaceId, triggerReason) {
1257
+ const binding = this.findEnabledBindingByWorkspaceId(workspaceId);
1258
+ const rootDir = binding?.rootDir?.trim() ?? "";
1259
+ if (!rootDir || binding?.enabled !== true) {
1260
+ return;
1261
+ }
1262
+ const activeTask = this.findBlockingAutoTask(workspaceId);
1263
+ if (activeTask) {
1264
+ writeAffairsLibraryDebugLog({
1265
+ event: AFFAIRS_LIBRARY_DEBUG_EVENTS.periodicAuditSkipped,
1266
+ processRole: "host",
1267
+ workspaceId,
1268
+ rootDir,
1269
+ taskType: activeTask.taskType,
1270
+ taskId: activeTask.taskId,
1271
+ source: "affairs_library.periodic_audit",
1272
+ reason: triggerReason,
1273
+ status: activeTask.status,
1274
+ details: {
1275
+ skipReason: "blocking_task_active",
1276
+ triggerReason
1277
+ }
1278
+ });
1279
+ return;
1280
+ }
1281
+ const result = this.evaluatePeriodicAudit(workspaceId, rootDir);
1282
+ writeAffairsLibraryDebugLog({
1283
+ event: AFFAIRS_LIBRARY_DEBUG_EVENTS.periodicAuditTick,
1284
+ processRole: "host",
1285
+ workspaceId,
1286
+ rootDir,
1287
+ source: "affairs_library.periodic_audit",
1288
+ reason: result.reason,
1289
+ status: result.status,
1290
+ details: {
1291
+ triggerReason,
1292
+ scope: result.scope,
1293
+ targetPaths: result.targetPaths
1294
+ }
1295
+ });
1296
+ if (result.status === AFFAIRS_LIBRARY_RECONCILE_STATUSES.healthy) {
1297
+ return;
1298
+ }
1299
+ writeAffairsLibraryDebugLog({
1300
+ event: AFFAIRS_LIBRARY_DEBUG_EVENTS.periodicAuditDriftDetected,
1301
+ processRole: "host",
1302
+ workspaceId,
1303
+ rootDir,
1304
+ source: "affairs_library.periodic_audit",
1305
+ reason: result.reason,
1306
+ status: result.status,
1307
+ details: {
1308
+ scope: result.scope,
1309
+ targetPaths: result.targetPaths,
1310
+ observedAt: result.observedAt
1311
+ }
1312
+ });
1313
+ this.scheduleAutoRefresh(workspaceId, result.reason);
1314
+ writeAffairsLibraryDebugLog({
1315
+ event: AFFAIRS_LIBRARY_DEBUG_EVENTS.periodicAuditScheduledRefresh,
1316
+ processRole: "host",
1317
+ workspaceId,
1318
+ rootDir,
1319
+ source: "affairs_library.periodic_audit",
1320
+ reason: result.reason,
1321
+ status: "queued",
1322
+ details: {
1323
+ scope: result.scope,
1324
+ targetPaths: result.targetPaths,
1325
+ observedAt: result.observedAt
1326
+ }
1327
+ });
1328
+ }
1329
+ evaluateLightweightReconcile(workspaceId, rootDir) {
1330
+ const observedAt = nowIso();
1331
+ const missingArtifact = detectMissingIndexArtifact(rootDir);
1332
+ if (missingArtifact) {
1333
+ return {
1334
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.lightweight,
1335
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.rebuildRequired,
1336
+ reason: `lightweight_reconcile:${missingArtifact.reason}`,
1337
+ targetPaths: [],
1338
+ observedAt,
1339
+ recentDirectoryPath: null
1340
+ };
1341
+ }
1342
+ const pendingAutoTaskState = this.autoTaskStateByWorkspace.get(workspaceId);
1343
+ if (pendingAutoTaskState && hasPendingAutoTasks(pendingAutoTaskState) && !pendingAutoTaskState.timer) {
1344
+ return {
1345
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.lightweight,
1346
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.driftDetected,
1347
+ reason: AFFAIRS_LIBRARY_RECONCILE_REASONS.pendingDirtySignal,
1348
+ targetPaths: [],
1349
+ observedAt,
1350
+ recentDirectoryPath: null
1351
+ };
1352
+ }
1353
+ const exportStatus = readIndexStatusFileSafe(rootDir);
1354
+ const runtimeStatus = readRuntimeStatusFileSafe(rootDir);
1355
+ if (runtimeStatus?.updatedAtMs
1356
+ && Number.isFinite(runtimeStatus.updatedAtMs)
1357
+ && runtimeStatus.updatedAtMs > (exportStatus?.exportedAtMs ?? 0) + LIGHTWEIGHT_RECONCILE_DRIFT_TOLERANCE_MS) {
1358
+ return {
1359
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.lightweight,
1360
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.driftDetected,
1361
+ reason: AFFAIRS_LIBRARY_RECONCILE_REASONS.runtimeStatusAhead,
1362
+ targetPaths: [],
1363
+ observedAt,
1364
+ recentDirectoryPath: null
1365
+ };
1366
+ }
1367
+ const recentDirectoryDrift = this.findRecentDirectoryDrift(workspaceId, rootDir, exportStatus?.exportedAtMs ?? 0);
1368
+ if (recentDirectoryDrift) {
1369
+ return {
1370
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.lightweight,
1371
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.driftDetected,
1372
+ reason: `${AFFAIRS_LIBRARY_RECONCILE_REASONS.recentDirectoryMtime}:${recentDirectoryDrift.directoryPath}`,
1373
+ targetPaths: [recentDirectoryDrift.directoryPath],
1374
+ observedAt,
1375
+ recentDirectoryPath: recentDirectoryDrift.directoryPath
1376
+ };
1377
+ }
1378
+ return {
1379
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.lightweight,
1380
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.healthy,
1381
+ reason: AFFAIRS_LIBRARY_RECONCILE_REASONS.timer,
1382
+ targetPaths: [],
1383
+ observedAt,
1384
+ recentDirectoryPath: null
1385
+ };
1386
+ }
1387
+ evaluatePeriodicAudit(workspaceId, rootDir) {
1388
+ const observedAt = nowIso();
1389
+ const missingArtifact = detectMissingIndexArtifact(rootDir);
1390
+ if (missingArtifact) {
1391
+ return {
1392
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.periodicAudit,
1393
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.rebuildRequired,
1394
+ reason: `periodic_audit:${missingArtifact.reason}`,
1395
+ targetPaths: [],
1396
+ observedAt
1397
+ };
1398
+ }
1399
+ const pendingAutoTaskState = this.autoTaskStateByWorkspace.get(workspaceId);
1400
+ if (pendingAutoTaskState && hasPendingAutoTasks(pendingAutoTaskState) && !pendingAutoTaskState.timer) {
1401
+ return {
1402
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.periodicAudit,
1403
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.driftDetected,
1404
+ reason: AFFAIRS_LIBRARY_RECONCILE_REASONS.periodicAuditPendingDirtySignal,
1405
+ targetPaths: [],
1406
+ observedAt
1407
+ };
1408
+ }
1409
+ const exportStatus = readIndexStatusFileSafe(rootDir);
1410
+ const runtimeStatus = readRuntimeStatusFileSafe(rootDir);
1411
+ if (runtimeStatus?.updatedAtMs
1412
+ && Number.isFinite(runtimeStatus.updatedAtMs)
1413
+ && runtimeStatus.updatedAtMs > (exportStatus?.exportedAtMs ?? 0) + LIGHTWEIGHT_RECONCILE_DRIFT_TOLERANCE_MS) {
1414
+ return {
1415
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.periodicAudit,
1416
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.driftDetected,
1417
+ reason: AFFAIRS_LIBRARY_RECONCILE_REASONS.periodicAuditRuntimeStatusAhead,
1418
+ targetPaths: [],
1419
+ observedAt
1420
+ };
1421
+ }
1422
+ const observation = this.reconcileObservationStateByWorkspace.get(workspaceId);
1423
+ if ((observation?.consecutiveLightweightDrifts ?? 0) >= 2) {
1424
+ return {
1425
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.periodicAudit,
1426
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.driftDetected,
1427
+ reason: `${AFFAIRS_LIBRARY_RECONCILE_REASONS.periodicAuditLightweightDriftStreak}:${observation?.lastLightweightReason ?? "unknown"}`,
1428
+ targetPaths: [],
1429
+ observedAt
1430
+ };
1431
+ }
1432
+ const rootStats = readAffairsLibraryStatsSafe(rootDir, ".");
1433
+ if (rootStats
1434
+ && Number.isFinite(rootStats.mtimeMs)
1435
+ && rootStats.mtimeMs > (exportStatus?.exportedAtMs ?? 0) + LIGHTWEIGHT_RECONCILE_DRIFT_TOLERANCE_MS) {
1436
+ return {
1437
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.periodicAudit,
1438
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.driftDetected,
1439
+ reason: AFFAIRS_LIBRARY_RECONCILE_REASONS.periodicAuditRootDirMtime,
1440
+ targetPaths: ["."],
1441
+ observedAt
1442
+ };
1443
+ }
1444
+ return {
1445
+ scope: AFFAIRS_LIBRARY_RECONCILE_SCOPES.periodicAudit,
1446
+ status: AFFAIRS_LIBRARY_RECONCILE_STATUSES.healthy,
1447
+ reason: AFFAIRS_LIBRARY_RECONCILE_REASONS.periodicAuditTimer,
1448
+ targetPaths: [],
1449
+ observedAt
1450
+ };
1451
+ }
1452
+ recordLightweightReconcileObservation(workspaceId, result) {
1453
+ const current = this.reconcileObservationStateByWorkspace.get(workspaceId) ?? {
1454
+ consecutiveLightweightDrifts: 0,
1455
+ lastLightweightObservedAt: null,
1456
+ lastLightweightReason: null
1457
+ };
1458
+ if (result.status === AFFAIRS_LIBRARY_RECONCILE_STATUSES.healthy) {
1459
+ this.reconcileObservationStateByWorkspace.set(workspaceId, {
1460
+ consecutiveLightweightDrifts: 0,
1461
+ lastLightweightObservedAt: result.observedAt,
1462
+ lastLightweightReason: null
1463
+ });
1464
+ return;
1465
+ }
1466
+ this.reconcileObservationStateByWorkspace.set(workspaceId, {
1467
+ consecutiveLightweightDrifts: current.consecutiveLightweightDrifts + 1,
1468
+ lastLightweightObservedAt: result.observedAt,
1469
+ lastLightweightReason: result.reason
1470
+ });
1471
+ }
1472
+ findRecentDirectoryDrift(workspaceId, rootDir, referenceTimestampMs) {
1473
+ const candidateDirectoryPaths = new Set(["."]);
1474
+ const recentDirectories = [...this.hotDirectoryCache.values()]
1475
+ .filter((entry) => entry.workspaceId === workspaceId)
1476
+ .sort((left, right) => right.updatedAtMs - left.updatedAtMs)
1477
+ .slice(0, HOT_DIRECTORY_MAX_PER_WORKSPACE);
1478
+ for (const entry of recentDirectories) {
1479
+ candidateDirectoryPaths.add(entry.directoryPath);
1480
+ }
1481
+ let selected = null;
1482
+ for (const directoryPath of candidateDirectoryPaths) {
1483
+ const stats = readAffairsLibraryStatsSafe(rootDir, directoryPath);
1484
+ if (!stats) {
1485
+ continue;
1486
+ }
1487
+ const observedAtMs = stats.mtimeMs;
1488
+ if (!Number.isFinite(observedAtMs)) {
1489
+ continue;
1490
+ }
1491
+ if (observedAtMs <= referenceTimestampMs + LIGHTWEIGHT_RECONCILE_DRIFT_TOLERANCE_MS) {
1492
+ continue;
1493
+ }
1494
+ const candidate = {
1495
+ directoryPath,
1496
+ observedAt: new Date(observedAtMs).toISOString(),
1497
+ observedAtMs
1498
+ };
1499
+ if (!selected || candidate.observedAtMs > selected.observedAtMs) {
1500
+ selected = candidate;
1501
+ }
1502
+ }
1503
+ return selected
1504
+ ? {
1505
+ directoryPath: selected.directoryPath,
1506
+ observedAt: selected.observedAt
1507
+ }
1508
+ : null;
1509
+ }
1510
+ dispose() {
1511
+ for (const state of this.autoTaskStateByWorkspace.values()) {
1512
+ if (state.timer) {
1513
+ clearTimeout(state.timer);
1514
+ }
1515
+ }
1516
+ this.autoTaskStateByWorkspace.clear();
1517
+ for (const timer of this.lightweightReconcileTimers.values()) {
1518
+ clearInterval(timer);
1519
+ }
1520
+ this.lightweightReconcileTimers.clear();
1521
+ this.reconcileObservationStateByWorkspace.clear();
1522
+ this.hotDirectoryCache.clear();
1523
+ }
1524
+ getRefreshTaskSnapshot(workspaceId) {
1525
+ return this.taskManager.peek(HOST_TASK_TYPES.affairsLibraryIndex, workspaceId);
1526
+ }
1527
+ readDirectoryStatus(workspaceId, _rootDir, directoryPath, fallbackSource) {
1528
+ const normalizedPath = normalizeFolderPath(directoryPath) || ".";
1529
+ const cacheKey = buildHotDirectoryCacheKey(workspaceId, normalizedPath);
1530
+ const entry = this.hotDirectoryCache.get(cacheKey);
1531
+ const snapshot = this.taskManager.peek(HOST_TASK_TYPES.affairsLibraryDirectoryHint, cacheKey);
1532
+ if (snapshot && (snapshot.status === "queued" || snapshot.status === "running" || snapshot.status === "queue_timeout")) {
1533
+ return {
1534
+ path: normalizedPath,
1535
+ state: snapshot.status === "running"
1536
+ ? "running"
1537
+ : snapshot.status === "queue_timeout"
1538
+ ? "queue_timeout"
1539
+ : "queued",
1540
+ source: entry?.source ?? fallbackSource,
1541
+ lastRequestedAt: toIso(snapshot.enqueuedAt),
1542
+ lastCompletedAt: entry?.lastRefreshCompletedAt ?? null,
1543
+ lastFailedAt: snapshot.status === "queue_timeout"
1544
+ ? toIso(snapshot.finishedAt)
1545
+ : entry?.lastRefreshFailedAt ?? null,
1546
+ runningTaskId: snapshot.status === "running" ? snapshot.taskId : null,
1547
+ errorSummary: snapshot.status === "queue_timeout"
1548
+ ? snapshot.errorMessage ?? entry?.lastError ?? null
1549
+ : entry?.lastError ?? null,
1550
+ generatedAt: entry?.generatedAt ?? null,
1551
+ filesystemObservedAt: entry?.filesystemObservedAt ?? null,
1552
+ staleReason: entry?.staleReason ?? null
1553
+ };
1554
+ }
1555
+ return {
1556
+ path: normalizedPath,
1557
+ state: entry?.status ?? "idle",
1558
+ source: entry?.source ?? fallbackSource,
1559
+ lastRequestedAt: entry?.lastRefreshRequestedAt ?? null,
1560
+ lastCompletedAt: entry?.lastRefreshCompletedAt ?? null,
1561
+ lastFailedAt: entry?.lastRefreshFailedAt ?? null,
1562
+ runningTaskId: null,
1563
+ errorSummary: entry?.lastError ?? null,
1564
+ generatedAt: entry?.generatedAt ?? null,
1565
+ filesystemObservedAt: entry?.filesystemObservedAt ?? null,
1566
+ staleReason: entry?.staleReason ?? null
1567
+ };
1568
+ }
1569
+ getOrCreateHotDirectoryEntry(workspaceId, rootDir, directoryPath) {
1570
+ const normalizedDirectoryPath = normalizeFolderPath(directoryPath) || ".";
1571
+ const cacheKey = buildHotDirectoryCacheKey(workspaceId, normalizedDirectoryPath);
1572
+ const existing = this.hotDirectoryCache.get(cacheKey);
1573
+ if (existing) {
1574
+ if (existing.rootDir !== rootDir) {
1575
+ existing.rootDir = rootDir;
1576
+ }
1577
+ return existing;
1578
+ }
1579
+ const entry = {
1580
+ workspaceId,
1581
+ rootDir,
1582
+ directoryPath: normalizedDirectoryPath,
1583
+ items: [],
1584
+ updatedAtMs: 0,
1585
+ source: "snapshot",
1586
+ dirty: true,
1587
+ pendingHintReasons: new Set(),
1588
+ lastHintAt: null,
1589
+ lastRefreshRequestedAt: null,
1590
+ lastRefreshCompletedAt: null,
1591
+ lastRefreshFailedAt: null,
1592
+ lastError: null,
1593
+ status: "idle",
1594
+ generatedAt: null,
1595
+ filesystemObservedAt: null,
1596
+ staleReason: null
1597
+ };
1598
+ this.hotDirectoryCache.set(cacheKey, entry);
1599
+ return entry;
1600
+ }
1601
+ updateHotDirectoryCache(workspaceId, rootDir, directoryPath, items, source, options = {}) {
1602
+ const entry = this.getOrCreateHotDirectoryEntry(workspaceId, rootDir, directoryPath);
1603
+ entry.rootDir = rootDir;
1604
+ entry.items = items;
1605
+ entry.updatedAtMs = Date.now();
1606
+ entry.source = source;
1607
+ entry.lastRefreshRequestedAt = options.requestedAt ?? entry.lastRefreshRequestedAt;
1608
+ entry.lastRefreshCompletedAt = options.completedAt ?? entry.lastRefreshCompletedAt;
1609
+ entry.lastRefreshFailedAt = options.failedAt ?? entry.lastRefreshFailedAt;
1610
+ entry.lastError = options.errorSummary ?? null;
1611
+ entry.generatedAt = options.generatedAt ?? entry.generatedAt;
1612
+ entry.filesystemObservedAt = options.filesystemObservedAt ?? entry.filesystemObservedAt;
1613
+ entry.staleReason = options.staleReason ?? null;
1614
+ entry.dirty = false;
1615
+ if (!options.preserveStatus) {
1616
+ entry.status = options.errorSummary ? "failed" : "fresh";
1617
+ }
1618
+ if (!options.preserveStatus) {
1619
+ entry.pendingHintReasons.clear();
1620
+ }
1621
+ this.ensureDirectoryWindow(workspaceId, rootDir, directoryPath);
1622
+ return entry;
1623
+ }
1624
+ markHotDirectoryCacheDirty(workspaceId, directoryPath, reason) {
1625
+ const binding = this.findEnabledBindingByWorkspaceId(workspaceId);
1626
+ const rootDir = binding?.rootDir?.trim() ?? "";
1627
+ if (!rootDir) {
1628
+ return;
1629
+ }
1630
+ const entry = this.getOrCreateHotDirectoryEntry(workspaceId, rootDir, directoryPath);
1631
+ entry.dirty = true;
1632
+ entry.status = entry.status === "running" ? "running" : "idle";
1633
+ entry.pendingHintReasons.add(reason);
1634
+ entry.lastHintAt = nowIso();
1635
+ entry.staleReason = null;
1636
+ this.ensureDirectoryWindow(workspaceId, rootDir, directoryPath);
1637
+ }
1638
+ ensureDirectoryWindow(workspaceId, rootDir, directoryPath) {
1639
+ const entry = this.getOrCreateHotDirectoryEntry(workspaceId, rootDir, directoryPath);
1640
+ entry.updatedAtMs = Math.max(entry.updatedAtMs, Date.now());
1641
+ const workspaceEntries = [...this.hotDirectoryCache.entries()]
1642
+ .filter(([, candidate]) => candidate.workspaceId === workspaceId)
1643
+ .sort((left, right) => right[1].updatedAtMs - left[1].updatedAtMs);
1644
+ const expireBefore = Date.now() - HOT_DIRECTORY_CACHE_TTL_MS;
1645
+ for (let index = 0; index < workspaceEntries.length; index += 1) {
1646
+ const [cacheKey, candidate] = workspaceEntries[index];
1647
+ const expired = candidate.updatedAtMs > 0 && candidate.updatedAtMs < expireBefore;
1648
+ const overflow = index >= HOT_DIRECTORY_MAX_PER_WORKSPACE;
1649
+ if (expired || overflow) {
1650
+ this.hotDirectoryCache.delete(cacheKey);
1651
+ }
1652
+ }
1653
+ }
1654
+ scheduleDirectoryHintRefresh(workspaceId, directoryPath, reason) {
1655
+ const binding = this.findEnabledBindingByWorkspaceId(workspaceId);
1656
+ const rootDir = binding?.rootDir?.trim() ?? "";
1657
+ if (!rootDir) {
1658
+ return;
1659
+ }
1660
+ const normalizedDirectoryPath = normalizeFolderPath(directoryPath) || ".";
1661
+ const entry = this.getOrCreateHotDirectoryEntry(workspaceId, rootDir, normalizedDirectoryPath);
1662
+ const cacheKey = buildHotDirectoryCacheKey(workspaceId, normalizedDirectoryPath);
1663
+ const now = Date.now();
1664
+ const isFreshEnough = entry.status === "fresh"
1665
+ && !entry.dirty
1666
+ && entry.updatedAtMs > 0
1667
+ && now - entry.updatedAtMs < HOT_DIRECTORY_CACHE_TTL_MS;
1668
+ if (reason === "list_documents" && isFreshEnough) {
1669
+ return;
1670
+ }
1671
+ entry.pendingHintReasons.add(reason);
1672
+ entry.lastHintAt = nowIso();
1673
+ entry.lastRefreshRequestedAt = nowIso();
1674
+ entry.status = "queued";
1675
+ this.ensureDirectoryWindow(workspaceId, rootDir, normalizedDirectoryPath);
1676
+ const current = this.taskManager.peek(HOST_TASK_TYPES.affairsLibraryDirectoryHint, cacheKey);
1677
+ if (current && (current.status === "queued" || current.status === "running")) {
1678
+ writeAffairsLibraryDebugLog({
1679
+ event: "directory_hint_task_deduped",
1680
+ processRole: "host",
1681
+ workspaceId,
1682
+ rootDir,
1683
+ taskType: current.taskType,
1684
+ taskId: current.taskId,
1685
+ source: "affairs_library.directory_hint",
1686
+ reason,
1687
+ targetPath: normalizedDirectoryPath,
1688
+ status: current.status
1689
+ });
1690
+ return;
1691
+ }
1692
+ const handle = this.taskManager.enqueue(HOST_TASK_TYPES.affairsLibraryDirectoryHint, {
1693
+ key: cacheKey,
1694
+ source: "affairs_library.directory_hint",
1695
+ input: {
1696
+ workspaceId,
1697
+ rootDir,
1698
+ directoryPath: normalizedDirectoryPath,
1699
+ reason
1700
+ }
1701
+ });
1702
+ this.attachDirectoryHintTaskFollowUp(workspaceId, cacheKey, handle, {
1703
+ rootDir,
1704
+ directoryPath: normalizedDirectoryPath,
1705
+ reason
1706
+ });
1707
+ }
1708
+ async runDirectoryHintTask(input) {
1709
+ const exportData = this.readAvailableExportData(input.rootDir);
1710
+ const previous = this.getOrCreateHotDirectoryEntry(input.workspaceId, input.rootDir, input.directoryPath).items;
1711
+ const liveResult = this.buildFreshFolderDocuments(input.rootDir, normalizeFolderPath(input.directoryPath), exportData);
1712
+ const completedAt = nowIso();
1713
+ const previousPathSet = new Set(previous.map((item) => item.path));
1714
+ const nextPathSet = new Set(liveResult.items.map((item) => item.path));
1715
+ const changedPaths = [
1716
+ ...liveResult.items.map((item) => item.path).filter((item) => !previousPathSet.has(item)),
1717
+ ...previous.map((item) => item.path).filter((item) => !nextPathSet.has(item))
1718
+ ].sort((left, right) => left.localeCompare(right, "zh-CN"));
1719
+ return {
1720
+ directoryPath: input.directoryPath,
1721
+ refreshedAt: completedAt,
1722
+ source: liveResult.source,
1723
+ itemCount: liveResult.items.length,
1724
+ changedPaths,
1725
+ items: liveResult.items,
1726
+ generatedAt: liveResult.generatedAt,
1727
+ filesystemObservedAt: liveResult.filesystemObservedAt
1728
+ };
1729
+ }
1730
+ attachDirectoryHintTaskFollowUp(workspaceId, cacheKey, handle, meta) {
1731
+ writeAffairsLibraryDebugLog({
1732
+ event: "task_enqueued",
1733
+ processRole: "host",
1734
+ workspaceId,
1735
+ rootDir: meta.rootDir,
1736
+ taskType: handle.taskType,
1737
+ taskId: handle.taskId,
1738
+ source: "affairs_library.directory_hint",
1739
+ reason: meta.reason,
1740
+ targetPath: meta.directoryPath,
1741
+ deduped: handle.deduped,
1742
+ status: "queued"
1743
+ });
1744
+ void handle.promise.then((result) => {
1745
+ this.updateHotDirectoryCache(workspaceId, meta.rootDir, meta.directoryPath, result.items, result.source, {
1746
+ requestedAt: this.getOrCreateHotDirectoryEntry(workspaceId, meta.rootDir, meta.directoryPath).lastRefreshRequestedAt,
1747
+ completedAt: result.refreshedAt,
1748
+ errorSummary: null,
1749
+ generatedAt: result.generatedAt,
1750
+ filesystemObservedAt: result.filesystemObservedAt,
1751
+ staleReason: null
1752
+ });
1753
+ writeAffairsLibraryDebugLog({
1754
+ event: "task_finished",
1755
+ processRole: "host",
1756
+ workspaceId,
1757
+ rootDir: meta.rootDir,
1758
+ taskType: handle.taskType,
1759
+ taskId: handle.taskId,
1760
+ source: "affairs_library.directory_hint",
1761
+ reason: meta.reason,
1762
+ targetPath: meta.directoryPath,
1763
+ status: "finished",
1764
+ resultSummary: {
1765
+ directoryPath: result.directoryPath,
1766
+ source: result.source,
1767
+ itemCount: result.itemCount,
1768
+ changedPaths: result.changedPaths
1769
+ }
1770
+ });
1771
+ }).catch((error) => {
1772
+ const entry = this.hotDirectoryCache.get(cacheKey);
1773
+ if (entry) {
1774
+ entry.status = "failed";
1775
+ entry.lastError = error instanceof Error ? error.message : String(error);
1776
+ entry.lastRefreshFailedAt = nowIso();
1777
+ entry.staleReason = AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.staleFallback;
1778
+ }
1779
+ writeAffairsLibraryDebugLog({
1780
+ event: "task_failed",
1781
+ processRole: "host",
1782
+ workspaceId,
1783
+ rootDir: meta.rootDir,
1784
+ taskType: handle.taskType,
1785
+ taskId: handle.taskId,
1786
+ source: "affairs_library.directory_hint",
1787
+ reason: meta.reason,
1788
+ targetPath: meta.directoryPath,
1789
+ status: "failed",
1790
+ message: error instanceof Error ? error.message : String(error)
1791
+ });
1792
+ });
1793
+ }
1794
+ readFavorites(workspaceId, userId) {
1795
+ const setting = this.resolveLibrarySetting(userId, workspaceId);
1796
+ const raw = setting?.favoritesJson?.trim();
1797
+ if (!raw) {
1798
+ return [];
1799
+ }
1800
+ try {
1801
+ const parsed = JSON.parse(raw);
1802
+ if (!Array.isArray(parsed)) {
1803
+ return [];
1804
+ }
1805
+ return parsed
1806
+ .filter((item) => Boolean(item) && typeof item === "object")
1807
+ .filter((item) => (item.kind === "folder" || item.kind === "tag") && typeof item.path === "string" && item.path.trim())
1808
+ .map((item) => ({
1809
+ kind: item.kind,
1810
+ path: item.path.trim(),
1811
+ label: typeof item.label === "string" && item.label.trim() ? item.label.trim() : item.path.trim()
1812
+ }));
1813
+ }
1814
+ catch {
1815
+ return [];
1816
+ }
1817
+ }
1818
+ readIndexStatus(workspaceId, binding) {
1819
+ const taskSnapshot = this.findRelevantIndexTaskSnapshot(workspaceId);
1820
+ const exportStatus = binding?.enabled ? readIndexStatusFileSafe(binding.rootDir) : null;
1821
+ const runtimeStatus = binding?.enabled ? readRuntimeStatusFileSafe(binding.rootDir) : null;
1822
+ const workerHealth = binding?.enabled
1823
+ ? mapTaskHelperWorkerHealth(getSharedTaskHelperPool().getWorkerHealth(binding.rootDir))
1824
+ : null;
1825
+ if (taskSnapshot?.status === "queue_timeout") {
1826
+ return {
1827
+ state: "queue_timeout",
1828
+ dirtyReasons: [AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.queueTimeout],
1829
+ lastRequestedAt: toIso(taskSnapshot.enqueuedAt),
1830
+ lastStartedAt: null,
1831
+ lastCompletedAt: null,
1832
+ lastFailedAt: toIso(taskSnapshot.finishedAt),
1833
+ nextAllowedAt: null,
1834
+ runningTaskId: null,
1835
+ runningStage: null,
1836
+ errorSummary: taskSnapshot.errorMessage ?? "文档库刷新排队等待超时",
1837
+ workerHealth,
1838
+ progress: runtimeStatus?.progress ?? null
1839
+ };
1840
+ }
1841
+ if (taskSnapshot && (taskSnapshot.status === "queued" || taskSnapshot.status === "running")) {
1842
+ const activeStartedAtMs = taskSnapshot.startedAt ?? taskSnapshot.enqueuedAt ?? null;
1843
+ const reconciledStatus = hasExportCaughtUp(exportStatus, activeStartedAtMs)
1844
+ ? buildCompletedStatusFromExport(exportStatus, taskSnapshot.enqueuedAt, taskSnapshot.startedAt)
1845
+ : null;
1846
+ if (reconciledStatus) {
1847
+ return {
1848
+ ...reconciledStatus,
1849
+ workerHealth,
1850
+ progress: runtimeStatus?.progress ?? null
1851
+ };
1852
+ }
1853
+ const orphanedRunningTask = binding?.enabled
1854
+ ? detectOrphanedRunningTask(binding.rootDir, taskSnapshot, runtimeStatus)
1855
+ : null;
1856
+ if (orphanedRunningTask) {
1857
+ return {
1858
+ state: "failed",
1859
+ dirtyReasons: [AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.refreshFailed, orphanedRunningTask.reason],
1860
+ lastRequestedAt: toIso(taskSnapshot.enqueuedAt),
1861
+ lastStartedAt: toIso(taskSnapshot.startedAt),
1862
+ lastCompletedAt: null,
1863
+ lastFailedAt: nowIso(),
1864
+ nextAllowedAt: null,
1865
+ runningTaskId: null,
1866
+ runningStage: null,
1867
+ errorSummary: orphanedRunningTask.errorSummary,
1868
+ workerHealth,
1869
+ progress: runtimeStatus?.progress ?? null,
1870
+ };
1871
+ }
1872
+ return {
1873
+ state: taskSnapshot.status === "queued" ? "queued" : "running",
1874
+ dirtyReasons: [AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.refreshRequested],
1875
+ lastRequestedAt: toIso(taskSnapshot.enqueuedAt),
1876
+ lastStartedAt: toIso(taskSnapshot.startedAt),
1877
+ lastCompletedAt: null,
1878
+ lastFailedAt: null,
1879
+ nextAllowedAt: null,
1880
+ runningTaskId: taskSnapshot.status === "running" ? taskSnapshot.taskId : null,
1881
+ runningStage: resolveAffairsLibraryRunningStage(workspaceId, taskSnapshot, runtimeStatus),
1882
+ errorSummary: null,
1883
+ workerHealth,
1884
+ progress: runtimeStatus?.progress ?? null,
1885
+ };
1886
+ }
1887
+ if (taskSnapshot?.status === "failed" || taskSnapshot?.status === "timeout" || taskSnapshot?.status === "cancelled") {
1888
+ const failedReferenceMs = taskSnapshot.finishedAt ?? taskSnapshot.startedAt ?? taskSnapshot.enqueuedAt ?? null;
1889
+ if (hasExportCaughtUp(exportStatus, failedReferenceMs)) {
1890
+ const completedStatus = buildCompletedStatusFromExport(exportStatus, taskSnapshot.enqueuedAt, taskSnapshot.startedAt);
1891
+ return completedStatus ? {
1892
+ ...completedStatus,
1893
+ workerHealth,
1894
+ progress: runtimeStatus?.progress ?? null
1895
+ } : {
1896
+ state: "fresh",
1897
+ dirtyReasons: [],
1898
+ lastRequestedAt: toIso(taskSnapshot.enqueuedAt),
1899
+ lastStartedAt: toIso(taskSnapshot.startedAt),
1900
+ lastCompletedAt: exportStatus?.exportedAt ?? null,
1901
+ lastFailedAt: null,
1902
+ nextAllowedAt: null,
1903
+ runningTaskId: null,
1904
+ runningStage: null,
1905
+ errorSummary: null,
1906
+ workerHealth,
1907
+ progress: runtimeStatus?.progress ?? null,
1908
+ };
1909
+ }
1910
+ const failedAt = toIso(taskSnapshot.finishedAt);
1911
+ const failedAtMs = taskSnapshot.finishedAt ?? Date.now();
1912
+ const nextAllowedAtMs = failedAtMs + INDEX_TASK_COOLDOWN_MS;
1913
+ const now = Date.now();
1914
+ return {
1915
+ state: now < nextAllowedAtMs ? "cooldown" : "failed",
1916
+ dirtyReasons: [AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.refreshFailed],
1917
+ lastRequestedAt: toIso(taskSnapshot.enqueuedAt),
1918
+ lastStartedAt: toIso(taskSnapshot.startedAt),
1919
+ lastCompletedAt: null,
1920
+ lastFailedAt: failedAt,
1921
+ nextAllowedAt: toIso(nextAllowedAtMs),
1922
+ runningTaskId: null,
1923
+ runningStage: null,
1924
+ errorSummary: taskSnapshot.errorMessage ?? "最近一次文档库刷新失败",
1925
+ workerHealth,
1926
+ progress: runtimeStatus?.progress ?? null,
1927
+ };
1928
+ }
1929
+ if (!binding) {
1930
+ return {
1931
+ state: "stale",
1932
+ dirtyReasons: [AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.bindingRequired],
1933
+ lastRequestedAt: null,
1934
+ lastStartedAt: null,
1935
+ lastCompletedAt: null,
1936
+ lastFailedAt: null,
1937
+ nextAllowedAt: null,
1938
+ runningTaskId: null,
1939
+ runningStage: null,
1940
+ errorSummary: null,
1941
+ workerHealth
1942
+ };
1943
+ }
1944
+ if (!binding.enabled) {
1945
+ return {
1946
+ state: "stale",
1947
+ dirtyReasons: [AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.libraryDisabled],
1948
+ lastRequestedAt: null,
1949
+ lastStartedAt: null,
1950
+ lastCompletedAt: null,
1951
+ lastFailedAt: null,
1952
+ nextAllowedAt: null,
1953
+ runningTaskId: null,
1954
+ runningStage: null,
1955
+ errorSummary: "文档库功能已关闭,启用后才会启动内置索引服务。",
1956
+ workerHealth
1957
+ };
1958
+ }
1959
+ const cachedExportData = this.readLastUsableExportData(binding.rootDir);
1960
+ const missingArtifact = detectMissingIndexArtifact(binding.rootDir);
1961
+ if (missingArtifact) {
1962
+ return {
1963
+ state: "stale",
1964
+ dirtyReasons: [missingArtifact.reason],
1965
+ lastRequestedAt: null,
1966
+ lastStartedAt: null,
1967
+ lastCompletedAt: cachedExportData?.generatedAt ?? null,
1968
+ lastFailedAt: null,
1969
+ nextAllowedAt: null,
1970
+ runningTaskId: null,
1971
+ runningStage: null,
1972
+ errorSummary: missingArtifact.errorSummary,
1973
+ workerHealth
1974
+ };
1975
+ }
1976
+ const completedStatus = buildCompletedStatusFromExport(exportStatus, null, null);
1977
+ return completedStatus ? {
1978
+ ...completedStatus,
1979
+ workerHealth,
1980
+ progress: runtimeStatus?.progress ?? null
1981
+ } : {
1982
+ state: "stale",
1983
+ dirtyReasons: [AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingExportStatus],
1984
+ lastRequestedAt: null,
1985
+ lastStartedAt: null,
1986
+ lastCompletedAt: null,
1987
+ lastFailedAt: null,
1988
+ nextAllowedAt: null,
1989
+ runningTaskId: null,
1990
+ runningStage: null,
1991
+ errorSummary: "文档库导出状态文件缺失,系统会自动补跑一次全量重建。",
1992
+ workerHealth
1993
+ };
1994
+ }
1995
+ registerBackgroundTasks() {
1996
+ if (!this.taskManager.has(HOST_TASK_TYPES.affairsLibraryApplyConfig)) {
1997
+ this.taskManager.register({
1998
+ taskType: HOST_TASK_TYPES.affairsLibraryApplyConfig,
1999
+ executionLane: "helper_process",
2000
+ helperProcessHandler: "affairs.library_apply_config",
2001
+ timeoutMs: INDEX_TASK_TIMEOUT_MS,
2002
+ queueWaitTimeoutMs: INDEX_TASK_QUEUE_WAIT_TIMEOUT_MS,
2003
+ run: async (input) => await this.runInternalCommand(input.rootDir, "apply-config", {
2004
+ reason: input.reason
2005
+ })
2006
+ });
2007
+ }
2008
+ if (!this.taskManager.has(HOST_TASK_TYPES.affairsLibraryDirectoryHint)) {
2009
+ this.taskManager.register({
2010
+ taskType: HOST_TASK_TYPES.affairsLibraryDirectoryHint,
2011
+ executionLane: "helper_process",
2012
+ helperProcessHandler: "affairs.library_directory_hint",
2013
+ timeoutMs: DIRECTORY_HINT_TASK_TIMEOUT_MS,
2014
+ queueWaitTimeoutMs: DIRECTORY_HINT_QUEUE_WAIT_TIMEOUT_MS,
2015
+ run: async (input) => await this.runDirectoryHintTask(input)
2016
+ });
2017
+ }
2018
+ if (!this.taskManager.has(HOST_TASK_TYPES.affairsLibraryIndex)) {
2019
+ this.taskManager.register({
2020
+ taskType: HOST_TASK_TYPES.affairsLibraryIndex,
2021
+ executionLane: "helper_process",
2022
+ helperProcessHandler: "affairs.library_index",
2023
+ timeoutMs: INDEX_TASK_TIMEOUT_MS,
2024
+ queueWaitTimeoutMs: INDEX_TASK_QUEUE_WAIT_TIMEOUT_MS,
2025
+ run: async (input) => await this.runInternalCommand(input.rootDir, input.commandMode === "incremental" || input.targetPath ? "watch-touch" : "index", {
2026
+ targetPath: input.targetPath,
2027
+ reason: input.reason
2028
+ })
2029
+ });
2030
+ }
2031
+ if (!this.taskManager.has(HOST_TASK_TYPES.affairsLibraryExport)) {
2032
+ this.taskManager.register({
2033
+ taskType: HOST_TASK_TYPES.affairsLibraryExport,
2034
+ executionLane: "helper_process",
2035
+ helperProcessHandler: "affairs.library_export",
2036
+ timeoutMs: INDEX_TASK_TIMEOUT_MS,
2037
+ queueWaitTimeoutMs: INDEX_TASK_QUEUE_WAIT_TIMEOUT_MS,
2038
+ run: async (input) => await this.runInternalCommand(input.rootDir, "export")
2039
+ });
2040
+ }
2041
+ }
2042
+ async runInternalCommand(rootDir, commandName, options = {}) {
2043
+ this.logger.info({
2044
+ rootDir,
2045
+ commandName,
2046
+ targetPath: options.targetPath ?? null,
2047
+ reason: options.reason ?? null,
2048
+ executionMode: "internal_helper"
2049
+ }, "开始执行内置事务视图文档库索引命令");
2050
+ return await runAffairsIndexerCommand(rootDir, commandName, options);
2051
+ }
2052
+ resumeEnabledBindings() {
2053
+ for (const setting of this.listEnabledSettingsWithWorkspace()) {
2054
+ const workspaceId = setting.lastWorkspaceId?.trim() ?? "";
2055
+ const userId = setting.userId?.trim() ?? "";
2056
+ if (!workspaceId || !userId) {
2057
+ continue;
2058
+ }
2059
+ const binding = this.getBinding(workspaceId, userId);
2060
+ const status = this.readIndexStatus(workspaceId, binding);
2061
+ if (status.state === "fresh" || status.state === "cooldown" || status.state === "queued" || status.state === "running") {
2062
+ this.logger.info({
2063
+ workspaceId,
2064
+ rootDir: binding?.rootDir ?? setting.rootDir ?? null,
2065
+ status: status.state,
2066
+ source: "affairs_library.startup_resume"
2067
+ }, "事务文档库启动恢复已跳过,当前索引状态无需补跑");
2068
+ continue;
2069
+ }
2070
+ this.scheduleAutoRefresh(workspaceId, "startup_resume");
2071
+ }
2072
+ }
2073
+ async flushAutoTasks(workspaceId) {
2074
+ const state = this.autoTaskStateByWorkspace.get(workspaceId);
2075
+ if (!state) {
2076
+ return;
2077
+ }
2078
+ state.timer = null;
2079
+ if (!hasPendingAutoTasks(state)) {
2080
+ this.autoTaskStateByWorkspace.delete(workspaceId);
2081
+ writeAffairsLibraryDebugLog({
2082
+ event: "auto_task_skipped",
2083
+ processRole: "host",
2084
+ workspaceId,
2085
+ source: "affairs_library.auto_task",
2086
+ status: "skipped",
2087
+ message: "当前工作区没有启用的文档库绑定"
2088
+ });
2089
+ return;
2090
+ }
2091
+ const binding = this.findEnabledBindingByWorkspaceId(workspaceId);
2092
+ const rootDir = binding?.rootDir?.trim() ?? "";
2093
+ if (!rootDir) {
2094
+ this.logger.info({
2095
+ workspaceId,
2096
+ skipped: "binding_missing",
2097
+ source: "affairs_library.auto_task"
2098
+ }, "事务文档库自动任务已跳过,当前工作区没有启用的文档库绑定");
2099
+ this.autoTaskStateByWorkspace.delete(workspaceId);
2100
+ writeAffairsLibraryDebugLog({
2101
+ event: "auto_task_skipped",
2102
+ processRole: "host",
2103
+ workspaceId,
2104
+ rootDir,
2105
+ source: "affairs_library.auto_task",
2106
+ status: "skipped",
2107
+ message: "当前根目录不可用"
2108
+ });
2109
+ return;
2110
+ }
2111
+ if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
2112
+ this.logger.info({
2113
+ workspaceId,
2114
+ rootDir,
2115
+ skipped: "root_dir_invalid",
2116
+ source: "affairs_library.auto_task"
2117
+ }, "事务文档库自动任务已跳过,当前根目录不可用");
2118
+ this.autoTaskStateByWorkspace.delete(workspaceId);
2119
+ return;
2120
+ }
2121
+ const missingArtifact = detectMissingIndexArtifact(rootDir);
2122
+ if (missingArtifact) {
2123
+ state.indexReasons.add(missingArtifact.reason);
2124
+ state.indexTargets.clear();
2125
+ writeAffairsLibraryDebugLog({
2126
+ event: "auto_task_detected_missing_artifact",
2127
+ processRole: "host",
2128
+ workspaceId,
2129
+ rootDir,
2130
+ source: "affairs_library.auto_task",
2131
+ reason: missingArtifact.reason,
2132
+ message: missingArtifact.errorSummary
2133
+ });
2134
+ }
2135
+ const blockingTask = this.reconcileOrphanedRunningTasks(workspaceId, rootDir, {
2136
+ source: "affairs_library.auto_task",
2137
+ triggerReason: "auto_refresh"
2138
+ });
2139
+ if (blockingTask) {
2140
+ this.logger.info({
2141
+ workspaceId,
2142
+ blockingTaskType: blockingTask.taskType,
2143
+ blockingTaskStatus: blockingTask.status,
2144
+ source: "affairs_library.auto_task"
2145
+ }, "事务文档库已有后台任务在跑,当前脏标记会等下一轮补跑");
2146
+ this.armAutoTaskTimer(workspaceId, AUTO_TASK_RETRY_WINDOW_MS);
2147
+ writeAffairsLibraryDebugLog({
2148
+ event: "auto_task_blocked_by_running_task",
2149
+ processRole: "host",
2150
+ workspaceId,
2151
+ rootDir,
2152
+ taskType: blockingTask.taskType,
2153
+ taskId: blockingTask.taskId,
2154
+ source: "affairs_library.auto_task",
2155
+ status: blockingTask.status,
2156
+ details: {
2157
+ blockingTaskStatus: blockingTask.status
2158
+ }
2159
+ });
2160
+ return;
2161
+ }
2162
+ if (state.applyConfigReasons.size > 0) {
2163
+ const reason = joinAutoTaskReasons(state.applyConfigReasons, `watch:${DEFAULT_CONFIG_RELATIVE_PATH}`);
2164
+ writeAffairsLibraryDebugLog({
2165
+ event: "auto_task_flush_apply_config",
2166
+ processRole: "host",
2167
+ workspaceId,
2168
+ rootDir,
2169
+ source: "affairs_library.watch_apply_config",
2170
+ reason,
2171
+ details: {
2172
+ pendingReasonCount: state.applyConfigReasons.size
2173
+ }
2174
+ });
2175
+ state.applyConfigReasons.clear();
2176
+ const handle = this.taskManager.enqueue(HOST_TASK_TYPES.affairsLibraryApplyConfig, {
2177
+ key: workspaceId,
2178
+ source: "affairs_library.watch_apply_config",
2179
+ input: {
2180
+ workspaceId,
2181
+ rootDir,
2182
+ reason
2183
+ }
2184
+ });
2185
+ this.attachAutoTaskFollowUp(workspaceId, handle, {
2186
+ rootDir,
2187
+ reason,
2188
+ source: "affairs_library.watch_apply_config"
2189
+ });
2190
+ return;
2191
+ }
2192
+ if (state.indexReasons.size > 0 || state.indexTargets.size > 0) {
2193
+ const forceFullRebuild = [...state.indexReasons].some((reason) => shouldForceFullRebuild(reason));
2194
+ const targetPath = forceFullRebuild ? undefined : pickNarrowestTargetPath([...state.indexTargets]);
2195
+ const reason = joinAutoTaskReasons(state.indexReasons, targetPath ? `watch:${targetPath}` : "watch:auto_refresh");
2196
+ writeAffairsLibraryDebugLog({
2197
+ event: "auto_task_flush_index",
2198
+ processRole: "host",
2199
+ workspaceId,
2200
+ rootDir,
2201
+ source: "affairs_library.auto_refresh",
2202
+ reason,
2203
+ targetPath: targetPath ?? null,
2204
+ details: {
2205
+ forceFullRebuild,
2206
+ pendingReasonCount: state.indexReasons.size,
2207
+ pendingTargetCount: state.indexTargets.size,
2208
+ pendingTargets: [...state.indexTargets].sort((a, b) => a.localeCompare(b, "zh-CN"))
2209
+ }
2210
+ });
2211
+ state.indexReasons.clear();
2212
+ state.indexTargets.clear();
2213
+ const handle = this.taskManager.enqueue(HOST_TASK_TYPES.affairsLibraryIndex, {
2214
+ key: workspaceId,
2215
+ source: "affairs_library.auto_refresh",
2216
+ input: {
2217
+ workspaceId,
2218
+ rootDir,
2219
+ reason,
2220
+ ...(targetPath ? {} : { commandMode: forceFullRebuild ? "full" : "incremental" }),
2221
+ ...(targetPath ? { targetPath } : {})
2222
+ }
2223
+ });
2224
+ this.attachAutoTaskFollowUp(workspaceId, handle, {
2225
+ rootDir,
2226
+ reason,
2227
+ targetPath,
2228
+ source: "affairs_library.auto_refresh"
2229
+ });
2230
+ return;
2231
+ }
2232
+ this.autoTaskStateByWorkspace.delete(workspaceId);
2233
+ }
2234
+ getOrCreateAutoTaskState(workspaceId) {
2235
+ const current = this.autoTaskStateByWorkspace.get(workspaceId);
2236
+ if (current) {
2237
+ return current;
2238
+ }
2239
+ const next = {
2240
+ timer: null,
2241
+ applyConfigReasons: new Set(),
2242
+ indexReasons: new Set(),
2243
+ indexTargets: new Set()
2244
+ };
2245
+ this.autoTaskStateByWorkspace.set(workspaceId, next);
2246
+ return next;
2247
+ }
2248
+ armAutoTaskTimer(workspaceId, delayMs) {
2249
+ const state = this.getOrCreateAutoTaskState(workspaceId);
2250
+ if (state.timer) {
2251
+ clearTimeout(state.timer);
2252
+ }
2253
+ state.timer = setTimeout(() => {
2254
+ void this.flushAutoTasks(workspaceId);
2255
+ }, delayMs);
2256
+ }
2257
+ reconcileOrphanedRunningTasks(workspaceId, rootDir, meta) {
2258
+ const runtimeStatus = readRuntimeStatusFileSafe(rootDir);
2259
+ const activeSnapshots = this.findActiveLibraryTaskSnapshots(workspaceId);
2260
+ for (const snapshot of activeSnapshots) {
2261
+ if (snapshot.status !== "running") {
2262
+ continue;
2263
+ }
2264
+ const orphanedRunningTask = detectOrphanedRunningTask(rootDir, snapshot, runtimeStatus);
2265
+ if (!orphanedRunningTask) {
2266
+ continue;
2267
+ }
2268
+ this.logger.warn?.({
2269
+ workspaceId,
2270
+ rootDir,
2271
+ taskType: snapshot.taskType,
2272
+ taskId: snapshot.taskId,
2273
+ reason: orphanedRunningTask.reason,
2274
+ ownerPid: orphanedRunningTask.ownerPid,
2275
+ heartbeatAgeMs: orphanedRunningTask.heartbeatAgeMs,
2276
+ runtimeUpdatedAt: orphanedRunningTask.runtimeUpdatedAt,
2277
+ runtimeAgeMs: orphanedRunningTask.runtimeAgeMs,
2278
+ runningStage: orphanedRunningTask.runningStage,
2279
+ source: meta.source,
2280
+ triggerReason: meta.triggerReason
2281
+ }, "检测到事务文档库 orphan running 任务,准备主动清理");
2282
+ writeAffairsLibraryDebugLog({
2283
+ event: "orphan_running_task_detected",
2284
+ processRole: "host",
2285
+ workspaceId,
2286
+ rootDir,
2287
+ taskType: snapshot.taskType,
2288
+ taskId: snapshot.taskId,
2289
+ source: meta.source,
2290
+ reason: orphanedRunningTask.reason,
2291
+ status: snapshot.status,
2292
+ details: {
2293
+ triggerReason: meta.triggerReason,
2294
+ ownerPid: orphanedRunningTask.ownerPid,
2295
+ heartbeatAgeMs: orphanedRunningTask.heartbeatAgeMs,
2296
+ runtimeUpdatedAt: orphanedRunningTask.runtimeUpdatedAt,
2297
+ runtimeAgeMs: orphanedRunningTask.runtimeAgeMs,
2298
+ runningStage: orphanedRunningTask.runningStage
2299
+ },
2300
+ message: orphanedRunningTask.errorSummary
2301
+ });
2302
+ this.taskManager.cancel(snapshot.taskType, workspaceId, `orphaned_helper_process:${orphanedRunningTask.reason}`);
2303
+ writeAffairsLibraryDebugLog({
2304
+ event: "orphan_running_task_cancelled",
2305
+ processRole: "host",
2306
+ workspaceId,
2307
+ rootDir,
2308
+ taskType: snapshot.taskType,
2309
+ taskId: snapshot.taskId,
2310
+ source: meta.source,
2311
+ reason: orphanedRunningTask.reason,
2312
+ status: "cancelled",
2313
+ details: {
2314
+ triggerReason: meta.triggerReason,
2315
+ ownerPid: orphanedRunningTask.ownerPid,
2316
+ heartbeatAgeMs: orphanedRunningTask.heartbeatAgeMs,
2317
+ runtimeUpdatedAt: orphanedRunningTask.runtimeUpdatedAt,
2318
+ runtimeAgeMs: orphanedRunningTask.runtimeAgeMs,
2319
+ runningStage: orphanedRunningTask.runningStage
2320
+ },
2321
+ message: "检测到 orphan running 任务后已主动取消,避免持续阻塞后续刷新。"
2322
+ });
2323
+ this.logger.warn?.({
2324
+ workspaceId,
2325
+ rootDir,
2326
+ taskType: snapshot.taskType,
2327
+ taskId: snapshot.taskId,
2328
+ reason: orphanedRunningTask.reason,
2329
+ source: meta.source,
2330
+ triggerReason: meta.triggerReason
2331
+ }, "事务文档库 orphan running 任务已主动取消");
2332
+ }
2333
+ return this.findBlockingAutoTask(workspaceId);
2334
+ }
2335
+ findActiveLibraryTaskSnapshots(workspaceId) {
2336
+ const taskTypes = [
2337
+ HOST_TASK_TYPES.affairsLibraryApplyConfig,
2338
+ HOST_TASK_TYPES.affairsLibraryIndex,
2339
+ HOST_TASK_TYPES.affairsLibraryExport
2340
+ ];
2341
+ return taskTypes
2342
+ .map((taskType) => this.taskManager.peek(taskType, workspaceId))
2343
+ .filter((snapshot) => Boolean(snapshot))
2344
+ .filter((snapshot) => snapshot.status === "queued" || snapshot.status === "running");
2345
+ }
2346
+ findBlockingAutoTask(workspaceId) {
2347
+ return this.findActiveLibraryTaskSnapshots(workspaceId)[0] ?? null;
2348
+ }
2349
+ findRelevantIndexTaskSnapshot(workspaceId) {
2350
+ const taskTypes = [
2351
+ HOST_TASK_TYPES.affairsLibraryApplyConfig,
2352
+ HOST_TASK_TYPES.affairsLibraryIndex,
2353
+ HOST_TASK_TYPES.affairsLibraryExport
2354
+ ];
2355
+ const snapshots = taskTypes
2356
+ .map((taskType) => this.taskManager.peek(taskType, workspaceId))
2357
+ .filter((snapshot) => Boolean(snapshot));
2358
+ const active = snapshots
2359
+ .filter((snapshot) => snapshot.status === "queued" || snapshot.status === "running")
2360
+ .sort((left, right) => (right.startedAt ?? right.enqueuedAt ?? 0) - (left.startedAt ?? left.enqueuedAt ?? 0));
2361
+ if (active.length > 0) {
2362
+ return active[0] ?? null;
2363
+ }
2364
+ const failed = snapshots
2365
+ .filter((snapshot) => snapshot.status === "failed"
2366
+ || snapshot.status === "timeout"
2367
+ || snapshot.status === "cancelled"
2368
+ || snapshot.status === "queue_timeout")
2369
+ .sort((left, right) => (right.finishedAt ?? right.startedAt ?? right.enqueuedAt ?? 0)
2370
+ - (left.finishedAt ?? left.startedAt ?? left.enqueuedAt ?? 0));
2371
+ return failed[0] ?? null;
2372
+ }
2373
+ attachAutoTaskFollowUp(workspaceId, handle, meta) {
2374
+ this.logger.info({
2375
+ workspaceId,
2376
+ rootDir: meta.rootDir,
2377
+ reason: meta.reason,
2378
+ targetPath: meta.targetPath ?? null,
2379
+ taskType: handle.taskType,
2380
+ taskId: handle.taskId,
2381
+ deduped: handle.deduped,
2382
+ source: meta.source
2383
+ }, "事务文档库自动任务已入队");
2384
+ writeAffairsLibraryDebugLog({
2385
+ event: "task_enqueued",
2386
+ processRole: "host",
2387
+ workspaceId,
2388
+ rootDir: meta.rootDir,
2389
+ taskType: handle.taskType,
2390
+ taskId: handle.taskId,
2391
+ source: meta.source,
2392
+ reason: meta.reason,
2393
+ targetPath: meta.targetPath ?? null,
2394
+ deduped: handle.deduped,
2395
+ status: "queued"
2396
+ });
2397
+ void handle.promise.then((result) => {
2398
+ this.invalidateExportCache(meta.rootDir);
2399
+ writeAffairsLibraryDebugLog({
2400
+ event: "task_finished",
2401
+ processRole: "host",
2402
+ workspaceId,
2403
+ rootDir: meta.rootDir,
2404
+ taskType: handle.taskType,
2405
+ taskId: handle.taskId,
2406
+ command: result.command,
2407
+ source: meta.source,
2408
+ reason: meta.reason,
2409
+ targetPath: meta.targetPath ?? null,
2410
+ durationMs: result.durationMs,
2411
+ status: "finished",
2412
+ deduped: handle.deduped,
2413
+ resultSummary: summarizeIndexerCommandResult(result.result)
2414
+ });
2415
+ this.logger.info({
2416
+ workspaceId,
2417
+ rootDir: meta.rootDir,
2418
+ reason: meta.reason,
2419
+ targetPath: meta.targetPath ?? null,
2420
+ taskType: handle.taskType,
2421
+ taskId: handle.taskId,
2422
+ command: result.command,
2423
+ durationMs: result.durationMs,
2424
+ resultSummary: summarizeIndexerCommandResult(result.result),
2425
+ source: meta.source
2426
+ }, "事务文档库自动任务执行完成");
2427
+ }, (error) => {
2428
+ writeAffairsLibraryDebugLog({
2429
+ event: "task_failed",
2430
+ processRole: "host",
2431
+ workspaceId,
2432
+ rootDir: meta.rootDir,
2433
+ taskType: handle.taskType,
2434
+ taskId: handle.taskId,
2435
+ source: meta.source,
2436
+ reason: meta.reason,
2437
+ targetPath: meta.targetPath ?? null,
2438
+ status: "failed",
2439
+ deduped: handle.deduped,
2440
+ message: error instanceof Error ? error.message : String(error)
2441
+ });
2442
+ this.logger.info({
2443
+ workspaceId,
2444
+ rootDir: meta.rootDir,
2445
+ reason: meta.reason,
2446
+ targetPath: meta.targetPath ?? null,
2447
+ taskType: handle.taskType,
2448
+ taskId: handle.taskId,
2449
+ error: error instanceof Error ? error.message : String(error),
2450
+ source: meta.source
2451
+ }, "事务文档库自动任务执行失败");
2452
+ }).finally(() => {
2453
+ const state = this.autoTaskStateByWorkspace.get(workspaceId);
2454
+ if (!state) {
2455
+ return;
2456
+ }
2457
+ if (!hasPendingAutoTasks(state)) {
2458
+ if (!state.timer) {
2459
+ this.autoTaskStateByWorkspace.delete(workspaceId);
2460
+ }
2461
+ return;
2462
+ }
2463
+ this.armAutoTaskTimer(workspaceId, 50);
2464
+ });
2465
+ }
2466
+ readExportData(rootDir) {
2467
+ const exportRoot = path.join(rootDir, EXPORT_DIR_RELATIVE_PATH);
2468
+ const manifestPath = path.join(rootDir, EXPORT_MANIFEST_RELATIVE_PATH);
2469
+ const signature = this.buildExportSignature(exportRoot, manifestPath);
2470
+ const cached = this.exportCache.get(rootDir);
2471
+ if (cached && cached.signature === signature) {
2472
+ return {
2473
+ documents: cached.documents,
2474
+ tags: cached.tags,
2475
+ folders: cached.folders,
2476
+ generatedAt: cached.generatedAt
2477
+ };
2478
+ }
2479
+ const diskCache = this.readExportCacheFile(exportRoot, signature);
2480
+ if (diskCache) {
2481
+ this.exportCache.set(rootDir, diskCache);
2482
+ return {
2483
+ documents: diskCache.documents,
2484
+ tags: diskCache.tags,
2485
+ folders: diskCache.folders,
2486
+ generatedAt: diskCache.generatedAt
2487
+ };
2488
+ }
2489
+ const parsed = this.parseExportData(rootDir, exportRoot, manifestPath);
2490
+ const cachePayload = {
2491
+ schemaVersion: SNAPSHOT_CACHE_SCHEMA_VERSION,
2492
+ signature,
2493
+ generatedAt: parsed.generatedAt,
2494
+ documents: parsed.documents,
2495
+ tags: parsed.tags,
2496
+ folders: parsed.folders
2497
+ };
2498
+ this.exportCache.set(rootDir, cachePayload);
2499
+ this.writeExportCacheFile(exportRoot, cachePayload);
2500
+ return parsed;
2501
+ }
2502
+ parseExportData(rootDir, exportRoot, manifestPath) {
2503
+ const manifest = readJsonFile(manifestPath);
2504
+ const metaShardPaths = (manifest.meta_shards ?? [])
2505
+ .map((item) => item.path?.trim() ?? "")
2506
+ .filter(Boolean);
2507
+ const documents = metaShardPaths.flatMap((relativePath) => {
2508
+ const payload = readJsonFile(path.join(exportRoot, relativePath));
2509
+ return (payload.documents ?? []).map((document) => {
2510
+ const safePath = document.path?.trim() ?? "";
2511
+ return {
2512
+ documentId: document.document_id?.trim() ?? safePath,
2513
+ path: safePath,
2514
+ title: document.title?.trim() || path.basename(safePath) || "未命名文档",
2515
+ summary: document.summary?.trim() ?? "",
2516
+ updatedAt: document.mtime?.trim() ?? "",
2517
+ createdAt: null,
2518
+ sizeBytes: null,
2519
+ tags: Array.isArray(document.direct_tags) ? document.direct_tags.filter(Boolean) : [],
2520
+ derivedTags: Array.isArray(document.derived_tags) ? document.derived_tags.filter(Boolean) : [],
2521
+ isFavorite: false
2522
+ };
2523
+ });
2524
+ });
2525
+ const taxonomyEntry = manifest.entries?.taxonomy?.trim() || "taxonomy.json";
2526
+ const taxonomy = readJsonFile(path.join(exportRoot, taxonomyEntry));
2527
+ const tags = (taxonomy.nodes ?? []).map((node) => ({
2528
+ path: node.path?.trim() ?? "",
2529
+ name: node.name?.trim() || node.path?.trim() || "未命名标签",
2530
+ rootType: node.root_type?.trim() || "unknown",
2531
+ parentPath: node.parent_path?.trim() || null,
2532
+ depth: Number.isFinite(node.depth) ? Number(node.depth) : 0,
2533
+ documentCount: countDocumentsForTag(documents, node.path?.trim() ?? "")
2534
+ })).filter((node) => node.path);
2535
+ const bootstrapEntry = manifest.entries?.bootstrap?.trim() || "bootstrap.json";
2536
+ const bootstrap = readJsonFile(path.join(exportRoot, bootstrapEntry));
2537
+ const folders = (bootstrap.folders ?? []).map((folder) => {
2538
+ const normalizedPath = folder.path?.trim() ?? ".";
2539
+ const folderStats = readAffairsLibraryStatsSafe(rootDir, normalizedPath);
2540
+ return {
2541
+ path: normalizedPath,
2542
+ name: folder.name?.trim() || "资料库",
2543
+ parentPath: folder.parent_path?.trim() || null,
2544
+ directDocumentCount: Number(folder.direct_document_count ?? 0),
2545
+ documentCount: Number(folder.document_count ?? 0),
2546
+ createdAt: toIsoOrNull(folderStats?.birthtime),
2547
+ updatedAt: toIsoOrNull(folderStats?.mtime)
2548
+ };
2549
+ });
2550
+ return {
2551
+ documents,
2552
+ tags,
2553
+ folders,
2554
+ generatedAt: manifest.generated_at?.trim() ?? null
2555
+ };
2556
+ }
2557
+ buildExportSignature(exportRoot, manifestPath) {
2558
+ const manifestStat = fs.statSync(manifestPath);
2559
+ const statusPath = path.join(exportRoot, "status.json");
2560
+ const statusStat = fs.existsSync(statusPath) ? fs.statSync(statusPath) : null;
2561
+ const statusPayload = statusStat ? readJsonFile(statusPath) : null;
2562
+ return [
2563
+ statusPayload?.exported_at?.trim() ?? "missing",
2564
+ statusPayload?.document_count ?? "missing",
2565
+ manifestStat.mtimeMs,
2566
+ manifestStat.size,
2567
+ statusStat?.mtimeMs ?? "missing",
2568
+ statusStat?.size ?? "missing"
2569
+ ].join(":");
2570
+ }
2571
+ invalidateExportCache(rootDir) {
2572
+ this.exportCache.delete(rootDir);
2573
+ const cachePath = path.join(rootDir, EXPORT_DIR_RELATIVE_PATH, SNAPSHOT_CACHE_FILE_NAME);
2574
+ writeAffairsLibraryDebugLog({
2575
+ event: "snapshot_cache_invalidated",
2576
+ processRole: "host",
2577
+ rootDir,
2578
+ source: "affairs_library.export_cache",
2579
+ details: {
2580
+ cachePath
2581
+ }
2582
+ });
2583
+ }
2584
+ readExportCacheFile(exportRoot, signature) {
2585
+ const cachePath = path.join(exportRoot, SNAPSHOT_CACHE_FILE_NAME);
2586
+ if (!fs.existsSync(cachePath)) {
2587
+ return null;
2588
+ }
2589
+ try {
2590
+ const payload = readJsonFile(cachePath);
2591
+ if (payload.schemaVersion !== SNAPSHOT_CACHE_SCHEMA_VERSION || payload.signature !== signature) {
2592
+ return null;
2593
+ }
2594
+ return payload;
2595
+ }
2596
+ catch {
2597
+ return null;
2598
+ }
2599
+ }
2600
+ writeExportCacheFile(exportRoot, payload) {
2601
+ const cachePath = path.join(exportRoot, SNAPSHOT_CACHE_FILE_NAME);
2602
+ try {
2603
+ fs.writeFileSync(cachePath, JSON.stringify(payload));
2604
+ }
2605
+ catch {
2606
+ // 缓存写失败不影响主流程,直接忽略。
2607
+ }
2608
+ }
2609
+ buildPreviewResult(input) {
2610
+ return {
2611
+ ...input,
2612
+ previewPath: null,
2613
+ previewUrl: null,
2614
+ onlyOffice: null,
2615
+ capabilities: buildPreviewCapabilities(input.kind, {
2616
+ supported: input.supported,
2617
+ content: input.content,
2618
+ version: input.version
2619
+ })
2620
+ };
2621
+ }
2622
+ readAvailableExportData(rootDir) {
2623
+ const startedAtMs = Date.now();
2624
+ try {
2625
+ const result = this.readExportData(rootDir);
2626
+ writeAffairsLibraryDebugLog({
2627
+ event: "export_data_read",
2628
+ processRole: "host",
2629
+ rootDir,
2630
+ source: "affairs_library.export_data",
2631
+ status: result ? "fresh" : "missing",
2632
+ durationMs: Math.max(0, Date.now() - startedAtMs),
2633
+ details: {
2634
+ generatedAt: result?.generatedAt ?? null,
2635
+ documentCount: result?.documents.length ?? 0
2636
+ }
2637
+ });
2638
+ return result;
2639
+ }
2640
+ catch {
2641
+ const fallback = this.readLastUsableExportData(rootDir);
2642
+ writeAffairsLibraryDebugLog({
2643
+ event: "export_data_read",
2644
+ processRole: "host",
2645
+ rootDir,
2646
+ source: "affairs_library.export_data",
2647
+ status: fallback ? "stale_fallback" : "missing",
2648
+ durationMs: Math.max(0, Date.now() - startedAtMs),
2649
+ details: {
2650
+ generatedAt: fallback?.generatedAt ?? null,
2651
+ documentCount: fallback?.documents.length ?? 0
2652
+ }
2653
+ });
2654
+ return fallback;
2655
+ }
2656
+ }
2657
+ readLastUsableExportData(rootDir) {
2658
+ const cached = this.exportCache.get(rootDir);
2659
+ if (cached) {
2660
+ return {
2661
+ documents: cached.documents,
2662
+ tags: cached.tags,
2663
+ folders: cached.folders,
2664
+ generatedAt: cached.generatedAt
2665
+ };
2666
+ }
2667
+ const exportRoot = path.join(rootDir, EXPORT_DIR_RELATIVE_PATH);
2668
+ const cachePath = path.join(exportRoot, SNAPSHOT_CACHE_FILE_NAME);
2669
+ if (!fs.existsSync(cachePath)) {
2670
+ return null;
2671
+ }
2672
+ try {
2673
+ const payload = readJsonFile(cachePath);
2674
+ this.exportCache.set(rootDir, payload);
2675
+ return {
2676
+ documents: payload.documents,
2677
+ tags: payload.tags,
2678
+ folders: payload.folders,
2679
+ generatedAt: payload.generatedAt
2680
+ };
2681
+ }
2682
+ catch {
2683
+ return null;
2684
+ }
2685
+ }
2686
+ readConfig(rootDir) {
2687
+ const configPath = path.join(rootDir, DEFAULT_CONFIG_RELATIVE_PATH);
2688
+ const payload = this.readRawConfigFile(configPath);
2689
+ return {
2690
+ mirrorRoot: normalizeOptionalAbsolutePath(payload.mirrorRoot),
2691
+ allowedExtensions: normalizeAllowedExtensions(payload.allowedExtensions ?? []),
2692
+ includedHiddenPaths: normalizeIncludedHiddenPaths(payload.includedHiddenPaths ?? []),
2693
+ folderOpenBehavior: normalizeFolderOpenBehavior(payload.folderOpenBehavior)
2694
+ };
2695
+ }
2696
+ readRawConfigFile(configPath) {
2697
+ if (!fs.existsSync(configPath)) {
2698
+ return {};
2699
+ }
2700
+ try {
2701
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
2702
+ }
2703
+ catch {
2704
+ return {};
2705
+ }
2706
+ }
2707
+ getGlobalSetting(userId) {
2708
+ return this.userAffairsLibrarySettingRepository.findByUserId(userId);
2709
+ }
2710
+ getEnabledBindingForWorkspace(workspaceId) {
2711
+ return this.findEnabledBindingByWorkspaceId(workspaceId);
2712
+ }
2713
+ listEnabledBindingsForWatch() {
2714
+ return this.listEnabledSettingsWithWorkspace().map((item) => ({
2715
+ workspaceId: item.lastWorkspaceId ?? null,
2716
+ rootDir: item.rootDir ?? null,
2717
+ enabled: item.enabled === true
2718
+ }));
2719
+ }
2720
+ getBindingForWatch(workspaceId) {
2721
+ const binding = this.findEnabledBindingByWorkspaceId(workspaceId);
2722
+ if (!binding) {
2723
+ return null;
2724
+ }
2725
+ return {
2726
+ workspaceId: binding.lastWorkspaceId ?? null,
2727
+ rootDir: binding.rootDir ?? null,
2728
+ enabled: binding.enabled === true
2729
+ };
2730
+ }
2731
+ buildBindingFromSetting(setting, fallbackWorkspaceId) {
2732
+ const rootDir = setting?.rootDir?.trim();
2733
+ if (!rootDir) {
2734
+ return null;
2735
+ }
2736
+ const config = this.readConfig(rootDir);
2737
+ return {
2738
+ workspaceId: setting?.lastWorkspaceId ?? fallbackWorkspaceId,
2739
+ rootDir,
2740
+ enabled: setting?.enabled === true,
2741
+ mirrorRoot: config.mirrorRoot,
2742
+ allowedExtensions: config.allowedExtensions,
2743
+ includedHiddenPaths: config.includedHiddenPaths,
2744
+ folderOpenBehavior: config.folderOpenBehavior,
2745
+ configRelativePath: DEFAULT_CONFIG_RELATIVE_PATH,
2746
+ exportMode: DEFAULT_EXPORT_MODE,
2747
+ updatedAt: setting?.updatedAt ?? nowIso()
2748
+ };
2749
+ }
2750
+ requireBinding(workspaceId, userId) {
2751
+ const binding = this.getBinding(workspaceId, userId);
2752
+ if (!binding) {
2753
+ throw new AppError({
2754
+ statusCode: 409,
2755
+ errorCode: "AFFAIRS_LIBRARY_BINDING_REQUIRED",
2756
+ detail: "当前工作区还没有绑定文档库路径"
2757
+ });
2758
+ }
2759
+ return binding;
2760
+ }
2761
+ resolveLibrarySetting(userId, workspaceId) {
2762
+ const currentSetting = this.userAffairsLibrarySettingRepository.findByUserId(userId);
2763
+ const currentRootDir = currentSetting?.rootDir?.trim() ?? "";
2764
+ if (currentRootDir) {
2765
+ return currentSetting;
2766
+ }
2767
+ const workspaceScope = workspaceId?.trim() ?? "";
2768
+ const legacyFromWorkspace = ((workspaceScope
2769
+ ? this.workspaceNavigationStateRepository.findByWorkspaceIdAndUserId(workspaceScope, userId)
2770
+ : null) ?? (workspaceScope
2771
+ ? this.workspaceNavigationStateRepository.findLatestAffairsLibraryByWorkspaceId(workspaceScope)
2772
+ : null)) ?? null;
2773
+ const legacyFromUser = !workspaceScope
2774
+ ? this.workspaceNavigationStateRepository
2775
+ .listByUserId(userId)
2776
+ .filter((item) => item.affairsLibraryRootPath?.trim())
2777
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? null
2778
+ : null;
2779
+ const legacy = legacyFromWorkspace ?? legacyFromUser;
2780
+ const legacyRootDir = legacy?.affairsLibraryRootPath?.trim() ?? "";
2781
+ if (!legacyRootDir) {
2782
+ return currentSetting;
2783
+ }
2784
+ const migrated = this.upsertLibrarySetting({
2785
+ userId,
2786
+ rootDir: legacyRootDir,
2787
+ enabled: legacy?.affairsLibraryEnabled === true,
2788
+ favoritesJson: legacy?.affairsLibraryFavoritesJson ?? "[]",
2789
+ lastWorkspaceId: legacy?.workspaceId ?? (workspaceScope || null),
2790
+ createdAt: currentSetting?.createdAt ?? legacy?.updatedAt ?? nowIso(),
2791
+ updatedAt: legacy?.updatedAt ?? nowIso()
2792
+ });
2793
+ return migrated;
2794
+ }
2795
+ upsertLibrarySetting(record) {
2796
+ return this.userAffairsLibrarySettingRepository.upsert({
2797
+ userId: record.userId,
2798
+ rootDir: record.rootDir?.trim() || null,
2799
+ enabled: record.enabled === true,
2800
+ favoritesJson: record.favoritesJson ?? null,
2801
+ lastWorkspaceId: record.lastWorkspaceId?.trim() || null,
2802
+ createdAt: record.createdAt,
2803
+ updatedAt: record.updatedAt
2804
+ });
2805
+ }
2806
+ listEnabledSettingsWithWorkspace() {
2807
+ if (typeof this.userAffairsLibrarySettingRepository.listEnabled === "function") {
2808
+ return this.userAffairsLibrarySettingRepository
2809
+ .listEnabled()
2810
+ .filter((item) => Boolean(item.lastWorkspaceId?.trim() && item.rootDir?.trim()));
2811
+ }
2812
+ return this.workspaceNavigationStateRepository
2813
+ .listEnabledAffairsLibraries()
2814
+ .map((item) => ({
2815
+ userId: item.userId,
2816
+ rootDir: item.affairsLibraryRootPath ?? null,
2817
+ enabled: item.affairsLibraryEnabled === true,
2818
+ favoritesJson: item.affairsLibraryFavoritesJson ?? null,
2819
+ lastWorkspaceId: item.workspaceId,
2820
+ createdAt: item.updatedAt,
2821
+ updatedAt: item.updatedAt
2822
+ }))
2823
+ .filter((item) => Boolean(item.lastWorkspaceId?.trim() && item.rootDir?.trim()));
2824
+ }
2825
+ findEnabledBindingByWorkspaceId(workspaceId) {
2826
+ const normalizedWorkspaceId = workspaceId.trim();
2827
+ if (!normalizedWorkspaceId) {
2828
+ return null;
2829
+ }
2830
+ if (typeof this.userAffairsLibrarySettingRepository.findEnabledByWorkspaceId === "function") {
2831
+ return this.userAffairsLibrarySettingRepository.findEnabledByWorkspaceId(normalizedWorkspaceId);
2832
+ }
2833
+ const legacy = this.workspaceNavigationStateRepository.findAnyEnabledAffairsLibraryByWorkspaceId(normalizedWorkspaceId);
2834
+ if (!legacy?.affairsLibraryRootPath?.trim()) {
2835
+ return null;
2836
+ }
2837
+ return {
2838
+ userId: legacy.userId,
2839
+ rootDir: legacy.affairsLibraryRootPath ?? null,
2840
+ enabled: legacy.affairsLibraryEnabled === true,
2841
+ favoritesJson: legacy.affairsLibraryFavoritesJson ?? null,
2842
+ lastWorkspaceId: legacy.workspaceId,
2843
+ createdAt: legacy.updatedAt,
2844
+ updatedAt: legacy.updatedAt
2845
+ };
2846
+ }
2847
+ normalizeAndValidateBindingRootDir(rootDir) {
2848
+ const normalizedRootDir = rootDir.trim();
2849
+ if (!normalizedRootDir) {
2850
+ throw new AppError({
2851
+ statusCode: 400,
2852
+ errorCode: "AFFAIRS_LIBRARY_ROOT_REQUIRED",
2853
+ detail: "文档库路径不能为空",
2854
+ field: "rootDir"
2855
+ });
2856
+ }
2857
+ if (!path.isAbsolute(normalizedRootDir)) {
2858
+ throw new AppError({
2859
+ statusCode: 400,
2860
+ errorCode: "AFFAIRS_LIBRARY_ROOT_NOT_ABSOLUTE",
2861
+ detail: "文档库路径必须是绝对路径",
2862
+ field: "rootDir"
2863
+ });
2864
+ }
2865
+ if (!fs.existsSync(normalizedRootDir) || !fs.statSync(normalizedRootDir).isDirectory()) {
2866
+ throw new AppError({
2867
+ statusCode: 400,
2868
+ errorCode: "AFFAIRS_LIBRARY_ROOT_INVALID",
2869
+ detail: "文档库路径不存在,或者不是文件夹",
2870
+ field: "rootDir"
2871
+ });
2872
+ }
2873
+ return normalizedRootDir;
2874
+ }
2875
+ normalizeFavorites(favorites) {
2876
+ return favorites
2877
+ .filter((item) => item && (item.kind === "folder" || item.kind === "tag") && item.path.trim())
2878
+ .map((item) => ({
2879
+ kind: item.kind,
2880
+ path: item.path.trim(),
2881
+ label: item.label.trim() || item.path.trim()
2882
+ }));
2883
+ }
2884
+ resolvePreferredWorkspaceId(preferredWorkspaceId) {
2885
+ const normalizedPreferredWorkspaceId = preferredWorkspaceId?.trim() ?? "";
2886
+ if (normalizedPreferredWorkspaceId) {
2887
+ try {
2888
+ this.workspaceService.getWorkspaceOrThrow(normalizedPreferredWorkspaceId);
2889
+ return normalizedPreferredWorkspaceId;
2890
+ }
2891
+ catch {
2892
+ // 旧的随机 workspaceId 已经失效时,直接降级到当前仍可见的工作区。
2893
+ }
2894
+ }
2895
+ return this.workspaceService.list()[0]?.id ?? null;
2896
+ }
2897
+ ensureLibraryEnabled(binding) {
2898
+ if (binding.enabled) {
2899
+ return;
2900
+ }
2901
+ throw new AppError({
2902
+ statusCode: 409,
2903
+ errorCode: "AFFAIRS_LIBRARY_DISABLED",
2904
+ detail: "文档库功能还没有启用,启用后才会启动内置索引服务。"
2905
+ });
2906
+ }
2907
+ assertLibraryRootDir(rootDir) {
2908
+ if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
2909
+ throw new AppError({
2910
+ statusCode: 400,
2911
+ errorCode: "AFFAIRS_LIBRARY_ROOT_INVALID",
2912
+ detail: "事务资料库路径不存在,或者不是文件夹",
2913
+ field: "path"
2914
+ });
2915
+ }
2916
+ }
2917
+ }
2918
+ function buildOfficeDocumentVersion(fileSize, updatedAt) {
2919
+ if (!updatedAt) {
2920
+ return null;
2921
+ }
2922
+ return `${updatedAt}:${fileSize}`;
2923
+ }
2924
+ function shouldEnableAffairsLibraryInlineEditing(previewKind, fileSize) {
2925
+ return fileSize <= MAX_TEXT_FILE_BYTES
2926
+ && (previewKind === "text" || previewKind === "markdown" || previewKind === "html");
2927
+ }
2928
+ function ensureEditableAffairsLibraryTextBuffer(buffer) {
2929
+ if (buffer.byteLength > MAX_TEXT_FILE_BYTES) {
2930
+ throw new AppError({
2931
+ statusCode: 400,
2932
+ errorCode: "FILE_TOO_LARGE",
2933
+ detail: "文件过大,暂不支持直接编辑",
2934
+ field: "srcPath"
2935
+ });
2936
+ }
2937
+ if (buffer.includes(0)) {
2938
+ throw new AppError({
2939
+ statusCode: 400,
2940
+ errorCode: "BINARY_FILE_NOT_SUPPORTED",
2941
+ detail: "二进制文件暂不支持直接编辑",
2942
+ field: "srcPath"
2943
+ });
2944
+ }
2945
+ }
2946
+ function ensureWritableAffairsLibraryTextBuffer(buffer) {
2947
+ if (buffer.byteLength > MAX_TEXT_FILE_BYTES) {
2948
+ throw new AppError({
2949
+ statusCode: 400,
2950
+ errorCode: "FILE_TOO_LARGE",
2951
+ detail: "文件过大,暂不支持直接保存",
2952
+ field: "content"
2953
+ });
2954
+ }
2955
+ }
2956
+ function hasPendingAutoTasks(state) {
2957
+ return state.applyConfigReasons.size > 0
2958
+ || state.indexReasons.size > 0
2959
+ || state.indexTargets.size > 0;
2960
+ }
2961
+ function joinAutoTaskReasons(reasons, fallback) {
2962
+ const items = [...reasons]
2963
+ .map((item) => item.trim())
2964
+ .filter(Boolean)
2965
+ .sort((a, b) => a.localeCompare(b, "zh-CN"));
2966
+ return items.length > 0 ? items.join(" | ") : fallback;
2967
+ }
2968
+ function pickNarrowestTargetPath(targets) {
2969
+ if (targets.length === 0) {
2970
+ return undefined;
2971
+ }
2972
+ let selected = targets[0]?.trim() || undefined;
2973
+ for (const target of targets) {
2974
+ const normalizedTarget = target.trim();
2975
+ if (!normalizedTarget) {
2976
+ continue;
2977
+ }
2978
+ if (!selected || selected.startsWith(`${normalizedTarget}/`)) {
2979
+ selected = normalizedTarget;
2980
+ }
2981
+ }
2982
+ return selected || undefined;
2983
+ }
2984
+ function summarizeIndexerCommandResult(result) {
2985
+ if (!result || typeof result !== "object") {
2986
+ return null;
2987
+ }
2988
+ const payload = result;
2989
+ const indexResult = payload.indexResult;
2990
+ if (indexResult && typeof indexResult === "object") {
2991
+ const indexPayload = indexResult;
2992
+ return {
2993
+ scannedCount: indexPayload.scannedCount ?? null,
2994
+ indexedCount: indexPayload.indexedCount ?? null,
2995
+ skippedCount: indexPayload.skippedCount ?? null,
2996
+ failedCount: indexPayload.failedCount ?? null,
2997
+ deletedCount: indexPayload.deletedCount ?? null,
2998
+ dirtyScope: indexPayload.dirtyScope ?? null
2999
+ };
3000
+ }
3001
+ if ("scannedCount" in payload || "indexedCount" in payload || "failedCount" in payload) {
3002
+ return {
3003
+ scannedCount: payload.scannedCount ?? null,
3004
+ indexedCount: payload.indexedCount ?? null,
3005
+ skippedCount: payload.skipStats && typeof payload.skipStats === "object"
3006
+ ? payload.skipStats.skippedCount ?? null
3007
+ : null,
3008
+ failedCount: payload.failedCount ?? null,
3009
+ deletedCount: payload.deletedCount ?? null
3010
+ };
3011
+ }
3012
+ if ("changed" in payload || "addedExtensions" in payload || "removedExtensions" in payload) {
3013
+ return {
3014
+ changed: payload.changed ?? null,
3015
+ addedExtensions: payload.addedExtensions ?? null,
3016
+ removedExtensions: payload.removedExtensions ?? null
3017
+ };
3018
+ }
3019
+ if ("documentCount" in payload || "exportedAt" in payload) {
3020
+ return {
3021
+ documentCount: payload.documentCount ?? null,
3022
+ exportedAt: payload.exportedAt ?? null
3023
+ };
3024
+ }
3025
+ return null;
3026
+ }
3027
+ function countDocumentsForTag(documents, tagPath) {
3028
+ if (!tagPath) {
3029
+ return 0;
3030
+ }
3031
+ return documents.filter((document) => matchesTagPath(document, tagPath)).length;
3032
+ }
3033
+ function normalizeSelectedTagPaths(tagPaths) {
3034
+ if (!Array.isArray(tagPaths)) {
3035
+ return [];
3036
+ }
3037
+ const unique = new Set();
3038
+ tagPaths.forEach((item) => {
3039
+ const normalized = item.trim();
3040
+ if (normalized) {
3041
+ unique.add(normalized);
3042
+ }
3043
+ });
3044
+ return Array.from(unique);
3045
+ }
3046
+ function buildTagFacetCounts(documents, selectedTagPaths, selectedFavoriteTagPath) {
3047
+ const activeTagPaths = selectedFavoriteTagPath?.trim()
3048
+ ? [selectedFavoriteTagPath.trim()]
3049
+ : selectedTagPaths;
3050
+ const counts = new Map();
3051
+ for (const document of documents) {
3052
+ const allTags = [...document.tags, ...document.derivedTags];
3053
+ const uniqueTags = new Set();
3054
+ allTags
3055
+ .map((item) => item.trim())
3056
+ .filter((item) => item.length > 0)
3057
+ .forEach((tag) => {
3058
+ uniqueTags.add(tag);
3059
+ buildAncestorPaths(tag).forEach((ancestorPath) => {
3060
+ uniqueTags.add(ancestorPath);
3061
+ });
3062
+ });
3063
+ uniqueTags.forEach((tag) => {
3064
+ const available = activeTagPaths
3065
+ .filter((selectedPath) => selectedPath !== tag)
3066
+ .every((selectedPath) => matchesTagPath(document, selectedPath));
3067
+ if (!available) {
3068
+ return;
3069
+ }
3070
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
3071
+ });
3072
+ }
3073
+ return Object.fromEntries(counts);
3074
+ }
3075
+ function buildAncestorPaths(tagPath) {
3076
+ const normalized = tagPath.trim();
3077
+ if (!normalized) {
3078
+ return [];
3079
+ }
3080
+ const segments = normalized.split("/");
3081
+ const paths = [];
3082
+ for (let index = 0; index < segments.length - 1; index += 1) {
3083
+ paths.push(segments.slice(0, index + 1).join("/"));
3084
+ }
3085
+ return paths;
3086
+ }
3087
+ function readAffairsLibraryStatsSafe(rootDir, relativePath) {
3088
+ const normalizedPath = relativePath.trim();
3089
+ const targetPath = !normalizedPath || normalizedPath === "."
3090
+ ? rootDir
3091
+ : path.resolve(rootDir, normalizedPath);
3092
+ try {
3093
+ if (!fs.existsSync(targetPath)) {
3094
+ return null;
3095
+ }
3096
+ return fs.statSync(targetPath);
3097
+ }
3098
+ catch {
3099
+ return null;
3100
+ }
3101
+ }
3102
+ function toIsoOrNull(value) {
3103
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
3104
+ return null;
3105
+ }
3106
+ return value.toISOString();
3107
+ }
3108
+ function resolveAffairsLibraryRelativePath(rootDir, absolutePath) {
3109
+ const relativePath = path.relative(path.resolve(rootDir), path.resolve(absolutePath)).replace(/\\/g, "/");
3110
+ if (!relativePath || relativePath === "." || relativePath.startsWith("../")) {
3111
+ return null;
3112
+ }
3113
+ return relativePath;
3114
+ }
3115
+ function normalizeMutationRefreshTarget(relativePath) {
3116
+ const normalizedPath = relativePath.trim().replace(/^\.\/+/, "").replace(/\/+$/, "");
3117
+ return normalizedPath || null;
3118
+ }
3119
+ function normalizeHintTargetPath(targetPath) {
3120
+ const normalized = targetPath?.trim().replace(/^\.\/+/, "").replace(/\/+$/, "") ?? "";
3121
+ return normalized || undefined;
3122
+ }
3123
+ function normalizeDirectoryPathFromTargetPath(targetPath) {
3124
+ const normalized = normalizeHintTargetPath(targetPath);
3125
+ if (!normalized) {
3126
+ return ".";
3127
+ }
3128
+ const parentPath = getParentFolderPath(normalized);
3129
+ return normalizeFolderPath(parentPath) || normalized;
3130
+ }
3131
+ function deriveDirectoryPathFromDocumentTarget(targetPath) {
3132
+ return normalizeDirectoryPathFromTargetPath(targetPath);
3133
+ }
3134
+ function buildHotDirectoryCacheKey(workspaceId, directoryPath) {
3135
+ return `${workspaceId}::${normalizeFolderPath(directoryPath) || "."}`;
3136
+ }
3137
+ function estimateFolderDocumentCount(normalizedFolderPath, exportData, cachedEntry) {
3138
+ if (cachedEntry?.items.length) {
3139
+ return cachedEntry.items.length;
3140
+ }
3141
+ const folderPath = normalizedFolderPath || ".";
3142
+ const folderNode = exportData?.folders.find((item) => normalizeFolderPath(item.path) === folderPath);
3143
+ if (folderNode) {
3144
+ return Math.max(0, folderNode.directDocumentCount);
3145
+ }
3146
+ if (!exportData) {
3147
+ return null;
3148
+ }
3149
+ if (!normalizedFolderPath) {
3150
+ return exportData.documents.length;
3151
+ }
3152
+ let count = 0;
3153
+ for (const document of exportData.documents) {
3154
+ if (!matchesDirectFolder(document.path, normalizedFolderPath)) {
3155
+ continue;
3156
+ }
3157
+ count += 1;
3158
+ if (count > LIVE_DIRECTORY_SYNC_SCAN_MAX_DOCUMENTS) {
3159
+ return count;
3160
+ }
3161
+ }
3162
+ return count;
3163
+ }
3164
+ function mapTaskHelperWorkerHealth(snapshot) {
3165
+ if (!snapshot) {
3166
+ return null;
3167
+ }
3168
+ return {
3169
+ ...snapshot
3170
+ };
3171
+ }
3172
+ function detectMissingIndexArtifact(rootDir) {
3173
+ const checks = [
3174
+ {
3175
+ relativePath: INDEX_DIR_RELATIVE_PATH,
3176
+ reason: AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingIndexArtifact,
3177
+ errorSummary: "文档库索引目录缺失,系统会自动补跑一次全量重建。"
3178
+ },
3179
+ {
3180
+ relativePath: EXPORT_DIR_RELATIVE_PATH,
3181
+ reason: AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingExportDir,
3182
+ errorSummary: "文档库导出目录缺失,系统会自动补跑一次全量重建。"
3183
+ },
3184
+ {
3185
+ relativePath: EXPORT_STATUS_RELATIVE_PATH,
3186
+ reason: AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingExportStatus,
3187
+ errorSummary: "文档库导出状态文件缺失,系统会自动补跑一次全量重建。"
3188
+ },
3189
+ {
3190
+ relativePath: EXPORT_MANIFEST_RELATIVE_PATH,
3191
+ reason: AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingExportManifest,
3192
+ errorSummary: "文档库导出清单缺失,系统会自动补跑一次全量重建。"
3193
+ }
3194
+ ];
3195
+ for (const check of checks) {
3196
+ if (!fs.existsSync(path.join(rootDir, check.relativePath))) {
3197
+ return {
3198
+ reason: check.reason,
3199
+ errorSummary: check.errorSummary
3200
+ };
3201
+ }
3202
+ }
3203
+ return null;
3204
+ }
3205
+ function shouldForceFullRebuild(reason) {
3206
+ const normalizedReason = reason.trim();
3207
+ return normalizedReason.includes(AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingIndexArtifact)
3208
+ || normalizedReason.includes(AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingExportDir)
3209
+ || normalizedReason.includes(AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingExportStatus)
3210
+ || normalizedReason.includes(AFFAIRS_LIBRARY_INDEX_DIRTY_REASONS.missingExportManifest);
3211
+ }
3212
+ function matchesFavorite(favorite, documentPath, directTags, derivedTags) {
3213
+ if (favorite.kind === "folder") {
3214
+ const normalizedPath = favorite.path === "." ? "" : favorite.path.replace(/\/+$/g, "");
3215
+ return !normalizedPath || documentPath === normalizedPath || documentPath.startsWith(`${normalizedPath}/`);
3216
+ }
3217
+ return [...directTags, ...derivedTags].some((tag) => tag === favorite.path || tag.startsWith(`${favorite.path}/`));
3218
+ }
3219
+ function buildFavoriteNodeId(kind, pathValue) {
3220
+ return `library:favorite:${kind}:${pathValue}`;
3221
+ }
3222
+ function matchesTagPath(document, tagPath) {
3223
+ const normalizedTagPath = tagPath.trim();
3224
+ if (!normalizedTagPath) {
3225
+ return true;
3226
+ }
3227
+ return [...document.tags, ...document.derivedTags].some((tag) => tag === normalizedTagPath || isTagTreeAncestor(normalizedTagPath, tag));
3228
+ }
3229
+ function isTagTreeAncestor(parentPath, childPath) {
3230
+ return childPath.startsWith(`${parentPath}/`);
3231
+ }
3232
+ function matchesDirectFolder(documentPath, folderPath) {
3233
+ return normalizeFolderPath(getParentFolderPath(documentPath)) === normalizeFolderPath(folderPath ?? null);
3234
+ }
3235
+ function getParentFolderPath(documentPath) {
3236
+ const normalized = documentPath.trim();
3237
+ const index = normalized.lastIndexOf("/");
3238
+ return index >= 0 ? normalized.slice(0, index) : null;
3239
+ }
3240
+ function normalizeFolderPath(value) {
3241
+ const normalized = value?.trim() ?? "";
3242
+ if (!normalized || normalized === ".") {
3243
+ return "";
3244
+ }
3245
+ return normalized.replace(/^\/+|\/+$/g, "");
3246
+ }
3247
+ function normalizePositiveInt(input, fallback, max) {
3248
+ if (!Number.isFinite(input)) {
3249
+ return fallback;
3250
+ }
3251
+ return Math.max(0, Math.min(Math.trunc(input), max));
3252
+ }
3253
+ function readAffairsLibraryExportDataSafe(rootDir) {
3254
+ try {
3255
+ return readAffairsLibraryExportDataFromDisk(rootDir);
3256
+ }
3257
+ catch {
3258
+ return null;
3259
+ }
3260
+ }
3261
+ function readAffairsLibraryExportDataFromDisk(rootDir) {
3262
+ const exportRoot = path.join(rootDir, EXPORT_DIR_RELATIVE_PATH);
3263
+ const manifestPath = path.join(exportRoot, "manifest.json");
3264
+ if (!fs.existsSync(manifestPath)) {
3265
+ return null;
3266
+ }
3267
+ const manifest = readJsonFile(manifestPath);
3268
+ const documents = (manifest.meta_shards ?? []).flatMap((shard) => {
3269
+ const shardPath = shard.path?.trim();
3270
+ if (!shardPath) {
3271
+ return [];
3272
+ }
3273
+ const payload = readJsonFile(path.join(exportRoot, shardPath));
3274
+ return (payload.documents ?? []).map((document) => ({
3275
+ documentId: document.document_id?.trim() || document.path?.trim() || "",
3276
+ path: document.path?.trim() || "",
3277
+ title: document.title?.trim() || document.path?.trim() || "未命名文档",
3278
+ summary: document.summary?.trim() || "",
3279
+ updatedAt: document.mtime?.trim() || "",
3280
+ createdAt: null,
3281
+ sizeBytes: null,
3282
+ tags: Array.isArray(document.direct_tags) ? document.direct_tags.filter(Boolean) : [],
3283
+ derivedTags: Array.isArray(document.derived_tags) ? document.derived_tags.filter(Boolean) : [],
3284
+ isFavorite: false
3285
+ }));
3286
+ });
3287
+ const taxonomyEntry = manifest.entries?.taxonomy?.trim() || "taxonomy.json";
3288
+ const taxonomy = readJsonFile(path.join(exportRoot, taxonomyEntry));
3289
+ const tags = (taxonomy.nodes ?? []).map((node) => ({
3290
+ path: node.path?.trim() ?? "",
3291
+ name: node.name?.trim() || node.path?.trim() || "未命名标签",
3292
+ rootType: node.root_type?.trim() || "unknown",
3293
+ parentPath: node.parent_path?.trim() || null,
3294
+ depth: Number.isFinite(node.depth) ? Number(node.depth) : 0,
3295
+ documentCount: countDocumentsForTag(documents, node.path?.trim() ?? "")
3296
+ })).filter((node) => node.path);
3297
+ const bootstrapEntry = manifest.entries?.bootstrap?.trim() || "bootstrap.json";
3298
+ const bootstrap = readJsonFile(path.join(exportRoot, bootstrapEntry));
3299
+ const folders = (bootstrap.folders ?? []).map((folder) => {
3300
+ const normalizedPath = folder.path?.trim() ?? ".";
3301
+ const folderStats = readAffairsLibraryStatsSafe(rootDir, normalizedPath);
3302
+ return {
3303
+ path: normalizedPath,
3304
+ name: folder.name?.trim() || "资料库",
3305
+ parentPath: folder.parent_path?.trim() || null,
3306
+ directDocumentCount: Number(folder.direct_document_count ?? 0),
3307
+ documentCount: Number(folder.document_count ?? 0),
3308
+ createdAt: toIsoOrNull(folderStats?.birthtime),
3309
+ updatedAt: toIsoOrNull(folderStats?.mtime)
3310
+ };
3311
+ });
3312
+ return {
3313
+ documents,
3314
+ tags,
3315
+ folders,
3316
+ generatedAt: manifest.generated_at?.trim() ?? null
3317
+ };
3318
+ }
3319
+ function normalizeFolderOpenBehavior(value) {
3320
+ return value === "single_click" ? "single_click" : "double_click";
3321
+ }
3322
+ function readAffairsLibraryConfigSafe(rootDir) {
3323
+ const configPath = path.join(rootDir, DEFAULT_CONFIG_RELATIVE_PATH);
3324
+ const payload = fs.existsSync(configPath)
3325
+ ? readJsonFile(configPath)
3326
+ : {};
3327
+ return {
3328
+ mirrorRoot: normalizeOptionalAbsolutePath(payload.mirrorRoot),
3329
+ allowedExtensions: normalizeAllowedExtensions(payload.allowedExtensions ?? []),
3330
+ includedHiddenPaths: normalizeIncludedHiddenPaths(payload.includedHiddenPaths ?? []),
3331
+ folderOpenBehavior: normalizeFolderOpenBehavior(payload.folderOpenBehavior)
3332
+ };
3333
+ }
3334
+ function buildAffairsFolderDocumentsFromFilesystem(rootDir, normalizedFolderPath, exportData, config, supportedExtensions = new Set(SUPPORTED_INDEX_EXTENSION_LIST)) {
3335
+ const targetDir = normalizedFolderPath
3336
+ ? path.resolve(rootDir, normalizedFolderPath)
3337
+ : rootDir;
3338
+ const configuredExtensions = new Set(config.allowedExtensions.map((item) => item.toLowerCase()));
3339
+ const documentMap = new Map();
3340
+ let hasSnapshotData = false;
3341
+ let hasLiveData = false;
3342
+ for (const document of exportData?.documents ?? []) {
3343
+ if (!matchesDirectFolder(document.path, normalizedFolderPath)) {
3344
+ continue;
3345
+ }
3346
+ const extension = path.extname(document.path).toLowerCase();
3347
+ if (!supportedExtensions.has(extension)) {
3348
+ continue;
3349
+ }
3350
+ if (configuredExtensions.size > 0 && !configuredExtensions.has(extension)) {
3351
+ continue;
3352
+ }
3353
+ const stat = readAffairsLibraryStatsSafe(rootDir, document.path);
3354
+ documentMap.set(document.path, {
3355
+ ...document,
3356
+ createdAt: document.createdAt ?? toIsoOrNull(stat?.birthtime),
3357
+ sizeBytes: document.sizeBytes ?? stat?.size ?? null,
3358
+ updatedAt: stat?.mtime.toISOString() ?? document.updatedAt,
3359
+ isFavorite: false
3360
+ });
3361
+ hasSnapshotData = true;
3362
+ }
3363
+ if (fs.existsSync(targetDir)) {
3364
+ let targetStats = null;
3365
+ try {
3366
+ targetStats = fs.statSync(targetDir);
3367
+ }
3368
+ catch {
3369
+ targetStats = null;
3370
+ }
3371
+ if (targetStats?.isDirectory()) {
3372
+ for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
3373
+ const relativePath = normalizedFolderPath ? `${normalizedFolderPath}/${entry.name}` : entry.name;
3374
+ if ((entry.name.startsWith(".") || hasHiddenPathSegment(relativePath))
3375
+ && !isIncludedHiddenPath(relativePath, config.includedHiddenPaths)) {
3376
+ continue;
3377
+ }
3378
+ if (!entry.isFile()) {
3379
+ continue;
3380
+ }
3381
+ const extension = path.extname(entry.name).toLowerCase();
3382
+ if (!supportedExtensions.has(extension)) {
3383
+ continue;
3384
+ }
3385
+ if (configuredExtensions.size > 0 && !configuredExtensions.has(extension)) {
3386
+ continue;
3387
+ }
3388
+ const stat = readAffairsLibraryStatsSafe(rootDir, relativePath);
3389
+ const exported = documentMap.get(relativePath);
3390
+ documentMap.set(relativePath, {
3391
+ documentId: exported?.documentId ?? relativePath,
3392
+ path: relativePath,
3393
+ title: exported?.title?.trim() || path.basename(entry.name, extension) || entry.name,
3394
+ summary: exported?.summary ?? "",
3395
+ updatedAt: stat?.mtime.toISOString() ?? exported?.updatedAt ?? "",
3396
+ createdAt: toIsoOrNull(stat?.birthtime) ?? exported?.createdAt ?? null,
3397
+ sizeBytes: stat?.size ?? exported?.sizeBytes ?? null,
3398
+ tags: exported?.tags ?? [],
3399
+ derivedTags: exported?.derivedTags ?? [],
3400
+ isFavorite: false
3401
+ });
3402
+ hasLiveData = true;
3403
+ }
3404
+ }
3405
+ }
3406
+ return {
3407
+ items: [...documentMap.values()],
3408
+ source: hasLiveData && hasSnapshotData
3409
+ ? "mixed"
3410
+ : hasLiveData
3411
+ ? "live"
3412
+ : "snapshot",
3413
+ generatedAt: exportData?.generatedAt ?? null,
3414
+ filesystemObservedAt: hasLiveData ? nowIso() : null,
3415
+ staleReason: null
3416
+ };
3417
+ }
3418
+ export async function runAffairsLibraryDirectoryHintInHelper(input) {
3419
+ if (input.signal?.aborted) {
3420
+ throw input.signal.reason ?? new Error("helper task aborted");
3421
+ }
3422
+ const exportData = readAffairsLibraryExportDataSafe(input.rootDir);
3423
+ const result = buildAffairsFolderDocumentsFromFilesystem(input.rootDir, normalizeFolderPath(input.directoryPath), exportData, readAffairsLibraryConfigSafe(input.rootDir));
3424
+ return {
3425
+ directoryPath: input.directoryPath,
3426
+ refreshedAt: nowIso(),
3427
+ source: result.source,
3428
+ itemCount: result.items.length,
3429
+ changedPaths: result.items.map((item) => item.path).sort((left, right) => left.localeCompare(right, "zh-CN")),
3430
+ items: result.items,
3431
+ generatedAt: result.generatedAt,
3432
+ filesystemObservedAt: result.filesystemObservedAt
3433
+ };
3434
+ }
3435
+ function isSameOrDescendantRelativePath(targetPath, candidatePath) {
3436
+ return candidatePath === targetPath || candidatePath.startsWith(`${targetPath}/`);
3437
+ }
3438
+ function readIndexStatusFileSafe(rootDir) {
3439
+ const filePath = path.join(rootDir, EXPORT_STATUS_RELATIVE_PATH);
3440
+ if (!fs.existsSync(filePath)) {
3441
+ return null;
3442
+ }
3443
+ try {
3444
+ const payload = readJsonFile(filePath);
3445
+ const exportedAt = payload.exported_at?.trim() ?? null;
3446
+ const exportedAtMs = exportedAt ? Date.parse(exportedAt) : Number.NaN;
3447
+ const documentCount = typeof payload.document_count === "number"
3448
+ ? payload.document_count
3449
+ : Number.isFinite(Number(payload.document_count))
3450
+ ? Number(payload.document_count)
3451
+ : null;
3452
+ return {
3453
+ exportedAt,
3454
+ exportedAtMs,
3455
+ documentCount
3456
+ };
3457
+ }
3458
+ catch {
3459
+ return null;
3460
+ }
3461
+ }
3462
+ function readRuntimeStatusFileSafe(rootDir) {
3463
+ const filePath = path.join(rootDir, RUNTIME_STATUS_RELATIVE_PATH);
3464
+ if (!fs.existsSync(filePath)) {
3465
+ return null;
3466
+ }
3467
+ try {
3468
+ const payload = readJsonFile(filePath);
3469
+ const updatedAt = payload.updatedAt?.trim() ?? null;
3470
+ const updatedAtMs = updatedAt ? Date.parse(updatedAt) : Number.NaN;
3471
+ const rawProgress = payload.progress;
3472
+ const progress = rawProgress && typeof rawProgress === "object"
3473
+ ? {
3474
+ scannedCount: Number(rawProgress.scannedCount ?? 0),
3475
+ indexedCount: Number(rawProgress.indexedCount ?? 0),
3476
+ skippedCount: Number(rawProgress.skippedCount ?? 0),
3477
+ failedCount: Number(rawProgress.failedCount ?? 0),
3478
+ unchangedCount: Number(rawProgress.unchangedCount ?? 0),
3479
+ totalCount: rawProgress.totalCount === null || rawProgress.totalCount === undefined
3480
+ ? null
3481
+ : Number(rawProgress.totalCount),
3482
+ maxConcurrency: rawProgress.maxConcurrency === null || rawProgress.maxConcurrency === undefined
3483
+ ? null
3484
+ : Number(rawProgress.maxConcurrency),
3485
+ }
3486
+ : null;
3487
+ return {
3488
+ status: payload.status?.trim() ?? null,
3489
+ stage: payload.stage?.trim() ?? null,
3490
+ command: payload.command?.trim() ?? null,
3491
+ taskId: payload.taskId?.trim() ?? null,
3492
+ taskType: payload.taskType?.trim() ?? null,
3493
+ updatedAt,
3494
+ updatedAtMs,
3495
+ errorSummary: payload.errorSummary?.trim() ?? null,
3496
+ progress: progress && Number.isFinite(progress.scannedCount)
3497
+ && Number.isFinite(progress.indexedCount)
3498
+ && Number.isFinite(progress.skippedCount)
3499
+ && Number.isFinite(progress.failedCount)
3500
+ && Number.isFinite(progress.unchangedCount)
3501
+ ? progress
3502
+ : null,
3503
+ };
3504
+ }
3505
+ catch {
3506
+ return null;
3507
+ }
3508
+ }
3509
+ function readCommandLockOwnerFileSafe(rootDir) {
3510
+ const filePath = path.join(rootDir, COMMAND_LOCK_OWNER_RELATIVE_PATH);
3511
+ if (!fs.existsSync(filePath)) {
3512
+ return null;
3513
+ }
3514
+ try {
3515
+ const payload = readJsonFile(filePath);
3516
+ return {
3517
+ pid: typeof payload.pid === "number" && Number.isFinite(payload.pid) ? payload.pid : null,
3518
+ command: payload.command?.trim() ?? null,
3519
+ taskId: payload.taskId?.trim() ?? null,
3520
+ taskType: payload.taskType?.trim() ?? null,
3521
+ acquiredAt: payload.acquiredAt?.trim() ?? null
3522
+ };
3523
+ }
3524
+ catch {
3525
+ return null;
3526
+ }
3527
+ }
3528
+ function readCommandLockHeartbeatFileSafe(rootDir) {
3529
+ const filePath = path.join(rootDir, COMMAND_LOCK_HEARTBEAT_RELATIVE_PATH);
3530
+ if (!fs.existsSync(filePath)) {
3531
+ return null;
3532
+ }
3533
+ try {
3534
+ const payload = readJsonFile(filePath);
3535
+ const ts = payload.ts?.trim() ?? null;
3536
+ const tsMs = ts ? Date.parse(ts) : Number.NaN;
3537
+ return {
3538
+ ts,
3539
+ tsMs
3540
+ };
3541
+ }
3542
+ catch {
3543
+ return null;
3544
+ }
3545
+ }
3546
+ function detectOrphanedRunningTask(rootDir, taskSnapshot, runtimeStatus) {
3547
+ if (taskSnapshot.status !== "running") {
3548
+ return null;
3549
+ }
3550
+ const runningStartedAtMs = taskSnapshot.startedAt ?? taskSnapshot.enqueuedAt ?? null;
3551
+ if (Number.isFinite(runningStartedAtMs ?? Number.NaN)
3552
+ && Date.now() - (runningStartedAtMs ?? 0) < ORPHAN_TASK_RECONCILE_GRACE_MS) {
3553
+ return null;
3554
+ }
3555
+ const lockDir = path.join(rootDir, COMMAND_LOCK_DIR_RELATIVE_PATH);
3556
+ if (!fs.existsSync(lockDir)) {
3557
+ return {
3558
+ reason: "command_lock_missing",
3559
+ errorSummary: "文档库索引任务已失去 helper 锁文件,上一轮运行可能异常退出。",
3560
+ ownerPid: null,
3561
+ heartbeatAgeMs: null,
3562
+ runtimeUpdatedAt: runtimeStatus?.updatedAt ?? null,
3563
+ runtimeAgeMs: Number.isFinite(runtimeStatus?.updatedAtMs ?? Number.NaN)
3564
+ ? Date.now() - (runtimeStatus?.updatedAtMs ?? 0)
3565
+ : null,
3566
+ runningStage: runtimeStatus?.stage ?? null
3567
+ };
3568
+ }
3569
+ const owner = readCommandLockOwnerFileSafe(rootDir);
3570
+ if (!owner) {
3571
+ return {
3572
+ reason: "command_lock_missing",
3573
+ errorSummary: "文档库索引任务缺少 helper 锁 owner 信息,上一轮运行可能异常退出。",
3574
+ ownerPid: null,
3575
+ heartbeatAgeMs: null,
3576
+ runtimeUpdatedAt: runtimeStatus?.updatedAt ?? null,
3577
+ runtimeAgeMs: Number.isFinite(runtimeStatus?.updatedAtMs ?? Number.NaN)
3578
+ ? Date.now() - (runtimeStatus?.updatedAtMs ?? 0)
3579
+ : null,
3580
+ runningStage: runtimeStatus?.stage ?? null
3581
+ };
3582
+ }
3583
+ if (owner.taskId && owner.taskId !== taskSnapshot.taskId) {
3584
+ return null;
3585
+ }
3586
+ if (owner.taskType && owner.taskType !== taskSnapshot.taskType) {
3587
+ return null;
3588
+ }
3589
+ if (owner.pid === null || !isProcessAliveSafe(owner.pid)) {
3590
+ return {
3591
+ reason: "command_lock_owner_dead",
3592
+ errorSummary: `文档库索引任务的 helper 进程(pid=${owner.pid ?? "unknown"})已经退出,但 Host 没收到结束回调。`,
3593
+ ownerPid: owner.pid,
3594
+ heartbeatAgeMs: null,
3595
+ runtimeUpdatedAt: runtimeStatus?.updatedAt ?? null,
3596
+ runtimeAgeMs: Number.isFinite(runtimeStatus?.updatedAtMs ?? Number.NaN)
3597
+ ? Date.now() - (runtimeStatus?.updatedAtMs ?? 0)
3598
+ : null,
3599
+ runningStage: runtimeStatus?.stage ?? null
3600
+ };
3601
+ }
3602
+ const heartbeat = readCommandLockHeartbeatFileSafe(rootDir);
3603
+ const heartbeatAgeMs = Number.isFinite(heartbeat?.tsMs ?? Number.NaN)
3604
+ ? Date.now() - (heartbeat?.tsMs ?? 0)
3605
+ : Number.POSITIVE_INFINITY;
3606
+ if (heartbeatAgeMs > COMMAND_LOCK_STALE_HEARTBEAT_MS) {
3607
+ return {
3608
+ reason: "command_lock_heartbeat_stale",
3609
+ errorSummary: "文档库索引任务的 helper 心跳已长时间不刷新,上一轮运行很可能已经卡死。",
3610
+ ownerPid: owner.pid,
3611
+ heartbeatAgeMs: Number.isFinite(heartbeatAgeMs) ? heartbeatAgeMs : null,
3612
+ runtimeUpdatedAt: runtimeStatus?.updatedAt ?? null,
3613
+ runtimeAgeMs: Number.isFinite(runtimeStatus?.updatedAtMs ?? Number.NaN)
3614
+ ? Date.now() - (runtimeStatus?.updatedAtMs ?? 0)
3615
+ : null,
3616
+ runningStage: runtimeStatus?.stage ?? null
3617
+ };
3618
+ }
3619
+ if (runtimeStatus?.status === "running"
3620
+ && runtimeStatus.taskId === taskSnapshot.taskId
3621
+ && Number.isFinite(runtimeStatus.updatedAtMs)) {
3622
+ const runtimeAgeMs = Date.now() - runtimeStatus.updatedAtMs;
3623
+ if (runtimeAgeMs > COMMAND_LOCK_STALE_HEARTBEAT_MS && heartbeatAgeMs > COMMAND_LOCK_STALE_HEARTBEAT_MS) {
3624
+ return {
3625
+ reason: "command_lock_heartbeat_stale",
3626
+ errorSummary: "文档库索引任务的运行状态和 helper 心跳都已长时间停止刷新,上一轮运行很可能已经卡死。",
3627
+ ownerPid: owner.pid,
3628
+ heartbeatAgeMs,
3629
+ runtimeUpdatedAt: runtimeStatus.updatedAt,
3630
+ runtimeAgeMs,
3631
+ runningStage: runtimeStatus.stage
3632
+ };
3633
+ }
3634
+ }
3635
+ return null;
3636
+ }
3637
+ function buildCompletedStatusFromExport(exportStatus, enqueuedAtMs, startedAtMs) {
3638
+ if (!exportStatus?.exportedAt || !Number.isFinite(exportStatus.exportedAtMs)) {
3639
+ return null;
3640
+ }
3641
+ const nextAllowedAtMs = exportStatus.exportedAtMs + INDEX_TASK_COOLDOWN_MS;
3642
+ const now = Date.now();
3643
+ const lastRequestedAtMs = Number.isFinite(enqueuedAtMs ?? Number.NaN)
3644
+ ? Math.max(exportStatus.exportedAtMs, enqueuedAtMs ?? Number.NaN)
3645
+ : exportStatus.exportedAtMs;
3646
+ const lastStartedAtMs = Number.isFinite(startedAtMs ?? Number.NaN)
3647
+ ? Math.max(exportStatus.exportedAtMs, startedAtMs ?? Number.NaN)
3648
+ : exportStatus.exportedAtMs;
3649
+ return {
3650
+ state: now < nextAllowedAtMs ? "cooldown" : "fresh",
3651
+ dirtyReasons: [],
3652
+ lastRequestedAt: toIso(lastRequestedAtMs),
3653
+ lastStartedAt: toIso(lastStartedAtMs),
3654
+ lastCompletedAt: exportStatus.exportedAt,
3655
+ lastFailedAt: null,
3656
+ nextAllowedAt: toIso(nextAllowedAtMs),
3657
+ runningTaskId: null,
3658
+ runningStage: null,
3659
+ errorSummary: null
3660
+ };
3661
+ }
3662
+ function hasExportCaughtUp(exportStatus, referenceTimestampMs) {
3663
+ if (!exportStatus?.exportedAt || !Number.isFinite(exportStatus.exportedAtMs)) {
3664
+ return false;
3665
+ }
3666
+ if (!referenceTimestampMs || !Number.isFinite(referenceTimestampMs)) {
3667
+ return true;
3668
+ }
3669
+ return exportStatus.exportedAtMs >= referenceTimestampMs;
3670
+ }
3671
+ function resolveAffairsLibraryRunningStage(workspaceId, taskSnapshot, runtimeStatus) {
3672
+ void workspaceId;
3673
+ if (taskSnapshot.status === "queued") {
3674
+ return "queued";
3675
+ }
3676
+ if (runtimeStatus?.status === "running"
3677
+ && runtimeStatus.stage
3678
+ && doesRuntimeStatusMatchTask(taskSnapshot, runtimeStatus)) {
3679
+ return runtimeStatus.stage;
3680
+ }
3681
+ switch (taskSnapshot.taskType) {
3682
+ case HOST_TASK_TYPES.affairsLibraryApplyConfig:
3683
+ return "apply_config";
3684
+ case HOST_TASK_TYPES.affairsLibraryExport:
3685
+ return "export";
3686
+ case HOST_TASK_TYPES.affairsLibraryIndex:
3687
+ return "index";
3688
+ default:
3689
+ return null;
3690
+ }
3691
+ }
3692
+ function doesRuntimeStatusMatchTask(taskSnapshot, runtimeStatus) {
3693
+ if (runtimeStatus.taskId && runtimeStatus.taskId === taskSnapshot.taskId) {
3694
+ return true;
3695
+ }
3696
+ if (runtimeStatus.taskType && runtimeStatus.taskType !== taskSnapshot.taskType) {
3697
+ return false;
3698
+ }
3699
+ const referenceMs = taskSnapshot.startedAt ?? taskSnapshot.enqueuedAt ?? Number.NaN;
3700
+ if (!Number.isFinite(referenceMs)) {
3701
+ return true;
3702
+ }
3703
+ return Number.isFinite(runtimeStatus.updatedAtMs) && runtimeStatus.updatedAtMs >= referenceMs;
3704
+ }
3705
+ function isProcessAliveSafe(pid) {
3706
+ if (!Number.isInteger(pid) || pid <= 0) {
3707
+ return false;
3708
+ }
3709
+ try {
3710
+ process.kill(pid, 0);
3711
+ return true;
3712
+ }
3713
+ catch (error) {
3714
+ return typeof error === "object"
3715
+ && error !== null
3716
+ && "code" in error
3717
+ && error.code === "EPERM";
3718
+ }
3719
+ }
3720
+ function readJsonFile(filePath) {
3721
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
3722
+ }
3723
+ function normalizeOptionalAbsolutePath(input) {
3724
+ const value = input?.trim() ?? "";
3725
+ if (!value) {
3726
+ return null;
3727
+ }
3728
+ return path.isAbsolute(value) ? value : null;
3729
+ }
3730
+ function normalizeAllowedExtensions(input) {
3731
+ const result = new Set();
3732
+ for (const item of input) {
3733
+ const trimmed = String(item ?? "").trim().toLowerCase();
3734
+ if (!trimmed) {
3735
+ continue;
3736
+ }
3737
+ const normalized = trimmed.startsWith(".") ? trimmed : `.${trimmed}`;
3738
+ result.add(normalized);
3739
+ }
3740
+ return [...result].sort((left, right) => left.localeCompare(right, "zh-Hans-CN"));
3741
+ }
3742
+ function toIso(timestamp) {
3743
+ if (!timestamp || !Number.isFinite(timestamp)) {
3744
+ return null;
3745
+ }
3746
+ return new Date(timestamp).toISOString();
3747
+ }
3748
+ function hasHiddenPathSegment(relativePath) {
3749
+ return relativePath
3750
+ .split("/")
3751
+ .map((segment) => segment.trim())
3752
+ .filter(Boolean)
3753
+ .some((segment) => segment.startsWith("."));
3754
+ }
3755
+ //# sourceMappingURL=affairs-library-service.js.map