@wu529778790/open-im 1.8.1-beta.14 → 1.8.1-beta.16

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.
@@ -255,19 +255,28 @@ export class ClaudeSDKAdapter {
255
255
  }
256
256
  }
257
257
  // 如果流正常结束但没有收到 result 消息
258
- if (!streamClosed && accumulated) {
259
- log.info('Stream ended without result message, using accumulated text');
260
- runSettled = true;
261
- clearRunTimeout();
262
- callbacks.onComplete({
263
- success: true,
264
- result: accumulated,
265
- accumulated,
266
- cost: 0,
267
- durationMs: 0,
268
- numTurns: 1,
269
- toolStats,
270
- });
258
+ if (!streamClosed) {
259
+ if (accumulated) {
260
+ log.info('Stream ended without result message, using accumulated text');
261
+ runSettled = true;
262
+ clearRunTimeout();
263
+ callbacks.onComplete({
264
+ success: true,
265
+ result: accumulated,
266
+ accumulated,
267
+ cost: 0,
268
+ durationMs: 0,
269
+ numTurns: 1,
270
+ toolStats,
271
+ });
272
+ }
273
+ else {
274
+ // 流结束但无 result 也无 accumulated:必须触发回调,否则 Promise 永远挂起
275
+ log.warn('Stream ended with no result and no accumulated text, calling onError to prevent stuck state');
276
+ runSettled = true;
277
+ clearRunTimeout();
278
+ callbacks.onError('AI 响应异常结束(无输出),请重试');
279
+ }
271
280
  }
272
281
  }
273
282
  finally {
@@ -194,7 +194,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
194
194
  }
195
195
  }
196
196
  };
197
- return (content, toolNote) => {
197
+ const wrapper = (content, toolNote) => {
198
198
  if (content.startsWith("💭 **思考中...**")) {
199
199
  return;
200
200
  }
@@ -215,6 +215,17 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
215
215
  performUpdate(content, toolNote);
216
216
  }, Math.max(DEBOUNCE_MS, baseDelay));
217
217
  };
218
+ // flush 排队的 debounce 更新,防止 sendComplete 时仍有 streaming 更新在排队
219
+ wrapper.flush = async () => {
220
+ if (debounceTimer) {
221
+ clearTimeout(debounceTimer);
222
+ debounceTimer = null;
223
+ }
224
+ while (updateInProgress) {
225
+ await new Promise((resolve) => setTimeout(resolve, 50));
226
+ }
227
+ };
228
+ return wrapper;
218
229
  };
219
230
  const streamUpdateWrapper = createStreamUpdateWrapper();
