@wu529778790/open-im 1.8.1-beta.12 → 1.8.1-beta.13

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.
@@ -103,6 +103,7 @@ export class ClaudeSDKAdapter {
103
103
  const abortController = new AbortController();
104
104
  let streamClosed = false;
105
105
  let actualSessionId;
106
+ let pendingTempId; // 记录临时 ID,用于 abort 时清理
106
107
  let runSettled = false;
107
108
  let timeoutId = null;
108
109
  const timeoutMs = options?.timeoutMs ?? 600_000;
@@ -139,7 +140,10 @@ export class ClaudeSDKAdapter {
139
140
  log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
140
141
  log.info(`[V2] model param=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
141
142
  // 获取或创建会话
142
- const { session } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
143
+ const { session, sessionId: returnedId } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
144
+ if (returnedId.startsWith('pending-')) {
145
+ pendingTempId = returnedId;
146
+ }
143
147
  // 发送用户消息
144
148
  await session.send(prompt);
145
149
  // 获取响应流
@@ -263,10 +267,11 @@ export class ClaudeSDKAdapter {
263
267
  if (abortController.signal.aborted) {
264
268
  log.info('Session run aborted');
265
269
  clearRunTimeout();
266
- // 清理 pending tempId
267
- if (actualSessionId?.startsWith('pending-')) {
268
- activeSessions.delete(actualSessionId);
269
- log.info(`Cleaned up pending session: ${actualSessionId}`);
270
+ // 清理 pending tempId(abort 可能在 init 消息之前发生)
271
+ const idToClean = actualSessionId ?? pendingTempId;
272
+ if (idToClean?.startsWith('pending-')) {
273
+ activeSessions.delete(idToClean);
274
+ log.info(`Cleaned up pending session: ${idToClean}`);
270
275
  }
271
276
  return;
272
277
  }
@@ -279,9 +284,10 @@ export class ClaudeSDKAdapter {
279
284
  log.error(`Error stack: ${errorObj.stack}`);
280
285
  }
281
286
  // 清理 pending tempId(session 在获取真实 ID 前就失败了)
282
- if (actualSessionId?.startsWith('pending-')) {
283
- activeSessions.delete(actualSessionId);
284
- log.info(`Cleaned up pending session after error: ${actualSessionId}`);
287
+ const errIdToClean = actualSessionId ?? pendingTempId;
288
+ if (errIdToClean?.startsWith('pending-')) {
289
+ activeSessions.delete(errIdToClean);
290
+ log.info(`Cleaned up pending session after error: ${errIdToClean}`);
285
291
  }
286
292
  callbacks.onError(msg);
287
293
  }
@@ -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 sessionWebhook = sessionWebhookByChat.get(chatId);
51
- if (!sessionWebhook) {
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
- const gatewayUrl = await getGatewayUrl(config);
151
- const token = await fetchAccessToken(config);
152
- await new Promise((resolve, reject) => {
153
- const socket = new WebSocket(gatewayUrl);
154
- ws = socket;
155
- let settled = false;
156
- const settle = (fn) => {
157
- if (settled)
158
- return;
159
- settled = true;
160
- fn();
161
- };
162
- socket.on("open", () => {
163
- log.info("QQ gateway connected");
164
- reconnectAttempt = 0;
165
- });
166
- socket.on("message", async (raw) => {
167
- try {
168
- const payload = JSON.parse(raw.toString());
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
- if (payload.op === 0 && payload.t === "RESUMED") {
202
- settle(resolve);
203
- return;
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
- const event = normalizeInboundEvent(payload);
206
- if (event && (event.content || (event.attachments?.length ?? 0) > 0)) {
207
- await handler(event);
226
+ catch (error) {
227
+ log.error("Failed to handle QQ gateway payload:", error);
208
228
  }
209
- }
210
- catch (error) {
211
- log.error("Failed to handle QQ gateway payload:", error);
212
- }
213
- });
214
- socket.on("error", (error) => {
215
- log.error("QQ gateway error:", error);
216
- settle(() => reject(error));
217
- });
218
- socket.on("close", (code, reason) => {
219
- clearTimers();
220
- ws = null;
221
- log.info(`QQ gateway closed: ${code} ${reason.toString()}`);
222
- if (stopped)
223
- return;
224
- if (code === 4004 || code === 4006 || code === 4007 || code === 4009) {
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
- }, delay);
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
- setTimeout(() => {
240
- settle(() => reject(new Error("QQ gateway ready timeout")));
241
- }, 15000);
242
- });
257
+ }
258
+ finally {
259
+ connecting = false;
260
+ }
243
261
  }
244
262
  export function getQQBot() {
245
263
  if (!client || !currentConfig) {
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.8.1-beta.12",
3
+ "version": "1.8.1-beta.13",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",