evolclaw 3.1.3 → 3.1.5

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 (100) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/assets/.env.template +4 -0
  3. package/assets/config.json.template +6 -0
  4. package/assets/wechat-group-qr.jpeg +0 -0
  5. package/dist/agents/claude-runner.js +348 -156
  6. package/dist/agents/kit-renderer.js +211 -42
  7. package/dist/aun/aid/agentmd.js +75 -139
  8. package/dist/aun/aid/client.js +1 -14
  9. package/dist/aun/aid/identity.js +381 -54
  10. package/dist/aun/aid/index.js +3 -2
  11. package/dist/aun/aid/store.js +74 -0
  12. package/dist/aun/msg/p2p.js +26 -2
  13. package/dist/aun/rpc/connection.js +23 -35
  14. package/dist/channels/aun.js +92 -144
  15. package/dist/channels/dingtalk.js +1 -0
  16. package/dist/channels/feishu.js +270 -190
  17. package/dist/channels/qqbot.js +1 -0
  18. package/dist/channels/wechat.js +1 -0
  19. package/dist/channels/wecom.js +1 -0
  20. package/dist/cli/agent.js +26 -27
  21. package/dist/cli/bench.js +45 -34
  22. package/dist/cli/help.js +23 -0
  23. package/dist/cli/index.js +538 -77
  24. package/dist/cli/init-channel.js +7 -4
  25. package/dist/cli/link-rules.js +2 -1
  26. package/dist/cli/model.js +324 -0
  27. package/dist/cli/net-check.js +138 -56
  28. package/dist/cli/watch-msg.js +7 -7
  29. package/dist/cli/watch-web/debug-log.js +18 -0
  30. package/dist/cli/watch-web/server.js +306 -0
  31. package/dist/cli/watch-web/sources/aid.js +63 -0
  32. package/dist/cli/watch-web/sources/msg.js +70 -0
  33. package/dist/cli/watch-web/sources/session.js +638 -0
  34. package/dist/cli/watch-web/sources/types.js +10 -0
  35. package/dist/cli/watch-web/static/app.js +546 -0
  36. package/dist/cli/watch-web/static/index.html +54 -0
  37. package/dist/cli/watch-web/static/style.css +247 -0
  38. package/dist/core/channel-loader.js +7 -4
  39. package/dist/core/command-handler.js +87 -93
  40. package/dist/core/evolagent-registry.js +1 -1
  41. package/dist/core/evolagent.js +4 -4
  42. package/dist/core/interaction-router.js +59 -0
  43. package/dist/core/message/message-bridge.js +6 -6
  44. package/dist/core/message/message-log.js +2 -2
  45. package/dist/core/message/message-processor.js +104 -118
  46. package/dist/core/message/stream-idle-monitor.js +21 -0
  47. package/dist/core/model/model-catalog.js +215 -0
  48. package/dist/core/model/model-scope.js +250 -0
  49. package/dist/core/relation/peer-identity.js +78 -44
  50. package/dist/core/relation/peer-key.js +16 -0
  51. package/dist/core/session/session-fs-store.js +34 -55
  52. package/dist/core/session/session-key.js +24 -0
  53. package/dist/core/session/session-manager.js +312 -251
  54. package/dist/core/session/session-mapper.js +9 -4
  55. package/dist/core/trigger/manager.js +37 -0
  56. package/dist/core/trigger/scheduler.js +2 -1
  57. package/dist/index.js +10 -3
  58. package/dist/ipc.js +22 -0
  59. package/dist/paths.js +87 -16
  60. package/dist/utils/npm-ops.js +18 -11
  61. package/kits/docs/GUIDE.md +2 -2
  62. package/kits/docs/INDEX.md +11 -7
  63. package/kits/docs/channels/aun.md +56 -17
  64. package/kits/docs/channels/feishu.md +41 -12
  65. package/kits/docs/context-assembly.md +181 -0
  66. package/kits/docs/evolclaw/agent.md +49 -0
  67. package/kits/docs/evolclaw/aid.md +49 -0
  68. package/kits/docs/evolclaw/ctl.md +46 -0
  69. package/kits/docs/evolclaw/group.md +82 -0
  70. package/kits/docs/evolclaw/msg.md +86 -0
  71. package/kits/docs/evolclaw/rpc.md +35 -0
  72. package/kits/docs/evolclaw/storage.md +49 -0
  73. package/kits/docs/venues/aun-group.md +10 -0
  74. package/kits/docs/venues/aun-private.md +10 -0
  75. package/kits/docs/venues/client-desktop.md +10 -0
  76. package/kits/docs/venues/client-mobile.md +10 -0
  77. package/kits/docs/venues/feishu-group.md +13 -0
  78. package/kits/docs/venues/feishu-private.md +9 -0
  79. package/kits/docs/venues/group.md +11 -0
  80. package/kits/docs/venues/private.md +10 -0
  81. package/kits/eck_manifest.json +75 -39
  82. package/kits/rules/01-overview.md +20 -10
  83. package/kits/rules/05-venue.md +2 -2
  84. package/kits/rules/06-channel.md +30 -27
  85. package/kits/templates/system-fragments/baseagent.md +7 -1
  86. package/kits/templates/system-fragments/channel.md +4 -1
  87. package/kits/templates/system-fragments/identity.md +4 -4
  88. package/kits/templates/system-fragments/relation.md +8 -5
  89. package/kits/templates/system-fragments/session.md +27 -0
  90. package/kits/templates/system-fragments/venue.md +13 -1
  91. package/package.json +13 -6
  92. package/dist/aun/aid/lifecycle-log.js +0 -33
  93. package/dist/net-check.js +0 -640
  94. package/dist/utils/aid-lifecycle-log.js +0 -33
  95. package/dist/watch-msg.js +0 -544
  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
  100. package/kits/templates/system-fragments/eckruntime.md +0 -14
