mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
refactor: 前端全面替换 any 为 unknown 并统一错误处理,后端用量记录补写请求头/体
- 前端 API 层、stores、conversation 解析器、组件全面替换 any 为 unknown/具体类型 - 错误处理统一使用 parseApiError/getErrorStatus 替代 err.response?.data?.detail 模式 - 后端 handler/TaskService/UsageLifecycle/StreamTracker 链路传递 request_headers/request_body - streaming/pending 状态更新时可补写客户端和提供商的请求头及请求体 - 新增 TaskService 和 UsageService 相关测试
This commit is contained in:
@@ -190,14 +190,14 @@ function propertyToField(
|
||||
export function buildRequestFromSchema(
|
||||
schema: CredentialsSchema,
|
||||
architectureId: string,
|
||||
formData: Record<string, any>,
|
||||
formData: Record<string, unknown>,
|
||||
providerWebsite?: string,
|
||||
): SaveConfigRequest {
|
||||
const baseUrl = formData.base_url || providerWebsite || schema['x-default-base-url'] || ''
|
||||
const baseUrl = (formData.base_url as string) || providerWebsite || schema['x-default-base-url'] || ''
|
||||
const authType = schema['x-auth-type'] || 'api_key'
|
||||
|
||||
// 构建 credentials:除 base_url 和代理字段外的所有 schema 属性
|
||||
const credentials: Record<string, any> = {}
|
||||
const credentials: Record<string, unknown> = {}
|
||||
for (const key of Object.keys(schema.properties)) {
|
||||
if (key === 'base_url') continue
|
||||
const v = formData[key]
|
||||
@@ -227,18 +227,21 @@ export function buildRequestFromSchema(
|
||||
*/
|
||||
export function parseConfigFromSchema(
|
||||
schema: CredentialsSchema,
|
||||
config: any,
|
||||
): Record<string, any> {
|
||||
const proxyData = parseProxyConfig(config?.connector?.config)
|
||||
const result: Record<string, any> = {
|
||||
base_url: config?.base_url || '',
|
||||
config: Record<string, unknown> | null | undefined,
|
||||
): Record<string, unknown> {
|
||||
const connector = config?.connector as Record<string, unknown> | undefined
|
||||
const connectorConfig = connector?.config as Record<string, unknown> | undefined
|
||||
const proxyData = parseProxyConfig(connectorConfig)
|
||||
const result: Record<string, unknown> = {
|
||||
base_url: (config?.base_url as string) || '',
|
||||
...proxyData,
|
||||
}
|
||||
|
||||
// 从 credentials 中提取各 schema 属性
|
||||
const credentials = connector?.credentials as Record<string, unknown> | undefined
|
||||
for (const key of Object.keys(schema.properties)) {
|
||||
if (key === 'base_url') continue
|
||||
result[key] = config?.connector?.credentials?.[key] || ''
|
||||
result[key] = credentials?.[key] || ''
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -246,13 +249,18 @@ export function parseConfigFromSchema(
|
||||
|
||||
// ==================== 验证 ====================
|
||||
|
||||
/** 安全获取字符串值并 trim(表单字段值可能为 string 或其他类型) */
|
||||
function trimValue(v: unknown): string {
|
||||
return typeof v === 'string' ? v.trim() : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 schema 验证表单数据
|
||||
* @returns 错误消息,无错误返回 null
|
||||
*/
|
||||
export function validateFromSchema(
|
||||
schema: CredentialsSchema,
|
||||
formData: Record<string, any>,
|
||||
formData: Record<string, unknown>,
|
||||
): string | null {
|
||||
const validations = schema['x-validation']
|
||||
if (!validations) return null
|
||||
@@ -262,7 +270,7 @@ export function validateFromSchema(
|
||||
case 'required': {
|
||||
if (!rule.fields) break
|
||||
for (const field of rule.fields) {
|
||||
if (!formData[field]?.trim?.()) {
|
||||
if (!trimValue(formData[field])) {
|
||||
return rule.message
|
||||
}
|
||||
}
|
||||
@@ -270,7 +278,7 @@ export function validateFromSchema(
|
||||
}
|
||||
case 'any_required': {
|
||||
if (!rule.fields) break
|
||||
const hasAny = rule.fields.some((f) => !!formData[f]?.trim?.())
|
||||
const hasAny = rule.fields.some((f) => !!trimValue(formData[f]))
|
||||
if (!hasAny) {
|
||||
return rule.message
|
||||
}
|
||||
@@ -282,12 +290,12 @@ export function validateFromSchema(
|
||||
const thenFields = rule.then
|
||||
if (!ifField || !thenFields) break
|
||||
|
||||
const ifHasValue = !!formData[ifField]?.trim?.()
|
||||
const unlessHasValue = unlessField ? !!formData[unlessField]?.trim?.() : false
|
||||
const ifHasValue = !!trimValue(formData[ifField])
|
||||
const unlessHasValue = unlessField ? !!trimValue(formData[unlessField]) : false
|
||||
|
||||
if (ifHasValue && !unlessHasValue) {
|
||||
for (const field of thenFields) {
|
||||
if (!formData[field]?.trim?.()) {
|
||||
if (!trimValue(formData[field])) {
|
||||
return rule.message
|
||||
}
|
||||
}
|
||||
@@ -331,7 +339,7 @@ export function formatQuotaFromSchema(
|
||||
*/
|
||||
export function formatBalanceExtraFromSchema(
|
||||
schema: CredentialsSchema,
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
): BalanceExtraItem[] {
|
||||
const formats = schema['x-balance-extra-format']
|
||||
if (!formats) return []
|
||||
@@ -367,31 +375,35 @@ export function formatBalanceExtraFromSchema(
|
||||
}
|
||||
|
||||
function formatWindowLimitItem(
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
fmt: BalanceExtraFormat,
|
||||
): BalanceExtraItem | null {
|
||||
if (!fmt.source) return null
|
||||
const limit = extra[fmt.source]
|
||||
if (!limit || limit.remaining === undefined || limit.limit === undefined || limit.limit === 0) {
|
||||
const rawLimit = extra[fmt.source]
|
||||
if (!rawLimit || typeof rawLimit !== 'object') return null
|
||||
const limit = rawLimit as Record<string, unknown>
|
||||
if (limit.remaining === undefined || limit.limit === undefined || limit.limit === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const percent = Math.round((limit.remaining / limit.limit) * 100)
|
||||
const limitVal = Number(limit.limit)
|
||||
const remainingVal = Number(limit.remaining)
|
||||
const percent = Math.round((remainingVal / limitVal) * 100)
|
||||
const divisor = fmt.unit_divisor || 1
|
||||
const remaining = (limit.remaining / divisor).toFixed(2)
|
||||
const total = (limit.limit / divisor).toFixed(2)
|
||||
const remaining = (remainingVal / divisor).toFixed(2)
|
||||
const total = (limitVal / divisor).toFixed(2)
|
||||
|
||||
return {
|
||||
label: fmt.label,
|
||||
value: `${percent}%`,
|
||||
percent,
|
||||
resetsAt: limit.resets_at,
|
||||
resetsAt: typeof limit.resets_at === 'number' ? limit.resets_at : undefined,
|
||||
tooltip: `$${remaining} / $${total}`,
|
||||
}
|
||||
}
|
||||
|
||||
function formatDailyQuotaItem(
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
fmt: BalanceExtraFormat,
|
||||
): BalanceExtraItem | null {
|
||||
const limitKey = fmt.source_limit || 'daily_quota_limit'
|
||||
@@ -410,7 +422,7 @@ function formatDailyQuotaItem(
|
||||
const startDateKey = fmt.source_start_date
|
||||
if (startDateKey && extra[startDateKey]) {
|
||||
try {
|
||||
const startDate = new Date(extra[startDateKey])
|
||||
const startDate = new Date(String(extra[startDateKey]))
|
||||
const now = new Date()
|
||||
const todayReset = new Date(now)
|
||||
todayReset.setHours(startDate.getHours(), startDate.getMinutes(), startDate.getSeconds(), 0)
|
||||
@@ -432,14 +444,14 @@ function formatDailyQuotaItem(
|
||||
}
|
||||
|
||||
function formatMonthlyExpiryItem(
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
fmt: BalanceExtraFormat,
|
||||
): BalanceExtraItem | null {
|
||||
const endDateKey = fmt.source_end_date || 'effective_end_date'
|
||||
if (!extra[endDateKey]) return null
|
||||
|
||||
try {
|
||||
const endDate = new Date(extra[endDateKey])
|
||||
const endDate = new Date(String(extra[endDateKey]))
|
||||
const now = new Date()
|
||||
const daysLeft = Math.ceil((endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
const resetsAt = Math.floor(endDate.getTime() / 1000)
|
||||
@@ -457,27 +469,27 @@ function formatMonthlyExpiryItem(
|
||||
}
|
||||
|
||||
function formatWeeklySpentItem(
|
||||
extra: Record<string, any>,
|
||||
extra: Record<string, unknown>,
|
||||
fmt: BalanceExtraFormat,
|
||||
): BalanceExtraItem | null {
|
||||
const limitKey = fmt.source_limit || 'weekly_limit'
|
||||
const spentKey = fmt.source_spent || 'weekly_spent'
|
||||
const resetsAtKey = fmt.source_resets_at || 'weekly_resets_at'
|
||||
|
||||
const limit = extra[limitKey]
|
||||
const spent = extra[spentKey]
|
||||
const limitNum = Number(extra[limitKey])
|
||||
const spentNum = Number(extra[spentKey])
|
||||
|
||||
if (limit === undefined || limit <= 0 || spent === undefined) return null
|
||||
if (extra[limitKey] === undefined || limitNum <= 0 || extra[spentKey] === undefined) return null
|
||||
|
||||
const remaining = Math.max(0, limit - spent)
|
||||
const percent = Math.round((remaining / limit) * 100)
|
||||
const remaining = Math.max(0, limitNum - spentNum)
|
||||
const percent = Math.round((remaining / limitNum) * 100)
|
||||
|
||||
return {
|
||||
label: fmt.label,
|
||||
value: `${percent}%`,
|
||||
percent,
|
||||
resetsAt: extra[resetsAtKey],
|
||||
tooltip: `$${remaining.toFixed(2)} / $${(limit as number).toFixed(2)}`,
|
||||
resetsAt: typeof extra[resetsAtKey] === 'number' ? extra[resetsAtKey] : undefined,
|
||||
tooltip: `$${remaining.toFixed(2)} / $${limitNum.toFixed(2)}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,8 +501,8 @@ function formatWeeklySpentItem(
|
||||
export function handleSchemaFieldChange(
|
||||
schema: CredentialsSchema,
|
||||
fieldKey: string,
|
||||
value: any,
|
||||
formData: Record<string, any>,
|
||||
value: unknown,
|
||||
formData: Record<string, unknown>,
|
||||
): void {
|
||||
const hooks = schema['x-field-hooks']
|
||||
if (!hooks) return
|
||||
@@ -499,9 +511,9 @@ export function handleSchemaFieldChange(
|
||||
if (!hook) return
|
||||
|
||||
// 目标字段为空时才填充
|
||||
if (formData[hook.target]?.trim?.()) return
|
||||
if (trimValue(formData[hook.target])) return
|
||||
|
||||
const result = executeFieldHook(hook.action, value)
|
||||
const result = executeFieldHook(hook.action, typeof value === 'string' ? value : String(value ?? ''))
|
||||
if (result) {
|
||||
formData[hook.target] = result
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export const PROXY_FIELD_GROUP: AuthTemplateFieldGroup = {
|
||||
* @param formData 表单数据
|
||||
* @returns 代理配置对象,展开到 connector.config 中
|
||||
*/
|
||||
export function buildProxyConfig(formData: Record<string, any>): { proxy_node_id?: string } {
|
||||
export function buildProxyConfig(formData: Record<string, unknown>): { proxy_node_id?: string } {
|
||||
if (!formData.proxy_enabled || !formData.proxy_node_id) {
|
||||
return {}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export function buildProxyConfig(formData: Record<string, any>): { proxy_node_id
|
||||
* @param config connector.config 对象
|
||||
* @returns 表单数据
|
||||
*/
|
||||
export function parseProxyConfig(config: any): Record<string, any> {
|
||||
export function parseProxyConfig(config: Record<string, unknown> | null | undefined): Record<string, unknown> {
|
||||
// 代理节点模式
|
||||
if (config?.proxy_node_id) {
|
||||
return {
|
||||
|
||||
@@ -113,10 +113,11 @@ import {
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { testModel } from '@/api/endpoints/providers'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
metadata: any
|
||||
metadata: Record<string, unknown> | null
|
||||
keyName: string
|
||||
providerId?: string
|
||||
keyId?: string
|
||||
@@ -138,17 +139,19 @@ const { error: showError, success: showSuccess } = useToast()
|
||||
const testingModel = ref<string | null>(null)
|
||||
|
||||
const items = computed<QuotaItem[]>(() => {
|
||||
const quotaByModel = props.metadata?.antigravity?.quota_by_model
|
||||
const antigravity = props.metadata?.antigravity
|
||||
if (!antigravity || typeof antigravity !== 'object') return []
|
||||
const quotaByModel = (antigravity as Record<string, unknown>).quota_by_model
|
||||
if (!quotaByModel || typeof quotaByModel !== 'object') return []
|
||||
|
||||
const result: QuotaItem[] = []
|
||||
for (const [model, rawInfo] of Object.entries(quotaByModel)) {
|
||||
for (const [model, rawInfo] of Object.entries(quotaByModel as Record<string, unknown>)) {
|
||||
if (!model) continue
|
||||
const info: any = rawInfo || {}
|
||||
const info = (rawInfo || {}) as Record<string, unknown>
|
||||
|
||||
let usedPercent = Number(info.used_percent)
|
||||
let usedPercent = Number(info['used_percent'])
|
||||
if (!Number.isFinite(usedPercent)) {
|
||||
const remainingFraction = Number(info.remaining_fraction)
|
||||
const remainingFraction = Number(info['remaining_fraction'])
|
||||
if (Number.isFinite(remainingFraction)) {
|
||||
usedPercent = (1 - remainingFraction) * 100
|
||||
} else {
|
||||
@@ -162,8 +165,9 @@ const items = computed<QuotaItem[]>(() => {
|
||||
const remainingPercent = Math.max(100 - usedPercent, 0)
|
||||
|
||||
let resetSeconds: number | null = null
|
||||
if (typeof info.reset_time === 'string' && info.reset_time.trim()) {
|
||||
const ts = Date.parse(info.reset_time.trim())
|
||||
const resetTime = info['reset_time']
|
||||
if (typeof resetTime === 'string' && resetTime.trim()) {
|
||||
const ts = Date.parse(resetTime.trim())
|
||||
if (!Number.isNaN(ts)) {
|
||||
const diff = Math.floor((ts - Date.now()) / 1000)
|
||||
resetSeconds = diff > 0 ? diff : 0
|
||||
@@ -203,9 +207,8 @@ async function handleTestModel(modelName: string) {
|
||||
} else {
|
||||
showError(`模型测试失败: ${result.error || '未知错误'}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err?.response?.data?.detail || err?.message || '测试请求失败'
|
||||
showError(`模型测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`模型测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingModel.value = null
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ async function handleSave() {
|
||||
try {
|
||||
await deleteModel(props.providerId, existingModel.id)
|
||||
totalSuccess++
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '移除失败'))
|
||||
}
|
||||
}
|
||||
@@ -541,7 +541,7 @@ async function handleSave() {
|
||||
try {
|
||||
await deleteModel(props.providerId, existingModel.id)
|
||||
totalSuccess++
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '移除失败'))
|
||||
}
|
||||
}
|
||||
@@ -556,7 +556,7 @@ async function handleSave() {
|
||||
if (result.errors.length > 0) {
|
||||
allErrors.push(...result.errors.map(e => e.error))
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '批量添加全局模型失败'))
|
||||
}
|
||||
}
|
||||
@@ -570,7 +570,7 @@ async function handleSave() {
|
||||
if (result.errors.length > 0) {
|
||||
allErrors.push(...result.errors.map(e => e.error))
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
allErrors.push(parseApiError(err, '导入上游模型失败'))
|
||||
}
|
||||
}
|
||||
@@ -585,7 +585,7 @@ async function handleSave() {
|
||||
|
||||
emit('changed')
|
||||
emit('update:open', false)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
// 即使出错,如果已执行过操作,也通知父组件刷新数据
|
||||
if (hasAnyOperation) {
|
||||
@@ -650,7 +650,7 @@ async function loadGlobalModels() {
|
||||
loadingGlobalModels.value = true
|
||||
const response = await getGlobalModels({ limit: 1000 })
|
||||
allGlobalModels.value = response.models
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载全局模型失败'), '错误')
|
||||
} finally {
|
||||
loadingGlobalModels.value = false
|
||||
@@ -661,7 +661,7 @@ async function loadGlobalModels() {
|
||||
async function loadExistingModels() {
|
||||
try {
|
||||
existingModels.value = await getProviderModels(props.providerId)
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载已关联模型失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,6 +783,7 @@ import {
|
||||
} from '@/components/ui'
|
||||
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio, CheckCircle, Save, Filter, HelpCircle } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
import {
|
||||
@@ -1000,8 +1001,7 @@ function prepareValueForJsonParse(raw: string): string {
|
||||
}
|
||||
|
||||
// 递归还原: 将 sentinel 字符串还原为 {{$original}}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function restoreOriginalPlaceholder(value: any): any {
|
||||
function restoreOriginalPlaceholder(value: unknown): unknown {
|
||||
if (typeof value === 'string') {
|
||||
if (value === ORIGINAL_SENTINEL) return ORIGINAL_PLACEHOLDER
|
||||
if (value.includes(ORIGINAL_SENTINEL)) {
|
||||
@@ -1011,8 +1011,8 @@ function restoreOriginalPlaceholder(value: any): any {
|
||||
}
|
||||
if (Array.isArray(value)) return value.map(restoreOriginalPlaceholder)
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const result: Record<string, any> = {}
|
||||
for (const [k, v] of Object.entries(value)) result[k] = restoreOriginalPlaceholder(v)
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) result[k] = restoreOriginalPlaceholder(v)
|
||||
return result
|
||||
}
|
||||
return value
|
||||
@@ -1045,7 +1045,7 @@ function parseBodyRulePathParts(path: string): string[] | null {
|
||||
return parts
|
||||
}
|
||||
|
||||
function initBodyRuleSetValueForEditor(value: any): { value: string } {
|
||||
function initBodyRuleSetValueForEditor(value: unknown): { value: string } {
|
||||
if (value === undefined) return { value: '' }
|
||||
|
||||
// 所有值都用 JSON 格式回显
|
||||
@@ -1110,7 +1110,7 @@ function isCodexUrl(baseUrl: string): boolean {
|
||||
// 读取端点的上游流式策略(endpoint.config.upstream_stream_policy)
|
||||
function getEndpointUpstreamStreamPolicy(endpoint: ProviderEndpoint): string {
|
||||
const cfg = endpoint.config || {}
|
||||
const raw = (cfg.upstream_stream_policy ?? cfg.upstreamStreamPolicy ?? cfg.upstream_stream) as any
|
||||
const raw = (cfg.upstream_stream_policy ?? cfg.upstreamStreamPolicy ?? cfg.upstream_stream) as unknown
|
||||
if (raw === null || raw === undefined) return 'auto'
|
||||
if (typeof raw === 'boolean') return raw ? 'force_stream' : 'force_non_stream'
|
||||
const s = String(raw).trim().toLowerCase()
|
||||
@@ -1509,7 +1509,7 @@ function validateBodySetValue(rule: EditableBodyRule): string | null {
|
||||
if (!raw) return '值不能为空'
|
||||
try {
|
||||
JSON.parse(prepareValueForJsonParse(raw))
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
return `JSON 格式错误:${msg}`
|
||||
}
|
||||
@@ -1559,7 +1559,7 @@ function getRegexPatternValidationTip(rule: EditableBodyRule): string {
|
||||
new RegExp(rule.pattern.trim())
|
||||
// 正则有效但 flags 无效
|
||||
return '无效的 flags(仅允许 i/m/s)'
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
return err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
}
|
||||
@@ -1576,7 +1576,7 @@ function getBodySetValueValidationTip(rule: EditableBodyRule): string {
|
||||
try {
|
||||
JSON.parse(prepareValueForJsonParse(rule.value.trim()))
|
||||
return ''
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
return err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
}
|
||||
@@ -1729,7 +1729,7 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
||||
return { path: rule.conditionPath.trim(), op }
|
||||
}
|
||||
const raw = rule.conditionValue.trim()
|
||||
let val: any = raw
|
||||
let val: unknown = raw
|
||||
try { val = JSON.parse(raw) } catch { /* 保留原字符串 */ }
|
||||
return { path: rule.conditionPath.trim(), op, value: val }
|
||||
}
|
||||
@@ -1737,7 +1737,7 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
||||
for (const rule of rules) {
|
||||
const condition = buildCondition(rule)
|
||||
if (rule.action === 'set' && rule.path.trim()) {
|
||||
let value: any = rule.value
|
||||
let value: unknown = rule.value
|
||||
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
||||
result.push({ action: 'set', path: rule.path.trim(), value, ...(condition ? { condition } : {}) })
|
||||
} else if (rule.action === 'drop' && rule.path.trim()) {
|
||||
@@ -1745,7 +1745,7 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
|
||||
} else if (rule.action === 'rename' && rule.from.trim() && rule.to.trim()) {
|
||||
result.push({ action: 'rename', from: rule.from.trim(), to: rule.to.trim(), ...(condition ? { condition } : {}) })
|
||||
} else if ((rule.action === 'insert' || rule.action === 'append') && rule.path.trim()) {
|
||||
let value: any = rule.value
|
||||
let value: unknown = rule.value
|
||||
try { value = restoreOriginalPlaceholder(JSON.parse(prepareValueForJsonParse(rule.value.trim()))) } catch { value = rule.value }
|
||||
const indexStr = rule.index.trim()
|
||||
if (indexStr === '') {
|
||||
@@ -1804,7 +1804,7 @@ function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
|
||||
if (!rule.pattern.trim()) return `${prefix}正则表达式不能为空`
|
||||
try {
|
||||
new RegExp(rule.pattern.trim())
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
return `${prefix}正则表达式无效:${err instanceof Error ? err.message : String(err)}`
|
||||
}
|
||||
const flags = rule.flags.trim()
|
||||
@@ -1984,7 +1984,7 @@ async function saveEndpoint(endpoint: ProviderEndpoint) {
|
||||
savingEndpointId.value = endpoint.id
|
||||
try {
|
||||
// 仅提交变更字段,避免 fixed provider 因 base_url/custom_path 被锁定而更新失败
|
||||
const payload: Record<string, any> = {}
|
||||
const payload: Record<string, unknown> = {}
|
||||
|
||||
if (!isFixedProvider.value) {
|
||||
if (state.url !== endpoint.base_url) payload.base_url = state.url
|
||||
@@ -2001,8 +2001,8 @@ async function saveEndpoint(endpoint: ProviderEndpoint) {
|
||||
await updateEndpoint(endpoint.id, payload)
|
||||
success('端点已更新')
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '更新失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '更新失败'), '错误')
|
||||
} finally {
|
||||
savingEndpointId.value = null
|
||||
}
|
||||
@@ -2020,8 +2020,8 @@ async function handleToggleFormatConversion(endpoint: ProviderEndpoint) {
|
||||
})
|
||||
success(newEnabled ? '已启用格式转换' : '已关闭格式转换')
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
} finally {
|
||||
togglingFormatEndpointId.value = null
|
||||
}
|
||||
@@ -2070,7 +2070,7 @@ async function handleCycleUpstreamStream(endpoint: ProviderEndpoint) {
|
||||
|
||||
savingEndpointId.value = endpoint.id
|
||||
try {
|
||||
const merged: Record<string, any> = { ...(endpoint.config || {}) }
|
||||
const merged: Record<string, unknown> = { ...(endpoint.config || {}) }
|
||||
// 清理旧的 key
|
||||
delete merged.upstream_stream_policy
|
||||
delete merged.upstreamStreamPolicy
|
||||
@@ -2091,8 +2091,8 @@ async function handleCycleUpstreamStream(endpoint: ProviderEndpoint) {
|
||||
|
||||
success(`已切换为${nextLabel}`)
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
} finally {
|
||||
savingEndpointId.value = null
|
||||
}
|
||||
@@ -2122,8 +2122,8 @@ async function handleAddEndpoint() {
|
||||
// 重置表单,保留 URL
|
||||
newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
|
||||
emit('endpointCreated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '添加失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '添加失败'), '错误')
|
||||
} finally {
|
||||
addingEndpoint.value = false
|
||||
}
|
||||
@@ -2137,8 +2137,8 @@ async function handleToggleEndpoint(endpoint: ProviderEndpoint) {
|
||||
await updateEndpoint(endpoint.id, { is_active: newStatus })
|
||||
success(newStatus ? '端点已启用' : '端点已停用')
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '操作失败'), '错误')
|
||||
} finally {
|
||||
togglingEndpointId.value = null
|
||||
}
|
||||
@@ -2162,8 +2162,8 @@ async function confirmDeleteEndpoint() {
|
||||
await deleteEndpoint(endpoint.id)
|
||||
success(`已删除 ${formatApiFormat(endpoint.api_format)} 端点`)
|
||||
emit('endpointUpdated')
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || '删除失败', '错误')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '删除失败'), '错误')
|
||||
} finally {
|
||||
deletingEndpointId.value = null
|
||||
endpointToDelete.value = null
|
||||
|
||||
@@ -142,6 +142,7 @@ import EndpointHealthTimeline from './EndpointHealthTimeline.vue'
|
||||
import { getEndpointStatusMonitor, getPublicEndpointStatusMonitor } from '@/api/endpoints/health'
|
||||
import type { EndpointStatusMonitor, PublicEndpointStatusMonitor } from '@/api/endpoints/types'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -176,8 +177,8 @@ async function loadMonitors() {
|
||||
const data = await getPublicEndpointStatusMonitor(params)
|
||||
monitors.value = data.formats || []
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载健康监控数据失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载健康监控数据失败'), '错误')
|
||||
} finally {
|
||||
loadingMonitors.value = false
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Checkbox from '@/components/ui/checkbox.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseUpstreamModelError } from '@/utils/errorParser'
|
||||
import { parseApiError, parseUpstreamModelError } from '@/utils/errorParser'
|
||||
import {
|
||||
importModelsFromUpstream,
|
||||
getProviderModels,
|
||||
@@ -305,9 +305,8 @@ async function fetchUpstreamModels() {
|
||||
// 上游返回空列表但无错误
|
||||
hasQueried.value = true
|
||||
}
|
||||
} catch (err: any) {
|
||||
const rawError = err.response?.data?.detail || err.message || '获取上游模型失败'
|
||||
errorMessage.value = parseUpstreamModelError(rawError)
|
||||
} catch (err: unknown) {
|
||||
errorMessage.value = parseUpstreamModelError(parseApiError(err, '获取上游模型失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -378,8 +377,8 @@ async function handleImport() {
|
||||
const errorMsg = response.errors?.[0]?.error || '导入失败'
|
||||
showError(errorMsg, '导入失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '导入失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '导入失败'), '错误')
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
|
||||
@@ -796,7 +796,7 @@ async function handleSave() {
|
||||
success('模型权限已更新', '成功')
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
} finally {
|
||||
saving.value = false
|
||||
|
||||
@@ -563,7 +563,7 @@ function parsePatternText(text: string): string[] {
|
||||
}
|
||||
|
||||
// 解析 Service Account JSON 文本
|
||||
function parseAuthConfig(): Record<string, any> | null {
|
||||
function parseAuthConfig(): Record<string, unknown> | null {
|
||||
if (form.value.auth_type !== 'vertex_ai') return null
|
||||
const text = form.value.auth_config_text.trim()
|
||||
if (!text) return null
|
||||
@@ -699,7 +699,7 @@ async function handleSave() {
|
||||
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '保存密钥失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
|
||||
@@ -153,6 +153,7 @@ import { ref, watch } from 'vue'
|
||||
import { Tag, Plus, X, Loader2, GripVertical, Info } from 'lucide-vue-next'
|
||||
import { Dialog, Button, Input, Label } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import type { Model, ProviderModelAlias } from '@/api/endpoints'
|
||||
|
||||
@@ -262,7 +263,7 @@ function handleDrop(targetIndex: number) {
|
||||
items.forEach(alias => {
|
||||
// 找到这个映射在原数组中的索引
|
||||
const originalIdx = aliases.value.findIndex(a => a === alias)
|
||||
const originalPriority = originalIdx >= 0 ? originalPriorityMap.get(originalIdx)! : alias.priority
|
||||
const originalPriority = originalIdx >= 0 ? (originalPriorityMap.get(originalIdx) ?? alias.priority) : alias.priority
|
||||
|
||||
if (alias === draggedItem) {
|
||||
// 被拖动的映射是独立的新组,获得当前优先级
|
||||
@@ -271,7 +272,7 @@ function handleDrop(targetIndex: number) {
|
||||
} else {
|
||||
if (groupNewPriority.has(originalPriority)) {
|
||||
// 这个组已经分配过优先级,使用相同的值
|
||||
alias.priority = groupNewPriority.get(originalPriority)!
|
||||
alias.priority = groupNewPriority.get(originalPriority) ?? currentPriority
|
||||
} else {
|
||||
// 这个组第一次出现,分配新优先级
|
||||
groupNewPriority.set(originalPriority, currentPriority)
|
||||
@@ -325,8 +326,8 @@ async function handleSubmit() {
|
||||
showSuccess('映射配置已保存')
|
||||
emit('update:open', false)
|
||||
emit('saved')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '保存失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import {
|
||||
type Model,
|
||||
type ProviderModelAlias,
|
||||
@@ -461,8 +462,8 @@ async function fetchUpstreamModels() {
|
||||
if (result.error) {
|
||||
showError(result.error, '获取上游模型失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '获取上游模型列表失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '获取上游模型列表失败'), '错误')
|
||||
} finally {
|
||||
loadingModels.value = false
|
||||
fetchingUpstreamModels.value = false
|
||||
@@ -579,8 +580,8 @@ async function handleSubmit() {
|
||||
showSuccess(props.editingGroup ? '映射组已更新' : '映射已添加')
|
||||
emit('update:open', false)
|
||||
emit('saved')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -92,34 +92,72 @@
|
||||
>
|
||||
<!-- Kiro: 设备授权模式 -->
|
||||
<template v-if="isKiroProvider">
|
||||
<!-- 初始状态:输入 Start URL / Region + 开始 -->
|
||||
<!-- 初始状态:选择授权类型 + 开始 -->
|
||||
<div
|
||||
v-if="!device.session_id && !device.starting"
|
||||
class="space-y-3"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Start URL</label>
|
||||
<input
|
||||
v-model="device.start_url"
|
||||
type="text"
|
||||
placeholder="https://view.awsapps.com/start"
|
||||
class="w-full h-8 px-2 text-xs rounded-md border border-border bg-background font-mono"
|
||||
spellcheck="false"
|
||||
<!-- Builder ID / Identity Center 切换 -->
|
||||
<div class="grid grid-cols-2 gap-1.5">
|
||||
<button
|
||||
v-for="opt in ([
|
||||
{ key: 'builder_id', label: 'Builder ID' },
|
||||
{ key: 'identity_center', label: 'Identity Center' },
|
||||
] as const)"
|
||||
:key="opt.key"
|
||||
class="h-8 text-xs font-medium rounded-md border transition-colors"
|
||||
:class="device.auth_type === opt.key
|
||||
? 'border-primary bg-primary/5 text-foreground'
|
||||
: 'border-border text-muted-foreground hover:text-foreground hover:border-foreground/20'"
|
||||
@click="device.auth_type = opt.key"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Region</label>
|
||||
<input
|
||||
v-model="device.region"
|
||||
type="text"
|
||||
placeholder="us-east-1"
|
||||
class="w-full h-8 px-2 text-xs rounded-md border border-border bg-background font-mono"
|
||||
spellcheck="false"
|
||||
|
||||
<!-- grid 叠放保持高度稳定 -->
|
||||
<div class="grid [&>*]:col-start-1 [&>*]:row-start-1">
|
||||
<!-- Builder ID: 说明文字 -->
|
||||
<div
|
||||
class="flex items-center justify-center transition-opacity duration-150"
|
||||
:class="device.auth_type === 'builder_id' ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
使用个人 AWS Builder ID 进行设备授权,无需额外配置。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Identity Center: Start URL + Region -->
|
||||
<div
|
||||
class="space-y-3 transition-opacity duration-150"
|
||||
:class="device.auth_type === 'identity_center' ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Start URL</label>
|
||||
<input
|
||||
v-model="device.start_url"
|
||||
type="text"
|
||||
placeholder="https://your-org.awsapps.com/start"
|
||||
class="w-full h-8 px-2 text-xs rounded-md border border-border bg-background font-mono"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Region</label>
|
||||
<input
|
||||
v-model="device.region"
|
||||
type="text"
|
||||
placeholder="us-east-1"
|
||||
class="w-full h-8 px-2 text-xs rounded-md border border-border bg-background font-mono"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full"
|
||||
:disabled="!device.start_url.trim()"
|
||||
:disabled="device.auth_type === 'identity_center' && !device.start_url.trim()"
|
||||
@click="startDeviceAuth"
|
||||
>
|
||||
开始授权
|
||||
@@ -480,7 +518,10 @@ function createInitialOAuthState(): OAuthState {
|
||||
const oauth = ref<OAuthState>(createInitialOAuthState())
|
||||
|
||||
// 设备授权状态
|
||||
type DeviceAuthType = 'builder_id' | 'identity_center'
|
||||
|
||||
interface DeviceAuthState {
|
||||
auth_type: DeviceAuthType
|
||||
start_url: string
|
||||
region: string
|
||||
starting: boolean
|
||||
@@ -494,8 +535,12 @@ interface DeviceAuthState {
|
||||
error: string
|
||||
}
|
||||
|
||||
const BUILDER_ID_START_URL = 'https://view.awsapps.com/start'
|
||||
const BUILDER_ID_REGION = 'us-east-1'
|
||||
|
||||
function createInitialDeviceState(): DeviceAuthState {
|
||||
return {
|
||||
auth_type: 'builder_id',
|
||||
start_url: '',
|
||||
region: 'us-east-1',
|
||||
starting: false,
|
||||
@@ -563,8 +608,9 @@ function stopDevicePolling() {
|
||||
|
||||
function resetDevice() {
|
||||
stopDevicePolling()
|
||||
const { start_url, region } = device.value
|
||||
const { auth_type, start_url, region } = device.value
|
||||
device.value = createInitialDeviceState()
|
||||
device.value.auth_type = auth_type
|
||||
device.value.start_url = start_url
|
||||
device.value.region = region
|
||||
}
|
||||
@@ -635,7 +681,7 @@ async function initOAuth() {
|
||||
oauth.value.redirect_uri = resp.redirect_uri
|
||||
oauth.value.instructions = resp.instructions
|
||||
oauth.value.provider_type = resp.provider_type
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '初始化授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
mode.value = 'import'
|
||||
@@ -655,7 +701,7 @@ async function handleCompleteOAuth() {
|
||||
success('授权成功,账号已添加')
|
||||
emit('saved')
|
||||
handleClose()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '完成授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
@@ -699,13 +745,14 @@ function parseImportText(text: string): { refresh_token: string; name?: string }
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
const parsed: unknown = JSON.parse(trimmed)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
const refreshToken = (parsed as any).refresh_token
|
||||
const obj = parsed as Record<string, unknown>
|
||||
const refreshToken = obj.refresh_token
|
||||
if (typeof refreshToken === 'string' && refreshToken.trim()) {
|
||||
return {
|
||||
refresh_token: refreshToken.trim(),
|
||||
name: (parsed as any).name || (parsed as any).oauth_email || undefined,
|
||||
name: (typeof obj.name === 'string' ? obj.name : undefined) || (typeof obj.oauth_email === 'string' ? obj.oauth_email : undefined),
|
||||
}
|
||||
}
|
||||
return null
|
||||
@@ -789,7 +836,7 @@ async function handleImport() {
|
||||
emit('saved')
|
||||
handleClose()
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '导入失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
@@ -821,9 +868,10 @@ async function startDeviceAuth() {
|
||||
device.value.starting = true
|
||||
device.value.error = ''
|
||||
try {
|
||||
const isBuilderID = device.value.auth_type === 'builder_id'
|
||||
const resp = await startDeviceAuthorize(props.providerId, {
|
||||
start_url: device.value.start_url.trim() || undefined,
|
||||
region: device.value.region.trim() || undefined,
|
||||
start_url: isBuilderID ? BUILDER_ID_START_URL : (device.value.start_url.trim() || undefined),
|
||||
region: isBuilderID ? BUILDER_ID_REGION : (device.value.region.trim() || undefined),
|
||||
proxy_node_id: selectedProxyNodeId.value || undefined,
|
||||
})
|
||||
device.value.session_id = resp.session_id
|
||||
@@ -835,7 +883,7 @@ async function startDeviceAuth() {
|
||||
device.value.status = 'pending'
|
||||
startCountdown()
|
||||
scheduleDevicePoll()
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '发起设备授权失败')
|
||||
showError(errorMessage, '错误')
|
||||
device.value.status = 'error'
|
||||
@@ -884,7 +932,7 @@ async function pollDevice() {
|
||||
device.value.error = result.error || '授权失败'
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
// 网络错误等,继续轮询
|
||||
scheduleDevicePoll()
|
||||
}
|
||||
|
||||
@@ -361,7 +361,7 @@ async function handleSave() {
|
||||
success('账号已更新', '成功')
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = parseApiError(err, '保存失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
|
||||
@@ -432,12 +432,14 @@ import { Dialog } from '@/components/ui'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { updateProvider, updateProviderKey } from '@/api/endpoints'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { batchQueryBalance, type ActionResultResponse, type BalanceInfo } from '@/api/providerOps'
|
||||
import { API_FORMAT_SHORT } from '@/api/endpoints/types'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
interface KeyWithMeta {
|
||||
id: string
|
||||
@@ -555,7 +557,7 @@ async function loadBalances() {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[loadBalances] 加载余额数据失败:', e)
|
||||
log.warn('[loadBalances] 加载余额数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,7 +634,7 @@ async function loadKeysByFormat() {
|
||||
|
||||
// 每个格式独立管理优先级,使用后端返回的 format_priority
|
||||
const data: Record<string, KeyWithMeta[]> = {}
|
||||
for (const [format, keys] of Object.entries(response.data as Record<string, any[]>)) {
|
||||
for (const [format, keys] of Object.entries(response.data as Record<string, Record<string, unknown>[]>)) {
|
||||
// 计算该格式下的默认优先级
|
||||
let maxPriority = 0
|
||||
for (const key of keys) {
|
||||
@@ -656,8 +658,8 @@ async function loadKeysByFormat() {
|
||||
if (formats.length > 0 && !formats.includes(activeFormatTab.value)) {
|
||||
activeFormatTab.value = formats[0]
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载 Key 列表失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载 Key 列表失败'), '错误')
|
||||
} finally {
|
||||
loadingKeys.value = false
|
||||
}
|
||||
@@ -681,8 +683,8 @@ async function toggleKeyActive(format: string, key: KeyWithMeta) {
|
||||
keysByFormat.value[fmt] = sortKeysByActiveAndPriority(keysByFormat.value[fmt])
|
||||
}
|
||||
success(newStatus ? 'Key 已启用' : 'Key 已停用')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,7 +799,7 @@ function handleProviderDrop(dropIndex: number) {
|
||||
let currentPriority = 1
|
||||
|
||||
items.forEach(provider => {
|
||||
const originalPriority = originalPriorityMap.get(provider.id)!
|
||||
const originalPriority = originalPriorityMap.get(provider.id) ?? 0
|
||||
|
||||
if (provider === draggedItem) {
|
||||
// 被拖动的项单独成组
|
||||
@@ -806,7 +808,7 @@ function handleProviderDrop(dropIndex: number) {
|
||||
} else {
|
||||
if (groupNewPriority.has(originalPriority)) {
|
||||
// 同组的其他项使用相同的新优先级
|
||||
provider.provider_priority = groupNewPriority.get(originalPriority)!
|
||||
provider.provider_priority = groupNewPriority.get(originalPriority) ?? currentPriority
|
||||
} else {
|
||||
// 新组,分配新优先级
|
||||
groupNewPriority.set(originalPriority, currentPriority)
|
||||
@@ -881,7 +883,7 @@ function handleKeyDrop(format: string, dropIndex: number) {
|
||||
let currentPriority = 1
|
||||
|
||||
items.forEach(key => {
|
||||
const originalPriority = originalPriorityMap.get(key.id)!
|
||||
const originalPriority = originalPriorityMap.get(key.id) ?? 0
|
||||
|
||||
if (key === draggedItem) {
|
||||
// 被拖动的项单独成组
|
||||
@@ -890,7 +892,7 @@ function handleKeyDrop(format: string, dropIndex: number) {
|
||||
} else {
|
||||
if (groupNewPriority.has(originalPriority)) {
|
||||
// 同组的其他项使用相同的新优先级
|
||||
key.priority = groupNewPriority.get(originalPriority)!
|
||||
key.priority = groupNewPriority.get(originalPriority) ?? currentPriority
|
||||
} else {
|
||||
// 新组,分配新优先级
|
||||
groupNewPriority.set(originalPriority, currentPriority)
|
||||
@@ -959,8 +961,8 @@ async function save() {
|
||||
if (activeMainTab.value === 'provider') {
|
||||
close()
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '保存失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '保存失败'), '错误')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
@@ -306,6 +306,7 @@ import {
|
||||
deleteProviderOpsConfig,
|
||||
type ArchitectureInfo,
|
||||
} from '@/api/providerOps'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import type { AuthTemplateFieldGroup } from '../auth-templates/types'
|
||||
@@ -325,7 +326,7 @@ const props = defineProps<{
|
||||
open: boolean
|
||||
providerId: string
|
||||
providerWebsite?: string
|
||||
currentConfig?: any
|
||||
currentConfig?: Record<string, unknown> | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -373,7 +374,7 @@ const architecturesLoaded = ref(false)
|
||||
// 当前选择
|
||||
const selectedArchitectureId = ref('new_api')
|
||||
const selectedAuthType = ref('')
|
||||
const formData = ref<Record<string, any>>({})
|
||||
const formData = ref<Record<string, unknown>>({})
|
||||
|
||||
// 当前架构支持的认证方式
|
||||
const currentAuthTypes = computed(() => {
|
||||
@@ -446,7 +447,7 @@ function handleAuthTypeChange() {
|
||||
formChanged.value = true
|
||||
}
|
||||
|
||||
function handleFieldChange(fieldKey: string, value: any) {
|
||||
function handleFieldChange(fieldKey: string, value: unknown) {
|
||||
formChanged.value = true
|
||||
|
||||
// 执行 schema 定义的字段钩子
|
||||
@@ -475,9 +476,9 @@ function resetFormData() {
|
||||
}
|
||||
|
||||
// 初始化表单数据
|
||||
const data: Record<string, any> = {}
|
||||
const data: Record<string, unknown> = {}
|
||||
for (const [key, prop] of Object.entries(schema.properties)) {
|
||||
data[key] = (prop as any)['x-default-value'] ?? ''
|
||||
data[key] = (prop as Record<string, unknown>)['x-default-value'] ?? ''
|
||||
}
|
||||
// 代理相关默认值
|
||||
data.proxy_enabled = false
|
||||
@@ -581,10 +582,9 @@ async function handleVerify() {
|
||||
|
||||
showError(result.message || '验证失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
verifyStatus.value = 'error'
|
||||
const errMsg = error.response?.data?.detail || error.message || '验证失败'
|
||||
showError(errMsg)
|
||||
showError(parseApiError(error, '验证失败'))
|
||||
} finally {
|
||||
isVerifying.value = false
|
||||
}
|
||||
@@ -632,8 +632,8 @@ async function handleSave() {
|
||||
} else {
|
||||
showError(result.message || '保存失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || error.message, '保存失败')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '保存失败'), '保存失败')
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
@@ -666,14 +666,14 @@ async function handleClear() {
|
||||
} else {
|
||||
showError(result.message || '清除失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
showError(error.response?.data?.detail || error.message, '清除失败')
|
||||
} catch (error: unknown) {
|
||||
showError(parseApiError(error, '清除失败'), '清除失败')
|
||||
} finally {
|
||||
isClearing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function loadFromConfig(config: any) {
|
||||
function loadFromConfig(config: Record<string, unknown>) {
|
||||
if (!config?.connector) return
|
||||
|
||||
hasExistingConfig.value = true
|
||||
|
||||
@@ -1007,6 +1007,7 @@ import {
|
||||
ShieldX,
|
||||
Globe,
|
||||
} from 'lucide-vue-next'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
@@ -1022,7 +1023,8 @@ import {
|
||||
updateProvider,
|
||||
getProviderModels,
|
||||
getProviderMappingPreview,
|
||||
type ProviderMappingPreviewResponse
|
||||
type ProviderMappingPreviewResponse,
|
||||
type ProviderWithEndpointsSummary,
|
||||
} from '@/api/endpoints'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import {
|
||||
@@ -1074,8 +1076,8 @@ interface Props {
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'edit', provider: any): void
|
||||
(e: 'toggleStatus', provider: any): void
|
||||
(e: 'edit', provider: ProviderWithEndpointsSummary): void
|
||||
(e: 'toggleStatus', provider: ProviderWithEndpointsSummary): void
|
||||
(e: 'refresh'): void
|
||||
}>()
|
||||
|
||||
@@ -1085,7 +1087,7 @@ const { copyToClipboard } = useClipboard()
|
||||
const { tick: countdownTick, start: startCountdownTimer, stop: stopCountdownTimer } = useCountdownTimer()
|
||||
|
||||
const loading = ref(false)
|
||||
const provider = ref<any>(null)
|
||||
const provider = ref<ProviderWithEndpointsSummary | null>(null)
|
||||
const endpoints = ref<ProviderEndpointWithKeys[]>([])
|
||||
const providerKeys = ref<EndpointAPIKey[]>([]) // Provider 级别的 keys
|
||||
const providerModels = ref<Model[]>([]) // Provider 级别的 models
|
||||
@@ -1364,8 +1366,8 @@ async function copyFullKey(key: EndpointAPIKey) {
|
||||
|
||||
revealedKeys.value.set(key.id, textToCopy)
|
||||
copyToClipboard(textToCopy)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '获取密钥失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '获取密钥失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1385,8 +1387,8 @@ async function downloadRefreshToken(key: EndpointAPIKey) {
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '导出失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '导出失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,8 +1411,8 @@ async function confirmDeleteKey() {
|
||||
// 刷新端点列表及模型数据(删除 Key 触发自动解除模型关联)
|
||||
await loadEndpoints()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '删除密钥失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除密钥失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1420,8 +1422,8 @@ async function handleRecoverKey(key: EndpointAPIKey) {
|
||||
showSuccess(result.message || 'Key已完全恢复')
|
||||
await loadEndpoints()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || 'Key恢复失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, 'Key恢复失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1446,8 +1448,8 @@ async function handleRefreshOAuth(key: EndpointAPIKey) {
|
||||
// Antigravity:token 刷新后可能完成了账号激活,触发配额获取
|
||||
// (不 emit('refresh'),避免触发全局 provider 余额刷新)
|
||||
void autoRefreshQuotaInBackground()
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || 'Token 刷新失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, 'Token 刷新失败'), '错误')
|
||||
} finally {
|
||||
refreshingOAuthKeyId.value = null
|
||||
}
|
||||
@@ -1483,8 +1485,8 @@ async function handleClearOAuthInvalid(key: EndpointAPIKey) {
|
||||
keyInList.is_active = true
|
||||
}
|
||||
await loadEndpoints()
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '清除失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '清除失败'), '错误')
|
||||
} finally {
|
||||
clearingOAuthInvalidKeyId.value = null
|
||||
}
|
||||
@@ -1703,9 +1705,9 @@ async function autoRefreshQuotaInBackground() {
|
||||
} else if (!hadCachedQuota && providerType === 'antigravity') {
|
||||
showError('没有获取到配额信息(请检查账号是否已授权、project_id 是否存在)', '提示')
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
if (!hadCachedQuota && providerType === 'antigravity') {
|
||||
showError(err.response?.data?.detail || '后台刷新配额失败', '错误')
|
||||
showError(parseApiError(err, '后台刷新配额失败'), '错误')
|
||||
}
|
||||
} finally {
|
||||
refreshingQuota.value = false
|
||||
@@ -1756,8 +1758,8 @@ async function toggleKeyActive(key: EndpointAPIKey) {
|
||||
key.is_active = newStatus
|
||||
showSuccess(newStatus ? '密钥已启用' : '密钥已停用')
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
} finally {
|
||||
togglingKeyId.value = null
|
||||
}
|
||||
@@ -1768,7 +1770,7 @@ async function toggleKeyActive(key: EndpointAPIKey) {
|
||||
/** 获取 Key 当前代理节点的名称(用于显示) */
|
||||
function getKeyProxyNodeName(key: EndpointAPIKey): string | null {
|
||||
if (!key.proxy?.node_id) return null
|
||||
const node = proxyNodesStore.nodes.find(n => n.id === key.proxy!.node_id)
|
||||
const node = proxyNodesStore.nodes.find(n => n.id === key.proxy?.node_id)
|
||||
return node ? node.name : `${key.proxy.node_id.slice(0, 8) }...`
|
||||
}
|
||||
|
||||
@@ -1791,8 +1793,8 @@ async function setKeyProxy(key: EndpointAPIKey, nodeId: string) {
|
||||
proxyPopoverOpenKeyId.value = null
|
||||
showSuccess('代理节点已设置')
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '设置代理失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '设置代理失败'), '错误')
|
||||
} finally {
|
||||
savingProxyKeyId.value = null
|
||||
}
|
||||
@@ -1807,8 +1809,8 @@ async function clearKeyProxy(key: EndpointAPIKey) {
|
||||
proxyPopoverOpenKeyId.value = null
|
||||
showSuccess('已清除账号代理,将使用提供商级别代理')
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '清除代理失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '清除代理失败'), '错误')
|
||||
} finally {
|
||||
savingProxyKeyId.value = null
|
||||
}
|
||||
@@ -1911,8 +1913,8 @@ async function savePriority(key: EndpointAPIKey) {
|
||||
// 重新排序
|
||||
providerKeys.value.sort((a, b) => (a.internal_priority ?? 0) - (b.internal_priority ?? 0))
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '更新优先级失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '更新优先级失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1999,8 +2001,8 @@ async function saveMultiplier(key: EndpointAPIKey, format: string) {
|
||||
keyToUpdate.rate_multipliers = rateMultipliers
|
||||
}
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '更新倍率失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '更新倍率失败'), '错误')
|
||||
} finally {
|
||||
multiplierSaving.value = false
|
||||
}
|
||||
@@ -2082,7 +2084,7 @@ async function handleKeyDrop(event: DragEvent, targetIndex: number) {
|
||||
const newPriorityMap = new Map<string, number>()
|
||||
|
||||
items.forEach(key => {
|
||||
const originalPriority = originalPriorityMap.get(key.id)!
|
||||
const originalPriority = originalPriorityMap.get(key.id) ?? 0
|
||||
|
||||
if (key === draggedKey) {
|
||||
// 被拖动的项单独成组
|
||||
@@ -2091,7 +2093,7 @@ async function handleKeyDrop(event: DragEvent, targetIndex: number) {
|
||||
} else {
|
||||
if (groupNewPriority.has(originalPriority)) {
|
||||
// 同组的其他项使用相同的新优先级
|
||||
newPriorityMap.set(key.id, groupNewPriority.get(originalPriority)!)
|
||||
newPriorityMap.set(key.id, groupNewPriority.get(originalPriority) ?? currentPriority)
|
||||
} else {
|
||||
// 新组,分配新优先级
|
||||
groupNewPriority.set(originalPriority, currentPriority)
|
||||
@@ -2115,8 +2117,8 @@ async function handleKeyDrop(event: DragEvent, targetIndex: number) {
|
||||
showSuccess('优先级已更新')
|
||||
await loadEndpoints()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '更新优先级失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '更新优先级失败'), '错误')
|
||||
await loadEndpoints()
|
||||
}
|
||||
}
|
||||
@@ -2479,8 +2481,8 @@ async function loadProvider() {
|
||||
if (!provider.value) {
|
||||
throw new Error('Provider 不存在')
|
||||
}
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || err.message || '加载失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载失败'), '错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -2511,8 +2513,8 @@ async function loadEndpoints() {
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载端点失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载端点失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -500,7 +500,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
emit('update:modelValue', false)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
const action = isEditMode.value ? '更新' : '创建'
|
||||
showError(parseApiError(error, `${action}提供商失败`), `${action}失败`)
|
||||
} finally {
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { Loader2, Layers, SquarePen, Plus, Trash2 } from 'lucide-vue-next'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -298,7 +299,7 @@ const VIDEO_RESOLUTION_PRICE_PRESETS: Record<
|
||||
const form = ref({
|
||||
global_model_id: '',
|
||||
price_per_request: undefined as number | undefined,
|
||||
config: {} as Record<string, any>,
|
||||
config: {} as Record<string, unknown>,
|
||||
// 能力配置
|
||||
supports_vision: undefined as boolean | undefined,
|
||||
supports_function_calling: undefined as boolean | undefined,
|
||||
@@ -392,53 +393,54 @@ function resetForm() {
|
||||
availableGlobalModels.value = []
|
||||
}
|
||||
|
||||
function getNested(obj: any, path: string): any {
|
||||
function getNested(obj: Record<string, unknown>, path: string): unknown {
|
||||
if (!obj || typeof obj !== 'object') return undefined
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
let cur: any = obj
|
||||
let cur: unknown = obj
|
||||
for (const p of parts) {
|
||||
if (!cur || typeof cur !== 'object') return undefined
|
||||
cur = cur[p]
|
||||
cur = (cur as Record<string, unknown>)[p]
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
function setNested(obj: any, path: string, value: any) {
|
||||
function setNested(obj: Record<string, unknown>, path: string, value: unknown) {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
if (parts.length === 0) return
|
||||
let cur: any = obj
|
||||
let cur: Record<string, unknown> = obj
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const p = parts[i]
|
||||
if (!cur[p] || typeof cur[p] !== 'object') {
|
||||
cur[p] = {}
|
||||
}
|
||||
cur = cur[p]
|
||||
cur = cur[p] as Record<string, unknown>
|
||||
}
|
||||
cur[parts[parts.length - 1]] = value
|
||||
}
|
||||
|
||||
function deleteNested(obj: any, path: string) {
|
||||
function deleteNested(obj: Record<string, unknown>, path: string) {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
const parts = path.split('.').filter(Boolean)
|
||||
if (parts.length === 0) return
|
||||
let cur: any = obj
|
||||
let cur: Record<string, unknown> = obj
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const p = parts[i]
|
||||
if (!cur[p] || typeof cur[p] !== 'object') return
|
||||
cur = cur[p]
|
||||
cur = cur[p] as Record<string, unknown>
|
||||
}
|
||||
delete cur[parts[parts.length - 1]]
|
||||
}
|
||||
|
||||
function pruneEmptyBillingConfig(cfg: Record<string, any>) {
|
||||
function pruneEmptyBillingConfig(cfg: Record<string, unknown>) {
|
||||
const billing = cfg.billing
|
||||
if (!billing || typeof billing !== 'object') return
|
||||
const video = billing.video
|
||||
const billingObj = billing as Record<string, unknown>
|
||||
const video = billingObj.video
|
||||
if (video && typeof video === 'object' && Object.keys(video).length === 0) {
|
||||
delete billing.video
|
||||
delete billingObj.video
|
||||
}
|
||||
if (Object.keys(billing).length === 0) {
|
||||
if (Object.keys(billingObj).length === 0) {
|
||||
delete cfg.billing
|
||||
}
|
||||
}
|
||||
@@ -461,7 +463,7 @@ function normalizeResolutionKey(raw: string): string {
|
||||
return k
|
||||
}
|
||||
|
||||
function loadVideoPricingFromConfig(cfg: Record<string, any>) {
|
||||
function loadVideoPricingFromConfig(cfg: Record<string, unknown>) {
|
||||
const raw = getNested(cfg, 'billing.video.price_per_second_by_resolution')
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
// 按分辨率从低到高排序
|
||||
@@ -475,7 +477,7 @@ function loadVideoPricingFromConfig(cfg: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyVideoPricingToConfig(cfg: Record<string, any>) {
|
||||
function applyVideoPricingToConfig(cfg: Record<string, unknown>) {
|
||||
// Clean legacy keys
|
||||
deleteNested(cfg, 'billing.video.price_per_second')
|
||||
deleteNested(cfg, 'billing.video.resolution_multipliers')
|
||||
@@ -546,8 +548,8 @@ async function loadAvailableGlobalModels() {
|
||||
availableGlobalModels.value = allGlobalModels.filter(
|
||||
(gm: GlobalModelResponse) => !existingGlobalModelIds.has(gm.id)
|
||||
)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载模型列表失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载模型列表失败'), '错误')
|
||||
} finally {
|
||||
loadingGlobalModels.value = false
|
||||
}
|
||||
@@ -612,8 +614,8 @@ async function handleSubmit() {
|
||||
}
|
||||
emit('update:open', false)
|
||||
emit('saved')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || (isEditing.value ? '更新失败' : '添加失败'), '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, isEditing.value ? '更新失败' : '添加失败'), '错误')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
@@ -200,11 +200,12 @@ import {
|
||||
type ProviderModelAlias
|
||||
} from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseTestModelError } from '@/utils/errorParser'
|
||||
import { parseApiError, parseTestModelError } from '@/utils/errorParser'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
provider: ProviderWithEndpointsSummary
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -264,7 +265,7 @@ const aliasGroups = computed<AliasGroup[]>(() => {
|
||||
groupMap.set(groupKey, group)
|
||||
groups.push(group)
|
||||
}
|
||||
groupMap.get(groupKey)!.aliases.push(alias)
|
||||
groupMap.get(groupKey)?.aliases.push(alias)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,8 +286,8 @@ async function loadModels() {
|
||||
try {
|
||||
loading.value = true
|
||||
models.value = await getProviderModels(props.provider.id)
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '加载失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '加载失败'), '错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -361,8 +362,8 @@ async function confirmDelete() {
|
||||
deletingGroup.value = null
|
||||
await loadModels()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '删除失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +374,7 @@ async function onDialogSaved() {
|
||||
}
|
||||
|
||||
// 测试模型映射
|
||||
async function testMapping(group: any, mapping: any) {
|
||||
async function testMapping(group: AliasGroup, mapping: ProviderModelAlias) {
|
||||
const testingKey = `${group.model.id}-${group.apiFormatsKey}-${mapping.name}`
|
||||
testingMapping.value = testingKey
|
||||
|
||||
@@ -407,9 +408,8 @@ async function testMapping(group: any, mapping: any) {
|
||||
} else {
|
||||
showError(`映射测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`映射测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`映射测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingMapping.value = null
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="编辑映射"
|
||||
@click="editGroup(item.group!)"
|
||||
@click="item.group && editGroup(item.group)"
|
||||
>
|
||||
<Edit class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -126,7 +126,7 @@
|
||||
size="icon"
|
||||
class="h-8 w-8 hover:text-destructive"
|
||||
title="删除映射"
|
||||
@click="deleteGroup(item.group!)"
|
||||
@click="item.group && deleteGroup(item.group)"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
@@ -362,7 +362,8 @@ import {
|
||||
import { type EndpointAPIKey } from '@/api/endpoints/keys'
|
||||
import type { ProviderEndpoint } from '@/api/endpoints/types'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseTestModelError } from '@/utils/errorParser'
|
||||
import { parseApiError, parseTestModelError } from '@/utils/errorParser'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
|
||||
interface MappingItem {
|
||||
name: string
|
||||
@@ -389,7 +390,7 @@ interface CombinedMapping {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
provider: ProviderWithEndpointsSummary
|
||||
providerKeys?: EndpointAPIKey[]
|
||||
models?: Model[]
|
||||
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||
@@ -456,7 +457,7 @@ const exactMappingGroups = computed<AliasGroup[]>(() => {
|
||||
groupMap.set(groupKey, group)
|
||||
groups.push(group)
|
||||
}
|
||||
groupMap.get(groupKey)!.aliases.push(alias)
|
||||
groupMap.get(groupKey)?.aliases.push(alias)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,10 +487,11 @@ const regexMappings = computed<CombinedMapping[]>(() => {
|
||||
mappings: [],
|
||||
matchedKeys: []
|
||||
})
|
||||
result.push(modelMap.get(gm.global_model_id)!)
|
||||
result.push(modelMap.get(gm.global_model_id) as CombinedMapping)
|
||||
}
|
||||
|
||||
const mapping = modelMap.get(gm.global_model_id)!
|
||||
const mapping = modelMap.get(gm.global_model_id)
|
||||
if (!mapping) continue
|
||||
|
||||
// 添加 Key 信息
|
||||
const keyMatches: MappingItem[] = gm.matched_models.map(m => ({
|
||||
@@ -497,7 +499,7 @@ const regexMappings = computed<CombinedMapping[]>(() => {
|
||||
pattern: m.mapping_pattern
|
||||
}))
|
||||
|
||||
mapping.matchedKeys!.push({
|
||||
mapping.matchedKeys?.push({
|
||||
keyId: keyInfo.key_id,
|
||||
keyName: keyInfo.key_name,
|
||||
maskedKey: keyInfo.masked_key,
|
||||
@@ -635,8 +637,8 @@ async function confirmDelete() {
|
||||
deleteConfirmOpen.value = false
|
||||
deletingGroup.value = null
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '删除失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '删除失败'), '错误')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,9 +717,8 @@ async function testMapping(item: CombinedMapping, mapping: MappingItem, apiForma
|
||||
} else {
|
||||
showError(`映射测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`映射测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`映射测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingMapping.value = null
|
||||
}
|
||||
@@ -741,9 +742,8 @@ async function testRegexMapping(item: CombinedMapping, keyItem: MatchedKeyInfo,
|
||||
} else {
|
||||
showError(`映射测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`映射测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`映射测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingMapping.value = null
|
||||
}
|
||||
|
||||
@@ -266,7 +266,8 @@ import {
|
||||
type ProviderMappingPreviewResponse
|
||||
} from '@/api/endpoints'
|
||||
import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseTestModelError } from '@/utils/errorParser'
|
||||
import { parseApiError, parseTestModelError } from '@/utils/errorParser'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
|
||||
interface Endpoint {
|
||||
id: string
|
||||
@@ -276,7 +277,7 @@ interface Endpoint {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
provider: any
|
||||
provider: ProviderWithEndpointsSummary
|
||||
endpoints?: Endpoint[]
|
||||
models?: Model[]
|
||||
mappingPreview?: ProviderMappingPreviewResponse | null
|
||||
@@ -464,8 +465,8 @@ async function toggleModelActive(model: Model) {
|
||||
await updateModel(props.provider.id, model.id, { is_active: newStatus })
|
||||
model.is_active = newStatus
|
||||
showSuccess(newStatus ? '模型已启用' : '模型已停用')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '操作失败', '错误')
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '操作失败'), '错误')
|
||||
} finally {
|
||||
togglingModelId.value = null
|
||||
}
|
||||
@@ -529,9 +530,8 @@ async function testModelConnection(model: Model, apiFormat?: string) {
|
||||
} else {
|
||||
showError(`模型测试失败: ${parseTestModelError(result)}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || '测试请求失败'
|
||||
showError(`模型测试失败: ${errorMsg}`)
|
||||
} catch (err: unknown) {
|
||||
showError(`模型测试失败: ${parseApiError(err, '测试请求失败')}`)
|
||||
} finally {
|
||||
testingModelId.value = null
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import { batchQueryBalance, getArchitectures, type ActionResultResponse, type ArchitectureInfo } from '@/api/providerOps'
|
||||
import { formatBalanceExtraFromSchema, type CredentialsSchema } from '@/features/providers/auth-templates/schema-utils'
|
||||
import type { BalanceExtraItem } from '@/features/providers/auth-templates'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const MAX_BALANCE_RETRIES = 3
|
||||
|
||||
@@ -96,7 +97,7 @@ export function useProviderBalance() {
|
||||
pendingTimers.add(timerId)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[loadBalances] 加载余额数据失败:', e)
|
||||
log.warn('[loadBalances] 加载余额数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +128,7 @@ export function useProviderBalance() {
|
||||
pendingTimers.add(timerId)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[retryPendingBalances] 重试加载余额失败:', e)
|
||||
log.warn('[retryPendingBalances] 重试加载余额失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ export function useProviderBalance() {
|
||||
if (!result || (result.status !== 'success' && result.status !== 'auth_expired') || !result.data) {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, any>
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra || extra.balance === undefined || extra.points === undefined) {
|
||||
return null
|
||||
@@ -216,7 +217,7 @@ export function useProviderBalance() {
|
||||
if (!result || result.status !== 'success' || !result.data) {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, any>
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra || extra.checkin_success === undefined) {
|
||||
return null
|
||||
@@ -236,7 +237,7 @@ export function useProviderBalance() {
|
||||
if (result.status !== 'success' && result.status !== 'auth_expired') {
|
||||
return null
|
||||
}
|
||||
const data = result.data as Record<string, any>
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra || !extra.cookie_expired) {
|
||||
return null
|
||||
@@ -288,7 +289,7 @@ export function useProviderBalance() {
|
||||
return []
|
||||
}
|
||||
|
||||
const data = result.data as Record<string, any>
|
||||
const data = result.data as Record<string, unknown>
|
||||
const extra = data.extra
|
||||
if (!extra) return []
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* 缓存已移至后端(Redis),前端只保留并发请求去重,避免同时发多个相同请求。
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { parseUpstreamModelError } from '@/utils/errorParser'
|
||||
import type { UpstreamModel } from '@/api/endpoints/types'
|
||||
@@ -42,6 +43,7 @@ export function useUpstreamModelsCache() {
|
||||
|
||||
// 强制刷新时不复用进行中的请求
|
||||
if (!forceRefresh && pendingRequests.has(requestKey)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return pendingRequests.get(requestKey)!
|
||||
}
|
||||
|
||||
@@ -62,9 +64,9 @@ export function useUpstreamModelsCache() {
|
||||
const rawError = response.data?.error || '获取上游模型失败'
|
||||
return { models: [], error: parseUpstreamModelError(rawError) }
|
||||
}
|
||||
} catch (err: any) {
|
||||
const rawError = err.response?.data?.detail || err.message || '获取上游模型失败'
|
||||
return { models: [], error: parseUpstreamModelError(rawError) }
|
||||
} catch (err: unknown) {
|
||||
const rawError = isAxiosError(err) ? (err.response?.data?.detail ?? err.message) : (err instanceof Error ? err.message : String(err))
|
||||
return { models: [], error: parseUpstreamModelError(rawError || '获取上游模型失败') }
|
||||
} finally {
|
||||
loadingMap.value.set(requestKey, false)
|
||||
pendingRequests.delete(requestKey)
|
||||
|
||||
Reference in New Issue
Block a user