feat: 支持从站点页快捷添加 API Key (#505)

Co-authored-by: juzi <956428403@qq.com>
This commit is contained in:
Ju Zi
2026-05-08 22:54:40 +08:00
committed by GitHub
parent 90e2f0c405
commit f19ddac865
2 changed files with 121 additions and 19 deletions
+77 -18
View File
@@ -1,3 +1,8 @@
/**
* @Author: 橘子
* @Project_description: Metapi 站点管理页
* @Description: 代码是我抄的,不会也是真的
*/
import { useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { api } from '../api.js';
@@ -105,6 +110,23 @@ function resolveSiteCreatedSessionLabel(platform?: string | null): string {
return '添加账号(用户名密码登录)';
}
/**
* 跳转到站点对应的连接补全流程。
*/
function buildSiteConnectionSearchParams(input: {
siteId: number;
initializationPresetId?: string | null;
}) {
const params = new URLSearchParams({
create: '1',
siteId: String(input.siteId),
});
if (input.initializationPresetId) {
params.set('initPreset', input.initializationPresetId);
}
return params;
}
function formatSubscriptionDate(value?: string | null): string {
if (!value) return '';
const parsed = Date.parse(value);
@@ -875,32 +897,45 @@ export default function Sites() {
});
};
const handleSiteCreatedChoice = (choice: 'session' | 'apikey' | 'later') => {
if (!createdSiteForChoice) return;
const siteId = createdSiteForChoice.id;
const platform = createdSiteForChoice.platform?.toLowerCase().trim();
const params = new URLSearchParams({
create: '1',
siteId: String(siteId),
/**
* 从站点页进入账号/API Key 连接创建流程。
*/
const openSiteConnectionFlow = (input: {
siteId: number;
platform?: string | null;
initializationPresetId?: string | null;
choice: 'session' | 'apikey';
}) => {
const platform = input.platform?.toLowerCase().trim();
const params = buildSiteConnectionSearchParams({
siteId: input.siteId,
initializationPresetId: input.initializationPresetId,
});
if (createdSiteForChoice.initializationPresetId) {
params.set('initPreset', createdSiteForChoice.initializationPresetId);
}
if (choice === 'session') {
// codex平台使用OAuth流程
if (input.choice === 'session') {
if (platform === 'codex') {
params.set('provider', 'codex');
navigate(`/oauth?${params.toString()}`);
} else {
// 其他平台跳转到账号创建页面(session模式)
navigate(`/accounts?${params.toString()}`);
return;
}
} else if (choice === 'apikey') {
// 跳转到账号创建页面(apikey模式)
navigate(`/accounts?${params.toString()}`);
return;
}
params.set('segment', 'apikey');
navigate(`/accounts?${params.toString()}`);
};
const handleSiteCreatedChoice = (choice: 'session' | 'apikey' | 'later') => {
if (!createdSiteForChoice) return;
if (choice === 'session' || choice === 'apikey') {
openSiteConnectionFlow({
siteId: createdSiteForChoice.id,
platform: createdSiteForChoice.platform,
initializationPresetId: createdSiteForChoice.initializationPresetId,
choice,
});
}
// choice === 'later': 不跳转,留在当前页面
@@ -985,6 +1020,18 @@ export default function Sites() {
}
};
/**
* 从站点列表直接进入 API Key 批量添加入口。
*/
const handleOpenSiteApiKey = (site: SiteRow) => {
openSiteConnectionFlow({
siteId: site.id,
platform: site.platform,
initializationPresetId: detectSiteInitializationPreset(site.url, site.platform)?.id || null,
choice: 'apikey',
});
};
const handleTogglePin = async (site: SiteRow) => {
const nextPinned = !site.isPinned;
setPinningSiteId(site.id);
@@ -1938,6 +1985,12 @@ export default function Sites() {
>
{isExpanded ? '收起' : '详情'}
</button>
<button
onClick={() => handleOpenSiteApiKey(site)}
className="btn btn-link btn-link-primary"
>
添加 Key
</button>
<button
onClick={() => openEdit(site)}
className="btn btn-link btn-link-primary"
@@ -2256,6 +2309,12 @@ export default function Sites() {
</button>
</>
)}
<button
onClick={() => handleOpenSiteApiKey(site)}
className="btn btn-link btn-link-primary"
>
添加 Key
</button>
<button
onClick={() => openEdit(site)}
className="btn btn-link btn-link-primary"
@@ -1,3 +1,8 @@
/**
* @Author: 橘子
* @Project_description: Metapi 站点创建跳转测试
* @Description: 代码是我抄的,不会也是真的
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
@@ -188,6 +193,44 @@ describe('Sites create redirect', () => {
expect(rendered).toContain('siteId=24');
});
it('opens API key flow from site list action', async () => {
apiMock.getSites.mockResolvedValue([
{
id: 51,
name: 'List Site',
url: 'https://list.example.com',
platform: 'openai',
},
]);
let root!: ReactTestRenderer;
try {
await act(async () => {
root = create(
<ToastProvider>
<MemoryRouter initialEntries={['/sites']}>
<Routes>
<Route path="/sites" element={<Sites />} />
<Route path="/accounts" element={<LocationProbe />} />
</Routes>
</MemoryRouter>
</ToastProvider>,
);
});
await flushMicrotasks();
const addKeyButton = findClickableButtonByText(root, '添加 Key');
await act(async () => {
addKeyButton.props.onClick();
});
await flushMicrotasks();
expect(JSON.stringify(root.toJSON())).toContain('/accounts?create=1&siteId=51&segment=apikey');
} finally {
root?.unmount();
}
});
it('lists vendor-specific site types directly in the platform selector', async () => {
apiMock.getSites.mockResolvedValue([]);
apiMock.addSite.mockResolvedValue({ id: 30, name: 'Demo Site', platform: 'openai' });