248 lines
9.2 KiB
JavaScript
248 lines
9.2 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Story Summary - Chunk Store (L1/L2 storage)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
import {
|
|
metaTable,
|
|
chunksTable,
|
|
chunkVectorsTable,
|
|
eventVectorsTable,
|
|
CHUNK_MAX_TOKENS,
|
|
} from '../data/db.js';
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 工具函数
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export function float32ToBuffer(arr) {
|
|
return arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
|
|
}
|
|
|
|
export function bufferToFloat32(buffer) {
|
|
return new Float32Array(buffer);
|
|
}
|
|
|
|
export function makeChunkId(floor, chunkIdx) {
|
|
return `c-${floor}-${chunkIdx}`;
|
|
}
|
|
|
|
export function hashText(text) {
|
|
let hash = 0;
|
|
for (let i = 0; i < text.length; i++) {
|
|
hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
|
|
}
|
|
return hash.toString(36);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Meta 表操作
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export async function getMeta(chatId) {
|
|
let meta = await metaTable.get(chatId);
|
|
if (!meta) {
|
|
meta = {
|
|
chatId,
|
|
fingerprint: null,
|
|
lastChunkFloor: -1,
|
|
updatedAt: Date.now(),
|
|
};
|
|
await metaTable.put(meta);
|
|
}
|
|
return meta;
|
|
}
|
|
|
|
export async function updateMeta(chatId, updates) {
|
|
await metaTable.update(chatId, {
|
|
...updates,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Chunks 表操作
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export async function saveChunks(chatId, chunks) {
|
|
const records = chunks.map(chunk => ({
|
|
chatId,
|
|
chunkId: chunk.chunkId,
|
|
floor: chunk.floor,
|
|
chunkIdx: chunk.chunkIdx,
|
|
speaker: chunk.speaker,
|
|
isUser: chunk.isUser,
|
|
text: chunk.text,
|
|
textHash: chunk.textHash,
|
|
createdAt: Date.now(),
|
|
}));
|
|
await chunksTable.bulkPut(records);
|
|
}
|
|
|
|
export async function getAllChunks(chatId) {
|
|
return await chunksTable.where('chatId').equals(chatId).toArray();
|
|
}
|
|
|
|
export async function getChunksByFloors(chatId, floors) {
|
|
const chunks = await chunksTable
|
|
.where('[chatId+floor]')
|
|
.anyOf(floors.map(f => [chatId, f]))
|
|
.toArray();
|
|
return chunks;
|
|
}
|
|
|
|
/**
|
|
* 删除指定楼层及之后的所有 chunk 和向量
|
|
*/
|
|
export async function deleteChunksFromFloor(chatId, fromFloor) {
|
|
const chunks = await chunksTable
|
|
.where('chatId')
|
|
.equals(chatId)
|
|
.filter(c => c.floor >= fromFloor)
|
|
.toArray();
|
|
|
|
const chunkIds = chunks.map(c => c.chunkId);
|
|
|
|
await chunksTable
|
|
.where('chatId')
|
|
.equals(chatId)
|
|
.filter(c => c.floor >= fromFloor)
|
|
.delete();
|
|
|
|
for (const chunkId of chunkIds) {
|
|
await chunkVectorsTable.delete([chatId, chunkId]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 删除指定楼层的 chunk 和向量
|
|
*/
|
|
export async function deleteChunksAtFloor(chatId, floor) {
|
|
const chunks = await chunksTable
|
|
.where('[chatId+floor]')
|
|
.equals([chatId, floor])
|
|
.toArray();
|
|
|
|
const chunkIds = chunks.map(c => c.chunkId);
|
|
|
|
await chunksTable.where('[chatId+floor]').equals([chatId, floor]).delete();
|
|
|
|
for (const chunkId of chunkIds) {
|
|
await chunkVectorsTable.delete([chatId, chunkId]);
|
|
}
|
|
}
|
|
|
|
export async function clearAllChunks(chatId) {
|
|
await chunksTable.where('chatId').equals(chatId).delete();
|
|
await chunkVectorsTable.where('chatId').equals(chatId).delete();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// ChunkVectors 表操作
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export async function saveChunkVectors(chatId, items, fingerprint) {
|
|
const records = items.map(item => ({
|
|
chatId,
|
|
chunkId: item.chunkId,
|
|
vector: float32ToBuffer(new Float32Array(item.vector)),
|
|
dims: item.vector.length,
|
|
fingerprint,
|
|
}));
|
|
await chunkVectorsTable.bulkPut(records);
|
|
}
|
|
|
|
export async function getAllChunkVectors(chatId) {
|
|
const records = await chunkVectorsTable.where('chatId').equals(chatId).toArray();
|
|
return records.map(r => ({
|
|
...r,
|
|
vector: bufferToFloat32(r.vector),
|
|
}));
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// EventVectors 表操作
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export async function saveEventVectors(chatId, items, fingerprint) {
|
|
const records = items.map(item => ({
|
|
chatId,
|
|
eventId: item.eventId,
|
|
vector: float32ToBuffer(new Float32Array(item.vector)),
|
|
dims: item.vector.length,
|
|
fingerprint,
|
|
}));
|
|
await eventVectorsTable.bulkPut(records);
|
|
}
|
|
|
|
export async function getAllEventVectors(chatId) {
|
|
const records = await eventVectorsTable.where('chatId').equals(chatId).toArray();
|
|
return records.map(r => ({
|
|
...r,
|
|
vector: bufferToFloat32(r.vector),
|
|
}));
|
|
}
|
|
|
|
export async function clearEventVectors(chatId) {
|
|
await eventVectorsTable.where('chatId').equals(chatId).delete();
|
|
}
|
|
|
|
/**
|
|
* 按 ID 列表删除 event 向量
|
|
*/
|
|
export async function deleteEventVectorsByIds(chatId, eventIds) {
|
|
for (const eventId of eventIds) {
|
|
await eventVectorsTable.delete([chatId, eventId]);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// 统计与工具
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export async function getStorageStats(chatId) {
|
|
const [meta, chunkCount, chunkVectorCount, eventCount] = await Promise.all([
|
|
getMeta(chatId),
|
|
chunksTable.where('chatId').equals(chatId).count(),
|
|
chunkVectorsTable.where('chatId').equals(chatId).count(),
|
|
eventVectorsTable.where('chatId').equals(chatId).count(),
|
|
]);
|
|
|
|
return {
|
|
fingerprint: meta.fingerprint,
|
|
lastChunkFloor: meta.lastChunkFloor,
|
|
chunks: chunkCount,
|
|
chunkVectors: chunkVectorCount,
|
|
eventVectors: eventCount,
|
|
};
|
|
}
|
|
|
|
export async function clearChatData(chatId) {
|
|
await Promise.all([
|
|
metaTable.delete(chatId),
|
|
chunksTable.where('chatId').equals(chatId).delete(),
|
|
chunkVectorsTable.where('chatId').equals(chatId).delete(),
|
|
eventVectorsTable.where('chatId').equals(chatId).delete(),
|
|
]);
|
|
}
|
|
|
|
export async function ensureFingerprintMatch(chatId, newFingerprint) {
|
|
const meta = await getMeta(chatId);
|
|
if (meta.fingerprint && meta.fingerprint !== newFingerprint) {
|
|
await Promise.all([
|
|
chunkVectorsTable.where('chatId').equals(chatId).delete(),
|
|
eventVectorsTable.where('chatId').equals(chatId).delete(),
|
|
]);
|
|
await updateMeta(chatId, {
|
|
fingerprint: newFingerprint,
|
|
lastChunkFloor: -1,
|
|
});
|
|
return false;
|
|
}
|
|
if (!meta.fingerprint) {
|
|
await updateMeta(chatId, { fingerprint: newFingerprint });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export { CHUNK_MAX_TOKENS };
|