evolcore 0.0.1 → 0.0.3

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 (379) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION-0.5.0.md +378 -0
  4. package/README.md +318 -14
  5. package/ROLE_ACCESS_CONTROL.md +174 -0
  6. package/assets/.env.template +4 -0
  7. package/bin/ec-safe-output.js +161 -0
  8. package/bin/ec.js +29 -0
  9. package/dist/agents/baseagent.js +163 -0
  10. package/dist/agents/claude-runner.js +2565 -0
  11. package/dist/agents/codex-app-server-client.js +448 -0
  12. package/dist/agents/codex-runner.js +2682 -0
  13. package/dist/agents/gemini-runner.js +666 -0
  14. package/dist/agents/runner-types.js +75 -0
  15. package/dist/aun/aid/agentmd.js +216 -0
  16. package/dist/aun/aid/client.js +132 -0
  17. package/dist/aun/aid/control-aid.js +91 -0
  18. package/dist/aun/aid/identity.js +518 -0
  19. package/dist/aun/aid/index.js +4 -0
  20. package/dist/aun/aid/store.js +74 -0
  21. package/dist/aun/aid/types.js +1 -0
  22. package/dist/aun/aid/validation.js +21 -0
  23. package/dist/aun/group-identity.js +10 -0
  24. package/dist/aun/msg/group-index.js +6 -0
  25. package/dist/aun/msg/group.js +1231 -0
  26. package/dist/aun/msg/history.js +123 -0
  27. package/dist/aun/msg/index.js +5 -0
  28. package/dist/aun/msg/p2p.js +393 -0
  29. package/dist/aun/msg/payload-type.js +27 -0
  30. package/dist/aun/msg/upload.js +137 -0
  31. package/dist/aun/outbox.js +168 -0
  32. package/dist/aun/rpc/caller.js +42 -0
  33. package/dist/aun/rpc/connection.js +25 -0
  34. package/dist/aun/rpc/index.js +2 -0
  35. package/dist/aun/service-proxy.js +225 -0
  36. package/dist/aun/storage/download.js +29 -0
  37. package/dist/aun/storage/index.js +3 -0
  38. package/dist/aun/storage/manage.js +10 -0
  39. package/dist/aun/storage/upload.js +68 -0
  40. package/dist/channels/aun.js +4164 -0
  41. package/dist/channels/contact-bind-code.js +134 -0
  42. package/dist/channels/daemon.js +422 -0
  43. package/dist/channels/dingtalk.js +1479 -0
  44. package/dist/channels/feishu.js +1865 -0
  45. package/dist/channels/qqbot.js +409 -0
  46. package/dist/channels/wechat.js +817 -0
  47. package/dist/channels/wecom-card.js +101 -0
  48. package/dist/channels/wecom-onboarding.js +82 -0
  49. package/dist/channels/wecom-state.js +191 -0
  50. package/dist/channels/wecom.js +1157 -0
  51. package/dist/cli/agent-command.js +642 -0
  52. package/dist/cli/agent.js +1059 -0
  53. package/dist/cli/aun-commands.js +2003 -0
  54. package/dist/cli/bench.js +1228 -0
  55. package/dist/cli/cli-argv.js +66 -0
  56. package/dist/cli/code-stats.js +329 -0
  57. package/dist/cli/command-log.js +82 -0
  58. package/dist/cli/config-selector.js +69 -0
  59. package/dist/cli/config.js +261 -0
  60. package/dist/cli/contact.js +71 -0
  61. package/dist/cli/ctl-command.js +62 -0
  62. package/dist/cli/daemon-commands.js +2750 -0
  63. package/dist/cli/fs-command.js +1447 -0
  64. package/dist/cli/handoff-command.js +302 -0
  65. package/dist/cli/help.js +35 -0
  66. package/dist/cli/index.js +374 -0
  67. package/dist/cli/init-channel.js +1372 -0
  68. package/dist/cli/init.js +590 -0
  69. package/dist/cli/link-rules.js +240 -0
  70. package/dist/cli/model.js +591 -0
  71. package/dist/cli/net-check.js +723 -0
  72. package/dist/cli/queue-command.js +150 -0
  73. package/dist/cli/raw-key-input.js +25 -0
  74. package/dist/cli/response.js +344 -0
  75. package/dist/cli/restart-monitor.js +480 -0
  76. package/dist/cli/stats.js +609 -0
  77. package/dist/cli/task-context.js +80 -0
  78. package/dist/cli/trigger-command.js +545 -0
  79. package/dist/cli/version.js +93 -0
  80. package/dist/cli/watch-logs.js +33 -0
  81. package/dist/cli/watch-msg.js +673 -0
  82. package/dist/config/boot-log.js +266 -0
  83. package/dist/config/builtin-role-templates.js +42 -0
  84. package/dist/config/builtin-roles.js +91 -0
  85. package/dist/config/config-batch-get.js +11 -0
  86. package/dist/config/config-field-policy.js +261 -0
  87. package/dist/config/config-manager.js +1120 -0
  88. package/dist/config/config-operation-service.js +384 -0
  89. package/dist/config/contact-alias.js +68 -0
  90. package/dist/config/contact-book-store.js +454 -0
  91. package/dist/config/contact-book-v2-startup.js +35 -0
  92. package/dist/config/contact-book.js +224 -0
  93. package/dist/config/contact-operation-service.js +110 -0
  94. package/dist/config/gateway-config.js +858 -0
  95. package/dist/config/lifecycle.js +17 -0
  96. package/dist/config/mention-mode.js +27 -0
  97. package/dist/config/merge.js +161 -0
  98. package/dist/config/owner-policy.js +4 -0
  99. package/dist/config/peer-role-resolver.js +218 -0
  100. package/dist/config/resolved-config-op.js +483 -0
  101. package/dist/config/role-config-v4-startup.js +32 -0
  102. package/dist/config/role-config-v5-startup.js +27 -0
  103. package/dist/config/role-ranks.js +18 -0
  104. package/dist/config/role-schema.js +105 -0
  105. package/dist/config/role-service.js +156 -0
  106. package/dist/config/role-store.js +215 -0
  107. package/dist/config/roles.js +64 -0
  108. package/dist/config/schema-registry.js +154 -0
  109. package/dist/config/snapshot.js +598 -0
  110. package/dist/config-store.js +501 -0
  111. package/dist/core/auth/agent-delegation.js +111 -0
  112. package/dist/core/auth/auth-gateway.js +166 -0
  113. package/dist/core/auth/authenticated-actor.js +6 -0
  114. package/dist/core/auth/authorization-audit.js +119 -0
  115. package/dist/core/auth/operation-authorizer.js +720 -0
  116. package/dist/core/auth/operation-catalog.js +731 -0
  117. package/dist/core/baseagent-loader.js +54 -0
  118. package/dist/core/bootstrap-service.js +175 -0
  119. package/dist/core/capability/capability-manager.js +316 -0
  120. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  121. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  122. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  123. package/dist/core/capability/types.js +27 -0
  124. package/dist/core/causation/audit.js +103 -0
  125. package/dist/core/causation/aun-association.js +111 -0
  126. package/dist/core/causation/context.js +93 -0
  127. package/dist/core/causation/index.js +4 -0
  128. package/dist/core/causation/types.js +2 -0
  129. package/dist/core/channel-loader.js +277 -0
  130. package/dist/core/command/agent-control.js +616 -0
  131. package/dist/core/command/cli-intent-parser.js +225 -0
  132. package/dist/core/command/command-handler.js +1733 -0
  133. package/dist/core/command/connect-menu.js +374 -0
  134. package/dist/core/command/menu-handler.js +3452 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1623 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3066 -0
  139. package/dist/core/daemon-file-cache.js +222 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +810 -0
  142. package/dist/core/evolagent-registry.js +545 -0
  143. package/dist/core/evolagent.js +342 -0
  144. package/dist/core/handoff/dispatcher.js +229 -0
  145. package/dist/core/handoff/mutex.js +46 -0
  146. package/dist/core/handoff/runtime.js +324 -0
  147. package/dist/core/handoff/store.js +537 -0
  148. package/dist/core/handoff/types.js +12 -0
  149. package/dist/core/inference/text-inference.js +173 -0
  150. package/dist/core/interaction-registration.js +10 -0
  151. package/dist/core/interaction-router.js +278 -0
  152. package/dist/core/message/create-status.js +67 -0
  153. package/dist/core/message/im-renderer.js +659 -0
  154. package/dist/core/message/items-formatter.js +76 -0
  155. package/dist/core/message/logical-queue-bridge.js +123 -0
  156. package/dist/core/message/message-bridge.js +874 -0
  157. package/dist/core/message/message-cache.js +56 -0
  158. package/dist/core/message/message-log.js +339 -0
  159. package/dist/core/message/message-processor.js +4 -0
  160. package/dist/core/message/message-queue.js +1446 -0
  161. package/dist/core/message/message-utils.js +76 -0
  162. package/dist/core/message/peer-mode.js +105 -0
  163. package/dist/core/message/pending-hints.js +232 -0
  164. package/dist/core/message/response-depth.js +33 -0
  165. package/dist/core/message/response-engine.js +4020 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/send-receipt.js +24 -0
  168. package/dist/core/message/stream-debouncer.js +139 -0
  169. package/dist/core/message/stream-idle-monitor.js +124 -0
  170. package/dist/core/model/config-scope.js +162 -0
  171. package/dist/core/model/field-scope.js +78 -0
  172. package/dist/core/model/model-catalog.js +227 -0
  173. package/dist/core/model/model-diagnostics.js +182 -0
  174. package/dist/core/model/model-permission.js +90 -0
  175. package/dist/core/permission/approval-gateway.js +1017 -0
  176. package/dist/core/permission/ec-command-parser.js +347 -0
  177. package/dist/core/permission/execution-sandbox.js +16 -0
  178. package/dist/core/permission/index.js +6 -0
  179. package/dist/core/permission/mode.js +24 -0
  180. package/dist/core/permission/sandbox-runtime.js +265 -0
  181. package/dist/core/permission/tool-policy.js +1019 -0
  182. package/dist/core/permission/unix-socket-policy.js +99 -0
  183. package/dist/core/protected-paths.js +332 -0
  184. package/dist/core/relation/peer-identity.js +222 -0
  185. package/dist/core/relation/peer-key.js +1 -0
  186. package/dist/core/role/runtime-policy.js +141 -0
  187. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  188. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  189. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  190. package/dist/core/session/session-file-adapter.js +7 -0
  191. package/dist/core/session/session-file-health.js +45 -0
  192. package/dist/core/session/session-fs-store.js +273 -0
  193. package/dist/core/session/session-key.js +24 -0
  194. package/dist/core/session/session-manager.js +1643 -0
  195. package/dist/core/session/session-mapper.js +100 -0
  196. package/dist/core/session/session-renew.js +314 -0
  197. package/dist/core/session/session-title.js +128 -0
  198. package/dist/core/session/session-turn-coordinator.js +205 -0
  199. package/dist/core/session/session-turns.js +67 -0
  200. package/dist/core/system-channels.js +29 -0
  201. package/dist/eck/baseagent-caps.js +18 -0
  202. package/dist/eck/detect.js +47 -0
  203. package/dist/eck/group-rules-sync.js +345 -0
  204. package/dist/eck/init.js +77 -0
  205. package/dist/eck/kit-renderer.js +359 -0
  206. package/dist/eck/manifest-engine.js +446 -0
  207. package/dist/eck/message-renderer.js +199 -0
  208. package/dist/eck/rules-loader.js +28 -0
  209. package/dist/index.js +2926 -4
  210. package/dist/ipc.js +777 -0
  211. package/dist/paths.js +262 -0
  212. package/dist/product.js +18 -0
  213. package/dist/response-system/context-builder.js +71 -0
  214. package/dist/response-system/coordinator.js +117 -0
  215. package/dist/response-system/decision-executor.js +86 -0
  216. package/dist/response-system/engines/v1/index.js +21 -0
  217. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  218. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  219. package/dist/response-system/engines/v1/types.js +1 -0
  220. package/dist/response-system/extensions.js +41 -0
  221. package/dist/response-system/index.js +6 -0
  222. package/dist/response-system/modes/index.js +7 -0
  223. package/dist/response-system/modes/single-session/index.js +72 -0
  224. package/dist/response-system/queues/fifo-queue.js +44 -0
  225. package/dist/response-system/queues/index.js +6 -0
  226. package/dist/response-system/queues/lifo-queue.js +42 -0
  227. package/dist/response-system/queues/priority-queue.js +63 -0
  228. package/dist/response-system/registry.js +97 -0
  229. package/dist/response-system/resolver.js +37 -0
  230. package/dist/response-system/selector.js +23 -0
  231. package/dist/response-system/types.js +7 -0
  232. package/dist/stats/billing.js +163 -0
  233. package/dist/stats/budget.js +93 -0
  234. package/dist/stats/db.js +403 -0
  235. package/dist/stats/eck-vars.js +89 -0
  236. package/dist/stats/index.js +11 -0
  237. package/dist/stats/normalizer.js +80 -0
  238. package/dist/stats/price-resolver.js +138 -0
  239. package/dist/stats/query.js +763 -0
  240. package/dist/stats/role-budget.js +168 -0
  241. package/dist/stats/writer.js +151 -0
  242. package/dist/trigger/anomaly-store.js +258 -0
  243. package/dist/trigger/audit.js +152 -0
  244. package/dist/trigger/event-source.js +119 -0
  245. package/dist/trigger/feedback.js +685 -0
  246. package/dist/trigger/history.js +290 -0
  247. package/dist/trigger/manager.js +294 -0
  248. package/dist/trigger/parser.js +595 -0
  249. package/dist/trigger/patch.js +155 -0
  250. package/dist/trigger/scheduler.js +1602 -0
  251. package/dist/trigger/script-executor.js +155 -0
  252. package/dist/trigger/state.js +145 -0
  253. package/dist/trigger/types.js +1 -0
  254. package/dist/trigger/validation.js +634 -0
  255. package/dist/types.js +12 -0
  256. package/dist/utils/aid-bind.js +313 -0
  257. package/dist/utils/atomic-write.js +95 -0
  258. package/dist/utils/avatar-upload.js +123 -0
  259. package/dist/utils/cross-platform.js +297 -0
  260. package/dist/utils/ecweb-utils.js +73 -0
  261. package/dist/utils/error-dict.json +153 -0
  262. package/dist/utils/error-utils.js +349 -0
  263. package/dist/utils/instance-registry.js +444 -0
  264. package/dist/utils/locale.js +21 -0
  265. package/dist/utils/log-writer.js +270 -0
  266. package/dist/utils/logger.js +89 -0
  267. package/dist/utils/markdown-to-plain-text.js +20 -0
  268. package/dist/utils/media-cache.js +274 -0
  269. package/dist/utils/model-prices.jsonl +20 -0
  270. package/dist/utils/npm-ops.js +210 -0
  271. package/dist/utils/process-introspect.js +133 -0
  272. package/dist/utils/process-tree-stats.js +271 -0
  273. package/dist/utils/project-path.js +74 -0
  274. package/dist/utils/restart-safety.js +31 -0
  275. package/dist/utils/stats.js +410 -0
  276. package/dist/utils/system-memory.js +62 -0
  277. package/dist/utils/tool-summary.js +284 -0
  278. package/dist/utils/welcome.js +268 -0
  279. package/kits/docs/GUIDE.md +20 -0
  280. package/kits/docs/INDEX.md +66 -0
  281. package/kits/docs/aun/CHEATSHEET.md +19 -0
  282. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  283. package/kits/docs/channels/aun.md +65 -0
  284. package/kits/docs/channels/feishu.md +56 -0
  285. package/kits/docs/context-assembly.md +366 -0
  286. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  287. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  288. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  289. package/kits/docs/eck_templates/runtime.template.md +19 -0
  290. package/kits/docs/evolcore/INDEX.md +68 -0
  291. package/kits/docs/evolcore/agent.md +77 -0
  292. package/kits/docs/evolcore/aid.md +52 -0
  293. package/kits/docs/evolcore/config.md +149 -0
  294. package/kits/docs/evolcore/contact.md +57 -0
  295. package/kits/docs/evolcore/ctl.md +46 -0
  296. package/kits/docs/evolcore/event.md +216 -0
  297. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  298. package/kits/docs/evolcore/fs.md +110 -0
  299. package/kits/docs/evolcore/group-fs.md +17 -0
  300. package/kits/docs/evolcore/group-rules.md +226 -0
  301. package/kits/docs/evolcore/group.md +150 -0
  302. package/kits/docs/evolcore/model.md +50 -0
  303. package/kits/docs/evolcore/msg.md +136 -0
  304. package/kits/docs/evolcore/response.md +75 -0
  305. package/kits/docs/evolcore/rpc.md +37 -0
  306. package/kits/docs/evolcore/self-summary.md +29 -0
  307. package/kits/docs/evolcore/stats.md +83 -0
  308. package/kits/docs/evolcore/storage.md +50 -0
  309. package/kits/docs/evolcore/trigger.md +539 -0
  310. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  311. package/kits/docs/identity/PATH_OPS.md +16 -0
  312. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  313. package/kits/docs/identity/identity-tools.md +26 -0
  314. package/kits/docs/path-registry.md +43 -0
  315. package/kits/docs/prompt-loading-architecture.md +266 -0
  316. package/kits/docs/venues/aun-group.md +45 -0
  317. package/kits/docs/venues/aun-private.md +10 -0
  318. package/kits/docs/venues/client-desktop.md +10 -0
  319. package/kits/docs/venues/client-mobile.md +10 -0
  320. package/kits/docs/venues/feishu-group.md +13 -0
  321. package/kits/docs/venues/feishu-private.md +9 -0
  322. package/kits/docs/venues/group.md +25 -0
  323. package/kits/docs/venues/private.md +10 -0
  324. package/kits/eck_manifest.auxiliary.json +43 -0
  325. package/kits/eck_manifest.json +203 -0
  326. package/kits/eck_message_manifest.json +63 -0
  327. package/kits/migrations/README-role-config-v4.md +32 -0
  328. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  329. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  330. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  331. package/kits/migrations/rename-config-file.mjs +99 -0
  332. package/kits/rules/01-overview.md +142 -0
  333. package/kits/rules/02-navigation.md +76 -0
  334. package/kits/rules/03-identity.md +34 -0
  335. package/kits/rules/04-relation.md +59 -0
  336. package/kits/rules/05-venue.md +44 -0
  337. package/kits/rules/06-channel.md +59 -0
  338. package/kits/schemas/_meta.json +32 -0
  339. package/kits/schemas/agent-config.schema.1.json +177 -0
  340. package/kits/schemas/agent-config.schema.2.json +239 -0
  341. package/kits/schemas/agent-config.schema.3.json +119 -0
  342. package/kits/schemas/agent-config.schema.4.json +208 -0
  343. package/kits/schemas/agent-config.schema.5.json +326 -0
  344. package/kits/schemas/agent-config.schema.6.json +322 -0
  345. package/kits/schemas/contact-book.schema.1.json +36 -0
  346. package/kits/schemas/contact-book.schema.2.json +43 -0
  347. package/kits/schemas/daemon.schema.1.json +90 -0
  348. package/kits/schemas/defaults.schema.1.json +81 -0
  349. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  350. package/kits/schemas/migrations/README.md +28 -0
  351. package/kits/schemas/relation-config.schema.1.json +158 -0
  352. package/kits/schemas/relation-config.schema.2.json +73 -0
  353. package/kits/schemas/relation-config.schema.3.json +50 -0
  354. package/kits/schemas/relation-config.schema.4.json +47 -0
  355. package/kits/schemas/relation-config.schema.5.json +47 -0
  356. package/kits/schemas/role-config.schema.1.json +201 -0
  357. package/kits/schemas/role-registry.schema.1.json +35 -0
  358. package/kits/schemas/single-session.schema.1.json +31 -0
  359. package/kits/templates/bootstrap-welcome.md +15 -0
  360. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  361. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  362. package/kits/templates/message-fragments/inject-default.md +2 -0
  363. package/kits/templates/message-fragments/item.md +2 -0
  364. package/kits/templates/roles/admin.json +9 -0
  365. package/kits/templates/roles/member.json +41 -0
  366. package/kits/templates/roles/owner.json +9 -0
  367. package/kits/templates/roles/visitor.json +40 -0
  368. package/kits/templates/system-fragments/baseagent.md +14 -0
  369. package/kits/templates/system-fragments/bootstrap.md +16 -0
  370. package/kits/templates/system-fragments/channel.md +48 -0
  371. package/kits/templates/system-fragments/commands.md +28 -0
  372. package/kits/templates/system-fragments/identity.md +11 -0
  373. package/kits/templates/system-fragments/relation.md +19 -0
  374. package/kits/templates/system-fragments/session.md +53 -0
  375. package/kits/templates/system-fragments/venue.md +31 -0
  376. package/package.json +50 -15
  377. package/dist/index.d.ts +0 -7
  378. package/dist/index.d.ts.map +0 -1
  379. package/dist/index.js.map +0 -1
