evolcore 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION-0.5.0.md +378 -0
  4. package/README.md +318 -14
  5. package/ROLE_ACCESS_CONTROL.md +174 -0
  6. package/assets/.env.template +4 -0
  7. package/bin/ec-safe-output.js +161 -0
  8. package/bin/ec.js +29 -0
  9. package/dist/agents/baseagent.js +163 -0
  10. package/dist/agents/claude-runner.js +2565 -0
  11. package/dist/agents/codex-app-server-client.js +448 -0
  12. package/dist/agents/codex-runner.js +2682 -0
  13. package/dist/agents/gemini-runner.js +666 -0
  14. package/dist/agents/runner-types.js +75 -0
  15. package/dist/aun/aid/agentmd.js +216 -0
  16. package/dist/aun/aid/client.js +132 -0
  17. package/dist/aun/aid/control-aid.js +91 -0
  18. package/dist/aun/aid/identity.js +518 -0
  19. package/dist/aun/aid/index.js +4 -0
  20. package/dist/aun/aid/store.js +74 -0
  21. package/dist/aun/aid/types.js +1 -0
  22. package/dist/aun/aid/validation.js +21 -0
  23. package/dist/aun/group-identity.js +10 -0
  24. package/dist/aun/msg/group-index.js +6 -0
  25. package/dist/aun/msg/group.js +1231 -0
  26. package/dist/aun/msg/history.js +123 -0
  27. package/dist/aun/msg/index.js +5 -0
  28. package/dist/aun/msg/p2p.js +393 -0
  29. package/dist/aun/msg/payload-type.js +27 -0
  30. package/dist/aun/msg/upload.js +137 -0
  31. package/dist/aun/outbox.js +168 -0
  32. package/dist/aun/rpc/caller.js +42 -0
  33. package/dist/aun/rpc/connection.js +25 -0
  34. package/dist/aun/rpc/index.js +2 -0
  35. package/dist/aun/service-proxy.js +225 -0
  36. package/dist/aun/storage/download.js +29 -0
  37. package/dist/aun/storage/index.js +3 -0
  38. package/dist/aun/storage/manage.js +10 -0
  39. package/dist/aun/storage/upload.js +68 -0
  40. package/dist/channels/aun.js +4164 -0
  41. package/dist/channels/contact-bind-code.js +134 -0
  42. package/dist/channels/daemon.js +422 -0
  43. package/dist/channels/dingtalk.js +1479 -0
  44. package/dist/channels/feishu.js +1865 -0
  45. package/dist/channels/qqbot.js +409 -0
  46. package/dist/channels/wechat.js +817 -0
  47. package/dist/channels/wecom-card.js +101 -0
  48. package/dist/channels/wecom-onboarding.js +82 -0
  49. package/dist/channels/wecom-state.js +191 -0
  50. package/dist/channels/wecom.js +1157 -0
  51. package/dist/cli/agent-command.js +642 -0
  52. package/dist/cli/agent.js +1059 -0
  53. package/dist/cli/aun-commands.js +2003 -0
  54. package/dist/cli/bench.js +1228 -0
  55. package/dist/cli/cli-argv.js +66 -0
  56. package/dist/cli/code-stats.js +329 -0
  57. package/dist/cli/command-log.js +82 -0
  58. package/dist/cli/config-selector.js +69 -0
  59. package/dist/cli/config.js +261 -0
  60. package/dist/cli/contact.js +71 -0
  61. package/dist/cli/ctl-command.js +62 -0
  62. package/dist/cli/daemon-commands.js +2750 -0
  63. package/dist/cli/fs-command.js +1447 -0
  64. package/dist/cli/handoff-command.js +302 -0
  65. package/dist/cli/help.js +35 -0
  66. package/dist/cli/index.js +374 -0
  67. package/dist/cli/init-channel.js +1372 -0
  68. package/dist/cli/init.js +590 -0
  69. package/dist/cli/link-rules.js +240 -0
  70. package/dist/cli/model.js +591 -0
  71. package/dist/cli/net-check.js +723 -0
  72. package/dist/cli/queue-command.js +150 -0
  73. package/dist/cli/raw-key-input.js +25 -0
  74. package/dist/cli/response.js +344 -0
  75. package/dist/cli/restart-monitor.js +480 -0
  76. package/dist/cli/stats.js +609 -0
  77. package/dist/cli/task-context.js +80 -0
  78. package/dist/cli/trigger-command.js +545 -0
  79. package/dist/cli/version.js +93 -0
  80. package/dist/cli/watch-logs.js +33 -0
  81. package/dist/cli/watch-msg.js +673 -0
  82. package/dist/config/boot-log.js +266 -0
  83. package/dist/config/builtin-role-templates.js +42 -0
  84. package/dist/config/builtin-roles.js +91 -0
  85. package/dist/config/config-batch-get.js +11 -0
  86. package/dist/config/config-field-policy.js +261 -0
  87. package/dist/config/config-manager.js +1120 -0
  88. package/dist/config/config-operation-service.js +384 -0
  89. package/dist/config/contact-alias.js +68 -0
  90. package/dist/config/contact-book-store.js +454 -0
  91. package/dist/config/contact-book-v2-startup.js +35 -0
  92. package/dist/config/contact-book.js +224 -0
  93. package/dist/config/contact-operation-service.js +110 -0
  94. package/dist/config/gateway-config.js +858 -0
  95. package/dist/config/lifecycle.js +17 -0
  96. package/dist/config/mention-mode.js +27 -0
  97. package/dist/config/merge.js +161 -0
  98. package/dist/config/owner-policy.js +4 -0
  99. package/dist/config/peer-role-resolver.js +218 -0
  100. package/dist/config/resolved-config-op.js +483 -0
  101. package/dist/config/role-config-v4-startup.js +32 -0
  102. package/dist/config/role-config-v5-startup.js +27 -0
  103. package/dist/config/role-ranks.js +18 -0
  104. package/dist/config/role-schema.js +105 -0
  105. package/dist/config/role-service.js +156 -0
  106. package/dist/config/role-store.js +215 -0
  107. package/dist/config/roles.js +64 -0
  108. package/dist/config/schema-registry.js +154 -0
  109. package/dist/config/snapshot.js +598 -0
  110. package/dist/config-store.js +501 -0
  111. package/dist/core/auth/agent-delegation.js +111 -0
  112. package/dist/core/auth/auth-gateway.js +166 -0
  113. package/dist/core/auth/authenticated-actor.js +6 -0
  114. package/dist/core/auth/authorization-audit.js +119 -0
  115. package/dist/core/auth/operation-authorizer.js +720 -0
  116. package/dist/core/auth/operation-catalog.js +731 -0
  117. package/dist/core/baseagent-loader.js +54 -0
  118. package/dist/core/bootstrap-service.js +175 -0
  119. package/dist/core/capability/capability-manager.js +316 -0
  120. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  121. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  122. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  123. package/dist/core/capability/types.js +27 -0
  124. package/dist/core/causation/audit.js +103 -0
  125. package/dist/core/causation/aun-association.js +111 -0
  126. package/dist/core/causation/context.js +93 -0
  127. package/dist/core/causation/index.js +4 -0
  128. package/dist/core/causation/types.js +2 -0
  129. package/dist/core/channel-loader.js +277 -0
  130. package/dist/core/command/agent-control.js +616 -0
  131. package/dist/core/command/cli-intent-parser.js +225 -0
  132. package/dist/core/command/command-handler.js +1733 -0
  133. package/dist/core/command/connect-menu.js +374 -0
  134. package/dist/core/command/menu-handler.js +3452 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1623 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3066 -0
  139. package/dist/core/daemon-file-cache.js +222 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +810 -0
  142. package/dist/core/evolagent-registry.js +545 -0
  143. package/dist/core/evolagent.js +342 -0
  144. package/dist/core/handoff/dispatcher.js +229 -0
  145. package/dist/core/handoff/mutex.js +46 -0
  146. package/dist/core/handoff/runtime.js +324 -0
  147. package/dist/core/handoff/store.js +537 -0
  148. package/dist/core/handoff/types.js +12 -0
  149. package/dist/core/inference/text-inference.js +173 -0
  150. package/dist/core/interaction-registration.js +10 -0
  151. package/dist/core/interaction-router.js +278 -0
  152. package/dist/core/message/create-status.js +67 -0
  153. package/dist/core/message/im-renderer.js +659 -0
  154. package/dist/core/message/items-formatter.js +76 -0
  155. package/dist/core/message/logical-queue-bridge.js +123 -0
  156. package/dist/core/message/message-bridge.js +874 -0
  157. package/dist/core/message/message-cache.js +56 -0
  158. package/dist/core/message/message-log.js +339 -0
  159. package/dist/core/message/message-processor.js +4 -0
  160. package/dist/core/message/message-queue.js +1446 -0
  161. package/dist/core/message/message-utils.js +76 -0
  162. package/dist/core/message/peer-mode.js +105 -0
  163. package/dist/core/message/pending-hints.js +232 -0
  164. package/dist/core/message/response-depth.js +33 -0
  165. package/dist/core/message/response-engine.js +4020 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/send-receipt.js +24 -0
  168. package/dist/core/message/stream-debouncer.js +139 -0
  169. package/dist/core/message/stream-idle-monitor.js +124 -0
  170. package/dist/core/model/config-scope.js +162 -0
  171. package/dist/core/model/field-scope.js +78 -0
  172. package/dist/core/model/model-catalog.js +227 -0
  173. package/dist/core/model/model-diagnostics.js +182 -0
  174. package/dist/core/model/model-permission.js +90 -0
  175. package/dist/core/permission/approval-gateway.js +1017 -0
  176. package/dist/core/permission/ec-command-parser.js +347 -0
  177. package/dist/core/permission/execution-sandbox.js +16 -0
  178. package/dist/core/permission/index.js +6 -0
  179. package/dist/core/permission/mode.js +24 -0
  180. package/dist/core/permission/sandbox-runtime.js +265 -0
  181. package/dist/core/permission/tool-policy.js +1019 -0
  182. package/dist/core/permission/unix-socket-policy.js +99 -0
  183. package/dist/core/protected-paths.js +332 -0
  184. package/dist/core/relation/peer-identity.js +222 -0
  185. package/dist/core/relation/peer-key.js +1 -0
  186. package/dist/core/role/runtime-policy.js +141 -0
  187. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  188. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  189. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  190. package/dist/core/session/session-file-adapter.js +7 -0
  191. package/dist/core/session/session-file-health.js +45 -0
  192. package/dist/core/session/session-fs-store.js +273 -0
  193. package/dist/core/session/session-key.js +24 -0
  194. package/dist/core/session/session-manager.js +1643 -0
  195. package/dist/core/session/session-mapper.js +100 -0
  196. package/dist/core/session/session-renew.js +314 -0
  197. package/dist/core/session/session-title.js +128 -0
  198. package/dist/core/session/session-turn-coordinator.js +205 -0
  199. package/dist/core/session/session-turns.js +67 -0
  200. package/dist/core/system-channels.js +29 -0
  201. package/dist/eck/baseagent-caps.js +18 -0
  202. package/dist/eck/detect.js +47 -0
  203. package/dist/eck/group-rules-sync.js +345 -0
  204. package/dist/eck/init.js +77 -0
  205. package/dist/eck/kit-renderer.js +359 -0
  206. package/dist/eck/manifest-engine.js +446 -0
  207. package/dist/eck/message-renderer.js +199 -0
  208. package/dist/eck/rules-loader.js +28 -0
  209. package/dist/index.js +2926 -4
  210. package/dist/ipc.js +777 -0
  211. package/dist/paths.js +262 -0
  212. package/dist/product.js +18 -0
  213. package/dist/response-system/context-builder.js +71 -0
  214. package/dist/response-system/coordinator.js +117 -0
  215. package/dist/response-system/decision-executor.js +86 -0
  216. package/dist/response-system/engines/v1/index.js +21 -0
  217. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  218. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  219. package/dist/response-system/engines/v1/types.js +1 -0
  220. package/dist/response-system/extensions.js +41 -0
  221. package/dist/response-system/index.js +6 -0
  222. package/dist/response-system/modes/index.js +7 -0
  223. package/dist/response-system/modes/single-session/index.js +72 -0
  224. package/dist/response-system/queues/fifo-queue.js +44 -0
  225. package/dist/response-system/queues/index.js +6 -0
  226. package/dist/response-system/queues/lifo-queue.js +42 -0
  227. package/dist/response-system/queues/priority-queue.js +63 -0
  228. package/dist/response-system/registry.js +97 -0
  229. package/dist/response-system/resolver.js +37 -0
  230. package/dist/response-system/selector.js +23 -0
  231. package/dist/response-system/types.js +7 -0
  232. package/dist/stats/billing.js +163 -0
  233. package/dist/stats/budget.js +93 -0
  234. package/dist/stats/db.js +403 -0
  235. package/dist/stats/eck-vars.js +89 -0
  236. package/dist/stats/index.js +11 -0
  237. package/dist/stats/normalizer.js +80 -0
  238. package/dist/stats/price-resolver.js +138 -0
  239. package/dist/stats/query.js +763 -0
  240. package/dist/stats/role-budget.js +168 -0
  241. package/dist/stats/writer.js +151 -0
  242. package/dist/trigger/anomaly-store.js +258 -0
  243. package/dist/trigger/audit.js +152 -0
  244. package/dist/trigger/event-source.js +119 -0
  245. package/dist/trigger/feedback.js +685 -0
  246. package/dist/trigger/history.js +290 -0
  247. package/dist/trigger/manager.js +294 -0
  248. package/dist/trigger/parser.js +595 -0
  249. package/dist/trigger/patch.js +155 -0
  250. package/dist/trigger/scheduler.js +1602 -0
  251. package/dist/trigger/script-executor.js +155 -0
  252. package/dist/trigger/state.js +145 -0
  253. package/dist/trigger/types.js +1 -0
  254. package/dist/trigger/validation.js +634 -0
  255. package/dist/types.js +12 -0
  256. package/dist/utils/aid-bind.js +313 -0
  257. package/dist/utils/atomic-write.js +95 -0
  258. package/dist/utils/avatar-upload.js +123 -0
  259. package/dist/utils/cross-platform.js +297 -0
  260. package/dist/utils/ecweb-utils.js +73 -0
  261. package/dist/utils/error-dict.json +153 -0
  262. package/dist/utils/error-utils.js +349 -0
  263. package/dist/utils/instance-registry.js +444 -0
  264. package/dist/utils/locale.js +21 -0
  265. package/dist/utils/log-writer.js +270 -0
  266. package/dist/utils/logger.js +89 -0
  267. package/dist/utils/markdown-to-plain-text.js +20 -0
  268. package/dist/utils/media-cache.js +274 -0
  269. package/dist/utils/model-prices.jsonl +20 -0
  270. package/dist/utils/npm-ops.js +210 -0
  271. package/dist/utils/process-introspect.js +133 -0
  272. package/dist/utils/process-tree-stats.js +271 -0
  273. package/dist/utils/project-path.js +74 -0
  274. package/dist/utils/restart-safety.js +31 -0
  275. package/dist/utils/stats.js +410 -0
  276. package/dist/utils/system-memory.js +62 -0
  277. package/dist/utils/tool-summary.js +284 -0
  278. package/dist/utils/welcome.js +268 -0
  279. package/kits/docs/GUIDE.md +20 -0
  280. package/kits/docs/INDEX.md +66 -0
  281. package/kits/docs/aun/CHEATSHEET.md +19 -0
  282. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  283. package/kits/docs/channels/aun.md +65 -0
  284. package/kits/docs/channels/feishu.md +56 -0
  285. package/kits/docs/context-assembly.md +366 -0
  286. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  287. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  288. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  289. package/kits/docs/eck_templates/runtime.template.md +19 -0
  290. package/kits/docs/evolcore/INDEX.md +68 -0
  291. package/kits/docs/evolcore/agent.md +77 -0
  292. package/kits/docs/evolcore/aid.md +52 -0
  293. package/kits/docs/evolcore/config.md +149 -0
  294. package/kits/docs/evolcore/contact.md +57 -0
  295. package/kits/docs/evolcore/ctl.md +46 -0
  296. package/kits/docs/evolcore/event.md +216 -0
  297. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  298. package/kits/docs/evolcore/fs.md +110 -0
  299. package/kits/docs/evolcore/group-fs.md +17 -0
  300. package/kits/docs/evolcore/group-rules.md +226 -0
  301. package/kits/docs/evolcore/group.md +150 -0
  302. package/kits/docs/evolcore/model.md +50 -0
  303. package/kits/docs/evolcore/msg.md +136 -0
  304. package/kits/docs/evolcore/response.md +75 -0
  305. package/kits/docs/evolcore/rpc.md +37 -0
  306. package/kits/docs/evolcore/self-summary.md +29 -0
  307. package/kits/docs/evolcore/stats.md +83 -0
  308. package/kits/docs/evolcore/storage.md +50 -0
  309. package/kits/docs/evolcore/trigger.md +539 -0
  310. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  311. package/kits/docs/identity/PATH_OPS.md +16 -0
  312. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  313. package/kits/docs/identity/identity-tools.md +26 -0
  314. package/kits/docs/path-registry.md +43 -0
  315. package/kits/docs/prompt-loading-architecture.md +266 -0
  316. package/kits/docs/venues/aun-group.md +45 -0
  317. package/kits/docs/venues/aun-private.md +10 -0
  318. package/kits/docs/venues/client-desktop.md +10 -0
  319. package/kits/docs/venues/client-mobile.md +10 -0
  320. package/kits/docs/venues/feishu-group.md +13 -0
  321. package/kits/docs/venues/feishu-private.md +9 -0
  322. package/kits/docs/venues/group.md +25 -0
  323. package/kits/docs/venues/private.md +10 -0
  324. package/kits/eck_manifest.auxiliary.json +43 -0
  325. package/kits/eck_manifest.json +203 -0
  326. package/kits/eck_message_manifest.json +63 -0
  327. package/kits/migrations/README-role-config-v4.md +32 -0
  328. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  329. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  330. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  331. package/kits/migrations/rename-config-file.mjs +99 -0
  332. package/kits/rules/01-overview.md +142 -0
  333. package/kits/rules/02-navigation.md +76 -0
  334. package/kits/rules/03-identity.md +34 -0
  335. package/kits/rules/04-relation.md +59 -0
  336. package/kits/rules/05-venue.md +44 -0
  337. package/kits/rules/06-channel.md +59 -0
  338. package/kits/schemas/_meta.json +32 -0
  339. package/kits/schemas/agent-config.schema.1.json +177 -0
  340. package/kits/schemas/agent-config.schema.2.json +239 -0
  341. package/kits/schemas/agent-config.schema.3.json +119 -0
  342. package/kits/schemas/agent-config.schema.4.json +208 -0
  343. package/kits/schemas/agent-config.schema.5.json +326 -0
  344. package/kits/schemas/agent-config.schema.6.json +322 -0
  345. package/kits/schemas/contact-book.schema.1.json +36 -0
  346. package/kits/schemas/contact-book.schema.2.json +43 -0
  347. package/kits/schemas/daemon.schema.1.json +90 -0
  348. package/kits/schemas/defaults.schema.1.json +81 -0
  349. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  350. package/kits/schemas/migrations/README.md +28 -0
  351. package/kits/schemas/relation-config.schema.1.json +158 -0
  352. package/kits/schemas/relation-config.schema.2.json +73 -0
  353. package/kits/schemas/relation-config.schema.3.json +50 -0
  354. package/kits/schemas/relation-config.schema.4.json +47 -0
  355. package/kits/schemas/relation-config.schema.5.json +47 -0
  356. package/kits/schemas/role-config.schema.1.json +201 -0
  357. package/kits/schemas/role-registry.schema.1.json +35 -0
  358. package/kits/schemas/single-session.schema.1.json +31 -0
  359. package/kits/templates/bootstrap-welcome.md +15 -0
  360. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  361. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  362. package/kits/templates/message-fragments/inject-default.md +2 -0
  363. package/kits/templates/message-fragments/item.md +2 -0
  364. package/kits/templates/roles/admin.json +9 -0
  365. package/kits/templates/roles/member.json +41 -0
  366. package/kits/templates/roles/owner.json +9 -0
  367. package/kits/templates/roles/visitor.json +40 -0
  368. package/kits/templates/system-fragments/baseagent.md +14 -0
  369. package/kits/templates/system-fragments/bootstrap.md +16 -0
  370. package/kits/templates/system-fragments/channel.md +48 -0
  371. package/kits/templates/system-fragments/commands.md +28 -0
  372. package/kits/templates/system-fragments/identity.md +11 -0
  373. package/kits/templates/system-fragments/relation.md +19 -0
  374. package/kits/templates/system-fragments/session.md +53 -0
  375. package/kits/templates/system-fragments/venue.md +31 -0
  376. package/package.json +50 -15
  377. package/dist/index.d.ts +0 -7
  378. package/dist/index.d.ts.map +0 -1
  379. package/dist/index.js.map +0 -1
