@wu529778790/open-im 1.3.2-beta.1 → 1.3.2-beta.2

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.
@@ -110,7 +110,7 @@ export class CommandHandler {
110
110
  const version = await this.getAiVersion();
111
111
  const workDir = this.deps.sessionManager.getWorkDir(userId);
112
112
  const convId = this.deps.sessionManager.getConvId(userId);
113
- const sessionId = this.deps.sessionManager.getSessionIdForConv(userId, convId);
113
+ const sessionId = this.deps.sessionManager.getSessionIdForConv(userId, convId, this.deps.config.aiCommand);
114
114
  const lines = [
115
115
  '📊 状态:',
116
116
  '',
@@ -126,7 +126,7 @@ export function setupFeishuHandlers(config, sessionManager) {
126
126
  return;
127
127
  }
128
128
  log.info(`[handleAIRequest] Adapter found, getting session...`);
129
- const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId) : undefined;
129
+ const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId, config.aiCommand) : undefined;
130
130
  log.info(`[handleAIRequest] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
131
131
  const toolId = config.aiCommand;
132
132
  // 使用 CardKit 打字机效果(80ms 节流,约 12 次/秒,比 patch 5 QPS 更流畅)
package/dist/index.js CHANGED
@@ -134,9 +134,9 @@ export async function main() {
134
134
  log.info(`启用平台: ${config.enabledPlatforms.join(", ")}`);
135
135
  const sessionManager = new SessionManager(config.claudeWorkDir, config.allowedBaseDirs);
136
136
  // CLI 工具(Cursor/Codex)的 session 是进程级别的,服务重启后一定无效。
137
- // 启动时清除所有 sessionId,防止 --resume 到上次被中断的任务(如未完成的 git commit)。
137
+ // 启动时仅清除 CLI 工具自己的 sessionId,保留 Claude 的持久上下文。
138
138
  if (config.aiCommand !== 'claude') {
139
- sessionManager.clearAllSessionIds();
139
+ sessionManager.clearAllCliSessionIds();
140
140
  }
141
141
  let telegramHandle = null;
142
142
  let feishuHandle = null;
@@ -1,3 +1,4 @@
1
+ type ToolId = 'claude' | 'codex' | 'cursor';
1
2
  export declare class SessionManager {
2
3
  private sessions;
3
4
  private convSessionMap;
@@ -5,12 +6,12 @@ export declare class SessionManager {
5
6
  private allowedBaseDirs;
6
7
  private saveTimer;
7
8
  constructor(defaultWorkDir: string, allowedBaseDirs: string[]);
8
- getSessionIdForConv(userId: string, convId: string): string | undefined;
9
- setSessionIdForConv(userId: string, convId: string, sessionId: string): void;
9
+ getSessionIdForConv(userId: string, convId: string, toolId: ToolId): string | undefined;
10
+ setSessionIdForConv(userId: string, convId: string, toolId: ToolId, sessionId: string): void;
10
11
  /** 清除指定会话的 sessionId(用于 SDK 报 "No conversation found" 时) */
11
- clearSessionForConv(userId: string, convId: string): void;
12
- getSessionIdForThread(_userId: string, _threadId: string): string | undefined;
13
- setSessionIdForThread(userId: string, threadId: string, sessionId: string): void;
12
+ clearSessionForConv(userId: string, convId: string, toolId: ToolId): void;
13
+ getSessionIdForThread(_userId: string, _threadId: string, _toolId: ToolId): string | undefined;
14
+ setSessionIdForThread(userId: string, threadId: string, toolId: ToolId, sessionId: string): void;
14
15
  getWorkDir(userId: string): string;
15
16
  hasUserSession(userId: string): boolean;
16
17
  getConvId(userId: string): string;
@@ -20,8 +21,9 @@ export declare class SessionManager {
20
21
  * 适用于 CLI 工具(Cursor/Codex),其 session 是进程级别的,
21
22
  * 服务重启后旧的 session 一定无效,若不清除会导致 --resume 到中断任务。
22
23
  */
23
- clearAllSessionIds(): void;
24
+ clearAllCliSessionIds(): void;
24
25
  newSession(userId: string): boolean;
26
+ clearActiveToolSession(userId: string, toolId: ToolId): boolean;
25
27
  addTurns(userId: string, turns: number): number;
26
28
  addTurnsForThread(userId: string, threadId: string, turns: number): number;
27
29
  getModel(userId: string, threadId?: string): string | undefined;
@@ -32,4 +34,11 @@ export declare class SessionManager {
32
34
  private flushSync;
33
35
  destroy(): void;
34
36
  private doFlush;
37
+ private getConvSessionKey;
38
+ private getToolSessionId;
39
+ private setToolSessionId;
40
+ private clearToolSessionId;
41
+ private persistActiveConvSessions;
42
+ private clearConvSessionMappings;
35
43
  }
44
+ export {};
@@ -17,42 +17,44 @@ export class SessionManager {
17
17
  this.allowedBaseDirs = allowedBaseDirs;
18
18
  this.load();
19
19
  }
20
- getSessionIdForConv(userId, convId) {
20
+ getSessionIdForConv(userId, convId, toolId) {
21
21
  const s = this.sessions.get(userId);
22
22
  if (s?.activeConvId === convId)
23
- return s.sessionId;
24
- return this.convSessionMap.get(`${userId}:${convId}`);
23
+ return this.getToolSessionId(s, toolId);
24
+ return this.convSessionMap.get(this.getConvSessionKey(userId, convId, toolId));
25
25
  }
26
- setSessionIdForConv(userId, convId, sessionId) {
26
+ setSessionIdForConv(userId, convId, toolId, sessionId) {
27
27
  const s = this.sessions.get(userId);
28
28
  if (s?.activeConvId === convId) {
29
- s.sessionId = sessionId;
29
+ this.setToolSessionId(s, toolId, sessionId);
30
30
  this.save();
31
31
  }
32
32
  else {
33
- this.convSessionMap.set(`${userId}:${convId}`, sessionId);
33
+ this.convSessionMap.set(this.getConvSessionKey(userId, convId, toolId), sessionId);
34
34
  }
35
35
  }
36
36
  /** 清除指定会话的 sessionId(用于 SDK 报 "No conversation found" 时) */
37
- clearSessionForConv(userId, convId) {
37
+ clearSessionForConv(userId, convId, toolId) {
38
38
  const s = this.sessions.get(userId);
39
39
  if (s?.activeConvId === convId) {
40
- s.sessionId = undefined;
40
+ this.clearToolSessionId(s, toolId);
41
41
  this.save();
42
42
  }
43
- this.convSessionMap.delete(`${userId}:${convId}`);
44
- log.info(`Cleared session for user ${userId}, convId=${convId}`);
43
+ this.convSessionMap.delete(this.getConvSessionKey(userId, convId, toolId));
44
+ log.info(`Cleared ${toolId} session for user ${userId}, convId=${convId}`);
45
45
  }
46
- getSessionIdForThread(_userId, _threadId) {
46
+ getSessionIdForThread(_userId, _threadId, _toolId) {
47
47
  return undefined;
48
48
  }
49
- setSessionIdForThread(userId, threadId, sessionId) {
49
+ setSessionIdForThread(userId, threadId, toolId, sessionId) {
50
50
  const s = this.sessions.get(userId);
51
51
  if (s && !s.threads)
52
52
  s.threads = {};
53
53
  const t = s?.threads?.[threadId];
54
54
  if (t) {
55
- t.sessionId = sessionId;
55
+ if (!t.sessionIds)
56
+ t.sessionIds = {};
57
+ t.sessionIds[toolId] = sessionId;
56
58
  this.save();
57
59
  }
58
60
  }
@@ -82,15 +84,12 @@ export class SessionManager {
82
84
  const s = this.sessions.get(userId);
83
85
  if (s) {
84
86
  const oldConvId = s.activeConvId;
85
- if (s.activeConvId && s.sessionId) {
86
- this.convSessionMap.set(`${userId}:${s.activeConvId}`, s.sessionId);
87
- }
87
+ this.persistActiveConvSessions(userId, s);
88
88
  s.workDir = realPath;
89
- s.sessionId = undefined;
89
+ s.sessionIds = {};
90
90
  s.activeConvId = randomBytes(4).toString('hex');
91
- // 清除旧的 convSessionMap 中的映射
92
91
  if (oldConvId) {
93
- this.convSessionMap.delete(`${userId}:${oldConvId}`);
92
+ this.clearConvSessionMappings(userId, oldConvId);
94
93
  }
95
94
  }
96
95
  else {
@@ -108,48 +107,69 @@ export class SessionManager {
108
107
  * 适用于 CLI 工具(Cursor/Codex),其 session 是进程级别的,
109
108
  * 服务重启后旧的 session 一定无效,若不清除会导致 --resume 到中断任务。
110
109
  */
111
- clearAllSessionIds() {
110
+ clearAllCliSessionIds() {
112
111
  let changed = false;
113
112
  for (const [, s] of this.sessions) {
114
- if (s.sessionId !== undefined) {
115
- s.sessionId = undefined;
116
- changed = true;
113
+ for (const toolId of ['codex', 'cursor']) {
114
+ if (this.getToolSessionId(s, toolId) !== undefined) {
115
+ this.clearToolSessionId(s, toolId);
116
+ changed = true;
117
+ }
117
118
  }
118
119
  if (s.threads) {
119
120
  for (const t of Object.values(s.threads)) {
120
- if (t.sessionId !== undefined) {
121
- t.sessionId = undefined;
122
- changed = true;
121
+ for (const toolId of ['codex', 'cursor']) {
122
+ if (t.sessionIds?.[toolId] !== undefined) {
123
+ delete t.sessionIds[toolId];
124
+ changed = true;
125
+ }
123
126
  }
124
127
  }
125
128
  }
126
129
  }
130
+ for (const key of [...this.convSessionMap.keys()]) {
131
+ if (key.endsWith(':codex') || key.endsWith(':cursor')) {
132
+ this.convSessionMap.delete(key);
133
+ changed = true;
134
+ }
135
+ }
127
136
  if (changed) {
128
137
  this.flushSync();
129
- log.info('Cleared all CLI session IDs on startup');
138
+ log.info('Cleared CLI session IDs for codex/cursor on startup');
130
139
  }
131
140
  }
132
141
  newSession(userId) {
133
142
  const s = this.sessions.get(userId);
134
143
  if (s) {
135
- const oldSessionId = s.sessionId;
144
+ const oldSessionIds = { ...(s.sessionIds ?? {}) };
136
145
  const oldConvId = s.activeConvId;
137
- if (s.activeConvId && s.sessionId) {
138
- this.convSessionMap.set(`${userId}:${s.activeConvId}`, s.sessionId);
139
- }
140
- s.sessionId = undefined;
146
+ this.persistActiveConvSessions(userId, s);
147
+ s.sessionIds = {};
141
148
  s.activeConvId = randomBytes(4).toString('hex');
142
149
  s.totalTurns = 0;
143
- // 清除旧的 convSessionMap 中的映射,防止恢复旧的 sessionId
144
150
  if (oldConvId) {
145
- this.convSessionMap.delete(`${userId}:${oldConvId}`);
151
+ this.clearConvSessionMappings(userId, oldConvId);
146
152
  }
147
153
  this.flushSync();
148
- log.info(`New session for user ${userId}: oldConvId=${oldConvId}, oldSessionId=${oldSessionId}, newConvId=${s.activeConvId}, sessionId=undefined`);
154
+ log.info(`New session for user ${userId}: oldConvId=${oldConvId}, oldSessionIds=${JSON.stringify(oldSessionIds)}, newConvId=${s.activeConvId}, sessionIds={}`);
149
155
  return true;
150
156
  }
151
157
  return false;
152
158
  }
159
+ clearActiveToolSession(userId, toolId) {
160
+ const s = this.sessions.get(userId);
161
+ if (!s)
162
+ return false;
163
+ const activeConvId = s.activeConvId;
164
+ const hadSession = this.getToolSessionId(s, toolId) !== undefined;
165
+ this.clearToolSessionId(s, toolId);
166
+ if (activeConvId) {
167
+ this.convSessionMap.delete(this.getConvSessionKey(userId, activeConvId, toolId));
168
+ }
169
+ this.flushSync();
170
+ log.info(`Cleared active ${toolId} session for user ${userId}, convId=${activeConvId ?? 'none'}`);
171
+ return hadSession;
172
+ }
153
173
  addTurns(userId, turns) {
154
174
  const s = this.sessions.get(userId);
155
175
  if (!s)
@@ -243,6 +263,22 @@ export class SessionManager {
243
263
  if (v && typeof v.workDir === 'string') {
244
264
  if (!v.activeConvId)
245
265
  v.activeConvId = randomBytes(4).toString('hex');
266
+ if (!v.sessionIds)
267
+ v.sessionIds = {};
268
+ if ('sessionId' in v) {
269
+ log.warn(`Legacy shared sessionId found for user ${k}; clearing it to avoid cross-tool resume conflicts`);
270
+ }
271
+ delete v.sessionId;
272
+ if (v.threads) {
273
+ for (const thread of Object.values(v.threads)) {
274
+ if (!thread.sessionIds)
275
+ thread.sessionIds = {};
276
+ if ('sessionId' in thread) {
277
+ log.warn(`Legacy thread sessionId found for user ${k}; clearing it during session migration`);
278
+ }
279
+ delete thread.sessionId;
280
+ }
281
+ }
246
282
  this.sessions.set(k, v);
247
283
  }
248
284
  }
@@ -288,4 +324,34 @@ export class SessionManager {
288
324
  log.error('Failed to save sessions:', err);
289
325
  }
290
326
  }
327
+ getConvSessionKey(userId, convId, toolId) {
328
+ return `${userId}:${convId}:${toolId}`;
329
+ }
330
+ getToolSessionId(session, toolId) {
331
+ return session.sessionIds?.[toolId];
332
+ }
333
+ setToolSessionId(session, toolId, sessionId) {
334
+ if (!session.sessionIds)
335
+ session.sessionIds = {};
336
+ session.sessionIds[toolId] = sessionId;
337
+ }
338
+ clearToolSessionId(session, toolId) {
339
+ if (!session.sessionIds)
340
+ return;
341
+ delete session.sessionIds[toolId];
342
+ }
343
+ persistActiveConvSessions(userId, session) {
344
+ if (!session.activeConvId || !session.sessionIds)
345
+ return;
346
+ for (const [toolId, sessionId] of Object.entries(session.sessionIds)) {
347
+ if (sessionId) {
348
+ this.convSessionMap.set(this.getConvSessionKey(userId, session.activeConvId, toolId), sessionId);
349
+ }
350
+ }
351
+ }
352
+ clearConvSessionMappings(userId, convId) {
353
+ for (const toolId of ['claude', 'codex', 'cursor']) {
354
+ this.convSessionMap.delete(this.getConvSessionKey(userId, convId, toolId));
355
+ }
356
+ }
291
357
  }
@@ -103,13 +103,13 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
103
103
  const handle = toolAdapter.run(prompt, ctx.sessionId, ctx.workDir, {
104
104
  onSessionId: (id) => {
105
105
  if (ctx.threadId)
106
- sessionManager.setSessionIdForThread(ctx.userId, ctx.threadId, id);
106
+ sessionManager.setSessionIdForThread(ctx.userId, ctx.threadId, config.aiCommand, id);
107
107
  else if (ctx.convId)
108
- sessionManager.setSessionIdForConv(ctx.userId, ctx.convId, id);
108
+ sessionManager.setSessionIdForConv(ctx.userId, ctx.convId, config.aiCommand, id);
109
109
  },
110
110
  onSessionInvalid: () => {
111
111
  if (ctx.convId)
112
- sessionManager.clearSessionForConv(ctx.userId, ctx.convId);
112
+ sessionManager.clearSessionForConv(ctx.userId, ctx.convId, config.aiCommand);
113
113
  },
114
114
  onThinking: (t) => {
115
115
  if (!firstContentLogged) {
@@ -186,9 +186,14 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
186
186
  log.error(`Task error for user ${ctx.userId}: ${error}`);
187
187
  // CLI 工具(cursor/codex)出错时清除 sessionId,
188
188
  // 防止 resume 到中途被中断的会话(如未完成的 tool call 循环)
189
- // Claude SDK 不需要,其 session 是对话上下文,出错仍可继续
189
+ // Claude SDK 不需要,其 session 是对话上下文,出错仍可继续。
190
+ // 这里只清当前任务所属会话里的当前工具 session,避免误伤其它 AI 工具
191
+ // 或用户在 /new、/cd 后已经切出的新会话。
190
192
  if (config.aiCommand !== 'claude') {
191
- sessionManager.newSession(ctx.userId);
193
+ if (ctx.convId)
194
+ sessionManager.clearSessionForConv(ctx.userId, ctx.convId, config.aiCommand);
195
+ else
196
+ sessionManager.clearActiveToolSession(ctx.userId, config.aiCommand);
192
197
  log.info(`Session reset for user ${ctx.userId} due to ${config.aiCommand} task error`);
193
198
  }
194
199
  try {
@@ -91,7 +91,7 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
91
91
  return;
92
92
  }
93
93
  const sessionId = convId
94
- ? sessionManager.getSessionIdForConv(userId, convId)
94
+ ? sessionManager.getSessionIdForConv(userId, convId, config.aiCommand)
95
95
  : undefined;
96
96
  log.info(`Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? "new"}`);
97
97
  const toolId = config.aiCommand;
@@ -37,7 +37,7 @@ export function setupWeChatHandlers(config, sessionManager) {
37
37
  await sendTextReply(chatId, `未配置 AI 工具: ${config.aiCommand}`);
38
38
  return;
39
39
  }
40
- const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId) : undefined;
40
+ const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId, config.aiCommand) : undefined;
41
41
  log.info(`[handleAIRequest] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
42
42
  const toolId = config.aiCommand;
43
43
  let msgId;
@@ -38,7 +38,7 @@ export function setupWeWorkHandlers(config, sessionManager) {
38
38
  await sendTextReply(chatId, `未配置 AI 工具: ${config.aiCommand}`, reqId);
39
39
  return;
40
40
  }
41
- const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId) : undefined;
41
+ const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId, config.aiCommand) : undefined;
42
42
  log.info(`[handleAIRequest] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
43
43
  const toolId = config.aiCommand;
44
44
  let msgId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.3.2-beta.1",
3
+ "version": "1.3.2-beta.2",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",