From 1670974fa435a28de3c6915c30c42b00157e2cd5 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 25 Jun 2026 21:38:28 +0800 Subject: [PATCH] feat: add knowledge base build script --- package.json | 1 + scripts/build-knowledge-base.mjs | 278 +++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 scripts/build-knowledge-base.mjs diff --git a/package.json b/package.json index 150e438cb..97650f824 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "release:agents": "node scripts/release.mjs agents", "release:app": "node scripts/release.mjs app", "release:packages": "node scripts/release.mjs packages", + "build:knowledge-base": "node scripts/build-knowledge-base.mjs", "typecheck": "vue-tsc --noEmit --project apps/desktop/tsconfig.json", "build": "vite build --config apps/desktop/vite.config.ts", "build:checked": "pnpm typecheck && pnpm build", diff --git a/scripts/build-knowledge-base.mjs b/scripts/build-knowledge-base.mjs new file mode 100644 index 000000000..10aaa97b8 --- /dev/null +++ b/scripts/build-knowledge-base.mjs @@ -0,0 +1,278 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +const DEFAULT_INPUT = "docs/content/docs"; +const DEFAULT_OUTPUT = "outputs/knowledge-base/DBX_knowledge_base_cn.md"; +const DEFAULT_LANG = "cn"; + +await main(); + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const inputDir = path.resolve(args.input ?? DEFAULT_INPUT); + const outputFile = path.resolve(args.output ?? DEFAULT_OUTPUT); + const lang = args.lang ?? DEFAULT_LANG; + const fileSuffix = lang ? `.${lang}.mdx` : ".mdx"; + + const files = (await fs.readdir(inputDir)) + .filter((name) => name.endsWith(fileSuffix)) + .sort((a, b) => a.localeCompare(b)); + + if (files.length === 0) { + throw new Error(`No ${fileSuffix} files found in ${inputDir}`); + } + + const documents = []; + let entryCount = 0; + + for (const file of files) { + const sourcePath = path.join(inputDir, file); + const displayPath = path.relative(process.cwd(), sourcePath).replaceAll(path.sep, "/"); + const raw = await fs.readFile(sourcePath, "utf8"); + const { fields, body } = parseFrontmatter(raw); + const title = fields.title || file.replace(/\.mdx$/, ""); + const description = fields.description || ""; + const sections = []; + + for (const section of splitSections(body)) { + const clean = normalizeMdx(section.lines.join("\n")); + if (!clean || clean.length < 20) continue; + + const chunks = chunkText(clean).map((chunk, index, all) => { + const heading = all.length > 1 ? `${section.heading}(${index + 1}/${all.length})` : section.heading; + return { + heading, + question: questionFor(title, section.heading, chunk), + keywords: keywordsFor(title, section.heading, description, chunk), + answer: chunk, + }; + }); + + entryCount += chunks.length; + sections.push(...chunks); + } + + documents.push({ source: displayPath, title, description, sections }); + } + + await fs.mkdir(path.dirname(outputFile), { recursive: true }); + await fs.writeFile(outputFile, renderMarkdown(documents), "utf8"); + + console.log(`Knowledge base written: ${path.relative(process.cwd(), outputFile)}`); + console.log(`Sources: ${files.length}`); + console.log(`Entries: ${entryCount}`); +} + +function parseArgs(argv) { + const parsed = {}; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--help" || arg === "-h") { + printHelp(); + process.exit(0); + } + if (!arg.startsWith("--")) { + throw new Error(`Unexpected argument: ${arg}`); + } + const key = arg.slice(2); + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing value for ${arg}`); + } + parsed[key] = value; + index += 1; + } + return parsed; +} + +function printHelp() { + console.log(`Usage: node scripts/build-knowledge-base.mjs [options] + +Options: + --input MDX docs directory. Default: ${DEFAULT_INPUT} + --output Output Markdown path. Default: ${DEFAULT_OUTPUT} + --lang File language suffix, e.g. cn for *.cn.mdx. Default: ${DEFAULT_LANG} +`); +} + +function parseFrontmatter(source) { + const match = source.match(/^---\n([\s\S]*?)\n---\n?/); + const fields = {}; + if (!match) return { fields, body: source }; + + for (const line of match[1].split("\n")) { + const separator = line.indexOf(":"); + if (separator === -1) continue; + const key = line.slice(0, separator).trim(); + const value = line.slice(separator + 1).trim().replace(/^["']|["']$/g, ""); + fields[key] = value; + } + + return { fields, body: source.slice(match[0].length) }; +} + +function splitSections(body) { + const sections = []; + let current = { heading: "概览", lines: [] }; + + for (const line of body.split("\n")) { + const heading = line.match(/^(#{2,3})\s+(.+?)\s*$/); + if (heading) { + if (current.lines.join("\n").trim()) sections.push(current); + current = { heading: heading[2].replace(/`/g, "").trim(), lines: [] }; + continue; + } + current.lines.push(line); + } + + if (current.lines.join("\n").trim()) sections.push(current); + return sections; +} + +function normalizeMdx(text) { + return text + .replace(/]*)>([\s\S]*?)<\/Card>/g, (_, attrs, inner) => { + const title = attrs.match(/title=["']([^"']+)["']/)?.[1] ?? ""; + const href = attrs.match(/href=["']([^"']+)["']/)?.[1] ?? ""; + return [title && `**${title}**`, normalizeMdx(inner), href && `参考链接:${href}`].filter(Boolean).join("\n\n"); + }) + .replace(/]*>([\s\S]*?)<\/Callout>/g, (_, inner) => normalizeMdx(inner)) + .replace(/]*>([\s\S]*?)<\/Tab>/g, (_, inner) => normalizeMdx(inner)) + .replace(/]*>([\s\S]*?)<\/Step>/g, (_, inner) => normalizeMdx(inner)) + .replace(/<[^>\n]+>/g, "") + .replace(/\{\/\*[\s\S]*?\*\/\}/g, "") + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1($2)") + .replace(/^\s{0,6}#{1,6}\s+(.+)$/gm, "$1") + .replace(/^\s*import\s+.*$/gm, "") + .replace(/^\s*export\s+.*$/gm, "") + .replace(/[ \t]+\n/g, "\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + +function chunkText(text, maxLength = 2200) { + if (text.length <= maxLength) return [text]; + + const chunks = []; + let current = ""; + for (const paragraph of text.split(/\n{2,}/)) { + const next = [current, paragraph].filter(Boolean).join("\n\n"); + if (next.length > maxLength && current) { + chunks.push(current); + current = paragraph; + } else { + current = next; + } + } + if (current) chunks.push(current); + + return chunks.flatMap((chunk) => { + if (chunk.length <= maxLength) return [chunk]; + const pieces = []; + for (let index = 0; index < chunk.length; index += maxLength) { + pieces.push(chunk.slice(index, index + maxLength)); + } + return pieces; + }); +} + +function questionFor(category, heading, text) { + if (heading === "概览") return `DBX 的${category}是什么?`; + if (/安装|配置|导入|导出|创建|连接|使用流程|快速开始|启动|构建|下载|打开|执行|添加|删除|更新/.test(heading)) { + return `DBX 中如何${heading.replace(/^(第一步|第二步|第三步|第四步|第五步):/, "")}?`; + } + if (/常见|问题|故障|错误|诊断|冲突|无法/.test(heading) || /失败|报错|检查/.test(text)) { + return `DBX ${heading}时应该检查什么?`; + } + if (/支持|覆盖|矩阵|类型|数据库|文件|供应商|快捷键|命令|端口/.test(heading)) { + return `DBX ${heading}支持哪些内容?`; + } + return `DBX 的${heading}是什么?`; +} + +function keywordsFor(category, heading, description, text) { + const pool = `${category} ${heading} ${description} ${text}`; + const candidates = [ + "DBX", + "AI", + "SQL", + "MCP", + "JDBC", + "SSH", + "Docker", + "CLI", + "MongoDB", + "Redis", + "MySQL", + "PostgreSQL", + "SQLite", + "DuckDB", + "ClickHouse", + "Oracle", + "SQL Server", + "Schema", + "ER", + "导入", + "导出", + "连接", + "表", + "字段", + "查询", + "快捷键", + "插件", + "驱动", + "配置", + "安全", + "数据传输", + "格式化", + "代码片段", + "隧道", + "生产环境", + ]; + const found = candidates.filter((word) => pool.includes(word)); + return [...new Set([category, heading, ...found])].slice(0, 14).join(", "); +} + +function renderMarkdown(documents) { + const lines = [ + "# DBX 中文知识库", + "", + "> 从 `docs/content/docs/*.cn.mdx` 生成的纯 Markdown 知识库内容。", + "", + "## 来源索引", + "", + "| 来源文件 | 标题 | 描述 | 知识条数 |", + "| --- | --- | --- | ---: |", + ]; + + for (const document of documents) { + lines.push( + `| ${escapeTableCell(document.source)} | ${escapeTableCell(document.title)} | ${escapeTableCell(document.description)} | ${document.sections.length} |`, + ); + } + + for (const document of documents) { + lines.push("", `## ${document.title}`, ""); + if (document.description) lines.push(document.description, ""); + lines.push(`来源:\`${document.source}\``, ""); + + for (const section of document.sections) { + lines.push(`### ${section.heading}`, ""); + lines.push(`问题:${section.question}`, ""); + lines.push(`关键词:${section.keywords}`, ""); + lines.push("答案:", ""); + lines.push(section.answer, ""); + lines.push("---", ""); + } + } + + return `${lines.join("\n").replace(/\n{4,}/g, "\n\n\n").trim()}\n`; +} + +function escapeTableCell(value) { + return String(value ?? "") + .replaceAll("\\", "\\\\") + .replaceAll("|", "\\|") + .replace(/\s+/g, " ") + .trim(); +}