evolcore 0.0.26 → 0.0.28
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 +36 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/baseagent.js +10 -5
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +70 -26
- package/dist/agents/codex/catalog.js +7 -1
- package/dist/agents/codex-runner.js +363 -421
- 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} +13 -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 +544 -239
- 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 +111 -162
- package/dist/cli/diag.js +3 -3
- 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 +13 -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 +81 -19
- 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 +105 -40
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/diag-service.js +292 -27
- 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 +364 -96
- package/dist/core/command/menu-protocol.js +43 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +61 -37
- 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 +94 -16
- package/dist/core/message/message-bridge.js +140 -13
- 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 +729 -118
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +76 -36
- 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 +8 -8
- 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 +38 -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 +322 -207
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +3 -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/atomic-write.js +98 -0
- package/dist/utils/cross-platform.js +87 -16
- 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/npm-ops.js +119 -4
- package/dist/utils/package-version-info.js +244 -0
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/dist/utils/windows-shell-trust.js +37 -0
- 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 +23 -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;
|
|
@@ -628,7 +715,9 @@ export class AUNChannel {
|
|
|
628
715
|
* 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
|
|
629
716
|
* 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
|
|
630
717
|
*/
|
|
631
|
-
callClient(method, params, lane = 'normal', onStart) {
|
|
718
|
+
callClient(method, params, lane = 'normal', onStart, onPrepared) {
|
|
719
|
+
params = menuTransportParams(method, params);
|
|
720
|
+
onPrepared?.(params);
|
|
632
721
|
return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)), lane, onStart);
|
|
633
722
|
}
|
|
634
723
|
withOutboundSendGate(method, run, lane = 'normal', onStart) {
|
|
@@ -696,7 +785,6 @@ export class AUNChannel {
|
|
|
696
785
|
});
|
|
697
786
|
}
|
|
698
787
|
async callAndTrace(method, params, opts) {
|
|
699
|
-
this.trace('OUT', method, params);
|
|
700
788
|
// 计时同时覆盖排队和 SDK 往返;callStartedAt 让日志能区分「本地队列堵塞」
|
|
701
789
|
// 与「网关慢」。两者对 /evolhelp 等命令的回复延迟都有直接影响。
|
|
702
790
|
const rpcStart = Date.now();
|
|
@@ -706,6 +794,11 @@ export class AUNChannel {
|
|
|
706
794
|
try {
|
|
707
795
|
const result = await this.callClient(method, params, opts?.lane ?? 'normal', () => {
|
|
708
796
|
callStartedAt = Date.now();
|
|
797
|
+
}, preparedParams => {
|
|
798
|
+
// Apply Menu compatibility exactly once, then log the same parameters
|
|
799
|
+
// passed to the SDK (TTL and device targeting may both be removed).
|
|
800
|
+
params = preparedParams;
|
|
801
|
+
this.trace('OUT', method, params);
|
|
709
802
|
});
|
|
710
803
|
const durationMs = Date.now() - rpcStart;
|
|
711
804
|
const rpcDurationMs = Date.now() - callStartedAt;
|
|
@@ -756,14 +849,17 @@ export class AUNChannel {
|
|
|
756
849
|
state.maxDurationMs = 0;
|
|
757
850
|
this.rpcWarningStates.set(key, state);
|
|
758
851
|
const summary = suppressed > 0 ? ` suppressed=${suppressed} max_duration=${maxDurationMs}ms` : '';
|
|
852
|
+
const menuTransport = method === 'message.send' && payloadType.startsWith('menu.')
|
|
853
|
+
? ` ttl=${params.ttl ?? 'default'} to_device_id=${JSON.stringify(params.to_device_id ?? null)}`
|
|
854
|
+
: '';
|
|
759
855
|
if (kind === 'failed') {
|
|
760
856
|
const name = error instanceof Error ? error.name : '';
|
|
761
857
|
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
762
858
|
const errorLabel = `${name}${code ? `(${code})` : ''} ${message}`.trim();
|
|
763
|
-
logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${errorLabel} queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary}`);
|
|
859
|
+
logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${errorLabel} queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${menuTransport}${summary}`);
|
|
764
860
|
return;
|
|
765
861
|
}
|
|
766
|
-
logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary} (发送管线可能饱和或链路抖动)`);
|
|
862
|
+
logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${menuTransport}${summary} (发送管线可能饱和或链路抖动)`);
|
|
767
863
|
}
|
|
768
864
|
observerQueueDropCount = 0;
|
|
769
865
|
observerQueueDropLastWarnAt = 0;
|
|
@@ -838,7 +934,7 @@ export class AUNChannel {
|
|
|
838
934
|
return;
|
|
839
935
|
}
|
|
840
936
|
const context = { delivery };
|
|
841
|
-
if (threadId)
|
|
937
|
+
if (typeof threadId === 'string')
|
|
842
938
|
context.threadId = threadId;
|
|
843
939
|
if (operatorId && delivery.chatType === 'group')
|
|
844
940
|
context.peerId = operatorId;
|
|
@@ -933,7 +1029,7 @@ export class AUNChannel {
|
|
|
933
1029
|
threadId,
|
|
934
1030
|
replyContext: {
|
|
935
1031
|
...(cardInfo.replyContext ?? {}),
|
|
936
|
-
...(threadId ? { threadId } : {}),
|
|
1032
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
937
1033
|
...(cardDelivery ? { delivery: cardDelivery } : {}),
|
|
938
1034
|
},
|
|
939
1035
|
source: 'card-trigger',
|
|
@@ -1295,14 +1391,14 @@ export class AUNChannel {
|
|
|
1295
1391
|
return sanitized;
|
|
1296
1392
|
return undefined;
|
|
1297
1393
|
}
|
|
1298
|
-
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
|
|
1394
|
+
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, lable) {
|
|
1299
1395
|
if (!groupId)
|
|
1300
1396
|
throw new Error('buildGroupReplyContext requires groupId');
|
|
1301
1397
|
const replyContext = {
|
|
1302
1398
|
metadata: { encrypted, chatmode },
|
|
1303
1399
|
delivery: { chatType: 'group', groupId },
|
|
1304
1400
|
};
|
|
1305
|
-
if (threadId)
|
|
1401
|
+
if (typeof threadId === 'string')
|
|
1306
1402
|
replyContext.threadId = threadId;
|
|
1307
1403
|
replyContext.peerId = senderAid;
|
|
1308
1404
|
if (messageId)
|
|
@@ -1313,20 +1409,24 @@ export class AUNChannel {
|
|
|
1313
1409
|
replyContext.metadata = {};
|
|
1314
1410
|
replyContext.metadata.topicName = topicName;
|
|
1315
1411
|
}
|
|
1412
|
+
if (lable && threadId)
|
|
1413
|
+
replyContext.metadata.lable = lable;
|
|
1316
1414
|
return replyContext;
|
|
1317
1415
|
}
|
|
1318
|
-
buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
|
|
1416
|
+
buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, lable) {
|
|
1319
1417
|
const replyContext = {
|
|
1320
1418
|
metadata: { encrypted, chatmode, refMessageId },
|
|
1321
1419
|
delivery: { chatType: 'private' },
|
|
1322
1420
|
peerId: senderAid,
|
|
1323
1421
|
};
|
|
1324
|
-
if (threadId)
|
|
1422
|
+
if (typeof threadId === 'string')
|
|
1325
1423
|
replyContext.threadId = threadId;
|
|
1326
1424
|
if (messageId)
|
|
1327
1425
|
replyContext.replyToMessageId = messageId;
|
|
1328
1426
|
if (topicName && threadId)
|
|
1329
1427
|
replyContext.metadata.topicName = topicName;
|
|
1428
|
+
if (lable && threadId)
|
|
1429
|
+
replyContext.metadata.lable = lable;
|
|
1330
1430
|
return replyContext;
|
|
1331
1431
|
}
|
|
1332
1432
|
claimInboundMessage(kind, messageId, seq) {
|
|
@@ -1443,6 +1543,7 @@ export class AUNChannel {
|
|
|
1443
1543
|
invalidInboundEnvelopeLastLogAt = 0;
|
|
1444
1544
|
groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
|
|
1445
1545
|
peerInfoCache = new Map();
|
|
1546
|
+
peerInfoRevision = 0;
|
|
1446
1547
|
messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
|
|
1447
1548
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
1448
1549
|
peerE2ee = new Map();
|
|
@@ -1557,6 +1658,44 @@ export class AUNChannel {
|
|
|
1557
1658
|
getAid() {
|
|
1558
1659
|
return this._aid ?? this.config.aid;
|
|
1559
1660
|
}
|
|
1661
|
+
setGroupInviteReviewHandlers(handlers) {
|
|
1662
|
+
if (this.config.pureIdentity && handlers)
|
|
1663
|
+
return;
|
|
1664
|
+
this.groupInviteHandler = handlers?.onInvite;
|
|
1665
|
+
this.groupInviteConnectedHandler = handlers?.onConnected;
|
|
1666
|
+
this.groupInviteDeliveryGate = handlers?.canDeliver;
|
|
1667
|
+
this.groupInviteDelivered = handlers?.onDelivered;
|
|
1668
|
+
}
|
|
1669
|
+
async getGroupInviteSummary(groupId) {
|
|
1670
|
+
if (!this.client || !this.connected)
|
|
1671
|
+
return {};
|
|
1672
|
+
try {
|
|
1673
|
+
const result = await this.callAndTrace('group.get_info', { group_id: groupId });
|
|
1674
|
+
const group = result?.group ?? result;
|
|
1675
|
+
return {
|
|
1676
|
+
...(typeof group?.name === 'string' ? { name: group.name.slice(0, 120) } : {}),
|
|
1677
|
+
...(typeof group?.description === 'string' ? { description: group.description.slice(0, 240) } : {}),
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
catch {
|
|
1681
|
+
return {};
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
getGroupInviteDeliveryState(outboxId) {
|
|
1685
|
+
return outbox.deliveryState(this.config.aid, outboxId);
|
|
1686
|
+
}
|
|
1687
|
+
async invalidateGroupInviteCard(interactionId, ownerAid, messageIds) {
|
|
1688
|
+
const known = new Set(this.interactionCardMessageIds.get(interactionId) ?? []);
|
|
1689
|
+
await this.invalidateInteraction(interactionId, 'group_invite_review');
|
|
1690
|
+
// After a daemon restart only the review store knows the delivered cards.
|
|
1691
|
+
for (const id of messageIds) {
|
|
1692
|
+
if (!known.has(id))
|
|
1693
|
+
await this.recallInteractionCard(id, ownerAid, { chatType: 'private' });
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
refreshGroupInviteMembership(groupId) {
|
|
1697
|
+
this.invalidateObserverGroupMembers(groupId);
|
|
1698
|
+
}
|
|
1560
1699
|
/** 是否已连接(client 存在且认证完成)。 */
|
|
1561
1700
|
isConnected() {
|
|
1562
1701
|
return this.connected && this.client !== null;
|
|
@@ -1667,6 +1806,33 @@ export class AUNChannel {
|
|
|
1667
1806
|
// instance must not mutate the new instance's health or message state.
|
|
1668
1807
|
const isCurrentClient = () => this.client === client;
|
|
1669
1808
|
// Register event handlers before connecting
|
|
1809
|
+
client.on('agentmd.changed', (data) => {
|
|
1810
|
+
if (!isCurrentClient() || this.intentionalDisconnect || typeof data !== 'string')
|
|
1811
|
+
return;
|
|
1812
|
+
let aid;
|
|
1813
|
+
try {
|
|
1814
|
+
aid = validateAIDFormat(data);
|
|
1815
|
+
}
|
|
1816
|
+
catch {
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
this.peerInfoRevision++;
|
|
1820
|
+
this.peerInfoCache.delete(aid);
|
|
1821
|
+
this.groupNameCache.delete(aid);
|
|
1822
|
+
invalidateAgentDisplayName(aid);
|
|
1823
|
+
PeerIdentityCache.invalidate('aun', aid, path.join(resolvePaths().agentsDir, this.config.aid));
|
|
1824
|
+
this.trace('IN', 'agentmd.changed', { aid });
|
|
1825
|
+
// Peer snapshots refresh lazily on the next lookup. The self name is
|
|
1826
|
+
// held separately by this channel, so refresh it with the existing API.
|
|
1827
|
+
if (aid === (this._aid || this.config.aid)) {
|
|
1828
|
+
void refreshAgentDisplayName(aid).then(name => {
|
|
1829
|
+
if (!name || !isCurrentClient() || this.intentionalDisconnect)
|
|
1830
|
+
return;
|
|
1831
|
+
this._selfName = name;
|
|
1832
|
+
this.aidStatsCollector?.setSelfName(this.config.aid, name);
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1670
1836
|
client.on('message.received', async (data) => {
|
|
1671
1837
|
if (!isCurrentClient())
|
|
1672
1838
|
return;
|
|
@@ -1712,6 +1878,9 @@ export class AUNChannel {
|
|
|
1712
1878
|
const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
|
|
1713
1879
|
this.trace('IN', 'group.invite_received', data);
|
|
1714
1880
|
logger.info(`${this.logPrefix()} Group invitation received: group=${groupId} pending=${inviteIds.length || 1}`);
|
|
1881
|
+
void Promise.resolve(this.groupInviteHandler?.(data)).catch(error => {
|
|
1882
|
+
logger.warn(`${this.logPrefix()} Group invitation review dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1883
|
+
});
|
|
1715
1884
|
});
|
|
1716
1885
|
}
|
|
1717
1886
|
// 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
|
|
@@ -1751,18 +1920,7 @@ export class AUNChannel {
|
|
|
1751
1920
|
client.on('message.recalled', (data) => {
|
|
1752
1921
|
if (!isCurrentClient())
|
|
1753
1922
|
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
|
-
}
|
|
1923
|
+
this.handleMessageRecalled(data, client, isCurrentClient);
|
|
1766
1924
|
});
|
|
1767
1925
|
client.on('message.undecryptable', (data) => {
|
|
1768
1926
|
if (!isCurrentClient())
|
|
@@ -1848,13 +2006,37 @@ export class AUNChannel {
|
|
|
1848
2006
|
this._aid = this.client.aid ?? undefined;
|
|
1849
2007
|
const deviceId = this.client._device_id ?? '';
|
|
1850
2008
|
this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
|
|
1851
|
-
//
|
|
1852
|
-
|
|
2009
|
+
// 控制 AID 也有标准 host agent.md。发布在后台执行,短暂的 Name
|
|
2010
|
+
// Service/Gateway 故障不能把控制通道判定为连接失败。
|
|
2011
|
+
if (this.config.pureIdentity) {
|
|
2012
|
+
this._selfName = buildControlAgentName(aidName);
|
|
2013
|
+
void ensureControlAgentMd(aidName, {
|
|
2014
|
+
store: this.store ?? undefined,
|
|
2015
|
+
aunPath,
|
|
2016
|
+
cacheRoot: resolveRoot(),
|
|
2017
|
+
})
|
|
2018
|
+
.then(result => {
|
|
2019
|
+
this._selfName = result.name;
|
|
2020
|
+
if (this.aidStatsCollector)
|
|
2021
|
+
this.aidStatsCollector.setSelfName(this.config.aid, result.name);
|
|
2022
|
+
if (result.published)
|
|
2023
|
+
logger.info(`${this.logPrefix()} 控制 AID agent.md 已发布: ${result.name}`);
|
|
2024
|
+
})
|
|
2025
|
+
.catch(error => logger.warn(`${this.logPrefix()} 控制 AID agent.md 补全失败(后台重试): ${error instanceof Error ? error.message : String(error)}`));
|
|
2026
|
+
}
|
|
2027
|
+
else {
|
|
2028
|
+
this._selfName = this.loadSelfName(aidName, aunPath);
|
|
2029
|
+
}
|
|
1853
2030
|
if (this._selfName && this.aidStatsCollector)
|
|
1854
2031
|
this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
|
|
1855
2032
|
this.connected = true;
|
|
1856
2033
|
this.connectedAt = Date.now();
|
|
1857
2034
|
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
|
|
2035
|
+
if (!this.config.pureIdentity) {
|
|
2036
|
+
void Promise.resolve(this.groupInviteConnectedHandler?.()).catch(error => {
|
|
2037
|
+
logger.warn(`${this.logPrefix()} Group invitation recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
1858
2040
|
// AUN seq_tracker is transport-scoped, while this channel's durable
|
|
1859
2041
|
// dedup log is application-scoped. A diagnostic consumer can advance
|
|
1860
2042
|
// the former without the application having seen those messages. On a
|
|
@@ -1912,7 +2094,7 @@ export class AUNChannel {
|
|
|
1912
2094
|
if (hasMessageLogOperation(chatDir, operationId)
|
|
1913
2095
|
|| hasMessageLogOperation(chatDir, postBootstrapWelcomeOperationId(aidName)))
|
|
1914
2096
|
continue;
|
|
1915
|
-
const prepared = outbox.findByDedupeKey(aidName, operationId);
|
|
2097
|
+
const prepared = outbox.findByDedupeKey(aidName, operationId, { includeTerminal: true });
|
|
1916
2098
|
if (prepared && prepared.channelId === owner && isDeliveryTarget(prepared.delivery) && sameDeliveryTarget(prepared.delivery, delivery))
|
|
1917
2099
|
continue;
|
|
1918
2100
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
@@ -1948,7 +2130,7 @@ export class AUNChannel {
|
|
|
1948
2130
|
}
|
|
1949
2131
|
async sendPostBootstrapWelcome() {
|
|
1950
2132
|
if (!await this.preparePostBootstrapWelcome())
|
|
1951
|
-
|
|
2133
|
+
throw new Error('Failed to persist post-bootstrap welcome');
|
|
1952
2134
|
return this.reconcilePostBootstrapWelcome();
|
|
1953
2135
|
}
|
|
1954
2136
|
async hasPendingPostBootstrapWelcome() {
|
|
@@ -1966,6 +2148,8 @@ export class AUNChannel {
|
|
|
1966
2148
|
const pull = client?._pullV2;
|
|
1967
2149
|
if (typeof repair !== 'function' || typeof pull !== 'function')
|
|
1968
2150
|
return;
|
|
2151
|
+
if (this.inboundP2PCursorRepairTimer)
|
|
2152
|
+
clearTimeout(this.inboundP2PCursorRepairTimer);
|
|
1969
2153
|
const ns = `p2p:${this.config.aid}`;
|
|
1970
2154
|
let appSeq = 0;
|
|
1971
2155
|
for (const [key, value] of this.inboundSeenMessages) {
|
|
@@ -1978,7 +2162,8 @@ export class AUNChannel {
|
|
|
1978
2162
|
}
|
|
1979
2163
|
if (appSeq <= 0)
|
|
1980
2164
|
return;
|
|
1981
|
-
setTimeout(() => {
|
|
2165
|
+
this.inboundP2PCursorRepairTimer = setTimeout(() => {
|
|
2166
|
+
this.inboundP2PCursorRepairTimer = null;
|
|
1982
2167
|
if (!isCurrentClient() || !this.connected)
|
|
1983
2168
|
return;
|
|
1984
2169
|
const transportSeq = Number(tracker.getContiguousSeq?.(ns) ?? 0);
|
|
@@ -1996,6 +2181,24 @@ export class AUNChannel {
|
|
|
1996
2181
|
}
|
|
1997
2182
|
}, 250);
|
|
1998
2183
|
}
|
|
2184
|
+
inboundP2PCursorRepairTimer = null;
|
|
2185
|
+
handleMessageRecalled(data, client, isCurrentClient) {
|
|
2186
|
+
this.trace('IN', 'message.recalled', data);
|
|
2187
|
+
if (data && typeof data === 'object') {
|
|
2188
|
+
const ids = data.message_ids;
|
|
2189
|
+
if (Array.isArray(ids)) {
|
|
2190
|
+
for (const id of ids) {
|
|
2191
|
+
if (typeof id === 'string') {
|
|
2192
|
+
logger.info(`${this.logPrefix()} Message recalled: ${id}`);
|
|
2193
|
+
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
// A recall tombstone may arrive at a high transport sequence and advance
|
|
2199
|
+
// the SDK cursor past messages that were never delivered to the app.
|
|
2200
|
+
this.scheduleInboundP2PCursorRepair(client, isCurrentClient);
|
|
2201
|
+
}
|
|
1999
2202
|
pendingPostBootstrapWelcomeOwners() {
|
|
2000
2203
|
return pendingPostBootstrapWelcomeOwners(this.config.aid);
|
|
2001
2204
|
}
|
|
@@ -2011,25 +2214,35 @@ export class AUNChannel {
|
|
|
2011
2214
|
const configuredAid = this.config.aid;
|
|
2012
2215
|
const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
|
|
2013
2216
|
const entries = findPostBootstrapWelcomeOutbox(aid);
|
|
2014
|
-
if (entries.length === 0)
|
|
2015
|
-
return false;
|
|
2016
2217
|
const agentConfig = loadAgent(aid);
|
|
2218
|
+
const owners = agentConfig?.owners ?? [];
|
|
2219
|
+
if (entries.length === 0)
|
|
2220
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
2017
2221
|
if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
|
|
2018
2222
|
logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
|
|
2019
|
-
return
|
|
2223
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
2020
2224
|
}
|
|
2021
2225
|
if (!this.connected || !this.client)
|
|
2022
|
-
return
|
|
2226
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
2023
2227
|
for (const entry of entries) {
|
|
2024
2228
|
if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
|
|
2025
2229
|
continue;
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2230
|
+
try {
|
|
2231
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
2232
|
+
if (sent.status === 'sent')
|
|
2233
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
2234
|
+
else if (sent.status === 'permanent')
|
|
2235
|
+
this.markPermanentOutboxFailure(entry, sent);
|
|
2236
|
+
else
|
|
2237
|
+
outbox.recordRetry(this.config.aid, entry, sent);
|
|
2238
|
+
}
|
|
2239
|
+
catch (error) {
|
|
2240
|
+
// Lifecycle has already committed. Preserve its success and the
|
|
2241
|
+
// durable operation even when local post-send bookkeeping fails.
|
|
2242
|
+
logger.warn(`${this.logPrefix()} Welcome reconciliation failed: entry=${entry.id} error=${error}`);
|
|
2243
|
+
}
|
|
2031
2244
|
}
|
|
2032
|
-
return
|
|
2245
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
2033
2246
|
}
|
|
2034
2247
|
// ── Event handlers ──────────────────────────────────────────
|
|
2035
2248
|
/**
|
|
@@ -2372,7 +2585,7 @@ export class AUNChannel {
|
|
|
2372
2585
|
const chatId = fromAid;
|
|
2373
2586
|
// 解析对端身份(30天缓存)
|
|
2374
2587
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
2375
|
-
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false);
|
|
2588
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
2376
2589
|
const shortAid = this.getShortAid(fromAid);
|
|
2377
2590
|
const displayName = peerIdentity.name || shortAid;
|
|
2378
2591
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
@@ -2391,13 +2604,15 @@ export class AUNChannel {
|
|
|
2391
2604
|
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
|
|
2392
2605
|
const installation = p2pPayloadType.startsWith('menu.')
|
|
2393
2606
|
? this.localSenderInstallation?.aunSenderInstallation(msg) : undefined;
|
|
2607
|
+
const peerDeviceId = p2pPayloadType.startsWith('menu.')
|
|
2608
|
+
? menuSenderDeviceId(msg.sender_device_id) : undefined;
|
|
2394
2609
|
this.acknowledgeImmediately(messageId, seq);
|
|
2395
2610
|
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
2396
2611
|
this.dispatchMessage({
|
|
2397
2612
|
channelId: chatId, userId: fromAid,
|
|
2398
2613
|
text: JSON.stringify(payload),
|
|
2399
2614
|
chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
|
|
2400
|
-
peerDeviceId
|
|
2615
|
+
peerDeviceId,
|
|
2401
2616
|
peerSlotId: installation?.slotId,
|
|
2402
2617
|
threadId,
|
|
2403
2618
|
peerName: displayName || undefined,
|
|
@@ -2406,7 +2621,14 @@ export class AUNChannel {
|
|
|
2406
2621
|
protectedHeaders,
|
|
2407
2622
|
mentions,
|
|
2408
2623
|
mentionAids: mentionEntryAids(mentions),
|
|
2409
|
-
replyContext:
|
|
2624
|
+
replyContext: {
|
|
2625
|
+
...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
|
|
2626
|
+
metadata: {
|
|
2627
|
+
...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId).metadata,
|
|
2628
|
+
...(protectedHeaders?.sdk_version ? { menuClientSdkVersion: protectedHeaders.sdk_version } : {}),
|
|
2629
|
+
},
|
|
2630
|
+
...(peerDeviceId ? { toDeviceId: peerDeviceId } : {}),
|
|
2631
|
+
},
|
|
2410
2632
|
msgType: logDescriptor.msgType,
|
|
2411
2633
|
payloadType: logDescriptor.payloadType,
|
|
2412
2634
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -2456,7 +2678,7 @@ export class AUNChannel {
|
|
|
2456
2678
|
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
2679
|
const isSystemP2P = p2pPayloadType === 'event';
|
|
2458
2680
|
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);
|
|
2681
|
+
const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLable(payload));
|
|
2460
2682
|
this.dispatchMessage({
|
|
2461
2683
|
channelId: chatId,
|
|
2462
2684
|
userId: fromAid,
|
|
@@ -2568,7 +2790,7 @@ export class AUNChannel {
|
|
|
2568
2790
|
peerName: displayName,
|
|
2569
2791
|
peerType: peerInfo?.type || 'unknown',
|
|
2570
2792
|
seq,
|
|
2571
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
|
|
2793
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLable(payload)),
|
|
2572
2794
|
createdAt,
|
|
2573
2795
|
});
|
|
2574
2796
|
return;
|
|
@@ -2638,7 +2860,7 @@ export class AUNChannel {
|
|
|
2638
2860
|
this.pendingEchoMessages.set(messageId, {
|
|
2639
2861
|
text: echoText,
|
|
2640
2862
|
channelId: groupId,
|
|
2641
|
-
context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId),
|
|
2863
|
+
context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLable(payload)),
|
|
2642
2864
|
receiveTs: Date.now(),
|
|
2643
2865
|
});
|
|
2644
2866
|
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
@@ -2660,8 +2882,20 @@ export class AUNChannel {
|
|
|
2660
2882
|
// 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
|
|
2661
2883
|
// TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
|
|
2662
2884
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
2885
|
+
try {
|
|
2886
|
+
if (text.trim() || this.collectAllAttachments(payload).length) {
|
|
2887
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, 'unmentioned', 'filtered');
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
catch (error) {
|
|
2891
|
+
// A failed local append must not suppress a subsequent SDK redelivery.
|
|
2892
|
+
const namespace = this.config.aid + ':' + (this.config.channelName ?? 'aun') + ':aun:group';
|
|
2893
|
+
this.inboundSeenMessages.delete(JSON.stringify([namespace, messageId]));
|
|
2894
|
+
this.rewriteInboundDedupe();
|
|
2895
|
+
logger.warn(`${this.logPrefix()} Deferred group message was not recorded; awaiting redelivery: ${error}`);
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2663
2898
|
this.acknowledgeImmediately(messageId, seq);
|
|
2664
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
2665
2899
|
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
2900
|
return;
|
|
2667
2901
|
}
|
|
@@ -2684,7 +2918,7 @@ export class AUNChannel {
|
|
|
2684
2918
|
// Process attachments
|
|
2685
2919
|
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
|
|
2686
2920
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
2687
|
-
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
|
|
2921
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
2688
2922
|
const shortAid = this.getShortAid(senderAid);
|
|
2689
2923
|
const displayName = peerIdentity.name || shortAid;
|
|
2690
2924
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
@@ -2736,7 +2970,7 @@ export class AUNChannel {
|
|
|
2736
2970
|
isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
|
|
2737
2971
|
source: isOwnHandoff ? 'handoff' : undefined,
|
|
2738
2972
|
encrypted: msgEncrypted,
|
|
2739
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
|
|
2973
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLable(payload)),
|
|
2740
2974
|
mentionMode: serverMentionMode,
|
|
2741
2975
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
2742
2976
|
msgType: logDescriptor.msgType,
|
|
@@ -2786,7 +3020,7 @@ export class AUNChannel {
|
|
|
2786
3020
|
channelId: event.channelId,
|
|
2787
3021
|
context: {
|
|
2788
3022
|
...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
|
|
2789
|
-
...(threadId ? { threadId } : {}),
|
|
3023
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
2790
3024
|
delivery,
|
|
2791
3025
|
},
|
|
2792
3026
|
receiveTs: Date.now(),
|
|
@@ -2801,7 +3035,7 @@ export class AUNChannel {
|
|
|
2801
3035
|
const replyContext = {
|
|
2802
3036
|
...(event.replyContext ?? {}),
|
|
2803
3037
|
delivery,
|
|
2804
|
-
...(threadId ? { threadId } : {}),
|
|
3038
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
2805
3039
|
};
|
|
2806
3040
|
this.messageHandler({
|
|
2807
3041
|
channelId: event.channelId || '',
|
|
@@ -2841,10 +3075,10 @@ export class AUNChannel {
|
|
|
2841
3075
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
2842
3076
|
});
|
|
2843
3077
|
}
|
|
2844
|
-
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
|
|
3078
|
+
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq, groupAttention, groupDelivery) {
|
|
2845
3079
|
try {
|
|
2846
3080
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
2847
|
-
|
|
3081
|
+
const entry = buildInboundEntry({
|
|
2848
3082
|
from: fromAid,
|
|
2849
3083
|
to: this.config.aid,
|
|
2850
3084
|
threadId: threadId || null,
|
|
@@ -2859,10 +3093,18 @@ export class AUNChannel {
|
|
|
2859
3093
|
timestamp: receivedAt,
|
|
2860
3094
|
receivedAt,
|
|
2861
3095
|
gatewaySeq,
|
|
2862
|
-
|
|
3096
|
+
groupAttention,
|
|
3097
|
+
groupDelivery,
|
|
3098
|
+
});
|
|
3099
|
+
if (groupDelivery === 'filtered')
|
|
3100
|
+
appendMessageLogStrict(chatDir, entry, true);
|
|
3101
|
+
else
|
|
3102
|
+
appendMessageLog(chatDir, entry);
|
|
2863
3103
|
}
|
|
2864
3104
|
catch (e) {
|
|
2865
|
-
logger.
|
|
3105
|
+
logger.warn(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
|
|
3106
|
+
if (groupDelivery === 'filtered')
|
|
3107
|
+
throw e;
|
|
2866
3108
|
}
|
|
2867
3109
|
}
|
|
2868
3110
|
// ── 观察者模式(Observer Mode) ──────────────────────────────
|
|
@@ -3641,7 +3883,9 @@ export class AUNChannel {
|
|
|
3641
3883
|
return Buffer.byteLength(logText, 'utf-8');
|
|
3642
3884
|
}
|
|
3643
3885
|
applyReplyContextToPayload(payload, context) {
|
|
3644
|
-
const finalPayload = this.stripUndefinedDeep({ ...payload });
|
|
3886
|
+
const finalPayload = this.stripUndefinedDeep(withAunAttention({ ...payload }, context?.attention));
|
|
3887
|
+
if (finalPayload.thread_id === '')
|
|
3888
|
+
delete finalPayload.thread_id;
|
|
3645
3889
|
if (context?.threadId && !finalPayload.thread_id)
|
|
3646
3890
|
finalPayload.thread_id = context.threadId;
|
|
3647
3891
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
@@ -3653,13 +3897,14 @@ export class AUNChannel {
|
|
|
3653
3897
|
return finalPayload;
|
|
3654
3898
|
}
|
|
3655
3899
|
/**
|
|
3656
|
-
* Validate
|
|
3900
|
+
* Validate AUN-specific structured fields before crossing the durable
|
|
3657
3901
|
* send boundary. Return a copy when mentions are present so callers do not
|
|
3658
3902
|
* retain an unvalidated array reference.
|
|
3659
3903
|
*/
|
|
3660
3904
|
normalizeAunPayloadMentions(payload) {
|
|
3661
3905
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
3662
3906
|
return payload;
|
|
3907
|
+
payload = withAunAttention(payload);
|
|
3663
3908
|
if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
|
|
3664
3909
|
return payload;
|
|
3665
3910
|
return {
|
|
@@ -3804,16 +4049,21 @@ export class AUNChannel {
|
|
|
3804
4049
|
registerCardPostSend(messageId, action, channelId, delivery, replyContext) {
|
|
3805
4050
|
if (action.type !== 'register_interaction_card')
|
|
3806
4051
|
return;
|
|
4052
|
+
// Card replies start a new input; an outbound attention marker must not
|
|
4053
|
+
// become the default for every message produced after clicking the card.
|
|
4054
|
+
const callbackContext = replyContext ? { ...replyContext } : undefined;
|
|
4055
|
+
if (callbackContext)
|
|
4056
|
+
delete callbackContext.attention;
|
|
3807
4057
|
this.cardMessageIdMap.set(messageId, {
|
|
3808
4058
|
requestId: action.requestId,
|
|
3809
4059
|
isCommandCard: action.isCommandCard,
|
|
3810
4060
|
initiatorAid: action.initiatorAid,
|
|
3811
4061
|
channelId,
|
|
3812
4062
|
delivery: delivery ?? action.delivery,
|
|
3813
|
-
replyContext,
|
|
4063
|
+
replyContext: callbackContext,
|
|
3814
4064
|
});
|
|
3815
4065
|
this.ownedCardMsgIds.add(messageId);
|
|
3816
|
-
if (replyContext?.threadId) {
|
|
4066
|
+
if (typeof replyContext?.threadId === 'string') {
|
|
3817
4067
|
this.ownedCardTopics.set(messageId, { channelId, threadId: replyContext.threadId });
|
|
3818
4068
|
}
|
|
3819
4069
|
let messageIds = this.interactionCardMessageIds.get(action.requestId);
|
|
@@ -3850,6 +4100,8 @@ export class AUNChannel {
|
|
|
3850
4100
|
return;
|
|
3851
4101
|
if (entry.postSend.type === 'register_interaction_card') {
|
|
3852
4102
|
this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery, entry.context);
|
|
4103
|
+
if (entry.postSend.requestId.startsWith('gir-'))
|
|
4104
|
+
this.groupInviteDelivered?.(entry.postSend.requestId, messageId);
|
|
3853
4105
|
}
|
|
3854
4106
|
}
|
|
3855
4107
|
async sendAunPayload(channelId, payload, context, label) {
|
|
@@ -3881,9 +4133,20 @@ export class AUNChannel {
|
|
|
3881
4133
|
const payloadWithCausationRef = causationRef
|
|
3882
4134
|
? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
|
|
3883
4135
|
: normalizedPayload;
|
|
4136
|
+
if (payloadWithCausationRef.thread_id === '')
|
|
4137
|
+
delete payloadWithCausationRef.thread_id;
|
|
3884
4138
|
// Truncate payload if too large (AUN SDK limit: 1MB)
|
|
3885
4139
|
const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
|
|
3886
4140
|
const params = { payload: truncatedPayload, encrypt };
|
|
4141
|
+
const isMenuPayload = typeof truncatedPayload.type === 'string' && truncatedPayload.type.startsWith('menu.');
|
|
4142
|
+
if (!isGroup && truncatedPayload.type === 'menu.response')
|
|
4143
|
+
params.to_device_id = context?.toDeviceId;
|
|
4144
|
+
if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
|
|
4145
|
+
params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
|
|
4146
|
+
}
|
|
4147
|
+
if (isMenuPayload && encrypt && context?.metadata?.protectedHeaders) {
|
|
4148
|
+
params.protected_headers = context.metadata.protectedHeaders;
|
|
4149
|
+
}
|
|
3887
4150
|
if (isGroup)
|
|
3888
4151
|
params.group_id = delivery.groupId;
|
|
3889
4152
|
else
|
|
@@ -3904,7 +4167,7 @@ export class AUNChannel {
|
|
|
3904
4167
|
return await callOnce(params, false);
|
|
3905
4168
|
}
|
|
3906
4169
|
catch (e) {
|
|
3907
|
-
if (encrypt && e instanceof E2EEError) {
|
|
4170
|
+
if (encrypt && e instanceof E2EEError && !isMenuPayload) {
|
|
3908
4171
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
3909
4172
|
logger.warn(`${this.logPrefix()} E2EE ${label} send failed to ${channelId}, retrying plaintext: ${e}`);
|
|
3910
4173
|
const fallbackParams = { ...params, encrypt: false };
|
|
@@ -3957,7 +4220,8 @@ export class AUNChannel {
|
|
|
3957
4220
|
...classifyAunPayloadForLog(payload), msgId: messageId, encrypt, context, isGroup, source,
|
|
3958
4221
|
transport: this.sendReceiptFromResult(result),
|
|
3959
4222
|
});
|
|
3960
|
-
|
|
4223
|
+
if (typeof payload.type !== 'string' || !payload.type.startsWith('menu.'))
|
|
4224
|
+
this.forwardOutbound(result);
|
|
3961
4225
|
}
|
|
3962
4226
|
async sendContentPayload(channelId, payload, opts) {
|
|
3963
4227
|
const delivery = this.requireDelivery(channelId, opts.context);
|
|
@@ -4000,6 +4264,7 @@ export class AUNChannel {
|
|
|
4000
4264
|
queue: opts.queue,
|
|
4001
4265
|
channelId,
|
|
4002
4266
|
delivery,
|
|
4267
|
+
dedupeKey: opts.dedupeKey,
|
|
4003
4268
|
type: 'payload',
|
|
4004
4269
|
contentKind: opts.contentKind,
|
|
4005
4270
|
payload: finalPayload,
|
|
@@ -4071,7 +4336,7 @@ export class AUNChannel {
|
|
|
4071
4336
|
base.initiator = context.peerId;
|
|
4072
4337
|
if (context?.replyToMessageId)
|
|
4073
4338
|
base.ref_message_id = context.replyToMessageId;
|
|
4074
|
-
return base;
|
|
4339
|
+
return withAunAttention(base, context?.attention);
|
|
4075
4340
|
}
|
|
4076
4341
|
activityLogText(raw) {
|
|
4077
4342
|
// 兼容两种入参:原始 ThoughtItem(顶层字段),或已构建的 activity payload(字段收在 .item 里)
|
|
@@ -4101,65 +4366,23 @@ export class AUNChannel {
|
|
|
4101
4366
|
* the encrypted AUN envelope and RPC framing.
|
|
4102
4367
|
*/
|
|
4103
4368
|
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) {
|
|
4369
|
+
const result = truncateAunPayloadIfNeeded(payload);
|
|
4370
|
+
if (result.originalSize <= AUN_MAX_PAYLOAD_SIZE) {
|
|
4110
4371
|
return payload; // No truncation needed
|
|
4111
4372
|
}
|
|
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`);
|
|
4373
|
+
logger.warn(`${this.logPrefix()} Payload too large (${formatSize(result.originalSize)}), truncating... label=${label}`);
|
|
4374
|
+
for (const field of result.truncatedFields) {
|
|
4375
|
+
logger.info(`${this.logPrefix()} Truncated field ${field.path}: ${formatSize(field.originalSize)} → ${formatSize(field.finalSize)}`);
|
|
4376
|
+
}
|
|
4377
|
+
if (result.dropped) {
|
|
4378
|
+
logger.error(`${this.logPrefix()} Payload still too large after truncation: ${formatSize(result.finalSize)}, dropping payload`);
|
|
4156
4379
|
return {
|
|
4157
4380
|
type: 'text',
|
|
4158
4381
|
text: '消息内容过大,无法完整发送,请缩小输出后重试',
|
|
4159
4382
|
};
|
|
4160
4383
|
}
|
|
4161
|
-
logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(
|
|
4162
|
-
return
|
|
4384
|
+
logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(result.originalSize)} → ${formatSize(result.finalSize)}`);
|
|
4385
|
+
return result.payload;
|
|
4163
4386
|
}
|
|
4164
4387
|
buildActivityPayload(envelope, context, item) {
|
|
4165
4388
|
const rawItem = item && typeof item === 'object' && !Array.isArray(item)
|
|
@@ -4171,11 +4394,11 @@ export class AUNChannel {
|
|
|
4171
4394
|
const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
|
|
4172
4395
|
? { ...rawItem, arguments: displayArguments }
|
|
4173
4396
|
: rawItem;
|
|
4174
|
-
return {
|
|
4397
|
+
return withAunAttention({
|
|
4175
4398
|
...this.buildTaskPayloadBase(envelope, context),
|
|
4176
4399
|
type: 'activity',
|
|
4177
4400
|
item: this.stripUndefinedDeep(activityItem),
|
|
4178
|
-
};
|
|
4401
|
+
});
|
|
4179
4402
|
}
|
|
4180
4403
|
async sendReliableStructured(channelId, payload, context, logText) {
|
|
4181
4404
|
const result = await this.sendContentPayload(channelId, payload, {
|
|
@@ -4197,6 +4420,7 @@ export class AUNChannel {
|
|
|
4197
4420
|
return result;
|
|
4198
4421
|
}
|
|
4199
4422
|
async sendMessage(channelId, text, context) {
|
|
4423
|
+
parseAunAttention(context?.attention);
|
|
4200
4424
|
if (!text?.trim()) {
|
|
4201
4425
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
4202
4426
|
return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
|
|
@@ -4313,6 +4537,7 @@ export class AUNChannel {
|
|
|
4313
4537
|
if (entry.deliveryReceipt?.messageId) {
|
|
4314
4538
|
return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
|
|
4315
4539
|
}
|
|
4540
|
+
outbox.recordRetry(this.config.aid, entry, result);
|
|
4316
4541
|
return {
|
|
4317
4542
|
status: 'queued',
|
|
4318
4543
|
outboxId: entry.id,
|
|
@@ -4332,6 +4557,8 @@ export class AUNChannel {
|
|
|
4332
4557
|
try {
|
|
4333
4558
|
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
4334
4559
|
finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
4560
|
+
if (finalPayload.thread_id === '')
|
|
4561
|
+
delete finalPayload.thread_id;
|
|
4335
4562
|
}
|
|
4336
4563
|
catch (e) {
|
|
4337
4564
|
return {
|
|
@@ -4433,11 +4660,7 @@ export class AUNChannel {
|
|
|
4433
4660
|
code: 'H_CLASS_PROTECTED',
|
|
4434
4661
|
});
|
|
4435
4662
|
}
|
|
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, {
|
|
4663
|
+
const built = await uploadFileAndBuildPayload(this.client, this.config.aid, file.filePath, {
|
|
4441
4664
|
as: file.as,
|
|
4442
4665
|
contentType: file.contentType,
|
|
4443
4666
|
text: file.text,
|
|
@@ -4458,6 +4681,8 @@ export class AUNChannel {
|
|
|
4458
4681
|
// removed from a malformed mentions array.
|
|
4459
4682
|
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
4460
4683
|
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
4684
|
+
if (finalPayload.thread_id === '')
|
|
4685
|
+
delete finalPayload.thread_id;
|
|
4461
4686
|
const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
|
|
4462
4687
|
? normalizeAunMentionEntries(finalPayload.mentions)
|
|
4463
4688
|
: undefined;
|
|
@@ -4612,10 +4837,21 @@ export class AUNChannel {
|
|
|
4612
4837
|
return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
|
|
4613
4838
|
}
|
|
4614
4839
|
}
|
|
4840
|
+
if (operationId.startsWith(bootstrapInitialMessageOperationPrefix(this.config.aid)) && !entry.deliveryReceipt) {
|
|
4841
|
+
const config = loadAgent(this.config.aid);
|
|
4842
|
+
if (config && resolveAgentLifecycle(config) === 'active') {
|
|
4843
|
+
return { status: 'permanent', code: 'BOOTSTRAP_SUPERSEDED', error: 'Bootstrap is complete; initial prompt cancelled' };
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4615
4846
|
}
|
|
4616
4847
|
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
4617
4848
|
const source = context?.metadata?.source ?? 'daemon';
|
|
4618
|
-
|
|
4849
|
+
// Put the label in the visible body without requiring client title support.
|
|
4850
|
+
// Derive it from the durable context without mutating the stored text.
|
|
4851
|
+
const title = delivery.chatType === 'private' ? context?.title?.trim() : undefined;
|
|
4852
|
+
const wireText = title && finalText.split(/\r?\n/, 1)[0].trim() !== title
|
|
4853
|
+
? `${title}\n\n${finalText}` : finalText;
|
|
4854
|
+
const payload = withAunAttention({ type: 'text', text: wireText }, context?.attention);
|
|
4619
4855
|
if (delivery.chatType === 'group') {
|
|
4620
4856
|
const extracted = this.extractMentionEntriesFromText(finalText);
|
|
4621
4857
|
if (extracted.length > 0)
|
|
@@ -4629,6 +4865,8 @@ export class AUNChannel {
|
|
|
4629
4865
|
payload.task_id = context.metadata.taskId;
|
|
4630
4866
|
if (context?.metadata?.chatmode)
|
|
4631
4867
|
payload.chatmode = context.metadata.chatmode;
|
|
4868
|
+
if (context?.metadata?.forkAnchor)
|
|
4869
|
+
payload.fork_anchor = context.metadata.forkAnchor;
|
|
4632
4870
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
4633
4871
|
const causationRef = delivery.chatType !== 'group' && causation
|
|
4634
4872
|
? `caus_${crypto.randomUUID().replace(/-/g, '')}`
|
|
@@ -4638,7 +4876,7 @@ export class AUNChannel {
|
|
|
4638
4876
|
registerAunCausation(causationRef, this.config.aid, channelId, causation);
|
|
4639
4877
|
}
|
|
4640
4878
|
// 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
|
|
4641
|
-
logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${
|
|
4879
|
+
logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${wireText.length}`);
|
|
4642
4880
|
const isGroup = delivery.chatType === 'group';
|
|
4643
4881
|
const targetAid = channelId;
|
|
4644
4882
|
if (entry.deliveryReceipt) {
|
|
@@ -4819,6 +5057,7 @@ export class AUNChannel {
|
|
|
4819
5057
|
queue: entry.queue,
|
|
4820
5058
|
};
|
|
4821
5059
|
const receipt = {
|
|
5060
|
+
acceptedAt: Date.now(),
|
|
4822
5061
|
messageId,
|
|
4823
5062
|
encrypt,
|
|
4824
5063
|
transport: this.sendReceiptFromResult(result),
|
|
@@ -4841,6 +5080,15 @@ export class AUNChannel {
|
|
|
4841
5080
|
logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
|
|
4842
5081
|
return { ok: true, status: 'sent' };
|
|
4843
5082
|
}
|
|
5083
|
+
if (interactionId?.startsWith('gir-')) {
|
|
5084
|
+
const result = this.groupInviteDeliveryGate?.(interactionId, entry.channelId) ?? 'stale';
|
|
5085
|
+
if (result === 'defer')
|
|
5086
|
+
return { ok: false, status: 'retry', error: 'group invitation callback is not ready', code: 'INVITE_REVIEW_NOT_READY' };
|
|
5087
|
+
if (result !== 'allow') {
|
|
5088
|
+
logger.info(`${this.logPrefix()} Discarded stale group invitation card: request=${interactionId} entry=${entry.id}`);
|
|
5089
|
+
return { ok: true, status: 'sent' };
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
4844
5092
|
const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
|
|
4845
5093
|
? entry.postSend.expiresAt
|
|
4846
5094
|
: undefined;
|
|
@@ -4988,6 +5236,8 @@ export class AUNChannel {
|
|
|
4988
5236
|
const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
|
|
4989
5237
|
? { ...finalPayload, chatmode: context.metadata.chatmode }
|
|
4990
5238
|
: finalPayload;
|
|
5239
|
+
if (payloadWithChatmode.thread_id === '')
|
|
5240
|
+
delete payloadWithChatmode.thread_id;
|
|
4991
5241
|
if (context?.threadId && !payloadWithChatmode.thread_id)
|
|
4992
5242
|
payloadWithChatmode.thread_id = context.threadId;
|
|
4993
5243
|
const params = {
|
|
@@ -5070,7 +5320,7 @@ export class AUNChannel {
|
|
|
5070
5320
|
// Validate before checking connection state so malformed producer payloads
|
|
5071
5321
|
// are reported deterministically instead of being hidden by a disconnect.
|
|
5072
5322
|
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
5073
|
-
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
5323
|
+
const finalPayload = this.stripUndefinedDeep(withAunAttention(validatedPayload, context?.attention));
|
|
5074
5324
|
if (!this.connected || !this.client) {
|
|
5075
5325
|
return {
|
|
5076
5326
|
ok: false,
|
|
@@ -5085,12 +5335,19 @@ export class AUNChannel {
|
|
|
5085
5335
|
const encrypt = context?.metadata?.encrypted != null
|
|
5086
5336
|
? !!(context.metadata.encrypted)
|
|
5087
5337
|
: this.shouldEncrypt(encryptTarget);
|
|
5338
|
+
if (finalPayload.thread_id === '')
|
|
5339
|
+
delete finalPayload.thread_id;
|
|
5088
5340
|
if (context?.threadId && !finalPayload.thread_id)
|
|
5089
5341
|
finalPayload.thread_id = context.threadId;
|
|
5090
5342
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
5091
5343
|
finalPayload.ref_message_id = context.replyToMessageId;
|
|
5092
5344
|
const isMenuPayload = typeof finalPayload.type === 'string' && finalPayload.type.startsWith('menu.');
|
|
5093
5345
|
const params = { payload: finalPayload, encrypt };
|
|
5346
|
+
if (!isGroup && finalPayload.type === 'menu.response')
|
|
5347
|
+
params.to_device_id = context?.toDeviceId;
|
|
5348
|
+
if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
|
|
5349
|
+
params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
|
|
5350
|
+
}
|
|
5094
5351
|
const rawHeaders = context?.metadata?.protectedHeaders;
|
|
5095
5352
|
if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
|
|
5096
5353
|
const protectedHeaders = {};
|
|
@@ -5156,21 +5413,23 @@ export class AUNChannel {
|
|
|
5156
5413
|
});
|
|
5157
5414
|
}
|
|
5158
5415
|
async sendFile(channelId, filePath, context) {
|
|
5416
|
+
parseAunAttention(context?.attention);
|
|
5159
5417
|
const delivery = this.requireDelivery(channelId, context);
|
|
5160
5418
|
context = this.withDelivery(context, delivery);
|
|
5419
|
+
const isFetch = typeof context?.metadata?.correlationId === 'string' && !!context.metadata.correlationId;
|
|
5161
5420
|
const absPath = path.resolve(filePath);
|
|
5162
5421
|
if (!fs.existsSync(absPath)) {
|
|
5163
5422
|
logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
|
|
5164
|
-
return;
|
|
5423
|
+
return { status: 'failed', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
|
|
5165
5424
|
}
|
|
5166
5425
|
const stat = fs.statSync(absPath);
|
|
5167
5426
|
if (stat.size === 0) {
|
|
5168
5427
|
logger.warn(`${this.logPrefix()} sendFile: file is empty`);
|
|
5169
|
-
return;
|
|
5428
|
+
return { status: 'failed', error: 'file is empty', code: 'EMPTY_FILE' };
|
|
5170
5429
|
}
|
|
5171
5430
|
if (stat.size > 10 * 1024 * 1024) {
|
|
5172
5431
|
logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
|
|
5173
|
-
return;
|
|
5432
|
+
return { status: 'failed', error: `file too large (max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`, code: 'FILE_TOO_LARGE' };
|
|
5174
5433
|
}
|
|
5175
5434
|
// Write-ahead: persist to outbox
|
|
5176
5435
|
const entry = outbox.enqueue(this.config.aid, {
|
|
@@ -5188,17 +5447,47 @@ export class AUNChannel {
|
|
|
5188
5447
|
if (!this.reconnectTimer && !this.client) {
|
|
5189
5448
|
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendFile failed: ${e}`));
|
|
5190
5449
|
}
|
|
5191
|
-
|
|
5450
|
+
const error = 'AUN channel is not connected';
|
|
5451
|
+
if (isFetch) {
|
|
5452
|
+
this.markPermanentOutboxFailure(entry, { error, code: 'AUN_NOT_CONNECTED' });
|
|
5453
|
+
return { status: 'failed', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
|
|
5454
|
+
}
|
|
5455
|
+
return { status: 'queued', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
|
|
5192
5456
|
}
|
|
5193
5457
|
const result = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
5194
5458
|
if (result.status === 'sent') {
|
|
5459
|
+
const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
|
|
5460
|
+
if (!messageId) {
|
|
5461
|
+
const error = 'file send returned no message_id';
|
|
5462
|
+
this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
|
|
5463
|
+
return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
|
|
5464
|
+
}
|
|
5195
5465
|
this.removeDeliveredOutboxEntry(entry);
|
|
5466
|
+
return { status: 'sent', messageId };
|
|
5196
5467
|
}
|
|
5197
|
-
|
|
5468
|
+
// Remote acceptance is authoritative even if local bookkeeping needs retry.
|
|
5469
|
+
if (entry.deliveryReceipt?.messageId)
|
|
5470
|
+
return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
|
|
5471
|
+
if (result.status === 'permanent' || isFetch) {
|
|
5472
|
+
// A correlated fetch must resolve promptly. Retrying it after reporting
|
|
5473
|
+
// failure could deliver an unsolicited file for an already closed request.
|
|
5198
5474
|
this.markPermanentOutboxFailure(entry, result);
|
|
5475
|
+
return {
|
|
5476
|
+
status: 'failed',
|
|
5477
|
+
outboxId: entry.id,
|
|
5478
|
+
error: result.error ?? 'permanent file send failure',
|
|
5479
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5480
|
+
};
|
|
5199
5481
|
}
|
|
5482
|
+
return {
|
|
5483
|
+
status: 'queued',
|
|
5484
|
+
outboxId: entry.id,
|
|
5485
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
5486
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5487
|
+
};
|
|
5200
5488
|
}
|
|
5201
5489
|
async sendImage(channelId, data, mimeType, alt, context) {
|
|
5490
|
+
parseAunAttention(context?.attention);
|
|
5202
5491
|
const delivery = this.requireDelivery(channelId, context);
|
|
5203
5492
|
context = this.withDelivery(context, delivery);
|
|
5204
5493
|
if (!Buffer.isBuffer(data)) {
|
|
@@ -5268,11 +5557,7 @@ export class AUNChannel {
|
|
|
5268
5557
|
}
|
|
5269
5558
|
const digest = crypto.createHash('sha256').update(data).digest('hex').slice(0, 12);
|
|
5270
5559
|
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 });
|
|
5560
|
+
const built = await uploadBufferAndBuildPayload(this.client, this.config.aid, data, filename, { as: 'image', contentType });
|
|
5276
5561
|
const imagePayload = { ...built.payload };
|
|
5277
5562
|
setIfDefined(imagePayload, 'alt', alt);
|
|
5278
5563
|
return imagePayload;
|
|
@@ -5359,57 +5644,14 @@ export class AUNChannel {
|
|
|
5359
5644
|
logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
|
|
5360
5645
|
return { status: 'permanent', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
|
|
5361
5646
|
}
|
|
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
5647
|
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
|
-
};
|
|
5648
|
+
const { attachment } = await uploadFileAndBuildPayload(this.client, this.config.aid, absPath, { as: 'file' });
|
|
5649
|
+
const { filename, size_bytes: size, content_type: contentType } = attachment;
|
|
5408
5650
|
const filePayload = buildAunFilePayload({
|
|
5409
5651
|
filename,
|
|
5410
|
-
size
|
|
5652
|
+
size,
|
|
5411
5653
|
contentType,
|
|
5412
|
-
attachment,
|
|
5654
|
+
attachment: { ...attachment },
|
|
5413
5655
|
context,
|
|
5414
5656
|
});
|
|
5415
5657
|
const fileTargetAid = channelId;
|
|
@@ -5495,7 +5737,7 @@ export class AUNChannel {
|
|
|
5495
5737
|
logger.error(`${this.logPrefix()} AUN accepted the durable file but its receipt could not be persisted: id=${entry.id} mid=${sentMid} error=${detail}`);
|
|
5496
5738
|
return { status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
|
|
5497
5739
|
}
|
|
5498
|
-
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(
|
|
5740
|
+
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(size)}) → ${channelId}`);
|
|
5499
5741
|
const fileText = filePayload.text;
|
|
5500
5742
|
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
5743
|
const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
|
|
@@ -5614,11 +5856,12 @@ export class AUNChannel {
|
|
|
5614
5856
|
// corrected Owner route was delivered.
|
|
5615
5857
|
if (entry.channelId !== owner || entry.delivery?.chatType !== 'private') {
|
|
5616
5858
|
delete repaired.deliveryReceipt;
|
|
5859
|
+
delete repaired.deliveryResult;
|
|
5860
|
+
delete repaired.terminal;
|
|
5861
|
+
delete repaired.lastError;
|
|
5862
|
+
delete repaired.lastErrorCode;
|
|
5863
|
+
delete repaired.attempts;
|
|
5617
5864
|
}
|
|
5618
|
-
delete repaired.terminal;
|
|
5619
|
-
delete repaired.lastError;
|
|
5620
|
-
delete repaired.lastErrorCode;
|
|
5621
|
-
delete repaired.attempts;
|
|
5622
5865
|
if (outbox.replaceIfRouteMatches(aid, entry, repaired) !== 'replaced')
|
|
5623
5866
|
continue;
|
|
5624
5867
|
logger.warn(`${this.logPrefix()} Repaired bootstrap outbox route to private Owner delivery: entry=${entry.id} owner=${owner}`);
|
|
@@ -5658,17 +5901,21 @@ export class AUNChannel {
|
|
|
5658
5901
|
notice: 'notice',
|
|
5659
5902
|
requires_action: 'requires_action',
|
|
5660
5903
|
};
|
|
5661
|
-
const statusPayload = {
|
|
5904
|
+
const statusPayload = withAunAttention({
|
|
5662
5905
|
type: 'task.status',
|
|
5663
5906
|
status: stateMap[status] ?? status,
|
|
5664
5907
|
task_id: taskId,
|
|
5665
5908
|
session_id: sessionId,
|
|
5666
5909
|
severity,
|
|
5667
|
-
|
|
5668
|
-
};
|
|
5910
|
+
});
|
|
5669
5911
|
const metadataInput = extraMeta ? { ...extraMeta } : undefined;
|
|
5670
5912
|
if (metadataInput)
|
|
5671
5913
|
delete metadataInput.severity;
|
|
5914
|
+
const statusKind = typeof extraMeta?.kind === 'string' ? extraMeta.kind : undefined;
|
|
5915
|
+
if (statusKind)
|
|
5916
|
+
statusPayload.kind = statusKind;
|
|
5917
|
+
if (metadataInput)
|
|
5918
|
+
delete metadataInput.kind;
|
|
5672
5919
|
const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
|
|
5673
5920
|
if (metadata && Object.keys(metadata).length > 0)
|
|
5674
5921
|
statusPayload.metadata = metadata;
|
|
@@ -5721,10 +5968,6 @@ export class AUNChannel {
|
|
|
5721
5968
|
// returns until the channel reconnects.
|
|
5722
5969
|
const delivery = this.requireDelivery(channelId, context);
|
|
5723
5970
|
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
5971
|
// SDK 0.3.0 E2EE requires payload to be an object
|
|
5729
5972
|
let payloadObj;
|
|
5730
5973
|
try {
|
|
@@ -5735,6 +5978,16 @@ export class AUNChannel {
|
|
|
5735
5978
|
catch {
|
|
5736
5979
|
payloadObj = { text: payload };
|
|
5737
5980
|
}
|
|
5981
|
+
// File fetch was already acknowledged. Its terminal error must survive a
|
|
5982
|
+
// disconnect so the pending App request is not silently abandoned.
|
|
5983
|
+
if (payloadObj.type === 'menu.response' && payloadObj.name === 'file' && payloadObj.error) {
|
|
5984
|
+
await this.sendReliableStructured(channelId, payloadObj, context);
|
|
5985
|
+
return;
|
|
5986
|
+
}
|
|
5987
|
+
if (!this.connected || !this.client) {
|
|
5988
|
+
logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
|
|
5989
|
+
return;
|
|
5990
|
+
}
|
|
5738
5991
|
await this.sendStructuredOrThrow(channelId, payloadObj, context);
|
|
5739
5992
|
}
|
|
5740
5993
|
async disconnect() {
|
|
@@ -5814,15 +6067,15 @@ export class AUNChannel {
|
|
|
5814
6067
|
};
|
|
5815
6068
|
}
|
|
5816
6069
|
/** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
|
|
5817
|
-
loadSelfName(aid) {
|
|
6070
|
+
loadSelfName(aid, aunPath) {
|
|
5818
6071
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
5819
|
-
const name = resolveAgentDisplayName(aidName);
|
|
6072
|
+
const name = resolveAgentDisplayName(aidName, { aunPath });
|
|
5820
6073
|
if (!name)
|
|
5821
|
-
void this.fetchAndCacheSelfName(aidName);
|
|
6074
|
+
void this.fetchAndCacheSelfName(aidName, aunPath);
|
|
5822
6075
|
return name;
|
|
5823
6076
|
}
|
|
5824
|
-
async fetchAndCacheSelfName(aidName) {
|
|
5825
|
-
const name = await refreshAgentDisplayName(aidName);
|
|
6077
|
+
async fetchAndCacheSelfName(aidName, aunPath) {
|
|
6078
|
+
const name = await refreshAgentDisplayName(aidName, { aunPath });
|
|
5826
6079
|
if (name) {
|
|
5827
6080
|
this._selfName = name;
|
|
5828
6081
|
if (this.aidStatsCollector)
|
|
@@ -5839,13 +6092,20 @@ export class AUNChannel {
|
|
|
5839
6092
|
if (!this.client)
|
|
5840
6093
|
return { type: null };
|
|
5841
6094
|
try {
|
|
6095
|
+
const client = this.client;
|
|
6096
|
+
const revision = this.peerInfoRevision;
|
|
5842
6097
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
5843
|
-
const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
|
|
6098
|
+
const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
5844
6099
|
const type = identity.type === 'human' ? 'human' : 'agent';
|
|
5845
6100
|
const name = identity.name || undefined;
|
|
5846
6101
|
const info = { type, name };
|
|
5847
|
-
this.
|
|
5848
|
-
|
|
6102
|
+
if (identity.lastCheckedAt !== 0 && this.client === client && this.peerInfoRevision === revision) {
|
|
6103
|
+
this.peerInfoCache.set(aid, info);
|
|
6104
|
+
setTimeout(() => {
|
|
6105
|
+
if (this.peerInfoCache.get(aid) === info)
|
|
6106
|
+
this.peerInfoCache.delete(aid);
|
|
6107
|
+
}, 30 * 60 * 1000);
|
|
6108
|
+
}
|
|
5849
6109
|
return info;
|
|
5850
6110
|
}
|
|
5851
6111
|
catch (e) {
|
|
@@ -5867,13 +6127,20 @@ export class AUNChannel {
|
|
|
5867
6127
|
if (!this.store)
|
|
5868
6128
|
throw new Error('not connected');
|
|
5869
6129
|
const { agentmdPut } = await import('../aun/aid/agentmd.js');
|
|
5870
|
-
await agentmdPut(content, {
|
|
6130
|
+
await agentmdPut(content, {
|
|
6131
|
+
aid: this.config.aid,
|
|
6132
|
+
store: this.store,
|
|
6133
|
+
aunPath: this.config.keystorePath || resolveRoot(),
|
|
6134
|
+
});
|
|
5871
6135
|
}
|
|
5872
6136
|
async downloadAgentMd(aid) {
|
|
5873
6137
|
if (!this.store)
|
|
5874
6138
|
throw new Error('not connected');
|
|
5875
6139
|
const { agentmdSync } = await import('../aun/aid/agentmd.js');
|
|
5876
|
-
const result = await agentmdSync(aid, {
|
|
6140
|
+
const result = await agentmdSync(aid, {
|
|
6141
|
+
store: this.store ?? undefined,
|
|
6142
|
+
aunPath: this.config.keystorePath || resolveRoot(),
|
|
6143
|
+
});
|
|
5877
6144
|
return result.content ?? '';
|
|
5878
6145
|
}
|
|
5879
6146
|
/**
|
|
@@ -5972,7 +6239,7 @@ export class AUNChannelPlugin {
|
|
|
5972
6239
|
const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
|
|
5973
6240
|
? undefined
|
|
5974
6241
|
: operationId;
|
|
5975
|
-
|
|
6242
|
+
let replyCtx = outboundCausation
|
|
5976
6243
|
? {
|
|
5977
6244
|
...(envelope.replyContext ?? {}),
|
|
5978
6245
|
delivery,
|
|
@@ -5981,6 +6248,15 @@ export class AUNChannelPlugin {
|
|
|
5981
6248
|
: (envelope.replyContext || envelope.delivery)
|
|
5982
6249
|
? { ...(envelope.replyContext ?? {}), delivery }
|
|
5983
6250
|
: undefined;
|
|
6251
|
+
const attention = aunOutboundAttention(payload);
|
|
6252
|
+
if (replyCtx) {
|
|
6253
|
+
replyCtx = { ...replyCtx };
|
|
6254
|
+
delete replyCtx.attention;
|
|
6255
|
+
// Assign titles per payload below, never inherit a final-reply title.
|
|
6256
|
+
delete replyCtx.title;
|
|
6257
|
+
}
|
|
6258
|
+
if (attention !== undefined)
|
|
6259
|
+
replyCtx = { ...(replyCtx ?? {}), attention };
|
|
5984
6260
|
if (outboundCausation) {
|
|
5985
6261
|
recordCausationSpan(outboundCausation, 'message.outbound', {
|
|
5986
6262
|
status: 'started',
|
|
@@ -6003,7 +6279,7 @@ export class AUNChannelPlugin {
|
|
|
6003
6279
|
subtype: payload.kind,
|
|
6004
6280
|
text: payload.text,
|
|
6005
6281
|
...(isError
|
|
6006
|
-
? { message: payload.text, user_message: payload.text, recoverable: true
|
|
6282
|
+
? { message: payload.text, user_message: payload.text, recoverable: true }
|
|
6007
6283
|
: { severity: 'info' }),
|
|
6008
6284
|
};
|
|
6009
6285
|
for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
|
|
@@ -6018,10 +6294,13 @@ export class AUNChannelPlugin {
|
|
|
6018
6294
|
...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
|
|
6019
6295
|
.filter(([key]) => key !== 'operationId')),
|
|
6020
6296
|
...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
|
|
6297
|
+
...(payload.kind === 'result.text' && payload.metadata?.forkAnchor
|
|
6298
|
+
? { forkAnchor: payload.metadata.forkAnchor } : {}),
|
|
6021
6299
|
},
|
|
6022
6300
|
};
|
|
6023
|
-
if (payload.kind === 'result.text' && payload.isFinal)
|
|
6301
|
+
if (payload.kind === 'result.text' && payload.isFinal && delivery.chatType === 'private') {
|
|
6024
6302
|
sendCtx.title = '✅ 最终回复:';
|
|
6303
|
+
}
|
|
6025
6304
|
let result;
|
|
6026
6305
|
try {
|
|
6027
6306
|
result = await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
@@ -6045,7 +6324,7 @@ export class AUNChannelPlugin {
|
|
|
6045
6324
|
messages: [{
|
|
6046
6325
|
messageId: result.messageId,
|
|
6047
6326
|
partIndex: 0,
|
|
6048
|
-
...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
|
|
6327
|
+
...(typeof sendCtx.threadId === 'string' ? { threadId: sendCtx.threadId } : {}),
|
|
6049
6328
|
}],
|
|
6050
6329
|
};
|
|
6051
6330
|
}
|
|
@@ -6081,14 +6360,13 @@ export class AUNChannelPlugin {
|
|
|
6081
6360
|
}
|
|
6082
6361
|
case 'system.error': {
|
|
6083
6362
|
const errorPayload = {
|
|
6084
|
-
type: 'error',
|
|
6363
|
+
type: payload.recoverable === true ? 'notice' : 'error',
|
|
6085
6364
|
...taskBase(),
|
|
6086
6365
|
subtype: payload.subtype,
|
|
6087
6366
|
text: payload.text,
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
terminal: payload.recoverable !== true,
|
|
6367
|
+
...(payload.recoverable === true
|
|
6368
|
+
? { severity: 'warn' }
|
|
6369
|
+
: { message: payload.text, user_message: payload.text, recoverable: false }),
|
|
6092
6370
|
};
|
|
6093
6371
|
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
6094
6372
|
return;
|
|
@@ -6101,7 +6379,6 @@ export class AUNChannelPlugin {
|
|
|
6101
6379
|
text: payload.text,
|
|
6102
6380
|
message: payload.text,
|
|
6103
6381
|
user_message: payload.text,
|
|
6104
|
-
terminal: true,
|
|
6105
6382
|
};
|
|
6106
6383
|
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
6107
6384
|
return;
|
|
@@ -6110,8 +6387,27 @@ export class AUNChannelPlugin {
|
|
|
6110
6387
|
const fileCtx = payload.correlationId
|
|
6111
6388
|
? { ...(replyCtx ?? {}), metadata: { ...(replyCtx?.metadata ?? {}), correlationId: payload.correlationId } }
|
|
6112
6389
|
: replyCtx;
|
|
6113
|
-
await channel.sendFile(channelId, payload.filePath, fileCtx);
|
|
6114
|
-
|
|
6390
|
+
const result = await channel.sendFile(channelId, payload.filePath, fileCtx);
|
|
6391
|
+
if (result.status === 'sent')
|
|
6392
|
+
return sentReceipt(envelope, [result.messageId], fileCtx?.threadId);
|
|
6393
|
+
if (result.status === 'queued') {
|
|
6394
|
+
return {
|
|
6395
|
+
status: 'queued',
|
|
6396
|
+
operationId,
|
|
6397
|
+
messages: [],
|
|
6398
|
+
outboxId: result.outboxId,
|
|
6399
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
6400
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
6401
|
+
};
|
|
6402
|
+
}
|
|
6403
|
+
return {
|
|
6404
|
+
status: 'failed',
|
|
6405
|
+
operationId,
|
|
6406
|
+
messages: [],
|
|
6407
|
+
...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
|
|
6408
|
+
error: result.error,
|
|
6409
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
6410
|
+
};
|
|
6115
6411
|
}
|
|
6116
6412
|
case 'result.image': {
|
|
6117
6413
|
await channel.sendImage(channelId, payload.data, payload.mimeType, payload.alt, replyCtx);
|
|
@@ -6255,13 +6551,13 @@ export class AUNChannelPlugin {
|
|
|
6255
6551
|
};
|
|
6256
6552
|
if (req.kind.kind === 'action') {
|
|
6257
6553
|
const action = req.kind;
|
|
6258
|
-
const aunCard = {
|
|
6554
|
+
const aunCard = withAunAttention({
|
|
6259
6555
|
type: 'action_card',
|
|
6260
6556
|
title: action.title,
|
|
6261
6557
|
actions: action.buttons.map(btn => ({
|
|
6262
6558
|
label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
|
|
6263
6559
|
})),
|
|
6264
|
-
};
|
|
6560
|
+
});
|
|
6265
6561
|
// AUN action_card 以 compose behavior 声明自由文本动作。新版客户端
|
|
6266
6562
|
// 在卡片内采集并确认;旧版客户端可继续从主编辑器采集。两者都结构化
|
|
6267
6563
|
// 回传 action_card_reply{behavior:"compose"},Core 不拦截普通消息队列。
|
|
@@ -6279,6 +6575,7 @@ export class AUNChannelPlugin {
|
|
|
6279
6575
|
const result = await channel.sendContentPayload(channelId, aunCard, {
|
|
6280
6576
|
contentKind: 'card',
|
|
6281
6577
|
context: replyCtx,
|
|
6578
|
+
dedupeKey: req.id.startsWith('gir-') ? operationId : undefined,
|
|
6282
6579
|
logText: action.title ? `[card] ${action.title}` : '[card]',
|
|
6283
6580
|
postSend: {
|
|
6284
6581
|
type: 'register_interaction_card',
|
|
@@ -6293,7 +6590,7 @@ export class AUNChannelPlugin {
|
|
|
6293
6590
|
}
|
|
6294
6591
|
else if (req.kind.kind === 'command-card') {
|
|
6295
6592
|
const card = req.kind;
|
|
6296
|
-
const aunCard = {
|
|
6593
|
+
const aunCard = withAunAttention({
|
|
6297
6594
|
type: 'action_card',
|
|
6298
6595
|
title: card.title,
|
|
6299
6596
|
actions: card.buttons.map(btn => {
|
|
@@ -6306,7 +6603,7 @@ export class AUNChannelPlugin {
|
|
|
6306
6603
|
setIfDefined(action, 'disabled', btn.disabled);
|
|
6307
6604
|
return action;
|
|
6308
6605
|
}),
|
|
6309
|
-
};
|
|
6606
|
+
});
|
|
6310
6607
|
if (card.body) {
|
|
6311
6608
|
aunCard.text = card.body;
|
|
6312
6609
|
setIfDefined(aunCard, 'format', card.bodyFormat);
|
|
@@ -6397,6 +6694,14 @@ export class AUNChannelPlugin {
|
|
|
6397
6694
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
6398
6695
|
handler(aunOptsToInbound(opts, adapter.channelName, channelType));
|
|
6399
6696
|
}), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
|
|
6697
|
+
bridge.registerGroupInviteReviewChannel({
|
|
6698
|
+
selfAid: aid,
|
|
6699
|
+
channelName: adapter.channelName,
|
|
6700
|
+
channelKey: adapter.channelKey,
|
|
6701
|
+
agentName: ctx.agentName,
|
|
6702
|
+
channel,
|
|
6703
|
+
adapter,
|
|
6704
|
+
});
|
|
6400
6705
|
},
|
|
6401
6706
|
registerHooks(hookCtx) {
|
|
6402
6707
|
channel.setEventBus(hookCtx.eventBus);
|