feat(docs): enhance documentation structure and add Docker support for standalone Next.js app

This commit is contained in:
HouYunFei
2026-06-02 13:23:11 +08:00
parent 7802771e56
commit 6413011e54
24 changed files with 250 additions and 94 deletions
+4
View File
@@ -2,6 +2,10 @@
.idea .idea
docs2 docs2
data data
docs/.next
docs/.source
docs/node_modules
docs/out
web/.next web/.next
web/node_modules web/node_modules
web/out web/out
+43
View File
@@ -0,0 +1,43 @@
name: Docs Docker image
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}-docs
tags: |
type=raw,value=latest
type=ref,event=tag
type=sha,prefix=
- uses: docker/build-push-action@v6
with:
context: .
file: ./docs/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=docs
cache-to: type=gha,mode=max,scope=docs
+5 -6
View File
@@ -3,15 +3,14 @@
## Unreleased ## Unreleased
+ [新增] 新增文档站点页面。 + [新增] 新增文档站点页面。
+ [优化] 优化画布连线交互。
## v0.2.0 - 2026-06-01 ## v0.2.0 - 2026-06-01
+ [新增] 支持通过火山方舟 Agent Plan 接入 Seedance 2.0 视频生成,后端会自动适配任务创建、轮询和结果回填接口 + [新增] 支持通过火山方舟AgentPlan接入。
+ [新增] 视频创作台和画布视频生成支持 Seedance 分辨率、比例、时长、生成声音、水印,以及参考图片、参考视频、参考音频输入。 + [新增] 视频生成支持声音、水印及图片/视频/音频参考输入。
+ [新增] 画布新增音频节点,并新增参考素材上传与公开访问接口,配合 `PUBLIC_BASE_URL` 供火山方舟拉取本地参考素材 + [新增] 画布新增音频节点。
+ [优化] 管理后台模型渠道支持 Agent Plan Base URL 归一、Seedance 模型配置提示、渠道探测超时和已启用模型自动同步到公开可用模型 + [优化] 图片/视频素材支持 `图片1`编号注入提示词
+ [优化] 图片/视频参考素材会按 `图片1``视频1``音频1` 编号注入提示词,图像请求尺寸和质量参数也会在请求前归一校验。
+ [修复] 修复 Docker 部署时 SQLite 相对路径可能写入错误目录的问题,并优化 AI 上游鉴权、限流和 Seedance 敏感参考视频错误提示。
## v0.1.1 - 2026-05-30 ## v0.1.1 - 2026-05-30
+1 -1
View File
@@ -3,7 +3,7 @@ FROM oven/bun:1.3.13 AS web-build
WORKDIR /app/web WORKDIR /app/web
COPY web/package.json web/bun.lock ./ COPY web/package.json web/bun.lock ./
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile --registry=https://registry.npmmirror.com --cache-dir=/root/.bun/install/cache RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile --cache-dir=/root/.bun/install/cache
COPY VERSION /app/VERSION COPY VERSION /app/VERSION
COPY CHANGELOG.md /app/CHANGELOG.md COPY CHANGELOG.md /app/CHANGELOG.md
COPY web ./ COPY web ./
+17
View File
@@ -101,6 +101,23 @@ https://infinite-canvas-cpco.onrender.com?apiKey={key}&baseUrl={address}
- [系统配置数据结构](docs2/system-settings.md) - [系统配置数据结构](docs2/system-settings.md)
- [接口响应约定](docs2/api-response.md) - [接口响应约定](docs2/api-response.md)
## 赞助支持
<div align="center">
如果这个项目对你有帮助,欢迎通过爱发电赞助支持,你的每一份鼓励都是持续更新的动力!
<br>
<a href="https://ifdian.net/a/basketikun">
<img src="https://img.shields.io/badge/%E7%88%B1%E5%8F%91%E7%94%B5-%E8%B5%9E%E5%8A%A9%E4%BD%9C%E8%80%85-946ce6?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyMS4zNWwtMS40NS0xLjMyQzUuNCAxNS4zNiAyIDEyLjI4IDIgOC41IDIgNS40MiA0LjQyIDMgNy41IDNjMS43NCAwIDMuNDEuODEgNC41IDIuMDlDMTMuMDkgMy44MSAxNC43NiAzIDE2LjUgMyAxOS41OCAzIDIyIDUuNDIgMjIgOC41YzAgMy43OC0zLjQgNi44Ni04LjU1IDExLjU0TDEyIDIxLjM1eiIvPjwvc3ZnPg==&logoColor=white" alt="爱发电赞助" />
</a>
<br>
<br>
</div>
## 社区支持 ## 社区支持
学 AI,上 L 站:[LinuxDO](https://linux.do/) 学 AI,上 L 站:[LinuxDO](https://linux.do/)
+9
View File
@@ -0,0 +1,9 @@
node_modules
.next
out
.source
coverage
*.log
.env*
!.env.example
.DS_Store
+28
View File
@@ -0,0 +1,28 @@
FROM oven/bun:1.3.13 AS docs-build
WORKDIR /app
COPY docs/package.json docs/bun.lock ./
RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile --ignore-scripts --cache-dir=/root/.bun/install/cache
COPY docs ./
COPY CHANGELOG.md /CHANGELOG.md
RUN bun run postinstall
RUN bun run build
FROM node:22-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
RUN groupadd --system --gid 1001 nodejs && useradd --system --uid 1001 --gid nodejs nextjs
COPY --from=docs-build --chown=nextjs:nodejs /app/public ./public
COPY --from=docs-build --chown=nextjs:nodejs /app/index.md ./index.md
COPY --from=docs-build --chown=nextjs:nodejs /app/content ./content
COPY --from=docs-build --chown=nextjs:nodejs /app/.source ./.source
COPY --from=docs-build --chown=nextjs:nodejs /app/source.config.ts ./source.config.ts
COPY --from=docs-build --chown=nextjs:nodejs /CHANGELOG.md /CHANGELOG.md
COPY --from=docs-build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=docs-build --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
+24 -19
View File
@@ -3,25 +3,39 @@
This is a Next.js application generated with This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs). [Create Fumadocs](https://github.com/fuma-nama/fumadocs).
It is a Next.js app with [Static Export](https://nextjs.org/docs/app/guides/static-exports) configured. It runs as a server-backed Next.js docs site and is configured for standalone
output. Route handlers such as search and LLM text remain available at runtime.
Run development server: Run development server:
```bash ```bash
npm run dev bun run dev
# or
pnpm dev
# or
yarn dev
``` ```
Open http://localhost:3000 with your browser to see the result. Build and run local production server:
```bash
bun run build
bun run start
```
Run the published image with Docker Compose:
```bash
docker compose up -d
```
Or build locally with Docker Compose:
```bash
docker compose -f docker-compose.local.yml up -d --build
```
## Explore ## Explore
In the project, you can see: In the project, you can see:
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content. - `lib/source.ts`: Code for content source adapter, `loader()` provides the interface to access your content.
- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep. - `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
| Route | Description | | Route | Description |
@@ -32,16 +46,7 @@ In the project, you can see:
### Fumadocs MDX ### Fumadocs MDX
A `source.config.ts` config file has been included, you can customise different options like frontmatter schema. A `source.config.ts` config file has been included, you can customise different
options like frontmatter schema.
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details. Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
## Learn More
To learn more about Next.js and Fumadocs, take a look at the following
resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs
+18
View File
@@ -38,6 +38,24 @@ cp .env.example .env
docker compose -f docker-compose.local.yml up -d --build docker compose -f docker-compose.local.yml up -d --build
``` ```
## 文档站镜像
文档站位于 `docs/` 目录,按带服务端能力的 Next.js standalone 应用单独构建,不打进主应用镜像。
使用发布镜像:
```bash
cd docs
docker compose up -d
```
基于当前源码本地构建:
```bash
cd docs
docker compose -f docker-compose.local.yml up -d --build
```
## 数据目录 ## 数据目录
`docker-compose.yml` 会把本地 `./data` 挂载到容器内 `/app/data`,用于保存 SQLite 数据库、提示词数据和上传素材。 `docker-compose.yml` 会把本地 `./data` 挂载到容器内 `/app/data`,用于保存 SQLite 数据库、提示词数据和上传素材。
@@ -5,6 +5,8 @@ description: 当前版本已实现但仍需人工验证的变更项
# 待测试 # 待测试
- 文档站搜索改为中英文混合 tokenizer,中文正文、标题和短语会按中文词、单字、二元和三元片段建立索引;需要验证 `/api/search` 和搜索弹窗能命中文档中的中文关键词。
- 文档站改为 Next.js standalone server 输出,新增 `docs/Dockerfile`、`docs/docker-compose.yml` 和 `docs/docker-compose.local.yml` 独立运行入口;需要验证文档页、搜索接口和 LLM 文本接口在文档站容器中可访问。
- 画布连线支持右键打开删除菜单;拖拽连线到目标卡片内部、连接点附近或卡片边缘外扩范围内会自动吸附并连接,拖到已有但不可连接的节点附近不会再误弹创建节点菜单。 - 画布连线支持右键打开删除菜单;拖拽连线到目标卡片内部、连接点附近或卡片边缘外扩范围内会自动吸附并连接,拖到已有但不可连接的节点附近不会再误弹创建节点菜单。
- 外部软件可通过 URL 查询参数 `baseUrl`/`baseurl` 和 `apiKey`/`apikey` 跳转到前端;读取后会从地址栏移除这些参数,后台允许自定义渠道时会自动切到自定义渠道、填入配置并打开配置弹窗,未允许时会打开配置弹窗并提示无法导入。 - 外部软件可通过 URL 查询参数 `baseUrl`/`baseurl` 和 `apiKey`/`apikey` 跳转到前端;读取后会从地址栏移除这些参数,后台允许自定义渠道时会自动切到自定义渠道、填入配置并打开配置弹窗,未允许时会打开配置弹窗并提示无法导入。
- 生图工作台和画布生图会把参考图按当前顺序显示为 `图片1`、`图片2` 等编号,并在图生图请求的实际提示词中注入编号说明;需要验证 `/image` 参考图排序、画布配置节点输入顺序和画布助手参考图编号一致。 - 生图工作台和画布生图会把参考图按当前顺序显示为 `图片1`、`图片2` 等编号,并在图生图请求的实际提示词中注入编号说明;需要验证 `/image` 参考图排序、画布配置节点输入顺序和画布助手参考图编号一致。
+8 -1
View File
@@ -15,4 +15,11 @@ description: 支持无限画布项目继续维护
- 给 GitHub 仓库点 Star,帮助项目被更多人看到。 - 给 GitHub 仓库点 Star,帮助项目被更多人看到。
- 金额打赏,用于支持 AI 工具订阅、模型服务和项目维护成本。 - 金额打赏,用于支持 AI 工具订阅、模型服务和项目维护成本。
- Codex、Claude 等 AI 编程工具订阅账号支持,帮助维护者持续使用 AI 辅助开发。 - Codex、Claude 等 AI 编程工具订阅账号支持,帮助维护者持续使用 AI 辅助开发。
<div className="not-prose mt-6 flex flex-wrap items-center gap-3">
<span className="text-lg font-semibold text-fd-foreground">赞助地址:</span>
<a href="https://ifdian.net/a/basketikun" target="_blank" rel="noreferrer" className="inline-flex">
<img src="https://img.shields.io/badge/%E7%88%B1%E5%8F%91%E7%94%B5-%E8%B5%9E%E5%8A%A9%E4%BD%9C%E8%80%85-946ce6?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAyMS4zNWwtMS40NS0xLjMyQzUuNCAxNS4zNiAyIDEyLjI4IDIgOC41IDIgNS40MiA0LjQyIDMgNy41IDNjMS43NCAwIDMuNDEuODEgNC41IDIuMDlDMTMuMDkgMy44MSAxNC43NiAzIDE2LjUgMyAxOS41OCAzIDIyIDUuNDIgMjIgOC41YzAgMy43OC0zLjQgNi44Ni04LjU1IDExLjU0TDEyIDIxLjM1eiIvPjwvc3ZnPg==&logoColor=white" alt="爱发电赞助" />
</a>
</div>
+8
View File
@@ -0,0 +1,8 @@
services:
docs:
build:
context: ..
dockerfile: docs/Dockerfile
ports:
- "3001:3000"
restart: unless-stopped
+6
View File
@@ -0,0 +1,6 @@
services:
docs:
image: ghcr.io/basketikun/infinite-canvas-docs:latest
ports:
- "3001:3000"
restart: unless-stopped
+1
View File
@@ -4,6 +4,7 @@ const withMDX = createMDX();
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const config = { const config = {
output: 'standalone',
reactStrictMode: true, reactStrictMode: true,
}; };
+2 -2
View File
@@ -5,7 +5,7 @@
"scripts": { "scripts": {
"build": "next build", "build": "next build",
"dev": "next dev", "dev": "next dev",
"start": "serve out", "start": "next start",
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit", "types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
"postinstall": "fumadocs-mdx" "postinstall": "fumadocs-mdx"
}, },
@@ -31,4 +31,4 @@
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.0",
"typescript": "^6.0.3" "typescript": "^6.0.3"
} }
} }
+4 -2
View File
@@ -1,9 +1,11 @@
import { source } from '@/lib/source'; import { source } from '@/lib/source';
import { createDocsSearchTokenizer } from '@/lib/search-tokenizer';
import { createFromSource } from 'fumadocs-core/search/server'; import { createFromSource } from 'fumadocs-core/search/server';
export const revalidate = false; export const revalidate = false;
export const { staticGET: GET } = createFromSource(source, { export const { staticGET: GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages components: {
language: 'english', tokenizer: createDocsSearchTokenizer(),
},
}); });
+9
View File
@@ -2,10 +2,19 @@
@import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css'; @import 'fumadocs-ui/css/preset.css';
:root {
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
--font-display: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
}
html { html {
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
body {
font-family: var(--font-body);
}
html > body[data-scroll-locked] { html > body[data-scroll-locked] {
margin-right: 0px !important; margin-right: 0px !important;
--removed-body-scroll-bar-size: 0px !important; --removed-body-scroll-bar-size: 0px !important;
+1 -18
View File
@@ -1,26 +1,9 @@
import { Noto_Sans_SC, Sora } from 'next/font/google';
import { Provider } from '@/components/provider'; import { Provider } from '@/components/provider';
import './global.css'; import './global.css';
const bodyFont = Noto_Sans_SC({
subsets: ['latin'],
weight: ['400', '500', '600'],
variable: '--font-body',
});
const displayFont = Sora({
subsets: ['latin'],
weight: ['500', '600', '700'],
variable: '--font-display',
});
export default function Layout({ children }: LayoutProps<'/'>) { export default function Layout({ children }: LayoutProps<'/'>) {
return ( return (
<html <html lang="zh-CN" suppressHydrationWarning>
lang="zh-CN"
className={`${bodyFont.className} ${bodyFont.variable} ${displayFont.variable}`}
suppressHydrationWarning
>
<body className="flex flex-col min-h-screen"> <body className="flex flex-col min-h-screen">
<Provider>{children}</Provider> <Provider>{children}</Provider>
</body> </body>
-28
View File
@@ -1,28 +0,0 @@
import { getPageImage, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
import { appName } from '@/lib/shared';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<DefaultImage title={page.data.title} description={page.data.description} site={appName} />,
{
width: 1200,
height: 630,
},
);
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
}
+4 -2
View File
@@ -13,12 +13,14 @@ import {
import { useDocsSearch } from 'fumadocs-core/search/client'; import { useDocsSearch } from 'fumadocs-core/search/client';
import { create } from '@orama/orama'; import { create } from '@orama/orama';
import { useI18n } from 'fumadocs-ui/contexts/i18n'; import { useI18n } from 'fumadocs-ui/contexts/i18n';
import { createDocsSearchTokenizer } from '@/lib/search-tokenizer';
function initOrama() { function initOrama() {
return create({ return create({
schema: { _: 'string' }, schema: { _: 'string' },
// https://docs.orama.com/docs/orama-js/supported-languages components: {
language: 'english', tokenizer: createDocsSearchTokenizer(),
},
}); });
} }
+1 -4
View File
@@ -1,6 +1,6 @@
import { getMDXComponents } from '@/components/mdx'; import { getMDXComponents } from '@/components/mdx';
import { gitConfig } from '@/lib/shared'; import { gitConfig } from '@/lib/shared';
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source'; import { getPageMarkdownUrl, source } from '@/lib/source';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx'; import { createRelativeLink } from 'fumadocs-ui/mdx';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';
@@ -41,8 +41,5 @@ export function getDocPageMetadata(page: DocPageData): Metadata {
return { return {
title: page.data.title, title: page.data.title,
description: page.data.description, description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
}; };
} }
+54
View File
@@ -0,0 +1,54 @@
type OramaTokenizer = {
language: string;
normalizationCache: Map<string, string>;
tokenize: (raw: string, language?: string, prop?: string, withCache?: boolean) => string[];
};
const wordPattern = /[\p{Script=Han}]+|[a-z0-9][a-z0-9_'-]*/giu;
const hanPattern = /^\p{Script=Han}+$/u;
const chineseSegmenter = 'Segmenter' in Intl ? new Intl.Segmenter('zh-CN', { granularity: 'word' }) : null;
function getChineseSegments(value: string) {
if (!chineseSegmenter) return [];
return Array.from(chineseSegmenter.segment(value))
.filter((item) => item.isWordLike)
.map((item) => item.segment);
}
function addChineseTokens(tokens: string[], value: string) {
const chars = Array.from(value);
if (chars.length <= 12) tokens.push(value);
tokens.push(...getChineseSegments(value));
for (let size = 1; size <= 3; size += 1) {
if (chars.length < size) continue;
for (let i = 0; i <= chars.length - size; i += 1) {
tokens.push(chars.slice(i, i + size).join(''));
}
}
}
export function createDocsSearchTokenizer(): OramaTokenizer {
return {
language: 'zh-CN',
normalizationCache: new Map(),
tokenize(raw) {
if (typeof raw !== 'string') return [raw];
const tokens: string[] = [];
const input = raw.normalize('NFKC').toLowerCase();
for (const match of input.matchAll(wordPattern)) {
const value = match[0];
if (hanPattern.test(value)) {
addChineseTokens(tokens, value);
} else {
tokens.push(value);
}
}
return Array.from(new Set(tokens.filter(Boolean)));
},
};
}
-1
View File
@@ -1,6 +1,5 @@
export const appName = '无限画布'; export const appName = '无限画布';
export const docsRoute = '/docs'; export const docsRoute = '/docs';
export const docsImageRoute = '/og/docs';
export const docsContentRoute = '/llms.mdx/docs'; export const docsContentRoute = '/llms.mdx/docs';
// fill this with your actual GitHub info, for example: // fill this with your actual GitHub info, for example:
+1 -10
View File
@@ -1,6 +1,6 @@
import { docs } from 'collections/server'; import { docs } from 'collections/server';
import { loader } from 'fumadocs-core/source'; import { loader } from 'fumadocs-core/source';
import { docsContentRoute, docsImageRoute, docsRoute } from './shared'; import { docsContentRoute, docsRoute } from './shared';
// See https://fumadocs.dev/docs/headless/source-api for more info // See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({ export const source = loader({
@@ -9,15 +9,6 @@ export const source = loader({
plugins: [], plugins: [],
}); });
export function getPageImage(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `${docsImageRoute}/${segments.join('/')}`,
};
}
export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) { export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'content.md']; const segments = [...page.slugs, 'content.md'];