evolclaw 2.8.3 → 3.0.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/README.md +21 -12
- package/dist/agents/claude-runner.js +102 -38
- package/dist/agents/codex-runner.js +11 -14
- package/dist/agents/gemini-runner.js +10 -12
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1051 -288
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +858 -847
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +309 -142
- package/dist/core/message/message-queue.js +3 -3
- package/dist/core/permission.js +21 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +704 -775
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/{templates → data}/prompts.md +34 -1
- package/dist/index.js +431 -275
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { logger } from '../../utils/logger.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { resolvePaths } from '../../paths.js';
|
|
5
|
+
let diagStream = null;
|
|
6
|
+
function getDiagStream() {
|
|
7
|
+
if (!diagStream) {
|
|
8
|
+
const logDir = resolvePaths().logs;
|
|
9
|
+
diagStream = fs.createWriteStream(path.join(logDir, 'im-renderer-diag.log'), { flags: 'a' });
|
|
10
|
+
}
|
|
11
|
+
return diagStream;
|
|
12
|
+
}
|
|
13
|
+
function diag(instanceId, action, meta = {}) {
|
|
14
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), id: instanceId, action, ...meta });
|
|
15
|
+
getDiagStream().write(line + '\n');
|
|
16
|
+
}
|
|
17
|
+
let instanceCounter = 0;
|
|
18
|
+
export class IMRenderer {
|
|
19
|
+
opts;
|
|
20
|
+
itemsQueue = [];
|
|
21
|
+
textBuffer = '';
|
|
22
|
+
timer;
|
|
23
|
+
lastFlush = Date.now();
|
|
24
|
+
allText = '';
|
|
25
|
+
sentContent = false;
|
|
26
|
+
flushCount = 0;
|
|
27
|
+
messageTimestamps = [];
|
|
28
|
+
instanceId;
|
|
29
|
+
diagEnabled;
|
|
30
|
+
/** 串行发送队列:保证消息按序到达 */
|
|
31
|
+
sendChain = Promise.resolve();
|
|
32
|
+
/** proactive:是否已发过 thinking 文本(用于去重 complete.result) */
|
|
33
|
+
hasEmittedThinking = false;
|
|
34
|
+
/** 自增 callId 兜底(runner 没提供时用) */
|
|
35
|
+
syntheticCallSeq = 0;
|
|
36
|
+
constructor(opts) {
|
|
37
|
+
this.opts = opts;
|
|
38
|
+
this.diagEnabled = opts.diagEnabled ?? false;
|
|
39
|
+
this.instanceId = `R${++instanceCounter}`;
|
|
40
|
+
if (this.diagEnabled) {
|
|
41
|
+
diag(this.instanceId, 'created', {
|
|
42
|
+
chatmode: opts.envelope.chatmode,
|
|
43
|
+
flushDelay: opts.flushDelay,
|
|
44
|
+
suppress: opts.suppressActivities,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// ── 公开接口 ──
|
|
49
|
+
/** 推入 AgentEvent,按 chatmode 投影 */
|
|
50
|
+
emit(event) {
|
|
51
|
+
try {
|
|
52
|
+
logger.event({ source: 'runner', taskId: this.opts.envelope.taskId, channelId: this.opts.envelope.channelId, event });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// logger.event 失败不影响业务
|
|
56
|
+
}
|
|
57
|
+
if (this.opts.envelope.chatmode === 'proactive') {
|
|
58
|
+
this.emitProactive(event);
|
|
59
|
+
}
|
|
60
|
+
// interactive 模式由 MessageProcessor 显式调 addText/addToolCall/... 推入 items
|
|
61
|
+
}
|
|
62
|
+
/** 强制刷新所有 pending 事件 */
|
|
63
|
+
async flush(isFinal) {
|
|
64
|
+
if (this.opts.envelope.chatmode === 'proactive') {
|
|
65
|
+
// proactive 是 fire-and-forget,无 pending buffer
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
return this.flushInternal(isFinal);
|
|
69
|
+
}
|
|
70
|
+
/** 仅 flush activities,保留 textBuffer(用于中间 complete 事件) */
|
|
71
|
+
async flushActivitiesOnly() {
|
|
72
|
+
if (this.opts.envelope.chatmode === 'proactive')
|
|
73
|
+
return;
|
|
74
|
+
return this.flushActivitiesInternal();
|
|
75
|
+
}
|
|
76
|
+
/** 是否有 pending 内容 */
|
|
77
|
+
hasContent() {
|
|
78
|
+
return this.textBuffer.length > 0 || this.itemsQueue.some(it => it.kind !== 'thinking');
|
|
79
|
+
}
|
|
80
|
+
/** 是否已发送过内容(用于决定最终 flush 是否带 isFinal 标题) */
|
|
81
|
+
hasSentContent() {
|
|
82
|
+
return this.sentContent;
|
|
83
|
+
}
|
|
84
|
+
/** 累积的全部文本(流式 + 最终) */
|
|
85
|
+
getFinalText() {
|
|
86
|
+
return this.allText;
|
|
87
|
+
}
|
|
88
|
+
/** 当前 buffer 中尚未 flush 的文本 */
|
|
89
|
+
getRemainingText() {
|
|
90
|
+
return this.textBuffer;
|
|
91
|
+
}
|
|
92
|
+
/** 从 buffer 中移除指定 pattern(用于文件标记预处理) */
|
|
93
|
+
stripFromBuffer(pattern) {
|
|
94
|
+
this.textBuffer = this.textBuffer.replace(pattern, '').trim();
|
|
95
|
+
// itemsQueue 中的 thinking items 也同步过滤
|
|
96
|
+
for (const item of this.itemsQueue) {
|
|
97
|
+
if (item.kind === 'thinking') {
|
|
98
|
+
item.text = item.text.replace(pattern, '');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── 文本/活动注入(替代 StreamFlusher.addText/addActivity)──
|
|
103
|
+
/** 添加文本片段(流式 text) */
|
|
104
|
+
addText(text) {
|
|
105
|
+
if (this.opts.envelope.chatmode === 'proactive')
|
|
106
|
+
return;
|
|
107
|
+
if (!text)
|
|
108
|
+
return;
|
|
109
|
+
// 同一窗口内连续 text delta 合并到最后一个 thinking item
|
|
110
|
+
const last = this.itemsQueue[this.itemsQueue.length - 1];
|
|
111
|
+
if (last && last.kind === 'thinking') {
|
|
112
|
+
last.text += text;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.itemsQueue.push({ kind: 'thinking', text });
|
|
116
|
+
}
|
|
117
|
+
this.textBuffer += text;
|
|
118
|
+
this.allText += text;
|
|
119
|
+
this.messageTimestamps.push(Date.now());
|
|
120
|
+
if (this.diagEnabled) {
|
|
121
|
+
diag(this.instanceId, 'addText', {
|
|
122
|
+
len: text.length,
|
|
123
|
+
preview: text.substring(0, 60),
|
|
124
|
+
bufLen: this.textBuffer.length,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
this.scheduleFlush();
|
|
128
|
+
}
|
|
129
|
+
/** 添加工具调用 */
|
|
130
|
+
addToolCall(name, input, callId, descText) {
|
|
131
|
+
if (this.opts.envelope.chatmode === 'proactive')
|
|
132
|
+
return;
|
|
133
|
+
if (this.opts.suppressActivities)
|
|
134
|
+
return;
|
|
135
|
+
this.itemsQueue.push({
|
|
136
|
+
kind: 'tool_call',
|
|
137
|
+
call_id: callId || this.synthCallId(),
|
|
138
|
+
name,
|
|
139
|
+
arguments: input,
|
|
140
|
+
text: descText,
|
|
141
|
+
});
|
|
142
|
+
this.messageTimestamps.push(Date.now());
|
|
143
|
+
if (this.diagEnabled)
|
|
144
|
+
diag(this.instanceId, 'addToolCall', { name, callId });
|
|
145
|
+
this.scheduleFlush();
|
|
146
|
+
}
|
|
147
|
+
/** 添加工具结果 */
|
|
148
|
+
addToolResult(name, ok, result, error, callId, durationMs, descText) {
|
|
149
|
+
if (this.opts.envelope.chatmode === 'proactive')
|
|
150
|
+
return;
|
|
151
|
+
if (this.opts.suppressActivities)
|
|
152
|
+
return;
|
|
153
|
+
this.itemsQueue.push({
|
|
154
|
+
kind: 'tool_result',
|
|
155
|
+
call_id: callId || this.synthCallId(),
|
|
156
|
+
name,
|
|
157
|
+
ok,
|
|
158
|
+
result,
|
|
159
|
+
error,
|
|
160
|
+
duration_ms: durationMs,
|
|
161
|
+
text: descText,
|
|
162
|
+
});
|
|
163
|
+
this.messageTimestamps.push(Date.now());
|
|
164
|
+
if (this.diagEnabled)
|
|
165
|
+
diag(this.instanceId, 'addToolResult', { name, ok, callId });
|
|
166
|
+
this.scheduleFlush();
|
|
167
|
+
}
|
|
168
|
+
/** 添加进度提示 */
|
|
169
|
+
addProgress(text, opts = {}) {
|
|
170
|
+
if (this.opts.envelope.chatmode === 'proactive')
|
|
171
|
+
return;
|
|
172
|
+
if (this.opts.suppressActivities)
|
|
173
|
+
return;
|
|
174
|
+
this.itemsQueue.push({
|
|
175
|
+
kind: 'progress',
|
|
176
|
+
text,
|
|
177
|
+
state: opts.state,
|
|
178
|
+
tool_uses: opts.toolUses,
|
|
179
|
+
duration_ms: opts.durationMs,
|
|
180
|
+
});
|
|
181
|
+
this.messageTimestamps.push(Date.now());
|
|
182
|
+
this.scheduleFlush();
|
|
183
|
+
}
|
|
184
|
+
/** 添加系统提示 / 通知。force=true 时绕过 suppressActivities(用于 compact/retry/error 等操作反馈) */
|
|
185
|
+
addNotice(text, severity, subtype, force = false) {
|
|
186
|
+
if (this.opts.envelope.chatmode === 'proactive')
|
|
187
|
+
return;
|
|
188
|
+
if (this.opts.suppressActivities && !force)
|
|
189
|
+
return;
|
|
190
|
+
this.itemsQueue.push({ kind: 'notice', text, severity, subtype });
|
|
191
|
+
this.messageTimestamps.push(Date.now());
|
|
192
|
+
this.scheduleFlush();
|
|
193
|
+
}
|
|
194
|
+
// ── 内部:interactive 模式聚合窗口 ──
|
|
195
|
+
synthCallId() {
|
|
196
|
+
return `synth-${this.opts.envelope.taskId}-${++this.syntheticCallSeq}`;
|
|
197
|
+
}
|
|
198
|
+
scheduleFlush() {
|
|
199
|
+
if (this.timer) {
|
|
200
|
+
if (this.diagEnabled)
|
|
201
|
+
diag(this.instanceId, 'scheduleFlush:skip', { reason: 'timer_exists' });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const interval = this.opts.flushDelay ?? 4000;
|
|
205
|
+
let targetDelay;
|
|
206
|
+
if (this.flushCount === 0) {
|
|
207
|
+
targetDelay = 500;
|
|
208
|
+
}
|
|
209
|
+
else if (this.flushCount <= 3) {
|
|
210
|
+
targetDelay = Math.ceil(interval / 2);
|
|
211
|
+
}
|
|
212
|
+
else if (this.messageTimestamps.length >= 5) {
|
|
213
|
+
targetDelay = this.calculateDynamicDelay(interval);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
targetDelay = interval;
|
|
217
|
+
}
|
|
218
|
+
const elapsed = Date.now() - this.lastFlush;
|
|
219
|
+
const delay = Math.max(0, targetDelay - elapsed);
|
|
220
|
+
if (this.diagEnabled) {
|
|
221
|
+
diag(this.instanceId, 'scheduleFlush:set', {
|
|
222
|
+
flushCount: this.flushCount,
|
|
223
|
+
targetDelay,
|
|
224
|
+
elapsed,
|
|
225
|
+
actualDelay: delay,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
this.timer = setTimeout(() => this.flushInternal(), delay);
|
|
229
|
+
}
|
|
230
|
+
calculateDynamicDelay(interval) {
|
|
231
|
+
const recent = this.messageTimestamps.slice(-10);
|
|
232
|
+
const intervals = [];
|
|
233
|
+
for (let i = 1; i < recent.length; i++) {
|
|
234
|
+
intervals.push(recent[i] - recent[i - 1]);
|
|
235
|
+
}
|
|
236
|
+
if (intervals.length === 0)
|
|
237
|
+
return interval;
|
|
238
|
+
const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
|
|
239
|
+
const dynamicDelay = avgInterval * 3;
|
|
240
|
+
const minDelay = interval;
|
|
241
|
+
const maxDelay = interval * 2.5;
|
|
242
|
+
return Math.max(minDelay, Math.min(maxDelay, dynamicDelay));
|
|
243
|
+
}
|
|
244
|
+
/** 仅 flush 非 thinking items(保留 textBuffer 用于后续 final flush) */
|
|
245
|
+
async flushActivitiesInternal() {
|
|
246
|
+
const nonThinking = this.itemsQueue.filter(it => it.kind !== 'thinking');
|
|
247
|
+
if (nonThinking.length === 0)
|
|
248
|
+
return;
|
|
249
|
+
if (this.timer) {
|
|
250
|
+
clearTimeout(this.timer);
|
|
251
|
+
this.timer = undefined;
|
|
252
|
+
}
|
|
253
|
+
// 移除已 flush 的 non-thinking items,保留 thinking items
|
|
254
|
+
this.itemsQueue = this.itemsQueue.filter(it => it.kind === 'thinking');
|
|
255
|
+
const payload = { kind: 'activity.batch', items: nonThinking };
|
|
256
|
+
if (this.diagEnabled)
|
|
257
|
+
diag(this.instanceId, 'flushActivitiesOnly', { itemCount: nonThinking.length });
|
|
258
|
+
this.sentContent = true;
|
|
259
|
+
this.sendChain = this.sendChain
|
|
260
|
+
.then(() => this.opts.send(payload))
|
|
261
|
+
.catch(e => logger.warn('[IMRenderer] activity.batch send failed:', e));
|
|
262
|
+
await this.sendChain;
|
|
263
|
+
this.lastFlush = Date.now();
|
|
264
|
+
this.flushCount++;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 完整 flush:把 itemsQueue 里所有 items 打包成 activity.batch 发送。
|
|
268
|
+
* 如果 isFinal=true,还会在 batch 之后单独发一条 result.text 作为最终回复。
|
|
269
|
+
*/
|
|
270
|
+
async flushInternal(isFinal) {
|
|
271
|
+
if (this.timer) {
|
|
272
|
+
clearTimeout(this.timer);
|
|
273
|
+
this.timer = undefined;
|
|
274
|
+
}
|
|
275
|
+
// 文件标记过滤
|
|
276
|
+
if (this.opts.fileMarkerPattern) {
|
|
277
|
+
this.textBuffer = this.textBuffer.replace(this.opts.fileMarkerPattern, '').trim();
|
|
278
|
+
for (const item of this.itemsQueue) {
|
|
279
|
+
if (item.kind === 'thinking')
|
|
280
|
+
item.text = item.text.replace(this.opts.fileMarkerPattern, '');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// 清掉空 thinking items
|
|
284
|
+
const items = this.itemsQueue.filter(it => {
|
|
285
|
+
if (it.kind === 'thinking')
|
|
286
|
+
return it.text.length > 0;
|
|
287
|
+
return true;
|
|
288
|
+
});
|
|
289
|
+
this.itemsQueue = [];
|
|
290
|
+
const finalText = isFinal ? this.textBuffer : '';
|
|
291
|
+
if (isFinal)
|
|
292
|
+
this.textBuffer = '';
|
|
293
|
+
// 非 final flush 保留 textBuffer,供最终 result.text 使用
|
|
294
|
+
if (this.diagEnabled) {
|
|
295
|
+
diag(this.instanceId, 'flush', {
|
|
296
|
+
isFinal,
|
|
297
|
+
itemCount: items.length,
|
|
298
|
+
finalTextLen: finalText.length,
|
|
299
|
+
flushCount: this.flushCount,
|
|
300
|
+
sinceLastFlush: Date.now() - this.lastFlush,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// 1. interactive 模式下:isFinal=true 时不发 thinking-only batch
|
|
304
|
+
// (避免和最终 result.text 重复——最终回复已在 textBuffer 里)
|
|
305
|
+
let itemsForBatch = items;
|
|
306
|
+
if (isFinal) {
|
|
307
|
+
itemsForBatch = items.filter(it => it.kind !== 'thinking');
|
|
308
|
+
}
|
|
309
|
+
if (itemsForBatch.length > 0) {
|
|
310
|
+
const payload = { kind: 'activity.batch', items: itemsForBatch };
|
|
311
|
+
this.sentContent = true;
|
|
312
|
+
this.sendChain = this.sendChain
|
|
313
|
+
.then(() => this.opts.send(payload))
|
|
314
|
+
.catch(e => logger.warn('[IMRenderer] activity.batch send failed:', e));
|
|
315
|
+
await this.sendChain;
|
|
316
|
+
this.lastFlush = Date.now();
|
|
317
|
+
this.flushCount++;
|
|
318
|
+
}
|
|
319
|
+
// 2. isFinal=true 时单独发最终回复文本
|
|
320
|
+
if (isFinal && finalText.length > 0) {
|
|
321
|
+
const payload = { kind: 'result.text', text: finalText, isFinal: true };
|
|
322
|
+
this.sentContent = true;
|
|
323
|
+
this.sendChain = this.sendChain
|
|
324
|
+
.then(() => this.opts.send(payload))
|
|
325
|
+
.catch(e => logger.warn('[IMRenderer] result.text send failed:', e));
|
|
326
|
+
await this.sendChain;
|
|
327
|
+
this.lastFlush = Date.now();
|
|
328
|
+
this.flushCount++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// ── 内部:proactive 模式(逐事件 activity.batch[1 item]) ──
|
|
332
|
+
emitProactive(event) {
|
|
333
|
+
// 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
|
|
334
|
+
if (event.type === 'complete' &&
|
|
335
|
+
!event.isError &&
|
|
336
|
+
event.result &&
|
|
337
|
+
this.hasEmittedThinking) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const item = this.mapEventToItem(event);
|
|
341
|
+
if (!item)
|
|
342
|
+
return;
|
|
343
|
+
if (item.kind === 'thinking') {
|
|
344
|
+
this.hasEmittedThinking = true;
|
|
345
|
+
this.allText += item.text;
|
|
346
|
+
}
|
|
347
|
+
const payload = { kind: 'activity.batch', items: [item] };
|
|
348
|
+
// fire-and-forget
|
|
349
|
+
this.opts.send(payload).catch(err => {
|
|
350
|
+
logger.debug(`[IMRenderer] proactive send failed: ${err.message}`);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
mapEventToItem(event) {
|
|
354
|
+
switch (event.type) {
|
|
355
|
+
case 'text':
|
|
356
|
+
if (!event.text)
|
|
357
|
+
return null;
|
|
358
|
+
return { kind: 'thinking', text: event.text };
|
|
359
|
+
case 'tool_use': {
|
|
360
|
+
const desc = this.summarizeInput(event.input, event.name);
|
|
361
|
+
return {
|
|
362
|
+
kind: 'tool_call',
|
|
363
|
+
call_id: event.callId || this.synthCallId(),
|
|
364
|
+
name: event.name,
|
|
365
|
+
arguments: event.input,
|
|
366
|
+
text: desc,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
case 'tool_result':
|
|
370
|
+
if (event.isError) {
|
|
371
|
+
return {
|
|
372
|
+
kind: 'tool_result',
|
|
373
|
+
call_id: event.callId || this.synthCallId(),
|
|
374
|
+
name: event.name,
|
|
375
|
+
ok: false,
|
|
376
|
+
error: event.error || (typeof event.result === 'string' ? event.result : '执行失败'),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
const resultText = this.truncate(this.stringifyResult(event.result), 200);
|
|
381
|
+
return {
|
|
382
|
+
kind: 'tool_result',
|
|
383
|
+
call_id: event.callId || this.synthCallId(),
|
|
384
|
+
name: event.name,
|
|
385
|
+
ok: true,
|
|
386
|
+
result: event.result,
|
|
387
|
+
text: resultText,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
case 'compact':
|
|
391
|
+
return {
|
|
392
|
+
kind: 'notice',
|
|
393
|
+
text: `💡 会话压缩完成 (压缩前 tokens: ${event.preTokens})`,
|
|
394
|
+
severity: 'info',
|
|
395
|
+
subtype: 'compact',
|
|
396
|
+
};
|
|
397
|
+
case 'task_progress': {
|
|
398
|
+
const stats = this.formatTaskStats(event);
|
|
399
|
+
const text = event.summary
|
|
400
|
+
? `子任务: ${event.summary}${stats ? ` (${stats})` : ''}`
|
|
401
|
+
: `子任务进行中${stats ? `: ${stats}` : ''}`;
|
|
402
|
+
return {
|
|
403
|
+
kind: 'progress',
|
|
404
|
+
text,
|
|
405
|
+
state: 'processing',
|
|
406
|
+
tool_uses: event.toolUses,
|
|
407
|
+
duration_ms: event.durationMs,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
case 'error':
|
|
411
|
+
return { kind: 'notice', text: event.error, severity: 'warn' };
|
|
412
|
+
case 'complete':
|
|
413
|
+
if (event.isError) {
|
|
414
|
+
const errText = event.errors?.join('; ') || event.result || '任务失败';
|
|
415
|
+
return {
|
|
416
|
+
kind: 'summary',
|
|
417
|
+
text: errText,
|
|
418
|
+
is_error: true,
|
|
419
|
+
subtype: event.subtype,
|
|
420
|
+
duration_ms: event.durationMs,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (event.result) {
|
|
424
|
+
return {
|
|
425
|
+
kind: 'summary',
|
|
426
|
+
text: event.result,
|
|
427
|
+
subtype: event.subtype,
|
|
428
|
+
duration_ms: event.durationMs,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
case 'session_id':
|
|
433
|
+
case 'state_changed':
|
|
434
|
+
case 'status':
|
|
435
|
+
return null;
|
|
436
|
+
default:
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
summarizeInput(input, toolName) {
|
|
441
|
+
if (!input || typeof input !== 'object')
|
|
442
|
+
return '';
|
|
443
|
+
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
444
|
+
const cmd = input.command;
|
|
445
|
+
if (cmd.includes('evolclaw ctl send') || cmd.includes('evolclaw ctl file')) {
|
|
446
|
+
return cmd;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return (input.description ||
|
|
450
|
+
input.file_path ||
|
|
451
|
+
input.pattern ||
|
|
452
|
+
(typeof input.command === 'string' ? input.command.substring(0, 80) : '') ||
|
|
453
|
+
(typeof input.prompt === 'string' ? input.prompt.substring(0, 80) : '') ||
|
|
454
|
+
(typeof input.query === 'string' ? input.query.substring(0, 80) : '') ||
|
|
455
|
+
'');
|
|
456
|
+
}
|
|
457
|
+
stringifyResult(result) {
|
|
458
|
+
if (result === null || result === undefined)
|
|
459
|
+
return '';
|
|
460
|
+
if (typeof result === 'string')
|
|
461
|
+
return result;
|
|
462
|
+
try {
|
|
463
|
+
return JSON.stringify(result);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return String(result);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
truncate(text, maxLen) {
|
|
470
|
+
return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
|
|
471
|
+
}
|
|
472
|
+
formatTaskStats(event) {
|
|
473
|
+
const parts = [];
|
|
474
|
+
if (event.toolUses)
|
|
475
|
+
parts.push(`${event.toolUses} tools`);
|
|
476
|
+
if (event.durationMs)
|
|
477
|
+
parts.push(`${Math.round(event.durationMs / 1000)}s`);
|
|
478
|
+
return parts.join(', ');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把结构化 ThoughtItem 数组降级为人类可读的纯文本。
|
|
3
|
+
* 用于不支持 thought 能力的渠道(Feishu/WeChat/DingTalk/QQBot/WeCom),
|
|
4
|
+
* 在 send() 中收到 activity.batch 后调用。
|
|
5
|
+
*/
|
|
6
|
+
export function formatItemsAsText(items) {
|
|
7
|
+
if (!items || items.length === 0)
|
|
8
|
+
return '';
|
|
9
|
+
const lines = [];
|
|
10
|
+
for (const item of items) {
|
|
11
|
+
const line = formatItem(item);
|
|
12
|
+
if (line)
|
|
13
|
+
lines.push(line);
|
|
14
|
+
}
|
|
15
|
+
return lines.join('\n');
|
|
16
|
+
}
|
|
17
|
+
function formatItem(item) {
|
|
18
|
+
switch (item.kind) {
|
|
19
|
+
case 'thinking':
|
|
20
|
+
return item.text;
|
|
21
|
+
case 'reasoning':
|
|
22
|
+
return `💭 ${item.text}`;
|
|
23
|
+
case 'tool_call': {
|
|
24
|
+
const desc = item.text || summarizeArgs(item.arguments);
|
|
25
|
+
return desc ? `🔧 ${item.name}: ${desc}` : `🔧 ${item.name}`;
|
|
26
|
+
}
|
|
27
|
+
case 'tool_result': {
|
|
28
|
+
if (!item.ok) {
|
|
29
|
+
const errMsg = item.error || (typeof item.result === 'string' ? item.result : '执行失败');
|
|
30
|
+
return `⚠️ ${item.name}: ${errMsg}`;
|
|
31
|
+
}
|
|
32
|
+
return item.text ? `✅ ${item.name}: ${item.text}` : `✅ ${item.name}`;
|
|
33
|
+
}
|
|
34
|
+
case 'progress':
|
|
35
|
+
return `⏳ ${item.text}`;
|
|
36
|
+
case 'notice':
|
|
37
|
+
return item.severity === 'warn' ? `⚠️ ${item.text}` : item.text;
|
|
38
|
+
case 'summary':
|
|
39
|
+
return item.is_error ? `❌ ${item.text}` : item.text;
|
|
40
|
+
default:
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function summarizeArgs(args) {
|
|
45
|
+
if (!args || typeof args !== 'object')
|
|
46
|
+
return '';
|
|
47
|
+
const a = args;
|
|
48
|
+
if (typeof a.description === 'string')
|
|
49
|
+
return a.description;
|
|
50
|
+
if (typeof a.file_path === 'string')
|
|
51
|
+
return a.file_path;
|
|
52
|
+
if (typeof a.pattern === 'string')
|
|
53
|
+
return a.pattern;
|
|
54
|
+
if (typeof a.command === 'string')
|
|
55
|
+
return a.command.substring(0, 80);
|
|
56
|
+
if (typeof a.prompt === 'string')
|
|
57
|
+
return a.prompt.substring(0, 80);
|
|
58
|
+
if (typeof a.query === 'string')
|
|
59
|
+
return a.query.substring(0, 80);
|
|
60
|
+
return '';
|
|
61
|
+
}
|