Files
Fulfilled-Knowledge/Understand-Anything-main/understand-anything-plugin/packages/dashboard/scripts/sync-wishfulfilled-graph.mjs
2026-05-27 15:40:32 +08:00

175 lines
5.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.
import fs from 'node:fs';
import path from 'node:path';
const root = 'D:/AIcoding/WishFulfilled/知识库/under-anything';
const wiki = `${root}/wishfulfilled-wiki`;
const reqDir = `${wiki}/05_需求文档`;
const graphPaths = [
`${wiki}/.understand-anything/knowledge-graph.json`,
`${root}/wishfulfilled-dashboard/knowledge-graph.json`,
];
const metaPaths = [
`${wiki}/.understand-anything/meta.json`,
`${root}/wishfulfilled-dashboard/meta.json`,
];
const files = fs.readdirSync(reqDir)
.filter((name) => /\.(md|html?)$/i.test(name))
.sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
function readText(filePath) {
return fs.readFileSync(filePath, 'utf8');
}
function cleanLineNumbers(text) {
const lines = text.split(/\r?\n/);
let changed = 0;
const cleaned = lines.map((line) => {
const match = line.match(/^\s*\d+\|(.+)$/);
if (!match) return line;
changed += 1;
return match[1];
});
return changed >= Math.max(3, Math.floor(lines.length / 4)) ? cleaned.join('\n') : text;
}
function stripHtml(text) {
return text
.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/\s+/g, ' ')
.trim();
}
function titleFor(fileName, text) {
if (/\.html?$/i.test(fileName)) {
const match = text.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
const title = match ? stripHtml(match[1]) : '';
if (title) return title;
}
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (trimmed.startsWith('#')) return trimmed.replace(/^#+\s*/, '').trim();
}
return path.basename(fileName, path.extname(fileName)).replace(/^\d+[-_]/, '').replace(/_/g, ' ');
}
function summaryFor(fileName, text) {
const plain = /\.html?$/i.test(fileName)
? stripHtml(text)
: text.replace(/[`*_>#|\-\[\]()]/g, ' ').replace(/\s+/g, ' ').trim();
return plain ? plain.slice(0, 180) : '需求文档。';
}
function tagsFor(text) {
const tags = ['05_需求文档', '需求文档'];
const match = text.match(/^tags:\s*\[(.*?)\]/m);
if (!match) return tags;
for (const item of match[1].split(/[,]/)) {
const tag = item.trim().replace(/^['"]|['"]$/g, '');
if (tag && !tags.includes(tag)) tags.push(tag);
}
return tags;
}
function complexityFor(text) {
if (text.length > 20000) return 'complex';
if (text.length > 5000) return 'moderate';
return 'simple';
}
function updateGraph(graphPath) {
const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8'));
graph.nodes ??= [];
graph.edges ??= [];
graph.layers ??= [];
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
const edgeKeys = new Set(graph.edges.map((edge) => `${edge.source}|${edge.target}|${edge.type}`));
let layer = graph.layers.find((item) => item.id === 'layer-requirements');
if (!layer) {
layer = {
id: 'layer-requirements',
name: '需求文档',
description: '所有正式需求、业务规则、需求变更和需求索引。',
nodeIds: ['flow:layer-requirements'],
};
graph.layers.push(layer);
}
layer.nodeIds ??= [];
let added = 0;
let updated = 0;
for (const fileName of files) {
const absolutePath = `${reqDir}/${fileName}`;
const relPath = `05_需求文档/${fileName}`;
const nodeId = `doc:${relPath.replace(/\.[^.]+$/, '')}`;
const text = cleanLineNumbers(readText(absolutePath));
const node = {
id: nodeId,
type: 'document',
name: titleFor(fileName, text),
filePath: relPath,
summary: summaryFor(fileName, text),
tags: tagsFor(text),
complexity: complexityFor(text),
knowledgeMeta: {
content: text,
wikilinks: [...text.matchAll(/\[\[([^\]]+)\]\]/g)].map((match) => match[1]),
category: 'layer-requirements',
},
};
if (byId.has(nodeId)) {
Object.assign(byId.get(nodeId), node);
updated += 1;
} else {
graph.nodes.push(node);
byId.set(nodeId, node);
added += 1;
}
if (!layer.nodeIds.includes(nodeId)) layer.nodeIds.push(nodeId);
const edgeKey = `flow:layer-requirements|${nodeId}|documents`;
if (!edgeKeys.has(edgeKey)) {
graph.edges.push({
source: 'flow:layer-requirements',
target: nodeId,
type: 'documents',
direction: 'forward',
description: '本层文档',
weight: 0.65,
});
edgeKeys.add(edgeKey);
}
}
const count = layer.nodeIds.filter((id) => id !== 'flow:layer-requirements').length;
const flow = byId.get('flow:layer-requirements');
if (flow) {
flow.summary = '所有正式需求、业务规则、需求变更和需求索引。点击本层可查看全部需求文档并检索。';
flow.knowledgeMeta ??= {};
flow.knowledgeMeta.content = `# 需求文档\n\n所有正式需求、业务规则、需求变更和需求索引。点击本层可查看全部需求文档并检索。\n\n本层包含 ${count} 个文档。点击右侧 Files 或在本层详情中选择具体文档查看内容。`;
flow.knowledgeMeta.category = 'layer-requirements';
}
graph.project ??= {};
graph.project.analyzedAt = new Date().toISOString();
fs.writeFileSync(graphPath, `${JSON.stringify(graph, null, 2)}\n`, 'utf8');
return { graphPath, added, updated, requirements: count, nodes: graph.nodes.length };
}
const results = graphPaths.map(updateGraph);
for (const metaPath of metaPaths) {
if (!fs.existsSync(metaPath)) continue;
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
const graph = JSON.parse(fs.readFileSync(`${path.dirname(metaPath)}/knowledge-graph.json`, 'utf8'));
meta.lastAnalyzedAt = new Date().toISOString();
meta.analyzedFiles = graph.nodes.filter((node) => String(node.id).startsWith('doc:')).length;
fs.writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
}
console.log(JSON.stringify(results, null, 2));