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
package/dist/index.js CHANGED
@@ -1,6 +1,2928 @@
1
+ // 【重要】最先加载环境变量,确保后续模块初始化时可用
2
+ import dotenv from 'dotenv';
3
+ import path from 'path';
4
+ import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath, eckDebugDir } from './paths.js';
5
+ // 立即加载 .env 文件(在其他模块导入之前)
6
+ try {
7
+ dotenv.config({ path: path.join(resolvePaths().root, '.env') });
8
+ }
9
+ catch {
10
+ // 首次运行时 .env 可能不存在,忽略错误
11
+ }
12
+ import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
13
+ import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
14
+ import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
15
+ import { loadDefaults, loadAllAgents, migrateIdentitiesIfNeeded, loadDaemonConfig, initializeEckSnapshotsConfig } from './config-store.js';
16
+ import { ConfigTarget, initConfigManager, onConfigWrite } from './config/config-manager.js';
17
+ import { ensureRoleConfigV4OnStartup } from './config/role-config-v4-startup.js';
18
+ import { ensureRoleConfigV5OnStartup } from './config/role-config-v5-startup.js';
19
+ import { ensureContactBookV2OnStartup } from './config/contact-book-v2-startup.js';
20
+ import { shouldFailFastForMissingOwners } from './config/owner-policy.js';
21
+ import { isManagementRole } from './config/builtin-roles.js';
22
+ import { checkRoleAccess, getFirstStaticAgentOwner, resolvePeerRoleDetail, roleToSessionIdentity } from './config/peer-role-resolver.js';
23
+ import { ensureStartupConfigSnapshot, isConfigSnapshotsEnabled, retentionCleanup, readCurrent, readWVersion, diffWorkingVsVersion, paramDiff, incrementSuccessCount } from './config/snapshot.js';
24
+ import { appendBootLog, selfDiagnose } from './config/boot-log.js';
25
+ import { CONFIG_SCHEMA_VERSION } from './types.js';
26
+ import { SessionManager } from './core/session/session-manager.js';
27
+ import { ClaudeAgentPlugin } from './agents/claude-runner.js';
28
+ import { CodexAgentPlugin } from './agents/codex-runner.js';
29
+ import { GeminiAgentPlugin } from './agents/gemini-runner.js';
30
+ import { FeishuChannelPlugin } from './channels/feishu.js';
31
+ import { WechatChannelPlugin } from './channels/wechat.js';
32
+ import { AUN_HANDOFF_MARKER_FIELD, AUNChannel, AUNChannelPlugin } from './channels/aun.js';
33
+ import { startServiceProxy } from './aun/service-proxy.js';
34
+ import { BindService } from './utils/aid-bind.js';
35
+ import { DingtalkChannelPlugin, registerPendingDingtalkContactBind } from './channels/dingtalk.js';
36
+ import { QQBotChannelPlugin } from './channels/qqbot.js';
37
+ import { WecomChannelPlugin, registerPendingWecomContactBind } from './channels/wecom.js';
38
+ import { buildEnvelope } from './core/message/message-utils.js';
39
+ import { ResponseEngine } from './core/message/response-engine.js';
40
+ import { MessageQueue } from './core/message/message-queue.js';
41
+ import { MessageBridge } from './core/message/message-bridge.js';
42
+ import { MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, parseMenuControl, validateMenuRequest } from './core/command/menu-protocol.js';
43
+ import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
44
+ import { HandoffRuntime } from './core/handoff/runtime.js';
45
+ import { BootstrapService } from './core/bootstrap-service.js';
46
+ import { MessageCache } from './core/message/message-cache.js';
47
+ import { CommandHandler, isProcessLevelOwner } from './core/command/command-handler.js';
48
+ import { EventBus } from './core/event-bus.js';
49
+ import { getEventCatalog } from './core/event-catalog.js';
50
+ import { StatsCollector } from './utils/stats.js';
51
+ import { AidStatsCollector } from './utils/stats.js';
52
+ import { PermissionGateway } from './core/permission/approval-gateway.js';
53
+ import { InteractionRouter } from './core/interaction-router.js';
54
+ import { registerAdapterInteractions } from './core/interaction-registration.js';
55
+ import { AgentDelegationRegistry, authorizeDelegatedAunMsgSend } from './core/auth/agent-delegation.js';
56
+ import { authorizeOperation, buildAuthSubject } from './core/auth/auth-gateway.js';
57
+ import { isHClassPath } from './core/protected-paths.js';
58
+ import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
59
+ import { AgentLoader } from './core/baseagent-loader.js';
60
+ import { EvolAgentRegistry } from './core/evolagent-registry.js';
61
+ import { buildReloadHooks } from './core/channel-loader.js';
62
+ import { IpcServer } from './ipc.js';
63
+ import { logger, setLogLevel } from './utils/logger.js';
64
+ import { fetchEcwebPairCode } from './utils/ecweb-utils.js';
65
+ import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
66
+ import { detectDuplicates } from './core/evolagent-registry.js';
67
+ import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './eck/kit-renderer.js';
68
+ import { initEck } from './eck/init.js';
69
+ import { TriggerDefinitionManager } from './trigger/manager.js';
70
+ import { TriggerRunStateStore } from './trigger/state.js';
71
+ import { TriggerAuditLogger } from './trigger/audit.js';
72
+ import { TriggerScriptExecutor } from './trigger/script-executor.js';
73
+ import { TriggerFeedbackDispatcher } from './trigger/feedback.js';
74
+ import { TriggerRuntimeScheduler } from './trigger/scheduler.js';
75
+ import { DaemonChannel } from './channels/daemon.js';
76
+ import { definitionRevision, normalizeTriggerDefinition } from './trigger/validation.js';
77
+ import { applyTriggerPatch } from './trigger/patch.js';
78
+ import { validateModelSelectionForRole } from './core/model/model-permission.js';
79
+ import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
80
+ import { atomicWriteJson } from './core/session/session-fs-store.js';
81
+ import { cleanupLegacySystemControlSessions } from './core/system-channels.js';
82
+ import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
83
+ import fs from 'fs';
84
+ import crypto from 'crypto';
85
+ import { fileURLToPath } from 'url';
86
+ import { spawn } from 'child_process';
87
+ import * as platform from './utils/cross-platform.js';
88
+ const controlMenuDeduper = new MenuRequestDeduper();
89
+ /** 出站 payload 摘要(用于 channel-out.log) */
90
+ function summarizeOutboundPayload(payload) {
91
+ if (!payload)
92
+ return { kind: 'unknown' };
93
+ const s = { kind: payload.kind };
94
+ switch (payload.kind) {
95
+ case 'activity.batch':
96
+ s.itemCount = payload.items?.length ?? 0;
97
+ s.items = payload.items;
98
+ break;
99
+ case 'result.text':
100
+ s.isFinal = payload.isFinal;
101
+ s.text = payload.text;
102
+ break;
103
+ case 'command.result':
104
+ case 'command.error':
105
+ case 'result.error':
106
+ s.text = payload.text;
107
+ break;
108
+ case 'result.file':
109
+ s.filePath = payload.filePath;
110
+ break;
111
+ case 'system.notice':
112
+ case 'system.error':
113
+ s.subtype = payload.subtype;
114
+ s.text = payload.text;
115
+ break;
116
+ case 'interaction':
117
+ s.interactionId = payload.interaction?.id;
118
+ s.interactionKind = payload.interaction?.kind?.kind;
119
+ break;
120
+ case 'status.started':
121
+ case 'status.progress':
122
+ case 'status.requires_action':
123
+ case 'status.queued':
124
+ case 'status.completed':
125
+ case 'status.interrupted':
126
+ case 'status.error':
127
+ case 'status.timeout':
128
+ s.metadata = payload.metadata;
129
+ break;
130
+ }
131
+ return s;
132
+ }
133
+ function shouldCountSentPayload(payload) {
134
+ return [
135
+ 'result.text',
136
+ 'result.file',
137
+ 'result.image',
138
+ 'result.error',
139
+ 'command.result',
140
+ 'command.error',
141
+ 'interaction',
142
+ ].includes(payload.kind);
143
+ }
144
+ function outboundPayloadToLogText(payload) {
145
+ switch (payload.kind) {
146
+ case 'result.text':
147
+ case 'command.result':
148
+ case 'command.error':
149
+ case 'result.error':
150
+ return { text: payload.text, msgType: 'text' };
151
+ case 'result.file':
152
+ return { text: payload.fileName || payload.filePath, msgType: 'file' };
153
+ case 'result.image':
154
+ return { text: payload.alt || '[image]', msgType: 'image' };
155
+ case 'interaction':
156
+ return { text: payload.fallbackText || '[interaction]', msgType: 'text' };
157
+ default:
158
+ return null;
159
+ }
160
+ }
161
+ function normalizeMessageSource(value) {
162
+ return value === 'cli' || value === 'msg' || value === 'ctl' || value === 'owner-inject'
163
+ ? value
164
+ : 'daemon';
165
+ }
166
+ function daemonConversationWatchdogMs(settings) {
167
+ const idleTimeoutSec = settings.idleMonitor?.timeout;
168
+ const idleMs = typeof idleTimeoutSec === 'number' && Number.isFinite(idleTimeoutSec) && idleTimeoutSec > 0
169
+ ? idleTimeoutSec * 1000
170
+ : 120_000;
171
+ return Math.ceil(idleMs * 5 + 60_000);
172
+ }
173
+ function daemonConversationTotalExecutionMs(settings) {
174
+ const seconds = settings.idleMonitor?.maxExecutionTime;
175
+ return typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0
176
+ ? seconds * 1000
177
+ : 60 * 60 * 1000;
178
+ }
179
+ function originFromActorSession(session, authenticatedPeerId, authenticatedChannelKey, authenticatedChatType) {
180
+ const peerId = authenticatedPeerId || session.metadata?.peerId;
181
+ if (!peerId)
182
+ return undefined;
183
+ const sessionChannelKey = session.metadata?.channelKey || session.channel;
184
+ const channelKey = authenticatedChannelKey || sessionChannelKey;
185
+ const sameConversation = channelKey === sessionChannelKey;
186
+ const isGroup = authenticatedChatType === 'group' || session.chatType === 'group';
187
+ return {
188
+ channelKey,
189
+ channelType: session.channelType || tryParseChannelKey(channelKey)?.type || channelKey,
190
+ channelId: peerId,
191
+ session: sameConversation && !isGroup && session.threadId ? 'thread' : 'main',
192
+ threadId: sameConversation && !isGroup ? (session.threadId || undefined) : undefined,
193
+ peerId,
194
+ sessionKey: sameConversation && !isGroup ? session.sessionKey : undefined,
195
+ };
196
+ }
197
+ function seedUpgradeCheckTrigger(manager, owner) {
198
+ const now = Date.now();
199
+ const scriptName = 'upgrade-check.sh';
200
+ const definition = normalizeTriggerDefinition({
201
+ $schema_version: 3.1,
202
+ id: '__upgrade-check',
203
+ agentAid: owner.aid,
204
+ enabled: true,
205
+ name: '__upgrade-check',
206
+ description: 'System trigger: check EvolCore upgrades after install or upgrade.',
207
+ createdAt: now,
208
+ updatedAt: now,
209
+ origin: {
210
+ channelKey: 'daemon',
211
+ channelType: 'daemon',
212
+ channelId: owner.originPeerId || owner.aid,
213
+ session: 'main',
214
+ peerId: owner.originPeerId || owner.aid,
215
+ sessionKey: `daemon#${owner.originPeerId || owner.aid}#__system__`,
216
+ },
217
+ source: {
218
+ type: 'cron',
219
+ expression: '59 3 * * *',
220
+ timezone: 'Asia/Shanghai',
221
+ },
222
+ execution: {
223
+ type: 'script',
224
+ script: {
225
+ path: scriptName,
226
+ runtime: 'bash',
227
+ timeoutMs: 60_000,
228
+ },
229
+ permissionMode: 'bypass',
230
+ onError: 'fail',
231
+ noopSentinel: '[[NOOP]]',
232
+ },
233
+ feedback: {
234
+ strategy: 'silent',
235
+ },
236
+ reliability: {
237
+ concurrency: 'forbid',
238
+ missedPolicy: 'run_once',
239
+ retry: {
240
+ maxAttempts: 0,
241
+ backoffMs: 30_000,
242
+ },
243
+ },
244
+ });
245
+ const dir = manager.triggerDir(definition.id);
246
+ fs.mkdirSync(dir, { recursive: true });
247
+ const scriptPath = path.join(dir, scriptName);
248
+ const packageRoot = getPackageRoot();
249
+ const cliPath = path.join(packageRoot, 'dist', 'cli', 'index.js');
250
+ const packageJsonPath = path.join(packageRoot, 'package.json');
251
+ const devMode = fs.existsSync(path.join(packageRoot, 'src', 'index.ts'));
252
+ fs.writeFileSync(scriptPath, [
253
+ '#!/usr/bin/env bash',
254
+ 'set -euo pipefail',
255
+ '',
256
+ `CLI_PATH=${JSON.stringify(cliPath)}`,
257
+ `PACKAGE_JSON=${JSON.stringify(packageJsonPath)}`,
258
+ `DEV_MODE=${devMode ? '1' : '0'}`,
259
+ '',
260
+ 'json() {',
261
+ ' local outcome="$1"',
262
+ ' local text="$2"',
263
+ ' node -e \'console.log(JSON.stringify({ outcome: process.argv[1], text: process.argv[2], files: [] }))\' "$outcome" "$text"',
264
+ '}',
265
+ '',
266
+ 'if [ "$DEV_MODE" = "1" ]; then',
267
+ ' json "noop" "upgrade check skipped in dev mode"',
268
+ ' exit 0',
269
+ 'fi',
270
+ '',
271
+ 'local_version="$(node -e \'try { console.log(require(process.argv[1]).version || "") } catch { process.exit(0) }\' "$PACKAGE_JSON")"',
272
+ 'remote_version="$(npm view ec version 2>/dev/null || true)"',
273
+ '',
274
+ 'if [ -z "$remote_version" ]; then',
275
+ ' json "error" "failed to check evolcore latest version"',
276
+ ' exit 0',
277
+ 'fi',
278
+ '',
279
+ 'if [ -z "$local_version" ]; then',
280
+ ' json "error" "failed to read local ec version"',
281
+ ' exit 0',
282
+ 'fi',
283
+ '',
284
+ 'version_cmp="$(node -e \'',
285
+ 'const [a, b] = process.argv.slice(1);',
286
+ 'const pa = a.split("-")[0].split(".").map(Number);',
287
+ 'const pb = b.split("-")[0].split(".").map(Number);',
288
+ 'const n = Math.max(pa.length, pb.length);',
289
+ 'let out = 0;',
290
+ 'for (let i = 0; i < n; i++) {',
291
+ ' const av = Number.isFinite(pa[i]) ? pa[i] : 0;',
292
+ ' const bv = Number.isFinite(pb[i]) ? pb[i] : 0;',
293
+ ' if (av < bv) { out = -1; break; }',
294
+ ' if (av > bv) { out = 1; break; }',
295
+ '}',
296
+ 'console.log(out);',
297
+ '\' "$local_version" "$remote_version")"',
298
+ '',
299
+ 'if [ "$version_cmp" != "-1" ]; then',
300
+ ' json "noop" "evolcore is already up to date ($local_version; latest $remote_version)"',
301
+ ' exit 0',
302
+ 'fi',
303
+ '',
304
+ 'if [ ! -f "$CLI_PATH" ]; then',
305
+ ' json "error" "restart-monitor entry not found: $CLI_PATH"',
306
+ ' exit 0',
307
+ 'fi',
308
+ '',
309
+ 'nohup node "$CLI_PATH" restart-monitor >/dev/null 2>&1 &',
310
+ 'json "success" "evolcore upgrade available: ${local_version:-unknown} -> $remote_version; restart-monitor started"',
311
+ '',
312
+ ].join('\n'));
313
+ fs.chmodSync(scriptPath, 0o755);
314
+ atomicWriteJson(manager.definitionPath(definition.id), definition);
315
+ fs.rmSync(manager.activePath(definition.id), { force: true });
316
+ }
317
+ function removeLegacyAgentUpgradeCheck(manager) {
318
+ let existing;
319
+ try {
320
+ existing = manager.get('__upgrade-check');
321
+ }
322
+ catch {
323
+ fs.rmSync(manager.triggerDir('__upgrade-check'), { recursive: true, force: true });
324
+ return;
325
+ }
326
+ if (!existing)
327
+ return;
328
+ const isLegacySystemOrigin = existing.origin?.channelKey === '__system__' || existing.origin?.channelKey === 'daemon';
329
+ const isUpgradeScript = existing.execution.type === 'script'
330
+ && existing.execution.script?.path === 'upgrade-check.sh';
331
+ if (!isLegacySystemOrigin || !isUpgradeScript)
332
+ return;
333
+ fs.rmSync(manager.triggerDir('__upgrade-check'), { recursive: true, force: true });
334
+ }
1
335
  /**
2
- * EvolCore - Core utilities and abstractions for EvolClaw ecosystem
3
- * @module evolcore
336
+ * 通过 adapter.send 发送系统类 payload(system.notice / system.error / 等)。
337
+ *
338
+ * 网关层(本文件)的所有出站系统通知(上线 / 重启完成 / 渠道告警 / agent 启动失败等)
339
+ * 走这里集中调度,让渠道按 capabilities 决定呈现方式。
340
+ *
341
+ * 当 adapter 还没实现 send(旧 adapter)时,按 payload.kind 降级到 sendText。
342
+ *
343
+ * Exported for unit test coverage; runtime callers are inside main() closure.
4
344
  */
