evolclaw 3.1.0 → 3.1.1

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.
@@ -245,27 +245,29 @@ export function isRetryableError(error) {
245
245
  return true;
246
246
  return false;
247
247
  }
248
- export function getErrorMessage(error, terminalReason) {
248
+ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
249
249
  // terminalReason 提供更精确的错误提示(SDK 0.2.100+)
250
250
  if (terminalReason) {
251
+ const prefix = includeEmoji ? '❌ ' : '';
252
+ const warnPrefix = includeEmoji ? '⚠️ ' : '';
251
253
  switch (terminalReason) {
252
254
  case 'max_turns':
253
- return '❌ 任务达到最大轮次限制,请简化需求或分步执行';
255
+ return `${prefix}任务达到最大轮次限制,请简化需求或分步执行`;
254
256
  case 'prompt_too_long':
255
- return '⚠️ 输入过长,请精简提问或使用 /compact 压缩上下文';
257
+ return `${warnPrefix}输入过长,请精简提问或使用 /compact 压缩上下文`;
256
258
  case 'rapid_refill_breaker':
257
- return '⚠️ API 限流中,请稍后重试';
259
+ return `${warnPrefix}API 限流中,请稍后重试`;
258
260
  case 'context_compact_failed':
259
- return '⚠️ 上下文过长,自动压缩失败,请手动输入 /compact 重试';
261
+ return `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
260
262
  case 'model_error':
261
- return '❌ 模型服务异常,请稍后重试';
263
+ return `${prefix}模型服务异常,请稍后重试`;
262
264
  case 'tool_error':
263
- return '❌ 工具执行失败,请检查操作或重试';
265
+ return `${prefix}工具执行失败,请检查操作或重试`;
264
266
  case 'permission_denied':
265
- return '❌ 权限被拒绝,操作已取消';
267
+ return `${prefix}权限被拒绝,操作已取消`;
266
268
  case 'aborted_streaming':
267
269
  case 'aborted_tools':
268
- return '❌ 任务已中断';
270
+ return `${prefix}任务已中断`;
269
271
  }
270
272
  }
271
273
  // 回退到原有的错误消息匹配逻辑
@@ -275,15 +277,17 @@ export function getErrorMessage(error, terminalReason) {
275
277
  if (rule?.message)
276
278
  return rule.message;
277
279
  // 内置兜底规则(结构性错误)
280
+ const warnPrefix = includeEmoji ? '⚠️ ' : '';
281
+ const errPrefix = includeEmoji ? '❌ ' : '';
278
282
  if (msg.includes('CONTEXT_COMPACT_FAILED') || msg.includes('context_length_exceeded')
279
283
  || msg.includes('Context limit')) {
280
- return '⚠️ 上下文过长,自动压缩失败,请手动输入 /compact 重试';
284
+ return `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
281
285
  }
282
286
  if (msg.includes('401') || msg.includes('authentication_error')) {
283
- return '❌ API Key 无效,请检查密钥配置。使用 /status 查看当前配置';
287
+ return `${errPrefix}API Key 无效,请检查密钥配置。使用 /status 查看当前配置`;
284
288
  }
285
289
  if (msg.includes('timeout')) {
286
- return '⚠️ 请求超时,请重试';
290
+ return `${warnPrefix}请求超时,请重试`;
287
291
  }
288
- return '❌ 处理消息时出错,请稍后重试';
292
+ return `${errPrefix}处理消息时出错,请稍后重试`;
289
293
  }
@@ -1,3 +1,5 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  export class StatsCollector {
2
4
  events = [];
3
5
  startTime;
@@ -100,6 +102,71 @@ export class StatsCollector {
100
102
  export class AidStatsCollector {
101
103
  entries = new Map();
102
104
  queueStatsProvider;
105
+ /** sessionId → 当前正在跑该 session 的 agent,task:started 写入,task:completed/error 清除 */
106
+ sessionToAgent = new Map();
107
+ constructor(eventBus) {
108
+ if (!eventBus)
109
+ return;
110
+ eventBus.subscribe('task:started', (event) => {
111
+ const e = event;
112
+ if (e.agentName)
113
+ this.onTaskStart(e.agentName, e.encrypt, e.chatmode);
114
+ if (e.agentName && e.sessionId)
115
+ this.sessionToAgent.set(e.sessionId, e.agentName);
116
+ });
117
+ eventBus.subscribe('task:completed', (event) => {
118
+ const e = event;
119
+ if (e.agentName)
120
+ this.onTaskEnd(e.agentName, 'completed', undefined, e.finalText, e.numTurns);
121
+ if (e.sessionId)
122
+ this.sessionToAgent.delete(e.sessionId);
123
+ });
124
+ eventBus.subscribe('task:error', (event) => {
125
+ const e = event;
126
+ if (e.agentName)
127
+ this.onTaskEnd(e.agentName, 'error', e.errorType);
128
+ if (e.sessionId)
129
+ this.sessionToAgent.delete(e.sessionId);
130
+ });
131
+ // thought.put 次数 + 最后一次 thought 文本
132
+ // 注意:thought.put 是 fire-and-forget async,可能在 task:completed 之后才到达,
133
+ // 所以同时累加到 currentTask(task 进行中)或 lastTaskEnd(task 已结束但 thought 属于它)
134
+ eventBus.subscribe('message:thought-put', (event) => {
135
+ const e = event;
136
+ if (!e.agentName)
137
+ return;
138
+ const entry = this.entries.get(e.agentName);
139
+ if (!entry)
140
+ return;
141
+ if (entry.currentTaskStartAt != null) {
142
+ // task 进行中
143
+ entry.currentTaskThoughtPutCount++;
144
+ if (e.text)
145
+ entry.currentTaskLastThoughtText = e.text;
146
+ }
147
+ else if (entry.lastTaskEnd) {
148
+ // task 已结束,回填到最近一次 task
149
+ entry.lastTaskEnd.thoughtPutCount++;
150
+ entry.lastTaskEnd.thoughtDuringTask = true;
151
+ if (e.text) {
152
+ const t = e.text.length > 100 ? e.text.slice(0, 100) + '…' : e.text;
153
+ entry.lastTaskEnd.lastThoughtText = t;
154
+ }
155
+ }
156
+ });
157
+ // 工具调用次数(tool:use 事件)
158
+ eventBus.subscribe('tool:use', (event) => {
159
+ const e = event;
160
+ if (!e.sessionId)
161
+ return;
162
+ const agent = this.sessionToAgent.get(e.sessionId);
163
+ if (!agent)
164
+ return;
165
+ const entry = this.entries.get(agent);
166
+ if (entry && entry.currentTaskStartAt != null)
167
+ entry.currentTaskToolUseCount++;
168
+ });
169
+ }
103
170
  setQueueStatsProvider(provider) {
104
171
  this.queueStatsProvider = provider;
105
172
  }
@@ -119,19 +186,145 @@ export class AidStatsCollector {
119
186
  lastSentAt: null,
120
187
  lastReceivedText: null,
121
188
  lastReceivedFrom: null,
189
+ lastReceivedEncrypt: null,
190
+ lastReceivedChatmode: null,
122
191
  lastSentText: null,
123
192
  lastSentTo: null,
193
+ lastSentEncrypt: null,
194
+ lastSentChatmode: null,
124
195
  uniquePeers: new Set(),
196
+ currentTaskStartAt: null,
197
+ currentTaskReplyCount: 0,
198
+ currentTaskThoughtPutCount: 0,
199
+ currentTaskToolUseCount: 0,
200
+ currentTaskNumTurns: 0,
201
+ currentTaskLastThoughtText: null,
202
+ currentTaskSessionId: null,
203
+ currentTaskChatmode: null,
204
+ currentTaskEncrypt: null,
205
+ lastTaskEnd: undefined,
125
206
  };
126
207
  this.entries.set(aid, entry);
127
208
  }
128
209
  return entry;
129
210
  }
211
+ sessionsDir;
212
+ setSessionsDir(dir) {
213
+ this.sessionsDir = dir;
214
+ }
215
+ onTaskStart(aid, encrypt, chatmode) {
216
+ const entry = this.getOrCreate(aid);
217
+ entry.currentTaskStartAt = Date.now();
218
+ entry.currentTaskReplyCount = 0;
219
+ entry.currentTaskThoughtPutCount = 0;
220
+ entry.currentTaskToolUseCount = 0;
221
+ entry.currentTaskNumTurns = 0;
222
+ entry.currentTaskLastThoughtText = null;
223
+ entry.currentTaskChatmode = chatmode ?? null;
224
+ entry.currentTaskEncrypt = encrypt ?? null;
225
+ }
226
+ onTaskEnd(aid, status, errorType, finalText, numTurns) {
227
+ const entry = this.getOrCreate(aid);
228
+ const startedAt = entry.currentTaskStartAt;
229
+ const taskEndTs = Date.now();
230
+ // 先用内存计数写入初始值(立即可用)
231
+ const buildTaskEnd = (msgCount, thoughtCount, lastThought) => ({
232
+ ts: taskEndTs,
233
+ status,
234
+ errorType,
235
+ sentDuringTask: msgCount > 0,
236
+ thoughtDuringTask: thoughtCount > 0,
237
+ lastThoughtText: lastThought,
238
+ replyCount: msgCount,
239
+ thoughtPutCount: thoughtCount,
240
+ toolUseCount: entry.currentTaskToolUseCount,
241
+ numTurns: numTurns ?? entry.currentTaskNumTurns,
242
+ finalText: finalText ? (finalText.length > 100 ? finalText.slice(0, 100) + '…' : finalText) : undefined,
243
+ chatmode: entry.currentTaskChatmode ?? undefined,
244
+ encrypt: entry.currentTaskEncrypt ?? undefined,
245
+ });
246
+ entry.lastTaskEnd = buildTaskEnd(entry.currentTaskReplyCount, entry.currentTaskThoughtPutCount, entry.currentTaskLastThoughtText ?? undefined);
247
+ // 500ms 后从 jsonl 重新统计(覆盖 thought.put 异步延迟问题)
248
+ if (this.sessionsDir && startedAt != null) {
249
+ const sessionsDir = this.sessionsDir;
250
+ const toolUseCount = entry.currentTaskToolUseCount;
251
+ const resolvedNumTurns = numTurns ?? entry.currentTaskNumTurns;
252
+ const chatmode = entry.currentTaskChatmode;
253
+ const encrypt = entry.currentTaskEncrypt;
254
+ setTimeout(() => {
255
+ try {
256
+ const { chatDirPath } = require('../core/session/session-fs-store.js');
257
+ // 找该 aid 下所有 peer 的 messages.jsonl,统计 ts >= startedAt 的出站条目
258
+ const aidDir = path.join(sessionsDir, 'aun', aid.replace(/[/%\\:*?"<>|]/g, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')));
259
+ if (!fs.existsSync(aidDir))
260
+ return;
261
+ let msgCount = 0, thoughtCount = 0;
262
+ let lastThoughtText;
263
+ let lastMsgText;
264
+ for (const peerDir of fs.readdirSync(aidDir, { withFileTypes: true })) {
265
+ if (!peerDir.isDirectory() || peerDir.name.startsWith('_'))
266
+ continue;
267
+ const msgFile = path.join(aidDir, peerDir.name, 'messages.jsonl');
268
+ if (!fs.existsSync(msgFile))
269
+ continue;
270
+ const lines = fs.readFileSync(msgFile, 'utf-8').split('\n').filter(Boolean);
271
+ for (const line of lines) {
272
+ try {
273
+ const rec = JSON.parse(line);
274
+ if (rec.dir !== 'out' || rec.ts < startedAt || rec.ts > taskEndTs + 2000)
275
+ continue;
276
+ if (rec.msgType === 'thought') {
277
+ thoughtCount++;
278
+ if (rec.content)
279
+ lastThoughtText = rec.content.length > 100 ? rec.content.slice(0, 100) + '…' : rec.content;
280
+ }
281
+ else if (rec.msgType === 'text') {
282
+ msgCount++;
283
+ if (rec.content)
284
+ lastMsgText = rec.content.length > 100 ? rec.content.slice(0, 100) + '…' : rec.content;
285
+ }
286
+ }
287
+ catch { }
288
+ }
289
+ }
290
+ const currentEntry = this.entries.get(aid);
291
+ if (currentEntry?.lastTaskEnd?.ts === taskEndTs) {
292
+ currentEntry.lastTaskEnd = {
293
+ ts: taskEndTs,
294
+ status,
295
+ errorType,
296
+ sentDuringTask: msgCount > 0,
297
+ thoughtDuringTask: thoughtCount > 0,
298
+ lastThoughtText: lastThoughtText,
299
+ replyCount: msgCount,
300
+ thoughtPutCount: thoughtCount,
301
+ toolUseCount,
302
+ numTurns: resolvedNumTurns,
303
+ finalText: finalText ? (finalText.length > 100 ? finalText.slice(0, 100) + '…' : finalText) : undefined,
304
+ chatmode: chatmode ?? undefined,
305
+ encrypt: encrypt ?? undefined,
306
+ };
307
+ // 更新 lastSentText 为最后一条 msg(如果有)
308
+ if (lastMsgText && msgCount > 0) {
309
+ currentEntry.lastSentText = lastMsgText;
310
+ }
311
+ }
312
+ }
313
+ catch { }
314
+ }, 500);
315
+ }
316
+ entry.currentTaskStartAt = null;
317
+ entry.currentTaskReplyCount = 0;
318
+ entry.currentTaskThoughtPutCount = 0;
319
+ entry.currentTaskToolUseCount = 0;
320
+ entry.currentTaskNumTurns = 0;
321
+ entry.currentTaskLastThoughtText = null;
322
+ }
130
323
  setSelfName(aid, name) {
131
324
  const entry = this.getOrCreate(aid);
132
325
  entry.selfName = name;
133
326
  }
134
- recordInbound(aid, fromPeer, byteLength, text, isSystem = false) {
327
+ recordInbound(aid, fromPeer, byteLength, text, isSystem = false, encrypt, chatmode) {
135
328
  const entry = this.getOrCreate(aid);
136
329
  if (isSystem) {
137
330
  entry.systemReceived++;
@@ -142,11 +335,15 @@ export class AidStatsCollector {
142
335
  entry.lastReceivedFrom = fromPeer;
143
336
  if (text)
144
337
  entry.lastReceivedText = text.length > 100 ? text.slice(0, 100) + '…' : text;
338
+ if (encrypt != null)
339
+ entry.lastReceivedEncrypt = encrypt;
340
+ if (chatmode)
341
+ entry.lastReceivedChatmode = chatmode;
145
342
  }
146
343
  entry.bytesReceived += byteLength;
147
344
  entry.uniquePeers.add(fromPeer);
148
345
  }
149
- recordOutbound(aid, toPeer, byteLength, text, isSystem = false) {
346
+ recordOutbound(aid, toPeer, byteLength, text, isSystem = false, encrypt, chatmode) {
150
347
  const entry = this.getOrCreate(aid);
151
348
  if (isSystem) {
152
349
  entry.systemSent++;
@@ -157,6 +354,18 @@ export class AidStatsCollector {
157
354
  entry.lastSentTo = toPeer;
158
355
  if (text)
159
356
  entry.lastSentText = text.length > 100 ? text.slice(0, 100) + '…' : text;
357
+ if (encrypt != null)
358
+ entry.lastSentEncrypt = encrypt;
359
+ if (chatmode)
360
+ entry.lastSentChatmode = chatmode;
361
+ // 累计当前 task 的回复数
362
+ if (entry.currentTaskStartAt != null) {
363
+ entry.currentTaskReplyCount++;
364
+ if (chatmode)
365
+ entry.currentTaskChatmode = chatmode;
366
+ if (encrypt != null)
367
+ entry.currentTaskEncrypt = encrypt;
368
+ }
160
369
  }
161
370
  entry.bytesSent += byteLength;
162
371
  entry.uniquePeers.add(toPeer);
@@ -180,11 +389,16 @@ export class AidStatsCollector {
180
389
  lastSentAt: entry.lastSentAt,
181
390
  lastReceivedText: entry.lastReceivedText,
182
391
  lastReceivedFrom: entry.lastReceivedFrom,
392
+ lastReceivedEncrypt: entry.lastReceivedEncrypt,
393
+ lastReceivedChatmode: entry.lastReceivedChatmode,
183
394
  lastSentText: entry.lastSentText,
184
395
  lastSentTo: entry.lastSentTo,
396
+ lastSentEncrypt: entry.lastSentEncrypt,
397
+ lastSentChatmode: entry.lastSentChatmode,
185
398
  uniquePeerCount: entry.uniquePeers.size,
186
399
  processing: queueStats.processing,
187
400
  queued: queueStats.queued,
401
+ lastTaskEnd: entry.lastTaskEnd,
188
402
  });
189
403
  }
190
404
  return out;
@@ -1,25 +1,72 @@
1
1
  # 私聊消息命令
2
2
 
3
- <!-- TODO: 填充私聊消息命令详细参考 -->
4
-
5
3
  ## 发送消息
6
4
 
5
+ ### 以指定 AID 发送(首选)
6
+
7
+ ```bash
8
+ # 明文
9
+ ec msg send <from-aid> <to-aid> "<message>"
10
+
11
+ # 密文(E2EE)
12
+ ec msg send <from-aid> <to-aid> "<message>" --encrypt
13
+ ```
14
+
15
+ ### 发送文件
16
+
7
17
  ```bash
8
- evolclaw msg send <from-aid> <to-aid> "<message>"
18
+ ec msg send <from-aid> <to-aid> --file <path>
19
+ ec msg send <from-aid> <to-aid> --file <path> --as image
20
+ ec msg send <from-aid> <to-aid> --file <path> --encrypt
9
21
  ```
10
22
 
23
+ `--as` 可选值:`image` | `video` | `voice` | `file`(默认按扩展名推断)
24
+
11
25
  ## 拉取消息
12
26
 
13
27
  ```bash
14
- evolclaw msg pull <self-aid> --app <app-name>
28
+ ec msg pull <self-aid> --app <app-name>
29
+ ec msg pull <self-aid> --app <app-name> --after-seq <N> --limit <N>
30
+ ```
31
+
32
+ ## 确认消息已读
33
+
34
+ ```bash
35
+ ec msg ack <self-aid> <seq> --app <app-name>
15
36
  ```
16
37
 
17
- ## 确认消息
38
+ `--app` 必须传,否则会污染 daemon 游标。
39
+
40
+ ## 撤回消息
18
41
 
19
42
  ```bash
20
- evolclaw msg ack <self-aid> --app <app-name> --seq <seq>
43
+ ec msg recall <self-aid> <message-id>
44
+ ```
45
+
46
+ ## 查询在线状态
47
+
48
+ ```bash
49
+ ec msg online <self-aid> <target-aid>
21
50
  ```
22
51
 
23
52
  ## 自主回复策略
24
53
 
25
54
  收到消息 ≠ 必须回复。是否回复、怎么回复、何时回复由 agent 自主决定。
55
+
56
+ 加密策略:
57
+ - 对端发来密文消息时,回复也应使用 `--encrypt`(保持对话加密一致性)
58
+ - 对端发来明文消息时,默认明文回复
59
+
60
+ ## 在当前会话中快速回复(备选)
61
+
62
+ 仅当无法使用 `ec msg send` 时(如不知道自己的 AID),可用 `ec ctl send`:
63
+
64
+ ```bash
65
+ # 明文
66
+ ec ctl send "<text>"
67
+
68
+ # 密文
69
+ ec ctl send --encrypt "<text>"
70
+ ```
71
+
72
+ `ec ctl send` 自动继承当前会话的 AID 和对端,无需指定。
@@ -13,6 +13,36 @@
13
13
 
14
14
  与其他主体通信时,**必须调用 CLI 命令**发消息,不要把输出当成发送给对方的内容。
15
15
 
16
+ ### 必须使用 `ec msg send`(首选)
17
+
18
+ ```bash
19
+ # 明文
20
+ ec msg send <self-aid> <to-aid> "<text>"
21
+
22
+ # 密文
23
+ ec msg send <self-aid> <to-aid> "<text>" --encrypt
24
+ ```
25
+
26
+ `<self-aid>` 是注入上下文里的 selfAid,`<to-aid>` 是 peerKey 解析出的对端 AID。
27
+
28
+ ### 仅在无法获取 selfAid 时才用 `ec ctl send`
29
+
30
+ ```bash
31
+ ec ctl send "<text>"
32
+ ec ctl send --encrypt "<text>"
33
+ ```
34
+
35
+ ### 加密策略
36
+
37
+ 对端发来密文消息时回复必须加密;明文消息默认明文回复。
38
+
39
+ ### 命令返回值
40
+
41
+ - 成功:`ok` 或包含 `✓ 已发送 ...` 的输出(exit 0)
42
+ - 失败:`✗ ...` 错误信息(exit 非零)
43
+
44
+ 发送成功后**继续后续处理**。一次任务可能发 0 到多条消息,不要因为看到"已发送"就反复发送同一条消息。
45
+
16
46
  不同渠道有不同的命令行工具,使用方式参见各渠道文档。
17
47
 
18
48
  ## Agent 管理命令
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "prepublishOnly": "npm run build && npm test"
27
27
  },
28
28
  "dependencies": {
29
- "@agentunion/fastaun": "^0.3.0",
29
+ "@agentunion/fastaun": "^0.3.2",
30
30
  "@anthropic-ai/claude-agent-sdk": "^0.2.100",
31
31
  "cron-parser": "^5.5.0",
32
32
  "image-type": "^6.0.0",