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
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { resolvePaths, getPackageRoot } from '../paths.js';
3
+ import { resolvePaths } from '../paths.js';
4
4
  import { decodeDirSegment, readAllJsonlLines } from '../core/session/session-fs-store.js';
5
5
  // ==================== ANSI ====================
6
6
  const isTTY = !!process.stdout.isTTY;
@@ -84,6 +84,34 @@ function formatTimeAgo(ms) {
84
84
  return `${hour}h`;
85
85
  return `${Math.floor(hour / 24)}d`;
86
86
  }
87
+ function getCodeTime(pkgRoot) {
88
+ let latestMtime = 0;
89
+ const scanDir = fs.existsSync(path.join(pkgRoot, 'dist')) ? path.join(pkgRoot, 'dist') : path.join(pkgRoot, 'src');
90
+ const scanRecursive = (dir) => {
91
+ try {
92
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
93
+ if (entry.name === 'node_modules')
94
+ continue;
95
+ const full = path.join(dir, entry.name);
96
+ if (entry.isDirectory()) {
97
+ scanRecursive(full);
98
+ continue;
99
+ }
100
+ if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
101
+ const mt = fs.statSync(full).mtimeMs;
102
+ if (mt > latestMtime)
103
+ latestMtime = mt;
104
+ }
105
+ }
106
+ }
107
+ catch { }
108
+ };
109
+ scanRecursive(scanDir);
110
+ if (!latestMtime)
111
+ return '?';
112
+ const d = new Date(latestMtime);
113
+ return `${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')}`;
114
+ }
87
115
  function formatNumber(n) {
88
116
  return n.toLocaleString('en-US');
89
117
  }