@@ -10,6 +10,93 @@ 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 = 60 * 60 * 1000; // 1h
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 (Object.keys(aliases).length > 0) {
74
+ modelAliasCache.set(baseUrl, { aliases, fetchedAt: Date.now() });
75
+ logger.info(`[AgentRunner] Refreshed model aliases from ${url}: ${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
+ }
13
100
  class MessageStream {
14
101
  queue = [];
15
102
  waiting = null;
@@ -89,6 +176,9 @@ export class AgentRunner {
89
176
  permissionContexts = new Map();
90
177
  currentEvolclawSessionId;
91
178
  claudeExecutablePath;
179
+ /** 每个 session 最近的子进程 stderr 行(环形缓冲),用于子进程崩溃时还原真正原因 */
180
+ recentStderr = new Map();
181
+ static STDERR_BUFFER_MAX = 80;
92
182
  constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
93
183
  this.apiKey = apiKey;
94
184
  this.model = model || 'sonnet';
@@ -102,11 +192,17 @@ export class AgentRunner {
102
192
  }
103
193
  }
104
194
  getAgentEnv() {
195
+ // SDK 0.3.x 起,CLI 在以 root 运行时会拒绝 --dangerously-skip-permissions
196
+ // (bypassPermissions 模式映射而来),报错 "cannot be used with root/sudo privileges"
197
+ // 并以 code 1 退出。IS_SANDBOX=1 是 CLI 提供的 root 守卫豁免开关。
198
+ // 仅在以 root 运行时注入,非 root 部署行为不变。
199
+ const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
105
200
  return {
106
201
  ...process.env,
107
202
  ANTHROPIC_AUTH_TOKEN: this.apiKey,
108
203
  PATH: process.env.PATH,
109
204
  DISABLE_AUTOUPDATER: '1',
205
+ ...(isRoot ? { IS_SANDBOX: '1' } : {}),
110
206
  ...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {}),
111
207
  ...(this.currentEvolclawSessionId ? { EVOLCLAW_SESSION_ID: this.currentEvolclawSessionId } : {}),
112
208
  };
@@ -118,7 +214,20 @@ export class AgentRunner {
118
214
  return this.model;
119
215
  }
120
216
  listModels() {
121
- return ['opus', 'sonnet', 'haiku'];
217
+ // 触发异步刷新(不阻塞)
218
+ if (this.baseUrl)
219
+ refreshModelAliases(this.baseUrl, this.apiKey);
220
+ // 有缓存时返回完整 ID 列表,否则返回短别名
221
+ if (this.baseUrl) {
222
+ const cached = modelAliasCache.get(this.baseUrl);
223
+ if (cached)
224
+ return Object.values(cached.aliases);
225
+ }
226
+ return Object.values(STATIC_MODEL_ALIASES);
227
+ }
228
+ /** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
229
+ resolveModelId(model) {
230
+ return resolveModelAlias(model, this.baseUrl);
122
231
  }
123
232
  setEffort(effort) {
124
233
  this.effort = effort;
@@ -157,7 +266,7 @@ export class AgentRunner {
157
266
  toSdkPermissionMode() {
158
267
  const map = {
159
268
  'auto': 'auto', // AI 分类器自动判断
160
- 'bypass': 'default', // 全部自动放行(通过 canUseTool 一律 allow,保留 hook 安全检查)
269
+ 'bypass': 'bypassPermissions', // 全部自动放行(SDK 跳过分类器,canUseTool 仍保留 hook 安全检查)
161
270
  'request': 'default', // 部分自动,部分询问
162
271
  'edit': 'acceptEdits',
163
272
  'plan': 'plan',
@@ -213,49 +322,70 @@ export class AgentRunner {
213
322
  if (!adapterHasInteractionPath) {
214
323
  return this.handleAskUserQuestionFallback(sessionId, input, questions);
215
324
  }
325
+ // 立即暂停 idle 监控,不等卡片发完再 register
326
+ permCtx.interactionRouter?.markWaiting(sessionId);
327
+ let waitMarked = true;
216
328
  const answers = {};
217
- // 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
218
- // 注意:sendPromptFn 是全局单例,多 channel 并发时会被覆盖,导致提示发到错误 channel
219
329
  const sendPrompt = permCtx.adapter && permCtx.channelId
220
330
  ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
221
331
  : this.sendPromptFn;
222
- // 逐个 question 发送卡片并等待用户选择
223
332
  for (let i = 0; i < questions.length; i++) {
224
333
  const q = questions[i];
225
334
  const requestId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
226
335
  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
- });
336
+ let interaction;
337
+ if (q.multiSelect) {
338
+ // 多选:使用 checkers + form 提交(JSON 2.0 CardKit 路径)
339
+ interaction = {
340
+ type: 'interaction',
341
+ id: requestId,
342
+ kind: {
343
+ kind: 'action',
344
+ title: cardTitle,
345
+ body: q.question,
346
+ checkers: q.options.map(opt => ({
347
+ key: opt.label,
348
+ label: opt.label,
349
+ description: opt.description,
350
+ })),
351
+ buttons: [
352
+ { key: 'submit', label: '✅ 确认选择', style: 'primary' },
353
+ ],
354
+ allowCustomInput: true,
355
+ },
356
+ channelId: permCtx.channelId,
357
+ sessionId,
358
+ expiresAt: Date.now() + 5 * 60 * 1000,
359
+ };
234
360
  }
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 => ({
361
+ else {
362
+ // 单选:保持按钮模式
363
+ const bodyLines = [q.question];
364
+ if (q.options.some(opt => opt.description)) {
365
+ bodyLines.push('');
366
+ q.options.forEach((opt, idx) => {
367
+ bodyLines.push(`${idx + 1}. **${opt.label}**${opt.description ? ` — ${opt.description}` : ''}`);
368
+ });
369
+ }
370
+ interaction = {
371
+ type: 'interaction',
372
+ id: requestId,
373
+ kind: {
374
+ kind: 'action',
375
+ title: cardTitle,
376
+ body: bodyLines.join('\n'),
377
+ buttons: q.options.map(opt => ({
244
378
  key: opt.label,
245
379
  label: opt.label,
246
380
  style: 'default',
247
381
  })),
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
- };
382
+ allowCustomInput: true,
383
+ },
384
+ channelId: permCtx.channelId,
385
+ sessionId,
386
+ expiresAt: Date.now() + 5 * 60 * 1000,
387
+ };
388
+ }
259
389
  let cardSent = false;
260
390
  try {
261
391
  const envelope = buildEnvelope({
@@ -275,7 +405,6 @@ export class AgentRunner {
275
405
  logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
276
406
  }
277
407
  if (!cardSent) {
278
- // 卡片发送失败,以纯文本展示选项并自动选推荐项
279
408
  const firstLabel = q.options[0]?.label || '';
280
409
  answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
281
410
  if (sendPrompt) {
@@ -284,35 +413,37 @@ export class AgentRunner {
284
413
  }
285
414
  continue;
286
415
  }
287
- // 等待用户交互
416
+ // 等待用户交互(unmark 占位,register 接管计数)
417
+ if (waitMarked) {
418
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
419
+ waitMarked = false;
420
+ }
288
421
  const answer = await new Promise((resolve) => {
289
422
  permCtx?.interactionRouter?.register(requestId, sessionId, (action, values) => {
290
423
  if (action === 'cancel') {
291
424
  resolve(null);
292
425
  }
293
- else if (action === '_custom_input' && permCtx.interceptNextMessage) {
294
- // "手动输入":发提示,拦截下一条消息
295
- const sendHint = async () => {
296
- if (sendPrompt) {
297
- await sendPrompt('✏️ 请输入你的想法,回复后继续……');
426
+ else if (action === '_custom_input') {
427
+ // 用户通过追加的 input 提交了自定义文本
428
+ const customText = values?.custom_text;
429
+ resolve(typeof customText === 'string' && customText.trim() ? customText.trim() : null);
430
+ }
431
+ else if (action === 'submit' && q.multiSelect && values) {
432
+ // checker 多选提交:从 form_value 收集 checked 选项
433
+ const selected = [];
434
+ q.options.forEach((opt, idx) => {
435
+ if (values[`opt_${idx}`] === true) {
436
+ selected.push(opt.label);
298
437
  }
299
- };
300
- sendHint().catch(() => { });
301
- permCtx.interceptNextMessage(sessionId, (msg) => {
302
- resolve(msg.content || null);
303
438
  });
304
- }
305
- else if (q.multiSelect) {
306
- // multiSelect 按钮点击:包装为数组
307
- resolve([action]);
439
+ resolve(selected.length > 0 ? selected : null);
308
440
  }
309
441
  else {
310
- resolve(action); // action = button key = option label
442
+ resolve(action);
311
443
  }
312
444
  });
313
445
  });
314
446
  if (answer === null) {
315
- // 取消,自动选第一项
316
447
  const firstLabel = q.options[0]?.label || '';
317
448
  answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
318
449
  }
@@ -320,6 +451,9 @@ export class AgentRunner {
320
451
  answers[q.question] = answer;
321
452
  }
322
453
  }
454
+ if (waitMarked) {
455
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
456
+ }
323
457
  const updatedInput = { ...input, answers };
324
458
  return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
325
459
  }
@@ -395,6 +529,8 @@ export class AgentRunner {
395
529
  if (!permCtx?.channelId || !sendPrompt) {
396
530
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
397
531
  }
532
+ // 立即暂停 idle 监控,不等卡片发完再 register
533
+ permCtx.interactionRouter?.markWaiting(sessionId);
398
534
  // 尝试发送交互卡片
399
535
  let cardSent = false;
400
536
  if (permCtx.adapter?.send) {
@@ -429,6 +565,7 @@ export class AgentRunner {
429
565
  { key: 'approve', label: '✅ 批准执行', style: 'primary' },
430
566
  { key: 'reject', label: '❌ 拒绝', style: 'danger' },
431
567
  ],
568
+ allowCustomInput: true,
432
569
  },
433
570
  channelId: permCtx.channelId,
434
571
  sessionId,
@@ -455,10 +592,15 @@ export class AgentRunner {
455
592
  logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
456
593
  }
457
594
  if (cardSent) {
595
+ permCtx.interactionRouter?.unmarkWaiting(sessionId);
458
596
  return new Promise((resolve) => {
459
- permCtx.interactionRouter?.register(requestId, sessionId, (action) => {
597
+ permCtx.interactionRouter?.register(requestId, sessionId, (action, values) => {
460
598
  const trimmed = action.trim();
461
- if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝' || trimmed === 'reject') {
599
+ if (trimmed === '_custom_input') {
600
+ const feedback = typeof values?.custom_text === 'string' ? values.custom_text.trim() : '';
601
+ resolve({ behavior: 'deny', message: feedback || '用户提交了反馈', decisionClassification: 'user_reject' });
602
+ }
603
+ else if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
462
604
  resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
463
605
  }
464
606
  else {
@@ -492,6 +634,7 @@ export class AgentRunner {
492
634
  },
493
635
  };
494
636
  await sendPrompt(renderActionAsText(fallbackInteraction));
637
+ permCtx.interactionRouter.unmarkWaiting(sessionId);
495
638
  return new Promise((resolve) => {
496
639
  permCtx.interactionRouter.register(fallbackRequestId, sessionId, (action) => {
497
640
  const trimmed = action.trim();
@@ -505,6 +648,7 @@ export class AgentRunner {
505
648
  });
506
649
  }
507
650
  // 无交互能力,发提示后直接 allow
651
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
508
652
  await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
509
653
  return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
510
654
  }
@@ -518,121 +662,151 @@ export class AgentRunner {
518
662
  const toolUseNames = new Map();
519
663
  let turnCount = 0;
520
664
  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++;
665
+ try {
666
+ for await (const event of sdkStream) {
667
+ // 提取 session_id(任意 SDK 事件都可能携带)
668
+ if (event.session_id && event.session_id !== lastSessionId) {
669
+ lastSessionId = event.session_id;
670
+ this.updateSessionId(sessionId, event.session_id);
671
+ yield { type: 'session_id', sessionId: event.session_id };
552
672
  }
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
- }
673
+ // system: compact_boundary compact
674
+ if (event.type === 'system' && event.subtype === 'compact_boundary') {
675
+ yield {
676
+ type: 'compact',
677
+ preTokens: event.compact_metadata?.pre_tokens || 0,
678
+ postTokens: event.compact_metadata?.post_tokens,
679
+ durationMs: event.compact_metadata?.duration_ms,
680
+ };
681
+ }
682
+ // system: task_progress → task_progress
683
+ if (event.type === 'system' && event.subtype === 'task_progress') {
684
+ yield {
685
+ type: 'task_progress',
686
+ summary: event.summary,
687
+ toolUses: event.tool_uses,
688
+ durationMs: event.duration_ms,
689
+ };
690
+ }
691
+ // system: session_state_changed → state_changed
692
+ if (event.type === 'system' && event.subtype === 'session_state_changed') {
693
+ yield { type: 'state_changed', state: event.state };
563
694
  }
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 };
695
+ // assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
696
+ if (event.type === 'assistant' && event.message?.content) {
697
+ const msgId = event.message.id;
698
+ if (!msgId || !seenMessageIds.has(msgId)) {
699
+ if (msgId)
700
+ seenMessageIds.add(msgId);
701
+ turnCount++;
569
702
  }
570
- else if (content.type === 'text' && content.text) {
571
- yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount };
703
+ // 统计本轮 base agent 全部输出字符数(text + tool_use input)
704
+ let turnOutputChars = 0;
705
+ for (const content of event.message.content) {
706
+ if (content.type === 'tool_use') {
707
+ const inputStr = typeof content.input === 'string' ? content.input : JSON.stringify(content.input || '');
708
+ turnOutputChars += inputStr.length;
709
+ }
710
+ else if (content.type === 'text' && content.text) {
711
+ turnOutputChars += content.text.length;
712
+ }
713
+ }
714
+ for (const content of event.message.content) {
715
+ if (content.type === 'tool_use') {
716
+ if (content.id)
717
+ toolUseNames.set(content.id, content.name);
718
+ yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id, turn: turnCount, outputTokens: turnOutputChars };
719
+ }
720
+ else if (content.type === 'text' && content.text) {
721
+ yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount };
722
+ }
572
723
  }
573
724
  }
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
- };
725
+ // user: 提取 tool_result 块(SDK 将工具结果嵌套在 SDKUserMessage 中)
726
+ if (event.type === 'user' && event.message?.content) {
727
+ const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
728
+ for (const block of contentArray) {
729
+ if (typeof block === 'object' && block !== null && block.type === 'tool_result') {
730
+ const toolName = toolUseNames.get(block.tool_use_id) || '';
731
+ const resultContent = typeof block.content === 'string'
732
+ ? block.content
733
+ : block.content != null ? JSON.stringify(block.content) : '';
734
+ yield {
735
+ type: 'tool_result',
736
+ name: toolName,
737
+ result: resultContent,
738
+ isError: block.is_error === true,
739
+ error: block.is_error === true ? resultContent : undefined,
740
+ callId: block.tool_use_id,
741
+ };
742
+ }
592
743
  }
593
744
  }
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
- };
745
+ // result → complete(含 permission_denials 提取)
746
+ if (event.type === 'result') {
747
+ // 先发出被拒绝的权限事件
748
+ if (Array.isArray(event.permission_denials)) {
749
+ for (const denial of event.permission_denials) {
750
+ yield {
751
+ type: 'tool_result',
752
+ name: denial.tool_name || '',
753
+ result: '',
754
+ isError: true,
755
+ error: `权限被拒绝: ${denial.tool_name}`,
756
+ };
757
+ }
607
758
  }
759
+ // 剥离 SDK result 中混入的 <thinking>...</thinking> 块
760
+ const cleanResult = typeof event.result === 'string'
761
+ ? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
762
+ : event.result;
763
+ yield {
764
+ type: 'complete',
765
+ result: cleanResult,
766
+ subtype: event.subtype,
767
+ isError: event.is_error,
768
+ errors: event.errors,
769
+ durationMs: event.duration_ms,
770
+ ttftMs: event.ttft_ms,
771
+ costUsd: event.total_cost_usd,
772
+ terminalReason: event.terminal_reason,
773
+ sessionTitle: event.session_title,
774
+ numTurns: event.num_turns,
775
+ usage: event.usage,
776
+ };
777
+ // result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
778
+ return;
608
779
  }
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
780
  }
629
781
  }
782
+ catch (err) {
783
+ // 子进程崩溃(如 exited with code 1)时,把缓冲的 stderr 打出来还原真实原因。
784
+ // SDK 包装后的错误信息不含子进程实际报错,缓冲区才是根因所在。
785
+ const buf = this.recentStderr.get(sessionId);
786
+ if (buf && buf.length > 0) {
787
+ logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}). Last ${buf.length} stderr line(s):\n${buf.join('\n')}`);
788
+ }
789
+ else {
790
+ logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}) with no captured stderr.`);
791
+ }
792
+ throw err;
793
+ }
794
+ finally {
795
+ this.recentStderr.delete(sessionId);
796
+ }
630
797
  }
631
- async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager) {
798
+ async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager, modelOverride) {
632
799
  // 记录当前 evolclaw session ID,用于 Agent ctl 环境变量注入
633
800
  this.currentEvolclawSessionId = sessionId;
634
801
  // 同步用户级配置到内存
635
802
  this.syncFromUserSettings();
803
+ // 异步刷新模型别名缓存(fire-and-forget,不阻塞查询)
804
+ if (this.baseUrl) {
805
+ const cached = modelAliasCache.get(this.baseUrl);
806
+ if (!cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS)) {
807
+ refreshModelAliases(this.baseUrl, this.apiKey);
808
+ }
809
+ }
636
810
  ensureDir(projectPath);
637
811
  ensureDir(path.join(projectPath, '.claude'));
638
812
  // 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
@@ -795,7 +969,11 @@ export class AgentRunner {
795
969
  const excludeDynamic = this.config?.agents?.claude?.excludeDynamicSections === true;
796
970
  // 公共 options(新旧模式共用)
797
971
  const sdkPermissionMode = this.toSdkPermissionMode();
798
- logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode}`);
972
+ // 本次调用使用的模型/强度:优先 modelOverride(message-processor 关系>agent>全局 解析后传入),
973
+ // 缺省回落 agent 级 this.model。作为 per-call 入参传入,无共享状态,多对端并发互不污染。
974
+ const callModel = modelOverride?.model || this.model;
975
+ const callEffort = (modelOverride?.effort ?? this.effort);
976
+ logger.info(`[AgentRunner] runQuery model=${callModel} effort=${callEffort ?? 'auto'} permMode=${this.permissionMode} sdkMode=${sdkPermissionMode}`);
799
977
  if (systemPromptAppend) {
800
978
  logger.info(`[AgentRunner] systemPromptAppend: ${systemPromptAppend.length} chars`);
801
979
  }
