@huyooo/ai-chat-storage 0.2.13 → 0.2.14
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-LZ2LBU6Q.js → chunk-6L6KHCOT.js} +128 -7
- package/dist/{chunk-7AKL7W3Y.js → chunk-CQ66EXL6.js} +120 -10
- package/dist/index.d.ts +53 -2
- package/dist/index.js +4 -4
- package/dist/{postgres-4AZE26G4.js → postgres-CIPWMGYI.js} +1 -1
- package/dist/{sqlite-5SSRI3KS.js → sqlite-MZGQZHVI.js} +1 -1
- package/package.json +1 -1
|
@@ -28,6 +28,8 @@ var messages = sqliteTable("messages", {
|
|
|
28
28
|
userId: text("user_id"),
|
|
29
29
|
role: text("role").notNull(),
|
|
30
30
|
content: text("content").notNull(),
|
|
31
|
+
/** 用户消息附带的图片 JSON 数组 */
|
|
32
|
+
images: text("images"),
|
|
31
33
|
/** 生成此消息时使用的模型 */
|
|
32
34
|
model: text("model"),
|
|
33
35
|
/** 生成此消息时使用的模式 (ask/agent) */
|
|
@@ -39,10 +41,13 @@ var messages = sqliteTable("messages", {
|
|
|
39
41
|
/** 执行步骤列表 JSON */
|
|
40
42
|
steps: text("steps"),
|
|
41
43
|
operationIds: text("operation_ids"),
|
|
44
|
+
/** 消息序号(会话内递增,用于分叉删除,必须) */
|
|
45
|
+
sequence: integer("sequence").notNull(),
|
|
42
46
|
timestamp: integer("timestamp", { mode: "timestamp" }).notNull()
|
|
43
47
|
}, (table) => [
|
|
44
48
|
index("idx_messages_session").on(table.sessionId),
|
|
45
|
-
index("idx_messages_tenant").on(table.appId, table.userId)
|
|
49
|
+
index("idx_messages_tenant").on(table.appId, table.userId),
|
|
50
|
+
index("idx_messages_sequence").on(table.sessionId, table.sequence)
|
|
46
51
|
]);
|
|
47
52
|
var operations = sqliteTable("operations", {
|
|
48
53
|
id: text("id").primaryKey(),
|
|
@@ -130,6 +135,7 @@ function initSchema(sqlite) {
|
|
|
130
135
|
user_id TEXT,
|
|
131
136
|
role TEXT NOT NULL,
|
|
132
137
|
content TEXT NOT NULL,
|
|
138
|
+
images TEXT,
|
|
133
139
|
model TEXT,
|
|
134
140
|
mode TEXT,
|
|
135
141
|
web_search_enabled INTEGER,
|
|
@@ -247,6 +253,55 @@ function initSchema(sqlite) {
|
|
|
247
253
|
sqlite.exec(`ALTER TABLE messages ADD COLUMN steps TEXT`);
|
|
248
254
|
} catch {
|
|
249
255
|
}
|
|
256
|
+
try {
|
|
257
|
+
sqlite.exec(`ALTER TABLE messages ADD COLUMN sequence INTEGER`);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
sqlite.exec(`ALTER TABLE messages ADD COLUMN images TEXT`);
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
sqlite.exec(`
|
|
266
|
+
UPDATE messages
|
|
267
|
+
SET sequence = (
|
|
268
|
+
SELECT COUNT(*) + 1
|
|
269
|
+
FROM messages m2
|
|
270
|
+
WHERE m2.session_id = messages.session_id
|
|
271
|
+
AND (m2.timestamp < messages.timestamp
|
|
272
|
+
OR (m2.timestamp = messages.timestamp AND m2.id < messages.id))
|
|
273
|
+
)
|
|
274
|
+
WHERE sequence IS NULL
|
|
275
|
+
`);
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
sqlite.exec(`
|
|
280
|
+
UPDATE messages
|
|
281
|
+
SET sequence = (
|
|
282
|
+
SELECT COUNT(*) + 1
|
|
283
|
+
FROM messages m2
|
|
284
|
+
WHERE m2.session_id = messages.session_id
|
|
285
|
+
AND (m2.timestamp < messages.timestamp
|
|
286
|
+
OR (m2.timestamp = messages.timestamp AND m2.id < messages.id))
|
|
287
|
+
)
|
|
288
|
+
WHERE sequence IS NULL
|
|
289
|
+
`);
|
|
290
|
+
sqlite.exec(`
|
|
291
|
+
UPDATE messages
|
|
292
|
+
SET sequence = (
|
|
293
|
+
SELECT COALESCE(MAX(sequence), 0) + 1
|
|
294
|
+
FROM messages m2
|
|
295
|
+
WHERE m2.session_id = messages.session_id
|
|
296
|
+
)
|
|
297
|
+
WHERE sequence IS NULL
|
|
298
|
+
`);
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
sqlite.exec(`CREATE INDEX IF NOT EXISTS idx_messages_sequence ON messages(session_id, sequence)`);
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
250
305
|
}
|
|
251
306
|
function buildSessionsTenantCondition(ctx) {
|
|
252
307
|
const conditions = [];
|
|
@@ -363,8 +418,41 @@ function deleteSession(db, id, ctx) {
|
|
|
363
418
|
}
|
|
364
419
|
|
|
365
420
|
// src/adapters/sqlite/messages.ts
|
|
366
|
-
import { eq as eq3, and as and3, gt } from "drizzle-orm";
|
|
421
|
+
import { eq as eq3, and as and3, gt, max } from "drizzle-orm";
|
|
367
422
|
function toMessageRecord(row) {
|
|
423
|
+
if (row.sequence === null || row.sequence === void 0) {
|
|
424
|
+
throw new Error(`\u6D88\u606F ${row.id} \u7F3A\u5C11\u5E8F\u53F7\uFF0C\u6570\u636E\u635F\u574F`);
|
|
425
|
+
}
|
|
426
|
+
const toSequence = (seq) => {
|
|
427
|
+
if (typeof seq === "number" && Number.isFinite(seq)) return seq;
|
|
428
|
+
if (typeof seq === "string") {
|
|
429
|
+
const parsed = Number(seq);
|
|
430
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
431
|
+
}
|
|
432
|
+
throw new Error(`\u6D88\u606F ${row.id} \u7684\u5E8F\u53F7\u65E0\u6548: ${seq}`);
|
|
433
|
+
};
|
|
434
|
+
const toTimestamp = (ts) => {
|
|
435
|
+
if (typeof ts === "number" && Number.isFinite(ts)) return ts;
|
|
436
|
+
if (ts instanceof Date) return ts.getTime();
|
|
437
|
+
if (typeof ts === "string") {
|
|
438
|
+
const parsed = Date.parse(ts);
|
|
439
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
440
|
+
const asNum = Number(ts);
|
|
441
|
+
if (Number.isFinite(asNum)) return asNum;
|
|
442
|
+
}
|
|
443
|
+
throw new Error(`\u6D88\u606F ${row.id} \u7684\u65F6\u95F4\u6233\u65E0\u6548: ${ts}`);
|
|
444
|
+
};
|
|
445
|
+
let images = [];
|
|
446
|
+
if (row.images) {
|
|
447
|
+
try {
|
|
448
|
+
const parsed = JSON.parse(row.images);
|
|
449
|
+
if (Array.isArray(parsed)) {
|
|
450
|
+
images = parsed;
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
console.warn(`\u6D88\u606F ${row.id} \u7684 images \u5B57\u6BB5\u89E3\u6790\u5931\u8D25:`, row.images);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
368
456
|
return {
|
|
369
457
|
id: row.id,
|
|
370
458
|
sessionId: row.sessionId,
|
|
@@ -372,19 +460,26 @@ function toMessageRecord(row) {
|
|
|
372
460
|
userId: row.userId,
|
|
373
461
|
role: row.role,
|
|
374
462
|
content: row.content,
|
|
463
|
+
images,
|
|
375
464
|
model: row.model,
|
|
376
465
|
mode: row.mode,
|
|
466
|
+
// Drizzle ORM 的 mode: 'boolean' 已自动处理类型转换
|
|
377
467
|
webSearchEnabled: row.webSearchEnabled,
|
|
378
468
|
thinkingEnabled: row.thinkingEnabled,
|
|
379
469
|
steps: row.steps,
|
|
380
470
|
operationIds: row.operationIds,
|
|
381
|
-
|
|
471
|
+
sequence: toSequence(row.sequence),
|
|
472
|
+
timestamp: toTimestamp(row.timestamp)
|
|
382
473
|
};
|
|
383
474
|
}
|
|
384
475
|
function getMessages(db, sessionId) {
|
|
385
|
-
const rows = db.select().from(messages).where(eq3(messages.sessionId, sessionId)).orderBy(messages.timestamp).all();
|
|
476
|
+
const rows = db.select().from(messages).where(eq3(messages.sessionId, sessionId)).orderBy(messages.sequence, messages.timestamp).all();
|
|
386
477
|
return rows.map(toMessageRecord);
|
|
387
478
|
}
|
|
479
|
+
function getMaxSequence(db, sessionId) {
|
|
480
|
+
const result = db.select({ maxSeq: max(messages.sequence) }).from(messages).where(eq3(messages.sessionId, sessionId)).get();
|
|
481
|
+
return result?.maxSeq ?? 0;
|
|
482
|
+
}
|
|
388
483
|
function getMessage(db, id, ctx) {
|
|
389
484
|
const tenantCondition = buildMessagesTenantCondition(ctx);
|
|
390
485
|
const condition = tenantCondition ? and3(eq3(messages.id, id), tenantCondition) : eq3(messages.id, id);
|
|
@@ -392,7 +487,8 @@ function getMessage(db, id, ctx) {
|
|
|
392
487
|
return row ? toMessageRecord(row) : null;
|
|
393
488
|
}
|
|
394
489
|
function saveMessage(db, input, ctx) {
|
|
395
|
-
const now =
|
|
490
|
+
const now = Date.now();
|
|
491
|
+
const sequence = getMaxSequence(db, input.sessionId) + 1;
|
|
396
492
|
const record = {
|
|
397
493
|
id: input.id,
|
|
398
494
|
sessionId: input.sessionId,
|
|
@@ -400,12 +496,14 @@ function saveMessage(db, input, ctx) {
|
|
|
400
496
|
userId: ctx.userId || null,
|
|
401
497
|
role: input.role,
|
|
402
498
|
content: input.content,
|
|
499
|
+
images: input.images || [],
|
|
403
500
|
model: input.model,
|
|
404
501
|
mode: input.mode,
|
|
405
502
|
webSearchEnabled: input.webSearchEnabled,
|
|
406
503
|
thinkingEnabled: input.thinkingEnabled,
|
|
407
504
|
steps: input.steps,
|
|
408
505
|
operationIds: input.operationIds,
|
|
506
|
+
sequence,
|
|
409
507
|
timestamp: now
|
|
410
508
|
};
|
|
411
509
|
db.insert(messages).values({
|
|
@@ -415,15 +513,18 @@ function saveMessage(db, input, ctx) {
|
|
|
415
513
|
userId: record.userId,
|
|
416
514
|
role: record.role,
|
|
417
515
|
content: record.content,
|
|
516
|
+
images: record.images.length > 0 ? JSON.stringify(record.images) : null,
|
|
418
517
|
model: record.model,
|
|
419
518
|
mode: record.mode,
|
|
420
519
|
webSearchEnabled: record.webSearchEnabled,
|
|
421
520
|
thinkingEnabled: record.thinkingEnabled,
|
|
422
521
|
steps: record.steps,
|
|
423
522
|
operationIds: record.operationIds,
|
|
424
|
-
|
|
523
|
+
sequence: record.sequence,
|
|
524
|
+
timestamp: new Date(record.timestamp)
|
|
525
|
+
// SQLite schema 需要 Date 对象,但 record.timestamp 是 number
|
|
425
526
|
}).run();
|
|
426
|
-
db.update(sessions).set({ updatedAt: now }).where(eq3(sessions.id, input.sessionId)).run();
|
|
527
|
+
db.update(sessions).set({ updatedAt: new Date(now) }).where(eq3(sessions.id, input.sessionId)).run();
|
|
427
528
|
return record;
|
|
428
529
|
}
|
|
429
530
|
function updateMessage(db, id, data, ctx) {
|
|
@@ -446,6 +547,15 @@ function deleteMessagesAfter(db, sessionId, timestamp) {
|
|
|
446
547
|
gt(messages.timestamp, timestamp)
|
|
447
548
|
)).run();
|
|
448
549
|
}
|
|
550
|
+
function deleteMessagesAfterMessage(db, sessionId, anchorId, anchorSequence) {
|
|
551
|
+
if (anchorSequence === null || anchorSequence === void 0) {
|
|
552
|
+
throw new Error(`\u6D88\u606F ${anchorId} \u7F3A\u5C11\u5E8F\u53F7\uFF0C\u65E0\u6CD5\u5220\u9664\u540E\u7EED\u6D88\u606F`);
|
|
553
|
+
}
|
|
554
|
+
db.delete(messages).where(and3(
|
|
555
|
+
eq3(messages.sessionId, sessionId),
|
|
556
|
+
gt(messages.sequence, anchorSequence)
|
|
557
|
+
)).run();
|
|
558
|
+
}
|
|
449
559
|
function deleteSessionMessages(db, sessionId) {
|
|
450
560
|
db.delete(messages).where(eq3(messages.sessionId, sessionId)).run();
|
|
451
561
|
}
|
|
@@ -761,6 +871,17 @@ var SqliteAdapter = class {
|
|
|
761
871
|
if (!session) return;
|
|
762
872
|
deleteMessagesAfter(this.db, sessionId, timestamp);
|
|
763
873
|
}
|
|
874
|
+
async deleteMessagesAfterMessageId(sessionId, messageId, ctx) {
|
|
875
|
+
const session = await this.getSession(sessionId, ctx);
|
|
876
|
+
if (!session) return;
|
|
877
|
+
const anchor = await this.getMessage(messageId, ctx);
|
|
878
|
+
if (!anchor) return;
|
|
879
|
+
if (anchor.sessionId !== sessionId) return;
|
|
880
|
+
if (anchor.sequence === null || anchor.sequence === void 0) {
|
|
881
|
+
throw new Error(`\u6D88\u606F ${messageId} \u7F3A\u5C11\u5E8F\u53F7\uFF0C\u65E0\u6CD5\u5220\u9664\u540E\u7EED\u6D88\u606F`);
|
|
882
|
+
}
|
|
883
|
+
deleteMessagesAfterMessage(this.db, sessionId, messageId, anchor.sequence);
|
|
884
|
+
}
|
|
764
885
|
// ============ 操作日志 ============
|
|
765
886
|
async getOperations(sessionId, ctx) {
|
|
766
887
|
const session = await this.getSession(sessionId, ctx);
|
|
@@ -28,15 +28,51 @@ async function initSchema(sql) {
|
|
|
28
28
|
user_id TEXT,
|
|
29
29
|
role TEXT NOT NULL,
|
|
30
30
|
content TEXT NOT NULL,
|
|
31
|
+
images TEXT,
|
|
31
32
|
model TEXT,
|
|
32
33
|
mode TEXT,
|
|
33
34
|
web_search_enabled BOOLEAN,
|
|
34
35
|
thinking_enabled BOOLEAN,
|
|
35
36
|
steps TEXT,
|
|
36
37
|
operation_ids TEXT,
|
|
38
|
+
sequence INTEGER,
|
|
37
39
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
38
40
|
)
|
|
39
41
|
`;
|
|
42
|
+
try {
|
|
43
|
+
await sql`ALTER TABLE messages ADD COLUMN IF NOT EXISTS sequence INTEGER`;
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await sql`ALTER TABLE messages ADD COLUMN IF NOT EXISTS images TEXT`;
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await sql`
|
|
52
|
+
UPDATE messages
|
|
53
|
+
SET sequence = sub.rank
|
|
54
|
+
FROM (
|
|
55
|
+
SELECT id, ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY timestamp, id) as rank
|
|
56
|
+
FROM messages
|
|
57
|
+
WHERE sequence IS NULL
|
|
58
|
+
) sub
|
|
59
|
+
WHERE messages.id = sub.id
|
|
60
|
+
`;
|
|
61
|
+
await sql`
|
|
62
|
+
UPDATE messages
|
|
63
|
+
SET sequence = (
|
|
64
|
+
SELECT COALESCE(MAX(sequence), 0) + 1
|
|
65
|
+
FROM messages m2
|
|
66
|
+
WHERE m2.session_id = messages.session_id
|
|
67
|
+
)
|
|
68
|
+
WHERE sequence IS NULL
|
|
69
|
+
`;
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_messages_sequence ON messages(session_id, sequence)`;
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
40
76
|
await sql`
|
|
41
77
|
CREATE TABLE IF NOT EXISTS operations (
|
|
42
78
|
id TEXT PRIMARY KEY,
|
|
@@ -241,6 +277,39 @@ async function deleteSession(sql, id, ctx) {
|
|
|
241
277
|
|
|
242
278
|
// src/adapters/postgres/messages.ts
|
|
243
279
|
function toMessageRecord(row) {
|
|
280
|
+
if (row.sequence === null || row.sequence === void 0) {
|
|
281
|
+
throw new Error(`\u6D88\u606F ${row.id} \u7F3A\u5C11\u5E8F\u53F7\uFF0C\u6570\u636E\u635F\u574F`);
|
|
282
|
+
}
|
|
283
|
+
const toSequence = (seq) => {
|
|
284
|
+
if (typeof seq === "number" && Number.isFinite(seq)) return seq;
|
|
285
|
+
if (typeof seq === "string") {
|
|
286
|
+
const parsed = Number(seq);
|
|
287
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
288
|
+
}
|
|
289
|
+
throw new Error(`\u6D88\u606F ${row.id} \u7684\u5E8F\u53F7\u65E0\u6548: ${seq}`);
|
|
290
|
+
};
|
|
291
|
+
const toTimestamp = (ts) => {
|
|
292
|
+
if (typeof ts === "number" && Number.isFinite(ts)) return ts;
|
|
293
|
+
if (ts instanceof Date) return ts.getTime();
|
|
294
|
+
if (typeof ts === "string") {
|
|
295
|
+
const parsed = Date.parse(ts);
|
|
296
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
297
|
+
const asNum = Number(ts);
|
|
298
|
+
if (Number.isFinite(asNum)) return asNum;
|
|
299
|
+
}
|
|
300
|
+
throw new Error(`\u6D88\u606F ${row.id} \u7684\u65F6\u95F4\u6233\u65E0\u6548: ${ts}`);
|
|
301
|
+
};
|
|
302
|
+
let images = [];
|
|
303
|
+
if (row.images) {
|
|
304
|
+
try {
|
|
305
|
+
const parsed = JSON.parse(row.images);
|
|
306
|
+
if (Array.isArray(parsed)) {
|
|
307
|
+
images = parsed;
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
console.warn(`\u6D88\u606F ${row.id} \u7684 images \u5B57\u6BB5\u89E3\u6790\u5931\u8D25:`, row.images);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
244
313
|
return {
|
|
245
314
|
id: row.id,
|
|
246
315
|
sessionId: row.session_id,
|
|
@@ -248,29 +317,41 @@ function toMessageRecord(row) {
|
|
|
248
317
|
userId: row.user_id,
|
|
249
318
|
role: row.role,
|
|
250
319
|
content: row.content,
|
|
320
|
+
images,
|
|
251
321
|
model: row.model,
|
|
252
322
|
mode: row.mode,
|
|
323
|
+
// PostgreSQL 的 BOOLEAN 类型直接返回 boolean | null
|
|
253
324
|
webSearchEnabled: row.web_search_enabled,
|
|
254
325
|
thinkingEnabled: row.thinking_enabled,
|
|
255
326
|
steps: row.steps,
|
|
256
327
|
operationIds: row.operation_ids,
|
|
257
|
-
|
|
328
|
+
sequence: toSequence(row.sequence),
|
|
329
|
+
timestamp: toTimestamp(row.timestamp)
|
|
258
330
|
};
|
|
259
331
|
}
|
|
260
332
|
async function getMessages(sql, sessionId) {
|
|
261
333
|
const rows = await sql`
|
|
262
|
-
SELECT id, session_id, app_id, user_id, role, content, model, mode,
|
|
263
|
-
web_search_enabled, thinking_enabled, steps, operation_ids, timestamp
|
|
334
|
+
SELECT id, session_id, app_id, user_id, role, content, images, model, mode,
|
|
335
|
+
web_search_enabled, thinking_enabled, steps, operation_ids, sequence, timestamp
|
|
264
336
|
FROM messages
|
|
265
337
|
WHERE session_id = ${sessionId}
|
|
266
|
-
ORDER BY timestamp
|
|
338
|
+
ORDER BY sequence NULLS LAST, timestamp
|
|
267
339
|
`;
|
|
268
340
|
return rows.map(toMessageRecord);
|
|
269
341
|
}
|
|
342
|
+
async function getMaxSequence(sql, sessionId) {
|
|
343
|
+
const rows = await sql`
|
|
344
|
+
SELECT MAX(sequence) as max_seq
|
|
345
|
+
FROM messages
|
|
346
|
+
WHERE session_id = ${sessionId}
|
|
347
|
+
`;
|
|
348
|
+
const maxSeq = rows[0]?.max_seq;
|
|
349
|
+
return maxSeq !== null && maxSeq !== void 0 ? maxSeq : 0;
|
|
350
|
+
}
|
|
270
351
|
async function getMessage(sql, id, ctx) {
|
|
271
352
|
const rows = await sql`
|
|
272
|
-
SELECT id, session_id, app_id, user_id, role, content, model, mode,
|
|
273
|
-
web_search_enabled, thinking_enabled, steps, operation_ids, timestamp
|
|
353
|
+
SELECT id, session_id, app_id, user_id, role, content, images, model, mode,
|
|
354
|
+
web_search_enabled, thinking_enabled, steps, operation_ids, sequence, timestamp
|
|
274
355
|
FROM messages
|
|
275
356
|
WHERE id = ${id}
|
|
276
357
|
AND (${ctx.appId || null}::TEXT IS NULL OR app_id = ${ctx.appId || null})
|
|
@@ -279,12 +360,16 @@ async function getMessage(sql, id, ctx) {
|
|
|
279
360
|
return rows[0] ? toMessageRecord(rows[0]) : null;
|
|
280
361
|
}
|
|
281
362
|
async function saveMessage(sql, input, ctx) {
|
|
282
|
-
const now =
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
const nowDate = new Date(now);
|
|
365
|
+
const sequence = await getMaxSequence(sql, input.sessionId);
|
|
366
|
+
const nextSequence = sequence + 1;
|
|
367
|
+
const imagesJson = input.images && input.images.length > 0 ? JSON.stringify(input.images) : null;
|
|
283
368
|
await sql`
|
|
284
|
-
INSERT INTO messages (id, session_id, app_id, user_id, role, content, model, mode, web_search_enabled, thinking_enabled, steps, operation_ids, timestamp)
|
|
285
|
-
VALUES (${input.id}, ${input.sessionId}, ${ctx.appId || null}, ${ctx.userId || null}, ${input.role}, ${input.content}, ${input.model}, ${input.mode}, ${input.webSearchEnabled}, ${input.thinkingEnabled}, ${input.steps}, ${input.operationIds}, ${
|
|
369
|
+
INSERT INTO messages (id, session_id, app_id, user_id, role, content, images, model, mode, web_search_enabled, thinking_enabled, steps, operation_ids, sequence, timestamp)
|
|
370
|
+
VALUES (${input.id}, ${input.sessionId}, ${ctx.appId || null}, ${ctx.userId || null}, ${input.role}, ${input.content}, ${imagesJson}, ${input.model}, ${input.mode}, ${input.webSearchEnabled}, ${input.thinkingEnabled}, ${input.steps}, ${input.operationIds}, ${nextSequence}, ${nowDate})
|
|
286
371
|
`;
|
|
287
|
-
await sql`UPDATE sessions SET updated_at = ${
|
|
372
|
+
await sql`UPDATE sessions SET updated_at = ${nowDate} WHERE id = ${input.sessionId}`;
|
|
288
373
|
return {
|
|
289
374
|
id: input.id,
|
|
290
375
|
sessionId: input.sessionId,
|
|
@@ -292,13 +377,16 @@ async function saveMessage(sql, input, ctx) {
|
|
|
292
377
|
userId: ctx.userId || null,
|
|
293
378
|
role: input.role,
|
|
294
379
|
content: input.content,
|
|
380
|
+
images: input.images || [],
|
|
295
381
|
model: input.model,
|
|
296
382
|
mode: input.mode,
|
|
297
383
|
webSearchEnabled: input.webSearchEnabled,
|
|
298
384
|
thinkingEnabled: input.thinkingEnabled,
|
|
299
385
|
steps: input.steps,
|
|
300
386
|
operationIds: input.operationIds,
|
|
387
|
+
sequence: nextSequence,
|
|
301
388
|
timestamp: now
|
|
389
|
+
// 返回 number,不受 JSON 序列化影响
|
|
302
390
|
};
|
|
303
391
|
}
|
|
304
392
|
async function updateMessage(sql, id, data, ctx) {
|
|
@@ -343,6 +431,16 @@ async function deleteMessagesAfter(sql, sessionId, timestamp) {
|
|
|
343
431
|
AND timestamp > ${timestamp}
|
|
344
432
|
`;
|
|
345
433
|
}
|
|
434
|
+
async function deleteMessagesAfterMessage(sql, sessionId, anchorId, anchorSequence) {
|
|
435
|
+
if (anchorSequence === null || anchorSequence === void 0) {
|
|
436
|
+
throw new Error(`\u6D88\u606F ${anchorId} \u7F3A\u5C11\u5E8F\u53F7\uFF0C\u65E0\u6CD5\u5220\u9664\u540E\u7EED\u6D88\u606F`);
|
|
437
|
+
}
|
|
438
|
+
await sql`
|
|
439
|
+
DELETE FROM messages
|
|
440
|
+
WHERE session_id = ${sessionId}
|
|
441
|
+
AND sequence > ${anchorSequence}
|
|
442
|
+
`;
|
|
443
|
+
}
|
|
346
444
|
async function deleteSessionMessages(sql, sessionId) {
|
|
347
445
|
await sql`DELETE FROM messages WHERE session_id = ${sessionId}`;
|
|
348
446
|
}
|
|
@@ -646,6 +744,18 @@ var PostgresAdapter = class {
|
|
|
646
744
|
if (!session) return;
|
|
647
745
|
await deleteMessagesAfter(this.sql, sessionId, timestamp);
|
|
648
746
|
}
|
|
747
|
+
async deleteMessagesAfterMessageId(sessionId, messageId, ctx) {
|
|
748
|
+
await this.ensureInitialized();
|
|
749
|
+
const session = await this.getSession(sessionId, ctx);
|
|
750
|
+
if (!session) return;
|
|
751
|
+
const anchor = await this.getMessage(messageId, ctx);
|
|
752
|
+
if (!anchor) return;
|
|
753
|
+
if (anchor.sessionId !== sessionId) return;
|
|
754
|
+
if (anchor.sequence === null || anchor.sequence === void 0) {
|
|
755
|
+
throw new Error(`\u6D88\u606F ${messageId} \u7F3A\u5C11\u5E8F\u53F7\uFF0C\u65E0\u6CD5\u5220\u9664\u540E\u7EED\u6D88\u606F`);
|
|
756
|
+
}
|
|
757
|
+
await deleteMessagesAfterMessage(this.sql, sessionId, messageId, anchor.sequence);
|
|
758
|
+
}
|
|
649
759
|
// ============ 操作日志 ============
|
|
650
760
|
async getOperations(sessionId, ctx) {
|
|
651
761
|
await this.ensureInitialized();
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ interface MessageRecord {
|
|
|
33
33
|
userId: string | null;
|
|
34
34
|
role: 'user' | 'assistant';
|
|
35
35
|
content: string;
|
|
36
|
+
/** 用户消息附带的图片(data URL 或 base64) */
|
|
37
|
+
images: string[];
|
|
36
38
|
/** 生成此消息时使用的模型 */
|
|
37
39
|
model: string | null;
|
|
38
40
|
/** 生成此消息时使用的模式 (ask/agent) */
|
|
@@ -44,9 +46,12 @@ interface MessageRecord {
|
|
|
44
46
|
/** 执行步骤列表 JSON */
|
|
45
47
|
steps: string | null;
|
|
46
48
|
operationIds: string | null;
|
|
47
|
-
|
|
49
|
+
/** 消息序号(会话内递增,用于分叉删除,必须) */
|
|
50
|
+
sequence: number;
|
|
51
|
+
/** 时间戳(毫秒,Unix 时间戳,不受 JSON 序列化影响) */
|
|
52
|
+
timestamp: number;
|
|
48
53
|
}
|
|
49
|
-
type CreateMessageInput = Omit<MessageRecord, 'timestamp' | 'appId' | 'userId'>;
|
|
54
|
+
type CreateMessageInput = Omit<MessageRecord, 'timestamp' | 'appId' | 'userId' | 'sequence'>;
|
|
50
55
|
/** 更新消息输入(只更新 content 和 steps) */
|
|
51
56
|
type UpdateMessageInput = Partial<Pick<MessageRecord, 'content' | 'steps'>>;
|
|
52
57
|
type OperationStatus = 'pending' | 'confirmed' | 'executed' | 'reverted' | 'failed';
|
|
@@ -123,6 +128,14 @@ interface StorageAdapter {
|
|
|
123
128
|
/** 更新消息(用于增量保存助手消息) */
|
|
124
129
|
updateMessage(id: string, data: UpdateMessageInput, ctx: StorageContext): Promise<void>;
|
|
125
130
|
deleteMessagesAfter(sessionId: string, timestamp: Date, ctx: StorageContext): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* 删除指定消息之后的所有消息(用于分叉/重新发送)
|
|
133
|
+
*
|
|
134
|
+
* 说明:
|
|
135
|
+
* - 前端/IPC 里 timestamp 可能出现 Date/string 等序列化差异
|
|
136
|
+
* - 用 messageId 作为锚点更稳定:后端根据 messageId 查出 timestamp,再执行 deleteMessagesAfter
|
|
137
|
+
*/
|
|
138
|
+
deleteMessagesAfterMessageId(sessionId: string, messageId: string, ctx: StorageContext): Promise<void>;
|
|
126
139
|
getOperations(sessionId: string, ctx: StorageContext): Promise<OperationRecord[]>;
|
|
127
140
|
getOperationsByMessage(messageId: string, ctx: StorageContext): Promise<OperationRecord[]>;
|
|
128
141
|
saveOperation(operation: CreateOperationInput, ctx: StorageContext): Promise<OperationRecord>;
|
|
@@ -484,6 +497,25 @@ declare const messages: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
484
497
|
}, {}, {
|
|
485
498
|
length: number | undefined;
|
|
486
499
|
}>;
|
|
500
|
+
images: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
501
|
+
name: "images";
|
|
502
|
+
tableName: "messages";
|
|
503
|
+
dataType: "string";
|
|
504
|
+
columnType: "SQLiteText";
|
|
505
|
+
data: string;
|
|
506
|
+
driverParam: string;
|
|
507
|
+
notNull: false;
|
|
508
|
+
hasDefault: false;
|
|
509
|
+
isPrimaryKey: false;
|
|
510
|
+
isAutoincrement: false;
|
|
511
|
+
hasRuntimeDefault: false;
|
|
512
|
+
enumValues: [string, ...string[]];
|
|
513
|
+
baseColumn: never;
|
|
514
|
+
identity: undefined;
|
|
515
|
+
generated: undefined;
|
|
516
|
+
}, {}, {
|
|
517
|
+
length: number | undefined;
|
|
518
|
+
}>;
|
|
487
519
|
model: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
488
520
|
name: "model";
|
|
489
521
|
tableName: "messages";
|
|
@@ -594,6 +626,23 @@ declare const messages: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
594
626
|
}, {}, {
|
|
595
627
|
length: number | undefined;
|
|
596
628
|
}>;
|
|
629
|
+
sequence: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
630
|
+
name: "sequence";
|
|
631
|
+
tableName: "messages";
|
|
632
|
+
dataType: "number";
|
|
633
|
+
columnType: "SQLiteInteger";
|
|
634
|
+
data: number;
|
|
635
|
+
driverParam: number;
|
|
636
|
+
notNull: true;
|
|
637
|
+
hasDefault: false;
|
|
638
|
+
isPrimaryKey: false;
|
|
639
|
+
isAutoincrement: false;
|
|
640
|
+
hasRuntimeDefault: false;
|
|
641
|
+
enumValues: undefined;
|
|
642
|
+
baseColumn: never;
|
|
643
|
+
identity: undefined;
|
|
644
|
+
generated: undefined;
|
|
645
|
+
}, {}, {}>;
|
|
597
646
|
timestamp: drizzle_orm_sqlite_core.SQLiteColumn<{
|
|
598
647
|
name: "timestamp";
|
|
599
648
|
tableName: "messages";
|
|
@@ -1376,6 +1425,7 @@ declare class SqliteAdapter implements StorageAdapter {
|
|
|
1376
1425
|
saveMessage(input: CreateMessageInput, ctx: StorageContext): Promise<MessageRecord>;
|
|
1377
1426
|
updateMessage(id: string, data: UpdateMessageInput, ctx: StorageContext): Promise<void>;
|
|
1378
1427
|
deleteMessagesAfter(sessionId: string, timestamp: Date, ctx: StorageContext): Promise<void>;
|
|
1428
|
+
deleteMessagesAfterMessageId(sessionId: string, messageId: string, ctx: StorageContext): Promise<void>;
|
|
1379
1429
|
getOperations(sessionId: string, ctx: StorageContext): Promise<OperationRecord[]>;
|
|
1380
1430
|
getOperationsByMessage(messageId: string, ctx: StorageContext): Promise<OperationRecord[]>;
|
|
1381
1431
|
saveOperation(input: CreateOperationInput, ctx: StorageContext): Promise<OperationRecord>;
|
|
@@ -1422,6 +1472,7 @@ declare class PostgresAdapter implements StorageAdapter {
|
|
|
1422
1472
|
saveMessage(input: CreateMessageInput, ctx: StorageContext): Promise<MessageRecord>;
|
|
1423
1473
|
updateMessage(id: string, data: UpdateMessageInput, ctx: StorageContext): Promise<void>;
|
|
1424
1474
|
deleteMessagesAfter(sessionId: string, timestamp: Date, ctx: StorageContext): Promise<void>;
|
|
1475
|
+
deleteMessagesAfterMessageId(sessionId: string, messageId: string, ctx: StorageContext): Promise<void>;
|
|
1425
1476
|
getOperations(sessionId: string, ctx: StorageContext): Promise<OperationRecord[]>;
|
|
1426
1477
|
getOperationsByMessage(messageId: string, ctx: StorageContext): Promise<OperationRecord[]>;
|
|
1427
1478
|
saveOperation(input: CreateOperationInput, ctx: StorageContext): Promise<OperationRecord>;
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
operations,
|
|
7
7
|
sessions,
|
|
8
8
|
trash
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6L6KHCOT.js";
|
|
10
10
|
import {
|
|
11
11
|
PostgresAdapter
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-CQ66EXL6.js";
|
|
13
13
|
|
|
14
14
|
// src/index.ts
|
|
15
15
|
import { mkdirSync, existsSync } from "fs";
|
|
@@ -24,14 +24,14 @@ async function createStorage(config) {
|
|
|
24
24
|
if (!existsSync(dir)) {
|
|
25
25
|
mkdirSync(dir, { recursive: true });
|
|
26
26
|
}
|
|
27
|
-
const { SqliteAdapter: SqliteAdapter2 } = await import("./sqlite-
|
|
27
|
+
const { SqliteAdapter: SqliteAdapter2 } = await import("./sqlite-MZGQZHVI.js");
|
|
28
28
|
return new SqliteAdapter2(config.sqlitePath, config);
|
|
29
29
|
}
|
|
30
30
|
case "postgres": {
|
|
31
31
|
if (!config.postgresUrl) {
|
|
32
32
|
throw new Error("PostgreSQL \u9700\u8981\u63D0\u4F9B postgresUrl \u914D\u7F6E");
|
|
33
33
|
}
|
|
34
|
-
const { PostgresAdapter: PostgresAdapter2 } = await import("./postgres-
|
|
34
|
+
const { PostgresAdapter: PostgresAdapter2 } = await import("./postgres-CIPWMGYI.js");
|
|
35
35
|
return new PostgresAdapter2(config.postgresUrl, config);
|
|
36
36
|
}
|
|
37
37
|
default:
|