200 lines
7.1 KiB
Python
200 lines
7.1 KiB
Python
"""
|
||
平台配置中心:7 个 LLM 平台的静态配置、别名解析、以及通过 account-manager 暴露的 CLI 查询已登录网页账号。
|
||
"""
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 平台静态配置
|
||
# ---------------------------------------------------------------------------
|
||
LLM_PROVIDERS = {
|
||
"doubao": {
|
||
"label": "豆包",
|
||
"aliases": ["豆包"],
|
||
"web_url": "https://www.doubao.com/chat/",
|
||
"api_base": "https://ark.volces.com/api/v3",
|
||
"api_model": None, # 豆包需要用户在火山引擎控制台建推理接入点,model=ep-xxx
|
||
"has_api": True,
|
||
"api_note": "需先在火山引擎控制台创建推理接入点,--model 传 endpoint_id(格式 ep-xxx)",
|
||
},
|
||
"deepseek": {
|
||
"label": "DeepSeek",
|
||
"aliases": ["深度求索"],
|
||
"web_url": "https://chat.deepseek.com",
|
||
"api_base": "https://api.deepseek.com/v1",
|
||
"api_model": "deepseek-chat",
|
||
"has_api": True,
|
||
"api_note": "模型可选:deepseek-chat / deepseek-reasoner",
|
||
},
|
||
"qianwen": {
|
||
"label": "通义千问",
|
||
"aliases": ["通义", "千问", "qwen", "tongyi"],
|
||
"web_url": "https://tongyi.aliyun.com/qianwen/",
|
||
"api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
"api_model": "qwen-plus",
|
||
"has_api": True,
|
||
"api_note": "模型可选:qwen-turbo / qwen-plus / qwen-max",
|
||
},
|
||
"kimi": {
|
||
"label": "Kimi",
|
||
"aliases": ["月之暗面", "moonshot"],
|
||
"web_url": "https://kimi.moonshot.cn",
|
||
"api_base": "https://api.moonshot.cn/v1",
|
||
"api_model": "moonshot-v1-8k",
|
||
"has_api": True,
|
||
"api_note": "模型可选:moonshot-v1-8k / moonshot-v1-32k / moonshot-v1-128k",
|
||
},
|
||
"yiyan": {
|
||
"label": "文心一言",
|
||
"aliases": ["文心", "一言", "ernie", "wenxin"],
|
||
"web_url": "https://yiyan.baidu.com",
|
||
"api_base": "https://qianfan.baidubce.com/v2",
|
||
"api_model": "ernie-4.0-8k",
|
||
"has_api": True,
|
||
"api_note": "模型可选:ernie-4.0-8k / ernie-3.5-8k",
|
||
},
|
||
"yuanbao": {
|
||
"label": "腾讯元宝",
|
||
"aliases": ["元宝"],
|
||
"web_url": "https://yuanbao.tencent.com/chat",
|
||
"api_base": None,
|
||
"api_model": None,
|
||
"has_api": False,
|
||
"api_note": "暂无公开 API,仅支持网页模式",
|
||
},
|
||
"minimax": {
|
||
"label": "MiniMax",
|
||
"aliases": ["minimax", "MiniMax", "海螺", "海螺AI"],
|
||
"web_url": "https://chat.minimax.io/",
|
||
"api_base": "https://api.minimax.chat/v1",
|
||
"api_model": "MiniMax-Text-01",
|
||
"has_api": True,
|
||
"api_note": "模型可按 MiniMax 控制台可用模型调整,建议通过 --model 显式指定。",
|
||
},
|
||
}
|
||
|
||
# 构建别名查找表(含中文、英文键、aliases)
|
||
_ALIAS_TO_KEY: dict = {}
|
||
for _k, _spec in LLM_PROVIDERS.items():
|
||
_ALIAS_TO_KEY[_k] = _k
|
||
_ALIAS_TO_KEY[_k.lower()] = _k
|
||
_ALIAS_TO_KEY[_spec["label"]] = _k
|
||
for _a in (_spec.get("aliases") or []):
|
||
_ALIAS_TO_KEY[_a] = _k
|
||
_ALIAS_TO_KEY[_a.lower()] = _k
|
||
|
||
|
||
def resolve_provider_key(name: str):
|
||
"""将用户输入的平台名称/别名解析为内部 slug;无法识别返回 None。"""
|
||
if not name:
|
||
return None
|
||
s = str(name).strip()
|
||
return _ALIAS_TO_KEY.get(s) or _ALIAS_TO_KEY.get(s.lower())
|
||
|
||
|
||
def provider_list_cn() -> str:
|
||
return "、".join(s["label"] for s in LLM_PROVIDERS.values())
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 路径帮助(与 account-manager/scripts/main.py 完全一致:仅 JIANGCHANG_*)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
_OPENCLAW_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
|
||
def get_data_root() -> str:
|
||
env = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip()
|
||
if env:
|
||
return env
|
||
if sys.platform == "win32":
|
||
return r"D:\jiangchang-data"
|
||
return os.path.join(os.path.expanduser("~"), ".jiangchang-data")
|
||
|
||
|
||
def get_user_id() -> str:
|
||
uid = (os.getenv("JIANGCHANG_USER_ID") or "").strip()
|
||
return uid or "_anon"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 跨技能:仅通过 account-manager 提供的 CLI 读取账号(不直接打开其数据库文件)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _account_manager_script_path() -> str:
|
||
return os.path.join(_OPENCLAW_DIR, "account-manager", "scripts", "main.py")
|
||
|
||
|
||
def find_logged_in_account(provider_key: str) -> dict | None:
|
||
"""
|
||
调用 account-manager:pick-logged-in <platform_key>,取该平台已登录(login_status=1)的优先账号。
|
||
成功返回与 account.py get 一致的 dict;失败或不可用时返回 None。
|
||
"""
|
||
script = _account_manager_script_path()
|
||
if not os.path.isfile(script):
|
||
return None
|
||
try:
|
||
proc = subprocess.run(
|
||
[sys.executable, script, "pick-logged-in", provider_key],
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
)
|
||
except OSError:
|
||
return None
|
||
raw = (proc.stdout or "").strip()
|
||
if not raw or raw.startswith("ERROR:"):
|
||
return None
|
||
try:
|
||
data = json.loads(raw.splitlines()[0])
|
||
except (json.JSONDecodeError, IndexError):
|
||
return None
|
||
if not isinstance(data, dict) or data.get("id") is None:
|
||
return None
|
||
plat = data.get("platform") or provider_key
|
||
if not (data.get("url") or "").strip():
|
||
data["url"] = LLM_PROVIDERS.get(provider_key, {}).get("web_url", "")
|
||
data["platform"] = plat
|
||
return data
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Chrome/Edge 检测(与 account-manager 逻辑保持一致)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _win_find_exe(candidates):
|
||
for p in candidates:
|
||
if p and os.path.isfile(p):
|
||
return p
|
||
return None
|
||
|
||
|
||
def resolve_chromium_channel() -> str | None:
|
||
"""返回 'chrome' | 'msedge' | None。"""
|
||
if sys.platform != "win32":
|
||
return "chrome"
|
||
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
||
pfx86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
||
local = os.environ.get("LocalAppData", "")
|
||
|
||
chrome = _win_find_exe([
|
||
os.path.join(pf, "Google", "Chrome", "Application", "chrome.exe"),
|
||
os.path.join(pfx86, "Google", "Chrome", "Application", "chrome.exe"),
|
||
os.path.join(local, "Google", "Chrome", "Application", "chrome.exe") if local else "",
|
||
])
|
||
if chrome:
|
||
return "chrome"
|
||
|
||
edge = _win_find_exe([
|
||
os.path.join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"),
|
||
os.path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"),
|
||
os.path.join(local, "Microsoft", "Edge", "Application", "msedge.exe") if local else "",
|
||
])
|
||
if edge:
|
||
return "msedge"
|
||
|
||
return None
|