evolclaw 3.2.0 → 3.4.0

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 (95) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -4
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -31
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1152 -140
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +58 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/outbox.js +14 -2
  11. package/dist/aun/storage/download.js +1 -1
  12. package/dist/aun/storage/upload.js +13 -1
  13. package/dist/channels/aun.js +869 -358
  14. package/dist/channels/dingtalk.js +77 -140
  15. package/dist/channels/feishu.js +125 -154
  16. package/dist/channels/qqbot.js +75 -138
  17. package/dist/channels/wechat.js +75 -136
  18. package/dist/channels/wecom.js +75 -138
  19. package/dist/cli/agent-command.js +591 -0
  20. package/dist/cli/agent.js +23 -8
  21. package/dist/cli/aun-commands.js +1444 -0
  22. package/dist/cli/ctl-command.js +78 -0
  23. package/dist/cli/daemon-commands.js +2707 -0
  24. package/dist/cli/index.js +23 -4905
  25. package/dist/cli/init.js +33 -6
  26. package/dist/cli/model.js +1 -1
  27. package/dist/cli/restart-monitor.js +539 -0
  28. package/dist/cli/stats.js +558 -0
  29. package/dist/cli/version.js +87 -0
  30. package/dist/cli/watch-logs.js +33 -0
  31. package/dist/cli/watch-msg.js +5 -2
  32. package/dist/config-store.js +12 -6
  33. package/dist/core/channel-loader.js +88 -83
  34. package/dist/core/command/command-handler.js +1189 -0
  35. package/dist/core/command/menu-handler.js +1478 -0
  36. package/dist/core/command/slash-gate.js +142 -0
  37. package/dist/core/command/slash-handler.js +2090 -0
  38. package/dist/core/evolagent-registry.js +82 -0
  39. package/dist/core/evolagent.js +17 -1
  40. package/dist/core/interaction-router.js +8 -0
  41. package/dist/core/message/command-handler-agent-control.js +63 -1
  42. package/dist/core/message/im-renderer.js +91 -51
  43. package/dist/core/message/items-formatter.js +9 -1
  44. package/dist/core/message/message-bridge.js +73 -24
  45. package/dist/core/message/message-log.js +1 -0
  46. package/dist/core/message/message-processor.js +432 -94
  47. package/dist/core/message/message-queue.js +70 -2
  48. package/dist/core/message/pending-hints.js +232 -0
  49. package/dist/core/model/model-catalog.js +1 -1
  50. package/dist/core/model/model-scope.js +2 -2
  51. package/dist/core/permission.js +25 -12
  52. package/dist/core/relation/peer-identity.js +16 -1
  53. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  54. package/dist/core/session/session-manager.js +86 -26
  55. package/dist/core/session/session-title.js +26 -0
  56. package/dist/core/stats/billing.js +151 -0
  57. package/dist/core/stats/budget.js +93 -0
  58. package/dist/core/stats/db.js +334 -0
  59. package/dist/core/stats/eck-vars.js +84 -0
  60. package/dist/core/stats/index.js +10 -0
  61. package/dist/core/stats/normalizer.js +78 -0
  62. package/dist/core/stats/query.js +760 -0
  63. package/dist/core/stats/writer.js +115 -0
  64. package/dist/core/trigger/manager.js +34 -0
  65. package/dist/core/trigger/parser.js +9 -3
  66. package/dist/core/trigger/scheduler.js +20 -17
  67. package/dist/data/error-dict.json +7 -0
  68. package/dist/{agents → eck}/manifest-engine.js +20 -1
  69. package/dist/{agents → eck}/message-renderer.js +24 -1
  70. package/dist/index.js +174 -9
  71. package/dist/ipc.js +116 -1
  72. package/dist/utils/cross-platform.js +58 -5
  73. package/dist/utils/ecweb-launch.js +49 -0
  74. package/dist/utils/ecweb-pair.js +20 -0
  75. package/dist/utils/error-utils.js +18 -5
  76. package/dist/utils/npm-ops.js +38 -8
  77. package/dist/utils/stats.js +77 -6
  78. package/kits/docs/evolclaw/INDEX.md +3 -1
  79. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  80. package/kits/docs/evolclaw/fs.md +131 -0
  81. package/kits/docs/evolclaw/group-fs.md +209 -0
  82. package/kits/docs/evolclaw/stats.md +70 -0
  83. package/kits/docs/venues/aun-group.md +29 -6
  84. package/kits/docs/venues/group.md +5 -4
  85. package/kits/eck_message_manifest.json +30 -3
  86. package/kits/rules/05-venue.md +1 -1
  87. package/kits/templates/message-fragments/inject-default.md +2 -0
  88. package/package.json +5 -6
  89. package/dist/agents/baseagent-normalize.js +0 -19
  90. package/dist/core/command-handler.js +0 -3876
  91. package/dist/core/relation/peer-key.js +0 -16
  92. package/dist/evolclaw-config.js +0 -11
  93. package/dist/utils/channel-helpers.js +0 -46
  94. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  95. /package/dist/{agents → eck}/kit-renderer.js +0 -0
