1
0
forked from boos/BoosAPI

fix connection and release regressions

This commit is contained in:
cita
2026-03-18 18:00:59 +08:00
parent d928e067c1
commit 38e46e165b
16 changed files with 528 additions and 63 deletions
+22 -4
View File
@@ -10,6 +10,9 @@ type CenteredModalProps = {
footer?: React.ReactNode;
maxWidth?: number;
bodyStyle?: React.CSSProperties;
closeOnBackdrop?: boolean;
closeOnEscape?: boolean;
showCloseButton?: boolean;
};
export default function CenteredModal({
@@ -20,6 +23,9 @@ export default function CenteredModal({
footer,
maxWidth = 860,
bodyStyle,
closeOnBackdrop = false,
closeOnEscape = false,
showCloseButton = true,
}: CenteredModalProps) {
const presence = useAnimatedVisibility(open, 220);
@@ -33,7 +39,7 @@ export default function CenteredModal({
}, [open]);
useEffect(() => {
if (!open || typeof document === 'undefined') return;
if (!open || !closeOnEscape || typeof document === 'undefined') return;
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose();
};
@@ -41,21 +47,33 @@ export default function CenteredModal({
return () => {
document.removeEventListener('keydown', handleKeydown);
};
}, [open, onClose]);
}, [closeOnEscape, open, onClose]);
if (!presence.shouldRender) return null;
const modal = (
<div
className={`modal-backdrop ${presence.isVisible ? '' : 'is-closing'}`.trim()}
onClick={onClose}
onClick={closeOnBackdrop ? onClose : undefined}
>
<div
className={`modal-content ${presence.isVisible ? '' : 'is-closing'}`.trim()}
style={{ maxWidth }}
onClick={(event) => event.stopPropagation()}
>
<div className="modal-header">{title}</div>
<div className="modal-header">
<div className="modal-title">{title}</div>
{showCloseButton ? (
<button
type="button"
className="modal-close-button"
onClick={onClose}
aria-label="关闭弹框"
>
×
</button>
) : null}
</div>
<div className="modal-body" style={bodyStyle}>
{children}
</div>
+39 -1
View File
@@ -1,8 +1,46 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { act, create } from 'react-test-renderer';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import CenteredModal from './CenteredModal.js';
describe('CenteredModal component', () => {
it('does not close on backdrop click by default and exposes an explicit close button', async () => {
const onClose = vi.fn();
let root: ReturnType<typeof create> | null = null;
try {
await act(async () => {
root = create(
<CenteredModal open onClose={onClose} title="测试弹框">
<div>content</div>
</CenteredModal>,
);
});
const closeButton = root.root.find((node) => (
node.type === 'button'
&& node.props['aria-label'] === '关闭弹框'
));
const backdrop = root.root.find((node) => (
typeof node.props.className === 'string'
&& node.props.className.includes('modal-backdrop')
));
expect(backdrop.props.onClick).toBeUndefined();
expect(onClose).not.toHaveBeenCalled();
await act(async () => {
closeButton.props.onClick();
});
expect(onClose).toHaveBeenCalledTimes(1);
} finally {
root?.unmount();
}
});
it('uses the shared centered modal shell pattern', () => {
const source = readFileSync(resolve(process.cwd(), 'src/web/components/CenteredModal.tsx'), 'utf8');
+36
View File
@@ -2598,11 +2598,47 @@ textarea {
.modal-header {
padding: 20px 24px 0;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary);
}
.modal-title {
min-width: 0;
flex: 1;
}
.modal-close-button {
border: 0;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
font-size: 24px;
line-height: 1;
padding: 0;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
transition: background 0.2s ease, color 0.2s ease;
}
.modal-close-button:hover {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.modal-close-button:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.modal-body {
padding: 16px 24px;
}
+3 -1
View File
@@ -358,7 +358,9 @@ export default function Accounts() {
skipModelFetch: tokenForm.skipModelFetch,
});
closeAddPanel();
if (result.tokenType === 'apikey') {
if (result.queued) {
toast.info(result.message || '账号已添加,后台正在同步初始化信息。');
} else if (result.tokenType === 'apikey') {
toast.success('已添加为 API Key 账号(可用于代理转发)');
} else {
const parts: string[] = [];
+1 -1
View File
@@ -162,7 +162,7 @@ function isExactModelPattern(modelPattern: string): boolean {
const normalized = modelPattern.trim();
if (!normalized) return false;
if (normalized.toLowerCase().startsWith('re:')) return false;
return !/[\*\?\[]/.test(normalized);
return !/[\*\?]/.test(normalized);
}
function routeTitle(route: RouteSelectorItem): string {
+1 -1
View File
@@ -159,7 +159,7 @@ function isExactModelPattern(modelPattern: string): boolean {
const normalized = modelPattern.trim();
if (!normalized) return false;
if (isRegexModelPattern(normalized)) return false;
return !/[\*\?\[]/.test(normalized);
return !/[\*\?]/.test(normalized);
}
function routeTitle(route: RouteSelectorItem): string {
+1 -1
View File
@@ -226,7 +226,7 @@ const isExactModelPattern = (modelPattern: string): boolean => {
const normalized = modelPattern.trim();
if (!normalized) return false;
if (normalized.toLowerCase().startsWith('re:')) return false;
return !/[\*\?\[]/.test(normalized);
return !/[\*\?]/.test(normalized);
};
const splitCommaSeparated = (value: string): string[] =>
@@ -0,0 +1,8 @@
import { describe, expect, it } from 'vitest';
import { isExactModelPattern } from './utils.js';
describe('isExactModelPattern', () => {
it('treats bracket-prefixed literal model names as exact patterns', () => {
expect(isExactModelPattern('[NV]deepseek-v3.1-terminus')).toBe(true);
});
});
+1 -1
View File
@@ -45,7 +45,7 @@ export function isExactModelPattern(modelPattern: string): boolean {
const normalized = modelPattern.trim();
if (!normalized) return false;
if (isRegexModelPattern(normalized)) return false;
return !/[\*\?\[]/.test(normalized);
return !/[\*\?]/.test(normalized);
}
export function parseRegexModelPattern(modelPattern: string): { regex: RegExp | null; error: string | null } {
@@ -79,6 +79,54 @@ describe('TokenRoutes grouped source models', () => {
vi.clearAllMocks();
});
it('does not treat bracket-prefixed exact model routes as group filters', async () => {
apiMock.getRoutesSummary.mockResolvedValue([
{
id: 4386, modelPattern: '[NV]deepseek-v3.1-terminus', displayName: null,
displayIcon: null, modelMapping: null, enabled: true,
channelCount: 1, enabledChannelCount: 1, siteNames: ['test'],
decisionSnapshot: null, decisionRefreshedAt: null,
},
{
id: 3383, modelPattern: 're:^claude-(opus|sonnet)-4-5$', displayName: 'claude-opus-4-6',
displayIcon: null, modelMapping: null, enabled: true,
channelCount: 4, enabledChannelCount: 4, siteNames: ['site-a', 'site-b'],
decisionSnapshot: null, decisionRefreshedAt: null,
},
]);
let root: ReturnType<typeof create> | null = null;
try {
await act(async () => {
root = create(
<MemoryRouter initialEntries={['/routes']}>
<ToastProvider>
<TokenRoutes />
</ToastProvider>
</MemoryRouter>,
);
});
await flushMicrotasks();
const filterToggle = findButtonByText(root.root, '筛选');
await act(async () => {
filterToggle.props.onClick();
});
await flushMicrotasks();
const text = collectText(root.root);
expect(text).toContain('全部群组1');
const bracketGroupButtons = root.root.findAll((node) => (
node.type === 'button'
&& collectText(node).includes('[NV]deepseek-v3.1-terminus')
));
expect(bracketGroupButtons).toHaveLength(0);
} finally {
root?.unmount();
}
});
it('collapses source-model groups by default for wildcard routes', async () => {
const channels = [
{