Files
life-echo/app-expo/src/features/voice/voice-segment-store.ts

179 lines
5.6 KiB
TypeScript
Raw Normal View History

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<void> {
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<string, unknown>): 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;
}
/**
* outboxpendingsent
* `status=sent` voice_session_id REST
*/
export const voiceSegmentStore = {
async enqueue(
entry: Omit<
SegmentOutboxEntry,
'id' | 'status' | 'retryCount' | 'createdAt'
>,
): Promise<number> {
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<void> {
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<VoicePlaybackRow[]> {
await ensureTable();
const rows = await querySql<Record<string, unknown>>(
`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<SegmentOutboxEntry[]> {
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<Record<string, unknown>>(sql, params);
return rows.map(mapRow);
},
async markSending(id: number): Promise<void> {
await ensureTable();
await executeSql(
`UPDATE segment_outbox SET status = 'sending' WHERE id = ?`,
[id],
);
},
async markSent(id: number): Promise<void> {
await ensureTable();
await executeSql(`UPDATE segment_outbox SET status = 'sent' WHERE id = ?`, [
id,
]);
},
async markFailed(id: number): Promise<void> {
await ensureTable();
await executeSql(
`UPDATE segment_outbox SET status = 'failed', retry_count = retry_count + 1 WHERE id = ?`,
[id],
);
},
async resetFailed(conversationId?: string): Promise<void> {
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<void> {
await ensureTable();
await executeSql(`DELETE FROM segment_outbox WHERE conversation_id = ?`, [
conversationId,
]);
},
/** @internal 测试用 */
_resetForTest(): void {
initialized = false;
},
} as const;