evolclaw 3.1.0 → 3.1.2

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 (45) hide show
  1. package/CHANGELOG.md +407 -0
  2. package/README.md +1 -1
  3. package/SKILLS.md +311 -0
  4. package/dist/agents/claude-runner.js +40 -3
  5. package/dist/aun/aid/agentmd.js +7 -6
  6. package/dist/aun/aid/client.js +5 -11
  7. package/dist/aun/aid/identity.js +32 -13
  8. package/dist/aun/msg/group.js +1 -0
  9. package/dist/aun/msg/p2p.js +51 -0
  10. package/dist/aun/msg/upload.js +57 -18
  11. package/dist/channels/aun.js +124 -50
  12. package/dist/channels/dingtalk.js +2 -0
  13. package/dist/channels/feishu.js +15 -6
  14. package/dist/channels/qqbot.js +2 -0
  15. package/dist/channels/wechat.js +2 -0
  16. package/dist/channels/wecom.js +2 -0
  17. package/dist/cli/agent.js +130 -35
  18. package/dist/cli/index.js +221 -48
  19. package/dist/cli/init-channel.js +4 -2
  20. package/dist/cli/init.js +44 -23
  21. package/dist/cli/watch-msg.js +109 -30
  22. package/dist/config-store.js +67 -1
  23. package/dist/core/channel-loader.js +4 -4
  24. package/dist/core/command-handler.js +95 -84
  25. package/dist/core/evolagent-registry.js +45 -9
  26. package/dist/core/evolagent.js +4 -4
  27. package/dist/core/message/im-renderer.js +47 -8
  28. package/dist/core/message/message-bridge.js +30 -1
  29. package/dist/core/message/message-log.js +6 -1
  30. package/dist/core/message/message-processor.js +29 -35
  31. package/dist/core/relation/peer-identity.js +161 -0
  32. package/dist/core/session/session-fs-store.js +23 -0
  33. package/dist/core/session/session-manager.js +11 -4
  34. package/dist/core/trigger/manager.js +16 -0
  35. package/dist/core/trigger/parser.js +110 -0
  36. package/dist/core/trigger/scheduler.js +6 -0
  37. package/dist/index.js +64 -20
  38. package/dist/paths.js +35 -0
  39. package/dist/utils/cross-platform.js +2 -1
  40. package/dist/utils/error-utils.js +17 -13
  41. package/dist/utils/stats.js +216 -2
  42. package/kits/docs/INDEX.md +6 -0
  43. package/kits/docs/evolclaw/MSG_PRIVATE.md +53 -6
  44. package/kits/rules/06-channel.md +30 -0
  45. package/package.json +6 -3
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session-file-adapter.js';
2
2
  import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
3
3
  import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
4
- import { ensureDataDirs, resolvePaths, getPackageRoot } from './paths.js';
4
+ import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath } from './paths.js';
5
5
  import { resolveAnthropicConfig } from './agents/resolve.js';
6
6
  import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded } from './config-store.js';
7
7
  import { CONFIG_SCHEMA_VERSION } from './types.js';
@@ -41,7 +41,6 @@ import { agentTriggersDir } from './paths.js';
41
41
  import { isLinkedInstall } from './utils/npm-ops.js';
42
42
  import path from 'path';
43
43
  import fs from 'fs';
44
- import os from 'os';
45
44
  import crypto from 'crypto';
46
45
  import { fileURLToPath } from 'url';
47
46
  /** 出站 payload 摘要(用于 channel-out.log) */
@@ -124,6 +123,46 @@ function readFastaunVersion() {
124
123
  }
125
124
  }
