front/xhs-agent-desktop/engine/xhs_bridge/ai_providers_store.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

172 lines
5.4 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 json
import uuid
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
from xhs_bridge.ai_provider_catalog import (
DEFAULT_PROVIDER_IDS,
default_base_url,
is_valid_provider_type,
provider_label,
)
from xhs_bridge.config import load_config
@dataclass
class AiProvider:
"""AI 平台连接配置(标题由平台类型决定)。"""
id: str
provider_type: str
base_url: str
api_key: str
api_secret: str
created_at: str
updated_at: str
@property
def type_label(self) -> str:
return provider_label(self.provider_type)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"provider_type": self.provider_type,
"base_url": self.base_url,
"api_key": self.api_key,
"api_secret": self.api_secret,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
@staticmethod
def from_dict(raw: dict[str, Any]) -> AiProvider:
ptype = str(raw.get("provider_type") or "chatgpt")
if not is_valid_provider_type(ptype):
ptype = "chatgpt"
return AiProvider(
id=str(raw.get("id") or uuid.uuid4().hex[:12]),
provider_type=ptype,
base_url=str(raw.get("base_url") or default_base_url(ptype)),
api_key=str(raw.get("api_key") or ""),
api_secret=str(raw.get("api_secret") or ""),
created_at=str(raw.get("created_at") or ""),
updated_at=str(raw.get("updated_at") or ""),
)
def mask_secret(value: str, *, visible_tail: int = 4) -> str:
text = (value or "").strip()
if not text:
return "(未填写)"
if len(text) <= visible_tail + 2:
return "*" * len(text)
return "*" * (len(text) - visible_tail) + text[-visible_tail:]
class AiProvidersStore:
"""AI 平台配置持久化(JSON)。"""
def __init__(self, path: Path | None = None) -> None:
if path is None:
cfg = load_config()
path = cfg.state_dir / "ai_providers.json"
self.path = path
self.path.parent.mkdir(parents=True, exist_ok=True)
self._items: list[AiProvider] = []
self.load()
def load(self) -> list[AiProvider]:
if not self.path.is_file() or self.path.stat().st_size == 0:
self._items = []
self.seed_default_providers()
return list(self._items)
try:
with self.path.open("r", encoding="utf-8") as f:
data = json.load(f)
items = data.get("providers") if isinstance(data, dict) else data
if not isinstance(items, list):
self._items = []
else:
self._items = [
AiProvider.from_dict(raw)
for raw in items
if isinstance(raw, dict)
]
except (json.JSONDecodeError, OSError):
self._items = []
if not self._items:
self.seed_default_providers()
return list(self._items)
def seed_default_providers(self) -> int:
"""写入默认平台卡片(仅补缺)。"""
existing_types = {p.provider_type for p in self._items}
existing_ids = {p.id for p in self._items}
added = 0
now = datetime.now().isoformat(timespec="seconds")
for preset_id, ptype in DEFAULT_PROVIDER_IDS:
if ptype in existing_types or preset_id in existing_ids:
continue
self._items.append(
AiProvider(
id=preset_id,
provider_type=ptype,
base_url=default_base_url(ptype),
api_key="",
api_secret="",
created_at=now,
updated_at=now,
)
)
added += 1
if added or not self.path.is_file() or self.path.stat().st_size == 0:
self.save()
return added
def save(self) -> None:
payload: dict[str, Any] = {
"providers": [p.to_dict() for p in self._items],
"updated_at": datetime.now().isoformat(timespec="seconds"),
}
with self.path.open("w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
def list_providers(self) -> list[AiProvider]:
return list(self._items)
def get(self, provider_id: str) -> AiProvider | None:
for p in self._items:
if p.id == provider_id:
return p
return None
def update(
self,
provider_id: str,
base_url: str,
api_key: str,
api_secret: str = "",
) -> AiProvider | None:
for p in self._items:
if p.id == provider_id:
p.base_url = base_url.strip()
p.api_key = api_key.strip()
p.api_secret = api_secret.strip()
p.updated_at = datetime.now().isoformat(timespec="seconds")
self.save()
return p
return None
def delete(self, provider_id: str) -> bool:
before = len(self._items)
self._items = [p for p in self._items if p.id != provider_id]
if len(self._items) < before:
self.save()
return True
return False