evolcore 0.0.1 → 0.0.2

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 (371) hide show
  1. package/CHANGELOG.md +813 -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/assets/brand/evolcore/README.md +19 -0
  8. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  9. package/assets/brand/evolcore/evolcore-app-icon.svg +13 -0
  10. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  11. package/assets/brand/evolcore/evolcore-brand-board.svg +126 -0
  12. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  13. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  14. package/assets/brand/evolcore/evolcore-logo-reverse.svg +14 -0
  15. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  16. package/assets/brand/evolcore/evolcore-logo.svg +14 -0
  17. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  18. package/assets/brand/evolcore/evolcore-mark.svg +10 -0
  19. package/bin/ec-safe-output.js +161 -0
  20. package/bin/ec.js +29 -0
  21. package/dist/agents/baseagent.js +163 -0
  22. package/dist/agents/claude-runner.js +2385 -0
  23. package/dist/agents/codex-app-server-client.js +448 -0
  24. package/dist/agents/codex-runner.js +2639 -0
  25. package/dist/agents/gemini-runner.js +666 -0
  26. package/dist/agents/runner-types.js +75 -0
  27. package/dist/aun/aid/agentmd.js +216 -0
  28. package/dist/aun/aid/client.js +132 -0
  29. package/dist/aun/aid/control-aid.js +91 -0
  30. package/dist/aun/aid/identity.js +518 -0
  31. package/dist/aun/aid/index.js +4 -0
  32. package/dist/aun/aid/store.js +74 -0
  33. package/dist/aun/aid/types.js +1 -0
  34. package/dist/aun/aid/validation.js +21 -0
  35. package/dist/aun/group-identity.js +10 -0
  36. package/dist/aun/msg/group-index.js +6 -0
  37. package/dist/aun/msg/group.js +1231 -0
  38. package/dist/aun/msg/history.js +123 -0
  39. package/dist/aun/msg/index.js +5 -0
  40. package/dist/aun/msg/p2p.js +393 -0
  41. package/dist/aun/msg/payload-type.js +27 -0
  42. package/dist/aun/msg/upload.js +137 -0
  43. package/dist/aun/outbox.js +160 -0
  44. package/dist/aun/rpc/caller.js +42 -0
  45. package/dist/aun/rpc/connection.js +25 -0
  46. package/dist/aun/rpc/index.js +2 -0
  47. package/dist/aun/service-proxy.js +225 -0
  48. package/dist/aun/storage/download.js +29 -0
  49. package/dist/aun/storage/index.js +3 -0
  50. package/dist/aun/storage/manage.js +10 -0
  51. package/dist/aun/storage/upload.js +68 -0
  52. package/dist/channels/aun.js +4147 -0
  53. package/dist/channels/daemon.js +422 -0
  54. package/dist/channels/dingtalk.js +649 -0
  55. package/dist/channels/feishu.js +1789 -0
  56. package/dist/channels/qqbot.js +409 -0
  57. package/dist/channels/wechat.js +817 -0
  58. package/dist/channels/wecom.js +565 -0
  59. package/dist/cli/agent-command.js +641 -0
  60. package/dist/cli/agent.js +1059 -0
  61. package/dist/cli/aun-commands.js +1948 -0
  62. package/dist/cli/bench.js +1228 -0
  63. package/dist/cli/cli-argv.js +66 -0
  64. package/dist/cli/code-stats.js +329 -0
  65. package/dist/cli/command-log.js +82 -0
  66. package/dist/cli/config-selector.js +69 -0
  67. package/dist/cli/config.js +261 -0
  68. package/dist/cli/ctl-command.js +62 -0
  69. package/dist/cli/daemon-commands.js +2887 -0
  70. package/dist/cli/fs-command.js +1447 -0
  71. package/dist/cli/handoff-command.js +302 -0
  72. package/dist/cli/help.js +31 -0
  73. package/dist/cli/index.js +358 -0
  74. package/dist/cli/init-channel.js +1377 -0
  75. package/dist/cli/init.js +553 -0
  76. package/dist/cli/link-rules.js +240 -0
  77. package/dist/cli/model.js +590 -0
  78. package/dist/cli/net-check.js +723 -0
  79. package/dist/cli/queue-command.js +126 -0
  80. package/dist/cli/response.js +345 -0
  81. package/dist/cli/restart-monitor.js +456 -0
  82. package/dist/cli/stats.js +607 -0
  83. package/dist/cli/task-context.js +80 -0
  84. package/dist/cli/trigger-command.js +505 -0
  85. package/dist/cli/version.js +88 -0
  86. package/dist/cli/watch-logs.js +33 -0
  87. package/dist/cli/watch-msg.js +673 -0
  88. package/dist/config/boot-log.js +266 -0
  89. package/dist/config/builtin-role-templates.js +30 -0
  90. package/dist/config/builtin-roles.js +85 -0
  91. package/dist/config/config-batch-get.js +11 -0
  92. package/dist/config/config-field-policy.js +261 -0
  93. package/dist/config/config-manager.js +916 -0
  94. package/dist/config/config-operation-service.js +384 -0
  95. package/dist/config/contact-book.js +371 -0
  96. package/dist/config/gateway-config.js +858 -0
  97. package/dist/config/lifecycle.js +17 -0
  98. package/dist/config/mention-mode.js +27 -0
  99. package/dist/config/merge.js +161 -0
  100. package/dist/config/owner-policy.js +4 -0
  101. package/dist/config/peer-role-resolver.js +139 -0
  102. package/dist/config/resolved-config-op.js +483 -0
  103. package/dist/config/role-config-v4-startup.js +32 -0
  104. package/dist/config/role-config-v5-startup.js +27 -0
  105. package/dist/config/role-schema.js +105 -0
  106. package/dist/config/role-service.js +159 -0
  107. package/dist/config/role-store.js +204 -0
  108. package/dist/config/roles.js +55 -0
  109. package/dist/config/schema-registry.js +154 -0
  110. package/dist/config/snapshot.js +598 -0
  111. package/dist/config-store.js +503 -0
  112. package/dist/core/auth/agent-delegation.js +111 -0
  113. package/dist/core/auth/auth-gateway.js +166 -0
  114. package/dist/core/auth/authenticated-actor.js +6 -0
  115. package/dist/core/auth/authorization-audit.js +110 -0
  116. package/dist/core/auth/operation-authorizer.js +718 -0
  117. package/dist/core/auth/operation-catalog.js +675 -0
  118. package/dist/core/baseagent-loader.js +54 -0
  119. package/dist/core/bootstrap-service.js +175 -0
  120. package/dist/core/capability/capability-manager.js +316 -0
  121. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  122. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  123. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  124. package/dist/core/capability/types.js +27 -0
  125. package/dist/core/causation/audit.js +103 -0
  126. package/dist/core/causation/aun-association.js +111 -0
  127. package/dist/core/causation/context.js +93 -0
  128. package/dist/core/causation/index.js +4 -0
  129. package/dist/core/causation/types.js +2 -0
  130. package/dist/core/channel-loader.js +277 -0
  131. package/dist/core/command/agent-control.js +616 -0
  132. package/dist/core/command/cli-intent-parser.js +225 -0
  133. package/dist/core/command/command-handler.js +1638 -0
  134. package/dist/core/command/menu-handler.js +3354 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1524 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3056 -0
  139. package/dist/core/daemon-file-cache.js +216 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +740 -0
  142. package/dist/core/evolagent-registry.js +546 -0
  143. package/dist/core/evolagent.js +331 -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 +657 -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 +803 -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 +1436 -0
  161. package/dist/core/message/message-utils.js +70 -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 +3776 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/stream-debouncer.js +130 -0
  168. package/dist/core/message/stream-idle-monitor.js +124 -0
  169. package/dist/core/model/config-scope.js +162 -0
  170. package/dist/core/model/field-scope.js +78 -0
  171. package/dist/core/model/model-catalog.js +227 -0
  172. package/dist/core/model/model-diagnostics.js +182 -0
  173. package/dist/core/model/model-permission.js +90 -0
  174. package/dist/core/permission/approval-gateway.js +1017 -0
  175. package/dist/core/permission/ec-command-parser.js +347 -0
  176. package/dist/core/permission/execution-sandbox.js +16 -0
  177. package/dist/core/permission/index.js +6 -0
  178. package/dist/core/permission/mode.js +24 -0
  179. package/dist/core/permission/sandbox-runtime.js +265 -0
  180. package/dist/core/permission/tool-policy.js +987 -0
  181. package/dist/core/permission/unix-socket-policy.js +99 -0
  182. package/dist/core/protected-paths.js +330 -0
  183. package/dist/core/relation/peer-identity.js +222 -0
  184. package/dist/core/relation/peer-key.js +1 -0
  185. package/dist/core/role/runtime-policy.js +141 -0
  186. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  187. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  188. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  189. package/dist/core/session/session-file-adapter.js +7 -0
  190. package/dist/core/session/session-file-health.js +45 -0
  191. package/dist/core/session/session-fs-store.js +232 -0
  192. package/dist/core/session/session-key.js +24 -0
  193. package/dist/core/session/session-manager.js +1587 -0
  194. package/dist/core/session/session-mapper.js +100 -0
  195. package/dist/core/session/session-renew.js +314 -0
  196. package/dist/core/session/session-title.js +128 -0
  197. package/dist/core/session/session-turn-coordinator.js +205 -0
  198. package/dist/core/session/session-turns.js +67 -0
  199. package/dist/core/system-channels.js +29 -0
  200. package/dist/eck/baseagent-caps.js +18 -0
  201. package/dist/eck/detect.js +47 -0
  202. package/dist/eck/group-rules-sync.js +345 -0
  203. package/dist/eck/init.js +77 -0
  204. package/dist/eck/kit-renderer.js +359 -0
  205. package/dist/eck/manifest-engine.js +446 -0
  206. package/dist/eck/message-renderer.js +199 -0
  207. package/dist/eck/rules-loader.js +28 -0
  208. package/dist/index.js +2870 -4
  209. package/dist/ipc.js +748 -0
  210. package/dist/paths.js +262 -0
  211. package/dist/product.js +18 -0
  212. package/dist/response-system/context-builder.js +71 -0
  213. package/dist/response-system/coordinator.js +117 -0
  214. package/dist/response-system/decision-executor.js +86 -0
  215. package/dist/response-system/engines/v1/index.js +21 -0
  216. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  217. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  218. package/dist/response-system/engines/v1/types.js +1 -0
  219. package/dist/response-system/extensions.js +41 -0
  220. package/dist/response-system/index.js +6 -0
  221. package/dist/response-system/modes/index.js +7 -0
  222. package/dist/response-system/modes/single-session/index.js +72 -0
  223. package/dist/response-system/queues/fifo-queue.js +44 -0
  224. package/dist/response-system/queues/index.js +6 -0
  225. package/dist/response-system/queues/lifo-queue.js +42 -0
  226. package/dist/response-system/queues/priority-queue.js +63 -0
  227. package/dist/response-system/registry.js +97 -0
  228. package/dist/response-system/resolver.js +37 -0
  229. package/dist/response-system/selector.js +23 -0
  230. package/dist/response-system/types.js +7 -0
  231. package/dist/stats/billing.js +151 -0
  232. package/dist/stats/budget.js +93 -0
  233. package/dist/stats/db.js +403 -0
  234. package/dist/stats/eck-vars.js +89 -0
  235. package/dist/stats/index.js +11 -0
  236. package/dist/stats/normalizer.js +80 -0
  237. package/dist/stats/price-resolver.js +138 -0
  238. package/dist/stats/query.js +763 -0
  239. package/dist/stats/role-budget.js +168 -0
  240. package/dist/stats/writer.js +151 -0
  241. package/dist/trigger/anomaly-store.js +258 -0
  242. package/dist/trigger/audit.js +152 -0
  243. package/dist/trigger/event-source.js +119 -0
  244. package/dist/trigger/feedback.js +685 -0
  245. package/dist/trigger/history.js +290 -0
  246. package/dist/trigger/manager.js +291 -0
  247. package/dist/trigger/parser.js +520 -0
  248. package/dist/trigger/patch.js +148 -0
  249. package/dist/trigger/scheduler.js +1600 -0
  250. package/dist/trigger/script-executor.js +155 -0
  251. package/dist/trigger/state.js +145 -0
  252. package/dist/trigger/types.js +1 -0
  253. package/dist/trigger/validation.js +621 -0
  254. package/dist/types.js +12 -0
  255. package/dist/utils/aid-bind.js +299 -0
  256. package/dist/utils/atomic-write.js +95 -0
  257. package/dist/utils/avatar-upload.js +123 -0
  258. package/dist/utils/cross-platform.js +297 -0
  259. package/dist/utils/ecweb-utils.js +73 -0
  260. package/dist/utils/error-dict.json +153 -0
  261. package/dist/utils/error-utils.js +349 -0
  262. package/dist/utils/instance-registry.js +437 -0
  263. package/dist/utils/locale.js +21 -0
  264. package/dist/utils/log-writer.js +224 -0
  265. package/dist/utils/logger.js +89 -0
  266. package/dist/utils/markdown-to-plain-text.js +20 -0
  267. package/dist/utils/media-cache.js +271 -0
  268. package/dist/utils/model-prices.jsonl +17 -0
  269. package/dist/utils/npm-ops.js +210 -0
  270. package/dist/utils/process-introspect.js +133 -0
  271. package/dist/utils/process-tree-stats.js +271 -0
  272. package/dist/utils/project-path.js +74 -0
  273. package/dist/utils/stats.js +410 -0
  274. package/dist/utils/tool-summary.js +284 -0
  275. package/dist/utils/welcome.js +268 -0
  276. package/kits/docs/GUIDE.md +20 -0
  277. package/kits/docs/INDEX.md +65 -0
  278. package/kits/docs/aun/CHEATSHEET.md +19 -0
  279. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  280. package/kits/docs/channels/aun.md +65 -0
  281. package/kits/docs/channels/feishu.md +56 -0
  282. package/kits/docs/context-assembly.md +366 -0
  283. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  284. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  285. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  286. package/kits/docs/eck_templates/runtime.template.md +19 -0
  287. package/kits/docs/evolcore/INDEX.md +66 -0
  288. package/kits/docs/evolcore/agent.md +69 -0
  289. package/kits/docs/evolcore/aid.md +49 -0
  290. package/kits/docs/evolcore/config.md +149 -0
  291. package/kits/docs/evolcore/ctl.md +46 -0
  292. package/kits/docs/evolcore/event.md +216 -0
  293. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  294. package/kits/docs/evolcore/fs.md +101 -0
  295. package/kits/docs/evolcore/group-fs.md +17 -0
  296. package/kits/docs/evolcore/group-rules.md +226 -0
  297. package/kits/docs/evolcore/group.md +141 -0
  298. package/kits/docs/evolcore/model.md +47 -0
  299. package/kits/docs/evolcore/msg.md +130 -0
  300. package/kits/docs/evolcore/response.md +80 -0
  301. package/kits/docs/evolcore/rpc.md +35 -0
  302. package/kits/docs/evolcore/self-summary.md +29 -0
  303. package/kits/docs/evolcore/stats.md +70 -0
  304. package/kits/docs/evolcore/storage.md +49 -0
  305. package/kits/docs/evolcore/trigger.md +524 -0
  306. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  307. package/kits/docs/identity/PATH_OPS.md +16 -0
  308. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  309. package/kits/docs/identity/identity-tools.md +26 -0
  310. package/kits/docs/path-registry.md +43 -0
  311. package/kits/docs/prompt-loading-architecture.md +266 -0
  312. package/kits/docs/venues/aun-group.md +45 -0
  313. package/kits/docs/venues/aun-private.md +10 -0
  314. package/kits/docs/venues/client-desktop.md +10 -0
  315. package/kits/docs/venues/client-mobile.md +10 -0
  316. package/kits/docs/venues/feishu-group.md +13 -0
  317. package/kits/docs/venues/feishu-private.md +9 -0
  318. package/kits/docs/venues/group.md +25 -0
  319. package/kits/docs/venues/private.md +10 -0
  320. package/kits/eck_manifest.auxiliary.json +43 -0
  321. package/kits/eck_manifest.json +191 -0
  322. package/kits/eck_message_manifest.json +63 -0
  323. package/kits/migrations/README-role-config-v4.md +32 -0
  324. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  325. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  326. package/kits/migrations/rename-config-file.mjs +99 -0
  327. package/kits/rules/01-overview.md +142 -0
  328. package/kits/rules/02-navigation.md +76 -0
  329. package/kits/rules/03-identity.md +34 -0
  330. package/kits/rules/04-relation.md +59 -0
  331. package/kits/rules/05-venue.md +44 -0
  332. package/kits/rules/06-channel.md +59 -0
  333. package/kits/schemas/_meta.json +29 -0
  334. package/kits/schemas/agent-config.schema.1.json +177 -0
  335. package/kits/schemas/agent-config.schema.2.json +239 -0
  336. package/kits/schemas/agent-config.schema.3.json +119 -0
  337. package/kits/schemas/agent-config.schema.4.json +208 -0
  338. package/kits/schemas/agent-config.schema.5.json +326 -0
  339. package/kits/schemas/contact-book.schema.1.json +36 -0
  340. package/kits/schemas/daemon.schema.1.json +90 -0
  341. package/kits/schemas/defaults.schema.1.json +81 -0
  342. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  343. package/kits/schemas/migrations/README.md +28 -0
  344. package/kits/schemas/relation-config.schema.1.json +158 -0
  345. package/kits/schemas/relation-config.schema.2.json +73 -0
  346. package/kits/schemas/relation-config.schema.3.json +50 -0
  347. package/kits/schemas/relation-config.schema.4.json +47 -0
  348. package/kits/schemas/role-config.schema.1.json +200 -0
  349. package/kits/schemas/role-registry.schema.1.json +35 -0
  350. package/kits/schemas/single-session.schema.1.json +31 -0
  351. package/kits/templates/bootstrap-welcome.md +15 -0
  352. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  353. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  354. package/kits/templates/message-fragments/inject-default.md +2 -0
  355. package/kits/templates/message-fragments/item.md +2 -0
  356. package/kits/templates/roles/admin.json +8 -0
  357. package/kits/templates/roles/member.json +40 -0
  358. package/kits/templates/roles/owner.json +8 -0
  359. package/kits/templates/roles/visitor.json +39 -0
  360. package/kits/templates/system-fragments/baseagent.md +14 -0
  361. package/kits/templates/system-fragments/bootstrap.md +16 -0
  362. package/kits/templates/system-fragments/channel.md +48 -0
  363. package/kits/templates/system-fragments/commands.md +26 -0
  364. package/kits/templates/system-fragments/identity.md +11 -0
  365. package/kits/templates/system-fragments/relation.md +19 -0
  366. package/kits/templates/system-fragments/session.md +53 -0
  367. package/kits/templates/system-fragments/venue.md +31 -0
  368. package/package.json +50 -15
  369. package/dist/index.d.ts +0 -7
  370. package/dist/index.d.ts.map +0 -1
  371. package/dist/index.js.map +0 -1
