evolcore 0.0.11 → 0.0.12
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 +22 -0
- package/bin/codex-managed-hook.mjs +80 -0
- package/dist/agents/claude-runner.js +35 -2
- package/dist/agents/codex-app-server-client.js +120 -14
- package/dist/agents/codex-runner.js +273 -35
- package/dist/agents/ecagent-runner.js +36 -4
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/aun/aid/client.js +11 -21
- package/dist/aun/aid/managed-operation.js +107 -0
- package/dist/aun/msg/group.js +81 -12
- package/dist/aun/msg/managed-operation.js +804 -0
- package/dist/aun/msg/mention-schema.js +20 -0
- package/dist/aun/msg/p2p.js +7 -0
- package/dist/channels/aun.js +288 -108
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +56 -8
- package/dist/cli/agent.js +170 -6
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/daemon-commands.js +31 -31
- package/dist/cli/fs-command.js +60 -0
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +30 -2
- package/dist/cli/managed-command-guard.js +41 -0
- package/dist/cli/model.js +11 -4
- package/dist/cli/queue-command.js +11 -1
- package/dist/cli/response.js +71 -0
- package/dist/cli/restart-monitor.js +4 -18
- package/dist/cli/trigger-command.js +4 -0
- package/dist/config/builtin-roles.js +4 -0
- 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/core/auth/agent-delegation.js +0 -1
- package/dist/core/auth/operation-authorizer.js +30 -7
- package/dist/core/auth/operation-catalog.js +187 -25
- package/dist/core/bootstrap-messages.js +18 -0
- package/dist/core/bootstrap-service.js +63 -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 +467 -6
- 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 +366 -0
- package/dist/core/command/menu-handler.js +147 -35
- package/dist/core/command/menu-protocol.js +71 -0
- package/dist/core/command/slash-gate.js +5 -2
- package/dist/core/command/slash-handler.js +41 -21
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/evolagent.js +4 -2
- package/dist/core/handoff/dispatcher.js +5 -2
- package/dist/core/handoff/runtime.js +145 -21
- package/dist/core/handoff/store.js +24 -0
- package/dist/core/handoff/types.js +1 -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 +236 -78
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +1 -0
- package/dist/core/message/response-engine.js +54 -36
- package/dist/core/permission/ec-command-parser.js +156 -10
- package/dist/core/permission/sandbox-runtime.js +22 -1
- package/dist/core/permission/tool-policy.js +73 -30
- package/dist/core/protected-paths.js +7 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +384 -98
- package/dist/ipc.js +218 -9
- 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/script-executor.js +1 -0
- 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/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/prompt-loading-architecture.md +1 -2
- package/kits/docs/venues/group.md +1 -1
- package/kits/eck_manifest.json +0 -12
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- 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 +22 -0
- package/kits/templates/roles/member.json +27 -0
- package/kits/templates/roles/visitor.json +33 -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/core/command/evol-menu-version-gate.js +0 -39
package/dist/channels/aun.js
CHANGED
|
@@ -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';
|
|
@@ -34,7 +34,9 @@ import { recordCausationSpan } from '../core/causation/audit.js';
|
|
|
34
34
|
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
35
35
|
import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
36
36
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
37
|
-
import { postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox } from '../core/bootstrap-messages.js';
|
|
37
|
+
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
38
|
+
import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
|
|
39
|
+
import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
|
|
38
40
|
export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
|
|
39
41
|
const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
40
42
|
const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
|
|
@@ -139,7 +141,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
|
|
|
139
141
|
replyContext: opts.replyContext,
|
|
140
142
|
source: opts.source,
|
|
141
143
|
images: opts.images,
|
|
142
|
-
|
|
144
|
+
mentionMode: opts.mentionMode,
|
|
143
145
|
};
|
|
144
146
|
}
|
|
145
147
|
function verifiedProtectedHeaders(msg, env) {
|
|
@@ -698,24 +700,20 @@ export class AUNChannel {
|
|
|
698
700
|
.replace(/[ \t]+/g, ' ')
|
|
699
701
|
.trim();
|
|
700
702
|
}
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
aids.push(m.aid);
|
|
703
|
+
parsePayloadMentionsOrReject(payload, context, messageId, seq) {
|
|
704
|
+
const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
705
|
+
? payload.mentions
|
|
706
|
+
: undefined;
|
|
707
|
+
try {
|
|
708
|
+
return normalizeAunMentionEntries(raw);
|
|
708
709
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (m && typeof m === 'object' && m.scope === 'all')
|
|
716
|
-
return true;
|
|
710
|
+
catch (error) {
|
|
711
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
712
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
713
|
+
const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
|
|
714
|
+
logger.error(`${this.logPrefix()} Dropped ${context}: invalid payload.mentions (${detail}) code=${code} mid=${messageId}`);
|
|
715
|
+
return undefined;
|
|
717
716
|
}
|
|
718
|
-
return false;
|
|
719
717
|
}
|
|
720
718
|
/** 从正文提取 @aid(AID 格式:至少三段域名),用于出站填充 payload.mentions */
|
|
721
719
|
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 +729,19 @@ export class AUNChannel {
|
|
|
731
729
|
}
|
|
732
730
|
return out;
|
|
733
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Convert the AUN text mention notation into the canonical wire entries.
|
|
734
|
+
* `@all` is intentionally represented by its scope discriminator instead
|
|
735
|
+
* of being treated as an AID.
|
|
736
|
+
*/
|
|
737
|
+
extractMentionEntriesFromText(text) {
|
|
738
|
+
const entries = [];
|
|
739
|
+
if (this.hasExplicitMention(text, 'all'))
|
|
740
|
+
entries.push({ scope: 'all' });
|
|
741
|
+
for (const aid of this.extractMentionAidsFromText(text))
|
|
742
|
+
entries.push({ aid });
|
|
743
|
+
return entries;
|
|
744
|
+
}
|
|
734
745
|
/** Sanitize a raw topicName from untrusted payload: trim, strip control chars, enforce length. */
|
|
735
746
|
sanitizeTopicName(raw) {
|
|
736
747
|
if (typeof raw !== 'string')
|
|
@@ -755,6 +766,31 @@ export class AUNChannel {
|
|
|
755
766
|
}
|
|
756
767
|
return replyContext;
|
|
757
768
|
}
|
|
769
|
+
claimInboundMessage(kind, messageId, seq) {
|
|
770
|
+
if (!messageId)
|
|
771
|
+
return true;
|
|
772
|
+
// Channel instance names are commonly reused across agents (the default
|
|
773
|
+
// is simply `aun`). Include the receiving AID so a server-scoped
|
|
774
|
+
// message_id cannot suppress an unrelated delivery in another channel.
|
|
775
|
+
const namespace = `${this.config.aid}:${this.config.channelName ?? 'aun'}:aun:${kind}`;
|
|
776
|
+
const dedupeKey = JSON.stringify([namespace, messageId]);
|
|
777
|
+
const existing = this.inboundSeenMessages.get(dedupeKey);
|
|
778
|
+
if (existing) {
|
|
779
|
+
if (seq !== undefined && existing.seq !== undefined && seq !== existing.seq) {
|
|
780
|
+
logger.warn(`${this.logPrefix()} Duplicate message_id with conflicting seq: namespace=${namespace} mid=${messageId} firstSeq=${existing.seq} duplicateSeq=${seq}`);
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
logger.debug(`${this.logPrefix()} Duplicate inbound message dropped: namespace=${namespace} mid=${messageId} seq=${seq ?? 'unknown'}`);
|
|
784
|
+
}
|
|
785
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
this.inboundSeenMessages.set(dedupeKey, { seenAt: Date.now(), seq });
|
|
789
|
+
if (seq !== undefined)
|
|
790
|
+
this.messageSeqMap.set(messageId, seq);
|
|
791
|
+
setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), 5 * 60 * 1000);
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
758
794
|
acknowledgeImmediately(messageId, _seq) {
|
|
759
795
|
// SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
|
|
760
796
|
// as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
|
|
@@ -771,10 +807,16 @@ export class AUNChannel {
|
|
|
771
807
|
_aid;
|
|
772
808
|
_selfName; // 本地 agent.md 中的 name,首次 connect 时读取
|
|
773
809
|
_chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
|
|
774
|
-
|
|
810
|
+
/**
|
|
811
|
+
* Inbound application dedupe. The AUN SDK may redeliver the same message
|
|
812
|
+
* with a different transport sequence, so message_id is the identity and
|
|
813
|
+
* seq is only retained for ACK/conflict diagnostics. Claiming happens at
|
|
814
|
+
* the event handler boundary, before payload/slash/mention parsing.
|
|
815
|
+
*/
|
|
816
|
+
inboundSeenMessages = new Map();
|
|
775
817
|
groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
|
|
776
818
|
peerInfoCache = new Map();
|
|
777
|
-
messageSeqMap = new Map(); // messageId → seq (for ack)
|
|
819
|
+
messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
|
|
778
820
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
779
821
|
peerE2ee = new Map();
|
|
780
822
|
static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
|
|
@@ -789,7 +831,7 @@ export class AUNChannel {
|
|
|
789
831
|
interactionCardMessageIds = new Map();
|
|
790
832
|
/** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
|
|
791
833
|
invalidatedInteractions = new Map();
|
|
792
|
-
|
|
834
|
+
mentionModeResolver;
|
|
793
835
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
794
836
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
795
837
|
'merge', 'link', 'location', 'personal_card',
|
|
@@ -798,6 +840,10 @@ export class AUNChannel {
|
|
|
798
840
|
static INJECT_REQUEST_TYPE = 'observer.inject';
|
|
799
841
|
/** 绑定请求类型(Evol App → 控制 AID):快速路径,绕过白名单透传原始 payload JSON 给 bridge。 */
|
|
800
842
|
static BIND_REQUEST_TYPE = 'bind.request';
|
|
843
|
+
/** 联系人申请本人状态查询:在普通消息准入前交给 bridge,身份只取认证信封。 */
|
|
844
|
+
static CONTACT_REQUEST_QUERY_TYPE = 'contact.request.query';
|
|
845
|
+
/** App 联系人申请提交:结构化控制请求,不是 slash command。 */
|
|
846
|
+
static CONTACT_REQUEST_SUBMIT_TYPE = 'contact.request.submit';
|
|
801
847
|
// Reconnect state
|
|
802
848
|
// SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
|
|
803
849
|
// 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
|
|
@@ -1138,6 +1184,14 @@ export class AUNChannel {
|
|
|
1138
1184
|
return false;
|
|
1139
1185
|
return this.reconcilePostBootstrapWelcome();
|
|
1140
1186
|
}
|
|
1187
|
+
async hasPendingPostBootstrapWelcome() {
|
|
1188
|
+
const aid = this.config.aid.replace(/^@/, '');
|
|
1189
|
+
return hasPendingPostBootstrapWelcomeOutbox(aid);
|
|
1190
|
+
}
|
|
1191
|
+
async bindPostBootstrapWelcomeToSession(sessionId) {
|
|
1192
|
+
const aid = this.config.aid.replace(/^@/, '');
|
|
1193
|
+
return bindPostBootstrapWelcomeOutboxSession(aid, sessionId);
|
|
1194
|
+
}
|
|
1141
1195
|
/**
|
|
1142
1196
|
* Resume delivery of an already prepared completion welcome.
|
|
1143
1197
|
*
|
|
@@ -1394,12 +1448,21 @@ export class AUNChannel {
|
|
|
1394
1448
|
if (!data || typeof data !== 'object')
|
|
1395
1449
|
return;
|
|
1396
1450
|
const msg = data;
|
|
1451
|
+
// Claim before any payload extraction or command parsing. Retransmits may
|
|
1452
|
+
// carry a new seq, but the application identity remains message_id.
|
|
1453
|
+
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
1454
|
+
const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
|
|
1455
|
+
if (!this.claimInboundMessage('private', messageId, seq))
|
|
1456
|
+
return;
|
|
1397
1457
|
// SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1398
1458
|
// message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
|
|
1399
1459
|
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1400
1460
|
const protectedHeaders = verifiedProtectedHeaders(msg, env);
|
|
1401
1461
|
const fromAid = env.from ?? '';
|
|
1402
1462
|
const payload = msg.payload ?? '';
|
|
1463
|
+
const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
|
|
1464
|
+
if (!mentions)
|
|
1465
|
+
return;
|
|
1403
1466
|
const causationRef = payload && typeof payload === 'object' && typeof payload._evolcore_causation_ref === 'string'
|
|
1404
1467
|
? payload._evolcore_causation_ref
|
|
1405
1468
|
: undefined;
|
|
@@ -1413,15 +1476,19 @@ export class AUNChannel {
|
|
|
1413
1476
|
const topicName = (typeof payload === 'object' && payload !== null)
|
|
1414
1477
|
? this.sanitizeTopicName(payload.topicName)
|
|
1415
1478
|
: undefined;
|
|
1416
|
-
const messageId = msg.message_id ?? '';
|
|
1417
1479
|
const causation = consumeAunCausation(causationRef || messageId, fromAid, this._aid || this.config.aid)
|
|
1418
1480
|
?? (causationRef ? consumeAunCausation(messageId, fromAid, this._aid || this.config.aid) : undefined);
|
|
1419
|
-
const
|
|
1481
|
+
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
1482
|
+
// 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
|
|
1483
|
+
if (inboundType === 'error') {
|
|
1484
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1485
|
+
logger.info(`${this.logPrefix()} P2P dropped terminal error: from=${fromAid || 'unknown'} mid=${messageId}`);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1420
1488
|
// Observer forward (inbound):在所有过滤之前转发原始明文 payload。
|
|
1421
1489
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
1422
1490
|
// 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
|
|
1423
1491
|
// (即便日后 from-owner 排除规则调整,也不会泄漏)。
|
|
1424
|
-
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
1425
1492
|
if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
|
|
1426
1493
|
this.forwardInbound(msg);
|
|
1427
1494
|
}
|
|
@@ -1437,10 +1504,13 @@ export class AUNChannel {
|
|
|
1437
1504
|
const msgEncrypted = !!env.encrypted;
|
|
1438
1505
|
if (!msgEncrypted)
|
|
1439
1506
|
this.plaintextRecv++;
|
|
1440
|
-
//
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1507
|
+
// Preserve structured mentions for the shared inbound message model.
|
|
1508
|
+
// Text scanning remains only a rendering fallback and is not promoted to
|
|
1509
|
+
// a structured @all decision.
|
|
1510
|
+
if (mentions.length === 0 && this._aid && text.includes(`@${this._aid}`)) {
|
|
1511
|
+
// Private channels historically exposed a self mention even when the
|
|
1512
|
+
// sender omitted payload.mentions; keep that compatibility signal.
|
|
1513
|
+
mentions.push({ aid: this._aid });
|
|
1444
1514
|
}
|
|
1445
1515
|
// Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
|
|
1446
1516
|
const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
|
|
@@ -1462,8 +1532,10 @@ export class AUNChannel {
|
|
|
1462
1532
|
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1463
1533
|
return;
|
|
1464
1534
|
}
|
|
1465
|
-
// menu.*
|
|
1466
|
-
if (p2pPayloadType.startsWith('menu.')
|
|
1535
|
+
// menu.* 与联系人本人状态查询:需要原始 payload JSON 传递给 bridge。
|
|
1536
|
+
if (p2pPayloadType.startsWith('menu.')
|
|
1537
|
+
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
|
|
1538
|
+
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
|
|
1467
1539
|
this.acknowledgeImmediately(messageId, seq);
|
|
1468
1540
|
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1469
1541
|
this.dispatchMessage({
|
|
@@ -1474,6 +1546,8 @@ export class AUNChannel {
|
|
|
1474
1546
|
peerType: peerIdentity.type,
|
|
1475
1547
|
encrypted: msgEncrypted,
|
|
1476
1548
|
protectedHeaders,
|
|
1549
|
+
mentions,
|
|
1550
|
+
mentionAids: mentionEntryAids(mentions),
|
|
1477
1551
|
replyContext: this.buildGroupReplyContext(threadId, fromAid, msgEncrypted, messageId),
|
|
1478
1552
|
msgType: logDescriptor.msgType,
|
|
1479
1553
|
payloadType: logDescriptor.payloadType,
|
|
@@ -1502,6 +1576,8 @@ export class AUNChannel {
|
|
|
1502
1576
|
peerName: displayName || undefined,
|
|
1503
1577
|
peerType: peerIdentity.type,
|
|
1504
1578
|
encrypted: msgEncrypted,
|
|
1579
|
+
mentions,
|
|
1580
|
+
mentionAids: mentionEntryAids(mentions),
|
|
1505
1581
|
msgType: logDescriptor.msgType,
|
|
1506
1582
|
payloadType: logDescriptor.payloadType,
|
|
1507
1583
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -1561,28 +1637,38 @@ export class AUNChannel {
|
|
|
1561
1637
|
if (!data || typeof data !== 'object')
|
|
1562
1638
|
return;
|
|
1563
1639
|
const msg = data;
|
|
1640
|
+
// Deduplicate at the adapter boundary, before mention/slash routing.
|
|
1641
|
+
const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
|
|
1642
|
+
const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
|
|
1643
|
+
if (!this.claimInboundMessage('group', messageId, seq))
|
|
1644
|
+
return;
|
|
1564
1645
|
// SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1565
|
-
// message_id / seq / payload / same_* /
|
|
1646
|
+
// message_id / seq / payload / same_* / mention_mode 等仍是顶层独立字段,不在 envelope 内。
|
|
1566
1647
|
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1567
1648
|
const protectedHeaders = verifiedProtectedHeaders(msg, env);
|
|
1568
1649
|
const groupId = env.group_id ?? '';
|
|
1569
1650
|
const senderAid = env.from ?? '';
|
|
1570
1651
|
const payload = msg.payload ?? '';
|
|
1652
|
+
const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
|
|
1653
|
+
if (!payloadMentionEntries)
|
|
1654
|
+
return;
|
|
1655
|
+
const payloadMentionAids = mentionEntryAids(payloadMentionEntries);
|
|
1571
1656
|
const logDescriptor = classifyAunPayloadForLog(payload);
|
|
1572
1657
|
const text = this.extractTextPayload(payload, groupId, senderAid);
|
|
1573
1658
|
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
1574
1659
|
const topicName = (typeof payload === 'object' && payload !== null)
|
|
1575
1660
|
? this.sanitizeTopicName(payload.topicName)
|
|
1576
1661
|
: undefined;
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1662
|
+
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
1663
|
+
// 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
|
|
1664
|
+
if (inboundType === 'error') {
|
|
1665
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1666
|
+
logger.info(`${this.logPrefix()} Group dropped terminal error: group=${groupId || 'unknown'} sender=${senderAid || 'unknown'} mid=${messageId}`);
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1579
1669
|
// Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
|
|
1580
1670
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
1581
1671
|
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
1672
|
logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
|
|
1587
1673
|
if (!groupId || !senderAid) {
|
|
1588
1674
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -1654,6 +1740,10 @@ export class AUNChannel {
|
|
|
1654
1740
|
chatType: 'group', messageId, seq, groupId,
|
|
1655
1741
|
encrypted: menuEncrypted,
|
|
1656
1742
|
protectedHeaders,
|
|
1743
|
+
mentions: payloadMentionEntries,
|
|
1744
|
+
mentionAids: payloadMentionAids,
|
|
1745
|
+
isMentioned: hasMentionAll(payloadMentionEntries)
|
|
1746
|
+
|| (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
|
|
1657
1747
|
replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
|
|
1658
1748
|
msgType: logDescriptor.msgType,
|
|
1659
1749
|
payloadType: logDescriptor.payloadType,
|
|
@@ -1673,17 +1763,17 @@ export class AUNChannel {
|
|
|
1673
1763
|
const msgEncrypted = !!env.encrypted;
|
|
1674
1764
|
if (!msgEncrypted)
|
|
1675
1765
|
this.plaintextRecv++;
|
|
1676
|
-
//
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
// @all 仅认结构化 mentions(payload.mentions
|
|
1686
|
-
const mentionedAll =
|
|
1766
|
+
// mention_mode 是唯一的服务端群 @ 策略字段;缺失/非法时使用协议内置默认 disabled。
|
|
1767
|
+
const serverMentionMode = normalizeMentionMode(msg.mention_mode)
|
|
1768
|
+
?? normalizeMentionMode(payload?.mention_mode)
|
|
1769
|
+
?? 'disabled';
|
|
1770
|
+
const localMentionMode = normalizeMentionMode(this.mentionModeResolver
|
|
1771
|
+
? await this.mentionModeResolver(groupId).catch(() => undefined)
|
|
1772
|
+
: undefined);
|
|
1773
|
+
const mentionMode = localMentionMode ?? serverMentionMode;
|
|
1774
|
+
const mentionedSelf = !!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid);
|
|
1775
|
+
// @all 仅认结构化 mentions(payload.mentions),不扫描正文,避免引述性 "@all" 误判。
|
|
1776
|
+
const mentionedAll = hasMentionAll(payloadMentionEntries);
|
|
1687
1777
|
// Echo 机制优先于 mention 过滤:消息第一行包含 echo 时触发
|
|
1688
1778
|
// 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1689
1779
|
const firstLineGroup = text.split('\n')[0] || '';
|
|
@@ -1718,16 +1808,17 @@ export class AUNChannel {
|
|
|
1718
1808
|
}
|
|
1719
1809
|
else {
|
|
1720
1810
|
// 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
|
|
1721
|
-
//
|
|
1811
|
+
// slash 命令在任何 mentionMode 下都强制走 mention-only 语义
|
|
1722
1812
|
// 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
|
|
1723
|
-
const enforceMention =
|
|
1813
|
+
const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
|
|
1724
1814
|
const isMentioned = mentionedSelf || mentionedAll;
|
|
1725
1815
|
// 过滤逻辑下移到响应层:这里只标记,不过滤
|
|
1726
1816
|
// 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
|
|
1727
1817
|
// TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
|
|
1728
1818
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
1729
1819
|
this.acknowledgeImmediately(messageId, seq);
|
|
1730
|
-
|
|
1820
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId);
|
|
1821
|
+
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
1822
|
return;
|
|
1732
1823
|
}
|
|
1733
1824
|
}
|
|
@@ -1745,9 +1836,7 @@ export class AUNChannel {
|
|
|
1745
1836
|
logger.debug(`${this.logPrefix()} Group dropped: empty text and no attachments (group=${groupId} sender=${senderAid} mid=${messageId})`);
|
|
1746
1837
|
return;
|
|
1747
1838
|
}
|
|
1748
|
-
const mentions =
|
|
1749
|
-
? ['all']
|
|
1750
|
-
: mentionedSelf && this._aid ? [this._aid] : [];
|
|
1839
|
+
const mentions = payloadMentionEntries;
|
|
1751
1840
|
// Process attachments
|
|
1752
1841
|
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
|
|
1753
1842
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
@@ -1758,27 +1847,27 @@ export class AUNChannel {
|
|
|
1758
1847
|
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
1759
1848
|
const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
|
|
1760
1849
|
const textMentionAll = this.hasExplicitMention(text, 'all');
|
|
1761
|
-
const structMentionSelf = this._aid ?
|
|
1762
|
-
const structMentionAll =
|
|
1850
|
+
const structMentionSelf = this._aid ? payloadMentionAids.includes(this._aid) : false;
|
|
1851
|
+
const structMentionAll = mentionedAll;
|
|
1763
1852
|
const reason = isOwnHandoff
|
|
1764
1853
|
? 'handoff.self'
|
|
1765
1854
|
: mentionedAll
|
|
1766
1855
|
? 'mention.all(struct)'
|
|
1767
1856
|
: mentionedSelf
|
|
1768
1857
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
1769
|
-
: `${
|
|
1770
|
-
logger.info(`${this.logPrefix()} Group
|
|
1858
|
+
: `${mentionMode}.no-mention`;
|
|
1859
|
+
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
1860
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1772
1861
|
if (payloadType === 'action_card_reply')
|
|
1773
1862
|
return;
|
|
1774
1863
|
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1775
|
-
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName})
|
|
1864
|
+
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mentionMode=${mentionMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
1776
1865
|
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
1866
|
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode, payloadType === 'event' ? 'notify' : 'send');
|
|
1778
1867
|
// 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
|
|
1779
1868
|
// 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
|
|
1780
1869
|
const renderMentionAids = Array.from(new Set([
|
|
1781
|
-
...
|
|
1870
|
+
...payloadMentionAids,
|
|
1782
1871
|
...this.extractMentionAidsFromText(text),
|
|
1783
1872
|
]));
|
|
1784
1873
|
this.dispatchMessage({
|
|
@@ -1801,7 +1890,7 @@ export class AUNChannel {
|
|
|
1801
1890
|
source: isOwnHandoff ? 'handoff' : undefined,
|
|
1802
1891
|
encrypted: msgEncrypted,
|
|
1803
1892
|
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
|
|
1804
|
-
|
|
1893
|
+
mentionMode: serverMentionMode,
|
|
1805
1894
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1806
1895
|
msgType: logDescriptor.msgType,
|
|
1807
1896
|
payloadType: logDescriptor.payloadType,
|
|
@@ -1810,17 +1899,9 @@ export class AUNChannel {
|
|
|
1810
1899
|
});
|
|
1811
1900
|
}
|
|
1812
1901
|
dispatchMessage(event) {
|
|
1813
|
-
//
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
return;
|
|
1817
|
-
this.seenMessages.set(event.messageId, Date.now());
|
|
1818
|
-
setTimeout(() => this.seenMessages.delete(event.messageId), 5 * 60 * 1000);
|
|
1819
|
-
// Track seq for acknowledge
|
|
1820
|
-
if (event.seq != null) {
|
|
1821
|
-
this.messageSeqMap.set(event.messageId, event.seq);
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1902
|
+
// Inbound events are claimed in handleIncomingPrivateMessage / GroupMessage
|
|
1903
|
+
// before parsing. Keeping dedupe there prevents duplicate slash commands
|
|
1904
|
+
// from reaching routing and mention policy code.
|
|
1824
1905
|
// Echo 机制:消息第一行包含 "echo"(不区分大小写)且原始内容 ≤10 字符时,直接回声
|
|
1825
1906
|
// 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1826
1907
|
const firstLine = event.text.split('\n')[0] || '';
|
|
@@ -1852,7 +1933,6 @@ export class AUNChannel {
|
|
|
1852
1933
|
}
|
|
1853
1934
|
if (!this.messageHandler)
|
|
1854
1935
|
return;
|
|
1855
|
-
const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
|
|
1856
1936
|
// Use caller-supplied replyContext (group path builds mentionUserIds);
|
|
1857
1937
|
// fall back to simple threadId-only context for private messages
|
|
1858
1938
|
let replyContext = event.replyContext;
|
|
@@ -1877,12 +1957,12 @@ export class AUNChannel {
|
|
|
1877
1957
|
messageId: event.messageId,
|
|
1878
1958
|
causation: event.causation,
|
|
1879
1959
|
threadId: event.threadId,
|
|
1880
|
-
mentions:
|
|
1960
|
+
mentions: event.mentions,
|
|
1881
1961
|
mentionAids: event.mentionAids,
|
|
1882
1962
|
isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
|
|
1883
1963
|
replyContext,
|
|
1884
1964
|
source: event.source,
|
|
1885
|
-
|
|
1965
|
+
mentionMode: event.mentionMode,
|
|
1886
1966
|
images: event.images,
|
|
1887
1967
|
msgType: event.msgType,
|
|
1888
1968
|
payloadType: event.payloadType,
|
|
@@ -2421,8 +2501,8 @@ export class AUNChannel {
|
|
|
2421
2501
|
onMessage(handler) {
|
|
2422
2502
|
this.messageHandler = handler;
|
|
2423
2503
|
}
|
|
2424
|
-
|
|
2425
|
-
this.
|
|
2504
|
+
setMentionModeResolver(resolver) {
|
|
2505
|
+
this.mentionModeResolver = resolver;
|
|
2426
2506
|
}
|
|
2427
2507
|
onRecall(handler) {
|
|
2428
2508
|
this.recallHandler = handler;
|
|
@@ -2511,6 +2591,21 @@ export class AUNChannel {
|
|
|
2511
2591
|
finalPayload.chatmode = context.metadata.chatmode;
|
|
2512
2592
|
return finalPayload;
|
|
2513
2593
|
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Validate the AUN-specific structured field before it crosses the durable
|
|
2596
|
+
* send boundary. Return a copy when mentions are present so callers do not
|
|
2597
|
+
* retain an unvalidated array reference.
|
|
2598
|
+
*/
|
|
2599
|
+
normalizeAunPayloadMentions(payload) {
|
|
2600
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
2601
|
+
return payload;
|
|
2602
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
|
|
2603
|
+
return payload;
|
|
2604
|
+
return {
|
|
2605
|
+
...payload,
|
|
2606
|
+
mentions: normalizeAunMentionEntries(payload.mentions),
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2514
2609
|
rememberInvalidatedInteraction(interactionId, reason) {
|
|
2515
2610
|
this.invalidatedInteractions.set(interactionId, reason);
|
|
2516
2611
|
const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
|
|
@@ -2602,6 +2697,16 @@ export class AUNChannel {
|
|
|
2602
2697
|
}
|
|
2603
2698
|
}
|
|
2604
2699
|
async sendAunPayload(channelId, payload, context, label) {
|
|
2700
|
+
let normalizedPayload;
|
|
2701
|
+
try {
|
|
2702
|
+
normalizedPayload = this.normalizeAunPayloadMentions(payload);
|
|
2703
|
+
}
|
|
2704
|
+
catch (error) {
|
|
2705
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2706
|
+
const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
|
|
2707
|
+
logger.error(`${this.logPrefix()} Dropped durable AUN payload: invalid payload.mentions (${detail}) code=${code} channel=${channelId}`);
|
|
2708
|
+
return { ok: false, discarded: true };
|
|
2709
|
+
}
|
|
2605
2710
|
if (!this.client || !this.connected)
|
|
2606
2711
|
return { ok: false };
|
|
2607
2712
|
const isGroup = this.isGroupId(channelId);
|
|
@@ -2616,8 +2721,8 @@ export class AUNChannel {
|
|
|
2616
2721
|
if (causationRef && causation)
|
|
2617
2722
|
registerAunCausation(causationRef, this.config.aid, targetAid, causation);
|
|
2618
2723
|
const payloadWithCausationRef = causationRef
|
|
2619
|
-
? { ...
|
|
2620
|
-
:
|
|
2724
|
+
? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
|
|
2725
|
+
: normalizedPayload;
|
|
2621
2726
|
// Truncate payload if too large (AUN SDK limit: 1MB)
|
|
2622
2727
|
const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
|
|
2623
2728
|
const params = { payload: truncatedPayload, encrypt };
|
|
@@ -2692,6 +2797,9 @@ export class AUNChannel {
|
|
|
2692
2797
|
this.forwardOutbound(result);
|
|
2693
2798
|
}
|
|
2694
2799
|
async sendContentPayload(channelId, payload, opts) {
|
|
2800
|
+
// Validate the caller's raw mentions before any early-return policy (such
|
|
2801
|
+
// as a cancelled interaction) can hide a malformed wire payload.
|
|
2802
|
+
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
2695
2803
|
const interactionId = opts.postSend?.type === 'register_interaction_card'
|
|
2696
2804
|
? opts.postSend.requestId
|
|
2697
2805
|
: undefined;
|
|
@@ -2699,7 +2807,12 @@ export class AUNChannel {
|
|
|
2699
2807
|
logger.info(`${this.logPrefix()} Dropped invalidated interaction before durable enqueue: request=${interactionId}`);
|
|
2700
2808
|
return {};
|
|
2701
2809
|
}
|
|
2702
|
-
|
|
2810
|
+
// Validate before write-ahead enqueue. Invalid AUN payloads must not be
|
|
2811
|
+
// hidden in the outbox and emitted later after a reconnect.
|
|
2812
|
+
// Validate the caller's raw mentions before deep-cleaning: that helper
|
|
2813
|
+
// intentionally removes undefined array elements, which would otherwise
|
|
2814
|
+
// turn malformed `mentions: [undefined]` into an apparently valid `[]`.
|
|
2815
|
+
const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, opts.context));
|
|
2703
2816
|
const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
|
|
2704
2817
|
const entry = outbox.enqueue(this.config.aid, {
|
|
2705
2818
|
channelId,
|
|
@@ -2859,9 +2972,27 @@ export class AUNChannel {
|
|
|
2859
2972
|
}
|
|
2860
2973
|
let finalText = text;
|
|
2861
2974
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
2862
|
-
if (this.isGroupId(channelId)
|
|
2863
|
-
|
|
2864
|
-
|
|
2975
|
+
if (this.isGroupId(channelId)) {
|
|
2976
|
+
const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
|
|
2977
|
+
.filter((id) => typeof id === 'string' && id.trim().length > 0)
|
|
2978
|
+
.map(id => id.trim())));
|
|
2979
|
+
// AUN's textual representation gives @all precedence over individual
|
|
2980
|
+
// targets, matching the cross-channel mention contract.
|
|
2981
|
+
const textMentionsAll = this.hasExplicitMention(finalText, 'all');
|
|
2982
|
+
const mentionTargets = requestedMentions.includes('all') || textMentionsAll
|
|
2983
|
+
? ['all']
|
|
2984
|
+
: requestedMentions;
|
|
2985
|
+
if (mentionTargets.length > 0) {
|
|
2986
|
+
for (const target of mentionTargets.reverse()) {
|
|
2987
|
+
if (!this.hasExplicitMention(finalText, target)) {
|
|
2988
|
+
finalText = `@${target} ` + finalText;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
else if (context?.peerId && !context.suppressPeerMention && !textMentionsAll) {
|
|
2993
|
+
if (!this.hasExplicitMention(finalText, context.peerId)) {
|
|
2994
|
+
finalText = `@${context.peerId} ` + finalText;
|
|
2995
|
+
}
|
|
2865
2996
|
}
|
|
2866
2997
|
}
|
|
2867
2998
|
const operationId = typeof context?.metadata?.operationId === 'string'
|
|
@@ -2905,16 +3036,27 @@ export class AUNChannel {
|
|
|
2905
3036
|
}
|
|
2906
3037
|
/** Daemon-side transport for `ec msg send` running inside an agent task. */
|
|
2907
3038
|
async sendDaemonMsg(args) {
|
|
2908
|
-
if (!this.connected || !this.client) {
|
|
2909
|
-
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
2910
|
-
}
|
|
2911
3039
|
if (!args.to || this.isGroupId(args.to)) {
|
|
2912
3040
|
return { ok: false, error: 'aun-msg-send only supports private AID targets', code: 'UNSUPPORTED_TARGET' };
|
|
2913
3041
|
}
|
|
2914
3042
|
if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
|
|
2915
3043
|
return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
|
|
2916
3044
|
}
|
|
2917
|
-
|
|
3045
|
+
let finalPayload;
|
|
3046
|
+
try {
|
|
3047
|
+
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
3048
|
+
finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
3049
|
+
}
|
|
3050
|
+
catch (e) {
|
|
3051
|
+
return {
|
|
3052
|
+
ok: false,
|
|
3053
|
+
error: e?.message || String(e),
|
|
3054
|
+
code: e?.code,
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
if (!this.connected || !this.client) {
|
|
3058
|
+
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3059
|
+
}
|
|
2918
3060
|
const info = await this.fetchPeerInfo(args.to);
|
|
2919
3061
|
const chatmode = info.type === 'human' ? 'interactive' : 'proactive';
|
|
2920
3062
|
if (!finalPayload.chatmode)
|
|
@@ -3019,20 +3161,34 @@ export class AUNChannel {
|
|
|
3019
3161
|
}
|
|
3020
3162
|
/** Daemon-side transport for `ec group send` running inside an agent task. */
|
|
3021
3163
|
async sendDaemonGroupMsg(args) {
|
|
3022
|
-
if (!this.connected || !this.client) {
|
|
3023
|
-
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3024
|
-
}
|
|
3025
3164
|
if (!args.groupId || !this.isGroupId(args.groupId)) {
|
|
3026
3165
|
return { ok: false, error: 'aun-msg-send requires a valid group target', code: 'UNSUPPORTED_TARGET' };
|
|
3027
3166
|
}
|
|
3028
3167
|
if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
|
|
3029
3168
|
return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
|
|
3030
3169
|
}
|
|
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
3170
|
try {
|
|
3171
|
+
// Validate before deep-cleaning so undefined entries cannot be silently
|
|
3172
|
+
// removed from a malformed mentions array.
|
|
3173
|
+
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
3174
|
+
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
3175
|
+
const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
|
|
3176
|
+
? normalizeAunMentionEntries(finalPayload.mentions)
|
|
3177
|
+
: undefined;
|
|
3178
|
+
if (args.mentions !== undefined) {
|
|
3179
|
+
const normalizedMentions = normalizeAunMentionEntries(args.mentions);
|
|
3180
|
+
if (normalizedMentions.length > 0)
|
|
3181
|
+
finalPayload.mentions = normalizedMentions;
|
|
3182
|
+
else if (payloadMentions !== undefined)
|
|
3183
|
+
finalPayload.mentions = payloadMentions;
|
|
3184
|
+
}
|
|
3185
|
+
else if (payloadMentions !== undefined) {
|
|
3186
|
+
finalPayload.mentions = payloadMentions;
|
|
3187
|
+
}
|
|
3188
|
+
if (!this.connected || !this.client) {
|
|
3189
|
+
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3190
|
+
}
|
|
3191
|
+
const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
|
|
3036
3192
|
const result = await this.callAndTrace('group.send', {
|
|
3037
3193
|
group_id: args.groupId,
|
|
3038
3194
|
payload: this.truncatePayloadIfNeeded(finalPayload, 'group-send-ipc'),
|
|
@@ -3154,7 +3310,7 @@ export class AUNChannel {
|
|
|
3154
3310
|
const source = context?.metadata?.source ?? 'daemon';
|
|
3155
3311
|
const payload = { type: 'text', text: finalText };
|
|
3156
3312
|
if (this.isGroupId(channelId)) {
|
|
3157
|
-
const extracted = this.
|
|
3313
|
+
const extracted = this.extractMentionEntriesFromText(finalText);
|
|
3158
3314
|
if (extracted.length > 0)
|
|
3159
3315
|
payload.mentions = extracted;
|
|
3160
3316
|
}
|
|
@@ -3353,6 +3509,10 @@ export class AUNChannel {
|
|
|
3353
3509
|
const context = entry.context;
|
|
3354
3510
|
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
3511
|
const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
|
|
3512
|
+
if (sent.discarded) {
|
|
3513
|
+
logger.warn(`${this.logPrefix()} Removed malformed durable payload from outbox: id=${entry.id} channel=${channelId}`);
|
|
3514
|
+
return { ok: true, discarded: true };
|
|
3515
|
+
}
|
|
3356
3516
|
if (!sent.ok || !sent.messageId)
|
|
3357
3517
|
return sent;
|
|
3358
3518
|
const isGroup = this.isGroupId(channelId);
|
|
@@ -3415,13 +3575,14 @@ export class AUNChannel {
|
|
|
3415
3575
|
* 存储键:group_id/peer_aid + sender_aid + context.type + context.id
|
|
3416
3576
|
*/
|
|
3417
3577
|
async sendThought(channelId, taskId, payload, context) {
|
|
3418
|
-
if (!this.connected || !this.client)
|
|
3419
|
-
return;
|
|
3420
3578
|
if (!taskId)
|
|
3421
3579
|
return;
|
|
3580
|
+
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
3581
|
+
const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
|
|
3582
|
+
if (!this.connected || !this.client)
|
|
3583
|
+
return;
|
|
3422
3584
|
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
3423
3585
|
const targetId = channelId;
|
|
3424
|
-
const finalPayload = this.stripUndefinedDeep(payload);
|
|
3425
3586
|
const encrypt = context?.metadata?.encrypted != null
|
|
3426
3587
|
? !!(context.metadata.encrypted)
|
|
3427
3588
|
: this.shouldEncrypt(targetId);
|
|
@@ -3491,6 +3652,10 @@ export class AUNChannel {
|
|
|
3491
3652
|
}
|
|
3492
3653
|
/** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
|
|
3493
3654
|
async sendStructured(channelId, payload, context) {
|
|
3655
|
+
// Validate before checking connection state so malformed producer payloads
|
|
3656
|
+
// are reported deterministically instead of being hidden by a disconnect.
|
|
3657
|
+
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
3658
|
+
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
3494
3659
|
if (!this.connected || !this.client)
|
|
3495
3660
|
return null;
|
|
3496
3661
|
const isGroup = this.isGroupId(channelId);
|
|
@@ -3499,7 +3664,6 @@ export class AUNChannel {
|
|
|
3499
3664
|
const encrypt = context?.metadata?.encrypted != null
|
|
3500
3665
|
? !!(context.metadata.encrypted)
|
|
3501
3666
|
: this.shouldEncrypt(encryptTarget);
|
|
3502
|
-
const finalPayload = this.stripUndefinedDeep({ ...payload });
|
|
3503
3667
|
if (context?.threadId && !finalPayload.thread_id)
|
|
3504
3668
|
finalPayload.thread_id = context.threadId;
|
|
3505
3669
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
@@ -4236,11 +4400,30 @@ export class AUNChannelPlugin {
|
|
|
4236
4400
|
return;
|
|
4237
4401
|
}
|
|
4238
4402
|
case 'system.error': {
|
|
4239
|
-
|
|
4403
|
+
const errorPayload = {
|
|
4404
|
+
type: 'error',
|
|
4405
|
+
...taskBase(),
|
|
4406
|
+
subtype: payload.subtype,
|
|
4407
|
+
text: payload.text,
|
|
4408
|
+
message: payload.text,
|
|
4409
|
+
user_message: payload.text,
|
|
4410
|
+
recoverable: payload.recoverable === true,
|
|
4411
|
+
terminal: payload.recoverable !== true,
|
|
4412
|
+
};
|
|
4413
|
+
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
4240
4414
|
return;
|
|
4241
4415
|
}
|
|
4242
4416
|
case 'result.error': {
|
|
4243
|
-
|
|
4417
|
+
const errorPayload = {
|
|
4418
|
+
type: 'error',
|
|
4419
|
+
...taskBase(),
|
|
4420
|
+
reason: payload.reason,
|
|
4421
|
+
text: payload.text,
|
|
4422
|
+
message: payload.text,
|
|
4423
|
+
user_message: payload.text,
|
|
4424
|
+
terminal: true,
|
|
4425
|
+
};
|
|
4426
|
+
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
4244
4427
|
return;
|
|
4245
4428
|
}
|
|
4246
4429
|
case 'result.file': {
|
|
@@ -4438,15 +4621,12 @@ export class AUNChannelPlugin {
|
|
|
4438
4621
|
});
|
|
4439
4622
|
});
|
|
4440
4623
|
}
|
|
4441
|
-
if (typeof channel.
|
|
4442
|
-
channel.
|
|
4443
|
-
|
|
4444
|
-
// dispatch_mode(mention/broadcast);未设值返回 undefined,回退服务端下发值。
|
|
4445
|
-
const mentionMode = resolveEffective({
|
|
4624
|
+
if (typeof channel.setMentionModeResolver === 'function') {
|
|
4625
|
+
channel.setMentionModeResolver(async (channelId) => {
|
|
4626
|
+
return resolveEffective({
|
|
4446
4627
|
self: aid,
|
|
4447
4628
|
peerKey: formatPeerKey('aun', channelId),
|
|
4448
4629
|
}, { cache: true }).mentionMode;
|
|
4449
|
-
return mentionModeToDispatch(mentionMode);
|
|
4450
4630
|
});
|
|
4451
4631
|
}
|
|
4452
4632
|
},
|