File size: 8,119 Bytes
f4541b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0231737
f4541b6
 
 
 
 
 
 
 
1481207
 
 
8088875
 
 
 
 
 
 
 
 
 
f4541b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5281872
f4541b6
0231737
 
f4541b6
 
0231737
 
 
 
f4541b6
2d8e7b4
 
 
 
 
 
 
 
 
 
 
 
f4541b6
 
 
 
 
8088875
 
f4541b6
 
 
 
 
 
 
 
0231737
 
2d8e7b4
 
 
 
f4541b6
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
config.py — 把 server.py 原本硬编码在顶层的 DEFAULT_* 全部改成从环境变量读。

设计要点:
  - 静态项(上游/sitekey/工厂页)只在启动时读一次, 改了要重启进程。
  - 动态项(worker_pages / shelf_size 等)只是给 TokenPool 的"初始值",
    运行时仍可被管理面板热改(见 server.py 的 setter)。
  - 敏感项(relay keys / admin key / proxy)只在私有环境变量里给,
    admin 面板里的敏感编辑默认禁用, 防公网误操作。
  - HF_SPACE 由 Hugging Face 自动注入; 命中后 server.py 会强制走容器化
    启动参数(--no-sandbox 等)。
  - 本模块不抛异常: 任何缺失走默认值, 保证容器最小配置即可起服务。