@@ -0,0 +1,803 @@
1
+ import { createHash, randomBytes } from 'crypto';
2
+ import { logger } from '../../utils/logger.js';
3
+ import { StreamDebouncer } from './stream-debouncer.js';
4
+ import { appendMessageLog, appendMessageLogStrict, buildInboundEntry, isTransientProtocolMessage } from './message-log.js';
5
+ import { buildEnvelope } from './message-utils.js';
6
+ import { chatDirPath } from '../session/session-fs-store.js';
7
+ import { tryParseChannelKey } from '../channel-loader.js';
8
+ import { resolvePaths } from '../../paths.js';
9
+ import { resolvePeerRoleDetail } from '../../config/peer-role-resolver.js';
10
+ import { handlePendingDingtalkContactBindMessage } from '../../channels/dingtalk.js';
11
+ import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
12
+ import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, } from '../command/menu-protocol.js';
13
+ import { SessionRenewService } from '../session/session-renew.js';
14
+ import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
15
+ import { recordCausationSpan } from '../causation/audit.js';
16
+ /**
17
+ * MessageBridge — Channel 与 Core 之间的消息桥梁
18
+ *
19
+ * 入站管线:Channel.onMessage → owner 绑定 → 命令路由 → session 解析
20
+ * → 策略前缀 → 构造 Message → debounce → ACK → enqueue
21
+ * 出站:命令响应通过 sendReply 回调直接发送到渠道
22
+ */
23
+ export class MessageBridge {
24
+ defaultProjectPath;
25
+ sessionManager;
26
+ processor;
27
+ messageQueue;
28
+ cmdHandler;
29
+ eventBus;
30
+ processOwnersProvider;
31
+ debouncers = new Map();
32
+ defaultDebounce;
33
+ agentRegistry;
34
+ bootstrapService;
35
+ menuDeduper = new MenuRequestDeduper();
36
+ menuDiagnostics = new MenuDiagnosticLimiter();
37
+ handoffRuntime;
38
+ sessionRenewService;
39
+ constructor(defaultProjectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, defaultDebounce, processOwnersProvider) {
40
+ this.defaultProjectPath = defaultProjectPath;
41
+ this.sessionManager = sessionManager;
42
+ this.processor = processor;
43
+ this.messageQueue = messageQueue;
44
+ this.cmdHandler = cmdHandler;
45
+ this.eventBus = eventBus;
46
+ this.processOwnersProvider = processOwnersProvider;
47
+ this.defaultDebounce = defaultDebounce ?? 0;
48
+ this.sessionRenewService = new SessionRenewService(sessionManager, processor);
49
+ }
50
+ /** Inject EvolAgentRegistry so owner lookups/writes route to agent.json for agent-owned channels. */
51
+ setAgentRegistry(registry) {
52
+ this.agentRegistry = registry;
53
+ }
54
+ setBootstrapService(service) {
55
+ this.bootstrapService = service;
56
+ }
57
+ setHandoffRuntime(runtime) {
58
+ this.handoffRuntime = runtime;
59
+ }
60
+ getDebouncer(channelName, channelType) {
61
+ let d = this.debouncers.get(channelName);
62
+ if (!d) {
63
+ // 从 owning agent 的 channel 配置取 debounce,找不到用全局默认
64
+ let seconds = this.defaultDebounce;
65
+ const agent = this.agentRegistry?.resolveByChannel(channelName);
66
+ if (agent) {
67
+ const merged = agent.config;
68
+ if (merged?.debounce !== undefined)
69
+ seconds = merged.debounce;
70
+ }
71
+ d = new StreamDebouncer(seconds);
72
+ this.debouncers.set(channelName, d);
73
+ }
74
+ return d;
75
+ }
76
+ availableBaseagentsFor(channelName) {
77
+ const owningAgent = this.agentRegistry?.resolveByChannel(channelName);
78
+ if (!owningAgent)
79
+ return [];
80
+ const prefix = `${owningAgent.name}::`;
81
+ return this.processor.getAvailableAgents()
82
+ .filter(key => key.startsWith(prefix))
83
+ .map(key => key.slice(prefix.length));
84
+ }
85
+ async alignSessionBaseagent(channelName, session) {
86
+ const owningAgent = this.agentRegistry?.resolveByChannel(channelName);
87
+ if (!owningAgent)
88
+ return session;
89
+ const available = this.availableBaseagentsFor(channelName);
90
+ if (available.length === 0 || available.includes(session.baseagent))
91
+ return session;
92
+ const preferred = available.includes(owningAgent.baseagent)
93
+ ? owningAgent.baseagent
94
+ : available[0];
95
+ logger.warn(`[MessageBridge] Aligning unavailable session baseagent: session=${session.id} ${session.baseagent} -> ${preferred} agent=${owningAgent.name} available=${available.join(',')}`);
96
+ await this.sessionManager.updateSession(session.id, { baseagent: preferred, agentSessionId: null });
97
+ return { ...session, baseagent: preferred, agentSessionId: undefined };
98
+ }
99
+ /**
100
+ * 为渠道注册消息桥梁:入站处理管线 + 出站命令响应
101
+ *
102
+ * @param channelName 渠道实例名(用于 debounce 隔离)
103
+ * @param onMessage 注册入站消息监听
104
+ * @param sendReply 出站:命令响应发送回调
105
+ * @param adapter 渠道适配器(用于 ACK)
106
+ * @param channelType 渠道类型(feishu/wechat/aun),用于 session 和 message.channel
107
+ */
108
+ register(channelName, onMessage, sendReply, adapter, channelType) {
109
+ const effectiveChannelType = channelType || channelName;
110
+ onMessage(async (msg) => {
111
+ try {
112
+ let content = msg.content.trim();
113
+ const channelKey = adapter?.channelKey || channelName;
114
+ const owningAgent = this.agentRegistry?.resolveByChannel(channelKey)
115
+ ?? this.agentRegistry?.resolveByChannel(channelName);
116
+ const parsedChannelKey = tryParseChannelKey(channelKey);
117
+ const chatType = msg.chatType || 'private';
118
+ const actorId = msg.peerId;
119
+ const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
120
+ const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
121
+ const resolvedChannelType = msg.channelType || parsedChannelKey?.type || effectiveChannelType;
122
+ const menuControl = resolvedChannelType === 'aun'
123
+ ? parseMenuControl(content)
124
+ : { isMenu: false };
125
+ if (menuControl.isMenu) {
126
+ this.logMenuInbound(channelName, msg, menuControl);
127
+ if (!hasValidMenuId(menuControl)) {
128
+ this.logMenuDiagnostic('missing-id', channelName, msg, menuControl);
129
+ return;
130
+ }
131
+ const validationError = validateMenuRequest(menuControl.request);
132
+ if (validationError) {
133
+ const header = { id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) };
134
+ await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, validationError));
135
+ return;
136
+ }
137
+ }
138
+ if (!menuControl.isMenu) {
139
+ const contactBind = handlePendingDingtalkContactBindMessage({
140
+ selfAid,
141
+ channelName: channelKey,
142
+ channelType: resolvedChannelType,
143
+ chatType,
144
+ actorId,
145
+ content,
146
+ });
147
+ if (contactBind.handled) {
148
+ logger.info(`[MessageBridge] DingTalk contact bind handled: channel=${channelKey} actor=${actorId ?? '<none>'} status=${contactBind.status}`);
149
+ if (contactBind.reply) {
150
+ await sendReply(msg.channelId, contactBind.reply, msg.replyContext);
151
+ }
152
+ return;
153
+ }
154
+ }
155
+ const trustedHandoffEcho = msg.source === 'handoff'
156
+ && resolvedChannelType === 'aun'
157
+ && !!selfAid
158
+ && actorId === selfAid;
159
+ const roleDetail = trustedHandoffEcho
160
+ ? {
161
+ effectiveRole: 'owner',
162
+ source: 'agent-owner',
163
+ isAuthenticated: true,
164
+ allowAccess: true,
165
+ roleExists: true,
166
+ actor: {
167
+ operatorId: actorId,
168
+ operatorChannelKey: channelKey,
169
+ operatorChannelType: String(resolvedChannelType || '').trim().toLowerCase(),
170
+ principalId: actorId,
171
+ principalStatus: 'native_aid',
172
+ },
173
+ }
174
+ : this.resolveInboundRole({
175
+ selfAid,
176
+ channelKey,
177
+ channelType: resolvedChannelType,
178
+ chatType,
179
+ actorId,
180
+ conversationId,
181
+ peerType: msg.peerType,
182
+ });
183
+ const authSubject = buildAuthSubject({
184
+ selfAid,
185
+ actorId,
186
+ channel: channelName,
187
+ channelType: resolvedChannelType,
188
+ channelId: msg.channelId,
189
+ chatType,
190
+ conversationId,
191
+ peerType: msg.peerType,
192
+ roleDetail,
193
+ fromControlChannel: msg.isControlChannel ?? false,
194
+ processOwners: [...(this.processOwnersProvider?.() ?? [])],
195
+ });
196
+ const identity = authSubject.identity;
197
+ if (menuControl.isMenu) {
198
+ await this.handleMenuControl(menuControl, channelName, resolvedChannelType, msg, adapter, authSubject);
199
+ return;
200
+ }
201
+ // 普通消息保留原始日志;控制 payload 使用脱敏元数据日志。
202
+ logger.channelIn({ channel: channelName, channelId: msg.channelId, peerId: msg.peerId, peerName: msg.peerName, chatType: msg.chatType, msgId: msg.messageId, threadId: msg.threadId, content, images: msg.images?.length ?? 0, mentions: msg.mentions, replyContext: msg.replyContext });
203
+ const accessDecision = authorizeAccess(authSubject);
204
+ if (!accessDecision.allow) {
205
+ logger.warn(`[MessageBridge] Access denied before command routing: channel=${channelName} channelId=${msg.channelId} actor=${actorId ?? '<none>'} role=${identity.role} reason=${accessDecision.reason}`);
206
+ await this.sendAccessDenied(adapter, channelName, msg, sendReply);
207
+ return;
208
+ }
209
+ // 0. 仅阻止协议遥测进入 session / model / messages.jsonl;协议本身已在
210
+ // channel/main/events/AUN/handoff 专用链路中处理和记录。
211
+ // 注:menu.* 自定义消息已由上方 handleMenuControl 快速路径处理。
212
+ if (isTransientProtocolMessage({
213
+ msgType: msg.msgType ?? 'custom',
214
+ payloadType: msg.payloadType,
215
+ })) {
216
+ logger.debug(`[MessageBridge] Transient protocol message ignored: channel=${channelName} type=${msg.payloadType || msg.msgType || '<unknown>'}`);
217
+ return;
218
+ }
219
+ // 1. owner 绑定(按实例名绑定)
220
+ if (adapter && actorId && roleDetail.effectiveRole === 'owner') {
221
+ await this.bootstrapService?.tryStartBootstrap({
222
+ adapter,
223
+ channelKey,
224
+ channelType: msg.channelType || effectiveChannelType,
225
+ channelId: msg.channelId,
226
+ recipientId: actorId,
227
+ recipientName: msg.peerName,
228
+ source: 'inbound',
229
+ });
230
+ }
231
+ // 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
232
+ const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
233
+ const cmdContent = contentForCmd || content;
234
+ const isCmd = msg.source !== 'handoff' && this.cmdHandler.isCommand(cmdContent);
235
+ if (isCmd) {
236
+ logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
237
+ // 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
238
+ try {
239
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfAID || '');
240
+ const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
241
+ const inboundChatmode = msg.replyContext?.metadata?.chatmode;
242
+ const inboundReplyTo = typeof msg.replyContext?.metadata?.refMessageId === 'string'
243
+ ? msg.replyContext.metadata.refMessageId
244
+ : null;
245
+ appendMessageLog(chatDir, buildInboundEntry({
246
+ from: msg.peerId || 'unknown',
247
+ to: msg.selfAID || 'self',
248
+ threadId: msg.threadId ?? null,
249
+ chatType: msg.chatType || 'private',
250
+ groupId: msg.groupId ?? null,
251
+ msgId: msg.messageId ?? null,
252
+ content: msg.messageLogContent ?? content,
253
+ replyTo: inboundReplyTo,
254
+ permMode: null,
255
+ timestamp: Date.now(),
256
+ encrypt: inboundEncrypt,
257
+ chatmode: inboundChatmode,
258
+ peerName: msg.peerName,
259
+ peerType: msg.peerType,
260
+ msgType: msg.msgType,
261
+ payloadType: msg.payloadType,
262
+ payloadSummary: msg.payloadSummary,
263
+ }));
264
+ }
265
+ catch (e) {
266
+ logger.debug(`[MessageBridge] Failed to log inbound command: ${e}`);
267
+ }
268
+ }
269
+ if (msg.source !== 'handoff' && await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
270
+ logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId: `cmd-${msg.messageId || Date.now()}`, payload: { kind: 'command.result', text } });
271
+ return sendReply(msg.channelId, text, msg.replyContext);
272
+ }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
273
+ return;
274
+ // 3. session 解析(使用 Channel 层填充的 chatType)
275
+ if (!(await this.canCreateThreadSession(channelName, msg, chatType))) {
276
+ // 静默丢弃:绝不向群里注入回复。
277
+ // 拒绝消息本身会带原 thread_id(AUN replyContext 透传),变成一条新群消息;
278
+ // 若发送者也是 agent,该拒绝消息又会 @ 回对方 → A 拒绝→B 收到→B 拒绝→A 收到 的无限循环。
279
+ // AUN 自主模式下「不响应」是合法的,因此无权限创建话题时只记日志、直接 return。
280
+ logger.info(`[MessageBridge] Thread creation denied (silent drop): channel=${channelName} channelId=${msg.channelId} thread=${msg.threadId} sender=${msg.peerId}`);
281
+ return;
282
+ }
283
+ const metadata = {};
284
+ // 话题会话创建时写入 replyContext(用于 threadId 路由);主会话不写(避免群聊覆盖)
285
+ if (msg.threadId && msg.replyContext)
286
+ metadata.replyContext = msg.replyContext;
287
+ // 写入实例名(审计 + 精确出站路由)
288
+ metadata.channelKey = channelKey;
289
+ if (chatType === 'private' && msg.peerId) {
290
+ metadata.peerId = msg.peerId;
291
+ if (msg.peerName)
292
+ metadata.peerName = msg.peerName;
293
+ if (msg.peerType)
294
+ metadata.peerType = msg.peerType;
295
+ }
296
+ if (chatType === 'group') {
297
+ // 群聊:peerId 是当前消息发送者;groupId 在 channel 提供时存到 metadata
298
+ if (msg.peerId)
299
+ metadata.peerId = msg.peerId;
300
+ if (msg.peerName)
301
+ metadata.peerName = msg.peerName;
302
+ if (msg.peerType)
303
+ metadata.peerType = msg.peerType;
304
+ if (msg.groupId)
305
+ metadata.groupId = msg.groupId;
306
+ }
307
+ // Resolve effective project path: 用通道所属 agent 的 projectPath;
308
+ // 通道找不到归属时退回到 globalConfig(一般是测试场景)
309
+ const effectiveProjectPath = owningAgent?.projectPath
310
+ ?? this.defaultProjectPath;
311
+ const hadMainSession = msg.threadId ? true : this.sessionManager.hasMainSession(channelName, msg.channelId, msg.channelType || effectiveChannelType, msg.selfAID || selfAid);
312
+ let session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, this.extractTopicName(msg), msg.peerId, chatType, owningAgent?.baseagent, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType, identity);
313
+ session = await this.alignSessionBaseagent(channelName, session);
314
+ const replyToMessageId = typeof msg.replyContext?.metadata?.refMessageId === 'string'
315
+ ? msg.replyContext.metadata.refMessageId
316
+ : msg.replyContext?.replyToMessageId ?? null;
317
+ const renewResult = await this.sessionRenewService.resolve({
318
+ session,
319
+ channelName,
320
+ channelType: msg.channelType || effectiveChannelType,
321
+ channelId: msg.channelId,
322
+ selfAid: session.selfAID || selfAid,
323
+ peerId: msg.peerId,
324
+ groupId: msg.groupId,
325
+ chatType,
326
+ role: identity.role,
327
+ content,
328
+ replyToMessageId,
329
+ isNewSession: !hadMainSession,
330
+ });
331
+ session = renewResult.session;
332
+ // 4. 群聊发送者标注由消息渲染层(message-renderer)逐条承担,不再在此硬编码前缀,
333
+ // 消息日志因此保存干净原文。policy.messagePrefix 暂保留(未来清理)。
334
+ // 5. 构造完整消息(channel 字段存实例名,用于 session 精确匹配)
335
+ const fullMessage = {
336
+ channel: channelName,
337
+ channelType: msg.channelType || effectiveChannelType,
338
+ channelId: msg.channelId, content,
339
+ selfAID: msg.selfAID,
340
+ baseagent: session.baseagent,
341
+ chatType,
342
+ images: msg.images, timestamp: Date.now(),
343
+ peerId: msg.peerId, peerName: msg.peerName,
344
+ peerType: msg.peerType,
345
+ sameDevice: msg.sameDevice,
346
+ sameNetwork: msg.sameNetwork,
347
+ sameEgressIp: msg.sameEgressIp,
348
+ // 入站加密态(仅 aun 渠道有意义;非 aun 为 undefined)。回复加密态跟随此值,
349
+ // 并经 mergeItems 逐条保留 + 密文优先聚合,message-renderer 据此标注。
350
+ encrypted: msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined,
351
+ messageId: msg.messageId,
352
+ mentions: msg.mentions, mentionAids: msg.mentionAids, threadId: msg.threadId,
353
+ isMentioned: msg.isMentioned,
354
+ topicName: this.extractTopicName(msg),
355
+ replyContext: msg.replyContext,
356
+ source: msg.source,
357
+ dispatchMode: msg.dispatchMode,
358
+ causation: (() => {
359
+ const restored = (msg.channelType || effectiveChannelType) === 'aun'
360
+ ? normalizeCausation(msg.causation)
361
+ : undefined;
362
+ const inbound = restored ? deriveCausation(restored) : createRootCausation();
363
+ recordCausationSpan(inbound, 'message.inbound', {
364
+ status: 'completed',
365
+ refs: { messageId: msg.messageId, sessionId: session.id },
366
+ });
367
+ return inbound;
368
+ })(),
369
+ };
370
+ const inboundEntry = (() => {
371
+ const chatDir = this.sessionManager.getChatDir(session);
372
+ const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
373
+ const inboundChatmode = msg.replyContext?.metadata?.chatmode;
374
+ const inboundReplyTo = typeof msg.replyContext?.metadata?.refMessageId === 'string'
375
+ ? msg.replyContext.metadata.refMessageId
376
+ : null;
377
+ return { chatDir, entry: buildInboundEntry({
378
+ from: msg.peerId || 'unknown',
379
+ to: msg.selfAID || 'self',
380
+ sessionId: session.id,
381
+ threadId: session.threadId || null,
382
+ chatType,
383
+ groupId: msg.groupId ?? null,
384
+ msgId: msg.messageId ?? null,
385
+ content: msg.messageLogContent ?? content,
386
+ replyTo: inboundReplyTo,
387
+ permMode: session.identity?.role ?? null,
388
+ timestamp: fullMessage.timestamp,
389
+ encrypt: inboundEncrypt,
390
+ chatmode: inboundChatmode,
391
+ peerName: msg.peerName,
392
+ peerType: msg.peerType,
393
+ msgType: msg.msgType,
394
+ payloadType: msg.payloadType,
395
+ payloadSummary: msg.payloadSummary,
396
+ }) };
397
+ })();
398
+ let handoffCandidate = false;
399
+ if (this.handoffRuntime &&
400
+ (msg.channelType || effectiveChannelType) === 'aun' &&
401
+ session.selfAID &&
402
+ fullMessage.messageId) {
403
+ const refMessageId = typeof msg.replyContext?.metadata?.refMessageId === 'string'
404
+ ? msg.replyContext.metadata.refMessageId
405
+ : null;
406
+ const bound = await this.handoffRuntime.bindReply({
407
+ selfAid: session.selfAID,
408
+ targetSessionId: session.id,
409
+ responseMessageId: fullMessage.messageId,
410
+ refMessageId,
411
+ persistReply: () => appendMessageLogStrict(inboundEntry.chatDir, {
412
+ ...inboundEntry.entry,
413
+ handoff_trace: { version: 2, reply_candidate: true },
414
+ }),
415
+ });
416
+ handoffCandidate = bound.candidate;
417
+ const handoffIds = bound.handoffIds ?? (bound.handoffId ? [bound.handoffId] : []);
418
+ if (handoffIds.length > 0) {
419
+ fullMessage.handoffDelivery = {
420
+ direction: 'target',
421
+ handoffId: handoffIds[0],
422
+ handoffIds,
423
+ };
424
+ }
425
+ }
426
+ if (!handoffCandidate)
427
+ appendMessageLog(inboundEntry.chatDir, inboundEntry.entry);
428
+ // 6. ACK + debounce/enqueue
429
+ // ACK 在到达时立即做(每条独立 ACK),不等合并
430
+ // Interrupt 模式(单聊)→ 入队前 debounce 合并
431
+ // FIFO 模式(群聊) → 跳过 debouncer,独立入队,出队时贪心合并
432
+ // 优化:队列空闲时跳过 Pin 表情,直接在开始执行时添加 CheckMark
433
+ if (fullMessage.messageId && this.messageQueue.getGlobalProcessingCount() > 0) {
434
+ adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
435
+ }
436
+ const isInterrupt = chatType !== 'group' && !handoffCandidate;
437
+ const enqueueAgentName = owningAgent?.name ?? '<unknown>';
438
+ const selfAID = session.selfAID;
439
+ const doEnqueue = async (m) => {
440
+ const enqueue = handoffCandidate ? this.messageQueue.enqueuePersisted.bind(this.messageQueue) : this.messageQueue.enqueue.bind(this.messageQueue);
441
+ try {
442
+ return await enqueue(session.id, m, session.projectPath, {
443
+ interruptible: isInterrupt,
444
+ interruptSamePeer: !isInterrupt, // 群聊:同人连发且队列无他人时打断
445
+ agentName: enqueueAgentName,
446
+ role: !isInterrupt ? (session.identity?.role ?? 'none') : undefined,
447
+ sessionKeyField: session.sessionKey,
448
+ selfAID, // ← 传递 selfAID,用于队列隔离
449
+ });
450
+ }
451
+ catch (error) {
452
+ if (handoffCandidate && m.handoffDelivery && session.selfAID) {
453
+ const handoffIds = m.handoffDelivery.handoffIds?.length
454
+ ? m.handoffDelivery.handoffIds
455
+ : [m.handoffDelivery.handoffId];
456
+ for (const handoffId of handoffIds) {
457
+ this.handoffRuntime?.markBindingIncomplete(session.selfAID, handoffId);
458
+ }
459
+ }
460
+ throw error;
461
+ }
462
+ };
463
+ if (handoffCandidate) {
464
+ await doEnqueue(fullMessage);
465
+ }
466
+ else if (isInterrupt) {
467
+ const debouncer = this.getDebouncer(channelName, effectiveChannelType);
468
+ if (debouncer.enabled) {
469
+ const debounceKey = msg.peerId ? `${session.id}:${msg.peerId}` : session.id;
470
+ await debouncer.submit(debounceKey, fullMessage, doEnqueue);
471
+ }
472
+ else {
473
+ await doEnqueue(fullMessage);
474
+ }
475
+ }
476
+ else {
477
+ // 群聊 FIFO:直接入队,由 MessageQueue.processNext 出队时合并
478
+ await doEnqueue(fullMessage);
479
+ }
480
+ }
481
+ catch (error) {
482
+ logger.error(`[MessageBridge] Error in onMessage handler for ${channelName}:`, error);
483
+ }
484
+ });
485
+ }
486
+ // ── Menu Protocol ──
487
+ static MENU_NAME_MAP = {
488
+ pwd: '/pwd',
489
+ session: '/session',
490
+ topic: '/topic',
491
+ baseagent: '/baseagent',
492
+ model: '/model',
493
+ effort: '/effort',
494
+ chatmode: '/chatmode',
495
+ mentionmode: '/mentionmode',
496
+ permission: '/perm',
497
+ activity: '/activity',
498
+ observable: '/observable',
499
+ system: '/system',
500
+ cli: '/cli',
501
+ agent: '/agent',
502
+ trigger: '/trigger',
503
+ file: '/file',
504
+ capability: '/capability',
505
+ role: '/role',
506
+ };
507
+ extractTopicName(msg) {
508
+ const raw = msg.topicName
509
+ ?? msg.replyContext?.title
510
+ ?? msg.replyContext?.metadata?.topicName
511
+ ?? msg.replyContext?.metadata?.title;
512
+ const name = typeof raw === 'string' ? raw.trim() : '';
513
+ return name || undefined;
514
+ }
515
+ async canCreateThreadSession(channel, msg, chatType) {
516
+ if (chatType !== 'group' || !msg.threadId)
517
+ return true;
518
+ const existing = await this.sessionManager.getThreadSession(channel, msg.channelId, msg.threadId);
519
+ if (existing)
520
+ return true;
521
+ // 群话题创建权限只看「发送者在该群里的角色」(AUN 经 group.get_admins 实时查询,权威源)。
522
+ // 仅群 owner/admin 可建话题;member / 非成员 / 查询失败(undefined)一律 fail-closed 拒绝。
523
+ // 这与 bot 的 owner/admin 无关——不引入 resolveIdentity 兜底。
524
+ // 不暴露群角色的渠道(adapter 无 getGroupMemberRole)不受此守卫约束,放行。
525
+ const adapter = this.processor?.getChannelInfo?.(channel)?.adapter;
526
+ if (adapter?.getGroupMemberRole) {
527
+ const groupRole = await adapter.getGroupMemberRole(msg.channelId, msg.peerId);
528
+ return groupRole === 'owner' || groupRole === 'admin';
529
+ }
530
+ return true;
531
+ }
532
+ resolveCmd(name, cmd) {
533
+ if (cmd)
534
+ return cmd;
535
+ const mapped = MessageBridge.MENU_NAME_MAP[name];
536
+ if (!mapped)
537
+ throw { code: 'NOT_SUPPORTED', message: `Unsupported menu name: ${name}` };
538
+ return mapped;
539
+ }
540
+ async handleMenuControl(parsed, channel, channelType, msg, adapter, authSubject) {
541
+ if (!hasValidMenuId(parsed))
542
+ return;
543
+ const header = { id: parsed.id, ...(parsed.name?.trim() ? { name: parsed.name } : {}) };
544
+ const access = authorizeAccess(authSubject);
545
+ if (!access.allow) {
546
+ this.logMenuDiagnostic('access-denied', channel, msg, parsed, access.code);
547
+ await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
548
+ code: 'ROLE_ACCESS_DENIED',
549
+ message: access.reason,
550
+ }));
551
+ return;
552
+ }
553
+ const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
554
+ const dedupKey = [msg.selfAID || channel, channelType, msg.chatType || 'private', scope, msg.peerId, msg.threadId || '', parsed.id].join('\u001f');
555
+ const deduped = await this.menuDeduper.execute(dedupKey, menuPayloadFingerprint(parsed.raw), () => this.dispatchMenuRequest(parsed.request, header, channel, msg, authSubject));
556
+ if ('conflict' in deduped) {
557
+ this.logMenuDiagnostic('request-conflict', channel, msg, parsed, 'CONFLICT');
558
+ await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
559
+ code: 'CONFLICT',
560
+ message: 'Request ID was already used with a different payload',
561
+ }));
562
+ return;
563
+ }
564
+ if (deduped.replayed)
565
+ this.logMenuDiagnostic('request-replay', channel, msg, parsed);
566
+ await this.sendMenuResponse(adapter, channel, msg, deduped.value);
567
+ }
568
+ async dispatchMenuRequest(request, header, channel, msg, authSubject) {
569
+ try {
570
+ const identity = authSubject.identity;
571
+ const name = header.name ?? '';
572
+ const cmd = typeof request.cmd === 'string' ? request.cmd : undefined;
573
+ const args = request.args;
574
+ switch (request.type) {
575
+ case 'menu.list':
576
+ return menuSuccess(header, this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private', msg.isControlChannel ? 'control' : 'agent'));
577
+ case 'menu.query': {
578
+ const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, msg.channelId, msg.peerId, args, msg.chatType, msg.isControlChannel ?? false, identity, authSubject);
579
+ if ('error' in result)
580
+ throw result;
581
+ return menuSuccess(header, result.data);
582
+ }
583
+ case 'menu.options': {
584
+ let optionArgs = args;
585
+ if ((name === 'topic' || cmd === '/topic') && args?.mode === 'fork-turns' && msg.threadId && !args?.sourceThreadId) {
586
+ optionArgs = { ...(args ?? {}), sourceThreadId: msg.threadId };
587
+ }
588
+ const data = await this.cmdHandler.getSubMenuItems(this.resolveCmd(name, cmd), channel, msg.channelId, msg.peerId, optionArgs, identity, msg.chatType, msg.isControlChannel ?? false, authSubject) ?? [];
589
+ return menuSuccess(header, data);
590
+ }
591
+ case 'menu.update': {
592
+ const result = await this.cmdHandler.execMenuUpdate(this.resolveCmd(name, cmd), request.value, channel, msg.channelId, msg.peerId, identity, msg.isControlChannel ?? false, args, authSubject);
593
+ if ('error' in result)
594
+ throw result;
595
+ return menuSuccess(header, result.data);
596
+ }
597
+ case 'menu.action': {
598
+ let actionArgs = args;
599
+ if ((name === 'topic' || cmd === '/topic') && request.action === 'fork' && msg.threadId) {
600
+ if (typeof args?.targetThreadId === 'string' && args.targetThreadId !== msg.threadId) {
601
+ return menuFailure(header, {
602
+ code: 'INVALID_ARGUMENT',
603
+ message: 'args.targetThreadId 必须与消息 thread_id 一致',
604
+ });
605
+ }
606
+ actionArgs = { ...(args ?? {}), targetThreadId: msg.threadId };
607
+ }
608
+ const result = await this.cmdHandler.execMenuAction(this.resolveCmd(name, cmd), request.action, actionArgs, channel, msg.channelId, msg.peerId, identity, msg.chatType, header.id, msg.isControlChannel ?? false, authSubject);
609
+ if ('error' in result)
610
+ throw result;
611
+ return menuSuccess(header, result.data);
612
+ }
613
+ default:
614
+ return menuFailure(header, {
615
+ code: 'METHOD_NOT_FOUND',
616
+ message: `Unknown menu request type: ${String(request.type)}`,
617
+ });
618
+ }
619
+ }
620
+ catch (error) {
621
+ return menuFailure(header, normalizeMenuError(error));
622
+ }
623
+ }
624
+ async sendMenuResponse(adapter, channel, msg, response) {
625
+ if (!adapter?.send) {
626
+ this.logMenuDiagnostic('transport-unavailable', channel, msg, {
627
+ isMenu: true,
628
+ raw: '',
629
+ request: {},
630
+ type: 'menu.response',
631
+ id: response.id,
632
+ name: response.name,
633
+ }, 'TEMPORARILY_UNAVAILABLE');
634
+ return;
635
+ }
636
+ const agentName = this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>';
637
+ const envelope = buildEnvelope({
638
+ taskId: `menu-${randomBytes(4).toString('hex')}`,
639
+ channel,
640
+ channelId: msg.channelId,
641
+ agentName,
642
+ replyContext: msg.replyContext,
643
+ });
644
+ await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: response });
645
+ }
646
+ logMenuInbound(channel, msg, parsed) {
647
+ logger.channelIn({
648
+ channel,
649
+ channelId: this.shortHash(msg.channelId),
650
+ peerId: this.shortHash(msg.peerId),
651
+ chatType: msg.chatType,
652
+ msgId: msg.messageId,
653
+ control: {
654
+ id: parsed.id,
655
+ type: parsed.type,
656
+ name: parsed.name,
657
+ action: parsed.action,
658
+ },
659
+ });
660
+ }
661
+ logMenuDiagnostic(category, channel, msg, parsed, code) {
662
+ const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
663
+ const key = `${category}:${channel}:${this.shortHash(scope)}`;
664
+ const decision = this.menuDiagnostics.check(key);
665
+ if (!decision.log)
666
+ return;
667
+ logger.warn(`[MenuControl] category=${category} request=${parsed.id || '<missing>'}`
668
+ + ` type=${parsed.type} name=${parsed.name || '<none>'} action=${parsed.action || '<none>'}`
669
+ + ` transport=${channel} scope=${this.shortHash(scope)} sender=${this.shortHash(msg.peerId)}`
670
+ + `${code ? ` code=${code}` : ''}${decision.suppressed ? ` suppressed=${decision.suppressed}` : ''}`);
671
+ }
672
+ shortHash(value) {
673
+ if (!value)
674
+ return 'none';
675
+ return createHash('sha256').update(value).digest('hex').slice(0, 12);
676
+ }
677
+ async sendAccessDenied(adapter, channel, msg, sendReply) {
678
+ const text = 'Access denied: this role is not allowed to access this agent.';
679
+ if (adapter?.send) {
680
+ const envelope = buildEnvelope({
681
+ taskId: `access-denied-${randomBytes(4).toString('hex')}`,
682
+ channel,
683
+ channelId: msg.channelId,
684
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
685
+ chatmode: 'interactive',
686
+ replyContext: msg.replyContext,
687
+ });
688
+ await adapter.send(envelope, {
689
+ kind: 'system.error',
690
+ text,
691
+ subtype: 'access_denied',
692
+ recoverable: false,
693
+ });
694
+ return;
695
+ }
696
+ await sendReply(msg.channelId, text, msg.replyContext);
697
+ }
698
+ resolveInboundRole(ctx) {
699
+ if (!ctx.selfAid || !ctx.actorId || !ctx.conversationId) {
700
+ return {
701
+ effectiveRole: null,
702
+ source: 'none',
703
+ isAuthenticated: false,
704
+ allowAccess: false,
705
+ roleExists: false,
706
+ actor: {
707
+ operatorId: String(ctx.actorId || '').trim(),
708
+ operatorChannelKey: ctx.channelKey,
709
+ operatorChannelType: String(ctx.channelType || '').trim().toLowerCase(),
710
+ principalStatus: 'error',
711
+ },
712
+ };
713
+ }
714
+ return resolvePeerRoleDetail({
715
+ selfAid: ctx.selfAid,
716
+ channelKey: ctx.channelKey,
717
+ channelType: ctx.channelType,
718
+ chatType: ctx.chatType,
719
+ actorId: ctx.actorId,
720
+ conversationId: ctx.conversationId,
721
+ peerType: ctx.peerType,
722
+ });
723
+ }
724
+ /** 命令快速路径:返回 true 表示已处理 */
725
+ async handleCommand(content, channel, channelId, sendReply, userId, threadId, chatType, source, replyContext, messageId, selfAID, authSubject) {
726
+ if (!this.cmdHandler.isCommand(content))
727
+ return false;
728
+ logger.info(`[${channel}] ${channelId}: ${content}${source === 'card-trigger' ? ' [card]' : ''}`);
729
+ const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject);
730
+ logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
731
+ if (cmdResult === undefined)
732
+ return false;
733
+ if (cmdResult) {
734
+ // 规范化为 OutboundPayload:string → command.result 包装;object → 透传
735
+ let payload;
736
+ if (typeof cmdResult === 'string') {
737
+ payload = { kind: 'command.result', text: cmdResult };
738
+ }
739
+ else if (typeof cmdResult === 'object' && cmdResult !== null && 'kind' in cmdResult) {
740
+ payload = cmdResult;
741
+ }
742
+ else {
743
+ // 不识别的返回值,按已处理但无回显处理
744
+ return true;
745
+ }
746
+ // 出站走 adapter.send 统一入口
747
+ const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
748
+ const envelope = buildEnvelope({
749
+ taskId: `cmd-${randomBytes(5).toString('hex')}`,
750
+ channel,
751
+ channelId,
752
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
753
+ chatmode: 'interactive',
754
+ replyContext,
755
+ });
756
+ try {
757
+ if (adapter?.send) {
758
+ await adapter.send(envelope, payload);
759
+ }
760
+ else {
761
+ // 降级路径:渠道未实现 send 时回退到原有 sendReply(仅文本)
762
+ const fallbackText = ('text' in payload && typeof payload.text === 'string') ? payload.text : '';
763
+ if (fallbackText)
764
+ await sendReply(fallbackText);
765
+ }
766
+ }
767
+ catch (error) {
768
+ logger.error(`[${channel}] Failed to send command response:`, error);
769
+ }
770
+ }
771
+ return true;
772
+ }
773
+ /**
774
+ * 撤回消息:先查 debounce 窗口,再查 message queue,最后查正在执行的任务。
775
+ * @returns true 如果找到并取消/中断
776
+ */
777
+ cancel(messageId) {
778
+ // 阶段 1: debounce 窗口(尚未入队)
779
+ for (const d of this.debouncers.values()) {
780
+ if (d.cancel(messageId))
781
+ return true;
782
+ }
783
+ // 阶段 2: 已入队但未处理(合并后 messageId 可能是逗号分隔的多个 id)
784
+ if (this.messageQueue.cancel(messageId))
785
+ return true;
786
+ // 阶段 3: 正在执行的任务 → 触发 interrupt
787
+ return this.messageQueue.cancelActive(messageId);
788
+ }
789
+ /** 清理资源 */
790
+ dispose() {
791
+ for (const d of this.debouncers.values())
792
+ d.dispose();
793
+ this.debouncers.clear();
794
+ }
795
+ /** 注销单个渠道的 debouncer(热重载断开渠道时调用) */
796
+ removeChannel(channelName) {
797
+ const d = this.debouncers.get(channelName);
798
+ if (d) {
799
+ d.dispose();
800
+ this.debouncers.delete(channelName);
801
+ }
802
+ }
803
+ }