@wu529778790/open-im 1.10.2-beta.5 → 1.10.2-beta.7

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.
@@ -9,6 +9,10 @@
9
9
  * 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
10
10
  */
11
11
  import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-adapter.interface.js';
12
+ /**
13
+ * 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
14
+ */
15
+ export declare function configureClaudeSdkSessionIdle(ttlMinutes: number): void;
12
16
  export declare class ClaudeSDKAdapter implements ToolAdapter {
13
17
  readonly toolId = "claude-sdk";
14
18
  /**
@@ -53,16 +53,32 @@ const activeStreams = new Set();
53
53
  const sessionLastUsed = new Map();
54
54
  // 跟踪正在执行任务的 session ID,防止空闲清理误杀运行中的长任务
55
55
  const runningSessions = new Set();
56
- const SESSION_IDLE_TTL_MS = 30 * 60 * 1000; // 30 分钟未使用则清理
56
+ let sessionIdleTtlMs = 30 * 60 * 1000; // 默认 30 分钟未使用则清理
57
+ let sessionIdleCleanupDisabled = false;
57
58
  const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 每 5 分钟检查一次
58
59
  const MAX_ACTIVE_SESSIONS = 100;
59
60
  let sessionSeq = 0;
61
+ /**
62
+ * 由 initAdapters 根据配置调用。ttlMinutes≤0 时关闭空闲回收(仍受 MAX_ACTIVE_SESSIONS 限制)。
63
+ */
64
+ export function configureClaudeSdkSessionIdle(ttlMinutes) {
65
+ if (ttlMinutes <= 0) {
66
+ sessionIdleCleanupDisabled = true;
67
+ log.info('Claude SDK: idle session cleanup disabled (sessionIdleTtlMinutes=0)');
68
+ }
69
+ else {
70
+ sessionIdleCleanupDisabled = false;
71
+ sessionIdleTtlMs = ttlMinutes * 60 * 1000;
72
+ }
73
+ }
60
74
  const cleanupInterval = setInterval(() => {
75
+ if (sessionIdleCleanupDisabled)
76
+ return;
61
77
  const now = Date.now();
62
78
  for (const [id, lastUsed] of sessionLastUsed) {
63
79
  if (runningSessions.has(id))
64
80
  continue; // 跳过正在运行任务的 session
65
- if (now - lastUsed > SESSION_IDLE_TTL_MS) {
81
+ if (now - lastUsed > sessionIdleTtlMs) {
66
82
  const session = activeSessions.get(id);
67
83
  if (session) {
68
84
  try {
@@ -1,5 +1,5 @@
1
1
  import { getConfiguredAiCommands } from '../config.js';
2
- import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
2
+ import { ClaudeSDKAdapter, configureClaudeSdkSessionIdle } from './claude-sdk-adapter.js';
3
3
  import { CodexAdapter } from './codex-adapter.js';
4
4
  import { CodeBuddyAdapter } from './codebuddy-adapter.js';
5
5
  import { createLogger } from '../logger.js';
@@ -10,6 +10,7 @@ export function initAdapters(config) {
10
10
  for (const aiCommand of getConfiguredAiCommands(config)) {
11
11
  if (aiCommand === 'claude') {
12
12
  log.info('Claude Agent SDK adapter enabled');
13
+ configureClaudeSdkSessionIdle(config.claudeSessionIdleTtlMinutes);
13
14
  adapters.set('claude', new ClaudeSDKAdapter());
14
15
  continue;
15
16
  }
@@ -15,10 +15,14 @@ export interface CommandHandlerDeps {
15
15
  getRunningTasksSize: () => number;
16
16
  }
17
17
  export type ClaudeRequestHandler = (userId: string, chatId: string, prompt: string, workDir: string, convId?: string, threadCtx?: ThreadContext, replyToMessageId?: string) => Promise<void>;
18
+ export declare function normalizeSlashCommandForDispatch(text: string): string;
18
19
  export declare class CommandHandler {
19
20
  private deps;
20
21
  constructor(deps: CommandHandlerDeps);
21
- dispatch(text: string, chatId: string, userId: string, platform: Platform, _handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
22
+ private replySender;
23
+ dispatch(text: string, chatId: string, userId: string, platform: Platform, _handleClaudeRequest: ClaudeRequestHandler,
24
+ /** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
25
+ senderOverride?: MessageSender): Promise<boolean>;
22
26
  private handleHelp;
23
27
  private handleSessions;
24
28
  private handleResume;
@@ -3,41 +3,79 @@ import { escapePathForMarkdown } from '../shared/utils.js';
3
3
  import { TERMINAL_ONLY_COMMANDS } from '../constants.js';
4
4
  import { createLogger } from '../logger.js';
5
5
  const log = createLogger('Commands');
6
+ import { AsyncLocalStorage } from 'node:async_hooks';
6
7
  import { execFile } from 'node:child_process';
7
8
  import { readdirSync } from 'node:fs';
8
9
  import { dirname, join } from 'node:path';
10
+ /**
11
+ * Telegram 群聊等场景下命令常为 `/new@BotName`,需与 `/new` 等价。
12
+ * 仅去掉「第一个」命令词上的 `@suffix`,保留 `/resume 1` 等参数。
13
+ */
14
+ /** 并发 dispatch 时,用 AsyncLocalStorage 绑定「本条消息」的 sender(如 WorkBuddy 需 msgId)。 */
15
+ const commandReplySender = new AsyncLocalStorage();
16
+ function mergeMessageSender(override, base) {
17
+ return {
18
+ sendTextReply: (chatId, text, threadCtx) => override.sendTextReply(chatId, text, threadCtx),
19
+ sendDirectorySelection: override.sendDirectorySelection ?? base.sendDirectorySelection,
20
+ };
21
+ }
22
+ export function normalizeSlashCommandForDispatch(text) {
23
+ const trimmed = text.trim();
24
+ if (!trimmed.startsWith("/") || !trimmed.includes("@"))
25
+ return trimmed;
26
+ const firstSpace = trimmed.indexOf(" ");
27
+ const firstSegment = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
28
+ if (!firstSegment.includes("@"))
29
+ return trimmed;
30
+ const at = firstSegment.indexOf("@");
31
+ const baseCmd = firstSegment.slice(0, at);
32
+ if (firstSpace === -1)
33
+ return baseCmd;
34
+ return `${baseCmd}${trimmed.slice(firstSpace)}`;
35
+ }
9
36
  export class CommandHandler {
10
37
  deps;
11
38
  constructor(deps) {
12
39
  this.deps = deps;
13
40
  }
14
- async dispatch(text, chatId, userId, platform, _handleClaudeRequest) {
15
- const t = text.trim();
16
- if (platform === 'telegram' && t === '/start') {
17
- await this.deps.sender.sendTextReply(chatId, '欢迎使用 open-im AI CLI 桥接!\n\n发送消息与 AI 交互,输入 /help 查看帮助。');
18
- return true;
19
- }
20
- if (t === '/help')
21
- return this.handleHelp(chatId);
22
- if (t === '/new')
23
- return this.handleNew(chatId, userId);
24
- if (t === '/sessions' || t === '/resume')
25
- return this.handleSessions(chatId, userId, platform);
26
- if (t.startsWith('/resume '))
27
- return this.handleResume(chatId, userId, t.slice(8).trim(), platform);
28
- if (t === '/pwd')
29
- return this.handlePwd(chatId, userId);
30
- if (t === '/status')
31
- return this.handleStatus(chatId, userId, platform);
32
- if (t === '/cd' || t.startsWith('/cd ')) {
33
- return this.handleCd(chatId, userId, t.slice(3).trim(), platform);
34
- }
35
- const cmd = t.split(/\s+/)[0];
36
- if (TERMINAL_ONLY_COMMANDS.has(cmd)) {
37
- await this.deps.sender.sendTextReply(chatId, `${cmd} 命令仅在终端可用。`);
38
- return true;
41
+ replySender() {
42
+ return commandReplySender.getStore() ?? this.deps.sender;
43
+ }
44
+ async dispatch(text, chatId, userId, platform, _handleClaudeRequest,
45
+ /** 若提供,本条消息的斜杠命令回复走此 sender(须与 handleTextFlow 的 sendTextReply 一致,如带 msgId)。 */
46
+ senderOverride) {
47
+ const runBody = async () => {
48
+ const t = normalizeSlashCommandForDispatch(text);
49
+ if (platform === 'telegram' && t === '/start') {
50
+ await this.replySender().sendTextReply(chatId, '欢迎使用 open-im AI CLI 桥接!\n\n发送消息与 AI 交互,输入 /help 查看帮助。');
51
+ return true;
52
+ }
53
+ if (t === '/help')
54
+ return this.handleHelp(chatId);
55
+ if (t === '/new')
56
+ return this.handleNew(chatId, userId);
57
+ if (t === '/sessions' || t === '/resume')
58
+ return this.handleSessions(chatId, userId, platform);
59
+ if (t.startsWith('/resume '))
60
+ return this.handleResume(chatId, userId, t.slice(8).trim(), platform);
61
+ if (t === '/pwd')
62
+ return this.handlePwd(chatId, userId);
63
+ if (t === '/status')
64
+ return this.handleStatus(chatId, userId, platform);
65
+ if (t === '/cd' || t.startsWith('/cd ')) {
66
+ return this.handleCd(chatId, userId, t.slice(3).trim(), platform);
67
+ }
68
+ const cmd = t.split(/\s+/)[0];
69
+ if (TERMINAL_ONLY_COMMANDS.has(cmd)) {
70
+ await this.replySender().sendTextReply(chatId, `${cmd} 命令仅在终端可用。`);
71
+ return true;
72
+ }
73
+ return false;
74
+ };
75
+ if (senderOverride) {
76
+ return commandReplySender.run(mergeMessageSender(senderOverride, this.deps.sender), runBody);
39
77
  }
40
- return false;
78
+ return runBody();
41
79
  }
42
80
  async handleHelp(chatId) {
43
81
  const help = [
@@ -51,14 +89,14 @@ export class CommandHandler {
51
89
  '/cd <路径> - 切换工作目录',
52
90
  '/pwd - 当前工作目录',
53
91
  ].join('\n');
54
- await this.deps.sender.sendTextReply(chatId, help);
92
+ await this.replySender().sendTextReply(chatId, help);
55
93
  return true;
56
94
  }
57
95
  async handleSessions(chatId, userId, _platform) {
58
96
  const history = this.deps.sessionManager.listConvHistory(userId);
59
97
  const active = this.deps.sessionManager.getActiveConvInfo(userId);
60
98
  if (history.length === 0 && !active) {
61
- await this.deps.sender.sendTextReply(chatId, '📋 暂无会话记录。');
99
+ await this.replySender().sendTextReply(chatId, '📋 暂无会话记录。');
62
100
  return true;
63
101
  }
64
102
  const lines = ['📋 会话列表:', ''];
@@ -70,40 +108,40 @@ export class CommandHandler {
70
108
  lines.push(`▸ ${num}. ${active.convId} · ${active.totalTurns}轮(当前)`);
71
109
  }
72
110
  lines.push('', '使用 /resume <序号> 恢复历史会话');
73
- await this.deps.sender.sendTextReply(chatId, lines.join('\n'));
111
+ await this.replySender().sendTextReply(chatId, lines.join('\n'));
74
112
  return true;
75
113
  }
76
114
  async handleResume(chatId, userId, arg, _platform) {
77
115
  const index = parseInt(arg, 10);
78
116
  if (isNaN(index) || index < 1) {
79
- await this.deps.sender.sendTextReply(chatId, '用法: /resume <序号>\n\n使用 /sessions 查看会话列表。');
117
+ await this.replySender().sendTextReply(chatId, '用法: /resume <序号>\n\n使用 /sessions 查看会话列表。');
80
118
  return true;
81
119
  }
82
120
  const history = this.deps.sessionManager.listConvHistory(userId);
83
121
  if (index > history.length) {
84
- await this.deps.sender.sendTextReply(chatId, `序号 ${index} 无效,共 ${history.length} 个历史会话。`);
122
+ await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${history.length} 个历史会话。`);
85
123
  return true;
86
124
  }
87
125
  const entry = history[index - 1];
88
126
  const ok = this.deps.sessionManager.resumeConv(userId, entry.convId);
89
127
  if (ok) {
90
- await this.deps.sender.sendTextReply(chatId, `✅ 已恢复会话 ${index} (${entry.convId}),共 ${entry.totalTurns}轮对话。\n继续发消息即可。`);
128
+ await this.replySender().sendTextReply(chatId, `✅ 已恢复会话 ${index} (${entry.convId}),共 ${entry.totalTurns}轮对话。\n继续发消息即可。`);
91
129
  }
92
130
  else {
93
- await this.deps.sender.sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
131
+ await this.replySender().sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
94
132
  }
95
133
  return true;
96
134
  }
97
135
  async handleNew(chatId, userId) {
98
136
  const ok = this.deps.sessionManager.newSession(userId);
99
- await this.deps.sender.sendTextReply(chatId, ok
137
+ await this.replySender().sendTextReply(chatId, ok
100
138
  ? '✅ AI 会话已重置,下一条消息将使用全新上下文。'
101
139
  : '当前没有活动会话。');
102
140
  return true;
103
141
  }
104
142
  async handlePwd(chatId, userId) {
105
143
  const workDir = this.deps.sessionManager.getWorkDir(userId);
106
- await this.deps.sender.sendTextReply(chatId, `当前工作目录: ${escapePathForMarkdown(workDir)}`);
144
+ await this.replySender().sendTextReply(chatId, `当前工作目录: ${escapePathForMarkdown(workDir)}`);
107
145
  return true;
108
146
  }
109
147
  async handleStatus(chatId, userId, platform) {
@@ -120,28 +158,29 @@ export class CommandHandler {
120
158
  `工作目录: ${escapePathForMarkdown(workDir)}`,
121
159
  `会话: ${sessionId ?? '无'}`,
122
160
  ];
123
- await this.deps.sender.sendTextReply(chatId, lines.join('\n'));
161
+ await this.replySender().sendTextReply(chatId, lines.join('\n'));
124
162
  return true;
125
163
  }
126
164
  async handleCd(chatId, userId, dir, _platform) {
127
165
  // 如果 dir 为空,显示目录选择界面
128
166
  if (!dir) {
129
167
  const currentDir = this.deps.sessionManager.getWorkDir(userId);
130
- if (this.deps.sender.sendDirectorySelection) {
131
- await this.deps.sender.sendDirectorySelection(chatId, currentDir, userId);
168
+ const s = this.replySender();
169
+ if (s.sendDirectorySelection) {
170
+ await s.sendDirectorySelection(chatId, currentDir, userId);
132
171
  }
133
172
  else {
134
- await this.deps.sender.sendTextReply(chatId, `当前目录: ${escapePathForMarkdown(currentDir)}\n使用 /cd <路径> 切换`);
173
+ await s.sendTextReply(chatId, `当前目录: ${escapePathForMarkdown(currentDir)}\n使用 /cd <路径> 切换`);
135
174
  }
136
175
  return true;
137
176
  }
138
177
  try {
139
178
  const resolved = await this.deps.sessionManager.setWorkDir(userId, dir);
140
- await this.deps.sender.sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(resolved)}\n\n` +
179
+ await this.replySender().sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(resolved)}\n\n` +
141
180
  `🔄 AI 会话已重置,下一条消息将使用全新上下文。`);
142
181
  }
143
182
  catch (err) {
144
- await this.deps.sender.sendTextReply(chatId, err instanceof Error ? err.message : String(err));
183
+ await this.replySender().sendTextReply(chatId, err instanceof Error ? err.message : String(err));
145
184
  }
146
185
  return true;
147
186
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { normalizeSlashCommandForDispatch } from "./handler.js";
3
+ describe("normalizeSlashCommandForDispatch", () => {
4
+ it("strips @bot suffix from first segment", () => {
5
+ expect(normalizeSlashCommandForDispatch("/new@my_open_im_bot")).toBe("/new");
6
+ expect(normalizeSlashCommandForDispatch(" /help@BotName ")).toBe("/help");
7
+ });
8
+ it("preserves arguments after command", () => {
9
+ expect(normalizeSlashCommandForDispatch("/resume@bot 2")).toBe("/resume 2");
10
+ expect(normalizeSlashCommandForDispatch("/cd@bot /tmp/foo")).toBe("/cd /tmp/foo");
11
+ });
12
+ it("leaves non-command or no-at unchanged", () => {
13
+ expect(normalizeSlashCommandForDispatch("/new")).toBe("/new");
14
+ expect(normalizeSlashCommandForDispatch("hello @user")).toBe("hello @user");
15
+ expect(normalizeSlashCommandForDispatch("plain")).toBe("plain");
16
+ });
17
+ });
@@ -29,6 +29,10 @@ export interface Config {
29
29
  /** Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) */
30
30
  codexProxy?: string;
31
31
  claudeWorkDir: string;
32
+ /**
33
+ * Claude SDK 进程内会话空闲多久后回收(分钟)。0 表示关闭空闲回收(仍受适配器内 MAX_ACTIVE_SESSIONS 限制)。默认 30。
34
+ */
35
+ claudeSessionIdleTtlMinutes: number;
32
36
  claudeModel?: string;
33
37
  /** 是否跳过 AI 工具的权限确认(默认 true) */
34
38
  skipPermissions?: boolean;
@@ -137,6 +141,8 @@ export interface FileToolClaude {
137
141
  cliPath?: string;
138
142
  workDir?: string;
139
143
  skipPermissions?: boolean;
144
+ /** 空闲会话回收间隔(分钟),0 表示关闭 */
145
+ sessionIdleTtlMinutes?: number;
140
146
  /** HTTP/HTTPS 代理,用于访问 Claude API(如 http://127.0.0.1:7890) */
141
147
  proxy?: string;
142
148
  /** Claude API 配置(优先级:环境变量 > tools.claude.env > ~/.claude/settings.json) */
@@ -458,6 +458,7 @@ function createProbeConfig(values) {
458
458
  aiCommand: "claude",
459
459
  codexCliPath: "codex",
460
460
  claudeWorkDir: process.cwd(),
461
+ claudeSessionIdleTtlMinutes: 30,
461
462
  logDir: "",
462
463
  logLevel: "INFO",
463
464
  codebuddyCliPath: "codebuddy",
package/dist/config.js CHANGED
@@ -219,6 +219,19 @@ export function loadConfig() {
219
219
  const skipPermissions = process.env.OPEN_IM_SKIP_PERMISSIONS === 'false'
220
220
  ? false
221
221
  : (tc.skipPermissions ?? true);
222
+ const envIdleRaw = process.env.OPEN_IM_CLAUDE_SESSION_IDLE_TTL_MINUTES;
223
+ const envIdleParsed = envIdleRaw !== undefined && envIdleRaw !== '' ? Number.parseInt(envIdleRaw, 10) : NaN;
224
+ const fileIdle = tc.sessionIdleTtlMinutes;
225
+ let claudeSessionIdleTtlMinutes;
226
+ if (Number.isFinite(envIdleParsed)) {
227
+ claudeSessionIdleTtlMinutes = Math.max(0, envIdleParsed);
228
+ }
229
+ else if (typeof fileIdle === 'number' && Number.isFinite(fileIdle)) {
230
+ claudeSessionIdleTtlMinutes = Math.max(0, fileIdle);
231
+ }
232
+ else {
233
+ claudeSessionIdleTtlMinutes = 30;
234
+ }
222
235
  // 6. 校验 Claude API 凭证(SDK 模式需要)
223
236
  // 支持:官方 API Key、Auth Token、或自定义 API(第三方模型等,BASE_URL + token)
224
237
  if (aiCommand === 'claude') {
@@ -442,6 +455,7 @@ export function loadConfig() {
442
455
  claudeProxy,
443
456
  codexProxy,
444
457
  claudeWorkDir,
458
+ claudeSessionIdleTtlMinutes,
445
459
  claudeModel: process.env.ANTHROPIC_MODEL,
446
460
  skipPermissions,
447
461
  logDir,
package/dist/index.js CHANGED
@@ -104,11 +104,8 @@ async function sendLifecycleNotification(platform, message) {
104
104
  return;
105
105
  }
106
106
  log.info(`[${platform}] Sending lifecycle notification to chatId=${chatId}`);
107
- await mod.sendNotification(chatId, message).then(() => {
108
- log.info(`[${platform}] Lifecycle notification sent successfully`);
109
- }).catch((err) => {
110
- log.warn(`Failed to send ${platform} notification:`, err);
111
- });
107
+ await mod.sendNotification(chatId, message);
108
+ log.info(`[${platform}] Lifecycle notification sent successfully`);
112
109
  }
113
110
  function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, sessionManager) {
114
111
  let sessionDir;
@@ -7,7 +7,8 @@
7
7
  * 1. Access control check → deny with error message
8
8
  * 2. setActiveChatId(platform, chatId)
9
9
  * 3. setChatUser(chatId, userId, platform)
10
- * 4. Command dispatch via commandHandler.dispatch()
10
+ * 4. Command dispatch via commandHandler.dispatch(..., commandSender)
11
+ * (与 sendTextReply 同源,保证 WorkBuddy 等平台的斜杠命令带上 msgId)
11
12
  * 5. If not handled: empty text → return, otherwise enqueue AI request
12
13
  * 6. Handle queue-full notification (rejected / queued)
13
14
  *
@@ -7,7 +7,8 @@
7
7
  * 1. Access control check → deny with error message
8
8
  * 2. setActiveChatId(platform, chatId)
9
9
  * 3. setChatUser(chatId, userId, platform)
10
- * 4. Command dispatch via commandHandler.dispatch()
10
+ * 4. Command dispatch via commandHandler.dispatch(..., commandSender)
11
+ * (与 sendTextReply 同源,保证 WorkBuddy 等平台的斜杠命令带上 msgId)
11
12
  * 5. If not handled: empty text → return, otherwise enqueue AI request
12
13
  * 6. Handle queue-full notification (rejected / queued)
13
14
  *
@@ -61,7 +62,12 @@ export async function handleTextFlow(params) {
61
62
  replyToMessageId,
62
63
  });
63
64
  };
64
- const handled = await ctx.commandHandler.dispatch(text, chatId, userId, platform, dispatchHandler);
65
+ const commandSender = {
66
+ sendTextReply: async (chatId, text, _threadCtx) => {
67
+ await sendTextReply(chatId, text);
68
+ },
69
+ };
70
+ const handled = await ctx.commandHandler.dispatch(text, chatId, userId, platform, dispatchHandler, commandSender);
65
71
  if (handled) {
66
72
  return true;
67
73
  }
@@ -87,7 +87,9 @@ describe('handleTextFlow', () => {
87
87
  expect(setActiveChatId).toHaveBeenCalledWith('telegram', 'chat-1');
88
88
  expect(setChatUser).toHaveBeenCalledWith('chat-1', 'user-1', 'telegram');
89
89
  // Command dispatch was called with correct args
90
- expect(dispatch).toHaveBeenCalledWith('/help', 'chat-1', 'user-1', 'telegram', expect.any(Function));
90
+ expect(dispatch).toHaveBeenCalledWith('/help', 'chat-1', 'user-1', 'telegram', expect.any(Function), expect.objectContaining({
91
+ sendTextReply: expect.any(Function),
92
+ }));
91
93
  // Does NOT enqueue (sendTextReply not called for queue messages)
92
94
  expect(sendTextReply).not.toHaveBeenCalled();
93
95
  });
package/dist/qq/client.js CHANGED
@@ -254,11 +254,20 @@ async function connectWebSocket(config, handler) {
254
254
  settle(() => { }); // 清理 ready timeout
255
255
  clearTimers();
256
256
  ws = null;
257
- log.info(`QQ gateway closed: ${code} ${reason.toString()}`);
257
+ const reasonStr = reason.toString();
258
+ if (code === 4009) {
259
+ log.info(`QQ gateway session idle timeout (4009), reconnecting… (${reasonStr})`);
260
+ }
261
+ else {
262
+ log.info(`QQ gateway closed: ${code} ${reasonStr}`);
263
+ }
258
264
  if (stopped)
259
265
  return;
260
- if (code === 4004 || code === 4006 || code === 4007 || code === 4009) {
266
+ // 4009 仅为长连接会话过期,HTTP access_token 仍有效,勿清空 tokenState
267
+ if (code === 4004 || code === 4006 || code === 4007) {
261
268
  tokenState = null;
269
+ }
270
+ if (code === 4004 || code === 4006 || code === 4007 || code === 4009) {
262
271
  sessionId = null;
263
272
  seq = null;
264
273
  }
@@ -124,6 +124,7 @@ export async function sendTextReply(chatId, text) {
124
124
  }
125
125
  catch (err) {
126
126
  log.error("Failed to send text:", err);
127
+ throw err;
127
128
  }
128
129
  }
129
130
  export async function sendImageReply(chatId, imagePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.10.2-beta.5",
3
+ "version": "1.10.2-beta.7",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",