evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -2,17 +2,85 @@ import { createHash, randomBytes } from 'crypto';
2
2
  import { logger } from '../../utils/logger.js';
3
3
  import { StreamDebouncer } from './stream-debouncer.js';
4
4
  import { appendMessageLog, appendMessageLogStrict, buildInboundEntry, isTransientProtocolMessage } from './message-log.js';
5
- import { buildEnvelope } from './message-utils.js';
5
+ import { buildEnvelope, sendInteractionPayload } from './message-utils.js';
6
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';
7
+ import { formatChannelKey, tryParseChannelKey } from '../channel-loader.js';
8
+ import { agentDir, resolvePaths } from '../../paths.js';
9
+ import { listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, } from '../../config/peer-role-resolver.js';
10
+ import { resolveContactView, resolvePeerDisplayLabel, resolvePrincipal } from '../../config/contact-book.js';
11
+ import { findContactRequestNote, expirePendingContactRequests, reviewContactRequest, submitContactRequest, } from '../../config/contact-request-service.js';
12
+ import { isValidAid } from '../../aun/aid/validation.js';
13
+ import { PeerIdentityCache } from '../relation/peer-identity.js';
14
+ import { handlePendingDingtalkContactBindMessage, registerPendingDingtalkContactBind } from '../../channels/dingtalk.js';
15
+ import { handlePendingFeishuContactBindMessage, registerPendingFeishuContactBind } from '../../channels/feishu.js';
16
+ import { handlePendingQQBotContactBindMessage, registerPendingQQBotContactBind } from '../../channels/qqbot.js';
17
+ import { handlePendingWecomContactBindMessage, registerPendingWecomContactBind } from '../../channels/wecom.js';
18
+ import { handlePendingWechatContactBindMessage, registerPendingWechatContactBind } from '../../channels/wechat.js';
11
19
  import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
12
20
  import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, } from '../command/menu-protocol.js';
21
+ import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired } from '../command/evol-menu-version-gate.js';
22
+ import { MenuTokenStore } from '../command/menu-token-store.js';
13
23
  import { SessionRenewService } from '../session/session-renew.js';
14
24
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
15
25
  import { recordCausationSpan } from '../causation/audit.js';
26
+ import { renderActionAsText } from '../interaction-router.js';
27
+ import { planApprovalRoute } from '../permission/approval-gateway.js';
28
+ import { consumeAdmissionReplyBudget, clearAdmissionReplyBudgets, evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission, } from './inbound-admission.js';
29
+ const CONTACT_REJECTION_NOTICE = '[rejection] Not accepting messages from you.';
30
+ const REJECTABLE_PEER_TYPES = new Set(['ai', 'bot', 'agent', 'service']);
31
+ const REJECTION_MAX = 3;
32
+ const REJECTION_WINDOW_MS = 60 * 60 * 1000;
33
+ const rejectionState = new Map();
34
+ const SELF_SERVICE_CONTACT_BIND_TYPES = ['feishu', 'dingtalk', 'wecom', 'qqbot', 'wechat'];
35
+ const SELF_SERVICE_CONTACT_BIND_DISPLAY = {
36
+ feishu: '飞书',
37
+ dingtalk: '钉钉',
38
+ wecom: '企微',
39
+ qqbot: 'QQ',
40
+ wechat: '微信',
41
+ };
42
+ export function shouldBlockInboundContact(input) {
43
+ if (!input.selfAid) {
44
+ return { blocked: false, status: 'unsupported-channel' };
45
+ }
46
+ if (input.chatType !== 'private')
47
+ return { blocked: false, status: 'unsupported-chat' };
48
+ const channelType = String(input.channelType || '').trim().toLowerCase();
49
+ if (channelType !== 'aun' && !input.channelKey) {
50
+ return { blocked: false, status: 'unsupported-channel' };
51
+ }
52
+ const actorId = String(input.actorId || '').trim();
53
+ const resolution = resolvePrincipal(input.selfAid, channelType, actorId, input.channelKey);
54
+ const primaryId = resolution.principalId;
55
+ if (!primaryId)
56
+ return { blocked: false, status: 'invalid-aid' };
57
+ if (resolveContactView(input.selfAid, primaryId).isOwner) {
58
+ return { blocked: false, status: 'owner-exempt', primaryId };
59
+ }
60
+ return {
61
+ blocked: resolveContactView(input.selfAid, primaryId).blocked,
62
+ status: 'resolved',
63
+ primaryId,
64
+ };
65
+ }
66
+ export function shouldSendContactRejection(peerType, selfAid, primaryId, now = Date.now()) {
67
+ if (!REJECTABLE_PEER_TYPES.has(String(peerType || '').trim().toLowerCase()))
68
+ return false;
69
+ const key = `${selfAid}\0${primaryId}`;
70
+ const state = rejectionState.get(key);
71
+ if (!state || now - state.windowStart >= REJECTION_WINDOW_MS) {
72
+ rejectionState.set(key, { count: 1, windowStart: now });
73
+ return true;
74
+ }
75
+ if (state.count >= REJECTION_MAX)
76
+ return false;
77
+ state.count += 1;
78
+ return true;
79
+ }
80
+ export function clearContactRejectionRateLimits() {
81
+ rejectionState.clear();
82
+ clearAdmissionReplyBudgets();
83
+ }
16
84
  /**
17
85
  * MessageBridge — Channel 与 Core 之间的消息桥梁
18
86
  *
@@ -34,8 +102,12 @@ export class MessageBridge {
34
102
  bootstrapService;
35
103
  menuDeduper = new MenuRequestDeduper();
36
104
  menuDiagnostics = new MenuDiagnosticLimiter();
105
+ menuTokenStore = new MenuTokenStore();
37
106
  handoffRuntime;
38
107
  sessionRenewService;
108
+ aidStatsCollector;
109
+ contactBindRuntimeChecker;
110
+ interactionRouter;
39
111
  constructor(defaultProjectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, defaultDebounce, processOwnersProvider) {
40
112
  this.defaultProjectPath = defaultProjectPath;
41
113
  this.sessionManager = sessionManager;
@@ -45,7 +117,7 @@ export class MessageBridge {
45
117
  this.eventBus = eventBus;
46
118
  this.processOwnersProvider = processOwnersProvider;
47
119
  this.defaultDebounce = defaultDebounce ?? 0;
48
- this.sessionRenewService = new SessionRenewService(sessionManager, processor);
120
+ this.sessionRenewService = new SessionRenewService(sessionManager, processor, { eventBus });
49
121
  }
50
122
  /** Inject EvolAgentRegistry so owner lookups/writes route to agent.json for agent-owned channels. */
