evolcore 0.0.21 → 0.0.23
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/AGENTS.md +62 -0
- package/CHANGELOG.md +53 -0
- package/README.md +2 -0
- package/bin/codex-managed-acl.ps1 +155 -0
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +376 -96
- package/dist/agents/baseagent-availability.js +48 -0
- package/dist/agents/baseagent.js +1 -1
- package/dist/agents/claude-runner.js +44 -226
- package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
- package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
- package/dist/agents/codex/catalog.js +70 -0
- package/dist/agents/codex/diagnostics.js +154 -0
- package/dist/agents/codex-runner.js +1292 -219
- package/dist/agents/ecagent-runner.js +10 -31
- package/dist/agents/gemini-runner.js +2 -6
- package/dist/aun/aid/store.js +2 -0
- package/dist/aun/menu-client.js +272 -0
- package/dist/aun/msg/group.js +65 -4
- package/dist/aun/msg/index.js +1 -1
- package/dist/aun/msg/managed-operation.js +51 -4
- package/dist/aun/outbox.js +7 -0
- package/dist/aun/rpc/client.js +8 -1
- package/dist/channels/aun.js +240 -36
- package/dist/channels/wechat.js +81 -10
- package/dist/cli/aun-commands.js +48 -4
- package/dist/cli/command-log.js +21 -2
- package/dist/cli/daemon-commands.js +169 -51
- package/dist/cli/index.js +20 -0
- package/dist/cli/init.js +2 -10
- package/dist/cli/menu-client.js +95 -0
- package/dist/cli/model.js +23 -8
- package/dist/cli/task-context.js +16 -2
- package/dist/cli/trigger-command.js +22 -5
- package/dist/config/config-field-policy.js +3 -0
- package/dist/config/config-manager.js +32 -5
- package/dist/config/contact-book-store.js +25 -3
- package/dist/config/schema-migration.js +14 -2
- package/dist/core/auth/agent-delegation.js +15 -0
- package/dist/core/auth/auth-gateway.js +8 -0
- package/dist/core/auth/authorization-audit.js +70 -6
- package/dist/core/auth/operation-catalog.js +4 -0
- package/dist/core/auth/remote-access-store.js +135 -0
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +93 -25
- package/dist/core/command/command-handler.js +62 -5
- package/dist/core/command/daemon-diagnostics.js +448 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +150 -33
- package/dist/core/command/menu-protocol.js +120 -1
- package/dist/core/command/slash-gate.js +3 -3
- package/dist/core/command/slash-handler.js +83 -40
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +32 -0
- package/dist/core/handoff/runtime.js +23 -3
- package/dist/core/message/im-renderer.js +7 -3
- package/dist/core/message/message-bridge.js +60 -2
- package/dist/core/message/message-log.js +33 -0
- package/dist/core/message/message-queue.js +41 -15
- package/dist/core/message/response-engine.js +246 -114
- package/dist/core/model/catalog-service.js +308 -0
- package/dist/core/model/model-catalog.js +124 -334
- package/dist/core/model/model-diagnostics.js +4 -5
- package/dist/core/model/model-fallback.js +9 -16
- package/dist/core/model/model-permission.js +1 -1
- package/dist/core/permission/approval-gateway.js +16 -1
- package/dist/core/permission/ec-command-parser.js +272 -70
- package/dist/core/permission/protected-paths.js +74 -14
- package/dist/core/permission/sandbox-runtime.js +4 -2
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +290 -30
- package/dist/core/runtime-lock.js +13 -8
- package/dist/core/session/session-manager.js +30 -0
- package/dist/core/session/session-renew.js +18 -1
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/index.js +104 -34
- package/dist/ipc.js +218 -96
- package/dist/paths.js +18 -0
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/feedback.js +14 -2
- package/dist/trigger/manager.js +3 -1
- package/dist/trigger/parser.js +86 -13
- package/dist/trigger/patch.js +104 -1
- package/dist/trigger/scheduler.js +20 -3
- package/dist/utils/ecweb-supervisor.js +11 -2
- package/dist/utils/ecweb-utils.js +8 -26
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/logger.js +9 -4
- package/dist/utils/npm-ops.js +47 -15
- package/dist/utils/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/evolcore/INDEX.md +3 -2
- package/kits/docs/evolcore/agent-create.md +159 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/group-collaboration.md +256 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/menu-client.md +34 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
- package/kits/migrations/migrate-role-config-v4.mjs +11 -5
- package/kits/migrations/migrate-role-config-v5.mjs +8 -2
- package/kits/schemas/_meta.json +3 -2
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/bootstrap.md +10 -2
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +11 -3
package/dist/channels/aun.js
CHANGED
|
@@ -34,7 +34,8 @@ import { deriveCausation, normalizeCausation } from '../core/causation/context.j
|
|
|
34
34
|
import { recordCausationSpan } from '../core/causation/audit.js';
|
|
35
35
|
import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
36
36
|
import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
|
|
37
|
-
import {
|
|
37
|
+
import { toolInputForDisplay } from '../utils/tool-summary.js';
|
|
38
|
+
import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
|
|
38
39
|
import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
|
|
39
40
|
import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
|
|
40
41
|
import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
|
|
@@ -45,6 +46,9 @@ const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
45
46
|
const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
|
|
46
47
|
const AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS = 3;
|
|
47
48
|
const AUN_ATTACHMENT_RETRY_DELAYS_MS = [250, 750];
|
|
49
|
+
const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
|
|
50
|
+
const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
|
|
51
|
+
const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
|
|
48
52
|
const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
|
|
49
53
|
export function formatAunSendLogText(text) {
|
|
50
54
|
return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
|
|
@@ -480,7 +484,8 @@ function redactMenuTokensForTrace(value, seen = new WeakSet()) {
|
|
|
480
484
|
return value.map(item => redactMenuTokensForTrace(item, seen));
|
|
481
485
|
const redacted = {};
|
|
482
486
|
for (const [key, item] of Object.entries(value)) {
|
|
483
|
-
|
|
487
|
+
const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
|
|
488
|
+
redacted[key] = /^(?:menu|ecweb)_token$/.test(normalizedKey)
|
|
484
489
|
? '[REDACTED]'
|
|
485
490
|
: redactMenuTokensForTrace(item, seen);
|
|
486
491
|
}
|
|
@@ -1062,6 +1067,18 @@ export class AUNChannel {
|
|
|
1062
1067
|
.replace(/[ \t]+/g, ' ')
|
|
1063
1068
|
.trim();
|
|
1064
1069
|
}
|
|
1070
|
+
/** Normalize a command after a client-rendered friendly mention label. */
|
|
1071
|
+
normalizeSlashCommandText(text) {
|
|
1072
|
+
const trimmed = text.trim();
|
|
1073
|
+
const leadingLabels = trimmed.match(/^(?:@[^\s]+\s+)+(?=\/)/u);
|
|
1074
|
+
const commandText = leadingLabels
|
|
1075
|
+
? trimmed.slice(leadingLabels[0].length).trimStart()
|
|
1076
|
+
: trimmed;
|
|
1077
|
+
// Keep the command contract consistent for mixed friendly/canonical @
|
|
1078
|
+
// tokens: selector labels are removed above, then known AID-shaped tokens
|
|
1079
|
+
// anywhere in the command are removed by the existing helper.
|
|
1080
|
+
return this.stripAllMentions(commandText);
|
|
1081
|
+
}
|
|
1065
1082
|
parsePayloadMentionsOrReject(payload, context, messageId, seq) {
|
|
1066
1083
|
const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
|
|
1067
1084
|
? payload.mentions
|
|
@@ -1277,6 +1294,8 @@ export class AUNChannel {
|
|
|
1277
1294
|
/** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
|
|
1278
1295
|
invalidatedInteractions = new Map();
|
|
1279
1296
|
mentionModeResolver;
|
|
1297
|
+
/** Agent-level policy controlling whether group slash commands require structured mentions. */
|
|
1298
|
+
groupSlashRequireMentionsResolver;
|
|
1280
1299
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
1281
1300
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
1282
1301
|
'merge', 'link', 'location', 'personal_card',
|
|
@@ -1401,6 +1420,14 @@ export class AUNChannel {
|
|
|
1401
1420
|
}
|
|
1402
1421
|
}
|
|
1403
1422
|
async _initClientInner() {
|
|
1423
|
+
this.observerGroupMembers.clear();
|
|
1424
|
+
for (const groupId of new Set([
|
|
1425
|
+
...this.observerGroupMembershipEpoch.keys(),
|
|
1426
|
+
...this.observerGroupMemberFetches.keys(),
|
|
1427
|
+
])) {
|
|
1428
|
+
this.observerGroupMembershipEpoch.set(groupId, (this.observerGroupMembershipEpoch.get(groupId) ?? 0) + 1);
|
|
1429
|
+
}
|
|
1430
|
+
this.observerGroupMemberFetches.clear();
|
|
1404
1431
|
// Clean up existing client if any
|
|
1405
1432
|
if (this.client) {
|
|
1406
1433
|
this.trace('OUT', 'client.close', { reason: 'initClient' });
|
|
@@ -1469,6 +1496,44 @@ export class AUNChannel {
|
|
|
1469
1496
|
logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
|
|
1470
1497
|
this.handleIncomingGroupMessage(data);
|
|
1471
1498
|
});
|
|
1499
|
+
client.on('group.changed', (data) => {
|
|
1500
|
+
if (!isCurrentClient())
|
|
1501
|
+
return;
|
|
1502
|
+
const record = data && typeof data === 'object' ? data : {};
|
|
1503
|
+
const envelope = record.envelope && typeof record.envelope === 'object'
|
|
1504
|
+
? record.envelope
|
|
1505
|
+
: {};
|
|
1506
|
+
this.invalidateObserverGroupMembers(envelope.group_id ?? record.group_id);
|
|
1507
|
+
});
|
|
1508
|
+
// fastaun 0.5.12+ exposes targeted invitations as a local, aggregated
|
|
1509
|
+
// event. It is a control signal, not a user message, so keep it out of
|
|
1510
|
+
// the Agent routing path while retaining it in diagnostics.
|
|
1511
|
+
client.on('group.invite_received', (data) => {
|
|
1512
|
+
if (!isCurrentClient())
|
|
1513
|
+
return;
|
|
1514
|
+
const record = data && typeof data === 'object' ? data : {};
|
|
1515
|
+
const groupId = record.group_id ?? record.group_aid ?? 'unknown';
|
|
1516
|
+
const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
|
|
1517
|
+
this.trace('IN', 'group.invite_received', data);
|
|
1518
|
+
logger.info(`${this.logPrefix()} Group invitation received: group=${groupId} pending=${inviteIds.length || 1}`);
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
// 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
|
|
1522
|
+
// progress. They must never be interpreted as inbound chat messages.
|
|
1523
|
+
for (const eventName of ['delivery.changed', 'sync.started', 'sync.progress', 'sync.stopped']) {
|
|
1524
|
+
client.on(eventName, (data) => {
|
|
1525
|
+
if (!isCurrentClient())
|
|
1526
|
+
return;
|
|
1527
|
+
this.trace('IN', eventName, data);
|
|
1528
|
+
if (eventName === 'sync.progress') {
|
|
1529
|
+
const record = data && typeof data === 'object' ? data : {};
|
|
1530
|
+
logger.debug(`${this.logPrefix()} AUN sync progress: namespace=${record.namespace ?? 'unknown'} page=${record.page ?? '?'} pulled=${record.page_pulled_count ?? record.pulled_count ?? 0}`);
|
|
1531
|
+
}
|
|
1532
|
+
else if (eventName === 'sync.stopped') {
|
|
1533
|
+
const record = data && typeof data === 'object' ? data : {};
|
|
1534
|
+
logger.info(`${this.logPrefix()} AUN sync stopped: reason=${record.reason ?? 'unknown'}`);
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1472
1537
|
}
|
|
1473
1538
|
const handleSdkState = (source) => (data) => {
|
|
1474
1539
|
if (!isCurrentClient())
|
|
@@ -2034,7 +2099,7 @@ export class AUNChannel {
|
|
|
2034
2099
|
// 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
|
|
2035
2100
|
// (即便日后 from-owner 排除规则调整,也不会泄漏)。
|
|
2036
2101
|
if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
|
|
2037
|
-
this.forwardInbound(msg);
|
|
2102
|
+
void this.forwardInbound(msg);
|
|
2038
2103
|
}
|
|
2039
2104
|
// 回声过滤:自己发出的消息会被 gateway fanout 回来,
|
|
2040
2105
|
// 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
|
|
@@ -2205,7 +2270,7 @@ export class AUNChannel {
|
|
|
2205
2270
|
}
|
|
2206
2271
|
// Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
|
|
2207
2272
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
2208
|
-
this.forwardInbound(msg);
|
|
2273
|
+
void this.forwardInbound(msg);
|
|
2209
2274
|
logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
|
|
2210
2275
|
if (!groupId || !senderAid) {
|
|
2211
2276
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -2316,9 +2381,13 @@ export class AUNChannel {
|
|
|
2316
2381
|
const firstLineGroup = text.split('\n')[0] || '';
|
|
2317
2382
|
const hasEvolCoreTraceGroup = /\[EvolCore\.(receive|reply|agent)\]/.test(text);
|
|
2318
2383
|
const isEchoMsg = /echo/i.test(firstLineGroup) && !hasEvolCoreTraceGroup;
|
|
2384
|
+
const slashCommandText = this.normalizeSlashCommandText(text);
|
|
2319
2385
|
// 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
|
|
2320
2386
|
// echo 消息走独立的 trace 流程,不参与命令语义判定。
|
|
2321
|
-
const isCommandMsg = !isEchoMsg &&
|
|
2387
|
+
const isCommandMsg = !isEchoMsg && slashCommandText.startsWith('/');
|
|
2388
|
+
const groupSlashRequireMentions = this.groupSlashRequireMentionsResolver
|
|
2389
|
+
? await this.groupSlashRequireMentionsResolver().catch(() => undefined) ?? true
|
|
2390
|
+
: true;
|
|
2322
2391
|
if (isEchoMsg) {
|
|
2323
2392
|
// 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
|
|
2324
2393
|
// >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
|
|
@@ -2345,9 +2414,10 @@ export class AUNChannel {
|
|
|
2345
2414
|
}
|
|
2346
2415
|
else {
|
|
2347
2416
|
// 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
|
|
2348
|
-
// slash
|
|
2349
|
-
//
|
|
2350
|
-
const enforceMention = mentionMode === 'mention-only'
|
|
2417
|
+
// slash 命令默认要求结构化 @;Agent 可通过
|
|
2418
|
+
// groupSlashRequireMentions=false 选择仅按正文命令格式处理。
|
|
2419
|
+
const enforceMention = (mentionMode === 'mention-only' && !isCommandMsg)
|
|
2420
|
+
|| (isCommandMsg && groupSlashRequireMentions);
|
|
2351
2421
|
const isMentioned = mentionedSelf || mentionedAll;
|
|
2352
2422
|
// 过滤逻辑下移到响应层:这里只标记,不过滤
|
|
2353
2423
|
// 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
|
|
@@ -2355,14 +2425,14 @@ export class AUNChannel {
|
|
|
2355
2425
|
if (enforceMention && !isMentioned && !isOwnHandoff) {
|
|
2356
2426
|
this.acknowledgeImmediately(messageId, seq);
|
|
2357
2427
|
this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
|
|
2358
|
-
logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
2428
|
+
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))})`);
|
|
2359
2429
|
return;
|
|
2360
2430
|
}
|
|
2361
2431
|
}
|
|
2362
2432
|
// 命令消息:剥离所有 @(多 agent 被 @ 时各自拿到干净的 /status 各自执行);
|
|
2363
2433
|
// 普通消息:仅在唯一 @ 是自己时剥离,保留其他 @ 供 agent 感知。
|
|
2364
2434
|
const strippedText = isCommandMsg
|
|
2365
|
-
?
|
|
2435
|
+
? slashCommandText
|
|
2366
2436
|
: this.stripSelfMentionIfOnly(text, this._aid);
|
|
2367
2437
|
// Detect attachments before the empty-text guard (顶层 + 嵌套)
|
|
2368
2438
|
const rawAttachments = this.collectAllAttachments(payload);
|
|
@@ -2393,7 +2463,7 @@ export class AUNChannel {
|
|
|
2393
2463
|
: mentionedSelf
|
|
2394
2464
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
2395
2465
|
: `${mentionMode}.no-mention`;
|
|
2396
|
-
logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
2466
|
+
logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
2397
2467
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
2398
2468
|
if (payloadType === 'action_card_reply')
|
|
2399
2469
|
return;
|
|
@@ -2564,6 +2634,9 @@ export class AUNChannel {
|
|
|
2564
2634
|
// observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
|
|
2565
2635
|
// in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
|
|
2566
2636
|
observerConfigResolver;
|
|
2637
|
+
observerGroupMembers = new Map();
|
|
2638
|
+
observerGroupMemberFetches = new Map();
|
|
2639
|
+
observerGroupMembershipEpoch = new Map();
|
|
2567
2640
|
/** 注入观察者配置读取器(daemon 侧从 EvolAgent merged config 读)。 */
|
|
2568
2641
|
setObserverConfigResolver(fn) {
|
|
2569
2642
|
this.observerConfigResolver = fn;
|
|
@@ -2582,23 +2655,129 @@ export class AUNChannel {
|
|
|
2582
2655
|
* data 为 SDK message.received / group.message_created 回调的整个对象,
|
|
2583
2656
|
* 不拆解、不重组——SDK 信封结构变化不影响此处。
|
|
2584
2657
|
*/
|
|
2658
|
+
async resolveObserverGroupMembers(groupId) {
|
|
2659
|
+
if (!groupId || !this.client)
|
|
2660
|
+
return undefined;
|
|
2661
|
+
const cached = this.observerGroupMembers.get(groupId);
|
|
2662
|
+
if (cached && cached.expiresAt > Date.now())
|
|
2663
|
+
return cached.aids;
|
|
2664
|
+
const pending = this.observerGroupMemberFetches.get(groupId);
|
|
2665
|
+
if (pending)
|
|
2666
|
+
return pending;
|
|
2667
|
+
const epoch = this.observerGroupMembershipEpoch.get(groupId) ?? 0;
|
|
2668
|
+
let request;
|
|
2669
|
+
request = (async () => {
|
|
2670
|
+
try {
|
|
2671
|
+
const result = await this.callAndTrace('group.get_members', {
|
|
2672
|
+
group_id: groupId,
|
|
2673
|
+
page: 1,
|
|
2674
|
+
size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
|
|
2675
|
+
});
|
|
2676
|
+
const rawMembers = Array.isArray(result?.members)
|
|
2677
|
+
? result.members
|
|
2678
|
+
: Array.isArray(result?.items) ? result.items : undefined;
|
|
2679
|
+
if (!rawMembers)
|
|
2680
|
+
return undefined;
|
|
2681
|
+
const hasTotal = Number.isFinite(Number(result?.total));
|
|
2682
|
+
const hasMore = result?.has_more === true || result?.hasMore === true;
|
|
2683
|
+
if (!hasTotal && (hasMore || rawMembers.length >= OBSERVER_GROUP_MEMBERS_PAGE_SIZE))
|
|
2684
|
+
return undefined;
|
|
2685
|
+
const allMembers = [...rawMembers];
|
|
2686
|
+
const total = Number(result?.total);
|
|
2687
|
+
const pageSize = Number(result?.size) > 0 ? Number(result.size) : OBSERVER_GROUP_MEMBERS_PAGE_SIZE;
|
|
2688
|
+
const pageCount = Number.isFinite(total) && total > allMembers.length
|
|
2689
|
+
? Math.ceil(total / pageSize)
|
|
2690
|
+
: 1;
|
|
2691
|
+
if (pageCount > OBSERVER_GROUP_MEMBERS_MAX_PAGES)
|
|
2692
|
+
return undefined;
|
|
2693
|
+
for (let page = 2; page <= pageCount; page++) {
|
|
2694
|
+
const next = await this.callAndTrace('group.get_members', {
|
|
2695
|
+
group_id: groupId,
|
|
2696
|
+
page,
|
|
2697
|
+
size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
|
|
2698
|
+
});
|
|
2699
|
+
const nextMembers = Array.isArray(next?.members)
|
|
2700
|
+
? next.members
|
|
2701
|
+
: Array.isArray(next?.items) ? next.items : undefined;
|
|
2702
|
+
if (!nextMembers)
|
|
2703
|
+
return undefined;
|
|
2704
|
+
allMembers.push(...nextMembers);
|
|
2705
|
+
}
|
|
2706
|
+
if (hasTotal && total > allMembers.length)
|
|
2707
|
+
return undefined;
|
|
2708
|
+
const aids = new Set();
|
|
2709
|
+
for (const member of allMembers) {
|
|
2710
|
+
const aid = typeof member === 'string'
|
|
2711
|
+
? member.trim()
|
|
2712
|
+
: typeof member?.aid === 'string' ? member.aid.trim()
|
|
2713
|
+
: typeof member?.member_aid === 'string' ? member.member_aid.trim() : '';
|
|
2714
|
+
if (aid)
|
|
2715
|
+
aids.add(aid);
|
|
2716
|
+
}
|
|
2717
|
+
if ((this.observerGroupMembershipEpoch.get(groupId) ?? 0) === epoch) {
|
|
2718
|
+
this.observerGroupMembers.set(groupId, { aids, expiresAt: Date.now() + OBSERVER_GROUP_MEMBERS_TTL_MS });
|
|
2719
|
+
}
|
|
2720
|
+
return aids;
|
|
2721
|
+
}
|
|
2722
|
+
catch (error) {
|
|
2723
|
+
logger.debug(`${this.logPrefix()} observer group membership lookup failed group=${groupId}: ${error}`);
|
|
2724
|
+
return undefined;
|
|
2725
|
+
}
|
|
2726
|
+
finally {
|
|
2727
|
+
if (this.observerGroupMemberFetches.get(groupId) === request) {
|
|
2728
|
+
this.observerGroupMemberFetches.delete(groupId);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
})();
|
|
2732
|
+
this.observerGroupMemberFetches.set(groupId, request);
|
|
2733
|
+
return request;
|
|
2734
|
+
}
|
|
2735
|
+
invalidateObserverGroupMembers(groupId) {
|
|
2736
|
+
if (typeof groupId !== 'string' || !groupId.trim())
|
|
2737
|
+
return;
|
|
2738
|
+
const key = groupId.trim();
|
|
2739
|
+
this.observerGroupMembers.delete(key);
|
|
2740
|
+
this.observerGroupMembershipEpoch.set(key, (this.observerGroupMembershipEpoch.get(key) ?? 0) + 1);
|
|
2741
|
+
}
|
|
2742
|
+
async observerRecipients(owners, envelope) {
|
|
2743
|
+
if (owners.length === 0)
|
|
2744
|
+
return owners;
|
|
2745
|
+
const groupId = typeof envelope.group_id === 'string' ? envelope.group_id.trim() : '';
|
|
2746
|
+
const members = groupId ? await this.resolveObserverGroupMembers(groupId) : undefined;
|
|
2747
|
+
// Unknown membership is fail-open for observation: only an explicit member
|
|
2748
|
+
// response may suppress the redundant owner DM.
|
|
2749
|
+
return owners.filter(owner => !(members?.has(owner)));
|
|
2750
|
+
}
|
|
2585
2751
|
forwardInbound(data) {
|
|
2586
2752
|
if (!this.connected || !this.client)
|
|
2587
|
-
return;
|
|
2753
|
+
return Promise.resolve();
|
|
2588
2754
|
if (payloadTypeForObserver(data)?.startsWith('menu.'))
|
|
2589
|
-
return;
|
|
2755
|
+
return Promise.resolve();
|
|
2590
2756
|
const { observable, owners } = this.getObserverConfig();
|
|
2591
2757
|
if (!observable || owners.length === 0)
|
|
2592
|
-
return;
|
|
2593
|
-
const env = (data?.envelope && typeof data.envelope === 'object')
|
|
2758
|
+
return Promise.resolve();
|
|
2759
|
+
const env = (data?.envelope && typeof data.envelope === 'object')
|
|
2760
|
+
? { ...data.envelope }
|
|
2761
|
+
: {};
|
|
2762
|
+
if (!env.group_id && typeof data.group_id === 'string')
|
|
2763
|
+
env.group_id = data.group_id;
|
|
2594
2764
|
const from = env.from ?? '';
|
|
2595
2765
|
if (this._aid && from === this._aid)
|
|
2596
|
-
return; // self-echo:已在出站转过
|
|
2766
|
+
return Promise.resolve(); // self-echo:已在出站转过
|
|
2597
2767
|
// 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
|
|
2598
|
-
const
|
|
2599
|
-
if (
|
|
2600
|
-
return;
|
|
2601
|
-
|
|
2768
|
+
const candidateOwners = owners.filter(o => o !== from);
|
|
2769
|
+
if (candidateOwners.length === 0)
|
|
2770
|
+
return Promise.resolve();
|
|
2771
|
+
if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
|
|
2772
|
+
this.emitForward('inbound', data, candidateOwners);
|
|
2773
|
+
return Promise.resolve();
|
|
2774
|
+
}
|
|
2775
|
+
return this.observerRecipients(candidateOwners, env)
|
|
2776
|
+
.then(recipientOwners => {
|
|
2777
|
+
if (recipientOwners.length > 0)
|
|
2778
|
+
this.emitForward('inbound', data, recipientOwners);
|
|
2779
|
+
})
|
|
2780
|
+
.catch(error => logger.debug(`${this.logPrefix()} observer inbound recipient lookup failed: ${error}`));
|
|
2602
2781
|
}
|
|
2603
2782
|
/**
|
|
2604
2783
|
* 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
|
|
@@ -2607,20 +2786,33 @@ export class AUNChannel {
|
|
|
2607
2786
|
*/
|
|
2608
2787
|
forwardOutbound(result) {
|
|
2609
2788
|
if (!this.connected || !this.client)
|
|
2610
|
-
return;
|
|
2789
|
+
return Promise.resolve();
|
|
2611
2790
|
if (payloadTypeForObserver(result)?.startsWith('menu.'))
|
|
2612
|
-
return;
|
|
2791
|
+
return Promise.resolve();
|
|
2613
2792
|
const { observable, owners } = this.getObserverConfig();
|
|
2614
2793
|
if (!observable || owners.length === 0)
|
|
2615
|
-
return;
|
|
2616
|
-
const env = (result?.envelope && typeof result.envelope === 'object')
|
|
2794
|
+
return Promise.resolve();
|
|
2795
|
+
const env = (result?.envelope && typeof result.envelope === 'object')
|
|
2796
|
+
? { ...result.envelope }
|
|
2797
|
+
: {};
|
|
2798
|
+
if (!env.group_id && typeof result.group_id === 'string')
|
|
2799
|
+
env.group_id = result.group_id;
|
|
2617
2800
|
const to = env.to ?? env.group_id ?? '';
|
|
2618
2801
|
// 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
|
|
2619
2802
|
// 但仍转发给其他 owner。
|
|
2620
|
-
const
|
|
2621
|
-
if (
|
|
2622
|
-
return;
|
|
2623
|
-
|
|
2803
|
+
const candidateOwners = owners.filter(o => o !== to);
|
|
2804
|
+
if (candidateOwners.length === 0)
|
|
2805
|
+
return Promise.resolve();
|
|
2806
|
+
if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
|
|
2807
|
+
this.emitForward('outbound', result, candidateOwners);
|
|
2808
|
+
return Promise.resolve();
|
|
2809
|
+
}
|
|
2810
|
+
return this.observerRecipients(candidateOwners, env)
|
|
2811
|
+
.then(recipientOwners => {
|
|
2812
|
+
if (recipientOwners.length > 0)
|
|
2813
|
+
this.emitForward('outbound', result, recipientOwners);
|
|
2814
|
+
})
|
|
2815
|
+
.catch(error => logger.debug(`${this.logPrefix()} observer outbound recipient lookup failed: ${error}`));
|
|
2624
2816
|
}
|
|
2625
2817
|
/**
|
|
2626
2818
|
* 实际投递 observer.forward 给每个 owner,使用 daemon 的全局 AUN 默认加密设置。
|
|
@@ -3104,6 +3296,9 @@ export class AUNChannel {
|
|
|
3104
3296
|
setMentionModeResolver(resolver) {
|
|
3105
3297
|
this.mentionModeResolver = resolver;
|
|
3106
3298
|
}
|
|
3299
|
+
setGroupSlashRequireMentionsResolver(resolver) {
|
|
3300
|
+
this.groupSlashRequireMentionsResolver = resolver;
|
|
3301
|
+
}
|
|
3107
3302
|
onRecall(handler) {
|
|
3108
3303
|
this.recallHandler = handler;
|
|
3109
3304
|
}
|
|
@@ -3711,9 +3906,15 @@ export class AUNChannel {
|
|
|
3711
3906
|
return truncated;
|
|
3712
3907
|
}
|
|
3713
3908
|
buildActivityPayload(envelope, context, item) {
|
|
3714
|
-
const
|
|
3909
|
+
const rawItem = item && typeof item === 'object' && !Array.isArray(item)
|
|
3715
3910
|
? { ...item }
|
|
3716
3911
|
: { kind: 'unknown', text: String(item ?? '') };
|
|
3912
|
+
const displayArguments = rawItem.kind === 'tool_call'
|
|
3913
|
+
? toolInputForDisplay(rawItem.name, rawItem.arguments)
|
|
3914
|
+
: undefined;
|
|
3915
|
+
const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
|
|
3916
|
+
? { ...rawItem, arguments: displayArguments }
|
|
3917
|
+
: rawItem;
|
|
3717
3918
|
return {
|
|
3718
3919
|
...this.buildTaskPayloadBase(envelope, context),
|
|
3719
3920
|
type: 'activity',
|
|
@@ -5119,13 +5320,11 @@ export class AUNChannel {
|
|
|
5119
5320
|
const owner = getFirstStaticAgentOwner(aid);
|
|
5120
5321
|
if (!owner)
|
|
5121
5322
|
return;
|
|
5122
|
-
const
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
.map(operationId => outbox.findByDedupeKey(aid, operationId, { includeTerminal: true }))
|
|
5128
|
-
.filter((entry) => !!entry?.channelId);
|
|
5323
|
+
const completionEntry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid), { includeTerminal: true });
|
|
5324
|
+
const entries = [
|
|
5325
|
+
...outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid), { includeTerminal: true }),
|
|
5326
|
+
...(completionEntry ? [completionEntry] : []),
|
|
5327
|
+
].filter((entry) => !!entry?.channelId);
|
|
5129
5328
|
for (const entry of entries) {
|
|
5130
5329
|
const delivery = { chatType: 'private' };
|
|
5131
5330
|
const nestedDelivery = entry.context?.delivery;
|
|
@@ -5915,6 +6114,11 @@ export class AUNChannelPlugin {
|
|
|
5915
6114
|
}, { cache: true }).mentionMode;
|
|
5916
6115
|
});
|
|
5917
6116
|
}
|
|
6117
|
+
if (typeof channel.setGroupSlashRequireMentionsResolver === 'function') {
|
|
6118
|
+
channel.setGroupSlashRequireMentionsResolver(async () => {
|
|
6119
|
+
return resolveEffective({ self: aid }, { cache: true }).groupSlashRequireMentions ?? true;
|
|
6120
|
+
});
|
|
6121
|
+
}
|
|
5918
6122
|
},
|
|
5919
6123
|
};
|
|
5920
6124
|
}
|
package/dist/channels/wechat.js
CHANGED
|
@@ -40,6 +40,8 @@ const BACKOFF_DELAY_MS = 30_000;
|
|
|
40
40
|
const RETRY_DELAY_MS = 2_000;
|
|
41
41
|
const TYPING_TICKET_TTL_MS = 5 * 60 * 1000; // 5 min cache
|
|
42
42
|
const SESSION_EXPIRED_ERRCODE = -14;
|
|
43
|
+
const INVALID_SYNC_CURSOR_ERRCODE = -1;
|
|
44
|
+
const MAX_SYNC_CURSOR_BYTES = 64 * 1024;
|
|
43
45
|
export class WechatApiError extends Error {
|
|
44
46
|
endpoint;
|
|
45
47
|
ret;
|
|
@@ -71,6 +73,26 @@ export class WechatContextTokenRefreshRequiredError extends Error {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* iLink cursors are opaque text, so validation deliberately stays conservative:
|
|
78
|
+
* reject values that cannot be safely round-tripped through the UTF-8 state file
|
|
79
|
+
* without imposing a specific encoding on future protocol versions.
|
|
80
|
+
*/
|
|
81
|
+
export function validateWechatSyncCursor(cursor) {
|
|
82
|
+
if (typeof cursor !== 'string') {
|
|
83
|
+
return { valid: false, reason: 'invalid_type' };
|
|
84
|
+
}
|
|
85
|
+
if (Buffer.byteLength(cursor, 'utf8') > MAX_SYNC_CURSOR_BYTES) {
|
|
86
|
+
return { valid: false, reason: 'too_large' };
|
|
87
|
+
}
|
|
88
|
+
if (cursor.includes('\uFFFD')) {
|
|
89
|
+
return { valid: false, reason: 'invalid_utf8' };
|
|
90
|
+
}
|
|
91
|
+
if (/[\s\u0000-\u001F\u007F-\u009F]/u.test(cursor)) {
|
|
92
|
+
return { valid: false, reason: 'invalid_character' };
|
|
93
|
+
}
|
|
94
|
+
return { valid: true };
|
|
95
|
+
}
|
|
74
96
|
/** Classify fetch/undici failures using the categories exposed by Tencent 2.4.6. */
|
|
75
97
|
export function classifyFetchError(err) {
|
|
76
98
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
@@ -196,6 +218,7 @@ export class WechatChannel {
|
|
|
196
218
|
pendingContextMessages = new Map();
|
|
197
219
|
typingTicketCache = new Map();
|
|
198
220
|
getUpdatesBuf = '';
|
|
221
|
+
lastRejectedSyncCursor;
|
|
199
222
|
syncBufPath;
|
|
200
223
|
contextTokensPath;
|
|
201
224
|
onSessionExpired;
|
|
@@ -243,14 +266,24 @@ export class WechatChannel {
|
|
|
243
266
|
throw new Error('WeChat reauthentication required; run: ec init wechat');
|
|
244
267
|
}
|
|
245
268
|
// 恢复游标
|
|
269
|
+
this.getUpdatesBuf = '';
|
|
270
|
+
this.lastRejectedSyncCursor = undefined;
|
|
246
271
|
try {
|
|
247
272
|
if (fs.existsSync(this.syncBufPath)) {
|
|
248
|
-
|
|
249
|
-
|
|
273
|
+
const restoredCursor = fs.readFileSync(this.syncBufPath, 'utf-8');
|
|
274
|
+
const validation = validateWechatSyncCursor(restoredCursor);
|
|
275
|
+
if (validation.valid) {
|
|
276
|
+
this.getUpdatesBuf = restoredCursor;
|
|
277
|
+
logger.info(`[WeChat] Restored sync cursor (${Buffer.byteLength(restoredCursor, 'utf8')} bytes)`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.clearSyncCursor(`persisted cursor is invalid (${validation.reason})`, restoredCursor);
|
|
281
|
+
}
|
|
250
282
|
}
|
|
251
283
|
}
|
|
252
|
-
catch {
|
|
253
|
-
|
|
284
|
+
catch (err) {
|
|
285
|
+
logger.warn(`[WeChat] Failed to restore sync cursor; starting from an empty cursor: ${err instanceof Error ? err.message : String(err)}`);
|
|
286
|
+
this.getUpdatesBuf = '';
|
|
254
287
|
}
|
|
255
288
|
// 通知 ilink 后端:bot 上线
|
|
256
289
|
try {
|
|
@@ -431,13 +464,22 @@ export class WechatChannel {
|
|
|
431
464
|
}
|
|
432
465
|
consecutiveFailures = 0;
|
|
433
466
|
// 保存游标
|
|
434
|
-
if (resp.get_updates_buf) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
467
|
+
if (resp.get_updates_buf !== undefined) {
|
|
468
|
+
const validation = validateWechatSyncCursor(resp.get_updates_buf);
|
|
469
|
+
if (!validation.valid) {
|
|
470
|
+
this.clearSyncCursor(`server returned an invalid cursor (${validation.reason})`, typeof resp.get_updates_buf === 'string' ? resp.get_updates_buf : undefined);
|
|
438
471
|
}
|
|
439
|
-
|
|
440
|
-
|
|
472
|
+
else if (resp.get_updates_buf) {
|
|
473
|
+
if (resp.get_updates_buf !== this.lastRejectedSyncCursor) {
|
|
474
|
+
this.lastRejectedSyncCursor = undefined;
|
|
475
|
+
}
|
|
476
|
+
this.getUpdatesBuf = resp.get_updates_buf;
|
|
477
|
+
try {
|
|
478
|
+
fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// best-effort
|
|
482
|
+
}
|
|
441
483
|
}
|
|
442
484
|
}
|
|
443
485
|
// 处理消息
|
|
@@ -450,6 +492,17 @@ export class WechatChannel {
|
|
|
450
492
|
return;
|
|
451
493
|
if (this.isSessionPaused())
|
|
452
494
|
return;
|
|
495
|
+
if (err instanceof WechatApiError &&
|
|
496
|
+
err.endpoint === 'ilink/bot/getupdates' &&
|
|
497
|
+
err.hasCode(INVALID_SYNC_CURSOR_ERRCODE) &&
|
|
498
|
+
this.getUpdatesBuf &&
|
|
499
|
+
this.getUpdatesBuf !== this.lastRejectedSyncCursor) {
|
|
500
|
+
const rejectedCursor = this.getUpdatesBuf;
|
|
501
|
+
this.clearSyncCursor('getUpdates returned ret=-1', rejectedCursor);
|
|
502
|
+
consecutiveFailures = 0;
|
|
503
|
+
logger.warn('[WeChat] Retrying getUpdates immediately with an empty sync cursor');
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
453
506
|
consecutiveFailures++;
|
|
454
507
|
if (err instanceof WechatApiError) {
|
|
455
508
|
logger.error(`[WeChat] getUpdates API error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${err.message}`);
|
|
@@ -470,6 +523,24 @@ export class WechatChannel {
|
|
|
470
523
|
}
|
|
471
524
|
}
|
|
472
525
|
}
|
|
526
|
+
clearSyncCursor(reason, rejectedCursor) {
|
|
527
|
+
if (rejectedCursor)
|
|
528
|
+
this.lastRejectedSyncCursor = rejectedCursor;
|
|
529
|
+
this.getUpdatesBuf = '';
|
|
530
|
+
try {
|
|
531
|
+
let persistedAction = 'no persisted cursor';
|
|
532
|
+
if (fs.existsSync(this.syncBufPath)) {
|
|
533
|
+
const quarantinePath = `${this.syncBufPath}.corrupt-${Date.now()}`;
|
|
534
|
+
fs.renameSync(this.syncBufPath, quarantinePath);
|
|
535
|
+
persistedAction = `quarantined as ${path.basename(quarantinePath)}`;
|
|
536
|
+
}
|
|
537
|
+
logger.warn(`[WeChat] Cleared sync cursor: ${reason} (${persistedAction})`);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
logger.warn(`[WeChat] Cleared in-memory sync cursor but failed to quarantine persisted cursor: ` +
|
|
541
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
473
544
|
// ── Inbound Message Handler ──────────────────────────────────────────
|
|
474
545
|
async handleInboundMessage(msg) {
|
|
475
546
|
if (msg.message_type !== MSG_TYPE_USER)
|