feat: add ai gateway local core flow
This commit is contained in:
+197
-22
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user