feat(web): 分类展示公开 API 文档并修复 DEV 启动 #27
@@ -2,8 +2,43 @@ import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { ApiDocSection, TaskForm } from '../types';
|
||||
import { ApiDocsPage } from './ApiDocsPage';
|
||||
import { publicApiCatalogGroups, publicApiEndpointCount } from './api-docs-catalog';
|
||||
|
||||
describe('ApiDocsPage extended task documentation', () => {
|
||||
it('separates the complete public catalog into common and compatibility interfaces', () => {
|
||||
const endpoints = publicApiCatalogGroups.flatMap((group) => group.endpoints);
|
||||
|
||||
expect(publicApiEndpointCount()).toBe(71);
|
||||
expect(publicApiEndpointCount('open')).toBe(54);
|
||||
expect(publicApiEndpointCount('compatibility')).toBe(17);
|
||||
expect(endpoints.every((endpoint) => endpoint.path.startsWith('/api/v1/'))).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the common open API catalog as a complete onboarding page', () => {
|
||||
const html = renderDocs('openApiOverview', { kind: 'chat.completions', model: 'gpt-4o-mini', prompt: '你好' });
|
||||
|
||||
expect(html).toContain('通用开放接口');
|
||||
expect(html).toContain('兼容接口');
|
||||
expect(html).toContain('https://ai.51easyai.com/api/v1');
|
||||
expect(html).toContain('接口数量');
|
||||
expect(html).toContain('>54<');
|
||||
expect(html).toContain('/api/v1/videos/generations');
|
||||
expect(html).toContain('/api/v1/resource/material');
|
||||
expect(html).toContain('不要求调用方显式传入');
|
||||
});
|
||||
|
||||
it('documents compatibility families and Kling drop-in configuration separately', () => {
|
||||
const html = renderDocs('compatKling', { kind: 'videos.generations', model: 'kling-v3-omni', prompt: '雨夜街道' });
|
||||
|
||||
expect(html).toContain('可灵兼容接口');
|
||||
expect(html).toContain('https://ai.51easyai.com/api/v1/kling');
|
||||
expect(html).toContain('/api/v1/kling/v1/videos/omni-video');
|
||||
expect(html).toContain('/api/v1/kling/v2/omni-video/{model}');
|
||||
expect(html).toContain('kling-video-o1');
|
||||
expect(html).toContain('kling-v3-omni');
|
||||
expect(html).toContain('上游凭据不会下发');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['guideBaseUrl', '前往创建 API Key'],
|
||||
['guideWebhook', 'TASK_PROGRESS_CALLBACK_ENABLED'],
|
||||
|
||||
@@ -6,14 +6,24 @@ 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?: 'GET' | 'POST';
|
||||
method?: PublicApiMethod;
|
||||
path?: string;
|
||||
surface: PublicApiSurface;
|
||||
title: string;
|
||||
}
|
||||
|
||||
@@ -35,17 +45,23 @@ interface ApiGuideItem {
|
||||
}
|
||||
|
||||
export const apiDocs: ApiDocItem[] = [
|
||||
{ key: 'chat', group: '文本', kind: 'chat.completions', method: 'POST', path: '/api/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口,支持本地 API Key 授权、simulation 测试和非流式/流式响应。' },
|
||||
{ key: 'responses', group: '文本', kind: 'responses', method: 'POST', path: '/api/v1/responses', title: 'Responses', lead: 'OpenAI 兼容的 Responses 接口,原生支持 input、previous_response_id、工具调用和流式输出;不支持原生 Responses 的模型会由网关转换到 Chat Completions。' },
|
||||
{ key: 'embeddings', group: '文本', kind: 'embeddings', method: 'POST', path: '/api/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口,可直接用 input 数组或字符串生成 embedding,API Key 需要 embedding 权限。' },
|
||||
{ key: 'reranks', group: '文本', kind: 'reranks', method: 'POST', path: '/api/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口,传入 query 和 documents 后返回 relevance_score,API Key 需要 rerank 权限。' },
|
||||
{ key: 'imageGeneration', group: '图片', kind: 'images.generations', method: 'POST', path: '/api/v1/images/generations', title: '创建图片', lead: 'OpenAI 兼容的图片生成接口,支持 prompt、size、quality 和 simulation 测试。' },
|
||||
{ key: 'imageEdit', group: '图片', kind: 'images.edits', method: 'POST', path: '/api/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: '/api/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源,后续请求可复用返回的文件 URL。' },
|
||||
{ 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[] = [
|
||||
@@ -98,9 +114,14 @@ export function ApiDocsPage(props: {
|
||||
const currentApiDoc = apiDocs.find((item) => item.key === props.activeDocSection) ?? (activeGuide ? undefined : apiDocs[0]);
|
||||
const current = activeGuide ?? currentApiDoc ?? apiDocs[0];
|
||||
const [opsSkillMetadata, setOpsSkillMetadata] = useState<GatewaySkillBundleMetadata>(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);
|
||||
@@ -218,28 +239,46 @@ export function ApiDocsPage(props: {
|
||||
</div>
|
||||
<label className="docsSearch">
|
||||
<Search size={15} />
|
||||
<input placeholder="搜索" />
|
||||
<input
|
||||
aria-label="搜索 API 文档"
|
||||
placeholder="搜索名称或路径"
|
||||
value={docsSearch}
|
||||
onChange={(event) => setDocsSearch(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<DocsGroup
|
||||
title="指南"
|
||||
items={guideItems.map((item) => ({
|
||||
active: item.key === props.activeDocSection,
|
||||
title: item.title,
|
||||
onClick: () => handleGuideClick(item),
|
||||
}))}
|
||||
/>
|
||||
{groupDocs(apiDocs).map((group) => (
|
||||
{visibleGuideItems.length > 0 && (
|
||||
<DocsGroup
|
||||
key={group.title}
|
||||
title={group.title}
|
||||
items={group.items.map((item) => ({
|
||||
title="指南"
|
||||
items={visibleGuideItems.map((item) => ({
|
||||
active: item.key === props.activeDocSection,
|
||||
method: item.method,
|
||||
title: item.title,
|
||||
onClick: () => handleDocClick(item),
|
||||
onClick: () => handleGuideClick(item),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
{groupDocsBySurface(visibleApiDocs).map((surface) => (
|
||||
<section className="docsSurface" data-surface={surface.key} key={surface.key}>
|
||||
<div className="docsSurfaceHeader">
|
||||
<strong>{apiSurfaceTitle(surface.key)}</strong>
|
||||
<span>{publicApiEndpointCount(surface.key)} 项</span>
|
||||
</div>
|
||||
{groupDocs(surface.items).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: () => handleDocClick(item),
|
||||
}))}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
{normalizedDocsSearch && visibleGuideItems.length === 0 && visibleApiDocs.length === 0 && (
|
||||
<div className="docsSearchEmpty">没有找到匹配的接口或指南</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<main className="docsArticle">
|
||||
@@ -253,7 +292,13 @@ export function ApiDocsPage(props: {
|
||||
)}
|
||||
<p className="docsLead">{current.lead}</p>
|
||||
|
||||
{isAsyncModeDoc ? (
|
||||
{isCatalogDoc && currentApiDoc ? (
|
||||
<ApiCatalogDetails
|
||||
family={currentApiDoc.catalogFamily}
|
||||
search={normalizedDocsSearch}
|
||||
surface={currentApiDoc.catalogSurface ?? currentApiDoc.surface}
|
||||
/>
|
||||
) : isAsyncModeDoc ? (
|
||||
<DocDetails section="asyncMode" />
|
||||
) : currentApiDoc ? (
|
||||
<>
|
||||
@@ -406,11 +451,15 @@ export function ApiDocsPage(props: {
|
||||
)}
|
||||
</form> : (
|
||||
<section className="docsGuideAside">
|
||||
<header><strong>{isAsyncModeDoc ? '能力说明' : '指南导航'}</strong></header>
|
||||
<header><strong>{isCatalogDoc ? '接口分类' : isAsyncModeDoc ? '能力说明' : '指南导航'}</strong></header>
|
||||
<div>
|
||||
<span>当前章节</span>
|
||||
<strong>{current.title}</strong>
|
||||
<p>{isAsyncModeDoc ? '异步模式不改变原接口的请求参数,只改变任务提交后的等待和响应方式。' : '使用左侧目录切换其他指南或 API 接口,浏览器前进、后退可保持当前章节。'}</p>
|
||||
<p>{isCatalogDoc
|
||||
? '所有新接入路径统一以 /api/v1 开头。选择兼容协议时只替换 Base URL 和 Gateway API Key。'
|
||||
: isAsyncModeDoc
|
||||
? '异步模式不改变原接口的请求参数,只改变任务提交后的等待和响应方式。'
|
||||
: '使用左侧目录切换其他指南或 API 接口,浏览器前进、后退可保持当前章节。'}</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -430,6 +479,151 @@ export function ApiDocsPage(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function ApiCatalogDetails(props: { family?: PublicApiFamily; search: string; surface: PublicApiSurface }) {
|
||||
const catalogGroups = publicApiCatalogGroups.filter((group) => (
|
||||
group.surface === props.surface && (!props.family || group.family === props.family)
|
||||
));
|
||||
const visibleGroups = catalogGroups
|
||||
.map((group) => {
|
||||
const groupMatches = searchText([group.title, group.description, group.family], props.search);
|
||||
const endpoints = groupMatches
|
||||
? group.endpoints
|
||||
: group.endpoints.filter((endpoint) => searchText([endpoint.method, endpoint.path, endpoint.description], props.search));
|
||||
return { ...group, endpoints };
|
||||
})
|
||||
.filter((group) => group.endpoints.length > 0);
|
||||
const endpointCount = catalogGroups.reduce((total, group) => total + group.endpoints.length, 0);
|
||||
const visibleEndpointCount = visibleGroups.reduce((total, group) => total + group.endpoints.length, 0);
|
||||
const isCompatibility = props.surface === 'compatibility';
|
||||
const baseUrl = props.family === 'kling'
|
||||
? 'https://ai.51easyai.com/api/v1/kling'
|
||||
: 'https://ai.51easyai.com/api/v1';
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="docsCatalogSummary" aria-label="API 接入摘要">
|
||||
<article>
|
||||
<span>{props.family === 'kling' ? '可灵客户端 Base URL' : '生产 Base URL'}</span>
|
||||
<code>{baseUrl}</code>
|
||||
</article>
|
||||
<article>
|
||||
<span>生成接口鉴权</span>
|
||||
<code>Authorization: Bearer {'<Gateway API Key>'}</code>
|
||||
</article>
|
||||
<article>
|
||||
<span>接口数量</span>
|
||||
<strong>{endpointCount}</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="docsCatalogNotice" data-surface={props.surface}>
|
||||
<strong>{isCompatibility ? '存量协议接入规则' : '新业务接入规则'}</strong>
|
||||
<p>{isCompatibility
|
||||
? '保留对应厂商或存量系统的请求字段、响应包络和任务语义。调用方替换 Base URL 与 Gateway API Key 即可,上游凭据不会下发。'
|
||||
: '优先使用通用开放接口。所有路径统一位于 /api/v1;模型能力由模型目录与服务端配置决定,不要求额外传入 modelType。'}</p>
|
||||
</section>
|
||||
|
||||
{props.surface === 'open' && <OpenApiUsageNotes />}
|
||||
{isCompatibility && <CompatibilityUsageNotes family={props.family} />}
|
||||
|
||||
{props.search && (
|
||||
<p className="docsCatalogSearchResult">当前筛选显示 {visibleEndpointCount} / {endpointCount} 项接口</p>
|
||||
)}
|
||||
{visibleGroups.map((group) => (
|
||||
<section className="paramCard docsCatalogGroup" key={group.family}>
|
||||
<header>
|
||||
<div>
|
||||
<h2>{group.title}</h2>
|
||||
<p>{group.description}</p>
|
||||
</div>
|
||||
<Badge variant="outline">{group.endpoints.length} 项</Badge>
|
||||
</header>
|
||||
<div className="docsCatalogEndpoints">
|
||||
{group.endpoints.map((endpoint) => (
|
||||
<div className="docsCatalogEndpoint" key={`${endpoint.method}-${endpoint.path}`}>
|
||||
<MethodBadge method={endpoint.method} />
|
||||
<code>{endpoint.path}</code>
|
||||
<span>{endpoint.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
{visibleGroups.length === 0 && (
|
||||
<div className="docsCatalogEmpty">当前分类中没有匹配“{props.search}”的接口。</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenApiUsageNotes() {
|
||||
return (
|
||||
<section className="paramCard docsDetailCard">
|
||||
<header><h2>统一调用约定</h2></header>
|
||||
<div className="docsStatusGrid">
|
||||
<code>Base URL</code><p>SDK 配置到 <code>https://ai.51easyai.com/api/v1</code>,请求资源路径时不要再次拼接 <code>/api/v1</code>。</p>
|
||||
<code>同步 / 异步</code><p>生成接口保留原请求 Body;需要持久化异步任务时增加 <code>X-Async: true</code>,再通过 <code>/tasks/{'{taskID}'}</code> 取回结果。</p>
|
||||
<code>全模态能力</code><p>模型可直接使用服务端声明的文生图、文生视频、图生视频等能力,不要求调用方显式传入 <code>modelType</code>。</p>
|
||||
<code>公开端点</code><p>健康检查和 public 目录无需登录;账号管理使用登录会话,生成与任务接口使用 Gateway API Key。</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CompatibilityUsageNotes(props: { family?: PublicApiFamily }) {
|
||||
if (props.family === 'kling') {
|
||||
return (
|
||||
<section className="paramCard docsDetailCard">
|
||||
<header><h2>可灵 V1 / API 2.0 约定</h2></header>
|
||||
<div className="docsStatusGrid">
|
||||
<code>SDK Base URL</code><p>设置为 <code>https://ai.51easyai.com/api/v1/kling</code>,之后按原客户端继续请求 <code>/v1/...</code> 或 <code>/v2/...</code>。</p>
|
||||
<code>O1</code><p>使用 <code>kling-video-o1</code>;兼容别名 <code>kling-o1</code> 会自动归一。</p>
|
||||
<code>3.0 Omni</code><p>使用 <code>kling-v3-omni</code>;兼容 <code>kling-3.0-omni</code> 和 <code>kling-3-omni</code>。</p>
|
||||
<code>鉴权</code><p>客户端只发送 Gateway API Key。可灵中国区 AK/SK 保存在网关平台配置中。</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
if (props.family === 'gemini') {
|
||||
return (
|
||||
<section className="paramCard docsDetailCard">
|
||||
<header><h2>Gemini 接入约定</h2></header>
|
||||
<div className="docsDetailContent">
|
||||
<p><code>generateContent</code> 保留 Gemini 内容结构;Files 上传的 <code>version</code> 支持 <code>v1</code> 和 <code>v1beta</code>。新接入路径仍统一放在 Gateway 的 <code>/api/v1</code> 前缀下。</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
if (props.family === 'volces') {
|
||||
return (
|
||||
<section className="paramCard docsDetailCard">
|
||||
<header><h2>火山任务约定</h2></header>
|
||||
<div className="docsDetailContent">
|
||||
<p>创建、列表、查询和删除接口保留火山任务结构。旧 <code>/api/v3</code> 仅作为兼容别名,不应写入新客户端配置。</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
if (props.family === 'serverMain') {
|
||||
return (
|
||||
<section className="paramCard docsDetailCard">
|
||||
<header><h2>server-main 接入约定</h2></header>
|
||||
<div className="docsDetailContent">
|
||||
<p>提交接口返回兼容的 <code>submitted</code> 和 <code>task_id</code>,现有调用方继续通过 <code>/ai/result/{'{taskID}'}</code> 轮询结果。</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<section className="paramCard docsDetailCard">
|
||||
<header><h2>兼容层边界</h2></header>
|
||||
<div className="docsDetailContent">
|
||||
<p>兼容接口用于无缝迁移已有 SDK 或存量调用。新业务优先选择通用开放接口,以获得统一的异步任务、结果归一、计费和模型能力语义。</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function OpsManagementSkillCard(props: { metadata: GatewaySkillBundleMetadata }) {
|
||||
const modules = props.metadata.modules.map(skillModuleLabel).join('、');
|
||||
return (
|
||||
@@ -787,18 +981,63 @@ function supportsAsyncMode(section: ApiDocSection) {
|
||||
|| section === 'videoGeneration';
|
||||
}
|
||||
|
||||
function MethodBadge(props: { method: NonNullable<ApiDocItem['method']> }) {
|
||||
return <Badge variant={props.method === 'GET' ? 'success' : 'warning'}>{props.method}</Badge>;
|
||||
function MethodBadge(props: { method: PublicApiMethod }) {
|
||||
const variant = props.method === 'GET'
|
||||
? 'success'
|
||||
: props.method === 'DELETE'
|
||||
? 'destructive'
|
||||
: props.method === 'PATCH'
|
||||
? 'secondary'
|
||||
: 'warning';
|
||||
return <Badge variant={variant}>{props.method}</Badge>;
|
||||
}
|
||||
|
||||
function groupDocs(items: typeof apiDocs) {
|
||||
const groups = new Map<string, typeof apiDocs>();
|
||||
function groupDocs(items: ApiDocItem[]) {
|
||||
const groups = new Map<string, ApiDocItem[]>();
|
||||
for (const item of items) {
|
||||
groups.set(item.group, [...(groups.get(item.group) ?? []), item]);
|
||||
}
|
||||
return Array.from(groups, ([title, groupItems]) => ({ title, items: groupItems }));
|
||||
}
|
||||
|
||||
function groupDocsBySurface(items: ApiDocItem[]) {
|
||||
const groups = new Map<PublicApiSurface, ApiDocItem[]>();
|
||||
for (const item of items) {
|
||||
groups.set(item.surface, [...(groups.get(item.surface) ?? []), item]);
|
||||
}
|
||||
return Array.from(groups, ([key, surfaceItems]) => ({ key, items: surfaceItems }));
|
||||
}
|
||||
|
||||
function apiSurfaceTitle(surface: PublicApiSurface) {
|
||||
return surface === 'open' ? '通用开放接口' : '兼容接口';
|
||||
}
|
||||
|
||||
function guideMatchesSearch(item: ApiGuideItem, search: string) {
|
||||
return searchText([item.title, item.lead, item.group], search);
|
||||
}
|
||||
|
||||
function apiDocMatchesSearch(item: ApiDocItem, search: string) {
|
||||
if (searchText([item.title, item.lead, item.path, item.group, apiSurfaceTitle(item.surface)], search)) {
|
||||
return true;
|
||||
}
|
||||
if (!item.catalogSurface && !item.catalogFamily) return false;
|
||||
return publicApiCatalogGroups
|
||||
.filter((group) => (
|
||||
(!item.catalogSurface || group.surface === item.catalogSurface)
|
||||
&& (!item.catalogFamily || group.family === item.catalogFamily)
|
||||
))
|
||||
.some((group) => searchText([
|
||||
group.title,
|
||||
group.description,
|
||||
...group.endpoints.flatMap((endpoint) => [endpoint.method, endpoint.path, endpoint.description]),
|
||||
], search));
|
||||
}
|
||||
|
||||
function searchText(values: Array<string | undefined>, search: string) {
|
||||
if (!search) return true;
|
||||
return values.some((value) => value?.toLowerCase().includes(search));
|
||||
}
|
||||
|
||||
function defaultTaskForDoc(kind: TaskForm['kind'], current: TaskForm, taskResult: GatewayTask | null): TaskForm {
|
||||
const next = defaultTaskForKind(kind, current);
|
||||
if (kind === 'tasks.retrieve' && !next.taskId && taskResult && !taskResult.id.startsWith('docs-')) {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
export type PublicApiMethod = 'DELETE' | 'GET' | 'PATCH' | 'POST';
|
||||
export type PublicApiSurface = 'compatibility' | 'open';
|
||||
export type PublicApiFamily =
|
||||
| 'account'
|
||||
| 'discovery'
|
||||
| 'gemini'
|
||||
| 'generation'
|
||||
| 'kling'
|
||||
| 'models'
|
||||
| 'portrait'
|
||||
| 'security'
|
||||
| 'serverMain'
|
||||
| 'tasks'
|
||||
| 'volces';
|
||||
|
||||
export interface PublicApiEndpoint {
|
||||
description: string;
|
||||
method: PublicApiMethod;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface PublicApiCatalogGroup {
|
||||
description: string;
|
||||
endpoints: PublicApiEndpoint[];
|
||||
family: PublicApiFamily;
|
||||
surface: PublicApiSurface;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const publicApiCatalogGroups: PublicApiCatalogGroup[] = [
|
||||
{
|
||||
family: 'discovery',
|
||||
surface: 'open',
|
||||
title: '运行状态与接口发现',
|
||||
description: '健康检查、OpenAPI 定义以及无需登录即可读取的公开目录和运维资源。',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/api/v1/healthz', description: '存活检查' },
|
||||
{ method: 'GET', path: '/api/v1/readyz', description: '就绪检查' },
|
||||
{ method: 'GET', path: '/api/v1/openapi.json', description: 'OpenAPI JSON' },
|
||||
{ method: 'GET', path: '/api/v1/openapi.yaml', description: 'OpenAPI YAML' },
|
||||
{ method: 'GET', path: '/api/v1/public/identity', description: '公开身份配置' },
|
||||
{ method: 'GET', path: '/api/v1/public/client-customization', description: '公开客户端配置' },
|
||||
{ method: 'GET', path: '/api/v1/public/catalog/providers', description: '公开供应商目录' },
|
||||
{ method: 'GET', path: '/api/v1/public/catalog/base-models', description: '公开基础模型目录' },
|
||||
{ method: 'GET', path: '/api/v1/public/skills/ai-gateway-ops-management/metadata', description: '运维 Skill 元数据' },
|
||||
{ method: 'GET', path: '/api/v1/public/skills/ai-gateway-ops-management/download', description: '下载运维 Skill' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'account',
|
||||
surface: 'open',
|
||||
title: '账号、授权与 API Key',
|
||||
description: '账号登录使用 JWT 或 OIDC,会话建立后可创建和管理供开放接口使用的 Gateway API Key。',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/api/v1/auth/register', description: '注册本地账号' },
|
||||
{ method: 'POST', path: '/api/v1/auth/login', description: '本地账号登录' },
|
||||
{ method: 'GET', path: '/api/v1/auth/oidc/login', description: '发起 OIDC 登录' },
|
||||
{ method: 'GET', path: '/api/v1/auth/oidc/callback', description: 'OIDC 回调' },
|
||||
{ method: 'POST', path: '/api/v1/auth/oidc/logout', description: 'OIDC 登出' },
|
||||
{ method: 'DELETE', path: '/api/v1/auth/oidc/session', description: '删除浏览器会话' },
|
||||
{ method: 'GET', path: '/api/v1/me', description: '查询当前用户' },
|
||||
{ method: 'GET', path: '/api/v1/api-keys', description: '查询 API Key' },
|
||||
{ method: 'POST', path: '/api/v1/api-keys', description: '创建 API Key' },
|
||||
{ method: 'GET', path: '/api/v1/api-keys/access-rules', description: '查询 Key 访问规则' },
|
||||
{ method: 'POST', path: '/api/v1/api-keys/access-rules/batch', description: '批量设置 Key 访问规则' },
|
||||
{ method: 'GET', path: '/api/v1/api-keys/assignable-models', description: '查询可分配模型' },
|
||||
{ method: 'PATCH', path: '/api/v1/api-keys/{apiKeyID}/scopes', description: '更新 Key 权限范围' },
|
||||
{ method: 'PATCH', path: '/api/v1/api-keys/{apiKeyID}/disable', description: '禁用 Key' },
|
||||
{ method: 'DELETE', path: '/api/v1/api-keys/{apiKeyID}', description: '删除 Key' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'models',
|
||||
surface: 'open',
|
||||
title: '模型、平台与计费',
|
||||
description: '查询账号可用的模型、平台、模型能力和 Playground 目录,并在提交前预估费用。',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/api/v1/model-catalog', description: '模型能力目录' },
|
||||
{ method: 'GET', path: '/api/v1/platforms', description: '当前用户可用平台' },
|
||||
{ method: 'GET', path: '/api/v1/models', description: '当前用户可用模型' },
|
||||
{ method: 'GET', path: '/api/v1/playground/models', description: 'Playground 可用模型' },
|
||||
{ method: 'POST', path: '/api/v1/pricing/estimate', description: '请求价格预估' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'generation',
|
||||
surface: 'open',
|
||||
title: '通用生成接口',
|
||||
description: '统一使用 Gateway 模型名和参数,覆盖文本、图片、视频、音乐、语音和文件上传。',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/api/v1/chat/completions', description: 'Chat Completions,支持 SSE' },
|
||||
{ method: 'POST', path: '/api/v1/responses', description: 'Responses,支持 SSE' },
|
||||
{ method: 'POST', path: '/api/v1/embeddings', description: '文本向量' },
|
||||
{ method: 'POST', path: '/api/v1/reranks', description: '文本重排序' },
|
||||
{ method: 'POST', path: '/api/v1/images/generations', description: '文生图' },
|
||||
{ method: 'POST', path: '/api/v1/images/edits', description: '图片编辑' },
|
||||
{ method: 'POST', path: '/api/v1/videos/generations', description: '文生视频、图生视频及多模态视频' },
|
||||
{ method: 'POST', path: '/api/v1/song/generations', description: '歌曲生成' },
|
||||
{ method: 'POST', path: '/api/v1/music/generations', description: '音乐生成' },
|
||||
{ method: 'POST', path: '/api/v1/speech/generations', description: '语音生成' },
|
||||
{ method: 'POST', path: '/api/v1/voice_clone', description: '声音克隆' },
|
||||
{ method: 'GET', path: '/api/v1/voice_clone/voices', description: '查询克隆声音' },
|
||||
{ method: 'DELETE', path: '/api/v1/voice_clone/voices/{voiceID}', description: '删除克隆声音' },
|
||||
{ method: 'POST', path: '/api/v1/files/upload', description: '上传生成任务输入文件' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'tasks',
|
||||
surface: 'open',
|
||||
title: '异步任务',
|
||||
description: '通用生成接口使用 X-Async: true 后,通过这些接口查询、取消任务并读取进度和预处理记录。',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/api/v1/tasks', description: '查询任务列表' },
|
||||
{ method: 'GET', path: '/api/v1/tasks/{taskID}', description: '查询任务详情和结果' },
|
||||
{ method: 'POST', path: '/api/v1/tasks/{taskID}/cancel', description: '取消任务' },
|
||||
{ method: 'GET', path: '/api/v1/tasks/{taskID}/events', description: '查询任务事件' },
|
||||
{ method: 'GET', path: '/api/v1/tasks/{taskID}/param-preprocessing', description: '查询参数预处理记录' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'portrait',
|
||||
surface: 'open',
|
||||
title: '真人资产',
|
||||
description: '查询、上传并同步用户真人素材,供支持真人资产的通用视频模型使用。',
|
||||
endpoints: [
|
||||
{ method: 'GET', path: '/api/v1/resource/material/seedance-portrait-assets/capability', description: '查询真人资产能力' },
|
||||
{ method: 'GET', path: '/api/v1/resource/material/user/materials', description: '查询用户真人资产' },
|
||||
{ method: 'POST', path: '/api/v1/resource/material', description: '上传真人资产' },
|
||||
{ method: 'POST', path: '/api/v1/resource/material/seedance-portrait-assets/sync', description: '同步真人资产到平台' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'security',
|
||||
surface: 'open',
|
||||
title: '安全集成',
|
||||
description: '按 RFC 8935 接收安全事件;该端点按 SSF 协议鉴权,不使用普通生成 API Key。',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/api/v1/security-events/ssf', description: 'RFC 8935 Security Event 接收端点' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'gemini',
|
||||
surface: 'compatibility',
|
||||
title: 'Gemini 兼容接口',
|
||||
description: '保留 Gemini generateContent 与 Files 上传请求结构,调用方改用 Gateway Base URL 和 API Key。',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/api/v1/models/{model}:generateContent', description: 'Gemini generateContent' },
|
||||
{ method: 'POST', path: '/api/v1/gemini/upload/{version}/files', description: 'Gemini Files 启动或直接上传' },
|
||||
{ method: 'POST', path: '/api/v1/gemini/upload/{version}/files/{uploadID}', description: '完成 Gemini 分段上传' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'kling',
|
||||
surface: 'compatibility',
|
||||
title: '可灵兼容接口',
|
||||
description: '兼容中国区可灵 V1 AK/SK Omni 和 API 2.0 协议;调用方只使用 Gateway API Key。',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/api/v1/videos/omni-video', description: '可灵官方 V1 Omni 创建任务' },
|
||||
{ method: 'GET', path: '/api/v1/videos/omni-video/{taskID}', description: '可灵官方 V1 Omni 查询任务' },
|
||||
{ method: 'POST', path: '/api/v1/kling/v1/videos/omni-video', description: '网关可灵 V1 创建任务' },
|
||||
{ method: 'GET', path: '/api/v1/kling/v1/videos/omni-video', description: '网关可灵 V1 任务列表' },
|
||||
{ method: 'GET', path: '/api/v1/kling/v1/videos/omni-video/{taskID}', description: '网关可灵 V1 查询任务' },
|
||||
{ method: 'POST', path: '/api/v1/kling/v2/omni-video/{model}', description: '网关可灵 API 2.0 创建任务' },
|
||||
{ method: 'GET', path: '/api/v1/kling/v2/tasks', description: '网关可灵 API 2.0 查询任务' },
|
||||
{ method: 'POST', path: '/api/v1/kling/v2/tasks', description: '网关可灵 API 2.0 任务列表' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'volces',
|
||||
surface: 'compatibility',
|
||||
title: '火山兼容接口',
|
||||
description: '兼容火山内容生成任务协议及任务列表、查询、取消操作。',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/api/v1/contents/generations/tasks', description: '创建火山内容生成任务' },
|
||||
{ method: 'GET', path: '/api/v1/contents/generations/tasks', description: '查询火山内容生成任务列表' },
|
||||
{ method: 'GET', path: '/api/v1/contents/generations/tasks/{taskID}', description: '查询火山内容生成任务' },
|
||||
{ method: 'DELETE', path: '/api/v1/contents/generations/tasks/{taskID}', description: '删除或取消火山内容生成任务' },
|
||||
],
|
||||
},
|
||||
{
|
||||
family: 'serverMain',
|
||||
surface: 'compatibility',
|
||||
title: 'server-main 兼容接口',
|
||||
description: '兼容 server-main 的视频提交与结果轮询响应结构,便于现有调用方只替换 Base URL 和 Key。',
|
||||
endpoints: [
|
||||
{ method: 'POST', path: '/api/v1/video/generations', description: 'server-main 兼容视频创建接口' },
|
||||
{ method: 'GET', path: '/api/v1/ai/result/{taskID}', description: 'server-main 兼容结果查询接口' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function publicApiEndpointCount(surface?: PublicApiSurface) {
|
||||
return publicApiCatalogGroups
|
||||
.filter((group) => !surface || group.surface === surface)
|
||||
.reduce((total, group) => total + group.endpoints.length, 0);
|
||||
}
|
||||
@@ -3,6 +3,12 @@ import { parseAppRoute, pathForApiDocSection } from './routing';
|
||||
|
||||
describe('API documentation routes', () => {
|
||||
it.each([
|
||||
['openApiOverview', '/docs/open-api'],
|
||||
['compatApiOverview', '/docs/compatible-api'],
|
||||
['compatGemini', '/docs/compatible-api/gemini'],
|
||||
['compatKling', '/docs/compatible-api/kling'],
|
||||
['compatVolces', '/docs/compatible-api/volces'],
|
||||
['compatServerMain', '/docs/compatible-api/server-main'],
|
||||
['guideBaseUrl', '/docs/auth'],
|
||||
['guideWebhook', '/docs/webhook'],
|
||||
['guideErrors', '/docs/errors'],
|
||||
@@ -24,4 +30,8 @@ describe('API documentation routes', () => {
|
||||
] as const)('keeps the legacy documentation URL %s working', (path, section) => {
|
||||
expect(parseAppRoute(path).apiDocSection).toBe(section);
|
||||
});
|
||||
|
||||
it('uses the common open API catalog as the documentation landing page', () => {
|
||||
expect(parseAppRoute('/docs').apiDocSection).toBe('openApiOverview');
|
||||
});
|
||||
});
|
||||
|
||||
+10
-4
@@ -12,7 +12,7 @@ export interface AppRouteState {
|
||||
export const defaultRouteState: AppRouteState = {
|
||||
activePage: 'home',
|
||||
adminSection: 'overview',
|
||||
apiDocSection: 'chat',
|
||||
apiDocSection: 'openApiOverview',
|
||||
playgroundMode: 'chat',
|
||||
workspaceSection: 'overview',
|
||||
workspaceTaskQuery: defaultWorkspaceTaskQuery(),
|
||||
@@ -43,6 +43,12 @@ const adminPaths: Record<AdminSection, string> = {
|
||||
};
|
||||
|
||||
const docsPaths: Record<ApiDocSection, string> = {
|
||||
openApiOverview: '/docs/open-api',
|
||||
compatApiOverview: '/docs/compatible-api',
|
||||
compatGemini: '/docs/compatible-api/gemini',
|
||||
compatKling: '/docs/compatible-api/kling',
|
||||
compatVolces: '/docs/compatible-api/volces',
|
||||
compatServerMain: '/docs/compatible-api/server-main',
|
||||
guideBaseUrl: '/docs/auth',
|
||||
guideWebhook: '/docs/webhook',
|
||||
guideErrors: '/docs/errors',
|
||||
@@ -129,7 +135,7 @@ export function pathForAdminSection(section: AdminSection) {
|
||||
}
|
||||
|
||||
export function pathForApiDocSection(section: ApiDocSection) {
|
||||
return docsPaths[section] ?? docsPaths.chat;
|
||||
return docsPaths[section] ?? docsPaths.openApiOverview;
|
||||
}
|
||||
|
||||
export function pathForPlaygroundMode(mode: PlaygroundMode) {
|
||||
@@ -148,7 +154,7 @@ function parseAdminSection(path: string): AdminSection {
|
||||
}
|
||||
|
||||
function parseDocSection(path: string): ApiDocSection {
|
||||
if (path === '/docs') return 'chat';
|
||||
if (path === '/docs') return 'openApiOverview';
|
||||
if (path === '/docs/playground') return 'guideTesting';
|
||||
if (path === '/docs/api/chat') return 'chat';
|
||||
if (path === '/docs/api/responses') return 'responses';
|
||||
@@ -157,7 +163,7 @@ function parseDocSection(path: string): ApiDocSection {
|
||||
if (path === '/docs/api/media') return 'imageGeneration';
|
||||
if (path === '/docs/api/videos') return 'videoGeneration';
|
||||
if (path === '/docs/api/tasks') return 'taskRetrieve';
|
||||
return docsSections[path] ?? 'chat';
|
||||
return docsSections[path] ?? 'openApiOverview';
|
||||
}
|
||||
|
||||
function parsePlaygroundMode(path: string): PlaygroundMode {
|
||||
|
||||
@@ -47,6 +47,46 @@
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.docsSurface {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin-top: 26px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.docsSurfaceHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.docsSurfaceHeader strong {
|
||||
color: var(--text-strong);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.docsSurfaceHeader span {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.docsSurface .docsGroup {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.docsSearchEmpty,
|
||||
.docsCatalogEmpty {
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-soft);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.docsGroup {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -120,6 +160,116 @@
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.docsCatalogSummary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1.4fr) minmax(100px, 0.45fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.docsCatalogSummary article {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.docsCatalogSummary span {
|
||||
color: var(--text-soft);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.docsCatalogSummary code {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--text-normal);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.docsCatalogSummary strong {
|
||||
color: var(--text-strong);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.docsCatalogNotice {
|
||||
padding: 16px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 10px;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.docsCatalogNotice[data-surface='compatibility'] {
|
||||
border-color: #ddd6fe;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
.docsCatalogNotice p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--text-soft);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.docsCatalogSearchResult {
|
||||
margin: 18px 0 0;
|
||||
color: var(--text-soft);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.docsCatalogGroup > header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.docsCatalogGroup > header > div:first-child {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.docsCatalogGroup header p {
|
||||
margin: 0;
|
||||
color: var(--text-soft);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.docsCatalogEndpoints {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.docsCatalogEndpoint {
|
||||
display: grid;
|
||||
grid-template-columns: 68px minmax(250px, 1.3fr) minmax(150px, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.docsCatalogEndpoint:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.docsCatalogEndpoint .shBadge {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.docsCatalogEndpoint code {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--text-normal);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.docsCatalogEndpoint span {
|
||||
color: var(--text-soft);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.agentResourceCard {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
@@ -476,6 +626,11 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.docsCatalogSummary,
|
||||
.docsCatalogEndpoint {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.agentResourceCard {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ export type PageKey = 'home' | 'playground' | 'models' | 'workspace' | 'admin' |
|
||||
export type PlaygroundMode = 'chat' | 'image' | 'video';
|
||||
export type WorkspaceSection = 'overview' | 'billing' | 'apiKeys' | 'tasks' | 'transactions';
|
||||
export type ApiDocSection =
|
||||
| 'openApiOverview'
|
||||
| 'compatApiOverview'
|
||||
| 'compatGemini'
|
||||
| 'compatKling'
|
||||
| 'compatVolces'
|
||||
| 'compatServerMain'
|
||||
| 'guideBaseUrl'
|
||||
| 'guideWebhook'
|
||||
| 'guideErrors'
|
||||
|
||||
Reference in New Issue
Block a user