51
123
  setAgentRegistry(registry) {
@@ -54,9 +126,388 @@ export class MessageBridge {
54
126
  setBootstrapService(service) {
55
127
  this.bootstrapService = service;
56
128
  }
129
+ /** 非 AUN 渠道的 per-AID 消息统计由本桥梁记账(AUN 在 channel 层自行记账) */
130
+ setAidStatsCollector(collector) {
131
+ this.aidStatsCollector = collector;
132
+ }
57
133
  setHandoffRuntime(runtime) {
58
134
  this.handoffRuntime = runtime;
59
135
  }
136
+ setContactBindRuntimeChecker(checker) {
137
+ this.contactBindRuntimeChecker = checker;
138
+ }
139
+ setInteractionRouter(router) {
140
+ this.interactionRouter = router;
141
+ }
142
+ /**
143
+ * AUN supplies an authenticated sender AID, while the IM-side code proves
144
+ * possession of the external account. This command only creates that
145
+ * identity mapping; it never changes a role assignment.
146
+ */
147
+ handleAunSelfServiceContactBind(input) {
148
+ const match = /^\s*\/bind(?:\s+(.+?))?\s*$/i.exec(input.content);
149
+ if (!match)
150
+ return { handled: false };
151
+ if (input.chatType !== 'private') {
152
+ return {
153
+ handled: true,
154
+ status: 'private-only',
155
+ reply: '联系人绑定只能在 AUN 私聊中发起。用法:/bind <渠道> [实例名]。',
156
+ };
157
+ }
158
+ const selfAid = String(input.selfAid || '').trim();
159
+ const actorId = String(input.actorId || '').trim();
160
+ if (!isValidAid(selfAid) || !isValidAid(actorId)) {
161
+ return {
162
+ handled: true,
163
+ status: 'invalid-aid',
164
+ reply: '无法验证当前 AUN 身份,未创建联系人绑定码。请通过已认证的 AUN 私聊重新发送请求。',
165
+ };
166
+ }
167
+ const args = String(match[1] || '').trim().split(/\s+/).filter(Boolean);
168
+ if (args.length < 1 || args.length > 2) {
169
+ return { handled: true, status: 'format', reply: this.aunSelfServiceBindUsage() };
170
+ }
171
+ const channelType = args[0].toLowerCase();
172
+ if (!SELF_SERVICE_CONTACT_BIND_TYPES.includes(channelType)) {
173
+ return { handled: true, status: 'unsupported-channel', reply: this.aunSelfServiceBindUsage() };
174
+ }
175
+ const agent = input.owningAgent ?? this.agentRegistry?.get(selfAid);
176
+ if (!agent || agent.aid !== selfAid) {
177
+ return {
178
+ handled: true,
179
+ status: 'agent-unavailable',
180
+ reply: '目标 Agent 当前不可用,未创建联系人绑定码。',
181
+ };
182
+ }
183
+ const instances = (agent.config.channels ?? []).filter((instance) => (String(instance?.type || '').trim().toLowerCase() === channelType
184
+ && instance.enabled !== false
185
+ && typeof instance.name === 'string'
186
+ && instance.name.trim().length > 0));
187
+ const requestedInstance = args[1];
188
+ let instance;
189
+ if (requestedInstance) {
190
+ instance = instances.find((candidate) => candidate.name === requestedInstance);
191
+ if (!instance) {
192
+ return {
193
+ handled: true,
194
+ status: 'unknown-instance',
195
+ reply: `未找到已启用的 ${SELF_SERVICE_CONTACT_BIND_DISPLAY[channelType]} 实例 “${requestedInstance}”。${this.aunSelfServiceBindInstances(channelType, instances)}`,
196
+ };
197
+ }
198
+ }
199
+ else if (instances.length === 1) {
200
+ instance = instances[0];
201
+ }
202
+ else if (instances.length === 0) {
203
+ return {
204
+ handled: true,
205
+ status: 'not-configured',
206
+ reply: `目标 Agent 未启用 ${SELF_SERVICE_CONTACT_BIND_DISPLAY[channelType]} 机器人,无法创建联系人绑定码。`,
207
+ };
208
+ }
209
+ else {
210
+ return {
211
+ handled: true,
212
+ status: 'instance-required',
213
+ reply: `该 Agent 有多个 ${SELF_SERVICE_CONTACT_BIND_DISPLAY[channelType]} 实例。${this.aunSelfServiceBindInstances(channelType, instances)}`,
214
+ };
215
+ }
216
+ const channelName = formatChannelKey({ type: channelType, selfAID: selfAid, name: instance.name });
217
+ const runtimeAvailable = this.contactBindRuntimeChecker
218
+ ? this.contactBindRuntimeChecker(channelName)
219
+ : !!this.processor.getChannelInfo?.(channelName);
220
+ if (!runtimeAvailable) {
221
+ return {
222
+ handled: true,
223
+ status: 'runtime-unavailable',
224
+ reply: `${SELF_SERVICE_CONTACT_BIND_DISPLAY[channelType]}实例当前未连接,未创建联系人绑定码。请等待机器人上线后重试。`,
225
+ };
226
+ }
227
+ const result = this.registerSelfServiceContactBind({
228
+ channelType,
229
+ selfAid,
230
+ channelName,
231
+ primaryId: actorId,
232
+ });
233
+ if (!result.ok) {
234
+ logger.warn(`[MessageBridge] Failed to register AUN self-service contact bind: self=${selfAid} actor=${actorId} channel=${channelName} error=${result.error}`);
235
+ return {
236
+ handled: true,
237
+ status: 'register-failed',
238
+ reply: `未能创建${SELF_SERVICE_CONTACT_BIND_DISPLAY[channelType]}身份绑定码:${result.error}。请稍后重试。`,
239
+ };
240
+ }
241
+ return {
242
+ handled: true,
243
+ status: result.replaced ? 'replaced' : 'issued',
244
+ reply: `请在 ${SELF_SERVICE_CONTACT_BIND_DISPLAY[channelType]} 机器人私聊中发送 /bind ${result.code} 完成联系人绑定。绑定 10 分钟内有效,不会修改 Owner、Admin 或关系角色配置。`,
245
+ };
246
+ }
247
+ registerSelfServiceContactBind(input) {
248
+ switch (input.channelType) {
249
+ case 'feishu': return registerPendingFeishuContactBind(input);
250
+ case 'dingtalk': return registerPendingDingtalkContactBind(input);
251
+ case 'wecom': return registerPendingWecomContactBind(input);
252
+ case 'qqbot': return registerPendingQQBotContactBind(input);
253
+ case 'wechat': return registerPendingWechatContactBind(input);
254
+ }
255
+ }
256
+ aunSelfServiceBindUsage() {
257
+ return `用法:/bind <渠道> [实例名]。支持渠道:${SELF_SERVICE_CONTACT_BIND_TYPES.join('、')}。`;
258
+ }
259
+ aunSelfServiceBindInstances(channelType, instances) {
260
+ const names = instances.map(instance => String(instance.name || '').trim()).filter(Boolean);
261
+ return names.length > 0
262
+ ? `可用实例:${names.join('、')}。请发送 /bind ${channelType} <实例名>。`
263
+ : '';
264
+ }
265
+ async handleReservedContactControl(input) {
266
+ const { context, msg, content, owningAgent, sendReply } = input;
267
+ const bindContext = {
268
+ selfAid: context.selfAid,
269
+ channelName: context.channelKey,
270
+ channelType: context.channelType,
271
+ chatType: context.chatType,
272
+ actorId: context.actorId,
273
+ content,
274
+ };
275
+ const contactBind = context.channelType === 'dingtalk'
276
+ ? handlePendingDingtalkContactBindMessage(bindContext)
277
+ : context.channelType === 'feishu'
278
+ ? handlePendingFeishuContactBindMessage(bindContext)
279
+ : context.channelType === 'wecom'
280
+ ? handlePendingWecomContactBindMessage(bindContext)
281
+ : context.channelType === 'qqbot'
282
+ ? handlePendingQQBotContactBindMessage(bindContext)
283
+ : context.channelType === 'wechat'
284
+ ? handlePendingWechatContactBindMessage(bindContext)
285
+ : { handled: false };
286
+ if (contactBind.handled) {
287
+ logger.info(`[MessageBridge] ${context.channelType} contact bind handled before admission: channel=${context.channelKey} actor=${context.actorId || '<none>'} status=${contactBind.status}`);
288
+ if (contactBind.reply) {
289
+ await this.sendAdmissionControlReply(context, msg, sendReply, contactBind.reply);
290
+ }
291
+ return true;
292
+ }
293
+ if (context.channelType === 'aun') {
294
+ const aunSelfServiceBind = this.handleAunSelfServiceContactBind({
295
+ selfAid: context.selfAid,
296
+ actorId: context.actorId,
297
+ chatType: context.chatType,
298
+ content,
299
+ owningAgent,
300
+ });
301
+ if (aunSelfServiceBind.handled) {
302
+ logger.info(`[MessageBridge] AUN self-service contact bind handled before admission: channel=${context.channelKey} actor=${context.actorId || '<none>'} status=${aunSelfServiceBind.status}`);
303
+ if (aunSelfServiceBind.reply) {
304
+ await this.sendAdmissionControlReply(context, msg, sendReply, aunSelfServiceBind.reply);
305
+ }
306
+ return true;
307
+ }
308
+ }
309
+ const requestMatch = /^\s*\/request(?:\s+([\s\S]*?))?\s*$/i.exec(content);
310
+ if (!requestMatch)
311
+ return false;
312
+ if (context.channelType !== 'aun') {
313
+ await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
314
+ return true;
315
+ }
316
+ if (context.chatType !== 'private') {
317
+ await this.sendAdmissionControlReply(context, msg, sendReply, '联系人申请仅支持 AUN 私聊。');
318
+ return true;
319
+ }
320
+ if (!context.primaryId) {
321
+ await this.sendAdmissionControlReply(context, msg, sendReply, '无法验证当前 AUN 身份,未提交联系人申请。');
322
+ return true;
323
+ }
324
+ let result;
325
+ try {
326
+ result = await submitContactRequest({
327
+ selfAid: context.selfAid,
328
+ applicantAid: context.primaryId,
329
+ note: requestMatch[1],
330
+ sourceChannelKey: context.channelKey,
331
+ });
332
+ }
333
+ catch (error) {
334
+ logger.warn(`[MessageBridge] Contact request submission failed: self=${context.selfAid} applicant=${context.primaryId} error=${error instanceof Error ? error.message : String(error)}`);
335
+ await this.sendAdmissionControlReply(context, msg, sendReply, '当前无法受理联系人申请,请稍后再试。');
336
+ return true;
337
+ }
338
+ if ((result.code === 'submitted' || result.code === 'resubmitted') && result.requestId) {
339
+ void this.deliverContactReviewCard(context.selfAid, context.primaryId, result, owningAgent)
340
+ .catch(error => logger.warn(`[MessageBridge] Contact review delivery failed: self=${context.selfAid} applicant=${context.primaryId} error=${error instanceof Error ? error.message : String(error)}`));
341
+ }
342
+ await this.sendAdmissionControlReply(context, msg, sendReply, this.contactRequestReply(result));
343
+ return true;
344
+ }
345
+ async sendAdmissionControlReply(context, msg, sendReply, text) {
346
+ if (!text || !consumeAdmissionReplyBudget(context))
347
+ return false;
348
+ await sendReply(msg.channelId, text, msg.replyContext);
349
+ return true;
350
+ }
351
+ admissionDeniedNotice(context, channelType) {
352
+ if (context.contactStatus === 'pending') {
353
+ return '联系人申请审核中;可再次发送 /request 更新说明。';
354
+ }
355
+ if (context.contactSource === 'contact' && context.contactStatus === 'active') {
356
+ return '当前不接受任何消息。';
357
+ }
358
+ if (channelType === 'aun') {
359
+ return '当前不接受陌生人消息。可发送 /request <申请说明> 申请成为联系人。';
360
+ }
361
+ return '当前不接受陌生人消息。联系人申请仅支持 AUN 私聊。';
362
+ }
363
+ contactRequestReply(result) {
364
+ switch (result.code) {
365
+ case 'submitted': return '联系人申请已提交,等待管理员审核。';
366
+ case 'resubmitted': return '联系人申请已更新,等待管理员审核。';
367
+ case 'already-contact': return '你已经是联系人。';
368
+ case 'owner': return '你已经是 Owner。';
369
+ case 'blocked': return CONTACT_REJECTION_NOTICE;
370
+ case 'rate-limited': return '联系人申请过于频繁,请稍后再试。';
371
+ case 'capacity': return '当前申请较多,请稍后再试。';
372
+ case 'no-owner': return '当前无法受理联系人申请。';
373
+ case 'invalid-applicant': return '无法验证当前 AUN 身份,未提交联系人申请。';
374
+ case 'invalid-note': return '申请说明不能超过 500 个字符。';
375
+ }
376
+ }
377
+ async deliverContactReviewCard(selfAid, applicantAid, submission, owningAgent) {
378
+ if (!this.interactionRouter || !submission.requestId || !submission.expiresAt || !submission.submittedAt) {
379
+ return false;
380
+ }
381
+ const ownerAdapterKey = formatChannelKey({ type: 'aun', selfAID: selfAid, name: 'main' });
382
+ const ownerAdapter = this.processor.getChannelInfo?.(ownerAdapterKey)?.adapter;
383
+ const resolveApproverCandidates = () => ({
384
+ owners: listStaticAgentOwners(selfAid, { fresh: true }),
385
+ admins: listStaticAgentAdmins(selfAid, { fresh: true }),
386
+ });
387
+ const { owners, admins } = resolveApproverCandidates();
388
+ const route = planApprovalRoute({
389
+ id: submission.requestId,
390
+ sessionId: `contact-request:${selfAid}:${submission.requestId}`,
391
+ toolName: 'contact.request.review',
392
+ toolInput: { applicantAid, requestId: submission.requestId },
393
+ summary: `Review contact request from ${applicantAid}`,
394
+ grantable: true,
395
+ approverPolicy: 'agent_manager',
396
+ createdAt: Date.parse(submission.submittedAt),
397
+ }, {
398
+ userId: applicantAid,
399
+ chatType: 'private',
400
+ selfAid,
401
+ approvalRouting: {
402
+ approverPolicy: 'agent_manager',
403
+ owners,
404
+ admins,
405
+ resolveApproverCandidates,
406
+ ownerAdapter,
407
+ forceHandoff: true,
408
+ selfAid,
409
+ originSessionId: `contact-request:${selfAid}:${submission.requestId}`,
410
+ originChannel: 'aun',
411
+ originChannelId: applicantAid,
412
+ originPeerId: applicantAid,
413
+ originPrincipalId: applicantAid,
414
+ },
415
+ });
416
+ if (route.kind !== 'handoff') {
417
+ logger.warn(`[MessageBridge] Contact review route unavailable: self=${selfAid} request=${submission.requestId} reason=${route.kind === 'unavailable' ? route.reason : 'local-route-not-allowed'}`);
418
+ return false;
419
+ }
420
+ let note = '申请说明不可用';
421
+ try {
422
+ note = findContactRequestNote(selfAid, submission.requestId) || '(未填写)';
423
+ }
424
+ catch { }
425
+ const expiresAtMs = Date.parse(submission.expiresAt);
426
+ const sessionId = `contact-request:${selfAid}:${submission.requestId}`;
427
+ const interaction = {
428
+ type: 'interaction',
429
+ id: submission.requestId,
430
+ channelId: route.approverOperatorId,
431
+ sessionId,
432
+ initiatorId: route.approverOperatorId,
433
+ expiresAt: expiresAtMs,
434
+ kind: {
435
+ kind: 'action',
436
+ title: '联系人申请审核',
437
+ bodyFormat: 'plain',
438
+ body: `申请人:${applicantAid}\n申请说明:${note}\n提交时间:${submission.submittedAt}\n截止时间:${submission.expiresAt}\n\n此卡片只处理当前这一次联系人申请。申请人重新提交、已被他人处理或审批超时,均不会修改当前联系人状态。卡片因重启或投递失效时,可在联系人列表处理仍未到期的 pending;已过期申请需由申请人重新发送 /request。`,
439
+ buttons: [
440
+ { key: 'approve', label: '通过', style: 'primary' },
441
+ { key: 'reject', label: '拒绝', style: 'default' },
442
+ { key: 'block', label: '拉黑', style: 'danger', confirm: { title: '拉黑申请人', body: `确定拉黑 ${applicantAid}?` } },
443
+ ],
444
+ },
445
+ };
446
+ this.interactionRouter.register(submission.requestId, sessionId, async (action, _values, operatorId) => {
447
+ if (action !== 'approve' && action !== 'reject' && action !== 'block')
448
+ return false;
449
+ const reviewed = await reviewContactRequest({
450
+ selfAid,
451
+ primaryId: applicantAid,
452
+ requestId: submission.requestId,
453
+ expectedContactRevision: submission.contactRevision,
454
+ approverId: route.approverOperatorId,
455
+ actorId: String(operatorId || ''),
456
+ decision: action,
457
+ });
458
+ try {
459
+ await this.sendContactReviewOutcome(route.adapter, route.approverOperatorId, applicantAid, reviewed.code, owningAgent?.name);
460
+ }
461
+ catch (error) {
462
+ logger.warn(`[MessageBridge] Contact review outcome notification failed: request=${submission.requestId} error=${error instanceof Error ? error.message : String(error)}`);
463
+ }
464
+ if (reviewed.code === 'approved' || reviewed.code === 'rejected' || reviewed.code === 'blocked') {
465
+ try {
466
+ await this.notifyContactApplicant(route.adapter, applicantAid, reviewed.code, owningAgent?.name);
467
+ }
468
+ catch (error) {
469
+ logger.warn(`[MessageBridge] Contact applicant notification failed: request=${submission.requestId} error=${error instanceof Error ? error.message : String(error)}`);
470
+ }
471
+ return true;
472
+ }
473
+ return false;
474
+ }, {
475
+ timeoutMs: Math.max(1, expiresAtMs - Date.now()),
476
+ initiatorId: route.approverOperatorId,
477
+ initiatorChannelKey: route.approverChannelKey,
478
+ onTimeout: () => { void expirePendingContactRequests(selfAid); },
479
+ onCancel: reason => route.adapter.invalidateInteraction?.(submission.requestId, reason),
480
+ });
481
+ const envelope = buildEnvelope({
482
+ taskId: `contact-request-${submission.requestId}`,
483
+ sessionId,
484
+ channel: route.adapter.channelName,
485
+ channelId: route.approverOperatorId,
486
+ agentName: owningAgent?.name ?? selfAid,
487
+ chatmode: 'interactive',
488
+ replyContext: { metadata: { source: 'handoff', chatmode: 'interactive' } },
489
+ });
490
+ const sent = await sendInteractionPayload(route.adapter, envelope, interaction, renderActionAsText(interaction), envelope.replyContext);
491
+ if (!sent)
492
+ await this.interactionRouter.cancel(submission.requestId, 'delivery_failed');
493
+ return !!sent;
494
+ }
495
+ async sendContactReviewOutcome(adapter, ownerAid, applicantAid, code, agentName) {
496
+ const text = code === 'approved' ? `已通过 ${applicantAid} 的联系人申请。`
497
+ : code === 'rejected' ? `已拒绝 ${applicantAid} 的联系人申请。`
498
+ : code === 'blocked' ? `已拉黑 ${applicantAid}。`
499
+ : code === 'request-expired' ? '审核已过期。'
500
+ : code === 'request-stale' ? '申请已更新或已被处理。'
501
+ : code === 'revision-conflict' ? '联系人列表已变化,请从联系人列表重新审核。'
502
+ : '当前无审核权限。';
503
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: ownerAid, agentName: agentName ?? '<unknown>' }), { kind: 'system.notice', text, subtype: 'contact-review' });
504
+ }
505
+ async notifyContactApplicant(adapter, applicantAid, code, agentName) {
506
+ const text = code === 'approved' ? '你的联系人申请已通过。'
507
+ : code === 'rejected' ? '你的联系人申请未通过。'
508
+ : '你的联系人申请未通过。';
509
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: applicantAid, agentName: agentName ?? '<unknown>' }), { kind: 'system.notice', text, subtype: 'contact-request-result' });
510
+ }
60
511
  getDebouncer(channelName, channelType) {
61
512
  let d = this.debouncers.get(channelName);
62
513
  if (!d) {
@@ -119,6 +570,60 @@ export class MessageBridge {
119
570
  const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
120
571
  const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
121
572
  const resolvedChannelType = msg.channelType || parsedChannelKey?.type || effectiveChannelType;
573
+ const trustedHandoffEcho = msg.source === 'handoff'
574
+ && resolvedChannelType === 'aun'
575
+ && !!selfAid
576
+ && actorId === selfAid;
577
+ const admissionPreflight = evaluateInboundAdmissionPreflight({
578
+ selfAid,
579
+ channelKey,
580
+ channelType: resolvedChannelType,
581
+ chatType,
582
+ actorId,
583
+ trustedHandoffEcho,
584
+ });
585
+ if (!admissionPreflight.allow) {
586
+ const requestCommand = /^\s*\/request(?:\s|$)/i.test(content);
587
+ if (admissionPreflight.reason === 'blocked' && admissionPreflight.context.primaryId) {
588
+ const primaryId = admissionPreflight.context.primaryId;
589
+ let peerType = msg.peerType;
590
+ if (chatType === 'private' && !peerType) {
591
+ peerType = PeerIdentityCache.get('aun', primaryId, agentDir(selfAid))?.type;
592
+ }
593
+ if (chatType === 'private'
594
+ && REJECTABLE_PEER_TYPES.has(String(peerType || '').trim().toLowerCase())) {
595
+ await this.sendAdmissionControlReply(admissionPreflight.context, msg, sendReply, CONTACT_REJECTION_NOTICE);
596
+ }
597
+ logger.info(`[MessageBridge] Blocked canonical inbound before business routing: self=${selfAid} peer=${primaryId} type=${peerType ?? 'unknown'} channel=${channelKey}`);
598
+ }
599
+ else if (admissionPreflight.reason === 'restricted_mode_without_owner' && requestCommand) {
600
+ await this.sendAdmissionControlReply(admissionPreflight.context, msg, sendReply, '当前无法受理联系人申请。');
601
+ }
602
+ else {
603
+ logger.warn(`[MessageBridge] Admission preflight denied: self=${selfAid ?? '<none>'} actor=${actorId ?? '<none>'} channel=${channelKey} reason=${admissionPreflight.reason}`);
604
+ }
605
+ return;
606
+ }
607
+ if (!trustedHandoffEcho && await this.handleReservedContactControl({
608
+ context: admissionPreflight.context,
609
+ msg,
610
+ content,
611
+ owningAgent,
612
+ sendReply,
613
+ })) {
614
+ return;
615
+ }
616
+ const admissionDecision = evaluateOrdinaryInboundAdmission(admissionPreflight.context);
617
+ if (!admissionDecision.allow) {
618
+ if (chatType === 'private') {
619
+ const notice = this.admissionDeniedNotice(admissionDecision.context, resolvedChannelType);
620
+ if (notice) {
621
+ await this.sendAdmissionControlReply(admissionDecision.context, msg, sendReply, notice);
622
+ }
623
+ }
624
+ logger.info(`[MessageBridge] Inbound admission denied before business routing: self=${selfAid} actor=${actorId ?? '<none>'} channel=${channelKey} reason=${admissionDecision.reason}`);
625
+ return;
626
+ }
122
627
  const menuControl = resolvedChannelType === 'aun'
123
628
  ? parseMenuControl(content)
124
629
  : { isMenu: false };
@@ -128,34 +633,46 @@ export class MessageBridge {
128
633
  this.logMenuDiagnostic('missing-id', channelName, msg, menuControl);
129
634
  return;
130
635
  }
131
- const validationError = validateMenuRequest(menuControl.request);
132
- if (validationError) {
636
+ const versionError = evaluateEvolMenuVersionGate({
637
+ encrypted: msg.encrypted,
638
+ protectedHeaders: msg.protectedHeaders,
639
+ });
640
+ if (versionError) {
133
641
  const header = { id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) };
134
- await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, validationError));
642
+ await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, versionError));
135
643
  return;
