mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
feat(channels): 逐厂商支持启动 —— TRAE 真实渠道接入 + checkin 表驱动
用户确认:仿照示例插件逐个支持厂商,自用、风险已知悉。
PLATFORMS 表升级为完整平台描述(对齐 dsh-our-free-model/channel-pack):
- adapter 字段:接入内核用哪个协议适配器
- checkin 字段:每日签到 {url, method, body, headers}(可缺省=不支持)
- hosts 字段:登录域与上游域(供 plugin.hosts 推导)
- claim_fn/claim_result_fn 钩子位:特殊签名/响应解析的厂商挂点
TRAE(字节)为第一个真实渠道,作为后续厂商的模板:
- 登录:callback 流(www.trae.cn/authorization,token 直传)
- 模型 fallback:DeepSeek-V4-Flash/Pro、Kimi K3、GLM-5 系、Doubao-Seed-2.1
- 签到:POST https://api.trae.cn/trae/api/v2/ug/checkin_credits/claim body {}
(端点逐字对齐参考仓库 trae-credits.ts)
plugin.hosts 不再手写:declaredHosts() 从平台表推导(hosts+base_url+checkin.url),
避免"两份名单漂移"(参考仓库同类真实缺陷的教训)。
内核零改动。UI 登录框补回 ch-vnc-wrap 内嵌容器(上次重构时遗漏,VNC 兜底
流程会引用不到)。
This commit is contained in:
@ -13,15 +13,15 @@
|
||||
--
|
||||
-- 设计边界(与内核的约定)
|
||||
-- ------------------------
|
||||
-- 内核提供三样基础设施,插件自己不实现这些:
|
||||
-- 内核提供基础设施,插件自己不实现这些:
|
||||
--
|
||||
-- (登录会话不再由内核提供 —— 见下)
|
||||
-- __llmsproxy_http 受出站白名单约束的 HTTP(登录/领额度都走它)
|
||||
-- __llmsproxy_secrets 凭据保险库(0600、独立文件、跨插件不可读)
|
||||
-- __llmsproxy_kernel 内核动作面(add_source / probe_all / reset_health…)
|
||||
--
|
||||
-- 本插件负责"平台知识":每个平台的登录地址、额度接口、认证头形状。
|
||||
-- 本插件负责"平台知识":每个平台的登录地址、额度接口、认证头形状、模型清单。
|
||||
-- 平台知识放插件而不是内核,是因为它变化快、且属于"某个厂商的具体行为"——
|
||||
-- 内核里出现厂商名就是分层被破坏了。
|
||||
-- 内核里出现厂商名就是分层被破坏了。登录会话、服务端浏览器栈也全归插件自持。
|
||||
--
|
||||
-- 权限(本插件需要,全部可在设置页看到"申请中")
|
||||
-- ------------------------------------------------
|
||||
@ -50,62 +50,121 @@ local plugin = {
|
||||
-- 申请的内核权限。设置页会把"申请中但未授权"标红。
|
||||
plugin.kernel = { "add_source", "list_sources", "probe_all", "reset_health", "status" }
|
||||
|
||||
-- 申请的登录主机(出站白名单)。内核里**不含**任何厂商名——这些只存在于插件。
|
||||
plugin.hosts = { "auth.example.com", "accounts.google.com" }
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- 平台表
|
||||
-- ============================================================================
|
||||
--
|
||||
-- 每个平台声明四件事:
|
||||
-- flow 登录方式:link-poll(给 URL 轮询)/ callback(OAuth 重定向)/
|
||||
-- paste(用户粘贴 cookie)
|
||||
-- login_url 用户要在**自己浏览器**打开的地址(link-poll/callback)
|
||||
-- claim_path 额度领取接口(相对 providers 的 base)
|
||||
-- source 接入网关时用的源定义模板({platform} 会被替换)
|
||||
-- 每个平台声明登录方式与各端点。字段:
|
||||
-- flow link-poll(给 URL 轮询)/ callback(OAuth 重定向回来带 code)/
|
||||
-- device-poll(设备码:先申请码、给用户 URL、再轮询换 token)/
|
||||
-- paste(用户粘贴 cookie/token)
|
||||
-- login_url 用户要在自己浏览器打开的地址
|
||||
-- base_url 接入网关后对话走的 OpenAI 兼容端点(adapter 决定协议形状)
|
||||
-- adapter 内核适配器名(openai / anthropic / …)
|
||||
-- models fallback 模型清单(登录后可用 refresh 动态拉取覆盖)
|
||||
-- checkin 每日额度领取:{ url, method, body, auth }(可缺省=不支持签到)
|
||||
--
|
||||
-- 新增平台 = 加一条。不需要动内核,也不需要动本文件的其它部分。
|
||||
-- 新增平台 = 加一条。不改内核,也不改本文件其它部分。
|
||||
--
|
||||
-- ⚠️ 真实厂商端点逆向自参考实现(dsh-our-free-model/vendor/channel-pack)。
|
||||
-- 仅在运维自己的网关、自己的账号范围内使用;法律风险运维自负(已确认)。
|
||||
-- WASM 签名(Qoder)/captcha(ZCode)/扫码(Loomy,Raccoon) 这类无法在当前 Lua
|
||||
-- 环境完成的流程标记 flow="manual",登录时给出明确说明而非假装成功。
|
||||
local PLATFORMS = {
|
||||
-- ===== 测试/示范用占位平台(example.com,不打真实厂商)=====
|
||||
{
|
||||
id = "example",
|
||||
name = "Example 平台",
|
||||
flow = "link-poll",
|
||||
login_url = "https://auth.example.com/device",
|
||||
claim_path = "/api/daily/claim",
|
||||
base_url = "https://api.example.com/v1",
|
||||
adapter = "openai",
|
||||
models = { "example-chat", "example-reason" },
|
||||
checkin = { url = "https://api.example.com/v1/api/daily/claim", method = "POST", body = "{}" },
|
||||
},
|
||||
{
|
||||
id = "googleish",
|
||||
name = "Google 系",
|
||||
name = "Google 系(示范 callback)",
|
||||
flow = "callback",
|
||||
login_url = "https://accounts.google.com/o/oauth2/auth",
|
||||
claim_path = "/api/quota/claim",
|
||||
base_url = "https://api.googleish.example/v1",
|
||||
adapter = "openai",
|
||||
models = { "g-model-1" },
|
||||
},
|
||||
|
||||
-- ===== 真实厂商渠道(逆向自参考仓库,端点逐字照抄)=====
|
||||
|
||||
-- TRAE(字节跳动)—— callback 登录 + ExchangeToken 续期 + 每日签到。
|
||||
-- 这是最标准的一条,作为"同类厂商"的实现模板。
|
||||
{
|
||||
id = "trae",
|
||||
name = "TRAE(字节)",
|
||||
vendor = "bytedance",
|
||||
flow = "callback",
|
||||
-- 用户在自己浏览器打开授权页;回调带 token 直传(auth_type=local)。
|
||||
login_url = "https://www.trae.cn/authorization",
|
||||
hosts = { "www.trae.cn", "api.trae.com.cn", "api.trae.cn", "trae-api-cn.mchost.guru" },
|
||||
base_url = "https://trae-api-cn.mchost.guru",
|
||||
adapter = "openai",
|
||||
models = {
|
||||
"DeepSeek-V4-Flash", "DeepSeek-V4-Pro", "kimi-k3", "kimi-k2.6",
|
||||
"glm-5", "glm-5.2", "Doubao-Seed-2.1-Pro",
|
||||
},
|
||||
-- 每日签到:POST /trae/api/v2/ug/checkin_credits/claim body {} 到 UG 主机。
|
||||
checkin = {
|
||||
url = "https://api.trae.cn/trae/api/v2/ug/checkin_credits/claim",
|
||||
method = "POST", body = "{}",
|
||||
},
|
||||
},
|
||||
|
||||
-- 服务端浏览器兜底示范(回环重定向类)。真实厂商若需要可改 flow="vnc"。
|
||||
{
|
||||
id = "loopbackish",
|
||||
name = "回环登录平台",
|
||||
-- 该类平台把授权重定向到 127.0.0.1。用户机器上的浏览器收不到(那是它自己的
|
||||
-- 回环),所以必须在服务端开浏览器 —— 本插件会把它内嵌进这个页签。
|
||||
name = "回环登录平台(示范 VNC 兜底)",
|
||||
flow = "vnc",
|
||||
needs_browser = true,
|
||||
login_url = "https://auth.loopbackish.example/authorize?redirect_uri=http://127.0.0.1:%d/callback",
|
||||
claim_path = "/api/quota/claim",
|
||||
base_url = "https://api.loopbackish.example/v1",
|
||||
adapter = "openai",
|
||||
models = { "lb-model-1" },
|
||||
},
|
||||
{
|
||||
id = "cookieish",
|
||||
name = "Cookie 平台",
|
||||
name = "Cookie 平台(示范 paste)",
|
||||
flow = "paste",
|
||||
claim_path = "/api/credit/claim",
|
||||
base_url = "https://api.cookieish.example/v1",
|
||||
adapter = "openai",
|
||||
models = { "c-model-1" },
|
||||
checkin = { url = "https://api.cookieish.example/v1/api/credit/claim", method = "POST", body = "{}" },
|
||||
},
|
||||
}
|
||||
|
||||
-- 申请的额外出站主机:从平台声明推导(登录域 + 上游域),避免两份清单漂移。
|
||||
-- 内核里**不含**任何厂商名 —— 这些只存在于本插件,且需运维在设置页逐个批准。
|
||||
local function declaredHosts()
|
||||
local seen, out = {}, {}
|
||||
for _, p in ipairs(PLATFORMS) do
|
||||
for _, h in ipairs(p.hosts or {}) do
|
||||
local hn = h:lower()
|
||||
if not seen[hn] then seen[hn] = true out[#out+1] = hn end
|
||||
end
|
||||
end
|
||||
-- 上游 base_url 的 host 也要(发对话/签到用)
|
||||
for _, p in ipairs(PLATFORMS) do
|
||||
local h = (p.base_url or ""):match("^https?://([^/:]+)")
|
||||
if h and not seen[h:lower()] then seen[h:lower()] = true out[#out+1] = h:lower() end
|
||||
end
|
||||
for _, p in ipairs(PLATFORMS) do
|
||||
local h = (p.checkin and p.checkin.url or ""):match("^https?://([^/:]+)")
|
||||
if h and not seen[h:lower()] then seen[h:lower()] = true out[#out+1] = h:lower() end
|
||||
end
|
||||
table.sort(out)
|
||||
return out
|
||||
end
|
||||
plugin.hosts = declaredHosts()
|
||||
|
||||
-- bodyOf 把 handler 的 req.body(**JSON 字符串**)解成 table。
|
||||
--
|
||||
-- 为什么需要它:handler 收到的是原始 body 字符串,不是解码后的表(内核不猜
|
||||
@ -384,15 +443,28 @@ function plugin.claim_daily(req)
|
||||
if args.platform == nil then args = req end
|
||||
local plat = platformById(args and args.platform)
|
||||
if not plat then return { status = 400, json = { error = "未知平台" } } end
|
||||
if not plat.checkin or not plat.checkin.url then
|
||||
return { status = 400, json = { error = plat.name .. " 不支持每日签到(平台未声明 checkin)" } }
|
||||
end
|
||||
local tok = __llmsproxy_secrets.get("channels:" .. plat.id)
|
||||
if not tok then return { status = 400, json = { error = "未登录" } } end
|
||||
|
||||
local resp, err = __llmsproxy_http({
|
||||
url = plat.base_url .. plat.claim_path,
|
||||
method = "POST",
|
||||
headers = { ["Authorization"] = "Bearer " .. tok, ["Content-Type"] = "application/json" },
|
||||
body = "{}",
|
||||
})
|
||||
-- 平台可自定义领取请求形状;默认 Bearer + POST {}。
|
||||
-- 参考实现的特殊形状(SDK-HMAC-SHA256 签名等)由该平台的 claim 钩子生成
|
||||
-- (见 PLATFORMS 的 claim_fn),这里是通用路径。
|
||||
local spec = {
|
||||
url = plat.checkin.url,
|
||||
method = plat.checkin.method or "POST",
|
||||
headers = plat.checkin.headers or { ["Authorization"] = "Bearer " .. tok, ["Content-Type"] = "application/json" },
|
||||
body = plat.checkin.body or "{}",
|
||||
}
|
||||
if plat.claim_fn then
|
||||
local custom, cerr = plat.claim_fn(plat, tok, args)
|
||||
if cerr then return { status = 400, json = { error = cerr } } end
|
||||
if custom then spec = custom end
|
||||
end
|
||||
|
||||
local resp, err = __llmsproxy_http(spec)
|
||||
if err then
|
||||
logEvent("领取失败 " .. plat.id .. ": " .. err)
|
||||
return { status = 400, json = { error = err } }
|
||||
@ -401,9 +473,14 @@ function plugin.claim_daily(req)
|
||||
local acct = plugin.state.accounts[plat.id] or {}
|
||||
acct.last_claim = os.time and os.time() or 0
|
||||
acct.last_claim_status = resp.status
|
||||
-- 平台可提供解析器把响应转成可读摘要(余额/获得额度)
|
||||
if plat.claim_result_fn and resp.status == 200 then
|
||||
local ok, summary = pcall(plat.claim_result_fn, resp.body)
|
||||
if ok and summary then acct.last_claim_summary = summary end
|
||||
end
|
||||
plugin.state.accounts[plat.id] = acct
|
||||
logEvent("领取返回 " .. tostring(resp.status))
|
||||
return { status = 200, json = { ok = true, upstream_status = resp.status, body = resp.body } }
|
||||
return { status = 200, json = { ok = true, upstream_status = resp.status, body = resp.body, summary = acct.last_claim_summary } }
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
@ -531,6 +608,12 @@ plugin.ui = {
|
||||
<div class="card" id="ch-login-box" style="display:none">
|
||||
<h2 id="ch-login-title">登录</h2>
|
||||
<div id="ch-login-body"></div>
|
||||
<!-- 服务端浏览器内嵌区(仅 VNC 兜底流程用;本机浏览器流程此区隐藏)。 -->
|
||||
<div id="ch-vnc-wrap" style="display:none;margin-top:10px">
|
||||
<p class="muted">请在下面的窗口里完成登录,完成后本页会自动继续。</p>
|
||||
<iframe id="ch-vnc" style="width:100%;height:640px;border:1px solid var(--line);border-radius:8px"
|
||||
allow="clipboard-read; clipboard-write"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>最近日志</h2>
|
||||
|
||||
Reference in New Issue
Block a user