Files
CipherTalk/electron/services/ai/aiDatabase.ts
T
ILoveBingLu 78dd50de17 重构AI摘要;
兼容最新版Deepseek
2026-04-25 01:50:50 +08:00

936 lines
28 KiB
TypeScript

import Database from 'better-sqlite3'
import { existsSync, mkdirSync } from 'fs'
import { join, dirname } from 'path'
import type {
AnalysisBlock,
ExtractedStructuredAnalysis,
StructuredAnalysis,
SummaryEvidenceRef
} from '../ai-agent/types/analysis'
import { parseStoredStructuredAnalysis } from '../ai-agent/types/analysis'
type AnalysisRunStatus =
| 'completed'
| 'fallback_legacy'
| 'backfill_facts_only'
| 'legacy_placeholder'
type AnalysisSourceKind =
| 'generate_summary'
| 'generate_summary_legacy'
| 'backfill_structured'
| 'backfill_legacy'
export interface SaveAnalysisArtifactsInput {
summaryId: number
sessionId: string
timeRangeStart: number
timeRangeEnd: number
timeRangeDays: number
rawMessageCount: number
effectiveMessageCount?: number
blockCount: number
provider: string
model: string
status: AnalysisRunStatus
sourceKind: AnalysisSourceKind
evidenceResolved: boolean
blocksAvailable: boolean
blocks?: AnalysisBlock[]
blockAnalyses?: ExtractedStructuredAnalysis[]
finalAnalysis?: StructuredAnalysis
createdAt?: number
updatedAt?: number
}
interface SummaryBackfillRow {
id: number
session_id: string
time_range_start: number
time_range_end: number
time_range_days: number
message_count: number
provider: string
model: string
created_at: number
structured_result_json?: string | null
}
interface FactRecord {
factType: 'topic' | 'decision' | 'todo' | 'risk' | 'event' | 'open_question'
factKey: string
sortOrder: number
displayText: string
confidence?: number
importance?: number
severity?: string
owner?: string
deadline?: string
status?: string
eventDate?: string
payloadJson: string
evidenceRefs: SummaryEvidenceRef[]
}
const ANALYSIS_STORAGE_BOOTSTRAP_KEY = 'analysis_storage_bootstrap_v1_completed'
function normalizeComparableText(value?: string): string {
return String(value || '')
.trim()
.toLowerCase()
.replace(/\s+/g, '')
.replace(/[,。!?;:、“”‘’()【】《》,.!?:;"'()\-_/\\[\]{}]+/g, '')
}
function hasAnyEvidenceRefs(analysis?: StructuredAnalysis): boolean {
if (!analysis) return false
return [
...analysis.decisions,
...analysis.todos,
...analysis.risks,
...analysis.events
].some((item) => item.evidenceRefs.length > 0)
}
function buildTopicFactRecords(analysis: StructuredAnalysis): FactRecord[] {
return analysis.topics.map((item, index) => ({
factType: 'topic',
factKey: normalizeComparableText(item.name),
sortOrder: index,
displayText: item.name,
importance: item.importance,
payloadJson: JSON.stringify(item),
evidenceRefs: []
}))
}
function buildDecisionFactRecords(analysis: StructuredAnalysis): FactRecord[] {
return analysis.decisions.map((item, index) => ({
factType: 'decision',
factKey: normalizeComparableText(item.text),
sortOrder: index,
displayText: item.text,
confidence: item.confidence,
payloadJson: JSON.stringify(item),
evidenceRefs: item.evidenceRefs
}))
}
function buildTodoFactRecords(analysis: StructuredAnalysis): FactRecord[] {
return analysis.todos.map((item, index) => ({
factType: 'todo',
factKey: `${normalizeComparableText(item.owner)}|${normalizeComparableText(item.task)}|${normalizeComparableText(item.deadline)}`,
sortOrder: index,
displayText: item.task,
confidence: item.confidence,
owner: item.owner,
deadline: item.deadline,
status: item.status,
payloadJson: JSON.stringify(item),
evidenceRefs: item.evidenceRefs
}))
}
function buildRiskFactRecords(analysis: StructuredAnalysis): FactRecord[] {
return analysis.risks.map((item, index) => ({
factType: 'risk',
factKey: normalizeComparableText(item.text),
sortOrder: index,
displayText: item.text,
confidence: item.confidence,
severity: item.severity,
payloadJson: JSON.stringify(item),
evidenceRefs: item.evidenceRefs
}))
}
function buildEventFactRecords(analysis: StructuredAnalysis): FactRecord[] {
return analysis.events.map((item, index) => ({
factType: 'event',
factKey: `${normalizeComparableText(item.text)}|${normalizeComparableText(item.date)}`,
sortOrder: index,
displayText: item.text,
confidence: item.confidence,
eventDate: item.date,
payloadJson: JSON.stringify(item),
evidenceRefs: item.evidenceRefs
}))
}
function buildOpenQuestionFactRecords(analysis: StructuredAnalysis): FactRecord[] {
return analysis.openQuestions.map((item, index) => ({
factType: 'open_question',
factKey: normalizeComparableText(item.text),
sortOrder: index,
displayText: item.text,
payloadJson: JSON.stringify(item),
evidenceRefs: []
}))
}
function buildFactRecords(analysis?: StructuredAnalysis): FactRecord[] {
if (!analysis) {
return []
}
return [
...buildTopicFactRecords(analysis),
...buildDecisionFactRecords(analysis),
...buildTodoFactRecords(analysis),
...buildRiskFactRecords(analysis),
...buildEventFactRecords(analysis),
...buildOpenQuestionFactRecords(analysis)
].filter((item) => item.factKey && item.displayText)
}
/**
* AI 专用数据库管理
*/
export class AIDatabase {
private db: Database.Database | null = null
private dbPath: string | null = null
/**
* 初始化数据库
*/
init(cachePath: string, wxid: string): void {
void wxid
this.dbPath = join(cachePath, 'ai_summary.db')
const dir = dirname(this.dbPath)
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}
this.db = new Database(this.dbPath)
this.createTables()
try {
this.bootstrapAnalysisStorageBackfill()
this.catchUpMissingAnalysisRuns()
} catch (error) {
console.warn('[AIDatabase] 分析存储回填初始化失败,将在后续启动继续补录:', error)
}
}
/**
* 创建表结构
*/
private createTables(): void {
if (!this.db) return
this.db.exec(`
CREATE TABLE IF NOT EXISTS summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
time_range_start INTEGER NOT NULL,
time_range_end INTEGER NOT NULL,
time_range_days INTEGER NOT NULL,
message_count INTEGER NOT NULL,
summary_text TEXT NOT NULL,
tokens_used INTEGER,
cost REAL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_summaries_session ON summaries(session_id);
CREATE INDEX IF NOT EXISTS idx_summaries_created ON summaries(created_at);
CREATE INDEX IF NOT EXISTS idx_summaries_time_range ON summaries(time_range_start, time_range_end);
CREATE TABLE IF NOT EXISTS usage_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
provider TEXT NOT NULL,
model TEXT,
total_tokens INTEGER DEFAULT 0,
total_cost REAL DEFAULT 0,
request_count INTEGER DEFAULT 0,
UNIQUE(date, provider, model)
);
CREATE INDEX IF NOT EXISTS idx_usage_date ON usage_stats(date);
CREATE TABLE IF NOT EXISTS summary_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cache_key TEXT UNIQUE NOT NULL,
summary_id INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
FOREIGN KEY (summary_id) REFERENCES summaries(id)
);
CREATE INDEX IF NOT EXISTS idx_cache_key ON summary_cache(cache_key);
CREATE INDEX IF NOT EXISTS idx_cache_expires ON summary_cache(expires_at);
CREATE TABLE IF NOT EXISTS db_meta (
meta_key TEXT PRIMARY KEY,
meta_value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS analysis_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
summary_id INTEGER NOT NULL,
session_id TEXT NOT NULL,
time_range_start INTEGER NOT NULL,
time_range_end INTEGER NOT NULL,
time_range_days INTEGER NOT NULL,
raw_message_count INTEGER NOT NULL,
effective_message_count INTEGER,
block_count INTEGER NOT NULL DEFAULT 0,
provider TEXT NOT NULL,
model TEXT NOT NULL,
status TEXT NOT NULL,
source_kind TEXT NOT NULL,
evidence_resolved INTEGER NOT NULL DEFAULT 0,
blocks_available INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_analysis_runs_summary ON analysis_runs(summary_id);
CREATE INDEX IF NOT EXISTS idx_analysis_runs_session ON analysis_runs(session_id);
CREATE TABLE IF NOT EXISTS analysis_blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
block_index INTEGER NOT NULL,
block_id TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
message_count INTEGER NOT NULL,
char_count INTEGER NOT NULL,
rendered_text TEXT NOT NULL,
messages_json TEXT NOT NULL,
extracted_result_json TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_analysis_blocks_run_block ON analysis_blocks(run_id, block_index);
CREATE INDEX IF NOT EXISTS idx_analysis_blocks_run ON analysis_blocks(run_id);
CREATE TABLE IF NOT EXISTS extracted_facts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id INTEGER NOT NULL,
fact_type TEXT NOT NULL,
fact_key TEXT NOT NULL,
sort_order INTEGER NOT NULL,
display_text TEXT NOT NULL,
confidence REAL,
importance REAL,
severity TEXT,
owner TEXT,
deadline TEXT,
status TEXT,
event_date TEXT,
payload_json TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_extracted_facts_run_type_key ON extracted_facts(run_id, fact_type, fact_key);
CREATE INDEX IF NOT EXISTS idx_extracted_facts_run_sort ON extracted_facts(run_id, fact_type, sort_order);
CREATE TABLE IF NOT EXISTS evidence_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fact_id INTEGER NOT NULL,
run_id INTEGER NOT NULL,
evidence_order INTEGER NOT NULL,
session_id TEXT NOT NULL,
local_id INTEGER NOT NULL,
create_time INTEGER NOT NULL,
sort_seq INTEGER NOT NULL,
sender_username TEXT,
sender_display_name TEXT,
preview_text TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_evidence_links_fact_message ON evidence_links(fact_id, local_id, create_time, sort_seq);
CREATE INDEX IF NOT EXISTS idx_evidence_links_run ON evidence_links(run_id);
`)
try {
this.db.exec("ALTER TABLE summaries ADD COLUMN prompt_text TEXT")
} catch (e) {
// 忽略错误,列已存在
}
try {
this.db.exec("ALTER TABLE summaries ADD COLUMN custom_name TEXT")
} catch (e) {
// 忽略错误,列已存在
}
try {
this.db.exec("ALTER TABLE summaries ADD COLUMN structured_result_json TEXT")
} catch (e) {
// 忽略错误,列已存在
}
}
/**
* 获取数据库实例
*/
getDb(): Database.Database {
if (!this.db) {
throw new Error('数据库未初始化')
}
return this.db
}
/**
* 保存摘要
*/
saveSummary(summary: {
sessionId: string
timeRangeStart: number
timeRangeEnd: number
timeRangeDays: number
messageCount: number
summaryText: string
tokensUsed: number
cost: number
provider: string
model: string
promptText?: string
structuredResultJson?: string
createdAt?: number
}): number {
const db = this.getDb()
const createdAt = summary.createdAt || Date.now()
console.log('[AIDatabase] 保存摘要:', {
sessionId: summary.sessionId,
timeRangeDays: summary.timeRangeDays,
messageCount: summary.messageCount,
provider: summary.provider,
model: summary.model
})
const result = db.prepare(`
INSERT INTO summaries (
session_id, time_range_start, time_range_end, time_range_days,
message_count, summary_text, tokens_used, cost,
provider, model, created_at, prompt_text, structured_result_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
summary.sessionId,
summary.timeRangeStart,
summary.timeRangeEnd,
summary.timeRangeDays,
summary.messageCount,
summary.summaryText,
summary.tokensUsed,
summary.cost,
summary.provider,
summary.model,
createdAt,
summary.promptText || '',
summary.structuredResultJson || null
)
console.log('[AIDatabase] 摘要已保存,ID:', result.lastInsertRowid)
return result.lastInsertRowid as number
}
saveAnalysisArtifacts(input: SaveAnalysisArtifactsInput): void {
const db = this.getDb()
const saveTx = db.transaction((payload: SaveAnalysisArtifactsInput) => {
const createdAt = payload.createdAt || Date.now()
const updatedAt = payload.updatedAt || createdAt
const existingRun = db.prepare(`
SELECT id FROM analysis_runs WHERE summary_id = ?
`).get(payload.summaryId) as { id: number } | undefined
let runId = existingRun?.id
if (runId) {
db.prepare('DELETE FROM evidence_links WHERE run_id = ?').run(runId)
db.prepare('DELETE FROM extracted_facts WHERE run_id = ?').run(runId)
db.prepare('DELETE FROM analysis_blocks WHERE run_id = ?').run(runId)
db.prepare(`
UPDATE analysis_runs
SET
session_id = ?,
time_range_start = ?,
time_range_end = ?,
time_range_days = ?,
raw_message_count = ?,
effective_message_count = ?,
block_count = ?,
provider = ?,
model = ?,
status = ?,
source_kind = ?,
evidence_resolved = ?,
blocks_available = ?,
updated_at = ?
WHERE id = ?
`).run(
payload.sessionId,
payload.timeRangeStart,
payload.timeRangeEnd,
payload.timeRangeDays,
payload.rawMessageCount,
payload.effectiveMessageCount ?? null,
payload.blockCount,
payload.provider,
payload.model,
payload.status,
payload.sourceKind,
payload.evidenceResolved ? 1 : 0,
payload.blocksAvailable ? 1 : 0,
updatedAt,
runId
)
} else {
const result = db.prepare(`
INSERT INTO analysis_runs (
summary_id, session_id, time_range_start, time_range_end, time_range_days,
raw_message_count, effective_message_count, block_count,
provider, model, status, source_kind,
evidence_resolved, blocks_available, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
payload.summaryId,
payload.sessionId,
payload.timeRangeStart,
payload.timeRangeEnd,
payload.timeRangeDays,
payload.rawMessageCount,
payload.effectiveMessageCount ?? null,
payload.blockCount,
payload.provider,
payload.model,
payload.status,
payload.sourceKind,
payload.evidenceResolved ? 1 : 0,
payload.blocksAvailable ? 1 : 0,
createdAt,
updatedAt
)
runId = result.lastInsertRowid as number
}
if (payload.blocksAvailable && payload.blocks?.length) {
const insertBlock = db.prepare(`
INSERT INTO analysis_blocks (
run_id, block_index, block_id, start_time, end_time,
message_count, char_count, rendered_text, messages_json, extracted_result_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
payload.blocks.forEach((block, index) => {
insertBlock.run(
runId,
index,
block.blockId,
block.startTime,
block.endTime,
block.messageCount,
block.charCount,
block.renderedText,
JSON.stringify(block.messages),
payload.blockAnalyses?.[index] ? JSON.stringify(payload.blockAnalyses[index]) : null
)
})
}
const factRecords = buildFactRecords(payload.finalAnalysis)
if (factRecords.length > 0) {
const insertFact = db.prepare(`
INSERT INTO extracted_facts (
run_id, fact_type, fact_key, sort_order, display_text,
confidence, importance, severity, owner, deadline, status, event_date, payload_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
const insertEvidenceLink = db.prepare(`
INSERT INTO evidence_links (
fact_id, run_id, evidence_order, session_id, local_id, create_time,
sort_seq, sender_username, sender_display_name, preview_text
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
for (const factRecord of factRecords) {
const factResult = insertFact.run(
runId,
factRecord.factType,
factRecord.factKey,
factRecord.sortOrder,
factRecord.displayText,
factRecord.confidence ?? null,
factRecord.importance ?? null,
factRecord.severity ?? null,
factRecord.owner ?? null,
factRecord.deadline ?? null,
factRecord.status ?? null,
factRecord.eventDate ?? null,
factRecord.payloadJson
)
const factId = factResult.lastInsertRowid as number
factRecord.evidenceRefs.forEach((evidenceRef, evidenceIndex) => {
insertEvidenceLink.run(
factId,
runId,
evidenceIndex,
evidenceRef.sessionId,
evidenceRef.localId,
evidenceRef.createTime,
evidenceRef.sortSeq,
evidenceRef.senderUsername ?? null,
evidenceRef.senderDisplayName ?? null,
evidenceRef.previewText
)
})
}
}
})
saveTx(input)
}
bootstrapAnalysisStorageBackfill(): void {
const db = this.getDb()
const bootstrapCompleted = this.getMetaValue(ANALYSIS_STORAGE_BOOTSTRAP_KEY)
if (bootstrapCompleted === '1') {
return
}
const insertedCount = this.backfillMissingAnalysisRuns()
this.setMetaValue(ANALYSIS_STORAGE_BOOTSTRAP_KEY, '1')
console.log('[AIDatabase] analysis storage bootstrap backfill 完成:', { insertedCount })
}
catchUpMissingAnalysisRuns(): void {
const insertedCount = this.backfillMissingAnalysisRuns()
if (insertedCount > 0) {
console.log('[AIDatabase] analysis storage catch-up 完成:', { insertedCount })
}
}
/**
* 保存缓存
*/
saveCache(cacheKey: string, summaryId: number, expiresAt: number): void {
const db = this.getDb()
db.prepare(`
INSERT OR REPLACE INTO summary_cache (cache_key, summary_id, expires_at)
VALUES (?, ?, ?)
`).run(cacheKey, summaryId, expiresAt)
}
/**
* 获取缓存的摘要
*/
getCachedSummary(cacheKey: string): any | null {
const db = this.getDb()
const now = Date.now()
const row: any = db.prepare(`
SELECT s.* FROM summaries s
JOIN summary_cache c ON s.id = c.summary_id
WHERE c.cache_key = ? AND c.expires_at > ?
`).get(cacheKey, now)
if (!row) return null
return {
id: row.id,
sessionId: row.session_id,
timeRangeStart: row.time_range_start,
timeRangeEnd: row.time_range_end,
timeRangeDays: row.time_range_days,
messageCount: row.message_count,
summaryText: row.summary_text,
tokensUsed: row.tokens_used,
cost: row.cost,
provider: row.provider,
model: row.model,
createdAt: row.created_at,
promptText: row.prompt_text,
structuredAnalysis: this.parseStructuredAnalysisColumn(row.structured_result_json)
}
}
/**
* 更新使用统计
*/
updateUsageStats(provider: string, model: string, tokens: number, cost: number): void {
const db = this.getDb()
const date = new Date().toISOString().split('T')[0]
db.prepare(`
INSERT INTO usage_stats (date, provider, model, total_tokens, total_cost, request_count)
VALUES (?, ?, ?, ?, ?, 1)
ON CONFLICT(date, provider, model) DO UPDATE SET
total_tokens = total_tokens + excluded.total_tokens,
total_cost = total_cost + excluded.total_cost,
request_count = request_count + 1
`).run(date, provider, model, tokens, cost)
}
/**
* 获取使用统计
*/
getUsageStats(startDate?: string, endDate?: string): any[] {
const db = this.getDb()
let query = 'SELECT * FROM usage_stats'
const params: any[] = []
if (startDate && endDate) {
query += ' WHERE date >= ? AND date <= ?'
params.push(startDate, endDate)
} else if (startDate) {
query += ' WHERE date >= ?'
params.push(startDate)
} else if (endDate) {
query += ' WHERE date <= ?'
params.push(endDate)
}
query += ' ORDER BY date DESC'
return db.prepare(query).all(...params)
}
/**
* 获取会话的摘要历史
*/
getSummaryHistory(sessionId: string, limit: number = 10): Array<{
id: number
sessionId: string
timeRangeStart: number
timeRangeEnd: number
timeRangeDays: number
messageCount: number
summaryText: string
tokensUsed: number
cost: number
provider: string
model: string
createdAt: number
promptText?: string
customName?: string
structuredAnalysis?: StructuredAnalysis
}> {
const db = this.getDb()
console.log('[AIDatabase] 查询历史记录:', { sessionId, limit })
const rows = db.prepare(`
SELECT * FROM summaries
WHERE session_id = ?
ORDER BY created_at DESC
LIMIT ?
`).all(sessionId, limit)
console.log('[AIDatabase] 查询到', rows.length, '条历史记录')
return rows.map((row: any) => ({
id: row.id,
sessionId: row.session_id,
timeRangeStart: row.time_range_start,
timeRangeEnd: row.time_range_end,
timeRangeDays: row.time_range_days,
messageCount: row.message_count,
summaryText: row.summary_text,
tokensUsed: row.tokens_used,
cost: row.cost,
provider: row.provider,
model: row.model,
createdAt: row.created_at,
promptText: row.prompt_text,
customName: row.custom_name,
structuredAnalysis: this.parseStructuredAnalysisColumn(row.structured_result_json)
}))
}
/**
* 删除摘要
*/
deleteSummary(id: number): boolean {
const db = this.getDb()
try {
const deleteTx = db.transaction((summaryId: number) => {
db.prepare('DELETE FROM summary_cache WHERE summary_id = ?').run(summaryId)
db.prepare(`
DELETE FROM evidence_links
WHERE run_id IN (SELECT id FROM analysis_runs WHERE summary_id = ?)
`).run(summaryId)
db.prepare(`
DELETE FROM extracted_facts
WHERE run_id IN (SELECT id FROM analysis_runs WHERE summary_id = ?)
`).run(summaryId)
db.prepare(`
DELETE FROM analysis_blocks
WHERE run_id IN (SELECT id FROM analysis_runs WHERE summary_id = ?)
`).run(summaryId)
db.prepare('DELETE FROM analysis_runs WHERE summary_id = ?').run(summaryId)
return db.prepare('DELETE FROM summaries WHERE id = ?').run(summaryId)
})
const result = deleteTx(id)
return result.changes > 0
} catch (e) {
console.error('[AIDatabase] 删除摘要失败:', e)
return false
}
}
/**
* 重命名摘要
*/
renameSummary(id: number, customName: string): boolean {
const db = this.getDb()
try {
const result = db.prepare('UPDATE summaries SET custom_name = ? WHERE id = ?').run(customName, id)
return result.changes > 0
} catch (e) {
console.error('[AIDatabase] 重命名摘要失败:', e)
return false
}
}
/**
* 清理过期缓存
*/
cleanExpiredCache(): void {
const db = this.getDb()
const now = Date.now()
db.prepare('DELETE FROM summary_cache WHERE expires_at <= ?').run(now)
}
/**
* 关闭数据库
*/
close(): void {
if (this.db) {
this.db.close()
this.db = null
}
}
private parseStructuredAnalysisColumn(rawValue: unknown): StructuredAnalysis | undefined {
const parsed = parseStoredStructuredAnalysis(rawValue)
if (!parsed && rawValue) {
console.warn('[AIDatabase] structured_result_json 解析失败,已忽略该字段')
}
return parsed
}
private getMetaValue(metaKey: string): string | undefined {
const db = this.getDb()
const row = db.prepare(`
SELECT meta_value FROM db_meta WHERE meta_key = ?
`).get(metaKey) as { meta_value: string } | undefined
return row?.meta_value
}
private setMetaValue(metaKey: string, metaValue: string): void {
const db = this.getDb()
db.prepare(`
INSERT INTO db_meta (meta_key, meta_value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(meta_key) DO UPDATE SET
meta_value = excluded.meta_value,
updated_at = excluded.updated_at
`).run(metaKey, metaValue, Date.now())
}
private backfillMissingAnalysisRuns(): number {
const candidates = this.getBackfillCandidates()
let insertedCount = 0
for (const summaryRow of candidates) {
try {
const structuredAnalysis = this.parseStructuredAnalysisColumn(summaryRow.structured_result_json)
const payload: SaveAnalysisArtifactsInput = structuredAnalysis
? {
summaryId: summaryRow.id,
sessionId: summaryRow.session_id,
timeRangeStart: summaryRow.time_range_start,
timeRangeEnd: summaryRow.time_range_end,
timeRangeDays: summaryRow.time_range_days,
rawMessageCount: summaryRow.message_count,
effectiveMessageCount: undefined,
blockCount: 0,
provider: summaryRow.provider,
model: summaryRow.model,
status: 'backfill_facts_only',
sourceKind: 'backfill_structured',
evidenceResolved: hasAnyEvidenceRefs(structuredAnalysis),
blocksAvailable: false,
finalAnalysis: structuredAnalysis,
createdAt: summaryRow.created_at,
updatedAt: Date.now()
}
: {
summaryId: summaryRow.id,
sessionId: summaryRow.session_id,
timeRangeStart: summaryRow.time_range_start,
timeRangeEnd: summaryRow.time_range_end,
timeRangeDays: summaryRow.time_range_days,
rawMessageCount: summaryRow.message_count,
effectiveMessageCount: undefined,
blockCount: 0,
provider: summaryRow.provider,
model: summaryRow.model,
status: 'legacy_placeholder',
sourceKind: 'backfill_legacy',
evidenceResolved: false,
blocksAvailable: false,
createdAt: summaryRow.created_at,
updatedAt: Date.now()
}
this.saveAnalysisArtifacts(payload)
insertedCount += 1
} catch (error) {
console.warn('[AIDatabase] 回填 analysis_runs 失败,已跳过该摘要:', {
summaryId: summaryRow.id,
error: String(error)
})
}
}
return insertedCount
}
private getBackfillCandidates(): SummaryBackfillRow[] {
const db = this.getDb()
return db.prepare(`
SELECT
s.id,
s.session_id,
s.time_range_start,
s.time_range_end,
s.time_range_days,
s.message_count,
s.provider,
s.model,
s.created_at,
s.structured_result_json
FROM summaries s
LEFT JOIN analysis_runs ar ON ar.summary_id = s.id
WHERE ar.id IS NULL
ORDER BY s.id ASC
`).all() as SummaryBackfillRow[]
}
}
export const aiDatabase = new AIDatabase()