// ═══════════════════════════════════════════════════════════════════════════ // 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 };