mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 04:07:25 +00:00
fix(channels): 登录后账号拿不到凭据 —— 完成路径不传 account_id + 只存裸 token
用户报障:「登录了,但插件这边无法获取账号信息」。实证: account_list → has_credential=false,credits/balances → 「无凭据」, 而 UI 上账号显示已登录。 ## 根因一:完成路径没把凭据写进池账号的键 UI 的真实流程是「account/add 建占位账号(credentialRef=channels:<plat>:<aid>) → login 带 account_id → 轮询 login/status」。但 poll_login 的两条完成分支 (device-poll / state-poll)调用 complete_login 时**没有传 account_id**, 于是走了兼容旧路径,凭据落到 `channels:<plat>` 这个与占位账号无关的键; 池账号的 credentialRef 指向 `channels:<plat>:<aid>`,从未写入 → account_list 的 has_credential 恒为 false。 修:两条分支都传 `account_id = s.account_id`(对齐参考 BuddyAuth.login 的 `flowOptions.accountId` 语义:多账号场景下凭据按账号 ref 写入)。 ## 根因二:只存裸 access_token,丢掉 refresh_token 与过期信息 state-poll 分支只取 `data.accessToken` 当凭据存,而参考 `runBuddyLoginFlow` 存的是 `buildCredential(token, account)` 的**完整 JSON** (access_token + refresh_token + expires_at + refresh_expires_at + token_type + scope + domain + user_id + nickname + enterprise_id + account_type)。 只存裸 token 的后果:refreshable 恒为 false → 无静默续期;expires_at 未知。 修:新增 buildCredential(逐字段对齐参考 buddy.ts)+ jwtClaim/absExpiryMs (参照实现:token 端点只回 expiresIn 不回 expiresAt,昵称只在 JWT 声明里) + credentialExpiresAtMs。state-poll 完成时存完整凭据 JSON 并回报 expires_at/refreshable。 ## 配套 - 新增 credentialOf(ref):凭据取值助手,**裸 token 与完整 JSON 两种形态都认** (历史数据/device-poll/paste 是裸 token,buddy 是 JSON)。所有消费点 (上游源 api_key、Bearer 头、HMAC 签名、备份)改走它,无须关心存储形态。 - complete_login 从凭据自动补齐 nickname/expiresAt/refreshable,调用方漏传也不丢。 - 内核新增 base64_decode(标准/base64url/无填充都认):插件读 JWT payload 必需,否则要在 Lua 里重写 base64。通用能力,无厂商知识。 ## 判据 TestBuddyStatePollBehavior 行2/行4: - 行2 断言凭据是完整 JSON(含 refresh_token + 昵称),非裸 token - 行4 回归用户报障:走 account/add → 带 account_id 登录,断言 ① 凭据落在 channels:buddy:<aid> ② account_list 报 has_credential=true ③ refreshable=true 变异验证:去掉 account_id → 行4 立即 FAIL(已实测)。 go test ./internal/core/ 与 ./internal/lua/ 全绿。
This commit is contained in:
@ -1358,6 +1358,38 @@ func setupGlobals(L *golua.State) {
|
||||
L.PushString(base64.StdEncoding.EncodeToString([]byte(L.ToString(1))))
|
||||
return 1
|
||||
})
|
||||
// base64_decode accepts standard, URL-safe and unpadded alphabets. It exists
|
||||
// because a plugin reading a JWT payload (e.g. to read `exp`/`nickname` when
|
||||
// the token endpoint omits them) must decode base64url, and hardcoding that
|
||||
// in Lua would mean reimplementing base64 in a plugin. Generic, no vendor
|
||||
// knowledge: an invalid string yields (nil, err), never a partial result.
|
||||
registerFn(L, "base64_decode", func(L *golua.State) int {
|
||||
s := strings.TrimSpace(L.ToString(1))
|
||||
if s == "" {
|
||||
L.PushString("")
|
||||
L.PushNil()
|
||||
return 2
|
||||
}
|
||||
// Normalise URL-safe alphabet and re-pad to a multiple of 4.
|
||||
s = strings.NewReplacer("-", "+", "_", "/").Replace(s)
|
||||
if pad := len(s) % 4; pad != 0 {
|
||||
if pad == 1 {
|
||||
L.PushNil()
|
||||
L.PushString("invalid base64 length")
|
||||
return 2
|
||||
}
|
||||
s += strings.Repeat("=", 4-pad)
|
||||
}
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
L.PushNil()
|
||||
L.PushString(err.Error())
|
||||
return 2
|
||||
}
|
||||
L.PushString(string(b))
|
||||
L.PushNil()
|
||||
return 2
|
||||
})
|
||||
registerFn(L, "tohex", func(L *golua.State) int {
|
||||
L.PushString(hex.EncodeToString([]byte(L.ToString(1))))
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user