evolclaw 2.3.0 → 2.5.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/dist/cli.js CHANGED
@@ -6,9 +6,10 @@ import { promisify } from 'util';
6
6
  import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
7
7
  import { loadConfig, validateConfigIntegrity, resolveAnthropicConfig } from './config.js';
8
8
  import { migrateProject } from './utils/migrate-project.js';
9
+ import readline from 'readline';
9
10
  import { cmdInit } from './utils/init.js';
10
11
  import { ipcQuery } from './ipc.js';
11
- import { cmdInitWechat, cmdInitFeishu, cmdInitAun } from './utils/init-channel.js';
12
+ import { cmdInitWechat, cmdInitFeishu, cmdInitAun, cmdInitDingtalk, cmdInitQQBot, cmdInitWecom } from './utils/init-channel.js';
12
13
  import * as platform from './utils/cross-platform.js';
13
14
  import { EventBus } from './core/event-bus.js';
14
15
  // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
@@ -53,8 +54,8 @@ function rotateLogs(logDir) {
53
54
  console.log(` Rotated: ${file} -> ${path.basename(newPath)}`);
54
55
  }
55
56
  }
56
- else if (file.includes('.log.')) {
57
- // 清理 7 天前的旧日志
57
+ else if (file.includes('.log.') || /^aun-\d{8}\.log$/.test(file)) {
58
+ // 清理 7 天前的旧日志(含按日轮转的 aun-YYYYMMDD.log)
58
59
  const stat = fs.statSync(filePath);
59
60
  if (stat.mtimeMs < cutoff) {
60
61
  fs.unlinkSync(filePath);
@@ -121,7 +122,9 @@ function countLines(pkgRoot, logDir) {
121
122
  }
122
123
  }
123
124
  if (shouldAppend) {
124
- const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
125
+ const _d = new Date();
126
+ const _p = (n) => String(n).padStart(2, '0');
127
+ const now = `${_d.getFullYear()}-${_p(_d.getMonth() + 1)}-${_p(_d.getDate())} ${_p(_d.getHours())}:${_p(_d.getMinutes())}:${_p(_d.getSeconds())}`;
125
128
  fs.appendFileSync(statsFile, `${now}\t${core}\t${agents}\t${channels}\t${utils}\t${entry}\t${total}\n`);
126
129
  }
127
130
  showHistory(statsFile);
@@ -391,6 +394,9 @@ function showConfigChannels(config) {
391
394
  { type: 'feishu', isValid: (inst) => !!inst.appId && inst.enabled !== false },
392
395
  { type: 'wechat', isValid: (inst) => !!inst.token && inst.enabled !== false },
393
396
  { type: 'aun', isValid: (inst) => !!inst.aid && inst.enabled !== false && !inst.aid.includes('your-') && !inst.aid.includes('placeholder') },
397
+ { type: 'dingtalk', isValid: (inst) => !!inst.clientId && inst.enabled !== false && !inst.clientId.includes('your-') && !inst.clientId.includes('placeholder') },
398
+ { type: 'qqbot', isValid: (inst) => !!inst.appId && inst.enabled !== false && !inst.appId.includes('your-') && !inst.appId.includes('placeholder') },
399
+ { type: 'wecom', isValid: (inst) => !!inst.botId && inst.enabled !== false && !inst.botId.includes('your-') && !inst.botId.includes('placeholder') },
394
400
  ];
395
401
  for (const { type, isValid } of channelChecks) {
396
402
  const raw = config.channels?.[type];
@@ -568,22 +574,111 @@ async function cmdStatus() {
568
574
  console.log(' (no log file yet)');
569
575
  }
570
576
  }
571
- function cmdLogs() {
577
+ // Log line pattern: [timestamp] [LEVEL] [Module?] message
578
+ const LOG_RE = /^(\[[^\]]+\]) (\[(?:INFO|WARN|ERROR|DEBUG)\]) ((?:\[[^\]]+\] )*)(.*)$/;
579
+ const MAX_MSG = 200; // truncate long messages
580
+ function makeColors(enabled) {
581
+ const e = (code) => enabled ? code : '';
582
+ return {
583
+ reset: e('\x1b[0m'), dim: e('\x1b[2m'), bold: e('\x1b[1m'),
584
+ red: e('\x1b[31m'), yellow: e('\x1b[33m'), cyan: e('\x1b[36m'),
585
+ magenta: e('\x1b[35m'), gray: e('\x1b[90m'),
586
+ };
587
+ }
588
+ function renderLogLine(line, opts) {
589
+ const m = line.match(LOG_RE);
590
+ if (!m)
591
+ return line; // passthrough non-standard lines (stack traces etc.)
592
+ const [, ts, levelTag, modulePart, msg] = m;
593
+ const level = levelTag.slice(1, -1); // strip brackets
594
+ // Level filter
595
+ if (opts.level) {
596
+ const want = opts.level.toUpperCase();
597
+ if (want === 'ERROR' && level !== 'ERROR')
598
+ return null;
599
+ if (want === 'WARN' && level !== 'WARN' && level !== 'ERROR')
600
+ return null;
601
+ }
602
+ // Module filter (case-insensitive substring match)
603
+ if (opts.module) {
604
+ const mod = modulePart.toLowerCase();
605
+ if (!mod.includes(opts.module.toLowerCase()))
606
+ return null;
607
+ }
608
+ // Truncate long messages (always, regardless of color)
609
+ const truncated = msg.length > MAX_MSG ? msg.slice(0, MAX_MSG) + '…' : msg;
610
+ const C = makeColors(opts.color);
611
+ // Color by level
612
+ const levelColor = level === 'ERROR' ? C.red : level === 'WARN' ? C.yellow : level === 'DEBUG' ? C.gray : '';
613
+ // Highlight user messages: [channel] channelId: text
614
+ const isUserMsg = modulePart && /^\S+: .+$/.test(truncated);
615
+ const renderedMsg = isUserMsg
616
+ ? C.cyan + truncated + C.reset
617
+ : levelColor + truncated + C.reset;
618
+ return (C.dim + ts + C.reset + ' ' +
619
+ levelColor + C.bold + levelTag + C.reset + ' ' +
620
+ C.magenta + modulePart.trimEnd() + C.reset +
621
+ (modulePart ? ' ' : '') +
622
+ renderedMsg);
623
+ }
624
+ function cmdLogs(args) {
625
+ const raw = args.includes('--raw');
626
+ const noColor = args.includes('--no-color');
627
+ const levelIdx = args.indexOf('--level');
628
+ const moduleIdx = args.indexOf('--module');
629
+ const level = levelIdx !== -1 ? args[levelIdx + 1] : undefined;
630
+ const module = moduleIdx !== -1 ? args[moduleIdx + 1] : undefined;
572
631
  const p = resolvePaths();
573
632
  const mainLog = path.join(p.logs, 'evolclaw.log');
574
633
  if (!fs.existsSync(mainLog)) {
575
634
  console.log(`❌ Log file not found: ${mainLog}`);
576
635
  process.exit(1);
577
636
  }
637
+ if (raw) {
638
+ // Raw mode: plain tail -f, no rendering at all
639
+ if (platform.isWindows) {
640
+ const tail = platform.tailFile(mainLog);
641
+ platform.onShutdown(() => tail.abort());
642
+ }
643
+ else {
644
+ const child = spawn('tail', ['-f', '-n', '50', mainLog], { stdio: 'inherit' });
645
+ child.on('exit', (code) => process.exit(code || 0));
646
+ }
647
+ return;
648
+ }
649
+ // Rendered mode: always filter+truncate, color depends on TTY
650
+ const useColor = !noColor && !!process.stdout.isTTY;
651
+ const opts = { level, module, color: useColor };
652
+ function processLine(line) {
653
+ const rendered = renderLogLine(line, opts);
654
+ if (rendered !== null)
655
+ process.stdout.write(rendered + '\n');
656
+ }
578
657
  if (platform.isWindows) {
579
- // Windows: use fs.watch for live tail
580
- const tail = platform.tailFile(mainLog);
581
- platform.onShutdown(() => tail.abort());
658
+ // Windows: read existing content + watch
659
+ const existing = fs.readFileSync(mainLog, 'utf-8').split('\n').slice(-50);
660
+ existing.forEach(processLine);
661
+ let size = fs.statSync(mainLog).size;
662
+ const watcher = fs.watch(mainLog, () => {
663
+ const newSize = fs.statSync(mainLog).size;
664
+ if (newSize <= size)
665
+ return;
666
+ const buf = Buffer.alloc(newSize - size);
667
+ const fd = fs.openSync(mainLog, 'r');
668
+ fs.readSync(fd, buf, 0, buf.length, size);
669
+ fs.closeSync(fd);
670
+ size = newSize;
671
+ buf.toString().split('\n').forEach(l => l && processLine(l));
672
+ });
673
+ platform.onShutdown(() => watcher.close());
582
674
  }
583
675
  else {
584
- // Unix: use tail -f
585
- const child = spawn('tail', ['-f', mainLog], { stdio: 'inherit' });
676
+ // Unix: spawn tail -f, pipe through renderer
677
+ const child = spawn('tail', ['-f', '-n', '50', mainLog]);
678
+ const rl = readline.createInterface({ input: child.stdout });
679
+ rl.on('line', processLine);
586
680
  child.on('exit', (code) => process.exit(code || 0));
681
+ platform.onShutdown(() => { child.kill(); });
587
682
  }
588
683
  }
589
684
  /**
@@ -598,7 +693,10 @@ async function cmdRestartMonitor() {
598
693
  const HEAL_TIMEOUT = 30 * 60 * 1000; // 30 分钟,让 claude 自然结束
599
694
  const eventBus = new EventBus();
600
695
  const log = (msg) => {
601
- const line = `[${new Date().toISOString().replace('T', ' ').slice(0, 19)}] ${msg}\n`;
696
+ const _d = new Date();
697
+ const _p = (n) => String(n).padStart(2, '0');
698
+ const ts = `${_d.getFullYear()}-${_p(_d.getMonth() + 1)}-${_p(_d.getDate())} ${_p(_d.getHours())}:${_p(_d.getMinutes())}:${_p(_d.getSeconds())}`;
699
+ const line = `[${ts}] ${msg}\n`;
602
700
  fs.appendFileSync(restartLog, line);
603
701
  };
604
702
  /** 检查服务是否已经在运行(ready signal 存在 + 进程存活) */
@@ -904,7 +1002,7 @@ function archiveSelfHealLog(p, log) {
904
1002
  * Searches across all channel types (feishu, wechat, aun) for a matching instance.
905
1003
  */
906
1004
  function resolveInstanceConfig(config, instanceName) {
907
- for (const type of ['feishu', 'wechat', 'aun']) {
1005
+ for (const type of ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom']) {
908
1006
  const raw = config.channels?.[type];
909
1007
  if (!raw)
910
1008
  continue;
@@ -1156,7 +1254,7 @@ async function cmdTui() {
1156
1254
  const pythonCheck = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1157
1255
  if (!platform.commandExists(pythonCheck)) {
1158
1256
  console.error(`[tui] Python 未找到 (${pythonCheck})`);
1159
- console.error(' → TUI 依赖 Python 和 aun-core: pip3 install aun-core');
1257
+ console.error(' → TUI 依赖 Python 和 aun-core (>=0.2.9): pip3 install -U aun-core');
1160
1258
  process.exit(1);
1161
1259
  }
1162
1260
  const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
@@ -1164,12 +1262,49 @@ async function cmdTui() {
1164
1262
  if (!fs.existsSync(cliScript)) {
1165
1263
  console.error(`[tui] aun_cli.py 不存在: ${cliScript}`);
1166
1264
  console.error(' → TUI 需要 AUN CLI 工具,请确认源码目录包含 aun/aun_cli.py');
1167
- console.error(' → 安装: pip3 install aun-core && 从源码仓库获取 aun_cli.py');
1265
+ console.error(' → 安装: pip3 install -U aun-core && 从源码仓库获取 aun_cli.py');
1168
1266
  process.exit(1);
1169
1267
  }
1170
1268
  const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
1171
1269
  child.on('exit', (code) => process.exit(code ?? 0));
1172
1270
  }
1271
+ // ==================== Ctl ====================
1272
+ async function cmdCtl(args) {
1273
+ if (args.length === 0) {
1274
+ console.error('用法: evolclaw ctl <command> [args...]');
1275
+ console.error('示例: evolclaw ctl model sonnet');
1276
+ console.error(' evolclaw ctl status');
1277
+ console.error(' evolclaw ctl effort high');
1278
+ process.exit(1);
1279
+ }
1280
+ const sessionId = process.env.EVOLCLAW_SESSION_ID;
1281
+ if (!sessionId) {
1282
+ console.error('错误: EVOLCLAW_SESSION_ID 未设置(仅在 evolclaw 托管环境中可用)');
1283
+ process.exit(1);
1284
+ }
1285
+ const cmd = '/' + args.join(' ');
1286
+ const socketPath = resolvePaths().socket;
1287
+ // compact/restart 等长时操作使用更长超时
1288
+ const longRunning = ['/compact', '/restart'];
1289
+ const timeout = longRunning.some(c => cmd.startsWith(c)) ? 60_000 : 10_000;
1290
+ const result = await ipcQuery(socketPath, {
1291
+ type: 'ctl',
1292
+ cmd,
1293
+ sessionId,
1294
+ }, timeout);
1295
+ if (!result) {
1296
+ console.error('错误: 无法连接 evolclaw 服务');
1297
+ process.exit(1);
1298
+ }
1299
+ const ctlResult = result;
1300
+ if (ctlResult.ok) {
1301
+ console.log(ctlResult.result);
1302
+ }
1303
+ else {
1304
+ console.error(ctlResult.error || '执行失败');
1305
+ process.exit(1);
1306
+ }
1307
+ }
1173
1308
  // ==================== Main ====================
1174
1309
  export async function main(args) {
1175
1310
  const cmd = args[0] || 'start';
@@ -1184,6 +1319,15 @@ export async function main(args) {
1184
1319
  else if (args[1] === 'aun') {
1185
1320
  await cmdInitAun();
1186
1321
  }
1322
+ else if (args[1] === 'dingtalk') {
1323
+ await cmdInitDingtalk();
1324
+ }
1325
+ else if (args[1] === 'qqbot') {
1326
+ await cmdInitQQBot();
1327
+ }
1328
+ else if (args[1] === 'wecom') {
1329
+ await cmdInitWecom();
1330
+ }
1187
1331
  else {
1188
1332
  await cmdInit();
1189
1333
  }
@@ -1201,7 +1345,7 @@ export async function main(args) {
1201
1345
  await cmdStatus();
1202
1346
  break;
1203
1347
  case 'logs':
1204
- cmdLogs();
1348
+ cmdLogs(args.slice(1));
1205
1349
  break;
1206
1350
  case 'restart-monitor':
1207
1351
  await cmdRestartMonitor();
@@ -1215,19 +1359,28 @@ export async function main(args) {
1215
1359
  case 'tui':
1216
1360
  await cmdTui();
1217
1361
  break;
1362
+ case 'ctl':
1363
+ await cmdCtl(args.slice(1));
1364
+ break;
1218
1365
  default:
1219
- console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|tui|diagnose|mv}
1366
+ console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|tui|ctl|diagnose|mv}
1220
1367
 
1221
1368
  Commands:
1222
1369
  init 创建配置文件 (${resolvePaths().config})
1223
1370
  init feishu 飞书扫码登录并写入配置
1224
1371
  init wechat 微信扫码登录并写入配置
1372
+ init dingtalk 钉钉扫码登录并写入配置
1373
+ init qqbot QQ 机器人扫码绑定并写入配置
1374
+ init wecom 企业微信 AI Bot 配置(手动输入 Bot ID + Secret)
1225
1375
  init aun AUN (AgentUnin.Network) 配置
1226
1376
  start 启动服务 (默认)
1227
1377
  stop 停止服务
1228
1378
  restart 重启服务
1229
1379
  status 查看状态
1230
- logs 查看日志 (tail -f)
1380
+ logs 查看日志 (tail -f, 着色渲染)
1381
+ --level error|warn 只显示指定级别及以上
1382
+ --module <name> 只显示指定模块(如 feishu、AgentRunner)
1383
+ --raw 原始输出,不着色
1231
1384
  tui 启动 AUN TUI 客户端
1232
1385
  diagnose 诊断启动环境(配置、数据库、进程)
1233
1386
  mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
package/dist/config.js CHANGED
@@ -174,7 +174,7 @@ export function saveConfig(config, configPath = resolvePaths().config) {
174
174
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
175
175
  }
176
176
  // ── Channel instance normalization ──
177
- export const channelTypes = ['feishu', 'wechat', 'aun'];
177
+ export const channelTypes = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot'];
178
178
  /**
179
179
  * Normalize a channel config value (single object, array, or undefined) into an array
180
180
  * where every element has a `name` field.
@@ -186,7 +186,10 @@ export function normalizeChannelInstances(cfg, defaultName) {
186
186
  if (cfg === undefined || cfg === null)
187
187
  return [];
188
188
  if (Array.isArray(cfg)) {
189
- return cfg;
189
+ return cfg.map((item, i) => ({
190
+ ...item,
191
+ name: item.name ?? (cfg.length === 1 ? defaultName : `${defaultName}-${i + 1}`),
192
+ }));
190
193
  }
191
194
  return [{ ...cfg, name: cfg.name ?? defaultName }];
192
195
  }
@@ -258,6 +261,50 @@ export function setOwner(config, instanceName, userId, configPath = resolvePaths
258
261
  return;
259
262
  }
260
263
  }
264
+ export function getChannelShowActivities(config, instanceName) {
265
+ for (const type of channelTypes) {
266
+ const raw = config.channels?.[type];
267
+ if (raw === undefined)
268
+ continue;
269
+ if (Array.isArray(raw)) {
270
+ const inst = raw.find((item) => item.name === instanceName);
271
+ if (inst)
272
+ return inst.showActivities ?? config.showActivities ?? 'all';
273
+ }
274
+ else {
275
+ const effectiveName = raw.name ?? type;
276
+ if (effectiveName === instanceName)
277
+ return raw.showActivities ?? config.showActivities ?? 'all';
278
+ }
279
+ }
280
+ return config.showActivities ?? 'all';
281
+ }
282
+ export function setChannelShowActivities(config, instanceName, mode) {
283
+ if (!config.channels)
284
+ config.channels = {};
285
+ const channels = config.channels;
286
+ for (const type of channelTypes) {
287
+ const raw = channels[type];
288
+ if (raw === undefined)
289
+ continue;
290
+ if (Array.isArray(raw)) {
291
+ const inst = raw.find((item) => item.name === instanceName);
292
+ if (inst) {
293
+ inst.showActivities = mode;
294
+ saveConfig(config);
295
+ return;
296
+ }
297
+ }
298
+ else {
299
+ const effectiveName = raw.name ?? type;
300
+ if (effectiveName === instanceName) {
301
+ raw.showActivities = mode;
302
+ saveConfig(config);
303
+ return;
304
+ }
305
+ }
306
+ }
307
+ }
261
308
  export function isOwner(config, channelOrType, userId) {
262
309
  // 按实例名精确匹配
263
310
  if (getOwner(config, channelOrType) === userId)
@@ -275,6 +322,31 @@ export function isOwner(config, channelOrType, userId) {
275
322
  }
276
323
  return false;
277
324
  }
325
+ export function isAdmin(config, channelOrType, userId) {
326
+ // 按实例名精确匹配
327
+ for (const type of channelTypes) {
328
+ const raw = config.channels?.[type];
329
+ const instances = normalizeChannelInstances(raw, type);
330
+ const found = instances.find((inst) => inst.name === channelOrType);
331
+ if (found) {
332
+ const admins = found.admins || [];
333
+ return admins.includes(userId);
334
+ }
335
+ }
336
+ // 按 channelType 匹配:检查该类型下所有实例
337
+ for (const type of channelTypes) {
338
+ if (type !== channelOrType)
339
+ continue;
340
+ const raw = config.channels?.[type];
341
+ const instances = normalizeChannelInstances(raw, type);
342
+ for (const inst of instances) {
343
+ const admins = inst.admins || [];
344
+ if (admins.includes(userId))
345
+ return true;
346
+ }
347
+ }
348
+ return false;
349
+ }
278
350
  function validateConfig(config) {
279
351
  // anthropic 部分不再强制校验,由 resolveAnthropicConfig() 处理
280
352
  // Feishu 配置可选,但如果配置了就要完整(支持 array / object 两种格式)
@@ -308,6 +380,30 @@ function validateConfig(config) {
308
380
  logger.warn(`⚠ WeChat${label} enabled but token not configured (WeChat channel will be disabled)`);
309
381
  }
310
382
  }
383
+ // DingTalk 配置可选,但如果配置了就需要 clientId + clientSecret
384
+ const dingtalkInstances = normalizeChannelInstances(config.channels?.dingtalk, 'dingtalk');
385
+ for (const inst of dingtalkInstances) {
386
+ if (inst.enabled === false)
387
+ continue;
388
+ const label = dingtalkInstances.length > 1 ? ` [${inst.name}]` : '';
389
+ const hasClientId = !!inst.clientId && !inst.clientId.includes('your-');
390
+ const hasClientSecret = !!inst.clientSecret && !inst.clientSecret.includes('your-');
391
+ if (hasClientId !== hasClientSecret) {
392
+ logger.warn(`⚠ DingTalk${label} clientId/clientSecret incomplete (DingTalk channel will be disabled)`);
393
+ }
394
+ }
395
+ // QQBot 配置可选,但如果配置了就需要 appId + clientSecret
396
+ const qqbotInstances = normalizeChannelInstances(config.channels?.qqbot, 'qqbot');
397
+ for (const inst of qqbotInstances) {
398
+ if (inst.enabled === false)
399
+ continue;
400
+ const label = qqbotInstances.length > 1 ? ` [${inst.name}]` : '';
401
+ const hasAppId = !!inst.appId && !inst.appId.includes('your-');
402
+ const hasSecret = !!inst.clientSecret && !inst.clientSecret.includes('your-');
403
+ if (hasAppId !== hasSecret) {
404
+ logger.warn(`⚠ QQBot${label} appId/clientSecret incomplete (QQBot channel will be disabled)`);
405
+ }
406
+ }
311
407
  }
312
408
  export function ensureDir(dirPath) {
313
409
  if (!fs.existsSync(dirPath)) {