From 64a4a68a6f4539b32f540ac0e3e30adbb7ef8e55 Mon Sep 17 00:00:00 2001 From: xiezhouwei Date: Mon, 22 Jun 2026 09:30:31 +0800 Subject: [PATCH] feat: add in-app docs page with sidebar navigation and quickstart guide, replace /docs redirect with built-in page --- router/main.go | 26 -- web/src/App.jsx | 9 + web/src/helpers/docsLink.js | 8 +- web/src/hooks/common/useNavigation.js | 18 +- web/src/i18n/locales/en.json | 12 +- web/src/i18n/locales/zh-CN.json | 12 +- web/src/pages/Docs/index.jsx | 573 ++++++++++++++++++++++++++ web/src/pages/Home/index.jsx | 12 +- 8 files changed, 622 insertions(+), 48 deletions(-) create mode 100644 web/src/pages/Docs/index.jsx diff --git a/router/main.go b/router/main.go index 35fc079..3667c63 100644 --- a/router/main.go +++ b/router/main.go @@ -9,7 +9,6 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/middleware" - "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/gin-gonic/gin" ) @@ -20,31 +19,6 @@ func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { SetRelayRouter(router) SetVideoRouter(router) - // 文档重定向:拦截 /docs 和 /{lang}/docs 路径,转发到配置的 docs_link - router.Use(func(c *gin.Context) { - path := c.Request.URL.Path - var prefix string - if path == "/docs" || strings.HasPrefix(path, "/docs/") { - prefix = "/docs" - } else { - for _, lp := range []string{"/zh/docs", "/en/docs", "/ja/docs"} { - if path == lp || strings.HasPrefix(path, lp+"/") { - prefix = lp - break - } - } - } - if prefix == "" { - c.Next() - return - } - docsLink := operation_setting.GetGeneralSetting().DocsLink - subPath := strings.TrimPrefix(path, prefix) - target := strings.TrimRight(docsLink, "/") + subPath - c.Redirect(http.StatusFound, target) - c.Abort() - }) - frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL") if common.IsMasterNode && frontendBaseUrl != "" { frontendBaseUrl = "" diff --git a/web/src/App.jsx b/web/src/App.jsx index 40a6853..1b2dab4 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -68,6 +68,7 @@ const InviteRedirect = lazy(() => import('./pages/InviteRedirect')); const About = lazy(() => import('./pages/About')); const UserAgreement = lazy(() => import('./pages/UserAgreement')); const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy')); +const Docs = lazy(() => import('./pages/Docs')); function DynamicOAuth2Callback() { const { provider } = useParams(); @@ -465,6 +466,14 @@ function App() { } /> + } key={location.pathname}> + + + } + /> { }, ...(docsNav?.href ? [ - { - text: t('文档'), - itemKey: 'docs', - isExternal: true, - externalLink: docsNav.href, - openInNewTab: docsNav.openInNewTab, - }, + (() => { + const isExternal = /^https?:\/\//i.test(docsNav.href); + return { + text: t('文档'), + itemKey: 'docs', + isExternal, + externalLink: isExternal ? docsNav.href : undefined, + to: isExternal ? undefined : docsNav.href, + openInNewTab: docsNav.openInNewTab, + }; + })(), ] : []), { diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index bf13e90..1420319 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -5680,6 +5680,16 @@ "兼容 OpenAI / Claude / Gemini 等协议,覆盖文本、图像、视频、语音,智能路由,统一计费。": "Compatible with OpenAI, Claude, Gemini, and more. Covers text, image, video, and voice with intelligent routing and unified billing.", "开始使用": "Get Started", "查看价格": "View Pricing", - "页面正在建设中...": "Page under construction..." + "页面正在建设中...": "Page under construction...", + "概述": "Overview", + "注册账号": "Register", + "创建 API 密钥": "Create API Key", + "发送请求": "Send Request", + "对话补全": "Chat Completions", + "图片生成": "Image Generation", + "语音 & 音频": "Audio & Speech", + "支持模型": "Supported Models", + "速率限制": "Rate Limits", + "常见问题": "FAQ" } } \ No newline at end of file diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index 04d6496..582b24f 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -5539,6 +5539,16 @@ "兼容 OpenAI / Claude / Gemini 等协议,覆盖文本、图像、视频、语音,智能路由,统一计费。": "兼容 OpenAI / Claude / Gemini 等协议,覆盖文本、图像、视频、语音,智能路由,统一计费。", "开始使用": "开始使用", "查看价格": "查看价格", - "页面正在建设中...": "页面正在建设中..." + "页面正在建设中...": "页面正在建设中...", + "概述": "概述", + "注册账号": "注册账号", + "创建 API 密钥": "创建 API 密钥", + "发送请求": "发送请求", + "对话补全": "对话补全", + "图片生成": "图片生成", + "语音 & 音频": "语音 & 音频", + "支持模型": "支持模型", + "速率限制": "速率限制", + "常见问题": "常见问题" } } \ No newline at end of file diff --git a/web/src/pages/Docs/index.jsx b/web/src/pages/Docs/index.jsx new file mode 100644 index 0000000..e4e1b06 --- /dev/null +++ b/web/src/pages/Docs/index.jsx @@ -0,0 +1,573 @@ +/* +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 ( +
+
+ {lang} + +
+
+        {code}
+      
+
+ ); +} + +/* ─── SectionTitle ─── */ +function SectionTitle({ id, icon: Icon, children }) { + return ( +

+ + + + {children} +

+ ); +} + +/* ─── SubSection ─── */ +function SubSection({ children, className = '' }) { + return

{children}

; +} + +/* ─── Paragraph ─── */ +function P({ children, className = '' }) { + return

{children}

; +} + +/* ─── Table Component ─── */ +function DocsTable({ headers, rows }) { + return ( +
+ + + + {headers.map((h, i) => ( + + ))} + + + + {rows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + + ))} + +
+ {h} +
{cell}
+
+ ); +} + +/* ─── Info Card ─── */ +function InfoCard({ icon: Icon, title, children, className = '' }) { + return ( +
+ +
+

{title}

+
{children}
+
+
+ ); +} + +/* ─── 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 ( +
+ {/* ── Top Nav Bar ── */} +
+
+ + + logo + TuringToken + + | + {t('文档')} +
+
+ +
+ {/* ── Sidebar (Desktop) ── */} + + + {/* ── Mobile Sidebar Overlay ── */} + {sidebarOpen && ( + <> +
setSidebarOpen(false)} /> + + + )} + + {/* ── Content ── */} +
+ + {/* ── Section: 概述 ── */} + {t('概述')} +

