Files
account-manager/scripts/util/platforms.py
chendelian 23f4a2c11a
All checks were successful
技能自动化发布 / release (push) Successful in 7s
chore: auto release commit (2026-06-19 09:53:29)
2026-06-19 09:53:29 +08:00

582 lines
20 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.
# -*- coding: utf-8 -*-
"""
Platform definitions and resolution.
Central authoritative registry for all platforms (logistics, LLM, content, etc.).
Import and use PLATFORMS dict, resolve_platform_key(), and seed_platforms().
"""
import json
import re
import time
from dataclasses import dataclass
from typing import Any, Optional
from util.logging_config import get_skill_logger
PLATFORM_KEY_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]{1,63}$")
DEFAULT_CAPABILITIES_JSON = '{"web": true, "api_key": false, "rpa": true}'
# ---------------------------------------------------------------------------
# Platform specification fields:
# display_name Human-readable name (e.g. "Maersk")
# domain Business domain: logistics / llm / content / ecommerce / generic
# provider_code Supplier code (logistics); can be same as key
# default_url Default entry URL (empty string if none)
# aliases List of name aliases (Chinese/English, for CLI resolution)
# capabilities Dict of capability flags:
# {"web": bool, "api_key": bool, "rpa": bool}
# ---------------------------------------------------------------------------
PLATFORMS: dict[str, dict[str, Any]] = {
# =========================================================================
# Logistics platforms
# =========================================================================
"maersk": {
"display_name": "Maersk",
"domain": "logistics",
"provider_code": "maersk",
"default_url": "https://www.maersk.com",
"aliases": ["马士基", "maersk", "Maersk"],
"capabilities": {"web": True, "api_key": False, "rpa": True},
},
"cosco": {
"display_name": "COSCO",
"domain": "logistics",
"provider_code": "cosco",
"default_url": "",
"aliases": ["中远海运", "COSCO", "cosco"],
"capabilities": {"web": True, "api_key": False, "rpa": True},
},
"msc": {
"display_name": "MSC",
"domain": "logistics",
"provider_code": "msc",
"default_url": "",
"aliases": ["MSC", "地中海航运"],
"capabilities": {"web": True, "api_key": False, "rpa": True},
},
"cma_cgm": {
"display_name": "CMA CGM",
"domain": "logistics",
"provider_code": "cma_cgm",
"default_url": "",
"aliases": ["达飞", "CMA CGM", "cma cgm"],
"capabilities": {"web": True, "api_key": False, "rpa": True},
},
"evergreen": {
"display_name": "Evergreen",
"domain": "logistics",
"provider_code": "evergreen",
"default_url": "",
"aliases": ["长荣", "Evergreen", "evergreen"],
"capabilities": {"web": True, "api_key": False, "rpa": True},
},
"cargo_wise": {
"display_name": "CargoWise",
"domain": "logistics",
"provider_code": "cargo_wise",
"default_url": "",
"aliases": ["CargoWise", "Cargo Wise", "cargo wise"],
"capabilities": {"web": True, "api_key": False, "rpa": True},
},
"freightos": {
"display_name": "Freightos",
"domain": "logistics",
"provider_code": "freightos",
"default_url": "",
"aliases": ["Freightos", "freightos"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"webcargo": {
"display_name": "WebCargo",
"domain": "logistics",
"provider_code": "webcargo",
"default_url": "",
"aliases": ["WebCargo", "web cargo"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"sinotrans": {
"display_name": "Sinotrans",
"domain": "logistics",
"provider_code": "sinotrans",
"default_url": "",
"aliases": ["中外运", "Sinotrans", "sinotrans"],
"capabilities": {"web": True, "api_key": False, "rpa": True},
},
# =========================================================================
# LLM / AI platforms
# =========================================================================
"deepseek": {
"display_name": "DeepSeek",
"domain": "llm",
"provider_code": "deepseek",
"default_url": "https://chat.deepseek.com",
"aliases": ["DeepSeek", "deepseek"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"doubao": {
"display_name": "Doubao",
"domain": "llm",
"provider_code": "doubao",
"default_url": "https://www.doubao.com",
"aliases": ["豆包", "Doubao"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"kimi": {
"display_name": "Kimi",
"domain": "llm",
"provider_code": "kimi",
"default_url": "https://kimi.moonshot.cn",
"aliases": ["Kimi", "月之暗面"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"qianwen": {
"display_name": "Qianwen",
"domain": "llm",
"provider_code": "qianwen",
"default_url": "https://tongyi.aliyun.com",
"aliases": ["通义千问", "通义", "千问", "Qianwen"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"yiyan": {
"display_name": "YiYan",
"domain": "llm",
"provider_code": "yiyan",
"default_url": "https://yiyan.baidu.com",
"aliases": ["文心一言", "文心", "一言", "YiYan"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"yuanbao": {
"display_name": "Yuanbao",
"domain": "llm",
"provider_code": "yuanbao",
"default_url": "https://yuanbao.tencent.com",
"aliases": ["腾讯元宝", "元宝", "Yuanbao"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"gemini": {
"display_name": "Gemini",
"domain": "llm",
"provider_code": "gemini",
"default_url": "https://gemini.google.com",
"aliases": ["Gemini", "谷歌Gemini", "Google Gemini", "Bard"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
"minimax": {
"display_name": "MiniMax",
"domain": "llm",
"provider_code": "minimax",
"default_url": "",
"aliases": ["MiniMax", "minimax"],
"capabilities": {"web": True, "api_key": True, "rpa": False},
},
# =========================================================================
# Content platforms (carried forward from v1)
# =========================================================================
"sohu": {
"display_name": "搜狐号",
"domain": "content",
"provider_code": "sohu",
"default_url": "https://mp.sohu.com",
"aliases": ["搜狐", "搜狐号"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"toutiao": {
"display_name": "头条号",
"domain": "content",
"provider_code": "toutiao",
"default_url": "https://mp.toutiao.com/",
"aliases": ["头条", "头条号"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"zhihu": {
"display_name": "知乎",
"domain": "content",
"provider_code": "zhihu",
"default_url": "https://www.zhihu.com",
"aliases": ["知乎", "知乎号"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"wechat": {
"display_name": "微信公众号",
"domain": "content",
"provider_code": "wechat",
"default_url": "https://mp.weixin.qq.com",
"aliases": ["公众号", "微信", "微信公众号"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
# =========================================================================
# Aligned with db.platform_defaults.PLATFORM_DEFAULT_AUTH_STRATEGY keys
# =========================================================================
"douyin": {
"display_name": "抖音",
"domain": "content",
"provider_code": "douyin",
"default_url": "https://www.douyin.com",
"aliases": ["抖音", "Douyin"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"wechat_mp": {
"display_name": "微信公众号",
"domain": "content",
"provider_code": "wechat_mp",
"default_url": "https://mp.weixin.qq.com",
"aliases": ["微信公众号", "公众号", "WeChat MP"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"xiaohongshu": {
"display_name": "小红书",
"domain": "content",
"provider_code": "xiaohongshu",
"default_url": "https://www.xiaohongshu.com",
"aliases": ["小红书", "Xiaohongshu", "RED"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"bilibili": {
"display_name": "哔哩哔哩",
"domain": "content",
"provider_code": "bilibili",
"default_url": "https://www.bilibili.com",
"aliases": ["B站", "哔哩哔哩", "Bilibili"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"github": {
"display_name": "GitHub",
"domain": "dev",
"provider_code": "github",
"default_url": "https://github.com/login",
"aliases": ["GitHub", "github"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"aws": {
"display_name": "AWS",
"domain": "dev",
"provider_code": "aws",
"default_url": "https://console.aws.amazon.com",
"aliases": ["AWS", "Amazon Web Services", "亚马逊云"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"gcp": {
"display_name": "GCP",
"domain": "dev",
"provider_code": "gcp",
"default_url": "https://console.cloud.google.com",
"aliases": ["GCP", "Google Cloud", "谷歌云"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"icbc_sim": {
"display_name": "工行仿真",
"domain": "sim",
"provider_code": "icbc_sim",
"default_url": "http://localhost:5180",
"aliases": ["工行仿真", "ICBC Simulator", "ICBC SIM"],
"capabilities": {"web": True, "api_key": False, "rpa": False},
},
"openai": {
"display_name": "OpenAI API",
"domain": "llm",
"provider_code": "openai",
"default_url": "https://platform.openai.com",
"aliases": ["OpenAI", "openai", "GPT", "ChatGPT API"],
"capabilities": {"web": False, "api_key": True, "rpa": False},
},
"anthropic": {
"display_name": "Anthropic",
"domain": "llm",
"provider_code": "anthropic",
"default_url": "https://console.anthropic.com",
"aliases": ["Anthropic", "anthropic", "Claude API"],
"capabilities": {"web": False, "api_key": True, "rpa": False},
},
}
# ---------------------------------------------------------------------------
# Derived alias map
# ---------------------------------------------------------------------------
def _build_alias_map() -> dict[str, str]:
"""Build a case-insensitive flat alias -> key map."""
m: dict[str, str] = {}
for key, spec in PLATFORMS.items():
# Register the key itself (lowercase)
m[key.lower()] = key
# Register all aliases (original + lowercase)
for alias in [spec["display_name"]] + spec.get("aliases", []):
a = str(alias).strip()
if a:
m[a] = key
m[a.lower()] = key
return m
_PLATFORM_ALIAS_MAP = _build_alias_map()
def resolve_platform_key(name: str) -> Optional[str]:
"""
Resolve user input to a canonical platform key (built-in PLATFORMS only).
Supports: key, display_name, any alias. Case-insensitive.
Returns None if unrecognized.
"""
if not name:
return None
s = str(name).strip()
if not s:
return None
sl = s.lower()
# Direct key match
if sl in PLATFORMS:
return sl
# Alias map
if sl in _PLATFORM_ALIAS_MAP:
return _PLATFORM_ALIAS_MAP[sl]
if s in _PLATFORM_ALIAS_MAP:
return _PLATFORM_ALIAS_MAP[s]
return None
def is_valid_platform_key_format(name: str) -> bool:
"""平台 key 格式:^[a-z0-9][a-z0-9_-]{1,63}$"""
s = (name or "").strip().lower()
if not s:
return False
return bool(PLATFORM_KEY_PATTERN.match(s))
def _parse_aliases_json(val: Any) -> list[str]:
if val is None:
return []
try:
data = json.loads(val) if isinstance(val, str) else val
if isinstance(data, list):
return [str(a).strip() for a in data if str(a).strip()]
except (json.JSONDecodeError, TypeError, ValueError):
pass
return []
@dataclass(frozen=True)
class PlatformResolveResult:
platform_key: Optional[str] = None
error_code: Optional[str] = None
message: Optional[str] = None
@property
def ok(self) -> bool:
return bool(self.platform_key) and not self.error_code
def _resolve_platform_from_db(conn, raw: str) -> PlatformResolveResult:
"""在 platforms 表中解析:精确 keyenabled=1与 aliases_json。"""
raw_stripped = (raw or "").strip()
raw_lower = raw_stripped.lower()
if is_valid_platform_key_format(raw_stripped):
row = conn.execute(
"SELECT platform_key, enabled FROM platforms WHERE platform_key = ?",
(raw_lower,),
).fetchone()
if row:
if not row[1]:
return PlatformResolveResult(
error_code="PLATFORM_DISABLED",
message=f"平台 {row[0]} 已禁用。",
)
return PlatformResolveResult(platform_key=row[0])
rows = conn.execute(
"SELECT platform_key, enabled, aliases_json FROM platforms"
).fetchall()
for platform_key, enabled, aliases_json in rows:
if not enabled:
continue
names = {platform_key.lower()}
for alias in _parse_aliases_json(aliases_json):
names.add(alias.lower())
names.add(alias)
if raw_lower in names or raw_stripped in names:
return PlatformResolveResult(platform_key=platform_key)
return PlatformResolveResult()
def resolve_platform_key_dynamic(input_name: str, conn=None) -> PlatformResolveResult:
"""
统一平台解析:内置 PLATFORMS → DB platforms 表 → 错误分类。
"""
raw = (input_name or "").strip()
if not raw:
return PlatformResolveResult(
error_code="INVALID_PLATFORM",
message="平台名不能为空。",
)
builtin = resolve_platform_key(raw)
if builtin:
return PlatformResolveResult(platform_key=builtin)
close_conn = False
if conn is None:
from db.connection import get_conn, init_db
init_db()
conn = get_conn()
close_conn = True
try:
db_result = _resolve_platform_from_db(conn, raw)
if db_result.ok or db_result.error_code:
return db_result
finally:
if close_conn:
conn.close()
if not is_valid_platform_key_format(raw):
return PlatformResolveResult(
error_code="INVALID_PLATFORM_KEY",
message=(
f"平台 key 格式非法:「{raw}」。"
"建议使用小写字母、数字、下划线或连字符,例如 jiangchang。"
),
)
key_hint = raw.lower()
return PlatformResolveResult(
error_code="PLATFORM_NOT_REGISTERED",
message=(
f"平台 {key_hint} 尚未注册,请先执行 "
f"account platform ensure --key {key_hint} --display-name <名称> [--url <url>]"
),
)
def get_platform_spec(platform_key: str, conn=None) -> dict[str, Any]:
"""内置 PLATFORMS 优先结构;自定义平台从 DB platforms 表读取。"""
spec = PLATFORMS.get(platform_key)
if spec:
return dict(spec)
close_conn = False
if conn is None:
from db.connection import get_conn, init_db
init_db()
conn = get_conn()
close_conn = True
try:
from db.accounts_repo import get_platform_from_db
plat = get_platform_from_db(conn, platform_key)
if plat:
return {
"display_name": plat["display_name"],
"domain": plat.get("domain") or "generic",
"provider_code": plat.get("provider_code") or platform_key,
"default_url": plat.get("default_url") or "",
"aliases": plat.get("aliases") or [],
"capabilities": plat.get("capabilities") or {},
}
finally:
if close_conn:
conn.close()
return {}
def get_platform(key: str) -> Optional[dict[str, Any]]:
"""Return the full platform spec dict for a validated built-in key."""
return PLATFORMS.get(key)
def list_platforms(domain: Optional[str] = None) -> list[dict[str, Any]]:
"""List all platforms, optionally filtered by domain.
Returns a list of dicts with metadata."""
result = []
for key, spec in PLATFORMS.items():
if domain and spec["domain"] != domain:
continue
result.append({
"platform_key": key,
"display_name": spec["display_name"],
"domain": spec["domain"],
"provider_code": spec.get("provider_code"),
"default_url": spec.get("default_url", ""),
"aliases": spec.get("aliases", []),
"capabilities": spec.get("capabilities", {}),
"enabled": True,
})
return result
def seed_platforms(conn) -> None:
"""
Upsert all platform definitions into the 'platforms' table.
Must be called after init_db() in the same connection lifecycle.
"""
from db.platform_defaults import PLATFORM_DEFAULT_AUTH_STRATEGY
log = get_skill_logger()
log.info("platform_seed_start")
cur = conn.cursor()
now = int(time.time())
upsert_count = 0
for key, spec in PLATFORMS.items():
aliases_json = json.dumps(spec.get("aliases", []), ensure_ascii=False)
capabilities_json = json.dumps(spec.get("capabilities", {}), ensure_ascii=False)
default_auth_strategy = PLATFORM_DEFAULT_AUTH_STRATEGY.get(key)
cur.execute(
"""
INSERT INTO platforms (platform_key, display_name, domain, provider_code,
default_url, aliases_json, capabilities_json,
enabled, default_auth_strategy, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
ON CONFLICT(platform_key) DO UPDATE SET
display_name = excluded.display_name,
domain = excluded.domain,
provider_code = excluded.provider_code,
default_url = excluded.default_url,
aliases_json = excluded.aliases_json,
capabilities_json = excluded.capabilities_json,
enabled = 1,
default_auth_strategy = excluded.default_auth_strategy,
updated_at = excluded.updated_at
""",
(
key,
spec["display_name"],
spec["domain"],
spec.get("provider_code"),
spec.get("default_url", ""),
aliases_json,
capabilities_json,
default_auth_strategy,
now,
now,
),
)
upsert_count += 1
conn.commit()
log.info("platform_seed_done count=%s", upsert_count)
# ---------------------------------------------------------------------------
# Legacy compatibility helpers (for old account_service v1 code references)
# ---------------------------------------------------------------------------
# Kept for backward compatibility so old imports don't break immediately.
# New code should use PLATFORMS dict directly and resolve_platform_key().
_PLATFORM_PRIMARY_CN: dict[str, str] = {
k: spec["display_name"] for k, spec in PLATFORMS.items()
}
PLATFORM_URLS: dict[str, str] = {
k: (spec.get("default_url") or "") for k, spec in PLATFORMS.items()
}
def _platform_list_cn_for_help() -> str:
"""帮助文案:一行展示名,供 CLI 用法提示。"""
return "".join(
spec["display_name"] for _k, spec in sorted(
PLATFORMS.items(), key=lambda x: x[1]["display_name"]
)
)