mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(usage): refine record details and error filters
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -135,13 +135,11 @@ pub struct OpsErrorFilter {
|
||||
pub provider_kind: Option<String>,
|
||||
pub provider_account_ref: Option<String>,
|
||||
pub operation: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub transport: Option<String>,
|
||||
pub attempt_index: Option<u32>,
|
||||
pub response_id: Option<String>,
|
||||
pub upstream_request_id: Option<String>,
|
||||
pub failure_kind: Option<String>,
|
||||
pub status_code: Option<u16>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
@@ -107,14 +107,12 @@ pub struct OpsQuery {
|
||||
pub provider: Option<String>,
|
||||
pub request_id: Option<String>,
|
||||
pub account_id: Option<String>,
|
||||
pub route: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status_code: Option<i64>,
|
||||
pub client_status_code: Option<i64>,
|
||||
pub upstream_status_code: Option<i64>,
|
||||
pub transport: Option<String>,
|
||||
pub attempt_index: Option<i64>,
|
||||
pub failure_class: Option<String>,
|
||||
pub response_id: Option<String>,
|
||||
pub upstream_request_id: Option<String>,
|
||||
pub search: Option<String>,
|
||||
@@ -352,13 +350,11 @@ pub(crate) fn ops_command(query: &OpsQuery) -> Result<domain::OpsErrorQuery, Wir
|
||||
provider_kind: non_empty(query.provider.clone()),
|
||||
provider_account_ref: non_empty(query.account_id.clone()),
|
||||
operation: non_empty(query.kind.clone()),
|
||||
endpoint: non_empty(query.route.clone()),
|
||||
model: non_empty(query.model.clone()),
|
||||
transport: non_empty(query.transport.clone()),
|
||||
attempt_index: parse_attempt_index(query.attempt_index)?,
|
||||
response_id: non_empty(query.response_id.clone()),
|
||||
upstream_request_id: non_empty(query.upstream_request_id.clone()),
|
||||
failure_kind: non_empty(query.failure_class.clone()),
|
||||
status_code,
|
||||
search: non_empty(query.search.clone()),
|
||||
},
|
||||
|
||||
@@ -49,6 +49,12 @@ fn ops_query_should_reject_page_size_above_terminal_limit() {
|
||||
assert_eq!(query.validate_pagination().unwrap_err().field(), "pageSize");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ops_query_should_reject_removed_filters() {
|
||||
assert!(serde_json::from_value::<OpsQuery>(json!({"route": "/v1/responses"})).is_err());
|
||||
assert!(serde_json::from_value::<OpsQuery>(json!({"failureClass": "rate_limited"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_query_should_keep_wire_dimension_name() {
|
||||
let query: DiagnosticsQuery =
|
||||
|
||||
@@ -51,13 +51,11 @@ pub(crate) fn store_ops_error_filter(
|
||||
provider_account_ref: filter.provider_account_ref,
|
||||
provider_kind: filter.provider_kind,
|
||||
operation: filter.operation,
|
||||
endpoint: filter.endpoint,
|
||||
model: filter.model,
|
||||
transport: filter.transport,
|
||||
attempt_index: filter.attempt_index,
|
||||
response_id: filter.response_id,
|
||||
upstream_request_id: filter.upstream_request_id,
|
||||
failure_kind: filter.failure_kind,
|
||||
status_code: filter.status_code,
|
||||
search: filter.search,
|
||||
}
|
||||
|
||||
@@ -126,13 +126,11 @@ pub struct OpsErrorFilter {
|
||||
pub provider_account_ref: Option<String>,
|
||||
pub provider_kind: Option<String>,
|
||||
pub operation: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub transport: Option<String>,
|
||||
pub attempt_index: Option<u32>,
|
||||
pub response_id: Option<String>,
|
||||
pub upstream_request_id: Option<String>,
|
||||
pub failure_kind: Option<String>,
|
||||
pub status_code: Option<u16>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
@@ -148,14 +146,12 @@ impl OpsErrorFilter {
|
||||
),
|
||||
(self.provider_kind.as_deref(), "provider filter"),
|
||||
(self.operation.as_deref(), "operation filter"),
|
||||
(self.endpoint.as_deref(), "endpoint filter"),
|
||||
(self.model.as_deref(), "model filter"),
|
||||
(self.transport.as_deref(), "transport filter"),
|
||||
(
|
||||
self.upstream_request_id.as_deref(),
|
||||
"upstream request ID filter",
|
||||
),
|
||||
(self.failure_kind.as_deref(), "failure kind filter"),
|
||||
] {
|
||||
validate_optional_text(value, MAX_FILTER_BYTES, field)?;
|
||||
}
|
||||
|
||||
@@ -120,16 +120,11 @@ fn push_request_error_predicates(
|
||||
("mr.provider_account_ref", &filter.provider_account_ref),
|
||||
("mr.provider_kind", &filter.provider_kind),
|
||||
("mr.operation", &filter.operation),
|
||||
("mr.endpoint", &filter.endpoint),
|
||||
("mr.upstream_transport", &filter.transport),
|
||||
("mr.upstream_request_id", &filter.upstream_request_id),
|
||||
] {
|
||||
push_text_equality(statement, column, value);
|
||||
}
|
||||
if let Some(value) = &filter.failure_kind {
|
||||
statement.push(" and coalesce(mr.error_kind, 'failed') = ");
|
||||
statement.push_bind(value.clone());
|
||||
}
|
||||
push_response_id_filter(statement, "mr.client_response_id", filter);
|
||||
push_text_equality(statement, "mr.upstream_model_id", &filter.model);
|
||||
if let Some(index) = filter.attempt_index {
|
||||
@@ -167,9 +162,7 @@ fn push_ops_event_predicates(
|
||||
("oe.provider_account_ref", &filter.provider_account_ref),
|
||||
("oe.provider_kind", &filter.provider_kind),
|
||||
("oe.operation", &filter.operation),
|
||||
("mr.endpoint", &filter.endpoint),
|
||||
("oe.upstream_request_id", &filter.upstream_request_id),
|
||||
("oe.failure_kind", &filter.failure_kind),
|
||||
] {
|
||||
push_text_equality(statement, column, value);
|
||||
}
|
||||
|
||||
@@ -762,9 +762,7 @@ async fn admin_observability_adapter_preserves_utc_queries_metrics_costs_and_det
|
||||
provider_kind: Some("openai".to_owned()),
|
||||
provider_account_ref: Some("acct_observe".to_owned()),
|
||||
operation: Some("responses".to_owned()),
|
||||
endpoint: Some("/v1/responses".to_owned()),
|
||||
model: Some("upstream-model".to_owned()),
|
||||
failure_kind: Some("rate_limited".to_owned()),
|
||||
status_code: Some(429),
|
||||
search: Some("req_observe_failed".to_owned()),
|
||||
..admin_observability::OpsErrorFilter::default()
|
||||
|
||||
@@ -420,10 +420,7 @@ interface PageQuery {
|
||||
|
||||
type UsagePageQuery = UsageRangeQuery & PageQuery
|
||||
|
||||
type OpsErrorPageQuery = UsagePageQuery & {
|
||||
failureClass?: string
|
||||
route?: string
|
||||
}
|
||||
type OpsErrorPageQuery = UsagePageQuery
|
||||
|
||||
interface UsageDetailQuery {
|
||||
id: string
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
import type { OpsError } from '@/api'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import BaseModal from '@/components/base/BaseModal/index.vue'
|
||||
import { displayValue } from '../utils/detail'
|
||||
import { presentOpsError } from '../utils/opsErrorPresentation'
|
||||
import UsageDetailCodePanel from './UsageDetailCodePanel.vue'
|
||||
import UsageDetailFieldGrid from './UsageDetailFieldGrid.vue'
|
||||
import UsageStatusCodeBadge from './UsageStatusCodeBadge.vue'
|
||||
|
||||
interface DetailField {
|
||||
label: string
|
||||
value: unknown
|
||||
mono?: boolean
|
||||
wrap?: boolean
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
record: OpsError | null
|
||||
@@ -15,8 +21,6 @@ const props = defineProps<{
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
|
||||
const presentation = computed(() => props.record ? presentOpsError(props.record) : null)
|
||||
|
||||
const panelClass = 'min-w-0 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5'
|
||||
const panelTitleClass = 'm-0 text-cp-sm leading-none font-heavy text-cp-text-secondary'
|
||||
|
||||
@@ -25,72 +29,56 @@ const accountLabel = computed(() => props.record?.accountEmail
|
||||
|| props.record?.metadata.accountLabel
|
||||
|| props.record?.accountId)
|
||||
|
||||
const requestFields = computed(() => [
|
||||
{ label: '客户端 IP', value: props.record?.clientIp, mono: true },
|
||||
{ label: 'User-Agent', value: props.record?.userAgent, mono: true, wrap: true },
|
||||
{ label: '协议', value: props.record?.protocol, mono: true },
|
||||
{ label: '客户端传输', value: props.record?.clientTransport, mono: true },
|
||||
{ label: '客户端 Key ID', value: props.record?.clientApiKeyId, mono: true },
|
||||
{ label: '请求 ID', value: props.record?.requestId, mono: true },
|
||||
{ label: '响应 ID', value: props.record?.responseId, mono: true },
|
||||
{ label: '上游请求 ID', value: props.record?.upstreamRequestId, mono: true },
|
||||
])
|
||||
const errorFields = computed(() => visibleFields([
|
||||
{ label: 'Provider 错误码', value: props.record?.providerErrorCode, mono: true },
|
||||
{ label: '失败分类', value: props.record?.failureClass, mono: true },
|
||||
{ label: '发送状态', value: props.record?.upstreamSendState, mono: true },
|
||||
{ label: '客户端状态', value: props.record?.clientStatusCode, mono: true },
|
||||
{ label: '上游状态', value: props.record?.upstreamStatusCode, mono: true },
|
||||
{ label: '耗时(ms)', value: props.record?.latencyMs, mono: true },
|
||||
]))
|
||||
|
||||
const routeFields = computed(() => [
|
||||
const requestFields = computed(() => visibleFields([
|
||||
{ label: '账号', value: accountLabel.value, mono: true },
|
||||
{ label: '时间', value: props.record?.createdAtDisplay, mono: true },
|
||||
{ label: '请求 ID', value: props.record?.requestId, mono: true },
|
||||
{ label: '客户端 Key ID', value: props.record?.clientApiKeyId, mono: true },
|
||||
{ label: '客户端 IP', value: props.record?.clientIp, mono: true },
|
||||
{ label: '协议', value: props.record?.protocol, mono: true },
|
||||
{ label: 'User-Agent', value: props.record?.userAgent, mono: true, wrap: true, fullWidth: true },
|
||||
]))
|
||||
|
||||
const routeFields = computed(() => visibleFields([
|
||||
{ label: '端点', value: props.record?.route, mono: true },
|
||||
{ label: 'Provider', value: props.record?.provider, mono: true },
|
||||
{ label: '认证类型', value: props.record?.authenticationKind, mono: true },
|
||||
{ label: '账号 ID', value: props.record?.accountId, mono: true },
|
||||
{ label: '请求模型', value: props.record?.requestedModel, mono: true },
|
||||
{ label: '上游模型', value: props.record?.upstreamModel, mono: true },
|
||||
{ label: '记录模型', value: props.record?.model, mono: true },
|
||||
{ label: '推理强度', value: props.record?.reasoningEffort, mono: true },
|
||||
{ label: '推理预设', value: props.record?.reasoningPreset, mono: true },
|
||||
{ label: '客户端传输', value: props.record?.clientTransport, mono: true },
|
||||
{ label: '上游传输', value: props.record?.transport, mono: true },
|
||||
{ label: '服务档位', value: props.record?.serviceTier, mono: true },
|
||||
{ label: '请求类型', value: props.record?.requestKind, mono: true },
|
||||
{ label: '子代理类型', value: props.record?.subagentKind, mono: true },
|
||||
{ label: '压缩请求', value: booleanDisplay(props.record?.compact) },
|
||||
{ label: '服务档位', value: props.record?.serviceTier, mono: true },
|
||||
{ label: '上游传输', value: props.record?.transport, mono: true },
|
||||
{ label: '压缩请求', value: props.record?.compact, mono: true },
|
||||
]))
|
||||
|
||||
const eventFields = computed(() => visibleFields([
|
||||
{ label: '记录来源', value: props.record?.metadata.source, mono: true },
|
||||
{ label: '组件', value: props.record?.metadata.component || props.record?.kind, mono: true },
|
||||
{ label: '事件类型', value: props.record?.kind, mono: true },
|
||||
{ label: '操作', value: props.record?.operation, mono: true },
|
||||
{ label: '尝试序号', value: props.record?.attemptIndex },
|
||||
])
|
||||
|
||||
const diagnosticFields = computed(() => [
|
||||
{ label: '账号', value: accountLabel.value, mono: true },
|
||||
{ label: '时间', value: props.record?.createdAtDisplay, mono: true },
|
||||
{ label: '记录来源', value: presentation.value?.sourceLabel },
|
||||
{ label: '组件', value: presentation.value?.componentLabel },
|
||||
{ label: '操作/触发', value: props.record?.operation, mono: true },
|
||||
{ label: '失败分类', value: presentation.value?.failureClassLabel },
|
||||
{ label: '发送状态', value: props.record?.upstreamSendState, mono: true },
|
||||
{ label: '平台/类型', value: providerKindLabel(props.record) },
|
||||
{ label: '账号 ID', value: props.record?.accountId, mono: true },
|
||||
{ label: 'Provider 错误码', value: props.record?.providerErrorCode, mono: true },
|
||||
{ label: '聚合次数', value: props.record?.occurrenceCount },
|
||||
{ label: '耗时', value: latencyDisplay(props.record?.latencyMs), mono: true },
|
||||
])
|
||||
{ label: '响应 ID', value: props.record?.responseId, mono: true },
|
||||
{ label: '上游请求 ID', value: props.record?.upstreamRequestId, mono: true },
|
||||
]))
|
||||
|
||||
const metadataText = computed(() => {
|
||||
const metadata = props.record?.metadata
|
||||
if (!metadata || (typeof metadata === 'object' && Object.keys(metadata).length === 0))
|
||||
return ''
|
||||
return JSON.stringify(metadata, null, 2)
|
||||
})
|
||||
|
||||
function latencyDisplay(value: unknown) {
|
||||
return typeof value === 'number' ? `${value} ms` : '—'
|
||||
}
|
||||
|
||||
function booleanDisplay(value: boolean | null | undefined) {
|
||||
if (value === true)
|
||||
return '是'
|
||||
if (value === false)
|
||||
return '否'
|
||||
return '—'
|
||||
}
|
||||
|
||||
function providerKindLabel(record: OpsError | null) {
|
||||
if (!record?.provider)
|
||||
return '—'
|
||||
return record.authenticationKind
|
||||
? `${record.provider} · ${record.authenticationKind}`
|
||||
: record.provider
|
||||
function visibleFields(items: DetailField[]) {
|
||||
return items.filter(({ value }) => value !== null && value !== undefined && value !== '')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,68 +86,57 @@ function providerKindLabel(record: OpsError | null) {
|
||||
<BaseModal
|
||||
v-model="open"
|
||||
title="错误明细"
|
||||
description="请求来源、链路上下文与上游错误原文"
|
||||
description="上游错误与请求上下文"
|
||||
tone="danger"
|
||||
size="xl"
|
||||
>
|
||||
<template v-if="record">
|
||||
<section class="rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<UsageStatusCodeBadge :status-code="record.clientStatusCode" />
|
||||
<span class="text-cp-sm font-bold text-cp-text-secondary">
|
||||
客户端 {{ displayValue(record.clientStatusCode) }}
|
||||
</span>
|
||||
<span class="text-cp-sm font-bold text-cp-text-secondary">
|
||||
上游 {{ displayValue(record.upstreamStatusCode) }}
|
||||
</span>
|
||||
<section :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
错误
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="errorFields" />
|
||||
<div v-if="record.message" class="mt-3">
|
||||
<UsageDetailCodePanel title="错误信息" max-height="220px" :content="record.message" />
|
||||
</div>
|
||||
<p class="mt-3 mb-0 text-cp leading-relaxed font-bold text-cp-text">
|
||||
{{ displayValue(presentation?.summary) }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section v-if="record.rawUpstreamError" class="mt-3" :class="panelClass">
|
||||
<UsageDetailCodePanel
|
||||
title="上游返回原文"
|
||||
max-height="360px"
|
||||
:content="record.rawUpstreamError"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div class="mt-3 grid min-w-0 gap-3 lg:grid-cols-2">
|
||||
<section :class="panelClass">
|
||||
<section v-if="requestFields.length" :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
请求追踪
|
||||
请求与客户端
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="requestFields" />
|
||||
</section>
|
||||
|
||||
<section :class="panelClass">
|
||||
<section v-if="routeFields.length" :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
模型与链路
|
||||
路由与模型
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="routeFields" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="mt-3" :class="panelClass">
|
||||
<section v-if="eventFields.length" class="mt-3" :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
错误事件
|
||||
事件记录
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="diagnosticFields" />
|
||||
<UsageDetailFieldGrid :items="eventFields" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<section class="mt-3" :class="panelClass">
|
||||
<UsageDetailCodePanel title="诊断摘要" max-height="260px" :content="record.message" />
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="record.rawUpstreamError"
|
||||
class="mt-3"
|
||||
:class="panelClass"
|
||||
>
|
||||
<UsageDetailCodePanel title="上游返回原文" max-height="360px" :content="record.rawUpstreamError" />
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="metadataText"
|
||||
class="mt-3"
|
||||
:class="panelClass"
|
||||
>
|
||||
<UsageDetailCodePanel title="Metadata" max-height="260px" :content="metadataText" />
|
||||
</section>
|
||||
<template #footer>
|
||||
<BaseButton variant="primary" @click="open = false">
|
||||
关闭
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
@@ -11,10 +11,7 @@ import BaseTable from '@/components/base/BaseTable/index.vue'
|
||||
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
|
||||
import { useOpsErrorsTable } from '../composables/useOpsErrorsTable'
|
||||
import { opsErrorColumns } from '../constants'
|
||||
import { presentOpsError } from '../utils/opsErrorPresentation'
|
||||
import OpsErrorDetailModal from './OpsErrorDetailModal.vue'
|
||||
import UsageStatusCodeBadge from './UsageStatusCodeBadge.vue'
|
||||
import UsageTransportBadge from './UsageTransportBadge.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
timeRangeParams: UsageTimeRangeParams
|
||||
@@ -25,8 +22,6 @@ const {
|
||||
refreshing,
|
||||
records,
|
||||
searchQuery,
|
||||
failureClass,
|
||||
route,
|
||||
pagination,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
@@ -36,29 +31,27 @@ const {
|
||||
const selectedRecord = shallowRef<OpsError | null>(null)
|
||||
const detailOpen = shallowRef(false)
|
||||
|
||||
const upstreamSendStateLabels: Record<string, string> = {
|
||||
sent: '已发送',
|
||||
not_sent: '未发送',
|
||||
ambiguous: '状态不明',
|
||||
}
|
||||
|
||||
function showDetail(record: OpsError) {
|
||||
selectedRecord.value = record
|
||||
detailOpen.value = true
|
||||
}
|
||||
|
||||
function failureClassText(record: OpsError) {
|
||||
return presentOpsError(record).failureClassLabel
|
||||
}
|
||||
|
||||
function errorSummary(record: OpsError) {
|
||||
return presentOpsError(record).summary
|
||||
}
|
||||
|
||||
function accountText(record: OpsError) {
|
||||
return record.accountEmail
|
||||
|| record.accountName
|
||||
|| record.metadata.accountLabel
|
||||
|| record.accountId
|
||||
|| '—'
|
||||
|| '未记录'
|
||||
}
|
||||
|
||||
function modelText(record: OpsError) {
|
||||
return record.requestedModel || record.model || record.upstreamModel || '—'
|
||||
return record.requestedModel || record.model || record.upstreamModel || '未记录'
|
||||
}
|
||||
|
||||
function modelTitle(record: OpsError) {
|
||||
@@ -69,8 +62,10 @@ function modelTitle(record: OpsError) {
|
||||
: modelText(record)
|
||||
}
|
||||
|
||||
function reasoningText(record: OpsError) {
|
||||
return record.reasoningPreset || record.reasoningEffort || '—'
|
||||
function upstreamSendStateText(value: string | null | undefined) {
|
||||
if (!value)
|
||||
return '未记录'
|
||||
return upstreamSendStateLabels[value] ?? value
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -79,22 +74,18 @@ function reasoningText(record: OpsError) {
|
||||
<div
|
||||
class="flex w-full flex-col gap-3 lg:flex-row lg:flex-wrap lg:items-center"
|
||||
role="group"
|
||||
aria-label="错误明细筛选与操作"
|
||||
aria-label="错误筛选与操作"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-2 lg:flex lg:items-center lg:gap-3">
|
||||
<BaseInput
|
||||
v-model="searchQuery"
|
||||
placeholder="事件、请求、Key 或账号 ID 前缀"
|
||||
class="min-w-0 sm:col-span-2 lg:min-w-64 lg:flex-1 lg:max-w-96"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="size-4.5 text-cp-text-tertiary" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
<BaseInput v-model="failureClass" placeholder="失败分类(精确)" class="min-w-0" />
|
||||
<BaseInput v-model="route" placeholder="端点(精确)" class="min-w-0" />
|
||||
</div>
|
||||
<BaseInput
|
||||
v-model="searchQuery"
|
||||
placeholder="请求 ID、Key 或账号"
|
||||
class="min-w-0 w-full lg:max-w-96"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="size-4.5 text-cp-text-tertiary" />
|
||||
</template>
|
||||
</BaseInput>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 self-end items-center justify-end gap-2 lg:ml-auto">
|
||||
@@ -120,23 +111,8 @@ function reasoningText(record: OpsError) {
|
||||
:columns="opsErrorColumns"
|
||||
:rows="records"
|
||||
:loading="loading"
|
||||
empty-text="暂无错误明细"
|
||||
empty-text="当前时段没有错误"
|
||||
>
|
||||
<template #upstreamStatusCode="{ row }">
|
||||
<UsageStatusCodeBadge
|
||||
:status-code="typeof row.upstreamStatusCode === 'number' ? row.upstreamStatusCode : null"
|
||||
/>
|
||||
</template>
|
||||
<template #clientStatusCode="{ row }">
|
||||
<UsageStatusCodeBadge
|
||||
:status-code="typeof row.clientStatusCode === 'number' ? row.clientStatusCode : null"
|
||||
/>
|
||||
</template>
|
||||
<template #failureClass="{ row }">
|
||||
<span class="font-mono text-cp-sm font-bold text-cp-error-text">
|
||||
{{ failureClassText(row) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #provider="{ row }">
|
||||
<ProviderIconGroup
|
||||
:provider="String(row.provider || '')"
|
||||
@@ -144,8 +120,24 @@ function reasoningText(record: OpsError) {
|
||||
/>
|
||||
</template>
|
||||
<template #message="{ row }">
|
||||
<span class="block max-w-full truncate text-cp-sm font-emphasis text-cp-text" :title="errorSummary(row)">
|
||||
{{ errorSummary(row) }}
|
||||
<div class="min-w-0 py-0.5" :title="row.message || row.providerErrorCode || row.failureClass">
|
||||
<code class="block max-w-full truncate font-mono text-cp-sm font-bold text-cp-error-text">
|
||||
{{ row.providerErrorCode || row.failureClass }}
|
||||
</code>
|
||||
<p
|
||||
v-if="row.message"
|
||||
class="mt-1 mb-0 line-clamp-1 text-cp-xs leading-[1.45] font-emphasis text-cp-text-secondary"
|
||||
>
|
||||
{{ row.message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #upstreamSendState="{ row }">
|
||||
<span
|
||||
class="inline-flex h-6 max-w-full items-center rounded-full bg-cp-fill-quaternary px-2.5 font-mono text-cp-sm leading-none font-bold text-cp-text-secondary"
|
||||
:title="row.upstreamSendState || '未记录'"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{ upstreamSendStateText(row.upstreamSendState) }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #accountId="{ row }">
|
||||
@@ -164,38 +156,14 @@ function reasoningText(record: OpsError) {
|
||||
{{ modelText(row) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #reasoningEffort="{ row }">
|
||||
<span class="text-cp-sm font-bold text-cp-text-secondary">
|
||||
{{ reasoningText(row) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #transport="{ row }">
|
||||
<UsageTransportBadge :transport="row.transport" />
|
||||
</template>
|
||||
<template #clientTransport="{ row }">
|
||||
<UsageTransportBadge :transport="row.clientTransport" />
|
||||
</template>
|
||||
<template #clientIp="{ row }">
|
||||
<span
|
||||
class="inline-flex h-6 max-w-full items-center rounded-full bg-cp-blue-bg px-2.5 font-mono text-cp-sm leading-none font-bold text-cp-blue-text-on-bg"
|
||||
:title="row.clientIp || '—'"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{ row.clientIp || '—' }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #userAgent="{ row }">
|
||||
<span class="block max-w-full wrap-break-word whitespace-normal font-mono text-cp-sm leading-[1.4] font-emphasis text-cp-text-secondary">
|
||||
{{ row.userAgent || '—' }}
|
||||
</span>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<BaseIconButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
size="md"
|
||||
label="查看错误详情"
|
||||
@click="showDetail(row)"
|
||||
>
|
||||
<Eye class="size-3.5" />
|
||||
<Eye class="size-4.5" />
|
||||
</BaseIconButton>
|
||||
</template>
|
||||
</BaseTable>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { displayValue, fieldLabelClass, fieldValueClass } from '../utils/detail'
|
||||
|
||||
// 详情弹窗的两列字段网格:label + 值(可选等宽字体),空值以“—”占位。
|
||||
defineProps<{
|
||||
items: Array<{ label: string, value: unknown, mono?: boolean, wrap?: boolean }>
|
||||
items: Array<{ label: string, value: unknown, mono?: boolean, wrap?: boolean, fullWidth?: boolean }>
|
||||
}>()
|
||||
|
||||
function valueClass(item: { mono?: boolean, wrap?: boolean }) {
|
||||
@@ -19,7 +19,12 @@ function valueClass(item: { mono?: boolean, wrap?: boolean }) {
|
||||
|
||||
<template>
|
||||
<dl class="mt-3 grid min-w-0 grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2">
|
||||
<div v-for="item in items" :key="item.label" class="min-w-0">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.label"
|
||||
class="min-w-0"
|
||||
:class="item.fullWidth ? 'sm:col-span-2' : undefined"
|
||||
>
|
||||
<dt :class="fieldLabelClass">
|
||||
{{ item.label }}
|
||||
</dt>
|
||||
|
||||
@@ -57,58 +57,51 @@ const finalAttemptIndex = computed(() => {
|
||||
return last?.attemptIndex ?? props.record?.attemptCount
|
||||
})
|
||||
const overviewItems = computed(() => [
|
||||
{ label: '时间', value: props.record?.createdAtDisplay, mono: true },
|
||||
{ label: '客户端传输', value: usageTransportType(props.record?.clientTransport) },
|
||||
{ label: '上游传输', value: usageTransportType(props.record?.upstreamTransport) },
|
||||
{ label: '耗时', value: props.record?.latencyMsDisplay, mono: true },
|
||||
{ label: '端点', value: props.record?.route, mono: true },
|
||||
{ label: '客户端传输', value: usageTransportType(props.record?.clientTransport), mono: true },
|
||||
{ label: '上游传输', value: usageTransportType(props.record?.upstreamTransport), mono: true },
|
||||
{ label: '总耗时', value: props.record?.latencyMsDisplay, mono: true },
|
||||
{
|
||||
label: latencyDetails.value?.firstOutputLabel ?? '首字',
|
||||
value: latencyDetails.value?.firstOutputDisplay ?? '—',
|
||||
mono: true,
|
||||
},
|
||||
{ label: '总 Token', value: tokenDetails.value?.totalTokensDisplay, mono: true },
|
||||
{ label: '请求 ID', value: props.record?.requestId, mono: true, wide: true },
|
||||
{ label: '消息', value: props.record?.message, wide: true },
|
||||
])
|
||||
|
||||
const detailGroups = computed(() => [
|
||||
const modelRouteItems = computed(() => [
|
||||
{ label: '端点', value: props.record?.route, mono: true },
|
||||
{ label: '推理强度', value: props.record ? usageReasoningEffort(props.record) : '—' },
|
||||
{ label: '请求模型', value: modelDisplay.value.primary, mono: true },
|
||||
{
|
||||
title: '模型与链路',
|
||||
items: [
|
||||
{ label: '端点', value: props.record?.route, mono: true },
|
||||
{ label: '请求模型', value: modelDisplay.value.primary, mono: true },
|
||||
{
|
||||
label: '上游模型',
|
||||
value: modelDisplay.value.secondary || props.record?.upstreamModel,
|
||||
mono: true,
|
||||
},
|
||||
{ label: '存储模型', value: props.record?.model, mono: true },
|
||||
{ label: '推理强度', value: props.record ? usageReasoningEffort(props.record) : '—' },
|
||||
{ label: '账号 ID', value: props.record?.accountId, mono: true },
|
||||
{ label: '请求 ID', value: props.record?.requestId, mono: true },
|
||||
{ label: '响应 ID', value: props.record?.responseId, mono: true },
|
||||
{ label: '上游请求 ID', value: props.record?.upstreamRequestId, mono: true },
|
||||
],
|
||||
label: '上游模型',
|
||||
value: modelDisplay.value.secondary || props.record?.upstreamModel,
|
||||
mono: true,
|
||||
},
|
||||
{ label: '存储模型', value: props.record?.model, mono: true },
|
||||
])
|
||||
|
||||
const clientUpstreamItems = computed(() => [
|
||||
{ label: '客户端 IP', value: props.record ? usageClientIp(props.record) : '—', mono: true },
|
||||
{ label: '服务档位', value: props.record?.serviceTier, mono: true },
|
||||
{ label: '事件类型', value: props.record?.kind, mono: true },
|
||||
{ label: '尝试序号', value: finalAttemptIndex.value },
|
||||
{
|
||||
title: '客户端与上游',
|
||||
items: [
|
||||
{ label: '客户端 IP', value: props.record ? usageClientIp(props.record) : '—', mono: true },
|
||||
{
|
||||
label: 'User-Agent',
|
||||
value: props.record ? usageUserAgent(props.record) : '',
|
||||
mono: true,
|
||||
wrap: true,
|
||||
},
|
||||
{ label: '事件类型', value: props.record?.kind, mono: true },
|
||||
{ label: '尝试序号', value: finalAttemptIndex.value },
|
||||
{ label: '服务档位', value: props.record?.serviceTier, mono: true },
|
||||
{ label: '客户端 Key ID', value: props.record?.clientApiKeyId, mono: true },
|
||||
],
|
||||
label: 'User-Agent',
|
||||
value: props.record ? usageUserAgent(props.record) : '',
|
||||
mono: true,
|
||||
wrap: true,
|
||||
fullWidth: true,
|
||||
},
|
||||
])
|
||||
const modelRouteGroup = computed(() => detailGroups.value[0])
|
||||
const clientUpstreamGroup = computed(() => detailGroups.value[1])
|
||||
|
||||
const identifierItems = computed(() => [
|
||||
{ label: '请求 ID', value: props.record?.requestId, mono: true, wrap: true, fullWidth: true },
|
||||
{ label: '响应 ID', value: props.record?.responseId, mono: true, wrap: true },
|
||||
{ label: '上游请求 ID', value: props.record?.upstreamRequestId, mono: true, wrap: true },
|
||||
{ label: '账号 ID', value: props.record?.accountId, mono: true, wrap: true },
|
||||
{ label: '客户端 Key ID', value: props.record?.clientApiKeyId, mono: true, wrap: true },
|
||||
])
|
||||
|
||||
interface AttemptRow {
|
||||
id: string
|
||||
@@ -286,34 +279,53 @@ const tokenDonutOption = computed<EChartsOption>(() => {
|
||||
>
|
||||
<div v-if="record" class="grid min-w-0 gap-3">
|
||||
<section :class="panelClass">
|
||||
<div class="min-w-0">
|
||||
<span :class="fieldLabelClass">账号</span>
|
||||
<p
|
||||
class="mt-1.5 mb-0 truncate font-mono text-cp leading-none font-heavy text-cp-text"
|
||||
:title="displayValue(accountDisplay)"
|
||||
>
|
||||
{{ displayValue(accountDisplay) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl
|
||||
class="mt-4 grid grid-cols-2 gap-x-5 gap-y-3 lg:grid-cols-[120px_120px_repeat(5,minmax(0,1fr))]"
|
||||
class="grid min-w-0 grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4 lg:grid-cols-[minmax(0,2fr)_100px_minmax(0,1fr)_180px]"
|
||||
>
|
||||
<div class="col-span-2 min-w-0 lg:col-span-1">
|
||||
<dt :class="fieldLabelClass">
|
||||
账号
|
||||
</dt>
|
||||
<dd
|
||||
class="mt-1.5 mb-0 min-w-0 break-all font-mono text-cp-sm leading-snug font-heavy text-cp-text"
|
||||
:title="displayValue(accountDisplay)"
|
||||
>
|
||||
{{ displayValue(accountDisplay) }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<dt :class="fieldLabelClass">
|
||||
状态码
|
||||
</dt>
|
||||
<dd class="mt-1.5 mb-0">
|
||||
<UsageStatusCodeBadge :status-code="record.statusCode" />
|
||||
<dd :class="fieldValueClass(true)" :title="displayValue(record.statusCode)">
|
||||
{{ displayValue(record.statusCode) }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="item in overviewItems"
|
||||
:key="item.label"
|
||||
class="min-w-0"
|
||||
:class="item.wide ? 'col-span-2 lg:col-span-2' : undefined"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<dt :class="fieldLabelClass">
|
||||
消息
|
||||
</dt>
|
||||
<dd :class="fieldValueClass()" :title="displayValue(record.message)">
|
||||
{{ displayValue(record.message) }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 min-w-0 lg:col-span-1">
|
||||
<dt :class="fieldLabelClass">
|
||||
时间
|
||||
</dt>
|
||||
<dd :class="fieldValueClass(true)" :title="displayValue(record.createdAtDisplay)">
|
||||
{{ displayValue(record.createdAtDisplay) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<dl
|
||||
class="mt-4 grid min-w-0 grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3 lg:grid-cols-[minmax(160px,1.4fr)_repeat(5,minmax(0,1fr))]"
|
||||
>
|
||||
<div v-for="item in overviewItems" :key="item.label" class="min-w-0">
|
||||
<dt :class="fieldLabelClass">
|
||||
{{ item.label }}
|
||||
</dt>
|
||||
@@ -324,74 +336,79 @@ const tokenDonutOption = computed<EChartsOption>(() => {
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="grid min-w-0 gap-3 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
|
||||
<div class="flex min-h-0 min-w-0 flex-col gap-3">
|
||||
<section :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
{{ modelRouteGroup.title }}
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="modelRouteGroup.items" />
|
||||
</section>
|
||||
<section class="grid min-w-0 gap-3 lg:grid-cols-2">
|
||||
<section :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
模型与路由
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="modelRouteItems" />
|
||||
</section>
|
||||
|
||||
<section class="flex min-h-0 flex-1 flex-col" :class="[panelClass]">
|
||||
<h3 :class="panelTitleClass">
|
||||
Token
|
||||
</h3>
|
||||
<div
|
||||
class="mt-3 grid min-h-38 min-w-0 flex-1 grid-cols-1 content-center items-center gap-3 sm:grid-cols-[150px_minmax(0,1fr)]"
|
||||
>
|
||||
<div class="relative mx-auto w-38 sm:mx-0">
|
||||
<BaseChart :option="tokenDonutOption" :height="152" />
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div class="grid text-center">
|
||||
<span class="text-cp-xs leading-none font-bold text-cp-text-quaternary">
|
||||
总计
|
||||
</span>
|
||||
<strong
|
||||
class="mt-1 font-mono text-[16px] leading-none font-extrabold tabular-nums text-cp-text"
|
||||
>
|
||||
{{ tokenDetails?.totalTokensDisplay ?? '—' }}
|
||||
</strong>
|
||||
</div>
|
||||
<section :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
客户端与上游
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="clientUpstreamItems" />
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
请求标识
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="identifierItems" />
|
||||
</section>
|
||||
|
||||
<section class="grid min-w-0 gap-3 lg:grid-cols-2">
|
||||
<section class="flex min-h-0 flex-col" :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
Token
|
||||
</h3>
|
||||
<div
|
||||
class="mt-3 grid min-h-38 min-w-0 flex-1 grid-cols-1 content-center items-center gap-3 sm:grid-cols-[150px_minmax(0,1fr)]"
|
||||
>
|
||||
<div class="relative mx-auto w-38 sm:mx-0">
|
||||
<BaseChart :option="tokenDonutOption" :height="152" />
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div class="grid text-center">
|
||||
<span class="text-cp-xs leading-none font-bold text-cp-text-quaternary">
|
||||
总计
|
||||
</span>
|
||||
<strong
|
||||
class="mt-1 font-mono text-[16px] leading-none font-extrabold tabular-nums text-cp-text"
|
||||
>
|
||||
{{ tokenDetails?.totalTokensDisplay ?? '—' }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="grid min-w-0 grid-cols-2 gap-x-4 gap-y-3">
|
||||
<div v-for="item in tokenChartItems" :key="item.label" class="min-w-0">
|
||||
<dt class="flex min-w-0 items-center gap-1.5" :class="fieldLabelClass">
|
||||
<i
|
||||
class="size-1.75 shrink-0 rounded-full"
|
||||
:style="{ backgroundColor: item.color }"
|
||||
/>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</dt>
|
||||
<dd
|
||||
class="font-mono tabular-nums" :class="[fieldValueBaseClass]"
|
||||
:title="displayValue(item.display)"
|
||||
>
|
||||
{{ displayValue(item.display) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-0 min-w-0 flex-col gap-3">
|
||||
<section class="flex-1" :class="[panelClass]">
|
||||
<h3 :class="panelTitleClass">
|
||||
{{ clientUpstreamGroup.title }}
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="clientUpstreamGroup.items" />
|
||||
</section>
|
||||
<dl class="grid min-w-0 grid-cols-2 gap-x-4 gap-y-3">
|
||||
<div v-for="item in tokenChartItems" :key="item.label" class="min-w-0">
|
||||
<dt class="flex min-w-0 items-center gap-1.5" :class="fieldLabelClass">
|
||||
<i
|
||||
class="size-1.75 shrink-0 rounded-full"
|
||||
:style="{ backgroundColor: item.color }"
|
||||
/>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</dt>
|
||||
<dd
|
||||
class="font-mono tabular-nums" :class="[fieldValueBaseClass]"
|
||||
:title="displayValue(item.display)"
|
||||
>
|
||||
{{ displayValue(item.display) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
费用
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="billingItems" />
|
||||
</section>
|
||||
</div>
|
||||
<section :class="panelClass">
|
||||
<h3 :class="panelTitleClass">
|
||||
费用
|
||||
</h3>
|
||||
<UsageDetailFieldGrid :items="billingItems" />
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section v-if="attemptRows.length" :class="panelClass">
|
||||
|
||||
@@ -11,16 +11,12 @@ import { errorMessage, withMinimumDuration } from '@/utils/async'
|
||||
export function useOpsErrorsTable(timeRangeParams: Readonly<Ref<UsageTimeRangeParams>>) {
|
||||
const refreshing = shallowRef(false)
|
||||
const searchQuery = shallowRef('')
|
||||
const failureClass = shallowRef('')
|
||||
const route = shallowRef('')
|
||||
const query = useStablePagedQuery({
|
||||
initialPageSize: 10,
|
||||
load: ({ currentPage, pageSize }) => getOpsErrors({
|
||||
currentPage,
|
||||
pageSize,
|
||||
search: searchQuery.value.trim() || undefined,
|
||||
failureClass: failureClass.value.trim() || undefined,
|
||||
route: route.value.trim() || undefined,
|
||||
...timeRangeParams.value,
|
||||
}),
|
||||
onError: error => toast.error(errorMessage(error, '加载错误明细失败')),
|
||||
@@ -56,7 +52,7 @@ export function useOpsErrorsTable(timeRangeParams: Readonly<Ref<UsageTimeRangePa
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
[searchQuery, failureClass, route],
|
||||
searchQuery,
|
||||
() => {
|
||||
void query.reloadFromStart()
|
||||
},
|
||||
@@ -74,8 +70,6 @@ export function useOpsErrorsTable(timeRangeParams: Readonly<Ref<UsageTimeRangePa
|
||||
refreshing,
|
||||
records: query.items,
|
||||
searchQuery,
|
||||
failureClass,
|
||||
route,
|
||||
pagination,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
|
||||
@@ -34,22 +34,16 @@ export const usageRecordColumns = defineTableColumns<UsageDisplayRecord>([
|
||||
])
|
||||
|
||||
export const opsErrorColumns = defineTableColumns<OpsErrorRow>([
|
||||
{ key: 'accountId', label: '账号', kind: 'mono', size: '3xl' },
|
||||
{ key: 'accountId', label: '账号', kind: 'identity', size: '3xl', emptyText: '未知账号' },
|
||||
{ key: 'provider', label: '平台/类型', kind: 'custom', size: 'sm' },
|
||||
{ key: 'message', label: '错误摘要', kind: 'custom', size: '3xl' },
|
||||
{ key: 'failureClass', label: '失败分类', kind: 'mono' },
|
||||
{ key: 'upstreamStatusCode', label: '上游状态', kind: 'status', size: 'sm' },
|
||||
{ key: 'clientStatusCode', label: '客户端状态', kind: 'status', size: 'md' },
|
||||
{ key: 'model', label: '模型', kind: 'custom', size: 'xl' },
|
||||
{ key: 'reasoningEffort', label: '推理强度', kind: 'custom' },
|
||||
{ key: 'route', label: '端点', kind: 'mono' },
|
||||
{ key: 'transport', label: '上游', kind: 'custom', size: 'md' },
|
||||
{ key: 'clientTransport', label: '接入', kind: 'custom', size: 'md' },
|
||||
{ key: 'message', label: '错误', kind: 'custom', size: '4xl' },
|
||||
{ key: 'upstreamSendState', label: '发送状态', kind: 'custom', size: 'xl' },
|
||||
{ key: 'model', label: '模型', kind: 'custom', size: 'xl', emptyText: '未记录模型' },
|
||||
{ key: 'route', label: '端点', kind: 'mono', size: 'xl', emptyText: '未记录' },
|
||||
{ key: 'createdAtDisplay', label: '时间', kind: 'datetime' },
|
||||
{ key: 'requestId', label: '请求 ID', kind: 'mono', size: '2xl' },
|
||||
{ key: 'kind', label: '事件', kind: 'mono' },
|
||||
{ key: 'clientIp', label: 'IP', kind: 'custom', size: '3xl' },
|
||||
{ key: 'userAgent', label: 'User-Agent', kind: 'custom', size: '4xl' },
|
||||
{ key: 'requestId', label: '请求 ID', kind: 'mono', size: '2xl', emptyText: '未记录' },
|
||||
{ key: 'clientIp', label: 'IP', kind: 'mono', size: '3xl', emptyText: '未记录' },
|
||||
{ key: 'userAgent', label: 'User-Agent', kind: 'mono', size: '4xl', emptyText: '未记录' },
|
||||
{ key: 'actions', label: '操作', kind: 'actions', size: 'sm' },
|
||||
])
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import type { OpsError } from '@/api/modules/usage'
|
||||
|
||||
const FAILURE_CLASS_LABELS: Record<string, string> = {
|
||||
invalid_request: '请求不合法',
|
||||
unsupported: '请求能力不受支持',
|
||||
unauthorized: '认证失败',
|
||||
auth_failed: '认证失败',
|
||||
permission_denied: '上游权限不足',
|
||||
policy_denied: '策略拒绝',
|
||||
model_not_found: '模型不存在',
|
||||
no_available_provider: '没有可用 Provider',
|
||||
no_eligible_account: '没有符合条件的账号',
|
||||
account_capacity_unavailable: '账号容量不足',
|
||||
provider_infrastructure_unavailable: 'Provider 基础设施不可用',
|
||||
rate_limited: '上游限流',
|
||||
quota_exhausted: '账号额度已耗尽',
|
||||
upstream_unavailable: '上游服务不可用',
|
||||
upstream_error: '上游响应错误',
|
||||
transport: '上游传输失败',
|
||||
protocol: '上游协议错误',
|
||||
unavailable: 'Provider 暂不可用',
|
||||
timeout: '请求超时',
|
||||
cancelled: '请求已取消',
|
||||
process_terminated: '进程终止',
|
||||
internal_error: '内部错误',
|
||||
failed: '请求失败',
|
||||
}
|
||||
|
||||
const COMPONENT_LABELS: Record<string, string> = {
|
||||
model_request: '模型请求',
|
||||
routing: '路由调度',
|
||||
account_probe: '账号连接测试',
|
||||
}
|
||||
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
model_request: '模型请求记录',
|
||||
ops_event: '运维事件',
|
||||
}
|
||||
|
||||
export interface OpsErrorPresentation {
|
||||
summary: string
|
||||
failureClassLabel: string
|
||||
componentLabel: string
|
||||
sourceLabel: string
|
||||
}
|
||||
|
||||
/** 将运维错误的稳定机器字段转换为中文摘要;原始诊断仍由调用方单独展示。 */
|
||||
export function presentOpsError(record: OpsError): OpsErrorPresentation {
|
||||
const failureClassLabel = mappedOrOriginal(FAILURE_CLASS_LABELS, record.failureClass, '未分类错误')
|
||||
const componentLabel = mappedOrOriginal(
|
||||
COMPONENT_LABELS,
|
||||
record.metadata.component || record.kind,
|
||||
'未知组件',
|
||||
)
|
||||
const sourceLabel = mappedOrOriginal(SOURCE_LABELS, record.metadata.source, '未知来源')
|
||||
const statuses = [
|
||||
statusText('客户端', record.clientStatusCode),
|
||||
statusText('上游', record.upstreamStatusCode),
|
||||
].filter(Boolean)
|
||||
const statusSuffix = statuses.length > 0 ? `(${statuses.join(',')})` : ''
|
||||
const providerCode = record.providerErrorCode?.trim()
|
||||
const codeSuffix = providerCode ? ` [${providerCode}]` : ''
|
||||
|
||||
return {
|
||||
summary: `${componentLabel}:${failureClassLabel}${codeSuffix}${statusSuffix}`,
|
||||
failureClassLabel,
|
||||
componentLabel,
|
||||
sourceLabel,
|
||||
}
|
||||
}
|
||||
|
||||
function mappedOrOriginal(
|
||||
labels: Record<string, string>,
|
||||
value: string | null | undefined,
|
||||
fallback: string,
|
||||
) {
|
||||
const normalized = value?.trim()
|
||||
return normalized ? labels[normalized] || normalized : fallback
|
||||
}
|
||||
|
||||
function statusText(owner: string, status: number | null) {
|
||||
return typeof status === 'number' ? `${owner} HTTP ${status}` : ''
|
||||
}
|
||||
Reference in New Issue
Block a user