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
90bce60b
Commit
90bce60b
authored
Jan 15, 2026
by
yangjianbo
Browse files
feat: merge dev
parent
a458e684
Changes
107
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/ops_service.go
View file @
90bce60b
...
@@ -208,6 +208,25 @@ func (s *OpsService) RecordError(ctx context.Context, entry *OpsInsertErrorLogIn
...
@@ -208,6 +208,25 @@ func (s *OpsService) RecordError(ctx context.Context, entry *OpsInsertErrorLogIn
out
.
Detail
=
""
out
.
Detail
=
""
}
}
out
.
UpstreamRequestBody
=
strings
.
TrimSpace
(
out
.
UpstreamRequestBody
)
if
out
.
UpstreamRequestBody
!=
""
{
// Reuse the same sanitization/trimming strategy as request body storage.
// Keep it small so it is safe to persist in ops_error_logs JSON.
sanitized
,
truncated
,
_
:=
sanitizeAndTrimRequestBody
([]
byte
(
out
.
UpstreamRequestBody
),
10
*
1024
)
if
sanitized
!=
""
{
out
.
UpstreamRequestBody
=
sanitized
if
truncated
{
out
.
Kind
=
strings
.
TrimSpace
(
out
.
Kind
)
if
out
.
Kind
==
""
{
out
.
Kind
=
"upstream"
}
out
.
Kind
=
out
.
Kind
+
":request_body_truncated"
}
}
else
{
out
.
UpstreamRequestBody
=
""
}
}
// Drop fully-empty events (can happen if only status code was known).
// Drop fully-empty events (can happen if only status code was known).
if
out
.
UpstreamStatusCode
==
0
&&
out
.
Message
==
""
&&
out
.
Detail
==
""
{
if
out
.
UpstreamStatusCode
==
0
&&
out
.
Message
==
""
&&
out
.
Detail
==
""
{
continue
continue
...
@@ -236,7 +255,13 @@ func (s *OpsService) GetErrorLogs(ctx context.Context, filter *OpsErrorLogFilter
...
@@ -236,7 +255,13 @@ func (s *OpsService) GetErrorLogs(ctx context.Context, filter *OpsErrorLogFilter
if
s
.
opsRepo
==
nil
{
if
s
.
opsRepo
==
nil
{
return
&
OpsErrorLogList
{
Errors
:
[]
*
OpsErrorLog
{},
Total
:
0
,
Page
:
1
,
PageSize
:
20
},
nil
return
&
OpsErrorLogList
{
Errors
:
[]
*
OpsErrorLog
{},
Total
:
0
,
Page
:
1
,
PageSize
:
20
},
nil
}
}
return
s
.
opsRepo
.
ListErrorLogs
(
ctx
,
filter
)
result
,
err
:=
s
.
opsRepo
.
ListErrorLogs
(
ctx
,
filter
)
if
err
!=
nil
{
log
.
Printf
(
"[Ops] GetErrorLogs failed: %v"
,
err
)
return
nil
,
err
}
return
result
,
nil
}
}
func
(
s
*
OpsService
)
GetErrorLogByID
(
ctx
context
.
Context
,
id
int64
)
(
*
OpsErrorLogDetail
,
error
)
{
func
(
s
*
OpsService
)
GetErrorLogByID
(
ctx
context
.
Context
,
id
int64
)
(
*
OpsErrorLogDetail
,
error
)
{
...
@@ -256,6 +281,46 @@ func (s *OpsService) GetErrorLogByID(ctx context.Context, id int64) (*OpsErrorLo
...
@@ -256,6 +281,46 @@ func (s *OpsService) GetErrorLogByID(ctx context.Context, id int64) (*OpsErrorLo
return
detail
,
nil
return
detail
,
nil
}
}
func
(
s
*
OpsService
)
ListRetryAttemptsByErrorID
(
ctx
context
.
Context
,
errorID
int64
,
limit
int
)
([]
*
OpsRetryAttempt
,
error
)
{
if
err
:=
s
.
RequireMonitoringEnabled
(
ctx
);
err
!=
nil
{
return
nil
,
err
}
if
s
.
opsRepo
==
nil
{
return
nil
,
infraerrors
.
ServiceUnavailable
(
"OPS_REPO_UNAVAILABLE"
,
"Ops repository not available"
)
}
if
errorID
<=
0
{
return
nil
,
infraerrors
.
BadRequest
(
"OPS_ERROR_INVALID_ID"
,
"invalid error id"
)
}
items
,
err
:=
s
.
opsRepo
.
ListRetryAttemptsByErrorID
(
ctx
,
errorID
,
limit
)
if
err
!=
nil
{
if
errors
.
Is
(
err
,
sql
.
ErrNoRows
)
{
return
[]
*
OpsRetryAttempt
{},
nil
}
return
nil
,
infraerrors
.
InternalServer
(
"OPS_RETRY_LIST_FAILED"
,
"Failed to list retry attempts"
)
.
WithCause
(
err
)
}
return
items
,
nil
}
func
(
s
*
OpsService
)
UpdateErrorResolution
(
ctx
context
.
Context
,
errorID
int64
,
resolved
bool
,
resolvedByUserID
*
int64
,
resolvedRetryID
*
int64
)
error
{
if
err
:=
s
.
RequireMonitoringEnabled
(
ctx
);
err
!=
nil
{
return
err
}
if
s
.
opsRepo
==
nil
{
return
infraerrors
.
ServiceUnavailable
(
"OPS_REPO_UNAVAILABLE"
,
"Ops repository not available"
)
}
if
errorID
<=
0
{
return
infraerrors
.
BadRequest
(
"OPS_ERROR_INVALID_ID"
,
"invalid error id"
)
}
// Best-effort ensure the error exists
if
_
,
err
:=
s
.
opsRepo
.
GetErrorLogByID
(
ctx
,
errorID
);
err
!=
nil
{
if
errors
.
Is
(
err
,
sql
.
ErrNoRows
)
{
return
infraerrors
.
NotFound
(
"OPS_ERROR_NOT_FOUND"
,
"ops error log not found"
)
}
return
infraerrors
.
InternalServer
(
"OPS_ERROR_LOAD_FAILED"
,
"Failed to load ops error log"
)
.
WithCause
(
err
)
}
return
s
.
opsRepo
.
UpdateErrorResolution
(
ctx
,
errorID
,
resolved
,
resolvedByUserID
,
resolvedRetryID
,
nil
)
}
func
sanitizeAndTrimRequestBody
(
raw
[]
byte
,
maxBytes
int
)
(
jsonString
string
,
truncated
bool
,
bytesLen
int
)
{
func
sanitizeAndTrimRequestBody
(
raw
[]
byte
,
maxBytes
int
)
(
jsonString
string
,
truncated
bool
,
bytesLen
int
)
{
bytesLen
=
len
(
raw
)
bytesLen
=
len
(
raw
)
if
len
(
raw
)
==
0
{
if
len
(
raw
)
==
0
{
...
@@ -296,14 +361,34 @@ func sanitizeAndTrimRequestBody(raw []byte, maxBytes int) (jsonString string, tr
...
@@ -296,14 +361,34 @@ func sanitizeAndTrimRequestBody(raw []byte, maxBytes int) (jsonString string, tr
}
}
}
}
// Last resort: store a minimal placeholder (still valid JSON).
// Last resort: keep JSON shape but drop big fields.
placeholder
:=
map
[
string
]
any
{
// This avoids downstream code that expects certain top-level keys from crashing.
"request_body_truncated"
:
true
,
if
root
,
ok
:=
decoded
.
(
map
[
string
]
any
);
ok
{
}
placeholder
:=
shallowCopyMap
(
root
)
if
model
:=
extractString
(
decoded
,
"model"
);
model
!=
""
{
placeholder
[
"request_body_truncated"
]
=
true
placeholder
[
"model"
]
=
model
// Replace potentially huge arrays/strings, but keep the keys present.
for
_
,
k
:=
range
[]
string
{
"messages"
,
"contents"
,
"input"
,
"prompt"
}
{
if
_
,
exists
:=
placeholder
[
k
];
exists
{
placeholder
[
k
]
=
[]
any
{}
}
}
for
_
,
k
:=
range
[]
string
{
"text"
}
{
if
_
,
exists
:=
placeholder
[
k
];
exists
{
placeholder
[
k
]
=
""
}
}
encoded4
,
err4
:=
json
.
Marshal
(
placeholder
)
if
err4
==
nil
{
if
len
(
encoded4
)
<=
maxBytes
{
return
string
(
encoded4
),
true
,
bytesLen
}
}
}
}
encoded4
,
err4
:=
json
.
Marshal
(
placeholder
)
// Final fallback: minimal valid JSON.
encoded4
,
err4
:=
json
.
Marshal
(
map
[
string
]
any
{
"request_body_truncated"
:
true
})
if
err4
!=
nil
{
if
err4
!=
nil
{
return
""
,
true
,
bytesLen
return
""
,
true
,
bytesLen
}
}
...
@@ -526,12 +611,3 @@ func sanitizeErrorBodyForStorage(raw string, maxBytes int) (sanitized string, tr
...
@@ -526,12 +611,3 @@ func sanitizeErrorBodyForStorage(raw string, maxBytes int) (sanitized string, tr
}
}
return
raw
,
false
return
raw
,
false
}
}
func
extractString
(
v
any
,
key
string
)
string
{
root
,
ok
:=
v
.
(
map
[
string
]
any
)
if
!
ok
{
return
""
}
s
,
_
:=
root
[
key
]
.
(
string
)
return
strings
.
TrimSpace
(
s
)
}
backend/internal/service/ops_settings.go
View file @
90bce60b
...
@@ -368,9 +368,11 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings {
...
@@ -368,9 +368,11 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings {
Aggregation
:
OpsAggregationSettings
{
Aggregation
:
OpsAggregationSettings
{
AggregationEnabled
:
false
,
AggregationEnabled
:
false
,
},
},
IgnoreCountTokensErrors
:
false
,
IgnoreCountTokensErrors
:
false
,
AutoRefreshEnabled
:
false
,
IgnoreContextCanceled
:
true
,
// Default to true - client disconnects are not errors
AutoRefreshIntervalSec
:
30
,
IgnoreNoAvailableAccounts
:
false
,
// Default to false - this is a real routing issue
AutoRefreshEnabled
:
false
,
AutoRefreshIntervalSec
:
30
,
}
}
}
}
...
@@ -482,13 +484,11 @@ const SettingKeyOpsMetricThresholds = "ops_metric_thresholds"
...
@@ -482,13 +484,11 @@ const SettingKeyOpsMetricThresholds = "ops_metric_thresholds"
func
defaultOpsMetricThresholds
()
*
OpsMetricThresholds
{
func
defaultOpsMetricThresholds
()
*
OpsMetricThresholds
{
slaMin
:=
99.5
slaMin
:=
99.5
latencyMax
:=
2000.0
ttftMax
:=
500.0
ttftMax
:=
500.0
reqErrMax
:=
5.0
reqErrMax
:=
5.0
upstreamErrMax
:=
5.0
upstreamErrMax
:=
5.0
return
&
OpsMetricThresholds
{
return
&
OpsMetricThresholds
{
SLAPercentMin
:
&
slaMin
,
SLAPercentMin
:
&
slaMin
,
LatencyP99MsMax
:
&
latencyMax
,
TTFTp99MsMax
:
&
ttftMax
,
TTFTp99MsMax
:
&
ttftMax
,
RequestErrorRatePercentMax
:
&
reqErrMax
,
RequestErrorRatePercentMax
:
&
reqErrMax
,
UpstreamErrorRatePercentMax
:
&
upstreamErrMax
,
UpstreamErrorRatePercentMax
:
&
upstreamErrMax
,
...
@@ -538,9 +538,6 @@ func (s *OpsService) UpdateMetricThresholds(ctx context.Context, cfg *OpsMetricT
...
@@ -538,9 +538,6 @@ func (s *OpsService) UpdateMetricThresholds(ctx context.Context, cfg *OpsMetricT
if
cfg
.
SLAPercentMin
!=
nil
&&
(
*
cfg
.
SLAPercentMin
<
0
||
*
cfg
.
SLAPercentMin
>
100
)
{
if
cfg
.
SLAPercentMin
!=
nil
&&
(
*
cfg
.
SLAPercentMin
<
0
||
*
cfg
.
SLAPercentMin
>
100
)
{
return
nil
,
errors
.
New
(
"sla_percent_min must be between 0 and 100"
)
return
nil
,
errors
.
New
(
"sla_percent_min must be between 0 and 100"
)
}
}
if
cfg
.
LatencyP99MsMax
!=
nil
&&
*
cfg
.
LatencyP99MsMax
<
0
{
return
nil
,
errors
.
New
(
"latency_p99_ms_max must be >= 0"
)
}
if
cfg
.
TTFTp99MsMax
!=
nil
&&
*
cfg
.
TTFTp99MsMax
<
0
{
if
cfg
.
TTFTp99MsMax
!=
nil
&&
*
cfg
.
TTFTp99MsMax
<
0
{
return
nil
,
errors
.
New
(
"ttft_p99_ms_max must be >= 0"
)
return
nil
,
errors
.
New
(
"ttft_p99_ms_max must be >= 0"
)
}
}
...
...
backend/internal/service/ops_settings_models.go
View file @
90bce60b
...
@@ -63,7 +63,6 @@ type OpsAlertSilencingSettings struct {
...
@@ -63,7 +63,6 @@ type OpsAlertSilencingSettings struct {
type
OpsMetricThresholds
struct
{
type
OpsMetricThresholds
struct
{
SLAPercentMin
*
float64
`json:"sla_percent_min,omitempty"`
// SLA低于此值变红
SLAPercentMin
*
float64
`json:"sla_percent_min,omitempty"`
// SLA低于此值变红
LatencyP99MsMax
*
float64
`json:"latency_p99_ms_max,omitempty"`
// 延迟P99高于此值变红
TTFTp99MsMax
*
float64
`json:"ttft_p99_ms_max,omitempty"`
// TTFT P99高于此值变红
TTFTp99MsMax
*
float64
`json:"ttft_p99_ms_max,omitempty"`
// TTFT P99高于此值变红
RequestErrorRatePercentMax
*
float64
`json:"request_error_rate_percent_max,omitempty"`
// 请求错误率高于此值变红
RequestErrorRatePercentMax
*
float64
`json:"request_error_rate_percent_max,omitempty"`
// 请求错误率高于此值变红
UpstreamErrorRatePercentMax
*
float64
`json:"upstream_error_rate_percent_max,omitempty"`
// 上游错误率高于此值变红
UpstreamErrorRatePercentMax
*
float64
`json:"upstream_error_rate_percent_max,omitempty"`
// 上游错误率高于此值变红
...
@@ -79,11 +78,13 @@ type OpsAlertRuntimeSettings struct {
...
@@ -79,11 +78,13 @@ type OpsAlertRuntimeSettings struct {
// OpsAdvancedSettings stores advanced ops configuration (data retention, aggregation).
// OpsAdvancedSettings stores advanced ops configuration (data retention, aggregation).
type
OpsAdvancedSettings
struct
{
type
OpsAdvancedSettings
struct
{
DataRetention
OpsDataRetentionSettings
`json:"data_retention"`
DataRetention
OpsDataRetentionSettings
`json:"data_retention"`
Aggregation
OpsAggregationSettings
`json:"aggregation"`
Aggregation
OpsAggregationSettings
`json:"aggregation"`
IgnoreCountTokensErrors
bool
`json:"ignore_count_tokens_errors"`
IgnoreCountTokensErrors
bool
`json:"ignore_count_tokens_errors"`
AutoRefreshEnabled
bool
`json:"auto_refresh_enabled"`
IgnoreContextCanceled
bool
`json:"ignore_context_canceled"`
AutoRefreshIntervalSec
int
`json:"auto_refresh_interval_seconds"`
IgnoreNoAvailableAccounts
bool
`json:"ignore_no_available_accounts"`
AutoRefreshEnabled
bool
`json:"auto_refresh_enabled"`
AutoRefreshIntervalSec
int
`json:"auto_refresh_interval_seconds"`
}
}
type
OpsDataRetentionSettings
struct
{
type
OpsDataRetentionSettings
struct
{
...
...
backend/internal/service/ops_upstream_context.go
View file @
90bce60b
...
@@ -15,6 +15,11 @@ const (
...
@@ -15,6 +15,11 @@ const (
OpsUpstreamErrorMessageKey
=
"ops_upstream_error_message"
OpsUpstreamErrorMessageKey
=
"ops_upstream_error_message"
OpsUpstreamErrorDetailKey
=
"ops_upstream_error_detail"
OpsUpstreamErrorDetailKey
=
"ops_upstream_error_detail"
OpsUpstreamErrorsKey
=
"ops_upstream_errors"
OpsUpstreamErrorsKey
=
"ops_upstream_errors"
// Best-effort capture of the current upstream request body so ops can
// retry the specific upstream attempt (not just the client request).
// This value is sanitized+trimmed before being persisted.
OpsUpstreamRequestBodyKey
=
"ops_upstream_request_body"
)
)
func
setOpsUpstreamError
(
c
*
gin
.
Context
,
upstreamStatusCode
int
,
upstreamMessage
,
upstreamDetail
string
)
{
func
setOpsUpstreamError
(
c
*
gin
.
Context
,
upstreamStatusCode
int
,
upstreamMessage
,
upstreamDetail
string
)
{
...
@@ -38,13 +43,21 @@ type OpsUpstreamErrorEvent struct {
...
@@ -38,13 +43,21 @@ type OpsUpstreamErrorEvent struct {
AtUnixMs
int64
`json:"at_unix_ms,omitempty"`
AtUnixMs
int64
`json:"at_unix_ms,omitempty"`
// Context
// Context
Platform
string
`json:"platform,omitempty"`
Platform
string
`json:"platform,omitempty"`
AccountID
int64
`json:"account_id,omitempty"`
AccountID
int64
`json:"account_id,omitempty"`
AccountName
string
`json:"account_name,omitempty"`
// Outcome
// Outcome
UpstreamStatusCode
int
`json:"upstream_status_code,omitempty"`
UpstreamStatusCode
int
`json:"upstream_status_code,omitempty"`
UpstreamRequestID
string
`json:"upstream_request_id,omitempty"`
UpstreamRequestID
string
`json:"upstream_request_id,omitempty"`
// Best-effort upstream request capture (sanitized+trimmed).
// Required for retrying a specific upstream attempt.
UpstreamRequestBody
string
`json:"upstream_request_body,omitempty"`
// Best-effort upstream response capture (sanitized+trimmed).
UpstreamResponseBody
string
`json:"upstream_response_body,omitempty"`
// Kind: http_error | request_error | retry_exhausted | failover
// Kind: http_error | request_error | retry_exhausted | failover
Kind
string
`json:"kind,omitempty"`
Kind
string
`json:"kind,omitempty"`
...
@@ -61,6 +74,8 @@ func appendOpsUpstreamError(c *gin.Context, ev OpsUpstreamErrorEvent) {
...
@@ -61,6 +74,8 @@ func appendOpsUpstreamError(c *gin.Context, ev OpsUpstreamErrorEvent) {
}
}
ev
.
Platform
=
strings
.
TrimSpace
(
ev
.
Platform
)
ev
.
Platform
=
strings
.
TrimSpace
(
ev
.
Platform
)
ev
.
UpstreamRequestID
=
strings
.
TrimSpace
(
ev
.
UpstreamRequestID
)
ev
.
UpstreamRequestID
=
strings
.
TrimSpace
(
ev
.
UpstreamRequestID
)
ev
.
UpstreamRequestBody
=
strings
.
TrimSpace
(
ev
.
UpstreamRequestBody
)
ev
.
UpstreamResponseBody
=
strings
.
TrimSpace
(
ev
.
UpstreamResponseBody
)
ev
.
Kind
=
strings
.
TrimSpace
(
ev
.
Kind
)
ev
.
Kind
=
strings
.
TrimSpace
(
ev
.
Kind
)
ev
.
Message
=
strings
.
TrimSpace
(
ev
.
Message
)
ev
.
Message
=
strings
.
TrimSpace
(
ev
.
Message
)
ev
.
Detail
=
strings
.
TrimSpace
(
ev
.
Detail
)
ev
.
Detail
=
strings
.
TrimSpace
(
ev
.
Detail
)
...
@@ -68,6 +83,16 @@ func appendOpsUpstreamError(c *gin.Context, ev OpsUpstreamErrorEvent) {
...
@@ -68,6 +83,16 @@ func appendOpsUpstreamError(c *gin.Context, ev OpsUpstreamErrorEvent) {
ev
.
Message
=
sanitizeUpstreamErrorMessage
(
ev
.
Message
)
ev
.
Message
=
sanitizeUpstreamErrorMessage
(
ev
.
Message
)
}
}
// If the caller didn't explicitly pass upstream request body but the gateway
// stored it on the context, attach it so ops can retry this specific attempt.
if
ev
.
UpstreamRequestBody
==
""
{
if
v
,
ok
:=
c
.
Get
(
OpsUpstreamRequestBodyKey
);
ok
{
if
s
,
ok
:=
v
.
(
string
);
ok
{
ev
.
UpstreamRequestBody
=
strings
.
TrimSpace
(
s
)
}
}
}
var
existing
[]
*
OpsUpstreamErrorEvent
var
existing
[]
*
OpsUpstreamErrorEvent
if
v
,
ok
:=
c
.
Get
(
OpsUpstreamErrorsKey
);
ok
{
if
v
,
ok
:=
c
.
Get
(
OpsUpstreamErrorsKey
);
ok
{
if
arr
,
ok
:=
v
.
([]
*
OpsUpstreamErrorEvent
);
ok
{
if
arr
,
ok
:=
v
.
([]
*
OpsUpstreamErrorEvent
);
ok
{
...
@@ -92,3 +117,15 @@ func marshalOpsUpstreamErrors(events []*OpsUpstreamErrorEvent) *string {
...
@@ -92,3 +117,15 @@ func marshalOpsUpstreamErrors(events []*OpsUpstreamErrorEvent) *string {
s
:=
string
(
raw
)
s
:=
string
(
raw
)
return
&
s
return
&
s
}
}
func
ParseOpsUpstreamErrors
(
raw
string
)
([]
*
OpsUpstreamErrorEvent
,
error
)
{
raw
=
strings
.
TrimSpace
(
raw
)
if
raw
==
""
{
return
[]
*
OpsUpstreamErrorEvent
{},
nil
}
var
out
[]
*
OpsUpstreamErrorEvent
if
err
:=
json
.
Unmarshal
([]
byte
(
raw
),
&
out
);
err
!=
nil
{
return
nil
,
err
}
return
out
,
nil
}
backend/internal/service/proxy.go
View file @
90bce60b
...
@@ -31,5 +31,16 @@ func (p *Proxy) URL() string {
...
@@ -31,5 +31,16 @@ func (p *Proxy) URL() string {
type
ProxyWithAccountCount
struct
{
type
ProxyWithAccountCount
struct
{
Proxy
Proxy
AccountCount
int64
AccountCount
int64
LatencyMs
*
int64
LatencyStatus
string
LatencyMessage
string
}
type
ProxyAccountSummary
struct
{
ID
int64
Name
string
Platform
string
Type
string
Notes
*
string
}
}
backend/internal/service/proxy_latency_cache.go
0 → 100644
View file @
90bce60b
package
service
import
(
"context"
"time"
)
type
ProxyLatencyInfo
struct
{
Success
bool
`json:"success"`
LatencyMs
*
int64
`json:"latency_ms,omitempty"`
Message
string
`json:"message,omitempty"`
UpdatedAt
time
.
Time
`json:"updated_at"`
}
type
ProxyLatencyCache
interface
{
GetProxyLatencies
(
ctx
context
.
Context
,
proxyIDs
[]
int64
)
(
map
[
int64
]
*
ProxyLatencyInfo
,
error
)
SetProxyLatency
(
ctx
context
.
Context
,
proxyID
int64
,
info
*
ProxyLatencyInfo
)
error
}
backend/internal/service/proxy_service.go
View file @
90bce60b
...
@@ -10,6 +10,7 @@ import (
...
@@ -10,6 +10,7 @@ import (
var
(
var
(
ErrProxyNotFound
=
infraerrors
.
NotFound
(
"PROXY_NOT_FOUND"
,
"proxy not found"
)
ErrProxyNotFound
=
infraerrors
.
NotFound
(
"PROXY_NOT_FOUND"
,
"proxy not found"
)
ErrProxyInUse
=
infraerrors
.
Conflict
(
"PROXY_IN_USE"
,
"proxy is in use by accounts"
)
)
)
type
ProxyRepository
interface
{
type
ProxyRepository
interface
{
...
@@ -26,6 +27,7 @@ type ProxyRepository interface {
...
@@ -26,6 +27,7 @@ type ProxyRepository interface {
ExistsByHostPortAuth
(
ctx
context
.
Context
,
host
string
,
port
int
,
username
,
password
string
)
(
bool
,
error
)
ExistsByHostPortAuth
(
ctx
context
.
Context
,
host
string
,
port
int
,
username
,
password
string
)
(
bool
,
error
)
CountAccountsByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
(
int64
,
error
)
CountAccountsByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
(
int64
,
error
)
ListAccountSummariesByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
([]
ProxyAccountSummary
,
error
)
}
}
// CreateProxyRequest 创建代理请求
// CreateProxyRequest 创建代理请求
...
...
backend/internal/service/ratelimit_service.go
View file @
90bce60b
...
@@ -179,7 +179,7 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account,
...
@@ -179,7 +179,7 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account,
start
:=
geminiDailyWindowStart
(
now
)
start
:=
geminiDailyWindowStart
(
now
)
totals
,
ok
:=
s
.
getGeminiUsageTotals
(
account
.
ID
,
start
,
now
)
totals
,
ok
:=
s
.
getGeminiUsageTotals
(
account
.
ID
,
start
,
now
)
if
!
ok
{
if
!
ok
{
stats
,
err
:=
s
.
usageRepo
.
GetModelStatsWithFilters
(
ctx
,
start
,
now
,
0
,
0
,
account
.
ID
)
stats
,
err
:=
s
.
usageRepo
.
GetModelStatsWithFilters
(
ctx
,
start
,
now
,
0
,
0
,
account
.
ID
,
0
,
nil
)
if
err
!=
nil
{
if
err
!=
nil
{
return
true
,
err
return
true
,
err
}
}
...
@@ -226,7 +226,7 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account,
...
@@ -226,7 +226,7 @@ func (s *RateLimitService) PreCheckUsage(ctx context.Context, account *Account,
if
limit
>
0
{
if
limit
>
0
{
start
:=
now
.
Truncate
(
time
.
Minute
)
start
:=
now
.
Truncate
(
time
.
Minute
)
stats
,
err
:=
s
.
usageRepo
.
GetModelStatsWithFilters
(
ctx
,
start
,
now
,
0
,
0
,
account
.
ID
)
stats
,
err
:=
s
.
usageRepo
.
GetModelStatsWithFilters
(
ctx
,
start
,
now
,
0
,
0
,
account
.
ID
,
0
,
nil
)
if
err
!=
nil
{
if
err
!=
nil
{
return
true
,
err
return
true
,
err
}
}
...
...
backend/internal/service/usage_log.go
View file @
90bce60b
...
@@ -33,6 +33,8 @@ type UsageLog struct {
...
@@ -33,6 +33,8 @@ type UsageLog struct {
TotalCost
float64
TotalCost
float64
ActualCost
float64
ActualCost
float64
RateMultiplier
float64
RateMultiplier
float64
// AccountRateMultiplier 账号计费倍率快照(nil 表示历史数据,按 1.0 处理)
AccountRateMultiplier
*
float64
BillingType
int8
BillingType
int8
Stream
bool
Stream
bool
...
...
backend/migrations/037_add_account_rate_multiplier.sql
0 → 100644
View file @
90bce60b
-- Add account billing rate multiplier and per-usage snapshot.
--
-- accounts.rate_multiplier: 账号计费倍率(>=0,允许 0 表示该账号计费为 0)。
-- usage_logs.account_rate_multiplier: 每条 usage log 的账号倍率快照,用于实现
-- “倍率调整仅影响之后请求”,并支持同一天分段倍率加权统计。
--
-- 注意:usage_logs.account_rate_multiplier 不做回填、不设置 NOT NULL。
-- 老数据为 NULL 时,统计口径按 1.0 处理(COALESCE)。
ALTER
TABLE
IF
EXISTS
accounts
ADD
COLUMN
IF
NOT
EXISTS
rate_multiplier
DECIMAL
(
10
,
4
)
NOT
NULL
DEFAULT
1
.
0
;
ALTER
TABLE
IF
EXISTS
usage_logs
ADD
COLUMN
IF
NOT
EXISTS
account_rate_multiplier
DECIMAL
(
10
,
4
);
backend/migrations/037_ops_alert_silences.sql
0 → 100644
View file @
90bce60b
-- +goose Up
-- +goose StatementBegin
-- Ops alert silences: scoped (rule_id + platform + group_id + region)
CREATE
TABLE
IF
NOT
EXISTS
ops_alert_silences
(
id
BIGSERIAL
PRIMARY
KEY
,
rule_id
BIGINT
NOT
NULL
,
platform
VARCHAR
(
64
)
NOT
NULL
,
group_id
BIGINT
,
region
VARCHAR
(
64
),
until
TIMESTAMPTZ
NOT
NULL
,
reason
TEXT
,
created_by
BIGINT
,
created_at
TIMESTAMPTZ
NOT
NULL
DEFAULT
NOW
()
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_alert_silences_lookup
ON
ops_alert_silences
(
rule_id
,
platform
,
group_id
,
region
,
until
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP
TABLE
IF
EXISTS
ops_alert_silences
;
-- +goose StatementEnd
backend/migrations/038_ops_errors_resolution_retry_results_and_standardize_classification.sql
0 → 100644
View file @
90bce60b
-- Add resolution tracking to ops_error_logs, persist retry results, and standardize error classification enums.
--
-- This migration is intentionally idempotent.
SET
LOCAL
lock_timeout
=
'5s'
;
SET
LOCAL
statement_timeout
=
'10min'
;
-- ============================================
-- 1) ops_error_logs: resolution fields
-- ============================================
ALTER
TABLE
ops_error_logs
ADD
COLUMN
IF
NOT
EXISTS
resolved
BOOLEAN
NOT
NULL
DEFAULT
false
;
ALTER
TABLE
ops_error_logs
ADD
COLUMN
IF
NOT
EXISTS
resolved_at
TIMESTAMPTZ
;
ALTER
TABLE
ops_error_logs
ADD
COLUMN
IF
NOT
EXISTS
resolved_by_user_id
BIGINT
;
ALTER
TABLE
ops_error_logs
ADD
COLUMN
IF
NOT
EXISTS
resolved_retry_id
BIGINT
;
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_error_logs_resolved_time
ON
ops_error_logs
(
resolved
,
created_at
DESC
);
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_error_logs_unresolved_time
ON
ops_error_logs
(
created_at
DESC
)
WHERE
resolved
=
false
;
-- ============================================
-- 2) ops_retry_attempts: persist execution results
-- ============================================
ALTER
TABLE
ops_retry_attempts
ADD
COLUMN
IF
NOT
EXISTS
success
BOOLEAN
;
ALTER
TABLE
ops_retry_attempts
ADD
COLUMN
IF
NOT
EXISTS
http_status_code
INT
;
ALTER
TABLE
ops_retry_attempts
ADD
COLUMN
IF
NOT
EXISTS
upstream_request_id
VARCHAR
(
128
);
ALTER
TABLE
ops_retry_attempts
ADD
COLUMN
IF
NOT
EXISTS
used_account_id
BIGINT
;
ALTER
TABLE
ops_retry_attempts
ADD
COLUMN
IF
NOT
EXISTS
response_preview
TEXT
;
ALTER
TABLE
ops_retry_attempts
ADD
COLUMN
IF
NOT
EXISTS
response_truncated
BOOLEAN
NOT
NULL
DEFAULT
false
;
CREATE
INDEX
IF
NOT
EXISTS
idx_ops_retry_attempts_success_time
ON
ops_retry_attempts
(
success
,
created_at
DESC
);
-- Backfill best-effort fields for existing rows.
UPDATE
ops_retry_attempts
SET
success
=
(
LOWER
(
COALESCE
(
status
,
''
))
=
'succeeded'
)
WHERE
success
IS
NULL
;
UPDATE
ops_retry_attempts
SET
upstream_request_id
=
result_request_id
WHERE
upstream_request_id
IS
NULL
AND
result_request_id
IS
NOT
NULL
;
-- ============================================
-- 3) Standardize classification enums in ops_error_logs
--
-- New enums:
-- error_phase: request|auth|routing|upstream|network|internal
-- error_owner: client|provider|platform
-- error_source: client_request|upstream_http|gateway
-- ============================================
-- Owner: legacy sub2api => platform.
UPDATE
ops_error_logs
SET
error_owner
=
'platform'
WHERE
LOWER
(
COALESCE
(
error_owner
,
''
))
=
'sub2api'
;
-- Owner: normalize empty/null to platform (best-effort).
UPDATE
ops_error_logs
SET
error_owner
=
'platform'
WHERE
COALESCE
(
TRIM
(
error_owner
),
''
)
=
''
;
-- Phase: map legacy phases.
UPDATE
ops_error_logs
SET
error_phase
=
CASE
WHEN
COALESCE
(
TRIM
(
error_phase
),
''
)
=
''
THEN
'internal'
WHEN
LOWER
(
error_phase
)
IN
(
'billing'
,
'concurrency'
,
'response'
)
THEN
'request'
WHEN
LOWER
(
error_phase
)
IN
(
'scheduling'
)
THEN
'routing'
WHEN
LOWER
(
error_phase
)
IN
(
'request'
,
'auth'
,
'routing'
,
'upstream'
,
'network'
,
'internal'
)
THEN
LOWER
(
error_phase
)
ELSE
'internal'
END
;
-- Source: map legacy sources.
UPDATE
ops_error_logs
SET
error_source
=
CASE
WHEN
COALESCE
(
TRIM
(
error_source
),
''
)
=
''
THEN
'gateway'
WHEN
LOWER
(
error_source
)
IN
(
'billing'
,
'concurrency'
)
THEN
'client_request'
WHEN
LOWER
(
error_source
)
IN
(
'upstream_http'
)
THEN
'upstream_http'
WHEN
LOWER
(
error_source
)
IN
(
'upstream_network'
)
THEN
'gateway'
WHEN
LOWER
(
error_source
)
IN
(
'internal'
)
THEN
'gateway'
WHEN
LOWER
(
error_source
)
IN
(
'client_request'
,
'upstream_http'
,
'gateway'
)
THEN
LOWER
(
error_source
)
ELSE
'gateway'
END
;
-- Auto-resolve recovered upstream errors (client status < 400).
UPDATE
ops_error_logs
SET
resolved
=
true
,
resolved_at
=
COALESCE
(
resolved_at
,
created_at
)
WHERE
resolved
=
false
AND
COALESCE
(
status_code
,
0
)
>
0
AND
COALESCE
(
status_code
,
0
)
<
400
;
frontend/src/api/admin/dashboard.ts
View file @
90bce60b
...
@@ -46,6 +46,10 @@ export interface TrendParams {
...
@@ -46,6 +46,10 @@ export interface TrendParams {
granularity
?:
'
day
'
|
'
hour
'
granularity
?:
'
day
'
|
'
hour
'
user_id
?:
number
user_id
?:
number
api_key_id
?:
number
api_key_id
?:
number
model
?:
string
account_id
?:
number
group_id
?:
number
stream
?:
boolean
}
}
export
interface
TrendResponse
{
export
interface
TrendResponse
{
...
@@ -70,6 +74,10 @@ export interface ModelStatsParams {
...
@@ -70,6 +74,10 @@ export interface ModelStatsParams {
end_date
?:
string
end_date
?:
string
user_id
?:
number
user_id
?:
number
api_key_id
?:
number
api_key_id
?:
number
model
?:
string
account_id
?:
number
group_id
?:
number
stream
?:
boolean
}
}
export
interface
ModelStatsResponse
{
export
interface
ModelStatsResponse
{
...
...
frontend/src/api/admin/ops.ts
View file @
90bce60b
...
@@ -17,6 +17,47 @@ export interface OpsRequestOptions {
...
@@ -17,6 +17,47 @@ export interface OpsRequestOptions {
export
interface
OpsRetryRequest
{
export
interface
OpsRetryRequest
{
mode
:
OpsRetryMode
mode
:
OpsRetryMode
pinned_account_id
?:
number
pinned_account_id
?:
number
force
?:
boolean
}
export
interface
OpsRetryAttempt
{
id
:
number
created_at
:
string
requested_by_user_id
:
number
source_error_id
:
number
mode
:
string
pinned_account_id
?:
number
|
null
pinned_account_name
?:
string
status
:
string
started_at
?:
string
|
null
finished_at
?:
string
|
null
duration_ms
?:
number
|
null
success
?:
boolean
|
null
http_status_code
?:
number
|
null
upstream_request_id
?:
string
|
null
used_account_id
?:
number
|
null
used_account_name
?:
string
response_preview
?:
string
|
null
response_truncated
?:
boolean
|
null
result_request_id
?:
string
|
null
result_error_id
?:
number
|
null
error_message
?:
string
|
null
}
export
type
OpsUpstreamErrorEvent
=
{
at_unix_ms
?:
number
platform
?:
string
account_id
?:
number
account_name
?:
string
upstream_status_code
?:
number
upstream_request_id
?:
string
upstream_request_body
?:
string
kind
?:
string
message
?:
string
detail
?:
string
}
}
export
interface
OpsRetryResult
{
export
interface
OpsRetryResult
{
...
@@ -626,8 +667,6 @@ export type MetricType =
...
@@ -626,8 +667,6 @@ export type MetricType =
|
'
success_rate
'
|
'
success_rate
'
|
'
error_rate
'
|
'
error_rate
'
|
'
upstream_error_rate
'
|
'
upstream_error_rate
'
|
'
p95_latency_ms
'
|
'
p99_latency_ms
'
|
'
cpu_usage_percent
'
|
'
cpu_usage_percent
'
|
'
memory_usage_percent
'
|
'
memory_usage_percent
'
|
'
concurrency_queue_depth
'
|
'
concurrency_queue_depth
'
...
@@ -663,7 +702,7 @@ export interface AlertEvent {
...
@@ -663,7 +702,7 @@ export interface AlertEvent {
id
:
number
id
:
number
rule_id
:
number
rule_id
:
number
severity
:
OpsSeverity
|
string
severity
:
OpsSeverity
|
string
status
:
'
firing
'
|
'
resolved
'
|
string
status
:
'
firing
'
|
'
resolved
'
|
'
manual_resolved
'
|
string
title
?:
string
title
?:
string
description
?:
string
description
?:
string
metric_value
?:
number
metric_value
?:
number
...
@@ -701,10 +740,9 @@ export interface EmailNotificationConfig {
...
@@ -701,10 +740,9 @@ export interface EmailNotificationConfig {
}
}
export
interface
OpsMetricThresholds
{
export
interface
OpsMetricThresholds
{
sla_percent_min
?:
number
|
null
// SLA低于此值变红
sla_percent_min
?:
number
|
null
// SLA低于此值变红
latency_p99_ms_max
?:
number
|
null
// 延迟P99高于此值变红
ttft_p99_ms_max
?:
number
|
null
// TTFT P99高于此值变红
ttft_p99_ms_max
?:
number
|
null
// TTFT P99高于此值变红
request_error_rate_percent_max
?:
number
|
null
// 请求错误率高于此值变红
request_error_rate_percent_max
?:
number
|
null
// 请求错误率高于此值变红
upstream_error_rate_percent_max
?:
number
|
null
// 上游错误率高于此值变红
upstream_error_rate_percent_max
?:
number
|
null
// 上游错误率高于此值变红
}
}
...
@@ -735,6 +773,8 @@ export interface OpsAdvancedSettings {
...
@@ -735,6 +773,8 @@ export interface OpsAdvancedSettings {
data_retention
:
OpsDataRetentionSettings
data_retention
:
OpsDataRetentionSettings
aggregation
:
OpsAggregationSettings
aggregation
:
OpsAggregationSettings
ignore_count_tokens_errors
:
boolean
ignore_count_tokens_errors
:
boolean
ignore_context_canceled
:
boolean
ignore_no_available_accounts
:
boolean
auto_refresh_enabled
:
boolean
auto_refresh_enabled
:
boolean
auto_refresh_interval_seconds
:
number
auto_refresh_interval_seconds
:
number
}
}
...
@@ -754,21 +794,37 @@ export interface OpsAggregationSettings {
...
@@ -754,21 +794,37 @@ export interface OpsAggregationSettings {
export
interface
OpsErrorLog
{
export
interface
OpsErrorLog
{
id
:
number
id
:
number
created_at
:
string
created_at
:
string
// Standardized classification
phase
:
OpsPhase
phase
:
OpsPhase
type
:
string
type
:
string
error_owner
:
'
client
'
|
'
provider
'
|
'
platform
'
|
string
error_source
:
'
client_request
'
|
'
upstream_http
'
|
'
gateway
'
|
string
severity
:
OpsSeverity
severity
:
OpsSeverity
status_code
:
number
status_code
:
number
platform
:
string
platform
:
string
model
:
string
model
:
string
latency_ms
?:
number
|
null
is_retryable
:
boolean
retry_count
:
number
resolved
:
boolean
resolved_at
?:
string
|
null
resolved_by_user_id
?:
number
|
null
resolved_retry_id
?:
number
|
null
client_request_id
:
string
client_request_id
:
string
request_id
:
string
request_id
:
string
message
:
string
message
:
string
user_id
?:
number
|
null
user_id
?:
number
|
null
user_email
:
string
api_key_id
?:
number
|
null
api_key_id
?:
number
|
null
account_id
?:
number
|
null
account_id
?:
number
|
null
account_name
:
string
group_id
?:
number
|
null
group_id
?:
number
|
null
group_name
:
string
client_ip
?:
string
|
null
client_ip
?:
string
|
null
request_path
?:
string
request_path
?:
string
...
@@ -890,7 +946,9 @@ export async function getErrorDistribution(
...
@@ -890,7 +946,9 @@ export async function getErrorDistribution(
return
data
return
data
}
}
export
async
function
listErrorLogs
(
params
:
{
export
type
OpsErrorListView
=
'
errors
'
|
'
excluded
'
|
'
all
'
export
type
OpsErrorListQueryParams
=
{
page
?:
number
page
?:
number
page_size
?:
number
page_size
?:
number
time_range
?:
string
time_range
?:
string
...
@@ -899,10 +957,20 @@ export async function listErrorLogs(params: {
...
@@ -899,10 +957,20 @@ export async function listErrorLogs(params: {
platform
?:
string
platform
?:
string
group_id
?:
number
|
null
group_id
?:
number
|
null
account_id
?:
number
|
null
account_id
?:
number
|
null
phase
?:
string
phase
?:
string
error_owner
?:
string
error_source
?:
string
resolved
?:
string
view
?:
OpsErrorListView
q
?:
string
q
?:
string
status_codes
?:
string
status_codes
?:
string
}):
Promise
<
OpsErrorLogsResponse
>
{
status_codes_other
?:
string
}
// Legacy unified endpoints
export
async
function
listErrorLogs
(
params
:
OpsErrorListQueryParams
):
Promise
<
OpsErrorLogsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorLogsResponse
>
(
'
/admin/ops/errors
'
,
{
params
})
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorLogsResponse
>
(
'
/admin/ops/errors
'
,
{
params
})
return
data
return
data
}
}
...
@@ -917,6 +985,70 @@ export async function retryErrorRequest(id: number, req: OpsRetryRequest): Promi
...
@@ -917,6 +985,70 @@ export async function retryErrorRequest(id: number, req: OpsRetryRequest): Promi
return
data
return
data
}
}
export
async
function
listRetryAttempts
(
errorId
:
number
,
limit
=
50
):
Promise
<
OpsRetryAttempt
[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsRetryAttempt
[]
>
(
`/admin/ops/errors/
${
errorId
}
/retries`
,
{
params
:
{
limit
}
})
return
data
}
export
async
function
updateErrorResolved
(
errorId
:
number
,
resolved
:
boolean
):
Promise
<
void
>
{
await
apiClient
.
put
(
`/admin/ops/errors/
${
errorId
}
/resolve`
,
{
resolved
})
}
// New split endpoints
export
async
function
listRequestErrors
(
params
:
OpsErrorListQueryParams
):
Promise
<
OpsErrorLogsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorLogsResponse
>
(
'
/admin/ops/request-errors
'
,
{
params
})
return
data
}
export
async
function
listUpstreamErrors
(
params
:
OpsErrorListQueryParams
):
Promise
<
OpsErrorLogsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorLogsResponse
>
(
'
/admin/ops/upstream-errors
'
,
{
params
})
return
data
}
export
async
function
getRequestErrorDetail
(
id
:
number
):
Promise
<
OpsErrorDetail
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorDetail
>
(
`/admin/ops/request-errors/
${
id
}
`
)
return
data
}
export
async
function
getUpstreamErrorDetail
(
id
:
number
):
Promise
<
OpsErrorDetail
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorDetail
>
(
`/admin/ops/upstream-errors/
${
id
}
`
)
return
data
}
export
async
function
retryRequestErrorClient
(
id
:
number
):
Promise
<
OpsRetryResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
OpsRetryResult
>
(
`/admin/ops/request-errors/
${
id
}
/retry-client`
,
{})
return
data
}
export
async
function
retryRequestErrorUpstreamEvent
(
id
:
number
,
idx
:
number
):
Promise
<
OpsRetryResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
OpsRetryResult
>
(
`/admin/ops/request-errors/
${
id
}
/upstream-errors/
${
idx
}
/retry`
,
{})
return
data
}
export
async
function
retryUpstreamError
(
id
:
number
):
Promise
<
OpsRetryResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
OpsRetryResult
>
(
`/admin/ops/upstream-errors/
${
id
}
/retry`
,
{})
return
data
}
export
async
function
updateRequestErrorResolved
(
errorId
:
number
,
resolved
:
boolean
):
Promise
<
void
>
{
await
apiClient
.
put
(
`/admin/ops/request-errors/
${
errorId
}
/resolve`
,
{
resolved
})
}
export
async
function
updateUpstreamErrorResolved
(
errorId
:
number
,
resolved
:
boolean
):
Promise
<
void
>
{
await
apiClient
.
put
(
`/admin/ops/upstream-errors/
${
errorId
}
/resolve`
,
{
resolved
})
}
export
async
function
listRequestErrorUpstreamErrors
(
id
:
number
,
params
:
OpsErrorListQueryParams
=
{},
options
:
{
include_detail
?:
boolean
}
=
{}
):
Promise
<
PaginatedResponse
<
OpsErrorDetail
>>
{
const
query
:
Record
<
string
,
any
>
=
{
...
params
}
if
(
options
.
include_detail
)
query
.
include_detail
=
'
1
'
const
{
data
}
=
await
apiClient
.
get
<
PaginatedResponse
<
OpsErrorDetail
>>
(
`/admin/ops/request-errors/
${
id
}
/upstream-errors`
,
{
params
:
query
})
return
data
}
export
async
function
listRequestDetails
(
params
:
OpsRequestDetailsParams
):
Promise
<
OpsRequestDetailsResponse
>
{
export
async
function
listRequestDetails
(
params
:
OpsRequestDetailsParams
):
Promise
<
OpsRequestDetailsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsRequestDetailsResponse
>
(
'
/admin/ops/requests
'
,
{
params
})
const
{
data
}
=
await
apiClient
.
get
<
OpsRequestDetailsResponse
>
(
'
/admin/ops/requests
'
,
{
params
})
return
data
return
data
...
@@ -942,11 +1074,45 @@ export async function deleteAlertRule(id: number): Promise<void> {
...
@@ -942,11 +1074,45 @@ export async function deleteAlertRule(id: number): Promise<void> {
await
apiClient
.
delete
(
`/admin/ops/alert-rules/
${
id
}
`
)
await
apiClient
.
delete
(
`/admin/ops/alert-rules/
${
id
}
`
)
}
}
export
async
function
listAlertEvents
(
limit
=
100
):
Promise
<
AlertEvent
[]
>
{
export
interface
AlertEventsQuery
{
const
{
data
}
=
await
apiClient
.
get
<
AlertEvent
[]
>
(
'
/admin/ops/alert-events
'
,
{
params
:
{
limit
}
})
limit
?:
number
status
?:
string
severity
?:
string
email_sent
?:
boolean
time_range
?:
string
start_time
?:
string
end_time
?:
string
before_fired_at
?:
string
before_id
?:
number
platform
?:
string
group_id
?:
number
}
export
async
function
listAlertEvents
(
params
:
AlertEventsQuery
=
{}):
Promise
<
AlertEvent
[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
AlertEvent
[]
>
(
'
/admin/ops/alert-events
'
,
{
params
})
return
data
}
export
async
function
getAlertEvent
(
id
:
number
):
Promise
<
AlertEvent
>
{
const
{
data
}
=
await
apiClient
.
get
<
AlertEvent
>
(
`/admin/ops/alert-events/
${
id
}
`
)
return
data
return
data
}
}
export
async
function
updateAlertEventStatus
(
id
:
number
,
status
:
'
resolved
'
|
'
manual_resolved
'
):
Promise
<
void
>
{
await
apiClient
.
put
(
`/admin/ops/alert-events/
${
id
}
/status`
,
{
status
})
}
export
async
function
createAlertSilence
(
payload
:
{
rule_id
:
number
platform
:
string
group_id
?:
number
|
null
region
?:
string
|
null
until
:
string
reason
?:
string
}):
Promise
<
void
>
{
await
apiClient
.
post
(
'
/admin/ops/alert-silences
'
,
payload
)
}
// Email notification config
// Email notification config
export
async
function
getEmailNotificationConfig
():
Promise
<
EmailNotificationConfig
>
{
export
async
function
getEmailNotificationConfig
():
Promise
<
EmailNotificationConfig
>
{
const
{
data
}
=
await
apiClient
.
get
<
EmailNotificationConfig
>
(
'
/admin/ops/email-notification/config
'
)
const
{
data
}
=
await
apiClient
.
get
<
EmailNotificationConfig
>
(
'
/admin/ops/email-notification/config
'
)
...
@@ -1001,15 +1167,35 @@ export const opsAPI = {
...
@@ -1001,15 +1167,35 @@ export const opsAPI = {
getAccountAvailabilityStats
,
getAccountAvailabilityStats
,
getRealtimeTrafficSummary
,
getRealtimeTrafficSummary
,
subscribeQPS
,
subscribeQPS
,
// Legacy unified endpoints
listErrorLogs
,
listErrorLogs
,
getErrorLogDetail
,
getErrorLogDetail
,
retryErrorRequest
,
retryErrorRequest
,
listRetryAttempts
,
updateErrorResolved
,
// New split endpoints
listRequestErrors
,
listUpstreamErrors
,
getRequestErrorDetail
,
getUpstreamErrorDetail
,
retryRequestErrorClient
,
retryRequestErrorUpstreamEvent
,
retryUpstreamError
,
updateRequestErrorResolved
,
updateUpstreamErrorResolved
,
listRequestErrorUpstreamErrors
,
listRequestDetails
,
listRequestDetails
,
listAlertRules
,
listAlertRules
,
createAlertRule
,
createAlertRule
,
updateAlertRule
,
updateAlertRule
,
deleteAlertRule
,
deleteAlertRule
,
listAlertEvents
,
listAlertEvents
,
getAlertEvent
,
updateAlertEventStatus
,
createAlertSilence
,
getEmailNotificationConfig
,
getEmailNotificationConfig
,
updateEmailNotificationConfig
,
updateEmailNotificationConfig
,
getAlertRuntimeSettings
,
getAlertRuntimeSettings
,
...
...
frontend/src/api/admin/proxies.ts
View file @
90bce60b
...
@@ -4,7 +4,13 @@
...
@@ -4,7 +4,13 @@
*/
*/
import
{
apiClient
}
from
'
../client
'
import
{
apiClient
}
from
'
../client
'
import
type
{
Proxy
,
CreateProxyRequest
,
UpdateProxyRequest
,
PaginatedResponse
}
from
'
@/types
'
import
type
{
Proxy
,
ProxyAccountSummary
,
CreateProxyRequest
,
UpdateProxyRequest
,
PaginatedResponse
}
from
'
@/types
'
/**
/**
* List all proxies with pagination
* List all proxies with pagination
...
@@ -160,8 +166,8 @@ export async function getStats(id: number): Promise<{
...
@@ -160,8 +166,8 @@ export async function getStats(id: number): Promise<{
* @param id - Proxy ID
* @param id - Proxy ID
* @returns List of accounts using the proxy
* @returns List of accounts using the proxy
*/
*/
export
async
function
getProxyAccounts
(
id
:
number
):
Promise
<
P
aginatedResponse
<
any
>
>
{
export
async
function
getProxyAccounts
(
id
:
number
):
Promise
<
P
roxyAccountSummary
[]
>
{
const
{
data
}
=
await
apiClient
.
get
<
P
aginatedResponse
<
any
>
>
(
`/admin/proxies/
${
id
}
/accounts`
)
const
{
data
}
=
await
apiClient
.
get
<
P
roxyAccountSummary
[]
>
(
`/admin/proxies/
${
id
}
/accounts`
)
return
data
return
data
}
}
...
@@ -189,6 +195,17 @@ export async function batchCreate(
...
@@ -189,6 +195,17 @@ export async function batchCreate(
return
data
return
data
}
}
export
async
function
batchDelete
(
ids
:
number
[]):
Promise
<
{
deleted_ids
:
number
[]
skipped
:
Array
<
{
id
:
number
;
reason
:
string
}
>
}
>
{
const
{
data
}
=
await
apiClient
.
post
<
{
deleted_ids
:
number
[]
skipped
:
Array
<
{
id
:
number
;
reason
:
string
}
>
}
>
(
'
/admin/proxies/batch-delete
'
,
{
ids
})
return
data
}
export
const
proxiesAPI
=
{
export
const
proxiesAPI
=
{
list
,
list
,
getAll
,
getAll
,
...
@@ -201,7 +218,8 @@ export const proxiesAPI = {
...
@@ -201,7 +218,8 @@ export const proxiesAPI = {
testProxy
,
testProxy
,
getStats
,
getStats
,
getProxyAccounts
,
getProxyAccounts
,
batchCreate
batchCreate
,
batchDelete
}
}
export
default
proxiesAPI
export
default
proxiesAPI
frontend/src/api/admin/usage.ts
View file @
90bce60b
...
@@ -16,6 +16,7 @@ export interface AdminUsageStatsResponse {
...
@@ -16,6 +16,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
average_duration_ms
:
number
average_duration_ms
:
number
}
}
...
...
frontend/src/components/account/AccountStatsModal.vue
View file @
90bce60b
...
@@ -73,11 +73,12 @@
...
@@ -73,11 +73,12 @@
</p>
</p>
<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.stats.accumulatedCost
'
)
}}
{{
t
(
'
admin.accounts.stats.accumulatedCost
'
)
}}
<span
class=
"text-gray-400 dark:text-gray-500"
<span
class=
"text-gray-400 dark:text-gray-500"
>
>
(
{{
t
(
'
admin.accounts.stats.standardCost
'
)
}}
: $
{{
(
{{
t
(
'
usage.userBilled
'
)
}}
: $
{{
formatCost
(
stats
.
summary
.
total_user_cost
)
}}
·
{{
t
(
'
admin.accounts.stats.standardCost
'
)
}}
: $
{{
formatCost
(
stats
.
summary
.
total_standard_cost
)
formatCost
(
stats
.
summary
.
total_standard_cost
)
}}
)
</span
}}
)
>
</span
>
</p>
</p>
</div>
</div>
...
@@ -121,12 +122,15 @@
...
@@ -121,12 +122,15 @@
<p
class=
"text-2xl font-bold text-gray-900 dark:text-white"
>
<p
class=
"text-2xl font-bold text-gray-900 dark:text-white"
>
$
{{
formatCost
(
stats
.
summary
.
avg_daily_cost
)
}}
$
{{
formatCost
(
stats
.
summary
.
avg_daily_cost
)
}}
</p>
</p>
<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.stats.basedOnActualDays
'
,
{
t
(
'
admin.accounts.stats.basedOnActualDays
'
,
{
days
:
stats
.
summary
.
actual_days_used
days
:
stats
.
summary
.
actual_days_used
}
)
}
)
}}
}}
<
span
class
=
"
text-gray-400 dark:text-gray-500
"
>
({{
t
(
'
usage.userBilled
'
)
}}
:
$
{{
formatCost
(
stats
.
summary
.
avg_daily_user_cost
)
}}
)
<
/span
>
<
/p
>
<
/p
>
<
/div
>
<
/div
>
...
@@ -189,13 +193,17 @@
...
@@ -189,13 +193,17 @@
<
/div
>
<
/div
>
<
div
class
=
"
space-y-2
"
>
<
div
class
=
"
space-y-2
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
usage.accountBilled
'
)
}}
<
/span
>
t
(
'
admin.accounts.stats.cost
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
>
$
{{
formatCost
(
stats
.
summary
.
today
?.
cost
||
0
)
}}
<
/spa
n
>
$
{{
formatCost
(
stats
.
summary
.
today
?.
cost
||
0
)
}}
<
/spa
n
>
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
usage.userBilled
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
>
$
{{
formatCost
(
stats
.
summary
.
today
?.
user_cost
||
0
)
}}
<
/spa
n
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.stats.requests
'
)
t
(
'
admin.accounts.stats.requests
'
)
...
@@ -240,13 +248,17 @@
...
@@ -240,13 +248,17 @@
}}
<
/span
>
}}
<
/span
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
usage.accountBilled
'
)
}}
<
/span
>
t
(
'
admin.accounts.stats.cost
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-semibold text-orange-600 dark:text-orange-400
"
<
span
class
=
"
text-sm font-semibold text-orange-600 dark:text-orange-400
"
>
$
{{
formatCost
(
stats
.
summary
.
highest_cost_day
?.
cost
||
0
)
}}
<
/spa
n
>
$
{{
formatCost
(
stats
.
summary
.
highest_cost_day
?.
cost
||
0
)
}}
<
/spa
n
>
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
usage.userBilled
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
>
$
{{
formatCost
(
stats
.
summary
.
highest_cost_day
?.
user_cost
||
0
)
}}
<
/spa
n
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.accounts.stats.requests
'
)
t
(
'
admin.accounts.stats.requests
'
)
...
@@ -291,13 +303,17 @@
...
@@ -291,13 +303,17 @@
}}
<
/span
>
}}
<
/span
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
usage.accountBilled
'
)
}}
<
/span
>
t
(
'
admin.accounts.stats.cost
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
>
$
{{
formatCost
(
stats
.
summary
.
highest_request_day
?.
cost
||
0
)
}}
<
/spa
n
>
$
{{
formatCost
(
stats
.
summary
.
highest_request_day
?.
cost
||
0
)
}}
<
/spa
n
>
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
usage.userBilled
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
>
$
{{
formatCost
(
stats
.
summary
.
highest_request_day
?.
user_cost
||
0
)
}}
<
/spa
n
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
...
@@ -397,13 +413,17 @@
...
@@ -397,13 +413,17 @@
}}
<
/span
>
}}
<
/span
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
usage.accountBilled
'
)
}}
<
/span
>
t
(
'
admin.accounts.stats.todayCost
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
>
$
{{
formatCost
(
stats
.
summary
.
today
?.
cost
||
0
)
}}
<
/spa
n
>
$
{{
formatCost
(
stats
.
summary
.
today
?.
cost
||
0
)
}}
<
/spa
n
>
>
<
/div
>
<
/div
>
<
div
class
=
"
flex items-center justify-between
"
>
<
span
class
=
"
text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
usage.userBilled
'
)
}}
<
/span
>
<
span
class
=
"
text-sm font-semibold text-gray-900 dark:text-white
"
>
$
{{
formatCost
(
stats
.
summary
.
today
?.
user_cost
||
0
)
}}
<
/spa
n
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
...
@@ -517,14 +537,24 @@ const trendChartData = computed(() => {
...
@@ -517,14 +537,24 @@ const trendChartData = computed(() => {
labels
:
stats
.
value
.
history
.
map
((
h
)
=>
h
.
label
),
labels
:
stats
.
value
.
history
.
map
((
h
)
=>
h
.
label
),
datasets
:
[
datasets
:
[
{
{
label
:
t
(
'
admin
.account
s.stats.cost
'
)
+
'
(USD)
'
,
label
:
t
(
'
usage
.account
Billed
'
)
+
'
(USD)
'
,
data
:
stats
.
value
.
history
.
map
((
h
)
=>
h
.
cost
),
data
:
stats
.
value
.
history
.
map
((
h
)
=>
h
.
actual_
cost
),
borderColor
:
'
#3b82f6
'
,
borderColor
:
'
#3b82f6
'
,
backgroundColor
:
'
rgba(59, 130, 246, 0.1)
'
,
backgroundColor
:
'
rgba(59, 130, 246, 0.1)
'
,
fill
:
true
,
fill
:
true
,
tension
:
0.3
,
tension
:
0.3
,
yAxisID
:
'
y
'
yAxisID
:
'
y
'
}
,
}
,
{
label
:
t
(
'
usage.userBilled
'
)
+
'
(USD)
'
,
data
:
stats
.
value
.
history
.
map
((
h
)
=>
h
.
user_cost
),
borderColor
:
'
#10b981
'
,
backgroundColor
:
'
rgba(16, 185, 129, 0.08)
'
,
fill
:
false
,
tension
:
0.3
,
borderDash
:
[
5
,
5
],
yAxisID
:
'
y
'
}
,
{
{
label
:
t
(
'
admin.accounts.stats.requests
'
),
label
:
t
(
'
admin.accounts.stats.requests
'
),
data
:
stats
.
value
.
history
.
map
((
h
)
=>
h
.
requests
),
data
:
stats
.
value
.
history
.
map
((
h
)
=>
h
.
requests
),
...
@@ -602,7 +632,7 @@ const lineChartOptions = computed(() => ({
...
@@ -602,7 +632,7 @@ const lineChartOptions = computed(() => ({
}
,
}
,
title
:
{
title
:
{
display
:
true
,
display
:
true
,
text
:
t
(
'
admin
.account
s.stats.cost
'
)
+
'
(USD)
'
,
text
:
t
(
'
usage
.account
Billed
'
)
+
'
(USD)
'
,
color
:
'
#3b82f6
'
,
color
:
'
#3b82f6
'
,
font
:
{
font
:
{
size
:
11
size
:
11
...
...
frontend/src/components/account/AccountTodayStatsCell.vue
View file @
90bce60b
...
@@ -32,15 +32,20 @@
...
@@ -32,15 +32,20 @@
formatTokens
(
stats
.
tokens
)
formatTokens
(
stats
.
tokens
)
}}
</span>
}}
</span>
</div>
</div>
<!-- Cost -->
<!-- Cost
(Account)
-->
<div
class=
"flex items-center gap-1"
>
<div
class=
"flex items-center gap-1"
>
<span
class=
"text-gray-500 dark:text-gray-400"
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
usage.accountBilled
'
)
}}
:
</span>
>
{{
t
(
'
admin.accounts.stats.cost
'
)
}}
:
</span
>
<span
class=
"font-medium text-emerald-600 dark:text-emerald-400"
>
{{
<span
class=
"font-medium text-emerald-600 dark:text-emerald-400"
>
{{
formatCurrency
(
stats
.
cost
)
formatCurrency
(
stats
.
cost
)
}}
</span>
}}
</span>
</div>
</div>
<!-- Cost (User/API Key) -->
<div
v-if=
"stats.user_cost != null"
class=
"flex items-center gap-1"
>
<span
class=
"text-gray-500 dark:text-gray-400"
>
{{
t
(
'
usage.userBilled
'
)
}}
:
</span>
<span
class=
"font-medium text-gray-700 dark:text-gray-300"
>
{{
formatCurrency
(
stats
.
user_cost
)
}}
</span>
</div>
</div>
</div>
<!-- No data -->
<!-- No data -->
...
...
frontend/src/components/account/BulkEditAccountModal.vue
View file @
90bce60b
...
@@ -459,7 +459,7 @@
...
@@ -459,7 +459,7 @@
<
/div
>
<
/div
>
<!--
Concurrency
&
Priority
-->
<!--
Concurrency
&
Priority
-->
<
div
class
=
"
grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600
lg:grid-cols-3
"
>
<
div
>
<
div
>
<
div
class
=
"
mb-3 flex items-center justify-between
"
>
<
div
class
=
"
mb-3 flex items-center justify-between
"
>
<
label
<
label
...
@@ -516,6 +516,36 @@
...
@@ -516,6 +516,36 @@
aria
-
labelledby
=
"
bulk-edit-priority-label
"
aria
-
labelledby
=
"
bulk-edit-priority-label
"
/>
/>
<
/div
>
<
/div
>
<
div
>
<
div
class
=
"
mb-3 flex items-center justify-between
"
>
<
label
id
=
"
bulk-edit-rate-multiplier-label
"
class
=
"
input-label mb-0
"
for
=
"
bulk-edit-rate-multiplier-enabled
"
>
{{
t
(
'
admin.accounts.billingRateMultiplier
'
)
}}
<
/label
>
<
input
v
-
model
=
"
enableRateMultiplier
"
id
=
"
bulk-edit-rate-multiplier-enabled
"
type
=
"
checkbox
"
aria
-
controls
=
"
bulk-edit-rate-multiplier
"
class
=
"
rounded border-gray-300 text-primary-600 focus:ring-primary-500
"
/>
<
/div
>
<
input
v
-
model
.
number
=
"
rateMultiplier
"
id
=
"
bulk-edit-rate-multiplier
"
type
=
"
number
"
min
=
"
0
"
step
=
"
0.01
"
:
disabled
=
"
!enableRateMultiplier
"
class
=
"
input
"
:
class
=
"
!enableRateMultiplier && 'cursor-not-allowed opacity-50'
"
aria
-
labelledby
=
"
bulk-edit-rate-multiplier-label
"
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.billingRateMultiplierHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<!--
Status
-->
<!--
Status
-->
...
@@ -655,6 +685,7 @@ const enableInterceptWarmup = ref(false)
...
@@ -655,6 +685,7 @@ const enableInterceptWarmup = ref(false)
const
enableProxy
=
ref
(
false
)
const
enableProxy
=
ref
(
false
)
const
enableConcurrency
=
ref
(
false
)
const
enableConcurrency
=
ref
(
false
)
const
enablePriority
=
ref
(
false
)
const
enablePriority
=
ref
(
false
)
const
enableRateMultiplier
=
ref
(
false
)
const
enableStatus
=
ref
(
false
)
const
enableStatus
=
ref
(
false
)
const
enableGroups
=
ref
(
false
)
const
enableGroups
=
ref
(
false
)
...
@@ -670,6 +701,7 @@ const interceptWarmupRequests = ref(false)
...
@@ -670,6 +701,7 @@ const interceptWarmupRequests = ref(false)
const
proxyId
=
ref
<
number
|
null
>
(
null
)
const
proxyId
=
ref
<
number
|
null
>
(
null
)
const
concurrency
=
ref
(
1
)
const
concurrency
=
ref
(
1
)
const
priority
=
ref
(
1
)
const
priority
=
ref
(
1
)
const
rateMultiplier
=
ref
(
1
)
const
status
=
ref
<
'
active
'
|
'
inactive
'
>
(
'
active
'
)
const
status
=
ref
<
'
active
'
|
'
inactive
'
>
(
'
active
'
)
const
groupIds
=
ref
<
number
[]
>
([])
const
groupIds
=
ref
<
number
[]
>
([])
...
@@ -863,6 +895,10 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
...
@@ -863,6 +895,10 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
updates
.
priority
=
priority
.
value
updates
.
priority
=
priority
.
value
}
}
if
(
enableRateMultiplier
.
value
)
{
updates
.
rate_multiplier
=
rateMultiplier
.
value
}
if
(
enableStatus
.
value
)
{
if
(
enableStatus
.
value
)
{
updates
.
status
=
status
.
value
updates
.
status
=
status
.
value
}
}
...
@@ -923,6 +959,7 @@ const handleSubmit = async () => {
...
@@ -923,6 +959,7 @@ const handleSubmit = async () => {
enableProxy
.
value
||
enableProxy
.
value
||
enableConcurrency
.
value
||
enableConcurrency
.
value
||
enablePriority
.
value
||
enablePriority
.
value
||
enableRateMultiplier
.
value
||
enableStatus
.
value
||
enableStatus
.
value
||
enableGroups
.
value
enableGroups
.
value
...
@@ -977,6 +1014,7 @@ watch(
...
@@ -977,6 +1014,7 @@ watch(
enableProxy
.
value
=
false
enableProxy
.
value
=
false
enableConcurrency
.
value
=
false
enableConcurrency
.
value
=
false
enablePriority
.
value
=
false
enablePriority
.
value
=
false
enableRateMultiplier
.
value
=
false
enableStatus
.
value
=
false
enableStatus
.
value
=
false
enableGroups
.
value
=
false
enableGroups
.
value
=
false
...
@@ -991,6 +1029,7 @@ watch(
...
@@ -991,6 +1029,7 @@ watch(
proxyId
.
value
=
null
proxyId
.
value
=
null
concurrency
.
value
=
1
concurrency
.
value
=
1
priority
.
value
=
1
priority
.
value
=
1
rateMultiplier
.
value
=
1
status
.
value
=
'
active
'
status
.
value
=
'
active
'
groupIds
.
value
=
[]
groupIds
.
value
=
[]
}
}
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
90bce60b
...
@@ -1196,7 +1196,7 @@
...
@@ -1196,7 +1196,7 @@
<
ProxySelector
v
-
model
=
"
form.proxy_id
"
:
proxies
=
"
proxies
"
/>
<
ProxySelector
v
-
model
=
"
form.proxy_id
"
:
proxies
=
"
proxies
"
/>
<
/div
>
<
/div
>
<
div
class
=
"
grid grid-cols-2 gap-4
"
>
<
div
class
=
"
grid grid-cols-2 gap-4
lg:grid-cols-3
"
>
<
div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.concurrency
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.concurrency
'
)
}}
<
/label
>
<
input
v
-
model
.
number
=
"
form.concurrency
"
type
=
"
number
"
min
=
"
1
"
class
=
"
input
"
/>
<
input
v
-
model
.
number
=
"
form.concurrency
"
type
=
"
number
"
min
=
"
1
"
class
=
"
input
"
/>
...
@@ -1212,6 +1212,11 @@
...
@@ -1212,6 +1212,11 @@
/>
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.priorityHint
'
)
}}
<
/p
>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.priorityHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
div
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.billingRateMultiplier
'
)
}}
<
/label
>
<
input
v
-
model
.
number
=
"
form.rate_multiplier
"
type
=
"
number
"
min
=
"
0
"
step
=
"
0.01
"
class
=
"
input
"
/>
<
p
class
=
"
input-hint
"
>
{{
t
(
'
admin.accounts.billingRateMultiplierHint
'
)
}}
<
/p
>
<
/div
>
<
/div
>
<
/div
>
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
div
class
=
"
border-t border-gray-200 pt-4 dark:border-dark-600
"
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.expiresAt
'
)
}}
<
/label
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.expiresAt
'
)
}}
<
/label
>
...
@@ -1832,6 +1837,7 @@ const form = reactive({
...
@@ -1832,6 +1837,7 @@ const form = reactive({
proxy_id
:
null
as
number
|
null
,
proxy_id
:
null
as
number
|
null
,
concurrency
:
10
,
concurrency
:
10
,
priority
:
1
,
priority
:
1
,
rate_multiplier
:
1
,
group_ids
:
[]
as
number
[],
group_ids
:
[]
as
number
[],
expires_at
:
null
as
number
|
null
expires_at
:
null
as
number
|
null
}
)
}
)
...
@@ -2119,6 +2125,7 @@ const resetForm = () => {
...
@@ -2119,6 +2125,7 @@ const resetForm = () => {
form
.
proxy_id
=
null
form
.
proxy_id
=
null
form
.
concurrency
=
10
form
.
concurrency
=
10
form
.
priority
=
1
form
.
priority
=
1
form
.
rate_multiplier
=
1
form
.
group_ids
=
[]
form
.
group_ids
=
[]
form
.
expires_at
=
null
form
.
expires_at
=
null
accountCategory
.
value
=
'
oauth-based
'
accountCategory
.
value
=
'
oauth-based
'
...
@@ -2272,6 +2279,7 @@ const createAccountAndFinish = async (
...
@@ -2272,6 +2279,7 @@ const createAccountAndFinish = async (
proxy_id
:
form
.
proxy_id
,
proxy_id
:
form
.
proxy_id
,
concurrency
:
form
.
concurrency
,
concurrency
:
form
.
concurrency
,
priority
:
form
.
priority
,
priority
:
form
.
priority
,
rate_multiplier
:
form
.
rate_multiplier
,
group_ids
:
form
.
group_ids
,
group_ids
:
form
.
group_ids
,
expires_at
:
form
.
expires_at
,
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
auto_pause_on_expired
:
autoPauseOnExpired
.
value
...
@@ -2490,6 +2498,7 @@ const handleCookieAuth = async (sessionKey: string) => {
...
@@ -2490,6 +2498,7 @@ const handleCookieAuth = async (sessionKey: string) => {
proxy_id
:
form
.
proxy_id
,
proxy_id
:
form
.
proxy_id
,
concurrency
:
form
.
concurrency
,
concurrency
:
form
.
concurrency
,
priority
:
form
.
priority
,
priority
:
form
.
priority
,
rate_multiplier
:
form
.
rate_multiplier
,
group_ids
:
form
.
group_ids
,
group_ids
:
form
.
group_ids
,
expires_at
:
form
.
expires_at
,
expires_at
:
form
.
expires_at
,
auto_pause_on_expired
:
autoPauseOnExpired
.
value
auto_pause_on_expired
:
autoPauseOnExpired
.
value
...
...
Prev
1
2
3
4
5
6
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