feat: add ai gateway local core flow

This commit is contained in:
2026-05-09 16:51:28 +08:00
parent 5b20f017eb
commit c0335bd5d0
20 changed files with 2332 additions and 156 deletions
+197 -22
View File
@@ -2,7 +2,9 @@ import { useEffect, useMemo, useState, type FormEvent } from 'react';
import type {
BaseModelCatalogItem,
CatalogProvider,
GatewayApiKey,
GatewayTenant,
GatewayTask,
GatewayUser,
IntegrationPlatform,
PlatformModel,
@@ -11,7 +13,12 @@ import type {
UserGroup,
} from '@easyai-ai-gateway/contracts';
import {
createApiKey,
createChatTask,
createPlatform,
getTask,
getHealth,
listApiKeys,
listBaseModels,
listCatalogProviders,
listModels,
@@ -96,8 +103,6 @@ export function App() {
email: '',
password: '',
displayName: '',
tenantKey: '',
tenantName: '',
invitationCode: '',
});
const [health, setHealth] = useState<HealthResponse | null>(null);
@@ -110,6 +115,22 @@ export function App() {
const [tenants, setTenants] = useState<GatewayTenant[]>([]);
const [users, setUsers] = useState<GatewayUser[]>([]);
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
const [apiKeys, setApiKeys] = useState<GatewayApiKey[]>([]);
const [apiKeyForm, setApiKeyForm] = useState({ name: 'Local smoke key' });
const [apiKeySecret, setApiKeySecret] = useState('');
const [platformForm, setPlatformForm] = useState({
provider: 'openai',
platformKey: 'openai-simulation',
name: 'OpenAI Simulation',
baseUrl: 'https://api.openai.com/v1',
});
const [taskForm, setTaskForm] = useState({
model: 'gpt-4o-mini',
prompt: '用一句话确认 AI Gateway simulation 链路正常。',
});
const [taskResult, setTaskResult] = useState<GatewayTask | null>(null);
const [coreState, setCoreState] = useState<LoadState>('idle');
const [coreMessage, setCoreMessage] = useState('');
const [state, setState] = useState<LoadState>('idle');
const [error, setError] = useState('');
@@ -151,6 +172,7 @@ export function App() {
tenantResponse,
userResponse,
userGroupResponse,
apiKeyResponse,
] = await Promise.all([
listPlatforms(nextToken),
listModels(nextToken),
@@ -161,6 +183,7 @@ export function App() {
listTenants(nextToken),
listUsers(nextToken),
listUserGroups(nextToken),
listApiKeys(nextToken),
]);
setPlatforms(platformResponse.items);
setModels(modelResponse.items);
@@ -171,6 +194,7 @@ export function App() {
setTenants(tenantResponse.items);
setUsers(userResponse.items);
setUserGroups(userGroupResponse.items);
setApiKeys(apiKeyResponse.items);
setState('ready');
} catch (err) {
setState('error');
@@ -229,6 +253,68 @@ export function App() {
setTenants([]);
setUsers([]);
setUserGroups([]);
setApiKeys([]);
setApiKeySecret('');
setTaskResult(null);
setCoreMessage('');
}
async function submitAPIKey(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setCoreState('loading');
setCoreMessage('');
try {
const response = await createApiKey(token, { name: apiKeyForm.name, scopes: ['chat', 'image', 'video'] });
setApiKeySecret(response.secret);
setApiKeys((current) => [response.apiKey, ...current.filter((item) => item.id !== response.apiKey.id)]);
setCoreState('ready');
setCoreMessage('API Key 已创建,secret 仅展示一次。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '创建 API Key 失败');
}
}
async function submitPlatform(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setCoreState('loading');
setCoreMessage('');
try {
const platform = await createPlatform(token, {
...platformForm,
authType: 'bearer',
credentials: { mode: 'simulation' },
config: { testMode: true },
});
setPlatforms((current) => [platform, ...current.filter((item) => item.id !== platform.id)]);
setCoreState('ready');
setCoreMessage('平台已创建,当前阶段平台凭证仅全局管理员可配置。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '创建平台失败');
}
}
async function submitTask(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const credential = apiKeySecret || token;
setCoreState('loading');
setCoreMessage('');
try {
const response = await createChatTask(credential, {
model: taskForm.model,
runMode: 'simulation',
simulation: true,
messages: [{ role: 'user', content: taskForm.prompt }],
});
const detail = await getTask(credential, response.task.id);
setTaskResult(detail);
setCoreState('ready');
setCoreMessage(apiKeySecret ? '任务已通过本地 API Key 完成 simulation。' : '任务已通过当前 Access Token 完成 simulation。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '测试任务失败');
}
}
return (
@@ -278,6 +364,23 @@ export function App() {
</button>
</section>
<CoreFlowPanel
apiKeyForm={apiKeyForm}
apiKeys={apiKeys}
apiKeySecret={apiKeySecret}
coreMessage={coreMessage}
coreState={coreState}
platformForm={platformForm}
taskForm={taskForm}
taskResult={taskResult}
onAPIKeyFormChange={setApiKeyForm}
onPlatformFormChange={setPlatformForm}
onSubmitAPIKey={submitAPIKey}
onSubmitPlatform={submitPlatform}
onSubmitTask={submitTask}
onTaskFormChange={setTaskForm}
/>
<Dashboard
baseModels={baseModels}
models={models}
@@ -302,8 +405,6 @@ function AuthPanel(props: {
email: string;
password: string;
displayName: string;
tenantKey: string;
tenantName: string;
invitationCode: string;
};
state: LoadState;
@@ -315,8 +416,6 @@ function AuthPanel(props: {
email: string;
password: string;
displayName: string;
tenantKey: string;
tenantName: string;
invitationCode: string;
}) => void;
onSubmitExternalToken: (event: FormEvent<HTMLFormElement>) => void;
@@ -414,22 +513,6 @@ function AuthPanel(props: {
placeholder="至少 8 位"
/>
</label>
<label>
<span> Key</span>
<input
value={props.registerForm.tenantKey}
onChange={(event) => props.onRegisterChange({ ...props.registerForm, tenantKey: event.target.value })}
placeholder="team-a"
/>
</label>
<label>
<span></span>
<input
value={props.registerForm.tenantName}
onChange={(event) => props.onRegisterChange({ ...props.registerForm, tenantName: event.target.value })}
placeholder="Team A"
/>
</label>
<label>
<span></span>
<input
@@ -464,6 +547,98 @@ function AuthPanel(props: {
);
}
function CoreFlowPanel(props: {
apiKeyForm: { name: string };
apiKeys: GatewayApiKey[];
apiKeySecret: string;
coreMessage: string;
coreState: LoadState;
platformForm: { provider: string; platformKey: string; name: string; baseUrl: string };
taskForm: { model: string; prompt: string };
taskResult: GatewayTask | null;
onAPIKeyFormChange: (value: { name: string }) => void;
onPlatformFormChange: (value: { provider: string; platformKey: string; name: string; baseUrl: string }) => void;
onSubmitAPIKey: (event: FormEvent<HTMLFormElement>) => void;
onSubmitPlatform: (event: FormEvent<HTMLFormElement>) => void;
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
onTaskFormChange: (value: { model: string; prompt: string }) => void;
}) {
return (
<section className="corePanel" aria-label="核心链路验证">
<div className="sectionHeader">
<div>
<p className="eyebrow">Smoke Flow</p>
<h2></h2>
</div>
<span>{props.coreState === 'loading' ? '运行中' : '本地闭环'}</span>
</div>
<div className="coreGrid">
<form className="inlineForm" onSubmit={props.onSubmitAPIKey}>
<h3>1. API Key</h3>
<label>
<span></span>
<input value={props.apiKeyForm.name} onChange={(event) => props.onAPIKeyFormChange({ name: event.target.value })} />
</label>
<button type="submit" disabled={props.coreState === 'loading'}>
API Key
</button>
<p className="formHint"> {props.apiKeys.length} Key</p>
{props.apiKeySecret && <code className="secretBox">{props.apiKeySecret}</code>}
</form>
<form className="inlineForm" onSubmit={props.onSubmitPlatform}>
<h3>2. </h3>
<label>
<span>Provider</span>
<input value={props.platformForm.provider} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, provider: event.target.value })} />
</label>
<label>
<span> Key</span>
<input value={props.platformForm.platformKey} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, platformKey: event.target.value })} />
</label>
<label>
<span></span>
<input value={props.platformForm.name} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, name: event.target.value })} />
</label>
<label>
<span>Base URL</span>
<input value={props.platformForm.baseUrl} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, baseUrl: event.target.value })} />
</label>
<button type="submit" disabled={props.coreState === 'loading'}>
</button>
</form>
<form className="inlineForm" onSubmit={props.onSubmitTask}>
<h3>3. Simulation </h3>
<label>
<span></span>
<input value={props.taskForm.model} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, model: event.target.value })} />
</label>
<label>
<span>Prompt</span>
<input value={props.taskForm.prompt} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, prompt: event.target.value })} />
</label>
<button type="submit" disabled={props.coreState === 'loading'}>
</button>
{props.taskResult && (
<div className="resultBox">
<div>
<span className="statusPill">{props.taskResult.status}</span>
<strong>{props.taskResult.model}</strong>
</div>
<pre>{JSON.stringify(props.taskResult.result ?? {}, null, 2)}</pre>
</div>
)}
</form>
</div>
{props.coreMessage && <p className="coreMessage" data-error={props.coreState === 'error'}>{props.coreMessage}</p>}
</section>
);
}
function Dashboard(props: {
baseModels: BaseModelCatalogItem[];
models: PlatformModel[];
+68 -3
View File
@@ -2,7 +2,10 @@ import type {
AuthResponse,
BaseModelCatalogItem,
CatalogProvider,
CreatedGatewayApiKey,
GatewayApiKey,
GatewayTenant,
GatewayTask,
GatewayUser,
IntegrationPlatform,
ListResponse,
@@ -30,8 +33,6 @@ export async function registerLocalAccount(input: {
email?: string;
password: string;
displayName?: string;
tenantKey?: string;
tenantName?: string;
invitationCode?: string;
}): Promise<AuthResponse> {
return request<AuthResponse>('/api/v1/auth/register', {
@@ -81,6 +82,58 @@ export async function listUserGroups(token: string): Promise<ListResponse<UserGr
return request<ListResponse<UserGroup>>('/api/v1/user-groups', { token });
}
export async function listApiKeys(token: string): Promise<ListResponse<GatewayApiKey>> {
return request<ListResponse<GatewayApiKey>>('/api/v1/api-keys', { token });
}
export async function createApiKey(
token: string,
input: { name: string; scopes?: string[] },
): Promise<CreatedGatewayApiKey> {
return request<CreatedGatewayApiKey>('/api/v1/api-keys', {
body: input,
method: 'POST',
token,
});
}
export async function createPlatform(
token: string,
input: {
provider: string;
platformKey?: string;
name: string;
baseUrl?: string;
authType?: string;
credentials?: Record<string, unknown>;
config?: Record<string, unknown>;
defaultPricingMode?: string;
defaultDiscountFactor?: number;
priority?: number;
},
): Promise<IntegrationPlatform> {
return request<IntegrationPlatform>('/api/v1/platforms', {
body: input,
method: 'POST',
token,
});
}
export async function createChatTask(
token: string,
input: { model: string; messages: Array<Record<string, unknown>>; runMode?: string; simulation?: boolean },
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/chat/completions', {
body: input,
method: 'POST',
token,
});
}
export async function getTask(token: string, taskId: string): Promise<GatewayTask> {
return request<GatewayTask>(`/api/v1/tasks/${taskId}`, { token });
}
export async function listRateLimitWindows(token: string): Promise<ListResponse<RateLimitWindow>> {
return request<ListResponse<RateLimitWindow>>('/api/v1/runtime/rate-limit-windows', { token });
}
@@ -103,7 +156,19 @@ async function request<T>(
});
if (!response.ok) {
const body = await response.text();
throw new Error(body || `Request failed: ${response.status}`);
throw new Error(parseErrorMessage(body) || `Request failed: ${response.status}`);
}
return response.json() as Promise<T>;
}
function parseErrorMessage(body: string) {
if (!body) {
return '';
}
try {
const parsed = JSON.parse(body) as { error?: { message?: string } };
return parsed.error?.message ?? body;
} catch {
return body;
}
}
+116 -5
View File
@@ -244,6 +244,116 @@ button:disabled {
background: #ffffff;
}
.corePanel {
padding: 18px;
margin-bottom: 18px;
border: 1px solid #dde3ee;
border-radius: 8px;
background: #ffffff;
}
.coreGrid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.inlineForm {
display: flex;
min-height: 260px;
flex-direction: column;
gap: 12px;
padding: 14px;
border: 1px solid #e4eaf3;
border-radius: 8px;
background: #fbfcff;
}
.inlineForm label {
display: grid;
gap: 7px;
color: #4a5568;
font-size: 13px;
font-weight: 700;
}
.inlineForm input {
width: 100%;
min-height: 40px;
padding: 0 11px;
border: 1px solid #cbd5e1;
border-radius: 6px;
color: #172033;
outline: none;
}
.inlineForm button {
margin-top: auto;
}
.formHint,
.coreMessage {
color: #667085;
font-size: 13px;
line-height: 1.5;
}
.coreMessage {
margin-top: 12px;
font-weight: 700;
}
.coreMessage[data-error="true"] {
color: #9b2c2c;
}
.secretBox {
display: block;
overflow: hidden;
padding: 10px;
border: 1px solid #d8e0ec;
border-radius: 6px;
background: #ffffff;
color: #214e8a;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.resultBox {
display: grid;
gap: 10px;
padding: 10px;
border: 1px solid #d8e0ec;
border-radius: 6px;
background: #ffffff;
}
.resultBox > div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.resultBox pre {
overflow: auto;
max-height: 150px;
margin: 0;
color: #2d3748;
font-size: 12px;
line-height: 1.45;
}
.statusPill {
padding: 4px 8px;
border-radius: 999px;
background: #e8f5ef;
color: #1b8a5a;
font-size: 12px;
font-weight: 800;
}
.sectionHeader {
display: flex;
align-items: center;
@@ -476,11 +586,12 @@ button:disabled {
flex-direction: column;
}
.metrics,
.split,
.moduleGrid,
.detailGrid {
grid-template-columns: 1fr;
.metrics,
.split,
.coreGrid,
.moduleGrid,
.detailGrid {
grid-template-columns: 1fr;
}
.row {