1
0
forked from boos/BoosAPI

[codex] polish settings transport and probe cards (#427)

* polish settings transport and probe cards

* restore compact responses fallback control
This commit is contained in:
Cita
2026-04-05 21:47:49 +08:00
committed by GitHub
parent d5560fb036
commit c10b1e7bca
3 changed files with 265 additions and 51 deletions
+245 -51
View File
@@ -1,11 +1,11 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../api.js'; import { api } from '../api.js';
import { useToast } from '../components/Toast.js'; import { useToast } from '../components/Toast.js';
import { useIsMobile } from '../components/useIsMobile.js'; import { useIsMobile } from '../components/useIsMobile.js';
import ChangeKeyModal from '../components/ChangeKeyModal.js'; import ChangeKeyModal from '../components/ChangeKeyModal.js';
import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js'; import { useAnimatedVisibility } from '../components/useAnimatedVisibility.js';
import ModernSelect from '../components/ModernSelect.js'; import ModernSelect from '../components/ModernSelect.js';
import ResponsiveFormGrid from '../components/ResponsiveFormGrid.js';
import DownstreamApiKeyModal from './settings/DownstreamApiKeyModal.js'; import DownstreamApiKeyModal from './settings/DownstreamApiKeyModal.js';
import FactoryResetModal from './settings/FactoryResetModal.js'; import FactoryResetModal from './settings/FactoryResetModal.js';
import ModelAvailabilityProbeConfirmModal from './settings/ModelAvailabilityProbeConfirmModal.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 DbDialect = 'sqlite' | 'mysql' | 'postgres';
type RouteCooldownUnit = typeof ROUTE_COOLDOWN_UNIT_OPTIONS[number]['value']; type RouteCooldownUnit = typeof ROUTE_COOLDOWN_UNIT_OPTIONS[number]['value'];
type SettingsPillTone = 'neutral' | 'primary' | 'danger' | 'warning';
type RuntimeSettings = { type RuntimeSettings = {
checkinCron: string; checkinCron: string;
@@ -229,7 +230,6 @@ function toRouteCooldownSeconds(value: number, unit: RouteCooldownUnit): number
} }
export default function Settings() { export default function Settings() {
const navigate = useNavigate();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [runtime, setRuntime] = useState<RuntimeSettings>({ const [runtime, setRuntime] = useState<RuntimeSettings>({
checkinCron: '0 8 * * *', checkinCron: '0 8 * * *',
@@ -418,6 +418,147 @@ export default function Settings() {
background: 'var(--color-bg)', background: 'var(--color-bg)',
color: 'var(--color-text-primary)', 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<SettingsPillTone, React.CSSProperties> = {
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 => { const toDateTimeLocal = (isoString: string | null | undefined): string => {
if (!isoString) return ''; if (!isoString) return '';
@@ -1429,34 +1570,54 @@ export default function Settings() {
</div> </div>
</div> </div>
<div className="card animate-slide-up stagger-4" style={{ padding: 20 }}> <div className="card animate-slide-up stagger-4" style={settingsModernCardStyle} data-settings-card="proxy-transport">
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>Codex 上游传输与会话并发</div> <div style={settingsModernHeaderStyle}>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}> <div style={settingsModernTitleBlockStyle}>
默认采用 HTTP 优先。只有这里开启后,metapi 才会在 Codex 请求上尝试把上游升级为 WebSocket。 <div style={settingsModernTitleStyle}>Codex 上游传输与会话并发</div>
下游 Codex 客户端也必须同时启用 `/v1/responses` websocket,单开这里不会生效。 <div style={settingsModernDescriptionStyle}>
默认采用 HTTP 优先。只有这里开启后,metapi 才会在 Codex 请求上尝试把上游升级为 WebSocket。下游 Codex 客户端也必须同时启用 `/v1/responses` websocket,单开这里不会生效。
</div>
</div>
<div style={settingsModernPillRowStyle}>
<span style={getSettingsPillStyle(runtime.codexUpstreamWebsocketEnabled ? 'primary' : 'neutral')}>
{proxyTransportModeLabel}
</span>
<span style={getSettingsPillStyle('neutral')}>
{proxyTransportQueueLabel}
</span>
</div>
</div> </div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}> <label style={settingsModernToggleStyle}>
从旧版本升级时,原先账号 `extraConfig.websockets` 的行为不再单独生效;现在统一以这里的全局设置和下游客户端是否开启为准。 <div style={settingsModernToggleCopyStyle}>
</div> <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-secondary)' }}>允许 metapi 到 Codex 上游使用 WebSocket</span>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--color-text-secondary)', marginBottom: 14 }}> <span style={{ fontSize: 12, lineHeight: 1.7, color: 'var(--color-text-muted)' }}>
仅在下游 Codex 客户端已同步开启 `/v1/responses` websocket 时启用;否则仍按 HTTP 优先执行。
</span>
</div>
<input <input
type="checkbox" type="checkbox"
checked={runtime.codexUpstreamWebsocketEnabled} checked={runtime.codexUpstreamWebsocketEnabled}
onChange={(e) => setRuntime((prev) => ({ ...prev, codexUpstreamWebsocketEnabled: e.target.checked }))} onChange={(e) => setRuntime((prev) => ({ ...prev, codexUpstreamWebsocketEnabled: e.target.checked }))}
style={{ width: 16, height: 16, marginTop: 2, flexShrink: 0 }}
/> />
允许 metapi 到 Codex 上游使用 WebSocket
</label> </label>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--color-text-secondary)', marginBottom: 14 }}> <label style={settingsModernToggleStyle}>
<div style={settingsModernToggleCopyStyle}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-secondary)' }}>Compact 明确不支持时回退到普通 Responses</span>
<span style={{ fontSize: 12, lineHeight: 1.7, color: 'var(--color-text-muted)' }}>
仅对 `/v1/responses/compact` 生效。当上游明确返回 compact 不支持时,允许自动回退到普通 `/responses`。
</span>
</div>
<input <input
type="checkbox" type="checkbox"
checked={runtime.responsesCompactFallbackToResponsesEnabled} checked={runtime.responsesCompactFallbackToResponsesEnabled}
onChange={(e) => setRuntime((prev) => ({ ...prev, responsesCompactFallbackToResponsesEnabled: e.target.checked }))} onChange={(e) => setRuntime((prev) => ({ ...prev, responsesCompactFallbackToResponsesEnabled: e.target.checked }))}
style={{ width: 16, height: 16, marginTop: 2, flexShrink: 0 }}
/> />
Compact 明确不支持时回退到普通 Responses
</label> </label>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 10 }}> <ResponsiveFormGrid columns={2}>
<div> <div style={settingsModernFieldCardStyle}>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>会话通道并发上限</div> <div style={settingsModernFieldLabelStyle}>会话通道并发上限</div>
<input <input
type="number" type="number"
min={0} min={0}
@@ -1472,9 +1633,12 @@ export default function Settings() {
}} }}
style={inputStyle} style={inputStyle}
/> />
<div style={settingsModernFieldHintStyle}>
只作用于能识别稳定 `session_id` 的会话型请求;普通请求不会进入这组 lease 池。
</div>
</div> </div>
<div> <div style={settingsModernFieldCardStyle}>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>排队等待时间(毫秒)</div> <div style={settingsModernFieldLabelStyle}>排队等待时间(毫秒)</div>
<input <input
type="number" type="number"
min={0} min={0}
@@ -1491,41 +1655,83 @@ export default function Settings() {
}} }}
style={inputStyle} style={inputStyle}
/> />
<div style={settingsModernFieldHintStyle}>
超过该时间仍拿不到会话通道时,本次请求会直接放弃排队,避免长期挂起。
</div>
</div> </div>
</div> </ResponsiveFormGrid>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}> <div style={settingsModernActionsStyle}>
这组 lease 只作用于能识别稳定 `session_id` 的会话型请求;没有稳定会话标识的普通请求不会进入这个池。
</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}>
默认只走原生 `/responses/compact`。开启回退后,只有当上游明确返回 compact 不支持或路径不存在时,才会退回普通 `/responses`。
</div>
<div>
<button onClick={saveProxyTransportSettings} disabled={savingProxyTransport} className="btn btn-primary"> <button onClick={saveProxyTransportSettings} disabled={savingProxyTransport} className="btn btn-primary">
{savingProxyTransport ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存传输与并发'} {savingProxyTransport ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存传输与并发'}
</button> </button>
</div> </div>
</div> </div>
<div className="card animate-slide-up stagger-4" style={{ padding: 20, border: '1px solid color-mix(in srgb, var(--color-danger) 24%, var(--color-border))' }}> <div className="card animate-slide-up stagger-4" style={settingsModernDangerCardStyle} data-settings-card="model-availability-probe">
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8, color: 'var(--color-danger)' }}>批量测活</div> <div style={settingsModernHeaderStyle}>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.8 }}> <div style={settingsModernTitleBlockStyle}>
默认关闭。开启后,metapi 会在后台定时对活跃账号模型发送最小化探测请求,用来校正“/models 能看到但实际不可用”的假阳性。 <div style={{ ...settingsModernTitleStyle, color: 'var(--color-danger)' }}>批量测活</div>
<div style={settingsModernDescriptionStyle}>
默认关闭。开启后,metapi 会在后台定时对活跃账号模型发送最小化探测请求,用来校正“/models 能看到但实际不可用”的假阳性。
</div>
</div>
<div style={settingsModernPillRowStyle}>
<span style={getSettingsPillStyle(modelAvailabilityProbeStatusTone)}>
{modelAvailabilityProbeStatusLabel}
</span>
<span style={getSettingsPillStyle('danger')}>
高风险操作
</span>
</div>
</div> </div>
<div style={{ padding: 12, borderRadius: 'var(--radius-sm)', background: 'var(--color-danger-bg)', color: 'var(--color-danger)', fontSize: 12, lineHeight: 1.8, marginBottom: 12 }}> <div
只有在你确认自己使用的中转站明确允许批量测活时才应该开启。若上游不允许,这类探测可能带来封号或风控风险。 style={{
...settingsModernCalloutStyle,
borderColor: 'color-mix(in srgb, var(--color-danger) 18%, var(--color-border-light))',
background: 'color-mix(in srgb, var(--color-danger-soft) 38%, var(--color-bg-card))',
}}
>
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-danger)' }}>风险提示</div>
<div style={{ fontSize: 12, lineHeight: 1.75, color: 'var(--color-text-secondary)' }}>
只有在你确认自己使用的中转站明确允许批量测活时才应该开启。若上游不允许,这类探测可能带来封号或风控风险。
</div>
</div> </div>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--color-text-secondary)', marginBottom: 12 }}> <label style={settingsModernToggleStyle}>
<div style={settingsModernToggleCopyStyle}>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-secondary)' }}>允许 metapi 后台主动批量测活</span>
<span style={{ fontSize: 12, lineHeight: 1.7, color: 'var(--color-text-muted)' }}>
首次从关闭切换到开启时,需要手动输入确认语句;关闭时可直接保存。
</span>
</div>
<input <input
type="checkbox" type="checkbox"
checked={runtime.modelAvailabilityProbeEnabled} checked={runtime.modelAvailabilityProbeEnabled}
onChange={(e) => setRuntime((prev) => ({ ...prev, modelAvailabilityProbeEnabled: e.target.checked }))} onChange={(e) => setRuntime((prev) => ({ ...prev, modelAvailabilityProbeEnabled: e.target.checked }))}
style={{ width: 16, height: 16, marginTop: 2, flexShrink: 0 }}
/> />
允许 metapi 后台主动批量测活
</label> </label>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.7, marginBottom: 12 }}> <ResponsiveFormGrid columns={2}>
当前状态:{savedModelAvailabilityProbeEnabled ? '已启用' : '已关闭'}。首次开启必须手动输入确认语句。 <div style={settingsModernFieldCardStyle}>
</div> <div style={settingsModernFieldLabelStyle}>当前生效状态</div>
<div> <div style={settingsModernPillRowStyle}>
<span style={getSettingsPillStyle(modelAvailabilityProbeStatusTone)}>
{modelAvailabilityProbeStatusLabel}
</span>
</div>
<div style={settingsModernFieldHintStyle}>
{savedModelAvailabilityProbeEnabled
? '后台会定时执行最小化探测请求,用于校正模型可用性。'
: '后台不会主动发起模型可用性探测请求。'}
</div>
</div>
<div style={settingsModernFieldCardStyle}>
<div style={settingsModernFieldLabelStyle}>启用门槛</div>
<div style={{ ...settingsModernFieldHintStyle, marginTop: 0 }}>
首次开启必须手动输入确认语句,避免误把高风险探测当成普通开关。
</div>
</div>
</ResponsiveFormGrid>
<div style={settingsModernActionsStyle}>
<button onClick={saveModelAvailabilityProbeSettings} disabled={savingModelAvailabilityProbe} className="btn btn-primary"> <button onClick={saveModelAvailabilityProbeSettings} disabled={savingModelAvailabilityProbe} className="btn btn-primary">
{savingModelAvailabilityProbe ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存批量测活设置'} {savingModelAvailabilityProbe ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存批量测活设置'}
</button> </button>
@@ -1625,18 +1831,6 @@ export default function Settings() {
</button> </button>
</div> </div>
<div className="card animate-slide-up stagger-5" style={{ padding: 20 }}>
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>下游密钥管理入口已迁移</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.8 }}>
下游 API Key 的新增、编辑、模型白名单、群组限制、趋势与用量分析,现统一收口到「控制台 / 下游密钥」页面,设置页不再保留重复管理入口。
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button onClick={() => navigate('/downstream-keys')} className="btn btn-primary">
打开下游密钥管理页
</button>
</div>
</div>
<div className="card animate-slide-up stagger-5" style={{ padding: 20 }}> <div className="card animate-slide-up stagger-5" style={{ padding: 20 }}>
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>路由策略</div> <div style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>路由策略</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}> <div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
@@ -96,6 +96,13 @@ describe('Settings model availability probe confirmation', () => {
}); });
await flushMicrotasks(); await flushMicrotasks();
const probeCard = root.root.find((node) => (
node.type === 'div'
&& node.props['data-settings-card'] === 'model-availability-probe'
));
expect(collectText(probeCard)).toContain('已关闭');
expect(collectText(probeCard)).toContain('高风险操作');
const toggleLabel = root.root.find((node) => ( const toggleLabel = root.root.find((node) => (
node.type === 'label' node.type === 'label'
&& collectText(node).includes('允许 metapi 后台主动批量测活') && collectText(node).includes('允许 metapi 后台主动批量测活')
@@ -107,6 +114,8 @@ describe('Settings model availability probe confirmation', () => {
toggle.props.onChange({ target: { checked: true } }); toggle.props.onChange({ target: { checked: true } });
}); });
expect(collectText(probeCard)).toContain('待保存');
const saveButton = root.root.find((node) => ( const saveButton = root.root.find((node) => (
node.type === 'button' node.type === 'button'
&& typeof node.props.onClick === 'function' && typeof node.props.onClick === 'function'
@@ -100,12 +100,20 @@ describe('Settings proxy transport', () => {
}); });
await flushMicrotasks(); await flushMicrotasks();
const proxyTransportCard = root.root.find((node) => (
node.type === 'div'
&& node.props['data-settings-card'] === 'proxy-transport'
));
expect(collectText(proxyTransportCard)).toContain('HTTP 优先');
expect(collectText(proxyTransportCard)).toContain('会话池 4 并发 / 3200ms');
const websocketToggleLabel = root.root.find((node) => ( const websocketToggleLabel = root.root.find((node) => (
node.type === 'label' node.type === 'label'
&& collectText(node).includes('允许 metapi 到 Codex 上游使用 WebSocket') && collectText(node).includes('允许 metapi 到 Codex 上游使用 WebSocket')
)); ));
const websocketToggle = websocketToggleLabel.findByType('input'); const websocketToggle = websocketToggleLabel.findByType('input');
expect(websocketToggle.props.checked).toBe(false); expect(websocketToggle.props.checked).toBe(false);
const compactFallbackToggleLabel = root.root.find((node) => ( const compactFallbackToggleLabel = root.root.find((node) => (
node.type === 'label' node.type === 'label'
&& collectText(node).includes('Compact 明确不支持时回退到普通 Responses') && collectText(node).includes('Compact 明确不支持时回退到普通 Responses')
@@ -131,6 +139,9 @@ describe('Settings proxy transport', () => {
queueWaitInput.props.onChange({ target: { value: '4200' } }); queueWaitInput.props.onChange({ target: { value: '4200' } });
}); });
expect(collectText(proxyTransportCard)).toContain('上游 WebSocket 已启用');
expect(collectText(proxyTransportCard)).toContain('会话池 6 并发 / 4200ms');
const saveButton = root.root.find((node) => ( const saveButton = root.root.find((node) => (
node.type === 'button' node.type === 'button'
&& typeof node.props.onClick === 'function' && typeof node.props.onClick === 'function'