Initial commit

This commit is contained in:
2026-05-19 22:27:43 +08:00
commit 2ee120f756
33 changed files with 11949 additions and 0 deletions

420
docker/API.md Normal file
View File

@@ -0,0 +1,420 @@
# VitePress API Documentation
## 概述
VitePress Docker 服务提供了一套完整的 RESTful API用于管理文档系统和 PDF 导出功能。
## 服务地址
- **API 服务**: `http://localhost:3001`
- **VitePress 文档**: `http://localhost:3000`
## API 端点概览
| 方法 | 端点 | 描述 |
|------|------|------|
| GET | `/health` | 健康检查 |
| GET | `/export-pdf` | 导出 PDF |
| GET | `/pdf-files` | 列出所有 PDF 文件 |
---
## API 端点详情
### 1. 健康检查
**端点**: `GET /health`
检查 API 服务和 VitePress 服务是否正常运行。
**响应示例**:
```json
{
"status": "ok",
"timestamp": "2026-04-11T12:00:00.000Z",
"services": {
"api": "running",
"vitepress": "running"
}
}
```
**状态码**:
- `200 OK`: 服务正常运行
**curl 示例**:
```bash
curl http://localhost:3001/health
```
---
### 2. 导出 PDF
**端点**: `GET /export-pdf`
启动 PDF 导出流程,将当前 VitePress 文档导出为 PDF 文件。
**请求参数**:
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|------|------|------|--------|------|
| `fileName` | string | 否 | `export-{timestamp}.pdf` | 导出文件名 |
**示例请求**:
```bash
# 使用默认文件名
curl "http://localhost:3001/export-pdf"
# 指定文件名
curl "http://localhost:3001/export-pdf?fileName=HF-MES-Manual.pdf"
```
**成功响应**:
```json
{
"success": true,
"message": "PDF exported successfully",
"pdfPath": "/app/docs/HF-MES-Manual.pdf",
"pdfUrl": "HF-MES-Manual.pdf",
"fileName": "HF-MES-Manual.pdf",
"fileSize": 1234567,
"fileSizeMB": "1.18"
}
```
**错误响应**:
```json
{
"success": false,
"error": "PDF file was not created",
"stack": "Error: PDF file was not created..."
}
```
**状态码**:
- `200 OK`: PDF 导出成功
- `500 Internal Server Error`: 导出失败
**工作流程**:
1. 清理旧的 `.vitepress/dist` 目录
2. 检查 VitePress 开发服务器是否运行
3. 执行 `npm run export-pdf` 命令
4. 查找生成的 PDF 文件
5. 返回 PDF 文件信息
**注意事项**:
- PDF 文件会保存到配置的 `DOCS_DIR` 目录下
- 如果 VitePress 服务未运行,导出会失败
- 首次导出可能需要较长时间
---
### 3. 列出 PDF 文件
**端点**: `GET /pdf-files`
列出 `DOCS_DIR` 目录下所有已导出的 PDF 文件。
**响应示例**:
```json
{
"count": 2,
"files": [
{
"name": "HF-MES-Manual.pdf",
"size": 1234567,
"sizeMB": "1.18",
"created": "2026-04-11T12:00:00.000Z",
"modified": "2026-04-11T12:00:00.000Z"
},
{
"name": "export-123456.pdf",
"size": 2345678,
"sizeMB": "2.24",
"created": "2026-04-11T11:00:00.000Z",
"modified": "2026-04-11T11:00:00.000Z"
}
]
}
```
**文件信息字段**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 文件名 |
| `size` | number | 文件大小(字节) |
| `sizeMB` | string | 文件大小MB保留两位小数 |
| `created` | string | 文件创建时间ISO 8601 |
| `modified` | string | 文件最后修改时间ISO 8601 |
**状态码**:
- `200 OK`: 成功获取文件列表
- `500 Internal Server Error`: 读取目录失败
**curl 示例**:
```bash
curl http://localhost:3001/pdf-files
```
---
## 使用示例
### 完整的 PDF 导出流程
```bash
# 1. 检查服务状态
curl http://localhost:3001/health
# 2. 导出 PDF指定文件名
curl "http://localhost:3001/export-pdf?fileName=My-Document.pdf"
# 3. 查看导出的文件
curl http://localhost:3001/pdf-files
# 4. 在浏览器中访问
# http://localhost:3000/My-Document.pdf
```
### 使用 JavaScript 调用
```javascript
// 检查服务状态
async function checkHealth() {
const response = await fetch('http://localhost:3001/health');
const data = await response.json();
console.log('Service Status:', data);
}
// 导出 PDF
async function exportPDF(fileName = null) {
const url = fileName
? `http://localhost:3001/export-pdf?fileName=${fileName}`
: 'http://localhost:3001/export-pdf';
const response = await fetch(url);
const data = await response.json();
if (data.success) {
console.log('PDF exported:', data.fileName);
console.log('File size:', data.fileSizeMB, 'MB');
} else {
console.error('Export failed:', data.error);
}
}
// 获取 PDF 列表
async function listPDFs() {
const response = await fetch('http://localhost:3001/pdf-files');
const data = await response.json();
console.log(`Found ${data.count} PDF files:`);
data.files.forEach(file => {
console.log(` - ${file.name} (${file.sizeMB} MB)`);
});
}
```
### 使用 Python 调用
```python
import requests
# 检查服务状态
def check_health():
response = requests.get('http://localhost:3001/health')
print(response.json())
# 导出 PDF
def export_pdf(file_name=None):
url = 'http://localhost:3001/export-pdf'
if file_name:
url += f'?fileName={file_name}'
response = requests.get(url)
data = response.json()
if data['success']:
print(f"PDF exported: {data['fileName']}")
print(f"File size: {data['fileSizeMB']} MB")
else:
print(f"Export failed: {data['error']}")
# 列出 PDF 文件
def list_pdfs():
response = requests.get('http://localhost:3001/pdf-files')
data = response.json()
print(f"Found {data['count']} PDF files:")
for file in data['files']:
print(f" - {file['name']} ({file['sizeMB']} MB)")
```
---
## 错误处理
### 常见错误
| 错误信息 | 可能原因 | 解决方案 |
|----------|----------|----------|
| `PDF file was not created` | VitePress 服务未运行 | 确认 http://localhost:3000 可访问 |
| `Export command failed` | PDF 导出命令执行失败 | 检查容器日志 |
| `ENOENT: no such file or directory` | 文档目录不存在 | 确认 DOCS_DIR 配置正确 |
### 错误响应格式
所有错误响应都遵循以下格式:
```json
{
"success": false,
"error": "错误描述信息",
"stack": "错误堆栈信息(仅在开发环境显示)"
}
```
---
## 配置说明
### 环境变量
| 变量名 | 默认值 | 说明 |
|--------|--------|------|
| `API_PORT` | 3001 | API 服务端口 |
| `DOCS_PATH` | /app/docs | 容器内文档目录路径 |
| `VITEPRESS_PORT` | 3000 | VitePress 服务端口 |
| `NODE_ENV` | production | Node.js 运行环境 |
### Docker Compose 配置
```yaml
services:
vitepress:
environment:
- API_PORT=3001
- DOCS_PATH=/app/docs
- VITEPRESS_PORT=3000
- NODE_ENV=production
ports:
- "3001:3001"
volumes:
- ${DOCS_DIR:-./docs}:/app/docs
```
---
## 注意事项
1. **PDF 文件位置**: 导出的 PDF 文件保存在配置的 `DOCS_DIR` 目录下,可直接在浏览器中通过 VitePress 服务访问
2. **VitePress 服务依赖**: PDF 导出功能依赖 VitePress 开发服务器运行,请确保 http://localhost:3000 可访问
3. **导出超时**: PDF 导出命令超时时间设置为 3 分钟,大型文档可能需要更长的时间
4. **并发限制**: 不建议同时发起多个 PDF 导出请求
5. **CORS 支持**: API 服务已启用 CORS允许来自任何源的请求
6. **文件覆盖**: 如果指定文件名已存在,导出时会被覆盖
---
## 技术实现
### API 服务架构
```
Express.js Server (端口 3001)
├── /health → 直接返回状态
├── /export-pdf → 执行 export-pdf 脚本
└── /pdf-files → 读取 DOCS_DIR 目录
```
### PDF 导出流程
```
1. Client Request
2. Clean .vitepress/dist
3. Check VitePress Server
4. Execute npm run export-pdf
5. Find Generated PDF
6. Return Response
```
### 依赖项
- **express**: Web 框架
- **cors**: 跨域资源共享
- **fs-extra**: 文件系统操作
- **vitepress-export-pdf**: PDF 导出插件
---
## 故障排除
### 服务无法访问
1. 检查容器状态:
```bash
docker ps | grep vitepress
```
2. 检查端口占用:
```bash
netstat -ano | findstr 3001
```
3. 查看日志:
```bash
docker-compose logs -f vitepress
```
### PDF 导出失败
1. 确认 VitePress 服务运行:
```bash
curl http://localhost:3000
```
2. 检查容器日志:
```bash
docker-compose logs -f
```
3. 验证文档目录挂载:
```bash
docker exec vitepress-docker ls -la /app/docs
```
4. 检查权限:
```bash
docker exec vitepress-docker whoami
```
### 文件列表为空
1. 确认 PDF 文件存在:
```bash
ls -la ./docs/*.pdf
```
2. 检查 DOCS_DIR 配置:
```bash
cat .env | grep DOCS_DIR
```
---
如有问题,请查看容器日志或联系技术支持。

97
docker/Dockerfile Normal file
View File

@@ -0,0 +1,97 @@
# VitePress Docker Image
FROM node:25
LABEL maintainer="xeden3"
LABEL description="VitePress Documentation with PDF Export Support"
# Install Chrome dependencies for PDF export (Debian)
RUN apt-get update && apt-get install -y \
chromium \
chromium-driver \
tzdata \
fonts-liberation \
libnss3 \
libatk-bridge2.0-0 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
libgbm1 \
libasound2 \
libpangocairo-1.0-0 \
libgtk-3-0 \
libxss1 \
libglib2.0-0 \
libxkbcommon0 \
libdrm2 \
libcups2 \
libdbus-1-3 \
# 中文字体支持开源字体Docker环境默认使用
fonts-wqy-microhei \
fonts-wqy-zenhei \
xfonts-intl-chinese \
fonts-noto-cjk \
# 俄语字体支持DejaVu 字体包含 Cyrillic 字符)
fonts-dejavu-core \
# 字体缓存工具
fontconfig \
&& fc-cache -fv \
&& rm -rf /var/lib/apt/lists/*
# 提示:如果需要使用微软雅黑字体,可以从 Windows 系统复制字体文件到容器
# Windows 字体目录C:\Windows\Fonts\msyh.ttc (微软雅黑)
# 将字体复制到容器docker cp msyh.ttc <container_name>:/usr/share/fonts/truetype/
# 然后运行fc-cache -fv
# Set environment variables
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
NODE_ENV=development \
CHROME_BIN=/usr/bin/chromium
WORKDIR /app
# Copy package files for main app
COPY package*.json ./
# Install main dependencies (vitepress, etc.)
RUN export NODE_ENV=development && \
rm -rf node_modules package-lock.json && \
npm install && \
npm install vitepress-export-pdf -D && \
npm install vitepress-plugin-mermaid mermaid && \
npm install medium-zoom markdown-it-mathjax3
# Copy API service
COPY docker/api/package*.json ./api/
WORKDIR /app/api
# Install API dependencies
RUN npm install
# Go back to app directory
WORKDIR /app
# Copy API service files
COPY docker/api/ ./api/
# Copy entry point script
COPY docker/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Create log directory
RUN mkdir -p /var/log
# Create working directory for docs
WORKDIR /app
# Expose ports
EXPOSE ${VITEPRESS_PORT:-3000} ${API_PORT:-3001}
# Volume for docs and PDF output
VOLUME ["/app/docs"]
WORKDIR /app
# Entry point
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD []

16
docker/api/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "vitepress-pdf-api",
"version": "1.0.0",
"description": "API service for VitePress PDF export",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"child_process": "^1.0.2",
"fs-extra": "^11.1.1"
}
}

220
docker/api/server.js Normal file
View File

@@ -0,0 +1,220 @@
import express from 'express';
import cors from 'cors';
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
const execAsync = promisify(exec);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.API_PORT || 3001;
// Enable CORS for all origins
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(express.json());
// Logging middleware
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
services: {
api: 'running',
vitepress: 'running'
}
});
});
// Export PDF endpoint
app.get('/export-pdf', async (req, res) => {
const docsPath = process.env.DOCS_PATH || '/app/docs';
const pdfFileName = req.query.fileName || `export-${Date.now()}.pdf`;
const vitepressPort = process.env.VITEPRESS_PORT || 3000;
console.log(`[${new Date().toISOString()}] Starting PDF export...`);
console.log(`Docs path: ${docsPath}`);
console.log(`PDF output: ${path.join(docsPath, pdfFileName)}`);
console.log(`VitePress port: ${vitepressPort}`);
try {
// Ensure PDF directory exists
await fs.ensureDir(docsPath);
// Clean old dist directory
const distDir = path.join(docsPath, '.vitepress', 'dist');
console.log('Cleaning old dist directory...');
await fs.remove(distDir).catch(() => {});
// Build the VitePress site
/*
console.log('Building VitePress site...');
try {
await execAsync('npm run docs:build', {
cwd: '/app',
stdio: 'inherit',
timeout: 120000 // 2 minutes timeout
});
} catch (buildError) {
console.error('Build warning/error:', buildError.message);
// Continue anyway, sometimes there are non-fatal warnings
}
*/
// Run PDF export (VitePress preview is already running)
console.log('Starting PDF export using running VitePress server...');
const exportEnv = {
...process.env,
NODE_ENV: 'development'
};
try {
/*
console.log('Installing vitepress-export-pdf...');
await execAsync('npm install vitepress-export-pdf -D', {
cwd: '/app',
stdio: 'inherit',
env: exportEnv,
timeout: 120000
});
*/
console.log('Running export-pdf...');
await execAsync('npm run export-pdf', {
cwd: '/app',
stdio: 'inherit',
env: exportEnv,
timeout: 180000,
maxBuffer: 1024 * 1024 * 100
});
} catch (exportError) {
console.error('Export command failed:', exportError.message);
throw new Error(`PDF export failed: ${exportError.message}`);
}
// Verify PDF was created
const pdfPath = path.join(docsPath, pdfFileName);
const pdfExists = await fs.pathExists(pdfPath);
if (!pdfExists) {
// Try to find any PDF file in the directory
const files = await fs.readdir(docsPath);
const pdfFiles = files.filter(f => f.endsWith('.pdf'));
if (pdfFiles.length > 0) {
console.log(`PDF found with different name: ${pdfFiles[0]}`);
const stats = await fs.stat(path.join(docsPath, pdfFiles[0]));
console.log(`[${new Date().toISOString()}] PDF export completed successfully!`);
console.log(`PDF size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
return res.json({
success: true,
message: 'PDF exported successfully',
pdfPath: path.join(docsPath, pdfFiles[0]),
pdfUrl: `${pdfFiles[0]}`,
fileName: pdfFiles[0],
fileSize: stats.size,
fileSizeMB: (stats.size / 1024 / 1024).toFixed(2)
});
} else {
throw new Error('PDF file was not created');
}
}
const stats = await fs.stat(pdfPath);
console.log(`[${new Date().toISOString()}] PDF export completed successfully!`);
console.log(`PDF path: ${pdfPath}`);
console.log(`PDF size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
res.json({
success: true,
message: 'PDF exported successfully',
pdfPath: pdfPath,
pdfUrl: `${pdfFileName}`,
fileName: pdfFileName,
fileSize: stats.size,
fileSizeMB: (stats.size / 1024 / 1024).toFixed(2)
});
} catch (error) {
console.error(`[${new Date().toISOString()}] PDF export failed:`, error);
res.status(500).json({
success: false,
error: error.message,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
});
}
});
// List all PDF files
app.get('/pdf-files', async (req, res) => {
const docsPath = process.env.DOCS_PATH || '/app/docs';
try {
await fs.ensureDir(docsPath);
const files = await fs.readdir(docsPath);
const pdfFiles = files.filter(f => f.endsWith('.pdf'));
const fileDetails = await Promise.all(
pdfFiles.map(async (file) => {
const stats = await fs.stat(path.join(docsPath, file));
return {
name: file,
size: stats.size,
sizeMB: (stats.size / 1024 / 1024).toFixed(2),
created: stats.birthtime,
modified: stats.mtime
};
})
);
res.json({
count: pdfFiles.length,
files: fileDetails
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] Error:`, err);
res.status(500).json({
error: err.message || 'Internal server error',
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
});
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`========================================`);
console.log(`VitePress API Server`);
console.log(`========================================`);
console.log(`API Server running on port: ${PORT}`);
console.log(`VitePress Server: http://localhost:${process.env.VITEPRESS_PORT || 3000}`);
console.log(`PDF output directory: ${process.env.DOCS_PATH || '/app/docs'}`);
console.log(`========================================`);
console.log(`Available API endpoints:`);
console.log(` GET /health - Health check`);
console.log(` GET /export-pdf - Export PDF`);
console.log(` GET /pdf-files - List all PDF files`);
console.log(`========================================`);
});

View File

@@ -0,0 +1,84 @@
#!/bin/sh
set -e
echo "========================================"
echo "VitePress Container"
echo "========================================"
# Set default values
export DOCS_PATH=${DOCS_PATH:-/app/docs}
export VITEPRESS_PORT=${VITEPRESS_PORT:-3000}
export API_PORT=${API_PORT:-3001}
echo "Configuration:"
echo " DOCS_PATH: $DOCS_PATH"
echo " VITEPRESS_PORT: $VITEPRESS_PORT"
echo " API_PORT: $API_PORT"
echo "========================================"
# Check if docs directory exists
if [ ! -d "$DOCS_PATH" ]; then
echo "Creating docs directory: $DOCS_PATH"
mkdir -p "$DOCS_PATH"
fi
# Check if package.json exists
if [ ! -f "/app/package.json" ]; then
echo "Error: /app/package.json not found!"
exit 1
fi
# Install API dependencies if needed
if [ ! -f "/app/api/node_modules/.package-lock.json" ]; then
echo "Installing API dependencies..."
cd /app/api && npm install
fi
echo ""
echo "========================================"
echo "Starting VitePress Documentation Server..."
echo "========================================"
# Start VitePress dev server in background (运行在 docs 目录下)
echo "[1/2] Starting VitePress dev server on port $VITEPRESS_PORT..."
cd /app
export NODE_ENV=development
nohup npm run docs:dev -- --port $VITEPRESS_PORT --host > /var/log/vitepress.log 2>&1 &
VITEPRESS_PID=$!
echo "VitePress started with PID: $VITEPRESS_PID"
# export NODE_ENV=development
# npm run docs:dev -- --port $VITEPRESS_PORT --host
# Wait for VitePress to start
echo "Waiting for VitePress to start..."
sleep 10
# Check if VitePress is running
if ! kill -0 $VITEPRESS_PID 2>/dev/null; then
echo "Warning: VitePress server may not have started properly"
echo "Check logs: cat /var/log/vitepress.log"
fi
echo "[2/2] Starting API server on port $API_PORT..."
echo ""
echo "========================================"
echo "All services started successfully!"
echo "========================================"
echo ""
echo "Services:"
echo " - VitePress Documentation: http://localhost:$VITEPRESS_PORT"
echo " - API Server: http://localhost:$API_PORT"
echo ""
echo "API Endpoints:"
echo " GET /health - Health check"
echo " GET /export-pdf - Export PDF"
echo "Logs:"
echo " VitePress: cat /var/log/vitepress.log"
echo " API Server: docker logs vitepress-docker"
echo ""
echo "========================================"
# Execute the API server with absolute path
exec node /app/api/server.js