feat: switch config to JSONC + split secrets.json

Load business config as JSONC (comments + trailing commas) via an
in-repo stripper then stdlib json — no new deps. Default path prefers
config.jsonc, falls back to legacy config.json. Auth write-back targets
only sibling secrets.json so JSONC comments are never wiped; secrets
overlay merges on load. Examples, compose, Docker, docs, favsort and
tools all share the same loader. Phase A only — no DB settings migration.
This commit was merged in pull request #4.
This commit is contained in:
bot-ci committed 2026-11-02 12:58:20 +08:00
1 parent b4f0f45c4e
commit 8e7ead1669
21 files changed
+652 -182

No files matched your search

+70 -13
View File
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""扫码授权流程;仅由 ``python app.py auth`` 调用。
认证结果只写入指定的 ``bilibili.read`` 或 ``bilibili.write`` 段,两个账号的
身份及设备指纹 Cookie 绝不互相覆盖。
认证结果只写入凭据文件(默认与主配置同目录的 ``secrets.json``)里指定的
``bilibili.read`` 或 ``bilibili.write`` 段,两个账号的身份及设备指纹 Cookie
绝不互相覆盖。主配置(可为 JSONC)不会被回写,注释得以保留。
"""
from __future__ import annotations
@@ -21,6 +22,8 @@ import zlib
from pathlib import Path
from typing import Any, Callable, Mapping
from ..config import resolve_secrets_path
from .. import jsonc
from .auth import (
IDENTITY_KEYS,
cookie_value,
@@ -154,22 +157,71 @@ class AuthService:
def __init__(self, config_path: str | Path, *, account: str, logger=None) -> None:
if account not in {"read", "write"}:
raise ValueError(f"未知账号角色: {account}")
self.path = Path(config_path)
self.config_path = Path(config_path)
# 兼容旧属性名:历史代码/测试可能读 path;现指向凭据文件
self.account = account
self.logger = logger
self.secrets_path = self._resolve_secrets_path()
self.path = self.secrets_path
def _resolve_secrets_path(self) -> Path:
raw: dict[str, Any] | None = None
if self.config_path.exists():
try:
loaded = jsonc.load(self.config_path)
except (OSError, ValueError):
loaded = None
if isinstance(loaded, dict):
raw = loaded
return resolve_secrets_path(self.config_path, raw)
@property
def backup_path(self) -> Path:
return self.path.with_name(self.path.name + ".bak")
return self.secrets_path.with_name(self.secrets_path.name + ".bak")
def _empty_account(self) -> dict[str, Any]:
return {"mid": 0, "csrf": "", "cookie": ""}
def _load_raw(self) -> dict[str, Any]:
with self.path.open(encoding="utf-8") as fh:
raw = json.load(fh)
if not isinstance(raw, dict):
raise ValueError("配置文件顶层必须是对象")
"""加载凭据文件;若尚不存在则从主配置 bilibili 段迁移种子。"""
if self.secrets_path.exists():
raw = jsonc.load(self.secrets_path)
if not isinstance(raw, dict):
raise ValueError("凭据文件顶层必须是对象")
else:
raw = {}
if self.config_path.exists():
try:
main = jsonc.load(self.config_path)
except (OSError, ValueError):
main = None
if isinstance(main, dict) and isinstance(main.get("bilibili"), dict):
# 仅迁移凭据段,不把整份主配置拷进 secrets
raw["bilibili"] = main["bilibili"]
section = raw.get("bilibili")
if not isinstance(section, dict) or not isinstance(section.get(self.account), dict):
raise ValueError(f"缺少 bilibili.{self.account} 账号配置")
if not isinstance(section, dict):
section = {}
raw["bilibili"] = section
if not isinstance(section.get(self.account), dict):
section[self.account] = self._empty_account()
# 保证另一账号键存在,避免后续手工编辑困惑
other = "write" if self.account == "read" else "read"
if not isinstance(section.get(other), dict):
# 尝试从主配置补齐;否则空壳
filled = False
if self.config_path.exists():
try:
main = jsonc.load(self.config_path)
except (OSError, ValueError):
main = None
if isinstance(main, dict) and isinstance(main.get("bilibili"), dict):
other_cred = main["bilibili"].get(other)
if isinstance(other_cred, dict):
section[other] = dict(other_cred)
filled = True
if not filled:
section[other] = self._empty_account()
return raw
@staticmethod
@@ -209,7 +261,9 @@ class AuthService:
if new_mid.isdigit():
credentials["mid"] = int(new_mid)
shutil.copy2(self.path, self.backup_path)
had_secrets = self.secrets_path.exists()
if had_secrets:
shutil.copy2(self.secrets_path, self.backup_path)
self._write_raw(raw)
try:
nav = self._get_json(credentials, f"{BASE}/x/web-interface/nav", True)
@@ -218,8 +272,11 @@ class AuthService:
except Exception as exc: # noqa: BLE001
data, ok = {"uname": str(exc)}, False
if not ok:
shutil.copy2(self.backup_path, self.path)
print(f"[auth] {self.account} 登录校验失败,已回滚 config.json")
if had_secrets and self.backup_path.exists():
shutil.copy2(self.backup_path, self.secrets_path)
elif self.secrets_path.exists():
self.secrets_path.unlink()
print(f"[auth] {self.account} 登录校验失败,已回滚 {self.secrets_path.name}")
return 4
if isinstance(data.get("mid"), int):