@wu529778790/open-im 1.11.1-beta.4 → 1.11.1-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.
package/README.md CHANGED
@@ -48,10 +48,10 @@ After `start`, the CLI prints the dashboard URL (default **`http://127.0.0.1:392
48
48
  | --- | --- |
49
49
  | `/help` | Help |
50
50
  | `/new` | New AI session |
51
- | `/sessions` | Session history |
52
- | `/resume <N>` | Resume by list number |
51
+ | `/sessions` | Session history with preview |
52
+ | `/resume [N]` | Resume session (no arg = most recent) |
53
53
  | `/status` | AI + session info |
54
- | `/cd` / `/pwd` | Working directory |
54
+ | `/cd` / `/pwd` | Switch work dir (auto-resumes that dir's session) |
55
55
  | `/allow` / `/y`, `/deny` / `/n` | Permission prompts |
56
56
 
57
57
  ## Session continuity
package/README.zh-CN.md CHANGED
@@ -48,10 +48,10 @@ npx @wu529778790/open-im start
48
48
  | --- | --- |
49
49
  | `/help` | 帮助 |
50
50
  | `/new` | 新 AI 会话 |
51
- | `/sessions` | 历史会话 |
52
- | `/resume <序号>` | 按列表序号恢复 |
51
+ | `/sessions` | 历史会话(含摘要预览) |
52
+ | `/resume [序号]` | 恢复会话(无参数恢复最近一条) |
53
53
  | `/status` | AI 与会话信息 |
54
- | `/cd` / `/pwd` | 工作目录 |
54
+ | `/cd` / `/pwd` | 切换工作目录(自动恢复该目录的历史会话) |
55
55
  | `/allow` / `/y`、`/deny` / `/n` | 权限确认 |
56
56
 
57
57
  ## 会话接力
@@ -3,6 +3,29 @@ 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
+ function formatRelativeTime(ts) {
7
+ const sec = Math.floor((Date.now() - ts) / 1000);
8
+ if (sec < 60)
9
+ return '刚刚';
10
+ const min = Math.floor(sec / 60);
11
+ if (min < 60)
12
+ return `${min}分钟前`;
13
+ const hr = Math.floor(min / 60);
14
+ if (hr < 24)
15
+ return `${hr}小时前`;
16
+ const day = Math.floor(hr / 24);
17
+ if (day === 1)
18
+ return '昨天';
19
+ if (day < 30)
20
+ return `${day}天前`;
21
+ return new Date(ts).toLocaleDateString('zh-CN');
22
+ }
23
+ function truncatePreview(msg, maxLen = 30) {
24
+ if (!msg)
25
+ return '新会话';
26
+ const firstLine = msg.split('\n')[0].trim();
27
+ return firstLine.length > maxLen ? firstLine.slice(0, maxLen) + '...' : firstLine;
28
+ }
6
29
  import { AsyncLocalStorage } from 'node:async_hooks';
7
30
  import { execFile } from 'node:child_process';
8
31
  import { readdirSync } from 'node:fs';
@@ -84,7 +107,7 @@ export class CommandHandler {
84
107
  '/help - 显示帮助',
85
108
  '/new - 开始新会话(AI 上下文重置)',
86
109
  '/sessions - 查看历史会话',
87
- '/resume <序号> - 恢复历史会话',
110
+ '/resume [序号] - 恢复历史会话(无参数恢复最近一条)',
88
111
  '/status - 显示状态',
89
112
  '/cd <路径> - 切换工作目录',
90
113
  '/pwd - 当前工作目录',
@@ -101,23 +124,44 @@ export class CommandHandler {
101
124
  }
102
125
  const lines = ['📋 会话列表:', ''];
103
126
  history.forEach((entry, i) => {
104
- lines.push(`${i + 1}. ${entry.convId} · ${entry.totalTurns}轮`);
127
+ const preview = truncatePreview(entry.firstMessage);
128
+ const time = entry.createdAt ? ` · ${formatRelativeTime(entry.createdAt)}` : '';
129
+ lines.push(`${i + 1}. ${preview} · ${entry.totalTurns}轮${time}`);
105
130
  });
106
131
  if (active) {
107
132
  const num = history.length + 1;
108
- lines.push(`▸ ${num}. ${active.convId} · ${active.totalTurns}轮(当前)`);
133
+ const preview = truncatePreview(active.firstMessage);
134
+ lines.push(`▸ ${num}. ${preview} · ${active.totalTurns}轮(当前)`);
109
135
  }
110
- lines.push('', '使用 /resume <序号> 恢复历史会话');
136
+ lines.push('', '使用 /resume <序号> 恢复,或 /resume 恢复最近一条');
111
137
  await this.replySender().sendTextReply(chatId, lines.join('\n'));
112
138
  return true;
113
139
  }
114
140
  async handleResume(chatId, userId, arg, _platform) {
141
+ const history = this.deps.sessionManager.listConvHistory(userId);
142
+ // /resume (no arg) — resume the most recent session
143
+ if (!arg) {
144
+ if (history.length === 0) {
145
+ await this.replySender().sendTextReply(chatId, '没有可恢复的历史会话。');
146
+ return true;
147
+ }
148
+ const entry = history[history.length - 1];
149
+ this.deps.requestQueue.cancelUser(userId);
150
+ const ok = this.deps.sessionManager.resumeConv(userId, entry.convId);
151
+ if (ok) {
152
+ const preview = truncatePreview(entry.firstMessage);
153
+ await this.replySender().sendTextReply(chatId, `✅ 已恢复最近会话: ${preview}(${entry.totalTurns}轮)\n继续发消息即可。`);
154
+ }
155
+ else {
156
+ await this.replySender().sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
157
+ }
158
+ return true;
159
+ }
115
160
  const index = parseInt(arg, 10);
116
161
  if (isNaN(index) || index < 1) {
117
- await this.replySender().sendTextReply(chatId, '用法: /resume <序号>\n\n使用 /sessions 查看会话列表。');
162
+ await this.replySender().sendTextReply(chatId, '用法: /resume [序号]\n\n不带序号则恢复最近一条会话。');
118
163
  return true;
119
164
  }
120
- const history = this.deps.sessionManager.listConvHistory(userId);
121
165
  if (index > history.length) {
122
166
  await this.replySender().sendTextReply(chatId, `序号 ${index} 无效,共 ${history.length} 个历史会话。`);
123
167
  return true;
@@ -126,7 +170,8 @@ export class CommandHandler {
126
170
  this.deps.requestQueue.cancelUser(userId);
127
171
  const ok = this.deps.sessionManager.resumeConv(userId, entry.convId);
128
172
  if (ok) {
129
- await this.replySender().sendTextReply(chatId, `✅ 已恢复会话 ${index} (${entry.convId}),共 ${entry.totalTurns}轮对话。\n继续发消息即可。`);
173
+ const preview = truncatePreview(entry.firstMessage);
174
+ await this.replySender().sendTextReply(chatId, `✅ 已恢复会话: ${preview}(${entry.totalTurns}轮)\n继续发消息即可。`);
130
175
  }
131
176
  else {
132
177
  await this.replySender().sendTextReply(chatId, '❌ 恢复会话失败,请重试。');
@@ -178,9 +223,11 @@ export class CommandHandler {
178
223
  }
179
224
  try {
180
225
  this.deps.requestQueue.cancelUser(userId);
181
- const resolved = await this.deps.sessionManager.setWorkDir(userId, dir);
182
- await this.replySender().sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(resolved)}\n\n` +
183
- `🔄 AI 会话已重置,下一条消息将使用全新上下文。`);
226
+ const result = await this.deps.sessionManager.setWorkDir(userId, dir);
227
+ const msg = result.resumed
228
+ ? `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n🔄 已恢复该目录的最近会话,继续之前的上下文。`
229
+ : `📁 工作目录已切换到: ${escapePathForMarkdown(result.path)}\n\n🆕 该目录暂无历史会话,已创建全新上下文。`;
230
+ await this.replySender().sendTextReply(chatId, msg);
184
231
  }
