import { Fragment, useEffect, 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 { applyTaskSubmissionMode, type RunTaskOptions } from '../lib/run-task'; import type { ApiDocSection, LoadState, TaskForm, TaskKind, TaskSubmissionMode } from '../types'; import { ApiKeySelect, apiKeyNoticeText, resolveSelectedApiKeyId } from './playground-shared'; import { publicApiCatalogGroups, publicApiEndpointCount, type PublicApiFamily, type PublicApiMethod, type PublicApiSurface, } from './api-docs-catalog'; interface ApiDocItem { catalogFamily?: PublicApiFamily; catalogSurface?: PublicApiSurface; group: string; key: ApiDocSection; kind?: TaskKind; lead: string; method?: PublicApiMethod; path?: string; surface: PublicApiSurface; 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: 'openApiOverview', surface: 'open', catalogSurface: 'open', group: '接口总览', title: '通用开放接口', lead: '统一的 Gateway 接口面向新业务接入,覆盖账号、模型、生成、异步任务、真人资产和安全集成;生产 Base URL 固定为 https://ai.51easyai.com/api/v1。' }, { key: 'chat', surface: 'open', group: '文本', kind: 'chat.completions', method: 'POST', path: '/api/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' }, { key: 'responses', surface: 'open', group: '文本', kind: 'responses', method: 'POST', path: '/api/v1/responses', title: 'Responses', lead: 'OpenAI 兼容的 Responses 接口,原生支持 input、previous_response_id、工具调用和流式输出;不支持原生 Responses 的模型会由网关转换到 Chat Completions。' }, { key: 'embeddings', surface: 'open', group: '文本', kind: 'embeddings', method: 'POST', path: '/api/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口,可直接用 input 数组或字符串生成 embedding,API Key 需要 embedding 权限。' }, { key: 'reranks', surface: 'open', group: '文本', kind: 'reranks', method: 'POST', path: '/api/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口,传入 query 和 documents 后返回 relevance_score,API Key 需要 rerank 权限。' }, { key: 'imageGeneration', surface: 'open', group: '图片', kind: 'images.generations', method: 'POST', path: '/api/v1/images/generations', title: '创建图片', lead: 'OpenAI 兼容的图片生成接口,支持 prompt、size、quality 和 simulation 测试。' }, { key: 'imageEdit', surface: 'open', group: '图片', kind: 'images.edits', method: 'POST', path: '/api/v1/images/edits', title: '编辑图片', lead: 'OpenAI 兼容的图片编辑接口,支持 image、mask、prompt 和 simulation 测试。' }, { key: 'videoGeneration', surface: 'open', group: '视频', kind: 'videos.generations', method: 'POST', path: '/api/v1/videos/generations', title: '生成视频', lead: '视频生成任务接口,支持文生视频、首尾帧、图片/视频/音频参考,以及时长、分辨率、画幅和声音等模型能力参数。' }, { key: 'asyncMode', surface: 'open', group: '异步任务', title: '异步模式', lead: '所有 AI 任务创建接口使用同一种异步开启方式:保留原接口和原请求 Body,只需增加 X-Async: true。' }, { key: 'taskRetrieve', surface: 'open', group: '异步任务', kind: 'tasks.retrieve', method: 'GET', path: '/api/v1/tasks/{taskID}', title: '取回任务', lead: '使用异步提交返回的 taskId 查询任务状态、结果、错误、用量、计费和执行尝试;queued、running、submitting 为进行中状态。' }, { key: 'pricing', surface: 'open', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估', lead: '按请求体估算输入输出 token、模型倍率和折扣后的预估费用。' }, { key: 'files', surface: 'open', group: '文件', method: 'POST', path: '/api/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源,后续请求可复用返回的文件 URL。' }, { key: 'compatApiOverview', surface: 'compatibility', catalogSurface: 'compatibility', group: '接口总览', title: '兼容接口', lead: '为已有厂商 SDK 和存量业务保留请求、响应及任务协议;调用方只需替换 Base URL 和 Gateway API Key。' }, { key: 'compatGemini', surface: 'compatibility', catalogFamily: 'gemini', group: '协议分类', title: 'Gemini 兼容接口', lead: '保留 generateContent 和 Gemini Files 请求结构,统一通过 /api/v1 前缀接入。' }, { key: 'compatKling', surface: 'compatibility', catalogFamily: 'kling', group: '协议分类', title: '可灵兼容接口', lead: '覆盖中国区可灵官方 V1 Omni、网关 V1 和 API 2.0;上游 AK/SK 只保存在网关服务端。' }, { key: 'compatVolces', surface: 'compatibility', catalogFamily: 'volces', group: '协议分类', title: '火山兼容接口', lead: '保留火山内容生成任务的创建、列表、查询和取消协议。' }, { key: 'compatServerMain', surface: 'compatibility', catalogFamily: 'serverMain', group: '协议分类', title: 'server-main 兼容接口', lead: '保留 server-main 的视频提交与结果轮询响应结构。' }, ]; 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/v1/openapi.json', apiDocsYamlPath: '/api/v1/openapi.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, options?: RunTaskOptions) => 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 [docsSearch, setDocsSearch] = useState(''); const normalizedDocsSearch = docsSearch.trim().toLowerCase(); const visibleGuideItems = guideItems.filter((item) => guideMatchesSearch(item, normalizedDocsSearch)); const visibleApiDocs = apiDocs.filter((item) => apiDocMatchesSearch(item, normalizedDocsSearch)); const isFileDoc = currentApiDoc?.key === 'files'; const isTaskRetrieveDoc = currentApiDoc?.key === 'taskRetrieve'; const isAsyncModeDoc = currentApiDoc?.key === 'asyncMode'; const isCatalogDoc = Boolean(currentApiDoc?.catalogSurface || currentApiDoc?.catalogFamily); const runnerAvailable = Boolean(currentApiDoc?.kind && currentApiDoc.method && currentApiDoc.path); const runnerModeEnabled = Boolean(runnerAvailable && currentApiDoc?.method !== 'GET'); const apiKeyNotice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById); const activeApiKeyId = resolveSelectedApiKeyId(props.apiKeys, props.apiKeySecretsById, props.selectedApiKeyId); const [submissionMode, setSubmissionMode] = useState('simulation'); const [bodyDraft, setBodyDraft] = useState(() => requestBodyExample(props.taskForm, currentApiDoc?.key ?? 'chat', 'simulation')); const [bodyError, setBodyError] = useState(''); 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(() => { setSubmissionMode('simulation'); setBodyDraft(requestBodyExample(props.taskForm, currentApiDoc?.key ?? 'chat', 'simulation')); setBodyError(''); }, [currentApiDoc?.key, props.taskForm]); 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) { event.preventDefault(); if (!runnerAvailable) { return; } if (!props.canRun) { props.onLogin(); return; } if (runnerModeEnabled) { const parsed = parseEditableRequestBody(bodyDraft); if (parsed.error || !parsed.body) { setBodyError(parsed.error); return; } const requestBody = applyTaskSubmissionMode(parsed.body, submissionMode); setBodyDraft(JSON.stringify(requestBody, null, 2)); setBodyError(''); props.onSubmitTask(event, { requestBody, submissionMode }); return; } props.onSubmitTask(event); } function handleBodyChange(value: string) { setBodyDraft(value); setBodyError(parseEditableRequestBody(value).error); } function handleBodyBlur() { const parsed = parseEditableRequestBody(bodyDraft); if (parsed.error || !parsed.body) { setBodyError(parsed.error); return; } setBodyDraft(JSON.stringify(applyTaskSubmissionMode(parsed.body, submissionMode), null, 2)); setBodyError(''); } function handleSubmissionModeChange(nextMode: TaskSubmissionMode) { const parsed = parseEditableRequestBody(bodyDraft); if (parsed.error || !parsed.body) { setBodyError('请先修正请求 Body 的 JSON 格式,再切换运行模式。'); return; } setSubmissionMode(nextMode); setBodyDraft(JSON.stringify(applyTaskSubmissionMode(parsed.body, nextMode), null, 2)); setBodyError(''); } 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}

{isCatalogDoc && currentApiDoc ? ( ) : 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}