@@ -1,5 +1,7 @@
1
+ import fs from 'fs';
1
2
  import { EvolAgent } from './evolagent.js';
2
3
  import { logger } from '../utils/logger.js';
4
+ import { agentMdPath } from '../paths.js';
3
5
  import { loadDefaults, loadAllAgents, mergeForAgent, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
4
6
  // ── Channel Fingerprint ────────────────────────────────────────────────────
5
7
  // 用于检测多 agent 之间复用同一外部凭证的冲突(appId、aid、token 等)。
@@ -333,6 +335,44 @@ export class EvolAgentRegistry {
333
335
  throw err;
334
336
  }
335
337
  }
338
+ // ── Stop / Start(运行时断连/重连,不改 config.enabled)──────────────────
339
+ async stopAgent(aidOrName, hooks) {
340
+ const agent = this.agents.get(aidOrName);
341
+ if (!agent)
342
+ throw new Error(`Agent "${aidOrName}" not found`);
343
+ if (agent.status === 'disabled')
344
+ throw new Error(`Agent is disabled; use enable/disable instead`);
345
+ if (agent.status === 'stopped')
346
+ return;
347
+ // 先断开 AID 连接(下线),让未送达的消息保留在云端;
348
+ // 然后中断正在执行的大模型调用(不等它跑完)。
349
+ for (const ch of agent.channelInstanceNames()) {
350
+ try {
351
+ await hooks.disconnectChannel(ch);
352
+ }
353
+ catch { }
354
+ }
355
+ agent.status = 'stopped';
356
+ this.channelIndex.clear();
357
+ this.buildChannelIndex();
358
+ logger.info(`[Registry] Stopped agent ${aidOrName}`);
359
+ }
360
+ async startAgent(aidOrName, hooks) {
361
+ const agent = this.agents.get(aidOrName);
362
+ if (!agent)
363
+ throw new Error(`Agent "${aidOrName}" not found`);
364
+ if (agent.status === 'disabled')
365
+ throw new Error(`Agent is disabled; use enable instead`);
366
+ if (agent.status === 'running')
367
+ return;
368
+ for (const ch of agent.channelInstanceNames()) {
369
+ await hooks.startChannel(agent, ch);
370
+ }
371
+ agent.status = 'running';
372
+ this.channelIndex.clear();
373
+ this.buildChannelIndex();
374
+ logger.info(`[Registry] Started agent ${aidOrName}`);
375
+ }
336
376
  checkConflictForReload(newRaw, excludeAid) {
337
377
  const newFps = new Set();
338
378
  for (const inst of newRaw.channels) {
@@ -353,9 +393,51 @@ export class EvolAgentRegistry {
353
393
  }
354
394
  return null;
355
395
  }
396
+ // ── 友好名缓存(从本地 agent.md 解析,缺失时异步从网络拉取)──
397
+ displayNameCache = new Map();
398
+ displayNamePending = new Set();
399
+ resolveDisplayName(aid) {
400
+ const cached = this.displayNameCache.get(aid);
401
+ if (cached)
402
+ return cached;
403
+ try {
404
+ const mdPath = agentMdPath(aid);
405
+ if (fs.existsSync(mdPath)) {
406
+ const content = fs.readFileSync(mdPath, 'utf-8');
407
+ const fm = content.match(/^---\n([\s\S]*?)\n---/);
408
+ if (fm) {
409
+ const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
410
+ if (nm?.[1]) {
411
+ this.displayNameCache.set(aid, nm[1]);
412
+ return nm[1];
413
+ }
414
+ }
415
+ }
416
+ }
417
+ catch { /* ignore */ }
418
+ // 异步从网络拉取(仅一次,不阻塞)
419
+ if (!this.displayNamePending.has(aid)) {
420
+ this.displayNamePending.add(aid);
421
+ import('../aun/aid/index.js').then(({ agentmdGet }) => {
422
+ agentmdGet(aid).then(content => {
423
+ if (typeof content === 'string') {
424
+ const fm = content.match(/^---\n([\s\S]*?)\n---/);
425
+ if (fm) {
426
+ const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
427
+ if (nm?.[1])
428
+ this.displayNameCache.set(aid, nm[1]);
429
+ }
430
+ }
431
+ }).catch(() => { }).finally(() => this.displayNamePending.delete(aid));
432
+ }).catch(() => { this.displayNamePending.delete(aid); });
433
+ }
434
+ return undefined;
435
+ }
356
436
  toInfo(agent) {
357
437
  return {
358
438
  name: agent.name,
439
+ displayName: this.resolveDisplayName(agent.aid),
440
+ aid: agent.aid,
359
441
  status: agent.status,
360
442
  channels: agent.channelInstanceNames(),
361
443
  projectPath: agent.projectPath,
@@ -3,7 +3,7 @@ import { logger } from '../utils/logger.js';
3
3
  import { saveAgent } from '../config-store.js';
4
4
  import { formatChannelKey, tryParseChannelKey } from './channel-loader.js';
5
5
  import { agentPersonalDir } from '../paths.js';
6
- import { fileCache } from './cache/file-cache.js';
6
+ import { fileCache } from './daemon-file-cache.js';
7
7
  /**
8
8
  * EvolAgent —— 一个 self-agent 的运行时表示。
9
9
  *
@@ -177,6 +177,14 @@ export class EvolAgent {
177
177
  delete block.model;
178
178
  else
179
179
  block.model = value;
180
+ // sync merged so getter reflects the change immediately
181
+ if (!this.merged.baseagents)
182
+ this.merged.baseagents = {};
183
+ const mBlock = ((this.merged.baseagents)[ba] ??= {});
184
+ if (value === undefined)
185
+ delete mBlock.model;
186
+ else
187
+ mBlock.model = value;
180
188
  this.persist();
181
189
  }
182
190
  setBaseagentEffort(value) {
@@ -189,6 +197,14 @@ export class EvolAgent {
189
197
  delete block[fieldName];
190
198
  else
191
199
  block[fieldName] = value;
200
+ // sync merged so getter reflects the change immediately
201
+ if (!this.merged.baseagents)
202
+ this.merged.baseagents = {};
203
+ const mBlock = ((this.merged.baseagents)[ba] ??= {});
204
+ if (value === undefined)
205
+ delete mBlock[fieldName];
206
+ else
207
+ mBlock[fieldName] = value;
192
208
  this.persist();
193
209
  }
194
210
  /** 设置私聊 chatmode(群聊/非 human 强制 proactive,无可写入项)。 */
@@ -68,6 +68,14 @@ export class InteractionRouter {
68
68
  const handler = this.handlers.get(response.id);
69
69
  if (!handler)
70
70
  return false;
71
+ // Initiator 校验(集中式 backstop):非发起者的操作直接丢弃,不消费 handler、不解除等待,
72
+ // 让真正的发起者仍可继续操作。身份只信渠道传入的已认证 operatorId(来自消息信封,非 payload 自报)。
73
+ // 渠道层若已自行校验(如飞书的 reject toast),此处不会重复命中(operatorId 已匹配)。
74
+ if (handler.initiatorId && response.operatorId
75
+ && response.operatorId !== handler.initiatorId) {
76
+ logger.info(`[InteractionRouter] rejected non-initiator: operator=${response.operatorId} initiator=${handler.initiatorId} id=${response.id}`);
77
+ return false;
78
+ }
71
79
  if (handler.timer)
72
80
  clearTimeout(handler.timer);
73
81
  this.handlers.delete(response.id);
@@ -1,7 +1,8 @@
1
- import { agentCreateNonInteractive, agentDelete, agentEnable, agentDisable, agentList, agentShow, agentSet, } from '../../cli/agent.js';
1
+ import { agentCreateNonInteractive, agentDelete, agentEnable, agentDisable, agentList, agentShow, agentSet, agentReload, } from '../../cli/agent.js';
2
2
  import { logger } from '../../utils/logger.js';
3
3
  import { resolvePaths } from '../../paths.js';
4
4
  import path from 'path';
5
+ import { loadAgent, saveAgent } from '../../config-store.js';
5
6
  import { CreateStatusWriter, readCreateStatus } from './create-status.js';
6
7
  /** 把 cli/agent.ts 的 error 字符串映射为结构化错误码 */
7
8
  function classifyError(error) {
@@ -116,8 +117,69 @@ export async function execAgentAction(action, args, peerId) {
116
117
  return { error: res.error, code: classifyError(res.error) };
117
118
  return { data: { aid: res.aid, enabled: res.enabled, reloaded: res.reloaded } };
118
119
  }
120
+ if (action === 'reload') {
121
+ if (!a.aid)
122
+ return { error: '缺少 aid', code: 'INVALID_ARGS' };
123
+ const res = await agentReload(a.aid);
124
+ if (!('ok' in res) || res.ok !== true)
125
+ return { error: res.error, code: classifyError(res.error) };
126
+ return { data: { aid: a.aid, reloaded: true } };
127
+ }
128
+ if (action === 'update') {
129
+ return await execAgentUpdate(a);
130
+ }
119
131
  return { error: `不支持的 action: ${action}`, code: 'INVALID_ARGS' };
120
132
  }
133
+ /** name=agent 的 menu.action=update:仅落盘 config patch,不触发 reload。
134
+ * 直接 loadAgent + saveAgent(不走 agentSet,避免其内部自动 evolagent.reload)——
135
+ * 重载由用户在 Agents 页操作列手动触发(带任务执行检查)。
136
+ * AUN 渠道绑定 agent 顶层 aid,不可通过 patch 编辑:拒绝改 aid、拒绝 channels 数组里出现 aun 条目。
137
+ * 可写字段:baseagents / projects / owners / chatmode / channels(非 aun)。 */
138
+ export async function execAgentUpdate(args) {
139
+ const a = args ?? {};
140
+ if (!a.aid)
141
+ return { error: '缺少 aid', code: 'INVALID_ARGS' };
142
+ const p = a.patch ?? {};
143
+ if (p.aid !== undefined) {
144
+ return { error: 'aid 不可修改(AUN 身份绑定,如需换 AID 请删除后重建)', code: 'INVALID_ARGS' };
145
+ }
146
+ if (Array.isArray(p.channels) && p.channels.some((c) => c?.type === 'aun')) {
147
+ return { error: 'AUN 渠道不可通过 patch 编辑(由 agent aid 隐式管理)', code: 'INVALID_ARGS' };
148
+ }
149
+ const config = loadAgent(a.aid);
150
+ if (!config)
151
+ return { error: `Agent "${a.aid}" not found`, code: 'NOT_FOUND' };
152
+ let touched = false;
153
+ if (p.baseagents !== undefined) {
154
+ config.baseagents = p.baseagents;
155
+ touched = true;
156
+ }
157
+ if (p.projects !== undefined) {
158
+ config.projects = p.projects;
159
+ touched = true;
160
+ }
161
+ if (p.owners !== undefined) {
162
+ config.owners = p.owners;
163
+ touched = true;
164
+ }
165
+ if (p.chatmode !== undefined) {
166
+ config.chatmode = p.chatmode;
167
+ touched = true;
168
+ }
169
+ if (p.channels !== undefined) {
170
+ config.channels = p.channels;
171
+ touched = true;
172
+ }
173
+ if (!touched)
174
+ return { error: 'patch 为空,无可写字段', code: 'INVALID_ARGS' };
175
+ try {
176
+ saveAgent(config);
177
+ }
178
+ catch (e) {
179
+ return { error: e?.message || String(e), code: classifyError(e?.message || String(e)) };
180
+ }
181
+ return { data: { aid: a.aid, saved: true } };
182
+ }
121
183
  /** project 兜底:显式值 > rootPath 合成 > defaultPath > undefined */
122
184
  export function resolveProjectPath(explicit, aid, defaults) {
123
185
  if (explicit && explicit.trim())
@@ -1,23 +1,9 @@
1
1
  import { logger } from '../../utils/logger.js';
2
2
  import { summarizeToolInput } from '../permission.js';
3
+ import { CONTEXT_TOO_LONG_PATTERN, isContextTooLongText } from '../../utils/error-utils.js';
3
4
  import fs from 'fs';
4
5
  import path from 'path';
5
6
  import { resolvePaths } from '../../paths.js';
6
- /**
7
- * 检测是否为上下文过长错误
8
- * 统一的检测逻辑,覆盖所有已知的错误文本模式
9
- */
10
- function isContextTooLongError(text) {
11
- if (!text)
12
- return false;
13
- const lower = text.toLowerCase();
14
- return (lower.includes('prompt is too long') ||
15
- lower.includes('input is too long') ||
16
- lower.includes('context too long') ||
17
- lower.includes('context limit') ||
18
- lower.includes('context_length_exceeded') ||
19
- text.includes('上下文过长'));
20
- }
21
7
  let diagStream = null;
22
8
  function getDiagStream() {
23
9
  if (!diagStream) {
@@ -107,11 +93,16 @@ export class IMRenderer {
107
93
  clearTimeout(this.timer);
108
94
  this.timer = undefined;
109
95
  }
110
- const text = this.textBuffer;
96
+ // 文件标记过滤:tool_use 前的文本块也不能把 marker 暴露给用户
97
+ const rawText = this.opts.fileMarkerPattern
98
+ ? this.textBuffer.replace(this.opts.fileMarkerPattern, '')
99
+ : this.textBuffer;
111
100
  this.textBuffer = '';
101
+ if (!rawText)
102
+ return;
112
103
  // 清掉 itemsQueue 中的 text items(已发出)
113
104
  this.itemsQueue = this.itemsQueue.filter(it => it.kind !== 'text');
114
- const payload = { kind: 'result.text', text, isFinal: false };
105
+ const payload = { kind: 'result.text', text: rawText, isFinal: false };
115
106
  this.sentContent = true;
116
107
  this.sendChain = this.sendChain
117
108
  .then(() => this.opts.send(payload))
@@ -135,9 +126,9 @@ export class IMRenderer {
135
126
  // ── 文本/活动注入(替代 StreamFlusher.addText/addActivity)──
136
127
  /** 添加文本片段(流式 text) */
137
128
  addText(text, outputTokens, turn) {
138
- this.emitProgress('text', outputTokens, turn);
139
129
  if (this.opts.envelope.chatmode === 'proactive')
140
130
  return;
131
+ this.emitProgress('text', outputTokens, turn);
141
132
  if (!text)
142
133
  return;
143
134
  // 同一窗口内连续 text delta 合并到最后一个 text item
@@ -162,9 +153,9 @@ export class IMRenderer {
162
153
  }
163
154
  /** 添加工具调用 */
164
155
  addToolCall(name, input, callId, descText, turn, outputTokens) {
165
- this.emitProgress('tool_call', outputTokens, turn, { toolName: name, callId });
166
156
  if (this.opts.envelope.chatmode === 'proactive')
167
157
  return;
158
+ this.emitProgress('tool_call', outputTokens, turn, { toolName: name, callId });
168
159
  if (this.opts.suppressActivities)
169
160
  return;
170
161
  this.itemsQueue.push({
@@ -181,9 +172,9 @@ export class IMRenderer {
181
172
  }
182
173
  /** 添加工具结果 */
183
174
  addToolResult(name, ok, result, error, callId, durationMs, descText) {
184
- this.emitProgress('tool_result', undefined, undefined, { toolName: name, callId, ok, durationMs });
185
175
  if (this.opts.envelope.chatmode === 'proactive')
186
176
  return;
177
+ this.emitProgress('tool_result', undefined, undefined, { toolName: name, callId, ok, durationMs });
187
178
  if (this.opts.suppressActivities)
188
179
  return;
189
180
  this.itemsQueue.push({
@@ -207,20 +198,35 @@ export class IMRenderer {
207
198
  return;
208
199
  if (this.opts.suppressActivities)
209
200
  return;
210
- this.itemsQueue.push({
211
- kind: 'progress',
201
+ this.emitProgress('progress', undefined, undefined, {
212
202
  text,
213
203
  state: opts.state,
214
- tool_uses: opts.toolUses,
215
- duration_ms: opts.durationMs,
204
+ toolUses: opts.toolUses,
205
+ durationMs: opts.durationMs,
216
206
  });
217
- this.messageTimestamps.push(Date.now());
218
- this.scheduleFlush();
219
207
  }
208
+ /**
209
+ * proactive 下放行为 thought 的 notice subtype 白名单——仅"真·终态错误":
210
+ * - context-too-long:上下文超限且无法 auto-compact,任务到此终止
211
+ * - process-exit:Agent 子进程异常崩溃(无 complete 事件,emit 完全覆盖不到)
212
+ * 二者都是用户必须知道、否则会困惑"任务为什么停了"的终态信号。
213
+ *
214
+ * 其余 subtype 一律不发:
215
+ * - compact / runtime-error / task-error:emit() 路径(mapEventToItem)已投影,重复
216
+ * - compact-start / compact-trigger / compact-retry / retry:内部机务噪音(压缩中/
217
+ * 重试中/压缩完成),proactive 下用户要的是工作产出而非流水账,压缩后 thought 会
218
+ * 继续输出,过程本身无需播报。
219
+ */
220
+ static PROACTIVE_NOTICE_ALLOW = new Set(['context-too-long', 'process-exit']);
220
221
  /** 添加系统提示 / 通知。force=true 时绕过 suppressActivities(用于 compact/retry/error 等操作反馈) */
221
222
  addNotice(text, severity, subtype, force = false) {
222
- if (this.opts.envelope.chatmode === 'proactive')
223
+ // proactive 模式:只放行真·终态错误,机务噪音(压缩/重试)和 emit 已覆盖的 subtype 均不发。
224
+ if (this.opts.envelope.chatmode === 'proactive') {
225
+ if (subtype == null || !IMRenderer.PROACTIVE_NOTICE_ALLOW.has(subtype))
226
+ return;
227
+ this.emitProactiveItem({ kind: 'notice', text, severity, subtype });
223
228
  return;
229
+ }
224
230
  if (this.opts.suppressActivities && !force)
225
231
  return;
226
232
  this.itemsQueue.push({ kind: 'notice', text, severity, subtype });
@@ -308,23 +314,33 @@ export class IMRenderer {
308
314
  clearTimeout(this.timer);
309
315
  this.timer = undefined;
310
316
  }
311
- // 上下文错误短语过滤:剔除错误关键词本身,保留前后内容
312
- const ctxErrPattern = /prompt is too long|input is too long|context too long|context limit|context_length_exceeded|上下文过长/gi;
313
- const stripCtxErr = (s) => s.replace(ctxErrPattern, '').trim();
314
- this.textBuffer = stripCtxErr(this.textBuffer);
315
- this.allText = stripCtxErr(this.allText);
316
- for (const item of this.itemsQueue) {
317
- if (item.kind === 'text')
318
- item.text = stripCtxErr(item.text);
319
- }
320
- // 文件标记过滤
317
+ // 文件标记过滤:marker 任何时候都不该出现在用户文本里,必须在 *每次* flush
318
+ // (含非最终的定时 flush)执行。否则文本块若滞留 buffer 后被定时器触发,会走
319
+ // 下方非-final result.text 路径,把 [SEND_FILE:...] 原样发给用户。
320
+ // 非最终 flush 不做 trim,避免 trim 掉 Markdown 块级换行。
321
321
  if (this.opts.fileMarkerPattern) {
322
- this.textBuffer = this.textBuffer.replace(this.opts.fileMarkerPattern, '').trim();
322
+ const strip = (s) => {
323
+ const replaced = s.replace(this.opts.fileMarkerPattern, '');
324
+ return isFinal ? replaced.trim() : replaced;
325
+ };
326
+ this.textBuffer = strip(this.textBuffer);
323
327
  for (const item of this.itemsQueue) {
324
328
  if (item.kind === 'text')
325
329
  item.text = item.text.replace(this.opts.fileMarkerPattern, '');
326
330
  }
327
331
  }
332
+ if (isFinal) {
333
+ // 上下文错误短语过滤:剔除错误关键词本身,保留前后内容。
334
+ // 只在最终 flush 清理,避免中间定时 flush trim 掉 Markdown 块级换行。
335
+ const ctxErrPattern = new RegExp(CONTEXT_TOO_LONG_PATTERN.source, 'gi');
336
+ const stripCtxErr = (s) => s.replace(ctxErrPattern, '').trim();
337
+ this.textBuffer = stripCtxErr(this.textBuffer);
338
+ this.allText = stripCtxErr(this.allText);
339
+ for (const item of this.itemsQueue) {
340
+ if (item.kind === 'text')
341
+ item.text = stripCtxErr(item.text);
342
+ }
343
+ }
328
344
  // 清掉空 text items
329
345
  const items = this.itemsQueue.filter(it => {
330
346
  if (it.kind === 'text')
@@ -356,6 +372,25 @@ export class IMRenderer {
356
372
  this.lastFlush = Date.now();
357
373
  this.flushCount++;
358
374
  }
375
+ // 1.5 非最终定时 flush:把已累积的文本块作为独立 result.text 发出。
376
+ // 每个 text 事件本身是完整语义块(runner 已合并流式 delta),工具调用前的
377
+ // 文本一向作为独立气泡发送(见 message-processor 的 flushText 调用)。
378
+ // 这里补上「文本块后面没有紧跟 tool_use」的情况——例如 readonly 拒绝写文件时
379
+ // SDK 直接拒绝、不产生 tool_use 事件,文本会一直滞留 buffer,直到下一个
380
+ // tool_use 才被 flushText 带出,并与其后的文本合并成一条(用户侧表现为:
381
+ // 第一条文本等待一分多钟后才和第二条凑成一条发出)。定时器到期即发,根除滞留。
382
+ if (!isFinal && this.textBuffer.length > 0) {
383
+ const text = this.textBuffer;
384
+ this.textBuffer = '';
385
+ const payload = { kind: 'result.text', text, isFinal: false };
386
+ this.sentContent = true;
387
+ this.sendChain = this.sendChain
388
+ .then(() => this.opts.send(payload))
389
+ .catch(e => logger.warn('[IMRenderer] timed result.text send failed:', e));
390
+ await this.sendChain;
391
+ this.lastFlush = Date.now();
392
+ this.flushCount++;
393
+ }
359
394
  // 2. isFinal=true 时单独发最终回复文本
360
395
  if (isFinal && finalText.length > 0) {
361
396
  const payload = { kind: 'result.text', text: finalText, isFinal: true };
@@ -380,6 +415,9 @@ export class IMRenderer {
380
415
  ...(extra?.callId != null && { callId: extra.callId }),
381
416
  ...(extra?.ok != null && { ok: extra.ok }),
382
417
  ...(extra?.durationMs != null && { durationMs: extra.durationMs }),
418
+ ...(extra?.text != null && { text: extra.text }),
419
+ ...(extra?.state != null && { state: extra.state }),
420
+ ...(extra?.toolUses != null && { toolUses: extra.toolUses }),
383
421
  },
384
422
  };
385
423
  this.opts.send(payload).catch(() => { });
@@ -400,17 +438,19 @@ export class IMRenderer {
400
438
  this.hasEmittedText = true;
401
439
  this.allText += item.text;
402
440
  }
403
- const outputTokens = event.outputTokens;
404
- const turn = event.turn;
405
- const activityType = item.kind === 'text' ? 'text' : item.kind === 'tool_call' ? 'tool_call' : 'tool_result';
406
- const extra = item.kind === 'tool_call'
407
- ? { toolName: item.name, callId: item.call_id }
408
- : item.kind === 'tool_result'
409
- ? { toolName: item.name, callId: item.call_id, ok: item.ok, durationMs: item.duration_ms }
410
- : undefined;
411
- this.emitProgress(activityType, outputTokens, turn, extra);
441
+ // proactive 模式:status.progress 是 interactive 的处理状态指示器,proactive 下
442
+ // 过程由 thought(activity.batch)表达,不再发 status.progress(与 addProgress 在
443
+ // proactive 下的拦截保持一致)。progress-kind item 若进 activity.batch 会被 aun
444
+ // 回转成 status.progress(见 aun.ts 'activity.batch' 分支),故直接丢弃。
445
+ // 终态 status(started/completed/interrupted/error)由 message-processor 发送,不受影响。
446
+ if (item.kind === 'progress') {
447
+ return;
448
+ }
449
+ this.emitProactiveItem(item);
450
+ }
451
+ /** proactive 模式逐条投影:单个 ThoughtItem 包成 activity.batch[1] 发出(fire-and-forget)。 */
452
+ emitProactiveItem(item) {
412
453
  const payload = { kind: 'activity.batch', items: [item] };
413
- // fire-and-forget
414
454
  this.opts.send(payload).catch(err => {
415
455
  logger.debug(`[IMRenderer] proactive send failed: ${err.message}`);
416
456
  });
@@ -474,15 +514,15 @@ export class IMRenderer {
474
514
  }
475
515
  case 'error': {
476
516
  // 上下文过长错误不输出(留给外层 auto-compact 处理)
477
- if (isContextTooLongError(event.error || ''))
517
+ if (isContextTooLongText(event.error || ''))
478
518
  return null;
479
519
  return { kind: 'notice', text: event.error, severity: 'warn' };
480
520
  }
481
521
  case 'complete': {
482
522
  // 上下文过长错误不输出(留给外层 auto-compact 处理)
483
523
  const hasContextError = event.terminalReason === 'prompt_too_long'
484
- || isContextTooLongError(event.errors?.join(' ') || '')
485
- || isContextTooLongError(event.result || '');
524
+ || isContextTooLongText(event.errors?.join(' ') || '')
525
+ || isContextTooLongText(event.result || '');
486
526
  if (event.isError && hasContextError) {
487
527
  return null;
488
528
  }
@@ -34,7 +34,7 @@ function formatItem(item) {
34
34
  case 'tool_result': {
35
35
  if (!item.ok) {
36
36
  const errMsg = item.error || (typeof item.result === 'string' ? item.result : '执行失败');
37
- return `⚠️ ${item.name}: ${errMsg}`;
37
+ return `⚠️ ${item.name}: ${capLines(errMsg, 5)}`;
38
38
  }
39
39
  return item.text ? `✓ ${item.name}: ${item.text}` : `✓ ${item.name}`;
40
40
  }
@@ -48,6 +48,14 @@ function formatItem(item) {
48
48
  return '';
49
49
  }
50
50
  }
51
+ /** 把多行文本截断到最多 maxLines 行,超出部分用省略提示替代。用于工具报错输出,避免刷屏。 */
52
+ function capLines(text, maxLines) {
53
+ const lines = text.split('\n');
54
+ if (lines.length <= maxLines)
55
+ return text;
56
+ const omitted = lines.length - maxLines;
57
+ return lines.slice(0, maxLines).join('\n') + `\n…(省略 ${omitted} 行)`;
58
+ }
51
59
  function summarizeArgs(args) {
52
60
  if (!args || typeof args !== 'object')
53
61
  return '';