evolclaw 2.6.0 → 2.6.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.
@@ -1,4 +1,4 @@
1
- import { AUNClient, GatewayDiscovery } from '@agentunion/aun-node';
1
+ import { AUNClient, GatewayDiscovery } from '@agentunion/fastaun';
2
2
  import crypto from 'crypto';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
@@ -317,7 +317,8 @@ export class AUNChannel {
317
317
  if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
318
318
  logger.warn(`[AUN] Owner ${owner} type is "${ownerInfo.type}" (not human). Consider using a human AID as owner.`);
319
319
  }
320
- // Name: owner agent.md name (first 12 chars) fallback to owner AID first label (first 12 chars)
320
+ // Name: prefer existing agent.md name if user has customized it,
321
+ // otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
321
322
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
322
323
  let ownerDisplayName;
323
324
  if (ownerInfo.name) {
@@ -326,7 +327,18 @@ export class AUNChannel {
326
327
  else {
327
328
  ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
328
329
  }
329
- const agentDisplayName = `${ownerDisplayName}的Evol助手`;
330
+ // Check if init wrote a meaningful name (vs just the aid first label default)
331
+ const currentNameMatch = frontmatter.match(/^name:\s*"?([^"\n]+)/m);
332
+ const currentName = currentNameMatch?.[1]?.trim();
333
+ const aidLabel = aidName.split('.')[0];
334
+ let agentDisplayName;
335
+ if (currentName && currentName !== aidLabel) {
336
+ // User or previous init set a custom name — keep it
337
+ agentDisplayName = currentName;
338
+ }
339
+ else {
340
+ agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
341
+ }
330
342
  // Generate new agent.md with proper fields
331
343
  const newAgentMd = `---
332
344
  aid: "${aid}"
@@ -531,11 +543,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
531
543
  this.acknowledgeImmediately(messageId, seq);
532
544
  return;
533
545
  }
546
+ // dispatch_mode from server tells agent how to work in this group
547
+ const dispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
534
548
  const mentionedSelf = this._aid
535
549
  ? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
536
550
  : false;
537
551
  const mentionedAll = this.hasExplicitMention(text, 'all') || payloadMentions.includes('all');
538
- if (!mentionedSelf && !mentionedAll) {
552
+ // In mention mode, only respond when explicitly mentioned; in broadcast mode, respond to all
553
+ if (dispatchMode === 'mention' && !mentionedSelf && !mentionedAll) {
539
554
  this.acknowledgeImmediately(messageId, seq);
540
555
  return;
541
556
  }
@@ -550,7 +565,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
550
565
  this.acknowledgeImmediately(messageId, seq);
551
566
  return;
552
567
  }
553
- const mentions = mentionedAll ? ['all'] : (this._aid ? [this._aid] : []);
568
+ const mentions = mentionedAll
569
+ ? ['all']
570
+ : mentionedSelf && this._aid ? [this._aid] : [];
554
571
  // Process attachments
555
572
  let finalText = strippedText;
556
573
  if (hasAttachments && this.client) {
@@ -732,6 +749,44 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
732
749
  logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
733
750
  }
734
751
  }
752
+ /**
753
+ * 发送 thought 内容(Proactive 模式可观测)
754
+ * 群聊:调用 group.thought.put
755
+ * 单聊:调用 message.thought.put
756
+ *
757
+ * selector 使用 context: { type: 'task', id: taskId }
758
+ * 存储键:group_id/peer_aid + sender_aid + context.type + context.id
759
+ */
760
+ async sendThought(channelId, taskId, payload) {
761
+ if (!this.connected || !this.client)
762
+ return;
763
+ if (!taskId)
764
+ return;
765
+ // Multi-instance routing
766
+ const colonIdx = channelId.indexOf(':');
767
+ const targetId = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
768
+ const params = {
769
+ context: { type: 'task', id: taskId },
770
+ payload,
771
+ encrypt: true,
772
+ };
773
+ try {
774
+ if (this.isGroupId(channelId)) {
775
+ params.group_id = targetId;
776
+ this.trace('OUT', 'group.thought.put', params);
777
+ await this.client.call('group.thought.put', params);
778
+ }
779
+ else {
780
+ params.to = targetId;
781
+ this.trace('OUT', 'message.thought.put', params);
782
+ await this.client.call('message.thought.put', params);
783
+ }
784
+ }
785
+ catch (e) {
786
+ this.trace('OUT', 'thought.put.error', { channelId, error: String(e) });
787
+ logger.debug(`[AUN] thought.put failed to ${channelId}: ${e}`);
788
+ }
789
+ }
735
790
  async sendFile(channelId, filePath, context) {
736
791
  if (!this.connected || !this.client) {
737
792
  logger.warn('[AUN] Cannot sendFile: not connected');
@@ -834,7 +889,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
834
889
  // to avoid duplicate "已送达" at the sender CLI
835
890
  this.messageSeqMap.delete(messageId);
836
891
  }
837
- sendProcessingStatus(channelId, status, sessionId, context) {
892
+ sendProcessingStatus(channelId, status, sessionId, taskId, context) {
838
893
  if (status === 'start')
839
894
  this.sentCount.delete(channelId); // 新任务开始,重置计数
840
895
  if (!this.client || !this.connected)
@@ -849,7 +904,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
849
904
  const payload = {
850
905
  type: 'event',
851
906
  event: eventMap[status] ?? `task.${status}`,
852
- data: { session_id: sessionId },
907
+ data: { task_id: taskId, session_id: sessionId },
853
908
  severity: status === 'error' || status === 'timeout' ? 'error' : 'info',
854
909
  };
855
910
  if (context?.threadId)
@@ -1048,10 +1103,11 @@ export class AUNChannelPlugin {
1048
1103
  sendText: (id, text, context) => channel.sendMessage(id, text, context),
1049
1104
  sendFile: (id, filePath, context) => channel.sendFile(id, filePath, context),
1050
1105
  acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
1051
- sendProcessingStatus: (id, status, sessionId, context) => channel.sendProcessingStatus(id, status, sessionId, context),
1106
+ sendProcessingStatus: (id, status, sessionId, taskId, context) => channel.sendProcessingStatus(id, status, sessionId, taskId, context),
1052
1107
  sendCustomPayload: (id, payload) => channel.sendCustomPayload(id, payload),
1053
1108
  uploadAgentMd: (content) => channel.uploadAgentMd(content),
1054
1109
  downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
1110
+ putThought: (id, taskId, payload) => channel.sendThought(id, taskId, payload),
1055
1111
  _selfAid: () => channel.getStatus().aid,
1056
1112
  };
1057
1113
  const policy = {
@@ -499,11 +499,12 @@ export class FeishuChannel {
499
499
  return;
500
500
  try {
501
501
  // 检测是否为图片,是则走 sendImage(内联预览)而非文件卡片
502
- const header = Buffer.alloc(12);
502
+ // 读取足够字节供 file-type 解析(ZIP-based 格式如 PPTX 需要更多字节)
503
+ const header = Buffer.alloc(4100);
503
504
  const fd = fs.openSync(filePath, 'r');
504
- fs.readSync(fd, header, 0, 12, 0);
505
+ const bytesRead = fs.readSync(fd, header, 0, 4100, 0);
505
506
  fs.closeSync(fd);
506
- const imgType = await imageType(header);
507
+ const imgType = await imageType(header.subarray(0, bytesRead)).catch(() => undefined);
507
508
  if (imgType) {
508
509
  logger.info(`[Feishu] Detected image (${imgType.mime}), sending as inline image:`, filePath);
509
510
  const buf = fs.readFileSync(filePath);
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { ipcQuery } from './ipc.js';
12
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
+ import { tryUpgrade } from './utils/upgrade.js';
15
16
  // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
16
17
  process.removeAllListeners('warning');
17
18
  process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
@@ -372,6 +373,25 @@ async function cmdStop() {
372
373
  async function cmdRestart() {
373
374
  console.log('🔄 Restarting EvolClaw...');
374
375
  const p = resolvePaths();
376
+ // 版本检查与自动升级
377
+ console.log('📦 Checking for updates...');
378
+ const upgrade = await tryUpgrade();
379
+ switch (upgrade.status) {
380
+ case 'upgraded':
381
+ console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
382
+ break;
383
+ case 'no-update':
384
+ console.log(`✓ Already up to date (${upgrade.from})`);
385
+ break;
386
+ case 'skipped':
387
+ console.log(upgrade.error
388
+ ? '⏭ Skipped upgrade (network unavailable)'
389
+ : '⏭ Skipped upgrade check (dev mode)');
390
+ break;
391
+ case 'failed':
392
+ console.log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}), continuing with current version`);
393
+ break;
394
+ }
375
395
  await stopAndWait(p.pid);
