Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
2fe8932c
Unverified
Commit
2fe8932c
authored
Feb 03, 2026
by
Call White
Committed by
GitHub
Feb 03, 2026
Browse files
Merge pull request #3 from cyhhao/main
merge to main
parents
2f2e76f9
adb77af1
Changes
267
Hide whitespace changes
Inline
Side-by-side
deploy/README.md
View file @
2fe8932c
...
...
@@ -401,3 +401,60 @@ sudo systemctl status redis
2.
**Database connection failed**
: Check PostgreSQL is running and credentials are correct
3.
**Redis connection failed**
: Check Redis is running and password is correct
4.
**Permission denied**
: Ensure proper file ownership for binary install
---
## TLS Fingerprint Configuration
Sub2API supports TLS fingerprint simulation to make requests appear as if they come from the official Claude CLI (Node.js client).
> **💡 Tip:** Visit **[tls.sub2api.org](https://tls.sub2api.org/)** to get TLS fingerprint information for different devices and browsers.
### Default Behavior
-
Built-in
`claude_cli_v2`
profile simulates Node.js 20.x + OpenSSL 3.x
-
JA3 Hash:
`1a28e69016765d92e3b381168d68922c`
-
JA4:
`t13d5911h1_a33745022dd6_1f22a2ca17c4`
-
Profile selection:
`accountID % profileCount`
### Configuration
```
yaml
gateway
:
tls_fingerprint
:
enabled
:
true
# Global switch
profiles
:
# Simple profile (uses default cipher suites)
profile_1
:
name
:
"
Profile
1"
# Profile with custom cipher suites (use compact array format)
profile_2
:
name
:
"
Profile
2"
cipher_suites
:
[
4866
,
4867
,
4865
,
49199
,
49195
,
49200
,
49196
]
curves
:
[
29
,
23
,
24
]
point_formats
:
[
0
]
# Another custom profile
profile_3
:
name
:
"
Profile
3"
cipher_suites
:
[
4865
,
4866
,
4867
,
49199
,
49200
]
curves
:
[
29
,
23
,
24
,
25
]
```
### Profile Fields
| Field | Type | Description |
|-------|------|-------------|
|
`name`
| string | Display name (required) |
|
`cipher_suites`
| []uint16 | Cipher suites in decimal. Empty = default |
|
`curves`
| []uint16 | Elliptic curves in decimal. Empty = default |
|
`point_formats`
| []uint8 | EC point formats. Empty = default |
### Common Values Reference
**Cipher Suites (TLS 1.3):**
`4865`
(AES_128_GCM),
`4866`
(AES_256_GCM),
`4867`
(CHACHA20)
**Cipher Suites (TLS 1.2):**
`49195`
,
`49196`
,
`49199`
,
`49200`
(ECDHE variants)
**Curves:**
`29`
(X25519),
`23`
(P-256),
`24`
(P-384),
`25`
(P-521)
build_image.sh
→
deploy/
build_image.sh
View file @
2fe8932c
File moved
deploy/config.example.yaml
View file @
2fe8932c
...
...
@@ -210,6 +210,19 @@ gateway:
outbox_backlog_rebuild_rows
:
10000
# 全量重建周期(秒),0 表示禁用
full_rebuild_interval_seconds
:
300
# TLS fingerprint simulation / TLS 指纹伪装
# Default profile "claude_cli_v2" simulates Node.js 20.x
# 默认模板 "claude_cli_v2" 模拟 Node.js 20.x 指纹
tls_fingerprint
:
enabled
:
true
# profiles:
# profile_1:
# name: "Custom Profile 1"
# profile_2:
# name: "Custom Profile 2"
# cipher_suites: [4866, 4867, 4865, 49199, 49195, 49200, 49196]
# curves: [29, 23, 24]
# point_formats: [0]
# =============================================================================
# API Key Auth Cache Configuration
...
...
@@ -292,6 +305,27 @@ dashboard_aggregation:
# 日聚合保留天数
daily_days
:
730
# =============================================================================
# Usage Cleanup Task Configuration
# 使用记录清理任务配置(重启生效)
# =============================================================================
usage_cleanup
:
# Enable cleanup task worker
# 启用清理任务执行器
enabled
:
true
# Max date range (days) per task
# 单次任务最大时间跨度(天)
max_range_days
:
31
# Batch delete size
# 单批删除数量
batch_size
:
5000
# Worker interval (seconds)
# 执行器轮询间隔(秒)
worker_interval_seconds
:
10
# Task execution timeout (seconds)
# 单次任务最大执行时长(秒)
task_timeout_seconds
:
1800
# =============================================================================
# Concurrency Wait Configuration
# 并发等待配置
...
...
@@ -369,6 +403,21 @@ jwt:
# 令牌过期时间(小时,最大 24)
expire_hour
:
24
# =============================================================================
# TOTP (2FA) Configuration
# TOTP 双因素认证配置
# =============================================================================
totp
:
# IMPORTANT: Set a fixed encryption key for TOTP secrets.
# 重要:设置固定的 TOTP 加密密钥。
# If left empty, a random key will be generated on each startup, causing all
# existing TOTP configurations to become invalid (users won't be able to
# login with 2FA).
# 如果留空,每次启动将生成随机密钥,导致现有的 TOTP 配置失效(用户无法使用
# 双因素认证登录)。
# Generate with / 生成命令: openssl rand -hex 32
encryption_key
:
"
"
# =============================================================================
# LinuxDo Connect OAuth Login (SSO)
# LinuxDo Connect OAuth 登录(用于 Sub2API 用户登录)
...
...
deploy/docker-compose.yml
View file @
2fe8932c
...
...
@@ -79,6 +79,16 @@ services:
-
JWT_SECRET=${JWT_SECRET:-}
-
JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24}
# =======================================================================
# TOTP (2FA) Configuration
# =======================================================================
# IMPORTANT: Set a fixed encryption key for TOTP secrets. If left empty,
# a random key will be generated on each startup, causing all existing
# TOTP configurations to become invalid (users won't be able to login
# with 2FA).
# Generate a secure key: openssl rand -hex 32
-
TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:-}
# =======================================================================
# Timezone Configuration
# This affects ALL time operations in the application:
...
...
frontend/package-lock.json
View file @
2fe8932c
...
...
@@ -15,6 +15,7 @@
"driver.js"
:
"^1.4.0"
,
"file-saver"
:
"^2.0.5"
,
"pinia"
:
"^2.1.7"
,
"qrcode"
:
"^1.5.4"
,
"vue"
:
"^3.4.0"
,
"vue-chartjs"
:
"^5.3.0"
,
"vue-i18n"
:
"^9.14.5"
,
...
...
@@ -25,6 +26,7 @@
"@types/file-saver"
:
"^2.0.7"
,
"@types/mdx"
:
"^2.0.13"
,
"@types/node"
:
"^20.10.5"
,
"@types/qrcode"
:
"^1.5.6"
,
"@typescript-eslint/eslint-plugin"
:
"^7.18.0"
,
"@typescript-eslint/parser"
:
"^7.18.0"
,
"@vitejs/plugin-vue"
:
"^5.2.3"
,
...
...
@@ -1680,6 +1682,16 @@
"integrity"
:
"sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
,
"license"
:
"MIT"
},
"node_modules/@types/qrcode"
:
{
"version"
:
"1.5.6"
,
"resolved"
:
"https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.6.tgz"
,
"integrity"
:
"sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="
,
"dev"
:
true
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@types/node"
:
"*"
}
},
"node_modules/@types/web-bluetooth"
:
{
"version"
:
"0.0.20"
,
"resolved"
:
"https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz"
,
...
...
@@ -2354,7 +2366,6 @@
"version"
:
"5.0.1"
,
"resolved"
:
"https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
,
"integrity"
:
"sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
,
"dev"
:
true
,
"license"
:
"MIT"
,
"engines"
:
{
"node"
:
">=8"
...
...
@@ -2364,7 +2375,6 @@
"version"
:
"4.3.0"
,
"resolved"
:
"https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
,
"integrity"
:
"sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="
,
"dev"
:
true
,
"license"
:
"MIT"
,
"dependencies"
:
{
"color-convert"
:
"^2.0.1"
...
...
@@ -2646,6 +2656,15 @@
"node"
:
">=6"
}
},
"node_modules/camelcase"
:
{
"version"
:
"5.3.1"
,
"resolved"
:
"https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz"
,
"integrity"
:
"sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
,
"license"
:
"MIT"
,
"engines"
:
{
"node"
:
">=6"
}
},
"node_modules/camelcase-css"
:
{
"version"
:
"2.0.1"
,
"resolved"
:
"https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
,
...
...
@@ -2784,6 +2803,51 @@
"node"
:
">= 6"
}
},
"node_modules/cliui"
:
{
"version"
:
"6.0.0"
,
"resolved"
:
"https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz"
,
"integrity"
:
"sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="
,
"license"
:
"ISC"
,
"dependencies"
:
{
"string-width"
:
"^4.2.0"
,
"strip-ansi"
:
"^6.0.0"
,
"wrap-ansi"
:
"^6.2.0"
}
},
"node_modules/cliui/node_modules/emoji-regex"
:
{
"version"
:
"8.0.0"
,
"resolved"
:
"https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz"
,
"integrity"
:
"sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
,
"license"
:
"MIT"
},
"node_modules/cliui/node_modules/string-width"
:
{
"version"
:
"4.2.3"
,
"resolved"
:
"https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
,
"integrity"
:
"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"emoji-regex"
:
"^8.0.0"
,
"is-fullwidth-code-point"
:
"^3.0.0"
,
"strip-ansi"
:
"^6.0.1"
},
"engines"
:
{
"node"
:
">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi"
:
{
"version"
:
"6.2.0"
,
"resolved"
:
"https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
,
"integrity"
:
"sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"ansi-styles"
:
"^4.0.0"
,
"string-width"
:
"^4.1.0"
,
"strip-ansi"
:
"^6.0.0"
},
"engines"
:
{
"node"
:
">=8"
}
},
"node_modules/clsx"
:
{
"version"
:
"2.1.1"
,
"resolved"
:
"https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
,
...
...
@@ -2806,7 +2870,6 @@
"version"
:
"2.0.1"
,
"resolved"
:
"https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
,
"integrity"
:
"sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="
,
"dev"
:
true
,
"license"
:
"MIT"
,
"dependencies"
:
{
"color-name"
:
"~1.1.4"
...
...
@@ -2819,7 +2882,6 @@
"version"
:
"1.1.4"
,
"resolved"
:
"https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
,
"integrity"
:
"sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
,
"dev"
:
true
,
"license"
:
"MIT"
},
"node_modules/combined-stream"
:
{
...
...
@@ -2989,6 +3051,15 @@
}
}
},
"node_modules/decamelize"
:
{
"version"
:
"1.2.0"
,
"resolved"
:
"https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz"
,
"integrity"
:
"sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="
,
"license"
:
"MIT"
,
"engines"
:
{
"node"
:
">=0.10.0"
}
},
"node_modules/decimal.js"
:
{
"version"
:
"10.6.0"
,
"resolved"
:
"https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz"
,
...
...
@@ -3029,6 +3100,12 @@
"dev"
:
true
,
"license"
:
"Apache-2.0"
},
"node_modules/dijkstrajs"
:
{
"version"
:
"1.0.3"
,
"resolved"
:
"https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz"
,
"integrity"
:
"sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
,
"license"
:
"MIT"
},
"node_modules/dir-glob"
:
{
"version"
:
"3.0.1"
,
"resolved"
:
"https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
,
...
...
@@ -3759,6 +3836,15 @@
"url"
:
"https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file"
:
{
"version"
:
"2.0.5"
,
"resolved"
:
"https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz"
,
"integrity"
:
"sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
,
"license"
:
"ISC"
,
"engines"
:
{
"node"
:
"6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic"
:
{
"version"
:
"1.3.0"
,
"resolved"
:
"https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
,
...
...
@@ -4156,7 +4242,6 @@
"version"
:
"3.0.0"
,
"resolved"
:
"https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
,
"integrity"
:
"sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
,
"dev"
:
true
,
"license"
:
"MIT"
,
"engines"
:
{
"node"
:
">=8"
...
...
@@ -4883,6 +4968,15 @@
"url"
:
"https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-try"
:
{
"version"
:
"2.2.0"
,
"resolved"
:
"https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz"
,
"integrity"
:
"sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
,
"license"
:
"MIT"
,
"engines"
:
{
"node"
:
">=6"
}
},
"node_modules/package-json-from-dist"
:
{
"version"
:
"1.0.1"
,
"resolved"
:
"https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz"
,
...
...
@@ -4957,7 +5051,6 @@
"version"
:
"4.0.0"
,
"resolved"
:
"https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
,
"integrity"
:
"sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
,
"dev"
:
true
,
"license"
:
"MIT"
,
"engines"
:
{
"node"
:
">=8"
...
...
@@ -5093,6 +5186,15 @@
"node"
:
">= 6"
}
},
"node_modules/pngjs"
:
{
"version"
:
"5.0.0"
,
"resolved"
:
"https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz"
,
"integrity"
:
"sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
,
"license"
:
"MIT"
,
"engines"
:
{
"node"
:
">=10.13.0"
}
},
"node_modules/polished"
:
{
"version"
:
"4.3.1"
,
"resolved"
:
"https://registry.npmjs.org/polished/-/polished-4.3.1.tgz"
,
...
...
@@ -5313,6 +5415,23 @@
"node"
:
">=6"
}
},
"node_modules/qrcode"
:
{
"version"
:
"1.5.4"
,
"resolved"
:
"https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz"
,
"integrity"
:
"sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"dijkstrajs"
:
"^1.0.1"
,
"pngjs"
:
"^5.0.0"
,
"yargs"
:
"^15.3.1"
},
"bin"
:
{
"qrcode"
:
"bin/qrcode"
},
"engines"
:
{
"node"
:
">=10.13.0"
}
},
"node_modules/querystringify"
:
{
"version"
:
"2.2.0"
,
"resolved"
:
"https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz"
,
...
...
@@ -5370,6 +5489,21 @@
"node"
:
">=8.10.0"
}
},
"node_modules/require-directory"
:
{
"version"
:
"2.1.1"
,
"resolved"
:
"https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz"
,
"integrity"
:
"sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
,
"license"
:
"MIT"
,
"engines"
:
{
"node"
:
">=0.10.0"
}
},
"node_modules/require-main-filename"
:
{
"version"
:
"2.0.0"
,
"resolved"
:
"https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz"
,
"integrity"
:
"sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
,
"license"
:
"ISC"
},
"node_modules/requires-port"
:
{
"version"
:
"1.0.0"
,
"resolved"
:
"https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz"
,
...
...
@@ -5543,6 +5677,12 @@
"node"
:
">=10"
}
},
"node_modules/set-blocking"
:
{
"version"
:
"2.0.0"
,
"resolved"
:
"https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz"
,
"integrity"
:
"sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
,
"license"
:
"ISC"
},
"node_modules/shebang-command"
:
{
"version"
:
"2.0.0"
,
"resolved"
:
"https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
,
...
...
@@ -5714,7 +5854,6 @@
"version"
:
"6.0.1"
,
"resolved"
:
"https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
,
"integrity"
:
"sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="
,
"dev"
:
true
,
"license"
:
"MIT"
,
"dependencies"
:
{
"ansi-regex"
:
"^5.0.1"
...
...
@@ -6715,6 +6854,12 @@
"node"
:
">= 8"
}
},
"node_modules/which-module"
:
{
"version"
:
"2.0.1"
,
"resolved"
:
"https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz"
,
"integrity"
:
"sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
,
"license"
:
"ISC"
},
"node_modules/why-is-node-running"
:
{
"version"
:
"2.3.0"
,
"resolved"
:
"https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz"
,
...
...
@@ -6928,6 +7073,12 @@
"dev"
:
true
,
"license"
:
"MIT"
},
"node_modules/y18n"
:
{
"version"
:
"4.0.3"
,
"resolved"
:
"https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz"
,
"integrity"
:
"sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
,
"license"
:
"ISC"
},
"node_modules/yaml"
:
{
"version"
:
"1.10.2"
,
"resolved"
:
"https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
,
...
...
@@ -6937,6 +7088,113 @@
"node"
:
">= 6"
}
},
"node_modules/yargs"
:
{
"version"
:
"15.4.1"
,
"resolved"
:
"https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz"
,
"integrity"
:
"sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"cliui"
:
"^6.0.0"
,
"decamelize"
:
"^1.2.0"
,
"find-up"
:
"^4.1.0"
,
"get-caller-file"
:
"^2.0.1"
,
"require-directory"
:
"^2.1.1"
,
"require-main-filename"
:
"^2.0.0"
,
"set-blocking"
:
"^2.0.0"
,
"string-width"
:
"^4.2.0"
,
"which-module"
:
"^2.0.0"
,
"y18n"
:
"^4.0.0"
,
"yargs-parser"
:
"^18.1.2"
},
"engines"
:
{
"node"
:
">=8"
}
},
"node_modules/yargs-parser"
:
{
"version"
:
"18.1.3"
,
"resolved"
:
"https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz"
,
"integrity"
:
"sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="
,
"license"
:
"ISC"
,
"dependencies"
:
{
"camelcase"
:
"^5.0.0"
,
"decamelize"
:
"^1.2.0"
},
"engines"
:
{
"node"
:
">=6"
}
},
"node_modules/yargs/node_modules/emoji-regex"
:
{
"version"
:
"8.0.0"
,
"resolved"
:
"https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz"
,
"integrity"
:
"sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
,
"license"
:
"MIT"
},
"node_modules/yargs/node_modules/find-up"
:
{
"version"
:
"4.1.0"
,
"resolved"
:
"https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz"
,
"integrity"
:
"sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"locate-path"
:
"^5.0.0"
,
"path-exists"
:
"^4.0.0"
},
"engines"
:
{
"node"
:
">=8"
}
},
"node_modules/yargs/node_modules/locate-path"
:
{
"version"
:
"5.0.0"
,
"resolved"
:
"https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz"
,
"integrity"
:
"sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"p-locate"
:
"^4.1.0"
},
"engines"
:
{
"node"
:
">=8"
}
},
"node_modules/yargs/node_modules/p-limit"
:
{
"version"
:
"2.3.0"
,
"resolved"
:
"https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz"
,
"integrity"
:
"sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"p-try"
:
"^2.0.0"
},
"engines"
:
{
"node"
:
">=6"
},
"funding"
:
{
"url"
:
"https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yargs/node_modules/p-locate"
:
{
"version"
:
"4.1.0"
,
"resolved"
:
"https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz"
,
"integrity"
:
"sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"p-limit"
:
"^2.2.0"
},
"engines"
:
{
"node"
:
">=8"
}
},
"node_modules/yargs/node_modules/string-width"
:
{
"version"
:
"4.2.3"
,
"resolved"
:
"https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
,
"integrity"
:
"sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"emoji-regex"
:
"^8.0.0"
,
"is-fullwidth-code-point"
:
"^3.0.0"
,
"strip-ansi"
:
"^6.0.1"
},
"engines"
:
{
"node"
:
">=8"
}
},
"node_modules/yocto-queue"
:
{
"version"
:
"0.1.0"
,
"resolved"
:
"https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
,
...
...
frontend/package.json
View file @
2fe8932c
...
...
@@ -22,6 +22,7 @@
"driver.js"
:
"^1.4.0"
,
"file-saver"
:
"^2.0.5"
,
"pinia"
:
"^2.1.7"
,
"qrcode"
:
"^1.5.4"
,
"vue"
:
"^3.4.0"
,
"vue-chartjs"
:
"^5.3.0"
,
"vue-i18n"
:
"^9.14.5"
,
...
...
@@ -32,6 +33,7 @@
"@types/file-saver"
:
"^2.0.7"
,
"@types/mdx"
:
"^2.0.13"
,
"@types/node"
:
"^20.10.5"
,
"@types/qrcode"
:
"^1.5.6"
,
"@typescript-eslint/eslint-plugin"
:
"^7.18.0"
,
"@typescript-eslint/parser"
:
"^7.18.0"
,
"@vitejs/plugin-vue"
:
"^5.2.3"
,
...
...
frontend/pnpm-lock.yaml
View file @
2fe8932c
...
...
@@ -29,6 +29,9 @@ importers:
pinia
:
specifier
:
^2.1.7
version
:
2.3.1(typescript@5.6.3)(vue@3.5.26(typescript@5.6.3))
qrcode
:
specifier
:
^1.5.4
version
:
1.5.4
vue
:
specifier
:
^3.4.0
version
:
3.5.26(typescript@5.6.3)
...
...
@@ -54,6 +57,9 @@ importers:
'
@types/node'
:
specifier
:
^20.10.5
version
:
20.19.27
'
@types/qrcode'
:
specifier
:
^1.5.6
version
:
1.5.6
'
@typescript-eslint/eslint-plugin'
:
specifier
:
^7.18.0
version
:
7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)
...
...
@@ -1239,56 +1245,67 @@ packages:
resolution
:
{
integrity
:
sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==
}
cpu
:
[
arm
]
os
:
[
linux
]
libc
:
[
glibc
]
'
@rollup/rollup-linux-arm-musleabihf@4.54.0'
:
resolution
:
{
integrity
:
sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==
}
cpu
:
[
arm
]
os
:
[
linux
]
libc
:
[
musl
]
'
@rollup/rollup-linux-arm64-gnu@4.54.0'
:
resolution
:
{
integrity
:
sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==
}
cpu
:
[
arm64
]
os
:
[
linux
]
libc
:
[
glibc
]
'
@rollup/rollup-linux-arm64-musl@4.54.0'
:
resolution
:
{
integrity
:
sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==
}
cpu
:
[
arm64
]
os
:
[
linux
]
libc
:
[
musl
]
'
@rollup/rollup-linux-loong64-gnu@4.54.0'
:
resolution
:
{
integrity
:
sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==
}
cpu
:
[
loong64
]
os
:
[
linux
]
libc
:
[
glibc
]
'
@rollup/rollup-linux-ppc64-gnu@4.54.0'
:
resolution
:
{
integrity
:
sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==
}
cpu
:
[
ppc64
]
os
:
[
linux
]
libc
:
[
glibc
]
'
@rollup/rollup-linux-riscv64-gnu@4.54.0'
:
resolution
:
{
integrity
:
sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==
}
cpu
:
[
riscv64
]
os
:
[
linux
]
libc
:
[
glibc
]
'
@rollup/rollup-linux-riscv64-musl@4.54.0'
:
resolution
:
{
integrity
:
sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==
}
cpu
:
[
riscv64
]
os
:
[
linux
]
libc
:
[
musl
]
'
@rollup/rollup-linux-s390x-gnu@4.54.0'
:
resolution
:
{
integrity
:
sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==
}
cpu
:
[
s390x
]
os
:
[
linux
]
libc
:
[
glibc
]
'
@rollup/rollup-linux-x64-gnu@4.54.0'
:
resolution
:
{
integrity
:
sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==
}
cpu
:
[
x64
]
os
:
[
linux
]
libc
:
[
glibc
]
'
@rollup/rollup-linux-x64-musl@4.54.0'
:
resolution
:
{
integrity
:
sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==
}
cpu
:
[
x64
]
os
:
[
linux
]
libc
:
[
musl
]
'
@rollup/rollup-openharmony-arm64@4.54.0'
:
resolution
:
{
integrity
:
sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==
}
...
...
@@ -1479,6 +1496,9 @@ packages:
'
@types/parse-json@4.0.2'
:
resolution
:
{
integrity
:
sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==
}
'
@types/qrcode@1.5.6'
:
resolution
:
{
integrity
:
sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==
}
'
@types/react@19.2.7'
:
resolution
:
{
integrity
:
sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==
}
...
...
@@ -1832,6 +1852,10 @@ packages:
resolution
:
{
integrity
:
sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
}
engines
:
{
node
:
'
>=
6'
}
camelcase@5.3.1
:
resolution
:
{
integrity
:
sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
}
engines
:
{
node
:
'
>=6'
}
caniuse-lite@1.0.30001761
:
resolution
:
{
integrity
:
sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==
}
...
...
@@ -1895,6 +1919,9 @@ packages:
classnames@2.5.1
:
resolution
:
{
integrity
:
sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
}
cliui@6.0.0
:
resolution
:
{
integrity
:
sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
}
clsx@1.2.1
:
resolution
:
{
integrity
:
sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
}
engines
:
{
node
:
'
>=6'
}
...
...
@@ -2164,6 +2191,10 @@ packages:
supports-color
:
optional
:
true
decamelize@1.2.0
:
resolution
:
{
integrity
:
sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
}
engines
:
{
node
:
'
>=0.10.0'
}
decimal.js@10.6.0
:
resolution
:
{
integrity
:
sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
}
...
...
@@ -2198,6 +2229,9 @@ packages:
didyoumean@1.2.2
:
resolution
:
{
integrity
:
sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
}
dijkstrajs@1.0.3
:
resolution
:
{
integrity
:
sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
}
dir-glob@3.0.1
:
resolution
:
{
integrity
:
sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
}
engines
:
{
node
:
'
>=8'
}
...
...
@@ -2424,6 +2458,10 @@ packages:
find-root@1.1.0
:
resolution
:
{
integrity
:
sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
}
find-up@4.1.0
:
resolution
:
{
integrity
:
sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
}
engines
:
{
node
:
'
>=8'
}
find-up@5.0.0
:
resolution
:
{
integrity
:
sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
}
engines
:
{
node
:
'
>=10'
}
...
...
@@ -2488,6 +2526,10 @@ packages:
function-bind@1.1.2
:
resolution
:
{
integrity
:
sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
}
get-caller-file@2.0.5
:
resolution
:
{
integrity
:
sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
}
engines
:
{
node
:
6.* || 8.* || >= 10.*
}
get-east-asian-width@1.4.0
:
resolution
:
{
integrity
:
sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==
}
engines
:
{
node
:
'
>=18'
}
...
...
@@ -2856,6 +2898,10 @@ packages:
lit@3.3.2
:
resolution
:
{
integrity
:
sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==
}
locate-path@5.0.0
:
resolution
:
{
integrity
:
sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
}
engines
:
{
node
:
'
>=8'
}
locate-path@6.0.0
:
resolution
:
{
integrity
:
sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
}
engines
:
{
node
:
'
>=10'
}
...
...
@@ -3239,14 +3285,26 @@ packages:
resolution
:
{
integrity
:
sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
}
engines
:
{
node
:
'
>=
0.8.0'
}
p-limit@2.3.0
:
resolution
:
{
integrity
:
sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
}
engines
:
{
node
:
'
>=6'
}
p-limit@3.1.0
:
resolution
:
{
integrity
:
sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
}
engines
:
{
node
:
'
>=10'
}
p-locate@4.1.0
:
resolution
:
{
integrity
:
sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
}
engines
:
{
node
:
'
>=8'
}
p-locate@5.0.0
:
resolution
:
{
integrity
:
sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
}
engines
:
{
node
:
'
>=10'
}
p-try@2.2.0
:
resolution
:
{
integrity
:
sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
}
engines
:
{
node
:
'
>=6'
}
package-json-from-dist@1.0.1
:
resolution
:
{
integrity
:
sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
}
...
...
@@ -3341,6 +3399,10 @@ packages:
pkg-types@1.3.1
:
resolution
:
{
integrity
:
sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==
}
pngjs@5.0.0
:
resolution
:
{
integrity
:
sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
}
engines
:
{
node
:
'
>=10.13.0'
}
points-on-curve@0.2.0
:
resolution
:
{
integrity
:
sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==
}
...
...
@@ -3421,6 +3483,11 @@ packages:
resolution
:
{
integrity
:
sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
}
engines
:
{
node
:
'
>=6'
}
qrcode@1.5.4
:
resolution
:
{
integrity
:
sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
}
engines
:
{
node
:
'
>=10.13.0'
}
hasBin
:
true
query-string@9.3.1
:
resolution
:
{
integrity
:
sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==
}
engines
:
{
node
:
'
>=18'
}
...
...
@@ -3664,6 +3731,13 @@ packages:
remark-stringify@11.0.0
:
resolution
:
{
integrity
:
sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==
}
require-directory@2.1.1
:
resolution
:
{
integrity
:
sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
}
engines
:
{
node
:
'
>=0.10.0'
}
require-main-filename@2.0.0
:
resolution
:
{
integrity
:
sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
}
requires-port@1.0.0
:
resolution
:
{
integrity
:
sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
}
...
...
@@ -3739,6 +3813,9 @@ packages:
engines
:
{
node
:
'
>=10'
}
hasBin
:
true
set-blocking@2.0.0
:
resolution
:
{
integrity
:
sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
}
set-value@2.0.1
:
resolution
:
{
integrity
:
sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
}
engines
:
{
node
:
'
>=0.10.0'
}
...
...
@@ -4263,6 +4340,9 @@ packages:
resolution
:
{
integrity
:
sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==
}
engines
:
{
node
:
'
>=18'
}
which-module@2.0.1
:
resolution
:
{
integrity
:
sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
}
which@2.0.2
:
resolution
:
{
integrity
:
sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
}
engines
:
{
node
:
'
>=
8'
}
...
...
@@ -4285,6 +4365,10 @@ packages:
resolution
:
{
integrity
:
sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
}
engines
:
{
node
:
'
>=0.8'
}
wrap-ansi@6.2.0
:
resolution
:
{
integrity
:
sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
}
engines
:
{
node
:
'
>=8'
}
wrap-ansi@7.0.0
:
resolution
:
{
integrity
:
sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
}
engines
:
{
node
:
'
>=10'
}
...
...
@@ -4324,10 +4408,21 @@ packages:
xmlchars@2.2.0
:
resolution
:
{
integrity
:
sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
}
y18n@4.0.3
:
resolution
:
{
integrity
:
sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
}
yaml@1.10.2
:
resolution
:
{
integrity
:
sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
}
engines
:
{
node
:
'
>=
6'
}
yargs-parser@18.1.3
:
resolution
:
{
integrity
:
sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
}
engines
:
{
node
:
'
>=6'
}
yargs@15.4.1
:
resolution
:
{
integrity
:
sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
}
engines
:
{
node
:
'
>=8'
}
yocto-queue@0.1.0
:
resolution
:
{
integrity
:
sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
}
engines
:
{
node
:
'
>=10'
}
...
...
@@ -5838,6 +5933,10 @@ snapshots:
'
@types/parse-json@4.0.2'
:
{}
'
@types/qrcode@1.5.6'
:
dependencies
:
'
@types/node'
:
20.19.27
'
@types/react@19.2.7'
:
dependencies
:
csstype
:
3.2.3
...
...
@@ -6321,6 +6420,8 @@ snapshots:
camelcase-css@2.0.1
:
{}
camelcase@5.3.1
:
{}
caniuse-lite@1.0.30001761
:
{}
ccount@2.0.1
:
{}
...
...
@@ -6395,6 +6496,12 @@ snapshots:
classnames@2.5.1
:
{}
cliui@6.0.0
:
dependencies
:
string-width
:
4.2.3
strip-ansi
:
6.0.1
wrap-ansi
:
6.2.0
clsx@1.2.1
:
{}
clsx@2.1.1
:
{}
...
...
@@ -6668,6 +6775,8 @@ snapshots:
dependencies
:
ms
:
2.1.3
decamelize@1.2.0
:
{}
decimal.js@10.6.0
:
{}
decode-named-character-reference@1.2.0
:
...
...
@@ -6694,6 +6803,8 @@ snapshots:
didyoumean@1.2.2
:
{}
dijkstrajs@1.0.3
:
{}
dir-glob@3.0.1
:
dependencies
:
path-type
:
4.0.0
...
...
@@ -6978,6 +7089,11 @@ snapshots:
find-root@1.1.0
:
{}
find-up@4.1.0
:
dependencies
:
locate-path
:
5.0.0
path-exists
:
4.0.0
find-up@5.0.0
:
dependencies
:
locate-path
:
6.0.0
...
...
@@ -7029,6 +7145,8 @@ snapshots:
function-bind@1.1.2
:
{}
get-caller-file@2.0.5
:
{}
get-east-asian-width@1.4.0
:
{}
get-intrinsic@1.3.0
:
...
...
@@ -7521,6 +7639,10 @@ snapshots:
lit-element
:
4.2.2
lit-html
:
3.3.2
locate-path@5.0.0
:
dependencies
:
p-locate
:
4.1.0
locate-path@6.0.0
:
dependencies
:
p-locate
:
5.0.0
...
...
@@ -8194,14 +8316,24 @@ snapshots:
type-check
:
0.4.0
word-wrap
:
1.2.5
p-limit@2.3.0
:
dependencies
:
p-try
:
2.2.0
p-limit@3.1.0
:
dependencies
:
yocto-queue
:
0.1.0
p-locate@4.1.0
:
dependencies
:
p-limit
:
2.3.0
p-locate@5.0.0
:
dependencies
:
p-limit
:
3.1.0
p-try@2.2.0
:
{}
package-json-from-dist@1.0.1
:
{}
package-manager-detector@1.6.0
:
{}
...
...
@@ -8284,6 +8416,8 @@ snapshots:
mlly
:
1.8.0
pathe
:
2.0.3
pngjs@5.0.0
:
{}
points-on-curve@0.2.0
:
{}
points-on-path@0.2.1
:
...
...
@@ -8352,6 +8486,12 @@ snapshots:
punycode@2.3.1
:
{}
qrcode@1.5.4
:
dependencies
:
dijkstrajs
:
1.0.3
pngjs
:
5.0.0
yargs
:
15.4.1
query-string@9.3.1
:
dependencies
:
decode-uri-component
:
0.4.1
...
...
@@ -8703,6 +8843,10 @@ snapshots:
mdast-util-to-markdown
:
2.1.2
unified
:
11.0.5
require-directory@2.1.1
:
{}
require-main-filename@2.0.0
:
{}
requires-port@1.0.0
:
{}
reselect@5.1.1
:
{}
...
...
@@ -8788,6 +8932,8 @@ snapshots:
semver@7.7.3
:
{}
set-blocking@2.0.0
:
{}
set-value@2.0.1
:
dependencies
:
extend-shallow
:
2.0.1
...
...
@@ -9298,6 +9444,8 @@ snapshots:
tr46
:
5.1.1
webidl-conversions
:
7.0.0
which-module@2.0.1
:
{}
which@2.0.2
:
dependencies
:
isexe
:
2.0.0
...
...
@@ -9313,6 +9461,12 @@ snapshots:
word@0.3.0
:
{}
wrap-ansi@6.2.0
:
dependencies
:
ansi-styles
:
4.3.0
string-width
:
4.2.3
strip-ansi
:
6.0.1
wrap-ansi@7.0.0
:
dependencies
:
ansi-styles
:
4.3.0
...
...
@@ -9345,8 +9499,29 @@ snapshots:
xmlchars@2.2.0
:
{}
y18n@4.0.3
:
{}
yaml@1.10.2
:
{}
yargs-parser@18.1.3
:
dependencies
:
camelcase
:
5.3.1
decamelize
:
1.2.0
yargs@15.4.1
:
dependencies
:
cliui
:
6.0.0
decamelize
:
1.2.0
find-up
:
4.1.0
get-caller-file
:
2.0.5
require-directory
:
2.1.1
require-main-filename
:
2.0.0
set-blocking
:
2.0.0
string-width
:
4.2.3
which-module
:
2.0.1
y18n
:
4.0.3
yargs-parser
:
18.1.3
yocto-queue@0.1.0
:
{}
zustand@3.7.2(react@19.2.3)
:
...
...
frontend/src/api/admin/dashboard.ts
View file @
2fe8932c
...
...
@@ -50,6 +50,7 @@ export interface TrendParams {
account_id
?:
number
group_id
?:
number
stream
?:
boolean
billing_type
?:
number
|
null
}
export
interface
TrendResponse
{
...
...
@@ -78,6 +79,7 @@ export interface ModelStatsParams {
account_id
?:
number
group_id
?:
number
stream
?:
boolean
billing_type
?:
number
|
null
}
export
interface
ModelStatsResponse
{
...
...
frontend/src/api/admin/groups.ts
View file @
2fe8932c
...
...
@@ -5,7 +5,7 @@
import
{
apiClient
}
from
'
../client
'
import
type
{
Group
,
Admin
Group
,
GroupPlatform
,
CreateGroupRequest
,
UpdateGroupRequest
,
...
...
@@ -31,8 +31,8 @@ export async function list(
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
Group
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Group
>>
(
'
/admin/groups
'
,
{
):
Promise
<
PaginatedResponse
<
Admin
Group
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Admin
Group
>>
(
'
/admin/groups
'
,
{
params
:
{
page
,
page_size
:
pageSize
,
...
...
@@ -48,8 +48,8 @@ export async function list(
* @param platform - Optional platform filter
* @returns List of all active groups
*/
export
async
function
getAll
(
platform
?:
GroupPlatform
):
Promise
<
Group
[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
Group
[]
>
(
'
/admin/groups/all
'
,
{
export
async
function
getAll
(
platform
?:
GroupPlatform
):
Promise
<
Admin
Group
[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
Admin
Group
[]
>
(
'
/admin/groups/all
'
,
{
params
:
platform
?
{
platform
}
:
undefined
})
return
data
...
...
@@ -60,7 +60,7 @@ export async function getAll(platform?: GroupPlatform): Promise<Group[]> {
* @param platform - Platform to filter by
* @returns List of groups for the specified platform
*/
export
async
function
getByPlatform
(
platform
:
GroupPlatform
):
Promise
<
Group
[]
>
{
export
async
function
getByPlatform
(
platform
:
GroupPlatform
):
Promise
<
Admin
Group
[]
>
{
return
getAll
(
platform
)
}
...
...
@@ -69,8 +69,8 @@ export async function getByPlatform(platform: GroupPlatform): Promise<Group[]> {
* @param id - Group ID
* @returns Group details
*/
export
async
function
getById
(
id
:
number
):
Promise
<
Group
>
{
const
{
data
}
=
await
apiClient
.
get
<
Group
>
(
`/admin/groups/
${
id
}
`
)
export
async
function
getById
(
id
:
number
):
Promise
<
Admin
Group
>
{
const
{
data
}
=
await
apiClient
.
get
<
Admin
Group
>
(
`/admin/groups/
${
id
}
`
)
return
data
}
...
...
@@ -79,8 +79,8 @@ export async function getById(id: number): Promise<Group> {
* @param groupData - Group data
* @returns Created group
*/
export
async
function
create
(
groupData
:
CreateGroupRequest
):
Promise
<
Group
>
{
const
{
data
}
=
await
apiClient
.
post
<
Group
>
(
'
/admin/groups
'
,
groupData
)
export
async
function
create
(
groupData
:
CreateGroupRequest
):
Promise
<
Admin
Group
>
{
const
{
data
}
=
await
apiClient
.
post
<
Admin
Group
>
(
'
/admin/groups
'
,
groupData
)
return
data
}
...
...
@@ -90,8 +90,8 @@ export async function create(groupData: CreateGroupRequest): Promise<Group> {
* @param updates - Fields to update
* @returns Updated group
*/
export
async
function
update
(
id
:
number
,
updates
:
UpdateGroupRequest
):
Promise
<
Group
>
{
const
{
data
}
=
await
apiClient
.
put
<
Group
>
(
`/admin/groups/
${
id
}
`
,
updates
)
export
async
function
update
(
id
:
number
,
updates
:
UpdateGroupRequest
):
Promise
<
Admin
Group
>
{
const
{
data
}
=
await
apiClient
.
put
<
Admin
Group
>
(
`/admin/groups/
${
id
}
`
,
updates
)
return
data
}
...
...
@@ -111,7 +111,7 @@ export async function deleteGroup(id: number): Promise<{ message: string }> {
* @param status - New status
* @returns Updated group
*/
export
async
function
toggleStatus
(
id
:
number
,
status
:
'
active
'
|
'
inactive
'
):
Promise
<
Group
>
{
export
async
function
toggleStatus
(
id
:
number
,
status
:
'
active
'
|
'
inactive
'
):
Promise
<
Admin
Group
>
{
return
update
(
id
,
{
status
})
}
...
...
frontend/src/api/admin/settings.ts
View file @
2fe8932c
...
...
@@ -12,6 +12,10 @@ export interface SystemSettings {
// Registration settings
registration_enabled
:
boolean
email_verify_enabled
:
boolean
promo_code_enabled
:
boolean
password_reset_enabled
:
boolean
totp_enabled
:
boolean
// TOTP 双因素认证
totp_encryption_key_configured
:
boolean
// TOTP 加密密钥是否已配置
// Default settings
default_balance
:
number
default_concurrency
:
number
...
...
@@ -23,6 +27,9 @@ export interface SystemSettings {
contact_info
:
string
doc_url
:
string
home_content
:
string
hide_ccs_import_button
:
boolean
purchase_subscription_enabled
:
boolean
purchase_subscription_url
:
string
// SMTP settings
smtp_host
:
string
smtp_port
:
number
...
...
@@ -63,6 +70,9 @@ export interface SystemSettings {
export
interface
UpdateSettingsRequest
{
registration_enabled
?:
boolean
email_verify_enabled
?:
boolean
promo_code_enabled
?:
boolean
password_reset_enabled
?:
boolean
totp_enabled
?:
boolean
// TOTP 双因素认证
default_balance
?:
number
default_concurrency
?:
number
site_name
?:
string
...
...
@@ -72,6 +82,9 @@ export interface UpdateSettingsRequest {
contact_info
?:
string
doc_url
?:
string
home_content
?:
string
hide_ccs_import_button
?:
boolean
purchase_subscription_enabled
?:
boolean
purchase_subscription_url
?:
string
smtp_host
?:
string
smtp_port
?:
number
smtp_username
?:
string
...
...
frontend/src/api/admin/subscriptions.ts
View file @
2fe8932c
...
...
@@ -17,7 +17,7 @@ import type {
* List all subscriptions with pagination
* @param page - Page number (default: 1)
* @param pageSize - Items per page (default: 20)
* @param filters - Optional filters (status, user_id, group_id)
* @param filters - Optional filters (status, user_id, group_id
, sort_by, sort_order
)
* @returns Paginated list of subscriptions
*/
export
async
function
list
(
...
...
@@ -27,6 +27,8 @@ export async function list(
status
?:
'
active
'
|
'
expired
'
|
'
revoked
'
user_id
?:
number
group_id
?:
number
sort_by
?:
string
sort_order
?:
'
asc
'
|
'
desc
'
},
options
?:
{
signal
?:
AbortSignal
...
...
frontend/src/api/admin/usage.ts
View file @
2fe8932c
...
...
@@ -4,7 +4,7 @@
*/
import
{
apiClient
}
from
'
../client
'
import
type
{
UsageLog
,
UsageQueryParams
,
PaginatedResponse
}
from
'
@/types
'
import
type
{
Admin
UsageLog
,
UsageQueryParams
,
PaginatedResponse
}
from
'
@/types
'
// ==================== Types ====================
...
...
@@ -31,6 +31,46 @@ export interface SimpleApiKey {
user_id
:
number
}
export
interface
UsageCleanupFilters
{
start_time
:
string
end_time
:
string
user_id
?:
number
api_key_id
?:
number
account_id
?:
number
group_id
?:
number
model
?:
string
|
null
stream
?:
boolean
|
null
billing_type
?:
number
|
null
}
export
interface
UsageCleanupTask
{
id
:
number
status
:
string
filters
:
UsageCleanupFilters
created_by
:
number
deleted_rows
:
number
error_message
?:
string
|
null
canceled_by
?:
number
|
null
canceled_at
?:
string
|
null
started_at
?:
string
|
null
finished_at
?:
string
|
null
created_at
:
string
updated_at
:
string
}
export
interface
CreateUsageCleanupTaskRequest
{
start_date
:
string
end_date
:
string
user_id
?:
number
api_key_id
?:
number
account_id
?:
number
group_id
?:
number
model
?:
string
|
null
stream
?:
boolean
|
null
billing_type
?:
number
|
null
timezone
?:
string
}
export
interface
AdminUsageQueryParams
extends
UsageQueryParams
{
user_id
?:
number
}
...
...
@@ -45,8 +85,8 @@ export interface AdminUsageQueryParams extends UsageQueryParams {
export
async
function
list
(
params
:
AdminUsageQueryParams
,
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
UsageLog
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
UsageLog
>>
(
'
/admin/usage
'
,
{
):
Promise
<
PaginatedResponse
<
Admin
UsageLog
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
Admin
UsageLog
>>
(
'
/admin/usage
'
,
{
params
,
signal
:
options
?.
signal
})
...
...
@@ -108,11 +148,51 @@ export async function searchApiKeys(userId?: number, keyword?: string): Promise<
return
data
}
/**
* List usage cleanup tasks (admin only)
* @param params - Query parameters for pagination
* @returns Paginated list of cleanup tasks
*/
export
async
function
listCleanupTasks
(
params
:
{
page
?:
number
;
page_size
?:
number
},
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
UsageCleanupTask
>>
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
UsageCleanupTask
>>
(
'
/admin/usage/cleanup-tasks
'
,
{
params
,
signal
:
options
?.
signal
})
return
data
}
/**
* Create a usage cleanup task (admin only)
* @param payload - Cleanup task parameters
* @returns Created cleanup task
*/
export
async
function
createCleanupTask
(
payload
:
CreateUsageCleanupTaskRequest
):
Promise
<
UsageCleanupTask
>
{
const
{
data
}
=
await
apiClient
.
post
<
UsageCleanupTask
>
(
'
/admin/usage/cleanup-tasks
'
,
payload
)
return
data
}
/**
* Cancel a usage cleanup task (admin only)
* @param taskId - Task ID to cancel
*/
export
async
function
cancelCleanupTask
(
taskId
:
number
):
Promise
<
{
id
:
number
;
status
:
string
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
id
:
number
;
status
:
string
}
>
(
`/admin/usage/cleanup-tasks/
${
taskId
}
/cancel`
)
return
data
}
export
const
adminUsageAPI
=
{
list
,
getStats
,
searchUsers
,
searchApiKeys
searchApiKeys
,
listCleanupTasks
,
createCleanupTask
,
cancelCleanupTask
}
export
default
adminUsageAPI
frontend/src/api/admin/users.ts
View file @
2fe8932c
...
...
@@ -4,7 +4,7 @@
*/
import
{
apiClient
}
from
'
../client
'
import
type
{
User
,
UpdateUserRequest
,
PaginatedResponse
}
from
'
@/types
'
import
type
{
Admin
User
,
UpdateUserRequest
,
PaginatedResponse
}
from
'
@/types
'
/**
* List all users with pagination
...
...
@@ -26,7 +26,7 @@ export async function list(
options
?:
{
signal
?:
AbortSignal
}
):
Promise
<
PaginatedResponse
<
User
>>
{
):
Promise
<
PaginatedResponse
<
Admin
User
>>
{
// Build params with attribute filters in attr[id]=value format
const
params
:
Record
<
string
,
any
>
=
{
page
,
...
...
@@ -44,8 +44,7 @@ export async function list(
}
}
}
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
User
>>
(
'
/admin/users
'
,
{
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
AdminUser
>>
(
'
/admin/users
'
,
{
params
,
signal
:
options
?.
signal
})
...
...
@@ -57,8 +56,8 @@ export async function list(
* @param id - User ID
* @returns User details
*/
export
async
function
getById
(
id
:
number
):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
get
<
User
>
(
`/admin/users/
${
id
}
`
)
export
async
function
getById
(
id
:
number
):
Promise
<
Admin
User
>
{
const
{
data
}
=
await
apiClient
.
get
<
Admin
User
>
(
`/admin/users/
${
id
}
`
)
return
data
}
...
...
@@ -73,8 +72,8 @@ export async function create(userData: {
balance
?:
number
concurrency
?:
number
allowed_groups
?:
number
[]
|
null
}):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
post
<
User
>
(
'
/admin/users
'
,
userData
)
}):
Promise
<
Admin
User
>
{
const
{
data
}
=
await
apiClient
.
post
<
Admin
User
>
(
'
/admin/users
'
,
userData
)
return
data
}
...
...
@@ -84,8 +83,8 @@ export async function create(userData: {
* @param updates - Fields to update
* @returns Updated user
*/
export
async
function
update
(
id
:
number
,
updates
:
UpdateUserRequest
):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
put
<
User
>
(
`/admin/users/
${
id
}
`
,
updates
)
export
async
function
update
(
id
:
number
,
updates
:
UpdateUserRequest
):
Promise
<
Admin
User
>
{
const
{
data
}
=
await
apiClient
.
put
<
Admin
User
>
(
`/admin/users/
${
id
}
`
,
updates
)
return
data
}
...
...
@@ -112,8 +111,8 @@ export async function updateBalance(
balance
:
number
,
operation
:
'
set
'
|
'
add
'
|
'
subtract
'
=
'
set
'
,
notes
?:
string
):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
post
<
User
>
(
`/admin/users/
${
id
}
/balance`
,
{
):
Promise
<
Admin
User
>
{
const
{
data
}
=
await
apiClient
.
post
<
Admin
User
>
(
`/admin/users/
${
id
}
/balance`
,
{
balance
,
operation
,
notes
:
notes
||
''
...
...
@@ -127,7 +126,7 @@ export async function updateBalance(
* @param concurrency - New concurrency limit
* @returns Updated user
*/
export
async
function
updateConcurrency
(
id
:
number
,
concurrency
:
number
):
Promise
<
User
>
{
export
async
function
updateConcurrency
(
id
:
number
,
concurrency
:
number
):
Promise
<
Admin
User
>
{
return
update
(
id
,
{
concurrency
})
}
...
...
@@ -137,7 +136,7 @@ export async function updateConcurrency(id: number, concurrency: number): Promis
* @param status - New status
* @returns Updated user
*/
export
async
function
toggleStatus
(
id
:
number
,
status
:
'
active
'
|
'
disabled
'
):
Promise
<
User
>
{
export
async
function
toggleStatus
(
id
:
number
,
status
:
'
active
'
|
'
disabled
'
):
Promise
<
Admin
User
>
{
return
update
(
id
,
{
status
})
}
...
...
frontend/src/api/auth.ts
View file @
2fe8932c
...
...
@@ -11,9 +11,23 @@ import type {
CurrentUserResponse
,
SendVerifyCodeRequest
,
SendVerifyCodeResponse
,
PublicSettings
PublicSettings
,
TotpLoginResponse
,
TotpLogin2FARequest
}
from
'
@/types
'
/**
* Login response type - can be either full auth or 2FA required
*/
export
type
LoginResponse
=
AuthResponse
|
TotpLoginResponse
/**
* Type guard to check if login response requires 2FA
*/
export
function
isTotp2FARequired
(
response
:
LoginResponse
):
response
is
TotpLoginResponse
{
return
'
requires_2fa
'
in
response
&&
response
.
requires_2fa
===
true
}
/**
* Store authentication token in localStorage
*/
...
...
@@ -38,11 +52,28 @@ export function clearAuthToken(): void {
/**
* User login
* @param credentials - Username and password
* @param credentials - Email and password
* @returns Authentication response with token and user data, or 2FA required response
*/
export
async
function
login
(
credentials
:
LoginRequest
):
Promise
<
LoginResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
LoginResponse
>
(
'
/auth/login
'
,
credentials
)
// Only store token if 2FA is not required
if
(
!
isTotp2FARequired
(
data
))
{
setAuthToken
(
data
.
access_token
)
localStorage
.
setItem
(
'
auth_user
'
,
JSON
.
stringify
(
data
.
user
))
}
return
data
}
/**
* Complete login with 2FA code
* @param request - Temp token and TOTP code
* @returns Authentication response with token and user data
*/
export
async
function
login
(
credentials
:
LoginRequest
):
Promise
<
AuthResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
AuthResponse
>
(
'
/auth/login
'
,
c
re
dentials
)
export
async
function
login
2FA
(
request
:
Totp
Login
2FA
Request
):
Promise
<
AuthResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
AuthResponse
>
(
'
/auth/login
/2fa
'
,
re
quest
)
// Store token and user data
setAuthToken
(
data
.
access_token
)
...
...
@@ -133,8 +164,61 @@ export async function validatePromoCode(code: string): Promise<ValidatePromoCode
return
data
}
/**
* Forgot password request
*/
export
interface
ForgotPasswordRequest
{
email
:
string
turnstile_token
?:
string
}
/**
* Forgot password response
*/
export
interface
ForgotPasswordResponse
{
message
:
string
}
/**
* Request password reset link
* @param request - Email and optional Turnstile token
* @returns Response with message
*/
export
async
function
forgotPassword
(
request
:
ForgotPasswordRequest
):
Promise
<
ForgotPasswordResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
ForgotPasswordResponse
>
(
'
/auth/forgot-password
'
,
request
)
return
data
}
/**
* Reset password request
*/
export
interface
ResetPasswordRequest
{
email
:
string
token
:
string
new_password
:
string
}
/**
* Reset password response
*/
export
interface
ResetPasswordResponse
{
message
:
string
}
/**
* Reset password with token
* @param request - Email, token, and new password
* @returns Response with message
*/
export
async
function
resetPassword
(
request
:
ResetPasswordRequest
):
Promise
<
ResetPasswordResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
ResetPasswordResponse
>
(
'
/auth/reset-password
'
,
request
)
return
data
}
export
const
authAPI
=
{
login
,
login2FA
,
isTotp2FARequired
,
register
,
getCurrentUser
,
logout
,
...
...
@@ -144,7 +228,9 @@ export const authAPI = {
clearAuthToken
,
getPublicSettings
,
sendVerifyCode
,
validatePromoCode
validatePromoCode
,
forgotPassword
,
resetPassword
}
export
default
authAPI
frontend/src/api/index.ts
View file @
2fe8932c
...
...
@@ -7,7 +7,7 @@
export
{
apiClient
}
from
'
./client
'
// Auth API
export
{
authAPI
}
from
'
./auth
'
export
{
authAPI
,
isTotp2FARequired
,
type
LoginResponse
}
from
'
./auth
'
// User APIs
export
{
keysAPI
}
from
'
./keys
'
...
...
@@ -15,6 +15,7 @@ export { usageAPI } from './usage'
export
{
userAPI
}
from
'
./user
'
export
{
redeemAPI
,
type
RedeemHistoryItem
}
from
'
./redeem
'
export
{
userGroupsAPI
}
from
'
./groups
'
export
{
totpAPI
}
from
'
./totp
'
// Admin APIs
export
{
adminAPI
}
from
'
./admin
'
...
...
frontend/src/api/totp.ts
0 → 100644
View file @
2fe8932c
/**
* TOTP (2FA) API endpoints
* Handles Two-Factor Authentication with Google Authenticator
*/
import
{
apiClient
}
from
'
./client
'
import
type
{
TotpStatus
,
TotpSetupRequest
,
TotpSetupResponse
,
TotpEnableRequest
,
TotpEnableResponse
,
TotpDisableRequest
,
TotpVerificationMethod
}
from
'
@/types
'
/**
* Get TOTP status for current user
* @returns TOTP status including enabled state and feature availability
*/
export
async
function
getStatus
():
Promise
<
TotpStatus
>
{
const
{
data
}
=
await
apiClient
.
get
<
TotpStatus
>
(
'
/user/totp/status
'
)
return
data
}
/**
* Get verification method for TOTP operations
* @returns Method ('email' or 'password') required for setup/disable
*/
export
async
function
getVerificationMethod
():
Promise
<
TotpVerificationMethod
>
{
const
{
data
}
=
await
apiClient
.
get
<
TotpVerificationMethod
>
(
'
/user/totp/verification-method
'
)
return
data
}
/**
* Send email verification code for TOTP operations
* @returns Success response
*/
export
async
function
sendVerifyCode
():
Promise
<
{
success
:
boolean
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
success
:
boolean
}
>
(
'
/user/totp/send-code
'
)
return
data
}
/**
* Initiate TOTP setup - generates secret and QR code
* @param request - Email code or password depending on verification method
* @returns Setup response with secret, QR code URL, and setup token
*/
export
async
function
initiateSetup
(
request
?:
TotpSetupRequest
):
Promise
<
TotpSetupResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
TotpSetupResponse
>
(
'
/user/totp/setup
'
,
request
||
{})
return
data
}
/**
* Complete TOTP setup by verifying the code
* @param request - TOTP code and setup token
* @returns Enable response with success status and enabled timestamp
*/
export
async
function
enable
(
request
:
TotpEnableRequest
):
Promise
<
TotpEnableResponse
>
{
const
{
data
}
=
await
apiClient
.
post
<
TotpEnableResponse
>
(
'
/user/totp/enable
'
,
request
)
return
data
}
/**
* Disable TOTP for current user
* @param request - Email code or password depending on verification method
* @returns Success response
*/
export
async
function
disable
(
request
:
TotpDisableRequest
):
Promise
<
{
success
:
boolean
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
success
:
boolean
}
>
(
'
/user/totp/disable
'
,
request
)
return
data
}
export
const
totpAPI
=
{
getStatus
,
getVerificationMethod
,
sendVerifyCode
,
initiateSetup
,
enable
,
disable
}
export
default
totpAPI
frontend/src/components/account/AccountStatusIndicator.vue
View file @
2fe8932c
<
template
>
<div
class=
"flex items-center gap-2"
>
<!-- Main Status Badge -->
<button
v-if=
"isTempUnschedulable"
type=
"button"
:class=
"['badge text-xs', statusClass, 'cursor-pointer']"
:title=
"t('admin.accounts.status.viewTempUnschedDetails')"
@
click=
"handleTempUnschedClick"
>
{{
statusText
}}
</button>
<span
v-else
:class=
"['badge text-xs', statusClass]"
>
{{
statusText
}}
</span>
<!-- Rate Limit Display (429) - Two-line layout -->
<div
v-if=
"isRateLimited"
class=
"flex flex-col items-center gap-1"
>
<span
class=
"badge text-xs badge-warning"
>
{{
t
(
'
admin.accounts.status.rateLimited
'
)
}}
</span>
<span
class=
"text-[11px] text-gray-400 dark:text-gray-500"
>
{{
rateLimitCountdown
}}
</span>
</div>
<!-- Overload Display (529) - Two-line layout -->
<div
v-else-if=
"isOverloaded"
class=
"flex flex-col items-center gap-1"
>
<span
class=
"badge text-xs badge-danger"
>
{{
t
(
'
admin.accounts.status.overloaded
'
)
}}
</span>
<span
class=
"text-[11px] text-gray-400 dark:text-gray-500"
>
{{
overloadCountdown
}}
</span>
</div>
<!-- Main Status Badge (shown when not rate limited/overloaded) -->
<template
v-else
>
<button
v-if=
"isTempUnschedulable"
type=
"button"
:class=
"['badge text-xs', statusClass, 'cursor-pointer']"
:title=
"t('admin.accounts.status.viewTempUnschedDetails')"
@
click=
"handleTempUnschedClick"
>
{{
statusText
}}
</button>
<span
v-else
:class=
"['badge text-xs', statusClass]"
>
{{
statusText
}}
</span>
</
template
>
<!-- Error Info Indicator -->
<div
v-if=
"hasError && account.error_message"
class=
"group/error relative"
>
...
...
@@ -42,44 +56,6 @@
></div>
</div>
</div>
<!-- Rate Limit Indicator (429) -->
<div
v-if=
"isRateLimited"
class=
"group relative"
>
<span
class=
"inline-flex items-center gap-1 rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
>
<Icon
name=
"exclamationTriangle"
size=
"xs"
:stroke-width=
"2"
/>
429
</span>
<!-- Tooltip -->
<div
class=
"pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
>
{{
t
(
'
admin.accounts.status.rateLimitedUntil
'
,
{
time
:
formatTime
(
account
.
rate_limit_reset_at
)
}
)
}}
<
div
class
=
"
absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700
"
><
/div
>
<
/div
>
<
/div
>
<!--
Overload
Indicator
(
529
)
-->
<
div
v
-
if
=
"
isOverloaded
"
class
=
"
group relative
"
>
<
span
class
=
"
inline-flex items-center gap-1 rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400
"
>
<
Icon
name
=
"
exclamationTriangle
"
size
=
"
xs
"
:
stroke
-
width
=
"
2
"
/>
529
<
/span
>
<!--
Tooltip
-->
<
div
class
=
"
pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700
"
>
{{
t
(
'
admin.accounts.status.overloadedUntil
'
,
{
time
:
formatTime
(
account
.
overload_until
)
}
)
}}
<
div
class
=
"
absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700
"
><
/div
>
<
/div
>
<
/div
>
</div>
</template>
...
...
@@ -87,8 +63,7 @@
import
{
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
type
{
Account
}
from
'
@/types
'
import
{
formatTime
}
from
'
@/utils/format
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
formatCountdownWithSuffix
}
from
'
@/utils/format
'
const
{
t
}
=
useI18n
()
...
...
@@ -123,6 +98,16 @@ const hasError = computed(() => {
return
props
.
account
.
status
===
'
error
'
})
// Computed: countdown text for rate limit (429)
const
rateLimitCountdown
=
computed
(()
=>
{
return
formatCountdownWithSuffix
(
props
.
account
.
rate_limit_reset_at
)
})
// Computed: countdown text for overload (529)
const
overloadCountdown
=
computed
(()
=>
{
return
formatCountdownWithSuffix
(
props
.
account
.
overload_until
)
})
// Computed: status badge class
const
statusClass
=
computed
(()
=>
{
if
(
hasError
.
value
)
{
...
...
@@ -131,7 +116,7 @@ const statusClass = computed(() => {
if
(
isTempUnschedulable
.
value
)
{
return
'
badge-warning
'
}
if
(
!
props
.
account
.
schedulable
||
isRateLimited
.
value
||
isOverloaded
.
value
)
{
if
(
!
props
.
account
.
schedulable
)
{
return
'
badge-gray
'
}
switch
(
props
.
account
.
status
)
{
...
...
@@ -157,9 +142,6 @@ const statusText = computed(() => {
if
(
!
props
.
account
.
schedulable
)
{
return
t
(
'
admin.accounts.status.paused
'
)
}
if
(
isRateLimited
.
value
||
isOverloaded
.
value
)
{
return
t
(
'
admin.accounts.status.limited
'
)
}
return
t
(
`admin.accounts.status.
${
props
.
account
.
status
}
`
)
})
...
...
@@ -167,5 +149,4 @@ const handleTempUnschedClick = () => {
if
(
!
isTempUnschedulable
.
value
)
return
emit
(
'
show-temp-unsched
'
,
props
.
account
)
}
</
script
>
frontend/src/components/account/AccountTestModal.vue
View file @
2fe8932c
...
...
@@ -292,8 +292,11 @@ const loadAvailableModels = async () => {
if
(
availableModels
.
value
.
length
>
0
)
{
if
(
props
.
account
.
platform
===
'
gemini
'
)
{
const
preferred
=
availableModels
.
value
.
find
((
m
)
=>
m
.
id
===
'
gemini-2.0-flash
'
)
||
availableModels
.
value
.
find
((
m
)
=>
m
.
id
===
'
gemini-2.5-flash
'
)
||
availableModels
.
value
.
find
((
m
)
=>
m
.
id
===
'
gemini-2.5-pro
'
)
||
availableModels
.
value
.
find
((
m
)
=>
m
.
id
===
'
gemini-3-pro
'
)
availableModels
.
value
.
find
((
m
)
=>
m
.
id
===
'
gemini-3-flash-preview
'
)
||
availableModels
.
value
.
find
((
m
)
=>
m
.
id
===
'
gemini-3-pro-preview
'
)
selectedModelId
.
value
=
preferred
?.
id
||
availableModels
.
value
[
0
].
id
}
else
{
// Try to select Sonnet as default, otherwise use first model
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
2fe8932c
...
...
@@ -648,7 +648,7 @@ import { ref, watch, computed } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Proxy
,
Group
}
from
'
@/types
'
import
type
{
Proxy
,
Admin
Group
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
...
...
@@ -659,7 +659,7 @@ interface Props {
show
:
boolean
accountIds
:
number
[]
proxies
:
Proxy
[]
groups
:
Group
[]
groups
:
Admin
Group
[]
}
const
props
=
defineProps
<
Props
>
()
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
2fe8932c
...
...
@@ -1191,6 +1191,190 @@
<
/div
>
<
/div
>
<!--
Quota
Control
Section
(
Anthropic
OAuth
/
SetupToken
only
)
-->
<
div
v
-
if
=
"
form.platform === 'anthropic' && accountCategory === 'oauth-based'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
>
<
div
class
=
"
mb-3
"
>
<
h3
class
=
"
input-label mb-0 text-base font-semibold
"
>
{{
t
(
'
admin.accounts.quotaControl.title
'
)
}}
<
/h3
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.hint
'
)
}}
<
/p
>
<
/div
>
<!--
Window
Cost
Limit
-->
<
div
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
mb-3 flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.quotaControl.windowCost.label
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.windowCost.hint
'
)
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
@
click
=
"
windowCostEnabled = !windowCostEnabled
"
:
class
=
"
[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
windowCostEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]
"
>
<
span
:
class
=
"
[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
windowCostEnabled ? 'translate-x-5' : 'translate-x-0'
]
"
/>
<
/button
>
<
/div
>
<
div
v
-
if
=
"
windowCostEnabled
"
class
=
"
grid grid-cols-2 gap-4
"
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.quotaControl.windowCost.limit
'
)
}}
<
/label
>
<
div
class
=
"
relative
"
>
<
span
class
=
"
absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400
"
>
$
<
/span
>
<
input
v
-
model
.
number
=
"
windowCostLimit
"
type
=
"
number
"
min
=
"
0
"
step
=
"
1
"
class
=
"
input pl-7
"
:
placeholder
=
"
t('admin.accounts.quotaControl.windowCost.limitPlaceholder')
"
/>
<
/div
>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.quotaControl.windowCost.limitHint
'
)
}}
<
/p
>
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.quotaControl.windowCost.stickyReserve
'
)
}}
<
/label
>
<
div
class
=
"
relative
"
>
<
span
class
=
"
absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400
"
>
$
<
/span
>
<
input
v
-
model
.
number
=
"
windowCostStickyReserve
"
type
=
"
number
"
min
=
"
0
"
step
=
"
1
"
class
=
"
input pl-7
"
:
placeholder
=
"
t('admin.accounts.quotaControl.windowCost.stickyReservePlaceholder')
"
/>
<
/div
>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.quotaControl.windowCost.stickyReserveHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<!--
Session
Limit
-->
<
div
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
mb-3 flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.quotaControl.sessionLimit.label
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.sessionLimit.hint
'
)
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
@
click
=
"
sessionLimitEnabled = !sessionLimitEnabled
"
:
class
=
"
[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
sessionLimitEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]
"
>
<
span
:
class
=
"
[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
sessionLimitEnabled ? 'translate-x-5' : 'translate-x-0'
]
"
/>
<
/button
>
<
/div
>
<
div
v
-
if
=
"
sessionLimitEnabled
"
class
=
"
grid grid-cols-2 gap-4
"
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.quotaControl.sessionLimit.maxSessions
'
)
}}
<
/label
>
<
input
v
-
model
.
number
=
"
maxSessions
"
type
=
"
number
"
min
=
"
1
"
step
=
"
1
"
class
=
"
input
"
:
placeholder
=
"
t('admin.accounts.quotaControl.sessionLimit.maxSessionsPlaceholder')
"
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.quotaControl.sessionLimit.maxSessionsHint
'
)
}}
<
/p
>
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.quotaControl.sessionLimit.idleTimeout
'
)
}}
<
/label
>
<
div
class
=
"
relative
"
>
<
input
v
-
model
.
number
=
"
sessionIdleTimeout
"
type
=
"
number
"
min
=
"
1
"
step
=
"
1
"
class
=
"
input pr-12
"
:
placeholder
=
"
t('admin.accounts.quotaControl.sessionLimit.idleTimeoutPlaceholder')
"
/>
<
span
class
=
"
absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
common.minutes
'
)
}}
<
/span
>
<
/div
>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.quotaControl.sessionLimit.idleTimeoutHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<!--
TLS
Fingerprint
-->
<
div
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.quotaControl.tlsFingerprint.label
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.tlsFingerprint.hint
'
)
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
@
click
=
"
tlsFingerprintEnabled = !tlsFingerprintEnabled
"
:
class
=
"
[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
tlsFingerprintEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]
"
>
<
span
:
class
=
"
[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
tlsFingerprintEnabled ? 'translate-x-5' : 'translate-x-0'
]
"
/>
<
/button
>
<
/div
>
<
/div
>
<!--
Session
ID
Masking
-->
<
div
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.quotaControl.sessionIdMasking.label
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaControl.sessionIdMasking.hint
'
)
}}
<
/p
>
<
/div
>
<
button
type
=
"
button
"
@
click
=
"
sessionIdMaskingEnabled = !sessionIdMaskingEnabled
"
:
class
=
"
[
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
sessionIdMaskingEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]
"
>
<
span
:
class
=
"
[
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
sessionIdMaskingEnabled ? 'translate-x-5' : 'translate-x-0'
]
"
/>
<
/button
>
<
/div
>
<
/div
>
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.proxy
'
)
}}
<
/label
>
<
ProxySelector
v
-
model
=
"
form.proxy_id
"
:
proxies
=
"
proxies
"
/>
...
...
@@ -1214,7 +1398,7 @@
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.billingRateMultiplier
'
)
}}
<
/label
>
<
input
v
-
model
.
number
=
"
form.rate_multiplier
"
type
=
"
number
"
min
=
"
0
"
step
=
"
0.01
"
class
=
"
input
"
/>
<
input
v
-
model
.
number
=
"
form.rate_multiplier
"
type
=
"
number
"
min
=
"
0
"
step
=
"
0.
0
01
"
class
=
"
input
"
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.billingRateMultiplierHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
...
...
@@ -1632,7 +1816,7 @@ import {
import
{
useOpenAIOAuth
}
from
'
@/composables/useOpenAIOAuth
'
import
{
useGeminiOAuth
}
from
'
@/composables/useGeminiOAuth
'
import
{
useAntigravityOAuth
}
from
'
@/composables/useAntigravityOAuth
'
import
type
{
Proxy
,
Group
,
AccountPlatform
,
AccountType
}
from
'
@/types
'
import
type
{
Proxy
,
Admin
Group
,
AccountPlatform
,
AccountType
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
ProxySelector
from
'
@/components/common/ProxySelector.vue
'
...
...
@@ -1678,7 +1862,7 @@ const apiKeyHint = computed(() => {
interface
Props
{
show
:
boolean
proxies
:
Proxy
[]
groups
:
Group
[]
groups
:
Admin
Group
[]
}
const
props
=
defineProps
<
Props
>
()
...
...
@@ -1763,6 +1947,16 @@ const geminiAIStudioOAuthEnabled = ref(false)
const
showAdvancedOAuth
=
ref
(
false
)
const
showGeminiHelpDialog
=
ref
(
false
)
// Quota control state (Anthropic OAuth/SetupToken only)
const
windowCostEnabled
=
ref
(
false
)
const
windowCostLimit
=
ref
<
number
|
null
>
(
null
)
const
windowCostStickyReserve
=
ref
<
number
|
null
>
(
null
)
const
sessionLimitEnabled
=
ref
(
false
)
const
maxSessions
=
ref
<
number
|
null
>
(
null
)
const
sessionIdleTimeout
=
ref
<
number
|
null
>
(
null
)
const
tlsFingerprintEnabled
=
ref
(
false
)
const
sessionIdMaskingEnabled
=
ref
(
false
)
// Gemini tier selection (used as fallback when auto-detection is unavailable/fails)
const
geminiTierGoogleOne
=
ref
<
'
google_one_free
'
|
'
google_ai_pro
'
|
'
google_ai_ultra
'
>
(
'
google_one_free
'
)
const
geminiTierGcp
=
ref
<
'
gcp_standard
'
|
'
gcp_enterprise
'
>
(
'
gcp_standard
'
)
...
...
@@ -2140,6 +2334,15 @@ const resetForm = () => {
customErrorCodeInput
.
value
=
null
interceptWarmupRequests
.
value
=
false
autoPauseOnExpired
.
value
=
true
// Reset quota control state
windowCostEnabled
.
value
=
false
windowCostLimit
.
value
=
null
windowCostStickyReserve
.
value
=
null
sessionLimitEnabled
.
value
=
false
maxSessions
.
value
=
null
sessionIdleTimeout
.
value
=
null
tlsFingerprintEnabled
.
value
=
false
sessionIdMaskingEnabled
.
value
=
false
tempUnschedEnabled
.
value
=
false
tempUnschedRules
.
value
=
[]
geminiOAuthType
.
value
=
'
code_assist
'
...
...
@@ -2407,7 +2610,32 @@ const handleAnthropicExchange = async (authCode: string) => {
...
proxyConfig
}
)
const
extra
=
oauth
.
buildExtraInfo
(
tokenInfo
)
// Build extra with quota control settings
const
baseExtra
=
oauth
.
buildExtraInfo
(
tokenInfo
)
||
{
}
const
extra
:
Record
<
string
,
unknown
>
=
{
...
baseExtra
}
// Add window cost limit settings
if
(
windowCostEnabled
.
value
&&
windowCostLimit
.
value
!=
null
&&
windowCostLimit
.
value
>
0
)
{
extra
.
window_cost_limit
=
windowCostLimit
.
value
extra
.
window_cost_sticky_reserve
=
windowCostStickyReserve
.
value
??
10
}
// Add session limit settings
if
(
sessionLimitEnabled
.
value
&&
maxSessions
.
value
!=
null
&&
maxSessions
.
value
>
0
)
{
extra
.
max_sessions
=
maxSessions
.
value
extra
.
session_idle_timeout_minutes
=
sessionIdleTimeout
.
value
??
5
}
// Add TLS fingerprint settings
if
(
tlsFingerprintEnabled
.
value
)
{
extra
.
enable_tls_fingerprint
=
true
}
// Add session ID masking settings
if
(
sessionIdMaskingEnabled
.
value
)
{
extra
.
session_id_masking_enabled
=
true
}
const
credentials
=
{
...
tokenInfo
,
...(
interceptWarmupRequests
.
value
?
{
intercept_warmup_requests
:
true
}
:
{
}
)
...
...
@@ -2475,7 +2703,32 @@ const handleCookieAuth = async (sessionKey: string) => {
...
proxyConfig
}
)
const
extra
=
oauth
.
buildExtraInfo
(
tokenInfo
)
// Build extra with quota control settings
const
baseExtra
=
oauth
.
buildExtraInfo
(
tokenInfo
)
||
{
}
const
extra
:
Record
<
string
,
unknown
>
=
{
...
baseExtra
}
// Add window cost limit settings
if
(
windowCostEnabled
.
value
&&
windowCostLimit
.
value
!=
null
&&
windowCostLimit
.
value
>
0
)
{
extra
.
window_cost_limit
=
windowCostLimit
.
value
extra
.
window_cost_sticky_reserve
=
windowCostStickyReserve
.
value
??
10
}
// Add session limit settings
if
(
sessionLimitEnabled
.
value
&&
maxSessions
.
value
!=
null
&&
maxSessions
.
value
>
0
)
{
extra
.
max_sessions
=
maxSessions
.
value
extra
.
session_idle_timeout_minutes
=
sessionIdleTimeout
.
value
??
5
}
// Add TLS fingerprint settings
if
(
tlsFingerprintEnabled
.
value
)
{
extra
.
enable_tls_fingerprint
=
true
}
// Add session ID masking settings
if
(
sessionIdMaskingEnabled
.
value
)
{
extra
.
session_id_masking_enabled
=
true
}
const
accountName
=
keys
.
length
>
1
?
`${form.name
}
#${i + 1
}
`
:
form
.
name
// Merge interceptWarmupRequests into credentials
...
...
Prev
1
…
7
8
9
10
11
12
13
14
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment