From 2d20f03bcb4f004356f1ab86845e9e6cffda7cad Mon Sep 17 00:00:00 2001 From: Zyy Date: Sat, 26 Sep 2026 17:14:54 +0800 Subject: [PATCH] refactor(admin): simplify diagnostics and refresh shared UI integration Adopt UI v0.3.0 and its shared Vite adapter, preserve request-volume ordering in diagnostics, stabilize sidebar animation, and track workbench v0.1.15. Co-authored-by: Codex --- CONTRIBUTING.md | 2 +- .../gateway-admin/src/model/observability.rs | 1 - .../src/use_case/observability.rs | 36 +------------- .../tests/use_case/observability.rs | 26 +++++++++- .../src/admin/observability/presenter.rs | 1 - .../src/admin/observability/wire.rs | 1 - .../tests/admin/observability/response.rs | 1 + docs/api.md | 3 +- docs/development.md | 2 +- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 18 ++++--- frontend/src/api/modules/usage.ts | 1 - frontend/src/layout/components/AppSidebar.vue | 13 +++-- .../usage/components/UsageDiagnosticCard.vue | 47 ++++--------------- frontend/vite.config.ts | 20 ++------ modules/plugins | 2 +- modules/ui | 2 +- 17 files changed, 67 insertions(+), 111 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3af7ae4e3..bb20ce0ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,7 +148,7 @@ AI 进行前端开发时,加载并使用 `$frontend-design` 技能。设计方 | 依赖安全 | Cargo Audit、pnpm Audit | 本次变更的权限、数据与信任边界 | | 工作流与容器 | Actionlint、相关容器检查 | 改动涉及的运行行为 | -UI 与示例源码分别在独立的 `codex-proxy-ui`、`codex-proxy-plugins` 仓库维护。前端的依赖、锁文件和 ESLint 配置均在 `frontend/` 管理,仓库根目录不维护 Node 包。UI 使用固定 GitHub 标签或提交依赖,锁文件记录实际提交、源码归档地址及完整性摘要,常规开发和 CI 不需要检出同级仓库。安装时 pnpm 按 UI 自身锁文件准备构建依赖,再执行 `prepack` 生成 JS、CSS 和类型声明。开发服务通过 UI 包的 `development` 入口使用 Vue SFC,生产构建使用 `dist`。 +UI 与示例源码分别在独立的 `codex-proxy-ui`、`codex-proxy-plugins` 仓库维护。前端的依赖、锁文件和 ESLint 配置均在 `frontend/` 管理,仓库根目录不维护 Node 包。UI 使用固定 GitHub 标签或提交依赖,锁文件记录实际提交、源码归档地址及完整性摘要,常规开发和 CI 不需要检出同级仓库。安装时 pnpm 按 UI 自身锁文件准备构建依赖,再执行 `prepack` 生成 JS、CSS 和类型声明。普通开发与生产构建都使用 UI 编译产物,源码联调通过 `@codex-proxy/ui/vite` 显式开启。 pnpm 版本由 `frontend/package.json` 的 `packageManager` 固定,CI 和 Docker 从该字段读取。`frontend/pnpm-workspace.yaml` 的 `allowBuilds` 按仓库放行自有 UI 包的构建脚本;升级依赖时核对源码并更新锁文件,无需逐次修改构建许可,不开放全部依赖脚本。 diff --git a/backend/crates/gateway-admin/src/model/observability.rs b/backend/crates/gateway-admin/src/model/observability.rs index 717341444..626d00957 100644 --- a/backend/crates/gateway-admin/src/model/observability.rs +++ b/backend/crates/gateway-admin/src/model/observability.rs @@ -1154,7 +1154,6 @@ pub struct DiagnosticsItem { pub non_completion_rate: f64, pub retry_count: u64, pub retry_rate: f64, - pub impact_score: f64, pub estimated_cost: Option, pub attempt_count: u64, pub total_tokens: u64, diff --git a/backend/crates/gateway-admin/src/use_case/observability.rs b/backend/crates/gateway-admin/src/use_case/observability.rs index 0eb6c913a..f4e609a09 100644 --- a/backend/crates/gateway-admin/src/use_case/observability.rs +++ b/backend/crates/gateway-admin/src/use_case/observability.rs @@ -342,21 +342,13 @@ impl ObservabilityService for DefaultObservabilityService { let total_requests = items.iter().fold(0_u64, |total, item| { total.saturating_add(item.request_count) }); - let mut items = items + let items = items .into_iter() .map(|item| { let error_rate = rate_or_zero(item.failure_count, item.request_count); let non_completion_rate = rate_or_zero(item.non_completion_count, item.request_count); let retry_rate = rate_or_zero(item.retry_count, item.request_count); - let impact_score = diagnostic_impact_score( - item.request_count, - total_requests, - error_rate, - non_completion_rate, - retry_rate, - item.first_token_p95_ms, - ); DiagnosticsItem { key: item.key, name: item.name, @@ -372,19 +364,12 @@ impl ObservabilityService for DefaultObservabilityService { non_completion_rate, retry_count: item.retry_count, retry_rate, - impact_score, estimated_cost: usd_cost(&item.costs), attempt_count: item.attempt_count, total_tokens: item.total_tokens, } }) .collect::>(); - items.sort_by(|left, right| { - right - .impact_score - .total_cmp(&left.impact_score) - .then_with(|| right.request_count.cmp(&left.request_count)) - }); Ok(DiagnosticsResult { dimension, items }) } @@ -724,25 +709,6 @@ fn decimal(value: &DecimalAmount) -> Option { value.as_str().parse().ok() } -fn diagnostic_impact_score( - request_count: u64, - total_requests: u64, - error_rate: f64, - non_completion_rate: f64, - retry_rate: f64, - first_token_p95_ms: Option, -) -> f64 { - let request_share = rate_or_zero(request_count, total_requests); - let slow_score = first_token_p95_ms - .map_or(0.0, |value| value as f64 / 30_000.0) - .min(1.0); - error_rate * 0.35 - + non_completion_rate * 0.25 - + retry_rate.min(1.0) * 0.20 - + request_share * 0.10 - + slow_score * 0.10 -} - fn usd_cost(costs: &[CurrencyCost]) -> Option { costs .iter() diff --git a/backend/crates/gateway-admin/tests/use_case/observability.rs b/backend/crates/gateway-admin/tests/use_case/observability.rs index 7bd731d3f..5a0292514 100644 --- a/backend/crates/gateway-admin/tests/use_case/observability.rs +++ b/backend/crates/gateway-admin/tests/use_case/observability.rs @@ -502,7 +502,31 @@ async fn observability_services_should_calculate_usage_insights_and_diagnostic_s ); assert_eq!(diagnostics.items[0].request_share, 0.75); assert_eq!(diagnostics.items[1].request_share, 0.25); - assert!((diagnostics.items[0].impact_score - 0.075).abs() < 1e-9); +} + +#[tokio::test] +async fn diagnostics_should_preserve_request_order_when_a_small_sample_fails() { + let now = Utc::now(); + let range = observation_range(now); + let store = Arc::new(FixtureObservabilityStore::new(range)); + let success = diagnostic("popular", 100); + let mut failure = diagnostic("single_failure", 1); + failure.success_count = 0; + failure.failure_count = 1; + store.replace_diagnostics(vec![success, failure]); + let services = observability_services_with_calculated_billing(store).await; + + let result = services + .observability() + .diagnostics(range, UsageFilter::default(), DiagnosticDimension::Model) + .await + .expect("diagnostics"); + + assert_eq!(result.items[0].key, "popular"); + assert_eq!(result.items[1].key, "single_failure"); + assert_eq!(result.items[0].error_count, 0); + assert_eq!(result.items[1].error_count, 1); + assert_eq!(result.items[1].error_rate, 1.0); } #[tokio::test] diff --git a/backend/crates/gateway-api/src/admin/observability/presenter.rs b/backend/crates/gateway-api/src/admin/observability/presenter.rs index d88819220..2f48d1b30 100644 --- a/backend/crates/gateway-api/src/admin/observability/presenter.rs +++ b/backend/crates/gateway-api/src/admin/observability/presenter.rs @@ -1294,7 +1294,6 @@ pub(crate) fn diagnostics_view( non_completion_rate: item.non_completion_rate, retry_count: item.retry_count, retry_rate: item.retry_rate, - impact_score: item.impact_score, estimated_cost: item.estimated_cost.as_ref().map(ToString::to_string), attempt_count: item.attempt_count, total_tokens: item.total_tokens, diff --git a/backend/crates/gateway-api/src/admin/observability/wire.rs b/backend/crates/gateway-api/src/admin/observability/wire.rs index 26e5a0f13..bc7be88fa 100644 --- a/backend/crates/gateway-api/src/admin/observability/wire.rs +++ b/backend/crates/gateway-api/src/admin/observability/wire.rs @@ -758,7 +758,6 @@ pub struct DiagnosticItemView { pub non_completion_rate: f64, pub retry_count: u64, pub retry_rate: f64, - pub impact_score: f64, pub estimated_cost: Option, pub attempt_count: u64, pub total_tokens: u64, diff --git a/backend/crates/gateway-api/tests/admin/observability/response.rs b/backend/crates/gateway-api/tests/admin/observability/response.rs index 2d36c5d37..d3cb93ed1 100644 --- a/backend/crates/gateway-api/tests/admin/observability/response.rs +++ b/backend/crates/gateway-api/tests/admin/observability/response.rs @@ -595,6 +595,7 @@ async fn diagnostics_should_keep_stable_key_and_display_name_contract() { .await .expect("diagnostics body"); let value: serde_json::Value = serde_json::from_slice(&body).expect("diagnostics JSON"); + assert!(value["data"]["items"][0].get("impactScore").is_none()); assert_eq!( ( &value["data"]["items"][0]["key"], diff --git a/docs/api.md b/docs/api.md index af0e7e093..0784bf312 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1473,7 +1473,8 @@ Dashboard 的 `capacityInfo.maxConcurrentPerAccount` 为默认账号并发上限 用量查询可组合页码/游标、时间范围、Provider、Client Key、账号、模型、route、transport、状态码、 request/response/upstream ID、outcome 与搜索文本。诊断 `dimension` 可取 `model`、`account`、 -`apiKey`、`provider`、`transport`、`failureClass`、`status`。 +`apiKey`、`provider`、`transport`、`failureClass`、`status`。诊断按请求量降序返回最多 100 项, +同请求量按维度标识稳定排列。 管理端请求列表及 Dashboard 最近请求中的 `accountNotes` 为账号当前备注,按内部账号 ID 关联。 备注不写入请求历史快照;无备注或账号已删除时返回 `null`,修改备注不改变历史请求的账号归属。 diff --git a/docs/development.md b/docs/development.md index e166f20bc..2f9fa5717 100644 --- a/docs/development.md +++ b/docs/development.md @@ -41,7 +41,7 @@ pnpm --dir modules/ui dev # 组件库交互示 管理端沿用 `frontend/vite.config.ts` 的后端代理,需按原有方式启动网关。插件独立预览的宿主桥使用模拟数据;已安装插件读取包内静态资源,修改后仍需重新构建、打包和安装。 -`dev:source` 使用 Vite 的 `source` 模式,在各自配置中将 UI 的公开入口解析到 `modules/ui`,由 Vite 热更新。插件单独检出时仍用普通 `dev`;`source` 模式需要上述子模块目录结构。 +`dev:source` 使用 Vite 的 `source` 模式,由 `@codex-proxy/ui/vite` 的 `CodexProxyUI` 适配器将公开入口解析到 `modules/ui`,并统一处理共享依赖、源码预构建排除与开发服务的文件访问范围。入口映射以 UI 包 `exports` 中的 `codex-proxy-source` 条件为准,由 Vite 热更新。插件单独检出时仍用普通 `dev`;`source` 模式需要上述子模块目录结构。 ```bash pnpm --dir frontend build:source diff --git a/frontend/package.json b/frontend/package.json index 52ddf7d41..61dcd5218 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@boxicons/vue": "1.0.1", - "@codex-proxy/ui": "github:zyycn/codex-proxy-ui#v0.2.0", + "@codex-proxy/ui": "github:zyycn/codex-proxy-ui#v0.3.0", "@fontsource-variable/inter": "^5.3.0", "@fontsource-variable/jetbrains-mono": "^5.3.0", "@lucide/vue": "^1.47.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 86a27f88c..957ac0491 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -170,8 +170,8 @@ importers: specifier: 1.0.1 version: 1.0.1(vue@3.5.43(typescript@6.0.3)) '@codex-proxy/ui': - specifier: github:zyycn/codex-proxy-ui#v0.2.0 - version: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/3e4af442ecf44ce19d456ebbdae589f82a449f07(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vue@3.5.43(typescript@6.0.3)) + specifier: github:zyycn/codex-proxy-ui#v0.3.0 + version: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/e1d4a164e7cfe72272301ebcb1b1d9c54b786522(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.0(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.43(typescript@6.0.3)) '@fontsource-variable/inter': specifier: ^5.3.0 version: 5.3.0 @@ -378,12 +378,16 @@ packages: resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} engines: {node: '>= 20.12.0'} - '@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/3e4af442ecf44ce19d456ebbdae589f82a449f07': - resolution: {gitHosted: true, integrity: sha512-QTdvAYEK3hDybYnbctSYZGVBNRblKIj/KZoeorqplTaHVD1UZG4c27LO8+RSa3BWumExoXcJlta1HSgM7Fzd8A==, tarball: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/3e4af442ecf44ce19d456ebbdae589f82a449f07} - version: 0.2.0 + '@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/e1d4a164e7cfe72272301ebcb1b1d9c54b786522': + resolution: {gitHosted: true, integrity: sha512-I2h13qjmqABk4IviUXdiZXPU9o//qO6WGbBBvav9rXKXyEjfFXvwvz0ofa/4UWOmTRBhmcRFmEIE0qUTzHZThQ==, tarball: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/e1d4a164e7cfe72272301ebcb1b1d9c54b786522} + version: 0.3.0 engines: {node: '>=24'} peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.5.0 + peerDependenciesMeta: + vite: + optional: true '@dprint/formatter@0.5.1': resolution: {integrity: sha512-cdZUrm0iv/FnnY3CKE2dEcVhNEzrC551aE2h2mTFwQCRBrqyARLDnb7D+3PlXTUVp3s34ftlnGOVCmhLT9DeKA==} @@ -2770,7 +2774,7 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/3e4af442ecf44ce19d456ebbdae589f82a449f07(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vue@3.5.43(typescript@6.0.3))': + '@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/e1d4a164e7cfe72272301ebcb1b1d9c54b786522(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.0(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.43(typescript@6.0.3))': dependencies: '@ant-design/colors': 8.0.1 '@ant-design/fast-color': 3.0.1 @@ -2783,6 +2787,8 @@ snapshots: marked: 18.0.14 sortablejs: 1.15.7 vue: 3.5.43(typescript@6.0.3) + optionalDependencies: + vite: 8.3.0(@types/node@26.6.2)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - async-validator - axios diff --git a/frontend/src/api/modules/usage.ts b/frontend/src/api/modules/usage.ts index 2de801872..34e867cd4 100644 --- a/frontend/src/api/modules/usage.ts +++ b/frontend/src/api/modules/usage.ts @@ -448,7 +448,6 @@ export interface UsageDiagnosticItem { nonCompletionRate: number retryCount: number retryRate: number - impactScore: number estimatedCost: string | null attemptCount: number totalTokens: number diff --git a/frontend/src/layout/components/AppSidebar.vue b/frontend/src/layout/components/AppSidebar.vue index 30e63fdb1..d00c2fd15 100644 --- a/frontend/src/layout/components/AppSidebar.vue +++ b/frontend/src/layout/components/AppSidebar.vue @@ -460,11 +460,16 @@ onBeforeUnmount(() => { > {{ item.label }} - + class="sidebar-label inline-flex size-4 shrink-0" + aria-hidden="true" + > + +
([ kind: 'numeric', size: 'sm', }, - { - key: 'impactScore', - label: '风险分', - kind: 'numeric', - size: 'sm', - }, { key: 'errorCount', - label: '错误 / 未完', + label: '失败', kind: 'numeric', size: 'sm', }, @@ -65,27 +59,17 @@ const diagnosticColumns = defineTableColumns([ }, ]) -const selectedDimensionLabel = computed( - () => dimensionOptions.find(option => option.value === dimension.value)?.label ?? '维度', -) - const resultDimension = computed(() => props.diagnostics.dimension || dimension.value) const resultDimensionLabel = computed( () => dimensionOptions.find(option => option.value === resultDimension.value)?.label ?? '维度', ) -const sortedItems = computed(() => - [...props.diagnostics.items].sort( - (left, right) => right.impactScore - left.impactScore || right.requestCount - left.requestCount, - ), -) - type DiagnosticDisplayItem = Diagnostics['items'][number] & { nameDisplay: ReturnType } const displayItems = computed(() => - sortedItems.value.map(item => ({ + props.diagnostics.items.map(item => ({ ...item, nameDisplay: diagnosticNameDisplay(item.name), })), @@ -113,7 +97,7 @@ function diagnosticNameDisplay(name: string) { -