evolclaw 3.1.4 → 3.1.6

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 (99) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/agents/claude-runner.js +398 -161
  3. package/dist/agents/kit-renderer.js +191 -25
  4. package/dist/aun/aid/agentmd.js +75 -103
  5. package/dist/aun/aid/client.js +1 -29
  6. package/dist/aun/aid/identity.js +105 -64
  7. package/dist/aun/aid/index.js +2 -1
  8. package/dist/aun/aid/store.js +74 -0
  9. package/dist/aun/msg/group.js +2 -2
  10. package/dist/aun/msg/p2p.js +26 -2
  11. package/dist/aun/rpc/connection.js +23 -30
  12. package/dist/channels/aun.js +174 -99
  13. package/dist/channels/dingtalk.js +2 -1
  14. package/dist/channels/feishu.js +301 -199
  15. package/dist/channels/qqbot.js +2 -1
  16. package/dist/channels/wechat.js +2 -1
  17. package/dist/channels/wecom.js +2 -1
  18. package/dist/cli/agent.js +21 -16
  19. package/dist/cli/bench.js +41 -28
  20. package/dist/cli/help.js +8 -0
  21. package/dist/cli/index.js +176 -87
  22. package/dist/cli/init-channel.js +5 -1
  23. package/dist/cli/init.js +37 -21
  24. package/dist/cli/link-rules.js +1 -7
  25. package/dist/cli/model.js +549 -0
  26. package/dist/cli/net-check.js +133 -50
  27. package/dist/cli/watch-msg.js +7 -7
  28. package/dist/cli/watch-web/debug-log.js +18 -0
  29. package/dist/cli/watch-web/server.js +306 -0
  30. package/dist/cli/watch-web/sources/aid.js +63 -0
  31. package/dist/cli/watch-web/sources/msg.js +70 -0
  32. package/dist/cli/watch-web/sources/session.js +638 -0
  33. package/dist/cli/watch-web/sources/types.js +10 -0
  34. package/dist/cli/watch-web/static/app.js +546 -0
  35. package/dist/cli/watch-web/static/index.html +54 -0
  36. package/dist/cli/watch-web/static/style.css +247 -0
  37. package/dist/config-store.js +1 -22
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +261 -133
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -22
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/im-renderer.js +9 -20
  44. package/dist/core/message/message-bridge.js +13 -9
  45. package/dist/core/message/message-log.js +2 -2
  46. package/dist/core/message/message-processor.js +211 -123
  47. package/dist/core/message/stream-idle-monitor.js +21 -0
  48. package/dist/core/model/model-catalog.js +215 -0
  49. package/dist/core/model/model-scope.js +250 -0
  50. package/dist/core/relation/peer-identity.js +58 -55
  51. package/dist/core/relation/peer-key.js +16 -0
  52. package/dist/core/session/session-fs-store.js +34 -55
  53. package/dist/core/session/session-key.js +24 -0
  54. package/dist/core/session/session-manager.js +308 -251
  55. package/dist/core/session/session-mapper.js +9 -4
  56. package/dist/core/trigger/manager.js +3 -3
  57. package/dist/core/trigger/parser.js +4 -4
  58. package/dist/core/trigger/scheduler.js +22 -7
  59. package/dist/index.js +61 -7
  60. package/dist/ipc.js +23 -1
  61. package/dist/utils/error-utils.js +6 -0
  62. package/dist/utils/process-introspect.js +7 -5
  63. package/kits/docs/GUIDE.md +2 -2
  64. package/kits/docs/INDEX.md +8 -8
  65. package/kits/docs/channels/aun.md +56 -17
  66. package/kits/docs/channels/feishu.md +41 -12
  67. package/kits/docs/context-assembly.md +182 -0
  68. package/kits/docs/evolclaw/INDEX.md +43 -0
  69. package/kits/docs/evolclaw/agent.md +49 -0
  70. package/kits/docs/evolclaw/aid.md +49 -0
  71. package/kits/docs/evolclaw/ctl.md +46 -0
  72. package/kits/docs/evolclaw/group.md +89 -0
  73. package/kits/docs/evolclaw/model.md +51 -0
  74. package/kits/docs/evolclaw/msg.md +91 -0
  75. package/kits/docs/evolclaw/rpc.md +35 -0
  76. package/kits/docs/evolclaw/storage.md +49 -0
  77. package/kits/docs/venues/aun-group.md +10 -0
  78. package/kits/docs/venues/aun-private.md +10 -0
  79. package/kits/docs/venues/client-desktop.md +10 -0
  80. package/kits/docs/venues/client-mobile.md +10 -0
  81. package/kits/docs/venues/feishu-group.md +13 -0
  82. package/kits/docs/venues/feishu-private.md +9 -0
  83. package/kits/docs/venues/group.md +23 -0
  84. package/kits/docs/venues/private.md +10 -0
  85. package/kits/eck_manifest.json +81 -36
  86. package/kits/rules/01-overview.md +20 -10
  87. package/kits/rules/06-channel.md +34 -27
  88. package/kits/templates/system-fragments/baseagent.md +7 -1
  89. package/kits/templates/system-fragments/channel.md +7 -5
  90. package/kits/templates/system-fragments/commands.md +19 -0
  91. package/kits/templates/system-fragments/session.md +19 -3
  92. package/kits/templates/system-fragments/venue.md +24 -0
  93. package/package.json +10 -5
  94. package/dist/aun/aid/lifecycle-log.js +0 -33
  95. package/dist/utils/aid-lifecycle-log.js +0 -33
  96. package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
  97. package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
  98. package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
  99. package/kits/docs/evolclaw/tools.md +0 -25
