evolcore 0.0.2 → 0.0.3

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.
Files changed (124) hide show
  1. package/CHANGELOG.md +44 -793
  2. package/dist/agents/claude-runner.js +197 -17
  3. package/dist/agents/codex-runner.js +46 -3
  4. package/dist/aun/outbox.js +8 -0
  5. package/dist/channels/aun.js +21 -4
  6. package/dist/channels/contact-bind-code.js +134 -0
  7. package/dist/channels/dingtalk.js +979 -149
  8. package/dist/channels/feishu.js +130 -54
  9. package/dist/channels/wecom-card.js +101 -0
  10. package/dist/channels/wecom-onboarding.js +82 -0
  11. package/dist/channels/wecom-state.js +191 -0
  12. package/dist/channels/wecom.js +755 -163
  13. package/dist/cli/agent-command.js +2 -1
  14. package/dist/cli/aun-commands.js +88 -33
  15. package/dist/cli/bench.js +2 -2
  16. package/dist/cli/contact.js +71 -0
  17. package/dist/cli/ctl-command.js +2 -2
  18. package/dist/cli/daemon-commands.js +77 -214
  19. package/dist/cli/handoff-command.js +2 -2
  20. package/dist/cli/help.js +9 -5
  21. package/dist/cli/index.js +132 -116
  22. package/dist/cli/init-channel.js +92 -97
  23. package/dist/cli/init.js +63 -26
  24. package/dist/cli/model.js +2 -1
  25. package/dist/cli/net-check.js +2 -2
  26. package/dist/cli/queue-command.js +30 -6
  27. package/dist/cli/raw-key-input.js +25 -0
  28. package/dist/cli/response.js +5 -6
  29. package/dist/cli/restart-monitor.js +25 -1
  30. package/dist/cli/stats.js +6 -4
  31. package/dist/cli/trigger-command.js +55 -15
  32. package/dist/cli/version.js +6 -1
  33. package/dist/config/builtin-role-templates.js +22 -10
  34. package/dist/config/builtin-roles.js +7 -1
  35. package/dist/config/config-manager.js +221 -17
  36. package/dist/config/contact-alias.js +68 -0
  37. package/dist/config/contact-book-store.js +454 -0
  38. package/dist/config/contact-book-v2-startup.js +35 -0
  39. package/dist/config/contact-book.js +156 -303
  40. package/dist/config/contact-operation-service.js +110 -0
  41. package/dist/config/peer-role-resolver.js +133 -54
  42. package/dist/config/role-ranks.js +18 -0
  43. package/dist/config/role-service.js +16 -19
  44. package/dist/config/role-store.js +16 -5
  45. package/dist/config/roles.js +10 -1
  46. package/dist/config-store.js +0 -2
  47. package/dist/core/auth/authorization-audit.js +10 -1
  48. package/dist/core/auth/operation-authorizer.js +2 -0
  49. package/dist/core/auth/operation-catalog.js +56 -0
  50. package/dist/core/command/command-handler.js +105 -10
  51. package/dist/core/command/connect-menu.js +374 -0
  52. package/dist/core/command/menu-handler.js +114 -16
  53. package/dist/core/command/role-menu.js +128 -29
  54. package/dist/core/command/slash-handler.js +28 -18
  55. package/dist/core/daemon-file-cache.js +12 -6
  56. package/dist/core/event-catalog.js +70 -0
  57. package/dist/core/evolagent-registry.js +0 -1
  58. package/dist/core/evolagent.js +20 -9
  59. package/dist/core/message/im-renderer.js +2 -0
  60. package/dist/core/message/message-bridge.js +79 -8
  61. package/dist/core/message/message-queue.js +10 -0
  62. package/dist/core/message/message-utils.js +8 -2
  63. package/dist/core/message/response-engine.js +269 -25
  64. package/dist/core/message/send-receipt.js +24 -0
  65. package/dist/core/message/stream-debouncer.js +11 -2
  66. package/dist/core/permission/tool-policy.js +47 -15
  67. package/dist/core/protected-paths.js +2 -0
  68. package/dist/core/session/session-fs-store.js +44 -3
  69. package/dist/core/session/session-manager.js +61 -5
  70. package/dist/index.js +62 -6
  71. package/dist/ipc.js +34 -5
  72. package/dist/stats/billing.js +20 -8
  73. package/dist/trigger/manager.js +3 -0
  74. package/dist/trigger/parser.js +77 -2
  75. package/dist/trigger/patch.js +8 -1
  76. package/dist/trigger/scheduler.js +3 -1
  77. package/dist/trigger/validation.js +13 -0
  78. package/dist/utils/aid-bind.js +43 -29
  79. package/dist/utils/instance-registry.js +14 -7
  80. package/dist/utils/log-writer.js +46 -0
  81. package/dist/utils/media-cache.js +4 -1
  82. package/dist/utils/model-prices.jsonl +6 -3
  83. package/dist/utils/restart-safety.js +31 -0
  84. package/dist/utils/system-memory.js +62 -0
  85. package/kits/docs/INDEX.md +2 -1
  86. package/kits/docs/evolcore/INDEX.md +5 -3
  87. package/kits/docs/evolcore/agent.md +9 -1
  88. package/kits/docs/evolcore/aid.md +5 -2
  89. package/kits/docs/evolcore/contact.md +57 -0
  90. package/kits/docs/evolcore/fs.md +9 -0
  91. package/kits/docs/evolcore/group.md +12 -3
  92. package/kits/docs/evolcore/model.md +4 -1
  93. package/kits/docs/evolcore/msg.md +9 -3
  94. package/kits/docs/evolcore/response.md +16 -21
  95. package/kits/docs/evolcore/rpc.md +2 -0
  96. package/kits/docs/evolcore/stats.md +15 -2
  97. package/kits/docs/evolcore/storage.md +1 -0
  98. package/kits/docs/evolcore/trigger.md +17 -2
  99. package/kits/eck_manifest.json +12 -0
  100. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  101. package/kits/schemas/_meta.json +7 -4
  102. package/kits/schemas/agent-config.schema.6.json +322 -0
  103. package/kits/schemas/contact-book.schema.2.json +43 -0
  104. package/kits/schemas/relation-config.schema.5.json +47 -0
  105. package/kits/schemas/role-config.schema.1.json +1 -0
  106. package/kits/schemas/role-registry.schema.1.json +2 -2
  107. package/kits/templates/roles/admin.json +1 -0
  108. package/kits/templates/roles/member.json +1 -0
  109. package/kits/templates/roles/owner.json +1 -0
  110. package/kits/templates/roles/visitor.json +1 -0
  111. package/kits/templates/system-fragments/commands.md +3 -1
  112. package/package.json +4 -4
  113. package/assets/brand/evolcore/README.md +0 -19
  114. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  115. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  116. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  117. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  118. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  119. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  120. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  121. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  122. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  123. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  124. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -12,7 +12,7 @@
