@wu529778790/open-im 1.7.0-beta.1 → 1.7.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.
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude SDK Adapter - 使用 Agent SDK
|
|
2
|
+
* Claude SDK Adapter - 使用 Agent SDK V2 Session API 实现真正的多轮对话
|
|
3
3
|
*
|
|
4
|
-
* 优势:
|
|
5
|
-
* 1. 进程内执行 - 无 fork/exec
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. 流式输出 -
|
|
4
|
+
* V2 API 优势:
|
|
5
|
+
* 1. 进程内执行 - 无 fork/exec 开销
|
|
6
|
+
* 2. 持久会话 - SDKSession 对象保持会话状态,支持真正的多轮对话
|
|
7
|
+
* 3. 流式输出 - 支持实时增量更新
|
|
8
8
|
*
|
|
9
|
-
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
9
|
+
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
11
|
import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
|
|
12
12
|
export declare class ClaudeSDKAdapter implements ToolAdapter {
|
|
13
13
|
readonly toolId = "claude-sdk";
|
|
14
14
|
/**
|
|
15
|
-
* 清理所有活跃的 SDK
|
|
15
|
+
* 清理所有活跃的 SDK 会话和流
|
|
16
16
|
*/
|
|
17
17
|
static destroy(): void;
|
|
18
18
|
run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude SDK Adapter - 使用 Agent SDK
|
|
2
|
+
* Claude SDK Adapter - 使用 Agent SDK V2 Session API 实现真正的多轮对话
|
|
3
3
|
*
|
|
4
|
-
* 优势:
|
|
5
|
-
* 1. 进程内执行 - 无 fork/exec
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. 流式输出 -
|
|
4
|
+
* V2 API 优势:
|
|
5
|
+
* 1. 进程内执行 - 无 fork/exec 开销
|
|
6
|
+
* 2. 持久会话 - SDKSession 对象保持会话状态,支持真正的多轮对话
|
|
7
|
+
* 3. 流式输出 - 支持实时增量更新
|
|
8
8
|
*
|
|
9
|
-
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
9
|
+
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { unstable_v2_createSession, unstable_v2_resumeSession } from '@anthropic-ai/claude-agent-sdk';
|
|
12
12
|
import { createLogger } from '../logger.js';
|
|
13
13
|
const log = createLogger('ClaudeSDK');
|
|
14
|
-
//
|
|
15
|
-
|
|
14
|
+
// 存储所有活跃的 SDKSession 对象,key 为 sessionId
|
|
15
|
+
// 使用 Map 而不是 Set,因为我们需要通过 sessionId 获取 session
|
|
16
|
+
const activeSessions = new Map();
|
|
17
|
+
// 存储正在进行的流式迭代器,用于中断
|
|
18
|
+
const activeStreams = new Set();
|
|
16
19
|
function isStreamEvent(msg) {
|
|
17
20
|
return msg.type === 'stream_event';
|
|
18
21
|
}
|
|
@@ -20,33 +23,82 @@ function isSystemInit(msg) {
|
|
|
20
23
|
const m = msg;
|
|
21
24
|
return m.type === 'system' && m.subtype === 'init';
|
|
22
25
|
}
|
|
26
|
+
function isAssistant(msg) {
|
|
27
|
+
return msg.type === 'assistant';
|
|
28
|
+
}
|
|
23
29
|
function isResult(msg) {
|
|
24
30
|
return msg.type === 'result';
|
|
25
31
|
}
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 获取或创建 SDKSession
|
|
34
|
+
* @param sessionId 已有的 sessionId,如果为 undefined 则创建新会话
|
|
35
|
+
* @param workDir 工作目录
|
|
36
|
+
* @param model 模型名称
|
|
37
|
+
* @param permissionMode 权限模式
|
|
38
|
+
* @returns SDKSession 对象和实际的 sessionId
|
|
39
|
+
*/
|
|
40
|
+
async function getOrCreateSession(sessionId, _workDir, // 保留参数以备将来使用
|
|
41
|
+
model, permissionMode) {
|
|
42
|
+
const sessionOptions = {
|
|
43
|
+
model: model || 'claude-opus-4-5',
|
|
44
|
+
permissionMode,
|
|
45
|
+
// 可以添加其他选项,如 hooks, allowedTools 等
|
|
46
|
+
};
|
|
47
|
+
let session;
|
|
48
|
+
if (sessionId) {
|
|
49
|
+
// 尝试恢复已有会话
|
|
50
|
+
try {
|
|
51
|
+
log.info(`Attempting to resume session: ${sessionId}`);
|
|
52
|
+
session = unstable_v2_resumeSession(sessionId, sessionOptions);
|
|
53
|
+
activeSessions.set(sessionId, session);
|
|
54
|
+
log.info(`Successfully resumed session: ${sessionId}`);
|
|
55
|
+
return { session, sessionId };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
log.warn(`Failed to resume session ${sessionId}, creating new one: ${err}`);
|
|
59
|
+
// 恢复失败,创建新会话
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 创建新会话
|
|
63
|
+
session = unstable_v2_createSession(sessionOptions);
|
|
64
|
+
// 新会话的 sessionId 需要从第一个消息中获取
|
|
65
|
+
// 暂时返回 undefined,稍后在 init 消息中获取
|
|
66
|
+
const tempId = `pending-${Date.now()}`;
|
|
67
|
+
activeSessions.set(tempId, session);
|
|
68
|
+
log.info(`Created new session (tempId: ${tempId})`);
|
|
69
|
+
return { session, sessionId: tempId };
|
|
28
70
|
}
|
|
29
71
|
export class ClaudeSDKAdapter {
|
|
30
72
|
toolId = 'claude-sdk';
|
|
31
73
|
/**
|
|
32
|
-
* 清理所有活跃的 SDK
|
|
74
|
+
* 清理所有活跃的 SDK 会话和流
|
|
33
75
|
*/
|
|
34
76
|
static destroy() {
|
|
35
|
-
for (const
|
|
77
|
+
for (const stream of activeStreams) {
|
|
36
78
|
try {
|
|
37
|
-
if (
|
|
38
|
-
|
|
79
|
+
if (stream && typeof stream.return === 'function') {
|
|
80
|
+
stream.return();
|
|
39
81
|
}
|
|
40
82
|
}
|
|
41
83
|
catch {
|
|
42
84
|
/* ignore */
|
|
43
85
|
}
|
|
44
86
|
}
|
|
45
|
-
|
|
87
|
+
activeStreams.clear();
|
|
88
|
+
for (const session of activeSessions.values()) {
|
|
89
|
+
try {
|
|
90
|
+
session.close();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* ignore */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
activeSessions.clear();
|
|
46
97
|
}
|
|
47
98
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
48
99
|
const abortController = new AbortController();
|
|
49
|
-
let
|
|
100
|
+
let streamClosed = false;
|
|
101
|
+
let actualSessionId;
|
|
50
102
|
const permissionMode = options?.skipPermissions
|
|
51
103
|
? 'bypassPermissions'
|
|
52
104
|
: options?.permissionMode === 'acceptEdits'
|
|
@@ -54,41 +106,47 @@ export class ClaudeSDKAdapter {
|
|
|
54
106
|
: options?.permissionMode === 'plan'
|
|
55
107
|
? 'plan'
|
|
56
108
|
: 'default';
|
|
57
|
-
const
|
|
109
|
+
const runSession = async () => {
|
|
58
110
|
try {
|
|
59
|
-
//
|
|
111
|
+
// 检查环境变量
|
|
60
112
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
61
113
|
const hasAuthToken = !!process.env.ANTHROPIC_AUTH_TOKEN;
|
|
62
|
-
|
|
63
|
-
if (!hasApiKey && !hasAuthToken && !hasBaseUrl) {
|
|
114
|
+
if (!hasApiKey && !hasAuthToken) {
|
|
64
115
|
log.warn('Claude SDK: No API credentials found in environment variables');
|
|
65
116
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
const q = query({
|
|
76
|
-
prompt,
|
|
77
|
-
options: opts,
|
|
78
|
-
});
|
|
79
|
-
// 将查询添加到活跃列表
|
|
80
|
-
activeQueries.add(q);
|
|
117
|
+
log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
|
|
118
|
+
// 获取或创建会话
|
|
119
|
+
const { session } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
|
|
120
|
+
// 发送用户消息
|
|
121
|
+
await session.send(prompt);
|
|
122
|
+
// 获取响应流
|
|
123
|
+
const stream = session.stream();
|
|
124
|
+
activeStreams.add(stream);
|
|
81
125
|
let accumulated = '';
|
|
82
126
|
let accumulatedThinking = '';
|
|
83
127
|
const toolStats = {};
|
|
84
128
|
try {
|
|
85
|
-
for await (const msg of
|
|
86
|
-
if (abortController.signal.aborted)
|
|
129
|
+
for await (const msg of stream) {
|
|
130
|
+
if (abortController.signal.aborted) {
|
|
131
|
+
log.info('Stream aborted by user');
|
|
87
132
|
break;
|
|
133
|
+
}
|
|
134
|
+
// 获取实际的 sessionId(从 init 消息中)
|
|
88
135
|
if (isSystemInit(msg)) {
|
|
89
|
-
|
|
136
|
+
const newSessionId = msg.session_id;
|
|
137
|
+
if (newSessionId && newSessionId !== actualSessionId) {
|
|
138
|
+
// 更新 sessionId 映射
|
|
139
|
+
if (actualSessionId && actualSessionId.startsWith('pending-')) {
|
|
140
|
+
activeSessions.delete(actualSessionId);
|
|
141
|
+
}
|
|
142
|
+
activeSessions.set(newSessionId, session);
|
|
143
|
+
actualSessionId = newSessionId;
|
|
144
|
+
log.info(`[V2] Got actual sessionId: ${newSessionId}`);
|
|
145
|
+
callbacks.onSessionId?.(newSessionId);
|
|
146
|
+
}
|
|
90
147
|
continue;
|
|
91
148
|
}
|
|
149
|
+
// 处理流式事件
|
|
92
150
|
if (isStreamEvent(msg)) {
|
|
93
151
|
const ev = msg.event;
|
|
94
152
|
if (ev?.type === 'content_block_delta' && ev.delta) {
|
|
@@ -103,6 +161,7 @@ export class ClaudeSDKAdapter {
|
|
|
103
161
|
}
|
|
104
162
|
continue;
|
|
105
163
|
}
|
|
164
|
+
// 处理助手消息(工具调用)
|
|
106
165
|
if (isAssistant(msg)) {
|
|
107
166
|
const content = msg.message?.content;
|
|
108
167
|
for (const block of content ?? []) {
|
|
@@ -113,16 +172,22 @@ export class ClaudeSDKAdapter {
|
|
|
113
172
|
}
|
|
114
173
|
continue;
|
|
115
174
|
}
|
|
175
|
+
// 处理结果消息
|
|
116
176
|
if (isResult(msg)) {
|
|
117
|
-
|
|
177
|
+
streamClosed = true;
|
|
118
178
|
const m = msg;
|
|
119
179
|
const success = m.subtype === 'success';
|
|
120
180
|
const errs = m.errors ?? [];
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
181
|
+
log.info(`[V2] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
|
|
182
|
+
// 检查会话错误
|
|
183
|
+
if (!success) {
|
|
184
|
+
const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
|
|
185
|
+
if (noConvErr) {
|
|
186
|
+
log.warn(`Session ${actualSessionId} not found, may need to create new one`);
|
|
187
|
+
callbacks.onSessionInvalid?.();
|
|
188
|
+
}
|
|
189
|
+
const errMsg = errs[0] || '未知错误';
|
|
190
|
+
callbacks.onError(errMsg);
|
|
126
191
|
return;
|
|
127
192
|
}
|
|
128
193
|
const resultText = m.result ?? '';
|
|
@@ -135,58 +200,55 @@ export class ClaudeSDKAdapter {
|
|
|
135
200
|
numTurns: m.num_turns ?? 0,
|
|
136
201
|
toolStats,
|
|
137
202
|
};
|
|
138
|
-
if (!result.accumulated && result.result)
|
|
203
|
+
if (!result.accumulated && result.result) {
|
|
139
204
|
result.accumulated = result.result;
|
|
205
|
+
}
|
|
140
206
|
if (!result.accumulated && !result.result && accumulated) {
|
|
141
|
-
log.debug(`Result event had no text but accumulated=${accumulated.length} chars, using accumulated`);
|
|
142
207
|
result.accumulated = accumulated;
|
|
143
208
|
result.result = accumulated;
|
|
144
209
|
}
|
|
145
|
-
if (!result.accumulated && !result.result) {
|
|
146
|
-
const errMsg = errs[0] ?? '未知错误';
|
|
147
|
-
log.warn(`SDK result empty: subtype=${m.subtype}, errors=${JSON.stringify(errs)}`);
|
|
148
|
-
callbacks.onError(errMsg);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
210
|
callbacks.onComplete(result);
|
|
152
211
|
return;
|
|
153
212
|
}
|
|
154
213
|
}
|
|
155
|
-
|
|
214
|
+
// 如果流正常结束但没有收到 result 消息
|
|
215
|
+
if (!streamClosed && accumulated) {
|
|
216
|
+
log.info('Stream ended without result message, using accumulated text');
|
|
156
217
|
callbacks.onComplete({
|
|
157
218
|
success: true,
|
|
158
219
|
result: accumulated,
|
|
159
220
|
accumulated,
|
|
160
221
|
cost: 0,
|
|
161
222
|
durationMs: 0,
|
|
162
|
-
numTurns:
|
|
223
|
+
numTurns: 1,
|
|
163
224
|
toolStats,
|
|
164
225
|
});
|
|
165
226
|
}
|
|
166
227
|
}
|
|
167
228
|
finally {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
activeQueries.delete(q);
|
|
229
|
+
// 从活跃列表中移除流
|
|
230
|
+
activeStreams.delete(stream);
|
|
171
231
|
}
|
|
172
232
|
}
|
|
173
233
|
catch (err) {
|
|
174
|
-
if (abortController.signal.aborted)
|
|
234
|
+
if (abortController.signal.aborted) {
|
|
235
|
+
log.info('Session run aborted');
|
|
175
236
|
return;
|
|
237
|
+
}
|
|
176
238
|
const errorObj = err;
|
|
177
239
|
const msg = errorObj.message || String(err);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (stack) {
|
|
182
|
-
log.error(`Error stack: ${stack}`);
|
|
240
|
+
log.error(`Claude SDK V2 error: ${msg}`);
|
|
241
|
+
if (errorObj.stack) {
|
|
242
|
+
log.error(`Error stack: ${errorObj.stack}`);
|
|
183
243
|
}
|
|
184
244
|
callbacks.onError(msg);
|
|
185
245
|
}
|
|
186
246
|
};
|
|
187
|
-
|
|
247
|
+
// 启动会话(不等待)
|
|
248
|
+
runSession();
|
|
188
249
|
return {
|
|
189
250
|
abort: () => {
|
|
251
|
+
log.info('Aborting session run');
|
|
190
252
|
abortController.abort();
|
|
191
253
|
},
|
|
192
254
|
};
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -112,14 +112,18 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
112
112
|
? config.codebuddyTimeoutMs
|
|
113
113
|
: config.claudeTimeoutMs;
|
|
114
114
|
const startRun = () => {
|
|
115
|
+
log.info(`[AITask] Starting: userId=${ctx.userId}, initialSessionId=${currentSessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
|
|
115
116
|
activeHandle = toolAdapter.run(prompt, currentSessionId, ctx.workDir, {
|
|
116
117
|
onSessionId: (id) => {
|
|
118
|
+
log.info(`[AITask] SessionId callback: old=${currentSessionId ?? 'none'}, new=${id}, aiCommand=${aiCommand}, userId=${ctx.userId}`);
|
|
117
119
|
currentSessionId = id;
|
|
118
120
|
// 使用 aiCommand 而不是 toolId,确保与查询时使用相同的 key
|
|
119
121
|
if (ctx.threadId)
|
|
120
122
|
sessionManager.setSessionIdForThread(ctx.userId, ctx.threadId, aiCommand, id);
|
|
121
123
|
else if (ctx.convId)
|
|
122
124
|
sessionManager.setSessionIdForConv(ctx.userId, ctx.convId, aiCommand, id);
|
|
125
|
+
else
|
|
126
|
+
log.info(`[AITask] No threadId or convId, sessionId not persisted to storage`);
|
|
123
127
|
},
|
|
124
128
|
onSessionInvalid: () => {
|
|
125
129
|
if (ctx.convId)
|