feat(llm-wiki): 迁入 llm-wiki 技能包及依赖(85 文件,含 baoyu-url-to-markdown 适配器)

This commit is contained in:
2026-09-15 07:04:50 +08:00
parent a9c4518b06
commit 4cb834de0d
85 changed files with 73954 additions and 0 deletions

View File

@@ -0,0 +1,371 @@
#!/usr/bin/env node
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const {
assembleGraphArtifactPair,
commitGraphArtifactPair,
prepareOfflineWarningPayload,
serializeJsonForHtmlScript,
verifyGraphArtifactPair
} = require("./lib/graph-warning-bundle");
const { normalizeRelativePosixPath } = require("./lib/wiki-file-discovery");
const { scanKnowledgeBaseLinks, validatePortableMarkdownFilename } = require("./lib/wiki-link-index");
const { renderWikilinkReplacement } = require("./lib/wikilink-parser");
function usage() {
return [
"Usage:",
" node scripts/wiki-link-cli.js graph <kb-root> <output-dir> [--test-mode]",
" node scripts/wiki-link-cli.js commit-pair <kb-root> <graph-input> <warning-groups> <candidate-sets> <graph-path>",
" node scripts/wiki-link-cli.js warning-embed <kb-root> <graph-path> <warning-path> <output-path>",
" node scripts/wiki-link-cli.js check <kb-root> [--strict] [--json]",
" node scripts/wiki-link-cli.js rename-scan <kb-root> <source-path> <new-name>"
].join("\n");
}
function sanitizeEntries(entries) {
return entries.map((item) => ({
path: item.path,
kind: item.kind,
editable: item.editable,
graphType: item.graphType
}));
}
function sanitizeInventory(inventory) {
return {
graphSources: sanitizeEntries(inventory.graphSources),
lintSources: sanitizeEntries(inventory.lintSources),
renameEditableSources: sanitizeEntries(inventory.renameEditableSources),
renameReadOnlySources: sanitizeEntries(inventory.renameReadOnlySources),
targets: sanitizeEntries(inventory.targets),
fileSetSha256: inventory.fileSetSha256
};
}
function hasStrictErrors(report) {
return report.groups.some((group) => group.severity === "error");
}
function buildCheckReport(kbRoot, policy) {
const scan = scanKnowledgeBaseLinks(kbRoot, policy);
return {
policy,
inventory: sanitizeInventory(scan.inventory),
edges: scan.edges,
candidate_sets: scan.candidate_sets,
groups: scan.groups,
occurrences: scan.occurrences,
source_metadata: scan.source_documents.map(({ _content, ...document }) => document),
stale_pending_wrappers: scan.stale_pending_wrappers,
metrics: scan.metrics
};
}
function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function writeGraphScanFiles(kbRoot, outputDir) {
const scan = scanKnowledgeBaseLinks(kbRoot, "graph");
fs.mkdirSync(outputDir, { recursive: true });
const nodes = scan.source_documents
.filter((document) => document.graph_type)
.map((document) => ({
id: document.source_path,
label: document.label,
type: document.graph_type,
source_path: document.source_path,
_content: document._content,
_signals: document._signals
}))
.sort((left, right) => left.id.localeCompare(right.id, "en"));
const edges = scan.edges.map((edge, index) => ({
id: `e${index + 1}`,
from: edge.from,
to: edge.to,
type: edge.confidence,
confidence: edge.confidence,
relation_type: edge.relation_type
}));
writeJson(path.join(outputDir, "nodes.json"), nodes);
writeJson(path.join(outputDir, "edges.json"), edges);
writeJson(path.join(outputDir, "warning-groups.json"), scan.groups);
writeJson(path.join(outputDir, "candidate-sets.json"), scan.candidate_sets);
writeJson(path.join(outputDir, "scan-metrics.json"), scan.metrics);
return { nodes, edges, scan };
}
async function commitPair(args) {
const graphData = JSON.parse(fs.readFileSync(args.graphInput, "utf8"));
const groups = JSON.parse(fs.readFileSync(args.warningGroups, "utf8"));
const candidateSets = JSON.parse(fs.readFileSync(args.candidateSets, "utf8"));
const kbRootReal = fs.realpathSync.native(args.kbRoot);
const requestedGraphPath = path.resolve(args.graphPath);
const graphPath = path.join(
fs.realpathSync.native(path.dirname(requestedGraphPath)),
path.basename(requestedGraphPath)
);
const warningPath = path.join(path.dirname(graphPath), "graph-warnings.json");
const detailsRef = path.relative(kbRootReal, warningPath).split(path.sep).join("/");
const pair = assembleGraphArtifactPair({ graphData, groups, candidateSets, detailsRef });
await commitGraphArtifactPair({
kbRoot: kbRootReal,
graphPath,
warningPath,
pair
});
}
async function writeWarningEmbed(args) {
const verified = await verifyGraphArtifactPair({
kbRoot: args.kbRoot,
graphPath: args.graphPath,
warningPath: args.warningPath
});
let payload;
let scriptBytes;
if (verified.status === "available") {
const prepared = prepareOfflineWarningPayload({
summary: verified.graphData.meta.warning_summary,
bundle: verified.warningBundle
});
payload = prepared.payload;
scriptBytes = prepared.scriptBytes;
} else {
payload = {
summary: sanitizeUnavailableSummary(verified.summary),
details_status: "unavailable",
details_unavailable_reason: verified.reason,
warning_details_truncated: false,
omitted_group_count: 0,
omitted_candidate_set_count: 0
};
scriptBytes = serializeJsonForHtmlScript(payload);
}
fs.writeFileSync(args.outputPath, scriptBytes);
}
function sanitizeUnavailableSummary(summary) {
if (!summary || typeof summary !== "object") return {};
const safe = {};
for (const key of ["build_id", "details_sha256"]) {
if (typeof summary[key] === "string" && /^[a-f0-9]{64}$/.test(summary[key])) {
safe[key] = summary[key];
}
}
for (const key of ["total_groups", "total_occurrences", "error_occurrences", "warning_occurrences"]) {
if (Number.isSafeInteger(summary[key]) && summary[key] >= 0) safe[key] = summary[key];
}
if (summary.by_code && typeof summary.by_code === "object" && !Array.isArray(summary.by_code)) {
safe.by_code = Object.fromEntries(
Object.entries(summary.by_code)
.filter(([, count]) => Number.isSafeInteger(count) && count >= 0),
);
}
if (typeof summary.details_ref === "string") {
try {
const normalized = normalizeRelativePosixPath(summary.details_ref);
if (normalized === summary.details_ref && path.posix.basename(normalized) === "graph-warnings.json") {
safe.details_ref = normalized;
}
} catch (_) {
// Never copy an invalid or machine-local path into an offline artifact.
}
}
return safe;
}
function replacementTargetForOccurrence(occurrence, targetPath) {
return targetPath;
}
function buildRenameScanReport(kbRoot, sourcePath, newName) {
const scan = scanKnowledgeBaseLinks(kbRoot, "rename");
const candidateSets = new Map(scan.candidate_sets.map((item) => [item.candidate_set_id, item.candidates]));
const sourceEntry = scan.inventory.graphSources.find((item) => item.path === sourcePath);
if (!sourceEntry) {
throw new Error(`Source is not a formal graph page: ${sourcePath}`);
}
const validation = validatePortableMarkdownFilename(sourcePath, newName, scan.inventory.targets);
if (!validation.ok) {
throw new Error(`Invalid target name (${validation.reason}): ${newName}`);
}
const renameTargetPath = validation.target_path;
const editable = [];
const readOnly = [];
const ambiguous = [];
for (const occurrence of scan.occurrences) {
const destination = occurrence.read_only ? readOnly : editable;
const resolution = occurrence.resolution;
if (resolution.status === "resolved" && resolution.target_path === sourcePath) {
destination.push({
source_path: occurrence.source_path,
file_sha256: occurrence.file_sha256,
start_byte: occurrence.start_byte,
end_byte: occurrence.end_byte,
raw_link: occurrence.raw_link,
replacement: renderWikilinkReplacement(
occurrence,
replacementTargetForOccurrence(occurrence, renameTargetPath)
)
});
continue;
}
const candidatePaths = candidateSets.get(resolution.candidate_set_id) || [];
if (resolution.status === "ambiguous" && candidatePaths.includes(sourcePath)) {
const rendered_candidates = candidatePaths.map((candidatePath) => ({
candidate_path: candidatePath,
replacement: renderWikilinkReplacement(
occurrence,
candidatePath === sourcePath
? replacementTargetForOccurrence(occurrence, renameTargetPath)
: candidatePath
)
}));
ambiguous.push({
source_path: occurrence.source_path,
classification: occurrence.read_only ? "read_only" : "editable",
read_only: occurrence.read_only,
file_sha256: occurrence.file_sha256,
start_byte: occurrence.start_byte,
end_byte: occurrence.end_byte,
raw_link: occurrence.raw_link,
candidate_paths: candidatePaths,
rendered_candidates
});
}
}
return {
file_set_sha256: scan.inventory.fileSetSha256,
source_path: sourcePath,
target_path: renameTargetPath,
validation,
editable_occurrences: editable,
read_only_occurrences: readOnly,
ambiguous_occurrences: ambiguous,
metrics: scan.metrics
};
}
function assertExactFlags(flags, allowedFlags) {
const seen = new Set();
for (const flag of flags) {
if (!allowedFlags.has(flag)) {
throw new Error(`Unknown argument: ${flag}`);
}
if (seen.has(flag)) {
throw new Error(`Duplicate argument: ${flag}`);
}
seen.add(flag);
}
return seen;
}
function parseArguments(command, rest) {
if (command === "graph") {
if (rest.length < 2 || rest.length > 3 || rest[0].startsWith("--") || rest[1].startsWith("--")) {
throw new Error("graph requires <kb-root> <output-dir> [--test-mode]");
}
const flags = assertExactFlags(rest.slice(2), new Set(["--test-mode"]));
return { kbRoot: rest[0], outputDir: rest[1], testMode: flags.has("--test-mode") };
}
if (command === "check") {
if (rest.length < 1 || rest[0].startsWith("--")) {
throw new Error("check requires <kb-root> [--strict] [--json]");
}
const flags = assertExactFlags(rest.slice(1), new Set(["--strict", "--json"]));
return { kbRoot: rest[0], strict: flags.has("--strict"), json: flags.has("--json") };
}
if (command === "commit-pair") {
if (rest.length !== 5 || rest.some((value) => value.startsWith("--"))) {
throw new Error("commit-pair requires <kb-root> <graph-input> <warning-groups> <candidate-sets> <graph-path>");
}
return {
kbRoot: rest[0],
graphInput: rest[1],
warningGroups: rest[2],
candidateSets: rest[3],
graphPath: rest[4]
};
}
if (command === "warning-embed") {
if (rest.length !== 4 || rest.some((value) => value.startsWith("--"))) {
throw new Error("warning-embed requires <kb-root> <graph-path> <warning-path> <output-path>");
}
return { kbRoot: rest[0], graphPath: rest[1], warningPath: rest[2], outputPath: rest[3] };
}
if (command === "rename-scan") {
if (rest.length !== 3 || rest.some((value) => value.startsWith("--"))) {
throw new Error("rename-scan requires <kb-root> <source-path> <new-name>");
}
return { kbRoot: rest[0], sourcePath: rest[1], newName: rest[2] };
}
throw new Error(`Unknown command: ${command}`);
}
async function main(argv) {
const [command, ...rest] = argv;
if (!command) {
console.error(usage());
return 1;
}
try {
const args = parseArguments(command, rest);
if (command === "graph") {
const result = writeGraphScanFiles(args.kbRoot, args.outputDir);
process.stdout.write(`Graph scan wrote ${result.nodes.length} nodes and ${result.edges.length} edges to ${args.outputDir}\n`);
return 0;
}
if (command === "commit-pair") {
await commitPair(args);
process.stdout.write(`Graph artifact pair committed: ${args.graphPath}\n`);
return 0;
}
if (command === "warning-embed") {
await writeWarningEmbed(args);
return 0;
}
if (command === "check") {
const report = buildCheckReport(args.kbRoot, "lint");
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
return args.strict && hasStrictErrors(report) ? 2 : 0;
}
if (command === "rename-scan") {
const report = buildRenameScanReport(args.kbRoot, args.sourcePath, args.newName);
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
return 0;
}
} catch (error) {
console.error(error.message);
return 1;
}
}
main(process.argv.slice(2)).then(
(code) => { process.exitCode = code; },
(error) => {
console.error(error && error.message ? error.message : String(error));
process.exitCode = 1;
}
);