12
12
  * 策略(reload/重启永远全量失效,无视策略;策略只决定"平时每次读怎么检查"):
13
13
  * - on-reload:平时不检查,直接用缓存(kits 文件、persona)。靠 reload/重启刷新。
14
14
  * - manual:同 on-reload,额外支持显式 invalidate(file) 单刷。
15
- * - mtime:每次读 statSync 门控 mtime,变了自动重读(working.md、config.json)。
15
+ * - mtime:每次读 statSync 门控 size + mtime,变了自动重读(working.md、config.json)。
16
16
  *
17
17
  * 容量:项数可无界增长的组(如 relation-prefs,每 peer 一文件)设 LRU 硬上限
18
18
  * (见 GROUP_CAPS),命中/写入移到末尾、超限驱逐同组最旧项;无 timer。
@@ -71,9 +71,9 @@ export class FileCache {
71
71
  const read = opts.read ?? readFileOrNull;
72
72
  this.bump(opts.group, opts.policy, 'gets');
73
73
  if (opts.policy === 'mtime') {
74
- const mtimeMs = statMtime(file);
74
+ const fingerprint = statFingerprint(file);
75
75
  this.bump(opts.group, opts.policy, 'statChecks');
76
- if (existing && existing.mtimeMs === mtimeMs) {
76
+ if (existing && sameFingerprint(existing.fingerprint, fingerprint)) {
77
77
  this.bump(opts.group, opts.policy, 'hits');
78
78
  this.touch(file, existing);
79
79
  return existing.value;
@@ -84,7 +84,7 @@ export class FileCache {
84
84
  this.bump(opts.group, opts.policy, 'misses');
85
85
  const raw = read(file);
86
86
  const value = loader(raw);
87
- this.store(file, { policy: 'mtime', group: opts.group, mtimeMs, value, bytes: approxBytes(raw) });
87
+ this.store(file, { policy: 'mtime', group: opts.group, fingerprint, value, bytes: approxBytes(raw) });
88
88
  return value;
89
89
  }
90
90
  // on-reload / manual:命中即用,不检查文件
@@ -192,14 +192,20 @@ export class FileCache {
192
192
  };
193
193
  }
194
194
  }
195
- function statMtime(file) {
195
+ function statFingerprint(file) {
196
196
  try {
197
- return fs.statSync(file).mtimeMs;
197
+ const stat = fs.statSync(file);
198
+ return { size: stat.size, mtimeMs: stat.mtimeMs };
198
199
  }
199
200
  catch {
200
201
  return null; // 文件不存在
201
202
  }
202
203
  }
204
+ function sameFingerprint(left, right) {
205
+ if (left === null || right === null)
206
+ return left === right;
207
+ return !!left && left.size === right.size && left.mtimeMs === right.mtimeMs;
208
+ }
203
209
  function readFileOrNull(file) {
204
210
  try {
205
211
  return fs.readFileSync(file, 'utf-8');
@@ -355,6 +355,76 @@ const CATALOG = [
355
355
  { path: 'preTokens', type: 'number' },
356
356
  ],
357
357
  },
358
+ {
359
+ type: 'runner:task-started',
360
+ namespace: 'runner',
361
+ name: 'task-started',
362
+ description: 'Runner 子任务已创建',
363
+ fields: [
364
+ { path: 'sessionId', type: 'string' },
365
+ { path: 'taskId', type: 'string' },
366
+ { path: 'taskKind', type: 'string' },
367
+ { path: 'toolUseId', type: 'string', optional: true },
368
+ { path: 'description', type: 'string' },
369
+ { path: 'subagentType', type: 'string', optional: true },
370
+ { path: 'taskType', type: 'string', optional: true },
371
+ { path: 'workflowName', type: 'string', optional: true },
372
+ { path: 'prompt', type: 'string', optional: true },
373
+ { path: 'skipTranscript', type: 'boolean', optional: true },
374
+ { path: 'timestamp', type: 'number', optional: true },
375
+ ],
376
+ },
377
+ {
378
+ type: 'runner:task-progress',
379
+ namespace: 'runner',
380
+ name: 'task-progress',
381
+ description: 'Runner 子任务进度',
382
+ fields: [
383
+ { path: 'sessionId', type: 'string' },
384
+ { path: 'taskId', type: 'string', optional: true },
385
+ { path: 'taskKind', type: 'string', optional: true },
386
+ { path: 'toolUseId', type: 'string', optional: true },
387
+ { path: 'description', type: 'string', optional: true },
388
+ { path: 'subagentType', type: 'string', optional: true },
389
+ { path: 'summary', type: 'string', optional: true },
390
+ { path: 'lastToolName', type: 'string', optional: true },
391
+ { path: 'totalTokens', type: 'number', optional: true },
392
+ { path: 'toolUses', type: 'number', optional: true },
393
+ { path: 'durationMs', type: 'number', optional: true },
394
+ { path: 'timestamp', type: 'number', optional: true },
395
+ ],
396
+ },
397
+ {
398
+ type: 'runner:task-notification',
399
+ namespace: 'runner',
400
+ name: 'task-notification',
401
+ description: 'Runner 子任务终态和输出',
402
+ fields: [
403
+ { path: 'sessionId', type: 'string' },
404
+ { path: 'taskId', type: 'string' },
405
+ { path: 'taskKind', type: 'string' },
406
+ { path: 'toolUseId', type: 'string', optional: true },
407
+ { path: 'description', type: 'string', optional: true },
408
+ { path: 'subagentType', type: 'string', optional: true },
409
+ { path: 'status', type: 'string' },
410
+ { path: 'outputFile', type: 'string', optional: true },
411
+ { path: 'summary', type: 'string', optional: true },
412
+ { path: 'usage', type: 'object', optional: true },
413
+ { path: 'skipTranscript', type: 'boolean', optional: true },
414
+ { path: 'timestamp', type: 'number', optional: true },
415
+ ],
416
+ },
417
+ {
418
+ type: 'runner:background-tasks-changed',
419
+ namespace: 'runner',
420
+ name: 'background-tasks-changed',
421
+ description: 'Runner 后台任务集合变化',
422
+ fields: [
423
+ { path: 'sessionId', type: 'string' },
424
+ { path: 'tasks', type: 'array' },
425
+ { path: 'timestamp', type: 'number', optional: true },
426
+ ],
427
+ },
358
428
  {
359
429
  type: 'runner:model-changed',
360
430
  namespace: 'runner',
@@ -129,7 +129,6 @@ export class EvolAgentRegistry {
129
129
  aid: raw.aid,
130
130
  enabled: raw.enabled,
131
131
  owners: raw.owners,
132
- admins: raw.admins,
133
132
  channels: Array.isArray(raw.channels) ? raw.channels : [],
134
133
  active_baseagent: raw.active_baseagent,
135
134
  baseagents: raw.baseagents,
@@ -1,9 +1,11 @@
1
1
  import path from 'path';
2
- import { formatChannelKey } from './channel-loader.js';
2
+ import { formatChannelKey, tryParseChannelKey } from './channel-loader.js';
3
3
  import { agentPersonalDir } from '../paths.js';
4
4
  import { fileCache } from './daemon-file-cache.js';
5
5
  import { ConfigTarget, read as cfgRead, write as cfgWrite, ensureFile as cfgEnsure, resolveEffective } from '../config/config-manager.js';
6
6
  import { withLifecycleForWrite } from '../config/lifecycle.js';
7
+ import { isStaticAgentAdmin, isStaticAgentOwner } from '../config/peer-role-resolver.js';
8
+ import { resolvePrincipal } from '../config/contact-book.js';
7
9
  /**
8
10
  * EvolAgent —— 一个 self-agent 的运行时表示。
9
11
  *
@@ -123,19 +125,28 @@ export class EvolAgent {
123
125
  /**
124
126
  * Check if a user has owner role for this agent (private scope).
125
127
  */
126
- isOwner(_channelKey, userId) {
127
- if (this.merged.owners?.includes(userId))
128
- return true;
129
- return false;
128
+ isOwner(channelKey, userId) {
129
+ const principalId = this.managementPrincipal(channelKey, userId);
130
+ return !!principalId && isStaticAgentOwner(this.aid, principalId);
130
131
  }
131
132
  /**
132
133
  * Check if a user has admin role for this agent (private scope).
133
- * Uses static owners/admins from agent config.
134
+ * Owner remains in Agent config; admin is resolved from the direct AUN relation.
134
135
  */
135
- isAdmin(_channelKey, userId) {
136
- if (this.isOwner(_channelKey, userId))
136
+ isAdmin(channelKey, userId) {
137
+ const principalId = this.managementPrincipal(channelKey, userId);
138
+ if (!principalId)
139
+ return false;
140
+ if (isStaticAgentOwner(this.aid, principalId))
137
141
  return true;
138
- return this.merged.admins?.includes(userId) ?? false;
142
+ return isStaticAgentAdmin(this.aid, principalId);
143
+ }
144
+ managementPrincipal(channelKey, userId) {
145
+ const channelType = tryParseChannelKey(channelKey)?.type ?? String(channelKey || '').split('#')[0];
146
+ const resolution = resolvePrincipal(this.aid, channelType, userId, channelKey);
147
+ return resolution.principalStatus === 'native_aid' || resolution.principalStatus === 'mapped'
148
+ ? resolution.principalId ?? null
149
+ : null;
139
150
  }
140
151
  /**
141
152
  * Get the first owner of this agent (private scope).
@@ -589,6 +589,8 @@ export class IMRenderer {
589
589
  return { kind: 'notice', text, severity: 'warn' };
590
590
  }
591
591
  case 'complete': {
592
+ if (event.queryFinal === false)
593
+ return null;
592
594
  // 上下文过长错误不输出(留给外层 auto-compact 处理)
593
595
  const hasContextError = event.terminalReason === 'prompt_too_long'
594
596
  || isContextTooLongText(event.errors?.join(' ') || '')
@@ -5,14 +5,58 @@ import { appendMessageLog, appendMessageLogStrict, buildInboundEntry, isTransien
5
5
  import { buildEnvelope } from './message-utils.js';
6
6
  import { chatDirPath } from '../session/session-fs-store.js';
7
7
  import { tryParseChannelKey } from '../channel-loader.js';
8
- import { resolvePaths } from '../../paths.js';
8
+ import { agentDir, resolvePaths } from '../../paths.js';
9
9
  import { resolvePeerRoleDetail } from '../../config/peer-role-resolver.js';
10
+ import { isBlockedContact, resolveContactView } from '../../config/contact-book.js';
11
+ import { isValidAid } from '../../aun/aid/validation.js';
12
+ import { PeerIdentityCache } from '../relation/peer-identity.js';
10
13
  import { handlePendingDingtalkContactBindMessage } from '../../channels/dingtalk.js';
14
+ import { handlePendingWecomContactBindMessage } from '../../channels/wecom.js';
11
15
  import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
12
16
  import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, } from '../command/menu-protocol.js';
13
17
  import { SessionRenewService } from '../session/session-renew.js';
14
18
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
15
19
  import { recordCausationSpan } from '../causation/audit.js';
20
+ const CONTACT_REJECTION_NOTICE = '[rejection] Not accepting messages from you.';
21
+ const REJECTABLE_PEER_TYPES = new Set(['ai', 'bot', 'agent', 'service']);
22
+ const REJECTION_MAX = 3;
23
+ const REJECTION_WINDOW_MS = 60 * 60 * 1000;
24
+ const rejectionState = new Map();
25
+ export function shouldBlockInboundContact(input) {
26
+ if (String(input.channelType || '').trim().toLowerCase() !== 'aun' || !input.selfAid) {
27
+ return { blocked: false, status: 'unsupported-channel' };
28
+ }
29
+ if (input.chatType !== 'private')
30
+ return { blocked: false, status: 'unsupported-chat' };
31
+ const primaryId = String(input.actorId || '').trim();
32
+ if (!isValidAid(primaryId))
33
+ return { blocked: false, status: 'invalid-aid' };
34
+ if (input.chatType === 'private' && resolveContactView(input.selfAid, primaryId).isOwner) {
35
+ return { blocked: false, status: 'owner-exempt', primaryId };
36
+ }
37
+ return {
38
+ blocked: isBlockedContact(input.selfAid, primaryId),
39
+ status: 'resolved',
40
+ primaryId,
41
+ };
42
+ }
43
+ export function shouldSendContactRejection(peerType, selfAid, primaryId, now = Date.now()) {
44
+ if (!REJECTABLE_PEER_TYPES.has(String(peerType || '').trim().toLowerCase()))
45
+ return false;
46
+ const key = `${selfAid}\0${primaryId}`;
47
+ const state = rejectionState.get(key);
48
+ if (!state || now - state.windowStart >= REJECTION_WINDOW_MS) {
49
+ rejectionState.set(key, { count: 1, windowStart: now });
50
+ return true;
51
+ }
52
+ if (state.count >= REJECTION_MAX)
53
+ return false;
54
+ state.count += 1;
55
+ return true;
56
+ }
57
+ export function clearContactRejectionRateLimits() {
58
+ rejectionState.clear();
59
+ }
16
60
  /**
17
61
  * MessageBridge — Channel 与 Core 之间的消息桥梁
18
62
  *
@@ -119,6 +163,25 @@ export class MessageBridge {
119
163
  const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
120
164
  const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
121
165
  const resolvedChannelType = msg.channelType || parsedChannelKey?.type || effectiveChannelType;
166
+ const blockDecision = shouldBlockInboundContact({
167
+ selfAid,
168
+ channelType: resolvedChannelType,
169
+ chatType,
170
+ actorId,
171
+ conversationId,
172
+ });
173
+ if (blockDecision.blocked && selfAid && blockDecision.primaryId) {
174
+ let peerType = msg.peerType;
175
+ if (chatType === 'private' && !peerType) {
176
+ peerType = PeerIdentityCache.get('aun', blockDecision.primaryId, agentDir(selfAid))?.type;
177
+ }
178
+ if (chatType === 'private'
179
+ && shouldSendContactRejection(peerType, selfAid, blockDecision.primaryId)) {
180
+ await sendReply(msg.channelId, CONTACT_REJECTION_NOTICE, msg.replyContext);
181
+ }
182
+ logger.info(`[MessageBridge] Blocked AUN inbound before business routing: self=${selfAid} peer=${blockDecision.primaryId} type=${peerType ?? 'unknown'} chatType=${chatType}`);
183
+ return;
184
+ }
122
185
  const menuControl = resolvedChannelType === 'aun'
123
186
  ? parseMenuControl(content)
124
187
  : { isMenu: false };
@@ -136,16 +199,21 @@ export class MessageBridge {
136
199
  }
137
200
  }
138
201
  if (!menuControl.isMenu) {
139
- const contactBind = handlePendingDingtalkContactBindMessage({
202
+ const bindContext = {
140
203
  selfAid,
141
204
  channelName: channelKey,
142
205
  channelType: resolvedChannelType,
143
206
  chatType,
144
207
  actorId,
145
208
  content,
146
- });
209
+ };
210
+ const contactBind = resolvedChannelType === 'dingtalk'
211
+ ? handlePendingDingtalkContactBindMessage(bindContext)
212
+ : resolvedChannelType === 'wecom'
213
+ ? handlePendingWecomContactBindMessage(bindContext)
214
+ : { handled: false };
147
215
  if (contactBind.handled) {
148
- logger.info(`[MessageBridge] DingTalk contact bind handled: channel=${channelKey} actor=${actorId ?? '<none>'} status=${contactBind.status}`);
216
+ logger.info(`[MessageBridge] ${resolvedChannelType} contact bind handled: channel=${channelKey} actor=${actorId ?? '<none>'} status=${contactBind.status}`);
149
217
  if (contactBind.reply) {
150
218
  await sendReply(msg.channelId, contactBind.reply, msg.replyContext);
151
219
  }
@@ -236,7 +304,7 @@ export class MessageBridge {
236
304
  logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
237
305
  // 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
238
306
  try {
239
- const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfAID || '');
307
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfAID || '', channelKey);
240
308
  const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
241
309
  const inboundChatmode = msg.replyContext?.metadata?.chatmode;
242
310
  const inboundReplyTo = typeof msg.replyContext?.metadata?.refMessageId === 'string'
@@ -429,8 +497,11 @@ export class MessageBridge {
429
497
  // ACK 在到达时立即做(每条独立 ACK),不等合并
430
498
  // Interrupt 模式(单聊)→ 入队前 debounce 合并
431
499
  // FIFO 模式(群聊) → 跳过 debouncer,独立入队,出队时贪心合并
432
- // 优化:队列空闲时跳过 Pin 表情,直接在开始执行时添加 CheckMark
433
- if (fullMessage.messageId && this.messageQueue.getGlobalProcessingCount() > 0) {
500
+ // 优化:当前会话队列空闲时跳过 Pin,直接在开始执行时添加 CheckMark
501
+ // 不能使用全局 processing 数,否则其他会话正在执行也会让本会话误显示“已排队”。
502
+ const currentSessionBusy = (this.messageQueue.isProcessing?.(session.id) ?? false)
503
+ || (this.messageQueue.getQueueLength?.(session.id) ?? 0) > 0;
504
+ if (fullMessage.messageId && currentSessionBusy) {
434
505
  adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
435
506
  }
436
507
  const isInterrupt = chatType !== 'group' && !handoffCandidate;
@@ -726,7 +797,7 @@ export class MessageBridge {
726
797
  if (!this.cmdHandler.isCommand(content))
727
798
  return false;
728
799
  logger.info(`[${channel}] ${channelId}: ${content}${source === 'card-trigger' ? ' [card]' : ''}`);
729
- const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject);
800
+ const cmdResult = await this.cmdHandler.handle(content, channel, channelId, (_cid, text, opts) => sendReply(text), userId, threadId, chatType, source, messageId, selfAID, authSubject?.identity, authSubject, replyContext);
730
801
  logger.debug(`[MessageBridge] handleCommand: result type=${typeof cmdResult}`);
731
802
  if (cmdResult === undefined)
732
803
  return false;
@@ -856,6 +856,7 @@ export class MessageQueue {
856
856
  * - items: 保留每条子消息(含各自 peer/timestamp),供消息渲染层逐条渲染
857
857
  * - images / mentions: 扁平合并
858
858
  * - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
859
+ * - sourceMessageIds: 保留全部原始入站消息 ID(用于渠道确认状态批量迁移)
859
860
  * - replyContext / peerName / 其余字段: 取最后一条
860
861
  */
861
862
  mergeItems(items) {
@@ -864,6 +865,8 @@ export class MessageQueue {
864
865
  const allMentions = [];
865
866
  const subMessages = [];
866
867
  let mergedPeerType;
868
+ const sourceMessageIds = [];
869
+ const seenSourceMessageIds = new Set();
867
870
  let hasRestartResume = false;
868
871
  let hasMessagesAfterRestartResume = false;
869
872
  for (const item of items) {
@@ -879,6 +882,12 @@ export class MessageQueue {
879
882
  allImages.push(...m.images);
880
883
  if (m.mentions)
881
884
  allMentions.push(...m.mentions);
885
+ for (const id of [...(m.sourceMessageIds ?? []), ...(m.messageId ? [m.messageId] : [])]) {
886
+ if (!seenSourceMessageIds.has(id)) {
887
+ seenSourceMessageIds.add(id);
888
+ sourceMessageIds.push(id);
889
+ }
890
+ }
882
891
  if (m.peerType && m.peerType !== 'human') {
883
892
  mergedPeerType = m.peerType;
884
893
  }
@@ -919,6 +928,7 @@ export class MessageQueue {
919
928
  images: allImages.length > 0 ? allImages : undefined,
920
929
  mentions: allMentions.length > 0 ? allMentions : undefined,
921
930
  messageId: latestMessageId,
931
+ sourceMessageIds: sourceMessageIds.length > 0 ? sourceMessageIds : undefined,
922
932
  // 密文优先:合并批次内任一条密文,则整轮(interactive 自动回复的单值出站)按密文。
923
933
  // 任一条 encrypted===true 即 true;否则若有明确的 false 则 false;全 undefined(非 aun)则 undefined。
924
934
  encrypted: subMessages.some(s => s.encrypted === true)
@@ -8,8 +8,10 @@ import { renderCommandCardAsText, renderActionAsText } from '../interaction-rout
8
8
  * 构造出站消息信封
9
9
  */
10
10
  export function buildEnvelope(opts) {
11
+ const taskId = opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
11
12
  return {
12
- taskId: opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
13
+ taskId,
14
+ operationId: opts.operationId ?? taskId,
13
15
  sessionId: opts.sessionId,
14
16
  channel: opts.channel,
15
17
  channelId: opts.channelId,
@@ -61,7 +63,11 @@ export async function sendInteractionPayload(adapter, envelope, interaction, fal
61
63
  const enriched = replyCtx
62
64
  ? { ...envelope, replyContext: replyCtx }
63
65
  : envelope;
64
- await adapter.send(enriched, payload);
66
+ const receipt = await adapter.send(enriched, payload);
67
+ if (receipt?.status === 'suppressed')
68
+ return false;
69
+ if (receipt?.status === 'sent')
70
+ return receipt.messages[0]?.messageId || false;
65
71
  return 'sent';
66
72
  }
67
73
  catch {