@@ -0,0 +1,598 @@
1
+ /**
2
+ * snapshot —— 配置快照与回滚。
3
+ *
4
+ * v3 设计快照范围:daemon.json + agents/defaults.json + 每 agent 的 config.json
5
+ * + 每关系的 config.json + extra_backup 声明文件。**不含 .env**(凭证不进版本库)。
6
+ *
7
+ * 版本号:全量百位递增(v100/v200…),增量嵌套其下(v101…v199,每全量 ≤99 个增量)。
8
+ * 目录:{root}/backups/config/v100/{meta.json, snapshot/...} + v101/{meta.json, delta/...}
9
+ * 指针:current.json {full, delta}。
10
+ *
11
+ * 两层比对:
12
+ * - 要不要建版本 → 与 current.json 当前版本比(一致跳过;schema-migration 无条件)
13
+ * - 建增量还是全量 → 与所属全量比(差异文件 > 半数 / 增量达99 / --full → 升全量)
14
+ */
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import { resolvePaths } from '../paths.js';
18
+ import { logger } from '../utils/logger.js';
19
+ import { ConfigTarget, read } from './config-manager.js';
20
+ const RETAIN_FULL = 10;
21
+ const RETAIN_DELTA = 20;
22
+ /** Process-level master switch. Missing config remains enabled for compatibility. */
23
+ export function isConfigSnapshotsEnabled() {
24
+ const config = read(ConfigTarget.Process, {}, { cache: true });
25
+ return config?.configSnapshots !== false;
26
+ }
27
+ /**
28
+ * Capture the validated working config after a successful normal startup.
29
+ * The first successful startup creates a full baseline; later starts only
30
+ * create a version when the managed config tree changed.
31
+ */
32
+ export function ensureStartupConfigSnapshot() {
33
+ if (!isConfigSnapshotsEnabled())
34
+ return null;
35
+ const workingVersion = readWVersion();
36
+ if (!workingVersion) {
37
+ snapshot('startup', { full: true, description: 'initial automatic snapshot' });
38
+ return readWVersion();
39
+ }
40
+ const diff = diffWorkingVsVersion(workingVersion.delta);
41
+ if ('error' in diff) {
42
+ logger.warn(`[snapshot] ${diff.error}; creating a new full startup baseline`);
43
+ snapshot('startup', { full: true, description: 'replace invalid startup baseline' });
44
+ return readWVersion();
45
+ }
46
+ const hasChanges = diff.modified.length + diff.added.length + diff.deleted.length > 0;
47
+ if (hasChanges)
48
+ snapshot('startup');
49
+ return readWVersion() ?? workingVersion;
50
+ }
51
+ // ── 配置文件集合扫描(相对 root 的 POSIX 路径)────────────────────────────────
52
+ /** 收集当前工作目录下所有受快照管理的配置文件(相对 root,正斜杠)。 */
53
+ export function collectConfigFiles(root) {
54
+ const out = [];
55
+ const rel = (abs) => path.relative(root, abs).split(path.sep).join('/');
56
+ const pushIf = (abs) => { if (fs.existsSync(abs))
57
+ out.push(rel(abs)); };
58
+ pushIf(path.join(root, 'daemon.json'));
59
+ const agentsRoot = path.join(root, 'agents');
60
+ pushIf(path.join(agentsRoot, 'defaults.json'));
61
+ if (fs.existsSync(agentsRoot)) {
62
+ for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
63
+ if (!entry.isDirectory())
64
+ continue;
65
+ const adir = path.join(agentsRoot, entry.name);
66
+ pushIf(path.join(adir, 'config.json'));
67
+ const rolesDir = path.join(adir, 'roles');
68
+ if (fs.existsSync(rolesDir)) {
69
+ for (const roleEntry of fs.readdirSync(rolesDir, { withFileTypes: true })) {
70
+ if (roleEntry.isFile() && roleEntry.name.endsWith('.json'))
71
+ pushIf(path.join(rolesDir, roleEntry.name));
72
+ }
73
+ }
74
+ // extra_backup 声明
75
+ collectExtraBackup(adir, root, out);
76
+ const relations = path.join(adir, 'relations');
77
+ if (fs.existsSync(relations)) {
78
+ for (const pk of fs.readdirSync(relations, { withFileTypes: true })) {
79
+ if (!pk.isDirectory() || pk.name.startsWith('_'))
80
+ continue;
81
+ const rdir = path.join(relations, pk.name);
82
+ pushIf(path.join(rdir, 'config.json'));
83
+ collectExtraBackup(rdir, root, out);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ return [...new Set(out)];
89
+ }
90
+ function collectExtraBackup(dir, root, out) {
91
+ const cfgPath = path.join(dir, 'config.json');
92
+ if (!fs.existsSync(cfgPath))
93
+ return;
94
+ let cfg;
95
+ try {
96
+ cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
97
+ }
98
+ catch {
99
+ return;
100
+ }
101
+ const eb = cfg.extra_backup;
102
+ if (!Array.isArray(eb))
103
+ return;
104
+ const rel = (abs) => path.relative(root, abs).split(path.sep).join('/');
105
+ for (const e of eb) {
106
+ if (!e || typeof e !== 'object' || !e.path)
107
+ continue;
108
+ const sub = path.join(dir, e.path);
109
+ if (!fs.existsSync(sub))
110
+ continue;
111
+ const pattern = e.pattern || '*';
112
+ if (pattern.toLowerCase().includes('.env'))
113
+ continue; // 安全:拒 .env
114
+ for (const f of fs.readdirSync(sub)) {
115
+ if (matchGlob(f, pattern) && !f.endsWith('.env')) {
116
+ const abs = path.join(sub, f);
117
+ if (fs.statSync(abs).isFile())
118
+ out.push(rel(abs));
119
+ }
120
+ }
121
+ }
122
+ }
123
+ function matchGlob(name, pattern) {
124
+ // 极简 glob:* → .*,? → .
125
+ const re = new RegExp('^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
126
+ return re.test(name);
127
+ }
128
+ // ── current.json 指针 ────────────────────────────────────────────────────────
129
+ export function readCurrent() {
130
+ const p = resolvePaths().configCurrentPointer;
131
+ try {
132
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ }
138
+ export function writeCurrent(ptr) {
139
+ const p = resolvePaths().configCurrentPointer;
140
+ fs.mkdirSync(path.dirname(p), { recursive: true });
141
+ fs.writeFileSync(p, JSON.stringify(ptr, null, 2) + '\n', 'utf-8');
142
+ }
143
+ // ── w-version.json(W 的版本标记)─────────────────────────────────────────────
144
+ export function readWVersion() {
145
+ const p = resolvePaths().configWVersion;
146
+ try {
147
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ export function writeWVersion(ptr) {
154
+ const p = resolvePaths().configWVersion;
155
+ fs.mkdirSync(path.dirname(p), { recursive: true });
156
+ fs.writeFileSync(p, JSON.stringify(ptr, null, 2) + '\n', 'utf-8');
157
+ }
158
+ // ── 版本目录 ──────────────────────────────────────────────────────────────────
159
+ function backupsDir() { return resolvePaths().configBackupsDir; }
160
+ /** 列出所有全量版本号(v100/v200…),升序。 */
161
+ function listFulls() {
162
+ const dir = backupsDir();
163
+ if (!fs.existsSync(dir))
164
+ return [];
165
+ return fs.readdirSync(dir, { withFileTypes: true })
166
+ .filter(e => e.isDirectory() && /^v\d+00$/.test(e.name))
167
+ .map(e => e.name)
168
+ .sort(cmpVersion);
169
+ }
170
+ /** 列出某全量目录下的增量版本号(v101…),升序。 */
171
+ function listDeltas(full) {
172
+ const dir = path.join(backupsDir(), full);
173
+ if (!fs.existsSync(dir))
174
+ return [];
175
+ return fs.readdirSync(dir, { withFileTypes: true })
176
+ .filter(e => e.isDirectory() && /^v\d+$/.test(e.name) && e.name !== full)
177
+ .map(e => e.name)
178
+ .sort(cmpVersion);
179
+ }
180
+ function cmpVersion(a, b) {
181
+ return parseInt(a.slice(1)) - parseInt(b.slice(1));
182
+ }
183
+ /** 所有版本(全量 + 增量)升序,附 type。 */
184
+ export function listAllVersions() {
185
+ const out = [];
186
+ for (const full of listFulls()) {
187
+ out.push(readMeta(full, full));
188
+ for (const d of listDeltas(full))
189
+ out.push(readMeta(full, d));
190
+ }
191
+ return out.filter(Boolean);
192
+ }
193
+ function readMeta(full, version) {
194
+ const metaPath = version === full
195
+ ? path.join(backupsDir(), full, 'meta.json')
196
+ : path.join(backupsDir(), full, version, 'meta.json');
197
+ try {
198
+ return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
199
+ }
200
+ catch {
201
+ return { version, type: version === full ? 'full' : 'delta', createdAt: '', trigger: 'manual', changedFiles: [], deletedFiles: [], successCount: 0 };
202
+ }
203
+ }
204
+ function writeMeta(full, version, meta) {
205
+ const metaPath = version === full
206
+ ? path.join(backupsDir(), full, 'meta.json')
207
+ : path.join(backupsDir(), full, version, 'meta.json');
208
+ fs.mkdirSync(path.dirname(metaPath), { recursive: true });
209
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n', 'utf-8');
210
+ }
211
+ /** 递增版本的 successCount(成功启动计数),用于回落资格判定。 */
212
+ export function incrementSuccessCount(version) {
213
+ const loc = locateVersion(version);
214
+ if (!loc)
215
+ return;
216
+ const meta = readMeta(loc.full, loc.delta);
217
+ meta.successCount = (meta.successCount || 0) + 1;
218
+ writeMeta(loc.full, loc.delta, meta);
219
+ }
220
+ function nextFullVersion() {
221
+ const fulls = listFulls();
222
+ if (fulls.length === 0)
223
+ return 'v100';
224
+ const last = parseInt(fulls[fulls.length - 1].slice(1));
225
+ return 'v' + (Math.floor(last / 100) * 100 + 100);
226
+ }
227
+ function nextDeltaVersion(full) {
228
+ const deltas = listDeltas(full);
229
+ const base = parseInt(full.slice(1));
230
+ if (deltas.length === 0)
231
+ return 'v' + (base + 1);
232
+ const last = parseInt(deltas[deltas.length - 1].slice(1));
233
+ return 'v' + (last + 1);
234
+ }
235
+ // ── 读文件内容(用于比对/写快照)─────────────────────────────────────────────
236
+ function readWorkFile(root, relPath) {
237
+ try {
238
+ return fs.readFileSync(path.join(root, relPath), 'utf-8');
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
244
+ /** 展开某版本到一份内容 map(相对路径 → 内容)。增量基于父全量叠加。 */
245
+ export function materializeVersion(full, delta) {
246
+ const map = new Map();
247
+ const snapDir = path.join(backupsDir(), full, 'snapshot');
248
+ if (fs.existsSync(snapDir))
249
+ walkInto(snapDir, snapDir, map);
250
+ if (delta !== full) {
251
+ const changesPath = path.join(backupsDir(), full, delta, 'delta', 'changes.jsonl');
252
+ const filesDir = path.join(backupsDir(), full, delta, 'delta', 'files');
253
+ if (fs.existsSync(changesPath)) {
254
+ for (const line of fs.readFileSync(changesPath, 'utf-8').split('\n')) {
255
+ if (!line.trim())
256
+ continue;
257
+ const ch = JSON.parse(line);
258
+ if (ch.op === 'delete') {
259
+ map.delete(ch.path);
260
+ }
261
+ else {
262
+ const content = readWorkFile(filesDir, ch.path);
263
+ if (content !== null)
264
+ map.set(ch.path, content);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ return map;
270
+ }
271
+ function walkInto(baseDir, cur, map) {
272
+ for (const e of fs.readdirSync(cur, { withFileTypes: true })) {
273
+ const abs = path.join(cur, e.name);
274
+ if (e.isDirectory())
275
+ walkInto(baseDir, abs, map);
276
+ else {
277
+ const rel = path.relative(baseDir, abs).split(path.sep).join('/');
278
+ map.set(rel, fs.readFileSync(abs, 'utf-8'));
279
+ }
280
+ }
281
+ }
282
+ /** 当前工作目录配置文件 → 内容 map。 */
283
+ function materializeWorkdir(root) {
284
+ const map = new Map();
285
+ for (const rel of collectConfigFiles(root)) {
286
+ const c = readWorkFile(root, rel);
287
+ if (c !== null)
288
+ map.set(rel, c);
289
+ }
290
+ return map;
291
+ }
292
+ /** 比对两份 map,返回差异。 */
293
+ function diffMaps(oldM, newM) {
294
+ const add = [], modify = [], del = [];
295
+ for (const [k, v] of newM) {
296
+ if (!oldM.has(k))
297
+ add.push(k);
298
+ else if (oldM.get(k) !== v)
299
+ modify.push(k);
300
+ }
301
+ for (const k of oldM.keys())
302
+ if (!newM.has(k))
303
+ del.push(k);
304
+ return { add, modify, del };
305
+ }
306
+ // ── snapshot:建版本 ──────────────────────────────────────────────────────────
307
+ export function snapshot(trigger, opts = {}) {
308
+ if (!isConfigSnapshotsEnabled())
309
+ return { created: false, reason: 'disabled' };
310
+ const root = resolvePaths().root;
311
+ const work = materializeWorkdir(root);
312
+ const cur = readCurrent();
313
+ // 决策一:要不要建版本(与 current 版本比;schema-migration 无条件)
314
+ if (trigger !== 'schema-migration' && cur) {
315
+ const curMap = materializeVersion(cur.full, cur.delta);
316
+ const d = diffMaps(curMap, work);
317
+ if (d.add.length === 0 && d.modify.length === 0 && d.del.length === 0) {
318
+ return { created: false, reason: 'no-change' };
319
+ }
320
+ }
321
+ // 决策二:增量 vs 全量(与所属全量比)
322
+ const fulls = listFulls();
323
+ const baseFull = cur?.full && fulls.includes(cur.full) ? cur.full : (fulls[fulls.length - 1] || null);
324
+ let makeFull = opts.full || !baseFull;
325
+ let baseMap = null;
326
+ if (!makeFull && baseFull) {
327
+ baseMap = materializeVersion(baseFull, baseFull);
328
+ const d = diffMaps(baseMap, work);
329
+ const fullFileCount = Math.max(baseMap.size, 1);
330
+ const changeCount = d.add.length + d.modify.length + d.del.length;
331
+ if (changeCount * 2 > fullFileCount)
332
+ makeFull = true; // 差异 > 半数
333
+ if (listDeltas(baseFull).length >= 99)
334
+ makeFull = true; // 增量达 99
335
+ }
336
+ if (makeFull)
337
+ return createFull(root, work, trigger, opts.description);
338
+ return createDelta(root, work, baseFull, baseMap, trigger, opts.description);
339
+ }
340
+ function createFull(root, work, trigger, description) {
341
+ const version = nextFullVersion();
342
+ const verDir = path.join(backupsDir(), version);
343
+ const snapDir = path.join(verDir, 'snapshot');
344
+ for (const [rel, content] of work) {
345
+ const dest = path.join(snapDir, rel);
346
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
347
+ fs.writeFileSync(dest, content, 'utf-8');
348
+ }
349
+ const meta = {
350
+ version, type: 'full', createdAt: nowIso(), trigger, description,
351
+ changedFiles: [...work.keys()], deletedFiles: [],
352
+ successCount: 0,
353
+ };
354
+ fs.writeFileSync(path.join(verDir, 'meta.json'), JSON.stringify(meta, null, 2) + '\n', 'utf-8');
355
+ writeCurrent({ full: version, delta: version });
356
+ writeWVersion({ full: version, delta: version });
357
+ logger.info(`[snapshot] created full ${version} (trigger=${trigger}, ${work.size} files)`);
358
+ return { created: true, version, type: 'full' };
359
+ }
360
+ function createDelta(root, work, baseFull, baseMap, trigger, description) {
361
+ const version = nextDeltaVersion(baseFull);
362
+ const verDir = path.join(backupsDir(), baseFull, version);
363
+ const deltaDir = path.join(verDir, 'delta');
364
+ const filesDir = path.join(deltaDir, 'files');
365
+ const d = diffMaps(baseMap, work);
366
+ const lines = [];
367
+ for (const p of d.add) {
368
+ lines.push(JSON.stringify({ op: 'add', path: p }));
369
+ writeDeltaFile(filesDir, p, work.get(p));
370
+ }
371
+ for (const p of d.modify) {
372
+ lines.push(JSON.stringify({ op: 'modify', path: p }));
373
+ writeDeltaFile(filesDir, p, work.get(p));
374
+ }
375
+ for (const p of d.del)
376
+ lines.push(JSON.stringify({ op: 'delete', path: p }));
377
+ fs.mkdirSync(deltaDir, { recursive: true });
378
+ fs.writeFileSync(path.join(deltaDir, 'changes.jsonl'), lines.join('\n') + (lines.length ? '\n' : ''), 'utf-8');
379
+ const meta = {
380
+ version, type: 'delta', baseVersion: baseFull, createdAt: nowIso(), trigger, description,
381
+ changedFiles: [...d.add, ...d.modify], deletedFiles: d.del,
382
+ successCount: 0,
383
+ };
384
+ fs.writeFileSync(path.join(verDir, 'meta.json'), JSON.stringify(meta, null, 2) + '\n', 'utf-8');
385
+ writeCurrent({ full: baseFull, delta: version });
386
+ writeWVersion({ full: baseFull, delta: version });
387
+ logger.info(`[snapshot] created delta ${version} on ${baseFull} (trigger=${trigger}, +${d.add.length} ~${d.modify.length} -${d.del.length})`);
388
+ return { created: true, version, type: 'delta' };
389
+ }
390
+ function writeDeltaFile(filesDir, rel, content) {
391
+ const dest = path.join(filesDir, rel);
392
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
393
+ fs.writeFileSync(dest, content, 'utf-8');
394
+ }
395
+ function nowIso() {
396
+ // Date 在 workflow 脚本里被禁;此处是运行时模块,正常可用。
397
+ return new Date().toISOString();
398
+ }
399
+ // ── restore:恢复到指定版本 ──────────────────────────────────────────────────
400
+ /** 找某版本号属于哪个全量(全量自身或某增量)。 */
401
+ function locateVersion(version) {
402
+ if (/^v\d+00$/.test(version))
403
+ return listFulls().includes(version) ? { full: version, delta: version } : null;
404
+ for (const full of listFulls()) {
405
+ if (listDeltas(full).includes(version))
406
+ return { full, delta: version };
407
+ }
408
+ return null;
409
+ }
410
+ /** 恢复:展开目标版本到工作目录 + 更新 current.json(先建 pre-restore 快照)。 */
411
+ export function restore(version) {
412
+ if (!isConfigSnapshotsEnabled())
413
+ return { ok: false, error: '配置快照功能已关闭' };
414
+ const loc = locateVersion(version);
415
+ if (!loc)
416
+ return { ok: false, error: `版本不存在: ${version}` };
417
+ // pre-restore 快照(保护当前工作目录状态)
418
+ snapshot('manual', { description: `pre-restore before ${version}` });
419
+ const root = resolvePaths().root;
420
+ const target = materializeVersion(loc.full, loc.delta);
421
+ const current = materializeWorkdir(root);
422
+ // 删除工作目录中目标版本没有的配置文件
423
+ for (const rel of current.keys()) {
424
+ if (!target.has(rel)) {
425
+ try {
426
+ fs.unlinkSync(path.join(root, rel));
427
+ }
428
+ catch { /* best effort */ }
429
+ }
430
+ }
431
+ // 写入目标版本所有文件
432
+ let applied = 0;
433
+ for (const [rel, content] of target) {
434
+ const dest = path.join(root, rel);
435
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
436
+ fs.writeFileSync(dest, content, 'utf-8');
437
+ applied++;
438
+ }
439
+ writeCurrent({ full: loc.full, delta: loc.delta });
440
+ writeWVersion({ full: loc.full, delta: loc.delta });
441
+ logger.info(`[snapshot] restored ${version} (${applied} files)`);
442
+ return { ok: true, version, appliedFiles: applied };
443
+ }
444
+ export function diffVersions(v1, v2) {
445
+ const a = locateVersion(v1), b = locateVersion(v2);
446
+ if (!a)
447
+ return { error: `版本不存在: ${v1}` };
448
+ if (!b)
449
+ return { error: `版本不存在: ${v2}` };
450
+ const ma = materializeVersion(a.full, a.delta);
451
+ const mb = materializeVersion(b.full, b.delta);
452
+ const d = diffMaps(ma, mb);
453
+ return { added: d.add, modified: d.modify, deleted: d.del };
454
+ }
455
+ /** W vs 指定版本的文件级差异。 */
456
+ export function diffWorkingVsVersion(version) {
457
+ const loc = locateVersion(version);
458
+ if (!loc)
459
+ return { error: `版本不存在: ${version}` };
460
+ const root = resolvePaths().root;
461
+ const work = materializeWorkdir(root);
462
+ const target = materializeVersion(loc.full, loc.delta);
463
+ const d = diffMaps(target, work);
464
+ return { added: d.add, modified: d.modify, deleted: d.del };
465
+ }
466
+ /** 参数级 diff(字段路径 + 值变化):W vs 指定版本。 */
467
+ export function paramDiff(version) {
468
+ const fileDiff = diffWorkingVsVersion(version);
469
+ if ('error' in fileDiff)
470
+ return fileDiff;
471
+ const root = resolvePaths().root;
472
+ const work = materializeWorkdir(root);
473
+ const loc = locateVersion(version);
474
+ const target = materializeVersion(loc.full, loc.delta);
475
+ const out = [];
476
+ for (const file of [...fileDiff.added, ...fileDiff.modified, ...fileDiff.deleted]) {
477
+ const wContent = work.get(file);
478
+ const tContent = target.get(file);
479
+ const changes = [];
480
+ // 文件新增/删除 = 整体 add/delete
481
+ if (!tContent && wContent) {
482
+ try {
483
+ changes.push({ path: '<file>', before: null, after: JSON.parse(wContent), op: 'add' });
484
+ }
485
+ catch { /* 无法解析,跳过 */ }
486
+ }
487
+ else if (tContent && !wContent) {
488
+ try {
489
+ changes.push({ path: '<file>', before: JSON.parse(tContent), after: null, op: 'delete' });
490
+ }
491
+ catch { /* */ }
492
+ }
493
+ else if (tContent && wContent) {
494
+ // 文件修改 = 字段级 diff
495
+ try {
496
+ const a = JSON.parse(tContent);
497
+ const b = JSON.parse(wContent);
498
+ diffObjects('', a, b, changes);
499
+ }
500
+ catch { /* 解析失败,跳过 */ }
501
+ }
502
+ if (changes.length > 0)
503
+ out.push({ file, changes });
504
+ }
505
+ return out;
506
+ }
507
+ function diffObjects(prefix, a, b, out) {
508
+ const allKeys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]);
509
+ for (const k of allKeys) {
510
+ const path = prefix ? `${prefix}.${k}` : k;
511
+ const aVal = a?.[k];
512
+ const bVal = b?.[k];
513
+ if (aVal === undefined && bVal !== undefined) {
514
+ out.push({ path, before: null, after: bVal, op: 'add' });
515
+ }
516
+ else if (aVal !== undefined && bVal === undefined) {
517
+ out.push({ path, before: aVal, after: null, op: 'delete' });
518
+ }
519
+ else if (JSON.stringify(aVal) !== JSON.stringify(bVal)) {
520
+ if (typeof aVal === 'object' && typeof bVal === 'object' && !Array.isArray(aVal) && !Array.isArray(bVal)) {
521
+ diffObjects(path, aVal, bVal, out);
522
+ }
523
+ else {
524
+ out.push({ path, before: aVal, after: bVal, op: 'modify' });
525
+ }
526
+ }
527
+ }
528
+ }
529
+ /**
530
+ * 清理:保留最近 keepFull 全量 / keepDelta 增量(跨全量统计)。dryRun=true 只列不删。
531
+ * 拒删:current 指向的版本、被保留区间内增量依赖的全量。
532
+ */
533
+ export function prune(opts = {}) {
534
+ const keepFull = opts.keepFull ?? RETAIN_FULL;
535
+ const keepDelta = opts.keepDelta ?? RETAIN_DELTA;
536
+ const cur = readCurrent();
537
+ const fulls = listFulls();
538
+ // 增量保留:跨全量按时间(版本号近似)取最近 keepDelta
539
+ const allDeltas = [];
540
+ for (const f of fulls)
541
+ for (const d of listDeltas(f))
542
+ allDeltas.push({ full: f, ver: d });
543
+ const keptDeltas = new Set(allDeltas.slice(-keepDelta).map(d => `${d.full}/${d.ver}`));
544
+ // 全量保留:最近 keepFull + 仍被保留增量依赖的全量 + current 全量
545
+ const keptFulls = new Set(fulls.slice(-keepFull));
546
+ for (const d of allDeltas)
547
+ if (keptDeltas.has(`${d.full}/${d.ver}`))
548
+ keptFulls.add(d.full);
549
+ if (cur)
550
+ keptFulls.add(cur.full);
551
+ const wouldDelete = [];
552
+ const deleted = [];
553
+ const kept = [];
554
+ for (const f of fulls) {
555
+ if (keptFulls.has(f)) {
556
+ kept.push(f);
557
+ // 该全量保留时,清理其下未保留的增量
558
+ for (const d of listDeltas(f)) {
559
+ const tag = `${f}/${d}`;
560
+ const isCurrent = cur && cur.full === f && cur.delta === d;
561
+ if (keptDeltas.has(tag) || isCurrent) {
562
+ kept.push(d);
563
+ continue;
564
+ }
565
+ wouldDelete.push(d);
566
+ if (!opts.dryRun) {
567
+ rmDir(path.join(backupsDir(), f, d));
568
+ deleted.push(d);
569
+ }
570
+ }
571
+ }
572
+ else {
573
+ wouldDelete.push(f);
574
+ if (!opts.dryRun) {
575
+ rmDir(path.join(backupsDir(), f));
576
+ deleted.push(f);
577
+ }
578
+ }
579
+ }
580
+ return { wouldDelete, deleted, kept };
581
+ }
582
+ function rmDir(dir) {
583
+ try {
584
+ fs.rmSync(dir, { recursive: true, force: true });
585
+ }
586
+ catch { /* best effort */ }
587
+ }
588
+ /** 启动后延迟清理:保留策略默认值,非 dry-run。 */
589
+ export function retentionCleanup() {
590
+ if (!isConfigSnapshotsEnabled())
591
+ return;
592
+ try {
593
+ prune({ dryRun: false });
594
+ }
595
+ catch (e) {
596
+ logger.warn(`[snapshot] retention cleanup failed: ${e}`);
597
+ }
598
+ }