UNPKG

vuepress-plugin-llms

Version:

📜 VuePress plugin for generating documentation friendly to Large Language Models (LLMs) | 📜 VuePress 插件,用于生成对大语言模型(LLMs)友好的文档。

226 lines (225 loc) 10.8 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import matter from 'gray-matter'; import { minimatch } from 'minimatch'; import pc from 'picocolors'; import { remark } from 'remark'; import remarkFrontmatter from 'remark-frontmatter'; import { remove } from 'unist-util-remove'; const PLUGIN_NAME = 'vuepress-plugin-llms'; import { defaultLLMsTxtTemplate } from './constants.js'; import { generateLLMsFullTxt, generateLLMsTxt } from './helpers/index.js'; import log from './helpers/logger.js'; import { extractTitle } from './helpers/utils.js'; /** * [VuePress](https://v2.vuepress.vuejs.org/) 插件,用于生成原始的 Markdown 格式文档,这种格式对**大语言模型(LLMs)**更轻量且更高效 * [VuePress](https://v2.vuepress.vuejs.org/) plugin for generating raw documentation for **LLMs** in Markdown format which is much lighter and more efficient for LLMs * * @param userSettings - Plugin settings. | 插件设置。 * @see https://github.com/guoqw7/vuepress-plugin-llms * @see https://llmstxt.org/ */ export default function llmstxt(userSettings = {}) { // Create a settings object with defaults explicitly merged // 创建一个设置对象,明确合并默认值 const settings = { generateLLMsTxt: true, generateLLMsFullTxt: true, stripHTML: true, ignoreFiles: [], workDir: '', ...userSettings, // Ensure workDir is set after merging }; // Set to store all markdown file paths // 用于存储所有markdown文件路径的集合 const mdFiles = new Set(); return { name: PLUGIN_NAME, /** Set up plugin on initialization | 在初始化时设置插件 */ onInitialized(app) { if (settings.workDir) { settings.workDir = path.resolve(app.dir.source(), settings.workDir); } else { settings.workDir = app.dir.source(); } log.info(`${pc.bold(PLUGIN_NAME)} initialized with workDir: ${pc.cyan(settings.workDir)}`); }, /** Register middleware to serve markdown files as plain text during dev | 在开发模式下注册中间件以纯文本形式提供markdown文件 */ onPrepared(app) { if (app.env.isDev) { // In dev mode, add middleware to serve .md and .txt files as plain text // This would require custom code to integrate with VuePress dev server // 在开发模式下,添加中间件以纯文本形式提供.md和.txt文件 // 这需要自定义代码与VuePress开发服务器集成 log.info('Dev server configured for serving plain text docs for LLMs'); } }, /** Process files after they've been prepared | 在文件准备好后处理它们 */ async onGenerated(app) { // Reset file collection // 重置文件集合 mdFiles.clear(); log.info('Starting markdown file collection'); // Collect all markdown files in the source directory // 收集源目录中的所有markdown文件 const collectMarkdownFiles = async (dir) => { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); // Skip files outside workDir if configured // 如果配置了workDir,跳过workDir之外的文件 if (!fullPath.startsWith(settings.workDir)) { continue; } // Process directories recursively // 递归处理目录 if (entry.isDirectory()) { await collectMarkdownFiles(fullPath); continue; } // Skip non-markdown files // 跳过非markdown文件 if (!entry.name.endsWith('.md')) { continue; } // Check if file should be ignored // 检查文件是否应该被忽略 if (settings.ignoreFiles?.length) { const relPath = path.relative(settings.workDir, fullPath); const shouldIgnore = settings.ignoreFiles.some(pattern => { if (typeof pattern === 'string') { return minimatch(relPath, pattern); } return false; }); if (shouldIgnore) { continue; } } // Add markdown file to collection // 将markdown文件添加到集合中 mdFiles.add(fullPath); } }; await collectMarkdownFiles(settings.workDir); const mdFilesList = Array.from(mdFiles); const fileCount = mdFilesList.length; // Skip if no files found // 如果没有找到文件则跳过 if (fileCount === 0) { log.warn(`No markdown files found to process. Check your \`${pc.bold('workDir')}\` and \`${pc.bold('ignoreFiles')}\` settings.`); return; } log.info(`Processing ${pc.bold(fileCount.toString())} markdown files from ${pc.cyan(settings.workDir)}`); // Prepare files for processing // 准备文件以进行处理 const preparedFiles = await Promise.all(mdFilesList.map(async (file) => { const content = await fs.readFile(file, 'utf-8'); let mdFile; if (settings.stripHTML) { const cleanedMarkdown = await remark() .use(remarkFrontmatter) .use(() => { // Strip HTML tags // 去除HTML标签 return (tree) => { remove(tree, { type: 'html' }); return tree; }; }) .process(content); mdFile = matter(String(cleanedMarkdown)); } else { mdFile = matter(content); } // Extract title from frontmatter or use the first heading // 从frontmatter中提取标题或使用第一个标题 const title = extractTitle(mdFile)?.trim() || 'Untitled'; const filePath = path.basename(file) === 'index.md' && path.dirname(file) !== settings.workDir ? `${path.dirname(file)}.md` : file; return { path: filePath, title, file: mdFile }; })); // Sort files by title for better organization // 按标题排序文件以获得更好的组织 preparedFiles.sort((a, b) => a.title.localeCompare(b.title)); const tasks = []; const outDir = app.dir.dest(); // Create output directory if it doesn't exist // 如果输出目录不存在则创建 try { await fs.access(outDir); } catch { log.info(`Creating output directory: ${pc.cyan(outDir)}`); await fs.mkdir(outDir, { recursive: true }); } // Generate llms.txt // 生成 llms.txt if (settings.generateLLMsTxt) { const llmsTxtPath = path.resolve(outDir, 'llms.txt'); const templateVariables = { title: settings.title, description: settings.description, details: settings.details, toc: settings.toc, ...settings.customTemplateVariables, }; tasks.push((async () => { log.info(`Generating ${pc.cyan('llms.txt')}...`); const siteConfig = app.siteData; // Find index.md file // 查找 index.md 文件 const indexMdPath = path.resolve(settings.workDir, 'index.md'); let indexMdExists = true; try { await fs.access(indexMdPath); } catch { indexMdExists = false; log.warn('index.md not found in workDir, using fallback values'); } const content = await generateLLMsTxt(preparedFiles, { indexMd: indexMdExists ? indexMdPath : preparedFiles[0]?.path || '', srcDir: settings.workDir, LLMsTxtTemplate: settings.customLLMsTxtTemplate || defaultLLMsTxtTemplate, templateVariables: templateVariables, siteConfig: { title: siteConfig.title, description: siteConfig.description, }, domain: settings.domain, linksExtension: '.md', cleanUrls: false, }); await fs.writeFile(llmsTxtPath, content, 'utf-8'); log.success(`Generated ${pc.cyan('llms.txt')} (${pc.bold(content.length.toString())} bytes)`); })()); } // Generate llms-full.txt // 生成 llms-full.txt if (settings.generateLLMsFullTxt) { const llmsFullTxtPath = path.resolve(outDir, 'llms-full.txt'); tasks.push((async () => { log.info(`Generating ${pc.cyan('llms-full.txt')}...`); const content = await generateLLMsFullTxt(preparedFiles, { srcDir: settings.workDir, domain: settings.domain, linksExtension: '.md', cleanUrls: false, }); await fs.writeFile(llmsFullTxtPath, content, 'utf-8'); log.success(`Generated ${pc.cyan('llms-full.txt')} (${pc.bold(content.length.toString())} bytes)`); })()); } // Wait for all tasks to complete // 等待所有任务完成 await Promise.all(tasks); log.success(`${pc.bold(PLUGIN_NAME)} completed all tasks`); } }; }