gitlink_help_center/scripts/build-knowledge.js

157 lines
4.8 KiB
JavaScript
Raw Permalink 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.

/**
* 帮助中心 AI 助手 —— 知识库构建脚本
*
* 作用:扫描 docs/ 下所有 Markdown 文档,去掉 frontmatter 与图片/代码噪声,
* 按标题(# / ## / ###切分成可检索的段落chunk输出为 knowledge.json
* 供前端 AI 助手做“客户端检索RAG”使用。
*
* 用法node scripts/build-knowledge.js
* (文档有更新后重新运行一次即可)
*/
const fs = require('fs');
const path = require('path');
const DOCS_DIR = path.resolve(__dirname, '..', 'docs');
const OUT_FILE = path.resolve(__dirname, '..', 'src', 'components', 'AIAssistant', 'knowledge.json');
/** 收集所有 .md 文件(相对路径) */
function walkMd(dir, base = '') {
const out = [];
for (const name of fs.readdirSync(dir)) {
const full = path.join(dir, name);
const rel = base ? `${base}/${name}` : name;
const stat = fs.statSync(full);
if (stat.isDirectory()) out.push(...walkMd(full, rel));
else if (name.endsWith('.md')) out.push({ abs: full, rel });
}
return out;
}
/** 去掉 frontmatter--- ... --- */
function stripFrontmatter(text) {
if (text.startsWith('---')) {
const end = text.indexOf('\n---', 3);
if (end !== -1) return text.slice(end + 4).replace(/^\s*\n/, '');
}
return text;
}
/** 取 frontmatter 里的 sidebar_label / label作为页面友好标题 */
function frontTitle(text, fileRel) {
const m = text.match(/^---([\s\S]*?)---/);
let label = '';
if (m) {
const lm = m[1].match(/sidebar_label:\s*['"]?([^'"\n]+)['"]?/);
const km = m[1].match(/label:\s*['"]?([^'"\n]+)['"]?/);
label = (lm && lm[1].trim()) || (km && km[1].trim()) || '';
}
if (!label) {
label = path.basename(fileRel, '.md');
}
return label.trim();
}
/** 计算页面在站点中的访问路径,与 Docusaurus routeBasePath:'/' 对齐 */
function pageRoute(fileRel, text) {
let slug = '';
const m = text.match(/^---([\s\S]*?)---/);
if (m) {
const sm = m[1].match(/slug:\s*['"]?([^'"\n]+)['"]?/);
if (sm) slug = sm[1].trim();
}
if (slug && slug !== '/') {
// 以 slug 里的内容作为路径(去掉前导斜杠)
return slug.replace(/^\/+/, '');
}
// 默认:目录/文件名(去掉 .md。intro.md 的 slug 是 / 单独处理。
const noExt = fileRel.replace(/\.md$/, '');
if (noExt === 'intro') return '';
return noExt;
}
/** 清洗一段文本去掉图片、HTML、链接保留文字、多余空行 */
function cleanText(text) {
return text
.replace(/!\[[^\]]*\]\([^)]*\)/g, '') // 图片
.replace(/<[^>]+>/g, ' ') // HTML 标签
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // 链接保留文字
.replace(/`{1,3}/g, '') // 代码反引号
.replace(/^\s{0,3}>\s?/gm, '') // 引用符
.replace(/\n{3,}/g, '\n\n')
.trim();
}
/** 按标题把一页切成多个 chunk */
function chunkPage(text, pageLabel, route, fileName) {
const lines = text.split('\n');
const chunks = [];
let curSection = pageLabel; // 当前小节标题
let buf = [];
const flush = () => {
const body = cleanText(buf.join('\n'));
if (body && body.length > 8) {
chunks.push({
title: pageLabel,
section: curSection,
path: '/' + route,
source: fileName,
text: body,
});
}
buf = [];
};
for (const raw of lines) {
const hm = raw.match(/^(#{1,4})\s+(.*)/);
if (hm) {
flush();
const level = hm[1].length;
const headText = cleanText(hm[2]);
// 用一级标题作为页面标题候选,二三级作为小节
if (level === 1) {
if (headText) { /* 一级标题作为页面正文一部分,不单独覆盖 pageLabel */ }
}
curSection = headText || pageLabel;
buf.push(raw); // 保留标题文字进正文
} else {
buf.push(raw);
}
}
flush();
return chunks;
}
function main() {
if (!fs.existsSync(DOCS_DIR)) {
console.error('[build-knowledge] docs 目录不存在:', DOCS_DIR);
process.exit(1);
}
const files = walkMd(DOCS_DIR);
const all = [];
let pageCount = 0;
for (const f of files) {
let raw;
try {
raw = fs.readFileSync(f.abs, 'utf8');
} catch (e) {
console.warn('[build-knowledge] 读取失败:', f.rel, e.message);
continue;
}
const label = frontTitle(raw, f.rel);
const route = pageRoute(f.rel, raw);
const body = stripFrontmatter(raw);
const chunks = chunkPage(body, label, route, f.rel);
if (chunks.length) {
all.push(...chunks);
pageCount += 1;
}
}
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
fs.writeFileSync(OUT_FILE, JSON.stringify(all, null, 0), 'utf8');
console.log(`[build-knowledge] 完成:${pageCount} 个文档,${all.length} 个段落 -> ${path.relative(process.cwd(), OUT_FILE)}`);
}
main();