376
396
  setTimeout(() => cmdStart(), 1000);
377
397
  }
@@ -737,6 +757,27 @@ async function cmdRestartMonitor() {
737
757
  });
738
758
  await sleep(3000);
739
759
  }
760
+ // 版本检查与自动升级
761
+ log('Checking for updates...');
762
+ const upgrade = await tryUpgrade();
763
+ switch (upgrade.status) {
764
+ case 'upgraded':
765
+ log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
766
+ await notifyChannel(p, pendingInfo, `📦 已升级 ${upgrade.from} → ${upgrade.to}`, log);
767
+ break;
768
+ case 'no-update':
769
+ log(`Already up to date (${upgrade.from})`);
770
+ break;
771
+ case 'skipped':
772
+ log(upgrade.error
773
+ ? 'Skipped upgrade (network unavailable)'
774
+ : 'Skipped upgrade check (dev mode)');
775
+ break;
776
+ case 'failed':
777
+ log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}): ${upgrade.error}`);
778
+ await notifyChannel(p, pendingInfo, `⚠️ 升级失败,使用当前版本继续`, log);
779
+ break;
780
+ }
740
781
  // 启动并检测 ready signal
741
782
  let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
742
783
  if (started) {
@@ -1244,14 +1285,43 @@ async function cmdDiagnose() {
1244
1285
  // ==================== Ctl ====================
1245
1286
  async function cmdCtl(args) {
1246
1287
  if (args.length === 0) {
1247
- console.error('用法: evolclaw ctl <command> [args...]');
1248
- console.error('示例: evolclaw ctl model sonnet');
1249
- console.error(' evolclaw ctl status');
1250
- console.error(' evolclaw ctl effort high');
1251
- console.error(' evolclaw ctl send "<消息内容>" # proactive 模式主动发消息');
1252
- console.error(' evolclaw ctl chatmode proactive # 切换会话模式');
1288
+ console.error(`用法: evolclaw ctl <command> [args...]
1289
+
1290
+ 查询:
1291
+ status 查看会话状态
1292
+ check 检查渠道健康状态
1293
+ help 显示帮助
1294
+
1295
+ 配置:
1296
+ model [model-id] 查看/切换模型(如 opus, sonnet, haiku)
1297
+ effort [low|medium|high] 查看/切换推理强度
1298
+ compact 压缩当前会话上下文
1299
+ chatmode [mode] 查看/切换会话模式
1300
+ activity [all|dm|owner|none] 查看/控制中间输出显示模式
1301
+ perm [mode] 查看/切换权限模式
1302
+
1303
+ 项目:
1304
+ bind <path> 注册项目目录(不切换当前会话)
1305
+
1306
+ 消息:
1307
+ send <消息内容> 主动发送文本消息(proactive 模式)
1308
+ file [channel] <path> 发送项目内文件
1309
+
1310
+ 运维:
1311
+ agentmd [put|set <内容>] 查看/管理 agent.md(仅 AUN 通道)
1312
+ restart [channel] 重启服务或重连指定渠道
1313
+
1314
+ 示例:
1315
+ evolclaw ctl model sonnet
1316
+ evolclaw ctl effort high
1317
+ evolclaw ctl compact
1318
+ evolclaw ctl chatmode proactive`);
1253
1319
  process.exit(1);
1254
1320
  }
1321
+ // help 不需要连接服务,直接复用无参数时的帮助输出
1322
+ if (args[0] === 'help') {
1323
+ return cmdCtl([]);
1324
+ }
1255
1325
  const sessionId = process.env.EVOLCLAW_SESSION_ID;
1256
1326
  if (!sessionId) {
1257
1327
  console.error('错误: EVOLCLAW_SESSION_ID 未设置(仅在 evolclaw 托管环境中可用)');
@@ -1294,7 +1364,32 @@ export async function main(args) {
1294
1364
  }
1295
1365
  switch (cmd) {
1296
1366
  case 'init':
1297
- if (args[1] === 'wechat') {
1367
+ if (args[1] === 'help') {
1368
+ console.log(`用法: evolclaw init [渠道] [选项]
1369
+
1370
+ 交互式初始化:
1371
+ evolclaw init 创建基础配置文件(交互式)
1372
+ evolclaw init feishu 飞书扫码登录并写入配置
1373
+ evolclaw init wechat 微信扫码登录并写入配置
1374
+ evolclaw init dingtalk 钉钉扫码登录并写入配置
1375
+ evolclaw init qqbot QQ 机器人扫码绑定并写入配置
1376
+ evolclaw init wecom 企业微信 AI Bot 配置(手动输入)
1377
+ evolclaw init aun AUN 交互式配置(AID 创建 + Owner 绑定)
1378
+
1379
+ 非交互式初始化:
1380
+ evolclaw init --non-interactive [选项]
1381
+
1382
+ 选项:
1383
+ --default-path <path> 项目目录(默认: 当前目录)
1384
+ --channel <name> 渠道类型(默认: aun)
1385
+ --aun-aid <aid> AUN Agent ID(必填,如 mybot.agentid.pub)
1386
+ --aun-owner <aid> Owner AID(可选,如 alice.agentid.pub)
1387
+
1388
+ 示例:
1389
+ evolclaw init --non-interactive --aun-aid mybot.agentid.pub --aun-owner alice.agentid.pub
1390
+ evolclaw init --non-interactive --default-path /home/user/project --aun-aid bot.agentid.pub`);
1391
+ }
1392
+ else if (args[1] === 'wechat') {
1298
1393
  await cmdInitWechat();
1299
1394
  }
1300
1395
  else if (args[1] === 'feishu') {
@@ -1380,6 +1475,8 @@ Commands:
1380
1475
  --level error|warn 只显示指定级别及以上
1381
1476
  --module <name> 只显示指定模块(如 feishu、AgentRunner)
1382
1477
  --raw 原始输出,不着色
1478
+ ctl 运行时自管理(模型切换、推理强度、压缩上下文等)
1479
+ evolclaw ctl help 查看完整命令列表
1383
1480
  diagnose 诊断启动环境(配置、数据库、进程)
1384
1481
  mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
1385
1482
 
package/dist/config.js CHANGED
@@ -177,7 +177,7 @@ export function saveConfig(config, configPath = resolvePaths().config) {
177
177
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
178
178
  }
179
179
  // ── Channel instance normalization ──
180
- export const channelTypes = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot'];
180
+ export const channelTypes = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom'];
181
181
  /**
182
182
  * Normalize a channel config value (single object, array, or undefined) into an array
183
183
  * where every element has a `name` field.
@@ -103,7 +103,7 @@ function formatIdleTime(ms) {
103
103
  return '刚刚';
104
104
  }
105
105
  // 支持的命令列表
106
- const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/agentmd', '/chatmode'];
106
+ const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/agentmd', '/chatmode'];
107
107
  // 命令别名映射
108
108
  const aliases = {
109
109
  '/p': '/project',
@@ -127,6 +127,7 @@ export class CommandHandler {
127
127
  permissionGateway;
128
128
  interactionRouter;
129
129
  statsCollector;
130
+ hotLoadChannel;
130
131
  agentMap;
131
132
  defaultAgentId;
132
133
  /** 按 agentId 获取 agent,回退到默认 */
@@ -266,6 +267,9 @@ export class CommandHandler {
266
267
  setMessageQueue(messageQueue) {
267
268
  this.messageQueue = messageQueue;
268
269
  }
270
+ setHotLoadChannel(fn) {
271
+ this.hotLoadChannel = fn;
272
+ }
269
273
  setPermissionGateway(gateway) {
270
274
  this.permissionGateway = gateway;
271
275
  }
@@ -407,6 +411,10 @@ export class CommandHandler {
407
411
  ] : []),
408
412
  ...(isOwner ? [
409
413
  { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
414
+ { cmd: '/aid', label: 'AID 管理', desc: '创建新 AID 并上线新 Agent 实例', next: { type: 'select', items: [
415
+ { value: 'list', label: '列表', desc: '列出所有 AUN 实例及连接状态' },
416
+ { value: 'new', label: '创建', desc: '创建新 AID 并热加载上线', next: { type: 'text' } },
417
+ ] } },
410
418
  { cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
411
419
  { value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
412
420
  { value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
@@ -634,6 +642,7 @@ export class CommandHandler {
634
642
  ...(isOwner ? [
635
643
  ' /restart - 重启服务',
636
644
  ' /file [channel] <path> - 发送项目内文件',
645
+ ' /aid [list|new <aid>] - AID 管理',
637
646
  ' /agentmd [put|set <内容>] - 管理 agent.md',
638
647
  ] : []),
639
648
  '',
@@ -1139,6 +1148,94 @@ export class CommandHandler {
1139
1148
  }
1140
1149
  return `✓ 推理强度: ${newEffort}`;
1141
1150
  }
1151
+ // /aid 命令:AID 管理(list / new)
1152
+ if (normalizedContent === '/aid' || normalizedContent === '/aid list' || normalizedContent.startsWith('/aid ')) {
1153
+ if (!isOwner)
1154
+ return '❌ 无权限:此命令仅限 owner 使用';
1155
+ const adapter = this.adapters.get(channel);
1156
+ const channelType = this.channelTypeMap.get(channel);
1157
+ if (channelType !== 'aun')
1158
+ return '❌ 此命令仅在 AUN 通道中可用';
1159
+ const arg = normalizedContent.slice(4).trim();
1160
+ // /aid 或 /aid list — 列出所有 AUN 实例
1161
+ if (!arg || arg === 'list') {
1162
+ const { normalizeChannelInstances } = await import('../config.js');
1163
+ const instances = normalizeChannelInstances(this.config.channels?.aun, 'aun');
1164
+ if (instances.length === 0)
1165
+ return '暂无 AUN 实例';
1166
+ const lines = ['AUN 实例:'];
1167
+ for (const inst of instances) {
1168
+ if (inst.enabled === false || !inst.aid)
1169
+ continue;
1170
+ const channelObj = this.channelObjects.get(inst.name);
1171
+ const status = channelObj?.getStatus?.();
1172
+ const connected = status?.connected ?? false;
1173
+ const icon = connected ? '✓' : '✗';
1174
+ const state = connected ? '已连接' : '未连接';
1175
+ lines.push(` ${icon} ${inst.name} ${inst.aid} ${state}`);
1176
+ }
1177
+ return lines.join('\n');
1178
+ }
1179
+ // /aid new <aid> — 创建新 AID 并热加载
1180
+ if (arg.startsWith('new ')) {
1181
+ const rawName = arg.slice(4).trim();
1182
+ if (!rawName)
1183
+ return '用法: /aid new <aid>\n例: /aid new reviewer';
1184
+ if (!this.hotLoadChannel)
1185
+ return '❌ 热加载未就绪';
1186
+ // Derive full AID: if no dots, append domain from current AID
1187
+ const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
1188
+ let fullAid = rawName;
1189
+ if (!rawName.includes('.')) {
1190
+ const domain = selfAid.split('.').slice(1).join('.');
1191
+ if (!domain)
1192
+ return '❌ 无法推导 AID 域(当前实例未连接)';
1193
+ fullAid = `${rawName}.${domain}`;
1194
+ }
1195
+ // Validate AID format
1196
+ const { isValidAid } = await import('../utils/init-channel.js');
1197
+ if (!isValidAid(fullAid))
1198
+ return `❌ 无效 AID 格式: ${fullAid}`;
1199
+ // Check instance name conflict
1200
+ const instName = rawName.includes('.') ? rawName.split('.')[0] : rawName;
1201
+ const { normalizeChannelInstances } = await import('../config.js');
1202
+ const existing = normalizeChannelInstances(this.config.channels?.aun, 'aun');
1203
+ if (existing.some(e => e.name === instName)) {
1204
+ return `❌ 实例名 "${instName}" 已存在`;
1205
+ }
1206
+ if (existing.some(e => e.aid === fullAid)) {
1207
+ return `❌ AID ${fullAid} 已在配置中`;
1208
+ }
1209
+ // Create AID (reuse init-channel.ts silent logic)
1210
+ try {
1211
+ const { createAidSilent, appendAunInstance } = await import('../utils/init-channel.js');
1212
+ const createResult = await createAidSilent({ aid: fullAid, owner: selfAid });
1213
+ // Resolve owner from current AUN instance config
1214
+ const owner = this.config.channels?.aun
1215
+ ? (Array.isArray(this.config.channels.aun)
1216
+ ? this.config.channels.aun.find((a) => a.aid === selfAid)?.owner
1217
+ : this.config.channels.aun.owner)
1218
+ : undefined;
1219
+ // Hot-load: build and register new channel instance BEFORE writing config
1220
+ const { AUNChannelPlugin } = await import('../channels/aun.js');
1221
+ const plugin = new AUNChannelPlugin();
1222
+ const tempConfig = JSON.parse(JSON.stringify(this.config));
1223
+ tempConfig.channels.aun = [{ name: instName, enabled: true, aid: fullAid, owner }];
1224
+ const newInstances = await plugin.createChannels(tempConfig);
1225
+ if (newInstances.length === 0)
1226
+ return '❌ 通道实例创建失败';
1227
+ await this.hotLoadChannel(newInstances[0]);
1228
+ // Write config only after successful hot-load
1229
+ appendAunInstance(this.config, { name: instName, aid: fullAid, owner });
1230
+ const verb = createResult.alreadyExisted ? '已存在,现已上线' : '已创建并上线';
1231
+ return `✓ ${fullAid} ${verb}\n 实例名: ${instName}\n 可在 AUN 中搜索该 AID 开始对话`;
1232
+ }
1233
+ catch (e) {
1234
+ return `❌ 创建失败: ${String(e.message || e).slice(0, 200)}`;
1235
+ }
1236
+ }
1237
+ return '用法: /aid [list|new <aid>]';
1238
+ }
1142
1239
  // /activity 命令:控制中间输出显示模式
1143
1240
  if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
1144
1241
  if (!isOwner)
@@ -2601,7 +2698,7 @@ export class CommandHandler {
2601
2698
  static CTL_COMMANDS = [
2602
2699
  '/help', '/status', '/check',
2603
2700
  '/model', '/effort', '/perm',
2604
- '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd',
2701
+ '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
2605
2702
  ];
2606
2703
  /**
2607
2704
  * 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
@@ -1,7 +1,9 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
+ import crypto from 'crypto';
3
4
  import { hasCompact } from '../../agents/claude-runner.js';
4
5
  import { StreamFlusher } from './stream-flusher.js';
6
+ import { ThoughtEmitter } from './thought-emitter.js';
5
7
  import { StreamIdleMonitor } from './stream-idle-monitor.js';
6
8
  import { logger } from '../../utils/logger.js';
7
9
  import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
@@ -27,7 +29,6 @@ export class MessageProcessor {
27
29
  interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
28
30
  interactionRouter;
29
31
  messageQueue;
30
- skillsHintDesc = undefined; // undefined=未加载, null=无模板, string=缓存描述
31
32
  skillsEnsured = false; // 全局 SKILLS.md 是否已确保
32
33
  /** 按 agentId 获取 agent,回退到默认 */
33
34
  getAgent(agentId) {
@@ -279,6 +280,8 @@ export class MessageProcessor {
279
280
  const { adapter, options } = channelInfo;
280
281
  const agent = this.getAgent(session.agentId);
281
282
  const streamKey = session.id;
283
+ // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
284
+ const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
282
285
  try {
283
286
  const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
284
287
  // 记录收到消息
@@ -302,7 +305,7 @@ export class MessageProcessor {
302
305
  logger.info(`[MessageProcessor] session=${session.id} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
