212 lines
6.2 KiB
JavaScript
Raw Normal View History

/**
*
* MCP 工具 · 结构操作buildPath / scanStructure / classify
*
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*/
'use strict';
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
/**
* buildPath 批量创建路径结构类似 mkdir -p
* 路径中已存在的节点跳过不存在的自动创建为 folder 类型
*/
async function buildPath(input) {
const { path: targetPath, owner } = input;
const segments = targetPath.split('/').filter(s => s.length > 0);
const createdNodes = [];
let parentId = null;
let currentPath = '';
for (const segment of segments) {
currentPath += '/' + segment;
// 检查此路径是否已存在
const existing = await db.query(
'SELECT id FROM brain_nodes WHERE path = $1 AND status != $2',
[currentPath, 'deleted']
);
if (existing.rows.length > 0) {
parentId = existing.rows[0].id;
continue;
}
// 创建 folder 节点
const id = uuidv4();
await db.query(
`INSERT INTO brain_nodes (id, title, node_type, parent_id, path, tags, source, owner, status, created_by)
VALUES ($1, $2, 'folder', $3, $4, '[]'::jsonb, 'agent', $5, 'active', $6)`,
[id, segment, parentId, currentPath, owner, 'buildPath']
);
createdNodes.push({ id, title: segment, path: currentPath });
parentId = id;
}
return {
created_nodes: createdNodes,
leaf_node_id: parentId
};
}
/**
* scanStructure 扫描大脑结构健康度
*/
async function scanStructure(input) {
const rootPath = input.root_path || null;
const checks = input.checks || ['orphans', 'broken_links', 'duplicates', 'empty_folders'];
const result = {
total_nodes: 0,
orphan_nodes: [],
broken_links: [],
duplicate_titles: [],
empty_folders: [],
health_score: 100
};
// 总节点数
const countResult = await db.query(
"SELECT COUNT(*) as total FROM brain_nodes WHERE status = 'active'"
);
result.total_nodes = parseInt(countResult.rows[0].total, 10);
if (result.total_nodes === 0) {
return result;
}
// 孤岛节点有parent_id但parent不存在
if (checks.includes('orphans')) {
const orphans = await db.query(
`SELECT n.id, n.title, n.path, n.parent_id
FROM brain_nodes n
LEFT JOIN brain_nodes p ON n.parent_id = p.id
WHERE n.parent_id IS NOT NULL
AND n.status = 'active'
AND (p.id IS NULL OR p.status = 'deleted')`
);
result.orphan_nodes = orphans.rows;
}
// 断链:关系指向不存在的节点
if (checks.includes('broken_links')) {
const broken = await db.query(
`SELECT r.*
FROM brain_relations r
LEFT JOIN brain_nodes fn ON r.from_node_id = fn.id AND fn.status = 'active'
LEFT JOIN brain_nodes tn ON r.to_node_id = tn.id AND tn.status = 'active'
WHERE fn.id IS NULL OR tn.id IS NULL`
);
result.broken_links = broken.rows;
}
// 重复标题
if (checks.includes('duplicates')) {
const dupes = await db.query(
`SELECT title, array_agg(id) as node_ids, COUNT(*) as cnt
FROM brain_nodes
WHERE status = 'active'
GROUP BY title
HAVING COUNT(*) > 1
ORDER BY cnt DESC
LIMIT 20`
);
result.duplicate_titles = dupes.rows;
}
// 空目录
if (checks.includes('empty_folders')) {
const empty = await db.query(
`SELECT f.id, f.title, f.path
FROM brain_nodes f
LEFT JOIN brain_nodes c ON c.parent_id = f.id AND c.status = 'active'
WHERE f.node_type = 'folder'
AND f.status = 'active'
AND c.id IS NULL`
);
result.empty_folders = empty.rows;
}
// 计算健康分
const issues = result.orphan_nodes.length + result.broken_links.length +
result.duplicate_titles.length + result.empty_folders.length;
result.health_score = Math.max(0, Math.round(100 - (issues / result.total_nodes) * 100));
return result;
}
/**
* classify 自动分类/打标签
* strategy=rule: 纯关键词匹配零成本
* strategy=model: 调用DeepSeek极少使用
*/
async function classify(input) {
const { node_ids, strategy, rules, dry_run } = input;
const classified = [];
const unclassified = [];
for (const nodeId of node_ids) {
const nodeResult = await db.query(
'SELECT id, title, summary, tags, path FROM brain_nodes WHERE id = $1',
[nodeId]
);
if (nodeResult.rows.length === 0) continue;
const node = nodeResult.rows[0];
if (strategy === 'rule' && rules) {
// 规则匹配
const matchResult = applyRules(node, rules);
if (matchResult) {
classified.push({
node_id: nodeId,
assigned_tags: matchResult.tags,
assigned_path: matchResult.path
});
if (!dry_run) {
// 实际更新
const newTags = [...new Set([...(node.tags || []), ...matchResult.tags])];
await db.query(
'UPDATE brain_nodes SET tags = $1, path = COALESCE($2, path) WHERE id = $3',
[JSON.stringify(newTags), matchResult.path || null, nodeId]
);
}
} else {
unclassified.push(nodeId);
}
} else if (strategy === 'model') {
// 模型分类 — 占位S5阶段实现
unclassified.push(nodeId);
} else {
unclassified.push(nodeId);
}
}
return { classified, unclassified };
}
/**
* 规则引擎关键词标签映射
*/
function applyRules(node, rules) {
const text = ((node.title || '') + ' ' + (node.summary || '')).toLowerCase();
for (const [pattern, result] of Object.entries(rules)) {
const keywords = pattern.toLowerCase().split('|');
if (keywords.some(kw => text.includes(kw))) {
return {
tags: Array.isArray(result.tags) ? result.tags : [result.tags],
path: result.path || null
};
}
}
return null;
}
module.exports = { buildPath, scanStructure, classify };