5
- export const version = '0.0.1';
6
- //# sourceMappingURL=index.js.map
345
+ export async function sendSystemPayload(adapter, envelope, payload) {
346
+ await adapter.send(envelope, payload);
347
+ }
348
+ async function runBindBootstrapDaemon(daemonCfg, defaults) {
349
+ logger.warn('[bind-bootstrap] starting control AID + IPC only');
350
+ const bindService = new BindService({
351
+ receiverAid: daemonCfg.aid,
352
+ getAvailableBaseagents: detectAvailableBaseagentsForBind,
353
+ getUptimeSeconds: () => Math.floor(process.uptime()),
354
+ });
355
+ bindService.startCleanup();
356
+ let controlChannel;
357
+ controlChannel = new AUNChannel({
358
+ aid: daemonCfg.aid,
359
+ agentName: daemonCfg.aid,
360
+ channelName: 'control',
361
+ pureIdentity: true,
362
+ aunTrace: daemonCfg.debug?.aunTrace ?? defaults.debug?.aunTrace,
363
+ aunSdkLog: daemonCfg.debug?.aunSdkLog ?? defaults.debug?.aunSdkLog,
364
+ });
365
+ try {
366
+ await controlChannel.connect();
367
+ logger.info(`✓ 控制 AID 已连接: ${daemonCfg.aid}`);
368
+ }
369
+ catch (e) {
370
+ logger.warn(`控制 AID 首连失败(后台自动重连,不影响 bootstrap IPC): ${e?.message || e}`);
371
+ }
372
+ controlChannel.onMessage(async (opts) => {
373
+ const text = (opts.content || '').trim();
374
+ let parsed = null;
375
+ try {
376
+ parsed = JSON.parse(text);
377
+ }
378
+ catch {
379
+ parsed = null;
380
+ }
381
+ const response = parsed ? await bindService.handleRequest(parsed, opts.peerId) : null;
382
+ if (response) {
383
+ await controlChannel.sendMessage(opts.channelId, JSON.stringify(response));
384
+ }
385
+ });
386
+ const ipcServer = new IpcServer(resolvePaths().socket, () => ({
387
+ pid: process.pid,
388
+ uptime: Math.round(process.uptime() * 1000),
389
+ channels: {},
390
+ channelsByType: {},
391
+ queue: { pending: 0, processing: 0 },
392
+ controlAid: { aid: daemonCfg.aid, connected: controlChannel?.getAidState().status === 'connected' },
393
+ }));
394
+ ipcServer.setBindExecutor({
395
+ begin: (cmd) => bindService.begin(cmd),
396
+ status: (taskId) => bindService.status(taskId),
397
+ cancel: (taskId) => bindService.cancel(taskId),
398
+ });
399
+ await ipcServer.start();
400
+ fs.writeFileSync(resolvePaths().readySignal, String(Date.now()));
401
+ logger.info(`✓ Bind bootstrap ready signal written: ${resolvePaths().readySignal}`);
402
+ const shutdown = async (signal) => {
403
+ logger.info(`[bind-bootstrap] shutting down${signal ? ` (${signal})` : ''}`);
404
+ ipcServer.stop();
405
+ bindService.stopCleanup();
406
+ if (controlChannel) {
407
+ try {
408
+ await controlChannel.disconnect();
409
+ }
410
+ catch { /* ignore */ }
411
+ }
412
+ removeAll();
413
+ process.exit(0);
414
+ };
415
+ process.on('SIGINT', () => shutdown('SIGINT'));
416
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
417
+ process.on('exit', () => removeAll());
418
+ }
419
+ export function readEvolcoreVersion() {
420
+ try {
421
+ const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
422
+ return pkg.version || 'unknown';
423
+ }
424
+ catch {
425
+ return 'unknown';
426
+ }
427
+ }
428
+ function detectAvailableBaseagentsForBind() {
429
+ const out = [];
430
+ for (const cmd of ['claude', 'gemini', 'codex']) {
431
+ if (commandExists(cmd))
432
+ out.push(cmd);
433
+ }
434
+ return out;
435
+ }
436
+ /**
437
+ * 启动失败时分类打印(不交互、不自动回落,只给准确提示)。
438
+ * 分类:W 解析失败 / W 有未存改动(param diff) / W==w-version(版本自身坏) → 建议自检命令。
439
+ */
440
+ function printConfigFailure(skipped) {
441
+ const root = resolvePaths().root;
442
+ const agentsDir = path.join(root, 'agents');
443
+ const lines = ['❌ 启动失败:无法加载任何 self-agent 配置。'];
444
+ // 先检查是否有解析错误(语法级失败)
445
+ const parseErrors = [];
446
+ if (fs.existsSync(agentsDir)) {
447
+ for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
448
+ if (!entry.isDirectory())
449
+ continue;
450
+ const cfgPath = path.join(agentsDir, entry.name, 'config.json');
451
+ if (!fs.existsSync(cfgPath))
452
+ continue;
453
+ try {
454
+ JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
455
+ }
456
+ catch (e) {
457
+ parseErrors.push(` agents/${entry.name}/config.json 无法解析: ${e instanceof Error ? e.message : String(e)}`);
458
+ }
459
+ }
460
+ }
461
+ if (parseErrors.length > 0) {
462
+ lines.push('配置文件语法错误:');
463
+ lines.push(...parseErrors);
464
+ const wv = readWVersion();
465
+ if (wv)
466
+ lines.push(` 回退到上一版本: ec config restore ${wv.delta}`);
467
+ }
468
+ else {
469
+ // 检查 W vs w-version
470
+ const wv = readWVersion();
471
+ if (wv) {
472
+ const diff = diffWorkingVsVersion(wv.delta);
473
+ if (!('error' in diff) && (diff.modified.length + diff.added.length + diff.deleted.length > 0)) {
474
+ lines.push(`当前参数与版本 ${wv.delta} 存在差异(可能是失败原因):`);
475
+ const pdiff = paramDiff(wv.delta);
476
+ if (!('error' in pdiff)) {
477
+ for (const fd of pdiff) {
478
+ for (const c of fd.changes) {
479
+ lines.push(` ${fd.file}: ${c.path}: ${JSON.stringify(c.before)} → ${JSON.stringify(c.after)}`);
480
+ }
481
+ }
482
+ }
483
+ lines.push(` 回退: ec config restore ${wv.delta}`);
484
+ }
485
+ else {
486
+ // W == w-version,版本自身有问题
487
+ lines.push('当前配置版本无法加载。');
488
+ lines.push(' 用自检模式逐版本回落: ec start --diagnose 或 ec restart --diagnose');
489
+ }
490
+ }
491
+ else {
492
+ lines.push('No self-agent configured. Run `ec aid new <name>` to create one.');
493
+ }
494
+ if (skipped.length > 0) {
495
+ lines.push(` 跳过的目录 (${skipped.length}):`);
496
+ for (const s of skipped)
497
+ lines.push(` - ${s.dirName}: ${s.reason}`);
498
+ }
499
+ }
500
+ const msg = lines.join('\n');
501
+ logger.error(msg);
502
+ console.error(msg);
503
+ }
504
+ function readFastaunVersion() {
505
+ try {
506
+ const url = import.meta.resolve?.('@agentunion/fastaun');
507
+ if (!url)
508
+ return 'unknown';
509
+ let dir = path.dirname(fileURLToPath(url));
510
+ while (dir !== path.dirname(dir)) {
511
+ const pkgPath = path.join(dir, 'package.json');
512
+ if (fs.existsSync(pkgPath)) {
513
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
514
+ if (pkg.name === '@agentunion/fastaun')
515
+ return pkg.version || 'unknown';
516
+ }
517
+ dir = path.dirname(dir);
518
+ }
519
+ return 'unknown';
520
+ }
521
+ catch {
522
+ return 'unknown';
523
+ }
524
+ }
525
+ async function main() {
526
+ // 启动信息:目录类型 + 版本号 + 代码最新时间戳
527
+ {
528
+ const pkgRoot = getPackageRoot();
529
+ const runDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'));
530
+ const isDist = runDir.includes(path.join(pkgRoot, 'dist'));
531
+ const isLinked = fs.existsSync(path.join(pkgRoot, '.git'));
532
+ const dirType = isDist ? (isLinked ? '开发仓/dist' : '安装路径/dist') : '源码(tsx)';
533
+ const scanDir = isDist ? path.join(pkgRoot, 'dist') : path.join(pkgRoot, 'src');
534
+ let latestMtime = 0;
535
+ const scanRecursive = (dir) => {
536
+ try {
537
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
538
+ if (entry.name === 'node_modules')
539
+ continue;
540
+ const full = path.join(dir, entry.name);
541
+ if (entry.isDirectory()) {
542
+ scanRecursive(full);
543
+ continue;
544
+ }
545
+ if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
546
+ const mt = fs.statSync(full).mtimeMs;
547
+ if (mt > latestMtime)
548
+ latestMtime = mt;
549
+ }
550
+ }
551
+ }
552
+ catch { }
553
+ };
554
+ scanRecursive(scanDir);
555
+ let version = '?';
556
+ try {
557
+ version = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8')).version;
558
+ }
559
+ catch { }
560
+ const fmtTime = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; };
561
+ console.error(`[EvolCore] EvolCore v${version}`);
562
+ console.error(`[EvolCore] 执行类型: ${dirType}`);
563
+ console.error(`[EvolCore] 包路径: ${pkgRoot}`);
564
+ console.error(`[EvolCore] 代码时间: ${latestMtime ? fmtTime(latestMtime) : '?'}`);
565
+ }
566
+ // 过滤飞书 SDK 的 info 日志
567
+ const originalLog = console.log;
568
+ const originalInfo = console.info;
569
+ const filter = (...args) => {
570
+ const firstArg = String(args[0] || '');
571
+ return firstArg.includes('[info]') || firstArg.includes('[ws]');
572
+ };
573
+ console.log = (...args) => {
574
+ if (filter(...args))
575
+ return;
576
+ originalLog(...args);
577
+ };
578
+ console.info = (...args) => {
579
+ if (filter(...args))
580
+ return;
581
+ originalInfo(...args);
582
+ };
583
+ logger.info(`EvolCore v${readEvolcoreVersion()} starting... (fastaun v${readFastaunVersion()})`);
584
+ // 确保数据目录存在
585
+ ensureDataDirs();
586
+ // .env 文件已在模块顶部加载,此处不再重复加载
587
+ // ── 单实例保护(pre-check + post-write self-check)──
588
+ // pre-check:发现已有活 main 直接退出,避免起任何副作用
589
+ {
590
+ const pre = scanInstances();
591
+ const aliveOthers = pre.mains.filter(m => m.alive && m.record.pid !== process.pid);
592
+ if (aliveOthers.length > 0) {
593
+ const pids = aliveOthers.map(m => m.record.pid).join(', ');
594
+ const msg = `❌ Another EvolCore instance is already running (PID: ${pids}). Use 'ec restart' to replace it.`;
595
+ logger.error(msg);
596
+ console.error(msg);
597
+ process.exit(1);
598
+ }
599
+ }
600
+ // 在登记 daemon 实例前自动备份并迁移旧角色配置;失败才阻止启动。
601
+ try {
602
+ // Crash recovery must precede schema gates and migrations so they never
603
+ // inspect or migrate a partially applied Role Menu transaction.
604
+ recoverAllRoleMutationsSync();
605
+ const migration = await ensureRoleConfigV4OnStartup();
606
+ if (migration) {
607
+ const msg = `✓ Role config v4 migration applied automatically: ${migration.changedFiles} file(s)` +
608
+ (migration.backup ? `; backup: ${migration.backup}` : '');
609
+ logger.info(msg);
610
+ console.error(msg);
611
+ }
612
+ const domainMigration = await ensureRoleConfigV5OnStartup();
613
+ if (domainMigration) {
614
+ const msg = `✓ Standalone role config migration applied automatically: ${domainMigration.changedFiles} file(s)` +
615
+ (domainMigration.backup ? `; backup: ${domainMigration.backup}` : '');
616
+ logger.info(msg);
617
+ console.error(msg);
618
+ }
619
+ const contactMigration = await ensureContactBookV2OnStartup();
620
+ if (contactMigration) {
621
+ const msg = `✓ Contact book v2 migration applied automatically: ${contactMigration.changedFiles} file(s)` +
622
+ (contactMigration.backup ? `; backup: ${contactMigration.backup}` : '');
623
+ logger.info(msg);
624
+ console.error(msg);
625
+ }
626
+ }
627
+ catch (e) {
628
+ const msg = `❌ ${e instanceof Error ? e.message : String(e)}`;
629
+ logger.error(msg);
630
+ console.error(msg);
631
+ process.exit(1);
632
+ }
633
+ // 立即登记自己(让其他并发启动者能看见我)
634
+ const launchedBy = process.env.EVOLCORE_LAUNCHED_BY || 'start';
635
+ writeMain(launchedBy);
636
+ logger.info(`✓ Instance record written: main-${process.pid}.json`);
637
+ // post-write 自检:写完 record 后再扫一次,发现并发对手时按 (startedAt, pid) 选赢家
638
+ {
639
+ const verdict = isMainWinner();
640
+ if (!verdict.winner) {
641
+ logger.warn(`Lost main election to PID ${verdict.conflictingPid}, yielding`);
642
+ console.error(`⚠ Another instance (PID ${verdict.conflictingPid}) started concurrently and won the election. Yielding.`);
643
+ removeAll();
644
+ process.exit(0);
645
+ }
646
+ }
647
+ // ── 自动迁移 ──
648
+ migrateIdentitiesIfNeeded();
649
+ // autoMigrateIfNeeded 已随配置体系 v2 退场(fresh init,不做兼容过渡)。
650
+ // ── 配置体系初始化(schema 字段不相交硬约束校验)──
651
+ try {
652
+ initConfigManager();
653
+ }
654
+ catch (e) {
655
+ const msg = `❌ 配置 schema 校验失败: ${e instanceof Error ? e.message : String(e)}`;
656
+ logger.error(msg);
657
+ console.error(msg);
658
+ process.exit(1);
659
+ }
660
+ const configSnapshotsEnabled = isConfigSnapshotsEnabled();
661
+ // ── 自检模式(EVOLCORE_DIAGNOSE=1 由 ec start --diagnose / ec restart --diagnose 注入)──
662
+ if (process.env.EVOLCORE_DIAGNOSE === '1') {
663
+ if (!configSnapshotsEnabled) {
664
+ const msg = '配置快照功能已关闭,无法执行版本回落;请设置 configSnapshots=true';
665
+ logger.error(`[diagnose] ${msg}`);
666
+ console.error(msg);
667
+ process.exit(1);
668
+ }
669
+ logger.info('[diagnose] 进入自检模式,逐版本回落尝试...');
670
+ const result = await selfDiagnose();
671
+ if (result.ok) {
672
+ logger.info(`[diagnose] ✓ 回落到 ${result.actualVersion?.delta} 成功,继续启动。`);
673
+ // W 已展开为好版本,继续正常启动流程(不再是诊断)
674
+ }
675
+ else {
676
+ const msg = result.message ?? '✗ 自检失败:未找到可用版本。';
677
+ logger.error(msg);
678
+ console.error(msg);
679
+ process.exit(1);
680
+ }
681
+ }
682
+ const daemonCfg = loadDaemonConfig();
683
+ if (initializeEckSnapshotsConfig(daemonCfg)) {
684
+ logger.warn(`[ECK] Debug snapshots enabled at ${eckDebugDir()}; files may contain sensitive context and local paths.`);
685
+ }
686
+ // ── ECK 运行时初始化 ──
687
+ initEck();
688
+ // 加载 ECK manifest + 清理旧调试文件
689
+ cleanEckDebug();
690
+ loadKitManifest();
691
+ // 加载配置(新结构:defaults.json + per-agent config.json)
692
+ const defaults = loadDefaults() ?? { $schema_version: CONFIG_SCHEMA_VERSION };
693
+ let processLevelOwners = daemonCfg.owners ?? [];
694
+ const bindBootstrapMode = process.env.EVOLCORE_BIND_BOOTSTRAP === '1';
695
+ if (processLevelOwners.length === 0 && shouldFailFastForMissingOwners()) {
696
+ throw new Error('daemon.json.owners is required when EVOLCORE_REQUIRE_OWNERS=1');
697
+ }
698
+ // 进程级 menu 操作(/system /agent)鉴权:owners 来自 daemon.json 顶层。
699
+ // owners 为空时这些操作一律 FORBIDDEN,启动时提示如何配置。
700
+ if (processLevelOwners.length === 0) {
701
+ logger.warn('[startup] daemon.json.owners 未配置:进程级 menu 操作(/system /agent)将一律拒绝。' +
702
+ '如需远程管理,请在 daemon.json 配置 owners: [<你的 AID>]');
703
+ }
704
+ // 应用配置中的日志级别(优先于环境变量)
705
+ // logLevel 现在不在新结构中——若要保留,将来可加 defaults.debug.logLevel
706
+ // 阶段 2c 暂跳过
707
+ const paths = resolvePaths();
708
+ if (bindBootstrapMode && daemonCfg.aid && loadAllAgents().agents.length === 0) {
709
+ await runBindBootstrapDaemon(daemonCfg, defaults);
710
+ return;
711
+ }
712
+ // ── EvolAgent Registry:加载 agents/<aid>/config.json ──
713
+ const agentRegistry = new EvolAgentRegistry(paths.agentsDir);
714
+ agentRegistry.loadAll();
715
+ const agentInfos = agentRegistry.list();
716
+ if (agentInfos.length === 0) {
717
+ const skipped = agentRegistry.getSkipped();
718
+ logger.info('✓ No self-agent configured; starting Control Plane only');
719
+ if (skipped.length > 0) {
720
+ logger.warn(`[startup] skipped ${skipped.length} agent director${skipped.length === 1 ? 'y' : 'ies'} while starting empty runtime`);
721
+ for (const s of skipped)
722
+ logger.warn(` - ${s.dirName}: ${s.reason}`);
723
+ }
724
+ }
725
+ else {
726
+ logger.info(`✓ Loaded ${agentInfos.length} self-agent(s)`);
727
+ for (const info of agentInfos) {
728
+ if (info.status === 'error') {
729
+ logger.error(` ✗ ${info.name}: ${info.error}`);
730
+ }
731
+ else if (info.status === 'disabled') {
732
+ logger.info(` ○ ${info.name} (disabled)`);
733
+ }
734
+ else {
735
+ logger.info(` ● ${info.name} ${info.baseagent} @ ${path.basename(info.projectPath)}`);
736
+ }
737
+ }
738
+ }
739
+ // 跨 agent 凭证冲突
740
+ {
741
+ const dups = detectDuplicates(agentRegistry.runnableAgents());
742
+ for (const d of dups) {
743
+ const owners = d.agents.map(o => `${o.aid}(${o.channelName})`).join(', ');
744
+ logger.warn(`⚠ Duplicate channel credential: ${d.fingerprint} claimed by ${owners}.`);
745
+ }
746
+ }
747
+ // 选定主 agent(启动期 anthropic resolve 用,配合 IPC `evolagent.list` 显示)。
748
+ // 空 runtime 下没有 primary agent,Control Plane 仍继续启动。
749
+ const primaryAgent = agentRegistry.runnableAgents()[0];
750
+ let agentRuntimeState = primaryAgent ? 'starting' : 'empty';
751
+ let agentRuntimeError;
752
+ if (!primaryAgent && agentInfos.length > 0) {
753
+ agentRuntimeState = 'error';
754
+ agentRuntimeError = 'No runnable self-agent (all are error/disabled).';
755
+ logger.warn(`[startup] ${agentRuntimeError} Control Plane will remain available.`);
756
+ }
757
+ // 进程级设置:idleMonitor 属于 daemon.json;debug 继续沿用 defaults 的现有行为。
758
+ const globalSettings = {
759
+ idleMonitor: daemonCfg.idleMonitor,
760
+ debug: defaults.debug,
761
+ };
762
+ if (globalSettings.debug?.logLevel) {
763
+ setLogLevel(globalSettings.debug.logLevel);
764
+ }
765
+ // 启动期 anthropic 凭证校验已移除:runner 创建时由 AgentLoader 错误处理
766
+ logger.info('✓ Config loaded');
767
+ // Store for IPC access (T10 will wire this)
768
+ // M4: removed dead globalThis.__evolcore_agentRegistry assignment
769
+ // 创建事件总线
770
+ const eventBus = new EventBus();
771
+ logger.info('✓ Event bus initialized');
772
+ // 把所有事件录到 events.log(受 EVENT_LOG 环境变量控制)
773
+ eventBus.subscribeAll((event) => logger.event(event));
774
+ eventBus.subscribe('agent:updated', (event) => {
775
+ if (event.nameChanged)
776
+ agentRegistry?.invalidateAgentDisplayCache?.(event.aid);
777
+ });
778
+ // 统计收集器(近 1 小时滚动统计)
779
+ const statsCollector = new StatsCollector(eventBus);
780
+ // Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
781
+ const aidStatsCollector = new AidStatsCollector(eventBus);
782
+ // 持久化网络流量到 message_events 表
783
+ aidStatsCollector.onMessage = (ev) => {
784
+ import('./stats/writer.js').then(({ insertMessageEvent }) => {
785
+ insertMessageEvent(paths.root, ev);
786
+ }).catch(() => { });
787
+ };
788
+ // 日聚合表 usage_daily:首次启动回填 + 每日自愈。
789
+ // 首次:表为空但明细非空时全量回填历史数据;之后靠 writer 写时增量维护。
790
+ // 自愈:每日全量重建一次,纠正任何写时漂移。
791
+ import('./stats/db.js').then(({ getDb, rebuildDailyRollup }) => {
792
+ const db = getDb(paths.root);
793
+ if (!db)
794
+ return;
795
+ try {
796
+ const daily = db.prepare('SELECT COUNT(*) AS n FROM usage_daily').get();
797
+ const events = db.prepare('SELECT COUNT(*) AS n FROM usage_events').get();
798
+ if (daily.n === 0 && events.n > 0) {
799
+ logger.info('[Stats] usage_daily 为空,回填历史数据…');
800
+ rebuildDailyRollup(paths.root);
801
+ }
802
+ }
803
+ catch (e) {
804
+ logger.warn(`[Stats] usage_daily 回填检测失败(非致命): ${e}`);
805
+ }
806
+ // 每日自愈(24h),纠正写时增量漂移。
807
+ setInterval(() => {
808
+ try {
809
+ rebuildDailyRollup(paths.root);
810
+ }
811
+ catch (e) {
812
+ logger.warn(`[Stats] usage_daily 自愈失败(非致命): ${e}`);
813
+ }
814
+ }, 24 * 60 * 60 * 1000);
815
+ }).catch(() => { });
816
+ // 初始化 SessionManager(文件系统后端)
817
+ const resolveSessionIdentity = (channel, userId, chatType, conversationId) => {
818
+ const parsed = tryParseChannelKey(channel);
819
+ const owningAgent = agentRegistry.resolveByChannel(channel);
820
+ const selfAid = owningAgent?.aid ?? parsed?.selfAID;
821
+ if (!selfAid || !userId)
822
+ return { role: 'none', mode: 'interactive' };
823
+ const actualChatType = chatType || 'private';
824
+ // 群聊角色需按群 ID 命中群成员角色表;私聊按 userId。缺省回退到 userId 保持旧行为。
825
+ const actualConversationId = actualChatType === 'group'
826
+ ? (conversationId || userId)
827
+ : userId;
828
+ const detail = resolvePeerRoleDetail({
829
+ selfAid,
830
+ channelKey: channel,
831
+ channelType: parsed?.type || channel,
832
+ chatType: actualChatType,
833
+ actorId: userId,
834
+ conversationId: actualConversationId,
835
+ });
836
+ return roleToSessionIdentity(detail.effectiveRole);
837
+ };
838
+ const removedLegacyControlSessions = cleanupLegacySystemControlSessions(paths.sessionsDir);
839
+ if (removedLegacyControlSessions.length > 0) {
840
+ logger.info(`[startup] Removed legacy synthetic control sessions: ${removedLegacyControlSessions.join(', ')}`);
841
+ }
842
+ const sessionManager = new SessionManager(paths.sessionsDir, eventBus, resolveSessionIdentity);
843
+ logger.info('✓ Database initialized');
844
+ // 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
845
+ sessionManager.registerFileAdapter(new ClaudeSessionFileAdapter());
846
+ sessionManager.registerFileAdapter(new CodexSessionFileAdapter());
847
+ sessionManager.registerFileAdapter(new GeminiSessionFileAdapter());
848
+ // Agent 插件系统:每个 EvolAgent × 每个 baseagent 一个独立 runner(H1/H2 修复)
849
+ const agentLoader = new AgentLoader();
850
+ agentLoader.register(new ClaudeAgentPlugin());
851
+ agentLoader.register(new CodexAgentPlugin());
852
+ agentLoader.register(new GeminiAgentPlugin());
853
+ const agentInstances = agentLoader.createAll(agentRegistry, {
854
+ onSessionIdUpdate: async (sessionId, agentSessionId) => {
855
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
856
+ },
857
+ });
858
+ // agentMap 复合键:${aid}::${baseagent}
859
+ const agentMap = new Map();
860
+ for (const inst of agentInstances) {
861
+ agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
862
+ }
863
+ const primaryBaseagent = primaryAgent?.baseagent ?? 'claude';
864
+ let primaryRunnerKey = primaryAgent ? `${primaryAgent.aid}::${primaryBaseagent}` : '<empty>::claude';
865
+ const agentRunner = agentMap.get(primaryRunnerKey) || agentInstances[0]?.agent;
866
+ if (primaryAgent && !agentRunner) {
867
+ agentRuntimeState = 'error';
868
+ agentRuntimeError = 'No agent backend available. Check baseagents config (no runners created).';
869
+ primaryAgent.status = 'error';
870
+ primaryAgent.error = agentRuntimeError;
871
+ logger.error(agentRuntimeError);
872
+ }
873
+ else if (!primaryAgent) {
874
+ logger.info('✓ Agent Runtime empty; no runners created');
875
+ }
876
+ else {
877
+ logger.info(`✓ Runners ready (primary key: ${primaryRunnerKey}, total: ${agentMap.size}, keys: ${[...agentMap.keys()].join(', ')})`);
878
+ }
879
+ // 权限审批网关
880
+ const permissionGateway = new PermissionGateway();
881
+ permissionGateway.setEventBus(eventBus);
882
+ onConfigWrite(({ target, selector }) => {
883
+ if ((target !== ConfigTarget.Agent
884
+ && target !== ConfigTarget.Contact
885
+ && target !== ConfigTarget.Relation) || !selector.self)
886
+ return;
887
+ const cancelled = permissionGateway.revalidatePendingApprovals(selector.self);
888
+ if (cancelled > 0) {
889
+ logger.info(`[PermissionGateway] Cancelled ${cancelled} stale management approval(s) after ${target} config write: agent=${selector.self}`);
890
+ }
891
+ });
892
+ eventBus.subscribe('task:interrupted', (event) => {
893
+ if ('sessionId' in event && event.sessionId) {
894
+ permissionGateway.cancelAll(event.sessionId, event.reason || 'interrupted');
895
+ }
896
+ });
897
+ eventBus.subscribe('task:error', (event) => {
898
+ if ('sessionId' in event && event.sessionId) {
899
+ permissionGateway.cancelAll(event.sessionId, 'task_error', false);
900
+ }
901
+ });
902
+ eventBus.subscribe('task:completed', (event) => {
903
+ if ('sessionId' in event && event.sessionId) {
904
+ permissionGateway.cancelAll(event.sessionId, 'task_completed', false);
905
+ }
906
+ });
907
+ // 交互路由器
908
+ const interactionRouter = new InteractionRouter();
909
+ // 为所有支持权限的 agent 设置 gateway
910
+ for (const inst of agentInstances) {
911
+ inst.agent.setPermissionGateway?.(permissionGateway);
912
+ }
913
+ // 创建消息缓存
914
+ const messageCache = new MessageCache();
915
+ logger.info('✓ Message cache initialized');
916
+ // 定期清理过期消息(每小时)
917
+ setInterval(() => {
918
+ messageCache.cleanupExpired();
919
+ }, 60 * 60 * 1000);
920
+ // 渠道插件系统
921
+ const channelLoader = new ChannelLoader();
922
+ channelLoader.register(new FeishuChannelPlugin());
923
+ channelLoader.register(new WechatChannelPlugin());
924
+ channelLoader.register(new AUNChannelPlugin());
925
+ channelLoader.register(new DingtalkChannelPlugin());
926
+ channelLoader.register(new QQBotChannelPlugin());
927
+ channelLoader.register(new WecomChannelPlugin());
928
+ // Create channel instances: 每个 self-agent 各自的 channels
929
+ const evolagentInstances = [];
930
+ for (const agent of agentRegistry.runnableAgents()) {
931
+ try {
932
+ const instances = await channelLoader.createForAgent(agent);
933
+ evolagentInstances.push(...instances);
934
+ }
935
+ catch (e) {
936
+ logger.error(`[Agent ${agent.aid}] Failed to create channels: ${e}`);
937
+ agent.status = 'error';
938
+ agent.error = `Channel creation failed: ${e}`;
939
+ }
940
+ }
941
+ const channelInstances = evolagentInstances;
942
+ logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
943
+ const bindService = daemonCfg.aid
944
+ ? new BindService({
945
+ receiverAid: daemonCfg.aid,
946
+ getAvailableBaseagents: detectAvailableBaseagentsForBind,
947
+ getUptimeSeconds: () => Math.floor(process.uptime()),
948
+ onDaemonOwnersUpdated: (owners) => { processLevelOwners = owners; },
949
+ })
950
+ : null;
951
+ bindService?.startCleanup();
952
+ const agentDelegationRegistry = new AgentDelegationRegistry();
953
+ // 创建命令处理器
954
+ const cmdHandler = new CommandHandler(sessionManager, agentMap, messageCache, eventBus, primaryRunnerKey);
955
+ cmdHandler.setAgentDelegationRegistry(agentDelegationRegistry);
956
+ cmdHandler.setPermissionGateway(permissionGateway);
957
+ cmdHandler.setInteractionRouter(interactionRouter);
958
+ cmdHandler.setStatsCollector(statsCollector);
959
+ // 创建消息处理器
960
+ // 默认使用 ResponseEngine(插件化引擎)。
961
+ // MessageProcessor(旧引擎)保留为参考真相,不删除但不再使用。
962
+ const responseEngine = new ResponseEngine(agentMap, sessionManager, globalSettings, messageCache, eventBus, (content, channel, channelId, userId, threadId) => {
963
+ const sendFn = async (id, text, opts) => {
964
+ const adapter = cmdHandler.getAdapter(channel);
965
+ if (!adapter)
966
+ return;
967
+ if (text) {
968
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: id, replyContext: opts }), { kind: 'system.notice', text, subtype: 'health' });
969
+ }
970
+ };
971
+ return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
972
+ }, primaryRunnerKey);
973
+ responseEngine.setAgentDelegationRegistry(agentDelegationRegistry);
974
+ const processor = responseEngine;
975
+ // 回填 processor 和 messageQueue 的引用
976
+ cmdHandler.setProcessor(processor);
977
+ // Inject EvolAgentRegistry (methods added by T6/T7)
978
+ if (processor.setAgentRegistry) {
979
+ processor.setAgentRegistry(agentRegistry);
980
+ }
981
+ if (cmdHandler.setAgentRegistry) {
982
+ cmdHandler.setAgentRegistry(agentRegistry);
983
+ }
984
+ // 设置交互路由器
985
+ processor.setInteractionRouter(interactionRouter);
986
+ // 设置 compact 开始回调(对所有支持的 agent)
987
+ for (const inst of agentInstances) {
988
+ inst.agent.setCompactStartCallback?.((sessionId) => {
989
+ processor.handleCompactStart(sessionId);
990
+ });
991
+ }
992
+ // 创建消息队列
993
+ const messageQueue = new MessageQueue(async (message) => {
994
+ await processor.processMessage(message);
995
+ }, {
996
+ persistencePath: path.join(resolvePaths().dataDir, 'message-queue.json'),
997
+ });
998
+ // 设置中断回调(精确中断正在处理的 agent)
999
+ messageQueue.setInterruptCallback(async (sessionKey, baseagent, evolagentName, reason = 'new_message') => {
1000
+ void baseagent;
1001
+ void evolagentName;
1002
+ await responseEngine.interruptSession(sessionKey, reason);
1003
+ });
1004
+ messageQueue.setEventBus(eventBus);
1005
+ // 进程退出时立即刷盘队列状态(防止 debounce 期间的消息丢失)
1006
+ onShutdown(() => messageQueue.persistQueuesImmediate());
1007
+ // 回填 messageQueue 引用
1008
+ cmdHandler.setMessageQueue(messageQueue);
1009
+ processor.setMessageQueue(messageQueue);
1010
+ const taskExecutionTtlMs = daemonConversationTotalExecutionMs(globalSettings);
1011
+ const handoffRuntime = new HandoffRuntime(sessionManager, messageQueue, async (handoff) => {
1012
+ const targetSession = await sessionManager.getSessionById(handoff.target_session_id);
1013
+ if (!targetSession)
1014
+ return { ok: false, error: 'target session not found' };
1015
+ const inst = channelInstances.find(candidate => (candidate.channelType === 'aun' && (candidate.adapter.channelKey === targetSession.metadata?.channelKey
1016
+ || candidate.adapter.channelName === targetSession.channel
1017
+ || tryParseChannelKey(candidate.adapter.channelKey)?.selfAID === targetSession.selfAID)));
1018
+ const channel = inst?.channel;
1019
+ if (!channel) {
1020
+ return { ok: false, error: `AUN channel not found for ${targetSession.selfAID || '<unknown>'}` };
1021
+ }
1022
+ const classified = classifyAunPayloadForLog(handoff.request.payload);
1023
+ const log = {
1024
+ content: classified.content,
1025
+ sessionId: targetSession.id,
1026
+ threadId: targetSession.threadId || null,
1027
+ msgType: classified.msgType,
1028
+ payloadType: classified.payloadType,
1029
+ payloadSummary: classified.payloadSummary,
1030
+ source: 'msg',
1031
+ handoffTrace: { version: 2, handoff_id: handoff.handoff_id },
1032
+ };
1033
+ const result = targetSession.chatType === 'group'
1034
+ ? typeof channel.sendDaemonGroupMsg === 'function'
1035
+ ? await channel.sendDaemonGroupMsg({
1036
+ groupId: targetSession.channelId,
1037
+ payload: { ...handoff.request.payload, [AUN_HANDOFF_MARKER_FIELD]: handoff.handoff_id },
1038
+ encrypt: handoff.request.encrypt,
1039
+ log,
1040
+ })
1041
+ : { ok: false, error: 'AUN channel does not support daemon group sends' }
1042
+ : typeof channel.sendDaemonMsg === 'function'
1043
+ ? await channel.sendDaemonMsg({
1044
+ to: targetSession.channelId,
1045
+ payload: handoff.request.payload,
1046
+ encrypt: handoff.request.encrypt,
1047
+ causation: handoff.causation,
1048
+ log,
1049
+ })
1050
+ : { ok: false, error: 'AUN channel does not support daemon private sends' };
1051
+ return { ok: result.ok, message_id: result.message_id, error: result.error };
1052
+ }, undefined, { queueTtlMs: taskExecutionTtlMs });
1053
+ responseEngine.setHandoffRuntime(handoffRuntime);
1054
+ // Trigger runtime: daemon-level script + feedback scheduler.
1055
+ const triggerSchedulers = new Map();
1056
+ const triggerAudit = new TriggerAuditLogger();
1057
+ const triggerScriptExecutor = new TriggerScriptExecutor();
1058
+ const daemonChannel = new DaemonChannel(sessionManager, messageQueue, {
1059
+ watchdogMs: daemonConversationWatchdogMs(globalSettings),
1060
+ totalExecutionMs: taskExecutionTtlMs,
1061
+ });
1062
+ const triggerControlToken = crypto.randomBytes(32).toString('base64url');
1063
+ fs.writeFileSync(resolvePaths().controlToken, triggerControlToken, { mode: 0o600 });
1064
+ try {
1065
+ fs.chmodSync(resolvePaths().controlToken, 0o600);
1066
+ }
1067
+ catch { }
1068
+ const triggerStartupAgents = [...agentRegistry.runnableAgents()];
1069
+ const controlAid = daemonCfg.aid || '__daemon__';
1070
+ const daemonTriggerOwner = {
1071
+ aid: triggerStartupAgents.some(agent => agent.aid === controlAid) ? `${controlAid}#daemon` : controlAid,
1072
+ originPeerId: controlAid,
1073
+ baseagent: primaryAgent?.baseagent || 'codex',
1074
+ projectPath: primaryAgent?.projectPath || process.cwd(),
1075
+ };
1076
+ const triggerBaseagentsForAgent = (agentAid) => {
1077
+ const ownerName = agentRegistry.get(agentAid)?.name
1078
+ ?? (agentAid === daemonTriggerOwner.aid ? primaryAgent?.name : undefined);
1079
+ if (!ownerName)
1080
+ return [];
1081
+ const prefix = `${ownerName}::`;
1082
+ return [...agentMap.keys()]
1083
+ .filter(key => key.startsWith(prefix))
1084
+ .map(key => key.slice(prefix.length));
1085
+ };
1086
+ const materializeTriggerCreateBaseagent = (input, agentAid) => {
1087
+ const rawExecution = input.execution;
1088
+ if (!rawExecution || typeof rawExecution !== 'object' || Array.isArray(rawExecution))
1089
+ return input;
1090
+ const execution = rawExecution;
1091
+ if (execution.type !== 'trigger_session') {
1092
+ if (execution.baseagent !== undefined) {
1093
+ throw new Error('execution.baseagent is only allowed when execution.type=trigger_session');
1094
+ }
1095
+ return input;
1096
+ }
1097
+ if (execution.baseagent !== undefined
1098
+ && (typeof execution.baseagent !== 'string' || !execution.baseagent.trim())) {
1099
+ throw new Error('execution.baseagent must be a non-empty string');
1100
+ }
1101
+ const baseagent = typeof execution.baseagent === 'string'
1102
+ ? execution.baseagent.trim()
1103
+ : agentRegistry.get(agentAid)?.baseagent
1104
+ ?? (agentAid === daemonTriggerOwner.aid ? daemonTriggerOwner.baseagent : undefined);
1105
+ if (!baseagent)
1106
+ throw new Error('unable to resolve Trigger session baseagent');
1107
+ const available = triggerBaseagentsForAgent(agentAid);
1108
+ if (!available.includes(baseagent)) {
1109
+ throw new Error(`Trigger baseagent unavailable for ${agentAid}: ${baseagent} (available: ${available.join(', ') || 'none'})`);
1110
+ }
1111
+ return { ...input, execution: { ...execution, baseagent } };
1112
+ };
1113
+ const getTriggerChannel = (agentAid, channelKey) => {
1114
+ if (agentAid === daemonTriggerOwner.aid && channelKey === daemonChannel.channelKey) {
1115
+ return {
1116
+ adapter: daemonChannel,
1117
+ agentAid,
1118
+ agentName: daemonTriggerOwner.aid,
1119
+ projectPath: daemonTriggerOwner.projectPath,
1120
+ baseagent: daemonTriggerOwner.baseagent,
1121
+ };
1122
+ }
1123
+ const agent = agentRegistry.get(agentAid);
1124
+ if (!agent)
1125
+ return undefined;
1126
+ const inst = channelInstances.find((candidate) => (candidate.adapter.channelKey === channelKey
1127
+ || candidate.adapter.channelName === channelKey));
1128
+ const parsed = inst ? tryParseChannelKey(inst.adapter.channelKey) : null;
1129
+ if (inst && parsed?.selfAID !== agentAid)
1130
+ return undefined;
1131
+ if (!inst)
1132
+ return undefined;
1133
+ return {
1134
+ adapter: inst.adapter,
1135
+ agentAid,
1136
+ agentName: agent.aid,
1137
+ projectPath: agent.projectPath,
1138
+ baseagent: agent.baseagent,
1139
+ };
1140
+ };
1141
+ const validateTriggerFeedbackChannels = (definition) => {
1142
+ if (definition.feedback.strategy === 'silent')
1143
+ return;
1144
+ const target = definition.feedback.strategy === 'target'
1145
+ ? definition.feedback.target
1146
+ : definition.origin;
1147
+ if (target && !getTriggerChannel(definition.agentAid, target.channelKey)) {
1148
+ throw new Error(`agent ${definition.agentAid} has no configured channel ${target.channelKey}`);
1149
+ }
1150
+ };
1151
+ const validateTriggerDefinitionForActor = (definition, actor) => {
1152
+ if (definition.execution.type === 'script' && !actor.management) {
1153
+ throw new Error('only owner/admin may create or update a script trigger');
1154
+ }
1155
+ if (definition.execution.permissionMode) {
1156
+ const permissionDecision = constrainRuntimePermissionMode({
1157
+ selfAid: definition.agentAid,
1158
+ role: actor.role,
1159
+ requestedValue: definition.execution.permissionMode,
1160
+ });
1161
+ if (permissionDecision.constrained) {
1162
+ throw new Error(`trigger permissionMode ${definition.execution.permissionMode} exceeds current role ${actor.role}`);
1163
+ }
1164
+ }
1165
+ if (definition.execution.model) {
1166
+ const agent = agentRegistry.get(definition.agentAid);
1167
+ const modelDecision = validateModelSelectionForRole({
1168
+ role: actor.role,
1169
+ baseagent: definition.execution.baseagent ?? agent?.baseagent,
1170
+ requestedModel: definition.execution.model,
1171
+ selfAid: definition.agentAid,
1172
+ });
1173
+ if (!modelDecision.ok)
1174
+ throw new Error(modelDecision.message || 'trigger model is not allowed by current role');
1175
+ }
1176
+ if (definition.execution.effort) {
1177
+ const baseagent = definition.execution.baseagent
1178
+ ?? agentRegistry.get(definition.agentAid)?.baseagent
1179
+ ?? 'claude';
1180
+ const effortDecision = validateRuntimeStringFieldOverride({
1181
+ selfAid: definition.agentAid,
1182
+ role: actor.role,
1183
+ field: `baseagents.${baseagent}.${baseagent === 'codex' ? 'reasoning' : 'effort'}`,
1184
+ value: definition.execution.effort,
1185
+ });
1186
+ if (!effortDecision.ok)
1187
+ throw new Error(effortDecision.message || 'trigger effort is not allowed by current role');
1188
+ }
1189
+ if (!actor.management && definition.feedback.strategy === 'target') {
1190
+ const target = definition.feedback.target;
1191
+ const origin = actor.origin;
1192
+ if (!target || !origin
1193
+ || target.channelKey !== origin.channelKey
1194
+ || target.channelId !== origin.channelId
1195
+ || target.session !== origin.session
1196
+ || (target.threadId ?? '') !== (origin.threadId ?? '')) {
1197
+ throw new Error('non-management trigger target must be the creator private conversation');
1198
+ }
1199
+ }
1200
+ };
1201
+ const authorizeTriggerExecution = (definition) => {
1202
+ const origin = definition.origin;
1203
+ if (!origin?.peerId)
1204
+ return { allowed: false, reason: 'trigger origin peer is missing' };
1205
+ if (origin.channelKey === 'daemon') {
1206
+ const subject = buildAuthSubject({
1207
+ selfAid: definition.agentAid,
1208
+ actorId: origin.peerId,
1209
+ channel: origin.channelKey,
1210
+ channelType: origin.channelType || 'daemon',
1211
+ channelId: origin.channelId,
1212
+ chatType: 'private',
1213
+ conversationId: origin.peerId,
1214
+ identity: roleToSessionIdentity('owner'),
1215
+ fromControlChannel: true,
1216
+ });
1217
+ const decision = authorizeOperation({
1218
+ source: 'control',
1219
+ subject,
1220
+ intent: {
1221
+ operation: 'trigger.run',
1222
+ scope: 'relation',
1223
+ source: 'control',
1224
+ args: { self: definition.agentAid, peer: origin.peerId, peerKey: subject.peerKey, triggerId: definition.id },
1225
+ },
1226
+ });
1227
+ return decision.allow ? { allowed: true } : { allowed: false, reason: decision.reason };
1228
+ }
1229
+ const channelType = origin.channelType || tryParseChannelKey(origin.channelKey)?.type;
1230
+ if (!channelType)
1231
+ return { allowed: false, reason: 'trigger origin channel type is missing' };
1232
+ const roleDetail = resolvePeerRoleDetail({
1233
+ selfAid: definition.agentAid,
1234
+ channelKey: origin.channelKey,
1235
+ channelType,
1236
+ chatType: 'private',
1237
+ actorId: origin.peerId,
1238
+ conversationId: origin.peerId,
1239
+ });
1240
+ const role = roleDetail.effectiveRole;
1241
+ if (!checkRoleAccess(role, definition.agentAid)) {
1242
+ return { allowed: false, reason: `trigger creator no longer has access (role=${role ?? 'none'})` };
1243
+ }
1244
+ const subject = buildAuthSubject({
1245
+ selfAid: definition.agentAid,
1246
+ actorId: origin.peerId,
1247
+ channel: origin.channelKey,
1248
+ channelType,
1249
+ channelId: origin.channelId,
1250
+ chatType: 'private',
1251
+ conversationId: origin.peerId,
1252
+ roleDetail,
1253
+ identity: roleToSessionIdentity(role),
1254
+ fromControlChannel: false,
1255
+ });
1256
+ const operationDecision = authorizeOperation({
1257
+ source: 'agent-tool',
1258
+ subject,
1259
+ intent: {
1260
+ operation: 'trigger.run',
1261
+ scope: 'relation',
1262
+ source: 'agent-tool',
1263
+ args: { self: definition.agentAid, peer: origin.peerId, peerKey: subject.peerKey, triggerId: definition.id },
1264
+ },
1265
+ });
1266
+ if (!operationDecision.allow) {
1267
+ return { allowed: false, reason: operationDecision.reason };
1268
+ }
1269
+ if (definition.execution.type === 'script' && !isManagementRole(role)) {
1270
+ return { allowed: false, reason: `script trigger requires current owner/admin role (role=${role ?? 'none'})` };
1271
+ }
1272
+ if (definition.feedback.strategy === 'target' && !isManagementRole(role)) {
1273
+ const target = definition.feedback.target;
1274
+ if (!target
1275
+ || target.channelKey !== origin.channelKey
1276
+ || target.channelId !== origin.channelId
1277
+ || target.session !== origin.session
1278
+ || (target.threadId ?? '') !== (origin.threadId ?? '')) {
1279
+ return { allowed: false, reason: `cross-conversation target requires current owner/admin role (role=${role ?? 'none'})` };
1280
+ }
1281
+ }
1282
+ return { allowed: true };
1283
+ };
1284
+ const authenticatedTriggerActor = async (agentAid, actorSessionId, delegationToken, controlToken) => {
1285
+ if (typeof actorSessionId !== 'string' || !actorSessionId) {
1286
+ const providedToken = typeof controlToken === 'string' ? Buffer.from(controlToken) : undefined;
1287
+ const expectedToken = Buffer.from(triggerControlToken);
1288
+ if (!providedToken
1289
+ || providedToken.length !== expectedToken.length
1290
+ || !crypto.timingSafeEqual(providedToken, expectedToken)) {
1291
+ throw new Error('valid local control token is required');
1292
+ }
1293
+ const origin = {
1294
+ channelKey: 'daemon',
1295
+ channelType: 'daemon',
1296
+ channelId: daemonTriggerOwner.originPeerId,
1297
+ session: 'main',
1298
+ peerId: daemonTriggerOwner.originPeerId,
1299
+ sessionKey: `daemon#${daemonTriggerOwner.originPeerId}#__control__`,
1300
+ };
1301
+ const subject = buildAuthSubject({
1302
+ selfAid: agentAid,
1303
+ actorId: origin.peerId,
1304
+ channel: origin.channelKey,
1305
+ channelType: origin.channelType,
1306
+ channelId: origin.channelId,
1307
+ chatType: 'private',
1308
+ conversationId: origin.peerId,
1309
+ identity: roleToSessionIdentity('owner'),
1310
+ fromControlChannel: true,
1311
+ });
1312
+ return {
1313
+ origin,
1314
+ management: true,
1315
+ control: true,
1316
+ role: 'owner',
1317
+ subject,
1318
+ };
1319
+ }
1320
+ const validation = agentDelegationRegistry.validate(typeof delegationToken === 'string' ? delegationToken : undefined, actorSessionId);
1321
+ if (!validation.ok)
1322
+ throw new Error(validation.reason);
1323
+ if (validation.grant.selfAid !== agentAid) {
1324
+ throw new Error('trigger agent does not match authenticated task agent');
1325
+ }
1326
+ const actorSession = await sessionManager.getSessionById(actorSessionId);
1327
+ const actorOrigin = actorSession ? originFromActorSession(actorSession, validation.grant.actorId, validation.grant.channel, validation.grant.chatType) : undefined;
1328
+ if (!actorSession || !actorOrigin?.peerId)
1329
+ throw new Error('trigger actor session has no authenticated peer');
1330
+ if (validation.grant.actorId !== actorOrigin.peerId) {
1331
+ throw new Error('trigger delegation actor does not match session peer');
1332
+ }
1333
+ const roleDetail = resolvePeerRoleDetail({
1334
+ selfAid: validation.grant.selfAid,
1335
+ channelKey: validation.grant.channel,
1336
+ channelType: validation.grant.channelType,
1337
+ chatType: 'private',
1338
+ actorId: validation.grant.actorId,
1339
+ conversationId: validation.grant.actorId,
1340
+ });
1341
+ const currentRole = roleDetail.effectiveRole;
1342
+ if (!checkRoleAccess(currentRole, validation.grant.selfAid)) {
1343
+ throw new Error('trigger actor no longer has access to this agent');
1344
+ }
1345
+ const subject = buildAuthSubject({
1346
+ selfAid: validation.grant.selfAid,
1347
+ actorId: validation.grant.actorId,
1348
+ channel: validation.grant.channel,
1349
+ channelType: validation.grant.channelType,
1350
+ channelId: actorOrigin.channelId,
1351
+ chatType: 'private',
1352
+ conversationId: validation.grant.actorId,
1353
+ roleDetail,
1354
+ identity: roleToSessionIdentity(currentRole),
1355
+ fromControlChannel: false,
1356
+ });
1357
+ return {
1358
+ origin: actorOrigin,
1359
+ management: isManagementRole(currentRole),
1360
+ selfAid: validation.grant.selfAid,
1361
+ control: false,
1362
+ role: currentRole || 'none',
1363
+ subject,
1364
+ };
1365
+ };
1366
+ const startTriggerScheduler = async (owner, opts = {}) => {
1367
+ await triggerSchedulers.get(owner.aid)?.stop();
1368
+ const manager = new TriggerDefinitionManager(owner.aid);
1369
+ if (opts.seedUpgradeCheck) {
1370
+ seedUpgradeCheckTrigger(manager, owner);
1371
+ }
1372
+ else {
1373
+ removeLegacyAgentUpgradeCheck(manager);
1374
+ }
1375
+ const state = new TriggerRunStateStore(manager);
1376
+ const agentTriggerAudit = triggerAudit.withHistory(manager.history);
1377
+ const dispatcher = new TriggerFeedbackDispatcher({
1378
+ getChannel: getTriggerChannel,
1379
+ sessionManager,
1380
+ messageQueue,
1381
+ eventBus,
1382
+ tryHandoff: async (input, text, target, binding) => {
1383
+ if (input.signal?.aborted) {
1384
+ throw input.signal.reason instanceof Error ? input.signal.reason : new Error('trigger feedback cancelled');
1385
+ }
1386
+ if (tryParseChannelKey(target.channelKey)?.type !== 'aun')
1387
+ return null;
1388
+ const inst = channelInstances.find(candidate => (candidate.channelType === 'aun'
1389
+ && (candidate.adapter.channelKey === target.channelKey || candidate.adapter.channelName === target.channelKey)
1390
+ && tryParseChannelKey(candidate.adapter.channelKey)?.selfAID === input.trigger.agentAid));
1391
+ const channel = inst?.channel;
1392
+ if (!channel || typeof channel.isGroupId !== 'function')
1393
+ return null;
1394
+ const targetChatType = channel.isGroupId(target.channelId) ? 'group' : 'private';
1395
+ if (targetChatType === 'group' && typeof channel.sendDaemonGroupMsg !== 'function')
1396
+ return null;
1397
+ if (targetChatType === 'private' && typeof channel.sendDaemonMsg !== 'function')
1398
+ return null;
1399
+ const originSession = await daemonChannel.getOrCreateConversationSession(input.trigger, binding.projectPath, binding.baseagent || owner.baseagent, input.runId);
1400
+ if (input.signal?.aborted) {
1401
+ throw input.signal.reason instanceof Error ? input.signal.reason : new Error('trigger feedback cancelled');
1402
+ }
1403
+ const payload = { type: 'text', text };
1404
+ if (target.session === 'thread' && target.threadId)
1405
+ payload.thread_id = target.threadId;
1406
+ const created = await handoffRuntime.createOutbound({
1407
+ selfAid: input.trigger.agentAid,
1408
+ to: target.channelId,
1409
+ originSessionId: originSession.id,
1410
+ originMessageId: input.runId,
1411
+ payload,
1412
+ encrypt: false,
1413
+ thread: target.session === 'thread' ? target.threadId : undefined,
1414
+ targetChatType,
1415
+ explicitReturnPolicy: 'none',
1416
+ causation: input.causation,
1417
+ signal: input.signal,
1418
+ });
1419
+ return created.crossSession && created.handoff
1420
+ ? { handoffId: created.handoff.handoff_id, targetSessionId: created.targetSession.id }
1421
+ : null;
1422
+ },
1423
+ });
1424
+ const scheduler = new TriggerRuntimeScheduler(manager, state, agentTriggerAudit, triggerScriptExecutor, dispatcher, daemonChannel, {
1425
+ projectPath: owner.projectPath,
1426
+ baseagent: owner.baseagent,
1427
+ getBaseagent: () => agentRegistry.get(owner.aid)?.baseagent || owner.baseagent,
1428
+ authorizeExecution: authorizeTriggerExecution,
1429
+ }, eventBus);
1430
+ triggerSchedulers.set(owner.aid, scheduler);
1431
+ try {
1432
+ await scheduler.init();
1433
+ }
1434
+ catch (err) {
1435
+ logger.error(`[Trigger] Scheduler init failed for ${owner.aid}: ${err}`);
1436
+ }
1437
+ };
1438
+ cmdHandler.setTriggerSchedulerResolver((agentAid) => triggerSchedulers.get(agentAid));
1439
+ const startedTriggerAgents = new Set();
1440
+ const ensureTriggerSchedulerStarted = async (agent) => {
1441
+ if (startedTriggerAgents.has(agent.aid))
1442
+ return;
1443
+ startedTriggerAgents.add(agent.aid);
1444
+ await startTriggerScheduler(agent);
1445
+ };
1446
+ const ensureDaemonTriggerSchedulerStarted = async () => {
1447
+ if (startedTriggerAgents.has(daemonTriggerOwner.aid))
1448
+ return;
1449
+ startedTriggerAgents.add(daemonTriggerOwner.aid);
1450
+ await startTriggerScheduler(daemonTriggerOwner, { seedUpgradeCheck: true });
1451
+ };
1452
+ // 默认策略
1453
+ const defaultPolicy = {
1454
+ canSwitchProject: (chatType, role) => chatType === 'private' ? isManagementRole(role) : role === 'owner',
1455
+ canListProjects: (chatType, role) => chatType === 'private' ? isManagementRole(role) : role === 'owner',
1456
+ canCreateSession: () => true,
1457
+ canDeleteSession: (chatType, role) => chatType === 'private' ? isManagementRole(role) : role === 'owner',
1458
+ canImportCliSession: (chatType, role) => chatType === 'private' ? isManagementRole(role) : role === 'owner',
1459
+ messagePrefix: () => '',
1460
+ showMiddleResult: () => true,
1461
+ showIdleMonitor: () => true,
1462
+ accumulateErrors: () => true,
1463
+ };
1464
+ processor.registerChannel(daemonChannel, defaultPolicy, { channelType: 'daemon' });
1465
+ cmdHandler.registerAdapter(daemonChannel);
1466
+ cmdHandler.registerChannel(daemonChannel.channelName, daemonChannel, 'daemon');
1467
+ // ── MessageBridge:Channel ↔ Core 消息桥梁 ──
1468
+ const defaultProjectPath = primaryAgent?.projectPath
1469
+ ?? defaults.projects?.defaultPath
1470
+ ?? path.join(paths.root, 'projects', 'default');
1471
+ const msgBridge = new MessageBridge(defaultProjectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, primaryAgent?.config.debounce, () => processLevelOwners);
1472
+ msgBridge.setAgentRegistry(agentRegistry);
1473
+ const bootstrapService = new BootstrapService(agentRegistry, eventBus);
1474
+ msgBridge.setBootstrapService(bootstrapService);
1475
+ msgBridge.setHandoffRuntime(handoffRuntime);
1476
+ // ── Channel instance registration (shared by startup and hot-load) ──
1477
+ function registerChannelInstance(inst) {
1478
+ // 0. 包装 adapter.send,记录所有出站到 channel-out.log
1479
+ const originalSend = inst.adapter.send.bind(inst.adapter);
1480
+ inst.adapter.send = async (envelope, payload) => {
1481
+ logger.channelOut({ channel: inst.adapter.channelName, channelId: envelope.channelId, taskId: envelope.taskId, payload: summarizeOutboundPayload(payload) });
1482
+ const result = await originalSend(envelope, payload);
1483
+ if (shouldCountSentPayload(payload)) {
1484
+ const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelKey)
1485
+ ?? agentRegistry.resolveByChannel(inst.adapter.channelName);
1486
+ if ((inst.channelType || inst.adapter.channelName) !== 'aun') {
1487
+ try {
1488
+ const logPayload = outboundPayloadToLogText(payload);
1489
+ const session = envelope.sessionId
1490
+ ? await sessionManager.getSessionById(envelope.sessionId)
1491
+ : envelope.replyContext?.threadId
1492
+ ? await sessionManager.getThreadSession(envelope.channel ?? inst.adapter.channelName, envelope.channelId, envelope.replyContext.threadId)
1493
+ : sessionManager.getActiveSessionSync(envelope.channel ?? inst.adapter.channelName, envelope.channelId, inst.channelType || inst.adapter.channelName, owningAgent?.aid);
1494
+ if (logPayload && session) {
1495
+ const chatDir = sessionManager.getChatDir(session);
1496
+ const isGroup = session.chatType === 'group';
1497
+ const target = isGroup
1498
+ ? (session.metadata?.groupId || session.channelId)
1499
+ : (session.metadata?.peerId || envelope.channelId);
1500
+ appendMessageLog(chatDir, buildOutboundEntry({
1501
+ from: owningAgent?.aid ?? envelope.agentName ?? session.selfAID ?? 'self',
1502
+ to: String(target || envelope.channelId),
1503
+ sessionId: session.id,
1504
+ threadId: envelope.replyContext?.threadId || session.threadId || null,
1505
+ chatType: isGroup ? 'group' : 'private',
1506
+ groupId: isGroup ? String(session.metadata?.groupId || session.channelId) : null,
1507
+ msgId: `${envelope.taskId || 'out'}_${Date.now()}`,
1508
+ content: logPayload.text,
1509
+ replyTo: envelope.replyContext?.replyToMessageId ?? null,
1510
+ agent: session.baseagent ?? null,
1511
+ model: null,
1512
+ durationMs: null,
1513
+ msgType: logPayload.msgType,
1514
+ source: normalizeMessageSource(envelope.replyContext?.metadata?.source),
1515
+ chatmode: envelope.chatmode,
1516
+ }));
1517
+ }
1518
+ }
1519
+ catch (err) {
1520
+ logger.debug(`[MessageLog] Failed to write outbound channel log: ${err}`);
1521
+ }
1522
+ }
1523
+ eventBus.publish({
1524
+ type: 'message:sent',
1525
+ sessionId: envelope.sessionId ?? envelope.taskId ?? `out-${Date.now()}`,
1526
+ channel: envelope.channel ?? inst.adapter.channelName,
1527
+ channelName: inst.adapter.channelName,
1528
+ channelId: envelope.channelId,
1529
+ agentName: owningAgent?.aid ?? envelope.agentName,
1530
+ payloadKind: payload.kind,
1531
+ timestamp: Date.now(),
1532
+ });
1533
+ }
1534
+ return result;
1535
+ };
1536
+ // 1. 项目路径提供器
1537
+ if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
1538
+ inst.channel.onProjectPathRequest(async (channelId) => {
1539
+ // Effective default path: use the agent that owns this channel.
1540
+ const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
1541
+ const effectiveDefault = owningAgent?.projectPath
1542
+ ?? defaultProjectPath;
1543
+ const parsedKey = tryParseChannelKey(inst.adapter.channelKey);
1544
+ const session = await sessionManager.getOrCreateSession(inst.adapter.channelKey, channelId, effectiveDefault, undefined, undefined, undefined, undefined, undefined, owningAgent?.baseagent, parsedKey?.selfAID, parsedKey?.type);
1545
+ return path.isAbsolute(session.projectPath)
1546
+ ? session.projectPath
1547
+ : path.resolve(process.cwd(), session.projectPath);
1548
+ });
1549
+ }
1550
+ // 2. 注册 adapter、policy 和 options(注入 channelType)
1551
+ const opts = inst.channelType
1552
+ ? { ...inst.options, channelType: inst.channelType }
1553
+ : inst.options;
1554
+ processor.registerChannel(inst.adapter, inst.policy || defaultPolicy, opts);
1555
+ cmdHandler.registerAdapter(inst.adapter);
1556
+ cmdHandler.registerChannel(inst.adapter.channelName, inst.channel, inst.channelType);
1557
+ if (inst.policy) {
1558
+ cmdHandler.registerPolicy(inst.adapter.channelName, inst.policy);
1559
+ }
1560
+ // 3. 交互回调
1561
+ registerAdapterInteractions(inst.adapter, interactionRouter);
1562
+ // 4. MessageBridge 注册
1563
+ const channelType = inst.channelType || inst.adapter.channelName;
1564
+ if (inst.registerBridge) {
1565
+ inst.registerBridge(msgBridge, channelType);
1566
+ }
1567
+ // 4b. 生命周期钩子
1568
+ if (inst.registerHooks) {
1569
+ inst.registerHooks({ eventBus, sessionManager });
1570
+ }
1571
+ // 4c. 观察者模式配置读取器(AUN):从 EvolAgent 的 merged config 读 observable/owners,
1572
+ // 不另建缓存——EvolAgent 那份在启动/重启/热重载时统一更新,是唯一真相源。
1573
+ const channelForObserver = inst.channel;
1574
+ if (typeof channelForObserver.setObserverConfigResolver === 'function') {
1575
+ const channelKey = inst.adapter.channelKey;
1576
+ channelForObserver.setObserverConfigResolver(() => {
1577
+ const owningAgent = agentRegistry.resolveByChannel(channelKey);
1578
+ return {
1579
+ observable: owningAgent?.getObservable() ?? false,
1580
+ owners: owningAgent ? Array.from(new Set(owningAgent.config.owners ?? [])) : [],
1581
+ };
1582
+ });
1583
+ }
1584
+ // 5. 撤回消息 → 中断执行中任务
1585
+ inst.channel.onRecall?.((messageId) => {
1586
+ msgBridge.cancel(messageId);
1587
+ });
1588
+ }
1589
+ // ── 注册所有渠道实例 ──
1590
+ for (const inst of channelInstances) {
1591
+ registerChannelInstance(inst);
1592
+ }
1593
+ // Bind adapters to their owning agents and mark running
1594
+ for (const inst of channelInstances) {
1595
+ const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
1596
+ if (!agent || agent.status === 'error')
1597
+ continue;
1598
+ agent.channels.set(inst.adapter.channelKey, inst.adapter);
1599
+ const hasRunner = agentInstances.some(runner => runner.evolagentName === agent.aid);
1600
+ if (agent.status === 'stopped' && hasRunner) {
1601
+ agent.status = 'running';
1602
+ }
1603
+ }
1604
+ if (agentRegistry.list().some((info) => info.status === 'running')) {
1605
+ agentRuntimeState = 'running';
1606
+ agentRuntimeError = undefined;
1607
+ }
1608
+ else if (agentRegistry.runnableAgents().length === 0 && agentRegistry.list().length === 0) {
1609
+ agentRuntimeState = 'empty';
1610
+ }
1611
+ else if (agentRuntimeState === 'starting') {
1612
+ agentRuntimeState = 'error';
1613
+ agentRuntimeError = agentRuntimeError ?? 'No runnable self-agent reached running state.';
1614
+ }
1615
+ // ── 配置快照 + 启动日志(启动完毕锚点,网络无关)──────────────────
1616
+ if (configSnapshotsEnabled) {
1617
+ try {
1618
+ const isDiagnoseMode = process.env.EVOLCORE_DIAGNOSE === '1';
1619
+ // 正常启动:首次建立全量基线,后续仅在配置树变化时建立新版本。
1620
+ // 自检模式已由 selfDiagnose 展开回落版本,避免重复存档。
1621
+ const startupVersion = isDiagnoseMode ? readWVersion() : ensureStartupConfigSnapshot();
1622
+ // P3/回落成功:W==w-version → successCount++
1623
+ if (startupVersion) {
1624
+ incrementSuccessCount(startupVersion.delta);
1625
+ }
1626
+ // boot-log
1627
+ const startMethod = process.env.EVOLCORE_DIAGNOSE === '1' ? 'diagnose'
1628
+ : (process.env.EVOLCORE_LAUNCHED_BY === 'start' ? 'manual' : 'auto');
1629
+ appendBootLog({
1630
+ bootedAt: new Date().toISOString(),
1631
+ startMethod: startMethod,
1632
+ selectedVersion: readCurrent(),
1633
+ actualVersion: startupVersion,
1634
+ fellBack: !!(isDiagnoseMode && startupVersion && readCurrent()?.delta !== startupVersion.delta),
1635
+ versions: { evolcore: readEvolcoreVersion(), '@agentunion/fastaun': readFastaunVersion(), node: process.version },
1636
+ platform: `${process.platform}/${process.arch}`,
1637
+ });
1638
+ retentionCleanup();
1639
+ }
1640
+ catch (e) {
1641
+ logger.warn(`[config] startup snapshot/boot-log failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
1642
+ }
1643
+ }
1644
+ else {
1645
+ logger.info('[config] snapshots disabled by configSnapshots=false');
1646
+ }
1647
+ // 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)。
1648
+ // 必须早于 connect(),后台连接后 Feishu 可能立即收到消息。
1649
+ for (const inst of channelInstances) {
1650
+ const channelType = inst.channelType || inst.adapter.channelName;
1651
+ if (channelType === 'feishu' && 'preloadThreads' in inst.channel) {
1652
+ const threadIds = sessionManager.getKnownThreadIds(inst.adapter.channelKey);
1653
+ inst.channel.preloadThreads(threadIds);
1654
+ }
1655
+ }
1656
+ const connectedChannels = new Set();
1657
+ const onlineNoticeSent = new Set();
1658
+ const pendingFile = path.join(resolvePaths().dataDir, 'restart-pending.json');
1659
+ let pendingRestartNoticeInFlight = null;
1660
+ let pendingRestartNoticeSent = false;
1661
+ const sendOnlineNoticeForChannel = (inst) => {
1662
+ const name = inst.adapter.channelName;
1663
+ const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey) ?? agentRegistry.resolveByChannel(name);
1664
+ if (!agent)
1665
+ return;
1666
+ if (!agent.config.debug?.upmsg)
1667
+ return;
1668
+ const ownerAid = getFirstStaticAgentOwner(agent.aid);
1669
+ if (!ownerAid)
1670
+ return;
1671
+ const noticeKey = `${agent.aid}#${name}`;
1672
+ if (onlineNoticeSent.has(noticeKey))
1673
+ return;
1674
+ onlineNoticeSent.add(noticeKey);
1675
+ setTimeout(() => {
1676
+ const adapter = agent.channels.get(inst.adapter.channelKey) ?? agent.channels.get(name);
1677
+ if (!adapter)
1678
+ return;
1679
+ let agentName = agent.aid;
1680
+ try {
1681
+ const mdPath = agentMdPath(agent.aid);
1682
+ const content = fs.readFileSync(mdPath, 'utf-8');
1683
+ const nameMatch = content.match(/^name:\s*"?([^"\n]+)/m);
1684
+ if (nameMatch)
1685
+ agentName = nameMatch[1].trim().replace(/"$/, '');
1686
+ }
1687
+ catch { }
1688
+ const projectDir = path.basename(agent.projectPath);
1689
+ const text = `✓ ${agentName} 已上线 | 工作目录: ${projectDir}`;
1690
+ const envelope = buildEnvelope({
1691
+ taskId: `system-online-${crypto.randomBytes(5).toString('hex')}`,
1692
+ channel: adapter.channelName,
1693
+ channelId: ownerAid,
1694
+ agentName,
1695
+ });
1696
+ sendSystemPayload(adapter, envelope, {
1697
+ kind: 'system.notice',
1698
+ text,
1699
+ subtype: 'restarted',
1700
+ }).catch(() => { });
1701
+ }, 1000 + Math.random() * 2000);
1702
+ };
1703
+ const trySendPendingRestartNotice = async () => {
1704
+ if (pendingRestartNoticeSent)
1705
+ return;
1706
+ if (pendingRestartNoticeInFlight) {
1707
+ try {
1708
+ await pendingRestartNoticeInFlight;
1709
+ }
1710
+ catch {
1711
+ // The first caller logs the actual send/read error.
1712
+ }
1713
+ return;
1714
+ }
1715
+ if (!fs.existsSync(pendingFile))
1716
+ return;
1717
+ pendingRestartNoticeInFlight = (async () => {
1718
+ if (pendingRestartNoticeSent)
1719
+ return;
1720
+ if (!fs.existsSync(pendingFile))
1721
+ return;
1722
+ const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
1723
+ const adapter = cmdHandler.getAdapter(pending.channel)
1724
+ ?? channelInstances.find(inst => inst.adapter.channelKey === pending.channel)?.adapter;
1725
+ if (!adapter)
1726
+ return;
1727
+ if (!connectedChannels.has(pending.channel)
1728
+ && !connectedChannels.has(adapter.channelName)
1729
+ && !connectedChannels.has(adapter.channelKey)) {
1730
+ logger.info(`[Restart] Pending notification waits for channel connection: ${pending.channel}`);
1731
+ return;
1732
+ }
1733
+ const replyContext = pending.rootId
1734
+ ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
1735
+ : undefined;
1736
+ const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
1737
+ const envelope = buildEnvelope({
1738
+ taskId: `system-restart-${process.pid}`,
1739
+ channel: adapter.channelKey,
1740
+ channelId: pending.channelId,
1741
+ agentName: owningAgent?.aid || 'evolcore',
1742
+ replyContext,
1743
+ });
1744
+ await sendSystemPayload(adapter, envelope, {
1745
+ kind: 'system.notice',
1746
+ text: '✅ 服务重启成功!',
1747
+ subtype: 'restarted',
1748
+ });
1749
+ pendingRestartNoticeSent = true;
1750
+ fs.rmSync(pendingFile, { force: true });
1751
+ logger.info(`[Restart] Notification sent via ${pending.channel}`);
1752
+ })();
1753
+ try {
1754
+ await pendingRestartNoticeInFlight;
1755
+ }
1756
+ catch (e) {
1757
+ logger.error('[Restart] Failed to send restart notification:', e);
1758
+ }
1759
+ finally {
1760
+ pendingRestartNoticeInFlight = null;
1761
+ }
1762
+ };
1763
+ const summarizeConnectedChannels = (connected) => {
1764
+ const connectedTypeCount = new Map();
1765
+ const typeOrder = [];
1766
+ for (const inst of channelInstances) {
1767
+ const name = inst.adapter.channelName;
1768
+ if (!connected.includes(name))
1769
+ continue;
1770
+ const type = inst.channelType || name;
1771
+ if (!connectedTypeCount.has(type)) {
1772
+ connectedTypeCount.set(type, 0);
1773
+ typeOrder.push(type);
1774
+ }
1775
+ connectedTypeCount.set(type, connectedTypeCount.get(type) + 1);
1776
+ }
1777
+ return typeOrder
1778
+ .map(type => {
1779
+ const n = connectedTypeCount.get(type);
1780
+ return n === 1 ? type : `${type}×${n}`;
1781
+ })
1782
+ .join(', ');
1783
+ };
1784
+ const markChannelConnected = async (inst) => {
1785
+ const name = inst.adapter.channelName;
1786
+ const key = inst.adapter.channelKey;
1787
+ if (connectedChannels.has(name))
1788
+ return;
1789
+ connectedChannels.add(name);
1790
+ connectedChannels.add(key);
1791
+ const type = inst.channelType || name;
1792
+ eventBus.publish({
1793
+ type: 'channel:connected',
1794
+ channel: type.toLowerCase(),
1795
+ channelName: name,
1796
+ timestamp: Date.now()
1797
+ });
1798
+ // Run async operations in parallel
1799
+ const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey) ?? agentRegistry.resolveByChannel(name);
1800
+ await Promise.all([
1801
+ bootstrapService.tryStartBootstrap({
1802
+ adapter: inst.adapter,
1803
+ channelKey: inst.adapter.channelKey,
1804
+ channelType: inst.channelType || type,
1805
+ source: 'connected',
1806
+ }),
1807
+ agent ? ensureTriggerSchedulerStarted(agent) : Promise.resolve(),
1808
+ Promise.resolve().then(() => sendOnlineNoticeForChannel(inst)),
1809
+ trySendPendingRestartNotice(),
1810
+ ]);
1811
+ };
1812
+ const markChannelDisconnected = (channelName) => {
1813
+ connectedChannels.delete(channelName);
1814
+ const inst = channelInstances.find(candidate => candidate.adapter.channelName === channelName);
1815
+ if (inst)
1816
+ connectedChannels.delete(inst.adapter.channelKey);
1817
+ };
1818
+ // ── 连接所有渠道(后台首连,AUN/任意渠道故障不阻塞 daemon 主流程)──
1819
+ logger.info(`🚀 EvolCore core is ready; connecting ${channelInstances.length} channel(s) in background`);
1820
+ const connectAllPromise = channelLoader.connectAll(channelInstances, {
1821
+ concurrency: 10,
1822
+ onConnected: markChannelConnected,
1823
+ onFailed: (inst, error) => {
1824
+ logger.warn(`[startup] ${inst.adapter.channelName} initial connect failed: ${error}`);
1825
+ },
1826
+ });
1827
+ connectAllPromise.then((connected) => {
1828
+ const channelSummary = summarizeConnectedChannels(connected);
1829
+ logger.info(`✅ ${connected.length} channel(s) connected: ${channelSummary}`);
1830
+ eventBus.publish({
1831
+ type: 'system:started',
1832
+ channels: connected.map(c => c.toLowerCase()),
1833
+ timestamp: Date.now()
1834
+ });
1835
+ }).catch((e) => {
1836
+ logger.warn(`[startup] channel connection task failed unexpectedly: ${e}`);
1837
+ });
1838
+ // Trigger scheduler 与渠道连接解耦:cron 定时任务独立于渠道可用性运行
1839
+ // (触发时若渠道未连,发送侧自行排队/重试)。markChannelConnected 里的
1840
+ // ensureTriggerSchedulerStarted 仅作"尽早启动"优化,此处保证即使渠道首连
1841
+ // 全部失败(如 gateway 宕机),trigger 仍无条件启动。Set 去重,不会重复。
1842
+ ensureDaemonTriggerSchedulerStarted().catch((e) => {
1843
+ logger.warn(`[startup] daemon trigger scheduler start failed for ${daemonTriggerOwner.aid}: ${e}`);
1844
+ });
1845
+ for (const agent of triggerStartupAgents) {
1846
+ ensureTriggerSchedulerStarted(agent).catch((e) => {
1847
+ logger.warn(`[startup] trigger scheduler start failed for ${agent.aid}: ${e}`);
1848
+ });
1849
+ }
1850
+ // ── 控制 AID(daemon 进程身份):pureIdentity 接入 AUN,独立于 evolagent ──
1851
+ // 证书缺失检测/生成在 CLI 侧(ec start)完成。daemon 是后台进程无终端,
1852
+ // 这里只做兜底:证书缺失时 warn 并继续(AUNChannel 内部后台重连),绝不阻塞。
1853
+ if (daemonCfg.aid) {
1854
+ const aunPath = resolvePaths().root;
1855
+ const certKey = path.join(aunPath, 'AIDs', daemonCfg.aid, 'private', 'key.json');
1856
+ if (!fs.existsSync(certKey)) {
1857
+ logger.warn(`控制 AID 证书缺失:${daemonCfg.aid}(AUN 控制通道后台重连;如需重建运行 ec init)`);
1858
+ }
1859
+ }
1860
+ let controlChannel;
1861
+ if (daemonCfg.aid) {
1862
+ controlChannel = new AUNChannel({
1863
+ aid: daemonCfg.aid,
1864
+ agentName: daemonCfg.aid,
1865
+ channelName: 'control',
1866
+ pureIdentity: true,
1867
+ aunTrace: daemonCfg.debug?.aunTrace ?? defaults.debug?.aunTrace,
1868
+ aunSdkLog: daemonCfg.debug?.aunSdkLog ?? defaults.debug?.aunSdkLog,
1869
+ });
1870
+ // connect() 失败不置空实例:AUNChannel 内部有无限重连(SDK auto_reconnect +
1871
+ // scheduleReconnect),首连失败后台会自愈;保留实例供 status 显示 disconnected。
1872
+ try {
1873
+ await controlChannel.connect();
1874
+ logger.info(`✓ 控制 AID 已连接: ${daemonCfg.aid}`);
1875
+ }
1876
+ catch (e) {
1877
+ logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
1878
+ }
1879
+ // ── ECWeb 自动启动 ──
1880
+ // 如果 config.ecweb.enabled,自动拉起 ecweb 服务
1881
+ if (daemonCfg.ecweb?.enabled) {
1882
+ const ecwebPath = path.join(resolvePaths().root, 'ecweb');
1883
+ const ecwebEntry = path.join(ecwebPath, 'dist', 'index.js');
1884
+ if (fs.existsSync(ecwebEntry)) {
1885
+ try {
1886
+ const ecwebProc = spawn('node', [ecwebEntry], {
1887
+ cwd: ecwebPath,
1888
+ detached: true,
1889
+ stdio: 'ignore',
1890
+ env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
1891
+ });
1892
+ ecwebProc.unref();
1893
+ logger.info(`✓ ECWeb 已启动 (PID: ${ecwebProc.pid})`);
1894
+ onShutdown(() => {
1895
+ try {
1896
+ if (ecwebProc.pid)
1897
+ platform.killProcess(ecwebProc.pid, true);
1898
+ logger.info(`✓ ECWeb 已停止`);
1899
+ }
1900
+ catch { }
1901
+ });
1902
+ }
1903
+ catch (e) {
1904
+ logger.warn(`ECWeb 启动失败: ${e?.message || e}`);
1905
+ }
1906
+ }
1907
+ else {
1908
+ logger.warn(`ECWeb 配置已启用但未找到 ${ecwebEntry}`);
1909
+ }
1910
+ }
1911
+ // 控制 AID 接收 owner 指令:
1912
+ // 1. /pair — ECWeb 配对码(文本快路径)
1913
+ // 2. menu.* JSON — 路由到 cmdHandler.execMenuForControl(进程级 + 全量权限)
1914
+ // 发送方身份由 AUN X.509 证书链验证,非 owner 完全静默。
1915
+ controlChannel.onMessage(async (opts) => {
1916
+ try {
1917
+ const text = (opts.content || '').trim();
1918
+ let parsed;
1919
+ try {
1920
+ parsed = JSON.parse(text);
1921
+ }
1922
+ catch {
1923
+ parsed = null;
1924
+ }
1925
+ if (bindService && parsed?.type === 'bind.request') {
1926
+ const response = await bindService.handleRequest(parsed, opts.peerId);
1927
+ if (response) {
1928
+ // 用 sendStructured 直发 typed payload(payload.type='bind.response'),
1929
+ // 不能用 sendMessage——它会把内容包成 {type:'text', text:...},App 无法识别。
1930
+ // encrypted 跟随入站请求:bind.response 与 bind.request 的加密/明文对称。
1931
+ await controlChannel.sendStructured(opts.channelId, response, { metadata: { encrypted: opts.encrypted } });
1932
+ }
1933
+ return;
1934
+ }
1935
+ const menuControl = parseMenuControl(text);
1936
+ const isRoleMenu = menuControl.isMenu
1937
+ && (menuControl.name === 'role' || menuControl.request.cmd === '/role');
1938
+ if (menuControl.isMenu && !hasValidMenuId(menuControl)) {
1939
+ logger.warn(`[ControlMenu] dropped malformed request without id type=${menuControl.type}`);
1940
+ return;
1941
+ }
1942
+ if (menuControl.isMenu && !isRoleMenu && !isProcessLevelOwner(opts.peerId, processLevelOwners)) {
1943
+ const response = menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, { code: 'ROLE_ACCESS_DENIED', message: 'Current identity cannot access the control channel' });
1944
+ await controlChannel.sendStructured(opts.channelId, response, { metadata: { encrypted: opts.encrypted } });
1945
+ return;
1946
+ }
1947
+ if (!isRoleMenu && !isProcessLevelOwner(opts.peerId, processLevelOwners)) {
1948
+ logger.debug(`控制 AID 收到非 owner 消息,忽略: from=${opts.peerId}`);
1949
+ return;
1950
+ }
1951
+ if (text.toLowerCase() === '/pair') {
1952
+ const port = daemonCfg.ecweb?.port ?? 42705;
1953
+ const pair = await fetchEcwebPairCode(port);
1954
+ let reply;
1955
+ if (pair) {
1956
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
1957
+ reply = `ECWeb 配对码:${pair.code}(约 ${mins} 分钟内有效)\n在浏览器打开 ECWeb 后输入此码登录`;
1958
+ }
1959
+ else {
1960
+ reply = 'ECWeb 未运行或暂不可达。请在主机运行 ec watch web 启动后重试。';
1961
+ }
1962
+ await controlChannel.sendMessage(opts.channelId, reply);
1963
+ return;
1964
+ }
1965
+ // menu.* JSON 路由:owner 已在上方校验,转交 execMenuForControl(fromControlChannel=true)
1966
+ if (menuControl.isMenu) {
1967
+ const validationError = validateMenuRequest(menuControl.request);
1968
+ if (validationError) {
1969
+ await controlChannel.sendStructured(opts.channelId, menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, validationError), { metadata: { encrypted: opts.encrypted } });
1970
+ return;
1971
+ }
1972
+ const deduped = await controlMenuDeduper.execute([opts.channelId, opts.peerId, menuControl.id].join('\u001f'), menuPayloadFingerprint(menuControl.raw), () => cmdHandler.execMenuForControl(parsed, opts.peerId));
1973
+ const response = 'conflict' in deduped
1974
+ ? menuFailure({ id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) }, { code: 'CONFLICT', message: 'Request ID was already used with a different payload' })
1975
+ : deduped.value;
1976
+ // 同 bind.response:sendStructured 直发 typed payload(payload.type='menu.response'),
1977
+ // 不能用 sendMessage(会包成 {type:'text',...});encrypted 跟随入站请求保持对称。
1978
+ await controlChannel.sendStructured(opts.channelId, response, { metadata: { encrypted: opts.encrypted } });
1979
+ return;
1980
+ }
1981
+ // owner 发的其他内容:提示可用指令
1982
+ await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)');
1983
+ }
1984
+ catch (e) {
1985
+ logger.warn(`控制 AID 消息处理失败: ${e?.message || e}`);
1986
+ }
1987
+ });
1988
+ // ── Service Proxy:把本地服务(ecweb 等)通过控制 AID 暴露到 AUN 网络 ──
1989
+ // 挂在控制 AUNChannel 上,动态解引用其 client(规避重连换 client)。
1990
+ // 失败只 warn,不影响 daemon 主流程。
1991
+ if (daemonCfg.serviceProxy?.enabled && daemonCfg.aid) {
1992
+ // 短暂延迟确保控制 channel 完全就绪
1993
+ const channel = controlChannel;
1994
+ const aid = daemonCfg.aid;
1995
+ const config = daemonCfg.serviceProxy;
1996
+ setTimeout(() => {
1997
+ if (!channel)
1998
+ return;
1999
+ const proxyHandle = startServiceProxy(channel, aid, config);
2000
+ if (proxyHandle) {
2001
+ onShutdown(() => proxyHandle.stop());
2002
+ }
2003
+ }, 500);
2004
+ }
2005
+ }
2006
+ // 统一 channel:health 跨通道通知(仅 auth_error)
2007
+ // 按 (channelType, ownerId) 去重,避免同类型多实例重复通知
2008
+ eventBus.subscribe('channel:error', (event) => {
2009
+ if (event.type !== 'channel:error' || event.status !== 'auth_error')
2010
+ return;
2011
+ const sourceChannelType = event.channel;
2012
+ const sourceChannelName = event.channelName || sourceChannelType;
2013
+ const msg = event.message;
2014
+ logger.error(`[ChannelHealth] ${sourceChannelName} auth_error: ${msg}`);
2015
+ const notified = new Set(); // channelType 去重(同类型只通知一次)
2016
+ for (const other of channelInstances) {
2017
+ const otherType = other.channelType || other.adapter.channelName;
2018
+ if (otherType === sourceChannelType)
2019
+ continue; // 跳过同类型通道
2020
+ if (notified.has(otherType))
2021
+ continue; // 同类型已通知过
2022
+ const owningAgent = agentRegistry.resolveByChannel(other.adapter.channelKey);
2023
+ const ownerId = owningAgent
2024
+ ? getFirstStaticAgentOwner(owningAgent.aid)
2025
+ : undefined;
2026
+ if (!ownerId)
2027
+ continue;
2028
+ notified.add(otherType);
2029
+ const envelope = buildEnvelope({
2030
+ taskId: `system-channel-down-${crypto.randomBytes(5).toString('hex')}`,
2031
+ channel: other.adapter.channelKey,
2032
+ channelId: ownerId,
2033
+ agentName: owningAgent?.aid || 'evolcore',
2034
+ });
2035
+ sendSystemPayload(other.adapter, envelope, {
2036
+ kind: 'system.error',
2037
+ text: msg,
2038
+ subtype: 'channel_down',
2039
+ recoverable: false,
2040
+ }).catch(err => {
2041
+ logger.error(`[ChannelHealth] Failed to notify ${other.adapter.channelName} owner:`, err);
2042
+ });
2043
+ }
2044
+ });
2045
+ // 先恢复消息队列。若某个 session 有原始 active/pending 消息,下面的泛化 resume 会跳过它。
2046
+ messageQueue.restorePersisted(false);
2047
+ const droppedTriggerSessionIds = new Set(messageQueue.consumeDroppedTriggerSessionIds());
2048
+ // runnableAgents() only returns pre-start `stopped` agents. Reuse the stable
2049
+ // startup snapshot because channel connection may already mark them running.
2050
+ const recoveryAids = triggerStartupAgents.map(agent => agent.aid);
2051
+ for (const aid of recoveryAids) {
2052
+ try {
2053
+ const expired = handoffRuntime.expireQueued([aid]);
2054
+ if (expired > 0)
2055
+ logger.info(`[Handoff] expired ${expired} stale queued delivery item(s) for ${aid}`);
2056
+ }
2057
+ catch (error) {
2058
+ logger.error(`[Handoff] startup TTL cleanup failed for ${aid}:`, error);
2059
+ }
2060
+ }
2061
+ const recoverHandoffsAndStartRestored = async () => {
2062
+ try {
2063
+ const outcomes = await Promise.allSettled(recoveryAids.map(aid => handoffRuntime.recover([aid])));
2064
+ outcomes.forEach((outcome, index) => {
2065
+ if (outcome.status === 'rejected') {
2066
+ logger.error(`[Handoff] startup recovery failed for ${recoveryAids[index]}:`, outcome.reason);
2067
+ }
2068
+ });
2069
+ }
2070
+ finally {
2071
+ messageQueue.startRestored();
2072
+ }
2073
+ };
2074
+ void connectAllPromise
2075
+ .then(recoverHandoffsAndStartRestored, recoverHandoffsAndStartRestored)
2076
+ .catch(error => logger.error('[Handoff] startup recovery coordinator failed:', error));
2077
+ // 恢复重启前未完成的会话。这里是兜底路径:仅当队列文件没有原始消息时,才注入恢复提示。
2078
+ const pendingSessions = sessionManager.getPendingProcessingSessions();
2079
+ if (pendingSessions.length > 0) {
2080
+ logger.info(`[Resume] Found ${pendingSessions.length} pending session(s) from before restart`);
2081
+ for (const session of pendingSessions) {
2082
+ if (droppedTriggerSessionIds.has(session.id)) {
2083
+ logger.info(`[Resume] session ${session.id}: dropped stale Trigger execution, clearing processing without resume`);
2084
+ sessionManager.clearProcessing(session.id);
2085
+ continue;
2086
+ }
2087
+ if (messageQueue.isProcessing(session.id) || messageQueue.getQueueLength(session.id) > 0) {
2088
+ logger.info(`[Resume] session ${session.id}: persisted queue already restored, skipping generic resume`);
2089
+ continue;
2090
+ }
2091
+ if (!session.agentSessionId) {
2092
+ sessionManager.clearProcessing(session.id);
2093
+ continue;
2094
+ }
2095
+ // 复合键:${aid}::${baseagent},从 channel 反查 self-agent
2096
+ const owningAgent = agentRegistry.resolveByChannel(session.channel);
2097
+ if (!owningAgent) {
2098
+ logger.warn(`[Resume] session ${session.id}: channel "${session.channel}" not routable, skipping`);
2099
+ sessionManager.clearProcessing(session.id);
2100
+ continue;
2101
+ }
2102
+ const evolName = owningAgent.aid;
2103
+ const baseagentName = session.baseagent || primaryBaseagent;
2104
+ const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(primaryRunnerKey);
2105
+ if (!agent) {
2106
+ sessionManager.clearProcessing(session.id);
2107
+ continue;
2108
+ }
2109
+ logger.info(`[Resume] Resuming session: ${session.id} (agent: ${evolName}::${baseagentName})`);
2110
+ const parsedResumeKey = tryParseChannelKey(session.channel);
2111
+ const resumeSelfAID = session.selfAID || parsedResumeKey?.selfAID;
2112
+ const resumeMessage = {
2113
+ channel: session.channel,
2114
+ channelType: session.channelType || parsedResumeKey?.type,
2115
+ selfAID: resumeSelfAID,
2116
+ channelId: session.channelId,
2117
+ content: '服务已重启,请继续之前未完成的任务。',
2118
+ timestamp: Date.now(),
2119
+ peerId: '',
2120
+ threadId: session.threadId || undefined,
2121
+ replyContext: session.metadata?.replyContext,
2122
+ };
2123
+ // 清除状态后入队(processMessage 会重新标记)
2124
+ sessionManager.clearProcessing(session.id);
2125
+ messageQueue.enqueue(session.id, resumeMessage, session.projectPath, { sessionKeyField: session.sessionKey, selfAID: resumeSelfAID }).catch(err => {
2126
+ logger.error(`[Resume] Failed to resume session ${session.id}:`, err);
2127
+ });
2128
+ }
2129
+ }
2130
+ // IPC server — 供 CLI 查询实时状态 + Agent ctl 指令执行
2131
+ const ipcServer = new IpcServer(resolvePaths().socket, () => {
2132
+ const channels = {};
2133
+ const channelsByType = {};
2134
+ for (const inst of channelInstances) {
2135
+ const name = inst.adapter.channelName;
2136
+ const status = inst.channel.getStatus?.() ?? { connected: true };
2137
+ const channelType = inst.channelType || name;
2138
+ channels[name] = { ...status, channelType };
2139
+ if (!channelsByType[channelType])
2140
+ channelsByType[channelType] = [];
2141
+ channelsByType[channelType].push(name);
2142
+ }
2143
+ const snap = statsCollector.getSnapshot();
2144
+ const agentListForStatus = agentRegistry.list();
2145
+ return {
2146
+ pid: process.pid,
2147
+ uptime: snap.uptimeMs,
2148
+ controlPlane: {
2149
+ ready: true,
2150
+ owned: processLevelOwners.length > 0,
2151
+ },
2152
+ agentRuntime: {
2153
+ state: agentRuntimeState,
2154
+ runnableAgents: agentListForStatus.filter((a) => a.status !== 'error' && a.status !== 'disabled').length,
2155
+ runningAgents: agentListForStatus.filter((a) => a.status === 'running').length,
2156
+ ...(agentRuntimeError ? { error: agentRuntimeError } : {}),
2157
+ },
2158
+ channels,
2159
+ channelsByType,
2160
+ queue: {
2161
+ pending: messageQueue.getGlobalQueueLength(),
2162
+ processing: messageQueue.getGlobalProcessingCount(),
2163
+ },
2164
+ stats: {
2165
+ received: snap.lastHour.received,
2166
+ sent: snap.lastHour.sent,
2167
+ completed: snap.lastHour.completed,
2168
+ errors: snap.lastHour.errors,
2169
+ avgResponseMs: snap.lastHour.avgResponseMs,
2170
+ },
2171
+ controlAid: daemonCfg.aid
2172
+ ? { aid: daemonCfg.aid, connected: controlChannel?.getAidState().status === 'connected' }
2173
+ : undefined,
2174
+ };
2175
+ }, async (cmd, sessionId, delegationToken) => {
2176
+ const delegation = agentDelegationRegistry.validate(delegationToken, sessionId);
2177
+ if (!delegation.ok)
2178
+ return { ok: false, code: delegation.code, error: delegation.reason };
2179
+ return cmdHandler.handleCtl(cmd, sessionId);
2180
+ });
2181
+ // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2182
+ ipcServer.setAgentRegistry(agentRegistry);
2183
+ ipcServer.setDingtalkContactBindExecutor({
2184
+ register: (cmd) => registerPendingDingtalkContactBind(cmd),
2185
+ });
2186
+ ipcServer.setWecomContactBindExecutor({
2187
+ register: (cmd) => registerPendingWecomContactBind(cmd),
2188
+ });
2189
+ ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2190
+ ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken));
2191
+ ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken));
2192
+ cmdHandler.setDaemonStatusProvider(() => {
2193
+ const aidState = controlChannel?.getAidState?.();
2194
+ return {
2195
+ aid: daemonCfg.aid ?? null,
2196
+ aun: aidState ? {
2197
+ connected: aidState.status === 'connected',
2198
+ status: aidState.status,
2199
+ reconnectCount: aidState.reconnectCount ?? 0,
2200
+ flapCount: aidState.flapCount ?? 0,
2201
+ ...(aidState.lastError ? { lastError: String(aidState.lastError).slice(0, 80) } : {}),
2202
+ ...(aidState.kickDetail?.reason ? { kickReason: String(aidState.kickDetail.reason).slice(0, 80) } : {}),
2203
+ } : {
2204
+ connected: false,
2205
+ status: daemonCfg.aid ? 'disconnected' : 'disabled',
2206
+ },
2207
+ };
2208
+ });
2209
+ if (bindService) {
2210
+ ipcServer.setBindExecutor({
2211
+ begin: (cmd) => bindService.begin(cmd),
2212
+ status: (taskId) => bindService.status(taskId),
2213
+ cancel: (taskId) => bindService.cancel(taskId),
2214
+ });
2215
+ }
2216
+ // 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
2217
+ ipcServer.setAunAidProvider(() => {
2218
+ const out = [];
2219
+ for (const inst of channelInstances) {
2220
+ if (inst.channelType !== 'aun')
2221
+ continue;
2222
+ const ch = inst.channel;
2223
+ if (typeof ch?.getAidState === 'function') {
2224
+ try {
2225
+ const aidState = ch.getAidState();
2226
+ // 增强:添加队列状态
2227
+ const agentName = aidState.agentName || aidState.aid;
2228
+ const processing = messageQueue.getProcessingCountByAgent(agentName);
2229
+ const queued = messageQueue.getQueueLengthByAgent(agentName);
2230
+ out.push({
2231
+ ...aidState,
2232
+ queueStatus: { processing, queued }
2233
+ });
2234
+ }
2235
+ catch { /* ignore */ }
2236
+ }
2237
+ }
2238
+ return out;
2239
+ });
2240
+ // 注入 Per-AID 统计收集器到所有 AUN channel 实例
2241
+ for (const inst of channelInstances) {
2242
+ if (inst.channelType !== 'aun')
2243
+ continue;
2244
+ const ch = inst.channel;
2245
+ if (typeof ch?.setAidStatsCollector === 'function') {
2246
+ ch.setAidStatsCollector(aidStatsCollector);
2247
+ }
2248
+ }
2249
+ // 注入 Per-AID 统计 IPC provider
2250
+ aidStatsCollector.setQueueStatsProvider((agentName) => ({
2251
+ processing: messageQueue.getProcessingCountByAgent(agentName),
2252
+ queued: messageQueue.getQueueLengthByAgent(agentName),
2253
+ muted: messageQueue.isAgentMuted(agentName),
2254
+ }));
2255
+ ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
2256
+ ipcServer.setAunAidStatsRecorder((params) => {
2257
+ aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
2258
+ });
2259
+ ipcServer.setTaskRuntimeContextProvider(({ sessionId }) => responseEngine.getTaskRuntimeContext(sessionId));
2260
+ ipcServer.setHandoffReturnExecutor((params) => responseEngine.returnHandoffResult(params));
2261
+ ipcServer.setHandoffStatusExecutor(async (params) => {
2262
+ if (!params.sessionId)
2263
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_REQUIRED', error: 'current session is required' };
2264
+ const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2265
+ const session = await sessionManager.getSessionById(params.sessionId);
2266
+ const selfAid = runtime?.selfAid || session?.selfAID;
2267
+ if (!selfAid)
2268
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2269
+ return handoffRuntime.status(selfAid, params.handoffId)
2270
+ ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2271
+ });
2272
+ const resolveHandoffQueryAid = async (params) => {
2273
+ if (params.sessionId) {
2274
+ const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2275
+ const session = await sessionManager.getSessionById(params.sessionId);
2276
+ const selfAid = runtime?.selfAid || session?.selfAID;
2277
+ if (!selfAid) {
2278
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is invalid' };
2279
+ }
2280
+ if (params.agent && params.agent !== selfAid) {
2281
+ return { ok: false, code: 'HANDOFF_AGENT_SCOPE_MISMATCH', error: 'agent does not match the current session' };
2282
+ }
2283
+ return { ok: true, aid: selfAid };
2284
+ }
2285
+ if (!params.agent) {
2286
+ return { ok: false, code: 'HANDOFF_AGENT_REQUIRED', error: '--agent is required outside a task context' };
2287
+ }
2288
+ if (!agentRegistry.get(params.agent)) {
2289
+ return { ok: false, code: 'HANDOFF_AGENT_NOT_FOUND', error: `agent not found: ${params.agent}` };
2290
+ }
2291
+ return { ok: true, aid: params.agent };
2292
+ };
2293
+ ipcServer.setHandoffListExecutor(async (params) => {
2294
+ const scope = await resolveHandoffQueryAid(params);
2295
+ if (!scope.ok)
2296
+ return scope;
2297
+ return handoffRuntime.listHandoffs({
2298
+ selfAid: scope.aid,
2299
+ state: params.state,
2300
+ sessionId: params.filterSessionId,
2301
+ limit: params.limit,
2302
+ });
2303
+ });
2304
+ ipcServer.setHandoffTraceExecutor(async (params) => {
2305
+ const scope = await resolveHandoffQueryAid(params);
2306
+ if (!scope.ok)
2307
+ return scope;
2308
+ return handoffRuntime.traceHandoff(scope.aid, params.handoffId, params.limit)
2309
+ ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2310
+ });
2311
+ ipcServer.setAunMsgSender(async (params) => {
2312
+ const runtimeCausation = params.originSessionId
2313
+ ? responseEngine.getTaskRuntimeContext(params.originSessionId)?.causation
2314
+ : undefined;
2315
+ const delegation = authorizeDelegatedAunMsgSend(agentDelegationRegistry, {
2316
+ delegationToken: params.delegationToken,
2317
+ sessionId: params.originSessionId,
2318
+ messageId: params.originMessageId,
2319
+ aid: params.aid,
2320
+ to: params.to,
2321
+ scope: params.scope,
2322
+ action: params.file ? 'file' : 'send',
2323
+ });
2324
+ if (!delegation.ok) {
2325
+ return { ok: false, error: delegation.reason, code: delegation.code };
2326
+ }
2327
+ const inst = channelInstances.find((candidate) => {
2328
+ if (candidate.channelType !== 'aun')
2329
+ return false;
2330
+ const ch = candidate.channel;
2331
+ try {
2332
+ const aidState = typeof ch?.getAidState === 'function' ? ch.getAidState() : null;
2333
+ if (aidState?.aid === params.aid)
2334
+ return true;
2335
+ if (typeof ch?.getAid === 'function' && ch.getAid() === params.aid)
2336
+ return true;
2337
+ }
2338
+ catch { /* ignore */ }
2339
+ return false;
2340
+ });
2341
+ const ch = inst?.channel;
2342
+ if (!ch) {
2343
+ return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
2344
+ }
2345
+ const targetIsGroup = typeof ch.isGroupId === 'function' && ch.isGroupId(params.to);
2346
+ if ((params.scope === 'group') !== targetIsGroup) {
2347
+ return { ok: false, error: 'AUN target does not match the delegated send scope', code: 'UNSUPPORTED_TARGET' };
2348
+ }
2349
+ let payload = params.payload;
2350
+ if (params.file) {
2351
+ if (isHClassPath(params.file.filePath)) {
2352
+ return {
2353
+ ok: false,
2354
+ error: 'H-class protected files cannot be uploaded by an agent task',
2355
+ code: 'H_CLASS_PROTECTED',
2356
+ };
2357
+ }
2358
+ if (typeof ch.buildDaemonFilePayload !== 'function') {
2359
+ return { ok: false, error: 'AUN channel does not support daemon file uploads', code: 'AUN_FILE_UNSUPPORTED' };
2360
+ }
2361
+ try {
2362
+ payload = await ch.buildDaemonFilePayload(params.file);
2363
+ }
2364
+ catch (error) {
2365
+ return {
2366
+ ok: false,
2367
+ error: error instanceof Error ? error.message : String(error),
2368
+ code: error?.code ?? 'AUN_FILE_UPLOAD_FAILED',
2369
+ };
2370
+ }
2371
+ }
2372
+ if (!payload) {
2373
+ return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
2374
+ }
2375
+ if (params.thread && typeof payload.thread_id !== 'string') {
2376
+ payload = { ...payload, thread_id: params.thread };
2377
+ }
2378
+ if (params.scope === 'group' && params.mentions?.length) {
2379
+ payload = { ...payload, mentions: params.mentions };
2380
+ }
2381
+ let targetSessionId;
2382
+ if (params.originSessionId && params.originMessageId) {
2383
+ try {
2384
+ const created = await handoffRuntime.createOutbound({
2385
+ selfAid: params.aid,
2386
+ to: params.to,
2387
+ originSessionId: params.originSessionId,
2388
+ originMessageId: params.originMessageId,
2389
+ payload,
2390
+ encrypt: params.encrypt === true,
2391
+ thread: params.thread,
2392
+ targetChatType: params.scope === 'group' ? 'group' : 'private',
2393
+ explicitReturnPolicy: params.returnPolicy,
2394
+ originAuthorization: {
2395
+ actorId: delegation.grant.actorId,
2396
+ channelKey: delegation.grant.channel,
2397
+ channelType: delegation.grant.channelType,
2398
+ chatType: delegation.grant.chatType,
2399
+ peerKey: delegation.grant.peerKey,
2400
+ },
2401
+ causation: runtimeCausation,
2402
+ });
2403
+ targetSessionId = created.targetSession.id;
2404
+ if (created.crossSession && created.handoff) {
2405
+ return {
2406
+ ok: true,
2407
+ status: created.handoff.state === 'target_sent' ? 'delivered' : 'queued',
2408
+ message_id: created.handoff.target_message_id ?? undefined,
2409
+ handoff_id: created.handoff.handoff_id,
2410
+ target_session_id: created.targetSession.id,
2411
+ };
2412
+ }
2413
+ if (params.scope === 'msg' && !payload.ref_message_id) {
2414
+ payload.ref_message_id = params.originMessageId;
2415
+ }
2416
+ }
2417
+ catch (error) {
2418
+ const code = error?.code;
2419
+ const handoffId = error?.handoffId
2420
+ ?? error?.blockingHandoffId;
2421
+ return {
2422
+ ok: false,
2423
+ error: error instanceof Error ? error.message : String(error),
2424
+ code,
2425
+ ...(handoffId ? { handoff_id: handoffId } : {}),
2426
+ };
2427
+ }
2428
+ }
2429
+ if (params.scope === 'group') {
2430
+ if (typeof ch.sendDaemonGroupMsg !== 'function') {
2431
+ return { ok: false, error: 'AUN channel does not support daemon group sends', code: 'AUN_GROUP_UNSUPPORTED' };
2432
+ }
2433
+ return await ch.sendDaemonGroupMsg({
2434
+ groupId: params.to,
2435
+ payload,
2436
+ mentions: params.mentions,
2437
+ encrypt: params.encrypt,
2438
+ log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
2439
+ });
2440
+ }
2441
+ if (typeof ch.sendDaemonMsg !== 'function') {
2442
+ return { ok: false, error: 'AUN channel does not support daemon private sends', code: 'AUN_MSG_UNSUPPORTED' };
2443
+ }
2444
+ return await ch.sendDaemonMsg({
2445
+ to: params.to,
2446
+ payload,
2447
+ encrypt: params.encrypt,
2448
+ causation: runtimeCausation,
2449
+ log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
2450
+ });
2451
+ });
2452
+ // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
2453
+ const reloadHooks = buildReloadHooks({
2454
+ channelLoader,
2455
+ channelInstances,
2456
+ registerChannelInstance,
2457
+ unregisterChannelInstance: (channelName) => {
2458
+ markChannelDisconnected(channelName);
2459
+ processor.unregisterChannel(channelName);
2460
+ cmdHandler.unregisterChannel(channelName);
2461
+ msgBridge.removeChannel(channelName);
2462
+ },
2463
+ onChannelStarted: (inst) => {
2464
+ // startChannel 重建渠道时重新注入 AidStatsCollector(与 hot-load 路径对齐)
2465
+ if (inst.channelType === 'aun') {
2466
+ const ch = inst.channel;
2467
+ if (typeof ch?.setAidStatsCollector === 'function')
2468
+ ch.setAidStatsCollector(aidStatsCollector);
2469
+ }
2470
+ },
2471
+ onChannelConnected: markChannelConnected,
2472
+ messageQueue,
2473
+ handoffRuntime,
2474
+ });
2475
+ // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
2476
+ globalThis.__evolcore_reloadHooks = reloadHooks;
2477
+ // Hot-load handler: dynamically add a new agent at runtime
2478
+ globalThis.__evolcore_hotLoadAgent = async (aid) => {
2479
+ handoffRuntime.pauseAgent(aid);
2480
+ let resumed = false;
2481
+ try {
2482
+ agentRuntimeState = 'starting';
2483
+ agentRuntimeError = undefined;
2484
+ const agent = agentRegistry.loadNewAgent(aid);
2485
+ if (!agent) {
2486
+ agentRuntimeState = agentRegistry.runnableAgents().length > 0 ? 'running' : 'error';
2487
+ agentRuntimeError = `Failed to load agent ${aid}`;
2488
+ throw new Error(agentRuntimeError);
2489
+ }
2490
+ const newAgentInstances = agentLoader.createForAgent(agent, {
2491
+ onSessionIdUpdate: async (sessionId, agentSessionId) => {
2492
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
2493
+ },
2494
+ });
2495
+ for (const inst of newAgentInstances) {
2496
+ agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
2497
+ inst.agent.setPermissionGateway?.(permissionGateway);
2498
+ inst.agent.setCompactStartCallback?.((sessionId) => {
2499
+ processor.handleCompactStart(sessionId);
2500
+ });
2501
+ }
2502
+ if (newAgentInstances.length === 0) {
2503
+ agent.status = 'error';
2504
+ agent.error = 'No baseagent runner created for hot-loaded agent';
2505
+ agentRuntimeState = 'error';
2506
+ agentRuntimeError = agent.error;
2507
+ throw new Error(agent.error);
2508
+ }
2509
+ // 创建 channels
2510
+ const instances = await channelLoader.createForAgent(agent);
2511
+ for (const inst of instances) {
2512
+ registerChannelInstance(inst);
2513
+ if (inst.channelType === 'aun') {
2514
+ const ch = inst.channel;
2515
+ if (typeof ch?.setAidStatsCollector === 'function')
2516
+ ch.setAidStatsCollector(aidStatsCollector);
2517
+ }
2518
+ agent.channels.set(inst.adapter.channelKey, inst.adapter);
2519
+ channelInstances.push(inst);
2520
+ }
2521
+ agent.status = 'running';
2522
+ // 连接
2523
+ await channelLoader.connectAll(instances, { onConnected: markChannelConnected });
2524
+ await handoffRuntime.recover([aid]);
2525
+ await ensureTriggerSchedulerStarted(agent);
2526
+ handoffRuntime.resumeAgent(aid);
2527
+ resumed = true;
2528
+ agentRuntimeState = 'running';
2529
+ agentRuntimeError = undefined;
2530
+ logger.info(`[HotLoad] ✓ Agent ${aid} online with ${instances.length} channel(s)`);
2531
+ }
2532
+ finally {
2533
+ if (!resumed)
2534
+ handoffRuntime.resumeAgent(aid);
2535
+ }
2536
+ };
2537
+ // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
2538
+ globalThis.__evolcore_resyncAgents = async () => {
2539
+ const { loadAllAgents: scanAgents, loadDefaults: readDefaults } = await import('./config-store.js');
2540
+ const { resolveEffective } = await import('./config/config-manager.js');
2541
+ const freshDefaults = readDefaults();
2542
+ const { agents: diskAgents } = scanAgents();
2543
+ const diskAidSet = new Set(diskAgents.map(a => a.aid));
2544
+ const results = [];
2545
+ // 1. 下线:运行时有但磁盘上没有 / disabled 的
2546
+ for (const [aid, agent] of [...agentRegistry.agents.entries()]) {
2547
+ const diskCfg = diskAgents.find(a => a.aid === aid);
2548
+ if (!diskCfg || diskCfg.enabled === false) {
2549
+ handoffRuntime.pauseAgent(aid);
2550
+ try {
2551
+ await handoffRuntime.drainAgent(aid);
2552
+ }
2553
+ catch (error) {
2554
+ handoffRuntime.resumeAgent(aid);
2555
+ results.push(`⚠ ${aid}: ${error instanceof Error ? error.message : String(error)}`);
2556
+ continue;
2557
+ }
2558
+ await triggerSchedulers.get(aid)?.stop();
2559
+ triggerSchedulers.delete(aid);
2560
+ // 断开所有 channels
2561
+ for (const chName of agent.channelInstanceNames()) {
2562
+ const inst = channelInstances.find(i => i.adapter.channelName === chName);
2563
+ if (inst) {
2564
+ try {
2565
+ await inst.disconnect();
2566
+ }
2567
+ catch { }
2568
+ markChannelDisconnected(inst.adapter.channelName);
2569
+ const idx = channelInstances.indexOf(inst);
2570
+ if (idx >= 0)
2571
+ channelInstances.splice(idx, 1);
2572
+ }
2573
+ }
2574
+ agentRegistry.agents.delete(aid);
2575
+ results.push(`- ${aid} (offline)`);
2576
+ continue;
2577
+ }
2578
+ }
2579
+ // 2. 新增:磁盘上有但运行时没有的
2580
+ for (const cfg of diskAgents) {
2581
+ if (cfg.enabled === false)
2582
+ continue;
2583
+ if (agentRegistry.agents.has(cfg.aid))
2584
+ continue;
2585
+ try {
2586
+ await globalThis.__evolcore_hotLoadAgent(cfg.aid);
2587
+ results.push(`+ ${cfg.aid} (online)`);
2588
+ }
2589
+ catch (e) {
2590
+ results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
2591
+ }
2592
+ }
2593
+ // 3. 已有的:重新 reload(config 可能改了)
2594
+ const hooks = globalThis.__evolcore_reloadHooks;
2595
+ for (const cfg of diskAgents) {
2596
+ if (cfg.enabled === false)
2597
+ continue;
2598
+ if (!agentRegistry.agents.has(cfg.aid))
2599
+ continue;
2600
+ // 只有磁盘上存在且运行时也存在的才 reload
2601
+ try {
2602
+ await agentRegistry.reload(cfg.aid, hooks);
2603
+ const runtimeAgent = agentRegistry.get(cfg.aid);
2604
+ if (runtimeAgent)
2605
+ await startTriggerScheduler(runtimeAgent);
2606
+ results.push(`↻ ${cfg.aid} (reloaded)`);
2607
+ }
2608
+ catch (e) {
2609
+ results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
2610
+ }
2611
+ }
2612
+ // 重建 channel index + 清除 kit 缓存
2613
+ invalidateKitCache();
2614
+ agentRegistry.channelIndex.clear();
2615
+ agentRegistry.buildChannelIndex();
2616
+ logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
2617
+ return results;
2618
+ };
2619
+ ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
2620
+ ipcServer.setAgentStatsProvider(() => agentRegistry.list().map((agent) => {
2621
+ const snap = statsCollector.getSnapshot(agent.aid);
2622
+ return {
2623
+ aid: agent.aid,
2624
+ received: snap.lastHour.received,
2625
+ sent: snap.lastHour.sent,
2626
+ completed: snap.lastHour.completed,
2627
+ errors: snap.lastHour.errors,
2628
+ interrupts: snap.lastHour.interrupts,
2629
+ avgResponseMs: snap.lastHour.avgResponseMs,
2630
+ processing: messageQueue.getProcessingCountByAgent(agent.aid),
2631
+ queued: messageQueue.getQueueLengthByAgent(agent.aid),
2632
+ muted: messageQueue.isAgentMuted(agent.aid),
2633
+ };
2634
+ }));
2635
+ // Queue snapshot & action (for ec queue --agent CLI)
2636
+ ipcServer.setQueueSnapshotProvider((params) => {
2637
+ const handle = agentRegistry.get(params.agent);
2638
+ const agentName = handle?.name;
2639
+ if (!agentName)
2640
+ return [];
2641
+ return messageQueue.getQueueItemsByAgent(agentName);
2642
+ });
2643
+ ipcServer.setQueueActionExecutor(async (params) => {
2644
+ const handle = agentRegistry.get(params.agent);
2645
+ const agentName = handle?.name;
2646
+ if (!agentName)
2647
+ return { ok: false, error: `agent not found: ${params.agent}` };
2648
+ switch (params.action) {
2649
+ case 'clear':
2650
+ return { ok: true, cleared: messageQueue.clearByAgent(agentName) };
2651
+ case 'cancel':
2652
+ if (!params.messageId)
2653
+ return { ok: false, error: 'missing messageId' };
2654
+ return { ok: true, cancelled: messageQueue.cancelMessageById(agentName, params.messageId) };
2655
+ case 'interrupt':
2656
+ if (!params.sessionKey)
2657
+ return { ok: false, error: 'missing sessionKey' };
2658
+ {
2659
+ const sessionId = messageQueue.findSessionIdBySessionKey(params.sessionKey);
2660
+ if (!sessionId)
2661
+ return { ok: false, error: `session not found: ${params.sessionKey}` };
2662
+ return { ok: true, interrupted: await messageQueue.interruptBySession(sessionId) };
2663
+ }
2664
+ default:
2665
+ return { ok: false, error: `unknown action: ${params.action}` };
2666
+ }
2667
+ });
2668
+ ipcServer.setTriggerExecutor(async (cmd) => {
2669
+ const schedulerFor = (agentAid) => {
2670
+ const scheduler = triggerSchedulers.get(agentAid);
2671
+ if (!scheduler)
2672
+ throw new Error(`trigger scheduler not found for agent: ${agentAid}`);
2673
+ return scheduler;
2674
+ };
2675
+ const requireAgent = (agentAid) => {
2676
+ if (typeof agentAid !== 'string' || !agentAid)
2677
+ throw new Error('missing agentAid');
2678
+ if (agentAid === daemonTriggerOwner.aid)
2679
+ return agentAid;
2680
+ if (!agentRegistry.get(agentAid))
2681
+ throw new Error(`agent not found: ${agentAid}`);
2682
+ return agentAid;
2683
+ };
2684
+ const authorizeTrigger = async (agentAid, operation, triggerId) => {
2685
+ const actor = await authenticatedTriggerActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.controlToken);
2686
+ if (!actor.control && actor.selfAid !== agentAid) {
2687
+ throw new Error('trigger agent does not match authenticated task agent');
2688
+ }
2689
+ const operationDecision = authorizeOperation({
2690
+ source: actor.control ? 'control' : 'agent-tool',
2691
+ subject: actor.subject,
2692
+ intent: {
2693
+ operation,
2694
+ scope: 'relation',
2695
+ source: actor.control ? 'control' : 'agent-tool',
2696
+ args: {
2697
+ self: agentAid,
2698
+ peer: actor.origin.peerId,
2699
+ peerKey: actor.subject.peerKey,
2700
+ ...(triggerId ? { triggerId } : {}),
2701
+ },
2702
+ },
2703
+ });
2704
+ if (!operationDecision.allow)
2705
+ throw new Error(operationDecision.reason);
2706
+ if (actor.management)
2707
+ return actor;
2708
+ if (!triggerId)
2709
+ return actor;
2710
+ const definition = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === triggerId);
2711
+ if (!definition
2712
+ || definition.origin?.peerId !== actor.origin?.peerId
2713
+ || definition.origin?.channelKey !== actor.origin?.channelKey) {
2714
+ throw new Error('trigger not found or access denied');
2715
+ }
2716
+ return actor;
2717
+ };
2718
+ switch (cmd.type) {
2719
+ case 'trigger.list': {
2720
+ const agentAid = requireAgent(cmd.agentAid);
2721
+ const actor = await authorizeTrigger(agentAid, 'trigger.list');
2722
+ const scheduler = schedulerFor(agentAid);
2723
+ const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => actor.management
2724
+ || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey));
2725
+ return { ok: true, triggers: scheduler.listItems(definitions) };
2726
+ }
2727
+ case 'trigger.show': {
2728
+ const agentAid = requireAgent(cmd.agentAid);
2729
+ if (!cmd.triggerId)
2730
+ throw new Error('missing triggerId');
2731
+ await authorizeTrigger(agentAid, 'trigger.show', cmd.triggerId);
2732
+ return {
2733
+ ok: true,
2734
+ ...schedulerFor(agentAid).show(cmd.triggerId, {
2735
+ includeScriptPreview: cmd.includeScriptPreview !== false,
2736
+ }),
2737
+ };
2738
+ }
2739
+ case 'trigger.history': {
2740
+ const agentAid = requireAgent(cmd.agentAid);
2741
+ const limit = cmd.limit === undefined ? 100 : Number(cmd.limit);
2742
+ if (!Number.isInteger(limit) || limit <= 0 || limit > 10_000)
2743
+ throw new Error('invalid history limit');
2744
+ if (cmd.triggerId !== undefined && (typeof cmd.triggerId !== 'string' || !cmd.triggerId)) {
2745
+ throw new Error('invalid triggerId');
2746
+ }
2747
+ if (cmd.triggerId)
2748
+ await authorizeTrigger(agentAid, 'trigger.history', cmd.triggerId);
2749
+ else if (!(await authorizeTrigger(agentAid, 'trigger.history')).management) {
2750
+ throw new Error('triggerId is required for non-management history access');
2751
+ }
2752
+ return { ok: true, events: schedulerFor(agentAid).history(cmd.triggerId, limit) };
2753
+ }
2754
+ case 'trigger.eventCatalog': {
2755
+ const agentAid = requireAgent(cmd.agentAid);
2756
+ await authorizeTrigger(agentAid, 'trigger.eventCatalog');
2757
+ return { ok: true, ...getEventCatalog({ includeInternal: cmd.includeInternal === true }) };
2758
+ }
2759
+ case 'trigger.create': {
2760
+ const rawDefinition = cmd.definition;
2761
+ if (!rawDefinition || typeof rawDefinition !== 'object' || Array.isArray(rawDefinition)) {
2762
+ throw new Error('trigger definition must be an object');
2763
+ }
2764
+ const agentAid = requireAgent(rawDefinition.agentAid);
2765
+ const actor = await authorizeTrigger(agentAid, 'trigger.create');
2766
+ const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin }, agentAid);
2767
+ const definition = normalizeTriggerDefinition(materialized);
2768
+ validateTriggerDefinitionForActor(definition, actor);
2769
+ requireAgent(definition.agentAid);
2770
+ validateTriggerFeedbackChannels(definition);
2771
+ const trigger = schedulerFor(definition.agentAid).create(definition, cmd.files ?? [], { enable: cmd.enable });
2772
+ return { ok: true, trigger };
2773
+ }
2774
+ case 'trigger.update': {
2775
+ const agentAid = requireAgent(cmd.agentAid);
2776
+ if (!cmd.triggerId)
2777
+ throw new Error('missing triggerId');
2778
+ const actor = await authorizeTrigger(agentAid, 'trigger.update', cmd.triggerId);
2779
+ const existing = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
2780
+ if (!existing)
2781
+ throw new Error(`trigger not found: ${cmd.triggerId}`);
2782
+ const currentRevision = definitionRevision(existing);
2783
+ if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
2784
+ throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
2785
+ }
2786
+ const definition = applyTriggerPatch(existing, cmd.patch);
2787
+ validateTriggerDefinitionForActor(definition, actor);
2788
+ validateTriggerFeedbackChannels(definition);
2789
+ const scheduler = schedulerFor(agentAid);
2790
+ const trigger = scheduler.update(cmd.triggerId, definition);
2791
+ return { ok: true, trigger: scheduler.listItem(trigger), revision: definitionRevision(trigger) };
2792
+ }
2793
+ case 'trigger.setEnabled': {
2794
+ const agentAid = requireAgent(cmd.agentAid);
2795
+ if (!cmd.triggerId)
2796
+ throw new Error('missing triggerId');
2797
+ if (typeof cmd.enabled !== 'boolean')
2798
+ throw new Error('missing enabled');
2799
+ await authorizeTrigger(agentAid, 'trigger.setEnabled', cmd.triggerId);
2800
+ const trigger = schedulerFor(agentAid).setEnabled(cmd.triggerId, cmd.enabled);
2801
+ return { ok: true, trigger };
2802
+ }
2803
+ case 'trigger.cancel': {
2804
+ const agentAid = requireAgent(cmd.agentAid);
2805
+ if (!cmd.triggerId)
2806
+ throw new Error('missing triggerId');
2807
+ await authorizeTrigger(agentAid, 'trigger.cancel', cmd.triggerId);
2808
+ const trigger = schedulerFor(agentAid).cancel(cmd.triggerId);
2809
+ return { ok: true, trigger };
2810
+ }
2811
+ case 'trigger.delete': {
2812
+ const agentAid = requireAgent(cmd.agentAid);
2813
+ if (!cmd.triggerId)
2814
+ throw new Error('missing triggerId');
2815
+ await authorizeTrigger(agentAid, 'trigger.delete', cmd.triggerId);
2816
+ const trigger = schedulerFor(agentAid).delete(cmd.triggerId);
2817
+ return { ok: true, trigger };
2818
+ }
2819
+ case 'trigger.run': {
2820
+ const agentAid = requireAgent(cmd.agentAid);
2821
+ if (!cmd.triggerId)
2822
+ throw new Error('missing triggerId');
2823
+ await authorizeTrigger(agentAid, 'trigger.run', cmd.triggerId);
2824
+ const result = await schedulerFor(agentAid).run(cmd.triggerId, {
2825
+ dryRun: cmd.dryRun === true,
2826
+ ...(cmd.eventPayload !== undefined ? { eventPayload: cmd.eventPayload } : {}),
2827
+ });
2828
+ return {
2829
+ ok: result.ok,
2830
+ result,
2831
+ runId: result.runId,
2832
+ triggerId: result.triggerId,
2833
+ status: result.status,
2834
+ reason: result.reason,
2835
+ conflictRunId: result.conflictRunId,
2836
+ error: result.error,
2837
+ audit: result.audit,
2838
+ };
2839
+ }
2840
+ default:
2841
+ return { ok: false, error: `unknown trigger command: ${cmd.type}` };
2842
+ }
2843
+ });
2844
+ ipcServer.startCpuTracking();
2845
+ // I3: start IPC server after all hooks/executors/providers are registered.
2846
+ await ipcServer.start();
2847
+ // 写入 ready 信号(Control Plane 已可通过 IPC 查询;channel 连接不阻塞启动判定)
2848
+ const readySignalPath = resolvePaths().readySignal;
2849
+ fs.writeFileSync(readySignalPath, String(Date.now()));
2850
+ logger.info(`✓ Ready signal written: ${readySignalPath}`);
2851
+ // 配置 reload 走 IPC `evolagent.reload` 触发,不再用 watchFile。
2852
+ // 双 rename 原子写下 watchFile 的语义会被破坏,且新结构有 N 个 config.json 要监控;
2853
+ // 显式触发更可控。
2854
+ // 优雅关闭
2855
+ let shutdownSignal = 'unknown';
2856
+ const shutdown = async (signal) => {
2857
+ if (signal)
2858
+ shutdownSignal = signal;
2859
+ const pid = process.pid;
2860
+ const ppid = process.ppid;
2861
+ logger.info(`\n\nShutting down gracefully... (signal=${shutdownSignal}, pid=${pid}, ppid=${ppid})`);
2862
+ ipcServer.stopCpuTracking();
2863
+ ipcServer.stop();
2864
+ bindService?.stopCleanup();
2865
+ await permissionGateway.cancelAllPending('daemon_restart');
2866
+ await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.waitForIdle(5_000)));
2867
+ await Promise.all([...triggerSchedulers.values()].map(scheduler => scheduler.stop()));
2868
+ eventBus.publish({
2869
+ type: 'system:shutdown',
2870
+ timestamp: Date.now()
2871
+ });
2872
+ // 断开插件系统的渠道
2873
+ await channelLoader.disconnectAll(channelInstances);
2874
+ for (const inst of channelInstances) {
2875
+ const type = inst.channelType || inst.adapter.channelName;
2876
+ eventBus.publish({ type: 'channel:disconnected', channel: type, channelName: inst.adapter.channelName, reason: 'shutdown' });
2877
+ }
2878
+ // 断开控制 AID(daemon 进程身份)
2879
+ if (controlChannel) {
2880
+ try {
2881
+ await controlChannel.disconnect();
2882
+ }
2883
+ catch { /* ignore */ }
2884
+ }
2885
+ sessionManager.close();
2886
+ removeAll();
2887
+ logger.info('✓ Shutdown complete');
2888
+ process.exit(0);
2889
+ };
2890
+ process.on('SIGINT', () => shutdown('SIGINT'));
2891
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
2892
+ // 全局错误处理:防止未捕获的 WebSocket 错误导致进程崩溃
2893
+ // 特别是 fastaun SDK 在连接超时时,WebSocket 可能发出未被监听的 'error' 事件
2894
+ process.on('uncaughtException', (error) => {
2895
+ // 检查是否是 WebSocket 连接超时相关错误
2896
+ const isWsError = error.message?.includes('WebSocket was closed before the connection was established');
2897
+ const isFastaunError = error.stack?.includes('@agentunion/fastaun');
2898
+ if (isWsError || isFastaunError) {
2899
+ logger.warn(`Caught WebSocket connection error (non-fatal): ${error.message}`);
2900
+ logger.debug(`WebSocket error stack: ${error.stack}`);
2901
+ // 不退出进程,让 AUN 重连机制处理
2902
+ return;
2903
+ }
2904
+ // 其他未捕获错误仍然是致命的
2905
+ logger.error('Uncaught exception:', error);
2906
+ console.error('Uncaught exception:', error);
2907
+ shutdown('uncaughtException');
2908
+ });
2909
+ process.on('unhandledRejection', (reason) => {
2910
+ logger.error('Unhandled promise rejection:', reason);
2911
+ console.error('Unhandled promise rejection:', reason);
2912
+ // Promise rejection 不立即退出,记录后继续运行
2913
+ });
2914
+ // 兜底:进程退出前同步删除 instance 文件(防 async shutdown 未完成就被杀)
2915
+ process.on('exit', () => {
2916
+ removeAll();
2917
+ });
2918
+ }
2919
+ // 仅在直接执行时启动;导入此模块(如单元测试)时不触发 main()。
2920
+ import { isMainScript, onShutdown, commandExists } from './utils/cross-platform.js';
2921
+ if (isMainScript(import.meta.url)) {
2922
+ main().catch((error) => {
2923
+ const msg = `Fatal error: ${error?.stack || error}`;
2924
+ logger.error('Fatal error:', error);
2925
+ console.error(msg); // ensure it lands in stdout.log for self-heal diagnostics
2926
+ process.exit(1);
2927
+ });
2928
+ }