feat: add cmhub AI backend

This commit is contained in:
chengma
2026-07-04 15:13:15 +08:00
parent c8a5e9ada8
commit 1efb095767
11 changed files with 1061 additions and 43 deletions
+93
View File
@@ -14,8 +14,10 @@ import urllib.request
CONFIG_PATH = "config.json"
AI_MODELS_PATH = os.path.join("config", "ai_models.json")
CMHUB_CONFIG_PATH = os.path.join("config", "cmhub.json")
CATEGORIES = {"text", "image"}
API_TYPES = {"chat", "images_edits", "auto"}
AI_BACKENDS = {"direct", "cmhub"}
DEFAULT_CONFIG = {
"chrome_path": r"C:\Program Files\Google\Chrome\Application\chrome.exe",
@@ -29,6 +31,14 @@ DEFAULT_CONFIG = {
"default_text_model": "GPT-5.5 文本",
"default_image_model": "Nano Banana 2",
"generate_cover": False,
"backend": "direct",
"cmhub": {
"base_url": "",
"title_alias": "",
"image_alias": "",
"connect_timeout": 10,
"check_balance_before_batch": False,
},
"title_concurrency": 4,
"image_concurrency": 4,
"retry": 2,
@@ -82,6 +92,10 @@ DEFAULT_AI_MODELS_CONFIG = {
]
}
DEFAULT_CMHUB_CONFIG = {
"api_key": "",
}
SECRET_FIELD_NAMES = {"api_key", "apikey", "key", "token", "password"}
@@ -183,6 +197,52 @@ def redact_secrets(text, secret_values=None) -> str:
return redacted
def default_cmhub_config() -> dict:
"""Return a new copy of the default cmhub key config."""
return copy.deepcopy(DEFAULT_CMHUB_CONFIG)
def _normalize_cmhub_config(config):
if config is None:
config = {}
if not isinstance(config, dict):
raise ConfigError("config/cmhub.json 必须是对象")
return {"api_key": str(config.get("api_key", "") or "")}
def load_cmhub_config(path=CMHUB_CONFIG_PATH) -> dict:
"""Load cmhub API key config. Missing file means key is not configured."""
if not os.path.exists(path):
return default_cmhub_config()
with open(path, "r", encoding="utf-8") as fh:
try:
loaded = json.load(fh)
except json.JSONDecodeError as exc:
raise ConfigError(f"cmhub 配置不是有效 JSON: {path}") from exc
return _normalize_cmhub_config(loaded)
def save_cmhub_config(config, path=CMHUB_CONFIG_PATH) -> dict:
"""Persist cmhub API key config, including the local plaintext key."""
normalized = _normalize_cmhub_config(config)
directory = os.path.dirname(os.path.abspath(path))
if directory:
os.makedirs(directory, exist_ok=True)
with open(path, "w", encoding="utf-8") as fh:
json.dump(normalized, fh, ensure_ascii=False, indent=2)
fh.write("\n")
return normalized
def get_cmhub_api_key(path=CMHUB_CONFIG_PATH, masked=False) -> str:
key = load_cmhub_config(path).get("api_key", "")
return mask_secret(key) if masked else key
def save_config(config, path=CONFIG_PATH) -> dict:
"""Persist config to JSON and return the normalized config."""
@@ -258,6 +318,39 @@ def ai_config(config=None) -> dict:
return copy.deepcopy(_config_or_load(config).get("ai", DEFAULT_CONFIG["ai"]))
def ai_backend(config=None) -> str:
ai = ai_config(config)
backend = str(ai.get("backend", "direct") or "direct").strip().lower()
if backend not in AI_BACKENDS:
raise ConfigError("AI backend 必须是 direct 或 cmhub")
return backend
def cmhub_config(config=None) -> dict:
ai = ai_config(config)
value = ai.get("cmhub", {})
if not isinstance(value, dict):
raise ConfigError("ai.cmhub 必须是对象")
merged = _deep_merge(DEFAULT_CONFIG["ai"]["cmhub"], value)
merged["base_url"] = str(merged.get("base_url", "") or "").strip()
merged["title_alias"] = str(merged.get("title_alias", "") or "").strip()
merged["image_alias"] = str(merged.get("image_alias", "") or "").strip()
merged["connect_timeout"] = int(merged.get("connect_timeout", 10) or 10)
merged["check_balance_before_batch"] = bool(merged.get("check_balance_before_batch", False))
if merged["connect_timeout"] <= 0:
raise ConfigError("ai.cmhub.connect_timeout 必须大于 0")
return merged
def cmhub_request_url(base_url, endpoint) -> str:
base = str(base_url or "").strip().rstrip("/")
path = "/" + str(endpoint or "").strip().lstrip("/")
if not base:
return path
return base + path
def response_timeout(config=None) -> int:
ai = ai_config(config)
resolution = str(ai.get("resolution", DEFAULT_CONFIG["ai"]["resolution"]))