mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-05 16:00:21 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acabdc2f99 | ||
|
|
169aa4716e | ||
|
|
c0753320a0 | ||
|
|
38d875b06f | ||
|
|
1ada6cf768 | ||
|
|
2b528c5f81 | ||
|
|
f6dd4752e7 | ||
|
|
b19c7875a4 | ||
|
|
d99a3ef14b | ||
|
|
fc8fa83fcc | ||
|
|
6dcd99468b | ||
|
|
d5ba7b80d3 |
6
Makefile
6
Makefile
@@ -9,7 +9,7 @@ build-backend:
|
|||||||
|
|
||||||
# 编译前端(需要已安装依赖)
|
# 编译前端(需要已安装依赖)
|
||||||
build-frontend:
|
build-frontend:
|
||||||
@npm --prefix frontend run build
|
@pnpm --dir frontend run build
|
||||||
|
|
||||||
# 运行测试(后端 + 前端)
|
# 运行测试(后端 + 前端)
|
||||||
test: test-backend test-frontend
|
test: test-backend test-frontend
|
||||||
@@ -18,5 +18,5 @@ test-backend:
|
|||||||
@$(MAKE) -C backend test
|
@$(MAKE) -C backend test
|
||||||
|
|
||||||
test-frontend:
|
test-frontend:
|
||||||
@npm --prefix frontend run lint:check
|
@pnpm --dir frontend run lint:check
|
||||||
@npm --prefix frontend run typecheck
|
@pnpm --dir frontend run typecheck
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot
|
|||||||
|
|
||||||
| Component | Technology |
|
| Component | Technology |
|
||||||
|-----------|------------|
|
|-----------|------------|
|
||||||
| Backend | Go 1.25.5, Gin, GORM |
|
| Backend | Go 1.25.5, Gin, Ent |
|
||||||
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
|
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||||
| Database | PostgreSQL 15+ |
|
| Database | PostgreSQL 15+ |
|
||||||
| Cache/Queue | Redis 7+ |
|
| Cache/Queue | Redis 7+ |
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅(
|
|||||||
|
|
||||||
| 组件 | 技术 |
|
| 组件 | 技术 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 后端 | Go 1.25.5, Gin, GORM |
|
| 后端 | Go 1.25.5, Gin, Ent |
|
||||||
| 前端 | Vue 3.4+, Vite 5+, TailwindCSS |
|
| 前端 | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||||
| 数据库 | PostgreSQL 15+ |
|
| 数据库 | PostgreSQL 15+ |
|
||||||
| 缓存/队列 | Redis 7+ |
|
| 缓存/队列 | Redis 7+ |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.21-alpine
|
FROM golang:1.25.5-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ func provideCleanup(
|
|||||||
entClient *ent.Client,
|
entClient *ent.Client,
|
||||||
rdb *redis.Client,
|
rdb *redis.Client,
|
||||||
tokenRefresh *service.TokenRefreshService,
|
tokenRefresh *service.TokenRefreshService,
|
||||||
|
accountExpiry *service.AccountExpiryService,
|
||||||
pricing *service.PricingService,
|
pricing *service.PricingService,
|
||||||
emailQueue *service.EmailQueueService,
|
emailQueue *service.EmailQueueService,
|
||||||
billingCache *service.BillingCacheService,
|
billingCache *service.BillingCacheService,
|
||||||
@@ -84,6 +85,10 @@ func provideCleanup(
|
|||||||
tokenRefresh.Stop()
|
tokenRefresh.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"AccountExpiryService", func() error {
|
||||||
|
accountExpiry.Stop()
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"PricingService", func() error {
|
{"PricingService", func() error {
|
||||||
pricing.Stop()
|
pricing.Stop()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
|
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
|
||||||
geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
|
geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
|
||||||
geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig)
|
geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig)
|
||||||
|
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
||||||
geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
|
geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
|
||||||
tempUnschedCache := repository.NewTempUnschedCache(redisClient)
|
tempUnschedCache := repository.NewTempUnschedCache(redisClient)
|
||||||
rateLimitService := service.NewRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache)
|
rateLimitService := service.NewRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache)
|
||||||
@@ -97,13 +98,12 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
geminiTokenCache := repository.NewGeminiTokenCache(redisClient)
|
geminiTokenCache := repository.NewGeminiTokenCache(redisClient)
|
||||||
geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
|
geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
|
||||||
gatewayCache := repository.NewGatewayCache(redisClient)
|
gatewayCache := repository.NewGatewayCache(redisClient)
|
||||||
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
|
||||||
antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
|
antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
|
||||||
httpUpstream := repository.NewHTTPUpstream(configConfig)
|
httpUpstream := repository.NewHTTPUpstream(configConfig)
|
||||||
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
||||||
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
|
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
|
||||||
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
||||||
concurrencyService := service.NewConcurrencyService(concurrencyCache)
|
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
|
||||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
|
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
|
||||||
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
||||||
@@ -114,7 +114,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
adminRedeemHandler := admin.NewRedeemHandler(adminService)
|
adminRedeemHandler := admin.NewRedeemHandler(adminService)
|
||||||
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService)
|
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService)
|
||||||
updateCache := repository.NewUpdateCache(redisClient)
|
updateCache := repository.NewUpdateCache(redisClient)
|
||||||
gitHubReleaseClient := repository.NewGitHubReleaseClient()
|
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
|
||||||
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
|
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
|
||||||
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
|
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
|
||||||
systemHandler := handler.ProvideSystemHandler(updateService)
|
systemHandler := handler.ProvideSystemHandler(updateService)
|
||||||
@@ -125,7 +125,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
|
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
|
||||||
userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService)
|
userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService)
|
||||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler)
|
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler)
|
||||||
pricingRemoteClient := repository.NewPricingRemoteClient(configConfig)
|
pricingRemoteClient := repository.ProvidePricingRemoteClient(configConfig)
|
||||||
pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
|
pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -148,7 +148,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
||||||
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
||||||
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, configConfig)
|
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, configConfig)
|
||||||
v := provideCleanup(client, redisClient, tokenRefreshService, pricingService, emailQueueService, billingCacheService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
|
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
||||||
|
v := provideCleanup(client, redisClient, tokenRefreshService, accountExpiryService, pricingService, emailQueueService, billingCacheService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
|
||||||
application := &Application{
|
application := &Application{
|
||||||
Server: httpServer,
|
Server: httpServer,
|
||||||
Cleanup: v,
|
Cleanup: v,
|
||||||
@@ -174,6 +175,7 @@ func provideCleanup(
|
|||||||
entClient *ent.Client,
|
entClient *ent.Client,
|
||||||
rdb *redis.Client,
|
rdb *redis.Client,
|
||||||
tokenRefresh *service.TokenRefreshService,
|
tokenRefresh *service.TokenRefreshService,
|
||||||
|
accountExpiry *service.AccountExpiryService,
|
||||||
pricing *service.PricingService,
|
pricing *service.PricingService,
|
||||||
emailQueue *service.EmailQueueService,
|
emailQueue *service.EmailQueueService,
|
||||||
billingCache *service.BillingCacheService,
|
billingCache *service.BillingCacheService,
|
||||||
@@ -194,6 +196,10 @@ func provideCleanup(
|
|||||||
tokenRefresh.Stop()
|
tokenRefresh.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"AccountExpiryService", func() error {
|
||||||
|
accountExpiry.Stop()
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"PricingService", func() error {
|
{"PricingService", func() error {
|
||||||
pricing.Stop()
|
pricing.Stop()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ type Account struct {
|
|||||||
ErrorMessage *string `json:"error_message,omitempty"`
|
ErrorMessage *string `json:"error_message,omitempty"`
|
||||||
// LastUsedAt holds the value of the "last_used_at" field.
|
// LastUsedAt holds the value of the "last_used_at" field.
|
||||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||||
|
// Account expiration time (NULL means no expiration).
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||||
|
// Auto pause scheduling when account expires.
|
||||||
|
AutoPauseOnExpired bool `json:"auto_pause_on_expired,omitempty"`
|
||||||
// Schedulable holds the value of the "schedulable" field.
|
// Schedulable holds the value of the "schedulable" field.
|
||||||
Schedulable bool `json:"schedulable,omitempty"`
|
Schedulable bool `json:"schedulable,omitempty"`
|
||||||
// RateLimitedAt holds the value of the "rate_limited_at" field.
|
// RateLimitedAt holds the value of the "rate_limited_at" field.
|
||||||
@@ -129,13 +133,13 @@ func (*Account) scanValues(columns []string) ([]any, error) {
|
|||||||
switch columns[i] {
|
switch columns[i] {
|
||||||
case account.FieldCredentials, account.FieldExtra:
|
case account.FieldCredentials, account.FieldExtra:
|
||||||
values[i] = new([]byte)
|
values[i] = new([]byte)
|
||||||
case account.FieldSchedulable:
|
case account.FieldAutoPauseOnExpired, account.FieldSchedulable:
|
||||||
values[i] = new(sql.NullBool)
|
values[i] = new(sql.NullBool)
|
||||||
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
|
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
|
||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldSessionWindowStatus:
|
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldSessionWindowStatus:
|
||||||
values[i] = new(sql.NullString)
|
values[i] = new(sql.NullString)
|
||||||
case account.FieldCreatedAt, account.FieldUpdatedAt, account.FieldDeletedAt, account.FieldLastUsedAt, account.FieldRateLimitedAt, account.FieldRateLimitResetAt, account.FieldOverloadUntil, account.FieldSessionWindowStart, account.FieldSessionWindowEnd:
|
case account.FieldCreatedAt, account.FieldUpdatedAt, account.FieldDeletedAt, account.FieldLastUsedAt, account.FieldExpiresAt, account.FieldRateLimitedAt, account.FieldRateLimitResetAt, account.FieldOverloadUntil, account.FieldSessionWindowStart, account.FieldSessionWindowEnd:
|
||||||
values[i] = new(sql.NullTime)
|
values[i] = new(sql.NullTime)
|
||||||
default:
|
default:
|
||||||
values[i] = new(sql.UnknownType)
|
values[i] = new(sql.UnknownType)
|
||||||
@@ -257,6 +261,19 @@ func (_m *Account) assignValues(columns []string, values []any) error {
|
|||||||
_m.LastUsedAt = new(time.Time)
|
_m.LastUsedAt = new(time.Time)
|
||||||
*_m.LastUsedAt = value.Time
|
*_m.LastUsedAt = value.Time
|
||||||
}
|
}
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field expires_at", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.ExpiresAt = new(time.Time)
|
||||||
|
*_m.ExpiresAt = value.Time
|
||||||
|
}
|
||||||
|
case account.FieldAutoPauseOnExpired:
|
||||||
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field auto_pause_on_expired", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.AutoPauseOnExpired = value.Bool
|
||||||
|
}
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field schedulable", values[i])
|
return fmt.Errorf("unexpected type %T for field schedulable", values[i])
|
||||||
@@ -416,6 +433,14 @@ func (_m *Account) String() string {
|
|||||||
builder.WriteString(v.Format(time.ANSIC))
|
builder.WriteString(v.Format(time.ANSIC))
|
||||||
}
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
if v := _m.ExpiresAt; v != nil {
|
||||||
|
builder.WriteString("expires_at=")
|
||||||
|
builder.WriteString(v.Format(time.ANSIC))
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("auto_pause_on_expired=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", _m.AutoPauseOnExpired))
|
||||||
|
builder.WriteString(", ")
|
||||||
builder.WriteString("schedulable=")
|
builder.WriteString("schedulable=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.Schedulable))
|
builder.WriteString(fmt.Sprintf("%v", _m.Schedulable))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ const (
|
|||||||
FieldErrorMessage = "error_message"
|
FieldErrorMessage = "error_message"
|
||||||
// FieldLastUsedAt holds the string denoting the last_used_at field in the database.
|
// FieldLastUsedAt holds the string denoting the last_used_at field in the database.
|
||||||
FieldLastUsedAt = "last_used_at"
|
FieldLastUsedAt = "last_used_at"
|
||||||
|
// FieldExpiresAt holds the string denoting the expires_at field in the database.
|
||||||
|
FieldExpiresAt = "expires_at"
|
||||||
|
// FieldAutoPauseOnExpired holds the string denoting the auto_pause_on_expired field in the database.
|
||||||
|
FieldAutoPauseOnExpired = "auto_pause_on_expired"
|
||||||
// FieldSchedulable holds the string denoting the schedulable field in the database.
|
// FieldSchedulable holds the string denoting the schedulable field in the database.
|
||||||
FieldSchedulable = "schedulable"
|
FieldSchedulable = "schedulable"
|
||||||
// FieldRateLimitedAt holds the string denoting the rate_limited_at field in the database.
|
// FieldRateLimitedAt holds the string denoting the rate_limited_at field in the database.
|
||||||
@@ -115,6 +119,8 @@ var Columns = []string{
|
|||||||
FieldStatus,
|
FieldStatus,
|
||||||
FieldErrorMessage,
|
FieldErrorMessage,
|
||||||
FieldLastUsedAt,
|
FieldLastUsedAt,
|
||||||
|
FieldExpiresAt,
|
||||||
|
FieldAutoPauseOnExpired,
|
||||||
FieldSchedulable,
|
FieldSchedulable,
|
||||||
FieldRateLimitedAt,
|
FieldRateLimitedAt,
|
||||||
FieldRateLimitResetAt,
|
FieldRateLimitResetAt,
|
||||||
@@ -172,6 +178,8 @@ var (
|
|||||||
DefaultStatus string
|
DefaultStatus string
|
||||||
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
||||||
StatusValidator func(string) error
|
StatusValidator func(string) error
|
||||||
|
// DefaultAutoPauseOnExpired holds the default value on creation for the "auto_pause_on_expired" field.
|
||||||
|
DefaultAutoPauseOnExpired bool
|
||||||
// DefaultSchedulable holds the default value on creation for the "schedulable" field.
|
// DefaultSchedulable holds the default value on creation for the "schedulable" field.
|
||||||
DefaultSchedulable bool
|
DefaultSchedulable bool
|
||||||
// SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
// SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
||||||
@@ -251,6 +259,16 @@ func ByLastUsedAt(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldLastUsedAt, opts...).ToFunc()
|
return sql.OrderByField(FieldLastUsedAt, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ByExpiresAt orders the results by the expires_at field.
|
||||||
|
func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldExpiresAt, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByAutoPauseOnExpired orders the results by the auto_pause_on_expired field.
|
||||||
|
func ByAutoPauseOnExpired(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldAutoPauseOnExpired, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
// BySchedulable orders the results by the schedulable field.
|
// BySchedulable orders the results by the schedulable field.
|
||||||
func BySchedulable(opts ...sql.OrderTermOption) OrderOption {
|
func BySchedulable(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldSchedulable, opts...).ToFunc()
|
return sql.OrderByField(FieldSchedulable, opts...).ToFunc()
|
||||||
|
|||||||
@@ -120,6 +120,16 @@ func LastUsedAt(v time.Time) predicate.Account {
|
|||||||
return predicate.Account(sql.FieldEQ(FieldLastUsedAt, v))
|
return predicate.Account(sql.FieldEQ(FieldLastUsedAt, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ.
|
||||||
|
func ExpiresAt(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoPauseOnExpired applies equality check predicate on the "auto_pause_on_expired" field. It's identical to AutoPauseOnExpiredEQ.
|
||||||
|
func AutoPauseOnExpired(v bool) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldAutoPauseOnExpired, v))
|
||||||
|
}
|
||||||
|
|
||||||
// Schedulable applies equality check predicate on the "schedulable" field. It's identical to SchedulableEQ.
|
// Schedulable applies equality check predicate on the "schedulable" field. It's identical to SchedulableEQ.
|
||||||
func Schedulable(v bool) predicate.Account {
|
func Schedulable(v bool) predicate.Account {
|
||||||
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
||||||
@@ -855,6 +865,66 @@ func LastUsedAtNotNil() predicate.Account {
|
|||||||
return predicate.Account(sql.FieldNotNull(FieldLastUsedAt))
|
return predicate.Account(sql.FieldNotNull(FieldLastUsedAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpiresAtEQ applies the EQ predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtEQ(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtNEQ(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNEQ(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtIn applies the In predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtIn(vs ...time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldIn(FieldExpiresAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtNotIn(vs ...time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNotIn(FieldExpiresAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtGT applies the GT predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtGT(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldGT(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtGTE applies the GTE predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtGTE(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldGTE(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtLT applies the LT predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtLT(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldLT(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtLTE applies the LTE predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtLTE(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldLTE(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtIsNil() predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldIsNull(FieldExpiresAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtNotNil() predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNotNull(FieldExpiresAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoPauseOnExpiredEQ applies the EQ predicate on the "auto_pause_on_expired" field.
|
||||||
|
func AutoPauseOnExpiredEQ(v bool) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldAutoPauseOnExpired, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoPauseOnExpiredNEQ applies the NEQ predicate on the "auto_pause_on_expired" field.
|
||||||
|
func AutoPauseOnExpiredNEQ(v bool) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNEQ(FieldAutoPauseOnExpired, v))
|
||||||
|
}
|
||||||
|
|
||||||
// SchedulableEQ applies the EQ predicate on the "schedulable" field.
|
// SchedulableEQ applies the EQ predicate on the "schedulable" field.
|
||||||
func SchedulableEQ(v bool) predicate.Account {
|
func SchedulableEQ(v bool) predicate.Account {
|
||||||
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
||||||
|
|||||||
@@ -195,6 +195,34 @@ func (_c *AccountCreate) SetNillableLastUsedAt(v *time.Time) *AccountCreate {
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (_c *AccountCreate) SetExpiresAt(v time.Time) *AccountCreate {
|
||||||
|
_c.mutation.SetExpiresAt(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
|
||||||
|
func (_c *AccountCreate) SetNillableExpiresAt(v *time.Time) *AccountCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetExpiresAt(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (_c *AccountCreate) SetAutoPauseOnExpired(v bool) *AccountCreate {
|
||||||
|
_c.mutation.SetAutoPauseOnExpired(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
|
||||||
|
func (_c *AccountCreate) SetNillableAutoPauseOnExpired(v *bool) *AccountCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetAutoPauseOnExpired(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (_c *AccountCreate) SetSchedulable(v bool) *AccountCreate {
|
func (_c *AccountCreate) SetSchedulable(v bool) *AccountCreate {
|
||||||
_c.mutation.SetSchedulable(v)
|
_c.mutation.SetSchedulable(v)
|
||||||
@@ -405,6 +433,10 @@ func (_c *AccountCreate) defaults() error {
|
|||||||
v := account.DefaultStatus
|
v := account.DefaultStatus
|
||||||
_c.mutation.SetStatus(v)
|
_c.mutation.SetStatus(v)
|
||||||
}
|
}
|
||||||
|
if _, ok := _c.mutation.AutoPauseOnExpired(); !ok {
|
||||||
|
v := account.DefaultAutoPauseOnExpired
|
||||||
|
_c.mutation.SetAutoPauseOnExpired(v)
|
||||||
|
}
|
||||||
if _, ok := _c.mutation.Schedulable(); !ok {
|
if _, ok := _c.mutation.Schedulable(); !ok {
|
||||||
v := account.DefaultSchedulable
|
v := account.DefaultSchedulable
|
||||||
_c.mutation.SetSchedulable(v)
|
_c.mutation.SetSchedulable(v)
|
||||||
@@ -464,6 +496,9 @@ func (_c *AccountCreate) check() error {
|
|||||||
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Account.status": %w`, err)}
|
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Account.status": %w`, err)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if _, ok := _c.mutation.AutoPauseOnExpired(); !ok {
|
||||||
|
return &ValidationError{Name: "auto_pause_on_expired", err: errors.New(`ent: missing required field "Account.auto_pause_on_expired"`)}
|
||||||
|
}
|
||||||
if _, ok := _c.mutation.Schedulable(); !ok {
|
if _, ok := _c.mutation.Schedulable(); !ok {
|
||||||
return &ValidationError{Name: "schedulable", err: errors.New(`ent: missing required field "Account.schedulable"`)}
|
return &ValidationError{Name: "schedulable", err: errors.New(`ent: missing required field "Account.schedulable"`)}
|
||||||
}
|
}
|
||||||
@@ -555,6 +590,14 @@ func (_c *AccountCreate) createSpec() (*Account, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(account.FieldLastUsedAt, field.TypeTime, value)
|
_spec.SetField(account.FieldLastUsedAt, field.TypeTime, value)
|
||||||
_node.LastUsedAt = &value
|
_node.LastUsedAt = &value
|
||||||
}
|
}
|
||||||
|
if value, ok := _c.mutation.ExpiresAt(); ok {
|
||||||
|
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
|
||||||
|
_node.ExpiresAt = &value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.AutoPauseOnExpired(); ok {
|
||||||
|
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
|
||||||
|
_node.AutoPauseOnExpired = value
|
||||||
|
}
|
||||||
if value, ok := _c.mutation.Schedulable(); ok {
|
if value, ok := _c.mutation.Schedulable(); ok {
|
||||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||||
_node.Schedulable = value
|
_node.Schedulable = value
|
||||||
@@ -898,6 +941,36 @@ func (u *AccountUpsert) ClearLastUsedAt() *AccountUpsert {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (u *AccountUpsert) SetExpiresAt(v time.Time) *AccountUpsert {
|
||||||
|
u.Set(account.FieldExpiresAt, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsert) UpdateExpiresAt() *AccountUpsert {
|
||||||
|
u.SetExcluded(account.FieldExpiresAt)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (u *AccountUpsert) ClearExpiresAt() *AccountUpsert {
|
||||||
|
u.SetNull(account.FieldExpiresAt)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (u *AccountUpsert) SetAutoPauseOnExpired(v bool) *AccountUpsert {
|
||||||
|
u.Set(account.FieldAutoPauseOnExpired, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsert) UpdateAutoPauseOnExpired() *AccountUpsert {
|
||||||
|
u.SetExcluded(account.FieldAutoPauseOnExpired)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (u *AccountUpsert) SetSchedulable(v bool) *AccountUpsert {
|
func (u *AccountUpsert) SetSchedulable(v bool) *AccountUpsert {
|
||||||
u.Set(account.FieldSchedulable, v)
|
u.Set(account.FieldSchedulable, v)
|
||||||
@@ -1308,6 +1381,41 @@ func (u *AccountUpsertOne) ClearLastUsedAt() *AccountUpsertOne {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (u *AccountUpsertOne) SetExpiresAt(v time.Time) *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetExpiresAt(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertOne) UpdateExpiresAt() *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateExpiresAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (u *AccountUpsertOne) ClearExpiresAt() *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.ClearExpiresAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (u *AccountUpsertOne) SetAutoPauseOnExpired(v bool) *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetAutoPauseOnExpired(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertOne) UpdateAutoPauseOnExpired() *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateAutoPauseOnExpired()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (u *AccountUpsertOne) SetSchedulable(v bool) *AccountUpsertOne {
|
func (u *AccountUpsertOne) SetSchedulable(v bool) *AccountUpsertOne {
|
||||||
return u.Update(func(s *AccountUpsert) {
|
return u.Update(func(s *AccountUpsert) {
|
||||||
@@ -1904,6 +2012,41 @@ func (u *AccountUpsertBulk) ClearLastUsedAt() *AccountUpsertBulk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (u *AccountUpsertBulk) SetExpiresAt(v time.Time) *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetExpiresAt(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertBulk) UpdateExpiresAt() *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateExpiresAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (u *AccountUpsertBulk) ClearExpiresAt() *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.ClearExpiresAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (u *AccountUpsertBulk) SetAutoPauseOnExpired(v bool) *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetAutoPauseOnExpired(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertBulk) UpdateAutoPauseOnExpired() *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateAutoPauseOnExpired()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (u *AccountUpsertBulk) SetSchedulable(v bool) *AccountUpsertBulk {
|
func (u *AccountUpsertBulk) SetSchedulable(v bool) *AccountUpsertBulk {
|
||||||
return u.Update(func(s *AccountUpsert) {
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
|||||||
@@ -247,6 +247,40 @@ func (_u *AccountUpdate) ClearLastUsedAt() *AccountUpdate {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (_u *AccountUpdate) SetExpiresAt(v time.Time) *AccountUpdate {
|
||||||
|
_u.mutation.SetExpiresAt(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdate) SetNillableExpiresAt(v *time.Time) *AccountUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetExpiresAt(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (_u *AccountUpdate) ClearExpiresAt() *AccountUpdate {
|
||||||
|
_u.mutation.ClearExpiresAt()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (_u *AccountUpdate) SetAutoPauseOnExpired(v bool) *AccountUpdate {
|
||||||
|
_u.mutation.SetAutoPauseOnExpired(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdate) SetNillableAutoPauseOnExpired(v *bool) *AccountUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetAutoPauseOnExpired(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (_u *AccountUpdate) SetSchedulable(v bool) *AccountUpdate {
|
func (_u *AccountUpdate) SetSchedulable(v bool) *AccountUpdate {
|
||||||
_u.mutation.SetSchedulable(v)
|
_u.mutation.SetSchedulable(v)
|
||||||
@@ -610,6 +644,15 @@ func (_u *AccountUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
|||||||
if _u.mutation.LastUsedAtCleared() {
|
if _u.mutation.LastUsedAtCleared() {
|
||||||
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
|
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.ExpiresAt(); ok {
|
||||||
|
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.ExpiresAtCleared() {
|
||||||
|
_spec.ClearField(account.FieldExpiresAt, field.TypeTime)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AutoPauseOnExpired(); ok {
|
||||||
|
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.Schedulable(); ok {
|
if value, ok := _u.mutation.Schedulable(); ok {
|
||||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||||
}
|
}
|
||||||
@@ -1016,6 +1059,40 @@ func (_u *AccountUpdateOne) ClearLastUsedAt() *AccountUpdateOne {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (_u *AccountUpdateOne) SetExpiresAt(v time.Time) *AccountUpdateOne {
|
||||||
|
_u.mutation.SetExpiresAt(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdateOne) SetNillableExpiresAt(v *time.Time) *AccountUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetExpiresAt(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (_u *AccountUpdateOne) ClearExpiresAt() *AccountUpdateOne {
|
||||||
|
_u.mutation.ClearExpiresAt()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (_u *AccountUpdateOne) SetAutoPauseOnExpired(v bool) *AccountUpdateOne {
|
||||||
|
_u.mutation.SetAutoPauseOnExpired(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdateOne) SetNillableAutoPauseOnExpired(v *bool) *AccountUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetAutoPauseOnExpired(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (_u *AccountUpdateOne) SetSchedulable(v bool) *AccountUpdateOne {
|
func (_u *AccountUpdateOne) SetSchedulable(v bool) *AccountUpdateOne {
|
||||||
_u.mutation.SetSchedulable(v)
|
_u.mutation.SetSchedulable(v)
|
||||||
@@ -1409,6 +1486,15 @@ func (_u *AccountUpdateOne) sqlSave(ctx context.Context) (_node *Account, err er
|
|||||||
if _u.mutation.LastUsedAtCleared() {
|
if _u.mutation.LastUsedAtCleared() {
|
||||||
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
|
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.ExpiresAt(); ok {
|
||||||
|
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.ExpiresAtCleared() {
|
||||||
|
_spec.ClearField(account.FieldExpiresAt, field.TypeTime)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AutoPauseOnExpired(); ok {
|
||||||
|
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.Schedulable(); ok {
|
if value, ok := _u.mutation.Schedulable(); ok {
|
||||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ var (
|
|||||||
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
||||||
{Name: "error_message", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
|
{Name: "error_message", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
|
||||||
{Name: "last_used_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{Name: "last_used_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
|
{Name: "expires_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
|
{Name: "auto_pause_on_expired", Type: field.TypeBool, Default: true},
|
||||||
{Name: "schedulable", Type: field.TypeBool, Default: true},
|
{Name: "schedulable", Type: field.TypeBool, Default: true},
|
||||||
{Name: "rate_limited_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{Name: "rate_limited_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
{Name: "rate_limit_reset_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{Name: "rate_limit_reset_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
@@ -97,7 +99,7 @@ var (
|
|||||||
ForeignKeys: []*schema.ForeignKey{
|
ForeignKeys: []*schema.ForeignKey{
|
||||||
{
|
{
|
||||||
Symbol: "accounts_proxies_proxy",
|
Symbol: "accounts_proxies_proxy",
|
||||||
Columns: []*schema.Column{AccountsColumns[22]},
|
Columns: []*schema.Column{AccountsColumns[24]},
|
||||||
RefColumns: []*schema.Column{ProxiesColumns[0]},
|
RefColumns: []*schema.Column{ProxiesColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
@@ -121,7 +123,7 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "account_proxy_id",
|
Name: "account_proxy_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[22]},
|
Columns: []*schema.Column{AccountsColumns[24]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_priority",
|
Name: "account_priority",
|
||||||
@@ -136,22 +138,22 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "account_schedulable",
|
Name: "account_schedulable",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[15]},
|
Columns: []*schema.Column{AccountsColumns[17]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_rate_limited_at",
|
Name: "account_rate_limited_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[16]},
|
Columns: []*schema.Column{AccountsColumns[18]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_rate_limit_reset_at",
|
Name: "account_rate_limit_reset_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[17]},
|
Columns: []*schema.Column{AccountsColumns[19]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_overload_until",
|
Name: "account_overload_until",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[18]},
|
Columns: []*schema.Column{AccountsColumns[20]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_deleted_at",
|
Name: "account_deleted_at",
|
||||||
@@ -371,6 +373,7 @@ var (
|
|||||||
{Name: "stream", Type: field.TypeBool, Default: false},
|
{Name: "stream", Type: field.TypeBool, Default: false},
|
||||||
{Name: "duration_ms", Type: field.TypeInt, Nullable: true},
|
{Name: "duration_ms", Type: field.TypeInt, Nullable: true},
|
||||||
{Name: "first_token_ms", Type: field.TypeInt, Nullable: true},
|
{Name: "first_token_ms", Type: field.TypeInt, Nullable: true},
|
||||||
|
{Name: "user_agent", Type: field.TypeString, Nullable: true, Size: 512},
|
||||||
{Name: "image_count", Type: field.TypeInt, Default: 0},
|
{Name: "image_count", Type: field.TypeInt, Default: 0},
|
||||||
{Name: "image_size", Type: field.TypeString, Nullable: true, Size: 10},
|
{Name: "image_size", Type: field.TypeString, Nullable: true, Size: 10},
|
||||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
@@ -388,31 +391,31 @@ var (
|
|||||||
ForeignKeys: []*schema.ForeignKey{
|
ForeignKeys: []*schema.ForeignKey{
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_api_keys_usage_logs",
|
Symbol: "usage_logs_api_keys_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[23]},
|
Columns: []*schema.Column{UsageLogsColumns[24]},
|
||||||
RefColumns: []*schema.Column{APIKeysColumns[0]},
|
RefColumns: []*schema.Column{APIKeysColumns[0]},
|
||||||
OnDelete: schema.NoAction,
|
OnDelete: schema.NoAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_accounts_usage_logs",
|
Symbol: "usage_logs_accounts_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[24]},
|
Columns: []*schema.Column{UsageLogsColumns[25]},
|
||||||
RefColumns: []*schema.Column{AccountsColumns[0]},
|
RefColumns: []*schema.Column{AccountsColumns[0]},
|
||||||
OnDelete: schema.NoAction,
|
OnDelete: schema.NoAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_groups_usage_logs",
|
Symbol: "usage_logs_groups_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[25]},
|
Columns: []*schema.Column{UsageLogsColumns[26]},
|
||||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_users_usage_logs",
|
Symbol: "usage_logs_users_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[26]},
|
Columns: []*schema.Column{UsageLogsColumns[27]},
|
||||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||||
OnDelete: schema.NoAction,
|
OnDelete: schema.NoAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_user_subscriptions_usage_logs",
|
Symbol: "usage_logs_user_subscriptions_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[27]},
|
Columns: []*schema.Column{UsageLogsColumns[28]},
|
||||||
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
|
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
@@ -421,32 +424,32 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "usagelog_user_id",
|
Name: "usagelog_user_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[26]},
|
Columns: []*schema.Column{UsageLogsColumns[27]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_api_key_id",
|
Name: "usagelog_api_key_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[23]},
|
Columns: []*schema.Column{UsageLogsColumns[24]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_account_id",
|
Name: "usagelog_account_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[24]},
|
Columns: []*schema.Column{UsageLogsColumns[25]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_group_id",
|
Name: "usagelog_group_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[25]},
|
Columns: []*schema.Column{UsageLogsColumns[26]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_subscription_id",
|
Name: "usagelog_subscription_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[27]},
|
Columns: []*schema.Column{UsageLogsColumns[28]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_created_at",
|
Name: "usagelog_created_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[22]},
|
Columns: []*schema.Column{UsageLogsColumns[23]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_model",
|
Name: "usagelog_model",
|
||||||
@@ -461,12 +464,12 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "usagelog_user_id_created_at",
|
Name: "usagelog_user_id_created_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[26], UsageLogsColumns[22]},
|
Columns: []*schema.Column{UsageLogsColumns[27], UsageLogsColumns[23]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_api_key_id_created_at",
|
Name: "usagelog_api_key_id_created_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[23], UsageLogsColumns[22]},
|
Columns: []*schema.Column{UsageLogsColumns[24], UsageLogsColumns[23]},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1006,6 +1006,8 @@ type AccountMutation struct {
|
|||||||
status *string
|
status *string
|
||||||
error_message *string
|
error_message *string
|
||||||
last_used_at *time.Time
|
last_used_at *time.Time
|
||||||
|
expires_at *time.Time
|
||||||
|
auto_pause_on_expired *bool
|
||||||
schedulable *bool
|
schedulable *bool
|
||||||
rate_limited_at *time.Time
|
rate_limited_at *time.Time
|
||||||
rate_limit_reset_at *time.Time
|
rate_limit_reset_at *time.Time
|
||||||
@@ -1770,6 +1772,91 @@ func (m *AccountMutation) ResetLastUsedAt() {
|
|||||||
delete(m.clearedFields, account.FieldLastUsedAt)
|
delete(m.clearedFields, account.FieldLastUsedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (m *AccountMutation) SetExpiresAt(t time.Time) {
|
||||||
|
m.expires_at = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAt returns the value of the "expires_at" field in the mutation.
|
||||||
|
func (m *AccountMutation) ExpiresAt() (r time.Time, exists bool) {
|
||||||
|
v := m.expires_at
|
||||||
|
if v == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return *v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// OldExpiresAt returns the old "expires_at" field's value of the Account entity.
|
||||||
|
// If the Account object wasn't provided to the builder, the object is fetched from the database.
|
||||||
|
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||||
|
func (m *AccountMutation) OldExpiresAt(ctx context.Context) (v *time.Time, err error) {
|
||||||
|
if !m.op.Is(OpUpdateOne) {
|
||||||
|
return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations")
|
||||||
|
}
|
||||||
|
if m.id == nil || m.oldValue == nil {
|
||||||
|
return v, errors.New("OldExpiresAt requires an ID field in the mutation")
|
||||||
|
}
|
||||||
|
oldValue, err := m.oldValue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err)
|
||||||
|
}
|
||||||
|
return oldValue.ExpiresAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (m *AccountMutation) ClearExpiresAt() {
|
||||||
|
m.expires_at = nil
|
||||||
|
m.clearedFields[account.FieldExpiresAt] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtCleared returns if the "expires_at" field was cleared in this mutation.
|
||||||
|
func (m *AccountMutation) ExpiresAtCleared() bool {
|
||||||
|
_, ok := m.clearedFields[account.FieldExpiresAt]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetExpiresAt resets all changes to the "expires_at" field.
|
||||||
|
func (m *AccountMutation) ResetExpiresAt() {
|
||||||
|
m.expires_at = nil
|
||||||
|
delete(m.clearedFields, account.FieldExpiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (m *AccountMutation) SetAutoPauseOnExpired(b bool) {
|
||||||
|
m.auto_pause_on_expired = &b
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoPauseOnExpired returns the value of the "auto_pause_on_expired" field in the mutation.
|
||||||
|
func (m *AccountMutation) AutoPauseOnExpired() (r bool, exists bool) {
|
||||||
|
v := m.auto_pause_on_expired
|
||||||
|
if v == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return *v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// OldAutoPauseOnExpired returns the old "auto_pause_on_expired" field's value of the Account entity.
|
||||||
|
// If the Account object wasn't provided to the builder, the object is fetched from the database.
|
||||||
|
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||||
|
func (m *AccountMutation) OldAutoPauseOnExpired(ctx context.Context) (v bool, err error) {
|
||||||
|
if !m.op.Is(OpUpdateOne) {
|
||||||
|
return v, errors.New("OldAutoPauseOnExpired is only allowed on UpdateOne operations")
|
||||||
|
}
|
||||||
|
if m.id == nil || m.oldValue == nil {
|
||||||
|
return v, errors.New("OldAutoPauseOnExpired requires an ID field in the mutation")
|
||||||
|
}
|
||||||
|
oldValue, err := m.oldValue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return v, fmt.Errorf("querying old value for OldAutoPauseOnExpired: %w", err)
|
||||||
|
}
|
||||||
|
return oldValue.AutoPauseOnExpired, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetAutoPauseOnExpired resets all changes to the "auto_pause_on_expired" field.
|
||||||
|
func (m *AccountMutation) ResetAutoPauseOnExpired() {
|
||||||
|
m.auto_pause_on_expired = nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (m *AccountMutation) SetSchedulable(b bool) {
|
func (m *AccountMutation) SetSchedulable(b bool) {
|
||||||
m.schedulable = &b
|
m.schedulable = &b
|
||||||
@@ -2269,7 +2356,7 @@ func (m *AccountMutation) Type() string {
|
|||||||
// order to get all numeric fields that were incremented/decremented, call
|
// order to get all numeric fields that were incremented/decremented, call
|
||||||
// AddedFields().
|
// AddedFields().
|
||||||
func (m *AccountMutation) Fields() []string {
|
func (m *AccountMutation) Fields() []string {
|
||||||
fields := make([]string, 0, 22)
|
fields := make([]string, 0, 24)
|
||||||
if m.created_at != nil {
|
if m.created_at != nil {
|
||||||
fields = append(fields, account.FieldCreatedAt)
|
fields = append(fields, account.FieldCreatedAt)
|
||||||
}
|
}
|
||||||
@@ -2315,6 +2402,12 @@ func (m *AccountMutation) Fields() []string {
|
|||||||
if m.last_used_at != nil {
|
if m.last_used_at != nil {
|
||||||
fields = append(fields, account.FieldLastUsedAt)
|
fields = append(fields, account.FieldLastUsedAt)
|
||||||
}
|
}
|
||||||
|
if m.expires_at != nil {
|
||||||
|
fields = append(fields, account.FieldExpiresAt)
|
||||||
|
}
|
||||||
|
if m.auto_pause_on_expired != nil {
|
||||||
|
fields = append(fields, account.FieldAutoPauseOnExpired)
|
||||||
|
}
|
||||||
if m.schedulable != nil {
|
if m.schedulable != nil {
|
||||||
fields = append(fields, account.FieldSchedulable)
|
fields = append(fields, account.FieldSchedulable)
|
||||||
}
|
}
|
||||||
@@ -2374,6 +2467,10 @@ func (m *AccountMutation) Field(name string) (ent.Value, bool) {
|
|||||||
return m.ErrorMessage()
|
return m.ErrorMessage()
|
||||||
case account.FieldLastUsedAt:
|
case account.FieldLastUsedAt:
|
||||||
return m.LastUsedAt()
|
return m.LastUsedAt()
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
return m.ExpiresAt()
|
||||||
|
case account.FieldAutoPauseOnExpired:
|
||||||
|
return m.AutoPauseOnExpired()
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
return m.Schedulable()
|
return m.Schedulable()
|
||||||
case account.FieldRateLimitedAt:
|
case account.FieldRateLimitedAt:
|
||||||
@@ -2427,6 +2524,10 @@ func (m *AccountMutation) OldField(ctx context.Context, name string) (ent.Value,
|
|||||||
return m.OldErrorMessage(ctx)
|
return m.OldErrorMessage(ctx)
|
||||||
case account.FieldLastUsedAt:
|
case account.FieldLastUsedAt:
|
||||||
return m.OldLastUsedAt(ctx)
|
return m.OldLastUsedAt(ctx)
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
return m.OldExpiresAt(ctx)
|
||||||
|
case account.FieldAutoPauseOnExpired:
|
||||||
|
return m.OldAutoPauseOnExpired(ctx)
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
return m.OldSchedulable(ctx)
|
return m.OldSchedulable(ctx)
|
||||||
case account.FieldRateLimitedAt:
|
case account.FieldRateLimitedAt:
|
||||||
@@ -2555,6 +2656,20 @@ func (m *AccountMutation) SetField(name string, value ent.Value) error {
|
|||||||
}
|
}
|
||||||
m.SetLastUsedAt(v)
|
m.SetLastUsedAt(v)
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
v, ok := value.(time.Time)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||||
|
}
|
||||||
|
m.SetExpiresAt(v)
|
||||||
|
return nil
|
||||||
|
case account.FieldAutoPauseOnExpired:
|
||||||
|
v, ok := value.(bool)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||||
|
}
|
||||||
|
m.SetAutoPauseOnExpired(v)
|
||||||
|
return nil
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
v, ok := value.(bool)
|
v, ok := value.(bool)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -2676,6 +2791,9 @@ func (m *AccountMutation) ClearedFields() []string {
|
|||||||
if m.FieldCleared(account.FieldLastUsedAt) {
|
if m.FieldCleared(account.FieldLastUsedAt) {
|
||||||
fields = append(fields, account.FieldLastUsedAt)
|
fields = append(fields, account.FieldLastUsedAt)
|
||||||
}
|
}
|
||||||
|
if m.FieldCleared(account.FieldExpiresAt) {
|
||||||
|
fields = append(fields, account.FieldExpiresAt)
|
||||||
|
}
|
||||||
if m.FieldCleared(account.FieldRateLimitedAt) {
|
if m.FieldCleared(account.FieldRateLimitedAt) {
|
||||||
fields = append(fields, account.FieldRateLimitedAt)
|
fields = append(fields, account.FieldRateLimitedAt)
|
||||||
}
|
}
|
||||||
@@ -2723,6 +2841,9 @@ func (m *AccountMutation) ClearField(name string) error {
|
|||||||
case account.FieldLastUsedAt:
|
case account.FieldLastUsedAt:
|
||||||
m.ClearLastUsedAt()
|
m.ClearLastUsedAt()
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
m.ClearExpiresAt()
|
||||||
|
return nil
|
||||||
case account.FieldRateLimitedAt:
|
case account.FieldRateLimitedAt:
|
||||||
m.ClearRateLimitedAt()
|
m.ClearRateLimitedAt()
|
||||||
return nil
|
return nil
|
||||||
@@ -2794,6 +2915,12 @@ func (m *AccountMutation) ResetField(name string) error {
|
|||||||
case account.FieldLastUsedAt:
|
case account.FieldLastUsedAt:
|
||||||
m.ResetLastUsedAt()
|
m.ResetLastUsedAt()
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
m.ResetExpiresAt()
|
||||||
|
return nil
|
||||||
|
case account.FieldAutoPauseOnExpired:
|
||||||
|
m.ResetAutoPauseOnExpired()
|
||||||
|
return nil
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
m.ResetSchedulable()
|
m.ResetSchedulable()
|
||||||
return nil
|
return nil
|
||||||
@@ -8107,6 +8234,7 @@ type UsageLogMutation struct {
|
|||||||
addduration_ms *int
|
addduration_ms *int
|
||||||
first_token_ms *int
|
first_token_ms *int
|
||||||
addfirst_token_ms *int
|
addfirst_token_ms *int
|
||||||
|
user_agent *string
|
||||||
image_count *int
|
image_count *int
|
||||||
addimage_count *int
|
addimage_count *int
|
||||||
image_size *string
|
image_size *string
|
||||||
@@ -9463,6 +9591,55 @@ func (m *UsageLogMutation) ResetFirstTokenMs() {
|
|||||||
delete(m.clearedFields, usagelog.FieldFirstTokenMs)
|
delete(m.clearedFields, usagelog.FieldFirstTokenMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserAgent sets the "user_agent" field.
|
||||||
|
func (m *UsageLogMutation) SetUserAgent(s string) {
|
||||||
|
m.user_agent = &s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgent returns the value of the "user_agent" field in the mutation.
|
||||||
|
func (m *UsageLogMutation) UserAgent() (r string, exists bool) {
|
||||||
|
v := m.user_agent
|
||||||
|
if v == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return *v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// OldUserAgent returns the old "user_agent" field's value of the UsageLog entity.
|
||||||
|
// If the UsageLog object wasn't provided to the builder, the object is fetched from the database.
|
||||||
|
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||||
|
func (m *UsageLogMutation) OldUserAgent(ctx context.Context) (v *string, err error) {
|
||||||
|
if !m.op.Is(OpUpdateOne) {
|
||||||
|
return v, errors.New("OldUserAgent is only allowed on UpdateOne operations")
|
||||||
|
}
|
||||||
|
if m.id == nil || m.oldValue == nil {
|
||||||
|
return v, errors.New("OldUserAgent requires an ID field in the mutation")
|
||||||
|
}
|
||||||
|
oldValue, err := m.oldValue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return v, fmt.Errorf("querying old value for OldUserAgent: %w", err)
|
||||||
|
}
|
||||||
|
return oldValue.UserAgent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearUserAgent clears the value of the "user_agent" field.
|
||||||
|
func (m *UsageLogMutation) ClearUserAgent() {
|
||||||
|
m.user_agent = nil
|
||||||
|
m.clearedFields[usagelog.FieldUserAgent] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentCleared returns if the "user_agent" field was cleared in this mutation.
|
||||||
|
func (m *UsageLogMutation) UserAgentCleared() bool {
|
||||||
|
_, ok := m.clearedFields[usagelog.FieldUserAgent]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserAgent resets all changes to the "user_agent" field.
|
||||||
|
func (m *UsageLogMutation) ResetUserAgent() {
|
||||||
|
m.user_agent = nil
|
||||||
|
delete(m.clearedFields, usagelog.FieldUserAgent)
|
||||||
|
}
|
||||||
|
|
||||||
// SetImageCount sets the "image_count" field.
|
// SetImageCount sets the "image_count" field.
|
||||||
func (m *UsageLogMutation) SetImageCount(i int) {
|
func (m *UsageLogMutation) SetImageCount(i int) {
|
||||||
m.image_count = &i
|
m.image_count = &i
|
||||||
@@ -9773,7 +9950,7 @@ func (m *UsageLogMutation) Type() string {
|
|||||||
// order to get all numeric fields that were incremented/decremented, call
|
// order to get all numeric fields that were incremented/decremented, call
|
||||||
// AddedFields().
|
// AddedFields().
|
||||||
func (m *UsageLogMutation) Fields() []string {
|
func (m *UsageLogMutation) Fields() []string {
|
||||||
fields := make([]string, 0, 27)
|
fields := make([]string, 0, 28)
|
||||||
if m.user != nil {
|
if m.user != nil {
|
||||||
fields = append(fields, usagelog.FieldUserID)
|
fields = append(fields, usagelog.FieldUserID)
|
||||||
}
|
}
|
||||||
@@ -9846,6 +10023,9 @@ func (m *UsageLogMutation) Fields() []string {
|
|||||||
if m.first_token_ms != nil {
|
if m.first_token_ms != nil {
|
||||||
fields = append(fields, usagelog.FieldFirstTokenMs)
|
fields = append(fields, usagelog.FieldFirstTokenMs)
|
||||||
}
|
}
|
||||||
|
if m.user_agent != nil {
|
||||||
|
fields = append(fields, usagelog.FieldUserAgent)
|
||||||
|
}
|
||||||
if m.image_count != nil {
|
if m.image_count != nil {
|
||||||
fields = append(fields, usagelog.FieldImageCount)
|
fields = append(fields, usagelog.FieldImageCount)
|
||||||
}
|
}
|
||||||
@@ -9911,6 +10091,8 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) {
|
|||||||
return m.DurationMs()
|
return m.DurationMs()
|
||||||
case usagelog.FieldFirstTokenMs:
|
case usagelog.FieldFirstTokenMs:
|
||||||
return m.FirstTokenMs()
|
return m.FirstTokenMs()
|
||||||
|
case usagelog.FieldUserAgent:
|
||||||
|
return m.UserAgent()
|
||||||
case usagelog.FieldImageCount:
|
case usagelog.FieldImageCount:
|
||||||
return m.ImageCount()
|
return m.ImageCount()
|
||||||
case usagelog.FieldImageSize:
|
case usagelog.FieldImageSize:
|
||||||
@@ -9974,6 +10156,8 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value
|
|||||||
return m.OldDurationMs(ctx)
|
return m.OldDurationMs(ctx)
|
||||||
case usagelog.FieldFirstTokenMs:
|
case usagelog.FieldFirstTokenMs:
|
||||||
return m.OldFirstTokenMs(ctx)
|
return m.OldFirstTokenMs(ctx)
|
||||||
|
case usagelog.FieldUserAgent:
|
||||||
|
return m.OldUserAgent(ctx)
|
||||||
case usagelog.FieldImageCount:
|
case usagelog.FieldImageCount:
|
||||||
return m.OldImageCount(ctx)
|
return m.OldImageCount(ctx)
|
||||||
case usagelog.FieldImageSize:
|
case usagelog.FieldImageSize:
|
||||||
@@ -10157,6 +10341,13 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error {
|
|||||||
}
|
}
|
||||||
m.SetFirstTokenMs(v)
|
m.SetFirstTokenMs(v)
|
||||||
return nil
|
return nil
|
||||||
|
case usagelog.FieldUserAgent:
|
||||||
|
v, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||||
|
}
|
||||||
|
m.SetUserAgent(v)
|
||||||
|
return nil
|
||||||
case usagelog.FieldImageCount:
|
case usagelog.FieldImageCount:
|
||||||
v, ok := value.(int)
|
v, ok := value.(int)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -10427,6 +10618,9 @@ func (m *UsageLogMutation) ClearedFields() []string {
|
|||||||
if m.FieldCleared(usagelog.FieldFirstTokenMs) {
|
if m.FieldCleared(usagelog.FieldFirstTokenMs) {
|
||||||
fields = append(fields, usagelog.FieldFirstTokenMs)
|
fields = append(fields, usagelog.FieldFirstTokenMs)
|
||||||
}
|
}
|
||||||
|
if m.FieldCleared(usagelog.FieldUserAgent) {
|
||||||
|
fields = append(fields, usagelog.FieldUserAgent)
|
||||||
|
}
|
||||||
if m.FieldCleared(usagelog.FieldImageSize) {
|
if m.FieldCleared(usagelog.FieldImageSize) {
|
||||||
fields = append(fields, usagelog.FieldImageSize)
|
fields = append(fields, usagelog.FieldImageSize)
|
||||||
}
|
}
|
||||||
@@ -10456,6 +10650,9 @@ func (m *UsageLogMutation) ClearField(name string) error {
|
|||||||
case usagelog.FieldFirstTokenMs:
|
case usagelog.FieldFirstTokenMs:
|
||||||
m.ClearFirstTokenMs()
|
m.ClearFirstTokenMs()
|
||||||
return nil
|
return nil
|
||||||
|
case usagelog.FieldUserAgent:
|
||||||
|
m.ClearUserAgent()
|
||||||
|
return nil
|
||||||
case usagelog.FieldImageSize:
|
case usagelog.FieldImageSize:
|
||||||
m.ClearImageSize()
|
m.ClearImageSize()
|
||||||
return nil
|
return nil
|
||||||
@@ -10539,6 +10736,9 @@ func (m *UsageLogMutation) ResetField(name string) error {
|
|||||||
case usagelog.FieldFirstTokenMs:
|
case usagelog.FieldFirstTokenMs:
|
||||||
m.ResetFirstTokenMs()
|
m.ResetFirstTokenMs()
|
||||||
return nil
|
return nil
|
||||||
|
case usagelog.FieldUserAgent:
|
||||||
|
m.ResetUserAgent()
|
||||||
|
return nil
|
||||||
case usagelog.FieldImageCount:
|
case usagelog.FieldImageCount:
|
||||||
m.ResetImageCount()
|
m.ResetImageCount()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -181,12 +181,16 @@ func init() {
|
|||||||
account.DefaultStatus = accountDescStatus.Default.(string)
|
account.DefaultStatus = accountDescStatus.Default.(string)
|
||||||
// account.StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
// account.StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
||||||
account.StatusValidator = accountDescStatus.Validators[0].(func(string) error)
|
account.StatusValidator = accountDescStatus.Validators[0].(func(string) error)
|
||||||
|
// accountDescAutoPauseOnExpired is the schema descriptor for auto_pause_on_expired field.
|
||||||
|
accountDescAutoPauseOnExpired := accountFields[13].Descriptor()
|
||||||
|
// account.DefaultAutoPauseOnExpired holds the default value on creation for the auto_pause_on_expired field.
|
||||||
|
account.DefaultAutoPauseOnExpired = accountDescAutoPauseOnExpired.Default.(bool)
|
||||||
// accountDescSchedulable is the schema descriptor for schedulable field.
|
// accountDescSchedulable is the schema descriptor for schedulable field.
|
||||||
accountDescSchedulable := accountFields[12].Descriptor()
|
accountDescSchedulable := accountFields[14].Descriptor()
|
||||||
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
|
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
|
||||||
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
|
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
|
||||||
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
|
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
|
||||||
accountDescSessionWindowStatus := accountFields[18].Descriptor()
|
accountDescSessionWindowStatus := accountFields[20].Descriptor()
|
||||||
// account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
// account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
||||||
account.SessionWindowStatusValidator = accountDescSessionWindowStatus.Validators[0].(func(string) error)
|
account.SessionWindowStatusValidator = accountDescSessionWindowStatus.Validators[0].(func(string) error)
|
||||||
accountgroupFields := schema.AccountGroup{}.Fields()
|
accountgroupFields := schema.AccountGroup{}.Fields()
|
||||||
@@ -521,16 +525,20 @@ func init() {
|
|||||||
usagelogDescStream := usagelogFields[21].Descriptor()
|
usagelogDescStream := usagelogFields[21].Descriptor()
|
||||||
// usagelog.DefaultStream holds the default value on creation for the stream field.
|
// usagelog.DefaultStream holds the default value on creation for the stream field.
|
||||||
usagelog.DefaultStream = usagelogDescStream.Default.(bool)
|
usagelog.DefaultStream = usagelogDescStream.Default.(bool)
|
||||||
|
// usagelogDescUserAgent is the schema descriptor for user_agent field.
|
||||||
|
usagelogDescUserAgent := usagelogFields[24].Descriptor()
|
||||||
|
// usagelog.UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save.
|
||||||
|
usagelog.UserAgentValidator = usagelogDescUserAgent.Validators[0].(func(string) error)
|
||||||
// usagelogDescImageCount is the schema descriptor for image_count field.
|
// usagelogDescImageCount is the schema descriptor for image_count field.
|
||||||
usagelogDescImageCount := usagelogFields[24].Descriptor()
|
usagelogDescImageCount := usagelogFields[25].Descriptor()
|
||||||
// usagelog.DefaultImageCount holds the default value on creation for the image_count field.
|
// usagelog.DefaultImageCount holds the default value on creation for the image_count field.
|
||||||
usagelog.DefaultImageCount = usagelogDescImageCount.Default.(int)
|
usagelog.DefaultImageCount = usagelogDescImageCount.Default.(int)
|
||||||
// usagelogDescImageSize is the schema descriptor for image_size field.
|
// usagelogDescImageSize is the schema descriptor for image_size field.
|
||||||
usagelogDescImageSize := usagelogFields[25].Descriptor()
|
usagelogDescImageSize := usagelogFields[26].Descriptor()
|
||||||
// usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
// usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
||||||
usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error)
|
usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error)
|
||||||
// usagelogDescCreatedAt is the schema descriptor for created_at field.
|
// usagelogDescCreatedAt is the schema descriptor for created_at field.
|
||||||
usagelogDescCreatedAt := usagelogFields[26].Descriptor()
|
usagelogDescCreatedAt := usagelogFields[27].Descriptor()
|
||||||
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
|
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||||
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
|
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
|
||||||
userMixin := schema.User{}.Mixin()
|
userMixin := schema.User{}.Mixin()
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ func (Account) Fields() []ent.Field {
|
|||||||
Optional().
|
Optional().
|
||||||
Nillable().
|
Nillable().
|
||||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||||
|
// expires_at: 账户过期时间(可为空)
|
||||||
|
field.Time("expires_at").
|
||||||
|
Optional().
|
||||||
|
Nillable().
|
||||||
|
Comment("Account expiration time (NULL means no expiration).").
|
||||||
|
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||||
|
// auto_pause_on_expired: 过期后自动暂停调度
|
||||||
|
field.Bool("auto_pause_on_expired").
|
||||||
|
Default(true).
|
||||||
|
Comment("Auto pause scheduling when account expires."),
|
||||||
|
|
||||||
// ========== 调度和速率限制相关字段 ==========
|
// ========== 调度和速率限制相关字段 ==========
|
||||||
// 这些字段在 migrations/005_schema_parity.sql 中添加
|
// 这些字段在 migrations/005_schema_parity.sql 中添加
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ func (UsageLog) Fields() []ent.Field {
|
|||||||
field.Int("first_token_ms").
|
field.Int("first_token_ms").
|
||||||
Optional().
|
Optional().
|
||||||
Nillable(),
|
Nillable(),
|
||||||
|
field.String("user_agent").
|
||||||
|
MaxLen(512).
|
||||||
|
Optional().
|
||||||
|
Nillable(),
|
||||||
|
|
||||||
// 图片生成字段(仅 gemini-3-pro-image 等图片模型使用)
|
// 图片生成字段(仅 gemini-3-pro-image 等图片模型使用)
|
||||||
field.Int("image_count").
|
field.Int("image_count").
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ type UsageLog struct {
|
|||||||
DurationMs *int `json:"duration_ms,omitempty"`
|
DurationMs *int `json:"duration_ms,omitempty"`
|
||||||
// FirstTokenMs holds the value of the "first_token_ms" field.
|
// FirstTokenMs holds the value of the "first_token_ms" field.
|
||||||
FirstTokenMs *int `json:"first_token_ms,omitempty"`
|
FirstTokenMs *int `json:"first_token_ms,omitempty"`
|
||||||
|
// UserAgent holds the value of the "user_agent" field.
|
||||||
|
UserAgent *string `json:"user_agent,omitempty"`
|
||||||
// ImageCount holds the value of the "image_count" field.
|
// ImageCount holds the value of the "image_count" field.
|
||||||
ImageCount int `json:"image_count,omitempty"`
|
ImageCount int `json:"image_count,omitempty"`
|
||||||
// ImageSize holds the value of the "image_size" field.
|
// ImageSize holds the value of the "image_size" field.
|
||||||
@@ -165,7 +167,7 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
|
|||||||
values[i] = new(sql.NullFloat64)
|
values[i] = new(sql.NullFloat64)
|
||||||
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount:
|
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount:
|
||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldImageSize:
|
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldUserAgent, usagelog.FieldImageSize:
|
||||||
values[i] = new(sql.NullString)
|
values[i] = new(sql.NullString)
|
||||||
case usagelog.FieldCreatedAt:
|
case usagelog.FieldCreatedAt:
|
||||||
values[i] = new(sql.NullTime)
|
values[i] = new(sql.NullTime)
|
||||||
@@ -338,6 +340,13 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
|
|||||||
_m.FirstTokenMs = new(int)
|
_m.FirstTokenMs = new(int)
|
||||||
*_m.FirstTokenMs = int(value.Int64)
|
*_m.FirstTokenMs = int(value.Int64)
|
||||||
}
|
}
|
||||||
|
case usagelog.FieldUserAgent:
|
||||||
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field user_agent", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.UserAgent = new(string)
|
||||||
|
*_m.UserAgent = value.String
|
||||||
|
}
|
||||||
case usagelog.FieldImageCount:
|
case usagelog.FieldImageCount:
|
||||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field image_count", values[i])
|
return fmt.Errorf("unexpected type %T for field image_count", values[i])
|
||||||
@@ -498,6 +507,11 @@ func (_m *UsageLog) String() string {
|
|||||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||||
}
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
if v := _m.UserAgent; v != nil {
|
||||||
|
builder.WriteString("user_agent=")
|
||||||
|
builder.WriteString(*v)
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
builder.WriteString("image_count=")
|
builder.WriteString("image_count=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.ImageCount))
|
builder.WriteString(fmt.Sprintf("%v", _m.ImageCount))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ const (
|
|||||||
FieldDurationMs = "duration_ms"
|
FieldDurationMs = "duration_ms"
|
||||||
// FieldFirstTokenMs holds the string denoting the first_token_ms field in the database.
|
// FieldFirstTokenMs holds the string denoting the first_token_ms field in the database.
|
||||||
FieldFirstTokenMs = "first_token_ms"
|
FieldFirstTokenMs = "first_token_ms"
|
||||||
|
// FieldUserAgent holds the string denoting the user_agent field in the database.
|
||||||
|
FieldUserAgent = "user_agent"
|
||||||
// FieldImageCount holds the string denoting the image_count field in the database.
|
// FieldImageCount holds the string denoting the image_count field in the database.
|
||||||
FieldImageCount = "image_count"
|
FieldImageCount = "image_count"
|
||||||
// FieldImageSize holds the string denoting the image_size field in the database.
|
// FieldImageSize holds the string denoting the image_size field in the database.
|
||||||
@@ -144,6 +146,7 @@ var Columns = []string{
|
|||||||
FieldStream,
|
FieldStream,
|
||||||
FieldDurationMs,
|
FieldDurationMs,
|
||||||
FieldFirstTokenMs,
|
FieldFirstTokenMs,
|
||||||
|
FieldUserAgent,
|
||||||
FieldImageCount,
|
FieldImageCount,
|
||||||
FieldImageSize,
|
FieldImageSize,
|
||||||
FieldCreatedAt,
|
FieldCreatedAt,
|
||||||
@@ -194,6 +197,8 @@ var (
|
|||||||
DefaultBillingType int8
|
DefaultBillingType int8
|
||||||
// DefaultStream holds the default value on creation for the "stream" field.
|
// DefaultStream holds the default value on creation for the "stream" field.
|
||||||
DefaultStream bool
|
DefaultStream bool
|
||||||
|
// UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save.
|
||||||
|
UserAgentValidator func(string) error
|
||||||
// DefaultImageCount holds the default value on creation for the "image_count" field.
|
// DefaultImageCount holds the default value on creation for the "image_count" field.
|
||||||
DefaultImageCount int
|
DefaultImageCount int
|
||||||
// ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
// ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
||||||
@@ -330,6 +335,11 @@ func ByFirstTokenMs(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldFirstTokenMs, opts...).ToFunc()
|
return sql.OrderByField(FieldFirstTokenMs, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ByUserAgent orders the results by the user_agent field.
|
||||||
|
func ByUserAgent(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldUserAgent, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
// ByImageCount orders the results by the image_count field.
|
// ByImageCount orders the results by the image_count field.
|
||||||
func ByImageCount(opts ...sql.OrderTermOption) OrderOption {
|
func ByImageCount(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldImageCount, opts...).ToFunc()
|
return sql.OrderByField(FieldImageCount, opts...).ToFunc()
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ func FirstTokenMs(v int) predicate.UsageLog {
|
|||||||
return predicate.UsageLog(sql.FieldEQ(FieldFirstTokenMs, v))
|
return predicate.UsageLog(sql.FieldEQ(FieldFirstTokenMs, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserAgent applies equality check predicate on the "user_agent" field. It's identical to UserAgentEQ.
|
||||||
|
func UserAgent(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldEQ(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
// ImageCount applies equality check predicate on the "image_count" field. It's identical to ImageCountEQ.
|
// ImageCount applies equality check predicate on the "image_count" field. It's identical to ImageCountEQ.
|
||||||
func ImageCount(v int) predicate.UsageLog {
|
func ImageCount(v int) predicate.UsageLog {
|
||||||
return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v))
|
return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v))
|
||||||
@@ -1110,6 +1115,81 @@ func FirstTokenMsNotNil() predicate.UsageLog {
|
|||||||
return predicate.UsageLog(sql.FieldNotNull(FieldFirstTokenMs))
|
return predicate.UsageLog(sql.FieldNotNull(FieldFirstTokenMs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserAgentEQ applies the EQ predicate on the "user_agent" field.
|
||||||
|
func UserAgentEQ(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldEQ(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentNEQ applies the NEQ predicate on the "user_agent" field.
|
||||||
|
func UserAgentNEQ(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldNEQ(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentIn applies the In predicate on the "user_agent" field.
|
||||||
|
func UserAgentIn(vs ...string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldIn(FieldUserAgent, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentNotIn applies the NotIn predicate on the "user_agent" field.
|
||||||
|
func UserAgentNotIn(vs ...string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldNotIn(FieldUserAgent, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentGT applies the GT predicate on the "user_agent" field.
|
||||||
|
func UserAgentGT(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldGT(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentGTE applies the GTE predicate on the "user_agent" field.
|
||||||
|
func UserAgentGTE(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldGTE(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentLT applies the LT predicate on the "user_agent" field.
|
||||||
|
func UserAgentLT(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldLT(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentLTE applies the LTE predicate on the "user_agent" field.
|
||||||
|
func UserAgentLTE(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldLTE(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentContains applies the Contains predicate on the "user_agent" field.
|
||||||
|
func UserAgentContains(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldContains(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentHasPrefix applies the HasPrefix predicate on the "user_agent" field.
|
||||||
|
func UserAgentHasPrefix(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldHasPrefix(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentHasSuffix applies the HasSuffix predicate on the "user_agent" field.
|
||||||
|
func UserAgentHasSuffix(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldHasSuffix(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentIsNil applies the IsNil predicate on the "user_agent" field.
|
||||||
|
func UserAgentIsNil() predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldIsNull(FieldUserAgent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentNotNil applies the NotNil predicate on the "user_agent" field.
|
||||||
|
func UserAgentNotNil() predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldNotNull(FieldUserAgent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentEqualFold applies the EqualFold predicate on the "user_agent" field.
|
||||||
|
func UserAgentEqualFold(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldEqualFold(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAgentContainsFold applies the ContainsFold predicate on the "user_agent" field.
|
||||||
|
func UserAgentContainsFold(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldContainsFold(FieldUserAgent, v))
|
||||||
|
}
|
||||||
|
|
||||||
// ImageCountEQ applies the EQ predicate on the "image_count" field.
|
// ImageCountEQ applies the EQ predicate on the "image_count" field.
|
||||||
func ImageCountEQ(v int) predicate.UsageLog {
|
func ImageCountEQ(v int) predicate.UsageLog {
|
||||||
return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v))
|
return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v))
|
||||||
|
|||||||
@@ -323,6 +323,20 @@ func (_c *UsageLogCreate) SetNillableFirstTokenMs(v *int) *UsageLogCreate {
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserAgent sets the "user_agent" field.
|
||||||
|
func (_c *UsageLogCreate) SetUserAgent(v string) *UsageLogCreate {
|
||||||
|
_c.mutation.SetUserAgent(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
|
||||||
|
func (_c *UsageLogCreate) SetNillableUserAgent(v *string) *UsageLogCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetUserAgent(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetImageCount sets the "image_count" field.
|
// SetImageCount sets the "image_count" field.
|
||||||
func (_c *UsageLogCreate) SetImageCount(v int) *UsageLogCreate {
|
func (_c *UsageLogCreate) SetImageCount(v int) *UsageLogCreate {
|
||||||
_c.mutation.SetImageCount(v)
|
_c.mutation.SetImageCount(v)
|
||||||
@@ -567,6 +581,11 @@ func (_c *UsageLogCreate) check() error {
|
|||||||
if _, ok := _c.mutation.Stream(); !ok {
|
if _, ok := _c.mutation.Stream(); !ok {
|
||||||
return &ValidationError{Name: "stream", err: errors.New(`ent: missing required field "UsageLog.stream"`)}
|
return &ValidationError{Name: "stream", err: errors.New(`ent: missing required field "UsageLog.stream"`)}
|
||||||
}
|
}
|
||||||
|
if v, ok := _c.mutation.UserAgent(); ok {
|
||||||
|
if err := usagelog.UserAgentValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
if _, ok := _c.mutation.ImageCount(); !ok {
|
if _, ok := _c.mutation.ImageCount(); !ok {
|
||||||
return &ValidationError{Name: "image_count", err: errors.New(`ent: missing required field "UsageLog.image_count"`)}
|
return &ValidationError{Name: "image_count", err: errors.New(`ent: missing required field "UsageLog.image_count"`)}
|
||||||
}
|
}
|
||||||
@@ -690,6 +709,10 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(usagelog.FieldFirstTokenMs, field.TypeInt, value)
|
_spec.SetField(usagelog.FieldFirstTokenMs, field.TypeInt, value)
|
||||||
_node.FirstTokenMs = &value
|
_node.FirstTokenMs = &value
|
||||||
}
|
}
|
||||||
|
if value, ok := _c.mutation.UserAgent(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldUserAgent, field.TypeString, value)
|
||||||
|
_node.UserAgent = &value
|
||||||
|
}
|
||||||
if value, ok := _c.mutation.ImageCount(); ok {
|
if value, ok := _c.mutation.ImageCount(); ok {
|
||||||
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
||||||
_node.ImageCount = value
|
_node.ImageCount = value
|
||||||
@@ -1247,6 +1270,24 @@ func (u *UsageLogUpsert) ClearFirstTokenMs() *UsageLogUpsert {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserAgent sets the "user_agent" field.
|
||||||
|
func (u *UsageLogUpsert) SetUserAgent(v string) *UsageLogUpsert {
|
||||||
|
u.Set(usagelog.FieldUserAgent, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsert) UpdateUserAgent() *UsageLogUpsert {
|
||||||
|
u.SetExcluded(usagelog.FieldUserAgent)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearUserAgent clears the value of the "user_agent" field.
|
||||||
|
func (u *UsageLogUpsert) ClearUserAgent() *UsageLogUpsert {
|
||||||
|
u.SetNull(usagelog.FieldUserAgent)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
// SetImageCount sets the "image_count" field.
|
// SetImageCount sets the "image_count" field.
|
||||||
func (u *UsageLogUpsert) SetImageCount(v int) *UsageLogUpsert {
|
func (u *UsageLogUpsert) SetImageCount(v int) *UsageLogUpsert {
|
||||||
u.Set(usagelog.FieldImageCount, v)
|
u.Set(usagelog.FieldImageCount, v)
|
||||||
@@ -1804,6 +1845,27 @@ func (u *UsageLogUpsertOne) ClearFirstTokenMs() *UsageLogUpsertOne {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserAgent sets the "user_agent" field.
|
||||||
|
func (u *UsageLogUpsertOne) SetUserAgent(v string) *UsageLogUpsertOne {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.SetUserAgent(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsertOne) UpdateUserAgent() *UsageLogUpsertOne {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.UpdateUserAgent()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearUserAgent clears the value of the "user_agent" field.
|
||||||
|
func (u *UsageLogUpsertOne) ClearUserAgent() *UsageLogUpsertOne {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.ClearUserAgent()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetImageCount sets the "image_count" field.
|
// SetImageCount sets the "image_count" field.
|
||||||
func (u *UsageLogUpsertOne) SetImageCount(v int) *UsageLogUpsertOne {
|
func (u *UsageLogUpsertOne) SetImageCount(v int) *UsageLogUpsertOne {
|
||||||
return u.Update(func(s *UsageLogUpsert) {
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
@@ -2533,6 +2595,27 @@ func (u *UsageLogUpsertBulk) ClearFirstTokenMs() *UsageLogUpsertBulk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserAgent sets the "user_agent" field.
|
||||||
|
func (u *UsageLogUpsertBulk) SetUserAgent(v string) *UsageLogUpsertBulk {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.SetUserAgent(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsertBulk) UpdateUserAgent() *UsageLogUpsertBulk {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.UpdateUserAgent()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearUserAgent clears the value of the "user_agent" field.
|
||||||
|
func (u *UsageLogUpsertBulk) ClearUserAgent() *UsageLogUpsertBulk {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.ClearUserAgent()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetImageCount sets the "image_count" field.
|
// SetImageCount sets the "image_count" field.
|
||||||
func (u *UsageLogUpsertBulk) SetImageCount(v int) *UsageLogUpsertBulk {
|
func (u *UsageLogUpsertBulk) SetImageCount(v int) *UsageLogUpsertBulk {
|
||||||
return u.Update(func(s *UsageLogUpsert) {
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
|||||||
@@ -504,6 +504,26 @@ func (_u *UsageLogUpdate) ClearFirstTokenMs() *UsageLogUpdate {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserAgent sets the "user_agent" field.
|
||||||
|
func (_u *UsageLogUpdate) SetUserAgent(v string) *UsageLogUpdate {
|
||||||
|
_u.mutation.SetUserAgent(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
|
||||||
|
func (_u *UsageLogUpdate) SetNillableUserAgent(v *string) *UsageLogUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetUserAgent(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearUserAgent clears the value of the "user_agent" field.
|
||||||
|
func (_u *UsageLogUpdate) ClearUserAgent() *UsageLogUpdate {
|
||||||
|
_u.mutation.ClearUserAgent()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetImageCount sets the "image_count" field.
|
// SetImageCount sets the "image_count" field.
|
||||||
func (_u *UsageLogUpdate) SetImageCount(v int) *UsageLogUpdate {
|
func (_u *UsageLogUpdate) SetImageCount(v int) *UsageLogUpdate {
|
||||||
_u.mutation.ResetImageCount()
|
_u.mutation.ResetImageCount()
|
||||||
@@ -644,6 +664,11 @@ func (_u *UsageLogUpdate) check() error {
|
|||||||
return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)}
|
return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v, ok := _u.mutation.UserAgent(); ok {
|
||||||
|
if err := usagelog.UserAgentValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
if v, ok := _u.mutation.ImageSize(); ok {
|
if v, ok := _u.mutation.ImageSize(); ok {
|
||||||
if err := usagelog.ImageSizeValidator(v); err != nil {
|
if err := usagelog.ImageSizeValidator(v); err != nil {
|
||||||
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
||||||
@@ -784,6 +809,12 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
|||||||
if _u.mutation.FirstTokenMsCleared() {
|
if _u.mutation.FirstTokenMsCleared() {
|
||||||
_spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt)
|
_spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.UserAgent(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldUserAgent, field.TypeString, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.UserAgentCleared() {
|
||||||
|
_spec.ClearField(usagelog.FieldUserAgent, field.TypeString)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.ImageCount(); ok {
|
if value, ok := _u.mutation.ImageCount(); ok {
|
||||||
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
||||||
}
|
}
|
||||||
@@ -1433,6 +1464,26 @@ func (_u *UsageLogUpdateOne) ClearFirstTokenMs() *UsageLogUpdateOne {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserAgent sets the "user_agent" field.
|
||||||
|
func (_u *UsageLogUpdateOne) SetUserAgent(v string) *UsageLogUpdateOne {
|
||||||
|
_u.mutation.SetUserAgent(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
|
||||||
|
func (_u *UsageLogUpdateOne) SetNillableUserAgent(v *string) *UsageLogUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetUserAgent(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearUserAgent clears the value of the "user_agent" field.
|
||||||
|
func (_u *UsageLogUpdateOne) ClearUserAgent() *UsageLogUpdateOne {
|
||||||
|
_u.mutation.ClearUserAgent()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetImageCount sets the "image_count" field.
|
// SetImageCount sets the "image_count" field.
|
||||||
func (_u *UsageLogUpdateOne) SetImageCount(v int) *UsageLogUpdateOne {
|
func (_u *UsageLogUpdateOne) SetImageCount(v int) *UsageLogUpdateOne {
|
||||||
_u.mutation.ResetImageCount()
|
_u.mutation.ResetImageCount()
|
||||||
@@ -1586,6 +1637,11 @@ func (_u *UsageLogUpdateOne) check() error {
|
|||||||
return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)}
|
return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v, ok := _u.mutation.UserAgent(); ok {
|
||||||
|
if err := usagelog.UserAgentValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
if v, ok := _u.mutation.ImageSize(); ok {
|
if v, ok := _u.mutation.ImageSize(); ok {
|
||||||
if err := usagelog.ImageSizeValidator(v); err != nil {
|
if err := usagelog.ImageSizeValidator(v); err != nil {
|
||||||
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
||||||
@@ -1743,6 +1799,12 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
|
|||||||
if _u.mutation.FirstTokenMsCleared() {
|
if _u.mutation.FirstTokenMsCleared() {
|
||||||
_spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt)
|
_spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.UserAgent(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldUserAgent, field.TypeString, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.UserAgentCleared() {
|
||||||
|
_spec.ClearField(usagelog.FieldUserAgent, field.TypeString)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.ImageCount(); ok {
|
if value, ok := _u.mutation.ImageCount(); ok {
|
||||||
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ type Config struct {
|
|||||||
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
|
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
|
||||||
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
|
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
|
||||||
Gemini GeminiConfig `mapstructure:"gemini"`
|
Gemini GeminiConfig `mapstructure:"gemini"`
|
||||||
|
Update UpdateConfig `mapstructure:"update"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConfig 在线更新相关配置
|
||||||
|
type UpdateConfig struct {
|
||||||
|
// ProxyURL 用于访问 GitHub 的代理地址
|
||||||
|
// 支持 http/https/socks5/socks5h 协议
|
||||||
|
// 例如: "http://127.0.0.1:7890", "socks5://127.0.0.1:1080"
|
||||||
|
ProxyURL string `mapstructure:"proxy_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GeminiConfig struct {
|
type GeminiConfig struct {
|
||||||
@@ -558,6 +567,10 @@ func setDefaults() {
|
|||||||
viper.SetDefault("gemini.oauth.client_secret", "")
|
viper.SetDefault("gemini.oauth.client_secret", "")
|
||||||
viper.SetDefault("gemini.oauth.scopes", "")
|
viper.SetDefault("gemini.oauth.scopes", "")
|
||||||
viper.SetDefault("gemini.quota.policy", "")
|
viper.SetDefault("gemini.quota.policy", "")
|
||||||
|
|
||||||
|
// Update - 在线更新配置
|
||||||
|
// 代理地址为空表示直连 GitHub(适用于海外服务器)
|
||||||
|
viper.SetDefault("update.proxy_url", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ type CreateAccountRequest struct {
|
|||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
GroupIDs []int64 `json:"group_ids"`
|
GroupIDs []int64 `json:"group_ids"`
|
||||||
|
ExpiresAt *int64 `json:"expires_at"`
|
||||||
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +103,8 @@ type UpdateAccountRequest struct {
|
|||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||||
GroupIDs *[]int64 `json:"group_ids"`
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
|
ExpiresAt *int64 `json:"expires_at"`
|
||||||
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +208,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
|||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Priority: req.Priority,
|
Priority: req.Priority,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
|
ExpiresAt: req.ExpiresAt,
|
||||||
|
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||||
SkipMixedChannelCheck: skipCheck,
|
SkipMixedChannelCheck: skipCheck,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -261,6 +267,8 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
|||||||
Priority: req.Priority, // 指针类型,nil 表示未提供
|
Priority: req.Priority, // 指针类型,nil 表示未提供
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
|
ExpiresAt: req.ExpiresAt,
|
||||||
|
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||||
SkipMixedChannelCheck: skipCheck,
|
SkipMixedChannelCheck: skipCheck,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
// Package dto provides data transfer objects for HTTP handlers.
|
// Package dto provides data transfer objects for HTTP handlers.
|
||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "github.com/Wei-Shaw/sub2api/internal/service"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
func UserFromServiceShallow(u *service.User) *User {
|
func UserFromServiceShallow(u *service.User) *User {
|
||||||
if u == nil {
|
if u == nil {
|
||||||
@@ -120,6 +124,8 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
|||||||
Status: a.Status,
|
Status: a.Status,
|
||||||
ErrorMessage: a.ErrorMessage,
|
ErrorMessage: a.ErrorMessage,
|
||||||
LastUsedAt: a.LastUsedAt,
|
LastUsedAt: a.LastUsedAt,
|
||||||
|
ExpiresAt: timeToUnixSeconds(a.ExpiresAt),
|
||||||
|
AutoPauseOnExpired: a.AutoPauseOnExpired,
|
||||||
CreatedAt: a.CreatedAt,
|
CreatedAt: a.CreatedAt,
|
||||||
UpdatedAt: a.UpdatedAt,
|
UpdatedAt: a.UpdatedAt,
|
||||||
Schedulable: a.Schedulable,
|
Schedulable: a.Schedulable,
|
||||||
@@ -157,6 +163,14 @@ func AccountFromService(a *service.Account) *Account {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func timeToUnixSeconds(value *time.Time) *int64 {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ts := value.Unix()
|
||||||
|
return &ts
|
||||||
|
}
|
||||||
|
|
||||||
func AccountGroupFromService(ag *service.AccountGroup) *AccountGroup {
|
func AccountGroupFromService(ag *service.AccountGroup) *AccountGroup {
|
||||||
if ag == nil {
|
if ag == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -60,21 +60,23 @@ type Group struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Notes *string `json:"notes"`
|
Notes *string `json:"notes"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Credentials map[string]any `json:"credentials"`
|
Credentials map[string]any `json:"credentials"`
|
||||||
Extra map[string]any `json:"extra"`
|
Extra map[string]any `json:"extra"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
ErrorMessage string `json:"error_message"`
|
ErrorMessage string `json:"error_message"`
|
||||||
LastUsedAt *time.Time `json:"last_used_at"`
|
LastUsedAt *time.Time `json:"last_used_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
ExpiresAt *int64 `json:"expires_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
AutoPauseOnExpired bool `json:"auto_pause_on_expired"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
Schedulable bool `json:"schedulable"`
|
Schedulable bool `json:"schedulable"`
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
// 获取订阅信息(可能为nil)- 提前获取用于后续检查
|
// 获取订阅信息(可能为nil)- 提前获取用于后续检查
|
||||||
subscription, _ := middleware2.GetSubscriptionFromContext(c)
|
subscription, _ := middleware2.GetSubscriptionFromContext(c)
|
||||||
|
|
||||||
|
// 获取 User-Agent
|
||||||
|
userAgent := c.Request.UserAgent()
|
||||||
|
|
||||||
// 0. 检查wait队列是否已满
|
// 0. 检查wait队列是否已满
|
||||||
maxWait := service.CalculateMaxWait(subject.Concurrency)
|
maxWait := service.CalculateMaxWait(subject.Concurrency)
|
||||||
canWait, err := h.concurrencyHelper.IncrementWaitCount(c.Request.Context(), subject.UserID, maxWait)
|
canWait, err := h.concurrencyHelper.IncrementWaitCount(c.Request.Context(), subject.UserID, maxWait)
|
||||||
@@ -267,7 +270,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 异步记录使用量(subscription已在函数开头获取)
|
// 异步记录使用量(subscription已在函数开头获取)
|
||||||
go func(result *service.ForwardResult, usedAccount *service.Account) {
|
go func(result *service.ForwardResult, usedAccount *service.Account, ua string) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
||||||
@@ -276,10 +279,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
User: apiKey.User,
|
User: apiKey.User,
|
||||||
Account: usedAccount,
|
Account: usedAccount,
|
||||||
Subscription: subscription,
|
Subscription: subscription,
|
||||||
|
UserAgent: ua,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("Record usage failed: %v", err)
|
log.Printf("Record usage failed: %v", err)
|
||||||
}
|
}
|
||||||
}(result, account)
|
}(result, account, userAgent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,7 +398,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 异步记录使用量(subscription已在函数开头获取)
|
// 异步记录使用量(subscription已在函数开头获取)
|
||||||
go func(result *service.ForwardResult, usedAccount *service.Account) {
|
go func(result *service.ForwardResult, usedAccount *service.Account, ua string) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
||||||
@@ -403,10 +407,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
User: apiKey.User,
|
User: apiKey.User,
|
||||||
Account: usedAccount,
|
Account: usedAccount,
|
||||||
Subscription: subscription,
|
Subscription: subscription,
|
||||||
|
UserAgent: ua,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("Record usage failed: %v", err)
|
log.Printf("Record usage failed: %v", err)
|
||||||
}
|
}
|
||||||
}(result, account)
|
}(result, account, userAgent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
|
|||||||
// Get subscription (may be nil)
|
// Get subscription (may be nil)
|
||||||
subscription, _ := middleware.GetSubscriptionFromContext(c)
|
subscription, _ := middleware.GetSubscriptionFromContext(c)
|
||||||
|
|
||||||
|
// 获取 User-Agent
|
||||||
|
userAgent := c.Request.UserAgent()
|
||||||
|
|
||||||
// For Gemini native API, do not send Claude-style ping frames.
|
// For Gemini native API, do not send Claude-style ping frames.
|
||||||
geminiConcurrency := NewConcurrencyHelper(h.concurrencyHelper.concurrencyService, SSEPingFormatNone, 0)
|
geminiConcurrency := NewConcurrencyHelper(h.concurrencyHelper.concurrencyService, SSEPingFormatNone, 0)
|
||||||
|
|
||||||
@@ -300,7 +303,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6) record usage async
|
// 6) record usage async
|
||||||
go func(result *service.ForwardResult, usedAccount *service.Account) {
|
go func(result *service.ForwardResult, usedAccount *service.Account, ua string) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
||||||
@@ -309,10 +312,11 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
|
|||||||
User: apiKey.User,
|
User: apiKey.User,
|
||||||
Account: usedAccount,
|
Account: usedAccount,
|
||||||
Subscription: subscription,
|
Subscription: subscription,
|
||||||
|
UserAgent: ua,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("Record usage failed: %v", err)
|
log.Printf("Record usage failed: %v", err)
|
||||||
}
|
}
|
||||||
}(result, account)
|
}(result, account, userAgent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Async record usage
|
// Async record usage
|
||||||
go func(result *service.OpenAIForwardResult, usedAccount *service.Account) {
|
go func(result *service.OpenAIForwardResult, usedAccount *service.Account, ua string) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
|
if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
|
||||||
@@ -251,10 +251,11 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
|||||||
User: apiKey.User,
|
User: apiKey.User,
|
||||||
Account: usedAccount,
|
Account: usedAccount,
|
||||||
Subscription: subscription,
|
Subscription: subscription,
|
||||||
|
UserAgent: ua,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("Record usage failed: %v", err)
|
log.Printf("Record usage failed: %v", err)
|
||||||
}
|
}
|
||||||
}(result, account)
|
}(result, account, userAgent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ func (r *accountRepository) Create(ctx context.Context, account *service.Account
|
|||||||
SetPriority(account.Priority).
|
SetPriority(account.Priority).
|
||||||
SetStatus(account.Status).
|
SetStatus(account.Status).
|
||||||
SetErrorMessage(account.ErrorMessage).
|
SetErrorMessage(account.ErrorMessage).
|
||||||
SetSchedulable(account.Schedulable)
|
SetSchedulable(account.Schedulable).
|
||||||
|
SetAutoPauseOnExpired(account.AutoPauseOnExpired)
|
||||||
|
|
||||||
if account.ProxyID != nil {
|
if account.ProxyID != nil {
|
||||||
builder.SetProxyID(*account.ProxyID)
|
builder.SetProxyID(*account.ProxyID)
|
||||||
@@ -84,6 +85,9 @@ func (r *accountRepository) Create(ctx context.Context, account *service.Account
|
|||||||
if account.LastUsedAt != nil {
|
if account.LastUsedAt != nil {
|
||||||
builder.SetLastUsedAt(*account.LastUsedAt)
|
builder.SetLastUsedAt(*account.LastUsedAt)
|
||||||
}
|
}
|
||||||
|
if account.ExpiresAt != nil {
|
||||||
|
builder.SetExpiresAt(*account.ExpiresAt)
|
||||||
|
}
|
||||||
if account.RateLimitedAt != nil {
|
if account.RateLimitedAt != nil {
|
||||||
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
||||||
}
|
}
|
||||||
@@ -280,7 +284,8 @@ func (r *accountRepository) Update(ctx context.Context, account *service.Account
|
|||||||
SetPriority(account.Priority).
|
SetPriority(account.Priority).
|
||||||
SetStatus(account.Status).
|
SetStatus(account.Status).
|
||||||
SetErrorMessage(account.ErrorMessage).
|
SetErrorMessage(account.ErrorMessage).
|
||||||
SetSchedulable(account.Schedulable)
|
SetSchedulable(account.Schedulable).
|
||||||
|
SetAutoPauseOnExpired(account.AutoPauseOnExpired)
|
||||||
|
|
||||||
if account.ProxyID != nil {
|
if account.ProxyID != nil {
|
||||||
builder.SetProxyID(*account.ProxyID)
|
builder.SetProxyID(*account.ProxyID)
|
||||||
@@ -292,6 +297,11 @@ func (r *accountRepository) Update(ctx context.Context, account *service.Account
|
|||||||
} else {
|
} else {
|
||||||
builder.ClearLastUsedAt()
|
builder.ClearLastUsedAt()
|
||||||
}
|
}
|
||||||
|
if account.ExpiresAt != nil {
|
||||||
|
builder.SetExpiresAt(*account.ExpiresAt)
|
||||||
|
} else {
|
||||||
|
builder.ClearExpiresAt()
|
||||||
|
}
|
||||||
if account.RateLimitedAt != nil {
|
if account.RateLimitedAt != nil {
|
||||||
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
||||||
} else {
|
} else {
|
||||||
@@ -570,6 +580,7 @@ func (r *accountRepository) ListSchedulable(ctx context.Context) ([]service.Acco
|
|||||||
dbaccount.StatusEQ(service.StatusActive),
|
dbaccount.StatusEQ(service.StatusActive),
|
||||||
dbaccount.SchedulableEQ(true),
|
dbaccount.SchedulableEQ(true),
|
||||||
tempUnschedulablePredicate(),
|
tempUnschedulablePredicate(),
|
||||||
|
notExpiredPredicate(now),
|
||||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||||
).
|
).
|
||||||
@@ -596,6 +607,7 @@ func (r *accountRepository) ListSchedulableByPlatform(ctx context.Context, platf
|
|||||||
dbaccount.StatusEQ(service.StatusActive),
|
dbaccount.StatusEQ(service.StatusActive),
|
||||||
dbaccount.SchedulableEQ(true),
|
dbaccount.SchedulableEQ(true),
|
||||||
tempUnschedulablePredicate(),
|
tempUnschedulablePredicate(),
|
||||||
|
notExpiredPredicate(now),
|
||||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||||
).
|
).
|
||||||
@@ -629,6 +641,7 @@ func (r *accountRepository) ListSchedulableByPlatforms(ctx context.Context, plat
|
|||||||
dbaccount.StatusEQ(service.StatusActive),
|
dbaccount.StatusEQ(service.StatusActive),
|
||||||
dbaccount.SchedulableEQ(true),
|
dbaccount.SchedulableEQ(true),
|
||||||
tempUnschedulablePredicate(),
|
tempUnschedulablePredicate(),
|
||||||
|
notExpiredPredicate(now),
|
||||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||||
).
|
).
|
||||||
@@ -727,6 +740,27 @@ func (r *accountRepository) SetSchedulable(ctx context.Context, id int64, schedu
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *accountRepository) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
|
||||||
|
result, err := r.sql.ExecContext(ctx, `
|
||||||
|
UPDATE accounts
|
||||||
|
SET schedulable = FALSE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND schedulable = TRUE
|
||||||
|
AND auto_pause_on_expired = TRUE
|
||||||
|
AND expires_at IS NOT NULL
|
||||||
|
AND expires_at <= $1
|
||||||
|
`, now)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
rows, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *accountRepository) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
|
func (r *accountRepository) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
|
||||||
if len(updates) == 0 {
|
if len(updates) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -861,6 +895,7 @@ func (r *accountRepository) queryAccountsByGroup(ctx context.Context, groupID in
|
|||||||
preds = append(preds,
|
preds = append(preds,
|
||||||
dbaccount.SchedulableEQ(true),
|
dbaccount.SchedulableEQ(true),
|
||||||
tempUnschedulablePredicate(),
|
tempUnschedulablePredicate(),
|
||||||
|
notExpiredPredicate(now),
|
||||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||||
)
|
)
|
||||||
@@ -971,6 +1006,14 @@ func tempUnschedulablePredicate() dbpredicate.Account {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notExpiredPredicate(now time.Time) dbpredicate.Account {
|
||||||
|
return dbaccount.Or(
|
||||||
|
dbaccount.ExpiresAtIsNil(),
|
||||||
|
dbaccount.ExpiresAtGT(now),
|
||||||
|
dbaccount.AutoPauseOnExpiredEQ(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *accountRepository) loadTempUnschedStates(ctx context.Context, accountIDs []int64) (map[int64]tempUnschedSnapshot, error) {
|
func (r *accountRepository) loadTempUnschedStates(ctx context.Context, accountIDs []int64) (map[int64]tempUnschedSnapshot, error) {
|
||||||
out := make(map[int64]tempUnschedSnapshot)
|
out := make(map[int64]tempUnschedSnapshot)
|
||||||
if len(accountIDs) == 0 {
|
if len(accountIDs) == 0 {
|
||||||
@@ -1086,6 +1129,8 @@ func accountEntityToService(m *dbent.Account) *service.Account {
|
|||||||
Status: m.Status,
|
Status: m.Status,
|
||||||
ErrorMessage: derefString(m.ErrorMessage),
|
ErrorMessage: derefString(m.ErrorMessage),
|
||||||
LastUsedAt: m.LastUsedAt,
|
LastUsedAt: m.LastUsedAt,
|
||||||
|
ExpiresAt: m.ExpiresAt,
|
||||||
|
AutoPauseOnExpired: m.AutoPauseOnExpired,
|
||||||
CreatedAt: m.CreatedAt,
|
CreatedAt: m.CreatedAt,
|
||||||
UpdatedAt: m.UpdatedAt,
|
UpdatedAt: m.UpdatedAt,
|
||||||
Schedulable: m.Schedulable,
|
Schedulable: m.Schedulable,
|
||||||
|
|||||||
@@ -14,23 +14,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type githubReleaseClient struct {
|
type githubReleaseClient struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
allowPrivateHosts bool
|
downloadHTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGitHubReleaseClient() service.GitHubReleaseClient {
|
// NewGitHubReleaseClient 创建 GitHub Release 客户端
|
||||||
allowPrivate := false
|
// proxyURL 为空时直连 GitHub,支持 http/https/socks5/socks5h 协议
|
||||||
|
func NewGitHubReleaseClient(proxyURL string) service.GitHubReleaseClient {
|
||||||
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
ValidateResolvedIP: true,
|
ProxyURL: proxyURL,
|
||||||
AllowPrivateHosts: allowPrivate,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载客户端需要更长的超时时间
|
||||||
|
downloadClient, err := httpclient.GetClient(httpclient.Options{
|
||||||
|
Timeout: 10 * time.Minute,
|
||||||
|
ProxyURL: proxyURL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
downloadClient = &http.Client{Timeout: 10 * time.Minute}
|
||||||
|
}
|
||||||
|
|
||||||
return &githubReleaseClient{
|
return &githubReleaseClient{
|
||||||
httpClient: sharedClient,
|
httpClient: sharedClient,
|
||||||
allowPrivateHosts: allowPrivate,
|
downloadHTTPClient: downloadClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,15 +78,8 @@ func (c *githubReleaseClient) DownloadFile(ctx context.Context, url, dest string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadClient, err := httpclient.GetClient(httpclient.Options{
|
// 使用预配置的下载客户端(已包含代理配置)
|
||||||
Timeout: 10 * time.Minute,
|
resp, err := c.downloadHTTPClient.Do(req)
|
||||||
ValidateResolvedIP: true,
|
|
||||||
AllowPrivateHosts: c.allowPrivateHosts,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
downloadClient = &http.Client{Timeout: 10 * time.Minute}
|
|
||||||
}
|
|
||||||
resp, err := downloadClient.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
|
|
||||||
func newTestGitHubReleaseClient() *githubReleaseClient {
|
func newTestGitHubReleaseClient() *githubReleaseClient {
|
||||||
return &githubReleaseClient{
|
return &githubReleaseClient{
|
||||||
httpClient: &http.Client{},
|
httpClient: &http.Client{},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Success() {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||||
},
|
},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
release, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||||
@@ -254,7 +254,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Non200() {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||||
},
|
},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||||
@@ -272,7 +272,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_InvalidJSON() {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||||
},
|
},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||||
@@ -288,7 +288,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_ContextCancel() {
|
|||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||||
},
|
},
|
||||||
allowPrivateHosts: true,
|
downloadHTTPClient: &http.Client{},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
)
|
)
|
||||||
@@ -17,17 +16,12 @@ type pricingRemoteClient struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPricingRemoteClient(cfg *config.Config) service.PricingRemoteClient {
|
// NewPricingRemoteClient 创建定价数据远程客户端
|
||||||
allowPrivate := false
|
// proxyURL 为空时直连,支持 http/https/socks5/socks5h 协议
|
||||||
validateResolvedIP := true
|
func NewPricingRemoteClient(proxyURL string) service.PricingRemoteClient {
|
||||||
if cfg != nil {
|
|
||||||
allowPrivate = cfg.Security.URLAllowlist.AllowPrivateHosts
|
|
||||||
validateResolvedIP = cfg.Security.URLAllowlist.Enabled
|
|
||||||
}
|
|
||||||
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
ValidateResolvedIP: validateResolvedIP,
|
ProxyURL: proxyURL,
|
||||||
AllowPrivateHosts: allowPrivate,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
@@ -20,13 +19,7 @@ type PricingServiceSuite struct {
|
|||||||
|
|
||||||
func (s *PricingServiceSuite) SetupTest() {
|
func (s *PricingServiceSuite) SetupTest() {
|
||||||
s.ctx = context.Background()
|
s.ctx = context.Background()
|
||||||
client, ok := NewPricingRemoteClient(&config.Config{
|
client, ok := NewPricingRemoteClient("").(*pricingRemoteClient)
|
||||||
Security: config.SecurityConfig{
|
|
||||||
URLAllowlist: config.URLAllowlistConfig{
|
|
||||||
AllowPrivateHosts: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).(*pricingRemoteClient)
|
|
||||||
require.True(s.T(), ok, "type assertion failed")
|
require.True(s.T(), ok, "type assertion failed")
|
||||||
s.client = client
|
s.client = client
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, image_count, image_size, created_at"
|
const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, user_agent, image_count, image_size, created_at"
|
||||||
|
|
||||||
type usageLogRepository struct {
|
type usageLogRepository struct {
|
||||||
client *dbent.Client
|
client *dbent.Client
|
||||||
@@ -109,6 +109,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
|||||||
stream,
|
stream,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
first_token_ms,
|
first_token_ms,
|
||||||
|
user_agent,
|
||||||
image_count,
|
image_count,
|
||||||
image_size,
|
image_size,
|
||||||
created_at
|
created_at
|
||||||
@@ -118,8 +119,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
|||||||
$8, $9, $10, $11,
|
$8, $9, $10, $11,
|
||||||
$12, $13,
|
$12, $13,
|
||||||
$14, $15, $16, $17, $18, $19,
|
$14, $15, $16, $17, $18, $19,
|
||||||
$20, $21, $22, $23, $24,
|
$20, $21, $22, $23, $24, $25, $26, $27, $28
|
||||||
$25, $26, $27
|
|
||||||
)
|
)
|
||||||
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
||||||
RETURNING id, created_at
|
RETURNING id, created_at
|
||||||
@@ -129,6 +129,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
|||||||
subscriptionID := nullInt64(log.SubscriptionID)
|
subscriptionID := nullInt64(log.SubscriptionID)
|
||||||
duration := nullInt(log.DurationMs)
|
duration := nullInt(log.DurationMs)
|
||||||
firstToken := nullInt(log.FirstTokenMs)
|
firstToken := nullInt(log.FirstTokenMs)
|
||||||
|
userAgent := nullString(log.UserAgent)
|
||||||
imageSize := nullString(log.ImageSize)
|
imageSize := nullString(log.ImageSize)
|
||||||
|
|
||||||
var requestIDArg any
|
var requestIDArg any
|
||||||
@@ -161,6 +162,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
|||||||
log.Stream,
|
log.Stream,
|
||||||
duration,
|
duration,
|
||||||
firstToken,
|
firstToken,
|
||||||
|
userAgent,
|
||||||
log.ImageCount,
|
log.ImageCount,
|
||||||
imageSize,
|
imageSize,
|
||||||
createdAt,
|
createdAt,
|
||||||
@@ -1870,6 +1872,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
|||||||
stream bool
|
stream bool
|
||||||
durationMs sql.NullInt64
|
durationMs sql.NullInt64
|
||||||
firstTokenMs sql.NullInt64
|
firstTokenMs sql.NullInt64
|
||||||
|
userAgent sql.NullString
|
||||||
imageCount int
|
imageCount int
|
||||||
imageSize sql.NullString
|
imageSize sql.NullString
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
@@ -1901,6 +1904,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
|||||||
&stream,
|
&stream,
|
||||||
&durationMs,
|
&durationMs,
|
||||||
&firstTokenMs,
|
&firstTokenMs,
|
||||||
|
&userAgent,
|
||||||
&imageCount,
|
&imageCount,
|
||||||
&imageSize,
|
&imageSize,
|
||||||
&createdAt,
|
&createdAt,
|
||||||
@@ -1952,6 +1956,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
|||||||
value := int(firstTokenMs.Int64)
|
value := int(firstTokenMs.Int64)
|
||||||
log.FirstTokenMs = &value
|
log.FirstTokenMs = &value
|
||||||
}
|
}
|
||||||
|
if userAgent.Valid {
|
||||||
|
log.UserAgent = &userAgent.String
|
||||||
|
}
|
||||||
if imageSize.Valid {
|
if imageSize.Valid {
|
||||||
log.ImageSize = &imageSize.String
|
log.ImageSize = &imageSize.String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ func ProvideConcurrencyCache(rdb *redis.Client, cfg *config.Config) service.Conc
|
|||||||
return NewConcurrencyCache(rdb, cfg.Gateway.ConcurrencySlotTTLMinutes, waitTTLSeconds)
|
return NewConcurrencyCache(rdb, cfg.Gateway.ConcurrencySlotTTLMinutes, waitTTLSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideGitHubReleaseClient 创建 GitHub Release 客户端
|
||||||
|
// 从配置中读取代理设置,支持国内服务器通过代理访问 GitHub
|
||||||
|
func ProvideGitHubReleaseClient(cfg *config.Config) service.GitHubReleaseClient {
|
||||||
|
return NewGitHubReleaseClient(cfg.Update.ProxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvidePricingRemoteClient 创建定价数据远程客户端
|
||||||
|
// 从配置中读取代理设置,支持国内服务器通过代理访问 GitHub 上的定价数据
|
||||||
|
func ProvidePricingRemoteClient(cfg *config.Config) service.PricingRemoteClient {
|
||||||
|
return NewPricingRemoteClient(cfg.Update.ProxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
// ProviderSet is the Wire provider set for all repositories
|
// ProviderSet is the Wire provider set for all repositories
|
||||||
var ProviderSet = wire.NewSet(
|
var ProviderSet = wire.NewSet(
|
||||||
NewUserRepository,
|
NewUserRepository,
|
||||||
@@ -53,8 +65,8 @@ var ProviderSet = wire.NewSet(
|
|||||||
|
|
||||||
// HTTP service ports (DI Strategy A: return interface directly)
|
// HTTP service ports (DI Strategy A: return interface directly)
|
||||||
NewTurnstileVerifier,
|
NewTurnstileVerifier,
|
||||||
NewPricingRemoteClient,
|
ProvidePricingRemoteClient,
|
||||||
NewGitHubReleaseClient,
|
ProvideGitHubReleaseClient,
|
||||||
NewProxyExitInfoProber,
|
NewProxyExitInfoProber,
|
||||||
NewClaudeUsageFetcher,
|
NewClaudeUsageFetcher,
|
||||||
NewClaudeOAuthClient,
|
NewClaudeOAuthClient,
|
||||||
|
|||||||
@@ -9,21 +9,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
Notes *string
|
Notes *string
|
||||||
Platform string
|
Platform string
|
||||||
Type string
|
Type string
|
||||||
Credentials map[string]any
|
Credentials map[string]any
|
||||||
Extra map[string]any
|
Extra map[string]any
|
||||||
ProxyID *int64
|
ProxyID *int64
|
||||||
Concurrency int
|
Concurrency int
|
||||||
Priority int
|
Priority int
|
||||||
Status string
|
Status string
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
LastUsedAt *time.Time
|
LastUsedAt *time.Time
|
||||||
CreatedAt time.Time
|
ExpiresAt *time.Time
|
||||||
UpdatedAt time.Time
|
AutoPauseOnExpired bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
|
||||||
Schedulable bool
|
Schedulable bool
|
||||||
|
|
||||||
@@ -60,6 +62,9 @@ func (a *Account) IsSchedulable() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
if a.AutoPauseOnExpired && a.ExpiresAt != nil && !now.Before(*a.ExpiresAt) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if a.OverloadUntil != nil && now.Before(*a.OverloadUntil) {
|
if a.OverloadUntil != nil && now.Before(*a.OverloadUntil) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
71
backend/internal/service/account_expiry_service.go
Normal file
71
backend/internal/service/account_expiry_service.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountExpiryService periodically pauses expired accounts when auto-pause is enabled.
|
||||||
|
type AccountExpiryService struct {
|
||||||
|
accountRepo AccountRepository
|
||||||
|
interval time.Duration
|
||||||
|
stopCh chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccountExpiryService(accountRepo AccountRepository, interval time.Duration) *AccountExpiryService {
|
||||||
|
return &AccountExpiryService{
|
||||||
|
accountRepo: accountRepo,
|
||||||
|
interval: interval,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccountExpiryService) Start() {
|
||||||
|
if s == nil || s.accountRepo == nil || s.interval <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer s.wg.Done()
|
||||||
|
ticker := time.NewTicker(s.interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
s.runOnce()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
s.runOnce()
|
||||||
|
case <-s.stopCh:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccountExpiryService) Stop() {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.stopOnce.Do(func() {
|
||||||
|
close(s.stopCh)
|
||||||
|
})
|
||||||
|
s.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccountExpiryService) runOnce() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
updated, err := s.accountRepo.AutoPauseExpiredAccounts(ctx, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[AccountExpiry] Auto pause expired accounts failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if updated > 0 {
|
||||||
|
log.Printf("[AccountExpiry] Auto paused %d expired accounts", updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ type AccountRepository interface {
|
|||||||
BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error
|
BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error
|
||||||
SetError(ctx context.Context, id int64, errorMsg string) error
|
SetError(ctx context.Context, id int64, errorMsg string) error
|
||||||
SetSchedulable(ctx context.Context, id int64, schedulable bool) error
|
SetSchedulable(ctx context.Context, id int64, schedulable bool) error
|
||||||
|
AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error)
|
||||||
BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error
|
BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error
|
||||||
|
|
||||||
ListSchedulable(ctx context.Context) ([]Account, error)
|
ListSchedulable(ctx context.Context) ([]Account, error)
|
||||||
@@ -71,29 +72,33 @@ type AccountBulkUpdate struct {
|
|||||||
|
|
||||||
// CreateAccountRequest 创建账号请求
|
// CreateAccountRequest 创建账号请求
|
||||||
type CreateAccountRequest struct {
|
type CreateAccountRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Notes *string `json:"notes"`
|
Notes *string `json:"notes"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Credentials map[string]any `json:"credentials"`
|
Credentials map[string]any `json:"credentials"`
|
||||||
Extra map[string]any `json:"extra"`
|
Extra map[string]any `json:"extra"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
GroupIDs []int64 `json:"group_ids"`
|
GroupIDs []int64 `json:"group_ids"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAccountRequest 更新账号请求
|
// UpdateAccountRequest 更新账号请求
|
||||||
type UpdateAccountRequest struct {
|
type UpdateAccountRequest struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Notes *string `json:"notes"`
|
Notes *string `json:"notes"`
|
||||||
Credentials *map[string]any `json:"credentials"`
|
Credentials *map[string]any `json:"credentials"`
|
||||||
Extra *map[string]any `json:"extra"`
|
Extra *map[string]any `json:"extra"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
Concurrency *int `json:"concurrency"`
|
Concurrency *int `json:"concurrency"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
GroupIDs *[]int64 `json:"group_ids"`
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountService 账号管理服务
|
// AccountService 账号管理服务
|
||||||
@@ -134,6 +139,12 @@ func (s *AccountService) Create(ctx context.Context, req CreateAccountRequest) (
|
|||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Priority: req.Priority,
|
Priority: req.Priority,
|
||||||
Status: StatusActive,
|
Status: StatusActive,
|
||||||
|
ExpiresAt: req.ExpiresAt,
|
||||||
|
}
|
||||||
|
if req.AutoPauseOnExpired != nil {
|
||||||
|
account.AutoPauseOnExpired = *req.AutoPauseOnExpired
|
||||||
|
} else {
|
||||||
|
account.AutoPauseOnExpired = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.accountRepo.Create(ctx, account); err != nil {
|
if err := s.accountRepo.Create(ctx, account); err != nil {
|
||||||
@@ -224,6 +235,12 @@ func (s *AccountService) Update(ctx context.Context, id int64, req UpdateAccount
|
|||||||
if req.Status != nil {
|
if req.Status != nil {
|
||||||
account.Status = *req.Status
|
account.Status = *req.Status
|
||||||
}
|
}
|
||||||
|
if req.ExpiresAt != nil {
|
||||||
|
account.ExpiresAt = req.ExpiresAt
|
||||||
|
}
|
||||||
|
if req.AutoPauseOnExpired != nil {
|
||||||
|
account.AutoPauseOnExpired = *req.AutoPauseOnExpired
|
||||||
|
}
|
||||||
|
|
||||||
// 先验证分组是否存在(在任何写操作之前)
|
// 先验证分组是否存在(在任何写操作之前)
|
||||||
if req.GroupIDs != nil {
|
if req.GroupIDs != nil {
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ func (s *accountRepoStub) SetSchedulable(ctx context.Context, id int64, schedula
|
|||||||
panic("unexpected SetSchedulable call")
|
panic("unexpected SetSchedulable call")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *accountRepoStub) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
|
||||||
|
panic("unexpected AutoPauseExpiredAccounts call")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *accountRepoStub) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
func (s *accountRepoStub) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
||||||
panic("unexpected BindGroups call")
|
panic("unexpected BindGroups call")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,16 +122,18 @@ type UpdateGroupInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateAccountInput struct {
|
type CreateAccountInput struct {
|
||||||
Name string
|
Name string
|
||||||
Notes *string
|
Notes *string
|
||||||
Platform string
|
Platform string
|
||||||
Type string
|
Type string
|
||||||
Credentials map[string]any
|
Credentials map[string]any
|
||||||
Extra map[string]any
|
Extra map[string]any
|
||||||
ProxyID *int64
|
ProxyID *int64
|
||||||
Concurrency int
|
Concurrency int
|
||||||
Priority int
|
Priority int
|
||||||
GroupIDs []int64
|
GroupIDs []int64
|
||||||
|
ExpiresAt *int64
|
||||||
|
AutoPauseOnExpired *bool
|
||||||
// SkipMixedChannelCheck skips the mixed channel risk check when binding groups.
|
// SkipMixedChannelCheck skips the mixed channel risk check when binding groups.
|
||||||
// This should only be set when the caller has explicitly confirmed the risk.
|
// This should only be set when the caller has explicitly confirmed the risk.
|
||||||
SkipMixedChannelCheck bool
|
SkipMixedChannelCheck bool
|
||||||
@@ -148,6 +150,8 @@ type UpdateAccountInput struct {
|
|||||||
Priority *int // 使用指针区分"未提供"和"设置为0"
|
Priority *int // 使用指针区分"未提供"和"设置为0"
|
||||||
Status string
|
Status string
|
||||||
GroupIDs *[]int64
|
GroupIDs *[]int64
|
||||||
|
ExpiresAt *int64
|
||||||
|
AutoPauseOnExpired *bool
|
||||||
SkipMixedChannelCheck bool // 跳过混合渠道检查(用户已确认风险)
|
SkipMixedChannelCheck bool // 跳过混合渠道检查(用户已确认风险)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,6 +704,15 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
|
|||||||
Status: StatusActive,
|
Status: StatusActive,
|
||||||
Schedulable: true,
|
Schedulable: true,
|
||||||
}
|
}
|
||||||
|
if input.ExpiresAt != nil && *input.ExpiresAt > 0 {
|
||||||
|
expiresAt := time.Unix(*input.ExpiresAt, 0)
|
||||||
|
account.ExpiresAt = &expiresAt
|
||||||
|
}
|
||||||
|
if input.AutoPauseOnExpired != nil {
|
||||||
|
account.AutoPauseOnExpired = *input.AutoPauseOnExpired
|
||||||
|
} else {
|
||||||
|
account.AutoPauseOnExpired = true
|
||||||
|
}
|
||||||
if err := s.accountRepo.Create(ctx, account); err != nil {
|
if err := s.accountRepo.Create(ctx, account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -755,6 +768,17 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
|||||||
if input.Status != "" {
|
if input.Status != "" {
|
||||||
account.Status = input.Status
|
account.Status = input.Status
|
||||||
}
|
}
|
||||||
|
if input.ExpiresAt != nil {
|
||||||
|
if *input.ExpiresAt <= 0 {
|
||||||
|
account.ExpiresAt = nil
|
||||||
|
} else {
|
||||||
|
expiresAt := time.Unix(*input.ExpiresAt, 0)
|
||||||
|
account.ExpiresAt = &expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.AutoPauseOnExpired != nil {
|
||||||
|
account.AutoPauseOnExpired = *input.AutoPauseOnExpired
|
||||||
|
}
|
||||||
|
|
||||||
// 先验证分组是否存在(在任何写操作之前)
|
// 先验证分组是否存在(在任何写操作之前)
|
||||||
if input.GroupIDs != nil {
|
if input.GroupIDs != nil {
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ func (m *mockAccountRepoForPlatform) SetError(ctx context.Context, id int64, err
|
|||||||
func (m *mockAccountRepoForPlatform) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
func (m *mockAccountRepoForPlatform) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (m *mockAccountRepoForPlatform) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
func (m *mockAccountRepoForPlatform) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
func (m *mockAccountRepoForPlatform) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const (
|
|||||||
stickySessionTTL = time.Hour // 粘性会话TTL
|
stickySessionTTL = time.Hour // 粘性会话TTL
|
||||||
defaultMaxLineSize = 10 * 1024 * 1024
|
defaultMaxLineSize = 10 * 1024 * 1024
|
||||||
claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude."
|
claudeCodeSystemPrompt = "You are Claude Code, Anthropic's official CLI for Claude."
|
||||||
|
maxCacheControlBlocks = 4 // Anthropic API 允许的最大 cache_control 块数量
|
||||||
)
|
)
|
||||||
|
|
||||||
// sseDataRe matches SSE data lines with optional whitespace after colon.
|
// sseDataRe matches SSE data lines with optional whitespace after colon.
|
||||||
@@ -43,6 +44,16 @@ var (
|
|||||||
sseDataRe = regexp.MustCompile(`^data:\s*`)
|
sseDataRe = regexp.MustCompile(`^data:\s*`)
|
||||||
sessionIDRegex = regexp.MustCompile(`session_([a-f0-9-]{36})`)
|
sessionIDRegex = regexp.MustCompile(`session_([a-f0-9-]{36})`)
|
||||||
claudeCliUserAgentRe = regexp.MustCompile(`^claude-cli/\d+\.\d+\.\d+`)
|
claudeCliUserAgentRe = regexp.MustCompile(`^claude-cli/\d+\.\d+\.\d+`)
|
||||||
|
|
||||||
|
// claudeCodePromptPrefixes 用于检测 Claude Code 系统提示词的前缀列表
|
||||||
|
// 支持多种变体:标准版、Agent SDK 版、Explore Agent 版、Compact 版等
|
||||||
|
// 注意:前缀之间不应存在包含关系,否则会导致冗余匹配
|
||||||
|
claudeCodePromptPrefixes = []string{
|
||||||
|
"You are Claude Code, Anthropic's official CLI for Claude", // 标准版 & Agent SDK 版(含 running within...)
|
||||||
|
"You are a Claude agent, built on Anthropic's Claude Agent SDK", // Agent SDK 变体
|
||||||
|
"You are a file search specialist for Claude Code", // Explore Agent 版
|
||||||
|
"You are a helpful AI assistant tasked with summarizing conversations", // Compact 版
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// allowedHeaders 白名单headers(参考CRS项目)
|
// allowedHeaders 白名单headers(参考CRS项目)
|
||||||
@@ -355,17 +366,8 @@ func (s *GatewayService) SelectAccountForModelWithExclusions(ctx context.Context
|
|||||||
return s.selectAccountWithMixedScheduling(ctx, groupID, sessionHash, requestedModel, excludedIDs, platform)
|
return s.selectAccountWithMixedScheduling(ctx, groupID, sessionHash, requestedModel, excludedIDs, platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制平台模式:优先按分组查找,找不到再查全部该平台账户
|
|
||||||
if hasForcePlatform && groupID != nil {
|
|
||||||
account, err := s.selectAccountForModelWithPlatform(ctx, groupID, sessionHash, requestedModel, excludedIDs, platform)
|
|
||||||
if err == nil {
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
// 分组中找不到,回退查询全部该平台账户
|
|
||||||
groupID = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// antigravity 分组、强制平台模式或无分组使用单平台选择
|
// antigravity 分组、强制平台模式或无分组使用单平台选择
|
||||||
|
// 注意:强制平台模式也必须遵守分组限制,不再回退到全平台查询
|
||||||
return s.selectAccountForModelWithPlatform(ctx, groupID, sessionHash, requestedModel, excludedIDs, platform)
|
return s.selectAccountForModelWithPlatform(ctx, groupID, sessionHash, requestedModel, excludedIDs, platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +445,8 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
|||||||
accountID, err := s.cache.GetSessionAccountID(ctx, sessionHash)
|
accountID, err := s.cache.GetSessionAccountID(ctx, sessionHash)
|
||||||
if err == nil && accountID > 0 && !isExcluded(accountID) {
|
if err == nil && accountID > 0 && !isExcluded(accountID) {
|
||||||
account, err := s.accountRepo.GetByID(ctx, accountID)
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
||||||
if err == nil && s.isAccountAllowedForPlatform(account, platform, useMixed) &&
|
if err == nil && s.isAccountInGroup(account, groupID) &&
|
||||||
|
s.isAccountAllowedForPlatform(account, platform, useMixed) &&
|
||||||
account.IsSchedulable() &&
|
account.IsSchedulable() &&
|
||||||
(requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
(requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
||||||
result, err := s.tryAcquireAccountSlot(ctx, accountID, account.Concurrency)
|
result, err := s.tryAcquireAccountSlot(ctx, accountID, account.Concurrency)
|
||||||
@@ -660,9 +663,7 @@ func (s *GatewayService) listSchedulableAccounts(ctx context.Context, groupID *i
|
|||||||
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
||||||
} else if groupID != nil {
|
} else if groupID != nil {
|
||||||
accounts, err = s.accountRepo.ListSchedulableByGroupIDAndPlatform(ctx, *groupID, platform)
|
accounts, err = s.accountRepo.ListSchedulableByGroupIDAndPlatform(ctx, *groupID, platform)
|
||||||
if err == nil && len(accounts) == 0 && hasForcePlatform {
|
// 分组内无账号则返回空列表,由上层处理错误,不再回退到全平台查询
|
||||||
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
||||||
}
|
}
|
||||||
@@ -685,6 +686,23 @@ func (s *GatewayService) isAccountAllowedForPlatform(account *Account, platform
|
|||||||
return account.Platform == platform
|
return account.Platform == platform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAccountInGroup checks if the account belongs to the specified group.
|
||||||
|
// Returns true if groupID is nil (no group restriction) or account belongs to the group.
|
||||||
|
func (s *GatewayService) isAccountInGroup(account *Account, groupID *int64) bool {
|
||||||
|
if groupID == nil {
|
||||||
|
return true // 无分组限制
|
||||||
|
}
|
||||||
|
if account == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, ag := range account.AccountGroups {
|
||||||
|
if ag.GroupID == *groupID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *GatewayService) tryAcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int) (*AcquireResult, error) {
|
func (s *GatewayService) tryAcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int) (*AcquireResult, error) {
|
||||||
if s.concurrencyService == nil {
|
if s.concurrencyService == nil {
|
||||||
return &AcquireResult{Acquired: true, ReleaseFunc: func() {}}, nil
|
return &AcquireResult{Acquired: true, ReleaseFunc: func() {}}, nil
|
||||||
@@ -723,8 +741,8 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
|
|||||||
if err == nil && accountID > 0 {
|
if err == nil && accountID > 0 {
|
||||||
if _, excluded := excludedIDs[accountID]; !excluded {
|
if _, excluded := excludedIDs[accountID]; !excluded {
|
||||||
account, err := s.accountRepo.GetByID(ctx, accountID)
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
||||||
// 检查账号平台是否匹配(确保粘性会话不会跨平台)
|
// 检查账号分组归属和平台匹配(确保粘性会话不会跨分组或跨平台)
|
||||||
if err == nil && account.Platform == platform && account.IsSchedulable() && (requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
if err == nil && s.isAccountInGroup(account, groupID) && account.Platform == platform && account.IsSchedulable() && (requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
||||||
if err := s.cache.RefreshSessionTTL(ctx, sessionHash, stickySessionTTL); err != nil {
|
if err := s.cache.RefreshSessionTTL(ctx, sessionHash, stickySessionTTL); err != nil {
|
||||||
log.Printf("refresh session ttl failed: session=%s err=%v", sessionHash, err)
|
log.Printf("refresh session ttl failed: session=%s err=%v", sessionHash, err)
|
||||||
}
|
}
|
||||||
@@ -812,8 +830,8 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
|
|||||||
if err == nil && accountID > 0 {
|
if err == nil && accountID > 0 {
|
||||||
if _, excluded := excludedIDs[accountID]; !excluded {
|
if _, excluded := excludedIDs[accountID]; !excluded {
|
||||||
account, err := s.accountRepo.GetByID(ctx, accountID)
|
account, err := s.accountRepo.GetByID(ctx, accountID)
|
||||||
// 检查账号是否有效:原生平台直接匹配,antigravity 需要启用混合调度
|
// 检查账号分组归属和有效性:原生平台直接匹配,antigravity 需要启用混合调度
|
||||||
if err == nil && account.IsSchedulable() && (requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
if err == nil && s.isAccountInGroup(account, groupID) && account.IsSchedulable() && (requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
||||||
if account.Platform == nativePlatform || (account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled()) {
|
if account.Platform == nativePlatform || (account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled()) {
|
||||||
if err := s.cache.RefreshSessionTTL(ctx, sessionHash, stickySessionTTL); err != nil {
|
if err := s.cache.RefreshSessionTTL(ctx, sessionHash, stickySessionTTL); err != nil {
|
||||||
log.Printf("refresh session ttl failed: session=%s err=%v", sessionHash, err)
|
log.Printf("refresh session ttl failed: session=%s err=%v", sessionHash, err)
|
||||||
@@ -1013,15 +1031,15 @@ func isClaudeCodeClient(userAgent string, metadataUserID string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// systemIncludesClaudeCodePrompt 检查 system 中是否已包含 Claude Code 提示词
|
// systemIncludesClaudeCodePrompt 检查 system 中是否已包含 Claude Code 提示词
|
||||||
// 支持 string 和 []any 两种格式
|
// 使用前缀匹配支持多种变体(标准版、Agent SDK 版等)
|
||||||
func systemIncludesClaudeCodePrompt(system any) bool {
|
func systemIncludesClaudeCodePrompt(system any) bool {
|
||||||
switch v := system.(type) {
|
switch v := system.(type) {
|
||||||
case string:
|
case string:
|
||||||
return v == claudeCodeSystemPrompt
|
return hasClaudeCodePrefix(v)
|
||||||
case []any:
|
case []any:
|
||||||
for _, item := range v {
|
for _, item := range v {
|
||||||
if m, ok := item.(map[string]any); ok {
|
if m, ok := item.(map[string]any); ok {
|
||||||
if text, ok := m["text"].(string); ok && text == claudeCodeSystemPrompt {
|
if text, ok := m["text"].(string); ok && hasClaudeCodePrefix(text) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1030,6 +1048,16 @@ func systemIncludesClaudeCodePrompt(system any) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasClaudeCodePrefix 检查文本是否以 Claude Code 提示词的特征前缀开头
|
||||||
|
func hasClaudeCodePrefix(text string) bool {
|
||||||
|
for _, prefix := range claudeCodePromptPrefixes {
|
||||||
|
if strings.HasPrefix(text, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// injectClaudeCodePrompt 在 system 开头注入 Claude Code 提示词
|
// injectClaudeCodePrompt 在 system 开头注入 Claude Code 提示词
|
||||||
// 处理 null、字符串、数组三种格式
|
// 处理 null、字符串、数组三种格式
|
||||||
func injectClaudeCodePrompt(body []byte, system any) []byte {
|
func injectClaudeCodePrompt(body []byte, system any) []byte {
|
||||||
@@ -1073,6 +1101,124 @@ func injectClaudeCodePrompt(body []byte, system any) []byte {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enforceCacheControlLimit 强制执行 cache_control 块数量限制(最多 4 个)
|
||||||
|
// 超限时优先从 messages 中移除 cache_control,保护 system 中的缓存控制
|
||||||
|
func enforceCacheControlLimit(body []byte) []byte {
|
||||||
|
var data map[string]any
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前 cache_control 块数量
|
||||||
|
count := countCacheControlBlocks(data)
|
||||||
|
if count <= maxCacheControlBlocks {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超限:优先从 messages 中移除,再从 system 中移除
|
||||||
|
for count > maxCacheControlBlocks {
|
||||||
|
if removeCacheControlFromMessages(data) {
|
||||||
|
count--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if removeCacheControlFromSystem(data) {
|
||||||
|
count--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// countCacheControlBlocks 统计 system 和 messages 中的 cache_control 块数量
|
||||||
|
func countCacheControlBlocks(data map[string]any) int {
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
// 统计 system 中的块
|
||||||
|
if system, ok := data["system"].([]any); ok {
|
||||||
|
for _, item := range system {
|
||||||
|
if m, ok := item.(map[string]any); ok {
|
||||||
|
if _, has := m["cache_control"]; has {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计 messages 中的块
|
||||||
|
if messages, ok := data["messages"].([]any); ok {
|
||||||
|
for _, msg := range messages {
|
||||||
|
if msgMap, ok := msg.(map[string]any); ok {
|
||||||
|
if content, ok := msgMap["content"].([]any); ok {
|
||||||
|
for _, item := range content {
|
||||||
|
if m, ok := item.(map[string]any); ok {
|
||||||
|
if _, has := m["cache_control"]; has {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeCacheControlFromMessages 从 messages 中移除一个 cache_control(从头开始)
|
||||||
|
// 返回 true 表示成功移除,false 表示没有可移除的
|
||||||
|
func removeCacheControlFromMessages(data map[string]any) bool {
|
||||||
|
messages, ok := data["messages"].([]any)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, msg := range messages {
|
||||||
|
msgMap, ok := msg.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content, ok := msgMap["content"].([]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, item := range content {
|
||||||
|
if m, ok := item.(map[string]any); ok {
|
||||||
|
if _, has := m["cache_control"]; has {
|
||||||
|
delete(m, "cache_control")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeCacheControlFromSystem 从 system 中移除一个 cache_control(从尾部开始,保护注入的 prompt)
|
||||||
|
// 返回 true 表示成功移除,false 表示没有可移除的
|
||||||
|
func removeCacheControlFromSystem(data map[string]any) bool {
|
||||||
|
system, ok := data["system"].([]any)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从尾部开始移除,保护开头注入的 Claude Code prompt
|
||||||
|
for i := len(system) - 1; i >= 0; i-- {
|
||||||
|
if m, ok := system[i].(map[string]any); ok {
|
||||||
|
if _, has := m["cache_control"]; has {
|
||||||
|
delete(m, "cache_control")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Forward 转发请求到Claude API
|
// Forward 转发请求到Claude API
|
||||||
func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest) (*ForwardResult, error) {
|
func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest) (*ForwardResult, error) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@@ -1093,6 +1239,9 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
|||||||
body = injectClaudeCodePrompt(body, parsed.System)
|
body = injectClaudeCodePrompt(body, parsed.System)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 强制执行 cache_control 块数量限制(最多 4 个)
|
||||||
|
body = enforceCacheControlLimit(body)
|
||||||
|
|
||||||
// 应用模型映射(仅对apikey类型账号)
|
// 应用模型映射(仅对apikey类型账号)
|
||||||
originalModel := reqModel
|
originalModel := reqModel
|
||||||
if account.Type == AccountTypeAPIKey {
|
if account.Type == AccountTypeAPIKey {
|
||||||
@@ -2003,6 +2152,7 @@ type RecordUsageInput struct {
|
|||||||
User *User
|
User *User
|
||||||
Account *Account
|
Account *Account
|
||||||
Subscription *UserSubscription // 可选:订阅信息
|
Subscription *UserSubscription // 可选:订阅信息
|
||||||
|
UserAgent string // 请求的 User-Agent
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordUsage 记录使用量并扣费(或更新订阅用量)
|
// RecordUsage 记录使用量并扣费(或更新订阅用量)
|
||||||
@@ -2088,6 +2238,11 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
|
|||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加 UserAgent
|
||||||
|
if input.UserAgent != "" {
|
||||||
|
usageLog.UserAgent = &input.UserAgent
|
||||||
|
}
|
||||||
|
|
||||||
// 添加分组和订阅关联
|
// 添加分组和订阅关联
|
||||||
if apiKey.GroupID != nil {
|
if apiKey.GroupID != nil {
|
||||||
usageLog.GroupID = apiKey.GroupID
|
usageLog.GroupID = apiKey.GroupID
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ func (m *mockAccountRepoForGemini) SetError(ctx context.Context, id int64, error
|
|||||||
func (m *mockAccountRepoForGemini) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
func (m *mockAccountRepoForGemini) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (m *mockAccountRepoForGemini) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
func (m *mockAccountRepoForGemini) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
func (m *mockAccountRepoForGemini) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1092,6 +1092,7 @@ type OpenAIRecordUsageInput struct {
|
|||||||
User *User
|
User *User
|
||||||
Account *Account
|
Account *Account
|
||||||
Subscription *UserSubscription
|
Subscription *UserSubscription
|
||||||
|
UserAgent string // 请求的 User-Agent
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordUsage records usage and deducts balance
|
// RecordUsage records usage and deducts balance
|
||||||
@@ -1161,6 +1162,11 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
|||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加 UserAgent
|
||||||
|
if input.UserAgent != "" {
|
||||||
|
usageLog.UserAgent = &input.UserAgent
|
||||||
|
}
|
||||||
|
|
||||||
if apiKey.GroupID != nil {
|
if apiKey.GroupID != nil {
|
||||||
usageLog.GroupID = apiKey.GroupID
|
usageLog.GroupID = apiKey.GroupID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type UsageLog struct {
|
|||||||
Stream bool
|
Stream bool
|
||||||
DurationMs *int
|
DurationMs *int
|
||||||
FirstTokenMs *int
|
FirstTokenMs *int
|
||||||
|
UserAgent *string
|
||||||
|
|
||||||
// 图片生成字段
|
// 图片生成字段
|
||||||
ImageCount int
|
ImageCount int
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ func ProvideTokenRefreshService(
|
|||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideAccountExpiryService creates and starts AccountExpiryService.
|
||||||
|
func ProvideAccountExpiryService(accountRepo AccountRepository) *AccountExpiryService {
|
||||||
|
svc := NewAccountExpiryService(accountRepo, time.Minute)
|
||||||
|
svc.Start()
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
// ProvideTimingWheelService creates and starts TimingWheelService
|
// ProvideTimingWheelService creates and starts TimingWheelService
|
||||||
func ProvideTimingWheelService() *TimingWheelService {
|
func ProvideTimingWheelService() *TimingWheelService {
|
||||||
svc := NewTimingWheelService()
|
svc := NewTimingWheelService()
|
||||||
@@ -110,6 +117,7 @@ var ProviderSet = wire.NewSet(
|
|||||||
NewCRSSyncService,
|
NewCRSSyncService,
|
||||||
ProvideUpdateService,
|
ProvideUpdateService,
|
||||||
ProvideTokenRefreshService,
|
ProvideTokenRefreshService,
|
||||||
|
ProvideAccountExpiryService,
|
||||||
ProvideTimingWheelService,
|
ProvideTimingWheelService,
|
||||||
ProvideDeferredService,
|
ProvideDeferredService,
|
||||||
NewAntigravityQuotaFetcher,
|
NewAntigravityQuotaFetcher,
|
||||||
|
|||||||
10
backend/migrations/028_add_usage_logs_user_agent.sql
Normal file
10
backend/migrations/028_add_usage_logs_user_agent.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- Add user_agent column to usage_logs table
|
||||||
|
-- Records the User-Agent header from API requests for analytics and debugging
|
||||||
|
|
||||||
|
ALTER TABLE usage_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS user_agent VARCHAR(512);
|
||||||
|
|
||||||
|
-- Optional: Add index for user_agent queries (uncomment if needed for analytics)
|
||||||
|
-- CREATE INDEX IF NOT EXISTS idx_usage_logs_user_agent ON usage_logs(user_agent);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN usage_logs.user_agent IS 'User-Agent header from the API request';
|
||||||
10
backend/migrations/030_add_account_expires_at.sql
Normal file
10
backend/migrations/030_add_account_expires_at.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- Add expires_at for account expiration configuration
|
||||||
|
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS expires_at timestamptz;
|
||||||
|
-- Document expires_at meaning
|
||||||
|
COMMENT ON COLUMN accounts.expires_at IS 'Account expiration time (NULL means no expiration).';
|
||||||
|
-- Add auto_pause_on_expired for account expiration scheduling control
|
||||||
|
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS auto_pause_on_expired boolean NOT NULL DEFAULT true;
|
||||||
|
-- Document auto_pause_on_expired meaning
|
||||||
|
COMMENT ON COLUMN accounts.auto_pause_on_expired IS 'Auto pause scheduling when account expires.';
|
||||||
|
-- Ensure existing accounts are enabled by default
|
||||||
|
UPDATE accounts SET auto_pause_on_expired = true;
|
||||||
@@ -123,3 +123,17 @@ GEMINI_OAUTH_SCOPES=
|
|||||||
# Example:
|
# Example:
|
||||||
# GEMINI_QUOTA_POLICY={"tiers":{"LEGACY":{"pro_rpd":50,"flash_rpd":1500,"cooldown_minutes":30},"PRO":{"pro_rpd":1500,"flash_rpd":4000,"cooldown_minutes":5},"ULTRA":{"pro_rpd":2000,"flash_rpd":0,"cooldown_minutes":5}}}
|
# GEMINI_QUOTA_POLICY={"tiers":{"LEGACY":{"pro_rpd":50,"flash_rpd":1500,"cooldown_minutes":30},"PRO":{"pro_rpd":1500,"flash_rpd":4000,"cooldown_minutes":5},"ULTRA":{"pro_rpd":2000,"flash_rpd":0,"cooldown_minutes":5}}}
|
||||||
GEMINI_QUOTA_POLICY=
|
GEMINI_QUOTA_POLICY=
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Update Configuration (在线更新配置)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Proxy URL for accessing GitHub (used for online updates and pricing data)
|
||||||
|
# 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取)
|
||||||
|
# Supports: http, https, socks5, socks5h
|
||||||
|
# Examples:
|
||||||
|
# HTTP proxy: http://127.0.0.1:7890
|
||||||
|
# SOCKS5 proxy: socks5://127.0.0.1:1080
|
||||||
|
# With authentication: http://user:pass@proxy.example.com:8080
|
||||||
|
# Leave empty for direct connection (recommended for overseas servers)
|
||||||
|
# 留空表示直连(适用于海外服务器)
|
||||||
|
UPDATE_PROXY_URL=
|
||||||
|
|||||||
@@ -388,3 +388,18 @@ gemini:
|
|||||||
# Cooldown time (minutes) after hitting quota
|
# Cooldown time (minutes) after hitting quota
|
||||||
# 达到配额后的冷却时间(分钟)
|
# 达到配额后的冷却时间(分钟)
|
||||||
cooldown_minutes: 5
|
cooldown_minutes: 5
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Update Configuration (在线更新配置)
|
||||||
|
# =============================================================================
|
||||||
|
update:
|
||||||
|
# Proxy URL for accessing GitHub (used for online updates and pricing data)
|
||||||
|
# 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取)
|
||||||
|
# Supports: http, https, socks5, socks5h
|
||||||
|
# Examples:
|
||||||
|
# - HTTP proxy: "http://127.0.0.1:7890"
|
||||||
|
# - SOCKS5 proxy: "socks5://127.0.0.1:1080"
|
||||||
|
# - With authentication: "http://user:pass@proxy.example.com:8080"
|
||||||
|
# Leave empty for direct connection (recommended for overseas servers)
|
||||||
|
# 留空表示直连(适用于海外服务器)
|
||||||
|
proxy_url: ""
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ services:
|
|||||||
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
||||||
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
|
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
|
||||||
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
|
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
|
||||||
|
|
||||||
|
# =======================================================================
|
||||||
|
# Update Configuration (在线更新配置)
|
||||||
|
# =======================================================================
|
||||||
|
# Proxy for accessing GitHub (online updates + pricing data)
|
||||||
|
# Examples: http://host:port, socks5://host:port
|
||||||
|
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -1012,7 +1012,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Temp Unschedulable Rules -->
|
<!-- Temp Unschedulable Rules -->
|
||||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
||||||
@@ -1213,46 +1213,81 @@
|
|||||||
<p class="input-hint">{{ t('admin.accounts.priorityHint') }}</p>
|
<p class="input-hint">{{ t('admin.accounts.priorityHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
|
<label class="input-label">{{ t('admin.accounts.expiresAt') }}</label>
|
||||||
|
<input v-model="expiresAtInput" type="datetime-local" class="input" />
|
||||||
|
<p class="input-hint">{{ t('admin.accounts.expiresAtHint') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mixed Scheduling (only for antigravity accounts) -->
|
<div>
|
||||||
<div v-if="form.platform === 'antigravity'" class="flex items-center gap-2">
|
<div class="flex items-center justify-between">
|
||||||
<label class="flex cursor-pointer items-center gap-2">
|
<div>
|
||||||
<input
|
<label class="input-label mb-0">{{
|
||||||
type="checkbox"
|
t('admin.accounts.autoPauseOnExpired')
|
||||||
v-model="mixedScheduling"
|
}}</label>
|
||||||
class="h-4 w-4 rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
/>
|
{{ t('admin.accounts.autoPauseOnExpiredDesc') }}
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
</p>
|
||||||
{{ t('admin.accounts.mixedScheduling') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div class="group relative">
|
|
||||||
<span
|
|
||||||
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
|
||||||
>
|
|
||||||
?
|
|
||||||
</span>
|
|
||||||
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
|
||||||
<div
|
|
||||||
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
|
||||||
<div
|
|
||||||
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="autoPauseOnExpired = !autoPauseOnExpired"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
autoPauseOnExpired ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
autoPauseOnExpired ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group Selection - 仅标准模式显示 -->
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
<GroupSelector
|
<!-- Mixed Scheduling (only for antigravity accounts) -->
|
||||||
v-if="!authStore.isSimpleMode"
|
<div v-if="form.platform === 'antigravity'" class="flex items-center gap-2">
|
||||||
v-model="form.group_ids"
|
<label class="flex cursor-pointer items-center gap-2">
|
||||||
:groups="groups"
|
<input
|
||||||
:platform="form.platform"
|
type="checkbox"
|
||||||
:mixed-scheduling="mixedScheduling"
|
v-model="mixedScheduling"
|
||||||
data-tour="account-form-groups"
|
class="h-4 w-4 rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
||||||
/>
|
/>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('admin.accounts.mixedScheduling') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="group relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
||||||
|
<div
|
||||||
|
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group Selection - 仅标准模式显示 -->
|
||||||
|
<GroupSelector
|
||||||
|
v-if="!authStore.isSimpleMode"
|
||||||
|
v-model="form.group_ids"
|
||||||
|
:groups="groups"
|
||||||
|
:platform="form.platform"
|
||||||
|
:mixed-scheduling="mixedScheduling"
|
||||||
|
data-tour="account-form-groups"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -1598,6 +1633,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
||||||
|
|
||||||
// Type for exposed OAuthAuthorizationFlow component
|
// Type for exposed OAuthAuthorizationFlow component
|
||||||
@@ -1713,6 +1749,7 @@ const customErrorCodesEnabled = ref(false)
|
|||||||
const selectedErrorCodes = ref<number[]>([])
|
const selectedErrorCodes = ref<number[]>([])
|
||||||
const customErrorCodeInput = ref<number | null>(null)
|
const customErrorCodeInput = ref<number | null>(null)
|
||||||
const interceptWarmupRequests = ref(false)
|
const interceptWarmupRequests = ref(false)
|
||||||
|
const autoPauseOnExpired = ref(true)
|
||||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||||
const tempUnschedEnabled = ref(false)
|
const tempUnschedEnabled = ref(false)
|
||||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||||
@@ -1795,7 +1832,8 @@ const form = reactive({
|
|||||||
proxy_id: null as number | null,
|
proxy_id: null as number | null,
|
||||||
concurrency: 10,
|
concurrency: 10,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
group_ids: [] as number[]
|
group_ids: [] as number[],
|
||||||
|
expires_at: null as number | null
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper to check if current type needs OAuth flow
|
// Helper to check if current type needs OAuth flow
|
||||||
@@ -1805,6 +1843,13 @@ const isManualInputMethod = computed(() => {
|
|||||||
return oauthFlowRef.value?.inputMethod === 'manual'
|
return oauthFlowRef.value?.inputMethod === 'manual'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const expiresAtInput = computed({
|
||||||
|
get: () => formatDateTimeLocal(form.expires_at),
|
||||||
|
set: (value: string) => {
|
||||||
|
form.expires_at = parseDateTimeLocal(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const canExchangeCode = computed(() => {
|
const canExchangeCode = computed(() => {
|
||||||
const authCode = oauthFlowRef.value?.authCode || ''
|
const authCode = oauthFlowRef.value?.authCode || ''
|
||||||
if (form.platform === 'openai') {
|
if (form.platform === 'openai') {
|
||||||
@@ -2055,6 +2100,7 @@ const resetForm = () => {
|
|||||||
form.concurrency = 10
|
form.concurrency = 10
|
||||||
form.priority = 1
|
form.priority = 1
|
||||||
form.group_ids = []
|
form.group_ids = []
|
||||||
|
form.expires_at = null
|
||||||
accountCategory.value = 'oauth-based'
|
accountCategory.value = 'oauth-based'
|
||||||
addMethod.value = 'oauth'
|
addMethod.value = 'oauth'
|
||||||
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
||||||
@@ -2066,6 +2112,7 @@ const resetForm = () => {
|
|||||||
selectedErrorCodes.value = []
|
selectedErrorCodes.value = []
|
||||||
customErrorCodeInput.value = null
|
customErrorCodeInput.value = null
|
||||||
interceptWarmupRequests.value = false
|
interceptWarmupRequests.value = false
|
||||||
|
autoPauseOnExpired.value = true
|
||||||
tempUnschedEnabled.value = false
|
tempUnschedEnabled.value = false
|
||||||
tempUnschedRules.value = []
|
tempUnschedRules.value = []
|
||||||
geminiOAuthType.value = 'code_assist'
|
geminiOAuthType.value = 'code_assist'
|
||||||
@@ -2133,7 +2180,6 @@ const handleSubmit = async () => {
|
|||||||
if (interceptWarmupRequests.value) {
|
if (interceptWarmupRequests.value) {
|
||||||
credentials.intercept_warmup_requests = true
|
credentials.intercept_warmup_requests = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applyTempUnschedConfig(credentials)) {
|
if (!applyTempUnschedConfig(credentials)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2144,7 +2190,8 @@ const handleSubmit = async () => {
|
|||||||
try {
|
try {
|
||||||
await adminAPI.accounts.create({
|
await adminAPI.accounts.create({
|
||||||
...form,
|
...form,
|
||||||
group_ids: form.group_ids
|
group_ids: form.group_ids,
|
||||||
|
auto_pause_on_expired: autoPauseOnExpired.value
|
||||||
})
|
})
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||||
emit('created')
|
emit('created')
|
||||||
@@ -2182,6 +2229,9 @@ const handleGenerateUrl = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatDateTimeLocal = formatDateTimeLocalInput
|
||||||
|
const parseDateTimeLocal = parseDateTimeLocalInput
|
||||||
|
|
||||||
// Create account and handle success/failure
|
// Create account and handle success/failure
|
||||||
const createAccountAndFinish = async (
|
const createAccountAndFinish = async (
|
||||||
platform: AccountPlatform,
|
platform: AccountPlatform,
|
||||||
@@ -2202,7 +2252,9 @@ const createAccountAndFinish = async (
|
|||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
group_ids: form.group_ids
|
group_ids: form.group_ids,
|
||||||
|
expires_at: form.expires_at,
|
||||||
|
auto_pause_on_expired: autoPauseOnExpired.value
|
||||||
})
|
})
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||||
emit('created')
|
emit('created')
|
||||||
@@ -2416,7 +2468,8 @@ const handleCookieAuth = async (sessionKey: string) => {
|
|||||||
extra,
|
extra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
priority: form.priority
|
priority: form.priority,
|
||||||
|
auto_pause_on_expired: autoPauseOnExpired.value
|
||||||
})
|
})
|
||||||
|
|
||||||
successCount++
|
successCount++
|
||||||
|
|||||||
@@ -365,7 +365,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Temp Unschedulable Rules -->
|
<!-- Temp Unschedulable Rules -->
|
||||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
||||||
@@ -565,39 +565,74 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
<div>
|
<label class="input-label">{{ t('admin.accounts.expiresAt') }}</label>
|
||||||
<label class="input-label">{{ t('common.status') }}</label>
|
<input v-model="expiresAtInput" type="datetime-local" class="input" />
|
||||||
<Select v-model="form.status" :options="statusOptions" />
|
<p class="input-hint">{{ t('admin.accounts.expiresAtHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mixed Scheduling (only for antigravity accounts, read-only in edit mode) -->
|
<div>
|
||||||
<div v-if="account?.platform === 'antigravity'" class="flex items-center gap-2">
|
<div class="flex items-center justify-between">
|
||||||
<label class="flex cursor-not-allowed items-center gap-2 opacity-60">
|
<div>
|
||||||
<input
|
<label class="input-label mb-0">{{
|
||||||
type="checkbox"
|
t('admin.accounts.autoPauseOnExpired')
|
||||||
v-model="mixedScheduling"
|
}}</label>
|
||||||
disabled
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
class="h-4 w-4 cursor-not-allowed rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
{{ t('admin.accounts.autoPauseOnExpiredDesc') }}
|
||||||
/>
|
</p>
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
</div>
|
||||||
{{ t('admin.accounts.mixedScheduling') }}
|
<button
|
||||||
</span>
|
type="button"
|
||||||
</label>
|
@click="autoPauseOnExpired = !autoPauseOnExpired"
|
||||||
<div class="group relative">
|
:class="[
|
||||||
<span
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
autoPauseOnExpired ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
?
|
<span
|
||||||
</span>
|
:class="[
|
||||||
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
<div
|
autoPauseOnExpired ? 'translate-x-5' : 'translate-x-0'
|
||||||
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
]"
|
||||||
>
|
/>
|
||||||
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('common.status') }}</label>
|
||||||
|
<Select v-model="form.status" :options="statusOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mixed Scheduling (only for antigravity accounts, read-only in edit mode) -->
|
||||||
|
<div v-if="account?.platform === 'antigravity'" class="flex items-center gap-2">
|
||||||
|
<label class="flex cursor-not-allowed items-center gap-2 opacity-60">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="mixedScheduling"
|
||||||
|
disabled
|
||||||
|
class="h-4 w-4 cursor-not-allowed rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('admin.accounts.mixedScheduling') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="group relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||||
></div>
|
>
|
||||||
|
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
||||||
|
<div
|
||||||
|
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -666,6 +701,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import {
|
import {
|
||||||
getPresetMappingsByPlatform,
|
getPresetMappingsByPlatform,
|
||||||
commonErrorCodes,
|
commonErrorCodes,
|
||||||
@@ -721,6 +757,7 @@ const customErrorCodesEnabled = ref(false)
|
|||||||
const selectedErrorCodes = ref<number[]>([])
|
const selectedErrorCodes = ref<number[]>([])
|
||||||
const customErrorCodeInput = ref<number | null>(null)
|
const customErrorCodeInput = ref<number | null>(null)
|
||||||
const interceptWarmupRequests = ref(false)
|
const interceptWarmupRequests = ref(false)
|
||||||
|
const autoPauseOnExpired = ref(false)
|
||||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||||
const tempUnschedEnabled = ref(false)
|
const tempUnschedEnabled = ref(false)
|
||||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||||
@@ -771,7 +808,8 @@ const form = reactive({
|
|||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
status: 'active' as 'active' | 'inactive',
|
status: 'active' as 'active' | 'inactive',
|
||||||
group_ids: [] as number[]
|
group_ids: [] as number[],
|
||||||
|
expires_at: null as number | null
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusOptions = computed(() => [
|
const statusOptions = computed(() => [
|
||||||
@@ -779,6 +817,13 @@ const statusOptions = computed(() => [
|
|||||||
{ value: 'inactive', label: t('common.inactive') }
|
{ value: 'inactive', label: t('common.inactive') }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const expiresAtInput = computed({
|
||||||
|
get: () => formatDateTimeLocal(form.expires_at),
|
||||||
|
set: (value: string) => {
|
||||||
|
form.expires_at = parseDateTimeLocal(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Watchers
|
// Watchers
|
||||||
watch(
|
watch(
|
||||||
() => props.account,
|
() => props.account,
|
||||||
@@ -791,10 +836,12 @@ watch(
|
|||||||
form.priority = newAccount.priority
|
form.priority = newAccount.priority
|
||||||
form.status = newAccount.status as 'active' | 'inactive'
|
form.status = newAccount.status as 'active' | 'inactive'
|
||||||
form.group_ids = newAccount.group_ids || []
|
form.group_ids = newAccount.group_ids || []
|
||||||
|
form.expires_at = newAccount.expires_at ?? null
|
||||||
|
|
||||||
// Load intercept warmup requests setting (applies to all account types)
|
// Load intercept warmup requests setting (applies to all account types)
|
||||||
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
||||||
interceptWarmupRequests.value = credentials?.intercept_warmup_requests === true
|
interceptWarmupRequests.value = credentials?.intercept_warmup_requests === true
|
||||||
|
autoPauseOnExpired.value = newAccount.auto_pause_on_expired === true
|
||||||
|
|
||||||
// Load mixed scheduling setting (only for antigravity accounts)
|
// Load mixed scheduling setting (only for antigravity accounts)
|
||||||
const extra = newAccount.extra as Record<string, unknown> | undefined
|
const extra = newAccount.extra as Record<string, unknown> | undefined
|
||||||
@@ -1042,6 +1089,9 @@ function toPositiveNumber(value: unknown) {
|
|||||||
return Math.trunc(num)
|
return Math.trunc(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatDateTimeLocal = formatDateTimeLocalInput
|
||||||
|
const parseDateTimeLocal = parseDateTimeLocalInput
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
emit('close')
|
emit('close')
|
||||||
@@ -1057,6 +1107,10 @@ const handleSubmit = async () => {
|
|||||||
if (updatePayload.proxy_id === null) {
|
if (updatePayload.proxy_id === null) {
|
||||||
updatePayload.proxy_id = 0
|
updatePayload.proxy_id = 0
|
||||||
}
|
}
|
||||||
|
if (form.expires_at === null) {
|
||||||
|
updatePayload.expires_at = 0
|
||||||
|
}
|
||||||
|
updatePayload.auto_pause_on_expired = autoPauseOnExpired.value
|
||||||
|
|
||||||
// For apikey type, handle credentials update
|
// For apikey type, handle credentials update
|
||||||
if (props.account.type === 'apikey') {
|
if (props.account.type === 'apikey') {
|
||||||
@@ -1097,7 +1151,6 @@ const handleSubmit = async () => {
|
|||||||
if (interceptWarmupRequests.value) {
|
if (interceptWarmupRequests.value) {
|
||||||
newCredentials.intercept_warmup_requests = true
|
newCredentials.intercept_warmup_requests = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applyTempUnschedConfig(newCredentials)) {
|
if (!applyTempUnschedConfig(newCredentials)) {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
return
|
return
|
||||||
@@ -1114,7 +1167,6 @@ const handleSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
delete newCredentials.intercept_warmup_requests
|
delete newCredentials.intercept_warmup_requests
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applyTempUnschedConfig(newCredentials)) {
|
if (!applyTempUnschedConfig(newCredentials)) {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -85,9 +85,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Account Filter -->
|
<!-- Account Filter -->
|
||||||
<div class="w-full sm:w-auto sm:min-w-[220px]">
|
<div ref="accountSearchRef" class="usage-filter-dropdown relative w-full sm:w-auto sm:min-w-[220px]">
|
||||||
<label class="input-label">{{ t('admin.usage.account') }}</label>
|
<label class="input-label">{{ t('admin.usage.account') }}</label>
|
||||||
<Select v-model="filters.account_id" :options="accountOptions" searchable @change="emitChange" />
|
<input
|
||||||
|
v-model="accountKeyword"
|
||||||
|
type="text"
|
||||||
|
class="input pr-8"
|
||||||
|
:placeholder="t('admin.usage.searchAccountPlaceholder')"
|
||||||
|
@input="debounceAccountSearch"
|
||||||
|
@focus="showAccountDropdown = true"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="filters.account_id"
|
||||||
|
type="button"
|
||||||
|
@click="clearAccount"
|
||||||
|
class="absolute right-2 top-9 text-gray-400"
|
||||||
|
aria-label="Clear account filter"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="showAccountDropdown && (accountResults.length > 0 || accountKeyword)"
|
||||||
|
class="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border bg-white shadow-lg dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="a in accountResults"
|
||||||
|
:key="a.id"
|
||||||
|
type="button"
|
||||||
|
@click="selectAccount(a)"
|
||||||
|
class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<span class="truncate">{{ a.name }}</span>
|
||||||
|
<span class="ml-2 text-xs text-gray-400">#{{ a.id }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stream Type Filter -->
|
<!-- Stream Type Filter -->
|
||||||
@@ -166,6 +197,7 @@ const filters = toRef(props, 'modelValue')
|
|||||||
|
|
||||||
const userSearchRef = ref<HTMLElement | null>(null)
|
const userSearchRef = ref<HTMLElement | null>(null)
|
||||||
const apiKeySearchRef = ref<HTMLElement | null>(null)
|
const apiKeySearchRef = ref<HTMLElement | null>(null)
|
||||||
|
const accountSearchRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const userKeyword = ref('')
|
const userKeyword = ref('')
|
||||||
const userResults = ref<SimpleUser[]>([])
|
const userResults = ref<SimpleUser[]>([])
|
||||||
@@ -177,9 +209,17 @@ const apiKeyResults = ref<SimpleApiKey[]>([])
|
|||||||
const showApiKeyDropdown = ref(false)
|
const showApiKeyDropdown = ref(false)
|
||||||
let apiKeySearchTimeout: ReturnType<typeof setTimeout> | null = null
|
let apiKeySearchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
interface SimpleAccount {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
const accountKeyword = ref('')
|
||||||
|
const accountResults = ref<SimpleAccount[]>([])
|
||||||
|
const showAccountDropdown = ref(false)
|
||||||
|
let accountSearchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
const modelOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allModels') }])
|
const modelOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allModels') }])
|
||||||
const groupOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allGroups') }])
|
const groupOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allGroups') }])
|
||||||
const accountOptions = ref<SelectOption[]>([{ value: null, label: t('admin.usage.allAccounts') }])
|
|
||||||
|
|
||||||
const streamTypeOptions = ref<SelectOption[]>([
|
const streamTypeOptions = ref<SelectOption[]>([
|
||||||
{ value: null, label: t('admin.usage.allTypes') },
|
{ value: null, label: t('admin.usage.allTypes') },
|
||||||
@@ -278,6 +318,37 @@ const onClearApiKey = () => {
|
|||||||
emitChange()
|
emitChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debounceAccountSearch = () => {
|
||||||
|
if (accountSearchTimeout) clearTimeout(accountSearchTimeout)
|
||||||
|
accountSearchTimeout = setTimeout(async () => {
|
||||||
|
if (!accountKeyword.value) {
|
||||||
|
accountResults.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.accounts.list(1, 20, { search: accountKeyword.value })
|
||||||
|
accountResults.value = res.items.map((a) => ({ id: a.id, name: a.name }))
|
||||||
|
} catch {
|
||||||
|
accountResults.value = []
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAccount = (a: SimpleAccount) => {
|
||||||
|
accountKeyword.value = a.name
|
||||||
|
showAccountDropdown.value = false
|
||||||
|
filters.value.account_id = a.id
|
||||||
|
emitChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAccount = () => {
|
||||||
|
accountKeyword.value = ''
|
||||||
|
accountResults.value = []
|
||||||
|
showAccountDropdown.value = false
|
||||||
|
filters.value.account_id = undefined
|
||||||
|
emitChange()
|
||||||
|
}
|
||||||
|
|
||||||
const onApiKeyFocus = () => {
|
const onApiKeyFocus = () => {
|
||||||
showApiKeyDropdown.value = true
|
showApiKeyDropdown.value = true
|
||||||
// Trigger search if no results yet
|
// Trigger search if no results yet
|
||||||
@@ -292,9 +363,11 @@ const onDocumentClick = (e: MouseEvent) => {
|
|||||||
|
|
||||||
const clickedInsideUser = userSearchRef.value?.contains(target) ?? false
|
const clickedInsideUser = userSearchRef.value?.contains(target) ?? false
|
||||||
const clickedInsideApiKey = apiKeySearchRef.value?.contains(target) ?? false
|
const clickedInsideApiKey = apiKeySearchRef.value?.contains(target) ?? false
|
||||||
|
const clickedInsideAccount = accountSearchRef.value?.contains(target) ?? false
|
||||||
|
|
||||||
if (!clickedInsideUser) showUserDropdown.value = false
|
if (!clickedInsideUser) showUserDropdown.value = false
|
||||||
if (!clickedInsideApiKey) showApiKeyDropdown.value = false
|
if (!clickedInsideApiKey) showApiKeyDropdown.value = false
|
||||||
|
if (!clickedInsideAccount) showAccountDropdown.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -333,20 +406,27 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filters.value.account_id,
|
||||||
|
(accountId) => {
|
||||||
|
if (!accountId) {
|
||||||
|
accountKeyword.value = ''
|
||||||
|
accountResults.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
document.addEventListener('click', onDocumentClick)
|
document.addEventListener('click', onDocumentClick)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [gs, ms, as] = await Promise.all([
|
const [gs, ms] = await Promise.all([
|
||||||
adminAPI.groups.list(1, 1000),
|
adminAPI.groups.list(1, 1000),
|
||||||
adminAPI.dashboard.getModelStats({ start_date: props.startDate, end_date: props.endDate }),
|
adminAPI.dashboard.getModelStats({ start_date: props.startDate, end_date: props.endDate })
|
||||||
adminAPI.accounts.list(1, 1000)
|
|
||||||
])
|
])
|
||||||
|
|
||||||
groupOptions.value.push(...gs.items.map((g: any) => ({ value: g.id, label: g.name })))
|
groupOptions.value.push(...gs.items.map((g: any) => ({ value: g.id, label: g.name })))
|
||||||
|
|
||||||
accountOptions.value.push(...as.items.map((a: any) => ({ value: a.id, label: a.name })))
|
|
||||||
|
|
||||||
const uniqueModels = new Set<string>()
|
const uniqueModels = new Set<string>()
|
||||||
ms.models?.forEach((s: any) => s.model && uniqueModels.add(s.model))
|
ms.models?.forEach((s: any) => s.model && uniqueModels.add(s.model))
|
||||||
modelOptions.value.push(
|
modelOptions.value.push(
|
||||||
|
|||||||
@@ -143,8 +143,8 @@
|
|||||||
>
|
>
|
||||||
<div class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800">
|
<div class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<div class="mb-2 border-b border-gray-700 pb-1.5">
|
<div>
|
||||||
<div class="text-xs font-semibold text-gray-300 mb-1">Token {{ t('usage.details') }}</div>
|
<div class="text-xs font-semibold text-gray-300 mb-1">{{ t('usage.tokenDetails') }}</div>
|
||||||
<div v-if="tokenTooltipData && tokenTooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
<div v-if="tokenTooltipData && tokenTooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
||||||
<span class="text-gray-400">{{ t('admin.usage.inputTokens') }}</span>
|
<span class="text-gray-400">{{ t('admin.usage.inputTokens') }}</span>
|
||||||
<span class="font-medium text-white">{{ tokenTooltipData.input_tokens.toLocaleString() }}</span>
|
<span class="font-medium text-white">{{ tokenTooltipData.input_tokens.toLocaleString() }}</span>
|
||||||
@@ -184,6 +184,27 @@
|
|||||||
>
|
>
|
||||||
<div class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800">
|
<div class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
|
<!-- Cost Breakdown -->
|
||||||
|
<div class="mb-2 border-b border-gray-700 pb-1.5">
|
||||||
|
<div class="text-xs font-semibold text-gray-300 mb-1">{{ t('usage.costDetails') }}</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.input_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.inputCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.input_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.output_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.outputCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.output_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.cache_creation_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.cacheCreationCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.cache_creation_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.cache_read_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.cacheReadCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.cache_read_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Rate and Summary -->
|
||||||
<div class="flex items-center justify-between gap-6">
|
<div class="flex items-center justify-between gap-6">
|
||||||
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
||||||
<span class="font-semibold text-blue-400">{{ (tooltipData?.rate_multiplier || 1).toFixed(2) }}x</span>
|
<span class="font-semibold text-blue-400">{{ (tooltipData?.rate_multiplier || 1).toFixed(2) }}x</span>
|
||||||
|
|||||||
@@ -105,10 +105,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Code Content -->
|
<!-- Code Content -->
|
||||||
<pre class="p-4 text-sm font-mono text-gray-100 overflow-x-auto">
|
<pre class="p-4 text-sm font-mono text-gray-100 overflow-x-auto"><code v-if="file.highlighted" v-html="file.highlighted"></code><code v-else v-text="file.content"></code></pre>
|
||||||
<code v-if="file.highlighted" v-html="file.highlighted"></code>
|
|
||||||
<code v-else v-text="file.content"></code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ t('dashboard.redeemCode') }}</p>
|
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ t('dashboard.redeemCode') }}</p>
|
||||||
<p class="text-xs text-gray-500 dark:text-dark-400">{{ t('dashboard.addBalance') }}</p>
|
<p class="text-xs text-gray-500 dark:text-dark-400">{{ t('dashboard.addBalanceWithCode') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<Icon
|
<Icon
|
||||||
name="chevronRight"
|
name="chevronRight"
|
||||||
|
|||||||
@@ -376,6 +376,8 @@ export default {
|
|||||||
usage: {
|
usage: {
|
||||||
title: 'Usage Records',
|
title: 'Usage Records',
|
||||||
description: 'View and analyze your API usage history',
|
description: 'View and analyze your API usage history',
|
||||||
|
costDetails: 'Cost Breakdown',
|
||||||
|
tokenDetails: 'Token Breakdown',
|
||||||
totalRequests: 'Total Requests',
|
totalRequests: 'Total Requests',
|
||||||
totalTokens: 'Total Tokens',
|
totalTokens: 'Total Tokens',
|
||||||
totalCost: 'Total Cost',
|
totalCost: 'Total Cost',
|
||||||
@@ -1009,6 +1011,7 @@ export default {
|
|||||||
groups: 'Groups',
|
groups: 'Groups',
|
||||||
usageWindows: 'Usage Windows',
|
usageWindows: 'Usage Windows',
|
||||||
lastUsed: 'Last Used',
|
lastUsed: 'Last Used',
|
||||||
|
expiresAt: 'Expires At',
|
||||||
actions: 'Actions'
|
actions: 'Actions'
|
||||||
},
|
},
|
||||||
tempUnschedulable: {
|
tempUnschedulable: {
|
||||||
@@ -1150,12 +1153,17 @@ export default {
|
|||||||
interceptWarmupRequests: 'Intercept Warmup Requests',
|
interceptWarmupRequests: 'Intercept Warmup Requests',
|
||||||
interceptWarmupRequestsDesc:
|
interceptWarmupRequestsDesc:
|
||||||
'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens',
|
'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens',
|
||||||
|
autoPauseOnExpired: 'Auto Pause On Expired',
|
||||||
|
autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires',
|
||||||
|
expired: 'Expired',
|
||||||
proxy: 'Proxy',
|
proxy: 'Proxy',
|
||||||
noProxy: 'No Proxy',
|
noProxy: 'No Proxy',
|
||||||
concurrency: 'Concurrency',
|
concurrency: 'Concurrency',
|
||||||
priority: 'Priority',
|
priority: 'Priority',
|
||||||
priorityHint: 'Higher priority accounts are used first',
|
priorityHint: 'Lower value accounts are used first',
|
||||||
higherPriorityFirst: 'Higher value means higher priority',
|
expiresAt: 'Expires At',
|
||||||
|
expiresAtHint: 'Leave empty for no expiration',
|
||||||
|
higherPriorityFirst: 'Lower value means higher priority',
|
||||||
mixedScheduling: 'Use in /v1/messages',
|
mixedScheduling: 'Use in /v1/messages',
|
||||||
mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling',
|
mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling',
|
||||||
mixedSchedulingTooltip:
|
mixedSchedulingTooltip:
|
||||||
@@ -1691,6 +1699,7 @@ export default {
|
|||||||
userFilter: 'User',
|
userFilter: 'User',
|
||||||
searchUserPlaceholder: 'Search user by email...',
|
searchUserPlaceholder: 'Search user by email...',
|
||||||
searchApiKeyPlaceholder: 'Search API key by name...',
|
searchApiKeyPlaceholder: 'Search API key by name...',
|
||||||
|
searchAccountPlaceholder: 'Search account by name...',
|
||||||
selectedUser: 'Selected',
|
selectedUser: 'Selected',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
@@ -1984,7 +1993,7 @@ export default {
|
|||||||
},
|
},
|
||||||
accountPriority: {
|
accountPriority: {
|
||||||
title: '⚖️ 4. Priority (Optional)',
|
title: '⚖️ 4. Priority (Optional)',
|
||||||
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set the account call priority.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📊 Priority Rules:</b><ul style="margin: 8px 0 0 16px;"><li>Higher number = higher priority</li><li>System uses high-priority accounts first</li><li>Same priority = random selection</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Use Case:</b> Set main account to high priority, backup accounts to low priority</p></div>',
|
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">Set the account call priority.</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📊 Priority Rules:</b><ul style="margin: 8px 0 0 16px;"><li>Lower number = higher priority</li><li>System uses low-value accounts first</li><li>Same priority = random selection</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Use Case:</b> Set main account to lower value, backup accounts to higher value</p></div>',
|
||||||
nextBtn: 'Next'
|
nextBtn: 'Next'
|
||||||
},
|
},
|
||||||
accountGroups: {
|
accountGroups: {
|
||||||
|
|||||||
@@ -373,6 +373,8 @@ export default {
|
|||||||
usage: {
|
usage: {
|
||||||
title: '使用记录',
|
title: '使用记录',
|
||||||
description: '查看和分析您的 API 使用历史',
|
description: '查看和分析您的 API 使用历史',
|
||||||
|
costDetails: '成本明细',
|
||||||
|
tokenDetails: 'Token 明细',
|
||||||
totalRequests: '总请求数',
|
totalRequests: '总请求数',
|
||||||
totalTokens: '总 Token',
|
totalTokens: '总 Token',
|
||||||
totalCost: '总消费',
|
totalCost: '总消费',
|
||||||
@@ -857,7 +859,7 @@ export default {
|
|||||||
accountsLabel: '指定账号',
|
accountsLabel: '指定账号',
|
||||||
accountsPlaceholder: '选择账号(留空则不限制)',
|
accountsPlaceholder: '选择账号(留空则不限制)',
|
||||||
priorityLabel: '优先级',
|
priorityLabel: '优先级',
|
||||||
priorityHint: '数值越高优先级越高,用于账号调度',
|
priorityHint: '数值越小优先级越高,用于账号调度',
|
||||||
statusLabel: '状态'
|
statusLabel: '状态'
|
||||||
},
|
},
|
||||||
exclusiveObj: {
|
exclusiveObj: {
|
||||||
@@ -1059,6 +1061,7 @@ export default {
|
|||||||
groups: '分组',
|
groups: '分组',
|
||||||
usageWindows: '用量窗口',
|
usageWindows: '用量窗口',
|
||||||
lastUsed: '最近使用',
|
lastUsed: '最近使用',
|
||||||
|
expiresAt: '过期时间',
|
||||||
actions: '操作'
|
actions: '操作'
|
||||||
},
|
},
|
||||||
clearRateLimit: '清除速率限制',
|
clearRateLimit: '清除速率限制',
|
||||||
@@ -1178,7 +1181,7 @@ export default {
|
|||||||
credentialsLabel: '凭证',
|
credentialsLabel: '凭证',
|
||||||
credentialsPlaceholder: '请输入 Cookie 或 API Key',
|
credentialsPlaceholder: '请输入 Cookie 或 API Key',
|
||||||
priorityLabel: '优先级',
|
priorityLabel: '优先级',
|
||||||
priorityHint: '数值越高优先级越高',
|
priorityHint: '数值越小优先级越高',
|
||||||
weightLabel: '权重',
|
weightLabel: '权重',
|
||||||
weightHint: '用于负载均衡的权重值',
|
weightHint: '用于负载均衡的权重值',
|
||||||
statusLabel: '状态'
|
statusLabel: '状态'
|
||||||
@@ -1284,12 +1287,17 @@ export default {
|
|||||||
errorCodeExists: '该错误码已被选中',
|
errorCodeExists: '该错误码已被选中',
|
||||||
interceptWarmupRequests: '拦截预热请求',
|
interceptWarmupRequests: '拦截预热请求',
|
||||||
interceptWarmupRequestsDesc: '启用后,标题生成等预热请求将返回 mock 响应,不消耗上游 token',
|
interceptWarmupRequestsDesc: '启用后,标题生成等预热请求将返回 mock 响应,不消耗上游 token',
|
||||||
|
autoPauseOnExpired: '过期自动暂停调度',
|
||||||
|
autoPauseOnExpiredDesc: '启用后,账号过期将自动暂停调度',
|
||||||
|
expired: '已过期',
|
||||||
proxy: '代理',
|
proxy: '代理',
|
||||||
noProxy: '无代理',
|
noProxy: '无代理',
|
||||||
concurrency: '并发数',
|
concurrency: '并发数',
|
||||||
priority: '优先级',
|
priority: '优先级',
|
||||||
priorityHint: '优先级越高的账号优先使用',
|
priorityHint: '优先级越小的账号优先使用',
|
||||||
higherPriorityFirst: '数值越高优先级越高',
|
expiresAt: '过期时间',
|
||||||
|
expiresAtHint: '留空表示不过期',
|
||||||
|
higherPriorityFirst: '数值越小优先级越高',
|
||||||
mixedScheduling: '在 /v1/messages 中使用',
|
mixedScheduling: '在 /v1/messages 中使用',
|
||||||
mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度',
|
mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度',
|
||||||
mixedSchedulingTooltip:
|
mixedSchedulingTooltip:
|
||||||
@@ -1836,6 +1844,7 @@ export default {
|
|||||||
userFilter: '用户',
|
userFilter: '用户',
|
||||||
searchUserPlaceholder: '按邮箱搜索用户...',
|
searchUserPlaceholder: '按邮箱搜索用户...',
|
||||||
searchApiKeyPlaceholder: '按名称搜索 API 密钥...',
|
searchApiKeyPlaceholder: '按名称搜索 API 密钥...',
|
||||||
|
searchAccountPlaceholder: '按名称搜索账号...',
|
||||||
selectedUser: '已选择',
|
selectedUser: '已选择',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
account: '账户',
|
account: '账户',
|
||||||
@@ -2126,7 +2135,7 @@ export default {
|
|||||||
},
|
},
|
||||||
accountPriority: {
|
accountPriority: {
|
||||||
title: '⚖️ 4. 优先级(可选)',
|
title: '⚖️ 4. 优先级(可选)',
|
||||||
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">设置账号的调用优先级。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📊 优先级规则:</b><ul style="margin: 8px 0 0 16px;"><li>数字越大,优先级越高</li><li>系统优先使用高优先级账号</li><li>相同优先级则随机选择</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 使用场景:</b>主账号设置高优先级,备用账号设置低优先级</p></div>',
|
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;">设置账号的调用优先级。</p><div style="padding: 8px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 13px; margin-bottom: 12px;"><b>📊 优先级规则:</b><ul style="margin: 8px 0 0 16px;"><li>数字越小,优先级越高</li><li>系统优先使用低数值账号</li><li>相同优先级则随机选择</li></ul></div><p style="padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 使用场景:</b>主账号设置低数值,备用账号设置高数值</p></div>',
|
||||||
nextBtn: '下一步'
|
nextBtn: '下一步'
|
||||||
},
|
},
|
||||||
accountGroups: {
|
accountGroups: {
|
||||||
|
|||||||
@@ -401,6 +401,8 @@ export interface Account {
|
|||||||
status: 'active' | 'inactive' | 'error'
|
status: 'active' | 'inactive' | 'error'
|
||||||
error_message: string | null
|
error_message: string | null
|
||||||
last_used_at: string | null
|
last_used_at: string | null
|
||||||
|
expires_at: number | null
|
||||||
|
auto_pause_on_expired: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
proxy?: Proxy
|
proxy?: Proxy
|
||||||
@@ -491,6 +493,8 @@ export interface CreateAccountRequest {
|
|||||||
concurrency?: number
|
concurrency?: number
|
||||||
priority?: number
|
priority?: number
|
||||||
group_ids?: number[]
|
group_ids?: number[]
|
||||||
|
expires_at?: number | null
|
||||||
|
auto_pause_on_expired?: boolean
|
||||||
confirm_mixed_channel_risk?: boolean
|
confirm_mixed_channel_risk?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,6 +510,8 @@ export interface UpdateAccountRequest {
|
|||||||
schedulable?: boolean
|
schedulable?: boolean
|
||||||
status?: 'active' | 'inactive'
|
status?: 'active' | 'inactive'
|
||||||
group_ids?: number[]
|
group_ids?: number[]
|
||||||
|
expires_at?: number | null
|
||||||
|
auto_pause_on_expired?: boolean
|
||||||
confirm_mixed_channel_risk?: boolean
|
confirm_mixed_channel_risk?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export function formatBytes(bytes: number, decimals: number = 2): string {
|
|||||||
* 格式化日期
|
* 格式化日期
|
||||||
* @param date 日期字符串或 Date 对象
|
* @param date 日期字符串或 Date 对象
|
||||||
* @param options Intl.DateTimeFormatOptions
|
* @param options Intl.DateTimeFormatOptions
|
||||||
|
* @param localeOverride 可选 locale 覆盖
|
||||||
* @returns 格式化后的日期字符串
|
* @returns 格式化后的日期字符串
|
||||||
*/
|
*/
|
||||||
export function formatDate(
|
export function formatDate(
|
||||||
@@ -108,14 +109,15 @@ export function formatDate(
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: false
|
hour12: false
|
||||||
}
|
},
|
||||||
|
localeOverride?: string
|
||||||
): string {
|
): string {
|
||||||
if (!date) return ''
|
if (!date) return ''
|
||||||
|
|
||||||
const d = new Date(date)
|
const d = new Date(date)
|
||||||
if (isNaN(d.getTime())) return ''
|
if (isNaN(d.getTime())) return ''
|
||||||
|
|
||||||
const locale = getLocale()
|
const locale = localeOverride ?? getLocale()
|
||||||
return new Intl.DateTimeFormat(locale, options).format(d)
|
return new Intl.DateTimeFormat(locale, options).format(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,10 +137,41 @@ export function formatDateOnly(date: string | Date | null | undefined): string {
|
|||||||
/**
|
/**
|
||||||
* 格式化日期时间(完整格式)
|
* 格式化日期时间(完整格式)
|
||||||
* @param date 日期字符串或 Date 对象
|
* @param date 日期字符串或 Date 对象
|
||||||
|
* @param options Intl.DateTimeFormatOptions
|
||||||
|
* @param localeOverride 可选 locale 覆盖
|
||||||
* @returns 格式化后的日期时间字符串
|
* @returns 格式化后的日期时间字符串
|
||||||
*/
|
*/
|
||||||
export function formatDateTime(date: string | Date | null | undefined): string {
|
export function formatDateTime(
|
||||||
return formatDate(date)
|
date: string | Date | null | undefined,
|
||||||
|
options?: Intl.DateTimeFormatOptions,
|
||||||
|
localeOverride?: string
|
||||||
|
): string {
|
||||||
|
return formatDate(date, options, localeOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化为 datetime-local 控件值(YYYY-MM-DDTHH:mm,使用本地时间)
|
||||||
|
*/
|
||||||
|
export function formatDateTimeLocalInput(timestampSeconds: number | null): string {
|
||||||
|
if (!timestampSeconds) return ''
|
||||||
|
const date = new Date(timestampSeconds * 1000)
|
||||||
|
if (isNaN(date.getTime())) return ''
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 datetime-local 控件值为时间戳(秒,使用本地时间)
|
||||||
|
*/
|
||||||
|
export function parseDateTimeLocalInput(value: string): number | null {
|
||||||
|
if (!value) return null
|
||||||
|
const date = new Date(value)
|
||||||
|
if (isNaN(date.getTime())) return null
|
||||||
|
return Math.floor(date.getTime() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -70,6 +70,25 @@
|
|||||||
<template #cell-last_used_at="{ value }">
|
<template #cell-last_used_at="{ value }">
|
||||||
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatRelativeTime(value) }}</span>
|
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatRelativeTime(value) }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
<template #cell-expires_at="{ row, value }">
|
||||||
|
<div class="flex flex-col items-start gap-1">
|
||||||
|
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatExpiresAt(value) }}</span>
|
||||||
|
<div v-if="isExpired(value) || (row.auto_pause_on_expired && value)" class="flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
v-if="isExpired(value)"
|
||||||
|
class="inline-flex items-center rounded-md bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.expired') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="row.auto_pause_on_expired && value"
|
||||||
|
class="inline-flex items-center rounded-md bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.autoPauseOnExpired') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button @click="handleEdit(row)" class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400">
|
<button @click="handleEdit(row)" class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400">
|
||||||
@@ -128,7 +147,7 @@ import AccountUsageCell from '@/components/account/AccountUsageCell.vue'
|
|||||||
import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vue'
|
import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vue'
|
||||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||||
import { formatRelativeTime } from '@/utils/format'
|
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||||
import type { Account, Proxy, Group } from '@/types'
|
import type { Account, Proxy, Group } from '@/types'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -178,6 +197,7 @@ const cols = computed(() => {
|
|||||||
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
||||||
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
||||||
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
||||||
|
{ key: 'expires_at', label: t('admin.accounts.columns.expiresAt'), sortable: true },
|
||||||
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
|
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
|
||||||
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
||||||
)
|
)
|
||||||
@@ -204,6 +224,25 @@ const confirmDelete = async () => { if(!deletingAcc.value) return; try { await a
|
|||||||
const handleToggleSchedulable = async (a: Account) => { togglingSchedulable.value = a.id; try { await adminAPI.accounts.setSchedulable(a.id, !a.schedulable); load() } finally { togglingSchedulable.value = null } }
|
const handleToggleSchedulable = async (a: Account) => { togglingSchedulable.value = a.id; try { await adminAPI.accounts.setSchedulable(a.id, !a.schedulable); load() } finally { togglingSchedulable.value = null } }
|
||||||
const handleShowTempUnsched = (a: Account) => { tempUnschedAcc.value = a; showTempUnsched.value = true }
|
const handleShowTempUnsched = (a: Account) => { tempUnschedAcc.value = a; showTempUnsched.value = true }
|
||||||
const handleTempUnschedReset = async () => { if(!tempUnschedAcc.value) return; try { await adminAPI.accounts.clearError(tempUnschedAcc.value.id); showTempUnsched.value = false; tempUnschedAcc.value = null; load() } catch (error) { console.error('Failed to reset temp unscheduled:', error) } }
|
const handleTempUnschedReset = async () => { if(!tempUnschedAcc.value) return; try { await adminAPI.accounts.clearError(tempUnschedAcc.value.id); showTempUnsched.value = false; tempUnschedAcc.value = null; load() } catch (error) { console.error('Failed to reset temp unscheduled:', error) } }
|
||||||
|
const formatExpiresAt = (value: number | null) => {
|
||||||
|
if (!value) return '-'
|
||||||
|
return formatDateTime(
|
||||||
|
new Date(value * 1000),
|
||||||
|
{
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
},
|
||||||
|
'sv-SE'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const isExpired = (value: number | null) => {
|
||||||
|
if (!value) return false
|
||||||
|
return value * 1000 <= Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => { load(); try { const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()]); proxies.value = p; groups.value = g } catch (error) { console.error('Failed to load proxies/groups:', error) } })
|
onMounted(async () => { load(); try { const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()]); proxies.value = p; groups.value = g } catch (error) { console.error('Failed to load proxies/groups:', error) } })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -85,11 +85,48 @@ const exportToExcel = async () => {
|
|||||||
if (all.length >= total || res.items.length < 100) break; p++
|
if (all.length >= total || res.items.length < 100) break; p++
|
||||||
}
|
}
|
||||||
if(!c.signal.aborted) {
|
if(!c.signal.aborted) {
|
||||||
// 动态加载 xlsx,降低首屏包体并减少高危依赖的常驻暴露面。
|
|
||||||
const XLSX = await import('xlsx')
|
const XLSX = await import('xlsx')
|
||||||
const ws = XLSX.utils.json_to_sheet(all); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Usage')
|
const headers = [
|
||||||
saveAs(new Blob([XLSX.write(wb, { bookType: 'xlsx', type: 'array' })], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), `usage_${Date.now()}.xlsx`)
|
t('usage.time'), t('admin.usage.user'), t('usage.apiKeyFilter'),
|
||||||
appStore.showSuccess('Export Success')
|
t('admin.usage.account'), t('usage.model'), t('admin.usage.group'),
|
||||||
|
t('usage.type'),
|
||||||
|
t('admin.usage.inputTokens'), t('admin.usage.outputTokens'),
|
||||||
|
t('admin.usage.cacheReadTokens'), t('admin.usage.cacheCreationTokens'),
|
||||||
|
t('admin.usage.inputCost'), t('admin.usage.outputCost'),
|
||||||
|
t('admin.usage.cacheReadCost'), t('admin.usage.cacheCreationCost'),
|
||||||
|
t('usage.rate'), t('usage.original'), t('usage.billed'),
|
||||||
|
t('usage.billingType'), t('usage.firstToken'), t('usage.duration'),
|
||||||
|
t('admin.usage.requestId')
|
||||||
|
]
|
||||||
|
const rows = all.map(log => [
|
||||||
|
log.created_at,
|
||||||
|
log.user?.email || '',
|
||||||
|
log.api_key?.name || '',
|
||||||
|
log.account?.name || '',
|
||||||
|
log.model,
|
||||||
|
log.group?.name || '',
|
||||||
|
log.stream ? t('usage.stream') : t('usage.sync'),
|
||||||
|
log.input_tokens,
|
||||||
|
log.output_tokens,
|
||||||
|
log.cache_read_tokens,
|
||||||
|
log.cache_creation_tokens,
|
||||||
|
log.input_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.output_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.cache_read_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.cache_creation_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.rate_multiplier?.toFixed(2) || '1.00',
|
||||||
|
log.total_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.actual_cost?.toFixed(6) || '0.000000',
|
||||||
|
log.billing_type === 1 ? t('usage.subscription') : t('usage.balance'),
|
||||||
|
log.first_token_ms ?? '',
|
||||||
|
log.duration_ms,
|
||||||
|
log.request_id || ''
|
||||||
|
])
|
||||||
|
const ws = XLSX.utils.aoa_to_sheet([headers, ...rows])
|
||||||
|
const wb = XLSX.utils.book_new()
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, 'Usage')
|
||||||
|
saveAs(new Blob([XLSX.write(wb, { bookType: 'xlsx', type: 'array' })], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), `usage_${filters.value.start_date}_to_${filters.value.end_date}.xlsx`)
|
||||||
|
appStore.showSuccess(t('usage.exportSuccess'))
|
||||||
}
|
}
|
||||||
} catch (error) { console.error('Failed to export:', error); appStore.showError('Export Failed') }
|
} catch (error) { console.error('Failed to export:', error); appStore.showError('Export Failed') }
|
||||||
finally { if(exportAbortController === c) { exportAbortController = null; exporting.value = false; exportProgress.show = false } }
|
finally { if(exportAbortController === c) { exportAbortController = null; exporting.value = false; exportProgress.show = false } }
|
||||||
|
|||||||
@@ -342,8 +342,8 @@
|
|||||||
>
|
>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<!-- Token Breakdown -->
|
<!-- Token Breakdown -->
|
||||||
<div class="mb-2 border-b border-gray-700 pb-1.5">
|
<div>
|
||||||
<div class="text-xs font-semibold text-gray-300 mb-1">Token 明细</div>
|
<div class="text-xs font-semibold text-gray-300 mb-1">{{ t('usage.tokenDetails') }}</div>
|
||||||
<div v-if="tokenTooltipData && tokenTooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
<div v-if="tokenTooltipData && tokenTooltipData.input_tokens > 0" class="flex items-center justify-between gap-4">
|
||||||
<span class="text-gray-400">{{ t('admin.usage.inputTokens') }}</span>
|
<span class="text-gray-400">{{ t('admin.usage.inputTokens') }}</span>
|
||||||
<span class="font-medium text-white">{{ tokenTooltipData.input_tokens.toLocaleString() }}</span>
|
<span class="font-medium text-white">{{ tokenTooltipData.input_tokens.toLocaleString() }}</span>
|
||||||
@@ -389,6 +389,27 @@
|
|||||||
class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800"
|
class="whitespace-nowrap rounded-lg border border-gray-700 bg-gray-900 px-3 py-2.5 text-xs text-white shadow-xl dark:border-gray-600 dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
|
<!-- Cost Breakdown -->
|
||||||
|
<div class="mb-2 border-b border-gray-700 pb-1.5">
|
||||||
|
<div class="text-xs font-semibold text-gray-300 mb-1">{{ t('usage.costDetails') }}</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.input_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.inputCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.input_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.output_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.outputCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.output_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.cache_creation_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.cacheCreationCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.cache_creation_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tooltipData && tooltipData.cache_read_cost > 0" class="flex items-center justify-between gap-4">
|
||||||
|
<span class="text-gray-400">{{ t('admin.usage.cacheReadCost') }}</span>
|
||||||
|
<span class="font-medium text-white">${{ tooltipData.cache_read_cost.toFixed(6) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Rate and Summary -->
|
||||||
<div class="flex items-center justify-between gap-6">
|
<div class="flex items-center justify-between gap-6">
|
||||||
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
<span class="text-gray-400">{{ t('usage.rate') }}</span>
|
||||||
<span class="font-semibold text-blue-400"
|
<span class="font-semibold text-blue-400"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
import checker from 'vite-plugin-checker'
|
import checker from 'vite-plugin-checker'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
@@ -29,7 +30,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 3000,
|
port: Number(process.env.VITE_DEV_PORT || 3000),
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080',
|
target: process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080',
|
||||||
|
|||||||
Reference in New Issue
Block a user