"""
import json
import os
from typing import Dict, Set


# --------------------------- 工具: 解析环境变量 ---------------------------
def _str(name: str, default: str) -> str:
    return os.environ.get(name, default)


def _int(name: str, default: int) -> int:
    raw = os.environ.get(name)
    if raw is None or raw == "":
        return default
    try:
        return int(raw)
    except ValueError:
        return default


def _bool(name: str, default: bool) -> bool:
    raw = os.environ.get(name)
    if raw is None:
        return default
    return raw.strip().lower() in ("1", "true", "yes", "on")


def _relay_keys(name: str, default: Dict[str, str]) -> Set[str]:
    """relay key 集合: 既支持 JSON 对象 ({"key":"user"}) 或 JSON 数组 (["k1","k2"]),
    也支持换行/逗号分隔的纯 key 列表(每项可带 =user 后缀)。"""
    raw = os.environ.get(name)
    if not raw:
        return set(default.keys())
    raw = raw.strip()
    keys: Set[str] = set()
    # 先试 JSON: dict 或 list 都收
    try:
        data = json.loads(raw)
        if isinstance(data, dict):
            keys.update(str(k).strip() for k in data if str(k).strip())
            return keys
        if isinstance(data, list):
            keys.update(str(k).strip() for k in data if str(k).strip())
            return keys
    except (json.JSONDecodeError, TypeError):
        pass
    # 否则当分隔列表(逗号或换行)
    for tok in raw.replace("\n", ",").split(","):
        tok = tok.strip()
        if "=" in tok:                  # 形如 "sk-relay-xxx=default-user"
            tok = tok.split("=", 1)[0].strip()
        if tok:
            keys.add(tok)
    return keys or set(default.keys())


# --------------------------- 默认值(本机开发兜底) ---------------------------
_DEFAULT_KEYS: set = set()   # 不内置任何默认 key; 未配 RELAY_KEYS 时所有 /v1 请求 401


class Config:
    """启动期一次性快照。运行时热改的动态项在 TokenPool 里自行持有。"""

    # --- 静态: 改需重启 ---
    UPSTREAM: str        = _str("UPSTREAM",        "https://api.deepinfra.com")
    SITEKEY: str         = _str("SITEKEY",         "0x4AAAAAADlBNBTRb73O02Vo")
    FACTORY_PAGE: str    = _str("FACTORY_PAGE",    "https://deepinfra.com/google/gemma-4-26B-A4B-it")
                            # token 通用(任一可路由页皆可); 选 gemma-4-26B:
                            # 激活参数最小(4B)+冷门, 签发不挤占热门模型的匿名配额
                            # 仅 real 模式使用; intercept/mock 模式只取其 host
    FACTORY_MODE: str    = _str("FACTORY_MODE",    "intercept")
                            # token 工厂页承载方式(面板可热切):
                            #   intercept(默认/推荐): Playwright route 拦截,
                            #     空壳页纯内存承载, 零基础设施, 不访问目标站
                            #   mock: --host-resolver-rules 域名映射 + 容器内
                            #     本地 HTTPS 空壳页(自签证书), 绕开 CDP 拦截层
                            #   real: 真实加载工厂页(旧行为, 保底)
    MOCK_PORT: int       = _int("MOCK_PORT",       8443)
                            # mock 模式本地 HTTPS 空壳页端口(仅容器内 loopback)
    UA: str              = _str("UA",
                                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                                "AppleWebKit/537.36 (KHTML, like Gecko) "
                                "Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0")

    # --- 鉴权(私有) ---
    RELAY_KEYS: Set[str] = _relay_keys("RELAY_KEYS", _DEFAULT_KEYS)
    ADMIN_KEY: str       = _str("ADMIN_KEY", "")            # 空=禁用 /admin 控制台
    ADMIN_ENABLED: bool  = _bool("ADMIN_ENABLED", False)    # HF 上默认关, 防 /admin 被公网发现

    # --- 浏览器/容器 ---
    HF_SPACE: bool       = bool(os.environ.get("HF_SPACE"))  # HF 自动注入
    HEADLESS: bool       = _bool("HEADLESS",
                                  os.environ.get("HF_SPACE") is not None)
                                  # 默认: HF 上 true(省资源), 本机 false(POC 验证)
                                  # 若 Turnstile 在 HF 出口拒真 headless, 设 HEADLESS=false
                                  # 让 Dockerfile 的 Xvfb 兜住"headless=False"的指纹
    PROFILE_DIR: str     = _str("PROFILE_DIR",
                                os.path.expanduser("~/.cache/relay-pw-profile"))
    PROXY: str           = _str("PROXY", "")                # 私有: 可信出口代理, 绕 HF 共享 IP

    # --- 动态初值(面板仍可热改) ---
    WORKER_PAGES: int        = _int("WORKER_PAGES",     3)
    MAX_WORKER_PAGES: int    = _int("MAX_WORKER_PAGES", 16)
    SHELF_SIZE: int          = _int("SHELF_SIZE",       100)
                                    # 货架容量上限(自适应模式下为封顶值)
    TOKEN_MAX_AGE: int       = _int("TOKEN_MAX_AGE",    250)
    IDLE_PAUSE: int          = _int("IDLE_PAUSE",       300)
    MAX_RETRY: int           = _int("MAX_RETRY",          5)
                                    # 上游错误重试次数(任意非负整数, 0=不重试)
    RETRY_STATUSES: str      = _str("RETRY_STATUSES",   "403,429,500,502,503,504")
                                    # 触发重试的上游状态码(逗号分隔); 400 类请求级错误不在其中

    # --- 可靠性/配额治理 ---
    ADAPTIVE_SHELF: bool  = _bool("ADAPTIVE_SHELF", True)
                                    # 货架目标随近 5min 实际 QPS 自动伸缩,
                                    # 避免固定大货架空转发白烧 Turnstile 签发
    BROWSER_MAX_AGE: int  = _int("BROWSER_MAX_AGE", 43200)
                                    # 浏览器上下文最大存活秒数, 到期由看门狗
                                    # 在空闲窗口回收重启(防长跑内存膨胀)
    ISSUE_TIMEOUT: int    = _int("ISSUE_TIMEOUT",      60)
                                    # 等 token 补货的最长秒数(超时 503)
    RATE_LIMIT_RPM: int   = _int("RATE_LIMIT_RPM",    120)
                                    # 每个 relay key 的滑动窗口限速(次/分钟), 0=关闭

    def as_dict(self) -> dict:
        """非敏感快照, 供 / 只读状态页回显。"""
        return {
            "upstream":         self.UPSTREAM,
            "factory_page":     self.FACTORY_PAGE,
            "factory_mode":      self.FACTORY_MODE,
            "sitekey":           self.SITEKEY,
            "hf_space":         self.HF_SPACE,
            "headless":         self.HEADLESS,
            "admin_enabled":    self.ADMIN_ENABLED,
            "worker_pages":     self.WORKER_PAGES,
            "max_worker_pages": self.MAX_WORKER_PAGES,
            "shelf_size":       self.SHELF_SIZE,
            "token_max_age":   self.TOKEN_MAX_AGE,
            "idle_pause":      self.IDLE_PAUSE,
            "max_retry":       self.MAX_RETRY,
            "retry_statuses":  self.RETRY_STATUSES,
            "adaptive_shelf":  self.ADAPTIVE_SHELF,
            "browser_max_age": self.BROWSER_MAX_AGE,
            "issue_timeout":   self.ISSUE_TIMEOUT,
            "rate_limit_rpm":  self.RATE_LIMIT_RPM,
            "proxy_configured": bool(self.PROXY),
        }


cfg = Config()