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
0b746501
Commit
0b746501
authored
Apr 16, 2026
by
陈曦
Browse files
1. merge upstream v0.1.113 2.提交migration相关文件
parents
45061102
be7551b9
Changes
225
Hide whitespace changes
Inline
Side-by-side
frontend/src/api/admin/channels.ts
View file @
0b746501
...
@@ -34,6 +34,14 @@ export interface ChannelModelPricing {
...
@@ -34,6 +34,14 @@ export interface ChannelModelPricing {
intervals
:
PricingInterval
[]
intervals
:
PricingInterval
[]
}
}
export
interface
AccountStatsPricingRule
{
id
?:
number
name
:
string
group_ids
:
number
[]
account_ids
:
number
[]
pricing
:
ChannelModelPricing
[]
}
export
interface
Channel
{
export
interface
Channel
{
id
:
number
id
:
number
name
:
string
name
:
string
...
@@ -41,9 +49,12 @@ export interface Channel {
...
@@ -41,9 +49,12 @@ export interface Channel {
status
:
string
status
:
string
billing_model_source
:
string
// "requested" | "upstream"
billing_model_source
:
string
// "requested" | "upstream"
restrict_models
:
boolean
restrict_models
:
boolean
features_config
?:
Record
<
string
,
unknown
>
group_ids
:
number
[]
group_ids
:
number
[]
model_pricing
:
ChannelModelPricing
[]
model_pricing
:
ChannelModelPricing
[]
model_mapping
:
Record
<
string
,
Record
<
string
,
string
>>
// platform → {src→dst}
model_mapping
:
Record
<
string
,
Record
<
string
,
string
>>
// platform → {src→dst}
apply_pricing_to_account_stats
:
boolean
account_stats_pricing_rules
:
AccountStatsPricingRule
[]
created_at
:
string
created_at
:
string
updated_at
:
string
updated_at
:
string
}
}
...
@@ -56,6 +67,9 @@ export interface CreateChannelRequest {
...
@@ -56,6 +67,9 @@ export interface CreateChannelRequest {
model_mapping
?:
Record
<
string
,
Record
<
string
,
string
>>
model_mapping
?:
Record
<
string
,
Record
<
string
,
string
>>
billing_model_source
?:
string
billing_model_source
?:
string
restrict_models
?:
boolean
restrict_models
?:
boolean
features_config
?:
Record
<
string
,
unknown
>
apply_pricing_to_account_stats
?:
boolean
account_stats_pricing_rules
?:
AccountStatsPricingRule
[]
}
}
export
interface
UpdateChannelRequest
{
export
interface
UpdateChannelRequest
{
...
@@ -67,6 +81,9 @@ export interface UpdateChannelRequest {
...
@@ -67,6 +81,9 @@ export interface UpdateChannelRequest {
model_mapping
?:
Record
<
string
,
Record
<
string
,
string
>>
model_mapping
?:
Record
<
string
,
Record
<
string
,
string
>>
billing_model_source
?:
string
billing_model_source
?:
string
restrict_models
?:
boolean
restrict_models
?:
boolean
features_config
?:
Record
<
string
,
unknown
>
apply_pricing_to_account_stats
?:
boolean
account_stats_pricing_rules
?:
AccountStatsPricingRule
[]
}
}
interface
PaginatedResponse
<
T
>
{
interface
PaginatedResponse
<
T
>
{
...
...
frontend/src/api/admin/payment.ts
View file @
0b746501
...
@@ -23,6 +23,7 @@ export interface AdminPaymentConfig {
...
@@ -23,6 +23,7 @@ export interface AdminPaymentConfig {
max_pending_orders
:
number
max_pending_orders
:
number
enabled_payment_types
:
string
[]
enabled_payment_types
:
string
[]
balance_disabled
:
boolean
balance_disabled
:
boolean
balance_recharge_multiplier
:
number
load_balance_strategy
:
string
load_balance_strategy
:
string
product_name_prefix
:
string
product_name_prefix
:
string
product_name_suffix
:
string
product_name_suffix
:
string
...
@@ -40,6 +41,7 @@ export interface UpdatePaymentConfigRequest {
...
@@ -40,6 +41,7 @@ export interface UpdatePaymentConfigRequest {
max_pending_orders
?:
number
max_pending_orders
?:
number
enabled_payment_types
?:
string
[]
enabled_payment_types
?:
string
[]
balance_disabled
?:
boolean
balance_disabled
?:
boolean
balance_recharge_multiplier
?:
number
load_balance_strategy
?:
string
load_balance_strategy
?:
string
product_name_prefix
?:
string
product_name_prefix
?:
string
product_name_suffix
?:
string
product_name_suffix
?:
string
...
...
frontend/src/api/admin/settings.ts
View file @
0b746501
...
@@ -4,7 +4,7 @@
...
@@ -4,7 +4,7 @@
*/
*/
import
{
apiClient
}
from
'
../client
'
import
{
apiClient
}
from
'
../client
'
import
type
{
CustomMenuItem
,
CustomEndpoint
}
from
'
@/types
'
import
type
{
CustomMenuItem
,
CustomEndpoint
,
NotifyEmailEntry
}
from
'
@/types
'
export
interface
DefaultSubscriptionSetting
{
export
interface
DefaultSubscriptionSetting
{
group_id
:
number
group_id
:
number
...
@@ -114,6 +114,7 @@ export interface SystemSettings {
...
@@ -114,6 +114,7 @@ export interface SystemSettings {
enable_fingerprint_unification
:
boolean
enable_fingerprint_unification
:
boolean
enable_metadata_passthrough
:
boolean
enable_metadata_passthrough
:
boolean
enable_cch_signing
:
boolean
enable_cch_signing
:
boolean
web_search_emulation_enabled
?:
boolean
// Payment configuration
// Payment configuration
payment_enabled
:
boolean
payment_enabled
:
boolean
...
@@ -124,6 +125,8 @@ export interface SystemSettings {
...
@@ -124,6 +125,8 @@ export interface SystemSettings {
payment_max_pending_orders
:
number
payment_max_pending_orders
:
number
payment_enabled_types
:
string
[]
payment_enabled_types
:
string
[]
payment_balance_disabled
:
boolean
payment_balance_disabled
:
boolean
payment_balance_recharge_multiplier
:
number
payment_recharge_fee_rate
:
number
payment_load_balance_strategy
:
string
payment_load_balance_strategy
:
string
payment_product_name_prefix
:
string
payment_product_name_prefix
:
string
payment_product_name_suffix
:
string
payment_product_name_suffix
:
string
...
@@ -134,6 +137,13 @@ export interface SystemSettings {
...
@@ -134,6 +137,13 @@ export interface SystemSettings {
payment_cancel_rate_limit_window
:
number
payment_cancel_rate_limit_window
:
number
payment_cancel_rate_limit_unit
:
string
payment_cancel_rate_limit_unit
:
string
payment_cancel_rate_limit_window_mode
:
string
payment_cancel_rate_limit_window_mode
:
string
// Balance & quota notification
balance_low_notify_enabled
:
boolean
balance_low_notify_threshold
:
number
balance_low_notify_recharge_url
:
string
account_quota_notify_enabled
:
boolean
account_quota_notify_emails
:
NotifyEmailEntry
[]
}
}
export
interface
UpdateSettingsRequest
{
export
interface
UpdateSettingsRequest
{
...
@@ -223,6 +233,8 @@ export interface UpdateSettingsRequest {
...
@@ -223,6 +233,8 @@ export interface UpdateSettingsRequest {
payment_max_pending_orders
?:
number
payment_max_pending_orders
?:
number
payment_enabled_types
?:
string
[]
payment_enabled_types
?:
string
[]
payment_balance_disabled
?:
boolean
payment_balance_disabled
?:
boolean
payment_balance_recharge_multiplier
?:
number
payment_recharge_fee_rate
?:
number
payment_load_balance_strategy
?:
string
payment_load_balance_strategy
?:
string
payment_product_name_prefix
?:
string
payment_product_name_prefix
?:
string
payment_product_name_suffix
?:
string
payment_product_name_suffix
?:
string
...
@@ -233,6 +245,12 @@ export interface UpdateSettingsRequest {
...
@@ -233,6 +245,12 @@ export interface UpdateSettingsRequest {
payment_cancel_rate_limit_window
?:
number
payment_cancel_rate_limit_window
?:
number
payment_cancel_rate_limit_unit
?:
string
payment_cancel_rate_limit_unit
?:
string
payment_cancel_rate_limit_window_mode
?:
string
payment_cancel_rate_limit_window_mode
?:
string
// Balance & quota notification
balance_low_notify_enabled
?:
boolean
balance_low_notify_threshold
?:
number
balance_low_notify_recharge_url
?:
string
account_quota_notify_enabled
?:
boolean
account_quota_notify_emails
?:
NotifyEmailEntry
[]
}
}
/**
/**
...
@@ -482,6 +500,63 @@ export async function updateBetaPolicySettings(
...
@@ -482,6 +500,63 @@ export async function updateBetaPolicySettings(
return
data
return
data
}
}
// --- Web Search Emulation Config ---
export
interface
WebSearchProviderConfig
{
type
:
'
brave
'
|
'
tavily
'
api_key
:
string
api_key_configured
:
boolean
quota_limit
:
number
|
null
subscribed_at
:
number
|
null
quota_used
?:
number
proxy_id
:
number
|
null
expires_at
:
number
|
null
}
export
interface
WebSearchEmulationConfig
{
enabled
:
boolean
providers
:
WebSearchProviderConfig
[]
}
export
interface
WebSearchTestResult
{
provider
:
string
results
:
{
url
:
string
;
title
:
string
;
snippet
:
string
;
page_age
?:
string
}[]
query
:
string
}
export
async
function
getWebSearchEmulationConfig
():
Promise
<
WebSearchEmulationConfig
>
{
const
{
data
}
=
await
apiClient
.
get
<
WebSearchEmulationConfig
>
(
'
/admin/settings/web-search-emulation
'
)
return
data
}
export
async
function
updateWebSearchEmulationConfig
(
config
:
WebSearchEmulationConfig
):
Promise
<
WebSearchEmulationConfig
>
{
const
{
data
}
=
await
apiClient
.
put
<
WebSearchEmulationConfig
>
(
'
/admin/settings/web-search-emulation
'
,
config
)
return
data
}
export
async
function
testWebSearchEmulation
(
query
:
string
):
Promise
<
WebSearchTestResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
WebSearchTestResult
>
(
'
/admin/settings/web-search-emulation/test
'
,
{
query
}
)
return
data
}
export
async
function
resetWebSearchUsage
(
payload
:
{
provider_type
:
string
}
):
Promise
<
void
>
{
await
apiClient
.
post
(
'
/admin/settings/web-search-emulation/reset-usage
'
,
payload
)
}
export
const
settingsAPI
=
{
export
const
settingsAPI
=
{
getSettings
,
getSettings
,
updateSettings
,
updateSettings
,
...
@@ -497,7 +572,11 @@ export const settingsAPI = {
...
@@ -497,7 +572,11 @@ export const settingsAPI = {
getRectifierSettings
,
getRectifierSettings
,
updateRectifierSettings
,
updateRectifierSettings
,
getBetaPolicySettings
,
getBetaPolicySettings
,
updateBetaPolicySettings
updateBetaPolicySettings
,
getWebSearchEmulationConfig
,
updateWebSearchEmulationConfig
,
testWebSearchEmulation
,
resetWebSearchUsage
}
}
export
default
settingsAPI
export
default
settingsAPI
frontend/src/api/admin/usage.ts
View file @
0b746501
...
@@ -17,7 +17,7 @@ export interface AdminUsageStatsResponse {
...
@@ -17,7 +17,7 @@ export interface AdminUsageStatsResponse {
total_tokens
:
number
total_tokens
:
number
total_cost
:
number
total_cost
:
number
total_actual_cost
:
number
total_actual_cost
:
number
total_account_cost
?
:
number
total_account_cost
:
number
average_duration_ms
:
number
average_duration_ms
:
number
endpoints
?:
EndpointStat
[]
endpoints
?:
EndpointStat
[]
upstream_endpoints
?:
EndpointStat
[]
upstream_endpoints
?:
EndpointStat
[]
...
...
frontend/src/api/client.ts
View file @
0b746501
...
@@ -270,9 +270,9 @@ apiClient.interceptors.response.use(
...
@@ -270,9 +270,9 @@ apiClient.interceptors.response.use(
return
Promise
.
reject
({
return
Promise
.
reject
({
status
,
status
,
code
:
apiData
.
code
,
code
:
apiData
.
code
,
reason
:
apiData
.
reason
,
error
:
apiData
.
error
,
error
:
apiData
.
error
,
message
:
apiData
.
message
||
apiData
.
detail
||
error
.
message
,
message
:
apiData
.
message
||
apiData
.
detail
||
error
.
message
,
reason
:
apiData
.
reason
,
metadata
:
apiData
.
metadata
,
metadata
:
apiData
.
metadata
,
})
})
}
}
...
...
frontend/src/api/payment.ts
View file @
0b746501
...
@@ -75,5 +75,10 @@ export const paymentAPI = {
...
@@ -75,5 +75,10 @@ export const paymentAPI = {
/** Request a refund for a completed order */
/** Request a refund for a completed order */
requestRefund
(
id
:
number
,
data
:
{
reason
:
string
})
{
requestRefund
(
id
:
number
,
data
:
{
reason
:
string
})
{
return
apiClient
.
post
(
`/payment/orders/
${
id
}
/refund-request`
,
data
)
return
apiClient
.
post
(
`/payment/orders/
${
id
}
/refund-request`
,
data
)
},
/** Get provider instance IDs that allow user refund */
getRefundEligibleProviders
()
{
return
apiClient
.
get
<
{
provider_instance_ids
:
string
[]
}
>
(
'
/payment/orders/refund-eligible-providers
'
)
}
}
}
}
frontend/src/api/user.ts
View file @
0b746501
...
@@ -4,7 +4,7 @@
...
@@ -4,7 +4,7 @@
*/
*/
import
{
apiClient
}
from
'
./client
'
import
{
apiClient
}
from
'
./client
'
import
type
{
User
,
ChangePasswordRequest
}
from
'
@/types
'
import
type
{
User
,
ChangePasswordRequest
,
NotifyEmailEntry
}
from
'
@/types
'
/**
/**
* Get current user profile
* Get current user profile
...
@@ -22,6 +22,9 @@ export async function getProfile(): Promise<User> {
...
@@ -22,6 +22,9 @@ export async function getProfile(): Promise<User> {
*/
*/
export
async
function
updateProfile
(
profile
:
{
export
async
function
updateProfile
(
profile
:
{
username
?:
string
username
?:
string
balance_notify_enabled
?:
boolean
balance_notify_threshold
?:
number
|
null
balance_notify_extra_emails
?:
NotifyEmailEntry
[]
}):
Promise
<
User
>
{
}):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
put
<
User
>
(
'
/user
'
,
profile
)
const
{
data
}
=
await
apiClient
.
put
<
User
>
(
'
/user
'
,
profile
)
return
data
return
data
...
@@ -45,10 +48,49 @@ export async function changePassword(
...
@@ -45,10 +48,49 @@ export async function changePassword(
return
data
return
data
}
}
/**
* Send verification code for adding a notify email
* @param email - Email address to verify
*/
export
async
function
sendNotifyEmailCode
(
email
:
string
):
Promise
<
void
>
{
await
apiClient
.
post
(
'
/user/notify-email/send-code
'
,
{
email
})
}
/**
* Verify and add a notify email
* @param email - Email address to add
* @param code - Verification code
*/
export
async
function
verifyNotifyEmail
(
email
:
string
,
code
:
string
):
Promise
<
void
>
{
await
apiClient
.
post
(
'
/user/notify-email/verify
'
,
{
email
,
code
})
}
/**
* Remove a notify email
* @param email - Email address to remove
*/
export
async
function
removeNotifyEmail
(
email
:
string
):
Promise
<
void
>
{
await
apiClient
.
delete
(
'
/user/notify-email
'
,
{
data
:
{
email
}
})
}
/**
* Toggle a notify email's disabled state
* @param email - Email address (empty string for primary email placeholder)
* @param disabled - Whether to disable the email
*/
export
async
function
toggleNotifyEmail
(
email
:
string
,
disabled
:
boolean
):
Promise
<
User
>
{
const
{
data
}
=
await
apiClient
.
put
<
User
>
(
'
/user/notify-email/toggle
'
,
{
email
,
disabled
})
return
data
}
export
const
userAPI
=
{
export
const
userAPI
=
{
getProfile
,
getProfile
,
updateProfile
,
updateProfile
,
changePassword
changePassword
,
sendNotifyEmailCode
,
verifyNotifyEmail
,
removeNotifyEmail
,
toggleNotifyEmail
}
}
export
default
userAPI
export
default
userAPI
frontend/src/components/account/AccountCapacityCell.vue
View file @
0b746501
<
template
>
<
template
>
<div
class=
"flex flex-col gap-
1
.5"
>
<div
class=
"flex flex-col gap-
0
.5"
>
<!-- 并发槽位 -->
<!-- 并发槽位 -->
<div
class=
"flex items-center gap-1.5"
>
<CapacityBadge
:color-class=
"concurrencyClass"
:current=
"currentConcurrency"
:max=
"account.concurrency"
>
<span
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
:class=
"[
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
/>
'inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium',
</svg>
concurrencyClass
</CapacityBadge>
]"
>
<!-- 5h窗口费用限制 -->
<svg
class=
"h-3 w-3"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<CapacityBadge
v-if=
"showWindowCost"
:color-class=
"windowCostClass"
:tooltip=
"windowCostTooltip"
:current=
"'$' + formatCost(currentWindowCost)"
:max=
"'$' + formatCost(account.window_cost_limit)"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"
/>
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
</svg>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
<span
class=
"font-mono"
>
{{
currentConcurrency
}}
</span>
</svg>
<span
class=
"text-gray-400 dark:text-gray-500"
>
/
</span>
</CapacityBadge>
<span
class=
"font-mono"
>
{{
account
.
concurrency
}}
</span>
</span>
<!-- 会话数量限制 -->
</div>
<CapacityBadge
v-if=
"showSessionLimit"
:color-class=
"sessionLimitClass"
:tooltip=
"sessionLimitTooltip"
:current=
"activeSessions"
:max=
"account.max_sessions!"
>
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<!-- 5h窗口费用限制(仅 Anthropic OAuth/SetupToken 且启用时显示) -->
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
/>
<div
v-if=
"showWindowCost"
class=
"flex items-center gap-1"
>
</svg>
<span
</CapacityBadge>
:class=
"[
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
<!-- RPM 限制 -->
windowCostClass
<CapacityBadge
v-if=
"showRpmLimit"
:color-class=
"rpmClass"
:tooltip=
"rpmTooltip"
:current=
"currentRPM"
:max=
"account.base_rpm!"
:suffix=
"rpmStrategyTag"
>
]"
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke-width=
"1.5"
stroke=
"currentColor"
>
:title=
"windowCostTooltip"
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
>
</svg>
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
</CapacityBadge>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span
class=
"font-mono"
>
$
{{
formatCost
(
currentWindowCost
)
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
/
</span>
<span
class=
"font-mono"
>
$
{{
formatCost
(
account
.
window_cost_limit
)
}}
</span>
</span>
</div>
<!-- 会话数量限制(仅 Anthropic OAuth/SetupToken 且启用时显示) -->
<div
v-if=
"showSessionLimit"
class=
"flex items-center gap-1"
>
<span
:class=
"[
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
sessionLimitClass
]"
:title=
"sessionLimitTooltip"
>
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"
/>
</svg>
<span
class=
"font-mono"
>
{{
activeSessions
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
/
</span>
<span
class=
"font-mono"
>
{{
account
.
max_sessions
}}
</span>
</span>
</div>
<!-- RPM 限制(仅 Anthropic OAuth/SetupToken 且启用时显示) -->
<div
v-if=
"showRpmLimit"
class=
"flex items-center gap-1"
>
<span
:class=
"[
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
rpmClass
]"
:title=
"rpmTooltip"
>
<svg
class=
"h-2.5 w-2.5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke-width=
"1.5"
stroke=
"currentColor"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
<span
class=
"font-mono"
>
{{
currentRPM
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
/
</span>
<span
class=
"font-mono"
>
{{
account
.
base_rpm
}}
</span>
<span
class=
"text-[9px] opacity-60"
>
{{
rpmStrategyTag
}}
</span>
</span>
</div>
<!-- API Key 账号配额限制 -->
<!-- API Key 账号配额限制 -->
<QuotaBadge
v-if=
"showDailyQuota"
:used=
"account.quota_daily_used ?? 0"
:limit=
"account.quota_daily_limit!"
label=
"D"
/>
<QuotaBadge
v-if=
"showDailyQuota"
:used=
"account.quota_daily_used ?? 0"
:limit=
"account.quota_daily_limit!"
label=
"D"
/>
...
@@ -83,7 +39,8 @@
...
@@ -83,7 +39,8 @@
import
{
computed
}
from
'
vue
'
import
{
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
type
{
Account
}
from
'
@/types
'
import
type
{
Account
}
from
'
@/types
'
import
QuotaBadge
from
'
./QuotaBadge.vue
'
import
CapacityBadge
from
'
@/components/account/CapacityBadge.vue
'
import
QuotaBadge
from
'
@/components/account/QuotaBadge.vue
'
const
props
=
defineProps
<
{
const
props
=
defineProps
<
{
account
:
Account
account
:
Account
...
@@ -91,225 +48,143 @@ const props = defineProps<{
...
@@ -91,225 +48,143 @@ const props = defineProps<{
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
//
当前并发数
//
====== 并发 ======
const
currentConcurrency
=
computed
(()
=>
props
.
account
.
current_concurrency
||
0
)
const
currentConcurrency
=
computed
(()
=>
props
.
account
.
current_concurrency
||
0
)
// 是否为 Anthropic OAuth/SetupToken 账号
const
isAnthropicOAuthOrSetupToken
=
computed
(()
=>
{
return
(
props
.
account
.
platform
===
'
anthropic
'
&&
(
props
.
account
.
type
===
'
oauth
'
||
props
.
account
.
type
===
'
setup-token
'
)
)
})
// 是否显示窗口费用限制
const
showWindowCost
=
computed
(()
=>
{
return
(
isAnthropicOAuthOrSetupToken
.
value
&&
props
.
account
.
window_cost_limit
!==
undefined
&&
props
.
account
.
window_cost_limit
!==
null
&&
props
.
account
.
window_cost_limit
>
0
)
})
// 当前窗口费用
const
currentWindowCost
=
computed
(()
=>
props
.
account
.
current_window_cost
??
0
)
// 是否显示会话限制
const
showSessionLimit
=
computed
(()
=>
{
return
(
isAnthropicOAuthOrSetupToken
.
value
&&
props
.
account
.
max_sessions
!==
undefined
&&
props
.
account
.
max_sessions
!==
null
&&
props
.
account
.
max_sessions
>
0
)
})
// 当前活跃会话数
const
activeSessions
=
computed
(()
=>
props
.
account
.
active_sessions
??
0
)
// 并发状态样式
const
concurrencyClass
=
computed
(()
=>
{
const
concurrencyClass
=
computed
(()
=>
{
const
current
=
currentConcurrency
.
value
const
current
=
currentConcurrency
.
value
const
max
=
props
.
account
.
concurrency
const
max
=
props
.
account
.
concurrency
if
(
current
>=
max
)
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
if
(
current
>=
max
)
{
if
(
current
>
0
)
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
}
if
(
current
>
0
)
{
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
}
return
'
bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400
'
return
'
bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400
'
})
})
// 窗口费用状态样式
// ====== 窗口费用 ======
const
isAnthropicOAuthOrSetupToken
=
computed
(()
=>
props
.
account
.
platform
===
'
anthropic
'
&&
(
props
.
account
.
type
===
'
oauth
'
||
props
.
account
.
type
===
'
setup-token
'
)
)
const
showWindowCost
=
computed
(()
=>
isAnthropicOAuthOrSetupToken
.
value
&&
props
.
account
.
window_cost_limit
!=
null
&&
props
.
account
.
window_cost_limit
>
0
)
const
currentWindowCost
=
computed
(()
=>
props
.
account
.
current_window_cost
??
0
)
const
windowCostClass
=
computed
(()
=>
{
const
windowCostClass
=
computed
(()
=>
{
if
(
!
showWindowCost
.
value
)
return
''
if
(
!
showWindowCost
.
value
)
return
''
const
current
=
currentWindowCost
.
value
const
current
=
currentWindowCost
.
value
const
limit
=
props
.
account
.
window_cost_limit
||
0
const
limit
=
props
.
account
.
window_cost_limit
||
0
const
reserve
=
props
.
account
.
window_cost_sticky_reserve
||
10
const
reserve
=
props
.
account
.
window_cost_sticky_reserve
||
10
if
(
current
>=
limit
+
reserve
)
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
if
(
current
>=
limit
+
reserve
)
{
if
(
current
>=
limit
)
return
'
bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400
'
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
if
(
current
>=
limit
*
0.8
)
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
}
if
(
current
>=
limit
)
{
return
'
bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400
'
}
if
(
current
>=
limit
*
0.8
)
{
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
}
return
'
bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400
'
return
'
bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400
'
})
})
// 窗口费用提示文字
const
windowCostTooltip
=
computed
(()
=>
{
const
windowCostTooltip
=
computed
(()
=>
{
if
(
!
showWindowCost
.
value
)
return
''
if
(
!
showWindowCost
.
value
)
return
''
const
current
=
currentWindowCost
.
value
const
current
=
currentWindowCost
.
value
const
limit
=
props
.
account
.
window_cost_limit
||
0
const
limit
=
props
.
account
.
window_cost_limit
||
0
const
reserve
=
props
.
account
.
window_cost_sticky_reserve
||
10
const
reserve
=
props
.
account
.
window_cost_sticky_reserve
||
10
if
(
current
>=
limit
+
reserve
)
return
t
(
'
admin.accounts.capacity.windowCost.blocked
'
)
if
(
current
>=
limit
+
reserve
)
{
if
(
current
>=
limit
)
return
t
(
'
admin.accounts.capacity.windowCost.stickyOnly
'
)
return
t
(
'
admin.accounts.capacity.windowCost.blocked
'
)
}
if
(
current
>=
limit
)
{
return
t
(
'
admin.accounts.capacity.windowCost.stickyOnly
'
)
}
return
t
(
'
admin.accounts.capacity.windowCost.normal
'
)
return
t
(
'
admin.accounts.capacity.windowCost.normal
'
)
})
})
// 会话限制状态样式
// ====== 会话限制 ======
const
showSessionLimit
=
computed
(()
=>
isAnthropicOAuthOrSetupToken
.
value
&&
props
.
account
.
max_sessions
!=
null
&&
props
.
account
.
max_sessions
>
0
)
const
activeSessions
=
computed
(()
=>
props
.
account
.
active_sessions
??
0
)
const
sessionLimitClass
=
computed
(()
=>
{
const
sessionLimitClass
=
computed
(()
=>
{
if
(
!
showSessionLimit
.
value
)
return
''
if
(
!
showSessionLimit
.
value
)
return
''
const
current
=
activeSessions
.
value
const
current
=
activeSessions
.
value
const
max
=
props
.
account
.
max_sessions
||
0
const
max
=
props
.
account
.
max_sessions
||
0
if
(
current
>=
max
)
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
if
(
current
>=
max
)
{
if
(
current
>=
max
*
0.8
)
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
}
if
(
current
>=
max
*
0.8
)
{
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
}
return
'
bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400
'
return
'
bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400
'
})
})
// 会话限制提示文字
const
sessionLimitTooltip
=
computed
(()
=>
{
const
sessionLimitTooltip
=
computed
(()
=>
{
if
(
!
showSessionLimit
.
value
)
return
''
if
(
!
showSessionLimit
.
value
)
return
''
const
current
=
activeSessions
.
value
const
current
=
activeSessions
.
value
const
max
=
props
.
account
.
max_sessions
||
0
const
max
=
props
.
account
.
max_sessions
||
0
const
idle
=
props
.
account
.
session_idle_timeout_minutes
||
5
const
idle
=
props
.
account
.
session_idle_timeout_minutes
||
5
if
(
current
>=
max
)
return
t
(
'
admin.accounts.capacity.sessions.full
'
,
{
idle
})
if
(
current
>=
max
)
{
return
t
(
'
admin.accounts.capacity.sessions.full
'
,
{
idle
})
}
return
t
(
'
admin.accounts.capacity.sessions.normal
'
,
{
idle
})
return
t
(
'
admin.accounts.capacity.sessions.normal
'
,
{
idle
})
})
})
// 是否显示 RPM 限制
// ====== RPM ======
const
showRpmLimit
=
computed
(()
=>
{
const
showRpmLimit
=
computed
(()
=>
return
(
isAnthropicOAuthOrSetupToken
.
value
&&
isAnthropicOAuthOrSetupToken
.
value
&&
props
.
account
.
base_rpm
!=
null
&&
props
.
account
.
base_rpm
!==
undefined
&&
props
.
account
.
base_rpm
>
0
props
.
account
.
base_rpm
!==
null
&&
)
props
.
account
.
base_rpm
>
0
)
})
// 当前 RPM 计数
const
currentRPM
=
computed
(()
=>
props
.
account
.
current_rpm
??
0
)
const
currentRPM
=
computed
(()
=>
props
.
account
.
current_rpm
??
0
)
// RPM 策略
const
rpmStrategy
=
computed
(()
=>
props
.
account
.
rpm_strategy
||
'
tiered
'
)
const
rpmStrategy
=
computed
(()
=>
props
.
account
.
rpm_strategy
||
'
tiered
'
)
const
rpmStrategyTag
=
computed
(()
=>
rpmStrategy
.
value
===
'
sticky_exempt
'
?
'
[S]
'
:
'
[T]
'
)
// RPM 策略标签
const
rpmStrategyTag
=
computed
(()
=>
{
return
rpmStrategy
.
value
===
'
sticky_exempt
'
?
'
[S]
'
:
'
[T]
'
})
// RPM buffer 计算(与后端一致:base
<=
0
时
buffer
为
0
)
const
rpmBuffer
=
computed
(()
=>
{
const
rpmBuffer
=
computed
(()
=>
{
const
base
=
props
.
account
.
base_rpm
||
0
const
base
=
props
.
account
.
base_rpm
||
0
return
props
.
account
.
rpm_sticky_buffer
??
(
base
>
0
?
Math
.
max
(
1
,
Math
.
floor
(
base
/
5
))
:
0
)
return
props
.
account
.
rpm_sticky_buffer
??
(
base
>
0
?
Math
.
max
(
1
,
Math
.
floor
(
base
/
5
))
:
0
)
})
})
// RPM 状态样式
const
rpmClass
=
computed
(()
=>
{
const
rpmClass
=
computed
(()
=>
{
if
(
!
showRpmLimit
.
value
)
return
''
if
(
!
showRpmLimit
.
value
)
return
''
const
current
=
currentRPM
.
value
const
current
=
currentRPM
.
value
const
base
=
props
.
account
.
base_rpm
??
0
const
base
=
props
.
account
.
base_rpm
??
0
const
buffer
=
rpmBuffer
.
value
const
buffer
=
rpmBuffer
.
value
if
(
rpmStrategy
.
value
===
'
tiered
'
)
{
if
(
rpmStrategy
.
value
===
'
tiered
'
)
{
if
(
current
>=
base
+
buffer
)
{
if
(
current
>=
base
+
buffer
)
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
return
'
bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
'
if
(
current
>=
base
)
return
'
bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400
'
}
if
(
current
>=
base
)
{
return
'
bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400
'
}
}
else
{
}
else
{
if
(
current
>=
base
)
{
if
(
current
>=
base
)
return
'
bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400
'
return
'
bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400
'
}
}
if
(
current
>=
base
*
0.8
)
{
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
}
}
if
(
current
>=
base
*
0.8
)
return
'
bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400
'
return
'
bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400
'
return
'
bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400
'
})
})
// RPM 提示文字(增强版:显示策略、区域、缓冲区)
const
rpmTooltip
=
computed
(()
=>
{
const
rpmTooltip
=
computed
(()
=>
{
if
(
!
showRpmLimit
.
value
)
return
''
if
(
!
showRpmLimit
.
value
)
return
''
const
current
=
currentRPM
.
value
const
current
=
currentRPM
.
value
const
base
=
props
.
account
.
base_rpm
??
0
const
base
=
props
.
account
.
base_rpm
??
0
const
buffer
=
rpmBuffer
.
value
const
buffer
=
rpmBuffer
.
value
if
(
rpmStrategy
.
value
===
'
tiered
'
)
{
if
(
rpmStrategy
.
value
===
'
tiered
'
)
{
if
(
current
>=
base
+
buffer
)
{
if
(
current
>=
base
+
buffer
)
return
t
(
'
admin.accounts.capacity.rpm.tieredBlocked
'
,
{
buffer
})
return
t
(
'
admin.accounts.capacity.rpm.tieredBlocked
'
,
{
buffer
})
if
(
current
>=
base
)
return
t
(
'
admin.accounts.capacity.rpm.tieredStickyOnly
'
,
{
buffer
})
}
if
(
current
>=
base
*
0.8
)
return
t
(
'
admin.accounts.capacity.rpm.tieredWarning
'
)
if
(
current
>=
base
)
{
return
t
(
'
admin.accounts.capacity.rpm.tieredStickyOnly
'
,
{
buffer
})
}
if
(
current
>=
base
*
0.8
)
{
return
t
(
'
admin.accounts.capacity.rpm.tieredWarning
'
)
}
return
t
(
'
admin.accounts.capacity.rpm.tieredNormal
'
)
return
t
(
'
admin.accounts.capacity.rpm.tieredNormal
'
)
}
else
{
}
else
{
if
(
current
>=
base
)
{
if
(
current
>=
base
)
return
t
(
'
admin.accounts.capacity.rpm.stickyExemptOver
'
)
return
t
(
'
admin.accounts.capacity.rpm.stickyExemptOver
'
)
if
(
current
>=
base
*
0.8
)
return
t
(
'
admin.accounts.capacity.rpm.stickyExemptWarning
'
)
}
if
(
current
>=
base
*
0.8
)
{
return
t
(
'
admin.accounts.capacity.rpm.stickyExemptWarning
'
)
}
return
t
(
'
admin.accounts.capacity.rpm.stickyExemptNormal
'
)
return
t
(
'
admin.accounts.capacity.rpm.stickyExemptNormal
'
)
}
}
})
})
// 是否显示各维度配额(apikey / bedrock 类型)
const
isQuotaEligible
=
computed
(()
=>
props
.
account
.
type
===
'
apikey
'
||
props
.
account
.
type
===
'
bedrock
'
)
const
showDailyQuota
=
computed
(()
=>
{
return
isQuotaEligible
.
value
&&
(
props
.
account
.
quota_daily_limit
??
0
)
>
0
})
const
showWeeklyQuota
=
computed
(()
=>
{
return
isQuotaEligible
.
value
&&
(
props
.
account
.
quota_weekly_limit
??
0
)
>
0
})
const
showTotalQuota
=
computed
(()
=>
{
return
isQuotaEligible
.
value
&&
(
props
.
account
.
quota_limit
??
0
)
>
0
})
// 格式化费用显示
// 格式化费用显示
const
formatCost
=
(
value
:
number
|
null
|
undefined
)
=>
{
const
formatCost
=
(
value
:
number
|
null
|
undefined
)
=>
{
if
(
value
===
null
||
value
===
undefined
)
return
'
0
'
if
(
value
===
null
||
value
===
undefined
)
return
'
0
'
return
value
.
toFixed
(
2
)
return
value
.
toFixed
(
2
)
}
}
// ====== 配额 ======
const
isQuotaEligible
=
computed
(()
=>
props
.
account
.
type
===
'
apikey
'
||
props
.
account
.
type
===
'
bedrock
'
)
const
showDailyQuota
=
computed
(()
=>
isQuotaEligible
.
value
&&
props
.
account
.
quota_daily_limit
!=
null
&&
props
.
account
.
quota_daily_limit
>
0
)
const
showWeeklyQuota
=
computed
(()
=>
isQuotaEligible
.
value
&&
props
.
account
.
quota_weekly_limit
!=
null
&&
props
.
account
.
quota_weekly_limit
>
0
)
const
showTotalQuota
=
computed
(()
=>
isQuotaEligible
.
value
&&
props
.
account
.
quota_limit
!=
null
&&
props
.
account
.
quota_limit
>
0
)
</
script
>
</
script
>
frontend/src/components/account/AccountTestModal.vue
View file @
0b746501
...
@@ -165,7 +165,6 @@
...
@@ -165,7 +165,6 @@
<button
<button
@
click=
"handleClose"
@
click=
"handleClose"
class=
"rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
class=
"rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
:disabled=
"status === 'connecting'"
>
>
{{
t
(
'
common.close
'
)
}}
{{
t
(
'
common.close
'
)
}}
</button>
</button>
...
@@ -249,7 +248,7 @@ const availableModels = ref<ClaudeModel[]>([])
...
@@ -249,7 +248,7 @@ const availableModels = ref<ClaudeModel[]>([])
const
selectedModelId
=
ref
(
''
)
const
selectedModelId
=
ref
(
''
)
const
testPrompt
=
ref
(
''
)
const
testPrompt
=
ref
(
''
)
const
loadingModels
=
ref
(
false
)
const
loadingModels
=
ref
(
false
)
let
eventSource
:
EventSource
|
null
=
null
let
abortController
:
AbortController
|
null
=
null
const
generatedImages
=
ref
<
PreviewImage
[]
>
([])
const
generatedImages
=
ref
<
PreviewImage
[]
>
([])
const
prioritizedGeminiModels
=
[
'
gemini-3.1-flash-image
'
,
'
gemini-2.5-flash-image
'
,
'
gemini-2.5-flash
'
,
'
gemini-2.5-pro
'
,
'
gemini-3-flash-preview
'
,
'
gemini-3-pro-preview
'
,
'
gemini-2.0-flash
'
]
const
prioritizedGeminiModels
=
[
'
gemini-3.1-flash-image
'
,
'
gemini-2.5-flash-image
'
,
'
gemini-2.5-flash
'
,
'
gemini-2.5-pro
'
,
'
gemini-3-flash-preview
'
,
'
gemini-3-pro-preview
'
,
'
gemini-2.0-flash
'
]
const
supportsGeminiImageTest
=
computed
(()
=>
{
const
supportsGeminiImageTest
=
computed
(()
=>
{
...
@@ -279,7 +278,7 @@ watch(
...
@@ -279,7 +278,7 @@ watch(
resetState
()
resetState
()
await
loadAvailableModels
()
await
loadAvailableModels
()
}
else
{
}
else
{
closeEventSource
()
abortStream
()
}
}
}
}
)
)
...
@@ -329,18 +328,14 @@ const resetState = () => {
...
@@ -329,18 +328,14 @@ const resetState = () => {
}
}
const
handleClose
=
()
=>
{
const
handleClose
=
()
=>
{
// 防止在连接测试进行中关闭对话框
abortStream
()
if
(
status
.
value
===
'
connecting
'
)
{
return
}
closeEventSource
()
emit
(
'
close
'
)
emit
(
'
close
'
)
}
}
const
closeEventSource
=
()
=>
{
const
abortStream
=
()
=>
{
if
(
eventSource
)
{
if
(
abortController
)
{
eventSource
.
close
()
abortController
.
abort
()
eventSource
=
null
abortController
=
null
}
}
}
}
...
@@ -365,7 +360,9 @@ const startTest = async () => {
...
@@ -365,7 +360,9 @@ const startTest = async () => {
addLine
(
t
(
'
admin.accounts.testAccountTypeLabel
'
,
{
type
:
props
.
account
.
type
}),
'
text-gray-400
'
)
addLine
(
t
(
'
admin.accounts.testAccountTypeLabel
'
,
{
type
:
props
.
account
.
type
}),
'
text-gray-400
'
)
addLine
(
''
,
'
text-gray-300
'
)
addLine
(
''
,
'
text-gray-300
'
)
closeEventSource
()
abortStream
()
abortController
=
new
AbortController
()
try
{
try
{
// Create EventSource for SSE
// Create EventSource for SSE
...
@@ -381,7 +378,8 @@ const startTest = async () => {
...
@@ -381,7 +378,8 @@ const startTest = async () => {
body
:
JSON
.
stringify
({
body
:
JSON
.
stringify
({
model_id
:
selectedModelId
.
value
,
model_id
:
selectedModelId
.
value
,
prompt
:
supportsGeminiImageTest
.
value
?
testPrompt
.
value
.
trim
()
:
''
prompt
:
supportsGeminiImageTest
.
value
?
testPrompt
.
value
.
trim
()
:
''
})
}),
signal
:
abortController
.
signal
})
})
if
(
!
response
.
ok
)
{
if
(
!
response
.
ok
)
{
...
@@ -418,10 +416,15 @@ const startTest = async () => {
...
@@ -418,10 +416,15 @@ const startTest = async () => {
}
}
}
}
}
}
}
catch
(
error
:
any
)
{
}
catch
(
error
:
unknown
)
{
if
(
error
instanceof
DOMException
&&
error
.
name
===
'
AbortError
'
)
{
status
.
value
=
'
idle
'
return
}
status
.
value
=
'
error
'
status
.
value
=
'
error
'
errorMessage
.
value
=
error
.
message
||
'
Unknown error
'
const
msg
=
error
instanceof
Error
?
error
.
message
:
'
Unknown error
'
addLine
(
`Error:
${
errorMessage
.
value
}
`
,
'
text-red-400
'
)
errorMessage
.
value
=
msg
addLine
(
`Error:
${
msg
}
`
,
'
text-red-400
'
)
}
}
}
}
...
...
frontend/src/components/account/AccountUsageCell.vue
View file @
0b746501
...
@@ -439,15 +439,20 @@
...
@@ -439,15 +439,20 @@
</template>
</template>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
,
onUnmounted
,
watch
}
from
'
vue
'
import
{
ref
,
computed
,
onMounted
,
onBeforeUnmount
,
onUnmounted
,
watch
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
type
{
Account
,
AccountUsageInfo
,
GeminiCredentials
,
WindowStats
}
from
'
@/types
'
import
type
{
Account
,
AccountUsageInfo
,
GeminiCredentials
,
WindowStats
}
from
'
@/types
'
import
{
buildOpenAIUsageRefreshKey
}
from
'
@/utils/accountUsageRefresh
'
import
{
buildOpenAIUsageRefreshKey
}
from
'
@/utils/accountUsageRefresh
'
import
{
enqueueUsageRequest
}
from
'
@/utils/usageLoadQueue
'
import
{
formatCompactNumber
}
from
'
@/utils/format
'
import
{
formatCompactNumber
}
from
'
@/utils/format
'
import
UsageProgressBar
from
'
./UsageProgressBar.vue
'
import
UsageProgressBar
from
'
./UsageProgressBar.vue
'
import
AccountQuotaInfo
from
'
./AccountQuotaInfo.vue
'
import
AccountQuotaInfo
from
'
./AccountQuotaInfo.vue
'
// Module-level cache shared across all AccountUsageCell instances
const
_usageCache
=
new
Map
<
number
,
{
data
:
AccountUsageInfo
;
ts
:
number
}
>
()
const
USAGE_CACHE_TTL
=
5
*
60
*
1000
// 5 minutes
const
props
=
withDefaults
(
const
props
=
withDefaults
(
defineProps
<
{
defineProps
<
{
account
:
Account
account
:
Account
...
@@ -465,6 +470,9 @@ const props = withDefaults(
...
@@ -465,6 +470,9 @@ const props = withDefaults(
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
desktopViewportQuery
=
'
(min-width: 768px)
'
const
desktopViewportQuery
=
'
(min-width: 768px)
'
const
unmounted
=
ref
(
false
)
onBeforeUnmount
(()
=>
{
unmounted
.
value
=
true
})
const
loading
=
ref
(
false
)
const
loading
=
ref
(
false
)
const
activeQueryLoading
=
ref
(
false
)
const
activeQueryLoading
=
ref
(
false
)
const
error
=
ref
<
string
|
null
>
(
null
)
const
error
=
ref
<
string
|
null
>
(
null
)
...
@@ -941,19 +949,36 @@ const isAnthropicOAuthOrSetupToken = computed(() => {
...
@@ -941,19 +949,36 @@ const isAnthropicOAuthOrSetupToken = computed(() => {
return
props
.
account
.
platform
===
'
anthropic
'
&&
(
props
.
account
.
type
===
'
oauth
'
||
props
.
account
.
type
===
'
setup-token
'
)
return
props
.
account
.
platform
===
'
anthropic
'
&&
(
props
.
account
.
type
===
'
oauth
'
||
props
.
account
.
type
===
'
setup-token
'
)
})
})
const
loadUsage
=
async
(
source
?:
'
passive
'
|
'
active
'
)
=>
{
const
loadUsage
=
async
(
options
?:
{
source
?:
'
passive
'
|
'
active
'
;
bypassCache
?:
boolean
}
)
=>
{
if
(
!
shouldFetchUsage
.
value
)
return
if
(
!
shouldFetchUsage
.
value
)
return
// Check cache
if
(
!
options
?.
bypassCache
)
{
const
cached
=
_usageCache
.
get
(
props
.
account
.
id
)
if
(
cached
&&
Date
.
now
()
-
cached
.
ts
<
USAGE_CACHE_TTL
)
{
usageInfo
.
value
=
cached
.
data
loading
.
value
=
false
return
}
}
loading
.
value
=
true
loading
.
value
=
true
error
.
value
=
null
error
.
value
=
null
try
{
try
{
usageInfo
.
value
=
await
adminAPI
.
accounts
.
getUsage
(
props
.
account
.
id
,
source
)
const
fetchFn
=
()
=>
adminAPI
.
accounts
.
getUsage
(
props
.
account
.
id
,
options
?.
source
)
const
result
=
await
enqueueUsageRequest
(
props
.
account
,
fetchFn
)
if
(
!
unmounted
.
value
)
{
usageInfo
.
value
=
result
_usageCache
.
set
(
props
.
account
.
id
,
{
data
:
result
,
ts
:
Date
.
now
()
})
}
}
catch
(
e
:
any
)
{
}
catch
(
e
:
any
)
{
error
.
value
=
t
(
'
common.error
'
)
if
(
!
unmounted
.
value
)
{
console
.
error
(
'
Failed to load usage:
'
,
e
)
error
.
value
=
t
(
'
common.error
'
)
console
.
error
(
'
Failed to load usage:
'
,
e
)
}
}
finally
{
}
finally
{
loading
.
value
=
false
if
(
!
unmounted
.
value
)
loading
.
value
=
false
}
}
}
}
...
@@ -962,7 +987,7 @@ const flushPendingAutoLoad = () => {
...
@@ -962,7 +987,7 @@ const flushPendingAutoLoad = () => {
const
source
=
pendingAutoLoadSource
.
value
const
source
=
pendingAutoLoadSource
.
value
pendingAutoLoad
.
value
=
false
pendingAutoLoad
.
value
=
false
pendingAutoLoadSource
.
value
=
undefined
pendingAutoLoadSource
.
value
=
undefined
loadUsage
(
source
).
catch
((
e
)
=>
{
loadUsage
(
{
source
}
).
catch
((
e
)
=>
{
console
.
error
(
'
Failed to load deferred usage:
'
,
e
)
console
.
error
(
'
Failed to load deferred usage:
'
,
e
)
})
})
}
}
...
@@ -974,7 +999,7 @@ const requestAutoLoad = (source?: 'passive' | 'active') => {
...
@@ -974,7 +999,7 @@ const requestAutoLoad = (source?: 'passive' | 'active') => {
pendingAutoLoadSource
.
value
=
source
pendingAutoLoadSource
.
value
=
source
return
return
}
}
loadUsage
(
source
).
catch
((
e
)
=>
{
loadUsage
(
{
source
}
).
catch
((
e
)
=>
{
console
.
error
(
'
Failed to auto load usage:
'
,
e
)
console
.
error
(
'
Failed to auto load usage:
'
,
e
)
})
})
}
}
...
@@ -1138,7 +1163,10 @@ watch(
...
@@ -1138,7 +1163,10 @@ watch(
if
(
!
shouldFetchUsage
.
value
)
return
if
(
!
shouldFetchUsage
.
value
)
return
const
source
=
isAnthropicOAuthOrSetupToken
.
value
?
'
passive
'
:
undefined
const
source
=
isAnthropicOAuthOrSetupToken
.
value
?
'
passive
'
:
undefined
requestAutoLoad
(
source
)
_usageCache
.
delete
(
props
.
account
.
id
)
loadUsage
({
source
,
bypassCache
:
true
}).
catch
((
e
)
=>
{
console
.
error
(
'
Failed to refresh usage after manual refresh:
'
,
e
)
})
}
}
)
)
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
0b746501
...
@@ -5,7 +5,7 @@
...
@@ -5,7 +5,7 @@
width=
"wide"
width=
"wide"
@
close=
"handleClose"
@
close=
"handleClose"
>
>
<form
id=
"bulk-edit-account-form"
class=
"space-y-5"
@
submit.prevent=
"handleSubmit"
>
<form
id=
"bulk-edit-account-form"
class=
"space-y-5"
@
submit.prevent=
"
() =>
handleSubmit
()
"
>
<!-- Info -->
<!-- Info -->
<div
class=
"rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20"
>
<div
class=
"rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20"
>
<p
class=
"text-sm text-blue-700 dark:text-blue-400"
>
<p
class=
"text-sm text-blue-700 dark:text-blue-400"
>
...
...
frontend/src/components/account/CapacityBadge.vue
0 → 100644
View file @
0b746501
<
script
setup
lang=
"ts"
>
defineProps
<
{
colorClass
:
string
tooltip
?:
string
current
:
string
|
number
max
:
string
|
number
suffix
?:
string
}
>
()
</
script
>
<
template
>
<span
:class=
"[
'inline-flex items-center gap-1 rounded-md px-1.5 py-px text-[10px] font-medium leading-tight',
colorClass
]"
:title=
"tooltip"
>
<slot
/>
<span
class=
"font-mono"
>
{{
current
}}
</span>
<span
class=
"text-gray-400 dark:text-gray-500"
>
/
</span>
<span
class=
"font-mono"
>
{{
max
}}
</span>
<span
v-if=
"suffix"
class=
"text-[9px] opacity-60"
>
{{
suffix
}}
</span>
</span>
</
template
>
frontend/src/components/account/CreateAccountModal.vue
View file @
0b746501
...
@@ -1477,10 +1477,65 @@
...
@@ -1477,10 +1477,65 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
API
Key
/
Bedrock
账号配额限制
-->
<!--
配额控制
(
Anthropic
apikey
/
bedrock
:
配额限制
+
亲和
)
-->
<
div
v
-
if
=
"
form.type === 'apikey' || form.type === 'bedrock'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
>
<
div
v
-
if
=
"
form.platform === 'anthropic' && (form.type === 'apikey' || form.type === 'bedrock')
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
>
<
div
class
=
"
mb-3
"
>
<
div
class
=
"
mb-3
"
>
<
h3
class
=
"
input-label mb-0 text-base font-semibold
"
>
{{
t
(
'
admin.accounts.quotaLimit
'
)
}}
<
/h3
>
<
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
>
<
QuotaLimitCard
:
totalLimit
=
"
editQuotaLimit
"
:
dailyLimit
=
"
editQuotaDailyLimit
"
:
weeklyLimit
=
"
editQuotaWeeklyLimit
"
:
quotaNotifyGlobalEnabled
=
"
quotaNotifyGlobalEnabled
"
:
quotaNotifyDailyEnabled
=
"
quotaNotifyState.daily.enabled
"
:
quotaNotifyDailyThreshold
=
"
quotaNotifyState.daily.threshold
"
:
quotaNotifyDailyThresholdType
=
"
quotaNotifyState.daily.thresholdType
"
:
quotaNotifyWeeklyEnabled
=
"
quotaNotifyState.weekly.enabled
"
:
quotaNotifyWeeklyThreshold
=
"
quotaNotifyState.weekly.threshold
"
:
quotaNotifyWeeklyThresholdType
=
"
quotaNotifyState.weekly.thresholdType
"
:
quotaNotifyTotalEnabled
=
"
quotaNotifyState.total.enabled
"
:
quotaNotifyTotalThreshold
=
"
quotaNotifyState.total.threshold
"
:
quotaNotifyTotalThresholdType
=
"
quotaNotifyState.total.thresholdType
"
:
dailyResetMode
=
"
editDailyResetMode
"
:
dailyResetHour
=
"
editDailyResetHour
"
:
weeklyResetMode
=
"
editWeeklyResetMode
"
:
weeklyResetDay
=
"
editWeeklyResetDay
"
:
weeklyResetHour
=
"
editWeeklyResetHour
"
:
resetTimezone
=
"
editResetTimezone
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
quotaNotifyDailyEnabled
=
"
quotaNotifyState.daily.enabled = $event
"
@
update
:
quotaNotifyDailyThreshold
=
"
quotaNotifyState.daily.threshold = $event
"
@
update
:
quotaNotifyDailyThresholdType
=
"
quotaNotifyState.daily.thresholdType = $event
"
@
update
:
quotaNotifyWeeklyEnabled
=
"
quotaNotifyState.weekly.enabled = $event
"
@
update
:
quotaNotifyWeeklyThreshold
=
"
quotaNotifyState.weekly.threshold = $event
"
@
update
:
quotaNotifyWeeklyThresholdType
=
"
quotaNotifyState.weekly.thresholdType = $event
"
@
update
:
quotaNotifyTotalEnabled
=
"
quotaNotifyState.total.enabled = $event
"
@
update
:
quotaNotifyTotalThreshold
=
"
quotaNotifyState.total.threshold = $event
"
@
update
:
quotaNotifyTotalThresholdType
=
"
quotaNotifyState.total.thresholdType = $event
"
@
update
:
dailyResetMode
=
"
editDailyResetMode = $event
"
@
update
:
dailyResetHour
=
"
editDailyResetHour = $event
"
@
update
:
weeklyResetMode
=
"
editWeeklyResetMode = $event
"
@
update
:
weeklyResetDay
=
"
editWeeklyResetDay = $event
"
@
update
:
weeklyResetHour
=
"
editWeeklyResetHour = $event
"
@
update
:
resetTimezone
=
"
editResetTimezone = $event
"
/>
<
/div
>
<!--
配额控制
(
非
Anthropic
apikey
/
bedrock
)
-->
<
div
v
-
else
-
if
=
"
form.type === 'apikey' || form.type === 'bedrock'
"
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
"
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaLimitHint
'
)
}}
{{
t
(
'
admin.accounts.quotaLimitHint
'
)
}}
<
/p
>
<
/p
>
...
@@ -1489,6 +1544,16 @@
...
@@ -1489,6 +1544,16 @@
:
totalLimit
=
"
editQuotaLimit
"
:
totalLimit
=
"
editQuotaLimit
"
:
dailyLimit
=
"
editQuotaDailyLimit
"
:
dailyLimit
=
"
editQuotaDailyLimit
"
:
weeklyLimit
=
"
editQuotaWeeklyLimit
"
:
weeklyLimit
=
"
editQuotaWeeklyLimit
"
:
quotaNotifyGlobalEnabled
=
"
quotaNotifyGlobalEnabled
"
:
quotaNotifyDailyEnabled
=
"
quotaNotifyState.daily.enabled
"
:
quotaNotifyDailyThreshold
=
"
quotaNotifyState.daily.threshold
"
:
quotaNotifyDailyThresholdType
=
"
quotaNotifyState.daily.thresholdType
"
:
quotaNotifyWeeklyEnabled
=
"
quotaNotifyState.weekly.enabled
"
:
quotaNotifyWeeklyThreshold
=
"
quotaNotifyState.weekly.threshold
"
:
quotaNotifyWeeklyThresholdType
=
"
quotaNotifyState.weekly.thresholdType
"
:
quotaNotifyTotalEnabled
=
"
quotaNotifyState.total.enabled
"
:
quotaNotifyTotalThreshold
=
"
quotaNotifyState.total.threshold
"
:
quotaNotifyTotalThresholdType
=
"
quotaNotifyState.total.thresholdType
"
:
dailyResetMode
=
"
editDailyResetMode
"
:
dailyResetMode
=
"
editDailyResetMode
"
:
dailyResetHour
=
"
editDailyResetHour
"
:
dailyResetHour
=
"
editDailyResetHour
"
:
weeklyResetMode
=
"
editWeeklyResetMode
"
:
weeklyResetMode
=
"
editWeeklyResetMode
"
...
@@ -1498,6 +1563,15 @@
...
@@ -1498,6 +1563,15 @@
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
quotaNotifyDailyEnabled
=
"
quotaNotifyState.daily.enabled = $event
"
@
update
:
quotaNotifyDailyThreshold
=
"
quotaNotifyState.daily.threshold = $event
"
@
update
:
quotaNotifyDailyThresholdType
=
"
quotaNotifyState.daily.thresholdType = $event
"
@
update
:
quotaNotifyWeeklyEnabled
=
"
quotaNotifyState.weekly.enabled = $event
"
@
update
:
quotaNotifyWeeklyThreshold
=
"
quotaNotifyState.weekly.threshold = $event
"
@
update
:
quotaNotifyWeeklyThresholdType
=
"
quotaNotifyState.weekly.thresholdType = $event
"
@
update
:
quotaNotifyTotalEnabled
=
"
quotaNotifyState.total.enabled = $event
"
@
update
:
quotaNotifyTotalThreshold
=
"
quotaNotifyState.total.threshold = $event
"
@
update
:
quotaNotifyTotalThresholdType
=
"
quotaNotifyState.total.thresholdType = $event
"
@
update
:
dailyResetMode
=
"
editDailyResetMode = $event
"
@
update
:
dailyResetMode
=
"
editDailyResetMode = $event
"
@
update
:
dailyResetHour
=
"
editDailyResetHour = $event
"
@
update
:
dailyResetHour
=
"
editDailyResetHour = $event
"
@
update
:
weeklyResetMode
=
"
editWeeklyResetMode = $event
"
@
update
:
weeklyResetMode
=
"
editWeeklyResetMode = $event
"
...
@@ -1823,7 +1897,7 @@
...
@@ -1823,7 +1897,7 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Quota
Control
Section
(
Anthropic
OAuth
/
SetupToken
only
)
-->
<!--
配额控制
(
Anthropic
OAuth
/
SetupToken
:
亲和
+
窗口费用
+
会话
+
RPM
等
)
-->
<
div
<
div
v
-
if
=
"
form.platform === 'anthropic' && accountCategory === 'oauth-based'
"
v
-
if
=
"
form.platform === 'anthropic' && accountCategory === 'oauth-based'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
...
@@ -2325,6 +2399,26 @@
...
@@ -2325,6 +2399,26 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Anthropic
API
Key
:
Web
Search
Emulation
(
hidden
when
global
disabled
)
-->
<
div
v
-
if
=
"
form.platform === 'anthropic' && accountCategory === 'apikey' && webSearchGlobalEnabled
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchEmulation
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchEmulationDesc
'
)
}}
<
/p
>
<
/div
>
<
select
v
-
model
=
"
webSearchEmulationMode
"
class
=
"
input w-24 text-sm
"
>
<
option
value
=
"
default
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchDefault
'
)
}}
<
/option
>
<
option
value
=
"
enabled
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchEnabled
'
)
}}
<
/option
>
<
option
value
=
"
disabled
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchDisabled
'
)
}}
<
/option
>
<
/select
>
<
/div
>
<
/div
>
<!--
OpenAI
OAuth
Codex
官方客户端限制开关
-->
<!--
OpenAI
OAuth
Codex
官方客户端限制开关
-->
<
div
<
div
v
-
if
=
"
form.platform === 'openai' && accountCategory === 'oauth-based'
"
v
-
if
=
"
form.platform === 'openai' && accountCategory === 'oauth-based'
"
...
@@ -2809,6 +2903,7 @@ import {
...
@@ -2809,6 +2903,7 @@ import {
}
from
'
@/composables/useModelWhitelist
'
}
from
'
@/composables/useModelWhitelist
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
useQuotaNotifyState
}
from
'
@/composables/useQuotaNotifyState
'
import
{
import
{
useAccountOAuth
,
useAccountOAuth
,
type
AddMethod
,
type
AddMethod
,
...
@@ -2980,6 +3075,21 @@ const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF
...
@@ -2980,6 +3075,21 @@ const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF
const
openaiAPIKeyResponsesWebSocketV2Mode
=
ref
<
OpenAIWSMode
>
(
OPENAI_WS_MODE_OFF
)
const
openaiAPIKeyResponsesWebSocketV2Mode
=
ref
<
OpenAIWSMode
>
(
OPENAI_WS_MODE_OFF
)
const
codexCLIOnlyEnabled
=
ref
(
false
)
const
codexCLIOnlyEnabled
=
ref
(
false
)
const
anthropicPassthroughEnabled
=
ref
(
false
)
const
anthropicPassthroughEnabled
=
ref
(
false
)
const
webSearchEmulationMode
=
ref
(
'
default
'
)
const
webSearchGlobalEnabled
=
ref
(
false
)
const
{
globalEnabled
:
quotaNotifyGlobalEnabled
,
state
:
quotaNotifyState
,
loadGlobalState
:
loadQuotaNotifyGlobal
,
writeToExtra
:
writeQuotaNotifyToExtra
,
}
=
useQuotaNotifyState
()
// Load global feature states once
adminAPI
.
settings
.
getWebSearchEmulationConfig
().
then
(
cfg
=>
{
webSearchGlobalEnabled
.
value
=
cfg
?.
enabled
===
true
&&
(
cfg
?.
providers
?.
length
??
0
)
>
0
}
).
catch
(()
=>
{
webSearchGlobalEnabled
.
value
=
false
}
)
loadQuotaNotifyGlobal
()
const
mixedScheduling
=
ref
(
false
)
// For antigravity accounts: enable mixed scheduling
const
mixedScheduling
=
ref
(
false
)
// For antigravity accounts: enable mixed scheduling
const
allowOverages
=
ref
(
false
)
// For antigravity accounts: enable AI Credits overages
const
allowOverages
=
ref
(
false
)
// For antigravity accounts: enable AI Credits overages
const
antigravityAccountType
=
ref
<
'
oauth
'
|
'
upstream
'
>
(
'
oauth
'
)
// For antigravity: oauth or upstream
const
antigravityAccountType
=
ref
<
'
oauth
'
|
'
upstream
'
>
(
'
oauth
'
)
// For antigravity: oauth or upstream
...
@@ -3307,6 +3417,7 @@ watch(
...
@@ -3307,6 +3417,7 @@ watch(
}
}
if
(
newPlatform
!==
'
anthropic
'
)
{
if
(
newPlatform
!==
'
anthropic
'
)
{
anthropicPassthroughEnabled
.
value
=
false
anthropicPassthroughEnabled
.
value
=
false
webSearchEmulationMode
.
value
=
'
default
'
}
}
// Reset OAuth states
// Reset OAuth states
oauth
.
resetState
()
oauth
.
resetState
()
...
@@ -3326,6 +3437,7 @@ watch(
...
@@ -3326,6 +3437,7 @@ watch(
}
}
if
(
platform
!==
'
anthropic
'
||
category
!==
'
apikey
'
)
{
if
(
platform
!==
'
anthropic
'
||
category
!==
'
apikey
'
)
{
anthropicPassthroughEnabled
.
value
=
false
anthropicPassthroughEnabled
.
value
=
false
webSearchEmulationMode
.
value
=
'
default
'
}
}
}
}
)
)
...
@@ -3690,6 +3802,7 @@ const resetForm = () => {
...
@@ -3690,6 +3802,7 @@ const resetForm = () => {
openaiAPIKeyResponsesWebSocketV2Mode
.
value
=
OPENAI_WS_MODE_OFF
openaiAPIKeyResponsesWebSocketV2Mode
.
value
=
OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled
.
value
=
false
codexCLIOnlyEnabled
.
value
=
false
anthropicPassthroughEnabled
.
value
=
false
anthropicPassthroughEnabled
.
value
=
false
webSearchEmulationMode
.
value
=
'
default
'
// Reset quota control state
// Reset quota control state
windowCostEnabled
.
value
=
false
windowCostEnabled
.
value
=
false
windowCostLimit
.
value
=
null
windowCostLimit
.
value
=
null
...
@@ -3777,6 +3890,11 @@ const buildAnthropicExtra = (base?: Record<string, unknown>): Record<string, unk
...
@@ -3777,6 +3890,11 @@ const buildAnthropicExtra = (base?: Record<string, unknown>): Record<string, unk
}
else
{
}
else
{
delete
extra
.
anthropic_passthrough
delete
extra
.
anthropic_passthrough
}
}
if
(
webSearchEmulationMode
.
value
===
'
default
'
)
{
delete
extra
.
web_search_emulation
}
else
{
extra
.
web_search_emulation
=
webSearchEmulationMode
.
value
}
return
Object
.
keys
(
extra
).
length
>
0
?
extra
:
undefined
return
Object
.
keys
(
extra
).
length
>
0
?
extra
:
undefined
}
}
...
@@ -4075,6 +4193,8 @@ const createAccountAndFinish = async (
...
@@ -4075,6 +4193,8 @@ const createAccountAndFinish = async (
if
(
editDailyResetMode
.
value
===
'
fixed
'
||
editWeeklyResetMode
.
value
===
'
fixed
'
)
{
if
(
editDailyResetMode
.
value
===
'
fixed
'
||
editWeeklyResetMode
.
value
===
'
fixed
'
)
{
quotaExtra
.
quota_reset_timezone
=
editResetTimezone
.
value
||
'
UTC
'
quotaExtra
.
quota_reset_timezone
=
editResetTimezone
.
value
||
'
UTC
'
}
}
// Quota notify config
writeQuotaNotifyToExtra
(
quotaExtra
,
'
create
'
)
if
(
Object
.
keys
(
quotaExtra
).
length
>
0
)
{
if
(
Object
.
keys
(
quotaExtra
).
length
>
0
)
{
finalExtra
=
quotaExtra
finalExtra
=
quotaExtra
}
}
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
0b746501
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
<BaseDialog
<BaseDialog
:show=
"show"
:show=
"show"
:title=
"t('admin.accounts.editAccount')"
:title=
"t('admin.accounts.editAccount')"
width=
"
normal
"
width=
"
wide
"
@
close=
"handleClose"
@
close=
"handleClose"
>
>
<form
<form
...
@@ -1149,10 +1149,84 @@
...
@@ -1149,10 +1149,84 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
API
Key
/
Bedrock
账号配额限制
-->
<!--
Anthropic
API
Key
:
Web
Search
Emulation
(
hidden
when
global
disabled
)
-->
<
div
v
-
if
=
"
account?.type === 'apikey' || account?.type === 'bedrock'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
>
<
div
v
-
if
=
"
account?.platform === 'anthropic' && account?.type === 'apikey' && webSearchGlobalEnabled
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
>
<
label
class
=
"
input-label mb-0
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchEmulation
'
)
}}
<
/label
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchEmulationDesc
'
)
}}
<
/p
>
<
/div
>
<
select
v
-
model
=
"
webSearchEmulationMode
"
class
=
"
input w-24 text-sm
"
>
<
option
value
=
"
default
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchDefault
'
)
}}
<
/option
>
<
option
value
=
"
enabled
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchEnabled
'
)
}}
<
/option
>
<
option
value
=
"
disabled
"
>
{{
t
(
'
admin.accounts.anthropic.webSearchDisabled
'
)
}}
<
/option
>
<
/select
>
<
/div
>
<
/div
>
<!--
配额控制
(
Anthropic
apikey
/
bedrock
:
配额限制
+
亲和
)
-->
<
div
v
-
if
=
"
account?.platform === 'anthropic' && (account?.type === 'apikey' || account?.type === 'bedrock')
"
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
>
<
QuotaLimitCard
:
totalLimit
=
"
editQuotaLimit
"
:
dailyLimit
=
"
editQuotaDailyLimit
"
:
weeklyLimit
=
"
editQuotaWeeklyLimit
"
:
dailyResetMode
=
"
editDailyResetMode
"
:
dailyResetHour
=
"
editDailyResetHour
"
:
weeklyResetMode
=
"
editWeeklyResetMode
"
:
weeklyResetDay
=
"
editWeeklyResetDay
"
:
weeklyResetHour
=
"
editWeeklyResetHour
"
:
resetTimezone
=
"
editResetTimezone
"
:
quotaNotifyGlobalEnabled
=
"
quotaNotifyGlobalEnabled
"
:
quotaNotifyDailyEnabled
=
"
quotaNotifyState.daily.enabled
"
:
quotaNotifyDailyThreshold
=
"
quotaNotifyState.daily.threshold
"
:
quotaNotifyDailyThresholdType
=
"
quotaNotifyState.daily.thresholdType
"
:
quotaNotifyWeeklyEnabled
=
"
quotaNotifyState.weekly.enabled
"
:
quotaNotifyWeeklyThreshold
=
"
quotaNotifyState.weekly.threshold
"
:
quotaNotifyWeeklyThresholdType
=
"
quotaNotifyState.weekly.thresholdType
"
:
quotaNotifyTotalEnabled
=
"
quotaNotifyState.total.enabled
"
:
quotaNotifyTotalThreshold
=
"
quotaNotifyState.total.threshold
"
:
quotaNotifyTotalThresholdType
=
"
quotaNotifyState.total.thresholdType
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
dailyResetMode
=
"
editDailyResetMode = $event
"
@
update
:
dailyResetHour
=
"
editDailyResetHour = $event
"
@
update
:
weeklyResetMode
=
"
editWeeklyResetMode = $event
"
@
update
:
weeklyResetDay
=
"
editWeeklyResetDay = $event
"
@
update
:
weeklyResetHour
=
"
editWeeklyResetHour = $event
"
@
update
:
resetTimezone
=
"
editResetTimezone = $event
"
@
update
:
quotaNotifyDailyEnabled
=
"
quotaNotifyState.daily.enabled = $event
"
@
update
:
quotaNotifyDailyThreshold
=
"
quotaNotifyState.daily.threshold = $event
"
@
update
:
quotaNotifyDailyThresholdType
=
"
quotaNotifyState.daily.thresholdType = $event
"
@
update
:
quotaNotifyWeeklyEnabled
=
"
quotaNotifyState.weekly.enabled = $event
"
@
update
:
quotaNotifyWeeklyThreshold
=
"
quotaNotifyState.weekly.threshold = $event
"
@
update
:
quotaNotifyWeeklyThresholdType
=
"
quotaNotifyState.weekly.thresholdType = $event
"
@
update
:
quotaNotifyTotalEnabled
=
"
quotaNotifyState.total.enabled = $event
"
@
update
:
quotaNotifyTotalThreshold
=
"
quotaNotifyState.total.threshold = $event
"
@
update
:
quotaNotifyTotalThresholdType
=
"
quotaNotifyState.total.thresholdType = $event
"
/>
<
/div
>
<!--
配额控制
(
非
Anthropic
apikey
/
bedrock
)
-->
<
div
v
-
else
-
if
=
"
account?.type === 'apikey' || account?.type === 'bedrock'
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
>
<
div
class
=
"
mb-3
"
>
<
div
class
=
"
mb-3
"
>
<
h3
class
=
"
input-label mb-0 text-base font-semibold
"
>
{{
t
(
'
admin.accounts.quota
Limit
'
)
}}
<
/h3
>
<
h3
class
=
"
input-label mb-0 text-base font-semibold
"
>
{{
t
(
'
admin.accounts.quota
Control.title
'
)
}}
<
/h3
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
<
p
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.quotaLimitHint
'
)
}}
{{
t
(
'
admin.accounts.quotaLimitHint
'
)
}}
<
/p
>
<
/p
>
...
@@ -1167,6 +1241,16 @@
...
@@ -1167,6 +1241,16 @@
:
weeklyResetDay
=
"
editWeeklyResetDay
"
:
weeklyResetDay
=
"
editWeeklyResetDay
"
:
weeklyResetHour
=
"
editWeeklyResetHour
"
:
weeklyResetHour
=
"
editWeeklyResetHour
"
:
resetTimezone
=
"
editResetTimezone
"
:
resetTimezone
=
"
editResetTimezone
"
:
quotaNotifyGlobalEnabled
=
"
quotaNotifyGlobalEnabled
"
:
quotaNotifyDailyEnabled
=
"
quotaNotifyState.daily.enabled
"
:
quotaNotifyDailyThreshold
=
"
quotaNotifyState.daily.threshold
"
:
quotaNotifyDailyThresholdType
=
"
quotaNotifyState.daily.thresholdType
"
:
quotaNotifyWeeklyEnabled
=
"
quotaNotifyState.weekly.enabled
"
:
quotaNotifyWeeklyThreshold
=
"
quotaNotifyState.weekly.threshold
"
:
quotaNotifyWeeklyThresholdType
=
"
quotaNotifyState.weekly.thresholdType
"
:
quotaNotifyTotalEnabled
=
"
quotaNotifyState.total.enabled
"
:
quotaNotifyTotalThreshold
=
"
quotaNotifyState.total.threshold
"
:
quotaNotifyTotalThresholdType
=
"
quotaNotifyState.total.thresholdType
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
...
@@ -1176,6 +1260,15 @@
...
@@ -1176,6 +1260,15 @@
@
update
:
weeklyResetDay
=
"
editWeeklyResetDay = $event
"
@
update
:
weeklyResetDay
=
"
editWeeklyResetDay = $event
"
@
update
:
weeklyResetHour
=
"
editWeeklyResetHour = $event
"
@
update
:
weeklyResetHour
=
"
editWeeklyResetHour = $event
"
@
update
:
resetTimezone
=
"
editResetTimezone = $event
"
@
update
:
resetTimezone
=
"
editResetTimezone = $event
"
@
update
:
quotaNotifyDailyEnabled
=
"
quotaNotifyState.daily.enabled = $event
"
@
update
:
quotaNotifyDailyThreshold
=
"
quotaNotifyState.daily.threshold = $event
"
@
update
:
quotaNotifyDailyThresholdType
=
"
quotaNotifyState.daily.thresholdType = $event
"
@
update
:
quotaNotifyWeeklyEnabled
=
"
quotaNotifyState.weekly.enabled = $event
"
@
update
:
quotaNotifyWeeklyThreshold
=
"
quotaNotifyState.weekly.threshold = $event
"
@
update
:
quotaNotifyWeeklyThresholdType
=
"
quotaNotifyState.weekly.thresholdType = $event
"
@
update
:
quotaNotifyTotalEnabled
=
"
quotaNotifyState.total.enabled = $event
"
@
update
:
quotaNotifyTotalThreshold
=
"
quotaNotifyState.total.threshold = $event
"
@
update
:
quotaNotifyTotalThresholdType
=
"
quotaNotifyState.total.thresholdType = $event
"
/>
/>
<
/div
>
<
/div
>
...
@@ -1237,7 +1330,7 @@
...
@@ -1237,7 +1330,7 @@
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<!--
Quota
Control
Section
(
Anthropic
OAuth
/
SetupToken
only
)
-->
<!--
配额控制
(
Anthropic
OAuth
/
SetupToken
:
亲和
+
窗口费用
+
会话
+
RPM
等
)
-->
<
div
<
div
v
-
if
=
"
account?.platform === 'anthropic' && (account?.type === 'oauth' || account?.type === 'setup-token')
"
v
-
if
=
"
account?.platform === 'anthropic' && (account?.type === 'oauth' || account?.type === 'setup-token')
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4
"
...
@@ -1751,6 +1844,7 @@ import { useI18n } from 'vue-i18n'
...
@@ -1751,6 +1844,7 @@ import { useI18n } from 'vue-i18n'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
adminAPI
}
from
'
@/api/admin
'
import
{
useQuotaNotifyState
}
from
'
@/composables/useQuotaNotifyState
'
import
type
{
Account
,
Proxy
,
AdminGroup
,
CheckMixedChannelResponse
}
from
'
@/types
'
import
type
{
Account
,
Proxy
,
AdminGroup
,
CheckMixedChannelResponse
}
from
'
@/types
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
...
@@ -1898,6 +1992,23 @@ const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF
...
@@ -1898,6 +1992,23 @@ const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF
const
openaiAPIKeyResponsesWebSocketV2Mode
=
ref
<
OpenAIWSMode
>
(
OPENAI_WS_MODE_OFF
)
const
openaiAPIKeyResponsesWebSocketV2Mode
=
ref
<
OpenAIWSMode
>
(
OPENAI_WS_MODE_OFF
)
const
codexCLIOnlyEnabled
=
ref
(
false
)
const
codexCLIOnlyEnabled
=
ref
(
false
)
const
anthropicPassthroughEnabled
=
ref
(
false
)
const
anthropicPassthroughEnabled
=
ref
(
false
)
const
webSearchEmulationMode
=
ref
(
'
default
'
)
const
webSearchGlobalEnabled
=
ref
(
false
)
const
{
globalEnabled
:
quotaNotifyGlobalEnabled
,
state
:
quotaNotifyState
,
loadGlobalState
:
loadQuotaNotifyGlobal
,
loadFromExtra
:
loadQuotaNotifyFromExtra
,
writeToExtra
:
writeQuotaNotifyToExtra
,
reset
:
resetQuotaNotify
,
}
=
useQuotaNotifyState
()
// Load global feature states once
adminAPI
.
settings
.
getWebSearchEmulationConfig
().
then
(
cfg
=>
{
webSearchGlobalEnabled
.
value
=
cfg
?.
enabled
===
true
&&
(
cfg
?.
providers
?.
length
??
0
)
>
0
}
).
catch
(()
=>
{
webSearchGlobalEnabled
.
value
=
false
}
)
loadQuotaNotifyGlobal
()
const
editQuotaLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaDailyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaDailyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaWeeklyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaWeeklyLimit
=
ref
<
number
|
null
>
(
null
)
...
@@ -2067,6 +2178,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
...
@@ -2067,6 +2178,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
openaiAPIKeyResponsesWebSocketV2Mode
.
value
=
OPENAI_WS_MODE_OFF
openaiAPIKeyResponsesWebSocketV2Mode
.
value
=
OPENAI_WS_MODE_OFF
codexCLIOnlyEnabled
.
value
=
false
codexCLIOnlyEnabled
.
value
=
false
anthropicPassthroughEnabled
.
value
=
false
anthropicPassthroughEnabled
.
value
=
false
webSearchEmulationMode
.
value
=
'
default
'
if
(
newAccount
.
platform
===
'
openai
'
&&
(
newAccount
.
type
===
'
oauth
'
||
newAccount
.
type
===
'
apikey
'
))
{
if
(
newAccount
.
platform
===
'
openai
'
&&
(
newAccount
.
type
===
'
oauth
'
||
newAccount
.
type
===
'
apikey
'
))
{
openaiPassthroughEnabled
.
value
=
extra
?.
openai_passthrough
===
true
||
extra
?.
openai_oauth_passthrough
===
true
openaiPassthroughEnabled
.
value
=
extra
?.
openai_passthrough
===
true
||
extra
?.
openai_oauth_passthrough
===
true
openaiOAuthResponsesWebSocketV2Mode
.
value
=
resolveOpenAIWSModeFromExtra
(
extra
,
{
openaiOAuthResponsesWebSocketV2Mode
.
value
=
resolveOpenAIWSModeFromExtra
(
extra
,
{
...
@@ -2087,6 +2199,15 @@ const syncFormFromAccount = (newAccount: Account | null) => {
...
@@ -2087,6 +2199,15 @@ const syncFormFromAccount = (newAccount: Account | null) => {
}
}
if
(
newAccount
.
platform
===
'
anthropic
'
&&
newAccount
.
type
===
'
apikey
'
)
{
if
(
newAccount
.
platform
===
'
anthropic
'
&&
newAccount
.
type
===
'
apikey
'
)
{
anthropicPassthroughEnabled
.
value
=
extra
?.
anthropic_passthrough
===
true
anthropicPassthroughEnabled
.
value
=
extra
?.
anthropic_passthrough
===
true
// 三态:string "default"/"enabled"/"disabled",向后兼容旧 bool
const
wsVal
=
extra
?.
web_search_emulation
if
(
wsVal
===
'
enabled
'
||
wsVal
===
'
disabled
'
)
{
webSearchEmulationMode
.
value
=
wsVal
}
else
if
(
wsVal
===
true
)
{
webSearchEmulationMode
.
value
=
'
enabled
'
}
else
{
webSearchEmulationMode
.
value
=
'
default
'
}
}
}
// Load quota limit for apikey/bedrock accounts (bedrock quota is also loaded in its own branch above)
// Load quota limit for apikey/bedrock accounts (bedrock quota is also loaded in its own branch above)
...
@@ -2104,6 +2225,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
...
@@ -2104,6 +2225,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
editWeeklyResetDay
.
value
=
(
extra
?.
quota_weekly_reset_day
as
number
)
??
null
editWeeklyResetDay
.
value
=
(
extra
?.
quota_weekly_reset_day
as
number
)
??
null
editWeeklyResetHour
.
value
=
(
extra
?.
quota_weekly_reset_hour
as
number
)
??
null
editWeeklyResetHour
.
value
=
(
extra
?.
quota_weekly_reset_hour
as
number
)
??
null
editResetTimezone
.
value
=
(
extra
?.
quota_reset_timezone
as
string
)
||
null
editResetTimezone
.
value
=
(
extra
?.
quota_reset_timezone
as
string
)
||
null
// Load quota notify config
loadQuotaNotifyFromExtra
(
extra
)
}
else
{
}
else
{
editQuotaLimit
.
value
=
null
editQuotaLimit
.
value
=
null
editQuotaDailyLimit
.
value
=
null
editQuotaDailyLimit
.
value
=
null
...
@@ -2114,6 +2237,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
...
@@ -2114,6 +2237,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
editWeeklyResetDay
.
value
=
null
editWeeklyResetDay
.
value
=
null
editWeeklyResetHour
.
value
=
null
editWeeklyResetHour
.
value
=
null
editResetTimezone
.
value
=
null
editResetTimezone
.
value
=
null
resetQuotaNotify
()
}
}
// Load antigravity model mapping (Antigravity 只支持映射模式)
// Load antigravity model mapping (Antigravity 只支持映射模式)
...
@@ -2228,6 +2352,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
...
@@ -2228,6 +2352,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
editQuotaLimit
.
value
=
typeof
bedrockExtra
.
quota_limit
===
'
number
'
?
bedrockExtra
.
quota_limit
:
null
editQuotaLimit
.
value
=
typeof
bedrockExtra
.
quota_limit
===
'
number
'
?
bedrockExtra
.
quota_limit
:
null
editQuotaDailyLimit
.
value
=
typeof
bedrockExtra
.
quota_daily_limit
===
'
number
'
?
bedrockExtra
.
quota_daily_limit
:
null
editQuotaDailyLimit
.
value
=
typeof
bedrockExtra
.
quota_daily_limit
===
'
number
'
?
bedrockExtra
.
quota_daily_limit
:
null
editQuotaWeeklyLimit
.
value
=
typeof
bedrockExtra
.
quota_weekly_limit
===
'
number
'
?
bedrockExtra
.
quota_weekly_limit
:
null
editQuotaWeeklyLimit
.
value
=
typeof
bedrockExtra
.
quota_weekly_limit
===
'
number
'
?
bedrockExtra
.
quota_weekly_limit
:
null
// Load quota notify for bedrock
loadQuotaNotifyFromExtra
(
bedrockExtra
)
// Load model mappings for bedrock
// Load model mappings for bedrock
const
existingMappings
=
bedrockCreds
.
model_mapping
as
Record
<
string
,
string
>
|
undefined
const
existingMappings
=
bedrockCreds
.
model_mapping
as
Record
<
string
,
string
>
|
undefined
...
@@ -2522,8 +2648,13 @@ function loadQuotaControlSettings(account: Account) {
...
@@ -2522,8 +2648,13 @@ function loadQuotaControlSettings(account: Account) {
customBaseUrlEnabled
.
value
=
false
customBaseUrlEnabled
.
value
=
false
customBaseUrl
.
value
=
''
customBaseUrl
.
value
=
''
// Only applies to Anthropic OAuth/SetupToken accounts
// Remaining quota control settings only apply to Anthropic accounts
if
(
account
.
platform
!==
'
anthropic
'
||
(
account
.
type
!==
'
oauth
'
&&
account
.
type
!==
'
setup-token
'
))
{
if
(
account
.
platform
!==
'
anthropic
'
)
{
return
}
// Window cost / session limit only apply to Anthropic OAuth/SetupToken accounts
if
(
account
.
type
!==
'
oauth
'
&&
account
.
type
!==
'
setup-token
'
)
{
return
return
}
}
...
@@ -2949,7 +3080,7 @@ const handleSubmit = async () => {
...
@@ -2949,7 +3080,7 @@ const handleSubmit = async () => {
// For Anthropic OAuth/SetupToken accounts, handle quota control settings in extra
// For Anthropic OAuth/SetupToken accounts, handle quota control settings in extra
if
(
props
.
account
.
platform
===
'
anthropic
'
&&
(
props
.
account
.
type
===
'
oauth
'
||
props
.
account
.
type
===
'
setup-token
'
))
{
if
(
props
.
account
.
platform
===
'
anthropic
'
&&
(
props
.
account
.
type
===
'
oauth
'
||
props
.
account
.
type
===
'
setup-token
'
))
{
const
currentExtra
=
(
props
.
account
.
extra
as
Record
<
string
,
unknown
>
)
||
{
}
const
currentExtra
=
(
updatePayload
.
extra
as
Record
<
string
,
unknown
>
)
||
(
props
.
account
.
extra
as
Record
<
string
,
unknown
>
)
||
{
}
const
newExtra
:
Record
<
string
,
unknown
>
=
{
...
currentExtra
}
const
newExtra
:
Record
<
string
,
unknown
>
=
{
...
currentExtra
}
// Window cost limit settings
// Window cost limit settings
...
@@ -3037,15 +3168,20 @@ const handleSubmit = async () => {
...
@@ -3037,15 +3168,20 @@ const handleSubmit = async () => {
updatePayload
.
extra
=
newExtra
updatePayload
.
extra
=
newExtra
}
}
// For Anthropic API Key accounts, handle passthrough mode in extra
// For Anthropic API Key accounts, handle passthrough mode
+ web search emulation
in extra
if
(
props
.
account
.
platform
===
'
anthropic
'
&&
props
.
account
.
type
===
'
apikey
'
)
{
if
(
props
.
account
.
platform
===
'
anthropic
'
&&
props
.
account
.
type
===
'
apikey
'
)
{
const
currentExtra
=
(
props
.
account
.
extra
as
Record
<
string
,
unknown
>
)
||
{
}
const
currentExtra
=
(
updatePayload
.
extra
as
Record
<
string
,
unknown
>
)
||
(
props
.
account
.
extra
as
Record
<
string
,
unknown
>
)
||
{
}
const
newExtra
:
Record
<
string
,
unknown
>
=
{
...
currentExtra
}
const
newExtra
:
Record
<
string
,
unknown
>
=
{
...
currentExtra
}
if
(
anthropicPassthroughEnabled
.
value
)
{
if
(
anthropicPassthroughEnabled
.
value
)
{
newExtra
.
anthropic_passthrough
=
true
newExtra
.
anthropic_passthrough
=
true
}
else
{
}
else
{
delete
newExtra
.
anthropic_passthrough
delete
newExtra
.
anthropic_passthrough
}
}
if
(
webSearchEmulationMode
.
value
===
'
default
'
)
{
delete
newExtra
.
web_search_emulation
}
else
{
newExtra
.
web_search_emulation
=
webSearchEmulationMode
.
value
}
updatePayload
.
extra
=
newExtra
updatePayload
.
extra
=
newExtra
}
}
...
@@ -3089,20 +3225,27 @@ const handleSubmit = async () => {
...
@@ -3089,20 +3225,27 @@ const handleSubmit = async () => {
const
currentExtra
=
(
updatePayload
.
extra
as
Record
<
string
,
unknown
>
)
||
const
currentExtra
=
(
updatePayload
.
extra
as
Record
<
string
,
unknown
>
)
||
(
props
.
account
.
extra
as
Record
<
string
,
unknown
>
)
||
{
}
(
props
.
account
.
extra
as
Record
<
string
,
unknown
>
)
||
{
}
const
newExtra
:
Record
<
string
,
unknown
>
=
{
...
currentExtra
}
const
newExtra
:
Record
<
string
,
unknown
>
=
{
...
currentExtra
}
// Total quota
if
(
editQuotaLimit
.
value
!=
null
&&
editQuotaLimit
.
value
>
0
)
{
if
(
editQuotaLimit
.
value
!=
null
&&
editQuotaLimit
.
value
>
0
)
{
newExtra
.
quota_limit
=
editQuotaLimit
.
value
newExtra
.
quota_limit
=
editQuotaLimit
.
value
}
else
{
}
else
{
delete
newExtra
.
quota_limit
delete
newExtra
.
quota_limit
}
}
// Daily quota
if
(
editQuotaDailyLimit
.
value
!=
null
&&
editQuotaDailyLimit
.
value
>
0
)
{
if
(
editQuotaDailyLimit
.
value
!=
null
&&
editQuotaDailyLimit
.
value
>
0
)
{
newExtra
.
quota_daily_limit
=
editQuotaDailyLimit
.
value
newExtra
.
quota_daily_limit
=
editQuotaDailyLimit
.
value
}
else
{
}
else
{
delete
newExtra
.
quota_daily_limit
delete
newExtra
.
quota_daily_limit
delete
newExtra
.
quota_daily_used
delete
newExtra
.
quota_daily_start
}
}
// Weekly quota
if
(
editQuotaWeeklyLimit
.
value
!=
null
&&
editQuotaWeeklyLimit
.
value
>
0
)
{
if
(
editQuotaWeeklyLimit
.
value
!=
null
&&
editQuotaWeeklyLimit
.
value
>
0
)
{
newExtra
.
quota_weekly_limit
=
editQuotaWeeklyLimit
.
value
newExtra
.
quota_weekly_limit
=
editQuotaWeeklyLimit
.
value
}
else
{
}
else
{
delete
newExtra
.
quota_weekly_limit
delete
newExtra
.
quota_weekly_limit
delete
newExtra
.
quota_weekly_used
delete
newExtra
.
quota_weekly_start
}
}
// Quota reset mode config
// Quota reset mode config
if
(
editDailyResetMode
.
value
===
'
fixed
'
)
{
if
(
editDailyResetMode
.
value
===
'
fixed
'
)
{
...
@@ -3126,6 +3269,8 @@ const handleSubmit = async () => {
...
@@ -3126,6 +3269,8 @@ const handleSubmit = async () => {
}
else
{
}
else
{
delete
newExtra
.
quota_reset_timezone
delete
newExtra
.
quota_reset_timezone
}
}
// Quota notify config
writeQuotaNotifyToExtra
(
newExtra
,
'
update
'
)
updatePayload
.
extra
=
newExtra
updatePayload
.
extra
=
newExtra
}
}
...
...
frontend/src/components/account/QuotaDimensionRow.vue
0 → 100644
View file @
0b746501
<
script
setup
lang=
"ts"
>
import
{
useI18n
}
from
'
vue-i18n
'
import
QuotaNotifyToggle
from
'
./QuotaNotifyToggle.vue
'
import
type
{
QuotaThresholdType
,
QuotaResetMode
}
from
'
@/constants/account
'
const
{
t
}
=
useI18n
()
const
props
=
defineProps
<
{
dim
:
'
daily
'
|
'
weekly
'
|
'
total
'
label
:
string
limit
:
number
|
null
quotaNotifyGlobalEnabled
:
boolean
notifyEnabled
:
boolean
|
null
notifyThreshold
:
number
|
null
notifyThresholdType
:
QuotaThresholdType
|
null
// Reset mode (only for daily/weekly, null for total)
resetMode
:
QuotaResetMode
|
null
resetHour
:
number
|
null
resetDay
:
number
|
null
// weekly only
resetTimezone
:
string
|
null
hintRolling
:
string
hintFixed
:
string
// Shared options passed from parent
hourOptions
:
number
[]
dayOptions
:
{
value
:
number
;
key
:
string
}[]
timezoneOptions
?:
string
[]
}
>
()
const
emit
=
defineEmits
<
{
'
update:limit
'
:
[
value
:
number
|
null
]
'
update:notifyEnabled
'
:
[
value
:
boolean
|
null
]
'
update:notifyThreshold
'
:
[
value
:
number
|
null
]
'
update:notifyThresholdType
'
:
[
value
:
QuotaThresholdType
|
null
]
'
update:resetMode
'
:
[
value
:
QuotaResetMode
|
null
]
'
update:resetHour
'
:
[
value
:
number
|
null
]
'
update:resetDay
'
:
[
value
:
number
|
null
]
'
update:resetTimezone
'
:
[
value
:
string
|
null
]
}
>
()
const
hasResetMode
=
props
.
dim
!==
'
total
'
const
onLimitInput
=
(
e
:
Event
)
=>
{
const
raw
=
(
e
.
target
as
HTMLInputElement
).
valueAsNumber
emit
(
'
update:limit
'
,
Number
.
isNaN
(
raw
)
?
null
:
raw
)
}
const
onModeChange
=
(
e
:
Event
)
=>
{
const
val
=
(
e
.
target
as
HTMLSelectElement
).
value
as
QuotaResetMode
emit
(
'
update:resetMode
'
,
val
)
if
(
val
===
'
fixed
'
)
{
if
(
props
.
resetHour
==
null
)
emit
(
'
update:resetHour
'
,
0
)
if
(
props
.
dim
===
'
weekly
'
&&
props
.
resetDay
==
null
)
emit
(
'
update:resetDay
'
,
1
)
if
(
!
props
.
resetTimezone
)
emit
(
'
update:resetTimezone
'
,
'
UTC
'
)
}
}
function
getTimezoneOffsetLabel
(
tz
:
string
):
string
{
try
{
const
dtf
=
new
Intl
.
DateTimeFormat
(
'
en-US
'
,
{
timeZone
:
tz
,
timeZoneName
:
'
shortOffset
'
})
const
parts
=
dtf
.
formatToParts
(
new
Date
())
const
tzPart
=
parts
.
find
(
p
=>
p
.
type
===
'
timeZoneName
'
)
return
tzPart
?
(
tzPart
.
value
===
'
GMT
'
?
'
GMT+0
'
:
tzPart
.
value
)
:
''
}
catch
{
return
''
}
}
</
script
>
<
template
>
<div>
<!-- Title row (only when global notify is enabled) -->
<div
v-if=
"quotaNotifyGlobalEnabled"
class=
"flex items-center gap-2 mb-1"
>
<span
class=
"text-xs font-medium text-gray-700 dark:text-gray-300 flex-1 min-w-0"
>
{{
label
}}
</span>
<span
v-if=
"limit && limit > 0"
class=
"text-xs font-medium text-gray-700 dark:text-gray-300 flex-1 min-w-0"
>
{{
t
(
'
admin.accounts.quotaNotify.alert
'
)
}}
</span>
</div>
<label
v-else
class=
"text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 block"
>
{{
label
}}
</label>
<!-- Input row -->
<div
class=
"flex items-center gap-2"
>
<div
:class=
"['relative', quotaNotifyGlobalEnabled ? 'flex-1 min-w-0' : 'flex-1']"
>
<span
class=
"absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400 text-sm"
>
$
</span>
<input
:value=
"limit"
@
input=
"onLimitInput"
type=
"number"
min=
"0"
step=
"0.01"
class=
"input pl-6 py-1.5 text-sm"
:placeholder=
"t('admin.accounts.quotaLimitPlaceholder')"
/>
</div>
<QuotaNotifyToggle
v-if=
"quotaNotifyGlobalEnabled && limit && limit > 0"
class=
"flex-1 min-w-0"
:enabled=
"notifyEnabled"
:threshold=
"notifyThreshold"
:threshold-type=
"notifyThresholdType"
@
update:enabled=
"emit('update:notifyEnabled', $event)"
@
update:threshold=
"emit('update:notifyThreshold', $event)"
@
update:threshold-type=
"emit('update:notifyThresholdType', $event)"
/>
</div>
<!-- Reset mode row (daily/weekly only) -->
<div
v-if=
"hasResetMode"
class=
"mt-1 flex items-center gap-2 flex-wrap"
>
<label
class=
"text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"
>
{{
t
(
'
admin.accounts.quotaResetMode
'
)
}}
</label>
<select
:value=
"resetMode || 'rolling'"
@
change=
"onModeChange"
class=
"input py-1 text-xs w-auto"
>
<option
value=
"rolling"
>
{{
t
(
'
admin.accounts.quotaResetModeRolling
'
)
}}
</option>
<option
value=
"fixed"
>
{{
t
(
'
admin.accounts.quotaResetModeFixed
'
)
}}
</option>
</select>
<template
v-if=
"resetMode === 'fixed'"
>
<!-- Weekly: day of week selector -->
<template
v-if=
"dim === 'weekly'"
>
<label
class=
"text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"
>
{{
t
(
'
admin.accounts.quotaWeeklyResetDay
'
)
}}
</label>
<select
:value=
"resetDay ?? 1"
@
change=
"emit('update:resetDay', Number(($event.target as HTMLSelectElement).value))"
class=
"input py-1 text-xs w-28"
>
<option
v-for=
"d in dayOptions"
:key=
"d.value"
:value=
"d.value"
>
{{
t
(
'
admin.accounts.dayOfWeek.
'
+
d
.
key
)
}}
</option>
</select>
</
template
>
<label
class=
"text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"
>
{{ t('admin.accounts.quotaResetHour') }}
</label>
<select
:value=
"resetHour ?? 0"
@
change=
"emit('update:resetHour', Number(($event.target as HTMLSelectElement).value))"
class=
"input py-1 text-xs w-24"
>
<option
v-for=
"h in hourOptions"
:key=
"h"
:value=
"h"
>
{{ String(h).padStart(2, '0') }}:00
</option>
</select>
<
template
v-if=
"timezoneOptions && timezoneOptions.length > 0"
>
<select
:value=
"resetTimezone || 'UTC'"
@
change=
"emit('update:resetTimezone', ($event.target as HTMLSelectElement).value)"
class=
"input py-1 text-xs w-auto"
>
<option
v-for=
"tz in timezoneOptions"
:key=
"tz"
:value=
"tz"
>
{{
tz
}}
(
{{
getTimezoneOffsetLabel
(
tz
)
}}
)
</option>
</select>
</
template
>
</template>
<span
class=
"text-[11px] text-gray-500 dark:text-gray-400"
>
<
template
v-if=
"resetMode === 'fixed'"
>
{{
hintFixed
}}
</
template
>
<
template
v-else
>
{{
hintRolling
}}
</
template
>
</span>
</div>
<!-- Total dimension hint (no reset mode) -->
<p
v-if=
"!hasResetMode"
class=
"input-hint mb-0 text-[11px]"
>
{{ hintRolling }}
</p>
</div>
</template>
frontend/src/components/account/QuotaLimitCard.vue
View file @
0b746501
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
watch
,
computed
}
from
'
vue
'
import
{
ref
,
watch
,
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
QuotaDimensionRow
from
'
./QuotaDimensionRow.vue
'
import
type
{
QuotaThresholdType
,
QuotaResetMode
}
from
'
@/constants/account
'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
props
=
defineProps
<
{
const
props
=
withDefaults
(
defineProps
<
{
totalLimit
:
number
|
null
totalLimit
:
number
|
null
dailyLimit
:
number
|
null
dailyLimit
:
number
|
null
weeklyLimit
:
number
|
null
weeklyLimit
:
number
|
null
dailyResetMode
:
'
rolling
'
|
'
fixed
'
|
null
dailyResetMode
:
QuotaResetMode
|
null
dailyResetHour
:
number
|
null
dailyResetHour
:
number
|
null
weeklyResetMode
:
'
rolling
'
|
'
fixed
'
|
null
weeklyResetMode
:
QuotaResetMode
|
null
weeklyResetDay
:
number
|
null
weeklyResetDay
:
number
|
null
weeklyResetHour
:
number
|
null
weeklyResetHour
:
number
|
null
resetTimezone
:
string
|
null
resetTimezone
:
string
|
null
}
>
()
quotaNotifyGlobalEnabled
?:
boolean
quotaNotifyDailyEnabled
?:
boolean
|
null
quotaNotifyDailyThreshold
?:
number
|
null
quotaNotifyDailyThresholdType
?:
QuotaThresholdType
|
null
quotaNotifyWeeklyEnabled
?:
boolean
|
null
quotaNotifyWeeklyThreshold
?:
number
|
null
quotaNotifyWeeklyThresholdType
?:
QuotaThresholdType
|
null
quotaNotifyTotalEnabled
?:
boolean
|
null
quotaNotifyTotalThreshold
?:
number
|
null
quotaNotifyTotalThresholdType
?:
QuotaThresholdType
|
null
}
>
(),
{
quotaNotifyGlobalEnabled
:
false
,
quotaNotifyDailyEnabled
:
null
,
quotaNotifyDailyThreshold
:
null
,
quotaNotifyDailyThresholdType
:
null
,
quotaNotifyWeeklyEnabled
:
null
,
quotaNotifyWeeklyThreshold
:
null
,
quotaNotifyWeeklyThresholdType
:
null
,
quotaNotifyTotalEnabled
:
null
,
quotaNotifyTotalThreshold
:
null
,
quotaNotifyTotalThresholdType
:
null
,
})
const
emit
=
defineEmits
<
{
const
emit
=
defineEmits
<
{
'
update:totalLimit
'
:
[
value
:
number
|
null
]
'
update:totalLimit
'
:
[
value
:
number
|
null
]
'
update:dailyLimit
'
:
[
value
:
number
|
null
]
'
update:dailyLimit
'
:
[
value
:
number
|
null
]
'
update:weeklyLimit
'
:
[
value
:
number
|
null
]
'
update:weeklyLimit
'
:
[
value
:
number
|
null
]
'
update:dailyResetMode
'
:
[
value
:
'
rolling
'
|
'
fixed
'
|
null
]
'
update:dailyResetMode
'
:
[
value
:
QuotaResetMode
|
null
]
'
update:dailyResetHour
'
:
[
value
:
number
|
null
]
'
update:dailyResetHour
'
:
[
value
:
number
|
null
]
'
update:weeklyResetMode
'
:
[
value
:
'
rolling
'
|
'
fixed
'
|
null
]
'
update:weeklyResetMode
'
:
[
value
:
QuotaResetMode
|
null
]
'
update:weeklyResetDay
'
:
[
value
:
number
|
null
]
'
update:weeklyResetDay
'
:
[
value
:
number
|
null
]
'
update:weeklyResetHour
'
:
[
value
:
number
|
null
]
'
update:weeklyResetHour
'
:
[
value
:
number
|
null
]
'
update:resetTimezone
'
:
[
value
:
string
|
null
]
'
update:resetTimezone
'
:
[
value
:
string
|
null
]
'
update:quotaNotifyDailyEnabled
'
:
[
value
:
boolean
|
null
]
'
update:quotaNotifyDailyThreshold
'
:
[
value
:
number
|
null
]
'
update:quotaNotifyDailyThresholdType
'
:
[
value
:
QuotaThresholdType
|
null
]
'
update:quotaNotifyWeeklyEnabled
'
:
[
value
:
boolean
|
null
]
'
update:quotaNotifyWeeklyThreshold
'
:
[
value
:
number
|
null
]
'
update:quotaNotifyWeeklyThresholdType
'
:
[
value
:
QuotaThresholdType
|
null
]
'
update:quotaNotifyTotalEnabled
'
:
[
value
:
boolean
|
null
]
'
update:quotaNotifyTotalThreshold
'
:
[
value
:
number
|
null
]
'
update:quotaNotifyTotalThresholdType
'
:
[
value
:
QuotaThresholdType
|
null
]
}
>
()
}
>
()
const
enabled
=
computed
(()
=>
const
enabled
=
computed
(()
=>
...
@@ -35,15 +67,17 @@ const enabled = computed(() =>
...
@@ -35,15 +67,17 @@ const enabled = computed(() =>
)
)
const
localEnabled
=
ref
(
enabled
.
value
)
const
localEnabled
=
ref
(
enabled
.
value
)
const
collapsed
=
ref
(
false
)
// Sync when props change externally
// Sync when props change externally
watch
(
enabled
,
(
val
)
=>
{
watch
(
enabled
,
(
val
)
=>
{
localEnabled
.
value
=
val
localEnabled
.
value
=
val
})
})
// When toggle is turned off, clear all values
// When toggle is turned off, clear all values
and expand
watch
(
localEnabled
,
(
val
)
=>
{
watch
(
localEnabled
,
(
val
)
=>
{
if
(
!
val
)
{
if
(
!
val
)
{
collapsed
.
value
=
false
emit
(
'
update:totalLimit
'
,
null
)
emit
(
'
update:totalLimit
'
,
null
)
emit
(
'
update:dailyLimit
'
,
null
)
emit
(
'
update:dailyLimit
'
,
null
)
emit
(
'
update:weeklyLimit
'
,
null
)
emit
(
'
update:weeklyLimit
'
,
null
)
...
@@ -56,31 +90,12 @@ watch(localEnabled, (val) => {
...
@@ -56,31 +90,12 @@ watch(localEnabled, (val) => {
}
}
})
})
// Whether any fixed mode is active (to show timezone selector)
const
hasFixedMode
=
computed
(()
=>
props
.
dailyResetMode
===
'
fixed
'
||
props
.
weeklyResetMode
===
'
fixed
'
)
// Common timezone options
// Common timezone options
const
timezoneOptions
=
[
const
timezoneOptions
=
[
'
UTC
'
,
'
UTC
'
,
'
Asia/Shanghai
'
,
'
Asia/Tokyo
'
,
'
Asia/Seoul
'
,
'
Asia/Singapore
'
,
'
Asia/Kolkata
'
,
'
Asia/Shanghai
'
,
'
Asia/Dubai
'
,
'
Europe/London
'
,
'
Europe/Paris
'
,
'
Europe/Berlin
'
,
'
Europe/Moscow
'
,
'
Asia/Tokyo
'
,
'
America/New_York
'
,
'
America/Chicago
'
,
'
America/Denver
'
,
'
America/Los_Angeles
'
,
'
Asia/Seoul
'
,
'
America/Sao_Paulo
'
,
'
Australia/Sydney
'
,
'
Pacific/Auckland
'
,
'
Asia/Singapore
'
,
'
Asia/Kolkata
'
,
'
Asia/Dubai
'
,
'
Europe/London
'
,
'
Europe/Paris
'
,
'
Europe/Berlin
'
,
'
Europe/Moscow
'
,
'
America/New_York
'
,
'
America/Chicago
'
,
'
America/Denver
'
,
'
America/Los_Angeles
'
,
'
America/Sao_Paulo
'
,
'
Australia/Sydney
'
,
'
Pacific/Auckland
'
,
]
]
// Hours for dropdown (0-23)
// Hours for dropdown (0-23)
...
@@ -97,47 +112,38 @@ const dayOptions = [
...
@@ -97,47 +112,38 @@ const dayOptions = [
{
value
:
0
,
key
:
'
sunday
'
},
{
value
:
0
,
key
:
'
sunday
'
},
]
]
const
onTotalInput
=
(
e
:
Event
)
=>
{
// Precomputed hint strings for the weekly fixed mode
const
raw
=
(
e
.
target
as
HTMLInputElement
).
valueAsNumber
const
weeklyFixedHint
=
computed
(()
=>
{
emit
(
'
update:totalLimit
'
,
Number
.
isNaN
(
raw
)
?
null
:
raw
)
const
dayKey
=
dayOptions
.
find
(
d
=>
d
.
value
===
(
props
.
weeklyResetDay
??
1
))?.
key
||
'
monday
'
}
return
t
(
'
admin.accounts.quotaWeeklyLimitHintFixed
'
,
{
const
onDailyInput
=
(
e
:
Event
)
=>
{
day
:
t
(
'
admin.accounts.dayOfWeek.
'
+
dayKey
),
const
raw
=
(
e
.
target
as
HTMLInputElement
).
valueAsNumber
hour
:
String
(
props
.
weeklyResetHour
??
0
).
padStart
(
2
,
'
0
'
),
emit
(
'
update:dailyLimit
'
,
Number
.
isNaN
(
raw
)
?
null
:
raw
)
timezone
:
props
.
resetTimezone
||
'
UTC
'
,
}
})
const
onWeeklyInput
=
(
e
:
Event
)
=>
{
})
const
raw
=
(
e
.
target
as
HTMLInputElement
).
valueAsNumber
emit
(
'
update:weeklyLimit
'
,
Number
.
isNaN
(
raw
)
?
null
:
raw
)
}
const
onDailyModeChange
=
(
e
:
Event
)
=>
{
const
val
=
(
e
.
target
as
HTMLSelectElement
).
value
as
'
rolling
'
|
'
fixed
'
emit
(
'
update:dailyResetMode
'
,
val
)
if
(
val
===
'
fixed
'
)
{
if
(
props
.
dailyResetHour
==
null
)
emit
(
'
update:dailyResetHour
'
,
0
)
if
(
!
props
.
resetTimezone
)
emit
(
'
update:resetTimezone
'
,
'
UTC
'
)
}
}
const
onWeeklyModeChange
=
(
e
:
Event
)
=>
{
const
dailyFixedHint
=
computed
(()
=>
const
val
=
(
e
.
target
as
HTMLSelectElement
).
value
as
'
rolling
'
|
'
fixed
'
t
(
'
admin.accounts.quotaDailyLimitHintFixed
'
,
{
emit
(
'
update:weeklyResetMode
'
,
val
)
hour
:
String
(
props
.
dailyResetHour
??
0
).
padStart
(
2
,
'
0
'
),
if
(
val
===
'
fixed
'
)
{
timezone
:
props
.
resetTimezone
||
'
UTC
'
,
if
(
props
.
weeklyResetDay
==
null
)
emit
(
'
update:weeklyResetDay
'
,
1
)
})
if
(
props
.
weeklyResetHour
==
null
)
emit
(
'
update:weeklyResetHour
'
,
0
)
)
if
(
!
props
.
resetTimezone
)
emit
(
'
update:resetTimezone
'
,
'
UTC
'
)
}
}
</
script
>
</
script
>
<
template
>
<
template
>
<div
class=
"rounded-lg border border-gray-200 p-4 dark:border-dark-600"
>
<div
class=
"rounded-lg border border-gray-200 dark:border-dark-600"
>
<div
class=
"mb-3 flex items-center justify-between"
>
<!-- Header: toggle + collapse -->
<div>
<div
class=
"flex items-center justify-between p-4"
:class=
"
{ 'pb-0': localEnabled
&&
!collapsed }">
<label
class=
"input-label mb-0"
>
{{
t
(
'
admin.accounts.quotaLimitToggle
'
)
}}
</label>
<div
class=
"flex items-center gap-2 flex-1 cursor-pointer"
@
click=
"localEnabled && (collapsed = !collapsed)"
>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
<svg
v-if=
"localEnabled"
class=
"h-4 w-4 text-gray-400 transition-transform"
:class=
"
{ '-rotate-90': collapsed }" viewBox="0 0 20 20" fill="currentColor">
{{
t
(
'
admin.accounts.quotaLimitToggleHint
'
)
}}
<path
fill-rule=
"evenodd"
d=
"M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule=
"evenodd"
/>
</p>
</svg>
<div>
<label
class=
"input-label mb-0 cursor-pointer"
>
{{
t
(
'
admin.accounts.quotaLimitToggle
'
)
}}
</label>
<p
class=
"mt-0.5 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.accounts.quotaLimitToggleHint
'
)
}}
</p>
</div>
</div>
</div>
<button
<button
type=
"button"
type=
"button"
...
@@ -156,140 +162,85 @@ const onWeeklyModeChange = (e: Event) => {
...
@@ -156,140 +162,85 @@ const onWeeklyModeChange = (e: Event) => {
</button>
</button>
</div>
</div>
<div
v-if=
"localEnabled"
class=
"space-y-3"
>
<!-- Collapsible content -->
<!-- 日配额 -->
<div
v-if=
"localEnabled && !collapsed"
class=
"space-y-2 p-4 pt-3"
>
<div>
<!-- Daily quota -->
<label
class=
"input-label"
>
{{
t
(
'
admin.accounts.quotaDailyLimit
'
)
}}
</label>
<QuotaDimensionRow
<div
class=
"relative"
>
dim=
"daily"
<span
class=
"absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400"
>
$
</span>
:label=
"t('admin.accounts.quotaDailyLimit')"
<input
:limit=
"dailyLimit"
:value=
"dailyLimit"
:quota-notify-global-enabled=
"quotaNotifyGlobalEnabled"
@
input=
"onDailyInput"
:notify-enabled=
"props.quotaNotifyDailyEnabled"
type=
"number"
:notify-threshold=
"props.quotaNotifyDailyThreshold"
min=
"0"
:notify-threshold-type=
"props.quotaNotifyDailyThresholdType"
step=
"0.01"
:reset-mode=
"dailyResetMode"
class=
"input pl-7"
:reset-hour=
"dailyResetHour"
:placeholder=
"t('admin.accounts.quotaLimitPlaceholder')"
:reset-day=
"null"
/>
:reset-timezone=
"resetTimezone"
</div>
:hint-rolling=
"t('admin.accounts.quotaDailyLimitHint')"
<!-- 日配额重置模式 -->
:hint-fixed=
"dailyFixedHint"
<div
class=
"mt-2 flex items-center gap-2"
>
:hour-options=
"hourOptions"
<label
class=
"text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"
>
{{
t
(
'
admin.accounts.quotaResetMode
'
)
}}
</label>
:day-options=
"dayOptions"
<select
:timezone-options=
"timezoneOptions"
:value=
"dailyResetMode || 'rolling'"
@
update:limit=
"emit('update:dailyLimit', $event)"
@
change=
"onDailyModeChange"
@
update:notify-enabled=
"emit('update:quotaNotifyDailyEnabled', $event)"
class=
"input py-1 text-xs"
@
update:notify-threshold=
"emit('update:quotaNotifyDailyThreshold', $event)"
>
@
update:notify-threshold-type=
"emit('update:quotaNotifyDailyThresholdType', $event)"
<option
value=
"rolling"
>
{{
t
(
'
admin.accounts.quotaResetModeRolling
'
)
}}
</option>
@
update:reset-mode=
"emit('update:dailyResetMode', $event)"
<option
value=
"fixed"
>
{{
t
(
'
admin.accounts.quotaResetModeFixed
'
)
}}
</option>
@
update:reset-hour=
"emit('update:dailyResetHour', $event)"
</select>
@
update:reset-timezone=
"emit('update:resetTimezone', $event)"
</div>
/>
<!-- 固定模式:小时选择 -->
<div
v-if=
"dailyResetMode === 'fixed'"
class=
"mt-2 flex items-center gap-2"
>
<!-- Weekly quota -->
<label
class=
"text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"
>
{{
t
(
'
admin.accounts.quotaResetHour
'
)
}}
</label>
<QuotaDimensionRow
<select
dim=
"weekly"
:value=
"dailyResetHour ?? 0"
:label=
"t('admin.accounts.quotaWeeklyLimit')"
@
change=
"emit('update:dailyResetHour', Number(($event.target as HTMLSelectElement).value))"
:limit=
"weeklyLimit"
class=
"input py-1 text-xs w-24"
:quota-notify-global-enabled=
"quotaNotifyGlobalEnabled"
>
:notify-enabled=
"props.quotaNotifyWeeklyEnabled"
<option
v-for=
"h in hourOptions"
:key=
"h"
:value=
"h"
>
{{
String
(
h
).
padStart
(
2
,
'
0
'
)
}}
:00
</option>
:notify-threshold=
"props.quotaNotifyWeeklyThreshold"
</select>
:notify-threshold-type=
"props.quotaNotifyWeeklyThresholdType"
</div>
:reset-mode=
"weeklyResetMode"
<p
class=
"input-hint"
>
:reset-hour=
"weeklyResetHour"
<template
v-if=
"dailyResetMode === 'fixed'"
>
:reset-day=
"weeklyResetDay"
{{
t
(
'
admin.accounts.quotaDailyLimitHintFixed
'
,
{
hour
:
String
(
dailyResetHour
??
0
).
padStart
(
2
,
'
0
'
),
timezone
:
resetTimezone
||
'
UTC
'
}
)
}}
:reset-timezone=
"resetTimezone"
<
/template
>
:hint-rolling=
"t('admin.accounts.quotaWeeklyLimitHint')"
<
template
v
-
else
>
:hint-fixed=
"weeklyFixedHint"
{{
t
(
'
admin.accounts.quotaDailyLimitHint
'
)
}}
:hour-options=
"hourOptions"
<
/template
>
:day-options=
"dayOptions"
<
/p
>
:timezone-options=
"timezoneOptions"
<
/div
>
@
update:limit=
"emit('update:weeklyLimit', $event)"
@
update:notify-enabled=
"emit('update:quotaNotifyWeeklyEnabled', $event)"
<!--
周配额
-->
@
update:notify-threshold=
"emit('update:quotaNotifyWeeklyThreshold', $event)"
<
div
>
@
update:notify-threshold-type=
"emit('update:quotaNotifyWeeklyThresholdType', $event)"
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.quotaWeeklyLimit
'
)
}}
<
/label
>
@
update:reset-mode=
"emit('update:weeklyResetMode', $event)"
<
div
class
=
"
relative
"
>
@
update:reset-hour=
"emit('update:weeklyResetHour', $event)"
<
span
class
=
"
absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400
"
>
$
<
/span
>
@
update:reset-day=
"emit('update:weeklyResetDay', $event)"
<
input
@
update:reset-timezone=
"emit('update:resetTimezone', $event)"
:
value
=
"
weeklyLimit
"
/>
@
input
=
"
onWeeklyInput
"
type
=
"
number
"
<!-- Total quota -->
min
=
"
0
"
<QuotaDimensionRow
step
=
"
0.01
"
dim=
"total"
class
=
"
input pl-7
"
:label=
"t('admin.accounts.quotaTotalLimit')"
:
placeholder
=
"
t('admin.accounts.quotaLimitPlaceholder')
"
:limit=
"totalLimit"
/>
:quota-notify-global-enabled=
"quotaNotifyGlobalEnabled"
<
/div
>
:notify-enabled=
"props.quotaNotifyTotalEnabled"
<!--
周配额重置模式
-->
:notify-threshold=
"props.quotaNotifyTotalThreshold"
<
div
class
=
"
mt-2 flex items-center gap-2
"
>
:notify-threshold-type=
"props.quotaNotifyTotalThresholdType"
<
label
class
=
"
text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap
"
>
{{
t
(
'
admin.accounts.quotaResetMode
'
)
}}
<
/label
>
:reset-mode=
"null"
<
select
:reset-hour=
"null"
:
value
=
"
weeklyResetMode || 'rolling'
"
:reset-day=
"null"
@
change
=
"
onWeeklyModeChange
"
:reset-timezone=
"null"
class
=
"
input py-1 text-xs
"
:hint-rolling=
"t('admin.accounts.quotaTotalLimitHint')"
>
hint-fixed=
""
<
option
value
=
"
rolling
"
>
{{
t
(
'
admin.accounts.quotaResetModeRolling
'
)
}}
<
/option
>
:hour-options=
"hourOptions"
<
option
value
=
"
fixed
"
>
{{
t
(
'
admin.accounts.quotaResetModeFixed
'
)
}}
<
/option
>
:day-options=
"dayOptions"
<
/select
>
@
update:limit=
"emit('update:totalLimit', $event)"
<
/div
>
@
update:notify-enabled=
"emit('update:quotaNotifyTotalEnabled', $event)"
<!--
固定模式
:
星期几
+
小时
-->
@
update:notify-threshold=
"emit('update:quotaNotifyTotalThreshold', $event)"
<
div
v
-
if
=
"
weeklyResetMode === 'fixed'
"
class
=
"
mt-2 flex items-center gap-2 flex-wrap
"
>
@
update:notify-threshold-type=
"emit('update:quotaNotifyTotalThresholdType', $event)"
<
label
class
=
"
text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap
"
>
{{
t
(
'
admin.accounts.quotaWeeklyResetDay
'
)
}}
<
/label
>
/>
<
select
:
value
=
"
weeklyResetDay ?? 1
"
@
change
=
"
emit('update:weeklyResetDay', Number(($event.target as HTMLSelectElement).value))
"
class
=
"
input py-1 text-xs w-28
"
>
<
option
v
-
for
=
"
d in dayOptions
"
:
key
=
"
d.value
"
:
value
=
"
d.value
"
>
{{
t
(
'
admin.accounts.dayOfWeek.
'
+
d
.
key
)
}}
<
/option
>
<
/select
>
<
label
class
=
"
text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap
"
>
{{
t
(
'
admin.accounts.quotaResetHour
'
)
}}
<
/label
>
<
select
:
value
=
"
weeklyResetHour ?? 0
"
@
change
=
"
emit('update:weeklyResetHour', Number(($event.target as HTMLSelectElement).value))
"
class
=
"
input py-1 text-xs w-24
"
>
<
option
v
-
for
=
"
h in hourOptions
"
:
key
=
"
h
"
:
value
=
"
h
"
>
{{
String
(
h
).
padStart
(
2
,
'
0
'
)
}}
:
00
<
/option
>
<
/select
>
<
/div
>
<
p
class
=
"
input-hint
"
>
<
template
v
-
if
=
"
weeklyResetMode === 'fixed'
"
>
{{
t
(
'
admin.accounts.quotaWeeklyLimitHintFixed
'
,
{
day
:
t
(
'
admin.accounts.dayOfWeek.
'
+
(
dayOptions
.
find
(
d
=>
d
.
value
===
(
weeklyResetDay
??
1
))?.
key
||
'
monday
'
)),
hour
:
String
(
weeklyResetHour
??
0
).
padStart
(
2
,
'
0
'
),
timezone
:
resetTimezone
||
'
UTC
'
}
)
}}
<
/template
>
<
template
v
-
else
>
{{
t
(
'
admin.accounts.quotaWeeklyLimitHint
'
)
}}
<
/template
>
<
/p
>
<
/div
>
<!--
时区选择
(
当任一维度使用固定模式时显示
)
-->
<
div
v
-
if
=
"
hasFixedMode
"
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.quotaResetTimezone
'
)
}}
<
/label
>
<
select
:
value
=
"
resetTimezone || 'UTC'
"
@
change
=
"
emit('update:resetTimezone', ($event.target as HTMLSelectElement).value)
"
class
=
"
input text-sm
"
>
<
option
v
-
for
=
"
tz in timezoneOptions
"
:
key
=
"
tz
"
:
value
=
"
tz
"
>
{{
tz
}}
<
/option
>
<
/select
>
<
/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>
</div>
</div>
</
template
>
</
template
>
frontend/src/components/account/QuotaNotifyToggle.vue
0 → 100644
View file @
0b746501
<
script
setup
lang=
"ts"
>
import
{
QUOTA_THRESHOLD_TYPE_FIXED
,
QUOTA_THRESHOLD_TYPE_PERCENTAGE
,
type
QuotaThresholdType
}
from
'
@/constants/account
'
defineProps
<
{
enabled
:
boolean
|
null
threshold
:
number
|
null
thresholdType
:
QuotaThresholdType
|
null
}
>
()
const
emit
=
defineEmits
<
{
'
update:enabled
'
:
[
value
:
boolean
|
null
]
'
update:threshold
'
:
[
value
:
number
|
null
]
'
update:thresholdType
'
:
[
value
:
QuotaThresholdType
|
null
]
}
>
()
</
script
>
<
template
>
<div
class=
"flex items-center gap-1.5"
>
<button
type=
"button"
@
click=
"emit('update:enabled', !enabled)"
:class=
"[
'relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none',
enabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
]"
>
<span
:class=
"[
'pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
enabled ? 'translate-x-4' : 'translate-x-0'
]"
/>
</button>
<template
v-if=
"enabled"
>
<input
:value=
"threshold"
@
input=
"emit('update:threshold', parseFloat(($event.target as HTMLInputElement).value) || null)"
type=
"number"
min=
"0"
:max=
"thresholdType === QUOTA_THRESHOLD_TYPE_PERCENTAGE ? 100 : undefined"
:step=
"thresholdType === QUOTA_THRESHOLD_TYPE_PERCENTAGE ? 1 : 0.01"
class=
"input py-1 text-sm flex-1 min-w-0"
/>
<select
:value=
"thresholdType || QUOTA_THRESHOLD_TYPE_FIXED"
@
change=
"emit('update:thresholdType', ($event.target as HTMLSelectElement).value as QuotaThresholdType)"
class=
"input py-1 text-xs w-[4.5rem] flex-shrink-0 text-center"
>
<option
:value=
"QUOTA_THRESHOLD_TYPE_FIXED"
>
$
</option>
<option
:value=
"QUOTA_THRESHOLD_TYPE_PERCENTAGE"
>
%
</option>
</select>
</
template
>
</div>
</template>
frontend/src/components/admin/account/AccountTestModal.vue
View file @
0b746501
...
@@ -165,7 +165,6 @@
...
@@ -165,7 +165,6 @@
<button
<button
@
click=
"handleClose"
@
click=
"handleClose"
class=
"rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
class=
"rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-dark-500"
:disabled=
"status === 'connecting'"
>
>
{{
t
(
'
common.close
'
)
}}
{{
t
(
'
common.close
'
)
}}
</button>
</button>
...
@@ -249,7 +248,7 @@ const availableModels = ref<ClaudeModel[]>([])
...
@@ -249,7 +248,7 @@ const availableModels = ref<ClaudeModel[]>([])
const
selectedModelId
=
ref
(
''
)
const
selectedModelId
=
ref
(
''
)
const
testPrompt
=
ref
(
''
)
const
testPrompt
=
ref
(
''
)
const
loadingModels
=
ref
(
false
)
const
loadingModels
=
ref
(
false
)
let
eventSource
:
EventSource
|
null
=
null
let
abortController
:
AbortController
|
null
=
null
const
generatedImages
=
ref
<
PreviewImage
[]
>
([])
const
generatedImages
=
ref
<
PreviewImage
[]
>
([])
const
prioritizedGeminiModels
=
[
'
gemini-3.1-flash-image
'
,
'
gemini-2.5-flash-image
'
,
'
gemini-2.5-flash
'
,
'
gemini-2.5-pro
'
,
'
gemini-3-flash-preview
'
,
'
gemini-3-pro-preview
'
,
'
gemini-2.0-flash
'
]
const
prioritizedGeminiModels
=
[
'
gemini-3.1-flash-image
'
,
'
gemini-2.5-flash-image
'
,
'
gemini-2.5-flash
'
,
'
gemini-2.5-pro
'
,
'
gemini-3-flash-preview
'
,
'
gemini-3-pro-preview
'
,
'
gemini-2.0-flash
'
]
const
supportsGeminiImageTest
=
computed
(()
=>
{
const
supportsGeminiImageTest
=
computed
(()
=>
{
...
@@ -279,7 +278,7 @@ watch(
...
@@ -279,7 +278,7 @@ watch(
resetState
()
resetState
()
await
loadAvailableModels
()
await
loadAvailableModels
()
}
else
{
}
else
{
closeEventSource
()
abortStream
()
}
}
}
}
)
)
...
@@ -329,18 +328,14 @@ const resetState = () => {
...
@@ -329,18 +328,14 @@ const resetState = () => {
}
}
const
handleClose
=
()
=>
{
const
handleClose
=
()
=>
{
// 防止在连接测试进行中关闭对话框
abortStream
()
if
(
status
.
value
===
'
connecting
'
)
{
return
}
closeEventSource
()
emit
(
'
close
'
)
emit
(
'
close
'
)
}
}
const
closeEventSource
=
()
=>
{
const
abortStream
=
()
=>
{
if
(
eventSource
)
{
if
(
abortController
)
{
eventSource
.
close
()
abortController
.
abort
()
eventSource
=
null
abortController
=
null
}
}
}
}
...
@@ -365,7 +360,9 @@ const startTest = async () => {
...
@@ -365,7 +360,9 @@ const startTest = async () => {
addLine
(
t
(
'
admin.accounts.testAccountTypeLabel
'
,
{
type
:
props
.
account
.
type
}),
'
text-gray-400
'
)
addLine
(
t
(
'
admin.accounts.testAccountTypeLabel
'
,
{
type
:
props
.
account
.
type
}),
'
text-gray-400
'
)
addLine
(
''
,
'
text-gray-300
'
)
addLine
(
''
,
'
text-gray-300
'
)
closeEventSource
()
abortStream
()
abortController
=
new
AbortController
()
try
{
try
{
// Create EventSource for SSE
// Create EventSource for SSE
...
@@ -381,7 +378,8 @@ const startTest = async () => {
...
@@ -381,7 +378,8 @@ const startTest = async () => {
body
:
JSON
.
stringify
({
body
:
JSON
.
stringify
({
model_id
:
selectedModelId
.
value
,
model_id
:
selectedModelId
.
value
,
prompt
:
supportsGeminiImageTest
.
value
?
testPrompt
.
value
.
trim
()
:
''
prompt
:
supportsGeminiImageTest
.
value
?
testPrompt
.
value
.
trim
()
:
''
})
}),
signal
:
abortController
.
signal
})
})
if
(
!
response
.
ok
)
{
if
(
!
response
.
ok
)
{
...
@@ -418,10 +416,15 @@ const startTest = async () => {
...
@@ -418,10 +416,15 @@ const startTest = async () => {
}
}
}
}
}
}
}
catch
(
error
:
any
)
{
}
catch
(
error
:
unknown
)
{
if
(
error
instanceof
DOMException
&&
error
.
name
===
'
AbortError
'
)
{
status
.
value
=
'
idle
'
return
}
status
.
value
=
'
error
'
status
.
value
=
'
error
'
errorMessage
.
value
=
error
.
message
||
'
Unknown error
'
const
msg
=
error
instanceof
Error
?
error
.
message
:
'
Unknown error
'
addLine
(
`Error:
${
errorMessage
.
value
}
`
,
'
text-red-400
'
)
errorMessage
.
value
=
msg
addLine
(
`Error:
${
msg
}
`
,
'
text-red-400
'
)
}
}
}
}
...
...
frontend/src/components/admin/payment/AdminOrderDetail.vue
View file @
0b746501
...
@@ -18,12 +18,20 @@
...
@@ -18,12 +18,20 @@
</span>
</span>
</div>
</div>
<div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.amount
'
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.baseAmount
'
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
$
{{
order
.
amount
.
toFixed
(
2
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
¥
{{
baseAmount
.
toFixed
(
2
)
}}
</p>
</div>
<div
v-if=
"order.fee_rate > 0"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.fee
'
)
}}
(
{{
order
.
fee_rate
}}
%)
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
¥
{{
feeAmount
.
toFixed
(
2
)
}}
</p>
</div>
</div>
<div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.payAmount
'
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.payAmount
'
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
$
{{
order
.
pay_amount
.
toFixed
(
2
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
¥
{{
order
.
pay_amount
.
toFixed
(
2
)
}}
</p>
</div>
<div
v-if=
"order.amount !== order.pay_amount"
>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.creditedAmount
'
)
}}
</p>
<p
class=
"text-sm font-medium text-gray-900 dark:text-white"
>
{{
order
.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
order
.
amount
.
toFixed
(
2
)
}}
</p>
</div>
</div>
<div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.paymentMethod
'
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.orders.paymentMethod
'
)
}}
</p>
...
@@ -31,10 +39,6 @@
...
@@ -31,10 +39,6 @@
{{
t
(
'
payment.methods.
'
+
order
.
payment_type
,
order
.
payment_type
)
}}
{{
t
(
'
payment.methods.
'
+
order
.
payment_type
,
order
.
payment_type
)
}}
</p>
</p>
</div>
</div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.feeRate
'
)
}}
</p>
<p
class=
"text-sm text-gray-700 dark:text-gray-300"
>
{{
(
order
.
fee_rate
*
100
).
toFixed
(
1
)
}}
%
</p>
</div>
<div>
<div>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.orderType
'
)
}}
</p>
<p
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
payment.admin.orderType
'
)
}}
</p>
<p
class=
"text-sm text-gray-700 dark:text-gray-300"
>
<p
class=
"text-sm text-gray-700 dark:text-gray-300"
>
...
@@ -73,7 +77,7 @@
...
@@ -73,7 +77,7 @@
<div
class=
"grid grid-cols-2 gap-2 text-sm"
>
<div
class=
"grid grid-cols-2 gap-2 text-sm"
>
<div>
<div>
<span
class=
"text-red-600 dark:text-red-400"
>
{{
t
(
'
payment.admin.refundAmount
'
)
}}
:
</span>
<span
class=
"text-red-600 dark:text-red-400"
>
{{
t
(
'
payment.admin.refundAmount
'
)
}}
:
</span>
<span
class=
"ml-1 font-medium text-red-700 dark:text-red-300"
>
$
{{
order
.
refund_amount
.
toFixed
(
2
)
}}
</span>
<span
class=
"ml-1 font-medium text-red-700 dark:text-red-300"
>
{{
order
.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}
{{
order
.
refund_amount
.
toFixed
(
2
)
}}
</span>
</div>
</div>
<div
v-if=
"order.refund_reason"
class=
"col-span-2"
>
<div
v-if=
"order.refund_reason"
class=
"col-span-2"
>
<span
class=
"text-red-600 dark:text-red-400"
>
{{
t
(
'
payment.admin.refundReason
'
)
}}
:
</span>
<span
class=
"text-red-600 dark:text-red-400"
>
{{
t
(
'
payment.admin.refundReason
'
)
}}
:
</span>
...
@@ -110,6 +114,7 @@
...
@@ -110,6 +114,7 @@
</
template
>
</
template
>
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
...
@@ -117,11 +122,24 @@ import { statusBadgeClass, canRefund as canRefundStatus, formatOrderDateTime } f
...
@@ -117,11 +122,24 @@ import { statusBadgeClass, canRefund as canRefundStatus, formatOrderDateTime } f
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
defineProps
<
{
const
props
=
defineProps
<
{
show
:
boolean
show
:
boolean
order
:
PaymentOrder
|
null
order
:
PaymentOrder
|
null
}
>
()
}
>
()
/** 充值金额 (base amount before fee) = pay_amount - fee = pay_amount / (1 + fee_rate/100) */
const
baseAmount
=
computed
(()
=>
{
if
(
!
props
.
order
)
return
0
if
(
props
.
order
.
fee_rate
<=
0
)
return
props
.
order
.
pay_amount
return
props
.
order
.
pay_amount
/
(
1
+
props
.
order
.
fee_rate
/
100
)
})
/** 手续费 = pay_amount - baseAmount */
const
feeAmount
=
computed
(()
=>
{
if
(
!
props
.
order
||
props
.
order
.
fee_rate
<=
0
)
return
0
return
props
.
order
.
pay_amount
-
baseAmount
.
value
})
const
emit
=
defineEmits
<
{
const
emit
=
defineEmits
<
{
(
e
:
'
close
'
):
void
(
e
:
'
close
'
):
void
(
e
:
'
cancel
'
,
order
:
PaymentOrder
):
void
(
e
:
'
cancel
'
,
order
:
PaymentOrder
):
void
...
...
frontend/src/components/admin/payment/AdminOrderTable.vue
View file @
0b746501
...
@@ -51,12 +51,15 @@
...
@@ -51,12 +51,15 @@
<span
class=
"text-sm text-gray-600 dark:text-gray-400"
>
#
{{
value
}}
</span>
<span
class=
"text-sm text-gray-600 dark:text-gray-400"
>
#
{{
value
}}
</span>
</
template
>
</
template
>
<
template
#cell-amount=
"{ value, row }"
>
<
template
#cell-
pay_
amount=
"{ value, row }"
>
<div
class=
"text-sm"
>
<div
class=
"text-sm"
>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
$
{{
value
.
toFixed
(
2
)
}}
</span>
<span
class=
"font-medium text-gray-900 dark:text-white"
>
¥
{{
value
.
toFixed
(
2
)
}}
</span>
<span
v-if=
"row.
pay_amount !== value
"
class=
"ml-1 text-xs text-gray-
5
00"
>
<span
v-if=
"row.
fee_rate > 0
"
class=
"ml-1 text-xs text-gray-
4
00"
:title=
"t('payment.orders.fee') + ': ' + row.fee_rate + '%'"
>
(
{{
t
(
'
payment.orders.payAmount
'
)
}}
: $
{{
row
.
pay_amount
.
toFixed
(
2
)
}}
)
(
{{
row
.
fee_rate
}}
%
)
</span>
</span>
<div
v-if=
"row.amount !== row.pay_amount"
class=
"text-xs text-gray-500"
>
{{
t
(
'
payment.orders.creditedAmount
'
)
}}
:
{{
row
.
order_type
===
'
balance
'
?
'
$
'
:
'
¥
'
}}{{
row
.
amount
.
toFixed
(
2
)
}}
</div>
</div>
</div>
</
template
>
</
template
>
...
@@ -183,7 +186,7 @@ function emitFiltersChanged() {
...
@@ -183,7 +186,7 @@ function emitFiltersChanged() {
const
columns
=
computed
<
Column
[]
>
(()
=>
[
const
columns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
id
'
,
label
:
t
(
'
payment.orders.orderId
'
)
},
{
key
:
'
id
'
,
label
:
t
(
'
payment.orders.orderId
'
)
},
{
key
:
'
user_id
'
,
label
:
t
(
'
payment.orders.userId
'
)
},
{
key
:
'
user_id
'
,
label
:
t
(
'
payment.orders.userId
'
)
},
{
key
:
'
amount
'
,
label
:
t
(
'
payment.orders.
a
mount
'
)
},
{
key
:
'
pay_
amount
'
,
label
:
t
(
'
payment.orders.
payA
mount
'
)
},
{
key
:
'
payment_type
'
,
label
:
t
(
'
payment.orders.paymentMethod
'
)
},
{
key
:
'
payment_type
'
,
label
:
t
(
'
payment.orders.paymentMethod
'
)
},
{
key
:
'
status
'
,
label
:
t
(
'
payment.orders.status
'
)
},
{
key
:
'
status
'
,
label
:
t
(
'
payment.orders.status
'
)
},
{
key
:
'
order_type
'
,
label
:
t
(
'
payment.orders.orderType
'
)
},
{
key
:
'
order_type
'
,
label
:
t
(
'
payment.orders.orderType
'
)
},
...
...
Prev
1
…
5
6
7
8
9
10
11
12
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