evolcore 0.0.10 → 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 +44 -0
- package/README.md +3 -3
- package/bin/codex-managed-hook.mjs +80 -0
- package/dist/agents/baseagent.js +4 -0
- package/dist/agents/claude-runner.js +158 -44
- package/dist/agents/codex-app-server-client.js +153 -23
- package/dist/agents/codex-runner.js +331 -43
- package/dist/agents/ecagent-runner.js +53 -6
- package/dist/agents/gemini-runner.js +1 -0
- package/dist/agents/request-identity.js +55 -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/aun/outbox.js +28 -31
- package/dist/channels/aun.js +418 -235
- package/dist/channels/daemon.js +13 -0
- package/dist/cli/agent-command.js +72 -17
- package/dist/cli/agent.js +249 -22
- package/dist/cli/aun-commands.js +270 -69
- package/dist/cli/bench.js +12 -3
- package/dist/cli/daemon-commands.js +52 -33
- package/dist/cli/fs-command.js +60 -0
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +106 -63
- package/dist/cli/init-cancel.js +208 -0
- package/dist/cli/init-channel.js +343 -195
- package/dist/cli/init.js +21 -9
- 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 +5 -0
- package/dist/config/contact-book-store.js +11 -3
- package/dist/config/contact-request-service.js +161 -15
- package/dist/config/gateway-config.js +26 -10
- package/dist/config/mention-mode.js +8 -24
- package/dist/core/agent-reload-coordinator.js +53 -0
- package/dist/core/auth/agent-delegation.js +0 -1
- package/dist/core/auth/operation-authorizer.js +62 -154
- package/dist/core/auth/operation-catalog.js +264 -22
- package/dist/core/bootstrap-messages.js +68 -0
- package/dist/core/bootstrap-service.js +144 -10
- 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 +24 -7
- package/dist/core/command/agent-control.js +14 -11
- 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 +213 -110
- 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 +45 -25
- package/dist/core/daemon-file-cache.js +2 -1
- package/dist/core/data-migration.js +9 -2
- package/dist/core/evolagent-registry.js +125 -35
- package/dist/core/evolagent.js +12 -5
- 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/inference/text-inference.js +38 -4
- 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 +237 -79
- package/dist/core/message/message-log.js +23 -0
- package/dist/core/message/message-queue.js +20 -4
- package/dist/core/message/response-engine.js +54 -36
- package/dist/core/model/model-catalog.js +143 -24
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/ec-command-parser.js +156 -10
- package/dist/core/permission/index.js +1 -0
- package/dist/core/permission/readonly-shell-query.js +532 -0
- package/dist/core/permission/sandbox-runtime.js +22 -1
- package/dist/core/permission/shell-environment.js +46 -0
- package/dist/core/permission/tool-policy.js +292 -111
- package/dist/core/protected-paths.js +17 -9
- package/dist/core/runner-reload-transaction.js +57 -0
- package/dist/core/session/session-manager.js +21 -2
- package/dist/eck/manifest-engine.js +39 -13
- package/dist/index.js +644 -180
- package/dist/ipc.js +246 -19
- 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/aid-bind.js +3 -8
- 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/log-writer.js +6 -10
- package/dist/utils/logger.js +5 -5
- package/dist/utils/npm-ops.js +5 -12
- package/dist/utils/tool-summary.js +11 -0
- package/kits/docs/channels/aun.md +1 -1
- package/kits/docs/context-assembly.md +2 -2
- package/kits/docs/evolcore/agent.md +11 -6
- package/kits/docs/evolcore/aid.md +14 -0
- package/kits/docs/evolcore/msg.md +13 -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/01-overview.md +9 -0
- package/kits/rules/04-relation.md +1 -1
- package/kits/rules/05-venue.md +2 -2
- package/kits/schemas/agent-config.schema.3.json +1 -1
- package/kits/schemas/agent-config.schema.4.json +1 -1
- package/kits/schemas/relation-config.schema.2.json +1 -1
- package/kits/schemas/role-config.schema.1.json +1 -1
- package/kits/schemas/single-session.schema.1.json +3 -3
- package/kits/schemas/single-session.schema.2.json +3 -3
- package/kits/templates/roles/admin.json +27 -0
- package/kits/templates/roles/member.json +44 -0
- package/kits/templates/roles/visitor.json +40 -2
- package/kits/templates/system-fragments/bootstrap.md +12 -6
- package/kits/templates/system-fragments/channel.md +6 -0
- package/kits/templates/system-fragments/session.md +2 -0
- package/package.json +3 -2
- package/skills/eclink/SKILL.md +15 -3
- package/skills/eclink/agents/openai.yaml +3 -3
- package/dist/core/command/evol-menu-version-gate.js +0 -39
package/dist/channels/aun.js
CHANGED
|
@@ -7,20 +7,21 @@ 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,
|
|
10
|
+
import { resolvePaths, agentDir as agentDirPath, resolveRoot } 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
|
-
import { appendMessageLog, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog } from '../core/message/message-log.js';
|
|
13
|
+
import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
|
|
14
14
|
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
15
15
|
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
16
16
|
import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
|
|
17
17
|
import { appendAidLifecycle } from '../aun/aid/identity.js';
|
|
18
18
|
import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
|
|
19
19
|
import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
|
|
20
|
-
import { loadAgent
|
|
20
|
+
import { loadAgent } from '../config-store.js';
|
|
21
|
+
import { normalizeAgentLifecycle } from '../config/lifecycle.js';
|
|
21
22
|
import { resolveEffective } from '../config/config-manager.js';
|
|
22
23
|
import { isManagementRole } from '../config/builtin-roles.js';
|
|
23
|
-
import {
|
|
24
|
+
import { normalizeMentionMode } from '../config/mention-mode.js';
|
|
24
25
|
import { getProcessStartTime } from '../utils/process-introspect.js';
|
|
25
26
|
import * as outbox from '../aun/outbox.js';
|
|
26
27
|
import { guessMime, formatSize } from '../utils/media-cache.js';
|
|
@@ -33,6 +34,9 @@ import { recordCausationSpan } from '../core/causation/audit.js';
|
|
|
33
34
|
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
34
35
|
import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
35
36
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.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';
|
|
36
40
|
export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
|
|
37
41
|
const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
38
42
|
const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
|
|
@@ -137,7 +141,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
|
|
|
137
141
|
replyContext: opts.replyContext,
|
|
138
142
|
source: opts.source,
|
|
139
143
|
images: opts.images,
|
|
140
|
-
|
|
144
|
+
mentionMode: opts.mentionMode,
|
|
141
145
|
};
|
|
142
146
|
}
|
|
143
147
|
function verifiedProtectedHeaders(msg, env) {
|
|
@@ -200,7 +204,6 @@ export class AUNChannel {
|
|
|
200
204
|
connected = false;
|
|
201
205
|
traceWriter = null;
|
|
202
206
|
eventBus = null;
|
|
203
|
-
ownerBoundHandler = null;
|
|
204
207
|
queuedHandler = null;
|
|
205
208
|
pendingEchoMessages = new Map();
|
|
206
209
|
isEchoSending = false;
|
|
@@ -697,24 +700,20 @@ export class AUNChannel {
|
|
|
697
700
|
.replace(/[ \t]+/g, ' ')
|
|
698
701
|
.trim();
|
|
699
702
|
}
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
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);
|
|
707
709
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (m && typeof m === 'object' && m.scope === 'all')
|
|
715
|
-
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;
|
|
716
716
|
}
|
|
717
|
-
return false;
|
|
718
717
|
}
|
|
719
718
|
/** 从正文提取 @aid(AID 格式:至少三段域名),用于出站填充 payload.mentions */
|
|
720
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;
|
|
@@ -730,6 +729,19 @@ export class AUNChannel {
|
|
|
730
729
|
}
|
|
731
730
|
return out;
|
|
732
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
|
+
}
|
|
733
745
|
/** Sanitize a raw topicName from untrusted payload: trim, strip control chars, enforce length. */
|
|
734
746
|
sanitizeTopicName(raw) {
|
|
735
747
|
if (typeof raw !== 'string')
|
|
@@ -754,6 +766,31 @@ export class AUNChannel {
|
|
|
754
766
|
}
|
|
755
767
|
return replyContext;
|
|
756
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
|
+
}
|
|
757
794
|
acknowledgeImmediately(messageId, _seq) {
|
|
758
795
|
// SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
|
|
759
796
|
// as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
|
|
@@ -770,10 +807,16 @@ export class AUNChannel {
|
|
|
770
807
|
_aid;
|
|
771
808
|
_selfName; // 本地 agent.md 中的 name,首次 connect 时读取
|
|
772
809
|
_chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
|
|
773
|
-
|
|
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();
|
|
774
817
|
groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
|
|
775
818
|
peerInfoCache = new Map();
|
|
776
|
-
messageSeqMap = new Map(); // messageId → seq (for ack)
|
|
819
|
+
messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
|
|
777
820
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
778
821
|
peerE2ee = new Map();
|
|
779
822
|
static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
|
|
@@ -788,7 +831,7 @@ export class AUNChannel {
|
|
|
788
831
|
interactionCardMessageIds = new Map();
|
|
789
832
|
/** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
|
|
790
833
|
invalidatedInteractions = new Map();
|
|
791
|
-
|
|
834
|
+
mentionModeResolver;
|
|
792
835
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
793
836
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
794
837
|
'merge', 'link', 'location', 'personal_card',
|
|
@@ -797,6 +840,10 @@ export class AUNChannel {
|
|
|
797
840
|
static INJECT_REQUEST_TYPE = 'observer.inject';
|
|
798
841
|
/** 绑定请求类型(Evol App → 控制 AID):快速路径,绕过白名单透传原始 payload JSON 给 bridge。 */
|
|
799
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';
|
|
800
847
|
// Reconnect state
|
|
801
848
|
// SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
|
|
802
849
|
// 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
|
|
@@ -833,9 +880,9 @@ export class AUNChannel {
|
|
|
833
880
|
baseName: 'aun',
|
|
834
881
|
logDir: resolvePaths().logs,
|
|
835
882
|
rotation: 'hourly',
|
|
836
|
-
retention: { hours:
|
|
883
|
+
retention: { hours: 24 },
|
|
837
884
|
});
|
|
838
|
-
logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation,
|
|
885
|
+
logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 24h retention): ${this.traceWriter.activePath()}`);
|
|
839
886
|
}
|
|
840
887
|
this.aidState = {
|
|
841
888
|
aid: config.aid,
|
|
@@ -1083,11 +1130,6 @@ export class AUNChannel {
|
|
|
1083
1130
|
logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
|
|
1084
1131
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
|
|
1085
1132
|
appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
|
|
1086
|
-
// Send welcome message to owner after first connection
|
|
1087
|
-
// pureIdentity(控制 AID):跳过 evolagent onboarding(根除 warn 噪声 + 永不 agentmdPut)
|
|
1088
|
-
if (!this.config.pureIdentity) {
|
|
1089
|
-
await this.sendWelcomeMessage();
|
|
1090
|
-
}
|
|
1091
1133
|
}
|
|
1092
1134
|
catch (e) {
|
|
1093
1135
|
this.trace('OUT', 'client.connect.error', { error: String(e) });
|
|
@@ -1097,122 +1139,89 @@ export class AUNChannel {
|
|
|
1097
1139
|
throw e;
|
|
1098
1140
|
}
|
|
1099
1141
|
}
|
|
1100
|
-
async
|
|
1142
|
+
async preparePostBootstrapWelcome() {
|
|
1101
1143
|
try {
|
|
1102
1144
|
const aid = this.config.aid;
|
|
1103
1145
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
1104
|
-
// Read initialized + owners from per-agent config.json
|
|
1105
|
-
// (config.json 是 owner 的真相来源——auto-bind 后会更新这里,但 this.config 是
|
|
1106
|
-
// channel 启动时的快照,不会自动同步)
|
|
1107
1146
|
const agentConfig = loadAgent(aidName);
|
|
1108
1147
|
if (!agentConfig) {
|
|
1109
1148
|
logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
|
|
1110
|
-
return;
|
|
1111
|
-
}
|
|
1112
|
-
if (agentConfig.initialized === true) {
|
|
1113
|
-
logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
|
|
1114
|
-
return;
|
|
1149
|
+
return false;
|
|
1115
1150
|
}
|
|
1116
1151
|
const owner = getFirstStaticAgentOwner(aidName);
|
|
1117
1152
|
if (!owner) {
|
|
1118
|
-
logger.info(`${this.logPrefix()} No owner configured, skipping welcome message
|
|
1119
|
-
return;
|
|
1153
|
+
logger.info(`${this.logPrefix()} No owner configured, skipping post-bootstrap welcome message`);
|
|
1154
|
+
return false;
|
|
1120
1155
|
}
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
// Fetch owner's agent.md to derive name and validate type
|
|
1126
|
-
const ownerInfo = await this.fetchPeerInfo(owner);
|
|
1127
|
-
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
1128
|
-
logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human). Consider using a human AID as owner.`);
|
|
1156
|
+
const operationId = postBootstrapWelcomeOperationId(aidName);
|
|
1157
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
|
|
1158
|
+
if (hasMessageLogOperation(chatDir, operationId) || outbox.findByDedupeKey(aidName, operationId)) {
|
|
1159
|
+
return true;
|
|
1129
1160
|
}
|
|
1130
|
-
// Name: prefer existing agent.md name if user has customized it,
|
|
1131
|
-
// otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
|
|
1132
1161
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
1133
|
-
|
|
1134
|
-
const currentNameMatch = existingFrontmatter.match(/^name:\s*"?([^"\n]+)/m);
|
|
1135
|
-
const currentName = currentNameMatch?.[1]?.trim().replace(/"$/, '');
|
|
1136
|
-
const aidLabel = aidName.split('.')[0];
|
|
1137
|
-
let agentDisplayName;
|
|
1138
|
-
if (currentName && currentName !== aidLabel) {
|
|
1139
|
-
agentDisplayName = currentName;
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
|
|
1143
|
-
}
|
|
1144
|
-
// Preserve user-provided description (from `agent new --description`), fallback to default
|
|
1145
|
-
const currentDescMatch = existingFrontmatter.match(/^description:\s*"?([^"\n]*)/m);
|
|
1146
|
-
const currentDesc = currentDescMatch?.[1]?.trim().replace(/"$/, '');
|
|
1147
|
-
const agentDescription = currentDesc
|
|
1148
|
-
? currentDesc
|
|
1149
|
-
: 'EvolCore AI Agent Gateway - 连接 Claude/Codex 到消息通道';
|
|
1150
|
-
// Generate new agent.md (no `initialized` frontmatter — that's now in config.json)
|
|
1151
|
-
const newAgentMd = `---
|
|
1152
|
-
aid: "${aid}"
|
|
1153
|
-
name: "${agentDisplayName}"
|
|
1154
|
-
type: "codeagent"
|
|
1155
|
-
version: "1.0.0"
|
|
1156
|
-
description: "${agentDescription}"
|
|
1157
|
-
tags:
|
|
1158
|
-
- evolcore
|
|
1159
|
-
- ai-agent
|
|
1160
|
-
- gateway
|
|
1161
|
-
---
|
|
1162
|
-
|
|
1163
|
-
# ${agentDisplayName}
|
|
1164
|
-
|
|
1165
|
-
EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
1166
|
-
`;
|
|
1167
|
-
// Write locally and publish to AUN network (auto-sign)
|
|
1168
|
-
try {
|
|
1169
|
-
const { agentmdPut } = await import('../aun/aid/agentmd.js');
|
|
1170
|
-
await agentmdPut(newAgentMd, { aid: aidName, store: this.store });
|
|
1171
|
-
logger.info(`${this.logPrefix()} Published agent.md to AUN network`);
|
|
1172
|
-
}
|
|
1173
|
-
catch (e) {
|
|
1174
|
-
logger.warn(`${this.logPrefix()} Failed to publish agent.md: ${e}`);
|
|
1175
|
-
}
|
|
1176
|
-
// Send welcome message
|
|
1177
|
-
const { generateWelcomeMessage } = await import('../utils/welcome.js');
|
|
1178
|
-
const welcomeText = generateWelcomeMessage({
|
|
1179
|
-
channelType: 'aun',
|
|
1180
|
-
agentName: agentDisplayName,
|
|
1181
|
-
ownerName: ownerDisplayName,
|
|
1182
|
-
includeBindingNote: true,
|
|
1183
|
-
});
|
|
1184
|
-
// First contact with Owner races against Owner's async cert fetch from
|
|
1185
|
-
// gateway PKI; a 3s pause lets the cert propagate. persist_required asks
|
|
1186
|
-
// the gateway to durably store the message so Owner can recover it via
|
|
1187
|
-
// pull if the initial E2EE push still arrives before the cert resolves.
|
|
1188
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1189
|
-
if (!this.client) {
|
|
1190
|
-
logger.warn(`${this.logPrefix()} Client disconnected before welcome message could be sent`);
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
await this.callAndTrace('message.send', {
|
|
1194
|
-
to: owner,
|
|
1195
|
-
payload: { type: 'text', text: welcomeText },
|
|
1196
|
-
encrypt: this.shouldEncrypt(owner),
|
|
1197
|
-
persist_required: true,
|
|
1198
|
-
});
|
|
1199
|
-
logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
|
|
1200
|
-
// Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
|
|
1162
|
+
let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
|
|
1201
1163
|
try {
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
logger.info(`${this.logPrefix()} Marked ${aidName} as initialized in config.json`);
|
|
1164
|
+
const ownerInfo = await this.fetchPeerInfo(owner);
|
|
1165
|
+
ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
|
|
1166
|
+
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
1167
|
+
logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human).`);
|
|
1207
1168
|
}
|
|
1208
1169
|
}
|
|
1209
1170
|
catch (e) {
|
|
1210
|
-
logger.warn(`${this.logPrefix()} Failed to
|
|
1171
|
+
logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
|
|
1211
1172
|
}
|
|
1173
|
+
preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName);
|
|
1174
|
+
logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
|
|
1175
|
+
return true;
|
|
1212
1176
|
}
|
|
1213
1177
|
catch (e) {
|
|
1214
|
-
logger.warn(`${this.logPrefix()} Failed to
|
|
1178
|
+
logger.warn(`${this.logPrefix()} Failed to prepare post-bootstrap welcome message: ${e}`);
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async sendPostBootstrapWelcome() {
|
|
1183
|
+
if (!await this.preparePostBootstrapWelcome())
|
|
1184
|
+
return false;
|
|
1185
|
+
return this.reconcilePostBootstrapWelcome();
|
|
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
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Resume delivery of an already prepared completion welcome.
|
|
1197
|
+
*
|
|
1198
|
+
* Unlike sendPostBootstrapWelcome(), this method never creates a new
|
|
1199
|
+
* operation. That distinction keeps repeated `ec agent ready` calls on an
|
|
1200
|
+
* already-active agent from manufacturing another welcome message.
|
|
1201
|
+
*/
|
|
1202
|
+
async reconcilePostBootstrapWelcome() {
|
|
1203
|
+
const configuredAid = this.config.aid;
|
|
1204
|
+
const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
|
|
1205
|
+
const operationId = postBootstrapWelcomeOperationId(aid);
|
|
1206
|
+
const entry = outbox.findByDedupeKey(aid, operationId);
|
|
1207
|
+
if (!entry) {
|
|
1208
|
+
const owner = getFirstStaticAgentOwner(aid);
|
|
1209
|
+
if (!owner)
|
|
1210
|
+
return false;
|
|
1211
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
|
|
1212
|
+
return hasMessageLogOperation(chatDir, operationId);
|
|
1213
|
+
}
|
|
1214
|
+
const agentConfig = loadAgent(aid);
|
|
1215
|
+
if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
|
|
1216
|
+
logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
|
|
1217
|
+
return true;
|
|
1215
1218
|
}
|
|
1219
|
+
if (!this.connected || !this.client)
|
|
1220
|
+
return true;
|
|
1221
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
|
|
1222
|
+
if (sent)
|
|
1223
|
+
outbox.remove(aid, entry.id);
|
|
1224
|
+
return true;
|
|
1216
1225
|
}
|
|
1217
1226
|
// ── Event handlers ──────────────────────────────────────────
|
|
1218
1227
|
/**
|
|
@@ -1439,12 +1448,21 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1439
1448
|
if (!data || typeof data !== 'object')
|
|
1440
1449
|
return;
|
|
1441
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;
|
|
1442
1457
|
// SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1443
1458
|
// message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
|
|
1444
1459
|
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1445
1460
|
const protectedHeaders = verifiedProtectedHeaders(msg, env);
|
|
1446
1461
|
const fromAid = env.from ?? '';
|
|
1447
1462
|
const payload = msg.payload ?? '';
|
|
1463
|
+
const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
|
|
1464
|
+
if (!mentions)
|
|
1465
|
+
return;
|
|
1448
1466
|
const causationRef = payload && typeof payload === 'object' && typeof payload._evolcore_causation_ref === 'string'
|
|
1449
1467
|
? payload._evolcore_causation_ref
|
|
1450
1468
|
: undefined;
|
|
@@ -1458,15 +1476,19 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1458
1476
|
const topicName = (typeof payload === 'object' && payload !== null)
|
|
1459
1477
|
? this.sanitizeTopicName(payload.topicName)
|
|
1460
1478
|
: undefined;
|
|
1461
|
-
const messageId = msg.message_id ?? '';
|
|
1462
1479
|
const causation = consumeAunCausation(causationRef || messageId, fromAid, this._aid || this.config.aid)
|
|
1463
1480
|
?? (causationRef ? consumeAunCausation(messageId, fromAid, this._aid || this.config.aid) : undefined);
|
|
1464
|
-
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
|
+
}
|
|
1465
1488
|
// Observer forward (inbound):在所有过滤之前转发原始明文 payload。
|
|
1466
1489
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
1467
1490
|
// 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
|
|
1468
1491
|
// (即便日后 from-owner 排除规则调整,也不会泄漏)。
|
|
1469
|
-
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
1470
1492
|
if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
|
|
1471
1493
|
this.forwardInbound(msg);
|
|
1472
1494
|
}
|
|
@@ -1482,10 +1504,13 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1482
1504
|
const msgEncrypted = !!env.encrypted;
|
|
1483
1505
|
if (!msgEncrypted)
|
|
1484
1506
|
this.plaintextRecv++;
|
|
1485
|
-
//
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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 });
|
|
1489
1514
|
}
|
|
1490
1515
|
// Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
|
|
1491
1516
|
const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
|
|
@@ -1507,8 +1532,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1507
1532
|
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1508
1533
|
return;
|
|
1509
1534
|
}
|
|
1510
|
-
// menu.*
|
|
1511
|
-
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) {
|
|
1512
1539
|
this.acknowledgeImmediately(messageId, seq);
|
|
1513
1540
|
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
|
|
1514
1541
|
this.dispatchMessage({
|
|
@@ -1519,6 +1546,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1519
1546
|
peerType: peerIdentity.type,
|
|
1520
1547
|
encrypted: msgEncrypted,
|
|
1521
1548
|
protectedHeaders,
|
|
1549
|
+
mentions,
|
|
1550
|
+
mentionAids: mentionEntryAids(mentions),
|
|
1522
1551
|
replyContext: this.buildGroupReplyContext(threadId, fromAid, msgEncrypted, messageId),
|
|
1523
1552
|
msgType: logDescriptor.msgType,
|
|
1524
1553
|
payloadType: logDescriptor.payloadType,
|
|
@@ -1547,6 +1576,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1547
1576
|
peerName: displayName || undefined,
|
|
1548
1577
|
peerType: peerIdentity.type,
|
|
1549
1578
|
encrypted: msgEncrypted,
|
|
1579
|
+
mentions,
|
|
1580
|
+
mentionAids: mentionEntryAids(mentions),
|
|
1550
1581
|
msgType: logDescriptor.msgType,
|
|
1551
1582
|
payloadType: logDescriptor.payloadType,
|
|
1552
1583
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -1606,28 +1637,38 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1606
1637
|
if (!data || typeof data !== 'object')
|
|
1607
1638
|
return;
|
|
1608
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;
|
|
1609
1645
|
// SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1610
|
-
// message_id / seq / payload / same_* /
|
|
1646
|
+
// message_id / seq / payload / same_* / mention_mode 等仍是顶层独立字段,不在 envelope 内。
|
|
1611
1647
|
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1612
1648
|
const protectedHeaders = verifiedProtectedHeaders(msg, env);
|
|
1613
1649
|
const groupId = env.group_id ?? '';
|
|
1614
1650
|
const senderAid = env.from ?? '';
|
|
1615
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);
|
|
1616
1656
|
const logDescriptor = classifyAunPayloadForLog(payload);
|
|
1617
1657
|
const text = this.extractTextPayload(payload, groupId, senderAid);
|
|
1618
1658
|
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
1619
1659
|
const topicName = (typeof payload === 'object' && payload !== null)
|
|
1620
1660
|
? this.sanitizeTopicName(payload.topicName)
|
|
1621
1661
|
: undefined;
|
|
1622
|
-
const
|
|
1623
|
-
|
|
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
|
+
}
|
|
1624
1669
|
// Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
|
|
1625
1670
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
1626
1671
|
this.forwardInbound(msg);
|
|
1627
|
-
// Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
|
|
1628
|
-
const payloadMentions = Array.isArray(payload?.mentions)
|
|
1629
|
-
? payload.mentions.filter((m) => typeof m === 'string')
|
|
1630
|
-
: [];
|
|
1631
1672
|
logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
|
|
1632
1673
|
if (!groupId || !senderAid) {
|
|
1633
1674
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -1699,6 +1740,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1699
1740
|
chatType: 'group', messageId, seq, groupId,
|
|
1700
1741
|
encrypted: menuEncrypted,
|
|
1701
1742
|
protectedHeaders,
|
|
1743
|
+
mentions: payloadMentionEntries,
|
|
1744
|
+
mentionAids: payloadMentionAids,
|
|
1745
|
+
isMentioned: hasMentionAll(payloadMentionEntries)
|
|
1746
|
+
|| (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
|
|
1702
1747
|
replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
|
|
1703
1748
|
msgType: logDescriptor.msgType,
|
|
1704
1749
|
payloadType: logDescriptor.payloadType,
|
|
@@ -1718,17 +1763,17 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1718
1763
|
const msgEncrypted = !!env.encrypted;
|
|
1719
1764
|
if (!msgEncrypted)
|
|
1720
1765
|
this.plaintextRecv++;
|
|
1721
|
-
//
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
// @all 仅认结构化 mentions(payload.mentions
|
|
1731
|
-
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);
|
|
1732
1777
|
// Echo 机制优先于 mention 过滤:消息第一行包含 echo 时触发
|
|
1733
1778
|
// 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1734
1779
|
const firstLineGroup = text.split('\n')[0] || '';
|
|
@@ -1763,16 +1808,17 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1763
1808
|
}
|
|
1764
1809
|
else {
|
|
1765
1810
|
// 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
|
|
1766
|
-
//
|
|
1811
|
+
// slash 命令在任何 mentionMode 下都强制走 mention-only 语义
|
|
1767
1812
|
// 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
|
|
1768
|
-
const enforceMention =
|
|
1813
|
+
const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
|
|
1769
1814
|
const isMentioned = mentionedSelf || mentionedAll;
|
|
1770
1815
|
// 过滤逻辑下移到响应层:这里只标记,不过滤
|
|
1771
1816
|
// 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
|
|
1772
1817
|
// TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
|
|
1773
1818
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
1774
1819
|
this.acknowledgeImmediately(messageId, seq);
|
|
1775
|
-
|
|
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))})`);
|
|
1776
1822
|
return;
|
|
1777
1823
|
}
|
|
1778
1824
|
}
|
|
@@ -1790,9 +1836,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1790
1836
|
logger.debug(`${this.logPrefix()} Group dropped: empty text and no attachments (group=${groupId} sender=${senderAid} mid=${messageId})`);
|
|
1791
1837
|
return;
|
|
1792
1838
|
}
|
|
1793
|
-
const mentions =
|
|
1794
|
-
? ['all']
|
|
1795
|
-
: mentionedSelf && this._aid ? [this._aid] : [];
|
|
1839
|
+
const mentions = payloadMentionEntries;
|
|
1796
1840
|
// Process attachments
|
|
1797
1841
|
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
|
|
1798
1842
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
@@ -1803,27 +1847,27 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1803
1847
|
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
1804
1848
|
const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
|
|
1805
1849
|
const textMentionAll = this.hasExplicitMention(text, 'all');
|
|
1806
|
-
const structMentionSelf = this._aid ?
|
|
1807
|
-
const structMentionAll =
|
|
1850
|
+
const structMentionSelf = this._aid ? payloadMentionAids.includes(this._aid) : false;
|
|
1851
|
+
const structMentionAll = mentionedAll;
|
|
1808
1852
|
const reason = isOwnHandoff
|
|
1809
1853
|
? 'handoff.self'
|
|
1810
1854
|
: mentionedAll
|
|
1811
1855
|
? 'mention.all(struct)'
|
|
1812
1856
|
: mentionedSelf
|
|
1813
1857
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
1814
|
-
: `${
|
|
1815
|
-
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))}`);
|
|
1816
1860
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1817
1861
|
if (payloadType === 'action_card_reply')
|
|
1818
1862
|
return;
|
|
1819
1863
|
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1820
|
-
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)}`);
|
|
1821
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 });
|
|
1822
1866
|
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode, payloadType === 'event' ? 'notify' : 'send');
|
|
1823
1867
|
// 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
|
|
1824
1868
|
// 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
|
|
1825
1869
|
const renderMentionAids = Array.from(new Set([
|
|
1826
|
-
...
|
|
1870
|
+
...payloadMentionAids,
|
|
1827
1871
|
...this.extractMentionAidsFromText(text),
|
|
1828
1872
|
]));
|
|
1829
1873
|
this.dispatchMessage({
|
|
@@ -1846,7 +1890,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1846
1890
|
source: isOwnHandoff ? 'handoff' : undefined,
|
|
1847
1891
|
encrypted: msgEncrypted,
|
|
1848
1892
|
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
|
|
1849
|
-
|
|
1893
|
+
mentionMode: serverMentionMode,
|
|
1850
1894
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1851
1895
|
msgType: logDescriptor.msgType,
|
|
1852
1896
|
payloadType: logDescriptor.payloadType,
|
|
@@ -1855,17 +1899,9 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1855
1899
|
});
|
|
1856
1900
|
}
|
|
1857
1901
|
dispatchMessage(event) {
|
|
1858
|
-
//
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
return;
|
|
1862
|
-
this.seenMessages.set(event.messageId, Date.now());
|
|
1863
|
-
setTimeout(() => this.seenMessages.delete(event.messageId), 5 * 60 * 1000);
|
|
1864
|
-
// Track seq for acknowledge
|
|
1865
|
-
if (event.seq != null) {
|
|
1866
|
-
this.messageSeqMap.set(event.messageId, event.seq);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
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.
|
|
1869
1905
|
// Echo 机制:消息第一行包含 "echo"(不区分大小写)且原始内容 ≤10 字符时,直接回声
|
|
1870
1906
|
// 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1871
1907
|
const firstLine = event.text.split('\n')[0] || '';
|
|
@@ -1897,7 +1933,6 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1897
1933
|
}
|
|
1898
1934
|
if (!this.messageHandler)
|
|
1899
1935
|
return;
|
|
1900
|
-
const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
|
|
1901
1936
|
// Use caller-supplied replyContext (group path builds mentionUserIds);
|
|
1902
1937
|
// fall back to simple threadId-only context for private messages
|
|
1903
1938
|
let replyContext = event.replyContext;
|
|
@@ -1922,12 +1957,12 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1922
1957
|
messageId: event.messageId,
|
|
1923
1958
|
causation: event.causation,
|
|
1924
1959
|
threadId: event.threadId,
|
|
1925
|
-
mentions:
|
|
1960
|
+
mentions: event.mentions,
|
|
1926
1961
|
mentionAids: event.mentionAids,
|
|
1927
1962
|
isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
|
|
1928
1963
|
replyContext,
|
|
1929
1964
|
source: event.source,
|
|
1930
|
-
|
|
1965
|
+
mentionMode: event.mentionMode,
|
|
1931
1966
|
images: event.images,
|
|
1932
1967
|
msgType: event.msgType,
|
|
1933
1968
|
payloadType: event.payloadType,
|
|
@@ -2445,31 +2480,12 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2445
2480
|
// ── Public API (same interface as before) ───────────────────
|
|
2446
2481
|
setEventBus(bus) {
|
|
2447
2482
|
// 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
|
|
2448
|
-
if (this.eventBus && this.ownerBoundHandler && typeof this.eventBus.unsubscribe === 'function') {
|
|
2449
|
-
this.eventBus.unsubscribe('channel:owner-bound', this.ownerBoundHandler);
|
|
2450
|
-
}
|
|
2451
2483
|
if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
|
|
2452
2484
|
this.eventBus.unsubscribe('task:queued', this.queuedHandler);
|
|
2453
2485
|
}
|
|
2454
|
-
this.ownerBoundHandler = null;
|
|
2455
2486
|
this.queuedHandler = null;
|
|
2456
2487
|
this.eventBus = bus;
|
|
2457
2488
|
if (bus && typeof bus.subscribe === 'function') {
|
|
2458
|
-
const handler = (event) => {
|
|
2459
|
-
if (event.channelName !== this.config.channelName)
|
|
2460
|
-
return;
|
|
2461
|
-
// sendWelcomeMessage 内部读 config.json 中最新的 owners[0],并幂等检查 initialized
|
|
2462
|
-
// 自身做 client 健康检查后再发
|
|
2463
|
-
if (!this.client) {
|
|
2464
|
-
logger.info(`${this.logPrefix()} owner-bound event received but client not connected; skip welcome retry`);
|
|
2465
|
-
return;
|
|
2466
|
-
}
|
|
2467
|
-
this.sendWelcomeMessage().catch(e => {
|
|
2468
|
-
logger.warn(`${this.logPrefix()} owner-bound welcome retry failed: ${e}`);
|
|
2469
|
-
});
|
|
2470
|
-
};
|
|
2471
|
-
bus.subscribe('channel:owner-bound', handler);
|
|
2472
|
-
this.ownerBoundHandler = handler;
|
|
2473
2489
|
const queuedHandler = (event) => {
|
|
2474
2490
|
if (event.channel !== this.config.channelName)
|
|
2475
2491
|
return;
|
|
@@ -2485,8 +2501,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2485
2501
|
onMessage(handler) {
|
|
2486
2502
|
this.messageHandler = handler;
|
|
2487
2503
|
}
|
|
2488
|
-
|
|
2489
|
-
this.
|
|
2504
|
+
setMentionModeResolver(resolver) {
|
|
2505
|
+
this.mentionModeResolver = resolver;
|
|
2490
2506
|
}
|
|
2491
2507
|
onRecall(handler) {
|
|
2492
2508
|
this.recallHandler = handler;
|
|
@@ -2575,6 +2591,21 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2575
2591
|
finalPayload.chatmode = context.metadata.chatmode;
|
|
2576
2592
|
return finalPayload;
|
|
2577
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
|
+
}
|
|
2578
2609
|
rememberInvalidatedInteraction(interactionId, reason) {
|
|
2579
2610
|
this.invalidatedInteractions.set(interactionId, reason);
|
|
2580
2611
|
const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
|
|
@@ -2666,6 +2697,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2666
2697
|
}
|
|
2667
2698
|
}
|
|
2668
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
|
+
}
|
|
2669
2710
|
if (!this.client || !this.connected)
|
|
2670
2711
|
return { ok: false };
|
|
2671
2712
|
const isGroup = this.isGroupId(channelId);
|
|
@@ -2680,8 +2721,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2680
2721
|
if (causationRef && causation)
|
|
2681
2722
|
registerAunCausation(causationRef, this.config.aid, targetAid, causation);
|
|
2682
2723
|
const payloadWithCausationRef = causationRef
|
|
2683
|
-
? { ...
|
|
2684
|
-
:
|
|
2724
|
+
? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
|
|
2725
|
+
: normalizedPayload;
|
|
2685
2726
|
// Truncate payload if too large (AUN SDK limit: 1MB)
|
|
2686
2727
|
const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
|
|
2687
2728
|
const params = { payload: truncatedPayload, encrypt };
|
|
@@ -2756,6 +2797,9 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2756
2797
|
this.forwardOutbound(result);
|
|
2757
2798
|
}
|
|
2758
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);
|
|
2759
2803
|
const interactionId = opts.postSend?.type === 'register_interaction_card'
|
|
2760
2804
|
? opts.postSend.requestId
|
|
2761
2805
|
: undefined;
|
|
@@ -2763,7 +2807,12 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2763
2807
|
logger.info(`${this.logPrefix()} Dropped invalidated interaction before durable enqueue: request=${interactionId}`);
|
|
2764
2808
|
return {};
|
|
2765
2809
|
}
|
|
2766
|
-
|
|
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));
|
|
2767
2816
|
const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
|
|
2768
2817
|
const entry = outbox.enqueue(this.config.aid, {
|
|
2769
2818
|
channelId,
|
|
@@ -2923,17 +2972,48 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2923
2972
|
}
|
|
2924
2973
|
let finalText = text;
|
|
2925
2974
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
2926
|
-
if (this.isGroupId(channelId)
|
|
2927
|
-
|
|
2928
|
-
|
|
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
|
+
}
|
|
2929
2996
|
}
|
|
2930
2997
|
}
|
|
2998
|
+
const operationId = typeof context?.metadata?.operationId === 'string'
|
|
2999
|
+
? context.metadata.operationId
|
|
3000
|
+
: undefined;
|
|
3001
|
+
if (operationId) {
|
|
3002
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
3003
|
+
if (hasMessageLogOperation(chatDir, operationId))
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
2931
3006
|
// Write-ahead: persist to outbox before attempting send
|
|
2932
3007
|
const entry = outbox.enqueue(this.config.aid, {
|
|
2933
3008
|
channelId,
|
|
3009
|
+
dedupeKey: operationId,
|
|
3010
|
+
critical: context?.metadata?.criticalDelivery === true,
|
|
2934
3011
|
type: 'text',
|
|
2935
3012
|
text: finalText,
|
|
2936
3013
|
context,
|
|
3014
|
+
ttl: typeof context?.metadata?.outboxTtl === 'number'
|
|
3015
|
+
? context.metadata.outboxTtl
|
|
3016
|
+
: undefined,
|
|
2937
3017
|
});
|
|
2938
3018
|
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
2939
3019
|
// 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
|
|
@@ -2956,16 +3036,27 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2956
3036
|
}
|
|
2957
3037
|
/** Daemon-side transport for `ec msg send` running inside an agent task. */
|
|
2958
3038
|
async sendDaemonMsg(args) {
|
|
2959
|
-
if (!this.connected || !this.client) {
|
|
2960
|
-
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
2961
|
-
}
|
|
2962
3039
|
if (!args.to || this.isGroupId(args.to)) {
|
|
2963
3040
|
return { ok: false, error: 'aun-msg-send only supports private AID targets', code: 'UNSUPPORTED_TARGET' };
|
|
2964
3041
|
}
|
|
2965
3042
|
if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
|
|
2966
3043
|
return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
|
|
2967
3044
|
}
|
|
2968
|
-
|
|
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
|
+
}
|
|
2969
3060
|
const info = await this.fetchPeerInfo(args.to);
|
|
2970
3061
|
const chatmode = info.type === 'human' ? 'interactive' : 'proactive';
|
|
2971
3062
|
if (!finalPayload.chatmode)
|
|
@@ -3070,20 +3161,34 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3070
3161
|
}
|
|
3071
3162
|
/** Daemon-side transport for `ec group send` running inside an agent task. */
|
|
3072
3163
|
async sendDaemonGroupMsg(args) {
|
|
3073
|
-
if (!this.connected || !this.client) {
|
|
3074
|
-
return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3075
|
-
}
|
|
3076
3164
|
if (!args.groupId || !this.isGroupId(args.groupId)) {
|
|
3077
3165
|
return { ok: false, error: 'aun-msg-send requires a valid group target', code: 'UNSUPPORTED_TARGET' };
|
|
3078
3166
|
}
|
|
3079
3167
|
if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
|
|
3080
3168
|
return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
|
|
3081
3169
|
}
|
|
3082
|
-
const finalPayload = this.stripUndefinedDeep({ ...args.payload });
|
|
3083
|
-
if (args.mentions?.length)
|
|
3084
|
-
finalPayload.mentions = args.mentions;
|
|
3085
|
-
const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
|
|
3086
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);
|
|
3087
3192
|
const result = await this.callAndTrace('group.send', {
|
|
3088
3193
|
group_id: args.groupId,
|
|
3089
3194
|
payload: this.truncatePayloadIfNeeded(finalPayload, 'group-send-ipc'),
|
|
@@ -3187,11 +3292,25 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3187
3292
|
const channelId = entry.channelId;
|
|
3188
3293
|
const finalText = entry.text;
|
|
3189
3294
|
const context = entry.context;
|
|
3295
|
+
const operationId = entry.dedupeKey
|
|
3296
|
+
|| (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
|
|
3297
|
+
if (operationId) {
|
|
3298
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
3299
|
+
if (hasMessageLogOperation(chatDir, operationId)) {
|
|
3300
|
+
logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
|
|
3301
|
+
return true;
|
|
3302
|
+
}
|
|
3303
|
+
if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
|
|
3304
|
+
const agentConfig = loadAgent(this.config.aid);
|
|
3305
|
+
if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active')
|
|
3306
|
+
return false;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3190
3309
|
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
3191
3310
|
const source = context?.metadata?.source ?? 'daemon';
|
|
3192
3311
|
const payload = { type: 'text', text: finalText };
|
|
3193
3312
|
if (this.isGroupId(channelId)) {
|
|
3194
|
-
const extracted = this.
|
|
3313
|
+
const extracted = this.extractMentionEntriesFromText(finalText);
|
|
3195
3314
|
if (extracted.length > 0)
|
|
3196
3315
|
payload.mentions = extracted;
|
|
3197
3316
|
}
|
|
@@ -3215,11 +3334,25 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3215
3334
|
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}`);
|
|
3216
3335
|
const isGroup = this.isGroupId(channelId);
|
|
3217
3336
|
const targetAid = channelId;
|
|
3337
|
+
if (operationId && entry.deliveryReceipt) {
|
|
3338
|
+
this.appendOutboundJsonl(channelId, {
|
|
3339
|
+
...classifyAunPayloadForLog(payload),
|
|
3340
|
+
msgId: entry.deliveryReceipt.messageId,
|
|
3341
|
+
encrypt: entry.deliveryReceipt.encrypt,
|
|
3342
|
+
context,
|
|
3343
|
+
isGroup,
|
|
3344
|
+
source,
|
|
3345
|
+
transport: entry.deliveryReceipt.transport,
|
|
3346
|
+
});
|
|
3347
|
+
return true;
|
|
3348
|
+
}
|
|
3218
3349
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
3219
3350
|
const encrypt = context?.metadata?.encrypted != null
|
|
3220
3351
|
? !!(context.metadata.encrypted)
|
|
3221
3352
|
: this.shouldEncrypt(encryptTarget);
|
|
3222
3353
|
const params = { payload, encrypt };
|
|
3354
|
+
if (context?.metadata?.persistRequired === true)
|
|
3355
|
+
params.persist_required = true;
|
|
3223
3356
|
try {
|
|
3224
3357
|
if (isGroup) {
|
|
3225
3358
|
params.group_id = channelId;
|
|
@@ -3237,6 +3370,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3237
3370
|
}
|
|
3238
3371
|
else {
|
|
3239
3372
|
this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
|
|
3373
|
+
this.checkpointTextDelivery(entry, mid, encrypt, result);
|
|
3240
3374
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
|
|
3241
3375
|
const statsContext = await this.groupStatsContext(channelId);
|
|
3242
3376
|
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send', statsContext);
|
|
@@ -3257,6 +3391,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3257
3391
|
}
|
|
3258
3392
|
else {
|
|
3259
3393
|
this.logAunSendAccepted('message.send', this.peerLabel(targetAid), result.message_id, encrypt, result, finalText);
|
|
3394
|
+
this.checkpointTextDelivery(entry, result.message_id, encrypt, result);
|
|
3260
3395
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
3261
3396
|
if (causation) {
|
|
3262
3397
|
registerAunCausation(result.message_id, this.config.aid, targetAid, causation);
|
|
@@ -3292,6 +3427,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3292
3427
|
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
3293
3428
|
return false;
|
|
3294
3429
|
}
|
|
3430
|
+
this.checkpointTextDelivery(entry, mid, false, result);
|
|
3295
3431
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
|
|
3296
3432
|
const statsContext = await this.groupStatsContext(channelId);
|
|
3297
3433
|
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode, 'send', statsContext);
|
|
@@ -3310,6 +3446,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3310
3446
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
3311
3447
|
return false;
|
|
3312
3448
|
}
|
|
3449
|
+
this.checkpointTextDelivery(entry, mid, false, result);
|
|
3313
3450
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
3314
3451
|
if (causation) {
|
|
3315
3452
|
registerAunCausation(mid, this.config.aid, targetAid, causation);
|
|
@@ -3341,6 +3478,18 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3341
3478
|
}
|
|
3342
3479
|
}
|
|
3343
3480
|
}
|
|
3481
|
+
checkpointTextDelivery(entry, messageId, encrypt, result) {
|
|
3482
|
+
if (!entry.dedupeKey)
|
|
3483
|
+
return;
|
|
3484
|
+
entry.deliveryReceipt = {
|
|
3485
|
+
messageId,
|
|
3486
|
+
encrypt,
|
|
3487
|
+
transport: this.sendReceiptFromResult(result),
|
|
3488
|
+
};
|
|
3489
|
+
if (!outbox.replace(this.config.aid, entry)) {
|
|
3490
|
+
logger.warn(`${this.logPrefix()} Failed to checkpoint durable operation receipt: ${entry.dedupeKey}`);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3344
3493
|
async deliverPayloadEntry(entry) {
|
|
3345
3494
|
const interactionId = entry.postSend?.type === 'register_interaction_card'
|
|
3346
3495
|
? entry.postSend.requestId
|
|
@@ -3360,6 +3509,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3360
3509
|
const context = entry.context;
|
|
3361
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}`);
|
|
3362
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
|
+
}
|
|
3363
3516
|
if (!sent.ok || !sent.messageId)
|
|
3364
3517
|
return sent;
|
|
3365
3518
|
const isGroup = this.isGroupId(channelId);
|
|
@@ -3371,13 +3524,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3371
3524
|
}
|
|
3372
3525
|
/** 有效会话正文写入 messages.jsonl(message.send/group.send 成功后调用)。 */
|
|
3373
3526
|
appendOutboundJsonl(channelId, descriptor) {
|
|
3527
|
+
const operationId = typeof descriptor.context?.metadata?.operationId === 'string'
|
|
3528
|
+
? descriptor.context.metadata.operationId
|
|
3529
|
+
: undefined;
|
|
3374
3530
|
try {
|
|
3375
3531
|
const { content, msgType, payloadType, payloadSummary, msgId, encrypt, transport, context, isGroup, source = 'daemon' } = descriptor;
|
|
3376
3532
|
const sessionsDir = resolvePaths().sessionsDir;
|
|
3377
3533
|
const selfAID = this.config.aid;
|
|
3378
3534
|
const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfAID);
|
|
3379
3535
|
const chatmode = context?.metadata?.chatmode;
|
|
3380
|
-
|
|
3536
|
+
const entry = buildOutboundEntry({
|
|
3381
3537
|
from: selfAID,
|
|
3382
3538
|
to: channelId,
|
|
3383
3539
|
sessionId: context?.sessionId,
|
|
@@ -3397,9 +3553,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3397
3553
|
payloadType,
|
|
3398
3554
|
payloadSummary,
|
|
3399
3555
|
source,
|
|
3400
|
-
|
|
3556
|
+
operationId,
|
|
3557
|
+
});
|
|
3558
|
+
if (operationId)
|
|
3559
|
+
appendMessageLogStrict(chatDir, entry);
|
|
3560
|
+
else
|
|
3561
|
+
appendMessageLog(chatDir, entry);
|
|
3401
3562
|
}
|
|
3402
3563
|
catch (e) {
|
|
3564
|
+
if (operationId)
|
|
3565
|
+
throw e;
|
|
3403
3566
|
logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
|
|
3404
3567
|
}
|
|
3405
3568
|
}
|
|
@@ -3412,13 +3575,14 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3412
3575
|
* 存储键:group_id/peer_aid + sender_aid + context.type + context.id
|
|
3413
3576
|
*/
|
|
3414
3577
|
async sendThought(channelId, taskId, payload, context) {
|
|
3415
|
-
if (!this.connected || !this.client)
|
|
3416
|
-
return;
|
|
3417
3578
|
if (!taskId)
|
|
3418
3579
|
return;
|
|
3580
|
+
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
3581
|
+
const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
|
|
3582
|
+
if (!this.connected || !this.client)
|
|
3583
|
+
return;
|
|
3419
3584
|
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
3420
3585
|
const targetId = channelId;
|
|
3421
|
-
const finalPayload = this.stripUndefinedDeep(payload);
|
|
3422
3586
|
const encrypt = context?.metadata?.encrypted != null
|
|
3423
3587
|
? !!(context.metadata.encrypted)
|
|
3424
3588
|
: this.shouldEncrypt(targetId);
|
|
@@ -3488,6 +3652,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3488
3652
|
}
|
|
3489
3653
|
/** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
|
|
3490
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);
|
|
3491
3659
|
if (!this.connected || !this.client)
|
|
3492
3660
|
return null;
|
|
3493
3661
|
const isGroup = this.isGroupId(channelId);
|
|
@@ -3496,7 +3664,6 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
3496
3664
|
const encrypt = context?.metadata?.encrypted != null
|
|
3497
3665
|
? !!(context.metadata.encrypted)
|
|
3498
3666
|
: this.shouldEncrypt(encryptTarget);
|
|
3499
|
-
const finalPayload = this.stripUndefinedDeep({ ...payload });
|
|
3500
3667
|
if (context?.threadId && !finalPayload.thread_id)
|
|
3501
3668
|
finalPayload.thread_id = context.threadId;
|
|
3502
3669
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
@@ -4233,11 +4400,30 @@ export class AUNChannelPlugin {
|
|
|
4233
4400
|
return;
|
|
4234
4401
|
}
|
|
4235
4402
|
case 'system.error': {
|
|
4236
|
-
|
|
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);
|
|
4237
4414
|
return;
|
|
4238
4415
|
}
|
|
4239
4416
|
case 'result.error': {
|
|
4240
|
-
|
|
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);
|
|
4241
4427
|
return;
|
|
4242
4428
|
}
|
|
4243
4429
|
case 'result.file': {
|
|
@@ -4435,15 +4621,12 @@ export class AUNChannelPlugin {
|
|
|
4435
4621
|
});
|
|
4436
4622
|
});
|
|
4437
4623
|
}
|
|
4438
|
-
if (typeof channel.
|
|
4439
|
-
channel.
|
|
4440
|
-
|
|
4441
|
-
// dispatch_mode(mention/broadcast);未设值返回 undefined,回退服务端下发值。
|
|
4442
|
-
const mentionMode = resolveEffective({
|
|
4624
|
+
if (typeof channel.setMentionModeResolver === 'function') {
|
|
4625
|
+
channel.setMentionModeResolver(async (channelId) => {
|
|
4626
|
+
return resolveEffective({
|
|
4443
4627
|
self: aid,
|
|
4444
4628
|
peerKey: formatPeerKey('aun', channelId),
|
|
4445
4629
|
}, { cache: true }).mentionMode;
|
|
4446
|
-
return mentionModeToDispatch(mentionMode);
|
|
4447
4630
|
});
|
|
4448
4631
|
}
|
|
4449
4632
|
},
|