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.
- package/CHANGELOG.md +93 -778
- 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 +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- 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/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- 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 +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- 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 +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- 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 +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- 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 +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- 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 +285 -94
- package/dist/ipc.js +114 -8
- 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/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -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 +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- 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.2.json +43 -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.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- 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 +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- 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 +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- 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 {
|
|
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
|
|
132
|
-
|
|
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,
|
|
642
|
+
await this.sendMenuResponse(adapter, channelName, msg, menuFailure(header, versionError));
|
|
135
643
|
return;
|
|
136
644
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
channelName
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
//
|
|
433
|
-
|
|
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
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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>',
|