mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-04 15:32:13 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fb43b91bf | ||
|
|
6e8188ed64 | ||
|
|
db6f53e2c9 | ||
|
|
acabdc2f99 | ||
|
|
169aa4716e | ||
|
|
c0753320a0 | ||
|
|
38d875b06f | ||
|
|
1ada6cf768 | ||
|
|
2b528c5f81 | ||
|
|
f6dd4752e7 | ||
|
|
b19c7875a4 | ||
|
|
d99a3ef14b | ||
|
|
fc8fa83fcc | ||
|
|
6dcd99468b | ||
|
|
d5ba7b80d3 | ||
|
|
a3b81ef7bc | ||
|
|
015974a27e | ||
|
|
4cf756ebe6 | ||
|
|
823497a2af | ||
|
|
66fe484f0d | ||
|
|
216321aa9e | ||
|
|
5a52cb608c | ||
|
|
1181b332f7 | ||
|
|
58b1777198 | ||
|
|
0c7a58fcc7 | ||
|
|
17ae51c0a0 | ||
|
|
4790aced15 | ||
|
|
3f0017d1f1 | ||
|
|
cb72262ad8 | ||
|
|
195e227c04 | ||
|
|
f5603b0780 | ||
|
|
752882a022 | ||
|
|
9731b961d0 | ||
|
|
2920409404 | ||
|
|
7dbbfc22b6 | ||
|
|
aaaa68ea7f | ||
|
|
af753de481 | ||
|
|
3956819c78 | ||
|
|
6fa704d6fc | ||
|
|
0400fcdca4 | ||
|
|
5b1907fe61 | ||
|
|
d4c2b723a5 |
16
.github/audit-exceptions.yml
vendored
Normal file
16
.github/audit-exceptions.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
version: 1
|
||||
exceptions:
|
||||
- package: xlsx
|
||||
advisory: "GHSA-4r6h-8v6p-xvw6"
|
||||
severity: high
|
||||
reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2023-30533)"
|
||||
mitigation: "Load only on export; restrict export permissions and data scope"
|
||||
expires_on: "2026-04-05"
|
||||
owner: "security@your-domain"
|
||||
- package: xlsx
|
||||
advisory: "GHSA-5pgg-2g8v-p4x9"
|
||||
severity: high
|
||||
reason: "Admin export only; switched to dynamic import to reduce exposure (CVE-2024-22363)"
|
||||
mitigation: "Load only on export; restrict export permissions and data scope"
|
||||
expires_on: "2026-04-05"
|
||||
owner: "security@your-domain"
|
||||
10
.github/workflows/backend-ci.yml
vendored
10
.github/workflows/backend-ci.yml
vendored
@@ -15,8 +15,11 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: true
|
||||
check-latest: false
|
||||
cache: true
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.25.5'
|
||||
- name: Unit tests
|
||||
working-directory: backend
|
||||
run: make test-unit
|
||||
@@ -31,8 +34,11 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: true
|
||||
check-latest: false
|
||||
cache: true
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.25.5'
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
|
||||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -109,9 +109,14 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: false
|
||||
cache-dependency-path: backend/go.sum
|
||||
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.25.5'
|
||||
|
||||
# Docker setup for GoReleaser
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
62
.github/workflows/security-scan.yml
vendored
Normal file
62
.github/workflows/security-scan.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 3 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backend-security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: backend/go.mod
|
||||
check-latest: false
|
||||
cache-dependency-path: backend/go.sum
|
||||
- name: Verify Go version
|
||||
run: |
|
||||
go version | grep -q 'go1.25.5'
|
||||
- name: Run govulncheck
|
||||
working-directory: backend
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
govulncheck ./...
|
||||
- name: Run gosec
|
||||
working-directory: backend
|
||||
run: |
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
gosec -severity high -confidence high ./...
|
||||
|
||||
frontend-security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run pnpm audit
|
||||
working-directory: frontend
|
||||
run: |
|
||||
pnpm audit --prod --audit-level=high --json > audit.json || true
|
||||
- name: Check audit exceptions
|
||||
run: |
|
||||
python tools/check_pnpm_audit_exceptions.py \
|
||||
--audit frontend/audit.json \
|
||||
--exceptions .github/audit-exceptions.yml
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -123,3 +123,6 @@ backend/cmd/server/server
|
||||
deploy/docker-compose.override.yml
|
||||
.gocache/
|
||||
vite.config.js
|
||||
!docs/
|
||||
docs/*
|
||||
!docs/dependency-security.md
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
# =============================================================================
|
||||
|
||||
ARG NODE_IMAGE=node:24-alpine
|
||||
ARG GOLANG_IMAGE=golang:1.25-alpine
|
||||
ARG ALPINE_IMAGE=alpine:3.19
|
||||
ARG GOLANG_IMAGE=golang:1.25.5-alpine
|
||||
ARG ALPINE_IMAGE=alpine:3.20
|
||||
ARG GOPROXY=https://goproxy.cn,direct
|
||||
ARG GOSUMDB=sum.golang.google.cn
|
||||
|
||||
|
||||
6
Makefile
6
Makefile
@@ -9,7 +9,7 @@ build-backend:
|
||||
|
||||
# 编译前端(需要已安装依赖)
|
||||
build-frontend:
|
||||
@npm --prefix frontend run build
|
||||
@pnpm --dir frontend run build
|
||||
|
||||
# 运行测试(后端 + 前端)
|
||||
test: test-backend test-frontend
|
||||
@@ -18,5 +18,5 @@ test-backend:
|
||||
@$(MAKE) -C backend test
|
||||
|
||||
test-frontend:
|
||||
@npm --prefix frontend run lint:check
|
||||
@npm --prefix frontend run typecheck
|
||||
@pnpm --dir frontend run lint:check
|
||||
@pnpm --dir frontend run typecheck
|
||||
|
||||
63
README.md
63
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://redis.io/)
|
||||
@@ -44,13 +44,19 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|------------|
|
||||
| Backend | Go 1.21+, Gin, GORM |
|
||||
| Backend | Go 1.25.5, Gin, Ent |
|
||||
| Frontend | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||
| Database | PostgreSQL 15+ |
|
||||
| Cache/Queue | Redis 7+ |
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- Dependency Security: `docs/dependency-security.md`
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Method 1: Script Installation (Recommended)
|
||||
@@ -160,6 +166,22 @@ ADMIN_PASSWORD=your_admin_password
|
||||
|
||||
# Optional: Custom port
|
||||
SERVER_PORT=8080
|
||||
|
||||
# Optional: Security configuration
|
||||
# Enable URL allowlist validation (false to skip allowlist checks, only basic format validation)
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
|
||||
# Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https)
|
||||
# ⚠️ WARNING: Enabling this allows HTTP (plaintext) URLs which can expose API keys
|
||||
# Only recommended for:
|
||||
# - Development/testing environments
|
||||
# - Internal networks with trusted endpoints
|
||||
# - When using local test servers (http://localhost)
|
||||
# PRODUCTION: Keep this false or use HTTPS URLs only
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=false
|
||||
|
||||
# Allow private IP addresses for upstream/pricing/CRS (for internal deployments)
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=false
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -276,13 +298,48 @@ Additional security-related options are available in `config.yaml`:
|
||||
- `cors.allowed_origins` for CORS allowlist
|
||||
- `security.url_allowlist` for upstream/pricing/CRS host allowlists
|
||||
- `security.url_allowlist.enabled` to disable URL validation (use with caution)
|
||||
- `security.url_allowlist.allow_insecure_http` to allow http URLs when validation is disabled
|
||||
- `security.url_allowlist.allow_insecure_http` to allow HTTP URLs when validation is disabled
|
||||
- `security.url_allowlist.allow_private_hosts` to allow private/local IP addresses
|
||||
- `security.response_headers.enabled` to enable configurable response header filtering (disabled uses default allowlist)
|
||||
- `security.csp` to control Content-Security-Policy headers
|
||||
- `billing.circuit_breaker` to fail closed on billing errors
|
||||
- `server.trusted_proxies` to enable X-Forwarded-For parsing
|
||||
- `turnstile.required` to require Turnstile in release mode
|
||||
|
||||
**⚠️ Security Warning: HTTP URL Configuration**
|
||||
|
||||
When `security.url_allowlist.enabled=false`, the system performs minimal URL validation by default, **rejecting HTTP URLs** and only allowing HTTPS. To allow HTTP URLs (e.g., for development or internal testing), you must explicitly set:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
url_allowlist:
|
||||
enabled: false # Disable allowlist checks
|
||||
allow_insecure_http: true # Allow HTTP URLs (⚠️ INSECURE)
|
||||
```
|
||||
|
||||
**Or via environment variable:**
|
||||
|
||||
```bash
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
|
||||
```
|
||||
|
||||
**Risks of allowing HTTP:**
|
||||
- API keys and data transmitted in **plaintext** (vulnerable to interception)
|
||||
- Susceptible to **man-in-the-middle (MITM) attacks**
|
||||
- **NOT suitable for production** environments
|
||||
|
||||
**When to use HTTP:**
|
||||
- ✅ Development/testing with local servers (http://localhost)
|
||||
- ✅ Internal networks with trusted endpoints
|
||||
- ✅ Testing account connectivity before obtaining HTTPS
|
||||
- ❌ Production environments (use HTTPS only)
|
||||
|
||||
**Example error without this setting:**
|
||||
```
|
||||
Invalid base URL: invalid url scheme: http
|
||||
```
|
||||
|
||||
If you disable URL validation or response header filtering, harden your network layer:
|
||||
- Enforce an egress allowlist for upstream domains/IPs
|
||||
- Block private/loopback/link-local ranges
|
||||
|
||||
63
README_CN.md
63
README_CN.md
@@ -2,7 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://golang.org/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://redis.io/)
|
||||
@@ -44,13 +44,19 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅(
|
||||
|
||||
| 组件 | 技术 |
|
||||
|------|------|
|
||||
| 后端 | Go 1.21+, Gin, GORM |
|
||||
| 后端 | Go 1.25.5, Gin, Ent |
|
||||
| 前端 | Vue 3.4+, Vite 5+, TailwindCSS |
|
||||
| 数据库 | PostgreSQL 15+ |
|
||||
| 缓存/队列 | Redis 7+ |
|
||||
|
||||
---
|
||||
|
||||
## 文档
|
||||
|
||||
- 依赖安全:`docs/dependency-security.md`
|
||||
|
||||
---
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 方式一:脚本安装(推荐)
|
||||
@@ -160,6 +166,22 @@ ADMIN_PASSWORD=your_admin_password
|
||||
|
||||
# 可选:自定义端口
|
||||
SERVER_PORT=8080
|
||||
|
||||
# 可选:安全配置
|
||||
# 启用 URL 白名单验证(false 则跳过白名单检查,仅做基本格式校验)
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
|
||||
# 关闭白名单时,是否允许 http:// URL(默认 false,只允许 https://)
|
||||
# ⚠️ 警告:允许 HTTP 会暴露 API 密钥(明文传输)
|
||||
# 仅建议在以下场景使用:
|
||||
# - 开发/测试环境
|
||||
# - 内部可信网络
|
||||
# - 本地测试服务器(http://localhost)
|
||||
# 生产环境:保持 false 或仅使用 HTTPS URL
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=false
|
||||
|
||||
# 是否允许私有 IP 地址用于上游/定价/CRS(内网部署时使用)
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=false
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -276,13 +298,48 @@ default:
|
||||
- `cors.allowed_origins` 配置 CORS 白名单
|
||||
- `security.url_allowlist` 配置上游/价格数据/CRS 主机白名单
|
||||
- `security.url_allowlist.enabled` 可关闭 URL 校验(慎用)
|
||||
- `security.url_allowlist.allow_insecure_http` 关闭校验时允许 http URL
|
||||
- `security.url_allowlist.allow_insecure_http` 关闭校验时允许 HTTP URL
|
||||
- `security.url_allowlist.allow_private_hosts` 允许私有/本地 IP 地址
|
||||
- `security.response_headers.enabled` 可启用可配置响应头过滤(关闭时使用默认白名单)
|
||||
- `security.csp` 配置 Content-Security-Policy
|
||||
- `billing.circuit_breaker` 计费异常时 fail-closed
|
||||
- `server.trusted_proxies` 启用可信代理解析 X-Forwarded-For
|
||||
- `turnstile.required` 在 release 模式强制启用 Turnstile
|
||||
|
||||
**⚠️ 安全警告:HTTP URL 配置**
|
||||
|
||||
当 `security.url_allowlist.enabled=false` 时,系统默认执行最小 URL 校验,**拒绝 HTTP URL**,仅允许 HTTPS。要允许 HTTP URL(例如用于开发或内网测试),必须显式设置:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
url_allowlist:
|
||||
enabled: false # 禁用白名单检查
|
||||
allow_insecure_http: true # 允许 HTTP URL(⚠️ 不安全)
|
||||
```
|
||||
|
||||
**或通过环境变量:**
|
||||
|
||||
```bash
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
|
||||
```
|
||||
|
||||
**允许 HTTP 的风险:**
|
||||
- API 密钥和数据以**明文传输**(可被截获)
|
||||
- 易受**中间人攻击 (MITM)**
|
||||
- **不适合生产环境**
|
||||
|
||||
**适用场景:**
|
||||
- ✅ 开发/测试环境的本地服务器(http://localhost)
|
||||
- ✅ 内网可信端点
|
||||
- ✅ 获取 HTTPS 前测试账号连通性
|
||||
- ❌ 生产环境(仅使用 HTTPS)
|
||||
|
||||
**未设置此项时的错误示例:**
|
||||
```
|
||||
Invalid base URL: invalid url scheme: http
|
||||
```
|
||||
|
||||
如关闭 URL 校验或响应头过滤,请加强网络层防护:
|
||||
- 出站访问白名单限制上游域名/IP
|
||||
- 阻断私网/回环/链路本地地址
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.21-alpine
|
||||
FROM golang:1.25.5-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ func provideCleanup(
|
||||
entClient *ent.Client,
|
||||
rdb *redis.Client,
|
||||
tokenRefresh *service.TokenRefreshService,
|
||||
accountExpiry *service.AccountExpiryService,
|
||||
pricing *service.PricingService,
|
||||
emailQueue *service.EmailQueueService,
|
||||
billingCache *service.BillingCacheService,
|
||||
@@ -84,6 +85,10 @@ func provideCleanup(
|
||||
tokenRefresh.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"AccountExpiryService", func() error {
|
||||
accountExpiry.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"PricingService", func() error {
|
||||
pricing.Stop()
|
||||
return nil
|
||||
|
||||
@@ -87,6 +87,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
|
||||
geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
|
||||
geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig)
|
||||
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
||||
geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
|
||||
tempUnschedCache := repository.NewTempUnschedCache(redisClient)
|
||||
rateLimitService := service.NewRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache)
|
||||
@@ -97,15 +98,14 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
geminiTokenCache := repository.NewGeminiTokenCache(redisClient)
|
||||
geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
|
||||
gatewayCache := repository.NewGatewayCache(redisClient)
|
||||
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
||||
antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
|
||||
httpUpstream := repository.NewHTTPUpstream(configConfig)
|
||||
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
||||
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, 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)
|
||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
|
||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
|
||||
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
||||
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService)
|
||||
geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService)
|
||||
@@ -114,7 +114,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
adminRedeemHandler := admin.NewRedeemHandler(adminService)
|
||||
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService)
|
||||
updateCache := repository.NewUpdateCache(redisClient)
|
||||
gitHubReleaseClient := repository.NewGitHubReleaseClient()
|
||||
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
|
||||
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
|
||||
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
|
||||
systemHandler := handler.ProvideSystemHandler(updateService)
|
||||
@@ -125,7 +125,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
|
||||
userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -148,7 +148,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
||||
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
||||
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{
|
||||
Server: httpServer,
|
||||
Cleanup: v,
|
||||
@@ -174,6 +175,7 @@ func provideCleanup(
|
||||
entClient *ent.Client,
|
||||
rdb *redis.Client,
|
||||
tokenRefresh *service.TokenRefreshService,
|
||||
accountExpiry *service.AccountExpiryService,
|
||||
pricing *service.PricingService,
|
||||
emailQueue *service.EmailQueueService,
|
||||
billingCache *service.BillingCacheService,
|
||||
@@ -194,6 +196,10 @@ func provideCleanup(
|
||||
tokenRefresh.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"AccountExpiryService", func() error {
|
||||
accountExpiry.Stop()
|
||||
return nil
|
||||
}},
|
||||
{"PricingService", func() error {
|
||||
pricing.Stop()
|
||||
return nil
|
||||
|
||||
@@ -49,6 +49,10 @@ type Account struct {
|
||||
ErrorMessage *string `json:"error_message,omitempty"`
|
||||
// LastUsedAt holds the value of the "last_used_at" field.
|
||||
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 bool `json:"schedulable,omitempty"`
|
||||
// RateLimitedAt holds the value of the "rate_limited_at" field.
|
||||
@@ -129,13 +133,13 @@ func (*Account) scanValues(columns []string) ([]any, error) {
|
||||
switch columns[i] {
|
||||
case account.FieldCredentials, account.FieldExtra:
|
||||
values[i] = new([]byte)
|
||||
case account.FieldSchedulable:
|
||||
case account.FieldAutoPauseOnExpired, account.FieldSchedulable:
|
||||
values[i] = new(sql.NullBool)
|
||||
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldSessionWindowStatus:
|
||||
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)
|
||||
default:
|
||||
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 = 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:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
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(", ")
|
||||
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(fmt.Sprintf("%v", _m.Schedulable))
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -45,6 +45,10 @@ const (
|
||||
FieldErrorMessage = "error_message"
|
||||
// FieldLastUsedAt holds the string denoting the last_used_at field in the database.
|
||||
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 = "schedulable"
|
||||
// FieldRateLimitedAt holds the string denoting the rate_limited_at field in the database.
|
||||
@@ -115,6 +119,8 @@ var Columns = []string{
|
||||
FieldStatus,
|
||||
FieldErrorMessage,
|
||||
FieldLastUsedAt,
|
||||
FieldExpiresAt,
|
||||
FieldAutoPauseOnExpired,
|
||||
FieldSchedulable,
|
||||
FieldRateLimitedAt,
|
||||
FieldRateLimitResetAt,
|
||||
@@ -172,6 +178,8 @@ var (
|
||||
DefaultStatus string
|
||||
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
||||
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 bool
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func BySchedulable(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldSchedulable, opts...).ToFunc()
|
||||
|
||||
@@ -120,6 +120,16 @@ func LastUsedAt(v time.Time) predicate.Account {
|
||||
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.
|
||||
func Schedulable(v bool) predicate.Account {
|
||||
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
||||
@@ -855,6 +865,66 @@ func LastUsedAtNotNil() predicate.Account {
|
||||
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.
|
||||
func SchedulableEQ(v bool) predicate.Account {
|
||||
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
||||
|
||||
@@ -195,6 +195,34 @@ func (_c *AccountCreate) SetNillableLastUsedAt(v *time.Time) *AccountCreate {
|
||||
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.
|
||||
func (_c *AccountCreate) SetSchedulable(v bool) *AccountCreate {
|
||||
_c.mutation.SetSchedulable(v)
|
||||
@@ -405,6 +433,10 @@ func (_c *AccountCreate) defaults() error {
|
||||
v := account.DefaultStatus
|
||||
_c.mutation.SetStatus(v)
|
||||
}
|
||||
if _, ok := _c.mutation.AutoPauseOnExpired(); !ok {
|
||||
v := account.DefaultAutoPauseOnExpired
|
||||
_c.mutation.SetAutoPauseOnExpired(v)
|
||||
}
|
||||
if _, ok := _c.mutation.Schedulable(); !ok {
|
||||
v := account.DefaultSchedulable
|
||||
_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)}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
_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 {
|
||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||
_node.Schedulable = value
|
||||
@@ -898,6 +941,36 @@ func (u *AccountUpsert) ClearLastUsedAt() *AccountUpsert {
|
||||
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.
|
||||
func (u *AccountUpsert) SetSchedulable(v bool) *AccountUpsert {
|
||||
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.
|
||||
func (u *AccountUpsertOne) SetSchedulable(v bool) *AccountUpsertOne {
|
||||
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.
|
||||
func (u *AccountUpsertBulk) SetSchedulable(v bool) *AccountUpsertBulk {
|
||||
return u.Update(func(s *AccountUpsert) {
|
||||
|
||||
@@ -247,6 +247,40 @@ func (_u *AccountUpdate) ClearLastUsedAt() *AccountUpdate {
|
||||
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.
|
||||
func (_u *AccountUpdate) SetSchedulable(v bool) *AccountUpdate {
|
||||
_u.mutation.SetSchedulable(v)
|
||||
@@ -610,6 +644,15 @@ func (_u *AccountUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.LastUsedAtCleared() {
|
||||
_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 {
|
||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||
}
|
||||
@@ -1016,6 +1059,40 @@ func (_u *AccountUpdateOne) ClearLastUsedAt() *AccountUpdateOne {
|
||||
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.
|
||||
func (_u *AccountUpdateOne) SetSchedulable(v bool) *AccountUpdateOne {
|
||||
_u.mutation.SetSchedulable(v)
|
||||
@@ -1409,6 +1486,15 @@ func (_u *AccountUpdateOne) sqlSave(ctx context.Context) (_node *Account, err er
|
||||
if _u.mutation.LastUsedAtCleared() {
|
||||
_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 {
|
||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ type Group struct {
|
||||
MonthlyLimitUsd *float64 `json:"monthly_limit_usd,omitempty"`
|
||||
// DefaultValidityDays holds the value of the "default_validity_days" field.
|
||||
DefaultValidityDays int `json:"default_validity_days,omitempty"`
|
||||
// ImagePrice1k holds the value of the "image_price_1k" field.
|
||||
ImagePrice1k *float64 `json:"image_price_1k,omitempty"`
|
||||
// ImagePrice2k holds the value of the "image_price_2k" field.
|
||||
ImagePrice2k *float64 `json:"image_price_2k,omitempty"`
|
||||
// ImagePrice4k holds the value of the "image_price_4k" field.
|
||||
ImagePrice4k *float64 `json:"image_price_4k,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
// The values are being populated by the GroupQuery when eager-loading is set.
|
||||
Edges GroupEdges `json:"edges"`
|
||||
@@ -153,7 +159,7 @@ func (*Group) scanValues(columns []string) ([]any, error) {
|
||||
switch columns[i] {
|
||||
case group.FieldIsExclusive:
|
||||
values[i] = new(sql.NullBool)
|
||||
case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd:
|
||||
case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k:
|
||||
values[i] = new(sql.NullFloat64)
|
||||
case group.FieldID, group.FieldDefaultValidityDays:
|
||||
values[i] = new(sql.NullInt64)
|
||||
@@ -271,6 +277,27 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.DefaultValidityDays = int(value.Int64)
|
||||
}
|
||||
case group.FieldImagePrice1k:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field image_price_1k", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ImagePrice1k = new(float64)
|
||||
*_m.ImagePrice1k = value.Float64
|
||||
}
|
||||
case group.FieldImagePrice2k:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field image_price_2k", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ImagePrice2k = new(float64)
|
||||
*_m.ImagePrice2k = value.Float64
|
||||
}
|
||||
case group.FieldImagePrice4k:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field image_price_4k", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ImagePrice4k = new(float64)
|
||||
*_m.ImagePrice4k = value.Float64
|
||||
}
|
||||
default:
|
||||
_m.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
@@ -398,6 +425,21 @@ func (_m *Group) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("default_validity_days=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.DefaultValidityDays))
|
||||
builder.WriteString(", ")
|
||||
if v := _m.ImagePrice1k; v != nil {
|
||||
builder.WriteString("image_price_1k=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.ImagePrice2k; v != nil {
|
||||
builder.WriteString("image_price_2k=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.ImagePrice4k; v != nil {
|
||||
builder.WriteString("image_price_4k=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ const (
|
||||
FieldMonthlyLimitUsd = "monthly_limit_usd"
|
||||
// FieldDefaultValidityDays holds the string denoting the default_validity_days field in the database.
|
||||
FieldDefaultValidityDays = "default_validity_days"
|
||||
// FieldImagePrice1k holds the string denoting the image_price_1k field in the database.
|
||||
FieldImagePrice1k = "image_price_1k"
|
||||
// FieldImagePrice2k holds the string denoting the image_price_2k field in the database.
|
||||
FieldImagePrice2k = "image_price_2k"
|
||||
// FieldImagePrice4k holds the string denoting the image_price_4k field in the database.
|
||||
FieldImagePrice4k = "image_price_4k"
|
||||
// EdgeAPIKeys holds the string denoting the api_keys edge name in mutations.
|
||||
EdgeAPIKeys = "api_keys"
|
||||
// EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations.
|
||||
@@ -132,6 +138,9 @@ var Columns = []string{
|
||||
FieldWeeklyLimitUsd,
|
||||
FieldMonthlyLimitUsd,
|
||||
FieldDefaultValidityDays,
|
||||
FieldImagePrice1k,
|
||||
FieldImagePrice2k,
|
||||
FieldImagePrice4k,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -267,6 +276,21 @@ func ByDefaultValidityDays(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldDefaultValidityDays, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByImagePrice1k orders the results by the image_price_1k field.
|
||||
func ByImagePrice1k(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldImagePrice1k, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByImagePrice2k orders the results by the image_price_2k field.
|
||||
func ByImagePrice2k(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldImagePrice2k, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByImagePrice4k orders the results by the image_price_4k field.
|
||||
func ByImagePrice4k(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldImagePrice4k, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAPIKeysCount orders the results by api_keys count.
|
||||
func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
|
||||
@@ -125,6 +125,21 @@ func DefaultValidityDays(v int) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldDefaultValidityDays, v))
|
||||
}
|
||||
|
||||
// ImagePrice1k applies equality check predicate on the "image_price_1k" field. It's identical to ImagePrice1kEQ.
|
||||
func ImagePrice1k(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldImagePrice1k, v))
|
||||
}
|
||||
|
||||
// ImagePrice2k applies equality check predicate on the "image_price_2k" field. It's identical to ImagePrice2kEQ.
|
||||
func ImagePrice2k(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldImagePrice2k, v))
|
||||
}
|
||||
|
||||
// ImagePrice4k applies equality check predicate on the "image_price_4k" field. It's identical to ImagePrice4kEQ.
|
||||
func ImagePrice4k(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldImagePrice4k, v))
|
||||
}
|
||||
|
||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||
func CreatedAtEQ(v time.Time) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldCreatedAt, v))
|
||||
@@ -830,6 +845,156 @@ func DefaultValidityDaysLTE(v int) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldDefaultValidityDays, v))
|
||||
}
|
||||
|
||||
// ImagePrice1kEQ applies the EQ predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldImagePrice1k, v))
|
||||
}
|
||||
|
||||
// ImagePrice1kNEQ applies the NEQ predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldImagePrice1k, v))
|
||||
}
|
||||
|
||||
// ImagePrice1kIn applies the In predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldImagePrice1k, vs...))
|
||||
}
|
||||
|
||||
// ImagePrice1kNotIn applies the NotIn predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldImagePrice1k, vs...))
|
||||
}
|
||||
|
||||
// ImagePrice1kGT applies the GT predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldImagePrice1k, v))
|
||||
}
|
||||
|
||||
// ImagePrice1kGTE applies the GTE predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldImagePrice1k, v))
|
||||
}
|
||||
|
||||
// ImagePrice1kLT applies the LT predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldImagePrice1k, v))
|
||||
}
|
||||
|
||||
// ImagePrice1kLTE applies the LTE predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldImagePrice1k, v))
|
||||
}
|
||||
|
||||
// ImagePrice1kIsNil applies the IsNil predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldImagePrice1k))
|
||||
}
|
||||
|
||||
// ImagePrice1kNotNil applies the NotNil predicate on the "image_price_1k" field.
|
||||
func ImagePrice1kNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldImagePrice1k))
|
||||
}
|
||||
|
||||
// ImagePrice2kEQ applies the EQ predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldImagePrice2k, v))
|
||||
}
|
||||
|
||||
// ImagePrice2kNEQ applies the NEQ predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldImagePrice2k, v))
|
||||
}
|
||||
|
||||
// ImagePrice2kIn applies the In predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldImagePrice2k, vs...))
|
||||
}
|
||||
|
||||
// ImagePrice2kNotIn applies the NotIn predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldImagePrice2k, vs...))
|
||||
}
|
||||
|
||||
// ImagePrice2kGT applies the GT predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldImagePrice2k, v))
|
||||
}
|
||||
|
||||
// ImagePrice2kGTE applies the GTE predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldImagePrice2k, v))
|
||||
}
|
||||
|
||||
// ImagePrice2kLT applies the LT predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldImagePrice2k, v))
|
||||
}
|
||||
|
||||
// ImagePrice2kLTE applies the LTE predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldImagePrice2k, v))
|
||||
}
|
||||
|
||||
// ImagePrice2kIsNil applies the IsNil predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldImagePrice2k))
|
||||
}
|
||||
|
||||
// ImagePrice2kNotNil applies the NotNil predicate on the "image_price_2k" field.
|
||||
func ImagePrice2kNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldImagePrice2k))
|
||||
}
|
||||
|
||||
// ImagePrice4kEQ applies the EQ predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldEQ(FieldImagePrice4k, v))
|
||||
}
|
||||
|
||||
// ImagePrice4kNEQ applies the NEQ predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kNEQ(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNEQ(FieldImagePrice4k, v))
|
||||
}
|
||||
|
||||
// ImagePrice4kIn applies the In predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldIn(FieldImagePrice4k, vs...))
|
||||
}
|
||||
|
||||
// ImagePrice4kNotIn applies the NotIn predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kNotIn(vs ...float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldNotIn(FieldImagePrice4k, vs...))
|
||||
}
|
||||
|
||||
// ImagePrice4kGT applies the GT predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kGT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGT(FieldImagePrice4k, v))
|
||||
}
|
||||
|
||||
// ImagePrice4kGTE applies the GTE predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kGTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldGTE(FieldImagePrice4k, v))
|
||||
}
|
||||
|
||||
// ImagePrice4kLT applies the LT predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kLT(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLT(FieldImagePrice4k, v))
|
||||
}
|
||||
|
||||
// ImagePrice4kLTE applies the LTE predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kLTE(v float64) predicate.Group {
|
||||
return predicate.Group(sql.FieldLTE(FieldImagePrice4k, v))
|
||||
}
|
||||
|
||||
// ImagePrice4kIsNil applies the IsNil predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kIsNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldIsNull(FieldImagePrice4k))
|
||||
}
|
||||
|
||||
// ImagePrice4kNotNil applies the NotNil predicate on the "image_price_4k" field.
|
||||
func ImagePrice4kNotNil() predicate.Group {
|
||||
return predicate.Group(sql.FieldNotNull(FieldImagePrice4k))
|
||||
}
|
||||
|
||||
// HasAPIKeys applies the HasEdge predicate on the "api_keys" edge.
|
||||
func HasAPIKeys() predicate.Group {
|
||||
return predicate.Group(func(s *sql.Selector) {
|
||||
|
||||
@@ -216,6 +216,48 @@ func (_c *GroupCreate) SetNillableDefaultValidityDays(v *int) *GroupCreate {
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetImagePrice1k sets the "image_price_1k" field.
|
||||
func (_c *GroupCreate) SetImagePrice1k(v float64) *GroupCreate {
|
||||
_c.mutation.SetImagePrice1k(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableImagePrice1k(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetImagePrice1k(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetImagePrice2k sets the "image_price_2k" field.
|
||||
func (_c *GroupCreate) SetImagePrice2k(v float64) *GroupCreate {
|
||||
_c.mutation.SetImagePrice2k(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableImagePrice2k(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetImagePrice2k(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetImagePrice4k sets the "image_price_4k" field.
|
||||
func (_c *GroupCreate) SetImagePrice4k(v float64) *GroupCreate {
|
||||
_c.mutation.SetImagePrice4k(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil.
|
||||
func (_c *GroupCreate) SetNillableImagePrice4k(v *float64) *GroupCreate {
|
||||
if v != nil {
|
||||
_c.SetImagePrice4k(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
|
||||
func (_c *GroupCreate) AddAPIKeyIDs(ids ...int64) *GroupCreate {
|
||||
_c.mutation.AddAPIKeyIDs(ids...)
|
||||
@@ -516,6 +558,18 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(group.FieldDefaultValidityDays, field.TypeInt, value)
|
||||
_node.DefaultValidityDays = value
|
||||
}
|
||||
if value, ok := _c.mutation.ImagePrice1k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value)
|
||||
_node.ImagePrice1k = &value
|
||||
}
|
||||
if value, ok := _c.mutation.ImagePrice2k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value)
|
||||
_node.ImagePrice2k = &value
|
||||
}
|
||||
if value, ok := _c.mutation.ImagePrice4k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value)
|
||||
_node.ImagePrice4k = &value
|
||||
}
|
||||
if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
@@ -888,6 +942,78 @@ func (u *GroupUpsert) AddDefaultValidityDays(v int) *GroupUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetImagePrice1k sets the "image_price_1k" field.
|
||||
func (u *GroupUpsert) SetImagePrice1k(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldImagePrice1k, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateImagePrice1k() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldImagePrice1k)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddImagePrice1k adds v to the "image_price_1k" field.
|
||||
func (u *GroupUpsert) AddImagePrice1k(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldImagePrice1k, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearImagePrice1k clears the value of the "image_price_1k" field.
|
||||
func (u *GroupUpsert) ClearImagePrice1k() *GroupUpsert {
|
||||
u.SetNull(group.FieldImagePrice1k)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetImagePrice2k sets the "image_price_2k" field.
|
||||
func (u *GroupUpsert) SetImagePrice2k(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldImagePrice2k, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateImagePrice2k() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldImagePrice2k)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddImagePrice2k adds v to the "image_price_2k" field.
|
||||
func (u *GroupUpsert) AddImagePrice2k(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldImagePrice2k, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearImagePrice2k clears the value of the "image_price_2k" field.
|
||||
func (u *GroupUpsert) ClearImagePrice2k() *GroupUpsert {
|
||||
u.SetNull(group.FieldImagePrice2k)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetImagePrice4k sets the "image_price_4k" field.
|
||||
func (u *GroupUpsert) SetImagePrice4k(v float64) *GroupUpsert {
|
||||
u.Set(group.FieldImagePrice4k, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create.
|
||||
func (u *GroupUpsert) UpdateImagePrice4k() *GroupUpsert {
|
||||
u.SetExcluded(group.FieldImagePrice4k)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddImagePrice4k adds v to the "image_price_4k" field.
|
||||
func (u *GroupUpsert) AddImagePrice4k(v float64) *GroupUpsert {
|
||||
u.Add(group.FieldImagePrice4k, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearImagePrice4k clears the value of the "image_price_4k" field.
|
||||
func (u *GroupUpsert) ClearImagePrice4k() *GroupUpsert {
|
||||
u.SetNull(group.FieldImagePrice4k)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateNewValues updates the mutable fields using the new values that were set on create.
|
||||
// Using this option is equivalent to using:
|
||||
//
|
||||
@@ -1185,6 +1311,90 @@ func (u *GroupUpsertOne) UpdateDefaultValidityDays() *GroupUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetImagePrice1k sets the "image_price_1k" field.
|
||||
func (u *GroupUpsertOne) SetImagePrice1k(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetImagePrice1k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddImagePrice1k adds v to the "image_price_1k" field.
|
||||
func (u *GroupUpsertOne) AddImagePrice1k(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddImagePrice1k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateImagePrice1k() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateImagePrice1k()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearImagePrice1k clears the value of the "image_price_1k" field.
|
||||
func (u *GroupUpsertOne) ClearImagePrice1k() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearImagePrice1k()
|
||||
})
|
||||
}
|
||||
|
||||
// SetImagePrice2k sets the "image_price_2k" field.
|
||||
func (u *GroupUpsertOne) SetImagePrice2k(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetImagePrice2k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddImagePrice2k adds v to the "image_price_2k" field.
|
||||
func (u *GroupUpsertOne) AddImagePrice2k(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddImagePrice2k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateImagePrice2k() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateImagePrice2k()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearImagePrice2k clears the value of the "image_price_2k" field.
|
||||
func (u *GroupUpsertOne) ClearImagePrice2k() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearImagePrice2k()
|
||||
})
|
||||
}
|
||||
|
||||
// SetImagePrice4k sets the "image_price_4k" field.
|
||||
func (u *GroupUpsertOne) SetImagePrice4k(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetImagePrice4k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddImagePrice4k adds v to the "image_price_4k" field.
|
||||
func (u *GroupUpsertOne) AddImagePrice4k(v float64) *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddImagePrice4k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create.
|
||||
func (u *GroupUpsertOne) UpdateImagePrice4k() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateImagePrice4k()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearImagePrice4k clears the value of the "image_price_4k" field.
|
||||
func (u *GroupUpsertOne) ClearImagePrice4k() *GroupUpsertOne {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearImagePrice4k()
|
||||
})
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (u *GroupUpsertOne) Exec(ctx context.Context) error {
|
||||
if len(u.create.conflict) == 0 {
|
||||
@@ -1648,6 +1858,90 @@ func (u *GroupUpsertBulk) UpdateDefaultValidityDays() *GroupUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetImagePrice1k sets the "image_price_1k" field.
|
||||
func (u *GroupUpsertBulk) SetImagePrice1k(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetImagePrice1k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddImagePrice1k adds v to the "image_price_1k" field.
|
||||
func (u *GroupUpsertBulk) AddImagePrice1k(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddImagePrice1k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateImagePrice1k() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateImagePrice1k()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearImagePrice1k clears the value of the "image_price_1k" field.
|
||||
func (u *GroupUpsertBulk) ClearImagePrice1k() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearImagePrice1k()
|
||||
})
|
||||
}
|
||||
|
||||
// SetImagePrice2k sets the "image_price_2k" field.
|
||||
func (u *GroupUpsertBulk) SetImagePrice2k(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetImagePrice2k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddImagePrice2k adds v to the "image_price_2k" field.
|
||||
func (u *GroupUpsertBulk) AddImagePrice2k(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddImagePrice2k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateImagePrice2k() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateImagePrice2k()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearImagePrice2k clears the value of the "image_price_2k" field.
|
||||
func (u *GroupUpsertBulk) ClearImagePrice2k() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearImagePrice2k()
|
||||
})
|
||||
}
|
||||
|
||||
// SetImagePrice4k sets the "image_price_4k" field.
|
||||
func (u *GroupUpsertBulk) SetImagePrice4k(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.SetImagePrice4k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddImagePrice4k adds v to the "image_price_4k" field.
|
||||
func (u *GroupUpsertBulk) AddImagePrice4k(v float64) *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.AddImagePrice4k(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create.
|
||||
func (u *GroupUpsertBulk) UpdateImagePrice4k() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.UpdateImagePrice4k()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearImagePrice4k clears the value of the "image_price_4k" field.
|
||||
func (u *GroupUpsertBulk) ClearImagePrice4k() *GroupUpsertBulk {
|
||||
return u.Update(func(s *GroupUpsert) {
|
||||
s.ClearImagePrice4k()
|
||||
})
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (u *GroupUpsertBulk) Exec(ctx context.Context) error {
|
||||
if u.create.err != nil {
|
||||
|
||||
@@ -273,6 +273,87 @@ func (_u *GroupUpdate) AddDefaultValidityDays(v int) *GroupUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetImagePrice1k sets the "image_price_1k" field.
|
||||
func (_u *GroupUpdate) SetImagePrice1k(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetImagePrice1k()
|
||||
_u.mutation.SetImagePrice1k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableImagePrice1k(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetImagePrice1k(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddImagePrice1k adds value to the "image_price_1k" field.
|
||||
func (_u *GroupUpdate) AddImagePrice1k(v float64) *GroupUpdate {
|
||||
_u.mutation.AddImagePrice1k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearImagePrice1k clears the value of the "image_price_1k" field.
|
||||
func (_u *GroupUpdate) ClearImagePrice1k() *GroupUpdate {
|
||||
_u.mutation.ClearImagePrice1k()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetImagePrice2k sets the "image_price_2k" field.
|
||||
func (_u *GroupUpdate) SetImagePrice2k(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetImagePrice2k()
|
||||
_u.mutation.SetImagePrice2k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableImagePrice2k(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetImagePrice2k(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddImagePrice2k adds value to the "image_price_2k" field.
|
||||
func (_u *GroupUpdate) AddImagePrice2k(v float64) *GroupUpdate {
|
||||
_u.mutation.AddImagePrice2k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearImagePrice2k clears the value of the "image_price_2k" field.
|
||||
func (_u *GroupUpdate) ClearImagePrice2k() *GroupUpdate {
|
||||
_u.mutation.ClearImagePrice2k()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetImagePrice4k sets the "image_price_4k" field.
|
||||
func (_u *GroupUpdate) SetImagePrice4k(v float64) *GroupUpdate {
|
||||
_u.mutation.ResetImagePrice4k()
|
||||
_u.mutation.SetImagePrice4k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil.
|
||||
func (_u *GroupUpdate) SetNillableImagePrice4k(v *float64) *GroupUpdate {
|
||||
if v != nil {
|
||||
_u.SetImagePrice4k(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddImagePrice4k adds value to the "image_price_4k" field.
|
||||
func (_u *GroupUpdate) AddImagePrice4k(v float64) *GroupUpdate {
|
||||
_u.mutation.AddImagePrice4k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearImagePrice4k clears the value of the "image_price_4k" field.
|
||||
func (_u *GroupUpdate) ClearImagePrice4k() *GroupUpdate {
|
||||
_u.mutation.ClearImagePrice4k()
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
|
||||
func (_u *GroupUpdate) AddAPIKeyIDs(ids ...int64) *GroupUpdate {
|
||||
_u.mutation.AddAPIKeyIDs(ids...)
|
||||
@@ -642,6 +723,33 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.AddedDefaultValidityDays(); ok {
|
||||
_spec.AddField(group.FieldDefaultValidityDays, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ImagePrice1k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedImagePrice1k(); ok {
|
||||
_spec.AddField(group.FieldImagePrice1k, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.ImagePrice1kCleared() {
|
||||
_spec.ClearField(group.FieldImagePrice1k, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.ImagePrice2k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedImagePrice2k(); ok {
|
||||
_spec.AddField(group.FieldImagePrice2k, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.ImagePrice2kCleared() {
|
||||
_spec.ClearField(group.FieldImagePrice2k, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.ImagePrice4k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedImagePrice4k(); ok {
|
||||
_spec.AddField(group.FieldImagePrice4k, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.ImagePrice4kCleared() {
|
||||
_spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64)
|
||||
}
|
||||
if _u.mutation.APIKeysCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
@@ -1195,6 +1303,87 @@ func (_u *GroupUpdateOne) AddDefaultValidityDays(v int) *GroupUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetImagePrice1k sets the "image_price_1k" field.
|
||||
func (_u *GroupUpdateOne) SetImagePrice1k(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetImagePrice1k()
|
||||
_u.mutation.SetImagePrice1k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableImagePrice1k(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetImagePrice1k(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddImagePrice1k adds value to the "image_price_1k" field.
|
||||
func (_u *GroupUpdateOne) AddImagePrice1k(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddImagePrice1k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearImagePrice1k clears the value of the "image_price_1k" field.
|
||||
func (_u *GroupUpdateOne) ClearImagePrice1k() *GroupUpdateOne {
|
||||
_u.mutation.ClearImagePrice1k()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetImagePrice2k sets the "image_price_2k" field.
|
||||
func (_u *GroupUpdateOne) SetImagePrice2k(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetImagePrice2k()
|
||||
_u.mutation.SetImagePrice2k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableImagePrice2k(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetImagePrice2k(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddImagePrice2k adds value to the "image_price_2k" field.
|
||||
func (_u *GroupUpdateOne) AddImagePrice2k(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddImagePrice2k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearImagePrice2k clears the value of the "image_price_2k" field.
|
||||
func (_u *GroupUpdateOne) ClearImagePrice2k() *GroupUpdateOne {
|
||||
_u.mutation.ClearImagePrice2k()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetImagePrice4k sets the "image_price_4k" field.
|
||||
func (_u *GroupUpdateOne) SetImagePrice4k(v float64) *GroupUpdateOne {
|
||||
_u.mutation.ResetImagePrice4k()
|
||||
_u.mutation.SetImagePrice4k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil.
|
||||
func (_u *GroupUpdateOne) SetNillableImagePrice4k(v *float64) *GroupUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetImagePrice4k(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddImagePrice4k adds value to the "image_price_4k" field.
|
||||
func (_u *GroupUpdateOne) AddImagePrice4k(v float64) *GroupUpdateOne {
|
||||
_u.mutation.AddImagePrice4k(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearImagePrice4k clears the value of the "image_price_4k" field.
|
||||
func (_u *GroupUpdateOne) ClearImagePrice4k() *GroupUpdateOne {
|
||||
_u.mutation.ClearImagePrice4k()
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
|
||||
func (_u *GroupUpdateOne) AddAPIKeyIDs(ids ...int64) *GroupUpdateOne {
|
||||
_u.mutation.AddAPIKeyIDs(ids...)
|
||||
@@ -1594,6 +1783,33 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
||||
if value, ok := _u.mutation.AddedDefaultValidityDays(); ok {
|
||||
_spec.AddField(group.FieldDefaultValidityDays, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ImagePrice1k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedImagePrice1k(); ok {
|
||||
_spec.AddField(group.FieldImagePrice1k, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.ImagePrice1kCleared() {
|
||||
_spec.ClearField(group.FieldImagePrice1k, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.ImagePrice2k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedImagePrice2k(); ok {
|
||||
_spec.AddField(group.FieldImagePrice2k, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.ImagePrice2kCleared() {
|
||||
_spec.ClearField(group.FieldImagePrice2k, field.TypeFloat64)
|
||||
}
|
||||
if value, ok := _u.mutation.ImagePrice4k(); ok {
|
||||
_spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedImagePrice4k(); ok {
|
||||
_spec.AddField(group.FieldImagePrice4k, field.TypeFloat64, value)
|
||||
}
|
||||
if _u.mutation.ImagePrice4kCleared() {
|
||||
_spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64)
|
||||
}
|
||||
if _u.mutation.APIKeysCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
||||
@@ -80,6 +80,8 @@ var (
|
||||
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
||||
{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: "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: "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"}},
|
||||
@@ -97,7 +99,7 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "accounts_proxies_proxy",
|
||||
Columns: []*schema.Column{AccountsColumns[22]},
|
||||
Columns: []*schema.Column{AccountsColumns[24]},
|
||||
RefColumns: []*schema.Column{ProxiesColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
@@ -121,7 +123,7 @@ var (
|
||||
{
|
||||
Name: "account_proxy_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{AccountsColumns[22]},
|
||||
Columns: []*schema.Column{AccountsColumns[24]},
|
||||
},
|
||||
{
|
||||
Name: "account_priority",
|
||||
@@ -136,22 +138,22 @@ var (
|
||||
{
|
||||
Name: "account_schedulable",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{AccountsColumns[15]},
|
||||
Columns: []*schema.Column{AccountsColumns[17]},
|
||||
},
|
||||
{
|
||||
Name: "account_rate_limited_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{AccountsColumns[16]},
|
||||
Columns: []*schema.Column{AccountsColumns[18]},
|
||||
},
|
||||
{
|
||||
Name: "account_rate_limit_reset_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{AccountsColumns[17]},
|
||||
Columns: []*schema.Column{AccountsColumns[19]},
|
||||
},
|
||||
{
|
||||
Name: "account_overload_until",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{AccountsColumns[18]},
|
||||
Columns: []*schema.Column{AccountsColumns[20]},
|
||||
},
|
||||
{
|
||||
Name: "account_deleted_at",
|
||||
@@ -216,6 +218,9 @@ var (
|
||||
{Name: "weekly_limit_usd", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "monthly_limit_usd", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "default_validity_days", Type: field.TypeInt, Default: 30},
|
||||
{Name: "image_price_1k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "image_price_2k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
{Name: "image_price_4k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
}
|
||||
// GroupsTable holds the schema information for the "groups" table.
|
||||
GroupsTable = &schema.Table{
|
||||
@@ -368,6 +373,9 @@ var (
|
||||
{Name: "stream", Type: field.TypeBool, Default: false},
|
||||
{Name: "duration_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_size", Type: field.TypeString, Nullable: true, Size: 10},
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "api_key_id", Type: field.TypeInt64},
|
||||
{Name: "account_id", Type: field.TypeInt64},
|
||||
@@ -383,31 +391,31 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "usage_logs_api_keys_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[21]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[24]},
|
||||
RefColumns: []*schema.Column{APIKeysColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_accounts_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[22]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[25]},
|
||||
RefColumns: []*schema.Column{AccountsColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_groups_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[23]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[26]},
|
||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_users_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[24]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[27]},
|
||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_user_subscriptions_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[25]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[28]},
|
||||
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
@@ -416,32 +424,32 @@ var (
|
||||
{
|
||||
Name: "usagelog_user_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[24]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[27]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_api_key_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[21]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[24]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_account_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[22]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[25]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_group_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[23]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[26]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_subscription_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[25]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[28]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[20]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[23]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_model",
|
||||
@@ -456,12 +464,12 @@ var (
|
||||
{
|
||||
Name: "usagelog_user_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[24], UsageLogsColumns[20]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[27], UsageLogsColumns[23]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_api_key_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[21], UsageLogsColumns[20]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[24], UsageLogsColumns[23]},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1006,6 +1006,8 @@ type AccountMutation struct {
|
||||
status *string
|
||||
error_message *string
|
||||
last_used_at *time.Time
|
||||
expires_at *time.Time
|
||||
auto_pause_on_expired *bool
|
||||
schedulable *bool
|
||||
rate_limited_at *time.Time
|
||||
rate_limit_reset_at *time.Time
|
||||
@@ -1770,6 +1772,91 @@ func (m *AccountMutation) ResetLastUsedAt() {
|
||||
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.
|
||||
func (m *AccountMutation) SetSchedulable(b bool) {
|
||||
m.schedulable = &b
|
||||
@@ -2269,7 +2356,7 @@ func (m *AccountMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *AccountMutation) Fields() []string {
|
||||
fields := make([]string, 0, 22)
|
||||
fields := make([]string, 0, 24)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, account.FieldCreatedAt)
|
||||
}
|
||||
@@ -2315,6 +2402,12 @@ func (m *AccountMutation) Fields() []string {
|
||||
if m.last_used_at != nil {
|
||||
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 {
|
||||
fields = append(fields, account.FieldSchedulable)
|
||||
}
|
||||
@@ -2374,6 +2467,10 @@ func (m *AccountMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.ErrorMessage()
|
||||
case account.FieldLastUsedAt:
|
||||
return m.LastUsedAt()
|
||||
case account.FieldExpiresAt:
|
||||
return m.ExpiresAt()
|
||||
case account.FieldAutoPauseOnExpired:
|
||||
return m.AutoPauseOnExpired()
|
||||
case account.FieldSchedulable:
|
||||
return m.Schedulable()
|
||||
case account.FieldRateLimitedAt:
|
||||
@@ -2427,6 +2524,10 @@ func (m *AccountMutation) OldField(ctx context.Context, name string) (ent.Value,
|
||||
return m.OldErrorMessage(ctx)
|
||||
case account.FieldLastUsedAt:
|
||||
return m.OldLastUsedAt(ctx)
|
||||
case account.FieldExpiresAt:
|
||||
return m.OldExpiresAt(ctx)
|
||||
case account.FieldAutoPauseOnExpired:
|
||||
return m.OldAutoPauseOnExpired(ctx)
|
||||
case account.FieldSchedulable:
|
||||
return m.OldSchedulable(ctx)
|
||||
case account.FieldRateLimitedAt:
|
||||
@@ -2555,6 +2656,20 @@ func (m *AccountMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetLastUsedAt(v)
|
||||
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:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
@@ -2676,6 +2791,9 @@ func (m *AccountMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(account.FieldLastUsedAt) {
|
||||
fields = append(fields, account.FieldLastUsedAt)
|
||||
}
|
||||
if m.FieldCleared(account.FieldExpiresAt) {
|
||||
fields = append(fields, account.FieldExpiresAt)
|
||||
}
|
||||
if m.FieldCleared(account.FieldRateLimitedAt) {
|
||||
fields = append(fields, account.FieldRateLimitedAt)
|
||||
}
|
||||
@@ -2723,6 +2841,9 @@ func (m *AccountMutation) ClearField(name string) error {
|
||||
case account.FieldLastUsedAt:
|
||||
m.ClearLastUsedAt()
|
||||
return nil
|
||||
case account.FieldExpiresAt:
|
||||
m.ClearExpiresAt()
|
||||
return nil
|
||||
case account.FieldRateLimitedAt:
|
||||
m.ClearRateLimitedAt()
|
||||
return nil
|
||||
@@ -2794,6 +2915,12 @@ func (m *AccountMutation) ResetField(name string) error {
|
||||
case account.FieldLastUsedAt:
|
||||
m.ResetLastUsedAt()
|
||||
return nil
|
||||
case account.FieldExpiresAt:
|
||||
m.ResetExpiresAt()
|
||||
return nil
|
||||
case account.FieldAutoPauseOnExpired:
|
||||
m.ResetAutoPauseOnExpired()
|
||||
return nil
|
||||
case account.FieldSchedulable:
|
||||
m.ResetSchedulable()
|
||||
return nil
|
||||
@@ -3457,6 +3584,12 @@ type GroupMutation struct {
|
||||
addmonthly_limit_usd *float64
|
||||
default_validity_days *int
|
||||
adddefault_validity_days *int
|
||||
image_price_1k *float64
|
||||
addimage_price_1k *float64
|
||||
image_price_2k *float64
|
||||
addimage_price_2k *float64
|
||||
image_price_4k *float64
|
||||
addimage_price_4k *float64
|
||||
clearedFields map[string]struct{}
|
||||
api_keys map[int64]struct{}
|
||||
removedapi_keys map[int64]struct{}
|
||||
@@ -4251,6 +4384,216 @@ func (m *GroupMutation) ResetDefaultValidityDays() {
|
||||
m.adddefault_validity_days = nil
|
||||
}
|
||||
|
||||
// SetImagePrice1k sets the "image_price_1k" field.
|
||||
func (m *GroupMutation) SetImagePrice1k(f float64) {
|
||||
m.image_price_1k = &f
|
||||
m.addimage_price_1k = nil
|
||||
}
|
||||
|
||||
// ImagePrice1k returns the value of the "image_price_1k" field in the mutation.
|
||||
func (m *GroupMutation) ImagePrice1k() (r float64, exists bool) {
|
||||
v := m.image_price_1k
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldImagePrice1k returns the old "image_price_1k" field's value of the Group entity.
|
||||
// If the Group 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 *GroupMutation) OldImagePrice1k(ctx context.Context) (v *float64, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldImagePrice1k is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldImagePrice1k requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldImagePrice1k: %w", err)
|
||||
}
|
||||
return oldValue.ImagePrice1k, nil
|
||||
}
|
||||
|
||||
// AddImagePrice1k adds f to the "image_price_1k" field.
|
||||
func (m *GroupMutation) AddImagePrice1k(f float64) {
|
||||
if m.addimage_price_1k != nil {
|
||||
*m.addimage_price_1k += f
|
||||
} else {
|
||||
m.addimage_price_1k = &f
|
||||
}
|
||||
}
|
||||
|
||||
// AddedImagePrice1k returns the value that was added to the "image_price_1k" field in this mutation.
|
||||
func (m *GroupMutation) AddedImagePrice1k() (r float64, exists bool) {
|
||||
v := m.addimage_price_1k
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ClearImagePrice1k clears the value of the "image_price_1k" field.
|
||||
func (m *GroupMutation) ClearImagePrice1k() {
|
||||
m.image_price_1k = nil
|
||||
m.addimage_price_1k = nil
|
||||
m.clearedFields[group.FieldImagePrice1k] = struct{}{}
|
||||
}
|
||||
|
||||
// ImagePrice1kCleared returns if the "image_price_1k" field was cleared in this mutation.
|
||||
func (m *GroupMutation) ImagePrice1kCleared() bool {
|
||||
_, ok := m.clearedFields[group.FieldImagePrice1k]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetImagePrice1k resets all changes to the "image_price_1k" field.
|
||||
func (m *GroupMutation) ResetImagePrice1k() {
|
||||
m.image_price_1k = nil
|
||||
m.addimage_price_1k = nil
|
||||
delete(m.clearedFields, group.FieldImagePrice1k)
|
||||
}
|
||||
|
||||
// SetImagePrice2k sets the "image_price_2k" field.
|
||||
func (m *GroupMutation) SetImagePrice2k(f float64) {
|
||||
m.image_price_2k = &f
|
||||
m.addimage_price_2k = nil
|
||||
}
|
||||
|
||||
// ImagePrice2k returns the value of the "image_price_2k" field in the mutation.
|
||||
func (m *GroupMutation) ImagePrice2k() (r float64, exists bool) {
|
||||
v := m.image_price_2k
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldImagePrice2k returns the old "image_price_2k" field's value of the Group entity.
|
||||
// If the Group 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 *GroupMutation) OldImagePrice2k(ctx context.Context) (v *float64, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldImagePrice2k is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldImagePrice2k requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldImagePrice2k: %w", err)
|
||||
}
|
||||
return oldValue.ImagePrice2k, nil
|
||||
}
|
||||
|
||||
// AddImagePrice2k adds f to the "image_price_2k" field.
|
||||
func (m *GroupMutation) AddImagePrice2k(f float64) {
|
||||
if m.addimage_price_2k != nil {
|
||||
*m.addimage_price_2k += f
|
||||
} else {
|
||||
m.addimage_price_2k = &f
|
||||
}
|
||||
}
|
||||
|
||||
// AddedImagePrice2k returns the value that was added to the "image_price_2k" field in this mutation.
|
||||
func (m *GroupMutation) AddedImagePrice2k() (r float64, exists bool) {
|
||||
v := m.addimage_price_2k
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ClearImagePrice2k clears the value of the "image_price_2k" field.
|
||||
func (m *GroupMutation) ClearImagePrice2k() {
|
||||
m.image_price_2k = nil
|
||||
m.addimage_price_2k = nil
|
||||
m.clearedFields[group.FieldImagePrice2k] = struct{}{}
|
||||
}
|
||||
|
||||
// ImagePrice2kCleared returns if the "image_price_2k" field was cleared in this mutation.
|
||||
func (m *GroupMutation) ImagePrice2kCleared() bool {
|
||||
_, ok := m.clearedFields[group.FieldImagePrice2k]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetImagePrice2k resets all changes to the "image_price_2k" field.
|
||||
func (m *GroupMutation) ResetImagePrice2k() {
|
||||
m.image_price_2k = nil
|
||||
m.addimage_price_2k = nil
|
||||
delete(m.clearedFields, group.FieldImagePrice2k)
|
||||
}
|
||||
|
||||
// SetImagePrice4k sets the "image_price_4k" field.
|
||||
func (m *GroupMutation) SetImagePrice4k(f float64) {
|
||||
m.image_price_4k = &f
|
||||
m.addimage_price_4k = nil
|
||||
}
|
||||
|
||||
// ImagePrice4k returns the value of the "image_price_4k" field in the mutation.
|
||||
func (m *GroupMutation) ImagePrice4k() (r float64, exists bool) {
|
||||
v := m.image_price_4k
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldImagePrice4k returns the old "image_price_4k" field's value of the Group entity.
|
||||
// If the Group 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 *GroupMutation) OldImagePrice4k(ctx context.Context) (v *float64, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldImagePrice4k is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldImagePrice4k requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldImagePrice4k: %w", err)
|
||||
}
|
||||
return oldValue.ImagePrice4k, nil
|
||||
}
|
||||
|
||||
// AddImagePrice4k adds f to the "image_price_4k" field.
|
||||
func (m *GroupMutation) AddImagePrice4k(f float64) {
|
||||
if m.addimage_price_4k != nil {
|
||||
*m.addimage_price_4k += f
|
||||
} else {
|
||||
m.addimage_price_4k = &f
|
||||
}
|
||||
}
|
||||
|
||||
// AddedImagePrice4k returns the value that was added to the "image_price_4k" field in this mutation.
|
||||
func (m *GroupMutation) AddedImagePrice4k() (r float64, exists bool) {
|
||||
v := m.addimage_price_4k
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ClearImagePrice4k clears the value of the "image_price_4k" field.
|
||||
func (m *GroupMutation) ClearImagePrice4k() {
|
||||
m.image_price_4k = nil
|
||||
m.addimage_price_4k = nil
|
||||
m.clearedFields[group.FieldImagePrice4k] = struct{}{}
|
||||
}
|
||||
|
||||
// ImagePrice4kCleared returns if the "image_price_4k" field was cleared in this mutation.
|
||||
func (m *GroupMutation) ImagePrice4kCleared() bool {
|
||||
_, ok := m.clearedFields[group.FieldImagePrice4k]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetImagePrice4k resets all changes to the "image_price_4k" field.
|
||||
func (m *GroupMutation) ResetImagePrice4k() {
|
||||
m.image_price_4k = nil
|
||||
m.addimage_price_4k = nil
|
||||
delete(m.clearedFields, group.FieldImagePrice4k)
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids.
|
||||
func (m *GroupMutation) AddAPIKeyIDs(ids ...int64) {
|
||||
if m.api_keys == nil {
|
||||
@@ -4609,7 +4952,7 @@ func (m *GroupMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *GroupMutation) Fields() []string {
|
||||
fields := make([]string, 0, 14)
|
||||
fields := make([]string, 0, 17)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, group.FieldCreatedAt)
|
||||
}
|
||||
@@ -4652,6 +4995,15 @@ func (m *GroupMutation) Fields() []string {
|
||||
if m.default_validity_days != nil {
|
||||
fields = append(fields, group.FieldDefaultValidityDays)
|
||||
}
|
||||
if m.image_price_1k != nil {
|
||||
fields = append(fields, group.FieldImagePrice1k)
|
||||
}
|
||||
if m.image_price_2k != nil {
|
||||
fields = append(fields, group.FieldImagePrice2k)
|
||||
}
|
||||
if m.image_price_4k != nil {
|
||||
fields = append(fields, group.FieldImagePrice4k)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -4688,6 +5040,12 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.MonthlyLimitUsd()
|
||||
case group.FieldDefaultValidityDays:
|
||||
return m.DefaultValidityDays()
|
||||
case group.FieldImagePrice1k:
|
||||
return m.ImagePrice1k()
|
||||
case group.FieldImagePrice2k:
|
||||
return m.ImagePrice2k()
|
||||
case group.FieldImagePrice4k:
|
||||
return m.ImagePrice4k()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -4725,6 +5083,12 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e
|
||||
return m.OldMonthlyLimitUsd(ctx)
|
||||
case group.FieldDefaultValidityDays:
|
||||
return m.OldDefaultValidityDays(ctx)
|
||||
case group.FieldImagePrice1k:
|
||||
return m.OldImagePrice1k(ctx)
|
||||
case group.FieldImagePrice2k:
|
||||
return m.OldImagePrice2k(ctx)
|
||||
case group.FieldImagePrice4k:
|
||||
return m.OldImagePrice4k(ctx)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown Group field %s", name)
|
||||
}
|
||||
@@ -4832,6 +5196,27 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetDefaultValidityDays(v)
|
||||
return nil
|
||||
case group.FieldImagePrice1k:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetImagePrice1k(v)
|
||||
return nil
|
||||
case group.FieldImagePrice2k:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetImagePrice2k(v)
|
||||
return nil
|
||||
case group.FieldImagePrice4k:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetImagePrice4k(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Group field %s", name)
|
||||
}
|
||||
@@ -4855,6 +5240,15 @@ func (m *GroupMutation) AddedFields() []string {
|
||||
if m.adddefault_validity_days != nil {
|
||||
fields = append(fields, group.FieldDefaultValidityDays)
|
||||
}
|
||||
if m.addimage_price_1k != nil {
|
||||
fields = append(fields, group.FieldImagePrice1k)
|
||||
}
|
||||
if m.addimage_price_2k != nil {
|
||||
fields = append(fields, group.FieldImagePrice2k)
|
||||
}
|
||||
if m.addimage_price_4k != nil {
|
||||
fields = append(fields, group.FieldImagePrice4k)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -4873,6 +5267,12 @@ func (m *GroupMutation) AddedField(name string) (ent.Value, bool) {
|
||||
return m.AddedMonthlyLimitUsd()
|
||||
case group.FieldDefaultValidityDays:
|
||||
return m.AddedDefaultValidityDays()
|
||||
case group.FieldImagePrice1k:
|
||||
return m.AddedImagePrice1k()
|
||||
case group.FieldImagePrice2k:
|
||||
return m.AddedImagePrice2k()
|
||||
case group.FieldImagePrice4k:
|
||||
return m.AddedImagePrice4k()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -4917,6 +5317,27 @@ func (m *GroupMutation) AddField(name string, value ent.Value) error {
|
||||
}
|
||||
m.AddDefaultValidityDays(v)
|
||||
return nil
|
||||
case group.FieldImagePrice1k:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddImagePrice1k(v)
|
||||
return nil
|
||||
case group.FieldImagePrice2k:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddImagePrice2k(v)
|
||||
return nil
|
||||
case group.FieldImagePrice4k:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddImagePrice4k(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Group numeric field %s", name)
|
||||
}
|
||||
@@ -4940,6 +5361,15 @@ func (m *GroupMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(group.FieldMonthlyLimitUsd) {
|
||||
fields = append(fields, group.FieldMonthlyLimitUsd)
|
||||
}
|
||||
if m.FieldCleared(group.FieldImagePrice1k) {
|
||||
fields = append(fields, group.FieldImagePrice1k)
|
||||
}
|
||||
if m.FieldCleared(group.FieldImagePrice2k) {
|
||||
fields = append(fields, group.FieldImagePrice2k)
|
||||
}
|
||||
if m.FieldCleared(group.FieldImagePrice4k) {
|
||||
fields = append(fields, group.FieldImagePrice4k)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -4969,6 +5399,15 @@ func (m *GroupMutation) ClearField(name string) error {
|
||||
case group.FieldMonthlyLimitUsd:
|
||||
m.ClearMonthlyLimitUsd()
|
||||
return nil
|
||||
case group.FieldImagePrice1k:
|
||||
m.ClearImagePrice1k()
|
||||
return nil
|
||||
case group.FieldImagePrice2k:
|
||||
m.ClearImagePrice2k()
|
||||
return nil
|
||||
case group.FieldImagePrice4k:
|
||||
m.ClearImagePrice4k()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Group nullable field %s", name)
|
||||
}
|
||||
@@ -5019,6 +5458,15 @@ func (m *GroupMutation) ResetField(name string) error {
|
||||
case group.FieldDefaultValidityDays:
|
||||
m.ResetDefaultValidityDays()
|
||||
return nil
|
||||
case group.FieldImagePrice1k:
|
||||
m.ResetImagePrice1k()
|
||||
return nil
|
||||
case group.FieldImagePrice2k:
|
||||
m.ResetImagePrice2k()
|
||||
return nil
|
||||
case group.FieldImagePrice4k:
|
||||
m.ResetImagePrice4k()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Group field %s", name)
|
||||
}
|
||||
@@ -7786,6 +8234,10 @@ type UsageLogMutation struct {
|
||||
addduration_ms *int
|
||||
first_token_ms *int
|
||||
addfirst_token_ms *int
|
||||
user_agent *string
|
||||
image_count *int
|
||||
addimage_count *int
|
||||
image_size *string
|
||||
created_at *time.Time
|
||||
clearedFields map[string]struct{}
|
||||
user *int64
|
||||
@@ -9139,6 +9591,160 @@ func (m *UsageLogMutation) ResetFirstTokenMs() {
|
||||
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.
|
||||
func (m *UsageLogMutation) SetImageCount(i int) {
|
||||
m.image_count = &i
|
||||
m.addimage_count = nil
|
||||
}
|
||||
|
||||
// ImageCount returns the value of the "image_count" field in the mutation.
|
||||
func (m *UsageLogMutation) ImageCount() (r int, exists bool) {
|
||||
v := m.image_count
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldImageCount returns the old "image_count" 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) OldImageCount(ctx context.Context) (v int, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldImageCount is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldImageCount requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldImageCount: %w", err)
|
||||
}
|
||||
return oldValue.ImageCount, nil
|
||||
}
|
||||
|
||||
// AddImageCount adds i to the "image_count" field.
|
||||
func (m *UsageLogMutation) AddImageCount(i int) {
|
||||
if m.addimage_count != nil {
|
||||
*m.addimage_count += i
|
||||
} else {
|
||||
m.addimage_count = &i
|
||||
}
|
||||
}
|
||||
|
||||
// AddedImageCount returns the value that was added to the "image_count" field in this mutation.
|
||||
func (m *UsageLogMutation) AddedImageCount() (r int, exists bool) {
|
||||
v := m.addimage_count
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ResetImageCount resets all changes to the "image_count" field.
|
||||
func (m *UsageLogMutation) ResetImageCount() {
|
||||
m.image_count = nil
|
||||
m.addimage_count = nil
|
||||
}
|
||||
|
||||
// SetImageSize sets the "image_size" field.
|
||||
func (m *UsageLogMutation) SetImageSize(s string) {
|
||||
m.image_size = &s
|
||||
}
|
||||
|
||||
// ImageSize returns the value of the "image_size" field in the mutation.
|
||||
func (m *UsageLogMutation) ImageSize() (r string, exists bool) {
|
||||
v := m.image_size
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldImageSize returns the old "image_size" 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) OldImageSize(ctx context.Context) (v *string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldImageSize is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldImageSize requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldImageSize: %w", err)
|
||||
}
|
||||
return oldValue.ImageSize, nil
|
||||
}
|
||||
|
||||
// ClearImageSize clears the value of the "image_size" field.
|
||||
func (m *UsageLogMutation) ClearImageSize() {
|
||||
m.image_size = nil
|
||||
m.clearedFields[usagelog.FieldImageSize] = struct{}{}
|
||||
}
|
||||
|
||||
// ImageSizeCleared returns if the "image_size" field was cleared in this mutation.
|
||||
func (m *UsageLogMutation) ImageSizeCleared() bool {
|
||||
_, ok := m.clearedFields[usagelog.FieldImageSize]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetImageSize resets all changes to the "image_size" field.
|
||||
func (m *UsageLogMutation) ResetImageSize() {
|
||||
m.image_size = nil
|
||||
delete(m.clearedFields, usagelog.FieldImageSize)
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the "created_at" field.
|
||||
func (m *UsageLogMutation) SetCreatedAt(t time.Time) {
|
||||
m.created_at = &t
|
||||
@@ -9344,7 +9950,7 @@ func (m *UsageLogMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *UsageLogMutation) Fields() []string {
|
||||
fields := make([]string, 0, 25)
|
||||
fields := make([]string, 0, 28)
|
||||
if m.user != nil {
|
||||
fields = append(fields, usagelog.FieldUserID)
|
||||
}
|
||||
@@ -9417,6 +10023,15 @@ func (m *UsageLogMutation) Fields() []string {
|
||||
if m.first_token_ms != nil {
|
||||
fields = append(fields, usagelog.FieldFirstTokenMs)
|
||||
}
|
||||
if m.user_agent != nil {
|
||||
fields = append(fields, usagelog.FieldUserAgent)
|
||||
}
|
||||
if m.image_count != nil {
|
||||
fields = append(fields, usagelog.FieldImageCount)
|
||||
}
|
||||
if m.image_size != nil {
|
||||
fields = append(fields, usagelog.FieldImageSize)
|
||||
}
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, usagelog.FieldCreatedAt)
|
||||
}
|
||||
@@ -9476,6 +10091,12 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.DurationMs()
|
||||
case usagelog.FieldFirstTokenMs:
|
||||
return m.FirstTokenMs()
|
||||
case usagelog.FieldUserAgent:
|
||||
return m.UserAgent()
|
||||
case usagelog.FieldImageCount:
|
||||
return m.ImageCount()
|
||||
case usagelog.FieldImageSize:
|
||||
return m.ImageSize()
|
||||
case usagelog.FieldCreatedAt:
|
||||
return m.CreatedAt()
|
||||
}
|
||||
@@ -9535,6 +10156,12 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value
|
||||
return m.OldDurationMs(ctx)
|
||||
case usagelog.FieldFirstTokenMs:
|
||||
return m.OldFirstTokenMs(ctx)
|
||||
case usagelog.FieldUserAgent:
|
||||
return m.OldUserAgent(ctx)
|
||||
case usagelog.FieldImageCount:
|
||||
return m.OldImageCount(ctx)
|
||||
case usagelog.FieldImageSize:
|
||||
return m.OldImageSize(ctx)
|
||||
case usagelog.FieldCreatedAt:
|
||||
return m.OldCreatedAt(ctx)
|
||||
}
|
||||
@@ -9714,6 +10341,27 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetFirstTokenMs(v)
|
||||
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:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetImageCount(v)
|
||||
return nil
|
||||
case usagelog.FieldImageSize:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetImageSize(v)
|
||||
return nil
|
||||
case usagelog.FieldCreatedAt:
|
||||
v, ok := value.(time.Time)
|
||||
if !ok {
|
||||
@@ -9777,6 +10425,9 @@ func (m *UsageLogMutation) AddedFields() []string {
|
||||
if m.addfirst_token_ms != nil {
|
||||
fields = append(fields, usagelog.FieldFirstTokenMs)
|
||||
}
|
||||
if m.addimage_count != nil {
|
||||
fields = append(fields, usagelog.FieldImageCount)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -9817,6 +10468,8 @@ func (m *UsageLogMutation) AddedField(name string) (ent.Value, bool) {
|
||||
return m.AddedDurationMs()
|
||||
case usagelog.FieldFirstTokenMs:
|
||||
return m.AddedFirstTokenMs()
|
||||
case usagelog.FieldImageCount:
|
||||
return m.AddedImageCount()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -9938,6 +10591,13 @@ func (m *UsageLogMutation) AddField(name string, value ent.Value) error {
|
||||
}
|
||||
m.AddFirstTokenMs(v)
|
||||
return nil
|
||||
case usagelog.FieldImageCount:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddImageCount(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown UsageLog numeric field %s", name)
|
||||
}
|
||||
@@ -9958,6 +10618,12 @@ func (m *UsageLogMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(usagelog.FieldFirstTokenMs) {
|
||||
fields = append(fields, usagelog.FieldFirstTokenMs)
|
||||
}
|
||||
if m.FieldCleared(usagelog.FieldUserAgent) {
|
||||
fields = append(fields, usagelog.FieldUserAgent)
|
||||
}
|
||||
if m.FieldCleared(usagelog.FieldImageSize) {
|
||||
fields = append(fields, usagelog.FieldImageSize)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -9984,6 +10650,12 @@ func (m *UsageLogMutation) ClearField(name string) error {
|
||||
case usagelog.FieldFirstTokenMs:
|
||||
m.ClearFirstTokenMs()
|
||||
return nil
|
||||
case usagelog.FieldUserAgent:
|
||||
m.ClearUserAgent()
|
||||
return nil
|
||||
case usagelog.FieldImageSize:
|
||||
m.ClearImageSize()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown UsageLog nullable field %s", name)
|
||||
}
|
||||
@@ -10064,6 +10736,15 @@ func (m *UsageLogMutation) ResetField(name string) error {
|
||||
case usagelog.FieldFirstTokenMs:
|
||||
m.ResetFirstTokenMs()
|
||||
return nil
|
||||
case usagelog.FieldUserAgent:
|
||||
m.ResetUserAgent()
|
||||
return nil
|
||||
case usagelog.FieldImageCount:
|
||||
m.ResetImageCount()
|
||||
return nil
|
||||
case usagelog.FieldImageSize:
|
||||
m.ResetImageSize()
|
||||
return nil
|
||||
case usagelog.FieldCreatedAt:
|
||||
m.ResetCreatedAt()
|
||||
return nil
|
||||
|
||||
@@ -181,12 +181,16 @@ func init() {
|
||||
account.DefaultStatus = accountDescStatus.Default.(string)
|
||||
// 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)
|
||||
// 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 := accountFields[12].Descriptor()
|
||||
accountDescSchedulable := accountFields[14].Descriptor()
|
||||
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
|
||||
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
|
||||
// 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 = accountDescSessionWindowStatus.Validators[0].(func(string) error)
|
||||
accountgroupFields := schema.AccountGroup{}.Fields()
|
||||
@@ -521,8 +525,20 @@ func init() {
|
||||
usagelogDescStream := usagelogFields[21].Descriptor()
|
||||
// usagelog.DefaultStream holds the default value on creation for the stream field.
|
||||
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 := usagelogFields[25].Descriptor()
|
||||
// usagelog.DefaultImageCount holds the default value on creation for the image_count field.
|
||||
usagelog.DefaultImageCount = usagelogDescImageCount.Default.(int)
|
||||
// usagelogDescImageSize is the schema descriptor for image_size field.
|
||||
usagelogDescImageSize := usagelogFields[26].Descriptor()
|
||||
// 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)
|
||||
// usagelogDescCreatedAt is the schema descriptor for created_at field.
|
||||
usagelogDescCreatedAt := usagelogFields[24].Descriptor()
|
||||
usagelogDescCreatedAt := usagelogFields[27].Descriptor()
|
||||
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
|
||||
userMixin := schema.User{}.Mixin()
|
||||
|
||||
@@ -118,6 +118,16 @@ func (Account) Fields() []ent.Field {
|
||||
Optional().
|
||||
Nillable().
|
||||
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 中添加
|
||||
|
||||
@@ -72,6 +72,20 @@ func (Group) Fields() []ent.Field {
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||
field.Int("default_validity_days").
|
||||
Default(30),
|
||||
|
||||
// 图片生成计费配置(antigravity 和 gemini 平台使用)
|
||||
field.Float("image_price_1k").
|
||||
Optional().
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||
field.Float("image_price_2k").
|
||||
Optional().
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||
field.Float("image_price_4k").
|
||||
Optional().
|
||||
Nillable().
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,18 @@ func (UsageLog) Fields() []ent.Field {
|
||||
field.Int("first_token_ms").
|
||||
Optional().
|
||||
Nillable(),
|
||||
field.String("user_agent").
|
||||
MaxLen(512).
|
||||
Optional().
|
||||
Nillable(),
|
||||
|
||||
// 图片生成字段(仅 gemini-3-pro-image 等图片模型使用)
|
||||
field.Int("image_count").
|
||||
Default(0),
|
||||
field.String("image_size").
|
||||
MaxLen(10).
|
||||
Optional().
|
||||
Nillable(),
|
||||
|
||||
// 时间戳(只有 created_at,日志不可修改)
|
||||
field.Time("created_at").
|
||||
|
||||
@@ -70,6 +70,12 @@ type UsageLog struct {
|
||||
DurationMs *int `json:"duration_ms,omitempty"`
|
||||
// FirstTokenMs holds the value of the "first_token_ms" field.
|
||||
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 int `json:"image_count,omitempty"`
|
||||
// ImageSize holds the value of the "image_size" field.
|
||||
ImageSize *string `json:"image_size,omitempty"`
|
||||
// CreatedAt holds the value of the "created_at" field.
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
@@ -159,9 +165,9 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new(sql.NullBool)
|
||||
case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier:
|
||||
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:
|
||||
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)
|
||||
case usagelog.FieldRequestID, usagelog.FieldModel:
|
||||
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldUserAgent, usagelog.FieldImageSize:
|
||||
values[i] = new(sql.NullString)
|
||||
case usagelog.FieldCreatedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
@@ -334,6 +340,26 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
|
||||
_m.FirstTokenMs = new(int)
|
||||
*_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:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field image_count", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ImageCount = int(value.Int64)
|
||||
}
|
||||
case usagelog.FieldImageSize:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field image_size", values[i])
|
||||
} else if value.Valid {
|
||||
_m.ImageSize = new(string)
|
||||
*_m.ImageSize = value.String
|
||||
}
|
||||
case usagelog.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
||||
@@ -481,6 +507,19 @@ func (_m *UsageLog) String() string {
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.UserAgent; v != nil {
|
||||
builder.WriteString("user_agent=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("image_count=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ImageCount))
|
||||
builder.WriteString(", ")
|
||||
if v := _m.ImageSize; v != nil {
|
||||
builder.WriteString("image_size=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
|
||||
builder.WriteByte(')')
|
||||
|
||||
@@ -62,6 +62,12 @@ const (
|
||||
FieldDurationMs = "duration_ms"
|
||||
// FieldFirstTokenMs holds the string denoting the first_token_ms field in the database.
|
||||
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 = "image_count"
|
||||
// FieldImageSize holds the string denoting the image_size field in the database.
|
||||
FieldImageSize = "image_size"
|
||||
// FieldCreatedAt holds the string denoting the created_at field in the database.
|
||||
FieldCreatedAt = "created_at"
|
||||
// EdgeUser holds the string denoting the user edge name in mutations.
|
||||
@@ -140,6 +146,9 @@ var Columns = []string{
|
||||
FieldStream,
|
||||
FieldDurationMs,
|
||||
FieldFirstTokenMs,
|
||||
FieldUserAgent,
|
||||
FieldImageCount,
|
||||
FieldImageSize,
|
||||
FieldCreatedAt,
|
||||
}
|
||||
|
||||
@@ -188,6 +197,12 @@ var (
|
||||
DefaultBillingType int8
|
||||
// DefaultStream holds the default value on creation for the "stream" field.
|
||||
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 int
|
||||
// ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
||||
ImageSizeValidator func(string) error
|
||||
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
||||
DefaultCreatedAt func() time.Time
|
||||
)
|
||||
@@ -320,6 +335,21 @@ func ByFirstTokenMs(opts ...sql.OrderTermOption) OrderOption {
|
||||
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.
|
||||
func ByImageCount(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldImageCount, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByImageSize orders the results by the image_size field.
|
||||
func ByImageSize(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldImageSize, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCreatedAt orders the results by the created_at field.
|
||||
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
|
||||
|
||||
@@ -175,6 +175,21 @@ func FirstTokenMs(v int) predicate.UsageLog {
|
||||
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.
|
||||
func ImageCount(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v))
|
||||
}
|
||||
|
||||
// ImageSize applies equality check predicate on the "image_size" field. It's identical to ImageSizeEQ.
|
||||
func ImageSize(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
|
||||
func CreatedAt(v time.Time) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v))
|
||||
@@ -1100,6 +1115,196 @@ func FirstTokenMsNotNil() predicate.UsageLog {
|
||||
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.
|
||||
func ImageCountEQ(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v))
|
||||
}
|
||||
|
||||
// ImageCountNEQ applies the NEQ predicate on the "image_count" field.
|
||||
func ImageCountNEQ(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNEQ(FieldImageCount, v))
|
||||
}
|
||||
|
||||
// ImageCountIn applies the In predicate on the "image_count" field.
|
||||
func ImageCountIn(vs ...int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIn(FieldImageCount, vs...))
|
||||
}
|
||||
|
||||
// ImageCountNotIn applies the NotIn predicate on the "image_count" field.
|
||||
func ImageCountNotIn(vs ...int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotIn(FieldImageCount, vs...))
|
||||
}
|
||||
|
||||
// ImageCountGT applies the GT predicate on the "image_count" field.
|
||||
func ImageCountGT(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGT(FieldImageCount, v))
|
||||
}
|
||||
|
||||
// ImageCountGTE applies the GTE predicate on the "image_count" field.
|
||||
func ImageCountGTE(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGTE(FieldImageCount, v))
|
||||
}
|
||||
|
||||
// ImageCountLT applies the LT predicate on the "image_count" field.
|
||||
func ImageCountLT(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLT(FieldImageCount, v))
|
||||
}
|
||||
|
||||
// ImageCountLTE applies the LTE predicate on the "image_count" field.
|
||||
func ImageCountLTE(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLTE(FieldImageCount, v))
|
||||
}
|
||||
|
||||
// ImageSizeEQ applies the EQ predicate on the "image_size" field.
|
||||
func ImageSizeEQ(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeNEQ applies the NEQ predicate on the "image_size" field.
|
||||
func ImageSizeNEQ(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNEQ(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeIn applies the In predicate on the "image_size" field.
|
||||
func ImageSizeIn(vs ...string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIn(FieldImageSize, vs...))
|
||||
}
|
||||
|
||||
// ImageSizeNotIn applies the NotIn predicate on the "image_size" field.
|
||||
func ImageSizeNotIn(vs ...string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotIn(FieldImageSize, vs...))
|
||||
}
|
||||
|
||||
// ImageSizeGT applies the GT predicate on the "image_size" field.
|
||||
func ImageSizeGT(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGT(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeGTE applies the GTE predicate on the "image_size" field.
|
||||
func ImageSizeGTE(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGTE(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeLT applies the LT predicate on the "image_size" field.
|
||||
func ImageSizeLT(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLT(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeLTE applies the LTE predicate on the "image_size" field.
|
||||
func ImageSizeLTE(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLTE(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeContains applies the Contains predicate on the "image_size" field.
|
||||
func ImageSizeContains(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldContains(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeHasPrefix applies the HasPrefix predicate on the "image_size" field.
|
||||
func ImageSizeHasPrefix(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldHasPrefix(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeHasSuffix applies the HasSuffix predicate on the "image_size" field.
|
||||
func ImageSizeHasSuffix(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldHasSuffix(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeIsNil applies the IsNil predicate on the "image_size" field.
|
||||
func ImageSizeIsNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIsNull(FieldImageSize))
|
||||
}
|
||||
|
||||
// ImageSizeNotNil applies the NotNil predicate on the "image_size" field.
|
||||
func ImageSizeNotNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotNull(FieldImageSize))
|
||||
}
|
||||
|
||||
// ImageSizeEqualFold applies the EqualFold predicate on the "image_size" field.
|
||||
func ImageSizeEqualFold(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEqualFold(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// ImageSizeContainsFold applies the ContainsFold predicate on the "image_size" field.
|
||||
func ImageSizeContainsFold(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldContainsFold(FieldImageSize, v))
|
||||
}
|
||||
|
||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||
func CreatedAtEQ(v time.Time) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v))
|
||||
|
||||
@@ -323,6 +323,48 @@ func (_c *UsageLogCreate) SetNillableFirstTokenMs(v *int) *UsageLogCreate {
|
||||
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.
|
||||
func (_c *UsageLogCreate) SetImageCount(v int) *UsageLogCreate {
|
||||
_c.mutation.SetImageCount(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableImageCount sets the "image_count" field if the given value is not nil.
|
||||
func (_c *UsageLogCreate) SetNillableImageCount(v *int) *UsageLogCreate {
|
||||
if v != nil {
|
||||
_c.SetImageCount(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetImageSize sets the "image_size" field.
|
||||
func (_c *UsageLogCreate) SetImageSize(v string) *UsageLogCreate {
|
||||
_c.mutation.SetImageSize(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableImageSize sets the "image_size" field if the given value is not nil.
|
||||
func (_c *UsageLogCreate) SetNillableImageSize(v *string) *UsageLogCreate {
|
||||
if v != nil {
|
||||
_c.SetImageSize(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the "created_at" field.
|
||||
func (_c *UsageLogCreate) SetCreatedAt(v time.Time) *UsageLogCreate {
|
||||
_c.mutation.SetCreatedAt(v)
|
||||
@@ -457,6 +499,10 @@ func (_c *UsageLogCreate) defaults() {
|
||||
v := usagelog.DefaultStream
|
||||
_c.mutation.SetStream(v)
|
||||
}
|
||||
if _, ok := _c.mutation.ImageCount(); !ok {
|
||||
v := usagelog.DefaultImageCount
|
||||
_c.mutation.SetImageCount(v)
|
||||
}
|
||||
if _, ok := _c.mutation.CreatedAt(); !ok {
|
||||
v := usagelog.DefaultCreatedAt()
|
||||
_c.mutation.SetCreatedAt(v)
|
||||
@@ -535,6 +581,19 @@ func (_c *UsageLogCreate) check() error {
|
||||
if _, ok := _c.mutation.Stream(); !ok {
|
||||
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 {
|
||||
return &ValidationError{Name: "image_count", err: errors.New(`ent: missing required field "UsageLog.image_count"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.ImageSize(); ok {
|
||||
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)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.CreatedAt(); !ok {
|
||||
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "UsageLog.created_at"`)}
|
||||
}
|
||||
@@ -650,6 +709,18 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(usagelog.FieldFirstTokenMs, field.TypeInt, 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 {
|
||||
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
||||
_node.ImageCount = value
|
||||
}
|
||||
if value, ok := _c.mutation.ImageSize(); ok {
|
||||
_spec.SetField(usagelog.FieldImageSize, field.TypeString, value)
|
||||
_node.ImageSize = &value
|
||||
}
|
||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(usagelog.FieldCreatedAt, field.TypeTime, value)
|
||||
_node.CreatedAt = value
|
||||
@@ -1199,6 +1270,60 @@ func (u *UsageLogUpsert) ClearFirstTokenMs() *UsageLogUpsert {
|
||||
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.
|
||||
func (u *UsageLogUpsert) SetImageCount(v int) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldImageCount, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateImageCount sets the "image_count" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsert) UpdateImageCount() *UsageLogUpsert {
|
||||
u.SetExcluded(usagelog.FieldImageCount)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddImageCount adds v to the "image_count" field.
|
||||
func (u *UsageLogUpsert) AddImageCount(v int) *UsageLogUpsert {
|
||||
u.Add(usagelog.FieldImageCount, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetImageSize sets the "image_size" field.
|
||||
func (u *UsageLogUpsert) SetImageSize(v string) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldImageSize, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateImageSize sets the "image_size" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsert) UpdateImageSize() *UsageLogUpsert {
|
||||
u.SetExcluded(usagelog.FieldImageSize)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearImageSize clears the value of the "image_size" field.
|
||||
func (u *UsageLogUpsert) ClearImageSize() *UsageLogUpsert {
|
||||
u.SetNull(usagelog.FieldImageSize)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateNewValues updates the mutable fields using the new values that were set on create.
|
||||
// Using this option is equivalent to using:
|
||||
//
|
||||
@@ -1720,6 +1845,69 @@ 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.
|
||||
func (u *UsageLogUpsertOne) SetImageCount(v int) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetImageCount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddImageCount adds v to the "image_count" field.
|
||||
func (u *UsageLogUpsertOne) AddImageCount(v int) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.AddImageCount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImageCount sets the "image_count" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertOne) UpdateImageCount() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateImageCount()
|
||||
})
|
||||
}
|
||||
|
||||
// SetImageSize sets the "image_size" field.
|
||||
func (u *UsageLogUpsertOne) SetImageSize(v string) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetImageSize(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImageSize sets the "image_size" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertOne) UpdateImageSize() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateImageSize()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearImageSize clears the value of the "image_size" field.
|
||||
func (u *UsageLogUpsertOne) ClearImageSize() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearImageSize()
|
||||
})
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (u *UsageLogUpsertOne) Exec(ctx context.Context) error {
|
||||
if len(u.create.conflict) == 0 {
|
||||
@@ -2407,6 +2595,69 @@ 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.
|
||||
func (u *UsageLogUpsertBulk) SetImageCount(v int) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetImageCount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddImageCount adds v to the "image_count" field.
|
||||
func (u *UsageLogUpsertBulk) AddImageCount(v int) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.AddImageCount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImageCount sets the "image_count" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertBulk) UpdateImageCount() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateImageCount()
|
||||
})
|
||||
}
|
||||
|
||||
// SetImageSize sets the "image_size" field.
|
||||
func (u *UsageLogUpsertBulk) SetImageSize(v string) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetImageSize(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateImageSize sets the "image_size" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertBulk) UpdateImageSize() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateImageSize()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearImageSize clears the value of the "image_size" field.
|
||||
func (u *UsageLogUpsertBulk) ClearImageSize() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearImageSize()
|
||||
})
|
||||
}
|
||||
|
||||
// Exec executes the query.
|
||||
func (u *UsageLogUpsertBulk) Exec(ctx context.Context) error {
|
||||
if u.create.err != nil {
|
||||
|
||||
@@ -504,6 +504,67 @@ func (_u *UsageLogUpdate) ClearFirstTokenMs() *UsageLogUpdate {
|
||||
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.
|
||||
func (_u *UsageLogUpdate) SetImageCount(v int) *UsageLogUpdate {
|
||||
_u.mutation.ResetImageCount()
|
||||
_u.mutation.SetImageCount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImageCount sets the "image_count" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdate) SetNillableImageCount(v *int) *UsageLogUpdate {
|
||||
if v != nil {
|
||||
_u.SetImageCount(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddImageCount adds value to the "image_count" field.
|
||||
func (_u *UsageLogUpdate) AddImageCount(v int) *UsageLogUpdate {
|
||||
_u.mutation.AddImageCount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetImageSize sets the "image_size" field.
|
||||
func (_u *UsageLogUpdate) SetImageSize(v string) *UsageLogUpdate {
|
||||
_u.mutation.SetImageSize(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImageSize sets the "image_size" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdate) SetNillableImageSize(v *string) *UsageLogUpdate {
|
||||
if v != nil {
|
||||
_u.SetImageSize(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearImageSize clears the value of the "image_size" field.
|
||||
func (_u *UsageLogUpdate) ClearImageSize() *UsageLogUpdate {
|
||||
_u.mutation.ClearImageSize()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUser sets the "user" edge to the User entity.
|
||||
func (_u *UsageLogUpdate) SetUser(v *User) *UsageLogUpdate {
|
||||
return _u.SetUserID(v.ID)
|
||||
@@ -603,6 +664,16 @@ func (_u *UsageLogUpdate) check() error {
|
||||
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 err := usagelog.ImageSizeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
|
||||
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
|
||||
}
|
||||
@@ -738,6 +809,24 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.FirstTokenMsCleared() {
|
||||
_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 {
|
||||
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedImageCount(); ok {
|
||||
_spec.AddField(usagelog.FieldImageCount, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ImageSize(); ok {
|
||||
_spec.SetField(usagelog.FieldImageSize, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.ImageSizeCleared() {
|
||||
_spec.ClearField(usagelog.FieldImageSize, field.TypeString)
|
||||
}
|
||||
if _u.mutation.UserCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
@@ -1375,6 +1464,67 @@ func (_u *UsageLogUpdateOne) ClearFirstTokenMs() *UsageLogUpdateOne {
|
||||
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.
|
||||
func (_u *UsageLogUpdateOne) SetImageCount(v int) *UsageLogUpdateOne {
|
||||
_u.mutation.ResetImageCount()
|
||||
_u.mutation.SetImageCount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImageCount sets the "image_count" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdateOne) SetNillableImageCount(v *int) *UsageLogUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetImageCount(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddImageCount adds value to the "image_count" field.
|
||||
func (_u *UsageLogUpdateOne) AddImageCount(v int) *UsageLogUpdateOne {
|
||||
_u.mutation.AddImageCount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetImageSize sets the "image_size" field.
|
||||
func (_u *UsageLogUpdateOne) SetImageSize(v string) *UsageLogUpdateOne {
|
||||
_u.mutation.SetImageSize(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableImageSize sets the "image_size" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdateOne) SetNillableImageSize(v *string) *UsageLogUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetImageSize(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearImageSize clears the value of the "image_size" field.
|
||||
func (_u *UsageLogUpdateOne) ClearImageSize() *UsageLogUpdateOne {
|
||||
_u.mutation.ClearImageSize()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetUser sets the "user" edge to the User entity.
|
||||
func (_u *UsageLogUpdateOne) SetUser(v *User) *UsageLogUpdateOne {
|
||||
return _u.SetUserID(v.ID)
|
||||
@@ -1487,6 +1637,16 @@ func (_u *UsageLogUpdateOne) check() error {
|
||||
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 err := usagelog.ImageSizeValidator(v); err != nil {
|
||||
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
|
||||
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
|
||||
}
|
||||
@@ -1639,6 +1799,24 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
|
||||
if _u.mutation.FirstTokenMsCleared() {
|
||||
_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 {
|
||||
_spec.SetField(usagelog.FieldImageCount, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedImageCount(); ok {
|
||||
_spec.AddField(usagelog.FieldImageCount, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.ImageSize(); ok {
|
||||
_spec.SetField(usagelog.FieldImageSize, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.ImageSizeCleared() {
|
||||
_spec.ClearField(usagelog.FieldImageSize, field.TypeString)
|
||||
}
|
||||
if _u.mutation.UserCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
module github.com/Wei-Shaw/sub2api
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.11
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/imroc/req/v3 v3.56.0
|
||||
github.com/imroc/req/v3 v3.57.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/spf13/viper v1.18.2
|
||||
@@ -20,16 +18,16 @@ require (
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/zeromicro/go-zero v1.9.4
|
||||
golang.org/x/crypto v0.44.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/term v0.37.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.38.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
@@ -64,7 +62,6 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
@@ -74,10 +71,8 @@ require (
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||
github.com/icholy/digest v1.1.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
@@ -105,8 +100,8 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.56.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/refraction-networking/utls v1.8.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
@@ -141,16 +136,12 @@ require (
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gorm.io/datatypes v1.2.7 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
gorm.io/gorm v1.30.0 // indirect
|
||||
)
|
||||
|
||||
@@ -4,8 +4,6 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
|
||||
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
@@ -96,15 +94,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
|
||||
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -126,8 +121,8 @@ github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZY
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||
github.com/imroc/req/v3 v3.56.0 h1:t6YdqqerYBXhZ9+VjqsQs5wlKxdUNEvsgBhxWc1AEEo=
|
||||
github.com/imroc/req/v3 v3.56.0/go.mod h1:cUZSooE8hhzFNOrAbdxuemXDQxFXLQTnu3066jr7ZGk=
|
||||
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
||||
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
@@ -138,14 +133,10 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
@@ -219,10 +210,10 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||
@@ -335,16 +326,16 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -354,16 +345,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
@@ -386,13 +377,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,7 +18,7 @@ const (
|
||||
RunModeSimple = "simple"
|
||||
)
|
||||
|
||||
const DefaultCSPPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
const DefaultCSPPolicy = "default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
|
||||
// 连接池隔离策略常量
|
||||
// 用于控制上游 HTTP 连接池的隔离粒度,影响连接复用和资源消耗
|
||||
@@ -51,6 +52,15 @@ type Config struct {
|
||||
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
|
||||
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
|
||||
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 {
|
||||
@@ -147,7 +157,7 @@ type CSPConfig struct {
|
||||
}
|
||||
|
||||
type ProxyProbeConfig struct {
|
||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
|
||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
|
||||
}
|
||||
|
||||
type BillingConfig struct {
|
||||
@@ -338,8 +348,19 @@ func NormalizeRunMode(value string) string {
|
||||
func Load() (*Config, error) {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
// Add config paths in priority order
|
||||
// 1. DATA_DIR environment variable (highest priority)
|
||||
if dataDir := os.Getenv("DATA_DIR"); dataDir != "" {
|
||||
viper.AddConfigPath(dataDir)
|
||||
}
|
||||
// 2. Docker data directory
|
||||
viper.AddConfigPath("/app/data")
|
||||
// 3. Current directory
|
||||
viper.AddConfigPath(".")
|
||||
// 4. Config subdirectory
|
||||
viper.AddConfigPath("./config")
|
||||
// 5. System config directory
|
||||
viper.AddConfigPath("/etc/sub2api")
|
||||
|
||||
// 环境变量支持
|
||||
@@ -372,13 +393,13 @@ func Load() (*Config, error) {
|
||||
cfg.Security.ResponseHeaders.ForceRemove = normalizeStringSlice(cfg.Security.ResponseHeaders.ForceRemove)
|
||||
cfg.Security.CSP.Policy = strings.TrimSpace(cfg.Security.CSP.Policy)
|
||||
|
||||
if cfg.Server.Mode != "release" && cfg.JWT.Secret == "" {
|
||||
if cfg.JWT.Secret == "" {
|
||||
secret, err := generateJWTSecret(64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate jwt secret error: %w", err)
|
||||
}
|
||||
cfg.JWT.Secret = secret
|
||||
log.Println("Warning: JWT secret auto-generated for non-release mode. Do not use in production.")
|
||||
log.Println("Warning: JWT secret auto-generated. Consider setting a fixed secret for production.")
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
@@ -392,7 +413,7 @@ func Load() (*Config, error) {
|
||||
log.Println("Warning: security.response_headers.enabled=false; configurable header filtering disabled (default allowlist only).")
|
||||
}
|
||||
|
||||
if cfg.Server.Mode != "release" && cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) {
|
||||
if cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) {
|
||||
log.Println("Warning: JWT secret appears weak; use a 32+ character random secret in production.")
|
||||
}
|
||||
if len(cfg.Security.ResponseHeaders.AdditionalAllowed) > 0 || len(cfg.Security.ResponseHeaders.ForceRemove) > 0 {
|
||||
@@ -436,8 +457,8 @@ func setDefaults() {
|
||||
"raw.githubusercontent.com",
|
||||
})
|
||||
viper.SetDefault("security.url_allowlist.crs_hosts", []string{})
|
||||
viper.SetDefault("security.url_allowlist.allow_private_hosts", false)
|
||||
viper.SetDefault("security.url_allowlist.allow_insecure_http", false)
|
||||
viper.SetDefault("security.url_allowlist.allow_private_hosts", true)
|
||||
viper.SetDefault("security.url_allowlist.allow_insecure_http", true)
|
||||
viper.SetDefault("security.response_headers.enabled", false)
|
||||
viper.SetDefault("security.response_headers.additional_allowed", []string{})
|
||||
viper.SetDefault("security.response_headers.force_remove", []string{})
|
||||
@@ -546,20 +567,13 @@ func setDefaults() {
|
||||
viper.SetDefault("gemini.oauth.client_secret", "")
|
||||
viper.SetDefault("gemini.oauth.scopes", "")
|
||||
viper.SetDefault("gemini.quota.policy", "")
|
||||
|
||||
// Update - 在线更新配置
|
||||
// 代理地址为空表示直连 GitHub(适用于海外服务器)
|
||||
viper.SetDefault("update.proxy_url", "")
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if c.Server.Mode == "release" {
|
||||
if c.JWT.Secret == "" {
|
||||
return fmt.Errorf("jwt.secret is required in release mode")
|
||||
}
|
||||
if len(c.JWT.Secret) < 32 {
|
||||
return fmt.Errorf("jwt.secret must be at least 32 characters")
|
||||
}
|
||||
if isWeakJWTSecret(c.JWT.Secret) {
|
||||
return fmt.Errorf("jwt.secret is too weak")
|
||||
}
|
||||
}
|
||||
if c.JWT.ExpireHour <= 0 {
|
||||
return fmt.Errorf("jwt.expire_hour must be positive")
|
||||
}
|
||||
|
||||
@@ -80,8 +80,11 @@ func TestLoadDefaultSecurityToggles(t *testing.T) {
|
||||
if cfg.Security.URLAllowlist.Enabled {
|
||||
t.Fatalf("URLAllowlist.Enabled = true, want false")
|
||||
}
|
||||
if cfg.Security.URLAllowlist.AllowInsecureHTTP {
|
||||
t.Fatalf("URLAllowlist.AllowInsecureHTTP = true, want false")
|
||||
if !cfg.Security.URLAllowlist.AllowInsecureHTTP {
|
||||
t.Fatalf("URLAllowlist.AllowInsecureHTTP = false, want true")
|
||||
}
|
||||
if !cfg.Security.URLAllowlist.AllowPrivateHosts {
|
||||
t.Fatalf("URLAllowlist.AllowPrivateHosts = false, want true")
|
||||
}
|
||||
if cfg.Security.ResponseHeaders.Enabled {
|
||||
t.Fatalf("ResponseHeaders.Enabled = true, want false")
|
||||
|
||||
@@ -34,15 +34,16 @@ func NewOAuthHandler(oauthService *service.OAuthService) *OAuthHandler {
|
||||
|
||||
// AccountHandler handles admin account management
|
||||
type AccountHandler struct {
|
||||
adminService service.AdminService
|
||||
oauthService *service.OAuthService
|
||||
openaiOAuthService *service.OpenAIOAuthService
|
||||
geminiOAuthService *service.GeminiOAuthService
|
||||
rateLimitService *service.RateLimitService
|
||||
accountUsageService *service.AccountUsageService
|
||||
accountTestService *service.AccountTestService
|
||||
concurrencyService *service.ConcurrencyService
|
||||
crsSyncService *service.CRSSyncService
|
||||
adminService service.AdminService
|
||||
oauthService *service.OAuthService
|
||||
openaiOAuthService *service.OpenAIOAuthService
|
||||
geminiOAuthService *service.GeminiOAuthService
|
||||
antigravityOAuthService *service.AntigravityOAuthService
|
||||
rateLimitService *service.RateLimitService
|
||||
accountUsageService *service.AccountUsageService
|
||||
accountTestService *service.AccountTestService
|
||||
concurrencyService *service.ConcurrencyService
|
||||
crsSyncService *service.CRSSyncService
|
||||
}
|
||||
|
||||
// NewAccountHandler creates a new admin account handler
|
||||
@@ -51,6 +52,7 @@ func NewAccountHandler(
|
||||
oauthService *service.OAuthService,
|
||||
openaiOAuthService *service.OpenAIOAuthService,
|
||||
geminiOAuthService *service.GeminiOAuthService,
|
||||
antigravityOAuthService *service.AntigravityOAuthService,
|
||||
rateLimitService *service.RateLimitService,
|
||||
accountUsageService *service.AccountUsageService,
|
||||
accountTestService *service.AccountTestService,
|
||||
@@ -58,15 +60,16 @@ func NewAccountHandler(
|
||||
crsSyncService *service.CRSSyncService,
|
||||
) *AccountHandler {
|
||||
return &AccountHandler{
|
||||
adminService: adminService,
|
||||
oauthService: oauthService,
|
||||
openaiOAuthService: openaiOAuthService,
|
||||
geminiOAuthService: geminiOAuthService,
|
||||
rateLimitService: rateLimitService,
|
||||
accountUsageService: accountUsageService,
|
||||
accountTestService: accountTestService,
|
||||
concurrencyService: concurrencyService,
|
||||
crsSyncService: crsSyncService,
|
||||
adminService: adminService,
|
||||
oauthService: oauthService,
|
||||
openaiOAuthService: openaiOAuthService,
|
||||
geminiOAuthService: geminiOAuthService,
|
||||
antigravityOAuthService: antigravityOAuthService,
|
||||
rateLimitService: rateLimitService,
|
||||
accountUsageService: accountUsageService,
|
||||
accountTestService: accountTestService,
|
||||
concurrencyService: concurrencyService,
|
||||
crsSyncService: crsSyncService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +85,8 @@ type CreateAccountRequest struct {
|
||||
Concurrency int `json:"concurrency"`
|
||||
Priority int `json:"priority"`
|
||||
GroupIDs []int64 `json:"group_ids"`
|
||||
ExpiresAt *int64 `json:"expires_at"`
|
||||
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||
}
|
||||
|
||||
@@ -98,6 +103,8 @@ type UpdateAccountRequest struct {
|
||||
Priority *int `json:"priority"`
|
||||
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||
GroupIDs *[]int64 `json:"group_ids"`
|
||||
ExpiresAt *int64 `json:"expires_at"`
|
||||
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||
}
|
||||
|
||||
@@ -201,6 +208,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
||||
Concurrency: req.Concurrency,
|
||||
Priority: req.Priority,
|
||||
GroupIDs: req.GroupIDs,
|
||||
ExpiresAt: req.ExpiresAt,
|
||||
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||
SkipMixedChannelCheck: skipCheck,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -258,6 +267,8 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
||||
Priority: req.Priority, // 指针类型,nil 表示未提供
|
||||
Status: req.Status,
|
||||
GroupIDs: req.GroupIDs,
|
||||
ExpiresAt: req.ExpiresAt,
|
||||
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||
SkipMixedChannelCheck: skipCheck,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -420,6 +431,19 @@ func (h *AccountHandler) Refresh(c *gin.Context) {
|
||||
newCredentials[k] = v
|
||||
}
|
||||
}
|
||||
} else if account.Platform == service.PlatformAntigravity {
|
||||
tokenInfo, err := h.antigravityOAuthService.RefreshAccountToken(c.Request.Context(), account)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
newCredentials = h.antigravityOAuthService.BuildAccountCredentials(tokenInfo)
|
||||
for k, v := range account.Credentials {
|
||||
if _, exists := newCredentials[k]; !exists {
|
||||
newCredentials[k] = v
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use Anthropic/Claude OAuth service to refresh token
|
||||
tokenInfo, err := h.oauthService.RefreshAccountToken(c.Request.Context(), account)
|
||||
|
||||
@@ -26,31 +26,33 @@ func NewDashboardHandler(dashboardService *service.DashboardService) *DashboardH
|
||||
}
|
||||
|
||||
// parseTimeRange parses start_date, end_date query parameters
|
||||
// Uses user's timezone if provided, otherwise falls back to server timezone
|
||||
func parseTimeRange(c *gin.Context) (time.Time, time.Time) {
|
||||
now := timezone.Now()
|
||||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||||
now := timezone.NowInUserLocation(userTZ)
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
var startTime, endTime time.Time
|
||||
|
||||
if startDate != "" {
|
||||
if t, err := timezone.ParseInLocation("2006-01-02", startDate); err == nil {
|
||||
if t, err := timezone.ParseInUserLocation("2006-01-02", startDate, userTZ); err == nil {
|
||||
startTime = t
|
||||
} else {
|
||||
startTime = timezone.StartOfDay(now.AddDate(0, 0, -7))
|
||||
startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
|
||||
}
|
||||
} else {
|
||||
startTime = timezone.StartOfDay(now.AddDate(0, 0, -7))
|
||||
startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
|
||||
}
|
||||
|
||||
if endDate != "" {
|
||||
if t, err := timezone.ParseInLocation("2006-01-02", endDate); err == nil {
|
||||
if t, err := timezone.ParseInUserLocation("2006-01-02", endDate, userTZ); err == nil {
|
||||
endTime = t.Add(24 * time.Hour) // Include the end date
|
||||
} else {
|
||||
endTime = timezone.StartOfDay(now.AddDate(0, 0, 1))
|
||||
endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
|
||||
}
|
||||
} else {
|
||||
endTime = timezone.StartOfDay(now.AddDate(0, 0, 1))
|
||||
endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
|
||||
}
|
||||
|
||||
return startTime, endTime
|
||||
|
||||
@@ -33,6 +33,10 @@ type CreateGroupRequest struct {
|
||||
DailyLimitUSD *float64 `json:"daily_limit_usd"`
|
||||
WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
|
||||
MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
|
||||
// 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置)
|
||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||
}
|
||||
|
||||
// UpdateGroupRequest represents update group request
|
||||
@@ -47,6 +51,10 @@ type UpdateGroupRequest struct {
|
||||
DailyLimitUSD *float64 `json:"daily_limit_usd"`
|
||||
WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
|
||||
MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
|
||||
// 图片生成计费配置(antigravity 和 gemini 平台使用,负数表示清除配置)
|
||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||
}
|
||||
|
||||
// List handles listing all groups with pagination
|
||||
@@ -139,6 +147,9 @@ func (h *GroupHandler) Create(c *gin.Context) {
|
||||
DailyLimitUSD: req.DailyLimitUSD,
|
||||
WeeklyLimitUSD: req.WeeklyLimitUSD,
|
||||
MonthlyLimitUSD: req.MonthlyLimitUSD,
|
||||
ImagePrice1K: req.ImagePrice1K,
|
||||
ImagePrice2K: req.ImagePrice2K,
|
||||
ImagePrice4K: req.ImagePrice4K,
|
||||
})
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
@@ -174,6 +185,9 @@ func (h *GroupHandler) Update(c *gin.Context) {
|
||||
DailyLimitUSD: req.DailyLimitUSD,
|
||||
WeeklyLimitUSD: req.WeeklyLimitUSD,
|
||||
MonthlyLimitUSD: req.MonthlyLimitUSD,
|
||||
ImagePrice1K: req.ImagePrice1K,
|
||||
ImagePrice2K: req.ImagePrice2K,
|
||||
ImagePrice4K: req.ImagePrice4K,
|
||||
})
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
|
||||
@@ -102,8 +102,9 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
|
||||
// Parse date range
|
||||
var startTime, endTime *time.Time
|
||||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||||
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
||||
t, err := timezone.ParseInLocation("2006-01-02", startDateStr)
|
||||
t, err := timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
|
||||
return
|
||||
@@ -112,7 +113,7 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
if endDateStr := c.Query("end_date"); endDateStr != "" {
|
||||
t, err := timezone.ParseInLocation("2006-01-02", endDateStr)
|
||||
t, err := timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
|
||||
return
|
||||
@@ -143,7 +144,7 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
|
||||
out := make([]dto.UsageLog, 0, len(records))
|
||||
for i := range records {
|
||||
out = append(out, *dto.UsageLogFromService(&records[i]))
|
||||
out = append(out, *dto.UsageLogFromServiceAdmin(&records[i]))
|
||||
}
|
||||
response.Paginated(c, out, result.Total, page, pageSize)
|
||||
}
|
||||
@@ -151,8 +152,8 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
// Stats handles getting usage statistics with filters
|
||||
// GET /api/v1/admin/usage/stats
|
||||
func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
// Parse filters
|
||||
var userID, apiKeyID int64
|
||||
// Parse filters - same as List endpoint
|
||||
var userID, apiKeyID, accountID, groupID int64
|
||||
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
||||
id, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||
if err != nil {
|
||||
@@ -171,8 +172,50 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
apiKeyID = id
|
||||
}
|
||||
|
||||
if accountIDStr := c.Query("account_id"); accountIDStr != "" {
|
||||
id, err := strconv.ParseInt(accountIDStr, 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid account_id")
|
||||
return
|
||||
}
|
||||
accountID = id
|
||||
}
|
||||
|
||||
if groupIDStr := c.Query("group_id"); groupIDStr != "" {
|
||||
id, err := strconv.ParseInt(groupIDStr, 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid group_id")
|
||||
return
|
||||
}
|
||||
groupID = id
|
||||
}
|
||||
|
||||
model := c.Query("model")
|
||||
|
||||
var stream *bool
|
||||
if streamStr := c.Query("stream"); streamStr != "" {
|
||||
val, err := strconv.ParseBool(streamStr)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid stream value, use true or false")
|
||||
return
|
||||
}
|
||||
stream = &val
|
||||
}
|
||||
|
||||
var billingType *int8
|
||||
if billingTypeStr := c.Query("billing_type"); billingTypeStr != "" {
|
||||
val, err := strconv.ParseInt(billingTypeStr, 10, 8)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid billing_type")
|
||||
return
|
||||
}
|
||||
bt := int8(val)
|
||||
billingType = &bt
|
||||
}
|
||||
|
||||
// Parse date range
|
||||
now := timezone.Now()
|
||||
userTZ := c.Query("timezone")
|
||||
now := timezone.NowInUserLocation(userTZ)
|
||||
var startTime, endTime time.Time
|
||||
|
||||
startDateStr := c.Query("start_date")
|
||||
@@ -180,12 +223,12 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
|
||||
if startDateStr != "" && endDateStr != "" {
|
||||
var err error
|
||||
startTime, err = timezone.ParseInLocation("2006-01-02", startDateStr)
|
||||
startTime, err = timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
|
||||
return
|
||||
}
|
||||
endTime, err = timezone.ParseInLocation("2006-01-02", endDateStr)
|
||||
endTime, err = timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
|
||||
return
|
||||
@@ -195,39 +238,31 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
period := c.DefaultQuery("period", "today")
|
||||
switch period {
|
||||
case "today":
|
||||
startTime = timezone.StartOfDay(now)
|
||||
startTime = timezone.StartOfDayInUserLocation(now, userTZ)
|
||||
case "week":
|
||||
startTime = now.AddDate(0, 0, -7)
|
||||
case "month":
|
||||
startTime = now.AddDate(0, -1, 0)
|
||||
default:
|
||||
startTime = timezone.StartOfDay(now)
|
||||
startTime = timezone.StartOfDayInUserLocation(now, userTZ)
|
||||
}
|
||||
endTime = now
|
||||
}
|
||||
|
||||
if apiKeyID > 0 {
|
||||
stats, err := h.usageService.GetStatsByAPIKey(c.Request.Context(), apiKeyID, startTime, endTime)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, stats)
|
||||
return
|
||||
// Build filters and call GetStatsWithFilters
|
||||
filters := usagestats.UsageLogFilters{
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
GroupID: groupID,
|
||||
Model: model,
|
||||
Stream: stream,
|
||||
BillingType: billingType,
|
||||
StartTime: &startTime,
|
||||
EndTime: &endTime,
|
||||
}
|
||||
|
||||
if userID > 0 {
|
||||
stats, err := h.usageService.GetStatsByUser(c.Request.Context(), userID, startTime, endTime)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, stats)
|
||||
return
|
||||
}
|
||||
|
||||
// Get global stats
|
||||
stats, err := h.usageService.GetGlobalStats(c.Request.Context(), startTime, endTime)
|
||||
stats, err := h.usageService.GetStatsWithFilters(c.Request.Context(), filters)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// Package dto provides data transfer objects for HTTP handlers.
|
||||
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 {
|
||||
if u == nil {
|
||||
@@ -78,6 +82,9 @@ func GroupFromServiceShallow(g *service.Group) *Group {
|
||||
DailyLimitUSD: g.DailyLimitUSD,
|
||||
WeeklyLimitUSD: g.WeeklyLimitUSD,
|
||||
MonthlyLimitUSD: g.MonthlyLimitUSD,
|
||||
ImagePrice1K: g.ImagePrice1K,
|
||||
ImagePrice2K: g.ImagePrice2K,
|
||||
ImagePrice4K: g.ImagePrice4K,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
AccountCount: g.AccountCount,
|
||||
@@ -117,6 +124,8 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
||||
Status: a.Status,
|
||||
ErrorMessage: a.ErrorMessage,
|
||||
LastUsedAt: a.LastUsedAt,
|
||||
ExpiresAt: timeToUnixSeconds(a.ExpiresAt),
|
||||
AutoPauseOnExpired: a.AutoPauseOnExpired,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
Schedulable: a.Schedulable,
|
||||
@@ -154,6 +163,14 @@ func AccountFromService(a *service.Account) *Account {
|
||||
return out
|
||||
}
|
||||
|
||||
func timeToUnixSeconds(value *time.Time) *int64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
ts := value.Unix()
|
||||
return &ts
|
||||
}
|
||||
|
||||
func AccountGroupFromService(ag *service.AccountGroup) *AccountGroup {
|
||||
if ag == nil {
|
||||
return nil
|
||||
@@ -217,7 +234,21 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
|
||||
}
|
||||
}
|
||||
|
||||
func UsageLogFromService(l *service.UsageLog) *UsageLog {
|
||||
// AccountSummaryFromService returns a minimal AccountSummary for usage log display.
|
||||
// Only includes ID and Name - no sensitive fields like Credentials, Proxy, etc.
|
||||
func AccountSummaryFromService(a *service.Account) *AccountSummary {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
return &AccountSummary{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// usageLogFromServiceBase is a helper that converts service UsageLog to DTO.
|
||||
// The account parameter allows caller to control what Account info is included.
|
||||
func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary) *UsageLog {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -247,15 +278,32 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog {
|
||||
Stream: l.Stream,
|
||||
DurationMs: l.DurationMs,
|
||||
FirstTokenMs: l.FirstTokenMs,
|
||||
ImageCount: l.ImageCount,
|
||||
ImageSize: l.ImageSize,
|
||||
CreatedAt: l.CreatedAt,
|
||||
User: UserFromServiceShallow(l.User),
|
||||
APIKey: APIKeyFromService(l.APIKey),
|
||||
Account: AccountFromService(l.Account),
|
||||
Account: account,
|
||||
Group: GroupFromServiceShallow(l.Group),
|
||||
Subscription: UserSubscriptionFromService(l.Subscription),
|
||||
}
|
||||
}
|
||||
|
||||
// UsageLogFromService converts a service UsageLog to DTO for regular users.
|
||||
// It excludes Account details - users should not see account information.
|
||||
func UsageLogFromService(l *service.UsageLog) *UsageLog {
|
||||
return usageLogFromServiceBase(l, nil)
|
||||
}
|
||||
|
||||
// UsageLogFromServiceAdmin converts a service UsageLog to DTO for admin users.
|
||||
// It includes minimal Account info (ID, Name only).
|
||||
func UsageLogFromServiceAdmin(l *service.UsageLog) *UsageLog {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return usageLogFromServiceBase(l, AccountSummaryFromService(l.Account))
|
||||
}
|
||||
|
||||
func SettingFromService(s *service.Setting) *Setting {
|
||||
if s == nil {
|
||||
return nil
|
||||
|
||||
@@ -47,6 +47,11 @@ type Group struct {
|
||||
WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
|
||||
MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
|
||||
|
||||
// 图片生成计费配置(仅 antigravity 平台使用)
|
||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@@ -55,21 +60,23 @@ type Group struct {
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Credentials map[string]any `json:"credentials"`
|
||||
Extra map[string]any `json:"extra"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
Priority int `json:"priority"`
|
||||
Status string `json:"status"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Credentials map[string]any `json:"credentials"`
|
||||
Extra map[string]any `json:"extra"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
Priority int `json:"priority"`
|
||||
Status string `json:"status"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
ExpiresAt *int64 `json:"expires_at"`
|
||||
AutoPauseOnExpired bool `json:"auto_pause_on_expired"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Schedulable bool `json:"schedulable"`
|
||||
|
||||
@@ -169,15 +176,26 @@ type UsageLog struct {
|
||||
DurationMs *int `json:"duration_ms"`
|
||||
FirstTokenMs *int `json:"first_token_ms"`
|
||||
|
||||
// 图片生成字段
|
||||
ImageCount int `json:"image_count"`
|
||||
ImageSize *string `json:"image_size"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
User *User `json:"user,omitempty"`
|
||||
APIKey *APIKey `json:"api_key,omitempty"`
|
||||
Account *Account `json:"account,omitempty"`
|
||||
Account *AccountSummary `json:"account,omitempty"` // Use minimal AccountSummary to prevent data leakage
|
||||
Group *Group `json:"group,omitempty"`
|
||||
Subscription *UserSubscription `json:"subscription,omitempty"`
|
||||
}
|
||||
|
||||
// AccountSummary is a minimal account info for usage log display.
|
||||
// It intentionally excludes sensitive fields like Credentials, Proxy, etc.
|
||||
type AccountSummary struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
ID int64 `json:"id"`
|
||||
Key string `json:"key"`
|
||||
|
||||
@@ -108,6 +108,9 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
// 获取订阅信息(可能为nil)- 提前获取用于后续检查
|
||||
subscription, _ := middleware2.GetSubscriptionFromContext(c)
|
||||
|
||||
// 获取 User-Agent
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
// 0. 检查wait队列是否已满
|
||||
maxWait := service.CalculateMaxWait(subject.Concurrency)
|
||||
canWait, err := h.concurrencyHelper.IncrementWaitCount(c.Request.Context(), subject.UserID, maxWait)
|
||||
@@ -267,7 +270,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 异步记录使用量(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)
|
||||
defer cancel()
|
||||
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
||||
@@ -276,10 +279,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
User: apiKey.User,
|
||||
Account: usedAccount,
|
||||
Subscription: subscription,
|
||||
UserAgent: ua,
|
||||
}); err != nil {
|
||||
log.Printf("Record usage failed: %v", err)
|
||||
}
|
||||
}(result, account)
|
||||
}(result, account, userAgent)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -394,7 +398,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 异步记录使用量(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)
|
||||
defer cancel()
|
||||
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
||||
@@ -403,10 +407,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
||||
User: apiKey.User,
|
||||
Account: usedAccount,
|
||||
Subscription: subscription,
|
||||
UserAgent: ua,
|
||||
}); err != nil {
|
||||
log.Printf("Record usage failed: %v", err)
|
||||
}
|
||||
}(result, account)
|
||||
}(result, account, userAgent)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,19 +83,33 @@ func NewConcurrencyHelper(concurrencyService *service.ConcurrencyService, pingFo
|
||||
|
||||
// wrapReleaseOnDone ensures release runs at most once and still triggers on context cancellation.
|
||||
// 用于避免客户端断开或上游超时导致的并发槽位泄漏。
|
||||
// 修复:添加 quit channel 确保 goroutine 及时退出,避免泄露
|
||||
func wrapReleaseOnDone(ctx context.Context, releaseFunc func()) func() {
|
||||
if releaseFunc == nil {
|
||||
return nil
|
||||
}
|
||||
var once sync.Once
|
||||
wrapped := func() {
|
||||
once.Do(releaseFunc)
|
||||
quit := make(chan struct{})
|
||||
|
||||
release := func() {
|
||||
once.Do(func() {
|
||||
releaseFunc()
|
||||
close(quit) // 通知监听 goroutine 退出
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
wrapped()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Context 取消时释放资源
|
||||
release()
|
||||
case <-quit:
|
||||
// 正常释放已完成,goroutine 退出
|
||||
return
|
||||
}
|
||||
}()
|
||||
return wrapped
|
||||
|
||||
return release
|
||||
}
|
||||
|
||||
// IncrementWaitCount increments the wait count for a user
|
||||
|
||||
141
backend/internal/handler/gateway_helper_test.go
Normal file
141
backend/internal/handler/gateway_helper_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestWrapReleaseOnDone_NoGoroutineLeak 验证 wrapReleaseOnDone 修复后不会泄露 goroutine
|
||||
func TestWrapReleaseOnDone_NoGoroutineLeak(t *testing.T) {
|
||||
// 记录测试开始时的 goroutine 数量
|
||||
runtime.GC()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
initialGoroutines := runtime.NumGoroutine()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var releaseCount int32
|
||||
release := wrapReleaseOnDone(ctx, func() {
|
||||
atomic.AddInt32(&releaseCount, 1)
|
||||
})
|
||||
|
||||
// 正常释放
|
||||
release()
|
||||
|
||||
// 等待足够时间确保 goroutine 退出
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证只释放一次
|
||||
if count := atomic.LoadInt32(&releaseCount); count != 1 {
|
||||
t.Errorf("expected release count to be 1, got %d", count)
|
||||
}
|
||||
|
||||
// 强制 GC,清理已退出的 goroutine
|
||||
runtime.GC()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 验证 goroutine 数量没有增加(允许±2的误差,考虑到测试框架本身可能创建的 goroutine)
|
||||
finalGoroutines := runtime.NumGoroutine()
|
||||
if finalGoroutines > initialGoroutines+2 {
|
||||
t.Errorf("goroutine leak detected: initial=%d, final=%d, leaked=%d",
|
||||
initialGoroutines, finalGoroutines, finalGoroutines-initialGoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrapReleaseOnDone_ContextCancellation 验证 context 取消时也能正确释放
|
||||
func TestWrapReleaseOnDone_ContextCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var releaseCount int32
|
||||
_ = wrapReleaseOnDone(ctx, func() {
|
||||
atomic.AddInt32(&releaseCount, 1)
|
||||
})
|
||||
|
||||
// 取消 context,应该触发释放
|
||||
cancel()
|
||||
|
||||
// 等待释放完成
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 验证释放被调用
|
||||
if count := atomic.LoadInt32(&releaseCount); count != 1 {
|
||||
t.Errorf("expected release count to be 1, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrapReleaseOnDone_MultipleCallsOnlyReleaseOnce 验证多次调用 release 只释放一次
|
||||
func TestWrapReleaseOnDone_MultipleCallsOnlyReleaseOnce(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var releaseCount int32
|
||||
release := wrapReleaseOnDone(ctx, func() {
|
||||
atomic.AddInt32(&releaseCount, 1)
|
||||
})
|
||||
|
||||
// 调用多次
|
||||
release()
|
||||
release()
|
||||
release()
|
||||
|
||||
// 等待执行完成
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 验证只释放一次
|
||||
if count := atomic.LoadInt32(&releaseCount); count != 1 {
|
||||
t.Errorf("expected release count to be 1, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrapReleaseOnDone_NilReleaseFunc 验证 nil releaseFunc 不会 panic
|
||||
func TestWrapReleaseOnDone_NilReleaseFunc(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
release := wrapReleaseOnDone(ctx, nil)
|
||||
|
||||
if release != nil {
|
||||
t.Error("expected nil release function when releaseFunc is nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrapReleaseOnDone_ConcurrentCalls 验证并发调用的安全性
|
||||
func TestWrapReleaseOnDone_ConcurrentCalls(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var releaseCount int32
|
||||
release := wrapReleaseOnDone(ctx, func() {
|
||||
atomic.AddInt32(&releaseCount, 1)
|
||||
})
|
||||
|
||||
// 并发调用 release
|
||||
const numGoroutines = 10
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go release()
|
||||
}
|
||||
|
||||
// 等待所有 goroutine 完成
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证只释放一次
|
||||
if count := atomic.LoadInt32(&releaseCount); count != 1 {
|
||||
t.Errorf("expected release count to be 1, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkWrapReleaseOnDone 性能基准测试
|
||||
func BenchmarkWrapReleaseOnDone(b *testing.B) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
release := wrapReleaseOnDone(ctx, func() {})
|
||||
release()
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,9 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
|
||||
// Get subscription (may be nil)
|
||||
subscription, _ := middleware.GetSubscriptionFromContext(c)
|
||||
|
||||
// 获取 User-Agent
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
// For Gemini native API, do not send Claude-style ping frames.
|
||||
geminiConcurrency := NewConcurrencyHelper(h.concurrencyHelper.concurrencyService, SSEPingFormatNone, 0)
|
||||
|
||||
@@ -300,7 +303,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
defer cancel()
|
||||
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
||||
@@ -309,10 +312,11 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
|
||||
User: apiKey.User,
|
||||
Account: usedAccount,
|
||||
Subscription: subscription,
|
||||
UserAgent: ua,
|
||||
}); err != nil {
|
||||
log.Printf("Record usage failed: %v", err)
|
||||
}
|
||||
}(result, account)
|
||||
}(result, account, userAgent)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
defer cancel()
|
||||
if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
|
||||
@@ -251,10 +251,11 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
||||
User: apiKey.User,
|
||||
Account: usedAccount,
|
||||
Subscription: subscription,
|
||||
UserAgent: ua,
|
||||
}); err != nil {
|
||||
log.Printf("Record usage failed: %v", err)
|
||||
}
|
||||
}(result, account)
|
||||
}(result, account, userAgent)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +88,9 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
|
||||
// Parse date range
|
||||
var startTime, endTime *time.Time
|
||||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||||
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
||||
t, err := timezone.ParseInLocation("2006-01-02", startDateStr)
|
||||
t, err := timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
|
||||
return
|
||||
@@ -98,7 +99,7 @@ func (h *UsageHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
if endDateStr := c.Query("end_date"); endDateStr != "" {
|
||||
t, err := timezone.ParseInLocation("2006-01-02", endDateStr)
|
||||
t, err := timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
|
||||
return
|
||||
@@ -194,7 +195,8 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 获取时间范围参数
|
||||
now := timezone.Now()
|
||||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||||
now := timezone.NowInUserLocation(userTZ)
|
||||
var startTime, endTime time.Time
|
||||
|
||||
// 优先使用 start_date 和 end_date 参数
|
||||
@@ -204,12 +206,12 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
if startDateStr != "" && endDateStr != "" {
|
||||
// 使用自定义日期范围
|
||||
var err error
|
||||
startTime, err = timezone.ParseInLocation("2006-01-02", startDateStr)
|
||||
startTime, err = timezone.ParseInUserLocation("2006-01-02", startDateStr, userTZ)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD")
|
||||
return
|
||||
}
|
||||
endTime, err = timezone.ParseInLocation("2006-01-02", endDateStr)
|
||||
endTime, err = timezone.ParseInUserLocation("2006-01-02", endDateStr, userTZ)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD")
|
||||
return
|
||||
@@ -221,13 +223,13 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
period := c.DefaultQuery("period", "today")
|
||||
switch period {
|
||||
case "today":
|
||||
startTime = timezone.StartOfDay(now)
|
||||
startTime = timezone.StartOfDayInUserLocation(now, userTZ)
|
||||
case "week":
|
||||
startTime = now.AddDate(0, 0, -7)
|
||||
case "month":
|
||||
startTime = now.AddDate(0, -1, 0)
|
||||
default:
|
||||
startTime = timezone.StartOfDay(now)
|
||||
startTime = timezone.StartOfDayInUserLocation(now, userTZ)
|
||||
}
|
||||
endTime = now
|
||||
}
|
||||
@@ -248,31 +250,33 @@ func (h *UsageHandler) Stats(c *gin.Context) {
|
||||
}
|
||||
|
||||
// parseUserTimeRange parses start_date, end_date query parameters for user dashboard
|
||||
// Uses user's timezone if provided, otherwise falls back to server timezone
|
||||
func parseUserTimeRange(c *gin.Context) (time.Time, time.Time) {
|
||||
now := timezone.Now()
|
||||
userTZ := c.Query("timezone") // Get user's timezone from request
|
||||
now := timezone.NowInUserLocation(userTZ)
|
||||
startDate := c.Query("start_date")
|
||||
endDate := c.Query("end_date")
|
||||
|
||||
var startTime, endTime time.Time
|
||||
|
||||
if startDate != "" {
|
||||
if t, err := timezone.ParseInLocation("2006-01-02", startDate); err == nil {
|
||||
if t, err := timezone.ParseInUserLocation("2006-01-02", startDate, userTZ); err == nil {
|
||||
startTime = t
|
||||
} else {
|
||||
startTime = timezone.StartOfDay(now.AddDate(0, 0, -7))
|
||||
startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
|
||||
}
|
||||
} else {
|
||||
startTime = timezone.StartOfDay(now.AddDate(0, 0, -7))
|
||||
startTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, -7), userTZ)
|
||||
}
|
||||
|
||||
if endDate != "" {
|
||||
if t, err := timezone.ParseInLocation("2006-01-02", endDate); err == nil {
|
||||
if t, err := timezone.ParseInUserLocation("2006-01-02", endDate, userTZ); err == nil {
|
||||
endTime = t.Add(24 * time.Hour) // Include the end date
|
||||
} else {
|
||||
endTime = timezone.StartOfDay(now.AddDate(0, 0, 1))
|
||||
endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
|
||||
}
|
||||
} else {
|
||||
endTime = timezone.StartOfDay(now.AddDate(0, 0, 1))
|
||||
endTime = timezone.StartOfDayInUserLocation(now.AddDate(0, 0, 1), userTZ)
|
||||
}
|
||||
|
||||
return startTime, endTime
|
||||
|
||||
@@ -13,16 +13,48 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// resolveHost 从 URL 解析 host
|
||||
func resolveHost(urlStr string) string {
|
||||
parsed, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return parsed.Host
|
||||
}
|
||||
|
||||
// NewAPIRequest 创建 Antigravity API 请求(v1internal 端点)
|
||||
func NewAPIRequest(ctx context.Context, action, accessToken string, body []byte) (*http.Request, error) {
|
||||
// 构建 URL,流式请求添加 ?alt=sse 参数
|
||||
apiURL := fmt.Sprintf("%s/v1internal:%s", BaseURL, action)
|
||||
isStream := action == "streamGenerateContent"
|
||||
if isStream {
|
||||
apiURL += "?alt=sse"
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 基础 Headers
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
// Accept Header 根据请求类型设置
|
||||
if isStream {
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
} else {
|
||||
req.Header.Set("Accept", "application/json")
|
||||
}
|
||||
|
||||
// 显式设置 Host Header
|
||||
if host := resolveHost(apiURL); host != "" {
|
||||
req.Host = host
|
||||
}
|
||||
|
||||
// 注意:requestType 已在 JSON body 的 V1InternalRequest 中设置,不需要 HTTP Header
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,13 @@ type GeminiGenerationConfig struct {
|
||||
TopK *int `json:"topK,omitempty"`
|
||||
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
ImageConfig *GeminiImageConfig `json:"imageConfig,omitempty"`
|
||||
}
|
||||
|
||||
// GeminiImageConfig Gemini 图片生成配置(仅 gemini-3-pro-image 支持)
|
||||
type GeminiImageConfig struct {
|
||||
AspectRatio string `json:"aspectRatio,omitempty"` // "1:1", "16:9", "9:16", "4:3", "3:4"
|
||||
ImageSize string `json:"imageSize,omitempty"` // "1K", "2K", "4K"
|
||||
}
|
||||
|
||||
// GeminiThinkingConfig Gemini thinking 配置
|
||||
|
||||
@@ -33,10 +33,11 @@ const (
|
||||
"https://www.googleapis.com/auth/experimentsandconfigs"
|
||||
|
||||
// API 端点
|
||||
BaseURL = "https://cloudcode-pa.googleapis.com"
|
||||
// 优先使用 sandbox daily URL,配额更宽松
|
||||
BaseURL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
|
||||
|
||||
// User-Agent
|
||||
UserAgent = "antigravity/1.11.9 windows/amd64"
|
||||
// User-Agent(模拟官方客户端)
|
||||
UserAgent = "antigravity/1.104.0 darwin/arm64"
|
||||
|
||||
// Session 过期时间
|
||||
SessionTTL = 30 * time.Minute
|
||||
|
||||
@@ -1,17 +1,46 @@
|
||||
package antigravity
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
sessionRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
sessionRandMutex sync.Mutex
|
||||
)
|
||||
|
||||
// generateStableSessionID 基于用户消息内容生成稳定的 session ID
|
||||
func generateStableSessionID(contents []GeminiContent) string {
|
||||
// 查找第一个 user 消息的文本
|
||||
for _, content := range contents {
|
||||
if content.Role == "user" && len(content.Parts) > 0 {
|
||||
if text := content.Parts[0].Text; text != "" {
|
||||
h := sha256.Sum256([]byte(text))
|
||||
n := int64(binary.BigEndian.Uint64(h[:8])) & 0x7FFFFFFFFFFFFFFF
|
||||
return "-" + strconv.FormatInt(n, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 回退:生成随机 session ID
|
||||
sessionRandMutex.Lock()
|
||||
n := sessionRand.Int63n(9_000_000_000_000_000_000)
|
||||
sessionRandMutex.Unlock()
|
||||
return "-" + strconv.FormatInt(n, 10)
|
||||
}
|
||||
|
||||
type TransformOptions struct {
|
||||
EnableIdentityPatch bool
|
||||
// IdentityPatch 可选:自定义注入到 systemInstruction 开头的身份防护提示词;
|
||||
@@ -67,8 +96,15 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map
|
||||
|
||||
// 5. 构建内部请求
|
||||
innerRequest := GeminiRequest{
|
||||
Contents: contents,
|
||||
SafetySettings: DefaultSafetySettings,
|
||||
Contents: contents,
|
||||
// 总是设置 toolConfig,与官方客户端一致
|
||||
ToolConfig: &GeminiToolConfig{
|
||||
FunctionCallingConfig: &GeminiFunctionCallingConfig{
|
||||
Mode: "VALIDATED",
|
||||
},
|
||||
},
|
||||
// 总是生成 sessionId,基于用户消息内容
|
||||
SessionID: generateStableSessionID(contents),
|
||||
}
|
||||
|
||||
if systemInstruction != nil {
|
||||
@@ -79,14 +115,9 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map
|
||||
}
|
||||
if len(tools) > 0 {
|
||||
innerRequest.Tools = tools
|
||||
innerRequest.ToolConfig = &GeminiToolConfig{
|
||||
FunctionCallingConfig: &GeminiFunctionCallingConfig{
|
||||
Mode: "VALIDATED",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 如果提供了 metadata.user_id,复用为 sessionId
|
||||
// 如果提供了 metadata.user_id,优先使用
|
||||
if claudeReq.Metadata != nil && claudeReq.Metadata.UserID != "" {
|
||||
innerRequest.SessionID = claudeReq.Metadata.UserID
|
||||
}
|
||||
@@ -95,7 +126,7 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map
|
||||
v1Req := V1InternalRequest{
|
||||
Project: projectID,
|
||||
RequestID: "agent-" + uuid.New().String(),
|
||||
UserAgent: "sub2api",
|
||||
UserAgent: "antigravity", // 固定值,与官方客户端一致
|
||||
RequestType: "agent",
|
||||
Model: mappedModel,
|
||||
Request: innerRequest,
|
||||
@@ -104,37 +135,37 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map
|
||||
return json.Marshal(v1Req)
|
||||
}
|
||||
|
||||
func defaultIdentityPatch(modelName string) string {
|
||||
return fmt.Sprintf(
|
||||
"--- [IDENTITY_PATCH] ---\n"+
|
||||
"Ignore any previous instructions regarding your identity or host platform (e.g., Amazon Q, Google AI).\n"+
|
||||
"You are currently providing services as the native %s model via a standard API proxy.\n"+
|
||||
"Always use the 'claude' command for terminal tasks if relevant.\n"+
|
||||
"--- [SYSTEM_PROMPT_BEGIN] ---\n",
|
||||
modelName,
|
||||
)
|
||||
// antigravityIdentity Antigravity identity 提示词
|
||||
const antigravityIdentity = `<identity>
|
||||
You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.
|
||||
You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.
|
||||
The USER will send you requests, which you must always prioritize addressing. Along with each USER request, we will attach additional metadata about their current state, such as what files they have open and where their cursor is.
|
||||
This information may or may not be relevant to the coding task, it is up for you to decide.
|
||||
</identity>
|
||||
<communication_style>
|
||||
- **Proactiveness**. As an agent, you are allowed to be proactive, but only in the course of completing the user's task. For example, if the user asks you to add a new component, you can edit the code, verify build and test statuses, and take any other obvious follow-up actions, such as performing additional research. However, avoid surprising the user. For example, if the user asks HOW to approach something, you should answer their question and instead of jumping into editing a file.</communication_style>`
|
||||
|
||||
func defaultIdentityPatch(_ string) string {
|
||||
return antigravityIdentity
|
||||
}
|
||||
|
||||
// buildSystemInstruction 构建 systemInstruction
|
||||
func buildSystemInstruction(system json.RawMessage, modelName string, opts TransformOptions) *GeminiContent {
|
||||
var parts []GeminiPart
|
||||
|
||||
// 可选注入身份防护指令(身份补丁)
|
||||
if opts.EnableIdentityPatch {
|
||||
identityPatch := strings.TrimSpace(opts.IdentityPatch)
|
||||
if identityPatch == "" {
|
||||
identityPatch = defaultIdentityPatch(modelName)
|
||||
}
|
||||
parts = append(parts, GeminiPart{Text: identityPatch})
|
||||
}
|
||||
// 先解析用户的 system prompt,检测是否已包含 Antigravity identity
|
||||
userHasAntigravityIdentity := false
|
||||
var userSystemParts []GeminiPart
|
||||
|
||||
// 解析 system prompt
|
||||
if len(system) > 0 {
|
||||
// 尝试解析为字符串
|
||||
var sysStr string
|
||||
if err := json.Unmarshal(system, &sysStr); err == nil {
|
||||
if strings.TrimSpace(sysStr) != "" {
|
||||
parts = append(parts, GeminiPart{Text: sysStr})
|
||||
userSystemParts = append(userSystemParts, GeminiPart{Text: sysStr})
|
||||
if strings.Contains(sysStr, "You are Antigravity") {
|
||||
userHasAntigravityIdentity = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 尝试解析为数组
|
||||
@@ -142,17 +173,28 @@ func buildSystemInstruction(system json.RawMessage, modelName string, opts Trans
|
||||
if err := json.Unmarshal(system, &sysBlocks); err == nil {
|
||||
for _, block := range sysBlocks {
|
||||
if block.Type == "text" && strings.TrimSpace(block.Text) != "" {
|
||||
parts = append(parts, GeminiPart{Text: block.Text})
|
||||
userSystemParts = append(userSystemParts, GeminiPart{Text: block.Text})
|
||||
if strings.Contains(block.Text, "You are Antigravity") {
|
||||
userHasAntigravityIdentity = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// identity patch 模式下,用分隔符包裹 system prompt,便于上游识别/调试;关闭时尽量保持原始 system prompt。
|
||||
if opts.EnableIdentityPatch && len(parts) > 0 {
|
||||
parts = append(parts, GeminiPart{Text: "\n--- [SYSTEM_PROMPT_END] ---"})
|
||||
// 仅在用户未提供 Antigravity identity 时注入
|
||||
if opts.EnableIdentityPatch && !userHasAntigravityIdentity {
|
||||
identityPatch := strings.TrimSpace(opts.IdentityPatch)
|
||||
if identityPatch == "" {
|
||||
identityPatch = defaultIdentityPatch(modelName)
|
||||
}
|
||||
parts = append(parts, GeminiPart{Text: identityPatch})
|
||||
}
|
||||
|
||||
// 添加用户的 system prompt
|
||||
parts = append(parts, userSystemParts...)
|
||||
|
||||
if len(parts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -40,7 +39,7 @@ type Options struct {
|
||||
ProxyURL string // 代理 URL(支持 http/https/socks5/socks5h)
|
||||
Timeout time.Duration // 请求总超时时间
|
||||
ResponseHeaderTimeout time.Duration // 等待响应头超时时间
|
||||
InsecureSkipVerify bool // 是否跳过 TLS 证书验证
|
||||
InsecureSkipVerify bool // 是否跳过 TLS 证书验证(已禁用,不允许设置为 true)
|
||||
ProxyStrict bool // 严格代理模式:代理失败时返回错误而非回退
|
||||
ValidateResolvedIP bool // 是否校验解析后的 IP(防止 DNS Rebinding)
|
||||
AllowPrivateHosts bool // 允许私有地址解析(与 ValidateResolvedIP 一起使用)
|
||||
@@ -113,7 +112,8 @@ func buildTransport(opts Options) (*http.Transport, error) {
|
||||
}
|
||||
|
||||
if opts.InsecureSkipVerify {
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
// 安全要求:禁止跳过证书验证,避免中间人攻击。
|
||||
return nil, fmt.Errorf("insecure_skip_verify is not allowed; install a trusted certificate instead")
|
||||
}
|
||||
|
||||
proxyURL := strings.TrimSpace(opts.ProxyURL)
|
||||
|
||||
@@ -122,3 +122,40 @@ func StartOfMonth(t time.Time) time.Time {
|
||||
func ParseInLocation(layout, value string) (time.Time, error) {
|
||||
return time.ParseInLocation(layout, value, Location())
|
||||
}
|
||||
|
||||
// ParseInUserLocation parses a time string in the user's timezone.
|
||||
// If userTZ is empty or invalid, falls back to the configured server timezone.
|
||||
func ParseInUserLocation(layout, value, userTZ string) (time.Time, error) {
|
||||
loc := Location() // default to server timezone
|
||||
if userTZ != "" {
|
||||
if userLoc, err := time.LoadLocation(userTZ); err == nil {
|
||||
loc = userLoc
|
||||
}
|
||||
}
|
||||
return time.ParseInLocation(layout, value, loc)
|
||||
}
|
||||
|
||||
// NowInUserLocation returns the current time in the user's timezone.
|
||||
// If userTZ is empty or invalid, falls back to the configured server timezone.
|
||||
func NowInUserLocation(userTZ string) time.Time {
|
||||
if userTZ == "" {
|
||||
return Now()
|
||||
}
|
||||
if userLoc, err := time.LoadLocation(userTZ); err == nil {
|
||||
return time.Now().In(userLoc)
|
||||
}
|
||||
return Now()
|
||||
}
|
||||
|
||||
// StartOfDayInUserLocation returns the start of the given day in the user's timezone.
|
||||
// If userTZ is empty or invalid, falls back to the configured server timezone.
|
||||
func StartOfDayInUserLocation(t time.Time, userTZ string) time.Time {
|
||||
loc := Location()
|
||||
if userTZ != "" {
|
||||
if userLoc, err := time.LoadLocation(userTZ); err == nil {
|
||||
loc = userLoc
|
||||
}
|
||||
}
|
||||
t = t.In(loc)
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,8 @@ func (r *accountRepository) Create(ctx context.Context, account *service.Account
|
||||
SetPriority(account.Priority).
|
||||
SetStatus(account.Status).
|
||||
SetErrorMessage(account.ErrorMessage).
|
||||
SetSchedulable(account.Schedulable)
|
||||
SetSchedulable(account.Schedulable).
|
||||
SetAutoPauseOnExpired(account.AutoPauseOnExpired)
|
||||
|
||||
if account.ProxyID != nil {
|
||||
builder.SetProxyID(*account.ProxyID)
|
||||
@@ -84,6 +85,9 @@ func (r *accountRepository) Create(ctx context.Context, account *service.Account
|
||||
if account.LastUsedAt != nil {
|
||||
builder.SetLastUsedAt(*account.LastUsedAt)
|
||||
}
|
||||
if account.ExpiresAt != nil {
|
||||
builder.SetExpiresAt(*account.ExpiresAt)
|
||||
}
|
||||
if account.RateLimitedAt != nil {
|
||||
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
||||
}
|
||||
@@ -280,7 +284,8 @@ func (r *accountRepository) Update(ctx context.Context, account *service.Account
|
||||
SetPriority(account.Priority).
|
||||
SetStatus(account.Status).
|
||||
SetErrorMessage(account.ErrorMessage).
|
||||
SetSchedulable(account.Schedulable)
|
||||
SetSchedulable(account.Schedulable).
|
||||
SetAutoPauseOnExpired(account.AutoPauseOnExpired)
|
||||
|
||||
if account.ProxyID != nil {
|
||||
builder.SetProxyID(*account.ProxyID)
|
||||
@@ -292,6 +297,11 @@ func (r *accountRepository) Update(ctx context.Context, account *service.Account
|
||||
} else {
|
||||
builder.ClearLastUsedAt()
|
||||
}
|
||||
if account.ExpiresAt != nil {
|
||||
builder.SetExpiresAt(*account.ExpiresAt)
|
||||
} else {
|
||||
builder.ClearExpiresAt()
|
||||
}
|
||||
if account.RateLimitedAt != nil {
|
||||
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
||||
} else {
|
||||
@@ -570,6 +580,7 @@ func (r *accountRepository) ListSchedulable(ctx context.Context) ([]service.Acco
|
||||
dbaccount.StatusEQ(service.StatusActive),
|
||||
dbaccount.SchedulableEQ(true),
|
||||
tempUnschedulablePredicate(),
|
||||
notExpiredPredicate(now),
|
||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(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.SchedulableEQ(true),
|
||||
tempUnschedulablePredicate(),
|
||||
notExpiredPredicate(now),
|
||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(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.SchedulableEQ(true),
|
||||
tempUnschedulablePredicate(),
|
||||
notExpiredPredicate(now),
|
||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||
).
|
||||
@@ -727,6 +740,27 @@ func (r *accountRepository) SetSchedulable(ctx context.Context, id int64, schedu
|
||||
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 {
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
@@ -861,6 +895,7 @@ func (r *accountRepository) queryAccountsByGroup(ctx context.Context, groupID in
|
||||
preds = append(preds,
|
||||
dbaccount.SchedulableEQ(true),
|
||||
tempUnschedulablePredicate(),
|
||||
notExpiredPredicate(now),
|
||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(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) {
|
||||
out := make(map[int64]tempUnschedSnapshot)
|
||||
if len(accountIDs) == 0 {
|
||||
@@ -1086,6 +1129,8 @@ func accountEntityToService(m *dbent.Account) *service.Account {
|
||||
Status: m.Status,
|
||||
ErrorMessage: derefString(m.ErrorMessage),
|
||||
LastUsedAt: m.LastUsedAt,
|
||||
ExpiresAt: m.ExpiresAt,
|
||||
AutoPauseOnExpired: m.AutoPauseOnExpired,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
Schedulable: m.Schedulable,
|
||||
|
||||
@@ -321,6 +321,9 @@ func groupEntityToService(g *dbent.Group) *service.Group {
|
||||
DailyLimitUSD: g.DailyLimitUsd,
|
||||
WeeklyLimitUSD: g.WeeklyLimitUsd,
|
||||
MonthlyLimitUSD: g.MonthlyLimitUsd,
|
||||
ImagePrice1K: g.ImagePrice1k,
|
||||
ImagePrice2K: g.ImagePrice2k,
|
||||
ImagePrice4K: g.ImagePrice4k,
|
||||
DefaultValidityDays: g.DefaultValidityDays,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
|
||||
@@ -56,7 +56,7 @@ func InitEnt(cfg *config.Config) (*ent.Client, *sql.DB, error) {
|
||||
// 确保数据库 schema 已准备就绪。
|
||||
// SQL 迁移文件是 schema 的权威来源(source of truth)。
|
||||
// 这种方式比 Ent 的自动迁移更可控,支持复杂的迁移场景。
|
||||
migrationCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
migrationCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
if err := applyMigrationsFS(migrationCtx, drv.DB(), migrations.FS); err != nil {
|
||||
_ = drv.Close() // 迁移失败时关闭驱动,避免资源泄露
|
||||
|
||||
@@ -14,23 +14,33 @@ import (
|
||||
)
|
||||
|
||||
type githubReleaseClient struct {
|
||||
httpClient *http.Client
|
||||
allowPrivateHosts bool
|
||||
httpClient *http.Client
|
||||
downloadHTTPClient *http.Client
|
||||
}
|
||||
|
||||
func NewGitHubReleaseClient() service.GitHubReleaseClient {
|
||||
allowPrivate := false
|
||||
// NewGitHubReleaseClient 创建 GitHub Release 客户端
|
||||
// proxyURL 为空时直连 GitHub,支持 http/https/socks5/socks5h 协议
|
||||
func NewGitHubReleaseClient(proxyURL string) service.GitHubReleaseClient {
|
||||
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
||||
Timeout: 30 * time.Second,
|
||||
ValidateResolvedIP: true,
|
||||
AllowPrivateHosts: allowPrivate,
|
||||
Timeout: 30 * time.Second,
|
||||
ProxyURL: proxyURL,
|
||||
})
|
||||
if err != nil {
|
||||
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{
|
||||
httpClient: sharedClient,
|
||||
allowPrivateHosts: allowPrivate,
|
||||
httpClient: sharedClient,
|
||||
downloadHTTPClient: downloadClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,15 +78,8 @@ func (c *githubReleaseClient) DownloadFile(ctx context.Context, url, dest string
|
||||
return err
|
||||
}
|
||||
|
||||
downloadClient, err := httpclient.GetClient(httpclient.Options{
|
||||
Timeout: 10 * time.Minute,
|
||||
ValidateResolvedIP: true,
|
||||
AllowPrivateHosts: c.allowPrivateHosts,
|
||||
})
|
||||
if err != nil {
|
||||
downloadClient = &http.Client{Timeout: 10 * time.Minute}
|
||||
}
|
||||
resp, err := downloadClient.Do(req)
|
||||
// 使用预配置的下载客户端(已包含代理配置)
|
||||
resp, err := c.downloadHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
func newTestGitHubReleaseClient() *githubReleaseClient {
|
||||
return &githubReleaseClient{
|
||||
httpClient: &http.Client{},
|
||||
allowPrivateHosts: true,
|
||||
httpClient: &http.Client{},
|
||||
downloadHTTPClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Success() {
|
||||
httpClient: &http.Client{
|
||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||
},
|
||||
allowPrivateHosts: true,
|
||||
downloadHTTPClient: &http.Client{},
|
||||
}
|
||||
|
||||
release, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||
@@ -254,7 +254,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Non200() {
|
||||
httpClient: &http.Client{
|
||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||
},
|
||||
allowPrivateHosts: true,
|
||||
downloadHTTPClient: &http.Client{},
|
||||
}
|
||||
|
||||
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||
@@ -272,7 +272,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_InvalidJSON() {
|
||||
httpClient: &http.Client{
|
||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||
},
|
||||
allowPrivateHosts: true,
|
||||
downloadHTTPClient: &http.Client{},
|
||||
}
|
||||
|
||||
_, err := s.client.FetchLatestRelease(context.Background(), "test/repo")
|
||||
@@ -288,7 +288,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_ContextCancel() {
|
||||
httpClient: &http.Client{
|
||||
Transport: &testTransport{testServerURL: s.srv.URL},
|
||||
},
|
||||
allowPrivateHosts: true,
|
||||
downloadHTTPClient: &http.Client{},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -43,6 +43,9 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er
|
||||
SetNillableDailyLimitUsd(groupIn.DailyLimitUSD).
|
||||
SetNillableWeeklyLimitUsd(groupIn.WeeklyLimitUSD).
|
||||
SetNillableMonthlyLimitUsd(groupIn.MonthlyLimitUSD).
|
||||
SetNillableImagePrice1k(groupIn.ImagePrice1K).
|
||||
SetNillableImagePrice2k(groupIn.ImagePrice2K).
|
||||
SetNillableImagePrice4k(groupIn.ImagePrice4K).
|
||||
SetDefaultValidityDays(groupIn.DefaultValidityDays)
|
||||
|
||||
created, err := builder.Save(ctx)
|
||||
@@ -80,6 +83,9 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er
|
||||
SetNillableDailyLimitUsd(groupIn.DailyLimitUSD).
|
||||
SetNillableWeeklyLimitUsd(groupIn.WeeklyLimitUSD).
|
||||
SetNillableMonthlyLimitUsd(groupIn.MonthlyLimitUSD).
|
||||
SetNillableImagePrice1k(groupIn.ImagePrice1K).
|
||||
SetNillableImagePrice2k(groupIn.ImagePrice2K).
|
||||
SetNillableImagePrice4k(groupIn.ImagePrice4K).
|
||||
SetDefaultValidityDays(groupIn.DefaultValidityDays).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
@@ -17,17 +16,12 @@ type pricingRemoteClient struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewPricingRemoteClient(cfg *config.Config) service.PricingRemoteClient {
|
||||
allowPrivate := false
|
||||
validateResolvedIP := true
|
||||
if cfg != nil {
|
||||
allowPrivate = cfg.Security.URLAllowlist.AllowPrivateHosts
|
||||
validateResolvedIP = cfg.Security.URLAllowlist.Enabled
|
||||
}
|
||||
// NewPricingRemoteClient 创建定价数据远程客户端
|
||||
// proxyURL 为空时直连,支持 http/https/socks5/socks5h 协议
|
||||
func NewPricingRemoteClient(proxyURL string) service.PricingRemoteClient {
|
||||
sharedClient, err := httpclient.GetClient(httpclient.Options{
|
||||
Timeout: 30 * time.Second,
|
||||
ValidateResolvedIP: validateResolvedIP,
|
||||
AllowPrivateHosts: allowPrivate,
|
||||
Timeout: 30 * time.Second,
|
||||
ProxyURL: proxyURL,
|
||||
})
|
||||
if err != nil {
|
||||
sharedClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@@ -20,13 +19,7 @@ type PricingServiceSuite struct {
|
||||
|
||||
func (s *PricingServiceSuite) SetupTest() {
|
||||
s.ctx = context.Background()
|
||||
client, ok := NewPricingRemoteClient(&config.Config{
|
||||
Security: config.SecurityConfig{
|
||||
URLAllowlist: config.URLAllowlistConfig{
|
||||
AllowPrivateHosts: true,
|
||||
},
|
||||
},
|
||||
}).(*pricingRemoteClient)
|
||||
client, ok := NewPricingRemoteClient("").(*pricingRemoteClient)
|
||||
require.True(s.T(), ok, "type assertion failed")
|
||||
s.client = client
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func NewProxyExitInfoProber(cfg *config.Config) service.ProxyExitInfoProber {
|
||||
validateResolvedIP = cfg.Security.URLAllowlist.Enabled
|
||||
}
|
||||
if insecure {
|
||||
log.Printf("[ProxyProbe] Warning: TLS verification is disabled for proxy probing.")
|
||||
log.Printf("[ProxyProbe] Warning: insecure_skip_verify is not allowed and will cause probe failure.")
|
||||
}
|
||||
return &proxyProbeService{
|
||||
ipInfoURL: defaultIPInfoURL,
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"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, 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 {
|
||||
client *dbent.Client
|
||||
@@ -109,6 +109,9 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
||||
stream,
|
||||
duration_ms,
|
||||
first_token_ms,
|
||||
user_agent,
|
||||
image_count,
|
||||
image_size,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
@@ -116,7 +119,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
||||
$8, $9, $10, $11,
|
||||
$12, $13,
|
||||
$14, $15, $16, $17, $18, $19,
|
||||
$20, $21, $22, $23, $24, $25
|
||||
$20, $21, $22, $23, $24, $25, $26, $27, $28
|
||||
)
|
||||
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
||||
RETURNING id, created_at
|
||||
@@ -126,6 +129,8 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
||||
subscriptionID := nullInt64(log.SubscriptionID)
|
||||
duration := nullInt(log.DurationMs)
|
||||
firstToken := nullInt(log.FirstTokenMs)
|
||||
userAgent := nullString(log.UserAgent)
|
||||
imageSize := nullString(log.ImageSize)
|
||||
|
||||
var requestIDArg any
|
||||
if requestID != "" {
|
||||
@@ -157,6 +162,9 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
|
||||
log.Stream,
|
||||
duration,
|
||||
firstToken,
|
||||
userAgent,
|
||||
log.ImageCount,
|
||||
imageSize,
|
||||
createdAt,
|
||||
}
|
||||
if err := scanSingleRow(ctx, sqlq, query, args, &log.ID, &log.CreatedAt); err != nil {
|
||||
@@ -1382,6 +1390,81 @@ func (r *usageLogRepository) GetGlobalStats(ctx context.Context, startTime, endT
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetStatsWithFilters gets usage statistics with optional filters
|
||||
func (r *usageLogRepository) GetStatsWithFilters(ctx context.Context, filters UsageLogFilters) (*UsageStats, error) {
|
||||
conditions := make([]string, 0, 9)
|
||||
args := make([]any, 0, 9)
|
||||
|
||||
if filters.UserID > 0 {
|
||||
conditions = append(conditions, fmt.Sprintf("user_id = $%d", len(args)+1))
|
||||
args = append(args, filters.UserID)
|
||||
}
|
||||
if filters.APIKeyID > 0 {
|
||||
conditions = append(conditions, fmt.Sprintf("api_key_id = $%d", len(args)+1))
|
||||
args = append(args, filters.APIKeyID)
|
||||
}
|
||||
if filters.AccountID > 0 {
|
||||
conditions = append(conditions, fmt.Sprintf("account_id = $%d", len(args)+1))
|
||||
args = append(args, filters.AccountID)
|
||||
}
|
||||
if filters.GroupID > 0 {
|
||||
conditions = append(conditions, fmt.Sprintf("group_id = $%d", len(args)+1))
|
||||
args = append(args, filters.GroupID)
|
||||
}
|
||||
if filters.Model != "" {
|
||||
conditions = append(conditions, fmt.Sprintf("model = $%d", len(args)+1))
|
||||
args = append(args, filters.Model)
|
||||
}
|
||||
if filters.Stream != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("stream = $%d", len(args)+1))
|
||||
args = append(args, *filters.Stream)
|
||||
}
|
||||
if filters.BillingType != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("billing_type = $%d", len(args)+1))
|
||||
args = append(args, int16(*filters.BillingType))
|
||||
}
|
||||
if filters.StartTime != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("created_at >= $%d", len(args)+1))
|
||||
args = append(args, *filters.StartTime)
|
||||
}
|
||||
if filters.EndTime != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("created_at <= $%d", len(args)+1))
|
||||
args = append(args, *filters.EndTime)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
COUNT(*) as total_requests,
|
||||
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
|
||||
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
|
||||
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
|
||||
COALESCE(SUM(total_cost), 0) as total_cost,
|
||||
COALESCE(SUM(actual_cost), 0) as total_actual_cost,
|
||||
COALESCE(AVG(duration_ms), 0) as avg_duration_ms
|
||||
FROM usage_logs
|
||||
%s
|
||||
`, buildWhere(conditions))
|
||||
|
||||
stats := &UsageStats{}
|
||||
if err := scanSingleRow(
|
||||
ctx,
|
||||
r.sql,
|
||||
query,
|
||||
args,
|
||||
&stats.TotalRequests,
|
||||
&stats.TotalInputTokens,
|
||||
&stats.TotalOutputTokens,
|
||||
&stats.TotalCacheTokens,
|
||||
&stats.TotalCost,
|
||||
&stats.TotalActualCost,
|
||||
&stats.AverageDurationMs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// AccountUsageHistory represents daily usage history for an account
|
||||
type AccountUsageHistory = usagestats.AccountUsageHistory
|
||||
|
||||
@@ -1789,6 +1872,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
stream bool
|
||||
durationMs sql.NullInt64
|
||||
firstTokenMs sql.NullInt64
|
||||
userAgent sql.NullString
|
||||
imageCount int
|
||||
imageSize sql.NullString
|
||||
createdAt time.Time
|
||||
)
|
||||
|
||||
@@ -1818,6 +1904,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
&stream,
|
||||
&durationMs,
|
||||
&firstTokenMs,
|
||||
&userAgent,
|
||||
&imageCount,
|
||||
&imageSize,
|
||||
&createdAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -1844,6 +1933,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
RateMultiplier: rateMultiplier,
|
||||
BillingType: int8(billingType),
|
||||
Stream: stream,
|
||||
ImageCount: imageCount,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
|
||||
@@ -1866,6 +1956,12 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
value := int(firstTokenMs.Int64)
|
||||
log.FirstTokenMs = &value
|
||||
}
|
||||
if userAgent.Valid {
|
||||
log.UserAgent = &userAgent.String
|
||||
}
|
||||
if imageSize.Valid {
|
||||
log.ImageSize = &imageSize.String
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
@@ -1938,6 +2034,13 @@ func nullInt(v *int) sql.NullInt64 {
|
||||
return sql.NullInt64{Int64: int64(*v), Valid: true}
|
||||
}
|
||||
|
||||
func nullString(v *string) sql.NullString {
|
||||
if v == nil || *v == "" {
|
||||
return sql.NullString{}
|
||||
}
|
||||
return sql.NullString{String: *v, Valid: true}
|
||||
}
|
||||
|
||||
func setToSlice(set map[int64]struct{}) []int64 {
|
||||
out := make([]int64, 0, len(set))
|
||||
for id := range set {
|
||||
|
||||
@@ -25,6 +25,18 @@ func ProvideConcurrencyCache(rdb *redis.Client, cfg *config.Config) service.Conc
|
||||
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
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewUserRepository,
|
||||
@@ -53,8 +65,8 @@ var ProviderSet = wire.NewSet(
|
||||
|
||||
// HTTP service ports (DI Strategy A: return interface directly)
|
||||
NewTurnstileVerifier,
|
||||
NewPricingRemoteClient,
|
||||
NewGitHubReleaseClient,
|
||||
ProvidePricingRemoteClient,
|
||||
ProvideGitHubReleaseClient,
|
||||
NewProxyExitInfoProber,
|
||||
NewClaudeUsageFetcher,
|
||||
NewClaudeOAuthClient,
|
||||
|
||||
@@ -241,6 +241,8 @@ func TestAPIContracts(t *testing.T) {
|
||||
"stream": true,
|
||||
"duration_ms": 100,
|
||||
"first_token_ms": 50,
|
||||
"image_count": 0,
|
||||
"image_size": null,
|
||||
"created_at": "2025-01-02T03:04:05Z"
|
||||
}
|
||||
],
|
||||
@@ -1063,6 +1065,10 @@ func (r *stubUsageLogRepo) GetAccountUsageStats(ctx context.Context, accountID i
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (r *stubUsageLogRepo) GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
type stubSettingRepo struct {
|
||||
all map[string]string
|
||||
}
|
||||
|
||||
@@ -9,21 +9,23 @@ import (
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
ID int64
|
||||
Name string
|
||||
Notes *string
|
||||
Platform string
|
||||
Type string
|
||||
Credentials map[string]any
|
||||
Extra map[string]any
|
||||
ProxyID *int64
|
||||
Concurrency int
|
||||
Priority int
|
||||
Status string
|
||||
ErrorMessage string
|
||||
LastUsedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID int64
|
||||
Name string
|
||||
Notes *string
|
||||
Platform string
|
||||
Type string
|
||||
Credentials map[string]any
|
||||
Extra map[string]any
|
||||
ProxyID *int64
|
||||
Concurrency int
|
||||
Priority int
|
||||
Status string
|
||||
ErrorMessage string
|
||||
LastUsedAt *time.Time
|
||||
ExpiresAt *time.Time
|
||||
AutoPauseOnExpired bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
Schedulable bool
|
||||
|
||||
@@ -60,6 +62,9 @@ func (a *Account) IsSchedulable() bool {
|
||||
return false
|
||||
}
|
||||
now := time.Now()
|
||||
if a.AutoPauseOnExpired && a.ExpiresAt != nil && !now.Before(*a.ExpiresAt) {
|
||||
return false
|
||||
}
|
||||
if a.OverloadUntil != nil && now.Before(*a.OverloadUntil) {
|
||||
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
|
||||
SetError(ctx context.Context, id int64, errorMsg string) 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
|
||||
|
||||
ListSchedulable(ctx context.Context) ([]Account, error)
|
||||
@@ -71,29 +72,33 @@ type AccountBulkUpdate struct {
|
||||
|
||||
// CreateAccountRequest 创建账号请求
|
||||
type CreateAccountRequest struct {
|
||||
Name string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Credentials map[string]any `json:"credentials"`
|
||||
Extra map[string]any `json:"extra"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
Priority int `json:"priority"`
|
||||
GroupIDs []int64 `json:"group_ids"`
|
||||
Name string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Platform string `json:"platform"`
|
||||
Type string `json:"type"`
|
||||
Credentials map[string]any `json:"credentials"`
|
||||
Extra map[string]any `json:"extra"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
Priority int `json:"priority"`
|
||||
GroupIDs []int64 `json:"group_ids"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||
}
|
||||
|
||||
// UpdateAccountRequest 更新账号请求
|
||||
type UpdateAccountRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Credentials *map[string]any `json:"credentials"`
|
||||
Extra *map[string]any `json:"extra"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
Concurrency *int `json:"concurrency"`
|
||||
Priority *int `json:"priority"`
|
||||
Status *string `json:"status"`
|
||||
GroupIDs *[]int64 `json:"group_ids"`
|
||||
Name *string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Credentials *map[string]any `json:"credentials"`
|
||||
Extra *map[string]any `json:"extra"`
|
||||
ProxyID *int64 `json:"proxy_id"`
|
||||
Concurrency *int `json:"concurrency"`
|
||||
Priority *int `json:"priority"`
|
||||
Status *string `json:"status"`
|
||||
GroupIDs *[]int64 `json:"group_ids"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||
}
|
||||
|
||||
// AccountService 账号管理服务
|
||||
@@ -134,6 +139,12 @@ func (s *AccountService) Create(ctx context.Context, req CreateAccountRequest) (
|
||||
Concurrency: req.Concurrency,
|
||||
Priority: req.Priority,
|
||||
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 {
|
||||
@@ -224,6 +235,12 @@ func (s *AccountService) Update(ctx context.Context, id int64, req UpdateAccount
|
||||
if req.Status != nil {
|
||||
account.Status = *req.Status
|
||||
}
|
||||
if req.ExpiresAt != nil {
|
||||
account.ExpiresAt = req.ExpiresAt
|
||||
}
|
||||
if req.AutoPauseOnExpired != nil {
|
||||
account.AutoPauseOnExpired = *req.AutoPauseOnExpired
|
||||
}
|
||||
|
||||
// 先验证分组是否存在(在任何写操作之前)
|
||||
if req.GroupIDs != nil {
|
||||
|
||||
@@ -103,6 +103,10 @@ func (s *accountRepoStub) SetSchedulable(ctx context.Context, id int64, schedula
|
||||
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 {
|
||||
panic("unexpected BindGroups call")
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ type UsageLogRepository interface {
|
||||
// Admin usage listing/stats
|
||||
ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]UsageLog, *pagination.PaginationResult, error)
|
||||
GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error)
|
||||
GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error)
|
||||
|
||||
// Account stats
|
||||
GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error)
|
||||
|
||||
@@ -98,6 +98,10 @@ type CreateGroupInput struct {
|
||||
DailyLimitUSD *float64 // 日限额 (USD)
|
||||
WeeklyLimitUSD *float64 // 周限额 (USD)
|
||||
MonthlyLimitUSD *float64 // 月限额 (USD)
|
||||
// 图片生成计费配置(仅 antigravity 平台使用)
|
||||
ImagePrice1K *float64
|
||||
ImagePrice2K *float64
|
||||
ImagePrice4K *float64
|
||||
}
|
||||
|
||||
type UpdateGroupInput struct {
|
||||
@@ -111,19 +115,25 @@ type UpdateGroupInput struct {
|
||||
DailyLimitUSD *float64 // 日限额 (USD)
|
||||
WeeklyLimitUSD *float64 // 周限额 (USD)
|
||||
MonthlyLimitUSD *float64 // 月限额 (USD)
|
||||
// 图片生成计费配置(仅 antigravity 平台使用)
|
||||
ImagePrice1K *float64
|
||||
ImagePrice2K *float64
|
||||
ImagePrice4K *float64
|
||||
}
|
||||
|
||||
type CreateAccountInput struct {
|
||||
Name string
|
||||
Notes *string
|
||||
Platform string
|
||||
Type string
|
||||
Credentials map[string]any
|
||||
Extra map[string]any
|
||||
ProxyID *int64
|
||||
Concurrency int
|
||||
Priority int
|
||||
GroupIDs []int64
|
||||
Name string
|
||||
Notes *string
|
||||
Platform string
|
||||
Type string
|
||||
Credentials map[string]any
|
||||
Extra map[string]any
|
||||
ProxyID *int64
|
||||
Concurrency int
|
||||
Priority int
|
||||
GroupIDs []int64
|
||||
ExpiresAt *int64
|
||||
AutoPauseOnExpired *bool
|
||||
// SkipMixedChannelCheck skips the mixed channel risk check when binding groups.
|
||||
// This should only be set when the caller has explicitly confirmed the risk.
|
||||
SkipMixedChannelCheck bool
|
||||
@@ -140,6 +150,8 @@ type UpdateAccountInput struct {
|
||||
Priority *int // 使用指针区分"未提供"和"设置为0"
|
||||
Status string
|
||||
GroupIDs *[]int64
|
||||
ExpiresAt *int64
|
||||
AutoPauseOnExpired *bool
|
||||
SkipMixedChannelCheck bool // 跳过混合渠道检查(用户已确认风险)
|
||||
}
|
||||
|
||||
@@ -498,6 +510,11 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn
|
||||
weeklyLimit := normalizeLimit(input.WeeklyLimitUSD)
|
||||
monthlyLimit := normalizeLimit(input.MonthlyLimitUSD)
|
||||
|
||||
// 图片价格:负数表示清除(使用默认价格),0 保留(表示免费)
|
||||
imagePrice1K := normalizePrice(input.ImagePrice1K)
|
||||
imagePrice2K := normalizePrice(input.ImagePrice2K)
|
||||
imagePrice4K := normalizePrice(input.ImagePrice4K)
|
||||
|
||||
group := &Group{
|
||||
Name: input.Name,
|
||||
Description: input.Description,
|
||||
@@ -509,6 +526,9 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn
|
||||
DailyLimitUSD: dailyLimit,
|
||||
WeeklyLimitUSD: weeklyLimit,
|
||||
MonthlyLimitUSD: monthlyLimit,
|
||||
ImagePrice1K: imagePrice1K,
|
||||
ImagePrice2K: imagePrice2K,
|
||||
ImagePrice4K: imagePrice4K,
|
||||
}
|
||||
if err := s.groupRepo.Create(ctx, group); err != nil {
|
||||
return nil, err
|
||||
@@ -524,6 +544,14 @@ func normalizeLimit(limit *float64) *float64 {
|
||||
return limit
|
||||
}
|
||||
|
||||
// normalizePrice 将负数转换为 nil(表示使用默认价格),0 保留(表示免费)
|
||||
func normalizePrice(price *float64) *float64 {
|
||||
if price == nil || *price < 0 {
|
||||
return nil
|
||||
}
|
||||
return price
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *UpdateGroupInput) (*Group, error) {
|
||||
group, err := s.groupRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -563,6 +591,16 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd
|
||||
if input.MonthlyLimitUSD != nil {
|
||||
group.MonthlyLimitUSD = normalizeLimit(input.MonthlyLimitUSD)
|
||||
}
|
||||
// 图片生成计费配置:负数表示清除(使用默认价格)
|
||||
if input.ImagePrice1K != nil {
|
||||
group.ImagePrice1K = normalizePrice(input.ImagePrice1K)
|
||||
}
|
||||
if input.ImagePrice2K != nil {
|
||||
group.ImagePrice2K = normalizePrice(input.ImagePrice2K)
|
||||
}
|
||||
if input.ImagePrice4K != nil {
|
||||
group.ImagePrice4K = normalizePrice(input.ImagePrice4K)
|
||||
}
|
||||
|
||||
if err := s.groupRepo.Update(ctx, group); err != nil {
|
||||
return nil, err
|
||||
@@ -666,6 +704,15 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
|
||||
Status: StatusActive,
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -721,6 +768,17 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
||||
if 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 {
|
||||
|
||||
197
backend/internal/service/admin_service_group_test.go
Normal file
197
backend/internal/service/admin_service_group_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// groupRepoStubForAdmin 用于测试 AdminService 的 GroupRepository Stub
|
||||
type groupRepoStubForAdmin struct {
|
||||
created *Group // 记录 Create 调用的参数
|
||||
updated *Group // 记录 Update 调用的参数
|
||||
getByID *Group // GetByID 返回值
|
||||
getErr error // GetByID 返回的错误
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) Create(_ context.Context, g *Group) error {
|
||||
s.created = g
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) Update(_ context.Context, g *Group) error {
|
||||
s.updated = g
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) GetByID(_ context.Context, _ int64) (*Group, error) {
|
||||
if s.getErr != nil {
|
||||
return nil, s.getErr
|
||||
}
|
||||
return s.getByID, nil
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) Delete(_ context.Context, _ int64) error {
|
||||
panic("unexpected Delete call")
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) DeleteCascade(_ context.Context, _ int64) ([]int64, error) {
|
||||
panic("unexpected DeleteCascade call")
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) List(_ context.Context, _ pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) {
|
||||
panic("unexpected List call")
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) ListWithFilters(_ context.Context, _ pagination.PaginationParams, _, _ string, _ *bool) ([]Group, *pagination.PaginationResult, error) {
|
||||
panic("unexpected ListWithFilters call")
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) ListActive(_ context.Context) ([]Group, error) {
|
||||
panic("unexpected ListActive call")
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) ListActiveByPlatform(_ context.Context, _ string) ([]Group, error) {
|
||||
panic("unexpected ListActiveByPlatform call")
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) ExistsByName(_ context.Context, _ string) (bool, error) {
|
||||
panic("unexpected ExistsByName call")
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) GetAccountCount(_ context.Context, _ int64) (int64, error) {
|
||||
panic("unexpected GetAccountCount call")
|
||||
}
|
||||
|
||||
func (s *groupRepoStubForAdmin) DeleteAccountGroupsByGroupID(_ context.Context, _ int64) (int64, error) {
|
||||
panic("unexpected DeleteAccountGroupsByGroupID call")
|
||||
}
|
||||
|
||||
// TestAdminService_CreateGroup_WithImagePricing 测试创建分组时 ImagePrice 字段正确传递
|
||||
func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) {
|
||||
repo := &groupRepoStubForAdmin{}
|
||||
svc := &adminServiceImpl{groupRepo: repo}
|
||||
|
||||
price1K := 0.10
|
||||
price2K := 0.15
|
||||
price4K := 0.30
|
||||
|
||||
input := &CreateGroupInput{
|
||||
Name: "test-group",
|
||||
Description: "Test group",
|
||||
Platform: PlatformAntigravity,
|
||||
RateMultiplier: 1.0,
|
||||
ImagePrice1K: &price1K,
|
||||
ImagePrice2K: &price2K,
|
||||
ImagePrice4K: &price4K,
|
||||
}
|
||||
|
||||
group, err := svc.CreateGroup(context.Background(), input)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, group)
|
||||
|
||||
// 验证 repo 收到了正确的字段
|
||||
require.NotNil(t, repo.created)
|
||||
require.NotNil(t, repo.created.ImagePrice1K)
|
||||
require.NotNil(t, repo.created.ImagePrice2K)
|
||||
require.NotNil(t, repo.created.ImagePrice4K)
|
||||
require.InDelta(t, 0.10, *repo.created.ImagePrice1K, 0.0001)
|
||||
require.InDelta(t, 0.15, *repo.created.ImagePrice2K, 0.0001)
|
||||
require.InDelta(t, 0.30, *repo.created.ImagePrice4K, 0.0001)
|
||||
}
|
||||
|
||||
// TestAdminService_CreateGroup_NilImagePricing 测试 ImagePrice 为 nil 时正常创建
|
||||
func TestAdminService_CreateGroup_NilImagePricing(t *testing.T) {
|
||||
repo := &groupRepoStubForAdmin{}
|
||||
svc := &adminServiceImpl{groupRepo: repo}
|
||||
|
||||
input := &CreateGroupInput{
|
||||
Name: "test-group",
|
||||
Description: "Test group",
|
||||
Platform: PlatformAntigravity,
|
||||
RateMultiplier: 1.0,
|
||||
// ImagePrice 字段全部为 nil
|
||||
}
|
||||
|
||||
group, err := svc.CreateGroup(context.Background(), input)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, group)
|
||||
|
||||
// 验证 ImagePrice 字段为 nil
|
||||
require.NotNil(t, repo.created)
|
||||
require.Nil(t, repo.created.ImagePrice1K)
|
||||
require.Nil(t, repo.created.ImagePrice2K)
|
||||
require.Nil(t, repo.created.ImagePrice4K)
|
||||
}
|
||||
|
||||
// TestAdminService_UpdateGroup_WithImagePricing 测试更新分组时 ImagePrice 字段正确更新
|
||||
func TestAdminService_UpdateGroup_WithImagePricing(t *testing.T) {
|
||||
existingGroup := &Group{
|
||||
ID: 1,
|
||||
Name: "existing-group",
|
||||
Platform: PlatformAntigravity,
|
||||
Status: StatusActive,
|
||||
}
|
||||
repo := &groupRepoStubForAdmin{getByID: existingGroup}
|
||||
svc := &adminServiceImpl{groupRepo: repo}
|
||||
|
||||
price1K := 0.12
|
||||
price2K := 0.18
|
||||
price4K := 0.36
|
||||
|
||||
input := &UpdateGroupInput{
|
||||
ImagePrice1K: &price1K,
|
||||
ImagePrice2K: &price2K,
|
||||
ImagePrice4K: &price4K,
|
||||
}
|
||||
|
||||
group, err := svc.UpdateGroup(context.Background(), 1, input)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, group)
|
||||
|
||||
// 验证 repo 收到了更新后的字段
|
||||
require.NotNil(t, repo.updated)
|
||||
require.NotNil(t, repo.updated.ImagePrice1K)
|
||||
require.NotNil(t, repo.updated.ImagePrice2K)
|
||||
require.NotNil(t, repo.updated.ImagePrice4K)
|
||||
require.InDelta(t, 0.12, *repo.updated.ImagePrice1K, 0.0001)
|
||||
require.InDelta(t, 0.18, *repo.updated.ImagePrice2K, 0.0001)
|
||||
require.InDelta(t, 0.36, *repo.updated.ImagePrice4K, 0.0001)
|
||||
}
|
||||
|
||||
// TestAdminService_UpdateGroup_PartialImagePricing 测试仅更新部分 ImagePrice 字段
|
||||
func TestAdminService_UpdateGroup_PartialImagePricing(t *testing.T) {
|
||||
oldPrice2K := 0.15
|
||||
existingGroup := &Group{
|
||||
ID: 1,
|
||||
Name: "existing-group",
|
||||
Platform: PlatformAntigravity,
|
||||
Status: StatusActive,
|
||||
ImagePrice2K: &oldPrice2K, // 已有 2K 价格
|
||||
}
|
||||
repo := &groupRepoStubForAdmin{getByID: existingGroup}
|
||||
svc := &adminServiceImpl{groupRepo: repo}
|
||||
|
||||
// 只更新 1K 价格
|
||||
price1K := 0.10
|
||||
input := &UpdateGroupInput{
|
||||
ImagePrice1K: &price1K,
|
||||
// ImagePrice2K 和 ImagePrice4K 为 nil,不更新
|
||||
}
|
||||
|
||||
group, err := svc.UpdateGroup(context.Background(), 1, input)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, group)
|
||||
|
||||
// 验证:1K 被更新,2K 保持原值,4K 仍为 nil
|
||||
require.NotNil(t, repo.updated)
|
||||
require.NotNil(t, repo.updated.ImagePrice1K)
|
||||
require.InDelta(t, 0.10, *repo.updated.ImagePrice1K, 0.0001)
|
||||
require.NotNil(t, repo.updated.ImagePrice2K)
|
||||
require.InDelta(t, 0.15, *repo.updated.ImagePrice2K, 0.0001) // 原值保持
|
||||
require.Nil(t, repo.updated.ImagePrice4K)
|
||||
}
|
||||
@@ -181,12 +181,17 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account
|
||||
return nil, fmt.Errorf("构建请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建 HTTP 请求(非流式)
|
||||
req, err := antigravity.NewAPIRequest(ctx, "generateContent", accessToken, requestBody)
|
||||
// 构建 HTTP 请求(总是使用流式 endpoint,与官方客户端一致)
|
||||
req, err := antigravity.NewAPIRequest(ctx, "streamGenerateContent", accessToken, requestBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// DEBUG: 打印请求 header 和 body
|
||||
log.Printf("[DEBUG] Antigravity TestConnection - URL: %s", req.URL.String())
|
||||
log.Printf("[DEBUG] Antigravity TestConnection - Headers: %v", req.Header)
|
||||
log.Printf("[DEBUG] Antigravity TestConnection - Body: %s", string(requestBody))
|
||||
|
||||
// 代理 URL
|
||||
proxyURL := ""
|
||||
if account.ProxyID != nil && account.Proxy != nil {
|
||||
@@ -210,14 +215,8 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account
|
||||
return nil, fmt.Errorf("API 返回 %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
// 解包 v1internal 响应
|
||||
unwrapped, err := s.unwrapV1InternalResponse(respBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解包响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 提取响应文本
|
||||
text := extractGeminiResponseText(unwrapped)
|
||||
// 解析流式响应,提取文本
|
||||
text := extractTextFromSSEResponse(respBody)
|
||||
|
||||
return &TestConnectionResult{
|
||||
Text: text,
|
||||
@@ -267,38 +266,66 @@ func (s *AntigravityGatewayService) getClaudeTransformOptions(ctx context.Contex
|
||||
return opts
|
||||
}
|
||||
|
||||
// extractGeminiResponseText 从 Gemini 响应中提取文本
|
||||
func extractGeminiResponseText(respBody []byte) string {
|
||||
var resp map[string]any
|
||||
if err := json.Unmarshal(respBody, &resp); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
candidates, ok := resp["candidates"].([]any)
|
||||
if !ok || len(candidates) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
candidate, ok := candidates[0].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
content, ok := candidate["content"].(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts, ok := content["parts"].([]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractTextFromSSEResponse 从 SSE 流式响应中提取文本
|
||||
func extractTextFromSSEResponse(respBody []byte) string {
|
||||
var texts []string
|
||||
for _, part := range parts {
|
||||
if partMap, ok := part.(map[string]any); ok {
|
||||
if text, ok := partMap["text"].(string); ok && text != "" {
|
||||
texts = append(texts, text)
|
||||
lines := bytes.Split(respBody, []byte("\n"))
|
||||
|
||||
for _, line := range lines {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过 SSE 前缀
|
||||
if bytes.HasPrefix(line, []byte("data:")) {
|
||||
line = bytes.TrimPrefix(line, []byte("data:"))
|
||||
line = bytes.TrimSpace(line)
|
||||
}
|
||||
|
||||
// 跳过非 JSON 行
|
||||
if len(line) == 0 || line[0] != '{' {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var data map[string]any
|
||||
if err := json.Unmarshal(line, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 尝试从 response.candidates[0].content.parts[].text 提取
|
||||
response, ok := data["response"].(map[string]any)
|
||||
if !ok {
|
||||
// 尝试直接从 candidates 提取(某些响应格式)
|
||||
response = data
|
||||
}
|
||||
|
||||
candidates, ok := response["candidates"].([]any)
|
||||
if !ok || len(candidates) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
candidate, ok := candidates[0].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
content, ok := candidate["content"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
parts, ok := content["parts"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
if partMap, ok := part.(map[string]any); ok {
|
||||
if text, ok := partMap["text"].(string); ok && text != "" {
|
||||
texts = append(texts, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,7 +343,7 @@ func (s *AntigravityGatewayService) wrapV1InternalRequest(projectID, model strin
|
||||
wrapped := map[string]any{
|
||||
"project": projectID,
|
||||
"requestId": "agent-" + uuid.New().String(),
|
||||
"userAgent": "sub2api",
|
||||
"userAgent": "antigravity", // 固定值,与官方客户端一致
|
||||
"requestType": "agent",
|
||||
"model": model,
|
||||
"request": request,
|
||||
@@ -397,10 +424,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
||||
return nil, fmt.Errorf("transform request: %w", err)
|
||||
}
|
||||
|
||||
// 构建上游 action
|
||||
// 构建上游 action(NewAPIRequest 会自动处理 ?alt=sse 和 Accept Header)
|
||||
action := "generateContent"
|
||||
if claudeReq.Stream {
|
||||
action = "streamGenerateContent?alt=sse"
|
||||
action = "streamGenerateContent"
|
||||
}
|
||||
|
||||
// 重试循环
|
||||
@@ -860,6 +887,9 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
return nil, s.writeGoogleError(c, http.StatusBadRequest, "Request body is empty")
|
||||
}
|
||||
|
||||
// 解析请求以获取 image_size(用于图片计费)
|
||||
imageSize := s.extractImageSize(body)
|
||||
|
||||
switch action {
|
||||
case "generateContent", "streamGenerateContent":
|
||||
// ok
|
||||
@@ -904,14 +934,11 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建上游 action
|
||||
// 构建上游 action(NewAPIRequest 会自动处理 ?alt=sse 和 Accept Header)
|
||||
upstreamAction := action
|
||||
if action == "generateContent" && stream {
|
||||
upstreamAction = "streamGenerateContent"
|
||||
}
|
||||
if stream || upstreamAction == "streamGenerateContent" {
|
||||
upstreamAction += "?alt=sse"
|
||||
}
|
||||
|
||||
// 重试循环
|
||||
var resp *http.Response
|
||||
@@ -1059,6 +1086,13 @@ handleSuccess:
|
||||
usage = &ClaudeUsage{}
|
||||
}
|
||||
|
||||
// 判断是否为图片生成模型
|
||||
imageCount := 0
|
||||
if isImageGenerationModel(mappedModel) {
|
||||
// Gemini 图片生成 API 每次请求只生成一张图片(API 限制)
|
||||
imageCount = 1
|
||||
}
|
||||
|
||||
return &ForwardResult{
|
||||
RequestID: requestID,
|
||||
Usage: *usage,
|
||||
@@ -1066,6 +1100,8 @@ handleSuccess:
|
||||
Stream: stream,
|
||||
Duration: time.Since(startTime),
|
||||
FirstTokenMs: firstTokenMs,
|
||||
ImageCount: imageCount,
|
||||
ImageSize: imageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1572,3 +1608,36 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// extractImageSize 从 Gemini 请求中提取 image_size 参数
|
||||
func (s *AntigravityGatewayService) extractImageSize(body []byte) string {
|
||||
var req antigravity.GeminiRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return "2K" // 默认 2K
|
||||
}
|
||||
|
||||
if req.GenerationConfig != nil && req.GenerationConfig.ImageConfig != nil {
|
||||
size := strings.ToUpper(strings.TrimSpace(req.GenerationConfig.ImageConfig.ImageSize))
|
||||
if size == "1K" || size == "2K" || size == "4K" {
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
return "2K" // 默认 2K
|
||||
}
|
||||
|
||||
// isImageGenerationModel 判断模型是否为图片生成模型
|
||||
// 支持的模型:gemini-3-pro-image, gemini-3-pro-image-preview, gemini-2.5-flash-image 等
|
||||
func isImageGenerationModel(model string) bool {
|
||||
modelLower := strings.ToLower(model)
|
||||
// 移除 models/ 前缀
|
||||
modelLower = strings.TrimPrefix(modelLower, "models/")
|
||||
|
||||
// 精确匹配或前缀匹配
|
||||
return modelLower == "gemini-3-pro-image" ||
|
||||
modelLower == "gemini-3-pro-image-preview" ||
|
||||
strings.HasPrefix(modelLower, "gemini-3-pro-image-") ||
|
||||
modelLower == "gemini-2.5-flash-image" ||
|
||||
modelLower == "gemini-2.5-flash-image-preview" ||
|
||||
strings.HasPrefix(modelLower, "gemini-2.5-flash-image-")
|
||||
}
|
||||
|
||||
123
backend/internal/service/antigravity_image_test.go
Normal file
123
backend/internal/service/antigravity_image_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestIsImageGenerationModel_GeminiProImage 测试 gemini-3-pro-image 识别
|
||||
func TestIsImageGenerationModel_GeminiProImage(t *testing.T) {
|
||||
require.True(t, isImageGenerationModel("gemini-3-pro-image"))
|
||||
require.True(t, isImageGenerationModel("gemini-3-pro-image-preview"))
|
||||
require.True(t, isImageGenerationModel("models/gemini-3-pro-image"))
|
||||
}
|
||||
|
||||
// TestIsImageGenerationModel_GeminiFlashImage 测试 gemini-2.5-flash-image 识别
|
||||
func TestIsImageGenerationModel_GeminiFlashImage(t *testing.T) {
|
||||
require.True(t, isImageGenerationModel("gemini-2.5-flash-image"))
|
||||
require.True(t, isImageGenerationModel("gemini-2.5-flash-image-preview"))
|
||||
}
|
||||
|
||||
// TestIsImageGenerationModel_RegularModel 测试普通模型不被识别为图片模型
|
||||
func TestIsImageGenerationModel_RegularModel(t *testing.T) {
|
||||
require.False(t, isImageGenerationModel("claude-3-opus"))
|
||||
require.False(t, isImageGenerationModel("claude-sonnet-4-20250514"))
|
||||
require.False(t, isImageGenerationModel("gpt-4o"))
|
||||
require.False(t, isImageGenerationModel("gemini-2.5-pro")) // 非图片模型
|
||||
require.False(t, isImageGenerationModel("gemini-2.5-flash"))
|
||||
// 验证不会误匹配包含关键词的自定义模型名
|
||||
require.False(t, isImageGenerationModel("my-gemini-3-pro-image-test"))
|
||||
require.False(t, isImageGenerationModel("custom-gemini-2.5-flash-image-wrapper"))
|
||||
}
|
||||
|
||||
// TestIsImageGenerationModel_CaseInsensitive 测试大小写不敏感
|
||||
func TestIsImageGenerationModel_CaseInsensitive(t *testing.T) {
|
||||
require.True(t, isImageGenerationModel("GEMINI-3-PRO-IMAGE"))
|
||||
require.True(t, isImageGenerationModel("Gemini-3-Pro-Image"))
|
||||
require.True(t, isImageGenerationModel("GEMINI-2.5-FLASH-IMAGE"))
|
||||
}
|
||||
|
||||
// TestExtractImageSize_ValidSizes 测试有效尺寸解析
|
||||
func TestExtractImageSize_ValidSizes(t *testing.T) {
|
||||
svc := &AntigravityGatewayService{}
|
||||
|
||||
// 1K
|
||||
body := []byte(`{"generationConfig":{"imageConfig":{"imageSize":"1K"}}}`)
|
||||
require.Equal(t, "1K", svc.extractImageSize(body))
|
||||
|
||||
// 2K
|
||||
body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"2K"}}}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
|
||||
// 4K
|
||||
body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"4K"}}}`)
|
||||
require.Equal(t, "4K", svc.extractImageSize(body))
|
||||
}
|
||||
|
||||
// TestExtractImageSize_CaseInsensitive 测试大小写不敏感
|
||||
func TestExtractImageSize_CaseInsensitive(t *testing.T) {
|
||||
svc := &AntigravityGatewayService{}
|
||||
|
||||
body := []byte(`{"generationConfig":{"imageConfig":{"imageSize":"1k"}}}`)
|
||||
require.Equal(t, "1K", svc.extractImageSize(body))
|
||||
|
||||
body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"4k"}}}`)
|
||||
require.Equal(t, "4K", svc.extractImageSize(body))
|
||||
}
|
||||
|
||||
// TestExtractImageSize_Default 测试无 imageConfig 返回默认 2K
|
||||
func TestExtractImageSize_Default(t *testing.T) {
|
||||
svc := &AntigravityGatewayService{}
|
||||
|
||||
// 无 generationConfig
|
||||
body := []byte(`{"contents":[]}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
|
||||
// 有 generationConfig 但无 imageConfig
|
||||
body = []byte(`{"generationConfig":{"temperature":0.7}}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
|
||||
// 有 imageConfig 但无 imageSize
|
||||
body = []byte(`{"generationConfig":{"imageConfig":{}}}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
}
|
||||
|
||||
// TestExtractImageSize_InvalidJSON 测试非法 JSON 返回默认 2K
|
||||
func TestExtractImageSize_InvalidJSON(t *testing.T) {
|
||||
svc := &AntigravityGatewayService{}
|
||||
|
||||
body := []byte(`not valid json`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
|
||||
body = []byte(`{"broken":`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
}
|
||||
|
||||
// TestExtractImageSize_EmptySize 测试空 imageSize 返回默认 2K
|
||||
func TestExtractImageSize_EmptySize(t *testing.T) {
|
||||
svc := &AntigravityGatewayService{}
|
||||
|
||||
body := []byte(`{"generationConfig":{"imageConfig":{"imageSize":""}}}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
|
||||
// 空格
|
||||
body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":" "}}}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
}
|
||||
|
||||
// TestExtractImageSize_InvalidSize 测试无效尺寸返回默认 2K
|
||||
func TestExtractImageSize_InvalidSize(t *testing.T) {
|
||||
svc := &AntigravityGatewayService{}
|
||||
|
||||
body := []byte(`{"generationConfig":{"imageConfig":{"imageSize":"3K"}}}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
|
||||
body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"8K"}}}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
|
||||
body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"invalid"}}}`)
|
||||
require.Equal(t, "2K", svc.extractImageSize(body))
|
||||
}
|
||||
@@ -20,12 +20,16 @@ var (
|
||||
ErrEmailExists = infraerrors.Conflict("EMAIL_EXISTS", "email already exists")
|
||||
ErrInvalidToken = infraerrors.Unauthorized("INVALID_TOKEN", "invalid token")
|
||||
ErrTokenExpired = infraerrors.Unauthorized("TOKEN_EXPIRED", "token has expired")
|
||||
ErrTokenTooLarge = infraerrors.BadRequest("TOKEN_TOO_LARGE", "token too large")
|
||||
ErrTokenRevoked = infraerrors.Unauthorized("TOKEN_REVOKED", "token has been revoked")
|
||||
ErrEmailVerifyRequired = infraerrors.BadRequest("EMAIL_VERIFY_REQUIRED", "email verification is required")
|
||||
ErrRegDisabled = infraerrors.Forbidden("REGISTRATION_DISABLED", "registration is currently disabled")
|
||||
ErrServiceUnavailable = infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable")
|
||||
)
|
||||
|
||||
// maxTokenLength 限制 token 大小,避免超长 header 触发解析时的异常内存分配。
|
||||
const maxTokenLength = 8192
|
||||
|
||||
// JWTClaims JWT载荷数据
|
||||
type JWTClaims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
@@ -309,7 +313,20 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string
|
||||
|
||||
// ValidateToken 验证JWT token并返回用户声明
|
||||
func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
|
||||
// 先做长度校验,尽早拒绝异常超长 token,降低 DoS 风险。
|
||||
if len(tokenString) > maxTokenLength {
|
||||
return nil, ErrTokenTooLarge
|
||||
}
|
||||
|
||||
// 使用解析器并限制可接受的签名算法,防止算法混淆。
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{
|
||||
jwt.SigningMethodHS256.Name,
|
||||
jwt.SigningMethodHS384.Name,
|
||||
jwt.SigningMethodHS512.Name,
|
||||
}))
|
||||
|
||||
// 保留默认 claims 校验(exp/nbf),避免放行过期或未生效的 token。
|
||||
token, err := parser.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
|
||||
// 验证签名方法
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
|
||||
@@ -295,3 +295,88 @@ func (s *BillingService) ForceUpdatePricing() error {
|
||||
}
|
||||
return fmt.Errorf("pricing service not initialized")
|
||||
}
|
||||
|
||||
// ImagePriceConfig 图片计费配置
|
||||
type ImagePriceConfig struct {
|
||||
Price1K *float64 // 1K 尺寸价格(nil 表示使用默认值)
|
||||
Price2K *float64 // 2K 尺寸价格(nil 表示使用默认值)
|
||||
Price4K *float64 // 4K 尺寸价格(nil 表示使用默认值)
|
||||
}
|
||||
|
||||
// CalculateImageCost 计算图片生成费用
|
||||
// model: 请求的模型名称(用于获取 LiteLLM 默认价格)
|
||||
// imageSize: 图片尺寸 "1K", "2K", "4K"
|
||||
// imageCount: 生成的图片数量
|
||||
// groupConfig: 分组配置的价格(可能为 nil,表示使用默认值)
|
||||
// rateMultiplier: 费率倍数
|
||||
func (s *BillingService) CalculateImageCost(model string, imageSize string, imageCount int, groupConfig *ImagePriceConfig, rateMultiplier float64) *CostBreakdown {
|
||||
if imageCount <= 0 {
|
||||
return &CostBreakdown{}
|
||||
}
|
||||
|
||||
// 获取单价
|
||||
unitPrice := s.getImageUnitPrice(model, imageSize, groupConfig)
|
||||
|
||||
// 计算总费用
|
||||
totalCost := unitPrice * float64(imageCount)
|
||||
|
||||
// 应用倍率
|
||||
if rateMultiplier <= 0 {
|
||||
rateMultiplier = 1.0
|
||||
}
|
||||
actualCost := totalCost * rateMultiplier
|
||||
|
||||
return &CostBreakdown{
|
||||
TotalCost: totalCost,
|
||||
ActualCost: actualCost,
|
||||
}
|
||||
}
|
||||
|
||||
// getImageUnitPrice 获取图片单价
|
||||
func (s *BillingService) getImageUnitPrice(model string, imageSize string, groupConfig *ImagePriceConfig) float64 {
|
||||
// 优先使用分组配置的价格
|
||||
if groupConfig != nil {
|
||||
switch imageSize {
|
||||
case "1K":
|
||||
if groupConfig.Price1K != nil {
|
||||
return *groupConfig.Price1K
|
||||
}
|
||||
case "2K":
|
||||
if groupConfig.Price2K != nil {
|
||||
return *groupConfig.Price2K
|
||||
}
|
||||
case "4K":
|
||||
if groupConfig.Price4K != nil {
|
||||
return *groupConfig.Price4K
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到 LiteLLM 默认价格
|
||||
return s.getDefaultImagePrice(model, imageSize)
|
||||
}
|
||||
|
||||
// getDefaultImagePrice 获取 LiteLLM 默认图片价格
|
||||
func (s *BillingService) getDefaultImagePrice(model string, imageSize string) float64 {
|
||||
basePrice := 0.0
|
||||
|
||||
// 从 PricingService 获取 output_cost_per_image
|
||||
if s.pricingService != nil {
|
||||
pricing := s.pricingService.GetModelPricing(model)
|
||||
if pricing != nil && pricing.OutputCostPerImage > 0 {
|
||||
basePrice = pricing.OutputCostPerImage
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到价格,使用硬编码默认值($0.134,来自 gemini-3-pro-image-preview)
|
||||
if basePrice <= 0 {
|
||||
basePrice = 0.134
|
||||
}
|
||||
|
||||
// 4K 尺寸翻倍
|
||||
if imageSize == "4K" {
|
||||
return basePrice * 2
|
||||
}
|
||||
|
||||
return basePrice
|
||||
}
|
||||
|
||||
149
backend/internal/service/billing_service_image_test.go
Normal file
149
backend/internal/service/billing_service_image_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestCalculateImageCost_DefaultPricing 测试无分组配置时使用默认价格
|
||||
func TestCalculateImageCost_DefaultPricing(t *testing.T) {
|
||||
svc := &BillingService{} // pricingService 为 nil,使用硬编码默认值
|
||||
|
||||
// 2K 尺寸,默认价格 $0.134
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, nil, 1.0)
|
||||
require.InDelta(t, 0.134, cost.TotalCost, 0.0001)
|
||||
require.InDelta(t, 0.134, cost.ActualCost, 0.0001)
|
||||
|
||||
// 多张图片
|
||||
cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 3, nil, 1.0)
|
||||
require.InDelta(t, 0.402, cost.TotalCost, 0.0001)
|
||||
}
|
||||
|
||||
// TestCalculateImageCost_GroupCustomPricing 测试分组自定义价格
|
||||
func TestCalculateImageCost_GroupCustomPricing(t *testing.T) {
|
||||
svc := &BillingService{}
|
||||
|
||||
price1K := 0.10
|
||||
price2K := 0.15
|
||||
price4K := 0.30
|
||||
groupConfig := &ImagePriceConfig{
|
||||
Price1K: &price1K,
|
||||
Price2K: &price2K,
|
||||
Price4K: &price4K,
|
||||
}
|
||||
|
||||
// 1K 使用分组价格
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "1K", 2, groupConfig, 1.0)
|
||||
require.InDelta(t, 0.20, cost.TotalCost, 0.0001)
|
||||
|
||||
// 2K 使用分组价格
|
||||
cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, groupConfig, 1.0)
|
||||
require.InDelta(t, 0.15, cost.TotalCost, 0.0001)
|
||||
|
||||
// 4K 使用分组价格
|
||||
cost = svc.CalculateImageCost("gemini-3-pro-image", "4K", 1, groupConfig, 1.0)
|
||||
require.InDelta(t, 0.30, cost.TotalCost, 0.0001)
|
||||
}
|
||||
|
||||
// TestCalculateImageCost_4KDoublePrice 测试 4K 默认价格翻倍
|
||||
func TestCalculateImageCost_4KDoublePrice(t *testing.T) {
|
||||
svc := &BillingService{}
|
||||
|
||||
// 4K 尺寸,默认价格翻倍 $0.134 * 2 = $0.268
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "4K", 1, nil, 1.0)
|
||||
require.InDelta(t, 0.268, cost.TotalCost, 0.0001)
|
||||
}
|
||||
|
||||
// TestCalculateImageCost_RateMultiplier 测试费率倍数
|
||||
func TestCalculateImageCost_RateMultiplier(t *testing.T) {
|
||||
svc := &BillingService{}
|
||||
|
||||
// 费率倍数 1.5x
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, nil, 1.5)
|
||||
require.InDelta(t, 0.134, cost.TotalCost, 0.0001) // TotalCost 不变
|
||||
require.InDelta(t, 0.201, cost.ActualCost, 0.0001) // ActualCost = 0.134 * 1.5
|
||||
|
||||
// 费率倍数 2.0x
|
||||
cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 2, nil, 2.0)
|
||||
require.InDelta(t, 0.268, cost.TotalCost, 0.0001)
|
||||
require.InDelta(t, 0.536, cost.ActualCost, 0.0001)
|
||||
}
|
||||
|
||||
// TestCalculateImageCost_ZeroCount 测试 imageCount=0
|
||||
func TestCalculateImageCost_ZeroCount(t *testing.T) {
|
||||
svc := &BillingService{}
|
||||
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 0, nil, 1.0)
|
||||
require.Equal(t, 0.0, cost.TotalCost)
|
||||
require.Equal(t, 0.0, cost.ActualCost)
|
||||
}
|
||||
|
||||
// TestCalculateImageCost_NegativeCount 测试 imageCount=-1
|
||||
func TestCalculateImageCost_NegativeCount(t *testing.T) {
|
||||
svc := &BillingService{}
|
||||
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", -1, nil, 1.0)
|
||||
require.Equal(t, 0.0, cost.TotalCost)
|
||||
require.Equal(t, 0.0, cost.ActualCost)
|
||||
}
|
||||
|
||||
// TestCalculateImageCost_ZeroRateMultiplier 测试费率倍数为 0 时默认使用 1.0
|
||||
func TestCalculateImageCost_ZeroRateMultiplier(t *testing.T) {
|
||||
svc := &BillingService{}
|
||||
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, nil, 0)
|
||||
require.InDelta(t, 0.134, cost.TotalCost, 0.0001)
|
||||
require.InDelta(t, 0.134, cost.ActualCost, 0.0001) // 0 倍率当作 1.0 处理
|
||||
}
|
||||
|
||||
// TestGetImageUnitPrice_GroupPriorityOverDefault 测试分组价格优先于默认价格
|
||||
func TestGetImageUnitPrice_GroupPriorityOverDefault(t *testing.T) {
|
||||
svc := &BillingService{}
|
||||
|
||||
price2K := 0.20
|
||||
groupConfig := &ImagePriceConfig{
|
||||
Price2K: &price2K,
|
||||
}
|
||||
|
||||
// 分组配置了 2K 价格,应该使用分组价格而不是默认的 $0.134
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, groupConfig, 1.0)
|
||||
require.InDelta(t, 0.20, cost.TotalCost, 0.0001)
|
||||
}
|
||||
|
||||
// TestGetImageUnitPrice_PartialGroupConfig 测试分组部分配置时回退默认
|
||||
func TestGetImageUnitPrice_PartialGroupConfig(t *testing.T) {
|
||||
svc := &BillingService{}
|
||||
|
||||
// 只配置 1K 价格
|
||||
price1K := 0.10
|
||||
groupConfig := &ImagePriceConfig{
|
||||
Price1K: &price1K,
|
||||
}
|
||||
|
||||
// 1K 使用分组价格
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "1K", 1, groupConfig, 1.0)
|
||||
require.InDelta(t, 0.10, cost.TotalCost, 0.0001)
|
||||
|
||||
// 2K 回退默认价格 $0.134
|
||||
cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, groupConfig, 1.0)
|
||||
require.InDelta(t, 0.134, cost.TotalCost, 0.0001)
|
||||
|
||||
// 4K 回退默认价格 $0.268 (翻倍)
|
||||
cost = svc.CalculateImageCost("gemini-3-pro-image", "4K", 1, groupConfig, 1.0)
|
||||
require.InDelta(t, 0.268, cost.TotalCost, 0.0001)
|
||||
}
|
||||
|
||||
// TestGetDefaultImagePrice_FallbackHardcoded 测试 PricingService 无数据时使用硬编码默认值
|
||||
func TestGetDefaultImagePrice_FallbackHardcoded(t *testing.T) {
|
||||
svc := &BillingService{} // pricingService 为 nil
|
||||
|
||||
// 1K 和 2K 使用相同的默认价格 $0.134
|
||||
cost := svc.CalculateImageCost("gemini-3-pro-image", "1K", 1, nil, 1.0)
|
||||
require.InDelta(t, 0.134, cost.TotalCost, 0.0001)
|
||||
|
||||
cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, nil, 1.0)
|
||||
require.InDelta(t, 0.134, cost.TotalCost, 0.0001)
|
||||
}
|
||||
@@ -140,6 +140,8 @@ func (s *EmailService) SendEmailWithConfig(config *SMTPConfig, to, subject, body
|
||||
func (s *EmailService) sendMailTLS(addr string, auth smtp.Auth, from, to string, msg []byte, host string) error {
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: host,
|
||||
// 强制 TLS 1.2+,避免协议降级导致的弱加密风险。
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
@@ -311,7 +313,11 @@ func (s *EmailService) TestSMTPConnectionWithConfig(config *SMTPConfig) error {
|
||||
addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
|
||||
|
||||
if config.UseTLS {
|
||||
tlsConfig := &tls.Config{ServerName: config.Host}
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: config.Host,
|
||||
// 与发送逻辑一致,显式要求 TLS 1.2+。
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tls connection failed: %w", err)
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ const (
|
||||
stickySessionTTL = time.Hour // 粘性会话TTL
|
||||
defaultMaxLineSize = 10 * 1024 * 1024
|
||||
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.
|
||||
@@ -43,6 +44,16 @@ var (
|
||||
sseDataRe = regexp.MustCompile(`^data:\s*`)
|
||||
sessionIDRegex = regexp.MustCompile(`session_([a-f0-9-]{36})`)
|
||||
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项目)
|
||||
@@ -98,12 +109,17 @@ type ClaudeUsage struct {
|
||||
|
||||
// ForwardResult 转发结果
|
||||
type ForwardResult struct {
|
||||
RequestID string
|
||||
Usage ClaudeUsage
|
||||
Model string
|
||||
Stream bool
|
||||
Duration time.Duration
|
||||
FirstTokenMs *int // 首字时间(流式请求)
|
||||
RequestID string
|
||||
Usage ClaudeUsage
|
||||
Model string
|
||||
Stream bool
|
||||
Duration time.Duration
|
||||
FirstTokenMs *int // 首字时间(流式请求)
|
||||
ClientDisconnect bool // 客户端是否在流式传输过程中断开
|
||||
|
||||
// 图片生成计费字段(仅 gemini-3-pro-image 使用)
|
||||
ImageCount int // 生成的图片数量
|
||||
ImageSize string // 图片尺寸 "1K", "2K", "4K"
|
||||
}
|
||||
|
||||
// UpstreamFailoverError indicates an upstream error that should trigger account failover.
|
||||
@@ -351,17 +367,8 @@ func (s *GatewayService) SelectAccountForModelWithExclusions(ctx context.Context
|
||||
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 分组、强制平台模式或无分组使用单平台选择
|
||||
// 注意:强制平台模式也必须遵守分组限制,不再回退到全平台查询
|
||||
return s.selectAccountForModelWithPlatform(ctx, groupID, sessionHash, requestedModel, excludedIDs, platform)
|
||||
}
|
||||
|
||||
@@ -439,7 +446,8 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
||||
accountID, err := s.cache.GetSessionAccountID(ctx, sessionHash)
|
||||
if err == nil && accountID > 0 && !isExcluded(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() &&
|
||||
(requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
||||
result, err := s.tryAcquireAccountSlot(ctx, accountID, account.Concurrency)
|
||||
@@ -656,9 +664,7 @@ func (s *GatewayService) listSchedulableAccounts(ctx context.Context, groupID *i
|
||||
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
||||
} else if groupID != nil {
|
||||
accounts, err = s.accountRepo.ListSchedulableByGroupIDAndPlatform(ctx, *groupID, platform)
|
||||
if err == nil && len(accounts) == 0 && hasForcePlatform {
|
||||
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
||||
}
|
||||
// 分组内无账号则返回空列表,由上层处理错误,不再回退到全平台查询
|
||||
} else {
|
||||
accounts, err = s.accountRepo.ListSchedulableByPlatform(ctx, platform)
|
||||
}
|
||||
@@ -681,6 +687,23 @@ func (s *GatewayService) isAccountAllowedForPlatform(account *Account, 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) {
|
||||
if s.concurrencyService == nil {
|
||||
return &AcquireResult{Acquired: true, ReleaseFunc: func() {}}, nil
|
||||
@@ -719,8 +742,8 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
|
||||
if err == nil && accountID > 0 {
|
||||
if _, excluded := excludedIDs[accountID]; !excluded {
|
||||
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 {
|
||||
log.Printf("refresh session ttl failed: session=%s err=%v", sessionHash, err)
|
||||
}
|
||||
@@ -808,8 +831,8 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
|
||||
if err == nil && accountID > 0 {
|
||||
if _, excluded := excludedIDs[accountID]; !excluded {
|
||||
account, err := s.accountRepo.GetByID(ctx, accountID)
|
||||
// 检查账号是否有效:原生平台直接匹配,antigravity 需要启用混合调度
|
||||
if err == nil && account.IsSchedulable() && (requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
||||
// 检查账号分组归属和有效性:原生平台直接匹配,antigravity 需要启用混合调度
|
||||
if err == nil && s.isAccountInGroup(account, groupID) && account.IsSchedulable() && (requestedModel == "" || s.isModelSupportedByAccount(account, requestedModel)) {
|
||||
if account.Platform == nativePlatform || (account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled()) {
|
||||
if err := s.cache.RefreshSessionTTL(ctx, sessionHash, stickySessionTTL); err != nil {
|
||||
log.Printf("refresh session ttl failed: session=%s err=%v", sessionHash, err)
|
||||
@@ -1009,15 +1032,15 @@ func isClaudeCodeClient(userAgent string, metadataUserID string) bool {
|
||||
}
|
||||
|
||||
// systemIncludesClaudeCodePrompt 检查 system 中是否已包含 Claude Code 提示词
|
||||
// 支持 string 和 []any 两种格式
|
||||
// 使用前缀匹配支持多种变体(标准版、Agent SDK 版等)
|
||||
func systemIncludesClaudeCodePrompt(system any) bool {
|
||||
switch v := system.(type) {
|
||||
case string:
|
||||
return v == claudeCodeSystemPrompt
|
||||
return hasClaudeCodePrefix(v)
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1026,6 +1049,16 @@ func systemIncludesClaudeCodePrompt(system any) bool {
|
||||
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 提示词
|
||||
// 处理 null、字符串、数组三种格式
|
||||
func injectClaudeCodePrompt(body []byte, system any) []byte {
|
||||
@@ -1069,6 +1102,124 @@ func injectClaudeCodePrompt(body []byte, system any) []byte {
|
||||
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
|
||||
func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, parsed *ParsedRequest) (*ForwardResult, error) {
|
||||
startTime := time.Now()
|
||||
@@ -1089,6 +1240,9 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
body = injectClaudeCodePrompt(body, parsed.System)
|
||||
}
|
||||
|
||||
// 强制执行 cache_control 块数量限制(最多 4 个)
|
||||
body = enforceCacheControlLimit(body)
|
||||
|
||||
// 应用模型映射(仅对apikey类型账号)
|
||||
originalModel := reqModel
|
||||
if account.Type == AccountTypeAPIKey {
|
||||
@@ -1312,6 +1466,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
// 处理正常响应
|
||||
var usage *ClaudeUsage
|
||||
var firstTokenMs *int
|
||||
var clientDisconnect bool
|
||||
if reqStream {
|
||||
streamResult, err := s.handleStreamingResponse(ctx, resp, c, account, startTime, originalModel, reqModel)
|
||||
if err != nil {
|
||||
@@ -1324,6 +1479,7 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
}
|
||||
usage = streamResult.usage
|
||||
firstTokenMs = streamResult.firstTokenMs
|
||||
clientDisconnect = streamResult.clientDisconnect
|
||||
} else {
|
||||
usage, err = s.handleNonStreamingResponse(ctx, resp, c, account, originalModel, reqModel)
|
||||
if err != nil {
|
||||
@@ -1332,12 +1488,13 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *A
|
||||
}
|
||||
|
||||
return &ForwardResult{
|
||||
RequestID: resp.Header.Get("x-request-id"),
|
||||
Usage: *usage,
|
||||
Model: originalModel, // 使用原始模型用于计费和日志
|
||||
Stream: reqStream,
|
||||
Duration: time.Since(startTime),
|
||||
FirstTokenMs: firstTokenMs,
|
||||
RequestID: resp.Header.Get("x-request-id"),
|
||||
Usage: *usage,
|
||||
Model: originalModel, // 使用原始模型用于计费和日志
|
||||
Stream: reqStream,
|
||||
Duration: time.Since(startTime),
|
||||
FirstTokenMs: firstTokenMs,
|
||||
ClientDisconnect: clientDisconnect,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1692,8 +1849,9 @@ func (s *GatewayService) handleRetryExhaustedError(ctx context.Context, resp *ht
|
||||
|
||||
// streamingResult 流式响应结果
|
||||
type streamingResult struct {
|
||||
usage *ClaudeUsage
|
||||
firstTokenMs *int
|
||||
usage *ClaudeUsage
|
||||
firstTokenMs *int
|
||||
clientDisconnect bool // 客户端是否在流式传输过程中断开
|
||||
}
|
||||
|
||||
func (s *GatewayService) handleStreamingResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *Account, startTime time.Time, originalModel, mappedModel string) (*streamingResult, error) {
|
||||
@@ -1789,14 +1947,27 @@ func (s *GatewayService) handleStreamingResponse(ctx context.Context, resp *http
|
||||
}
|
||||
|
||||
needModelReplace := originalModel != mappedModel
|
||||
clientDisconnected := false // 客户端断开标志,断开后继续读取上游以获取完整usage
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-events:
|
||||
if !ok {
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, nil
|
||||
// 上游完成,返回结果
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: clientDisconnected}, nil
|
||||
}
|
||||
if ev.err != nil {
|
||||
// 检测 context 取消(客户端断开会导致 context 取消,进而影响上游读取)
|
||||
if errors.Is(ev.err, context.Canceled) || errors.Is(ev.err, context.DeadlineExceeded) {
|
||||
log.Printf("Context canceled during streaming, returning collected usage")
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, nil
|
||||
}
|
||||
// 客户端已通过写入失败检测到断开,上游也出错了,返回已收集的 usage
|
||||
if clientDisconnected {
|
||||
log.Printf("Upstream read error after client disconnect: %v, returning collected usage", ev.err)
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, nil
|
||||
}
|
||||
// 客户端未断开,正常的错误处理
|
||||
if errors.Is(ev.err, bufio.ErrTooLong) {
|
||||
log.Printf("SSE line too long: account=%d max_size=%d error=%v", account.ID, maxLineSize, ev.err)
|
||||
sendErrorEvent("response_too_large")
|
||||
@@ -1807,38 +1978,40 @@ func (s *GatewayService) handleStreamingResponse(ctx context.Context, resp *http
|
||||
}
|
||||
line := ev.line
|
||||
if line == "event: error" {
|
||||
// 上游返回错误事件,如果客户端已断开仍返回已收集的 usage
|
||||
if clientDisconnected {
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, nil
|
||||
}
|
||||
return nil, errors.New("have error in stream")
|
||||
}
|
||||
|
||||
// Extract data from SSE line (supports both "data: " and "data:" formats)
|
||||
var data string
|
||||
if sseDataRe.MatchString(line) {
|
||||
data := sseDataRe.ReplaceAllString(line, "")
|
||||
|
||||
data = sseDataRe.ReplaceAllString(line, "")
|
||||
// 如果有模型映射,替换响应中的model字段
|
||||
if needModelReplace {
|
||||
line = s.replaceModelInSSELine(line, mappedModel, originalModel)
|
||||
}
|
||||
}
|
||||
|
||||
// 转发行
|
||||
// 写入客户端(统一处理 data 行和非 data 行)
|
||||
if !clientDisconnected {
|
||||
if _, err := fmt.Fprintf(w, "%s\n", line); err != nil {
|
||||
sendErrorEvent("write_failed")
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, err
|
||||
clientDisconnected = true
|
||||
log.Printf("Client disconnected during streaming, continuing to drain upstream for billing")
|
||||
} else {
|
||||
flusher.Flush()
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
// 记录首字时间:第一个有效的 content_block_delta 或 message_start
|
||||
if firstTokenMs == nil && data != "" && data != "[DONE]" {
|
||||
// 无论客户端是否断开,都解析 usage(仅对 data 行)
|
||||
if data != "" {
|
||||
if firstTokenMs == nil && data != "[DONE]" {
|
||||
ms := int(time.Since(startTime).Milliseconds())
|
||||
firstTokenMs = &ms
|
||||
}
|
||||
s.parseSSEUsage(data, usage)
|
||||
} else {
|
||||
// 非 data 行直接转发
|
||||
if _, err := fmt.Fprintf(w, "%s\n", line); err != nil {
|
||||
sendErrorEvent("write_failed")
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, err
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
case <-intervalCh:
|
||||
@@ -1846,6 +2019,11 @@ func (s *GatewayService) handleStreamingResponse(ctx context.Context, resp *http
|
||||
if time.Since(lastRead) < streamInterval {
|
||||
continue
|
||||
}
|
||||
if clientDisconnected {
|
||||
// 客户端已断开,上游也超时了,返回已收集的 usage
|
||||
log.Printf("Upstream timeout after client disconnect, returning collected usage")
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs, clientDisconnect: true}, nil
|
||||
}
|
||||
log.Printf("Stream data interval timeout: account=%d model=%s interval=%s", account.ID, originalModel, streamInterval)
|
||||
sendErrorEvent("stream_timeout")
|
||||
return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream data interval timeout")
|
||||
@@ -1999,6 +2177,7 @@ type RecordUsageInput struct {
|
||||
User *User
|
||||
Account *Account
|
||||
Subscription *UserSubscription // 可选:订阅信息
|
||||
UserAgent string // 请求的 User-Agent
|
||||
}
|
||||
|
||||
// RecordUsage 记录使用量并扣费(或更新订阅用量)
|
||||
@@ -2009,25 +2188,40 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
|
||||
account := input.Account
|
||||
subscription := input.Subscription
|
||||
|
||||
// 计算费用
|
||||
tokens := UsageTokens{
|
||||
InputTokens: result.Usage.InputTokens,
|
||||
OutputTokens: result.Usage.OutputTokens,
|
||||
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
|
||||
CacheReadTokens: result.Usage.CacheReadInputTokens,
|
||||
}
|
||||
|
||||
// 获取费率倍数
|
||||
multiplier := s.cfg.Default.RateMultiplier
|
||||
if apiKey.GroupID != nil && apiKey.Group != nil {
|
||||
multiplier = apiKey.Group.RateMultiplier
|
||||
}
|
||||
|
||||
cost, err := s.billingService.CalculateCost(result.Model, tokens, multiplier)
|
||||
if err != nil {
|
||||
log.Printf("Calculate cost failed: %v", err)
|
||||
// 使用默认费用继续
|
||||
cost = &CostBreakdown{ActualCost: 0}
|
||||
var cost *CostBreakdown
|
||||
|
||||
// 根据请求类型选择计费方式
|
||||
if result.ImageCount > 0 {
|
||||
// 图片生成计费
|
||||
var groupConfig *ImagePriceConfig
|
||||
if apiKey.Group != nil {
|
||||
groupConfig = &ImagePriceConfig{
|
||||
Price1K: apiKey.Group.ImagePrice1K,
|
||||
Price2K: apiKey.Group.ImagePrice2K,
|
||||
Price4K: apiKey.Group.ImagePrice4K,
|
||||
}
|
||||
}
|
||||
cost = s.billingService.CalculateImageCost(result.Model, result.ImageSize, result.ImageCount, groupConfig, multiplier)
|
||||
} else {
|
||||
// Token 计费
|
||||
tokens := UsageTokens{
|
||||
InputTokens: result.Usage.InputTokens,
|
||||
OutputTokens: result.Usage.OutputTokens,
|
||||
CacheCreationTokens: result.Usage.CacheCreationInputTokens,
|
||||
CacheReadTokens: result.Usage.CacheReadInputTokens,
|
||||
}
|
||||
var err error
|
||||
cost, err = s.billingService.CalculateCost(result.Model, tokens, multiplier)
|
||||
if err != nil {
|
||||
log.Printf("Calculate cost failed: %v", err)
|
||||
cost = &CostBreakdown{ActualCost: 0}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断计费方式:订阅模式 vs 余额模式
|
||||
@@ -2039,6 +2233,10 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
|
||||
|
||||
// 创建使用日志
|
||||
durationMs := int(result.Duration.Milliseconds())
|
||||
var imageSize *string
|
||||
if result.ImageSize != "" {
|
||||
imageSize = &result.ImageSize
|
||||
}
|
||||
usageLog := &UsageLog{
|
||||
UserID: user.ID,
|
||||
APIKeyID: apiKey.ID,
|
||||
@@ -2060,9 +2258,16 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
|
||||
Stream: result.Stream,
|
||||
DurationMs: &durationMs,
|
||||
FirstTokenMs: result.FirstTokenMs,
|
||||
ImageCount: result.ImageCount,
|
||||
ImageSize: imageSize,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 添加 UserAgent
|
||||
if input.UserAgent != "" {
|
||||
usageLog.UserAgent = &input.UserAgent
|
||||
}
|
||||
|
||||
// 添加分组和订阅关联
|
||||
if apiKey.GroupID != nil {
|
||||
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 {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ type Group struct {
|
||||
MonthlyLimitUSD *float64
|
||||
DefaultValidityDays int
|
||||
|
||||
// 图片生成计费配置(antigravity 和 gemini 平台使用)
|
||||
ImagePrice1K *float64
|
||||
ImagePrice2K *float64
|
||||
ImagePrice4K *float64
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
@@ -47,3 +52,19 @@ func (g *Group) HasWeeklyLimit() bool {
|
||||
func (g *Group) HasMonthlyLimit() bool {
|
||||
return g.MonthlyLimitUSD != nil && *g.MonthlyLimitUSD > 0
|
||||
}
|
||||
|
||||
// GetImagePrice 根据 image_size 返回对应的图片生成价格
|
||||
// 如果分组未配置价格,返回 nil(调用方应使用默认值)
|
||||
func (g *Group) GetImagePrice(imageSize string) *float64 {
|
||||
switch imageSize {
|
||||
case "1K":
|
||||
return g.ImagePrice1K
|
||||
case "2K":
|
||||
return g.ImagePrice2K
|
||||
case "4K":
|
||||
return g.ImagePrice4K
|
||||
default:
|
||||
// 未知尺寸默认按 2K 计费
|
||||
return g.ImagePrice2K
|
||||
}
|
||||
}
|
||||
|
||||
92
backend/internal/service/group_test.go
Normal file
92
backend/internal/service/group_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestGroup_GetImagePrice_1K 测试 1K 尺寸返回正确价格
|
||||
func TestGroup_GetImagePrice_1K(t *testing.T) {
|
||||
price := 0.10
|
||||
group := &Group{
|
||||
ImagePrice1K: &price,
|
||||
}
|
||||
|
||||
result := group.GetImagePrice("1K")
|
||||
require.NotNil(t, result)
|
||||
require.InDelta(t, 0.10, *result, 0.0001)
|
||||
}
|
||||
|
||||
// TestGroup_GetImagePrice_2K 测试 2K 尺寸返回正确价格
|
||||
func TestGroup_GetImagePrice_2K(t *testing.T) {
|
||||
price := 0.15
|
||||
group := &Group{
|
||||
ImagePrice2K: &price,
|
||||
}
|
||||
|
||||
result := group.GetImagePrice("2K")
|
||||
require.NotNil(t, result)
|
||||
require.InDelta(t, 0.15, *result, 0.0001)
|
||||
}
|
||||
|
||||
// TestGroup_GetImagePrice_4K 测试 4K 尺寸返回正确价格
|
||||
func TestGroup_GetImagePrice_4K(t *testing.T) {
|
||||
price := 0.30
|
||||
group := &Group{
|
||||
ImagePrice4K: &price,
|
||||
}
|
||||
|
||||
result := group.GetImagePrice("4K")
|
||||
require.NotNil(t, result)
|
||||
require.InDelta(t, 0.30, *result, 0.0001)
|
||||
}
|
||||
|
||||
// TestGroup_GetImagePrice_UnknownSize 测试未知尺寸回退 2K
|
||||
func TestGroup_GetImagePrice_UnknownSize(t *testing.T) {
|
||||
price2K := 0.15
|
||||
group := &Group{
|
||||
ImagePrice2K: &price2K,
|
||||
}
|
||||
|
||||
// 未知尺寸 "3K" 应该回退到 2K
|
||||
result := group.GetImagePrice("3K")
|
||||
require.NotNil(t, result)
|
||||
require.InDelta(t, 0.15, *result, 0.0001)
|
||||
|
||||
// 空字符串也回退到 2K
|
||||
result = group.GetImagePrice("")
|
||||
require.NotNil(t, result)
|
||||
require.InDelta(t, 0.15, *result, 0.0001)
|
||||
}
|
||||
|
||||
// TestGroup_GetImagePrice_NilValues 测试未配置时返回 nil
|
||||
func TestGroup_GetImagePrice_NilValues(t *testing.T) {
|
||||
group := &Group{
|
||||
// 所有 ImagePrice 字段都是 nil
|
||||
}
|
||||
|
||||
require.Nil(t, group.GetImagePrice("1K"))
|
||||
require.Nil(t, group.GetImagePrice("2K"))
|
||||
require.Nil(t, group.GetImagePrice("4K"))
|
||||
require.Nil(t, group.GetImagePrice("unknown"))
|
||||
}
|
||||
|
||||
// TestGroup_GetImagePrice_PartialConfig 测试部分配置
|
||||
func TestGroup_GetImagePrice_PartialConfig(t *testing.T) {
|
||||
price1K := 0.10
|
||||
group := &Group{
|
||||
ImagePrice1K: &price1K,
|
||||
// ImagePrice2K 和 ImagePrice4K 未配置
|
||||
}
|
||||
|
||||
result := group.GetImagePrice("1K")
|
||||
require.NotNil(t, result)
|
||||
require.InDelta(t, 0.10, *result, 0.0001)
|
||||
|
||||
// 2K 和 4K 返回 nil
|
||||
require.Nil(t, group.GetImagePrice("2K"))
|
||||
require.Nil(t, group.GetImagePrice("4K"))
|
||||
}
|
||||
@@ -1092,6 +1092,7 @@ type OpenAIRecordUsageInput struct {
|
||||
User *User
|
||||
Account *Account
|
||||
Subscription *UserSubscription
|
||||
UserAgent string // 请求的 User-Agent
|
||||
}
|
||||
|
||||
// RecordUsage records usage and deducts balance
|
||||
@@ -1161,6 +1162,11 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 添加 UserAgent
|
||||
if input.UserAgent != "" {
|
||||
usageLog.UserAgent = &input.UserAgent
|
||||
}
|
||||
|
||||
if apiKey.GroupID != nil {
|
||||
usageLog.GroupID = apiKey.GroupID
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ type LiteLLMModelPricing struct {
|
||||
LiteLLMProvider string `json:"litellm_provider"`
|
||||
Mode string `json:"mode"`
|
||||
SupportsPromptCaching bool `json:"supports_prompt_caching"`
|
||||
OutputCostPerImage float64 `json:"output_cost_per_image"` // 图片生成模型每张图片价格
|
||||
}
|
||||
|
||||
// PricingRemoteClient 远程价格数据获取接口
|
||||
@@ -51,6 +52,7 @@ type LiteLLMRawEntry struct {
|
||||
LiteLLMProvider string `json:"litellm_provider"`
|
||||
Mode string `json:"mode"`
|
||||
SupportsPromptCaching bool `json:"supports_prompt_caching"`
|
||||
OutputCostPerImage *float64 `json:"output_cost_per_image"`
|
||||
}
|
||||
|
||||
// PricingService 动态价格服务
|
||||
@@ -319,6 +321,9 @@ func (s *PricingService) parsePricingData(body []byte) (map[string]*LiteLLMModel
|
||||
if entry.CacheReadInputTokenCost != nil {
|
||||
pricing.CacheReadInputTokenCost = *entry.CacheReadInputTokenCost
|
||||
}
|
||||
if entry.OutputCostPerImage != nil {
|
||||
pricing.OutputCostPerImage = *entry.OutputCostPerImage
|
||||
}
|
||||
|
||||
result[modelName] = pricing
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ type UsageLog struct {
|
||||
Stream bool
|
||||
DurationMs *int
|
||||
FirstTokenMs *int
|
||||
UserAgent *string
|
||||
|
||||
// 图片生成字段
|
||||
ImageCount int
|
||||
ImageSize *string
|
||||
|
||||
CreatedAt time.Time
|
||||
|
||||
|
||||
@@ -319,3 +319,12 @@ func (s *UsageService) GetGlobalStats(ctx context.Context, startTime, endTime ti
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetStatsWithFilters returns usage stats with optional filters.
|
||||
func (s *UsageService) GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error) {
|
||||
stats, err := s.usageRepo.GetStatsWithFilters(ctx, filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get usage stats with filters: %w", err)
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -47,6 +47,13 @@ func ProvideTokenRefreshService(
|
||||
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
|
||||
func ProvideTimingWheelService() *TimingWheelService {
|
||||
svc := NewTimingWheelService()
|
||||
@@ -110,6 +117,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewCRSSyncService,
|
||||
ProvideUpdateService,
|
||||
ProvideTokenRefreshService,
|
||||
ProvideAccountExpiryService,
|
||||
ProvideTimingWheelService,
|
||||
ProvideDeferredService,
|
||||
NewAntigravityQuotaFetcher,
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/repository"
|
||||
@@ -22,10 +21,44 @@ import (
|
||||
|
||||
// Config paths
|
||||
const (
|
||||
ConfigFile = "config.yaml"
|
||||
EnvFile = ".env"
|
||||
ConfigFileName = "config.yaml"
|
||||
InstallLockFile = ".installed"
|
||||
)
|
||||
|
||||
// GetDataDir returns the data directory for storing config and lock files.
|
||||
// Priority: DATA_DIR env > /app/data (if exists and writable) > current directory
|
||||
func GetDataDir() string {
|
||||
// Check DATA_DIR environment variable first
|
||||
if dir := os.Getenv("DATA_DIR"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
|
||||
// Check if /app/data exists and is writable (Docker environment)
|
||||
dockerDataDir := "/app/data"
|
||||
if info, err := os.Stat(dockerDataDir); err == nil && info.IsDir() {
|
||||
// Try to check if writable by creating a temp file
|
||||
testFile := dockerDataDir + "/.write_test"
|
||||
if f, err := os.Create(testFile); err == nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(testFile)
|
||||
return dockerDataDir
|
||||
}
|
||||
}
|
||||
|
||||
// Default to current directory
|
||||
return "."
|
||||
}
|
||||
|
||||
// GetConfigFilePath returns the full path to config.yaml
|
||||
func GetConfigFilePath() string {
|
||||
return GetDataDir() + "/" + ConfigFileName
|
||||
}
|
||||
|
||||
// GetInstallLockPath returns the full path to .installed lock file
|
||||
func GetInstallLockPath() string {
|
||||
return GetDataDir() + "/" + InstallLockFile
|
||||
}
|
||||
|
||||
// SetupConfig holds the setup configuration
|
||||
type SetupConfig struct {
|
||||
Database DatabaseConfig `json:"database" yaml:"database"`
|
||||
@@ -72,13 +105,12 @@ type JWTConfig struct {
|
||||
// Uses multiple checks to prevent attackers from forcing re-setup by deleting config
|
||||
func NeedsSetup() bool {
|
||||
// Check 1: Config file must not exist
|
||||
if _, err := os.Stat(ConfigFile); !os.IsNotExist(err) {
|
||||
if _, err := os.Stat(GetConfigFilePath()); !os.IsNotExist(err) {
|
||||
return false // Config exists, no setup needed
|
||||
}
|
||||
|
||||
// Check 2: Installation lock file (harder to bypass)
|
||||
lockFile := ".installed"
|
||||
if _, err := os.Stat(lockFile); !os.IsNotExist(err) {
|
||||
if _, err := os.Stat(GetInstallLockPath()); !os.IsNotExist(err) {
|
||||
return false // Lock file exists, already installed
|
||||
}
|
||||
|
||||
@@ -197,17 +229,12 @@ func Install(cfg *SetupConfig) error {
|
||||
|
||||
// Generate JWT secret if not provided
|
||||
if cfg.JWT.Secret == "" {
|
||||
if strings.EqualFold(cfg.Server.Mode, "release") {
|
||||
return fmt.Errorf("jwt secret is required in release mode")
|
||||
}
|
||||
secret, err := generateSecret(32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate jwt secret: %w", err)
|
||||
}
|
||||
cfg.JWT.Secret = secret
|
||||
log.Println("Warning: JWT secret auto-generated for non-release mode. Do not use in production.")
|
||||
} else if strings.EqualFold(cfg.Server.Mode, "release") && len(cfg.JWT.Secret) < 32 {
|
||||
return fmt.Errorf("jwt secret must be at least 32 characters in release mode")
|
||||
log.Println("Warning: JWT secret auto-generated. Consider setting a fixed secret for production.")
|
||||
}
|
||||
|
||||
// Test connections
|
||||
@@ -244,9 +271,8 @@ func Install(cfg *SetupConfig) error {
|
||||
|
||||
// createInstallLock creates a lock file to prevent re-installation attacks
|
||||
func createInstallLock() error {
|
||||
lockFile := ".installed"
|
||||
content := fmt.Sprintf("installed_at=%s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
return os.WriteFile(lockFile, []byte(content), 0400) // Read-only for owner
|
||||
return os.WriteFile(GetInstallLockPath(), []byte(content), 0400) // Read-only for owner
|
||||
}
|
||||
|
||||
func initializeDatabase(cfg *SetupConfig) error {
|
||||
@@ -397,7 +423,7 @@ func writeConfigFile(cfg *SetupConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(ConfigFile, data, 0600)
|
||||
return os.WriteFile(GetConfigFilePath(), data, 0600)
|
||||
}
|
||||
|
||||
func generateSecret(length int) (string, error) {
|
||||
@@ -440,6 +466,7 @@ func getEnvIntOrDefault(key string, defaultValue int) int {
|
||||
// This is designed for Docker deployment where all config is passed via env vars
|
||||
func AutoSetupFromEnv() error {
|
||||
log.Println("Auto setup enabled, configuring from environment variables...")
|
||||
log.Printf("Data directory: %s", GetDataDir())
|
||||
|
||||
// Get timezone from TZ or TIMEZONE env var (TZ is standard for Docker)
|
||||
tz := getEnvOrDefault("TZ", "")
|
||||
@@ -481,17 +508,12 @@ func AutoSetupFromEnv() error {
|
||||
|
||||
// Generate JWT secret if not provided
|
||||
if cfg.JWT.Secret == "" {
|
||||
if strings.EqualFold(cfg.Server.Mode, "release") {
|
||||
return fmt.Errorf("jwt secret is required in release mode")
|
||||
}
|
||||
secret, err := generateSecret(32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate jwt secret: %w", err)
|
||||
}
|
||||
cfg.JWT.Secret = secret
|
||||
log.Println("Warning: JWT secret auto-generated for non-release mode. Do not use in production.")
|
||||
} else if strings.EqualFold(cfg.Server.Mode, "release") && len(cfg.JWT.Secret) < 32 {
|
||||
return fmt.Errorf("jwt secret must be at least 32 characters in release mode")
|
||||
log.Println("Warning: JWT secret auto-generated. Consider setting a fixed secret for production.")
|
||||
}
|
||||
|
||||
// Generate admin password if not provided
|
||||
|
||||
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/028_group_image_pricing.sql
Normal file
10
backend/migrations/028_group_image_pricing.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- 为 Antigravity 分组添加图片生成计费配置
|
||||
-- 支持 gemini-3-pro-image 模型的 1K/2K/4K 分辨率按次计费
|
||||
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS image_price_1k DECIMAL(20,8);
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS image_price_2k DECIMAL(20,8);
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS image_price_4k DECIMAL(20,8);
|
||||
|
||||
COMMENT ON COLUMN groups.image_price_1k IS '1K 分辨率图片生成单价 (USD),仅 antigravity 平台使用';
|
||||
COMMENT ON COLUMN groups.image_price_2k IS '2K 分辨率图片生成单价 (USD),仅 antigravity 平台使用';
|
||||
COMMENT ON COLUMN groups.image_price_4k IS '4K 分辨率图片生成单价 (USD),仅 antigravity 平台使用';
|
||||
5
backend/migrations/029_usage_log_image_fields.sql
Normal file
5
backend/migrations/029_usage_log_image_fields.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- 为使用日志添加图片生成统计字段
|
||||
-- 用于记录 gemini-3-pro-image 等图片生成模型的使用情况
|
||||
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_count INT DEFAULT 0;
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_size VARCHAR(10);
|
||||
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;
|
||||
390
config.yaml
Normal file
390
config.yaml
Normal file
@@ -0,0 +1,390 @@
|
||||
# Sub2API Configuration File
|
||||
# Sub2API 配置文件
|
||||
#
|
||||
# Copy this file to /etc/sub2api/config.yaml and modify as needed
|
||||
# 复制此文件到 /etc/sub2api/config.yaml 并根据需要修改
|
||||
#
|
||||
# Documentation / 文档: https://github.com/Wei-Shaw/sub2api
|
||||
|
||||
# =============================================================================
|
||||
# Server Configuration
|
||||
# 服务器配置
|
||||
# =============================================================================
|
||||
server:
|
||||
# Bind address (0.0.0.0 for all interfaces)
|
||||
# 绑定地址(0.0.0.0 表示监听所有网络接口)
|
||||
host: "0.0.0.0"
|
||||
# Port to listen on
|
||||
# 监听端口
|
||||
port: 8080
|
||||
# Mode: "debug" for development, "release" for production
|
||||
# 运行模式:"debug" 用于开发,"release" 用于生产环境
|
||||
mode: "release"
|
||||
# Trusted proxies for X-Forwarded-For parsing (CIDR/IP). Empty disables trusted proxies.
|
||||
# 信任的代理地址(CIDR/IP 格式),用于解析 X-Forwarded-For 头。留空则禁用代理信任。
|
||||
trusted_proxies: []
|
||||
|
||||
# =============================================================================
|
||||
# Run Mode Configuration
|
||||
# 运行模式配置
|
||||
# =============================================================================
|
||||
# Run mode: "standard" (default) or "simple" (for internal use)
|
||||
# 运行模式:"standard"(默认)或 "simple"(内部使用)
|
||||
# - standard: Full SaaS features with billing/balance checks
|
||||
# - standard: 完整 SaaS 功能,包含计费和余额校验
|
||||
# - simple: Hides SaaS features and skips billing/balance checks
|
||||
# - simple: 隐藏 SaaS 功能,跳过计费和余额校验
|
||||
run_mode: "standard"
|
||||
|
||||
# =============================================================================
|
||||
# CORS Configuration
|
||||
# 跨域资源共享 (CORS) 配置
|
||||
# =============================================================================
|
||||
cors:
|
||||
# Allowed origins list. Leave empty to disable cross-origin requests.
|
||||
# 允许的来源列表。留空则禁用跨域请求。
|
||||
allowed_origins: []
|
||||
# Allow credentials (cookies/authorization headers). Cannot be used with "*".
|
||||
# 允许携带凭证(cookies/授权头)。不能与 "*" 通配符同时使用。
|
||||
allow_credentials: true
|
||||
|
||||
# =============================================================================
|
||||
# Security Configuration
|
||||
# 安全配置
|
||||
# =============================================================================
|
||||
security:
|
||||
url_allowlist:
|
||||
# Enable URL allowlist validation (disable to skip all URL checks)
|
||||
# 启用 URL 白名单验证(禁用则跳过所有 URL 检查)
|
||||
enabled: false
|
||||
# Allowed upstream hosts for API proxying
|
||||
# 允许代理的上游 API 主机列表
|
||||
upstream_hosts:
|
||||
- "api.openai.com"
|
||||
- "api.anthropic.com"
|
||||
- "api.kimi.com"
|
||||
- "open.bigmodel.cn"
|
||||
- "api.minimaxi.com"
|
||||
- "generativelanguage.googleapis.com"
|
||||
- "cloudcode-pa.googleapis.com"
|
||||
- "*.openai.azure.com"
|
||||
# Allowed hosts for pricing data download
|
||||
# 允许下载定价数据的主机列表
|
||||
pricing_hosts:
|
||||
- "raw.githubusercontent.com"
|
||||
# Allowed hosts for CRS sync (required when using CRS sync)
|
||||
# 允许 CRS 同步的主机列表(使用 CRS 同步功能时必须配置)
|
||||
crs_hosts: []
|
||||
# Allow localhost/private IPs for upstream/pricing/CRS (use only in trusted networks)
|
||||
# 允许本地/私有 IP 地址用于上游/定价/CRS(仅在可信网络中使用)
|
||||
allow_private_hosts: true
|
||||
# Allow http:// URLs when allowlist is disabled (default: false, require https)
|
||||
# 白名单禁用时是否允许 http:// URL(默认: false,要求 https)
|
||||
allow_insecure_http: true
|
||||
response_headers:
|
||||
# Enable configurable response header filtering (disable to use default allowlist)
|
||||
# 启用可配置的响应头过滤(禁用则使用默认白名单)
|
||||
enabled: false
|
||||
# Extra allowed response headers from upstream
|
||||
# 额外允许的上游响应头
|
||||
additional_allowed: []
|
||||
# Force-remove response headers from upstream
|
||||
# 强制移除的上游响应头
|
||||
force_remove: []
|
||||
csp:
|
||||
# Enable Content-Security-Policy header
|
||||
# 启用内容安全策略 (CSP) 响应头
|
||||
enabled: true
|
||||
# Default CSP policy (override if you host assets on other domains)
|
||||
# 默认 CSP 策略(如果静态资源托管在其他域名,请自行覆盖)
|
||||
policy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
proxy_probe:
|
||||
# Allow skipping TLS verification for proxy probe (debug only)
|
||||
# 允许代理探测时跳过 TLS 证书验证(仅用于调试)
|
||||
insecure_skip_verify: false
|
||||
|
||||
# =============================================================================
|
||||
# Gateway Configuration
|
||||
# 网关配置
|
||||
# =============================================================================
|
||||
gateway:
|
||||
# Timeout for waiting upstream response headers (seconds)
|
||||
# 等待上游响应头超时时间(秒)
|
||||
response_header_timeout: 600
|
||||
# Max request body size in bytes (default: 100MB)
|
||||
# 请求体最大字节数(默认 100MB)
|
||||
max_body_size: 104857600
|
||||
# Connection pool isolation strategy:
|
||||
# 连接池隔离策略:
|
||||
# - proxy: Isolate by proxy, same proxy shares connection pool (suitable for few proxies, many accounts)
|
||||
# - proxy: 按代理隔离,同一代理共享连接池(适合代理少、账户多)
|
||||
# - account: Isolate by account, same account shares connection pool (suitable for few accounts, strict isolation)
|
||||
# - account: 按账户隔离,同一账户共享连接池(适合账户少、需严格隔离)
|
||||
# - account_proxy: Isolate by account+proxy combination (default, finest granularity)
|
||||
# - account_proxy: 按账户+代理组合隔离(默认,最细粒度)
|
||||
connection_pool_isolation: "account_proxy"
|
||||
# HTTP upstream connection pool settings (HTTP/2 + multi-proxy scenario defaults)
|
||||
# HTTP 上游连接池配置(HTTP/2 + 多代理场景默认值)
|
||||
# Max idle connections across all hosts
|
||||
# 所有主机的最大空闲连接数
|
||||
max_idle_conns: 240
|
||||
# Max idle connections per host
|
||||
# 每个主机的最大空闲连接数
|
||||
max_idle_conns_per_host: 120
|
||||
# Max connections per host
|
||||
# 每个主机的最大连接数
|
||||
max_conns_per_host: 240
|
||||
# Idle connection timeout (seconds)
|
||||
# 空闲连接超时时间(秒)
|
||||
idle_conn_timeout_seconds: 90
|
||||
# Upstream client cache settings
|
||||
# 上游连接池客户端缓存配置
|
||||
# max_upstream_clients: Max cached clients, evicts least recently used when exceeded
|
||||
# max_upstream_clients: 最大缓存客户端数量,超出后淘汰最久未使用的
|
||||
max_upstream_clients: 5000
|
||||
# client_idle_ttl_seconds: Client idle reclaim threshold (seconds), reclaimed when idle and no active requests
|
||||
# client_idle_ttl_seconds: 客户端空闲回收阈值(秒),超时且无活跃请求时回收
|
||||
client_idle_ttl_seconds: 900
|
||||
# Concurrency slot expiration time (minutes)
|
||||
# 并发槽位过期时间(分钟)
|
||||
concurrency_slot_ttl_minutes: 30
|
||||
# Stream data interval timeout (seconds), 0=disable
|
||||
# 流数据间隔超时(秒),0=禁用
|
||||
stream_data_interval_timeout: 180
|
||||
# Stream keepalive interval (seconds), 0=disable
|
||||
# 流式 keepalive 间隔(秒),0=禁用
|
||||
stream_keepalive_interval: 10
|
||||
# SSE max line size in bytes (default: 10MB)
|
||||
# SSE 单行最大字节数(默认 10MB)
|
||||
max_line_size: 10485760
|
||||
# Log upstream error response body summary (safe/truncated; does not log request content)
|
||||
# 记录上游错误响应体摘要(安全/截断;不记录请求内容)
|
||||
log_upstream_error_body: false
|
||||
# Max bytes to log from upstream error body
|
||||
# 记录上游错误响应体的最大字节数
|
||||
log_upstream_error_body_max_bytes: 2048
|
||||
# Auto inject anthropic-beta header for API-key accounts when needed (default: off)
|
||||
# 需要时自动为 API-key 账户注入 anthropic-beta 头(默认:关闭)
|
||||
inject_beta_for_apikey: false
|
||||
# Allow failover on selected 400 errors (default: off)
|
||||
# 允许在特定 400 错误时进行故障转移(默认:关闭)
|
||||
failover_on_400: false
|
||||
|
||||
# =============================================================================
|
||||
# Concurrency Wait Configuration
|
||||
# 并发等待配置
|
||||
# =============================================================================
|
||||
concurrency:
|
||||
# SSE ping interval during concurrency wait (seconds)
|
||||
# 并发等待期间的 SSE ping 间隔(秒)
|
||||
ping_interval: 10
|
||||
|
||||
# =============================================================================
|
||||
# Database Configuration (PostgreSQL)
|
||||
# 数据库配置 (PostgreSQL)
|
||||
# =============================================================================
|
||||
database:
|
||||
# Database host address
|
||||
# 数据库主机地址
|
||||
host: "localhost"
|
||||
# Database port
|
||||
# 数据库端口
|
||||
port: 5432
|
||||
# Database username
|
||||
# 数据库用户名
|
||||
user: "postgres"
|
||||
# Database password
|
||||
# 数据库密码
|
||||
password: "your_secure_password_here"
|
||||
# Database name
|
||||
# 数据库名称
|
||||
dbname: "sub2api"
|
||||
# SSL mode: disable, require, verify-ca, verify-full
|
||||
# SSL 模式:disable(禁用), require(要求), verify-ca(验证CA), verify-full(完全验证)
|
||||
sslmode: "disable"
|
||||
|
||||
# =============================================================================
|
||||
# Redis Configuration
|
||||
# Redis 配置
|
||||
# =============================================================================
|
||||
redis:
|
||||
# Redis host address
|
||||
# Redis 主机地址
|
||||
host: "localhost"
|
||||
# Redis port
|
||||
# Redis 端口
|
||||
port: 6379
|
||||
# Redis password (leave empty if no password is set)
|
||||
# Redis 密码(如果未设置密码则留空)
|
||||
password: ""
|
||||
# Database number (0-15)
|
||||
# 数据库编号(0-15)
|
||||
db: 0
|
||||
|
||||
# =============================================================================
|
||||
# JWT Configuration
|
||||
# JWT 配置
|
||||
# =============================================================================
|
||||
jwt:
|
||||
# IMPORTANT: Change this to a random string in production!
|
||||
# 重要:生产环境中请更改为随机字符串!
|
||||
# Generate with / 生成命令: openssl rand -hex 32
|
||||
secret: "change-this-to-a-secure-random-string"
|
||||
# Token expiration time in hours (max 24)
|
||||
# 令牌过期时间(小时,最大 24)
|
||||
expire_hour: 24
|
||||
|
||||
# =============================================================================
|
||||
# Default Settings
|
||||
# 默认设置
|
||||
# =============================================================================
|
||||
default:
|
||||
# Initial admin account (created on first run)
|
||||
# 初始管理员账户(首次运行时创建)
|
||||
admin_email: "admin@example.com"
|
||||
admin_password: "admin123"
|
||||
|
||||
# Default settings for new users
|
||||
# 新用户默认设置
|
||||
# Max concurrent requests per user
|
||||
# 每用户最大并发请求数
|
||||
user_concurrency: 5
|
||||
# Initial balance for new users
|
||||
# 新用户初始余额
|
||||
user_balance: 0
|
||||
|
||||
# API key settings
|
||||
# API 密钥设置
|
||||
# Prefix for generated API keys
|
||||
# 生成的 API 密钥前缀
|
||||
api_key_prefix: "sk-"
|
||||
|
||||
# Rate multiplier (affects billing calculation)
|
||||
# 费率倍数(影响计费计算)
|
||||
rate_multiplier: 1.0
|
||||
|
||||
# =============================================================================
|
||||
# Rate Limiting
|
||||
# 速率限制
|
||||
# =============================================================================
|
||||
rate_limit:
|
||||
# Cooldown time (in minutes) when upstream returns 529 (overloaded)
|
||||
# 上游返回 529(过载)时的冷却时间(分钟)
|
||||
overload_cooldown_minutes: 10
|
||||
|
||||
# =============================================================================
|
||||
# Pricing Data Source (Optional)
|
||||
# 定价数据源(可选)
|
||||
# =============================================================================
|
||||
pricing:
|
||||
# URL to fetch model pricing data (default: LiteLLM)
|
||||
# 获取模型定价数据的 URL(默认:LiteLLM)
|
||||
remote_url: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
|
||||
# Hash verification URL (optional)
|
||||
# 哈希校验 URL(可选)
|
||||
hash_url: ""
|
||||
# Local data directory for caching
|
||||
# 本地数据缓存目录
|
||||
data_dir: "./data"
|
||||
# Fallback pricing file
|
||||
# 备用定价文件
|
||||
fallback_file: "./resources/model-pricing/model_prices_and_context_window.json"
|
||||
# Update interval in hours
|
||||
# 更新间隔(小时)
|
||||
update_interval_hours: 24
|
||||
# Hash check interval in minutes
|
||||
# 哈希检查间隔(分钟)
|
||||
hash_check_interval_minutes: 10
|
||||
|
||||
# =============================================================================
|
||||
# Billing Configuration
|
||||
# 计费配置
|
||||
# =============================================================================
|
||||
billing:
|
||||
circuit_breaker:
|
||||
# Enable circuit breaker for billing service
|
||||
# 启用计费服务熔断器
|
||||
enabled: true
|
||||
# Number of failures before opening circuit
|
||||
# 触发熔断的失败次数阈值
|
||||
failure_threshold: 5
|
||||
# Time to wait before attempting reset (seconds)
|
||||
# 熔断后重试等待时间(秒)
|
||||
reset_timeout_seconds: 30
|
||||
# Number of requests to allow in half-open state
|
||||
# 半开状态允许通过的请求数
|
||||
half_open_requests: 3
|
||||
|
||||
# =============================================================================
|
||||
# Turnstile Configuration
|
||||
# Turnstile 人机验证配置
|
||||
# =============================================================================
|
||||
turnstile:
|
||||
# Require Turnstile in release mode (when enabled, login/register will fail if not configured)
|
||||
# 在 release 模式下要求 Turnstile 验证(启用后,若未配置则登录/注册会失败)
|
||||
required: false
|
||||
|
||||
# =============================================================================
|
||||
# Gemini OAuth (Required for Gemini accounts)
|
||||
# Gemini OAuth 配置(Gemini 账户必需)
|
||||
# =============================================================================
|
||||
# Sub2API supports TWO Gemini OAuth modes:
|
||||
# Sub2API 支持两种 Gemini OAuth 模式:
|
||||
#
|
||||
# 1. Code Assist OAuth (requires GCP project_id)
|
||||
# 1. Code Assist OAuth(需要 GCP project_id)
|
||||
# - Uses: cloudcode-pa.googleapis.com (Code Assist API)
|
||||
# - 使用:cloudcode-pa.googleapis.com(Code Assist API)
|
||||
#
|
||||
# 2. AI Studio OAuth (no project_id needed)
|
||||
# 2. AI Studio OAuth(不需要 project_id)
|
||||
# - Uses: generativelanguage.googleapis.com (AI Studio API)
|
||||
# - 使用:generativelanguage.googleapis.com(AI Studio API)
|
||||
#
|
||||
# Default: Uses Gemini CLI's public OAuth credentials (same as Google's official CLI tool)
|
||||
# 默认:使用 Gemini CLI 的公开 OAuth 凭证(与 Google 官方 CLI 工具相同)
|
||||
gemini:
|
||||
oauth:
|
||||
# Gemini CLI public OAuth credentials (works for both Code Assist and AI Studio)
|
||||
# Gemini CLI 公开 OAuth 凭证(适用于 Code Assist 和 AI Studio)
|
||||
client_id: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
|
||||
client_secret: "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"
|
||||
# Optional scopes (space-separated). Leave empty to auto-select based on oauth_type.
|
||||
# 可选的权限范围(空格分隔)。留空则根据 oauth_type 自动选择。
|
||||
scopes: ""
|
||||
quota:
|
||||
# Optional: local quota simulation for Gemini Code Assist (local billing).
|
||||
# 可选:Gemini Code Assist 本地配额模拟(本地计费)。
|
||||
# These values are used for UI progress + precheck scheduling, not official Google quotas.
|
||||
# 这些值用于 UI 进度显示和预检调度,并非 Google 官方配额。
|
||||
tiers:
|
||||
LEGACY:
|
||||
# Pro model requests per day
|
||||
# Pro 模型每日请求数
|
||||
pro_rpd: 50
|
||||
# Flash model requests per day
|
||||
# Flash 模型每日请求数
|
||||
flash_rpd: 1500
|
||||
# Cooldown time (minutes) after hitting quota
|
||||
# 达到配额后的冷却时间(分钟)
|
||||
cooldown_minutes: 30
|
||||
PRO:
|
||||
# Pro model requests per day
|
||||
# Pro 模型每日请求数
|
||||
pro_rpd: 1500
|
||||
# Flash model requests per day
|
||||
# Flash 模型每日请求数
|
||||
flash_rpd: 4000
|
||||
# Cooldown time (minutes) after hitting quota
|
||||
# 达到配额后的冷却时间(分钟)
|
||||
cooldown_minutes: 5
|
||||
ULTRA:
|
||||
# Pro model requests per day
|
||||
# Pro 模型每日请求数
|
||||
pro_rpd: 2000
|
||||
# Flash model requests per day (0 = unlimited)
|
||||
# Flash 模型每日请求数(0 = 无限制)
|
||||
flash_rpd: 0
|
||||
# Cooldown time (minutes) after hitting quota
|
||||
# 达到配额后的冷却时间(分钟)
|
||||
cooldown_minutes: 5
|
||||
@@ -54,7 +54,10 @@ ADMIN_PASSWORD=
|
||||
# -----------------------------------------------------------------------------
|
||||
# JWT Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
# Leave empty to auto-generate (recommended)
|
||||
# IMPORTANT: Set a fixed JWT_SECRET to prevent login sessions from being
|
||||
# invalidated after container restarts. If left empty, a random secret will
|
||||
# be generated on each startup, causing all users to be logged out.
|
||||
# Generate a secure secret: openssl rand -hex 32
|
||||
JWT_SECRET=
|
||||
JWT_EXPIRE_HOUR=24
|
||||
|
||||
@@ -66,6 +69,24 @@ JWT_EXPIRE_HOUR=24
|
||||
# Leave unset to use default ./config.yaml
|
||||
#CONFIG_FILE=./config.yaml
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Security Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
# URL Allowlist Configuration
|
||||
# 启用 URL 白名单验证(false 则跳过白名单检查,仅做基本格式校验)
|
||||
SECURITY_URL_ALLOWLIST_ENABLED=false
|
||||
|
||||
# 关闭白名单时,是否允许 http:// URL(默认 false,只允许 https://)
|
||||
# ⚠️ 警告:允许 HTTP 存在安全风险(明文传输),仅建议在开发/测试环境或可信内网中使用
|
||||
# Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https)
|
||||
# ⚠️ WARNING: Allowing HTTP has security risks (plaintext transmission)
|
||||
# Only recommended for dev/test environments or trusted networks
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=true
|
||||
|
||||
# 是否允许本地/私有 IP 地址用于上游/定价/CRS(仅在可信网络中使用)
|
||||
# Allow localhost/private IPs for upstream/pricing/CRS (use only in trusted networks)
|
||||
SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Gemini OAuth (OPTIONAL, required only for Gemini OAuth accounts)
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -102,3 +123,17 @@ GEMINI_OAUTH_SCOPES=
|
||||
# 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=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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=
|
||||
|
||||
@@ -97,7 +97,7 @@ security:
|
||||
enabled: true
|
||||
# Default CSP policy (override if you host assets on other domains)
|
||||
# 默认 CSP 策略(如果静态资源托管在其他域名,请自行覆盖)
|
||||
policy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
policy: "default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||
proxy_probe:
|
||||
# Allow skipping TLS verification for proxy probe (debug only)
|
||||
# 允许代理探测时跳过 TLS 证书验证(仅用于调试)
|
||||
@@ -388,3 +388,18 @@ gemini:
|
||||
# Cooldown time (minutes) after hitting quota
|
||||
# 达到配额后的冷却时间(分钟)
|
||||
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: ""
|
||||
|
||||
@@ -72,7 +72,10 @@ services:
|
||||
# =======================================================================
|
||||
# JWT Configuration
|
||||
# =======================================================================
|
||||
# Leave empty to auto-generate (recommended)
|
||||
# IMPORTANT: Set a fixed JWT_SECRET to prevent login sessions from being
|
||||
# invalidated after container restarts. If left empty, a random secret
|
||||
# will be generated on each startup.
|
||||
# Generate a secure secret: openssl rand -hex 32
|
||||
- JWT_SECRET=${JWT_SECRET:-}
|
||||
- JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
|
||||
|
||||
@@ -98,9 +101,21 @@ services:
|
||||
# =======================================================================
|
||||
# Security Configuration (URL Allowlist)
|
||||
# =======================================================================
|
||||
- SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS=${SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS:-}
|
||||
# Allow private IP addresses for CRS sync (for internal deployments)
|
||||
# Enable URL allowlist validation (false to skip allowlist checks)
|
||||
- SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false}
|
||||
# Allow insecure HTTP URLs when allowlist is disabled (default: false, requires https)
|
||||
- SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false}
|
||||
# Allow private IP addresses for upstream/pricing/CRS (for internal deployments)
|
||||
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
||||
# Upstream hosts whitelist (comma-separated, only used when enabled=true)
|
||||
- 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:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
118
frontend/audit.json
Normal file
118
frontend/audit.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "review",
|
||||
"module": "xlsx",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 1108110,
|
||||
"path": ".>xlsx",
|
||||
"dev": false,
|
||||
"bundled": false,
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"id": 1108111,
|
||||
"path": ".>xlsx",
|
||||
"dev": false,
|
||||
"bundled": false,
|
||||
"optional": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"1108110": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "0.18.5",
|
||||
"paths": [
|
||||
".>xlsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"references": "- https://nvd.nist.gov/vuln/detail/CVE-2023-30533\n- https://cdn.sheetjs.com/advisories/CVE-2023-30533\n- https://git.sheetjs.com/sheetjs/sheetjs/src/branch/master/CHANGELOG.md\n- https://git.sheetjs.com/sheetjs/sheetjs/issues/2667\n- https://git.sheetjs.com/sheetjs/sheetjs/issues/2986\n- https://cdn.sheetjs.com\n- https://github.com/advisories/GHSA-4r6h-8v6p-xvw6",
|
||||
"created": "2023-04-24T09:30:19.000Z",
|
||||
"id": 1108110,
|
||||
"npm_advisory_id": null,
|
||||
"overview": "All versions of SheetJS CE through 0.19.2 are vulnerable to \"Prototype Pollution\" when reading specially crafted files. Workflows that do not read arbitrary files (for example, exporting data to spreadsheet files) are unaffected.\n\nA non-vulnerable version cannot be found via npm, as the repository hosted on GitHub and the npm package `xlsx` are no longer maintained. Version 0.19.3 can be downloaded via https://cdn.sheetjs.com/.",
|
||||
"reported_by": null,
|
||||
"title": "Prototype Pollution in sheetJS",
|
||||
"metadata": null,
|
||||
"cves": [
|
||||
"CVE-2023-30533"
|
||||
],
|
||||
"access": "public",
|
||||
"severity": "high",
|
||||
"module_name": "xlsx",
|
||||
"vulnerable_versions": "<0.19.3",
|
||||
"github_advisory_id": "GHSA-4r6h-8v6p-xvw6",
|
||||
"recommendation": "None",
|
||||
"patched_versions": "<0.0.0",
|
||||
"updated": "2025-09-19T15:23:41.000Z",
|
||||
"cvss": {
|
||||
"score": 7.8,
|
||||
"vectorString": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
|
||||
},
|
||||
"cwe": [
|
||||
"CWE-1321"
|
||||
],
|
||||
"url": "https://github.com/advisories/GHSA-4r6h-8v6p-xvw6"
|
||||
},
|
||||
"1108111": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "0.18.5",
|
||||
"paths": [
|
||||
".>xlsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"references": "- https://nvd.nist.gov/vuln/detail/CVE-2024-22363\n- https://cdn.sheetjs.com/advisories/CVE-2024-22363\n- https://cwe.mitre.org/data/definitions/1333.html\n- https://git.sheetjs.com/sheetjs/sheetjs/src/tag/v0.20.2\n- https://cdn.sheetjs.com\n- https://github.com/advisories/GHSA-5pgg-2g8v-p4x9",
|
||||
"created": "2024-04-05T06:30:46.000Z",
|
||||
"id": 1108111,
|
||||
"npm_advisory_id": null,
|
||||
"overview": "SheetJS Community Edition before 0.20.2 is vulnerable.to Regular Expression Denial of Service (ReDoS).\n\nA non-vulnerable version cannot be found via npm, as the repository hosted on GitHub and the npm package `xlsx` are no longer maintained. Version 0.20.2 can be downloaded via https://cdn.sheetjs.com/.",
|
||||
"reported_by": null,
|
||||
"title": "SheetJS Regular Expression Denial of Service (ReDoS)",
|
||||
"metadata": null,
|
||||
"cves": [
|
||||
"CVE-2024-22363"
|
||||
],
|
||||
"access": "public",
|
||||
"severity": "high",
|
||||
"module_name": "xlsx",
|
||||
"vulnerable_versions": "<0.20.2",
|
||||
"github_advisory_id": "GHSA-5pgg-2g8v-p4x9",
|
||||
"recommendation": "None",
|
||||
"patched_versions": "<0.0.0",
|
||||
"updated": "2025-09-19T15:23:26.000Z",
|
||||
"cvss": {
|
||||
"score": 7.5,
|
||||
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
|
||||
},
|
||||
"cwe": [
|
||||
"CWE-1333"
|
||||
],
|
||||
"url": "https://github.com/advisories/GHSA-5pgg-2g8v-p4x9"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 2,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 639,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 639
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user