evolclaw 2.4.0 → 2.5.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.
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { migrateProject } from './utils/migrate-project.js';
9
9
  import readline from 'readline';
10
10
  import { cmdInit } from './utils/init.js';
11
11
  import { ipcQuery } from './ipc.js';
12
- import { cmdInitWechat, cmdInitFeishu, cmdInitAun } from './utils/init-channel.js';
12
+ import { cmdInitWechat, cmdInitFeishu, cmdInitAun, cmdInitDingtalk, cmdInitQQBot, cmdInitWecom } from './utils/init-channel.js';
13
13
  import * as platform from './utils/cross-platform.js';
14
14
  import { EventBus } from './core/event-bus.js';
15
15
  // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
@@ -54,8 +54,8 @@ function rotateLogs(logDir) {
54
54
  console.log(` Rotated: ${file} -> ${path.basename(newPath)}`);
55
55
  }
56
56
  }
57
- else if (file.includes('.log.')) {
58
- // 清理 7 天前的旧日志
57
+ else if (file.includes('.log.') || /^aun-\d{8}\.log$/.test(file)) {
58
+ // 清理 7 天前的旧日志(含按日轮转的 aun-YYYYMMDD.log)
59
59
  const stat = fs.statSync(filePath);
60
60
  if (stat.mtimeMs < cutoff) {
61
61
  fs.unlinkSync(filePath);
@@ -122,7 +122,9 @@ function countLines(pkgRoot, logDir) {
122
122
  }
123
123
  }
124
124
  if (shouldAppend) {
125
- 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())}`;
126
128
  fs.appendFileSync(statsFile, `${now}\t${core}\t${agents}\t${channels}\t${utils}\t${entry}\t${total}\n`);
127
129
  }
128
130
  showHistory(statsFile);
@@ -392,6 +394,9 @@ function showConfigChannels(config) {
392
394
  { type: 'feishu', isValid: (inst) => !!inst.appId && inst.enabled !== false },
393
395
  { type: 'wechat', isValid: (inst) => !!inst.token && inst.enabled !== false },
394
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') },
395
400
  ];
396
401
  for (const { type, isValid } of channelChecks) {
397
402
  const raw = config.channels?.[type];
@@ -688,7 +693,10 @@ async function cmdRestartMonitor() {
688
693
  const HEAL_TIMEOUT = 30 * 60 * 1000; // 30 分钟,让 claude 自然结束
689
694
  const eventBus = new EventBus();
690
695
  const log = (msg) => {
691
- 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`;
692
700
  fs.appendFileSync(restartLog, line);
693
701
  };
694
702
  /** 检查服务是否已经在运行(ready signal 存在 + 进程存活) */
@@ -994,7 +1002,7 @@ function archiveSelfHealLog(p, log) {
994
1002
  * Searches across all channel types (feishu, wechat, aun) for a matching instance.
995
1003
  */
996
1004
  function resolveInstanceConfig(config, instanceName) {
997
- for (const type of ['feishu', 'wechat', 'aun']) {
1005
+ for (const type of ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom']) {
998
1006
  const raw = config.channels?.[type];
999
1007
  if (!raw)
1000
1008
  continue;
@@ -1246,7 +1254,7 @@ async function cmdTui() {
1246
1254
  const pythonCheck = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1247
1255
  if (!platform.commandExists(pythonCheck)) {
1248
1256
  console.error(`[tui] Python 未找到 (${pythonCheck})`);
1249
- 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');
1250
1258
  process.exit(1);
1251
1259
  }
1252
1260
  const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
@@ -1254,13 +1262,54 @@ async function cmdTui() {
1254
1262
  if (!fs.existsSync(cliScript)) {
1255
1263
  console.error(`[tui] aun_cli.py 不存在: ${cliScript}`);
1256
1264
  console.error(' → TUI 需要 AUN CLI 工具,请确认源码目录包含 aun/aun_cli.py');
1257
- console.error(' → 安装: pip3 install aun-core && 从源码仓库获取 aun_cli.py');
1265
+ console.error(' → 安装: pip3 install -U aun-core && 从源码仓库获取 aun_cli.py');
1258
1266
  process.exit(1);
1259
1267
  }
1260
1268
  const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
1261
1269
  child.on('exit', (code) => process.exit(code ?? 0));
1262
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
+ }
1263
1308
  // ==================== Main ====================
1309
+ function getArgValue(args, flag) {
1310
+ const idx = args.indexOf(flag);
1311
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
1312
+ }
1264
1313
  export async function main(args) {
1265
1314
  const cmd = args[0] || 'start';
1266
1315
  switch (cmd) {
@@ -1274,8 +1323,29 @@ export async function main(args) {
1274
1323
  else if (args[1] === 'aun') {
1275
1324
  await cmdInitAun();
1276
1325
  }
1326
+ else if (args[1] === 'dingtalk') {
1327
+ await cmdInitDingtalk();
1328
+ }
1329
+ else if (args[1] === 'qqbot') {
1330
+ await cmdInitQQBot();
1331
+ }
1332
+ else if (args[1] === 'wecom') {
1333
+ await cmdInitWecom();
1334
+ }
1277
1335
  else {
1278
- await cmdInit();
1336
+ const nonInteractive = args.includes('--non-interactive');
1337
+ if (nonInteractive) {
1338
+ await cmdInit({
1339
+ nonInteractive: true,
1340
+ defaultPath: getArgValue(args, '--default-path') || process.cwd(),
1341
+ channel: getArgValue(args, '--channel') || 'aun',
1342
+ aunAid: getArgValue(args, '--aun-aid'),
1343
+ aunOwner: getArgValue(args, '--aun-owner'),
1344
+ });
1345
+ }
1346
+ else {
1347
+ await cmdInit();
1348
+ }
1279
1349
  }
1280
1350
  break;
1281
1351
  case 'start':
@@ -1305,13 +1375,19 @@ export async function main(args) {
1305
1375
  case 'tui':
1306
1376
  await cmdTui();
1307
1377
  break;
1378
+ case 'ctl':
1379
+ await cmdCtl(args.slice(1));
1380
+ break;
1308
1381
  default:
1309
- console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|tui|diagnose|mv}
1382
+ console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|tui|ctl|diagnose|mv}
1310
1383
 
1311
1384
  Commands:
1312
1385
  init 创建配置文件 (${resolvePaths().config})
1313
1386
  init feishu 飞书扫码登录并写入配置
1314
1387
  init wechat 微信扫码登录并写入配置
1388
+ init dingtalk 钉钉扫码登录并写入配置
1389
+ init qqbot QQ 机器人扫码绑定并写入配置
1390
+ init wecom 企业微信 AI Bot 配置(手动输入 Bot ID + Secret)
1315
1391
  init aun AUN (AgentUnin.Network) 配置
1316
1392
  start 启动服务 (默认)
1317
1393
  stop 停止服务
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)) {