evolcore 0.0.12 → 0.0.14
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 +37 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/ec-safe-output.js +26 -7
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +105 -11
- package/dist/agents/codex-app-server-client.js +176 -7
- package/dist/agents/codex-runner.js +248 -84
- package/dist/agents/ecagent-runner.js +21 -2
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/aid/control-aid.js +36 -13
- 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 +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +176 -55
- 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 +514 -150
- package/dist/channels/daemon.js +2 -0
- package/dist/cli/agent.js +9 -96
- package/dist/cli/aun-commands.js +1 -1
- 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 +39 -24
- package/dist/cli/fs-command.js +65 -19
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/task-context.js +172 -0
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +7 -0
- package/dist/config/config-manager.js +27 -3
- package/dist/config/contact-book-store.js +7 -2
- 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 -1
- package/dist/core/auth/auth-gateway.js +30 -1
- package/dist/core/auth/authorization-audit.js +47 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +10 -1
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +8 -1
- package/dist/core/command/command-handler.js +201 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +8 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +34 -1
- package/dist/core/message/message-bridge.js +68 -13
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +242 -48
- 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 +649 -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 +21 -0
- package/dist/core/permission/tool-policy.js +266 -56
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/runtime-lock.js +101 -0
- package/dist/core/session/session-manager.js +21 -2
- 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/index.js +191 -89
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/response-system/engines/v1/proactive-flow.js +92 -8
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/history.js +42 -7
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +17 -0
- package/dist/utils/logger.js +37 -4
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +7 -0
- package/kits/templates/roles/member.json +8 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- 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/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';
|
|
@@ -31,15 +31,70 @@ 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
36
|
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
38
37
|
import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
|
|
39
38
|
import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
|
|
39
|
+
import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
40
40
|
export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
|
|
41
41
|
const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
42
|
+
const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
42
43
|
const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
|
|
44
|
+
// AUN limits the complete encrypted thought envelope to 8192 bytes. The
|
|
45
|
+
// encrypted group envelope grows with the number of recipient devices, so a
|
|
46
|
+
// conservative plaintext budget avoids rejecting otherwise valid thoughts.
|
|
47
|
+
const AUN_THOUGHT_PLAINTEXT_MAX_BYTES = 4096;
|
|
48
|
+
const AUN_THOUGHT_TRUNCATION_SUFFIX = '\n\n[thought truncated]';
|
|
49
|
+
function truncateUtf8ToBytes(value, maxBytes) {
|
|
50
|
+
if (Buffer.byteLength(value, 'utf8') <= maxBytes)
|
|
51
|
+
return value;
|
|
52
|
+
if (maxBytes <= 0)
|
|
53
|
+
return '';
|
|
54
|
+
let bytes = 0;
|
|
55
|
+
let end = 0;
|
|
56
|
+
for (const char of value) {
|
|
57
|
+
const charBytes = Buffer.byteLength(char, 'utf8');
|
|
58
|
+
if (bytes + charBytes > maxBytes)
|
|
59
|
+
break;
|
|
60
|
+
bytes += charBytes;
|
|
61
|
+
end += char.length;
|
|
62
|
+
}
|
|
63
|
+
return value.slice(0, end);
|
|
64
|
+
}
|
|
65
|
+
function truncateUtf8WithSuffix(value, maxBytes, suffix) {
|
|
66
|
+
if (Buffer.byteLength(value, 'utf8') <= maxBytes)
|
|
67
|
+
return value;
|
|
68
|
+
if (maxBytes <= 0)
|
|
69
|
+
return '';
|
|
70
|
+
const fittedSuffix = truncateUtf8ToBytes(suffix, maxBytes);
|
|
71
|
+
const headBytes = Math.max(0, maxBytes - Buffer.byteLength(fittedSuffix, 'utf8'));
|
|
72
|
+
return `${truncateUtf8ToBytes(value, headBytes)}${fittedSuffix}`;
|
|
73
|
+
}
|
|
74
|
+
function isThoughtTooLargeError(error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
76
|
+
return /thought too large/i.test(message);
|
|
77
|
+
}
|
|
78
|
+
function requireEnvelopeDelivery(channelId, envelopeDelivery, nestedDelivery) {
|
|
79
|
+
if (!isDeliveryTarget(envelopeDelivery)) {
|
|
80
|
+
throw Object.assign(new Error(`AUN outbound envelope requires a top-level delivery route for channelId=${channelId}`), {
|
|
81
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (nestedDelivery !== undefined) {
|
|
85
|
+
if (!isDeliveryTarget(nestedDelivery) || !sameDeliveryTarget(envelopeDelivery, nestedDelivery)) {
|
|
86
|
+
throw Object.assign(new Error(`AUN outbound envelope has conflicting delivery routes for channelId=${channelId}`), {
|
|
87
|
+
code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (envelopeDelivery.chatType === 'group' && envelopeDelivery.groupId !== channelId) {
|
|
92
|
+
throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
|
|
93
|
+
code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return envelopeDelivery;
|
|
97
|
+
}
|
|
43
98
|
function normalizeImageMimeType(mimeType) {
|
|
44
99
|
const normalized = mimeType?.split(';', 1)[0].trim().toLowerCase();
|
|
45
100
|
if (!normalized)
|
|
@@ -133,6 +188,8 @@ export function aunOptsToInbound(opts, channel, channelType) {
|
|
|
133
188
|
encrypted: opts.encrypted,
|
|
134
189
|
protectedHeaders: opts.protectedHeaders,
|
|
135
190
|
messageId: opts.messageId,
|
|
191
|
+
receivedAt: opts.receivedAt,
|
|
192
|
+
gatewaySeq: opts.gatewaySeq,
|
|
136
193
|
causation: opts.causation,
|
|
137
194
|
mentions: opts.mentions,
|
|
138
195
|
mentionAids: opts.mentionAids,
|
|
@@ -289,14 +346,36 @@ export class AUNChannel {
|
|
|
289
346
|
throw e;
|
|
290
347
|
}
|
|
291
348
|
}
|
|
292
|
-
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
|
|
349
|
+
/** Resolve the authenticated transport route for an ordinary outbound send.
|
|
350
|
+
* The channel id is an address only; it is intentionally never used to infer
|
|
351
|
+
* whether the address is a group.
|
|
296
352
|
*/
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
353
|
+
requireDelivery(channelId, context, explicit) {
|
|
354
|
+
const supplied = explicit ?? context?.delivery;
|
|
355
|
+
if (supplied !== undefined && !isDeliveryTarget(supplied)) {
|
|
356
|
+
throw Object.assign(new Error(`AUN outbound send has an invalid delivery route for channelId=${channelId}`), {
|
|
357
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (!supplied) {
|
|
361
|
+
throw Object.assign(new Error(`AUN outbound send requires an explicit delivery route for channelId=${channelId}`), {
|
|
362
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (supplied.chatType === 'group' && supplied.groupId !== channelId) {
|
|
366
|
+
throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
|
|
367
|
+
code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return supplied;
|
|
371
|
+
}
|
|
372
|
+
requirePersistedDelivery(channelId, delivery) {
|
|
373
|
+
// Persisted sends only trust the top-level route. In particular, old
|
|
374
|
+
// outbox entries cannot recover routing from their nested reply context.
|
|
375
|
+
return this.requireDelivery(channelId, undefined, delivery);
|
|
376
|
+
}
|
|
377
|
+
withDelivery(context, delivery) {
|
|
378
|
+
return { ...(context ?? {}), delivery };
|
|
300
379
|
}
|
|
301
380
|
getShortAid(aid) {
|
|
302
381
|
if (!aid)
|
|
@@ -318,15 +397,19 @@ export class AUNChannel {
|
|
|
318
397
|
const name = cached?.name;
|
|
319
398
|
return name && name !== short ? `${short}(${name})` : short;
|
|
320
399
|
}
|
|
321
|
-
notifyCardActionFailure(channelId, text, operatorId, threadId) {
|
|
400
|
+
notifyCardActionFailure(channelId, text, operatorId, threadId, delivery) {
|
|
322
401
|
if (!channelId) {
|
|
323
402
|
logger.warn(`${this.logPrefix()} Card action failure without channelId: ${text}`);
|
|
324
403
|
return;
|
|
325
404
|
}
|
|
326
|
-
|
|
405
|
+
if (!delivery) {
|
|
406
|
+
logger.warn(`${this.logPrefix()} Card action failure without trusted delivery route: ${text}`);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const context = { delivery };
|
|
327
410
|
if (threadId)
|
|
328
411
|
context.threadId = threadId;
|
|
329
|
-
if (operatorId &&
|
|
412
|
+
if (operatorId && delivery.chatType === 'group')
|
|
330
413
|
context.peerId = operatorId;
|
|
331
414
|
void this.sendMessage(channelId, text, context).catch((err) => {
|
|
332
415
|
logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
|
|
@@ -336,7 +419,7 @@ export class AUNChannel {
|
|
|
336
419
|
logger.info(`${this.logPrefix()} ActionInteraction audit: behavior=${behavior ?? 'reply'} `
|
|
337
420
|
+ `cardId=${cardMessageId} interactionId=${interactionId} result=${result} textLength=${textLength}`);
|
|
338
421
|
}
|
|
339
|
-
extractTextPayload(payload, channelId, senderAid) {
|
|
422
|
+
extractTextPayload(payload, channelId, senderAid, delivery) {
|
|
340
423
|
if (typeof payload === 'string')
|
|
341
424
|
return payload;
|
|
342
425
|
if (payload && typeof payload === 'object') {
|
|
@@ -347,6 +430,7 @@ export class AUNChannel {
|
|
|
347
430
|
const cardMsgId = typeof obj.ref_message_id === 'string' ? obj.ref_message_id
|
|
348
431
|
: typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
|
|
349
432
|
const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
|
|
433
|
+
const cardDelivery = cardInfo?.delivery ?? delivery;
|
|
350
434
|
if (cardInfo) {
|
|
351
435
|
// compose 按钮由新版客户端在卡片内采集输入;旧版客户端可继续从主编辑器
|
|
352
436
|
// 采集。两者最终都回传 action_card_reply{behavior:"compose", text}。
|
|
@@ -371,25 +455,25 @@ export class AUNChannel {
|
|
|
371
455
|
// payloads omit senderAid. A group channelId is only a conversation
|
|
372
456
|
// identifier and must never be treated as the clicking operator.
|
|
373
457
|
const cardClickerAid = senderAid
|
|
374
|
-
|| (
|
|
458
|
+
|| (cardDelivery?.chatType === 'private' && channelId ? channelId : '')
|
|
375
459
|
|| '';
|
|
376
460
|
if (cardInfo.isCommandCard) {
|
|
377
461
|
// CommandCard:action_value 是完整 slash 命令,构造伪入站消息。
|
|
378
462
|
// 先完成点击者鉴权,再消费 mapping;否则群成员的一次误点会让发起者
|
|
379
463
|
// 无法继续操作同一张卡片。
|
|
380
|
-
const chatType =
|
|
464
|
+
const chatType = cardDelivery?.chatType ?? 'private';
|
|
381
465
|
if (this.messageHandler && actionValue.startsWith('/')) {
|
|
382
466
|
// Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
|
|
383
467
|
// 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
|
|
384
468
|
if (chatType === 'group' && !cardClickerAid) {
|
|
385
469
|
logger.info(`${this.logPrefix()} CommandCard rejected: missing authenticated clicker mid=${cardMsgId}`);
|
|
386
|
-
this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
|
|
470
|
+
this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
|
|
387
471
|
return '';
|
|
388
472
|
}
|
|
389
473
|
if (chatType === 'group' && cardInfo.initiatorAid
|
|
390
474
|
&& cardClickerAid !== cardInfo.initiatorAid) {
|
|
391
475
|
logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
|
|
392
|
-
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
|
|
476
|
+
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
|
|
393
477
|
return '';
|
|
394
478
|
}
|
|
395
479
|
// 已通过鉴权并即将进入处理流程;此时才消费 mapping,避免非发起者
|
|
@@ -408,28 +492,28 @@ export class AUNChannel {
|
|
|
408
492
|
})).catch((err) => {
|
|
409
493
|
logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
|
|
410
494
|
const message = err instanceof Error && err.message ? err.message : String(err || '未知错误');
|
|
411
|
-
this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId);
|
|
495
|
+
this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId, cardDelivery);
|
|
412
496
|
});
|
|
413
497
|
}
|
|
414
498
|
else if (!this.messageHandler) {
|
|
415
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId);
|
|
499
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId, cardDelivery);
|
|
416
500
|
}
|
|
417
501
|
else {
|
|
418
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId);
|
|
502
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId, cardDelivery);
|
|
419
503
|
}
|
|
420
504
|
}
|
|
421
505
|
else {
|
|
422
506
|
// ActionInteraction:后端明确接受后才消费 mapping。撤权、过期或
|
|
423
507
|
// 重复点击返回 false 时保持 fail-closed,并提示用户卡片已失效。
|
|
424
|
-
if (
|
|
508
|
+
if (cardDelivery?.chatType === 'group' && !cardClickerAid) {
|
|
425
509
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_unauthenticated', text.length);
|
|
426
|
-
this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
|
|
510
|
+
this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
|
|
427
511
|
return '';
|
|
428
512
|
}
|
|
429
|
-
if (
|
|
513
|
+
if (cardDelivery?.chatType === 'group' && cardInfo.initiatorAid
|
|
430
514
|
&& cardClickerAid !== cardInfo.initiatorAid) {
|
|
431
515
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_non_initiator', text.length);
|
|
432
|
-
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
|
|
516
|
+
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
|
|
433
517
|
return '';
|
|
434
518
|
}
|
|
435
519
|
// Compose reply 入参校验:text 必须非空且未超长。
|
|
@@ -439,19 +523,19 @@ export class AUNChannel {
|
|
|
439
523
|
const MAX_COMPOSE_TEXT_LENGTH = 64 * 1024; // 64K 字符
|
|
440
524
|
if (!trimmedText) {
|
|
441
525
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_empty', text.length);
|
|
442
|
-
this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId);
|
|
526
|
+
this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId, cardDelivery);
|
|
443
527
|
return '';
|
|
444
528
|
}
|
|
445
529
|
if (text.length > MAX_COMPOSE_TEXT_LENGTH) {
|
|
446
530
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_too_long', text.length);
|
|
447
|
-
this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId);
|
|
531
|
+
this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId, cardDelivery);
|
|
448
532
|
return '';
|
|
449
533
|
}
|
|
450
534
|
}
|
|
451
535
|
if (this.interactionCallback) {
|
|
452
536
|
if (this.interactionActionsInFlight.has(cardMsgId)) {
|
|
453
537
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_in_flight', text.length);
|
|
454
|
-
this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId);
|
|
538
|
+
this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId, cardDelivery);
|
|
455
539
|
return '';
|
|
456
540
|
}
|
|
457
541
|
this.interactionActionsInFlight.add(cardMsgId);
|
|
@@ -484,7 +568,7 @@ export class AUNChannel {
|
|
|
484
568
|
// captured the message id before the callback returned.
|
|
485
569
|
this.forgetInteractionCard(cardMsgId, cardInfo.requestId);
|
|
486
570
|
}
|
|
487
|
-
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId);
|
|
571
|
+
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId, cardDelivery);
|
|
488
572
|
};
|
|
489
573
|
try {
|
|
490
574
|
const result = this.interactionCallback(response);
|
|
@@ -493,7 +577,7 @@ export class AUNChannel {
|
|
|
493
577
|
this.interactionActionsInFlight.delete(cardMsgId);
|
|
494
578
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
|
|
495
579
|
logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
|
|
496
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
|
|
580
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
|
|
497
581
|
});
|
|
498
582
|
}
|
|
499
583
|
else {
|
|
@@ -504,19 +588,19 @@ export class AUNChannel {
|
|
|
504
588
|
this.interactionActionsInFlight.delete(cardMsgId);
|
|
505
589
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
|
|
506
590
|
logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
|
|
507
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
|
|
591
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
|
|
508
592
|
}
|
|
509
593
|
}
|
|
510
594
|
else {
|
|
511
595
|
this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'handler_unavailable', text.length);
|
|
512
|
-
this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId);
|
|
596
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId, cardDelivery);
|
|
513
597
|
}
|
|
514
598
|
}
|
|
515
599
|
}
|
|
516
600
|
else if (this.ownedCardMsgIds.has(cardMsgId)) {
|
|
517
601
|
// 本 agent 发出的卡片,但 entry 已过期(24h TTL)
|
|
518
602
|
logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
|
|
519
|
-
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起');
|
|
603
|
+
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', undefined, undefined, delivery);
|
|
520
604
|
}
|
|
521
605
|
else {
|
|
522
606
|
// 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
|
|
@@ -751,8 +835,13 @@ export class AUNChannel {
|
|
|
751
835
|
return sanitized;
|
|
752
836
|
return undefined;
|
|
753
837
|
}
|
|
754
|
-
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName) {
|
|
755
|
-
|
|
838
|
+
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
|
|
839
|
+
if (!groupId)
|
|
840
|
+
throw new Error('buildGroupReplyContext requires groupId');
|
|
841
|
+
const replyContext = {
|
|
842
|
+
metadata: { encrypted, chatmode },
|
|
843
|
+
delivery: { chatType: 'group', groupId },
|
|
844
|
+
};
|
|
756
845
|
if (threadId)
|
|
757
846
|
replyContext.threadId = threadId;
|
|
758
847
|
replyContext.peerId = senderAid;
|
|
@@ -766,6 +855,20 @@ export class AUNChannel {
|
|
|
766
855
|
}
|
|
767
856
|
return replyContext;
|
|
768
857
|
}
|
|
858
|
+
buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
|
|
859
|
+
const replyContext = {
|
|
860
|
+
metadata: { encrypted, chatmode, refMessageId },
|
|
861
|
+
delivery: { chatType: 'private' },
|
|
862
|
+
peerId: senderAid,
|
|
863
|
+
};
|
|
864
|
+
if (threadId)
|
|
865
|
+
replyContext.threadId = threadId;
|
|
866
|
+
if (messageId)
|
|
867
|
+
replyContext.replyToMessageId = messageId;
|
|
868
|
+
if (topicName && threadId)
|
|
869
|
+
replyContext.metadata.topicName = topicName;
|
|
870
|
+
return replyContext;
|
|
871
|
+
}
|
|
769
872
|
claimInboundMessage(kind, messageId, seq) {
|
|
770
873
|
if (!messageId)
|
|
771
874
|
return true;
|
|
@@ -788,9 +891,72 @@ export class AUNChannel {
|
|
|
788
891
|
this.inboundSeenMessages.set(dedupeKey, { seenAt: Date.now(), seq });
|
|
789
892
|
if (seq !== undefined)
|
|
790
893
|
this.messageSeqMap.set(messageId, seq);
|
|
791
|
-
|
|
894
|
+
this.appendInboundDedupe(dedupeKey, messageId, kind, seq);
|
|
895
|
+
setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), AUN_INBOUND_DEDUP_TTL_MS);
|
|
792
896
|
return true;
|
|
793
897
|
}
|
|
898
|
+
inboundDedupeFile() {
|
|
899
|
+
const channelName = this.config.channelName ?? 'aun';
|
|
900
|
+
return channelStatePath(channelName, 'dedup', 'aun', `${encodeURIComponent(this.config.aid)}.jsonl`);
|
|
901
|
+
}
|
|
902
|
+
loadInboundDedupe() {
|
|
903
|
+
const file = this.inboundDedupeFile();
|
|
904
|
+
const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
|
|
905
|
+
let lines;
|
|
906
|
+
try {
|
|
907
|
+
lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
let needsRewrite = false;
|
|
913
|
+
for (const line of lines) {
|
|
914
|
+
try {
|
|
915
|
+
const entry = JSON.parse(line);
|
|
916
|
+
if (typeof entry.key !== 'string' || typeof entry.seenAt !== 'number' || !Number.isFinite(entry.seenAt) || entry.seenAt <= cutoff) {
|
|
917
|
+
needsRewrite = true;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
this.inboundSeenMessages.set(entry.key, {
|
|
921
|
+
seenAt: entry.seenAt,
|
|
922
|
+
seq: typeof entry.seq === 'number' && Number.isFinite(entry.seq) ? entry.seq : undefined,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
needsRewrite = true;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (needsRewrite)
|
|
930
|
+
this.rewriteInboundDedupe();
|
|
931
|
+
}
|
|
932
|
+
appendInboundDedupe(key, messageId, kind, seq) {
|
|
933
|
+
try {
|
|
934
|
+
const file = this.inboundDedupeFile();
|
|
935
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
936
|
+
fs.appendFileSync(file, JSON.stringify({ key, messageId, kind, seenAt: Date.now(), seq }) + '\n');
|
|
937
|
+
}
|
|
938
|
+
catch (error) {
|
|
939
|
+
logger.warn(`${this.logPrefix()} Failed to persist inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
rewriteInboundDedupe() {
|
|
943
|
+
const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
|
|
944
|
+
for (const [key, value] of this.inboundSeenMessages) {
|
|
945
|
+
if (value.seenAt <= cutoff)
|
|
946
|
+
this.inboundSeenMessages.delete(key);
|
|
947
|
+
}
|
|
948
|
+
try {
|
|
949
|
+
const file = this.inboundDedupeFile();
|
|
950
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
951
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
952
|
+
const lines = [...this.inboundSeenMessages].map(([key, value]) => JSON.stringify({ key, seenAt: value.seenAt, seq: value.seq }));
|
|
953
|
+
fs.writeFileSync(tmp, lines.length > 0 ? `${lines.join('\n')}\n` : '');
|
|
954
|
+
fs.renameSync(tmp, file);
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
logger.warn(`${this.logPrefix()} Failed to compact inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
794
960
|
acknowledgeImmediately(messageId, _seq) {
|
|
795
961
|
// SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
|
|
796
962
|
// as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
|
|
@@ -875,6 +1041,7 @@ export class AUNChannel {
|
|
|
875
1041
|
constructor(config) {
|
|
876
1042
|
this.config = config;
|
|
877
1043
|
this.agentDir = agentDirPath(config.aid);
|
|
1044
|
+
this.loadInboundDedupe();
|
|
878
1045
|
if (config.aunTrace) {
|
|
879
1046
|
this.traceWriter = new LogWriter({
|
|
880
1047
|
baseName: 'aun',
|
|
@@ -972,7 +1139,7 @@ export class AUNChannel {
|
|
|
972
1139
|
// gateway metadata cache when well-known is temporarily unavailable.
|
|
973
1140
|
const configuredGateway = this.config.gatewayUrl || '';
|
|
974
1141
|
logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${configuredGateway || '<sdk-discovery>'}, aun_path=${aunPath}`);
|
|
975
|
-
// 构造 AIDStore
|
|
1142
|
+
// 构造 AIDStore。daemon 使用独立 slot,CLI/netcheck 不能触碰业务入站游标。
|
|
976
1143
|
// encryptionSeed / rootCaPath 由 getAidStore 内部注入
|
|
977
1144
|
const store = await getAidStore({
|
|
978
1145
|
slotId: SLOT.daemon,
|
|
@@ -1025,7 +1192,7 @@ export class AUNChannel {
|
|
|
1025
1192
|
for (const id of ids) {
|
|
1026
1193
|
if (typeof id === 'string') {
|
|
1027
1194
|
logger.info(`${this.logPrefix()} Message recalled: ${id}`);
|
|
1028
|
-
this.recallHandler?.(id);
|
|
1195
|
+
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
1029
1196
|
}
|
|
1030
1197
|
}
|
|
1031
1198
|
}
|
|
@@ -1057,7 +1224,7 @@ export class AUNChannel {
|
|
|
1057
1224
|
for (const id of ids) {
|
|
1058
1225
|
if (typeof id === 'string') {
|
|
1059
1226
|
logger.info(`${this.logPrefix()} Group message recalled: group=${env.group_id ?? ''} mid=${id}`);
|
|
1060
|
-
this.recallHandler?.(id);
|
|
1227
|
+
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
1061
1228
|
}
|
|
1062
1229
|
}
|
|
1063
1230
|
}
|
|
@@ -1095,7 +1262,7 @@ export class AUNChannel {
|
|
|
1095
1262
|
});
|
|
1096
1263
|
this.trace('OUT', 'client.connect', { gateway: this.gatewayUrl, extra_info: extraInfo });
|
|
1097
1264
|
await client.connect({
|
|
1098
|
-
// connection_kind 默认 long;slot 已由 AID 携带(evolcore
|
|
1265
|
+
// connection_kind 默认 long;slot 已由 AID 携带(evolcore-daemon)
|
|
1099
1266
|
// extra_info:互踢诊断名片(0.4.3 公开 connect 已支持透传)
|
|
1100
1267
|
extra_info: extraInfo,
|
|
1101
1268
|
// max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
|
|
@@ -1155,7 +1322,9 @@ export class AUNChannel {
|
|
|
1155
1322
|
}
|
|
1156
1323
|
const operationId = postBootstrapWelcomeOperationId(aidName);
|
|
1157
1324
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
|
|
1158
|
-
|
|
1325
|
+
const prepared = outbox.findByDedupeKey(aidName, operationId);
|
|
1326
|
+
if (hasMessageLogOperation(chatDir, operationId)
|
|
1327
|
+
|| (prepared?.delivery?.chatType === 'private' && prepared.channelId === owner)) {
|
|
1159
1328
|
return true;
|
|
1160
1329
|
}
|
|
1161
1330
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
@@ -1211,6 +1380,11 @@ export class AUNChannel {
|
|
|
1211
1380
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
|
|
1212
1381
|
return hasMessageLogOperation(chatDir, operationId);
|
|
1213
1382
|
}
|
|
1383
|
+
if (entry.delivery?.chatType !== 'private') {
|
|
1384
|
+
logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
|
|
1385
|
+
outbox.remove(aid, entry.id);
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1214
1388
|
const agentConfig = loadAgent(aid);
|
|
1215
1389
|
if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
|
|
1216
1390
|
logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
|
|
@@ -1234,7 +1408,7 @@ export class AUNChannel {
|
|
|
1234
1408
|
* @param channelId 下载归属(私聊 fromAid / 群聊 groupId)
|
|
1235
1409
|
* @param preCollected 已收集的附件(群聊路径会提前 collect,避免重复)
|
|
1236
1410
|
*/
|
|
1237
|
-
async processAttachments(payload, baseText, channelId, preCollected) {
|
|
1411
|
+
async processAttachments(payload, baseText, channelId, preCollected, delivery) {
|
|
1238
1412
|
const rawAttachments = preCollected ?? this.collectAllAttachments(payload);
|
|
1239
1413
|
const images = [];
|
|
1240
1414
|
let finalText = baseText;
|
|
@@ -1243,7 +1417,7 @@ export class AUNChannel {
|
|
|
1243
1417
|
}
|
|
1244
1418
|
const fileParts = [];
|
|
1245
1419
|
for (const att of rawAttachments) {
|
|
1246
|
-
const filePath = await this.downloadAttachment(att, channelId);
|
|
1420
|
+
const filePath = await this.downloadAttachment(att, channelId, delivery);
|
|
1247
1421
|
if (!filePath)
|
|
1248
1422
|
continue;
|
|
1249
1423
|
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
@@ -1275,8 +1449,11 @@ export class AUNChannel {
|
|
|
1275
1449
|
logger.info(`${this.logPrefix()} [attachments] count=${rawAttachments.length} images=${images.length} files=${fileParts.length}`);
|
|
1276
1450
|
return { finalText, images };
|
|
1277
1451
|
}
|
|
1278
|
-
async downloadAttachment(att, channelId) {
|
|
1279
|
-
const ownerAid = att.owner_aid
|
|
1452
|
+
async downloadAttachment(att, channelId, delivery) {
|
|
1453
|
+
const ownerAid = att.owner_aid
|
|
1454
|
+
|| (delivery?.chatType === 'private' ? channelId : '')
|
|
1455
|
+
|| this._aid
|
|
1456
|
+
|| '';
|
|
1280
1457
|
const objectKey = att.object_key;
|
|
1281
1458
|
if (!objectKey) {
|
|
1282
1459
|
logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
|
|
@@ -1448,6 +1625,7 @@ export class AUNChannel {
|
|
|
1448
1625
|
if (!data || typeof data !== 'object')
|
|
1449
1626
|
return;
|
|
1450
1627
|
const msg = data;
|
|
1628
|
+
const receivedAt = Date.now();
|
|
1451
1629
|
// Claim before any payload extraction or command parsing. Retransmits may
|
|
1452
1630
|
// carry a new seq, but the application identity remains message_id.
|
|
1453
1631
|
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
@@ -1467,7 +1645,7 @@ export class AUNChannel {
|
|
|
1467
1645
|
? payload._evolcore_causation_ref
|
|
1468
1646
|
: undefined;
|
|
1469
1647
|
const logDescriptor = classifyAunPayloadForLog(payload);
|
|
1470
|
-
const text = this.extractTextPayload(payload, fromAid, fromAid);
|
|
1648
|
+
const text = this.extractTextPayload(payload, fromAid, fromAid, { chatType: 'private' });
|
|
1471
1649
|
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
1472
1650
|
const refMessageId = typeof payload === 'object' && payload !== null && typeof payload.ref_message_id === 'string'
|
|
1473
1651
|
? payload.ref_message_id
|
|
@@ -1513,7 +1691,7 @@ export class AUNChannel {
|
|
|
1513
1691
|
mentions.push({ aid: this._aid });
|
|
1514
1692
|
}
|
|
1515
1693
|
// Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
|
|
1516
|
-
const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
|
|
1694
|
+
const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid, undefined, { chatType: 'private' });
|
|
1517
1695
|
// 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
|
|
1518
1696
|
// device_id 仅 SDK 内部多实例去重用,evolcore session 层面跨端共享会话
|
|
1519
1697
|
const chatId = fromAid;
|
|
@@ -1529,7 +1707,7 @@ export class AUNChannel {
|
|
|
1529
1707
|
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))}`);
|
|
1530
1708
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1531
1709
|
if (p2pPayloadType === 'action_card_reply') {
|
|
1532
|
-
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1710
|
+
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
1533
1711
|
return;
|
|
1534
1712
|
}
|
|
1535
1713
|
// menu.* 与联系人本人状态查询:需要原始 payload JSON 传递给 bridge。
|
|
@@ -1537,18 +1715,18 @@ export class AUNChannel {
|
|
|
1537
1715
|
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
|
|
1538
1716
|
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
|
|
1539
1717
|
this.acknowledgeImmediately(messageId, seq);
|
|
1540
|
-
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1718
|
+
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
1541
1719
|
this.dispatchMessage({
|
|
1542
1720
|
channelId: chatId, userId: fromAid,
|
|
1543
1721
|
text: JSON.stringify(payload),
|
|
1544
|
-
chatType: 'private', messageId, seq,
|
|
1722
|
+
chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
|
|
1545
1723
|
peerName: displayName || undefined,
|
|
1546
1724
|
peerType: peerIdentity.type,
|
|
1547
1725
|
encrypted: msgEncrypted,
|
|
1548
1726
|
protectedHeaders,
|
|
1549
1727
|
mentions,
|
|
1550
1728
|
mentionAids: mentionEntryAids(mentions),
|
|
1551
|
-
replyContext: this.
|
|
1729
|
+
replyContext: this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
|
|
1552
1730
|
msgType: logDescriptor.msgType,
|
|
1553
1731
|
payloadType: logDescriptor.payloadType,
|
|
1554
1732
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -1568,11 +1746,11 @@ export class AUNChannel {
|
|
|
1568
1746
|
// 透传 encrypted:bind.response 须与请求的加密/明文对称(见 onMessage 处理)。
|
|
1569
1747
|
if (p2pPayloadType === AUNChannel.BIND_REQUEST_TYPE) {
|
|
1570
1748
|
this.acknowledgeImmediately(messageId, seq);
|
|
1571
|
-
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1749
|
+
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
1572
1750
|
this.dispatchMessage({
|
|
1573
1751
|
channelId: chatId, userId: fromAid,
|
|
1574
1752
|
text: JSON.stringify(payload),
|
|
1575
|
-
chatType: 'private', messageId, seq,
|
|
1753
|
+
chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
|
|
1576
1754
|
peerName: displayName || undefined,
|
|
1577
1755
|
peerType: peerIdentity.type,
|
|
1578
1756
|
encrypted: msgEncrypted,
|
|
@@ -1588,7 +1766,7 @@ export class AUNChannel {
|
|
|
1588
1766
|
// payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
|
|
1589
1767
|
if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
|
|
1590
1768
|
this.acknowledgeImmediately(messageId, seq);
|
|
1591
|
-
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1769
|
+
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
1592
1770
|
logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
|
|
1593
1771
|
return;
|
|
1594
1772
|
}
|
|
@@ -1597,24 +1775,15 @@ export class AUNChannel {
|
|
|
1597
1775
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
|
|
1598
1776
|
const isSystemP2P = p2pPayloadType === 'event';
|
|
1599
1777
|
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
|
|
1600
|
-
const replyContext =
|
|
1601
|
-
if (threadId)
|
|
1602
|
-
replyContext.threadId = threadId;
|
|
1603
|
-
replyContext.peerId = fromAid;
|
|
1604
|
-
if (messageId)
|
|
1605
|
-
replyContext.replyToMessageId = messageId;
|
|
1606
|
-
// 协议 v2.4 §6.4/§12.1: 话题会话创建时传入 topicName
|
|
1607
|
-
if (topicName && threadId) {
|
|
1608
|
-
if (!replyContext.metadata)
|
|
1609
|
-
replyContext.metadata = {};
|
|
1610
|
-
replyContext.metadata.topicName = topicName;
|
|
1611
|
-
}
|
|
1778
|
+
const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId);
|
|
1612
1779
|
this.dispatchMessage({
|
|
1613
1780
|
channelId: chatId,
|
|
1614
1781
|
userId: fromAid,
|
|
1615
1782
|
text: finalText,
|
|
1616
1783
|
chatType: 'private',
|
|
1617
1784
|
messageId,
|
|
1785
|
+
receivedAt,
|
|
1786
|
+
gatewaySeq: seq,
|
|
1618
1787
|
causation,
|
|
1619
1788
|
seq,
|
|
1620
1789
|
threadId,
|
|
@@ -1637,6 +1806,7 @@ export class AUNChannel {
|
|
|
1637
1806
|
if (!data || typeof data !== 'object')
|
|
1638
1807
|
return;
|
|
1639
1808
|
const msg = data;
|
|
1809
|
+
const receivedAt = Date.now();
|
|
1640
1810
|
// Deduplicate at the adapter boundary, before mention/slash routing.
|
|
1641
1811
|
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
1642
1812
|
const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
|
|
@@ -1654,7 +1824,7 @@ export class AUNChannel {
|
|
|
1654
1824
|
return;
|
|
1655
1825
|
const payloadMentionAids = mentionEntryAids(payloadMentionEntries);
|
|
1656
1826
|
const logDescriptor = classifyAunPayloadForLog(payload);
|
|
1657
|
-
const text = this.extractTextPayload(payload, groupId, senderAid);
|
|
1827
|
+
const text = this.extractTextPayload(payload, groupId, senderAid, { chatType: 'group', groupId });
|
|
1658
1828
|
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
1659
1829
|
const topicName = (typeof payload === 'object' && payload !== null)
|
|
1660
1830
|
? this.sanitizeTopicName(payload.topicName)
|
|
@@ -1710,7 +1880,7 @@ export class AUNChannel {
|
|
|
1710
1880
|
peerName: displayName,
|
|
1711
1881
|
peerType: peerInfo?.type || 'unknown',
|
|
1712
1882
|
seq,
|
|
1713
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName),
|
|
1883
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
|
|
1714
1884
|
createdAt,
|
|
1715
1885
|
});
|
|
1716
1886
|
return;
|
|
@@ -1720,7 +1890,7 @@ export class AUNChannel {
|
|
|
1720
1890
|
{
|
|
1721
1891
|
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
1722
1892
|
if (payloadType === 'action_card_reply') {
|
|
1723
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
|
|
1893
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
1724
1894
|
return;
|
|
1725
1895
|
}
|
|
1726
1896
|
}
|
|
@@ -1733,18 +1903,18 @@ export class AUNChannel {
|
|
|
1733
1903
|
if (payloadType.startsWith('menu.')) {
|
|
1734
1904
|
const menuEncrypted = !!env.encrypted;
|
|
1735
1905
|
this.acknowledgeImmediately(messageId, seq);
|
|
1736
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId);
|
|
1906
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
1737
1907
|
this.dispatchMessage({
|
|
1738
1908
|
channelId: groupId, userId: senderAid,
|
|
1739
1909
|
text: JSON.stringify(payload),
|
|
1740
|
-
chatType: 'group', messageId, seq, groupId,
|
|
1910
|
+
chatType: 'group', messageId, seq, receivedAt, gatewaySeq: seq, groupId,
|
|
1741
1911
|
encrypted: menuEncrypted,
|
|
1742
1912
|
protectedHeaders,
|
|
1743
1913
|
mentions: payloadMentionEntries,
|
|
1744
1914
|
mentionAids: payloadMentionAids,
|
|
1745
1915
|
isMentioned: hasMentionAll(payloadMentionEntries)
|
|
1746
1916
|
|| (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
|
|
1747
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
|
|
1917
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName, groupId),
|
|
1748
1918
|
msgType: logDescriptor.msgType,
|
|
1749
1919
|
payloadType: logDescriptor.payloadType,
|
|
1750
1920
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -1754,7 +1924,7 @@ export class AUNChannel {
|
|
|
1754
1924
|
}
|
|
1755
1925
|
if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
|
|
1756
1926
|
this.acknowledgeImmediately(messageId, seq);
|
|
1757
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
|
|
1927
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
1758
1928
|
logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
|
|
1759
1929
|
return;
|
|
1760
1930
|
}
|
|
@@ -1795,7 +1965,7 @@ export class AUNChannel {
|
|
|
1795
1965
|
this.pendingEchoMessages.set(messageId, {
|
|
1796
1966
|
text: echoText,
|
|
1797
1967
|
channelId: groupId,
|
|
1798
|
-
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined),
|
|
1968
|
+
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined, groupId),
|
|
1799
1969
|
receiveTs: Date.now(),
|
|
1800
1970
|
});
|
|
1801
1971
|
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
@@ -1817,7 +1987,7 @@ export class AUNChannel {
|
|
|
1817
1987
|
// TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
|
|
1818
1988
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
1819
1989
|
this.acknowledgeImmediately(messageId, seq);
|
|
1820
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId);
|
|
1990
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
1821
1991
|
logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
1822
1992
|
return;
|
|
1823
1993
|
}
|
|
@@ -1838,7 +2008,7 @@ export class AUNChannel {
|
|
|
1838
2008
|
}
|
|
1839
2009
|
const mentions = payloadMentionEntries;
|
|
1840
2010
|
// Process attachments
|
|
1841
|
-
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
|
|
2011
|
+
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
|
|
1842
2012
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
1843
2013
|
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
|
|
1844
2014
|
const shortAid = this.getShortAid(senderAid);
|
|
@@ -1882,6 +2052,8 @@ export class AUNChannel {
|
|
|
1882
2052
|
text: finalText,
|
|
1883
2053
|
chatType: 'group',
|
|
1884
2054
|
messageId,
|
|
2055
|
+
receivedAt,
|
|
2056
|
+
gatewaySeq: seq,
|
|
1885
2057
|
seq,
|
|
1886
2058
|
threadId,
|
|
1887
2059
|
mentions,
|
|
@@ -1889,7 +2061,7 @@ export class AUNChannel {
|
|
|
1889
2061
|
isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
|
|
1890
2062
|
source: isOwnHandoff ? 'handoff' : undefined,
|
|
1891
2063
|
encrypted: msgEncrypted,
|
|
1892
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
|
|
2064
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
|
|
1893
2065
|
mentionMode: serverMentionMode,
|
|
1894
2066
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1895
2067
|
msgType: logDescriptor.msgType,
|
|
@@ -1899,6 +2071,19 @@ export class AUNChannel {
|
|
|
1899
2071
|
});
|
|
1900
2072
|
}
|
|
1901
2073
|
dispatchMessage(event) {
|
|
2074
|
+
const authenticatedDelivery = event.chatType === 'group'
|
|
2075
|
+
? { chatType: 'group', groupId: event.groupId ?? event.channelId }
|
|
2076
|
+
: { chatType: 'private' };
|
|
2077
|
+
const suppliedDelivery = event.replyContext?.delivery;
|
|
2078
|
+
const suppliedMatches = suppliedDelivery?.chatType === 'private'
|
|
2079
|
+
? authenticatedDelivery.chatType === 'private'
|
|
2080
|
+
: suppliedDelivery?.chatType === 'group'
|
|
2081
|
+
&& authenticatedDelivery.chatType === 'group'
|
|
2082
|
+
&& suppliedDelivery.groupId === authenticatedDelivery.groupId;
|
|
2083
|
+
const delivery = suppliedDelivery && suppliedMatches ? suppliedDelivery : authenticatedDelivery;
|
|
2084
|
+
if (suppliedDelivery && delivery !== suppliedDelivery) {
|
|
2085
|
+
logger.warn(`${this.logPrefix()} Ignoring mismatched inbound reply route: channel=${event.channelId} chatType=${event.chatType}`);
|
|
2086
|
+
}
|
|
1902
2087
|
// Inbound events are claimed in handleIncomingPrivateMessage / GroupMessage
|
|
1903
2088
|
// before parsing. Keeping dedupe there prevents duplicate slash commands
|
|
1904
2089
|
// from reaching routing and mention policy code.
|
|
@@ -1926,19 +2111,24 @@ export class AUNChannel {
|
|
|
1926
2111
|
this.pendingEchoMessages.set(event.messageId, {
|
|
1927
2112
|
text: echoText,
|
|
1928
2113
|
channelId: event.channelId,
|
|
1929
|
-
context:
|
|
2114
|
+
context: {
|
|
2115
|
+
...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
|
|
2116
|
+
delivery,
|
|
2117
|
+
},
|
|
1930
2118
|
receiveTs: Date.now(),
|
|
1931
2119
|
});
|
|
1932
2120
|
logger.info(`${this.logPrefix()} [Echo] long echo stored: mid=${event.messageId} channelId=${event.channelId}`);
|
|
1933
2121
|
}
|
|
1934
2122
|
if (!this.messageHandler)
|
|
1935
2123
|
return;
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
replyContext
|
|
1941
|
-
|
|
2124
|
+
// Preserve the caller-supplied context (group paths add mention fields),
|
|
2125
|
+
// but always attach the route derived from this authenticated event when
|
|
2126
|
+
// a synthetic/test event omitted it. Never infer the route from the AID.
|
|
2127
|
+
const replyContext = {
|
|
2128
|
+
...(event.replyContext ?? {}),
|
|
2129
|
+
delivery,
|
|
2130
|
+
...(event.threadId && !event.replyContext?.threadId ? { threadId: event.threadId } : {}),
|
|
2131
|
+
};
|
|
1942
2132
|
this.messageHandler({
|
|
1943
2133
|
channelId: event.channelId || '',
|
|
1944
2134
|
channelType: 'aun',
|
|
@@ -1955,6 +2145,8 @@ export class AUNChannel {
|
|
|
1955
2145
|
encrypted: event.encrypted,
|
|
1956
2146
|
protectedHeaders: event.protectedHeaders,
|
|
1957
2147
|
messageId: event.messageId,
|
|
2148
|
+
receivedAt: event.receivedAt,
|
|
2149
|
+
gatewaySeq: event.gatewaySeq ?? event.seq,
|
|
1958
2150
|
causation: event.causation,
|
|
1959
2151
|
threadId: event.threadId,
|
|
1960
2152
|
mentions: event.mentions,
|
|
@@ -1972,7 +2164,7 @@ export class AUNChannel {
|
|
|
1972
2164
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
1973
2165
|
});
|
|
1974
2166
|
}
|
|
1975
|
-
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId) {
|
|
2167
|
+
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
|
|
1976
2168
|
try {
|
|
1977
2169
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
1978
2170
|
appendMessageLog(chatDir, buildInboundEntry({
|
|
@@ -1987,6 +2179,9 @@ export class AUNChannel {
|
|
|
1987
2179
|
msgType: descriptor.msgType,
|
|
1988
2180
|
payloadType: descriptor.payloadType,
|
|
1989
2181
|
payloadSummary: descriptor.payloadSummary,
|
|
2182
|
+
timestamp: receivedAt,
|
|
2183
|
+
receivedAt,
|
|
2184
|
+
gatewaySeq,
|
|
1990
2185
|
}));
|
|
1991
2186
|
}
|
|
1992
2187
|
catch (e) {
|
|
@@ -2190,7 +2385,14 @@ export class AUNChannel {
|
|
|
2190
2385
|
}
|
|
2191
2386
|
const replyTarget = event.channelId;
|
|
2192
2387
|
// 不传 peerId,避免 sendMessage 在头部追加 @peer
|
|
2193
|
-
const
|
|
2388
|
+
const authenticatedDelivery = event.chatType === 'group'
|
|
2389
|
+
? { chatType: 'group', groupId: event.channelId }
|
|
2390
|
+
: { chatType: 'private' };
|
|
2391
|
+
const context = {
|
|
2392
|
+
threadId: event.threadId,
|
|
2393
|
+
metadata: event.replyContext?.metadata,
|
|
2394
|
+
delivery: authenticatedDelivery,
|
|
2395
|
+
};
|
|
2194
2396
|
const sendStart = Date.now();
|
|
2195
2397
|
this.isEchoSending = true;
|
|
2196
2398
|
this.sendMessage(replyTarget, echoText, context).then(() => {
|
|
@@ -2606,19 +2808,100 @@ export class AUNChannel {
|
|
|
2606
2808
|
mentions: normalizeAunMentionEntries(payload.mentions),
|
|
2607
2809
|
};
|
|
2608
2810
|
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Keep thought payloads below the encrypted AUN envelope limit. Thought
|
|
2813
|
+
* payloads are usually activity records, so preserving their structure and
|
|
2814
|
+
* truncating large string leaves is more useful than dropping the item.
|
|
2815
|
+
*/
|
|
2816
|
+
limitThoughtPayload(payload) {
|
|
2817
|
+
let serialized;
|
|
2818
|
+
try {
|
|
2819
|
+
serialized = JSON.stringify(payload);
|
|
2820
|
+
}
|
|
2821
|
+
catch {
|
|
2822
|
+
return { type: 'thought', text: '[thought unavailable: payload is not serializable]' };
|
|
2823
|
+
}
|
|
2824
|
+
if (typeof serialized !== 'string') {
|
|
2825
|
+
return { type: 'thought', text: '[thought unavailable: payload is not serializable]' };
|
|
2826
|
+
}
|
|
2827
|
+
if (Buffer.byteLength(serialized, 'utf8') <= AUN_THOUGHT_PLAINTEXT_MAX_BYTES)
|
|
2828
|
+
return payload;
|
|
2829
|
+
const limited = JSON.parse(serialized);
|
|
2830
|
+
limited.thought_truncated = true;
|
|
2831
|
+
const stringLeaves = () => {
|
|
2832
|
+
const leaves = [];
|
|
2833
|
+
const stack = [limited];
|
|
2834
|
+
while (stack.length > 0) {
|
|
2835
|
+
const current = stack.pop();
|
|
2836
|
+
if (!current || typeof current !== 'object')
|
|
2837
|
+
continue;
|
|
2838
|
+
for (const key of Object.keys(current)) {
|
|
2839
|
+
const value = current[key];
|
|
2840
|
+
if (typeof value === 'string') {
|
|
2841
|
+
leaves.push({ parent: current, key, value, bytes: Buffer.byteLength(value, 'utf8') });
|
|
2842
|
+
}
|
|
2843
|
+
else if (value && typeof value === 'object') {
|
|
2844
|
+
stack.push(value);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
return leaves;
|
|
2849
|
+
};
|
|
2850
|
+
let limitedBytes = Buffer.byteLength(JSON.stringify(limited), 'utf8');
|
|
2851
|
+
while (limitedBytes > AUN_THOUGHT_PLAINTEXT_MAX_BYTES) {
|
|
2852
|
+
const leaf = stringLeaves().sort((a, b) => b.bytes - a.bytes)[0];
|
|
2853
|
+
if (!leaf || leaf.bytes === 0)
|
|
2854
|
+
break;
|
|
2855
|
+
if (!Array.isArray(leaf.parent) && typeof leaf.key === 'string') {
|
|
2856
|
+
leaf.parent[`${leaf.key}_truncated`] = true;
|
|
2857
|
+
}
|
|
2858
|
+
limitedBytes = Buffer.byteLength(JSON.stringify(limited), 'utf8');
|
|
2859
|
+
const bytesToRemove = limitedBytes - AUN_THOUGHT_PLAINTEXT_MAX_BYTES + 32;
|
|
2860
|
+
const targetBytes = Math.max(0, leaf.bytes - bytesToRemove);
|
|
2861
|
+
const source = leaf.value.endsWith(AUN_THOUGHT_TRUNCATION_SUFFIX)
|
|
2862
|
+
? leaf.value.slice(0, -AUN_THOUGHT_TRUNCATION_SUFFIX.length)
|
|
2863
|
+
: leaf.value;
|
|
2864
|
+
leaf.parent[leaf.key] = truncateUtf8WithSuffix(source, targetBytes, AUN_THOUGHT_TRUNCATION_SUFFIX);
|
|
2865
|
+
const nextBytes = Buffer.byteLength(JSON.stringify(limited), 'utf8');
|
|
2866
|
+
if (nextBytes >= limitedBytes)
|
|
2867
|
+
break;
|
|
2868
|
+
limitedBytes = nextBytes;
|
|
2869
|
+
}
|
|
2870
|
+
if (limitedBytes <= AUN_THOUGHT_PLAINTEXT_MAX_BYTES) {
|
|
2871
|
+
logger.info(`${this.logPrefix()} Thought payload truncated: ${Buffer.byteLength(serialized, 'utf8')} -> ${limitedBytes} bytes`);
|
|
2872
|
+
return limited;
|
|
2873
|
+
}
|
|
2874
|
+
return this.thoughtPayloadFallback(limited);
|
|
2875
|
+
}
|
|
2876
|
+
thoughtPayloadFallback(payload) {
|
|
2877
|
+
const text = this.activityLogText(payload);
|
|
2878
|
+
const fallback = {
|
|
2879
|
+
type: 'thought',
|
|
2880
|
+
text: truncateUtf8WithSuffix(text, 256, AUN_THOUGHT_TRUNCATION_SUFFIX),
|
|
2881
|
+
thought_truncated: true,
|
|
2882
|
+
};
|
|
2883
|
+
if (typeof payload.chatmode === 'string')
|
|
2884
|
+
fallback.chatmode = truncateUtf8ToBytes(payload.chatmode, 32);
|
|
2885
|
+
logger.info(`${this.logPrefix()} Thought payload reduced to fallback (${Buffer.byteLength(JSON.stringify(fallback), 'utf8')} bytes)`);
|
|
2886
|
+
return fallback;
|
|
2887
|
+
}
|
|
2609
2888
|
rememberInvalidatedInteraction(interactionId, reason) {
|
|
2610
2889
|
this.invalidatedInteractions.set(interactionId, reason);
|
|
2611
2890
|
const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
|
|
2612
2891
|
timer.unref?.();
|
|
2613
2892
|
}
|
|
2614
|
-
async recallInteractionCard(messageId, channelId) {
|
|
2893
|
+
async recallInteractionCard(messageId, channelId, delivery) {
|
|
2615
2894
|
if (!this.client || !this.connected)
|
|
2616
2895
|
return false;
|
|
2617
2896
|
try {
|
|
2618
|
-
|
|
2897
|
+
if (!channelId || !delivery) {
|
|
2898
|
+
logger.warn(`${this.logPrefix()} Cannot recall interaction card without trusted delivery route: mid=${messageId}`);
|
|
2899
|
+
return false;
|
|
2900
|
+
}
|
|
2901
|
+
const isGroup = delivery.chatType === 'group';
|
|
2619
2902
|
const method = isGroup ? 'group.recall' : 'message.recall';
|
|
2620
2903
|
const params = isGroup
|
|
2621
|
-
? { group_id:
|
|
2904
|
+
? { group_id: delivery.groupId, message_ids: [messageId] }
|
|
2622
2905
|
: { message_ids: [messageId] };
|
|
2623
2906
|
await this.callAndTrace(method, params, { silentError: true });
|
|
2624
2907
|
return true;
|
|
@@ -2641,9 +2924,10 @@ export class AUNChannel {
|
|
|
2641
2924
|
}
|
|
2642
2925
|
const messageIds = [...(this.interactionCardMessageIds.get(interactionId) ?? [])];
|
|
2643
2926
|
for (const messageId of messageIds) {
|
|
2644
|
-
const
|
|
2927
|
+
const card = this.cardMessageIdMap.get(messageId);
|
|
2928
|
+
const channelId = card?.channelId;
|
|
2645
2929
|
this.forgetInteractionCard(messageId, interactionId);
|
|
2646
|
-
await this.recallInteractionCard(messageId, channelId);
|
|
2930
|
+
await this.recallInteractionCard(messageId, channelId, card?.delivery);
|
|
2647
2931
|
}
|
|
2648
2932
|
}
|
|
2649
2933
|
forgetInteractionCard(messageId, interactionId) {
|
|
@@ -2653,7 +2937,7 @@ export class AUNChannel {
|
|
|
2653
2937
|
if (ids && ids.size === 0)
|
|
2654
2938
|
this.interactionCardMessageIds.delete(interactionId);
|
|
2655
2939
|
}
|
|
2656
|
-
registerCardPostSend(messageId, action, channelId) {
|
|
2940
|
+
registerCardPostSend(messageId, action, channelId, delivery) {
|
|
2657
2941
|
if (action.type !== 'register_interaction_card')
|
|
2658
2942
|
return;
|
|
2659
2943
|
this.cardMessageIdMap.set(messageId, {
|
|
@@ -2661,6 +2945,7 @@ export class AUNChannel {
|
|
|
2661
2945
|
isCommandCard: action.isCommandCard,
|
|
2662
2946
|
initiatorAid: action.initiatorAid,
|
|
2663
2947
|
channelId,
|
|
2948
|
+
delivery: delivery ?? action.delivery,
|
|
2664
2949
|
});
|
|
2665
2950
|
this.ownedCardMsgIds.add(messageId);
|
|
2666
2951
|
let messageIds = this.interactionCardMessageIds.get(action.requestId);
|
|
@@ -2686,17 +2971,18 @@ export class AUNChannel {
|
|
|
2686
2971
|
const invalidationReason = this.invalidatedInteractions.get(action.requestId);
|
|
2687
2972
|
if (invalidationReason) {
|
|
2688
2973
|
this.cardMessageIdMap.delete(messageId);
|
|
2689
|
-
void this.recallInteractionCard(messageId, channelId);
|
|
2974
|
+
void this.recallInteractionCard(messageId, channelId, delivery ?? action.delivery);
|
|
2690
2975
|
}
|
|
2691
2976
|
}
|
|
2692
2977
|
runPostSend(entry, messageId) {
|
|
2693
2978
|
if (!entry.postSend)
|
|
2694
2979
|
return;
|
|
2695
2980
|
if (entry.postSend.type === 'register_interaction_card') {
|
|
2696
|
-
this.registerCardPostSend(messageId, entry.postSend, entry.channelId);
|
|
2981
|
+
this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery);
|
|
2697
2982
|
}
|
|
2698
2983
|
}
|
|
2699
2984
|
async sendAunPayload(channelId, payload, context, label) {
|
|
2985
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
2700
2986
|
let normalizedPayload;
|
|
2701
2987
|
try {
|
|
2702
2988
|
normalizedPayload = this.normalizeAunPayloadMentions(payload);
|
|
@@ -2709,7 +2995,7 @@ export class AUNChannel {
|
|
|
2709
2995
|
}
|
|
2710
2996
|
if (!this.client || !this.connected)
|
|
2711
2997
|
return { ok: false };
|
|
2712
|
-
const isGroup =
|
|
2998
|
+
const isGroup = delivery.chatType === 'group';
|
|
2713
2999
|
const targetAid = channelId;
|
|
2714
3000
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
2715
3001
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -2727,7 +3013,7 @@ export class AUNChannel {
|
|
|
2727
3013
|
const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
|
|
2728
3014
|
const params = { payload: truncatedPayload, encrypt };
|
|
2729
3015
|
if (isGroup)
|
|
2730
|
-
params.group_id =
|
|
3016
|
+
params.group_id = delivery.groupId;
|
|
2731
3017
|
else
|
|
2732
3018
|
params.to = targetAid;
|
|
2733
3019
|
const callOnce = async (sendParams, fallback) => {
|
|
@@ -2797,6 +3083,8 @@ export class AUNChannel {
|
|
|
2797
3083
|
this.forwardOutbound(result);
|
|
2798
3084
|
}
|
|
2799
3085
|
async sendContentPayload(channelId, payload, opts) {
|
|
3086
|
+
const delivery = this.requireDelivery(channelId, opts.context);
|
|
3087
|
+
const context = this.withDelivery(opts.context, delivery);
|
|
2800
3088
|
// Validate the caller's raw mentions before any early-return policy (such
|
|
2801
3089
|
// as a cancelled interaction) can hide a malformed wire payload.
|
|
2802
3090
|
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
@@ -2812,14 +3100,15 @@ export class AUNChannel {
|
|
|
2812
3100
|
// Validate the caller's raw mentions before deep-cleaning: that helper
|
|
2813
3101
|
// intentionally removes undefined array elements, which would otherwise
|
|
2814
3102
|
// turn malformed `mentions: [undefined]` into an apparently valid `[]`.
|
|
2815
|
-
const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload,
|
|
3103
|
+
const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
|
|
2816
3104
|
const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
|
|
2817
3105
|
const entry = outbox.enqueue(this.config.aid, {
|
|
2818
3106
|
channelId,
|
|
3107
|
+
delivery,
|
|
2819
3108
|
type: 'payload',
|
|
2820
3109
|
contentKind: opts.contentKind,
|
|
2821
3110
|
payload: finalPayload,
|
|
2822
|
-
context
|
|
3111
|
+
context,
|
|
2823
3112
|
logText,
|
|
2824
3113
|
ttl: opts.ttl,
|
|
2825
3114
|
postSend: opts.postSend,
|
|
@@ -2966,13 +3255,15 @@ export class AUNChannel {
|
|
|
2966
3255
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
2967
3256
|
return;
|
|
2968
3257
|
}
|
|
3258
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
3259
|
+
const routedContext = this.withDelivery(context, delivery);
|
|
2969
3260
|
// 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
|
|
2970
3261
|
if (!this.isEchoSending) {
|
|
2971
|
-
await this.flushPendingEcho(channelId);
|
|
3262
|
+
await this.flushPendingEcho(channelId, delivery);
|
|
2972
3263
|
}
|
|
2973
3264
|
let finalText = text;
|
|
2974
3265
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
2975
|
-
if (
|
|
3266
|
+
if (delivery.chatType === 'group') {
|
|
2976
3267
|
const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
|
|
2977
3268
|
.filter((id) => typeof id === 'string' && id.trim().length > 0)
|
|
2978
3269
|
.map(id => id.trim())));
|
|
@@ -2995,24 +3286,31 @@ export class AUNChannel {
|
|
|
2995
3286
|
}
|
|
2996
3287
|
}
|
|
2997
3288
|
}
|
|
2998
|
-
const operationId = typeof
|
|
2999
|
-
?
|
|
3289
|
+
const operationId = typeof routedContext.metadata?.operationId === 'string'
|
|
3290
|
+
? routedContext.metadata.operationId
|
|
3000
3291
|
: undefined;
|
|
3001
3292
|
if (operationId) {
|
|
3002
3293
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
3003
|
-
if (hasMessageLogOperation(chatDir, operationId))
|
|
3294
|
+
if (hasMessageLogOperation(chatDir, operationId)) {
|
|
3295
|
+
const duplicate = outbox.findByDedupeKey(this.config.aid, operationId);
|
|
3296
|
+
if (duplicate) {
|
|
3297
|
+
outbox.remove(this.config.aid, duplicate.id);
|
|
3298
|
+
logger.info(`${this.logPrefix()} Removed stale outbox entry for completed operation: ${operationId}`);
|
|
3299
|
+
}
|
|
3004
3300
|
return;
|
|
3301
|
+
}
|
|
3005
3302
|
}
|
|
3006
3303
|
// Write-ahead: persist to outbox before attempting send
|
|
3007
3304
|
const entry = outbox.enqueue(this.config.aid, {
|
|
3008
3305
|
channelId,
|
|
3306
|
+
delivery,
|
|
3009
3307
|
dedupeKey: operationId,
|
|
3010
|
-
critical:
|
|
3308
|
+
critical: routedContext.metadata?.criticalDelivery === true,
|
|
3011
3309
|
type: 'text',
|
|
3012
3310
|
text: finalText,
|
|
3013
|
-
context,
|
|
3014
|
-
ttl: typeof
|
|
3015
|
-
?
|
|
3311
|
+
context: routedContext,
|
|
3312
|
+
ttl: typeof routedContext.metadata?.outboxTtl === 'number'
|
|
3313
|
+
? routedContext.metadata.outboxTtl
|
|
3016
3314
|
: undefined,
|
|
3017
3315
|
});
|
|
3018
3316
|
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
@@ -3036,8 +3334,8 @@ export class AUNChannel {
|
|
|
3036
3334
|
}
|
|
3037
3335
|
/** Daemon-side transport for `ec msg send` running inside an agent task. */
|
|
3038
3336
|
async sendDaemonMsg(args) {
|
|
3039
|
-
if (!args.to
|
|
3040
|
-
return { ok: false, error: 'aun-msg-send
|
|
3337
|
+
if (!args.to) {
|
|
3338
|
+
return { ok: false, error: 'aun-msg-send requires a private target', code: 'UNSUPPORTED_TARGET' };
|
|
3041
3339
|
}
|
|
3042
3340
|
if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
|
|
3043
3341
|
return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
|
|
@@ -3161,8 +3459,8 @@ export class AUNChannel {
|
|
|
3161
3459
|
}
|
|
3162
3460
|
/** Daemon-side transport for `ec group send` running inside an agent task. */
|
|
3163
3461
|
async sendDaemonGroupMsg(args) {
|
|
3164
|
-
if (!args.groupId
|
|
3165
|
-
return { ok: false, error: 'aun
|
|
3462
|
+
if (!args.groupId) {
|
|
3463
|
+
return { ok: false, error: 'aun group send requires a group target', code: 'UNSUPPORTED_TARGET' };
|
|
3166
3464
|
}
|
|
3167
3465
|
if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
|
|
3168
3466
|
return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
|
|
@@ -3246,7 +3544,7 @@ export class AUNChannel {
|
|
|
3246
3544
|
return { ok: false, error: e?.message || String(e), code: e?.code };
|
|
3247
3545
|
}
|
|
3248
3546
|
}
|
|
3249
|
-
async flushPendingEcho(channelId) {
|
|
3547
|
+
async flushPendingEcho(channelId, delivery) {
|
|
3250
3548
|
// 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
|
|
3251
3549
|
for (const [key, echo] of this.pendingEchoMessages) {
|
|
3252
3550
|
if (echo.channelId === channelId) {
|
|
@@ -3270,18 +3568,32 @@ export class AUNChannel {
|
|
|
3270
3568
|
ts: Date.now(),
|
|
3271
3569
|
aid: this.config.aid,
|
|
3272
3570
|
channelId,
|
|
3571
|
+
delivery,
|
|
3273
3572
|
type: 'text',
|
|
3274
3573
|
text: echoText,
|
|
3574
|
+
context: this.withDelivery(echo.context, delivery),
|
|
3275
3575
|
ttl: 300_000,
|
|
3276
3576
|
};
|
|
3277
3577
|
const ok = await this.deliverTextEntry(echoEntry);
|
|
3278
3578
|
if (!ok) {
|
|
3279
|
-
outbox.enqueue(this.config.aid, {
|
|
3579
|
+
outbox.enqueue(this.config.aid, {
|
|
3580
|
+
channelId,
|
|
3581
|
+
delivery,
|
|
3582
|
+
type: 'text',
|
|
3583
|
+
text: echoText,
|
|
3584
|
+
context: this.withDelivery(echo.context, delivery),
|
|
3585
|
+
});
|
|
3280
3586
|
}
|
|
3281
3587
|
logger.info(`${this.logPrefix()} [Echo] long echo trace delivered=${ok} to ${channelId} (agent ${agentDuration}ms)`);
|
|
3282
3588
|
}
|
|
3283
3589
|
else {
|
|
3284
|
-
outbox.enqueue(this.config.aid, {
|
|
3590
|
+
outbox.enqueue(this.config.aid, {
|
|
3591
|
+
channelId,
|
|
3592
|
+
delivery,
|
|
3593
|
+
type: 'text',
|
|
3594
|
+
text: echoText,
|
|
3595
|
+
context: this.withDelivery(echo.context, delivery),
|
|
3596
|
+
});
|
|
3285
3597
|
logger.warn(`${this.logPrefix()} [Echo] not connected, echo trace queued in outbox`);
|
|
3286
3598
|
}
|
|
3287
3599
|
break;
|
|
@@ -3291,7 +3603,8 @@ export class AUNChannel {
|
|
|
3291
3603
|
async deliverTextEntry(entry) {
|
|
3292
3604
|
const channelId = entry.channelId;
|
|
3293
3605
|
const finalText = entry.text;
|
|
3294
|
-
const
|
|
3606
|
+
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
3607
|
+
const context = this.withDelivery(entry.context, delivery);
|
|
3295
3608
|
const operationId = entry.dedupeKey
|
|
3296
3609
|
|| (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
|
|
3297
3610
|
if (operationId) {
|
|
@@ -3309,7 +3622,7 @@ export class AUNChannel {
|
|
|
3309
3622
|
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
3310
3623
|
const source = context?.metadata?.source ?? 'daemon';
|
|
3311
3624
|
const payload = { type: 'text', text: finalText };
|
|
3312
|
-
if (
|
|
3625
|
+
if (delivery.chatType === 'group') {
|
|
3313
3626
|
const extracted = this.extractMentionEntriesFromText(finalText);
|
|
3314
3627
|
if (extracted.length > 0)
|
|
3315
3628
|
payload.mentions = extracted;
|
|
@@ -3323,7 +3636,7 @@ export class AUNChannel {
|
|
|
3323
3636
|
if (context?.metadata?.chatmode)
|
|
3324
3637
|
payload.chatmode = context.metadata.chatmode;
|
|
3325
3638
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
3326
|
-
const causationRef =
|
|
3639
|
+
const causationRef = delivery.chatType !== 'group' && causation
|
|
3327
3640
|
? `caus_${crypto.randomUUID().replace(/-/g, '')}`
|
|
3328
3641
|
: undefined;
|
|
3329
3642
|
if (causationRef && causation) {
|
|
@@ -3332,7 +3645,7 @@ export class AUNChannel {
|
|
|
3332
3645
|
}
|
|
3333
3646
|
// 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
|
|
3334
3647
|
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}`);
|
|
3335
|
-
const isGroup =
|
|
3648
|
+
const isGroup = delivery.chatType === 'group';
|
|
3336
3649
|
const targetAid = channelId;
|
|
3337
3650
|
if (operationId && entry.deliveryReceipt) {
|
|
3338
3651
|
this.appendOutboundJsonl(channelId, {
|
|
@@ -3506,7 +3819,8 @@ export class AUNChannel {
|
|
|
3506
3819
|
}
|
|
3507
3820
|
const contentKind = entry.contentKind;
|
|
3508
3821
|
const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
|
|
3509
|
-
const
|
|
3822
|
+
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
3823
|
+
const context = this.withDelivery(entry.context, delivery);
|
|
3510
3824
|
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}`);
|
|
3511
3825
|
const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
|
|
3512
3826
|
if (sent.discarded) {
|
|
@@ -3515,7 +3829,7 @@ export class AUNChannel {
|
|
|
3515
3829
|
}
|
|
3516
3830
|
if (!sent.ok || !sent.messageId)
|
|
3517
3831
|
return sent;
|
|
3518
|
-
const isGroup =
|
|
3832
|
+
const isGroup = delivery.chatType === 'group';
|
|
3519
3833
|
const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
|
|
3520
3834
|
this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
|
|
3521
3835
|
this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
|
|
@@ -3577,6 +3891,8 @@ export class AUNChannel {
|
|
|
3577
3891
|
async sendThought(channelId, taskId, payload, context) {
|
|
3578
3892
|
if (!taskId)
|
|
3579
3893
|
return;
|
|
3894
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
3895
|
+
context = this.withDelivery(context, delivery);
|
|
3580
3896
|
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
3581
3897
|
const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
|
|
3582
3898
|
if (!this.connected || !this.client)
|
|
@@ -3586,18 +3902,17 @@ export class AUNChannel {
|
|
|
3586
3902
|
const encrypt = context?.metadata?.encrypted != null
|
|
3587
3903
|
? !!(context.metadata.encrypted)
|
|
3588
3904
|
: this.shouldEncrypt(targetId);
|
|
3905
|
+
const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
|
|
3906
|
+
? { ...finalPayload, chatmode: context.metadata.chatmode }
|
|
3907
|
+
: finalPayload;
|
|
3589
3908
|
const params = {
|
|
3590
3909
|
context: { type: 'task', id: taskId },
|
|
3591
|
-
payload:
|
|
3910
|
+
payload: this.limitThoughtPayload(payloadWithChatmode),
|
|
3592
3911
|
encrypt,
|
|
3593
3912
|
};
|
|
3594
|
-
// 补齐 chatmode(与 deliverTextEntry/applyReplyContextToPayload 对齐)
|
|
3595
|
-
// 前端强依赖此字段用于消息过滤(proactive 模式的 thought 不显示在聊天流)
|
|
3596
|
-
if (context?.metadata?.chatmode && !params.payload.chatmode) {
|
|
3597
|
-
params.payload.chatmode = context.metadata.chatmode;
|
|
3598
|
-
}
|
|
3599
3913
|
try {
|
|
3600
|
-
|
|
3914
|
+
let thoughtPayload = params.payload;
|
|
3915
|
+
const items = thoughtPayload?.items;
|
|
3601
3916
|
const itemCount = Array.isArray(items) ? items.length : 1;
|
|
3602
3917
|
const stage = finalPayload?.stage ?? (finalPayload?.kind ? `kind=${finalPayload.kind}` : `items=${itemCount}`);
|
|
3603
3918
|
// 提取 thought 文本:兼容旧 items[] 和新扁平 activity payload。
|
|
@@ -3619,12 +3934,28 @@ export class AUNChannel {
|
|
|
3619
3934
|
}
|
|
3620
3935
|
}
|
|
3621
3936
|
else {
|
|
3622
|
-
thoughtText = this.activityLogText(
|
|
3937
|
+
thoughtText = this.activityLogText(thoughtPayload);
|
|
3623
3938
|
}
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3939
|
+
const method = delivery.chatType === 'group' ? 'group.thought.put' : 'message.thought.put';
|
|
3940
|
+
if (delivery.chatType === 'group')
|
|
3941
|
+
params.group_id = delivery.groupId;
|
|
3942
|
+
else
|
|
3943
|
+
params.to = targetId;
|
|
3944
|
+
let putRes;
|
|
3945
|
+
try {
|
|
3946
|
+
putRes = await this.callAndTrace(method, params);
|
|
3947
|
+
}
|
|
3948
|
+
catch (error) {
|
|
3949
|
+
if (!isThoughtTooLargeError(error))
|
|
3950
|
+
throw error;
|
|
3951
|
+
thoughtPayload = this.thoughtPayloadFallback(thoughtPayload);
|
|
3952
|
+
params.payload = thoughtPayload;
|
|
3953
|
+
thoughtText = this.activityLogText(thoughtPayload);
|
|
3954
|
+
logger.warn(`${this.logPrefix()} Retrying ${method} with minimal payload after server size rejection`);
|
|
3955
|
+
putRes = await this.callAndTrace(method, params);
|
|
3956
|
+
}
|
|
3957
|
+
const tid = putRes?.thought_id;
|
|
3958
|
+
if (delivery.chatType === 'group') {
|
|
3628
3959
|
logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
3629
3960
|
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
3630
3961
|
this.forwardOutbound(putRes);
|
|
@@ -3634,9 +3965,6 @@ export class AUNChannel {
|
|
|
3634
3965
|
}
|
|
3635
3966
|
}
|
|
3636
3967
|
else {
|
|
3637
|
-
params.to = targetId;
|
|
3638
|
-
const putRes = await this.callAndTrace('message.thought.put', params);
|
|
3639
|
-
const tid = putRes?.thought_id;
|
|
3640
3968
|
logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
3641
3969
|
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
3642
3970
|
this.forwardOutbound(putRes);
|
|
@@ -3652,13 +3980,15 @@ export class AUNChannel {
|
|
|
3652
3980
|
}
|
|
3653
3981
|
/** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
|
|
3654
3982
|
async sendStructured(channelId, payload, context) {
|
|
3983
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
3984
|
+
context = this.withDelivery(context, delivery);
|
|
3655
3985
|
// Validate before checking connection state so malformed producer payloads
|
|
3656
3986
|
// are reported deterministically instead of being hidden by a disconnect.
|
|
3657
3987
|
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
3658
3988
|
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
3659
3989
|
if (!this.connected || !this.client)
|
|
3660
3990
|
return null;
|
|
3661
|
-
const isGroup =
|
|
3991
|
+
const isGroup = delivery.chatType === 'group';
|
|
3662
3992
|
const targetAid = channelId;
|
|
3663
3993
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
3664
3994
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -3684,7 +4014,7 @@ export class AUNChannel {
|
|
|
3684
4014
|
}
|
|
3685
4015
|
try {
|
|
3686
4016
|
if (isGroup) {
|
|
3687
|
-
params.group_id =
|
|
4017
|
+
params.group_id = delivery.groupId;
|
|
3688
4018
|
const result = await this.callAndTrace('group.send', params);
|
|
3689
4019
|
const mid = result?.message?.message_id ?? result?.message_id ?? null;
|
|
3690
4020
|
logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
|
|
@@ -3708,6 +4038,8 @@ export class AUNChannel {
|
|
|
3708
4038
|
}
|
|
3709
4039
|
}
|
|
3710
4040
|
async sendFile(channelId, filePath, context) {
|
|
4041
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
4042
|
+
context = this.withDelivery(context, delivery);
|
|
3711
4043
|
const absPath = path.resolve(filePath);
|
|
3712
4044
|
if (!fs.existsSync(absPath)) {
|
|
3713
4045
|
logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
|
|
@@ -3725,6 +4057,7 @@ export class AUNChannel {
|
|
|
3725
4057
|
// Write-ahead: persist to outbox
|
|
3726
4058
|
const entry = outbox.enqueue(this.config.aid, {
|
|
3727
4059
|
channelId,
|
|
4060
|
+
delivery,
|
|
3728
4061
|
type: 'file',
|
|
3729
4062
|
filePath: absPath,
|
|
3730
4063
|
context,
|
|
@@ -3743,6 +4076,8 @@ export class AUNChannel {
|
|
|
3743
4076
|
}
|
|
3744
4077
|
}
|
|
3745
4078
|
async sendImage(channelId, data, mimeType, alt, context) {
|
|
4079
|
+
const delivery = this.requireDelivery(channelId, context);
|
|
4080
|
+
context = this.withDelivery(context, delivery);
|
|
3746
4081
|
if (!Buffer.isBuffer(data)) {
|
|
3747
4082
|
throw new Error('result.image data must be a Buffer');
|
|
3748
4083
|
}
|
|
@@ -3764,6 +4099,7 @@ export class AUNChannel {
|
|
|
3764
4099
|
const logText = alt ? `[image] ${alt}` : '[image]';
|
|
3765
4100
|
const entry = outbox.enqueue(this.config.aid, {
|
|
3766
4101
|
channelId,
|
|
4102
|
+
delivery,
|
|
3767
4103
|
type: 'image',
|
|
3768
4104
|
contentKind: 'image',
|
|
3769
4105
|
image: { dataBase64: data.toString('base64'), mimeType: normalizedMimeType, alt },
|
|
@@ -3822,6 +4158,9 @@ export class AUNChannel {
|
|
|
3822
4158
|
logger.warn(`${this.logPrefix()} deliverImageEntry: missing image data (outbox id=${entry.id})`);
|
|
3823
4159
|
return true;
|
|
3824
4160
|
}
|
|
4161
|
+
const delivery = this.requirePersistedDelivery(entry.channelId, entry.delivery);
|
|
4162
|
+
entry.delivery = delivery;
|
|
4163
|
+
entry.context = this.withDelivery(entry.context, delivery);
|
|
3825
4164
|
const data = Buffer.from(image.dataBase64, 'base64');
|
|
3826
4165
|
if (data.length === 0) {
|
|
3827
4166
|
logger.warn(`${this.logPrefix()} deliverImageEntry: invalid image data (outbox id=${entry.id})`);
|
|
@@ -3839,6 +4178,7 @@ export class AUNChannel {
|
|
|
3839
4178
|
if (!outbox.replace(this.config.aid, entry)) {
|
|
3840
4179
|
payloadEntry = outbox.enqueue(this.config.aid, {
|
|
3841
4180
|
channelId: entry.channelId,
|
|
4181
|
+
delivery,
|
|
3842
4182
|
type: 'payload',
|
|
3843
4183
|
contentKind: 'image',
|
|
3844
4184
|
payload: entry.payload,
|
|
@@ -3856,7 +4196,8 @@ export class AUNChannel {
|
|
|
3856
4196
|
async deliverFileEntry(entry) {
|
|
3857
4197
|
const channelId = entry.channelId;
|
|
3858
4198
|
const absPath = entry.filePath;
|
|
3859
|
-
const
|
|
4199
|
+
const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
|
|
4200
|
+
const context = this.withDelivery(entry.context, delivery);
|
|
3860
4201
|
if (!fs.existsSync(absPath)) {
|
|
3861
4202
|
logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
|
|
3862
4203
|
return true; // remove from outbox, file no longer exists
|
|
@@ -3914,7 +4255,7 @@ export class AUNChannel {
|
|
|
3914
4255
|
attachment,
|
|
3915
4256
|
context,
|
|
3916
4257
|
});
|
|
3917
|
-
const isGroup =
|
|
4258
|
+
const isGroup = delivery.chatType === 'group';
|
|
3918
4259
|
const fileTargetAid = channelId;
|
|
3919
4260
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
3920
4261
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -3925,7 +4266,7 @@ export class AUNChannel {
|
|
|
3925
4266
|
let sentMid = null;
|
|
3926
4267
|
try {
|
|
3927
4268
|
if (isGroup) {
|
|
3928
|
-
params.group_id =
|
|
4269
|
+
params.group_id = delivery.groupId;
|
|
3929
4270
|
this.trace('OUT', 'group.send.file', params);
|
|
3930
4271
|
const result = await this.client.call('group.send', params);
|
|
3931
4272
|
sendResult = result;
|
|
@@ -4033,6 +4374,10 @@ export class AUNChannel {
|
|
|
4033
4374
|
return;
|
|
4034
4375
|
logger.info(`${this.logPrefix()} Draining outbox...`);
|
|
4035
4376
|
const result = await outbox.drain(this.config.aid, async (entry) => {
|
|
4377
|
+
if (!isDeliveryTarget(entry.delivery)) {
|
|
4378
|
+
logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
|
|
4379
|
+
return true;
|
|
4380
|
+
}
|
|
4036
4381
|
if (entry.type === 'text') {
|
|
4037
4382
|
return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
|
|
4038
4383
|
}
|
|
@@ -4058,11 +4403,23 @@ export class AUNChannel {
|
|
|
4058
4403
|
this.messageSeqMap.delete(messageId);
|
|
4059
4404
|
}
|
|
4060
4405
|
sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
|
|
4406
|
+
let delivery;
|
|
4407
|
+
try {
|
|
4408
|
+
delivery = this.requireDelivery(channelId, context);
|
|
4409
|
+
}
|
|
4410
|
+
catch (error) {
|
|
4411
|
+
logger.warn(`${this.logPrefix()} task.${status} notify skipped without trusted delivery route: channel=${channelId} task=${taskId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
context = this.withDelivery(context, delivery);
|
|
4061
4415
|
if (status === 'start')
|
|
4062
4416
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
4063
4417
|
if (!this.client || !this.connected)
|
|
4064
4418
|
return;
|
|
4065
|
-
const
|
|
4419
|
+
const requestedSeverity = extraMeta?.severity;
|
|
4420
|
+
const severity = requestedSeverity === 'warn'
|
|
4421
|
+
? 'warn'
|
|
4422
|
+
: status === 'error' || status === 'timeout' ? 'error' : 'info';
|
|
4066
4423
|
const stateMap = {
|
|
4067
4424
|
start: 'started',
|
|
4068
4425
|
done: 'completed',
|
|
@@ -4081,7 +4438,10 @@ export class AUNChannel {
|
|
|
4081
4438
|
severity,
|
|
4082
4439
|
terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
|
|
4083
4440
|
};
|
|
4084
|
-
const
|
|
4441
|
+
const metadataInput = extraMeta ? { ...extraMeta } : undefined;
|
|
4442
|
+
if (metadataInput)
|
|
4443
|
+
delete metadataInput.severity;
|
|
4444
|
+
const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
|
|
4085
4445
|
if (metadata && Object.keys(metadata).length > 0)
|
|
4086
4446
|
statusPayload.metadata = metadata;
|
|
4087
4447
|
if (context?.threadId)
|
|
@@ -4093,8 +4453,8 @@ export class AUNChannel {
|
|
|
4093
4453
|
if (context?.replyToMessageId)
|
|
4094
4454
|
statusPayload.ref_message_id = context.replyToMessageId;
|
|
4095
4455
|
const notifyOptions = { ttlMs: 60_000 };
|
|
4096
|
-
if (
|
|
4097
|
-
notifyOptions.groupId =
|
|
4456
|
+
if (delivery.chatType === 'group')
|
|
4457
|
+
notifyOptions.groupId = delivery.groupId;
|
|
4098
4458
|
else
|
|
4099
4459
|
notifyOptions.to = channelId;
|
|
4100
4460
|
this.trace('OUT', 'notify.task_status', {
|
|
@@ -4115,7 +4475,7 @@ export class AUNChannel {
|
|
|
4115
4475
|
}).catch(() => { });
|
|
4116
4476
|
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true, undefined, undefined, 'notify');
|
|
4117
4477
|
// 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
|
|
4118
|
-
const targetLabel =
|
|
4478
|
+
const targetLabel = delivery.chatType === 'group' ? delivery.groupId : this.peerLabel(channelId);
|
|
4119
4479
|
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
4120
4480
|
const initiator = statusPayload.initiator ?? '';
|
|
4121
4481
|
const refMsgId = statusPayload.ref_message_id ?? '';
|
|
@@ -4127,8 +4487,6 @@ export class AUNChannel {
|
|
|
4127
4487
|
logger.info(logTaskStatus);
|
|
4128
4488
|
}
|
|
4129
4489
|
async sendCustomPayload(channelId, payload, context) {
|
|
4130
|
-
if (!this.client || !this.connected)
|
|
4131
|
-
return;
|
|
4132
4490
|
// SDK 0.3.0 E2EE requires payload to be an object
|
|
4133
4491
|
let payloadObj;
|
|
4134
4492
|
try {
|
|
@@ -4362,21 +4720,25 @@ export class AUNChannelPlugin {
|
|
|
4362
4720
|
channelKey: inst.name, // channelName 实际上就是 channelKey
|
|
4363
4721
|
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true, authenticatedApproval: true },
|
|
4364
4722
|
send: async (envelope, payload) => {
|
|
4723
|
+
const channelId = envelope?.channelId;
|
|
4724
|
+
const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
|
|
4365
4725
|
const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
|
|
4366
4726
|
const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
|
|
4367
4727
|
const replyCtx = outboundCausation
|
|
4368
4728
|
? {
|
|
4369
4729
|
...(envelope.replyContext ?? {}),
|
|
4730
|
+
delivery,
|
|
4370
4731
|
metadata: { ...(envelope.replyContext?.metadata ?? {}), causation: outboundCausation },
|
|
4371
4732
|
}
|
|
4372
|
-
: envelope.replyContext
|
|
4733
|
+
: (envelope.replyContext || envelope.delivery)
|
|
4734
|
+
? { ...(envelope.replyContext ?? {}), delivery }
|
|
4735
|
+
: undefined;
|
|
4373
4736
|
if (outboundCausation) {
|
|
4374
4737
|
recordCausationSpan(outboundCausation, 'message.outbound', {
|
|
4375
4738
|
status: 'started',
|
|
4376
4739
|
refs: { taskId: envelope.taskId, sessionId: envelope.sessionId },
|
|
4377
4740
|
});
|
|
4378
4741
|
}
|
|
4379
|
-
const channelId = envelope.channelId;
|
|
4380
4742
|
const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
|
|
4381
4743
|
switch (payload.kind) {
|
|
4382
4744
|
case 'result.text':
|
|
@@ -4505,7 +4867,7 @@ export class AUNChannelPlugin {
|
|
|
4505
4867
|
aunCard.text = action.body;
|
|
4506
4868
|
setIfDefined(aunCard, 'format', action.bodyFormat);
|
|
4507
4869
|
}
|
|
4508
|
-
if (req.initiatorId &&
|
|
4870
|
+
if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
|
|
4509
4871
|
aunCard.initiator = req.initiatorId;
|
|
4510
4872
|
if (replyCtx?.threadId)
|
|
4511
4873
|
aunCard.thread_id = replyCtx.threadId;
|
|
@@ -4518,6 +4880,7 @@ export class AUNChannelPlugin {
|
|
|
4518
4880
|
requestId: req.id,
|
|
4519
4881
|
isCommandCard: false,
|
|
4520
4882
|
initiatorAid: req.initiatorId,
|
|
4883
|
+
delivery: replyCtx?.delivery,
|
|
4521
4884
|
expiresAt: Date.now() + cardTtlMs,
|
|
4522
4885
|
},
|
|
4523
4886
|
});
|
|
@@ -4542,7 +4905,7 @@ export class AUNChannelPlugin {
|
|
|
4542
4905
|
aunCard.text = card.body;
|
|
4543
4906
|
setIfDefined(aunCard, 'format', card.bodyFormat);
|
|
4544
4907
|
}
|
|
4545
|
-
if (req.initiatorId &&
|
|
4908
|
+
if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
|
|
4546
4909
|
aunCard.initiator = req.initiatorId;
|
|
4547
4910
|
if (replyCtx?.threadId)
|
|
4548
4911
|
aunCard.thread_id = replyCtx.threadId;
|
|
@@ -4555,6 +4918,7 @@ export class AUNChannelPlugin {
|
|
|
4555
4918
|
requestId: req.id,
|
|
4556
4919
|
isCommandCard: true,
|
|
4557
4920
|
initiatorAid: req.initiatorId,
|
|
4921
|
+
delivery: replyCtx?.delivery,
|
|
4558
4922
|
expiresAt: Date.now() + cardTtlMs,
|
|
4559
4923
|
},
|
|
4560
4924
|
});
|