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(/|/gi, ' ') .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/\s+/g, ' ') .trim(); } function titleFor(fileName, text) { if (/\.html?$/i.test(fileName)) { const match = text.match(/]*>([\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));