diff --git a/internal/lua/plugins/channels.lua b/internal/lua/plugins/channels.lua index 42e1a26..7e06b07 100644 --- a/internal/lua/plugins/channels.lua +++ b/internal/lua/plugins/channels.lua @@ -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 = {