TuringToken/web/src/pages/Docs/index.jsx

574 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
BookOpen,
Zap,
Key,
Terminal,
Box,
Gauge,
HelpCircle,
ChevronRight,
Copy,
Check,
ExternalLink,
ArrowRight,
Code,
Server,
Globe,
Shield,
Clock,
Sparkles,
Layers,
Menu,
X,
Rocket,
MessageSquare,
Image,
Music,
Video,
FileText,
} from 'lucide-react';
/* ─── DOCS SECTIONS ─── */
const DOC_SECTIONS = [
{ id: 'overview', icon: BookOpen, label: '概述', labelEn: 'Overview' },
{ id: 'quickstart', icon: Rocket, label: '快速开始', labelEn: 'Quick Start' },
{ id: 'apikey', icon: Key, label: 'API 密钥', labelEn: 'API Keys' },
{ id: 'chat', icon: MessageSquare, label: '对话补全', labelEn: 'Chat Completions' },
{ id: 'image', icon: Image, label: '图片生成', labelEn: 'Image Generation' },
{ id: 'audio', icon: Music, label: '语音 & 音频', labelEn: 'Audio & Speech' },
{ id: 'video', icon: Video, label: '视频生成', labelEn: 'Video Generation' },
{ id: 'models', icon: Box, label: '支持模型', labelEn: 'Supported Models' },
{ id: 'ratelimit', icon: Gauge, label: '速率限制', labelEn: 'Rate Limits' },
{ id: 'faq', icon: HelpCircle, label: '常见问题', labelEn: 'FAQ' },
];
/* ─── CodeBlock Component ─── */
function CodeBlock({ code, lang = 'bash' }) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
<div className="relative group my-4 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<div className="flex items-center justify-between px-4 py-2 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<span className="text-xs font-mono text-gray-500 dark:text-gray-400 uppercase">{lang}</span>
<button
onClick={copy}
className="flex items-center gap-1 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
{copied ? t('已复制') : t('复制')}
</button>
</div>
<pre className="p-4 overflow-x-auto text-sm font-mono text-gray-800 dark:text-gray-200 leading-relaxed">
<code>{code}</code>
</pre>
</div>
);
}
/* ─── SectionTitle ─── */
function SectionTitle({ id, icon: Icon, children }) {
return (
<h2 id={id} className="group flex items-center gap-3 mt-12 mb-5 scroll-mt-24">
<span className="flex items-center justify-center w-9 h-9 rounded-lg bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400">
<Icon size={18} />
</span>
<span className="text-2xl font-bold text-gray-900 dark:text-white">{children}</span>
</h2>
);
}
/* ─── SubSection ─── */
function SubSection({ children, className = '' }) {
return <h3 className={`text-lg font-semibold text-gray-800 dark:text-gray-200 mt-8 mb-3 ${className}`}>{children}</h3>;
}
/* ─── Paragraph ─── */
function P({ children, className = '' }) {
return <p className={`text-gray-600 dark:text-gray-400 leading-relaxed mb-4 ${className}`}>{children}</p>;
}
/* ─── Table Component ─── */
function DocsTable({ headers, rows }) {
return (
<div className="overflow-x-auto my-4 rounded-lg border border-gray-200 dark:border-gray-700">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 dark:bg-gray-800/50">
{headers.map((h, i) => (
<th key={i} className="px-4 py-3 text-left font-semibold text-gray-700 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
{h}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} className="border-b border-gray-100 dark:border-gray-800 last:border-0 hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors">
{row.map((cell, j) => (
<td key={j} className="px-4 py-3 text-gray-600 dark:text-gray-400">{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
/* ─── Info Card ─── */
function InfoCard({ icon: Icon, title, children, className = '' }) {
return (
<div className={`flex gap-4 p-5 rounded-xl border border-violet-200 dark:border-violet-800 bg-violet-50 dark:bg-violet-900/20 ${className}`}>
<span className="flex-shrink-0 mt-0.5 text-violet-500 dark:text-violet-400"><Icon size={20} /></span>
<div>
<h4 className="text-sm font-semibold text-violet-700 dark:text-violet-300 mb-1">{title}</h4>
<div className="text-sm text-violet-600 dark:text-violet-400">{children}</div>
</div>
</div>
);
}
/* ─── MAIN DOCS PAGE ─── */
export default function Docs() {
const { t } = useTranslation();
const [activeSection, setActiveSection] = useState('overview');
const [sidebarOpen, setSidebarOpen] = useState(false);
const contentRef = useRef(null);
// Scroll spy
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setActiveSection(entry.target.id);
break;
}
}
},
{ rootMargin: '-80px 0px -70% 0px', threshold: 0 }
);
const ids = DOC_SECTIONS.map((s) => s.id);
ids.forEach((id) => {
const el = document.getElementById(id);
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, []);
const scrollTo = (id) => {
setSidebarOpen(false);
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
};
const BASE_HOST = window.location.origin;
return (
<div className="min-h-screen bg-white dark:bg-gray-950">
{/* ── Top Nav Bar ── */}
<header className="sticky top-0 z-40 h-14 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-950/80 backdrop-blur-md">
<div className="flex items-center h-full px-4 lg:px-6 max-w-[1400px] mx-auto gap-3">
<button
className="lg:hidden p-2 -ml-2 rounded-md text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800"
onClick={() => setSidebarOpen(!sidebarOpen)}
>
{sidebarOpen ? <X size={20} /> : <Menu size={20} />}
</button>
<a href="/" className="flex items-center gap-2 flex-shrink-0">
<img src="/logo.jpg" alt="logo" className="w-7 h-7 rounded-full object-cover" />
<span className="font-heading text-lg font-bold tracking-tight text-gray-900 dark:text-white">TuringToken</span>
</a>
<span className="hidden sm:inline text-sm text-gray-400 dark:text-gray-500">|</span>
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">{t('文档')}</span>
</div>
</header>
<div className="max-w-[1400px] mx-auto flex">
{/* ── Sidebar (Desktop) ── */}
<aside className="hidden lg:block w-[240px] flex-shrink-0 sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto border-r border-gray-200 dark:border-gray-800 py-6 px-3">
<nav className="space-y-1">
{DOC_SECTIONS.map((s) => (
<button
key={s.id}
onClick={() => scrollTo(s.id)}
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-all duration-150 text-left
${activeSection === s.id
? 'bg-violet-50 dark:bg-violet-900/20 text-violet-700 dark:text-violet-300 font-semibold'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 hover:text-gray-900 dark:hover:text-white'
}`}
>
<s.icon size={16} className={activeSection === s.id ? 'text-violet-500' : 'text-gray-400'} />
{s.label}
{activeSection === s.id && <ChevronRight size={14} className="ml-auto text-violet-400" />}
</button>
))}
</nav>
</aside>
{/* ── Mobile Sidebar Overlay ── */}
{sidebarOpen && (
<>
<div className="fixed inset-0 z-40 bg-black/40 lg:hidden" onClick={() => setSidebarOpen(false)} />
<aside className="fixed top-14 left-0 z-50 w-[260px] h-[calc(100vh-3.5rem)] overflow-y-auto bg-white dark:bg-gray-950 border-r border-gray-200 dark:border-gray-800 py-6 px-3 lg:hidden">
<nav className="space-y-1">
{DOC_SECTIONS.map((s) => (
<button
key={s.id}
onClick={() => scrollTo(s.id)}
className={`w-full flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-sm transition-all text-left
${activeSection === s.id
? 'bg-violet-50 dark:bg-violet-900/20 text-violet-700 dark:text-violet-300 font-semibold'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50'
}`}
>
<s.icon size={16} className={activeSection === s.id ? 'text-violet-500' : 'text-gray-400'} />
{s.label}
</button>
))}
</nav>
</aside>
</>
)}
{/* ── Content ── */}
<main ref={contentRef} className="flex-1 min-w-0 py-8 px-6 lg:px-10 xl:px-16 max-w-4xl">
{/* ── Section: 概述 ── */}
<SectionTitle id="overview" icon={BookOpen}>{t('概述')}</SectionTitle>
<P>
TuringToken 是一个统一的 AI API 网关聚合了
<strong className="text-gray-900 dark:text-white"> 40+ 主流 AI 提供商</strong>OpenAIClaudeGeminiDeepSeekQwen
提供与 OpenAI API 完全兼容的接口格式一次接入即可访问所有模型无需分别对接各个厂商
</P>
<div className="grid sm:grid-cols-2 gap-4 mt-6 mb-8">
{[
{ icon: Server, title: '统一接口', desc: '与 OpenAI API 格式完全兼容,零改造迁移' },
{ icon: Globe, title: '多供应商聚合', desc: '一站式接入 40+ AI 厂商,按需切换' },
{ icon: Shield, title: '安全可靠', desc: 'API 密钥管理、速率限制、访问控制' },
{ icon: Clock, title: '按量计费', desc: '按 Token 用量计费,灵活成本控制' },
].map((f, i) => (
<div key={i} className="flex gap-3 p-4 rounded-xl border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/50">
<span className="flex-shrink-0 text-violet-500 dark:text-violet-400"><f.icon size={20} /></span>
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">{f.title}</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">{f.desc}</p>
</div>
</div>
))}
</div>
{/* ── Section: 快速开始 ── */}
<SectionTitle id="quickstart" icon={Rocket}>{t('快速开始')}</SectionTitle>
<P>只需三步即可接入 AI 能力</P>
<div className="space-y-6 mt-6">
<div className="flex gap-4">
<span className="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-violet-500 text-white text-sm font-bold">1</span>
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-1">{t('注册账号')}</h4>
<P className="!mb-0">访问 <a href="/register" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">注册页面</a> TuringToken </P>
</div>
</div>
<div className="flex gap-4">
<span className="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-violet-500 text-white text-sm font-bold">2</span>
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-1">{t('创建 API 密钥')}</h4>
<P className="!mb-0">进入 <a href="/console/token" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">控制台 API Key</a>""</P>
</div>
</div>
<div className="flex gap-4">
<span className="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-violet-500 text-white text-sm font-bold">3</span>
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-1">{t('发送请求')}</h4>
<P className="!mb-0">使用下方示例代码 Base URL 替换为 TuringToken 地址即可</P>
</div>
</div>
</div>
<InfoCard icon={Zap} title="Base URL" className="mt-6">
<code className="font-mono text-sm bg-violet-100 dark:bg-violet-900/40 px-2 py-0.5 rounded">{BASE_HOST}/v1</code>
</InfoCard>
{/* ── Section: API 密钥 ── */}
<SectionTitle id="apikey" icon={Key}>{t('API 密钥')}</SectionTitle>
<P>
API 密钥是与 TuringToken 通信的凭证每个密钥可以设置额度上限有效期和 IP 白名单
</P>
<SubSection>创建密钥</SubSection>
<P>
登录后进入 <a href="/console/token" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">控制台 API Key</a>
点击"新建 API Key"填写名称后即可生成请务必妥善保存密钥关闭页面后将无法再次查看完整密钥
</P>
<SubSection>使用密钥</SubSection>
<P>在所有 API 请求中通过 Bearer Token 方式传递密钥</P>
<CodeBlock
lang="http"
code={`Authorization: Bearer sk-your-api-key-here`}
/>
<InfoCard icon={Shield} title="安全提醒">
请勿将 API 密钥硬编码在前端代码或公开仓库中建议使用环境变量或后端代理方式管理密钥
</InfoCard>
{/* ── Section: 对话补全 ── */}
<SectionTitle id="chat" icon={MessageSquare}>{t('对话补全')}</SectionTitle>
<P>
对话补全是最常用的 API 接口支持多轮对话流式输出和 Function Calling
兼容 OpenAI Chat Completions API 格式
</P>
<SubSection>基础请求</SubSection>
<CodeBlock
lang="bash"
code={`curl ${BASE_HOST}/v1/chat/completions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer sk-your-api-key" \\
-d '{
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "你是一个有用的AI助手。"},
{"role": "user", "content": "你好,请介绍一下你自己。"}
]
}'`}
/>
<SubSection>流式输出</SubSection>
<P>设置 <code className="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-violet-600 dark:text-violet-400">stream: true</code> SSE </P>
<CodeBlock
lang="bash"
code={`curl ${BASE_HOST}/v1/chat/completions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer sk-your-api-key" \\
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "写一首关于春天的诗"}],
"stream": true
}'`}
/>
<SubSection>Python SDK 示例</SubSection>
<CodeBlock
lang="python"
code={`from openai import OpenAI
client = OpenAI(
base_url="${BASE_HOST}/v1",
api_key="sk-your-api-key",
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "你好,请介绍一下你自己。"}
],
)
print(response.choices[0].message.content)`}
/>
<SubSection>Node.js SDK 示例</SubSection>
<CodeBlock
lang="javascript"
code={`import OpenAI from 'openai';
const client = new OpenAI({
baseURL: '${BASE_HOST}/v1',
apiKey: 'sk-your-api-key',
});
const completion = await client.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'user', content: '你好,请介绍一下你自己。' }
],
});
console.log(completion.choices[0].message.content);`}
/>
{/* ── Section: 图片生成 ── */}
<SectionTitle id="image" icon={Image}>{t('图片生成')}</SectionTitle>
<P>
支持 DALL·EStable Diffusion 等多种图片生成模型兼容 OpenAI Images API 格式
</P>
<SubSection>生成图片</SubSection>
<CodeBlock
lang="bash"
code={`curl ${BASE_HOST}/v1/images/generations \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer sk-your-api-key" \\
-d '{
"model": "dall-e-3",
"prompt": "一只正在敲代码的可爱猫咪,赛博朋克风格",
"n": 1,
"size": "1024x1024"
}'`}
/>
{/* ── Section: 语音 & 音频 ── */}
<SectionTitle id="audio" icon={Music}>{t('语音 & 音频')}</SectionTitle>
<P>支持语音转文字STT和文字转语音TTS兼容 OpenAI Audio API 格式</P>
<SubSection>语音转文字 (STT)</SubSection>
<CodeBlock
lang="bash"
code={`curl ${BASE_HOST}/v1/audio/transcriptions \\
-H "Authorization: Bearer sk-your-api-key" \\
-F "file=@audio.mp3" \\
-F "model=whisper-1"`}
/>
<SubSection>文字转语音 (TTS)</SubSection>
<CodeBlock
lang="bash"
code={`curl ${BASE_HOST}/v1/audio/speech \\
-H "Authorization: Bearer sk-your-api-key" \\
-H "Content-Type: application/json" \\
-d '{
"model": "tts-1",
"input": "你好,欢迎使用 TuringToken",
"voice": "alloy"
}' \\
--output speech.mp3`}
/>
{/* ── Section: 视频生成 ── */}
<SectionTitle id="video" icon={Video}>{t('视频生成')}</SectionTitle>
<P>
TuringToken 集成了多家视频生成厂商可灵即梦ViduSora
支持文生视频和图生视频能力
</P>
<P>
视频生成接口因厂商而异详细参数请参考 <a href="/pricing" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">模型列表</a>
或访问 <a href="/console" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">控制台</a> 线
</P>
{/* ── Section: 支持模型 ── */}
<SectionTitle id="models" icon={Box}>{t('支持模型')}</SectionTitle>
<P>TuringToken 聚合了以下主流 AI 模型的 API 访问</P>
<DocsTable
headers={['供应商', '代表模型', '能力']}
rows={[
['OpenAI', 'GPT-4o, GPT-4o-mini, o3, o4-mini', '对话、推理、视觉'],
['Anthropic', 'Claude 3.5 Sonnet, Claude 3 Opus, Claude 4', '对话、长文本'],
['Google', 'Gemini 2.5 Pro, Gemini 2.5 Flash', '对话、多模态'],
['DeepSeek', 'DeepSeek-V3, DeepSeek-R1', '对话、推理'],
['阿里巴巴', 'Qwen3, Qwen-Max, Qwen-Plus', '对话、视觉'],
['智谱', 'GLM-4-Plus, GLM-4-Flash', '对话、工具调用'],
['Moonshot', 'Kimi-K2, Moonshot-v1', '对话、长文本'],
['字节跳动', 'Doubao-Pro, Seedance', '对话、视频'],
['MiniMax', 'abab6.5s, MiniMax-M1', '对话'],
['Mistral', 'Mistral Large, Mixtral 8x22B', '对话、开源'],
['xAI', 'Grok-3, Grok-2', '对话'],
['Cohere', 'Command R+, Command R', '对话、RAG'],
['Meta', 'Llama 4, Llama 3.3 70B', '对话、开源'],
['百度', 'ERNIE-4.0, ERNIE-Speed', '对话'],
['讯飞', 'Spark 4.0 Ultra', '对话'],
['腾讯', 'Hunyuan-Pro, Hunyuan-Turbo', '对话'],
]}
/>
<P className="mt-4">
查看 <a href="/pricing" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">完整模型列表和定价 </a>
</P>
{/* ── Section: 速率限制 ── */}
<SectionTitle id="ratelimit" icon={Gauge}>{t('速率限制')}</SectionTitle>
<P>
为确保服务稳定性TuringToken 对不同用户类型设置了速率限制
</P>
<DocsTable
headers={['用户类型', '请求频率', '并发连接', '每日限额']}
rows={[
['免费用户', '30 次/分钟', '5', '500 次/天'],
['基础用户', '120 次/分钟', '10', '5000 次/天'],
['专业用户', '600 次/分钟', '30', '50000 次/天'],
['企业用户', '3000 次/分钟', '100', '无限制'],
]}
/>
<InfoCard icon={Gauge} title="提示" className="mt-6">
具体限制以控制台中实际配置为准管理员可在后台自定义速率限制策略超出限制时 API 将返回 HTTP 429 状态码
</InfoCard>
{/* ── Section: 常见问题 ── */}
<SectionTitle id="faq" icon={HelpCircle}>{t('常见问题')}</SectionTitle>
<div className="space-y-6 mt-6">
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-2">TuringToken 与直接使用 OpenAI API 有什么区别</h4>
<P>
TuringToken 提供与 OpenAI 完全兼容的接口但同时聚合了 40+ 其他 AI 厂商的模型
您无需分别对接每个厂商一键切换模型按量统一计费
</P>
</div>
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-2">如何切换不同的 AI 模型</h4>
<P>
只需在请求中修改 <code className="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-violet-600 dark:text-violet-400">model</code>
例如将 <code className="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">gpt-4o</code> <code className="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">claude-3-5-sonnet-20241022</code>
</P>
</div>
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-2">支持流式响应 (Streaming) </h4>
<P>
支持设置 <code className="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-violet-600 dark:text-violet-400">stream: true</code> SSE
</P>
</div>
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-2">支持 Function Calling / Tool Use </h4>
<P>
支持GPT-4oClaudeGeminiQwenDeepSeek 等模型均支持 Function Calling可用于构建 Agent 和工具调用场景
</P>
</div>
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-2">如何查看我的 API 使用量和消费明细</h4>
<P>
登录后进入 <a href="/console" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">控制台</a>
</P>
</div>
<div>
<h4 className="text-base font-semibold text-gray-900 dark:text-white mb-2">密钥泄露了怎么办</h4>
<P>
立即前往 <a href="/console/token" className="text-violet-600 dark:text-violet-400 hover:underline font-medium">控制台 API Key</a>
</P>
</div>
</div>
{/* ── Footer ── */}
<div className="mt-20 pt-8 border-t border-gray-200 dark:border-gray-800">
<P className="text-center text-sm text-gray-400 !mb-0">
更多问题请联系我们的技术支持团队获取帮助
</P>
</div>
</main>
</div>
</div>
);
}