from __future__ import annotations import os import sys from dataclasses import dataclass, field from pathlib import Path from typing import Any import yaml from xhs_bridge.human_behavior import ( HumanBehaviorConfig, parse_human_behavior_config, write_human_behavior_json, ) from xhs_bridge.human_guard import HumanGuardConfig, parse_human_guard_config def application_root() -> Path: """OMS Agent 桌面端:engine/ 目录;PyInstaller 打包后为运行时根目录。""" if getattr(sys, "frozen", False): from xhs_bridge.pack_runtime import get_runtime_root return get_runtime_root() return Path(__file__).resolve().parent.parent def _is_frozen() -> bool: return bool(getattr(sys, "frozen", False)) PROJECT_ROOT = application_root() DEFAULT_CONFIG_PATH = PROJECT_ROOT / "config.yaml" DEFAULT_ENGINE_DIR = PROJECT_ROOT @dataclass class SkillsConfig: """内嵌 xiaohongshu-skills 引擎配置。""" project_dir: Path = field(default_factory=lambda: DEFAULT_ENGINE_DIR) python: str = "" # 空则自动探测项目根目录 .venv 或当前解释器 bridge_port: int = 9333 startup_timeout_sec: int = 45 @property def bridge_ws_url(self) -> str: return f"ws://127.0.0.1:{self.bridge_port}" def python_path(self) -> Path: if self.python: return Path(os.path.expanduser(self.python)).resolve() for folder in ("python", ".venv"): candidate = self.project_dir / folder / "Scripts" / "python.exe" if candidate.is_file(): return candidate if not _is_frozen(): return Path(sys.executable).resolve() raise FileNotFoundError("打包版应使用内置 skills_cli / XHSBridge,无需 Python。") def bridge_launcher(self, *, port: int) -> list[str]: if _is_frozen(): from xhs_bridge.pack_runtime import bundled_engine_dir exe = bundled_engine_dir() / "XHSBridge.exe" if exe.is_file(): return [str(exe), "--port", str(port)] else: bundled = self.project_dir / "engine" / "XHSBridge.exe" if bundled.is_file(): return [str(bundled), "--port", str(port)] return [str(self.python_path()), str(self.bridge_server_script()), "--port", str(port)] def cli_launcher(self, argv: list[str]) -> list[str]: if _is_frozen(): from xhs_bridge.pack_runtime import bundled_engine_dir exe = bundled_engine_dir() / "skills_cli.exe" if exe.is_file(): return [str(exe), *argv] else: bundled = self.project_dir / "engine" / "skills_cli.exe" if bundled.is_file(): return [str(bundled), *argv] return [str(self.python_path()), str(self.cli_script()), *argv] def scripts_dir(self) -> Path: return self.project_dir / "scripts" def cli_script(self) -> Path: return self.scripts_dir() / "cli.py" def bridge_server_script(self) -> Path: return self.scripts_dir() / "bridge_server.py" def extension_dir(self) -> Path: return self.project_dir / "extension" @dataclass class LicenseConfig: provider: str = "mock" api_base_url: str = "https://api.example.com" cache_path: str = "~/.xhs_bridge/license.json" def resolved_cache_path(self) -> Path: return Path(os.path.expanduser(self.cache_path)) @dataclass class OmsConfig: """OMS 物流查价 API(POST /order/price-check)。""" api_base_url: str = "http://api.nanafa.com/admin" timeout_sec: int = 30 auth_token: str = "" contact_name: str = "" contact_phone: str = "" wechat_qrcode_path: str = "" @dataclass class AppConfig: bin_dir: Path = PROJECT_ROOT skills: SkillsConfig = field(default_factory=SkillsConfig) license: LicenseConfig = field(default_factory=LicenseConfig) oms: OmsConfig = field(default_factory=OmsConfig) human_guard: HumanGuardConfig = field(default_factory=HumanGuardConfig) human_behavior: HumanBehaviorConfig = field(default_factory=HumanBehaviorConfig) state_dir: Path = field(default_factory=lambda: PROJECT_ROOT / ".xhs_bridge") def human_guard_state_path(self) -> Path: return self.state_dir / "human_guard_state.json" def bridge_pid_file(self) -> Path: return self.state_dir / "bridge.pid" def _expand_path(value: str | Path, *, base: Path | None = None) -> Path: raw = Path(os.path.expanduser(str(value))) if raw.is_absolute(): return raw.resolve() anchor = (base or Path.cwd()).resolve() return (anchor / raw).resolve() def load_config(path: Path | None = None) -> AppConfig: cfg_path = path or DEFAULT_CONFIG_PATH data: dict[str, Any] = {} if cfg_path.is_file(): with cfg_path.open("r", encoding="utf-8") as f: data = yaml.safe_load(f) or {} root = application_root() bin_dir = _expand_path(data.get("bin_dir", root), base=root) skills_raw = data.get("skills") or {} license_raw = data.get("license") or {} oms_raw = data.get("oms") or {} human_guard = parse_human_guard_config(data.get("human_guard")) human_behavior = parse_human_behavior_config(data.get("human_behavior")) if _is_frozen(): project_dir = root.resolve() else: project_dir = _expand_path(skills_raw.get("project_dir", root), base=root) skills = SkillsConfig( project_dir=project_dir, python=str(skills_raw.get("python") or ""), bridge_port=int(skills_raw.get("bridge_port", 9333)), startup_timeout_sec=int(skills_raw.get("startup_timeout_sec", 45)), ) license_cfg = LicenseConfig( provider=license_raw.get("provider", "mock"), api_base_url=license_raw.get("api_base_url", LicenseConfig.api_base_url), cache_path=license_raw.get("cache_path", LicenseConfig.cache_path), ) oms_cfg = OmsConfig( api_base_url=str(oms_raw.get("api_base_url", OmsConfig.api_base_url)), timeout_sec=int(oms_raw.get("timeout_sec", OmsConfig.timeout_sec)), auth_token=str(oms_raw.get("auth_token", "") or ""), contact_name=str(oms_raw.get("contact_name", "") or ""), contact_phone=str(oms_raw.get("contact_phone", "") or ""), wechat_qrcode_path=str(oms_raw.get("wechat_qrcode_path", "") or ""), ) state_dir = bin_dir / ".xhs_bridge" state_dir.mkdir(parents=True, exist_ok=True) write_human_behavior_json(human_behavior, state_dir) return AppConfig( bin_dir=bin_dir, skills=skills, license=license_cfg, oms=oms_cfg, human_guard=human_guard, human_behavior=human_behavior, state_dir=state_dir, ) def config_file_path(path: Path | None = None) -> Path: return path or DEFAULT_CONFIG_PATH def load_config_yaml(path: Path | None = None) -> dict[str, Any]: cfg_path = config_file_path(path) if not cfg_path.is_file(): return {} with cfg_path.open("r", encoding="utf-8") as f: return yaml.safe_load(f) or {} def save_human_guard_config(cfg: HumanGuardConfig, path: Path | None = None) -> Path: from xhs_bridge.human_guard import human_guard_config_to_dict cfg_path = config_file_path(path) data = load_config_yaml(cfg_path) data["human_guard"] = human_guard_config_to_dict(cfg) with cfg_path.open("w", encoding="utf-8") as f: yaml.dump( data, f, allow_unicode=True, default_flow_style=False, sort_keys=False, ) return cfg_path def save_human_behavior_config( cfg: HumanBehaviorConfig, state_dir: Path, path: Path | None = None, ) -> Path: from xhs_bridge.human_behavior import human_behavior_config_to_dict cfg_path = config_file_path(path) data = load_config_yaml(cfg_path) if "human_guard" in data: del data["human_guard"] data["human_behavior"] = human_behavior_config_to_dict(cfg) with cfg_path.open("w", encoding="utf-8") as f: yaml.dump( data, f, allow_unicode=True, default_flow_style=False, sort_keys=False, ) write_human_behavior_json(cfg, state_dir) return cfg_path