@ -75,6 +75,30 @@ plugin.K = {
REMOTE_CATALOG_COOLDOWN = 30 , -- 秒,照参考 REMOTE_CATALOG_COOLDOWN_MS
INJECT_PLATFORM_LIMIT = 32 ,
ZCODE_ORIGIN = " https://zcode.z.ai " ,
-- ★ 双区端点表( 2026-10-19) : buddy/WorkBuddy 与 qoder 国际/国内两份 host。
-- 放 plugin.K 而非顶层 local —— 本文件主 chunk 的 local 数已顶到 Lua 上限
-- 200( TestChannelsLuaHasLocalHeadroom 钉着),新增常量必须走这里。
-- qoder: intl=openapi.qoder.sh, cn=openapi.qoder.com.cn( CN 无公开 chat
-- 端点,只做登录/领取) ; CN 的 client_id 与授权页都不同
-- (照参考 qoder-product.ts QODER_CN: qoder.cn / 732aef47-…)。
-- buddy: cn=copilot.tencent.com( platform=ide) , intl=www.workbuddy.ai
-- ( platform=workbuddy-ai, productCode=workbuddy, 照参考 product.ts WORKBUDDY) 。
QODER_REGION_BASE = {
intl = " https://openapi.qoder.sh " ,
cn = " https://openapi.qoder.com.cn " ,
} ,
QODER_REGION_AUTH = {
intl = { auth = " https://qoder.com " , client_id = " e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb " } ,
cn = { auth = " https://qoder.cn " , client_id = " 732aef47-9cf2-46a2-95fe-4cebb5d0d1fa " } ,
} ,
BUDDY_REGION_HOST = {
intl = " www.workbuddy.ai " ,
cn = " copilot.tencent.com " ,
} ,
GEMINI_DEFAULT_PROJECT = " aicode-consumers " ,
GEMINI_UPSTREAM_USER_AGENT = " antigravity " ,
CLI_PREFIX = " You are ZCode, an interactive coding agent " ,
QODER_QUEUE_CODE = " 10605 " , -- 10605=排队(重试),与 110( 额度耗尽) 语义相反
}
-- ★★ plugin.adaptersDir():适配器目录 = 插件目录的**兄弟**目录
@ -116,7 +140,7 @@ plugin.kernel = {
-- WebUI 可调用的导出函数( POST /api/plugins/channels/call/{fn})。
-- set_endpoints 让运维把渠道端点指向自建代理/镜像;测试用它打假上游。
plugin.exports = { " set_endpoints " , " account_add " , " account_list " , " account_update " , " account_clear_credentialless " , " account_delete " , " account_test " , " account_refresh " , " account_reorder " , " account_reset " , " account_retest " , " account_adopt " , " model_list " , " model_refresh " , " model_set_disabled " , " model_set_disabled_many " , " model_set_all_disabled " , " model_clear_dead " , " provider_status " , " provider_set_enabled " , " provider_get_order " , " provider_set_order " , " credits_claim_all " , " credits_status " , " credits_balances " , " credits_pool_reason " , " credits_permanent_lock " , " backup_export " , " backup_import " , " backup_status " , " auto_checkin " , " gateway_get_enabled " , " gateway_set_enabled " , " usage_token_ledger " , " usage_badge " , " usage_badge_preference " , " onboarding_status " , " onboarding_claim " , " __trae_derive_probe " , " __codearts_sign_probe " , " __qoder_machine_probe " , " __qoder_refresh_probe " , " __codearts_dpop_probe " , " __codearts_cred_probe " , " __codearts_adapter_sign_probe " , " __sync_source_probe " , " __trae_solo_probe " , " __adapter_install_probe " , " __store_dir_probe " , " __set_loopback_probe " , " reap_loopbacks " , " reap_orphan_sources " , " __add_source_for_test " , " __forget_account_probe " , " __set_pool_cred_probe " , " __cred_headers_probe " , " __b64_probe " , " __codearts_jwk_pem_probe " , " __platform_def_probe " , " __zcode_diag_probe " , " __trae_entry_probe " , " __list_sources_probe " , " __clear_priorities_probe " , " __resync_sources_probe " , " __utf8_clip_probe " , " __event_log_probe " , " __is_model_gone_probe " , " __rank_accounts_probe " , " __inject_refresh_platform_probe " , " __clear_capabilities_probe " , " __qoder_claim_outcome_probe " , " __is_free_probe " , " __claim_covers_probe " , " __schedule_probe " , " __parse_capability_probe " , " __inject_capability_probe " , " __ttl_reset_probe " , " __ttl_probe " , " __ttl_invalidate_probe " , " __serial_probe " , " __serial_release_probe " , " __set_legacy_cred_probe " , " __read_cred_probe " , " __clear_all_creds_probe " , " credits_tier_probe " , " __set_lock_permanent_probe " , " __set_expiring_window_probe " , " __expiring_window_probe " , " __record_dead_probe " , " __catalog_gate_probe " , " __catalog_gate_fail_probe " , " credits_capability " , " claims_capable " }
plugin.exports = { " set_endpoints " , " account_add " , " account_list " , " account_update " , " account_clear_credentialless " , " account_delete " , " account_test " , " account_refresh " , " account_reorder " , " account_reset " , " account_retest " , " account_adopt " , " model_list " , " model_refresh " , " model_set_disabled " , " model_set_disabled_many " , " model_set_all_disabled " , " model_clear_dead " , " provider_status " , " provider_set_enabled " , " provider_get_order " , " provider_set_order " , " credits_claim_all " , " credits_status " , " credits_balances " , " credits_pool_reason " , " credits_permanent_lock " , " backup_export " , " backup_import " , " backup_status " , " auto_checkin " , " gateway_get_enabled " , " gateway_set_enabled " , " usage_token_ledger " , " usage_badge " , " usage_badge_preference " , " onboarding_status " , " onboarding_claim " , " __trae_derive_probe " , " __codearts_sign_probe " , " __qoder_machine_probe " , " __qoder_refresh_probe " , " __codearts_dpop_probe " , " __codearts_cred_probe " , " __codearts_adapter_sign_probe " , " __sync_source_probe " , " __trae_solo_probe " , " __adapter_install_probe " , " __store_dir_probe " , " __set_loopback_probe " , " reap_loopbacks " , " reap_orphan_sources " , " __add_source_for_test " , " __forget_account_probe " , " __set_pool_cred_probe " , " __cred_headers_probe " , " __b64_probe " , " __codearts_jwk_pem_probe " , " __platform_def_probe " , " __zcode_diag_probe " , " __trae_entry_probe " , " __list_sources_probe " , " __clear_priorities_probe " , " __resync_sources_probe " , " __utf8_clip_probe " , " __event_log_probe " , " __is_model_gone_probe " , " __rank_accounts_probe " , " __chosen_account_probe " , " __inject_refresh_platform_probe " , " __clear_capabilities_probe " , " __qoder_claim_outcome_probe " , " __is_free_probe " , " __claim_covers_probe " , " __schedule_probe " , " __parse_capability_probe " , " __inject_capability_probe " , " __ttl_reset_probe " , " __ttl_probe " , " __ttl_invalidate_probe " , " __serial_probe " , " __serial_release_probe " , " __set_legacy_cred_probe " , " __read_cred_probe " , " __clear_all_creds_probe " , " credits_tier_probe " , " __set_lock_permanent_probe " , " __set_expiring_window_probe " , " __expiring_window_probe " , " __record_dead_probe " , " __catalog_gate_probe " , " __catalog_gate_fail_probe " , " credits_capability " , " claims_capable " }
@ -254,7 +278,7 @@ local PLATFORMS = {
-- 载荷须转 config_name/function 格式(见 trae-adapter.ts:374) 。
base_url = " https://trae-api-cn.mchost.guru " ,
-- TRAE 非 OpenAI 兼容,用**本插件自装**的 SOLO 适配器
-- (见 TRAE_SOLO_ADAPTER_SRC) 。端点由适配器声明 /api/agent/v3/llm_utils_chat。
-- (见 plugin.K. TRAE_SOLO_ADAPTER_SRC) 。端点由适配器声明 /api/agent/v3/llm_utils_chat。
adapter = " channels-trae " ,
-- ★ 让 syncAccountSource 把凭据里的设备身份放进源的 meta ——
-- trae 的推理头需要 X-Uid / X-Device-Id / X-Machine-Id 派生,
@ -550,10 +574,16 @@ local PLATFORMS = {
name = " CodeBuddy( 腾讯) " ,
vendor = " tencent " ,
flow = " state-poll " ,
-- ★ 双区( 2026-10-19, 照参考 product.ts 的 CODEBUDDY / WORKBUDDY 两份配置):
-- * cn( 国内, 默认) → copilot.tencent.com, platform=ide, X-Domain/UA 用国内值
-- * intl( 国际, WorkBuddy) → www.workbuddy.ai, platform=workbuddy-ai,
-- productCode=workbuddy, UA=WorkBuddyIDE/5.5.2( 2026-10-19 实测
-- POST {intl}/v2/plugin/auth/state?platform=workbuddy-ai 回 200 信封)
-- 区域按**账号**记录(凭据 JSON 的 region 字段);登录请求可带 region。
state = {
state_url = " https://copilot.tencent.com /v2/plugin/auth/state?platform=ide " ,
token_url = " https://copilot.tencent.com /v2/plugin/auth/token " ,
account_url = " https://copilot.tencent.com /v2/plugin/login/account " ,
state_url = " https://{buddyhost} /v2/plugin/auth/state?platform={platform} " ,
token_url = " https://{buddyhost} /v2/plugin/auth/token " ,
account_url = " https://{buddyhost} /v2/plugin/login/account " ,
code_not_ready = 11217 ,
interval = 1 ,
} ,
@ -585,14 +615,22 @@ local PLATFORMS = {
-- 跨产品迁移后会指错区) ; X-Product 是部署类型( SaaS) ;
-- X-Product-Code 是产品码( codebuddy) ; X-User-Id 取凭据的 user_id。
headers_fn = function ( accessToken , cred )
-- ★ 双区:凭据 region 决定身份头(照参考 product.ts 两份产品配置)。
local region = cred and tostring ( cred.region or " " ) or " "
local isIntl = region == " intl " or ( region == " " and false )
-- 未声明 region 的存量凭据: X-Domain 快照若是 workbuddy 则按国际区,
-- 否则按国内(升级前全部是国内账号,默认值不变)。
if region == " " then
isIntl = cred and cred.domain == " www.workbuddy.ai " or false
end
local h = {
[ " Authorization " ] = " Bearer " .. accessToken ,
[ " Accept " ] = " application/json " ,
[ " Content-Type " ] = " application/json " ,
[ " X-Domain " ] = " copilot.tencent.com " ,
[ " X-Domain " ] = isIntl and " www.workbuddy.ai " or " copilot.tencent.com " ,
[ " X-Product " ] = " SaaS " ,
[ " X-Product-Code " ] = " codebuddy " ,
[ " User-Agent " ] = " CodeBuddyIDE/1.106.1 " ,
[ " X-Product-Code " ] = isIntl and " workbuddy " or " codebuddy " ,
[ " User-Agent " ] = isIntl and " WorkBuddyIDE/5.5.2 " or " CodeBuddyIDE/1.106.1 " ,
}
local uid = cred and ( cred.user_id or cred.uid )
if type ( uid ) == " string " and uid ~= " " then h [ " X-User-Id " ] = uid end
@ -1045,13 +1083,16 @@ local PLATFORMS = {
flow = " device-poll " ,
device = {
-- 授权页在 {authBase}/device/selectAccounts, query 由 pkce 表单拼装
code_url = " https://qoder.com/device/selectAccounts " ,
-- ★ 双区: host 与 client_id 都用占位符({auth} / {client_id}) ,
-- start_login 按会话区域从 QODER_REGION_AUTH 替换。
code_url = " {auth}/device/selectAccounts " ,
code_method = " GET_QUERY " ,
-- code_url 的 query 模板:{challenge}/{nonce}/{machine_id}/{client_id}
auth_query = " challenge={challenge}&challenge_method=S256&nonce={nonce} "
.. " &machine_id={machine_id}&client_id=" .. " e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb " ,
-- 轮询在 openApiBase
token_url = " https://openapi.qoder.sh/api/v1/deviceToken/poll " ,
.. " &machine_id={machine_id}&client_id={client_id} " ,
-- 轮询在 openApiBase。★ host 用 {openapi} 占位符( 双区: intl=openapi.qoder.sh /
-- cn=openapi.qoder.com.cn) , deviceOneStep 按会话区域替换。
token_url = " https://{openapi}/api/v1/deviceToken/poll " ,
token_method = " GET_QUERY " ,
-- poll 的 query 模板: PKCE 用 verifier( 不是 challenge)
poll_query = " nonce={nonce}&verifier={verifier}&challenge_method=S256 " ,
@ -1270,12 +1311,32 @@ local PLATFORMS = {
-- 短信路径(/sms/send + /sms/verify) 需要阿里云滑块 captcha_param 与手机号
-- AES 加密 —— 那一条才真需要人工,故不走。
flow = " qr-poll " ,
-- ★★ 2026-10-20 实测重写(用户报障「商汤那个链接点进去是无效链接」)。
-- 旧实现两个致命错:
-- ① code **本地随机造**,但上游早已不认自造码:旧端点
-- /api/web/auth/v1/login_with_qrcode_code 对任意 code 恒回 pending
-- (服务端根本没这个会话)⇒ 扫码永远不会完成,账号一直是空凭据。
-- ② 登录页 /login/mp **已 404**(站点 Next 路由已删)⇒ 点开是无效链接。
-- 现行流程(逐行对齐上游网页客户端 2026-10-20 的 auth chunk) :
-- ① POST {base}/api/iam/v1/auth/mp/qrcode → {qrcode_code, scan_path,
-- expires_in, poll_interval}( **服务端建会话**)
-- ② 二维码/链接 = {base}{scan_page}?code=…&appname=…&source=platform
-- 页面用 /login( 实测 200, 且渲染「微信扫码」面板并接受 code 预置);
-- 服务端回的 scan_path 是 ""/login/mp"",该路由已 404, 故以 scan_page 为准,
-- scan_path 仅在它非 "/login/mp" 时采用(上游换路由时自动跟随)。
-- ③ 轮询 POST {base}/api/iam/v1/auth/mp/qrcode/login {qrcode_code}
-- → {code:"OK", data:{status: pending|logging|canceled|success, access_token?}}
-- 旧端点保留为 legacy_poll_path 兜底( IAM 不可用时退回旧行为)。
qr = {
api_base = " https://xiaohuanxiong.com " ,
auth_prefix = " /api/web/auth/v1 " ,
poll_path = " /login_with_qrcode_code " ,
login _page = " /login/mp " ,
auth_prefix = " /api/web/auth/v1 " , -- 旧端点前缀(兜底)
poll_path = " /login_with_qrcode_code " , -- 旧轮询(兜底)
create _path = " /api/iam/v1/auth/mp/qrcode " ,
iam_poll_path = " /api/iam/v1/auth/mp/qrcode/login " ,
scan_page = " /login " ,
login_page = " /login/mp " , -- 上游已废弃,仅作兼容判断
app_name = " 商汤小浣熊官网 " ,
source = " platform " ,
interval = 2 ,
} ,
-- 推理是标准 OpenAI 兼容(实测 POST /api/web/llm/v2/chat/completions)
@ -1994,6 +2055,25 @@ end
-- 可被 set_endpoints 覆盖(测试指到假上游用)。
QODER_SASH_BASE = " https://openapi.qoder.sh "
-- qoderRegionBase(cred):该账号区域的 SASH/OpenAPI 基址。
-- ★ 双区( 2026-10-19, 照参考 qoder-product.ts 的 QODER / QODER_CN 两份配置)。
-- 端点表在 plugin.K.QODER_REGION_BASE / QODER_REGION_AUTH( 主 chunk local
-- 数已顶到 Lua 上限 200, 新增常量必须进 plugin.K —— 见 TestChannelsLuaHasLocalHeadroom) 。
-- ⚠️ CN 没有可用的公开 chat 端点(参考注释明确)→ CN 账号**只做登录/领取**,不产源。
-- 区域按**账号**记录(凭据 JSON 的 region 字段);未声明 region 的存量凭据
-- 一律视为 intl( 与升级前行为一致) 。
local function qoderRegionBase ( cred )
local region = type ( cred ) == " table " and tostring ( cred.region or " " ) or " "
if region == " " then region = " intl " end
-- set_endpoints 的 qoder_sash_base 覆盖**优先**于区域表:测试把上游指到
-- httptest 假服务器靠它;此时区域表必须让路,否则请求仍打真上游被出站
-- 白名单拒绝(实测 TestQoderClaim* 全挂在这个上)。
if QODER_SASH_BASE ~= " https://openapi.qoder.sh " then
return QODER_SASH_BASE
end
return plugin.K . QODER_REGION_BASE [ region ] or QODER_SASH_BASE
end
-- UTC+8 偏移(毫秒)。活动按 UTC+8 结算,口径与参考一致。
-- hasQoderCampaignRefreshedToday():今天的活动列表是否已刷新。
@ -2056,7 +2136,7 @@ local function qoderLoadCampaigns(plat, cred)
[ " Accept " ] = " application/json " , [ " Cosy-ClientType " ] = " 10 " , [ " User-Agent " ] = " Qoder " }
end
local resp , err = __llmsproxy_http ( {
url = QODER_SASH_BASE .. " /sash/api/v1/me/campaigns " , method = " GET " , headers = hdrs ,
url = qoderRegionBase ( cred ) .. " /sash/api/v1/me/campaigns " , method = " GET " , headers = hdrs ,
} )
if err then return nil , " 活动列表查询失败: " .. tostring ( err ) end
if not resp or resp.status ~= 200 then
@ -2076,7 +2156,7 @@ local function qoderUsageRaw(plat, cred)
end
local resp = nil
if hdrs then
resp , _ = __llmsproxy_http ( { url = QODER_SASH_BASE .. " /sash/api/v2/me/usage " , method = " GET " , headers = hdrs } )
resp , _ = __llmsproxy_http ( { url = qoderRegionBase ( cred ) .. " /sash/api/v2/me/usage " , method = " GET " , headers = hdrs } )
end
if not resp or resp.status ~= 200 then return nil end
local ok , rec = pcall ( json.decode , resp.body or " " )
@ -2097,7 +2177,7 @@ local function qoderClaimCampaign(plat, cred, campaignId)
if not hdrs then hdrs = { [ " Cosy-ClientType " ] = " 10 " } end
hdrs [ " Content-Type " ] = " application/json "
local resp , err = __llmsproxy_http ( {
url = QODER_SASH_BASE .. " /sash/api/v1/me/campaigns/ "
url = qoderRegionBase ( cred ) .. " /sash/api/v1/me/campaigns/ "
.. rfc3986Escape ( campaignId ) .. " /claim " ,
method = " POST " , headers = hdrs , body = " " ,
} )
@ -2509,6 +2589,13 @@ local function declaredHosts()
if p.qr then addHost ( p.qr . api_base ) end
if p.device then addHost ( p.device . code_url ) ; addHost ( p.device . token_url ) end
if p.state then addHost ( p.state . state_url ) ; addHost ( p.state . token_url ) ; addHost ( p.state . account_url ) end
-- ★ 双区占位符 URL( buddy {buddyhost} / qoder {auth}、{openapi})在 addHost
-- 里提取不到真实 host, 这里把两个区域的 host 直接补进声明,否则出站被拒
-- (参考 WorkBuddy www.workbuddy.ai、Qoder CN openapi.qoder.com.cn) 。
if p.id == " buddy " then addHost ( " https://www.workbuddy.ai " ) end
if p.id == " qoder " then
addHost ( " https://qoder.cn " ) ; addHost ( " https://openapi.qoder.com.cn " )
end
if p.models_remote then
for _ , m in ipairs ( p.models_remote . endpoints or { } ) do addHost ( m.url ) end
end
@ -2674,6 +2761,15 @@ end
local function deviceOneStep ( plat , sess )
local d = plat.device
local url , method , body = d.token_url , d.token_method or " POST " , nil
-- ★ 双区( 2026-10-19) : 会话创建时记录的 region 决定 poll 端点。
-- 平台声明里 token_url 的 host 部分用 {openapi} 占位符表达;按会话区域
-- 换成对应基址。未用占位符的平台不受影响( gsub 找不到就不动)。
-- setmetatable+__index 只换 token_url 一个键,其余仍读原表。
if type ( d.token_url ) == " string " and d.token_url : find ( " {openapi} " , 1 , true ) then
local region = tostring ( sess.region or " intl " )
d = setmetatable ( { token_url = ( d.token_url : gsub ( " {openapi} " ,
plugin.K . QODER_REGION_BASE [ region ] or QODER_SASH_BASE ) ) } , { __index = d } )
end
if d.poll_query or ( d.token_method == " GET_QUERY " ) then
-- 形态②( Qoder) : GET + query 模板。PKCE 用 verifier( 不是 challenge) ,
-- 且 nonce 必须是**会话创建时那一个**(服务端按它找回待授权会话)。
@ -3431,10 +3527,44 @@ local function credentialExpiresAtMs(cred)
local n = tonumber ( raw )
if n then return n > 1e12 and n or n * 1000 end
end
-- ISO 8601: 解析 "YYYY-MM-DDThh:mm:ss" 形态(无时区偏移时按 UTC)
local Y , Mo , D , H , Mi , S = raw : match ( " ^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d) " )
if Y then return os.time ( { year = tonumber ( Y ) , month = tonumber ( Mo ) , day = tonumber ( D ) ,
hour = tonumber ( H ) , min = tonumber ( Mi ) , sec = tonumber ( S ) , isdst = false } ) * 1000 end
-- ISO 8601: 解析 "YYYY-MM-DDThh:mm:ss[.fff][Z|±hh:mm]"。
-- ★ 无偏移后缀时必须按 **UTC** 算,不能直接喂 os.time( 它按本地时区解释) 。
-- 真实缺陷( 2026-10-19) : codearts 的 expires_at 是 UTC ISO
-- ( "2026-10-19T16:42:43.06Z"),本机 +08:00 直接 os.time 解析 ⇒ 过期
-- 时刻虚早 8 小时 ⇒ refresh_creds 每一轮都判"已过期"⇒ 每半小时真打一次
-- 华为云 STS( token 本来还有 23 小时寿命)。
-- 修法:先用 os.date("!...", epoch_of_naive_interpretation) 技巧不可靠,
-- 直接用「本地 epoch − 本地偏移」还原 UTC: os.time 表按本地解释得 t,
-- t − (os.time() − os.time(os.date("!*t"))) 即同一钟面时刻的 UTC epoch。
-- 显式带 Z 或 ±hh:mm 后缀的按其偏移换算。
local Y , Mo , D , H , Mi , S , rest =
raw : match ( " ^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)(.*)$ " )
if Y then
-- os.time 把字段按**本地时区**解释。对"本地钟面 = 字段值"的时刻,
-- os.time 给出的就是正确的 epoch。要做的是把"声明时区的钟面"换算成
-- "本地钟面"再喂 os.time:
-- 字段带 Z / 无后缀 = UTC 钟面 ⇒ 本地钟面 = 字段 + localOff ⇒
-- epoch = os.time(字段 + localOff) = naive + localOff。
-- 字段带 ±hh[:mm] = 声明时区钟面 ⇒ 本地钟面 = 字段 + localOff − off。
local naive = os.time ( { year = tonumber ( Y ) , month = tonumber ( Mo ) , day = tonumber ( D ) ,
hour = tonumber ( H ) , min = tonumber ( Mi ) , sec = tonumber ( S ) , isdst = false } )
local localOff = os.time ( ) - os.time ( os.date ( " !*t " ) )
if rest : sub ( 1 , 1 ) == " + " or rest : sub ( 1 , 1 ) == " - " then
-- 偏移形如 "+08:00" / "-05:00" / "+0800"。字符类 [%+%-] 在部分 Lua
-- 构建下不可靠(实测 luajit 匹配 nil) , 用 sub 拆更稳。
local sign = rest : sub ( 1 , 1 )
local oh , om = rest : sub ( 2 ) : match ( " ^(%d%d):?(%d*) " )
local off = ( tonumber ( oh ) or 0 ) * 3600
+ ( tonumber ( om ~= " " and om or " 0 " ) or 0 ) * 60
if sign == " - " then off = - off end
-- 字段是"声明时区 off 的钟面"。本地钟面 = 字段 + (localOff − off):
-- 例(本机 +08) : 字段 "11:42:43-05:00"( =16:42:43Z) 的本地钟面是
-- 00:42:43(+1d); localOff− off = 28800− (− 18000) = 46800 = 13h,
-- 11:42:43 + 13h = 00:42:43(+1d)。✓
return ( naive + ( localOff - off ) ) * 1000
end
return ( naive + localOff ) * 1000
end
end
local exp = jwtClaim ( cred.access_token , " exp " )
if type ( exp ) == " number " then return exp * 1000 end
@ -4336,9 +4466,19 @@ local function syncProviderSource(provider)
.. tostring ( rerr or " (无错误信息) " ) , " warn " , provider )
end
end
-- ★ 双区: buddy 的 chat host 按所选账号的区域取( WorkBuddy=www.workbuddy.ai) 。
-- 选号在上方完成( plugin.state.chosen[provider] 已记录),从其凭据读 region。
local chatBase = plat.base_url
if provider == " buddy " and plugin.state . chosen and plugin.state . chosen [ provider ] then
local chosenCred = credentialFull (
( poolGet ( provider , plugin.state . chosen [ provider ] ) or { } ) . credentialRef )
if type ( chosenCred ) == " table " and chosenCred.region == " intl " then
chatBase = " https:// " .. plugin.K . BUDDY_REGION_HOST.intl
end
end
local src = {
name = name ,
base_url = plat.base_url ,
base_url = chatBase ,
adapter = plat.adapter or " openai " ,
api_key = tok ,
-- ⚠️ max_concurrent=1 + rpm 节流,而不是 2。
@ -5519,6 +5659,11 @@ local function initLoginSession(plat, args, now)
end
sessions ( ) [ sess.id ] = sess
sess.account_id = args and args.account_id
-- ★ 双区:登录请求可带 region( "intl"|"cn")。会话带着它选 poll/授权页端点;
-- 完成登录后 complete_login 把 region 写进凭据 JSON, 推理/领取按账号取区。
local region = tostring ( ( args and args.region ) or " " )
if region ~= " cn " then region = " intl " end
sess.region = region
return sess
end
@ -5593,11 +5738,17 @@ function plugin.start_login(req)
return { status = 200 , json = out }
elseif plat.flow == " state-poll " then
-- Buddy: 先拿 state + authUrl。页面给 authUrl, 轮询 token 每推进一步。
-- ★ 双区: state_url 的 {buddyhost}/{platform} 占位符按会话区域替换。
local region = tostring ( sess.region or " intl " )
local bhost = plugin.K . BUDDY_REGION_HOST [ region ] or plugin.K . BUDDY_REGION_HOST.intl
local bplat = ( region == " cn " ) and " ide " or " workbuddy-ai "
local stateUrl = tostring ( plat.state . state_url )
: gsub ( " {buddyhost} " , bhost ) : gsub ( " {platform} " , bplat )
local resp , err = __llmsproxy_http ( {
url = plat.state . state_u rl,
url = stateU rl,
method = " POST " ,
headers = { [ " Accept " ] = " application/json " , [ " Content-Type " ] = " application/json " } ,
body = " { \" platform \" : \" ide \" } " ,
body = " { \" platform \" : \" " .. bplat .. " \" } " ,
} )
if err or resp.status ~= 200 then
sess.state = " failed "
@ -5630,15 +5781,50 @@ function plugin.start_login(req)
logEvent ( " 服务端浏览器已就绪 " .. plat.id .. " (CDP : " .. tostring ( bs.cdp_port ) .. " ) " )
end
elseif plat.flow == " qr-poll " then
-- 自带二维码的 扫码登录( Raccoon) : code 由**本地随机生成**,服务端接受
-- 任意 自造 code 并进入 pending( 参考注释实测) 。二维码图交给前端渲染 。
-- 扫码登录( Raccoon) : **必须由服务端建会话**( 2026-10-20 实测重写,见平台定义)。
-- 本地 自造 code 会被旧端点一律回 pending ⇒ 永远扫不上(此前账号一直空凭据) 。
local q = plat.qr
local code = tostring ( sha256_hex ( tostring ( os.time and os.time ( ) or 0 ) .. tostring ( math.random ( 1e12 ) ) ) ) : sub ( 1 , 32 )
local code , scanPath , interval
local usedIAM = false
-- ① 服务端建码
if type ( q.create_path ) == " string " and q.create_path ~= " " then
local cresp , cerr = __llmsproxy_http ( {
url = q.api_base .. q.create_path ,
method = " POST " ,
headers = { [ " Accept " ] = " application/json " , [ " Content-Type " ] = " application/json " } ,
body = " {} " ,
} )
if not cerr and cresp and cresp.status == 200 then
local okc , crec = pcall ( json.decode , cresp.body or " " )
local d = okc and type ( crec ) == " table " and ( crec.data or crec ) or nil
if type ( d ) == " table " and type ( d.qrcode_code ) == " string " and d.qrcode_code ~= " " then
code = d.qrcode_code
usedIAM = true
-- scan_path 只在**不是已废弃的 /login/mp** 时采用(上游换路由时自动跟随)
if type ( d.scan_path ) == " string " and d.scan_path ~= " "
and d.scan_path ~= q.login_page then
scanPath = d.scan_path
end
if type ( d.poll_interval ) == " number " and d.poll_interval > 0 then
interval = math.max ( 1 , math.min ( 60 , d.poll_interval ) )
end
end
end
end
-- ② 服务端不可用 → 退回旧的本地造码(不拦截登录入口,但日志要说清)
if code == nil then
code = tostring ( sha256_hex ( tostring ( os.time and os.time ( ) or 0 ) .. tostring ( math.random ( 1e12 ) ) ) ) : sub ( 1 , 32 )
logEvent ( " 二维码走了本地兜底(服务端建码失败,扫码可能无法完成) " .. plat.id , " warn " , plat.id )
end
sess.qr_code = code
out .qr_url = q.api_base .. q.login_page .. " ?code= " .. cod e
.. " &appname= " .. rfc3986Escape ( q.app_name )
sess .qr_iam = usedIAM and true or fals e
-- ③ 链接/二维码内容:优先服务端 scan_path, 否则 scan_page( 实测可用)
out.qr_url = q.api_base .. ( scanPath or q.scan_page or q.login_page )
.. " ?code= " .. code
.. " &appname= " .. rfc3986Escape ( q.app_name or " " )
.. " &source= " .. rfc3986Escape ( q.source or " platform " )
out.url = out.qr_url
out.session . interval = q.interval or 2
out.session . interval = interval or q.interval or 2
logEvent ( " 二维码已生成 " .. plat.id )
elseif plat.flow == " wechat-poll " then
-- 微信扫码:取 uuid → 把二维码给前端渲染 → 长轮询等扫码确认。
@ -5727,10 +5913,19 @@ function plugin.start_login(req)
local rec = nil
if dm == " GET_QUERY " or plat.device . auth_query then
-- 形态②:授权页 URL 由模板拼装,无网络申请
-- ★ 双区: code_url 的 {auth} / {openapi} 占位符与 query 模板里的
-- {client_id} 按会话区域从 QODER_REGION_AUTH 替换( CN 的 client_id
-- 与国际版不同,用错授权页报「参数无效」—— 参考 qoder-product.ts 点名)。
local ra = plugin.K . QODER_REGION_AUTH [ tostring ( sess.region or " intl " ) ]
or plugin.K . QODER_REGION_AUTH.intl
local codeUrl = tostring ( plat.device . code_url or " " )
codeUrl = codeUrl : gsub ( " {auth} " , ra.auth ) : gsub ( " {openapi} " ,
plugin.K . QODER_REGION_BASE [ sess.region or " intl " ] or QODER_SASH_BASE )
local q = ( plat.device . auth_query or " " )
: gsub ( " {challenge} " , challenge ) : gsub ( " {nonce} " , sess.nonce )
: gsub ( " {machine_id} " , machineId ) : gsub ( " {verifier} " , verifier )
out.url = plat.device . code_url .. " ? " .. q
: gsub ( " {client_id} " , ra.client_id )
out.url = codeUrl .. " ? " .. q
sess.device_verifier = verifier
sess.device_challenge = challenge
sess.device_nonce = sess.nonce
@ -6084,6 +6279,8 @@ function plugin.poll_login(req)
else
-- 必须带 account_id: 页面流程先 account/add 建了占位账号,
-- 不带就会落到与占位账号无关的 `channels:<plat>` 键(见 poll_login 顶部注释)。
-- ★ 双区:把会话区域写进凭据 JSON( 推理/领取按账号取区)。
if type ( cred ) == " table " and s.region then cred.region = s.region end
local res = plugin.complete_login ( { platform = s.platform , credential = cred ,
account = acct or payload.account , account_id = s.account_id } )
if res.json and res.json . ok then
@ -6106,8 +6303,16 @@ function plugin.poll_login(req)
local plat = platformById ( s.platform )
local q = plat and plat.qr
if q then
-- ★ 2026-10-20: 服务端建的会话必须打 **IAM** 轮询端点;旧端点只认它自己的
-- 会话,对 IAM 码恒回 pending。反之, 本地兜底码只能走旧端点。
local pollUrl
if s.qr_iam and type ( q.iam_poll_path ) == " string " and q.iam_poll_path ~= " " then
pollUrl = q.api_base .. q.iam_poll_path
else
pollUrl = q.api_base .. q.auth_prefix .. q.poll_path
end
local resp , err = __llmsproxy_http ( {
url = q.api_base .. q.auth_prefix .. q.poll_path ,
url = pollUrl ,
method = " POST " ,
headers = { [ " Accept " ] = " application/json " , [ " Content-Type " ] = " application/json " } ,
body = json.encode ( { qrcode_code = s.qr_code } ) ,
@ -6115,7 +6320,9 @@ function plugin.poll_login(req)
if not err and resp then
local ok , rec = pcall ( json.decode , resp.body or " " )
local env = ok and type ( rec ) == " table " and rec or nil
if env and env.code == 0 and type ( env.data ) == " table " then
-- IAM 信封:{code:"OK", data:{...}};旧信封:{code:0, data:{...}}。两种都认。
local codeOk = env and ( env.code == 0 or env.code == " OK " )
if codeOk and type ( env.data ) == " table " then
local st = env.data . status
if st == " canceled " then
s.state = " failed "
@ -6283,8 +6490,11 @@ function plugin.poll_login(req)
-- Buddy( CodeBuddy) : state 轮询。状态里的 state_token 是 auth/state 下发的。
local plat = platformById ( s.platform )
local sp = plat and plat.state
-- ★ 双区:轮询 host 按会话区域替换(与 start_login 的 state_url 同一映射)。
local bhost = plugin.K . BUDDY_REGION_HOST [ tostring ( s.region or " intl " ) ]
or plugin.K . BUDDY_REGION_HOST.intl
local resp , err = __llmsproxy_http ( {
url = ( sp.token_url or " " ) .. " ?state= " .. tostring ( s.state_token ) ,
url = ( sp.token_url or " " ) : gsub ( " {buddyhost} " , bhost ) .. " ?state= " .. tostring ( s.state_token ) ,
method = " GET " ,
headers = { [ " Accept " ] = " application/json " , [ " X-No-Authorization " ] = " true " } ,
} )
@ -6299,7 +6509,7 @@ function plugin.poll_login(req)
local access = data.accessToken or data.access_token
local accountData = nil
local aresp , aerr = __llmsproxy_http ( {
url = ( sp.account_url or " " ) .. " ?state= " .. tostring ( s.state_token ) ,
url = ( sp.account_url or " " ) : gsub ( " {buddyhost} " , bhost ) .. " ?state= " .. tostring ( s.state_token ) ,
method = " GET " ,
headers = { [ " Accept " ] = " application/json " , [ " Authorization " ] = " Bearer " .. access } ,
} )
@ -6320,6 +6530,9 @@ function plugin.poll_login(req)
acct = credTable.user_id
end
if acct == nil or acct == " " then acct = " codebuddy " end
-- ★ 双区:凭据记录区域(推理/余额头按账号取区)
credTable.region = tostring ( s.region or " intl " )
credJson = json.encode ( credTable )
-- ② 必须带 account_id( 页面流程的占位账号) ; 否则凭据落到无关的
-- `channels:<plat>` 键,账号池查不到。
local res = plugin.complete_login ( { platform = s.platform , credential = credJson ,
@ -9601,7 +9814,7 @@ end
-- * 端点是 /api/v2/chat/completions( 含 /api/v2 前缀)
-- * Agent-Type: PromptCenter / X-Language: zh-cn( **签名后**追加,
-- 不参与 canonical —— 参考注释点名)
local CODEARTS_ADAPTER_SRC = [==[
plugin.K . CODEARTS_ADAPTER_SRC = [==[
local adapter = {}
adapter.name = "codearts"
adapter.version = "1.0.0"
@ -9753,7 +9966,7 @@ return adapter
-- 6. assistant 消息的 tool_calls: function → function_call( SOLO 字段名),
-- 并剔除没有 name 的条目(孤儿会让整条会话被拒)
-- 7. tools[].function.parameters: object → JSON **字符串**( SOLO 要求)
local TRAE_SOLO_ADAPTER_SRC = [==[
plugin.K . TRAE_SOLO_ADAPTER_SRC = [==[
local adapter = {}
adapter.name = "channels-trae"
adapter.version = "1.0.0"
@ -10345,11 +10558,11 @@ end
local function installTraeAdapter ( )
if not __llmsproxy_adapters then return false , " 无适配器桥(旧内核?) " end
-- 本地预编译:装载失败只回"源码无法编译", 不给行号; load 失败给行号。
local f , lerr = load ( TRAE_SOLO_ADAPTER_SRC )
local f , lerr = load ( plugin.K . TRAE_SOLO_ADAPTER_SRC)
if not f then return false , " 源码语法错误: " .. tostring ( lerr ) end
local _ , uerr = __llmsproxy_adapters.update ( " channels-trae " , TRAE_SOLO_ADAPTER_SRC )
local _ , uerr = __llmsproxy_adapters.update ( " channels-trae " , plugin.K . TRAE_SOLO_ADAPTER_SRC)
if uerr then
local _ , ierr = __llmsproxy_adapters.install ( " channels-trae " , TRAE_SOLO_ADAPTER_SRC )
local _ , ierr = __llmsproxy_adapters.install ( " channels-trae " , plugin.K . TRAE_SOLO_ADAPTER_SRC)
if ierr then return false , ierr end
end
return true , nil
@ -10373,7 +10586,7 @@ end
--
-- ⛔ 不做: gemini-sigstore( 签名持久化, 268 行)。我们推理走无状态适配器,
-- 插件侧缓存会成为死代码。签名缺失时上游会拒,适配器如实透传错误。
local GEMINI_PRIVATE_ADAPTER_SRC = [==[
plugin.K . GEMINI_PRIVATE_ADAPTER_SRC = [==[
local adapter = {}
adapter.name = "channels-gemini"
adapter.version = "1.0.0"
@ -10392,8 +10605,6 @@ adapter.headers = {
-- ⚠️ 刻意**不带** Accept( 参考: 流式请求不带 Accept, 抓包一致) 。
-- 也不需要 x-goog-api-key( Cloud Code 用 OAuth Bearer) 。
local GEMINI_DEFAULT_PROJECT = "aicode-consumers"
local GEMINI_UPSTREAM_USER_AGENT = "antigravity"
-- 字母序 JSON 序列化(参考 marshalAlphabetical → sortedStringify) 。
-- 为什么必须排序:上游按**字节**比对/缓存, 键序不同即不同请求。Go 的
@ -10545,7 +10756,7 @@ function adapter.transform_request(raw_body)
if req.top_p then gen.topP = req.top_p end
if next(gen) then greq.generationConfig = gen end
local project = (req.project and tostring(req.project)) or GEMINI_DEFAULT_PROJECT
local project = (req.project and tostring(req.project)) or "aicode-consumers"
greq.sessionId = derive_session_id(project, first_user_text(contents))
-- ③ 信封(字母序序列化)
@ -10554,7 +10765,7 @@ function adapter.transform_request(raw_body)
project = project,
request = greq,
requestId = "agent/" .. tostring(os.time()) .. "/" .. string.sub(sha256_hex(tostring(os.clock())), 1, 8),
userAgent = GEMINI_UPSTREAM_USER_AGENT ,
userAgent = "antigravity" ,
}
return sorted_json(envelope)
end
@ -10619,7 +10830,7 @@ return adapter
-- 厂商码值留在**插件**里(内核零厂商名)—— 内核只把 transform_error 返回的
-- 文本当作客户端可读原因; 额度类语义由内核既有的关键词表识别( quota/billing
-- 等通用英文短语),故这里只要把话**说清楚**即可。
local QODER_ADAPTER_SRC = [==[
plugin.K . QODER_ADAPTER_SRC = [==[
local adapter = { name = "channels-qoder", version = "1.0.0" }
-- 出向就是标准 OpenAI 形状( Qoder 的 chat 端点是 OpenAI 兼容的),
@ -10634,7 +10845,7 @@ function adapter.transform_response(raw_body) return __prelude.openai_response(r
function adapter.transform_stream_chunk(raw_chunk) return __prelude.openai_stream_chunk(raw_chunk) end
-- 厂商码值只出现在**这个适配器**里(内核零厂商名)。
local QODER_QUEUE_CODE = "10605"
QODER_QUEUE_CODE = "10605"
-- qoder_unwrap(root):广度优先展开 data/result/message/body。
-- ⚠️ 必须展开 `message` 这类**字符串再解析**的分支:只读顶层永远拿不到内层
@ -11019,6 +11230,7 @@ local ZD = (function()
end ) ( )
local ZCODE_ADAPTER_SRC = [===[
local CLI_PREFIX = "You are ZCode, an interactive coding agent"
-- ============================================================================
-- ZCode 专用适配器( 插件自装: channels-zcode)
--
@ -11048,7 +11260,6 @@ local ZCODE_ADAPTER_SRC = [===[
local adapter = { name = "channels-zcode", version = "1.0.0" }
-- ★ 官方 CLI 前缀(准入必需)。逐字复刻,勿改。
local CLI_PREFIX = "You are ZCode, an interactive coding agent"
-- ★ 官方 stable 段(准入必需;三段以 "\n\n" 连接)。逐字复刻,勿改。
-- 官方完整身份块还含 ~5KB 的 dynamic 段(给 coding agent 的行为指令),与准入
@ -11361,7 +11572,7 @@ return adapter
-- ⚠️ 时长 token 必须**累加**且**不吃重复匹配**: `minutes?` 与 `m` 都能匹配
-- 同一段会让时长翻倍(参考注释点名)。我们用一次扫描 + 贪婪单位即可。
local CLINE_ADAPTER_SRC = [==[
plugin.K . CLINE_ADAPTER_SRC = [==[
local adapter = { name = "channels-cline", version = "1.0.0" }
function adapter.transform_request(raw_body) return raw_body end
@ -11456,11 +11667,11 @@ return adapter
-- installClineAdapter():装/更新本插件自己的 Cline 适配器。
local function installClineAdapter ( )
if not __llmsproxy_adapters then return false , " 无适配器桥(旧内核?) " end
local f , lerr = load ( CLINE_ADAPTER_SRC )
local f , lerr = load ( plugin.K . CLINE_ADAPTER_SRC)
if not f then return false , " 源码语法错误: " .. tostring ( lerr ) end
local _ , uerr = __llmsproxy_adapters.update ( " channels-cline " , CLINE_ADAPTER_SRC )
local _ , uerr = __llmsproxy_adapters.update ( " channels-cline " , plugin.K . CLINE_ADAPTER_SRC)
if uerr then
local _ , ierr = __llmsproxy_adapters.install ( " channels-cline " , CLINE_ADAPTER_SRC )
local _ , ierr = __llmsproxy_adapters.install ( " channels-cline " , plugin.K . CLINE_ADAPTER_SRC)
if ierr then return false , ierr end
end
return true , nil
@ -11468,11 +11679,11 @@ end
local function installQoderAdapter ( )
if not __llmsproxy_adapters then return false , " 无适配器桥(旧内核?) " end
local f , lerr = load ( QODER_ADAPTER_SRC )
local f , lerr = load ( plugin.K . QODER_ADAPTER_SRC)
if not f then return false , " 源码语法错误: " .. tostring ( lerr ) end
local _ , uerr = __llmsproxy_adapters.update ( " channels-qoder " , QODER_ADAPTER_SRC )
local _ , uerr = __llmsproxy_adapters.update ( " channels-qoder " , plugin.K . QODER_ADAPTER_SRC)
if uerr then
local _ , ierr = __llmsproxy_adapters.install ( " channels-qoder " , QODER_ADAPTER_SRC )
local _ , ierr = __llmsproxy_adapters.install ( " channels-qoder " , plugin.K . QODER_ADAPTER_SRC)
if ierr then return false , ierr end
end
return true , nil
@ -11483,11 +11694,11 @@ local function installGeminiAdapter()
if not __llmsproxy_adapters then return false , " 无适配器桥(旧内核?) " end
-- 先在本地编译一次,把语法错误在**装载失败之前**变成可读信息
-- ( load 失败只给"源码无法编译",不给行号)
local f , lerr = load ( GEMINI_PRIVATE_ADAPTER_SRC )
local f , lerr = load ( plugin.K . GEMINI_PRIVATE_ADAPTER_SRC)
if not f then return false , " 源码语法错误: " .. tostring ( lerr ) end
local _ , uerr = __llmsproxy_adapters.update ( " channels-gemini " , GEMINI_PRIVATE_ADAPTER_SRC )
local _ , uerr = __llmsproxy_adapters.update ( " channels-gemini " , plugin.K . GEMINI_PRIVATE_ADAPTER_SRC)
if uerr then
local _ , ierr = __llmsproxy_adapters.install ( " channels-gemini " , GEMINI_PRIVATE_ADAPTER_SRC )
local _ , ierr = __llmsproxy_adapters.install ( " channels-gemini " , plugin.K . GEMINI_PRIVATE_ADAPTER_SRC)
if ierr then return false , ierr end
end
return true , nil
@ -11542,11 +11753,11 @@ end
local function installCodeartsAdapter ( )
if not __llmsproxy_adapters then return false , " 无适配器桥(旧内核?) " end
-- 本地预编译:装载失败只回"源码无法编译", 不给行号; load 失败给行号。
local f , lerr = load ( CODEARTS_ADAPTER_SRC )
local f , lerr = load ( plugin.K . CODEARTS_ADAPTER_SRC)
if not f then return false , " 源码语法错误: " .. tostring ( lerr ) end
local _ , uerr = __llmsproxy_adapters.update ( " codearts " , CODEARTS_ADAPTER_SRC )
local _ , uerr = __llmsproxy_adapters.update ( " codearts " , plugin.K . CODEARTS_ADAPTER_SRC)
if uerr then
local _ , ierr = __llmsproxy_adapters.install ( " codearts " , CODEARTS_ADAPTER_SRC )
local _ , ierr = __llmsproxy_adapters.install ( " codearts " , plugin.K . CODEARTS_ADAPTER_SRC)
if ierr then return false , ierr end
end
return true , nil
@ -11560,8 +11771,8 @@ function plugin.__codearts_adapter_sign_probe(args)
args = args or { }
-- 从适配器池里取 build_headers 的结果不易(插件 VM 与适配器 VM 分离),
-- 故用**同一份源码**在本 VM 里求值后调用 —— 源码是单一定义(上面那个
-- CODEARTS_ADAPTER_SRC 常量),不存在两处漂移。
local f = load ( CODEARTS_ADAPTER_SRC )
-- plugin.K. CODEARTS_ADAPTER_SRC 常量),不存在两处漂移。
local f = load ( plugin.K . CODEARTS_ADAPTER_SRC)
if not f then return { json = { error = " 适配器源码无法编译 " } } end
local adapter = f ( )
local meta = {
@ -11917,7 +12128,10 @@ plugin.ui = {
const cap = (await call("channels/auto_checkin","POST",{})) && true; // noop, 保持顺序
// 动作区:登录(加账号)/ 刷新全部 / 一键领取 / 供应商开关
const acts = [];
acts.push('<button class="small" onclick="chStartLogin()">添加账号(登录)</button>');
acts.push('<button class="small" onclick="chStartLogin()">添加账号(登录)</button>'
+ (["buddy","qoder"].includes(id)
? ' <button class="small" title="登录国际区( WorkBuddy / qoder.com) " onclick="chStartLogin(\'intl\')">+国际区</button>'
: ''));
if ((s.accounts.total||0) > 0) acts.push('<button class="small" onclick="chRefreshAll()">刷新全部账号</button>');
acts.push('<button class="small" onclick="chClaimAll()">一键领取额度</button>');
acts.push('<button class="small" onclick="chResetAll()">重置限流标记</button>');
@ -12092,13 +12306,16 @@ plugin.ui = {
function notice(msg) { q("#ch-notice").textContent = msg; }
// ---- 登录(对齐参考两步式:立即拿 URL/弹窗,后台轮询完成)----
window.chStartLogin = async () => {
window.chStartLogin = async (region ) => {
const id = state.selected; if (!id) return;
// ★ 双区:按钮按住 Shift 点 = 登录国际区( buddy→WorkBuddy / qoder→qoder.com) 。
// 简单可靠:不需要为双区改版式,一行 title 提示即可。默认国内/国际为主区。
const r = (region === "intl") ? "intl" : "cn";
// 池路径:先建占位账号,登录完成后回填凭据到这个账号
const aj = await call("channels/account/add", "POST", { platform: id });
if (aj.error) { alert(aj.error); return; }
const aid = aj.account_id;
const j = await call("channels/login", "POST", { platform: id, account_id: aid });
const j = await call("channels/login", "POST", { platform: id, account_id: aid, region: r });
if (j.error) { alert(j.error); return; }
const box = q("#ch-login-box"); box.style.display = "block";
const vnc = q("#ch-vnc-wrap");
@ -12121,8 +12338,13 @@ plugin.ui = {
"<p class='muted' id='ch-login-state'></p>";
} else {
vnc.style.display = "none";
// 扫码类流程( qr-poll) 的地址是「用微信扫」的落地页, 直接点开也能拿到
// 页面里的二维码,故文案要说清楚,别让用户以为点开就自动登录了。
const isQr = !!(j.session && j.session.flow === "qr-poll");
q("#ch-login-body").innerHTML =
"<p>请在你自己的浏览器打开下面的地址完成登录:</p> " +
"<p>" + (isQr
? "用<b>微信扫码</b>登录(本机点开下面地址,页面里也有二维码):"
: "请在你自己的浏览器打开下面的地址完成登录:") + "</p>" +
"<p><a href='" + esc(j.url) + "' target='_blank' rel='noopener'>" + esc(j.url) + "</a></p>" +
(j.session && j.session.user_code ? "<p>设备码:<b>" + esc(j.session.user_code) + "</b></p>" : "") +
"<p class='muted' id='ch-login-state'>等待完成…</p>";
@ -12499,10 +12721,17 @@ function plugin.__inject_refresh_platform_probe(args)
hosts = { " 127.0.0.1 " } ,
}
if repl then lst [ repl ] = entry else lst [ # lst + 1 ] = entry end
-- 建账号(凭据走保险库)
-- 建账号(凭据走保险库)。expiresAt 从凭据的 expires_at 派生(毫秒→秒),
-- 供"账号已过期"类判据与 account_list 展示;解析失败保持 0。
local ref = " channels: " .. id
__llmsproxy_secrets.set ( ref , tostring ( args.credential or " {} " ) )
poolAdd ( id , { id = " a1 " , credentialRef = ref , priority = 1 , expires At = 0 } )
local expAt = 0
local dok , dcred = pcall ( json.decode , tostring ( args.credential or " {} " ) )
if dok and type ( dcred ) == " table " then
local ms = credentialExpiresAtMs ( dcred )
if ms then expAt = math.floor ( ms / 1000 ) end
end
poolAdd ( id , { id = " a1 " , credentialRef = ref , priority = 1 , expiresAt = expAt } )
return { json = { ok = true } }
end
@ -12609,6 +12838,15 @@ function plugin.__rank_accounts_probe(args)
usable_n = # ( r.usable or { } ) } }
end
-- plugin.__chosen_account_probe: 只给测试用 —— 当前为该通道服务的账号 id。
-- 一通道一源之后, account_reorder 的效果不再是"源列表顺序",而是
-- **选中的账号变了**( 源同名覆盖, api_key 换成新账号的 token) → 内核先试它。
function plugin . __chosen_account_probe ( args )
args = args or { }
local provider = tostring ( args.platform or " " )
return { json = { account_id = ( plugin.state . chosen or { } ) [ provider ] or " " } }
end
-- plugin.__expiring_window_probe: 只给测试用 —— 读当前生效的窗口天数。
function plugin . __expiring_window_probe ( )
return { json = { days = expiringWindowDays ( ) } }