136
644
  }
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);
645
+ const header = { id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) };
646
+ const isTokenRequest = menuControl.type === 'menu.token.request';
647
+ const menuTokenRequired = isAunMenuTokenRequired();
648
+ if ((menuTokenRequired || isTokenRequest) && !msg.encrypted) {
649
+ await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, {
650
+ code: 'MENU_TOKEN_ENCRYPTION_REQUIRED',
651
+ message: 'Menu 请求必须使用 E2EE',
652
+ }), true);
653
+ return;
654
+ }
655
+ if (!isTokenRequest) {
656
+ const tokenOwnerAid = this.menuTokenOwnerAid(channelName, msg);
657
+ const tokenSupplied = Object.prototype.hasOwnProperty.call(menuControl.request, 'menu_token');
658
+ const tokenValid = (menuTokenRequired || tokenSupplied)
659
+ && !!tokenOwnerAid
660
+ && this.menuTokenStore.use(tokenOwnerAid, msg.peerId, menuControl.request.menu_token);
661
+ if (menuTokenRequired && !tokenValid) {
662
+ this.logMenuDiagnostic('token-required', channelName, msg, menuControl, 'MENU_TOKEN_REQUIRED');
663
+ await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, {
664
+ code: 'MENU_TOKEN_REQUIRED',
665
+ message: 'Menu token 无效或已过期,请发送 menu.token.request 重新申请',
666
+ }));
667
+ return;
151
668
  }
