evolcore 0.0.26 → 0.0.27
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 +25 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +63 -26
- package/dist/agents/codex-runner.js +316 -411
- package/dist/agents/ecagent-runner.js +170 -25
- package/dist/agents/gemini-runner.js +6 -0
- package/dist/agents/runner-types.js +50 -2
- package/dist/aun/aid/agentmd.js +73 -31
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +3 -3
- package/dist/aun/group/history.js +303 -0
- package/dist/aun/group/identity.js +43 -0
- package/dist/aun/group/index.js +5 -0
- package/dist/aun/{msg/group.js → group/operations.js} +161 -38
- package/dist/aun/{menu-client.js → menu/client.js} +12 -7
- package/dist/aun/menu/index.js +2 -0
- package/dist/aun/menu/transport.js +49 -0
- package/dist/aun/msg/index.js +2 -1
- package/dist/aun/msg/managed-operation.js +84 -18
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +13 -8
- package/dist/aun/msg/upload.js +43 -88
- package/dist/aun/outbox.js +29 -7
- package/dist/aun/rpc/client.js +1 -0
- package/dist/aun/storage/client.js +39 -50
- package/dist/channels/aun.js +522 -233
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +108 -6
- package/dist/cli/agent-create.js +515 -2
- package/dist/cli/agent.js +109 -8
- package/dist/cli/aun-commands.js +191 -42
- package/dist/cli/config.js +8 -5
- package/dist/cli/contact.js +2 -2
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +35 -23
- package/dist/cli/diag.js +2 -2
- package/dist/cli/fs-command.js +3 -3
- package/dist/cli/handoff-command.js +6 -6
- package/dist/cli/index.js +8 -2
- package/dist/cli/init.js +34 -1
- package/dist/cli/managed-command-guard.js +2 -1
- package/dist/cli/menu-client.js +9 -5
- package/dist/cli/model.js +3 -3
- package/dist/cli/queue-command.js +2 -2
- package/dist/cli/response.js +2 -2
- package/dist/cli/restart-monitor.js +16 -2
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +20 -10
- package/dist/config/builtin-roles.js +10 -0
- package/dist/config/config-field-policy.js +11 -0
- package/dist/config/config-manager.js +32 -4
- package/dist/config/config-operation-service.js +20 -5
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +1 -1
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +5 -0
- package/dist/core/audit/event-key.js +17 -2
- package/dist/core/audit/log-integrity.js +34 -8
- package/dist/core/auth/agent-delegation.js +48 -14
- package/dist/core/auth/auth-gateway.js +2 -0
- package/dist/core/auth/authorization-audit.js +34 -38
- package/dist/core/auth/operation-authorizer.js +67 -5
- package/dist/core/auth/operation-catalog.js +110 -0
- package/dist/core/auth/remote-access-store.js +115 -9
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +110 -19
- package/dist/core/command/agent-control.js +88 -7
- package/dist/core/command/agent-storage-menu.js +443 -0
- package/dist/core/command/cli-intent-parser.js +19 -0
- package/dist/core/command/command-handler.js +76 -39
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +34 -4
- package/dist/core/command/menu-handler.js +348 -77
- package/dist/core/command/menu-protocol.js +40 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +37 -22
- package/dist/core/event-catalog.js +4 -0
- package/dist/core/evolagent-registry.js +28 -2
- package/dist/core/group/invite-service.js +924 -0
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +76 -15
- package/dist/core/message/message-bridge.js +94 -11
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +73 -41
- package/dist/core/message/response-engine.js +709 -117
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +61 -29
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +72 -3
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +7 -7
- package/dist/core/relation/peer-identity.js +57 -6
- package/dist/core/relation/shared-data.js +106 -0
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/runtime-lock.js +10 -26
- package/dist/core/session/runner-task-history.js +170 -0
- package/dist/core/session/session-manager.js +34 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +308 -54
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -0
- package/dist/trigger/event-source.js +19 -0
- package/dist/trigger/feedback.js +233 -40
- package/dist/trigger/parser.js +55 -5
- package/dist/trigger/patch.js +9 -0
- package/dist/trigger/scheduler.js +30 -4
- package/dist/trigger/validation.js +20 -3
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/INDEX.md +0 -6
- package/kits/docs/channels/aun.md +15 -2
- package/kits/docs/channels/feishu.md +1 -1
- package/kits/docs/context-assembly.md +29 -17
- package/kits/docs/evolcore/INDEX.md +8 -3
- package/kits/docs/evolcore/agent-create.md +39 -7
- package/kits/docs/evolcore/agent.md +33 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/config.md +40 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +117 -2
- package/kits/docs/evolcore/menu-client.md +16 -0
- package/kits/docs/evolcore/msg.md +3 -0
- package/kits/docs/evolcore/storage.md +3 -0
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +51 -3
- package/kits/docs/venues/group.md +5 -0
- package/kits/eck_manifest.json +27 -0
- package/kits/rules/01-overview.md +11 -7
- package/kits/rules/04-relation.md +2 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +6 -4
- package/kits/schemas/agent-config.schema.12.json +25 -1
- package/kits/schemas/daemon.schema.6.json +2 -2
- package/kits/schemas/defaults.schema.5.json +1 -1
- package/kits/schemas/relation-config.schema.1.json +1 -1
- package/kits/schemas/relation-config.schema.5.json +1 -1
- package/kits/schemas/role-config.schema.1.json +14 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/baseagent.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +26 -9
- package/kits/templates/system-fragments/channel.md +8 -44
- package/kits/templates/system-fragments/commands.md +10 -2
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/reply.md +56 -0
- package/package.json +7 -3
- package/dist/aun/group-identity.js +0 -9
- /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
- /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
package/dist/channels/aun.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { E2EEError } from '@agentunion/fastaun';
|
|
1
|
+
import { E2EEError, validateAIDFormat } from '@agentunion/fastaun';
|
|
2
|
+
import { sanitizeMenuLogValue } from '../core/command/menu-protocol.js';
|
|
2
3
|
import crypto from 'crypto';
|
|
3
4
|
import { performance } from 'node:perf_hooks';
|
|
4
5
|
import fs from 'fs';
|
|
@@ -15,6 +16,7 @@ import { appendAidEvent } from '../utils/instance-registry.js';
|
|
|
15
16
|
import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
|
|
16
17
|
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
17
18
|
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
19
|
+
import { readTopicLable } from '../core/session/session-mapper.js';
|
|
18
20
|
import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
|
|
19
21
|
import { appendAidLifecycle } from '../aun/aid/identity.js';
|
|
20
22
|
import { enableFullGroupPullPagination, enableReliableMessageDelivery, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
|
|
@@ -26,19 +28,20 @@ import { isManagementRole } from '../config/builtin-roles.js';
|
|
|
26
28
|
import { normalizeMentionMode } from '../config/mention-mode.js';
|
|
27
29
|
import { getProcessStartTime } from '../utils/process-introspect.js';
|
|
28
30
|
import * as outbox from '../aun/outbox.js';
|
|
29
|
-
import {
|
|
31
|
+
import { formatSize } from '../utils/media-cache.js';
|
|
30
32
|
import { formatPeerKey, PeerIdentityCache } from '../core/relation/peer-identity.js';
|
|
31
33
|
import { isHClassPath } from '../core/permission/protected-paths.js';
|
|
32
34
|
import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
|
|
33
35
|
import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
|
|
34
36
|
import { recordCausationSpan } from '../core/causation/audit.js';
|
|
35
|
-
import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
37
|
+
import { invalidateAgentDisplayName, refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
38
|
+
import { buildControlAgentName, ensureControlAgentMd } from '../aun/aid/control-aid.js';
|
|
36
39
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
37
|
-
import { ManagedMenuClient } from '../aun/menu
|
|
40
|
+
import { ManagedMenuClient, menuSenderDeviceId, menuTransportParams, } from '../aun/menu/index.js';
|
|
38
41
|
import { toolInputForDisplay } from '../utils/tool-summary.js';
|
|
39
|
-
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, pendingPostBootstrapWelcomeOwners, isPostBootstrapWelcomeOperationId, findPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
42
|
+
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, pendingPostBootstrapWelcomeOwners, isPostBootstrapWelcomeOperationId, findPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, postBootstrapWelcomeStatus, } from '../core/bootstrap-messages.js';
|
|
40
43
|
import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
|
|
41
|
-
import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
|
|
44
|
+
import { aunOutboundAttention, normalizeAunMentionEntries, parseAunAttention, withAunAttention, } from '../aun/msg/mention-schema.js';
|
|
42
45
|
import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
43
46
|
import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
|
|
44
47
|
export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
|
|
@@ -51,6 +54,100 @@ const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
|
|
|
51
54
|
const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
|
|
52
55
|
const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
|
|
53
56
|
const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
|
|
57
|
+
const AUN_MAX_PAYLOAD_SIZE = 512 * 1024;
|
|
58
|
+
const AUN_MAX_FIELD_SIZE = 256 * 1024;
|
|
59
|
+
const AUN_MIN_FIELD_SIZE = 1024;
|
|
60
|
+
function jsonUtf8Size(value) {
|
|
61
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
62
|
+
}
|
|
63
|
+
function truncatedString(original, originalSize, maxJsonBytes) {
|
|
64
|
+
const fallback = '[输出已截断]';
|
|
65
|
+
if (jsonUtf8Size(fallback) > maxJsonBytes)
|
|
66
|
+
return fallback;
|
|
67
|
+
const candidate = (prefix) => (`${prefix}\n\n[截断:原始输出 ${formatSize(originalSize)},仅显示前 ${formatSize(Buffer.byteLength(prefix, 'utf8'))}]`);
|
|
68
|
+
let low = 0;
|
|
69
|
+
let high = original.length;
|
|
70
|
+
while (low < high) {
|
|
71
|
+
const middle = Math.ceil((low + high) / 2);
|
|
72
|
+
if (jsonUtf8Size(candidate(original.slice(0, middle))) <= maxJsonBytes)
|
|
73
|
+
low = middle;
|
|
74
|
+
else
|
|
75
|
+
high = middle - 1;
|
|
76
|
+
}
|
|
77
|
+
return candidate(original.slice(0, low));
|
|
78
|
+
}
|
|
79
|
+
function collectAunPayloadStrings(value, path, entries) {
|
|
80
|
+
if (!value || typeof value !== 'object')
|
|
81
|
+
return;
|
|
82
|
+
for (const key of Object.keys(value)) {
|
|
83
|
+
const child = value[key];
|
|
84
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
85
|
+
if (typeof child === 'string') {
|
|
86
|
+
entries.push({
|
|
87
|
+
parent: value,
|
|
88
|
+
key,
|
|
89
|
+
path: childPath,
|
|
90
|
+
original: child,
|
|
91
|
+
originalSize: Buffer.byteLength(child, 'utf8'),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (child && typeof child === 'object') {
|
|
95
|
+
collectAunPayloadStrings(child, childPath, entries);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Enforce the AUN payload budget using final JSON/UTF-8 size, including escaping. */
|
|
100
|
+
export function truncateAunPayloadIfNeeded(payload, maxPayloadBytes = AUN_MAX_PAYLOAD_SIZE, maxFieldBytes = AUN_MAX_FIELD_SIZE) {
|
|
101
|
+
const originalSize = jsonUtf8Size(payload);
|
|
102
|
+
if (originalSize <= maxPayloadBytes) {
|
|
103
|
+
return { payload, originalSize, finalSize: originalSize, truncatedFields: [], dropped: false };
|
|
104
|
+
}
|
|
105
|
+
const truncated = JSON.parse(JSON.stringify(payload));
|
|
106
|
+
const entries = [];
|
|
107
|
+
collectAunPayloadStrings(truncated, '', entries);
|
|
108
|
+
const changed = new Map();
|
|
109
|
+
const applyLimit = (entry, targetBytes) => {
|
|
110
|
+
const current = entry.parent[entry.key];
|
|
111
|
+
const currentSize = typeof current === 'string' ? jsonUtf8Size(current) : 0;
|
|
112
|
+
const next = truncatedString(entry.original, entry.originalSize, targetBytes);
|
|
113
|
+
const nextSize = jsonUtf8Size(next);
|
|
114
|
+
if (typeof current !== 'string' || nextSize >= currentSize)
|
|
115
|
+
return false;
|
|
116
|
+
entry.parent[entry.key] = next;
|
|
117
|
+
entry.parent[`${String(entry.key)}_truncated`] = true;
|
|
118
|
+
changed.set(entry.path, { originalSize: entry.originalSize, finalSize: nextSize });
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
// First cap any individual string by its serialized contribution. This
|
|
122
|
+
// catches escaped JSON/log output whose raw string is deceptively small.
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (jsonUtf8Size(entry.parent[entry.key]) > maxFieldBytes) {
|
|
125
|
+
applyLimit(entry, Math.floor(maxFieldBytes / 4));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let finalSize = jsonUtf8Size(truncated);
|
|
129
|
+
let guard = 0;
|
|
130
|
+
while (finalSize > maxPayloadBytes && guard++ < entries.length * 16 + 16) {
|
|
131
|
+
const candidates = entries
|
|
132
|
+
.map(entry => ({ entry, size: jsonUtf8Size(entry.parent[entry.key]) }))
|
|
133
|
+
.filter(item => item.size > jsonUtf8Size('[输出已截断]') + AUN_MIN_FIELD_SIZE)
|
|
134
|
+
.sort((a, b) => b.size - a.size);
|
|
135
|
+
const largest = candidates[0];
|
|
136
|
+
if (!largest)
|
|
137
|
+
break;
|
|
138
|
+
if (!applyLimit(largest.entry, Math.max(AUN_MIN_FIELD_SIZE, Math.floor(largest.size / 2))))
|
|
139
|
+
break;
|
|
140
|
+
finalSize = jsonUtf8Size(truncated);
|
|
141
|
+
}
|
|
142
|
+
const truncatedFields = [...changed.entries()].map(([path, sizes]) => ({ path, ...sizes }));
|
|
143
|
+
return {
|
|
144
|
+
payload: truncated,
|
|
145
|
+
originalSize,
|
|
146
|
+
finalSize,
|
|
147
|
+
truncatedFields,
|
|
148
|
+
dropped: finalSize > maxPayloadBytes,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
54
151
|
export function formatAunSendLogText(text) {
|
|
55
152
|
return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
|
|
56
153
|
}
|
|
@@ -248,7 +345,7 @@ export function buildAunFilePayload(params) {
|
|
|
248
345
|
payload.name = params.filename;
|
|
249
346
|
payload.content_type = params.contentType;
|
|
250
347
|
}
|
|
251
|
-
return payload;
|
|
348
|
+
return withAunAttention(payload, params.context?.attention);
|
|
252
349
|
}
|
|
253
350
|
const AUN_PERMANENT_ERROR_CODES = new Set([
|
|
254
351
|
400, 401, 403, 404,
|
|
@@ -522,22 +619,8 @@ function verifiedProtectedHeaders(msg, env) {
|
|
|
522
619
|
}
|
|
523
620
|
return undefined;
|
|
524
621
|
}
|
|
525
|
-
function redactMenuTokensForTrace(value
|
|
526
|
-
|
|
527
|
-
return value;
|
|
528
|
-
if (seen.has(value))
|
|
529
|
-
return '[Circular]';
|
|
530
|
-
seen.add(value);
|
|
531
|
-
if (Array.isArray(value))
|
|
532
|
-
return value.map(item => redactMenuTokensForTrace(item, seen));
|
|
533
|
-
const redacted = {};
|
|
534
|
-
for (const [key, item] of Object.entries(value)) {
|
|
535
|
-
const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
|
|
536
|
-
redacted[key] = /^(?:menu|ecweb)_token$/.test(normalizedKey)
|
|
537
|
-
? '[REDACTED]'
|
|
538
|
-
: redactMenuTokensForTrace(item, seen);
|
|
539
|
-
}
|
|
540
|
-
return redacted;
|
|
622
|
+
export function redactMenuTokensForTrace(value) {
|
|
623
|
+
return sanitizeMenuLogValue(value);
|
|
541
624
|
}
|
|
542
625
|
function payloadTypeForObserver(value) {
|
|
543
626
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -581,6 +664,10 @@ export class AUNChannel {
|
|
|
581
664
|
traceWriter = null;
|
|
582
665
|
eventBus = null;
|
|
583
666
|
queuedHandler = null;
|
|
667
|
+
groupInviteHandler;
|
|
668
|
+
groupInviteConnectedHandler;
|
|
669
|
+
groupInviteDeliveryGate;
|
|
670
|
+
groupInviteDelivered;
|
|
584
671
|
pendingEchoMessages = new Map();
|
|
585
672
|
isEchoSending = false;
|
|
586
673
|
agentDir;
|
|
@@ -629,6 +716,7 @@ export class AUNChannel {
|
|
|
629
716
|
* 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
|
|
630
717
|
*/
|
|
631
718
|
callClient(method, params, lane = 'normal', onStart) {
|
|
719
|
+
params = menuTransportParams(method, params);
|
|
632
720
|
return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)), lane, onStart);
|
|
633
721
|
}
|
|
634
722
|
withOutboundSendGate(method, run, lane = 'normal', onStart) {
|
|
@@ -838,7 +926,7 @@ export class AUNChannel {
|
|
|
838
926
|
return;
|
|
839
927
|
}
|
|
840
928
|
const context = { delivery };
|
|
841
|
-
if (threadId)
|
|
929
|
+
if (typeof threadId === 'string')
|
|
842
930
|
context.threadId = threadId;
|
|
843
931
|
if (operatorId && delivery.chatType === 'group')
|
|
844
932
|
context.peerId = operatorId;
|
|
@@ -933,7 +1021,7 @@ export class AUNChannel {
|
|
|
933
1021
|
threadId,
|
|
934
1022
|
replyContext: {
|
|
935
1023
|
...(cardInfo.replyContext ?? {}),
|
|
936
|
-
...(threadId ? { threadId } : {}),
|
|
1024
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
937
1025
|
...(cardDelivery ? { delivery: cardDelivery } : {}),
|
|
938
1026
|
},
|
|
939
1027
|
source: 'card-trigger',
|
|
@@ -1295,14 +1383,14 @@ export class AUNChannel {
|
|
|
1295
1383
|
return sanitized;
|
|
1296
1384
|
return undefined;
|
|
1297
1385
|
}
|
|
1298
|
-
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
|
|
1386
|
+
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, lable) {
|
|
1299
1387
|
if (!groupId)
|
|
1300
1388
|
throw new Error('buildGroupReplyContext requires groupId');
|
|
1301
1389
|
const replyContext = {
|
|
1302
1390
|
metadata: { encrypted, chatmode },
|
|
1303
1391
|
delivery: { chatType: 'group', groupId },
|
|
1304
1392
|
};
|
|
1305
|
-
if (threadId)
|
|
1393
|
+
if (typeof threadId === 'string')
|
|
1306
1394
|
replyContext.threadId = threadId;
|
|
1307
1395
|
replyContext.peerId = senderAid;
|
|
1308
1396
|
if (messageId)
|
|
@@ -1313,20 +1401,24 @@ export class AUNChannel {
|
|
|
1313
1401
|
replyContext.metadata = {};
|
|
1314
1402
|
replyContext.metadata.topicName = topicName;
|
|
1315
1403
|
}
|
|
1404
|
+
if (lable && threadId)
|
|
1405
|
+
replyContext.metadata.lable = lable;
|
|
1316
1406
|
return replyContext;
|
|
1317
1407
|
}
|
|
1318
|
-
buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
|
|
1408
|
+
buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, lable) {
|
|
1319
1409
|
const replyContext = {
|
|
1320
1410
|
metadata: { encrypted, chatmode, refMessageId },
|
|
1321
1411
|
delivery: { chatType: 'private' },
|
|
1322
1412
|
peerId: senderAid,
|
|
1323
1413
|
};
|
|
1324
|
-
if (threadId)
|
|
1414
|
+
if (typeof threadId === 'string')
|
|
1325
1415
|
replyContext.threadId = threadId;
|
|
1326
1416
|
if (messageId)
|
|
1327
1417
|
replyContext.replyToMessageId = messageId;
|
|
1328
1418
|
if (topicName && threadId)
|
|
1329
1419
|
replyContext.metadata.topicName = topicName;
|
|
1420
|
+
if (lable && threadId)
|
|
1421
|
+
replyContext.metadata.lable = lable;
|
|
1330
1422
|
return replyContext;
|
|
1331
1423
|
}
|
|
1332
1424
|
claimInboundMessage(kind, messageId, seq) {
|
|
@@ -1443,6 +1535,7 @@ export class AUNChannel {
|
|
|
1443
1535
|
invalidInboundEnvelopeLastLogAt = 0;
|
|
1444
1536
|
groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
|
|
1445
1537
|
peerInfoCache = new Map();
|
|
1538
|
+
peerInfoRevision = 0;
|
|
1446
1539
|
messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
|
|
1447
1540
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
1448
1541
|
peerE2ee = new Map();
|
|
@@ -1557,6 +1650,44 @@ export class AUNChannel {
|
|
|
1557
1650
|
getAid() {
|
|
1558
1651
|
return this._aid ?? this.config.aid;
|
|
1559
1652
|
}
|
|
1653
|
+
setGroupInviteReviewHandlers(handlers) {
|
|
1654
|
+
if (this.config.pureIdentity && handlers)
|
|
1655
|
+
return;
|
|
1656
|
+
this.groupInviteHandler = handlers?.onInvite;
|
|
1657
|
+
this.groupInviteConnectedHandler = handlers?.onConnected;
|
|
1658
|
+
this.groupInviteDeliveryGate = handlers?.canDeliver;
|
|
1659
|
+
this.groupInviteDelivered = handlers?.onDelivered;
|
|
1660
|
+
}
|
|
1661
|
+
async getGroupInviteSummary(groupId) {
|
|
1662
|
+
if (!this.client || !this.connected)
|
|
1663
|
+
return {};
|
|
1664
|
+
try {
|
|
1665
|
+
const result = await this.callAndTrace('group.get_info', { group_id: groupId });
|
|
1666
|
+
const group = result?.group ?? result;
|
|
1667
|
+
return {
|
|
1668
|
+
...(typeof group?.name === 'string' ? { name: group.name.slice(0, 120) } : {}),
|
|
1669
|
+
...(typeof group?.description === 'string' ? { description: group.description.slice(0, 240) } : {}),
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
catch {
|
|
1673
|
+
return {};
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
getGroupInviteDeliveryState(outboxId) {
|
|
1677
|
+
return outbox.deliveryState(this.config.aid, outboxId);
|
|
1678
|
+
}
|
|
1679
|
+
async invalidateGroupInviteCard(interactionId, ownerAid, messageIds) {
|
|
1680
|
+
const known = new Set(this.interactionCardMessageIds.get(interactionId) ?? []);
|
|
1681
|
+
await this.invalidateInteraction(interactionId, 'group_invite_review');
|
|
1682
|
+
// After a daemon restart only the review store knows the delivered cards.
|
|
1683
|
+
for (const id of messageIds) {
|
|
1684
|
+
if (!known.has(id))
|
|
1685
|
+
await this.recallInteractionCard(id, ownerAid, { chatType: 'private' });
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
refreshGroupInviteMembership(groupId) {
|
|
1689
|
+
this.invalidateObserverGroupMembers(groupId);
|
|
1690
|
+
}
|
|
1560
1691
|
/** 是否已连接(client 存在且认证完成)。 */
|
|
1561
1692
|
isConnected() {
|
|
1562
1693
|
return this.connected && this.client !== null;
|
|
@@ -1667,6 +1798,33 @@ export class AUNChannel {
|
|
|
1667
1798
|
// instance must not mutate the new instance's health or message state.
|
|
1668
1799
|
const isCurrentClient = () => this.client === client;
|
|
1669
1800
|
// Register event handlers before connecting
|
|
1801
|
+
client.on('agentmd.changed', (data) => {
|
|
1802
|
+
if (!isCurrentClient() || this.intentionalDisconnect || typeof data !== 'string')
|
|
1803
|
+
return;
|
|
1804
|
+
let aid;
|
|
1805
|
+
try {
|
|
1806
|
+
aid = validateAIDFormat(data);
|
|
1807
|
+
}
|
|
1808
|
+
catch {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
this.peerInfoRevision++;
|
|
1812
|
+
this.peerInfoCache.delete(aid);
|
|
1813
|
+
this.groupNameCache.delete(aid);
|
|
1814
|
+
invalidateAgentDisplayName(aid);
|
|
1815
|
+
PeerIdentityCache.invalidate('aun', aid, path.join(resolvePaths().agentsDir, this.config.aid));
|
|
1816
|
+
this.trace('IN', 'agentmd.changed', { aid });
|
|
1817
|
+
// Peer snapshots refresh lazily on the next lookup. The self name is
|
|
1818
|
+
// held separately by this channel, so refresh it with the existing API.
|
|
1819
|
+
if (aid === (this._aid || this.config.aid)) {
|
|
1820
|
+
void refreshAgentDisplayName(aid).then(name => {
|
|
1821
|
+
if (!name || !isCurrentClient() || this.intentionalDisconnect)
|
|
1822
|
+
return;
|
|
1823
|
+
this._selfName = name;
|
|
1824
|
+
this.aidStatsCollector?.setSelfName(this.config.aid, name);
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1670
1828
|
client.on('message.received', async (data) => {
|
|
1671
1829
|
if (!isCurrentClient())
|
|
1672
1830
|
return;
|
|
@@ -1712,6 +1870,9 @@ export class AUNChannel {
|
|
|
1712
1870
|
const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
|
|
1713
1871
|
this.trace('IN', 'group.invite_received', data);
|
|
1714
1872
|
logger.info(`${this.logPrefix()} Group invitation received: group=${groupId} pending=${inviteIds.length || 1}`);
|
|
1873
|
+
void Promise.resolve(this.groupInviteHandler?.(data)).catch(error => {
|
|
1874
|
+
logger.warn(`${this.logPrefix()} Group invitation review dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1875
|
+
});
|
|
1715
1876
|
});
|
|
1716
1877
|
}
|
|
1717
1878
|
// 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
|
|
@@ -1751,18 +1912,7 @@ export class AUNChannel {
|
|
|
1751
1912
|
client.on('message.recalled', (data) => {
|
|
1752
1913
|
if (!isCurrentClient())
|
|
1753
1914
|
return;
|
|
1754
|
-
this.
|
|
1755
|
-
if (data && typeof data === 'object') {
|
|
1756
|
-
const ids = data.message_ids;
|
|
1757
|
-
if (Array.isArray(ids)) {
|
|
1758
|
-
for (const id of ids) {
|
|
1759
|
-
if (typeof id === 'string') {
|
|
1760
|
-
logger.info(`${this.logPrefix()} Message recalled: ${id}`);
|
|
1761
|
-
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1915
|
+
this.handleMessageRecalled(data, client, isCurrentClient);
|
|
1766
1916
|
});
|
|
1767
1917
|
client.on('message.undecryptable', (data) => {
|
|
1768
1918
|
if (!isCurrentClient())
|
|
@@ -1848,13 +1998,37 @@ export class AUNChannel {
|
|
|
1848
1998
|
this._aid = this.client.aid ?? undefined;
|
|
1849
1999
|
const deviceId = this.client._device_id ?? '';
|
|
1850
2000
|
this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
|
|
1851
|
-
//
|
|
1852
|
-
|
|
2001
|
+
// 控制 AID 也有标准 host agent.md。发布在后台执行,短暂的 Name
|
|
2002
|
+
// Service/Gateway 故障不能把控制通道判定为连接失败。
|
|
2003
|
+
if (this.config.pureIdentity) {
|
|
2004
|
+
this._selfName = buildControlAgentName(aidName);
|
|
2005
|
+
void ensureControlAgentMd(aidName, {
|
|
2006
|
+
store: this.store ?? undefined,
|
|
2007
|
+
aunPath,
|
|
2008
|
+
cacheRoot: resolveRoot(),
|
|
2009
|
+
})
|
|
2010
|
+
.then(result => {
|
|
2011
|
+
this._selfName = result.name;
|
|
2012
|
+
if (this.aidStatsCollector)
|
|
2013
|
+
this.aidStatsCollector.setSelfName(this.config.aid, result.name);
|
|
2014
|
+
if (result.published)
|
|
2015
|
+
logger.info(`${this.logPrefix()} 控制 AID agent.md 已发布: ${result.name}`);
|
|
2016
|
+
})
|
|
2017
|
+
.catch(error => logger.warn(`${this.logPrefix()} 控制 AID agent.md 补全失败(后台重试): ${error instanceof Error ? error.message : String(error)}`));
|
|
2018
|
+
}
|
|
2019
|
+
else {
|
|
2020
|
+
this._selfName = this.loadSelfName(aidName, aunPath);
|
|
2021
|
+
}
|
|
1853
2022
|
if (this._selfName && this.aidStatsCollector)
|
|
1854
2023
|
this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
|
|
1855
2024
|
this.connected = true;
|
|
1856
2025
|
this.connectedAt = Date.now();
|
|
1857
2026
|
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
|
|
2027
|
+
if (!this.config.pureIdentity) {
|
|
2028
|
+
void Promise.resolve(this.groupInviteConnectedHandler?.()).catch(error => {
|
|
2029
|
+
logger.warn(`${this.logPrefix()} Group invitation recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
1858
2032
|
// AUN seq_tracker is transport-scoped, while this channel's durable
|
|
1859
2033
|
// dedup log is application-scoped. A diagnostic consumer can advance
|
|
1860
2034
|
// the former without the application having seen those messages. On a
|
|
@@ -1912,7 +2086,7 @@ export class AUNChannel {
|
|
|
1912
2086
|
if (hasMessageLogOperation(chatDir, operationId)
|
|
1913
2087
|
|| hasMessageLogOperation(chatDir, postBootstrapWelcomeOperationId(aidName)))
|
|
1914
2088
|
continue;
|
|
1915
|
-
const prepared = outbox.findByDedupeKey(aidName, operationId);
|
|
2089
|
+
const prepared = outbox.findByDedupeKey(aidName, operationId, { includeTerminal: true });
|
|
1916
2090
|
if (prepared && prepared.channelId === owner && isDeliveryTarget(prepared.delivery) && sameDeliveryTarget(prepared.delivery, delivery))
|
|
1917
2091
|
continue;
|
|
1918
2092
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
@@ -1948,7 +2122,7 @@ export class AUNChannel {
|
|
|
1948
2122
|
}
|
|
1949
2123
|
async sendPostBootstrapWelcome() {
|
|
1950
2124
|
if (!await this.preparePostBootstrapWelcome())
|
|
1951
|
-
|
|
2125
|
+
throw new Error('Failed to persist post-bootstrap welcome');
|
|
1952
2126
|
return this.reconcilePostBootstrapWelcome();
|
|
1953
2127
|
}
|
|
1954
2128
|
async hasPendingPostBootstrapWelcome() {
|
|
@@ -1966,6 +2140,8 @@ export class AUNChannel {
|
|
|
1966
2140
|
const pull = client?._pullV2;
|
|
1967
2141
|
if (typeof repair !== 'function' || typeof pull !== 'function')
|
|
1968
2142
|
return;
|
|
2143
|
+
if (this.inboundP2PCursorRepairTimer)
|
|
2144
|
+
clearTimeout(this.inboundP2PCursorRepairTimer);
|
|
1969
2145
|
const ns = `p2p:${this.config.aid}`;
|
|
1970
2146
|
let appSeq = 0;
|
|
1971
2147
|
for (const [key, value] of this.inboundSeenMessages) {
|
|
@@ -1978,7 +2154,8 @@ export class AUNChannel {
|
|
|
1978
2154
|
}
|
|
1979
2155
|
if (appSeq <= 0)
|
|
1980
2156
|
return;
|
|
1981
|
-
setTimeout(() => {
|
|
2157
|
+
this.inboundP2PCursorRepairTimer = setTimeout(() => {
|
|
2158
|
+
this.inboundP2PCursorRepairTimer = null;
|
|
1982
2159
|
if (!isCurrentClient() || !this.connected)
|
|
1983
2160
|
return;
|
|
1984
2161
|
const transportSeq = Number(tracker.getContiguousSeq?.(ns) ?? 0);
|
|
@@ -1996,6 +2173,24 @@ export class AUNChannel {
|
|
|
1996
2173
|
}
|
|
1997
2174
|
}, 250);
|
|
1998
2175
|
}
|
|
2176
|
+
inboundP2PCursorRepairTimer = null;
|
|
2177
|
+
handleMessageRecalled(data, client, isCurrentClient) {
|
|
2178
|
+
this.trace('IN', 'message.recalled', data);
|
|
2179
|
+
if (data && typeof data === 'object') {
|
|
2180
|
+
const ids = data.message_ids;
|
|
2181
|
+
if (Array.isArray(ids)) {
|
|
2182
|
+
for (const id of ids) {
|
|
2183
|
+
if (typeof id === 'string') {
|
|
2184
|
+
logger.info(`${this.logPrefix()} Message recalled: ${id}`);
|
|
2185
|
+
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
// A recall tombstone may arrive at a high transport sequence and advance
|
|
2191
|
+
// the SDK cursor past messages that were never delivered to the app.
|
|
2192
|
+
this.scheduleInboundP2PCursorRepair(client, isCurrentClient);
|
|
2193
|
+
}
|
|
1999
2194
|
pendingPostBootstrapWelcomeOwners() {
|
|
2000
2195
|
return pendingPostBootstrapWelcomeOwners(this.config.aid);
|
|
2001
2196
|
}
|
|
@@ -2011,25 +2206,35 @@ export class AUNChannel {
|
|
|
2011
2206
|
const configuredAid = this.config.aid;
|
|
2012
2207
|
const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
|
|
2013
2208
|
const entries = findPostBootstrapWelcomeOutbox(aid);
|
|
2014
|
-
if (entries.length === 0)
|
|
2015
|
-
return false;
|
|
2016
2209
|
const agentConfig = loadAgent(aid);
|
|
2210
|
+
const owners = agentConfig?.owners ?? [];
|
|
2211
|
+
if (entries.length === 0)
|
|
2212
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
2017
2213
|
if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
|
|
2018
2214
|
logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
|
|
2019
|
-
return
|
|
2215
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
2020
2216
|
}
|
|
2021
2217
|
if (!this.connected || !this.client)
|
|
2022
|
-
return
|
|
2218
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
2023
2219
|
for (const entry of entries) {
|
|
2024
2220
|
if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
|
|
2025
2221
|
continue;
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2222
|
+
try {
|
|
2223
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
2224
|
+
if (sent.status === 'sent')
|
|
2225
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
2226
|
+
else if (sent.status === 'permanent')
|
|
2227
|
+
this.markPermanentOutboxFailure(entry, sent);
|
|
2228
|
+
else
|
|
2229
|
+
outbox.recordRetry(this.config.aid, entry, sent);
|
|
2230
|
+
}
|
|
2231
|
+
catch (error) {
|
|
2232
|
+
// Lifecycle has already committed. Preserve its success and the
|
|
2233
|
+
// durable operation even when local post-send bookkeeping fails.
|
|
2234
|
+
logger.warn(`${this.logPrefix()} Welcome reconciliation failed: entry=${entry.id} error=${error}`);
|
|
2235
|
+
}
|
|
2031
2236
|
}
|
|
2032
|
-
return
|
|
2237
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
2033
2238
|
}
|
|
2034
2239
|
// ── Event handlers ──────────────────────────────────────────
|
|
2035
2240
|
/**
|
|
@@ -2372,7 +2577,7 @@ export class AUNChannel {
|
|
|
2372
2577
|
const chatId = fromAid;
|
|
2373
2578
|
// 解析对端身份(30天缓存)
|
|
2374
2579
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
2375
|
-
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false);
|
|
2580
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
2376
2581
|
const shortAid = this.getShortAid(fromAid);
|
|
2377
2582
|
const displayName = peerIdentity.name || shortAid;
|
|
2378
2583
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
@@ -2391,13 +2596,15 @@ export class AUNChannel {
|
|
|
2391
2596
|
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
|
|
2392
2597
|
const installation = p2pPayloadType.startsWith('menu.')
|
|
2393
2598
|
? this.localSenderInstallation?.aunSenderInstallation(msg) : undefined;
|
|
2599
|
+
const peerDeviceId = p2pPayloadType.startsWith('menu.')
|
|
2600
|
+
? menuSenderDeviceId(msg.sender_device_id) : undefined;
|
|
2394
2601
|
this.acknowledgeImmediately(messageId, seq);
|
|
2395
2602
|
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
2396
2603
|
this.dispatchMessage({
|
|
2397
2604
|
channelId: chatId, userId: fromAid,
|
|
2398
2605
|
text: JSON.stringify(payload),
|
|
2399
2606
|
chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
|
|
2400
|
-
peerDeviceId
|
|
2607
|
+
peerDeviceId,
|
|
2401
2608
|
peerSlotId: installation?.slotId,
|
|
2402
2609
|
threadId,
|
|
2403
2610
|
peerName: displayName || undefined,
|
|
@@ -2406,7 +2613,14 @@ export class AUNChannel {
|
|
|
2406
2613
|
protectedHeaders,
|
|
2407
2614
|
mentions,
|
|
2408
2615
|
mentionAids: mentionEntryAids(mentions),
|
|
2409
|
-
replyContext:
|
|
2616
|
+
replyContext: {
|
|
2617
|
+
...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
|
|
2618
|
+
metadata: {
|
|
2619
|
+
...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId).metadata,
|
|
2620
|
+
...(protectedHeaders?.sdk_version ? { menuClientSdkVersion: protectedHeaders.sdk_version } : {}),
|
|
2621
|
+
},
|
|
2622
|
+
...(peerDeviceId ? { toDeviceId: peerDeviceId } : {}),
|
|
2623
|
+
},
|
|
2410
2624
|
msgType: logDescriptor.msgType,
|
|
2411
2625
|
payloadType: logDescriptor.payloadType,
|
|
2412
2626
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -2456,7 +2670,7 @@ export class AUNChannel {
|
|
|
2456
2670
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
|
|
2457
2671
|
const isSystemP2P = p2pPayloadType === 'event';
|
|
2458
2672
|
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
|
|
2459
|
-
const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId);
|
|
2673
|
+
const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLable(payload));
|
|
2460
2674
|
this.dispatchMessage({
|
|
2461
2675
|
channelId: chatId,
|
|
2462
2676
|
userId: fromAid,
|
|
@@ -2568,7 +2782,7 @@ export class AUNChannel {
|
|
|
2568
2782
|
peerName: displayName,
|
|
2569
2783
|
peerType: peerInfo?.type || 'unknown',
|
|
2570
2784
|
seq,
|
|
2571
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
|
|
2785
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLable(payload)),
|
|
2572
2786
|
createdAt,
|
|
2573
2787
|
});
|
|
2574
2788
|
return;
|
|
@@ -2638,7 +2852,7 @@ export class AUNChannel {
|
|
|
2638
2852
|
this.pendingEchoMessages.set(messageId, {
|
|
2639
2853
|
text: echoText,
|
|
2640
2854
|
channelId: groupId,
|
|
2641
|
-
context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId),
|
|
2855
|
+
context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLable(payload)),
|
|
2642
2856
|
receiveTs: Date.now(),
|
|
2643
2857
|
});
|
|
2644
2858
|
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
@@ -2660,8 +2874,20 @@ export class AUNChannel {
|
|
|
2660
2874
|
// 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
|
|
2661
2875
|
// TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
|
|
2662
2876
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
2877
|
+
try {
|
|
2878
|
+
if (text.trim() || this.collectAllAttachments(payload).length) {
|
|
2879
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, 'unmentioned', 'filtered');
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
catch (error) {
|
|
2883
|
+
// A failed local append must not suppress a subsequent SDK redelivery.
|
|
2884
|
+
const namespace = this.config.aid + ':' + (this.config.channelName ?? 'aun') + ':aun:group';
|
|
2885
|
+
this.inboundSeenMessages.delete(JSON.stringify([namespace, messageId]));
|
|
2886
|
+
this.rewriteInboundDedupe();
|
|
2887
|
+
logger.warn(`${this.logPrefix()} Deferred group message was not recorded; awaiting redelivery: ${error}`);
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2663
2890
|
this.acknowledgeImmediately(messageId, seq);
|
|
2664
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
2665
2891
|
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))})`);
|
|
2666
2892
|
return;
|
|
2667
2893
|
}
|
|
@@ -2684,7 +2910,7 @@ export class AUNChannel {
|
|
|
2684
2910
|
// Process attachments
|
|
2685
2911
|
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
|
|
2686
2912
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
2687
|
-
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
|
|
2913
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
2688
2914
|
const shortAid = this.getShortAid(senderAid);
|
|
2689
2915
|
const displayName = peerIdentity.name || shortAid;
|
|
2690
2916
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
@@ -2736,7 +2962,7 @@ export class AUNChannel {
|
|
|
2736
2962
|
isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
|
|
2737
2963
|
source: isOwnHandoff ? 'handoff' : undefined,
|
|
2738
2964
|
encrypted: msgEncrypted,
|
|
2739
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
|
|
2965
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLable(payload)),
|
|
2740
2966
|
mentionMode: serverMentionMode,
|
|
2741
2967
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
2742
2968
|
msgType: logDescriptor.msgType,
|
|
@@ -2786,7 +3012,7 @@ export class AUNChannel {
|
|
|
2786
3012
|
channelId: event.channelId,
|
|
2787
3013
|
context: {
|
|
2788
3014
|
...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
|
|
2789
|
-
...(threadId ? { threadId } : {}),
|
|
3015
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
2790
3016
|
delivery,
|
|
2791
3017
|
},
|
|
2792
3018
|
receiveTs: Date.now(),
|
|
@@ -2801,7 +3027,7 @@ export class AUNChannel {
|
|
|
2801
3027
|
const replyContext = {
|
|
2802
3028
|
...(event.replyContext ?? {}),
|
|
2803
3029
|
delivery,
|
|
2804
|
-
...(threadId ? { threadId } : {}),
|
|
3030
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
2805
3031
|
};
|
|
2806
3032
|
this.messageHandler({
|
|
2807
3033
|
channelId: event.channelId || '',
|
|
@@ -2841,10 +3067,10 @@ export class AUNChannel {
|
|
|
2841
3067
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
2842
3068
|
});
|
|
2843
3069
|
}
|
|
2844
|
-
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
|
|
3070
|
+
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq, groupAttention, groupDelivery) {
|
|
2845
3071
|
try {
|
|
2846
3072
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
2847
|
-
|
|
3073
|
+
const entry = buildInboundEntry({
|
|
2848
3074
|
from: fromAid,
|
|
2849
3075
|
to: this.config.aid,
|
|
2850
3076
|
threadId: threadId || null,
|
|
@@ -2859,10 +3085,18 @@ export class AUNChannel {
|
|
|
2859
3085
|
timestamp: receivedAt,
|
|
2860
3086
|
receivedAt,
|
|
2861
3087
|
gatewaySeq,
|
|
2862
|
-
|
|
3088
|
+
groupAttention,
|
|
3089
|
+
groupDelivery,
|
|
3090
|
+
});
|
|
3091
|
+
if (groupDelivery === 'filtered')
|
|
3092
|
+
appendMessageLogStrict(chatDir, entry, true);
|
|
3093
|
+
else
|
|
3094
|
+
appendMessageLog(chatDir, entry);
|
|
2863
3095
|
}
|
|
2864
3096
|
catch (e) {
|
|
2865
|
-
logger.
|
|
3097
|
+
logger.warn(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
|
|
3098
|
+
if (groupDelivery === 'filtered')
|
|
3099
|
+
throw e;
|
|
2866
3100
|
}
|
|
2867
3101
|
}
|
|
2868
3102
|
// ── 观察者模式(Observer Mode) ──────────────────────────────
|
|
@@ -3641,7 +3875,9 @@ export class AUNChannel {
|
|
|
3641
3875
|
return Buffer.byteLength(logText, 'utf-8');
|
|
3642
3876
|
}
|
|
3643
3877
|
applyReplyContextToPayload(payload, context) {
|
|
3644
|
-
const finalPayload = this.stripUndefinedDeep({ ...payload });
|
|
3878
|
+
const finalPayload = this.stripUndefinedDeep(withAunAttention({ ...payload }, context?.attention));
|
|
3879
|
+
if (finalPayload.thread_id === '')
|
|
3880
|
+
delete finalPayload.thread_id;
|
|
3645
3881
|
if (context?.threadId && !finalPayload.thread_id)
|
|
3646
3882
|
finalPayload.thread_id = context.threadId;
|
|
3647
3883
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
@@ -3653,13 +3889,14 @@ export class AUNChannel {
|
|
|
3653
3889
|
return finalPayload;
|
|
3654
3890
|
}
|
|
3655
3891
|
/**
|
|
3656
|
-
* Validate
|
|
3892
|
+
* Validate AUN-specific structured fields before crossing the durable
|
|
3657
3893
|
* send boundary. Return a copy when mentions are present so callers do not
|
|
3658
3894
|
* retain an unvalidated array reference.
|
|
3659
3895
|
*/
|
|
3660
3896
|
normalizeAunPayloadMentions(payload) {
|
|
3661
3897
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
3662
3898
|
return payload;
|
|
3899
|
+
payload = withAunAttention(payload);
|
|
3663
3900
|
if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
|
|
3664
3901
|
return payload;
|
|
3665
3902
|
return {
|
|
@@ -3804,16 +4041,21 @@ export class AUNChannel {
|
|
|
3804
4041
|
registerCardPostSend(messageId, action, channelId, delivery, replyContext) {
|
|
3805
4042
|
if (action.type !== 'register_interaction_card')
|
|
3806
4043
|
return;
|
|
4044
|
+
// Card replies start a new input; an outbound attention marker must not
|
|
4045
|
+
// become the default for every message produced after clicking the card.
|
|
4046
|
+
const callbackContext = replyContext ? { ...replyContext } : undefined;
|
|
4047
|
+
if (callbackContext)
|
|
4048
|
+
delete callbackContext.attention;
|
|
3807
4049
|
this.cardMessageIdMap.set(messageId, {
|
|
3808
4050
|
requestId: action.requestId,
|
|
3809
4051
|
isCommandCard: action.isCommandCard,
|
|
3810
4052
|
initiatorAid: action.initiatorAid,
|
|
3811
4053
|
channelId,
|
|
3812
4054
|
delivery: delivery ?? action.delivery,
|
|
3813
|
-
replyContext,
|
|
4055
|
+
replyContext: callbackContext,
|
|
3814
4056
|
});
|
|
3815
4057
|
this.ownedCardMsgIds.add(messageId);
|
|
3816
|
-
if (replyContext?.threadId) {
|
|
4058
|
+
if (typeof replyContext?.threadId === 'string') {
|
|
3817
4059
|
this.ownedCardTopics.set(messageId, { channelId, threadId: replyContext.threadId });
|
|
3818
4060
|
}
|
|
3819
4061
|
let messageIds = this.interactionCardMessageIds.get(action.requestId);
|
|
@@ -3850,6 +4092,8 @@ export class AUNChannel {
|
|
|
3850
4092
|
return;
|
|
3851
4093
|
if (entry.postSend.type === 'register_interaction_card') {
|
|
3852
4094
|
this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery, entry.context);
|
|
4095
|
+
if (entry.postSend.requestId.startsWith('gir-'))
|
|
4096
|
+
this.groupInviteDelivered?.(entry.postSend.requestId, messageId);
|
|
3853
4097
|
}
|
|
3854
4098
|
}
|
|
3855
4099
|
async sendAunPayload(channelId, payload, context, label) {
|
|
@@ -3881,9 +4125,20 @@ export class AUNChannel {
|
|
|
3881
4125
|
const payloadWithCausationRef = causationRef
|
|
3882
4126
|
? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
|
|
3883
4127
|
: normalizedPayload;
|
|
4128
|
+
if (payloadWithCausationRef.thread_id === '')
|
|
4129
|
+
delete payloadWithCausationRef.thread_id;
|
|
3884
4130
|
// Truncate payload if too large (AUN SDK limit: 1MB)
|
|
3885
4131
|
const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
|
|
3886
4132
|
const params = { payload: truncatedPayload, encrypt };
|
|
4133
|
+
const isMenuPayload = typeof truncatedPayload.type === 'string' && truncatedPayload.type.startsWith('menu.');
|
|
4134
|
+
if (!isGroup && truncatedPayload.type === 'menu.response')
|
|
4135
|
+
params.to_device_id = context?.toDeviceId;
|
|
4136
|
+
if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
|
|
4137
|
+
params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
|
|
4138
|
+
}
|
|
4139
|
+
if (isMenuPayload && encrypt && context?.metadata?.protectedHeaders) {
|
|
4140
|
+
params.protected_headers = context.metadata.protectedHeaders;
|
|
4141
|
+
}
|
|
3887
4142
|
if (isGroup)
|
|
3888
4143
|
params.group_id = delivery.groupId;
|
|
3889
4144
|
else
|
|
@@ -3904,7 +4159,7 @@ export class AUNChannel {
|
|
|
3904
4159
|
return await callOnce(params, false);
|
|
3905
4160
|
}
|
|
3906
4161
|
catch (e) {
|
|
3907
|
-
if (encrypt && e instanceof E2EEError) {
|
|
4162
|
+
if (encrypt && e instanceof E2EEError && !isMenuPayload) {
|
|
3908
4163
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
3909
4164
|
logger.warn(`${this.logPrefix()} E2EE ${label} send failed to ${channelId}, retrying plaintext: ${e}`);
|
|
3910
4165
|
const fallbackParams = { ...params, encrypt: false };
|
|
@@ -3957,7 +4212,8 @@ export class AUNChannel {
|
|
|
3957
4212
|
...classifyAunPayloadForLog(payload), msgId: messageId, encrypt, context, isGroup, source,
|
|
3958
4213
|
transport: this.sendReceiptFromResult(result),
|
|
3959
4214
|
});
|
|
3960
|
-
|
|
4215
|
+
if (typeof payload.type !== 'string' || !payload.type.startsWith('menu.'))
|
|
4216
|
+
this.forwardOutbound(result);
|
|
3961
4217
|
}
|
|
3962
4218
|
async sendContentPayload(channelId, payload, opts) {
|
|
3963
4219
|
const delivery = this.requireDelivery(channelId, opts.context);
|
|
@@ -4000,6 +4256,7 @@ export class AUNChannel {
|
|
|
4000
4256
|
queue: opts.queue,
|
|
4001
4257
|
channelId,
|
|
4002
4258
|
delivery,
|
|
4259
|
+
dedupeKey: opts.dedupeKey,
|
|
4003
4260
|
type: 'payload',
|
|
4004
4261
|
contentKind: opts.contentKind,
|
|
4005
4262
|
payload: finalPayload,
|
|
@@ -4071,7 +4328,7 @@ export class AUNChannel {
|
|
|
4071
4328
|
base.initiator = context.peerId;
|
|
4072
4329
|
if (context?.replyToMessageId)
|
|
4073
4330
|
base.ref_message_id = context.replyToMessageId;
|
|
4074
|
-
return base;
|
|
4331
|
+
return withAunAttention(base, context?.attention);
|
|
4075
4332
|
}
|
|
4076
4333
|
activityLogText(raw) {
|
|
4077
4334
|
// 兼容两种入参:原始 ThoughtItem(顶层字段),或已构建的 activity payload(字段收在 .item 里)
|
|
@@ -4101,65 +4358,23 @@ export class AUNChannel {
|
|
|
4101
4358
|
* the encrypted AUN envelope and RPC framing.
|
|
4102
4359
|
*/
|
|
4103
4360
|
truncatePayloadIfNeeded(payload, label) {
|
|
4104
|
-
const
|
|
4105
|
-
|
|
4106
|
-
// First check total size
|
|
4107
|
-
const payloadJson = JSON.stringify(payload);
|
|
4108
|
-
const totalSize = Buffer.byteLength(payloadJson, 'utf-8');
|
|
4109
|
-
if (totalSize <= MAX_PAYLOAD_SIZE) {
|
|
4361
|
+
const result = truncateAunPayloadIfNeeded(payload);
|
|
4362
|
+
if (result.originalSize <= AUN_MAX_PAYLOAD_SIZE) {
|
|
4110
4363
|
return payload; // No truncation needed
|
|
4111
4364
|
}
|
|
4112
|
-
logger.warn(`${this.logPrefix()} Payload too large (${formatSize(
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
return false;
|
|
4119
|
-
let changed = false;
|
|
4120
|
-
for (const key of Object.keys(obj)) {
|
|
4121
|
-
const value = obj[key];
|
|
4122
|
-
const fieldPath = path ? `${path}.${key}` : key;
|
|
4123
|
-
if (typeof value === 'string') {
|
|
4124
|
-
const fieldSize = Buffer.byteLength(value, 'utf-8');
|
|
4125
|
-
if (fieldSize > MAX_FIELD_SIZE) {
|
|
4126
|
-
const truncatedValue = value.substring(0, MAX_FIELD_SIZE / 4); // Rough estimate, UTF-8 safe
|
|
4127
|
-
const actualTruncatedSize = Buffer.byteLength(truncatedValue, 'utf-8');
|
|
4128
|
-
obj[key] = truncatedValue + `\n\n[截断:原始输出 ${formatSize(fieldSize)},仅显示前 ${formatSize(actualTruncatedSize)}]`;
|
|
4129
|
-
obj[`${key}_truncated`] = true;
|
|
4130
|
-
logger.info(`${this.logPrefix()} Truncated field ${fieldPath}: ${formatSize(fieldSize)} → ${formatSize(actualTruncatedSize)}`);
|
|
4131
|
-
changed = true;
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
else if (Array.isArray(value)) {
|
|
4135
|
-
for (let i = 0; i < value.length; i++) {
|
|
4136
|
-
if (truncateObject(value[i], `${fieldPath}[${i}]`)) {
|
|
4137
|
-
changed = true;
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
}
|
|
4141
|
-
else if (typeof value === 'object' && value !== null) {
|
|
4142
|
-
if (truncateObject(value, fieldPath)) {
|
|
4143
|
-
changed = true;
|
|
4144
|
-
}
|
|
4145
|
-
}
|
|
4146
|
-
}
|
|
4147
|
-
return changed;
|
|
4148
|
-
};
|
|
4149
|
-
truncateObject(truncated, '');
|
|
4150
|
-
// Check size again after truncation
|
|
4151
|
-
const newJson = JSON.stringify(truncated);
|
|
4152
|
-
const newSize = Buffer.byteLength(newJson, 'utf-8');
|
|
4153
|
-
if (newSize > MAX_PAYLOAD_SIZE) {
|
|
4154
|
-
// Still too large - this shouldn't happen with 256KB field limit, but log it
|
|
4155
|
-
logger.error(`${this.logPrefix()} Payload still too large after truncation: ${formatSize(newSize)}, dropping payload`);
|
|
4365
|
+
logger.warn(`${this.logPrefix()} Payload too large (${formatSize(result.originalSize)}), truncating... label=${label}`);
|
|
4366
|
+
for (const field of result.truncatedFields) {
|
|
4367
|
+
logger.info(`${this.logPrefix()} Truncated field ${field.path}: ${formatSize(field.originalSize)} → ${formatSize(field.finalSize)}`);
|
|
4368
|
+
}
|
|
4369
|
+
if (result.dropped) {
|
|
4370
|
+
logger.error(`${this.logPrefix()} Payload still too large after truncation: ${formatSize(result.finalSize)}, dropping payload`);
|
|
4156
4371
|
return {
|
|
4157
4372
|
type: 'text',
|
|
4158
4373
|
text: '消息内容过大,无法完整发送,请缩小输出后重试',
|
|
4159
4374
|
};
|
|
4160
4375
|
}
|
|
4161
|
-
logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(
|
|
4162
|
-
return
|
|
4376
|
+
logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(result.originalSize)} → ${formatSize(result.finalSize)}`);
|
|
4377
|
+
return result.payload;
|
|
4163
4378
|
}
|
|
4164
4379
|
buildActivityPayload(envelope, context, item) {
|
|
4165
4380
|
const rawItem = item && typeof item === 'object' && !Array.isArray(item)
|
|
@@ -4171,11 +4386,11 @@ export class AUNChannel {
|
|
|
4171
4386
|
const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
|
|
4172
4387
|
? { ...rawItem, arguments: displayArguments }
|
|
4173
4388
|
: rawItem;
|
|
4174
|
-
return {
|
|
4389
|
+
return withAunAttention({
|
|
4175
4390
|
...this.buildTaskPayloadBase(envelope, context),
|
|
4176
4391
|
type: 'activity',
|
|
4177
4392
|
item: this.stripUndefinedDeep(activityItem),
|
|
4178
|
-
};
|
|
4393
|
+
});
|
|
4179
4394
|
}
|
|
4180
4395
|
async sendReliableStructured(channelId, payload, context, logText) {
|
|
4181
4396
|
const result = await this.sendContentPayload(channelId, payload, {
|
|
@@ -4197,6 +4412,7 @@ export class AUNChannel {
|
|
|
4197
4412
|
return result;
|
|
4198
4413
|
}
|
|
4199
4414
|
async sendMessage(channelId, text, context) {
|
|
4415
|
+
parseAunAttention(context?.attention);
|
|
4200
4416
|
if (!text?.trim()) {
|
|
4201
4417
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
4202
4418
|
return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
|
|
@@ -4313,6 +4529,7 @@ export class AUNChannel {
|
|
|
4313
4529
|
if (entry.deliveryReceipt?.messageId) {
|
|
4314
4530
|
return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
|
|
4315
4531
|
}
|
|
4532
|
+
outbox.recordRetry(this.config.aid, entry, result);
|
|
4316
4533
|
return {
|
|
4317
4534
|
status: 'queued',
|
|
4318
4535
|
outboxId: entry.id,
|
|
@@ -4332,6 +4549,8 @@ export class AUNChannel {
|
|
|
4332
4549
|
try {
|
|
4333
4550
|
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
4334
4551
|
finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
4552
|
+
if (finalPayload.thread_id === '')
|
|
4553
|
+
delete finalPayload.thread_id;
|
|
4335
4554
|
}
|
|
4336
4555
|
catch (e) {
|
|
4337
4556
|
return {
|
|
@@ -4433,11 +4652,7 @@ export class AUNChannel {
|
|
|
4433
4652
|
code: 'H_CLASS_PROTECTED',
|
|
4434
4653
|
});
|
|
4435
4654
|
}
|
|
4436
|
-
const
|
|
4437
|
-
call: (method, params) => this.callAndTrace(method, params),
|
|
4438
|
-
close: async () => { },
|
|
4439
|
-
};
|
|
4440
|
-
const built = await uploadFileAndBuildPayload(connection, this.config.aid, file.filePath, {
|
|
4655
|
+
const built = await uploadFileAndBuildPayload(this.client, this.config.aid, file.filePath, {
|
|
4441
4656
|
as: file.as,
|
|
4442
4657
|
contentType: file.contentType,
|
|
4443
4658
|
text: file.text,
|
|
@@ -4458,6 +4673,8 @@ export class AUNChannel {
|
|
|
4458
4673
|
// removed from a malformed mentions array.
|
|
4459
4674
|
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
4460
4675
|
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
4676
|
+
if (finalPayload.thread_id === '')
|
|
4677
|
+
delete finalPayload.thread_id;
|
|
4461
4678
|
const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
|
|
4462
4679
|
? normalizeAunMentionEntries(finalPayload.mentions)
|
|
4463
4680
|
: undefined;
|
|
@@ -4612,10 +4829,16 @@ export class AUNChannel {
|
|
|
4612
4829
|
return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
|
|
4613
4830
|
}
|
|
4614
4831
|
}
|
|
4832
|
+
if (operationId.startsWith(bootstrapInitialMessageOperationPrefix(this.config.aid)) && !entry.deliveryReceipt) {
|
|
4833
|
+
const config = loadAgent(this.config.aid);
|
|
4834
|
+
if (config && resolveAgentLifecycle(config) === 'active') {
|
|
4835
|
+
return { status: 'permanent', code: 'BOOTSTRAP_SUPERSEDED', error: 'Bootstrap is complete; initial prompt cancelled' };
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4615
4838
|
}
|
|
4616
4839
|
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
4617
4840
|
const source = context?.metadata?.source ?? 'daemon';
|
|
4618
|
-
const payload = { type: 'text', text: finalText };
|
|
4841
|
+
const payload = withAunAttention({ type: 'text', text: finalText }, context?.attention);
|
|
4619
4842
|
if (delivery.chatType === 'group') {
|
|
4620
4843
|
const extracted = this.extractMentionEntriesFromText(finalText);
|
|
4621
4844
|
if (extracted.length > 0)
|
|
@@ -4629,6 +4852,8 @@ export class AUNChannel {
|
|
|
4629
4852
|
payload.task_id = context.metadata.taskId;
|
|
4630
4853
|
if (context?.metadata?.chatmode)
|
|
4631
4854
|
payload.chatmode = context.metadata.chatmode;
|
|
4855
|
+
if (context?.metadata?.forkAnchor)
|
|
4856
|
+
payload.fork_anchor = context.metadata.forkAnchor;
|
|
4632
4857
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
4633
4858
|
const causationRef = delivery.chatType !== 'group' && causation
|
|
4634
4859
|
? `caus_${crypto.randomUUID().replace(/-/g, '')}`
|
|
@@ -4819,6 +5044,7 @@ export class AUNChannel {
|
|
|
4819
5044
|
queue: entry.queue,
|
|
4820
5045
|
};
|
|
4821
5046
|
const receipt = {
|
|
5047
|
+
acceptedAt: Date.now(),
|
|
4822
5048
|
messageId,
|
|
4823
5049
|
encrypt,
|
|
4824
5050
|
transport: this.sendReceiptFromResult(result),
|
|
@@ -4841,6 +5067,15 @@ export class AUNChannel {
|
|
|
4841
5067
|
logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
|
|
4842
5068
|
return { ok: true, status: 'sent' };
|
|
4843
5069
|
}
|
|
5070
|
+
if (interactionId?.startsWith('gir-')) {
|
|
5071
|
+
const result = this.groupInviteDeliveryGate?.(interactionId, entry.channelId) ?? 'stale';
|
|
5072
|
+
if (result === 'defer')
|
|
5073
|
+
return { ok: false, status: 'retry', error: 'group invitation callback is not ready', code: 'INVITE_REVIEW_NOT_READY' };
|
|
5074
|
+
if (result !== 'allow') {
|
|
5075
|
+
logger.info(`${this.logPrefix()} Discarded stale group invitation card: request=${interactionId} entry=${entry.id}`);
|
|
5076
|
+
return { ok: true, status: 'sent' };
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
4844
5079
|
const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
|
|
4845
5080
|
? entry.postSend.expiresAt
|
|
4846
5081
|
: undefined;
|
|
@@ -4988,6 +5223,8 @@ export class AUNChannel {
|
|
|
4988
5223
|
const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
|
|
4989
5224
|
? { ...finalPayload, chatmode: context.metadata.chatmode }
|
|
4990
5225
|
: finalPayload;
|
|
5226
|
+
if (payloadWithChatmode.thread_id === '')
|
|
5227
|
+
delete payloadWithChatmode.thread_id;
|
|
4991
5228
|
if (context?.threadId && !payloadWithChatmode.thread_id)
|
|
4992
5229
|
payloadWithChatmode.thread_id = context.threadId;
|
|
4993
5230
|
const params = {
|
|
@@ -5070,7 +5307,7 @@ export class AUNChannel {
|
|
|
5070
5307
|
// Validate before checking connection state so malformed producer payloads
|
|
5071
5308
|
// are reported deterministically instead of being hidden by a disconnect.
|
|
5072
5309
|
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
5073
|
-
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
5310
|
+
const finalPayload = this.stripUndefinedDeep(withAunAttention(validatedPayload, context?.attention));
|
|
5074
5311
|
if (!this.connected || !this.client) {
|
|
5075
5312
|
return {
|
|
5076
5313
|
ok: false,
|
|
@@ -5085,12 +5322,19 @@ export class AUNChannel {
|
|
|
5085
5322
|
const encrypt = context?.metadata?.encrypted != null
|
|
5086
5323
|
? !!(context.metadata.encrypted)
|
|
5087
5324
|
: this.shouldEncrypt(encryptTarget);
|
|
5325
|
+
if (finalPayload.thread_id === '')
|
|
5326
|
+
delete finalPayload.thread_id;
|
|
5088
5327
|
if (context?.threadId && !finalPayload.thread_id)
|
|
5089
5328
|
finalPayload.thread_id = context.threadId;
|
|
5090
5329
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
5091
5330
|
finalPayload.ref_message_id = context.replyToMessageId;
|
|
5092
5331
|
const isMenuPayload = typeof finalPayload.type === 'string' && finalPayload.type.startsWith('menu.');
|
|
5093
5332
|
const params = { payload: finalPayload, encrypt };
|
|
5333
|
+
if (!isGroup && finalPayload.type === 'menu.response')
|
|
5334
|
+
params.to_device_id = context?.toDeviceId;
|
|
5335
|
+
if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
|
|
5336
|
+
params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
|
|
5337
|
+
}
|
|
5094
5338
|
const rawHeaders = context?.metadata?.protectedHeaders;
|
|
5095
5339
|
if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
|
|
5096
5340
|
const protectedHeaders = {};
|
|
@@ -5156,21 +5400,23 @@ export class AUNChannel {
|
|
|
5156
5400
|
});
|
|
5157
5401
|
}
|
|
5158
5402
|
async sendFile(channelId, filePath, context) {
|
|
5403
|
+
parseAunAttention(context?.attention);
|
|
5159
5404
|
const delivery = this.requireDelivery(channelId, context);
|
|
5160
5405
|
context = this.withDelivery(context, delivery);
|
|
5406
|
+
const isFetch = typeof context?.metadata?.correlationId === 'string' && !!context.metadata.correlationId;
|
|
5161
5407
|
const absPath = path.resolve(filePath);
|
|
5162
5408
|
if (!fs.existsSync(absPath)) {
|
|
5163
5409
|
logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
|
|
5164
|
-
return;
|
|
5410
|
+
return { status: 'failed', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
|
|
5165
5411
|
}
|
|
5166
5412
|
const stat = fs.statSync(absPath);
|
|
5167
5413
|
if (stat.size === 0) {
|
|
5168
5414
|
logger.warn(`${this.logPrefix()} sendFile: file is empty`);
|
|
5169
|
-
return;
|
|
5415
|
+
return { status: 'failed', error: 'file is empty', code: 'EMPTY_FILE' };
|
|
5170
5416
|
}
|
|
5171
5417
|
if (stat.size > 10 * 1024 * 1024) {
|
|
5172
5418
|
logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
|
|
5173
|
-
return;
|
|
5419
|
+
return { status: 'failed', error: `file too large (max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`, code: 'FILE_TOO_LARGE' };
|
|
5174
5420
|
}
|
|
5175
5421
|
// Write-ahead: persist to outbox
|
|
5176
5422
|
const entry = outbox.enqueue(this.config.aid, {
|
|
@@ -5188,17 +5434,47 @@ export class AUNChannel {
|
|
|
5188
5434
|
if (!this.reconnectTimer && !this.client) {
|
|
5189
5435
|
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendFile failed: ${e}`));
|
|
5190
5436
|
}
|
|
5191
|
-
|
|
5437
|
+
const error = 'AUN channel is not connected';
|
|
5438
|
+
if (isFetch) {
|
|
5439
|
+
this.markPermanentOutboxFailure(entry, { error, code: 'AUN_NOT_CONNECTED' });
|
|
5440
|
+
return { status: 'failed', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
|
|
5441
|
+
}
|
|
5442
|
+
return { status: 'queued', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
|
|
5192
5443
|
}
|
|
5193
5444
|
const result = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
5194
5445
|
if (result.status === 'sent') {
|
|
5446
|
+
const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
|
|
5447
|
+
if (!messageId) {
|
|
5448
|
+
const error = 'file send returned no message_id';
|
|
5449
|
+
this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
|
|
5450
|
+
return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
|
|
5451
|
+
}
|
|
5195
5452
|
this.removeDeliveredOutboxEntry(entry);
|
|
5453
|
+
return { status: 'sent', messageId };
|
|
5196
5454
|
}
|
|
5197
|
-
|
|
5455
|
+
// Remote acceptance is authoritative even if local bookkeeping needs retry.
|
|
5456
|
+
if (entry.deliveryReceipt?.messageId)
|
|
5457
|
+
return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
|
|
5458
|
+
if (result.status === 'permanent' || isFetch) {
|
|
5459
|
+
// A correlated fetch must resolve promptly. Retrying it after reporting
|
|
5460
|
+
// failure could deliver an unsolicited file for an already closed request.
|
|
5198
5461
|
this.markPermanentOutboxFailure(entry, result);
|
|
5462
|
+
return {
|
|
5463
|
+
status: 'failed',
|
|
5464
|
+
outboxId: entry.id,
|
|
5465
|
+
error: result.error ?? 'permanent file send failure',
|
|
5466
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5467
|
+
};
|
|
5199
5468
|
}
|
|
5469
|
+
return {
|
|
5470
|
+
status: 'queued',
|
|
5471
|
+
outboxId: entry.id,
|
|
5472
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
5473
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5474
|
+
};
|
|
5200
5475
|
}
|
|
5201
5476
|
async sendImage(channelId, data, mimeType, alt, context) {
|
|
5477
|
+
parseAunAttention(context?.attention);
|
|
5202
5478
|
const delivery = this.requireDelivery(channelId, context);
|
|
5203
5479
|
context = this.withDelivery(context, delivery);
|
|
5204
5480
|
if (!Buffer.isBuffer(data)) {
|
|
@@ -5268,11 +5544,7 @@ export class AUNChannel {
|
|
|
5268
5544
|
}
|
|
5269
5545
|
const digest = crypto.createHash('sha256').update(data).digest('hex').slice(0, 12);
|
|
5270
5546
|
const filename = `image-${digest}.${extension}`;
|
|
5271
|
-
const
|
|
5272
|
-
call: (method, params) => this.callAndTrace(method, params),
|
|
5273
|
-
close: async () => { },
|
|
5274
|
-
};
|
|
5275
|
-
const built = await uploadBufferAndBuildPayload(connection, this.config.aid, data, filename, { as: 'image', contentType });
|
|
5547
|
+
const built = await uploadBufferAndBuildPayload(this.client, this.config.aid, data, filename, { as: 'image', contentType });
|
|
5276
5548
|
const imagePayload = { ...built.payload };
|
|
5277
5549
|
setIfDefined(imagePayload, 'alt', alt);
|
|
5278
5550
|
return imagePayload;
|
|
@@ -5359,57 +5631,14 @@ export class AUNChannel {
|
|
|
5359
5631
|
logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
|
|
5360
5632
|
return { status: 'permanent', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
|
|
5361
5633
|
}
|
|
5362
|
-
const filename = path.basename(absPath);
|
|
5363
|
-
const fileData = fs.readFileSync(absPath);
|
|
5364
|
-
const stat = fs.statSync(absPath);
|
|
5365
|
-
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
|
|
5366
|
-
const contentType = guessMime(filename);
|
|
5367
|
-
const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
|
|
5368
5634
|
try {
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
object_key: objectKey,
|
|
5372
|
-
content: fileData.toString('base64'),
|
|
5373
|
-
content_type: contentType,
|
|
5374
|
-
is_private: false,
|
|
5375
|
-
overwrite: true,
|
|
5376
|
-
});
|
|
5377
|
-
}
|
|
5378
|
-
else {
|
|
5379
|
-
const session = await this.callAndTrace('storage.create_upload_session', {
|
|
5380
|
-
object_key: objectKey,
|
|
5381
|
-
size_bytes: stat.size,
|
|
5382
|
-
content_type: contentType,
|
|
5383
|
-
});
|
|
5384
|
-
const uploadUrl = session.upload_url;
|
|
5385
|
-
if (!uploadUrl)
|
|
5386
|
-
throw new Error('No upload_url in session response');
|
|
5387
|
-
this.trace('OUT', 'http.put.upload_url', { object_key: objectKey, size: stat.size });
|
|
5388
|
-
const uploadResp = await fetch(uploadUrl, { method: 'PUT', body: fileData });
|
|
5389
|
-
this.trace('OUT', uploadResp.ok ? 'http.put.upload_url.ok' : 'http.put.upload_url.error', { status: uploadResp.status });
|
|
5390
|
-
if (!uploadResp.ok)
|
|
5391
|
-
throw new Error(`HTTP upload failed: ${uploadResp.status}`);
|
|
5392
|
-
await this.callAndTrace('storage.complete_upload', {
|
|
5393
|
-
object_key: objectKey,
|
|
5394
|
-
sha256,
|
|
5395
|
-
content_type: contentType,
|
|
5396
|
-
is_private: false,
|
|
5397
|
-
size_bytes: stat.size,
|
|
5398
|
-
});
|
|
5399
|
-
}
|
|
5400
|
-
const attachment = {
|
|
5401
|
-
owner_aid: this._aid || '',
|
|
5402
|
-
object_key: objectKey,
|
|
5403
|
-
filename,
|
|
5404
|
-
size_bytes: stat.size,
|
|
5405
|
-
sha256,
|
|
5406
|
-
content_type: contentType,
|
|
5407
|
-
};
|
|
5635
|
+
const { attachment } = await uploadFileAndBuildPayload(this.client, this.config.aid, absPath, { as: 'file' });
|
|
5636
|
+
const { filename, size_bytes: size, content_type: contentType } = attachment;
|
|
5408
5637
|
const filePayload = buildAunFilePayload({
|
|
5409
5638
|
filename,
|
|
5410
|
-
size
|
|
5639
|
+
size,
|
|
5411
5640
|
contentType,
|
|
5412
|
-
attachment,
|
|
5641
|
+
attachment: { ...attachment },
|
|
5413
5642
|
context,
|
|
5414
5643
|
});
|
|
5415
5644
|
const fileTargetAid = channelId;
|
|
@@ -5495,7 +5724,7 @@ export class AUNChannel {
|
|
|
5495
5724
|
logger.error(`${this.logPrefix()} AUN accepted the durable file but its receipt could not be persisted: id=${entry.id} mid=${sentMid} error=${detail}`);
|
|
5496
5725
|
return { status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
|
|
5497
5726
|
}
|
|
5498
|
-
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(
|
|
5727
|
+
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(size)}) → ${channelId}`);
|
|
5499
5728
|
const fileText = filePayload.text;
|
|
5500
5729
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: sentMid, kind: 'file', len: fileText.length, ...(isGroup && { groupId: channelId }) });
|
|
5501
5730
|
const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
|
|
@@ -5614,11 +5843,12 @@ export class AUNChannel {
|
|
|
5614
5843
|
// corrected Owner route was delivered.
|
|
5615
5844
|
if (entry.channelId !== owner || entry.delivery?.chatType !== 'private') {
|
|
5616
5845
|
delete repaired.deliveryReceipt;
|
|
5846
|
+
delete repaired.deliveryResult;
|
|
5847
|
+
delete repaired.terminal;
|
|
5848
|
+
delete repaired.lastError;
|
|
5849
|
+
delete repaired.lastErrorCode;
|
|
5850
|
+
delete repaired.attempts;
|
|
5617
5851
|
}
|
|
5618
|
-
delete repaired.terminal;
|
|
5619
|
-
delete repaired.lastError;
|
|
5620
|
-
delete repaired.lastErrorCode;
|
|
5621
|
-
delete repaired.attempts;
|
|
5622
5852
|
if (outbox.replaceIfRouteMatches(aid, entry, repaired) !== 'replaced')
|
|
5623
5853
|
continue;
|
|
5624
5854
|
logger.warn(`${this.logPrefix()} Repaired bootstrap outbox route to private Owner delivery: entry=${entry.id} owner=${owner}`);
|
|
@@ -5658,17 +5888,21 @@ export class AUNChannel {
|
|
|
5658
5888
|
notice: 'notice',
|
|
5659
5889
|
requires_action: 'requires_action',
|
|
5660
5890
|
};
|
|
5661
|
-
const statusPayload = {
|
|
5891
|
+
const statusPayload = withAunAttention({
|
|
5662
5892
|
type: 'task.status',
|
|
5663
5893
|
status: stateMap[status] ?? status,
|
|
5664
5894
|
task_id: taskId,
|
|
5665
5895
|
session_id: sessionId,
|
|
5666
5896
|
severity,
|
|
5667
|
-
|
|
5668
|
-
};
|
|
5897
|
+
});
|
|
5669
5898
|
const metadataInput = extraMeta ? { ...extraMeta } : undefined;
|
|
5670
5899
|
if (metadataInput)
|
|
5671
5900
|
delete metadataInput.severity;
|
|
5901
|
+
const statusKind = typeof extraMeta?.kind === 'string' ? extraMeta.kind : undefined;
|
|
5902
|
+
if (statusKind)
|
|
5903
|
+
statusPayload.kind = statusKind;
|
|
5904
|
+
if (metadataInput)
|
|
5905
|
+
delete metadataInput.kind;
|
|
5672
5906
|
const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
|
|
5673
5907
|
if (metadata && Object.keys(metadata).length > 0)
|
|
5674
5908
|
statusPayload.metadata = metadata;
|
|
@@ -5721,10 +5955,6 @@ export class AUNChannel {
|
|
|
5721
5955
|
// returns until the channel reconnects.
|
|
5722
5956
|
const delivery = this.requireDelivery(channelId, context);
|
|
5723
5957
|
context = this.withDelivery(context, delivery);
|
|
5724
|
-
if (!this.connected || !this.client) {
|
|
5725
|
-
logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
|
|
5726
|
-
return;
|
|
5727
|
-
}
|
|
5728
5958
|
// SDK 0.3.0 E2EE requires payload to be an object
|
|
5729
5959
|
let payloadObj;
|
|
5730
5960
|
try {
|
|
@@ -5735,6 +5965,16 @@ export class AUNChannel {
|
|
|
5735
5965
|
catch {
|
|
5736
5966
|
payloadObj = { text: payload };
|
|
5737
5967
|
}
|
|
5968
|
+
// File fetch was already acknowledged. Its terminal error must survive a
|
|
5969
|
+
// disconnect so the pending App request is not silently abandoned.
|
|
5970
|
+
if (payloadObj.type === 'menu.response' && payloadObj.name === 'file' && payloadObj.error) {
|
|
5971
|
+
await this.sendReliableStructured(channelId, payloadObj, context);
|
|
5972
|
+
return;
|
|
5973
|
+
}
|
|
5974
|
+
if (!this.connected || !this.client) {
|
|
5975
|
+
logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5738
5978
|
await this.sendStructuredOrThrow(channelId, payloadObj, context);
|
|
5739
5979
|
}
|
|
5740
5980
|
async disconnect() {
|
|
@@ -5814,15 +6054,15 @@ export class AUNChannel {
|
|
|
5814
6054
|
};
|
|
5815
6055
|
}
|
|
5816
6056
|
/** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
|
|
5817
|
-
loadSelfName(aid) {
|
|
6057
|
+
loadSelfName(aid, aunPath) {
|
|
5818
6058
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
5819
|
-
const name = resolveAgentDisplayName(aidName);
|
|
6059
|
+
const name = resolveAgentDisplayName(aidName, { aunPath });
|
|
5820
6060
|
if (!name)
|
|
5821
|
-
void this.fetchAndCacheSelfName(aidName);
|
|
6061
|
+
void this.fetchAndCacheSelfName(aidName, aunPath);
|
|
5822
6062
|
return name;
|
|
5823
6063
|
}
|
|
5824
|
-
async fetchAndCacheSelfName(aidName) {
|
|
5825
|
-
const name = await refreshAgentDisplayName(aidName);
|
|
6064
|
+
async fetchAndCacheSelfName(aidName, aunPath) {
|
|
6065
|
+
const name = await refreshAgentDisplayName(aidName, { aunPath });
|
|
5826
6066
|
if (name) {
|
|
5827
6067
|
this._selfName = name;
|
|
5828
6068
|
if (this.aidStatsCollector)
|
|
@@ -5839,13 +6079,20 @@ export class AUNChannel {
|
|
|
5839
6079
|
if (!this.client)
|
|
5840
6080
|
return { type: null };
|
|
5841
6081
|
try {
|
|
6082
|
+
const client = this.client;
|
|
6083
|
+
const revision = this.peerInfoRevision;
|
|
5842
6084
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
5843
|
-
const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
|
|
6085
|
+
const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
5844
6086
|
const type = identity.type === 'human' ? 'human' : 'agent';
|
|
5845
6087
|
const name = identity.name || undefined;
|
|
5846
6088
|
const info = { type, name };
|
|
5847
|
-
this.
|
|
5848
|
-
|
|
6089
|
+
if (identity.lastCheckedAt !== 0 && this.client === client && this.peerInfoRevision === revision) {
|
|
6090
|
+
this.peerInfoCache.set(aid, info);
|
|
6091
|
+
setTimeout(() => {
|
|
6092
|
+
if (this.peerInfoCache.get(aid) === info)
|
|
6093
|
+
this.peerInfoCache.delete(aid);
|
|
6094
|
+
}, 30 * 60 * 1000);
|
|
6095
|
+
}
|
|
5849
6096
|
return info;
|
|
5850
6097
|
}
|
|
5851
6098
|
catch (e) {
|
|
@@ -5867,13 +6114,20 @@ export class AUNChannel {
|
|
|
5867
6114
|
if (!this.store)
|
|
5868
6115
|
throw new Error('not connected');
|
|
5869
6116
|
const { agentmdPut } = await import('../aun/aid/agentmd.js');
|
|
5870
|
-
await agentmdPut(content, {
|
|
6117
|
+
await agentmdPut(content, {
|
|
6118
|
+
aid: this.config.aid,
|
|
6119
|
+
store: this.store,
|
|
6120
|
+
aunPath: this.config.keystorePath || resolveRoot(),
|
|
6121
|
+
});
|
|
5871
6122
|
}
|
|
5872
6123
|
async downloadAgentMd(aid) {
|
|
5873
6124
|
if (!this.store)
|
|
5874
6125
|
throw new Error('not connected');
|
|
5875
6126
|
const { agentmdSync } = await import('../aun/aid/agentmd.js');
|
|
5876
|
-
const result = await agentmdSync(aid, {
|
|
6127
|
+
const result = await agentmdSync(aid, {
|
|
6128
|
+
store: this.store ?? undefined,
|
|
6129
|
+
aunPath: this.config.keystorePath || resolveRoot(),
|
|
6130
|
+
});
|
|
5877
6131
|
return result.content ?? '';
|
|
5878
6132
|
}
|
|
5879
6133
|
/**
|
|
@@ -5972,7 +6226,7 @@ export class AUNChannelPlugin {
|
|
|
5972
6226
|
const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
|
|
5973
6227
|
? undefined
|
|
5974
6228
|
: operationId;
|
|
5975
|
-
|
|
6229
|
+
let replyCtx = outboundCausation
|
|
5976
6230
|
? {
|
|
5977
6231
|
...(envelope.replyContext ?? {}),
|
|
5978
6232
|
delivery,
|
|
@@ -5981,6 +6235,13 @@ export class AUNChannelPlugin {
|
|
|
5981
6235
|
: (envelope.replyContext || envelope.delivery)
|
|
5982
6236
|
? { ...(envelope.replyContext ?? {}), delivery }
|
|
5983
6237
|
: undefined;
|
|
6238
|
+
const attention = aunOutboundAttention(payload);
|
|
6239
|
+
if (replyCtx) {
|
|
6240
|
+
replyCtx = { ...replyCtx };
|
|
6241
|
+
delete replyCtx.attention;
|
|
6242
|
+
}
|
|
6243
|
+
if (attention !== undefined)
|
|
6244
|
+
replyCtx = { ...(replyCtx ?? {}), attention };
|
|
5984
6245
|
if (outboundCausation) {
|
|
5985
6246
|
recordCausationSpan(outboundCausation, 'message.outbound', {
|
|
5986
6247
|
status: 'started',
|
|
@@ -6003,7 +6264,7 @@ export class AUNChannelPlugin {
|
|
|
6003
6264
|
subtype: payload.kind,
|
|
6004
6265
|
text: payload.text,
|
|
6005
6266
|
...(isError
|
|
6006
|
-
? { message: payload.text, user_message: payload.text, recoverable: true
|
|
6267
|
+
? { message: payload.text, user_message: payload.text, recoverable: true }
|
|
6007
6268
|
: { severity: 'info' }),
|
|
6008
6269
|
};
|
|
6009
6270
|
for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
|
|
@@ -6018,6 +6279,8 @@ export class AUNChannelPlugin {
|
|
|
6018
6279
|
...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
|
|
6019
6280
|
.filter(([key]) => key !== 'operationId')),
|
|
6020
6281
|
...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
|
|
6282
|
+
...(payload.kind === 'result.text' && payload.metadata?.forkAnchor
|
|
6283
|
+
? { forkAnchor: payload.metadata.forkAnchor } : {}),
|
|
6021
6284
|
},
|
|
6022
6285
|
};
|
|
6023
6286
|
if (payload.kind === 'result.text' && payload.isFinal)
|
|
@@ -6045,7 +6308,7 @@ export class AUNChannelPlugin {
|
|
|
6045
6308
|
messages: [{
|
|
6046
6309
|
messageId: result.messageId,
|
|
6047
6310
|
partIndex: 0,
|
|
6048
|
-
...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
|
|
6311
|
+
...(typeof sendCtx.threadId === 'string' ? { threadId: sendCtx.threadId } : {}),
|
|
6049
6312
|
}],
|
|
6050
6313
|
};
|
|
6051
6314
|
}
|
|
@@ -6081,14 +6344,13 @@ export class AUNChannelPlugin {
|
|
|
6081
6344
|
}
|
|
6082
6345
|
case 'system.error': {
|
|
6083
6346
|
const errorPayload = {
|
|
6084
|
-
type: 'error',
|
|
6347
|
+
type: payload.recoverable === true ? 'notice' : 'error',
|
|
6085
6348
|
...taskBase(),
|
|
6086
6349
|
subtype: payload.subtype,
|
|
6087
6350
|
text: payload.text,
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
terminal: payload.recoverable !== true,
|
|
6351
|
+
...(payload.recoverable === true
|
|
6352
|
+
? { severity: 'warn' }
|
|
6353
|
+
: { message: payload.text, user_message: payload.text, recoverable: false }),
|
|
6092
6354
|
};
|
|
6093
6355
|
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
6094
6356
|
return;
|
|
@@ -6101,7 +6363,6 @@ export class AUNChannelPlugin {
|
|
|
6101
6363
|
text: payload.text,
|
|
6102
6364
|
message: payload.text,
|
|
6103
6365
|
user_message: payload.text,
|
|
6104
|
-
terminal: true,
|
|
6105
6366
|
};
|
|
6106
6367
|
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
6107
6368
|
return;
|
|
@@ -6110,8 +6371,27 @@ export class AUNChannelPlugin {
|
|
|
6110
6371
|
const fileCtx = payload.correlationId
|
|
6111
6372
|
? { ...(replyCtx ?? {}), metadata: { ...(replyCtx?.metadata ?? {}), correlationId: payload.correlationId } }
|
|
6112
6373
|
: replyCtx;
|
|
6113
|
-
await channel.sendFile(channelId, payload.filePath, fileCtx);
|
|
6114
|
-
|
|
6374
|
+
const result = await channel.sendFile(channelId, payload.filePath, fileCtx);
|
|
6375
|
+
if (result.status === 'sent')
|
|
6376
|
+
return sentReceipt(envelope, [result.messageId], fileCtx?.threadId);
|
|
6377
|
+
if (result.status === 'queued') {
|
|
6378
|
+
return {
|
|
6379
|
+
status: 'queued',
|
|
6380
|
+
operationId,
|
|
6381
|
+
messages: [],
|
|
6382
|
+
outboxId: result.outboxId,
|
|
6383
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
6384
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
6385
|
+
};
|
|
6386
|
+
}
|
|
6387
|
+
return {
|
|
6388
|
+
status: 'failed',
|
|
6389
|
+
operationId,
|
|
6390
|
+
messages: [],
|
|
6391
|
+
...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
|
|
6392
|
+
error: result.error,
|
|
6393
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
6394
|
+
};
|
|
6115
6395
|
}
|
|
6116
6396
|
case 'result.image': {
|
|
6117
6397
|
await channel.sendImage(channelId, payload.data, payload.mimeType, payload.alt, replyCtx);
|
|
@@ -6255,13 +6535,13 @@ export class AUNChannelPlugin {
|
|
|
6255
6535
|
};
|
|
6256
6536
|
if (req.kind.kind === 'action') {
|
|
6257
6537
|
const action = req.kind;
|
|
6258
|
-
const aunCard = {
|
|
6538
|
+
const aunCard = withAunAttention({
|
|
6259
6539
|
type: 'action_card',
|
|
6260
6540
|
title: action.title,
|
|
6261
6541
|
actions: action.buttons.map(btn => ({
|
|
6262
6542
|
label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
|
|
6263
6543
|
})),
|
|
6264
|
-
};
|
|
6544
|
+
});
|
|
6265
6545
|
// AUN action_card 以 compose behavior 声明自由文本动作。新版客户端
|
|
6266
6546
|
// 在卡片内采集并确认;旧版客户端可继续从主编辑器采集。两者都结构化
|
|
6267
6547
|
// 回传 action_card_reply{behavior:"compose"},Core 不拦截普通消息队列。
|
|
@@ -6279,6 +6559,7 @@ export class AUNChannelPlugin {
|
|
|
6279
6559
|
const result = await channel.sendContentPayload(channelId, aunCard, {
|
|
6280
6560
|
contentKind: 'card',
|
|
6281
6561
|
context: replyCtx,
|
|
6562
|
+
dedupeKey: req.id.startsWith('gir-') ? operationId : undefined,
|
|
6282
6563
|
logText: action.title ? `[card] ${action.title}` : '[card]',
|
|
6283
6564
|
postSend: {
|
|
6284
6565
|
type: 'register_interaction_card',
|
|
@@ -6293,7 +6574,7 @@ export class AUNChannelPlugin {
|
|
|
6293
6574
|
}
|
|
6294
6575
|
else if (req.kind.kind === 'command-card') {
|
|
6295
6576
|
const card = req.kind;
|
|
6296
|
-
const aunCard = {
|
|
6577
|
+
const aunCard = withAunAttention({
|
|
6297
6578
|
type: 'action_card',
|
|
6298
6579
|
title: card.title,
|
|
6299
6580
|
actions: card.buttons.map(btn => {
|
|
@@ -6306,7 +6587,7 @@ export class AUNChannelPlugin {
|
|
|
6306
6587
|
setIfDefined(action, 'disabled', btn.disabled);
|
|
6307
6588
|
return action;
|
|
6308
6589
|
}),
|
|
6309
|
-
};
|
|
6590
|
+
});
|
|
6310
6591
|
if (card.body) {
|
|
6311
6592
|
aunCard.text = card.body;
|
|
6312
6593
|
setIfDefined(aunCard, 'format', card.bodyFormat);
|
|
@@ -6397,6 +6678,14 @@ export class AUNChannelPlugin {
|
|
|
6397
6678
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
6398
6679
|
handler(aunOptsToInbound(opts, adapter.channelName, channelType));
|
|
6399
6680
|
}), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
|
|
6681
|
+
bridge.registerGroupInviteReviewChannel({
|
|
6682
|
+
selfAid: aid,
|
|
6683
|
+
channelName: adapter.channelName,
|
|
6684
|
+
channelKey: adapter.channelKey,
|
|
6685
|
+
agentName: ctx.agentName,
|
|
6686
|
+
channel,
|
|
6687
|
+
adapter,
|
|
6688
|
+
});
|
|
6400
6689
|
},
|
|
6401
6690
|
registerHooks(hookCtx) {
|
|
6402
6691
|
channel.setEventBus(hookCtx.eventBus);
|