feat: add cmhub AI backend
This commit is contained in:
@@ -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"]))
|
||||
|
||||
Reference in New Issue
Block a user