@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.
@@ -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
+ };