first commit
This commit is contained in:
203
lib/docs.ts
Normal file
203
lib/docs.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
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;
|
||||
});
|
||||
Reference in New Issue
Block a user