evolclaw 2.5.5 → 2.5.6

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) => {
@@ -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 提供更友好的错误提示
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.5.5",
3
+ "version": "2.5.6",
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"]