[codex] Split site API endpoint pool from panel host (#373)

* feat: add site api endpoint schema scaffolding

* feat: wire site api endpoints into backup migration

* feat: add site api endpoint selection service

* feat: add site api endpoint contracts

* feat: route discovery through site api endpoints

* feat: route proxy traffic through site api endpoints

* feat: add site api endpoint pool editor

* fix: keep proxy video tasks on site endpoint pool

* fix: restore core test compatibility for site endpoint pool

* fix: address endpoint pool review findings
This commit is contained in:
Cita
2026-04-01 20:58:37 +08:00
committed by GitHub
parent ce77bde95a
commit 76c85d0a13
49 changed files with 7226 additions and 986 deletions
+219 -6
View File
@@ -20,11 +20,14 @@ import { shouldIgnoreRowSelectionClick } from './helpers/rowSelection.js';
import { resolveInitialConnectionSegment } from './helpers/defaultConnectionSegment.js';
import {
buildSiteSaveAction,
emptySiteApiEndpoint,
emptySiteCustomHeader,
emptySiteForm,
serializeSiteApiEndpoints,
serializeSiteCustomHeaders,
siteFormFromSite,
type SiteEditorState,
type SiteApiEndpointField,
type SiteForm,
} from './helpers/sitesEditor.js';
import {
@@ -59,12 +62,33 @@ type SiteRow = {
totalBalance?: number;
subscriptionSummary?: SiteSubscriptionSummary | null;
createdAt?: string;
apiEndpoints?: Array<{
id?: number;
url: string;
enabled?: boolean;
sortOrder?: number;
cooldownUntil?: string | null;
lastFailureReason?: string | null;
}>;
};
function hasConfiguredCustomHeaders(customHeaders?: string | null): boolean {
return typeof customHeaders === 'string' && customHeaders.trim().length > 0;
}
function getConfiguredSiteApiEndpoints(site?: Pick<SiteRow, 'apiEndpoints'> | null) {
return Array.isArray(site?.apiEndpoints)
? site.apiEndpoints.filter((item) => typeof item?.url === 'string' && item.url.trim())
: [];
}
function buildSiteApiEndpointSummary(site?: Pick<SiteRow, 'apiEndpoints'> | null): string {
const endpoints = getConfiguredSiteApiEndpoints(site);
if (endpoints.length <= 0) return '跟随主站点 URL';
const enabledCount = endpoints.filter((item) => item.enabled !== false).length;
return `${enabledCount}/${endpoints.length} 条启用`;
}
function formatUsd(value?: number | null): string {
return `$${(value || 0).toFixed(2)}`;
}
@@ -203,7 +227,27 @@ export default function Sites() {
const [sortMode, setSortMode] = useState<SortMode>('custom');
const [highlightSiteId, setHighlightSiteId] = useState<number | null>(null);
const [editor, setEditor] = useState<SiteEditorState | null>(null);
const [form, setForm] = useState<SiteForm>(emptySiteForm());
const apiEndpointDraftIdRef = useRef(0);
const createApiEndpointDraftId = () => {
apiEndpointDraftIdRef.current += 1;
return `site-api-endpoint-draft-${apiEndpointDraftIdRef.current}`;
};
const createEmptyApiEndpointRow = (): SiteApiEndpointField => ({
...emptySiteApiEndpoint(),
draftId: createApiEndpointDraftId(),
});
const hydrateSiteForm = (value: SiteForm): SiteForm => {
const sourceRows = value.apiEndpoints.length > 0 ? value.apiEndpoints : [createEmptyApiEndpointRow()];
return {
...value,
apiEndpoints: sourceRows.map((endpoint) => ({
...endpoint,
draftId: endpoint.draftId || createApiEndpointDraftId(),
})),
};
};
const createEmptySiteForm = (): SiteForm => hydrateSiteForm(emptySiteForm());
const [form, setForm] = useState<SiteForm>(() => createEmptySiteForm());
const [detecting, setDetecting] = useState(false);
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState<number | null>(null);
@@ -364,7 +408,7 @@ export default function Sites() {
const closeEditor = () => {
setEditor(null);
setForm(emptySiteForm());
setForm(createEmptySiteForm());
setSelectedInitializationPresetId(null);
};
@@ -381,14 +425,14 @@ export default function Sites() {
return;
}
setEditor({ mode: 'add' });
setForm(emptySiteForm());
setForm(createEmptySiteForm());
setSelectedInitializationPresetId(null);
scrollToEditorTop();
};
const openEdit = (site: SiteRow) => {
setEditor({ mode: 'edit', editingSiteId: site.id });
setForm(siteFormFromSite(site));
setForm(hydrateSiteForm(siteFormFromSite(site)));
setSelectedInitializationPresetId(detectSiteInitializationPreset(site.url, site.platform)?.id || null);
scrollToEditorTop();
// Load disabled models and discovered models independently so a best-effort
@@ -471,6 +515,11 @@ export default function Sites() {
toast.error(serializedCustomHeaders.error || '自定义请求头格式不正确');
return;
}
const serializedApiEndpoints = serializeSiteApiEndpoints(form.apiEndpoints);
if (!serializedApiEndpoints.valid) {
toast.error(serializedApiEndpoints.error || 'AI 请求地址格式不正确');
return;
}
const payload = {
name: form.name.trim(),
@@ -480,6 +529,7 @@ export default function Sites() {
initializationPresetId: selectedInitializationPresetId,
proxyUrl: form.proxyUrl.trim(),
useSystemProxy: !!form.useSystemProxy,
apiEndpoints: serializedApiEndpoints.apiEndpoints,
customHeaders: serializedCustomHeaders.customHeaders,
globalWeight: Number(parsedGlobalWeight.toFixed(3)),
};
@@ -551,6 +601,48 @@ export default function Sites() {
});
};
const updateApiEndpointRow = (index: number, patch: Partial<SiteApiEndpointField>) => {
setForm((prev) => ({
...prev,
apiEndpoints: prev.apiEndpoints.map((item, itemIndex) => (
itemIndex === index
? { ...item, ...patch }
: item
)),
}));
};
const addApiEndpointRow = () => {
setForm((prev) => ({
...prev,
apiEndpoints: [...prev.apiEndpoints, createEmptyApiEndpointRow()],
}));
};
const removeApiEndpointRow = (index: number) => {
setForm((prev) => {
const nextEndpoints = prev.apiEndpoints.filter((_, itemIndex) => itemIndex !== index);
return {
...prev,
apiEndpoints: nextEndpoints.length > 0 ? nextEndpoints : [createEmptyApiEndpointRow()],
};
});
};
const moveApiEndpointRow = (index: number, direction: 'up' | 'down') => {
setForm((prev) => {
const targetIndex = direction === 'up' ? index - 1 : index + 1;
if (targetIndex < 0 || targetIndex >= prev.apiEndpoints.length) return prev;
const nextEndpoints = [...prev.apiEndpoints];
const [current] = nextEndpoints.splice(index, 1);
nextEndpoints.splice(targetIndex, 0, current);
return {
...prev,
apiEndpoints: nextEndpoints,
};
});
};
const handleSiteCreatedChoice = (choice: 'session' | 'apikey' | 'later') => {
if (!createdSiteForChoice) return;
@@ -935,7 +1027,8 @@ export default function Sites() {
/>
<div style={{ display: 'flex', gap: 8, flexDirection: isMobile ? 'column' : 'row' }}>
<input
placeholder="站点 URL (例如 https://api.example.com)"
data-testid="site-primary-url-input"
placeholder="站点 URL(面板/登录/签到地址,如 https://nih.cc)"
value={form.url}
onChange={(e) => setForm((prev) => ({ ...prev, url: e.target.value }))}
onBlur={() => {
@@ -1008,6 +1101,100 @@ export default function Sites() {
</div>
</div>
)}
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.6 }}>
主站点 URL 用于登录、签到、面板接口和系统访问令牌管理;如果 AI 请求地址和主站点不同,请在下面的 AI 请求地址池里填写。
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 10,
padding: 12,
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
background: 'color-mix(in srgb, var(--color-surface) 82%, transparent)',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<div style={{ fontSize: 13, fontWeight: 600 }}>
AI 请求地址池
</div>
<button
type="button"
onClick={addApiEndpointRow}
className="btn btn-ghost"
style={{ border: '1px solid var(--color-border)' }}
>
+ 添加 AI 地址
</button>
</div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.7 }}>
这里只用于 `/v1/*`、模型发现和 API Key 验证。不填时默认跟随主站点 URL;多条地址会按列表顺序参与轮询,禁用的地址不会参与调度。
</div>
{form.apiEndpoints.map((endpoint, index) => (
<div
key={endpoint.draftId || `site-api-endpoint-draft-${index}`}
style={{
display: 'flex',
flexDirection: 'column',
gap: 8,
padding: 10,
border: '1px solid var(--color-border-light)',
borderRadius: 'var(--radius-sm)',
background: 'var(--color-bg)',
}}
>
<div style={{ display: 'flex', gap: 8, flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'stretch' : 'center' }}>
<input
placeholder="AI 请求地址(如 https://api.nih.cc)"
value={endpoint.url}
onChange={(e) => updateApiEndpointRow(index, { url: e.target.value })}
style={{ ...formInputStyle, flex: 1, fontFamily: 'var(--font-mono)' }}
/>
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--color-text-secondary)' }}>
<input
type="checkbox"
checked={endpoint.enabled !== false}
onChange={(e) => updateApiEndpointRow(index, { enabled: e.target.checked })}
/>
启用
</label>
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', fontSize: 11, color: 'var(--color-text-muted)' }}>
<span>顺序 #{index + 1}</span>
{endpoint.cooldownUntil ? <span>冷却至 {formatDateTimeLocal(endpoint.cooldownUntil)}</span> : null}
{endpoint.lastFailureReason ? <span>最近失败: {endpoint.lastFailureReason}</span> : null}
</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button
type="button"
onClick={() => moveApiEndpointRow(index, 'up')}
disabled={index === 0}
className="btn btn-link btn-link-muted"
>
上移
</button>
<button
type="button"
onClick={() => moveApiEndpointRow(index, 'down')}
disabled={index >= form.apiEndpoints.length - 1}
className="btn btn-link btn-link-muted"
>
下移
</button>
<button
type="button"
onClick={() => removeApiEndpointRow(index)}
className="btn btn-link btn-link-danger"
>
删除
</button>
</div>
</div>
</div>
))}
</div>
<div
style={{
display: 'flex',
@@ -1218,7 +1405,7 @@ export default function Sites() {
style={formInputStyle}
/>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>
填写后优先使用站点代理;留空则使用系统代理或直连(取决于设置开关状态)。
这里只是 HTTP/SOCKS 代理地址,不是上游 AI API 地址。填写后优先使用站点代理;留空则使用系统代理或直连(取决于设置开关状态)。
</div>
</div>
<label style={{
@@ -1368,6 +1555,29 @@ export default function Sites() {
</a>
) : '-'}
/>
<MobileField
label="AI 请求地址"
stacked
value={(
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<span>{buildSiteApiEndpointSummary(site)}</span>
{getConfiguredSiteApiEndpoints(site).map((endpoint, endpointIndex) => (
<span
key={`mobile-site-endpoint-${site.id}-${endpoint.id ?? endpointIndex}`}
style={{
fontSize: 11,
fontFamily: 'var(--font-mono)',
color: endpoint.enabled === false ? 'var(--color-text-muted)' : 'var(--color-text-secondary)',
wordBreak: 'break-all',
}}
>
{endpoint.url}
{endpoint.enabled === false ? '(已禁用)' : ''}
</span>
))}
</div>
)}
/>
<MobileField
label="系统代理"
value={(
@@ -1505,6 +1715,9 @@ export default function Sites() {
自定义头
</span>
) : null}
<span className={`badge ${getConfiguredSiteApiEndpoints(site).length > 0 ? 'badge-warning' : 'badge-muted'}`} style={{ fontSize: 11 }}>
AI 地址: {buildSiteApiEndpointSummary(site)}
</span>
</div>
</td>
<td className="sites-url-cell" style={{ maxWidth: 300 }}>
+55
View File
@@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest';
import {
buildSiteSaveAction,
emptySiteApiEndpoint,
emptySiteCustomHeader,
emptySiteForm,
serializeSiteApiEndpoints,
serializeSiteCustomHeaders,
siteFormFromSite,
} from './sitesEditor.js';
@@ -17,6 +19,10 @@ describe('buildSiteSaveAction', () => {
externalCheckinUrl: 'https://checkin.a.example.com',
platform: 'new-api',
proxyUrl: 'socks5://127.0.0.1:1080',
apiEndpoints: [
{ url: 'https://api-a.example.com', enabled: true, sortOrder: 0 },
{ url: 'https://api-b.example.com', enabled: false, sortOrder: 1 },
],
customHeaders: '{"x-site-token":"alpha"}',
useSystemProxy: false,
globalWeight: 1.2,
@@ -31,6 +37,10 @@ describe('buildSiteSaveAction', () => {
externalCheckinUrl: 'https://checkin.a.example.com',
platform: 'new-api',
proxyUrl: 'socks5://127.0.0.1:1080',
apiEndpoints: [
{ url: 'https://api-a.example.com', enabled: true, sortOrder: 0 },
{ url: 'https://api-b.example.com', enabled: false, sortOrder: 1 },
],
customHeaders: '{"x-site-token":"alpha"}',
useSystemProxy: false,
globalWeight: 1.2,
@@ -48,6 +58,7 @@ describe('buildSiteSaveAction', () => {
platform: 'one-api',
proxyUrl: '',
useSystemProxy: true,
apiEndpoints: [],
customHeaders: '',
globalWeight: 0.8,
},
@@ -63,6 +74,7 @@ describe('buildSiteSaveAction', () => {
platform: 'one-api',
proxyUrl: '',
useSystemProxy: true,
apiEndpoints: [],
customHeaders: '',
globalWeight: 0.8,
},
@@ -80,6 +92,7 @@ describe('buildSiteSaveAction', () => {
platform: '',
proxyUrl: '',
useSystemProxy: false,
apiEndpoints: [],
customHeaders: '',
globalWeight: 1,
},
@@ -94,6 +107,14 @@ describe('buildSiteSaveAction', () => {
externalCheckinUrl: null,
platform: 'new-api',
proxyUrl: 'http://127.0.0.1:8080',
apiEndpoints: [
{
url: 'https://api.example.com',
enabled: false,
cooldownUntil: '2026-04-01T00:05:00.000Z',
lastFailureReason: 'HTTP 502',
},
],
customHeaders: '{"x-site-token":"alpha"}',
globalWeight: 1,
apiKey: 'sk-legacy-site-key',
@@ -101,11 +122,20 @@ describe('buildSiteSaveAction', () => {
expect(emptySiteForm()).not.toHaveProperty('apiKey');
expect(emptySiteForm().customHeaders).toEqual([emptySiteCustomHeader()]);
expect(emptySiteForm().apiEndpoints).toEqual([emptySiteApiEndpoint()]);
expect(emptySiteForm().proxyUrl).toBe('');
expect(siteFormFromSite(legacySite)).not.toHaveProperty('apiKey');
expect(siteFormFromSite({
proxyUrl: 'http://127.0.0.1:8080',
}).proxyUrl).toBe('http://127.0.0.1:8080');
expect(siteFormFromSite(legacySite).apiEndpoints).toEqual([
{
url: 'https://api.example.com',
enabled: false,
cooldownUntil: '2026-04-01T00:05:00.000Z',
lastFailureReason: 'HTTP 502',
},
]);
});
it('parses custom headers json into key value rows', () => {
@@ -139,4 +169,29 @@ describe('buildSiteSaveAction', () => {
error: '请求头 "authorization" 重复了',
});
});
it('serializes api endpoint rows into ordered payloads', () => {
expect(serializeSiteApiEndpoints([
{ url: 'https://api-a.example.com/', enabled: true },
{ url: 'https://api-b.example.com', enabled: false },
emptySiteApiEndpoint(),
])).toEqual({
valid: true,
apiEndpoints: [
{ url: 'https://api-a.example.com', enabled: true, sortOrder: 0 },
{ url: 'https://api-b.example.com', enabled: false, sortOrder: 1 },
],
});
});
it('rejects duplicate api endpoints after normalization', () => {
expect(serializeSiteApiEndpoints([
{ url: 'https://api.example.com/', enabled: true },
{ url: 'https://api.example.com', enabled: true },
])).toEqual({
valid: false,
apiEndpoints: [],
error: 'AI 请求地址 "https://api.example.com" 重复了',
});
});
});
+112 -1
View File
@@ -3,6 +3,14 @@ export type SiteCustomHeaderField = {
value: string;
};
export type SiteApiEndpointField = {
draftId?: string;
url: string;
enabled: boolean;
cooldownUntil?: string | null;
lastFailureReason?: string | null;
};
export type SiteForm = {
name: string;
url: string;
@@ -10,6 +18,7 @@ export type SiteForm = {
platform: string;
proxyUrl: string;
useSystemProxy: boolean;
apiEndpoints: SiteApiEndpointField[];
customHeaders: SiteCustomHeaderField[];
globalWeight: string;
};
@@ -26,6 +35,11 @@ export type SiteSavePayload = {
initializationPresetId?: string | null;
proxyUrl: string;
useSystemProxy: boolean;
apiEndpoints: Array<{
url: string;
enabled: boolean;
sortOrder: number;
}>;
customHeaders: string;
globalWeight: number;
};
@@ -38,6 +52,15 @@ export function emptySiteCustomHeader(): SiteCustomHeaderField {
return { key: '', value: '' };
}
export function emptySiteApiEndpoint(): SiteApiEndpointField {
return {
url: '',
enabled: true,
cooldownUntil: null,
lastFailureReason: null,
};
}
function ensureSiteCustomHeaderRows(rows: SiteCustomHeaderField[]): SiteCustomHeaderField[] {
return rows.length > 0 ? rows : [emptySiteCustomHeader()];
}
@@ -50,11 +73,16 @@ export function emptySiteForm(): SiteForm {
platform: '',
proxyUrl: '',
useSystemProxy: false,
apiEndpoints: [emptySiteApiEndpoint()],
customHeaders: [emptySiteCustomHeader()],
globalWeight: '1',
};
}
function ensureSiteApiEndpointRows(rows: SiteApiEndpointField[]): SiteApiEndpointField[] {
return rows.length > 0 ? rows : [emptySiteApiEndpoint()];
}
function parseCustomHeadersForEditor(raw: unknown): SiteCustomHeaderField[] {
if (typeof raw !== 'string') {
return ensureSiteCustomHeaderRows([]);
@@ -80,10 +108,35 @@ function parseCustomHeadersForEditor(raw: unknown): SiteCustomHeaderField[] {
}
}
export function siteFormFromSite(site: Partial<Omit<SiteForm, 'customHeaders' | 'globalWeight' | 'externalCheckinUrl' | 'proxyUrl' | 'useSystemProxy'>> & {
function parseApiEndpointsForEditor(raw: unknown): SiteApiEndpointField[] {
if (!Array.isArray(raw)) {
return ensureSiteApiEndpointRows([]);
}
const rows: SiteApiEndpointField[] = [];
for (const item of raw) {
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
rows.push({
url: typeof row.url === 'string' ? row.url : '',
enabled: row.enabled !== false,
cooldownUntil: typeof row.cooldownUntil === 'string' ? row.cooldownUntil : null,
lastFailureReason: typeof row.lastFailureReason === 'string' ? row.lastFailureReason : null,
});
}
return ensureSiteApiEndpointRows(rows);
}
export function siteFormFromSite(site: Partial<Omit<SiteForm, 'apiEndpoints' | 'customHeaders' | 'globalWeight' | 'externalCheckinUrl' | 'proxyUrl' | 'useSystemProxy'>> & {
externalCheckinUrl?: string | null;
proxyUrl?: string | null;
useSystemProxy?: boolean | null;
apiEndpoints?: Array<{
url?: string | null;
enabled?: boolean | null;
cooldownUntil?: string | null;
lastFailureReason?: string | null;
}> | null;
customHeaders?: string | null;
globalWeight?: number | string | null;
}): SiteForm {
@@ -96,11 +149,27 @@ export function siteFormFromSite(site: Partial<Omit<SiteForm, 'customHeaders' |
platform: site.platform ?? '',
proxyUrl: site.proxyUrl ?? '',
useSystemProxy: !!site.useSystemProxy,
apiEndpoints: parseApiEndpointsForEditor(site.apiEndpoints),
customHeaders: parseCustomHeadersForEditor(site.customHeaders),
globalWeight,
};
}
// Keep this in sync with normalizeSiteApiEndpointBaseUrl in
// src/server/services/siteApiEndpointService.ts.
function normalizeSiteApiEndpointUrl(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) return '';
try {
const parsed = new URL(trimmed);
parsed.search = '';
parsed.hash = '';
return parsed.toString().replace(/\/+$/, '');
} catch {
return trimmed.replace(/\/+$/, '');
}
}
export function serializeSiteCustomHeaders(fields: SiteCustomHeaderField[]): {
valid: boolean;
customHeaders: string;
@@ -131,6 +200,48 @@ export function serializeSiteCustomHeaders(fields: SiteCustomHeaderField[]): {
};
}
export function serializeSiteApiEndpoints(fields: SiteApiEndpointField[]): {
valid: boolean;
apiEndpoints: Array<{
url: string;
enabled: boolean;
sortOrder: number;
}>;
error?: string;
} {
const apiEndpoints: Array<{
url: string;
enabled: boolean;
sortOrder: number;
}> = [];
const seen = new Set<string>();
for (const field of fields) {
const rawUrl = field.url.trim();
if (!rawUrl) continue;
const normalizedUrl = normalizeSiteApiEndpointUrl(rawUrl);
if (!normalizedUrl) continue;
if (seen.has(normalizedUrl)) {
return {
valid: false,
apiEndpoints: [],
error: `AI 请求地址 "${normalizedUrl}" 重复了`,
};
}
seen.add(normalizedUrl);
apiEndpoints.push({
url: normalizedUrl || rawUrl,
enabled: field.enabled !== false,
sortOrder: apiEndpoints.length,
});
}
return {
valid: true,
apiEndpoints,
};
}
export function buildSiteSaveAction(editor: SiteEditorState, form: SiteSavePayload): SiteSaveAction {
if (editor.mode === 'edit') {
if (!Number.isFinite(editor.editingSiteId)) {
+12 -17
View File
@@ -33,6 +33,13 @@ async function flushMicrotasks() {
});
}
function findPrimarySiteUrlInput(root: ReactTestRenderer) {
return root.root.find((node) => (
node.type === 'input'
&& node.props['data-testid'] === 'site-primary-url-input'
));
}
function LocationProbe() {
const location = useLocation();
return <div>{`${location.pathname}${location.search}`}</div>;
@@ -78,7 +85,7 @@ async function createSiteAndClickModalChoice(
const nameInput = root.root.find((node) => node.type === 'input' && node.props.placeholder === '站点名称');
const urlInput = root.root.find((node) => (
node.type === 'input'
&& node.props.placeholder === '站点 URL (例如 https://api.example.com)'
&& node.props['data-testid'] === 'site-primary-url-input'
));
const selects = root.root.findAllByType(ModernSelect);
const platformSelect = selects.at(-1);
@@ -307,10 +314,7 @@ describe('Sites create redirect', () => {
});
await flushMicrotasks();
const urlInput = root.root.find((node) => (
node.type === 'input'
&& node.props.placeholder === '站点 URL (例如 https://api.example.com)'
));
const urlInput = findPrimarySiteUrlInput(root);
const selects = root.root.findAllByType(ModernSelect);
const platformSelect = selects.at(-1);
@@ -363,10 +367,7 @@ describe('Sites create redirect', () => {
});
await flushMicrotasks();
const urlInput = root.root.find((node) => (
node.type === 'input'
&& node.props.placeholder === '站点 URL (例如 https://api.example.com)'
));
const urlInput = findPrimarySiteUrlInput(root);
const platformSelect = root.root.findAllByType(ModernSelect).at(-1);
await act(async () => {
@@ -419,10 +420,7 @@ describe('Sites create redirect', () => {
});
await flushMicrotasks();
const urlInput = root.root.find((node) => (
node.type === 'input'
&& node.props.placeholder === '站点 URL (例如 https://api.example.com)'
));
const urlInput = findPrimarySiteUrlInput(root);
await act(async () => {
urlInput.props.onChange({ target: { value: 'https://coding.dashscope.aliyuncs.com/v1' } });
@@ -531,10 +529,7 @@ describe('Sites create redirect', () => {
// Fill form
const nameInput = root.root.find((node) => node.type === 'input' && node.props.placeholder === '站点名称');
const urlInput = root.root.find((node) => (
node.type === 'input'
&& node.props.placeholder === '站点 URL (例如 https://api.example.com)'
));
const urlInput = findPrimarySiteUrlInput(root);
const selects = root.root.findAllByType(ModernSelect);
const platformSelect = selects.at(-1);
const saveButton = root.root.find((node) => (