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.
- package/CHANGELOG.md +53 -0
- package/README.md +7 -4
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -31
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1152 -140
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +58 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/outbox.js +14 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +869 -358
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +125 -154
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +23 -8
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +23 -4905
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +88 -83
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +82 -0
- package/dist/core/evolagent.js +17 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- package/dist/core/message/im-renderer.js +91 -51
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +73 -24
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +432 -94
- package/dist/core/message/message-queue.js +70 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +25 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +86 -26
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +334 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/data/error-dict.json +7 -0
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +174 -9
- package/dist/ipc.js +116 -1
- package/dist/utils/cross-platform.js +58 -5
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +77 -6
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/package.json +5 -6
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/command-handler.js +0 -3876
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /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,
|
package/dist/core/evolagent.js
CHANGED
|
@@ -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 './
|
|
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
|
-
|
|
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.
|
|
211
|
-
kind: 'progress',
|
|
201
|
+
this.emitProgress('progress', undefined, undefined, {
|
|
212
202
|
text,
|
|
213
203
|
state: opts.state,
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.
|
|
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 (
|
|
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
|
-
||
|
|
485
|
-
||
|
|
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 '';
|