@wu529778790/open-im 1.9.4-beta.5 → 1.9.4-beta.6

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.
@@ -199,9 +199,9 @@ export function setupDingTalkHandlers(config, sessionManager) {
199
199
  async function enqueuePrompt(userId, chatId, prompt, dingtalkTarget) {
200
200
  const workDir = sessionManager.getWorkDir(userId);
201
201
  const convId = sessionManager.getConvId(userId);
202
- return requestQueue.enqueue(userId, convId, prompt, async (nextPrompt) => {
202
+ return requestQueue.enqueue(userId, convId, prompt, async (nextPrompt, signal) => {
203
203
  senderCtx.dingtalkTarget = dingtalkTarget;
204
- await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId });
204
+ await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, signal });
205
205
  });
206
206
  }
207
207
  async function handleEvent(data) {
@@ -459,8 +459,8 @@ export function setupFeishuHandlers(config, sessionManager) {
459
459
  });
460
460
  const work = sessionManager.getWorkDir(senderId);
461
461
  const convId = sessionManager.getConvId(senderId);
462
- const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p) => {
463
- await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir: work, convId, replyToMessageId: messageId });
462
+ const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p, signal) => {
463
+ await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir: work, convId, replyToMessageId: messageId, signal });
464
464
  });
465
465
  if (enqueueResult === 'rejected') {
466
466
  sendTextReply(chatId, 'Request queue is full. Please try again later.').catch((sendErr) => {
@@ -507,8 +507,8 @@ export function setupFeishuHandlers(config, sessionManager) {
507
507
  });
508
508
  const workDir = sessionManager.getWorkDir(senderId);
509
509
  const convId = sessionManager.getConvId(senderId);
510
- const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p) => {
511
- await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir, convId, replyToMessageId: messageId });
510
+ const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p, signal) => {
511
+ await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir, convId, replyToMessageId: messageId, signal });
512
512
  });
