mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-22 18:40:04 +00:00
Merge remote-tracking branch 'origin/release/custom-0.1.92' into worktree-feature/sora
This commit is contained in:
@@ -2132,7 +2132,7 @@ func (r *stubAccountRepoForHandler) BulkUpdate(context.Context, []int64, service
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (r *stubAccountRepoForHandler) IncrementQuotaUsed(context.Context, int64, float64, string) error {
|
||||
func (r *stubAccountRepoForHandler) IncrementQuotaUsed(context.Context, int64, float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ func (r *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates s
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (r *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
|
||||
func (r *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1676,54 +1676,53 @@ func (r *accountRepository) FindByExtraField(ctx context.Context, key string, va
|
||||
return r.accountsToService(ctx, accounts)
|
||||
}
|
||||
|
||||
// IncrementQuotaUsed 原子递增账号的 extra.quota_used 字段
|
||||
// period 为 "daily"/"weekly" 时,使用 CTE 原子重置+递增;为空时保持累计制。
|
||||
func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, period string) error {
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
// nowUTC is a SQL expression to generate a UTC RFC3339 timestamp string.
|
||||
const nowUTC = `to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`
|
||||
|
||||
interval := periodToInterval(period)
|
||||
if interval == "" {
|
||||
// 累计制:保持原有逻辑
|
||||
rows, err = r.sql.QueryContext(ctx,
|
||||
`UPDATE accounts SET extra = jsonb_set(
|
||||
COALESCE(extra, '{}'::jsonb),
|
||||
'{quota_used}',
|
||||
to_jsonb(COALESCE((extra->>'quota_used')::numeric, 0) + $1)
|
||||
), updated_at = NOW()
|
||||
WHERE id = $2 AND deleted_at IS NULL
|
||||
RETURNING
|
||||
COALESCE((extra->>'quota_used')::numeric, 0),
|
||||
COALESCE((extra->>'quota_limit')::numeric, 0)`,
|
||||
amount, id)
|
||||
} else {
|
||||
// 周期制:单条 UPDATE 内联过期判断,避免 CTE snapshot 在 READ COMMITTED 下的 lost update
|
||||
rows, err = r.sql.QueryContext(ctx,
|
||||
`UPDATE accounts SET extra = jsonb_set(
|
||||
jsonb_set(
|
||||
COALESCE(extra, '{}'::jsonb),
|
||||
'{quota_used}',
|
||||
to_jsonb(
|
||||
CASE WHEN COALESCE((extra->>'quota_period_start')::timestamptz, '1970-01-01'::timestamptz)
|
||||
+ $3::interval <= NOW()
|
||||
THEN $1
|
||||
ELSE COALESCE((extra->>'quota_used')::numeric, 0) + $1 END
|
||||
)
|
||||
),
|
||||
'{quota_period_start}',
|
||||
to_jsonb(
|
||||
CASE WHEN COALESCE((extra->>'quota_period_start')::timestamptz, '1970-01-01'::timestamptz)
|
||||
+ $3::interval <= NOW()
|
||||
THEN to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')
|
||||
ELSE COALESCE(extra->>'quota_period_start', to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')) END
|
||||
// IncrementQuotaUsed 原子递增账号的配额用量(总/日/周三个维度)
|
||||
// 日/周额度在周期过期时自动重置为 0 再递增。
|
||||
func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||
rows, err := r.sql.QueryContext(ctx,
|
||||
`UPDATE accounts SET extra = (
|
||||
COALESCE(extra, '{}'::jsonb)
|
||||
-- 总额度:始终递增
|
||||
|| jsonb_build_object('quota_used', COALESCE((extra->>'quota_used')::numeric, 0) + $1)
|
||||
-- 日额度:仅在 quota_daily_limit > 0 时处理
|
||||
|| CASE WHEN COALESCE((extra->>'quota_daily_limit')::numeric, 0) > 0 THEN
|
||||
jsonb_build_object(
|
||||
'quota_daily_used',
|
||||
CASE WHEN COALESCE((extra->>'quota_daily_start')::timestamptz, '1970-01-01'::timestamptz)
|
||||
+ '24 hours'::interval <= NOW()
|
||||
THEN $1
|
||||
ELSE COALESCE((extra->>'quota_daily_used')::numeric, 0) + $1 END,
|
||||
'quota_daily_start',
|
||||
CASE WHEN COALESCE((extra->>'quota_daily_start')::timestamptz, '1970-01-01'::timestamptz)
|
||||
+ '24 hours'::interval <= NOW()
|
||||
THEN `+nowUTC+`
|
||||
ELSE COALESCE(extra->>'quota_daily_start', `+nowUTC+`) END
|
||||
)
|
||||
), updated_at = NOW()
|
||||
WHERE id = $2 AND deleted_at IS NULL
|
||||
RETURNING
|
||||
COALESCE((extra->>'quota_used')::numeric, 0),
|
||||
COALESCE((extra->>'quota_limit')::numeric, 0)`,
|
||||
amount, id, interval)
|
||||
}
|
||||
ELSE '{}'::jsonb END
|
||||
-- 周额度:仅在 quota_weekly_limit > 0 时处理
|
||||
|| CASE WHEN COALESCE((extra->>'quota_weekly_limit')::numeric, 0) > 0 THEN
|
||||
jsonb_build_object(
|
||||
'quota_weekly_used',
|
||||
CASE WHEN COALESCE((extra->>'quota_weekly_start')::timestamptz, '1970-01-01'::timestamptz)
|
||||
+ '168 hours'::interval <= NOW()
|
||||
THEN $1
|
||||
ELSE COALESCE((extra->>'quota_weekly_used')::numeric, 0) + $1 END,
|
||||
'quota_weekly_start',
|
||||
CASE WHEN COALESCE((extra->>'quota_weekly_start')::timestamptz, '1970-01-01'::timestamptz)
|
||||
+ '168 hours'::interval <= NOW()
|
||||
THEN `+nowUTC+`
|
||||
ELSE COALESCE(extra->>'quota_weekly_start', `+nowUTC+`) END
|
||||
)
|
||||
ELSE '{}'::jsonb END
|
||||
), updated_at = NOW()
|
||||
WHERE id = $2 AND deleted_at IS NULL
|
||||
RETURNING
|
||||
COALESCE((extra->>'quota_used')::numeric, 0),
|
||||
COALESCE((extra->>'quota_limit')::numeric, 0)`,
|
||||
amount, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1739,7 +1738,7 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am
|
||||
return err
|
||||
}
|
||||
|
||||
// 配额刚超限时触发调度快照刷新,使账号及时从调度候选中移除
|
||||
// 任一维度配额刚超限时触发调度快照刷新
|
||||
if limit > 0 && newUsed >= limit && (newUsed-amount) < limit {
|
||||
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &id, nil, nil); err != nil {
|
||||
logger.LegacyPrintf("repository.account", "[SchedulerOutbox] enqueue quota exceeded failed: account=%d err=%v", id, err)
|
||||
@@ -1748,25 +1747,13 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am
|
||||
return nil
|
||||
}
|
||||
|
||||
// periodToInterval 将周期类型转为 PostgreSQL interval 字符串
|
||||
func periodToInterval(period string) string {
|
||||
switch period {
|
||||
case "daily":
|
||||
return "24 hours"
|
||||
case "weekly":
|
||||
return "168 hours"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ResetQuotaUsed 重置账号的 extra.quota_used 为 0,同时清除 quota_period_start 以开始新周期
|
||||
// ResetQuotaUsed 重置账号所有维度的配额用量为 0
|
||||
func (r *accountRepository) ResetQuotaUsed(ctx context.Context, id int64) error {
|
||||
_, err := r.sql.ExecContext(ctx,
|
||||
`UPDATE accounts SET extra = (
|
||||
COALESCE(extra, '{}'::jsonb)
|
||||
|| '{"quota_used": 0}'::jsonb
|
||||
) - 'quota_period_start', updated_at = NOW()
|
||||
|| '{"quota_used": 0, "quota_daily_used": 0, "quota_weekly_used": 0}'::jsonb
|
||||
) - 'quota_daily_start' - 'quota_weekly_start', updated_at = NOW()
|
||||
WHERE id = $1 AND deleted_at IS NULL`,
|
||||
id)
|
||||
if err != nil {
|
||||
|
||||
@@ -1096,7 +1096,7 @@ func (s *stubAccountRepo) UpdateExtra(ctx context.Context, id int64, updates map
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
|
||||
func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -1152,45 +1152,51 @@ func (a *Account) GetCacheTTLOverrideTarget() string {
|
||||
// GetQuotaLimit 获取 API Key 账号的配额限制(美元)
|
||||
// 返回 0 表示未启用
|
||||
func (a *Account) GetQuotaLimit() float64 {
|
||||
if a.Extra == nil {
|
||||
return 0
|
||||
}
|
||||
if v, ok := a.Extra["quota_limit"]; ok {
|
||||
return parseExtraFloat64(v)
|
||||
}
|
||||
return 0
|
||||
return a.getExtraFloat64("quota_limit")
|
||||
}
|
||||
|
||||
// GetQuotaUsed 获取 API Key 账号的已用配额(美元)
|
||||
func (a *Account) GetQuotaUsed() float64 {
|
||||
return a.getExtraFloat64("quota_used")
|
||||
}
|
||||
|
||||
// GetQuotaDailyLimit 获取日额度限制(美元),0 表示未启用
|
||||
func (a *Account) GetQuotaDailyLimit() float64 {
|
||||
return a.getExtraFloat64("quota_daily_limit")
|
||||
}
|
||||
|
||||
// GetQuotaDailyUsed 获取当日已用额度(美元)
|
||||
func (a *Account) GetQuotaDailyUsed() float64 {
|
||||
return a.getExtraFloat64("quota_daily_used")
|
||||
}
|
||||
|
||||
// GetQuotaWeeklyLimit 获取周额度限制(美元),0 表示未启用
|
||||
func (a *Account) GetQuotaWeeklyLimit() float64 {
|
||||
return a.getExtraFloat64("quota_weekly_limit")
|
||||
}
|
||||
|
||||
// GetQuotaWeeklyUsed 获取本周已用额度(美元)
|
||||
func (a *Account) GetQuotaWeeklyUsed() float64 {
|
||||
return a.getExtraFloat64("quota_weekly_used")
|
||||
}
|
||||
|
||||
// getExtraFloat64 从 Extra 中读取指定 key 的 float64 值
|
||||
func (a *Account) getExtraFloat64(key string) float64 {
|
||||
if a.Extra == nil {
|
||||
return 0
|
||||
}
|
||||
if v, ok := a.Extra["quota_used"]; ok {
|
||||
if v, ok := a.Extra[key]; ok {
|
||||
return parseExtraFloat64(v)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetQuotaPeriod 获取配额周期类型("daily"/"weekly",空字符串表示累计制)
|
||||
func (a *Account) GetQuotaPeriod() string {
|
||||
if a.Extra == nil {
|
||||
return ""
|
||||
}
|
||||
if v, ok := a.Extra["quota_period"]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetQuotaPeriodStart 获取当前周期的起始时间
|
||||
func (a *Account) GetQuotaPeriodStart() time.Time {
|
||||
// getExtraTime 从 Extra 中读取 RFC3339 时间戳
|
||||
func (a *Account) getExtraTime(key string) time.Time {
|
||||
if a.Extra == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if v, ok := a.Extra["quota_period_start"]; ok {
|
||||
if v, ok := a.Extra[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
|
||||
return t
|
||||
@@ -1203,38 +1209,40 @@ func (a *Account) GetQuotaPeriodStart() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// quotaPeriodDuration 返回周期长度
|
||||
func quotaPeriodDuration(period string) time.Duration {
|
||||
switch period {
|
||||
case "daily":
|
||||
return 24 * time.Hour
|
||||
case "weekly":
|
||||
return 7 * 24 * time.Hour
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
// HasAnyQuotaLimit 检查是否配置了任一维度的配额限制
|
||||
func (a *Account) HasAnyQuotaLimit() bool {
|
||||
return a.GetQuotaLimit() > 0 || a.GetQuotaDailyLimit() > 0 || a.GetQuotaWeeklyLimit() > 0
|
||||
}
|
||||
|
||||
// isQuotaPeriodExpired 检查配额周期是否已过期
|
||||
func isQuotaPeriodExpired(period string, periodStart time.Time) bool {
|
||||
dur := quotaPeriodDuration(period)
|
||||
if dur == 0 || periodStart.IsZero() {
|
||||
return false
|
||||
// isPeriodExpired 检查指定周期(自 periodStart 起经过 dur)是否已过期
|
||||
func isPeriodExpired(periodStart time.Time, dur time.Duration) bool {
|
||||
if periodStart.IsZero() {
|
||||
return true // 从未使用过,视为过期(下次 increment 会初始化)
|
||||
}
|
||||
return time.Since(periodStart) >= dur
|
||||
}
|
||||
|
||||
// IsQuotaExceeded 检查 API Key 账号配额是否已超限
|
||||
// IsQuotaExceeded 检查 API Key 账号配额是否已超限(任一维度超限即返回 true)
|
||||
func (a *Account) IsQuotaExceeded() bool {
|
||||
limit := a.GetQuotaLimit()
|
||||
if limit <= 0 {
|
||||
return false
|
||||
// 总额度
|
||||
if limit := a.GetQuotaLimit(); limit > 0 && a.GetQuotaUsed() >= limit {
|
||||
return true
|
||||
}
|
||||
// 周期性配额:如果当前周期已过期,视为未超限(下次 increment 时会自动重置)
|
||||
if isQuotaPeriodExpired(a.GetQuotaPeriod(), a.GetQuotaPeriodStart()) {
|
||||
return false
|
||||
// 日额度(周期过期视为未超限,下次 increment 会重置)
|
||||
if limit := a.GetQuotaDailyLimit(); limit > 0 {
|
||||
start := a.getExtraTime("quota_daily_start")
|
||||
if !isPeriodExpired(start, 24*time.Hour) && a.GetQuotaDailyUsed() >= limit {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return a.GetQuotaUsed() >= limit
|
||||
// 周额度
|
||||
if limit := a.GetQuotaWeeklyLimit(); limit > 0 {
|
||||
start := a.getExtraTime("quota_weekly_start")
|
||||
if !isPeriodExpired(start, 7*24*time.Hour) && a.GetQuotaWeeklyUsed() >= limit {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetWindowCostLimit 获取 5h 窗口费用阈值(美元)
|
||||
|
||||
@@ -68,8 +68,8 @@ type AccountRepository interface {
|
||||
UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error
|
||||
UpdateExtra(ctx context.Context, id int64, updates map[string]any) error
|
||||
BulkUpdate(ctx context.Context, ids []int64, updates AccountBulkUpdate) (int64, error)
|
||||
// IncrementQuotaUsed 原子递增 API Key 账号的配额用量
|
||||
IncrementQuotaUsed(ctx context.Context, id int64, amount float64, period string) error
|
||||
// IncrementQuotaUsed 原子递增 API Key 账号的配额用量(总/日/周)
|
||||
IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error
|
||||
// ResetQuotaUsed 重置 API Key 账号的配额用量为 0
|
||||
ResetQuotaUsed(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ func (s *accountRepoStub) BulkUpdate(ctx context.Context, ids []int64, updates A
|
||||
panic("unexpected BulkUpdate call")
|
||||
}
|
||||
|
||||
func (s *accountRepoStub) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
|
||||
func (s *accountRepoStub) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ func (m *mockAccountRepoForPlatform) BulkUpdate(ctx context.Context, ids []int64
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockAccountRepoForPlatform) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
|
||||
func (m *mockAccountRepoForPlatform) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6583,10 +6583,9 @@ func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *bill
|
||||
}
|
||||
|
||||
// 4. 账号配额用量(账号口径:TotalCost × 账号计费倍率)
|
||||
if cost.TotalCost > 0 && p.Account.Type == AccountTypeAPIKey && p.Account.GetQuotaLimit() > 0 {
|
||||
if cost.TotalCost > 0 && p.Account.Type == AccountTypeAPIKey && p.Account.HasAnyQuotaLimit() {
|
||||
accountCost := cost.TotalCost * p.AccountRateMultiplier
|
||||
period := p.Account.GetQuotaPeriod()
|
||||
if err := deps.accountRepo.IncrementQuotaUsed(ctx, p.Account.ID, accountCost, period); err != nil {
|
||||
if err := deps.accountRepo.IncrementQuotaUsed(ctx, p.Account.ID, accountCost); err != nil {
|
||||
slog.Error("increment account quota used failed", "account_id", p.Account.ID, "cost", accountCost, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ func (m *mockAccountRepoForGemini) BulkUpdate(ctx context.Context, ids []int64,
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockAccountRepoForGemini) IncrementQuotaUsed(ctx context.Context, id int64, amount float64, _ string) error {
|
||||
func (m *mockAccountRepoForGemini) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1228,7 +1228,7 @@
|
||||
</div>
|
||||
|
||||
<!-- API Key 账号配额限制 -->
|
||||
<QuotaLimitCard v-if="form.type === 'apikey'" v-model="editQuotaLimit" :period="editQuotaPeriod" @update:period="editQuotaPeriod = $event" />
|
||||
<QuotaLimitCard v-if="form.type === 'apikey'" :totalLimit="editQuotaLimit" :dailyLimit="editQuotaDailyLimit" :weeklyLimit="editQuotaWeeklyLimit" @update:totalLimit="editQuotaLimit = $event" @update:dailyLimit="editQuotaDailyLimit = $event" @update:weeklyLimit="editQuotaWeeklyLimit = $event" />
|
||||
|
||||
<!-- Temp Unschedulable Rules -->
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||
@@ -2473,7 +2473,8 @@ const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-
|
||||
const apiKeyBaseUrl = ref('https://api.anthropic.com')
|
||||
const apiKeyValue = ref('')
|
||||
const editQuotaLimit = ref<number | null>(null)
|
||||
const editQuotaPeriod = ref<string>('')
|
||||
const editQuotaDailyLimit = ref<number | null>(null)
|
||||
const editQuotaWeeklyLimit = ref<number | null>(null)
|
||||
const modelMappings = ref<ModelMapping[]>([])
|
||||
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||
const allowedModels = ref<string[]>([])
|
||||
@@ -3137,7 +3138,8 @@ const resetForm = () => {
|
||||
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
||||
apiKeyValue.value = ''
|
||||
editQuotaLimit.value = null
|
||||
editQuotaPeriod.value = ''
|
||||
editQuotaDailyLimit.value = null
|
||||
editQuotaWeeklyLimit.value = null
|
||||
modelMappings.value = []
|
||||
modelRestrictionMode.value = 'whitelist'
|
||||
allowedModels.value = [...claudeModels] // Default fill related models
|
||||
@@ -3552,12 +3554,23 @@ const createAccountAndFinish = async (
|
||||
if (!applyTempUnschedConfig(credentials)) {
|
||||
return
|
||||
}
|
||||
// Inject quota_limit for apikey accounts
|
||||
// Inject quota limits for apikey accounts
|
||||
let finalExtra = extra
|
||||
if (type === 'apikey' && editQuotaLimit.value != null && editQuotaLimit.value > 0) {
|
||||
finalExtra = { ...(extra || {}), quota_limit: editQuotaLimit.value }
|
||||
if (editQuotaPeriod.value) {
|
||||
finalExtra.quota_period = editQuotaPeriod.value
|
||||
if (type === 'apikey') {
|
||||
const hasAnyQuota = (editQuotaLimit.value != null && editQuotaLimit.value > 0) ||
|
||||
(editQuotaDailyLimit.value != null && editQuotaDailyLimit.value > 0) ||
|
||||
(editQuotaWeeklyLimit.value != null && editQuotaWeeklyLimit.value > 0)
|
||||
if (hasAnyQuota) {
|
||||
finalExtra = { ...(extra || {}) }
|
||||
if (editQuotaLimit.value != null && editQuotaLimit.value > 0) {
|
||||
finalExtra.quota_limit = editQuotaLimit.value
|
||||
}
|
||||
if (editQuotaDailyLimit.value != null && editQuotaDailyLimit.value > 0) {
|
||||
finalExtra.quota_daily_limit = editQuotaDailyLimit.value
|
||||
}
|
||||
if (editQuotaWeeklyLimit.value != null && editQuotaWeeklyLimit.value > 0) {
|
||||
finalExtra.quota_weekly_limit = editQuotaWeeklyLimit.value
|
||||
}
|
||||
}
|
||||
}
|
||||
await doCreateAccount({
|
||||
|
||||
@@ -798,7 +798,7 @@
|
||||
</div>
|
||||
|
||||
<!-- API Key 账号配额限制 -->
|
||||
<QuotaLimitCard v-if="account?.type === 'apikey'" v-model="editQuotaLimit" :period="editQuotaPeriod" @update:period="editQuotaPeriod = $event" />
|
||||
<QuotaLimitCard v-if="account?.type === 'apikey'" :totalLimit="editQuotaLimit" :dailyLimit="editQuotaDailyLimit" :weeklyLimit="editQuotaWeeklyLimit" @update:totalLimit="editQuotaLimit = $event" @update:dailyLimit="editQuotaDailyLimit = $event" @update:weeklyLimit="editQuotaWeeklyLimit = $event" />
|
||||
|
||||
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
|
||||
<div
|
||||
@@ -1430,7 +1430,8 @@ const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OF
|
||||
const codexCLIOnlyEnabled = ref(false)
|
||||
const anthropicPassthroughEnabled = ref(false)
|
||||
const editQuotaLimit = ref<number | null>(null)
|
||||
const editQuotaPeriod = ref<string>('')
|
||||
const editQuotaDailyLimit = ref<number | null>(null)
|
||||
const editQuotaWeeklyLimit = ref<number | null>(null)
|
||||
const openAIWSModeOptions = computed(() => [
|
||||
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
|
||||
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
|
||||
@@ -1594,10 +1595,14 @@ watch(
|
||||
if (newAccount.type === 'apikey') {
|
||||
const quotaVal = extra?.quota_limit as number | undefined
|
||||
editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null
|
||||
editQuotaPeriod.value = (extra?.quota_period as string) || ''
|
||||
const dailyVal = extra?.quota_daily_limit as number | undefined
|
||||
editQuotaDailyLimit.value = (dailyVal && dailyVal > 0) ? dailyVal : null
|
||||
const weeklyVal = extra?.quota_weekly_limit as number | undefined
|
||||
editQuotaWeeklyLimit.value = (weeklyVal && weeklyVal > 0) ? weeklyVal : null
|
||||
} else {
|
||||
editQuotaLimit.value = null
|
||||
editQuotaPeriod.value = ''
|
||||
editQuotaDailyLimit.value = null
|
||||
editQuotaWeeklyLimit.value = null
|
||||
}
|
||||
|
||||
// Load antigravity model mapping (Antigravity 只支持映射模式)
|
||||
@@ -2390,22 +2395,32 @@ const handleSubmit = async () => {
|
||||
updatePayload.extra = newExtra
|
||||
}
|
||||
|
||||
// For apikey accounts, handle quota_limit in extra
|
||||
// For apikey accounts, handle quota limits in extra
|
||||
if (props.account.type === 'apikey') {
|
||||
const currentExtra = (updatePayload.extra as Record<string, unknown>) ||
|
||||
(props.account.extra as Record<string, unknown>) || {}
|
||||
const newExtra: Record<string, unknown> = { ...currentExtra }
|
||||
// Total quota
|
||||
if (editQuotaLimit.value != null && editQuotaLimit.value > 0) {
|
||||
newExtra.quota_limit = editQuotaLimit.value
|
||||
if (editQuotaPeriod.value) {
|
||||
newExtra.quota_period = editQuotaPeriod.value
|
||||
} else {
|
||||
delete newExtra.quota_period
|
||||
}
|
||||
} else {
|
||||
delete newExtra.quota_limit
|
||||
delete newExtra.quota_period
|
||||
delete newExtra.quota_period_start
|
||||
}
|
||||
// Daily quota
|
||||
if (editQuotaDailyLimit.value != null && editQuotaDailyLimit.value > 0) {
|
||||
newExtra.quota_daily_limit = editQuotaDailyLimit.value
|
||||
} else {
|
||||
delete newExtra.quota_daily_limit
|
||||
delete newExtra.quota_daily_used
|
||||
delete newExtra.quota_daily_start
|
||||
}
|
||||
// Weekly quota
|
||||
if (editQuotaWeeklyLimit.value != null && editQuotaWeeklyLimit.value > 0) {
|
||||
newExtra.quota_weekly_limit = editQuotaWeeklyLimit.value
|
||||
} else {
|
||||
delete newExtra.quota_weekly_limit
|
||||
delete newExtra.quota_weekly_used
|
||||
delete newExtra.quota_weekly_start
|
||||
}
|
||||
updatePayload.extra = newExtra
|
||||
}
|
||||
|
||||
@@ -1,40 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number | null
|
||||
period: string
|
||||
totalLimit: number | null
|
||||
dailyLimit: number | null
|
||||
weeklyLimit: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number | null]
|
||||
'update:period': [value: string]
|
||||
'update:totalLimit': [value: number | null]
|
||||
'update:dailyLimit': [value: number | null]
|
||||
'update:weeklyLimit': [value: number | null]
|
||||
}>()
|
||||
|
||||
const enabled = ref(props.modelValue != null && props.modelValue > 0)
|
||||
|
||||
// Sync enabled state when modelValue changes externally (e.g. account load)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
enabled.value = val != null && val > 0
|
||||
}
|
||||
const enabled = computed(() =>
|
||||
(props.totalLimit != null && props.totalLimit > 0) ||
|
||||
(props.dailyLimit != null && props.dailyLimit > 0) ||
|
||||
(props.weeklyLimit != null && props.weeklyLimit > 0)
|
||||
)
|
||||
|
||||
// When toggle is turned off, clear the value
|
||||
const localEnabled = ref(enabled.value)
|
||||
|
||||
// Sync when props change externally
|
||||
watch(enabled, (val) => {
|
||||
localEnabled.value = val
|
||||
})
|
||||
|
||||
// When toggle is turned off, clear all values
|
||||
watch(localEnabled, (val) => {
|
||||
if (!val) {
|
||||
emit('update:modelValue', null)
|
||||
emit('update:period', '')
|
||||
emit('update:totalLimit', null)
|
||||
emit('update:dailyLimit', null)
|
||||
emit('update:weeklyLimit', null)
|
||||
}
|
||||
})
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
const onTotalInput = (e: Event) => {
|
||||
const raw = (e.target as HTMLInputElement).valueAsNumber
|
||||
emit('update:modelValue', Number.isNaN(raw) ? null : raw)
|
||||
emit('update:totalLimit', Number.isNaN(raw) ? null : raw)
|
||||
}
|
||||
const onDailyInput = (e: Event) => {
|
||||
const raw = (e.target as HTMLInputElement).valueAsNumber
|
||||
emit('update:dailyLimit', Number.isNaN(raw) ? null : raw)
|
||||
}
|
||||
const onWeeklyInput = (e: Event) => {
|
||||
const raw = (e.target as HTMLInputElement).valueAsNumber
|
||||
emit('update:weeklyLimit', Number.isNaN(raw) ? null : raw)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -57,29 +71,30 @@ const onInput = (e: Event) => {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="enabled = !enabled"
|
||||
@click="localEnabled = !localEnabled"
|
||||
: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',
|
||||
enabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
localEnabled ? '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',
|
||||
enabled ? 'translate-x-5' : 'translate-x-0'
|
||||
localEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="enabled" class="space-y-3">
|
||||
<div v-if="localEnabled" class="space-y-3">
|
||||
<!-- 日配额 -->
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.quotaLimitAmount') }}</label>
|
||||
<label class="input-label">{{ t('admin.accounts.quotaDailyLimit') }}</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
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
:value="dailyLimit"
|
||||
@input="onDailyInput"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
@@ -87,21 +102,43 @@ const onInput = (e: Event) => {
|
||||
:placeholder="t('admin.accounts.quotaLimitPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<p class="input-hint">{{ t('admin.accounts.quotaLimitAmountHint') }}</p>
|
||||
<p class="input-hint">{{ t('admin.accounts.quotaDailyLimitHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 周配额 -->
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.quotaPeriod') }}</label>
|
||||
<select
|
||||
:value="period"
|
||||
@change="$emit('update:period', ($event.target as HTMLSelectElement).value)"
|
||||
class="input"
|
||||
>
|
||||
<option value="">{{ t('admin.accounts.quotaPeriodNone') }}</option>
|
||||
<option value="daily">{{ t('admin.accounts.quotaPeriodDaily') }}</option>
|
||||
<option value="weekly">{{ t('admin.accounts.quotaPeriodWeekly') }}</option>
|
||||
</select>
|
||||
<p class="input-hint">{{ t('admin.accounts.quotaPeriodHint') }}</p>
|
||||
<label class="input-label">{{ t('admin.accounts.quotaWeeklyLimit') }}</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
|
||||
:value="weeklyLimit"
|
||||
@input="onWeeklyInput"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="input pl-7"
|
||||
:placeholder="t('admin.accounts.quotaLimitPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<p class="input-hint">{{ t('admin.accounts.quotaWeeklyLimitHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 总配额 -->
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.quotaTotalLimit') }}</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
|
||||
:value="totalLimit"
|
||||
@input="onTotalInput"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
class="input pl-7"
|
||||
:placeholder="t('admin.accounts.quotaLimitPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<p class="input-hint">{{ t('admin.accounts.quotaTotalLimitHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1797,13 +1797,14 @@ export default {
|
||||
quotaLimitHint: 'Set max spending limit (USD). Account will be paused when reached. Changing limit won\'t reset usage.',
|
||||
quotaLimitToggle: 'Enable Quota Limit',
|
||||
quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit',
|
||||
quotaLimitAmount: 'Limit Amount',
|
||||
quotaLimitAmountHint: 'Maximum spending limit (USD). Account will be auto-paused when reached. Changing limit won\'t reset usage.',
|
||||
quotaPeriod: 'Reset Period',
|
||||
quotaPeriodNone: 'Never (cumulative)',
|
||||
quotaPeriodDaily: 'Daily (every 24 hours)',
|
||||
quotaPeriodWeekly: 'Weekly (every 7 days)',
|
||||
quotaPeriodHint: 'Choose how often the usage counter resets. Period starts from first usage.',
|
||||
quotaLimitAmount: 'Total Limit',
|
||||
quotaLimitAmountHint: 'Cumulative spending limit (USD). Never resets automatically.',
|
||||
quotaDailyLimit: 'Daily Limit',
|
||||
quotaDailyLimitHint: 'Maximum spending per 24-hour period. Resets automatically from first usage.',
|
||||
quotaWeeklyLimit: 'Weekly Limit',
|
||||
quotaWeeklyLimitHint: 'Maximum spending per 7-day period. Resets automatically from first usage.',
|
||||
quotaTotalLimit: 'Total Limit',
|
||||
quotaTotalLimitHint: 'Cumulative spending limit (USD). Never resets automatically.',
|
||||
testConnection: 'Test Connection',
|
||||
reAuthorize: 'Re-Authorize',
|
||||
refreshToken: 'Refresh Token',
|
||||
|
||||
@@ -1796,13 +1796,14 @@ export default {
|
||||
quotaLimitHint: '设置最大使用额度(美元),达到后账号暂停调度。修改限额不会重置已用额度。',
|
||||
quotaLimitToggle: '启用配额限制',
|
||||
quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度',
|
||||
quotaLimitAmount: '限额金额',
|
||||
quotaLimitAmountHint: '账号最大可用额度(美元),达到后自动暂停。修改限额不会重置已用额度。',
|
||||
quotaPeriod: '重置周期',
|
||||
quotaPeriodNone: '不重置(累计)',
|
||||
quotaPeriodDaily: '每日(24小时周期)',
|
||||
quotaPeriodWeekly: '每周(7天周期)',
|
||||
quotaPeriodHint: '选择用量计数器自动重置的频率。周期从首次使用时开始计算。',
|
||||
quotaLimitAmount: '总限额',
|
||||
quotaLimitAmountHint: '累计用量上限(美元),不自动重置。',
|
||||
quotaDailyLimit: '日限额',
|
||||
quotaDailyLimitHint: '每24小时最大可用额度,从首次使用时开始自动重置。',
|
||||
quotaWeeklyLimit: '周限额',
|
||||
quotaWeeklyLimitHint: '每7天最大可用额度,从首次使用时开始自动重置。',
|
||||
quotaTotalLimit: '总限额',
|
||||
quotaTotalLimitHint: '累计用量上限(美元),不自动重置。',
|
||||
testConnection: '测试连接',
|
||||
reAuthorize: '重新授权',
|
||||
refreshToken: '刷新令牌',
|
||||
|
||||
Reference in New Issue
Block a user