front/xhs-agent-desktop/engine/xhs_bridge/config.py
zzh 5a26ae0048 feat: 小红书模块迁移、登录重构与 xhs-agent-desktop 桌面 Agent
- 新增小红书前端模块(启动服务、内容分析、爆款创作、发布记录、技能卡片、拟人设置、AI 助手)

- 新增 xhs-agent-desktop 独立桌面 Agent,对接 OMS 任务调度

- 登录页重构,整合微信登录与 OMS JWT 认证

- 订单、渠道、用户中心等页面与 API 增强

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 16:13:13 +08:00

253 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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