+ TuringToken 是一个统一的 AI API 网关,聚合了 + 40+ 主流 AI 提供商(OpenAI、Claude、Gemini、DeepSeek、Qwen 等), + 提供与 OpenAI API 完全兼容的接口格式。一次接入,即可访问所有模型,无需分别对接各个厂商。 +

+ +
+ {[ + { 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) => ( +
+ +
+

{f.title}

+

{f.desc}

+
+
+ ))} +
+ + {/* ── Section: 快速开始 ── */} + {t('快速开始')} +

只需三步,即可接入 AI 能力:

+ +
+
+ 1 +
+

{t('注册账号')}

+

访问 注册页面 创建 TuringToken 账号。

+
+
+
+ 2 +
+

{t('创建 API 密钥')}

+

进入 控制台 → API Key,点击"新建"生成密钥。

+
+
+
+ 3 +
+

{t('发送请求')}

+

使用下方示例代码,将 Base URL 替换为 TuringToken 地址即可。

+
+
+
+ + + {BASE_HOST}/v1 + + + {/* ── Section: API 密钥 ── */} + {t('API 密钥')} +

+ API 密钥是与 TuringToken 通信的凭证。每个密钥可以设置额度上限、有效期和 IP 白名单。 +

+ + 创建密钥 +

+ 登录后进入 控制台 → API Key, + 点击"新建 API Key",填写名称后即可生成。请务必妥善保存密钥,关闭页面后将无法再次查看完整密钥。 +

+ + 使用密钥 +

在所有 API 请求中,通过 Bearer Token 方式传递密钥:

+ + + + 请勿将 API 密钥硬编码在前端代码或公开仓库中。建议使用环境变量或后端代理方式管理密钥。 + + + {/* ── Section: 对话补全 ── */} + {t('对话补全')} +