185
232
  catch (err) {
186
233
  await this.replySender().sendTextReply(chatId, err instanceof Error ? err.message : String(err));
@@ -3,6 +3,8 @@ interface ConvHistoryEntry {
3
3
  convId: string;
4
4
  totalTurns: number;
5
5
  createdAt: number;
6
+ workDir?: string;
7
+ firstMessage?: string;
6
8
  }
7
9
  export declare function resolveWorkDirInput(baseDir: string, targetDir: string): string;
8
10
  export declare class SessionManager {
@@ -24,17 +26,23 @@ export declare class SessionManager {
24
26
  getWorkDir(userId: string): string;
25
27
  hasUserSession(userId: string): boolean;
26
28
  getConvId(userId: string): string;
27
- setWorkDir(userId: string, workDir: string): Promise<string>;
29
+ setWorkDir(userId: string, workDir: string): Promise<{
30
+ path: string;
31
+ resumed: boolean;
32
+ }>;
28
33
  newSession(userId: string): boolean;
29
34
  clearActiveToolSession(userId: string, toolId: ToolId): boolean;
30
35
  listConvHistory(userId: string): ConvHistoryEntry[];
31
36
  getActiveConvInfo(userId: string): {
32
37
  convId: string;
33
38
  totalTurns: number;
39
+ firstMessage?: string;
34
40
  } | undefined;
35
41
  resumeConv(userId: string, convId: string): boolean;
36
42
  addTurns(userId: string, turns: number): number;
37
43
  addTurnsForThread(userId: string, threadId: string, turns: number): number;
44
+ /** Save the first message preview for the current conversation (only on first turn). */
45
+ setConvPreview(userId: string, preview: string): void;
38
46
  getModel(userId: string, threadId?: string): string | undefined;
39
47
  setModel(userId: string, model: string | undefined, threadId?: string): void;
40
48
  private resolveAndValidate;
@@ -108,9 +108,15 @@ export class SessionManager {
108
108
  const realPath = await this.resolveAndValidate(currentDir, workDir);
109
109
  const s = this.sessions.get(userId);
110
110
  let oldConvId;
111
+ let resumed = false;
111
112
  if (s) {
113
+ // Same directory — no-op
114
+ if (realPath === currentDir) {
115
+ return { path: realPath, resumed: false };
116
+ }
112
117
  oldConvId = s.activeConvId;
113
118
  this.persistActiveConvSessions(userId, s);
119
+ // Archive current conversation with its workDir
114
120
  if (oldConvId) {
115
121
  if (!s.convHistory)
116
122
  s.convHistory = [];
@@ -118,13 +124,38 @@ export class SessionManager {
118
124
  convId: oldConvId,
119
125
  totalTurns: s.totalTurns ?? 0,
120
126
  createdAt: Date.now(),
127
+ workDir: currentDir,
128
+ firstMessage: s.firstMessage,
121
129
  });
122
130
  if (s.convHistory.length > 10)
123
131
  s.convHistory = s.convHistory.slice(-10);
124
132
  }
133
+ // Look for a previous conversation in the target directory
134
+ if (!s.convHistory)
135
+ s.convHistory = [];
136
+ const matchIdx = s.convHistory.findIndex((e) => e.workDir === realPath);
137
+ if (matchIdx !== -1) {
138
+ const [entry] = s.convHistory.splice(matchIdx, 1);
139
+ s.activeConvId = entry.convId;
140
+ s.totalTurns = entry.totalTurns;
141
+ s.sessionIds = {};
142
+ for (const toolId of ['claude', 'codex', 'codebuddy', 'opencode']) {
143
+ const sessionId = this.convSessionMap.get(this.getConvSessionKey(userId, entry.convId, toolId));
144
+ if (sessionId) {
145
+ if (!s.sessionIds)
146
+ s.sessionIds = {};
147
+ s.sessionIds[toolId] = sessionId;
148
+ }
149
+ }
150
+ resumed = true;
151
+ log.info(`Resumed session for user ${userId}: convId=${entry.convId}, workDir=${realPath}, turns=${entry.totalTurns}`);
152
+ }
153
+ else {
154
+ s.sessionIds = {};
155
+ s.activeConvId = randomBytes(4).toString('hex');
156
+ s.firstMessage = undefined;
157
+ }
125
158
  s.workDir = realPath;
126
- s.sessionIds = {};
127
- s.activeConvId = randomBytes(4).toString('hex');
128
159
  }
129
160
  else {
130
161
  this.sessions.set(userId, {
@@ -133,8 +164,8 @@ export class SessionManager {
133
164
  });
134
165
  }
135
166
  this.flushSync();
136
- log.info(`WorkDir changed for user ${userId}: ${realPath}, oldConvId=${oldConvId}`);
137
- return realPath;
167
+ log.info(`WorkDir changed for user ${userId}: ${realPath}, oldConvId=${oldConvId}, resumed=${resumed}`);
168
+ return { path: realPath, resumed };
138
169
  }
139
170
  newSession(userId) {
140
171
  const s = this.sessions.get(userId);
@@ -150,6 +181,8 @@ export class SessionManager {
150
181
  convId: oldConvId,
151
182
  totalTurns: s.totalTurns ?? 0,
152
183
  createdAt: Date.now(),
184
+ workDir: s.workDir,
185
+ firstMessage: s.firstMessage,
153
186
  });
154
187
  // Keep last 10 entries
155
188
  if (s.convHistory.length > 10)
@@ -158,6 +191,7 @@ export class SessionManager {
158
191
  s.sessionIds = {};
159
192
  s.activeConvId = randomBytes(4).toString('hex');
160
193
  s.totalTurns = 0;
194
+ s.firstMessage = undefined;
161
195
  this.flushSync();
162
196
  log.info(`New session for user ${userId}: oldConvId=${oldConvId}, oldSessionIds=${JSON.stringify(oldSessionIds)}, newConvId=${s.activeConvId}, sessionIds={}`);
163
197
  return true;
@@ -188,7 +222,7 @@ export class SessionManager {
188
222
  const s = this.sessions.get(userId);
189
223
  if (!s?.activeConvId)
190
224
  return undefined;
191
- return { convId: s.activeConvId, totalTurns: s.totalTurns ?? 0 };
225
+ return { convId: s.activeConvId, totalTurns: s.totalTurns ?? 0, firstMessage: s.firstMessage };
192
226
  }
193
227
  resumeConv(userId, convId) {
194
228
  const s = this.sessions.get(userId);
@@ -207,6 +241,8 @@ export class SessionManager {
207
241
  convId: s.activeConvId,
208
242
  totalTurns: s.totalTurns ?? 0,
209
243
  createdAt: Date.now(),
244
+ workDir: s.workDir,
245
+ firstMessage: s.firstMessage,
210
246
  });
211
247
  }
212
248
  // Remove target from history
@@ -214,6 +250,7 @@ export class SessionManager {
214
250
  // Restore target as active
215
251
  s.activeConvId = entry.convId;
216
252
  s.totalTurns = entry.totalTurns;
253
+ s.firstMessage = entry.firstMessage;
217
254
  s.sessionIds = {};
218
255
  // Restore sessionIds from convSessionMap
219
256
  for (const toolId of ['claude', 'codex', 'codebuddy']) {
@@ -249,6 +286,14 @@ export class SessionManager {
249
286
  this.save();
250
287
  return t.totalTurns;
251
288
  }
289
+ /** Save the first message preview for the current conversation (only on first turn). */
290
+ setConvPreview(userId, preview) {
291
+ const s = this.sessions.get(userId);
292
+ if (!s || (s.totalTurns ?? 0) > 0)
293
+ return;
294
+ s.firstMessage = preview;
295
+ this.save();
296
+ }
252
297
  getModel(userId, threadId) {
253
298
  const s = this.sessions.get(userId);
254
299
  if (threadId) {
@@ -153,6 +153,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
153
153
  const aiCommand = resolvePlatformAiCommand(config, ctx.platform);
154
154
  const startRun = () => {
155
155
  log.info(`[AITask] Starting: userId=${ctx.userId}, initialSessionId=${currentSessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
156
+ sessionManager.setConvPreview(ctx.userId, prompt.slice(0, 80));
156
157
  emitStructuredEvent('AITask', 'ai.task.start', {
157
158
  platform: ctx.platform,
158
159
  taskKey: hashUserId(ctx.taskKey),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.1-beta.4",
3
+ "version": "1.11.1-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",