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
@@ -219,7 +219,7 @@ export class FeishuChannel {
219
219
  const imageData = await this.downloadAndSaveImage(imageKey, msg.chat_id, msg.message_id, projectPath);
220
220
  if (imageData) {
221
221
  const allImages = [...quotedImages, imageData];
222
- const prompt = quotedText + '用户发送了一张图片,请分析这张图片的内容。';
222
+ const prompt = quotedText + '用户发送了一张图片,请结合上下文理解用户意图并回应。';
223
223
  await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: allImages, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
224
224
  }
225
225
  else {
@@ -329,12 +329,11 @@ export class FeishuChannel {
329
329
  }
330
330
  logger.info(`[Feishu] CommandCard trigger: command=${value._command}, operator=${operatorId}`);
331
331
  if (this.messageHandler) {
332
- // Feishu chatId 前缀:oc_ = group chat,ou_ = private user open_id
333
- const chatType = typeof chatId === 'string' && chatId.startsWith('oc_') ? 'group' : 'private';
332
+ // 卡片回调不传 chatType——oc_ 前缀不区分群聊/单聊,
333
+ // ensureSession 从已有 session 中继承正确的 chatType
334
334
  await this.messageHandler({
335
335
  channelId: chatId,
336
336
  content: value._command,
337
- chatType,
338
337
  peerId: operatorId,
339
338
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
340
339
  source: 'card-trigger',
@@ -573,7 +572,14 @@ export class FeishuChannel {
573
572
  const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
574
573
  return this.sendMessage(chatId, truncated, options);
575
574
  }
576
- logger.error('[Feishu] Failed to send message:', error);
575
+ const respData = error?.response?.data;
576
+ const code = respData?.code;
577
+ logger.error('[Feishu] Failed to send message:', respData ? JSON.stringify(respData) : error?.message ?? error);
578
+ // post 格式相关错误(400/230001):降级为纯文本重试
579
+ if (!options?.forceText && (error?.response?.status === 400 || code === 230001)) {
580
+ logger.warn('[Feishu] Retrying as plain text (forceText)');
581
+ return this.sendMessage(chatId, content, { ...options, forceText: true });
582
+ }
577
583
  throw error;
578
584
  }
579
585
  }
@@ -1320,6 +1326,7 @@ export class FeishuChannelPlugin {
1320
1326
  });
1321
1327
  const adapter = {
1322
1328
  channelName: inst.name,
1329
+ channelKey: inst.name,
1323
1330
  capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
1324
1331
  send: async (envelope, payload) => {
1325
1332
  const ctx = envelope.replyContext;
@@ -1357,6 +1364,7 @@ export class FeishuChannelPlugin {
1357
1364
  case 'status.interrupted':
1358
1365
  case 'status.error':
1359
1366
  case 'status.timeout':
1367
+ case 'status.progress':
1360
1368
  // Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
1361
1369
  return;
1362
1370
  case 'interaction':
@@ -1416,7 +1424,8 @@ export class FeishuChannelPlugin {
1416
1424
  registerBridge(bridge, channelType) {
1417
1425
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
1418
1426
  await handler({
1419
- channel: adapter.channelName, channelType, channelId: chatId, content, images, chatType,
1427
+ channel: adapter.channelName, channelType, channelId: chatId, content, images,
1428
+ chatType: chatType || 'private',
1420
1429
  peerId: peerId || '', peerName, messageId, mentions, threadId,
1421
1430
  replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
1422
1431
  source,
@@ -334,6 +334,7 @@ export class QQBotChannelPlugin {
334
334
  });
335
335
  const adapter = {
336
336
  channelName: inst.name,
337
+ channelKey: inst.name,
337
338
  capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
338
339
  send: async (envelope, payload) => {
339
340
  const ctx = envelope.replyContext;
@@ -369,6 +370,7 @@ export class QQBotChannelPlugin {
369
370
  case 'status.interrupted':
370
371
  case 'status.error':
371
372
  case 'status.timeout':
373
+ case 'status.progress':
372
374
  case 'custom':
373
375
  return;
374
376
  default:
@@ -730,6 +730,7 @@ export class WechatChannelPlugin {
730
730
  });
731
731
  const adapter = {
732
732
  channelName: inst.name,
733
+ channelKey: inst.name,
733
734
  capabilities: { file: false, image: false, interaction: false, markdown: false, thought: false, status: true },
734
735
  send: async (envelope, payload) => {
735
736
  const channelId = envelope.channelId;
@@ -766,6 +767,7 @@ export class WechatChannelPlugin {
766
767
  case 'status.interrupted':
767
768
  case 'status.error':
768
769
  case 'status.timeout':
770
+ case 'status.progress':
769
771
  case 'custom':
770
772
  return;
771
773
  default:
@@ -490,6 +490,7 @@ export class WecomChannelPlugin {
490
490
  });
491
491
  const adapter = {
492
492
  channelName: inst.name,
493
+ channelKey: inst.name,
493
494
  capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
494
495
  send: async (envelope, payload) => {
495
496
  const ctx = envelope.replyContext;
@@ -525,6 +526,7 @@ export class WecomChannelPlugin {
525
526
  case 'status.interrupted':
526
527
  case 'status.error':
527
528
  case 'status.timeout':
529
+ case 'status.progress':
528
530
  case 'custom':
529
531
  return;
530
532
  default:
package/dist/cli/agent.js CHANGED
@@ -239,22 +239,30 @@ export async function agentShow(aid) {
239
239
  }
240
240
  export async function agentCreateInteractive(opts = {}) {
241
241
  const p = resolvePaths();
242
- const rl = readline.createInterface({
242
+ const ownRl = !opts.rl;
243
+ const rl = opts.rl ?? readline.createInterface({
243
244
  input: opts.stdin || process.stdin,
244
245
  output: opts.stdout || process.stdout,
245
246
  });
246
247
  const ask = (q) => new Promise(r => rl.question(q, r));
247
248
  try {
249
+ const { isValidAid, aidCreate } = await import('../aun/aid/index.js');
248
250
  const aidPrompt = opts.suggestedName
249
251
  ? `AID [${opts.suggestedName}]: `
250
252
  : 'AID (e.g. mybot.agentid.pub): ';
251
- const aidInput = (await ask(aidPrompt)).trim();
252
- const aid = aidInput || opts.suggestedName;
253
- if (!aid)
254
- return { ok: false, error: 'AID is required.' };
255
- const { isValidAid, aidCreate } = await import('../aun/aid/index.js');
256
- if (!isValidAid(aid)) {
257
- return { ok: false, error: `Invalid AID "${aid}": must be a valid multi-level domain (e.g. mybot.agentid.pub)` };
253
+ let aid = '';
254
+ while (!aid) {
255
+ const aidInput = (await ask(aidPrompt)).trim();
256
+ const candidate = aidInput || opts.suggestedName;
257
+ if (!candidate) {
258
+ console.log(' ⚠ AID is required.');
259
+ continue;
260
+ }
261
+ if (!isValidAid(candidate)) {
262
+ console.log(` ⚠ Invalid AID "${candidate}": must be a valid multi-level domain (e.g. mybot.agentid.pub)`);
263
+ continue;
264
+ }
265
+ aid = candidate;
258
266
  }
259
267
  const agentDirPath = path.join(p.agentsDir, aid);
260
268
  const configExists = fs.existsSync(path.join(agentDirPath, 'config.json'));
@@ -311,26 +319,48 @@ export async function agentCreateInteractive(opts = {}) {
311
319
  return { ok: false, error: `No baseagent CLI detected on PATH. Install one of: ${BASEAGENT_CANDIDATES.join('/')}` };
312
320
  }
313
321
  const defaultBa = pickDefaultBaseagent(available);
314
- let baseagent = null;
315
- while (baseagent === null) {
316
- const input = (await ask(`Baseagent (${available.join('/')}) [${defaultBa}]: `)).trim() || defaultBa;
317
- if (!BASEAGENT_CANDIDATES.includes(input)) {
318
- console.log(` Invalid choice. Options: ${BASEAGENT_CANDIDATES.join('/')}`);
319
- continue;
322
+ let baseagent;
323
+ if (available.length === 1) {
324
+ console.log(` Baseagent: ${defaultBa}`);
325
+ baseagent = defaultBa;
326
+ }
327
+ else {
328
+ let chosen = null;
329
+ while (chosen === null) {
330
+ const input = (await ask(`Baseagent (${available.join('/')}) [${defaultBa}]: `)).trim() || defaultBa;
331
+ if (!BASEAGENT_CANDIDATES.includes(input)) {
332
+ console.log(` Invalid choice. Options: ${BASEAGENT_CANDIDATES.join('/')}`);
333
+ continue;
334
+ }
335
+ if (!available.includes(input)) {
336
+ console.log(` ${input} not detected on PATH. Available: ${available.join('/')}`);
337
+ continue;
338
+ }
339
+ chosen = input;
340
+ }
341
+ baseagent = chosen;
342
+ }
343
+ // Owner
344
+ let owner;
345
+ while (true) {
346
+ const ownerInput = (await ask('Owner AID (leave empty for auto-bind on first message): ')).trim();
347
+ if (!ownerInput) {
348
+ owner = undefined;
349
+ break;
320
350
  }
321
- if (!available.includes(input)) {
322
- console.log(` ${input} not detected on PATH. Available: ${available.join('/')}`);
351
+ if (!isValidAid(ownerInput)) {
352
+ console.log(` ⚠ Invalid Owner AID "${ownerInput}": must be a valid multi-level domain (e.g. alice.agentid.pub)`);
323
353
  continue;
324
354
  }
325
- baseagent = input;
355
+ owner = ownerInput;
356
+ break;
326
357
  }
327
- // Owner
328
- const owner = (await ask('Owner AID (leave empty for auto-bind on first message): ')).trim() || undefined;
329
358
  // Name + description for agent.md
330
359
  const defaultName = aid.split('.')[0];
331
360
  const agentName = (await ask(`Display name [${defaultName}]: `)).trim() || defaultName;
332
361
  const agentDescription = (await ask('Description (optional): ')).trim() || '';
333
- rl.close();
362
+ if (ownRl)
363
+ rl.close();
334
364
  const agentConfig = {
335
365
  $schema_version: CONFIG_SCHEMA_VERSION,
336
366
  aid,
@@ -359,31 +389,64 @@ export async function agentCreateInteractive(opts = {}) {
359
389
  const agentMdPath = path.join(aunPath, 'AIDs', aid, 'agent.md');
360
390
  fs.mkdirSync(path.dirname(agentMdPath), { recursive: true });
361
391
  fs.writeFileSync(agentMdPath, content, 'utf-8');
362
- try {
363
- await agentmdPut(content, { aid, aunPath });
364
- agentmdUploaded = true;
392
+ // Upload with retry (3 attempts, 2s delay between retries)
393
+ const MAX_ATTEMPTS = 3;
394
+ const RETRY_DELAY_MS = 2000;
395
+ let lastError;
396
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
397
+ try {
398
+ if (attempt > 1) {
399
+ process.stdout.write(` ↻ agent.md 上传重试 (${attempt}/${MAX_ATTEMPTS})...\n`);
400
+ await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
401
+ }
402
+ await agentmdPut(content, { aid, aunPath });
403
+ agentmdUploaded = true;
404
+ break;
405
+ }
406
+ catch (e) {
407
+ lastError = e;
408
+ }
365
409
  }
366
- catch (e) {
367
- console.warn(` ⚠ agent.md upload failed: ${e?.message || e}`);
410
+ if (!agentmdUploaded) {
411
+ console.warn(` ⚠ agent.md upload failed: ${lastError?.message || lastError}`);
368
412
  console.warn(` → Retry later with: evolclaw aid agentmd put ${aid}`);
369
413
  }
414
+ // Yield to allow the SDK WebSocket to fully close before IPC
415
+ await new Promise(r => setTimeout(r, 0));
370
416
  }
371
417
  catch (e) {
372
418
  console.warn(` ⚠ agent.md generation failed: ${e?.message || e}`);
373
419
  }
420
+ // Attempt hot-load via IPC (if daemon is running)
421
+ let hotLoaded = false;
422
+ let hotLoadError;
423
+ try {
424
+ const ipcResult = await ipcQuery(p.socket, { type: 'evolagent.load', aid });
425
+ if (ipcResult?.ok) {
426
+ hotLoaded = true;
427
+ }
428
+ else if (ipcResult) {
429
+ hotLoadError = ipcResult.error;
430
+ }
431
+ }
432
+ catch { /* daemon not running */ }
374
433
  return {
375
434
  ok: true,
376
435
  aid,
377
436
  configPath: toPosix(path.join(agentDirPath, 'config.json')),
378
437
  aidCreated,
379
438
  agentmdUploaded,
439
+ hotLoaded,
440
+ hotLoadError,
380
441
  };
381
442
  }
382
443
  finally {
383
- try {
384
- rl.close();
444
+ if (ownRl) {
445
+ try {
446
+ rl.close();
447
+ }
448
+ catch { /* ignore */ }
385
449
  }
386
- catch { /* ignore */ }
387
450
  }
388
451
  }
389
452
  export async function agentCreateNonInteractive(opts) {
@@ -482,24 +545,51 @@ export async function agentCreateNonInteractive(opts) {
482
545
  const agentMdPath = path.join(aunPath, 'AIDs', opts.aid, 'agent.md');
483
546
  fs.mkdirSync(path.dirname(agentMdPath), { recursive: true });
484
547
  fs.writeFileSync(agentMdPath, content, 'utf-8');
485
- try {
486
- await agentmdPut(content, { aid: opts.aid, aunPath });
487
- agentmdUploaded = true;
548
+ const MAX_ATTEMPTS = 3;
549
+ const RETRY_DELAY_MS = 2000;
550
+ let lastError;
551
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
552
+ try {
553
+ if (attempt > 1)
554
+ await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
555
+ await agentmdPut(content, { aid: opts.aid, aunPath });
556
+ agentmdUploaded = true;
557
+ break;
558
+ }
559
+ catch (e) {
560
+ lastError = e;
561
+ }
488
562
  }
489
- catch (e) {
490
- console.warn(`⚠ agent.md upload failed: ${e?.message || e}`);
563
+ if (!agentmdUploaded) {
564
+ console.warn(`⚠ agent.md upload failed: ${lastError?.message || lastError}`);
491
565
  console.warn(` Retry later with: evolclaw aid agentmd put ${opts.aid}`);
492
566
  }
567
+ await new Promise(r => setTimeout(r, 0));
493
568
  }
494
569
  catch (e) {
495
570
  console.warn(`⚠ agent.md generation failed: ${e?.message || e}`);
496
571
  }
572
+ // Attempt hot-load via IPC (if daemon is running)
573
+ let hotLoaded = false;
574
+ let hotLoadError;
575
+ try {
576
+ const ipcResult = await ipcQuery(p.socket, { type: 'evolagent.load', aid: opts.aid });
577
+ if (ipcResult?.ok) {
578
+ hotLoaded = true;
579
+ }
580
+ else if (ipcResult) {
581
+ hotLoadError = ipcResult.error;
582
+ }
583
+ }
584
+ catch { /* daemon not running */ }
497
585
  return {
498
586
  ok: true,
499
587
  aid: opts.aid,
500
588
  configPath: toPosix(path.join(agentDirPath, 'config.json')),
501
589
  aidCreated,
502
590
  agentmdUploaded,
591
+ hotLoaded,
592
+ hotLoadError,
503
593
  };
504
594
  }
505
595
  // ==================== agentSyncAids ====================
@@ -665,7 +755,12 @@ export async function agentSet(aid, key, rawValue) {
665
755
  }
666
756
  const value = parseJsonValue(rawValue);
667
757
  setNestedValue(config, key, value);
668
- saveAgent(config);
758
+ try {
759
+ saveAgent(config);
760
+ }
761
+ catch (e) {
762
+ return { ok: false, error: e?.message || String(e) };
763
+ }
669
764
  // Try hot-reload
670
765
  let reloaded = false;
671
766
  try {
@@ -733,7 +828,7 @@ export async function agentChannelUpsert(opts) {
733
828
  return {
734
829
  ok: true,
735
830
  aid: opts.aid,
736
- channelKey: `${opts.aid}#${opts.channel.type}#${opts.channel.name}`,
831
+ channelKey: `${opts.channel.type}#${encodeURIComponent(opts.aid)}#${opts.channel.name}`,
737
832
  reloaded,
738
833
  };
739
834
  }