@@ -267,43 +295,73 @@ function renderStatsPanel(state, width, height) {
267
295
  // ==================== Messages Panel ====================
268
296
  function renderMessagesPanel(state, width, height) {
269
297
  const lines = [];
270
- const title = `${DIM}─ Messages ─${RST}`;
298
+ const lastTs = state.messages.length > 0 ? state.messages[state.messages.length - 1].ts : 0;
299
+ const lastTimeStr = lastTs ? formatDateTime(lastTs) : '--';
300
+ const title = `${DIM}─ Messages (${state.messages.length}, last: ${lastTimeStr}) ─${RST}`;
271
301
  lines.push(padRight(title, width));
272
302
  const contentHeight = height - 1;
273
303
  const msgs = state.messages;
274
304
  const totalMsgs = msgs.length;
275
- const visibleCount = contentHeight;
276
- const startIdx = Math.max(0, totalMsgs - visibleCount - state.messageScrollOffset);
277
- const endIdx = Math.min(totalMsgs, startIdx + visibleCount);
278
- const scrollbar = renderScrollbar(totalMsgs, visibleCount, state.messageScrollOffset, contentHeight);
279
305
  const msgWidth = width - 3;
280
306
  const contentLineWidth = msgWidth - 2;
281
307
  const maxContentLines = 3;
282
- for (let i = startIdx; i < endIdx; i++) {
283
- const m = msgs[i];
308
+ // 先构造每条消息的渲染行,从最末尾的可见消息往回收集,直到填满 contentHeight
309
+ function renderOneMsg(m) {
284
310
  const time = formatDateTime(m.ts);
285
311
  const dir = m.dir === 'in' ? `${GREEN}↓${RST}` : `${BLUE}↑${RST}`;
286
312
  const isGroup = m.chatType === 'group';
287
- const chatTag = isGroup ? `${MAGENTA}[群聊]${RST} ` : '';
313
+ const chatTag = isGroup ? `${MAGENTA}[群聊]${RST}` : '';
314
+ const encLabel = m.encrypt ? '密文' : '明文';
315
+ const modeLabel = m.chatmode === 'proactive' ? '自主' : '响应';
316
+ const metaTags = (m.encrypt != null || m.chatmode) ? `${MAGENTA}[${encLabel}|${modeLabel}]${RST}` : '';
317
+ let typeTag = '';
318
+ if (m.dir === 'out') {
319
+ const rawSource = m.source;
320
+ // 4 种来源: daemon | ctl | msg | cli
321
+ const source = (rawSource === 'ctl' || rawSource === 'msg' || rawSource === 'cli') ? rawSource : 'daemon';
322
+ const method = m.msgType === 'thought' ? 'thought' : 'send';
323
+ typeTag = `${DIM}[${source}|${method}]${RST}`;
324
+ }
288
325
  const byteLen = Buffer.byteLength(m.content, 'utf-8');
289
326
  const lenTag = `${DIM}${formatNumber(byteLen)}B${RST}`;
290
- const fromDisplay = isGroup && m.groupId && m.dir === 'in' ? m.groupId : m.from;
291
- const toDisplay = isGroup && m.groupId && m.dir === 'out' ? m.groupId : m.to;
292
- const header = `${DIM}${time}${RST} ${dir}${chatTag} ${ORANGE}${fromDisplay}${RST}${DIM}→${RST}${GREEN}${toDisplay}${RST} ${lenTag}`;
293
- const headerLine = padRight(header, msgWidth);
294
- const sbIdx = lines.length - 1;
295
- lines.push(`${headerLine} ${scrollbar[sbIdx] || ' '}`);
296
- if (lines.length - 1 < contentHeight) {
297
- const rawContent = m.content.replace(/\n/g, ' ');
298
- const wrappedLines = wrapText(rawContent, contentLineWidth, maxContentLines);
299
- for (const wl of wrappedLines) {
300
- if (lines.length - 1 >= contentHeight)
301
- break;
302
- const contentLine = padRight(` ${wl}`, msgWidth);
303
- const sbIdx2 = lines.length - 1;
304
- lines.push(`${contentLine} ${scrollbar[sbIdx2] || ' '}`);
305
- }
327
+ const fromDisplay = isGroup && m.groupId && m.dir === 'in' ? m.groupId : m.from.split('.')[0];
328
+ const toDisplay = isGroup && m.groupId && m.dir === 'out' ? m.groupId : m.to.split('.')[0];
329
+ const header = `${DIM}${time}${RST} ${dir}${chatTag}${metaTags}${typeTag} ${ORANGE}${fromDisplay}${RST}${DIM}→${RST}${GREEN}${toDisplay}${RST} ${lenTag}`;
330
+ const out = [padRight(header, msgWidth)];
331
+ const rawContent = m.content.replace(/\n/g, ' ');
332
+ const wrappedLines = wrapText(rawContent, contentLineWidth, maxContentLines);
333
+ for (const wl of wrappedLines) {
334
+ out.push(padRight(` ${wl}`, msgWidth));
335
+ }
336
+ return out;
337
+ }
338
+ // 从 endIdx-1 开始倒序,往回累积,直到行数填满 contentHeight
339
+ const endIdx = Math.max(0, totalMsgs - state.messageScrollOffset);
340
+ const collected = []; // 每条消息的行数组
341
+ let totalLines = 0;
342
+ let firstShownIdx = endIdx; // 首条可见消息的下标
343
+ for (let i = endIdx - 1; i >= 0; i--) {
344
+ const rendered = renderOneMsg(msgs[i]);
345
+ if (totalLines + rendered.length > contentHeight && collected.length > 0)
346
+ break;
347
+ collected.unshift(rendered);
348
+ totalLines += rendered.length;
349
+ firstShownIdx = i;
350
+ if (totalLines >= contentHeight)
351
+ break;
352
+ }
353
+ const visibleMsgCount = endIdx - firstShownIdx;
354
+ const scrollbar = renderScrollbar(totalMsgs, visibleMsgCount, state.messageScrollOffset, contentHeight);
355
+ // 正序输出(旧→新)
356
+ for (const rendered of collected) {
357
+ for (const line of rendered) {
358
+ if (lines.length - 1 >= contentHeight)
359
+ break;
360
+ const sbIdx = lines.length - 1;
361
+ lines.push(`${line} ${scrollbar[sbIdx] || ' '}`);
306
362
  }
363
+ if (lines.length - 1 >= contentHeight)
364
+ break;
307
365
  }
308
366
  while (lines.length < height) {
309
367
  const sbIdx = lines.length - 1;
@@ -314,11 +372,11 @@ function renderMessagesPanel(state, width, height) {
314
372
  // ==================== Main Render ====================
315
373
  function renderFrame(state) {
316
374
  const cols = process.stdout.columns || 120;
317
- const rows = (process.stdout.rows || 40) - 3;
375
+ const rows = (process.stdout.rows || 40);
376
+ const bodyHeight = rows - 4;
318
377
  const leftW = Math.max(20, Math.floor(cols * 0.20));
319
378
  const midW = Math.max(24, Math.floor(cols * 0.22));
320
379
  const rightW = Math.max(40, cols - leftW - midW - 4);
321
- const bodyHeight = rows - 2;
322
380
  const leftLines = renderScopePanel(state, leftW, bodyHeight);
323
381
  const midLines = renderStatsPanel(state, midW, bodyHeight);
324
382
  const msgLines = renderMessagesPanel(state, rightW, bodyHeight);
@@ -334,11 +392,11 @@ function renderFrame(state) {
334
392
  }
335
393
  const bottomBorder = `${DIM}├${'─'.repeat(leftW)}┴${'─'.repeat(midW)}┴${'─'.repeat(rightW + 1)}┤${RST}`;
336
394
  buf += `\x1b[2K${bottomBorder}\n`;
337
- const pkgRoot = getPackageRoot();
338
- const helpLine = `${DIM}│ Tab: panel ↑↓: nav Enter: select Backspace: back ESC: exit ${pkgRoot}${RST}`;
395
+ const helpText = `Tab: panel ↑↓: nav Enter: select Backspace: back ESC: exit`;
396
+ const helpLine = `${DIM}│ ${helpText.slice(0, cols - 4)} ${RST}`;
339
397
  buf += `\x1b[2K${helpLine}\n`;
340
398
  const closeBorder = `${DIM}└${'─'.repeat(cols - 2)}┘${RST}`;
341
- buf += `\x1b[2K${closeBorder}\n`;
399
+ buf += `\x1b[2K${closeBorder}`;
342
400
  return buf;
343
401
  }
344
402
  // ==================== Main ====================
@@ -409,6 +467,7 @@ export async function cmdWatchMsg() {
409
467
  if (!state.selectedLocalAid)
410
468
  return;
411
469
  state.peers = loadPeerInfos(aunDir, state.selectedLocalAid);
470
+ const prevCount = state.messages.length;
412
471
  if (state.selectedPeer) {
413
472
  state.messages = readMessages(aunDir, state.selectedLocalAid, state.selectedPeer);
414
473
  if (state.messages.length > 1000)
@@ -417,6 +476,10 @@ export async function cmdWatchMsg() {
417
476
  else {
418
477
  state.messages = loadAllMessages(aunDir, state.selectedLocalAid);
419
478
  }
479
+ // 有新消息时自动滚到底部
480
+ if (state.messages.length > prevCount) {
481
+ state.messageScrollOffset = 0;
482
+ }
420
483
  // Also refresh scope stats for the selected AID
421
484
  const idx = state.localAids.findIndex(a => a.aid === state.selectedLocalAid);
422
485
  if (idx >= 0) {
@@ -431,6 +494,7 @@ export async function cmdWatchMsg() {
431
494
  watcher.close();
432
495
  watcher = null;
433
496
  }
497
+ clearInterval(pollTimer);
434
498
  if (process.stdin.isTTY)
435
499
  try {
436
500
  process.stdin.setRawMode(false);
@@ -579,6 +643,21 @@ export async function cmdWatchMsg() {
579
643
  loadScope();
580
644
  process.stdout.write('\x1b[?25l\x1b[2J\x1b[H');
581
645
  render();
646
+ // 定时轮询:5 秒检查一次,有变化才刷新
647
+ let lastMsgCount = state.messages.length;
648
+ let lastMsgTs = state.messages.length > 0 ? state.messages[state.messages.length - 1].ts : 0;
649
+ const pollTimer = setInterval(() => {
650
+ if (!state.selectedLocalAid)
651
+ return;
652
+ refreshData();
653
+ const newCount = state.messages.length;
654
+ const newTs = newCount > 0 ? state.messages[newCount - 1].ts : 0;
655
+ if (newCount !== lastMsgCount || newTs !== lastMsgTs) {
656
+ lastMsgCount = newCount;
657
+ lastMsgTs = newTs;
658
+ render();
659
+ }
660
+ }, 5000);
582
661
  if (process.stdin.isTTY) {
583
662
  process.stdin.setRawMode(true);
584
663
  process.stdin.resume();
@@ -75,8 +75,53 @@ export function loadDefaults() {
75
75
  return expandEnvRefs(raw);
76
76
  }
77
77
  export function saveDefaults(value) {
78
+ backupDefaults(resolvePaths().defaultsConfig);
78
79
  atomicWriteJson(resolvePaths().defaultsConfig, value);
79
80
  }
81
+ /**
82
+ * 备份 defaults.json 为 defaults_YYYYMMDDhhmmss.json。文件不存在时为 no-op。
83
+ * 同秒重复调用会被覆盖(同一秒内的内容相同,可接受)。
84
+ */
85
+ function backupDefaults(filePath) {
86
+ if (!fs.existsSync(filePath))
87
+ return;
88
+ const now = new Date();
89
+ const ts = now.getFullYear().toString()
90
+ + String(now.getMonth() + 1).padStart(2, '0')
91
+ + String(now.getDate()).padStart(2, '0')
92
+ + String(now.getHours()).padStart(2, '0')
93
+ + String(now.getMinutes()).padStart(2, '0')
94
+ + String(now.getSeconds()).padStart(2, '0');
95
+ const backupPath = path.join(path.dirname(filePath), `defaults_${ts}.json`);
96
+ try {
97
+ fs.copyFileSync(filePath, backupPath);
98
+ }
99
+ catch (e) {
100
+ logger.warn(`[config] backup failed: ${backupPath}: ${e}`);
101
+ }
102
+ }
103
+ /**
104
+ * 安全写入 defaults.json:备份现有文件 → 深合并 patch → 原子写入。
105
+ *
106
+ * 与 saveDefaults() 不同,本函数保留现有字段,仅覆盖 patch 中显式指定的字段。
107
+ * 适用场景:evolclaw init 仅修改 active_baseagent/baseagents 时,不应丢失 chatmode/projects 等其它字段。
108
+ */
109
+ export function saveDefaultsSafe(patch) {
110
+ const p = resolvePaths().defaultsConfig;
111
+ let existing = null;
112
+ try {
113
+ existing = atomicReadJson(p);
114
+ }
115
+ catch (e) {
116
+ logger.warn(`[config] existing defaults.json unparsable, will be backed up and replaced: ${e}`);
117
+ }
118
+ fs.mkdirSync(path.dirname(p), { recursive: true });
119
+ backupDefaults(p);
120
+ const merged = existing
121
+ ? deepMergeObject(existing, patch)
122
+ : { $schema_version: CONFIG_SCHEMA_VERSION, ...patch };
123
+ atomicWriteJson(p, merged);
124
+ }
80
125
  export function loadProcessConfig() {
81
126
  const raw = atomicReadJson(resolvePaths().processConfig);
82
127
  if (raw === null)
@@ -303,9 +348,30 @@ export function loadAgent(aid) {
303
348
  if (raw.aid !== aid) {
304
349
  throw new Error(`[config] ${p}: aid field "${raw.aid}" != directory name "${aid}"`);
305
350
  }
306
- return expandEnvRefs(raw);
351
+ const cfg = expandEnvRefs(raw);
352
+ if (cfg.projects?.defaultPath) {
353
+ cfg.projects.defaultPath = cfg.projects.defaultPath.replace(/[/\\]+$/, '');
354
+ }
355
+ return cfg;
307
356
  }
308
357
  export function saveAgent(value) {
358
+ if (!isValidAid(value.aid)) {
359
+ throw new Error(`[config] saveAgent: invalid aid "${value.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
360
+ }
361
+ if (value.owners) {
362
+ for (const o of value.owners) {
363
+ if (!isValidAid(o)) {
364
+ throw new Error(`[config] saveAgent: invalid owner AID "${o}" in ${value.aid} (must be a valid multi-level domain like alice.agentid.pub)`);
365
+ }
366
+ }
367
+ }
368
+ if (value.admins) {
369
+ for (const a of value.admins) {
370
+ if (!isValidAid(a)) {
371
+ throw new Error(`[config] saveAgent: invalid admin AID "${a}" in ${value.aid} (must be a valid multi-level domain like alice.agentid.pub)`);
372
+ }
373
+ }
374
+ }
309
375
  atomicWriteJson(agentConfigPath(value.aid), value);
310
376
  }
311
377
  /**
@@ -128,18 +128,18 @@ export class ChannelLoader {
128
128
  }
129
129
  const SEP = '#';
130
130
  export function formatChannelKey(k) {
131
- return `${k.aid}${SEP}${k.type}${SEP}${k.name}`;
131
+ return `${k.type}${SEP}${encodeURIComponent(k.selfPeerId)}${SEP}${k.name}`;
132
132
  }
133
133
  export function parseChannelKey(key) {
134
134
  const parts = key.split(SEP);
135
135
  if (parts.length !== 3) {
136
136
  throw new Error(`Invalid channel key (expected 3 segments separated by '#'): ${key}`);
137
137
  }
138
- const [aid, type, name] = parts;
139
- if (!aid || !type || !name) {
138
+ const [type, encodedSelfPeerId, name] = parts;
139
+ if (!type || !encodedSelfPeerId || !name) {
140
140
  throw new Error(`Invalid channel key (empty segment): ${key}`);
141
141
  }
142
- return { aid, type, name };
142
+ return { type, selfPeerId: decodeURIComponent(encodedSelfPeerId), name };
143
143
  }
144
144
  export function tryParseChannelKey(key) {
145
145
  try {