feat: 支持从站点页快捷添加 API Key (#505)
Co-authored-by: juzi <956428403@qq.com>
This commit is contained in:
+77
-18
@@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @Author: 橘子
|
||||||
|
* @Project_description: Metapi 站点管理页
|
||||||
|
* @Description: 代码是我抄的,不会也是真的
|
||||||
|
*/
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
@@ -105,6 +110,23 @@ function resolveSiteCreatedSessionLabel(platform?: string | null): string {
|
|||||||
return '添加账号(用户名密码登录)';
|
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 {
|
function formatSubscriptionDate(value?: string | null): string {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
const parsed = Date.parse(value);
|
const parsed = Date.parse(value);
|
||||||
@@ -875,32 +897,45 @@ export default function Sites() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSiteCreatedChoice = (choice: 'session' | 'apikey' | 'later') => {
|
/**
|
||||||
if (!createdSiteForChoice) return;
|
* 从站点页进入账号/API Key 连接创建流程。
|
||||||
|
*/
|
||||||
const siteId = createdSiteForChoice.id;
|
const openSiteConnectionFlow = (input: {
|
||||||
const platform = createdSiteForChoice.platform?.toLowerCase().trim();
|
siteId: number;
|
||||||
const params = new URLSearchParams({
|
platform?: string | null;
|
||||||
create: '1',
|
initializationPresetId?: string | null;
|
||||||
siteId: String(siteId),
|
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') {
|
if (input.choice === 'session') {
|
||||||
// codex平台使用OAuth流程
|
|
||||||
if (platform === 'codex') {
|
if (platform === 'codex') {
|
||||||
params.set('provider', 'codex');
|
params.set('provider', 'codex');
|
||||||
navigate(`/oauth?${params.toString()}`);
|
navigate(`/oauth?${params.toString()}`);
|
||||||
} else {
|
return;
|
||||||
// 其他平台跳转到账号创建页面(session模式)
|
|
||||||
navigate(`/accounts?${params.toString()}`);
|
|
||||||
}
|
}
|
||||||
} else if (choice === 'apikey') {
|
navigate(`/accounts?${params.toString()}`);
|
||||||
// 跳转到账号创建页面(apikey模式)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
params.set('segment', 'apikey');
|
params.set('segment', 'apikey');
|
||||||
navigate(`/accounts?${params.toString()}`);
|
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': 不跳转,留在当前页面
|
// 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 handleTogglePin = async (site: SiteRow) => {
|
||||||
const nextPinned = !site.isPinned;
|
const nextPinned = !site.isPinned;
|
||||||
setPinningSiteId(site.id);
|
setPinningSiteId(site.id);
|
||||||
@@ -1938,6 +1985,12 @@ export default function Sites() {
|
|||||||
>
|
>
|
||||||
{isExpanded ? '收起' : '详情'}
|
{isExpanded ? '收起' : '详情'}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpenSiteApiKey(site)}
|
||||||
|
className="btn btn-link btn-link-primary"
|
||||||
|
>
|
||||||
|
添加 Key
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openEdit(site)}
|
onClick={() => openEdit(site)}
|
||||||
className="btn btn-link btn-link-primary"
|
className="btn btn-link btn-link-primary"
|
||||||
@@ -2256,6 +2309,12 @@ export default function Sites() {
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpenSiteApiKey(site)}
|
||||||
|
className="btn btn-link btn-link-primary"
|
||||||
|
>
|
||||||
|
添加 Key
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openEdit(site)}
|
onClick={() => openEdit(site)}
|
||||||
className="btn btn-link btn-link-primary"
|
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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
||||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
@@ -188,6 +193,44 @@ describe('Sites create redirect', () => {
|
|||||||
expect(rendered).toContain('siteId=24');
|
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 () => {
|
it('lists vendor-specific site types directly in the platform selector', async () => {
|
||||||
apiMock.getSites.mockResolvedValue([]);
|
apiMock.getSites.mockResolvedValue([]);
|
||||||
apiMock.addSite.mockResolvedValue({ id: 30, name: 'Demo Site', platform: 'openai' });
|
apiMock.addSite.mockResolvedValue({ id: 30, name: 'Demo Site', platform: 'openai' });
|
||||||
|
|||||||
Reference in New Issue
Block a user