234 lines
9.0 KiB
TypeScript
234 lines
9.0 KiB
TypeScript
import { useMemo, type FormEvent } from 'react';
|
|
import type { GatewayTask } from '@easyai-ai-gateway/contracts';
|
|
import { BookOpen, Play, Search, Send } from 'lucide-react';
|
|
import { Badge, Button, Select, Textarea } from '../components/ui';
|
|
import type { ApiDocSection, LoadState, TaskForm } from '../types';
|
|
|
|
const docs: Array<{ key: ApiDocSection; group: string; method: string; path: string; title: string }> = [
|
|
{ key: 'chat', group: '聊天(Chat)', method: 'POST', path: '/v1/chat/completions', title: 'Chat(聊天)' },
|
|
{ key: 'imageGeneration', group: '图片', method: 'POST', path: '/v1/images/generations', title: '创建图片' },
|
|
{ key: 'imageEdit', group: '图片', method: 'POST', path: '/v1/images/edits', title: '编辑图片' },
|
|
{ key: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估' },
|
|
{ key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件' },
|
|
];
|
|
|
|
const guideItems = ['获取 Base URL 和 API Key', '通知设置-WebHook 参数介绍', '错误码', '测试模式'];
|
|
|
|
const taskKindOptions = [
|
|
['chat.completions', 'Chat'],
|
|
['images.generations', '生图'],
|
|
['images.edits', '图像编辑'],
|
|
] as const;
|
|
|
|
export function ApiDocsPage(props: {
|
|
activeDocSection: ApiDocSection;
|
|
apiKeySecret: string;
|
|
canRun: boolean;
|
|
coreMessage: string;
|
|
coreState: LoadState;
|
|
taskForm: TaskForm;
|
|
taskResult: GatewayTask | null;
|
|
onLogin: () => void;
|
|
onDocSectionChange: (value: ApiDocSection) => void;
|
|
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
|
|
onTaskFormChange: (value: TaskForm) => void;
|
|
}) {
|
|
const current = docs.find((item) => item.key === props.activeDocSection) ?? docs[0];
|
|
const isFileDoc = current.key === 'files';
|
|
const bodyExample = useMemo(() => requestBodyExample(props.taskForm), [props.taskForm]);
|
|
|
|
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
if (!props.canRun) {
|
|
event.preventDefault();
|
|
props.onLogin();
|
|
return;
|
|
}
|
|
props.onSubmitTask(event);
|
|
}
|
|
|
|
return (
|
|
<div className="apiDocsShell">
|
|
<aside className="docsSidebar">
|
|
<div className="docsBrand">
|
|
<BookOpen size={19} />
|
|
<strong>API Docs</strong>
|
|
</div>
|
|
<label className="docsSearch">
|
|
<Search size={15} />
|
|
<input placeholder="搜索" />
|
|
</label>
|
|
<DocsGroup title="指南" items={guideItems.map((title) => ({ title }))} />
|
|
{groupDocs(docs).map((group) => (
|
|
<DocsGroup
|
|
key={group.title}
|
|
title={group.title}
|
|
items={group.items.map((item) => ({
|
|
active: item.key === props.activeDocSection,
|
|
method: item.method,
|
|
title: item.title,
|
|
onClick: () => props.onDocSectionChange(item.key),
|
|
}))}
|
|
/>
|
|
))}
|
|
</aside>
|
|
|
|
<main className="docsArticle">
|
|
<p className="eyebrow">{current.group}</p>
|
|
<h1>{current.title}</h1>
|
|
<div className="endpointBar">
|
|
<Badge variant="warning">{current.method}</Badge>
|
|
<code>{current.path}</code>
|
|
</div>
|
|
<p className="docsLead">
|
|
保持与原 integration-platform / OpenAI 兼容接口一致,支持本地 API Key 和 server-main 接入 token。
|
|
</p>
|
|
|
|
<section className="paramCard">
|
|
<header>
|
|
<h2>Header 参数</h2>
|
|
<Button type="button" variant="secondary" size="sm">生成代码</Button>
|
|
</header>
|
|
<ParamRow name="Content-Type" type="string" required value={isFileDoc ? 'multipart/form-data' : 'application/json'} />
|
|
<ParamRow name="Accept" type="string" required value="application/json" />
|
|
<ParamRow name="Authorization" type="string" value="Bearer {{YOUR_API_KEY}}" />
|
|
</section>
|
|
|
|
<section className="paramCard">
|
|
<header>
|
|
<h2>Body 参数</h2>
|
|
<Badge variant="outline">application/json</Badge>
|
|
</header>
|
|
{isFileDoc ? (
|
|
<>
|
|
<ParamRow name="file" type="file" required value="multipart 文件字段" />
|
|
<ParamRow name="source" type="string" value="上传来源标记" />
|
|
</>
|
|
) : (
|
|
<>
|
|
<ParamRow name="model" type="string" required value="模型 ID 或别名" />
|
|
<ParamRow name="messages / prompt" type="array|string" required value="对话消息或图片提示词" />
|
|
<ParamRow name="simulation" type="boolean" value="测试模式开关" />
|
|
<ParamRow name="stream" type="boolean" value="对话进度流式返回" />
|
|
</>
|
|
)}
|
|
</section>
|
|
</main>
|
|
|
|
<aside className="docsRunner">
|
|
<form onSubmit={handleSubmit}>
|
|
<header>
|
|
<strong>在线运行</strong>
|
|
<Button type="submit" size="sm" disabled={props.canRun && props.coreState === 'loading'}>
|
|
<Send size={14} />
|
|
{props.canRun ? '发送' : '登录'}
|
|
</Button>
|
|
</header>
|
|
<div className="runnerEndpoint">
|
|
<Badge variant="warning">POST</Badge>
|
|
<span>{current.path}</span>
|
|
</div>
|
|
<label className="shLabel">
|
|
能力
|
|
<Select value={props.taskForm.kind} onChange={(event) => props.onTaskFormChange(defaultTaskForKind(event.target.value as TaskForm['kind'], props.taskForm))}>
|
|
{taskKindOptions.map(([value, label]) => (
|
|
<option value={value} key={value}>{label}</option>
|
|
))}
|
|
</Select>
|
|
</label>
|
|
<label className="shLabel">
|
|
请求 Body
|
|
<Textarea value={bodyExample} onChange={(event) => props.onTaskFormChange(parseBody(event.target.value, props.taskForm))} />
|
|
</label>
|
|
<Button type="submit" disabled={props.canRun && props.coreState === 'loading'}>
|
|
<Play size={15} />
|
|
{!props.canRun ? '登录后运行' : props.coreState === 'loading' ? '运行中' : '运行测试'}
|
|
</Button>
|
|
</form>
|
|
<section className="runnerResult">
|
|
<strong>返回结果</strong>
|
|
{props.taskResult ? (
|
|
<pre>{JSON.stringify(props.taskResult.result ?? props.taskResult, null, 2)}</pre>
|
|
) : (
|
|
<div className="emptyState">
|
|
<span>点击发送获取返回结果</span>
|
|
</div>
|
|
)}
|
|
{props.coreMessage && <p>{props.coreMessage}</p>}
|
|
</section>
|
|
</aside>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DocsGroup(props: {
|
|
items: Array<{ active?: boolean; method?: string; onClick?: () => void; title: string }>;
|
|
title: string;
|
|
}) {
|
|
return (
|
|
<section className="docsGroup">
|
|
<h3>{props.title}</h3>
|
|
{props.items.map((item) => (
|
|
<button type="button" data-active={item.active} key={item.title} onClick={item.onClick}>
|
|
<span>{item.title}</span>
|
|
{item.method && <Badge variant="warning">{item.method}</Badge>}
|
|
</button>
|
|
))}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ParamRow(props: { name: string; required?: boolean; type: string; value: string }) {
|
|
return (
|
|
<div className="paramRow">
|
|
<code>{props.name}</code>
|
|
<span>{props.type}</span>
|
|
<p>{props.value}</p>
|
|
<em>{props.required ? '必需' : '可选'}</em>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function groupDocs(items: typeof docs) {
|
|
const groups = new Map<string, typeof docs>();
|
|
for (const item of items) {
|
|
groups.set(item.group, [...(groups.get(item.group) ?? []), item]);
|
|
}
|
|
return Array.from(groups, ([title, groupItems]) => ({ title, items: groupItems }));
|
|
}
|
|
|
|
function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm {
|
|
if (kind === 'chat.completions') return { ...current, kind, model: 'gpt-4o-mini' };
|
|
if (kind === 'images.edits') return { ...current, kind, image: current.image ?? 'https://example.com/source.png', mask: current.mask ?? 'https://example.com/mask.png', model: 'gpt-image-1' };
|
|
return { ...current, kind, model: 'gpt-image-1' };
|
|
}
|
|
|
|
function requestBodyExample(task: TaskForm) {
|
|
const body = task.kind === 'chat.completions'
|
|
? { model: task.model, messages: [{ role: 'user', content: task.prompt }], simulation: true, stream: true }
|
|
: task.kind === 'images.edits'
|
|
? { model: task.model, prompt: task.prompt, image: task.image, mask: task.mask, simulation: true }
|
|
: { model: task.model, prompt: task.prompt, quality: 'medium', simulation: true, size: '1024x1024' };
|
|
return JSON.stringify(body, null, 2);
|
|
}
|
|
|
|
function parseBody(value: string, current: TaskForm): TaskForm {
|
|
try {
|
|
const body = JSON.parse(value) as {
|
|
image?: string;
|
|
mask?: string;
|
|
messages?: Array<{ content?: string }>;
|
|
model?: string;
|
|
prompt?: string;
|
|
};
|
|
return {
|
|
...current,
|
|
image: body.image ?? current.image,
|
|
mask: body.mask ?? current.mask,
|
|
model: body.model ?? current.model,
|
|
prompt: body.prompt ?? body.messages?.[0]?.content ?? current.prompt,
|
|
};
|
|
} catch {
|
|
return current;
|
|
}
|
|
}
|