From c10b1e7bca356c7fdc08827a021e0e3addc7f8e7 Mon Sep 17 00:00:00 2001 From: Cita Date: Sun, 5 Apr 2026 21:47:49 +0800 Subject: [PATCH] [codex] polish settings transport and probe cards (#427) * polish settings transport and probe cards * restore compact responses fallback control --- src/web/pages/Settings.tsx | 296 +++++++++++++++--- ...settings.model-availability-probe.test.tsx | 9 + .../pages/settings.proxy-transport.test.tsx | 11 + 3 files changed, 265 insertions(+), 51 deletions(-) diff --git a/src/web/pages/Settings.tsx b/src/web/pages/Settings.tsx index 58dedfa..1d5f4c1 100644 --- a/src/web/pages/Settings.tsx +++ b/src/web/pages/Settings.tsx @@ -1,11 +1,11 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { api } from '../api.js'; import { useToast } from '../components/Toast.js'; import { useIsMobile } from '../components/useIsMobile.js'; import ChangeKeyModal from '../components/ChangeKeyModal.js'; import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js'; import ModernSelect from '../components/ModernSelect.js'; +import ResponsiveFormGrid from '../components/ResponsiveFormGrid.js'; import DownstreamApiKeyModal from './settings/DownstreamApiKeyModal.js'; import FactoryResetModal from './settings/FactoryResetModal.js'; import ModelAvailabilityProbeConfirmModal from './settings/ModelAvailabilityProbeConfirmModal.js'; @@ -50,6 +50,7 @@ const CHECKIN_INTERVAL_OPTIONS = Array.from({ length: 24 }, (_, index) => { }); type DbDialect = 'sqlite' | 'mysql' | 'postgres'; type RouteCooldownUnit = typeof ROUTE_COOLDOWN_UNIT_OPTIONS[number]['value']; +type SettingsPillTone = 'neutral' | 'primary' | 'danger' | 'warning'; type RuntimeSettings = { checkinCron: string; @@ -229,7 +230,6 @@ function toRouteCooldownSeconds(value: number, unit: RouteCooldownUnit): number } export default function Settings() { - const navigate = useNavigate(); const isMobile = useIsMobile(); const [runtime, setRuntime] = useState({ checkinCron: '0 8 * * *', @@ -418,6 +418,147 @@ export default function Settings() { background: 'var(--color-bg)', color: 'var(--color-text-primary)', }; + const settingsModernCardStyle: React.CSSProperties = { + padding: isMobile ? 20 : 24, + display: 'flex', + flexDirection: 'column', + gap: 16, + }; + const settingsModernDangerCardStyle: React.CSSProperties = { + ...settingsModernCardStyle, + borderColor: 'color-mix(in srgb, var(--color-danger) 22%, var(--color-border))', + background: 'linear-gradient(180deg, color-mix(in srgb, var(--color-danger-soft) 18%, var(--color-bg-card)) 0%, var(--color-bg-card) 100%)', + }; + const settingsModernHeaderStyle: React.CSSProperties = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-start', + gap: 12, + flexWrap: 'wrap', + }; + const settingsModernTitleBlockStyle: React.CSSProperties = { + display: 'grid', + gap: 6, + minWidth: 0, + }; + const settingsModernTitleStyle: React.CSSProperties = { + fontSize: 15, + fontWeight: 600, + lineHeight: 1.35, + color: 'var(--color-text-primary)', + }; + const settingsModernDescriptionStyle: React.CSSProperties = { + fontSize: 12, + lineHeight: 1.75, + color: 'var(--color-text-muted)', + }; + const settingsModernPillRowStyle: React.CSSProperties = { + display: 'flex', + flexWrap: 'wrap', + gap: 8, + }; + const settingsModernCalloutStyle: React.CSSProperties = { + display: 'grid', + gap: 6, + padding: '14px 16px', + borderRadius: 'var(--radius-md)', + border: '1px solid var(--color-border-light)', + background: 'color-mix(in srgb, var(--color-bg) 82%, var(--color-bg-card))', + }; + const settingsModernToggleStyle: React.CSSProperties = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-start', + gap: isMobile ? 12 : 16, + padding: '14px 16px', + borderRadius: 'var(--radius-md)', + border: '1px solid var(--color-border-light)', + background: 'color-mix(in srgb, var(--color-bg) 78%, var(--color-bg-card))', + cursor: 'pointer', + }; + const settingsModernToggleCopyStyle: React.CSSProperties = { + display: 'grid', + gap: 6, + minWidth: 0, + }; + const settingsModernFieldCardStyle: React.CSSProperties = { + display: 'grid', + gap: 10, + padding: '14px 16px', + borderRadius: 'var(--radius-md)', + border: '1px solid var(--color-border-light)', + background: 'color-mix(in srgb, var(--color-bg) 82%, var(--color-bg-card))', + }; + const settingsModernFieldLabelStyle: React.CSSProperties = { + fontSize: 12, + fontWeight: 600, + color: 'var(--color-text-secondary)', + }; + const settingsModernFieldHintStyle: React.CSSProperties = { + fontSize: 12, + lineHeight: 1.7, + color: 'var(--color-text-muted)', + marginTop: -2, + }; + const settingsModernActionsStyle: React.CSSProperties = { + display: 'flex', + flexWrap: 'wrap', + alignItems: 'center', + gap: 10, + }; + + const getSettingsPillStyle = (tone: SettingsPillTone): React.CSSProperties => { + const toneStyles: Record = { + neutral: { + borderColor: 'color-mix(in srgb, var(--color-text-muted) 12%, var(--color-border-light))', + background: 'color-mix(in srgb, var(--color-text-muted) 8%, var(--color-bg-card))', + color: 'var(--color-text-secondary)', + }, + primary: { + borderColor: 'color-mix(in srgb, var(--color-primary) 20%, var(--color-border-light))', + background: 'color-mix(in srgb, var(--color-primary-light) 64%, var(--color-bg-card))', + color: 'var(--color-primary)', + }, + warning: { + borderColor: 'color-mix(in srgb, var(--color-warning) 20%, var(--color-border-light))', + background: 'color-mix(in srgb, var(--color-warning-soft) 68%, var(--color-bg-card))', + color: 'var(--color-warning)', + }, + danger: { + borderColor: 'color-mix(in srgb, var(--color-danger) 20%, var(--color-border-light))', + background: 'color-mix(in srgb, var(--color-danger-soft) 66%, var(--color-bg-card))', + color: 'var(--color-danger)', + }, + }; + + return { + display: 'inline-flex', + alignItems: 'center', + gap: 6, + padding: '5px 10px', + borderRadius: 999, + border: '1px solid var(--color-border-light)', + fontSize: 12, + fontWeight: 600, + lineHeight: 1.2, + whiteSpace: 'nowrap', + ...toneStyles[tone], + }; + }; + + const proxyTransportModeLabel = runtime.codexUpstreamWebsocketEnabled ? '上游 WebSocket 已启用' : 'HTTP 优先'; + const proxyTransportQueueLabel = `会话池 ${runtime.proxySessionChannelConcurrencyLimit} 并发 / ${runtime.proxySessionChannelQueueWaitMs}ms`; + const modelAvailabilityProbeDirty = runtime.modelAvailabilityProbeEnabled !== savedModelAvailabilityProbeEnabled; + const modelAvailabilityProbeStatusTone: SettingsPillTone = modelAvailabilityProbeDirty + ? 'warning' + : savedModelAvailabilityProbeEnabled + ? 'danger' + : 'neutral'; + const modelAvailabilityProbeStatusLabel = modelAvailabilityProbeDirty + ? '待保存' + : savedModelAvailabilityProbeEnabled + ? '已启用' + : '已关闭'; const toDateTimeLocal = (isoString: string | null | undefined): string => { if (!isoString) return ''; @@ -1429,34 +1570,54 @@ export default function Settings() { -
-
Codex 上游传输与会话并发
-
- 默认采用 HTTP 优先。只有这里开启后,metapi 才会在 Codex 请求上尝试把上游升级为 WebSocket。 - 下游 Codex 客户端也必须同时启用 `/v1/responses` websocket,单开这里不会生效。 +
+
+
+
Codex 上游传输与会话并发
+
+ 默认采用 HTTP 优先。只有这里开启后,metapi 才会在 Codex 请求上尝试把上游升级为 WebSocket。下游 Codex 客户端也必须同时启用 `/v1/responses` websocket,单开这里不会生效。 +
+
+
+ + {proxyTransportModeLabel} + + + {proxyTransportQueueLabel} + +
-
- 从旧版本升级时,原先账号 `extraConfig.websockets` 的行为不再单独生效;现在统一以这里的全局设置和下游客户端是否开启为准。 -
-