evolcore 0.0.25 → 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +632 -565
- package/LICENSE +21 -21
- package/README.md +455 -455
- package/bin/codex-managed-acl.ps1 +203 -160
- package/bin/codex-managed-hook.mjs +208 -208
- package/bin/ec-safe-output.js +327 -327
- package/bin/ec.js +40 -29
- package/bin/install-codex-managed-hooks.mjs +582 -555
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +172 -33
- package/dist/agents/codex/app-server-client.js +76 -31
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +434 -446
- package/dist/agents/ecagent-runner.js +174 -34
- package/dist/agents/gemini-runner.js +8 -7
- package/dist/agents/runner-types.js +75 -19
- package/dist/aun/aid/agentmd.js +151 -36
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +4 -4
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +17 -13
- 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} +162 -39
- package/dist/aun/{menu-client.js → menu/client.js} +85 -6
- 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 +85 -19
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +19 -9
- package/dist/aun/msg/upload.js +43 -80
- 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 +842 -318
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +212 -89
- package/dist/cli/agent-create.js +626 -11
- package/dist/cli/agent.js +125 -13
- package/dist/cli/aun-commands.js +487 -337
- package/dist/cli/bench.js +20 -20
- package/dist/cli/config.js +55 -33
- package/dist/cli/contact.js +72 -15
- package/dist/cli/ctl-command.js +24 -24
- package/dist/cli/daemon-commands.js +140 -63
- package/dist/cli/diag.js +31 -0
- package/dist/cli/fs-command.js +70 -70
- package/dist/cli/handoff-command.js +46 -14
- package/dist/cli/index.js +91 -79
- package/dist/cli/init.js +34 -1
- package/dist/cli/link-rules.js +21 -21
- package/dist/cli/managed-command-guard.js +5 -1
- package/dist/cli/menu-client.js +51 -32
- package/dist/cli/model.js +3 -3
- package/dist/cli/net-check.js +22 -22
- package/dist/cli/queue-command.js +31 -31
- package/dist/cli/response.js +32 -32
- package/dist/cli/restart-monitor.js +103 -42
- package/dist/cli/stats.js +51 -51
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +74 -64
- package/dist/config/builtin-roles.js +11 -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 +160 -0
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +97 -8
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +30 -7
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +7 -1
- 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 +53 -49
- package/dist/core/auth/operation-authorizer.js +90 -7
- package/dist/core/auth/operation-catalog.js +145 -4
- package/dist/core/auth/remote-access-store.js +155 -10
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +111 -20
- 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 +159 -65
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +46 -7
- package/dist/core/command/menu-handler.js +392 -100
- package/dist/core/command/menu-protocol.js +45 -6
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +123 -43
- 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/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +146 -34
- package/dist/core/message/message-bridge.js +163 -20
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +99 -41
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +755 -144
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +112 -41
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +108 -17
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +9 -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 +34 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +588 -217
- package/dist/ipc.js +193 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -0
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/db.js +159 -159
- package/dist/stats/eck-vars.js +9 -9
- package/dist/stats/price-resolver.js +28 -10
- package/dist/stats/query.js +139 -139
- package/dist/stats/role-budget.js +6 -6
- package/dist/stats/writer.js +51 -51
- 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/cross-platform.js +30 -1
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-dict.json +160 -160
- package/dist/utils/error-utils.js +61 -2
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +26 -14
- package/dist/utils/macos-autostart.js +22 -22
- package/dist/utils/model-prices.jsonl +23 -23
- package/dist/utils/npm-ops.js +176 -35
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +196 -23
- package/ecagent/LICENSE +21 -21
- package/ecagent/NOTICE +11 -11
- package/ecagent/README.md +61 -61
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
- package/ecagent/dist/harness/compaction/compaction.js +83 -83
- package/ecagent/dist/harness/messages.js +7 -7
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/GUIDE.md +20 -20
- package/kits/docs/INDEX.md +60 -66
- package/kits/docs/aun/CHEATSHEET.md +19 -19
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
- package/kits/docs/channels/aun.md +69 -56
- package/kits/docs/channels/feishu.md +56 -56
- package/kits/docs/context-assembly.md +379 -367
- package/kits/docs/eck_templates/GUIDE.template.md +22 -22
- package/kits/docs/eck_templates/INDEX.template.md +28 -28
- package/kits/docs/eck_templates/path-registry.template.md +33 -33
- package/kits/docs/eck_templates/runtime.template.md +19 -19
- package/kits/docs/evolcore/INDEX.md +76 -71
- package/kits/docs/evolcore/agent-create.md +202 -165
- package/kits/docs/evolcore/agent.md +99 -67
- package/kits/docs/evolcore/aid.md +68 -60
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
- package/kits/docs/evolcore/config.md +241 -195
- package/kits/docs/evolcore/contact.md +75 -68
- package/kits/docs/evolcore/ctl.md +46 -46
- package/kits/docs/evolcore/event.md +216 -216
- package/kits/docs/evolcore/fs-architecture.md +1215 -1215
- package/kits/docs/evolcore/fs.md +110 -110
- package/kits/docs/evolcore/group-collaboration.md +244 -244
- package/kits/docs/evolcore/group-fs.md +17 -17
- package/kits/docs/evolcore/group-rules.md +251 -251
- package/kits/docs/evolcore/group.md +267 -152
- package/kits/docs/evolcore/menu-client.md +58 -34
- package/kits/docs/evolcore/model.md +50 -50
- package/kits/docs/evolcore/msg.md +169 -165
- package/kits/docs/evolcore/response.md +75 -75
- package/kits/docs/evolcore/rpc.md +37 -37
- package/kits/docs/evolcore/self-summary.md +29 -29
- package/kits/docs/evolcore/stats.md +83 -83
- package/kits/docs/evolcore/storage.md +53 -50
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +623 -575
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
- package/kits/docs/identity/PATH_OPS.md +16 -16
- package/kits/docs/identity/ROLE_DETAIL.md +25 -25
- package/kits/docs/identity/identity-tools.md +26 -26
- package/kits/docs/path-registry.md +62 -62
- package/kits/docs/prompt-loading-architecture.md +269 -269
- package/kits/docs/venues/aun-group.md +45 -45
- package/kits/docs/venues/aun-private.md +10 -10
- package/kits/docs/venues/client-desktop.md +10 -10
- package/kits/docs/venues/client-mobile.md +10 -10
- package/kits/docs/venues/feishu-group.md +13 -13
- package/kits/docs/venues/feishu-private.md +9 -9
- package/kits/docs/venues/group.md +42 -25
- package/kits/docs/venues/private.md +10 -10
- package/kits/eck_manifest.auxiliary.json +43 -43
- package/kits/eck_manifest.json +217 -190
- package/kits/eck_message_manifest.json +74 -74
- package/kits/migrations/README-role-config-v4.md +32 -32
- package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
- package/kits/migrations/migrate-role-config-v4.mjs +614 -614
- package/kits/migrations/migrate-role-config-v5.mjs +365 -365
- package/kits/migrations/rename-config-file.mjs +99 -99
- package/kits/rules/01-overview.md +166 -162
- package/kits/rules/02-navigation.md +96 -96
- package/kits/rules/03-identity.md +36 -36
- package/kits/rules/04-relation.md +76 -75
- package/kits/rules/05-venue.md +60 -60
- package/kits/rules/06-channel.md +65 -63
- package/kits/schemas/_meta.json +53 -53
- package/kits/schemas/agent-config.schema.1.json +177 -177
- package/kits/schemas/agent-config.schema.10.json +395 -395
- package/kits/schemas/agent-config.schema.11.json +421 -421
- package/kits/schemas/agent-config.schema.12.json +451 -427
- package/kits/schemas/agent-config.schema.2.json +239 -239
- package/kits/schemas/agent-config.schema.3.json +119 -119
- package/kits/schemas/agent-config.schema.4.json +208 -208
- package/kits/schemas/agent-config.schema.5.json +326 -326
- package/kits/schemas/agent-config.schema.6.json +322 -322
- package/kits/schemas/agent-config.schema.7.json +303 -303
- package/kits/schemas/agent-config.schema.8.json +304 -304
- package/kits/schemas/agent-config.schema.9.json +364 -364
- package/kits/schemas/contact-book.schema.1.json +36 -36
- package/kits/schemas/contact-book.schema.2.json +43 -43
- package/kits/schemas/contact-book.schema.3.json +67 -67
- package/kits/schemas/daemon.schema.1.json +91 -91
- package/kits/schemas/daemon.schema.2.json +101 -101
- package/kits/schemas/daemon.schema.3.json +123 -123
- package/kits/schemas/daemon.schema.4.json +136 -136
- package/kits/schemas/daemon.schema.5.json +135 -135
- package/kits/schemas/daemon.schema.6.json +131 -131
- package/kits/schemas/defaults.schema.1.json +81 -81
- package/kits/schemas/defaults.schema.2.json +85 -85
- package/kits/schemas/defaults.schema.3.json +73 -73
- package/kits/schemas/defaults.schema.4.json +107 -107
- package/kits/schemas/defaults.schema.5.json +119 -119
- package/kits/schemas/menu-exec-schema-commands.md +208 -208
- package/kits/schemas/migrations/README.md +30 -30
- package/kits/schemas/relation-config.schema.1.json +158 -158
- package/kits/schemas/relation-config.schema.2.json +73 -73
- package/kits/schemas/relation-config.schema.3.json +50 -50
- package/kits/schemas/relation-config.schema.4.json +47 -47
- package/kits/schemas/relation-config.schema.5.json +47 -47
- package/kits/schemas/relation-config.schema.6.json +46 -46
- package/kits/schemas/relation-config.schema.7.json +59 -59
- package/kits/schemas/relation-config.schema.8.json +103 -103
- package/kits/schemas/role-config.schema.1.json +215 -201
- package/kits/schemas/role-registry.schema.1.json +35 -35
- package/kits/schemas/single-session.schema.1.json +31 -31
- package/kits/schemas/single-session.schema.2.json +57 -57
- package/kits/schemas/single-session.schema.3.json +32 -32
- package/kits/templates/bootstrap-welcome.md +17 -17
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
- package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
- package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
- package/kits/templates/message-fragments/inject-default.md +2 -2
- package/kits/templates/message-fragments/item.md +2 -2
- package/kits/templates/roles/admin.json +92 -92
- package/kits/templates/roles/member.json +101 -96
- package/kits/templates/roles/owner.json +9 -9
- package/kits/templates/roles/visitor.json +90 -85
- package/kits/templates/system-fragments/baseagent.md +14 -14
- package/kits/templates/system-fragments/bootstrap.md +53 -34
- package/kits/templates/system-fragments/channel.md +28 -64
- package/kits/templates/system-fragments/commands.md +36 -28
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/identity.md +13 -13
- package/kits/templates/system-fragments/relation.md +19 -19
- package/kits/templates/system-fragments/reply.md +56 -0
- package/kits/templates/system-fragments/session.md +59 -59
- package/kits/templates/system-fragments/venue.md +31 -31
- package/package.json +104 -98
- 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,9 +1,11 @@
|
|
|
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';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import os from 'os';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
9
|
import { logger, localTimestamp } from '../utils/logger.js';
|
|
8
10
|
import { LogWriter } from '../utils/log-writer.js';
|
|
9
11
|
import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
|
|
@@ -14,6 +16,7 @@ import { appendAidEvent } from '../utils/instance-registry.js';
|
|
|
14
16
|
import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
|
|
15
17
|
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
16
18
|
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
19
|
+
import { readTopicLable } from '../core/session/session-mapper.js';
|
|
17
20
|
import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
|
|
18
21
|
import { appendAidLifecycle } from '../aun/aid/identity.js';
|
|
19
22
|
import { enableFullGroupPullPagination, enableReliableMessageDelivery, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
|
|
@@ -25,18 +28,20 @@ import { isManagementRole } from '../config/builtin-roles.js';
|
|
|
25
28
|
import { normalizeMentionMode } from '../config/mention-mode.js';
|
|
26
29
|
import { getProcessStartTime } from '../utils/process-introspect.js';
|
|
27
30
|
import * as outbox from '../aun/outbox.js';
|
|
28
|
-
import {
|
|
31
|
+
import { formatSize } from '../utils/media-cache.js';
|
|
29
32
|
import { formatPeerKey, PeerIdentityCache } from '../core/relation/peer-identity.js';
|
|
30
33
|
import { isHClassPath } from '../core/permission/protected-paths.js';
|
|
31
34
|
import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
|
|
32
35
|
import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
|
|
33
36
|
import { recordCausationSpan } from '../core/causation/audit.js';
|
|
34
|
-
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';
|
|
35
39
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
40
|
+
import { ManagedMenuClient, menuSenderDeviceId, menuTransportParams, } from '../aun/menu/index.js';
|
|
36
41
|
import { toolInputForDisplay } from '../utils/tool-summary.js';
|
|
37
|
-
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';
|
|
38
43
|
import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
|
|
39
|
-
import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
|
|
44
|
+
import { aunOutboundAttention, normalizeAunMentionEntries, parseAunAttention, withAunAttention, } from '../aun/msg/mention-schema.js';
|
|
40
45
|
import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
41
46
|
import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
|
|
42
47
|
export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
|
|
@@ -49,9 +54,148 @@ const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
|
|
|
49
54
|
const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
|
|
50
55
|
const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
|
|
51
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
|
+
}
|
|
52
151
|
export function formatAunSendLogText(text) {
|
|
53
152
|
return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
|
|
54
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Echo trace 行的墙钟前缀。
|
|
156
|
+
*
|
|
157
|
+
* 格式必须保持 `HH:MM:SS.mmm` 且毫秒后紧跟空格——Evol App 的
|
|
158
|
+
* `evol/lib/utils/echo_utils.dart` 用 `^(\d{2}):(\d{2}):(\d{2})\.(\d{3})\s` 匹配这一段,
|
|
159
|
+
* 在毫秒和空格之间插任何东西(比如时区偏移)都会让旧版 App 静默失配、不再显示 RTT。
|
|
160
|
+
* 时区信息改由行尾的 {@link echoTraceOriginLabel} 提供,前缀一字不动,两端发版顺序因此解耦。
|
|
161
|
+
*/
|
|
162
|
+
export function echoTraceClock(now = Date.now()) {
|
|
163
|
+
const d = new Date(now);
|
|
164
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
165
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Echo trace 行尾的权威时间标记:`epoch=<ms> tz=<±HH:MM>`。
|
|
169
|
+
*
|
|
170
|
+
* 前缀的墙钟没有日期也没有时区,消费方只能按自己的本地时区解析,两端 TZ 不一致时算出的
|
|
171
|
+
* RTT 会整小时偏移(UTC 对 Asia/Shanghai 就是 +28800000ms),跨午夜还会得到负值。
|
|
172
|
+
* `epoch` 是绝对值,消费方应当用它相减算延迟;`tz` 仅供人工核对墙钟,不参与计算。
|
|
173
|
+
*/
|
|
174
|
+
export function echoTraceOriginLabel(now = Date.now()) {
|
|
175
|
+
const d = new Date(now);
|
|
176
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
177
|
+
const offsetMinutes = -d.getTimezoneOffset();
|
|
178
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
179
|
+
const absOffset = Math.abs(offsetMinutes);
|
|
180
|
+
return ` epoch=${now} tz=${sign}${pad(Math.floor(absOffset / 60))}:${pad(absOffset % 60)}`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 入站腿延迟(对端/网关发出 → 本进程收到)。
|
|
184
|
+
*
|
|
185
|
+
* `created_at` 由对端或网关打戳,因此结果含对端时钟漂移,只能当估算值——标签用
|
|
186
|
+
* `inbound_est` 而非 `inbound` 就是为了让消费方知道这点。负值说明对端时钟快于本机,
|
|
187
|
+
* 照实输出而不夹到 0:那本身就是有用的时钟偏移信号。
|
|
188
|
+
*/
|
|
189
|
+
export function echoInboundLegLabel(createdAt, receivedAt) {
|
|
190
|
+
if (typeof createdAt !== 'number' || !Number.isFinite(createdAt) || createdAt <= 0)
|
|
191
|
+
return '';
|
|
192
|
+
// 网关历史上有按秒打戳的实现,按量级归一到毫秒。
|
|
193
|
+
const createdMs = createdAt < 1e12 ? createdAt * 1000 : createdAt;
|
|
194
|
+
const delta = receivedAt - createdMs;
|
|
195
|
+
if (!Number.isFinite(delta))
|
|
196
|
+
return '';
|
|
197
|
+
return ` inbound_est=${Math.round(delta)}ms`;
|
|
198
|
+
}
|
|
55
199
|
function attachmentDownloadHost(url) {
|
|
56
200
|
try {
|
|
57
201
|
return new URL(url).host || '<unknown>';
|
|
@@ -201,7 +345,7 @@ export function buildAunFilePayload(params) {
|
|
|
201
345
|
payload.name = params.filename;
|
|
202
346
|
payload.content_type = params.contentType;
|
|
203
347
|
}
|
|
204
|
-
return payload;
|
|
348
|
+
return withAunAttention(payload, params.context?.attention);
|
|
205
349
|
}
|
|
206
350
|
const AUN_PERMANENT_ERROR_CODES = new Set([
|
|
207
351
|
400, 401, 403, 404,
|
|
@@ -431,6 +575,8 @@ export function aunOptsToInbound(opts, channel, channelType) {
|
|
|
431
575
|
content: opts.content,
|
|
432
576
|
chatType: opts.chatType || 'private',
|
|
433
577
|
peerId: opts.peerId || '',
|
|
578
|
+
peerDeviceId: opts.peerDeviceId,
|
|
579
|
+
peerSlotId: opts.peerSlotId,
|
|
434
580
|
peerName: opts.peerName,
|
|
435
581
|
peerType: opts.peerType,
|
|
436
582
|
sameDevice: opts.sameDevice,
|
|
@@ -473,22 +619,8 @@ function verifiedProtectedHeaders(msg, env) {
|
|
|
473
619
|
}
|
|
474
620
|
return undefined;
|
|
475
621
|
}
|
|
476
|
-
function redactMenuTokensForTrace(value
|
|
477
|
-
|
|
478
|
-
return value;
|
|
479
|
-
if (seen.has(value))
|
|
480
|
-
return '[Circular]';
|
|
481
|
-
seen.add(value);
|
|
482
|
-
if (Array.isArray(value))
|
|
483
|
-
return value.map(item => redactMenuTokensForTrace(item, seen));
|
|
484
|
-
const redacted = {};
|
|
485
|
-
for (const [key, item] of Object.entries(value)) {
|
|
486
|
-
const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
|
|
487
|
-
redacted[key] = /^(?:menu|ecweb)_token$/.test(normalizedKey)
|
|
488
|
-
? '[REDACTED]'
|
|
489
|
-
: redactMenuTokensForTrace(item, seen);
|
|
490
|
-
}
|
|
491
|
-
return redacted;
|
|
622
|
+
export function redactMenuTokensForTrace(value) {
|
|
623
|
+
return sanitizeMenuLogValue(value);
|
|
492
624
|
}
|
|
493
625
|
function payloadTypeForObserver(value) {
|
|
494
626
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -502,9 +634,26 @@ function payloadTypeForObserver(value) {
|
|
|
502
634
|
const type = payload.type;
|
|
503
635
|
return typeof type === 'string' ? type : undefined;
|
|
504
636
|
}
|
|
637
|
+
function errorCodeForLog(error) {
|
|
638
|
+
if (!error || typeof error !== 'object')
|
|
639
|
+
return '';
|
|
640
|
+
const value = error;
|
|
641
|
+
return typeof value.code === 'string' || typeof value.code === 'number' ? String(value.code) : '';
|
|
642
|
+
}
|
|
505
643
|
export class AUNChannel {
|
|
506
644
|
config;
|
|
645
|
+
managedMenuClient = new ManagedMenuClient(params => {
|
|
646
|
+
if (!this.connected || !this.client)
|
|
647
|
+
return Promise.reject(new Error('AUN channel is not connected'));
|
|
648
|
+
return this.callAndTrace('message.send', params);
|
|
649
|
+
});
|
|
650
|
+
requestRemoteMenu(options) {
|
|
651
|
+
if (options.fromAid !== (this._aid || this.config.aid))
|
|
652
|
+
throw new Error('Menu sender does not match channel identity');
|
|
653
|
+
return this.managedMenuClient.request(options);
|
|
654
|
+
}
|
|
507
655
|
client = null;
|
|
656
|
+
localSenderInstallation;
|
|
508
657
|
store = null;
|
|
509
658
|
/** 实际连接的网关 URL(来自 authenticate() 返回值 / connection.state 事件),替代旧 (client as any)._gatewayUrl。 */
|
|
510
659
|
gatewayUrl = '';
|
|
@@ -515,6 +664,10 @@ export class AUNChannel {
|
|
|
515
664
|
traceWriter = null;
|
|
516
665
|
eventBus = null;
|
|
517
666
|
queuedHandler = null;
|
|
667
|
+
groupInviteHandler;
|
|
668
|
+
groupInviteConnectedHandler;
|
|
669
|
+
groupInviteDeliveryGate;
|
|
670
|
+
groupInviteDelivered;
|
|
518
671
|
pendingEchoMessages = new Map();
|
|
519
672
|
isEchoSending = false;
|
|
520
673
|
agentDir;
|
|
@@ -562,46 +715,89 @@ export class AUNChannel {
|
|
|
562
715
|
* 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
|
|
563
716
|
* 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
|
|
564
717
|
*/
|
|
565
|
-
callClient(method, params) {
|
|
566
|
-
|
|
718
|
+
callClient(method, params, lane = 'normal', onStart) {
|
|
719
|
+
params = menuTransportParams(method, params);
|
|
720
|
+
return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)), lane, onStart);
|
|
567
721
|
}
|
|
568
|
-
withOutboundSendGate(method, run) {
|
|
569
|
-
if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method))
|
|
722
|
+
withOutboundSendGate(method, run, lane = 'normal', onStart) {
|
|
723
|
+
if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method)) {
|
|
724
|
+
onStart?.();
|
|
570
725
|
return run();
|
|
726
|
+
}
|
|
727
|
+
const queue = lane === 'observer' ? this.observerSendQueue : this.outboundSendQueue;
|
|
728
|
+
if (lane === 'observer' && queue.length >= AUNChannel.OBSERVER_SEND_QUEUE_MAX) {
|
|
729
|
+
this.noteObserverQueueDrop();
|
|
730
|
+
const error = Object.assign(new Error('observer send queue is full'), {
|
|
731
|
+
code: 'AUN_OBSERVER_QUEUE_FULL',
|
|
732
|
+
});
|
|
733
|
+
return Promise.reject(error);
|
|
734
|
+
}
|
|
571
735
|
return new Promise((resolve, reject) => {
|
|
572
|
-
|
|
736
|
+
queue.push({
|
|
573
737
|
run: run,
|
|
574
738
|
resolve: resolve,
|
|
575
739
|
reject,
|
|
740
|
+
onStart,
|
|
576
741
|
});
|
|
577
742
|
this.pumpOutboundSendGate();
|
|
578
743
|
});
|
|
579
744
|
}
|
|
580
745
|
pumpOutboundSendGate() {
|
|
581
|
-
while (this.outboundSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
|
|
582
|
-
|
|
583
|
-
|
|
746
|
+
while (this.outboundSendActive + this.observerSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY && this.outboundSendQueue.length > 0) {
|
|
747
|
+
this.startOutboundSendJob(this.outboundSendQueue.shift(), 'normal');
|
|
748
|
+
}
|
|
749
|
+
while (this.outboundSendActive + this.observerSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
|
|
750
|
+
&& this.observerSendActive < AUNChannel.OBSERVER_SEND_CONCURRENCY
|
|
751
|
+
&& this.outboundSendQueue.length === 0
|
|
752
|
+
&& this.observerSendQueue.length > 0) {
|
|
753
|
+
this.startOutboundSendJob(this.observerSendQueue.shift(), 'observer');
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
clearQueuedObserverSends(reason) {
|
|
757
|
+
if (this.observerSendQueue.length === 0)
|
|
758
|
+
return;
|
|
759
|
+
const error = Object.assign(new Error(`observer send discarded while ${reason}`), {
|
|
760
|
+
code: 'AUN_OBSERVER_QUEUE_DROPPED',
|
|
761
|
+
});
|
|
762
|
+
const queued = this.observerSendQueue.splice(0);
|
|
763
|
+
for (const job of queued)
|
|
764
|
+
job.reject(error);
|
|
765
|
+
this.noteObserverQueueDrop(queued.length);
|
|
766
|
+
}
|
|
767
|
+
startOutboundSendJob(job, lane) {
|
|
768
|
+
if (lane === 'observer')
|
|
769
|
+
this.observerSendActive++;
|
|
770
|
+
else
|
|
584
771
|
this.outboundSendActive++;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
772
|
+
Promise.resolve()
|
|
773
|
+
.then(() => {
|
|
774
|
+
job.onStart?.();
|
|
775
|
+
return job.run();
|
|
776
|
+
})
|
|
777
|
+
.then(job.resolve, job.reject)
|
|
778
|
+
.finally(() => {
|
|
779
|
+
if (lane === 'observer')
|
|
780
|
+
this.observerSendActive--;
|
|
781
|
+
else
|
|
589
782
|
this.outboundSendActive--;
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
783
|
+
this.pumpOutboundSendGate();
|
|
784
|
+
});
|
|
593
785
|
}
|
|
594
786
|
async callAndTrace(method, params, opts) {
|
|
595
787
|
this.trace('OUT', method, params);
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
// 是 /evolhelp 等命令「回复慢/无回复」的直接可观测信号。
|
|
788
|
+
// 计时同时覆盖排队和 SDK 往返;callStartedAt 让日志能区分「本地队列堵塞」
|
|
789
|
+
// 与「网关慢」。两者对 /evolhelp 等命令的回复延迟都有直接影响。
|
|
599
790
|
const rpcStart = Date.now();
|
|
600
791
|
// SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
|
|
601
792
|
const SLOW_RPC_WARN_MS = 3000;
|
|
793
|
+
let callStartedAt = rpcStart;
|
|
602
794
|
try {
|
|
603
|
-
const result = await this.callClient(method, params)
|
|
795
|
+
const result = await this.callClient(method, params, opts?.lane ?? 'normal', () => {
|
|
796
|
+
callStartedAt = Date.now();
|
|
797
|
+
});
|
|
604
798
|
const durationMs = Date.now() - rpcStart;
|
|
799
|
+
const rpcDurationMs = Date.now() - callStartedAt;
|
|
800
|
+
const queueWaitMs = Math.max(0, callStartedAt - rpcStart);
|
|
605
801
|
if (!opts?.silentOk) {
|
|
606
802
|
const r = result;
|
|
607
803
|
const snap = r && typeof r === 'object'
|
|
@@ -609,25 +805,66 @@ export class AUNChannel {
|
|
|
609
805
|
: { duration_ms: durationMs };
|
|
610
806
|
this.trace('OUT', `${method}.ok`, snap);
|
|
611
807
|
}
|
|
612
|
-
if (durationMs >= SLOW_RPC_WARN_MS)
|
|
613
|
-
|
|
614
|
-
}
|
|
808
|
+
if (durationMs >= SLOW_RPC_WARN_MS)
|
|
809
|
+
this.logRpcWarning('slow', method, params, durationMs, queueWaitMs, rpcDurationMs);
|
|
615
810
|
return result;
|
|
616
811
|
}
|
|
617
812
|
catch (e) {
|
|
618
813
|
const durationMs = Date.now() - rpcStart;
|
|
814
|
+
const queueWaitMs = Math.max(0, callStartedAt - rpcStart);
|
|
815
|
+
const rpcDurationMs = Math.max(0, Date.now() - callStartedAt);
|
|
619
816
|
this.trace('OUT', `${method}.error`, {
|
|
620
817
|
error: e?.message ?? String(e),
|
|
621
818
|
code: e?.code,
|
|
622
819
|
name: e?.name,
|
|
623
820
|
duration_ms: durationMs,
|
|
624
821
|
});
|
|
625
|
-
if (!opts?.silentError) {
|
|
626
|
-
|
|
822
|
+
if (!opts?.silentError && e?.code !== 'AUN_OBSERVER_QUEUE_FULL' && e?.code !== 'AUN_OBSERVER_QUEUE_DROPPED') {
|
|
823
|
+
this.logRpcWarning('failed', method, params, durationMs, queueWaitMs, rpcDurationMs, e);
|
|
627
824
|
}
|
|
628
825
|
throw e;
|
|
629
826
|
}
|
|
630
827
|
}
|
|
828
|
+
logRpcWarning(kind, method, params, durationMs, queueWaitMs, rpcDurationMs, error) {
|
|
829
|
+
const payloadType = payloadTypeForObserver(params) ?? 'unknown';
|
|
830
|
+
const code = kind === 'failed' ? errorCodeForLog(error) : '';
|
|
831
|
+
const key = `${kind}|${method}|${payloadType}`;
|
|
832
|
+
const now = Date.now();
|
|
833
|
+
const state = this.rpcWarningStates.get(key) ?? { lastWarnAt: 0, suppressed: 0, maxDurationMs: 0 };
|
|
834
|
+
state.maxDurationMs = Math.max(state.maxDurationMs, durationMs);
|
|
835
|
+
if (state.lastWarnAt > 0 && now - state.lastWarnAt < AUNChannel.RPC_WARN_INTERVAL_MS) {
|
|
836
|
+
state.suppressed++;
|
|
837
|
+
this.rpcWarningStates.set(key, state);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const suppressed = state.suppressed;
|
|
841
|
+
const maxDurationMs = state.maxDurationMs;
|
|
842
|
+
state.lastWarnAt = now;
|
|
843
|
+
state.suppressed = 0;
|
|
844
|
+
state.maxDurationMs = 0;
|
|
845
|
+
this.rpcWarningStates.set(key, state);
|
|
846
|
+
const summary = suppressed > 0 ? ` suppressed=${suppressed} max_duration=${maxDurationMs}ms` : '';
|
|
847
|
+
if (kind === 'failed') {
|
|
848
|
+
const name = error instanceof Error ? error.name : '';
|
|
849
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
850
|
+
const errorLabel = `${name}${code ? `(${code})` : ''} ${message}`.trim();
|
|
851
|
+
logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${errorLabel} queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary}`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary} (发送管线可能饱和或链路抖动)`);
|
|
855
|
+
}
|
|
856
|
+
observerQueueDropCount = 0;
|
|
857
|
+
observerQueueDropLastWarnAt = 0;
|
|
858
|
+
noteObserverQueueDrop(count = 1) {
|
|
859
|
+
this.observerQueueDropCount += count;
|
|
860
|
+
const now = Date.now();
|
|
861
|
+
if (now - this.observerQueueDropLastWarnAt < AUNChannel.RPC_WARN_INTERVAL_MS)
|
|
862
|
+
return;
|
|
863
|
+
const dropped = this.observerQueueDropCount;
|
|
864
|
+
this.observerQueueDropCount = 0;
|
|
865
|
+
this.observerQueueDropLastWarnAt = now;
|
|
866
|
+
logger.warn(`${this.logPrefix()} observer send queue full: dropped=${dropped} (observer telemetry is being sampled while the gateway is unavailable)`);
|
|
867
|
+
}
|
|
631
868
|
/** Resolve the authenticated transport route for an ordinary outbound send.
|
|
632
869
|
* The channel id is an address only; it is intentionally never used to infer
|
|
633
870
|
* whether the address is a group.
|
|
@@ -689,11 +926,17 @@ export class AUNChannel {
|
|
|
689
926
|
return;
|
|
690
927
|
}
|
|
691
928
|
const context = { delivery };
|
|
692
|
-
if (threadId)
|
|
929
|
+
if (typeof threadId === 'string')
|
|
693
930
|
context.threadId = threadId;
|
|
694
931
|
if (operatorId && delivery.chatType === 'group')
|
|
695
932
|
context.peerId = operatorId;
|
|
696
|
-
|
|
933
|
+
const payload = {
|
|
934
|
+
type: 'notice',
|
|
935
|
+
...this.buildTaskPayloadBase({}, context),
|
|
936
|
+
text,
|
|
937
|
+
severity: 'info',
|
|
938
|
+
};
|
|
939
|
+
void this.sendReliableStructured(channelId, payload, context, text).catch((err) => {
|
|
697
940
|
logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
|
|
698
941
|
});
|
|
699
942
|
}
|
|
@@ -713,6 +956,12 @@ export class AUNChannel {
|
|
|
713
956
|
: typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
|
|
714
957
|
const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
|
|
715
958
|
const cardDelivery = cardInfo?.delivery ?? delivery;
|
|
959
|
+
const savedTopic = this.ownedCardTopics.get(cardMsgId);
|
|
960
|
+
// Keep the topic after the actionable mapping expires, including for
|
|
961
|
+
// older clients that omit thread_id in their callbacks.
|
|
962
|
+
const threadId = cardInfo?.replyContext?.threadId
|
|
963
|
+
?? (savedTopic?.channelId === channelId ? savedTopic?.threadId : undefined)
|
|
964
|
+
?? (typeof obj.thread_id === 'string' ? obj.thread_id : undefined);
|
|
716
965
|
if (cardInfo) {
|
|
717
966
|
// compose 按钮由新版客户端在卡片内采集输入;旧版客户端可继续从主编辑器
|
|
718
967
|
// 采集。两者最终都回传 action_card_reply{behavior:"compose", text}。
|
|
@@ -728,10 +977,6 @@ export class AUNChannel {
|
|
|
728
977
|
const actionValue = isComposeReply ? '_custom_input'
|
|
729
978
|
: typeof obj.value === 'string' ? obj.value
|
|
730
979
|
: typeof obj.action_value === 'string' ? obj.action_value : text;
|
|
731
|
-
// Prefer the trusted context persisted with the card. A callback's
|
|
732
|
-
// thread_id is client-controlled and may be absent or stale.
|
|
733
|
-
const threadId = cardInfo?.replyContext?.threadId
|
|
734
|
-
?? (typeof obj.thread_id === 'string' ? obj.thread_id : undefined);
|
|
735
980
|
// 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
|
|
736
981
|
// payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
|
|
737
982
|
// 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
|
|
@@ -774,9 +1019,11 @@ export class AUNChannel {
|
|
|
774
1019
|
peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
|
|
775
1020
|
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
776
1021
|
threadId,
|
|
777
|
-
replyContext:
|
|
778
|
-
|
|
779
|
-
:
|
|
1022
|
+
replyContext: {
|
|
1023
|
+
...(cardInfo.replyContext ?? {}),
|
|
1024
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
1025
|
+
...(cardDelivery ? { delivery: cardDelivery } : {}),
|
|
1026
|
+
},
|
|
780
1027
|
source: 'card-trigger',
|
|
781
1028
|
})).catch((err) => {
|
|
782
1029
|
logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
|
|
@@ -887,9 +1134,9 @@ export class AUNChannel {
|
|
|
887
1134
|
}
|
|
888
1135
|
}
|
|
889
1136
|
else if (this.ownedCardMsgIds.has(cardMsgId)) {
|
|
890
|
-
// 本 agent 发出的卡片,但 entry
|
|
1137
|
+
// 本 agent 发出的卡片,但 entry 已过期或被清理。
|
|
891
1138
|
logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
|
|
892
|
-
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起',
|
|
1139
|
+
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', senderAid, threadId, delivery);
|
|
893
1140
|
}
|
|
894
1141
|
else {
|
|
895
1142
|
// 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
|
|
@@ -1136,14 +1383,14 @@ export class AUNChannel {
|
|
|
1136
1383
|
return sanitized;
|
|
1137
1384
|
return undefined;
|
|
1138
1385
|
}
|
|
1139
|
-
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
|
|
1386
|
+
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, lable) {
|
|
1140
1387
|
if (!groupId)
|
|
1141
1388
|
throw new Error('buildGroupReplyContext requires groupId');
|
|
1142
1389
|
const replyContext = {
|
|
1143
1390
|
metadata: { encrypted, chatmode },
|
|
1144
1391
|
delivery: { chatType: 'group', groupId },
|
|
1145
1392
|
};
|
|
1146
|
-
if (threadId)
|
|
1393
|
+
if (typeof threadId === 'string')
|
|
1147
1394
|
replyContext.threadId = threadId;
|
|
1148
1395
|
replyContext.peerId = senderAid;
|
|
1149
1396
|
if (messageId)
|
|
@@ -1154,20 +1401,24 @@ export class AUNChannel {
|
|
|
1154
1401
|
replyContext.metadata = {};
|
|
1155
1402
|
replyContext.metadata.topicName = topicName;
|
|
1156
1403
|
}
|
|
1404
|
+
if (lable && threadId)
|
|
1405
|
+
replyContext.metadata.lable = lable;
|
|
1157
1406
|
return replyContext;
|
|
1158
1407
|
}
|
|
1159
|
-
buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
|
|
1408
|
+
buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, lable) {
|
|
1160
1409
|
const replyContext = {
|
|
1161
1410
|
metadata: { encrypted, chatmode, refMessageId },
|
|
1162
1411
|
delivery: { chatType: 'private' },
|
|
1163
1412
|
peerId: senderAid,
|
|
1164
1413
|
};
|
|
1165
|
-
if (threadId)
|
|
1414
|
+
if (typeof threadId === 'string')
|
|
1166
1415
|
replyContext.threadId = threadId;
|
|
1167
1416
|
if (messageId)
|
|
1168
1417
|
replyContext.replyToMessageId = messageId;
|
|
1169
1418
|
if (topicName && threadId)
|
|
1170
1419
|
replyContext.metadata.topicName = topicName;
|
|
1420
|
+
if (lable && threadId)
|
|
1421
|
+
replyContext.metadata.lable = lable;
|
|
1171
1422
|
return replyContext;
|
|
1172
1423
|
}
|
|
1173
1424
|
claimInboundMessage(kind, messageId, seq) {
|
|
@@ -1284,6 +1535,7 @@ export class AUNChannel {
|
|
|
1284
1535
|
invalidInboundEnvelopeLastLogAt = 0;
|
|
1285
1536
|
groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
|
|
1286
1537
|
peerInfoCache = new Map();
|
|
1538
|
+
peerInfoRevision = 0;
|
|
1287
1539
|
messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
|
|
1288
1540
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
1289
1541
|
peerE2ee = new Map();
|
|
@@ -1295,6 +1547,7 @@ export class AUNChannel {
|
|
|
1295
1547
|
cardMessageIdMap = new Map();
|
|
1296
1548
|
/** 本 agent 曾发出过的卡片 msgId(只增不删,用于区分"过期失效"vs"他人发的卡") */
|
|
1297
1549
|
ownedCardMsgIds = new Set();
|
|
1550
|
+
ownedCardTopics = new Map();
|
|
1298
1551
|
/** requestId → 已投递的卡片 message_id 集合(撤权/超时时撤回) */
|
|
1299
1552
|
interactionCardMessageIds = new Map();
|
|
1300
1553
|
/** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
|
|
@@ -1304,7 +1557,7 @@ export class AUNChannel {
|
|
|
1304
1557
|
groupSlashRequireMentionsResolver;
|
|
1305
1558
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
1306
1559
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
1307
|
-
'merge', 'link', 'location', 'personal_card',
|
|
1560
|
+
'merge', 'link', 'location', 'personal_card', 'contact.request.response',
|
|
1308
1561
|
]);
|
|
1309
1562
|
/** 观察者插话请求类型(owner → agent.AID)。详见 docs/observer-insert-design.md。 */
|
|
1310
1563
|
static INJECT_REQUEST_TYPE = 'observer.inject';
|
|
@@ -1344,7 +1597,7 @@ export class AUNChannel {
|
|
|
1344
1597
|
aidState;
|
|
1345
1598
|
aidStatsCollector;
|
|
1346
1599
|
outboxInFlight = new Set();
|
|
1347
|
-
/** Shared gateway-facing send
|
|
1600
|
+
/** Shared total gateway-facing send capacity; normal traffic has priority over observer telemetry. */
|
|
1348
1601
|
static OUTBOUND_SEND_METHODS = new Set([
|
|
1349
1602
|
'message.send',
|
|
1350
1603
|
'group.send',
|
|
@@ -1352,8 +1605,14 @@ export class AUNChannel {
|
|
|
1352
1605
|
'group.thought.put',
|
|
1353
1606
|
]);
|
|
1354
1607
|
static OUTBOUND_SEND_CONCURRENCY = 4;
|
|
1608
|
+
static OBSERVER_SEND_CONCURRENCY = 1;
|
|
1609
|
+
static OBSERVER_SEND_QUEUE_MAX = 256;
|
|
1610
|
+
static RPC_WARN_INTERVAL_MS = 60_000;
|
|
1355
1611
|
outboundSendActive = 0;
|
|
1356
1612
|
outboundSendQueue = [];
|
|
1613
|
+
observerSendActive = 0;
|
|
1614
|
+
observerSendQueue = [];
|
|
1615
|
+
rpcWarningStates = new Map();
|
|
1357
1616
|
constructor(config) {
|
|
1358
1617
|
this.config = config;
|
|
1359
1618
|
this.agentDir = agentDirPath(config.aid);
|
|
@@ -1391,6 +1650,44 @@ export class AUNChannel {
|
|
|
1391
1650
|
getAid() {
|
|
1392
1651
|
return this._aid ?? this.config.aid;
|
|
1393
1652
|
}
|
|
1653
|
+
setGroupInviteReviewHandlers(handlers) {
|
|
1654
|
+
if (this.config.pureIdentity && handlers)
|
|
1655
|
+
return;
|
|
1656
|
+
this.groupInviteHandler = handlers?.onInvite;
|
|
1657
|
+
this.groupInviteConnectedHandler = handlers?.onConnected;
|
|
1658
|
+
this.groupInviteDeliveryGate = handlers?.canDeliver;
|
|
1659
|
+
this.groupInviteDelivered = handlers?.onDelivered;
|
|
1660
|
+
}
|
|
1661
|
+
async getGroupInviteSummary(groupId) {
|
|
1662
|
+
if (!this.client || !this.connected)
|
|
1663
|
+
return {};
|
|
1664
|
+
try {
|
|
1665
|
+
const result = await this.callAndTrace('group.get_info', { group_id: groupId });
|
|
1666
|
+
const group = result?.group ?? result;
|
|
1667
|
+
return {
|
|
1668
|
+
...(typeof group?.name === 'string' ? { name: group.name.slice(0, 120) } : {}),
|
|
1669
|
+
...(typeof group?.description === 'string' ? { description: group.description.slice(0, 240) } : {}),
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
catch {
|
|
1673
|
+
return {};
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
getGroupInviteDeliveryState(outboxId) {
|
|
1677
|
+
return outbox.deliveryState(this.config.aid, outboxId);
|
|
1678
|
+
}
|
|
1679
|
+
async invalidateGroupInviteCard(interactionId, ownerAid, messageIds) {
|
|
1680
|
+
const known = new Set(this.interactionCardMessageIds.get(interactionId) ?? []);
|
|
1681
|
+
await this.invalidateInteraction(interactionId, 'group_invite_review');
|
|
1682
|
+
// After a daemon restart only the review store knows the delivered cards.
|
|
1683
|
+
for (const id of messageIds) {
|
|
1684
|
+
if (!known.has(id))
|
|
1685
|
+
await this.recallInteractionCard(id, ownerAid, { chatType: 'private' });
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
refreshGroupInviteMembership(groupId) {
|
|
1689
|
+
this.invalidateObserverGroupMembers(groupId);
|
|
1690
|
+
}
|
|
1394
1691
|
/** 是否已连接(client 存在且认证完成)。 */
|
|
1395
1692
|
isConnected() {
|
|
1396
1693
|
return this.connected && this.client !== null;
|
|
@@ -1425,6 +1722,27 @@ export class AUNChannel {
|
|
|
1425
1722
|
this.initInProgress = false;
|
|
1426
1723
|
}
|
|
1427
1724
|
}
|
|
1725
|
+
/** Optional local compatibility code is never required by a clean checkout or npm install. */
|
|
1726
|
+
async loadLocalSenderInstallation(client) {
|
|
1727
|
+
this.localSenderInstallation = undefined;
|
|
1728
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
1729
|
+
const modulePath = path.join(path.dirname(currentFile), '..', 'aun', `sender-installation${path.extname(currentFile)}`);
|
|
1730
|
+
if (!fs.existsSync(modulePath))
|
|
1731
|
+
return;
|
|
1732
|
+
try {
|
|
1733
|
+
// Use a runtime URL so TypeScript can build when the local file is absent.
|
|
1734
|
+
const extension = await import(pathToFileURL(modulePath).href);
|
|
1735
|
+
if (typeof extension.enableAunSenderInstallation !== 'function'
|
|
1736
|
+
|| typeof extension.aunSenderInstallation !== 'function') {
|
|
1737
|
+
throw new Error('Invalid local sender installation module');
|
|
1738
|
+
}
|
|
1739
|
+
extension.enableAunSenderInstallation(client);
|
|
1740
|
+
this.localSenderInstallation = extension;
|
|
1741
|
+
}
|
|
1742
|
+
catch (error) {
|
|
1743
|
+
logger.warn(`${this.logPrefix()} Optional local sender installation disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1428
1746
|
async _initClientInner() {
|
|
1429
1747
|
this.observerGroupMembers.clear();
|
|
1430
1748
|
for (const groupId of new Set([
|
|
@@ -1454,6 +1772,7 @@ export class AUNChannel {
|
|
|
1454
1772
|
this.store = null;
|
|
1455
1773
|
}
|
|
1456
1774
|
this.connected = false;
|
|
1775
|
+
this.clearQueuedObserverSends('client reinitialization');
|
|
1457
1776
|
this.lastConnectionStateSignature = '';
|
|
1458
1777
|
const aunPath = this.config.keystorePath || resolveRoot();
|
|
1459
1778
|
const aidName = this.config.aid;
|
|
@@ -1473,11 +1792,39 @@ export class AUNChannel {
|
|
|
1473
1792
|
const client = await loadClient(store, aidName);
|
|
1474
1793
|
enableFullGroupPullPagination(client);
|
|
1475
1794
|
enableReliableMessageDelivery(client);
|
|
1795
|
+
await this.loadLocalSenderInstallation(client);
|
|
1476
1796
|
this.client = client;
|
|
1477
1797
|
// A reconnect replaces the SDK client. Late events from the retired
|
|
1478
1798
|
// instance must not mutate the new instance's health or message state.
|
|
1479
1799
|
const isCurrentClient = () => this.client === client;
|
|
1480
1800
|
// Register event handlers before connecting
|
|
1801
|
+
client.on('agentmd.changed', (data) => {
|
|
1802
|
+
if (!isCurrentClient() || this.intentionalDisconnect || typeof data !== 'string')
|
|
1803
|
+
return;
|
|
1804
|
+
let aid;
|
|
1805
|
+
try {
|
|
1806
|
+
aid = validateAIDFormat(data);
|
|
1807
|
+
}
|
|
1808
|
+
catch {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
this.peerInfoRevision++;
|
|
1812
|
+
this.peerInfoCache.delete(aid);
|
|
1813
|
+
this.groupNameCache.delete(aid);
|
|
1814
|
+
invalidateAgentDisplayName(aid);
|
|
1815
|
+
PeerIdentityCache.invalidate('aun', aid, path.join(resolvePaths().agentsDir, this.config.aid));
|
|
1816
|
+
this.trace('IN', 'agentmd.changed', { aid });
|
|
1817
|
+
// Peer snapshots refresh lazily on the next lookup. The self name is
|
|
1818
|
+
// held separately by this channel, so refresh it with the existing API.
|
|
1819
|
+
if (aid === (this._aid || this.config.aid)) {
|
|
1820
|
+
void refreshAgentDisplayName(aid).then(name => {
|
|
1821
|
+
if (!name || !isCurrentClient() || this.intentionalDisconnect)
|
|
1822
|
+
return;
|
|
1823
|
+
this._selfName = name;
|
|
1824
|
+
this.aidStatsCollector?.setSelfName(this.config.aid, name);
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1481
1828
|
client.on('message.received', async (data) => {
|
|
1482
1829
|
if (!isCurrentClient())
|
|
1483
1830
|
return;
|
|
@@ -1523,6 +1870,9 @@ export class AUNChannel {
|
|
|
1523
1870
|
const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
|
|
1524
1871
|
this.trace('IN', 'group.invite_received', data);
|
|
1525
1872
|
logger.info(`${this.logPrefix()} Group invitation received: group=${groupId} pending=${inviteIds.length || 1}`);
|
|
1873
|
+
void Promise.resolve(this.groupInviteHandler?.(data)).catch(error => {
|
|
1874
|
+
logger.warn(`${this.logPrefix()} Group invitation review dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1875
|
+
});
|
|
1526
1876
|
});
|
|
1527
1877
|
}
|
|
1528
1878
|
// 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
|
|
@@ -1562,18 +1912,7 @@ export class AUNChannel {
|
|
|
1562
1912
|
client.on('message.recalled', (data) => {
|
|
1563
1913
|
if (!isCurrentClient())
|
|
1564
1914
|
return;
|
|
1565
|
-
this.
|
|
1566
|
-
if (data && typeof data === 'object') {
|
|
1567
|
-
const ids = data.message_ids;
|
|
1568
|
-
if (Array.isArray(ids)) {
|
|
1569
|
-
for (const id of ids) {
|
|
1570
|
-
if (typeof id === 'string') {
|
|
1571
|
-
logger.info(`${this.logPrefix()} Message recalled: ${id}`);
|
|
1572
|
-
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1915
|
+
this.handleMessageRecalled(data, client, isCurrentClient);
|
|
1577
1916
|
});
|
|
1578
1917
|
client.on('message.undecryptable', (data) => {
|
|
1579
1918
|
if (!isCurrentClient())
|
|
@@ -1659,13 +1998,37 @@ export class AUNChannel {
|
|
|
1659
1998
|
this._aid = this.client.aid ?? undefined;
|
|
1660
1999
|
const deviceId = this.client._device_id ?? '';
|
|
1661
2000
|
this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
|
|
1662
|
-
//
|
|
1663
|
-
|
|
2001
|
+
// 控制 AID 也有标准 host agent.md。发布在后台执行,短暂的 Name
|
|
2002
|
+
// Service/Gateway 故障不能把控制通道判定为连接失败。
|
|
2003
|
+
if (this.config.pureIdentity) {
|
|
2004
|
+
this._selfName = buildControlAgentName(aidName);
|
|
2005
|
+
void ensureControlAgentMd(aidName, {
|
|
2006
|
+
store: this.store ?? undefined,
|
|
2007
|
+
aunPath,
|
|
2008
|
+
cacheRoot: resolveRoot(),
|
|
2009
|
+
})
|
|
2010
|
+
.then(result => {
|
|
2011
|
+
this._selfName = result.name;
|
|
2012
|
+
if (this.aidStatsCollector)
|
|
2013
|
+
this.aidStatsCollector.setSelfName(this.config.aid, result.name);
|
|
2014
|
+
if (result.published)
|
|
2015
|
+
logger.info(`${this.logPrefix()} 控制 AID agent.md 已发布: ${result.name}`);
|
|
2016
|
+
})
|
|
2017
|
+
.catch(error => logger.warn(`${this.logPrefix()} 控制 AID agent.md 补全失败(后台重试): ${error instanceof Error ? error.message : String(error)}`));
|
|
2018
|
+
}
|
|
2019
|
+
else {
|
|
2020
|
+
this._selfName = this.loadSelfName(aidName, aunPath);
|
|
2021
|
+
}
|
|
1664
2022
|
if (this._selfName && this.aidStatsCollector)
|
|
1665
2023
|
this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
|
|
1666
2024
|
this.connected = true;
|
|
1667
2025
|
this.connectedAt = Date.now();
|
|
1668
2026
|
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
|
|
2027
|
+
if (!this.config.pureIdentity) {
|
|
2028
|
+
void Promise.resolve(this.groupInviteConnectedHandler?.()).catch(error => {
|
|
2029
|
+
logger.warn(`${this.logPrefix()} Group invitation recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
1669
2032
|
// AUN seq_tracker is transport-scoped, while this channel's durable
|
|
1670
2033
|
// dedup log is application-scoped. A diagnostic consumer can advance
|
|
1671
2034
|
// the former without the application having seen those messages. On a
|
|
@@ -1723,7 +2086,7 @@ export class AUNChannel {
|
|
|
1723
2086
|
if (hasMessageLogOperation(chatDir, operationId)
|
|
1724
2087
|
|| hasMessageLogOperation(chatDir, postBootstrapWelcomeOperationId(aidName)))
|
|
1725
2088
|
continue;
|
|
1726
|
-
const prepared = outbox.findByDedupeKey(aidName, operationId);
|
|
2089
|
+
const prepared = outbox.findByDedupeKey(aidName, operationId, { includeTerminal: true });
|
|
1727
2090
|
if (prepared && prepared.channelId === owner && isDeliveryTarget(prepared.delivery) && sameDeliveryTarget(prepared.delivery, delivery))
|
|
1728
2091
|
continue;
|
|
1729
2092
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
@@ -1759,7 +2122,7 @@ export class AUNChannel {
|
|
|
1759
2122
|
}
|
|
1760
2123
|
async sendPostBootstrapWelcome() {
|
|
1761
2124
|
if (!await this.preparePostBootstrapWelcome())
|
|
1762
|
-
|
|
2125
|
+
throw new Error('Failed to persist post-bootstrap welcome');
|
|
1763
2126
|
return this.reconcilePostBootstrapWelcome();
|
|
1764
2127
|
}
|
|
1765
2128
|
async hasPendingPostBootstrapWelcome() {
|
|
@@ -1777,6 +2140,8 @@ export class AUNChannel {
|
|
|
1777
2140
|
const pull = client?._pullV2;
|
|
1778
2141
|
if (typeof repair !== 'function' || typeof pull !== 'function')
|
|
1779
2142
|
return;
|
|
2143
|
+
if (this.inboundP2PCursorRepairTimer)
|
|
2144
|
+
clearTimeout(this.inboundP2PCursorRepairTimer);
|
|
1780
2145
|
const ns = `p2p:${this.config.aid}`;
|
|
1781
2146
|
let appSeq = 0;
|
|
1782
2147
|
for (const [key, value] of this.inboundSeenMessages) {
|
|
@@ -1789,7 +2154,8 @@ export class AUNChannel {
|
|
|
1789
2154
|
}
|
|
1790
2155
|
if (appSeq <= 0)
|
|
1791
2156
|
return;
|
|
1792
|
-
setTimeout(() => {
|
|
2157
|
+
this.inboundP2PCursorRepairTimer = setTimeout(() => {
|
|
2158
|
+
this.inboundP2PCursorRepairTimer = null;
|
|
1793
2159
|
if (!isCurrentClient() || !this.connected)
|
|
1794
2160
|
return;
|
|
1795
2161
|
const transportSeq = Number(tracker.getContiguousSeq?.(ns) ?? 0);
|
|
@@ -1807,6 +2173,24 @@ export class AUNChannel {
|
|
|
1807
2173
|
}
|
|
1808
2174
|
}, 250);
|
|
1809
2175
|
}
|
|
2176
|
+
inboundP2PCursorRepairTimer = null;
|
|
2177
|
+
handleMessageRecalled(data, client, isCurrentClient) {
|
|
2178
|
+
this.trace('IN', 'message.recalled', data);
|
|
2179
|
+
if (data && typeof data === 'object') {
|
|
2180
|
+
const ids = data.message_ids;
|
|
2181
|
+
if (Array.isArray(ids)) {
|
|
2182
|
+
for (const id of ids) {
|
|
2183
|
+
if (typeof id === 'string') {
|
|
2184
|
+
logger.info(`${this.logPrefix()} Message recalled: ${id}`);
|
|
2185
|
+
this.recallHandler?.(id, this._aid || this.config.aid);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
// A recall tombstone may arrive at a high transport sequence and advance
|
|
2191
|
+
// the SDK cursor past messages that were never delivered to the app.
|
|
2192
|
+
this.scheduleInboundP2PCursorRepair(client, isCurrentClient);
|
|
2193
|
+
}
|
|
1810
2194
|
pendingPostBootstrapWelcomeOwners() {
|
|
1811
2195
|
return pendingPostBootstrapWelcomeOwners(this.config.aid);
|
|
1812
2196
|
}
|
|
@@ -1822,25 +2206,35 @@ export class AUNChannel {
|
|
|
1822
2206
|
const configuredAid = this.config.aid;
|
|
1823
2207
|
const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
|
|
1824
2208
|
const entries = findPostBootstrapWelcomeOutbox(aid);
|
|
1825
|
-
if (entries.length === 0)
|
|
1826
|
-
return false;
|
|
1827
2209
|
const agentConfig = loadAgent(aid);
|
|
2210
|
+
const owners = agentConfig?.owners ?? [];
|
|
2211
|
+
if (entries.length === 0)
|
|
2212
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
1828
2213
|
if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
|
|
1829
2214
|
logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
|
|
1830
|
-
return
|
|
2215
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
1831
2216
|
}
|
|
1832
2217
|
if (!this.connected || !this.client)
|
|
1833
|
-
return
|
|
2218
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
1834
2219
|
for (const entry of entries) {
|
|
1835
2220
|
if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
|
|
1836
2221
|
continue;
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2222
|
+
try {
|
|
2223
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
2224
|
+
if (sent.status === 'sent')
|
|
2225
|
+
this.removeDeliveredOutboxEntry(entry);
|
|
2226
|
+
else if (sent.status === 'permanent')
|
|
2227
|
+
this.markPermanentOutboxFailure(entry, sent);
|
|
2228
|
+
else
|
|
2229
|
+
outbox.recordRetry(this.config.aid, entry, sent);
|
|
2230
|
+
}
|
|
2231
|
+
catch (error) {
|
|
2232
|
+
// Lifecycle has already committed. Preserve its success and the
|
|
2233
|
+
// durable operation even when local post-send bookkeeping fails.
|
|
2234
|
+
logger.warn(`${this.logPrefix()} Welcome reconciliation failed: entry=${entry.id} error=${error}`);
|
|
2235
|
+
}
|
|
1842
2236
|
}
|
|
1843
|
-
return
|
|
2237
|
+
return postBootstrapWelcomeStatus(aid, owners);
|
|
1844
2238
|
}
|
|
1845
2239
|
// ── Event handlers ──────────────────────────────────────────
|
|
1846
2240
|
/**
|
|
@@ -2115,6 +2509,15 @@ export class AUNChannel {
|
|
|
2115
2509
|
return;
|
|
2116
2510
|
const protectedHeaders = verifiedProtectedHeaders(msg, env);
|
|
2117
2511
|
const payload = msg.payload ?? '';
|
|
2512
|
+
if (this.managedMenuClient.accept(fromAid, payload, !!env.encrypted)) {
|
|
2513
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
if (payloadTypeForObserver(msg) === 'observer.forward') {
|
|
2517
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
2518
|
+
logger.debug(`${this.logPrefix()} P2P dropped observer.forward control envelope: mid=${messageId}`);
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2118
2521
|
const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
|
|
2119
2522
|
if (!mentions)
|
|
2120
2523
|
return;
|
|
@@ -2174,7 +2577,7 @@ export class AUNChannel {
|
|
|
2174
2577
|
const chatId = fromAid;
|
|
2175
2578
|
// 解析对端身份(30天缓存)
|
|
2176
2579
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
2177
|
-
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false);
|
|
2580
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
2178
2581
|
const shortAid = this.getShortAid(fromAid);
|
|
2179
2582
|
const displayName = peerIdentity.name || shortAid;
|
|
2180
2583
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
@@ -2191,19 +2594,33 @@ export class AUNChannel {
|
|
|
2191
2594
|
if (p2pPayloadType.startsWith('menu.')
|
|
2192
2595
|
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
|
|
2193
2596
|
|| p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
|
|
2597
|
+
const installation = p2pPayloadType.startsWith('menu.')
|
|
2598
|
+
? this.localSenderInstallation?.aunSenderInstallation(msg) : undefined;
|
|
2599
|
+
const peerDeviceId = p2pPayloadType.startsWith('menu.')
|
|
2600
|
+
? menuSenderDeviceId(msg.sender_device_id) : undefined;
|
|
2194
2601
|
this.acknowledgeImmediately(messageId, seq);
|
|
2195
2602
|
this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
|
|
2196
2603
|
this.dispatchMessage({
|
|
2197
2604
|
channelId: chatId, userId: fromAid,
|
|
2198
2605
|
text: JSON.stringify(payload),
|
|
2199
2606
|
chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
|
|
2607
|
+
peerDeviceId,
|
|
2608
|
+
peerSlotId: installation?.slotId,
|
|
2609
|
+
threadId,
|
|
2200
2610
|
peerName: displayName || undefined,
|
|
2201
2611
|
peerType: peerIdentity.type,
|
|
2202
2612
|
encrypted: msgEncrypted,
|
|
2203
2613
|
protectedHeaders,
|
|
2204
2614
|
mentions,
|
|
2205
2615
|
mentionAids: mentionEntryAids(mentions),
|
|
2206
|
-
replyContext:
|
|
2616
|
+
replyContext: {
|
|
2617
|
+
...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
|
|
2618
|
+
metadata: {
|
|
2619
|
+
...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId).metadata,
|
|
2620
|
+
...(protectedHeaders?.sdk_version ? { menuClientSdkVersion: protectedHeaders.sdk_version } : {}),
|
|
2621
|
+
},
|
|
2622
|
+
...(peerDeviceId ? { toDeviceId: peerDeviceId } : {}),
|
|
2623
|
+
},
|
|
2207
2624
|
msgType: logDescriptor.msgType,
|
|
2208
2625
|
payloadType: logDescriptor.payloadType,
|
|
2209
2626
|
payloadSummary: logDescriptor.payloadSummary,
|
|
@@ -2228,6 +2645,7 @@ export class AUNChannel {
|
|
|
2228
2645
|
channelId: chatId, userId: fromAid,
|
|
2229
2646
|
text: JSON.stringify(payload),
|
|
2230
2647
|
chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
|
|
2648
|
+
threadId,
|
|
2231
2649
|
peerName: displayName || undefined,
|
|
2232
2650
|
peerType: peerIdentity.type,
|
|
2233
2651
|
encrypted: msgEncrypted,
|
|
@@ -2252,7 +2670,7 @@ export class AUNChannel {
|
|
|
2252
2670
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
|
|
2253
2671
|
const isSystemP2P = p2pPayloadType === 'event';
|
|
2254
2672
|
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
|
|
2255
|
-
const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId);
|
|
2673
|
+
const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLable(payload));
|
|
2256
2674
|
this.dispatchMessage({
|
|
2257
2675
|
channelId: chatId,
|
|
2258
2676
|
userId: fromAid,
|
|
@@ -2260,6 +2678,7 @@ export class AUNChannel {
|
|
|
2260
2678
|
chatType: 'private',
|
|
2261
2679
|
messageId,
|
|
2262
2680
|
receivedAt,
|
|
2681
|
+
createdAt: typeof msg.created_at === 'number' ? msg.created_at : undefined,
|
|
2263
2682
|
gatewaySeq: seq,
|
|
2264
2683
|
causation,
|
|
2265
2684
|
seq,
|
|
@@ -2297,6 +2716,12 @@ export class AUNChannel {
|
|
|
2297
2716
|
const groupId = env.group_id ?? '';
|
|
2298
2717
|
const senderAid = env.from ?? '';
|
|
2299
2718
|
const payload = msg.payload ?? '';
|
|
2719
|
+
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
2720
|
+
if (inboundType === 'observer.forward') {
|
|
2721
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
2722
|
+
logger.debug(`${this.logPrefix()} Group dropped observer.forward control envelope: mid=${messageId}`);
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2300
2725
|
const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
|
|
2301
2726
|
if (!payloadMentionEntries)
|
|
2302
2727
|
return;
|
|
@@ -2307,7 +2732,6 @@ export class AUNChannel {
|
|
|
2307
2732
|
const topicName = (typeof payload === 'object' && payload !== null)
|
|
2308
2733
|
? this.sanitizeTopicName(payload.topicName)
|
|
2309
2734
|
: undefined;
|
|
2310
|
-
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
2311
2735
|
// 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
|
|
2312
2736
|
if (inboundType === 'error') {
|
|
2313
2737
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -2358,7 +2782,7 @@ export class AUNChannel {
|
|
|
2358
2782
|
peerName: displayName,
|
|
2359
2783
|
peerType: peerInfo?.type || 'unknown',
|
|
2360
2784
|
seq,
|
|
2361
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
|
|
2785
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLable(payload)),
|
|
2362
2786
|
createdAt,
|
|
2363
2787
|
});
|
|
2364
2788
|
return;
|
|
@@ -2377,27 +2801,10 @@ export class AUNChannel {
|
|
|
2377
2801
|
{
|
|
2378
2802
|
const payloadObj = (payload && typeof payload === 'object') ? payload : null;
|
|
2379
2803
|
const payloadType = typeof payloadObj?.type === 'string' ? payloadObj.type : '';
|
|
2380
|
-
//
|
|
2804
|
+
// Menu control requests are private-only, including token issuance.
|
|
2381
2805
|
if (payloadType.startsWith('menu.')) {
|
|
2382
|
-
const menuEncrypted = !!env.encrypted;
|
|
2383
2806
|
this.acknowledgeImmediately(messageId, seq);
|
|
2384
|
-
this.
|
|
2385
|
-
this.dispatchMessage({
|
|
2386
|
-
channelId: groupId, userId: senderAid,
|
|
2387
|
-
text: JSON.stringify(payload),
|
|
2388
|
-
chatType: 'group', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq, groupId,
|
|
2389
|
-
encrypted: menuEncrypted,
|
|
2390
|
-
protectedHeaders,
|
|
2391
|
-
mentions: payloadMentionEntries,
|
|
2392
|
-
mentionAids: payloadMentionAids,
|
|
2393
|
-
isMentioned: hasMentionAll(payloadMentionEntries)
|
|
2394
|
-
|| (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
|
|
2395
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName, groupId),
|
|
2396
|
-
msgType: logDescriptor.msgType,
|
|
2397
|
-
payloadType: logDescriptor.payloadType,
|
|
2398
|
-
payloadSummary: logDescriptor.payloadSummary,
|
|
2399
|
-
messageLogContent: logDescriptor.content,
|
|
2400
|
-
});
|
|
2807
|
+
logger.debug(`${this.logPrefix()} Group Menu dropped: code=MENU_PRIVATE_ONLY mid=${messageId}`);
|
|
2401
2808
|
return;
|
|
2402
2809
|
}
|
|
2403
2810
|
if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
|
|
@@ -2437,17 +2844,15 @@ export class AUNChannel {
|
|
|
2437
2844
|
if (isEchoMsg) {
|
|
2438
2845
|
// 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
|
|
2439
2846
|
// >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2442
|
-
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
2443
|
-
};
|
|
2847
|
+
const echoCreatedAt = data.created_at;
|
|
2848
|
+
const echoReceiveTs = Date.now();
|
|
2444
2849
|
let echoText = text;
|
|
2445
|
-
echoText += `\n${
|
|
2850
|
+
echoText += `\n${echoTraceClock(echoReceiveTs)} [EvolCore.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((echoReceiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(echoCreatedAt, receivedAt)}${echoTraceOriginLabel(echoReceiveTs)}`;
|
|
2446
2851
|
const msgChatmodeEcho = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
2447
2852
|
this.pendingEchoMessages.set(messageId, {
|
|
2448
2853
|
text: echoText,
|
|
2449
2854
|
channelId: groupId,
|
|
2450
|
-
context: this.buildGroupReplyContext(
|
|
2855
|
+
context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLable(payload)),
|
|
2451
2856
|
receiveTs: Date.now(),
|
|
2452
2857
|
});
|
|
2453
2858
|
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
@@ -2469,8 +2874,20 @@ export class AUNChannel {
|
|
|
2469
2874
|
// 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
|
|
2470
2875
|
// TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
|
|
2471
2876
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
2877
|
+
try {
|
|
2878
|
+
if (text.trim() || this.collectAllAttachments(payload).length) {
|
|
2879
|
+
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, 'unmentioned', 'filtered');
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
catch (error) {
|
|
2883
|
+
// A failed local append must not suppress a subsequent SDK redelivery.
|
|
2884
|
+
const namespace = this.config.aid + ':' + (this.config.channelName ?? 'aun') + ':aun:group';
|
|
2885
|
+
this.inboundSeenMessages.delete(JSON.stringify([namespace, messageId]));
|
|
2886
|
+
this.rewriteInboundDedupe();
|
|
2887
|
+
logger.warn(`${this.logPrefix()} Deferred group message was not recorded; awaiting redelivery: ${error}`);
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2472
2890
|
this.acknowledgeImmediately(messageId, seq);
|
|
2473
|
-
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
2474
2891
|
logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
2475
2892
|
return;
|
|
2476
2893
|
}
|
|
@@ -2493,7 +2910,7 @@ export class AUNChannel {
|
|
|
2493
2910
|
// Process attachments
|
|
2494
2911
|
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
|
|
2495
2912
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
2496
|
-
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
|
|
2913
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
2497
2914
|
const shortAid = this.getShortAid(senderAid);
|
|
2498
2915
|
const displayName = peerIdentity.name || shortAid;
|
|
2499
2916
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
@@ -2536,6 +2953,7 @@ export class AUNChannel {
|
|
|
2536
2953
|
chatType: 'group',
|
|
2537
2954
|
messageId,
|
|
2538
2955
|
receivedAt,
|
|
2956
|
+
createdAt: typeof msg.created_at === 'number' ? msg.created_at : undefined,
|
|
2539
2957
|
gatewaySeq: seq,
|
|
2540
2958
|
seq,
|
|
2541
2959
|
threadId,
|
|
@@ -2544,7 +2962,7 @@ export class AUNChannel {
|
|
|
2544
2962
|
isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
|
|
2545
2963
|
source: isOwnHandoff ? 'handoff' : undefined,
|
|
2546
2964
|
encrypted: msgEncrypted,
|
|
2547
|
-
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
|
|
2965
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLable(payload)),
|
|
2548
2966
|
mentionMode: serverMentionMode,
|
|
2549
2967
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
2550
2968
|
msgType: logDescriptor.msgType,
|
|
@@ -2564,6 +2982,7 @@ export class AUNChannel {
|
|
|
2564
2982
|
&& authenticatedDelivery.chatType === 'group'
|
|
2565
2983
|
&& suppliedDelivery.groupId === authenticatedDelivery.groupId;
|
|
2566
2984
|
const delivery = suppliedDelivery && suppliedMatches ? suppliedDelivery : authenticatedDelivery;
|
|
2985
|
+
const threadId = event.threadId ?? event.replyContext?.threadId;
|
|
2567
2986
|
if (suppliedDelivery && delivery !== suppliedDelivery) {
|
|
2568
2987
|
logger.warn(`${this.logPrefix()} Ignoring mismatched inbound reply route: channel=${event.channelId} chatType=${event.chatType}`);
|
|
2569
2988
|
}
|
|
@@ -2585,17 +3004,15 @@ export class AUNChannel {
|
|
|
2585
3004
|
}
|
|
2586
3005
|
// 长 echo(>10 字符):存 pending,继续交给 agent 处理
|
|
2587
3006
|
if (/echo/i.test(firstLine) && firstLine.trim().length > 10 && !hasEvolCoreTracePrivate) {
|
|
2588
|
-
const
|
|
2589
|
-
const d = new Date();
|
|
2590
|
-
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
2591
|
-
};
|
|
3007
|
+
const echoReceiveTs = Date.now();
|
|
2592
3008
|
let echoText = event.text;
|
|
2593
|
-
echoText += `\n${
|
|
3009
|
+
echoText += `\n${echoTraceClock(echoReceiveTs)} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((echoReceiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(event.createdAt, event.receivedAt ?? echoReceiveTs)}${echoTraceOriginLabel(echoReceiveTs)}`;
|
|
2594
3010
|
this.pendingEchoMessages.set(event.messageId, {
|
|
2595
3011
|
text: echoText,
|
|
2596
3012
|
channelId: event.channelId,
|
|
2597
3013
|
context: {
|
|
2598
3014
|
...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
|
|
3015
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
2599
3016
|
delivery,
|
|
2600
3017
|
},
|
|
2601
3018
|
receiveTs: Date.now(),
|
|
@@ -2610,7 +3027,7 @@ export class AUNChannel {
|
|
|
2610
3027
|
const replyContext = {
|
|
2611
3028
|
...(event.replyContext ?? {}),
|
|
2612
3029
|
delivery,
|
|
2613
|
-
...(
|
|
3030
|
+
...(typeof threadId === 'string' ? { threadId } : {}),
|
|
2614
3031
|
};
|
|
2615
3032
|
this.messageHandler({
|
|
2616
3033
|
channelId: event.channelId || '',
|
|
@@ -2622,6 +3039,8 @@ export class AUNChannel {
|
|
|
2622
3039
|
peerId: event.userId || event.channelId || '',
|
|
2623
3040
|
peerName: event.peerName,
|
|
2624
3041
|
peerType: event.peerType,
|
|
3042
|
+
peerDeviceId: event.peerDeviceId,
|
|
3043
|
+
peerSlotId: event.peerSlotId,
|
|
2625
3044
|
sameDevice: event.sameDevice,
|
|
2626
3045
|
sameNetwork: event.sameNetwork,
|
|
2627
3046
|
sameEgressIp: event.sameEgressIp,
|
|
@@ -2632,7 +3051,7 @@ export class AUNChannel {
|
|
|
2632
3051
|
receivedAtMono: event.receivedAtMono,
|
|
2633
3052
|
gatewaySeq: event.gatewaySeq ?? event.seq,
|
|
2634
3053
|
causation: event.causation,
|
|
2635
|
-
threadId
|
|
3054
|
+
threadId,
|
|
2636
3055
|
mentions: event.mentions,
|
|
2637
3056
|
mentionAids: event.mentionAids,
|
|
2638
3057
|
isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
|
|
@@ -2648,10 +3067,10 @@ export class AUNChannel {
|
|
|
2648
3067
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
2649
3068
|
});
|
|
2650
3069
|
}
|
|
2651
|
-
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
|
|
3070
|
+
appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq, groupAttention, groupDelivery) {
|
|
2652
3071
|
try {
|
|
2653
3072
|
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
|
|
2654
|
-
|
|
3073
|
+
const entry = buildInboundEntry({
|
|
2655
3074
|
from: fromAid,
|
|
2656
3075
|
to: this.config.aid,
|
|
2657
3076
|
threadId: threadId || null,
|
|
@@ -2666,10 +3085,18 @@ export class AUNChannel {
|
|
|
2666
3085
|
timestamp: receivedAt,
|
|
2667
3086
|
receivedAt,
|
|
2668
3087
|
gatewaySeq,
|
|
2669
|
-
|
|
3088
|
+
groupAttention,
|
|
3089
|
+
groupDelivery,
|
|
3090
|
+
});
|
|
3091
|
+
if (groupDelivery === 'filtered')
|
|
3092
|
+
appendMessageLogStrict(chatDir, entry, true);
|
|
3093
|
+
else
|
|
3094
|
+
appendMessageLog(chatDir, entry);
|
|
2670
3095
|
}
|
|
2671
3096
|
catch (e) {
|
|
2672
|
-
logger.
|
|
3097
|
+
logger.warn(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
|
|
3098
|
+
if (groupDelivery === 'filtered')
|
|
3099
|
+
throw e;
|
|
2673
3100
|
}
|
|
2674
3101
|
}
|
|
2675
3102
|
// ── 观察者模式(Observer Mode) ──────────────────────────────
|
|
@@ -2797,7 +3224,10 @@ export class AUNChannel {
|
|
|
2797
3224
|
forwardInbound(data) {
|
|
2798
3225
|
if (!this.connected || !this.client)
|
|
2799
3226
|
return Promise.resolve();
|
|
2800
|
-
|
|
3227
|
+
const inboundType = payloadTypeForObserver(data);
|
|
3228
|
+
// Observer envelopes are control-plane telemetry. Mirroring one again
|
|
3229
|
+
// would amplify every activity event across observer-connected agents.
|
|
3230
|
+
if (inboundType === 'observer.forward' || inboundType?.startsWith('menu.'))
|
|
2801
3231
|
return Promise.resolve();
|
|
2802
3232
|
const { observable, owners } = this.getObserverConfig();
|
|
2803
3233
|
if (!observable || owners.length === 0)
|
|
@@ -2833,7 +3263,8 @@ export class AUNChannel {
|
|
|
2833
3263
|
forwardOutbound(result) {
|
|
2834
3264
|
if (!this.connected || !this.client)
|
|
2835
3265
|
return Promise.resolve();
|
|
2836
|
-
|
|
3266
|
+
const outboundType = payloadTypeForObserver(result);
|
|
3267
|
+
if (outboundType === 'observer.forward' || outboundType?.startsWith('menu.'))
|
|
2837
3268
|
return Promise.resolve();
|
|
2838
3269
|
const { observable, owners } = this.getObserverConfig();
|
|
2839
3270
|
if (!observable || owners.length === 0)
|
|
@@ -2874,8 +3305,8 @@ export class AUNChannel {
|
|
|
2874
3305
|
};
|
|
2875
3306
|
const encrypt = this.shouldEncrypt('observer.forward');
|
|
2876
3307
|
for (const ownerAid of owners) {
|
|
2877
|
-
this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
|
|
2878
|
-
.catch(e => logger.
|
|
3308
|
+
this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt }, { lane: 'observer' })
|
|
3309
|
+
.catch(e => logger.debug(`${this.logPrefix()} observer.forward send to ${ownerAid} failed: ${e}`));
|
|
2879
3310
|
}
|
|
2880
3311
|
}
|
|
2881
3312
|
// ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
|
|
@@ -2978,14 +3409,12 @@ export class AUNChannel {
|
|
|
2978
3409
|
}
|
|
2979
3410
|
}
|
|
2980
3411
|
handleEcho(event) {
|
|
2981
|
-
const
|
|
2982
|
-
const d = new Date();
|
|
2983
|
-
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
2984
|
-
};
|
|
3412
|
+
const receiveTs = Date.now();
|
|
2985
3413
|
// 在收到的消息文本末尾追加 trace 行(只追加,不修改原文)
|
|
2986
3414
|
let echoText = event.text;
|
|
2987
|
-
echoText += `\n${
|
|
2988
|
-
|
|
3415
|
+
echoText += `\n${echoTraceClock(receiveTs)} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((receiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(event.createdAt, receiveTs)}${echoTraceOriginLabel(receiveTs)}`;
|
|
3416
|
+
const replyTs = Date.now();
|
|
3417
|
+
echoText += `\n${echoTraceClock(replyTs)} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((replyTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoTraceOriginLabel(replyTs)}`;
|
|
2989
3418
|
if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
|
|
2990
3419
|
echoText += `\n[TRUNCATED]`;
|
|
2991
3420
|
}
|
|
@@ -2995,7 +3424,7 @@ export class AUNChannel {
|
|
|
2995
3424
|
? { chatType: 'group', groupId: event.channelId }
|
|
2996
3425
|
: { chatType: 'private' };
|
|
2997
3426
|
const context = {
|
|
2998
|
-
threadId: event.threadId,
|
|
3427
|
+
threadId: event.threadId ?? event.replyContext?.threadId,
|
|
2999
3428
|
metadata: event.replyContext?.metadata,
|
|
3000
3429
|
delivery: authenticatedDelivery,
|
|
3001
3430
|
};
|
|
@@ -3098,6 +3527,7 @@ export class AUNChannel {
|
|
|
3098
3527
|
else if (state === 'disconnected' || state === 'reconnecting') {
|
|
3099
3528
|
const wasConnected = this.connected;
|
|
3100
3529
|
this.connected = false;
|
|
3530
|
+
this.clearQueuedObserverSends(state);
|
|
3101
3531
|
// The nine-state SDK emits standby -> retry_backoff -> reconnecting for
|
|
3102
3532
|
// one reconnect cycle. Count the connected-to-down edge once instead of
|
|
3103
3533
|
// inflating the metric for every intermediate state.
|
|
@@ -3281,6 +3711,7 @@ export class AUNChannel {
|
|
|
3281
3711
|
this.client = null;
|
|
3282
3712
|
}
|
|
3283
3713
|
this.connected = false;
|
|
3714
|
+
this.clearQueuedObserverSends(`takeover_${reason}`);
|
|
3284
3715
|
const delaySec = Math.round(delayMs / 1000);
|
|
3285
3716
|
logger.info(`${this.logPrefix()} Scheduling TS-layer reconnect (${reason}) in ${delaySec}s`);
|
|
3286
3717
|
this.trace('OUT', 'reconnect.scheduled', { reason, delayMs, generation: gen });
|
|
@@ -3444,7 +3875,9 @@ export class AUNChannel {
|
|
|
3444
3875
|
return Buffer.byteLength(logText, 'utf-8');
|
|
3445
3876
|
}
|
|
3446
3877
|
applyReplyContextToPayload(payload, context) {
|
|
3447
|
-
const finalPayload = this.stripUndefinedDeep({ ...payload });
|
|
3878
|
+
const finalPayload = this.stripUndefinedDeep(withAunAttention({ ...payload }, context?.attention));
|
|
3879
|
+
if (finalPayload.thread_id === '')
|
|
3880
|
+
delete finalPayload.thread_id;
|
|
3448
3881
|
if (context?.threadId && !finalPayload.thread_id)
|
|
3449
3882
|
finalPayload.thread_id = context.threadId;
|
|
3450
3883
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
@@ -3456,13 +3889,14 @@ export class AUNChannel {
|
|
|
3456
3889
|
return finalPayload;
|
|
3457
3890
|
}
|
|
3458
3891
|
/**
|
|
3459
|
-
* Validate
|
|
3892
|
+
* Validate AUN-specific structured fields before crossing the durable
|
|
3460
3893
|
* send boundary. Return a copy when mentions are present so callers do not
|
|
3461
3894
|
* retain an unvalidated array reference.
|
|
3462
3895
|
*/
|
|
3463
3896
|
normalizeAunPayloadMentions(payload) {
|
|
3464
3897
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
3465
3898
|
return payload;
|
|
3899
|
+
payload = withAunAttention(payload);
|
|
3466
3900
|
if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
|
|
3467
3901
|
return payload;
|
|
3468
3902
|
return {
|
|
@@ -3498,6 +3932,9 @@ export class AUNChannel {
|
|
|
3498
3932
|
if (!current || typeof current !== 'object')
|
|
3499
3933
|
continue;
|
|
3500
3934
|
for (const key of Object.keys(current)) {
|
|
3935
|
+
// Topic routing must survive content truncation and fallback.
|
|
3936
|
+
if (current === limited && key === 'thread_id')
|
|
3937
|
+
continue;
|
|
3501
3938
|
const value = current[key];
|
|
3502
3939
|
if (typeof value === 'string') {
|
|
3503
3940
|
leaves.push({ parent: current, key, value, bytes: Buffer.byteLength(value, 'utf8') });
|
|
@@ -3544,6 +3981,8 @@ export class AUNChannel {
|
|
|
3544
3981
|
};
|
|
3545
3982
|
if (typeof payload.chatmode === 'string')
|
|
3546
3983
|
fallback.chatmode = truncateUtf8ToBytes(payload.chatmode, 32);
|
|
3984
|
+
if (typeof payload.thread_id === 'string')
|
|
3985
|
+
fallback.thread_id = payload.thread_id;
|
|
3547
3986
|
logger.info(`${this.logPrefix()} Thought payload reduced to fallback (${Buffer.byteLength(JSON.stringify(fallback), 'utf8')} bytes)`);
|
|
3548
3987
|
return fallback;
|
|
3549
3988
|
}
|
|
@@ -3602,15 +4041,23 @@ export class AUNChannel {
|
|
|
3602
4041
|
registerCardPostSend(messageId, action, channelId, delivery, replyContext) {
|
|
3603
4042
|
if (action.type !== 'register_interaction_card')
|
|
3604
4043
|
return;
|
|
4044
|
+
// Card replies start a new input; an outbound attention marker must not
|
|
4045
|
+
// become the default for every message produced after clicking the card.
|
|
4046
|
+
const callbackContext = replyContext ? { ...replyContext } : undefined;
|
|
4047
|
+
if (callbackContext)
|
|
4048
|
+
delete callbackContext.attention;
|
|
3605
4049
|
this.cardMessageIdMap.set(messageId, {
|
|
3606
4050
|
requestId: action.requestId,
|
|
3607
4051
|
isCommandCard: action.isCommandCard,
|
|
3608
4052
|
initiatorAid: action.initiatorAid,
|
|
3609
4053
|
channelId,
|
|
3610
4054
|
delivery: delivery ?? action.delivery,
|
|
3611
|
-
replyContext,
|
|
4055
|
+
replyContext: callbackContext,
|
|
3612
4056
|
});
|
|
3613
4057
|
this.ownedCardMsgIds.add(messageId);
|
|
4058
|
+
if (typeof replyContext?.threadId === 'string') {
|
|
4059
|
+
this.ownedCardTopics.set(messageId, { channelId, threadId: replyContext.threadId });
|
|
4060
|
+
}
|
|
3614
4061
|
let messageIds = this.interactionCardMessageIds.get(action.requestId);
|
|
3615
4062
|
if (!messageIds) {
|
|
3616
4063
|
messageIds = new Set();
|
|
@@ -3629,7 +4076,10 @@ export class AUNChannel {
|
|
|
3629
4076
|
this.interactionCardMessageIds.delete(action.requestId);
|
|
3630
4077
|
}, mapTtl);
|
|
3631
4078
|
mapTimer.unref?.();
|
|
3632
|
-
const ownedTimer = setTimeout(() =>
|
|
4079
|
+
const ownedTimer = setTimeout(() => {
|
|
4080
|
+
this.ownedCardMsgIds.delete(messageId);
|
|
4081
|
+
this.ownedCardTopics.delete(messageId);
|
|
4082
|
+
}, 24 * 60 * 60 * 1000);
|
|
3633
4083
|
ownedTimer.unref?.();
|
|
3634
4084
|
const invalidationReason = this.invalidatedInteractions.get(action.requestId);
|
|
3635
4085
|
if (invalidationReason) {
|
|
@@ -3642,6 +4092,8 @@ export class AUNChannel {
|
|
|
3642
4092
|
return;
|
|
3643
4093
|
if (entry.postSend.type === 'register_interaction_card') {
|
|
3644
4094
|
this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery, entry.context);
|
|
4095
|
+
if (entry.postSend.requestId.startsWith('gir-'))
|
|
4096
|
+
this.groupInviteDelivered?.(entry.postSend.requestId, messageId);
|
|
3645
4097
|
}
|
|
3646
4098
|
}
|
|
3647
4099
|
async sendAunPayload(channelId, payload, context, label) {
|
|
@@ -3673,9 +4125,20 @@ export class AUNChannel {
|
|
|
3673
4125
|
const payloadWithCausationRef = causationRef
|
|
3674
4126
|
? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
|
|
3675
4127
|
: normalizedPayload;
|
|
4128
|
+
if (payloadWithCausationRef.thread_id === '')
|
|
4129
|
+
delete payloadWithCausationRef.thread_id;
|
|
3676
4130
|
// Truncate payload if too large (AUN SDK limit: 1MB)
|
|
3677
4131
|
const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
|
|
3678
4132
|
const params = { payload: truncatedPayload, encrypt };
|
|
4133
|
+
const isMenuPayload = typeof truncatedPayload.type === 'string' && truncatedPayload.type.startsWith('menu.');
|
|
4134
|
+
if (!isGroup && truncatedPayload.type === 'menu.response')
|
|
4135
|
+
params.to_device_id = context?.toDeviceId;
|
|
4136
|
+
if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
|
|
4137
|
+
params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
|
|
4138
|
+
}
|
|
4139
|
+
if (isMenuPayload && encrypt && context?.metadata?.protectedHeaders) {
|
|
4140
|
+
params.protected_headers = context.metadata.protectedHeaders;
|
|
4141
|
+
}
|
|
3679
4142
|
if (isGroup)
|
|
3680
4143
|
params.group_id = delivery.groupId;
|
|
3681
4144
|
else
|
|
@@ -3696,7 +4159,7 @@ export class AUNChannel {
|
|
|
3696
4159
|
return await callOnce(params, false);
|
|
3697
4160
|
}
|
|
3698
4161
|
catch (e) {
|
|
3699
|
-
if (encrypt && e instanceof E2EEError) {
|
|
4162
|
+
if (encrypt && e instanceof E2EEError && !isMenuPayload) {
|
|
3700
4163
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
3701
4164
|
logger.warn(`${this.logPrefix()} E2EE ${label} send failed to ${channelId}, retrying plaintext: ${e}`);
|
|
3702
4165
|
const fallbackParams = { ...params, encrypt: false };
|
|
@@ -3749,7 +4212,8 @@ export class AUNChannel {
|
|
|
3749
4212
|
...classifyAunPayloadForLog(payload), msgId: messageId, encrypt, context, isGroup, source,
|
|
3750
4213
|
transport: this.sendReceiptFromResult(result),
|
|
3751
4214
|
});
|
|
3752
|
-
|
|
4215
|
+
if (typeof payload.type !== 'string' || !payload.type.startsWith('menu.'))
|
|
4216
|
+
this.forwardOutbound(result);
|
|
3753
4217
|
}
|
|
3754
4218
|
async sendContentPayload(channelId, payload, opts) {
|
|
3755
4219
|
const delivery = this.requireDelivery(channelId, opts.context);
|
|
@@ -3792,6 +4256,7 @@ export class AUNChannel {
|
|
|
3792
4256
|
queue: opts.queue,
|
|
3793
4257
|
channelId,
|
|
3794
4258
|
delivery,
|
|
4259
|
+
dedupeKey: opts.dedupeKey,
|
|
3795
4260
|
type: 'payload',
|
|
3796
4261
|
contentKind: opts.contentKind,
|
|
3797
4262
|
payload: finalPayload,
|
|
@@ -3863,7 +4328,7 @@ export class AUNChannel {
|
|
|
3863
4328
|
base.initiator = context.peerId;
|
|
3864
4329
|
if (context?.replyToMessageId)
|
|
3865
4330
|
base.ref_message_id = context.replyToMessageId;
|
|
3866
|
-
return base;
|
|
4331
|
+
return withAunAttention(base, context?.attention);
|
|
3867
4332
|
}
|
|
3868
4333
|
activityLogText(raw) {
|
|
3869
4334
|
// 兼容两种入参:原始 ThoughtItem(顶层字段),或已构建的 activity payload(字段收在 .item 里)
|
|
@@ -3889,68 +4354,27 @@ export class AUNChannel {
|
|
|
3889
4354
|
/**
|
|
3890
4355
|
* Truncate payload if it exceeds AUN protocol limits.
|
|
3891
4356
|
* AUN SDK enforces MAX_WS_PAYLOAD_SIZE = 1_000_000 bytes (1MB) for the entire JSON-RPC message.
|
|
3892
|
-
* We use a conservative
|
|
4357
|
+
* We use a conservative 512KB limit for the payload itself to leave room for
|
|
4358
|
+
* the encrypted AUN envelope and RPC framing.
|
|
3893
4359
|
*/
|
|
3894
4360
|
truncatePayloadIfNeeded(payload, label) {
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
// First check total size
|
|
3898
|
-
const payloadJson = JSON.stringify(payload);
|
|
3899
|
-
const totalSize = Buffer.byteLength(payloadJson, 'utf-8');
|
|
3900
|
-
if (totalSize <= MAX_PAYLOAD_SIZE) {
|
|
4361
|
+
const result = truncateAunPayloadIfNeeded(payload);
|
|
4362
|
+
if (result.originalSize <= AUN_MAX_PAYLOAD_SIZE) {
|
|
3901
4363
|
return payload; // No truncation needed
|
|
3902
4364
|
}
|
|
3903
|
-
logger.warn(`${this.logPrefix()} Payload too large (${formatSize(
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
return false;
|
|
3910
|
-
let changed = false;
|
|
3911
|
-
for (const key of Object.keys(obj)) {
|
|
3912
|
-
const value = obj[key];
|
|
3913
|
-
const fieldPath = path ? `${path}.${key}` : key;
|
|
3914
|
-
if (typeof value === 'string') {
|
|
3915
|
-
const fieldSize = Buffer.byteLength(value, 'utf-8');
|
|
3916
|
-
if (fieldSize > MAX_FIELD_SIZE) {
|
|
3917
|
-
const truncatedValue = value.substring(0, MAX_FIELD_SIZE / 4); // Rough estimate, UTF-8 safe
|
|
3918
|
-
const actualTruncatedSize = Buffer.byteLength(truncatedValue, 'utf-8');
|
|
3919
|
-
obj[key] = truncatedValue + `\n\n[截断:原始输出 ${formatSize(fieldSize)},仅显示前 ${formatSize(actualTruncatedSize)}]`;
|
|
3920
|
-
obj[`${key}_truncated`] = true;
|
|
3921
|
-
logger.info(`${this.logPrefix()} Truncated field ${fieldPath}: ${formatSize(fieldSize)} → ${formatSize(actualTruncatedSize)}`);
|
|
3922
|
-
changed = true;
|
|
3923
|
-
}
|
|
3924
|
-
}
|
|
3925
|
-
else if (Array.isArray(value)) {
|
|
3926
|
-
for (let i = 0; i < value.length; i++) {
|
|
3927
|
-
if (truncateObject(value[i], `${fieldPath}[${i}]`)) {
|
|
3928
|
-
changed = true;
|
|
3929
|
-
}
|
|
3930
|
-
}
|
|
3931
|
-
}
|
|
3932
|
-
else if (typeof value === 'object' && value !== null) {
|
|
3933
|
-
if (truncateObject(value, fieldPath)) {
|
|
3934
|
-
changed = true;
|
|
3935
|
-
}
|
|
3936
|
-
}
|
|
3937
|
-
}
|
|
3938
|
-
return changed;
|
|
3939
|
-
};
|
|
3940
|
-
truncateObject(truncated, '');
|
|
3941
|
-
// Check size again after truncation
|
|
3942
|
-
const newJson = JSON.stringify(truncated);
|
|
3943
|
-
const newSize = Buffer.byteLength(newJson, 'utf-8');
|
|
3944
|
-
if (newSize > MAX_PAYLOAD_SIZE) {
|
|
3945
|
-
// Still too large - this shouldn't happen with 256KB field limit, but log it
|
|
3946
|
-
logger.error(`${this.logPrefix()} Payload still too large after truncation: ${formatSize(newSize)}, dropping payload`);
|
|
4365
|
+
logger.warn(`${this.logPrefix()} Payload too large (${formatSize(result.originalSize)}), truncating... label=${label}`);
|
|
4366
|
+
for (const field of result.truncatedFields) {
|
|
4367
|
+
logger.info(`${this.logPrefix()} Truncated field ${field.path}: ${formatSize(field.originalSize)} → ${formatSize(field.finalSize)}`);
|
|
4368
|
+
}
|
|
4369
|
+
if (result.dropped) {
|
|
4370
|
+
logger.error(`${this.logPrefix()} Payload still too large after truncation: ${formatSize(result.finalSize)}, dropping payload`);
|
|
3947
4371
|
return {
|
|
3948
4372
|
type: 'text',
|
|
3949
4373
|
text: '消息内容过大,无法完整发送,请缩小输出后重试',
|
|
3950
4374
|
};
|
|
3951
4375
|
}
|
|
3952
|
-
logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(
|
|
3953
|
-
return
|
|
4376
|
+
logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(result.originalSize)} → ${formatSize(result.finalSize)}`);
|
|
4377
|
+
return result.payload;
|
|
3954
4378
|
}
|
|
3955
4379
|
buildActivityPayload(envelope, context, item) {
|
|
3956
4380
|
const rawItem = item && typeof item === 'object' && !Array.isArray(item)
|
|
@@ -3962,11 +4386,11 @@ export class AUNChannel {
|
|
|
3962
4386
|
const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
|
|
3963
4387
|
? { ...rawItem, arguments: displayArguments }
|
|
3964
4388
|
: rawItem;
|
|
3965
|
-
return {
|
|
4389
|
+
return withAunAttention({
|
|
3966
4390
|
...this.buildTaskPayloadBase(envelope, context),
|
|
3967
4391
|
type: 'activity',
|
|
3968
4392
|
item: this.stripUndefinedDeep(activityItem),
|
|
3969
|
-
};
|
|
4393
|
+
});
|
|
3970
4394
|
}
|
|
3971
4395
|
async sendReliableStructured(channelId, payload, context, logText) {
|
|
3972
4396
|
const result = await this.sendContentPayload(channelId, payload, {
|
|
@@ -3981,11 +4405,14 @@ export class AUNChannel {
|
|
|
3981
4405
|
throw Object.assign(new Error(result.error ?? 'AUN structured send failed'), {
|
|
3982
4406
|
code: result.code ?? 'AUN_SEND_FAILED',
|
|
3983
4407
|
outboxId: result.outboxId,
|
|
4408
|
+
permanent: true,
|
|
4409
|
+
status: 'failed',
|
|
3984
4410
|
});
|
|
3985
4411
|
}
|
|
3986
4412
|
return result;
|
|
3987
4413
|
}
|
|
3988
4414
|
async sendMessage(channelId, text, context) {
|
|
4415
|
+
parseAunAttention(context?.attention);
|
|
3989
4416
|
if (!text?.trim()) {
|
|
3990
4417
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
3991
4418
|
return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
|
|
@@ -3994,7 +4421,7 @@ export class AUNChannel {
|
|
|
3994
4421
|
const routedContext = this.withDelivery(context, delivery);
|
|
3995
4422
|
// 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
|
|
3996
4423
|
if (!this.isEchoSending) {
|
|
3997
|
-
await this.flushPendingEcho(channelId, delivery);
|
|
4424
|
+
await this.flushPendingEcho(channelId, delivery, context?.threadId);
|
|
3998
4425
|
}
|
|
3999
4426
|
let finalText = text;
|
|
4000
4427
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
@@ -4102,6 +4529,7 @@ export class AUNChannel {
|
|
|
4102
4529
|
if (entry.deliveryReceipt?.messageId) {
|
|
4103
4530
|
return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
|
|
4104
4531
|
}
|
|
4532
|
+
outbox.recordRetry(this.config.aid, entry, result);
|
|
4105
4533
|
return {
|
|
4106
4534
|
status: 'queued',
|
|
4107
4535
|
outboxId: entry.id,
|
|
@@ -4121,6 +4549,8 @@ export class AUNChannel {
|
|
|
4121
4549
|
try {
|
|
4122
4550
|
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
4123
4551
|
finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
4552
|
+
if (finalPayload.thread_id === '')
|
|
4553
|
+
delete finalPayload.thread_id;
|
|
4124
4554
|
}
|
|
4125
4555
|
catch (e) {
|
|
4126
4556
|
return {
|
|
@@ -4222,11 +4652,7 @@ export class AUNChannel {
|
|
|
4222
4652
|
code: 'H_CLASS_PROTECTED',
|
|
4223
4653
|
});
|
|
4224
4654
|
}
|
|
4225
|
-
const
|
|
4226
|
-
call: (method, params) => this.callAndTrace(method, params),
|
|
4227
|
-
close: async () => { },
|
|
4228
|
-
};
|
|
4229
|
-
const built = await uploadFileAndBuildPayload(connection, this.config.aid, file.filePath, {
|
|
4655
|
+
const built = await uploadFileAndBuildPayload(this.client, this.config.aid, file.filePath, {
|
|
4230
4656
|
as: file.as,
|
|
4231
4657
|
contentType: file.contentType,
|
|
4232
4658
|
text: file.text,
|
|
@@ -4247,6 +4673,8 @@ export class AUNChannel {
|
|
|
4247
4673
|
// removed from a malformed mentions array.
|
|
4248
4674
|
const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
|
|
4249
4675
|
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
4676
|
+
if (finalPayload.thread_id === '')
|
|
4677
|
+
delete finalPayload.thread_id;
|
|
4250
4678
|
const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
|
|
4251
4679
|
? normalizeAunMentionEntries(finalPayload.mentions)
|
|
4252
4680
|
: undefined;
|
|
@@ -4321,20 +4749,20 @@ export class AUNChannel {
|
|
|
4321
4749
|
return { ok: false, error: e?.message || String(e), code: e?.code };
|
|
4322
4750
|
}
|
|
4323
4751
|
}
|
|
4324
|
-
async flushPendingEcho(channelId, delivery) {
|
|
4752
|
+
async flushPendingEcho(channelId, delivery, threadId) {
|
|
4325
4753
|
// 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
|
|
4326
4754
|
for (const [key, echo] of this.pendingEchoMessages) {
|
|
4327
|
-
if (echo.channelId === channelId
|
|
4755
|
+
if (echo.channelId === channelId
|
|
4756
|
+
&& (echo.context?.threadId ?? '') === (threadId ?? '')
|
|
4757
|
+
&& echo.context?.delivery?.chatType === delivery.chatType) {
|
|
4328
4758
|
this.pendingEchoMessages.delete(key);
|
|
4329
4759
|
logger.info(`${this.logPrefix()} [Echo] flushPendingEcho triggered: key=${key} channelId=${channelId} pendingCount=${this.pendingEchoMessages.size}`);
|
|
4330
|
-
const
|
|
4331
|
-
|
|
4332
|
-
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
4333
|
-
};
|
|
4334
|
-
const agentDuration = Date.now() - echo.receiveTs;
|
|
4760
|
+
const agentDoneTs = Date.now();
|
|
4761
|
+
const agentDuration = agentDoneTs - echo.receiveTs;
|
|
4335
4762
|
let echoText = echo.text;
|
|
4336
|
-
echoText += `\n${
|
|
4337
|
-
|
|
4763
|
+
echoText += `\n${echoTraceClock(agentDoneTs)} [EvolCore.agent] duration=${agentDuration}ms${echoTraceOriginLabel(agentDoneTs)}`;
|
|
4764
|
+
const replyTs = Date.now();
|
|
4765
|
+
echoText += `\n${echoTraceClock(replyTs)} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((replyTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoTraceOriginLabel(replyTs)}`;
|
|
4338
4766
|
if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
|
|
4339
4767
|
echoText += `\n[TRUNCATED]`;
|
|
4340
4768
|
}
|
|
@@ -4401,10 +4829,16 @@ export class AUNChannel {
|
|
|
4401
4829
|
return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
|
|
4402
4830
|
}
|
|
4403
4831
|
}
|
|
4832
|
+
if (operationId.startsWith(bootstrapInitialMessageOperationPrefix(this.config.aid)) && !entry.deliveryReceipt) {
|
|
4833
|
+
const config = loadAgent(this.config.aid);
|
|
4834
|
+
if (config && resolveAgentLifecycle(config) === 'active') {
|
|
4835
|
+
return { status: 'permanent', code: 'BOOTSTRAP_SUPERSEDED', error: 'Bootstrap is complete; initial prompt cancelled' };
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4404
4838
|
}
|
|
4405
4839
|
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
4406
4840
|
const source = context?.metadata?.source ?? 'daemon';
|
|
4407
|
-
const payload = { type: 'text', text: finalText };
|
|
4841
|
+
const payload = withAunAttention({ type: 'text', text: finalText }, context?.attention);
|
|
4408
4842
|
if (delivery.chatType === 'group') {
|
|
4409
4843
|
const extracted = this.extractMentionEntriesFromText(finalText);
|
|
4410
4844
|
if (extracted.length > 0)
|
|
@@ -4418,6 +4852,8 @@ export class AUNChannel {
|
|
|
4418
4852
|
payload.task_id = context.metadata.taskId;
|
|
4419
4853
|
if (context?.metadata?.chatmode)
|
|
4420
4854
|
payload.chatmode = context.metadata.chatmode;
|
|
4855
|
+
if (context?.metadata?.forkAnchor)
|
|
4856
|
+
payload.fork_anchor = context.metadata.forkAnchor;
|
|
4421
4857
|
const causation = normalizeCausation(context?.metadata?.causation);
|
|
4422
4858
|
const causationRef = delivery.chatType !== 'group' && causation
|
|
4423
4859
|
? `caus_${crypto.randomUUID().replace(/-/g, '')}`
|
|
@@ -4608,6 +5044,7 @@ export class AUNChannel {
|
|
|
4608
5044
|
queue: entry.queue,
|
|
4609
5045
|
};
|
|
4610
5046
|
const receipt = {
|
|
5047
|
+
acceptedAt: Date.now(),
|
|
4611
5048
|
messageId,
|
|
4612
5049
|
encrypt,
|
|
4613
5050
|
transport: this.sendReceiptFromResult(result),
|
|
@@ -4630,6 +5067,15 @@ export class AUNChannel {
|
|
|
4630
5067
|
logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
|
|
4631
5068
|
return { ok: true, status: 'sent' };
|
|
4632
5069
|
}
|
|
5070
|
+
if (interactionId?.startsWith('gir-')) {
|
|
5071
|
+
const result = this.groupInviteDeliveryGate?.(interactionId, entry.channelId) ?? 'stale';
|
|
5072
|
+
if (result === 'defer')
|
|
5073
|
+
return { ok: false, status: 'retry', error: 'group invitation callback is not ready', code: 'INVITE_REVIEW_NOT_READY' };
|
|
5074
|
+
if (result !== 'allow') {
|
|
5075
|
+
logger.info(`${this.logPrefix()} Discarded stale group invitation card: request=${interactionId} entry=${entry.id}`);
|
|
5076
|
+
return { ok: true, status: 'sent' };
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
4633
5079
|
const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
|
|
4634
5080
|
? entry.postSend.expiresAt
|
|
4635
5081
|
: undefined;
|
|
@@ -4777,6 +5223,10 @@ export class AUNChannel {
|
|
|
4777
5223
|
const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
|
|
4778
5224
|
? { ...finalPayload, chatmode: context.metadata.chatmode }
|
|
4779
5225
|
: finalPayload;
|
|
5226
|
+
if (payloadWithChatmode.thread_id === '')
|
|
5227
|
+
delete payloadWithChatmode.thread_id;
|
|
5228
|
+
if (context?.threadId && !payloadWithChatmode.thread_id)
|
|
5229
|
+
payloadWithChatmode.thread_id = context.threadId;
|
|
4780
5230
|
const params = {
|
|
4781
5231
|
context: { type: 'task', id: taskId },
|
|
4782
5232
|
payload: this.limitThoughtPayload(payloadWithChatmode),
|
|
@@ -4857,7 +5307,7 @@ export class AUNChannel {
|
|
|
4857
5307
|
// Validate before checking connection state so malformed producer payloads
|
|
4858
5308
|
// are reported deterministically instead of being hidden by a disconnect.
|
|
4859
5309
|
const validatedPayload = this.normalizeAunPayloadMentions(payload);
|
|
4860
|
-
const finalPayload = this.stripUndefinedDeep(validatedPayload);
|
|
5310
|
+
const finalPayload = this.stripUndefinedDeep(withAunAttention(validatedPayload, context?.attention));
|
|
4861
5311
|
if (!this.connected || !this.client) {
|
|
4862
5312
|
return {
|
|
4863
5313
|
ok: false,
|
|
@@ -4872,12 +5322,19 @@ export class AUNChannel {
|
|
|
4872
5322
|
const encrypt = context?.metadata?.encrypted != null
|
|
4873
5323
|
? !!(context.metadata.encrypted)
|
|
4874
5324
|
: this.shouldEncrypt(encryptTarget);
|
|
5325
|
+
if (finalPayload.thread_id === '')
|
|
5326
|
+
delete finalPayload.thread_id;
|
|
4875
5327
|
if (context?.threadId && !finalPayload.thread_id)
|
|
4876
5328
|
finalPayload.thread_id = context.threadId;
|
|
4877
5329
|
if (context?.replyToMessageId && !finalPayload.ref_message_id)
|
|
4878
5330
|
finalPayload.ref_message_id = context.replyToMessageId;
|
|
4879
5331
|
const isMenuPayload = typeof finalPayload.type === 'string' && finalPayload.type.startsWith('menu.');
|
|
4880
5332
|
const params = { payload: finalPayload, encrypt };
|
|
5333
|
+
if (!isGroup && finalPayload.type === 'menu.response')
|
|
5334
|
+
params.to_device_id = context?.toDeviceId;
|
|
5335
|
+
if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
|
|
5336
|
+
params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
|
|
5337
|
+
}
|
|
4881
5338
|
const rawHeaders = context?.metadata?.protectedHeaders;
|
|
4882
5339
|
if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
|
|
4883
5340
|
const protectedHeaders = {};
|
|
@@ -4943,21 +5400,23 @@ export class AUNChannel {
|
|
|
4943
5400
|
});
|
|
4944
5401
|
}
|
|
4945
5402
|
async sendFile(channelId, filePath, context) {
|
|
5403
|
+
parseAunAttention(context?.attention);
|
|
4946
5404
|
const delivery = this.requireDelivery(channelId, context);
|
|
4947
5405
|
context = this.withDelivery(context, delivery);
|
|
5406
|
+
const isFetch = typeof context?.metadata?.correlationId === 'string' && !!context.metadata.correlationId;
|
|
4948
5407
|
const absPath = path.resolve(filePath);
|
|
4949
5408
|
if (!fs.existsSync(absPath)) {
|
|
4950
5409
|
logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
|
|
4951
|
-
return;
|
|
5410
|
+
return { status: 'failed', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
|
|
4952
5411
|
}
|
|
4953
5412
|
const stat = fs.statSync(absPath);
|
|
4954
5413
|
if (stat.size === 0) {
|
|
4955
5414
|
logger.warn(`${this.logPrefix()} sendFile: file is empty`);
|
|
4956
|
-
return;
|
|
5415
|
+
return { status: 'failed', error: 'file is empty', code: 'EMPTY_FILE' };
|
|
4957
5416
|
}
|
|
4958
5417
|
if (stat.size > 10 * 1024 * 1024) {
|
|
4959
5418
|
logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
|
|
4960
|
-
return;
|
|
5419
|
+
return { status: 'failed', error: `file too large (max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`, code: 'FILE_TOO_LARGE' };
|
|
4961
5420
|
}
|
|
4962
5421
|
// Write-ahead: persist to outbox
|
|
4963
5422
|
const entry = outbox.enqueue(this.config.aid, {
|
|
@@ -4975,17 +5434,47 @@ export class AUNChannel {
|
|
|
4975
5434
|
if (!this.reconnectTimer && !this.client) {
|
|
4976
5435
|
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendFile failed: ${e}`));
|
|
4977
5436
|
}
|
|
4978
|
-
|
|
5437
|
+
const error = 'AUN channel is not connected';
|
|
5438
|
+
if (isFetch) {
|
|
5439
|
+
this.markPermanentOutboxFailure(entry, { error, code: 'AUN_NOT_CONNECTED' });
|
|
5440
|
+
return { status: 'failed', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
|
|
5441
|
+
}
|
|
5442
|
+
return { status: 'queued', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
|
|
4979
5443
|
}
|
|
4980
5444
|
const result = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
|
|
4981
5445
|
if (result.status === 'sent') {
|
|
5446
|
+
const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
|
|
5447
|
+
if (!messageId) {
|
|
5448
|
+
const error = 'file send returned no message_id';
|
|
5449
|
+
this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
|
|
5450
|
+
return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
|
|
5451
|
+
}
|
|
4982
5452
|
this.removeDeliveredOutboxEntry(entry);
|
|
5453
|
+
return { status: 'sent', messageId };
|
|
4983
5454
|
}
|
|
4984
|
-
|
|
5455
|
+
// Remote acceptance is authoritative even if local bookkeeping needs retry.
|
|
5456
|
+
if (entry.deliveryReceipt?.messageId)
|
|
5457
|
+
return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
|
|
5458
|
+
if (result.status === 'permanent' || isFetch) {
|
|
5459
|
+
// A correlated fetch must resolve promptly. Retrying it after reporting
|
|
5460
|
+
// failure could deliver an unsolicited file for an already closed request.
|
|
4985
5461
|
this.markPermanentOutboxFailure(entry, result);
|
|
5462
|
+
return {
|
|
5463
|
+
status: 'failed',
|
|
5464
|
+
outboxId: entry.id,
|
|
5465
|
+
error: result.error ?? 'permanent file send failure',
|
|
5466
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5467
|
+
};
|
|
4986
5468
|
}
|
|
5469
|
+
return {
|
|
5470
|
+
status: 'queued',
|
|
5471
|
+
outboxId: entry.id,
|
|
5472
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
5473
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
5474
|
+
};
|
|
4987
5475
|
}
|
|
4988
5476
|
async sendImage(channelId, data, mimeType, alt, context) {
|
|
5477
|
+
parseAunAttention(context?.attention);
|
|
4989
5478
|
const delivery = this.requireDelivery(channelId, context);
|
|
4990
5479
|
context = this.withDelivery(context, delivery);
|
|
4991
5480
|
if (!Buffer.isBuffer(data)) {
|
|
@@ -5055,11 +5544,7 @@ export class AUNChannel {
|
|
|
5055
5544
|
}
|
|
5056
5545
|
const digest = crypto.createHash('sha256').update(data).digest('hex').slice(0, 12);
|
|
5057
5546
|
const filename = `image-${digest}.${extension}`;
|
|
5058
|
-
const
|
|
5059
|
-
call: (method, params) => this.callAndTrace(method, params),
|
|
5060
|
-
close: async () => { },
|
|
5061
|
-
};
|
|
5062
|
-
const built = await uploadBufferAndBuildPayload(connection, this.config.aid, data, filename, { as: 'image', contentType });
|
|
5547
|
+
const built = await uploadBufferAndBuildPayload(this.client, this.config.aid, data, filename, { as: 'image', contentType });
|
|
5063
5548
|
const imagePayload = { ...built.payload };
|
|
5064
5549
|
setIfDefined(imagePayload, 'alt', alt);
|
|
5065
5550
|
return imagePayload;
|
|
@@ -5146,57 +5631,14 @@ export class AUNChannel {
|
|
|
5146
5631
|
logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
|
|
5147
5632
|
return { status: 'permanent', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
|
|
5148
5633
|
}
|
|
5149
|
-
const filename = path.basename(absPath);
|
|
5150
|
-
const fileData = fs.readFileSync(absPath);
|
|
5151
|
-
const stat = fs.statSync(absPath);
|
|
5152
|
-
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
|
|
5153
|
-
const contentType = guessMime(filename);
|
|
5154
|
-
const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
|
|
5155
5634
|
try {
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
object_key: objectKey,
|
|
5159
|
-
content: fileData.toString('base64'),
|
|
5160
|
-
content_type: contentType,
|
|
5161
|
-
is_private: false,
|
|
5162
|
-
overwrite: true,
|
|
5163
|
-
});
|
|
5164
|
-
}
|
|
5165
|
-
else {
|
|
5166
|
-
const session = await this.callAndTrace('storage.create_upload_session', {
|
|
5167
|
-
object_key: objectKey,
|
|
5168
|
-
size_bytes: stat.size,
|
|
5169
|
-
content_type: contentType,
|
|
5170
|
-
});
|
|
5171
|
-
const uploadUrl = session.upload_url;
|
|
5172
|
-
if (!uploadUrl)
|
|
5173
|
-
throw new Error('No upload_url in session response');
|
|
5174
|
-
this.trace('OUT', 'http.put.upload_url', { object_key: objectKey, size: stat.size });
|
|
5175
|
-
const uploadResp = await fetch(uploadUrl, { method: 'PUT', body: fileData });
|
|
5176
|
-
this.trace('OUT', uploadResp.ok ? 'http.put.upload_url.ok' : 'http.put.upload_url.error', { status: uploadResp.status });
|
|
5177
|
-
if (!uploadResp.ok)
|
|
5178
|
-
throw new Error(`HTTP upload failed: ${uploadResp.status}`);
|
|
5179
|
-
await this.callAndTrace('storage.complete_upload', {
|
|
5180
|
-
object_key: objectKey,
|
|
5181
|
-
sha256,
|
|
5182
|
-
content_type: contentType,
|
|
5183
|
-
is_private: false,
|
|
5184
|
-
size_bytes: stat.size,
|
|
5185
|
-
});
|
|
5186
|
-
}
|
|
5187
|
-
const attachment = {
|
|
5188
|
-
owner_aid: this._aid || '',
|
|
5189
|
-
object_key: objectKey,
|
|
5190
|
-
filename,
|
|
5191
|
-
size_bytes: stat.size,
|
|
5192
|
-
sha256,
|
|
5193
|
-
content_type: contentType,
|
|
5194
|
-
};
|
|
5635
|
+
const { attachment } = await uploadFileAndBuildPayload(this.client, this.config.aid, absPath, { as: 'file' });
|
|
5636
|
+
const { filename, size_bytes: size, content_type: contentType } = attachment;
|
|
5195
5637
|
const filePayload = buildAunFilePayload({
|
|
5196
5638
|
filename,
|
|
5197
|
-
size
|
|
5639
|
+
size,
|
|
5198
5640
|
contentType,
|
|
5199
|
-
attachment,
|
|
5641
|
+
attachment: { ...attachment },
|
|
5200
5642
|
context,
|
|
5201
5643
|
});
|
|
5202
5644
|
const fileTargetAid = channelId;
|
|
@@ -5282,7 +5724,7 @@ export class AUNChannel {
|
|
|
5282
5724
|
logger.error(`${this.logPrefix()} AUN accepted the durable file but its receipt could not be persisted: id=${entry.id} mid=${sentMid} error=${detail}`);
|
|
5283
5725
|
return { status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
|
|
5284
5726
|
}
|
|
5285
|
-
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(
|
|
5727
|
+
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(size)}) → ${channelId}`);
|
|
5286
5728
|
const fileText = filePayload.text;
|
|
5287
5729
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: sentMid, kind: 'file', len: fileText.length, ...(isGroup && { groupId: channelId }) });
|
|
5288
5730
|
const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
|
|
@@ -5401,11 +5843,12 @@ export class AUNChannel {
|
|
|
5401
5843
|
// corrected Owner route was delivered.
|
|
5402
5844
|
if (entry.channelId !== owner || entry.delivery?.chatType !== 'private') {
|
|
5403
5845
|
delete repaired.deliveryReceipt;
|
|
5846
|
+
delete repaired.deliveryResult;
|
|
5847
|
+
delete repaired.terminal;
|
|
5848
|
+
delete repaired.lastError;
|
|
5849
|
+
delete repaired.lastErrorCode;
|
|
5850
|
+
delete repaired.attempts;
|
|
5404
5851
|
}
|
|
5405
|
-
delete repaired.terminal;
|
|
5406
|
-
delete repaired.lastError;
|
|
5407
|
-
delete repaired.lastErrorCode;
|
|
5408
|
-
delete repaired.attempts;
|
|
5409
5852
|
if (outbox.replaceIfRouteMatches(aid, entry, repaired) !== 'replaced')
|
|
5410
5853
|
continue;
|
|
5411
5854
|
logger.warn(`${this.logPrefix()} Repaired bootstrap outbox route to private Owner delivery: entry=${entry.id} owner=${owner}`);
|
|
@@ -5445,17 +5888,21 @@ export class AUNChannel {
|
|
|
5445
5888
|
notice: 'notice',
|
|
5446
5889
|
requires_action: 'requires_action',
|
|
5447
5890
|
};
|
|
5448
|
-
const statusPayload = {
|
|
5891
|
+
const statusPayload = withAunAttention({
|
|
5449
5892
|
type: 'task.status',
|
|
5450
5893
|
status: stateMap[status] ?? status,
|
|
5451
5894
|
task_id: taskId,
|
|
5452
5895
|
session_id: sessionId,
|
|
5453
5896
|
severity,
|
|
5454
|
-
|
|
5455
|
-
};
|
|
5897
|
+
});
|
|
5456
5898
|
const metadataInput = extraMeta ? { ...extraMeta } : undefined;
|
|
5457
5899
|
if (metadataInput)
|
|
5458
5900
|
delete metadataInput.severity;
|
|
5901
|
+
const statusKind = typeof extraMeta?.kind === 'string' ? extraMeta.kind : undefined;
|
|
5902
|
+
if (statusKind)
|
|
5903
|
+
statusPayload.kind = statusKind;
|
|
5904
|
+
if (metadataInput)
|
|
5905
|
+
delete metadataInput.kind;
|
|
5459
5906
|
const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
|
|
5460
5907
|
if (metadata && Object.keys(metadata).length > 0)
|
|
5461
5908
|
statusPayload.metadata = metadata;
|
|
@@ -5508,10 +5955,6 @@ export class AUNChannel {
|
|
|
5508
5955
|
// returns until the channel reconnects.
|
|
5509
5956
|
const delivery = this.requireDelivery(channelId, context);
|
|
5510
5957
|
context = this.withDelivery(context, delivery);
|
|
5511
|
-
if (!this.connected || !this.client) {
|
|
5512
|
-
logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
|
|
5513
|
-
return;
|
|
5514
|
-
}
|
|
5515
5958
|
// SDK 0.3.0 E2EE requires payload to be an object
|
|
5516
5959
|
let payloadObj;
|
|
5517
5960
|
try {
|
|
@@ -5522,9 +5965,20 @@ export class AUNChannel {
|
|
|
5522
5965
|
catch {
|
|
5523
5966
|
payloadObj = { text: payload };
|
|
5524
5967
|
}
|
|
5968
|
+
// File fetch was already acknowledged. Its terminal error must survive a
|
|
5969
|
+
// disconnect so the pending App request is not silently abandoned.
|
|
5970
|
+
if (payloadObj.type === 'menu.response' && payloadObj.name === 'file' && payloadObj.error) {
|
|
5971
|
+
await this.sendReliableStructured(channelId, payloadObj, context);
|
|
5972
|
+
return;
|
|
5973
|
+
}
|
|
5974
|
+
if (!this.connected || !this.client) {
|
|
5975
|
+
logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5525
5978
|
await this.sendStructuredOrThrow(channelId, payloadObj, context);
|
|
5526
5979
|
}
|
|
5527
5980
|
async disconnect() {
|
|
5981
|
+
this.managedMenuClient.close();
|
|
5528
5982
|
this.intentionalDisconnect = true;
|
|
5529
5983
|
if (this.reconnectTimer) {
|
|
5530
5984
|
clearTimeout(this.reconnectTimer);
|
|
@@ -5549,6 +6003,7 @@ export class AUNChannel {
|
|
|
5549
6003
|
this.store = null;
|
|
5550
6004
|
}
|
|
5551
6005
|
this.connected = false;
|
|
6006
|
+
this.clearQueuedObserverSends('intentional disconnect');
|
|
5552
6007
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
|
|
5553
6008
|
appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
|
|
5554
6009
|
this.setAidStatus('disabled');
|
|
@@ -5599,15 +6054,15 @@ export class AUNChannel {
|
|
|
5599
6054
|
};
|
|
5600
6055
|
}
|
|
5601
6056
|
/** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
|
|
5602
|
-
loadSelfName(aid) {
|
|
6057
|
+
loadSelfName(aid, aunPath) {
|
|
5603
6058
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
5604
|
-
const name = resolveAgentDisplayName(aidName);
|
|
6059
|
+
const name = resolveAgentDisplayName(aidName, { aunPath });
|
|
5605
6060
|
if (!name)
|
|
5606
|
-
void this.fetchAndCacheSelfName(aidName);
|
|
6061
|
+
void this.fetchAndCacheSelfName(aidName, aunPath);
|
|
5607
6062
|
return name;
|
|
5608
6063
|
}
|
|
5609
|
-
async fetchAndCacheSelfName(aidName) {
|
|
5610
|
-
const name = await refreshAgentDisplayName(aidName);
|
|
6064
|
+
async fetchAndCacheSelfName(aidName, aunPath) {
|
|
6065
|
+
const name = await refreshAgentDisplayName(aidName, { aunPath });
|
|
5611
6066
|
if (name) {
|
|
5612
6067
|
this._selfName = name;
|
|
5613
6068
|
if (this.aidStatsCollector)
|
|
@@ -5624,13 +6079,20 @@ export class AUNChannel {
|
|
|
5624
6079
|
if (!this.client)
|
|
5625
6080
|
return { type: null };
|
|
5626
6081
|
try {
|
|
6082
|
+
const client = this.client;
|
|
6083
|
+
const revision = this.peerInfoRevision;
|
|
5627
6084
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
5628
|
-
const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
|
|
5629
|
-
const type = identity.type === 'human' ? 'human' : '
|
|
6085
|
+
const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
|
|
6086
|
+
const type = identity.type === 'human' ? 'human' : 'agent';
|
|
5630
6087
|
const name = identity.name || undefined;
|
|
5631
6088
|
const info = { type, name };
|
|
5632
|
-
this.
|
|
5633
|
-
|
|
6089
|
+
if (identity.lastCheckedAt !== 0 && this.client === client && this.peerInfoRevision === revision) {
|
|
6090
|
+
this.peerInfoCache.set(aid, info);
|
|
6091
|
+
setTimeout(() => {
|
|
6092
|
+
if (this.peerInfoCache.get(aid) === info)
|
|
6093
|
+
this.peerInfoCache.delete(aid);
|
|
6094
|
+
}, 30 * 60 * 1000);
|
|
6095
|
+
}
|
|
5634
6096
|
return info;
|
|
5635
6097
|
}
|
|
5636
6098
|
catch (e) {
|
|
@@ -5652,13 +6114,20 @@ export class AUNChannel {
|
|
|
5652
6114
|
if (!this.store)
|
|
5653
6115
|
throw new Error('not connected');
|
|
5654
6116
|
const { agentmdPut } = await import('../aun/aid/agentmd.js');
|
|
5655
|
-
await agentmdPut(content, {
|
|
6117
|
+
await agentmdPut(content, {
|
|
6118
|
+
aid: this.config.aid,
|
|
6119
|
+
store: this.store,
|
|
6120
|
+
aunPath: this.config.keystorePath || resolveRoot(),
|
|
6121
|
+
});
|
|
5656
6122
|
}
|
|
5657
6123
|
async downloadAgentMd(aid) {
|
|
5658
6124
|
if (!this.store)
|
|
5659
6125
|
throw new Error('not connected');
|
|
5660
6126
|
const { agentmdSync } = await import('../aun/aid/agentmd.js');
|
|
5661
|
-
const result = await agentmdSync(aid, {
|
|
6127
|
+
const result = await agentmdSync(aid, {
|
|
6128
|
+
store: this.store ?? undefined,
|
|
6129
|
+
aunPath: this.config.keystorePath || resolveRoot(),
|
|
6130
|
+
});
|
|
5662
6131
|
return result.content ?? '';
|
|
5663
6132
|
}
|
|
5664
6133
|
/**
|
|
@@ -5757,7 +6226,7 @@ export class AUNChannelPlugin {
|
|
|
5757
6226
|
const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
|
|
5758
6227
|
? undefined
|
|
5759
6228
|
: operationId;
|
|
5760
|
-
|
|
6229
|
+
let replyCtx = outboundCausation
|
|
5761
6230
|
? {
|
|
5762
6231
|
...(envelope.replyContext ?? {}),
|
|
5763
6232
|
delivery,
|
|
@@ -5766,6 +6235,13 @@ export class AUNChannelPlugin {
|
|
|
5766
6235
|
: (envelope.replyContext || envelope.delivery)
|
|
5767
6236
|
? { ...(envelope.replyContext ?? {}), delivery }
|
|
5768
6237
|
: undefined;
|
|
6238
|
+
const attention = aunOutboundAttention(payload);
|
|
6239
|
+
if (replyCtx) {
|
|
6240
|
+
replyCtx = { ...replyCtx };
|
|
6241
|
+
delete replyCtx.attention;
|
|
6242
|
+
}
|
|
6243
|
+
if (attention !== undefined)
|
|
6244
|
+
replyCtx = { ...(replyCtx ?? {}), attention };
|
|
5769
6245
|
if (outboundCausation) {
|
|
5770
6246
|
recordCausationSpan(outboundCausation, 'message.outbound', {
|
|
5771
6247
|
status: 'started',
|
|
@@ -5777,12 +6253,34 @@ export class AUNChannelPlugin {
|
|
|
5777
6253
|
case 'result.text':
|
|
5778
6254
|
case 'command.result':
|
|
5779
6255
|
case 'command.error': {
|
|
6256
|
+
// Group command feedback is control traffic, not model input for
|
|
6257
|
+
// the other agents in the conversation.
|
|
6258
|
+
if (delivery.chatType === 'group'
|
|
6259
|
+
&& (payload.kind === 'command.result' || payload.kind === 'command.error')) {
|
|
6260
|
+
const isError = payload.kind === 'command.error';
|
|
6261
|
+
const feedback = {
|
|
6262
|
+
type: isError ? 'error' : 'notice',
|
|
6263
|
+
...taskBase(),
|
|
6264
|
+
subtype: payload.kind,
|
|
6265
|
+
text: payload.text,
|
|
6266
|
+
...(isError
|
|
6267
|
+
? { message: payload.text, user_message: payload.text, recoverable: true }
|
|
6268
|
+
: { severity: 'info' }),
|
|
6269
|
+
};
|
|
6270
|
+
for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
|
|
6271
|
+
setIfDefined(feedback, key, payload[key]);
|
|
6272
|
+
}
|
|
6273
|
+
await channel.sendReliableStructured(channelId, feedback, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
6274
|
+
return;
|
|
6275
|
+
}
|
|
5780
6276
|
const sendCtx = {
|
|
5781
6277
|
...(replyCtx ?? {}),
|
|
5782
6278
|
metadata: {
|
|
5783
6279
|
...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
|
|
5784
6280
|
.filter(([key]) => key !== 'operationId')),
|
|
5785
6281
|
...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
|
|
6282
|
+
...(payload.kind === 'result.text' && payload.metadata?.forkAnchor
|
|
6283
|
+
? { forkAnchor: payload.metadata.forkAnchor } : {}),
|
|
5786
6284
|
},
|
|
5787
6285
|
};
|
|
5788
6286
|
if (payload.kind === 'result.text' && payload.isFinal)
|
|
@@ -5810,7 +6308,7 @@ export class AUNChannelPlugin {
|
|
|
5810
6308
|
messages: [{
|
|
5811
6309
|
messageId: result.messageId,
|
|
5812
6310
|
partIndex: 0,
|
|
5813
|
-
...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
|
|
6311
|
+
...(typeof sendCtx.threadId === 'string' ? { threadId: sendCtx.threadId } : {}),
|
|
5814
6312
|
}],
|
|
5815
6313
|
};
|
|
5816
6314
|
}
|
|
@@ -5846,14 +6344,13 @@ export class AUNChannelPlugin {
|
|
|
5846
6344
|
}
|
|
5847
6345
|
case 'system.error': {
|
|
5848
6346
|
const errorPayload = {
|
|
5849
|
-
type: 'error',
|
|
6347
|
+
type: payload.recoverable === true ? 'notice' : 'error',
|
|
5850
6348
|
...taskBase(),
|
|
5851
6349
|
subtype: payload.subtype,
|
|
5852
6350
|
text: payload.text,
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
terminal: payload.recoverable !== true,
|
|
6351
|
+
...(payload.recoverable === true
|
|
6352
|
+
? { severity: 'warn' }
|
|
6353
|
+
: { message: payload.text, user_message: payload.text, recoverable: false }),
|
|
5857
6354
|
};
|
|
5858
6355
|
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
5859
6356
|
return;
|
|
@@ -5866,7 +6363,6 @@ export class AUNChannelPlugin {
|
|
|
5866
6363
|
text: payload.text,
|
|
5867
6364
|
message: payload.text,
|
|
5868
6365
|
user_message: payload.text,
|
|
5869
|
-
terminal: true,
|
|
5870
6366
|
};
|
|
5871
6367
|
await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
|
|
5872
6368
|
return;
|
|
@@ -5875,8 +6371,27 @@ export class AUNChannelPlugin {
|
|
|
5875
6371
|
const fileCtx = payload.correlationId
|
|
5876
6372
|
? { ...(replyCtx ?? {}), metadata: { ...(replyCtx?.metadata ?? {}), correlationId: payload.correlationId } }
|
|
5877
6373
|
: replyCtx;
|
|
5878
|
-
await channel.sendFile(channelId, payload.filePath, fileCtx);
|
|
5879
|
-
|
|
6374
|
+
const result = await channel.sendFile(channelId, payload.filePath, fileCtx);
|
|
6375
|
+
if (result.status === 'sent')
|
|
6376
|
+
return sentReceipt(envelope, [result.messageId], fileCtx?.threadId);
|
|
6377
|
+
if (result.status === 'queued') {
|
|
6378
|
+
return {
|
|
6379
|
+
status: 'queued',
|
|
6380
|
+
operationId,
|
|
6381
|
+
messages: [],
|
|
6382
|
+
outboxId: result.outboxId,
|
|
6383
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
6384
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
6385
|
+
};
|
|
6386
|
+
}
|
|
6387
|
+
return {
|
|
6388
|
+
status: 'failed',
|
|
6389
|
+
operationId,
|
|
6390
|
+
messages: [],
|
|
6391
|
+
...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
|
|
6392
|
+
error: result.error,
|
|
6393
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
6394
|
+
};
|
|
5880
6395
|
}
|
|
5881
6396
|
case 'result.image': {
|
|
5882
6397
|
await channel.sendImage(channelId, payload.data, payload.mimeType, payload.alt, replyCtx);
|
|
@@ -6020,13 +6535,13 @@ export class AUNChannelPlugin {
|
|
|
6020
6535
|
};
|
|
6021
6536
|
if (req.kind.kind === 'action') {
|
|
6022
6537
|
const action = req.kind;
|
|
6023
|
-
const aunCard = {
|
|
6538
|
+
const aunCard = withAunAttention({
|
|
6024
6539
|
type: 'action_card',
|
|
6025
6540
|
title: action.title,
|
|
6026
6541
|
actions: action.buttons.map(btn => ({
|
|
6027
6542
|
label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
|
|
6028
6543
|
})),
|
|
6029
|
-
};
|
|
6544
|
+
});
|
|
6030
6545
|
// AUN action_card 以 compose behavior 声明自由文本动作。新版客户端
|
|
6031
6546
|
// 在卡片内采集并确认;旧版客户端可继续从主编辑器采集。两者都结构化
|
|
6032
6547
|
// 回传 action_card_reply{behavior:"compose"},Core 不拦截普通消息队列。
|
|
@@ -6044,6 +6559,7 @@ export class AUNChannelPlugin {
|
|
|
6044
6559
|
const result = await channel.sendContentPayload(channelId, aunCard, {
|
|
6045
6560
|
contentKind: 'card',
|
|
6046
6561
|
context: replyCtx,
|
|
6562
|
+
dedupeKey: req.id.startsWith('gir-') ? operationId : undefined,
|
|
6047
6563
|
logText: action.title ? `[card] ${action.title}` : '[card]',
|
|
6048
6564
|
postSend: {
|
|
6049
6565
|
type: 'register_interaction_card',
|
|
@@ -6058,7 +6574,7 @@ export class AUNChannelPlugin {
|
|
|
6058
6574
|
}
|
|
6059
6575
|
else if (req.kind.kind === 'command-card') {
|
|
6060
6576
|
const card = req.kind;
|
|
6061
|
-
const aunCard = {
|
|
6577
|
+
const aunCard = withAunAttention({
|
|
6062
6578
|
type: 'action_card',
|
|
6063
6579
|
title: card.title,
|
|
6064
6580
|
actions: card.buttons.map(btn => {
|
|
@@ -6071,7 +6587,7 @@ export class AUNChannelPlugin {
|
|
|
6071
6587
|
setIfDefined(action, 'disabled', btn.disabled);
|
|
6072
6588
|
return action;
|
|
6073
6589
|
}),
|
|
6074
|
-
};
|
|
6590
|
+
});
|
|
6075
6591
|
if (card.body) {
|
|
6076
6592
|
aunCard.text = card.body;
|
|
6077
6593
|
setIfDefined(aunCard, 'format', card.bodyFormat);
|
|
@@ -6162,6 +6678,14 @@ export class AUNChannelPlugin {
|
|
|
6162
6678
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
6163
6679
|
handler(aunOptsToInbound(opts, adapter.channelName, channelType));
|
|
6164
6680
|
}), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
|
|
6681
|
+
bridge.registerGroupInviteReviewChannel({
|
|
6682
|
+
selfAid: aid,
|
|
6683
|
+
channelName: adapter.channelName,
|
|
6684
|
+
channelKey: adapter.channelKey,
|
|
6685
|
+
agentName: ctx.agentName,
|
|
6686
|
+
channel,
|
|
6687
|
+
adapter,
|
|
6688
|
+
});
|
|
6165
6689
|
},
|
|
6166
6690
|
registerHooks(hookCtx) {
|
|
6167
6691
|
channel.setEventBus(hookCtx.eventBus);
|