513
513
  if (enqueueResult === 'rejected') {
514
514
  sendTextReply(chatId, 'Request queue is full. Please try again later.').catch((sendErr) => {
@@ -92,6 +92,8 @@ export interface HandleAIRequestParams {
92
92
  workDir: string;
93
93
  convId?: string;
94
94
  replyToMessageId?: string;
95
+ /** AbortSignal from the request queue; fires on task timeout */
96
+ signal?: AbortSignal;
95
97
  }
96
98
  /**
97
99
  * Creates a platform-specific handleAIRequest function.
@@ -29,7 +29,7 @@ const log = createLogger('PlatformAI');
29
29
  export function createPlatformAIRequestHandler(deps) {
30
30
  const { platform, config, sessionManager, sender, throttleMs, runningTasks, minContentDeltaChars, taskKeyBuilder, onThinkingToText, extraInit, taskCallbacks, taskCallbacksFactory, } = deps;
31
31
  async function handleAIRequest(params) {
32
- const { userId, chatId, prompt, workDir, convId, replyToMessageId } = params;
32
+ const { userId, chatId, prompt, workDir, convId, replyToMessageId, signal } = params;
33
33
  log.info(`[${platform}] AI request: userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
34
34
  // 1. Resolve AI command and adapter
35
35
  const aiCommand = resolvePlatformAiCommand(config, platform);
@@ -156,6 +156,7 @@ export function createPlatformAIRequestHandler(deps) {
156
156
  convId,
157
157
  platform,
158
158
  taskKey,
159
+ signal,
159
160
  }, prompt, toolAdapter, mergedCallbacks);
160
161
  }
161
162
  catch (err) {
@@ -92,7 +92,7 @@ export async function handleTextFlow(params) {
92
92
  const { requestQueue } = ctx;
93
93
  const workDir = workDirOverride;
94
94
  const convId = convIdOverride;
95
- const enqueueResult = requestQueue.enqueue(userId, convId ?? '', text, async (prompt) => {
95
+ const enqueueResult = requestQueue.enqueue(userId, convId ?? '', text, async (prompt, signal) => {
96
96
  await handleAIRequest({
97
97
  userId,
98
98
  chatId,
@@ -100,6 +100,7 @@ export async function handleTextFlow(params) {
100
100
  workDir: workDir ?? '',
101
101
  convId,
102
102
  replyToMessageId,
103
+ signal,
103
104
  });
104
105
  });
105
106
  if (enqueueResult === 'rejected') {
@@ -167,7 +167,7 @@ export function setupQQHandlers(config, sessionManager) {
167
167
  });
168
168
  // Wrap factory handleAIRequest to match ClaudeRequestHandler signature
169
169
  // (used by commandHandler.dispatch and handleTextFlow)
170
- async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
170
+ async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId, signal) {
171
171
  await factoryHandleAIRequest({
172
172
  userId,
173
173
  chatId,
@@ -175,6 +175,7 @@ export function setupQQHandlers(config, sessionManager) {
175
175
  workDir,
176
176
  convId,
177
177
  replyToMessageId,
178
+ signal,
178
179
  });
179
180
  }
180
181
  function cleanupRecentEvents(now) {
@@ -264,8 +265,8 @@ export function setupQQHandlers(config, sessionManager) {
264
265
  setActiveChatId("qq", chatId);
265
266
  setChatUser(chatId, userId, "qq");
266
267
  // Enqueue attachment prompt
267
- const enqueueResult = requestQueue.enqueue(userId, convId ?? '', attachmentPrompt, async (prompt) => {
268
- await handleAIRequest(userId, chatId, prompt, workDir, convId, undefined, event.id);
268
+ const enqueueResult = requestQueue.enqueue(userId, convId ?? '', attachmentPrompt, async (prompt, signal) => {
269
+ await handleAIRequest(userId, chatId, prompt, workDir, convId, undefined, event.id, signal);
269
270
  });
270
271
  if (enqueueResult === "rejected") {
271
272
  await sendTextReply(chatId, "Request queue is full. Please try again later.");
@@ -1,7 +1,7 @@
1
1
  export type EnqueueResult = 'running' | 'queued' | 'rejected';
2
2
  export declare class RequestQueue {
3
3
  private queues;
4
- enqueue(userId: string, convId: string, prompt: string, execute: (prompt: string) => Promise<void>): EnqueueResult;
4
+ enqueue(userId: string, convId: string, prompt: string, execute: (prompt: string, signal: AbortSignal) => Promise<void>): EnqueueResult;
5
5
  /** 清除指定用户会话的所有排队任务(不中止正在运行的任务) */
6
6
  clear(userId: string, convId: string): number;
7
7
  private run;
@@ -35,12 +35,16 @@ export class RequestQueue {
35
35
  return cleared;
36
36
  }
37
37
  async run(key, prompt, execute) {
38
+ const controller = new AbortController();
38
39
  let timer;
39
40
  try {
40
41
  const timeoutPromise = new Promise((_, reject) => {
41
- timer = setTimeout(() => reject(new Error(`Task timed out after ${TASK_TIMEOUT_MS / 1000}s`)), TASK_TIMEOUT_MS);
42
+ timer = setTimeout(() => {
43
+ controller.abort();
44
+ reject(new Error(`Task timed out after ${TASK_TIMEOUT_MS / 1000}s`));
45
+ }, TASK_TIMEOUT_MS);
42
46
  });
43
- await Promise.race([execute(prompt), timeoutPromise]);
47
+ await Promise.race([execute(prompt, controller.signal), timeoutPromise]);
44
48
  }
45
49
  catch (err) {
46
50
  log.error(`Error executing task for ${key}:`, err);
@@ -8,7 +8,7 @@ describe('RequestQueue', () => {
8
8
  expect(result).toBe('running');
9
9
  // Allow microtask queue to settle
10
10
  await new Promise((r) => setTimeout(r, 10));
11
- expect(execute).toHaveBeenCalledWith('hello');
11
+ expect(execute).toHaveBeenCalledWith('hello', expect.any(AbortSignal));
12
12
  });
13
13
  it('returns "queued" when a task is already running', () => {
14
14
  const queue = new RequestQueue();
@@ -37,11 +37,11 @@ describe('RequestQueue', () => {
37
37
  queue.enqueue('user1', 'conv1', 'first', execute);
38
38
  queue.enqueue('user1', 'conv1', 'second', execute);
39
39
  expect(execute).toHaveBeenCalledTimes(1);
40
- expect(execute).toHaveBeenCalledWith('first');
40
+ expect(execute).toHaveBeenCalledWith('first', expect.any(AbortSignal));
41
41
  resolveFirst();
42
42
  await new Promise((r) => setTimeout(r, 20));
43
43
  expect(execute).toHaveBeenCalledTimes(2);
44
- expect(execute).toHaveBeenCalledWith('second');
44
+ expect(execute).toHaveBeenCalledWith('second', expect.any(AbortSignal));
45
45
  });
46
46
  it('isolates queues per user:convId', () => {
47
47
  const queue = new RequestQueue();
@@ -90,7 +90,25 @@ describe('RequestQueue', () => {
90
90
  queue.enqueue('user1', 'conv1', 'second', execute);
91
91
  await new Promise((r) => setTimeout(r, 20));
92
92
  expect(execute).toHaveBeenCalledTimes(2);
93
- expect(execute).toHaveBeenCalledWith('first');
94
- expect(execute).toHaveBeenCalledWith('second');
93
+ expect(execute).toHaveBeenCalledWith('first', expect.any(AbortSignal));
94
+ expect(execute).toHaveBeenCalledWith('second', expect.any(AbortSignal));
95
+ });
96
+ it('aborts the AbortSignal on timeout', async () => {
97
+ vi.useFakeTimers();
98
+ const queue = new RequestQueue();
99
+ let receivedSignal;
100
+ const execute = vi.fn().mockImplementation(async (_prompt, signal) => {
101
+ receivedSignal = signal;
102
+ // Never resolve — simulates a stuck task
103
+ await new Promise(() => { });
104
+ });
105
+ queue.enqueue('user1', 'conv1', 'hello', execute);
106
+ await vi.advanceTimersByTimeAsync(10);
107
+ expect(receivedSignal?.aborted).toBe(false);
108
+ // Advance past timeout
109
+ vi.advanceTimersByTime(10 * 60 * 1000 + 1);
110
+ await vi.advanceTimersByTimeAsync(100);
111
+ expect(receivedSignal?.aborted).toBe(true);
112
+ vi.useRealTimers();
95
113
  });
96
114
  });
@@ -17,6 +17,8 @@ export interface TaskContext {
17
17
  threadId?: string;
18
18
  platform: string;
19
19
  taskKey: string;
20
+ /** AbortSignal from the request queue; fires on task timeout to abort the running SDK session */
21
+ signal?: AbortSignal;
20
22
  }
21
23
  export interface TaskAdapter {
22
24
  streamUpdate(content: string, toolNote?: string): void;
@@ -255,5 +255,14 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
255
255
  return;
256
256
  }
257
257
  platformAdapter.onTaskReady(taskState);
258
+ // Wire queue abort signal to the running task's abort handle
259
+ if (ctx.signal) {
260
+ if (ctx.signal.aborted) {
261
+ taskState.handle.abort();
262
+ }
263
+ else {
264
+ ctx.signal.addEventListener('abort', () => taskState.handle.abort(), { once: true });
265
+ }
266
+ }
258
267
  });
259
268
  }
@@ -254,8 +254,8 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
254
254
  });
255
255
  const workDir = sessionManager.getWorkDir(userId);
256
256
  const convId = sessionManager.getConvId(userId);
257
- return requestQueue.enqueue(userId, convId, prompt, async (nextPrompt) => {
258
- await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId });
257
+ return requestQueue.enqueue(userId, convId, prompt, async (nextPrompt, signal) => {
258
+ await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, signal });
259
259
  });