@@ -10,6 +10,114 @@ import os from 'os';
10
10
  import { logger } from '../utils/logger.js';
11
11
  import { checkBlacklist, checkReadonly, summarizeToolInput } from '../core/permission.js';
12
12
  import { encodePath } from '../utils/cross-platform.js';
13
+ // ── 模型别名解析 ──
14
+ // SDK 内置的别名表可能落后于代理实际可用的最新模型,
15
+ // 因此优先从 {baseUrl}/models 动态获取各系列最新版本,失败则回退静态表。
16
+ // 已验证可用但尚未出现在 /models 列表中的模型 ID 会被注入候选列表,
17
+ // 等列表更新后注入自动变为 no-op。
18
+ const MODEL_FAMILIES = ['opus', 'sonnet', 'haiku'];
19
+ /** 已验证可用但可能尚未出现在 /models 列表中的模型 ID(注入候选) */
20
+ const INJECTED_MODELS = [];
21
+ /** 静态回退表:动态获取失败时使用 */
22
+ const STATIC_MODEL_ALIASES = {
23
+ 'opus': 'claude-opus-4-8',
24
+ 'sonnet': 'claude-sonnet-4-6',
25
+ 'haiku': 'claude-haiku-4-5-20251001',
26
+ };
27
+ const MODEL_ALIAS_TTL_MS = 5 * 60 * 1000; // 5min
28
+ const modelAliasCache = new Map(); // key: baseUrl
29
+ const modelAliasInFlight = new Set(); // 去重并发刷新
30
+ /** 从模型 ID 列表中提取各 claude 系列的最新版本(按 major.minor 取最高) */
31
+ function deriveAliasesFromModelIds(ids) {
32
+ // 注入已验证可用的模型(如果列表中已有则去重无影响)
33
+ const allIds = [...new Set([...ids, ...INJECTED_MODELS])];
34
+ const best = {};
35
+ for (const id of allIds) {
36
+ const m = id.match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)/);
37
+ if (!m)
38
+ continue;
39
+ const [, family, majorStr, minorStr] = m;
40
+ const major = parseInt(majorStr, 10);
41
+ const minor = parseInt(minorStr, 10);
42
+ const cur = best[family];
43
+ if (!cur || major > cur.major || (major === cur.major && minor > cur.minor)) {
44
+ best[family] = { id, major, minor };
45
+ }
46
+ }
47
+ const aliases = {};
48
+ for (const [family, info] of Object.entries(best))
49
+ aliases[family] = info.id;
50
+ return aliases;
51
+ }
52
+ /** 异步刷新某 baseUrl 的别名缓存(失败静默,不抛出) */
53
+ async function refreshModelAliases(baseUrl, apiKey) {
54
+ if (modelAliasInFlight.has(baseUrl))
55
+ return;
56
+ modelAliasInFlight.add(baseUrl);
57
+ try {
58
+ const url = `${baseUrl.replace(/\/$/, '')}/models`;
59
+ const controller = new AbortController();
60
+ const timer = setTimeout(() => controller.abort(), 5000);
61
+ const resp = await fetch(url, {
62
+ signal: controller.signal,
63
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
64
+ });
65
+ clearTimeout(timer);
66
+ if (!resp.ok)
67
+ return;
68
+ const json = await resp.json();
69
+ const ids = Array.isArray(json?.data)
70
+ ? json.data.map((m) => m?.id).filter((x) => typeof x === 'string')
71
+ : [];
72
+ const aliases = deriveAliasesFromModelIds(ids);
73
+ if (ids.length > 0 || Object.keys(aliases).length > 0) {
74
+ modelAliasCache.set(baseUrl, { aliases, ids, fetchedAt: Date.now() });
75
+ logger.info(`[AgentRunner] Refreshed models from ${url}: ${ids.length} ids, aliases ${JSON.stringify(aliases)}`);
76
+ }
77
+ }
78
+ catch {
79
+ // 网络/解析失败:保持静态回退,不打断查询
80
+ }
81
+ finally {
82
+ modelAliasInFlight.delete(baseUrl);
83
+ }
84
+ }
85
+ /** 将短别名展开为完整 model ID,已是完整 ID 则原样返回 */
86
+ function resolveModelAlias(model, baseUrl) {
87
+ // 非短别名(已经是完整 ID)直接返回
88
+ if (!MODEL_FAMILIES.includes(model))
89
+ return model;
90
+ // 优先使用动态缓存
91
+ if (baseUrl) {
92
+ const cached = modelAliasCache.get(baseUrl);
93
+ if (cached && (Date.now() - cached.fetchedAt < MODEL_ALIAS_TTL_MS)) {
94
+ return cached.aliases[model] || STATIC_MODEL_ALIASES[model] || model;
95
+ }
96
+ }
97
+ // 回退静态表
98
+ return STATIC_MODEL_ALIASES[model] || model;
99
+ }
100
+ /** 支持 1M 上下文窗口的模型 ID 前缀(SDK 通过 `[1m]` 后缀启用)。 */
101
+ const ONE_M_CONTEXT_PREFIXES = ['claude-opus-4-8', 'claude-sonnet-4-6'];
102
+ /**
103
+ * 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
104
+ * 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
105
+ */
106
+ function applyContextWindow(modelId) {
107
+ if (/\[1m\]$/.test(modelId))
108
+ return modelId; // 已带后缀
109
+ if (ONE_M_CONTEXT_PREFIXES.some(p => modelId === p))
110
+ return `${modelId}[1m]`;
111
+ return modelId;
112
+ }
113
+ /** 根据 SDK model 串(含 [1m] 后缀)返回合适的 autoCompactWindow 值。 */
114
+ function contextWindowFor(sdkModel) {
115
+ return /\[1m\]$/.test(sdkModel) ? 900000 : 200000;
116
+ }
117
+ /** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
118
+ function resolveSdkModel(model, baseUrl) {
119
+ return applyContextWindow(resolveModelAlias(model, baseUrl));
120
+ }
13
121
  class MessageStream {
14
122
  queue = [];
15
123
  waiting = null;
@@ -89,6 +197,9 @@ export class AgentRunner {
89
197
  permissionContexts = new Map();
90
198
  currentEvolclawSessionId;
91
199
  claudeExecutablePath;
200
+ /** 每个 session 最近的子进程 stderr 行(环形缓冲),用于子进程崩溃时还原真正原因 */
201
+ recentStderr = new Map();
202
+ static STDERR_BUFFER_MAX = 80;
92
203
  constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
