evolcore 0.0.3 → 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.
- package/CHANGELOG.md +64 -0
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +7 -5
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +10 -13
- package/dist/core/command/connect-menu.js +249 -24
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +18 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-manager.js +25 -3
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +224 -89
- package/dist/ipc.js +81 -4
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +7 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +18 -7
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
|
@@ -2,40 +2,63 @@ 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';
|
|
7
|
+
import { formatChannelKey, tryParseChannelKey } from '../channel-loader.js';
|
|
8
8
|
import { agentDir, resolvePaths } from '../../paths.js';
|
|
9
|
-
import { resolvePeerRoleDetail } from '../../config/peer-role-resolver.js';
|
|
10
|
-
import {
|
|
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';
|
|
11
12
|
import { isValidAid } from '../../aun/aid/validation.js';
|
|
12
13
|
import { PeerIdentityCache } from '../relation/peer-identity.js';
|
|
13
|
-
import { handlePendingDingtalkContactBindMessage } from '../../channels/dingtalk.js';
|
|
14
|
-
import {
|
|
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';
|
|
15
19
|
import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
16
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';
|
|
17
23
|
import { SessionRenewService } from '../session/session-renew.js';
|
|
18
24
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
19
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';
|
|
20
29
|
const CONTACT_REJECTION_NOTICE = '[rejection] Not accepting messages from you.';
|
|
21
30
|
const REJECTABLE_PEER_TYPES = new Set(['ai', 'bot', 'agent', 'service']);
|
|
22
31
|
const REJECTION_MAX = 3;
|
|
23
32
|
const REJECTION_WINDOW_MS = 60 * 60 * 1000;
|
|
24
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
|
+
};
|
|
25
42
|
export function shouldBlockInboundContact(input) {
|
|
26
|
-
if (
|
|
43
|
+
if (!input.selfAid) {
|
|
27
44
|
return { blocked: false, status: 'unsupported-channel' };
|
|
28
45
|
}
|
|
29
46
|
if (input.chatType !== 'private')
|
|
30
47
|
return { blocked: false, status: 'unsupported-chat' };
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
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)
|
|
33
56
|
return { blocked: false, status: 'invalid-aid' };
|
|
34
|
-
if (
|
|
57
|
+
if (resolveContactView(input.selfAid, primaryId).isOwner) {
|
|
35
58
|
return { blocked: false, status: 'owner-exempt', primaryId };
|
|
36
59
|
}
|
|
37
60
|
return {
|
|
38
|
-
blocked:
|
|
61
|
+
blocked: resolveContactView(input.selfAid, primaryId).blocked,
|
|
39
62
|
status: 'resolved',
|
|
40
63
|
primaryId,
|
|
41
64
|
};
|
|
@@ -56,6 +79,7 @@ export function shouldSendContactRejection(peerType, selfAid, primaryId, now = D
|
|
|
56
79
|
}
|
|
57
80
|
export function clearContactRejectionRateLimits() {
|
|
58
81
|
rejectionState.clear();
|
|
82
|
+
clearAdmissionReplyBudgets();
|
|
59
83
|
}
|
|
60
84
|
/**
|
|
61
85
|
* MessageBridge — Channel 与 Core 之间的消息桥梁
|
|
@@ -78,8 +102,12 @@ export class MessageBridge {
|
|
|
78
102
|
bootstrapService;
|
|
79
103
|
menuDeduper = new MenuRequestDeduper();
|
|
80
104
|
menuDiagnostics = new MenuDiagnosticLimiter();
|
|
105
|
+
menuTokenStore = new MenuTokenStore();
|
|
81
106
|
handoffRuntime;
|
|
82
107
|
sessionRenewService;
|
|
108
|
+
aidStatsCollector;
|
|
109
|
+
contactBindRuntimeChecker;
|
|
110
|
+
interactionRouter;
|
|
83
111
|
constructor(defaultProjectPath, sessionManager, processor, messageQueue, cmdHandler, eventBus, defaultDebounce, processOwnersProvider) {
|
|
84
112
|
this.defaultProjectPath = defaultProjectPath;
|
|
85
113
|
this.sessionManager = sessionManager;
|
|
@@ -89,7 +117,7 @@ export class MessageBridge {
|
|
|
89
117
|
this.eventBus = eventBus;
|
|
90
118
|
this.processOwnersProvider = processOwnersProvider;
|
|
91
119
|
this.defaultDebounce = defaultDebounce ?? 0;
|
|
92
|
-
this.sessionRenewService = new SessionRenewService(sessionManager, processor);
|
|
120
|
+
this.sessionRenewService = new SessionRenewService(sessionManager, processor, { eventBus });
|
|
93
121
|
}
|
|
94
122
|
/** Inject EvolAgentRegistry so owner lookups/writes route to agent.json for agent-owned channels. */
|
|
95
123
|
setAgentRegistry(registry) {
|
|
@@ -98,9 +126,388 @@ export class MessageBridge {
|
|
|
98
126
|
setBootstrapService(service) {
|
|
99
127
|
this.bootstrapService = service;
|
|
100
128
|
}
|
|
129
|
+
/** 非 AUN 渠道的 per-AID 消息统计由本桥梁记账(AUN 在 channel 层自行记账) */
|
|
130
|
+
setAidStatsCollector(collector) {
|
|
131
|
+
this.aidStatsCollector = collector;
|
|
132
|
+
}
|
|
101
133
|
setHandoffRuntime(runtime) {
|
|
102
134
|
this.handoffRuntime = runtime;
|
|
103
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
|
+
}
|
|
104
511
|
getDebouncer(channelName, channelType) {
|
|
105
512
|
let d = this.debouncers.get(channelName);
|
|
106
513
|
if (!d) {
|
|
@@ -163,23 +570,58 @@ export class MessageBridge {
|
|
|
163
570
|
const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
|
|
164
571
|
const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
|
|
165
572
|
const resolvedChannelType = msg.channelType || parsedChannelKey?.type || effectiveChannelType;
|
|
166
|
-
const
|
|
573
|
+
const trustedHandoffEcho = msg.source === 'handoff'
|
|
574
|
+
&& resolvedChannelType === 'aun'
|
|
575
|
+
&& !!selfAid
|
|
576
|
+
&& actorId === selfAid;
|
|
577
|
+
const admissionPreflight = evaluateInboundAdmissionPreflight({
|
|
167
578
|
selfAid,
|
|
579
|
+
channelKey,
|
|
168
580
|
channelType: resolvedChannelType,
|
|
169
581
|
chatType,
|
|
170
582
|
actorId,
|
|
171
|
-
|
|
583
|
+
trustedHandoffEcho,
|
|
172
584
|
});
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
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, '当前无法受理联系人申请。');
|
|
177
601
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await sendReply(msg.channelId, CONTACT_REJECTION_NOTICE, msg.replyContext);
|
|
602
|
+
else {
|
|
603
|
+
logger.warn(`[MessageBridge] Admission preflight denied: self=${selfAid ?? '<none>'} actor=${actorId ?? '<none>'} channel=${channelKey} reason=${admissionPreflight.reason}`);
|
|
181
604
|
}
|
|
182
|
-
|
|
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}`);
|
|
183
625
|
return;
|
|
184
626
|
}
|
|
185
627
|
const menuControl = resolvedChannelType === 'aun'
|
|
@@ -191,39 +633,46 @@ export class MessageBridge {
|
|
|
191
633
|
this.logMenuDiagnostic('missing-id', channelName, msg, menuControl);
|
|
192
634
|
return;
|
|
193
635
|
}
|
|
194
|
-
const
|
|
195
|
-
|
|
636
|
+
const versionError = evaluateEvolMenuVersionGate({
|
|
637
|
+
encrypted: msg.encrypted,
|
|
638
|
+
protectedHeaders: msg.protectedHeaders,
|
|
639
|
+
});
|
|
640
|
+
if (versionError) {
|
|
196
641
|
const header = { id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) };
|
|
197
|
-
await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header,
|
|
642
|
+
await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, versionError));
|
|
198
643
|
return;
|
|
199
644
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
channelName
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
await
|
|
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;
|
|
219
668
|
}
|
|
669
|
+
}
|
|
670
|
+
const validationError = validateMenuRequest(menuControl.request);
|
|
671
|
+
if (validationError) {
|
|
672
|
+
await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, validationError));
|
|
220
673
|
return;
|
|
221
674
|
}
|
|
222
675
|
}
|
|
223
|
-
const trustedHandoffEcho = msg.source === 'handoff'
|
|
224
|
-
&& resolvedChannelType === 'aun'
|
|
225
|
-
&& !!selfAid
|
|
226
|
-
&& actorId === selfAid;
|
|
227
676
|
const roleDetail = trustedHandoffEcho
|
|
228
677
|
? {
|
|
229
678
|
effectiveRole: 'owner',
|
|
@@ -284,6 +733,18 @@ export class MessageBridge {
|
|
|
284
733
|
logger.debug(`[MessageBridge] Transient protocol message ignored: channel=${channelName} type=${msg.payloadType || msg.msgType || '<unknown>'}`);
|
|
285
734
|
return;
|
|
286
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
|
+
}
|
|
287
748
|
// 1. owner 绑定(按实例名绑定)
|
|
288
749
|
if (adapter && actorId && roleDetail.effectiveRole === 'owner') {
|
|
289
750
|
await this.bootstrapService?.tryStartBootstrap({
|
|
@@ -382,6 +843,12 @@ export class MessageBridge {
|
|
|
382
843
|
const replyToMessageId = typeof msg.replyContext?.metadata?.refMessageId === 'string'
|
|
383
844
|
? msg.replyContext.metadata.refMessageId
|
|
384
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();
|
|
385
852
|
const renewResult = await this.sessionRenewService.resolve({
|
|
386
853
|
session,
|
|
387
854
|
channelName,
|
|
@@ -395,6 +862,7 @@ export class MessageBridge {
|
|
|
395
862
|
content,
|
|
396
863
|
replyToMessageId,
|
|
397
864
|
isNewSession: !hadMainSession,
|
|
865
|
+
causation: inboundCausation,
|
|
398
866
|
});
|
|
399
867
|
session = renewResult.session;
|
|
400
868
|
// 4. 群聊发送者标注由消息渲染层(message-renderer)逐条承担,不再在此硬编码前缀,
|
|
@@ -422,19 +890,15 @@ export class MessageBridge {
|
|
|
422
890
|
topicName: this.extractTopicName(msg),
|
|
423
891
|
replyContext: msg.replyContext,
|
|
424
892
|
source: msg.source,
|
|
893
|
+
boundSessionId: session.id,
|
|
894
|
+
resolvedIdentity: identity,
|
|
425
895
|
dispatchMode: msg.dispatchMode,
|
|
426
|
-
causation:
|
|
427
|
-
const restored = (msg.channelType || effectiveChannelType) === 'aun'
|
|
428
|
-
? normalizeCausation(msg.causation)
|
|
429
|
-
: undefined;
|
|
430
|
-
const inbound = restored ? deriveCausation(restored) : createRootCausation();
|
|
431
|
-
recordCausationSpan(inbound, 'message.inbound', {
|
|
432
|
-
status: 'completed',
|
|
433
|
-
refs: { messageId: msg.messageId, sessionId: session.id },
|
|
434
|
-
});
|
|
435
|
-
return inbound;
|
|
436
|
-
})(),
|
|
896
|
+
causation: inboundCausation,
|
|
437
897
|
};
|
|
898
|
+
recordCausationSpan(inboundCausation, 'message.inbound', {
|
|
899
|
+
status: 'completed',
|
|
900
|
+
refs: { messageId: msg.messageId, sessionId: session.id },
|
|
901
|
+
});
|
|
438
902
|
const inboundEntry = (() => {
|
|
439
903
|
const chatDir = this.sessionManager.getChatDir(session);
|
|
440
904
|
const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
|
|
@@ -564,6 +1028,7 @@ export class MessageBridge {
|
|
|
564
1028
|
effort: '/effort',
|
|
565
1029
|
chatmode: '/chatmode',
|
|
566
1030
|
mentionmode: '/mentionmode',
|
|
1031
|
+
group: '/group',
|
|
567
1032
|
permission: '/perm',
|
|
568
1033
|
activity: '/activity',
|
|
569
1034
|
observable: '/observable',
|
|
@@ -574,6 +1039,7 @@ export class MessageBridge {
|
|
|
574
1039
|
file: '/file',
|
|
575
1040
|
capability: '/capability',
|
|
576
1041
|
role: '/role',
|
|
1042
|
+
connect: '/connect',
|
|
577
1043
|
};
|
|
578
1044
|
extractTopicName(msg) {
|
|
579
1045
|
const raw = msg.topicName
|
|
@@ -612,18 +1078,40 @@ export class MessageBridge {
|
|
|
612
1078
|
if (!hasValidMenuId(parsed))
|
|
613
1079
|
return;
|
|
614
1080
|
const header = { id: parsed.id, ...(parsed.name?.trim() ? { name: parsed.name } : {}) };
|
|
615
|
-
const
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
}
|
|
623
1108
|
}
|
|
624
1109
|
const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
|
|
625
1110
|
const dedupKey = [msg.selfAID || channel, channelType, msg.chatType || 'private', scope, msg.peerId, msg.threadId || '', parsed.id].join('\u001f');
|
|
626
|
-
|
|
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; }));
|
|
627
1115
|
if ('conflict' in deduped) {
|
|
628
1116
|
this.logMenuDiagnostic('request-conflict', channel, msg, parsed, 'CONFLICT');
|
|
629
1117
|
await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
|
|
@@ -634,9 +1122,18 @@ export class MessageBridge {
|
|
|
634
1122
|
}
|
|
635
1123
|
if (deduped.replayed)
|
|
636
1124
|
this.logMenuDiagnostic('request-replay', channel, msg, parsed);
|
|
637
|
-
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
|
+
}
|
|
638
1135
|
}
|
|
639
|
-
async dispatchMenuRequest(request, header, channel, msg, authSubject) {
|
|
1136
|
+
async dispatchMenuRequest(request, header, channel, msg, authSubject, registerAfterResponse) {
|
|
640
1137
|
try {
|
|
641
1138
|
const identity = authSubject.identity;
|
|
642
1139
|
const name = header.name ?? '';
|
|
@@ -644,7 +1141,7 @@ export class MessageBridge {
|
|
|
644
1141
|
const args = request.args;
|
|
645
1142
|
switch (request.type) {
|
|
646
1143
|
case 'menu.list':
|
|
647
|
-
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));
|
|
648
1145
|
case 'menu.query': {
|
|
649
1146
|
const result = await this.cmdHandler.execMenuQuery(this.resolveCmd(name, cmd), channel, msg.channelId, msg.peerId, args, msg.chatType, msg.isControlChannel ?? false, identity, authSubject);
|
|
650
1147
|
if ('error' in result)
|
|
@@ -676,7 +1173,9 @@ export class MessageBridge {
|
|
|
676
1173
|
}
|
|
677
1174
|
actionArgs = { ...(args ?? {}), targetThreadId: msg.threadId };
|
|
678
1175
|
}
|
|
679
|
-
const
|
|
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);
|
|
680
1179
|
if ('error' in result)
|
|
681
1180
|
throw result;
|
|
682
1181
|
return menuSuccess(header, result.data);
|
|
@@ -692,7 +1191,7 @@ export class MessageBridge {
|
|
|
692
1191
|
return menuFailure(header, normalizeMenuError(error));
|
|
693
1192
|
}
|
|
694
1193
|
}
|
|
695
|
-
async sendMenuResponse(adapter, channel, msg, response) {
|
|
1194
|
+
async sendMenuResponse(adapter, channel, msg, response, forceEncryption = false) {
|
|
696
1195
|
if (!adapter?.send) {
|
|
697
1196
|
this.logMenuDiagnostic('transport-unavailable', channel, msg, {
|
|
698
1197
|
isMenu: true,
|
|
@@ -705,15 +1204,47 @@ export class MessageBridge {
|
|
|
705
1204
|
return;
|
|
706
1205
|
}
|
|
707
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
|
+
};
|
|
708
1214
|
const envelope = buildEnvelope({
|
|
709
1215
|
taskId: `menu-${randomBytes(4).toString('hex')}`,
|
|
710
1216
|
channel,
|
|
711
1217
|
channelId: msg.channelId,
|
|
712
1218
|
agentName,
|
|
713
|
-
replyContext
|
|
1219
|
+
replyContext,
|
|
714
1220
|
});
|
|
715
1221
|
await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: response });
|
|
716
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
|
+
}
|
|
717
1248
|
logMenuInbound(channel, msg, parsed) {
|
|
718
1249
|
logger.channelIn({
|
|
719
1250
|
channel,
|
|
@@ -816,8 +1347,19 @@ export class MessageBridge {
|
|
|
816
1347
|
}
|
|
817
1348
|
// 出站走 adapter.send 统一入口
|
|
818
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
|
+
}
|
|
819
1360
|
const envelope = buildEnvelope({
|
|
820
1361
|
taskId: `cmd-${randomBytes(5).toString('hex')}`,
|
|
1362
|
+
sessionId: responseSessionId,
|
|
821
1363
|
channel,
|
|
822
1364
|
channelId,
|
|
823
1365
|
agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
|