260
260
  }
261
261
  bot.on("callback_query", async (ctx) => {
@@ -240,8 +240,8 @@ export function setupWeWorkHandlers(config, sessionManager) {
240
240
  async function enqueuePrompt(userId, chatId, prompt, reqId) {
241
241
  const workDir = sessionManager.getWorkDir(userId);
242
242
  const convId = sessionManager.getConvId(userId);
243
- const enqueueResult = ctx.requestQueue.enqueue(userId, convId, prompt, async (nextPrompt) => {
244
- await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, replyToMessageId: undefined });
243
+ const enqueueResult = ctx.requestQueue.enqueue(userId, convId, prompt, async (nextPrompt, signal) => {
244
+ await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, replyToMessageId: undefined, signal });
245
245
  });
246
246
  if (enqueueResult === 'rejected') {
247
247
  await sendTextReply(chatId, 'Request queue is full. Please try again later.', reqId);
@@ -83,7 +83,7 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
83
83
  extraInit,
84
84
  });
85
85
  // WorkBuddy-specific wrapper that captures msgId
86
- async function handleAIRequest(userId, chatId, msgId, prompt, workDir, convId) {
86
+ async function handleAIRequest(userId, chatId, msgId, prompt, workDir, convId, signal) {
87
87
  log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, msgId=${msgId}, promptLength=${prompt.length}`);
88
88
  // WorkBuddy uses incoming msgId as taskKey (no thinking message needed)
89
89
  const taskKey = `${userId}:${msgId}`;
@@ -104,7 +104,7 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
104
104
  log.info(`[handleAIRequest] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
105
105
  // Set up task tracking key mapping
106
106
  taskKeyByChatId.set(chatId, taskKey);
107
- await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'workbuddy', taskKey }, prompt, toolAdapter, {
107
+ await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'workbuddy', taskKey, signal }, prompt, toolAdapter, {
108
108
  throttleMs: WORKBUDDY_THROTTLE_MS,
109
109
  streamUpdate: async (content) => {
110
110
  log.debug(`Stream update (not sent): ${content.substring(0, 50)}...`);
@@ -172,9 +172,9 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
172
172
  if (!text) {
173
173
  return;
174
174
  }
175
- const enqueueResult = ctx.requestQueue.enqueue(userId, convId, text, async (nextPrompt) => {
175
+ const enqueueResult = ctx.requestQueue.enqueue(userId, convId, text, async (nextPrompt, signal) => {
176
176
  log.info(`Executing AI request for: ${nextPrompt}`);
177
- await handleAIRequest(userId, chatId, msgId, nextPrompt, workDir, convId);
177
+ await handleAIRequest(userId, chatId, msgId, nextPrompt, workDir, convId, signal);
178
178
  });
179
179
  if (enqueueResult === 'rejected') {
180
180
  await sendErrorReply(null, chatId, 'Request queue is full. Please try again later.', msgId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.9.4-beta.5",
3
+ "version": "1.9.4-beta.6",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",