import { Fragment, useEffect, useMemo, useState, type CSSProperties, type FormEvent, type ReactNode } from 'react'; import type { GatewayApiKey, GatewaySkillBundleMetadata, GatewayTask } from '@easyai-ai-gateway/contracts'; import { BookOpen, Download, ExternalLink, FileJson, KeyRound, Play, Search, Send, Wrench } from 'lucide-react'; import { Badge, Button, Input, Select, Textarea } from '../components/ui'; import { getOpsManagementSkillMetadata, resolveApiAssetUrl } from '../api'; import type { ApiDocSection, LoadState, TaskForm, TaskKind } from '../types'; import { ApiKeySelect, apiKeyNoticeText, resolveSelectedApiKeyId } from './playground-shared'; interface ApiDocItem { group: string; key: ApiDocSection; kind?: TaskKind; lead: string; method?: 'GET' | 'POST'; path?: string; title: string; } interface ApiDocParam { children?: ApiDocParam[]; name: string; required?: boolean; type: string; value: string; } type ApiGuideSection = Extract; interface ApiGuideItem { group: '指南'; key: ApiGuideSection; lead: string; title: string; } export const apiDocs: ApiDocItem[] = [ { key: 'chat', group: '文本', kind: 'chat.completions', method: 'POST', path: '/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' }, { key: 'responses', group: '文本', kind: 'responses', method: 'POST', path: '/v1/responses', title: 'Responses', lead: 'OpenAI 兼容的 Responses 接口,原生支持 input、previous_response_id、工具调用和流式输出;不支持原生 Responses 的模型会由网关转换到 Chat Completions。' }, { key: 'embeddings', group: '文本', kind: 'embeddings', method: 'POST', path: '/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口,可直接用 input 数组或字符串生成 embedding,API Key 需要 embedding 权限。' }, { key: 'reranks', group: '文本', kind: 'reranks', method: 'POST', path: '/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口,传入 query 和 documents 后返回 relevance_score,API Key 需要 rerank 权限。' }, { key: 'imageGeneration', group: '图片', kind: 'images.generations', method: 'POST', path: '/v1/images/generations', title: '创建图片', lead: 'OpenAI 兼容的图片生成接口,支持 prompt、size、quality 和 simulation 测试。' }, { key: 'imageEdit', group: '图片', kind: 'images.edits', method: 'POST', path: '/v1/images/edits', title: '编辑图片', lead: 'OpenAI 兼容的图片编辑接口,支持 image、mask、prompt 和 simulation 测试。' }, { key: 'videoGeneration', group: '视频', kind: 'videos.generations', method: 'POST', path: '/api/v1/videos/generations', title: '生成视频', lead: '视频生成任务接口,支持文生视频、首尾帧、图片/视频/音频参考,以及时长、分辨率、画幅和声音等模型能力参数。' }, { key: 'asyncMode', group: '异步任务', title: '异步模式', lead: '所有 AI 任务创建接口使用同一种异步开启方式:保留原接口和原请求 Body,只需增加 X-Async: true。' }, { key: 'taskRetrieve', group: '异步任务', kind: 'tasks.retrieve', method: 'GET', path: '/api/v1/tasks/{taskID}', title: '取回任务', lead: '使用异步提交返回的 taskId 查询任务状态、结果、错误、用量、计费和执行尝试;queued、running、submitting 为进行中状态。' }, { key: 'pricing', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估', lead: '按请求体估算输入输出 token、模型倍率和折扣后的预估费用。' }, { key: 'files', group: '文件', method: 'POST', path: '/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源,后续请求可复用返回的文件 URL。' }, ]; const guideItems: ApiGuideItem[] = [ { key: 'guideBaseUrl', group: '指南', title: '获取 Base URL 和 API Key', lead: '确认当前部署环境的网关地址,创建 API Key,并使用 Bearer 鉴权调用公开接口。' }, { key: 'guideWebhook', group: '指南', title: '通知设置-WebHook 参数介绍', lead: '了解任务进度 WebHook 的启用方式、回调结构、可靠投递机制和公开 API 的状态取回方式。' }, { key: 'guideErrors', group: '指南', title: '错误码', lead: '根据 HTTP 状态、error.code、requestId 和异步任务错误字段快速定位请求失败原因。' }, { key: 'guideTesting', group: '指南', title: '测试模式', lead: '使用 simulation 完整验证鉴权、路由、队列、限流、进度和结果归一,而不向真实供应商提交任务。' }, ]; const taskKindOptions = [ ['chat.completions', 'Chat'], ['responses', 'Responses'], ['embeddings', '文本向量'], ['reranks', '重排序'], ['images.generations', '生图'], ['images.edits', '图像编辑'], ['videos.generations', '视频生成'], ['tasks.retrieve', '取回任务'], ] as const; const defaultOpsSkillMetadata: GatewaySkillBundleMetadata = { name: 'ai-gateway-ops-management', version: '', displayName: 'AI Gateway 运维管理', modules: ['model-runtime'], fileName: 'ai-gateway-ops-management.zip', downloadPath: '/api/v1/public/skills/ai-gateway-ops-management/download', apiDocsJsonPath: '/api-docs-json', apiDocsYamlPath: '/api-docs-yaml', }; export function ApiDocsPage(props: { activeDocSection: ApiDocSection; apiKeySecretsById: Record; apiKeys: GatewayApiKey[]; canRun: boolean; coreMessage: string; coreState: LoadState; selectedApiKeyId: string; taskForm: TaskForm; taskResult: GatewayTask | null; onApiKeyChange: (apiKeyId: string) => void; onCreateApiKey: () => void; onLogin: () => void; onDocSectionChange: (value: ApiDocSection) => void; onSubmitTask: (event: FormEvent) => void; onTaskFormChange: (value: TaskForm) => void; }) { const activeGuide = guideItems.find((item) => item.key === props.activeDocSection); const currentApiDoc = apiDocs.find((item) => item.key === props.activeDocSection) ?? (activeGuide ? undefined : apiDocs[0]); const current = activeGuide ?? currentApiDoc ?? apiDocs[0]; const [opsSkillMetadata, setOpsSkillMetadata] = useState(defaultOpsSkillMetadata); const isFileDoc = currentApiDoc?.key === 'files'; const isTaskRetrieveDoc = currentApiDoc?.key === 'taskRetrieve'; const isAsyncModeDoc = currentApiDoc?.key === 'asyncMode'; const runnerAvailable = Boolean(currentApiDoc?.kind && currentApiDoc.method && currentApiDoc.path); const apiKeyNotice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById); const activeApiKeyId = resolveSelectedApiKeyId(props.apiKeys, props.apiKeySecretsById, props.selectedApiKeyId); const bodyExample = useMemo( () => requestBodyExample(props.taskForm, currentApiDoc?.key ?? 'chat'), [currentApiDoc?.key, props.taskForm], ); const runnerPath = currentApiDoc?.path ? isTaskRetrieveDoc ? currentApiDoc.path.replace('{taskID}', props.taskForm.taskId?.trim() || '{taskID}') : currentApiDoc.path : ''; useEffect(() => { if (currentApiDoc?.kind && props.taskForm.kind !== currentApiDoc.kind) { props.onTaskFormChange(defaultTaskForDoc(currentApiDoc.kind, props.taskForm, props.taskResult)); } }, [currentApiDoc?.kind, props.taskForm.kind, props.taskResult?.id]); useEffect(() => { let active = true; getOpsManagementSkillMetadata() .then((metadata) => { if (active) setOpsSkillMetadata(metadata); }) .catch(() => { // Keep stable public fallback paths visible when metadata is temporarily unavailable. }); return () => { active = false; }; }, []); function handleSubmit(event: FormEvent) { if (!runnerAvailable) { event.preventDefault(); return; } if (!props.canRun) { event.preventDefault(); props.onLogin(); return; } props.onSubmitTask(event); } function handleDocClick(item: ApiDocItem) { if (item.kind) { props.onTaskFormChange(defaultTaskForDoc(item.kind, props.taskForm, props.taskResult)); } props.onDocSectionChange(item.key); } function handleGuideClick(item: ApiGuideItem) { props.onDocSectionChange(item.key); } function handleKindChange(kind: TaskKind) { props.onTaskFormChange(defaultTaskForDoc(kind, props.taskForm, props.taskResult)); const nextSection = docSectionForKind(kind); if (nextSection !== props.activeDocSection) { props.onDocSectionChange(nextSection); } } return (

{current.group}

{current.title}

{currentApiDoc?.method && currentApiDoc.path && (
{currentApiDoc.path}
)}

{current.lead}

{isAsyncModeDoc ? ( ) : currentApiDoc ? ( <> {!isTaskRetrieveDoc && }

Header 参数

{currentApiDoc.method !== 'GET' && } {supportsAsyncMode(currentApiDoc.key) && ( )}

{isTaskRetrieveDoc ? 'Path 参数' : 'Body 参数'}

{!isTaskRetrieveDoc && {isFileDoc ? 'multipart/form-data' : 'application/json'}}
{isTaskRetrieveDoc ? ( ) : isFileDoc ? ( <> ) : ( )}
{isTaskRetrieveDoc && } ) : activeGuide ? ( ) : null}