mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 新增 Anyrouter/Cubence/YesCode 架构及余额监控增强
- 新增三个 provider 架构:Anyrouter、Cubence、YesCode - 前端新增对应的认证配置模板 - 余额监控增强:支持窗口限额显示(进度条+倒计时)、签到状态、错误信息展示 - 架构基类新增 prepare_verify_config 异步预处理方法 - API 返回 ops_architecture_id 字段用于前端展示
This commit is contained in:
@@ -311,6 +311,7 @@ export interface ProviderWithEndpointsSummary {
|
||||
api_formats: string[]
|
||||
endpoint_health_details: EndpointHealthDetail[]
|
||||
ops_configured: boolean // 是否配置了扩展操作(余额监控等)
|
||||
ops_architecture_id?: string // 扩展操作使用的架构 ID(如 cubence, anyrouter)
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
@@ -83,7 +83,11 @@ export interface BalanceInfo {
|
||||
total_available: number | null
|
||||
expires_at: string | null
|
||||
currency: string
|
||||
extra: Record<string, any>
|
||||
extra: Record<string, any> & {
|
||||
// Anyrouter 签到信息
|
||||
checkin_success?: boolean | null // true=成功, false=失败, null=已签到/跳过
|
||||
checkin_message?: string
|
||||
}
|
||||
}
|
||||
|
||||
/** 签到信息 */
|
||||
|
||||
84
frontend/src/features/providers/auth-templates/anyrouter.ts
Normal file
84
frontend/src/features/providers/auth-templates/anyrouter.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Anyrouter 认证模板
|
||||
*
|
||||
* 适用于 Anyrouter 中转站:
|
||||
* - 使用 Cookie 认证(session)
|
||||
* - 自动处理 acw_sc__v2 反爬 Cookie(后端处理)
|
||||
* - quota 单位是 1/500000 美元
|
||||
*/
|
||||
|
||||
import type { AuthTemplate, AuthTemplateFieldGroup } from './types'
|
||||
import type { SaveConfigRequest } from '@/api/providerOps'
|
||||
|
||||
export const anyrouterTemplate: AuthTemplate = {
|
||||
id: 'anyrouter',
|
||||
name: 'Anyrouter',
|
||||
description: '适用于 Anyrouter 中转站,使用 Cookie 认证',
|
||||
|
||||
getFields(providerWebsite?: string): AuthTemplateFieldGroup[] {
|
||||
return [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
key: 'base_url',
|
||||
label: '站点地址',
|
||||
type: 'text',
|
||||
placeholder: providerWebsite || 'https://anyrouter.top',
|
||||
required: !providerWebsite,
|
||||
helpText: '通常为 https://anyrouter.top',
|
||||
},
|
||||
{
|
||||
key: 'session_cookie',
|
||||
label: 'Cookie',
|
||||
type: 'password',
|
||||
placeholder: 'session=MTc2ODc4...; acw_sc__v2=...',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
helpText: '从浏览器开发者工具复制完整 Cookie,或仅填写 session 值',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
buildRequest(formData: Record<string, any>, providerWebsite?: string): SaveConfigRequest {
|
||||
const baseUrl = formData.base_url || providerWebsite || ''
|
||||
|
||||
return {
|
||||
architecture_id: 'anyrouter',
|
||||
base_url: baseUrl,
|
||||
connector: {
|
||||
auth_type: 'cookie',
|
||||
config: {},
|
||||
credentials: {
|
||||
session_cookie: formData.session_cookie,
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
schedule: {},
|
||||
}
|
||||
},
|
||||
|
||||
parseConfig(config: any): Record<string, any> {
|
||||
return {
|
||||
base_url: config?.base_url || '',
|
||||
session_cookie: config?.connector?.credentials?.session_cookie || '',
|
||||
}
|
||||
},
|
||||
|
||||
validate(formData: Record<string, any>): string | null {
|
||||
if (!formData.session_cookie?.trim()) {
|
||||
return '请填写 Session Cookie'
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
formatQuota(quota: number): string {
|
||||
// Anyrouter 的 quota 单位是 1/500000 美元
|
||||
const usd = quota / 500000
|
||||
if (usd >= 1) {
|
||||
return `$${usd.toFixed(2)}`
|
||||
}
|
||||
return `$${usd.toFixed(4)}`
|
||||
},
|
||||
}
|
||||
157
frontend/src/features/providers/auth-templates/cubence.ts
Normal file
157
frontend/src/features/providers/auth-templates/cubence.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Cubence 认证模板
|
||||
*
|
||||
* 适用于 Cubence 中转站:
|
||||
* - 使用 Cookie 认证(token JWT)
|
||||
* - 余额单位直接是美元
|
||||
* - 支持窗口限额查询(5小时/每周)
|
||||
*/
|
||||
|
||||
import type { AuthTemplate, AuthTemplateFieldGroup, BalanceExtraItem } from './types'
|
||||
import type { SaveConfigRequest } from '@/api/providerOps'
|
||||
|
||||
/**
|
||||
* 格式化窗口限额显示(百分比格式)
|
||||
* 单位是美元(原始值除以 1000000)
|
||||
*/
|
||||
function formatWindowLimit(limit: {
|
||||
limit?: number
|
||||
used?: number
|
||||
remaining?: number
|
||||
resets_at?: number
|
||||
}): { text: string; percent: number } {
|
||||
if (!limit || limit.remaining === undefined || limit.limit === undefined || limit.limit === 0) {
|
||||
return { text: '-', percent: 0 }
|
||||
}
|
||||
|
||||
// 计算剩余百分比
|
||||
const percent = Math.round((limit.remaining / limit.limit) * 100)
|
||||
|
||||
return { text: `${percent}%`, percent }
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化窗口限额详细信息(用于 tooltip)
|
||||
*/
|
||||
function formatWindowLimitDetail(limit: {
|
||||
limit?: number
|
||||
used?: number
|
||||
remaining?: number
|
||||
resets_at?: number
|
||||
}): string {
|
||||
if (!limit || limit.remaining === undefined || limit.limit === undefined) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 转换为美元(原始值除以 1000000)
|
||||
const remaining = (limit.remaining / 1000000).toFixed(2)
|
||||
const total = (limit.limit / 1000000).toFixed(2)
|
||||
|
||||
return `$${remaining} / $${total}`
|
||||
}
|
||||
|
||||
export const cubenceTemplate: AuthTemplate = {
|
||||
id: 'cubence',
|
||||
name: 'Cubence',
|
||||
description: '适用于 Cubence 中转站,使用 Cookie 认证',
|
||||
|
||||
getFields(providerWebsite?: string): AuthTemplateFieldGroup[] {
|
||||
return [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
key: 'base_url',
|
||||
label: '站点地址',
|
||||
type: 'text',
|
||||
placeholder: providerWebsite || 'https://cubence.com',
|
||||
required: !providerWebsite,
|
||||
helpText: '通常为 https://cubence.com',
|
||||
},
|
||||
{
|
||||
key: 'token_cookie',
|
||||
label: 'Token Cookie',
|
||||
type: 'password',
|
||||
placeholder: 'token=eyJhbGciOiJI...',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
helpText: '从浏览器开发者工具复制 Cookie 中的 token 值(JWT 格式)',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
buildRequest(formData: Record<string, any>, providerWebsite?: string): SaveConfigRequest {
|
||||
const baseUrl = formData.base_url || providerWebsite || ''
|
||||
|
||||
return {
|
||||
architecture_id: 'cubence',
|
||||
base_url: baseUrl,
|
||||
connector: {
|
||||
auth_type: 'cookie',
|
||||
config: {},
|
||||
credentials: {
|
||||
token_cookie: formData.token_cookie,
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
schedule: {},
|
||||
}
|
||||
},
|
||||
|
||||
parseConfig(config: any): Record<string, any> {
|
||||
return {
|
||||
base_url: config?.base_url || '',
|
||||
token_cookie: config?.connector?.credentials?.token_cookie || '',
|
||||
}
|
||||
},
|
||||
|
||||
validate(formData: Record<string, any>): string | null {
|
||||
if (!formData.token_cookie?.trim()) {
|
||||
return '请填写 Token Cookie'
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
formatQuota(quota: number): string {
|
||||
// Cubence 的余额单位直接是美元
|
||||
if (quota >= 1) {
|
||||
return `$${quota.toFixed(2)}`
|
||||
}
|
||||
return `$${quota.toFixed(4)}`
|
||||
},
|
||||
|
||||
formatBalanceExtra(extra: Record<string, any>): BalanceExtraItem[] {
|
||||
const items: BalanceExtraItem[] = []
|
||||
|
||||
// 5小时窗口限额
|
||||
if (extra.five_hour_limit) {
|
||||
const limit = extra.five_hour_limit
|
||||
const detail = formatWindowLimitDetail(limit)
|
||||
const { text, percent } = formatWindowLimit(limit)
|
||||
items.push({
|
||||
label: '5h',
|
||||
value: text,
|
||||
percent,
|
||||
resetsAt: limit.resets_at,
|
||||
tooltip: detail || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// 每周窗口限额
|
||||
if (extra.weekly_limit) {
|
||||
const limit = extra.weekly_limit
|
||||
const detail = formatWindowLimitDetail(limit)
|
||||
const { text, percent } = formatWindowLimit(limit)
|
||||
items.push({
|
||||
label: '周',
|
||||
value: text,
|
||||
percent,
|
||||
resetsAt: limit.resets_at,
|
||||
tooltip: detail || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
},
|
||||
}
|
||||
@@ -22,12 +22,15 @@
|
||||
*/
|
||||
|
||||
import type { AuthTemplate, AuthTemplateRegistry } from './types'
|
||||
import { anyrouterTemplate } from './anyrouter'
|
||||
import { cubenceTemplate } from './cubence'
|
||||
import { newApiTemplate } from './new-api'
|
||||
import { yescodeTemplate } from './yescode'
|
||||
|
||||
// ==================== 模板注册 ====================
|
||||
// 在这里添加新模板
|
||||
|
||||
const templates: AuthTemplate[] = [newApiTemplate]
|
||||
const templates: AuthTemplate[] = [newApiTemplate, anyrouterTemplate, cubenceTemplate, yescodeTemplate]
|
||||
|
||||
// ==================== 注册表实现 ====================
|
||||
|
||||
@@ -63,4 +66,7 @@ export const authTemplateRegistry: AuthTemplateRegistry = {
|
||||
// ==================== 导出 ====================
|
||||
|
||||
export * from './types'
|
||||
export { anyrouterTemplate } from './anyrouter'
|
||||
export { cubenceTemplate } from './cubence'
|
||||
export { newApiTemplate } from './new-api'
|
||||
export { yescodeTemplate } from './yescode'
|
||||
|
||||
@@ -103,6 +103,29 @@ export interface AuthTemplate {
|
||||
* @param quota quota 值
|
||||
*/
|
||||
formatQuota?(quota: number): string
|
||||
|
||||
/**
|
||||
* 格式化余额 extra 信息(如窗口限额等)
|
||||
* 返回一个数组,每个元素包含 label 和 value
|
||||
* @param extra 余额 extra 字段
|
||||
*/
|
||||
formatBalanceExtra?(extra: Record<string, any>): BalanceExtraItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 余额附加信息项
|
||||
*/
|
||||
export interface BalanceExtraItem {
|
||||
/** 显示标签 */
|
||||
label: string
|
||||
/** 显示值 */
|
||||
value: string
|
||||
/** 百分比数值 (0-100),用于进度条显示 */
|
||||
percent?: number
|
||||
/** 重置时间戳(Unix 秒),用于倒计时显示 */
|
||||
resetsAt?: number
|
||||
/** 可选的提示文本 */
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
135
frontend/src/features/providers/auth-templates/yescode.ts
Normal file
135
frontend/src/features/providers/auth-templates/yescode.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* YesCode 认证模板
|
||||
*
|
||||
* 适用于 YesCode 中转站:
|
||||
* - 使用 Cookie 认证(yescode_auth JWT + yescode_csrf)
|
||||
* - 余额单位直接是美元
|
||||
* - subscription_balance: 套餐每日额度
|
||||
* - weekly_limit: 周限额(用完后套餐额度也无法使用)
|
||||
*/
|
||||
|
||||
import type { AuthTemplate, AuthTemplateFieldGroup, BalanceExtraItem } from './types'
|
||||
import type { SaveConfigRequest } from '@/api/providerOps'
|
||||
|
||||
/**
|
||||
* 格式化限额显示(百分比格式)
|
||||
*/
|
||||
function formatLimitPercent(remaining: number, limit: number): { text: string; percent: number } {
|
||||
if (!limit || limit === 0) {
|
||||
return { text: '-', percent: 0 }
|
||||
}
|
||||
|
||||
const percent = Math.round((remaining / limit) * 100)
|
||||
return { text: `${percent}%`, percent }
|
||||
}
|
||||
|
||||
export const yescodeTemplate: AuthTemplate = {
|
||||
id: 'yescode',
|
||||
name: 'YesCode',
|
||||
description: '适用于 YesCode 中转站,使用 Cookie 认证',
|
||||
|
||||
getFields(providerWebsite?: string): AuthTemplateFieldGroup[] {
|
||||
return [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
key: 'base_url',
|
||||
label: '站点地址',
|
||||
type: 'text',
|
||||
placeholder: providerWebsite || 'https://co.yes.vg',
|
||||
required: !providerWebsite,
|
||||
helpText: '通常为 https://co.yes.vg',
|
||||
},
|
||||
{
|
||||
key: 'auth_cookie',
|
||||
label: 'Auth Cookie',
|
||||
type: 'password',
|
||||
placeholder: 'yescode_auth=eyJhbGciOiJI...; yescode_csrf=...',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
helpText: '从浏览器开发者工具复制 Cookie(包含 yescode_auth 和 yescode_csrf)',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
buildRequest(formData: Record<string, any>, providerWebsite?: string): SaveConfigRequest {
|
||||
const baseUrl = formData.base_url || providerWebsite || ''
|
||||
|
||||
return {
|
||||
architecture_id: 'yescode',
|
||||
base_url: baseUrl,
|
||||
connector: {
|
||||
auth_type: 'cookie',
|
||||
config: {},
|
||||
credentials: {
|
||||
auth_cookie: formData.auth_cookie,
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
schedule: {},
|
||||
}
|
||||
},
|
||||
|
||||
parseConfig(config: any): Record<string, any> {
|
||||
return {
|
||||
base_url: config?.base_url || '',
|
||||
auth_cookie: config?.connector?.credentials?.auth_cookie || '',
|
||||
}
|
||||
},
|
||||
|
||||
validate(formData: Record<string, any>): string | null {
|
||||
if (!formData.auth_cookie?.trim()) {
|
||||
return '请填写 Auth Cookie'
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
formatQuota(quota: number): string {
|
||||
// YesCode 的余额单位直接是美元
|
||||
if (quota >= 1) {
|
||||
return `$${quota.toFixed(2)}`
|
||||
}
|
||||
return `$${quota.toFixed(4)}`
|
||||
},
|
||||
|
||||
formatBalanceExtra(extra: Record<string, any>): BalanceExtraItem[] {
|
||||
const items: BalanceExtraItem[] = []
|
||||
|
||||
const weeklyLimit = extra.weekly_limit
|
||||
const weeklySpent = extra.weekly_spent
|
||||
const dailyLimit = extra.daily_limit
|
||||
const dailySpent = extra.daily_spent
|
||||
const dailyResetsAt = extra.daily_resets_at
|
||||
const weeklyResetsAt = extra.weekly_resets_at
|
||||
|
||||
// 天限
|
||||
if (dailyLimit !== undefined && dailyLimit > 0 && dailySpent !== undefined) {
|
||||
const dailyRemaining = Math.max(0, dailyLimit - dailySpent)
|
||||
const { text, percent } = formatLimitPercent(dailyRemaining, dailyLimit)
|
||||
items.push({
|
||||
label: '天',
|
||||
value: text,
|
||||
percent,
|
||||
resetsAt: dailyResetsAt,
|
||||
tooltip: `$${dailyRemaining.toFixed(2)} / $${(dailyLimit as number).toFixed(2)}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 周限
|
||||
if (weeklyLimit !== undefined && weeklyLimit > 0 && weeklySpent !== undefined) {
|
||||
const weeklyRemaining = Math.max(0, weeklyLimit - weeklySpent)
|
||||
const { text, percent } = formatLimitPercent(weeklyRemaining, weeklyLimit)
|
||||
items.push({
|
||||
label: '周',
|
||||
value: text,
|
||||
percent,
|
||||
resetsAt: weeklyResetsAt,
|
||||
tooltip: `$${weeklyRemaining.toFixed(2)} / $${(weeklyLimit as number).toFixed(2)}`,
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
},
|
||||
}
|
||||
@@ -175,7 +175,7 @@ import {
|
||||
} from '../auth-templates'
|
||||
|
||||
// 敏感字段列表(用于验证和加载配置时的特殊处理)
|
||||
const SENSITIVE_FIELDS = ['api_key', 'password', 'session_token', 'cookie_string', 'cookies'] as const
|
||||
const SENSITIVE_FIELDS = ['api_key', 'password', 'session_token', 'session_cookie', 'token_cookie', 'auth_cookie', 'cookie_string', 'cookies'] as const
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -427,11 +427,11 @@ function loadFromConfig(config: any) {
|
||||
|
||||
hasExistingConfig.value = true
|
||||
|
||||
// 目前只支持 new_api 模板
|
||||
selectedTemplateId.value = 'new_api'
|
||||
|
||||
// 使用模板解析配置
|
||||
// 根据已保存的 architecture_id 选择对应模板,不存在则回退到 new_api
|
||||
const architectureId = config.architecture_id || 'new_api'
|
||||
selectedTemplateId.value = authTemplateRegistry.get(architectureId) ? architectureId : 'new_api'
|
||||
const template = authTemplateRegistry.get(selectedTemplateId.value)
|
||||
|
||||
if (template) {
|
||||
const parsedData = template.parseConfig(config)
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<TableHead class="w-[180px] h-11 font-medium text-foreground/80">
|
||||
提供商信息
|
||||
</TableHead>
|
||||
<TableHead class="w-[140px] h-11 font-medium text-foreground/80 text-center">
|
||||
<TableHead class="w-[100px] h-11 font-medium text-foreground/80">
|
||||
余额监控
|
||||
</TableHead>
|
||||
<TableHead class="w-[120px] h-11 font-medium text-foreground/80 text-center">
|
||||
@@ -142,15 +142,73 @@
|
||||
</a>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="py-3.5 text-center">
|
||||
<TableCell class="py-3.5">
|
||||
<!-- 显示从上游 API 查询的余额 -->
|
||||
<div
|
||||
v-if="provider.ops_configured && getProviderBalance(provider.id)"
|
||||
class="text-xs"
|
||||
class="flex items-center gap-2 text-xs"
|
||||
>
|
||||
<span class="font-semibold text-foreground/90">
|
||||
<!-- 余额文字 -->
|
||||
<span class="font-semibold text-foreground/90 min-w-[4.5rem] tabular-nums">
|
||||
{{ formatBalanceDisplay(getProviderBalance(provider.id)) }}
|
||||
</span>
|
||||
<!-- 窗口限额 + 签到状态 -->
|
||||
<div
|
||||
v-if="getProviderBalanceExtra(provider.id, provider.ops_architecture_id).length > 0 || getProviderCheckin(provider.id)"
|
||||
class="text-muted-foreground/70 space-y-0.5"
|
||||
>
|
||||
<!-- 限额(进度条 + 倒计时,每行一个) -->
|
||||
<template
|
||||
v-for="item in getProviderBalanceExtra(provider.id, provider.ops_architecture_id)"
|
||||
:key="item.label"
|
||||
>
|
||||
<div
|
||||
:title="item.tooltip"
|
||||
:class="['flex items-center gap-1', item.tooltip ? 'cursor-help' : '']"
|
||||
>
|
||||
<span class="text-[10px] text-muted-foreground/60 w-4">{{ item.label }}</span>
|
||||
<div class="w-12 h-1.5 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full"
|
||||
:class="[
|
||||
item.percent !== undefined && item.percent >= 50 ? 'bg-green-500' :
|
||||
item.percent !== undefined && item.percent >= 20 ? 'bg-amber-500' : 'bg-red-500'
|
||||
]"
|
||||
:style="{ width: `${item.percent ?? 0}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-[10px] text-muted-foreground/50 w-7 text-right tabular-nums">{{ item.value }}</span>
|
||||
<span
|
||||
v-if="item.resetsAt"
|
||||
class="text-[10px] text-muted-foreground/40 w-14 text-right tabular-nums"
|
||||
>{{ formatResetCountdown(item.resetsAt) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 签到状态 -->
|
||||
<div
|
||||
v-if="getProviderCheckin(provider.id)"
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<span
|
||||
v-if="getProviderCheckin(provider.id)?.success === true"
|
||||
class="text-[10px] text-muted-foreground/60"
|
||||
:title="getProviderCheckin(provider.id)?.message"
|
||||
>已签到</span>
|
||||
<span
|
||||
v-else-if="getProviderCheckin(provider.id)?.success === false"
|
||||
class="text-[10px] text-destructive/70"
|
||||
:title="getProviderCheckin(provider.id)?.message"
|
||||
>签到失败</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 余额查询失败时显示错误 -->
|
||||
<div
|
||||
v-else-if="provider.ops_configured && getProviderBalanceError(provider.id)"
|
||||
class="text-xs text-destructive/80"
|
||||
:title="getProviderBalanceError(provider.id)?.message"
|
||||
>
|
||||
{{ getProviderBalanceError(provider.id)?.message }}
|
||||
</div>
|
||||
<!-- 显示本地配置的月度配额 -->
|
||||
<div
|
||||
@@ -371,6 +429,25 @@
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
余额 <span class="font-semibold text-foreground/90">{{ formatBalanceDisplay(getProviderBalance(provider.id)) }}</span>
|
||||
<!-- 签到状态显示 -->
|
||||
<span
|
||||
v-if="getProviderCheckin(provider.id)?.success === true"
|
||||
class="ml-1 text-muted-foreground"
|
||||
:title="getProviderCheckin(provider.id)?.message"
|
||||
>已签到</span>
|
||||
<span
|
||||
v-else-if="getProviderCheckin(provider.id)?.success === false"
|
||||
class="ml-1 text-destructive/70"
|
||||
:title="getProviderCheckin(provider.id)?.message"
|
||||
>签到失败</span>
|
||||
</span>
|
||||
<!-- 余额查询失败时显示错误 -->
|
||||
<span
|
||||
v-else-if="provider.ops_configured && getProviderBalanceError(provider.id)"
|
||||
class="text-destructive/80"
|
||||
:title="getProviderBalanceError(provider.id)?.message"
|
||||
>
|
||||
{{ getProviderBalanceError(provider.id)?.message }}
|
||||
</span>
|
||||
<!-- 本地配额 -->
|
||||
<span
|
||||
@@ -459,7 +536,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
@@ -496,6 +573,7 @@ import {
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { batchQueryBalance, type ActionResultResponse } from '@/api/providerOps'
|
||||
import { formatBillingType } from '@/utils/format'
|
||||
import { authTemplateRegistry, type BalanceExtraItem } from '@/features/providers/auth-templates'
|
||||
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
const { confirmDanger } = useConfirm()
|
||||
@@ -658,6 +736,46 @@ function getProviderBalance(providerId: string): { available: number | null; cur
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 provider 余额查询的错误状态
|
||||
function getProviderBalanceError(providerId: string): { status: string; message: string } | null {
|
||||
const result = balanceCache.value[providerId]
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
// 认证失败或过期
|
||||
if (result.status === 'auth_failed' || result.status === 'auth_expired') {
|
||||
return {
|
||||
status: result.status,
|
||||
message: result.message || '认证失败'
|
||||
}
|
||||
}
|
||||
// 其他错误
|
||||
if (result.status !== 'success') {
|
||||
return {
|
||||
status: result.status,
|
||||
message: result.message || '查询失败'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取 provider 的签到信息(从 extra 字段)
|
||||
function getProviderCheckin(providerId: string): { success: boolean | null; message: string } | null {
|
||||
const result = balanceCache.value[providerId]
|
||||
if (!result || result.status !== 'success' || !result.data) {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, any>
|
||||
const extra = data.extra
|
||||
if (!extra || extra.checkin_success === undefined) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
success: extra.checkin_success,
|
||||
message: extra.checkin_message || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化余额显示
|
||||
function formatBalanceDisplay(balance: { available: number | null; currency: string } | null): string {
|
||||
if (!balance || balance.available == null) {
|
||||
@@ -667,6 +785,48 @@ function formatBalanceDisplay(balance: { available: number | null; currency: str
|
||||
return `${symbol}${balance.available.toFixed(2)}`
|
||||
}
|
||||
|
||||
// 格式化重置倒计时(从 Unix 时间戳)
|
||||
function formatResetCountdown(resetsAt: number): string {
|
||||
// 依赖 tickCounter 触发响应式更新
|
||||
void tickCounter.value
|
||||
|
||||
const now = Date.now() / 1000
|
||||
const diff = resetsAt - now
|
||||
|
||||
if (diff <= 0) return '即将重置'
|
||||
|
||||
const totalHours = Math.floor(diff / 3600)
|
||||
const minutes = Math.floor((diff % 3600) / 60)
|
||||
const seconds = Math.floor(diff % 60)
|
||||
|
||||
const pad = (n: number) => n.toString().padStart(2, '0')
|
||||
|
||||
if (totalHours > 0) {
|
||||
return `${totalHours}:${pad(minutes)}:${pad(seconds)}`
|
||||
}
|
||||
return `${minutes}:${pad(seconds)}`
|
||||
}
|
||||
|
||||
// 获取 provider 余额的额外信息(如窗口限额)
|
||||
function getProviderBalanceExtra(providerId: string, architectureId?: string): BalanceExtraItem[] {
|
||||
if (!architectureId) return []
|
||||
|
||||
const result = balanceCache.value[providerId]
|
||||
if (!result || result.status !== 'success' || !result.data) {
|
||||
return []
|
||||
}
|
||||
|
||||
const data = result.data as Record<string, any>
|
||||
const extra = data.extra
|
||||
if (!extra) return []
|
||||
|
||||
// 获取对应的模板
|
||||
const template = authTemplateRegistry.get(architectureId)
|
||||
if (!template?.formatBalanceExtra) return []
|
||||
|
||||
return template.formatBalanceExtra(extra)
|
||||
}
|
||||
|
||||
|
||||
// 格式化官网显示
|
||||
function formatWebsiteDisplay(url: string): string {
|
||||
@@ -844,8 +1004,22 @@ async function toggleProviderStatus(provider: ProviderWithEndpointsSummary) {
|
||||
}
|
||||
}
|
||||
|
||||
// 用于触发倒计时更新的响应式计数器
|
||||
const tickCounter = ref(0)
|
||||
let tickInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
loadProviders()
|
||||
loadPriorityMode()
|
||||
// 每秒更新一次倒计时
|
||||
tickInterval = setInterval(() => {
|
||||
tickCounter.value++
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (tickInterval) {
|
||||
clearInterval(tickInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user