evolcore 0.0.12 → 0.0.13
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 +22 -0
- package/bin/ec-safe-output.js +26 -7
- package/dist/agents/claude-runner.js +53 -7
- package/dist/agents/codex-app-server-client.js +53 -5
- package/dist/agents/codex-runner.js +104 -59
- package/dist/agents/ecagent-runner.js +4 -1
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +116 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +371 -136
- package/dist/cli/agent.js +9 -96
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +19 -10
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +5 -0
- package/dist/config/config-manager.js +21 -1
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +29 -1
- package/dist/core/auth/authorization-audit.js +15 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +1 -0
- package/dist/core/command/command-handler.js +198 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +6 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +19 -0
- package/dist/core/message/message-bridge.js +63 -11
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +107 -41
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +93 -14
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +249 -27
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +161 -86
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +10 -0
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +7 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -269,6 +269,9 @@ export class MenuRequestDeduper {
|
|
|
269
269
|
ttlMs;
|
|
270
270
|
maxEntries;
|
|
271
271
|
entries = new Map();
|
|
272
|
+
/** Retain failed request identities briefly so a subsequent identical
|
|
273
|
+
* request is observable as a retry instead of an unrelated first attempt. */
|
|
274
|
+
failures = new Map();
|
|
272
275
|
constructor(ttlMs = 30_000, maxEntries = 2_048) {
|
|
273
276
|
this.ttlMs = ttlMs;
|
|
274
277
|
this.maxEntries = maxEntries;
|
|
@@ -279,7 +282,13 @@ export class MenuRequestDeduper {
|
|
|
279
282
|
if (existing) {
|
|
280
283
|
if (existing.fingerprint !== fingerprint)
|
|
281
284
|
return { conflict: true };
|
|
282
|
-
return { value: await existing.promise, replayed: true };
|
|
285
|
+
return { value: await existing.promise, replayed: true, retry: false };
|
|
286
|
+
}
|
|
287
|
+
const previousFailure = this.failures.get(key);
|
|
288
|
+
if (previousFailure) {
|
|
289
|
+
if (previousFailure.fingerprint !== fingerprint)
|
|
290
|
+
return { conflict: true };
|
|
291
|
+
this.failures.delete(key);
|
|
283
292
|
}
|
|
284
293
|
const promise = operation();
|
|
285
294
|
const entry = { fingerprint, promise, expiresAt: Date.now() + this.ttlMs };
|
|
@@ -288,10 +297,12 @@ export class MenuRequestDeduper {
|
|
|
288
297
|
try {
|
|
289
298
|
const value = await promise;
|
|
290
299
|
entry.expiresAt = Date.now() + this.ttlMs;
|
|
291
|
-
return { value, replayed: false };
|
|
300
|
+
return { value, replayed: false, retry: !!previousFailure };
|
|
292
301
|
}
|
|
293
302
|
catch (error) {
|
|
294
303
|
this.entries.delete(key);
|
|
304
|
+
this.failures.set(key, { fingerprint, expiresAt: Date.now() + this.ttlMs });
|
|
305
|
+
this.enforceLimit();
|
|
295
306
|
throw error;
|
|
296
307
|
}
|
|
297
308
|
}
|
|
@@ -301,6 +312,10 @@ export class MenuRequestDeduper {
|
|
|
301
312
|
if (entry.expiresAt <= now)
|
|
302
313
|
this.entries.delete(key);
|
|
303
314
|
}
|
|
315
|
+
for (const [key, entry] of this.failures) {
|
|
316
|
+
if (entry.expiresAt <= now)
|
|
317
|
+
this.failures.delete(key);
|
|
318
|
+
}
|
|
304
319
|
}
|
|
305
320
|
enforceLimit() {
|
|
306
321
|
while (this.entries.size > this.maxEntries) {
|
|
@@ -309,6 +324,12 @@ export class MenuRequestDeduper {
|
|
|
309
324
|
break;
|
|
310
325
|
this.entries.delete(oldest);
|
|
311
326
|
}
|
|
327
|
+
while (this.failures.size > this.maxEntries) {
|
|
328
|
+
const oldest = this.failures.keys().next().value;
|
|
329
|
+
if (!oldest)
|
|
330
|
+
break;
|
|
331
|
+
this.failures.delete(oldest);
|
|
332
|
+
}
|
|
312
333
|
}
|
|
313
334
|
}
|
|
314
335
|
export class MenuDiagnosticLimiter {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
|
|
2
2
|
import { getCodexEfforts } from '../../agents/codex-runner.js';
|
|
3
|
-
import { buildEnvelope } from '../message/message-utils.js';
|
|
3
|
+
import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
|
|
4
4
|
import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
6
|
import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
|
|
@@ -2492,17 +2492,50 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2492
2492
|
// 执行重启逻辑(共用于卡片回调和文本确认)
|
|
2493
2493
|
const executeRestart = async () => {
|
|
2494
2494
|
const suppressRealRestart = shouldSuppressRealRestart();
|
|
2495
|
-
let replyContext;
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2495
|
+
let restartReplyContext = replyContext;
|
|
2496
|
+
try {
|
|
2497
|
+
if (!restartReplyContext?.delivery && threadId) {
|
|
2498
|
+
const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
2499
|
+
if (threadSession)
|
|
2500
|
+
restartReplyContext = this.getReplyContext(threadSession);
|
|
2501
|
+
}
|
|
2502
|
+
else if (!restartReplyContext?.delivery && activeSession) {
|
|
2503
|
+
restartReplyContext = this.getReplyContext(activeSession);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
catch (error) {
|
|
2507
|
+
const code = error?.code;
|
|
2508
|
+
if (channel.split('#', 1)[0].toLowerCase() !== 'aun'
|
|
2509
|
+
|| (code !== 'AUN_OUTBOUND_ROUTE_REQUIRED' && code !== 'AUN_OUTBOUND_ROUTE_MISMATCH')) {
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2512
|
+
logger.warn(`[System] Refusing restart from invalid persisted AUN session route: channel=${channel} channelId=${channelId} code=${code}`);
|
|
2513
|
+
return false;
|
|
2514
|
+
}
|
|
2515
|
+
// The authenticated inbound transport context takes precedence over a
|
|
2516
|
+
// potentially stale session snapshot. When no session exists, the
|
|
2517
|
+
// explicit command chat type is the only acceptable local fallback.
|
|
2518
|
+
if (restartReplyContext?.delivery === undefined) {
|
|
2519
|
+
restartReplyContext = {
|
|
2520
|
+
...(restartReplyContext ?? {}),
|
|
2521
|
+
...(chatType === 'group'
|
|
2522
|
+
? { delivery: { chatType: 'group', groupId: channelId } }
|
|
2523
|
+
: chatType === 'private'
|
|
2524
|
+
? { delivery: { chatType: 'private' } }
|
|
2525
|
+
: {}),
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
if (channel.split('#', 1)[0].toLowerCase() === 'aun'
|
|
2529
|
+
&& !isDeliveryTargetForChannel(restartReplyContext?.delivery, channelId)) {
|
|
2530
|
+
logger.warn(`[System] Refusing restart without a valid explicit delivery route: channel=${channel} channelId=${channelId}`);
|
|
2531
|
+
return false;
|
|
2500
2532
|
}
|
|
2501
2533
|
const restartInfo = {
|
|
2502
2534
|
channel,
|
|
2503
2535
|
channelId,
|
|
2504
2536
|
timestamp: Date.now(),
|
|
2505
|
-
...(
|
|
2537
|
+
...(restartReplyContext?.delivery ? { delivery: restartReplyContext.delivery } : {}),
|
|
2538
|
+
...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
|
|
2506
2539
|
};
|
|
2507
2540
|
if (!suppressRealRestart) {
|
|
2508
2541
|
const controlDir = daemonControlDir();
|
|
@@ -2527,7 +2560,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2527
2560
|
channelId,
|
|
2528
2561
|
agentName: 'system',
|
|
2529
2562
|
chatmode: 'interactive',
|
|
2530
|
-
replyContext
|
|
2563
|
+
replyContext: restartReplyContext ?? (chatType === 'group'
|
|
2564
|
+
? { delivery: { chatType: 'group', groupId: channelId } }
|
|
2565
|
+
: chatType === 'private'
|
|
2566
|
+
? { delivery: { chatType: 'private' } }
|
|
2567
|
+
: undefined),
|
|
2531
2568
|
});
|
|
2532
2569
|
await adapter.send(envelope, { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' });
|
|
2533
2570
|
// 等待消息发送完成后再延迟 kill
|
|
@@ -2570,7 +2607,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2570
2607
|
return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
|
|
2571
2608
|
}
|
|
2572
2609
|
}
|
|
2573
|
-
await executeRestart();
|
|
2610
|
+
const restarted = await executeRestart();
|
|
2611
|
+
if (!restarted) {
|
|
2612
|
+
return { kind: 'command.error', text: '❌ 无法确定重启通知的出站路由,请从可信会话上下文重试' };
|
|
2613
|
+
}
|
|
2574
2614
|
// executeRestart 内部已经发送了反馈消息,这里返回 null 避免重复发送
|
|
2575
2615
|
return null;
|
|
2576
2616
|
}
|
|
@@ -2728,7 +2768,15 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
2728
2768
|
}
|
|
2729
2769
|
// 发送文件
|
|
2730
2770
|
try {
|
|
2731
|
-
const replyCtx =
|
|
2771
|
+
const replyCtx = isCrossChannel
|
|
2772
|
+
? { delivery: { chatType: 'private' } }
|
|
2773
|
+
: sendSession
|
|
2774
|
+
? this.getReplyContext(sendSession)
|
|
2775
|
+
: chatType === 'group'
|
|
2776
|
+
? { delivery: { chatType: 'group', groupId: channelId } }
|
|
2777
|
+
: chatType === 'private'
|
|
2778
|
+
? { delivery: { chatType: 'private' } }
|
|
2779
|
+
: undefined;
|
|
2732
2780
|
await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
|
|
2733
2781
|
const sizeStr = stat.size < 1024 ? `${stat.size} B`
|
|
2734
2782
|
: stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
|
|
@@ -313,6 +313,9 @@ const CATALOG = [
|
|
|
313
313
|
{ path: 'sessionId', type: 'string' },
|
|
314
314
|
{ path: 'toolName', type: 'string' },
|
|
315
315
|
{ path: 'input', type: 'object' },
|
|
316
|
+
{ path: 'callId', type: 'string', optional: true },
|
|
317
|
+
{ path: 'agentAid', type: 'string', optional: true },
|
|
318
|
+
{ path: 'permissionMode', type: 'string', optional: true },
|
|
316
319
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
317
320
|
],
|
|
318
321
|
},
|
|
@@ -326,6 +329,9 @@ const CATALOG = [
|
|
|
326
329
|
{ path: 'toolName', type: 'string' },
|
|
327
330
|
{ path: 'isError', type: 'boolean', optional: true },
|
|
328
331
|
{ path: 'agentName', type: 'string', optional: true },
|
|
332
|
+
{ path: 'callId', type: 'string', optional: true },
|
|
333
|
+
{ path: 'agentAid', type: 'string', optional: true },
|
|
334
|
+
{ path: 'permissionMode', type: 'string', optional: true },
|
|
329
335
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
330
336
|
],
|
|
331
337
|
},
|
|
@@ -6,6 +6,7 @@ import { HandoffStore } from './store.js';
|
|
|
6
6
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
7
7
|
import { recordCausationSpan } from '../causation/audit.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
|
+
import { replyContextFromSession } from '../message/message-utils.js';
|
|
9
10
|
const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
|
|
10
11
|
const HANDOFF_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
12
|
const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
|
|
@@ -79,7 +80,12 @@ export class HandoffRuntime {
|
|
|
79
80
|
if (!origin || origin.selfAID !== input.selfAid)
|
|
80
81
|
throw new Error('origin session not found');
|
|
81
82
|
const targetChannelKey = formatChannelKey({ type: 'aun', selfAID: input.selfAid, name: 'main' });
|
|
82
|
-
|
|
83
|
+
if (!input.targetChatType) {
|
|
84
|
+
throw Object.assign(new Error('AUN handoff requires an explicit target chat type'), {
|
|
85
|
+
code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const targetChatType = input.targetChatType;
|
|
83
89
|
const target = await this.sessionManager.getOrCreateSession(targetChannelKey, input.to, origin.projectPath, input.thread, {
|
|
84
90
|
channelKey: targetChannelKey,
|
|
85
91
|
peerId: input.to,
|
|
@@ -112,6 +118,9 @@ export class HandoffRuntime {
|
|
|
112
118
|
targetSessionId: target.id,
|
|
113
119
|
payload: input.payload,
|
|
114
120
|
encrypt: input.encrypt,
|
|
121
|
+
delivery: targetChatType === 'group'
|
|
122
|
+
? { chatType: 'group', groupId: input.to }
|
|
123
|
+
: { chatType: 'private' },
|
|
115
124
|
returnPolicy: input.explicitReturnPolicy ?? 'required',
|
|
116
125
|
originAuthorization: input.originAuthorization,
|
|
117
126
|
causation: handoffCausation,
|
|
@@ -209,6 +218,8 @@ export class HandoffRuntime {
|
|
|
209
218
|
messageId,
|
|
210
219
|
timestamp: Date.now(),
|
|
211
220
|
source: 'handoff',
|
|
221
|
+
groupId: origin.chatType === 'group' ? (origin.metadata?.groupId || origin.channelId) : undefined,
|
|
222
|
+
replyContext: replyContextFromSession(origin),
|
|
212
223
|
boundSessionId: origin.id,
|
|
213
224
|
causation: instance.return_causation
|
|
214
225
|
? deriveCausation(instance.return_causation)
|
|
@@ -115,7 +115,11 @@ export class HandoffStore {
|
|
|
115
115
|
origin_session_id: input.originSessionId,
|
|
116
116
|
origin_message_id: input.originMessageId,
|
|
117
117
|
target_session_id: input.targetSessionId,
|
|
118
|
-
request: {
|
|
118
|
+
request: {
|
|
119
|
+
payload: clonePayload(input.payload),
|
|
120
|
+
encrypt: input.encrypt,
|
|
121
|
+
...(input.delivery ? { delivery: input.delivery } : {}),
|
|
122
|
+
},
|
|
119
123
|
...(input.originAuthorization
|
|
120
124
|
? { origin_authorization: { ...input.originAuthorization } }
|
|
121
125
|
: {}),
|
|
@@ -228,6 +228,16 @@ export class InteractionRouter {
|
|
|
228
228
|
return undefined;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
+
/** Bind an adapter response to its runtime-owned channel identity domain. */
|
|
232
|
+
export function registerAdapterInteractions(adapter, router) {
|
|
233
|
+
const channelKey = adapter.channelKey;
|
|
234
|
+
adapter.onInteraction?.(async (response) => {
|
|
235
|
+
return router.handleAsync({ ...response, channelKey });
|
|
236
|
+
});
|
|
237
|
+
adapter.onInteractionInvalidated?.((interactionId, reason) => {
|
|
238
|
+
return router.cancelFromChannel(interactionId, reason);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
231
241
|
/** 把 CommandCard 渲染为文本提示,channel 不支持卡片时使用 */
|
|
232
242
|
export function renderCommandCardAsText(card) {
|
|
233
243
|
const lines = [card.title];
|
|
@@ -244,6 +244,25 @@ export class IMRenderer {
|
|
|
244
244
|
durationMs: opts.durationMs,
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
|
+
/** 发送不进入 activity/thought 历史的非终态 warning status。 */
|
|
248
|
+
async sendWarningStatus(message, subtype, details) {
|
|
249
|
+
const payload = {
|
|
250
|
+
kind: 'status.progress',
|
|
251
|
+
metadata: {
|
|
252
|
+
activityType: 'progress',
|
|
253
|
+
message,
|
|
254
|
+
severity: 'warn',
|
|
255
|
+
...(subtype !== undefined && { subtype }),
|
|
256
|
+
...(details?.missingItems !== undefined && { missingItems: details.missingItems }),
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
await this.opts.send(payload);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
logger.warn(`[IMRenderer] warning status send failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
247
266
|
/** 添加任务生命周期边界事件,复用 activity.batch 投影路径。 */
|
|
248
267
|
addLifecycle(phase, metadata, text) {
|
|
249
268
|
const item = {
|
|
@@ -600,7 +600,7 @@ export class MessageBridge {
|
|
|
600
600
|
channelId: route.approverOperatorId,
|
|
601
601
|
agentName: owningAgent?.name ?? selfAid,
|
|
602
602
|
chatmode: 'interactive',
|
|
603
|
-
replyContext: { metadata: { source: 'handoff', chatmode: 'interactive' } },
|
|
603
|
+
replyContext: { delivery: { chatType: 'private' }, metadata: { source: 'handoff', chatmode: 'interactive' } },
|
|
604
604
|
});
|
|
605
605
|
const sent = await sendInteractionPayload(route.adapter, envelope, interaction, renderActionAsText(interaction), envelope.replyContext);
|
|
606
606
|
if (!sent)
|
|
@@ -615,14 +615,14 @@ export class MessageBridge {
|
|
|
615
615
|
: code === 'request-stale' ? '申请已更新或已被处理。'
|
|
616
616
|
: code === 'revision-conflict' ? '联系人列表已变化,请从联系人列表重新审核。'
|
|
617
617
|
: '当前无审核权限。';
|
|
618
|
-
await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: ownerAid, agentName: agentName ?? '<unknown>' }), { kind: 'system.notice', text, subtype: 'contact-review' });
|
|
618
|
+
await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId: ownerAid, agentName: agentName ?? '<unknown>', delivery: { chatType: 'private' } }), { kind: 'system.notice', text, subtype: 'contact-review' });
|
|
619
619
|
}
|
|
620
620
|
async notifyContactApplicant(adapter, applicantAid, code, requestId, expiresAt, agentName) {
|
|
621
621
|
await adapter.send(buildEnvelope({
|
|
622
622
|
channel: adapter.channelName,
|
|
623
623
|
channelId: applicantAid,
|
|
624
624
|
agentName: agentName ?? '<unknown>',
|
|
625
|
-
replyContext: { metadata: evolMenuResponseTransportMetadata() },
|
|
625
|
+
replyContext: { delivery: { chatType: 'private' }, metadata: evolMenuResponseTransportMetadata() },
|
|
626
626
|
}), {
|
|
627
627
|
kind: 'custom',
|
|
628
628
|
channelType: 'aun',
|
|
@@ -687,11 +687,23 @@ export class MessageBridge {
|
|
|
687
687
|
onMessage(async (msg) => {
|
|
688
688
|
try {
|
|
689
689
|
let content = msg.content.trim();
|
|
690
|
+
// Channels that expose adapter receive time (notably AUN) must keep
|
|
691
|
+
// that timestamp through session resolution and queue processing.
|
|
692
|
+
// The fallback is only for legacy adapters that do not provide it.
|
|
693
|
+
const receivedAt = msg.receivedAt ?? Date.now();
|
|
690
694
|
const channelKey = adapter?.channelKey || channelName;
|
|
691
695
|
const owningAgent = this.agentRegistry?.resolveByChannel(channelKey)
|
|
692
696
|
?? this.agentRegistry?.resolveByChannel(channelName);
|
|
693
697
|
const parsedChannelKey = tryParseChannelKey(channelKey);
|
|
694
698
|
const chatType = msg.chatType || 'private';
|
|
699
|
+
const delivery = chatType === 'group'
|
|
700
|
+
? { chatType: 'group', groupId: msg.groupId || msg.channelId }
|
|
701
|
+
: { chatType: 'private' };
|
|
702
|
+
msg.groupId = delivery.chatType === 'group' ? delivery.groupId : undefined;
|
|
703
|
+
msg.replyContext = {
|
|
704
|
+
...(msg.replyContext ?? {}),
|
|
705
|
+
delivery,
|
|
706
|
+
};
|
|
695
707
|
const actorId = msg.peerId;
|
|
696
708
|
const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
|
|
697
709
|
const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
|
|
@@ -925,7 +937,9 @@ export class MessageBridge {
|
|
|
925
937
|
content: msg.messageLogContent ?? content,
|
|
926
938
|
replyTo: inboundReplyTo,
|
|
927
939
|
permMode: null,
|
|
928
|
-
timestamp:
|
|
940
|
+
timestamp: receivedAt,
|
|
941
|
+
receivedAt,
|
|
942
|
+
gatewaySeq: msg.gatewaySeq,
|
|
929
943
|
encrypt: inboundEncrypt,
|
|
930
944
|
chatmode: inboundChatmode,
|
|
931
945
|
peerName: msg.peerName,
|
|
@@ -1017,9 +1031,12 @@ export class MessageBridge {
|
|
|
1017
1031
|
channelType: msg.channelType || effectiveChannelType,
|
|
1018
1032
|
channelId: msg.channelId, content,
|
|
1019
1033
|
selfAID: msg.selfAID,
|
|
1034
|
+
groupId: msg.groupId,
|
|
1020
1035
|
baseagent: session.baseagent,
|
|
1021
1036
|
chatType,
|
|
1022
|
-
images: msg.images, timestamp:
|
|
1037
|
+
images: msg.images, timestamp: receivedAt,
|
|
1038
|
+
receivedAt,
|
|
1039
|
+
gatewaySeq: msg.gatewaySeq,
|
|
1023
1040
|
peerId: msg.peerId, peerName: msg.peerName,
|
|
1024
1041
|
peerType: msg.peerType,
|
|
1025
1042
|
sameDevice: msg.sameDevice,
|
|
@@ -1062,6 +1079,8 @@ export class MessageBridge {
|
|
|
1062
1079
|
replyTo: inboundReplyTo,
|
|
1063
1080
|
permMode: session.identity?.role ?? null,
|
|
1064
1081
|
timestamp: fullMessage.timestamp,
|
|
1082
|
+
receivedAt: msg.receivedAt,
|
|
1083
|
+
gatewaySeq: msg.gatewaySeq,
|
|
1065
1084
|
encrypt: inboundEncrypt,
|
|
1066
1085
|
chatmode: inboundChatmode,
|
|
1067
1086
|
peerName: msg.peerName,
|
|
@@ -1200,6 +1219,31 @@ export class MessageBridge {
|
|
|
1200
1219
|
if (!hasValidMenuId(parsed))
|
|
1201
1220
|
return;
|
|
1202
1221
|
const header = { id: parsed.id, ...(parsed.name?.trim() ? { name: parsed.name } : {}) };
|
|
1222
|
+
// Menu IDs are the protocol's stable correlation key. Bind it, together
|
|
1223
|
+
// with the active EvolCore session, before authorization so every
|
|
1224
|
+
// CommandAudit record and denial can be joined to the same request.
|
|
1225
|
+
let activeSession;
|
|
1226
|
+
if (msg.threadId && typeof this.sessionManager.getThreadSession === 'function') {
|
|
1227
|
+
try {
|
|
1228
|
+
activeSession = await this.sessionManager.getThreadSession(channel, msg.channelId, msg.threadId);
|
|
1229
|
+
}
|
|
1230
|
+
catch {
|
|
1231
|
+
activeSession = undefined;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (!activeSession && typeof this.sessionManager.getActiveSession === 'function') {
|
|
1235
|
+
try {
|
|
1236
|
+
activeSession = await this.sessionManager.getActiveSession(channel, msg.channelId);
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
activeSession = undefined;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
const menuAuthSubject = {
|
|
1243
|
+
...authSubject,
|
|
1244
|
+
requestId: parsed.id,
|
|
1245
|
+
...(activeSession?.id ? { sessionId: activeSession.id } : {}),
|
|
1246
|
+
};
|
|
1203
1247
|
const isTokenRequest = parsed.type === 'menu.token.request';
|
|
1204
1248
|
if (isTokenRequest) {
|
|
1205
1249
|
if (msg.chatType !== 'private') {
|
|
@@ -1218,7 +1262,7 @@ export class MessageBridge {
|
|
|
1218
1262
|
}
|
|
1219
1263
|
}
|
|
1220
1264
|
if (!isTokenRequest) {
|
|
1221
|
-
const access = authorizeAccess(
|
|
1265
|
+
const access = authorizeAccess(menuAuthSubject);
|
|
1222
1266
|
if (!access.allow) {
|
|
1223
1267
|
this.logMenuDiagnostic('access-denied', channel, msg, parsed, access.code);
|
|
1224
1268
|
await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
|
|
@@ -1233,7 +1277,7 @@ export class MessageBridge {
|
|
|
1233
1277
|
let afterResponse;
|
|
1234
1278
|
const deduped = await this.menuDeduper.execute(dedupKey, menuPayloadFingerprint(parsed.raw), () => isTokenRequest
|
|
1235
1279
|
? this.issueMenuTokenResponse(header, channel, msg)
|
|
1236
|
-
: this.dispatchMenuRequest(parsed.request, header, channel, msg,
|
|
1280
|
+
: this.dispatchMenuRequest(parsed.request, header, channel, msg, menuAuthSubject, effect => { afterResponse = effect; }));
|
|
1237
1281
|
if ('conflict' in deduped) {
|
|
1238
1282
|
this.logMenuDiagnostic('request-conflict', channel, msg, parsed, 'CONFLICT');
|
|
1239
1283
|
await this.sendMenuResponse(adapter, channel, msg, menuFailure(header, {
|
|
@@ -1244,6 +1288,8 @@ export class MessageBridge {
|
|
|
1244
1288
|
}
|
|
1245
1289
|
if (deduped.replayed)
|
|
1246
1290
|
this.logMenuDiagnostic('request-replay', channel, msg, parsed);
|
|
1291
|
+
if (deduped.retry)
|
|
1292
|
+
this.logMenuDiagnostic('request-retry', channel, msg, parsed);
|
|
1247
1293
|
const response = responseWarning && 'data' in deduped.value
|
|
1248
1294
|
? { ...deduped.value, warning: responseWarning }
|
|
1249
1295
|
: deduped.value;
|
|
@@ -1380,6 +1426,10 @@ export class MessageBridge {
|
|
|
1380
1426
|
...evolMenuResponseTransportMetadata(),
|
|
1381
1427
|
},
|
|
1382
1428
|
};
|
|
1429
|
+
if (msg.channelType === 'aun' && !replyContext.delivery) {
|
|
1430
|
+
logger.warn(`[MessageBridge] dropping AUN menu response without trusted delivery route: channel=${channel} channelId=${msg.channelId}`);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1383
1433
|
const envelope = buildEnvelope({
|
|
1384
1434
|
taskId: `menu-${randomBytes(4).toString('hex')}`,
|
|
1385
1435
|
channel,
|
|
@@ -1438,6 +1488,8 @@ export class MessageBridge {
|
|
|
1438
1488
|
logger.warn(`[MenuControl] category=${category} request=${parsed.id || '<missing>'}`
|
|
1439
1489
|
+ ` type=${parsed.type} name=${parsed.name || '<none>'} action=${parsed.action || '<none>'}`
|
|
1440
1490
|
+ ` transport=${channel} scope=${this.shortHash(scope)} sender=${this.shortHash(msg.peerId)}`
|
|
1491
|
+
+ (category === 'request-replay' ? ' replay=true' : '')
|
|
1492
|
+
+ (category === 'request-retry' ? ' retry=true' : '')
|
|
1441
1493
|
+ `${code ? ` code=${code}` : ''}${decision.suppressed ? ` suppressed=${decision.suppressed}` : ''}`);
|
|
1442
1494
|
}
|
|
1443
1495
|
shortHash(value) {
|
|
@@ -1556,17 +1608,17 @@ export class MessageBridge {
|
|
|
1556
1608
|
* 撤回消息:先查 debounce 窗口,再查 message queue,最后查正在执行的任务。
|
|
1557
1609
|
* @returns true 如果找到并取消/中断
|
|
1558
1610
|
*/
|
|
1559
|
-
cancel(messageId) {
|
|
1611
|
+
cancel(messageId, selfAID) {
|
|
1560
1612
|
// 阶段 1: debounce 窗口(尚未入队)
|
|
1561
1613
|
for (const d of this.debouncers.values()) {
|
|
1562
|
-
if (d.cancel(messageId))
|
|
1614
|
+
if (d.cancel(messageId, selfAID))
|
|
1563
1615
|
return true;
|
|
1564
1616
|
}
|
|
1565
1617
|
// 阶段 2: 已入队但未处理(合并后 messageId 可能是逗号分隔的多个 id)
|
|
1566
|
-
if (this.messageQueue.cancel(messageId))
|
|
1618
|
+
if (this.messageQueue.cancel(messageId, selfAID))
|
|
1567
1619
|
return true;
|
|
1568
1620
|
// 阶段 3: 正在执行的任务 → 触发 interrupt
|
|
1569
|
-
return this.messageQueue.cancelActive(messageId);
|
|
1621
|
+
return this.messageQueue.cancelActive(messageId, selfAID);
|
|
1570
1622
|
}
|
|
1571
1623
|
/** 清理资源 */
|
|
1572
1624
|
dispose() {
|
|
@@ -289,6 +289,8 @@ export function buildInboundEntry(opts) {
|
|
|
289
289
|
const isCommand = isCommandCapable && opts.content.startsWith('/');
|
|
290
290
|
return {
|
|
291
291
|
ts,
|
|
292
|
+
...(opts.receivedAt !== undefined ? { receivedAt: opts.receivedAt } : {}),
|
|
293
|
+
...(opts.gatewaySeq !== undefined ? { gatewaySeq: opts.gatewaySeq } : {}),
|
|
292
294
|
time: formatTimestampMs(ts),
|
|
293
295
|
sessionId: opts.sessionId,
|
|
294
296
|
threadId: opts.threadId ?? null,
|