220
231
  await runAITask({ config, sessionManager }, {
@@ -232,12 +243,30 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
232
243
  },
233
244
  sendComplete: async (content, note) => {
234
245
  throttle.reset();
235
- try {
236
- await sendFinalMessages(chatId, msgId, content, note, toolId);
237
- }
238
- catch (err) {
239
- log.error("Failed to send complete message:", err);
240
- await updateMessage(chatId, msgId, content, "done", note, toolId);
246
+ // 先 flush 排队的 streaming 更新,防止它覆盖后续的 done 消息
247
+ await streamUpdateWrapper.flush?.();
248
+ const maxAttempts = 3;
249
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
250
+ try {
251
+ await sendFinalMessages(chatId, msgId, content, note, toolId);
252
+ return;
253
+ }
254
+ catch (err) {
255
+ log.error(`Failed to send complete message (attempt ${attempt}/${maxAttempts}):`, err);
256
+ if (attempt < maxAttempts) {
257
+ await new Promise((r) => setTimeout(r, 2000 * attempt));
258
+ }
259
+ else {
260
+ // 最终失败:尝试发送纯文本作为最后手段
261
+ try {
262
+ await sendTextReply(chatId, `⚠️ 消息更新失败(网络异常),以下是 AI 回复:\n\n${content.slice(0, 4000)}`);
263
+ }
264
+ catch (fallbackErr) {
265
+ log.error("All send attempts failed:", fallbackErr);
266
+ throw err;
267
+ }
268
+ }
269
+ }
241
270
  }
242
271
  },
243
272
  sendError: async (error) => {
@@ -85,6 +85,10 @@ export async function updateMessage(chatId, messageId, content, status, note, to
85
85
  }
86
86
  else {
87
87
  log.error("Failed to update message:", err);
88
+ // 对 done/error 状态的更新失败必须 throw,否则消息永远卡在 streaming
89
+ if (status === "done" || status === "error") {
90
+ throw err;
91
+ }
88
92
  }
89
93
  }
90
94
  if (status === "done" || status === "error") {
@@ -21,6 +21,8 @@ import { buildMediaContext } from '../shared/media-context.js';
21
21
  import { buildErrorNote, buildProgressNote } from '../shared/message-note.js';
22
22
  const log = createLogger('WeWorkHandler');
23
23
  const WEWORK_MEDIA_TIMEOUT_MS = 60_000;
24
+ // Safety timeout: abort hung tasks before stream expires (5 min TTL → 4.5 min safety)
25
+ const WEWORK_TASK_SAFETY_TIMEOUT_MS = 4.5 * 60 * 1000;
24
26
  async function saveWeWorkUrlMedia(payload, fallbackExtension) {
25
27
  if (!payload.url) {
26
28
  throw new Error("Missing WeWork media URL");
@@ -199,6 +201,24 @@ export function setupWeWorkHandlers(config, sessionManager) {
199
201
  }
200
202
  const stopTyping = startTypingLoop(chatId);
201
203
  const taskKey = `${userId}:${msgId}`;
204
+ // Safety timeout: abort hung tasks before stream expires, unblocking the queue
205
+ let safetyTimer = setTimeout(() => {
206
+ safetyTimer = null;
207
+ const state = runningTasks.get(taskKey);
208
+ if (state) {
209
+ log.warn(`[SAFETY_TIMEOUT] Task ${taskKey} exceeded ${WEWORK_TASK_SAFETY_TIMEOUT_MS}ms, aborting`);
210
+ state.handle.abort();
211
+ runningTasks.delete(taskKey);
212
+ stopTyping();
213
+ sendTextReply(chatId, `AI 处理超时(${Math.round(WEWORK_TASK_SAFETY_TIMEOUT_MS / 1000)}s),已自动取消。请重试。`, reqId).catch(() => { });
214
+ }
215
+ }, WEWORK_TASK_SAFETY_TIMEOUT_MS);
216
+ const clearSafetyTimer = () => {
217
+ if (safetyTimer) {
218
+ clearTimeout(safetyTimer);
219
+ safetyTimer = null;
220
+ }
221
+ };
202
222
  await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'wework', taskKey }, prompt, toolAdapter, {
203
223
  throttleMs: WEWORK_THROTTLE_MS,
204
224
  streamUpdate: async (content, toolNote) => {
@@ -217,6 +237,7 @@ export function setupWeWorkHandlers(config, sessionManager) {
217
237
  await updateMessage(chatId, msgId, `Error: ${error}`, 'error', buildErrorNote(), toolId, reqId);
218
238
  },
219
239
  extraCleanup: () => {
240
+ clearSafetyTimer();
220
241
  stopTyping();
221
242
  runningTasks.delete(taskKey);
222
243
  },
@@ -187,6 +187,12 @@ export async function updateMessage(chatId, streamId, content, status, note, too
187
187
  return;
188
188
  if (Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS) {
189
189
  markExpired(state, streamId);
190
+ // Stream expired - fall back to text delivery for errors and final states
191
+ if (status === 'error' || status === 'done') {
192
+ const reqIdUsed = getReqId(reqId);
193
+ sendText(reqIdUsed, message);
194
+ log.info(`Stream expired, sent ${status} via text fallback: streamId=${streamId}`);
195
+ }
190
196
  return;
191
197
  }
192
198
  state.pendingUpdate = { message, status, reqId };
@@ -207,17 +213,24 @@ export async function sendFinalMessages(chatId, streamId, fullContent, note, too
207
213
  const title = getToolTitle(toolId, 'done');
208
214
  const parts = splitLongContent(contentToSend, MAX_WEWORK_MESSAGE_LENGTH);
209
215
  const finalMessage = formatWeWorkMessage(title, parts[0], 'done', parts.length > 1 ? `内容较长,已分段发送 (1/${parts.length})` : note);
210
- try {
211
- const state = streamStates.get(streamId);
212
- const shouldFallbackToText = !!state && (state.expired || Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS);
213
- if (!shouldFallbackToText && state && contentToSend.length > 0) {
214
- // 先发一条「输出中」带正文,再发 finish 的最终条,避免企微端一直停在「思考中」不刷新
216
+ const state = streamStates.get(streamId);
217
+ const shouldFallbackToText = !!state && (state.expired || Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS);
218
+ // 先发一条「输出中」带正文,再发 finish 的最终条,避免企微端一直停在「思考中」不刷新
219
+ // 独立 try-catch:即使此步失败,仍须发送 finish=true,否则企微永远卡在「正在思考」
220
+ if (!shouldFallbackToText && state && contentToSend.length > 0) {
221
+ try {
215
222
  await updateMessage(chatId, streamId, contentToSend, 'streaming', note, toolId, reqId);
216
223
  }
217
- if (state) {
218
- state.closed = true;
219
- state.pendingUpdate = undefined;
224
+ catch (err) {
225
+ log.warn('Pre-finish streaming update failed, will still send finish=true:', err);
220
226
  }
227
+ }
228
+ if (state) {
229
+ state.closed = true;
230
+ state.pendingUpdate = undefined;
231
+ }
232
+ // finish=true 是关键:必须保证发出,否则企微 UI 永远停留在「正在思考」
233
+ try {
221
234
  if (!shouldFallbackToText) {
222
235
  if (state) {
223
236
  const elapsed = Date.now() - state.lastSentAt;
@@ -232,21 +245,28 @@ export async function sendFinalMessages(chatId, streamId, fullContent, note, too
232
245
  sendText(getReqId(reqId), finalMessage);
233
246
  log.info(`Final stream expired, sent text fallback instead: streamId=${streamId}`);
234
247
  }
235
- streamStates.delete(streamId);
236
- for (let i = 1; i < parts.length; i++) {
237
- try {
238
- const partContent = `${parts[i]}\n\n_*(续 ${i + 1}/${parts.length})*_`;
239
- const partMessage = formatWeWorkMessage(title, partContent, 'done', i === parts.length - 1 ? note : undefined);
240
- sendText(getReqId(reqId), partMessage);
241
- log.info(`Final message part ${i + 1}/${parts.length} sent`);
242
- }
243
- catch (err) {
244
- log.error(`Failed to send part ${i + 1}:`, err);
245
- }
246
- }
247
248
  }
248
249
  catch (err) {
249
- log.error('Failed to send final messages:', err);
250
+ log.warn('Primary finish send failed, trying text fallback:', err);
251
+ try {
252
+ sendText(getReqId(reqId), finalMessage);
253
+ log.info(`Fallback text sent after primary finish failure, streamId=${streamId}`);
254
+ }
255
+ catch (fallbackErr) {
256
+ log.error('Both primary and fallback finish sends failed:', fallbackErr);
257
+ }
258
+ }
259
+ streamStates.delete(streamId);
260
+ for (let i = 1; i < parts.length; i++) {
261
+ try {
262
+ const partContent = `${parts[i]}\n\n_*(续 ${i + 1}/${parts.length})*_`;
263
+ const partMessage = formatWeWorkMessage(title, partContent, 'done', i === parts.length - 1 ? note : undefined);
264
+ sendText(getReqId(reqId), partMessage);
265
+ log.info(`Final message part ${i + 1}/${parts.length} sent`);
266
+ }
267
+ catch (err) {
268
+ log.error(`Failed to send part ${i + 1}:`, err);
269
+ }
250
270
  }
251
271
  }
252
272
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.8.1-beta.14",
3
+ "version": "1.8.1-beta.16",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",