evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -7,10 +7,11 @@ import { logger, localTimestamp } from '../utils/logger.js';
7
7
  import { LogWriter } from '../utils/log-writer.js';
8
8
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
9
9
  import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
10
- import { resolvePaths, getPackageRoot, agentMdPath as agentMdPathFn, agentDir as agentDirPath, resolveRoot } from '../paths.js';
10
+ import { resolvePaths, agentMdPath as agentMdPathFn, agentDir as agentDirPath, resolveRoot } from '../paths.js';
11
11
  import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
12
12
  import { appendAidEvent } from '../utils/instance-registry.js';
13
13
  import { appendMessageLog, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog } from '../core/message/message-log.js';
14
+ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
14
15
  import { chatDirPath } from '../core/session/session-fs-store.js';
15
16
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
16
17
  import { appendAidLifecycle } from '../aun/aid/identity.js';
@@ -31,6 +32,8 @@ import { consumeAunCausation, registerAunCausation } from '../core/causation/aun
31
32
  import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
32
33
  import { recordCausationSpan } from '../core/causation/audit.js';
33
34
  import { isExplicitGroupId } from '../aun/group-identity.js';
35
+ import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
36
+ import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
34
37
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
35
38
  /**
36
39
  * 构造 connect extra_info:自描述本进程身份。
@@ -45,7 +48,7 @@ function buildConnectExtraInfo(opts) {
45
48
  const startedAt = getProcessStartTime(process.pid) ?? Date.now();
46
49
  return {
47
50
  app: 'evolcore',
48
- version: getEvolcoreVersion(),
51
+ version: readInstalledEvolcoreVersion(),
49
52
  pid: process.pid,
50
53
  started_at: startedAt,
51
54
  started_at_iso: new Date(startedAt).toISOString(),
@@ -59,23 +62,14 @@ function buildConnectExtraInfo(opts) {
59
62
  channel_name: opts.channelName ?? '',
60
63
  };
61
64
  }
62
- let _cachedVersion = null;
63
- function getEvolcoreVersion() {
64
- if (_cachedVersion !== null)
65
- return _cachedVersion;
66
- try {
67
- const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
68
- _cachedVersion = String(pkg.version ?? '');
69
- }
70
- catch {
71
- _cachedVersion = '';
72
- }
73
- return _cachedVersion;
74
- }
75
65
  export function buildAunFilePayload(params) {
76
66
  const correlationId = params.context?.metadata?.correlationId;
77
67
  const payload = {
78
- type: correlationId ? 'result.file' : 'file',
68
+ // `result.file` is EvolCore's internal outbound kind. On the AUN wire this
69
+ // must remain the standard `file` payload type so clients route it through
70
+ // their attachment/download pipeline; correlation_id carries the control
71
+ // request association without changing the media type.
72
+ type: 'file',
79
73
  text: `📎 ${params.filename} (${formatSize(params.size)})`,
80
74
  attachments: [params.attachment],
81
75
  };
@@ -121,6 +115,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
121
115
  sameNetwork: opts.sameNetwork,
122
116
  sameEgressIp: opts.sameEgressIp,
123
117
  encrypted: opts.encrypted,
118
+ protectedHeaders: opts.protectedHeaders,
124
119
  messageId: opts.messageId,
125
120
  causation: opts.causation,
126
121
  mentions: opts.mentions,
@@ -133,6 +128,54 @@ export function aunOptsToInbound(opts, channel, channelType) {
133
128
  dispatchMode: opts.dispatchMode,
134
129
  };
135
130
  }
131
+ function verifiedProtectedHeaders(msg, env) {
132
+ const candidates = [
133
+ msg.e2ee?.protected_headers,
134
+ env.protected_headers,
135
+ msg.protected_headers,
136
+ ];
137
+ for (const candidate of candidates) {
138
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
139
+ continue;
140
+ const headers = {};
141
+ for (const [key, value] of Object.entries(candidate)) {
142
+ const name = key.trim().toLowerCase();
143
+ if (!/^[a-z0-9_-]+$/.test(name) || typeof value !== 'string')
144
+ continue;
145
+ headers[name] = value;
146
+ }
147
+ return Object.keys(headers).length > 0 ? headers : undefined;
148
+ }
149
+ return undefined;
150
+ }
151
+ function redactMenuTokensForTrace(value, seen = new WeakSet()) {
152
+ if (!value || typeof value !== 'object')
153
+ return value;
154
+ if (seen.has(value))
155
+ return '[Circular]';
156
+ seen.add(value);
157
+ if (Array.isArray(value))
158
+ return value.map(item => redactMenuTokensForTrace(item, seen));
159
+ const redacted = {};
160
+ for (const [key, item] of Object.entries(value)) {
161
+ redacted[key] = key.toLowerCase() === 'menu_token'
162
+ ? '[REDACTED]'
163
+ : redactMenuTokensForTrace(item, seen);
164
+ }
165
+ return redacted;
166
+ }
167
+ function payloadTypeForObserver(value) {
168
+ if (!value || typeof value !== 'object' || Array.isArray(value))
169
+ return undefined;
170
+ const record = value;
171
+ const payload = record.payload
172
+ ?? (record.message && typeof record.message === 'object' ? record.message.payload : undefined)
173
+ ?? (record.envelope && typeof record.envelope === 'object' ? record.envelope.payload : undefined);
174
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
175
+ return undefined;
176
+ const type = payload.type;
177
+ return typeof type === 'string' ? type : undefined;
178
+ }
136
179
  export class AUNChannel {
137
180
  config;
138
181
  client = null;
@@ -157,7 +200,8 @@ export class AUNChannel {
157
200
  return;
158
201
  // 自动从 data 推断顶层字段(self_aid / peer_aid / group_id / task_id / chatmode),
159
202
  // 便于 jq 过滤:`jq 'select(.task_id == "task-xxx")'`
160
- const d = (data && typeof data === 'object') ? data : {};
203
+ const safeData = redactMenuTokensForTrace(data);
204
+ const d = (safeData && typeof safeData === 'object') ? safeData : {};
161
205
  const payload = d.payload ?? {};
162
206
  // 入站事件(IN)路由字段在 d.envelope.*(SDK 0.5.*);出站 params(OUT)字段在顶层。两者兼顾。
163
207
  const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
@@ -178,7 +222,7 @@ export class AUNChannel {
178
222
  const chatmode = payload.chatmode;
179
223
  if (chatmode)
180
224
  topContext.chatmode = chatmode;
181
- const line = JSON.stringify({ ts: localTimestamp(), dir, event, ...topContext, data });
225
+ const line = JSON.stringify({ ts: localTimestamp(), dir, event, ...topContext, data: safeData });
182
226
  this.traceWriter.write(line);
183
227
  }
184
228
  /** 日志前缀(含 self aid 简称,多实例可识别) */
