mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(admin): improve quota forecast and subscription display
Move the quota forecast entry after reset credits and propagate account updates from the quota panel. Include each error account current subscription in troubleshooting and unify subscription labels. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -809,6 +809,8 @@ pub struct OpsError {
|
||||
pub provider_account_ref: Option<String>,
|
||||
pub provider_account_name: Option<String>,
|
||||
pub provider_account_email: Option<String>,
|
||||
pub provider_account_plan_type: Option<String>,
|
||||
pub provider_account_plan_type_display: Option<String>,
|
||||
pub provider_account_authentication_kind: Option<String>,
|
||||
pub upstream_model_id: Option<String>,
|
||||
pub upstream_transport: Option<String>,
|
||||
|
||||
@@ -382,10 +382,19 @@ impl ObservabilityService for DefaultObservabilityService {
|
||||
}
|
||||
|
||||
async fn ops_errors(&self, query: OpsErrorQuery) -> Result<OpsErrorPage, AdminError> {
|
||||
self.store
|
||||
let mut page = self
|
||||
.store
|
||||
.list_ops_errors(query)
|
||||
.await
|
||||
.map_err(|error| map_store_error(error, "operations errors"))
|
||||
.map_err(|error| map_store_error(error, "operations errors"))?;
|
||||
for error in &mut page.items {
|
||||
error.provider_account_plan_type_display =
|
||||
error.provider_kind.as_deref().and_then(|provider| {
|
||||
self.providers
|
||||
.plan_type_display(provider, error.provider_account_plan_type.as_deref())
|
||||
});
|
||||
}
|
||||
Ok(page)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -583,6 +583,8 @@ pub(crate) fn ops_error_view(error: domain::OpsError) -> OpsErrorView {
|
||||
account_id: error.provider_account_ref,
|
||||
account_name: error.provider_account_name,
|
||||
account_email: error.provider_account_email,
|
||||
account_plan_type: error.provider_account_plan_type,
|
||||
account_plan_type_display: error.provider_account_plan_type_display,
|
||||
route: error.endpoint.unwrap_or_else(|| "—".to_owned()),
|
||||
model,
|
||||
requested_model: error.requested_model_id,
|
||||
|
||||
@@ -792,6 +792,8 @@ pub struct OpsErrorView {
|
||||
pub account_id: Option<String>,
|
||||
pub account_name: Option<String>,
|
||||
pub account_email: Option<String>,
|
||||
pub account_plan_type: Option<String>,
|
||||
pub account_plan_type_display: Option<String>,
|
||||
pub route: String,
|
||||
pub model: Option<String>,
|
||||
pub requested_model: Option<String>,
|
||||
|
||||
@@ -447,6 +447,8 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
|
||||
provider_account_ref: Some("acct_err".to_owned()),
|
||||
provider_account_name: None,
|
||||
provider_account_email: Some("err@example.invalid".to_owned()),
|
||||
provider_account_plan_type: Some("pro".to_owned()),
|
||||
provider_account_plan_type_display: Some("Pro".to_owned()),
|
||||
provider_account_authentication_kind: Some("api_key".to_owned()),
|
||||
upstream_model_id: Some("upstream-err".to_owned()),
|
||||
upstream_transport: Some("http_sse".to_owned()),
|
||||
@@ -509,6 +511,8 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
|
||||
"kind": value["data"]["items"][0]["kind"],
|
||||
"accountId": value["data"]["items"][0]["accountId"],
|
||||
"accountLabel": value["data"]["items"][0]["metadata"]["accountLabel"],
|
||||
"accountPlanType": value["data"]["items"][0]["accountPlanType"],
|
||||
"accountPlanTypeDisplay": value["data"]["items"][0]["accountPlanTypeDisplay"],
|
||||
"clientStatusCode": value["data"]["items"][0]["clientStatusCode"],
|
||||
"route": value["data"]["items"][0]["route"],
|
||||
"requestedModel": value["data"]["items"][0]["requestedModel"],
|
||||
@@ -528,6 +532,8 @@ async fn ops_errors_should_keep_account_label_and_authentication_contract() {
|
||||
"kind": "model_request",
|
||||
"accountId": "acct_err",
|
||||
"accountLabel": "err@example.invalid",
|
||||
"accountPlanType": "pro",
|
||||
"accountPlanTypeDisplay": "Pro",
|
||||
"clientStatusCode": 502,
|
||||
"route": "/v1/responses",
|
||||
"requestedModel": "gpt-5.4",
|
||||
|
||||
@@ -168,6 +168,8 @@ fn error_record() -> OpsError {
|
||||
provider_account_name: Some("private-sentinel".to_owned()),
|
||||
provider_account_email: Some("private-sentinel".to_owned()),
|
||||
provider_account_authentication_kind: Some("private-sentinel".to_owned()),
|
||||
provider_account_plan_type: Some("private-sentinel".to_owned()),
|
||||
provider_account_plan_type_display: Some("private-sentinel".to_owned()),
|
||||
upstream_model_id: Some("private-sentinel".to_owned()),
|
||||
upstream_transport: Some("websocket".to_owned()),
|
||||
failure_kind: "private-sentinel".to_owned(),
|
||||
|
||||
@@ -757,6 +757,8 @@ pub(crate) fn admin_ops_error(error: OpsErrorRecord) -> admin_observability::Ops
|
||||
provider_account_ref: error.provider_account_ref,
|
||||
provider_account_name: error.provider_account_name,
|
||||
provider_account_email: error.provider_account_email,
|
||||
provider_account_plan_type: error.provider_account_plan_type,
|
||||
provider_account_plan_type_display: None,
|
||||
provider_account_authentication_kind: error.provider_account_authentication_kind,
|
||||
upstream_model_id: error.upstream_model_id,
|
||||
upstream_transport: error.upstream_transport,
|
||||
@@ -955,6 +957,7 @@ pub(crate) fn ops_error_from_row(row: &sqlx::postgres::PgRow) -> StoreResult<Ops
|
||||
provider_account_ref: get(row, "provider_account_ref")?,
|
||||
provider_account_name: get(row, "provider_account_name")?,
|
||||
provider_account_email: get(row, "provider_account_email")?,
|
||||
provider_account_plan_type: get(row, "provider_account_plan_type")?,
|
||||
provider_account_authentication_kind: get(row, "provider_account_authentication_kind")?,
|
||||
upstream_model_id: get(row, "upstream_model_id")?,
|
||||
upstream_transport: get(row, "upstream_transport")?,
|
||||
|
||||
@@ -722,6 +722,7 @@ pub struct OpsErrorRecord {
|
||||
pub provider_account_ref: Option<String>,
|
||||
pub provider_account_name: Option<String>,
|
||||
pub provider_account_email: Option<String>,
|
||||
pub provider_account_plan_type: Option<String>,
|
||||
pub provider_account_authentication_kind: Option<String>,
|
||||
pub upstream_model_id: Option<String>,
|
||||
pub upstream_transport: Option<String>,
|
||||
|
||||
@@ -10,6 +10,7 @@ const REQUEST_ERROR_SELECT: &str = "select 'model_request'::text as source,
|
||||
mr.endpoint, mr.provider_kind, mr.provider_account_ref,
|
||||
mr.provider_account_name_snapshot as provider_account_name,
|
||||
mr.provider_account_email_snapshot as provider_account_email,
|
||||
account.plan_type as provider_account_plan_type,
|
||||
mr.provider_account_authentication_kind_snapshot
|
||||
as provider_account_authentication_kind,
|
||||
mr.upstream_model_id, mr.upstream_transport,
|
||||
@@ -33,6 +34,7 @@ const REQUEST_ERROR_SELECT: &str = "select 'model_request'::text as source,
|
||||
'model_request:' || mr.id as stable_sort_id
|
||||
from model_requests mr
|
||||
left join client_api_keys client_key on client_key.id = mr.client_api_key_ref
|
||||
left join provider_accounts account on account.id = mr.provider_account_ref
|
||||
where true";
|
||||
|
||||
const OPS_EVENT_SELECT: &str = "select 'ops_event'::text as source,
|
||||
@@ -42,6 +44,7 @@ const OPS_EVENT_SELECT: &str = "select 'ops_event'::text as source,
|
||||
mr.endpoint, oe.provider_kind, oe.provider_account_ref,
|
||||
oe.provider_account_name_snapshot as provider_account_name,
|
||||
oe.provider_account_email_snapshot as provider_account_email,
|
||||
account.plan_type as provider_account_plan_type,
|
||||
oe.provider_account_authentication_kind_snapshot
|
||||
as provider_account_authentication_kind,
|
||||
oe.upstream_model_id, null::text as upstream_transport, oe.failure_kind,
|
||||
@@ -66,6 +69,7 @@ const OPS_EVENT_SELECT: &str = "select 'ops_event'::text as source,
|
||||
from ops_events oe
|
||||
left join model_requests mr on mr.id = oe.model_request_id
|
||||
left join client_api_keys client_key on client_key.id = mr.client_api_key_ref
|
||||
left join provider_accounts account on account.id = oe.provider_account_ref
|
||||
where true";
|
||||
|
||||
pub(crate) async fn list_ops_errors(
|
||||
|
||||
@@ -653,6 +653,114 @@ async fn ops_search_should_treat_sql_wildcards_as_literals() {
|
||||
database.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ops_errors_should_use_each_event_accounts_current_subscription() {
|
||||
let Some(database) = TestDatabase::create("ops_account_subscription").await else {
|
||||
return;
|
||||
};
|
||||
let now = Utc::now();
|
||||
seed_observability_facts(&database.pool, now)
|
||||
.await
|
||||
.expect("seed observability facts");
|
||||
sqlx::query(
|
||||
"insert into provider_accounts (
|
||||
id, provider_kind, name, email, upstream_user_id, upstream_account_id,
|
||||
plan_type, authentication_kind, provider_credentials_json,
|
||||
credential_revision, credential_observed_at, has_refresh_token,
|
||||
created_at, updated_at
|
||||
) select 'acct_retry', provider_kind, name, email, upstream_user_id,
|
||||
'retry-workspace', 'team', authentication_kind,
|
||||
provider_credentials_json, credential_revision, credential_observed_at,
|
||||
has_refresh_token, created_at, updated_at
|
||||
from provider_accounts where id = 'acct_observe'",
|
||||
)
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.expect("seed same-email retry account");
|
||||
sqlx::query(
|
||||
"update ops_events set provider_account_id = 'acct_retry',
|
||||
provider_account_ref = 'acct_retry'
|
||||
where id = 'ops_observe_retry'",
|
||||
)
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.expect("assign retry event to its own account");
|
||||
let query = admin_observability::OpsErrorQuery {
|
||||
range: admin_observability::TimeRange::new(
|
||||
now - TimeDelta::hours(1),
|
||||
now + TimeDelta::hours(1),
|
||||
)
|
||||
.expect("ops range"),
|
||||
filter: admin_observability::OpsErrorFilter::default(),
|
||||
current_page: 1,
|
||||
page_size: PageSize::new(10).expect("page size"),
|
||||
};
|
||||
let store = admin_observability_store(&database.pool);
|
||||
for plan in [Some("pro"), Some("plus"), None] {
|
||||
sqlx::query("update provider_accounts set plan_type = $1 where id = 'acct_observe'")
|
||||
.bind(plan)
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.expect("update current subscription");
|
||||
let page = store
|
||||
.list_ops_errors(query.clone())
|
||||
.await
|
||||
.expect("ops page");
|
||||
assert_eq!(page.total, 2);
|
||||
assert_eq!(page.items.len(), 2);
|
||||
let request = page
|
||||
.items
|
||||
.iter()
|
||||
.find(|error| error.source == "model_request")
|
||||
.expect("request error");
|
||||
let event = page
|
||||
.items
|
||||
.iter()
|
||||
.find(|error| error.source == "ops_event")
|
||||
.expect("retry event");
|
||||
assert_eq!(request.provider_account_email, event.provider_account_email);
|
||||
assert_eq!(request.provider_account_plan_type.as_deref(), plan);
|
||||
assert_eq!(event.provider_account_plan_type.as_deref(), Some("team"));
|
||||
}
|
||||
sqlx::query(
|
||||
"update ops_events set model_request_id = null, attempt_index = null
|
||||
where id = 'ops_observe_retry'",
|
||||
)
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.expect("detach event from request");
|
||||
let page = store
|
||||
.list_ops_errors(query.clone())
|
||||
.await
|
||||
.expect("ops page");
|
||||
assert_eq!(
|
||||
page.items
|
||||
.iter()
|
||||
.find(|error| error.source == "ops_event")
|
||||
.expect("standalone event")
|
||||
.provider_account_plan_type
|
||||
.as_deref(),
|
||||
Some("team")
|
||||
);
|
||||
sqlx::query("delete from provider_accounts where id = 'acct_retry'")
|
||||
.execute(&database.pool)
|
||||
.await
|
||||
.expect("delete retry account");
|
||||
let page = store
|
||||
.list_ops_errors(query)
|
||||
.await
|
||||
.expect("deleted account history");
|
||||
assert_eq!(page.total, 2);
|
||||
let event = page
|
||||
.items
|
||||
.iter()
|
||||
.find(|error| error.source == "ops_event")
|
||||
.expect("deleted account event");
|
||||
assert_eq!(event.provider_account_ref.as_deref(), Some("acct_retry"));
|
||||
assert_eq!(event.provider_account_plan_type, None);
|
||||
database.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ops_should_include_incomplete_upstream_errors() {
|
||||
let Some(database) = TestDatabase::create("ops_incomplete_error").await else {
|
||||
|
||||
+2
-2
@@ -1562,8 +1562,8 @@ request/response/upstream ID、outcome 与搜索文本。诊断 `dimension` 可
|
||||
`requestShare` 的分母为该维度筛选后、截取前的全部请求数;`failureClass` 只在带错误类型的请求内计算占比
|
||||
`retryCount` 为额外执行尝试次数之和,`retryRate` 为发生过重试的请求数占该组请求数的比例,同一请求多次重试只计一次
|
||||
|
||||
账号维度诊断项和使用记录列表的 `accountPlanType` / `accountPlanTypeDisplay` 返回账号当前套餐及展示名称,
|
||||
按内部账号 ID 关联;套餐未知或账号已删除时为 `null`,不作为请求发生时的套餐快照
|
||||
账号维度诊断项、使用记录列表和错误排查列表的 `accountPlanType` / `accountPlanTypeDisplay` 返回账号当前订阅及展示名称,
|
||||
按各记录的内部账号 ID 关联;订阅未知或账号已删除时为 `null`,不作为请求发生时的订阅快照
|
||||
|
||||
管理端请求列表及 Dashboard 最近请求中的 `accountNotes` 为账号当前备注,按内部账号 ID 关联。
|
||||
备注不写入请求历史快照;无备注或账号已删除时返回 `null`,修改备注不改变历史请求的账号归属
|
||||
|
||||
@@ -278,6 +278,8 @@ export interface OpsError {
|
||||
accountId: string | null
|
||||
accountName: string | null
|
||||
accountEmail: string | null
|
||||
accountPlanType: string | null
|
||||
accountPlanTypeDisplay: string | null
|
||||
route: string
|
||||
model: string | null
|
||||
requestedModel: string | null
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
import type { AccountRow } from '../../constants'
|
||||
import { BaseEmpty, BaseIconButton } from '@codex-proxy/ui'
|
||||
|
||||
import { RefreshCw, UserRound } from '@lucide/vue'
|
||||
import { ChartNoAxesCombined, RefreshCw, UserRound } from '@lucide/vue'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
import { formatProviderLabel } from '@/utils/providers'
|
||||
import { groupedAccountQuotaWindows, orderedPanelQuotaWindows } from '../../constants'
|
||||
import AccountPlanBadge from '../AccountPlanBadge.vue'
|
||||
import AccountProfileModal from '../AccountProfileModal/index.vue'
|
||||
import AccountQuotaForecastModal from '../AccountQuotaForecastModal/index.vue'
|
||||
import AccountQuotaPanelEntry from './Entry.vue'
|
||||
import AccountResetCredits from './ResetCredits.vue'
|
||||
|
||||
@@ -17,6 +18,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
accountUpdated: [account: AccountRow]
|
||||
refreshQuota: [accountId: string]
|
||||
quotaReset: [accountId: string]
|
||||
}>()
|
||||
@@ -25,8 +27,10 @@ const quotaEntries = computed(() => groupedAccountQuotaWindows(
|
||||
orderedPanelQuotaWindows(props.account.quota.windows),
|
||||
))
|
||||
const profileOpen = shallowRef(false)
|
||||
const forecastOpen = shallowRef(false)
|
||||
const hasForecast = computed(() => props.account.authenticationKind !== 'api_key')
|
||||
const hasPersonalInfo = computed(() => props.account.capabilities.profile || props.account.capabilities.subscription)
|
||||
const hasActions = computed(() => hasPersonalInfo.value || props.account.capabilities.quotaRefresh || props.account.capabilities.resetCredits)
|
||||
const hasActions = computed(() => hasPersonalInfo.value || hasForecast.value || props.account.capabilities.quotaRefresh || props.account.capabilities.resetCredits)
|
||||
watch(hasPersonalInfo, (available) => {
|
||||
if (!available)
|
||||
profileOpen.value = false
|
||||
@@ -69,6 +73,17 @@ watch(hasPersonalInfo, (available) => {
|
||||
:account="account"
|
||||
@consumed="emit('quotaReset', $event)"
|
||||
/>
|
||||
<BaseIconButton
|
||||
v-if="hasForecast"
|
||||
label="预测周/月额度"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-haspopup="dialog"
|
||||
:pressed="forecastOpen"
|
||||
@click="forecastOpen = true"
|
||||
>
|
||||
<ChartNoAxesCombined class="size-3.5" :stroke-width="1.75" />
|
||||
</BaseIconButton>
|
||||
<BaseIconButton
|
||||
v-if="account.capabilities.quotaRefresh"
|
||||
variant="ghost"
|
||||
@@ -107,4 +122,10 @@ watch(hasPersonalInfo, (available) => {
|
||||
v-model="profileOpen"
|
||||
:account="account"
|
||||
/>
|
||||
<AccountQuotaForecastModal
|
||||
v-if="hasForecast"
|
||||
v-model="forecastOpen"
|
||||
:account="account"
|
||||
@account-updated="emit('accountUpdated', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccountRow } from '../constants'
|
||||
|
||||
import { BaseIconButton, BaseTable, defineTableColumns } from '@codex-proxy/ui'
|
||||
import { ChartNoAxesCombined, Sigma } from '@lucide/vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { BaseTable, defineTableColumns } from '@codex-proxy/ui'
|
||||
import { Sigma } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
import { modelSuccessRateTextClass } from '../constants'
|
||||
import AccountQuotaForecastModal from './AccountQuotaForecastModal/index.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
account: AccountRow
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
accountUpdated: [account: AccountRow]
|
||||
}>()
|
||||
const forecastOpen = ref(false)
|
||||
|
||||
type AccountModelUsage = AccountRow['usage']['models'][number]
|
||||
|
||||
const totalBilling = computed(() => props.account.usage.costs.find(cost => cost.currency.toUpperCase() === 'USD'))
|
||||
@@ -80,21 +74,9 @@ const modelUsageColumns = defineTableColumns<AccountModelUsage>([
|
||||
|
||||
<div class="min-w-0 pt-4 xl:flex xl:min-h-0 xl:flex-col xl:pt-0 xl:pl-4">
|
||||
<div class="mb-3 flex shrink-0 flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<h3 class="m-0 text-cp-lg font-heavy text-cp-text">
|
||||
模型使用排行
|
||||
</h3>
|
||||
<BaseIconButton
|
||||
v-if="account.authenticationKind !== 'api_key'"
|
||||
label="预测周/月额度"
|
||||
size="sm"
|
||||
class="h-5 w-5"
|
||||
aria-haspopup="dialog"
|
||||
@click="forecastOpen = true"
|
||||
>
|
||||
<ChartNoAxesCombined class="size-3.5" :stroke-width="1.75" />
|
||||
</BaseIconButton>
|
||||
</div>
|
||||
<h3 class="m-0 shrink-0 text-cp-lg font-heavy text-cp-text">
|
||||
模型使用排行
|
||||
</h3>
|
||||
|
||||
<div class="ml-auto flex items-baseline gap-4">
|
||||
<div v-if="hasUsageSummary" class="flex items-baseline gap-1.5 whitespace-nowrap">
|
||||
@@ -134,11 +116,4 @@ const modelUsageColumns = defineTableColumns<AccountModelUsage>([
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AccountQuotaForecastModal
|
||||
v-if="account.authenticationKind !== 'api_key'"
|
||||
v-model="forecastOpen"
|
||||
:account="account"
|
||||
@account-updated="emit('accountUpdated', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -37,7 +37,7 @@ export const accountColumns = defineTableColumns<AccountRow>([
|
||||
format: value => accountProviderLabel(typeof value === 'string' ? value : null),
|
||||
},
|
||||
{ key: 'status', label: '状态', kind: 'status', align: 'left', sortable: true },
|
||||
{ key: 'planType', label: '套餐', kind: 'status', sortable: true },
|
||||
{ key: 'planType', label: '订阅', kind: 'status', sortable: true },
|
||||
{ key: 'usage', label: '用量', kind: 'custom', size: '2xl', sortable: true },
|
||||
{ key: 'groups', label: '账号分组', kind: 'status' },
|
||||
{
|
||||
|
||||
@@ -344,13 +344,11 @@ const {
|
||||
<AccountQuotaPanel
|
||||
:account="row"
|
||||
:refreshing="refreshingQuotaAccountIds.has(row.id)"
|
||||
@account-updated="void replaceAccount($event)"
|
||||
@quota-reset="handleQuotaReset"
|
||||
@refresh-quota="handleRefreshQuota"
|
||||
/>
|
||||
<AccountUsagePanel
|
||||
:account="row"
|
||||
@account-updated="void replaceAccount($event)"
|
||||
/>
|
||||
<AccountUsagePanel :account="row" />
|
||||
</div>
|
||||
</template>
|
||||
</BaseTable>
|
||||
|
||||
@@ -30,7 +30,7 @@ const { accounts, loading, error, pagination, search, setPage, setPageSize, remo
|
||||
const columns = defineTableColumns<OutboundProxyAccount>([
|
||||
{ key: 'identity', label: '账号', kind: 'identity', size: '3xl' },
|
||||
{ key: 'provider', label: '平台/类型', kind: 'custom', size: 'md', align: 'center' },
|
||||
{ key: 'plan', label: '套餐', kind: 'custom', size: 'sm', align: 'center' },
|
||||
{ key: 'plan', label: '订阅', kind: 'custom', size: 'sm', align: 'center' },
|
||||
{ key: 'groups', label: '分组', kind: 'custom', size: 'lg', align: 'center' },
|
||||
{ key: 'actions', label: '操作', kind: 'actions', size: 'xs', align: 'center' },
|
||||
])
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BaseIconButton, BaseInput, BaseTable, BaseTableColumnSettings, BaseTabl
|
||||
import { Eye, RefreshCw, Search } from '@lucide/vue'
|
||||
import { shallowRef, toRef } from 'vue'
|
||||
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
|
||||
import AccountPlanBadge from '@/views/accounts/components/AccountPlanBadge.vue'
|
||||
import { useOpsErrorsTable } from '../composables/useOpsErrorsTable'
|
||||
import { opsErrorColumns } from '../constants'
|
||||
import { opsErrorSummary } from '../utils/opsErrorPresentation'
|
||||
@@ -152,6 +153,15 @@ function upstreamSendStateText(value: string | null | undefined) {
|
||||
:authentication-kind="row.authenticationKind"
|
||||
/>
|
||||
</template>
|
||||
<template #accountPlanType="{ row }">
|
||||
<AccountPlanBadge
|
||||
v-if="row.accountPlanType"
|
||||
:plan-type="row.accountPlanType"
|
||||
:plan-type-display="row.accountPlanTypeDisplay || row.accountPlanType"
|
||||
size="sm"
|
||||
/>
|
||||
<span v-else class="text-cp-text-quaternary">—</span>
|
||||
</template>
|
||||
<template #message="{ row }">
|
||||
<div class="min-w-0 py-0.5" :title="row.message || opsErrorSummary(row)">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
|
||||
@@ -56,7 +56,7 @@ const diagnosticColumns = computed(() => defineTableColumns<DiagnosticDisplayIte
|
||||
},
|
||||
...(resultDimension.value === 'account'
|
||||
? defineTableColumns<DiagnosticDisplayItem>([
|
||||
{ key: 'accountPlanType', label: '套餐', kind: 'status', size: 'sm', fixedWidth: true },
|
||||
{ key: 'accountPlanType', label: '订阅', kind: 'status', size: 'sm', fixedWidth: true },
|
||||
])
|
||||
: []),
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ export const usageRecordColumns = defineTableColumns<UsageDisplayRecord>([
|
||||
kind: 'identity',
|
||||
size: '3xl',
|
||||
},
|
||||
{ key: 'accountPlanType', label: '套餐', kind: 'status', size: 'sm', fixedWidth: true },
|
||||
{ key: 'accountPlanType', label: '订阅', kind: 'status', size: 'sm', fixedWidth: true },
|
||||
{
|
||||
key: 'provider',
|
||||
label: '平台/类型',
|
||||
@@ -40,6 +40,7 @@ export const usageRecordColumns = defineTableColumns<UsageDisplayRecord>([
|
||||
export const opsErrorColumns = defineTableColumns<OpsErrorRow>([
|
||||
{ key: 'clientApiKeyName', label: '密钥名称', kind: 'identity', size: 'xl' },
|
||||
{ key: 'accountId', label: '账号', kind: 'identity', size: '3xl', emptyText: '未知账号', hideable: false },
|
||||
{ key: 'accountPlanType', label: '订阅', kind: 'status', size: 'sm', fixedWidth: true },
|
||||
{ key: 'provider', label: '平台/类型', kind: 'custom', size: 'sm' },
|
||||
{ key: 'message', label: '错误', kind: 'custom', size: '4xl', hideable: false },
|
||||
{ key: 'upstreamSendState', label: '发送状态', kind: 'custom', size: 'xl' },
|
||||
|
||||
Reference in New Issue
Block a user