- 新增小红书前端模块(启动服务、内容分析、爆款创作、发布记录、技能卡片、拟人设置、AI 助手) - 新增 xhs-agent-desktop 独立桌面 Agent,对接 OMS 任务调度 - 登录页重构,整合微信登录与 OMS JWT 认证 - 订单、渠道、用户中心等页面与 API 增强 Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
5.4 KiB
Python
172 lines
5.4 KiB
Python
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
|