-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathsessions.js
More file actions
294 lines (265 loc) · 9.92 KB
/
Copy pathsessions.js
File metadata and controls
294 lines (265 loc) · 9.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// SQLite 会话持久化(bun:sqlite,零外部依赖)
// 支持多后端:backend 字段区分 claude / codex
import { Database } from "bun:sqlite";
import { join, isAbsolute } from "path";
const DB_PATH = process.env.SESSIONS_DB
? (isAbsolute(process.env.SESSIONS_DB) ? process.env.SESSIONS_DB : join(import.meta.dir, process.env.SESSIONS_DB))
: join(import.meta.dir, "sessions.db");
const db = new Database(DB_PATH);
db.exec("PRAGMA journal_mode = WAL");
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
chat_id INTEGER PRIMARY KEY,
session_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
last_active INTEGER NOT NULL,
display_name TEXT DEFAULT '',
backend TEXT DEFAULT 'claude',
ownership TEXT DEFAULT 'owned'
)
`);
// 迁移:旧表没有 backend 列时自动加
try {
db.exec("ALTER TABLE sessions ADD COLUMN backend TEXT DEFAULT 'claude'");
} catch {
// 列已存在,忽略
}
try {
db.exec("ALTER TABLE sessions ADD COLUMN ownership TEXT DEFAULT 'owned'");
} catch {
// 列已存在,忽略
}
// 会话历史表(保留所有历史会话,不被 /new 或 upsert 覆盖)
db.exec(`
CREATE TABLE IF NOT EXISTS session_history (
session_id TEXT PRIMARY KEY,
chat_id INTEGER NOT NULL,
created_at INTEGER NOT NULL,
last_active INTEGER NOT NULL,
display_name TEXT DEFAULT '',
backend TEXT DEFAULT 'claude',
ownership TEXT DEFAULT 'owned'
)
`);
try {
db.exec("ALTER TABLE session_history ADD COLUMN ownership TEXT DEFAULT 'owned'");
} catch {
// 列已存在,忽略
}
// 后端偏好表(每个 chat 独立选后端)
db.exec(`
CREATE TABLE IF NOT EXISTS chat_backend (
chat_id INTEGER PRIMARY KEY,
backend TEXT NOT NULL DEFAULT 'claude'
)
`);
// Prepared statements — sessions
const stmtGet = db.prepare("SELECT session_id, last_active, backend, ownership FROM sessions WHERE chat_id = ?");
const stmtUpsert = db.prepare(`
INSERT INTO sessions (chat_id, session_id, created_at, last_active, display_name, backend, ownership)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(chat_id) DO UPDATE SET
session_id = excluded.session_id,
last_active = excluded.last_active,
display_name = excluded.display_name,
backend = excluded.backend,
ownership = excluded.ownership
`);
const stmtDelete = db.prepare("DELETE FROM sessions WHERE chat_id = ?");
const stmtRecentAll = db.prepare(`
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM (
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM sessions
UNION ALL
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM session_history
) ORDER BY last_active DESC LIMIT ?
`);
const stmtRecentByChat = db.prepare(`
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM (
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM sessions
UNION ALL
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM session_history
) WHERE chat_id = ?
ORDER BY last_active DESC LIMIT ?
`);
const stmtRecentByChatAndBackend = db.prepare(`
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM (
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM sessions
UNION ALL
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM session_history
) WHERE chat_id = ? AND backend = ?
ORDER BY last_active DESC LIMIT ?
`);
const stmtRecentByChatBackendAndOwnership = db.prepare(`
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM (
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM sessions
UNION ALL
SELECT chat_id, session_id, created_at, last_active, display_name, backend, ownership FROM session_history
) WHERE chat_id = ? AND backend = ? AND ownership = ?
ORDER BY last_active DESC LIMIT ?
`);
const stmtCleanup = db.prepare("DELETE FROM sessions WHERE last_active < ?");
const stmtCleanupHistory = db.prepare("DELETE FROM session_history WHERE last_active < ?");
const stmtTouch = db.prepare("UPDATE sessions SET last_active = ? WHERE chat_id = ?");
// History statements
const stmtArchive = db.prepare(`
INSERT OR REPLACE INTO session_history (session_id, chat_id, created_at, last_active, display_name, backend, ownership)
SELECT session_id, chat_id, created_at, last_active, display_name, backend, ownership FROM sessions WHERE chat_id = ?
`);
const stmtGetHistory = db.prepare(
"SELECT session_id, chat_id, created_at, last_active, display_name, backend, ownership FROM session_history WHERE session_id = ?"
);
const stmtDeleteFromHistory = db.prepare("DELETE FROM session_history WHERE session_id = ?");
const stmtHasSessionForChat = db.prepare(`
SELECT 1 AS ok FROM (
SELECT chat_id, session_id, backend, ownership FROM sessions
UNION ALL
SELECT chat_id, session_id, backend, ownership FROM session_history
)
WHERE chat_id = ?
AND session_id = ?
AND (? IS NULL OR backend = ?)
AND (? IS NULL OR ownership = ?)
LIMIT 1
`);
// Prepared statements — chat_backend
const stmtGetBackendPref = db.prepare("SELECT backend FROM chat_backend WHERE chat_id = ?");
const stmtSetBackendPref = db.prepare(`
INSERT INTO chat_backend (chat_id, backend) VALUES (?, ?)
ON CONFLICT(chat_id) DO UPDATE SET backend = excluded.backend
`);
// 模型偏好表(每个 chat 独立选模型,跨重启持久化)
db.exec(`
CREATE TABLE IF NOT EXISTS chat_model (
chat_id INTEGER PRIMARY KEY,
model TEXT NOT NULL
)
`);
const stmtGetModelPref = db.prepare("SELECT model FROM chat_model WHERE chat_id = ?");
const stmtSetModelPref = db.prepare(`
INSERT INTO chat_model (chat_id, model) VALUES (?, ?)
ON CONFLICT(chat_id) DO UPDATE SET model = excluded.model
`);
const stmtDeleteModelPref = db.prepare("DELETE FROM chat_model WHERE chat_id = ?");
// Effort 偏好表(每个 chat 独立选 effort,跨重启持久化)
db.exec(`
CREATE TABLE IF NOT EXISTS chat_effort (
chat_id INTEGER PRIMARY KEY,
effort TEXT NOT NULL
)
`);
const stmtGetEffortPref = db.prepare("SELECT effort FROM chat_effort WHERE chat_id = ?");
const stmtSetEffortPref = db.prepare(`
INSERT INTO chat_effort (chat_id, effort) VALUES (?, ?)
ON CONFLICT(chat_id) DO UPDATE SET effort = excluded.effort
`);
const stmtDeleteEffortPref = db.prepare("DELETE FROM chat_effort WHERE chat_id = ?");
export function getSession(chatId) {
const row = stmtGet.get(chatId);
if (!row) return null;
// Touch last_active
stmtTouch.run(Date.now(), chatId);
return {
session_id: row.session_id,
backend: row.backend || "claude",
ownership: row.ownership || "owned",
};
}
// 无 touch 副作用的只读版(getSession 会刷新 last_active;turn 收尾的写回防护不该改活跃时间)
export function peekSession(chatId) {
const row = stmtGet.get(chatId);
if (!row) return null;
return {
session_id: row.session_id,
backend: row.backend || "claude",
ownership: row.ownership || "owned",
};
}
// chatId → 最近一次 deleteSession 的时间(内存即可:bridge 重启时在途 turn 的收尾回调一并消失)
const sessionResetAt = new Map();
export function getSessionResetAt(chatId) {
return sessionResetAt.get(chatId) || 0;
}
export function setSession(
chatId,
sessionId,
displayName = "",
backend = "claude",
ownership = "owned",
) {
// 归档旧会话(如果有)
stmtArchive.run(chatId);
// 从历史中移除(避免恢复后重复出现)
stmtDeleteFromHistory.run(sessionId);
const now = Date.now();
stmtUpsert.run(chatId, sessionId, now, now, displayName, backend, ownership);
}
export function deleteSession(chatId) {
// 归档到历史再删除
stmtArchive.run(chatId);
stmtDelete.run(chatId);
sessionResetAt.set(chatId, Date.now());
}
export function getHistorySession(sessionId) {
return stmtGetHistory.get(sessionId) || null;
}
export function recentSessions(limit = 8, options = {}) {
const { chatId = null, backend = null, ownership = null } = options;
if (chatId != null && backend && ownership) {
return stmtRecentByChatBackendAndOwnership.all(chatId, backend, ownership, limit);
}
if (chatId != null && backend) {
return stmtRecentByChatAndBackend.all(chatId, backend, limit);
}
if (chatId != null) {
return stmtRecentByChat.all(chatId, limit);
}
return stmtRecentAll.all(limit);
}
export function cleanupExpired() {
// 当前会话默认长期保留,直到用户显式 /new 或 /resume 切换。
// 历史表仍然定期清理,避免只读预览列表无限增长。
const historyCutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
const result = stmtCleanupHistory.run(historyCutoff);
return result.changes;
}
export function sessionBelongsToChat(chatId, sessionId, backend = null, ownership = null) {
return Boolean(
stmtHasSessionForChat.get(
chatId,
sessionId,
backend,
backend,
ownership,
ownership,
),
);
}
export function getChatBackend(chatId) {
const row = stmtGetBackendPref.get(chatId);
return row?.backend || null;
}
export function setChatBackend(chatId, backend) {
stmtSetBackendPref.run(chatId, backend);
}
export function getChatModel(chatId) {
const row = stmtGetModelPref.get(chatId);
return row?.model || null;
}
export function setChatModel(chatId, model) {
stmtSetModelPref.run(chatId, model);
}
export function deleteChatModel(chatId) {
stmtDeleteModelPref.run(chatId);
}
export function getChatEffort(chatId) {
const row = stmtGetEffortPref.get(chatId);
return row?.effort || null;
}
export function setChatEffort(chatId, effort) {
stmtSetEffortPref.run(chatId, effort);
}
export function deleteChatEffort(chatId) {
stmtDeleteEffortPref.run(chatId);
}
// 每 30 分钟自动清理过期会话
setInterval(cleanupExpired, 30 * 60 * 1000);