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:
JianFeeeee
2026-10-11 19:26:21 +08:00
parent 50997a4e3a
commit 6de9f28269

View File

@ -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>