mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(account): 重新授权不再清空 Extra 配置
Claude / OpenAI 账号重新授权走通用 PUT /accounts/:id 时,后端 UpdateAccount 会全量覆盖 account.Extra(仅保留 5 个 quota 用量键), 导致 base_rpm / window_cost_limit / window_cost_sticky_reserve / max_sessions / quota_* / privacy_mode 等持久化配置全部丢失。 新增专用接口 POST /accounts/:id/apply-oauth-credentials,沿用 现有 /refresh 路径模式:Credentials-only update + Extra JSONB key 级合并(UpdateAccountExtra) + ClearError + InvalidateToken。 作用域:Claude OAuth / Claude Cookie auth / OpenAI OAuth 三个 调用点。Gemini / Antigravity 现有路径本就不传 extra,保持不变。 顺带修复:旧重新授权路径未调用 InvalidateToken,导致重新授权后 首请求可能仍用缓存中的旧 token 而立即 401。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
QTom
co-authored by
Claude Opus 4.7
parent
9ef144874a
commit
11fe7de926
@@ -981,6 +981,100 @@ func (h *AccountHandler) Refresh(c *gin.Context) {
|
||||
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), updatedAccount))
|
||||
}
|
||||
|
||||
// ApplyOAuthCredentialsRequest is the payload for persisting re-authorized OAuth credentials.
|
||||
type ApplyOAuthCredentialsRequest struct {
|
||||
Type string `json:"type" binding:"required,oneof=oauth setup-token"`
|
||||
Credentials map[string]any `json:"credentials" binding:"required"`
|
||||
Extra map[string]any `json:"extra"`
|
||||
}
|
||||
|
||||
// ApplyOAuthCredentials 将"重新授权"得到的新凭据原子落库。
|
||||
// POST /api/v1/admin/accounts/:id/apply-oauth-credentials
|
||||
//
|
||||
// 与通用 PUT /:id (Update) 接口的关键区别:
|
||||
// - 仅接收 type / credentials / extra 三个字段(不接受 concurrency / rpm / quota_* 等可能误传的字段)
|
||||
// - Extra 走 UpdateAccountExtra(JSONB key 级合并),**绝不**全量覆盖;
|
||||
// 避免 base_rpm / window_cost_limit / max_sessions / quota_* / privacy_mode
|
||||
// 等持久化配置在重新授权后丢失
|
||||
// - 内置 ClearError + InvalidateToken,避免前端额外两次调用,
|
||||
// 并修复旧路径未失效 token 缓存导致重新授权后立即 401 的隐性 bug
|
||||
//
|
||||
// 与 /refresh 的区别:/refresh 用现有 refresh_token 换 access_token(无用户交互),
|
||||
// 本接口承接前端完成完整 OAuth 流程后的落库步骤。
|
||||
func (h *AccountHandler) ApplyOAuthCredentials(c *gin.Context) {
|
||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid account ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req ApplyOAuthCredentialsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 预检查账号存在 + OAuth 类型(与 Refresh handler 语义一致,提供更友好的错误信息)。
|
||||
existing, err := h.adminService.GetAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
response.NotFound(c, "Account not found")
|
||||
return
|
||||
}
|
||||
if !existing.IsOAuth() {
|
||||
response.ErrorFrom(c, infraerrors.BadRequest("NOT_OAUTH", "cannot apply oauth credentials to non-OAuth account"))
|
||||
return
|
||||
}
|
||||
|
||||
updatedAccount, err := h.adminService.UpdateAccount(ctx, accountID, &service.UpdateAccountInput{
|
||||
Type: req.Type,
|
||||
Credentials: req.Credentials,
|
||||
})
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 增量合并 Extra(JSONB key 级 merge,绝不覆盖 base_rpm / window_cost_limit /
|
||||
// max_sessions / quota_* / privacy_mode 等持久化键)。
|
||||
// best-effort:失败仅记日志;下方 ClearAccountError 会从 DB 重新读取最新 account,
|
||||
// 因此响应里的 extra 始终以 DB 为准——这里不需要手动维护内存快照。
|
||||
if len(req.Extra) > 0 {
|
||||
if extraErr := h.adminService.UpdateAccountExtra(ctx, accountID, req.Extra); extraErr != nil {
|
||||
extraKeys := make([]string, 0, len(req.Extra))
|
||||
for k := range req.Extra {
|
||||
extraKeys = append(extraKeys, k)
|
||||
}
|
||||
slog.Error("apply_oauth_credentials.update_extra_failed",
|
||||
"account_id", accountID,
|
||||
"extra_keys", extraKeys,
|
||||
"err", extraErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if cleared, clearErr := h.adminService.ClearAccountError(ctx, accountID); clearErr != nil {
|
||||
slog.Warn("apply_oauth_credentials.clear_error_failed",
|
||||
"account_id", accountID,
|
||||
"err", clearErr,
|
||||
)
|
||||
} else if cleared != nil {
|
||||
updatedAccount = cleared
|
||||
}
|
||||
|
||||
if h.tokenCacheInvalidator != nil && updatedAccount.IsOAuth() {
|
||||
if invalidateErr := h.tokenCacheInvalidator.InvalidateToken(ctx, updatedAccount); invalidateErr != nil {
|
||||
slog.Warn("apply_oauth_credentials.invalidate_token_failed",
|
||||
"account_id", accountID,
|
||||
"err", invalidateErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, h.buildAccountResponseWithRuntime(ctx, updatedAccount))
|
||||
}
|
||||
|
||||
// GetStats handles getting account statistics
|
||||
// GET /api/v1/admin/accounts/:id/stats
|
||||
func (h *AccountHandler) GetStats(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user