669
+ }
670
+ const validationError = validateMenuRequest(menuControl.request);
671
+ if (validationError) {
672
+ await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, validationError));
152
673
  return;
153
674
  }
154
675
  }
155
- const trustedHandoffEcho = msg.source === 'handoff'
156
- && resolvedChannelType === 'aun'
157
- && !!selfAid
158
- && actorId === selfAid;
159
676
  const roleDetail = trustedHandoffEcho
160
677
  ? {
161
678
  effectiveRole: 'owner',
@@ -216,6 +733,18 @@ export class MessageBridge {
216
733
  logger.debug(`[MessageBridge] Transient protocol message ignored: channel=${channelName} type=${msg.payloadType || msg.msgType || '<unknown>'}`);
217
734
  return;
218
735
  }
736
+ // 0.5 Per-AID 消息统计(预览行/tooltip 数据源)。
737
+ // AUN 在 channel 层自行记账(早于本桥梁,且含协议细节),这里只补非 AUN 渠道,避免重复计数。
738
+ if (resolvedChannelType !== 'aun' && selfAid) {
739
+ this.aidStatsCollector?.recordInbound(selfAid, conversationId || msg.channelId, Buffer.byteLength(content, 'utf-8'), content, false, undefined, undefined, 'send', {
740
+ channelType: resolvedChannelType,
741
+ // 联系人簿的 displayName 是本端主动命名,优先于渠道昵称;都没有才退回原生 ID
742
+ peerLabel: resolvePeerDisplayLabel(selfAid, resolvedChannelType, actorId ?? '', channelKey)
743
+ || msg.peerName || actorId || msg.channelId,
744
+ // 飞书私聊的 channelId 是 oc_*、peerId 是 ou_*;缓存二者关联供所有出站类型复用。
745
+ ...(chatType === 'private' ? { routeId: msg.channelId } : {}),
746
+ });
747
+ }
219
748
  // 1. owner 绑定(按实例名绑定)
220
749
  if (adapter && actorId && roleDetail.effectiveRole === 'owner') {
221
750
  await this.bootstrapService?.tryStartBootstrap({
@@ -236,7 +765,7 @@ export class MessageBridge {
236
765
  logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
237
766
  // 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
238
767
  try {
239
- const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfAID || '');
768
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfAID || '', channelKey);
240
769
  const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
241
770
  const inboundChatmode = msg.replyContext?.metadata?.chatmode;
242
771
  const inboundReplyTo = typeof msg.replyContext?.metadata?.refMessageId === 'string'
@@ -314,6 +843,12 @@ export class MessageBridge {
314
843
  const replyToMessageId = typeof msg.replyContext?.metadata?.refMessageId === 'string'
315
844
  ? msg.replyContext.metadata.refMessageId
316
845
  : msg.replyContext?.replyToMessageId ?? null;
846
+ const restoredCausation = (msg.channelType || effectiveChannelType) === 'aun'
847
+ ? normalizeCausation(msg.causation)
848
+ : undefined;
849
+ const inboundCausation = restoredCausation
850
+ ? deriveCausation(restoredCausation)
851
+ : createRootCausation();
317
852
  const renewResult = await this.sessionRenewService.resolve({
318
853
  session,
319
854
  channelName,
@@ -327,6 +862,7 @@ export class MessageBridge {
327
862
  content,
328
863
  replyToMessageId,
329
864
  isNewSession: !hadMainSession,
865
+ causation: inboundCausation,
330
866
  });
331
867
  session = renewResult.session;
332
868
  // 4. 群聊发送者标注由消息渲染层(message-renderer)逐条承担,不再在此硬编码前缀,
@@ -354,19 +890,15 @@ export class MessageBridge {
354
890
  topicName: this.extractTopicName(msg),
355
891
  replyContext: msg.replyContext,
356
892
  source: msg.source,
893
+ boundSessionId: session.id,
894
+ resolvedIdentity: identity,
357
895
  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
- })(),
896
+ causation: inboundCausation,
369
897
  };
898
+ recordCausationSpan(inboundCausation, 'message.inbound', {
899
+ status: 'completed',
900
+ refs: { messageId: msg.messageId, sessionId: session.id },
901
+ });
370
902
  const inboundEntry = (() => {
371
903
  const chatDir = this.sessionManager.getChatDir(session);
372
904
  const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
@@ -429,8 +961,11 @@ export class MessageBridge {
429
961
  // ACK 在到达时立即做(每条独立 ACK),不等合并
430
962
  // Interrupt 模式(单聊)→ 入队前 debounce 合并
431
963
  // FIFO 模式(群聊) → 跳过 debouncer,独立入队,出队时贪心合并
432
- // 优化:队列空闲时跳过 Pin 表情,直接在开始执行时添加 CheckMark
433
- if (fullMessage.messageId && this.messageQueue.getGlobalProcessingCount() > 0) {
964
+ // 优化:当前会话队列空闲时跳过 Pin,直接在开始执行时添加 CheckMark
965
+ // 不能使用全局 processing 数,否则其他会话正在执行也会让本会话误显示“已排队”。
966
+ const currentSessionBusy = (this.messageQueue.isProcessing?.(session.id) ?? false)
967
+ || (this.messageQueue.getQueueLength?.(session.id) ?? 0) > 0;
968
+ if (fullMessage.messageId && currentSessionBusy) {
434
969
  adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
435
970
  }
436
971
  const isInterrupt = chatType !== 'group' && !handoffCandidate;
@@ -493,6 +1028,7 @@ export class MessageBridge {
493
1028
  effort: '/effort',
494
1029
  chatmode: '/chatmode',
495
1030
  mentionmode: '/mentionmode',
1031
+ group: '/group',
496
1032
  permission: '/perm',
497
1033
  activity: '/activity',
498
1034
  observable: '/observable',
@@ -503,6 +1039,7 @@ export class MessageBridge {
503
1039
  file: '/file',
504
1040
  capability: '/capability',
505
1041
  role: '/role',
1042
+ connect: '/connect',
506
1043
  };
507
1044
  extractTopicName(msg) {
508
1045
  const raw = msg.topicName
@@ -541,18 +1078,40 @@ export class MessageBridge {
541
1078
  if (!hasValidMenuId(parsed))
542
1079
  return;
543
1080
  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;
1081
+ const isTokenRequest = parsed.type === 'menu.token.request';
1082
+ if (isTokenRequest) {
1083
+ if (msg.chatType !== 'private') {
1084
+ await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
1085
+ code: 'MENU_TOKEN_REJECTED',
1086
+ message: '授权申请未通过',
1087
+ }));
1088
+ return;
1089
+ }
1090
+ if (!this.menuTokenOwnerAid(channel, msg) || !msg.peerId) {
1091
+ await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
1092
+ code: 'MENU_TOKEN_REJECTED',
1093
+ message: '授权申请未通过',
1094
+ }));
1095
+ return;
1096
+ }
1097
+ }
1098
+ if (!isTokenRequest) {
1099
+ const access = authorizeAccess(authSubject);
1100
+ if (!access.allow) {
1101
+ this.logMenuDiagnostic('access-denied', channel, msg, parsed, access.code);
1102
+ await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
1103
+ code: 'ROLE_ACCESS_DENIED',
1104
+ message: access.reason,
1105
+ }));
1106
+ return;
1107
+ }
552
1108
  }
553
1109
  const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
554
1110
  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));
