@wu529778790/open-im 1.11.1-beta.5 → 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
|
|
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, '❌ 恢复会话失败,请重试。');
|
|
@@ -4,6 +4,7 @@ interface ConvHistoryEntry {
|
|
|
4
4
|
totalTurns: number;
|
|
5
5
|
createdAt: number;
|
|
6
6
|
workDir?: string;
|
|
7
|
+
firstMessage?: string;
|
|
7
8
|
}
|
|
8
9
|
export declare function resolveWorkDirInput(baseDir: string, targetDir: string): string;
|
|
9
10
|
export declare class SessionManager {
|
|
@@ -35,10 +36,13 @@ export declare class SessionManager {
|
|
|
35
36
|
getActiveConvInfo(userId: string): {
|
|
36
37
|
convId: string;
|
|
37
38
|
totalTurns: number;
|
|
39
|
+
firstMessage?: string;
|
|
38
40
|
} | undefined;
|
|
39
41
|
resumeConv(userId: string, convId: string): boolean;
|
|
40
42
|
addTurns(userId: string, turns: number): number;
|
|
41
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;
|
|
42
46
|
getModel(userId: string, threadId?: string): string | undefined;
|
|
43
47
|
setModel(userId: string, model: string | undefined, threadId?: string): void;
|
|
44
48
|
private resolveAndValidate;
|
|
@@ -125,6 +125,7 @@ export class SessionManager {
|
|
|
125
125
|
totalTurns: s.totalTurns ?? 0,
|
|
126
126
|
createdAt: Date.now(),
|
|
127
127
|
workDir: currentDir,
|
|
128
|
+
firstMessage: s.firstMessage,
|
|
128
129
|
});
|
|
129
130
|
if (s.convHistory.length > 10)
|
|
130
131
|
s.convHistory = s.convHistory.slice(-10);
|
|
@@ -152,6 +153,7 @@ export class SessionManager {
|
|
|
152
153
|
else {
|
|
153
154
|
s.sessionIds = {};
|
|
154
155
|
s.activeConvId = randomBytes(4).toString('hex');
|
|
156
|
+
s.firstMessage = undefined;
|
|
155
157
|
}
|
|
156
158
|
s.workDir = realPath;
|
|
157
159
|
}
|
|
@@ -180,6 +182,7 @@ export class SessionManager {
|
|
|
180
182
|
totalTurns: s.totalTurns ?? 0,
|
|
181
183
|
createdAt: Date.now(),
|
|
182
184
|
workDir: s.workDir,
|
|
185
|
+
firstMessage: s.firstMessage,
|
|
183
186
|
});
|
|
184
187
|
// Keep last 10 entries
|
|
185
188
|
if (s.convHistory.length > 10)
|
|
@@ -188,6 +191,7 @@ export class SessionManager {
|
|
|
188
191
|
s.sessionIds = {};
|
|
189
192
|
s.activeConvId = randomBytes(4).toString('hex');
|
|
190
193
|
s.totalTurns = 0;
|
|
194
|
+
s.firstMessage = undefined;
|
|
191
195
|
this.flushSync();
|
|
192
196
|
log.info(`New session for user ${userId}: oldConvId=${oldConvId}, oldSessionIds=${JSON.stringify(oldSessionIds)}, newConvId=${s.activeConvId}, sessionIds={}`);
|
|
193
197
|
return true;
|
|
@@ -218,7 +222,7 @@ export class SessionManager {
|
|
|
218
222
|
const s = this.sessions.get(userId);
|
|
219
223
|
if (!s?.activeConvId)
|
|
220
224
|
return undefined;
|
|
221
|
-
return { convId: s.activeConvId, totalTurns: s.totalTurns ?? 0 };
|
|
225
|
+
return { convId: s.activeConvId, totalTurns: s.totalTurns ?? 0, firstMessage: s.firstMessage };
|
|
222
226
|
}
|
|
223
227
|
resumeConv(userId, convId) {
|
|
224
228
|
const s = this.sessions.get(userId);
|
|
@@ -238,6 +242,7 @@ export class SessionManager {
|
|
|
238
242
|
totalTurns: s.totalTurns ?? 0,
|
|
239
243
|
createdAt: Date.now(),
|
|
240
244
|
workDir: s.workDir,
|
|
245
|
+
firstMessage: s.firstMessage,
|
|
241
246
|
});
|
|
242
247
|
}
|
|
243
248
|
// Remove target from history
|
|
@@ -245,6 +250,7 @@ export class SessionManager {
|
|
|
245
250
|
// Restore target as active
|
|
246
251
|
s.activeConvId = entry.convId;
|
|
247
252
|
s.totalTurns = entry.totalTurns;
|
|
253
|
+
s.firstMessage = entry.firstMessage;
|
|
248
254
|
s.sessionIds = {};
|
|
249
255
|
// Restore sessionIds from convSessionMap
|
|
250
256
|
for (const toolId of ['claude', 'codex', 'codebuddy']) {
|
|
@@ -280,6 +286,14 @@ export class SessionManager {
|
|
|
280
286
|
this.save();
|
|
281
287
|
return t.totalTurns;
|
|
282
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
|
+
}
|
|
283
297
|
getModel(userId, threadId) {
|
|
284
298
|
const s = this.sessions.get(userId);
|
|
285
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),
|