|
| 1 | +/** |
| 2 | + * AI 助手对话持久化 — SQLite CRUD |
| 3 | + * |
| 4 | + * @ai-context: 助手会话/消息/触发记录的数据库访问层; |
| 5 | + * 依赖 sqliteService.getConnection() 获取连接,表 DDL 在 schema.ts 中定义。 |
| 6 | + * 纯数据访问,无业务逻辑——业务编排在渲染进程 hooks 中完成。 |
| 7 | + */ |
| 8 | +import { randomUUID } from 'crypto'; |
| 9 | +import { getConnection } from './sqliteService.js'; |
| 10 | +import { logger } from '../logger.js'; |
| 11 | + |
| 12 | +// ── 类型(主进程侧,与渲染进程 types.ts 结构对齐) ────────── |
| 13 | + |
| 14 | +export interface SessionRow { |
| 15 | + id: string; |
| 16 | + title: string; |
| 17 | + created_at: number; |
| 18 | + updated_at: number; |
| 19 | + is_archived: number; |
| 20 | + metadata: string | null; |
| 21 | +} |
| 22 | + |
| 23 | +export interface MessageRow { |
| 24 | + id: string; |
| 25 | + session_id: string; |
| 26 | + role: 'user' | 'assistant' | 'system'; |
| 27 | + content: string; |
| 28 | + content_type: string; |
| 29 | + trigger_type: string | null; |
| 30 | + tokens_used: number | null; |
| 31 | + model: string | null; |
| 32 | + latency_ms: number | null; |
| 33 | + created_at: number; |
| 34 | +} |
| 35 | + |
| 36 | +// ── 会话 CRUD ───────────────────────────────────────────────── |
| 37 | + |
| 38 | +export function createSession(title = '新对话'): SessionRow { |
| 39 | + const db = getConnection(); |
| 40 | + const now = Date.now(); |
| 41 | + const row: SessionRow = { id: randomUUID(), title, created_at: now, updated_at: now, is_archived: 0, metadata: null }; |
| 42 | + db.prepare('INSERT INTO assistant_sessions (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)') |
| 43 | + .run(row.id, row.title, row.created_at, row.updated_at); |
| 44 | + logger.info(`[ChatRepo] Session created: ${row.id}`); |
| 45 | + return row; |
| 46 | +} |
| 47 | + |
| 48 | +export function getLatestSession(): SessionRow | null { |
| 49 | + const db = getConnection(); |
| 50 | + return db.prepare('SELECT * FROM assistant_sessions WHERE is_archived = 0 ORDER BY updated_at DESC LIMIT 1').get() as SessionRow | undefined ?? null; |
| 51 | +} |
| 52 | + |
| 53 | +export function touchSession(id: string): void { |
| 54 | + const db = getConnection(); |
| 55 | + db.prepare('UPDATE assistant_sessions SET updated_at = ? WHERE id = ?').run(Date.now(), id); |
| 56 | +} |
| 57 | + |
| 58 | +export function updateSessionTitle(id: string, title: string): void { |
| 59 | + const db = getConnection(); |
| 60 | + db.prepare('UPDATE assistant_sessions SET title = ? WHERE id = ?').run(title, id); |
| 61 | +} |
| 62 | + |
| 63 | +// ── 消息 CRUD ───────────────────────────────────────────────── |
| 64 | + |
| 65 | +export function insertMessage(msg: Omit<MessageRow, 'id' | 'created_at'> & { id?: string; created_at?: number }): MessageRow { |
| 66 | + const db = getConnection(); |
| 67 | + const row: MessageRow = { |
| 68 | + id: msg.id ?? randomUUID(), |
| 69 | + created_at: msg.created_at ?? Date.now(), |
| 70 | + session_id: msg.session_id, |
| 71 | + role: msg.role, |
| 72 | + content: msg.content, |
| 73 | + content_type: msg.content_type, |
| 74 | + trigger_type: msg.trigger_type, |
| 75 | + tokens_used: msg.tokens_used, |
| 76 | + model: msg.model, |
| 77 | + latency_ms: msg.latency_ms, |
| 78 | + }; |
| 79 | + db.prepare(`INSERT INTO assistant_messages (id, session_id, role, content, content_type, trigger_type, tokens_used, model, latency_ms, created_at) |
| 80 | + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) |
| 81 | + .run(row.id, row.session_id, row.role, row.content, row.content_type, row.trigger_type, row.tokens_used, row.model, row.latency_ms, row.created_at); |
| 82 | + return row; |
| 83 | +} |
| 84 | + |
| 85 | +export function getMessages(sessionId: string, limit = 50, before?: number): MessageRow[] { |
| 86 | + const db = getConnection(); |
| 87 | + if (before) { |
| 88 | + return db.prepare('SELECT * FROM assistant_messages WHERE session_id = ? AND created_at < ? ORDER BY created_at DESC LIMIT ?') |
| 89 | + .all(sessionId, before, limit) as MessageRow[]; |
| 90 | + } |
| 91 | + return db.prepare('SELECT * FROM assistant_messages WHERE session_id = ? ORDER BY created_at DESC LIMIT ?') |
| 92 | + .all(sessionId, limit) as MessageRow[]; |
| 93 | +} |
| 94 | + |
| 95 | +// ── 触发记录 ────────────────────────────────────────────────── |
| 96 | + |
| 97 | +export function insertTrigger(ruleId: string): string { |
| 98 | + const db = getConnection(); |
| 99 | + const id = randomUUID(); |
| 100 | + db.prepare('INSERT INTO assistant_triggers (id, rule_id, triggered_at) VALUES (?, ?, ?)').run(id, ruleId, Date.now()); |
| 101 | + return id; |
| 102 | +} |
| 103 | + |
| 104 | +export function getLastTriggerTime(ruleId: string): number | null { |
| 105 | + const db = getConnection(); |
| 106 | + const row = db.prepare('SELECT triggered_at FROM assistant_triggers WHERE rule_id = ? ORDER BY triggered_at DESC LIMIT 1').get(ruleId) as { triggered_at: number } | undefined; |
| 107 | + return row?.triggered_at ?? null; |
| 108 | +} |
| 109 | + |
| 110 | +export function getRecentTriggerCount(sinceMs: number): number { |
| 111 | + const db = getConnection(); |
| 112 | + const row = db.prepare('SELECT COUNT(*) as cnt FROM assistant_triggers WHERE triggered_at > ?').get(Date.now() - sinceMs) as { cnt: number }; |
| 113 | + return row.cnt; |
| 114 | +} |
| 115 | + |
| 116 | +export function getConsecutiveIgnores(): number { |
| 117 | + const db = getConnection(); |
| 118 | + const rows = db.prepare('SELECT dismissed, responded FROM assistant_triggers ORDER BY triggered_at DESC LIMIT ?').all(10) as Array<{ dismissed: number; responded: number }>; |
| 119 | + let count = 0; |
| 120 | + for (const r of rows) { |
| 121 | + if (r.dismissed && !r.responded) count++; |
| 122 | + else break; |
| 123 | + } |
| 124 | + return count; |
| 125 | +} |
| 126 | + |
| 127 | +export function markTriggerResponded(id: string): void { |
| 128 | + const db = getConnection(); |
| 129 | + db.prepare('UPDATE assistant_triggers SET responded = 1 WHERE id = ?').run(id); |
| 130 | +} |
| 131 | + |
| 132 | +export function markTriggerDismissed(id: string): void { |
| 133 | + const db = getConnection(); |
| 134 | + db.prepare('UPDATE assistant_triggers SET dismissed = 1 WHERE id = ?').run(id); |
| 135 | +} |
0 commit comments