Files
djangoadmin-wiki/DjangoAdmin_Django_NaiveVue/scripts/generate-llms-txt.mjs
T
2026-10-25 22:36:33 +08:00

123 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 生成 public/llms.txt(llmstxt.org 标准)——AI 爬虫的站点摘要索引。
*
* 数据源:zh 目录下全部 Markdown 的 H1 + :::tip 概述首句(与 meta
* description 同源,复用 .vitepress/config/extract.mjs)。按侧边栏章节分组。
*
* 用法:node scripts/generate-llms-txt.mjs
* 已挂入 package.json 的 docs:build 前置步骤。
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
SITE_URL,
SITE_DESCRIPTION,
extractDescription,
} from "../.vitepress/config/extract.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const ZH = path.join(ROOT, "zh");
/** 顶层目录 → 侧边栏章节名(与 .vitepress/config/zh.ts 保持一致) */
const SECTIONS = [
["", "1. 了解项目"],
["volume1", "2. 快速入门"],
["volume2", "3. 模块开发实战"],
["volume3", "4. 架构设计"],
["volume4", "5. 开发指南"],
["volume5", "6. 代码生成器"],
["volume6", "7. 运维部署"],
["volume7", "8. 规范标准"],
["other", "附录"],
];
function walk(dir, out = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, entry.name);
if (entry.isDirectory()) walk(p, out);
else if (entry.name.endsWith(".md")) out.push(p);
}
return out;
}
/** 提取首个 H1 文本 */
function h1Of(md) {
// 围栏感知:跳过 ``` 代码块内的 # 注释,取真正的页面 H1
let inFence = false;
for (const line of md.split("\n")) {
if (line.startsWith("```")) { inFence = !inFence; continue; }
if (!inFence && line.startsWith("# ") && !line.startsWith("## ")) {
return line.slice(2).trim();
}
}
return "";
}
/** H1 编号排序键:"5.10 x" → [5,10];无编号排最后 */
function sortKey(h1) {
const m = h1.match(/^(\d+(?:\.\d+)*)/);
if (m) return m[1].split(".").map(Number);
const letter = h1.match(/^([A-Z])\./);
if (letter) return [1000 + letter[1].charCodeAt(0)];
return [9999];
}
function cmpKey(a, b) {
const len = Math.max(a.length, b.length);
for (let i = 0; i < len; i++) {
const x = a[i] ?? 0;
const y = b[i] ?? 0;
if (x !== y) return x - y;
}
return 0;
}
const files = walk(ZH);
const groups = new Map(SECTIONS.map(([dir, label]) => [label, []]));
for (const file of files) {
const rel = path.relative(ZH, file).replace(/\\/g, "/");
const topDir = rel.includes("/") ? rel.split("/")[0] : "";
const base = rel.split("/").pop();
const label = (topDir === "" && base === "faq.md") ? "附录"
: SECTIONS.find(([dir]) => dir === topDir)?.[1] ?? "附录";
const md = fs.readFileSync(file, "utf8");
const h1 = h1Of(md);
if (!h1) continue;
const desc = extractDescription(md);
const url = `${SITE_URL}/zh/${rel.replace(/\.md$/, ".html")}`;
groups.get(label).push({ h1, desc, url, key: sortKey(h1) });
}
const lines = [
"# DjangoAdmin Django+NaiveUI 开发文档",
"",
`> ${SITE_DESCRIPTION}。内置 RBAC 权限体系、代码生成器等开箱即用功能,支持五种数据库一键切换。本文档共 ${files.length} 篇,按章节组织。`,
"",
];
// 按章节顺序输出,合并重复 label(appendix/other 同为「附录」,只输出一次)
const printed = new Set();
for (const [, label] of SECTIONS) {
if (printed.has(label)) continue;
const items = groups.get(label);
if (!items?.length) continue;
printed.add(label);
lines.push(`## ${label}`, "");
items.sort((a, b) => cmpKey(a.key, b.key));
for (const { h1, desc, url } of items) {
lines.push(desc ? `- [${h1}](${url}): ${desc}` : `- [${h1}](${url})`);
}
lines.push("");
}
const out = path.join(ROOT, "public", "llms.txt");
fs.writeFileSync(out, lines.join("\n"), "utf8");
console.log(
`llms.txt: ${files.length} 页 → ${path.relative(ROOT, out)} (${printed.size} 个章节)`,
);