first commit

This commit is contained in:
Mai-Vu Tran
2026-03-16 18:14:44 +07:00
commit 6699c516cd
31 changed files with 9946 additions and 0 deletions
+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 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 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()