@@ -0,0 +1,1733 @@
1
+ import { BaseagentRunnerUnavailableError } from '../../agents/runner-types.js';
2
+ import { renderCommandCardAsText } from '../interaction-router.js';
3
+ import { buildEnvelope, sendInteractionPayload } from '../message/message-utils.js';
4
+ import { resolvePaths, getPackageRoot } from '../../paths.js';
5
+ import { loadDaemonConfig } from '../../config-store.js';
6
+ import { logger } from '../../utils/logger.js';
7
+ import { resolvePeerRoleDetail, roleToSessionIdentity } from '../../config/peer-role-resolver.js';
8
+ import { isManagementRole } from '../../config/builtin-roles.js';
9
+ import { formatPeerKey } from '../relation/peer-identity.js';
10
+ import crypto from 'crypto';
11
+ import path from 'path';
12
+ import { parseDuration, parseTriggerSet, parseTriggerUpdate } from '../../trigger/parser.js';
13
+ import { applyTriggerPatch, replaceEditableTriggerDefinition } from '../../trigger/patch.js';
14
+ import { definitionRevision } from '../../trigger/validation.js';
15
+ import { tryParseChannelKey } from '../channel-loader.js';
16
+ import { displaySessionTitle } from '../session/session-title.js';
17
+ import { buildSessionTurnList } from '../session/session-turns.js';
18
+ import { resolveConfigCommand } from '../../config/resolved-config-op.js';
19
+ import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand } from '../../config/config-operation-service.js';
20
+ import { hashArgv } from '../auth/authorization-audit.js';
21
+ import { executeResolvedContactCommand, resolveContactCommand } from '../../config/contact-operation-service.js';
22
+ import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
23
+ import { validateModelSelectionForRole } from '../model/model-permission.js';
24
+ import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride } from '../role/runtime-policy.js';
25
+ import { parsePeerKey } from '../relation/peer-identity.js';
26
+ import { isQuickCommand } from './slash-gate.js';
27
+ import { handleSlashCommand } from './slash-handler.js';
28
+ import { resolveChatMode } from '../message/peer-mode.js';
29
+ import { execMenuAction as menuExecMenuAction, execMenuForEcweb as menuExecMenuForEcweb, execMenuForControl as menuExecMenuForControl, execMenuQuery as menuExecMenuQuery, execMenuUpdate as menuExecMenuUpdate, getMenuItems as menuGetMenuItems, getSubMenuItems as menuGetSubMenuItems, } from './menu-handler.js';
30
+ export { isProcessLevelOwner } from './menu-handler.js';
31
+ const CLI_EXEC_TIMEOUT_MS = 15_000;
32
+ const CLI_EXEC_MAX_OUTPUT = 128 * 1024;
33
+ /**
34
+ * 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
35
+ * ⚠️ 已禁用:按需求不再修改用户的 ~/.claude/settings.json 文件。
36
+ * 原会把 model/effortLevel 写入 settings.json(仅在找不到 owning agent 的 fallback 路径触发)。
37
+ * 现直接返回成功且不落盘;如需恢复,取消下方注释即可。
38
+ */
39
+ function writeUserSettings(updates) {
40
+ void updates;
41
+ return { success: true };
42
+ /* eslint-disable no-unreachable */
43
+ // try {
44
+ // const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
45
+ // let settings: any = {};
46
+ //
47
+ // if (fs.existsSync(settingsPath)) {
48
+ // settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
49
+ // }
50
+ //
51
+ // if (updates.model !== undefined) settings.model = updates.model;
52
+ // if (updates.effortLevel !== undefined) {
53
+ // if (updates.effortLevel === null) {
54
+ // delete settings.effortLevel;
55
+ // } else {
56
+ // settings.effortLevel = updates.effortLevel;
57
+ // }
58
+ // }
59
+ //
60
+ // const claudeDir = path.join(os.homedir(), '.claude');
61
+ // if (!fs.existsSync(claudeDir)) {
62
+ // fs.mkdirSync(claudeDir, { recursive: true });
63
+ // }
64
+ //
65
+ // fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
66
+ // return { success: true };
67
+ // } catch (error: any) {
68
+ // return { success: false, error: error.message };
69
+ // }
70
+ /* eslint-enable no-unreachable */
71
+ }
72
+ function isAdminRole(role) {
73
+ return isManagementRole(role);
74
+ }
75
+ export class CommandHandler {
76
+ sessionManager;
77
+ messageCache;
78
+ eventBus;
79
+ adapters = new Map();
80
+ policies = new Map();
81
+ channelObjects = new Map(); // name → actual channel instance (for /check)
82
+ channelTypeMap = new Map(); // name → channelType (for grouping)
83
+ processor;
84
+ messageQueue;
85
+ permissionGateway;
86
+ interactionRouter;
87
+ statsCollector;
88
+ agentMap;
89
+ primaryRunnerKey;
90
+ agentRegistry;
91
+ triggerSchedulerResolver;
92
+ triggerSchedulerFallback;
93
+ triggerSchedulerFallbackAid;
94
+ daemonStatusProvider;
95
+ agentDelegationRegistry;
96
+ /**
97
+ * Get the runner for a (channel, baseagent) pair.
98
+ *
99
+ * Resolves the owning EvolAgent via the registry. If the owner is known and
100
+ * the requested baseagent runner is absent, do not silently fall back to a
101
+ * different backend; that would corrupt session metadata.
102
+ */
103
+ getAgent(channel, baseagent) {
104
+ if (channel && baseagent) {
105
+ const owner = this.agentRegistry?.resolveByChannel(channel);
106
+ const evolName = owner?.name || '<unknown>';
107
+ const key = `${evolName}::${baseagent}`;
108
+ if (this.agentMap.has(key))
109
+ return this.agentMap.get(key);
110
+ if (owner) {
111
+ throw new BaseagentRunnerUnavailableError(evolName, baseagent, this.getAvailableBaseagentsForOwner(evolName));
112
+ }
113
+ const globalRunner = [...this.agentMap.entries()]
114
+ .find(([runnerKey]) => runnerKey === baseagent || runnerKey.endsWith(`::${baseagent}`));
115
+ if (globalRunner)
116
+ return globalRunner[1];
117
+ }
118
+ if (this.agentMap.has(this.primaryRunnerKey))
119
+ return this.agentMap.get(this.primaryRunnerKey);
120
+ return this.agentMap.values().next().value;
121
+ }
122
+ getAvailableBaseagentsForOwner(evolName) {
123
+ const prefix = `${evolName}::`;
124
+ return [...this.agentMap.keys()]
125
+ .filter(key => key.startsWith(prefix))
126
+ .map(key => key.slice(prefix.length));
127
+ }
128
+ formatBaseagentUnavailable(error) {
129
+ const available = error.availableBaseagents.length ? error.availableBaseagents.join(', ') : '(none)';
130
+ return `❌ 当前会话绑定的 baseagent 不可用: ${error.baseagent}\nAgent: ${error.evolagentName}\n可用: ${available}\n请使用 /baseagent 切换到可用后端。`;
131
+ }
132
+ /**
133
+ * Return the list of baseagents available to a given channel.
134
+ *
135
+ * Agent channels stay isolated to their owning EvolAgent. Control/daemon
136
+ * channels are virtual and have no owner, so expose the loaded runner types
137
+ * globally for menu.options(name=baseagent).
138
+ */
139
+ getAvailableBaseagents(channel) {
140
+ const owner = this.agentRegistry?.resolveByChannel(channel);
141
+ if (!owner) {
142
+ const result = new Set();
143
+ for (const key of this.agentMap.keys()) {
144
+ const idx = key.indexOf('::');
145
+ result.add(idx >= 0 ? key.slice(idx + 2) : key);
146
+ }
147
+ return [...result];
148
+ }
149
+ const evolName = owner.name || '<unknown>';
150
+ const prefix = `${evolName}::`;
151
+ const result = [];
152
+ for (const key of this.agentMap.keys()) {
153
+ if (key.startsWith(prefix))
154
+ result.push(key.slice(prefix.length));
155
+ }
156
+ return result;
157
+ }
158
+ /** Extract the baseagent component from `primaryRunnerKey` (e.g. `aid::claude` → `claude`). */
159
+ parseDefaultBaseagent() {
160
+ const idx = this.primaryRunnerKey.indexOf('::');
161
+ return idx >= 0 ? this.primaryRunnerKey.slice(idx + 2) : this.primaryRunnerKey;
162
+ }
163
+ constructor(sessionManager, agentRunnerOrMap, messageCache, eventBus, primaryRunnerKey) {
164
+ this.sessionManager = sessionManager;
165
+ this.messageCache = messageCache;
166
+ this.eventBus = eventBus;
167
+ if (agentRunnerOrMap instanceof Map) {
168
+ this.agentMap = agentRunnerOrMap;
169
+ this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
170
+ }
171
+ else {
172
+ // 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
173
+ this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
174
+ this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
175
+ }
176
+ }
177
+ /** 注入 EvolAgentRegistry,用于判断通道是否被 EvolAgent 管理 */
178
+ setAgentRegistry(registry) {
179
+ this.agentRegistry = registry;
180
+ }
181
+ setTriggerSchedulerResolver(resolver) {
182
+ this.triggerSchedulerResolver = resolver;
183
+ }
184
+ /** Compatibility entrypoint for older tests/callers that inject a single scheduler. */
185
+ setTriggerScheduler(scheduler, manager) {
186
+ this.triggerSchedulerFallback = scheduler;
187
+ this.triggerSchedulerFallbackAid = manager?.agentAid;
188
+ this.triggerSchedulerResolver = (agentAid) => {
189
+ if (!this.triggerSchedulerFallbackAid || agentAid === this.triggerSchedulerFallbackAid) {
190
+ return scheduler;
191
+ }
192
+ return undefined;
193
+ };
194
+ }
195
+ setDaemonStatusProvider(provider) {
196
+ this.daemonStatusProvider = provider;
197
+ }
198
+ /** 返回管理当前通道的 EvolAgent,无则返回 null */
199
+ getOwningAgent(channel) {
200
+ if (!this.agentRegistry)
201
+ return null;
202
+ return this.agentRegistry.resolveByChannel(channel);
203
+ }
204
+ getTriggerSchedulerForChannel(channel) {
205
+ const agentAid = this.getOwningAgent(channel)?.aid ?? tryParseChannelKey(channel)?.selfAID;
206
+ return agentAid ? this.triggerSchedulerResolver?.(agentAid) : this.triggerSchedulerFallback;
207
+ }
208
+ getTriggerSchedulerForAgent(agentAid) {
209
+ return this.triggerSchedulerResolver?.(agentAid) ?? (this.triggerSchedulerFallbackAid === agentAid ? this.triggerSchedulerFallback : undefined);
210
+ }
211
+ triggerSourceFromSchedule(scheduleType, scheduleValue) {
212
+ if (scheduleType === 'once')
213
+ return { type: 'once' };
214
+ if (scheduleType === 'delay')
215
+ return { type: 'delay', afterMs: this.triggerDurationMs(scheduleValue) };
216
+ if (scheduleType === 'at')
217
+ return { type: 'at', at: scheduleValue };
218
+ if (scheduleType === 'cron')
219
+ return { type: 'cron', expression: scheduleValue };
220
+ if (scheduleType === 'interval')
221
+ return { type: 'interval', everyMs: this.triggerDurationMs(scheduleValue) };
222
+ if (scheduleType === 'event')
223
+ return { type: 'event', eventPattern: scheduleValue };
224
+ throw new Error(`unsupported scheduleType: ${scheduleType}`);
225
+ }
226
+ triggerDurationMs(value) {
227
+ const numeric = Number(value);
228
+ if (Number.isFinite(numeric))
229
+ return numeric;
230
+ const parsed = parseDuration(value);
231
+ if (parsed != null)
232
+ return parsed;
233
+ return Number.NaN;
234
+ }
235
+ scheduleViewFromSource(source) {
236
+ switch (source.type) {
237
+ case 'once': return { scheduleType: 'once', scheduleValue: '' };
238
+ case 'delay': return { scheduleType: 'delay', scheduleValue: String(source.afterMs) };
239
+ case 'at': return { scheduleType: 'at', scheduleValue: source.at };
240
+ case 'cron': return { scheduleType: 'cron', scheduleValue: source.expression };
241
+ case 'interval': return { scheduleType: 'interval', scheduleValue: String(source.everyMs) };
242
+ case 'event': return { scheduleType: 'event', scheduleValue: source.eventPattern };
243
+ }
244
+ }
245
+ triggerPrompt(definition) {
246
+ return definition.execution.type === 'script'
247
+ ? (definition.feedback.onReply?.template ?? '')
248
+ : (definition.execution.prompt ?? '');
249
+ }
250
+ definitionToTriggerView(definition, scheduler) {
251
+ const target = this.primaryFeedbackTarget(definition);
252
+ const schedule = this.scheduleViewFromSource(definition.source);
253
+ let schedulerDetails;
254
+ try {
255
+ schedulerDetails = scheduler?.show(definition.id);
256
+ }
257
+ catch { /* trigger state is best-effort for views */ }
258
+ // 运行统计(fireCount/failCount/lastFiredAt/lastResult)不写入 trigger.json,
259
+ // 按需从 append-only trigger history 汇总。
260
+ let stats = { fireCount: 0, failCount: 0 };
261
+ try {
262
+ if (scheduler?.stats)
263
+ stats = scheduler.stats(definition.id);
264
+ }
265
+ catch { /* trigger history 缺失/损坏时退回零值 */ }
266
+ return {
267
+ id: definition.id,
268
+ name: definition.name,
269
+ enabled: definition.enabled,
270
+ scheduleType: schedule.scheduleType,
271
+ scheduleValue: schedule.scheduleValue,
272
+ nextFireAt: definition.source.type === 'delay'
273
+ ? definition.createdAt + definition.source.afterMs
274
+ : definition.source.type === 'at'
275
+ ? new Date(definition.source.at).getTime()
276
+ : definition.source.type === 'event'
277
+ ? undefined
278
+ : schedulerDetails?.schedule?.nextFireAt,
279
+ targetChannel: target?.channelKey,
280
+ targetChannelName: target?.channelKey,
281
+ targetChannelId: target?.channelId,
282
+ targetChannelType: target ? this.resolveChannelType(target.channelKey) : undefined,
283
+ executionType: definition.execution.type,
284
+ feedbackStrategy: definition.feedback.strategy,
285
+ targetSessionStrategy: target?.session ?? 'main',
286
+ targetThreadId: target?.threadId,
287
+ model: definition.execution.model,
288
+ effort: definition.execution.effort,
289
+ permissionMode: definition.execution.permissionMode,
290
+ prompt: this.triggerPrompt(definition),
291
+ createdByPeerId: definition.origin?.peerId ?? '',
292
+ createdByChannel: definition.origin?.channelKey ?? '',
293
+ schedulerAid: definition.agentAid,
294
+ fireCount: stats.fireCount,
295
+ failCount: stats.failCount,
296
+ lastFiredAt: stats.lastFiredAt,
297
+ lastResult: stats.lastResult,
298
+ lastScheduledAt: schedulerDetails?.schedule?.lastScheduledAt,
299
+ createdAt: definition.createdAt,
300
+ updatedAt: definition.updatedAt,
301
+ status: definition.enabled ? 'active' : 'disabled',
302
+ limits: definition.limits,
303
+ limitState: schedulerDetails?.limitState,
304
+ subscription: schedulerDetails?.subscription,
305
+ };
306
+ }
307
+ primaryFeedbackTarget(definition) {
308
+ if (definition.feedback.strategy === 'target')
309
+ return definition.feedback.target;
310
+ if (!definition.origin)
311
+ return undefined;
312
+ return {
313
+ channelKey: definition.origin.channelKey,
314
+ channelId: definition.origin.channelId,
315
+ session: definition.origin.session,
316
+ threadId: definition.origin.threadId,
317
+ };
318
+ }
319
+ canAccessTriggerDefinition(definition, peerId, channel, isAdmin) {
320
+ if (isAdmin)
321
+ return true;
322
+ return definition.origin?.peerId === peerId && definition.origin?.channelKey === channel;
323
+ }
324
+ triggerPermissionFromParsed(parsed) {
325
+ return parsed.permissionMode ?? undefined;
326
+ }
327
+ validateTriggerPermissionModeForRole(mode, isAdmin, role, selfAid) {
328
+ if (!mode)
329
+ return undefined;
330
+ if (!role) {
331
+ if (mode === 'bypass' && !isAdmin)
332
+ return '无权限:只有 owner/admin 可以创建或修改 bypass trigger';
333
+ return undefined;
334
+ }
335
+ const decision = constrainRuntimePermissionMode({ selfAid, role, requestedValue: mode });
336
+ if (decision.constrained)
337
+ return `无权限:角色 ${role} 不能使用 ${mode} trigger permissionMode`;
338
+ return undefined;
339
+ }
340
+ triggerRoleContext(selfAid, channel, peerId, chatType) {
341
+ const owning = this.getOwningAgent(channel);
342
+ if (!owning || owning.aid !== selfAid)
343
+ return {};
344
+ const role = resolvePeerRoleDetail({
345
+ selfAid,
346
+ channelKey: channel,
347
+ channelType: this.resolveChannelType(channel),
348
+ chatType: chatType === 'group' ? 'group' : 'private',
349
+ actorId: peerId,
350
+ conversationId: peerId,
351
+ }).effectiveRole;
352
+ return { role: role || 'none', baseagent: owning.baseagent };
353
+ }
354
+ validateTriggerRuntimeFields(definition, role, baseagent) {
355
+ if (!role)
356
+ return undefined;
357
+ const executionBaseagent = definition.execution.type === 'trigger_session'
358
+ ? definition.execution.baseagent ?? baseagent
359
+ : baseagent;
360
+ if (definition.execution.model) {
361
+ const modelDecision = validateModelSelectionForRole({
362
+ role,
363
+ baseagent: executionBaseagent,
364
+ requestedModel: definition.execution.model,
365
+ selfAid: definition.agentAid,
366
+ });
367
+ if (!modelDecision.ok)
368
+ return modelDecision.message || '当前角色不允许使用该 Trigger 模型';
369
+ }
370
+ if (definition.execution.effort) {
371
+ const base = executionBaseagent || 'claude';
372
+ const effortDecision = validateRuntimeStringFieldOverride({
373
+ selfAid: definition.agentAid,
374
+ role,
375
+ field: `baseagents.${base}.${base === 'codex' ? 'reasoning' : 'effort'}`,
376
+ value: definition.execution.effort,
377
+ });
378
+ if (!effortDecision.ok)
379
+ return effortDecision.message || '当前角色不允许使用该 Trigger 推理强度';
380
+ }
381
+ return undefined;
382
+ }
383
+ findTriggerDefinition(scheduler, nameOrId, peerId, channel, isAdmin) {
384
+ return scheduler
385
+ .list({ all: true })
386
+ .find(definition => (definition.id === nameOrId || definition.name === nameOrId)
387
+ && this.canAccessTriggerDefinition(definition, peerId, channel, isAdmin));
388
+ }
389
+ async buildTriggerDefinitionFromParsed(parsed, channel, channelId, peerId, isAdmin, messageId, chatType, threadId) {
390
+ const now = Date.now();
391
+ const id = `trig_${now}_${crypto.randomBytes(4).toString('hex')}`;
392
+ const name = parsed.name ?? `trigger-${now.toString(36)}`;
393
+ const feedbackChannel = parsed.targetChannel ?? channel;
394
+ if (parsed.targetChannel && !this.adapters.has(parsed.targetChannel)) {
395
+ return { error: `目标渠道不存在或未启用:${parsed.targetChannel}` };
396
+ }
397
+ const targetChannelType = this.resolveChannelType(feedbackChannel);
398
+ const feedbackChannelId = parsed.targetChannelId ?? peerId;
399
+ if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChannelId.includes('.')) {
400
+ return { error: `AUN 渠道的 --target-channel-id 必须是 AID 格式(如 user.agentid.pub),收到:"${parsed.targetChannelId}"` };
401
+ }
402
+ const schedulerAid = parsed.agentId
403
+ ?? this.getOwningAgent(feedbackChannel)?.aid
404
+ ?? tryParseChannelKey(feedbackChannel)?.selfAID
405
+ ?? this.triggerSchedulerFallbackAid;
406
+ const scheduler = schedulerAid ? this.getTriggerSchedulerForAgent(schedulerAid) : this.triggerSchedulerFallback;
407
+ if (!schedulerAid || !scheduler) {
408
+ return { error: `目标 agent 不存在或未就绪:${schedulerAid ?? feedbackChannel}` };
409
+ }
410
+ const target = {
411
+ channelKey: feedbackChannel,
412
+ channelId: feedbackChannelId,
413
+ session: parsed.targetSession,
414
+ threadId: parsed.targetSession === 'thread' ? parsed.targetThreadId : undefined,
415
+ };
416
+ if (target.session === 'thread') {
417
+ const adapter = this.adapters.get(feedbackChannel);
418
+ if (!adapter?.capabilities.thread)
419
+ return { error: '目标渠道不支持 thread 会话' };
420
+ if (!target.threadId)
421
+ return { error: '--target-session thread 需要 --target-thread-id' };
422
+ }
423
+ const originIsGroup = chatType === 'group';
424
+ const activeSession = originIsGroup
425
+ ? undefined
426
+ : this.sessionManager.getActiveSessionSync(channel, peerId, this.resolveChannelType(channel), schedulerAid);
427
+ const origin = {
428
+ channelKey: channel,
429
+ channelType: this.resolveChannelType(channel),
430
+ channelId: peerId,
431
+ session: !originIsGroup && threadId ? 'thread' : 'main',
432
+ threadId: !originIsGroup ? threadId : undefined,
433
+ peerId,
434
+ sessionKey: activeSession?.sessionKey,
435
+ };
436
+ if (!isAdmin && parsed.feedbackStrategy === 'target' && !sameFeedbackTarget(target, origin)) {
437
+ return { error: '无权限:非 admin 只能把 trigger 投递到来源会话' };
438
+ }
439
+ const source = this.triggerSourceFromSchedule(parsed.scheduleType, parsed.scheduleValue);
440
+ if (parsed.timezone && source.type === 'cron') {
441
+ source.timezone = parsed.timezone;
442
+ }
443
+ const script = parsed.scriptPath ? {
444
+ path: parsed.scriptPath,
445
+ runtime: parsed.scriptRuntime,
446
+ args: parsed.scriptArgs,
447
+ timeoutMs: parsed.scriptTimeoutMs ?? 30_000,
448
+ } : undefined;
449
+ const permissionMode = this.triggerPermissionFromParsed(parsed);
450
+ const triggerRole = this.triggerRoleContext(schedulerAid, channel, peerId, chatType);
451
+ const permissionError = this.validateTriggerPermissionModeForRole(permissionMode, isAdmin, triggerRole.role, schedulerAid);
452
+ if (permissionError)
453
+ return { error: permissionError };
454
+ if (parsed.baseagent !== undefined) {
455
+ if (typeof parsed.baseagent !== 'string' || !parsed.baseagent.trim()) {
456
+ return { error: 'Trigger baseagent 不能为空' };
457
+ }
458
+ if (parsed.executionType !== 'trigger_session') {
459
+ return { error: 'Trigger baseagent 仅适用于 trigger_session' };
460
+ }
461
+ }
462
+ const triggerOwner = this.agentRegistry?.get?.(schedulerAid) ?? this.getOwningAgent(channel);
463
+ const executionBaseagent = parsed.executionType === 'trigger_session'
464
+ ? parsed.baseagent?.trim() ?? triggerOwner?.baseagent ?? triggerRole.baseagent
465
+ : undefined;
466
+ if (parsed.executionType === 'trigger_session') {
467
+ if (!executionBaseagent)
468
+ return { error: '无法确定 Trigger 会话的 baseagent' };
469
+ const ownerName = triggerOwner?.name || '<unknown>';
470
+ const available = this.getAvailableBaseagentsForOwner(ownerName);
471
+ if (!available.includes(executionBaseagent)) {
472
+ return {
473
+ error: `Trigger baseagent 不可用:${executionBaseagent}(可用:${available.join(' / ') || '无'})`,
474
+ };
475
+ }
476
+ }
477
+ const definition = {
478
+ $schema_version: 3.1,
479
+ id,
480
+ agentAid: schedulerAid,
481
+ enabled: true,
482
+ name,
483
+ createdAt: now,
484
+ updatedAt: now,
485
+ origin,
486
+ source,
487
+ execution: {
488
+ type: parsed.executionType,
489
+ ...(script ? { script } : { prompt: parsed.prompt || '' }),
490
+ ...(parsed.executionType === 'trigger_session' ? {
491
+ thread: parsed.triggerThread ?? 'by_trigger',
492
+ baseagent: executionBaseagent,
493
+ } : {}),
494
+ model: parsed.model,
495
+ effort: parsed.effort,
496
+ ...(permissionMode !== undefined ? { permissionMode } : {}),
497
+ onError: 'retry',
498
+ noopSentinel: '[[NOOP]]',
499
+ },
500
+ feedback: {
501
+ strategy: parsed.feedbackStrategy,
502
+ ...(parsed.feedbackStrategy === 'target' ? { target } : {}),
503
+ },
504
+ reliability: {
505
+ concurrency: 'forbid',
506
+ missedPolicy: 'run_once',
507
+ retry: { maxAttempts: 0, backoffMs: 30_000 },
508
+ },
509
+ limits: this.limitsFromParsed(parsed),
510
+ };
511
+ const runtimeFieldError = this.validateTriggerRuntimeFields(definition, triggerRole.role, triggerRole.baseagent);
512
+ if (runtimeFieldError)
513
+ return { error: runtimeFieldError };
514
+ return { definition, scheduler };
515
+ }
516
+ /** 返回当前通道的有效项目路径:从 owning agent 取。*/
517
+ getEffectiveDefaultPath(channel) {
518
+ const owning = this.getOwningAgent(channel);
519
+ if (owning)
520
+ return owning.projectPath;
521
+ return process.cwd();
522
+ }
523
+ /**
524
+ * 持久化 baseagent.model:写到 agent config.json;找不到 owning agent 时
525
+ * 退到用户级 ~/.claude/settings.json(Claude 专用)。
526
+ */
527
+ persistBaseagentModel(channel, baseagentName, newModel) {
528
+ const owning = this.getOwningAgent(channel);
529
+ if (owning) {
530
+ try {
531
+ owning.setBaseagentModel(newModel, baseagentName);
532
+ }
533
+ catch (e) {
534
+ return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
535
+ }
536
+ return undefined;
537
+ }
538
+ // 无 owning agent(罕见,新结构下应当不会发生)→ 仅 Claude 走用户级 fallback
539
+ if (baseagentName !== 'claude') {
540
+ return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
541
+ }
542
+ const updates = {};
543
+ if (newModel)
544
+ updates.model = newModel;
545
+ const writeResult = writeUserSettings(updates);
546
+ // writeUserSettings 已禁用写入,总是返回 success: true
547
+ if (!writeResult.success) {
548
+ return `⚠️ 写入用户配置失败: ${writeResult.error}`;
549
+ }
550
+ return undefined;
551
+ }
552
+ /**
553
+ * 持久化 baseagent.effort:写到 agent config.json;找不到时退到用户级 settings。
554
+ */
555
+ persistBaseagentEffort(channel, baseagentName, newEffort) {
556
+ const owning = this.getOwningAgent(channel);
557
+ if (owning) {
558
+ try {
559
+ owning.setBaseagentEffort(newEffort, baseagentName);
560
+ }
561
+ catch (e) {
562
+ return `⚠️ 写入 agent config 失败: ${e?.message || e}`;
563
+ }
564
+ return undefined;
565
+ }
566
+ if (baseagentName !== 'claude') {
567
+ return `⚠️ 找不到通道 "${channel}" 所属的 self-agent`;
568
+ }
569
+ const updates = { effortLevel: newEffort ?? null };
570
+ const writeResult = writeUserSettings(updates);
571
+ // writeUserSettings 已禁用写入,总是返回 success: true
572
+ if (!writeResult.success) {
573
+ return `⚠️ 写入用户配置失败: ${writeResult.error}`;
574
+ }
575
+ return undefined;
576
+ }
577
+ /** 项目列表快捷访问(无 channel 上下文时的 fallback,尽量不用) */
578
+ get projects() {
579
+ return {};
580
+ }
581
+ /** 根据项目路径查找配置中的项目名称 */
582
+ getConfiguredProjectName(projectPath) {
583
+ return Object.entries(this.projects).find(([_, p]) => p === projectPath)?.[0];
584
+ }
585
+ /** 根据项目路径查找项目名称(未配置时回退到目录名) */
586
+ getProjectName(projectPath) {
587
+ return this.getConfiguredProjectName(projectPath) || path.basename(projectPath);
588
+ }
589
+ /** 格式化运行时间 */
590
+ formatUptime(ms) {
591
+ const sec = Math.floor(ms / 1000);
592
+ const d = Math.floor(sec / 86400);
593
+ const h = Math.floor((sec % 86400) / 3600);
594
+ const m = Math.floor((sec % 3600) / 60);
595
+ const s = sec % 60;
596
+ const parts = [];
597
+ if (d > 0)
598
+ parts.push(`${d}天`);
599
+ if (h > 0)
600
+ parts.push(`${h}时`);
601
+ if (m > 0)
602
+ parts.push(`${m}分`);
603
+ if (parts.length === 0)
604
+ parts.push(`${s}秒`);
605
+ return parts.join('');
606
+ }
607
+ /** 获取消息队列 key:话题用 session.id,主会话用 channel-channelId */
608
+ getQueueKey(session, _channel, _channelId) {
609
+ // 队列和 agent 均使用 session.id 作为 key
610
+ return session?.id || '';
611
+ }
612
+ /** 从 session 提取渠道预构建的回复上下文 */
613
+ getReplyContext(session) {
614
+ return session.metadata?.replyContext;
615
+ }
616
+ /**
617
+ * 发送 CommandCard 卡片。卡片成功返回 null(调用方直接 return),失败返回降级文本。
618
+ * CommandCard 不进 InteractionRouter,按钮点击由 channel 直接构造伪命令入站消息。
619
+ *
620
+ * 走统一 adapter.send(envelope, { kind: 'interaction', ... }) 入口。
621
+ */
622
+ async sendCommandCard(opts) {
623
+ const adapter = this.adapters.get(opts.channel);
624
+ if (opts.interaction.kind.kind !== 'command-card') {
625
+ logger.warn(`[CommandHandler] sendCommandCard called with non-CommandCard kind`);
626
+ return null;
627
+ }
628
+ const card = opts.interaction.kind;
629
+ if (opts.canWrite === false)
630
+ return renderCommandCardAsText(card);
631
+ if (!adapter?.send)
632
+ return renderCommandCardAsText(card);
633
+ try {
634
+ const envelope = buildEnvelope({
635
+ channel: opts.channel,
636
+ channelId: opts.channelId,
637
+ agentName: this.agentRegistry?.resolveByChannel(opts.channel)?.name,
638
+ replyContext: opts.replyCtx,
639
+ });
640
+ const fallbackText = renderCommandCardAsText(card);
641
+ const messageId = await sendInteractionPayload(adapter, envelope, opts.interaction, fallbackText, opts.replyCtx);
642
+ if (messageId)
643
+ return null;
644
+ }
645
+ catch (e) {
646
+ logger.warn(`[CommandHandler] sendCommandCard failed: ${e}`);
647
+ }
648
+ return renderCommandCardAsText(card);
649
+ }
650
+ /**
651
+ * 通用降级应答入口:按 (sessionId, fallbackCommand) 查找 pending interaction 并路由。
652
+ * 返回 { matched: true } 表示已处理,调用方直接返回 result。
653
+ */
654
+ async handleInteractionFallback(command, args, sessionId, userId) {
655
+ if (!this.interactionRouter)
656
+ return { matched: false };
657
+ const pendingId = this.interactionRouter.findPendingByCommand(sessionId, command);
658
+ if (!pendingId)
659
+ return { matched: false };
660
+ const initiatorId = this.interactionRouter.getInitiator(pendingId);
661
+ if (initiatorId && initiatorId !== userId) {
662
+ return { matched: true, result: '⚠️ 仅卡片发起者可应答' };
663
+ }
664
+ const handled = this.interactionRouter.handle({
665
+ type: 'interaction.response',
666
+ id: pendingId,
667
+ action: args,
668
+ operatorId: userId,
669
+ });
670
+ return { matched: true, result: handled ? '✓ 已回答' : '⚠️ 无法验证应答者身份' };
671
+ }
672
+ setProcessor(processor) {
673
+ this.processor = processor;
674
+ }
675
+ setMessageQueue(messageQueue) {
676
+ this.messageQueue = messageQueue;
677
+ }
678
+ setPermissionGateway(gateway) {
679
+ this.permissionGateway = gateway;
680
+ }
681
+ setInteractionRouter(router) {
682
+ this.interactionRouter = router;
683
+ }
684
+ setStatsCollector(collector) {
685
+ this.statsCollector = collector;
686
+ }
687
+ registerAdapter(adapter) {
688
+ this.adapters.set(adapter.channelName, adapter);
689
+ }
690
+ registerChannel(name, channel, channelType) {
691
+ this.channelObjects.set(name, channel);
692
+ if (channelType)
693
+ this.channelTypeMap.set(name, channelType);
694
+ }
695
+ hasRegisteredChannel(channelName, channelType) {
696
+ if (!this.channelObjects.has(channelName))
697
+ return false;
698
+ return channelType === undefined || this.resolveChannelType(channelName) === channelType;
699
+ }
700
+ /** 将实例名解析为渠道类型(用于 session 查询) */
701
+ resolveChannelType(channelName) {
702
+ return this.channelTypeMap.get(channelName) || tryParseChannelKey(channelName)?.type || channelName;
703
+ }
704
+ /** CommandCard success is acknowledged by the card UI; failures still return command.error text. */
705
+ shouldSuppressCardTriggerResult(source, _channel) {
706
+ return source === 'card-trigger';
707
+ }
708
+ /**
709
+ * 从 channel key(<type>#<selfAID>#<name>)解析本地身份 AID。
710
+ * 非 evolagent 通道(裸 channelType,如 'feishu')解析失败返回 undefined。
711
+ * aun 通道创建 session 时必须提供 selfAID,故所有 getOrCreateSession 调用都经此兜底。
712
+ */
713
+ resolveSelfAID(channel) {
714
+ return tryParseChannelKey(channel)?.selfAID;
715
+ }
716
+ resolveCtlIdentity(session, userId) {
717
+ const parsed = tryParseChannelKey(session.channel);
718
+ const owningAgent = this.getOwningAgent(session.channel);
719
+ const selfAid = session.selfAID || owningAgent?.aid || parsed?.selfAID;
720
+ const channelType = session.channelType || parsed?.type || this.resolveChannelType(session.channel);
721
+ const chatType = session.chatType === 'group' ? 'group' : 'private';
722
+ const actorId = userId || session.metadata?.peerId;
723
+ const conversationId = chatType === 'group'
724
+ ? (session.metadata?.groupId || session.channelId)
725
+ : actorId;
726
+ if (!selfAid || !actorId || !conversationId) {
727
+ const fallback = session.identity ?? this.sessionManager.resolveIdentity(session.channel, userId, chatType, conversationId ?? undefined);
728
+ logger.info(`[ctl] identity fallback: sessionId=${session.id} role=${fallback.role} selfAid=${selfAid ?? 'none'} actor=${actorId ?? 'none'} conversation=${conversationId ?? 'none'}`);
729
+ return fallback;
730
+ }
731
+ const detail = resolvePeerRoleDetail({
732
+ selfAid,
733
+ channelKey: session.channel,
734
+ channelType,
735
+ chatType,
736
+ actorId,
737
+ conversationId,
738
+ peerType: session.metadata?.peerType,
739
+ });
740
+ logger.info(`[ctl] identity resolved: sessionId=${session.id} role=${detail.effectiveRole} source=${detail.source} selfAid=${selfAid} actor=${actorId} conversation=${conversationId}`);
741
+ return roleToSessionIdentity(detail.effectiveRole);
742
+ }
743
+ registerPolicy(channelName, policy) {
744
+ this.policies.set(channelName, policy);
745
+ }
746
+ /**
747
+ * 注销渠道(热重载断开渠道时调用)。清理所有按实例名登记的 map,
748
+ * 避免死实例残留在 /status、菜单路由和 adapter 查找里。
749
+ */
750
+ unregisterChannel(channelName) {
751
+ this.adapters.delete(channelName);
752
+ this.channelObjects.delete(channelName);
753
+ this.channelTypeMap.delete(channelName);
754
+ this.policies.delete(channelName);
755
+ }
756
+ getAdapter(channelName) {
757
+ // 先按实例名查找,再按 channelType 查找
758
+ let adapter = this.adapters.get(channelName);
759
+ if (adapter)
760
+ return adapter;
761
+ for (const [name, a] of this.adapters) {
762
+ if ((this.channelTypeMap.get(name) || name) === channelName)
763
+ return a;
764
+ }
765
+ return undefined;
766
+ }
767
+ getPolicy(channel) {
768
+ return this.policies.get(channel) || {
769
+ canSwitchProject: () => true,
770
+ canListProjects: () => true,
771
+ canCreateSession: () => true,
772
+ canDeleteSession: () => true,
773
+ canImportCliSession: () => true,
774
+ messagePrefix: () => '',
775
+ showMiddleResult: () => true,
776
+ showIdleMonitor: () => true,
777
+ accumulateErrors: () => true,
778
+ };
779
+ }
780
+ resolveMenuChatType(channel, channelId, explicit) {
781
+ if (explicit)
782
+ return explicit;
783
+ const active = this.sessionManager.getActiveSessionSync(channel, channelId);
784
+ return active?.chatType === 'group' ? 'group' : 'private';
785
+ }
786
+ canReadTopics(role) {
787
+ return role !== 'none';
788
+ }
789
+ canDeleteTopic(role, chatType, topic, userId) {
790
+ if (role === 'none')
791
+ return false;
792
+ if (isAdminRole(role))
793
+ return true;
794
+ if (chatType === 'group')
795
+ return false;
796
+ return !!userId && topic.metadata?.peerId === userId;
797
+ }
798
+ buildTopicMenuItem(s) {
799
+ const displayName = displaySessionTitle(s.name, s.threadId || s.id.slice(0, 8));
800
+ const item = {
801
+ value: s.threadId,
802
+ label: displayName,
803
+ };
804
+ if (s.agentSessionId) {
805
+ item.agentSessionId = s.agentSessionId;
806
+ const fileInfo = this.sessionManager.getSessionFileInfo(s.projectPath, s.agentSessionId, s.baseagent);
807
+ if (fileInfo.turns)
808
+ item.turns = fileInfo.turns;
809
+ const firstMsg = this.sessionManager.readSessionFirstMessage(s.projectPath, s.agentSessionId, s.baseagent);
810
+ if (firstMsg)
811
+ item.preview = firstMsg.length > 80 ? firstMsg.slice(0, 80) + '...' : firstMsg;
812
+ }
813
+ if (s.updatedAt)
814
+ item.lastActive = s.updatedAt;
815
+ return item;
816
+ }
817
+ /**
818
+ * 返回结构化命令菜单(供 menu.query 使用)
819
+ * owner 看到全部命令,admin 看到管理级命令(不含 owner-only),visitor/member 仅看到用户级命令
820
+ */
821
+ getMenuItems(role, chatType = 'private', scope = 'agent') {
822
+ return menuGetMenuItems.call(this, role, chatType, scope);
823
+ }
824
+ /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
825
+ async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
826
+ return await menuGetSubMenuItems.call(this, cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel, authSubject, source);
827
+ }
828
+ // ── Menu Protocol exec ────────────────────────────────────────────────
829
+ async loadMenuContext(channel, channelId) {
830
+ const session = await this.sessionManager.getActiveSession(channel, channelId);
831
+ const evolagent = this.agentRegistry?.resolveByChannel(channel) ?? null;
832
+ return { session, evolagent };
833
+ }
834
+ requireSession(s) {
835
+ return s ? null : { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
836
+ }
837
+ /** menu.query — 查询当前值。 */
838
+ async execMenuQuery(cmd, channel, channelId, userId, args, explicitChatType, fromControlChannel = false, overrideIdentity, authSubject, source = 'menu') {
839
+ return await menuExecMenuQuery.call(this, cmd, channel, channelId, userId, args, explicitChatType, fromControlChannel, overrideIdentity, authSubject, source);
840
+ }
841
+ /** menu.update — 写入新值。 */
842
+ async execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu') {
843
+ return await menuExecMenuUpdate.call(this, cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel, args, authSubject, source);
844
+ }
845
+ /** menu.action — 触发动词。 */
846
+ async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu') {
847
+ return await menuExecMenuAction.call(this, cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel, authSubject, source);
848
+ }
849
+ /** ECWeb 专用入口:身份只取可信 IPC 信封。 */
850
+ async execMenuForEcweb(payload, auth) {
851
+ return await menuExecMenuForEcweb.call(this, payload, auth);
852
+ }
853
+ /** 控制 AID channel 专用入口:peerId 须 ∈ evolcore.owners,全量权限。 */
854
+ async execMenuForControl(payload, peerId) {
855
+ return await menuExecMenuForControl.call(this, payload, peerId);
856
+ }
857
+ /**
858
+ * CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
859
+ * 不 in-process 调用(CLI handler 用 console.log + process.exit,spawn 行为与终端一致且隔离)。
860
+ * 调用方已完成 owner 校验与白名单过滤。
861
+ */
862
+ async execCliPassthrough(argv, authenticatedRole) {
863
+ const { spawn } = await import('child_process');
864
+ const cliEntry = path.join(getPackageRoot(), 'dist', 'cli', 'index.js');
865
+ const startedAt = Date.now();
866
+ return await new Promise((resolve) => {
867
+ let stdout = '';
868
+ let stderr = '';
869
+ let total = 0;
870
+ let truncated = false;
871
+ let settled = false;
872
+ let stdoutChunks = 0;
873
+ let stderrChunks = 0;
874
+ const child = spawn('node', [cliEntry, ...argv], {
875
+ env: {
876
+ ...process.env,
877
+ EVOLCORE_HOME: resolvePaths().root,
878
+ ...(authenticatedRole ? { [AUTHENTICATED_CONFIG_ROLE_ENV]: authenticatedRole } : {}),
879
+ },
880
+ windowsHide: true,
881
+ });
882
+ const append = (buf, sink) => {
883
+ if (truncated)
884
+ return;
885
+ const remaining = CLI_EXEC_MAX_OUTPUT - total;
886
+ if (remaining <= 0) {
887
+ truncated = true;
888
+ return;
889
+ }
890
+ const chunk = buf.length > remaining ? buf.subarray(0, remaining) : buf;
891
+ total += chunk.length;
892
+ if (sink === 'out') {
893
+ stdout += chunk.toString('utf-8');
894
+ stdoutChunks++;
895
+ }
896
+ else {
897
+ stderr += chunk.toString('utf-8');
898
+ stderrChunks++;
899
+ }
900
+ if (buf.length > remaining)
901
+ truncated = true;
902
+ };
903
+ child.stdout?.on('data', (b) => {
904
+ append(b, 'out');
905
+ });
906
+ child.stderr?.on('data', (b) => {
907
+ append(b, 'err');
908
+ });
909
+ const timer = setTimeout(() => {
910
+ if (settled)
911
+ return;
912
+ settled = true;
913
+ try {
914
+ child.kill('SIGKILL');
915
+ }
916
+ catch { }
917
+ logger.warn(`[CommandHandler] cli exec timeout: command=${argv[0] || '<none>'} durationMs=${CLI_EXEC_TIMEOUT_MS}`);
918
+ resolve({ error: `CLI execution exceeded ${CLI_EXEC_TIMEOUT_MS / 1000}s`, code: 'EXECUTION_TIMEOUT' });
919
+ }, CLI_EXEC_TIMEOUT_MS);
920
+ child.on('error', (e) => {
921
+ if (settled)
922
+ return;
923
+ settled = true;
924
+ clearTimeout(timer);
925
+ logger.warn(`[CommandHandler] cli exec unavailable: command=${argv[0] || '<none>'} code=${e?.code || 'unknown'}`);
926
+ resolve({ error: 'CLI execution infrastructure is unavailable', code: 'TEMPORARILY_UNAVAILABLE' });
927
+ });
928
+ child.on('close', (exitCode) => {
929
+ if (settled)
930
+ return;
931
+ settled = true;
932
+ clearTimeout(timer);
933
+ const elapsed = Date.now() - startedAt;
934
+ resolve({ data: {
935
+ exitCode: exitCode ?? -1,
936
+ stdout, stderr, truncated,
937
+ durationMs: elapsed,
938
+ } });
939
+ });
940
+ });
941
+ }
942
+ /** 把 menu.action 委派给已有 slash 命令处理逻辑,把 OutboundPayload 包成结构化结果。 */
943
+ async delegateAsAction(action, slashCmd, channel, channelId, userId, opts = {}) {
944
+ try {
945
+ const result = await this._handleInternal(slashCmd, channel, channelId, undefined, userId, undefined, undefined, undefined, undefined, undefined, opts.overrideIdentity, opts.authSubject);
946
+ if (result == null) {
947
+ // null / undefined: 命令未识别或前置守卫拦截(如 idle 检查),视为失败
948
+ return { error: '命令未执行(可能被前置守卫拦截)', code: 'EXEC_FAILED' };
949
+ }
950
+ if (typeof result !== 'object' || !('kind' in result)) {
951
+ return { data: { action, success: true } };
952
+ }
953
+ const payload = result;
954
+ if (payload.kind === 'command.error') {
955
+ return { error: payload.text || '执行失败', code: 'EXEC_FAILED' };
956
+ }
957
+ const data = payload.structured && typeof payload.structured === 'object'
958
+ ? { ...payload.structured }
959
+ : {};
960
+ data.action = action;
961
+ data.success = true;
962
+ if (payload.text)
963
+ data.message = payload.text;
964
+ if (payload.structured && typeof payload.structured === 'object') {
965
+ data.structured = payload.structured;
966
+ }
967
+ // 对于切换/创建类动作,附加切换后的活跃 session 信息便于客户端继续操作
968
+ if (opts.enrichSession) {
969
+ const newSession = await this.sessionManager.getActiveSession(channel, channelId);
970
+ if (newSession) {
971
+ data.session = { id: newSession.id, name: newSession.name || null };
972
+ if (newSession.agentSessionId)
973
+ data.session.agentSessionId = newSession.agentSessionId;
974
+ }
975
+ }
976
+ return { data };
977
+ }
978
+ catch (e) {
979
+ return { error: e?.message || String(e), code: 'INTERNAL' };
980
+ }
981
+ }
982
+ isCommand(content) {
983
+ return isQuickCommand(content);
984
+ }
985
+ /**
986
+ * 主命令处理入口
987
+ */
988
+ async handle(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
989
+ try {
990
+ const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
991
+ return result;
992
+ }
993
+ catch (error) {
994
+ if (error instanceof BaseagentRunnerUnavailableError) {
995
+ logger.error(`[CommandHandler] baseagent mismatch blocked: channel=${channel} requested=${error.baseagent} owner=${error.evolagentName} available=${error.availableBaseagents.join(',') || '<none>'}`);
996
+ return { kind: 'command.error', text: this.formatBaseagentUnavailable(error), reason: error.code };
997
+ }
998
+ throw error;
999
+ }
1000
+ }
1001
+ async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext) {
1002
+ return await handleSlashCommand.call(this, content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, authSubject, replyContext);
1003
+ }
1004
+ async handleTriggerCommand(scheduler, content, channel, channelId, peerId, isAdmin, messageId, chatType, threadId) {
1005
+ if (content === '/trigger') {
1006
+ const visible = scheduler
1007
+ .list()
1008
+ .filter(definition => this.canAccessTriggerDefinition(definition, peerId, channel, isAdmin));
1009
+ if (visible.length === 0)
1010
+ return '📭 当前没有活跃的触发器';
1011
+ const lines = visible.map(definition => {
1012
+ const view = this.definitionToTriggerView(definition, scheduler);
1013
+ const next = view.nextFireAt ? new Date(view.nextFireAt).toLocaleString() : '未计算';
1014
+ return `• **${view.name}** [${view.scheduleType}] 下次: ${next}`;
1015
+ });
1016
+ return `📋 活跃触发器(${visible.length} 个):\n\n${lines.join('\n')}`;
1017
+ }
1018
+ const sub = content.slice('/trigger '.length).trim();
1019
+ if (sub === 'list' || sub.startsWith('list ')) {
1020
+ const includeDisabled = sub.includes('--all') || sub.includes('all');
1021
+ const visible = scheduler
1022
+ .list({ all: includeDisabled })
1023
+ .filter(definition => this.canAccessTriggerDefinition(definition, peerId, channel, isAdmin));
1024
+ if (visible.length === 0)
1025
+ return '📭 没有触发器记录';
1026
+ const lines = visible.map(definition => {
1027
+ const view = this.definitionToTriggerView(definition, scheduler);
1028
+ const next = view.nextFireAt ? new Date(view.nextFireAt).toLocaleString() : '未计算';
1029
+ const status = definition.enabled ? 'active' : 'disabled';
1030
+ return `• ${view.name} [${view.scheduleType}] ${status} | 下次: ${next}`;
1031
+ });
1032
+ return `📋 触发器(${visible.length} 个):\n\n${lines.join('\n')}`;
1033
+ }
1034
+ // Helper: extract nameOrId and find trigger with permission check
1035
+ const extractAndFindTrigger = (sub, prefix, action) => {
1036
+ const nameOrId = sub.slice(prefix.length).trim();
1037
+ if (!nameOrId)
1038
+ throw new Error(`❌ 用法:/trigger ${action} <名称>`);
1039
+ const definition = this.findTriggerDefinition(scheduler, nameOrId, peerId, channel, isAdmin);
1040
+ if (!definition) {
1041
+ throw new Error(isAdmin
1042
+ ? `❌ 未找到触发器:${nameOrId}`
1043
+ : `❌ 未找到触发器 "${nameOrId}",或无权限${action === 'show' ? '查看' : '修改'}`);
1044
+ }
1045
+ return { definition, nameOrId };
1046
+ };
1047
+ if (sub.startsWith('cancel ')) {
1048
+ const { definition } = extractAndFindTrigger(sub, 'cancel ', 'cancel');
1049
+ const cancelled = scheduler.cancel(definition.id);
1050
+ this.eventBus.publish({ type: 'trigger:cancelled', triggerId: cancelled.id, name: cancelled.name, by: peerId });
1051
+ return `✅ 触发器已禁用:**${cancelled.name}**`;
1052
+ }
1053
+ if (sub.startsWith('delete ') || sub.startsWith('remove ') || sub.startsWith('rm ')) {
1054
+ const prefix = sub.startsWith('delete ') ? 'delete ' : sub.startsWith('remove ') ? 'remove ' : 'rm ';
1055
+ const { definition } = extractAndFindTrigger(sub, prefix, 'delete');
1056
+ const deleted = scheduler.delete(definition.id);
1057
+ return `✅ 触发器已删除:**${deleted.name}**`;
1058
+ }
1059
+ if (sub.startsWith('enable ')) {
1060
+ const { definition } = extractAndFindTrigger(sub, 'enable ', 'enable');
1061
+ scheduler.setEnabled(definition.id, true);
1062
+ return `✅ 触发器已启用:**${definition.name}**`;
1063
+ }
1064
+ if (sub.startsWith('disable ')) {
1065
+ const { definition } = extractAndFindTrigger(sub, 'disable ', 'disable');
1066
+ scheduler.setEnabled(definition.id, false);
1067
+ return `✅ 触发器已暂停:**${definition.name}**`;
1068
+ }
1069
+ if (sub.startsWith('show ')) {
1070
+ const { definition } = extractAndFindTrigger(sub, 'show ', 'show');
1071
+ const details = scheduler.show(definition.id);
1072
+ const view = this.definitionToTriggerView(definition, scheduler);
1073
+ const nextStr = view.nextFireAt ? new Date(view.nextFireAt).toLocaleString() : '未计算';
1074
+ const activeRuns = details.active?.length ?? 0;
1075
+ const recentRuns = details.recentRuns?.slice(0, 5) ?? [];
1076
+ let output = `📋 **${definition.name}** (${definition.id})\n`;
1077
+ output += `状态: ${definition.enabled ? 'active' : 'disabled'}\n`;
1078
+ output += `调度: ${view.scheduleType} | 下次: ${nextStr}\n`;
1079
+ output += `处理: ${definition.execution.type}\n`;
1080
+ output += `模型: ${definition.execution.model ?? 'inherit'}\n`;
1081
+ output += `推理强度: ${definition.execution.effort ?? 'inherit'}\n`;
1082
+ output += `权限: ${definition.execution.permissionMode ?? 'inherit'}\n`;
1083
+ output += `反馈: ${definition.feedback.strategy}\n`;
1084
+ if (definition.execution.type === 'script')
1085
+ output += `脚本: ${this.scriptCommandLabel(definition.execution.script)}\n`;
1086
+ if (definition.limits?.maxRuns !== undefined || definition.limits?.maxDuration !== undefined) {
1087
+ const limitParts = [
1088
+ definition.limits.maxRuns !== undefined ? `maxRuns=${definition.limits.maxRuns}` : undefined,
1089
+ definition.limits.maxDuration !== undefined ? `maxDuration=${definition.limits.maxDuration}` : undefined,
1090
+ ].filter(Boolean);
1091
+ output += `限制: ${limitParts.join(', ')}\n`;
1092
+ }
1093
+ if (details.limitState) {
1094
+ const stateParts = [
1095
+ `runCount=${details.limitState.runCount}`,
1096
+ `startedAt=${new Date(details.limitState.startedAt).toLocaleString()}`,
1097
+ details.limitState.disabledReason ? `disabledReason=${details.limitState.disabledReason}` : undefined,
1098
+ ].filter(Boolean);
1099
+ if (definition.limits?.maxDuration) {
1100
+ stateParts.push(`expiresAt=${new Date(details.limitState.startedAt + this.limitDurationMs(definition.limits.maxDuration)).toLocaleString()}`);
1101
+ }
1102
+ output += `限制状态: ${stateParts.join(', ')}\n`;
1103
+ }
1104
+ output += `活跃运行: ${activeRuns}\n`;
1105
+ if (recentRuns.length > 0) {
1106
+ output += `\n最近运行:\n`;
1107
+ for (const r of recentRuns) {
1108
+ const ts = new Date(r.finishedAt).toLocaleString();
1109
+ output += ` • ${r.status} ${r.reason ?? ''} (${ts})\n`;
1110
+ }
1111
+ }
1112
+ return output;
1113
+ }
1114
+ if (sub.startsWith('run ')) {
1115
+ const args = sub.slice('run '.length).trim();
1116
+ const parts = args.split(/\s+/);
1117
+ const dryRunIdx = parts.indexOf('--dry-run');
1118
+ const dryRun = dryRunIdx >= 0;
1119
+ const nameOrId = dryRunIdx >= 0
1120
+ ? parts.filter((_, i) => i !== dryRunIdx).join(' ').trim()
1121
+ : args.trim();
1122
+ if (!nameOrId)
1123
+ return '❌ 用法:/trigger run <名称> [--dry-run]';
1124
+ const definition = this.findTriggerDefinition(scheduler, nameOrId, peerId, channel, isAdmin);
1125
+ if (!definition) {
1126
+ return isAdmin
1127
+ ? `❌ 未找到触发器:${nameOrId}`
1128
+ : `❌ 未找到触发器 "${nameOrId}",或无权限执行`;
1129
+ }
1130
+ try {
1131
+ const result = await scheduler.run(definition.id, { dryRun });
1132
+ const prefix = dryRun ? '🔍 试运行' : '▶️ 手动触发';
1133
+ return `${prefix}:**${definition.name}**\n状态: ${result.status}${result.reason ? ` (${result.reason})` : ''}`;
1134
+ }
1135
+ catch (err) {
1136
+ return `❌ 执行失败:${err?.message || err}`;
1137
+ }
1138
+ }
1139
+ if (sub.startsWith('update ')) {
1140
+ const args = sub.slice('update '.length);
1141
+ const result = parseTriggerUpdate(args);
1142
+ if (!result.ok)
1143
+ return `❌ ${result.error}`;
1144
+ const updated = await this.updateTriggerFromPatch(scheduler, result.nameOrId, result.value, channel, channelId, peerId, isAdmin, messageId, threadId);
1145
+ if (!updated.ok)
1146
+ return `❌ ${updated.error}`;
1147
+ const nextStr = updated.trigger.nextFireAt ? new Date(updated.trigger.nextFireAt).toLocaleString() : '未计算';
1148
+ return `✅ 触发器已更新:**${updated.trigger.name}**\n下次触发:${nextStr}`;
1149
+ }
1150
+ const createSubcommand = sub.startsWith('create ')
1151
+ ? 'create'
1152
+ : sub.startsWith('set ')
1153
+ ? 'set'
1154
+ : undefined;
1155
+ if (createSubcommand) {
1156
+ const args = sub.slice(`${createSubcommand} `.length);
1157
+ const result = parseTriggerSet(args);
1158
+ if (!result.ok)
1159
+ return `❌ ${result.error}`;
1160
+ const reg = await this.registerTriggerFromParsed(result.value, channel, channelId, peerId, messageId, chatType, threadId, isAdmin);
1161
+ if (!reg.ok)
1162
+ return `❌ ${reg.error}`;
1163
+ const nextStr = reg.trigger.nextFireAt ? new Date(reg.trigger.nextFireAt).toLocaleString() : '未计算';
1164
+ return `✅ 触发器已注册:**${reg.trigger.name}**\n下次触发:${nextStr}`;
1165
+ }
1166
+ return `❌ 未知子命令。用法:
1167
+ /trigger — 查看活跃触发器
1168
+ /trigger list [--all] — 查看所有触发器
1169
+ /trigger create <参数> — 创建触发器(set 为兼容别名)
1170
+ /trigger update <名称|ID> <参数> — 修改触发器
1171
+ 常用参数:--delay/--at/--cron/--every、--prompt、--model、--effort、--permission(省略则继承;update 可用 inherit 清除覆盖)
1172
+ /trigger enable <名称|ID> — 启用触发器
1173
+ /trigger disable <名称|ID> — 暂停触发器
1174
+ /trigger show <名称|ID> — 查看触发器详情
1175
+ /trigger run <名称|ID> [--dry-run] — 手动触发
1176
+ /trigger cancel <名称|ID> — 禁用触发器(disable 的兼容别名)
1177
+ /trigger delete <名称|ID> — 删除触发器`;
1178
+ }
1179
+ async handleTrigger(content, channel, channelId, peerId, isAdmin, messageId, chatType, threadId) {
1180
+ const scheduler = this.getTriggerSchedulerForChannel(channel);
1181
+ if (!scheduler)
1182
+ return '⚠️ 触发器功能未启用';
1183
+ return await this.handleTriggerCommand(scheduler, content, channel, channelId, peerId, isAdmin, messageId, chatType, threadId);
1184
+ }
1185
+ async updateTriggerFromPatch(scheduler, nameOrId, patch, channel, channelId, peerId, isAdmin, messageId, threadId) {
1186
+ const definition = this.findTriggerDefinition(scheduler, nameOrId, peerId, channel, isAdmin);
1187
+ if (!definition) {
1188
+ return { ok: false, error: isAdmin ? `未找到触发器:${nameOrId}` : `未找到触发器 "${nameOrId}",或无权限修改` };
1189
+ }
1190
+ const currentRevision = definitionRevision(definition);
1191
+ const hasFullDefinition = patch?.definition !== undefined;
1192
+ const expectedRevision = patch?.expectedRevision;
1193
+ if (expectedRevision !== undefined && (typeof expectedRevision !== 'string' || !expectedRevision.trim())) {
1194
+ return { ok: false, code: 'INVALID_REVISION', error: 'expectedRevision 必须是非空字符串', currentRevision };
1195
+ }
1196
+ if (hasFullDefinition && expectedRevision === undefined) {
1197
+ return {
1198
+ ok: false,
1199
+ code: 'REVISION_REQUIRED',
1200
+ error: '完整 Trigger 定义替换必须提供 expectedRevision,请刷新后重试',
1201
+ currentRevision,
1202
+ };
1203
+ }
1204
+ if (expectedRevision !== undefined && expectedRevision !== currentRevision) {
1205
+ return {
1206
+ ok: false,
1207
+ code: 'REVISION_CONFLICT',
1208
+ error: `Trigger 已被其他修改,请刷新后重试(expected ${expectedRevision}, current ${currentRevision})`,
1209
+ currentRevision,
1210
+ };
1211
+ }
1212
+ let updated;
1213
+ try {
1214
+ const { nameOrId: _nameOrId, expectedRevision: _expectedRevision, ...definitionPatch } = patch;
1215
+ updated = definitionPatch.definition !== undefined
1216
+ ? replaceEditableTriggerDefinition(definition, definitionPatch.definition)
1217
+ : applyTriggerPatch(definition, definitionPatch);
1218
+ }
1219
+ catch (err) {
1220
+ return { ok: false, error: err?.message || String(err) };
1221
+ }
1222
+ const target = this.primaryFeedbackTarget(updated);
1223
+ if (!isAdmin && target && !sameFeedbackTarget(target, definition.origin)) {
1224
+ return { ok: false, error: '无权限:非 admin 只能把 trigger 投递到来源会话' };
1225
+ }
1226
+ if (!isAdmin && updated.execution.type === 'script') {
1227
+ return { ok: false, error: '无权限:只有 owner/admin 可以修改 script trigger' };
1228
+ }
1229
+ const triggerRole = this.triggerRoleContext(definition.agentAid, definition.origin?.channelKey || channel, definition.origin?.peerId || peerId);
1230
+ const permissionError = this.validateTriggerPermissionModeForRole(updated.execution.permissionMode, isAdmin, triggerRole.role, definition.agentAid);
1231
+ if (permissionError)
1232
+ return { ok: false, error: permissionError };
1233
+ const runtimeFieldError = this.validateTriggerRuntimeFields(updated, triggerRole.role, triggerRole.baseagent);
1234
+ if (runtimeFieldError)
1235
+ return { ok: false, error: runtimeFieldError };
1236
+ try {
1237
+ const saved = scheduler.update(definition.id, updated);
1238
+ return {
1239
+ ok: true,
1240
+ trigger: this.definitionToTriggerView(saved, scheduler),
1241
+ revision: definitionRevision(saved),
1242
+ };
1243
+ }
1244
+ catch (err) {
1245
+ return { ok: false, error: `更新失败:${err?.message || err}` };
1246
+ }
1247
+ }
1248
+ /** 从已解析的 trigger 参数组装 definition 并注册。文本路径(handleTrigger)与 menu 路径共用。
1249
+ * parsed 形状 = parseTriggerSet 的 result.value(ParsedTriggerSet)。
1250
+ * 失败 return { ok:false, error };成功 return { ok:true, trigger }。 */
1251
+ async registerTriggerFromParsed(parsed, channel, channelId, peerId, messageId, chatType, threadId, isAdmin = false) {
1252
+ const built = await this.buildTriggerDefinitionFromParsed(parsed, channel, channelId, peerId, isAdmin, messageId, chatType, threadId);
1253
+ if ('error' in built)
1254
+ return { ok: false, error: built.error };
1255
+ try {
1256
+ const created = built.scheduler.create(built.definition, [], { enable: true });
1257
+ return { ok: true, trigger: this.definitionToTriggerView(created, built.scheduler) };
1258
+ }
1259
+ catch (err) {
1260
+ return { ok: false, error: `注册失败:${err?.message || err}` };
1261
+ }
1262
+ }
1263
+ limitsFromParsed(parsed) {
1264
+ const limits = {};
1265
+ if (parsed.maxRuns !== undefined)
1266
+ limits.maxRuns = parsed.maxRuns;
1267
+ if (parsed.maxDuration !== undefined)
1268
+ limits.maxDuration = parsed.maxDuration;
1269
+ return limits.maxRuns === undefined && limits.maxDuration === undefined ? undefined : limits;
1270
+ }
1271
+ scriptCommandLabel(script) {
1272
+ const args = Array.isArray(script.args) ? script.args.map(arg => this.shellQuoteArg(String(arg))).join(' ') : '';
1273
+ const command = this.shellQuoteArg(script.path);
1274
+ return `${script.runtime} ${command}${args ? ` ${args}` : ''}`;
1275
+ }
1276
+ shellQuoteArg(value) {
1277
+ if (!value)
1278
+ return "''";
1279
+ return /[\s"'\\]/.test(value) ? `"${value.replace(/(["\\$`])/g, '\\$1')}"` : value;
1280
+ }
1281
+ limitDurationMs(value) {
1282
+ const amount = Number(value.slice(0, -1));
1283
+ const unit = value.slice(-1);
1284
+ if (unit === 'd')
1285
+ return amount * 86_400_000;
1286
+ if (unit === 'h')
1287
+ return amount * 3_600_000;
1288
+ if (unit === 'm')
1289
+ return amount * 60_000;
1290
+ return amount * 1_000;
1291
+ }
1292
+ // ── /rewind helpers ──
1293
+ async handleRewindList(session, agent) {
1294
+ try {
1295
+ const messages = await agent.getSessionMessages(session.agentSessionId, session.projectPath);
1296
+ const turns = buildSessionTurnList(messages);
1297
+ if (turns.length === 0) {
1298
+ return '📋 当前会话暂无对话记录';
1299
+ }
1300
+ const lines = turns.map(t => `#${t.index} ${t.userContent}`);
1301
+ return [
1302
+ `📋 会话历史 (共 ${turns.length} 轮)`,
1303
+ '',
1304
+ ...lines,
1305
+ '',
1306
+ '💡 /rewind <N> chat|file|all — 撤销第N轮',
1307
+ ].join('\n');
1308
+ }
1309
+ catch (error) {
1310
+ logger.error('[CommandHandler] Failed to read session messages:', error);
1311
+ return `❌ 读取会话历史失败: ${error instanceof Error ? error.message : '未知错误'}`;
1312
+ }
1313
+ }
1314
+ async handleRewind(session, agent, turnNum, mode) {
1315
+ try {
1316
+ const messages = await agent.getSessionMessages(session.agentSessionId, session.projectPath);
1317
+ const turns = buildSessionTurnList(messages);
1318
+ if (turnNum < 1 || turnNum > turns.length) {
1319
+ return `❌ 轮次超出范围,当前共 ${turns.length} 轮`;
1320
+ }
1321
+ // /rewind N = 撤销第N轮(及之后),保留 1..N-1
1322
+ const rewindTarget = turns[turnNum - 1]; // 被撤销的轮次(用于文件回退)
1323
+ const keepTarget = turnNum >= 2 ? turns[turnNum - 2] : null; // 保留到的轮次(用于对话回退)
1324
+ const results = [];
1325
+ // 文件回退(立即执行)
1326
+ if (mode === 'file' || mode === 'all') {
1327
+ if (!agent.rewindFiles) {
1328
+ return '❌ 当前 Agent 不支持文件回退';
1329
+ }
1330
+ const fileResult = await agent.rewindFiles(session.agentSessionId, session.projectPath, rewindTarget.userUuid);
1331
+ if (!fileResult.canRewind) {
1332
+ if (mode === 'file') {
1333
+ return `❌ 当前会话无文件快照,无法回退文件${fileResult.error ? `\n原因: ${fileResult.error}` : ''}`;
1334
+ }
1335
+ results.push(`⚠️ 文件回退失败${fileResult.error ? `: ${fileResult.error}` : '(无文件快照)'}`);
1336
+ }
1337
+ else {
1338
+ const detail = fileResult.filesChanged
1339
+ ? `(恢复了 ${fileResult.filesChanged.length} 个文件)`
1340
+ : '';
1341
+ if (agent.capabilities?.fileRewind === 'git-head') {
1342
+ results.push(`✅ 已按 Git HEAD 恢复文件${detail}(Codex 当前不提供逐轮文件快照)`);
1343
+ }
1344
+ else {
1345
+ results.push(`✅ 已恢复文件到第 ${turnNum} 轮之前的状态${detail}`);
1346
+ }
1347
+ }
1348
+ }
1349
+ // 对话回退:Codex app-server 可直接 rollback;Claude 走 resumeAt 延迟到下次消息生效。
1350
+ if (mode === 'chat' || mode === 'all') {
1351
+ const discarded = turns.length - turnNum + 1;
1352
+ if (agent.rollbackSessionTurns) {
1353
+ const ok = await agent.rollbackSessionTurns(session.agentSessionId, session.projectPath, discarded);
1354
+ if (!ok)
1355
+ return '❌ 对话回退失败';
1356
+ const meta = { ...(session.metadata || {}) };
1357
+ delete meta.resumeAt;
1358
+ await this.sessionManager.updateSession(session.id, { metadata: meta });
1359
+ }
1360
+ else if (keepTarget) {
1361
+ const meta = { ...(session.metadata || {}), resumeAt: keepTarget.assistantUuid };
1362
+ await this.sessionManager.updateSession(session.id, { metadata: meta });
1363
+ }
1364
+ else {
1365
+ // N=1:撤销全部对话,清空 session 从头开始
1366
+ const meta = { ...(session.metadata || {}) };
1367
+ delete meta.resumeAt;
1368
+ await this.sessionManager.updateSession(session.id, {
1369
+ metadata: meta,
1370
+ agentSessionId: null,
1371
+ });
1372
+ }
1373
+ results.push(`✅ 已撤销第 ${turnNum} 轮${discarded > 1 ? `及后续共 ${discarded} 轮` : ''}`, keepTarget ? `下次发言将从第 ${turnNum - 1} 轮继续` : '下次发言将开始全新对话');
1374
+ }
1375
+ this.eventBus.publish({
1376
+ type: 'session:rewind',
1377
+ sessionId: session.id,
1378
+ turnNum,
1379
+ mode,
1380
+ });
1381
+ return results.join('\n');
1382
+ }
1383
+ catch (error) {
1384
+ logger.error('[CommandHandler] Rewind failed:', error);
1385
+ return `❌ 回退失败: ${error instanceof Error ? error.message : '未知错误'}`;
1386
+ }
1387
+ }
1388
+ // ── Agent Ctl ──
1389
+ static CTL_COMMANDS = [
1390
+ '/status', '/pwd', '/file', '/send', '/rename', '/name', '/queue', '/restart', '/model', '/effort',
1391
+ ];
1392
+ /**
1393
+ * 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
1394
+ * - 群聊话题:metadata.replyContext.{threadId,peerId}
1395
+ * - 私聊:metadata.peerId
1396
+ * - taskId/chatmode:从 processing_state 和 effective chatmode 注入
1397
+ */
1398
+ buildCtlReplyContext(session) {
1399
+ const ctx = {};
1400
+ const meta = session.metadata;
1401
+ if (meta?.replyContext?.threadId)
1402
+ ctx.threadId = meta.replyContext.threadId;
1403
+ if (meta?.replyContext?.peerId)
1404
+ ctx.peerId = meta.replyContext.peerId;
1405
+ if (!ctx.peerId && meta?.peerId)
1406
+ ctx.peerId = meta.peerId;
1407
+ // 话题(Feishu thread)路由:透传 replyToMessageId + replyInThread,
1408
+ // 否则 ctl send/file 会丢失话题归属、落到主会话气泡。
1409
+ if (meta?.replyContext?.replyToMessageId)
1410
+ ctx.replyToMessageId = meta.replyContext.replyToMessageId;
1411
+ if (meta?.replyContext?.replyInThread)
1412
+ ctx.replyInThread = meta.replyContext.replyInThread;
1413
+ const taskId = this.sessionManager.getActiveTaskId(session.id);
1414
+ const chatmode = this.resolveEffectiveChatmodeForSession(session);
1415
+ const encrypted = this.sessionManager.getSessionEncrypt(session.id);
1416
+ // 诊断日志:记录 task_id 解析结果
1417
+ logger.info(`[CommandHandler] buildCtlReplyContext: sessionId=${session.id} taskId=${taskId ?? 'none'} chatmode=${chatmode} threadId=${ctx.threadId ?? 'none'} replyTo=${ctx.replyToMessageId ?? 'none'} inThread=${ctx.replyInThread ?? false}`);
1418
+ if (taskId || chatmode !== 'interactive' || encrypted != null) {
1419
+ ctx.metadata = {};
1420
+ if (taskId)
1421
+ ctx.metadata.taskId = taskId;
1422
+ if (chatmode !== 'interactive')
1423
+ ctx.metadata.chatmode = chatmode;
1424
+ if (encrypted != null)
1425
+ ctx.metadata.encrypted = encrypted;
1426
+ }
1427
+ return Object.keys(ctx).length > 0 ? ctx : undefined;
1428
+ }
1429
+ resolveEffectiveChatmodeForSession(session) {
1430
+ const peerType = session.metadata?.peerType;
1431
+ try {
1432
+ const self = session.selfAID || this.getOwningAgent(session.channel)?.aid;
1433
+ const channelType = session.channelType || this.resolveChannelType(session.channel);
1434
+ const peerKeyId = session.chatType === 'group'
1435
+ ? (session.metadata?.groupId || session.channelId)
1436
+ : (session.metadata?.peerId || session.channelId);
1437
+ const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
1438
+ return resolveChatMode({
1439
+ self: self || undefined,
1440
+ peerKey,
1441
+ role: session.identity?.role,
1442
+ chatType: session.chatType,
1443
+ peerType,
1444
+ });
1445
+ }
1446
+ catch {
1447
+ return 'interactive';
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Agent managed CLI config entrypoint. Identity and relation are resolved
1452
+ * from the daemon-owned session rather than caller-provided flags.
1453
+ */
1454
+ async handleConfigOperation(argv, sessionId, delegationToken) {
1455
+ if (!this.agentDelegationRegistry) {
1456
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1457
+ }
1458
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId);
1459
+ if (!delegation.ok)
1460
+ return { ok: false, code: delegation.code, error: delegation.reason };
1461
+ const session = await this.sessionManager.getSessionById(sessionId);
1462
+ if (!session)
1463
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1464
+ const grant = delegation.grant;
1465
+ let conversationId;
1466
+ try {
1467
+ conversationId = parsePeerKey(grant.peerKey).channelId;
1468
+ }
1469
+ catch {
1470
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1471
+ }
1472
+ const subject = buildAuthSubject({
1473
+ selfAid: grant.selfAid,
1474
+ actorId: grant.actorId,
1475
+ channel: grant.channel,
1476
+ channelType: grant.channelType,
1477
+ channelId: conversationId,
1478
+ chatType: grant.chatType,
1479
+ conversationId,
1480
+ processOwners: loadDaemonConfig().owners ?? [],
1481
+ });
1482
+ const resolved = resolveConfigCommand(argv, {
1483
+ defaultRelation: { self: grant.selfAid, peerKey: grant.peerKey },
1484
+ });
1485
+ if (!resolved.ok)
1486
+ return { ok: false, code: resolved.code, error: resolved.reason };
1487
+ const decision = authorizeOperation({
1488
+ source: 'agent-tool',
1489
+ subject,
1490
+ intent: {
1491
+ operation: resolved.command.operationId,
1492
+ scope: resolved.command.commandScope,
1493
+ source: 'agent-tool',
1494
+ args: {
1495
+ ...(resolved.command.self ? { self: resolved.command.self } : {}),
1496
+ ...(resolved.command.peerKey ? { peer: resolved.command.peerKey, peerKey: resolved.command.peerKey } : {}),
1497
+ ...('field' in resolved.command && resolved.command.field ? { field: resolved.command.field } : {}),
1498
+ },
1499
+ dangerous: resolved.command.dangerous,
1500
+ },
1501
+ resolvedConfigCommand: resolved.command,
1502
+ auditMetadata: {
1503
+ argvHash: hashArgv(resolved.command.canonicalArgv),
1504
+ taskId: grant.taskId,
1505
+ messageId: grant.messageId,
1506
+ },
1507
+ });
1508
+ if (!decision.allow)
1509
+ return { ok: false, code: decision.code, error: decision.reason };
1510
+ const result = executeResolvedConfigCommand(resolved.command, { role: subject.role });
1511
+ return result.ok
1512
+ ? { ok: true, result }
1513
+ : { ok: false, code: result.code, error: result.error, ...(result.data !== undefined ? { data: result.data } : {}) };
1514
+ }
1515
+ /** Agent-managed contact CLI entrypoint. The daemon owns actor resolution and authorization. */
1516
+ async handleContactOperation(argv, sessionId, delegationToken) {
1517
+ if (!this.agentDelegationRegistry) {
1518
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: 'Task delegation is not configured' };
1519
+ }
1520
+ const delegation = this.agentDelegationRegistry.validate(delegationToken, sessionId);
1521
+ if (!delegation.ok)
1522
+ return { ok: false, code: delegation.code, error: delegation.reason };
1523
+ const session = await this.sessionManager.getSessionById(sessionId);
1524
+ if (!session)
1525
+ return { ok: false, code: 'INVALID_SESSION', error: 'Invalid session' };
1526
+ const grant = delegation.grant;
1527
+ let conversationId;
1528
+ try {
1529
+ conversationId = parsePeerKey(grant.peerKey).channelId;
1530
+ }
1531
+ catch {
1532
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'Delegation contains an invalid relation key' };
1533
+ }
1534
+ const resolved = resolveContactCommand(argv);
1535
+ if (!resolved.ok)
1536
+ return { ok: false, code: resolved.code, error: resolved.reason };
1537
+ if (resolved.command.selfAid !== grant.selfAid) {
1538
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'Only the current Agent contact book can be targeted' };
1539
+ }
1540
+ const subject = buildAuthSubject({
1541
+ selfAid: grant.selfAid,
1542
+ actorId: grant.actorId,
1543
+ channel: grant.channel,
1544
+ channelType: grant.channelType,
1545
+ channelId: conversationId,
1546
+ chatType: grant.chatType,
1547
+ conversationId,
1548
+ processOwners: loadDaemonConfig().owners ?? [],
1549
+ });
1550
+ const decision = authorizeOperation({
1551
+ source: 'agent-tool',
1552
+ subject,
1553
+ intent: {
1554
+ operation: resolved.command.operationId,
1555
+ scope: 'agent',
1556
+ source: 'agent-tool',
1557
+ args: {
1558
+ self: resolved.command.selfAid,
1559
+ ...('primaryId' in resolved.command ? { primaryId: resolved.command.primaryId } : {}),
1560
+ },
1561
+ },
1562
+ auditMetadata: {
1563
+ argvHash: hashArgv(resolved.command.canonicalArgv),
1564
+ taskId: grant.taskId,
1565
+ messageId: grant.messageId,
1566
+ },
1567
+ });
1568
+ if (!decision.allow)
1569
+ return { ok: false, code: decision.code, error: decision.reason };
1570
+ const result = await executeResolvedContactCommand(resolved.command, grant.actorId);
1571
+ return result.ok
1572
+ ? { ok: true, result }
1573
+ : { ok: false, code: result.code, error: result.error, ...(result.data ? { data: result.data } : {}) };
1574
+ }
1575
+ setAgentDelegationRegistry(registry) {
1576
+ this.agentDelegationRegistry = registry;
1577
+ }
1578
+ /**
1579
+ * Agent ctl 入口:通过 IPC 接收 Agent 自主管理指令
1580
+ * 复用现有 slash cmd 逻辑,权限继承 session 用户角色
1581
+ */
1582
+ async handleCtl(cmd, sessionId) {
1583
+ logger.info(`[ctl] cmd="${cmd}" sessionId=${sessionId}`);
1584
+ // 1. 白名单检查
1585
+ const inputCmd = cmd.split(' ')[0];
1586
+ if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
1587
+ return { ok: false, error: `不允许的指令: ${inputCmd}` };
1588
+ }
1589
+ // 2. 通过 sessionId 查 session
1590
+ const session = await this.sessionManager.getSessionById(sessionId);
1591
+ if (!session) {
1592
+ return { ok: false, error: '无效的 session' };
1593
+ }
1594
+ // 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
1595
+ const userId = session.metadata?.peerId;
1596
+ const ctlIdentity = this.resolveCtlIdentity(session, userId);
1597
+ // 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
1598
+ if (cmd.startsWith('/send ') || cmd === '/send') {
1599
+ // 解析 --encrypt 标志和消息文本
1600
+ const raw = cmd.startsWith('/send ') ? cmd.slice(6).trim() : '';
1601
+ const forceEncrypt = raw.startsWith('--encrypt ');
1602
+ const text = forceEncrypt ? raw.slice(10).trim() : raw;
1603
+ if (!text)
1604
+ return { ok: false, error: '消息内容不能为空' };
1605
+ const adapter = this.adapters.get(session.channel);
1606
+ if (!adapter)
1607
+ return { ok: false, error: `adapter 未找到: ${session.channel}` };
1608
+ try {
1609
+ const replyContext = this.buildCtlReplyContext(session);
1610
+ const taskId = replyContext?.metadata?.taskId;
1611
+ const chatmode = replyContext?.metadata?.chatmode ?? 'interactive';
1612
+ // --encrypt 覆盖 session 加密状态
1613
+ // 添加 source: 'ctl' 标记(用于区分 ec ctl send)
1614
+ const enrichedReplyContext = forceEncrypt
1615
+ ? { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), encrypted: true, source: 'ctl' } }
1616
+ : { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), source: 'ctl' } };
1617
+ await adapter.send(buildEnvelope({ taskId, channel: adapter.channelName, channelId: session.channelId, chatmode, replyContext: enrichedReplyContext }), { kind: 'result.text', text, isFinal: true });
1618
+ // 出方向 jsonl 写入已下沉到 aun.ts:deliverTextEntry,message.send 成功后统一写入。
1619
+ return { ok: true, result: 'ok' };
1620
+ }
1621
+ catch (err) {
1622
+ return { ok: false, error: err.message || String(err) };
1623
+ }
1624
+ }
1625
+ // 5. file 路径限制:只允许 projectPath 下的文件
1626
+ if (cmd.startsWith('/file')) {
1627
+ const sendArgs = cmd.slice(5).trim();
1628
+ const parts = sendArgs.split(/\s+/);
1629
+ const filePath = parts[parts.length - 1];
1630
+ if (filePath) {
1631
+ const resolved = path.resolve(session.projectPath, filePath).replace(/\\/g, '/');
1632
+ const projectPath = session.projectPath.replace(/\\/g, '/');
1633
+ if (!resolved.startsWith(projectPath)) {
1634
+ return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
1635
+ }
1636
+ }
1637
+ }
1638
+ // 5.1 /queue: 消息队列查询与操作(直接操作 MessageQueue,不走 handle())
1639
+ if (cmd === '/queue' || cmd.startsWith('/queue ')) {
1640
+ const args = cmd.slice('/queue'.length).trim();
1641
+ return await this.handleQueueCommand(sessionId, args);
1642
+ }
1643
+ // 6. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
1644
+ try {
1645
+ const result = await this._handleInternal(cmd, session.channel, session.channelId, undefined, // 不发送消息
1646
+ userId, session.threadId || undefined, session.chatType, undefined, undefined, session.selfAID, ctlIdentity);
1647
+ const text = typeof result === 'string' ? result : (result && 'text' in result ? result.text : '(无输出)');
1648
+ if (result && typeof result === 'object' && 'kind' in result && result.kind === 'command.error') {
1649
+ return { ok: false, error: text || '执行失败' };
1650
+ }
1651
+ return { ok: true, result: text || '(无输出)', ...(result && typeof result === 'object' && 'structured' in result ? { data: result.structured } : {}) };
1652
+ }
1653
+ catch (err) {
1654
+ return { ok: false, error: err.message };
1655
+ }
1656
+ }
1657
+ // ── Queue command ──
1658
+ /** 提取命名参数值(如 --cancel msg123 → "msg123") */
1659
+ extractArg(args, flag) {
1660
+ const idx = args.indexOf(flag);
1661
+ if (idx === -1)
1662
+ return '';
1663
+ const rest = args.slice(idx + flag.length).trim();
1664
+ const spaceIdx = rest.indexOf(' ');
1665
+ return spaceIdx >= 0 ? rest.slice(0, spaceIdx) : rest;
1666
+ }
1667
+ async handleQueueCommand(sessionId, args) {
1668
+ const showId = args.includes('--showid');
1669
+ const formatJson = args.includes('--format json');
1670
+ const full = args.includes('--full');
1671
+ // 操作分支
1672
+ if (args.includes('--clear')) {
1673
+ const count = this.messageQueue.clearBySession(sessionId);
1674
+ return { ok: true, result: `✅ 已清空 ${count} 条待处理消息` };
1675
+ }
1676
+ if (args.includes('--cancel')) {
1677
+ const msgId = this.extractArg(args, '--cancel');
1678
+ if (!msgId)
1679
+ return { ok: false, error: '❌ --cancel 需要指定 messageId' };
1680
+ const success = this.messageQueue.cancelMessageByIdInSession(sessionId, msgId);
1681
+ return success
1682
+ ? { ok: true, result: `✅ 已取消消息 ${msgId}` }
1683
+ : { ok: false, error: `❌ 未找到消息 ${msgId}` };
1684
+ }
1685
+ if (args.includes('--interrupt')) {
1686
+ const interrupted = await this.messageQueue.interruptBySession(sessionId);
1687
+ return interrupted
1688
+ ? { ok: true, result: `✅ 已打断处理中任务` }
1689
+ : { ok: false, error: `❌ 当前无处理中任务` };
1690
+ }
1691
+ // 查询
1692
+ const items = this.messageQueue.getQueueItemsBySession(sessionId);
1693
+ if (formatJson) {
1694
+ return { ok: true, result: JSON.stringify({ items }, null, 2) };
1695
+ }
1696
+ return { ok: true, result: renderQueueItemsCtl(items, showId, full) };
1697
+ }
1698
+ }
1699
+ /**
1700
+ * ctl 专用渲染:不显示 session 标识列(因为只有一个 session)
1701
+ */
1702
+ function renderQueueItemsCtl(items, showId, full) {
1703
+ if (items.length === 0) {
1704
+ return `当前会话队列 (0 条待处理)\n\n(无待处理消息)`;
1705
+ }
1706
+ const lines = [`当前会话队列 (${items.length} 条待处理)`, ''];
1707
+ // 计算列宽
1708
+ const maxIdLen = showId ? Math.max(...items.map(i => i.messageId?.length ?? 0)) : 0;
1709
+ const maxNameLen = Math.max(...items.map(i => (i.peerName ? `[${i.peerName}]`.length : 0)));
1710
+ for (const item of items) {
1711
+ const parts = [' '];
1712
+ if (showId) {
1713
+ const id = (item.messageId || '').padEnd(maxIdLen);
1714
+ parts.push(` ${id} `);
1715
+ }
1716
+ if (item.peerName) {
1717
+ const name = `[${item.peerName}]`.padEnd(maxNameLen);
1718
+ parts.push(` ${name} `);
1719
+ }
1720
+ const content = full ? item.preview.replace('...', '') : item.preview;
1721
+ parts.push(`"${content}"`);
1722
+ lines.push(parts.join(''));
1723
+ }
1724
+ return lines.join('\n');
1725
+ }
1726
+ function sameFeedbackTarget(target, origin) {
1727
+ if (!origin)
1728
+ return false;
1729
+ return target.channelKey === origin.channelKey
1730
+ && target.channelId === origin.channelId
1731
+ && target.session === origin.session
1732
+ && (target.threadId ?? '') === (origin.threadId ?? '');
1733
+ }