@@ -804,8 +982,8 @@ export class AgentRunner {
804
982
  }
805
983
  const commonOptions = {
806
984
  cwd: projectPath,
807
- model: this.model,
808
- ...(this.effort ? { effort: this.effort } : {}),
985
+ model: resolveModelAlias(callModel, this.baseUrl),
986
+ ...(callEffort ? { effort: callEffort } : {}),
809
987
  ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
810
988
  autoCompactWindow: 200000,
811
989
  advisorModel: 'haiku',
@@ -820,11 +998,23 @@ export class AgentRunner {
820
998
  },
821
999
  ...(enableSummaries ? { agentProgressSummaries: true } : {}),
822
1000
  stderr: (msg) => {
1001
+ const trimmed = msg.trim();
1002
+ if (trimmed) {
1003
+ // 环形缓冲:保留最近 N 行,供子进程崩溃时还原真实原因
1004
+ let buf = this.recentStderr.get(sessionId);
1005
+ if (!buf) {
1006
+ buf = [];
1007
+ this.recentStderr.set(sessionId, buf);
1008
+ }
1009
+ buf.push(trimmed);
1010
+ if (buf.length > AgentRunner.STDERR_BUFFER_MAX)
1011
+ buf.shift();
1012
+ }
823
1013
  if (msg.includes('[ERROR]') || msg.includes('[WARN]') || msg.includes('Stream started')) {
824
- logger.info(`[Claude-stderr] ${msg.trim()}`);
1014
+ logger.info(`[Claude-stderr] ${trimmed}`);
825
1015
  }
826
1016
  else {
827
- logger.debug(`[Claude-stderr] ${msg.trim()}`);
1017
+ logger.debug(`[Claude-stderr] ${trimmed}`);
828
1018
  }
829
1019
  },
830
1020
  env: this.getAgentEnv()
@@ -961,6 +1151,7 @@ export class AgentRunner {
961
1151
  cleanupStream(sessionId) {
962
1152
  this.activeStreams.delete(sessionId);
963
1153
  this.interruptFns.delete(sessionId);
1154
+ this.recentStderr.delete(sessionId);
964
1155
  }
965
1156
  updateSessionId(sessionId, agentSessionId) {
966
1157
  logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
@@ -974,7 +1165,7 @@ export class AgentRunner {
974
1165
  prompt,
975
1166
  options: {
976
1167
  cwd: projectPath,
977
- model: this.model,
1168
+ model: resolveModelAlias(this.model, this.baseUrl),
978
1169
  resume: agentSessionId,
979
1170
  maxTurns: 1,
980
1171
  permissionMode: this.toSdkPermissionMode(),
@@ -1060,6 +1251,7 @@ export class AgentRunner {
1060
1251
  enableFileCheckpointing: true,
1061
1252
  permissionMode: this.toSdkPermissionMode(),
1062
1253
  stderr: (data) => { stderrChunks.push(data); },
1254
+ env: this.getAgentEnv(),
1063
1255
  }
1064
1256
  });
1065
1257
  try {