zhizhi/server/age-os/mcp-server/tools/structure-ops.js

212 lines
6.2 KiB
JavaScript
Raw Permalink 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.

/**
* ═══════════════════════════════════════════════════════════
* 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 };