Files
account-manager/scripts/util/platforms.py
chendelian 3f1cde1f99
All checks were successful
技能自动化发布 / release (push) Successful in 6s
fix: validate platform keys at the gate and stop auto-register
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 15:40:30 +08:00

506 lines
18 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()
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_key_dynamic(input_name: str, conn=None) -> PlatformResolveResult:
"""引用已有平台(键 / 唯一展示名或别名)。实现见 util.platform_validate。"""
from util.platform_validate import resolve_platform_ref
return resolve_platform_ref(input_name, conn)
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"]
)
)