@jingyi0605/codingns 0.8.5 → 0.9.5

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