@wu529778790/open-im 1.1.3 → 1.1.4-beta.0

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/README.md CHANGED
@@ -84,6 +84,22 @@ npm run dev # 直接运行源码(tsx,无需 build)
84
84
  | `ALLOWED_BASE_DIRS` | 允许访问的目录(逗号分隔) |
85
85
  | `LOG_DIR` | 日志目录,默认 `~/.open-im/logs` |
86
86
  | `LOG_LEVEL` | 日志级别:INFO/DEBUG/WARN/ERROR |
87
+ | `USE_SDK_MODE` | 默认 `true`(SDK 模式,更快);设为 `false` 使用 CLI 模式 |
88
+
89
+ ### SDK 模式(默认,更快)
90
+
91
+ 默认使用 Agent SDK 模式,进程内执行,每次对话不再 spawn 新进程,响应更快。若需改用 CLI 模式,可配置:
92
+
93
+ ```json
94
+ {
95
+ "useSdkMode": false,
96
+ "aiCommand": "claude"
97
+ }
98
+ ```
99
+
100
+ 或环境变量:`USE_SDK_MODE=false`
101
+
102
+ **认证**:需设置 `ANTHROPIC_API_KEY`(从 [Console](https://console.anthropic.com/) 获取)或运行 `claude setup-token` 生成 `CLAUDE_CODE_OAUTH_TOKEN`。SDK 模式无需安装 Claude CLI。
87
103
 
88
104
  ### 配置文件
89
105
 
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Claude SDK Adapter - 使用 Agent SDK 实现持久会话,无需每次 spawn 进程
3
+ *
4
+ * 优势:
5
+ * 1. 进程内执行 - 无 fork/exec 开销,响应更快
6
+ * 2. 会话复用 - resume 保留上下文,无需重新加载历史
7
+ * 3. 流式输出 - includePartialMessages 支持 text_delta、thinking_delta
8
+ *
9
+ * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN(claude setup-token)
10
+ */
11
+ import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
12
+ export declare class ClaudeSDKAdapter implements ToolAdapter {
13
+ readonly toolId = "claude-sdk";
14
+ run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
15
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Claude SDK Adapter - 使用 Agent SDK 实现持久会话,无需每次 spawn 进程
3
+ *
4
+ * 优势:
5
+ * 1. 进程内执行 - 无 fork/exec 开销,响应更快
6
+ * 2. 会话复用 - resume 保留上下文,无需重新加载历史
7
+ * 3. 流式输出 - includePartialMessages 支持 text_delta、thinking_delta
8
+ *
9
+ * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN(claude setup-token)
10
+ */
11
+ import { query } from '@anthropic-ai/claude-agent-sdk';
12
+ import { createLogger } from '../logger.js';
13
+ const log = createLogger('ClaudeSDK');
14
+ function isStreamEvent(msg) {
15
+ return msg.type === 'stream_event';
16
+ }
17
+ function isSystemInit(msg) {
18
+ const m = msg;
19
+ return m.type === 'system' && m.subtype === 'init';
20
+ }
21
+ function isResult(msg) {
22
+ return msg.type === 'result';
23
+ }
24
+ function isAssistant(msg) {
25
+ return msg.type === 'assistant';
26
+ }
27
+ export class ClaudeSDKAdapter {
28
+ toolId = 'claude-sdk';
29
+ run(prompt, sessionId, workDir, callbacks, options) {
30
+ const abortController = new AbortController();
31
+ let queryClosed = false;
32
+ const permissionMode = options?.skipPermissions
33
+ ? 'bypassPermissions'
34
+ : options?.permissionMode === 'acceptEdits'
35
+ ? 'acceptEdits'
36
+ : options?.permissionMode === 'plan'
37
+ ? 'plan'
38
+ : 'default';
39
+ const runQuery = async () => {
40
+ try {
41
+ const opts = {
42
+ cwd: workDir,
43
+ resume: sessionId,
44
+ includePartialMessages: true,
45
+ permissionMode,
46
+ model: options?.model,
47
+ abortController,
48
+ allowDangerouslySkipPermissions: permissionMode === 'bypassPermissions',
49
+ };
50
+ const q = query({
51
+ prompt,
52
+ options: opts,
53
+ });
54
+ let accumulated = '';
55
+ let accumulatedThinking = '';
56
+ const toolStats = {};
57
+ try {
58
+ for await (const msg of q) {
59
+ if (abortController.signal.aborted)
60
+ break;
61
+ if (isSystemInit(msg)) {
62
+ callbacks.onSessionId?.(msg.session_id);
63
+ continue;
64
+ }
65
+ if (isStreamEvent(msg)) {
66
+ const ev = msg.event;
67
+ if (ev?.type === 'content_block_delta' && ev.delta) {
68
+ if (ev.delta.type === 'text_delta' && ev.delta.text) {
69
+ accumulated += ev.delta.text;
70
+ callbacks.onText(accumulated);
71
+ }
72
+ else if (ev.delta.type === 'thinking_delta' && ev.delta.thinking) {
73
+ accumulatedThinking += ev.delta.thinking;
74
+ callbacks.onThinking?.(accumulatedThinking);
75
+ }
76
+ }
77
+ continue;
78
+ }
79
+ if (isAssistant(msg)) {
80
+ const content = msg.message?.content;
81
+ for (const block of content ?? []) {
82
+ if (block?.type === 'tool_use' && block.name) {
83
+ toolStats[block.name] = (toolStats[block.name] || 0) + 1;
84
+ callbacks.onToolUse?.(block.name, block.input);
85
+ }
86
+ }
87
+ continue;
88
+ }
89
+ if (isResult(msg)) {
90
+ queryClosed = true;
91
+ const m = msg;
92
+ const success = m.subtype === 'success';
93
+ const result = {
94
+ success,
95
+ result: m.result ?? '',
96
+ accumulated: success ? accumulated : '',
97
+ cost: m.total_cost_usd ?? 0,
98
+ durationMs: m.duration_ms ?? 0,
99
+ numTurns: m.num_turns ?? 0,
100
+ toolStats,
101
+ };
102
+ if (!result.accumulated && result.result)
103
+ result.accumulated = result.result;
104
+ callbacks.onComplete(result);
105
+ return;
106
+ }
107
+ }
108
+ if (!queryClosed) {
109
+ callbacks.onComplete({
110
+ success: true,
111
+ result: accumulated,
112
+ accumulated,
113
+ cost: 0,
114
+ durationMs: 0,
115
+ numTurns: 0,
116
+ toolStats,
117
+ });
118
+ }
119
+ }
120
+ finally {
121
+ q.close();
122
+ }
123
+ }
124
+ catch (err) {
125
+ if (abortController.signal.aborted)
126
+ return;
127
+ const msg = err instanceof Error ? err.message : String(err);
128
+ log.error(`Claude SDK error: ${msg}`);
129
+ callbacks.onError(msg);
130
+ }
131
+ };
132
+ runQuery();
133
+ return {
134
+ abort: () => {
135
+ abortController.abort();
136
+ },
137
+ };
138
+ }
139
+ }
@@ -1,13 +1,20 @@
1
1
  import { ClaudeAdapter } from './claude-adapter.js';
2
+ import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
2
3
  const adapters = new Map();
3
4
  export function initAdapters(config) {
4
5
  adapters.clear();
5
6
  if (config.aiCommand === 'claude') {
6
- // Enable process pool with 2 minute idle timeout
7
- adapters.set('claude', new ClaudeAdapter(config.claudeCliPath, {
8
- useProcessPool: true,
9
- idleTimeoutMs: 2 * 60 * 1000, // 2 minutes
10
- }));
7
+ if (config.useSdkMode) {
8
+ console.log(' 启用 Claude Agent SDK 模式 - 进程内执行,响应更快');
9
+ adapters.set('claude', new ClaudeSDKAdapter());
10
+ }
11
+ else {
12
+ console.log('🚀 使用标准 Claude 适配器');
13
+ adapters.set('claude', new ClaudeAdapter(config.claudeCliPath, {
14
+ useProcessPool: true,
15
+ idleTimeoutMs: 2 * 60 * 1000, // 2 minutes
16
+ }));
17
+ }
11
18
  }
12
19
  }
13
20
  export function getAdapter(aiCommand) {
package/dist/config.d.ts CHANGED
@@ -33,6 +33,8 @@ export interface Config {
33
33
  hookPort: number;
34
34
  logDir: string;
35
35
  logLevel: LogLevel;
36
+ /** 是否使用 Agent SDK(进程内执行,无 spawn 开销,响应更快) */
37
+ useSdkMode: boolean;
36
38
  platforms: {
37
39
  telegram?: {
38
40
  enabled: boolean;
package/dist/config.js CHANGED
@@ -146,8 +146,11 @@ export function loadConfig() {
146
146
  const hookPort = process.env.HOOK_PORT !== undefined
147
147
  ? parseInt(process.env.HOOK_PORT, 10) || 35801
148
148
  : file.hookPort ?? 35801;
149
- // 6. 校验 Claude CLI
150
- if (aiCommand === 'claude') {
149
+ const useSdkMode = process.env.USE_SDK_MODE !== undefined
150
+ ? process.env.USE_SDK_MODE === 'true'
151
+ : file.useSdkMode ?? true;
152
+ // 6. 校验 Claude CLI(SDK 模式不需要 CLI)
153
+ if (aiCommand === 'claude' && !useSdkMode) {
151
154
  if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
152
155
  try {
153
156
  accessSync(claudeCliPath, constants.F_OK | constants.X_OK);
@@ -272,6 +275,7 @@ export function loadConfig() {
272
275
  hookPort,
273
276
  logDir,
274
277
  logLevel,
278
+ useSdkMode,
275
279
  platforms,
276
280
  };
277
281
  }
package/dist/setup.js CHANGED
@@ -409,6 +409,7 @@ export async function runInteractiveSetup() {
409
409
  claudeWorkDir: (commonResp.workDir || process.cwd()).trim(),
410
410
  claudeSkipPermissions: base?.claudeSkipPermissions ?? true,
411
411
  aiCommand: commonResp.aiCommand ?? base?.aiCommand ?? "claude",
412
+ useSdkMode: base?.useSdkMode ?? true,
412
413
  };
413
414
  if (selectedPlatforms.includes("telegram")) {
414
415
  out.platforms.telegram = {
@@ -74,9 +74,13 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
74
74
  }
75
75
  };
76
76
  const mode = getPermissionMode(ctx.userId, config.defaultPermissionMode);
77
- // 全部交给 Claude 自己处理:yolo 用 --dangerously-skip-permissions,其他用 --permission-mode
78
- const skipPermissions = mode === 'yolo' || config.claudeSkipPermissions;
79
- const permissionMode = !skipPermissions
77
+ process.env.CC_IM_CHAT_ID = ctx.chatId;
78
+ // 构建运行选项
79
+ let skipPermissions;
80
+ let permissionMode;
81
+ // 标准模式 / SDK 模式:传递权限模式参数
82
+ skipPermissions = mode === 'yolo' || config.claudeSkipPermissions;
83
+ permissionMode = !skipPermissions
80
84
  ? (mode === 'ask'
81
85
  ? 'default'
82
86
  : mode === 'accept-edits'
@@ -85,7 +89,6 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
85
89
  ? 'plan'
86
90
  : undefined)
87
91
  : undefined;
88
- process.env.CC_IM_CHAT_ID = ctx.chatId;
89
92
  const handle = toolAdapter.run(prompt, ctx.sessionId, ctx.workDir, {
90
93
  onSessionId: (id) => {
91
94
  if (ctx.threadId)
@@ -171,7 +174,8 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
171
174
  timeoutMs: config.claudeTimeoutMs,
172
175
  model: sessionManager.getModel(ctx.userId, ctx.threadId) ?? config.claudeModel,
173
176
  chatId: ctx.chatId,
174
- hookPort: config.hookPort,
177
+ // SDK 模式下不使用 hookPort
178
+ ...(config.useSdkMode ? {} : { hookPort: config.hookPort }),
175
179
  });
176
180
  taskState = { handle, latestContent: '', settle, startedAt: Date.now() };
177
181
  platformAdapter.onTaskReady(taskState);
@@ -34,6 +34,8 @@ export declare function initWeWork(cfg: Config, eventHandler: (data: WeWorkCallb
34
34
  export declare function sendMessage(message: WeWorkResponseMessage): void;
35
35
  /**
36
36
  * Send text message via WebSocket (requires req_id from callback)
37
+ * 企业微信 aibot_respond_msg 仅支持 stream 和 template_card,不支持 text/markdown
38
+ * 使用 stream 格式,finish=true 表示一次性回复
37
39
  */
38
40
  export declare function sendText(reqId: string, content: string): void;
39
41
  /**
@@ -289,11 +289,14 @@ export function sendMessage(message) {
289
289
  }
290
290
  /**
291
291
  * Send text message via WebSocket (requires req_id from callback)
292
+ * 企业微信 aibot_respond_msg 仅支持 stream 和 template_card,不支持 text/markdown
293
+ * 使用 stream 格式,finish=true 表示一次性回复
292
294
  */
293
295
  export function sendText(reqId, content) {
296
+ const streamId = `${Date.now()}-${randomBytes(8).toString('hex')}`;
294
297
  sendWebSocketReply(reqId, {
295
- msgtype: 'text',
296
- text: { content },
298
+ msgtype: 'stream',
299
+ stream: { id: streamId, finish: true, content },
297
300
  });
298
301
  }
299
302
  /**
@@ -162,9 +162,12 @@ export async function sendProactiveTextReply(chatId, text) {
162
162
  */
163
163
  export async function sendTextReply(chatId, text, threadCtxOrReqId) {
164
164
  const message = formatWeWorkMessage('📢 open-im', text, 'done');
165
- const reqId = typeof threadCtxOrReqId === 'string' ? threadCtxOrReqId : undefined;
165
+ // 显式传递的 reqId(用于兼容 MessageSender 接口)
166
+ const explicitReqId = typeof threadCtxOrReqId === 'string' ? threadCtxOrReqId : undefined;
167
+ // 回退到当前请求的 reqId(在 handleEvent 中通过 setCurrentReqId 设置)
168
+ const effectiveReqId = explicitReqId ?? currentReqId;
166
169
  try {
167
- sendText(getReqId(reqId), message);
170
+ sendText(getReqId(effectiveReqId ?? undefined), message);
168
171
  log.info(`Text reply sent to user ${chatId}`);
169
172
  }
170
173
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.1.3",
3
+ "version": "1.1.4-beta.0",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,7 +19,8 @@
19
19
  "init": "tsx src/cli.ts init",
20
20
  "start": "node dist/cli.js start",
21
21
  "stop": "node dist/cli.js stop",
22
- "test": "npm run build",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest watch",
23
24
  "prepublishOnly": "npm run build"
24
25
  },
25
26
  "keywords": [
@@ -42,6 +43,7 @@
42
43
  "url": "https://github.com/wu529778790/open-im/issues"
43
44
  },
44
45
  "dependencies": {
46
+ "@anthropic-ai/claude-agent-sdk": "^0.2.72",
45
47
  "@larksuiteoapi/node-sdk": "^1.59.0",
46
48
  "@wecom/wecom-openclaw-plugin": "^1.0.6",
47
49
  "prompts": "^2.4.2",
@@ -54,9 +56,11 @@
54
56
  "@types/prompts": "^2.4.9",
55
57
  "@types/qrcode-terminal": "^0.12.2",
56
58
  "@types/ws": "^8.5.13",
59
+ "@vitest/coverage-v8": "4.0.18",
57
60
  "dotenv": "^16.0.0",
58
61
  "tsx": "^4.0.0",
59
- "typescript": "^5.0.0"
62
+ "typescript": "^5.0.0",
63
+ "vitest": "4.0.18"
60
64
  },
61
65
  "engines": {
62
66
  "node": ">=20"