""" 平台配置中心: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 ,取该平台已登录(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