| """网关配置:从 config.toml 加载(pydantic.BaseModel + tomllib,无 pydantic-settings)。 |
| |
| ``config.toml`` 只放「与账号无关」的配置(网关 / 行为 / 上游 / 代理 / 日志 / 注册机); |
| 每个账号凭据存 ``account/<name>.json``,由 :mod:`app.account` 管理。 |
| """ |
| from __future__ import annotations |
|
|
| import os |
| import tomllib |
| from functools import lru_cache |
| from pathlib import Path |
|
|
| from pydantic import BaseModel |
|
|
|
|
| class Settings(BaseModel): |
| """网关行为与端点配置(不含账号凭据)。""" |
|
|
| |
| host: str = "0.0.0.0" |
| port: int = 8088 |
| gateway_api_key: str = "" |
|
|
| |
| request_timeout: float = 120.0 |
| |
| |
| image_success_cooldown: float = 300.0 |
| poll_interval: float = 1.2 |
| token_refresh_margin: int = 300 |
| tool_call_retries: int = 3 |
| tool_call_dup_limit: int = 2 |
| tool_forge_limit: int = 8 |
| tool_read_cap: int = 30 |
|
|
| |
| soften_system: bool = False |
| refusal_detect: bool = False |
|
|
| |
| upstream_chat_url: str = "" |
| upstream_strategy: str = "prompt" |
|
|
| |
| proxy_url: str = "" |
| registrar_proxy_url: str = "" |
|
|
| |
| log_enabled: bool = True |
| log_dir: str = "logs" |
| log_filename: str = "gateway.log" |
| log_level: str = "INFO" |
| log_max_bytes: int = 10 * 1024 * 1024 |
| log_backup_count: int = 5 |
| log_request_body: bool = True |
| log_response_body: bool = True |
| log_max_body_chars: int = 4000 |
|
|
| |
| account_dir: str = "account" |
|
|
| |
| target_account_count: int = 0 |
| auto_register_interval: float = 300.0 |
| auto_register_workers: int = 1 |
| |
| |
| pool_watch_interval: float = 30.0 |
|
|
| |
| quota_exhausted_action: str = "cooldown" |
| cooldown_seconds: float = 600.0 |
| cooldown_seconds_quota: float | None = None |
| cooldown_seconds_cf: float | None = None |
|
|
| |
| admin_auth_key: str = "" |
|
|
| def effective_proxy(self) -> str | None: |
| """网关上游请求用的代理;未配置返回 ``None``(直连)。""" |
| p = (self.proxy_url or "").strip() |
| return p or None |
|
|
| def effective_registrar_proxy(self) -> str | None: |
| """注册机用代理:优先 registrar_proxy_url,否则回退 proxy_url;皆空则 ``None``。""" |
| p = (self.registrar_proxy_url or "").strip() or (self.proxy_url or "").strip() |
| return p or None |
|
|
|
|
| def _flatten_toml(data: dict) -> dict: |
| """平铺 [gateway]/[upstream]/[registry]/[admin]/[proxy]/[logging]; |
| 忽略 [email]/[captcha](仅注册机用)。 |
| |
| toml 简短键名映射到 Settings 字段(``api_key`` → ``gateway_api_key``, |
| ``auth_key`` → ``admin_auth_key``;``[proxy].url`` / ``registrar_url`` → |
| ``proxy_url`` / ``registrar_proxy_url``;``[logging].*`` → ``log_*``)。 |
| """ |
| flat: dict = {} |
| for section in ("gateway", "upstream", "registry", "admin"): |
| flat.update(data.get(section, {})) |
| if "api_key" in flat and "gateway_api_key" not in flat: |
| flat["gateway_api_key"] = flat.pop("api_key") |
| if "auth_key" in flat and "admin_auth_key" not in flat: |
| flat["admin_auth_key"] = flat.pop("auth_key") |
| |
| if "strategy" in flat and "upstream_strategy" not in flat: |
| flat["upstream_strategy"] = flat.pop("strategy") |
|
|
| |
| proxy = data.get("proxy") or {} |
| if isinstance(proxy, dict): |
| if "proxy_url" not in flat: |
| flat["proxy_url"] = str(proxy.get("url") or proxy.get("default") or "") |
| if "registrar_proxy_url" not in flat: |
| flat["registrar_proxy_url"] = str( |
| proxy.get("registrar_url") or proxy.get("registrar") or "" |
| ) |
|
|
| |
| logging_sec = data.get("logging") or {} |
| if isinstance(logging_sec, dict): |
| key_map = { |
| "enabled": "log_enabled", |
| "dir": "log_dir", |
| "log_dir": "log_dir", |
| "filename": "log_filename", |
| "level": "log_level", |
| "max_bytes": "log_max_bytes", |
| "backup_count": "log_backup_count", |
| "log_request_body": "log_request_body", |
| "log_response_body": "log_response_body", |
| "max_body_chars": "log_max_body_chars", |
| } |
| for src, dst in key_map.items(): |
| if src in logging_sec and dst not in flat: |
| flat[dst] = logging_sec[src] |
| return flat |
|
|
|
|
| @lru_cache(maxsize=8) |
| def get_settings(path: str | None = None) -> Settings: |
| """加载 config.toml 构造 Settings。 |
| |
| ``path`` 默认 ``$TWOAPI_CONFIG`` 或 ``config.toml``。文件缺失时回退全默认值。 |
| 环境变量覆盖:``$PORT``(HF Space 强制)覆盖 gateway.port。 |
| 被 :func:`clear_settings_cache` 用于测试重读。 |
| """ |
| p = path or os.getenv("TWOAPI_CONFIG", "config.toml") |
| fpath = Path(p) |
| if fpath.is_file(): |
| with fpath.open("rb") as f: |
| data = tomllib.load(f) |
| settings = Settings(**_flatten_toml(data)) |
| else: |
| settings = Settings() |
| env_port = os.getenv("PORT") |
| if env_port: |
| try: |
| settings.port = int(env_port) |
| except (TypeError, ValueError): |
| pass |
| |
| |
| |
| for env_name, attr in (("ANUMA_ADMIN_KEY", "admin_auth_key"), |
| ("ANUMA_API_KEY", "gateway_api_key")): |
| val = os.getenv(env_name) |
| if val: |
| setattr(settings, attr, val) |
| return settings |
|
|
|
|
| def clear_settings_cache() -> None: |
| """清空 get_settings 的 lru_cache,供测试重读配置。""" |
| get_settings.cache_clear() |
|
|