1
0
forked from boos/BoosAPI

fix(proxy): add system proxy connectivity test and align proxy UI copy

This commit is contained in:
Brucents
2026-03-20 19:13:23 +08:00
parent a66cbafeb8
commit 088d0455a9
10 changed files with 539 additions and 9 deletions
+20
View File
@@ -156,6 +156,21 @@ export type ProxyTestJobResponse = {
expiresAt?: string;
};
export type SystemProxyTestRequest = {
proxyUrl?: string;
};
export type SystemProxyTestResponse = {
success: true;
proxyUrl: string;
probeUrl: string;
finalUrl: string;
reachable: true;
ok: boolean;
statusCode: number;
latencyMs: number;
};
export type ProxyLogStatusFilter = 'all' | 'success' | 'failed';
export type ProxyLogClientConfidence = 'exact' | 'heuristic' | 'unknown' | null;
@@ -511,6 +526,11 @@ export const api = {
method: 'PUT',
body: JSON.stringify(data),
}),
testSystemProxy: (data: SystemProxyTestRequest) => request('/api/settings/system-proxy/test', {
method: 'POST',
body: JSON.stringify(data),
timeoutMs: 20_000,
}),
getRuntimeDatabaseConfig: () => request('/api/settings/database/runtime'),
updateRuntimeDatabaseConfig: (data: { dialect: 'sqlite' | 'mysql' | 'postgres'; connectionString: string; ssl?: boolean }) =>
request('/api/settings/database/runtime', {
+62 -4
View File
@@ -55,6 +55,11 @@ type RuntimeSettings = {
currentAdminIp?: string;
};
type SystemProxyTestState =
| { kind: 'success'; text: string }
| { kind: 'error'; text: string }
| null;
type DownstreamApiKeyItem = {
id: number;
name: string;
@@ -222,6 +227,8 @@ export default function Settings() {
const [testingCheckin, setTestingCheckin] = useState(false);
const [savingToken, setSavingToken] = useState(false);
const [savingSystemProxy, setSavingSystemProxy] = useState(false);
const [testingSystemProxy, setTestingSystemProxy] = useState(false);
const [systemProxyTestState, setSystemProxyTestState] = useState<SystemProxyTestState>(null);
const [savingProxyFailureRules, setSavingProxyFailureRules] = useState(false);
const [savingRouting, setSavingRouting] = useState(false);
const [showAdvancedRouting, setShowAdvancedRouting] = useState(false);
@@ -576,6 +583,31 @@ export default function Settings() {
}
};
const testSystemProxy = async () => {
const proxyUrl = runtime.systemProxyUrl.trim();
if (!proxyUrl) {
const message = '请先填写系统代理地址';
setSystemProxyTestState({ kind: 'error', text: message });
toast.info(message);
return;
}
setTestingSystemProxy(true);
setSystemProxyTestState(null);
try {
const res = await api.testSystemProxy({ proxyUrl });
const summary = `连通成功,延迟 ${res.latencyMs} ms`;
setSystemProxyTestState({ kind: 'success', text: summary });
toast.success(`系统代理测试成功(${res.latencyMs} ms)`);
} catch (err: any) {
const message = err?.message || '系统代理测试失败';
setSystemProxyTestState({ kind: 'error', text: message });
toast.error(message);
} finally {
setTestingSystemProxy(false);
}
};
const saveProxyFailureRules = async () => {
setSavingProxyFailureRules(true);
try {
@@ -1114,13 +1146,39 @@ export default function Settings() {
</div>
<input
value={runtime.systemProxyUrl}
onChange={(e) => setRuntime((prev) => ({ ...prev, systemProxyUrl: e.target.value }))}
onChange={(e) => {
setRuntime((prev) => ({ ...prev, systemProxyUrl: e.target.value }));
setSystemProxyTestState(null);
}}
placeholder="系统代理 URL(可选,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080)"
style={{ ...inputStyle, fontFamily: 'var(--font-mono)', marginBottom: 10 }}
/>
<button onClick={saveSystemProxy} disabled={savingSystemProxy} className="btn btn-primary">
{savingSystemProxy ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存系统代理'}
</button>
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
<button onClick={saveSystemProxy} disabled={savingSystemProxy} className="btn btn-primary">
{savingSystemProxy ? <><span className="spinner spinner-sm" style={{ borderTopColor: 'white', borderColor: 'rgba(255,255,255,0.3)' }} /> 保存中...</> : '保存系统代理'}
</button>
<button
onClick={testSystemProxy}
disabled={testingSystemProxy}
className="btn btn-ghost"
style={{ border: '1px solid var(--color-border)' }}
>
{testingSystemProxy ? <><span className="spinner spinner-sm" /> 测试中...</> : '测试系统代理'}
</button>
</div>
{systemProxyTestState && (
<div
style={{
fontSize: 12,
marginTop: 10,
color: systemProxyTestState.kind === 'success'
? 'var(--color-success)'
: 'var(--color-danger)',
}}
>
{systemProxyTestState.text}
</div>
)}
</div>
<div className="card animate-slide-up stagger-4" style={{ padding: 20 }}>
+2 -2
View File
@@ -921,13 +921,13 @@ export default function Sites() {
)}
</div>
<input
placeholder="站点代理 URL(可选,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080)"
placeholder="站点代理(可选,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080)"
value={form.proxyUrl}
onChange={(e) => setForm((prev) => ({ ...prev, proxyUrl: e.target.value }))}
style={formInputStyle}
/>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
留空则直连;填写后优先于系统代理。
填写后优先使用站点代理;留空则使用系统代理或直连(取决于设置开关状态)。
</div>
<label style={{
display: 'flex',
@@ -12,6 +12,7 @@ const { apiMock } = vi.hoisted(() => ({
getRoutesLite: vi.fn(),
getRuntimeDatabaseConfig: vi.fn(),
updateRuntimeSettings: vi.fn(),
testSystemProxy: vi.fn(),
},
}));
@@ -67,6 +68,16 @@ describe('Settings system proxy', () => {
success: true,
systemProxyUrl: 'http://127.0.0.1:7890',
});
apiMock.testSystemProxy.mockResolvedValue({
success: true,
proxyUrl: 'http://127.0.0.1:7890',
probeUrl: 'https://www.gstatic.com/generate_204',
finalUrl: 'https://www.gstatic.com/generate_204',
reachable: true,
ok: true,
statusCode: 204,
latencyMs: 321,
});
});
afterEach(() => {
@@ -112,4 +123,45 @@ describe('Settings system proxy', () => {
root?.unmount();
}
});
it('tests system proxy from settings', async () => {
let root: ReturnType<typeof create> | null = null;
try {
await act(async () => {
root = create(
<MemoryRouter>
<ToastProvider>
<Settings />
</ToastProvider>
</MemoryRouter>,
);
});
await flushMicrotasks();
const proxyInput = root.root.find((node) => (
node.type === 'input'
&& node.props.placeholder === '系统代理 URL(可选,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080)'
));
await act(async () => {
proxyInput.props.onChange({ target: { value: 'http://127.0.0.1:7890' } });
});
const testButton = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& collectText(node).trim() === '测试系统代理'
));
await act(async () => {
testButton.props.onClick();
});
await flushMicrotasks();
expect(apiMock.testSystemProxy).toHaveBeenCalledWith({
proxyUrl: 'http://127.0.0.1:7890',
});
expect(collectText(root.root)).toContain('连通成功,延迟 321 ms');
} finally {
root?.unmount();
}
});
});