@huyooo/ai-chat-storage 0.1.0
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/dist/chunk-BRWTH4LQ.js +642 -0
- package/dist/chunk-GCKDCC3O.js +678 -0
- package/dist/index.d.ts +1378 -0
- package/dist/index.js +66 -0
- package/dist/postgres-PBPM2KDK.js +6 -0
- package/dist/sqlite-AJRNISP2.js +6 -0
- package/package.json +36 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
// src/adapters/postgres/index.ts
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
|
|
4
|
+
// src/adapters/postgres/base.ts
|
|
5
|
+
async function initSchema(sql) {
|
|
6
|
+
try {
|
|
7
|
+
await sql`CREATE EXTENSION IF NOT EXISTS vector`;
|
|
8
|
+
} catch {
|
|
9
|
+
console.warn("pgvector \u6269\u5C55\u4E0D\u53EF\u7528\uFF0C\u5411\u91CF\u641C\u7D22\u529F\u80FD\u5C06\u964D\u7EA7");
|
|
10
|
+
}
|
|
11
|
+
await sql`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
app_id TEXT,
|
|
15
|
+
user_id TEXT,
|
|
16
|
+
title TEXT NOT NULL,
|
|
17
|
+
model TEXT NOT NULL,
|
|
18
|
+
mode TEXT NOT NULL,
|
|
19
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
20
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
21
|
+
)
|
|
22
|
+
`;
|
|
23
|
+
await sql`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
session_id TEXT NOT NULL,
|
|
27
|
+
app_id TEXT,
|
|
28
|
+
user_id TEXT,
|
|
29
|
+
role TEXT NOT NULL,
|
|
30
|
+
content TEXT NOT NULL,
|
|
31
|
+
thinking TEXT,
|
|
32
|
+
tool_calls TEXT,
|
|
33
|
+
search_results TEXT,
|
|
34
|
+
operation_ids TEXT,
|
|
35
|
+
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
36
|
+
)
|
|
37
|
+
`;
|
|
38
|
+
await sql`
|
|
39
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
session_id TEXT NOT NULL,
|
|
42
|
+
message_id TEXT,
|
|
43
|
+
app_id TEXT,
|
|
44
|
+
user_id TEXT,
|
|
45
|
+
command TEXT NOT NULL,
|
|
46
|
+
operation_type TEXT NOT NULL,
|
|
47
|
+
affected_files TEXT NOT NULL,
|
|
48
|
+
backup_path TEXT,
|
|
49
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
50
|
+
error_message TEXT,
|
|
51
|
+
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
52
|
+
)
|
|
53
|
+
`;
|
|
54
|
+
await sql`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS backups (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
operation_id TEXT NOT NULL,
|
|
58
|
+
original_path TEXT NOT NULL,
|
|
59
|
+
backup_path TEXT NOT NULL,
|
|
60
|
+
file_size INTEGER NOT NULL,
|
|
61
|
+
file_hash TEXT NOT NULL,
|
|
62
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
63
|
+
expires_at TIMESTAMPTZ NOT NULL
|
|
64
|
+
)
|
|
65
|
+
`;
|
|
66
|
+
await sql`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS trash (
|
|
68
|
+
id TEXT PRIMARY KEY,
|
|
69
|
+
session_id TEXT NOT NULL,
|
|
70
|
+
app_id TEXT,
|
|
71
|
+
user_id TEXT,
|
|
72
|
+
original_path TEXT NOT NULL,
|
|
73
|
+
trash_path TEXT NOT NULL,
|
|
74
|
+
deleted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
75
|
+
auto_delete_at TIMESTAMPTZ NOT NULL
|
|
76
|
+
)
|
|
77
|
+
`;
|
|
78
|
+
try {
|
|
79
|
+
await sql`
|
|
80
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
81
|
+
id TEXT PRIMARY KEY,
|
|
82
|
+
session_id TEXT NOT NULL,
|
|
83
|
+
message_id TEXT,
|
|
84
|
+
app_id TEXT,
|
|
85
|
+
user_id TEXT,
|
|
86
|
+
content TEXT NOT NULL,
|
|
87
|
+
content_type TEXT NOT NULL,
|
|
88
|
+
embedding vector(1536),
|
|
89
|
+
metadata JSONB,
|
|
90
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
91
|
+
)
|
|
92
|
+
`;
|
|
93
|
+
} catch {
|
|
94
|
+
await sql`
|
|
95
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
96
|
+
id TEXT PRIMARY KEY,
|
|
97
|
+
session_id TEXT NOT NULL,
|
|
98
|
+
message_id TEXT,
|
|
99
|
+
app_id TEXT,
|
|
100
|
+
user_id TEXT,
|
|
101
|
+
content TEXT NOT NULL,
|
|
102
|
+
content_type TEXT NOT NULL,
|
|
103
|
+
embedding TEXT,
|
|
104
|
+
metadata JSONB,
|
|
105
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
106
|
+
)
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_sessions_tenant ON sessions(app_id, user_id)`;
|
|
110
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC)`;
|
|
111
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)`;
|
|
112
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_messages_tenant ON messages(app_id, user_id)`;
|
|
113
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_operations_session ON operations(session_id)`;
|
|
114
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_operations_message ON operations(message_id)`;
|
|
115
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_backups_operation ON backups(operation_id)`;
|
|
116
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_backups_expires ON backups(expires_at)`;
|
|
117
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_trash_tenant ON trash(app_id, user_id)`;
|
|
118
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_trash_auto_delete ON trash(auto_delete_at)`;
|
|
119
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_embeddings_session ON embeddings(session_id)`;
|
|
120
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_embeddings_tenant ON embeddings(app_id, user_id)`;
|
|
121
|
+
try {
|
|
122
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_embeddings_vector ON embeddings USING hnsw (embedding vector_cosine_ops)`;
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function cosineSimilarity(a, b) {
|
|
127
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
128
|
+
let dotProduct = 0;
|
|
129
|
+
let normA = 0;
|
|
130
|
+
let normB = 0;
|
|
131
|
+
for (let i = 0; i < a.length; i++) {
|
|
132
|
+
dotProduct += a[i] * b[i];
|
|
133
|
+
normA += a[i] * a[i];
|
|
134
|
+
normB += b[i] * b[i];
|
|
135
|
+
}
|
|
136
|
+
if (normA === 0 || normB === 0) return 0;
|
|
137
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/adapters/postgres/sessions.ts
|
|
141
|
+
function toSessionRecord(row) {
|
|
142
|
+
return {
|
|
143
|
+
id: row.id,
|
|
144
|
+
appId: row.app_id,
|
|
145
|
+
userId: row.user_id,
|
|
146
|
+
title: row.title,
|
|
147
|
+
model: row.model,
|
|
148
|
+
mode: row.mode,
|
|
149
|
+
createdAt: new Date(row.created_at),
|
|
150
|
+
updatedAt: new Date(row.updated_at)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function getSessions(sql, ctx) {
|
|
154
|
+
const rows = await sql`
|
|
155
|
+
SELECT id, app_id, user_id, title, model, mode, created_at, updated_at
|
|
156
|
+
FROM sessions
|
|
157
|
+
WHERE (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
158
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
159
|
+
ORDER BY updated_at DESC
|
|
160
|
+
`;
|
|
161
|
+
return rows.map(toSessionRecord);
|
|
162
|
+
}
|
|
163
|
+
async function getSession(sql, id, ctx) {
|
|
164
|
+
const rows = await sql`
|
|
165
|
+
SELECT id, app_id, user_id, title, model, mode, created_at, updated_at
|
|
166
|
+
FROM sessions
|
|
167
|
+
WHERE id = ${id}
|
|
168
|
+
AND (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
169
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
170
|
+
`;
|
|
171
|
+
return rows[0] ? toSessionRecord(rows[0]) : null;
|
|
172
|
+
}
|
|
173
|
+
async function createSession(sql, input, ctx) {
|
|
174
|
+
const now = /* @__PURE__ */ new Date();
|
|
175
|
+
await sql`
|
|
176
|
+
INSERT INTO sessions (id, app_id, user_id, title, model, mode, created_at, updated_at)
|
|
177
|
+
VALUES (${input.id}, ${ctx.appId || null}, ${ctx.userId || null}, ${input.title}, ${input.model}, ${input.mode}, ${now}, ${now})
|
|
178
|
+
`;
|
|
179
|
+
return {
|
|
180
|
+
id: input.id,
|
|
181
|
+
appId: ctx.appId || null,
|
|
182
|
+
userId: ctx.userId || null,
|
|
183
|
+
title: input.title,
|
|
184
|
+
model: input.model,
|
|
185
|
+
mode: input.mode,
|
|
186
|
+
createdAt: now,
|
|
187
|
+
updatedAt: now
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function updateSession(sql, id, data, ctx) {
|
|
191
|
+
const now = /* @__PURE__ */ new Date();
|
|
192
|
+
await sql`
|
|
193
|
+
UPDATE sessions
|
|
194
|
+
SET updated_at = ${now},
|
|
195
|
+
title = COALESCE(${data.title || null}, title),
|
|
196
|
+
model = COALESCE(${data.model || null}, model),
|
|
197
|
+
mode = COALESCE(${data.mode || null}, mode)
|
|
198
|
+
WHERE id = ${id}
|
|
199
|
+
AND (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
200
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
async function deleteSession(sql, id, ctx) {
|
|
204
|
+
await sql`
|
|
205
|
+
DELETE FROM sessions
|
|
206
|
+
WHERE id = ${id}
|
|
207
|
+
AND (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
208
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/adapters/postgres/messages.ts
|
|
213
|
+
function toMessageRecord(row) {
|
|
214
|
+
return {
|
|
215
|
+
id: row.id,
|
|
216
|
+
sessionId: row.session_id,
|
|
217
|
+
appId: row.app_id,
|
|
218
|
+
userId: row.user_id,
|
|
219
|
+
role: row.role,
|
|
220
|
+
content: row.content,
|
|
221
|
+
thinking: row.thinking,
|
|
222
|
+
toolCalls: row.tool_calls,
|
|
223
|
+
searchResults: row.search_results,
|
|
224
|
+
operationIds: row.operation_ids,
|
|
225
|
+
timestamp: new Date(row.timestamp)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function getMessages(sql, sessionId) {
|
|
229
|
+
const rows = await sql`
|
|
230
|
+
SELECT id, session_id, app_id, user_id, role, content, thinking,
|
|
231
|
+
tool_calls, search_results, operation_ids, timestamp
|
|
232
|
+
FROM messages
|
|
233
|
+
WHERE session_id = ${sessionId}
|
|
234
|
+
ORDER BY timestamp
|
|
235
|
+
`;
|
|
236
|
+
return rows.map(toMessageRecord);
|
|
237
|
+
}
|
|
238
|
+
async function getMessage(sql, id, ctx) {
|
|
239
|
+
const rows = await sql`
|
|
240
|
+
SELECT id, session_id, app_id, user_id, role, content, thinking,
|
|
241
|
+
tool_calls, search_results, operation_ids, timestamp
|
|
242
|
+
FROM messages
|
|
243
|
+
WHERE id = ${id}
|
|
244
|
+
AND (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
245
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
246
|
+
`;
|
|
247
|
+
return rows[0] ? toMessageRecord(rows[0]) : null;
|
|
248
|
+
}
|
|
249
|
+
async function saveMessage(sql, input, ctx) {
|
|
250
|
+
const now = /* @__PURE__ */ new Date();
|
|
251
|
+
await sql`
|
|
252
|
+
INSERT INTO messages (id, session_id, app_id, user_id, role, content, thinking, tool_calls, search_results, operation_ids, timestamp)
|
|
253
|
+
VALUES (${input.id}, ${input.sessionId}, ${ctx.appId || null}, ${ctx.userId || null}, ${input.role}, ${input.content}, ${input.thinking}, ${input.toolCalls}, ${input.searchResults}, ${input.operationIds}, ${now})
|
|
254
|
+
`;
|
|
255
|
+
await sql`UPDATE sessions SET updated_at = ${now} WHERE id = ${input.sessionId}`;
|
|
256
|
+
return {
|
|
257
|
+
id: input.id,
|
|
258
|
+
sessionId: input.sessionId,
|
|
259
|
+
appId: ctx.appId || null,
|
|
260
|
+
userId: ctx.userId || null,
|
|
261
|
+
role: input.role,
|
|
262
|
+
content: input.content,
|
|
263
|
+
thinking: input.thinking,
|
|
264
|
+
toolCalls: input.toolCalls,
|
|
265
|
+
searchResults: input.searchResults,
|
|
266
|
+
operationIds: input.operationIds,
|
|
267
|
+
timestamp: now
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async function deleteMessagesAfter(sql, sessionId, timestamp) {
|
|
271
|
+
await sql`
|
|
272
|
+
DELETE FROM messages
|
|
273
|
+
WHERE session_id = ${sessionId}
|
|
274
|
+
AND timestamp > ${timestamp}
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
async function deleteSessionMessages(sql, sessionId) {
|
|
278
|
+
await sql`DELETE FROM messages WHERE session_id = ${sessionId}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/adapters/postgres/operations.ts
|
|
282
|
+
function toOperationRecord(row) {
|
|
283
|
+
return {
|
|
284
|
+
id: row.id,
|
|
285
|
+
sessionId: row.session_id,
|
|
286
|
+
messageId: row.message_id,
|
|
287
|
+
appId: row.app_id,
|
|
288
|
+
userId: row.user_id,
|
|
289
|
+
command: row.command,
|
|
290
|
+
operationType: row.operation_type,
|
|
291
|
+
affectedFiles: row.affected_files,
|
|
292
|
+
backupPath: row.backup_path,
|
|
293
|
+
status: row.status,
|
|
294
|
+
errorMessage: row.error_message,
|
|
295
|
+
timestamp: new Date(row.timestamp)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function getOperations(sql, sessionId) {
|
|
299
|
+
const rows = await sql`
|
|
300
|
+
SELECT id, session_id, message_id, app_id, user_id, command, operation_type,
|
|
301
|
+
affected_files, backup_path, status, error_message, timestamp
|
|
302
|
+
FROM operations
|
|
303
|
+
WHERE session_id = ${sessionId}
|
|
304
|
+
ORDER BY timestamp
|
|
305
|
+
`;
|
|
306
|
+
return rows.map(toOperationRecord);
|
|
307
|
+
}
|
|
308
|
+
async function getOperationsByMessage(sql, messageId, ctx) {
|
|
309
|
+
const rows = await sql`
|
|
310
|
+
SELECT id, session_id, message_id, app_id, user_id, command, operation_type,
|
|
311
|
+
affected_files, backup_path, status, error_message, timestamp
|
|
312
|
+
FROM operations
|
|
313
|
+
WHERE message_id = ${messageId}
|
|
314
|
+
AND (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
315
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
316
|
+
`;
|
|
317
|
+
return rows.map(toOperationRecord);
|
|
318
|
+
}
|
|
319
|
+
async function saveOperation(sql, input, ctx) {
|
|
320
|
+
const now = /* @__PURE__ */ new Date();
|
|
321
|
+
const status = input.status || "pending";
|
|
322
|
+
await sql`
|
|
323
|
+
INSERT INTO operations (id, session_id, message_id, app_id, user_id, command, operation_type, affected_files, backup_path, status, timestamp)
|
|
324
|
+
VALUES (${input.id}, ${input.sessionId}, ${input.messageId || null}, ${ctx.appId || null}, ${ctx.userId || null}, ${input.command}, ${input.operationType}, ${input.affectedFiles}, ${input.backupPath || null}, ${status}, ${now})
|
|
325
|
+
`;
|
|
326
|
+
return {
|
|
327
|
+
id: input.id,
|
|
328
|
+
sessionId: input.sessionId,
|
|
329
|
+
messageId: input.messageId,
|
|
330
|
+
appId: ctx.appId || null,
|
|
331
|
+
userId: ctx.userId || null,
|
|
332
|
+
command: input.command,
|
|
333
|
+
operationType: input.operationType,
|
|
334
|
+
affectedFiles: input.affectedFiles,
|
|
335
|
+
backupPath: input.backupPath,
|
|
336
|
+
status,
|
|
337
|
+
errorMessage: null,
|
|
338
|
+
timestamp: now
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async function updateOperationStatus(sql, id, status, errorMessage) {
|
|
342
|
+
await sql`
|
|
343
|
+
UPDATE operations
|
|
344
|
+
SET status = ${status}, error_message = ${errorMessage || null}
|
|
345
|
+
WHERE id = ${id}
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
async function deleteSessionOperations(sql, sessionId) {
|
|
349
|
+
await sql`DELETE FROM operations WHERE session_id = ${sessionId}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/adapters/postgres/backups.ts
|
|
353
|
+
function toBackupRecord(row) {
|
|
354
|
+
return {
|
|
355
|
+
id: row.id,
|
|
356
|
+
operationId: row.operation_id,
|
|
357
|
+
originalPath: row.original_path,
|
|
358
|
+
backupPath: row.backup_path,
|
|
359
|
+
fileSize: row.file_size,
|
|
360
|
+
fileHash: row.file_hash,
|
|
361
|
+
createdAt: new Date(row.created_at),
|
|
362
|
+
expiresAt: new Date(row.expires_at)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function getBackups(sql, operationId) {
|
|
366
|
+
const rows = await sql`
|
|
367
|
+
SELECT id, operation_id, original_path, backup_path, file_size, file_hash, created_at, expires_at
|
|
368
|
+
FROM backups
|
|
369
|
+
WHERE operation_id = ${operationId}
|
|
370
|
+
`;
|
|
371
|
+
return rows.map(toBackupRecord);
|
|
372
|
+
}
|
|
373
|
+
async function saveBackup(sql, input) {
|
|
374
|
+
const now = /* @__PURE__ */ new Date();
|
|
375
|
+
await sql`
|
|
376
|
+
INSERT INTO backups (id, operation_id, original_path, backup_path, file_size, file_hash, created_at, expires_at)
|
|
377
|
+
VALUES (${input.id}, ${input.operationId}, ${input.originalPath}, ${input.backupPath}, ${input.fileSize}, ${input.fileHash}, ${now}, ${input.expiresAt})
|
|
378
|
+
`;
|
|
379
|
+
return { ...input, createdAt: now };
|
|
380
|
+
}
|
|
381
|
+
async function deleteExpiredBackups(sql) {
|
|
382
|
+
const result = await sql`DELETE FROM backups WHERE expires_at < NOW()`;
|
|
383
|
+
return result.count;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/adapters/postgres/trash.ts
|
|
387
|
+
function toTrashRecord(row) {
|
|
388
|
+
return {
|
|
389
|
+
id: row.id,
|
|
390
|
+
sessionId: row.session_id,
|
|
391
|
+
appId: row.app_id,
|
|
392
|
+
userId: row.user_id,
|
|
393
|
+
originalPath: row.original_path,
|
|
394
|
+
trashPath: row.trash_path,
|
|
395
|
+
deletedAt: new Date(row.deleted_at),
|
|
396
|
+
autoDeleteAt: new Date(row.auto_delete_at)
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
async function getTrashItems(sql, ctx) {
|
|
400
|
+
const rows = await sql`
|
|
401
|
+
SELECT id, session_id, app_id, user_id, original_path, trash_path, deleted_at, auto_delete_at
|
|
402
|
+
FROM trash
|
|
403
|
+
WHERE (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
404
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
405
|
+
ORDER BY deleted_at DESC
|
|
406
|
+
`;
|
|
407
|
+
return rows.map(toTrashRecord);
|
|
408
|
+
}
|
|
409
|
+
async function moveToTrash(sql, input, ctx, config) {
|
|
410
|
+
const now = /* @__PURE__ */ new Date();
|
|
411
|
+
const retentionDays = config.trashRetentionDays || 30;
|
|
412
|
+
const autoDeleteAt = new Date(now.getTime() + retentionDays * 24 * 60 * 60 * 1e3);
|
|
413
|
+
await sql`
|
|
414
|
+
INSERT INTO trash (id, session_id, app_id, user_id, original_path, trash_path, deleted_at, auto_delete_at)
|
|
415
|
+
VALUES (${input.id}, ${input.sessionId}, ${ctx.appId || null}, ${ctx.userId || null}, ${input.originalPath}, ${input.trashPath}, ${now}, ${autoDeleteAt})
|
|
416
|
+
`;
|
|
417
|
+
return {
|
|
418
|
+
...input,
|
|
419
|
+
appId: ctx.appId || null,
|
|
420
|
+
userId: ctx.userId || null,
|
|
421
|
+
deletedAt: now,
|
|
422
|
+
autoDeleteAt
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async function restoreFromTrash(sql, id, ctx) {
|
|
426
|
+
const rows = await sql`
|
|
427
|
+
SELECT id, session_id, app_id, user_id, original_path, trash_path, deleted_at, auto_delete_at
|
|
428
|
+
FROM trash
|
|
429
|
+
WHERE id = ${id}
|
|
430
|
+
AND (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
431
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
432
|
+
`;
|
|
433
|
+
if (!rows[0]) {
|
|
434
|
+
throw new Error("\u56DE\u6536\u7AD9\u8BB0\u5F55\u4E0D\u5B58\u5728");
|
|
435
|
+
}
|
|
436
|
+
await sql`DELETE FROM trash WHERE id = ${id}`;
|
|
437
|
+
return toTrashRecord(rows[0]);
|
|
438
|
+
}
|
|
439
|
+
async function emptyExpiredTrash(sql) {
|
|
440
|
+
const result = await sql`DELETE FROM trash WHERE auto_delete_at < NOW()`;
|
|
441
|
+
return result.count;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/adapters/postgres/embeddings.ts
|
|
445
|
+
async function saveEmbedding(sql, id, content, embedding, metadata, ctx) {
|
|
446
|
+
const embeddingStr = `[${embedding.join(",")}]`;
|
|
447
|
+
try {
|
|
448
|
+
await sql`
|
|
449
|
+
INSERT INTO embeddings (id, session_id, message_id, app_id, user_id, content, content_type, embedding, metadata, created_at)
|
|
450
|
+
VALUES (${id}, ${metadata.sessionId}, ${metadata.messageId || null}, ${ctx.appId || null}, ${ctx.userId || null}, ${content}, ${metadata.contentType}, ${embeddingStr}::vector, ${JSON.stringify(metadata)}, NOW())
|
|
451
|
+
`;
|
|
452
|
+
} catch {
|
|
453
|
+
await sql`
|
|
454
|
+
INSERT INTO embeddings (id, session_id, message_id, app_id, user_id, content, content_type, embedding, metadata, created_at)
|
|
455
|
+
VALUES (${id}, ${metadata.sessionId}, ${metadata.messageId || null}, ${ctx.appId || null}, ${ctx.userId || null}, ${content}, ${metadata.contentType}, ${embeddingStr}, ${JSON.stringify(metadata)}, NOW())
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function searchSimilar(sql, queryEmbedding, options, ctx) {
|
|
460
|
+
const { limit = 10, threshold = 0.7, sessionId } = options;
|
|
461
|
+
const embeddingStr = `[${queryEmbedding.join(",")}]`;
|
|
462
|
+
try {
|
|
463
|
+
const rows = await sql`
|
|
464
|
+
SELECT id, content, content_type, metadata,
|
|
465
|
+
1 - (embedding <=> ${embeddingStr}::vector) as similarity
|
|
466
|
+
FROM embeddings
|
|
467
|
+
WHERE (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
468
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
469
|
+
AND (${sessionId || null}::TEXT IS NULL OR session_id = ${sessionId || null})
|
|
470
|
+
AND 1 - (embedding <=> ${embeddingStr}::vector) > ${threshold}
|
|
471
|
+
ORDER BY embedding <=> ${embeddingStr}::vector
|
|
472
|
+
LIMIT ${limit}
|
|
473
|
+
`;
|
|
474
|
+
return rows.map((row) => ({
|
|
475
|
+
id: row.id,
|
|
476
|
+
content: row.content,
|
|
477
|
+
contentType: row.content_type,
|
|
478
|
+
similarity: row.similarity || 0,
|
|
479
|
+
metadata: row.metadata
|
|
480
|
+
}));
|
|
481
|
+
} catch {
|
|
482
|
+
console.warn("pgvector \u4E0D\u53EF\u7528\uFF0C\u4F7F\u7528\u5185\u5B58\u8BA1\u7B97\u76F8\u4F3C\u5EA6");
|
|
483
|
+
const rows = await sql`
|
|
484
|
+
SELECT id, content, content_type, embedding, metadata
|
|
485
|
+
FROM embeddings
|
|
486
|
+
WHERE (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
487
|
+
AND (${ctx.userId || null}::TEXT IS NULL OR user_id = ${ctx.userId || null})
|
|
488
|
+
AND (${sessionId || null}::TEXT IS NULL OR session_id = ${sessionId || null})
|
|
489
|
+
`;
|
|
490
|
+
return rows.map((row) => {
|
|
491
|
+
let stored = [];
|
|
492
|
+
if (typeof row.embedding === "string") {
|
|
493
|
+
try {
|
|
494
|
+
stored = JSON.parse(row.embedding.replace(/^\[|\]$/g, "").split(",").map(Number).join(","));
|
|
495
|
+
} catch {
|
|
496
|
+
stored = row.embedding.slice(1, -1).split(",").map(Number);
|
|
497
|
+
}
|
|
498
|
+
} else if (Array.isArray(row.embedding)) {
|
|
499
|
+
stored = row.embedding;
|
|
500
|
+
}
|
|
501
|
+
const similarity = cosineSimilarity(queryEmbedding, stored);
|
|
502
|
+
return {
|
|
503
|
+
id: row.id,
|
|
504
|
+
content: row.content,
|
|
505
|
+
contentType: row.content_type,
|
|
506
|
+
similarity,
|
|
507
|
+
metadata: row.metadata
|
|
508
|
+
};
|
|
509
|
+
}).filter((r) => r.similarity >= threshold).sort((a, b) => b.similarity - a.similarity).slice(0, limit);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async function deleteSessionEmbeddings(sql, sessionId) {
|
|
513
|
+
await sql`DELETE FROM embeddings WHERE session_id = ${sessionId}`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/adapters/postgres/index.ts
|
|
517
|
+
var PostgresAdapter = class {
|
|
518
|
+
sql;
|
|
519
|
+
config;
|
|
520
|
+
initialized = false;
|
|
521
|
+
constructor(connectionString, config = { type: "postgres" }) {
|
|
522
|
+
this.sql = postgres(connectionString);
|
|
523
|
+
this.config = config;
|
|
524
|
+
}
|
|
525
|
+
/** 确保表结构已初始化 */
|
|
526
|
+
async ensureInitialized() {
|
|
527
|
+
if (this.initialized) return;
|
|
528
|
+
await initSchema(this.sql);
|
|
529
|
+
this.initialized = true;
|
|
530
|
+
}
|
|
531
|
+
// ============ 会话 ============
|
|
532
|
+
async getSessions(ctx) {
|
|
533
|
+
await this.ensureInitialized();
|
|
534
|
+
return getSessions(this.sql, ctx);
|
|
535
|
+
}
|
|
536
|
+
async getSession(id, ctx) {
|
|
537
|
+
await this.ensureInitialized();
|
|
538
|
+
return getSession(this.sql, id, ctx);
|
|
539
|
+
}
|
|
540
|
+
async createSession(input, ctx) {
|
|
541
|
+
await this.ensureInitialized();
|
|
542
|
+
return createSession(this.sql, input, ctx);
|
|
543
|
+
}
|
|
544
|
+
async updateSession(id, data, ctx) {
|
|
545
|
+
await this.ensureInitialized();
|
|
546
|
+
await updateSession(this.sql, id, data, ctx);
|
|
547
|
+
}
|
|
548
|
+
async deleteSession(id, ctx) {
|
|
549
|
+
await this.ensureInitialized();
|
|
550
|
+
await deleteSessionMessages(this.sql, id);
|
|
551
|
+
await deleteSessionOperations(this.sql, id);
|
|
552
|
+
await deleteSessionEmbeddings(this.sql, id);
|
|
553
|
+
await deleteSession(this.sql, id, ctx);
|
|
554
|
+
}
|
|
555
|
+
// ============ 消息 ============
|
|
556
|
+
async getMessages(sessionId, ctx) {
|
|
557
|
+
await this.ensureInitialized();
|
|
558
|
+
const session = await this.getSession(sessionId, ctx);
|
|
559
|
+
if (!session) return [];
|
|
560
|
+
return getMessages(this.sql, sessionId);
|
|
561
|
+
}
|
|
562
|
+
async getMessage(id, ctx) {
|
|
563
|
+
await this.ensureInitialized();
|
|
564
|
+
return getMessage(this.sql, id, ctx);
|
|
565
|
+
}
|
|
566
|
+
async saveMessage(input, ctx) {
|
|
567
|
+
await this.ensureInitialized();
|
|
568
|
+
return saveMessage(this.sql, input, ctx);
|
|
569
|
+
}
|
|
570
|
+
async deleteMessagesAfter(sessionId, timestamp, ctx) {
|
|
571
|
+
await this.ensureInitialized();
|
|
572
|
+
const session = await this.getSession(sessionId, ctx);
|
|
573
|
+
if (!session) return;
|
|
574
|
+
await deleteMessagesAfter(this.sql, sessionId, timestamp);
|
|
575
|
+
}
|
|
576
|
+
// ============ 操作日志 ============
|
|
577
|
+
async getOperations(sessionId, ctx) {
|
|
578
|
+
await this.ensureInitialized();
|
|
579
|
+
const session = await this.getSession(sessionId, ctx);
|
|
580
|
+
if (!session) return [];
|
|
581
|
+
return getOperations(this.sql, sessionId);
|
|
582
|
+
}
|
|
583
|
+
async getOperationsByMessage(messageId, ctx) {
|
|
584
|
+
await this.ensureInitialized();
|
|
585
|
+
return getOperationsByMessage(this.sql, messageId, ctx);
|
|
586
|
+
}
|
|
587
|
+
async saveOperation(input, ctx) {
|
|
588
|
+
await this.ensureInitialized();
|
|
589
|
+
return saveOperation(this.sql, input, ctx);
|
|
590
|
+
}
|
|
591
|
+
async updateOperationStatus(id, status, errorMessage) {
|
|
592
|
+
await this.ensureInitialized();
|
|
593
|
+
await updateOperationStatus(this.sql, id, status, errorMessage);
|
|
594
|
+
}
|
|
595
|
+
// ============ 备份 ============
|
|
596
|
+
async getBackups(operationId) {
|
|
597
|
+
await this.ensureInitialized();
|
|
598
|
+
return getBackups(this.sql, operationId);
|
|
599
|
+
}
|
|
600
|
+
async saveBackup(input) {
|
|
601
|
+
await this.ensureInitialized();
|
|
602
|
+
return saveBackup(this.sql, input);
|
|
603
|
+
}
|
|
604
|
+
async deleteExpiredBackups() {
|
|
605
|
+
await this.ensureInitialized();
|
|
606
|
+
return deleteExpiredBackups(this.sql);
|
|
607
|
+
}
|
|
608
|
+
// ============ 回收站 ============
|
|
609
|
+
async getTrashItems(ctx) {
|
|
610
|
+
await this.ensureInitialized();
|
|
611
|
+
return getTrashItems(this.sql, ctx);
|
|
612
|
+
}
|
|
613
|
+
async moveToTrash(input, ctx) {
|
|
614
|
+
await this.ensureInitialized();
|
|
615
|
+
return moveToTrash(this.sql, input, ctx, this.config);
|
|
616
|
+
}
|
|
617
|
+
async restoreFromTrash(id, ctx) {
|
|
618
|
+
await this.ensureInitialized();
|
|
619
|
+
return restoreFromTrash(this.sql, id, ctx);
|
|
620
|
+
}
|
|
621
|
+
async emptyExpiredTrash() {
|
|
622
|
+
await this.ensureInitialized();
|
|
623
|
+
return emptyExpiredTrash(this.sql);
|
|
624
|
+
}
|
|
625
|
+
// ============ 向量搜索 ============
|
|
626
|
+
async saveEmbedding(id, content, embedding, metadata, ctx) {
|
|
627
|
+
await this.ensureInitialized();
|
|
628
|
+
await saveEmbedding(this.sql, id, content, embedding, metadata, ctx);
|
|
629
|
+
}
|
|
630
|
+
async searchSimilar(queryEmbedding, options, ctx) {
|
|
631
|
+
await this.ensureInitialized();
|
|
632
|
+
return searchSimilar(this.sql, queryEmbedding, options, ctx);
|
|
633
|
+
}
|
|
634
|
+
// ============ 生命周期 ============
|
|
635
|
+
async close() {
|
|
636
|
+
await this.sql.end();
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export {
|
|
641
|
+
PostgresAdapter
|
|
642
|
+
};
|