from __future__ import annotations import sqlite3 from contextlib import contextmanager from pathlib import Path from typing import Any, Iterator def utc_now() -> str: from datetime import datetime, timezone return datetime.now(timezone.utc).replace(microsecond=0).isoformat() def dict_factory(cursor: sqlite3.Cursor, row: sqlite3.Row) -> dict[str, Any]: return {col[0]: row[idx] for idx, col in enumerate(cursor.description)} class Database: def __init__(self, path: str) -> None: self.path = Path(path) self.path.parent.mkdir(parents=True, exist_ok=True) def connect(self) -> sqlite3.Connection: conn = sqlite3.connect(self.path) conn.row_factory = dict_factory conn.execute("PRAGMA foreign_keys = ON") return conn @contextmanager def session(self) -> Iterator[sqlite3.Connection]: conn = self.connect() try: yield conn conn.commit() finally: conn.close() def fetch_one(self, sql: str, params: tuple[Any, ...] = ()) -> dict[str, Any] | None: with self.session() as conn: return conn.execute(sql, params).fetchone() def fetch_all(self, sql: str, params: tuple[Any, ...] = ()) -> list[dict[str, Any]]: with self.session() as conn: return list(conn.execute(sql, params).fetchall()) def execute(self, sql: str, params: tuple[Any, ...] = ()) -> None: with self.session() as conn: conn.execute(sql, params) def init_schema(self) -> None: schema = """ CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, password_salt TEXT NOT NULL, display_name TEXT NOT NULL, role TEXT NOT NULL, approval_status TEXT NOT NULL, approved_by TEXT, approved_at TEXT, preferred_analysis_model_id TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS auth_tokens ( token TEXT PRIMARY KEY, account_id TEXT NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS model_profiles ( id TEXT PRIMARY KEY, owner_account_id TEXT, name TEXT NOT NULL, provider TEXT NOT NULL, base_url TEXT NOT NULL, api_key TEXT NOT NULL DEFAULT '', model_name TEXT NOT NULL, is_system INTEGER NOT NULL DEFAULT 0, is_default INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(owner_account_id) REFERENCES accounts(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS knowledge_bases ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', fastgpt_dataset_id TEXT, sync_status TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES accounts(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS knowledge_documents ( id TEXT PRIMARY KEY, knowledge_base_id TEXT NOT NULL, title TEXT NOT NULL, source_type TEXT NOT NULL, source_url TEXT NOT NULL DEFAULT '', transcript_text TEXT NOT NULL DEFAULT '', style_summary TEXT NOT NULL DEFAULT '', combined_text TEXT NOT NULL DEFAULT '', fastgpt_collection_id TEXT NOT NULL DEFAULT '', analysis_model_profile_id TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(knowledge_base_id) REFERENCES knowledge_bases(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS assistants ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', system_prompt TEXT NOT NULL DEFAULT '', generation_goal TEXT NOT NULL DEFAULT '', fastgpt_app_key TEXT NOT NULL DEFAULT '', model_profile_id TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES accounts(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS assistant_knowledge_bases ( assistant_id TEXT NOT NULL, knowledge_base_id TEXT NOT NULL, PRIMARY KEY (assistant_id, knowledge_base_id), FOREIGN KEY(assistant_id) REFERENCES assistants(id) ON DELETE CASCADE, FOREIGN KEY(knowledge_base_id) REFERENCES knowledge_bases(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS jobs ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, assistant_id TEXT, knowledge_base_id TEXT NOT NULL, source_type TEXT NOT NULL, source_url TEXT, title TEXT NOT NULL, language TEXT NOT NULL DEFAULT 'auto', status TEXT NOT NULL, transcript_text TEXT NOT NULL DEFAULT '', style_summary TEXT NOT NULL DEFAULT '', fastgpt_collection_id TEXT NOT NULL DEFAULT '', upload_status TEXT NOT NULL DEFAULT 'pending', error TEXT NOT NULL DEFAULT '', artifacts_json TEXT NOT NULL DEFAULT '{}', analysis_model_profile_id TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY(assistant_id) REFERENCES assistants(id) ON DELETE SET NULL, FOREIGN KEY(knowledge_base_id) REFERENCES knowledge_bases(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS app_updates ( id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, channel TEXT NOT NULL, version_code INTEGER NOT NULL, version_name TEXT NOT NULL, min_supported_code INTEGER NOT NULL, apk_url TEXT NOT NULL, apk_sha256 TEXT NOT NULL DEFAULT '', notes TEXT NOT NULL DEFAULT '', force_update INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, published_at INTEGER NOT NULL, created_by TEXT NOT NULL ); """ with self.session() as conn: conn.executescript(schema)