1111
+ let afterResponse;
1112
+ const deduped = await this.menuDeduper.execute(dedupKey, menuPayloadFingerprint(parsed.raw), () => isTokenRequest
1113
+ ? this.issueMenuTokenResponse(header, channel, msg)
1114
+ : this.dispatchMenuRequest(parsed.request, header, channel, msg, authSubject, effect => { afterResponse = effect; }));
556
1115
  if ('conflict' in deduped) {
557
1116
  this.logMenuDiagnostic('request-conflict', channel, msg, parsed, 'CONFLICT');
558
1117
  await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
@@ -563,9 +1122,18 @@ export class MessageBridge {
563
1122
  }
564
1123
  if (deduped.replayed)
565
1124
  this.logMenuDiagnostic('request-replay', channel, msg, parsed);
566
- await this.sendMenuResponse(adapter, channel, msg, deduped.value);
1125
+ await this.sendMenuResponse(adapter, channel, msg, deduped.value, isTokenRequest);
1126
+ const deferredEffect = afterResponse;
1127
+ if (!deduped.replayed && deferredEffect) {
1128
+ try {
1129
+ await deferredEffect();
1130
+ }
1131
+ catch (error) {
1132
+ logger.warn(`[MenuControl] deferred effect failed: request=${parsed.id} error=${error instanceof Error ? error.message : String(error)}`);
1133
+ }
1134
+ }
567
1135
  }
