Spaces:
Paused
Paused
File size: 5,874 Bytes
3df0958 1f6d7ba 3df0958 1f6d7ba 3df0958 478c16c a4ea50b 478c16c a4ea50b 478c16c a4ea50b 478c16c a4ea50b 478c16c a4ea50b 478c16c | 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 167 168 169 | """运行时可变配置(由面板写入,重启后仍生效)。
与 admin_state.py 区分:admin_state 只放管理员登录态(session/password/...),白名单字段严格;
本模块放"用户在面板里可以调的业务配置",目前只有 register_domain(子号注册用的 CloudMail 域名),
将来可以扩 batch_size、cool_down 等。持久化到项目根 `runtime_config.json`。
"""
import json
import logging
import os
import threading
import time
from pathlib import Path
from autoteam.textio import read_text, write_text
logger = logging.getLogger(__name__)
PROJECT_ROOT = Path(__file__).parent.parent.parent
RUNTIME_CONFIG_FILE = PROJECT_ROOT / "runtime_config.json"
RUNTIME_CONFIG_MODE = 0o666
_LOCK = threading.Lock()
def _load():
if not RUNTIME_CONFIG_FILE.exists():
return {}
try:
raw = read_text(RUNTIME_CONFIG_FILE).strip()
if not raw:
return {}
data = json.loads(raw)
return data if isinstance(data, dict) else {}
except Exception as exc:
# 静默吞会把用户在面板里设的 register_domain 等覆盖值丢掉,下一轮 _save 会把
# 损坏文件写回空 dict。保留一份 .corrupt-<ts>.json 便于事后排查。
corrupt_path = RUNTIME_CONFIG_FILE.with_suffix(f".corrupt-{int(time.time())}.json")
try:
RUNTIME_CONFIG_FILE.rename(corrupt_path)
logger.error("[runtime_config] 解析失败, 已保留原文件为 %s: %s", corrupt_path.name, exc)
except Exception as rename_exc:
logger.error("[runtime_config] 解析失败且无法重命名 (%s): %s", exc, rename_exc)
return {}
def _save(data):
target = RUNTIME_CONFIG_FILE.resolve()
write_text(target, json.dumps(data, indent=2, ensure_ascii=False))
try:
os.chmod(target, RUNTIME_CONFIG_MODE)
except Exception:
pass
def get(key, default=None):
with _LOCK:
return _load().get(key, default)
def set_value(key, value):
with _LOCK:
data = _load()
data[key] = value
_save(data)
return data
def get_register_domain():
"""返回用于子号注册的 CloudMail 域名。
优先级:runtime_config.json → 环境变量 CLOUDMAIL_DOMAIN(向后兼容)。
返回值已 lstrip "@"。
"""
from autoteam.config import CLOUDMAIL_DOMAIN
override = (get("register_domain") or "").strip()
if override:
return override.lstrip("@").strip()
return (CLOUDMAIL_DOMAIN or "").lstrip("@").strip()
def set_register_domain(domain):
"""写入 register_domain 覆盖值。空串表示清除 override 走环境变量。"""
cleaned = (domain or "").strip().lstrip("@").strip()
set_value("register_domain", cleaned)
return cleaned
# SPEC-2 FR-E2/E3 — sync_account_states 探测被踢识别的并发上限 + 去重冷却。
# 默认 concurrency=5(单次 sync 最多 5 个账号并发探测 wham/usage),
# cooldown=30 分钟(同一账号 30 分钟内不重复探测,避免抖动)。
# 上下界:concurrency [1, 16],cooldown [1, 1440] 分钟。
_SYNC_PROBE_CONCURRENCY_DEFAULT = 5
_SYNC_PROBE_COOLDOWN_MINUTES_DEFAULT = 30
def get_sync_probe_concurrency():
"""返回 sync_account_states 内并发探测被踢账号的最大 worker 数。"""
raw = get("sync_probe_concurrency", _SYNC_PROBE_CONCURRENCY_DEFAULT)
try:
n = int(raw)
except (TypeError, ValueError):
return _SYNC_PROBE_CONCURRENCY_DEFAULT
return max(1, min(16, n))
def set_sync_probe_concurrency(value):
try:
n = int(value)
except (TypeError, ValueError):
n = _SYNC_PROBE_CONCURRENCY_DEFAULT
n = max(1, min(16, n))
set_value("sync_probe_concurrency", n)
return n
def get_sync_probe_cooldown_minutes():
"""返回同一账号被探测后多久内不重复探测(分钟)。"""
raw = get("sync_probe_cooldown_minutes", _SYNC_PROBE_COOLDOWN_MINUTES_DEFAULT)
try:
n = int(raw)
except (TypeError, ValueError):
return _SYNC_PROBE_COOLDOWN_MINUTES_DEFAULT
return max(1, min(1440, n))
def set_sync_probe_cooldown_minutes(value):
try:
n = int(value)
except (TypeError, ValueError):
n = _SYNC_PROBE_COOLDOWN_MINUTES_DEFAULT
n = max(1, min(1440, n))
set_value("sync_probe_cooldown_minutes", n)
return n
# SPEC-2 FR-G — 邀请席位偏好。
# "default" 走 default→usage_based 兜底 + PATCH 升级,优先 ChatGPT 完整席位(老行为,默认)
# "codex" 直接 usage_based 邀请,跳过 PATCH,锁 codex-only 席位(节约 ChatGPT 席位时使用)
# "chatgpt" 别名,Round 7 P2.1 转移期支持,setter/getter 内部归一化为 "default"
_PREFERRED_SEAT_TYPE_DEFAULT = "default"
_PREFERRED_SEAT_TYPE_VALID = {"default", "chatgpt", "codex"}
_PREFERRED_SEAT_TYPE_NORMALIZE = {"chatgpt": "default"}
def _normalize_preferred_seat_type(raw):
"""把任意输入归一化为 {default, codex} 之一(chatgpt 别名 → default,非法/空 → default)。"""
val = (str(raw or "") or _PREFERRED_SEAT_TYPE_DEFAULT).strip().lower()
if val not in _PREFERRED_SEAT_TYPE_VALID:
return _PREFERRED_SEAT_TYPE_DEFAULT
return _PREFERRED_SEAT_TYPE_NORMALIZE.get(val, val)
def get_preferred_seat_type():
"""返回邀请席位偏好。'default'(默认/优先 PATCH 升级 ChatGPT 席位) 或 'codex'(锁 codex-only)。
Round 7 P2.1:已落盘的 'chatgpt' 旧值在读取时也归一化为 'default'。
"""
raw = get("preferred_seat_type", _PREFERRED_SEAT_TYPE_DEFAULT)
return _normalize_preferred_seat_type(raw)
def set_preferred_seat_type(value):
"""写入席位偏好;接受 'chatgpt' 作为 'default' 的转移期别名(Round 7 P2.1)。"""
val = _normalize_preferred_seat_type(value)
set_value("preferred_seat_type", val)
return val
|