"""BetterGI adapter helpers for reading the active party preset name.""" from __future__ import annotations import copy import json import re from dataclasses import dataclass from pathlib import Path from typing import Any try: from .bettergi_daily import DailyAutomationError, JsonUpdate except ImportError: from bettergi_daily import DailyAutomationError, JsonUpdate CURRENT_PARTY_SCRIPT_NAME = "LiveCurrentParty" CURRENT_PARTY_GROUP_NAME = "直播系统读取当前队伍" CURRENT_PARTY_STATUS_FILE = "status.json" _REQUEST_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$") _INVALID_BGI_NAME = re.compile(r'[<>:"/\\|?*\x00-\x1f]') class CurrentPartyReadError(DailyAutomationError): """Raised when the managed current-party reader cannot complete safely.""" @dataclass(frozen=True) class PreparedCurrentPartyRead: request_id: str group_name: str status_path: Path updates: tuple[JsonUpdate, ...] @dataclass(frozen=True) class CurrentPartyReadResult: party_name: str candidates: tuple[str, ...] = () def _read_json_object(path: Path, label: str) -> dict[str, Any]: if not path.is_file(): raise CurrentPartyReadError(f"未找到{label}: {path}") try: data = json.loads(path.read_text(encoding="utf-8-sig")) except (OSError, json.JSONDecodeError) as exc: raise CurrentPartyReadError(f"读取{label}失败: {exc}") from exc if not isinstance(data, dict): raise CurrentPartyReadError(f"{label}必须是 JSON 对象: {path}") return data def _safe_group_name(value: str) -> str: name = str(value or "").strip() if not name: raise CurrentPartyReadError("当前队伍读取配置组名称不能为空") if name in {".", ".."} or _INVALID_BGI_NAME.search(name): raise CurrentPartyReadError(f"当前队伍读取配置组名称包含非法字符: {name}") return name def _next_group_index(group_dir: Path) -> int: indexes: list[int] = [] for path in group_dir.glob("*.json"): try: data = json.loads(path.read_text(encoding="utf-8-sig")) value = data.get("index") if isinstance(data, dict) else None if isinstance(value, int): indexes.append(value) except (OSError, json.JSONDecodeError): continue return max(indexes, default=0) + 1 def _load_group_template(group_dir: Path, managed_path: Path) -> dict[str, Any]: if managed_path.is_file(): return _read_json_object(managed_path, "托管当前队伍读取配置组") for name in ("切换队伍", "修改队员", "每日委托"): candidate = group_dir / f"{name}.json" if candidate.is_file(): template = _read_json_object(candidate, f"配置组“{name}”") template["index"] = _next_group_index(group_dir) return template raise CurrentPartyReadError( "无法生成当前队伍读取配置组:请先在 BGI 创建“切换队伍”“修改队员”或“每日委托”中的任意一个配置组" ) def prepare_current_party_read( work_dir: str | Path, request_id: str, *, group_name: str = CURRENT_PARTY_GROUP_NAME, ) -> PreparedCurrentPartyRead: work_path = Path(work_dir) if not work_path.is_dir(): raise CurrentPartyReadError(f"BetterGI 工作目录不存在: {work_path}") request = str(request_id or "").strip() if not _REQUEST_ID_PATTERN.fullmatch(request): raise CurrentPartyReadError("当前队伍读取请求 ID 格式无效") managed_name = _safe_group_name(group_name) group_dir = work_path / "User" / "ScriptGroup" if not group_dir.is_dir(): raise CurrentPartyReadError(f"BGI 配置组目录不存在: {group_dir}") managed_path = group_dir / f"{managed_name}.json" managed = copy.deepcopy(_load_group_template(group_dir, managed_path)) managed["name"] = managed_name managed["projects"] = [ { "name": "读取当前队伍名称", "folderName": CURRENT_PARTY_SCRIPT_NAME, "jsScriptSettingsObject": {"requestId": request}, "index": 1, "type": "Javascript", "status": "Enabled", "schedule": "Daily", "runNum": 1, "allowJsNotification": True, "allowJsHTTPHash": "", } ] status_path = ( work_path / "User" / "JsScript" / CURRENT_PARTY_SCRIPT_NAME / CURRENT_PARTY_STATUS_FILE ) update = JsonUpdate(managed_path, managed, "生成当前队伍读取配置组") return PreparedCurrentPartyRead( request_id=request, group_name=managed_name, status_path=status_path, updates=(update,), ) def clear_current_party_status(status_path: str | Path) -> None: path = Path(status_path) try: path.unlink(missing_ok=True) except OSError as exc: raise CurrentPartyReadError(f"清理当前队伍读取状态失败: {exc}") from exc def read_current_party_status( status_path: str | Path, request_id: str, ) -> CurrentPartyReadResult | None: path = Path(status_path) if not path.is_file(): return None try: data = json.loads(path.read_text(encoding="utf-8-sig")) except (OSError, json.JSONDecodeError): return None if not isinstance(data, dict) or str(data.get("request_id") or "") != request_id: return None state = str(data.get("state") or "").strip().casefold() if state in {"", "running"}: return None candidates = tuple( str(value).strip() for value in data.get("candidates", []) if str(value).strip() ) if isinstance(data.get("candidates"), list) else () if state == "success": party_name = str(data.get("party_name") or "").strip() if not party_name: raise CurrentPartyReadError("当前队伍读取脚本返回成功,但队伍名称为空") return CurrentPartyReadResult(party_name=party_name, candidates=candidates) if state == "error": message = str(data.get("message") or "读取当前队伍失败").strip() raise CurrentPartyReadError(message) raise CurrentPartyReadError(f"当前队伍读取脚本返回未知状态: {state}")