568
- async dispatchMenuRequest(request, header, channel, msg, authSubject) {
1136
+ async dispatchMenuRequest(request, header, channel, msg, authSubject, registerAfterResponse) {
569
1137
  try {
570
1138
  const identity = authSubject.identity;
571
1139
  const name = header.name ?? '';
@@ -573,7 +1141,7 @@ export class MessageBridge {
573
1141
  const args = request.args;
574
1142
  switch (request.type) {
575
1143
  case 'menu.list':
576
- return menuSuccess(header, this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private', msg.isControlChannel ? 'control' : 'agent'));
1144
+ return menuSuccess(header, this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private', msg.isControlChannel ? 'control' : 'agent', authSubject));
577
1145
  case 'menu.query': {
578
1146
  const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, msg.channelId, msg.peerId, args, msg.chatType, msg.isControlChannel ?? false, identity, authSubject);
579
1147
  if ('error' in result)
@@ -605,7 +1173,9 @@ export class MessageBridge {
605
1173
  }
606
1174
  actionArgs = { ...(args ?? {}), targetThreadId: msg.threadId };
607
1175
  }
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);
1176
+ const resolvedCmd = this.resolveCmd(name, cmd);
1177
+ const isFileFetch = resolvedCmd.trim().split(' ')[0] === '/file' && request.action === 'fetch';
1178
+ const result = await this.cmdHandler.execMenuAction(resolvedCmd, request.action, actionArgs, channel, msg.channelId, msg.peerId, identity, msg.chatType, header.id, msg.isControlChannel ?? false, authSubject, 'menu', msg.replyContext, isFileFetch ? registerAfterResponse : undefined);
609
1179
  if ('error' in result)
610
1180
  throw result;
611
1181
  return menuSuccess(header, result.data);
@@ -621,7 +1191,7 @@ export class MessageBridge {
621
1191
  return menuFailure(header, normalizeMenuError(error));
622
1192
  }
623
1193
  }
624
- async sendMenuResponse(adapter, channel, msg, response) {
1194
+ async sendMenuResponse(adapter, channel, msg, response, forceEncryption = false) {
625
1195
  if (!adapter?.send) {
626
1196
  this.logMenuDiagnostic('transport-unavailable', channel, msg, {
627
1197
  isMenu: true,
@@ -634,15 +1204,47 @@ export class MessageBridge {
634
1204
  return;
635
1205
  }
636
1206
  const agentName = this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>';
1207
+ const replyContext = {
1208
+ ...(msg.replyContext ?? {}),
1209
+ metadata: {
1210
+ ...(msg.replyContext?.metadata ?? {}),
1211
+ ...evolMenuResponseTransportMetadata(forceEncryption ? true : msg.encrypted),
1212
+ },
1213
+ };
637
1214
  const envelope = buildEnvelope({
638
1215
  taskId: `menu-${randomBytes(4).toString('hex')}`,
639
1216
  channel,
640
1217
  channelId: msg.channelId,
641
1218
  agentName,
642
- replyContext: msg.replyContext,
1219
+ replyContext,
643
1220
  });
644
1221
  await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: response });
645
1222
  }
1223
+ menuTokenOwnerAid(channel, msg) {
1224
+ return msg.selfAID || this.agentRegistry?.resolveByChannel(channel)?.aid;
1225
+ }
1226
+ async issueMenuTokenResponse(header, channel, msg) {
1227
+ const tokenOwnerAid = this.menuTokenOwnerAid(channel, msg);
1228
+ if (!tokenOwnerAid || !msg.peerId) {
1229
+ return menuFailure(header, { code: 'MENU_TOKEN_REJECTED', message: '授权申请未通过' });
1230
+ }
1231
+ const headers = msg.protectedHeaders ?? {};
1232
+ const issued = this.menuTokenStore.issue(tokenOwnerAid, msg.peerId, {
1233
+ clientName: this.menuTokenHeaderValue(headers.client_name ?? headers.app_name),
1234
+ clientVersion: this.menuTokenHeaderValue(headers.client_version ?? headers.app_version),
1235
+ evolVersion: this.menuTokenHeaderValue(headers.evol_version),
1236
+ });
1237
+ if (!issued) {
1238
+ return menuFailure(header, { code: 'MENU_TOKEN_REJECTED', message: '授权申请未通过' });
1239
+ }
1240
+ return menuSuccess(header, { menu_token: issued.menuToken, idle_timeout: issued.idleTimeout });
1241
+ }
1242
+ menuTokenHeaderValue(value) {
1243
+ if (typeof value !== 'string')
1244
+ return undefined;
1245
+ const normalized = value.trim();
1246
+ return normalized.length > 0 && normalized.length <= 256 ? normalized : undefined;
1247
+ }
646
1248
  logMenuInbound(channel, msg, parsed) {
647
1249
  logger.channelIn({
648
1250
  channel,
@@ -726,7 +1328,7 @@ export class MessageBridge {
726
1328
  if (!this.cmdHandler.isCommand(content))
727
1329
  return false;
728
1330
  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);
1331
+ const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject, replyContext);
730
1332
  logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
731
1333
  if (cmdResult === undefined)
732
1334
  return false;
@@ -745,8 +1347,19 @@ export class MessageBridge {
745
1347
  }
746
1348
  // 出站走 adapter.send 统一入口
747
1349
  const adapter = this.processor.getChannelInfo?.(channel)?.adapter;
1350
+ let responseSessionId;
1351
+ try {
1352
+ const responseSession = threadId
1353
+ ? await this.sessionManager.getThreadSession(channel, channelId, threadId)
1354
+ : await this.sessionManager.getActiveSession(channel, channelId);
1355
+ responseSessionId = responseSession?.id;
1356
+ }
1357
+ catch (error) {
1358
+ logger.debug(`[MessageBridge] Unable to bind command response session: ${error instanceof Error ? error.message : String(error)}`);
1359
+ }
748
1360
  const envelope = buildEnvelope({
749
1361
  taskId: `cmd-${randomBytes(5).toString('hex')}`,
1362
+ sessionId: responseSessionId,
750
1363
  channel,
751
1364
  channelId,
752
1365
  agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',