refactor: Restructure project into core, services, ui, and utils directories, moving existing modules and adding new UI tabs.
This commit is contained in:
+646
@@ -0,0 +1,646 @@
|
||||
"""
|
||||
Mô-đun Quản lý cấu hình - Hỗ trợ mã hóa thông tin nhạy cảm, quản lý phiên bản, xác thực
|
||||
|
||||
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 json
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
from locales.i18n import t
|
||||
from core.database import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Giới hạn kích thước tệp tối đa (50MB)
|
||||
MAX_FILE_SIZE = 50 * 1024 * 1024
|
||||
|
||||
# Các định dạng tệp cấu hình được hỗ trợ
|
||||
SUPPORTED_CONFIG_FORMATS = [".json", ".yaml", ".yml"]
|
||||
|
||||
# Cấu hình nhà cung cấp API
|
||||
API_PROVIDERS = {
|
||||
"openai": {
|
||||
"name": "OpenAI",
|
||||
"default_model": "gpt-4o-mini",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "OpenAI API chính thức"
|
||||
},
|
||||
"openai_compatible": {
|
||||
"name": "OpenAI (Giao diện tương thích)",
|
||||
"default_model": "gpt-3.5-turbo",
|
||||
"base_url": "",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": True,
|
||||
"description": "Dịch vụ bên thứ ba tương thích với định dạng OpenAI API"
|
||||
},
|
||||
"anthropic": {
|
||||
"name": "Anthropic",
|
||||
"default_model": "claude-3-5-sonnet-20241022",
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"api_key_field": "x-api-key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Mô hình Claude"
|
||||
},
|
||||
"google": {
|
||||
"name": "Google",
|
||||
"default_model": "gemini-1.5-pro",
|
||||
"base_url": "https://generativelanguage.googleapis.com",
|
||||
"api_key_field": "key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Mô hình Gemini"
|
||||
},
|
||||
"alibaba": {
|
||||
"name": "Alibaba DashScope",
|
||||
"default_model": "qwen-turbo",
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Dòng Qwen"
|
||||
},
|
||||
"deepseek": {
|
||||
"name": "DeepSeek",
|
||||
"default_model": "deepseek-chat",
|
||||
"base_url": "https://api.deepseek.com/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "DeepSeek-V3"
|
||||
},
|
||||
"zhipu": {
|
||||
"name": "Zhipu AI",
|
||||
"default_model": "glm-4",
|
||||
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Dòng GLM"
|
||||
},
|
||||
"groq": {
|
||||
"name": "Groq",
|
||||
"default_model": "llama3-70b-8192",
|
||||
"base_url": "https://api.groq.com/openai/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Llama3, Mixtral"
|
||||
},
|
||||
"together": {
|
||||
"name": "Together AI",
|
||||
"default_model": "meta-llama/Llama-3-70b-chat-hf",
|
||||
"base_url": "https://api.together.xyz/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Llama, Qwen"
|
||||
},
|
||||
"fireworks": {
|
||||
"name": "Fireworks AI",
|
||||
"default_model": "accounts/fireworks/models/llama-v3-70b-instruct",
|
||||
"base_url": "https://api.fireworks.ai/inference/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Llama, Mixtral"
|
||||
},
|
||||
"mistral": {
|
||||
"name": "Mistral AI",
|
||||
"default_model": "mistral-large-latest",
|
||||
"base_url": "https://api.mistral.ai/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Mistral Large, Pixtral"
|
||||
},
|
||||
"openrouter": {
|
||||
"name": "OpenRouter",
|
||||
"default_model": "anthropic/claude-3.5-sonnet",
|
||||
"base_url": "https://openrouter.ai/api/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Tổng hợp đa mô hình (GPT, Claude, v.v.)"
|
||||
},
|
||||
"deepinfra": {
|
||||
"name": "DeepInfra",
|
||||
"default_model": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
||||
"base_url": "https://api.deepinfra.com/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Lưu trữ mô hình mã nguồn mở"
|
||||
},
|
||||
"anyscale": {
|
||||
"name": "Anyscale Endpoints",
|
||||
"default_model": "meta-llama/Llama-3-70b-chat-hf",
|
||||
"base_url": "https://api.endpoints.anyscale.com/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Llama, Mistral"
|
||||
},
|
||||
"perplexity": {
|
||||
"name": "Perplexity AI",
|
||||
"default_model": "llama-3.1-sonar-small-128k-online",
|
||||
"base_url": "https://api.perplexity.ai",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Sonar, Llama"
|
||||
},
|
||||
"hyperbolic": {
|
||||
"name": "Hyperbolic",
|
||||
"default_model": "meta-llama/Meta-Llama-3.1-70B-Instruct",
|
||||
"base_url": "https://api.hyperbolic.xyz/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Mô hình mã nguồn mở"
|
||||
},
|
||||
"siliconflow": {
|
||||
"name": "SiliconFlow",
|
||||
"default_model": "Qwen/Qwen2.5-72B-Instruct",
|
||||
"base_url": "https://api.siliconflow.cn/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Qwen, Llama"
|
||||
},
|
||||
"moonshot": {
|
||||
"name": "Moonshot AI (Kimi)",
|
||||
"default_model": "moonshot-v1-8k",
|
||||
"base_url": "https://api.moonshot.ai/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Dòng Kimi"
|
||||
},
|
||||
"novita": {
|
||||
"name": "Novita AI",
|
||||
"default_model": "meta-llama/Meta-Llama-3.1-70B-Instruct",
|
||||
"base_url": "https://api.novita.ai/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Lưu trữ mô hình mã nguồn mở"
|
||||
},
|
||||
"baichuan": {
|
||||
"name": "Baichuan AI",
|
||||
"default_model": "Baichuan4",
|
||||
"base_url": "https://api.baichuan-ai.com/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Dòng Baichuan"
|
||||
},
|
||||
"cerebras": {
|
||||
"name": "Cerebras",
|
||||
"default_model": "llama3.1-70b",
|
||||
"base_url": "https://api.cerebras.ai/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Dòng Llama"
|
||||
},
|
||||
"sambanova": {
|
||||
"name": "SambaNova",
|
||||
"default_model": "Meta-Llama-3.1-70B-Instruct",
|
||||
"base_url": "https://api.sambanova.ai/v1",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Dòng Llama"
|
||||
},
|
||||
"volcengine": {
|
||||
"name": "Volcengine (Doubao)",
|
||||
"default_model": "doubao-pro-4k",
|
||||
"base_url": "https://ark.volcengine.com/api/v3",
|
||||
"api_key_field": "api_key",
|
||||
"requires_custom_url": False,
|
||||
"description": "Dòng Doubao"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Backend:
|
||||
"""Lớp dữ liệu cấu hình phụ trợ"""
|
||||
name: str
|
||||
type: str
|
||||
base_url: str
|
||||
api_key: str
|
||||
model: str
|
||||
enabled: bool = True
|
||||
timeout: int = 120
|
||||
retry_times: int = 3
|
||||
is_default: bool = False
|
||||
|
||||
def validate(self) -> tuple[bool, str]:
|
||||
"""Xác minh tính hợp lệ của cấu hình"""
|
||||
if not self.name or not self.name.strip():
|
||||
return False, t("config.backend_name_empty")
|
||||
if self.type not in ["ollama", "openai", "claude", "other"]:
|
||||
return False, t("config.unsupported_type", type=self.type)
|
||||
if not self.base_url or not self.base_url.strip().startswith(("http://", "https://")):
|
||||
return False, t("config.base_url_invalid")
|
||||
# loại ollama cho phép api_key trống
|
||||
if self.type != "ollama" and (not self.api_key or not self.api_key.strip()):
|
||||
return False, t("config.api_key_empty")
|
||||
if not self.model or not self.model.strip():
|
||||
return False, t("config.model_empty")
|
||||
if self.timeout < 5 or self.timeout > 10000:
|
||||
return False, t("config.timeout_range")
|
||||
if self.retry_times < 1 or self.retry_times > 10:
|
||||
return False, t("config.retry_range")
|
||||
return True, "OK"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GenerationConfig:
|
||||
"""Cấu hình tham số tạo"""
|
||||
temperature: float = 0.7
|
||||
top_p: float = 0.9
|
||||
top_k: int = 40
|
||||
max_tokens: int = 16384
|
||||
chapter_target_words: int = 4000
|
||||
writing_style: str = "Trôi chảy tự nhiên, tình tiết chặt chẽ, miêu tả nhân vật tinh tế"
|
||||
writing_tone: str = "Trung lập"
|
||||
character_development: str = "Chi tiết"
|
||||
plot_complexity: str = "Trung bình"
|
||||
|
||||
def validate(self) -> tuple[bool, str]:
|
||||
"""Xác minh tính hợp lệ của các tham số"""
|
||||
if not 0.1 <= self.temperature <= 2.0:
|
||||
return False, t("config.temp_range")
|
||||
if not 0.1 <= self.top_p <= 1.0:
|
||||
return False, t("config.top_p_range")
|
||||
if self.max_tokens < 100 or self.max_tokens > 100000:
|
||||
return False, t("config.max_tokens_range")
|
||||
if self.chapter_target_words < 500 or self.chapter_target_words > 65536:
|
||||
return False, t("config.chapter_words_range")
|
||||
return True, "OK"
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""Trình quản lý cấu hình - Chế độ đơn"""
|
||||
_instance: Optional["ConfigManager"] = None
|
||||
|
||||
def __new__(cls) -> "ConfigManager":
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self.backends: List[Backend] = []
|
||||
self.generation: GenerationConfig = GenerationConfig()
|
||||
self.version: str = "4.0.0"
|
||||
self.last_modified: str = datetime.now().isoformat()
|
||||
self._load()
|
||||
self._initialized = True
|
||||
|
||||
def _load(self) -> None:
|
||||
"""Tải cấu hình từ SQLite"""
|
||||
try:
|
||||
conn = get_db()
|
||||
|
||||
# Tải backends
|
||||
rows = conn.execute("SELECT * FROM backends ORDER BY id").fetchall()
|
||||
if rows:
|
||||
for row in rows:
|
||||
try:
|
||||
backend = Backend(
|
||||
name=row["name"],
|
||||
type=row["type"],
|
||||
base_url=row["base_url"],
|
||||
api_key=row["api_key"],
|
||||
model=row["model"],
|
||||
enabled=bool(row["enabled"]),
|
||||
timeout=row["timeout"],
|
||||
retry_times=row["retry_times"],
|
||||
is_default=bool(row["is_default"])
|
||||
)
|
||||
valid, msg = backend.validate()
|
||||
if valid:
|
||||
self.backends.append(backend)
|
||||
else:
|
||||
logger.warning(f"Skip invalid backend {backend.name}: {msg}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Load backend config failed: {e}")
|
||||
|
||||
# Tải generation config
|
||||
gen_row = conn.execute("SELECT value FROM config WHERE key = 'generation'").fetchone()
|
||||
if gen_row:
|
||||
try:
|
||||
gen_data = json.loads(gen_row["value"])
|
||||
gen_data = {k: v for k, v in gen_data.items()
|
||||
if k in GenerationConfig.__dataclass_fields__}
|
||||
self.generation = GenerationConfig(**gen_data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Load generation config failed: {e}")
|
||||
|
||||
# Tải version
|
||||
ver_row = conn.execute("SELECT value FROM config WHERE key = 'version'").fetchone()
|
||||
if ver_row:
|
||||
self.version = ver_row["value"]
|
||||
|
||||
mod_row = conn.execute("SELECT value FROM config WHERE key = 'last_modified'").fetchone()
|
||||
if mod_row:
|
||||
self.last_modified = mod_row["value"]
|
||||
|
||||
if rows or gen_row:
|
||||
logger.info("Config loaded from core.database")
|
||||
else:
|
||||
logger.info("No config in database, using defaults")
|
||||
self._init_default()
|
||||
except Exception as e:
|
||||
logger.error(f"Config load failed: {e}")
|
||||
self._init_default()
|
||||
|
||||
def _init_default(self) -> None:
|
||||
"""Khởi tạo cấu hình mặc định"""
|
||||
self.backends = [
|
||||
Backend(
|
||||
name=t("config.default_backend_name"),
|
||||
type="ollama",
|
||||
base_url="http://localhost:11434/v1",
|
||||
api_key="ollama",
|
||||
model="llama3.1:latest"
|
||||
)
|
||||
]
|
||||
self.generation = GenerationConfig()
|
||||
self.save()
|
||||
|
||||
def save(self) -> tuple[bool, str]:
|
||||
"""Lưu cấu hình vào SQLite"""
|
||||
try:
|
||||
conn = get_db()
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
# Tạo bản sao lưu
|
||||
backup_data = {
|
||||
"version": self.version,
|
||||
"last_modified": self.last_modified,
|
||||
"backends": [asdict(b) for b in self.backends],
|
||||
"generation": asdict(self.generation),
|
||||
}
|
||||
conn.execute(
|
||||
"INSERT INTO config_backups (data, created_at) VALUES (?, ?)",
|
||||
(json.dumps(backup_data, ensure_ascii=False), now)
|
||||
)
|
||||
|
||||
# Xóa backends cũ và insert lại
|
||||
conn.execute("DELETE FROM backends")
|
||||
for b in self.backends:
|
||||
conn.execute("""
|
||||
INSERT INTO backends
|
||||
(name, type, base_url, api_key, model, enabled, timeout, retry_times, is_default, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
b.name, b.type, b.base_url, b.api_key, b.model,
|
||||
1 if b.enabled else 0, b.timeout, b.retry_times,
|
||||
1 if b.is_default else 0, now, now
|
||||
))
|
||||
|
||||
# Lưu generation config
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
|
||||
("generation", json.dumps(asdict(self.generation), ensure_ascii=False), now)
|
||||
)
|
||||
|
||||
# Lưu version + last_modified
|
||||
self.last_modified = now
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
|
||||
("version", self.version, now)
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
|
||||
("last_modified", now, now)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
logger.info("Config saved to database")
|
||||
return True, t("config.config_save_success")
|
||||
except Exception as e:
|
||||
logger.error(f"Config save failed: {e}")
|
||||
return False, t("config.config_save_failed", error=str(e))
|
||||
|
||||
def add_backend(self, backend: Backend) -> tuple[bool, str]:
|
||||
"""Thêm backend"""
|
||||
valid, msg = backend.validate()
|
||||
if not valid:
|
||||
return False, msg
|
||||
|
||||
# Kiểm tra sự trùng lặp
|
||||
if any(b.name == backend.name for b in self.backends):
|
||||
return False, t("config.backend_exists", name=backend.name)
|
||||
|
||||
self.backends.append(backend)
|
||||
success, msg = self.save()
|
||||
return success, msg if not success else t("config.backend_add_success")
|
||||
|
||||
def update_backend(self, name: str, **kwargs) -> tuple[bool, str]:
|
||||
"""Cập nhật cấu hình phụ trợ"""
|
||||
for backend in self.backends:
|
||||
if backend.name == name:
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(backend, key):
|
||||
setattr(backend, key, value)
|
||||
|
||||
valid, msg = backend.validate()
|
||||
if not valid:
|
||||
return False, msg
|
||||
|
||||
success, msg = self.save()
|
||||
return success, msg if not success else t("config.backend_update_success")
|
||||
|
||||
return False, t("config.backend_not_found", name=name)
|
||||
|
||||
def delete_backend(self, name: str) -> tuple[bool, str]:
|
||||
"""Xóa backend"""
|
||||
self.backends = [b for b in self.backends if b.name != name]
|
||||
success, msg = self.save()
|
||||
return success, msg if not success else t("config.backend_delete_success", name=name)
|
||||
|
||||
def set_default_backend(self, name: str) -> tuple[bool, str]:
|
||||
"""Đặt giao diện làm mặc định"""
|
||||
found = False
|
||||
for backend in self.backends:
|
||||
if backend.name == name:
|
||||
backend.is_default = True
|
||||
found = True
|
||||
else:
|
||||
backend.is_default = False
|
||||
|
||||
if found:
|
||||
success, msg = self.save()
|
||||
return success, msg if not success else "Success"
|
||||
return False, t("config.backend_not_found", name=name)
|
||||
|
||||
def get_enabled_backends(self) -> List[Backend]:
|
||||
"""Nhận tất cả các phụ trợ được kích hoạt"""
|
||||
return [b for b in self.backends if b.enabled]
|
||||
|
||||
def update_generation_config(self, **kwargs) -> tuple[bool, str]:
|
||||
"""Cập nhật cấu hình bản dựng"""
|
||||
for key, value in kwargs.items():
|
||||
if hasattr(self.generation, key):
|
||||
setattr(self.generation, key, value)
|
||||
|
||||
valid, msg = self.generation.validate()
|
||||
if not valid:
|
||||
return False, msg
|
||||
|
||||
success, msg = self.save()
|
||||
return success, msg if not success else t("config.gen_params_update_success")
|
||||
|
||||
def export_config(self, filepath: str) -> tuple[bool, str]:
|
||||
"""Xuất cấu hình (không chứa thông tin nhạy cảm)"""
|
||||
try:
|
||||
data = {
|
||||
"version": self.version,
|
||||
"backends": [{"name": b.name, "type": b.type, "model": b.model}
|
||||
for b in self.backends],
|
||||
"generation": asdict(self.generation),
|
||||
}
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
return True, t("config.config_export_success", filepath=filepath)
|
||||
except Exception as e:
|
||||
return False, t("config.config_export_failed", error=str(e))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Chuyển đổi cấu hình sang định dạng từ điển"""
|
||||
return {
|
||||
"backends": [asdict(b) for b in self.backends],
|
||||
"generation": asdict(self.generation),
|
||||
"system": {
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"file": "logs/novel_generator.log",
|
||||
"console_output": True
|
||||
},
|
||||
"concurrency": {
|
||||
"max_workers": 4,
|
||||
"request_timeout": 30
|
||||
},
|
||||
"cache": {
|
||||
"enabled": True,
|
||||
"type": "file",
|
||||
"location": "cache",
|
||||
"ttl": 3600
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"default_format": "markdown",
|
||||
"output_directory": "output",
|
||||
"supported_formats": ["markdown", "pdf", "docx", "txt", "epub"]
|
||||
},
|
||||
"ui": {
|
||||
"theme": "light",
|
||||
"language": "zh-CN",
|
||||
"editor": {
|
||||
"font_size": 14,
|
||||
"font_family": "Microsoft YaHei, sans-serif",
|
||||
"tab_size": 2,
|
||||
"word_wrap": True
|
||||
}
|
||||
},
|
||||
"project": {
|
||||
"auto_save": {
|
||||
"enabled": True,
|
||||
"interval": 300,
|
||||
"backup_count": 5
|
||||
},
|
||||
"backup": {
|
||||
"enabled": True,
|
||||
"location": "backups",
|
||||
"schedule": "daily",
|
||||
"keep_days": 30
|
||||
},
|
||||
"templates": {
|
||||
"enabled": True,
|
||||
"location": "project_templates",
|
||||
"default_template": "standard_novel"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"enabled": True,
|
||||
"directory": "plugins",
|
||||
"auto_load": True,
|
||||
"enabled_plugins": [
|
||||
"style_analyzer",
|
||||
"grammar_checker",
|
||||
"character_tracker",
|
||||
"plot_generator"
|
||||
]
|
||||
},
|
||||
"advanced": {
|
||||
"performance": {
|
||||
"enable_profiling": False,
|
||||
"memory_limit": "1GB",
|
||||
"cpu_limit": 80
|
||||
},
|
||||
"debug": {
|
||||
"show_errors": False,
|
||||
"debug_mode": False,
|
||||
"trace_requests": False
|
||||
},
|
||||
"monitoring": {
|
||||
"enabled": False,
|
||||
"metrics_port": 8080,
|
||||
"health_check_interval": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_api_providers() -> Dict[str, Dict[str, Any]]:
|
||||
"""Nhận tất cả cấu hình của nhà cung cấp API"""
|
||||
return API_PROVIDERS
|
||||
|
||||
@staticmethod
|
||||
def get_api_provider_choices() -> List[str]:
|
||||
"""Nhận danh sách lựa chọn nhà cung cấp API"""
|
||||
return [provider["name"] for provider in API_PROVIDERS.values()]
|
||||
|
||||
@staticmethod
|
||||
def get_api_provider_info(provider_key: str) -> Optional[Dict[str, Any]]:
|
||||
"""Nhận thông tin nhà cung cấp dựa trên khóa nhà cung cấp"""
|
||||
return API_PROVIDERS.get(provider_key)
|
||||
|
||||
@staticmethod
|
||||
def get_api_provider_key_by_name(provider_name: str) -> Optional[str]:
|
||||
"""Nhận khóa nhà cung cấp dựa trên tên nhà cung cấp"""
|
||||
for key, provider in API_PROVIDERS.items():
|
||||
if provider["name"] == provider_name:
|
||||
return key
|
||||
return None
|
||||
|
||||
|
||||
def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Tải cấu hình từ database hoặc file chỉ định
|
||||
|
||||
Args:
|
||||
config_path: đường dẫn file cấu hình (hỗ trợ import từ file)
|
||||
|
||||
Returns:
|
||||
Từ điển cấu hình
|
||||
"""
|
||||
if config_path:
|
||||
# Import từ file chỉ định
|
||||
if not os.path.exists(config_path):
|
||||
raise FileNotFoundError(t("config.config_file_missing", path=config_path))
|
||||
|
||||
file_ext = os.path.splitext(config_path)[1].lower()
|
||||
if file_ext == ".json":
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
raise ValueError(t("config.config_format_unsupported", ext=file_ext))
|
||||
else:
|
||||
return get_config().to_dict()
|
||||
|
||||
def get_config() -> ConfigManager:
|
||||
"""Nhận phiên bản cấu hình toàn cầu"""
|
||||
return ConfigManager()
|
||||
|
||||
def get_config_manager() -> ConfigManager:
|
||||
"""Nhận phiên bản trình quản lý cấu hình toàn cầu (bí danh)"""
|
||||
return get_config()
|
||||
@@ -0,0 +1,300 @@
|
||||
"""
|
||||
Mô-đun Quản lý cấu hình Web API
|
||||
Hỗ trợ thêm, sửa, xóa, kiểm tra giao diện API qua Web UI
|
||||
|
||||
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 json
|
||||
from typing import Dict, List, Tuple, Any
|
||||
from dataclasses import asdict
|
||||
from core.config import Backend, get_config
|
||||
from services.api_client import get_api_client, reinit_api_client
|
||||
from core.logger import get_logger
|
||||
from locales.i18n import t
|
||||
|
||||
logger = get_logger("ConfigAPI")
|
||||
|
||||
|
||||
class ConfigAPIManager:
|
||||
"""API quản lý cấu hình"""
|
||||
|
||||
@staticmethod
|
||||
def list_backends() -> Dict[str, Any]:
|
||||
"""Nhận danh sách tất cả các phụ trợ"""
|
||||
try:
|
||||
config = get_config()
|
||||
backends_data = []
|
||||
for backend in config.backends:
|
||||
backend_dict = asdict(backend)
|
||||
backends_data.append(backend_dict)
|
||||
return {
|
||||
"success": True,
|
||||
"data": backends_data,
|
||||
"message": t("app.backends_loaded", count=len(backends_data))
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"List backends failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"data": [],
|
||||
"message": f"❌ {str(e)}"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def add_backend(name: str, type: str, base_url: str, api_key: str,
|
||||
model: str, timeout: int = 30, retry_times: int = 3,
|
||||
enabled: bool = True) -> Dict[str, Any]:
|
||||
"""Thêm cấu hình phụ trợ mới"""
|
||||
try:
|
||||
config = get_config()
|
||||
|
||||
# Kiểm tra xem tên có trùng lặp không
|
||||
for backend in config.backends:
|
||||
if backend.name == name:
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.name_exists", name=name)
|
||||
}
|
||||
|
||||
# Tạo phụ trợ mới
|
||||
new_backend = Backend(
|
||||
name=name,
|
||||
type=type,
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
timeout=timeout,
|
||||
retry_times=retry_times,
|
||||
enabled=enabled
|
||||
)
|
||||
|
||||
# Xác minh cấu hình phụ trợ
|
||||
valid, msg = new_backend.validate()
|
||||
if not valid:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"❌ {msg}"
|
||||
}
|
||||
|
||||
# Thêm phụ trợ
|
||||
config.backends.append(new_backend)
|
||||
success, save_msg = config.save()
|
||||
|
||||
if success:
|
||||
logger.info(f"Backend added: {name}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": t("config_api.add_success", name=name),
|
||||
"backend": asdict(new_backend)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.add_failed", error=save_msg)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Add backend failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.add_failed", error=str(e))
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def update_backend(name: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Cập nhật cấu hình backend"""
|
||||
try:
|
||||
config = get_config()
|
||||
success, msg = config.update_backend(name, **kwargs)
|
||||
|
||||
if success:
|
||||
logger.info(f"Backend updated: {name}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": msg
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": msg
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Update backend failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.update_failed", error=str(e))
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def delete_backend(name: str) -> Dict[str, Any]:
|
||||
"""Xóa cấu hình phụ trợ"""
|
||||
try:
|
||||
config = get_config()
|
||||
success, msg = config.delete_backend(name)
|
||||
|
||||
if success:
|
||||
logger.info(f"Backend deleted: {name}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": msg
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": msg
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Delete backend failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.delete_failed", error=str(e))
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def toggle_backend(name: str, enabled: bool) -> Dict[str, Any]:
|
||||
"""Bật/tắt phụ trợ"""
|
||||
try:
|
||||
config = get_config()
|
||||
success, msg = config.update_backend(name, enabled=enabled)
|
||||
|
||||
if success:
|
||||
status = t("config_api.toggle_enabled") if enabled else t("config_api.toggle_disabled")
|
||||
logger.info(f"Backend {name}: {status}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": t("config_api.toggle_success", name=name, status=status)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": msg
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Toggle backend failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.toggle_failed", error=str(e))
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def set_default_backend(name: str) -> Dict[str, Any]:
|
||||
"""Đặt giao diện làm mặc định"""
|
||||
try:
|
||||
config = get_config()
|
||||
success, msg = config.set_default_backend(name)
|
||||
|
||||
if success:
|
||||
logger.info(f"Backend set to default: {name}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": t("config_api.default_success", name=name)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": msg
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Set default backend failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.default_failed", error=str(e))
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def test_backend(name: str) -> Dict[str, Any]:
|
||||
"""Kiểm tra kết nối backend"""
|
||||
try:
|
||||
config = get_config()
|
||||
backend = None
|
||||
|
||||
# Tìm phần phụ trợ được chỉ định
|
||||
for b in config.backends:
|
||||
if b.name == name:
|
||||
backend = b
|
||||
break
|
||||
|
||||
if not backend:
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.test_not_found", name=name)
|
||||
}
|
||||
|
||||
# kết nối thử nghiệm
|
||||
if not backend.enabled:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"❌ {name} disabled"
|
||||
}
|
||||
|
||||
# Kiểm tra bằng ứng dụng khách API
|
||||
try:
|
||||
api_client = get_api_client()
|
||||
# Hãy thử lấy thông tin model để kiểm tra kết nối
|
||||
test_response = api_client.test_connection(backend.base_url, backend.api_key, backend.model)
|
||||
|
||||
if test_response:
|
||||
logger.info(f"Backend test passed: {name}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": t("config_api.test_success", name=name),
|
||||
"backend": name,
|
||||
"model": backend.model
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.test_failed", name=name, error="No response")
|
||||
}
|
||||
except Exception as test_error:
|
||||
logger.error(f"Backend test error: {test_error}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.test_failed", name=name, error=str(test_error))
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Test backend failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.test_failed", name="", error=str(e))
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_backend_types() -> List[str]:
|
||||
"""Nhận danh sách các loại phụ trợ được hỗ trợ"""
|
||||
return ["ollama", "openai", "claude", "other"]
|
||||
|
||||
@staticmethod
|
||||
def export_config(filepath: str) -> Dict[str, Any]:
|
||||
"""Xuất file cấu hình"""
|
||||
try:
|
||||
config = get_config()
|
||||
success, msg = config.export_config(filepath)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": msg
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": msg
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Export config failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": t("config_api.export_failed", error=str(e))
|
||||
}
|
||||
|
||||
|
||||
# Phiên bản trình quản lý API toàn cầu
|
||||
config_api = ConfigAPIManager()
|
||||
@@ -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
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Mô-đun theo dõi và nhật ký - Hệ thống nhật ký cấp sản xuất
|
||||
|
||||
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 logging
|
||||
import logging.handlers
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
LOG_DIR = "logs"
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
# Cấu hình tệp nhật ký
|
||||
LOG_FILE = os.path.join(LOG_DIR, f"novel_tool_{datetime.now().strftime('%Y%m%d')}.log")
|
||||
ERROR_LOG_FILE = os.path.join(LOG_DIR, f"errors_{datetime.now().strftime('%Y%m%d')}.log")
|
||||
|
||||
|
||||
def setup_logger(
|
||||
name: str,
|
||||
log_level: int = logging.INFO,
|
||||
log_to_file: bool = True,
|
||||
force_reconfigure: bool = False
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
Khởi tạo logger cấp sản xuất
|
||||
|
||||
Args:
|
||||
name: Tên logger
|
||||
log_level: Cấp độ nhật ký
|
||||
log_to_file: Có xuất ra tệp hay không
|
||||
force_reconfigure: Có bắt buộc cấu hình lại không (xóa các handler cũ)
|
||||
|
||||
Returns:
|
||||
Biến logger đã được cấu hình
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(log_level)
|
||||
|
||||
# Nếu bắt buộc cấu hình lại, xóa bỏ handler hiện có
|
||||
if force_reconfigure:
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
# Tránh thêm handler lặp lại
|
||||
elif logger.handlers:
|
||||
return logger
|
||||
|
||||
# Định dạng đầu ra bảng điều khiển
|
||||
console_formatter = logging.Formatter(
|
||||
'[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# Đầu ra tệp (nếu được bật)
|
||||
if log_to_file:
|
||||
file_formatter = logging.Formatter(
|
||||
'[%(asctime)s] [%(name)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
# Nhật ký chung
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
LOG_FILE,
|
||||
maxBytes=10*1024*1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Nhật ký lỗi - Luôn thêm vào, đảm bảo chắc chắn sẽ thu thập được nhật ký cấp độ ERROR
|
||||
error_handler = logging.handlers.RotatingFileHandler(
|
||||
ERROR_LOG_FILE,
|
||||
maxBytes=10*1024*1024,
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
error_handler.setFormatter(file_formatter)
|
||||
logger.addHandler(error_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
class PerformanceMonitor:
|
||||
"""Công cụ theo dõi hiệu suất"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = setup_logger("PerformanceMonitor")
|
||||
self.metrics = {}
|
||||
|
||||
def record_metric(self, name: str, value: float, unit: str = "ms") -> None:
|
||||
"""Ghi lại số liệu hiệu suất"""
|
||||
if name not in self.metrics:
|
||||
self.metrics[name] = []
|
||||
|
||||
self.metrics[name].append(value)
|
||||
|
||||
# Chỉ lưu 1000 bản ghi gần nhất
|
||||
if len(self.metrics[name]) > 1000:
|
||||
self.metrics[name] = self.metrics[name][-1000:]
|
||||
|
||||
def get_average(self, name: str) -> Optional[float]:
|
||||
"""Lấy giá trị trung bình"""
|
||||
if name not in self.metrics or not self.metrics[name]:
|
||||
return None
|
||||
return sum(self.metrics[name]) / len(self.metrics[name])
|
||||
|
||||
def report(self) -> str:
|
||||
"""Tạo báo cáo hiệu suất"""
|
||||
if not self.metrics:
|
||||
return "No performance data yet"
|
||||
|
||||
report = "=== Performance Report ===\n"
|
||||
for name, values in self.metrics.items():
|
||||
if values:
|
||||
avg = sum(values) / len(values)
|
||||
max_val = max(values)
|
||||
min_val = min(values)
|
||||
report += f"{name}: avg={avg:.2f}ms, max={max_val:.2f}ms, min={min_val:.2f}ms, count={len(values)}\n"
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# Phiên bản toàn cục
|
||||
_logger = setup_logger("NovelTool")
|
||||
_performance_monitor = PerformanceMonitor()
|
||||
|
||||
|
||||
def get_logger(name: str = "NovelTool") -> logging.Logger:
|
||||
"""Lấy biến instance của logger"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
def get_performance_monitor() -> PerformanceMonitor:
|
||||
"""Lấy trình theo dõi hiệu suất"""
|
||||
return _performance_monitor
|
||||
@@ -0,0 +1,21 @@
|
||||
import threading
|
||||
from typing import Optional
|
||||
from services.novel_generator import NovelGenerator, get_generator, NovelProject
|
||||
|
||||
class AppState:
|
||||
"""Trạng thái ứng dụng dùng chung giữa các tab"""
|
||||
def __init__(self):
|
||||
self.is_generating = False
|
||||
self.stop_requested = False
|
||||
self.lock = threading.Lock()
|
||||
self.current_project: Optional[NovelProject] = None
|
||||
self.generator: Optional[NovelGenerator] = None
|
||||
|
||||
def get_generator(self) -> NovelGenerator:
|
||||
"""Lấy hoặc tạo generator"""
|
||||
if self.generator is None:
|
||||
self.generator = get_generator()
|
||||
return self.generator
|
||||
|
||||
# Global app state singleton
|
||||
app_state = AppState()
|
||||
Reference in New Issue
Block a user