+ 对话补全是最常用的 API 接口,支持多轮对话、流式输出和 Function Calling。 + 兼容 OpenAI Chat Completions API 格式。 +

+ + 基础请求 + + + 流式输出 +

设置 stream: true 即可启用 SSE 流式响应:

+ + + Python SDK 示例 + + + Node.js SDK 示例 + + + {/* ── Section: 图片生成 ── */} + {t('图片生成')} +

+ 支持 DALL·E、Stable Diffusion 等多种图片生成模型,兼容 OpenAI Images API 格式。 +

+ + 生成图片 + + + {/* ── Section: 语音 & 音频 ── */} + {t('语音 & 音频')} +

支持语音转文字(STT)和文字转语音(TTS),兼容 OpenAI Audio API 格式。

+ + 语音转文字 (STT) + + + 文字转语音 (TTS) + + + {/* ── Section: 视频生成 ── */} + {t('视频生成')} +

+ TuringToken 集成了多家视频生成厂商(可灵、即梦、Vidu、Sora 等), + 支持文生视频和图生视频能力。 +

+

+ 视频生成接口因厂商而异,详细参数请参考 模型列表 中各视频模型的说明, + 或访问 控制台 在线体验。 +

+ + {/* ── Section: 支持模型 ── */} + {t('支持模型')} +

TuringToken 聚合了以下主流 AI 模型的 API 访问:

+ + + +

+ 查看 完整模型列表和定价 → +

+ + {/* ── Section: 速率限制 ── */} + {t('速率限制')} +

+ 为确保服务稳定性,TuringToken 对不同用户类型设置了速率限制: +

+ + + + + 具体限制以控制台中实际配置为准。管理员可在后台自定义速率限制策略。超出限制时 API 将返回 HTTP 429 状态码。 + + + {/* ── Section: 常见问题 ── */} + {t('常见问题')} + +
+
+

TuringToken 与直接使用 OpenAI API 有什么区别?

+

+ TuringToken 提供与 OpenAI 完全兼容的接口,但同时聚合了 40+ 其他 AI 厂商的模型。 + 您无需分别对接每个厂商,一键切换模型,按量统一计费。 +

+
+
+

如何切换不同的 AI 模型?

+

+ 只需在请求中修改 model 参数即可,接口格式完全一致。 + 例如将 gpt-4o 替换为 claude-3-5-sonnet-20241022。 +

+
+
+

支持流式响应 (Streaming) 吗?

+

+ 支持。设置 stream: true 即可启用 SSE 流式输出,所有模型均支持。 +

+
+
+

支持 Function Calling / Tool Use 吗?

+

+ 支持。GPT-4o、Claude、Gemini、Qwen、DeepSeek 等模型均支持 Function Calling,可用于构建 Agent 和工具调用场景。 +

+
+
+

如何查看我的 API 使用量和消费明细?

+

+ 登录后进入 控制台,可在仪表盘查看实时用量统计和消费明细。 +

+
+
+

密钥泄露了怎么办?

+

+ 立即前往 控制台 → API Key 删除泄露的密钥,并创建新密钥。 +

+
+
+ + {/* ── Footer ── */} +
+

+ 更多问题?请联系我们的技术支持团队获取帮助。 +

+
+
+
+
+ ); +} diff --git a/web/src/pages/Home/index.jsx b/web/src/pages/Home/index.jsx index 208bc2c..e158062 100644 --- a/web/src/pages/Home/index.jsx +++ b/web/src/pages/Home/index.jsx @@ -162,15 +162,11 @@ function HeroSection() { {[ { key: "model", label: "模型", icon: , to: "/pricing" }, { key: "benchmark", label: "评测", icon: , to: "/benchmarks" }, - { key: "docs", label: "文档", icon: , to: "/docs", isDocs: true }, + { key: "docs", label: "文档", icon: , to: "/docs" }, { key: "console", label: "控制台", icon: , to: "/console" }, - ].map((item) => { - const linkClass = "inline-flex items-center gap-1.5 px-3 py-1.5 text-[14px] rounded-md transition-colors duration-150 text-white/60 hover:text-white"; - if (item.isDocs) { - return {item.icon}{t(item.label)}; - } - return {item.icon}{t(item.label)}; - })} + ].map((item) => ( + {item.icon}{t(item.label)} + ))}