diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index fb970ceab..9182134f0 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -1,75 +1,80 @@ -name: Preview FastGPT docs - +name: Preview documents on: pull_request_target: paths: - - 'docSite/**' + - 'document/**' workflow_dispatch: -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains jobs "deploy-production" - deploy-preview: + build-fastgpt-docs-images: + runs-on: ubuntu-latest + permissions: contents: read packages: write attestations: write id-token: write - pull-requests: write - # The environment this job references - environment: - name: Preview - url: ${{ steps.vercel-action.outputs.preview-url }} - # The type of runner that the job will run on - runs-on: ubuntu-24.04 - - # Job outputs - outputs: - url: ${{ steps.vercel-action.outputs.preview-url }} - - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Step 1 - Checks-out your repository under $GITHUB_WORKSPACE - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + # list of Docker images to use as base name for tags + images: | + ${{ secrets.ALI_IMAGE_NAME }}//fastgpt-docs + tags: | + ${{ steps.datetime.outputs.datetime }} + flavor: latest=false + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Aliyun + uses: docker/login-action@v3 + with: + registry: registry.cn-hangzhou.aliyuncs.com + username: ${{ secrets.ALI_HUB_USERNAME }} + password: ${{ secrets.ALI_HUB_PASSWORD }} + + - name: Build and push Docker images + uses: docker/build-push-action@v5 + with: + context: . + file: ./document/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + NEXT_PUBLIC_SEARCH_APPKEY=c4708d48f2de6ac5d2f0f443979ef92a + NEXT_PUBLIC_SEARCH_APPID=HZAF4C2T88 + NEXT_PUBLIC_DOMAIN=https://vzldqobwbwna.sealoshzh.site + outputs: + tags: ${{ steps.datetime.outputs.datetime }} + + update-docs-image: + needs: build-fastgpt-docs-images + runs-on: ubuntu-24.04 + if: github.repository == 'labring/FastGPT' + steps: + - name: Checkout code uses: actions/checkout@v3 + - uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - submodules: recursive # Fetch submodules - fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - token: ${{ secrets.GITHUB_TOKEN }} - - # Step 2 Detect changes to Docs Content - - name: Detect changes in doc content - uses: dorny/paths-filter@v2 - id: filter + args: set image deployment/fastgpt-docs-preview fastgpt-docs-preview=${{ secrets.ALI_IMAGE_NAME }}//fastgpt-docs:${{ needs.build-fastgpt-docs-images.outputs.tags }} + - uses: actions-hub/kubectl@master + env: + KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }} with: - filters: | - docs: - - 'docSite/content/docs/**' - base: main - - # Step 3 - Install Hugo (specific version) - - name: Install Hugo - uses: peaceiris/actions-hugo@v2 - with: - hugo-version: '0.117.0' - extended: true - - # Step 4 - Builds the site using Hugo - - name: Build - run: cd docSite && hugo mod get -u github.com/colinwilson/lotusdocs@6d0568e && hugo -v --minify - - # Step 5 - Push our generated site to Cloudflare - - name: Deploy to Cloudflare Pages - id: deploy - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy ./docSite/public --project-name=fastgpt-doc - packageManager: npm + args: annotate deployment/fastgpt-docs-preview originImageName="${{ secrets.ALI_IMAGE_NAME }}//fastgpt-docs:${{ needs.build-fastgpt-docs-images.outputs.tags }}" --overwrite - name: '@finleyge/github-tools' uses: FinleyGe/github-tools@0.0.1 @@ -81,5 +86,5 @@ jobs: title: 'Docs Preview:' body: | ``` - 🔗 Preview URL: ${{deploymentUrl}} + 🔗 Preview URL: https://vzldqobwbwna.sealoshzh.site ``` diff --git a/.gitignore b/.gitignore index ec9cb7ca9..415b95932 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ files/helm/fastgpt/charts/*.tgz tmp/ coverage +document/.source \ No newline at end of file diff --git a/document/.dockerignore b/document/.dockerignore new file mode 100644 index 000000000..1712dae54 --- /dev/null +++ b/document/.dockerignore @@ -0,0 +1,24 @@ +# 开发环境文件 +.git +.gitignore +.env* +.next +node_modules + +# Docker 相关 +Dockerfile +.dockerignore + +# 其他 +README.md +*.log +.DS_Store + +# 缓存文件 +.cache +.npm +.pnpm-store + +# IDE 配置 +.vscode +.idea \ No newline at end of file diff --git a/document/.env.template b/document/.env.template new file mode 100644 index 000000000..33f5c41c4 --- /dev/null +++ b/document/.env.template @@ -0,0 +1,4 @@ +NEXT_PUBLIC_SEARCH_APPKEY= +NEXT_PUBLIC_SEARCH_APPWRITEKEY= +NEXT_PUBLIC_SEARCH_APPID= +NEXT_PUBLIC_DOMAIN= \ No newline at end of file diff --git a/document/Dockerfile b/document/Dockerfile new file mode 100644 index 000000000..eb28774d1 --- /dev/null +++ b/document/Dockerfile @@ -0,0 +1,51 @@ +FROM node:20-alpine AS base + +FROM base AS builder +RUN apk add --no-cache \ + libc6-compat \ + git \ + build-base \ + g++ \ + cairo-dev \ + jpeg-dev \ + pango-dev \ + giflib-dev \ + librsvg-dev \ + freetype-dev \ + harfbuzz-dev \ + fribidi-dev \ + udev \ + ttf-opensans \ + fontconfig +WORKDIR /app + +ARG NEXT_PUBLIC_SEARCH_APPKEY +ARG NEXT_PUBLIC_SEARCH_APPID +ARG NEXT_PUBLIC_DOMAIN +ARG NEXT_PUBLIC_SEARCH_APPWRITEKEY + +ENV NEXT_PUBLIC_SEARCH_APPKEY=$NEXT_PUBLIC_SEARCH_APPKEY +ENV NEXT_PUBLIC_SEARCH_APPWRITEKEY=$NEXT_PUBLIC_SEARCH_APPWRITEKEY +ENV NEXT_PUBLIC_SEARCH_APPID=$NEXT_PUBLIC_SEARCH_APPID +ENV NEXT_PUBLIC_DOMAIN=$NEXT_PUBLIC_DOMAIN + +COPY . . +RUN npm install && npm run build + +FROM base AS runner +RUN apk add --no-cache curl +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs +WORKDIR /app +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/document/README.md b/document/README.md new file mode 100644 index 000000000..2f36fbb09 --- /dev/null +++ b/document/README.md @@ -0,0 +1,94 @@ +# fast + +这是FastGPT的官方文档,采用fumadoc框架。 + +# 获取搜索应用 + +点击[Algolia](https://dashboard.algolia.com/account/overview),进行注册账号,注册成功后需要点击页面的搜索,然后查看应用,默认会有一个应用。 + +![](./public/readme/algolia.png) + +拥有应用后点击个人头像,点击设置,点击`API Keys`查看自己的应用id和key。 + +![](./public/readme/algolia2.png) + +页面中的`Application ID`和`Search API Key`,`Write API KEY`就是环境变量对应的`NEXT_PUBLIC_SEARCH_APPID`和`NEXT_PUBLIC_SEARCH_APPKEY`,`NEXT_PUBLIC_SEARCH_APPWRITEKEY` + +![](./public/readme/algolia3.png) + +# 运行项目 + +要运行文档,首先需要进行环境变量配置,在文档的根目录下创建`.env.local`文件,填写以下环境变量: + +```bash +NEXT_PUBLIC_SEARCH_APPWRITEKEY = #这是上面获取的Write api key +NEXT_PUBLIC_SEARCH_APPKEY = #这是上面获取的搜索key +NEXT_PUBLIC_SEARCH_APPID = #这是上面的搜索id +NEXT_PUBLIC_DOMAIN = #要跳转的FastGPT项目的域名,默认海外版 +``` + +你可以在FastGPT项目根目录下执行以下命令来运行文档。 + +```bash +npm install #只能npm install,不能pnpm +npm run dev +``` +项目会默认跑在`http:localhost:3000`端口 + +# 书写文档 + +文档采用`mdx`格式,大体和`md`一致,但是现在文档的元数据只支持`title` `description`和`icon`三个字段,参考以下示例代码: + +```bash +--- +title: FastGPT 文档 +description: FastGPT 官方文档 +icon: menu #icon采用`lucide-react`第三方库。 +--- + +import { Alert } from '@/components/docs/Alert'; #高亮块组件 + + +快速开始体验 +- 海外版:[https://fastgpt.io](https://fastgpt.io) +- 国内版:[https://fastgpt.cn](https://fastgpt.cn) + + +import {Redirect} from '@/components/docs/Redirect' #重定向组件,如果你希望用户点击这个文件跳转到别的文件的话,详情参考 `FAQ`的`Docker 部署问题`文档。 + + + + #tabs组件用法,渲染效果参考`introduction`下`development`的`faq`文档 + Javascript is weird + Rust is fast + + +import FastGPTLink from '@/components/docs/linkFastGPT'; #FastGPT跳转链接组件,通过接收一个域名环境变量,来实现跳转到海外或者国内 + +本文档介绍了如何设置开发环境以构建和测试 FastGPT。 + + +``` + +在书写完文档后,需要在对应的目录下的`meta.json`文件的`pages`字段合适位置添加自己的文件名。例如在`content/docs`(默认这是所有文档的根目录)的`introduction`目录下书写了一个`hello.mdx`文件。则需要去`introduction`目录下的`meta.json`添加以下内容: + +```bash +{ + "title": "FastGPT Docs", + "root": true, + "pages": ["[Handshake][联系我们](https://fael3z0zfze.feishu.cn/share/base/form/shrcnRxj3utrzjywsom96Px4sud)","index","guide","development","FAQ","shopping_cart","community","hello"], #"hello"原本没有,此外,这里的顺序就是最后文档的展示顺序,现在"hello"文档将会在`introduction`的最后展示 + "order": 1 +} +``` + +# i18n + +在`content/docs`下的所有`.mdx`文件为默认语言文件(当前默认语言中文),`.en.mdx`文件为`i18n`支持的英文文件,例如,你可以将`hello.mdx`文档翻译后,写一个`hello.en.mdx`,同时,在对应目录的`meta.en.json`的`"pages"`字段写下对应的文件名来支持英文文档。 + +# ps + +`meta.json`的`"pages"`中的`"[Handshake][联系我们](https://fael3z0zfze.feishu.cn/share/base/form/shrcnRxj3utrzjywsom96Px4sud)"`这个字段是目录的链接形式,表现效果为,点击后跳转到对应的url。 + +![](./public/readme/link.png) + +最后,如果依然有问题,可以进入`https://fumadocs.dev/docs/ui`官网,询问官网提供的ai来了解文档框架的使用。 \ No newline at end of file diff --git a/document/app/[lang]/(home)/layout.tsx b/document/app/[lang]/(home)/layout.tsx new file mode 100644 index 000000000..f9dd2e352 --- /dev/null +++ b/document/app/[lang]/(home)/layout.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react'; +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import LogoLight from '@/components/docs/logo'; + +export default async function Layout({ + params, + children +}: { + params: Promise<{ lang: string }>; + children: ReactNode; +}) { + const lang = (await params).lang; + return ( + + + + ) + }} + i18n + > + {children} + + ); +} diff --git a/document/app/[lang]/(home)/page.tsx b/document/app/[lang]/(home)/page.tsx new file mode 100644 index 000000000..8be24052d --- /dev/null +++ b/document/app/[lang]/(home)/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default function HomePage() { + redirect(`/docs/introduction`); +} diff --git a/document/app/[lang]/docs/[[...slug]]/page.tsx b/document/app/[lang]/docs/[[...slug]]/page.tsx new file mode 100644 index 000000000..845c8d5df --- /dev/null +++ b/document/app/[lang]/docs/[[...slug]]/page.tsx @@ -0,0 +1,61 @@ +import { source } from '@/lib/source'; +import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/page'; +import { notFound } from 'next/navigation'; +import { createRelativeLink } from 'fumadocs-ui/mdx'; +import { getMDXComponents } from '@/mdx-components'; + +export default async function Page({ + params +}: { + params: Promise<{ lang: string; slug?: string[] }>; +}) { + const { lang, slug } = await params; + const page = source.getPage(slug, lang); + if (!page || !page.data || !page.file) notFound(); + + const MDXContent = page.data.body; + + return ( + + {page.data.title} + {page.data.description} + + + + + ); +} + +export async function generateStaticParams() { + return source.generateParams(); +} + +export async function generateMetadata(props: { + params: Promise<{ lang: string; slug?: string[] }>; +}) { + const { lang, slug } = await props.params; + const page = source.getPage(slug, lang); + if (!page || !page.data) notFound(); + + return { + title: `${page.data.title} | FastGPT`, + description: page.data.description + }; +} diff --git a/document/app/[lang]/docs/layout.tsx b/document/app/[lang]/docs/layout.tsx new file mode 100644 index 000000000..7c8ae09d7 --- /dev/null +++ b/document/app/[lang]/docs/layout.tsx @@ -0,0 +1,101 @@ +import { type ReactNode } from 'react'; +import { source } from '@/lib/source'; +import { DocsLayout } from 'fumadocs-ui/layouts/notebook'; +import { baseOptions } from '@/app/layout.config'; +import { t } from '@/lib/i18n'; +import LogoLight from '@/components/docs/logo'; +import LogoDark from '@/components/docs/logoDark'; +import '@/app/global.css'; +import { CustomSidebarComponents } from '@/components/sideBar'; +import FeishuLogoLight from '@/components/docs/feishuLogoLIght'; +import FeishuLogoDark from '@/components/docs/feishuLogoDark'; +import GithubLogoLight from '@/components/docs/githubLogoLight'; +import GithubLogoDark from '@/components/docs/githubLogoDark'; + +export default async function Layout({ + params, + children +}: { + params: Promise<{ lang: string }>; + children: ReactNode; +}) { + const { lang } = await params; + + const tab = [ + { + title: t('common:introduction', lang), + url: lang === 'zh-CN' ? '/docs/introduction' : '/en/docs/introduction' + }, + { + title: t('common:use-cases', lang), + url: lang === 'zh-CN' ? '/docs/use-cases' : '/en/docs/use-cases' + }, + { + title: t('common:protocol', lang), + url: lang === 'zh-CN' ? '/docs/protocol' : '/en/docs/protocol' + } + ]; + + return ( + +
+ +
+
+ +
+ + ), + mode: 'top' + }} + links={[ + { + type: 'icon', + icon: ( +
+
+ +
+
+ +
+
+ ), + url: 'https://oss.laf.run/otnvvf-imgs/fastgpt-feishu1.png', + text: '飞书群' + }, + { + type: 'icon', + icon: ( +
+
+ +
+
+ +
+
+ ), + url: 'https://github.com/labring/FastGPT', + text: 'github' + } + ]} + tree={source.pageTree[lang]} + searchToggle={{ + enabled: true + }} + sidebar={{ + tabs: tab, + collapsible: false, + components: CustomSidebarComponents + }} + tabMode="navbar" + > + {children} +
+ ); +} diff --git a/document/app/[lang]/layout.tsx b/document/app/[lang]/layout.tsx new file mode 100644 index 000000000..427fcb4b8 --- /dev/null +++ b/document/app/[lang]/layout.tsx @@ -0,0 +1,79 @@ +import '@/app/global.css'; +import { RootProvider } from 'fumadocs-ui/provider'; +import { Inter } from 'next/font/google'; +import type { ReactNode } from 'react'; +import type { Translations } from 'fumadocs-ui/i18n'; +import CustomSearchDialog from '@/components/CustomSearchDialog'; + +const inter = Inter({ + subsets: ['latin'] +}); + +const zh_CN: Partial = { + search: '搜索', + nextPage: '下一页', + previousPage: '上一页', + lastUpdate: '最后更新于', + editOnGithub: '在 GitHub 上编辑', + searchNoResult: '没有找到相关内容', + toc: '本页导航', + tocNoHeadings: '本页没有导航', + chooseLanguage: '选择语言' +}; + +const locales = [ + { + name: 'English', + locale: 'en' + }, + { + name: '简体中文', + locale: 'zh-CN' + } +]; + +export default async function Layout({ + children, + params +}: { + children: ReactNode; + params: Promise<{ lang: string }>; +}) { + const { lang } = await params; + + return ( + + + + {children} + + + + ); +} diff --git a/document/app/[lang]/llms.txt/route.ts b/document/app/[lang]/llms.txt/route.ts new file mode 100644 index 000000000..cd284cb46 --- /dev/null +++ b/document/app/[lang]/llms.txt/route.ts @@ -0,0 +1,56 @@ +import * as fs from 'node:fs/promises'; +import fg from 'fast-glob'; +import matter from 'gray-matter'; +import { remark } from 'remark'; +import remarkGfm from 'remark-gfm'; +import remarkStringify from 'remark-stringify'; +import remarkMdx from 'remark-mdx'; +import { remarkInclude } from 'fumadocs-mdx/config'; +import { i18n } from '@/lib/i18n'; + +export const revalidate = false; + +const processor = remark() + .use(remarkMdx) + // https://fumadocs.vercel.app/docs/mdx/include + .use(remarkInclude) + // gfm styles + .use(remarkGfm) + // .use(your remark plugins) + .use(remarkStringify); // to string + +export async function GET() { + // all scanned content + // Select files based on the default language + const defaultLanguage = i18n.defaultLanguage; + let globPattern; + + if (defaultLanguage === 'zh-CN') { + // For Chinese, select *.mdx files + globPattern = ['./content/docs/**/*.mdx']; + } else { + // For other languages (default English), select *.en.mdx files that don't have .mdx. in their path + globPattern = ['./content/docs/**/*.en.mdx']; + } + + const files = await fg(globPattern); + + const scan = files.map(async (file: string) => { + const fileContent = await fs.readFile(file); + const { content, data } = matter(fileContent.toString()); + + const processed = await processor.process({ + path: file, + value: content + }); + + return `file: ${file} +meta: ${JSON.stringify(data, null, 2)} + +${processed}`; + }); + + const scanned = await Promise.all(scan); + + return new Response(scanned.join('\n\n')); +} diff --git a/document/app/api/robots/route.ts b/document/app/api/robots/route.ts new file mode 100644 index 000000000..810971b4e --- /dev/null +++ b/document/app/api/robots/route.ts @@ -0,0 +1,24 @@ +// app/api/robots/route.ts +import { i18n } from '@/lib/i18n'; +import { NextResponse } from 'next/server'; + +export async function GET() { + const host = + i18n.defaultLanguage === 'zh-cn' ? 'https://localhost:3000' : 'https://localhost:3000/en'; + + const robotsTxt = `User-agent: * +Allow: / +Allow: /en/ +Disallow: /zh-cn/ + + +Host: ${host} + +Sitemap: ${host}/sitemap.xml`; + + return new NextResponse(robotsTxt, { + headers: { + 'Content-Type': 'text/plain' + } + }); +} diff --git a/document/app/api/search/route.ts b/document/app/api/search/route.ts new file mode 100644 index 000000000..4fadc43d1 --- /dev/null +++ b/document/app/api/search/route.ts @@ -0,0 +1,7 @@ +import { source } from '@/lib/source'; +import { createFromSource } from 'fumadocs-core/search/server'; + +export const { GET } = createFromSource(source, { + // https://docs.orama.com/open-source/supported-languages + language: 'english' +}); diff --git a/document/app/global.css b/document/app/global.css new file mode 100644 index 000000000..58ff33e03 --- /dev/null +++ b/document/app/global.css @@ -0,0 +1,270 @@ +@import 'tailwindcss'; +@import 'fumadocs-ui/css/preset.css'; + +/* 在文件开头添加这些基础变量 */ +:root { + /* 基础颜色 */ + --primary-50-hsl: 210, 40%, 98%; + --primary-hsl: 217, 91%, 60%; + --emerald-50-hsl: 152, 81%, 96%; + --emerald-500-hsl: 152, 76%, 40%; + --cardinal-50-hsl: 0, 86%, 97%; + --cardinal-500-hsl: 0, 74%, 42%; + --yellow-50-hsl: 55, 92%, 95%; + --yellow-500-hsl: 45, 93%, 47%; + --blue-500-hsl: 217, 91%, 60%; + --fd-layout-width: 1400px; + + /* 文本颜色 */ + --text-default: #374151; + --text-default-inv: #ffffff; + --text-muted: #6b7280; + --content-link-color: #2563eb; + + /* 其他变量 */ + --font-size-sm: 0.875rem; + --gray-200: #e5e7eb; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + + /* 组件颜色 */ + --primary-200: #bfdbfe; + --blue-200: #bfdbfe; + --blue-800: #1e40af; + --emerald-200: #a7f3d0; + --emerald-800: #065f46; + --cardinal-200: #fecaca; + --cardinal-800: #991b1b; + --yellow-200: #fde68a; + --yellow-800: #92400e; + + /* Tabs 样式 */ + --nav-tabs-border-width: none; + --nav-tabs-link-active-bg: none; + --nav-tabs-link-active-color: var(--text-default); + --nav-tabs-border-color: var(--gray-400); +} + +[data-dark-mode] { + /* Tabs 样式 */ + --nav-tabs-border-color: var(--gray-800); + + --text-muted: #9ca3af; + --content-link-color: #60a5fa; +} + +/* 全局代码块样式 */ +pre, +code { + font-size: 0.9rem !important; + line-height: 1.6 !important; +} + +/* 行内代码样式 */ +/* 行内代码样式 */ +:not(pre) > code { + padding: 0.2em 0.4em !important; + margin: 0 0.2em !important; + color: #2563eb !important; +} + +/* 代码块中的滚动条样式优化 */ + +/* 图片居中显示 */ +.fumadocs-content img, +.mdx-content img, +.prose img, +img { + display: block !important; + margin-left: auto !important; + margin-right: auto !important; + max-width: 100% !important; + height: auto !important; + border-radius: 8px !important; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important; +} + +/* MDX 表格样式 */ +.fumadocs-content table, +.mdx-content table, +.prose table { + width: 100% !important; + border-collapse: separate !important; + margin: 1rem 0 !important; +} + +.fumadocs-content table td, +.fumadocs-content table th, +.mdx-content table td, +.mdx-content table th, +.prose table td, +.prose table th { + padding: 0.75rem 1rem !important; + text-align: left !important; +} + +/* Tabs 样式 */ +.nav-tabs { + display: flex; + gap: 0.5rem; + border-bottom: 1px solid var(--nav-tabs-border-color); + margin-bottom: 0.8rem; +} + +.nav-tabs .nav-link { + color: var(--text-muted) !important; + margin-bottom: -1px; + padding: 0.75rem 1.5rem; + border: none; + background: none; + cursor: pointer; + font-size: 1rem; + transition: all 0.2s ease; +} + +.nav-tabs .nav-link:hover { + text-decoration: none !important; +} + +.nav-tabs .nav-link.active { + border-bottom: 2px solid var(--content-link-color); + color: var(--content-link-color) !important; +} + +.tab-content { + margin-bottom: 0.8rem; + padding: 1rem 0; +} + +div[data-state='open'].fixed.inset-0.z-50 { + background-color: rgba(255, 255, 255, 0.4) !important; +} + +#nd-subnav > div:nth-of-type(1) button:nth-of-type(1) { + box-shadow: + 0px 1px 2px 0px rgba(19, 51, 107, 0.05), + 0px 0px 1px 0px rgba(19, 51, 107, 0.08) !important; + background-color: none !important; + &:hover { + cursor: pointer; + } +} + +figure.shiki button[aria-label='Copy Text'] { + background-color: none !important; + &:hover { + cursor: pointer; + } +} + +#nd-subnav > div:nth-of-type(1) button { + &:hover { + cursor: pointer; + } +} +#nd-subnav > div:nth-of-type(1) { + border-bottom: 0.1px solid #f4f4f7 !important; +} +#nd-subnav > div:nth-of-type(2) { + border-bottom: 0.1px solid #dfe2ea !important; + height: 100%; +} +.dark #nd-subnav > div:nth-of-type(1) { + border-bottom: 0.1px solid #363b4a58 !important; +} +.dark #nd-subnav > div:nth-of-type(2) { + border-bottom: 0.1px solid #61646fc6 !important; +} + +div[data-rmiz-modal-content] { + background-color: none !important; +} + +div[data-rmiz-modal-overlay='visible'] { + background-color: #ffffff00 !important; + backdrop-filter: blur(4px); +} +.dark div[data-rmiz-modal-overlay='visible'] { + background-color: #060c1a00 !important; + backdrop-filter: blur(4px); +} +.dark div[data-rmiz-modal-content] { + background-color: #060c1a00 !important; +} + +#nd-subnav > div:nth-of-type(2) a { + text-decoration: none; + color: #485264; + transition: color 0.2s ease; + background-color: transparent !important; + font-weight: 400; + /* 先清除默认下划线 */ + &:hover { + text-decoration: underline; + text-decoration-color: #c5d7ff; + text-decoration-thickness: 3px; /* 下划线粗细 */ + text-underline-offset: 17px; /* 下划线与文字距离 */ + } + &.text-fd-primary { + text-decoration: underline; + text-decoration-color: #3370ff; + text-decoration-thickness: 3px; /* 下划线粗细 */ + text-underline-offset: 17px; /* 下划线与文字距离 */ + background-color: transparent !important; + font-weight: 600; + color: #111824; + } +} +.dark #nd-subnav > div:nth-of-type(2) a { + color: #ffffff; +} + +@theme { + --color-fd-muted: hsl(0, 0%, 96.1%); + --color-fd-popover: hsl(0, 0%, 100%); + --color-fd-popover-foreground: hsl(0, 0%, 15.1%); + --color-fd-card-foreground: hsl(0, 0%, 3.9%); + --color-fd-border: hsl(0, 0%, 89.8%); + --color-fd-primary-foreground: hsl(0, 0%, 98%); + --color-fd-secondary-foreground: hsl(0, 0%, 9%); + --color-fd-accent: hsl(0, 0%, 94.1%); + --color-fd-ring: hsl(0, 0%, 63.9%); + + --color-fd-background: hsl(0, 0%, 100%); + --color-fd-card: hsl(0, 0%, 100%); + --color-fd-foreground: hsl(240, 6%, 25%); + --color-fd-muted-foreground: hsl(240, 6%, 50%); + --color-fd-secondary: hsl(240, 6%, 97%); + --color-fd-accent-foreground: hsl(240, 6%, 25%); + --color-fd-primary: hsl(226, 55%, 45%); +} + +.dark { + --color-fd-background: #060c1a; + --color-fd-foreground: hsl(220, 60%, 94.5%); + --color-fd-muted: hsl(220, 50%, 10%); + --color-fd-muted-foreground: hsl(220, 30%, 65%); + --color-fd-popover: hsl(220, 50%, 10%); + --color-fd-popover-foreground: hsl(220, 60%, 94.5%); + --color-fd-card: hsla(220, 56%, 15%, 0.4); + --color-fd-card-foreground: hsl(220, 60%, 94.5%); + --color-fd-border: hsla(220, 50%, 50%, 0.2); + --color-fd-primary: #3370ff; /* 文本高亮色 */ + --color-fd-primary-foreground: hsl(0, 0%, 9%); + --color-fd-secondary: hsl(220, 50%, 20%); + --color-fd-secondary-foreground: hsl(220, 80%, 90%); + --color-fd-accent: hsl(220, 40%, 20%); + --color-fd-accent-foreground: hsl(220, 80%, 90%); + --color-fd-ring: hsl(205, 100%, 85%); +} + +#nd-sidebar { + border-color: transparent; +} + +button[data-search-full] { + background-color: var(--color-fd-background); +} diff --git a/document/app/layout.config.tsx b/document/app/layout.config.tsx new file mode 100644 index 000000000..a62b7fb77 --- /dev/null +++ b/document/app/layout.config.tsx @@ -0,0 +1,36 @@ +import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; +import { i18n } from '@/lib/i18n'; + +/** + * Shared layout configurations + * + * you can customise layouts individually from: + * Home Layout: app/(home)/layout.tsx + * Docs Layout: app/docs/layout.tsx + */ +export const baseOptions = (locale: string): BaseLayoutProps => { + return { + themeSwitch: { + enabled: true, + mode: 'light-dark' + }, + nav: { + title: ( +
+
+ FastGPT +
+
12321
+
+ ) + }, + i18n: { + languages: ['zh-CN', 'en'], + defaultLanguage: 'zh-CN', + hideLocale: 'always' + }, + searchToggle: { + enabled: true + } + }; +}; diff --git a/document/app/static.json/route.ts b/document/app/static.json/route.ts new file mode 100644 index 000000000..cb02df869 --- /dev/null +++ b/document/app/static.json/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; +import { type DocumentRecord } from 'fumadocs-core/search/algolia'; +import { source } from '@/lib/source'; + +export const revalidate = false; + +export function GET() { + const results: DocumentRecord[] = []; + + for (const page of source.getPages()) { + results.push({ + _id: page.url, + structured: page.data.structuredData, + url: page.url, + title: page.data.title, + description: page.data.description + }); + } + + return NextResponse.json(results); +} diff --git a/document/clean-frontmatter.js b/document/clean-frontmatter.js new file mode 100644 index 000000000..f0bcd3956 --- /dev/null +++ b/document/clean-frontmatter.js @@ -0,0 +1,43 @@ +const fs = require('fs'); +const path = require('path'); +const matter = require('gray-matter'); + +// ✅ 设置要处理的根目录(可修改为你的文档目录) +const rootDir = path.resolve(__dirname, 'content/docs'); + +// ✅ 仅保留的 frontmatter 字段 +const KEEP_FIELDS = ['title', 'description']; + +function cleanFrontmatter(filePath) { + const raw = fs.readFileSync(filePath, 'utf-8'); + const parsed = matter(raw); + + // 仅保留需要的字段 + const newData = {}; + for (const key of KEEP_FIELDS) { + if (parsed.data[key] !== undefined) { + newData[key] = parsed.data[key]; + } + } + + const cleaned = matter.stringify(parsed.content, newData); + fs.writeFileSync(filePath, cleaned, 'utf-8'); + console.log(`✔ Cleaned: ${path.relative(rootDir, filePath)}`); +} + +function walk(dir) { + const entries = fs.readdirSync(dir); + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + walk(fullPath); // 🔁 递归子目录 + } else if (entry.endsWith('.mdx')) { + cleanFrontmatter(fullPath); + } + } +} + +// 🚀 开始执行 +walk(rootDir); diff --git a/document/components/CustomSearchDialog.tsx b/document/components/CustomSearchDialog.tsx new file mode 100644 index 000000000..cb41f594c --- /dev/null +++ b/document/components/CustomSearchDialog.tsx @@ -0,0 +1,49 @@ +'use client'; +// components/CustomSearchDialog.tsx +import { liteClient } from 'algoliasearch/lite'; +import { useDocsSearch } from 'fumadocs-core/search/client'; +import { + SearchDialog, + SearchDialogOverlay, + SearchDialogContent, + SearchDialogHeader, + SearchDialogIcon, + SearchDialogInput, + SearchDialogClose, + SearchDialogList, + type SharedProps +} from 'fumadocs-ui/components/dialog/search'; +import { useI18n } from 'fumadocs-ui/contexts/i18n'; + +if (!process.env.NEXT_PUBLIC_SEARCH_APPID || !process.env.NEXT_PUBLIC_SEARCH_APPKEY) { + throw new Error('NEXT_PUBLIC_SEARCH_APPID and NEXT_PUBLIC_SEARCH_APPKEY are not set'); +} + +const client = liteClient( + process.env.NEXT_PUBLIC_SEARCH_APPID, + process.env.NEXT_PUBLIC_SEARCH_APPKEY +); + +export default function CustomSearchDialog(props: SharedProps) { + const { locale } = useI18n(); + const { search, setSearch, query } = useDocsSearch({ + type: 'algolia', + client, + indexName: 'document', + locale + }); + + return ( + + + + + + + + + + + + ); +} diff --git a/document/components/docs/Alert.tsx b/document/components/docs/Alert.tsx new file mode 100644 index 000000000..510de786d --- /dev/null +++ b/document/components/docs/Alert.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from 'react'; + +interface AlertProps { + icon: ReactNode; + context: 'success' | 'warning' | 'error' | 'info'; + children: ReactNode; +} + +export function Alert({ icon, context = 'info', children }: AlertProps) { + const contextStyles = { + success: + 'bg-green-50 border-green-200 text-green-700 dark:bg-white/5 dark:border-teal-300 dark:text-gray-200', + warning: + 'bg-yellow-50 border-yellow-200 text-yellow-700 dark:dark:bg-white/5 dark:border-indigo-500 dark:text-gray-200', + error: + 'bg-red-50 border-red-200 text-red-700 dark:bg-white/5 dark:border-red-800 dark:text-gray-200', + info: 'bg-blue-50 border-blue-200 text-blue-700 dark:bg-white/5 dark:border-blue-400 dark:text-gray-200' + }; + + return ( +
+
{icon}
+
{children}
+
+ ); +} diff --git a/document/components/docs/Redirect.tsx b/document/components/docs/Redirect.tsx new file mode 100644 index 000000000..98fca7447 --- /dev/null +++ b/document/components/docs/Redirect.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +interface RedirectProps { + to: string; +} + +export function Redirect({ to }: RedirectProps) { + const router = useRouter(); + + useEffect(() => { + router.push(to); + }, [to, router]); + + return null; +} diff --git a/document/components/docs/Tabs.tsx b/document/components/docs/Tabs.tsx new file mode 100644 index 000000000..a264a7754 --- /dev/null +++ b/document/components/docs/Tabs.tsx @@ -0,0 +1,38 @@ +'use client'; + +import React, { useState } from 'react'; + +interface TabProps { + title: string; + children: React.ReactNode; +} + +interface TabsProps { + children: React.ReactNode; +} + +export const Tab: React.FC = ({ children }) => { + return
{children}
; +}; + +export const Tabs: React.FC = ({ children }) => { + const tabs = React.Children.toArray(children) as React.ReactElement[]; + const [activeTab, setActiveTab] = useState(0); + + return ( +
+ +
{tabs[activeTab]}
+
+ ); +}; diff --git a/document/components/docs/Video.tsx b/document/components/docs/Video.tsx new file mode 100644 index 000000000..8cd909610 --- /dev/null +++ b/document/components/docs/Video.tsx @@ -0,0 +1,12 @@ +export default function YouTube({ id }: { id: string }) { + return ( +
+