@wu529778790/open-im 1.8.1-beta.12 → 1.8.1-beta.14
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/dist/adapters/claude-sdk-adapter.js +49 -31
- package/dist/dingtalk/client.js +11 -3
- package/dist/qq/client.js +105 -87
- package/dist/wechat/client.js +5 -0
- package/package.json +1 -1
|
@@ -37,39 +37,51 @@ function isResult(msg) {
|
|
|
37
37
|
* @param permissionMode 权限模式
|
|
38
38
|
* @returns SDKSession 对象和实际的 sessionId
|
|
39
39
|
*/
|
|
40
|
-
async function getOrCreateSession(sessionId,
|
|
41
|
-
model, permissionMode) {
|
|
40
|
+
async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
42
41
|
const resolvedModel = model?.trim() || 'claude-opus-4-5';
|
|
43
42
|
const sessionOptions = {
|
|
44
43
|
model: resolvedModel,
|
|
45
44
|
permissionMode,
|
|
46
|
-
// 可以添加其他选项,如 hooks, allowedTools 等
|
|
47
45
|
};
|
|
48
46
|
const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
|
|
49
|
-
log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl}`);
|
|
47
|
+
log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl} workDir=${workDir}`);
|
|
50
48
|
let session;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return { session, sessionId };
|
|
49
|
+
// SDK V2 的 SDKSessionOptions 没有 cwd 字段,
|
|
50
|
+
// 需要通过 process.chdir() 临时切换工作目录。
|
|
51
|
+
// 因为 createSession/resumeSession 是同步调用,且 JS 单线程,所以是安全的。
|
|
52
|
+
const originalCwd = process.cwd();
|
|
53
|
+
try {
|
|
54
|
+
if (workDir && workDir !== originalCwd) {
|
|
55
|
+
process.chdir(workDir);
|
|
59
56
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
if (sessionId) {
|
|
58
|
+
// 尝试恢复已有会话
|
|
59
|
+
try {
|
|
60
|
+
log.info(`Attempting to resume session: ${sessionId}`);
|
|
61
|
+
session = unstable_v2_resumeSession(sessionId, sessionOptions);
|
|
62
|
+
activeSessions.set(sessionId, session);
|
|
63
|
+
log.info(`Successfully resumed session: ${sessionId}`);
|
|
64
|
+
return { session, sessionId };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
log.warn(`Failed to resume session ${sessionId}, creating new one: ${err}`);
|
|
68
|
+
// 恢复失败,创建新会话
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 创建新会话
|
|
72
|
+
session = unstable_v2_createSession(sessionOptions);
|
|
73
|
+
// 新会话的 sessionId 需要从第一个消息中获取
|
|
74
|
+
// 暂时返回 undefined,稍后在 init 消息中获取
|
|
75
|
+
const tempId = `pending-${Date.now()}`;
|
|
76
|
+
activeSessions.set(tempId, session);
|
|
77
|
+
log.info(`Created new session (tempId: ${tempId})`);
|
|
78
|
+
return { session, sessionId: tempId };
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (workDir && workDir !== originalCwd) {
|
|
82
|
+
process.chdir(originalCwd);
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
|
-
// 创建新会话
|
|
66
|
-
session = unstable_v2_createSession(sessionOptions);
|
|
67
|
-
// 新会话的 sessionId 需要从第一个消息中获取
|
|
68
|
-
// 暂时返回 undefined,稍后在 init 消息中获取
|
|
69
|
-
const tempId = `pending-${Date.now()}`;
|
|
70
|
-
activeSessions.set(tempId, session);
|
|
71
|
-
log.info(`Created new session (tempId: ${tempId})`);
|
|
72
|
-
return { session, sessionId: tempId };
|
|
73
85
|
}
|
|
74
86
|
export class ClaudeSDKAdapter {
|
|
75
87
|
toolId = 'claude-sdk';
|
|
@@ -103,6 +115,7 @@ export class ClaudeSDKAdapter {
|
|
|
103
115
|
const abortController = new AbortController();
|
|
104
116
|
let streamClosed = false;
|
|
105
117
|
let actualSessionId;
|
|
118
|
+
let pendingTempId; // 记录临时 ID,用于 abort 时清理
|
|
106
119
|
let runSettled = false;
|
|
107
120
|
let timeoutId = null;
|
|
108
121
|
const timeoutMs = options?.timeoutMs ?? 600_000;
|
|
@@ -139,7 +152,10 @@ export class ClaudeSDKAdapter {
|
|
|
139
152
|
log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
|
|
140
153
|
log.info(`[V2] model param=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
|
|
141
154
|
// 获取或创建会话
|
|
142
|
-
const { session } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
|
|
155
|
+
const { session, sessionId: returnedId } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
|
|
156
|
+
if (returnedId.startsWith('pending-')) {
|
|
157
|
+
pendingTempId = returnedId;
|
|
158
|
+
}
|
|
143
159
|
// 发送用户消息
|
|
144
160
|
await session.send(prompt);
|
|
145
161
|
// 获取响应流
|
|
@@ -263,10 +279,11 @@ export class ClaudeSDKAdapter {
|
|
|
263
279
|
if (abortController.signal.aborted) {
|
|
264
280
|
log.info('Session run aborted');
|
|
265
281
|
clearRunTimeout();
|
|
266
|
-
// 清理 pending tempId
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
282
|
+
// 清理 pending tempId(abort 可能在 init 消息之前发生)
|
|
283
|
+
const idToClean = actualSessionId ?? pendingTempId;
|
|
284
|
+
if (idToClean?.startsWith('pending-')) {
|
|
285
|
+
activeSessions.delete(idToClean);
|
|
286
|
+
log.info(`Cleaned up pending session: ${idToClean}`);
|
|
270
287
|
}
|
|
271
288
|
return;
|
|
272
289
|
}
|
|
@@ -279,9 +296,10 @@ export class ClaudeSDKAdapter {
|
|
|
279
296
|
log.error(`Error stack: ${errorObj.stack}`);
|
|
280
297
|
}
|
|
281
298
|
// 清理 pending tempId(session 在获取真实 ID 前就失败了)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
299
|
+
const errIdToClean = actualSessionId ?? pendingTempId;
|
|
300
|
+
if (errIdToClean?.startsWith('pending-')) {
|
|
301
|
+
activeSessions.delete(errIdToClean);
|
|
302
|
+
log.info(`Cleaned up pending session after error: ${errIdToClean}`);
|
|
285
303
|
}
|
|
286
304
|
callbacks.onError(msg);
|
|
287
305
|
}
|
package/dist/dingtalk/client.js
CHANGED
|
@@ -7,7 +7,9 @@ const TEXT_MSG_KEY = 'sampleText';
|
|
|
7
7
|
const DINGTALK_STREAM_HOST = 'wss-open-connection.dingtalk.com';
|
|
8
8
|
let client = null;
|
|
9
9
|
let messageHandler = null;
|
|
10
|
+
// sessionWebhook 有过期时间(约 2 小时),需要记录时间戳
|
|
10
11
|
const sessionWebhookByChat = new Map();
|
|
12
|
+
const WEBHOOK_TTL_MS = 90 * 60 * 1000; // 90 分钟后视为过期
|
|
11
13
|
const unionIdByUserId = new Map();
|
|
12
14
|
let dingtalkWarnFilterInstalled = false;
|
|
13
15
|
export function shouldSuppressDingTalkSocketWarn(args) {
|
|
@@ -44,13 +46,19 @@ function getClient() {
|
|
|
44
46
|
export function registerSessionWebhook(chatId, sessionWebhook) {
|
|
45
47
|
if (!chatId || !sessionWebhook)
|
|
46
48
|
return;
|
|
47
|
-
sessionWebhookByChat.set(chatId, sessionWebhook);
|
|
49
|
+
sessionWebhookByChat.set(chatId, { webhook: sessionWebhook, registeredAt: Date.now() });
|
|
48
50
|
}
|
|
49
51
|
async function sendByWebhook(chatId, body) {
|
|
50
|
-
const
|
|
51
|
-
if (!
|
|
52
|
+
const entry = sessionWebhookByChat.get(chatId);
|
|
53
|
+
if (!entry) {
|
|
52
54
|
throw new Error(`DingTalk sessionWebhook unavailable for chat ${chatId}`);
|
|
53
55
|
}
|
|
56
|
+
// 检查 webhook 是否过期
|
|
57
|
+
if (Date.now() - entry.registeredAt > WEBHOOK_TTL_MS) {
|
|
58
|
+
sessionWebhookByChat.delete(chatId);
|
|
59
|
+
throw new Error(`DingTalk sessionWebhook expired for chat ${chatId}`);
|
|
60
|
+
}
|
|
61
|
+
const sessionWebhook = entry.webhook;
|
|
54
62
|
const accessToken = await getClient().getAccessToken();
|
|
55
63
|
const res = await fetch(sessionWebhook, {
|
|
56
64
|
method: 'POST',
|
package/dist/qq/client.js
CHANGED
|
@@ -17,6 +17,7 @@ let stopped = false;
|
|
|
17
17
|
let seq = null;
|
|
18
18
|
let sessionId = null;
|
|
19
19
|
let reconnectAttempt = 0;
|
|
20
|
+
let connecting = false; // 防止并发 connectWebSocket
|
|
20
21
|
let currentConfig = null;
|
|
21
22
|
let currentHandler = null;
|
|
22
23
|
let tokenState = null;
|
|
@@ -147,99 +148,116 @@ function startHeartbeat(intervalMs) {
|
|
|
147
148
|
}, intervalMs);
|
|
148
149
|
}
|
|
149
150
|
async function connectWebSocket(config, handler) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (typeof payload.s === "number")
|
|
170
|
-
seq = payload.s;
|
|
171
|
-
if (payload.op === 10) {
|
|
172
|
-
const heartbeatInterval = Number(payload.d?.heartbeat_interval ?? 30000);
|
|
173
|
-
startHeartbeat(heartbeatInterval);
|
|
174
|
-
socket.send(JSON.stringify({
|
|
175
|
-
op: sessionId ? 6 : 2,
|
|
176
|
-
d: sessionId
|
|
177
|
-
? {
|
|
178
|
-
token: `QQBot ${token}`,
|
|
179
|
-
session_id: sessionId,
|
|
180
|
-
seq,
|
|
181
|
-
}
|
|
182
|
-
: {
|
|
183
|
-
token: `QQBot ${token}`,
|
|
184
|
-
intents: INTENTS.GROUP_AND_C2C |
|
|
185
|
-
INTENTS.DIRECT_MESSAGE |
|
|
186
|
-
INTENTS.PUBLIC_GUILD_MESSAGES,
|
|
187
|
-
properties: {
|
|
188
|
-
os: process.platform,
|
|
189
|
-
browser: "open-im",
|
|
190
|
-
device: "open-im",
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
}));
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (payload.op === 0 && payload.t === "READY") {
|
|
197
|
-
sessionId = String(payload.d?.session_id ?? "");
|
|
198
|
-
settle(resolve);
|
|
151
|
+
// 防止并发连接
|
|
152
|
+
if (connecting) {
|
|
153
|
+
log.warn("QQ gateway connection already in progress");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
connecting = true;
|
|
157
|
+
try {
|
|
158
|
+
const gatewayUrl = await getGatewayUrl(config);
|
|
159
|
+
const token = await fetchAccessToken(config);
|
|
160
|
+
await new Promise((resolve, reject) => {
|
|
161
|
+
const socket = new WebSocket(gatewayUrl);
|
|
162
|
+
ws = socket;
|
|
163
|
+
let settled = false;
|
|
164
|
+
let readyTimeoutId = setTimeout(() => {
|
|
165
|
+
readyTimeoutId = null;
|
|
166
|
+
settle(() => reject(new Error("QQ gateway ready timeout")));
|
|
167
|
+
}, 15000);
|
|
168
|
+
const settle = (fn) => {
|
|
169
|
+
if (settled)
|
|
199
170
|
return;
|
|
171
|
+
settled = true;
|
|
172
|
+
if (readyTimeoutId) {
|
|
173
|
+
clearTimeout(readyTimeoutId);
|
|
174
|
+
readyTimeoutId = null;
|
|
200
175
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
176
|
+
fn();
|
|
177
|
+
};
|
|
178
|
+
socket.on("open", () => {
|
|
179
|
+
log.info("QQ gateway connected");
|
|
180
|
+
reconnectAttempt = 0;
|
|
181
|
+
});
|
|
182
|
+
socket.on("message", async (raw) => {
|
|
183
|
+
try {
|
|
184
|
+
const payload = JSON.parse(raw.toString());
|
|
185
|
+
if (typeof payload.s === "number")
|
|
186
|
+
seq = payload.s;
|
|
187
|
+
if (payload.op === 10) {
|
|
188
|
+
const heartbeatInterval = Number(payload.d?.heartbeat_interval ?? 30000);
|
|
189
|
+
startHeartbeat(heartbeatInterval);
|
|
190
|
+
socket.send(JSON.stringify({
|
|
191
|
+
op: sessionId ? 6 : 2,
|
|
192
|
+
d: sessionId
|
|
193
|
+
? {
|
|
194
|
+
token: `QQBot ${token}`,
|
|
195
|
+
session_id: sessionId,
|
|
196
|
+
seq,
|
|
197
|
+
}
|
|
198
|
+
: {
|
|
199
|
+
token: `QQBot ${token}`,
|
|
200
|
+
intents: INTENTS.GROUP_AND_C2C |
|
|
201
|
+
INTENTS.DIRECT_MESSAGE |
|
|
202
|
+
INTENTS.PUBLIC_GUILD_MESSAGES,
|
|
203
|
+
properties: {
|
|
204
|
+
os: process.platform,
|
|
205
|
+
browser: "open-im",
|
|
206
|
+
device: "open-im",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (payload.op === 0 && payload.t === "READY") {
|
|
213
|
+
sessionId = String(payload.d?.session_id ?? "");
|
|
214
|
+
settle(resolve);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (payload.op === 0 && payload.t === "RESUMED") {
|
|
218
|
+
settle(resolve);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const event = normalizeInboundEvent(payload);
|
|
222
|
+
if (event && (event.content || (event.attachments?.length ?? 0) > 0)) {
|
|
223
|
+
await handler(event);
|
|
224
|
+
}
|
|
204
225
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
await handler(event);
|
|
226
|
+
catch (error) {
|
|
227
|
+
log.error("Failed to handle QQ gateway payload:", error);
|
|
208
228
|
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
log.error("
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
tokenState = null;
|
|
226
|
-
sessionId = null;
|
|
227
|
-
seq = null;
|
|
228
|
-
}
|
|
229
|
-
const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
230
|
-
reconnectAttempt += 1;
|
|
231
|
-
reconnectTimer = setTimeout(() => {
|
|
232
|
-
if (currentConfig && currentHandler) {
|
|
233
|
-
connectWebSocket(currentConfig, currentHandler).catch((err) => {
|
|
234
|
-
log.error("QQ reconnect failed:", err);
|
|
235
|
-
});
|
|
229
|
+
});
|
|
230
|
+
socket.on("error", (error) => {
|
|
231
|
+
log.error("QQ gateway error:", error);
|
|
232
|
+
settle(() => reject(error));
|
|
233
|
+
});
|
|
234
|
+
socket.on("close", (code, reason) => {
|
|
235
|
+
settle(() => { }); // 清理 ready timeout
|
|
236
|
+
clearTimers();
|
|
237
|
+
ws = null;
|
|
238
|
+
log.info(`QQ gateway closed: ${code} ${reason.toString()}`);
|
|
239
|
+
if (stopped)
|
|
240
|
+
return;
|
|
241
|
+
if (code === 4004 || code === 4006 || code === 4007 || code === 4009) {
|
|
242
|
+
tokenState = null;
|
|
243
|
+
sessionId = null;
|
|
244
|
+
seq = null;
|
|
236
245
|
}
|
|
237
|
-
|
|
246
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
247
|
+
reconnectAttempt += 1;
|
|
248
|
+
reconnectTimer = setTimeout(() => {
|
|
249
|
+
if (currentConfig && currentHandler) {
|
|
250
|
+
connectWebSocket(currentConfig, currentHandler).catch((err) => {
|
|
251
|
+
log.error("QQ reconnect failed:", err);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}, delay);
|
|
255
|
+
});
|
|
238
256
|
});
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
connecting = false;
|
|
260
|
+
}
|
|
243
261
|
}
|
|
244
262
|
export function getQQBot() {
|
|
245
263
|
if (!client || !currentConfig) {
|
package/dist/wechat/client.js
CHANGED
|
@@ -19,6 +19,7 @@ let currentToken = null;
|
|
|
19
19
|
let tokenStoragePath = null;
|
|
20
20
|
let lastServerResponseTime = 0; // 上次收到服务端消息的时间
|
|
21
21
|
let wsConfigRef = null; // 保存配置供心跳重连使用
|
|
22
|
+
let isStopping = false; // 防止 stop 后重连定时器继续触发
|
|
22
23
|
// Event handlers
|
|
23
24
|
let messageHandler = null;
|
|
24
25
|
let stateChangeHandler = null;
|
|
@@ -73,6 +74,7 @@ export async function initWeChat(config, eventHandler, onStateChange) {
|
|
|
73
74
|
}
|
|
74
75
|
messageHandler = eventHandler;
|
|
75
76
|
stateChangeHandler = onStateChange ?? null;
|
|
77
|
+
isStopping = false;
|
|
76
78
|
// Set up token storage path
|
|
77
79
|
const baseDir = config.logDir ?? join(process.env.HOME ?? '', '.open-im');
|
|
78
80
|
tokenStoragePath = join(baseDir, 'data');
|
|
@@ -242,6 +244,8 @@ function stopHeartbeat() {
|
|
|
242
244
|
* 超过 maxAttempts 后自动重置计数器继续重试,避免永久断连
|
|
243
245
|
*/
|
|
244
246
|
function scheduleReconnect(config) {
|
|
247
|
+
if (isStopping)
|
|
248
|
+
return;
|
|
245
249
|
const maxAttempts = config.maxReconnectAttempts ?? 10;
|
|
246
250
|
if (reconnectTimer) {
|
|
247
251
|
return;
|
|
@@ -312,6 +316,7 @@ function saveToken() {
|
|
|
312
316
|
* Stop WeChat client
|
|
313
317
|
*/
|
|
314
318
|
export function stopWeChat() {
|
|
319
|
+
isStopping = true;
|
|
315
320
|
stopHeartbeat();
|
|
316
321
|
if (reconnectTimer) {
|
|
317
322
|
clearTimeout(reconnectTimer);
|