@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 +16 -0
- package/dist/adapters/claude-sdk-adapter.d.ts +15 -0
- package/dist/adapters/claude-sdk-adapter.js +139 -0
- package/dist/adapters/registry.js +12 -5
- package/dist/config.d.ts +2 -0
- package/dist/config.js +6 -2
- package/dist/setup.js +1 -0
- package/dist/shared/ai-task.js +9 -5
- package/dist/wework/client.d.ts +2 -0
- package/dist/wework/client.js +5 -2
- package/dist/wework/message-sender.js +5 -2
- package/package.json +7 -3
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
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
|
-
|
|
150
|
-
|
|
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 = {
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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);
|
package/dist/wework/client.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/wework/client.js
CHANGED
|
@@ -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: '
|
|
296
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
"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": "
|
|
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"
|