@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 +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/commands/handler.js +57 -10
- package/dist/session/session-manager.d.ts +9 -1
- package/dist/session/session-manager.js +50 -5
- package/dist/shared/ai-task.js +1 -0
- package/package.json +1 -1
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
|
|
51
|
+
| `/sessions` | Session history with preview |
|
|
52
|
+
| `/resume [N]` | Resume session (no arg = most recent) |
|
|
53
53
|
| `/status` | AI + session info |
|
|
54
|
-
| `/cd` / `/pwd` |
|
|
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
|
## 会话接力
|
package/dist/commands/handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
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<
|
|
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) {
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -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),
|