"use client"; import { useEffect, useRef, useState } from "react"; import { ChevronDown, FileArchive, FileText, KeyRound, ListChecks, LoaderCircle, Play, RotateCcw, Square, Terminal, type LucideIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import webConfig from "@/constants/common-env"; import { getStoredAuthSession } from "@/store/auth"; import { ApiOnlinePlayground as SharedApiOnlinePlayground } from "./api-online-playground"; type ParamRow = [string, string, string]; type ApiDoc = { title: string; method: string; path: string; icon: LucideIcon; input: ParamRow[]; output: ParamRow[]; example: (baseUrl: string, key: string) => string; }; type OnlineResult = { ok: boolean; status: number; statusText: string; durationMs: number; contentType: string; body: string; }; const docs: ApiDoc[] = [ { title: "模型列表", method: "GET", path: "/v1/models", icon: ListChecks, input: [ ["Authorization", "header", "Bearer 。"], ], output: [ ["data", "array", "模型列表,包含 id、object、created、owned_by。"], ], example: (baseUrl: string, key: string) => `curl ${baseUrl}/models \\ -H "Authorization: Bearer ${key}"`, }, { title: "聊天补全", method: "POST", path: "/v1/chat/completions", icon: FileText, input: [ ["model", "string", "模型名,例如 gpt-5-5-mini,也可用于图片兼容场景。"], ["messages", "array", "OpenAI 兼容消息数组。"], ["stream", "boolean", "可选,是否流式返回。"], ["n", "number", "可选,图片兼容场景会解析为生成数量。"], ], output: [ ["id", "string", "响应 ID。"], ["choices", "array", "OpenAI 兼容 choices。"], ["usage", "object", "可选,token 使用信息。"], ], example: (baseUrl: string, key: string) => `curl ${baseUrl}/chat/completions \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${key}" \\ -d '{"model":"gpt-5-5-mini","messages":[{"role":"user","content":"你好"}]}'`, }, { title: "Responses", method: "POST", path: "/v1/responses", icon: FileText, input: [ ["model", "string", "模型名。"], ["input", "string | array | object", "用户输入,图片生成会从中解析提示词。"], ["tools", "array", "可选,Responses 工具定义。"], ["stream", "boolean", "可选,是否流式返回。"], ], output: [ ["id", "string", "响应 ID。"], ["output", "array", "Responses 兼容输出。"], ["status", "string", "响应状态。"], ], example: (baseUrl: string, key: string) => `curl ${baseUrl}/responses \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${key}" \\ -d '{"model":"gpt-5-5-mini","input":"生成一张未来城市图片"}'`, }, { title: "搜索", method: "POST", path: "/v1/search", icon: ListChecks, input: [ ["prompt", "string", "搜索问题或检索指令。"], ], output: [ ["answer", "string", "搜索后的回答内容,具体字段以返回结果为准。"], ["sources", "array", "可选,搜索引用来源。"], ], example: (baseUrl: string, key: string) => `curl ${baseUrl}/search \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${key}" \\ -d '{"prompt":"搜索 chatgpt2api 最新使用方式"}'`, }, { title: "图片生成", method: "POST", path: "/v1/images/generations", icon: FileArchive, input: [ ["prompt", "string", "图片生成提示词。"], ["model", "string", "可选,默认 gpt-image-2。"], ["n", "number", "可选,生成数量,当前限制 1-4。"], ["size", "string", "可选,图片尺寸。"], ["quality", "string", "可选,默认 auto。"], ["response_format", "string", "可选,默认 url;需要内联图片时可传 b64_json。"], ], output: [ ["data", "array", "图片结果列表。"], ["data[].b64_json", "string", "base64 图片内容。"], ["data[].url", "string", "部分配置下返回图片 URL。"], ], example: (baseUrl: string, key: string) => `curl ${baseUrl}/images/generations \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${key}" \\ -d '{"model":"gpt-image-2","prompt":"一张极简产品海报","n":1}'`, }, { title: "图片编辑", method: "POST", path: "/v1/images/edits", icon: FileArchive, input: [ ["image", "file | file[] | URL", "参考图,支持 multipart 上传,也支持 JSON 图片链接。"], ["prompt", "string", "编辑提示词。"], ["model", "string", "可选,默认 gpt-image-2。"], ["n", "number", "可选,生成数量,当前限制 1-4。"], ["size", "string", "可选,图片尺寸。"], ["quality", "string", "可选,默认 auto。"], ], output: [ ["data", "array", "编辑后的图片结果列表。"], ["data[].b64_json", "string", "base64 图片内容。"], ["data[].url", "string", "部分配置下返回图片 URL。"], ], example: (baseUrl: string, key: string) => `curl ${baseUrl}/images/edits \\ -H "Authorization: Bearer ${key}" \\ -F "model=gpt-image-2" \\ -F "prompt=改成赛博朋克夜景" \\ -F "image=@./input.png"`, }, { title: "创建 PPT 任务", method: "POST", path: "/v1/ppt/generations", icon: FileText, input: [ ["prompt", "string", "PPT 需求描述,可为空但建议填写完整主题、页数、风格和内容结构。"], ["base64_images", "string[]", "可选,图片 data URL/base64,用作 PPT 参考素材。"], ["client_task_id", "string", "可选,客户端幂等任务 ID;重复提交同 ID 会返回已有任务。"], ], output: [ ["id / taskId", "string", "任务 ID,用于轮询状态。"], ["status", "queued | running | success | error", "任务状态。"], ["kind", "ppt", "任务类型。"], ["created_at / updated_at", "string", "任务创建和更新时间。"], ], example: (baseUrl: string, key: string) => `curl ${baseUrl}/ppt/generations \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${key}" \\ -d '{"prompt":"制作一份 8 页以内的季度业务汇报 PPT","base64_images":[]}'`, }, { title: "创建 PSD 任务", method: "POST", path: "/v1/psd/generations", icon: FileArchive, input: [ ["prompt", "string", "PSD 拆分与合成要求,例如保留图层、位置、背景和素材 zip。"], ["base64_images", "string[]", "必填,至少一张图片 data URL/base64,作为 PSD 拆分源图。"], ["client_task_id", "string", "可选,客户端幂等任务 ID。"], ], output: [ ["id / taskId", "string", "任务 ID,用于轮询状态。"], ["status", "queued | running | success | error", "任务状态。"], ["kind", "psd", "任务类型。"], ["error", "string", "失败时返回错误信息。"], ], example: (baseUrl: string, key: string) => `curl ${baseUrl}/psd/generations \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${key}" \\ -d '{"prompt":"按原图位置拆分海报元素并合成可编辑 PSD","base64_images":["data:image/png;base64,..."]}'`, }, { title: "任务状态查询", method: "GET", path: "/v1/editable-file-tasks?ids={taskId1,taskId2}", icon: ListChecks, input: [ ["ids", "string", "可选,逗号分隔任务 ID;不传则返回当前用户全部可编辑文件任务。"], ], output: [ ["items", "array", "任务列表。成功任务的 result 内包含 primary_url 和 zip_url。"], ["missing_ids", "string[]", "查询指定 ids 时,返回未找到的任务 ID。"], ["result.primary_url", "string", "主文件下载地址。"], ["result.zip_url", "string", "素材 zip 下载地址。"], ], example: (baseUrl: string, key: string) => `curl "${baseUrl}/editable-file-tasks?ids=" \\ -H "Authorization: Bearer ${key}"`, }, { title: "结果文件下载", method: "GET", path: "/files/{file_path}", icon: FileArchive, input: [ ["file_path", "string", "由任务 result.primary_url 或 result.zip_url 返回,通常不需要手动拼接。"], ], output: [ ["binary", "file", "返回 pptx/psd/zip 文件流。"], ], example: (baseUrl: string, _key: string) => `curl ${baseUrl.replace(/\/v1$/, "")}/files/ -o result.zip`, }, ]; const usableModels = ["gpt-image-2", "codex-gpt-image-2", "auto", "gpt-5", "gpt-5-1", "gpt-5-2", "gpt-5-3", "gpt-5-3-mini", "gpt-5-5-mini"]; function onlineDefaults(item: ApiDoc) { const path = item.path.startsWith("/v1/editable-file-tasks?") ? "/v1/editable-file-tasks" : item.path; const bodies: Record = { "/v1/chat/completions": { model: "gpt-5-5-mini", messages: [{ role: "user", content: "你好,请用一句话介绍你自己。" }], stream: false }, "/v1/responses": { model: "gpt-5-5-mini", input: "用一句话介绍这个接口。", stream: false }, "/v1/search": { prompt: "搜索 chatgpt2api 最新使用方式" }, "/v1/images/generations": { model: "gpt-image-2", prompt: "一张极简产品海报", n: 1, response_format: "url" }, "/v1/images/edits": { model: "gpt-image-2", prompt: "改成赛博朋克夜景", image: "https://example.com/input.png", n: 1, response_format: "url" }, "/v1/ppt/generations": { prompt: "制作一份 8 页以内的季度业务汇报 PPT", base64_images: [] }, "/v1/psd/generations": { prompt: "按原图位置拆分海报元素并合成可编辑 PSD", base64_images: ["data:image/png;base64,..."] }, }; const body = bodies[item.path]; return { path, body: body === undefined ? "" : JSON.stringify(body, null, 2), }; } function compactResponseValue(value: unknown, depth = 0): unknown { if (typeof value === "string") { if (value.length <= 1200) return value; return `${value.slice(0, 500)}\n… 已省略 ${value.length - 500} 个字符(原始长度 ${value.length.toLocaleString()})`; } if (Array.isArray(value)) { const items = value.slice(0, 20).map((item) => compactResponseValue(item, depth + 1)); if (value.length > 20) items.push(`… 另有 ${value.length - 20} 项`); return items; } if (value && typeof value === "object") { if (depth > 8) return "[内容层级过深,已省略]"; return Object.fromEntries( Object.entries(value as Record) .slice(0, 60) .map(([key, item]) => [key, compactResponseValue(item, depth + 1)]), ); } return value; } function formatOnlineResponse(text: string) { if (!text) return "(响应体为空)"; try { return JSON.stringify(compactResponseValue(JSON.parse(text)), null, 2); } catch { return text.length > 30_000 ? `${text.slice(0, 30_000)}\n\n… 已省略 ${text.length - 30_000} 个字符` : text; } } function ApiOnlinePlaygroundLegacy({ item, serviceBaseUrl, authKey, example }: { item: ApiDoc; serviceBaseUrl: string; authKey: string; example: string }) { const defaults = onlineDefaults(item); const [isOpen, setIsOpen] = useState(false); const [requestPath, setRequestPath] = useState(defaults.path); const [requestBody, setRequestBody] = useState(defaults.body); const [isCalling, setIsCalling] = useState(false); const [result, setResult] = useState(null); const abortRef = useRef(null); const reset = () => { setRequestPath(defaults.path); setRequestBody(defaults.body); setResult(null); }; const call = async () => { const trimmedPath = requestPath.trim(); if (!trimmedPath) return; let parsedBody: unknown; if (item.method !== "GET" && requestBody.trim()) { try { parsedBody = JSON.parse(requestBody); } catch (error) { setResult({ ok: false, status: 0, statusText: "请求 JSON 无效", durationMs: 0, contentType: "application/json", body: error instanceof Error ? error.message : "请检查 JSON 格式。", }); return; } } const controller = new AbortController(); abortRef.current = controller; setIsCalling(true); setResult(null); const startedAt = Date.now(); try { const targetUrl = /^https?:\/\//i.test(trimmedPath) ? trimmedPath : `${serviceBaseUrl}${trimmedPath.startsWith("/") ? "" : "/"}${trimmedPath}`; const headers: Record = {}; if (authKey) headers.Authorization = `Bearer ${authKey}`; if (item.method !== "GET") headers["Content-Type"] = "application/json"; const response = await fetch(targetUrl, { method: item.method, headers, body: item.method === "GET" ? undefined : JSON.stringify(parsedBody ?? {}), signal: controller.signal, }); const contentType = response.headers.get("content-type") || ""; let responseBody = ""; if (/application\/json|text\//i.test(contentType)) { responseBody = formatOnlineResponse(await response.text()); } else { const blob = await response.blob(); responseBody = `二进制响应\n类型:${blob.type || contentType || "未知"}\n大小:${blob.size.toLocaleString()} bytes`; } setResult({ ok: response.ok, status: response.status, statusText: response.statusText, durationMs: Date.now() - startedAt, contentType, body: responseBody, }); } catch (error) { const aborted = error instanceof DOMException && error.name === "AbortError"; setResult({ ok: false, status: 0, statusText: aborted ? "已取消" : "网络错误", durationMs: Date.now() - startedAt, contentType: "", body: aborted ? "请求已由用户取消。" : error instanceof Error ? error.message : "请求失败", }); } finally { abortRef.current = null; setIsCalling(false); } }; return (

调用示例

{example}
{isOpen ? (
{item.method} setRequestPath(event.target.value)} className="h-9 min-w-0 flex-1 rounded-lg border-stone-200 bg-white font-mono text-xs" aria-label={`${item.title} 在线调用地址`} placeholder="/v1/..." />
{isCalling ? ( ) : ( )}
{item.method !== "GET" ? (