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:
+240
-46
@@ -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<RuntimeSettings>({
|
||||
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<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 => {
|
||||
if (!isoString) return '';
|
||||
@@ -1429,34 +1570,54 @@ export default function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card animate-slide-up stagger-4" style={{ padding: 20 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>Codex 上游传输与会话并发</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}>
|
||||
默认采用 HTTP 优先。只有这里开启后,metapi 才会在 Codex 请求上尝试把上游升级为 WebSocket。
|
||||
下游 Codex 客户端也必须同时启用 `/v1/responses` websocket,单开这里不会生效。
|
||||
<div className="card animate-slide-up stagger-4" style={settingsModernCardStyle} data-settings-card="proxy-transport">
|
||||
<div style={settingsModernHeaderStyle}>
|
||||
<div style={settingsModernTitleBlockStyle}>
|
||||
<div style={settingsModernTitleStyle}>Codex 上游传输与会话并发</div>
|
||||
<div style={settingsModernDescriptionStyle}>
|
||||
默认采用 HTTP 优先。只有这里开启后,metapi 才会在 Codex 请求上尝试把上游升级为 WebSocket。下游 Codex 客户端也必须同时启用 `/v1/responses` websocket,单开这里不会生效。
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}>
|
||||
从旧版本升级时,原先账号 `extraConfig.websockets` 的行为不再单独生效;现在统一以这里的全局设置和下游客户端是否开启为准。
|
||||
</div>
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--color-text-secondary)', marginBottom: 14 }}>
|
||||
<div style={settingsModernPillRowStyle}>
|
||||
<span style={getSettingsPillStyle(runtime.codexUpstreamWebsocketEnabled ? 'primary' : 'neutral')}>
|
||||
{proxyTransportModeLabel}
|
||||
</span>
|
||||
<span style={getSettingsPillStyle('neutral')}>
|
||||
{proxyTransportQueueLabel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<label style={settingsModernToggleStyle}>
|
||||
<div style={settingsModernToggleCopyStyle}>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text-secondary)' }}>允许 metapi 到 Codex 上游使用 WebSocket</span>
|
||||
<span style={{ fontSize: 12, lineHeight: 1.7, color: 'var(--color-text-muted)' }}>
|
||||
仅在下游 Codex 客户端已同步开启 `/v1/responses` websocket 时启用;否则仍按 HTTP 优先执行。
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={runtime.codexUpstreamWebsocketEnabled}
|
||||
onChange={(e) => setRuntime((prev) => ({ ...prev, codexUpstreamWebsocketEnabled: e.target.checked }))}
|
||||
style={{ width: 16, height: 16, marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
允许 metapi 到 Codex 上游使用 WebSocket
|
||||
</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
|
||||
type="checkbox"
|
||||
checked={runtime.responsesCompactFallbackToResponsesEnabled}
|
||||
onChange={(e) => setRuntime((prev) => ({ ...prev, responsesCompactFallbackToResponsesEnabled: e.target.checked }))}
|
||||
style={{ width: 16, height: 16, marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
Compact 明确不支持时回退到普通 Responses
|
||||
</label>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 10 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>会话通道并发上限</div>
|
||||
<ResponsiveFormGrid columns={2}>
|
||||
<div style={settingsModernFieldCardStyle}>
|
||||
<div style={settingsModernFieldLabelStyle}>会话通道并发上限</div>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
@@ -1472,9 +1633,12 @@ export default function Settings() {
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
<div style={settingsModernFieldHintStyle}>
|
||||
只作用于能识别稳定 `session_id` 的会话型请求;普通请求不会进入这组 lease 池。
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>排队等待时间(毫秒)</div>
|
||||
</div>
|
||||
<div style={settingsModernFieldCardStyle}>
|
||||
<div style={settingsModernFieldLabelStyle}>排队等待时间(毫秒)</div>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
@@ -1491,41 +1655,83 @@ export default function Settings() {
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
<div style={settingsModernFieldHintStyle}>
|
||||
超过该时间仍拿不到会话通道时,本次请求会直接放弃排队,避免长期挂起。
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}>
|
||||
这组 lease 只作用于能识别稳定 `session_id` 的会话型请求;没有稳定会话标识的普通请求不会进入这个池。
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.7 }}>
|
||||
默认只走原生 `/responses/compact`。开启回退后,只有当上游明确返回 compact 不支持或路径不存在时,才会退回普通 `/responses`。
|
||||
</div>
|
||||
<div>
|
||||
</ResponsiveFormGrid>
|
||||
<div style={settingsModernActionsStyle}>
|
||||
<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)' }} /> 保存中...</> : '保存传输与并发'}
|
||||
</button>
|
||||
</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 style={{ fontWeight: 600, fontSize: 14, marginBottom: 8, color: 'var(--color-danger)' }}>批量测活</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12, lineHeight: 1.8 }}>
|
||||
<div className="card animate-slide-up stagger-4" style={settingsModernDangerCardStyle} data-settings-card="model-availability-probe">
|
||||
<div style={settingsModernHeaderStyle}>
|
||||
<div style={settingsModernTitleBlockStyle}>
|
||||
<div style={{ ...settingsModernTitleStyle, color: 'var(--color-danger)' }}>批量测活</div>
|
||||
<div style={settingsModernDescriptionStyle}>
|
||||
默认关闭。开启后,metapi 会在后台定时对活跃账号模型发送最小化探测请求,用来校正“/models 能看到但实际不可用”的假阳性。
|
||||
</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>
|
||||
<div style={settingsModernPillRowStyle}>
|
||||
<span style={getSettingsPillStyle(modelAvailabilityProbeStatusTone)}>
|
||||
{modelAvailabilityProbeStatusLabel}
|
||||
</span>
|
||||
<span style={getSettingsPillStyle('danger')}>
|
||||
高风险操作
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--color-text-secondary)', marginBottom: 12 }}>
|
||||
</div>
|
||||
<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
|
||||
type="checkbox"
|
||||
checked={runtime.modelAvailabilityProbeEnabled}
|
||||
onChange={(e) => setRuntime((prev) => ({ ...prev, modelAvailabilityProbeEnabled: e.target.checked }))}
|
||||
style={{ width: 16, height: 16, marginTop: 2, flexShrink: 0 }}
|
||||
/>
|
||||
允许 metapi 后台主动批量测活
|
||||
</label>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.7, marginBottom: 12 }}>
|
||||
当前状态:{savedModelAvailabilityProbeEnabled ? '已启用' : '已关闭'}。首次开启必须手动输入确认语句。
|
||||
<ResponsiveFormGrid columns={2}>
|
||||
<div style={settingsModernFieldCardStyle}>
|
||||
<div style={settingsModernFieldLabelStyle}>当前生效状态</div>
|
||||
<div style={settingsModernPillRowStyle}>
|
||||
<span style={getSettingsPillStyle(modelAvailabilityProbeStatusTone)}>
|
||||
{modelAvailabilityProbeStatusLabel}
|
||||
</span>
|
||||
</div>
|
||||
<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">
|
||||
{savingModelAvailabilityProbe ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存批量测活设置'}
|
||||
</button>
|
||||
@@ -1625,18 +1831,6 @@ export default function Settings() {
|
||||
</button>
|
||||
</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 style={{ fontWeight: 600, fontSize: 14, marginBottom: 10 }}>路由策略</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 12 }}>
|
||||
|
||||
@@ -96,6 +96,13 @@ describe('Settings model availability probe confirmation', () => {
|
||||
});
|
||||
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) => (
|
||||
node.type === 'label'
|
||||
&& collectText(node).includes('允许 metapi 后台主动批量测活')
|
||||
@@ -107,6 +114,8 @@ describe('Settings model availability probe confirmation', () => {
|
||||
toggle.props.onChange({ target: { checked: true } });
|
||||
});
|
||||
|
||||
expect(collectText(probeCard)).toContain('待保存');
|
||||
|
||||
const saveButton = root.root.find((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.onClick === 'function'
|
||||
|
||||
@@ -100,12 +100,20 @@ describe('Settings proxy transport', () => {
|
||||
});
|
||||
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) => (
|
||||
node.type === 'label'
|
||||
&& collectText(node).includes('允许 metapi 到 Codex 上游使用 WebSocket')
|
||||
));
|
||||
const websocketToggle = websocketToggleLabel.findByType('input');
|
||||
expect(websocketToggle.props.checked).toBe(false);
|
||||
|
||||
const compactFallbackToggleLabel = root.root.find((node) => (
|
||||
node.type === 'label'
|
||||
&& collectText(node).includes('Compact 明确不支持时回退到普通 Responses')
|
||||
@@ -131,6 +139,9 @@ describe('Settings proxy transport', () => {
|
||||
queueWaitInput.props.onChange({ target: { value: '4200' } });
|
||||
});
|
||||
|
||||
expect(collectText(proxyTransportCard)).toContain('上游 WebSocket 已启用');
|
||||
expect(collectText(proxyTransportCard)).toContain('会话池 6 并发 / 4200ms');
|
||||
|
||||
const saveButton = root.root.find((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.onClick === 'function'
|
||||
|
||||
Reference in New Issue
Block a user