204 lines
6.7 KiB
TypeScript
204 lines
6.7 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import matter from 'gray-matter';
|
|
import { remark } from 'remark';
|
|
import html from 'remark-html';
|
|
import remarkGfm from 'remark-gfm';
|
|
import { cache } from 'react';
|
|
|
|
const contentDir = path.join(process.cwd(), 'content');
|
|
|
|
// Simple in-memory cache for development performance
|
|
const docCache = new Map<string, any>();
|
|
const sidebarCache: { data: any | null } = { data: null };
|
|
|
|
export interface DocMeta {
|
|
title: string;
|
|
slug: string[];
|
|
description?: string;
|
|
summary?: string;
|
|
}
|
|
|
|
export interface DocPage {
|
|
meta: DocMeta;
|
|
contentHtml: string;
|
|
}
|
|
|
|
export interface SidebarItem {
|
|
title: string;
|
|
slug: string;
|
|
children?: SidebarItem[];
|
|
}
|
|
|
|
// Category display name mapping
|
|
const categoryNames: Record<string, string> = {
|
|
gateway: '网关配置',
|
|
channels: '渠道接入',
|
|
install: '安装部署',
|
|
cli: '命令行工具',
|
|
concepts: '核心概念',
|
|
tools: '工具系统',
|
|
providers: '模型供应商',
|
|
platforms: '平台适配',
|
|
start: '快速入门',
|
|
automation: '自动化',
|
|
help: '帮助中心',
|
|
nodes: '节点管理',
|
|
plugins: '插件系统',
|
|
web: 'Web 界面',
|
|
security: '安全配置',
|
|
reference: '参考手册',
|
|
experiments: '实验功能',
|
|
debug: '调试排查',
|
|
diagnostics: '诊断工具',
|
|
refactor: '重构指南',
|
|
design: '设计文档',
|
|
pipelines: '开发流水线',
|
|
};
|
|
|
|
export function getCategoryName(key: string): string {
|
|
return categoryNames[key] || key;
|
|
}
|
|
|
|
function getAllMdFiles(dir: string, baseSlug: string[] = []): { filePath: string; slug: string[] }[] {
|
|
const items: { filePath: string; slug: string[] }[] = [];
|
|
if (!fs.existsSync(dir)) return items;
|
|
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
items.push(...getAllMdFiles(fullPath, [...baseSlug, entry.name]));
|
|
} else if (entry.name.endsWith('.md')) {
|
|
const name = entry.name.replace(/\.md$/, '');
|
|
items.push({ filePath: fullPath, slug: [...baseSlug, name] });
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
export function getAllDocSlugs(): string[][] {
|
|
return getAllMdFiles(contentDir).map(f => f.slug);
|
|
}
|
|
|
|
export const getDocBySlug = cache(async (slug: string[]): Promise<DocPage | null> => {
|
|
const cacheKey = slug.join('/');
|
|
if (docCache.has(cacheKey)) return docCache.get(cacheKey);
|
|
|
|
// Try exact path first
|
|
let filePath = path.join(contentDir, ...slug) + '.md';
|
|
if (!fs.existsSync(filePath)) {
|
|
// Try as directory with index.md
|
|
filePath = path.join(contentDir, ...slug, 'index.md');
|
|
if (!fs.existsSync(filePath)) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
const { data, content } = matter(fileContent);
|
|
|
|
// Process markdown
|
|
const processedContent = await remark()
|
|
.use(remarkGfm)
|
|
.use(html, { sanitize: false })
|
|
.process(content);
|
|
|
|
let contentHtml = processedContent.toString();
|
|
|
|
// Fix internal links:
|
|
// 1. Convert unparsed markdown links inside tags: [text](/link) -> <a href="/docs/link">text</a>
|
|
// 2. Prefix absolute links with /docs (e.g. /gateway/config -> /docs/gateway/config)
|
|
// 3. Strip .md from links (e.g. config.md -> config)
|
|
contentHtml = contentHtml
|
|
// Fix [text](/link) -> <a href="/docs/link">text</a> (happens inside custom tags like <Note>)
|
|
.replace(/\[([^\]]+)\]\(\/([^)]+)\)/g, (match, text, link) => {
|
|
const cleanLink = link.startsWith('docs/') ? link : `docs/${link.replace(/\.md$/, '')}`;
|
|
return `<a href="/${cleanLink}">${text}</a>`;
|
|
})
|
|
// Fix href="/link" in standard tags or cards
|
|
.replace(/href="\/([^/][^"]*)"/g, (match, p1) => {
|
|
if (p1.startsWith('docs/')) return match;
|
|
if (p1.startsWith('http')) return match;
|
|
return `href="/docs/${p1.replace(/\.md$/, '')}"`;
|
|
})
|
|
// Fix any remaining .md in hrefs
|
|
.replace(/href="([^"]+)\.md(#?[^"]*)"/g, 'href="$1$2"');
|
|
|
|
// Clear cache to ensure changes are picked up immediately
|
|
docCache.clear();
|
|
sidebarCache.data = null;
|
|
|
|
const result = {
|
|
meta: {
|
|
title: data.title || slug[slug.length - 1],
|
|
slug,
|
|
description: data.description,
|
|
summary: data.summary,
|
|
},
|
|
contentHtml,
|
|
};
|
|
|
|
docCache.set(cacheKey, result);
|
|
return result;
|
|
});
|
|
|
|
export const getSidebarStructure = cache((): SidebarItem[] => {
|
|
if (sidebarCache.data) return sidebarCache.data;
|
|
|
|
const items: SidebarItem[] = [];
|
|
if (!fs.existsSync(contentDir)) return items;
|
|
const entries = fs.readdirSync(contentDir, { withFileTypes: true });
|
|
|
|
// Root-level .md files
|
|
const rootFiles = entries.filter(e => !e.isDirectory() && e.name.endsWith('.md'));
|
|
for (const f of rootFiles) {
|
|
const name = f.name.replace(/\.md$/, '');
|
|
const filePath = path.join(contentDir, f.name);
|
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
const { data } = matter(fileContent);
|
|
items.push({
|
|
title: data.title || name,
|
|
slug: `/docs/${name}`,
|
|
});
|
|
}
|
|
|
|
// Directories
|
|
const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => {
|
|
const order = ['start', 'install', 'gateway', 'channels', 'concepts', 'tools', 'providers', 'platforms', 'automation', 'cli', 'plugins', 'nodes', 'web', 'security', 'reference', 'help'];
|
|
const ai = order.indexOf(a.name);
|
|
const bi = order.indexOf(b.name);
|
|
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
|
});
|
|
|
|
for (const dir of dirs) {
|
|
const dirPath = path.join(contentDir, dir.name);
|
|
const children: SidebarItem[] = [];
|
|
|
|
const subEntries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
for (const sub of subEntries) {
|
|
if (sub.isDirectory()) continue;
|
|
if (!sub.name.endsWith('.md')) continue;
|
|
const subName = sub.name.replace(/\.md$/, '');
|
|
const subFilePath = path.join(dirPath, sub.name);
|
|
const subContent = fs.readFileSync(subFilePath, 'utf-8');
|
|
const { data } = matter(subContent);
|
|
children.push({
|
|
title: data.title || subName,
|
|
slug: `/docs/${dir.name}/${subName}`,
|
|
});
|
|
}
|
|
|
|
if (children.length > 0) {
|
|
items.push({
|
|
title: getCategoryName(dir.name),
|
|
slug: `/docs/${dir.name}`,
|
|
children,
|
|
});
|
|
}
|
|
}
|
|
|
|
sidebarCache.data = items;
|
|
return items;
|
|
});
|