refactor: Restructure project into core, services, ui, and utils directories, moving existing modules and adding new UI tabs.
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Module quản lý cơ sở dữ liệu SQLite - Lưu trữ tập trung
|
||||
|
||||
Bản quyền © 2026 Công ty TNHH Công nghệ An ninh mạng Huyễn Thành Tân Cương (Công nghệ Huyễn Thành)
|
||||
Tác giả: Huyễn Thành
|
||||
"""
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DB_DIR = "data"
|
||||
DB_FILE = os.path.join(DB_DIR, "tinix_story.db")
|
||||
os.makedirs(DB_DIR, exist_ok=True)
|
||||
|
||||
_connection: Optional[sqlite3.Connection] = None
|
||||
|
||||
|
||||
def get_db() -> sqlite3.Connection:
|
||||
"""Lấy kết nối DB singleton (WAL mode, foreign keys ON)"""
|
||||
global _connection
|
||||
if _connection is None:
|
||||
_connection = sqlite3.connect(DB_FILE, check_same_thread=False)
|
||||
_connection.execute("PRAGMA journal_mode=WAL")
|
||||
_connection.execute("PRAGMA foreign_keys=ON")
|
||||
_connection.row_factory = sqlite3.Row
|
||||
init_db(_connection)
|
||||
logger.info(f"Database connected: {DB_FILE}")
|
||||
return _connection
|
||||
|
||||
|
||||
def init_db(conn: Optional[sqlite3.Connection] = None) -> None:
|
||||
"""Tạo tất cả bảng nếu chưa có"""
|
||||
if conn is None:
|
||||
conn = get_db()
|
||||
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backends (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL,
|
||||
base_url TEXT NOT NULL,
|
||||
api_key TEXT NOT NULL DEFAULT '',
|
||||
model TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
timeout INTEGER NOT NULL DEFAULT 30,
|
||||
retry_times INTEGER NOT NULL DEFAULT 3,
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS config_backups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS response_cache (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
ttl INTEGER NOT NULL DEFAULT 3600
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS generation_cache (
|
||||
project_id TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chapter_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id TEXT NOT NULL,
|
||||
chapter_num INTEGER NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
generated_at TEXT NOT NULL,
|
||||
UNIQUE(project_id, chapter_num)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
genre TEXT NOT NULL DEFAULT '',
|
||||
sub_genres TEXT NOT NULL DEFAULT '[]',
|
||||
character_setting TEXT NOT NULL DEFAULT '',
|
||||
world_setting TEXT NOT NULL DEFAULT '',
|
||||
plot_idea TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chapters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id TEXT NOT NULL,
|
||||
num INTEGER NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
desc TEXT NOT NULL DEFAULT '',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
word_count INTEGER NOT NULL DEFAULT 0,
|
||||
generated_at TEXT,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
UNIQUE(project_id, num)
|
||||
);
|
||||
""")
|
||||
|
||||
# Đảm bảo schema cũ được cập nhật
|
||||
try:
|
||||
conn.execute("ALTER TABLE projects ADD COLUMN sub_genres TEXT NOT NULL DEFAULT '[]'")
|
||||
except sqlite3.OperationalError:
|
||||
pass # Đã có cột
|
||||
|
||||
conn.commit()
|
||||
logger.info("Database tables initialized")
|
||||
|
||||
|
||||
def migrate_from_files() -> str:
|
||||
"""
|
||||
Đọc dữ liệu cũ từ file JSON → insert vào DB.
|
||||
Không xóa file cũ (giữ lại để phòng lỗi).
|
||||
|
||||
Returns:
|
||||
Báo cáo migration
|
||||
"""
|
||||
conn = get_db()
|
||||
report = []
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
# 1. Migrate config
|
||||
config_file = os.path.join("config", "novel_tool_config.json")
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Migrate backends
|
||||
backends = data.get("backends", [])
|
||||
migrated_backends = 0
|
||||
for b in backends:
|
||||
try:
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO backends
|
||||
(name, type, base_url, api_key, model, enabled, timeout, retry_times, is_default, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
b.get("name", ""),
|
||||
b.get("type", "openai"),
|
||||
b.get("base_url", ""),
|
||||
b.get("api_key", ""),
|
||||
b.get("model", ""),
|
||||
1 if b.get("enabled", True) else 0,
|
||||
b.get("timeout", 30),
|
||||
b.get("retry_times", 3),
|
||||
1 if b.get("is_default", False) else 0,
|
||||
now, now
|
||||
))
|
||||
migrated_backends += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Migrate backend failed: {e}")
|
||||
|
||||
# Migrate generation config
|
||||
gen = data.get("generation", {})
|
||||
if gen:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
|
||||
("generation", json.dumps(gen, ensure_ascii=False), now)
|
||||
)
|
||||
|
||||
# Migrate version
|
||||
version = data.get("version", "4.0.0")
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
|
||||
("version", version, now)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
report.append(f"✅ Config: {migrated_backends} backends migrated")
|
||||
except Exception as e:
|
||||
report.append(f"❌ Config migration failed: {e}")
|
||||
else:
|
||||
report.append("⏭ Config file not found, skipped")
|
||||
|
||||
# 2. Migrate config backups
|
||||
backup_dir = os.path.join("config", "backups")
|
||||
if os.path.exists(backup_dir):
|
||||
migrated_backups = 0
|
||||
for fname in os.listdir(backup_dir):
|
||||
fpath = os.path.join(backup_dir, fname)
|
||||
if fname.endswith(".json") and os.path.isfile(fpath):
|
||||
try:
|
||||
with open(fpath, "r", encoding="utf-8") as f:
|
||||
backup_data = f.read()
|
||||
# Extract timestamp from filename if possible
|
||||
created = now
|
||||
if fname.startswith("backup_"):
|
||||
parts = fname.replace("backup_", "").replace(".json", "")
|
||||
try:
|
||||
created = datetime.strptime(parts, "%Y%m%d_%H%M%S").isoformat()
|
||||
except ValueError:
|
||||
pass
|
||||
conn.execute(
|
||||
"INSERT INTO config_backups (data, created_at) VALUES (?, ?)",
|
||||
(backup_data, created)
|
||||
)
|
||||
migrated_backups += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Migrate backup {fname} failed: {e}")
|
||||
conn.commit()
|
||||
report.append(f"✅ Config backups: {migrated_backups} backups migrated")
|
||||
|
||||
# 3. Migrate response cache
|
||||
cache_file = os.path.join("cache", "response_cache.json")
|
||||
if os.path.exists(cache_file):
|
||||
try:
|
||||
with open(cache_file, "r", encoding="utf-8") as f:
|
||||
cache_data = json.load(f)
|
||||
migrated_cache = 0
|
||||
for k, v in cache_data.items():
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO response_cache (key, value, timestamp, ttl) VALUES (?, ?, ?, ?)",
|
||||
(k, v.get("value", ""), v.get("timestamp", now), int(v.get("ttl", 3600)))
|
||||
)
|
||||
migrated_cache += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Migrate cache entry failed: {e}")
|
||||
conn.commit()
|
||||
report.append(f"✅ Response cache: {migrated_cache} entries migrated")
|
||||
except Exception as e:
|
||||
report.append(f"❌ Response cache migration failed: {e}")
|
||||
else:
|
||||
report.append("⏭ Response cache not found, skipped")
|
||||
|
||||
# 4. Migrate generation cache
|
||||
gen_cache_dir = Path("cache/generation")
|
||||
if gen_cache_dir.exists():
|
||||
migrated_gen = 0
|
||||
for cache_file in gen_cache_dir.glob("*.json"):
|
||||
try:
|
||||
with open(cache_file, "r", encoding="utf-8") as f:
|
||||
gen_data = f.read()
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO generation_cache (project_id, data, updated_at) VALUES (?, ?, ?)",
|
||||
(cache_file.stem, gen_data, now)
|
||||
)
|
||||
migrated_gen += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Migrate generation cache {cache_file.name} failed: {e}")
|
||||
conn.commit()
|
||||
report.append(f"✅ Generation cache: {migrated_gen} entries migrated")
|
||||
|
||||
# 5. Migrate chapter summaries
|
||||
summary_dir = Path("cache/summaries")
|
||||
if summary_dir.exists():
|
||||
migrated_summaries = 0
|
||||
for project_dir in summary_dir.iterdir():
|
||||
if not project_dir.is_dir():
|
||||
continue
|
||||
for summary_file in project_dir.glob("*.json"):
|
||||
try:
|
||||
with open(summary_file, "r", encoding="utf-8") as f:
|
||||
summary_data = json.load(f)
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO chapter_summaries
|
||||
(project_id, chapter_num, summary, generated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (
|
||||
project_dir.name,
|
||||
summary_data.get("chapter_num", int(summary_file.stem)),
|
||||
summary_data.get("summary", ""),
|
||||
summary_data.get("generated_at", now)
|
||||
))
|
||||
migrated_summaries += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Migrate summary {summary_file} failed: {e}")
|
||||
conn.commit()
|
||||
report.append(f"✅ Chapter summaries: {migrated_summaries} entries migrated")
|
||||
|
||||
# 6. Migrate projects
|
||||
projects_dir = "projects"
|
||||
if os.path.exists(projects_dir):
|
||||
migrated_projects = 0
|
||||
for project_id in os.listdir(projects_dir):
|
||||
project_path = os.path.join(projects_dir, project_id)
|
||||
if not os.path.isdir(project_path):
|
||||
continue
|
||||
metadata_file = os.path.join(project_path, "metadata.json")
|
||||
if not os.path.exists(metadata_file):
|
||||
continue
|
||||
try:
|
||||
with open(metadata_file, "r", encoding="utf-8") as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO projects
|
||||
(id, title, genre, character_setting, world_setting, plot_idea, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
metadata.get("id", project_id),
|
||||
metadata.get("title", ""),
|
||||
metadata.get("genre", ""),
|
||||
metadata.get("character_setting", ""),
|
||||
metadata.get("world_setting", ""),
|
||||
metadata.get("plot_idea", ""),
|
||||
metadata.get("created_at", now),
|
||||
metadata.get("updated_at", now)
|
||||
))
|
||||
|
||||
# Migrate chapters
|
||||
for ch in metadata.get("chapters", []):
|
||||
try:
|
||||
conn.execute("""
|
||||
INSERT OR IGNORE INTO chapters
|
||||
(project_id, num, title, desc, content, word_count, generated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
metadata.get("id", project_id),
|
||||
ch.get("num", 0),
|
||||
ch.get("title", ""),
|
||||
ch.get("desc", ""),
|
||||
ch.get("content", ""),
|
||||
ch.get("word_count", 0),
|
||||
ch.get("generated_at")
|
||||
))
|
||||
except Exception as e:
|
||||
logger.warning(f"Migrate chapter {ch.get('num')} failed: {e}")
|
||||
|
||||
migrated_projects += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"Migrate project {project_id} failed: {e}")
|
||||
|
||||
conn.commit()
|
||||
report.append(f"✅ Projects: {migrated_projects} projects migrated")
|
||||
else:
|
||||
report.append("⏭ Projects directory not found, skipped")
|
||||
|
||||
result = "\n".join(report)
|
||||
logger.info(f"Migration complete:\n{result}")
|
||||
return result
|
||||
Reference in New Issue
Block a user