Add OpenClaw skills, platform kit, and template docs

Made-with: Cursor
This commit is contained in:
2026-04-04 10:35:02 +08:00
parent e37b03c00f
commit 35f4758da2
83 changed files with 8971 additions and 0 deletions

575
llm-manager/scripts/main.py Normal file
View File

@@ -0,0 +1,575 @@
"""
llm-manager 主入口 CLI。
子命令:
health 快速离线健康检查
version 输出版本 JSON
add <platform> [api_key] 添加 API Key不传 api_key 则走网页账号关联)
key list [platform] 列出 Key打码
key del <key_id> 删除 Key
generate <platform_or_account_id> 生成内容(优先网页模式,备用 API Key 模式)
"<prompt>"
generate 调度规则:
1. 若 target 为纯数字 → 视为 account-manager 账号 ID → 强制网页模式
2. 若 target 为平台名/别名:
a. 先查 account-manager 有无该平台已登录账号 → 网页模式(免费)
b. 再查 llm_keys 有无可用 Key → API Key 模式(付费)
c. 两者均无 → 报错并给出操作指引
"""
import sys
import json
import os
import asyncio
import subprocess
# Windows GBK 编码兼容修复
if sys.platform == "win32":
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OPENCLAW_DIR = os.path.dirname(BASE_DIR)
# 确保 scripts 目录在 sys.path使 providers/db 可直接 import
_scripts_dir = os.path.dirname(os.path.abspath(__file__))
if _scripts_dir not in sys.path:
sys.path.insert(0, _scripts_dir)
from providers import (
LLM_PROVIDERS,
resolve_provider_key,
provider_list_cn,
find_logged_in_account,
resolve_chromium_channel,
)
from db import (
add_key,
upsert_web_account,
list_keys,
list_web_accounts,
get_key_by_id,
delete_key,
find_active_key,
mark_key_used,
_mask_key,
)
from engines.api_engine import ApiEngine
from engines.kimi import KimiEngine
from engines.doubao import DoubaoEngine
from engines.deepseek import DeepSeekEngine
from engines.qianwen import QianwenEngine
from engines.yiyan import YiyanEngine
from engines.yuanbao import YuanbaoEngine
# 平台 slug → 网页引擎类(全部 6 个平台)
WEB_ENGINES = {
"doubao": DoubaoEngine,
"deepseek": DeepSeekEngine,
"qianwen": QianwenEngine,
"kimi": KimiEngine,
"yiyan": YiyanEngine,
"yuanbao": YuanbaoEngine,
}
SKILL_VERSION = "1.0.3"
def _engine_result_is_error(text: str) -> bool:
"""网页/API 引擎约定:失败时返回以 ERROR: 开头的字符串,不得当作正文包进 LLM 标记块。"""
return (text or "").lstrip().startswith("ERROR:")
def _unix_to_iso(ts):
if ts is None:
return ""
try:
import datetime as _dt
return _dt.datetime.fromtimestamp(int(ts)).isoformat(timespec="seconds")
except Exception:
return str(ts)
# ---------------------------------------------------------------------------
# account-manager 跨 Skill 调用
# ---------------------------------------------------------------------------
def _get_account_from_manager(account_id) -> dict | None:
"""通过 account-manager CLI 按 ID 取账号 JSON。"""
script = os.path.join(OPENCLAW_DIR, "account-manager", "scripts", "main.py")
try:
result = subprocess.run(
[sys.executable, script, "get", str(account_id)],
capture_output=True, text=True, encoding="utf-8", errors="replace",
)
raw = result.stdout.strip()
if raw.startswith("ERROR"):
return None
return json.loads(raw)
except Exception:
return None
# ---------------------------------------------------------------------------
# 网页模式
# ---------------------------------------------------------------------------
async def _run_web_generate(account: dict, prompt: str) -> bool:
from playwright.async_api import async_playwright
platform = account.get("platform", "")
profile_dir = account.get("profile_dir", "").strip()
engine_cls = WEB_ENGINES.get(platform)
if not engine_cls:
print(f"ERROR:ENGINE_NOT_FOUND 平台 {platform} 暂无网页引擎实现。")
return False
if not profile_dir or not os.path.isdir(profile_dir):
print(f"ERROR:PROFILE_DIR_MISSING 账号 {account.get('id')} 的浏览器数据目录不存在:{profile_dir}")
print("请先通过 account-manager 执行登录python account-manager/scripts/main.py login <id>")
return False
channel = resolve_chromium_channel()
if not channel:
print("ERROR:浏览器未找到。请安装 Google Chrome 或 Microsoft Edge 后重试。")
return False
print(f"[llm-manager] 使用网页模式 | 平台: {LLM_PROVIDERS.get(platform, {}).get('label', platform)} | 账号: {account.get('name', account.get('id'))}")
async with async_playwright() as p:
browser = await p.chromium.launch_persistent_context(
user_data_dir=profile_dir,
headless=False,
channel=channel,
no_viewport=True,
permissions=["clipboard-read", "clipboard-write"],
args=["--start-maximized"],
)
try:
page = browser.pages[0] if browser.pages else await browser.new_page()
engine = engine_cls(page)
try:
result = await engine.generate(prompt)
except Exception as e:
print(f"ERROR:WEB_GENERATE_FAILED 网页模式生成失败:{e}")
return False
result = (result or "").strip()
if _engine_result_is_error(result):
print(result)
return False
print("===LLM_START===")
print(result)
print("===LLM_END===")
return True
finally:
await asyncio.sleep(3)
await browser.close()
# ---------------------------------------------------------------------------
# API Key 模式
# ---------------------------------------------------------------------------
def _run_api_generate(provider_key: str, key_record: dict, prompt: str) -> bool:
provider = LLM_PROVIDERS[provider_key]
api_base = provider.get("api_base")
model = (key_record.get("default_model") or "").strip() or (provider.get("api_model") or "").strip()
if not api_base:
print(f"ERROR:NO_API_BASE {provider['label']} 暂无 API 地址(不支持 API 模式)。")
return False
if not model:
print(
f"ERROR:API_MODEL_MISSING {provider['label']} 需要指定模型名称。\n"
f"提示:{provider.get('api_note', '')}\n"
f"请删除该 Key 并重新添加时带上 --model 参数python main.py add {provider_key} \"Key\" --model \"模型名\""
)
return False
print(f"[llm-manager] 使用 API Key 模式 | 平台: {provider['label']} | 模型: {model}")
try:
engine = ApiEngine(api_base=api_base, api_key=key_record["api_key"], model=model)
result = engine.generate(prompt)
except Exception as e:
print(f"ERROR:API_GENERATE_FAILED API 模式调用失败:{e}")
return False
result = (result or "").strip()
if _engine_result_is_error(result):
print(result)
return False
mark_key_used(key_record["id"])
print("===LLM_START===")
print(result)
print("===LLM_END===")
return True
# ---------------------------------------------------------------------------
# generate 命令
# ---------------------------------------------------------------------------
def cmd_generate(target: str, prompt: str) -> bool:
"""
target 可以是:
- 纯数字 → account-manager 账号 ID强制网页模式
- 平台名/别名 → 自动选择模式网页优先API Key 备用)
成功返回 True任一步失败返回 False调用方应设进程退出码非 0
"""
target = (target or "").strip()
prompt = (prompt or "").strip()
if not prompt:
print("ERROR:PROMPT_EMPTY 提示词不能为空。")
return False
# ---- 情况 1显式 account_id纯数字→ 强制网页模式 ----
if target.isdigit():
account = _get_account_from_manager(target)
if not account:
print(f"ERROR:ACCOUNT_NOT_FOUND 未在「模型管理」对应的账号列表中找到 id={target}")
print("请先在模型管理中添加该平台账号,或检查 id 是否抄错。")
return False
if account.get("login_status") != 1:
print(
f"ERROR:REQUIRE_LOGIN 账号「{account.get('name', target)}」尚未完成网页登录。\n"
"请先在「模型管理」里确认已添加该账号,再执行下面命令完成登录:\n"
f" python account-manager/scripts/main.py login {target}\n"
"若使用网页模式:请先在对应平台官网注册好账号,再回到本工具添加并登录。"
)
return False
return asyncio.run(_run_web_generate(account, prompt))
# ---- 情况 2平台名 → 自动选模式 ----
provider_key = resolve_provider_key(target)
if not provider_key:
print(f"ERROR:INVALID_PLATFORM 无法识别的平台「{target}」。")
print("支持的平台:" + provider_list_cn())
return False
provider = LLM_PROVIDERS[provider_key]
label = provider["label"]
web_url = (provider.get("web_url") or "").strip()
# 优先级 1查 account-manager 已登录账号(网页模式,免费)
account = find_logged_in_account(provider_key)
if account:
return asyncio.run(_run_web_generate(account, prompt))
# 优先级 2查本地 API Key付费
key_record = find_active_key(provider_key)
if key_record:
return _run_api_generate(provider_key, key_record, prompt)
# 两种凭据均无 → 明确说明「模型管理 + 网页模式需官网账号」
print(f"ERROR:NO_CREDENTIAL 当前没有可用的「{label}」调用凭据(既没有已登录的网页账号,也没有可用的 API Key")
print()
print("【推荐 · 网页模式(免费)】按顺序做:")
print(f" ① 打开「模型管理」(与 OpenClaw 里 account-manager 账号管理是同一套数据),先把「{label}」账号添加进去。")
print(" 命令行示例(在 OpenClaw 根目录执行):")
print(f" python account-manager/scripts/main.py add \"{label}\" \"你的手机号或登录名\"")
print(" python account-manager/scripts/main.py login <上一步返回的账号 id>")
print(" ② 若走网页模式:请先在对应平台官方网站注册并创建好账号(否则浏览器里登录会失败)。")
if web_url:
print(f"{label}」网页入口:{web_url}")
print()
if provider.get("has_api"):
print("【备选 · API Key付费】若已在厂商控制台开通 API可本地登记 Key 后走接口调用:")
print(f" python llm-manager/scripts/main.py add {provider_key} \"你的API Key\"")
if provider.get("api_note"):
print(f" 说明:{provider['api_note']}")
else:
print(f"说明:「{label}」暂无公开 API只能使用上面的网页模式。")
return False
# ---------------------------------------------------------------------------
# key 子命令
# ---------------------------------------------------------------------------
def cmd_key_add(provider_input: str, api_key: str = "", model: str = None, label: str = ""):
provider_key = resolve_provider_key(provider_input)
if not provider_key:
print(f"ERROR:INVALID_PLATFORM 无法识别的平台「{provider_input}」。")
print("支持:" + provider_list_cn())
return
provider = LLM_PROVIDERS[provider_key]
api_key = (api_key or "").strip()
# 不传 api_key默认走网页版本检查 account-manager 是否已有该平台已登录账号
if not api_key:
account = find_logged_in_account(provider_key)
if account:
web_row_id = upsert_web_account(
provider=provider_key,
account_id=int(account.get("id")),
account_name=(account.get("name") or account.get("account_name") or ""),
login_status=int(account.get("login_status") or 0),
)
print(
f"OK:WEB_ACCOUNT_READY 已关联网页模式账号 | 平台: {provider['label']} "
f"| account_id: {account.get('id')} | 账号: {account.get('name') or account.get('account_name') or ''}"
)
print(f"已写入 llm-manager 记录WEB_ID {web_row_id}")
print(f"后续可直接调用python main.py generate {provider_key} \"<提示词>\"")
return
print(f"ERROR:WEB_ACCOUNT_NOT_FOUND 未找到已登录的「{provider['label']}」账号,暂时无法走网页模式。")
print("请先在 account-manager 添加并登录该平台账号:")
print(f" python account-manager/scripts/main.py add \"{provider['label']}\" \"你的登录名\"")
print(" python account-manager/scripts/main.py login <账号id>")
if provider.get("has_api"):
print("或直接提供 API Key")
print(f" python main.py add {provider_key} \"<API_Key>\"")
return
if not provider.get("has_api"):
print(f"ERROR:{provider['label']} 暂无公开 API不支持 API Key 模式。")
print("请改用网页模式并先在 account-manager 完成登录。")
return
new_id = add_key(provider=provider_key, api_key=api_key, model=model, label=label)
print(f"✅ 已保存 API KeyID {new_id} | {provider['label']} | 模型: {model or provider.get('api_model') or '(未指定)'} | {_mask_key(api_key)}")
if not model and not provider.get("api_model"):
print(f"⚠️ 注意:该平台需要指定模型名称,否则调用时会报错。")
print(f" {provider.get('api_note', '')}")
print(f" 可删除后重新添加python main.py key del {new_id}")
def cmd_key_list(provider_input: str = None, limit: int = 10):
provider_key = None
if provider_input:
provider_key = resolve_provider_key(provider_input)
if not provider_key:
print(f"ERROR:INVALID_PLATFORM 无法识别的平台「{provider_input}」。")
return
keys = list_keys(provider_key, limit=limit)
web_accounts = list_web_accounts(provider_key, limit=limit)
if not keys and not web_accounts:
print("暂无记录API Key / 网页账号关联 都为空)。")
print("添加 API Keypython main.py add <platform> \"API Key\" [--model 模型名]")
print("添加网页账号关联python main.py add <platform>")
return
sep_line = "_" * 39
rows = []
for k in keys:
rows.append({
"type": "api_key",
"created_at": int(k.get("created_at") or 0),
"payload": k,
})
for w in web_accounts:
rows.append({
"type": "web_account",
"created_at": int(w.get("created_at") or 0),
"payload": w,
})
rows.sort(key=lambda x: (x["created_at"], int(x["payload"].get("id") or 0)), reverse=True)
for idx, row in enumerate(rows):
if row["type"] == "api_key":
k = row["payload"]
print("record_typeapi_key")
print(f"id{k['id']}")
print(f"platform{k['provider']}")
print(f"platform_cn{LLM_PROVIDERS.get(k['provider'], {}).get('label', k['provider'])}")
print(f"label{k.get('label') or ''}")
print(f"api_key{_mask_key(k.get('api_key') or '')}")
print(f"default_model{k.get('default_model') or ''}")
print(f"is_active{int(k.get('is_active') or 0)}")
print(f"last_used_at{_unix_to_iso(k.get('last_used_at'))}")
print(f"created_at{_unix_to_iso(k.get('created_at'))}")
else:
w = row["payload"]
print("record_typeweb_account")
print(f"id{w['id']}")
print(f"platform{w['provider']}")
print(f"platform_cn{LLM_PROVIDERS.get(w['provider'], {}).get('label', w['provider'])}")
print(f"account_id{w.get('account_id')}")
print(f"account_name{w.get('account_name') or ''}")
print(f"login_status{int(w.get('login_status') or 0)}")
print(f"created_at{_unix_to_iso(w.get('created_at'))}")
print(f"updated_at{_unix_to_iso(w.get('updated_at'))}")
if idx != len(rows) - 1:
print(sep_line)
print()
def cmd_key_del(key_id_str: str):
if not key_id_str.isdigit():
print(f"ERROR:INVALID_ID key_id 须为正整数,收到「{key_id_str}」。")
return
key_id = int(key_id_str)
key = get_key_by_id(key_id)
if not key:
print(f"ERROR:KEY_NOT_FOUND 未找到 ID={key_id} 的 API Key。")
return
delete_key(key_id)
label_str = f"{key['label']}" if key.get("label") else ""
print(f"✅ 已删除ID {key_id} | {LLM_PROVIDERS.get(key['provider'], {}).get('label', key['provider'])}{label_str} | {_mask_key(key['api_key'])}")
# ---------------------------------------------------------------------------
# health / version
# ---------------------------------------------------------------------------
def cmd_health():
ok = sys.version_info >= (3, 10)
sys.exit(0 if ok else 1)
def cmd_version():
print(json.dumps({"version": SKILL_VERSION, "skill": "llm-manager"}, ensure_ascii=False))
# ---------------------------------------------------------------------------
# CLI 解析
# ---------------------------------------------------------------------------
def _print_usage():
print("用法:")
print(" python main.py health")
print(" python main.py version")
print(" python main.py generate <平台名或account_id> \"<提示词>\"")
print(" python main.py add <平台> [API_Key] [--model 模型名] [--label 备注]")
print(" python main.py list [平台] [--limit 条数]")
print(" python main.py del <key_id>")
print()
print("支持的平台:" + provider_list_cn())
print()
print("generate 说明:")
print(" · 传入 account_id数字→ 指定账号网页模式(需先用 account-manager 登录)")
print(" · 传入平台名 → 自动选择:优先网页模式(免费),无可用账号时才用 API Key付费")
print()
print("兼容说明key add/list/del 旧写法仍可用。")
def main(argv=None) -> int:
args = argv if argv is not None else sys.argv[1:]
if not args:
_print_usage()
return 1
if args[0] in ("-h", "--help"):
_print_usage()
return 0
def _parse_list_args(rest_args):
platform_filter = None
limit = 10
i = 0
while i < len(rest_args):
if rest_args[i] == "--limit" and i + 1 < len(rest_args):
try:
limit = int(rest_args[i + 1])
except ValueError:
print(f"ERROR:CLI_KEY_LIST_BAD_LIMIT limit 必须是整数,收到「{rest_args[i + 1]}」。")
return None, None, 1
i += 2
else:
if platform_filter is None:
platform_filter = rest_args[i]
i += 1
return platform_filter, limit, 0
def _parse_add_args(rest_args):
if len(rest_args) < 1:
print("ERROR:CLI_ADD_MISSING_ARGS")
print("用法python main.py add <平台> [API_Key] [--model 模型] [--label 备注]")
return None, None, None, None, 1
platform_arg = rest_args[0]
api_key_arg = ""
model_arg = None
label_arg = ""
i = 1
if i < len(rest_args) and not rest_args[i].startswith("--"):
api_key_arg = rest_args[i]
i += 1
while i < len(rest_args):
if rest_args[i] == "--model" and i + 1 < len(rest_args):
model_arg = rest_args[i + 1]
i += 2
elif rest_args[i] == "--label" and i + 1 < len(rest_args):
label_arg = rest_args[i + 1]
i += 2
else:
i += 1
return platform_arg, api_key_arg, model_arg, label_arg, 0
cmd = args[0]
if cmd == "health":
cmd_health()
elif cmd == "version":
cmd_version()
elif cmd == "generate":
if len(args) < 3:
print("ERROR:CLI_GENERATE_MISSING_ARGS")
print("用法python main.py generate <平台名或account_id> \"<提示词>\"")
return 1
if not cmd_generate(args[1], args[2]):
return 1
elif cmd == "list":
platform_filter, limit, err = _parse_list_args(args[1:])
if err:
return err
cmd_key_list(platform_filter, limit=limit)
elif cmd == "add":
platform_arg, api_key_arg, model_arg, label_arg, err = _parse_add_args(args[1:])
if err:
return err
cmd_key_add(platform_arg, api_key_arg, model=model_arg, label=label_arg)
elif cmd == "del":
if len(args) < 2:
print("ERROR:CLI_DEL_MISSING_ARGS 用法python main.py del <key_id>")
return 1
cmd_key_del(args[1])
elif cmd == "key":
if len(args) < 2:
print("ERROR:CLI_KEY_MISSING_SUBCOMMAND 请指定子命令add / list / del")
return 1
sub = args[1]
if sub == "add":
platform_arg, api_key_arg, model_arg, label_arg, err = _parse_add_args(args[2:])
if err:
return err
cmd_key_add(platform_arg, api_key_arg, model=model_arg, label=label_arg)
elif sub == "list":
platform_filter, limit, err = _parse_list_args(args[2:])
if err:
return err
cmd_key_list(platform_filter, limit=limit)
elif sub == "del":
if len(args) < 3:
print("ERROR:CLI_KEY_DEL_MISSING_ARGS 用法python main.py key del <key_id>")
return 1
cmd_key_del(args[2])
else:
print(f"ERROR:CLI_UNKNOWN_KEY_SUB 未知 key 子命令「{sub}支持add / list / del")
return 1
else:
print(f"ERROR:CLI_UNKNOWN_COMMAND 未知命令「{cmd}」。")
_print_usage()
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())