AutoTeam-F / src /autoteam /runtime_config.py
ZRainbow's picture
fix(round-7): P2 收尾 + Round 6 deferred — 命名/抽组件/lifespan/24h去重/409/state-machine
a4ea50b
Raw
History Blame Contribute Delete
5.87 kB
"""运行时可变配置(由面板写入,重启后仍生效)。
与 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