evolclaw 2.6.0 → 2.6.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.
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.2",
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",