93
204
  this.apiKey = apiKey;
94
205
  this.model = model || 'sonnet';
@@ -102,11 +213,17 @@ export class AgentRunner {
102
213
  }
103
214
  }
104
215
  getAgentEnv() {
216
+ // SDK 0.3.x 起,CLI 在以 root 运行时会拒绝 --dangerously-skip-permissions
217
+ // (bypassPermissions 模式映射而来),报错 "cannot be used with root/sudo privileges"
218
+ // 并以 code 1 退出。IS_SANDBOX=1 是 CLI 提供的 root 守卫豁免开关。
219
+ // 仅在以 root 运行时注入,非 root 部署行为不变。
220
+ const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
105
221
  return {
106
222
  ...process.env,
107
223
  ANTHROPIC_AUTH_TOKEN: this.apiKey,
108
224
  PATH: process.env.PATH,
109
225
  DISABLE_AUTOUPDATER: '1',
226
+ ...(isRoot ? { IS_SANDBOX: '1' } : {}),
110
227
  ...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {}),
111
228
  ...(this.currentEvolclawSessionId ? { EVOLCLAW_SESSION_ID: this.currentEvolclawSessionId } : {}),
112
229
  };
@@ -117,8 +234,28 @@ export class AgentRunner {
117
234
  getModel() {
118
235
  return this.model;
119
236
  }
120
- listModels() {
121
- return ['opus', 'sonnet', 'haiku'];
237
+ async listModels() {
238
+ if (this.baseUrl) {
239
+ let cached = modelAliasCache.get(this.baseUrl);
240
+ const stale = !cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS);
241
+ // 缓存为空(首次打开)→ 等待刷新;缓存仅过期 → 后台刷新不阻塞
242
+ if (!cached) {
243
+ await refreshModelAliases(this.baseUrl, this.apiKey);
244
+ cached = modelAliasCache.get(this.baseUrl);
245
+ }
246
+ else if (stale) {
247
+ refreshModelAliases(this.baseUrl, this.apiKey);
248
+ }
249
+ // 有缓存时返回网关 /models 的全量原始 ID
250
+ if (cached && cached.ids.length > 0)
251
+ return cached.ids;
252
+ }
253
+ // 无 baseUrl / 刷新超时或失败 → 回退短别名
254
+ return Object.values(STATIC_MODEL_ALIASES);
255
+ }
256
+ /** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
257
+ resolveModelId(model) {
258
+ return resolveModelAlias(model, this.baseUrl);
122
259
  }
123
260
  setEffort(effort) {
124
261
  this.effort = effort;
@@ -157,7 +294,7 @@ export class AgentRunner {
157
294
  toSdkPermissionMode() {
158
295
  const map = {
159
296
  'auto': 'auto', // AI 分类器自动判断
160
- 'bypass': 'default', // 全部自动放行(通过 canUseTool 一律 allow,保留 hook 安全检查)
297
+ 'bypass': 'bypassPermissions', // 全部自动放行(SDK 跳过分类器,canUseTool 仍保留 hook 安全检查)
161
298
  'request': 'default', // 部分自动,部分询问
162
299
  'edit': 'acceptEdits',
163
300
  'plan': 'plan',
@@ -213,49 +350,70 @@ export class AgentRunner {
213
350
  if (!adapterHasInteractionPath) {
214
351
  return this.handleAskUserQuestionFallback(sessionId, input, questions);
215
352
  }
353
+ // 立即暂停 idle 监控,不等卡片发完再 register
354
+ permCtx.interactionRouter?.markWaiting(sessionId);
355
+ let waitMarked = true;
216
356
  const answers = {};
217
- // 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
218
- // 注意:sendPromptFn 是全局单例,多 channel 并发时会被覆盖,导致提示发到错误 channel
219
357
  const sendPrompt = permCtx.adapter && permCtx.channelId
220
358
  ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
221
359
  : this.sendPromptFn;
222
- // 逐个 question 发送卡片并等待用户选择
223
360
  for (let i = 0; i < questions.length; i++) {
224
361
  const q = questions[i];
225
362
  const requestId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
226
363
  const cardTitle = q.header ? `💬 ${q.header}` : `💬 问题 ${i + 1}/${questions.length}`;
227
- // 统一使用 action 按钮卡片(单选 / 多选均用按钮)
228
- const bodyLines = [q.question];
229
- if (q.options.some(opt => opt.description)) {
230
- bodyLines.push('');
231
- q.options.forEach((opt, idx) => {
232
- bodyLines.push(`${idx + 1}. **${opt.label}**${opt.description ? ` — ${opt.description}` : ''}`);
233
- });
364
+ let interaction;
365
+ if (q.multiSelect) {
366
+ // 多选:使用 checkers + form 提交(JSON 2.0 CardKit 路径)
367
+ interaction = {
368
+ type: 'interaction',
369
+ id: requestId,
370
+ kind: {
371
+ kind: 'action',
372
+ title: cardTitle,
373
+ body: q.question,
374
+ checkers: q.options.map(opt => ({
375
+ key: opt.label,
376
+ label: opt.label,
377
+ description: opt.description,
378
+ })),
379
+ buttons: [
380
+ { key: 'submit', label: '✅ 确认选择', style: 'primary' },
381
+ ],
382
+ allowCustomInput: true,
383
+ },
384
+ channelId: permCtx.channelId,
385
+ sessionId,
386
+ expiresAt: Date.now() + 5 * 60 * 1000,
387
+ };
234
388
  }
235
- const interaction = {
236
- type: 'interaction',
237
- id: requestId,
238
- kind: {
239
- kind: 'action',
240
- title: cardTitle,
241
- body: bodyLines.join('\n'),
242
- buttons: [
243
- ...q.options.map(opt => ({
389
+ else {
390
+ // 单选:保持按钮模式
391
+ const bodyLines = [q.question];
392
+ if (q.options.some(opt => opt.description)) {
393
+ bodyLines.push('');
394
+ q.options.forEach((opt, idx) => {
395
+ bodyLines.push(`${idx + 1}. **${opt.label}**${opt.description ? ` — ${opt.description}` : ''}`);
396
+ });
397
+ }
398
+ interaction = {
399
+ type: 'interaction',
400
+ id: requestId,
401
+ kind: {
402
+ kind: 'action',
403
+ title: cardTitle,
404
+ body: bodyLines.join('\n'),
405
+ buttons: q.options.map(opt => ({
244
406
  key: opt.label,
245
407
  label: opt.label,
246
408
  style: 'default',
247
409
  })),
248
- ...(permCtx.interceptNextMessage ? [{
249
- key: '_custom_input',
250
- label: '✏️ 手动输入',
251
- style: 'default',
252
- }] : []),
253
- ],
254
- },
255
- channelId: permCtx.channelId,
256
- sessionId,
257
- expiresAt: Date.now() + 5 * 60 * 1000,
258
- };
410
+ allowCustomInput: true,
411
+ },
412
+ channelId: permCtx.channelId,
413
+ sessionId,
414
+ expiresAt: Date.now() + 5 * 60 * 1000,
415
+ };
416
+ }
259
417
  let cardSent = false;
260
418
  try {
261
419
  const envelope = buildEnvelope({
@@ -275,7 +433,6 @@ export class AgentRunner {
275
433
  logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
276
434
  }
277
435
  if (!cardSent) {
278
- // 卡片发送失败,以纯文本展示选项并自动选推荐项
279
436
  const firstLabel = q.options[0]?.label || '';
280
437
  answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
281
438
  if (sendPrompt) {
@@ -284,35 +441,39 @@ export class AgentRunner {
284
441
  }
285
442
  continue;
286
443
  }
287
- // 等待用户交互
444
+ // 等待用户交互:先 register 接管计数,再 unmark 占位,消除空窗期
445
+ // (unmark 必须在 register 之后,否则计数短暂降为 0 触发 onWaitEnd→resume,idle 时钟被重置)
288
446
  const answer = await new Promise((resolve) => {
289
447
  permCtx?.interactionRouter?.register(requestId, sessionId, (action, values) => {
290
448
  if (action === 'cancel') {
291
449
  resolve(null);
292
450
  }
293
- else if (action === '_custom_input' && permCtx.interceptNextMessage) {
294
- // "手动输入":发提示,拦截下一条消息
295
- const sendHint = async () => {
296
- if (sendPrompt) {
297
- await sendPrompt('✏️ 请输入你的想法,回复后继续……');
451
+ else if (action === '_custom_input') {
452
+ // 用户通过追加的 input 提交了自定义文本
453
+ const customText = values?.custom_text;
454
+ resolve(typeof customText === 'string' && customText.trim() ? customText.trim() : null);
455
+ }
456
+ else if (action === 'submit' && q.multiSelect && values) {
457
+ // checker 多选提交:从 form_value 收集 checked 选项
458
+ const selected = [];
459
+ q.options.forEach((opt, idx) => {
460
+ if (values[`opt_${idx}`] === true) {
461
+ selected.push(opt.label);
298
462
  }
299
- };
300
- sendHint().catch(() => { });
301
- permCtx.interceptNextMessage(sessionId, (msg) => {
302
- resolve(msg.content || null);
303
463
  });
304
- }
305
- else if (q.multiSelect) {
306
- // multiSelect 按钮点击:包装为数组
307
- resolve([action]);
464
+ resolve(selected.length > 0 ? selected : null);
308
465
  }
309
466
  else {
310
- resolve(action); // action = button key = option label
467
+ resolve(action);
311
468
  }
312
469
  });
470
+ // register 已接管计数(计数 +1),现在才能安全释放 markWaiting 占位(计数 -1),避免空窗
471
+ if (waitMarked) {
472
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
473
+ waitMarked = false;
474
+ }
313
475
  });
314
476
  if (answer === null) {
315
- // 取消,自动选第一项
316
477
  const firstLabel = q.options[0]?.label || '';
317
478
  answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
318
479
  }
@@ -320,6 +481,9 @@ export class AgentRunner {
320
481
  answers[q.question] = answer;
321
482
  }
322
483
  }
484
+ if (waitMarked) {
485
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
486
+ }
323
487
  const updatedInput = { ...input, answers };
324
488
  return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
325
489
  }
@@ -395,6 +559,8 @@ export class AgentRunner {
395
559
  if (!permCtx?.channelId || !sendPrompt) {
396
560
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
397
561
  }
562
+ // 立即暂停 idle 监控,不等卡片发完再 register
563
+ permCtx.interactionRouter?.markWaiting(sessionId);
398
564
  // 尝试发送交互卡片
399
565
  let cardSent = false;
400
566
  if (permCtx.adapter?.send) {
@@ -429,6 +595,7 @@ export class AgentRunner {
429
595
  { key: 'approve', label: '✅ 批准执行', style: 'primary' },
430
596
  { key: 'reject', label: '❌ 拒绝', style: 'danger' },
431
597
  ],
598
+ allowCustomInput: true,
432
599
  },
433
600
  channelId: permCtx.channelId,
434
601
  sessionId,
@@ -455,10 +622,15 @@ export class AgentRunner {
455
622
  logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
456
623
  }
457
624
  if (cardSent) {
625
+ permCtx.interactionRouter?.unmarkWaiting(sessionId);
458
626
  return new Promise((resolve) => {
459
- permCtx.interactionRouter?.register(requestId, sessionId, (action) => {
627
+ permCtx.interactionRouter?.register(requestId, sessionId, (action, values) => {
460
628
  const trimmed = action.trim();
461
- if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝' || trimmed === 'reject') {
629
+ if (trimmed === '_custom_input') {
630
+ const feedback = typeof values?.custom_text === 'string' ? values.custom_text.trim() : '';
631
+ resolve({ behavior: 'deny', message: feedback || '用户提交了反馈', decisionClassification: 'user_reject' });
632
+ }
633
+ else if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
462
634
  resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
463
635
  }
464
636
  else {
@@ -492,6 +664,7 @@ export class AgentRunner {
492
664
  },
493
665
  };
494
666
  await sendPrompt(renderActionAsText(fallbackInteraction));
667
+ permCtx.interactionRouter.unmarkWaiting(sessionId);
495
668
  return new Promise((resolve) => {
496
669
  permCtx.interactionRouter.register(fallbackRequestId, sessionId, (action) => {
497
670
  const trimmed = action.trim();
@@ -505,6 +678,7 @@ export class AgentRunner {
505
678
  });
506
679
  }
507
680
  // 无交互能力,发提示后直接 allow
681
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
508
682
  await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
509
683
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
510
684
  }
@@ -512,127 +686,171 @@ export class AgentRunner {
512
686
  * SDK 原始事件 → 标准 AgentEvent 转换
513
687
  * 所有 SDK 特有的事件类型引用封装在此方法内
514
688
  */
515
- async *transformStream(sdkStream, sessionId) {
689
+ async *transformStream(sdkStream, sessionId, callModel, callEffort, sdkModel) {
516
690
  let lastSessionId;
517
691
  // tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
518
692
  const toolUseNames = new Map();
519
693
  let turnCount = 0;
520
694
  const seenMessageIds = new Set();
521
- for await (const event of sdkStream) {
522
- // 提取 session_id(任意 SDK 事件都可能携带)
523
- if (event.session_id && event.session_id !== lastSessionId) {
524
- lastSessionId = event.session_id;
525
- this.updateSessionId(sessionId, event.session_id);
526
- yield { type: 'session_id', sessionId: event.session_id };
527
- }
528
- // system: compact_boundary → compact
529
- if (event.type === 'system' && event.subtype === 'compact_boundary') {
530
- yield { type: 'compact', preTokens: event.compact_metadata?.pre_tokens || 0 };
531
- }
532
- // system: task_progress → task_progress
533
- if (event.type === 'system' && event.subtype === 'task_progress') {
534
- yield {
535
- type: 'task_progress',
536
- summary: event.summary,
537
- toolUses: event.tool_uses,
538
- durationMs: event.duration_ms,
539
- };
540
- }
541
- // system: session_state_changed → state_changed
542
- if (event.type === 'system' && event.subtype === 'session_state_changed') {
543
- yield { type: 'state_changed', state: event.state };
544
- }
545
- // assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
546
- if (event.type === 'assistant' && event.message?.content) {
547
- const msgId = event.message.id;
548
- if (!msgId || !seenMessageIds.has(msgId)) {
549
- if (msgId)
550
- seenMessageIds.add(msgId);
551
- turnCount++;
695
+ try {
696
+ for await (const event of sdkStream) {
697
+ // 提取 session_id(任意 SDK 事件都可能携带)
698
+ if (event.session_id && event.session_id !== lastSessionId) {
699
+ lastSessionId = event.session_id;
700
+ this.updateSessionId(sessionId, event.session_id);
701
+ yield { type: 'session_id', sessionId: event.session_id };
552
702
  }
553
- // 统计本轮 base agent 全部输出字符数(text + tool_use input)
554
- let turnOutputChars = 0;
555
- for (const content of event.message.content) {
556
- if (content.type === 'tool_use') {
557
- const inputStr = typeof content.input === 'string' ? content.input : JSON.stringify(content.input || '');
558
- turnOutputChars += inputStr.length;
559
- }
560
- else if (content.type === 'text' && content.text) {
561
- turnOutputChars += content.text.length;
562
- }
703
+ // system: compact_boundary compact
704
+ if (event.type === 'system' && event.subtype === 'compact_boundary') {
705
+ yield {
706
+ type: 'compact',
707
+ preTokens: event.compact_metadata?.pre_tokens || 0,
708
+ postTokens: event.compact_metadata?.post_tokens,
709
+ durationMs: event.compact_metadata?.duration_ms,
710
+ };
563
711
  }
564
- for (const content of event.message.content) {
565
- if (content.type === 'tool_use') {
566
- if (content.id)
567
- toolUseNames.set(content.id, content.name);
568
- yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id, turn: turnCount, outputTokens: turnOutputChars };
712
+ // system: task_progress task_progress
713
+ if (event.type === 'system' && event.subtype === 'task_progress') {
714
+ yield {
715
+ type: 'task_progress',
716
+ summary: event.summary,
717
+ toolUses: event.tool_uses,
718
+ durationMs: event.duration_ms,
719
+ };
720
+ }
721
+ // system: session_state_changed → state_changed
722
+ if (event.type === 'system' && event.subtype === 'session_state_changed') {
723
+ yield { type: 'state_changed', state: event.state };
724
+ }
725
+ // assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
726
+ if (event.type === 'assistant' && event.message?.content) {
727
+ const msgId = event.message.id;
728
+ if (!msgId || !seenMessageIds.has(msgId)) {
729
+ if (msgId)
730
+ seenMessageIds.add(msgId);
731
+ turnCount++;
569
732
  }
570
- else if (content.type === 'text' && content.text) {
571
- yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount };
733
+ // 统计本轮 base agent 全部输出字符数(text + tool_use input)
734
+ let turnOutputChars = 0;
735
+ for (const content of event.message.content) {
736
+ if (content.type === 'tool_use') {
737
+ const inputStr = typeof content.input === 'string' ? content.input : JSON.stringify(content.input || '');
738
+ turnOutputChars += inputStr.length;
739
+ }
740
+ else if (content.type === 'text' && content.text) {
741
+ turnOutputChars += content.text.length;
742
+ }
743
+ }
744
+ for (const content of event.message.content) {
745
+ if (content.type === 'tool_use') {
746
+ if (content.id)
747
+ toolUseNames.set(content.id, content.name);
748
+ yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id, turn: turnCount, outputTokens: turnOutputChars };
749
+ }
750
+ else if (content.type === 'text' && content.text) {
751
+ yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount };
752
+ }
572
753
  }
573
754
  }
574
- }
575
- // user: 提取 tool_result 块(SDK 将工具结果嵌套在 SDKUserMessage 中)
576
- if (event.type === 'user' && event.message?.content) {
577
- const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
578
- for (const block of contentArray) {
579
- if (typeof block === 'object' && block !== null && block.type === 'tool_result') {
580
- const toolName = toolUseNames.get(block.tool_use_id) || '';
581
- const resultContent = typeof block.content === 'string'
582
- ? block.content
583
- : block.content != null ? JSON.stringify(block.content) : '';
584
- yield {
585
- type: 'tool_result',
586
- name: toolName,
587
- result: resultContent,
588
- isError: block.is_error === true,
589
- error: block.is_error === true ? resultContent : undefined,
590
- callId: block.tool_use_id,
591
- };
755
+ // user: 提取 tool_result 块(SDK 将工具结果嵌套在 SDKUserMessage 中)
756
+ if (event.type === 'user' && event.message?.content) {
757
+ const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
758
+ for (const block of contentArray) {
759
+ if (typeof block === 'object' && block !== null && block.type === 'tool_result') {
760
+ const toolName = toolUseNames.get(block.tool_use_id) || '';
761
+ const resultContent = typeof block.content === 'string'
762
+ ? block.content
763
+ : block.content != null ? JSON.stringify(block.content) : '';
764
+ yield {
765
+ type: 'tool_result',
766
+ name: toolName,
767
+ result: resultContent,
768
+ isError: block.is_error === true,
769
+ error: block.is_error === true ? resultContent : undefined,
770
+ callId: block.tool_use_id,
771
+ };
772
+ }
592
773
  }
593
774
  }
594
- }
595
- // result complete(含 permission_denials 提取)
596
- if (event.type === 'result') {
597
- // 先发出被拒绝的权限事件
598
- if (Array.isArray(event.permission_denials)) {
599
- for (const denial of event.permission_denials) {
600
- yield {
601
- type: 'tool_result',
602
- name: denial.tool_name || '',
603
- result: '',
604
- isError: true,
605
- error: `权限被拒绝: ${denial.tool_name}`,
606
- };
775
+ // result → complete(含 permission_denials 提取)
776
+ if (event.type === 'result') {
777
+ // 先发出被拒绝的权限事件
778
+ if (Array.isArray(event.permission_denials)) {
779
+ for (const denial of event.permission_denials) {
780
+ yield {
781
+ type: 'tool_result',
782
+ name: denial.tool_name || '',
783
+ result: '',
784
+ isError: true,
785
+ error: `权限被拒绝: ${denial.tool_name}`,
786
+ };
787
+ }
607
788
  }
789
+ // 剥离 SDK result 中混入的 <thinking>...</thinking> 块
790
+ const cleanResult = typeof event.result === 'string'
791
+ ? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
792
+ : event.result;
793
+ // 从 usage 三项求和得到当前上下文占用(与 claude-hud getTotalTokens 相同算法)
794
+ const u = event.usage;
795
+ const totalTokens = u
796
+ ? (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
797
+ : 0;
798
+ const maxTokens = sdkModel ? contextWindowFor(sdkModel) : 200000;
799
+ const contextUsage = totalTokens > 0 ? {
800
+ totalTokens,
801
+ maxTokens,
802
+ percentage: Math.round((totalTokens / maxTokens) * 100),
803
+ model: callModel ?? this.model,
804
+ effort: callEffort ?? this.effort,
805
+ } : undefined;
806
+ yield {
807
+ type: 'complete',
808
+ result: cleanResult,
809
+ subtype: event.subtype,
810
+ isError: event.is_error,
811
+ errors: event.errors,
812
+ durationMs: event.duration_ms,
813
+ ttftMs: event.ttft_ms,
814
+ costUsd: event.total_cost_usd,
815
+ terminalReason: event.terminal_reason,
816
+ sessionTitle: event.session_title,
817
+ numTurns: event.num_turns,
818
+ tokenUsage: event.usage,
819
+ contextUsage,
820
+ };
821
+ // result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
822
+ return;
608
823
  }
609
- // 剥离 SDK result 中混入的 <thinking>...</thinking> 块
610
- const cleanResult = typeof event.result === 'string'
611
- ? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
612
- : event.result;
613
- yield {
614
- type: 'complete',
615
- result: cleanResult,
616
- subtype: event.subtype,
617
- isError: event.is_error,
618
- errors: event.errors,
619
- durationMs: event.duration_ms,
620
- costUsd: event.total_cost_usd,
621
- terminalReason: event.terminal_reason,
622
- sessionTitle: event.session_title,
623
- numTurns: event.num_turns,
624
- usage: event.usage,
625
- };
626
- // result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
627
- return;
628
824
  }
629
825
  }
826
+ catch (err) {
827
+ // 子进程崩溃(如 exited with code 1)时,把缓冲的 stderr 打出来还原真实原因。
828
+ // SDK 包装后的错误信息不含子进程实际报错,缓冲区才是根因所在。
829
+ const buf = this.recentStderr.get(sessionId);
830
+ if (buf && buf.length > 0) {
831
+ logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}). Last ${buf.length} stderr line(s):\n${buf.join('\n')}`);
832
+ }
833
+ else {
834
+ logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}) with no captured stderr.`);
835
+ }
836
+ throw err;
837
+ }
838
+ finally {
839
+ this.recentStderr.delete(sessionId);
840
+ }
630
841
  }
631
- async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager) {
842
+ async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager, modelOverride) {
632
843
  // 记录当前 evolclaw session ID,用于 Agent ctl 环境变量注入
633
844
  this.currentEvolclawSessionId = sessionId;
634
845
  // 同步用户级配置到内存
635
846
  this.syncFromUserSettings();
847
+ // 异步刷新模型别名缓存(fire-and-forget,不阻塞查询)
848
+ if (this.baseUrl) {
849
+ const cached = modelAliasCache.get(this.baseUrl);
850
+ if (!cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS)) {
851
+ refreshModelAliases(this.baseUrl, this.apiKey);
852
+ }
853
+ }
636
854
  ensureDir(projectPath);
637
855
  ensureDir(path.join(projectPath, '.claude'));
638
856
  // 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
@@ -795,19 +1013,24 @@ export class AgentRunner {
795
1013
  const excludeDynamic = this.config?.agents?.claude?.excludeDynamicSections === true;
796
1014
  // 公共 options(新旧模式共用)
797
1015
  const sdkPermissionMode = this.toSdkPermissionMode();
798
- logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode}`);
1016
+ // 本次调用使用的模型/强度:优先 modelOverride(message-processor 关系>agent>全局 解析后传入),
1017
+ // 缺省回落 agent 级 this.model。作为 per-call 入参传入,无共享状态,多对端并发互不污染。
1018
+ const callModel = modelOverride?.model || this.model;
1019
+ const callEffort = (modelOverride?.effort ?? this.effort);
1020
+ logger.info(`[AgentRunner] runQuery model=${callModel} effort=${callEffort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode}`);
799
1021
  if (systemPromptAppend) {
800
1022
  logger.info(`[AgentRunner] systemPromptAppend: ${systemPromptAppend.length} chars`);
801
1023
  }
802
1024
  else {
803
1025
  logger.info(`[AgentRunner] systemPromptAppend: none`);
804
1026
  }
1027
+ const sdkModel = resolveSdkModel(callModel, this.baseUrl);
805
1028
  const commonOptions = {
806
1029
  cwd: projectPath,
807
- model: this.model,
808
- ...(this.effort ? { effort: this.effort } : {}),
1030
+ model: sdkModel,
1031
+ ...(callEffort ? { effort: callEffort } : {}),
809
1032
  ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
810
- autoCompactWindow: 200000,
1033
+ autoCompactWindow: contextWindowFor(sdkModel),
811
1034
  advisorModel: 'haiku',
812
1035
  canUseTool: canUseToolCallback,
813
1036
  permissionMode: sdkPermissionMode,
@@ -820,11 +1043,23 @@ export class AgentRunner {
820
1043
  },
821
1044
  ...(enableSummaries ? { agentProgressSummaries: true } : {}),
822
1045
  stderr: (msg) => {
1046
+ const trimmed = msg.trim();
1047
+ if (trimmed) {
1048
+ // 环形缓冲:保留最近 N 行,供子进程崩溃时还原真实原因
1049
+ let buf = this.recentStderr.get(sessionId);
1050
+ if (!buf) {
1051
+ buf = [];
1052
+ this.recentStderr.set(sessionId, buf);
1053
+ }
1054
+ buf.push(trimmed);
1055
+ if (buf.length > AgentRunner.STDERR_BUFFER_MAX)
1056
+ buf.shift();
1057
+ }
823
1058
  if (msg.includes('[ERROR]') || msg.includes('[WARN]') || msg.includes('Stream started')) {
824
- logger.info(`[Claude-stderr] ${msg.trim()}`);
1059
+ logger.info(`[Claude-stderr] ${trimmed}`);
825
1060
  }
826
1061
  else {
827
- logger.debug(`[Claude-stderr] ${msg.trim()}`);
1062
+ logger.debug(`[Claude-stderr] ${trimmed}`);
828
1063
  }
829
1064
  },
830
1065
  env: this.getAgentEnv()
@@ -920,7 +1155,7 @@ export class AgentRunner {
920
1155
  }
921
1156
  let sdkStream;
922
1157
  if (images && images.length > 0) {
923
- logger.debug('[AgentRunner] Creating query with images, images:', images.length);
1158
+ logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
924
1159
  logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
925
1160
  const stream = new MessageStream();
926
1161
  stream.push(prompt, images);
@@ -936,7 +1171,7 @@ export class AgentRunner {
936
1171
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
937
1172
  }
938
1173
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
939
- return this.transformStream(sdkStream, sessionId);
1174
+ return this.transformStream(sdkStream, sessionId, callModel, callEffort, sdkModel);
940
1175
  }
941
1176
  async interrupt(sessionId) {
942
1177
  const fn = this.interruptFns.get(sessionId);
@@ -961,6 +1196,7 @@ export class AgentRunner {
961
1196
  cleanupStream(sessionId) {
962
1197
  this.activeStreams.delete(sessionId);
963
1198
  this.interruptFns.delete(sessionId);
1199
+ this.recentStderr.delete(sessionId);
964
1200
  }
965
1201
  updateSessionId(sessionId, agentSessionId) {
966
1202
  logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
@@ -974,7 +1210,7 @@ export class AgentRunner {
974
1210
  prompt,
975
1211
  options: {
976
1212
  cwd: projectPath,
977
- model: this.model,
1213
+ model: resolveSdkModel(this.model, this.baseUrl),
978
1214
  resume: agentSessionId,
979
1215
  maxTurns: 1,
980
1216
  permissionMode: this.toSdkPermissionMode(),
@@ -1060,6 +1296,7 @@ export class AgentRunner {
1060
1296
  enableFileCheckpointing: true,
1061
1297
  permissionMode: this.toSdkPermissionMode(),
1062
1298
  stderr: (data) => { stderrChunks.push(data); },
1299
+ env: this.getAgentEnv(),
1063
1300
  }
1064
1301
  });
1065
1302
  try {