mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(frontend): centralize admin API error notifications
Show server envelope messages from the response interceptor, with shared transport fallbacks, cancellation handling, and explicit silent requests. Remove duplicate page notifications while preserving contextual batch and irreversible-operation feedback. Propagate request options through API modules and document notification ownership. Verify the notification-only tree with ESLint, vue-tsc, and a Vite build. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -259,6 +259,16 @@ Provider 管理适配在仍持有结构化失败事实时选择静态 `public_me
|
||||
连接测试的 `gateway` / `provider` / `upstream` 来源以及 `not_sent` / `sent` / `ambiguous` 发送状态由 Core 在
|
||||
仍持有完整执行错误时一次判定;Vue 只能根据稳定字段生成摘要,不能匹配英文错误句子反推来源。
|
||||
|
||||
Vue 普通管理请求的错误提示由 `api/request.ts` 响应拦截器统一负责:优先展示安全信封的 `message`,
|
||||
缺失或空白时才使用请求层的 HTTP、网络或超时兜底;成功业务码为 `200`,HTTP 成功但业务码失败也会拒绝。
|
||||
规范化异常保留 `status`、`code`、`requestId` 与 `kind`。页面和 `useAsyncAction` 不再重复弹出接口错误;
|
||||
查询可以保留失败状态与重试入口,本地校验、文件操作、SSE 诊断和成功响应中的业务结果仍归各自 owner。
|
||||
|
||||
API 模块通过统一的 `RequestOptions` 传递 `signal`、`timeout` 和 `silent`。取消或已被新查询取代的请求
|
||||
不弹提示;后台轮询、重启探测等显式使用 `silent`,它只关闭提示,不吞异常,也不跳过会话失效处理。
|
||||
批量操作的部分成功汇总、不可逆操作的结果未知等必要业务处理先将对应请求静默,再由业务 owner 提供
|
||||
一次有上下文的反馈;不得为普通失败重新维护一套消息或业务码映射。
|
||||
|
||||
## 6. 路由、账号范围与 continuation
|
||||
|
||||
Client Key 与账号分组形成授权范围:
|
||||
|
||||
+23
-11
@@ -1,4 +1,4 @@
|
||||
import type { AxiosError, AxiosResponseHeaders, RawAxiosResponseHeaders } from 'axios'
|
||||
import type { AxiosError, AxiosResponse, AxiosResponseHeaders, RawAxiosResponseHeaders } from 'axios'
|
||||
|
||||
export type ApiErrorKind = 'api' | 'timeout' | 'network' | 'http' | 'cancelled'
|
||||
|
||||
@@ -23,31 +23,43 @@ export class ApiError extends Error {
|
||||
export function normalizeApiError(error: AxiosError<unknown>) {
|
||||
const status = error.response?.status ?? 0
|
||||
const requestId = responseHeader(error.response?.headers, 'x-request-id')
|
||||
const envelope = adminErrorEnvelope(error.response?.data)
|
||||
|
||||
if (envelope) {
|
||||
return new ApiError(envelope.message, status, envelope.code, requestId, 'api')
|
||||
}
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || error.code === 'ERR_TIMEOUT') {
|
||||
return new ApiError('请求超时,请稍后重试', status, undefined, requestId, 'timeout')
|
||||
}
|
||||
if (error.code === 'ERR_CANCELED') {
|
||||
return new ApiError('请求已取消', status, undefined, requestId, 'cancelled')
|
||||
}
|
||||
const apiError = error.response && normalizeApiResponseError(error.response)
|
||||
if (apiError)
|
||||
return apiError
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || error.code === 'ERR_TIMEOUT') {
|
||||
return new ApiError('请求超时,请稍后重试', status, undefined, requestId, 'timeout')
|
||||
}
|
||||
if (!error.response) {
|
||||
return new ApiError('网络连接失败,请检查网络后重试', 0, undefined, requestId, 'network')
|
||||
}
|
||||
return new ApiError(httpFallback(status), status, undefined, requestId, 'http')
|
||||
}
|
||||
|
||||
export function normalizeApiResponseError(response: AxiosResponse<unknown>) {
|
||||
const envelope = adminErrorEnvelope(response.data)
|
||||
if (!envelope || envelope.code === 200)
|
||||
return undefined
|
||||
return new ApiError(
|
||||
envelope.message || httpFallback(response.status >= 400 ? response.status : 0),
|
||||
response.status,
|
||||
envelope.code,
|
||||
responseHeader(response.headers, 'x-request-id'),
|
||||
'api',
|
||||
)
|
||||
}
|
||||
|
||||
function adminErrorEnvelope(value: unknown): AdminErrorEnvelope | null {
|
||||
if (!value || typeof value !== 'object')
|
||||
return null
|
||||
const record = value as Record<string, unknown>
|
||||
if (typeof record.code !== 'number' || typeof record.message !== 'string')
|
||||
if (typeof record.code !== 'number' || !Number.isSafeInteger(record.code))
|
||||
return null
|
||||
const message = record.message.trim()
|
||||
return message ? { code: record.code, message } : null
|
||||
const message = typeof record.message === 'string' ? record.message.trim() : ''
|
||||
return { code: record.code, message }
|
||||
}
|
||||
|
||||
function responseHeader(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import request from '../request'
|
||||
|
||||
export interface AccountGroupRef {
|
||||
@@ -78,50 +79,56 @@ interface AccountGroupIdParam {
|
||||
id: string
|
||||
}
|
||||
|
||||
export function getAccountGroups(data: AccountGroupListParams) {
|
||||
export function getAccountGroups(data: AccountGroupListParams, options: RequestOptions = {}) {
|
||||
return request<AccountGroupListResponse>({
|
||||
url: '/api/admin/account-groups',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function createAccountGroup(data: AccountGroupCreateParam) {
|
||||
export function createAccountGroup(data: AccountGroupCreateParam, options: RequestOptions = {}) {
|
||||
return request<AccountGroupMutationResponse>({
|
||||
url: '/api/admin/account-groups/create',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateAccountGroup(data: AccountGroupUpdateParam) {
|
||||
export function updateAccountGroup(data: AccountGroupUpdateParam, options: RequestOptions = {}) {
|
||||
return request<AccountGroupMutationResponse>({
|
||||
url: '/api/admin/account-groups/update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function enableAccountGroup(data: AccountGroupIdParam) {
|
||||
export function enableAccountGroup(data: AccountGroupIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountGroupMutationResponse>({
|
||||
url: '/api/admin/account-groups/enable',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function disableAccountGroup(data: AccountGroupIdParam) {
|
||||
export function disableAccountGroup(data: AccountGroupIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountGroupMutationResponse>({
|
||||
url: '/api/admin/account-groups/disable',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAccountGroup(data: AccountGroupIdParam) {
|
||||
export function deleteAccountGroup(data: AccountGroupIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountGroupMutationResponse>({
|
||||
url: '/api/admin/account-groups/delete',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import type { AccountGroupRef } from './account-groups'
|
||||
import request from '../request'
|
||||
|
||||
@@ -349,43 +350,48 @@ interface AccountExportParam {
|
||||
confirm: string
|
||||
}
|
||||
|
||||
export function getAccounts(data: AccountListParams) {
|
||||
export function getAccounts(data: AccountListParams, options: RequestOptions = {}) {
|
||||
return request<AccountListResponse>({
|
||||
url: '/api/admin/accounts',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function exportAccounts(data: AccountExportParam) {
|
||||
export function exportAccounts(data: AccountExportParam, options: RequestOptions = {}) {
|
||||
return request<unknown>({
|
||||
url: '/api/admin/accounts/export',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function refreshAccount(data: AccountIdParam) {
|
||||
export function refreshAccount(data: AccountIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountRefreshResponse>({
|
||||
url: '/api/admin/accounts/refresh',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function recoverAccount(data: AccountIdParam) {
|
||||
export function recoverAccount(data: AccountIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountRefreshResponse>({
|
||||
url: '/api/admin/accounts/recover',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getAccountProfileStatistics(data: AccountIdParam) {
|
||||
export function getAccountProfileStatistics(data: AccountIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountProfileStatisticsResponse>({
|
||||
url: '/api/admin/accounts/profile-statistics',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -404,90 +410,101 @@ function stableAvatarVersion(value: string) {
|
||||
return hash.toString(36)
|
||||
}
|
||||
|
||||
export function refreshAccountQuota(data: AccountIdParam) {
|
||||
export function refreshAccountQuota(data: AccountIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountQuotaResponse>({
|
||||
url: '/api/admin/accounts/quota/refresh',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getAccountResetCredits(data: AccountIdParam) {
|
||||
export function getAccountResetCredits(data: AccountIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountResetCreditsResponse>({
|
||||
url: '/api/admin/accounts/reset-credits',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function consumeAccountResetCredit(data: AccountResetCreditConsumeParam) {
|
||||
export function consumeAccountResetCredit(data: AccountResetCreditConsumeParam, options: RequestOptions = {}) {
|
||||
return request<AccountResetCreditResultResponse>({
|
||||
url: '/api/admin/accounts/reset-credits',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getAccountModels(data: AccountIdParam) {
|
||||
export function getAccountModels(data: AccountIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountModelsResponse>({
|
||||
url: '/api/admin/accounts/models',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function refreshAccountModels(data: AccountIdParam) {
|
||||
export function refreshAccountModels(data: AccountIdParam, options: RequestOptions = {}) {
|
||||
return request<AccountModelsResponse>({
|
||||
url: '/api/admin/accounts/models/refresh',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function importAccounts(data: AccountImportParam) {
|
||||
export function importAccounts(data: AccountImportParam, options: RequestOptions = {}) {
|
||||
return request<AccountImportResponse>({
|
||||
url: '/api/admin/accounts/import',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateAccount(data: AccountUpdateParam) {
|
||||
export function updateAccount(data: AccountUpdateParam, options: RequestOptions = {}) {
|
||||
return request<AccountUpdateResponse>({
|
||||
url: '/api/admin/accounts/update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function batchUpdateAccounts(data: AccountBatchUpdateParam) {
|
||||
export function batchUpdateAccounts(data: AccountBatchUpdateParam, options: RequestOptions = {}) {
|
||||
return request<AccountBatchUpdateResponse>({
|
||||
url: '/api/admin/accounts/batch-update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAccounts(data: AccountDeleteParams) {
|
||||
export function deleteAccounts(data: AccountDeleteParams, options: RequestOptions = {}) {
|
||||
return request<AccountDeletionResponse>({
|
||||
url: '/api/admin/accounts/delete',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function startAccountOAuth(data: AccountOAuthStartParam) {
|
||||
export function startAccountOAuth(data: AccountOAuthStartParam, options: RequestOptions = {}) {
|
||||
return request<AccountOAuthStartResponse>({
|
||||
url: '/api/admin/accounts/oauth/start',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function completeAccountOAuth(data: AccountOAuthCompleteParam) {
|
||||
export function completeAccountOAuth(data: AccountOAuthCompleteParam, options: RequestOptions = {}) {
|
||||
return request<AccountOAuthCompleteResponse>({
|
||||
url: '/api/admin/accounts/oauth/complete',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import type { AccountGroupRef } from './account-groups'
|
||||
import request from '../request'
|
||||
|
||||
@@ -73,58 +74,65 @@ interface ApiKeyIdParam {
|
||||
id: string
|
||||
}
|
||||
|
||||
export function getApiKeys(data: ApiKeyListParams) {
|
||||
export function getApiKeys(data: ApiKeyListParams, options: RequestOptions = {}) {
|
||||
return request<ApiKeyListResponse>({
|
||||
url: '/api/admin/client-keys',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function createApiKey(data: ApiKeyWriteParam) {
|
||||
export function createApiKey(data: ApiKeyWriteParam, options: RequestOptions = {}) {
|
||||
return request<ApiKeyCreateResponse>({
|
||||
url: '/api/admin/client-keys/create',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateApiKey(data: ApiKeyUpdateParam) {
|
||||
export function updateApiKey(data: ApiKeyUpdateParam, options: RequestOptions = {}) {
|
||||
return request<ApiKeyMutationResponse>({
|
||||
url: '/api/admin/client-keys/update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function revealApiKey(data: ApiKeyIdParam) {
|
||||
export function revealApiKey(data: ApiKeyIdParam, options: RequestOptions = {}) {
|
||||
return request<ApiKeyRevealResponse>({
|
||||
url: '/api/admin/client-keys/reveal',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteApiKey(data: ApiKeyIdParam) {
|
||||
export function deleteApiKey(data: ApiKeyIdParam, options: RequestOptions = {}) {
|
||||
return request<ApiKeyMutationResponse>({
|
||||
url: '/api/admin/client-keys/delete',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function disableApiKey(data: ApiKeyIdParam) {
|
||||
export function disableApiKey(data: ApiKeyIdParam, options: RequestOptions = {}) {
|
||||
return request<ApiKeyMutationResponse>({
|
||||
url: '/api/admin/client-keys/disable',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function enableApiKey(data: ApiKeyIdParam) {
|
||||
export function enableApiKey(data: ApiKeyIdParam, options: RequestOptions = {}) {
|
||||
return request<ApiKeyMutationResponse>({
|
||||
url: '/api/admin/client-keys/enable',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import request from '../request'
|
||||
|
||||
export interface LoginResponse {
|
||||
@@ -17,24 +18,27 @@ interface LoginParam {
|
||||
password: string
|
||||
}
|
||||
|
||||
export function login(data: LoginParam) {
|
||||
export function login(data: LoginParam, options: RequestOptions = {}) {
|
||||
return request<LoginResponse>({
|
||||
url: '/api/admin/auth/login',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getAuthStatus() {
|
||||
export function getAuthStatus(options: RequestOptions = {}) {
|
||||
return request<AuthStatusResponse>({
|
||||
url: '/api/admin/auth/status',
|
||||
method: 'GET',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
export function logout(options: RequestOptions = {}) {
|
||||
return request<LogoutResponse>({
|
||||
url: '/api/admin/auth/logout',
|
||||
method: 'POST',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import request from '../request'
|
||||
|
||||
export interface BackupSettingsView {
|
||||
@@ -95,63 +96,71 @@ export interface DownloadUrlResult {
|
||||
expiresInSeconds: number
|
||||
}
|
||||
|
||||
export function getBackupSettings() {
|
||||
export function getBackupSettings(options: RequestOptions = {}) {
|
||||
return request<BackupSettingsView>({
|
||||
url: '/api/admin/settings/backups',
|
||||
method: 'GET',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateBackupStorage(data: UpdateBackupStoragePayload) {
|
||||
export function updateBackupStorage(data: UpdateBackupStoragePayload, options: RequestOptions = {}) {
|
||||
return request<BackupSettingsView>({
|
||||
url: '/api/admin/settings/backups/storage/update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function testBackupStorage() {
|
||||
export function testBackupStorage(options: RequestOptions = {}) {
|
||||
return request<ConnectionTestResult>({
|
||||
url: '/api/admin/settings/backups/storage/test',
|
||||
method: 'POST',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateBackupSchedule(data: UpdateBackupSchedulePayload) {
|
||||
export function updateBackupSchedule(data: UpdateBackupSchedulePayload, options: RequestOptions = {}) {
|
||||
return request<BackupSettingsView>({
|
||||
url: '/api/admin/settings/backups/schedule/update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getBackupRecords(params: BackupRecordsParams) {
|
||||
export function getBackupRecords(params: BackupRecordsParams, options: RequestOptions = {}) {
|
||||
return request<BackupRecordPage>({
|
||||
url: '/api/admin/settings/backups/records',
|
||||
method: 'GET',
|
||||
params,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function createBackup() {
|
||||
export function createBackup(options: RequestOptions = {}) {
|
||||
return request<BackupRecord>({
|
||||
url: '/api/admin/settings/backups/create',
|
||||
method: 'POST',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getBackupDownloadUrl(backupId: string) {
|
||||
export function getBackupDownloadUrl(backupId: string, options: RequestOptions = {}) {
|
||||
return request<DownloadUrlResult>({
|
||||
url: '/api/admin/settings/backups/download-url',
|
||||
method: 'POST',
|
||||
data: { backupId },
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteBackup(backupId: string) {
|
||||
export function deleteBackup(backupId: string, options: RequestOptions = {}) {
|
||||
return request<BackupRecord>({
|
||||
url: '/api/admin/settings/backups/delete',
|
||||
method: 'POST',
|
||||
data: { backupId },
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import type { AccountQuotaWindow } from './accounts'
|
||||
import type { UsageListRecord } from './usage'
|
||||
import request from '../request'
|
||||
@@ -185,18 +186,20 @@ interface DashboardQuery {
|
||||
kind: string
|
||||
}
|
||||
|
||||
export function getDashboardSummary(data: DashboardQuery) {
|
||||
export function getDashboardSummary(data: DashboardQuery, options: RequestOptions = {}) {
|
||||
return request<DashboardSummaryResponse>({
|
||||
url: '/api/admin/dashboard/summary',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getDashboardTrend(data: DashboardQuery) {
|
||||
export function getDashboardTrend(data: DashboardQuery, options: RequestOptions = {}) {
|
||||
return request<DashboardTrendResponse>({
|
||||
url: '/api/admin/dashboard/trend',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import type { AccountGroupRef } from './account-groups'
|
||||
import request from '../request'
|
||||
|
||||
@@ -43,19 +44,21 @@ interface ProxyAccountPage {
|
||||
page: ProxyPage['page']
|
||||
}
|
||||
|
||||
export function getProxyAccounts(params: { proxyId: string, page: number, pageSize: number, search?: string }) {
|
||||
export function getProxyAccounts(params: { proxyId: string, page: number, pageSize: number, search?: string }, options: RequestOptions = {}) {
|
||||
return request<ProxyAccountPage>({
|
||||
url: '/api/admin/proxies/accounts',
|
||||
method: 'GET',
|
||||
params,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function removeProxyAccount(data: { proxyId: string, accountId: string }) {
|
||||
export function removeProxyAccount(data: { proxyId: string, accountId: string }, options: RequestOptions = {}) {
|
||||
return request<{ configRevision: number }>({
|
||||
url: '/api/admin/proxies/accounts/remove',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,52 +67,58 @@ interface ProxyMutation {
|
||||
configRevision: number
|
||||
}
|
||||
|
||||
export function getProxies(params: { page: number, pageSize: number, search?: string }) {
|
||||
export function getProxies(params: { page: number, pageSize: number, search?: string }, options: RequestOptions = {}) {
|
||||
return request<ProxyPage>({
|
||||
url: '/api/admin/proxies',
|
||||
method: 'GET',
|
||||
params,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function createProxy(data: { name: string, proxyUrl: string }) {
|
||||
export function createProxy(data: { name: string, proxyUrl: string }, options: RequestOptions = {}) {
|
||||
return request<ProxyMutation>({
|
||||
url: '/api/admin/proxies/create',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateProxy(data: { id: string, revision: number, name: string, proxyUrl?: string }) {
|
||||
export function updateProxy(data: { id: string, revision: number, name: string, proxyUrl?: string }, options: RequestOptions = {}) {
|
||||
return request<ProxyMutation>({
|
||||
url: '/api/admin/proxies/update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteProxy(data: { id: string, revision: number }) {
|
||||
export function deleteProxy(data: { id: string, revision: number }, options: RequestOptions = {}) {
|
||||
return request<{ configRevision: number }>({
|
||||
url: '/api/admin/proxies/delete',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function testProxy(data: { id: string, revision: number }) {
|
||||
export function testProxy(data: { id: string, revision: number }, options: RequestOptions = {}) {
|
||||
return request<OutboundProxyRecord>({
|
||||
url: '/api/admin/proxies/test',
|
||||
method: 'POST',
|
||||
data,
|
||||
timeout: 25000,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function probeProxy(data: { proxyUrl: string }) {
|
||||
export function probeProxy(data: { proxyUrl: string }, options: RequestOptions = {}) {
|
||||
return request<OutboundProxyTest>({
|
||||
url: '/api/admin/proxies/probe',
|
||||
method: 'POST',
|
||||
data,
|
||||
timeout: 25000,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import request from '../request'
|
||||
|
||||
export type RotationStrategy = 'smart' | 'quota_reset_priority' | 'round_robin' | 'sticky'
|
||||
@@ -49,48 +50,54 @@ export interface DeletedAdminApiKey {
|
||||
message: string
|
||||
}
|
||||
|
||||
export function getSettings() {
|
||||
export function getSettings(options: RequestOptions = {}) {
|
||||
return request<RuntimeSettings>({
|
||||
url: '/api/admin/settings',
|
||||
method: 'GET',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateSettingsParam = Omit<RuntimeSettings, 'updatedAt'>
|
||||
|
||||
export function updateSettings(data: UpdateSettingsParam) {
|
||||
export function updateSettings(data: UpdateSettingsParam, options: RequestOptions = {}) {
|
||||
return request<RuntimeSettings>({
|
||||
url: '/api/admin/settings/update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getAdminApiKeyStatus() {
|
||||
export function getAdminApiKeyStatus(options: RequestOptions = {}) {
|
||||
return request<AdminApiKeyStatus>({
|
||||
url: '/api/admin/settings/admin-api-key',
|
||||
method: 'GET',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function regenerateAdminApiKey() {
|
||||
export function regenerateAdminApiKey(options: RequestOptions = {}) {
|
||||
return request<RegeneratedAdminApiKey>({
|
||||
url: '/api/admin/settings/admin-api-key/regenerate',
|
||||
method: 'POST',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAdminApiKey() {
|
||||
export function deleteAdminApiKey(options: RequestOptions = {}) {
|
||||
return request<DeletedAdminApiKey>({
|
||||
url: '/api/admin/settings/admin-api-key/delete',
|
||||
method: 'POST',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getCodexDesktopWindowsDownloads(refresh = false) {
|
||||
export function getCodexDesktopWindowsDownloads(refresh = false, options: RequestOptions = {}) {
|
||||
return request<CodexDesktopWindowsDownloads>({
|
||||
url: '/api/admin/settings/client-downloads/codex-desktop/windows',
|
||||
method: 'GET',
|
||||
params: refresh ? { refresh: true } : undefined,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import request from '../request'
|
||||
|
||||
export interface SystemVersion {
|
||||
@@ -42,11 +43,11 @@ export interface SystemRestartAccepted {
|
||||
operationId: string
|
||||
}
|
||||
|
||||
export function getSystemVersion(timeout = 0) {
|
||||
export function getSystemVersion(options: RequestOptions = {}) {
|
||||
return request<SystemVersion>({
|
||||
url: '/api/admin/system/version',
|
||||
method: 'GET',
|
||||
...(timeout ? { timeout } : {}),
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,25 +59,28 @@ interface SystemUpdateTarget {
|
||||
targetVersion?: string
|
||||
}
|
||||
|
||||
export function getSystemUpdateDetail(data: SystemUpdateDetailQuery) {
|
||||
export function getSystemUpdateDetail(data: SystemUpdateDetailQuery, options: RequestOptions = {}) {
|
||||
return request<SystemUpdateDetail>({
|
||||
url: '/api/admin/system/update/detail',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function performSystemUpdate(data: SystemUpdateTarget) {
|
||||
export function performSystemUpdate(data: SystemUpdateTarget, options: RequestOptions = {}) {
|
||||
return request<SystemUpdateAccepted>({
|
||||
url: '/api/admin/system/update',
|
||||
method: 'POST',
|
||||
data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function restartSystem() {
|
||||
export function restartSystem(options: RequestOptions = {}) {
|
||||
return request<SystemRestartAccepted>({
|
||||
url: '/api/admin/system/restart',
|
||||
method: 'POST',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '../request'
|
||||
import request from '../request'
|
||||
|
||||
export interface UsageTokenDetails {
|
||||
@@ -471,50 +472,56 @@ interface UsageDetailQuery {
|
||||
|
||||
type UsageDiagnosticsQuery = UsageRangeQuery & { dimension: string }
|
||||
|
||||
export function getUsageRecords(data: UsagePageQuery) {
|
||||
export function getUsageRecords(data: UsagePageQuery, options: RequestOptions = {}) {
|
||||
return request<UsageRecordsResponse>({
|
||||
url: '/api/admin/usage/records',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getOpsErrors(data: OpsErrorPageQuery) {
|
||||
export function getOpsErrors(data: OpsErrorPageQuery, options: RequestOptions = {}) {
|
||||
return request<OpsErrorsResponse>({
|
||||
url: '/api/admin/operations/errors',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUsageRecordDetail(data: UsageDetailQuery) {
|
||||
export function getUsageRecordDetail(data: UsageDetailQuery, options: RequestOptions = {}) {
|
||||
return request<UsageRecordDetail>({
|
||||
url: '/api/admin/usage/records/detail',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUsageRecordSummary(data: UsageRangeQuery) {
|
||||
export function getUsageRecordSummary(data: UsageRangeQuery, options: RequestOptions = {}) {
|
||||
return request<UsageSummaryResponse>({
|
||||
url: '/api/admin/usage/records/summary',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUsageRecordInsightsOverview(data: UsageRangeQuery) {
|
||||
export function getUsageRecordInsightsOverview(data: UsageRangeQuery, options: RequestOptions = {}) {
|
||||
return request<UsageInsightsOverviewResponse>({
|
||||
url: '/api/admin/usage/insights/overview',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function getUsageRecordInsightsDiagnostics(data: UsageDiagnosticsQuery) {
|
||||
export function getUsageRecordInsightsDiagnostics(data: UsageDiagnosticsQuery, options: RequestOptions = {}) {
|
||||
return request<UsageDiagnosticsResponse>({
|
||||
url: '/api/admin/usage/insights/diagnostics',
|
||||
method: 'GET',
|
||||
params: data,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
+34
-15
@@ -4,13 +4,24 @@ import type {
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
} from 'axios'
|
||||
import axios from 'axios'
|
||||
import type { ApiError } from './error'
|
||||
|
||||
import axios from 'axios'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { API_BASE_URL, API_TIMEOUT_MS } from './constants'
|
||||
import { normalizeApiError } from './error'
|
||||
import { normalizeApiError, normalizeApiResponseError } from './error'
|
||||
|
||||
export { ApiError } from './error'
|
||||
|
||||
export interface RequestOptions {
|
||||
// 静默只关闭全局提示,不吞掉异常,也不跳过会话失效处理。
|
||||
silent?: boolean
|
||||
signal?: AbortSignal
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
type RequestConfig = AxiosRequestConfig & RequestOptions
|
||||
|
||||
const http: AxiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: API_TIMEOUT_MS,
|
||||
@@ -42,22 +53,30 @@ function handleUnauthorizedOnce() {
|
||||
}
|
||||
|
||||
http.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
(response: AxiosResponse<unknown>) => {
|
||||
const error = normalizeApiResponseError(response)
|
||||
if (error)
|
||||
return rejectRequest(error, response.config)
|
||||
return response
|
||||
},
|
||||
(error: AxiosError<unknown>) => {
|
||||
const { response } = error
|
||||
|
||||
const status = response?.status ?? 0
|
||||
|
||||
if (status === 401 && !isAuthenticationRequest(error.config?.url)) {
|
||||
handleUnauthorizedOnce()
|
||||
}
|
||||
|
||||
return Promise.reject(normalizeApiError(error))
|
||||
return rejectRequest(normalizeApiError(error), error.config)
|
||||
},
|
||||
)
|
||||
|
||||
function rejectRequest(error: ApiError, config?: AxiosRequestConfig & Pick<RequestOptions, 'silent'>) {
|
||||
if (error.kind === 'cancelled' || config?.signal?.aborted)
|
||||
return Promise.reject(error)
|
||||
|
||||
const sessionExpired = error.status === 401 && !isAuthenticationRequest(config?.url)
|
||||
const alreadyHandled = sessionExpired && unauthorizedHandled
|
||||
if (sessionExpired)
|
||||
handleUnauthorizedOnce()
|
||||
if (!config?.silent && !alreadyHandled)
|
||||
toast.error(error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
interface ApiEnvelope {
|
||||
code: number
|
||||
message: string
|
||||
@@ -69,12 +88,12 @@ function isApiEnvelope(value: unknown): value is ApiEnvelope {
|
||||
typeof value === 'object'
|
||||
&& value !== null
|
||||
&& 'data' in value
|
||||
&& 'code' in value
|
||||
&& 'message' in value
|
||||
&& 'code' in value && typeof value.code === 'number'
|
||||
&& 'message' in value && typeof value.message === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
export default async function request<T = unknown>(config: AxiosRequestConfig): Promise<T> {
|
||||
export default async function request<T = unknown>(config: RequestConfig): Promise<T> {
|
||||
const response = await http.request<unknown>({
|
||||
...config,
|
||||
})
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { AccountGroup } from '@/api'
|
||||
|
||||
import { onMounted, shallowRef } from 'vue'
|
||||
import { getAccountGroups } from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
export function useAccountGroupCatalog(options: { immediate?: boolean } = {}) {
|
||||
const groups = shallowRef<AccountGroup[]>([])
|
||||
@@ -21,8 +19,7 @@ export function useAccountGroupCatalog(options: { immediate?: boolean } = {}) {
|
||||
groups.value = items
|
||||
return items
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '账号分组加载失败'))
|
||||
catch {
|
||||
return []
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
import { ApiError } from '@/api/request'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>
|
||||
type AsyncActionErrorText = string | false | ((error: unknown) => string | false)
|
||||
|
||||
interface AsyncActionRunOptions {
|
||||
errorText?: AsyncActionErrorText
|
||||
// 仅用于本地校验、文件与浏览器操作;接口错误由请求层负责提示。
|
||||
errorText?: string | false
|
||||
minimumMs?: number
|
||||
onError?: (error: unknown) => void
|
||||
rethrow?: boolean
|
||||
}
|
||||
|
||||
function resolveErrorText(error: unknown, errorText: AsyncActionErrorText | undefined) {
|
||||
const fallback = typeof errorText === 'function' ? errorText(error) : errorText
|
||||
if (fallback === false) {
|
||||
function resolveErrorText(error: unknown, errorText: string | false | undefined) {
|
||||
if (error instanceof ApiError || errorText === false) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return errorMessage(error, '') || fallback || ''
|
||||
return errorMessage(error, errorText || '操作失败')
|
||||
}
|
||||
|
||||
export function useAsyncAction() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '@/api/request'
|
||||
import { clamp } from 'es-toolkit'
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
@@ -15,7 +16,7 @@ interface PageResult {
|
||||
|
||||
export function usePagedQuery<Result extends PageResult>(options: {
|
||||
initialPageSize: number
|
||||
load: (pagination: { page: number, pageSize: number }) => Promise<Result>
|
||||
load: (pagination: { page: number, pageSize: number }, options: RequestOptions) => Promise<Result>
|
||||
onSuccess?: (result: Result) => void
|
||||
onError?: (error: unknown) => void
|
||||
}) {
|
||||
@@ -26,14 +27,16 @@ export function usePagedQuery<Result extends PageResult>(options: {
|
||||
const request = useRequestState(options.onError)
|
||||
const { loading, error, invalidate } = request
|
||||
|
||||
async function execute(execution: { silent?: boolean } = {}) {
|
||||
const requestId = request.start(execution.silent)
|
||||
async function execute(execution: { silent?: boolean, background?: boolean } = {}) {
|
||||
// 保留当前列表的刷新不一定静默;手动刷新仍需由请求层提示失败。
|
||||
const background = execution.background || execution.silent
|
||||
const requestId = request.start(background)
|
||||
|
||||
try {
|
||||
const result = await options.load({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
})
|
||||
}, { silent: execution.silent, signal: request.signal })
|
||||
if (!request.isCurrent(requestId))
|
||||
return false
|
||||
|
||||
@@ -50,7 +53,7 @@ export function usePagedQuery<Result extends PageResult>(options: {
|
||||
return true
|
||||
}
|
||||
catch (cause: unknown) {
|
||||
request.fail(requestId, cause, execution.silent)
|
||||
request.fail(requestId, cause, background)
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { OutboundProxyRecord } from '@/api'
|
||||
import { onMounted, shallowRef } from 'vue'
|
||||
import { getProxies } from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
export function useProxyCatalog() {
|
||||
const proxies = shallowRef<OutboundProxyRecord[]>([])
|
||||
@@ -20,9 +18,7 @@ export function useProxyCatalog() {
|
||||
}
|
||||
proxies.value = items
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(errorMessage(error, '代理列表加载失败'))
|
||||
}
|
||||
catch {}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { onScopeDispose, shallowRef } from 'vue'
|
||||
|
||||
import { ApiError } from '@/api/request'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
export function useRequestState(onError?: (error: unknown) => void) {
|
||||
const loading = shallowRef(false)
|
||||
const error = shallowRef('')
|
||||
let sequence = 0
|
||||
let controller: AbortController | undefined
|
||||
|
||||
function start(silent = false) {
|
||||
const requestId = ++sequence
|
||||
controller?.abort()
|
||||
controller = new AbortController()
|
||||
if (!silent) {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
@@ -21,9 +25,9 @@ export function useRequestState(onError?: (error: unknown) => void) {
|
||||
}
|
||||
|
||||
function fail(requestId: number, cause: unknown, silent = false) {
|
||||
if (!isCurrent(requestId) || silent)
|
||||
if (!isCurrent(requestId) || silent || (cause instanceof ApiError && cause.kind === 'cancelled'))
|
||||
return
|
||||
error.value = errorMessage(cause, '加载失败')
|
||||
error.value = errorMessage(cause)
|
||||
onError?.(cause)
|
||||
}
|
||||
|
||||
@@ -34,10 +38,23 @@ export function useRequestState(onError?: (error: unknown) => void) {
|
||||
|
||||
function invalidate() {
|
||||
sequence += 1
|
||||
controller?.abort()
|
||||
controller = undefined
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
onScopeDispose(invalidate)
|
||||
|
||||
return { loading, error, start, isCurrent, fail, finish, invalidate }
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
start,
|
||||
isCurrent,
|
||||
fail,
|
||||
finish,
|
||||
invalidate,
|
||||
get signal() {
|
||||
return controller?.signal
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RequestOptions } from '@/api/request'
|
||||
import { shallowRef } from 'vue'
|
||||
|
||||
import { useRequestState } from './useRequestState'
|
||||
@@ -16,7 +17,7 @@ interface PageRequest {
|
||||
|
||||
export function useStablePagedQuery<Result extends PageResult>(options: {
|
||||
initialPageSize: number
|
||||
load: (pagination: PageRequest) => Promise<Result>
|
||||
load: (pagination: PageRequest, options: RequestOptions) => Promise<Result>
|
||||
onSuccess?: (result: Result) => void
|
||||
onError?: (error: unknown) => void
|
||||
}) {
|
||||
@@ -34,7 +35,7 @@ export function useStablePagedQuery<Result extends PageResult>(options: {
|
||||
const result = await options.load({
|
||||
currentPage: Math.max(1, targetPage),
|
||||
pageSize: pageSize.value,
|
||||
})
|
||||
}, { silent: execution.silent, signal: request.signal })
|
||||
if (!request.isCurrent(requestId))
|
||||
return false
|
||||
|
||||
|
||||
@@ -99,9 +99,7 @@ async function handleCheckUpdates(force = true) {
|
||||
const data = await checkUpdates(force)
|
||||
toast.success(data?.hasUpdate ? '发现可用更新' : '当前已是最新版本')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '检查更新失败'))
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
async function handleUpdateRequest() {
|
||||
@@ -129,9 +127,7 @@ async function handleUpdateRequest() {
|
||||
}
|
||||
await runConfirmedUpdate(remoteTargetVersion)
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '检查更新失败'))
|
||||
}
|
||||
catch {}
|
||||
finally {
|
||||
preparingUpdate.value = false
|
||||
}
|
||||
@@ -144,9 +140,7 @@ async function runConfirmedUpdate(targetVersion: string) {
|
||||
toast.success('更新完成,请重启服务')
|
||||
}
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '更新失败'))
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
async function handleConfirmUpdate() {
|
||||
@@ -170,9 +164,7 @@ async function handleRestart() {
|
||||
|
||||
watch(open, (visible) => {
|
||||
if (visible && !loadedOnce.value) {
|
||||
void loadSystem(false).catch((error: unknown) => {
|
||||
toast.error(errorMessage(error, '加载系统更新信息失败'))
|
||||
})
|
||||
void loadSystem(false).catch(() => undefined)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,17 +3,15 @@ import { ref } from 'vue'
|
||||
|
||||
import { login as apiLogin, logout as apiLogout, getAuthStatus } from '@/api'
|
||||
import { resetUnauthorizedHandling } from '@/api/request'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const isAuthenticated = ref(false)
|
||||
const sessionChecked = ref(false)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const status = await getAuthStatus()
|
||||
const status = await getAuthStatus({ silent: true })
|
||||
isAuthenticated.value = status.authenticated
|
||||
if (status.authenticated)
|
||||
resetUnauthorizedHandling()
|
||||
@@ -31,7 +29,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
async function login(payload: Parameters<typeof apiLogin>[0]) {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
await apiLogin(payload)
|
||||
|
||||
isAuthenticated.value = true
|
||||
@@ -40,8 +37,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
return true
|
||||
}
|
||||
catch (cause: unknown) {
|
||||
error.value = errorMessage(cause, '登录失败')
|
||||
catch {
|
||||
isAuthenticated.value = false
|
||||
return false
|
||||
}
|
||||
@@ -52,7 +48,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await apiLogout()
|
||||
await apiLogout({ silent: true })
|
||||
}
|
||||
catch {
|
||||
// 忽略登出错误
|
||||
@@ -67,14 +63,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
isAuthenticated.value = false
|
||||
sessionChecked.value = true
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
sessionChecked,
|
||||
loading,
|
||||
error,
|
||||
checkAuth,
|
||||
login,
|
||||
logout,
|
||||
|
||||
@@ -197,7 +197,7 @@ export const useSystemUpdateStore = defineStore('system-update', () => {
|
||||
if (loadVersionPromise)
|
||||
return loadVersionPromise
|
||||
|
||||
loadVersionPromise = getSystemVersion()
|
||||
loadVersionPromise = getSystemVersion({ silent: true })
|
||||
try {
|
||||
const versionData = await loadVersionPromise
|
||||
version.value = versionData
|
||||
@@ -252,7 +252,7 @@ export const useSystemUpdateStore = defineStore('system-update', () => {
|
||||
return result
|
||||
}
|
||||
catch (error: unknown) {
|
||||
updateError.value = errorMessage(error, '更新失败')
|
||||
updateError.value = errorMessage(error)
|
||||
appendUpdateLog({
|
||||
id: `update-client-error-${Date.now()}`,
|
||||
level: 'error',
|
||||
@@ -270,7 +270,7 @@ export const useSystemUpdateStore = defineStore('system-update', () => {
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const readyVersion = await getSystemVersion(restartProbeTimeoutMs)
|
||||
const readyVersion = await getSystemVersion({ timeout: restartProbeTimeoutMs, silent: true })
|
||||
if (normalizeSystemVersion(readyVersion.version) === expectedVersion) {
|
||||
window.location.reload()
|
||||
return
|
||||
@@ -306,12 +306,13 @@ export const useSystemUpdateStore = defineStore('system-update', () => {
|
||||
disconnectUpdateEvents()
|
||||
|
||||
try {
|
||||
await restartSystem()
|
||||
// 进程可能在返回响应前退出,由下面的目标版本探测判定是否完成。
|
||||
await restartSystem({ silent: true })
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (error instanceof ApiError && error.status > 0) {
|
||||
setPhase({ kind: 'failed' })
|
||||
updateError.value = errorMessage(error, '重启失败')
|
||||
updateError.value = errorMessage(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,14 +47,13 @@ export function useAccountGroups() {
|
||||
|
||||
const query = usePagedQuery({
|
||||
initialPageSize: 20,
|
||||
load: ({ page, pageSize }) =>
|
||||
load: ({ page, pageSize }, options) =>
|
||||
getAccountGroups({
|
||||
page,
|
||||
pageSize,
|
||||
search: searchQuery.value.trim() || undefined,
|
||||
enabled: statusQuery.value ? statusQuery.value === 'enabled' : undefined,
|
||||
}),
|
||||
onError: error => toast.error(errorMessage(error, '账号分组加载失败')),
|
||||
}, options),
|
||||
})
|
||||
|
||||
const groups = computed(() => query.items.value.map(group => ({
|
||||
@@ -148,8 +147,6 @@ export function useAccountGroups() {
|
||||
form.value = emptyForm()
|
||||
await Promise.all([query.execute(), loadReferenceKeys()])
|
||||
toast.success(updating ? '分组已更新' : '分组已创建')
|
||||
}, {
|
||||
errorText: editingGroup.value ? '分组更新失败' : '分组创建失败',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -172,7 +169,7 @@ export function useAccountGroups() {
|
||||
pendingDisableGroup.value = null
|
||||
await query.execute()
|
||||
toast.success('分组已禁用')
|
||||
}, { errorText: '禁用分组失败' })
|
||||
})
|
||||
}
|
||||
|
||||
async function toggleStatus(group: AccountGroup) {
|
||||
@@ -183,9 +180,7 @@ export function useAccountGroups() {
|
||||
await query.execute()
|
||||
toast.success(group.enabled ? '分组已禁用' : '分组已启用')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '分组状态更新失败'))
|
||||
}
|
||||
catch {}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -207,7 +202,7 @@ export function useAccountGroups() {
|
||||
pendingDeleteGroup.value = null
|
||||
await query.execute()
|
||||
toast.success('分组已删除')
|
||||
}, { errorText: '删除分组失败', onError: () => void query.execute() })
|
||||
}, { onError: () => void query.execute() })
|
||||
}
|
||||
|
||||
async function confirmBatchDelete() {
|
||||
@@ -219,7 +214,7 @@ export function useAccountGroups() {
|
||||
const failures: unknown[] = []
|
||||
for (const groupId of [...selectedIds.value]) {
|
||||
try {
|
||||
await deleteAccountGroup({ id: groupId })
|
||||
await deleteAccountGroup({ id: groupId }, { silent: true })
|
||||
deletedCount += 1
|
||||
const remaining = new Set(selectedIds.value)
|
||||
remaining.delete(groupId)
|
||||
|
||||
@@ -61,9 +61,6 @@ const { profile, loading, error, load } = useAccountProfileStatistics(accountId,
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span v-if="error && profile" class="mr-auto self-center text-cp-xs font-semibold text-cp-error-text">
|
||||
{{ error }}
|
||||
</span>
|
||||
<BaseButton variant="secondary" @click="open = false">
|
||||
关闭
|
||||
</BaseButton>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function useAccountBatchEditor(options: {
|
||||
options.selectedIds.value = new Set()
|
||||
await Promise.all([options.reloadAccounts(), options.reloadGroups()])
|
||||
toast.success(`已更新 ${accountIds.length} 个账号`)
|
||||
}, { errorText: '批量更新账号失败', onError: () => void options.reloadAccounts() })
|
||||
}, { onError: () => void options.reloadAccounts() })
|
||||
}
|
||||
|
||||
function selectedAccounts() {
|
||||
|
||||
@@ -407,7 +407,10 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
|
||||
modelsRequestMode.value = refresh ? 'refresh' : 'load'
|
||||
connectionTestError.value = ''
|
||||
try {
|
||||
const result = await (refresh ? refreshAccountModels : getAccountModels)({ accountId: account.id })
|
||||
const result = await (refresh ? refreshAccountModels : getAccountModels)(
|
||||
{ accountId: account.id },
|
||||
{ signal: modelsRequest.signal },
|
||||
)
|
||||
if (!modelsRequest.isCurrent(requestId))
|
||||
return
|
||||
applyConnectionTestModels(result, refresh)
|
||||
@@ -421,11 +424,8 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
|
||||
catch (error: unknown) {
|
||||
if (!modelsRequest.isCurrent(requestId))
|
||||
return
|
||||
connectionTestError.value = errorMessage(error, refresh ? '刷新上游模型失败' : '加载测试模型失败')
|
||||
if (refresh) {
|
||||
toast.error(connectionTestError.value)
|
||||
}
|
||||
else {
|
||||
connectionTestError.value = errorMessage(error)
|
||||
if (!refresh) {
|
||||
connectionTestModelOptions.value = []
|
||||
connectionTestSelectedModel.value = ''
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function useAccountEditor(options: {
|
||||
showEditModal.value = false
|
||||
await Promise.all([options.reloadAccounts(), options.reloadGroups()])
|
||||
toast.success('账号已更新')
|
||||
}, { errorText: '账号更新失败' })
|
||||
})
|
||||
}
|
||||
|
||||
watch([showEditModal, saving], ([open, isSaving]) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { getAccounts } from '@/api'
|
||||
import type { RequestOptions } from '@/api/request'
|
||||
import dayjs from 'dayjs'
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
@@ -83,7 +84,6 @@ export function useAccountMutations(options: {
|
||||
await loadAccounts()
|
||||
toast.success('账号已删除')
|
||||
},
|
||||
{ errorText: '删除失败' },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export function useAccountMutations(options: {
|
||||
async () => {
|
||||
const selected = accountsById([...options.selectedIds.value])
|
||||
for (const accounts of accountDeletionGroups(selected)) {
|
||||
await deleteAccountBatch(accounts)
|
||||
await deleteAccountBatch(accounts, { silent: true })
|
||||
deletedCount += accounts.length
|
||||
const deletedIds = new Set(accounts.map(account => account.id))
|
||||
const remaining = new Set(options.selectedIds.value)
|
||||
@@ -164,9 +164,7 @@ export function useAccountMutations(options: {
|
||||
}
|
||||
toast.success('Token 已刷新')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '刷新失败'))
|
||||
}
|
||||
catch {}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,9 +180,7 @@ export function useAccountMutations(options: {
|
||||
}
|
||||
toast.success('额度已刷新')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '额度刷新失败'))
|
||||
}
|
||||
catch {}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -200,9 +196,7 @@ export function useAccountMutations(options: {
|
||||
}
|
||||
toast.success('账号状态已恢复')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '恢复状态失败'))
|
||||
}
|
||||
catch {}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -217,7 +211,7 @@ export function useAccountMutations(options: {
|
||||
return accounts
|
||||
}
|
||||
|
||||
async function deleteAccountBatch(accounts: AccountRow[]) {
|
||||
async function deleteAccountBatch(accounts: AccountRow[], options?: RequestOptions) {
|
||||
const account = accounts[0]
|
||||
if (!account)
|
||||
return
|
||||
@@ -225,7 +219,7 @@ export function useAccountMutations(options: {
|
||||
provider: account.provider,
|
||||
accountIds: accounts.map(account => account.id),
|
||||
}
|
||||
const result = await deleteAccounts(payload)
|
||||
const result = await deleteAccounts(payload, options)
|
||||
if (!result)
|
||||
throw new Error(`不支持的 Provider:${account.provider}`)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ export function useAccountOnboarding(options: {
|
||||
: await importAccountDocument()
|
||||
await finishCreate(message)
|
||||
},
|
||||
{ errorText: '导入失败' },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,7 +97,6 @@ export function useAccountOnboarding(options: {
|
||||
}
|
||||
toast.success('授权链接已生成')
|
||||
},
|
||||
{ errorText: '授权链接生成失败' },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,9 +129,6 @@ export function useAccountOnboarding(options: {
|
||||
: 'OpenAI OAuth 账号已添加',
|
||||
)
|
||||
},
|
||||
{
|
||||
errorText: reauthorizingAccount.value ? '重新授权失败' : 'OAuth 授权导入失败',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -200,7 +195,7 @@ export function useAccountOnboarding(options: {
|
||||
settings: accountImportSettings(createForm.value),
|
||||
outboundProxyId: createForm.value.proxyMode === 'proxy' ? createForm.value.proxyId.trim() : undefined,
|
||||
data: entry.document,
|
||||
})
|
||||
}, { silent: true })
|
||||
importedCount += result.importedCount
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@@ -2,14 +2,13 @@ import type { Ref } from 'vue'
|
||||
import { shallowRef, watch } from 'vue'
|
||||
|
||||
import { getAccountProfileStatistics } from '@/api'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { useRequestState } from '@/composables/useRequestState'
|
||||
|
||||
export function useAccountProfileStatistics(accountId: Ref<string>, open: Ref<boolean>) {
|
||||
const profile = shallowRef<Awaited<ReturnType<typeof getAccountProfileStatistics>> | null>(null)
|
||||
const loadedAccountId = shallowRef('')
|
||||
const loading = shallowRef(false)
|
||||
const error = shallowRef('')
|
||||
let requestVersion = 0
|
||||
const request = useRequestState()
|
||||
const { loading, error } = request
|
||||
|
||||
async function load(force = false) {
|
||||
const targetAccountId = accountId.value
|
||||
@@ -18,31 +17,26 @@ export function useAccountProfileStatistics(accountId: Ref<string>, open: Ref<bo
|
||||
if (!force && profile.value && loadedAccountId.value === targetAccountId)
|
||||
return
|
||||
|
||||
const version = ++requestVersion
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const version = request.start()
|
||||
try {
|
||||
const result = await getAccountProfileStatistics({ accountId: targetAccountId })
|
||||
if (version !== requestVersion)
|
||||
const result = await getAccountProfileStatistics({ accountId: targetAccountId }, { signal: request.signal })
|
||||
if (!request.isCurrent(version))
|
||||
return
|
||||
profile.value = result
|
||||
loadedAccountId.value = targetAccountId
|
||||
}
|
||||
catch (cause) {
|
||||
if (version !== requestVersion)
|
||||
return
|
||||
error.value = errorMessage(cause, '官方个人资料加载失败')
|
||||
request.fail(version, cause)
|
||||
}
|
||||
finally {
|
||||
if (version === requestVersion)
|
||||
loading.value = false
|
||||
request.finish(version)
|
||||
}
|
||||
}
|
||||
|
||||
watch([open, accountId], ([isOpen, nextAccountId], previousValues) => {
|
||||
const previousAccountId = previousValues?.[1]
|
||||
if (nextAccountId !== previousAccountId) {
|
||||
requestVersion += 1
|
||||
request.invalidate()
|
||||
profile.value = null
|
||||
loadedAccountId.value = ''
|
||||
loading.value = false
|
||||
@@ -50,6 +44,8 @@ export function useAccountProfileStatistics(accountId: Ref<string>, open: Ref<bo
|
||||
}
|
||||
if (isOpen)
|
||||
void load()
|
||||
else
|
||||
request.invalidate()
|
||||
}, { immediate: true })
|
||||
|
||||
return {
|
||||
|
||||
@@ -31,6 +31,7 @@ interface ResetCreditsSession {
|
||||
loading: boolean
|
||||
loadError: string
|
||||
loadSequence: number
|
||||
loadController?: AbortController
|
||||
accountUpdatedListeners: Set<(account: Account) => void>
|
||||
}
|
||||
|
||||
@@ -55,12 +56,15 @@ function getResetCreditsSession(accountId: string) {
|
||||
return session
|
||||
}
|
||||
|
||||
async function loadSessionCredits(session: ResetCreditsSession) {
|
||||
async function loadSessionCredits(session: ResetCreditsSession, silent = false) {
|
||||
const sequence = ++session.loadSequence
|
||||
session.loadController?.abort()
|
||||
const controller = new AbortController()
|
||||
session.loadController = controller
|
||||
session.loading = true
|
||||
session.loadError = ''
|
||||
try {
|
||||
const result = await getAccountResetCredits({ accountId: session.accountId })
|
||||
const result = await getAccountResetCredits({ accountId: session.accountId }, { silent, signal: controller.signal })
|
||||
if (sequence !== session.loadSequence)
|
||||
return
|
||||
session.snapshot = {
|
||||
@@ -70,7 +74,7 @@ async function loadSessionCredits(session: ResetCreditsSession) {
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (sequence === session.loadSequence)
|
||||
session.loadError = errorMessage(error, '重置卡查询失败')
|
||||
session.loadError = errorMessage(error)
|
||||
}
|
||||
finally {
|
||||
if (sequence === session.loadSequence)
|
||||
@@ -173,17 +177,18 @@ export function useAccountResetCredits(options: {
|
||||
target.pendingOperation = operation
|
||||
target.consuming = true
|
||||
try {
|
||||
// 不可逆消费由当前会话区分已确认失败与结果未知,不能先弹出可重试的通用错误。
|
||||
const result = await consumeAccountResetCredit({
|
||||
accountId: operation.accountId,
|
||||
creditId: operation.creditId,
|
||||
redeemRequestId: operation.redeemRequestId,
|
||||
})
|
||||
}, { silent: true })
|
||||
const confirmed = result.code === 'reset'
|
||||
|| (result.code === 'already_redeemed' && operation.hasTransportFailure)
|
||||
target.pendingOperation = null
|
||||
if (!confirmed) {
|
||||
toast.error(resetResultMessage(result.code))
|
||||
await loadSessionCredits(target)
|
||||
await loadSessionCredits(target, true)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -191,9 +196,9 @@ export function useAccountResetCredits(options: {
|
||||
? '上次重置已完成'
|
||||
: '额度已重置'
|
||||
applyConfirmedConsumption(target, operation)
|
||||
await loadSessionCredits(target)
|
||||
await loadSessionCredits(target, true)
|
||||
try {
|
||||
const quota = await refreshAccountQuota({ accountId: operation.accountId })
|
||||
const quota = await refreshAccountQuota({ accountId: operation.accountId }, { silent: true })
|
||||
// 原组件可能已经卸载或换号,只通知仍订阅该账号的实例。
|
||||
for (const listener of target.accountUpdatedListeners)
|
||||
listener(quota.account)
|
||||
@@ -218,7 +223,7 @@ export function useAccountResetCredits(options: {
|
||||
else {
|
||||
target.pendingOperation = null
|
||||
toast.error(errorMessage(error, '额度重置失败'))
|
||||
await loadSessionCredits(target)
|
||||
await loadSessionCredits(target, true)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { watchDebounced } from '@vueuse/core'
|
||||
|
||||
import { computed, onMounted, shallowRef, watch } from 'vue'
|
||||
import { getAccounts } from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { usePagedQuery } from '@/composables/usePagedQuery'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
type AccountRow = Awaited<ReturnType<typeof getAccounts>>['items'][number]
|
||||
|
||||
@@ -26,7 +24,7 @@ export function useAccountsQuery() {
|
||||
|
||||
const query = usePagedQuery({
|
||||
initialPageSize: 20,
|
||||
load: ({ page, pageSize }) =>
|
||||
load: ({ page, pageSize }, options) =>
|
||||
getAccounts({
|
||||
page,
|
||||
pageSize,
|
||||
@@ -36,13 +34,10 @@ export function useAccountsQuery() {
|
||||
groupId: groupQuery.value || undefined,
|
||||
sortBy: sort.value?.key,
|
||||
sortDirection: sort.value?.direction,
|
||||
}),
|
||||
}, options),
|
||||
onSuccess: (result) => {
|
||||
accountSummary.value = result.summary
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(errorMessage(error, '账号加载失败'))
|
||||
},
|
||||
})
|
||||
|
||||
const accountPagination = computed(() => ({
|
||||
|
||||
@@ -13,7 +13,6 @@ import { toast } from '@/components/base/BaseToast'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { useCopyText } from '@/composables/useCopyText'
|
||||
import { useIdSet } from '@/composables/useIdSet'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
type ApiKeyRow = Awaited<ReturnType<typeof getApiKeys>>['items'][number]
|
||||
|
||||
@@ -125,7 +124,7 @@ export function useApiKeyMutations(options: {
|
||||
toast.success('API Key 创建成功')
|
||||
}
|
||||
},
|
||||
{ errorText: editingKey.value ? '更新失败' : '创建失败', onError: () => void options.reload() },
|
||||
{ onError: () => void options.reload() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -176,7 +175,7 @@ export function useApiKeyMutations(options: {
|
||||
await options.reload()
|
||||
toast.success('删除成功')
|
||||
},
|
||||
{ errorText: '删除失败', onError: () => void options.reload() },
|
||||
{ onError: () => void options.reload() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -197,7 +196,7 @@ export function useApiKeyMutations(options: {
|
||||
await options.reload()
|
||||
toast.success(`已删除 ${deleteCount} 个 API Key`)
|
||||
},
|
||||
{ errorText: '批量删除失败', onError: () => void options.reload() },
|
||||
{ onError: () => void options.reload() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -209,9 +208,8 @@ export function useApiKeyMutations(options: {
|
||||
await options.reload()
|
||||
toast.success(key.enabled ? '已禁用' : '已启用')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
catch {
|
||||
void options.reload()
|
||||
toast.error(errorMessage(error, '状态更新失败'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -221,19 +219,25 @@ export function useApiKeyMutations(options: {
|
||||
}
|
||||
|
||||
async function revealPlaintextKey(apiKey: ApiKeyRow) {
|
||||
const result = await revealingKeys.run(apiKey.id, () => revealApiKey({ id: apiKey.id }))
|
||||
if (!result?.plaintextKey)
|
||||
throw new Error('完整 API Key 不可用')
|
||||
return result.plaintextKey
|
||||
try {
|
||||
const result = await revealingKeys.run(apiKey.id, () => revealApiKey({ id: apiKey.id }))
|
||||
if (!result)
|
||||
return undefined
|
||||
if (!result.plaintextKey) {
|
||||
toast.error('完整 API Key 不可用')
|
||||
return undefined
|
||||
}
|
||||
return result.plaintextKey
|
||||
}
|
||||
catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function copyApiKey(apiKey: ApiKeyRow) {
|
||||
try {
|
||||
await copyToClipboard(await revealPlaintextKey(apiKey))
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '读取完整密钥失败'))
|
||||
}
|
||||
const key = await revealPlaintextKey(apiKey)
|
||||
if (key)
|
||||
await copyToClipboard(key)
|
||||
}
|
||||
|
||||
watch(showKeyModal, (open) => {
|
||||
|
||||
@@ -3,8 +3,6 @@ import type { getApiKeys } from '@/api'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
|
||||
import { API_BASE_URL } from '@/api/constants'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { buildCodexCcSwitchImportDeeplink } from '../utils/ccswitchImport'
|
||||
|
||||
// “使用密钥”弹窗展示明文时,在列表行上补挂 reveal 得到的完整 key。
|
||||
@@ -15,7 +13,7 @@ type ApiKeyRow = Awaited<ReturnType<typeof getApiKeys>>['items'][number] & { key
|
||||
export function useApiKeyUse(options: {
|
||||
createdKey: Readonly<Ref<string>>
|
||||
createdKeyName: Readonly<Ref<string>>
|
||||
revealPlaintextKey: (apiKey: ApiKeyRow) => Promise<string>
|
||||
revealPlaintextKey: (apiKey: ApiKeyRow) => Promise<string | undefined>
|
||||
}) {
|
||||
const showUseKeyModal = shallowRef(false)
|
||||
const selectedUseKey: ShallowRef<ApiKeyRow | null> = shallowRef(null)
|
||||
@@ -54,28 +52,22 @@ export function useApiKeyUse(options: {
|
||||
}
|
||||
|
||||
async function openUseKeyModal(apiKey: ApiKeyRow) {
|
||||
try {
|
||||
const key = await options.revealPlaintextKey(apiKey)
|
||||
selectedUseKey.value = { ...apiKey, key }
|
||||
showUseKeyModal.value = true
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '读取完整密钥失败'))
|
||||
}
|
||||
const key = await options.revealPlaintextKey(apiKey)
|
||||
if (!key)
|
||||
return
|
||||
selectedUseKey.value = { ...apiKey, key }
|
||||
showUseKeyModal.value = true
|
||||
}
|
||||
|
||||
async function importToCcs(apiKey: ApiKeyRow) {
|
||||
try {
|
||||
const key = await options.revealPlaintextKey(apiKey)
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
apiKey: key,
|
||||
baseUrl: openAiBaseUrl.value,
|
||||
providerName: apiKey.name || apiKey.prefix || 'codex-proxy-rs',
|
||||
})
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '读取完整密钥失败'))
|
||||
}
|
||||
const key = await options.revealPlaintextKey(apiKey)
|
||||
if (!key)
|
||||
return
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
apiKey: key,
|
||||
baseUrl: openAiBaseUrl.value,
|
||||
providerName: apiKey.name || apiKey.prefix || 'codex-proxy-rs',
|
||||
})
|
||||
}
|
||||
|
||||
watch(showUseKeyModal, (open) => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { BaseTableSort } from '@/components/base/BaseTable/columns'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
|
||||
import { computed, onMounted, onScopeDispose, shallowRef } from 'vue'
|
||||
import { computed, onMounted, shallowRef } from 'vue'
|
||||
import { getApiKeys } from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { useRequestState } from '@/composables/useRequestState'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
export function useApiKeysQuery() {
|
||||
@@ -18,9 +17,9 @@ export function useApiKeysQuery() {
|
||||
createdAtDisplay: string
|
||||
}>
|
||||
>([])
|
||||
const loading = shallowRef(false)
|
||||
const request = useRequestState()
|
||||
const { loading } = request
|
||||
const cursors = new Map<number, string | undefined>([[1, undefined]])
|
||||
let requestSequence = 0
|
||||
|
||||
const apiKeyPagination = computed(() => ({
|
||||
currentPage: page.value,
|
||||
@@ -41,14 +40,14 @@ export function useApiKeysQuery() {
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchPage(cursor: string | undefined, limit: number, search: string | undefined) {
|
||||
async function fetchPage(cursor: string | undefined, limit: number, search: string | undefined, signal?: AbortSignal) {
|
||||
return getApiKeys({
|
||||
cursor,
|
||||
limit,
|
||||
search,
|
||||
sortBy: sort.value?.key,
|
||||
sortDirection: sort.value?.direction,
|
||||
})
|
||||
}, { signal })
|
||||
}
|
||||
|
||||
function applyPage(result: Awaited<ReturnType<typeof getApiKeys>>, targetPage: number) {
|
||||
@@ -61,19 +60,19 @@ export function useApiKeysQuery() {
|
||||
}
|
||||
|
||||
async function execute(targetPage = page.value) {
|
||||
const requestId = ++requestSequence
|
||||
const requestId = request.start()
|
||||
const signal = request.signal
|
||||
const requestedPage = Math.max(1, targetPage)
|
||||
const requestedPageSize = pageSize.value
|
||||
const requestedSearch = searchQuery.value.trim() || undefined
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
let currentPage = knownPageBefore(requestedPage)
|
||||
let result
|
||||
|
||||
while (currentPage < requestedPage) {
|
||||
result = await fetchPage(cursors.get(currentPage), requestedPageSize, requestedSearch)
|
||||
if (requestId !== requestSequence)
|
||||
result = await fetchPage(cursors.get(currentPage), requestedPageSize, requestedSearch, signal)
|
||||
if (!request.isCurrent(requestId))
|
||||
return false
|
||||
|
||||
if (!result.nextCursor) {
|
||||
@@ -85,8 +84,8 @@ export function useApiKeysQuery() {
|
||||
currentPage += 1
|
||||
}
|
||||
|
||||
result = await fetchPage(cursors.get(currentPage), requestedPageSize, requestedSearch)
|
||||
if (requestId !== requestSequence)
|
||||
result = await fetchPage(cursors.get(currentPage), requestedPageSize, requestedSearch, signal)
|
||||
if (!request.isCurrent(requestId))
|
||||
return false
|
||||
|
||||
if (result.nextCursor)
|
||||
@@ -96,14 +95,11 @@ export function useApiKeysQuery() {
|
||||
applyPage(result, currentPage)
|
||||
return true
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (requestId === requestSequence)
|
||||
toast.error(errorMessage(error, 'API Key 加载失败'))
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
if (requestId === requestSequence)
|
||||
loading.value = false
|
||||
request.finish(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,9 +133,6 @@ export function useApiKeysQuery() {
|
||||
onMounted(() => {
|
||||
void execute()
|
||||
})
|
||||
onScopeDispose(() => {
|
||||
requestSequence += 1
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
|
||||
@@ -19,6 +19,8 @@ export function useDashboard() {
|
||||
const lastRefreshedAt = shallowRef('')
|
||||
let trendRequestId = 0
|
||||
let disposed = false
|
||||
let summaryController: AbortController | undefined
|
||||
let trendController: AbortController | undefined
|
||||
|
||||
const metrics = computed(() => snapshot.value.metrics)
|
||||
const healthTimeline = computed(() => snapshot.value.healthTimeline)
|
||||
@@ -36,18 +38,18 @@ export function useDashboard() {
|
||||
|
||||
const { resume: startAutoRefresh } = useIntervalFn(
|
||||
() => {
|
||||
void loadDashboardData()
|
||||
void loadDashboardData(true)
|
||||
},
|
||||
30_000,
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
async function loadDashboardData() {
|
||||
async function loadDashboardData(silent = false) {
|
||||
if (loading.value || refreshing.value)
|
||||
return
|
||||
try {
|
||||
loading.value = true
|
||||
await loadDashboardSnapshot()
|
||||
await loadDashboardSnapshot(silent)
|
||||
}
|
||||
catch {
|
||||
// 自动刷新会继续重试,保留最后一次成功快照。
|
||||
@@ -76,16 +78,18 @@ export function useDashboard() {
|
||||
const trendKind = normalizeDashboardTrendKind(kind)
|
||||
activeTrendKind.value = trendKind
|
||||
const requestId = ++trendRequestId
|
||||
trendController?.abort()
|
||||
trendController = new AbortController()
|
||||
trendLoading.value = true
|
||||
trendError.value = ''
|
||||
try {
|
||||
const result = await getDashboardTrend({ kind: trendKind })
|
||||
const result = await getDashboardTrend({ kind: trendKind }, { signal: trendController.signal })
|
||||
if (isCurrentTrendRequest(requestId, trendKind))
|
||||
trend.value = result
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (isCurrentTrendRequest(requestId, trendKind))
|
||||
trendError.value = errorMessage(error, '趋势加载失败')
|
||||
trendError.value = errorMessage(error)
|
||||
}
|
||||
finally {
|
||||
if (isCurrentTrendRequest(requestId, trendKind))
|
||||
@@ -93,13 +97,16 @@ export function useDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDashboardSnapshot() {
|
||||
async function loadDashboardSnapshot(silent = false) {
|
||||
const trendKind = activeTrendKind.value
|
||||
const requestId = ++trendRequestId
|
||||
summaryController?.abort()
|
||||
trendController?.abort()
|
||||
summaryController = new AbortController()
|
||||
trendLoading.value = true
|
||||
trendError.value = ''
|
||||
try {
|
||||
const summary = await getDashboardSummary({ kind: trendKind })
|
||||
const summary = await getDashboardSummary({ kind: trendKind }, { silent, signal: summaryController.signal })
|
||||
if (disposed)
|
||||
return
|
||||
snapshot.value = dashboardSnapshotView(summary)
|
||||
@@ -110,7 +117,7 @@ export function useDashboard() {
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (isCurrentTrendRequest(requestId, trendKind))
|
||||
trendError.value = errorMessage(error, '趋势加载失败')
|
||||
trendError.value = errorMessage(error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
@@ -134,6 +141,8 @@ export function useDashboard() {
|
||||
onScopeDispose(() => {
|
||||
disposed = true
|
||||
trendRequestId += 1
|
||||
summaryController?.abort()
|
||||
trendController?.abort()
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { CircleAlert, Eye, EyeOff, KeyRound, Mail, Moon, Sun } from '@lucide/vue'
|
||||
import { Eye, EyeOff, KeyRound, Mail, Moon, Sun } from '@lucide/vue'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
|
||||
import AppBrandMark from '@/components/AppBrandMark.vue'
|
||||
@@ -13,7 +13,6 @@ type ThemeName = 'light' | 'dark'
|
||||
type PasswordInputType = 'password' | 'text'
|
||||
|
||||
const props = defineProps<{
|
||||
error?: string | null
|
||||
loading: boolean
|
||||
submitDisabled: boolean
|
||||
effectiveTheme: ThemeName
|
||||
@@ -94,11 +93,6 @@ function togglePasswordVisible(): void {
|
||||
</section>
|
||||
|
||||
<div class="grid gap-3">
|
||||
<div v-if="props.error" class="login-error" role="alert">
|
||||
<CircleAlert :size="16" />
|
||||
<p>{{ props.error }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-2">
|
||||
<span class="text-cp leading-[1.1] font-bold text-(--cp-login-label-color)">管理员账号</span>
|
||||
<BaseInput
|
||||
@@ -289,26 +283,6 @@ function togglePasswordVisible(): void {
|
||||
transform: translateX(33px);
|
||||
}
|
||||
|
||||
.login-error {
|
||||
display: flex;
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--cp-login-error-bg);
|
||||
padding: 0 12px;
|
||||
color: var(--cp-login-error-icon-color);
|
||||
}
|
||||
|
||||
.login-error p {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--cp-login-error-text-color);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.login-password-toggle {
|
||||
--cp-color-fill-quaternary: color-mix(in srgb, var(--cp-input-hover-bg) 62%, transparent);
|
||||
--cp-color-fill-tertiary: color-mix(in srgb, var(--cp-input-hover-bg) 88%, transparent);
|
||||
|
||||
@@ -61,7 +61,6 @@ async function handleSubmit(): Promise<void> {
|
||||
<LoginPanel
|
||||
v-model:username="username"
|
||||
v-model:password="password"
|
||||
:error="authStore.error"
|
||||
:loading="loginLoading"
|
||||
:submit-disabled="submitDisabled"
|
||||
:effective-theme="effectiveTheme"
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getProxyAccounts, removeProxyAccount } from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { usePagedQuery } from '@/composables/usePagedQuery'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
export function useProxyAccounts(options: {
|
||||
isOpen: () => boolean
|
||||
@@ -15,12 +14,11 @@ export function useProxyAccounts(options: {
|
||||
const removeAction = useAsyncAction()
|
||||
const query = usePagedQuery({
|
||||
initialPageSize: 20,
|
||||
load: pagination => getProxyAccounts({
|
||||
load: (pagination, requestOptions) => getProxyAccounts({
|
||||
...pagination,
|
||||
proxyId: options.proxyId() ?? '',
|
||||
search: search.value.trim() || undefined,
|
||||
}),
|
||||
onError: error => toast.error(errorMessage(error, '关联账号加载失败')),
|
||||
}, requestOptions),
|
||||
})
|
||||
const pagination = computed(() => ({
|
||||
currentPage: query.page.value,
|
||||
@@ -60,7 +58,7 @@ export function useProxyAccounts(options: {
|
||||
await query.execute()
|
||||
}
|
||||
return true
|
||||
}, { errorText: '移除关联账号失败' }) ?? false
|
||||
}) ?? false
|
||||
}
|
||||
|
||||
watch([options.isOpen, options.proxyId], () => {
|
||||
|
||||
@@ -16,7 +16,6 @@ import BaseTable from '@/components/base/BaseTable/index.vue'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { usePagedQuery } from '@/composables/usePagedQuery'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import ProxyAccountsModal from './components/ProxyAccountsModal.vue'
|
||||
import ProxyFormModal from './components/ProxyFormModal.vue'
|
||||
@@ -24,8 +23,7 @@ import ProxyFormModal from './components/ProxyFormModal.vue'
|
||||
const search = shallowRef('')
|
||||
const query = usePagedQuery({
|
||||
initialPageSize: 20,
|
||||
load: pagination => getProxies({ ...pagination, search: search.value.trim() || undefined }),
|
||||
onError: error => toast.error(errorMessage(error, '代理列表加载失败')),
|
||||
load: (pagination, options) => getProxies({ ...pagination, search: search.value.trim() || undefined }, options),
|
||||
})
|
||||
const { items: proxies, loading } = query
|
||||
const pagination = computed(() => ({ currentPage: query.page.value, pageSize: query.pageSize.value, total: query.total.value }))
|
||||
@@ -70,9 +68,7 @@ async function checkProxy(proxy: OutboundProxyRecord) {
|
||||
else
|
||||
toast.error(result.lastTest?.message ?? '代理测试失败')
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(errorMessage(error, '代理测试失败'))
|
||||
}
|
||||
catch {}
|
||||
finally {
|
||||
testingIds.value.delete(proxy.id)
|
||||
await query.execute({ silent: true })
|
||||
@@ -98,7 +94,7 @@ async function testConnection() {
|
||||
toast.success(`连接成功,耗时 ${result.latencyMs} ms`)
|
||||
else
|
||||
toast.error(result.message)
|
||||
}, { errorText: '代理测试失败' })
|
||||
})
|
||||
}
|
||||
|
||||
async function save() {
|
||||
@@ -121,7 +117,7 @@ async function save() {
|
||||
search.value = ''
|
||||
query.page.value = 1
|
||||
await query.execute()
|
||||
}, { errorText: '代理保存失败' })
|
||||
})
|
||||
}
|
||||
|
||||
function requestDelete(proxy: OutboundProxyRecord) {
|
||||
@@ -138,7 +134,7 @@ async function confirmDelete() {
|
||||
showDelete.value = false
|
||||
await query.execute()
|
||||
toast.success('代理已删除')
|
||||
}, { errorText: '代理删除失败,请确认没有账号使用该代理' })
|
||||
})
|
||||
}
|
||||
|
||||
function setPage(page: number) {
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export function useClientDownloads() {
|
||||
downloads.value = await getCodexDesktopWindowsDownloads(refresh)
|
||||
}
|
||||
catch (cause: unknown) {
|
||||
error.value = errorMessage(cause, 'Windows 离线安装包加载失败')
|
||||
error.value = errorMessage(cause)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { useCopyText } from '@/composables/useCopyText'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
export function useAdminApiKey() {
|
||||
const loading = shallowRef(true)
|
||||
@@ -26,9 +25,7 @@ export function useAdminApiKey() {
|
||||
loading.value = true
|
||||
status.exists = (await getAdminApiKeyStatus()).exists
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '管理员 API Key 状态加载失败'))
|
||||
}
|
||||
catch {}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -45,7 +42,6 @@ export function useAdminApiKey() {
|
||||
status.exists = true
|
||||
toast.success(wasEnabled ? '管理员 API Key 已更新' : '管理员 API Key 已生成')
|
||||
},
|
||||
{ errorText: false, onError: error => toast.error(errorMessage(error, '生成失败')) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,7 +57,6 @@ export function useAdminApiKey() {
|
||||
showDeleteModal.value = false
|
||||
toast.success('管理员 API Key 已删除')
|
||||
},
|
||||
{ errorText: false, onError: error => toast.error(errorMessage(error, '删除失败')) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getBackupDownloadUrl,
|
||||
getBackupRecords,
|
||||
} from '@/api'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { usePagedQuery } from '@/composables/usePagedQuery'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
@@ -31,9 +32,8 @@ export function useBackupRecords() {
|
||||
page: { page: number, pageSize: number, total: number, totalPages: number }
|
||||
}>({
|
||||
initialPageSize: 10,
|
||||
load: ({ page, pageSize }) =>
|
||||
getBackupRecords({ page, pageSize }),
|
||||
onError: () => undefined,
|
||||
load: ({ page, pageSize }, options) =>
|
||||
getBackupRecords({ page, pageSize }, options),
|
||||
})
|
||||
|
||||
const records = computed<BackupRecord[]>(() => paged.items.value as BackupRecord[])
|
||||
@@ -92,7 +92,7 @@ export function useBackupRecords() {
|
||||
return
|
||||
refreshing.value = true
|
||||
try {
|
||||
await paged.execute({ silent: true })
|
||||
await paged.execute({ background: true })
|
||||
}
|
||||
finally {
|
||||
refreshing.value = false
|
||||
@@ -119,9 +119,7 @@ export function useBackupRecords() {
|
||||
toast.success('备份任务已创建')
|
||||
await paged.execute({ silent: true })
|
||||
}
|
||||
catch (cause) {
|
||||
toast.error(errorMessage(cause, '创建备份失败'))
|
||||
}
|
||||
catch {}
|
||||
finally {
|
||||
creating.value = false
|
||||
}
|
||||
@@ -142,7 +140,8 @@ export function useBackupRecords() {
|
||||
link.remove()
|
||||
}
|
||||
catch (cause) {
|
||||
toast.error(errorMessage(cause, '下载备份失败'))
|
||||
if (!(cause instanceof ApiError))
|
||||
toast.error(errorMessage(cause, '下载备份失败'))
|
||||
}
|
||||
finally {
|
||||
downloadStates.value = { ...downloadStates.value, [record.id]: false }
|
||||
@@ -163,9 +162,7 @@ export function useBackupRecords() {
|
||||
toast.success('已请求删除备份')
|
||||
await paged.execute({ silent: true })
|
||||
}
|
||||
catch (cause) {
|
||||
toast.error(errorMessage(cause, '删除备份失败'))
|
||||
}
|
||||
catch {}
|
||||
finally {
|
||||
deleting.value = false
|
||||
deleteTarget.value = null
|
||||
|
||||
@@ -52,7 +52,7 @@ export function useBackupSettings() {
|
||||
loaded.value = true
|
||||
}
|
||||
catch (cause) {
|
||||
error.value = errorMessage(cause, '加载备份配置失败')
|
||||
error.value = errorMessage(cause)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -97,8 +97,7 @@ export function useBackupSettings() {
|
||||
toast.success(schedulePaused ? '存储配置已保存,定时备份已暂停,请测试连接后重新启用' : '存储配置已保存')
|
||||
return true
|
||||
}
|
||||
catch (cause) {
|
||||
toast.error(errorMessage(cause, '保存存储配置失败'))
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
@@ -119,9 +118,8 @@ export function useBackupSettings() {
|
||||
toast.error(`${result.stage}: ${result.message}`)
|
||||
}
|
||||
}
|
||||
catch (cause) {
|
||||
catch {
|
||||
verified.value = false
|
||||
toast.error(errorMessage(cause, '连接测试失败'))
|
||||
}
|
||||
finally {
|
||||
testing.value = false
|
||||
@@ -142,8 +140,7 @@ export function useBackupSettings() {
|
||||
toast.success('调度配置已保存')
|
||||
return true
|
||||
}
|
||||
catch (cause) {
|
||||
toast.error(errorMessage(cause, '保存调度配置失败'))
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -2,14 +2,17 @@ import type { rotationOptions } from '../constants'
|
||||
import { computed, reactive, ref, shallowRef } from 'vue'
|
||||
|
||||
import { getSettings, updateSettings } from '@/api'
|
||||
import { ApiError } from '@/api/request'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
|
||||
type RotationStrategy = (typeof rotationOptions)[number]['value']
|
||||
|
||||
export function useSettingsForm() {
|
||||
const loading = shallowRef(true)
|
||||
const saving = shallowRef(false)
|
||||
const saveAction = useAsyncAction()
|
||||
const saving = saveAction.loading
|
||||
const error = shallowRef('')
|
||||
const mappings = ref<Array<{ requestedModel: string, upstreamModel: string }>>([])
|
||||
const form = reactive({
|
||||
@@ -68,15 +71,14 @@ export function useSettingsForm() {
|
||||
}))
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
async function loadSettings(silent = false) {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
applySettings(await getSettings())
|
||||
applySettings(await getSettings({ silent }))
|
||||
}
|
||||
catch (cause: unknown) {
|
||||
error.value = errorMessage(cause, '设置加载失败')
|
||||
toast.error(error.value)
|
||||
error.value = errorMessage(cause)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -127,8 +129,7 @@ export function useSettingsForm() {
|
||||
toast.warning('请修正客户端最低版本格式')
|
||||
return
|
||||
}
|
||||
try {
|
||||
saving.value = true
|
||||
await saveAction.run(async () => {
|
||||
const result = await updateSettings({
|
||||
modelMappings: mappingPayload(),
|
||||
refreshMarginSeconds,
|
||||
@@ -144,15 +145,12 @@ export function useSettingsForm() {
|
||||
})
|
||||
applySettings(result)
|
||||
toast.success('设置已保存')
|
||||
}
|
||||
catch (cause: unknown) {
|
||||
error.value = errorMessage(cause, '保存失败')
|
||||
toast.error(error.value)
|
||||
await loadSettings()
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}, {
|
||||
onError: (cause) => {
|
||||
if (cause instanceof ApiError)
|
||||
void loadSettings(true)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,9 +4,8 @@ import { watchDebounced } from '@vueuse/core'
|
||||
|
||||
import { computed, onScopeDispose, shallowRef, watch } from 'vue'
|
||||
import { getOpsErrors } from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { useStablePagedQuery } from '@/composables/useStablePagedQuery'
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
import { withMinimumDuration } from '@/utils/async'
|
||||
import { usageSearchParam } from '../utils/search'
|
||||
|
||||
interface UseOpsErrorsTableOptions {
|
||||
@@ -34,12 +33,11 @@ export function useOpsErrorsTable(options: UseOpsErrorsTableOptions) {
|
||||
|
||||
const query = useStablePagedQuery({
|
||||
initialPageSize: 10,
|
||||
load: ({ currentPage, pageSize }) => getOpsErrors({
|
||||
load: ({ currentPage, pageSize }, requestOptions) => getOpsErrors({
|
||||
currentPage,
|
||||
pageSize,
|
||||
...tableParams,
|
||||
}),
|
||||
onError: error => toast.error(errorMessage(error, '加载错误明细失败')),
|
||||
}, requestOptions),
|
||||
})
|
||||
const pagination = computed(() => ({
|
||||
currentPage: query.currentPage.value,
|
||||
|
||||
@@ -14,18 +14,22 @@ export function useRequestDiagnostics(requestId: () => string) {
|
||||
watch(requestId, id => selectedId.value = id)
|
||||
watch([selectedId, revision], async ([id], _previous, onCleanup) => {
|
||||
let active = true
|
||||
onCleanup(() => active = false)
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => {
|
||||
active = false
|
||||
controller.abort()
|
||||
})
|
||||
detail.value = null
|
||||
error.value = ''
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await getUsageRecordDetail({ id })
|
||||
const result = await getUsageRecordDetail({ id }, { signal: controller.signal })
|
||||
if (active)
|
||||
detail.value = result
|
||||
}
|
||||
catch (cause: unknown) {
|
||||
if (active)
|
||||
error.value = errorMessage(cause, '诊断记录暂不可用,请稍后刷新')
|
||||
error.value = errorMessage(cause)
|
||||
}
|
||||
finally {
|
||||
if (active)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { UsageDisplayRecord, UsageViewModel } from '../utils/records'
|
||||
import { shallowRef } from 'vue'
|
||||
import { getUsageRecordDetail } from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
import { errorMessage } from '@/utils/async'
|
||||
import { normalizeUsageRecord } from '../utils/records'
|
||||
|
||||
export function useUsageRecordDetail() {
|
||||
@@ -15,9 +13,7 @@ export function useUsageRecordDetail() {
|
||||
selectedUsageRecord.value = normalizeUsageRecord(detail)
|
||||
showDetailModal.value = true
|
||||
}
|
||||
catch (error: unknown) {
|
||||
toast.error(errorMessage(error, '加载详情失败'))
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
getUsageRecords,
|
||||
getUsageRecordSummary,
|
||||
} from '@/api'
|
||||
import { toast } from '@/components/base/BaseToast'
|
||||
|
||||
import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
import { withMinimumDuration } from '@/utils/async'
|
||||
import { usageSearchParam } from '../utils/search'
|
||||
|
||||
interface UseUsageRecordsTableOptions {
|
||||
@@ -46,6 +44,9 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
let tableRequestId = 0
|
||||
let analyticsRequestId = 0
|
||||
let diagnosticRequestId = 0
|
||||
let tableController: AbortController | undefined
|
||||
let analyticsController: AbortController | undefined
|
||||
let diagnosticController: AbortController | undefined
|
||||
let disposed = false
|
||||
const scopedParams = () => ({
|
||||
...options.timeRangeParams.value,
|
||||
@@ -86,13 +87,15 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
|
||||
async function loadUsagePage(background: boolean) {
|
||||
const requestId = ++tableRequestId
|
||||
tableController?.abort()
|
||||
tableController = new AbortController()
|
||||
loading.value = !background
|
||||
try {
|
||||
const result = await getUsageRecords({
|
||||
currentPage: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
...tableParams,
|
||||
})
|
||||
}, { signal: tableController.signal })
|
||||
if (requestId !== tableRequestId)
|
||||
return
|
||||
|
||||
@@ -101,11 +104,7 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
totalRecords.value = result.total
|
||||
currentPage.value = result.currentPage
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (requestId !== tableRequestId)
|
||||
return
|
||||
toast.error(errorMessage(error, '加载失败'))
|
||||
}
|
||||
catch {}
|
||||
finally {
|
||||
if (requestId === tableRequestId) {
|
||||
loading.value = false
|
||||
@@ -116,16 +115,20 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
async function loadUsageAnalytics(globalParams: ReturnType<typeof scopedParams>, background: boolean) {
|
||||
const requestId = ++analyticsRequestId
|
||||
const diagnosticsId = ++diagnosticRequestId
|
||||
analyticsController?.abort()
|
||||
diagnosticController?.abort()
|
||||
analyticsController = new AbortController()
|
||||
const requestOptions = { signal: analyticsController.signal }
|
||||
const dimension = diagnosticDimension.value
|
||||
analyticsLoading.value = !background
|
||||
try {
|
||||
const [nextSummary, overview, diagnostics] = await Promise.all([
|
||||
getUsageRecordSummary(globalParams),
|
||||
getUsageRecordInsightsOverview(globalParams),
|
||||
getUsageRecordSummary(globalParams, requestOptions),
|
||||
getUsageRecordInsightsOverview(globalParams, requestOptions),
|
||||
getUsageRecordInsightsDiagnostics({
|
||||
...globalParams,
|
||||
dimension,
|
||||
}),
|
||||
}, requestOptions),
|
||||
])
|
||||
if (requestId !== analyticsRequestId)
|
||||
return
|
||||
@@ -139,11 +142,7 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
: insights.value.diagnostics,
|
||||
}
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (requestId !== analyticsRequestId)
|
||||
return
|
||||
toast.error(errorMessage(error, '加载失败'))
|
||||
}
|
||||
catch {}
|
||||
finally {
|
||||
if (requestId === analyticsRequestId) {
|
||||
analyticsLoading.value = false
|
||||
@@ -153,13 +152,15 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
|
||||
async function loadDiagnostics() {
|
||||
const requestId = ++diagnosticRequestId
|
||||
diagnosticController?.abort()
|
||||
diagnosticController = new AbortController()
|
||||
const dimension = diagnosticDimension.value
|
||||
const params = scopedParams()
|
||||
try {
|
||||
const diagnostics = await getUsageRecordInsightsDiagnostics({
|
||||
...params,
|
||||
dimension,
|
||||
})
|
||||
}, { signal: diagnosticController.signal })
|
||||
if (requestId !== diagnosticRequestId || dimension !== diagnosticDimension.value)
|
||||
return
|
||||
insights.value = {
|
||||
@@ -167,11 +168,7 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (requestId !== diagnosticRequestId || dimension !== diagnosticDimension.value)
|
||||
return
|
||||
toast.error(errorMessage(error, '加载失败'))
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
async function refreshUsageRecords() {
|
||||
@@ -224,10 +221,13 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
})
|
||||
|
||||
watch(options.active, (active) => {
|
||||
if (active)
|
||||
if (active) {
|
||||
void reloadLatestTable()
|
||||
else
|
||||
}
|
||||
else {
|
||||
tableRequestId += 1
|
||||
tableController?.abort()
|
||||
}
|
||||
})
|
||||
|
||||
watchDebounced(
|
||||
@@ -244,6 +244,9 @@ export function useUsageRecordsTable(options: UseUsageRecordsTableOptions) {
|
||||
tableRequestId += 1
|
||||
analyticsRequestId += 1
|
||||
diagnosticRequestId += 1
|
||||
tableController?.abort()
|
||||
analyticsController?.abort()
|
||||
diagnosticController?.abort()
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user