noke / core /config.py
i3t2y
feat: noke v0.3.0 — 5-batch architecture upgrade
5039d41
Raw
History Blame Contribute Delete
6.36 kB
import json
import os
import hashlib
import secrets
import copy
from pathlib import Path
DEFAULT_UPSTREAM_URL = "https://web.tabbitbrowser.com"
CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.json"
# Edition presets (国际版 / 国内版)
EDITION_PRESETS = {
"international": {
"base_url": "https://web.tabbitbrowser.com",
"client_id": "e7fa44387b1238ef1f6f",
},
"cn": {
"base_url": "https://web.tabbitbrowser.cn",
"client_id": "e7fa44387b1238ef1f6f", # 国内版 client_id 可能与国际版不同,这里占位
},
}
DEFAULT_CONFIG = {
"server": {"host": "0.0.0.0", "port": 8800},
"admin": {"password_hash": "", "salt": "", "jwt_secret": ""},
"noke": {
"edition": "international", # 新增,用于预设
"base_url": DEFAULT_UPSTREAM_URL,
"client_id": "e7fa44387b1238ef1f6f",
"verify_tls": True,
},
"tokens": [],
"proxy": {"api_key": "", "system_prompt": "", "rate_limit_qps": 0, "rate_limit_burst": 0},
"claude": {"default_model": "best", "system_prompt": ""},
"logging": {"max_entries": 500},
}
def _deep_merge(base: dict, override: dict) -> dict:
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = _deep_merge(result[key], value)
else:
result[key] = value
return result
def hash_password(password: str, salt: str | None = None) -> tuple[str, str]:
if salt is None:
salt = secrets.token_hex(16)
hashed = hashlib.sha256((password + salt).encode()).hexdigest()
return hashed, salt
def _apply_edition(config: dict) -> dict:
"""根据 edition 字段填充 base_url/client_id(仅当用户未显式覆盖时)。"""
edition = config.get("noke", {}).get("edition", "international")
preset = EDITION_PRESETS.get(edition)
if not preset:
return config
# 仅当当前值与默认值一致时才覆写(即用户未手动修改)
default_noke = DEFAULT_CONFIG.get("noke", {})
if config["noke"]["base_url"] == default_noke.get("base_url"):
config["noke"]["base_url"] = preset["base_url"]
if config["noke"]["client_id"] == default_noke.get("client_id"):
config["noke"]["client_id"] = preset["client_id"]
return config
def _apply_env_overrides(config: dict) -> dict:
"""从环境变量覆盖所有可配置项,环境变量优先级最高"""
# 服务器
if v := os.environ.get("SERVER_HOST"):
config["server"]["host"] = v
if v := os.environ.get("SERVER_PORT"):
config["server"]["port"] = int(v)
# 管理员认证
if v := os.environ.get("ADMIN_PASSWORD_HASH"):
config["admin"]["password_hash"] = v
if v := os.environ.get("ADMIN_SALT"):
config["admin"]["salt"] = v
if v := os.environ.get("JWT_SECRET"):
config["admin"]["jwt_secret"] = v
# Edition (新增)
if v := os.environ.get("EDITION"):
if v in EDITION_PRESETS:
config["noke"]["edition"] = v
# Upstream
if v := os.environ.get("UPSTREAM_BASE_URL"):
config["noke"]["base_url"] = v
if v := os.environ.get("UPSTREAM_CLIENT_ID"):
config["noke"]["client_id"] = v
# 兼容新名 VERIFY_TLS,同时保留旧名 UPSTREAM_VERIFY_TLS
verify_val = os.environ.get("VERIFY_TLS") or os.environ.get("UPSTREAM_VERIFY_TLS")
if verify_val:
config["noke"]["verify_tls"] = verify_val.lower() in ("true", "1", "yes")
# 代理
if v := os.environ.get("PROXY_API_KEY"):
config["proxy"]["api_key"] = v
if v := os.environ.get("PROXY_SYSTEM_PROMPT"):
config["proxy"]["system_prompt"] = v
# Claude
if v := os.environ.get("CLAUDE_DEFAULT_MODEL"):
config["claude"]["default_model"] = v
if v := os.environ.get("CLAUDE_SYSTEM_PROMPT"):
config["claude"]["system_prompt"] = v
# Tokens(JSON 数组格式)
if v := os.environ.get("UPSTREAM_TOKENS"):
try:
config["tokens"] = json.loads(v)
except Exception:
pass
# 日志
if v := os.environ.get("LOG_MAX_ENTRIES"):
config["logging"]["max_entries"] = int(v)
return config
class ConfigManager:
def __init__(self, path: str | Path | None = None):
self.path = Path(path) if path else CONFIG_PATH
self.config = self._load()
def _load(self) -> dict:
if self.path.exists():
with open(self.path, "r", encoding="utf-8") as f:
saved = json.load(f)
config = _deep_merge(copy.deepcopy(DEFAULT_CONFIG), saved)
# 应用 edition 预设(填充 base_url/client_id 默认值)
config = _apply_edition(config)
self._save(config)
else:
config = copy.deepcopy(DEFAULT_CONFIG)
config["admin"]["jwt_secret"] = secrets.token_hex(32)
admin_pw = os.environ.get("NOKE_ADMIN_PASSWORD") or secrets.token_urlsafe(12)
pw_hash, salt = hash_password(admin_pw)
config["admin"]["password_hash"] = pw_hash
config["admin"]["salt"] = salt
self._save(config)
if not os.environ.get("NOKE_ADMIN_PASSWORD"):
print(f"\n[Noke] 初始管理员密码: {admin_pw} (仅首次显示,请立即保存并登录修改)\n")
# 环境变量最后覆盖,优先级最高
config = _apply_env_overrides(config)
return config
def _save(self, config: dict | None = None):
if config is None:
config = self.config
with open(self.path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
def save(self):
self._save()
def get(self, *keys, default=None):
val = self.config
for k in keys:
if isinstance(val, dict):
val = val.get(k)
else:
return default
if val is None:
return default
return val
def set_val(self, *keys_and_value):
keys = keys_and_value[:-1]
value = keys_and_value[-1]
d = self.config
for k in keys[:-1]:
d = d.setdefault(k, {})
d[keys[-1]] = value
self.save()