126
125
  async function main() {
126
+ // 启动信息:目录类型 + 版本号 + 代码最新时间戳
127
+ {
128
+ const pkgRoot = getPackageRoot();
129
+ const runDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'));
130
+ const isDist = runDir.includes(path.join(pkgRoot, 'dist'));
131
+ const isLinked = fs.existsSync(path.join(pkgRoot, '.git'));
132
+ const dirType = isDist ? (isLinked ? '开发仓/dist' : '安装路径/dist') : '源码(tsx)';
133
+ const scanDir = isDist ? path.join(pkgRoot, 'dist') : path.join(pkgRoot, 'src');
134
+ let latestMtime = 0;
135
+ const scanRecursive = (dir) => {
136
+ try {
137
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
138
+ if (entry.name === 'node_modules')
139
+ continue;
140
+ const full = path.join(dir, entry.name);
141
+ if (entry.isDirectory()) {
142
+ scanRecursive(full);
143
+ continue;
144
+ }
145
+ if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
146
+ const mt = fs.statSync(full).mtimeMs;
147
+ if (mt > latestMtime)
148
+ latestMtime = mt;
149
+ }
150
+ }
151
+ }
152
+ catch { }
153
+ };
154
+ scanRecursive(scanDir);
155
+ let version = '?';
156
+ try {
157
+ version = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8')).version;
158
+ }
159
+ catch { }
160
+ const fmtTime = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`; };
161
+ console.error(`[EvolClaw] EvolClaw v${version}`);
162
+ console.error(`[EvolClaw] 执行类型: ${dirType}`);
163
+ console.error(`[EvolClaw] 包路径: ${pkgRoot}`);
164
+ console.error(`[EvolClaw] 代码时间: ${latestMtime ? fmtTime(latestMtime) : '?'}`);
165
+ }
127
166
  // 过滤飞书 SDK 的 info 日志
128
167
  const originalLog = console.log;
129
168
  const originalInfo = console.info;
@@ -261,16 +300,22 @@ async function main() {
261
300
  // 统计收集器(近 1 小时滚动统计)
262
301
  const statsCollector = new StatsCollector(eventBus);
263
302
  // Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
264
- const aidStatsCollector = new AidStatsCollector();
303
+ const aidStatsCollector = new AidStatsCollector(eventBus);
304
+ aidStatsCollector.setSessionsDir(paths.sessionsDir);
265
305
  // 初始化 SessionManager(文件系统后端)
266
306
  const sessionManager = new SessionManager(paths.sessionsDir, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId), (channel, userId) => agentRegistry.isAdmin(channel, userId));
267
307
  // sessionMode 解析:从 channel 路由到具体 agent,按 agent.config.chatmode
268
- sessionManager.setSessionModeResolver((channelKey, chatType) => {
308
+ sessionManager.setSessionModeResolver((channelKey, chatType, peerType) => {
269
309
  const agent = agentRegistry.resolveByChannel(channelKey);
270
310
  const cm = agent?.config.chatmode;
271
311
  if (!cm)
272
312
  return undefined;
273
- return chatType === 'group' ? cm.group : cm.private;
313
+ // 优先级:群聊 > nothuman > private
314
+ if (chatType === 'group')
315
+ return cm.group;
316
+ if (peerType && peerType !== 'human' && peerType !== 'unknown')
317
+ return cm.nothuman;
318
+ return cm.private;
274
319
  });
275
320
  logger.info('✓ Database initialized');
276
321
  // 注册会话文件适配器(Claude / Codex 各自的会话文件操作)
@@ -427,7 +472,7 @@ async function main() {
427
472
  logger.error(`[Trigger] Scheduler init failed for ${agent.aid}: ${err}`);
428
473
  }
429
474
  }
430
- // Inject primary agent's trigger scheduler into cmdHandler
475
+ // Inject primary agent's trigger scheduler as fallback (used when owning agent has no scheduler)
431
476
  const primaryAgentForTrigger = agentRegistry.runnableAgents()[0];
432
477
  if (primaryAgentForTrigger?.triggerScheduler && primaryAgentForTrigger?.triggerManager) {
433
478
  cmdHandler.setTriggerScheduler(primaryAgentForTrigger.triggerScheduler, primaryAgentForTrigger.triggerManager);
@@ -502,10 +547,10 @@ async function main() {
502
547
  if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
503
548
  inst.channel.onProjectPathRequest(async (channelId) => {
504
549
  // Effective default path: use the agent that owns this channel.
505
- const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelName);
550
+ const owningAgent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
506
551
  const effectiveDefault = owningAgent?.projectPath
507
552
  ?? primaryAgent.projectPath;
508
- const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
553
+ const session = await sessionManager.getOrCreateSession(inst.adapter.channelKey, channelId, effectiveDefault, undefined, undefined, undefined, undefined);
509
554
  return path.isAbsolute(session.projectPath)
510
555
  ? session.projectPath
511
556
  : path.resolve(process.cwd(), session.projectPath);
@@ -547,10 +592,10 @@ async function main() {
547
592
  }
548
593
  // Bind adapters to their owning agents and mark running
549
594
  for (const inst of channelInstances) {
550
- const agent = agentRegistry.resolveByChannel(inst.adapter.channelName);
595
+ const agent = agentRegistry.resolveByChannel(inst.adapter.channelKey);
551
596
  if (!agent || agent.status === 'error')
552
597
  continue;
553
- agent.channels.set(inst.adapter.channelName, inst.adapter);
598
+ agent.channels.set(inst.adapter.channelKey, inst.adapter);
554
599
  if (agent.status === 'stopped') {
555
600
  agent.status = 'running';
556
601
  }
@@ -565,7 +610,7 @@ async function main() {
565
610
  for (const inst of channelInstances) {
566
611
  const channelType = inst.channelType || inst.adapter.channelName;
567
612
  if (channelType === 'feishu' && 'preloadThreads' in inst.channel) {
568
- const threadIds = sessionManager.getKnownThreadIds(inst.adapter.channelName);
613
+ const threadIds = sessionManager.getKnownThreadIds(inst.adapter.channelKey);
569
614
  inst.channel.preloadThreads(threadIds);
570
615
  }
571
616
  }
@@ -597,9 +642,8 @@ async function main() {
597
642
  // 尝试从 agent.md 读取 name
598
643
  let agentName = agent.aid;
599
644
  try {
600
- const aunPath = process.env.AUN_HOME || path.join(os.homedir(), '.aun');
601
- const agentMdPath = path.join(aunPath, 'AIDs', agent.aid, 'agent.md');
602
- const content = fs.readFileSync(agentMdPath, 'utf-8');
645
+ const mdPath = agentMdPath(agent.aid);
646
+ const content = fs.readFileSync(mdPath, 'utf-8');
603
647
  const nameMatch = content.match(/^name:\s*"?([^"\n]+)/m);
604
648
  if (nameMatch)
605
649
  agentName = nameMatch[1].trim().replace(/"$/, '');
@@ -636,14 +680,14 @@ async function main() {
636
680
  continue; // 跳过同类型通道
637
681
  if (notified.has(otherType))
638
682
  continue; // 同类型已通知过
639
- const ownerId = agentRegistry.getOwner(other.adapter.channelName);
683
+ const ownerId = agentRegistry.getOwner(other.adapter.channelKey);
640
684
  if (!ownerId)
641
685
  continue;
642
686
  notified.add(otherType);
643
- const owningAgent = agentRegistry.resolveByChannel(other.adapter.channelName);
687
+ const owningAgent = agentRegistry.resolveByChannel(other.adapter.channelKey);
644
688
  const envelope = buildEnvelope({
645
689
  taskId: `system-channel-down-${crypto.randomBytes(5).toString('hex')}`,
646
- channel: other.adapter.channelName,
690
+ channel: other.adapter.channelKey,
647
691
  channelId: ownerId,
648
692
  agentName: owningAgent?.aid || 'evolclaw',
649
693
  });
@@ -734,10 +778,10 @@ async function main() {
734
778
  const replyContext = pending.rootId
735
779
  ? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
736
780
  : undefined;
737
- const owningAgent = agentRegistry.resolveByChannel(adapter.channelName);
781
+ const owningAgent = agentRegistry.resolveByChannel(adapter.channelKey);
738
782
  const envelope = buildEnvelope({
739
783
  taskId: `system-restart-${process.pid}`,
740
- channel: adapter.channelName,
784
+ channel: adapter.channelKey,
741
785
  channelId: pending.channelId,
742
786
  agentName: owningAgent?.aid || 'evolclaw',
743
787
  replyContext,
@@ -837,7 +881,7 @@ async function main() {
837
881
  const instances = await channelLoader.createForAgent(agent);
838
882
  for (const inst of instances) {
839
883
  registerChannelInstance(inst);
840
- agent.channels.set(inst.adapter.channelName, inst.adapter);
884
+ agent.channels.set(inst.adapter.channelKey, inst.adapter);
841
885
  channelInstances.push(inst);
842
886
  }
843
887
  agent.status = 'running';
package/dist/paths.js CHANGED
@@ -45,6 +45,16 @@ export function resolvePaths() {
45
45
  aidLogsDir: path.join(root, 'logs', 'aids'),
46
46
  };
47
47
  }
48
+ // ── AID 路径(agent.md 存放在 $EVOLCLAW_HOME/AIDs/<aid>/)──
49
+ export function aidsDir() {
50
+ return path.join(resolveRoot(), 'AIDs');
51
+ }
52
+ export function aidLocalDir(aid) {
53
+ return path.join(resolveRoot(), 'AIDs', aid);
54
+ }
55
+ export function agentMdPath(aid) {
56
+ return path.join(resolveRoot(), 'AIDs', aid, 'agent.md');
57
+ }
48
58
  // ── per-agent 路径(参数化,不进 resolvePaths() 的固定 map)──
49
59
  export function agentDir(aid) {
50
60
  return path.join(resolveRoot(), 'agents', aid);
@@ -98,6 +108,31 @@ export function ensureDataDirs() {
98
108
  fs.mkdirSync(p.outboxDir, { recursive: true });
99
109
  fs.mkdirSync(p.eckDir, { recursive: true });
100
110
  fs.mkdirSync(eckDebugDir(), { recursive: true });
111
+ fs.mkdirSync(aidsDir(), { recursive: true });
112
+ migrateAgentMdFromAun();
113
+ }
114
+ /**
115
+ * One-time migration: copy agent.md from ~/.aun/AIDs/<aid>/ to $EVOLCLAW_HOME/AIDs/<aid>/
116
+ * if the new location doesn't have it yet.
117
+ */
118
+ function migrateAgentMdFromAun() {
119
+ const aunAidsDir = path.join(os.homedir(), '.aun', 'AIDs');
120
+ const ecAids = aidsDir();
121
+ if (!fs.existsSync(aunAidsDir) || aunAidsDir === ecAids)
122
+ return;
123
+ try {
124
+ for (const entry of fs.readdirSync(aunAidsDir, { withFileTypes: true })) {
125
+ if (!entry.isDirectory())
126
+ continue;
127
+ const oldMd = path.join(aunAidsDir, entry.name, 'agent.md');
128
+ const newMd = path.join(ecAids, entry.name, 'agent.md');
129
+ if (fs.existsSync(oldMd) && !fs.existsSync(newMd)) {
130
+ fs.mkdirSync(path.join(ecAids, entry.name), { recursive: true });
131
+ fs.copyFileSync(oldMd, newMd);
132
+ }
133
+ }
134
+ }
135
+ catch { /* best-effort migration */ }
101
136
  }
102
137
  // ── kits 路径(始终从包内读取,不复制到 EVOLCLAW_HOME)──
103
138
  export function kitsDir() {
@@ -12,7 +12,8 @@ export const isWindows = process.platform === 'win32';
12
12
  * C:\Users\project -> C--Users-project
13
13
  */
14
14
  export function encodePath(projectPath) {
15
- return projectPath.replace(/[/\\:]/g, '-');
15
+ const normalized = projectPath.replace(/[/\\]+$/, '');
16
+ return normalized.replace(/[/\\:]/g, '-');
16
17
  }
17
18
  /**
18
19
  * Cross-platform process liveness check.
@@ -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;
@@ -50,3 +50,9 @@
50
50
  | 路径注册表模板 | `eck_templates/path-registry.template.md` | 路径实例模板 |
51
51
  | 索引模板 | `eck_templates/INDEX.template.md` | agent 级索引模板 |
52
52
  | 指南模板 | `eck_templates/GUIDE.template.md` | agent 级查阅指南模板 |
53
+
54
+ ## Base Agent
55
+
56
+ | 文档 | 路径 | 说明 |
57
+ |------|------|------|
58
+ | Claude Code 日志 | `baseagent/cc-logs.md` | CC 会话日志查阅(找完整对话/工具调用/注入) |
@@ -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 和对端,无需指定。