2026-05-30 23:30:55 +08:00

121 lines
3.9 KiB
Python

from __future__ import annotations
from contextlib import contextmanager
from pathlib import Path
import json
import sqlite3
from typing import Any, Iterator
def connect(database_path: Path) -> sqlite3.Connection:
database_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(database_path)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
return conn
@contextmanager
def session(database_path: Path) -> Iterator[sqlite3.Connection]:
conn = connect(database_path)
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def init_db(conn: sqlite3.Connection) -> None:
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS raw_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
source_item_id TEXT NOT NULL,
source_url TEXT NOT NULL,
content_type TEXT NOT NULL,
author_id TEXT,
author_name TEXT,
title TEXT,
published_at INTEGER,
published_at_text TEXT,
collected_at INTEGER NOT NULL,
updated_at_source INTEGER,
content TEXT NOT NULL,
raw_json TEXT NOT NULL,
content_hash TEXT NOT NULL,
analysis_status TEXT NOT NULL DEFAULT 'pending',
UNIQUE(source, source_item_id)
);
CREATE TABLE IF NOT EXISTS analysis_results (
raw_item_id INTEGER PRIMARY KEY,
model TEXT NOT NULL,
sentiment TEXT NOT NULL,
is_positive INTEGER NOT NULL,
is_negative INTEGER NOT NULL,
has_actionable_feedback INTEGER NOT NULL,
feedback_types TEXT NOT NULL,
reply_recommended INTEGER NOT NULL,
reply_priority TEXT NOT NULL,
reply_suggestion TEXT NOT NULL,
summary TEXT NOT NULL,
priority TEXT NOT NULL,
confidence REAL NOT NULL,
reason TEXT NOT NULL,
model_json TEXT NOT NULL,
analyzed_at INTEGER NOT NULL,
FOREIGN KEY(raw_item_id) REFERENCES raw_items(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS work_items (
raw_item_id INTEGER PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'new',
owner TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL DEFAULT '',
last_handled_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY(raw_item_id) REFERENCES raw_items(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS sync_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at INTEGER NOT NULL,
finished_at INTEGER,
mode TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT NOT NULL DEFAULT '',
stats_json TEXT NOT NULL DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS idx_raw_items_collected_at ON raw_items(collected_at DESC);
CREATE INDEX IF NOT EXISTS idx_raw_items_content_type ON raw_items(content_type);
CREATE INDEX IF NOT EXISTS idx_raw_items_analysis_status ON raw_items(analysis_status);
CREATE INDEX IF NOT EXISTS idx_work_items_status ON work_items(status);
"""
)
def encode_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def decode_json(value: str | None, default: Any = None) -> Any:
if value is None:
return default
try:
return json.loads(value)
except json.JSONDecodeError:
return default