evolcore 0.0.11 → 0.0.13
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 +44 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +86 -7
- package/dist/agents/codex-app-server-client.js +174 -20
- package/dist/agents/codex-runner.js +375 -92
- package/dist/agents/ecagent-runner.js +39 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +91 -44
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +865 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +655 -240
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +108 -31
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +70 -55
- package/dist/cli/fs-command.js +79 -10
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/init.js +77 -8
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +53 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +7 -32
- package/dist/cli/trigger-command.js +11 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +9 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/mention-mode.js +8 -24
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -2
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +31 -7
- package/dist/core/auth/operation-catalog.js +196 -25
- package/dist/core/bootstrap-messages.js +25 -0
- package/dist/core/bootstrap-service.js +64 -4
- package/dist/core/capability/providers/claude-capability-provider.js +9 -31
- package/dist/core/capability/providers/codex-capability-provider.js +4 -15
- package/dist/core/capability/skill-discovery.js +55 -0
- package/dist/core/channel-loader.js +1 -1
- package/dist/core/command/cli-intent-parser.js +9 -0
- package/dist/core/command/command-handler.js +650 -42
- package/dist/core/command/connect-menu.js +15 -0
- package/dist/core/command/group-menu.js +4 -4
- package/dist/core/command/menu-catalog.js +370 -0
- package/dist/core/command/menu-handler.js +230 -51
- package/dist/core/command/menu-protocol.js +94 -2
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +99 -31
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +157 -22
- package/dist/core/handoff/store.js +29 -1
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/logical-queue-bridge.js +1 -0
- package/dist/core/message/mention-schema.js +126 -0
- package/dist/core/message/message-bridge.js +298 -88
- package/dist/core/message/message-log.js +3 -0
- package/dist/core/message/message-queue.js +268 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +161 -77
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +243 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +43 -1
- package/dist/core/permission/tool-policy.js +322 -57
- package/dist/core/protected-paths.js +114 -4
- package/dist/core/session/session-manager.js +42 -4
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +541 -180
- package/dist/ipc.js +232 -10
- package/dist/paths.js +3 -0
- package/dist/response-system/coordinator.js +6 -4
- package/dist/response-system/engines/v1/proactive-flow.js +26 -8
- package/dist/response-system/modes/single-session/index.js +14 -1
- package/dist/response-system/selector.js +6 -5
- package/dist/trigger/anomaly-store.js +4 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/script-executor.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/codex-app-server-registry.js +90 -0
- package/dist/utils/codex-cli.js +5 -1
- package/dist/utils/cross-platform.js +15 -0
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/docs/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +34 -1
- package/kits/templates/roles/visitor.json +35 -3
- package/kits/templates/system-fragments/bootstrap.md +1 -1
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +2 -2
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/command/evol-menu-version-gate.js +0 -39
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
package/dist/channels/aun.js
CHANGED
|
@@ -7,7 +7,7 @@ import { logger, localTimestamp } from '../utils/logger.js';
|
|
|
7
7
|
import { LogWriter } from '../utils/log-writer.js';
|
|
8
8
|
import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
|
|
9
9
|
import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
|
|
10
|
-
import { resolvePaths, agentDir as agentDirPath, resolveRoot } from '../paths.js';
|
|
10
|
+
import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
|
|
11
11
|
import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
|
|
12
12
|
import { appendAidEvent } from '../utils/instance-registry.js';
|
|
13
13
|
import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
|
|
@@ -21,7 +21,7 @@ import { loadAgent } from '../config-store.js';
|
|
|
21
21
|
import { normalizeAgentLifecycle } from '../config/lifecycle.js';
|
|
22
22
|
import { resolveEffective } from '../config/config-manager.js';
|
|
23
23
|
import { isManagementRole } from '../config/builtin-roles.js';
|
|
24
|
-
import {
|
|
24
|
+
import { normalizeMentionMode } from '../config/mention-mode.js';
|
|
25
25
|
import { getProcessStartTime } from '../utils/process-introspect.js';
|
|
26
26
|
import * as outbox from '../aun/outbox.js';
|
|
27
27
|
import { guessMime, formatSize } from '../utils/media-cache.js';
|
|
@@ -31,13 +31,36 @@ import { isHClassPath } from '../core/protected-paths.js';
|
|
|
31
31
|
import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
|
|
32
32
|
import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
|
|
33
33
|
import { recordCausationSpan } from '../core/causation/audit.js';
|
|
34
|
-
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
35
34
|
import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
36
35
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
37
|
-
import { postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox } from '../core/bootstrap-messages.js';
|
|
36
|
+
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
37
|
+
import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
|
|
38
|
+
import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
|
|
39
|
+
import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
38
40
|
export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
|
|
39
41
|
const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
40
43
|
const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
|
|
44
|
+
function requireEnvelopeDelivery(channelId, envelopeDelivery, nestedDelivery) {
|
|
45
|
+
if (!isDeliveryTarget(envelopeDelivery)) {
|
|
46
|
+
throw Object.assign(new Error(`AUN outbound envelope requires a top-level delivery route for channelId=${channelId}`), {
|
|
47
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (nestedDelivery !== undefined) {
|
|
51
|
+
if (!isDeliveryTarget(nestedDelivery) || !sameDeliveryTarget(envelopeDelivery, nestedDelivery)) {
|
|
52
|
+
throw Object.assign(new Error(`AUN outbound envelope has conflicting delivery routes for channelId=${channelId}`), {
|
|
53
|
+
code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (envelopeDelivery.chatType === 'group' && envelopeDelivery.groupId !== channelId) {
|
|
58
|
+
throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
|
|
59
|
+
code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return envelopeDelivery;
|
|
63
|
+
}
|
|
41
64
|
function normalizeImageMimeType(mimeType) {
|
|
42
65
|
const normalized = mimeType?.split(';', 1)[0].trim().toLowerCase();
|
|
43
66
|
if (!normalized)
|
|
@@ -131,6 +154,8 @@ export function aunOptsToInbound(opts, channel, channelType) {
|
|
|
131
154
|
encrypted: opts.encrypted,
|
|
132
155
|
protectedHeaders: opts.protectedHeaders,
|
|
133
156
|
messageId: opts.messageId,
|
|
157
|
+
receivedAt: opts.receivedAt,
|
|
158
|
+
gatewaySeq: opts.gatewaySeq,
|
|
134
159
|
causation: opts.causation,
|
|
135
160
|
mentions: opts.mentions,
|
|
136
161
|
mentionAids: opts.mentionAids,
|
|
@@ -139,7 +164,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
|
|
|
139
164
|
replyContext: opts.replyContext,
|
|
140
165
|
source: opts.source,
|
|
141
166
|
images: opts.images,
|
|
142
|
-
|
|
167
|
+
mentionMode: opts.mentionMode,
|
|
143
168
|
};
|
|
144
169
|
}
|
|
145
170
|
function verifiedProtectedHeaders(msg, env) {
|
|
@@ -287,14 +312,36 @@ export class AUNChannel {
|
|
|
287
312
|
throw e;
|
|
288
313
|
}
|
|
289
314
|
}
|
|
290
|
-
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
|
|
315
|
+
/** Resolve the authenticated transport route for an ordinary outbound send.
|
|
316
|
+
* The channel id is an address only; it is intentionally never used to infer
|
|
317
|
+
* whether the address is a group.
|
|
294
318
|
*/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
319
|
+
requireDelivery(channelId, context, explicit) {
|
|
320
|
+
const supplied = explicit ?? context?.delivery;
|
|
321
|
+
if (supplied !== undefined && !isDeliveryTarget(supplied)) {
|
|
322
|
+
throw Object.assign(new Error(`AUN outbound send has an invalid delivery route for channelId=${channelId}`), {
|
|
323
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (!supplied) {
|
|
327
|
+
throw Object.assign(new Error(`AUN outbound send requires an explicit delivery route for channelId=${channelId}`), {
|
|
328
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (supplied.chatType === 'group' && supplied.groupId !== channelId) {
|
|
332
|
+
throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
|
|
333
|
+
code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return supplied;
|
|
337
|
+
}
|
|
338
|
+
requirePersistedDelivery(channelId, delivery) {
|
|
339
|
+
// Persisted sends only trust the top-level route. In particular, old
|
|
340
|
+
// outbox entries cannot recover routing from their nested reply context.
|
|
341
|
+
return this.requireDelivery(channelId, undefined, delivery);
|
|
342
|
+
}
|
|
343
|
+
withDelivery(context, delivery) {
|
|
344
|
+
return { ...(context ?? {}), delivery };
|
|
298
345
|
}
|
|
299
346
|
getShortAid(aid) {
|
|
300
347
|
if (!aid)
|
|
@@ -316,15 +363,19 @@ export class AUNChannel {
|
|
|
316
363
|
const name = cached?.name;
|
|
317
364
|
return name && name !== short ? `${short}(${name})` : short;
|
|
318
365
|
}
|
|
319
|
-
notifyCardActionFailure(channelId, text, operatorId, threadId) {
|
|
366
|
+
notifyCardActionFailure(channelId, text, operatorId, threadId, delivery) {
|
|
320
367
|
if (!channelId) {
|
|
321
368
|
logger.warn(`${this.logPrefix()} Card action failure without channelId: ${text}`);
|
|
322
369
|
return;
|
|
323
370
|
}
|
|
324
|
-
|
|
371
|
+
if (!delivery) {
|
|
372
|
+
logger.warn(`${this.logPrefix()} Card action failure without trusted delivery route: ${text}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const context = { delivery };
|
|
325
376
|
if (threadId)
|
|
326
377
|
context.threadId = threadId;
|
|
327
|
-
if (operatorId &&
|
|
378
|
+
if (operatorId && delivery.chatType === 'group')
|
|
328
379
|
context.peerId = operatorId;
|
|
329
380
|
void this.sendMessage(channelId, text, context).catch((err) => {
|
|
330
381
|
logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
|
|
@@ -334,7 +385,7 @@ export class AUNChannel {
|
|
|
334
385
|
logger.info(`${this.logPrefix()} ActionInteraction audit: behavior=${behavior ?? 'reply'} `
|
|
335
386
|
+ `cardId=${cardMessageId} interactionId=${interactionId} result=${result} textLength=${textLength}`);
|
|
336
387
|
}
|
|
337
|
-
extractTextPayload(payload, channelId, senderAid) {
|
|
388
|
+
extractTextPayload(payload, channelId, senderAid, delivery) {
|
|
338
389
|
if (typeof payload === 'string')
|
|
339
390
|
return payload;
|
|
340
391
|
if (payload && typeof payload === 'object') {
|
|
@@ -345,6 +396,7 @@ export class AUNChannel {
|
|
|
345
396
|
const cardMsgId = typeof obj.ref_message_id === 'string' ? obj.ref_message_id
|
|
346
397
|
: typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
|
|
347
398
|
const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
|
|
399
|
+
const cardDelivery = cardInfo?.delivery ?? delivery;
|
|
348
400
|
if (cardInfo) {
|
|
349
401
|
// compose 按钮由新版客户端在卡片内采集输入;旧版客户端可继续从主编辑器
|
|
350
402
|
// 采集。两者最终都回传 action_card_reply{behavior:"compose", text}。
|
|
@@ -369,25 +421,25 @@ export class AUNChannel {
|
|
|
369
421
|
// payloads omit senderAid. A group channelId is only a conversation
|
|
370
422
|
// identifier and must never be treated as the clicking operator.
|
|
371
423
|
const cardClickerAid = senderAid
|
|
372
|
-
|| (
|
|
424
|
+
|| (cardDelivery?.chatType === 'private' && channelId ? channelId : '')
|
|
373
425
|
|| '';
|
|
374
426
|
if (cardInfo.isCommandCard) {
|
|
375
427
|
// CommandCard:action_value 是完整 slash 命令,构造伪入站消息。
|
|
376
428
|
// 先完成点击者鉴权,再消费 mapping;否则群成员的一次误点会让发起者
|
|
377
429
|
// 无法继续操作同一张卡片。
|
|
378
|
-
const chatType =
|
|
430
|
+
const chatType = cardDelivery?.chatType ?? 'private';
|
|
379
431
|
if (this.messageHandler && actionValue.startsWith('/')) {
|
|
380
432
|
// Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
|
|
381
433
|
// 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
|
|
382
434
|
if (chatType === 'group' && !cardClickerAid) {
|
|
383
435
|
logger.info(`${this.logPrefix()} CommandCard rejected: missing authenticated clicker mid=${cardMsgId}`);
|
|
384
|
-
this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
|
|
436
|
+
this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
|
|
385
437
|
return '';
|
|
386
438
|
}
|
|
387
439
|
if (chatType === 'group' && cardInfo.initiatorAid
|
|
388
440
|
&& cardClickerAid !== cardInfo.initiatorAid) {
|
|
389
441
|
logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
|
|
390
|
-
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
|
|
442
|
+
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
|
|
391
443
|
return '';
|
|
392
444
|
}
|
|
393
445
|
// 已通过鉴权并即将进入处理流程;此时才消费 mapping,避免非发起者
|
|
@@ -406,28 +458,28 @@ export class AUNChannel {
|
|
|
406
458
|
})).catch((err) => {
|
|
407
459
|
logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
|
|
408
460
|
const message = err instanceof Error && err.message ? err.message : String(err || '未知错误');
|
|
409
|
-
this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId);
|
|
461
|
+
this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId, cardDelivery);
|
|
410
462
|
});
|
|
411
463
|
}
|
|
412
464
|
else if (!this.messageHandler) {
|
|
413
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId);
|
|
465
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId, cardDelivery);
|
|
414
466
|
}
|
|
415
467
|
else {
|
|
416
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId);
|
|
468
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId, cardDelivery);
|
|
417
469
|
}
|
|
418
470
|
}
|
|
419
471
|
else {
|
|
420
472
|
// ActionInteraction:后端明确接受后才消费 mapping。撤权、过期或
|
|
421
473
|
// 重复点击返回 false 时保持 fail-closed,并提示用户卡片已失效。
|
|
422
|
-
if (
|
|
474
|
+
if (cardDelivery?.chatType === 'group' && !cardClickerAid) {
|
|
423
475
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_unauthenticated', text.length);
|
|
424
|
-
this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
|
|
476
|
+
this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
|
|
425
477
|
return '';
|
|
426
478
|
}
|
|
427
|
-
if (
|
|
479
|
+
if (cardDelivery?.chatType === 'group' && cardInfo.initiatorAid
|
|
428
480
|
&& cardClickerAid !== cardInfo.initiatorAid) {
|
|
429
481
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_non_initiator', text.length);
|
|
430
|
-
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
|
|
482
|
+
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
|
|
431
483
|
return '';
|
|
432
484
|
}
|
|
433
485
|
// Compose reply 入参校验:text 必须非空且未超长。
|
|
@@ -437,19 +489,19 @@ export class AUNChannel {
|
|
|
437
489
|
const MAX_COMPOSE_TEXT_LENGTH = 64 * 1024; // 64K 字符
|
|
438
490
|
if (!trimmedText) {
|
|
439
491
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_empty', text.length);
|
|
440
|
-
this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId);
|
|
492
|
+
this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId, cardDelivery);
|
|
441
493
|
return '';
|
|
442
494
|
}
|
|
443
495
|
if (text.length > MAX_COMPOSE_TEXT_LENGTH) {
|
|
444
496
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_too_long', text.length);
|
|
445
|
-
this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId);
|
|
497
|
+
this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId, cardDelivery);
|
|
446
498
|
return '';
|
|
447
499
|
}
|
|
448
500
|
}
|
|
449
501
|
if (this.interactionCallback) {
|
|
450
502
|
if (this.interactionActionsInFlight.has(cardMsgId)) {
|
|
451
503
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_in_flight', text.length);
|
|
452
|
-
this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId);
|
|
504
|
+
this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId, cardDelivery);
|
|
453
505
|
return '';
|
|
454
506
|
}
|
|
455
507
|
this.interactionActionsInFlight.add(cardMsgId);
|
|
@@ -482,7 +534,7 @@ export class AUNChannel {
|
|
|
482
534
|
// captured the message id before the callback returned.
|
|
483
535
|
this.forgetInteractionCard(cardMsgId, cardInfo.requestId);
|
|
484
536
|
}
|
|
485
|
-
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId);
|
|
537
|
+
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId, cardDelivery);
|
|
486
538
|
};
|
|
487
539
|
try {
|
|
488
540
|
const result = this.interactionCallback(response);
|
|
@@ -491,7 +543,7 @@ export class AUNChannel {
|
|
|
491
543
|
this.interactionActionsInFlight.delete(cardMsgId);
|
|
492
544
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
|
|
493
545
|
logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
|
|
494
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
|
|
546
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
|
|
495
547
|
});
|
|
496
548
|
}
|
|
497
549
|
else {
|
|
@@ -502,19 +554,19 @@ export class AUNChannel {
|
|
|
502
554
|
this.interactionActionsInFlight.delete(cardMsgId);
|
|
503
555
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
|
|
504
556
|
logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
|
|
505
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
|
|
557
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
|
|
506
558
|
}
|
|
507
559
|
}
|
|
508
560
|
else {
|
|
509
561
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'handler_unavailable', text.length);
|
|
510
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId);
|
|
562
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId, cardDelivery);
|
|
511
563
|
}
|
|
512
564
|
}
|
|
513
565
|
}
|
|
514
566
|
else if (this.ownedCardMsgIds.has(cardMsgId)) {
|
|
515
567
|
// 本 agent 发出的卡片,但 entry 已过期(24h TTL)
|
|
516
568
|
logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
|
|
517
|
-
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起');
|
|
569
|
+
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', undefined, undefined, delivery);
|
|
518
570
|
}
|
|
519
571
|
else {
|
|
520
572
|
// 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
|
|
@@ -698,24 +750,20 @@ export class AUNChannel {
|
|
|
698
750
|
.replace(/[ \t]+/g, ' ')
|
|
699
751
|
.trim();
|
|
700
752
|
}
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
aids.push(m.aid);
|
|
753
|
+
parsePayloadMentionsOrReject(payload, context, messageId, seq) {
|
|
754
|
+
const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
755
|
+
? payload.mentions
|
|
756
|
+
: undefined;
|
|
757
|
+
try {
|
|
758
|
+
return normalizeAunMentionEntries(raw);
|
|
708
759
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (m && typeof m === 'object' && m.scope === 'all')
|
|
716
|
-
return true;
|
|
760
|
+
catch (error) {
|
|
761
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
762
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
763
|
+
const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
|
|
764
|
+
logger.error(`${this.logPrefix()} Dropped ${context}: invalid payload.mentions (${detail}) code=${code} mid=${messageId}`);
|
|
765
|
+
return undefined;
|
|
717
766
|
}
|
|
718
|
-
return false;
|
|
719
767
|
}
|
|
720
768
|
/** 从正文提取 @aid(AID 格式:至少三段域名),用于出站填充 payload.mentions */
|
|
721
769
|
static MENTION_AID_RE = /@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?){2,})/g;
|
|
@@ -731,6 +779,19 @@ export class AUNChannel {
|
|
|
731
779
|
}
|
|
732
780
|
return out;
|
|
733
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* Convert the AUN text mention notation into the canonical wire entries.
|
|
784
|
+
* `@all` is intentionally represented by its scope discriminator instead
|
|
785
|
+
* of being treated as an AID.
|
|
786
|
+
*/
|
|
787
|
+
extractMentionEntriesFromText(text) {
|
|
788
|
+
const entries = [];
|
|
789
|
+
if (this.hasExplicitMention(text, 'all'))
|
|
790
|
+
entries.push({ scope: 'all' });
|
|
791
|
+
for (const aid of this.extractMentionAidsFromText(text))
|
|
792
|
+
entries.push({ aid });
|
|
793
|
+
return entries;
|
|
794
|
+
}
|
|
734
795
|
/** Sanitize a raw topicName from untrusted payload: trim, strip control chars, enforce length. */
|
|
735
796
|
sanitizeTopicName(raw) {
|
|
736
797
|
if (typeof raw !== 'string')
|
|
@@ -740,8 +801,13 @@ export class AUNChannel {
|
|
|
740
801
|
return sanitized;
|
|
741
802
|
return undefined;
|
|
742
803
|
}
|
|
743
|
-
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName) {
|
|
744
|
-
|
|
804
|
+
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
|
|
805
|
+
if (!groupId)
|
|
806
|
+
throw new Error('buildGroupReplyContext requires groupId');
|
|
807
|
+
const replyContext = {
|
|
808
|
+
metadata: { encrypted, chatmode },
|
|
809
|
+
delivery: { chatType: 'group', groupId },
|
|
810
|
+
};
|
|
745
811
|
if (threadId)
|
|
746
812
|
replyContext.threadId = threadId;
|
|
747
813
|
replyContext.peerId = senderAid;
|
|
@@ -755,6 +821,108 @@ export class AUNChannel {
|
|
|
755
821
|
}
|
|
756
822
|
return replyContext;
|
|
757
823
|
}
|
|
824
|
+
buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
|
|
825
|
+
const replyContext = {
|
|
826
|
+
metadata: { encrypted, chatmode, refMessageId },
|
|
827
|
+
delivery: { chatType: 'private' },
|
|
828
|
+
peerId: senderAid,
|
|
829
|
+
};
|
|
830
|
+
if (threadId)
|
|
831
|
+
replyContext.threadId = threadId;
|
|
832
|
+
if (messageId)
|
|
833
|
+
replyContext.replyToMessageId = messageId;
|
|
834
|
+
if (topicName && threadId)
|
|
835
|
+
replyContext.metadata.topicName = topicName;
|
|
836
|
+
return replyContext;
|
|
837
|
+
}
|
|
838
|
+
claimInboundMessage(kind, messageId, seq) {
|
|
839
|
+
if (!messageId)
|
|
840
|
+
return true;
|
|
841
|
+
// Channel instance names are commonly reused across agents (the default
|
|
842
|
+
// is simply `aun`). Include the receiving AID so a server-scoped
|
|
843
|
+
// message_id cannot suppress an unrelated delivery in another channel.
|
|
844
|
+
const namespace = `${this.config.aid}:${this.config.channelName ?? 'aun'}:aun:${kind}`;
|
|
845
|
+
const dedupeKey = JSON.stringify([namespace, messageId]);
|
|
846
|
+
const existing = this.inboundSeenMessages.get(dedupeKey);
|
|
847
|
+
if (existing) {
|
|
848
|
+
if (seq !== undefined && existing.seq !== undefined && seq !== existing.seq) {
|
|
849
|
+
logger.warn(`${this.logPrefix()} Duplicate message_id with conflicting seq: namespace=${namespace} mid=${messageId} firstSeq=${existing.seq} duplicateSeq=${seq}`);
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
logger.debug(`${this.logPrefix()} Duplicate inbound message dropped: namespace=${namespace} mid=${messageId} seq=${seq ?? 'unknown'}`);
|
|
853
|
+
}
|
|
854
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
this.inboundSeenMessages.set(dedupeKey, { seenAt: Date.now(), seq });
|
|
858
|
+
if (seq !== undefined)
|
|
859
|
+
this.messageSeqMap.set(messageId, seq);
|
|
860
|
+
this.appendInboundDedupe(dedupeKey, messageId, kind, seq);
|
|
861
|
+
setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), AUN_INBOUND_DEDUP_TTL_MS);
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
inboundDedupeFile() {
|
|
865
|
+
const channelName = this.config.channelName ?? 'aun';
|
|
866
|
+
return channelStatePath(channelName, 'dedup', 'aun', `${encodeURIComponent(this.config.aid)}.jsonl`);
|
|
867
|
+
}
|
|
868
|
+
loadInboundDedupe() {
|
|
869
|
+
const file = this.inboundDedupeFile();
|
|
870
|
+
const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
|
|
871
|
+
let lines;
|
|
872
|
+
try {
|
|
873
|
+
lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
let needsRewrite = false;
|
|
879
|
+
for (const line of lines) {
|
|
880
|
+
try {
|
|
881
|
+
const entry = JSON.parse(line);
|
|
882
|
+
if (typeof entry.key !== 'string' || typeof entry.seenAt !== 'number' || !Number.isFinite(entry.seenAt) || entry.seenAt <= cutoff) {
|
|
883
|
+
needsRewrite = true;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
this.inboundSeenMessages.set(entry.key, {
|
|
887
|
+
seenAt: entry.seenAt,
|
|
888
|
+
seq: typeof entry.seq === 'number' && Number.isFinite(entry.seq) ? entry.seq : undefined,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
needsRewrite = true;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (needsRewrite)
|
|
896
|
+
this.rewriteInboundDedupe();
|
|
897
|
+
}
|
|
898
|
+
appendInboundDedupe(key, messageId, kind, seq) {
|
|
899
|
+
try {
|
|
900
|
+
const file = this.inboundDedupeFile();
|
|
901
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
902
|
+
fs.appendFileSync(file, JSON.stringify({ key, messageId, kind, seenAt: Date.now(), seq }) + '\n');
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
logger.warn(`${this.logPrefix()} Failed to persist inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
rewriteInboundDedupe() {
|
|
909
|
+
const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
|
|
910
|
+
for (const [key, value] of this.inboundSeenMessages) {
|
|
911
|
+
if (value.seenAt <= cutoff)
|
|
912
|
+
this.inboundSeenMessages.delete(key);
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
const file = this.inboundDedupeFile();
|
|
916
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
917
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
918
|
+
const lines = [...this.inboundSeenMessages].map(([key, value]) => JSON.stringify({ key, seenAt: value.seenAt, seq: value.seq }));
|
|
919
|
+
fs.writeFileSync(tmp, lines.length > 0 ? `${lines.join('\n')}\n` : '');
|
|
920
|
+
fs.renameSync(tmp, file);
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
logger.warn(`${this.logPrefix()} Failed to compact inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
758
926
|
acknowledgeImmediately(messageId, _seq) {
|
|
759
927
|
// SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
|
|
760
928
|
// as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
|
|
@@ -771,10 +939,16 @@ export class AUNChannel {
|
|
|
771
939
|
_aid;
|
|
772
940
|
_selfName; // 本地 agent.md 中的 name,首次 connect 时读取
|
|
773
941
|
_chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
|
|
774
|
-
|
|
942
|
+
/**
|
|
943
|
+
* Inbound application dedupe. The AUN SDK may redeliver the same message
|
|
944
|
+
* with a different transport sequence, so message_id is the identity and
|
|
945
|
+
* seq is only retained for ACK/conflict diagnostics. Claiming happens at
|
|
946
|
+
* the event handler boundary, before payload/slash/mention parsing.
|
|
947
|
+
*/
|
|
948
|
+
inboundSeenMessages = new Map();
|
|
775
949
|
groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
|
|
776
950
|
peerInfoCache = new Map();
|
|
777
|
-
messageSeqMap = new Map(); // messageId → seq (for ack)
|
|
951
|
+
messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
|
|
778
952
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
779
953
|
peerE2ee = new Map();
|
|
780
954
|
static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
|
|
@@ -789,7 +963,7 @@ export class AUNChannel {
|
|
|
789
963
|
interactionCardMessageIds = new Map();
|
|
790
964
|
/** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
|
|
791
965
|
invalidatedInteractions = new Map();
|
|
792
|
-
|
|
966
|
+
mentionModeResolver;
|
|
793
967
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
794
968
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
795
969
|
'merge', 'link', 'location', 'personal_card',
|
|
@@ -798,6 +972,10 @@ export class AUNChannel {
|
|
|
798
972
|
static INJECT_REQUEST_TYPE = 'observer.inject';
|
|
799
973
|
/** 绑定请求类型(Evol App → 控制 AID):快速路径,绕过白名单透传原始 payload JSON 给 bridge。 */
|
|
800
974
|
static BIND_REQUEST_TYPE = 'bind.request';
|
|
975
|
+
/** 联系人申请本人状态查询:在普通消息准入前交给 bridge,身份只取认证信封。 */
|
|
976
|
+
static CONTACT_REQUEST_QUERY_TYPE = 'contact.request.query';
|
|
977
|
+
/** App 联系人申请提交:结构化控制请求,不是 slash command。 */
|
|
978
|
+
static CONTACT_REQUEST_SUBMIT_TYPE = 'contact.request.submit';
|
|
801
979
|
// Reconnect state
|
|
802
980
|
// SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
|
|
803
981
|
// 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
|
|
@@ -829,6 +1007,7 @@ export class AUNChannel {
|
|
|
829
1007
|
constructor(config) {
|
|
830
1008
|
this.config = config;
|
|
831
1009
|
this.agentDir = agentDirPath(config.aid);
|
|
1010
|
+
this.loadInboundDedupe();
|
|
832
1011
|
if (config.aunTrace) {
|
|
833
1012
|
this.traceWriter = new LogWriter({
|
|
834
1013
|
baseName: 'aun',
|
|
@@ -926,7 +1105,7 @@ export class AUNChannel {
|
|
|
926
1105
|
// gateway metadata cache when well-known is temporarily unavailable.
|
|
927
1106
|
const configuredGateway = this.config.gatewayUrl || '';
|
|
928
1107
|
logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${configuredGateway || '<sdk-discovery>'}, aun_path=${aunPath}`);
|
|
929
|
-
// 构造 AIDStore
|
|
1108
|
+
// 构造 AIDStore。daemon 使用独立 slot,CLI/netcheck 不能触碰业务入站游标。
|
|
930
1109
|
// encryptionSeed / rootCaPath 由 getAidStore 内部注入
|
|
931
1110
|
const store = await getAidStore({
|
|
932
1111
|
slotId: SLOT.daemon,
|
|
@@ -979,7 +1158,7 @@ export class AUNChannel {
|
|
|
979
1158
|
for (const id of ids) {
|
|
980
1159
|
if (typeof id === 'string') {
|
|
981
1160
|
logger.info(`${this.logPrefix()} Message recalled: ${id}`);
|
|
982
|
-
this.recallHandler?.(id);
|
|
1161
|
+
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
983
1162
|
}
|
|
984
1163
|
}
|
|
985
1164
|
}
|
|
@@ -1011,7 +1190,7 @@ export class AUNChannel {
|
|
|
1011
1190
|
for (const id of ids) {
|
|
1012
1191
|
if (typeof id === 'string') {
|
|
1013
1192
|
logger.info(`${this.logPrefix()} Group message recalled: group=${env.group_id ?? ''} mid=${id}`);
|
|
1014
|
-
this.recallHandler?.(id);
|
|
1193
|
+
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
1015
1194
|
}
|
|
1016
1195
|
}
|
|
1017
1196
|
}
|
|
@@ -1049,7 +1228,7 @@ export class AUNChannel {
|
|
|
1049
1228
|
});
|
|
1050
1229
|
this.trace('OUT', 'client.connect', { gateway: this.gatewayUrl, extra_info: extraInfo });
|
|
1051
1230
|
await client.connect({
|
|
1052
|
-
// connection_kind 默认 long;slot 已由 AID 携带(evolcore
|
|
1231
|
+
// connection_kind 默认 long;slot 已由 AID 携带(evolcore-daemon)
|
|
1053
1232
|
// extra_info:互踢诊断名片(0.4.3 公开 connect 已支持透传)
|
|
1054
1233
|
extra_info: extraInfo,
|
|
1055
1234
|
// max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
|
|
@@ -1109,7 +1288,9 @@ export class AUNChannel {
|
|
|
1109
1288
|
}
|
|
1110
1289
|
const operationId = postBootstrapWelcomeOperationId(aidName);
|
|
1111
1290
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
|
|
1112
|
-
|
|
1291
|
+
const prepared = outbox.findByDedupeKey(aidName, operationId);
|
|
1292
|
+
if (hasMessageLogOperation(chatDir, operationId)
|
|
1293
|
+
|| (prepared?.delivery?.chatType === 'private' && prepared.channelId === owner)) {
|
|
1113
1294
|
return true;
|
|
1114
1295
|
}
|
|
1115
1296
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
@@ -1138,6 +1319,14 @@ export class AUNChannel {
|
|
|
1138
1319
|
return false;
|
|
1139
1320
|
return this.reconcilePostBootstrapWelcome();
|
|
1140
1321
|
}
|
|
1322
|
+
async hasPendingPostBootstrapWelcome() {
|
|
1323
|
+
const aid = this.config.aid.replace(/^@/, '');
|
|
1324
|
+
return hasPendingPostBootstrapWelcomeOutbox(aid);
|
|
1325
|
+
}
|
|
1326
|
+
async bindPostBootstrapWelcomeToSession(sessionId) {
|
|
1327
|
+
const aid = this.config.aid.replace(/^@/, '');
|
|
1328
|
+
return bindPostBootstrapWelcomeOutboxSession(aid, sessionId);
|
|
1329
|
+
}
|
|
1141
1330
|
/**
|
|
1142
1331
|
* Resume delivery of an already prepared completion welcome.
|
|
1143
1332
|
*
|
|
@@ -1157,6 +1346,11 @@ export class AUNChannel {
|
|
|
1157
1346
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
|
|
1158
1347
|
return hasMessageLogOperation(chatDir, operationId);
|
|
1159
1348
|
}
|
|
1349
|
+
if (entry.delivery?.chatType !== 'private') {
|
|
1350
|
+
logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
|
|
1351
|
+
outbox.remove(aid, entry.id);
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1160
1354
|
const agentConfig = loadAgent(aid);
|
|
1161
1355
|
if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
|
|
1162
1356
|
logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
|
|
@@ -1180,7 +1374,7 @@ export class AUNChannel {
|
|
|
1180
1374
|
* @param channelId 下载归属(私聊 fromAid / 群聊 groupId)
|
|
1181
1375
|
* @param preCollected 已收集的附件(群聊路径会提前 collect,避免重复)
|
|
1182
1376
|
*/
|
|
1183
|
-
async processAttachments(payload, baseText, channelId, preCollected) {
|
|
1377
|
+
async processAttachments(payload, baseText, channelId, preCollected, delivery) {
|
|
1184
1378
|
const rawAttachments = preCollected ?? this.collectAllAttachments(payload);
|
|
1185
1379
|
const images = [];
|
|
1186
1380
|
let finalText = baseText;
|
|
@@ -1189,7 +1383,7 @@ export class AUNChannel {
|
|
|
1189
1383
|
}
|
|
1190
1384
|
const fileParts = [];
|
|
1191
1385
|
for (const att of rawAttachments) {
|
|
1192
|
-
const filePath = await this.downloadAttachment(att, channelId);
|
|
1386
|
+
const filePath = await this.downloadAttachment(att, channelId, delivery);
|
|
1193
1387
|
if (!filePath)
|
|
1194
1388
|
continue;
|
|
1195
1389
|
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
@@ -1221,8 +1415,11 @@ export class AUNChannel {
|
|
|
1221
1415
|
logger.info(`${this.logPrefix()} [attachments] count=${rawAttachments.length} images=${images.length} files=${fileParts.length}`);
|
|
1222
1416
|
return { finalText, images };
|
|
1223
1417
|
}
|
|
1224
|
-
async downloadAttachment(att, channelId) {
|
|
1225
|
-
const ownerAid = att.owner_aid
|
|
1418
|
+
async downloadAttachment(att, channelId, delivery) {
|
|
1419
|
+
const ownerAid = att.owner_aid
|
|
1420
|
+
|| (delivery?.chatType === 'private' ? channelId : '')
|
|
1421
|
+
|| this._aid
|
|
1422
|
+
|| '';
|
|
1226
1423
|
const objectKey = att.object_key;
|
|
1227
1424
|
if (!objectKey) {
|
|
1228
1425
|
logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
|
|
@@ -1394,17 +1591,27 @@ export class AUNChannel {
|
|
|
1394
1591
|
if (!data || typeof data !== 'object')
|
|
1395
1592
|
return;
|
|
1396
1593
|
const msg = data;
|
|
1594
|
+
const receivedAt = Date.now();
|
|
1595
|
+
// Claim before any payload extraction or command parsing. Retransmits may
|
|
1596
|
+
// carry a new seq, but the application identity remains message_id.
|
|
1597
|
+
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
1598
|
+
const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
|
|
1599
|
+
if (!this.claimInboundMessage('private', messageId, seq))
|
|
1600
|
+
return;
|
|
1397
1601
|
// SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1398
1602
|
// message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
|
|
1399
1603
|
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1400
1604
|
const protectedHeaders = verifiedProtectedHeaders(msg, env);
|
|
1401
1605
|
const fromAid = env.from ?? '';
|
|
1402
1606
|
const payload = msg.payload ?? '';
|
|
1607
|
+
const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
|
|
1608
|
+
if (!mentions)
|
|
1609
|
+
return;
|
|
1403
1610
|
const causationRef = payload && typeof payload === 'object' && typeof payload._evolcore_causation_ref === 'string'
|
|
1404
1611
|
? payload._evolcore_causation_ref
|
|
1405
1612
|
: undefined;
|
|
1406
1613
|
const logDescriptor = classifyAunPayloadForLog(payload);
|
|
1407
|
-
const text = this.extractTextPayload(payload, fromAid, fromAid);
|
|
1614
|
+
const text = this.extractTextPayload(payload, fromAid, fromAid, { chatType: 'private' });
|
|
1408
1615
|
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
1409
1616
|
const refMessageId = typeof payload === 'object' && payload !== null && typeof payload.ref_message_id === 'string'
|
|
1410
1617
|
? payload.ref_message_id
|
|
@@ -1413,15 +1620,19 @@ export class AUNChannel {
|
|
|
1413
1620
|
const topicName = (typeof payload === 'object' && payload !== null)
|
|
1414
1621
|
? this.sanitizeTopicName(payload.topicName)
|
|
1415
1622
|
: undefined;
|
|
1416
|
-
const messageId = msg.message_id ?? '';
|
|
1417
1623
|
const causation = consumeAunCausation(causationRef || messageId, fromAid, this._aid || this.config.aid)
|
|
1418
1624
|
?? (causationRef ? consumeAunCausation(messageId, fromAid, this._aid || this.config.aid) : undefined);
|
|
1419
|
-
const
|
|
1625
|
+
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
1626
|
+
// 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
|
|
1627
|
+
if (inboundType === 'error') {
|
|
1628
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1629
|
+
logger.info(`${this.logPrefix()} P2P dropped terminal error: from=${fromAid || 'unknown'} mid=${messageId}`);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1420
1632
|
// Observer forward (inbound):在所有过滤之前转发原始明文 payload。
|
|
1421
1633
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
1422
1634
|
// 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
|
|
1423
1635
|
// (即便日后 from-owner 排除规则调整,也不会泄漏)。
|
|
1424
|
-
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
1425
1636
|
if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
|
|
1426
1637
|
this.forwardInbound(msg);
|
|
1427
1638
|
}
|
|
@@ -1437,13 +1648,16 @@ export class AUNChannel {
|
|
|
1437
1648
|
const msgEncrypted = !!env.encrypted;
|
|
1438
1649
|
if (!msgEncrypted)
|
|
1439
1650
|
this.plaintextRecv++;
|
|
1440
|
-
//
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1651
|
+
// Preserve structured mentions for the shared inbound message model.
|
|
1652
|
+
// Text scanning remains only a rendering fallback and is not promoted to
|
|
1653
|
+
// a structured @all decision.
|
|
1654
|
+
if (mentions.length === 0 && this._aid && text.includes(`@${this._aid}`)) {
|
|
1655
|
+
// Private channels historically exposed a self mention even when the
|
|
1656
|
+
// sender omitted payload.mentions; keep that compatibility signal.
|
|
1657
|
+
mentions.push({ aid: this._aid });
|
|
1444
1658
|
}
|
|
1445
1659
|
// Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
|
|
1446
|
-
const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
|
|
1660
|
+
const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid, undefined, { chatType: 'private' });
|
|
1447
1661
|
// 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
|
|
1448
1662
|
// device_id 仅 SDK 内部多实例去重用,evolcore session 层面跨端共享会话
|
|
1449
1663
|
const chatId = fromAid;
|
|
@@ -1459,22 +1673,26 @@ export class AUNChannel {
|
|
|
1459
1673
|
logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
1460
1674
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1461
1675
|
if (p2pPayloadType === 'action_card_reply') {
|
|
1462
|
-
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1676
|
+
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
1463
1677
|
return;
|
|
1464
1678
|
}
|
|
1465
|
-
// menu.*
|
|
1466
|
-
if (p2pPayloadType.startsWith('menu.')
|
|
1679
|
+
// menu.* 与联系人本人状态查询:需要原始 payload JSON 传递给 bridge。
|
|
1680
|
+
if (p2pPayloadType.startsWith('menu.')
|
|
1681
|
+
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
|
|
1682
|
+
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
|
|
1467
1683
|
this.acknowledgeImmediately(messageId, seq);
|
|
1468
|
-
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1684
|
+
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
1469
1685
|
this.dispatchMessage({
|
|
1470
1686
|
channelId: chatId, userId: fromAid,
|
|
1471
1687
|
text: JSON.stringify(payload),
|
|
1472
|
-
chatType: 'private', messageId, seq,
|
|
1688
|
+
chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
|
|
1473
1689
|
peerName: displayName || undefined,
|
|
1474
1690
|
peerType: peerIdentity.type,
|
|
1475
1691
|
encrypted: msgEncrypted,
|
|
1476
1692
|
protectedHeaders,
|
|
1477
|
-
|
|
1693
|
+
mentions,
|
|
1694
|
+
mentionAids: mentionEntryAids(mentions),
|
|
1695
|
+
replyContext: this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
|
|
1478
1696
|
msgType: logDescriptor.msgType,
|
|
1479
1697
|
payloadType: logDescriptor.payloadType,
|
|
1480
1698
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -1494,14 +1712,16 @@ export class AUNChannel {
|
|
|
1494
1712
|
// 透传 encrypted:bind.response 须与请求的加密/明文对称(见 onMessage 处理)。
|
|
1495
1713
|
if (p2pPayloadType === AUNChannel.BIND_REQUEST_TYPE) {
|
|
1496
1714
|
this.acknowledgeImmediately(messageId, seq);
|
|
1497
|
-
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1715
|
+
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
1498
1716
|
this.dispatchMessage({
|
|
1499
1717
|
channelId: chatId, userId: fromAid,
|
|
1500
1718
|
text: JSON.stringify(payload),
|
|
1501
|
-
chatType: 'private', messageId, seq,
|
|
1719
|
+
chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
|
|
1502
1720
|
peerName: displayName || undefined,
|
|
1503
1721
|
peerType: peerIdentity.type,
|
|
1504
1722
|
encrypted: msgEncrypted,
|
|
1723
|
+
mentions,
|
|
1724
|
+
mentionAids: mentionEntryAids(mentions),
|
|
1505
1725
|
msgType: logDescriptor.msgType,
|
|
1506
1726
|
payloadType: logDescriptor.payloadType,
|
|
1507
1727
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -1512,7 +1732,7 @@ export class AUNChannel {
|
|
|
1512
1732
|
// payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
|
|
1513
1733
|
if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
|
|
1514
1734
|
this.acknowledgeImmediately(messageId, seq);
|
|
1515
|
-
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1735
|
+
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
1516
1736
|
logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
|
|
1517
1737
|
return;
|
|
1518
1738
|
}
|
|
@@ -1521,24 +1741,15 @@ export class AUNChannel {
|
|
|
1521
1741
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
|
|
1522
1742
|
const isSystemP2P = p2pPayloadType === 'event';
|
|
1523
1743
|
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
|
|
1524
|
-
const replyContext =
|
|
1525
|
-
if (threadId)
|
|
1526
|
-
replyContext.threadId = threadId;
|
|
1527
|
-
replyContext.peerId = fromAid;
|
|
1528
|
-
if (messageId)
|
|
1529
|
-
replyContext.replyToMessageId = messageId;
|
|
1530
|
-
// 协议 v2.4 §6.4/§12.1: 话题会话创建时传入 topicName
|
|
1531
|
-
if (topicName && threadId) {
|
|
1532
|
-
if (!replyContext.metadata)
|
|
1533
|
-
replyContext.metadata = {};
|
|
1534
|
-
replyContext.metadata.topicName = topicName;
|
|
1535
|
-
}
|
|
1744
|
+
const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId);
|
|
1536
1745
|
this.dispatchMessage({
|
|
1537
1746
|
channelId: chatId,
|
|
1538
1747
|
userId: fromAid,
|
|
1539
1748
|
text: finalText,
|
|
1540
1749
|
chatType: 'private',
|
|
1541
1750
|
messageId,
|
|
1751
|
+
receivedAt,
|
|
1752
|
+
gatewaySeq: seq,
|
|
1542
1753
|
causation,
|
|
1543
1754
|
seq,
|
|
1544
1755
|
threadId,
|
|
@@ -1561,28 +1772,39 @@ export class AUNChannel {
|
|
|
1561
1772
|
if (!data || typeof data !== 'object')
|
|
1562
1773
|
return;
|
|
1563
1774
|
const msg = data;
|
|
1775
|
+
const receivedAt = Date.now();
|
|
1776
|
+
// Deduplicate at the adapter boundary, before mention/slash routing.
|
|
1777
|
+
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
1778
|
+
const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
|
|
1779
|
+
if (!this.claimInboundMessage('group', messageId, seq))
|
|
1780
|
+
return;
|
|
1564
1781
|
// SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1565
|
-
// message_id / seq / payload / same_* /
|
|
1782
|
+
// message_id / seq / payload / same_* / mention_mode 等仍是顶层独立字段,不在 envelope 内。
|
|
1566
1783
|
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1567
1784
|
const protectedHeaders = verifiedProtectedHeaders(msg, env);
|
|
1568
1785
|
const groupId = env.group_id ?? '';
|
|
1569
1786
|
const senderAid = env.from ?? '';
|
|
1570
1787
|
const payload = msg.payload ?? '';
|
|
1788
|
+
const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
|
|
1789
|
+
if (!payloadMentionEntries)
|
|
1790
|
+
return;
|
|
1791
|
+
const payloadMentionAids = mentionEntryAids(payloadMentionEntries);
|
|
1571
1792
|
const logDescriptor = classifyAunPayloadForLog(payload);
|
|
1572
|
-
const text = this.extractTextPayload(payload, groupId, senderAid);
|
|
1793
|
+
const text = this.extractTextPayload(payload, groupId, senderAid, { chatType: 'group', groupId });
|
|
1573
1794
|
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
1574
1795
|
const topicName = (typeof payload === 'object' && payload !== null)
|
|
1575
1796
|
? this.sanitizeTopicName(payload.topicName)
|
|
1576
1797
|
: undefined;
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1798
|
+
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
1799
|
+
// 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
|
|
1800
|
+
if (inboundType === 'error') {
|
|
1801
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1802
|
+
logger.info(`${this.logPrefix()} Group dropped terminal error: group=${groupId || 'unknown'} sender=${senderAid || 'unknown'} mid=${messageId}`);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1579
1805
|
// Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
|
|
1580
1806
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
1581
1807
|
this.forwardInbound(msg);
|
|
1582
|
-
// Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
|
|
1583
|
-
const payloadMentions = Array.isArray(payload?.mentions)
|
|
1584
|
-
? payload.mentions.filter((m) => typeof m === 'string')
|
|
1585
|
-
: [];
|
|
1586
1808
|
logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
|
|
1587
1809
|
if (!groupId || !senderAid) {
|
|
1588
1810
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -1624,7 +1846,7 @@ export class AUNChannel {
|
|
|
1624
1846
|
peerName: displayName,
|
|
1625
1847
|
peerType: peerInfo?.type || 'unknown',
|
|
1626
1848
|
seq,
|
|
1627
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName),
|
|
1849
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
|
|
1628
1850
|
createdAt,
|
|
1629
1851
|
});
|
|
1630
1852
|
return;
|
|
@@ -1634,7 +1856,7 @@ export class AUNChannel {
|
|
|
1634
1856
|
{
|
|
1635
1857
|
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
1636
1858
|
if (payloadType === 'action_card_reply') {
|
|
1637
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
|
|
1859
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
1638
1860
|
return;
|
|
1639
1861
|
}
|
|
1640
1862
|
}
|
|
@@ -1647,14 +1869,18 @@ export class AUNChannel {
|
|
|
1647
1869
|
if (payloadType.startsWith('menu.')) {
|
|
1648
1870
|
const menuEncrypted = !!env.encrypted;
|
|
1649
1871
|
this.acknowledgeImmediately(messageId, seq);
|
|
1650
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId);
|
|
1872
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
1651
1873
|
this.dispatchMessage({
|
|
1652
1874
|
channelId: groupId, userId: senderAid,
|
|
1653
1875
|
text: JSON.stringify(payload),
|
|
1654
|
-
chatType: 'group', messageId, seq, groupId,
|
|
1876
|
+
chatType: 'group', messageId, seq, receivedAt, gatewaySeq: seq, groupId,
|
|
1655
1877
|
encrypted: menuEncrypted,
|
|
1656
1878
|
protectedHeaders,
|
|
1657
|
-
|
|
1879
|
+
mentions: payloadMentionEntries,
|
|
1880
|
+
mentionAids: payloadMentionAids,
|
|
1881
|
+
isMentioned: hasMentionAll(payloadMentionEntries)
|
|
1882
|
+
|| (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
|
|
1883
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName, groupId),
|
|
1658
1884
|
msgType: logDescriptor.msgType,
|
|
1659
1885
|
payloadType: logDescriptor.payloadType,
|
|
1660
1886
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -1664,7 +1890,7 @@ export class AUNChannel {
|
|
|
1664
1890
|
}
|
|
1665
1891
|
if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
|
|
1666
1892
|
this.acknowledgeImmediately(messageId, seq);
|
|
1667
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
|
|
1893
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
1668
1894
|
logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
|
|
1669
1895
|
return;
|
|
1670
1896
|
}
|
|
@@ -1673,17 +1899,17 @@ export class AUNChannel {
|
|
|
1673
1899
|
const msgEncrypted = !!env.encrypted;
|
|
1674
1900
|
if (!msgEncrypted)
|
|
1675
1901
|
this.plaintextRecv++;
|
|
1676
|
-
//
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
// @all 仅认结构化 mentions(payload.mentions
|
|
1686
|
-
const mentionedAll =
|
|
1902
|
+
// mention_mode 是唯一的服务端群 @ 策略字段;缺失/非法时使用协议内置默认 disabled。
|
|
1903
|
+
const serverMentionMode = normalizeMentionMode(msg.mention_mode)
|
|
1904
|
+
?? normalizeMentionMode(payload?.mention_mode)
|
|
1905
|
+
?? 'disabled';
|
|
1906
|
+
const localMentionMode = normalizeMentionMode(this.mentionModeResolver
|
|
1907
|
+
? await this.mentionModeResolver(groupId).catch(() => undefined)
|
|
1908
|
+
: undefined);
|
|
1909
|
+
const mentionMode = localMentionMode ?? serverMentionMode;
|
|
1910
|
+
const mentionedSelf = !!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid);
|
|
1911
|
+
// @all 仅认结构化 mentions(payload.mentions),不扫描正文,避免引述性 "@all" 误判。
|
|
1912
|
+
const mentionedAll = hasMentionAll(payloadMentionEntries);
|
|
1687
1913
|
// Echo 机制优先于 mention 过滤:消息第一行包含 echo 时触发
|
|
1688
1914
|
// 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1689
1915
|
const firstLineGroup = text.split('\n')[0] || '';
|
|
@@ -1705,7 +1931,7 @@ export class AUNChannel {
|
|
|
1705
1931
|
this.pendingEchoMessages.set(messageId, {
|
|
1706
1932
|
text: echoText,
|
|
1707
1933
|
channelId: groupId,
|
|
1708
|
-
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined),
|
|
1934
|
+
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined, groupId),
|
|
1709
1935
|
receiveTs: Date.now(),
|
|
1710
1936
|
});
|
|
1711
1937
|
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
@@ -1718,16 +1944,17 @@ export class AUNChannel {
|
|
|
1718
1944
|
}
|
|
1719
1945
|
else {
|
|
1720
1946
|
// 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
|
|
1721
|
-
//
|
|
1947
|
+
// slash 命令在任何 mentionMode 下都强制走 mention-only 语义
|
|
1722
1948
|
// 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
|
|
1723
|
-
const enforceMention =
|
|
1949
|
+
const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
|
|
1724
1950
|
const isMentioned = mentionedSelf || mentionedAll;
|
|
1725
1951
|
// 过滤逻辑下移到响应层:这里只标记,不过滤
|
|
1726
1952
|
// 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
|
|
1727
1953
|
// TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
|
|
1728
1954
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
1729
1955
|
this.acknowledgeImmediately(messageId, seq);
|
|
1730
|
-
|
|
1956
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
1957
|
+
logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
1731
1958
|
return;
|
|
1732
1959
|
}
|
|
1733
1960
|
}
|
|
@@ -1745,11 +1972,9 @@ export class AUNChannel {
|
|
|
1745
1972
|
logger.debug(`${this.logPrefix()} Group dropped: empty text and no attachments (group=${groupId} sender=${senderAid} mid=${messageId})`);
|
|
1746
1973
|
return;
|
|
1747
1974
|
}
|
|
1748
|
-
const mentions =
|
|
1749
|
-
? ['all']
|
|
1750
|
-
: mentionedSelf && this._aid ? [this._aid] : [];
|
|
1975
|
+
const mentions = payloadMentionEntries;
|
|
1751
1976
|
// Process attachments
|
|
1752
|
-
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
|
|
1977
|
+
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
|
|
1753
1978
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
1754
1979
|
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
|
|
1755
1980
|
const shortAid = this.getShortAid(senderAid);
|
|
@@ -1758,27 +1983,27 @@ export class AUNChannel {
|
|
|
1758
1983
|
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
1759
1984
|
const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
|
|
1760
1985
|
const textMentionAll = this.hasExplicitMention(text, 'all');
|
|
1761
|
-
const structMentionSelf = this._aid ?
|
|
1762
|
-
const structMentionAll =
|
|
1986
|
+
const structMentionSelf = this._aid ? payloadMentionAids.includes(this._aid) : false;
|
|
1987
|
+
const structMentionAll = mentionedAll;
|
|
1763
1988
|
const reason = isOwnHandoff
|
|
1764
1989
|
? 'handoff.self'
|
|
1765
1990
|
: mentionedAll
|
|
1766
1991
|
? 'mention.all(struct)'
|
|
1767
1992
|
: mentionedSelf
|
|
1768
1993
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
1769
|
-
: `${
|
|
1770
|
-
logger.info(`${this.logPrefix()} Group
|
|
1994
|
+
: `${mentionMode}.no-mention`;
|
|
1995
|
+
logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
1771
1996
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1772
1997
|
if (payloadType === 'action_card_reply')
|
|
1773
1998
|
return;
|
|
1774
1999
|
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1775
|
-
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName})
|
|
2000
|
+
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mentionMode=${mentionMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
1776
2001
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: senderAid, msgId: messageId, kind: 'text', len: finalText.length, groupId });
|
|
1777
2002
|
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode, payloadType === 'event' ? 'notify' : 'send');
|
|
1778
2003
|
// 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
|
|
1779
2004
|
// 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
|
|
1780
2005
|
const renderMentionAids = Array.from(new Set([
|
|
1781
|
-
...
|
|
2006
|
+
...payloadMentionAids,
|
|
1782
2007
|
...this.extractMentionAidsFromText(text),
|
|
1783
2008
|
]));
|
|
1784
2009
|
this.dispatchMessage({
|
|
@@ -1793,6 +2018,8 @@ export class AUNChannel {
|
|
|
1793
2018
|
text: finalText,
|
|
1794
2019
|
chatType: 'group',
|
|
1795
2020
|
messageId,
|
|
2021
|
+
receivedAt,
|
|
2022
|
+
gatewaySeq: seq,
|
|
1796
2023
|
seq,
|
|
1797
2024
|
threadId,
|
|
1798
2025
|
mentions,
|
|
@@ -1800,8 +2027,8 @@ export class AUNChannel {
|
|
|
1800
2027
|
isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
|
|
1801
2028
|
source: isOwnHandoff ? 'handoff' : undefined,
|
|
1802
2029
|
encrypted: msgEncrypted,
|
|
1803
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
|
|
1804
|
-
|
|
2030
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
|
|
2031
|
+
mentionMode: serverMentionMode,
|
|
1805
2032
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1806
2033
|
msgType: logDescriptor.msgType,
|
|
1807
2034
|
payloadType: logDescriptor.payloadType,
|
|
@@ -1810,17 +2037,22 @@ export class AUNChannel {
|
|
|
1810
2037
|
});
|
|
1811
2038
|
}
|
|
1812
2039
|
dispatchMessage(event) {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2040
|
+
const authenticatedDelivery = event.chatType === 'group'
|
|
2041
|
+
? { chatType: 'group', groupId: event.groupId ?? event.channelId }
|
|
2042
|
+
: { chatType: 'private' };
|
|
2043
|
+
const suppliedDelivery = event.replyContext?.delivery;
|
|
2044
|
+
const suppliedMatches = suppliedDelivery?.chatType === 'private'
|
|
2045
|
+
? authenticatedDelivery.chatType === 'private'
|
|
2046
|
+
: suppliedDelivery?.chatType === 'group'
|
|
2047
|
+
&& authenticatedDelivery.chatType === 'group'
|
|
2048
|
+
&& suppliedDelivery.groupId === authenticatedDelivery.groupId;
|
|
2049
|
+
const delivery = suppliedDelivery && suppliedMatches ? suppliedDelivery : authenticatedDelivery;
|
|
2050
|
+
if (suppliedDelivery && delivery !== suppliedDelivery) {
|
|
2051
|
+
logger.warn(`${this.logPrefix()} Ignoring mismatched inbound reply route: channel=${event.channelId} chatType=${event.chatType}`);
|
|
2052
|
+
}
|
|
2053
|
+
// Inbound events are claimed in handleIncomingPrivateMessage / GroupMessage
|
|
2054
|
+
// before parsing. Keeping dedupe there prevents duplicate slash commands
|
|
2055
|
+
// from reaching routing and mention policy code.
|
|
1824
2056
|
// Echo 机制:消息第一行包含 "echo"(不区分大小写)且原始内容 ≤10 字符时,直接回声
|
|
1825
2057
|
// 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1826
2058
|
const firstLine = event.text.split('\n')[0] || '';
|
|
@@ -1845,20 +2077,24 @@ export class AUNChannel {
|
|
|
1845
2077
|
this.pendingEchoMessages.set(event.messageId, {
|
|
1846
2078
|
text: echoText,
|
|
1847
2079
|
channelId: event.channelId,
|
|
1848
|
-
context:
|
|
2080
|
+
context: {
|
|
2081
|
+
...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
|
|
2082
|
+
delivery,
|
|
2083
|
+
},
|
|
1849
2084
|
receiveTs: Date.now(),
|
|
1850
2085
|
});
|
|
1851
2086
|
logger.info(`${this.logPrefix()} [Echo] long echo stored: mid=${event.messageId} channelId=${event.channelId}`);
|
|
1852
2087
|
}
|
|
1853
2088
|
if (!this.messageHandler)
|
|
1854
2089
|
return;
|
|
1855
|
-
|
|
1856
|
-
//
|
|
1857
|
-
//
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2090
|
+
// Preserve the caller-supplied context (group paths add mention fields),
|
|
2091
|
+
// but always attach the route derived from this authenticated event when
|
|
2092
|
+
// a synthetic/test event omitted it. Never infer the route from the AID.
|
|
2093
|
+
const replyContext = {
|
|
2094
|
+
...(event.replyContext ?? {}),
|
|
2095
|
+
delivery,
|
|
2096
|
+
...(event.threadId && !event.replyContext?.threadId ? { threadId: event.threadId } : {}),
|
|
2097
|
+
};
|
|
1862
2098
|
this.messageHandler({
|
|
1863
2099
|
channelId: event.channelId || '',
|
|
1864
2100
|
channelType: 'aun',
|
|
@@ -1875,14 +2111,16 @@ export class AUNChannel {
|
|
|
1875
2111
|
encrypted: event.encrypted,
|
|
1876
2112
|
protectedHeaders: event.protectedHeaders,
|
|
1877
2113
|
messageId: event.messageId,
|
|
2114
|
+
receivedAt: event.receivedAt,
|
|
2115
|
+
gatewaySeq: event.gatewaySeq ?? event.seq,
|
|
1878
2116
|
causation: event.causation,
|
|
1879
2117
|
threadId: event.threadId,
|
|
1880
|
-
mentions:
|
|
2118
|
+
mentions: event.mentions,
|
|
1881
2119
|
mentionAids: event.mentionAids,
|
|
1882
2120
|
isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
|
|
1883
2121
|
replyContext,
|
|
1884
2122
|
source: event.source,
|
|
1885
|
-
|
|
2123
|
+
mentionMode: event.mentionMode,
|
|
1886
2124
|
images: event.images,
|
|
1887
2125
|
msgType: event.msgType,
|
|
1888
2126
|
payloadType: event.payloadType,
|
|
@@ -1892,7 +2130,7 @@ export class AUNChannel {
|
|
|
1892
2130
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
1893
2131
|
});
|
|
1894
2132
|
}
|
|
1895
|
-
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId) {
|
|
2133
|
+
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
|
|
1896
2134
|
try {
|
|
1897
2135
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
1898
2136
|
appendMessageLog(chatDir, buildInboundEntry({
|
|
@@ -1907,6 +2145,9 @@ export class AUNChannel {
|
|
|
1907
2145
|
msgType: descriptor.msgType,
|
|
1908
2146
|
payloadType: descriptor.payloadType,
|
|
1909
2147
|
payloadSummary: descriptor.payloadSummary,
|
|
2148
|
+
timestamp: receivedAt,
|
|
2149
|
+
receivedAt,
|
|
2150
|
+
gatewaySeq,
|
|
1910
2151
|
}));
|
|
1911
2152
|
}
|
|
1912
2153
|
catch (e) {
|
|
@@ -2110,7 +2351,14 @@ export class AUNChannel {
|
|
|
2110
2351
|
}
|
|
2111
2352
|
const replyTarget = event.channelId;
|
|
2112
2353
|
// 不传 peerId,避免 sendMessage 在头部追加 @peer
|
|
2113
|
-
const
|
|
2354
|
+
const authenticatedDelivery = event.chatType === 'group'
|
|
2355
|
+
? { chatType: 'group', groupId: event.channelId }
|
|
2356
|
+
: { chatType: 'private' };
|
|
2357
|
+
const context = {
|
|
2358
|
+
threadId: event.threadId,
|
|
2359
|
+
metadata: event.replyContext?.metadata,
|
|
2360
|
+
delivery: authenticatedDelivery,
|
|
2361
|
+
};
|
|
2114
2362
|
const sendStart = Date.now();
|
|
2115
2363
|
this.isEchoSending = true;
|
|
2116
2364
|
this.sendMessage(replyTarget, echoText, context).then(() => {
|
|
@@ -2421,8 +2669,8 @@ export class AUNChannel {
|
|
|
2421
2669
|
onMessage(handler) {
|
|
2422
2670
|
this.messageHandler = handler;
|
|
2423
2671
|
}
|
|
2424
|
-
|
|
2425
|
-
this.
|
|
2672
|
+
setMentionModeResolver(resolver) {
|
|
2673
|
+
this.mentionModeResolver = resolver;
|
|
2426
2674
|
}
|
|
2427
2675
|
onRecall(handler) {
|
|
2428
2676
|
this.recallHandler = handler;
|
|
@@ -2511,19 +2759,38 @@ export class AUNChannel {
|
|
|
2511
2759
|
finalPayload.chatmode = context.metadata.chatmode;
|
|
2512
2760
|
return finalPayload;
|
|
2513
2761
|
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Validate the AUN-specific structured field before it crosses the durable
|
|
2764
|
+
* send boundary. Return a copy when mentions are present so callers do not
|
|
2765
|
+
* retain an unvalidated array reference.
|
|
2766
|
+
*/
|
|
2767
|
+
normalizeAunPayloadMentions(payload) {
|
|
2768
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
2769
|
+
return payload;
|
|
2770
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
|
|
2771
|
+
return payload;
|
|
2772
|
+
return {
|
|
2773
|
+
...payload,
|
|
2774
|
+
mentions: normalizeAunMentionEntries(payload.mentions),
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2514
2777
|
rememberInvalidatedInteraction(interactionId, reason) {
|
|
2515
2778
|
this.invalidatedInteractions.set(interactionId, reason);
|
|
2516
2779
|
const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
|
|
2517
2780
|
timer.unref?.();
|
|
2518
2781
|
}
|
|
2519
|
-
async recallInteractionCard(messageId, channelId) {
|
|
2782
|
+
async recallInteractionCard(messageId, channelId, delivery) {
|
|
2520
2783
|
if (!this.client || !this.connected)
|
|
2521
2784
|
return false;
|
|
2522
2785
|
try {
|
|
2523
|
-
|
|
2786
|
+
if (!channelId || !delivery) {
|
|
2787
|
+
logger.warn(`${this.logPrefix()} Cannot recall interaction card without trusted delivery route: mid=${messageId}`);
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
const isGroup = delivery.chatType === 'group';
|
|
2524
2791
|
const method = isGroup ? 'group.recall' : 'message.recall';
|
|
2525
2792
|
const params = isGroup
|
|
2526
|
-
? { group_id:
|
|
2793
|
+
? { group_id: delivery.groupId, message_ids: [messageId] }
|
|
2527
2794
|
: { message_ids: [messageId] };
|
|
2528
2795
|
await this.callAndTrace(method, params, { silentError: true });
|
|
2529
2796
|
return true;
|
|
@@ -2546,9 +2813,10 @@ export class AUNChannel {
|
|
|
2546
2813
|
}
|
|
2547
2814
|
const messageIds = [...(this.interactionCardMessageIds.get(interactionId) ?? [])];
|
|
2548
2815
|
for (const messageId of messageIds) {
|
|
2549
|
-
const
|
|
2816
|
+
const card = this.cardMessageIdMap.get(messageId);
|
|
2817
|
+
const channelId = card?.channelId;
|
|
2550
2818
|
this.forgetInteractionCard(messageId, interactionId);
|
|
2551
|
-
await this.recallInteractionCard(messageId, channelId);
|
|
2819
|
+
await this.recallInteractionCard(messageId, channelId, card?.delivery);
|
|
2552
2820
|
}
|
|
2553
2821
|
}
|
|
2554
2822
|
forgetInteractionCard(messageId, interactionId) {
|
|
@@ -2558,7 +2826,7 @@ export class AUNChannel {
|
|
|
2558
2826
|
if (ids && ids.size === 0)
|
|
2559
2827
|
this.interactionCardMessageIds.delete(interactionId);
|
|
2560
2828
|
}
|
|
2561
|
-
registerCardPostSend(messageId, action, channelId) {
|
|
2829
|
+
registerCardPostSend(messageId, action, channelId, delivery) {
|
|
2562
2830
|
if (action.type !== 'register_interaction_card')
|
|
2563
2831
|
return;
|
|
2564
2832
|
this.cardMessageIdMap.set(messageId, {
|
|
@@ -2566,6 +2834,7 @@ export class AUNChannel {
|
|
|
2566
2834
|
isCommandCard: action.isCommandCard,
|
|
2567
2835
|
initiatorAid: action.initiatorAid,
|
|
2568
2836
|
channelId,
|
|
2837
|
+
delivery: delivery ?? action.delivery,
|
|
2569
2838
|
});
|
|
2570
2839
|
this.ownedCardMsgIds.add(messageId);
|
|
2571
2840
|
let messageIds = this.interactionCardMessageIds.get(action.requestId);
|
|
@@ -2591,20 +2860,31 @@ export class AUNChannel {
|
|
|
2591
2860
|
const invalidationReason = this.invalidatedInteractions.get(action.requestId);
|
|
2592
2861
|
if (invalidationReason) {
|
|
2593
2862
|
this.cardMessageIdMap.delete(messageId);
|
|
2594
|
-
void this.recallInteractionCard(messageId, channelId);
|
|
2863
|
+
void this.recallInteractionCard(messageId, channelId, delivery ?? action.delivery);
|
|
2595
2864
|
}
|
|
2596
2865
|
}
|
|
2597
2866
|
runPostSend(entry, messageId) {
|
|
2598
2867
|
if (!entry.postSend)
|
|
2599
2868
|
return;
|
|
2600
2869
|
if (entry.postSend.type === 'register_interaction_card') {
|
|
2601
|
-
this.registerCardPostSend(messageId, entry.postSend, entry.channelId);
|
|
2870
|
+
this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery);
|
|
2602
2871
|
}
|
|
2603
2872
|
}
|
|
2604
2873
|
async sendAunPayload(channelId, payload, context, label) {
|
|
2874
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
2875
|
+
let normalizedPayload;
|
|
2876
|
+
try {
|
|
2877
|
+
normalizedPayload = this.normalizeAunPayloadMentions(payload);
|
|
2878
|
+
}
|
|
2879
|
+
catch (error) {
|
|
2880
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2881
|
+
const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
|
|
2882
|
+
logger.error(`${this.logPrefix()} Dropped durable AUN payload: invalid payload.mentions (${detail}) code=${code} channel=${channelId}`);
|
|
2883
|
+
return { ok: false, discarded: true };
|
|
2884
|
+
}
|
|
2605
2885
|
if (!this.client || !this.connected)
|
|
2606
2886
|
return { ok: false };
|
|
2607
|
-
const isGroup =
|
|
2887
|
+
const isGroup = delivery.chatType === 'group';
|
|
2608
2888
|
const targetAid = channelId;
|
|
2609
2889
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
2610
2890
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -2616,13 +2896,13 @@ export class AUNChannel {
|
|
|
2616
2896
|
if (causationRef && causation)
|
|
2617
2897
|
registerAunCausation(causationRef, this.config.aid, targetAid, causation);
|
|
2618
2898
|
const payloadWithCausationRef = causationRef
|
|
2619
|
-
? { ...
|
|
2620
|
-
:
|
|
2899
|
+
? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
|
|
2900
|
+
: normalizedPayload;
|
|
2621
2901
|
// Truncate payload if too large (AUN SDK limit: 1MB)
|
|
2622
2902
|
const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
|
|
2623
2903
|
const params = { payload: truncatedPayload, encrypt };
|
|
2624
2904
|
if (isGroup)
|
|
2625
|
-
params.group_id =
|
|
2905
|
+
params.group_id = delivery.groupId;
|
|
2626
2906
|
else
|
|
2627
2907
|
params.to = targetAid;
|
|
2628
2908
|
const callOnce = async (sendParams, fallback) => {
|
|
@@ -2692,6 +2972,11 @@ export class AUNChannel {
|
|
|
2692
2972
|
this.forwardOutbound(result);
|
|
2693
2973
|
}
|
|
2694
2974
|
async sendContentPayload(channelId, payload, opts) {
|
|
2975
|
+
const delivery = this.requireDelivery(channelId, opts.context);
|
|
2976
|
+
const context = this.withDelivery(opts.context, delivery);
|
|
2977
|
+
// Validate the caller's raw mentions before any early-return policy (such
|
|
2978
|
+
// as a cancelled interaction) can hide a malformed wire payload.
|
|
2979
|
+
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
2695
2980
|
const interactionId = opts.postSend?.type === 'register_interaction_card'
|
|
2696
2981
|
? opts.postSend.requestId
|
|
2697
2982
|
: undefined;
|
|
@@ -2699,14 +2984,20 @@ export class AUNChannel {
|
|
|
2699
2984
|
logger.info(`${this.logPrefix()} Dropped invalidated interaction before durable enqueue: request=${interactionId}`);
|
|
2700
2985
|
return {};
|
|
2701
2986
|
}
|
|
2702
|
-
|
|
2987
|
+
// Validate before write-ahead enqueue. Invalid AUN payloads must not be
|
|
2988
|
+
// hidden in the outbox and emitted later after a reconnect.
|
|
2989
|
+
// Validate the caller's raw mentions before deep-cleaning: that helper
|
|
2990
|
+
// intentionally removes undefined array elements, which would otherwise
|
|
2991
|
+
// turn malformed `mentions: [undefined]` into an apparently valid `[]`.
|
|
2992
|
+
const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
|
|
2703
2993
|
const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
|
|
2704
2994
|
const entry = outbox.enqueue(this.config.aid, {
|
|
2705
2995
|
channelId,
|
|
2996
|
+
delivery,
|
|
2706
2997
|
type: 'payload',
|
|
2707
2998
|
contentKind: opts.contentKind,
|
|
2708
2999
|
payload: finalPayload,
|
|
2709
|
-
context
|
|
3000
|
+
context,
|
|
2710
3001
|
logText,
|
|
2711
3002
|
ttl: opts.ttl,
|
|
2712
3003
|
postSend: opts.postSend,
|
|
@@ -2853,19 +3144,39 @@ export class AUNChannel {
|
|
|
2853
3144
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
2854
3145
|
return;
|
|
2855
3146
|
}
|
|
3147
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
3148
|
+
const routedContext = this.withDelivery(context, delivery);
|
|
2856
3149
|
// 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
|
|
2857
3150
|
if (!this.isEchoSending) {
|
|
2858
|
-
await this.flushPendingEcho(channelId);
|
|
3151
|
+
await this.flushPendingEcho(channelId, delivery);
|
|
2859
3152
|
}
|
|
2860
3153
|
let finalText = text;
|
|
2861
3154
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
2862
|
-
if (
|
|
2863
|
-
|
|
2864
|
-
|
|
3155
|
+
if (delivery.chatType === 'group') {
|
|
3156
|
+
const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
|
|
3157
|
+
.filter((id) => typeof id === 'string' && id.trim().length > 0)
|
|
3158
|
+
.map(id => id.trim())));
|
|
3159
|
+
// AUN's textual representation gives @all precedence over individual
|
|
3160
|
+
// targets, matching the cross-channel mention contract.
|
|
3161
|
+
const textMentionsAll = this.hasExplicitMention(finalText, 'all');
|
|
3162
|
+
const mentionTargets = requestedMentions.includes('all') || textMentionsAll
|
|
3163
|
+
? ['all']
|
|
3164
|
+
: requestedMentions;
|
|
3165
|
+
if (mentionTargets.length > 0) {
|
|
3166
|
+
for (const target of mentionTargets.reverse()) {
|
|
3167
|
+
if (!this.hasExplicitMention(finalText, target)) {
|
|
3168
|
+
finalText = `@${target} ` + finalText;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
else if (context?.peerId && !context.suppressPeerMention && !textMentionsAll) {
|
|
3173
|
+
if (!this.hasExplicitMention(finalText, context.peerId)) {
|
|
3174
|
+
finalText = `@${context.peerId} ` + finalText;
|
|
3175
|
+
}
|
|
2865
3176
|
}
|
|
2866
3177
|
}
|
|
2867
|
-
const operationId = typeof
|
|
2868
|
-
?
|
|
3178
|
+
const operationId = typeof routedContext.metadata?.operationId === 'string'
|
|
3179
|
+
? routedContext.metadata.operationId
|
|
2869
3180
|
: undefined;
|
|
2870
3181
|
if (operationId) {
|
|
2871
3182
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
@@ -2875,13 +3186,14 @@ export class AUNChannel {
|
|
|
2875
3186
|
// Write-ahead: persist to outbox before attempting send
|
|
2876
3187
|
const entry = outbox.enqueue(this.config.aid, {
|
|
2877
3188
|
channelId,
|
|
3189
|
+
delivery,
|
|
2878
3190
|
dedupeKey: operationId,
|
|
2879
|
-
critical:
|
|
3191
|
+
critical: routedContext.metadata?.criticalDelivery === true,
|
|
2880
3192
|
type: 'text',
|
|
2881
3193
|
text: finalText,
|
|
2882
|
-
context,
|
|
2883
|
-
ttl: typeof
|
|
2884
|
-
?
|
|
3194
|
+
context: routedContext,
|
|
3195
|
+
ttl: typeof routedContext.metadata?.outboxTtl === 'number'
|
|
3196
|
+
? routedContext.metadata.outboxTtl
|
|
2885
3197
|
: undefined,
|
|
2886
3198
|
});
|
|
2887
3199
|
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
@@ -2905,16 +3217,27 @@ export class AUNChannel {
|
|
|
2905
3217
|
}
|
|
2906
3218
|
/** Daemon-side transport for `ec msg send` running inside an agent task. */
|
|
2907
3219
|
async sendDaemonMsg(args) {
|
|
2908
|
-
if (!
|
|
2909
|
-
return { ok: false, error: '
|
|
2910
|
-
}
|
|
2911
|
-
if (!args.to || this.isGroupId(args.to)) {
|
|
2912
|
-
return { ok: false, error: 'aun-msg-send only supports private AID targets', code: 'UNSUPPORTED_TARGET' };
|
|
3220
|
+
if (!args.to) {
|
|
3221
|
+
return { ok: false, error: 'aun-msg-send requires a private target', code: 'UNSUPPORTED_TARGET' };
|
|
2913
3222
|
}
|
|
2914
3223
|
if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
|
|
2915
3224
|
return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
|
|
2916
3225
|
}
|
|
2917
|
-
|
|
3226
|
+
let finalPayload;
|
|
3227
|
+
try {
|
|
3228
|
+
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
3229
|
+
finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
3230
|
+
}
|
|
3231
|
+
catch (e) {
|
|
3232
|
+
return {
|
|
3233
|
+
ok: false,
|
|
3234
|
+
error: e?.message || String(e),
|
|
3235
|
+
code: e?.code,
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
if (!this.connected || !this.client) {
|
|
3239
|
+
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3240
|
+
}
|
|
2918
3241
|
const info = await this.fetchPeerInfo(args.to);
|
|
2919
3242
|
const chatmode = info.type === 'human' ? 'interactive' : 'proactive';
|
|
2920
3243
|
if (!finalPayload.chatmode)
|
|
@@ -3019,20 +3342,34 @@ export class AUNChannel {
|
|
|
3019
3342
|
}
|
|
3020
3343
|
/** Daemon-side transport for `ec group send` running inside an agent task. */
|
|
3021
3344
|
async sendDaemonGroupMsg(args) {
|
|
3022
|
-
if (!
|
|
3023
|
-
return { ok: false, error: '
|
|
3024
|
-
}
|
|
3025
|
-
if (!args.groupId || !this.isGroupId(args.groupId)) {
|
|
3026
|
-
return { ok: false, error: 'aun-msg-send requires a valid group target', code: 'UNSUPPORTED_TARGET' };
|
|
3345
|
+
if (!args.groupId) {
|
|
3346
|
+
return { ok: false, error: 'aun group send requires a group target', code: 'UNSUPPORTED_TARGET' };
|
|
3027
3347
|
}
|
|
3028
3348
|
if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
|
|
3029
3349
|
return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
|
|
3030
3350
|
}
|
|
3031
|
-
const finalPayload = this.stripUndefinedDeep({ ...args.payload });
|
|
3032
|
-
if (args.mentions?.length)
|
|
3033
|
-
finalPayload.mentions = args.mentions;
|
|
3034
|
-
const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
|
|
3035
3351
|
try {
|
|
3352
|
+
// Validate before deep-cleaning so undefined entries cannot be silently
|
|
3353
|
+
// removed from a malformed mentions array.
|
|
3354
|
+
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
3355
|
+
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
3356
|
+
const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
|
|
3357
|
+
? normalizeAunMentionEntries(finalPayload.mentions)
|
|
3358
|
+
: undefined;
|
|
3359
|
+
if (args.mentions !== undefined) {
|
|
3360
|
+
const normalizedMentions = normalizeAunMentionEntries(args.mentions);
|
|
3361
|
+
if (normalizedMentions.length > 0)
|
|
3362
|
+
finalPayload.mentions = normalizedMentions;
|
|
3363
|
+
else if (payloadMentions !== undefined)
|
|
3364
|
+
finalPayload.mentions = payloadMentions;
|
|
3365
|
+
}
|
|
3366
|
+
else if (payloadMentions !== undefined) {
|
|
3367
|
+
finalPayload.mentions = payloadMentions;
|
|
3368
|
+
}
|
|
3369
|
+
if (!this.connected || !this.client) {
|
|
3370
|
+
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3371
|
+
}
|
|
3372
|
+
const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
|
|
3036
3373
|
const result = await this.callAndTrace('group.send', {
|
|
3037
3374
|
group_id: args.groupId,
|
|
3038
3375
|
payload: this.truncatePayloadIfNeeded(finalPayload, 'group-send-ipc'),
|
|
@@ -3090,7 +3427,7 @@ export class AUNChannel {
|
|
|
3090
3427
|
return { ok: false, error: e?.message || String(e), code: e?.code };
|
|
3091
3428
|
}
|
|
3092
3429
|
}
|
|
3093
|
-
async flushPendingEcho(channelId) {
|
|
3430
|
+
async flushPendingEcho(channelId, delivery) {
|
|
3094
3431
|
// 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
|
|
3095
3432
|
for (const [key, echo] of this.pendingEchoMessages) {
|
|
3096
3433
|
if (echo.channelId === channelId) {
|
|
@@ -3114,18 +3451,32 @@ export class AUNChannel {
|
|
|
3114
3451
|
ts: Date.now(),
|
|
3115
3452
|
aid: this.config.aid,
|
|
3116
3453
|
channelId,
|
|
3454
|
+
delivery,
|
|
3117
3455
|
type: 'text',
|
|
3118
3456
|
text: echoText,
|
|
3457
|
+
context: this.withDelivery(echo.context, delivery),
|
|
3119
3458
|
ttl: 300_000,
|
|
3120
3459
|
};
|
|
3121
3460
|
const ok = await this.deliverTextEntry(echoEntry);
|
|
3122
3461
|
if (!ok) {
|
|
3123
|
-
outbox.enqueue(this.config.aid, {
|
|
3462
|
+
outbox.enqueue(this.config.aid, {
|
|
3463
|
+
channelId,
|
|
3464
|
+
delivery,
|
|
3465
|
+
type: 'text',
|
|
3466
|
+
text: echoText,
|
|
3467
|
+
context: this.withDelivery(echo.context, delivery),
|
|
3468
|
+
});
|
|
3124
3469
|
}
|
|
3125
3470
|
logger.info(`${this.logPrefix()} [Echo] long echo trace delivered=${ok} to ${channelId} (agent ${agentDuration}ms)`);
|
|
3126
3471
|
}
|
|
3127
3472
|
else {
|
|
3128
|
-
outbox.enqueue(this.config.aid, {
|
|
3473
|
+
outbox.enqueue(this.config.aid, {
|
|
3474
|
+
channelId,
|
|
3475
|
+
delivery,
|
|
3476
|
+
type: 'text',
|
|
3477
|
+
text: echoText,
|
|
3478
|
+
context: this.withDelivery(echo.context, delivery),
|
|
3479
|
+
});
|
|
3129
3480
|
logger.warn(`${this.logPrefix()} [Echo] not connected, echo trace queued in outbox`);
|
|
3130
3481
|
}
|
|
3131
3482
|
break;
|
|
@@ -3135,7 +3486,8 @@ export class AUNChannel {
|
|
|
3135
3486
|
async deliverTextEntry(entry) {
|
|
3136
3487
|
const channelId = entry.channelId;
|
|
3137
3488
|
const finalText = entry.text;
|
|
3138
|
-
const
|
|
3489
|
+
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
3490
|
+
const context = this.withDelivery(entry.context, delivery);
|
|
3139
3491
|
const operationId = entry.dedupeKey
|
|
3140
3492
|
|| (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
|
|
3141
3493
|
if (operationId) {
|
|
@@ -3153,8 +3505,8 @@ export class AUNChannel {
|
|
|
3153
3505
|
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
3154
3506
|
const source = context?.metadata?.source ?? 'daemon';
|
|
3155
3507
|
const payload = { type: 'text', text: finalText };
|
|
3156
|
-
if (
|
|
3157
|
-
const extracted = this.
|
|
3508
|
+
if (delivery.chatType === 'group') {
|
|
3509
|
+
const extracted = this.extractMentionEntriesFromText(finalText);
|
|
3158
3510
|
if (extracted.length > 0)
|
|
3159
3511
|
payload.mentions = extracted;
|
|
3160
3512
|
}
|
|
@@ -3167,7 +3519,7 @@ export class AUNChannel {
|
|
|
3167
3519
|
if (context?.metadata?.chatmode)
|
|
3168
3520
|
payload.chatmode = context.metadata.chatmode;
|
|
3169
3521
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
3170
|
-
const causationRef =
|
|
3522
|
+
const causationRef = delivery.chatType !== 'group' && causation
|
|
3171
3523
|
? `caus_${crypto.randomUUID().replace(/-/g, '')}`
|
|
3172
3524
|
: undefined;
|
|
3173
3525
|
if (causationRef && causation) {
|
|
@@ -3176,7 +3528,7 @@ export class AUNChannel {
|
|
|
3176
3528
|
}
|
|
3177
3529
|
// 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
|
|
3178
3530
|
logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
|
|
3179
|
-
const isGroup =
|
|
3531
|
+
const isGroup = delivery.chatType === 'group';
|
|
3180
3532
|
const targetAid = channelId;
|
|
3181
3533
|
if (operationId && entry.deliveryReceipt) {
|
|
3182
3534
|
this.appendOutboundJsonl(channelId, {
|
|
@@ -3350,12 +3702,17 @@ export class AUNChannel {
|
|
|
3350
3702
|
}
|
|
3351
3703
|
const contentKind = entry.contentKind;
|
|
3352
3704
|
const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
|
|
3353
|
-
const
|
|
3705
|
+
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
3706
|
+
const context = this.withDelivery(entry.context, delivery);
|
|
3354
3707
|
logger.info(`${this.logPrefix()} deliverPayloadEntry: id=${entry.id} kind=${contentKind ?? payload.type ?? 'payload'} channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} textLen=${logText.length}`);
|
|
3355
3708
|
const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
|
|
3709
|
+
if (sent.discarded) {
|
|
3710
|
+
logger.warn(`${this.logPrefix()} Removed malformed durable payload from outbox: id=${entry.id} channel=${channelId}`);
|
|
3711
|
+
return { ok: true, discarded: true };
|
|
3712
|
+
}
|
|
3356
3713
|
if (!sent.ok || !sent.messageId)
|
|
3357
3714
|
return sent;
|
|
3358
|
-
const isGroup =
|
|
3715
|
+
const isGroup = delivery.chatType === 'group';
|
|
3359
3716
|
const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
|
|
3360
3717
|
this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
|
|
3361
3718
|
this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
|
|
@@ -3415,13 +3772,16 @@ export class AUNChannel {
|
|
|
3415
3772
|
* 存储键:group_id/peer_aid + sender_aid + context.type + context.id
|
|
3416
3773
|
*/
|
|
3417
3774
|
async sendThought(channelId, taskId, payload, context) {
|
|
3418
|
-
if (!this.connected || !this.client)
|
|
3419
|
-
return;
|
|
3420
3775
|
if (!taskId)
|
|
3421
3776
|
return;
|
|
3777
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
3778
|
+
context = this.withDelivery(context, delivery);
|
|
3779
|
+
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
3780
|
+
const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
|
|
3781
|
+
if (!this.connected || !this.client)
|
|
3782
|
+
return;
|
|
3422
3783
|
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
3423
3784
|
const targetId = channelId;
|
|
3424
|
-
const finalPayload = this.stripUndefinedDeep(payload);
|
|
3425
3785
|
const encrypt = context?.metadata?.encrypted != null
|
|
3426
3786
|
? !!(context.metadata.encrypted)
|
|
3427
3787
|
: this.shouldEncrypt(targetId);
|
|
@@ -3460,8 +3820,8 @@ export class AUNChannel {
|
|
|
3460
3820
|
else {
|
|
3461
3821
|
thoughtText = this.activityLogText(finalPayload);
|
|
3462
3822
|
}
|
|
3463
|
-
if (
|
|
3464
|
-
params.group_id =
|
|
3823
|
+
if (delivery.chatType === 'group') {
|
|
3824
|
+
params.group_id = delivery.groupId;
|
|
3465
3825
|
const putRes = await this.callAndTrace('group.thought.put', params);
|
|
3466
3826
|
const tid = putRes?.thought_id;
|
|
3467
3827
|
logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
@@ -3491,15 +3851,20 @@ export class AUNChannel {
|
|
|
3491
3851
|
}
|
|
3492
3852
|
/** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
|
|
3493
3853
|
async sendStructured(channelId, payload, context) {
|
|
3854
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
3855
|
+
context = this.withDelivery(context, delivery);
|
|
3856
|
+
// Validate before checking connection state so malformed producer payloads
|
|
3857
|
+
// are reported deterministically instead of being hidden by a disconnect.
|
|
3858
|
+
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
3859
|
+
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
3494
3860
|
if (!this.connected || !this.client)
|
|
3495
3861
|
return null;
|
|
3496
|
-
const isGroup =
|
|
3862
|
+
const isGroup = delivery.chatType === 'group';
|
|
3497
3863
|
const targetAid = channelId;
|
|
3498
3864
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
3499
3865
|
const encrypt = context?.metadata?.encrypted != null
|
|
3500
3866
|
? !!(context.metadata.encrypted)
|
|
3501
3867
|
: this.shouldEncrypt(encryptTarget);
|
|
3502
|
-
const finalPayload = this.stripUndefinedDeep({ ...payload });
|
|
3503
3868
|
if (context?.threadId && !finalPayload.thread_id)
|
|
3504
3869
|
finalPayload.thread_id = context.threadId;
|
|
3505
3870
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
@@ -3520,7 +3885,7 @@ export class AUNChannel {
|
|
|
3520
3885
|
}
|
|
3521
3886
|
try {
|
|
3522
3887
|
if (isGroup) {
|
|
3523
|
-
params.group_id =
|
|
3888
|
+
params.group_id = delivery.groupId;
|
|
3524
3889
|
const result = await this.callAndTrace('group.send', params);
|
|
3525
3890
|
const mid = result?.message?.message_id ?? result?.message_id ?? null;
|
|
3526
3891
|
logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
|
|
@@ -3544,6 +3909,8 @@ export class AUNChannel {
|
|
|
3544
3909
|
}
|
|
3545
3910
|
}
|
|
3546
3911
|
async sendFile(channelId, filePath, context) {
|
|
3912
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
3913
|
+
context = this.withDelivery(context, delivery);
|
|
3547
3914
|
const absPath = path.resolve(filePath);
|
|
3548
3915
|
if (!fs.existsSync(absPath)) {
|
|
3549
3916
|
logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
|
|
@@ -3561,6 +3928,7 @@ export class AUNChannel {
|
|
|
3561
3928
|
// Write-ahead: persist to outbox
|
|
3562
3929
|
const entry = outbox.enqueue(this.config.aid, {
|
|
3563
3930
|
channelId,
|
|
3931
|
+
delivery,
|
|
3564
3932
|
type: 'file',
|
|
3565
3933
|
filePath: absPath,
|
|
3566
3934
|
context,
|
|
@@ -3579,6 +3947,8 @@ export class AUNChannel {
|
|
|
3579
3947
|
}
|
|
3580
3948
|
}
|
|
3581
3949
|
async sendImage(channelId, data, mimeType, alt, context) {
|
|
3950
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
3951
|
+
context = this.withDelivery(context, delivery);
|
|
3582
3952
|
if (!Buffer.isBuffer(data)) {
|
|
3583
3953
|
throw new Error('result.image data must be a Buffer');
|
|
3584
3954
|
}
|
|
@@ -3600,6 +3970,7 @@ export class AUNChannel {
|
|
|
3600
3970
|
const logText = alt ? `[image] ${alt}` : '[image]';
|
|
3601
3971
|
const entry = outbox.enqueue(this.config.aid, {
|
|
3602
3972
|
channelId,
|
|
3973
|
+
delivery,
|
|
3603
3974
|
type: 'image',
|
|
3604
3975
|
contentKind: 'image',
|
|
3605
3976
|
image: { dataBase64: data.toString('base64'), mimeType: normalizedMimeType, alt },
|
|
@@ -3658,6 +4029,9 @@ export class AUNChannel {
|
|
|
3658
4029
|
logger.warn(`${this.logPrefix()} deliverImageEntry: missing image data (outbox id=${entry.id})`);
|
|
3659
4030
|
return true;
|
|
3660
4031
|
}
|
|
4032
|
+
const delivery = this.requirePersistedDelivery(entry.channelId, entry.delivery);
|
|
4033
|
+
entry.delivery = delivery;
|
|
4034
|
+
entry.context = this.withDelivery(entry.context, delivery);
|
|
3661
4035
|
const data = Buffer.from(image.dataBase64, 'base64');
|
|
3662
4036
|
if (data.length === 0) {
|
|
3663
4037
|
logger.warn(`${this.logPrefix()} deliverImageEntry: invalid image data (outbox id=${entry.id})`);
|
|
@@ -3675,6 +4049,7 @@ export class AUNChannel {
|
|
|
3675
4049
|
if (!outbox.replace(this.config.aid, entry)) {
|
|
3676
4050
|
payloadEntry = outbox.enqueue(this.config.aid, {
|
|
3677
4051
|
channelId: entry.channelId,
|
|
4052
|
+
delivery,
|
|
3678
4053
|
type: 'payload',
|
|
3679
4054
|
contentKind: 'image',
|
|
3680
4055
|
payload: entry.payload,
|
|
@@ -3692,7 +4067,8 @@ export class AUNChannel {
|
|
|
3692
4067
|
async deliverFileEntry(entry) {
|
|
3693
4068
|
const channelId = entry.channelId;
|
|
3694
4069
|
const absPath = entry.filePath;
|
|
3695
|
-
const
|
|
4070
|
+
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
4071
|
+
const context = this.withDelivery(entry.context, delivery);
|
|
3696
4072
|
if (!fs.existsSync(absPath)) {
|
|
3697
4073
|
logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
|
|
3698
4074
|
return true; // remove from outbox, file no longer exists
|
|
@@ -3750,7 +4126,7 @@ export class AUNChannel {
|
|
|
3750
4126
|
attachment,
|
|
3751
4127
|
context,
|
|
3752
4128
|
});
|
|
3753
|
-
const isGroup =
|
|
4129
|
+
const isGroup = delivery.chatType === 'group';
|
|
3754
4130
|
const fileTargetAid = channelId;
|
|
3755
4131
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
3756
4132
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -3761,7 +4137,7 @@ export class AUNChannel {
|
|
|
3761
4137
|
let sentMid = null;
|
|
3762
4138
|
try {
|
|
3763
4139
|
if (isGroup) {
|
|
3764
|
-
params.group_id =
|
|
4140
|
+
params.group_id = delivery.groupId;
|
|
3765
4141
|
this.trace('OUT', 'group.send.file', params);
|
|
3766
4142
|
const result = await this.client.call('group.send', params);
|
|
3767
4143
|
sendResult = result;
|
|
@@ -3869,6 +4245,10 @@ export class AUNChannel {
|
|
|
3869
4245
|
return;
|
|
3870
4246
|
logger.info(`${this.logPrefix()} Draining outbox...`);
|
|
3871
4247
|
const result = await outbox.drain(this.config.aid, async (entry) => {
|
|
4248
|
+
if (!isDeliveryTarget(entry.delivery)) {
|
|
4249
|
+
logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
|
|
4250
|
+
return true;
|
|
4251
|
+
}
|
|
3872
4252
|
if (entry.type === 'text') {
|
|
3873
4253
|
return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
|
|
3874
4254
|
}
|
|
@@ -3894,11 +4274,23 @@ export class AUNChannel {
|
|
|
3894
4274
|
this.messageSeqMap.delete(messageId);
|
|
3895
4275
|
}
|
|
3896
4276
|
sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
|
|
4277
|
+
let delivery;
|
|
4278
|
+
try {
|
|
4279
|
+
delivery = this.requireDelivery(channelId, context);
|
|
4280
|
+
}
|
|
4281
|
+
catch (error) {
|
|
4282
|
+
logger.warn(`${this.logPrefix()} task.${status} notify skipped without trusted delivery route: channel=${channelId} task=${taskId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
context = this.withDelivery(context, delivery);
|
|
3897
4286
|
if (status === 'start')
|
|
3898
4287
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
3899
4288
|
if (!this.client || !this.connected)
|
|
3900
4289
|
return;
|
|
3901
|
-
const
|
|
4290
|
+
const requestedSeverity = extraMeta?.severity;
|
|
4291
|
+
const severity = requestedSeverity === 'warn'
|
|
4292
|
+
? 'warn'
|
|
4293
|
+
: status === 'error' || status === 'timeout' ? 'error' : 'info';
|
|
3902
4294
|
const stateMap = {
|
|
3903
4295
|
start: 'started',
|
|
3904
4296
|
done: 'completed',
|
|
@@ -3917,7 +4309,10 @@ export class AUNChannel {
|
|
|
3917
4309
|
severity,
|
|
3918
4310
|
terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
|
|
3919
4311
|
};
|
|
3920
|
-
const
|
|
4312
|
+
const metadataInput = extraMeta ? { ...extraMeta } : undefined;
|
|
4313
|
+
if (metadataInput)
|
|
4314
|
+
delete metadataInput.severity;
|
|
4315
|
+
const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
|
|
3921
4316
|
if (metadata && Object.keys(metadata).length > 0)
|
|
3922
4317
|
statusPayload.metadata = metadata;
|
|
3923
4318
|
if (context?.threadId)
|
|
@@ -3929,8 +4324,8 @@ export class AUNChannel {
|
|
|
3929
4324
|
if (context?.replyToMessageId)
|
|
3930
4325
|
statusPayload.ref_message_id = context.replyToMessageId;
|
|
3931
4326
|
const notifyOptions = { ttlMs: 60_000 };
|
|
3932
|
-
if (
|
|
3933
|
-
notifyOptions.groupId =
|
|
4327
|
+
if (delivery.chatType === 'group')
|
|
4328
|
+
notifyOptions.groupId = delivery.groupId;
|
|
3934
4329
|
else
|
|
3935
4330
|
notifyOptions.to = channelId;
|
|
3936
4331
|
this.trace('OUT', 'notify.task_status', {
|
|
@@ -3951,7 +4346,7 @@ export class AUNChannel {
|
|
|
3951
4346
|
}).catch(() => { });
|
|
3952
4347
|
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true, undefined, undefined, 'notify');
|
|
3953
4348
|
// 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
|
|
3954
|
-
const targetLabel =
|
|
4349
|
+
const targetLabel = delivery.chatType === 'group' ? delivery.groupId : this.peerLabel(channelId);
|
|
3955
4350
|
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
3956
4351
|
const initiator = statusPayload.initiator ?? '';
|
|
3957
4352
|
const refMsgId = statusPayload.ref_message_id ?? '';
|
|
@@ -3963,8 +4358,6 @@ export class AUNChannel {
|
|
|
3963
4358
|
logger.info(logTaskStatus);
|
|
3964
4359
|
}
|
|
3965
4360
|
async sendCustomPayload(channelId, payload, context) {
|
|
3966
|
-
if (!this.client || !this.connected)
|
|
3967
|
-
return;
|
|
3968
4361
|
// SDK 0.3.0 E2EE requires payload to be an object
|
|
3969
4362
|
let payloadObj;
|
|
3970
4363
|
try {
|
|
@@ -4198,21 +4591,25 @@ export class AUNChannelPlugin {
|
|
|
4198
4591
|
channelKey: inst.name, // channelName 实际上就是 channelKey
|
|
4199
4592
|
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true, authenticatedApproval: true },
|
|
4200
4593
|
send: async (envelope, payload) => {
|
|
4594
|
+
const channelId = envelope?.channelId;
|
|
4595
|
+
const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
|
|
4201
4596
|
const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
|
|
4202
4597
|
const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
|
|
4203
4598
|
const replyCtx = outboundCausation
|
|
4204
4599
|
? {
|
|
4205
4600
|
...(envelope.replyContext ?? {}),
|
|
4601
|
+
delivery,
|
|
4206
4602
|
metadata: { ...(envelope.replyContext?.metadata ?? {}), causation: outboundCausation },
|
|
4207
4603
|
}
|
|
4208
|
-
: envelope.replyContext
|
|
4604
|
+
: (envelope.replyContext || envelope.delivery)
|
|
4605
|
+
? { ...(envelope.replyContext ?? {}), delivery }
|
|
4606
|
+
: undefined;
|
|
4209
4607
|
if (outboundCausation) {
|
|
4210
4608
|
recordCausationSpan(outboundCausation, 'message.outbound', {
|
|
4211
4609
|
status: 'started',
|
|
4212
4610
|
refs: { taskId: envelope.taskId, sessionId: envelope.sessionId },
|
|
4213
4611
|
});
|
|
4214
4612
|
}
|
|
4215
|
-
const channelId = envelope.channelId;
|
|
4216
4613
|
const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
|
|
4217
4614
|
switch (payload.kind) {
|
|
4218
4615
|
case 'result.text':
|
|
@@ -4236,11 +4633,30 @@ export class AUNChannelPlugin {
|
|
|
4236
4633
|
return;
|
|
4237
4634
|
}
|
|
4238
4635
|
case 'system.error': {
|
|
4239
|
-
|
|
4636
|
+
const errorPayload = {
|
|
4637
|
+
type: 'error',
|
|
4638
|
+
...taskBase(),
|
|
4639
|
+
subtype: payload.subtype,
|
|
4640
|
+
text: payload.text,
|
|
4641
|
+
message: payload.text,
|
|
4642
|
+
user_message: payload.text,
|
|
4643
|
+
recoverable: payload.recoverable === true,
|
|
4644
|
+
terminal: payload.recoverable !== true,
|
|
4645
|
+
};
|
|
4646
|
+
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
4240
4647
|
return;
|
|
4241
4648
|
}
|
|
4242
4649
|
case 'result.error': {
|
|
4243
|
-
|
|
4650
|
+
const errorPayload = {
|
|
4651
|
+
type: 'error',
|
|
4652
|
+
...taskBase(),
|
|
4653
|
+
reason: payload.reason,
|
|
4654
|
+
text: payload.text,
|
|
4655
|
+
message: payload.text,
|
|
4656
|
+
user_message: payload.text,
|
|
4657
|
+
terminal: true,
|
|
4658
|
+
};
|
|
4659
|
+
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
4244
4660
|
return;
|
|
4245
4661
|
}
|
|
4246
4662
|
case 'result.file': {
|
|
@@ -4322,7 +4738,7 @@ export class AUNChannelPlugin {
|
|
|
4322
4738
|
aunCard.text = action.body;
|
|
4323
4739
|
setIfDefined(aunCard, 'format', action.bodyFormat);
|
|
4324
4740
|
}
|
|
4325
|
-
if (req.initiatorId &&
|
|
4741
|
+
if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
|
|
4326
4742
|
aunCard.initiator = req.initiatorId;
|
|
4327
4743
|
if (replyCtx?.threadId)
|
|
4328
4744
|
aunCard.thread_id = replyCtx.threadId;
|
|
@@ -4335,6 +4751,7 @@ export class AUNChannelPlugin {
|
|
|
4335
4751
|
requestId: req.id,
|
|
4336
4752
|
isCommandCard: false,
|
|
4337
4753
|
initiatorAid: req.initiatorId,
|
|
4754
|
+
delivery: replyCtx?.delivery,
|
|
4338
4755
|
expiresAt: Date.now() + cardTtlMs,
|
|
4339
4756
|
},
|
|
4340
4757
|
});
|
|
@@ -4359,7 +4776,7 @@ export class AUNChannelPlugin {
|
|
|
4359
4776
|
aunCard.text = card.body;
|
|
4360
4777
|
setIfDefined(aunCard, 'format', card.bodyFormat);
|
|
4361
4778
|
}
|
|
4362
|
-
if (req.initiatorId &&
|
|
4779
|
+
if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
|
|
4363
4780
|
aunCard.initiator = req.initiatorId;
|
|
4364
4781
|
if (replyCtx?.threadId)
|
|
4365
4782
|
aunCard.thread_id = replyCtx.threadId;
|
|
@@ -4372,6 +4789,7 @@ export class AUNChannelPlugin {
|
|
|
4372
4789
|
requestId: req.id,
|
|
4373
4790
|
isCommandCard: true,
|
|
4374
4791
|
initiatorAid: req.initiatorId,
|
|
4792
|
+
delivery: replyCtx?.delivery,
|
|
4375
4793
|
expiresAt: Date.now() + cardTtlMs,
|
|
4376
4794
|
},
|
|
4377
4795
|
});
|
|
@@ -4438,15 +4856,12 @@ export class AUNChannelPlugin {
|
|
|
4438
4856
|
});
|
|
4439
4857
|
});
|
|
4440
4858
|
}
|
|
4441
|
-
if (typeof channel.
|
|
4442
|
-
channel.
|
|
4443
|
-
|
|
4444
|
-
// dispatch_mode(mention/broadcast);未设值返回 undefined,回退服务端下发值。
|
|
4445
|
-
const mentionMode = resolveEffective({
|
|
4859
|
+
if (typeof channel.setMentionModeResolver === 'function') {
|
|
4860
|
+
channel.setMentionModeResolver(async (channelId) => {
|
|
4861
|
+
return resolveEffective({
|
|
4446
4862
|
self: aid,
|
|
4447
4863
|
peerKey: formatPeerKey('aun', channelId),
|
|
4448
4864
|
}, { cache: true }).mentionMode;
|
|
4449
|
-
return mentionModeToDispatch(mentionMode);
|
|
4450
4865
|
});
|
|
4451
4866
|
}
|
|
4452
4867
|
},
|