mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-19 14:24:45 +08:00
feat: Anthropic 账号被动用量采样,页面默认展示被动数据
从上游 /v1/messages 响应头被动采集 5h/7d utilization 并存储到 Account.Extra,页面加载时直接读取本地数据而非调用外部 Usage API。 用户可点击"查询"按钮主动拉取最新数据,主动查询结果自动回写被动缓存。 后端: - UpdateSessionWindow 合并采集 5h + 7d headers 为单次 DB 写入 - 新增 GetPassiveUsage 从 Extra 构建 UsageInfo (复用 estimateSetupTokenUsage) - GetUsage 主动查询后 syncActiveToPassive 回写被动缓存 - passive_usage_ 前缀注册为 scheduler-neutral 前端: - Anthropic 账号 mount/refresh 默认 source=passive - 新增"被动采样"标签和"查询"按钮 (带 loading 动画)
This commit is contained in:
@@ -177,6 +177,7 @@ type AICredit struct {
|
||||
|
||||
// UsageInfo 账号使用量信息
|
||||
type UsageInfo struct {
|
||||
Source string `json:"source,omitempty"` // "passive" or "active"
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"` // 更新时间
|
||||
FiveHour *UsageProgress `json:"five_hour"` // 5小时窗口
|
||||
SevenDay *UsageProgress `json:"seven_day,omitempty"` // 7天窗口
|
||||
@@ -393,6 +394,9 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U
|
||||
// 4. 添加窗口统计(有独立缓存,1 分钟)
|
||||
s.addWindowStats(ctx, account, usage)
|
||||
|
||||
// 5. 将主动查询结果同步到被动缓存,下次 passive 加载即为最新值
|
||||
s.syncActiveToPassive(ctx, account.ID, usage)
|
||||
|
||||
s.tryClearRecoverableAccountError(ctx, account)
|
||||
return usage, nil
|
||||
}
|
||||
@@ -409,6 +413,81 @@ func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*U
|
||||
return nil, fmt.Errorf("account type %s does not support usage query", account.Type)
|
||||
}
|
||||
|
||||
// GetPassiveUsage 从 Account.Extra 中的被动采样数据构建 UsageInfo,不调用外部 API。
|
||||
// 仅适用于 Anthropic OAuth / SetupToken 账号。
|
||||
func (s *AccountUsageService) GetPassiveUsage(ctx context.Context, accountID int64) (*UsageInfo, error) {
|
||||
account, err := s.accountRepo.GetByID(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get account failed: %w", err)
|
||||
}
|
||||
|
||||
if !account.IsAnthropicOAuthOrSetupToken() {
|
||||
return nil, fmt.Errorf("passive usage only supported for Anthropic OAuth/SetupToken accounts")
|
||||
}
|
||||
|
||||
// 复用 estimateSetupTokenUsage 构建 5h 窗口(OAuth 和 SetupToken 逻辑一致)
|
||||
info := s.estimateSetupTokenUsage(account)
|
||||
info.Source = "passive"
|
||||
|
||||
// 设置采样时间
|
||||
if raw, ok := account.Extra["passive_usage_sampled_at"]; ok {
|
||||
if str, ok := raw.(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, str); err == nil {
|
||||
info.UpdatedAt = &t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建 7d 窗口(从被动采样数据)
|
||||
util7d := parseExtraFloat64(account.Extra["passive_usage_7d_utilization"])
|
||||
reset7dRaw := parseExtraFloat64(account.Extra["passive_usage_7d_reset"])
|
||||
if util7d > 0 || reset7dRaw > 0 {
|
||||
var resetAt *time.Time
|
||||
var remaining int
|
||||
if reset7dRaw > 0 {
|
||||
t := time.Unix(int64(reset7dRaw), 0)
|
||||
resetAt = &t
|
||||
remaining = int(time.Until(t).Seconds())
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
}
|
||||
info.SevenDay = &UsageProgress{
|
||||
Utilization: util7d * 100,
|
||||
ResetsAt: resetAt,
|
||||
RemainingSeconds: remaining,
|
||||
}
|
||||
}
|
||||
|
||||
// 添加窗口统计
|
||||
s.addWindowStats(ctx, account, info)
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// syncActiveToPassive 将主动查询的最新数据回写到 Extra 被动缓存,
|
||||
// 这样下次被动加载时能看到最新值。
|
||||
func (s *AccountUsageService) syncActiveToPassive(ctx context.Context, accountID int64, usage *UsageInfo) {
|
||||
extraUpdates := make(map[string]any, 4)
|
||||
|
||||
if usage.FiveHour != nil {
|
||||
extraUpdates["session_window_utilization"] = usage.FiveHour.Utilization / 100
|
||||
}
|
||||
if usage.SevenDay != nil {
|
||||
extraUpdates["passive_usage_7d_utilization"] = usage.SevenDay.Utilization / 100
|
||||
if usage.SevenDay.ResetsAt != nil {
|
||||
extraUpdates["passive_usage_7d_reset"] = usage.SevenDay.ResetsAt.Unix()
|
||||
}
|
||||
}
|
||||
|
||||
if len(extraUpdates) > 0 {
|
||||
extraUpdates["passive_usage_sampled_at"] = time.Now().UTC().Format(time.RFC3339)
|
||||
if err := s.accountRepo.UpdateExtra(ctx, accountID, extraUpdates); err != nil {
|
||||
slog.Warn("sync_active_to_passive_failed", "account_id", accountID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AccountUsageService) getOpenAIUsage(ctx context.Context, account *Account) (*UsageInfo, error) {
|
||||
now := time.Now()
|
||||
usage := &UsageInfo{UpdatedAt: &now}
|
||||
|
||||
Reference in New Issue
Block a user