@@ -195,25 +239,37 @@ export class AUNChannel {
195
239
  */
196
240
  async callAndTrace(method, params, opts) {
197
241
  this.trace('OUT', method, params);
242
+ // RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
243
+ // duration 若普遍逼近 SDK 的 rpc 超时(默认 10s),说明发送管线已饱和或链路抖动,
244
+ // 是 /evolhelp 等命令「回复慢/无回复」的直接可观测信号。
245
+ const rpcStart = Date.now();
246
+ // SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
247
+ const SLOW_RPC_WARN_MS = 3000;
198
248
  try {
199
249
  const result = await this.client.call(method, params);
250
+ const durationMs = Date.now() - rpcStart;
200
251
  if (!opts?.silentOk) {
201
252
  const r = result;
202
253
  const snap = r && typeof r === 'object'
203
- ? { message_id: r.message_id, ok: r.ok, thought_id: r.thought_id }
204
- : undefined;
205
- this.trace('OUT', `${method}.ok`, snap ?? {});
254
+ ? { message_id: r.message_id, ok: r.ok, thought_id: r.thought_id, duration_ms: durationMs }
255
+ : { duration_ms: durationMs };
256
+ this.trace('OUT', `${method}.ok`, snap);
257
+ }
258
+ if (durationMs >= SLOW_RPC_WARN_MS) {
259
+ logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms (发送管线可能饱和或链路抖动)`);
206
260
  }
207
261
  return result;
208
262
  }
209
263
  catch (e) {
264
+ const durationMs = Date.now() - rpcStart;
210
265
  this.trace('OUT', `${method}.error`, {
211
266
  error: e?.message ?? String(e),
212
267
  code: e?.code,
213
268
  name: e?.name,
269
+ duration_ms: durationMs,
214
270
  });
215
271
  if (!opts?.silentError) {
216
- logger.warn(`${this.logPrefix()} rpc ${method} failed: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
272
+ logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
217
273
  }
218
274
  throw e;
219
275
  }
@@ -273,8 +329,20 @@ export class AUNChannel {
273
329
  : typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
274
330
  const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
275
331
  if (cardInfo) {
276
- const actionValue = typeof obj.value === 'string' ? obj.value
277
- : typeof obj.action_value === 'string' ? obj.action_value : text;
332
+ // compose 按钮点击后客户端收卡聚焦输入框,用户随后发送的自由文本由客户端结构化
333
+ // 回传为 action_card_reply{behavior:"compose", text},没有按钮 value/action_value
334
+ // 映射为 _custom_input + values.custom_text,复用 claude-runner 已有的自定义输入分支
335
+ // (原为多选表单提交而设),不需要新增 action 类型,也不必拦截消息队列。
336
+ const behaviorRaw = typeof obj.behavior === 'string' ? obj.behavior : undefined;
337
+ if (behaviorRaw && behaviorRaw !== 'reply' && behaviorRaw !== 'compose' && behaviorRaw !== 'dismiss') {
338
+ // 非法 behavior 值:按协议兜底为 reply 处理,但记录下来便于排查不合规客户端
339
+ // (例如本该是 compose 手动输入却被误判为普通按钮点击)。
340
+ logger.warn(`${this.logPrefix()} action_card_reply with unrecognized behavior="${behaviorRaw}" mid=${cardMsgId}, treating as reply`);
341
+ }
342
+ const isComposeReply = behaviorRaw === 'compose';
343
+ const actionValue = isComposeReply ? '_custom_input'
344
+ : typeof obj.value === 'string' ? obj.value
345
+ : typeof obj.action_value === 'string' ? obj.action_value : text;
278
346
  const threadId = typeof obj.thread_id === 'string' ? obj.thread_id : undefined;
279
347
  // 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
280
348
  // payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
@@ -349,7 +417,13 @@ export class AUNChannel {
349
417
  type: 'interaction.response',
350
418
  id: cardInfo.requestId,
351
419
  action: actionValue,
352
- values: { text, action_label: obj.label ?? obj.action_label, behavior: obj.behavior, card_message_id: cardMsgId },
420
+ values: {
421
+ text,
422
+ action_label: obj.label ?? obj.action_label,
423
+ behavior: obj.behavior,
424
+ card_message_id: cardMsgId,
425
+ ...(isComposeReply ? { custom_text: text } : {}),
426
+ },
353
427
  operatorId: cardClickerAid || undefined,
354
428
  };
355
429
  const settle = (accepted) => {
@@ -641,10 +715,6 @@ export class AUNChannel {
641
715
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
642
716
  'merge', 'link', 'location', 'personal_card',
643
717
  ]);
644
- /** Menu protocol 请求类型:自定义消息快速路径,绕过白名单直接分发到 bridge */
645
- static MENU_REQUEST_TYPES = new Set([
646
- 'menu.list', 'menu.query', 'menu.options', 'menu.update', 'menu.action',
647
- ]);
648
718
  /** 观察者插话请求类型(owner → agent.AID)。详见 docs/observer-insert-design.md。 */
649
719
  static INJECT_REQUEST_TYPE = 'observer.inject';
650
720
  /** 绑定请求类型(Evol App → 控制 AID):快速路径,绕过白名单透传原始 payload JSON 给 bridge。 */
@@ -1304,6 +1374,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1304
1374
  // SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1305
1375
  // message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
1306
1376
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1377
+ const protectedHeaders = verifiedProtectedHeaders(msg, env);
1307
1378
  const fromAid = env.from ?? '';
1308
1379
  const payload = msg.payload ?? '';
1309
1380
  const causationRef = payload && typeof payload === 'object' && typeof payload._evolcore_causation_ref === 'string'
@@ -1359,7 +1430,9 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1359
1430
  const shortAid = this.getShortAid(fromAid);
1360
1431
  const displayName = peerIdentity.name || shortAid;
1361
1432
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
1362
- const p2pPayloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1433
+ const p2pPayloadType = (payload && typeof payload === 'object' && typeof payload.type === 'string')
1434
+ ? payload.type
1435
+ : '';
1363
1436
  logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
1364
1437
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1365
1438
  if (p2pPayloadType === 'action_card_reply') {
@@ -1367,7 +1440,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1367
1440
  return;
1368
1441
  }
1369
1442
  // menu.* 协议:自定义消息快速路径,需要原始 payload JSON 传递给 bridge
1370
- if (AUNChannel.MENU_REQUEST_TYPES.has(p2pPayloadType)) {
1443
+ if (p2pPayloadType.startsWith('menu.')) {
1371
1444
  this.acknowledgeImmediately(messageId, seq);
1372
1445
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1373
1446
  this.dispatchMessage({
@@ -1376,6 +1449,9 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1376
1449
  chatType: 'private', messageId, seq,
1377
1450
  peerName: displayName || undefined,
1378
1451
  peerType: peerIdentity.type,
1452
+ encrypted: msgEncrypted,
1453
+ protectedHeaders,
1454
+ replyContext: this.buildGroupReplyContext(threadId, fromAid, msgEncrypted, messageId),
1379
1455
  msgType: logDescriptor.msgType,
1380
1456
  payloadType: logDescriptor.payloadType,
1381
1457
  payloadSummary: logDescriptor.payloadSummary,
@@ -1465,6 +1541,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1465
1541
  // SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1466
1542
  // message_id / seq / payload / same_* / dispatch_mode 等仍是顶层独立字段,不在 envelope 内。
1467
1543
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1544
+ const protectedHeaders = verifiedProtectedHeaders(msg, env);
1468
1545
  const groupId = env.group_id ?? '';
1469
1546
  const senderAid = env.from ?? '';
1470
1547
  const payload = msg.payload ?? '';
@@ -1542,15 +1619,19 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1542
1619
  // 信号类消息(status / event / thought / task.update 等)不进 Agent
1543
1620
  {
1544
1621
  const payloadObj = (payload && typeof payload === 'object') ? payload : null;
1545
- const payloadType = payloadObj?.type ?? '';
1622
+ const payloadType = typeof payloadObj?.type === 'string' ? payloadObj.type : '';
1546
1623
  // menu.* 协议:自定义消息快速路径
1547
- if (AUNChannel.MENU_REQUEST_TYPES.has(payloadType)) {
1624
+ if (payloadType.startsWith('menu.')) {
1625
+ const menuEncrypted = !!env.encrypted;
1548
1626
  this.acknowledgeImmediately(messageId, seq);
1549
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
1627
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId);
1550
1628
  this.dispatchMessage({
1551
1629
  channelId: groupId, userId: senderAid,
1552
1630
  text: JSON.stringify(payload),
1553
1631
  chatType: 'group', messageId, seq, groupId,
1632
+ encrypted: menuEncrypted,
1633
+ protectedHeaders,
1634
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
1554
1635
  msgType: logDescriptor.msgType,
1555
1636
  payloadType: logDescriptor.payloadType,
1556
1637
  payloadSummary: logDescriptor.payloadSummary,
@@ -1769,6 +1850,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1769
1850
  sameNetwork: event.sameNetwork,
1770
1851
  sameEgressIp: event.sameEgressIp,
1771
1852
  encrypted: event.encrypted,
1853
+ protectedHeaders: event.protectedHeaders,
1772
1854
  messageId: event.messageId,
1773
1855
  causation: event.causation,
1774
1856
  threadId: event.threadId,
@@ -1837,6 +1919,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1837
1919
  forwardInbound(data) {
1838
1920
  if (!this.connected || !this.client)
1839
1921
  return;
1922
+ if (payloadTypeForObserver(data)?.startsWith('menu.'))
1923
+ return;
1840
1924
  const { observable, owners } = this.getObserverConfig();
1841
1925
  if (!observable || owners.length === 0)
1842
1926
  return;
@@ -1858,6 +1942,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1858
1942
  forwardOutbound(result) {
1859
1943
  if (!this.connected || !this.client)
1860
1944
  return;
1945
+ if (payloadTypeForObserver(result)?.startsWith('menu.'))
1946
+ return;
1861
1947
  const { observable, owners } = this.getObserverConfig();
1862
1948
  if (!observable || owners.length === 0)
1863
1949
  return;
@@ -2350,6 +2436,30 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2350
2436
  messageIdFromSendResult(result) {
2351
2437
  return result?.message?.message_id ?? result?.message_id ?? null;
2352
2438
  }
2439
+ /**
2440
+ * An AUN send result is only a gateway transport result. It is not a
2441
+ * receipt that the peer client has pulled, decrypted, stored, or rendered it.
2442
+ * Preserve the compact fields for later correlation with the receiving side.
2443
+ */
2444
+ sendReceiptFromResult(result) {
2445
+ if (!result || typeof result !== 'object')
2446
+ return {};
2447
+ const response = result;
2448
+ return {
2449
+ status: typeof response.status === 'string' ? response.status : undefined,
2450
+ seq: typeof response.seq === 'number' ? response.seq : undefined,
2451
+ timestamp: typeof response.timestamp === 'number' ? response.timestamp : undefined,
2452
+ deliveryMode: typeof response.delivery_mode === 'string' ? response.delivery_mode : undefined,
2453
+ };
2454
+ }
2455
+ receiptLogFields(result) {
2456
+ const receipt = this.sendReceiptFromResult(result);
2457
+ return `aun_status=${receipt.status ?? 'unknown'} seq=${receipt.seq ?? 'unknown'} delivery_mode=${receipt.deliveryMode ?? 'unknown'} gateway_timestamp=${receipt.timestamp ?? 'unknown'}`;
2458
+ }
2459
+ logAunSendAccepted(method, target, messageId, encrypt, result, text) {
2460
+ const preview = text === undefined ? '' : ` text=${text.slice(0, 60)}`;
2461
+ logger.info(`${this.logPrefix()} ${method} accepted by AUN: target=${target} mid=${messageId} encrypt=${encrypt} ${this.receiptLogFields(result)}${preview}`);
2462
+ }
2353
2463
  stripUndefinedDeep(value) {
2354
2464
  if (Array.isArray(value)) {
2355
2465
  return value
@@ -2542,7 +2652,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2542
2652
  return { ok: false };
2543
2653
  }
2544
2654
  }
2545
- recordDurableOutbound(channelId, payload, messageId, encrypt, context, isGroup, contentKind, logText, result) {
2655
+ recordDurableOutbound(channelId, payload, messageId, encrypt, context, isGroup, contentKind, logText, result, statsContext) {
2546
2656
  const causation = normalizeCausation(context?.metadata?.causation);
2547
2657
  if (!isGroup && causation)
2548
2658
  registerAunCausation(messageId, this.config.aid, channelId, causation);
@@ -2564,10 +2674,11 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2564
2674
  len: Buffer.byteLength(logText, 'utf-8'),
2565
2675
  ...(isGroup && { groupId: channelId }),
2566
2676
  });
2567
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, this.payloadByteLength(payload, logText), logText, false, encrypt, context?.metadata?.chatmode);
2677
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, this.payloadByteLength(payload, logText), logText, false, encrypt, context?.metadata?.chatmode, 'send', statsContext);
2568
2678
  const source = context?.metadata?.source ?? 'daemon';
2569
2679
  this.appendOutboundJsonl(channelId, {
2570
2680
  ...classifyAunPayloadForLog(payload), msgId: messageId, encrypt, context, isGroup, source,
2681
+ transport: this.sendReceiptFromResult(result),
2571
2682
  });
2572
2683
  this.forwardOutbound(result);
2573
2684
  }
@@ -2752,6 +2863,11 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2752
2863
  context,
2753
2864
  });
2754
2865
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
2866
+ // 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
2867
+ const backlog = outbox.pendingCount(this.config.aid);
2868
+ if (backlog >= 5) {
2869
+ logger.warn(`${this.logPrefix()} Outbox backlog=${backlog} channel=${channelId} (发送管线堵塞,回复将延迟)`);
2870
+ }
2755
2871
  if (!this.connected || !this.client) {
2756
2872
  logger.warn(`${this.logPrefix()} Not connected, message queued in outbox (id=${entry.id}). Triggering reconnect.`);
2757
2873
  if (!this.reconnectTimer && !this.client) {
@@ -2800,7 +2916,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2800
2916
  if (!messageId) {
2801
2917
  return { ok: false, error: `message.send returned no message_id: ${JSON.stringify(result)}`, code: 'MISSING_MESSAGE_ID' };
2802
2918
  }
2803
- logger.info(`${this.logPrefix()} daemon msg.send ok: to=${this.peerLabel(args.to)} mid=${messageId} encrypt=${encrypt} chatmode=${chatmode}`);
2919
+ this.logAunSendAccepted('message.send', this.peerLabel(args.to), messageId, encrypt, result);
2804
2920
  if (outboundCausation) {
2805
2921
  registerAunCausation(messageId, this.config.aid, args.to, outboundCausation);
2806
2922
  recordCausationSpan(outboundCausation, 'message.outbound', {
@@ -2829,6 +2945,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2829
2945
  payloadSummary: classified.payloadSummary ?? args.log.payloadSummary,
2830
2946
  source: args.log.source ?? 'msg',
2831
2947
  handoff_trace: args.log.handoffTrace,
2948
+ transport: this.sendReceiptFromResult(result),
2832
2949
  }));
2833
2950
  this.aidStatsCollector?.recordOutbound(this.config.aid, args.to, Buffer.byteLength(classified.content || args.log.content, 'utf-8'), classified.content || args.log.content, false, encrypt, chatmode, 'send');
2834
2951
  logWritten = true;
@@ -2836,10 +2953,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2836
2953
  return {
2837
2954
  ok: true,
2838
2955
  message_id: messageId,
2839
- seq: result?.seq,
2840
- timestamp: result?.timestamp,
2841
- status: result?.status,
2842
- delivery_mode: result?.delivery_mode,
2956
+ seq: this.sendReceiptFromResult(result).seq,
2957
+ timestamp: this.sendReceiptFromResult(result).timestamp,
2958
+ status: this.sendReceiptFromResult(result).status,
2959
+ delivery_mode: this.sendReceiptFromResult(result).deliveryMode,
2843
2960
  encrypt,
2844
2961
  chatmode,
2845
2962
  log_written: logWritten,
@@ -2903,7 +3020,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2903
3020
  if (!messageId) {
2904
3021
  return { ok: false, error: `group.send returned no message_id: ${JSON.stringify(result)}`, code: 'MISSING_MESSAGE_ID' };
2905
3022
  }
2906
- logger.info(`${this.logPrefix()} daemon group.send ok: group=${args.groupId} mid=${messageId} encrypt=${encrypt}`);
3023
+ this.logAunSendAccepted('group.send', args.groupId, messageId, encrypt, result);
2907
3024
  this.forwardOutbound(result);
2908
3025
  let logWritten = false;
2909
3026
  if (args.log?.content !== undefined) {
@@ -2927,8 +3044,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2927
3044
  payloadSummary: classified.payloadSummary ?? args.log.payloadSummary,
2928
3045
  source: args.log.source ?? 'msg',
2929
3046
  handoff_trace: args.log.handoffTrace,
3047
+ transport: this.sendReceiptFromResult(result),
2930
3048
  }));
2931
- this.aidStatsCollector?.recordOutbound(this.config.aid, args.groupId, Buffer.byteLength(content, 'utf-8'), content, false, encrypt, typeof finalPayload.chatmode === 'string' ? finalPayload.chatmode : undefined, 'send');
3049
+ const statsContext = await this.groupStatsContext(args.groupId);
3050
+ this.aidStatsCollector?.recordOutbound(this.config.aid, args.groupId, Buffer.byteLength(content, 'utf-8'), content, false, encrypt, typeof finalPayload.chatmode === 'string' ? finalPayload.chatmode : undefined, 'send', statsContext);
2932
3051
  logWritten = true;
2933
3052
  }
2934
3053
  return {
@@ -2937,6 +3056,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2937
3056
  group_id: result?.group_id ?? args.groupId,
2938
3057
  message: result?.message,
2939
3058
  event: result?.event,
3059
+ seq: this.sendReceiptFromResult(result).seq,
3060
+ timestamp: this.sendReceiptFromResult(result).timestamp,
3061
+ status: this.sendReceiptFromResult(result).status,
3062
+ delivery_mode: this.sendReceiptFromResult(result).deliveryMode,
2940
3063
  encrypt,
2941
3064
  log_written: logWritten,
2942
3065
  };
@@ -3040,11 +3163,13 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3040
3163
  return false;
3041
3164
  }
3042
3165
  else {
3043
- logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
3166
+ this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
3044
3167
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
3045
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send');
3168
+ const statsContext = await this.groupStatsContext(channelId);
3169
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send', statsContext);
3046
3170
  this.appendOutboundJsonl(channelId, {
3047
3171
  ...classifyAunPayloadForLog(payload), msgId: mid, encrypt, context, isGroup: true, source,
3172
+ transport: this.sendReceiptFromResult(result),
3048
3173
  });
3049
3174
  // Observer forward: outbound (group) — 原样转发 SDK SendResult(含 envelope + payload)
3050
3175
  this.forwardOutbound(result);
@@ -3058,7 +3183,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3058
3183
  return false;
3059
3184
  }
3060
3185
  else {
3061
- logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
3186
+ this.logAunSendAccepted('message.send', this.peerLabel(targetAid), result.message_id, encrypt, result, finalText);
3062
3187
  const causation = normalizeCausation(context?.metadata?.causation);
3063
3188
  if (causation) {
3064
3189
  registerAunCausation(result.message_id, this.config.aid, targetAid, causation);
@@ -3071,6 +3196,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3071
3196
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send');
3072
3197
  this.appendOutboundJsonl(targetAid, {
3073
3198
  ...classifyAunPayloadForLog(payload), msgId: result.message_id, encrypt, context, isGroup: false, source,
3199
+ transport: this.sendReceiptFromResult(result),
3074
3200
  });
3075
3201
  // Observer forward: outbound (private) — 原样转发 SDK SendResult(含 envelope + payload)
3076
3202
  this.forwardOutbound(result);
@@ -3094,9 +3220,11 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3094
3220
  return false;
3095
3221
  }
3096
3222
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
3097
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode);
3223
+ const statsContext = await this.groupStatsContext(channelId);
3224
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode, 'send', statsContext);
3098
3225
  this.appendOutboundJsonl(channelId, {
3099
3226
  ...classifyAunPayloadForLog(payload), msgId: mid, encrypt: false, context, isGroup: true, source,
3227
+ transport: this.sendReceiptFromResult(result),
3100
3228
  });
3101
3229
  this.forwardOutbound(result);
3102
3230
  }
@@ -3121,6 +3249,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3121
3249
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode);
3122
3250
  this.appendOutboundJsonl(targetAid, {
3123
3251
  ...classifyAunPayloadForLog(payload), msgId: mid, encrypt: false, context, isGroup: false, source,
3252
+ transport: this.sendReceiptFromResult(result),
3124
3253
  });
3125
3254
  this.forwardOutbound(result);
3126
3255
  }
@@ -3161,15 +3290,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3161
3290
  if (!sent.ok || !sent.messageId)
3162
3291
  return sent;
3163
3292
  const isGroup = this.isGroupId(channelId);
3164
- logger.info(`${this.logPrefix()} durable payload sent: kind=${contentKind ?? payload.type ?? 'payload'} target=${isGroup ? channelId : this.peerLabel(channelId)} mid=${sent.messageId} encrypt=${sent.encrypt} text=${logText.slice(0, 60)}`);
3165
- this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result);
3293
+ const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
3294
+ this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
3295
+ this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
3166
3296
  this.runPostSend(entry, sent.messageId);
3167
3297
  return sent;
3168
3298
  }
3169
3299
  /** 有效会话正文写入 messages.jsonl(message.send/group.send 成功后调用)。 */
3170
3300
  appendOutboundJsonl(channelId, descriptor) {
3171
3301
  try {
3172
- const { content, msgType, payloadType, payloadSummary, msgId, encrypt, context, isGroup, source = 'daemon' } = descriptor;
3302
+ const { content, msgType, payloadType, payloadSummary, msgId, encrypt, transport, context, isGroup, source = 'daemon' } = descriptor;
3173
3303
  const sessionsDir = resolvePaths().sessionsDir;
3174
3304
  const selfAID = this.config.aid;
3175
3305
  const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfAID);
@@ -3189,6 +3319,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3189
3319
  durationMs: null,
3190
3320
  encrypt,
3191
3321
  chatmode,
3322
+ transport,
3192
3323
  msgType,
3193
3324
  payloadType,
3194
3325
  payloadSummary,
@@ -3261,7 +3392,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3261
3392
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
3262
3393
  this.forwardOutbound(putRes);
3263
3394
  if (thoughtText) {
3264
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive', 'thought');
3395
+ const statsContext = await this.groupStatsContext(channelId);
3396
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive', 'thought', statsContext);
3265
3397
  }
3266
3398
  }
3267
3399
  else {
@@ -3296,21 +3428,36 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3296
3428
  finalPayload.thread_id = context.threadId;
3297
3429
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
3298
3430
  finalPayload.ref_message_id = context.replyToMessageId;
3431
+ const isMenuPayload = typeof finalPayload.type === 'string' && finalPayload.type.startsWith('menu.');
3299
3432
  const params = { payload: finalPayload, encrypt };
3433
+ const rawHeaders = context?.metadata?.protectedHeaders;
3434
+ if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
3435
+ const protectedHeaders = {};
3436
+ for (const [key, value] of Object.entries(rawHeaders)) {
3437
+ const name = key.trim().toLowerCase();
3438
+ if (!/^[a-z0-9_-]+$/.test(name) || typeof value !== 'string')
3439
+ continue;
3440
+ protectedHeaders[name] = value;
3441
+ }
3442
+ if (Object.keys(protectedHeaders).length > 0)
3443
+ params.protected_headers = protectedHeaders;
3444
+ }
3300
3445
  try {
3301
3446
  if (isGroup) {
3302
3447
  params.group_id = channelId;
3303
3448
  const result = await this.callAndTrace('group.send', params);
3304
3449
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
3305
3450
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
3306
- this.forwardOutbound(result);
3451
+ if (!isMenuPayload)
3452
+ this.forwardOutbound(result);
3307
3453
  return mid;
3308
3454
  }
3309
3455
  else {
3310
3456
  params.to = targetAid;
3311
3457
  const result = await this.callAndTrace('message.send', params);
3312
3458
  logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
3313
- this.forwardOutbound(result);
3459
+ if (!isMenuPayload)
3460
+ this.forwardOutbound(result);
3314
3461
  return result?.message_id ?? null;
3315
3462
  }
3316
3463
  }
@@ -3493,7 +3640,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3493
3640
  if (sentMid) {
3494
3641
  const fileText = filePayload.text;
3495
3642
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: sentMid, kind: 'file', len: fileText.length, ...(isGroup && { groupId: channelId }) });
3496
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(fileText, 'utf-8'), fileText, false, !!params.encrypt, context?.metadata?.chatmode);
3643
+ const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
3644
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(fileText, 'utf-8'), fileText, false, !!params.encrypt, context?.metadata?.chatmode, 'send', statsContext);
3497
3645
  const source = context?.metadata?.source ?? 'daemon';
