342 lines
12 KiB
Python
342 lines
12 KiB
Python
# -*- 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 time
|
||
from typing import Any, Optional
|
||
|
||
from util.logging_config import get_skill_logger
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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},
|
||
},
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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.
|
||
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 get_platform(key: str) -> Optional[dict[str, Any]]:
|
||
"""Return the full platform spec dict for a validated 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.
|
||
"""
|
||
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)
|
||
cur.execute(
|
||
"""
|
||
INSERT INTO platforms (platform_key, display_name, domain, provider_code,
|
||
default_url, aliases_json, capabilities_json,
|
||
enabled, 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,
|
||
updated_at = excluded.updated_at
|
||
""",
|
||
(
|
||
key,
|
||
spec["display_name"],
|
||
spec["domain"],
|
||
spec.get("provider_code"),
|
||
spec.get("default_url", ""),
|
||
aliases_json,
|
||
capabilities_json,
|
||
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"]
|
||
)
|
||
)
|