evolclaw 2.5.5 → 2.5.7

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.
@@ -5,7 +5,6 @@
5
5
  * Implements the same interface surface as AgentRunner (claude-runner.ts)
6
6
  * so MessageProcessor and CommandHandler can work with it transparently.
7
7
  */
8
- import { Codex } from '@openai/codex-sdk';
9
8
  import { resolveOpenaiConfig } from '../config.js';
10
9
  import { logger } from '../utils/logger.js';
11
10
  import fs from 'fs';
@@ -24,24 +23,33 @@ const CODEX_MODELS = ['gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5-codex', 'gpt-5.2'
24
23
  export class CodexRunner {
25
24
  name = 'codex';
26
25
  capabilities = { clear: false, compact: false, fork: false };
27
- codex;
26
+ codex = null;
27
+ codexModule = null;
28
28
  model;
29
29
  effort;
30
30
  activeAbortControllers = new Map();
31
31
  activeStreams = new Map();
32
32
  activeSessions = new Map(); // sessionId → threadId
33
33
  onSessionIdUpdate;
34
+ resolvedConfig;
34
35
  constructor(config, callbacks) {
35
- const resolved = resolveOpenaiConfig(config);
36
- this.codex = new Codex({
37
- apiKey: resolved.apiKey,
38
- baseUrl: resolved.baseUrl,
39
- });
40
- this.model = resolved.model;
41
- if (resolved.effort)
42
- this.effort = resolved.effort;
36
+ this.resolvedConfig = resolveOpenaiConfig(config);
37
+ this.model = this.resolvedConfig.model;
38
+ if (this.resolvedConfig.effort)
39
+ this.effort = this.resolvedConfig.effort;
43
40
  this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
44
41
  }
42
+ async ensureCodex() {
43
+ if (!this.codex || !this.codexModule) {
44
+ const { requireOptional } = await import('../utils/init-channel.js');
45
+ this.codexModule = await requireOptional('@openai/codex-sdk');
46
+ this.codex = new this.codexModule.Codex({
47
+ apiKey: this.resolvedConfig.apiKey,
48
+ baseUrl: this.resolvedConfig.baseUrl,
49
+ });
50
+ }
51
+ return { codex: this.codex, mod: this.codexModule };
52
+ }
45
53
  // ── ModelSwitcher ──
46
54
  setModel(model) { this.model = model; }
47
55
  getModel() { return this.model; }
@@ -89,6 +97,7 @@ export class CodexRunner {
89
97
  async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager) {
90
98
  // Agent ctl: 注入 EVOLCLAW_SESSION_ID 供子进程使用
91
99
  process.env.EVOLCLAW_SESSION_ID = sessionId;
100
+ const { codex } = await this.ensureCodex();
92
101
  let agentSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
93
102
  const threadOptions = {
94
103
  workingDirectory: projectPath,
@@ -99,8 +108,8 @@ export class CodexRunner {
99
108
  ...(this.effort ? { modelReasoningEffort: this.effort } : {}),
100
109
  };
101
110
  const thread = agentSessionId
102
- ? this.codex.resumeThread(agentSessionId, threadOptions)
103
- : this.codex.startThread(threadOptions);
111
+ ? codex.resumeThread(agentSessionId, threadOptions)
112
+ : codex.startThread(threadOptions);
104
113
  const controller = new AbortController();
105
114
  this.activeAbortControllers.set(sessionId, controller);
106
115
  // 构建输入:将 base64 图片写入临时文件,转换为 Codex SDK 的 local_image 格式
@@ -226,7 +235,7 @@ export class CodexRunner {
226
235
  yield { type: 'tool_use', name: `MCP:${item.server}/${item.tool}`, input: item.arguments };
227
236
  }
228
237
  else if (item.type === 'file_change') {
229
- const desc = item.changes.map(c => `${c.kind} ${c.path}`).join(', ');
238
+ const desc = item.changes.map((c) => `${c.kind} ${c.path}`).join(', ');
230
239
  yield { type: 'tool_use', name: 'FileChange', input: { description: desc } };
231
240
  }
232
241
  else if (item.type === 'web_search') {
@@ -1,4 +1,5 @@
1
1
  import { logger } from '../utils/logger.js';
2
+ import { requireOptional } from '../utils/init-channel.js';
2
3
  import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
3
4
  // ── Webhook SSRF validation ────────────────────────────────────────────────────
4
5
  const WEBHOOK_RE = /^https:\/\/(api|oapi)\.dingtalk\.com\//;
@@ -56,7 +57,7 @@ export class DingtalkChannel {
56
57
  if (!clientId || !clientSecret || clientId.includes('your-') || clientSecret.includes('your-')) {
57
58
  throw new Error('DingTalk clientId/clientSecret not configured');
58
59
  }
59
- const { DWClient, TOPIC_ROBOT } = await import('dingtalk-stream');
60
+ const { DWClient, TOPIC_ROBOT } = await requireOptional('dingtalk-stream');
60
61
  this.client = new DWClient({ clientId, clientSecret });
61
62
  this.client.registerCallbackListener(TOPIC_ROBOT, async (msg) => {
62
63
  await this.handleIncoming(msg);
@@ -320,7 +321,7 @@ export class DingtalkChannel {
320
321
  return;
321
322
  }
322
323
  // Step 1: Upload media
323
- const FormData = (await import('form-data')).default;
324
+ const FormData = (await requireOptional('form-data')).default;
324
325
  const form = new FormData();
325
326
  form.append('type', 'image');
326
327
  form.append('media', png, { filename: 'image.png', contentType: 'image/png' });
@@ -360,7 +361,7 @@ export class DingtalkChannel {
360
361
  return;
361
362
  }
362
363
  // Step 1: Upload media
363
- const FormData = (await import('form-data')).default;
364
+ const FormData = (await requireOptional('form-data')).default;
364
365
  const form = new FormData();
365
366
  form.append('type', 'file');
366
367
  form.append('media', fs.createReadStream(filePath), { filename: path.basename(filePath) });
@@ -1,4 +1,3 @@
1
- import * as lark from '@larksuiteoapi/node-sdk';
2
1
  import fs from 'fs';
3
2
  import path from 'path';
4
3
  import imageType from 'image-type';
@@ -41,6 +40,8 @@ export class FeishuChannel {
41
40
  if (this.config.appId.startsWith('YOUR_') || this.config.appSecret.startsWith('YOUR_')) {
42
41
  throw new Error('Feishu credentials not configured (placeholder values detected)');
43
42
  }
43
+ const { requireOptional } = await import('../utils/init-channel.js');
44
+ const lark = await requireOptional('@larksuiteoapi/node-sdk');
44
45
  try {
45
46
  this.client = new lark.Client({
46
47
  appId: this.config.appId,
@@ -1,5 +1,6 @@
1
1
  import { logger } from '../utils/logger.js';
2
2
  import { markdownToPlainText } from '../utils/format.js';
3
+ import { requireOptional } from '../utils/init-channel.js';
3
4
  import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
4
5
  // ── QQBotChannel ────────────────────────────────────────────────────────────
5
6
  export class QQBotChannel {
@@ -37,7 +38,7 @@ export class QQBotChannel {
37
38
  if (!appId || !clientSecret || appId.includes('your-') || clientSecret.includes('your-')) {
38
39
  throw new Error('QQBot appId/clientSecret not configured');
39
40
  }
40
- const { QQBotClient } = await import('pure-qqbot');
41
+ const { QQBotClient } = await requireOptional('pure-qqbot');
41
42
  this.client = new QQBotClient({
42
43
  appId,
43
44
  clientSecret,
@@ -1,5 +1,6 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { logger } from '../utils/logger.js';
3
+ import { requireOptional } from '../utils/init-channel.js';
3
4
  import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
4
5
  // ── WecomChannel ───────────────────────────────────────────────────────────────
5
6
  export class WecomChannel {
@@ -31,7 +32,7 @@ export class WecomChannel {
31
32
  if (!botId || !secret) {
32
33
  throw new Error('WeCom botId/secret not configured');
33
34
  }
34
- const { WSClient } = await import('@wecom/aibot-node-sdk');
35
+ const { WSClient } = await requireOptional('@wecom/aibot-node-sdk');
35
36
  this.client = new WSClient({ botId, secret });
36
37
  // Message events
37
38
  this.client.on('message', (frame) => {
package/dist/cli.js CHANGED
@@ -1307,7 +1307,7 @@ export async function main(args) {
1307
1307
  else if (args[1] === 'wecom') {
1308
1308
  await cmdInitWecom();
1309
1309
  }
1310
- else if (args[1]) {
1310
+ else if (args[1] && !args[1].startsWith('-')) {
1311
1311
  const supported = ['feishu', 'wechat', 'aun', 'dingtalk', 'qqbot', 'wecom'];
1312
1312
  console.error(`❌ 不支持的渠道: ${args[1]}`);
1313
1313
  console.error(` 支持的渠道: ${supported.join(', ')}`);
@@ -681,7 +681,7 @@ export class MessageProcessor {
681
681
  // 区分超时 / 中断 / 错误
682
682
  const errType = classifyError(error);
683
683
  const interruptReason = this.interruptedSessions.get(session.id);
684
- const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop';
684
+ const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
685
685
  const procStatus = errType === ErrorType.SDK_TIMEOUT ? 'timeout'
686
686
  : errType === ErrorType.STREAM_ERROR ? 'interrupted'
687
687
  : 'error';
@@ -889,7 +889,7 @@ export class MessageProcessor {
889
889
  // 失败且无前置错误输出:显示 errors 摘要
890
890
  // 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
891
891
  const interruptReason = this.interruptedSessions.get(session.id);
892
- const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop';
892
+ const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
893
893
  if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt) {
894
894
  const errorSummary = event.errors?.join('; ') || '\u4efb\u52a1\u6267\u884c\u5931\u8d25';
895
895
  // 使用 terminalReason 提供更友好的错误提示
package/dist/ipc.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import net from 'net';
2
2
  import fs from 'fs';
3
3
  import { logger } from './utils/logger.js';
4
+ const isWindows = process.platform === 'win32';
5
+ const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
4
6
  export class IpcServer {
5
7
  socketPath;
6
8
  getStatus;
@@ -12,11 +14,13 @@ export class IpcServer {
12
14
  this.commandExecutor = commandExecutor;
13
15
  }
14
16
  start() {
15
- // Remove stale socket file
16
- try {
17
- fs.unlinkSync(this.socketPath);
17
+ // Remove stale socket file (Unix only — named pipes auto-cleanup on process exit)
18
+ if (!isNamedPipe(this.socketPath)) {
19
+ try {
20
+ fs.unlinkSync(this.socketPath);
21
+ }
22
+ catch { }
18
23
  }
19
- catch { }
20
24
  this.server = net.createServer((conn) => {
21
25
  let buf = '';
22
26
  conn.on('data', async (data) => {
@@ -42,11 +46,13 @@ export class IpcServer {
42
46
  logger.error('[IPC] Server error:', err);
43
47
  });
44
48
  this.server.listen(this.socketPath, () => {
45
- // Ensure socket is readable by current user only
46
- try {
47
- fs.chmodSync(this.socketPath, 0o600);
49
+ // Restrict to current user (Unix only named pipes use Windows ACLs)
50
+ if (!isNamedPipe(this.socketPath)) {
51
+ try {
52
+ fs.chmodSync(this.socketPath, 0o600);
53
+ }
54
+ catch { }
48
55
  }
49
- catch { }
50
56
  logger.info(`[IPC] Listening on ${this.socketPath}`);
51
57
  });
52
58
  }
@@ -55,10 +61,12 @@ export class IpcServer {
55
61
  this.server.close();
56
62
  this.server = null;
57
63
  }
58
- try {
59
- fs.unlinkSync(this.socketPath);
64
+ if (!isNamedPipe(this.socketPath)) {
65
+ try {
66
+ fs.unlinkSync(this.socketPath);
67
+ }
68
+ catch { }
60
69
  }
61
- catch { }
62
70
  }
63
71
  async handleCommand(cmd) {
64
72
  switch (cmd.type) {
package/dist/paths.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import crypto from 'crypto';
5
+ const isWindows = process.platform === 'win32';
4
6
  let _root = null;
5
7
  export function resolveRoot() {
6
8
  if (_root)
@@ -33,9 +35,16 @@ export function resolvePaths() {
33
35
  lineStats: path.join(root, 'logs', 'line-stats.log'),
34
36
  readySignal: path.join(root, 'logs', 'ready.signal'),
35
37
  selfHealLog: path.join(root, 'logs', 'self-heal.md'),
36
- socket: path.join(root, 'logs', 'evolclaw.sock'),
38
+ socket: resolveSocketPath(root),
37
39
  };
38
40
  }
41
+ function resolveSocketPath(root) {
42
+ if (isWindows) {
43
+ const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
44
+ return `\\\\.\\pipe\\evolclaw-${hash}`;
45
+ }
46
+ return path.join(root, 'logs', 'evolclaw.sock');
47
+ }
39
48
  export function ensureDataDirs() {
40
49
  const p = resolvePaths();
41
50
  fs.mkdirSync(p.dataDir, { recursive: true });
@@ -36,6 +36,22 @@ export async function npmInstallGlobal(pkg) {
36
36
  }
37
37
  }
38
38
  }
39
+ /** Dynamic import with auto-install fallback for optional dependencies */
40
+ export async function requireOptional(pkg, autoInstall = true) {
41
+ try {
42
+ return await import(pkg);
43
+ }
44
+ catch (e) {
45
+ if (e.code !== 'ERR_MODULE_NOT_FOUND' && e.code !== 'MODULE_NOT_FOUND')
46
+ throw e;
47
+ if (!autoInstall)
48
+ throw new Error(`依赖 ${pkg} 未安装。请运行: npm install -g ${pkg}`);
49
+ const { logger } = await import('./logger.js');
50
+ logger.info(`正在安装可选依赖 ${pkg}...`);
51
+ await npmInstallGlobal(pkg);
52
+ return await import(pkg);
53
+ }
54
+ }
39
55
  function ask(rl, question) {
40
56
  return new Promise(resolve => rl.question(question, resolve));
41
57
  }
@@ -738,14 +754,26 @@ export async function setupAunAid(rl, _config) {
738
754
  const agentType = typeInput === 'human' ? 'human' : 'ai';
739
755
  const agentName = aid.split('.')[0];
740
756
  const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
757
+ const agentMdPath = path.join(aidDir, 'agent.md');
741
758
  try {
742
759
  await client.auth.uploadAgentMd(agentMdContent);
743
760
  console.log(' ✓ agent.md 已发布');
744
- fs.writeFileSync(path.join(aidDir, 'agent.md'), agentMdContent, 'utf-8');
745
761
  }
746
762
  catch (e) {
747
763
  console.log(` ⚠ agent.md 发布失败(可稍后用 /agentmd put 重试): ${String(e.message || e).slice(0, 100)}`);
748
764
  }
765
+ fs.writeFileSync(agentMdPath, agentMdContent, 'utf-8');
766
+ if (!fs.existsSync(agentMdPath)) {
767
+ try {
768
+ await client.close();
769
+ }
770
+ catch { /* ignore */ }
771
+ console.log(` ✗ agent.md 本地写入校验失败: ${agentMdPath}`);
772
+ failed = true;
773
+ }
774
+ else {
775
+ console.log(' ✓ agent.md 已写入本地');
776
+ }
749
777
  try {
750
778
  await client.close();
751
779
  }
@@ -445,11 +445,21 @@ export async function cmdInit(options) {
445
445
  // 写入初始 agent.md(initialized: false)
446
446
  const agentName = options.aunAid.split('.')[0];
447
447
  const agentMd = `---\naid: "${options.aunAid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
448
+ const agentMdPath = path.join(aidDir, 'agent.md');
448
449
  try {
449
450
  await client.auth.uploadAgentMd(agentMd);
450
- fs.writeFileSync(path.join(aidDir, 'agent.md'), agentMd, 'utf-8');
451
451
  }
452
- catch { }
452
+ catch (e) {
453
+ console.warn(`⚠ agent.md 网络发布失败(可稍后重试): ${String(e?.message || e).slice(0, 100)}`);
454
+ }
455
+ fs.writeFileSync(agentMdPath, agentMd, 'utf-8');
456
+ if (!fs.existsSync(agentMdPath)) {
457
+ try {
458
+ await client.close();
459
+ }
460
+ catch { }
461
+ throw new Error(`agent.md 写入校验失败: ${agentMdPath}`);
462
+ }
453
463
  try {
454
464
  await client.close();
455
465
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.5.5",
3
+ "version": "2.5.7",
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",
@@ -11,8 +11,7 @@
11
11
  "dist/",
12
12
  "!dist/experimental/",
13
13
  "data/evolclaw.sample.json",
14
- "evolclaw-install-aun.md",
15
- "aun/pyproject.toml"
14
+ "evolclaw-install-aun.md"
16
15
  ],
17
16
  "scripts": {
18
17
  "dev": "tsx watch src/index.ts",
@@ -26,19 +25,20 @@
26
25
  "dependencies": {
27
26
  "@anthropic-ai/claude-agent-sdk": "^0.2.100",
28
27
  "@agentunion/aun-node": "^0.2.12",
28
+ "image-type": "^6.0.0",
29
+ "qrcode-terminal": "^0.12.0"
30
+ },
31
+ "optionalDependencies": {
29
32
  "@larksuiteoapi/node-sdk": "^1.59.0",
30
33
  "@openai/codex-sdk": "^0.118.0",
31
- "@types/form-data": "^2.2.1",
32
34
  "@wecom/aibot-node-sdk": "^1.0.6",
33
35
  "dingtalk-stream": "^2.1.6-beta.1",
34
- "fast-xml-parser": "^5.7.2",
35
36
  "form-data": "^4.0.5",
36
- "image-type": "^6.0.0",
37
- "pure-qqbot": "^2.0.0",
38
- "qrcode-terminal": "^0.12.0"
37
+ "pure-qqbot": "^2.0.0"
39
38
  },
40
39
  "devDependencies": {
41
40
  "@types/node": "^25.5.0",
41
+ "@types/form-data": "^2.2.1",
42
42
  "@types/qrcode-terminal": "^0.12.2",
43
43
  "@vitest/coverage-v8": "^4.1.0",
44
44
  "tsx": "^4.19.0",
@@ -1,20 +0,0 @@
1
- [project]
2
- name = "aun-cli"
3
- version = "0.1.0"
4
- description = "AUN CLI - Interactive command-line client for AUN protocol"
5
- requires-python = ">=3.10"
6
- dependencies = [
7
- "aunp>=0.2.12",
8
- "prompt-toolkit>=3.0.0",
9
- "rich>=13.0.0",
10
- ]
11
-
12
- [project.scripts]
13
- aun = "aun_cli:cli_main"
14
-
15
- [build-system]
16
- requires = ["setuptools>=61.0"]
17
- build-backend = "setuptools.build_meta"
18
-
19
- [tool.setuptools]
20
- py-modules = ["aun_cli"]