3498
3646
  this.appendOutboundJsonl(channelId, {
3499
3647
  ...classifyAunPayloadForLog(filePayload), msgId: sentMid, encrypt: !!params.encrypt,
@@ -3618,9 +3766,13 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3618
3766
  const initiator = statusPayload.initiator ?? '';
3619
3767
  const refMsgId = statusPayload.ref_message_id ?? '';
3620
3768
  const metaStr = statusPayload.metadata ? ` meta=${JSON.stringify(statusPayload.metadata)}` : '';
3621
- logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}${metaStr}`);
3769
+ const logTaskStatus = `${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}${metaStr}`;
3770
+ if (status === 'error' || status === 'timeout')
3771
+ logger.error(logTaskStatus);
3772
+ else
3773
+ logger.info(logTaskStatus);
3622
3774
  }
3623
- sendCustomPayload(channelId, payload) {
3775
+ async sendCustomPayload(channelId, payload, context) {
3624
3776
  if (!this.client || !this.connected)
3625
3777
  return;
3626
3778
  // SDK 0.3.0 E2EE requires payload to be an object
@@ -3633,19 +3785,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3633
3785
  catch {
3634
3786
  payloadObj = { text: payload };
3635
3787
  }
3636
- // 私聊 channelId = 对端 AID(不含 device_id)
3637
- const customTargetAid = channelId;
3638
- const sendParams = {
3639
- to: customTargetAid, payload: payloadObj,
3640
- encrypt: true,
3641
- };
3642
- this.trace('OUT', 'message.send.custom', sendParams);
3643
- this.client.call('message.send', sendParams).then((result) => {
3644
- this.trace('OUT', 'message.send.custom.ok', { message_id: result?.message_id });
3645
- }).catch(e => {
3646
- this.trace('OUT', 'message.send.custom.error', { error: String(e) });
3647
- logger.warn(`${this.logPrefix()} Custom payload failed: ${e}`);
3648
- });
3788
+ await this.sendStructured(channelId, payloadObj, context);
3649
3789
  }
3650
3790
  async disconnect() {
3651
3791
  this.intentionalDisconnect = true;
@@ -3721,44 +3861,18 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3721
3861
  }
3722
3862
  /** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
3723
3863
  loadSelfName(aid) {
3724
- try {
3725
- const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
3726
- const mdPath = agentMdPathFn(aidName);
3727
- if (!fs.existsSync(mdPath)) {
3728
- // 异步拉取,不阻塞连接流程
3729
- this.fetchAndCacheSelfName(aidName);
3730
- return undefined;
3731
- }
3732
- const content = fs.readFileSync(mdPath, 'utf-8');
3733
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
3734
- if (!fmMatch)
3735
- return undefined;
3736
- const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
3737
- return nameMatch?.[1]?.trim() || undefined;
3738
- }
3739
- catch {
3740
- return undefined;
3741
- }
3864
+ const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
3865
+ const name = resolveAgentDisplayName(aidName);
3866
+ if (!name)
3867
+ void this.fetchAndCacheSelfName(aidName);
3868
+ return name;
3742
3869
  }
3743
3870
  async fetchAndCacheSelfName(aidName) {
3744
- try {
3745
- const { agentmdGet } = await import('../aun/aid/index.js');
3746
- const content = await agentmdGet(aidName);
3747
- if (content) {
3748
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
3749
- if (fmMatch) {
3750
- const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
3751
- const name = nameMatch?.[1]?.trim();
3752
- if (name) {
3753
- this._selfName = name;
3754
- if (this.aidStatsCollector)
3755
- this.aidStatsCollector.setSelfName(this.config.aid, name);
3756
- }
3757
- }
3758
- }
3759
- }
3760
- catch {
3761
- // ignore — name will remain undefined
3871
+ const name = await refreshAgentDisplayName(aidName);
3872
+ if (name) {
3873
+ this._selfName = name;
3874
+ if (this.aidStatsCollector)
3875
+ this.aidStatsCollector.setSelfName(this.config.aid, name);
3762
3876
  }
3763
3877
  }
3764
3878
  getSelfName() {
@@ -3809,7 +3923,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3809
3923
  return result.content ?? '';
3810
3924
  }
3811
3925
  /**
3812
- * 取群显示名(group.get → group.name),进程内缓存。
3926
+ * 取群显示名(group.get_info → group.name),进程内缓存。
3813
3927
  * 走长连接 callAndTrace,失败/未连接返回 undefined —— 绝不抛出阻塞消息处理。
3814
3928
  */
3815
3929
  async getGroupName(groupId) {
@@ -3821,9 +3935,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3821
3935
  if (!this.client)
3822
3936
  return undefined;
3823
3937
  try {
3824
- const result = await this.callAndTrace('group.get', { group_id: groupId });
3825
- const name = result?.group?.name;
3826
- if (typeof name === 'string' && name) {
3938
+ const result = await this.callAndTrace('group.get_info', { group_id: groupId });
3939
+ const group = result?.group ?? result;
3940
+ const name = typeof group?.name === 'string' ? group.name.trim() : '';
3941
+ if (name) {
3827
3942
  this.groupNameCache.set(groupId, name);
3828
3943
  return name;
3829
3944
  }
@@ -3834,6 +3949,13 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3834
3949
  return undefined; // 不写缓存,下次仍可重试
3835
3950
  }
3836
3951
  }
3952
+ /** 统计关系键继续使用群 ID,仅为智能体预览补充可读群名。 */
3953
+ async groupStatsContext(groupId) {
3954
+ return {
3955
+ channelType: 'aun',
3956
+ peerLabel: await this.getGroupName(groupId) ?? groupId,
3957
+ };
3958
+ }
3837
3959
  /**
3838
3960
  * 查询某成员在群里的角色(经 group.get_admins)。
3839
3961
  * 仅用于话题创建权限校验(稀有事件),故不缓存:每次查权威源,结果天然最新。
@@ -3998,6 +4120,7 @@ export class AUNChannelPlugin {
3998
4120
  return;
3999
4121
  case 'interaction': {
4000
4122
  const req = payload.interaction;
4123
+ const cardTtlMs = 20 * 60 * 1000;
4001
4124
  if (req.kind.kind === 'action') {
4002
4125
  const action = req.kind;
4003
4126
  const aunCard = {
@@ -4007,10 +4130,12 @@ export class AUNChannelPlugin {
4007
4130
  label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
4008
4131
  })),
4009
4132
  };
4010
- // AUN action_card 无法内嵌输入框(只有按钮),手动输入降级为「✏️ 手动输入」按钮:
4011
- // 点击回传 _show_input → claude-runner 用 interceptNextMessage 拦截下一条消息作为答案。
4133
+ // AUN action_card 无法内嵌输入框(只有按钮),手动输入用协议原生的 compose
4134
+ // behavior:客户端收到点击后自行收卡+聚焦输入框,用户在 timeout 内的下一条
4135
+ // 消息由客户端结构化回传为 action_card_reply{behavior:"compose"}(见
4136
+ // extractTextPayload 的 compose 分支),不依赖 interceptNextMessage 拦截消息队列。
4012
4137
  if (action.allowCustomInput) {
4013
- aunCard.actions.push({ label: '✏️ 手动输入', value: '_show_input', style: 'default', behavior: 'reply' });
4138
+ aunCard.actions.push({ label: '✏️ 手动输入', style: 'default', behavior: 'compose', compose_timeout_ms: cardTtlMs });
4014
4139
  }
4015
4140
  if (action.body) {
4016
4141
  aunCard.text = action.body;
@@ -4029,7 +4154,7 @@ export class AUNChannelPlugin {
4029
4154
  requestId: req.id,
4030
4155
  isCommandCard: false,
4031
4156
  initiatorAid: req.initiatorId,
4032
- expiresAt: Date.now() + 20 * 60 * 1000,
4157
+ expiresAt: Date.now() + cardTtlMs,
4033
4158
  },
4034
4159
  });
4035
4160
  }
@@ -4064,7 +4189,7 @@ export class AUNChannelPlugin {
4064
4189
  requestId: req.id,
4065
4190
  isCommandCard: true,
4066
4191
  initiatorAid: req.initiatorId,
4067
- expiresAt: Date.now() + 20 * 60 * 1000,
4192
+ expiresAt: Date.now() + cardTtlMs,
4068
4193
  },
4069
4194
  });
4070
4195
  }
@@ -4075,7 +4200,7 @@ export class AUNChannelPlugin {
4075
4200
  }
4076
4201
  case 'custom': {
4077
4202
  const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
4078
- channel.sendCustomPayload(channelId, text);
4203
+ await channel.sendCustomPayload(channelId, text, replyCtx);
4079
4204
  return;
4080
4205
  }
4081
4206
  default:
@@ -4107,7 +4232,7 @@ export class AUNChannelPlugin {
4107
4232
  return {
4108
4233
  channelType: 'aun', adapter, channel,
4109
4234
  policy,
4110
- options: { flushDelay: inst.flushDelay ?? DEFAULT_FLUSH_DELAY_SECONDS, fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g },
4235
+ options: { flushDelay: inst.flushDelay ?? DEFAULT_FLUSH_DELAY_SECONDS, fileMarkerPattern: createSendFileMarkerPattern() },
4111
4236
  connect: () => channel.connect(),
4112
4237
  disconnect: () => channel.disconnect(),
4113
4238
  onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),