refactor: Restructure project into core, services, ui, and utils directories, moving existing modules and adding new UI tabs.

This commit is contained in:
tinix-ai
2026-03-17 14:54:16 +07:00
parent 6699c516cd
commit a40d8ef4a6
28 changed files with 1970 additions and 953 deletions
View File
+646
View File
@@ -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()
+300
View File
@@ -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()
+350
View File
@@ -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
View File
@@ -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
+21
View File
@@ -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()