evolcore 0.0.20 → 0.0.22
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 +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +30 -6
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
package/dist/channels/aun.js
CHANGED
|
@@ -11,7 +11,7 @@ import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
|
|
|
11
11
|
import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
|
|
12
12
|
import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
|
|
13
13
|
import { appendAidEvent } from '../utils/instance-registry.js';
|
|
14
|
-
import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
|
|
14
|
+
import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
|
|
15
15
|
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
16
16
|
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
17
17
|
import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
|
|
@@ -28,22 +28,31 @@ import * as outbox from '../aun/outbox.js';
|
|
|
28
28
|
import { guessMime, formatSize } from '../utils/media-cache.js';
|
|
29
29
|
import { formatPeerKey, PeerIdentityCache } from '../core/relation/peer-identity.js';
|
|
30
30
|
import { getFirstStaticAgentOwner } from '../config/peer-role-resolver.js';
|
|
31
|
-
import { isHClassPath } from '../core/protected-paths.js';
|
|
31
|
+
import { isHClassPath } from '../core/permission/protected-paths.js';
|
|
32
32
|
import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
|
|
33
33
|
import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
|
|
34
34
|
import { recordCausationSpan } from '../core/causation/audit.js';
|
|
35
35
|
import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
36
36
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
37
|
-
import {
|
|
37
|
+
import { toolInputForDisplay } from '../utils/tool-summary.js';
|
|
38
|
+
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
38
39
|
import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
|
|
39
40
|
import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
|
|
40
41
|
import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
42
|
+
import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
|
|
41
43
|
export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
|
|
42
44
|
const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
43
45
|
const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
44
46
|
const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
|
|
45
47
|
const AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS = 3;
|
|
46
48
|
const AUN_ATTACHMENT_RETRY_DELAYS_MS = [250, 750];
|
|
49
|
+
const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
|
|
50
|
+
const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
|
|
51
|
+
const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
|
|
52
|
+
const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
|
|
53
|
+
export function formatAunSendLogText(text) {
|
|
54
|
+
return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
|
|
55
|
+
}
|
|
47
56
|
function attachmentDownloadHost(url) {
|
|
48
57
|
try {
|
|
49
58
|
return new URL(url).host || '<unknown>';
|
|
@@ -326,7 +335,7 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
|
|
|
326
335
|
const permanentHttpStatus = httpStatus !== undefined
|
|
327
336
|
&& httpStatus >= 400 && httpStatus < 500
|
|
328
337
|
&& !transientHttpStatus;
|
|
329
|
-
const permanentByText = !relayTargetMissing &&
|
|
338
|
+
const permanentByText = !relayTargetMissing && /\b(?:group|peer|agent|recipient|target|member|object|task|stream)(?:[\s_-]+(?:id|aid))?[\s_-]*(?:not[\s_-]*found|does[\s_-]*not[\s_-]*exist)|不存在|group[_ -]?not[_ -]?found|not[_ -]?a[_ -]?member|(?:permission|access|role).*(?:denied|forbidden|拒绝|无权限)|(?:invalid|malformed|bad)\s*(?:argument|param|request)|unauthori[sz]ed|authentication failed|signature invalid/i.test(text);
|
|
330
339
|
const retryByText = /timeout|timed out|temporar|unavailable|overload|rate.?limit|too many requests|try again|not connected|connection|network|socket|econn|eai_again|etimedout|epipe|broken pipe|connection refused|connect timeout|fetch failed|dns|reset by peer|gateway service degraded|upstream/i.test(text);
|
|
331
340
|
const acceptedDispatch = details.dispatchStatus !== undefined
|
|
332
341
|
&& AUN_ACCEPTED_DISPATCH_STATUSES.has(details.dispatchStatus);
|
|
@@ -397,8 +406,10 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
|
|
|
397
406
|
: {}),
|
|
398
407
|
};
|
|
399
408
|
}
|
|
400
|
-
function sentOutboxResult() {
|
|
401
|
-
return
|
|
409
|
+
function sentOutboxResult(messageId) {
|
|
410
|
+
return messageId
|
|
411
|
+
? { status: 'sent', messageId }
|
|
412
|
+
: { status: 'permanent', error: 'AUN send completed without a remote message_id', code: 'MISSING_MESSAGE_ID' };
|
|
402
413
|
}
|
|
403
414
|
function setIfDefined(target, key, value) {
|
|
404
415
|
if (value !== undefined)
|
|
@@ -551,6 +562,35 @@ export class AUNChannel {
|
|
|
551
562
|
* 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
|
|
552
563
|
* 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
|
|
553
564
|
*/
|
|
565
|
+
callClient(method, params) {
|
|
566
|
+
return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)));
|
|
567
|
+
}
|
|
568
|
+
withOutboundSendGate(method, run) {
|
|
569
|
+
if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method))
|
|
570
|
+
return run();
|
|
571
|
+
return new Promise((resolve, reject) => {
|
|
572
|
+
this.outboundSendQueue.push({
|
|
573
|
+
run: run,
|
|
574
|
+
resolve: resolve,
|
|
575
|
+
reject,
|
|
576
|
+
});
|
|
577
|
+
this.pumpOutboundSendGate();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
pumpOutboundSendGate() {
|
|
581
|
+
while (this.outboundSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
|
|
582
|
+
&& this.outboundSendQueue.length > 0) {
|
|
583
|
+
const job = this.outboundSendQueue.shift();
|
|
584
|
+
this.outboundSendActive++;
|
|
585
|
+
Promise.resolve()
|
|
586
|
+
.then(job.run)
|
|
587
|
+
.then(job.resolve, job.reject)
|
|
588
|
+
.finally(() => {
|
|
589
|
+
this.outboundSendActive--;
|
|
590
|
+
this.pumpOutboundSendGate();
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
554
594
|
async callAndTrace(method, params, opts) {
|
|
555
595
|
this.trace('OUT', method, params);
|
|
556
596
|
// RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
|
|
@@ -560,7 +600,7 @@ export class AUNChannel {
|
|
|
560
600
|
// SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
|
|
561
601
|
const SLOW_RPC_WARN_MS = 3000;
|
|
562
602
|
try {
|
|
563
|
-
const result = await this.
|
|
603
|
+
const result = await this.callClient(method, params);
|
|
564
604
|
const durationMs = Date.now() - rpcStart;
|
|
565
605
|
if (!opts?.silentOk) {
|
|
566
606
|
const r = result;
|
|
@@ -1026,6 +1066,18 @@ export class AUNChannel {
|
|
|
1026
1066
|
.replace(/[ \t]+/g, ' ')
|
|
1027
1067
|
.trim();
|
|
1028
1068
|
}
|
|
1069
|
+
/** Normalize a command after a client-rendered friendly mention label. */
|
|
1070
|
+
normalizeSlashCommandText(text) {
|
|
1071
|
+
const trimmed = text.trim();
|
|
1072
|
+
const leadingLabels = trimmed.match(/^(?:@[^\s]+\s+)+(?=\/)/u);
|
|
1073
|
+
const commandText = leadingLabels
|
|
1074
|
+
? trimmed.slice(leadingLabels[0].length).trimStart()
|
|
1075
|
+
: trimmed;
|
|
1076
|
+
// Keep the command contract consistent for mixed friendly/canonical @
|
|
1077
|
+
// tokens: selector labels are removed above, then known AID-shaped tokens
|
|
1078
|
+
// anywhere in the command are removed by the existing helper.
|
|
1079
|
+
return this.stripAllMentions(commandText);
|
|
1080
|
+
}
|
|
1029
1081
|
parsePayloadMentionsOrReject(payload, context, messageId, seq) {
|
|
1030
1082
|
const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
1031
1083
|
? payload.mentions
|
|
@@ -1241,6 +1293,8 @@ export class AUNChannel {
|
|
|
1241
1293
|
/** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
|
|
1242
1294
|
invalidatedInteractions = new Map();
|
|
1243
1295
|
mentionModeResolver;
|
|
1296
|
+
/** Agent-level policy controlling whether group slash commands require structured mentions. */
|
|
1297
|
+
groupSlashRequireMentionsResolver;
|
|
1244
1298
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
1245
1299
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
1246
1300
|
'merge', 'link', 'location', 'personal_card',
|
|
@@ -1283,6 +1337,16 @@ export class AUNChannel {
|
|
|
1283
1337
|
aidState;
|
|
1284
1338
|
aidStatsCollector;
|
|
1285
1339
|
outboxInFlight = new Set();
|
|
1340
|
+
/** Shared gateway-facing send limit for replies, activity, observer and thought messages. */
|
|
1341
|
+
static OUTBOUND_SEND_METHODS = new Set([
|
|
1342
|
+
'message.send',
|
|
1343
|
+
'group.send',
|
|
1344
|
+
'message.thought.put',
|
|
1345
|
+
'group.thought.put',
|
|
1346
|
+
]);
|
|
1347
|
+
static OUTBOUND_SEND_CONCURRENCY = 4;
|
|
1348
|
+
outboundSendActive = 0;
|
|
1349
|
+
outboundSendQueue = [];
|
|
1286
1350
|
constructor(config) {
|
|
1287
1351
|
this.config = config;
|
|
1288
1352
|
this.agentDir = agentDirPath(config.aid);
|
|
@@ -1355,6 +1419,14 @@ export class AUNChannel {
|
|
|
1355
1419
|
}
|
|
1356
1420
|
}
|
|
1357
1421
|
async _initClientInner() {
|
|
1422
|
+
this.observerGroupMembers.clear();
|
|
1423
|
+
for (const groupId of new Set([
|
|
1424
|
+
...this.observerGroupMembershipEpoch.keys(),
|
|
1425
|
+
...this.observerGroupMemberFetches.keys(),
|
|
1426
|
+
])) {
|
|
1427
|
+
this.observerGroupMembershipEpoch.set(groupId, (this.observerGroupMembershipEpoch.get(groupId) ?? 0) + 1);
|
|
1428
|
+
}
|
|
1429
|
+
this.observerGroupMemberFetches.clear();
|
|
1358
1430
|
// Clean up existing client if any
|
|
1359
1431
|
if (this.client) {
|
|
1360
1432
|
this.trace('OUT', 'client.close', { reason: 'initClient' });
|
|
@@ -1423,6 +1495,15 @@ export class AUNChannel {
|
|
|
1423
1495
|
logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
|
|
1424
1496
|
this.handleIncomingGroupMessage(data);
|
|
1425
1497
|
});
|
|
1498
|
+
client.on('group.changed', (data) => {
|
|
1499
|
+
if (!isCurrentClient())
|
|
1500
|
+
return;
|
|
1501
|
+
const record = data && typeof data === 'object' ? data : {};
|
|
1502
|
+
const envelope = record.envelope && typeof record.envelope === 'object'
|
|
1503
|
+
? record.envelope
|
|
1504
|
+
: {};
|
|
1505
|
+
this.invalidateObserverGroupMembers(envelope.group_id ?? record.group_id);
|
|
1506
|
+
});
|
|
1426
1507
|
}
|
|
1427
1508
|
const handleSdkState = (source) => (data) => {
|
|
1428
1509
|
if (!isCurrentClient())
|
|
@@ -1988,7 +2069,7 @@ export class AUNChannel {
|
|
|
1988
2069
|
// 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
|
|
1989
2070
|
// (即便日后 from-owner 排除规则调整,也不会泄漏)。
|
|
1990
2071
|
if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
|
|
1991
|
-
this.forwardInbound(msg);
|
|
2072
|
+
void this.forwardInbound(msg);
|
|
1992
2073
|
}
|
|
1993
2074
|
// 回声过滤:自己发出的消息会被 gateway fanout 回来,
|
|
1994
2075
|
// 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
|
|
@@ -2159,7 +2240,7 @@ export class AUNChannel {
|
|
|
2159
2240
|
}
|
|
2160
2241
|
// Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
|
|
2161
2242
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
2162
|
-
this.forwardInbound(msg);
|
|
2243
|
+
void this.forwardInbound(msg);
|
|
2163
2244
|
logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
|
|
2164
2245
|
if (!groupId || !senderAid) {
|
|
2165
2246
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -2270,9 +2351,13 @@ export class AUNChannel {
|
|
|
2270
2351
|
const firstLineGroup = text.split('\n')[0] || '';
|
|
2271
2352
|
const hasEvolCoreTraceGroup = /\[EvolCore\.(receive|reply|agent)\]/.test(text);
|
|
2272
2353
|
const isEchoMsg = /echo/i.test(firstLineGroup) && !hasEvolCoreTraceGroup;
|
|
2354
|
+
const slashCommandText = this.normalizeSlashCommandText(text);
|
|
2273
2355
|
// 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
|
|
2274
2356
|
// echo 消息走独立的 trace 流程,不参与命令语义判定。
|
|
2275
|
-
const isCommandMsg = !isEchoMsg &&
|
|
2357
|
+
const isCommandMsg = !isEchoMsg && slashCommandText.startsWith('/');
|
|
2358
|
+
const groupSlashRequireMentions = this.groupSlashRequireMentionsResolver
|
|
2359
|
+
? await this.groupSlashRequireMentionsResolver().catch(() => undefined) ?? true
|
|
2360
|
+
: true;
|
|
2276
2361
|
if (isEchoMsg) {
|
|
2277
2362
|
// 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
|
|
2278
2363
|
// >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
|
|
@@ -2299,9 +2384,10 @@ export class AUNChannel {
|
|
|
2299
2384
|
}
|
|
2300
2385
|
else {
|
|
2301
2386
|
// 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
|
|
2302
|
-
// slash
|
|
2303
|
-
//
|
|
2304
|
-
const enforceMention = mentionMode === 'mention-only'
|
|
2387
|
+
// slash 命令默认要求结构化 @;Agent 可通过
|
|
2388
|
+
// groupSlashRequireMentions=false 选择仅按正文命令格式处理。
|
|
2389
|
+
const enforceMention = (mentionMode === 'mention-only' && !isCommandMsg)
|
|
2390
|
+
|| (isCommandMsg && groupSlashRequireMentions);
|
|
2305
2391
|
const isMentioned = mentionedSelf || mentionedAll;
|
|
2306
2392
|
// 过滤逻辑下移到响应层:这里只标记,不过滤
|
|
2307
2393
|
// 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
|
|
@@ -2309,14 +2395,14 @@ export class AUNChannel {
|
|
|
2309
2395
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
2310
2396
|
this.acknowledgeImmediately(messageId, seq);
|
|
2311
2397
|
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
2312
|
-
logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
2398
|
+
logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
2313
2399
|
return;
|
|
2314
2400
|
}
|
|
2315
2401
|
}
|
|
2316
2402
|
// 命令消息:剥离所有 @(多 agent 被 @ 时各自拿到干净的 /status 各自执行);
|
|
2317
2403
|
// 普通消息:仅在唯一 @ 是自己时剥离,保留其他 @ 供 agent 感知。
|
|
2318
2404
|
const strippedText = isCommandMsg
|
|
2319
|
-
?
|
|
2405
|
+
? slashCommandText
|
|
2320
2406
|
: this.stripSelfMentionIfOnly(text, this._aid);
|
|
2321
2407
|
// Detect attachments before the empty-text guard (顶层 + 嵌套)
|
|
2322
2408
|
const rawAttachments = this.collectAllAttachments(payload);
|
|
@@ -2347,7 +2433,7 @@ export class AUNChannel {
|
|
|
2347
2433
|
: mentionedSelf
|
|
2348
2434
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
2349
2435
|
: `${mentionMode}.no-mention`;
|
|
2350
|
-
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))}`);
|
|
2436
|
+
logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
2351
2437
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
2352
2438
|
if (payloadType === 'action_card_reply')
|
|
2353
2439
|
return;
|
|
@@ -2518,6 +2604,9 @@ export class AUNChannel {
|
|
|
2518
2604
|
// observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
|
|
2519
2605
|
// in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
|
|
2520
2606
|
observerConfigResolver;
|
|
2607
|
+
observerGroupMembers = new Map();
|
|
2608
|
+
observerGroupMemberFetches = new Map();
|
|
2609
|
+
observerGroupMembershipEpoch = new Map();
|
|
2521
2610
|
/** 注入观察者配置读取器(daemon 侧从 EvolAgent merged config 读)。 */
|
|
2522
2611
|
setObserverConfigResolver(fn) {
|
|
2523
2612
|
this.observerConfigResolver = fn;
|
|
@@ -2536,23 +2625,129 @@ export class AUNChannel {
|
|
|
2536
2625
|
* data 为 SDK message.received / group.message_created 回调的整个对象,
|
|
2537
2626
|
* 不拆解、不重组——SDK 信封结构变化不影响此处。
|
|
2538
2627
|
*/
|
|
2628
|
+
async resolveObserverGroupMembers(groupId) {
|
|
2629
|
+
if (!groupId || !this.client)
|
|
2630
|
+
return undefined;
|
|
2631
|
+
const cached = this.observerGroupMembers.get(groupId);
|
|
2632
|
+
if (cached && cached.expiresAt > Date.now())
|
|
2633
|
+
return cached.aids;
|
|
2634
|
+
const pending = this.observerGroupMemberFetches.get(groupId);
|
|
2635
|
+
if (pending)
|
|
2636
|
+
return pending;
|
|
2637
|
+
const epoch = this.observerGroupMembershipEpoch.get(groupId) ?? 0;
|
|
2638
|
+
let request;
|
|
2639
|
+
request = (async () => {
|
|
2640
|
+
try {
|
|
2641
|
+
const result = await this.callAndTrace('group.get_members', {
|
|
2642
|
+
group_id: groupId,
|
|
2643
|
+
page: 1,
|
|
2644
|
+
size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
|
|
2645
|
+
});
|
|
2646
|
+
const rawMembers = Array.isArray(result?.members)
|
|
2647
|
+
? result.members
|
|
2648
|
+
: Array.isArray(result?.items) ? result.items : undefined;
|
|
2649
|
+
if (!rawMembers)
|
|
2650
|
+
return undefined;
|
|
2651
|
+
const hasTotal = Number.isFinite(Number(result?.total));
|
|
2652
|
+
const hasMore = result?.has_more === true || result?.hasMore === true;
|
|
2653
|
+
if (!hasTotal && (hasMore || rawMembers.length >= OBSERVER_GROUP_MEMBERS_PAGE_SIZE))
|
|
2654
|
+
return undefined;
|
|
2655
|
+
const allMembers = [...rawMembers];
|
|
2656
|
+
const total = Number(result?.total);
|
|
2657
|
+
const pageSize = Number(result?.size) > 0 ? Number(result.size) : OBSERVER_GROUP_MEMBERS_PAGE_SIZE;
|
|
2658
|
+
const pageCount = Number.isFinite(total) && total > allMembers.length
|
|
2659
|
+
? Math.ceil(total / pageSize)
|
|
2660
|
+
: 1;
|
|
2661
|
+
if (pageCount > OBSERVER_GROUP_MEMBERS_MAX_PAGES)
|
|
2662
|
+
return undefined;
|
|
2663
|
+
for (let page = 2; page <= pageCount; page++) {
|
|
2664
|
+
const next = await this.callAndTrace('group.get_members', {
|
|
2665
|
+
group_id: groupId,
|
|
2666
|
+
page,
|
|
2667
|
+
size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
|
|
2668
|
+
});
|
|
2669
|
+
const nextMembers = Array.isArray(next?.members)
|
|
2670
|
+
? next.members
|
|
2671
|
+
: Array.isArray(next?.items) ? next.items : undefined;
|
|
2672
|
+
if (!nextMembers)
|
|
2673
|
+
return undefined;
|
|
2674
|
+
allMembers.push(...nextMembers);
|
|
2675
|
+
}
|
|
2676
|
+
if (hasTotal && total > allMembers.length)
|
|
2677
|
+
return undefined;
|
|
2678
|
+
const aids = new Set();
|
|
2679
|
+
for (const member of allMembers) {
|
|
2680
|
+
const aid = typeof member === 'string'
|
|
2681
|
+
? member.trim()
|
|
2682
|
+
: typeof member?.aid === 'string' ? member.aid.trim()
|
|
2683
|
+
: typeof member?.member_aid === 'string' ? member.member_aid.trim() : '';
|
|
2684
|
+
if (aid)
|
|
2685
|
+
aids.add(aid);
|
|
2686
|
+
}
|
|
2687
|
+
if ((this.observerGroupMembershipEpoch.get(groupId) ?? 0) === epoch) {
|
|
2688
|
+
this.observerGroupMembers.set(groupId, { aids, expiresAt: Date.now() + OBSERVER_GROUP_MEMBERS_TTL_MS });
|
|
2689
|
+
}
|
|
2690
|
+
return aids;
|
|
2691
|
+
}
|
|
2692
|
+
catch (error) {
|
|
2693
|
+
logger.debug(`${this.logPrefix()} observer group membership lookup failed group=${groupId}: ${error}`);
|
|
2694
|
+
return undefined;
|
|
2695
|
+
}
|
|
2696
|
+
finally {
|
|
2697
|
+
if (this.observerGroupMemberFetches.get(groupId) === request) {
|
|
2698
|
+
this.observerGroupMemberFetches.delete(groupId);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
})();
|
|
2702
|
+
this.observerGroupMemberFetches.set(groupId, request);
|
|
2703
|
+
return request;
|
|
2704
|
+
}
|
|
2705
|
+
invalidateObserverGroupMembers(groupId) {
|
|
2706
|
+
if (typeof groupId !== 'string' || !groupId.trim())
|
|
2707
|
+
return;
|
|
2708
|
+
const key = groupId.trim();
|
|
2709
|
+
this.observerGroupMembers.delete(key);
|
|
2710
|
+
this.observerGroupMembershipEpoch.set(key, (this.observerGroupMembershipEpoch.get(key) ?? 0) + 1);
|
|
2711
|
+
}
|
|
2712
|
+
async observerRecipients(owners, envelope) {
|
|
2713
|
+
if (owners.length === 0)
|
|
2714
|
+
return owners;
|
|
2715
|
+
const groupId = typeof envelope.group_id === 'string' ? envelope.group_id.trim() : '';
|
|
2716
|
+
const members = groupId ? await this.resolveObserverGroupMembers(groupId) : undefined;
|
|
2717
|
+
// Unknown membership is fail-open for observation: only an explicit member
|
|
2718
|
+
// response may suppress the redundant owner DM.
|
|
2719
|
+
return owners.filter(owner => !(members?.has(owner)));
|
|
2720
|
+
}
|
|
2539
2721
|
forwardInbound(data) {
|
|
2540
2722
|
if (!this.connected || !this.client)
|
|
2541
|
-
return;
|
|
2723
|
+
return Promise.resolve();
|
|
2542
2724
|
if (payloadTypeForObserver(data)?.startsWith('menu.'))
|
|
2543
|
-
return;
|
|
2725
|
+
return Promise.resolve();
|
|
2544
2726
|
const { observable, owners } = this.getObserverConfig();
|
|
2545
2727
|
if (!observable || owners.length === 0)
|
|
2546
|
-
return;
|
|
2547
|
-
const env = (data?.envelope && typeof data.envelope === 'object')
|
|
2728
|
+
return Promise.resolve();
|
|
2729
|
+
const env = (data?.envelope && typeof data.envelope === 'object')
|
|
2730
|
+
? { ...data.envelope }
|
|
2731
|
+
: {};
|
|
2732
|
+
if (!env.group_id && typeof data.group_id === 'string')
|
|
2733
|
+
env.group_id = data.group_id;
|
|
2548
2734
|
const from = env.from ?? '';
|
|
2549
2735
|
if (this._aid && from === this._aid)
|
|
2550
|
-
return; // self-echo:已在出站转过
|
|
2736
|
+
return Promise.resolve(); // self-echo:已在出站转过
|
|
2551
2737
|
// 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
|
|
2552
|
-
const
|
|
2553
|
-
if (
|
|
2554
|
-
return;
|
|
2555
|
-
|
|
2738
|
+
const candidateOwners = owners.filter(o => o !== from);
|
|
2739
|
+
if (candidateOwners.length === 0)
|
|
2740
|
+
return Promise.resolve();
|
|
2741
|
+
if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
|
|
2742
|
+
this.emitForward('inbound', data, candidateOwners);
|
|
2743
|
+
return Promise.resolve();
|
|
2744
|
+
}
|
|
2745
|
+
return this.observerRecipients(candidateOwners, env)
|
|
2746
|
+
.then(recipientOwners => {
|
|
2747
|
+
if (recipientOwners.length > 0)
|
|
2748
|
+
this.emitForward('inbound', data, recipientOwners);
|
|
2749
|
+
})
|
|
2750
|
+
.catch(error => logger.debug(`${this.logPrefix()} observer inbound recipient lookup failed: ${error}`));
|
|
2556
2751
|
}
|
|
2557
2752
|
/**
|
|
2558
2753
|
* 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
|
|
@@ -2561,20 +2756,33 @@ export class AUNChannel {
|
|
|
2561
2756
|
*/
|
|
2562
2757
|
forwardOutbound(result) {
|
|
2563
2758
|
if (!this.connected || !this.client)
|
|
2564
|
-
return;
|
|
2759
|
+
return Promise.resolve();
|
|
2565
2760
|
if (payloadTypeForObserver(result)?.startsWith('menu.'))
|
|
2566
|
-
return;
|
|
2761
|
+
return Promise.resolve();
|
|
2567
2762
|
const { observable, owners } = this.getObserverConfig();
|
|
2568
2763
|
if (!observable || owners.length === 0)
|
|
2569
|
-
return;
|
|
2570
|
-
const env = (result?.envelope && typeof result.envelope === 'object')
|
|
2764
|
+
return Promise.resolve();
|
|
2765
|
+
const env = (result?.envelope && typeof result.envelope === 'object')
|
|
2766
|
+
? { ...result.envelope }
|
|
2767
|
+
: {};
|
|
2768
|
+
if (!env.group_id && typeof result.group_id === 'string')
|
|
2769
|
+
env.group_id = result.group_id;
|
|
2571
2770
|
const to = env.to ?? env.group_id ?? '';
|
|
2572
2771
|
// 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
|
|
2573
2772
|
// 但仍转发给其他 owner。
|
|
2574
|
-
const
|
|
2575
|
-
if (
|
|
2576
|
-
return;
|
|
2577
|
-
|
|
2773
|
+
const candidateOwners = owners.filter(o => o !== to);
|
|
2774
|
+
if (candidateOwners.length === 0)
|
|
2775
|
+
return Promise.resolve();
|
|
2776
|
+
if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
|
|
2777
|
+
this.emitForward('outbound', result, candidateOwners);
|
|
2778
|
+
return Promise.resolve();
|
|
2779
|
+
}
|
|
2780
|
+
return this.observerRecipients(candidateOwners, env)
|
|
2781
|
+
.then(recipientOwners => {
|
|
2782
|
+
if (recipientOwners.length > 0)
|
|
2783
|
+
this.emitForward('outbound', result, recipientOwners);
|
|
2784
|
+
})
|
|
2785
|
+
.catch(error => logger.debug(`${this.logPrefix()} observer outbound recipient lookup failed: ${error}`));
|
|
2578
2786
|
}
|
|
2579
2787
|
/**
|
|
2580
2788
|
* 实际投递 observer.forward 给每个 owner,使用 daemon 的全局 AUN 默认加密设置。
|
|
@@ -3058,6 +3266,9 @@ export class AUNChannel {
|
|
|
3058
3266
|
setMentionModeResolver(resolver) {
|
|
3059
3267
|
this.mentionModeResolver = resolver;
|
|
3060
3268
|
}
|
|
3269
|
+
setGroupSlashRequireMentionsResolver(resolver) {
|
|
3270
|
+
this.groupSlashRequireMentionsResolver = resolver;
|
|
3271
|
+
}
|
|
3061
3272
|
onRecall(handler) {
|
|
3062
3273
|
this.recallHandler = handler;
|
|
3063
3274
|
}
|
|
@@ -3083,7 +3294,11 @@ export class AUNChannel {
|
|
|
3083
3294
|
}
|
|
3084
3295
|
}
|
|
3085
3296
|
removeDeliveredOutboxEntry(entry) {
|
|
3086
|
-
|
|
3297
|
+
const messageId = entry.deliveryReceipt?.messageId;
|
|
3298
|
+
const finalized = messageId
|
|
3299
|
+
? outbox.markDelivered(this.config.aid, entry.id, messageId, entry)
|
|
3300
|
+
: outbox.removeIfRouteMatches(this.config.aid, entry);
|
|
3301
|
+
if (!finalized) {
|
|
3087
3302
|
logger.warn(`${this.logPrefix()} Preserved outbox entry whose route changed while delivery was in flight: id=${entry.id} submittedChannel=${entry.channelId}`);
|
|
3088
3303
|
}
|
|
3089
3304
|
}
|
|
@@ -3115,7 +3330,7 @@ export class AUNChannel {
|
|
|
3115
3330
|
return `aun_status=${receipt.status ?? 'unknown'} seq=${receipt.seq ?? 'unknown'} delivery_mode=${receipt.deliveryMode ?? 'unknown'} gateway_timestamp=${receipt.timestamp ?? 'unknown'}`;
|
|
3116
3331
|
}
|
|
3117
3332
|
logAunSendAccepted(method, target, messageId, encrypt, result, text) {
|
|
3118
|
-
const preview = text === undefined ? '' : ` text=${text
|
|
3333
|
+
const preview = text === undefined ? '' : ` text=${formatAunSendLogText(text)}`;
|
|
3119
3334
|
logger.info(`${this.logPrefix()} ${method} accepted by AUN: target=${target} mid=${messageId} encrypt=${encrypt} ${this.receiptLogFields(result)}${preview}`);
|
|
3120
3335
|
}
|
|
3121
3336
|
stripUndefinedDeep(value) {
|
|
@@ -3326,8 +3541,8 @@ export class AUNChannel {
|
|
|
3326
3541
|
}
|
|
3327
3542
|
messageIds.add(messageId);
|
|
3328
3543
|
const now = Date.now();
|
|
3329
|
-
const mapTtl = action.expiresAt && action.expiresAt
|
|
3330
|
-
? action.expiresAt - now
|
|
3544
|
+
const mapTtl = typeof action.expiresAt === 'number' && Number.isFinite(action.expiresAt)
|
|
3545
|
+
? Math.max(0, action.expiresAt - now)
|
|
3331
3546
|
: AUN_INTERACTION_CARD_TTL_MS;
|
|
3332
3547
|
const mapTimer = setTimeout(() => {
|
|
3333
3548
|
this.cardMessageIdMap.delete(messageId);
|
|
@@ -3390,7 +3605,7 @@ export class AUNChannel {
|
|
|
3390
3605
|
params.to = targetAid;
|
|
3391
3606
|
const callOnce = async (sendParams, fallback) => {
|
|
3392
3607
|
const result = fallback
|
|
3393
|
-
? await this.
|
|
3608
|
+
? await this.callClient(method, sendParams)
|
|
3394
3609
|
: await this.callAndTrace(method, sendParams);
|
|
3395
3610
|
const mid = this.messageIdFromSendResult(result);
|
|
3396
3611
|
if (!mid) {
|
|
@@ -3479,39 +3694,81 @@ export class AUNChannel {
|
|
|
3479
3694
|
// turn malformed `mentions: [undefined]` into an apparently valid `[]`.
|
|
3480
3695
|
const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
|
|
3481
3696
|
const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
|
|
3482
|
-
const
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3697
|
+
const expiresAt = opts.postSend?.type === 'register_interaction_card'
|
|
3698
|
+
? opts.postSend.expiresAt
|
|
3699
|
+
: undefined;
|
|
3700
|
+
if (typeof expiresAt === 'number' && Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
|
|
3701
|
+
return {
|
|
3702
|
+
status: 'permanent',
|
|
3703
|
+
error: 'interaction card has expired',
|
|
3704
|
+
code: 'INTERACTION_EXPIRED',
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
const remainingTtl = typeof expiresAt === 'number' && Number.isFinite(expiresAt)
|
|
3708
|
+
? Math.max(0, expiresAt - Date.now())
|
|
3709
|
+
: undefined;
|
|
3710
|
+
const requestedTtl = opts.ttl ?? outbox.defaultTtl(opts.queue);
|
|
3711
|
+
const ttl = remainingTtl === undefined ? opts.ttl : Math.min(requestedTtl, remainingTtl);
|
|
3712
|
+
let entry;
|
|
3713
|
+
try {
|
|
3714
|
+
entry = outbox.enqueue(this.config.aid, {
|
|
3715
|
+
queue: opts.queue,
|
|
3716
|
+
channelId,
|
|
3717
|
+
delivery,
|
|
3718
|
+
type: 'payload',
|
|
3719
|
+
contentKind: opts.contentKind,
|
|
3720
|
+
payload: finalPayload,
|
|
3721
|
+
context,
|
|
3722
|
+
logText,
|
|
3723
|
+
ttl,
|
|
3724
|
+
postSend: opts.postSend,
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
catch (error) {
|
|
3728
|
+
// Queue saturation is a caller-visible failure. Keep route/schema
|
|
3729
|
+
// validation errors as exceptions so their existing fail-closed path is
|
|
3730
|
+
// preserved, but expose OUTBOX_FULL as a structured send result.
|
|
3731
|
+
if (error?.code !== 'OUTBOX_FULL')
|
|
3732
|
+
throw error;
|
|
3733
|
+
return {
|
|
3734
|
+
status: 'permanent',
|
|
3735
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3736
|
+
code: 'OUTBOX_FULL',
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
logger.debug(`${this.logPrefix()} Outbox enqueued payload: id=${entry.id} queue=${opts.queue ?? 'default'} kind=${opts.contentKind} channel=${channelId} text=${logText.slice(0, 40)}`);
|
|
3494
3740
|
if (!this.connected || !this.client) {
|
|
3495
3741
|
logger.warn(`${this.logPrefix()} Not connected, payload queued in outbox (id=${entry.id}, kind=${opts.contentKind}). Triggering reconnect.`);
|
|
3496
3742
|
if (!this.reconnectTimer && !this.client) {
|
|
3497
3743
|
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendContentPayload failed: ${e}`));
|
|
3498
3744
|
}
|
|
3499
|
-
return {
|
|
3745
|
+
return {
|
|
3746
|
+
queued: true,
|
|
3747
|
+
outboxId: entry.id,
|
|
3748
|
+
error: 'AUN channel is not connected',
|
|
3749
|
+
code: 'AUN_NOT_CONNECTED',
|
|
3750
|
+
};
|
|
3500
3751
|
}
|
|
3501
3752
|
const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
|
|
3502
3753
|
if (result.ok) {
|
|
3503
3754
|
this.removeDeliveredOutboxEntry(entry);
|
|
3504
3755
|
return { messageId: result.messageId };
|
|
3505
3756
|
}
|
|
3506
|
-
if (result.status === 'permanent') {
|
|
3757
|
+
if (result.status === 'permanent' || result.status === 'failed') {
|
|
3507
3758
|
this.markPermanentOutboxFailure(entry, result);
|
|
3508
3759
|
return {
|
|
3509
3760
|
status: 'permanent',
|
|
3761
|
+
outboxId: entry.id,
|
|
3510
3762
|
...(result.error !== undefined ? { error: result.error } : {}),
|
|
3511
3763
|
...(result.code !== undefined ? { code: result.code } : {}),
|
|
3512
3764
|
};
|
|
3513
3765
|
}
|
|
3514
|
-
return {
|
|
3766
|
+
return {
|
|
3767
|
+
queued: true,
|
|
3768
|
+
outboxId: entry.id,
|
|
3769
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
3770
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
3771
|
+
};
|
|
3515
3772
|
}
|
|
3516
3773
|
buildTaskPayloadBase(envelope, context) {
|
|
3517
3774
|
const base = {};
|
|
@@ -3619,9 +3876,15 @@ export class AUNChannel {
|
|
|
3619
3876
|
return truncated;
|
|
3620
3877
|
}
|
|
3621
3878
|
buildActivityPayload(envelope, context, item) {
|
|
3622
|
-
const
|
|
3879
|
+
const rawItem = item && typeof item === 'object' && !Array.isArray(item)
|
|
3623
3880
|
? { ...item }
|
|
3624
3881
|
: { kind: 'unknown', text: String(item ?? '') };
|
|
3882
|
+
const displayArguments = rawItem.kind === 'tool_call'
|
|
3883
|
+
? toolInputForDisplay(rawItem.name, rawItem.arguments)
|
|
3884
|
+
: undefined;
|
|
3885
|
+
const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
|
|
3886
|
+
? { ...rawItem, arguments: displayArguments }
|
|
3887
|
+
: rawItem;
|
|
3625
3888
|
return {
|
|
3626
3889
|
...this.buildTaskPayloadBase(envelope, context),
|
|
3627
3890
|
type: 'activity',
|
|
@@ -3629,16 +3892,22 @@ export class AUNChannel {
|
|
|
3629
3892
|
};
|
|
3630
3893
|
}
|
|
3631
3894
|
async sendReliableStructured(channelId, payload, context, logText) {
|
|
3632
|
-
await this.sendContentPayload(channelId, payload, {
|
|
3895
|
+
const result = await this.sendContentPayload(channelId, payload, {
|
|
3633
3896
|
contentKind: 'custom',
|
|
3634
3897
|
context,
|
|
3635
3898
|
logText: logText ?? this.payloadLogText(payload, 'custom'),
|
|
3636
3899
|
});
|
|
3900
|
+
if (result.status === 'permanent' || result.status === 'failed') {
|
|
3901
|
+
throw Object.assign(new Error(result.error ?? 'AUN structured send failed'), {
|
|
3902
|
+
code: result.code ?? 'AUN_SEND_FAILED',
|
|
3903
|
+
outboxId: result.outboxId,
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3637
3906
|
}
|
|
3638
3907
|
async sendMessage(channelId, text, context) {
|
|
3639
3908
|
if (!text?.trim()) {
|
|
3640
3909
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
3641
|
-
return;
|
|
3910
|
+
return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
|
|
3642
3911
|
}
|
|
3643
3912
|
const delivery = this.requireDelivery(channelId, context);
|
|
3644
3913
|
const routedContext = this.withDelivery(context, delivery);
|
|
@@ -3676,13 +3945,14 @@ export class AUNChannel {
|
|
|
3676
3945
|
: undefined;
|
|
3677
3946
|
if (operationId) {
|
|
3678
3947
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
3679
|
-
|
|
3948
|
+
const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
|
|
3949
|
+
if (completedMessageId) {
|
|
3680
3950
|
const duplicate = outbox.findByDedupeKey(this.config.aid, operationId);
|
|
3681
3951
|
if (duplicate) {
|
|
3682
3952
|
outbox.remove(this.config.aid, duplicate.id);
|
|
3683
3953
|
logger.info(`${this.logPrefix()} Removed stale outbox entry for completed operation: ${operationId}`);
|
|
3684
3954
|
}
|
|
3685
|
-
return;
|
|
3955
|
+
return { status: 'sent', messageId: completedMessageId };
|
|
3686
3956
|
}
|
|
3687
3957
|
}
|
|
3688
3958
|
// Write-ahead: persist to outbox before attempting send
|
|
@@ -3698,9 +3968,19 @@ export class AUNChannel {
|
|
|
3698
3968
|
? routedContext.metadata.outboxTtl
|
|
3699
3969
|
: undefined,
|
|
3700
3970
|
});
|
|
3971
|
+
if (entry.deliveryResult?.messageId || entry.deliveryReceipt?.messageId) {
|
|
3972
|
+
const messageId = entry.deliveryResult?.messageId ?? entry.deliveryReceipt.messageId;
|
|
3973
|
+
logger.info(`${this.logPrefix()} Reusing completed durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} mid=${messageId}`);
|
|
3974
|
+
return { status: 'sent', messageId };
|
|
3975
|
+
}
|
|
3701
3976
|
if (entry.terminal) {
|
|
3702
3977
|
logger.warn(`${this.logPrefix()} Skipping previously terminated durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} code=${entry.lastErrorCode ?? 'unknown'}`);
|
|
3703
|
-
return
|
|
3978
|
+
return {
|
|
3979
|
+
status: 'failed',
|
|
3980
|
+
outboxId: entry.id,
|
|
3981
|
+
error: entry.lastError ?? entry.terminal.error,
|
|
3982
|
+
...(entry.lastErrorCode !== undefined ? { code: entry.lastErrorCode } : {}),
|
|
3983
|
+
};
|
|
3704
3984
|
}
|
|
3705
3985
|
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
3706
3986
|
// 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
|
|
@@ -3713,16 +3993,40 @@ export class AUNChannel {
|
|
|
3713
3993
|
if (!this.reconnectTimer && !this.client) {
|
|
3714
3994
|
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendMessage failed: ${e}`));
|
|
3715
3995
|
}
|
|
3716
|
-
return;
|
|
3996
|
+
return { status: 'queued', outboxId: entry.id, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
|
|
3717
3997
|
}
|
|
3718
3998
|
// Attempt immediate delivery
|
|
3719
3999
|
const result = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
3720
4000
|
if (result.status === 'sent') {
|
|
4001
|
+
const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
|
|
4002
|
+
if (!messageId) {
|
|
4003
|
+
const error = 'AUN send completed without a remote message_id';
|
|
4004
|
+
this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
|
|
4005
|
+
return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
|
|
4006
|
+
}
|
|
3721
4007
|
this.removeDeliveredOutboxEntry(entry);
|
|
4008
|
+
return { status: 'sent', messageId };
|
|
3722
4009
|
}
|
|
3723
4010
|
else if (result.status === 'permanent') {
|
|
3724
4011
|
this.markPermanentOutboxFailure(entry, result);
|
|
4012
|
+
return {
|
|
4013
|
+
status: 'failed',
|
|
4014
|
+
outboxId: entry.id,
|
|
4015
|
+
error: result.error ?? 'permanent AUN send failure',
|
|
4016
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
4017
|
+
};
|
|
3725
4018
|
}
|
|
4019
|
+
// A gateway receipt may already have been checkpointed even if local
|
|
4020
|
+
// post-send bookkeeping needs a retry. Remote acceptance still wins.
|
|
4021
|
+
if (entry.deliveryReceipt?.messageId) {
|
|
4022
|
+
return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
|
|
4023
|
+
}
|
|
4024
|
+
return {
|
|
4025
|
+
status: 'queued',
|
|
4026
|
+
outboxId: entry.id,
|
|
4027
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
4028
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
4029
|
+
};
|
|
3726
4030
|
}
|
|
3727
4031
|
/** Daemon-side transport for `ec msg send` running inside an agent task. */
|
|
3728
4032
|
async sendDaemonMsg(args) {
|
|
@@ -4005,9 +4309,10 @@ export class AUNChannel {
|
|
|
4005
4309
|
|| (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
|
|
4006
4310
|
if (operationId) {
|
|
4007
4311
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
4008
|
-
|
|
4312
|
+
const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
|
|
4313
|
+
if (completedMessageId) {
|
|
4009
4314
|
logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
|
|
4010
|
-
return sentOutboxResult();
|
|
4315
|
+
return sentOutboxResult(completedMessageId);
|
|
4011
4316
|
}
|
|
4012
4317
|
if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
|
|
4013
4318
|
const agentConfig = loadAgent(this.config.aid);
|
|
@@ -4054,7 +4359,7 @@ export class AUNChannel {
|
|
|
4054
4359
|
source,
|
|
4055
4360
|
transport: entry.deliveryReceipt.transport,
|
|
4056
4361
|
});
|
|
4057
|
-
return sentOutboxResult();
|
|
4362
|
+
return sentOutboxResult(entry.deliveryReceipt.messageId);
|
|
4058
4363
|
}
|
|
4059
4364
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
4060
4365
|
const encrypt = context?.metadata?.encrypted != null
|
|
@@ -4063,6 +4368,7 @@ export class AUNChannel {
|
|
|
4063
4368
|
const params = { payload, encrypt };
|
|
4064
4369
|
if (context?.metadata?.persistRequired === true)
|
|
4065
4370
|
params.persist_required = true;
|
|
4371
|
+
let acceptedMessageId;
|
|
4066
4372
|
try {
|
|
4067
4373
|
if (isGroup) {
|
|
4068
4374
|
params.group_id = channelId;
|
|
@@ -4082,6 +4388,7 @@ export class AUNChannel {
|
|
|
4082
4388
|
return failure;
|
|
4083
4389
|
}
|
|
4084
4390
|
else {
|
|
4391
|
+
acceptedMessageId = mid;
|
|
4085
4392
|
this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
|
|
4086
4393
|
this.checkpointTextDelivery(entry, mid, encrypt, result);
|
|
4087
4394
|
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 });
|
|
@@ -4104,6 +4411,7 @@ export class AUNChannel {
|
|
|
4104
4411
|
return classifyAunSendFailure(result, 'message.send returned no message_id');
|
|
4105
4412
|
}
|
|
4106
4413
|
else {
|
|
4414
|
+
acceptedMessageId = mid;
|
|
4107
4415
|
this.logAunSendAccepted('message.send', this.peerLabel(targetAid), mid, encrypt, result, finalText);
|
|
4108
4416
|
this.checkpointTextDelivery(entry, mid, encrypt, result);
|
|
4109
4417
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
@@ -4124,7 +4432,7 @@ export class AUNChannel {
|
|
|
4124
4432
|
this.forwardOutbound(result);
|
|
4125
4433
|
}
|
|
4126
4434
|
}
|
|
4127
|
-
return sentOutboxResult();
|
|
4435
|
+
return sentOutboxResult(acceptedMessageId);
|
|
4128
4436
|
}
|
|
4129
4437
|
catch (e) {
|
|
4130
4438
|
if (entry.deliveryReceipt) {
|
|
@@ -4139,7 +4447,7 @@ export class AUNChannel {
|
|
|
4139
4447
|
try {
|
|
4140
4448
|
if (isGroup) {
|
|
4141
4449
|
this.trace('OUT', 'group.send.fallback', params);
|
|
4142
|
-
const result = await this.
|
|
4450
|
+
const result = await this.callClient('group.send', params);
|
|
4143
4451
|
const mid = this.messageIdFromSendResult(result);
|
|
4144
4452
|
if (!mid) {
|
|
4145
4453
|
const resultRecord = errorRecord(result);
|
|
@@ -4148,6 +4456,7 @@ export class AUNChannel {
|
|
|
4148
4456
|
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
4149
4457
|
return classifyAunSendFailure(result, 'group.send plaintext fallback returned no message_id');
|
|
4150
4458
|
}
|
|
4459
|
+
acceptedMessageId = mid;
|
|
4151
4460
|
this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
|
|
4152
4461
|
this.checkpointTextDelivery(entry, mid, false, result);
|
|
4153
4462
|
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 });
|
|
@@ -4161,13 +4470,14 @@ export class AUNChannel {
|
|
|
4161
4470
|
}
|
|
4162
4471
|
else {
|
|
4163
4472
|
this.trace('OUT', 'message.send.fallback', params);
|
|
4164
|
-
const result = await this.
|
|
4473
|
+
const result = await this.callClient('message.send', params);
|
|
4165
4474
|
const mid = this.messageIdFromSendResult(result);
|
|
4166
4475
|
if (!mid) {
|
|
4167
4476
|
this.trace('OUT', 'message.send.fallback.missing_id', {});
|
|
4168
4477
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
4169
4478
|
return classifyAunSendFailure(result, 'message.send plaintext fallback returned no message_id');
|
|
4170
4479
|
}
|
|
4480
|
+
acceptedMessageId = mid;
|
|
4171
4481
|
this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
|
|
4172
4482
|
this.checkpointTextDelivery(entry, mid, false, result);
|
|
4173
4483
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
@@ -4186,7 +4496,7 @@ export class AUNChannel {
|
|
|
4186
4496
|
});
|
|
4187
4497
|
this.forwardOutbound(result);
|
|
4188
4498
|
}
|
|
4189
|
-
return sentOutboxResult();
|
|
4499
|
+
return sentOutboxResult(acceptedMessageId);
|
|
4190
4500
|
}
|
|
4191
4501
|
catch (e2) {
|
|
4192
4502
|
if (entry.deliveryReceipt) {
|
|
@@ -4214,6 +4524,7 @@ export class AUNChannel {
|
|
|
4214
4524
|
id: entry.id,
|
|
4215
4525
|
channelId: entry.channelId,
|
|
4216
4526
|
delivery: entry.delivery,
|
|
4527
|
+
queue: entry.queue,
|
|
4217
4528
|
};
|
|
4218
4529
|
const receipt = {
|
|
4219
4530
|
messageId,
|
|
@@ -4238,6 +4549,20 @@ export class AUNChannel {
|
|
|
4238
4549
|
logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
|
|
4239
4550
|
return { ok: true, status: 'sent' };
|
|
4240
4551
|
}
|
|
4552
|
+
const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
|
|
4553
|
+
? entry.postSend.expiresAt
|
|
4554
|
+
: undefined;
|
|
4555
|
+
if (typeof interactionExpiresAt === 'number'
|
|
4556
|
+
&& Number.isFinite(interactionExpiresAt)
|
|
4557
|
+
&& interactionExpiresAt <= Date.now()) {
|
|
4558
|
+
logger.info(`${this.logPrefix()} Discarded expired interaction from durable outbox: request=${interactionId ?? '<unknown>'} entry=${entry.id}`);
|
|
4559
|
+
return {
|
|
4560
|
+
ok: false,
|
|
4561
|
+
status: 'permanent',
|
|
4562
|
+
error: 'interaction card has expired',
|
|
4563
|
+
code: 'INTERACTION_EXPIRED',
|
|
4564
|
+
};
|
|
4565
|
+
}
|
|
4241
4566
|
const channelId = entry.channelId;
|
|
4242
4567
|
const payload = entry.payload;
|
|
4243
4568
|
if (!payload) {
|
|
@@ -4696,7 +5021,7 @@ export class AUNChannel {
|
|
|
4696
5021
|
return { status: 'permanent', error: 'image outbox entry disappeared during upload', code: 'OUTBOX_ENTRY_MISSING' };
|
|
4697
5022
|
}
|
|
4698
5023
|
const sent = await this.deliverPayloadEntry(entry);
|
|
4699
|
-
return sent.ok ? sentOutboxResult() : sent;
|
|
5024
|
+
return sent.ok ? sentOutboxResult(sent.messageId) : sent;
|
|
4700
5025
|
}
|
|
4701
5026
|
catch (error) {
|
|
4702
5027
|
const failure = classifyAunSendFailure(error, 'image upload or send failed');
|
|
@@ -4728,7 +5053,7 @@ export class AUNChannel {
|
|
|
4728
5053
|
source,
|
|
4729
5054
|
transport: entry.deliveryReceipt.transport,
|
|
4730
5055
|
});
|
|
4731
|
-
return sentOutboxResult();
|
|
5056
|
+
return sentOutboxResult(entry.deliveryReceipt.messageId);
|
|
4732
5057
|
}
|
|
4733
5058
|
catch (error) {
|
|
4734
5059
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -4805,7 +5130,7 @@ export class AUNChannel {
|
|
|
4805
5130
|
if (isGroup) {
|
|
4806
5131
|
params.group_id = delivery.groupId;
|
|
4807
5132
|
this.trace('OUT', 'group.send.file', params);
|
|
4808
|
-
const result = await this.
|
|
5133
|
+
const result = await this.callClient('group.send', params);
|
|
4809
5134
|
sendResult = result;
|
|
4810
5135
|
const fileMid = this.messageIdFromSendResult(result);
|
|
4811
5136
|
sentMid = fileMid ?? null;
|
|
@@ -4818,7 +5143,7 @@ export class AUNChannel {
|
|
|
4818
5143
|
else {
|
|
4819
5144
|
params.to = fileTargetAid;
|
|
4820
5145
|
this.trace('OUT', 'message.send.file', params);
|
|
4821
|
-
const result = await this.
|
|
5146
|
+
const result = await this.callClient('message.send', params);
|
|
4822
5147
|
sendResult = result;
|
|
4823
5148
|
sentMid = this.messageIdFromSendResult(result);
|
|
4824
5149
|
this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
|
|
@@ -4839,7 +5164,7 @@ export class AUNChannel {
|
|
|
4839
5164
|
params.encrypt = false;
|
|
4840
5165
|
if (isGroup) {
|
|
4841
5166
|
this.trace('OUT', 'group.send.file.fallback', params);
|
|
4842
|
-
const result = await this.
|
|
5167
|
+
const result = await this.callClient('group.send', params);
|
|
4843
5168
|
sendResult = result;
|
|
4844
5169
|
const fbMid = this.messageIdFromSendResult(result);
|
|
4845
5170
|
sentMid = fbMid ?? null;
|
|
@@ -4851,7 +5176,7 @@ export class AUNChannel {
|
|
|
4851
5176
|
}
|
|
4852
5177
|
else {
|
|
4853
5178
|
this.trace('OUT', 'message.send.file.fallback', params);
|
|
4854
|
-
const result = await this.
|
|
5179
|
+
const result = await this.callClient('message.send', params);
|
|
4855
5180
|
sendResult = result;
|
|
4856
5181
|
sentMid = this.messageIdFromSendResult(result);
|
|
4857
5182
|
this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
|
|
@@ -4887,7 +5212,7 @@ export class AUNChannel {
|
|
|
4887
5212
|
});
|
|
4888
5213
|
if (sendResult)
|
|
4889
5214
|
this.forwardOutbound(sendResult);
|
|
4890
|
-
return sentOutboxResult();
|
|
5215
|
+
return sentOutboxResult(sentMid);
|
|
4891
5216
|
}
|
|
4892
5217
|
catch (e) {
|
|
4893
5218
|
if (entry.deliveryReceipt) {
|
|
@@ -4907,7 +5232,7 @@ export class AUNChannel {
|
|
|
4907
5232
|
if (this.outboxTimer)
|
|
4908
5233
|
return;
|
|
4909
5234
|
this.outboxTimer = setInterval(() => {
|
|
4910
|
-
if (this.connected && this.client && outbox.hasPending(this.config.aid)) {
|
|
5235
|
+
if (this.connected && this.client && (outbox.hasPending(this.config.aid) || outbox.hasPending(this.config.aid, 'activity'))) {
|
|
4911
5236
|
this.drainOutbox();
|
|
4912
5237
|
}
|
|
4913
5238
|
}, 30_000);
|
|
@@ -4922,36 +5247,42 @@ export class AUNChannel {
|
|
|
4922
5247
|
if (!this.connected || !this.client)
|
|
4923
5248
|
return;
|
|
4924
5249
|
this.repairBootstrapRoutes();
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
5250
|
+
const drainQueue = async (queue) => {
|
|
5251
|
+
if (!outbox.hasPending(this.config.aid, queue))
|
|
5252
|
+
return;
|
|
5253
|
+
logger.info(`${this.logPrefix()} Draining ${queue} outbox...`);
|
|
5254
|
+
const result = await outbox.drain(this.config.aid, async (entry) => {
|
|
5255
|
+
if (!isDeliveryTarget(entry.delivery)) {
|
|
5256
|
+
logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
|
|
5257
|
+
return {
|
|
5258
|
+
status: 'permanent',
|
|
5259
|
+
error: 'outbox entry has no valid delivery route',
|
|
5260
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
5261
|
+
};
|
|
5262
|
+
}
|
|
5263
|
+
if (entry.type === 'text') {
|
|
5264
|
+
return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
5265
|
+
}
|
|
5266
|
+
else if (entry.type === 'file') {
|
|
5267
|
+
return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
5268
|
+
}
|
|
5269
|
+
else if (entry.type === 'image') {
|
|
5270
|
+
return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
5271
|
+
}
|
|
5272
|
+
else if (entry.type === 'payload') {
|
|
5273
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
|
|
5274
|
+
return sent.ok
|
|
5275
|
+
? { status: 'sent', ...(sent.messageId ? { messageId: sent.messageId } : {}) }
|
|
5276
|
+
: sent;
|
|
5277
|
+
}
|
|
5278
|
+
return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
|
|
5279
|
+
}, queue);
|
|
5280
|
+
if (result.sent > 0 || result.expired > 0 || result.permanent) {
|
|
5281
|
+
logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
|
|
4949
5282
|
}
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
|
|
4954
|
-
}
|
|
5283
|
+
};
|
|
5284
|
+
await drainQueue('default');
|
|
5285
|
+
await drainQueue('activity');
|
|
4955
5286
|
}
|
|
4956
5287
|
/** Repair bootstrap outbox entries to the configured personal Owner route. */
|
|
4957
5288
|
repairBootstrapRoutes() {
|
|
@@ -4959,13 +5290,11 @@ export class AUNChannel {
|
|
|
4959
5290
|
const owner = getFirstStaticAgentOwner(aid);
|
|
4960
5291
|
if (!owner)
|
|
4961
5292
|
return;
|
|
4962
|
-
const
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
.map(operationId => outbox.findByDedupeKey(aid, operationId, { includeTerminal: true }))
|
|
4968
|
-
.filter((entry) => !!entry?.channelId);
|
|
5293
|
+
const completionEntry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid), { includeTerminal: true });
|
|
5294
|
+
const entries = [
|
|
5295
|
+
...outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid), { includeTerminal: true }),
|
|
5296
|
+
...(completionEntry ? [completionEntry] : []),
|
|
5297
|
+
].filter((entry) => !!entry?.channelId);
|
|
4969
5298
|
for (const entry of entries) {
|
|
4970
5299
|
const delivery = { chatType: 'private' };
|
|
4971
5300
|
const nestedDelivery = entry.context?.delivery;
|
|
@@ -5325,6 +5654,13 @@ export class AUNChannelPlugin {
|
|
|
5325
5654
|
const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
|
|
5326
5655
|
const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
|
|
5327
5656
|
const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
|
|
5657
|
+
const operationId = envelope.operationId ?? envelope.taskId;
|
|
5658
|
+
// A task can emit multiple independent intermediate text chunks. Keep
|
|
5659
|
+
// the task operation ID in the receipt, but do not use it as the
|
|
5660
|
+
// durable-send dedupe key for each non-final chunk.
|
|
5661
|
+
const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
|
|
5662
|
+
? undefined
|
|
5663
|
+
: operationId;
|
|
5328
5664
|
const replyCtx = outboundCausation
|
|
5329
5665
|
? {
|
|
5330
5666
|
...(envelope.replyContext ?? {}),
|
|
@@ -5345,11 +5681,61 @@ export class AUNChannelPlugin {
|
|
|
5345
5681
|
case 'result.text':
|
|
5346
5682
|
case 'command.result':
|
|
5347
5683
|
case 'command.error': {
|
|
5348
|
-
const sendCtx = {
|
|
5684
|
+
const sendCtx = {
|
|
5685
|
+
...(replyCtx ?? {}),
|
|
5686
|
+
metadata: {
|
|
5687
|
+
...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
|
|
5688
|
+
.filter(([key]) => key !== 'operationId')),
|
|
5689
|
+
...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
|
|
5690
|
+
},
|
|
5691
|
+
};
|
|
5349
5692
|
if (payload.kind === 'result.text' && payload.isFinal)
|
|
5350
5693
|
sendCtx.title = '✅ 最终回复:';
|
|
5351
|
-
|
|
5352
|
-
|
|
5694
|
+
let result;
|
|
5695
|
+
try {
|
|
5696
|
+
result = await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
5697
|
+
}
|
|
5698
|
+
catch (error) {
|
|
5699
|
+
if (error?.code !== 'OUTBOX_FULL')
|
|
5700
|
+
throw error;
|
|
5701
|
+
return {
|
|
5702
|
+
status: 'failed',
|
|
5703
|
+
operationId,
|
|
5704
|
+
messages: [],
|
|
5705
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5706
|
+
code: 'OUTBOX_FULL',
|
|
5707
|
+
};
|
|
5708
|
+
}
|
|
5709
|
+
if (result.status === 'sent') {
|
|
5710
|
+
return {
|
|
5711
|
+
status: 'sent',
|
|
5712
|
+
operationId,
|
|
5713
|
+
messageId: result.messageId,
|
|
5714
|
+
messages: [{
|
|
5715
|
+
messageId: result.messageId,
|
|
5716
|
+
partIndex: 0,
|
|
5717
|
+
...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
|
|
5718
|
+
}],
|
|
5719
|
+
};
|
|
5720
|
+
}
|
|
5721
|
+
if (result.status === 'queued') {
|
|
5722
|
+
return {
|
|
5723
|
+
status: 'queued',
|
|
5724
|
+
operationId,
|
|
5725
|
+
messages: [],
|
|
5726
|
+
outboxId: result.outboxId,
|
|
5727
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
5728
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5731
|
+
return {
|
|
5732
|
+
status: 'failed',
|
|
5733
|
+
operationId,
|
|
5734
|
+
messages: [],
|
|
5735
|
+
...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
|
|
5736
|
+
error: result.error,
|
|
5737
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5738
|
+
};
|
|
5353
5739
|
}
|
|
5354
5740
|
case 'system.notice': {
|
|
5355
5741
|
const noticePayload = {
|
|
@@ -5402,6 +5788,9 @@ export class AUNChannelPlugin {
|
|
|
5402
5788
|
}
|
|
5403
5789
|
case 'activity.batch': {
|
|
5404
5790
|
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
5791
|
+
const messageIds = [];
|
|
5792
|
+
let queuedResult;
|
|
5793
|
+
let failedResult;
|
|
5405
5794
|
for (const item of items) {
|
|
5406
5795
|
if (item?.kind === 'progress') {
|
|
5407
5796
|
const metadata = { activityType: 'progress' };
|
|
@@ -5417,10 +5806,51 @@ export class AUNChannelPlugin {
|
|
|
5417
5806
|
await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
|
|
5418
5807
|
}
|
|
5419
5808
|
else {
|
|
5420
|
-
await channel.
|
|
5809
|
+
const result = await channel.sendContentPayload(channelId, aunPayload, {
|
|
5810
|
+
queue: 'activity',
|
|
5811
|
+
contentKind: 'custom',
|
|
5812
|
+
context: replyCtx,
|
|
5813
|
+
logText: channel.activityLogText(item),
|
|
5814
|
+
});
|
|
5815
|
+
if (result.status === 'permanent' || result.status === 'failed')
|
|
5816
|
+
failedResult ??= result;
|
|
5817
|
+
else if (result.status === 'retry' || result.status === 'queued' || result.queued)
|
|
5818
|
+
queuedResult ??= result;
|
|
5819
|
+
else if (result.messageId)
|
|
5820
|
+
messageIds.push(result.messageId);
|
|
5421
5821
|
}
|
|
5422
5822
|
}
|
|
5423
|
-
|
|
5823
|
+
if (failedResult) {
|
|
5824
|
+
return {
|
|
5825
|
+
status: 'failed',
|
|
5826
|
+
operationId,
|
|
5827
|
+
messages: [],
|
|
5828
|
+
...(failedResult.outboxId !== undefined ? { outboxId: failedResult.outboxId } : {}),
|
|
5829
|
+
error: failedResult.error ?? 'AUN activity delivery failed',
|
|
5830
|
+
...(failedResult.code !== undefined ? { code: failedResult.code } : {}),
|
|
5831
|
+
};
|
|
5832
|
+
}
|
|
5833
|
+
if (queuedResult) {
|
|
5834
|
+
return queuedResult.outboxId
|
|
5835
|
+
? {
|
|
5836
|
+
status: 'queued',
|
|
5837
|
+
operationId,
|
|
5838
|
+
messages: [],
|
|
5839
|
+
outboxId: queuedResult.outboxId,
|
|
5840
|
+
...(queuedResult.error !== undefined ? { error: queuedResult.error } : {}),
|
|
5841
|
+
...(queuedResult.code !== undefined ? { code: queuedResult.code } : {}),
|
|
5842
|
+
}
|
|
5843
|
+
: {
|
|
5844
|
+
status: 'failed',
|
|
5845
|
+
operationId,
|
|
5846
|
+
messages: [],
|
|
5847
|
+
error: 'queued activity is missing its outbox id',
|
|
5848
|
+
code: 'MISSING_OUTBOX_ID',
|
|
5849
|
+
};
|
|
5850
|
+
}
|
|
5851
|
+
if (messageIds.length > 0)
|
|
5852
|
+
return sentReceipt(envelope, messageIds, replyCtx?.threadId);
|
|
5853
|
+
return suppressedReceipt(envelope, 'empty_activity');
|
|
5424
5854
|
}
|
|
5425
5855
|
case 'status.progress':
|
|
5426
5856
|
channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
@@ -5449,6 +5879,44 @@ export class AUNChannelPlugin {
|
|
|
5449
5879
|
case 'interaction': {
|
|
5450
5880
|
const req = payload.interaction;
|
|
5451
5881
|
const cardTtlMs = AUN_INTERACTION_CARD_TTL_MS;
|
|
5882
|
+
const cardExpiresAt = typeof req.expiresAt === 'number' && Number.isFinite(req.expiresAt)
|
|
5883
|
+
? req.expiresAt
|
|
5884
|
+
: Date.now() + cardTtlMs;
|
|
5885
|
+
const toInteractionReceipt = (result) => {
|
|
5886
|
+
if ((result.status === undefined || result.status === 'sent') && result.messageId) {
|
|
5887
|
+
return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
|
|
5888
|
+
}
|
|
5889
|
+
if (result.status === 'retry' || result.status === 'queued' || result.queued) {
|
|
5890
|
+
if (!result.outboxId) {
|
|
5891
|
+
return {
|
|
5892
|
+
status: 'failed',
|
|
5893
|
+
operationId,
|
|
5894
|
+
messages: [],
|
|
5895
|
+
error: 'queued interaction is missing its outbox id',
|
|
5896
|
+
code: 'MISSING_OUTBOX_ID',
|
|
5897
|
+
};
|
|
5898
|
+
}
|
|
5899
|
+
return {
|
|
5900
|
+
status: 'queued',
|
|
5901
|
+
operationId,
|
|
5902
|
+
messages: [],
|
|
5903
|
+
outboxId: result.outboxId,
|
|
5904
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
5905
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5906
|
+
};
|
|
5907
|
+
}
|
|
5908
|
+
if (result.status === 'permanent' || result.status === 'failed') {
|
|
5909
|
+
return {
|
|
5910
|
+
status: 'failed',
|
|
5911
|
+
operationId,
|
|
5912
|
+
messages: [],
|
|
5913
|
+
...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
|
|
5914
|
+
error: result.error ?? 'AUN interaction delivery failed',
|
|
5915
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5916
|
+
};
|
|
5917
|
+
}
|
|
5918
|
+
return suppressedReceipt(envelope, 'interaction_not_sent');
|
|
5919
|
+
};
|
|
5452
5920
|
if (req.kind.kind === 'action') {
|
|
5453
5921
|
const action = req.kind;
|
|
5454
5922
|
const aunCard = {
|
|
@@ -5472,7 +5940,7 @@ export class AUNChannelPlugin {
|
|
|
5472
5940
|
aunCard.initiator = req.initiatorId;
|
|
5473
5941
|
if (replyCtx?.threadId)
|
|
5474
5942
|
aunCard.thread_id = replyCtx.threadId;
|
|
5475
|
-
await channel.sendContentPayload(channelId, aunCard, {
|
|
5943
|
+
const result = await channel.sendContentPayload(channelId, aunCard, {
|
|
5476
5944
|
contentKind: 'card',
|
|
5477
5945
|
context: replyCtx,
|
|
5478
5946
|
logText: action.title ? `[card] ${action.title}` : '[card]',
|
|
@@ -5482,9 +5950,10 @@ export class AUNChannelPlugin {
|
|
|
5482
5950
|
isCommandCard: false,
|
|
5483
5951
|
initiatorAid: req.initiatorId,
|
|
5484
5952
|
delivery: replyCtx?.delivery,
|
|
5485
|
-
expiresAt:
|
|
5953
|
+
expiresAt: cardExpiresAt,
|
|
5486
5954
|
},
|
|
5487
5955
|
});
|
|
5956
|
+
return toInteractionReceipt(result);
|
|
5488
5957
|
}
|
|
5489
5958
|
else if (req.kind.kind === 'command-card') {
|
|
5490
5959
|
const card = req.kind;
|
|
@@ -5510,7 +5979,7 @@ export class AUNChannelPlugin {
|
|
|
5510
5979
|
aunCard.initiator = req.initiatorId;
|
|
5511
5980
|
if (replyCtx?.threadId)
|
|
5512
5981
|
aunCard.thread_id = replyCtx.threadId;
|
|
5513
|
-
await channel.sendContentPayload(channelId, aunCard, {
|
|
5982
|
+
const result = await channel.sendContentPayload(channelId, aunCard, {
|
|
5514
5983
|
contentKind: 'card',
|
|
5515
5984
|
context: replyCtx,
|
|
5516
5985
|
logText: card.title ? `[card] ${card.title}` : '[card]',
|
|
@@ -5520,14 +5989,35 @@ export class AUNChannelPlugin {
|
|
|
5520
5989
|
isCommandCard: true,
|
|
5521
5990
|
initiatorAid: req.initiatorId,
|
|
5522
5991
|
delivery: replyCtx?.delivery,
|
|
5523
|
-
expiresAt:
|
|
5992
|
+
expiresAt: cardExpiresAt,
|
|
5524
5993
|
},
|
|
5525
5994
|
});
|
|
5995
|
+
return toInteractionReceipt(result);
|
|
5526
5996
|
}
|
|
5527
5997
|
else if (payload.fallbackText) {
|
|
5528
|
-
await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
|
|
5998
|
+
const result = await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
|
|
5999
|
+
if (result.status === 'sent')
|
|
6000
|
+
return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
|
|
6001
|
+
if (result.status === 'queued') {
|
|
6002
|
+
return {
|
|
6003
|
+
status: 'queued',
|
|
6004
|
+
operationId,
|
|
6005
|
+
messages: [],
|
|
6006
|
+
outboxId: result.outboxId,
|
|
6007
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
6008
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
6011
|
+
return {
|
|
6012
|
+
status: 'failed',
|
|
6013
|
+
operationId,
|
|
6014
|
+
messages: [],
|
|
6015
|
+
...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
|
|
6016
|
+
error: result.error,
|
|
6017
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
6018
|
+
};
|
|
5529
6019
|
}
|
|
5530
|
-
return;
|
|
6020
|
+
return suppressedReceipt(envelope, 'empty_interaction');
|
|
5531
6021
|
}
|
|
5532
6022
|
case 'custom': {
|
|
5533
6023
|
const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
|
|
@@ -5570,7 +6060,7 @@ export class AUNChannelPlugin {
|
|
|
5570
6060
|
registerBridge(bridge, channelType) {
|
|
5571
6061
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
5572
6062
|
handler(aunOptsToInbound(opts, adapter.channelName, channelType));
|
|
5573
|
-
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
|
|
6063
|
+
}), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
|
|
5574
6064
|
},
|
|
5575
6065
|
registerHooks(hookCtx) {
|
|
5576
6066
|
channel.setEventBus(hookCtx.eventBus);
|
|
@@ -5594,6 +6084,11 @@ export class AUNChannelPlugin {
|
|
|
5594
6084
|
}, { cache: true }).mentionMode;
|
|
5595
6085
|
});
|
|
5596
6086
|
}
|
|
6087
|
+
if (typeof channel.setGroupSlashRequireMentionsResolver === 'function') {
|
|
6088
|
+
channel.setGroupSlashRequireMentionsResolver(async () => {
|
|
6089
|
+
return resolveEffective({ self: aid }, { cache: true }).groupSlashRequireMentions ?? true;
|
|
6090
|
+
});
|
|
6091
|
+
}
|
|
5597
6092
|
},
|
|
5598
6093
|
};
|
|
5599
6094
|
}
|