diff --git a/backend/internal/handler/dto/account_list_item_opencode_usage_test.go b/backend/internal/handler/dto/account_list_item_opencode_usage_test.go new file mode 100644 index 0000000000..74997e8e2a --- /dev/null +++ b/backend/internal/handler/dto/account_list_item_opencode_usage_test.go @@ -0,0 +1,57 @@ +package dto + +import ( + "encoding/json" + "testing" + "time" + + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/stretchr/testify/require" +) + +// 账号列表页固定传 lite=1,lite 投影(AccountListItemFromAccount)必须与完整 +// Account DTO 一样携带 opencode_go_usage,否则前端用量单元格会退回「余额 --」。 +func TestAccountListItemFromAccount_CarriesOpenCodeGoUsage(t *testing.T) { + now := time.Now().UTC() + // 生产账号 164 的形态:deepseek 平台 + OpenCode Go 官方基址 + apikey + eligible := &service.Account{ + ID: 164, Name: "DeepSeek OpenCode", Platform: service.PlatformDeepseek, Type: service.AccountTypeAPIKey, + Credentials: map[string]any{"base_url": "https://opencode.ai/zen/go/v1", "api_key": "test-key"}, + Extra: map[string]any{ + service.OpenCodeGoUsageAutoRefreshExtraKey: true, + service.OpenCodeGoUsageSnapshotExtraKey: &service.OpenCodeGoUsageSnapshot{ + Status: service.OpenCodeGoUsageStatusOK, + Data: &service.OpenCodeGoUsageData{Rolling: service.OpenCodeGoUsageWindow{Status: "ok", Percent: 6}}, + LastAttemptAt: now, NextRefreshAt: now.Add(time.Hour), + }, + }, + Status: service.StatusActive, + } + + item := AccountListItemFromAccount(AccountFromServiceShallow(eligible)) + require.NotNil(t, item.OpenCodeGoUsage) + require.True(t, item.OpenCodeGoUsage.Eligible) + require.True(t, item.OpenCodeGoUsage.AutoRefreshEnabled) + require.NotNil(t, item.OpenCodeGoUsage.Snapshot) + require.NotNil(t, item.OpenCodeGoUsage.Snapshot.Data) + require.Equal(t, 6.0, item.OpenCodeGoUsage.Snapshot.Data.Rolling.Percent) + + raw, err := json.Marshal(item) + require.NoError(t, err) + require.Contains(t, string(raw), `"opencode_go_usage"`) + require.Contains(t, string(raw), `"eligible":true`) + + // 对照:base_url 指向 api.deepseek.com 的普通 deepseek 账号不参与用量窗口 + ineligible := &service.Account{ + ID: 165, Name: "DeepSeek Direct", Platform: service.PlatformDeepseek, Type: service.AccountTypeAPIKey, + Credentials: map[string]any{"base_url": "https://api.deepseek.com", "api_key": "test-key-2"}, + Status: service.StatusActive, + } + + plainItem := AccountListItemFromAccount(AccountFromServiceShallow(ineligible)) + require.Nil(t, plainItem.OpenCodeGoUsage) + + plainRaw, err := json.Marshal(plainItem) + require.NoError(t, err) + require.NotContains(t, string(plainRaw), `"opencode_go_usage"`) +} diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index c30ce7d0a9..42a1d82594 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -465,8 +465,8 @@ func AccountListItemFromAccount(a *Account) *AccountListItem { return &AccountListItem{ ID: a.ID, Name: a.Name, Notes: a.Notes, Platform: a.Platform, Type: a.Type, Credentials: a.Credentials, CredentialsStatus: a.CredentialsStatus, Extra: a.Extra, - OllamaCloudUsage: a.OllamaCloudUsage, - ProxyID: a.ProxyID, ProxyFallbackOriginID: a.ProxyFallbackOriginID, ProxyFallbackOriginName: a.ProxyFallbackOriginName, + OllamaCloudUsage: a.OllamaCloudUsage, OpenCodeGoUsage: a.OpenCodeGoUsage, + ProxyID: a.ProxyID, ProxyFallbackOriginID: a.ProxyFallbackOriginID, ProxyFallbackOriginName: a.ProxyFallbackOriginName, Concurrency: a.Concurrency, LoadFactor: a.LoadFactor, Priority: a.Priority, RateMultiplier: a.RateMultiplier, Status: a.Status, ErrorMessage: a.ErrorMessage, LastUsedAt: a.LastUsedAt, ExpiresAt: a.ExpiresAt, AutoPauseOnExpired: a.AutoPauseOnExpired, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 1177f68737..73c02a7af7 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -341,6 +341,7 @@ type AccountListItem struct { CredentialsStatus map[string]bool `json:"credentials_status,omitempty"` Extra map[string]any `json:"extra,omitempty"` OllamaCloudUsage *service.OllamaCloudUsageState `json:"ollama_cloud_usage,omitempty"` + OpenCodeGoUsage *service.OpenCodeGoUsageState `json:"opencode_go_usage,omitempty"` ProxyID *int64 `json:"proxy_id"` ProxyFallbackOriginID *int64 `json:"proxy_fallback_origin_id"`