import { executeSql, querySql } from '@/core/storage/sqlite'; import type { SegmentOutboxEntry, SegmentOutboxStatus } from './types'; const CREATE_TABLE_SQL = ` CREATE TABLE IF NOT EXISTS segment_outbox ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id TEXT NOT NULL, voice_session_id TEXT NOT NULL, segment_index INTEGER NOT NULL, file_uri TEXT NOT NULL, duration_ms INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'pending', retry_count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); `; let initialized = false; async function ensureTable(): Promise { if (initialized) return; await executeSql(CREATE_TABLE_SQL); await executeSql( `CREATE UNIQUE INDEX IF NOT EXISTS uq_segment_outbox_voice_session_segment ON segment_outbox(voice_session_id, segment_index)`, ); initialized = true; } function mapRow(row: Record): SegmentOutboxEntry { return { id: row.id as number, conversationId: row.conversation_id as string, voiceSessionId: row.voice_session_id as string, segmentIndex: row.segment_index as number, fileUri: row.file_uri as string, durationMs: row.duration_ms as number, status: row.status as SegmentOutboxStatus, retryCount: row.retry_count as number, createdAt: row.created_at as number, }; } export interface VoicePlaybackRow { voiceSessionId: string; fileUri: string; durationMs: number; } /** * 本地语音分段:outbox(pending→sent)与可回放元数据共用同一张表。 * 音频文件在文件系统;`status=sent` 行保留用于按 voice_session_id 关联 REST 历史。 */ export const voiceSegmentStore = { async enqueue( entry: Omit< SegmentOutboxEntry, 'id' | 'status' | 'retryCount' | 'createdAt' >, ): Promise { await ensureTable(); const result = await executeSql( `INSERT INTO segment_outbox (conversation_id, voice_session_id, segment_index, file_uri, duration_ms, status, retry_count, created_at) VALUES (?, ?, ?, ?, ?, 'pending', 0, ?)`, [ entry.conversationId, entry.voiceSessionId, entry.segmentIndex, entry.fileUri, entry.durationMs, Date.now(), ], ); return result.lastInsertRowId; }, /** 发送成功后写入(或覆盖)同一条 voice+segment,用于回放与 outbox 终态统一 */ async recordSentSegment(entry: { conversationId: string; voiceSessionId: string; segmentIndex?: number; fileUri: string; durationMs: number; }): Promise { await ensureTable(); const segmentIndex = entry.segmentIndex ?? 0; const now = Date.now(); await executeSql( `INSERT INTO segment_outbox (conversation_id, voice_session_id, segment_index, file_uri, duration_ms, status, retry_count, created_at) VALUES (?, ?, ?, ?, ?, 'sent', 0, ?) ON CONFLICT(voice_session_id, segment_index) DO UPDATE SET conversation_id = excluded.conversation_id, file_uri = excluded.file_uri, duration_ms = excluded.duration_ms, status = 'sent', retry_count = 0`, [ entry.conversationId, entry.voiceSessionId, segmentIndex, entry.fileUri, entry.durationMs, now, ], ); }, async listPlaybackForConversation( conversationId: string, ): Promise { await ensureTable(); const rows = await querySql>( `SELECT voice_session_id AS voiceSessionId, file_uri AS fileUri, duration_ms AS durationMs FROM segment_outbox WHERE conversation_id = ? AND status = 'sent'`, [conversationId], ); return rows.map((r) => ({ voiceSessionId: r.voiceSessionId as string, fileUri: r.fileUri as string, durationMs: r.durationMs as number, })); }, async getPending(conversationId?: string): Promise { await ensureTable(); const sql = conversationId ? `SELECT * FROM segment_outbox WHERE status = 'pending' AND conversation_id = ? ORDER BY created_at ASC` : `SELECT * FROM segment_outbox WHERE status = 'pending' ORDER BY created_at ASC`; const params = conversationId ? [conversationId] : []; const rows = await querySql>(sql, params); return rows.map(mapRow); }, async markSending(id: number): Promise { await ensureTable(); await executeSql( `UPDATE segment_outbox SET status = 'sending' WHERE id = ?`, [id], ); }, async markSent(id: number): Promise { await ensureTable(); await executeSql(`UPDATE segment_outbox SET status = 'sent' WHERE id = ?`, [ id, ]); }, async markFailed(id: number): Promise { await ensureTable(); await executeSql( `UPDATE segment_outbox SET status = 'failed', retry_count = retry_count + 1 WHERE id = ?`, [id], ); }, async resetFailed(conversationId?: string): Promise { await ensureTable(); const sql = conversationId ? `UPDATE segment_outbox SET status = 'pending' WHERE status = 'failed' AND conversation_id = ?` : `UPDATE segment_outbox SET status = 'pending' WHERE status = 'failed'`; const params = conversationId ? [conversationId] : []; await executeSql(sql, params); }, async clearConversation(conversationId: string): Promise { await ensureTable(); await executeSql(`DELETE FROM segment_outbox WHERE conversation_id = ?`, [ conversationId, ]); }, /** @internal 测试用 */ _resetForTest(): void { initialized = false; }, } as const;