303
306
  // 记录开始处理
304
307
  this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
305
- adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, this.getReplyContext(message));
308
+ adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, this.getReplyContext(message));
306
309
  logger.message({
307
310
  msgId: messageId,
308
311
  sessionId: session.id,
@@ -338,6 +341,12 @@ export class MessageProcessor {
338
341
  }, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
339
342
  // 保存当前 flusher,用于 compact 事件
340
343
  this.currentFlusher = flusher;
344
+ // Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
345
+ // selector: context = { type: 'task', id: taskId }
346
+ let thoughtEmitter = null;
347
+ if (isProactive && adapter.putThought) {
348
+ thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId);
349
+ }
341
350
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
342
351
  // 捕获当前消息的上下文(闭包),避免后续消息处理时串台
343
352
  const capturedChannelId = message.channelId;
@@ -439,14 +448,16 @@ export class MessageProcessor {
439
448
  contextParts.push(`[群聊回复规则] 回复时必须在开头添加 @${message.peerId} 来通知对方`);
440
449
  }
441
450
  // 5. Agent ctl 自管理指令提示 + SKILLS.md 生成
442
- if (!this.skillsEnsured) {
443
- this.ensureSkillsFile();
444
- this.skillsEnsured = true;
445
- }
446
- const skillsHint = this.getSkillsHint();
447
- if (skillsHint) {
448
- contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
449
- }
451
+ // 暂时注释:排查 proactive 模式下 agent 未调用 Bash 工具的问题,
452
+ // 怀疑此段与 [Proactive 模式] 语义重合稀释了后者的权重
453
+ // if (!this.skillsEnsured) {
454
+ // this.ensureSkillsFile();
455
+ // this.skillsEnsured = true;
456
+ // }
457
+ // const skillsHint = this.getSkillsHint();
458
+ // if (skillsHint) {
459
+ // contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
460
+ // }
450
461
  // 6. Proactive 模式提示词:agent 的输出不会自动发送,必须主动调用 ctl send/file
451
462
  if (isProactive) {
452
463
  contextParts.push('[Proactive 模式] 本次对话中你的流式输出不会自动发送给用户,必须通过以下命令主动发送:\n' +
@@ -463,7 +474,7 @@ export class MessageProcessor {
463
474
  const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
464
475
  agent.registerStream(streamKey, stream);
465
476
  streamRegistered = true;
466
- streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress);
477
+ streamResult = await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
467
478
  break; // 成功,跳出重试循环
468
479
  }
469
480
  catch (retryError) {
@@ -493,7 +504,7 @@ export class MessageProcessor {
493
504
  flusher.addActivity('\u2705 压缩完成,正在重试...');
494
505
  const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, options?.systemPromptAppend, this.sessionManager);
495
506
  agent.registerStream(streamKey, retryStream);
496
- streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress);
507
+ streamResult = await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter);
497
508
  }
