mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
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 <noreply@openai.com>
This commit is contained in:
+1
-1
@@ -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 包的构建脚本;升级依赖时核对源码并更新锁文件,无需逐次修改构建许可,不开放全部依赖脚本。
|
||||
|
||||
|
||||
@@ -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<DecimalAmount>,
|
||||
pub attempt_count: u64,
|
||||
pub total_tokens: u64,
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
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<gateway_core::metering::Decimal> {
|
||||
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<u64>,
|
||||
) -> 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<DecimalAmount> {
|
||||
costs
|
||||
.iter()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String>,
|
||||
pub attempt_count: u64,
|
||||
pub total_tokens: u64,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
+2
-1
@@ -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`,修改备注不改变历史请求的账号归属。
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+12
-6
@@ -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
|
||||
|
||||
@@ -448,7 +448,6 @@ export interface UsageDiagnosticItem {
|
||||
nonCompletionRate: number
|
||||
retryCount: number
|
||||
retryRate: number
|
||||
impactScore: number
|
||||
estimatedCost: string | null
|
||||
attemptCount: number
|
||||
totalTokens: number
|
||||
|
||||
@@ -460,11 +460,16 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<ChevronDown
|
||||
<span
|
||||
v-if="!isCollapsed"
|
||||
class="sidebar-label size-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none"
|
||||
:class="pluginGroupExpanded ? undefined : '-rotate-90'"
|
||||
/>
|
||||
class="sidebar-label inline-flex size-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<ChevronDown
|
||||
class="size-4 transition-transform duration-200 motion-reduce:transition-none"
|
||||
:class="pluginGroupExpanded ? undefined : '-rotate-90'"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
|
||||
@@ -44,15 +44,9 @@ const diagnosticColumns = defineTableColumns<DiagnosticDisplayItem>([
|
||||
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<DiagnosticDisplayItem>([
|
||||
},
|
||||
])
|
||||
|
||||
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<typeof diagnosticNameDisplay>
|
||||
}
|
||||
|
||||
const displayItems = computed<DiagnosticDisplayItem[]>(() =>
|
||||
sortedItems.value.map(item => ({
|
||||
props.diagnostics.items.map(item => ({
|
||||
...item,
|
||||
nameDisplay: diagnosticNameDisplay(item.name),
|
||||
})),
|
||||
@@ -113,7 +97,7 @@ function diagnosticNameDisplay(name: string) {
|
||||
<BaseCard
|
||||
as="article"
|
||||
title="热点诊断"
|
||||
:description="`按${selectedDimensionLabel}定位高影响请求`"
|
||||
:description="`按${resultDimensionLabel}聚合`"
|
||||
class="h-105 min-h-105 max-h-105 min-w-0 w-full lg:h-full lg:min-h-90 lg:max-h-105"
|
||||
>
|
||||
<template #actions>
|
||||
@@ -178,32 +162,17 @@ function diagnosticNameDisplay(name: string) {
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #impactScore="{ row }">
|
||||
<template #errorCount="{ row }">
|
||||
<strong
|
||||
class="font-mono font-bold tabular-nums"
|
||||
:class="row.impactScore > 0.1 ? 'text-cp-error-text' : 'text-cp-text-secondary'"
|
||||
title="综合错误、未完成、重试、请求占比与 TTFT 的风险分(0–100)"
|
||||
:class="row.errorCount > 0 ? 'text-cp-error-text' : 'text-cp-text-quaternary'"
|
||||
:aria-label="`失败 ${formatCompactNumber(row.errorCount)} 次`"
|
||||
:title="`失败率 ${formatPercent(row.errorRate)}`"
|
||||
>
|
||||
{{ formatCompactNumber(row.impactScore * 100) }}
|
||||
{{ formatCompactNumber(row.errorCount) }}
|
||||
</strong>
|
||||
</template>
|
||||
|
||||
<template #errorCount="{ row }">
|
||||
<div
|
||||
class="flex items-center justify-end gap-1.5 whitespace-nowrap text-right font-mono leading-none tabular-nums"
|
||||
:aria-label="`错误 ${formatCompactNumber(row.errorCount)},未完成 ${formatCompactNumber(row.nonCompletionCount)}`"
|
||||
:title="`错误率 ${formatPercent(row.errorRate)},未完成率 ${formatPercent(row.nonCompletionRate)}`"
|
||||
>
|
||||
<strong :class="row.errorCount > 0 ? 'text-cp-error-text' : 'text-cp-text-quaternary'">
|
||||
{{ formatCompactNumber(row.errorCount) }}
|
||||
</strong>
|
||||
<span class="text-[9px] text-cp-text-quaternary">/</span>
|
||||
<strong :class="row.nonCompletionCount > 0 ? 'text-cp-orange-text' : 'text-cp-text-quaternary'">
|
||||
{{ formatCompactNumber(row.nonCompletionCount) }}
|
||||
</strong>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #firstTokenP95Ms="{ row }">
|
||||
<div
|
||||
class="grid justify-items-end gap-1 font-mono leading-none tabular-nums"
|
||||
|
||||
+4
-16
@@ -1,4 +1,5 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import CodexProxyUI from '@codex-proxy/ui/vite'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
@@ -6,28 +7,15 @@ import { defineConfig } from 'vite'
|
||||
export default defineConfig(({ mode }) => {
|
||||
const sourceUi = mode === 'source'
|
||||
const uiRoot = new URL('../modules/ui/', import.meta.url)
|
||||
|
||||
return {
|
||||
base: '/',
|
||||
plugins: [vue(), tailwindcss()],
|
||||
plugins: [vue(), tailwindcss(), CodexProxyUI({ source: sourceUi ? uiRoot : undefined })],
|
||||
resolve: {
|
||||
dedupe: ['vue', '@lucide/vue'],
|
||||
// 源码联调显式启用;普通开发与发行构建继续使用锁定的包。
|
||||
alias: [
|
||||
{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },
|
||||
...(sourceUi
|
||||
? [
|
||||
{ find: /^@codex-proxy\/ui$/, replacement: fileURLToPath(new URL('src/index.ts', uiRoot)) },
|
||||
{ find: '@codex-proxy/ui/theme', replacement: fileURLToPath(new URL('src/theme/index.ts', uiRoot)) },
|
||||
{ find: '@codex-proxy/ui/styles.css', replacement: fileURLToPath(new URL('src/styles/index.css', uiRoot)) },
|
||||
{ find: '@codex-proxy/ui/tailwind.css', replacement: fileURLToPath(new URL('src/styles/tailwind.css', uiRoot)) },
|
||||
{ find: /^@codex-proxy\/ui\/(.+)$/, replacement: fileURLToPath(new URL('src/components/$1/index.ts', uiRoot)) },
|
||||
]
|
||||
: []),
|
||||
],
|
||||
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
fs: sourceUi ? { allow: [fileURLToPath(new URL('.', import.meta.url)), fileURLToPath(uiRoot)] } : undefined,
|
||||
proxy: {
|
||||
'/dev': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
|
||||
+1
-1
Submodule modules/plugins updated: c8cb953c8b...a5abda05be
+1
-1
Submodule modules/ui updated: 3e4af442ec...e1d4a164e7
Reference in New Issue
Block a user