498
509
  else {
499
510
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -607,7 +618,7 @@ export class MessageProcessor {
607
618
  const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
608
619
  const rawSubtype = streamResult.subtype || 'agent_error';
609
620
  const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
610
- adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, this.getReplyContext(message));
621
+ adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId, this.getReplyContext(message));
611
622
  this.eventBus.publish({
612
623
  type: 'message:error',
613
624
  sessionId: session.id,
@@ -635,7 +646,7 @@ export class MessageProcessor {
635
646
  }
636
647
  else {
637
648
  // 真正的成功
638
- adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, this.getReplyContext(message));
649
+ adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId, this.getReplyContext(message));
639
650
  await this.sessionManager.recordSuccess(session.id);
640
651
  this.eventBus.publish({
641
652
  type: 'message:completed',
@@ -688,7 +699,7 @@ export class MessageProcessor {
688
699
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
689
700
  if (!isUserInterrupt) {
690
701
  try {
691
- adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, this.getReplyContext(message));
702
+ adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, this.getReplyContext(message));
692
703
  }
693
704
  catch { }
694
705
  }
@@ -764,7 +775,7 @@ export class MessageProcessor {
764
775
  * 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
765
776
  * SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
766
777
  */
767
- async processEventStream(stream, session, flusher, resetTimer, shouldSuppress) {
778
+ async processEventStream(stream, session, flusher, resetTimer, shouldSuppress, thoughtEmitter) {
768
779
  let hasReceivedText = false;
769
780
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
770
781
  let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
@@ -777,6 +788,10 @@ export class MessageProcessor {
777
788
  resetTimer(event.type, toolName);
778
789
  // 记录所有事件类型
779
790
  logger.info(`[MessageProcessor] Event: type=${event.type}`);
791
+ // Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
792
+ if (thoughtEmitter) {
793
+ thoughtEmitter.emit(event).catch(() => { });
794
+ }
780
795
  // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
781
796
  if (event.type === 'session_id') {
782
797
  logger.info(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
@@ -1077,42 +1092,28 @@ export class MessageProcessor {
1077
1092
  return 0;
1078
1093
  }
1079
1094
  /**
1080
- * 从模板 frontmatter 缓存提示(懒加载,整个进程只读一次模板文件)
1095
+ * data/SKILLS.md 读取 frontmatter 并生成提示。
1096
+ * 不缓存:每次读取保证用户编辑立即生效。
1097
+ * 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
1081
1098
  */
1082
1099
  getSkillsHint() {
1083
- if (this.skillsHintDesc === undefined) {
1084
- this.skillsHintDesc = this.loadSkillsHint();
1085
- }
1086
- return this.skillsHintDesc;
1087
- }
1088
- /**
1089
- * 从包模板源读取 frontmatter 并生成提示(仅执行一次)
1090
- */
1091
- loadSkillsHint() {
1092
1100
  try {
1093
- const candidates = [
1094
- path.join(getPackageRoot(), 'src', 'templates', 'skills.md'),
1095
- path.join(getPackageRoot(), 'dist', 'templates', 'skills.md'),
1101
+ const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
1102
+ if (!fs.existsSync(skillsPath))
1103
+ return null;
1104
+ const content = fs.readFileSync(skillsPath, 'utf-8');
1105
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1106
+ if (!frontmatterMatch)
1107
+ return null;
1108
+ const fm = frontmatterMatch[1];
1109
+ const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
1110
+ const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
1111
+ const parts = [
1112
+ `可通过 Bash 指令管理运行时,${desc}。`,
1113
+ trigger ? `触发时机:${trigger}。` : '',
1114
+ `完整文档见 ${skillsPath}`,
1096
1115
  ];
1097
- for (const templatePath of candidates) {
1098
- if (!fs.existsSync(templatePath))
1099
- continue;
1100
- const content = fs.readFileSync(templatePath, 'utf-8');
1101
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1102
- if (!frontmatterMatch)
1103
- continue;
1104
- const fm = frontmatterMatch[1];
1105
- const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
1106
- const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
1107
- const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
1108
- const parts = [
1109
- `可通过 Bash 执行 \`evolclaw ctl <cmd>\` 管理运行时:${desc}`,
1110
- trigger ? `触发时机:${trigger}` : '',
1111
- `完整文档见 ${skillsPath}`,
1112
- ];
1113
- return parts.filter(Boolean).join('\n');
1114
- }
1115
- return null;
1116
+ return parts.filter(Boolean).join('');
1116
1117
  }
1117
1118
  catch {
1118
1119
  return null;
@@ -169,7 +169,7 @@ export class MessageQueue {
169
169
  * 合并多条同 peerId 消息:
170
170
  * - content: \n 连接
171
171
  * - images / mentions: 扁平合并
172
- * - messageId: 置空(合并后不代表某一条具体消息)
172
+ * - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
173
173
  * - replyContext / peerName / 其余字段: 取最后一条
174
174
  */
175
175
  mergeItems(items) {
@@ -185,12 +185,20 @@ export class MessageQueue {
185
185
  allMentions.push(...m.mentions);
186
186
  }
187
187
  const last = items[items.length - 1];
188
+ // 保留最新一条的 messageId(若最后一条无 ID 则回退到前面已有的 ID)
189
+ let latestMessageId;
190
+ for (let i = items.length - 1; i >= 0; i--) {
191
+ if (items[i].message.messageId) {
192
+ latestMessageId = items[i].message.messageId;
193
+ break;
194
+ }
195
+ }
188
196
  const merged = {
189
197
  ...last.message,
190
198
  content: contents.join('\n'),
191
199
  images: allImages.length > 0 ? allImages : undefined,
192
200
  mentions: allMentions.length > 0 ? allMentions : undefined,
193
- messageId: undefined,
201
+ messageId: latestMessageId,
194
202
  };
195
203
  return {
196
204
  message: merged,
@@ -95,13 +95,21 @@ export class StreamDebouncer {
95
95
  allMentions.push(...e.mentions);
96
96
  }
97
97
  const last = entries[entries.length - 1];
98
+ // 合并后保留最新一条的 messageId(用于 thought 锚定与中断追踪)
99
+ let latestMessageId;
100
+ for (let i = entries.length - 1; i >= 0; i--) {
101
+ if (entries[i].messageId) {
102
+ latestMessageId = entries[i].messageId;
103
+ break;
104
+ }
105
+ }
98
106
  const merged = {
99
107
  ...last.rest,
100
108
  content: contents.join('\n'),
101
109
  images: allImages.length > 0 ? allImages : undefined,
102
110
  mentions: allMentions.length > 0 ? allMentions : undefined,
103
111
  replyContext: last.replyContext,
104
- messageId: entries.length > 1 ? undefined : last.messageId,
112
+ messageId: latestMessageId,
105
113
  };
106
114
  const resolves = entries.map(e => e.resolve);
107
115
  const rejects = entries.map(e => e.reject);
@@ -0,0 +1,146 @@
1
+ import { logger } from '../../utils/logger.js';
2
+ /**
3
+ * ThoughtEmitter — 将 Proactive 模式下的流式 AgentEvent 实时发送为 thought
4
+ *
5
+ * 设计特点:
6
+ * - 不做聚合/batching,逐事件调用 adapter.putThought()
7
+ * - 不感知 group vs P2P,通道差异由 adapter 内部处理
8
+ * - taskId 映射为 context: { type: 'task', id: taskId }(协议 selector)
9
+ * - fire-and-forget:调用方不 await emit(),错误被内部捕获
10
+ */
11
+ export class ThoughtEmitter {
12
+ adapter;
13
+ channelId;
14
+ taskId;
15
+ hasEmittedText = false;
16
+ constructor(adapter, channelId, taskId) {
17
+ if (!taskId) {
18
+ throw new Error('[ThoughtEmitter] taskId is required at construction');
19
+ }
20
+ this.adapter = adapter;
21
+ this.channelId = channelId;
22
+ this.taskId = taskId;
23
+ }
24
+ async emit(event) {
25
+ // 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
26
+ if (event.type === 'complete' &&
27
+ !event.isError &&
28
+ event.result &&
29
+ this.hasEmittedText) {
30
+ return;
31
+ }
32
+ const payload = this.mapEventToPayload(event);
33
+ if (!payload)
34
+ return;
35
+ if (!this.adapter.putThought)
36
+ return;
37
+ if (payload.stage === 'thinking') {
38
+ this.hasEmittedText = true;
39
+ }
40
+ try {
41
+ await this.adapter.putThought(this.channelId, this.taskId, payload);
42
+ }
43
+ catch (err) {
44
+ logger.debug(`[ThoughtEmitter] putThought failed: ${err.message}`);
45
+ }
46
+ }
47
+ mapEventToPayload(event) {
48
+ switch (event.type) {
49
+ case 'text':
50
+ if (!event.text)
51
+ return null;
52
+ return { type: 'thought', text: event.text, stage: 'thinking' };
53
+ case 'tool_use': {
54
+ const desc = this.summarizeInput(event.input);
55
+ return {
56
+ type: 'thought',
57
+ text: desc ? `🔧 ${event.name}: ${desc}` : `🔧 ${event.name}`,
58
+ stage: 'tool',
59
+ metadata: { tool: event.name, input: desc },
60
+ };
61
+ }
62
+ case 'tool_result':
63
+ if (event.isError) {
64
+ return {
65
+ type: 'thought',
66
+ text: `⚠️ ${event.name}: ${event.error || '执行失败'}`,
67
+ stage: 'tool',
68
+ metadata: { tool: event.name, ok: false },
69
+ };
70
+ }
71
+ {
72
+ const resultText = this.truncate(this.stringifyResult(event.result), 200);
73
+ return {
74
+ type: 'thought',
75
+ text: resultText ? `✅ ${event.name}: ${resultText}` : `✅ ${event.name}`,
76
+ stage: 'tool',
77
+ metadata: { tool: event.name, ok: true },
78
+ };
79
+ }
80
+ case 'compact':
81
+ return {
82
+ type: 'thought',
83
+ text: `💡 会话压缩完成 (压缩前 tokens: ${event.preTokens})`,
84
+ stage: 'system',
85
+ };
86
+ case 'task_progress': {
87
+ const stats = this.formatTaskStats(event);
88
+ const text = event.summary
89
+ ? `⏳ 子任务: ${event.summary}${stats ? ` (${stats})` : ''}`
90
+ : `⏳ 子任务进行中${stats ? `: ${stats}` : ''}`;
91
+ return { type: 'thought', text, stage: 'planning' };
92
+ }
93
+ case 'error':
94
+ return { type: 'thought', text: `❌ ${event.error}`, stage: 'error' };
95
+ case 'complete':
96
+ if (event.isError) {
97
+ const errText = event.errors?.join('; ') || event.result || '任务失败';
98
+ return { type: 'thought', text: `❌ ${errText}`, stage: 'error' };
99
+ }
100
+ if (event.result) {
101
+ return { type: 'thought', text: event.result, stage: 'summary' };
102
+ }
103
+ return null;
104
+ case 'session_id':
105
+ case 'state_changed':
106
+ case 'status':
107
+ return null;
108
+ default:
109
+ return null;
110
+ }
111
+ }
112
+ summarizeInput(input) {
113
+ if (!input || typeof input !== 'object')
114
+ return '';
115
+ return (input.description ||
116
+ input.file_path ||
117
+ input.pattern ||
118
+ (typeof input.command === 'string' ? input.command.substring(0, 80) : '') ||
119
+ (typeof input.prompt === 'string' ? input.prompt.substring(0, 80) : '') ||
120
+ (typeof input.query === 'string' ? input.query.substring(0, 80) : '') ||
121
+ '');
122
+ }
123
+ stringifyResult(result) {
124
+ if (result === null || result === undefined)
125
+ return '';
126
+ if (typeof result === 'string')
127
+ return result;
128
+ try {
129
+ return JSON.stringify(result);
130
+ }
131
+ catch {
132
+ return String(result);
133
+ }
134
+ }
135
+ truncate(text, maxLen) {
136
+ return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
137
+ }
138
+ formatTaskStats(event) {
139
+ const parts = [];
140
+ if (event.toolUses)
141
+ parts.push(`${event.toolUses} tools`);
142
+ if (event.durationMs)
143
+ parts.push(`${Math.round(event.durationMs / 1000)}s`);
144
+ return parts.join(', ');
145
+ }
146
+ }
package/dist/index.js CHANGED
@@ -204,9 +204,11 @@ async function main() {
204
204
  showIdleMonitor: () => true,
205
205
  accumulateErrors: () => true,
206
206
  };
207
- // 注册渠道插件的 adapter policy
208
- for (const inst of channelInstances) {
209
- // 设置项目路径提供器(如果需要)
207
+ // ── MessageBridge:Channel Core 消息桥梁 ──
208
+ const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
209
+ // ── Channel instance registration (shared by startup and hot-load) ──
210
+ function registerChannelInstance(inst) {
211
+ // 1. 项目路径提供器
210
212
  if (inst.onProjectPathRequest && inst.channel.onProjectPathRequest) {
211
213
  inst.channel.onProjectPathRequest(async (channelId) => {
212
214
  const session = await sessionManager.getOrCreateSession(inst.adapter.channelName, channelId, config.projects?.defaultPath || process.cwd(), undefined, undefined, undefined, undefined);
@@ -215,7 +217,7 @@ async function main() {
215
217
  : path.resolve(process.cwd(), session.projectPath);
216
218
  });
217
219
  }
218
- // 注册 adapter、policy 和 options(注入 channelType)
220
+ // 2. 注册 adapter、policy 和 options(注入 channelType)
219
221
  const opts = inst.channelType
220
222
  ? { ...inst.options, channelType: inst.channelType }
221
223
  : inst.options;
@@ -225,18 +227,13 @@ async function main() {
225
227
  if (inst.policy) {
226
228
  cmdHandler.registerPolicy(inst.adapter.channelName, inst.policy);
227
229
  }
228
- // 注册交互回调:渠道收到用户操作后路由到 InteractionRouter
230
+ // 3. 交互回调
229
231
  if (inst.adapter.onInteraction) {
230
232
  inst.adapter.onInteraction((response) => {
231
233
  interactionRouter.handle(response);
232
234
  });
233
235
  }
234
- }
235
- // ── MessageBridge:Channel ↔ Core 消息桥梁 ──
236
- const msgBridge = new MessageBridge(config, sessionManager, processor, messageQueue, cmdHandler, eventBus);
237
- // ── 渠道消息注册 ──
238
- // 连接插件系统的渠道
239
- for (const inst of channelInstances) {
236
+ // 4. MessageBridge 注册(按 channelType 分发)
240
237
  const channelType = inst.channelType || inst.adapter.channelName;
241
238
  if (channelType === 'feishu') {
242
239
  msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType }) => {
@@ -251,7 +248,6 @@ async function main() {
251
248
  }), inst.adapter, channelType);
252
249
  }
253
250
  if (channelType === 'wechat') {
254
- // 注入 EventBus(用于 channel:health 事件)
255
251
  if (inst.channel.setEventBus) {
256
252
  inst.channel.setEventBus(eventBus);
257
253
  }
@@ -281,6 +277,19 @@ async function main() {
281
277
  replyContext: opts.replyContext,
282
278
  });
283
279
  }), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, replyContext), inst.adapter, channelType);
280
+ // AUN 重连失败通知
281
+ if (inst.channel.setOnChannelDown) {
282
+ inst.channel.setOnChannelDown(() => {
283
+ eventBus.publish({
284
+ type: 'channel:health',
285
+ channel: channelType,
286
+ channelName: inst.adapter.channelName,
287
+ status: 'auth_error',
288
+ message: `⚠️ AUN 渠道 ${inst.adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
289
+ timestamp: Date.now(),
290
+ });
291
+ });
292
+ }
284
293
  }
285
294
  if (channelType === 'dingtalk') {
286
295
  msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
@@ -310,11 +319,40 @@ async function main() {
310
319
  });
311
320
  }), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
312
321
  }
313
- // 通用:撤回消息 中断执行中任务(所有支持 onRecall 的渠道)
322
+ if (channelType === 'wecom') {
323
+ msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
324
+ handler({
325
+ channel: channelType,
326
+ channelId: event.channelId,
327
+ content: event.content,
328
+ chatType: event.chatType || 'private',
329
+ peerId: event.peerId || '',
330
+ peerName: event.peerName,
331
+ messageId: event.messageId,
332
+ });
333
+ }), (channelId, text) => inst.channel.sendMessage(channelId, text), inst.adapter, channelType);
334
+ }
335
+ // 5. 撤回消息 → 中断执行中任务
314
336
  inst.channel.onRecall?.((messageId) => {
315
337
  msgBridge.cancel(messageId);
316
338
  });
317
339
  }
340
+ // ── 注册所有渠道实例 ──
341
+ for (const inst of channelInstances) {
342
+ registerChannelInstance(inst);
343
+ }
344
+ // ── 设置热加载回调 ──
345
+ cmdHandler.setHotLoadChannel(async (inst) => {
346
+ registerChannelInstance(inst);
347
+ channelInstances.push(inst);
348
+ await inst.connect();
349
+ eventBus.publish({
350
+ type: 'channel:connected',
351
+ channel: (inst.channelType || inst.adapter.channelName).toLowerCase(),
352
+ channelName: inst.adapter.channelName,
353
+ timestamp: Date.now(),
354
+ });
355
+ });
318
356
  // ── 连接所有渠道 ──
319
357
  const connected = await channelLoader.connectAll(channelInstances);
320
358
  // 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
@@ -326,7 +364,6 @@ async function main() {
326
364
  }
327
365
  }
328
366
  for (const name of connected) {
329
- // 查找对应实例以获取 channelType
330
367
  const inst = channelInstances.find(i => i.adapter.channelName === name);
331
368
  const type = inst?.channelType || name;
332
369
  eventBus.publish({
@@ -336,22 +373,6 @@ async function main() {
336
373
  timestamp: Date.now()
337
374
  });
338
375
  }
339
- // AUN 重连失败通知:通过 channel:health 事件
340
- for (const inst of channelInstances) {
341
- const channelType = inst.channelType || inst.adapter.channelName;
342
- if (channelType === 'aun' && inst.channel.setOnChannelDown) {
343
- inst.channel.setOnChannelDown(() => {
344
- eventBus.publish({
345
- type: 'channel:health',
346
- channel: channelType,
347
- channelName: inst.adapter.channelName,
348
- status: 'auth_error',
349
- message: `⚠️ AUN 渠道 ${inst.adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
350
- timestamp: Date.now(),
351
- });
352
- });
353
- }
354
- }
355
376
  // 统一 channel:health 跨通道通知(仅 auth_error)
356
377
  // 按 (channelType, ownerId) 去重,避免同类型多实例重复通知
357
378
  eventBus.subscribe('channel:health', (event) => {
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: evolclaw-ctl
3
- version: 1.0.0
4
- description: EvolClaw 运行时自管理指令,仅在 evolclaw 托管环境中可用
5
- trigger: 用户询问或需要切换模型、调整推理强度、查看运行状态、压缩上下文、检查通道健康、管理权限模式、发送文件、重启服务、重连渠道时
3
+ version: 1.1.0
4
+ description: 仅在 evolclaw 运行时可用
5
+ trigger: 用户询问或需要切换模型、调整推理强度、查看运行状态、压缩上下文、检查通道健康、管理权限模式、重启服务、重连渠道等
6
6
  ---
7
7
 
8
8
  # EvolClaw Ctl
@@ -35,6 +35,8 @@ trigger: 用户询问或需要切换模型、调整推理强度、查看运行
35
35
  - `evolclaw ctl agentmd` — 查看当前 agent.md
36
36
  - `evolclaw ctl agentmd put` — 发布本地 agent.md
37
37
  - `evolclaw ctl agentmd set <内容>` — 直接设置 agent.md 内容
38
+ - `evolclaw ctl aid` — 列出所有 AUN 实例及连接状态
39
+ - `evolclaw ctl aid new <aid>` — 创建新 AID 并热加载(仅 AUN 通道)
38
40
 
39
41
  ## 使用示例
40
42
 
@@ -580,9 +580,9 @@ export async function cmdInitWechat() {
580
580
  process.exit(1);
581
581
  }
582
582
  // ==================== AUN ====================
583
- // 最低 @agentunion/aun-node 版本要求
584
- const MIN_AUN_CORE_SDK = [0, 2, 12];
585
- const AUN_CORE_SDK_PKG = '@agentunion/aun-node';
583
+ // 最低 @agentunion/fastaun 版本要求
584
+ const MIN_AUN_CORE_SDK = [0, 2, 14];
585
+ const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
586
586
  function compareVersion(a, min) {
587
587
  const parts = a.split('.').map(n => parseInt(n, 10));
588
588
  if (parts.length < 3 || parts.some(isNaN))
@@ -711,10 +711,83 @@ export async function checkAunEnvironment(rl) {
711
711
  console.log('');
712
712
  return true;
713
713
  }
714
- function isValidAid(name) {
714
+ export function isValidAid(name) {
715
715
  const labels = name.split('.');
716
716
  return labels.length >= 3 && labels.every(l => /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(l));
717
717
  }
718
+ /**
719
+ * Non-interactive AID creation + agent.md publish.
720
+ * Reuses the same logic as `evolclaw init --non-interactive --channel aun`.
721
+ *
722
+ * Returns the created AID string, or throws on failure.
723
+ */
724
+ export async function createAidSilent(opts) {
725
+ const aunPath = path.join(os.homedir(), '.aun');
726
+ const aidDir = path.join(aunPath, 'AIDs', opts.aid);
727
+ // Skip creation if AID already exists locally
728
+ if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
729
+ return { aid: opts.aid, alreadyExisted: true };
730
+ }
731
+ const { AUNClient } = await import('@agentunion/fastaun');
732
+ let client = new AUNClient({ aun_path: aunPath });
733
+ const result = await client.auth.createAid({ aid: opts.aid });
734
+ // Download CA root cert (if not already present)
735
+ const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '');
736
+ // Rebuild client with CA cert + AID identity for uploadAgentMd
737
+ const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
738
+ if (caDownloaded && fs.existsSync(caCertPath)) {
739
+ try {
740
+ await client.close();
741
+ }
742
+ catch { /* ignore */ }
743
+ client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid: opts.aid });
744
+ }
745
+ // Write initial agent.md (initialized: false, name = aid first label)
746
+ const agentName = opts.aid.split('.')[0];
747
+ const agentMdContent = `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
748
+ const agentMdPath = path.join(aidDir, 'agent.md');
749
+ try {
750
+ await client.auth.uploadAgentMd(agentMdContent);
751
+ }
752
+ catch (e) {
753
+ // Non-fatal: first connection will auto-retry
754
+ }
755
+ fs.writeFileSync(agentMdPath, agentMdContent, 'utf-8');
756
+ try {
757
+ await client.close();
758
+ }
759
+ catch { /* ignore */ }
760
+ if (!fs.existsSync(agentMdPath)) {
761
+ throw new Error(`agent.md write verification failed: ${agentMdPath}`);
762
+ }
763
+ return { aid: opts.aid, alreadyExisted: false };
764
+ }
765
+ /**
766
+ * Append a new AUN instance to the config's channels.aun array and save.
767
+ * Handles upgrade from single-object to array format.
768
+ */
769
+ export function appendAunInstance(config, inst) {
770
+ if (!config.channels)
771
+ config.channels = {};
772
+ const newInst = {
773
+ name: inst.name,
774
+ enabled: inst.enabled ?? true,
775
+ aid: inst.aid,
776
+ ...(inst.owner && { owner: inst.owner }),
777
+ };
778
+ if (Array.isArray(config.channels.aun)) {
779
+ config.channels.aun.push(newInst);
780
+ }
781
+ else if (config.channels.aun) {
782
+ const oldInst = { ...config.channels.aun, name: config.channels.aun.name || 'aun' };
783
+ config.channels.aun = [oldInst, newInst];
784
+ }
785
+ else {
786
+ config.channels.aun = [newInst];
787
+ }
788
+ const p = resolvePaths();
789
+ fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
790
+ }
718
791
  export async function setupAunAid(rl, _config) {
719
792
  let aid = '';
720
793
  let gatewayPort; // only used locally for AID creation, not written to config
@@ -755,7 +828,7 @@ export async function setupAunAid(rl, _config) {
755
828
  console.log(' 正在创建 AID...');
756
829
  let failed = false;
757
830
  try {
758
- const { AUNClient } = await import('@agentunion/aun-node');
831
+ const { AUNClient } = await import('@agentunion/fastaun');
759
832
  let client = new AUNClient({ aun_path: aunPath });
760
833
  // 如果用户指定了自定义端口,手动设置 gateway URL;否则让 SDK 自动发现
761
834
  if (gatewayPort) {
@@ -408,14 +408,14 @@ export async function cmdInit(options) {
408
408
  // 自动安装 AUN SDK
409
409
  const { resolveAunCoreSdkPkg, npmInstallGlobal, downloadCaRoot } = await import('./init-channel.js');
410
410
  if (!resolveAunCoreSdkPkg()) {
411
- console.log('正在安装 @agentunion/aun-node...');
412
- await npmInstallGlobal('@agentunion/aun-node@latest');
411
+ console.log('正在安装 @agentunion/fastaun...');
412
+ await npmInstallGlobal('@agentunion/fastaun@latest');
413
413
  }
414
414
  // 创建 AID(如果本地不存在)
415
415
  const aunPath = path.join(os.homedir(), '.aun');
416
416
  const aidDir = path.join(aunPath, 'AIDs', options.aunAid);
417
417
  if (!fs.existsSync(path.join(aidDir, 'private'))) {
418
- const { AUNClient } = await import('@agentunion/aun-node');
418
+ const { AUNClient } = await import('@agentunion/fastaun');
419
419
  let client = new AUNClient({ aun_path: aunPath });
420
420
  // 让 SDK 通过 well-known 自动发现网关
421
421
  const result = await client.auth.createAid({ aid: options.aunAid });
@@ -0,0 +1,100 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execFile } from 'child_process';
4
+ import { getPackageRoot } from '../paths.js';
5
+ /**
6
+ * 比较两个 semver 版本号 (a.b.c 格式)
7
+ * 返回 -1 (a < b), 0 (a == b), 1 (a > b)
8
+ * 自动剥离 pre-release 标签 (e.g. 2.6.0-beta.1 → 2.6.0)
9
+ */
10
+ export function compareVersions(a, b) {
11
+ const pa = a.split('-')[0].split('.').map(Number);
12
+ const pb = b.split('-')[0].split('.').map(Number);
13
+ const len = Math.max(pa.length, pb.length);
14
+ for (let i = 0; i < len; i++) {
15
+ const na = pa[i] ?? 0;
16
+ const nb = pb[i] ?? 0;
17
+ if (na < nb)
18
+ return -1;
19
+ if (na > nb)
20
+ return 1;
21
+ }
22
+ return 0;
23
+ }
24
+ /**
25
+ * 检查当前安装是否为 npm link 开发模式。
26
+ * 正式全局安装的路径结构为 .../node_modules/evolclaw,
27
+ * 而 npm link 指向项目源码目录,其父目录不是 node_modules。
28
+ */
29
+ export function isLinkedInstall() {
30
+ const pkgRoot = getPackageRoot();
31
+ return path.basename(path.dirname(pkgRoot)) !== 'node_modules';
32
+ }
33
+ /** 获取本地 package.json 中的版本号 */
34
+ export function getLocalVersion() {
35
+ const pkgPath = path.join(getPackageRoot(), 'package.json');
36
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
37
+ return pkg.version;
38
+ }
39
+ /**
40
+ * 查询 npm registry 上 evolclaw 的最新版本。
41
+ * 超时 15 秒,失败返回 null。
42
+ */
43
+ export function checkLatestVersion() {
44
+ return new Promise((resolve) => {
45
+ execFile('npm', ['view', 'evolclaw', 'version'], { timeout: 15000 }, (err, stdout) => {
46
+ if (err) {
47
+ resolve(null);
48
+ return;
49
+ }
50
+ const ver = stdout.trim();
51
+ resolve(ver || null);
52
+ });
53
+ });
54
+ }
55
+ /**
56
+ * 执行 npm install -g evolclaw@latest
57
+ */
58
+ function runInstall() {
59
+ return new Promise((resolve) => {
60
+ execFile('npm', ['install', '-g', 'evolclaw@latest'], { timeout: 120000 }, (err, _stdout, stderr) => {
61
+ if (err) {
62
+ resolve({ ok: false, error: stderr || err.message });
63
+ }
64
+ else {
65
+ resolve({ ok: true });
66
+ }
67
+ });
68
+ });
69
+ }
70
+ /**
71
+ * 完整升级流程:检查 → 比较 → 安装(失败重试一次)
72
+ */
73
+ export async function tryUpgrade() {
74
+ // 开发模式跳过
75
+ if (isLinkedInstall()) {
76
+ return { status: 'skipped' };
77
+ }
78
+ const localVer = getLocalVersion();
79
+ // 查询 registry
80
+ const remoteVer = await checkLatestVersion();
81
+ if (!remoteVer) {
82
+ return { status: 'skipped', error: 'Failed to check remote version' };
83
+ }
84
+ // 版本比较
85
+ if (compareVersions(localVer, remoteVer) >= 0) {
86
+ return { status: 'no-update', from: localVer };
87
+ }
88
+ // 有新版本,执行升级(失败重试一次)
89
+ for (let attempt = 0; attempt < 2; attempt++) {
90
+ const result = await runInstall();
91
+ if (result.ok) {
92
+ return { status: 'upgraded', from: localVer, to: remoteVer };
93
+ }
94
+ if (attempt === 1) {
95
+ return { status: 'failed', from: localVer, to: remoteVer, error: result.error };
96
+ }
97
+ }
98
+ // unreachable
99
+ return { status: 'failed', from: localVer, to: remoteVer };
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,8 +23,8 @@
23
23
  "prepublishOnly": "npm run build && npm test"
24
24
  },
25
25
  "dependencies": {
26
+ "@agentunion/fastaun": "^0.2.15",
26
27
  "@anthropic-ai/claude-agent-sdk": "^0.2.100",
27
- "@agentunion/aun-node": "^0.2.12",
28
28
  "image-type": "^6.0.0",
29
29
  "qrcode-terminal": "^0.12.0"
30
30
  },
@@ -37,8 +37,8 @@
37
37
  "pure-qqbot": "^2.0.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/node": "^25.5.0",
41
40
  "@types/form-data": "^2.2.1",
41
+ "@types/node": "^25.5.0",
42
42
  "@types/qrcode-terminal": "^0.12.2",
43
43
  "@vitest/coverage-v8": "^4.1.0",
44
44
  "tsx": "^4.19.0",