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

255 lines
7.3 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.

/**
* ═══════════════════════════════════════════════════════════
* MCP 工具 · 节点操作createNode / updateNode / deleteNode / queryNodes / getNode
* ═══════════════════════════════════════════════════════════
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*/
'use strict';
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');
const db = require('../db');
const cos = require('../cos');
// 内容长度阈值超过此值自动上传COS
const COS_THRESHOLD = 512;
/**
* createNode — 创建大脑节点
*/
async function createNode(input) {
const id = uuidv4();
const {
title, node_type, parent_id, path: nodePath, tags,
source, source_url, content, owner, created_by
} = input;
let summary = null;
let content_url = null;
let content_hash = null;
if (content) {
content_hash = crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
if (content.length <= COS_THRESHOLD) {
summary = content;
} else {
// 长内容上传COSsummary取前256字
summary = content.substring(0, 256);
const cosKey = `brain/${owner}/${node_type}/${id}.md`;
const result = await cos.write('hot', cosKey, content);
content_url = result.url;
}
}
// 如果没给path但给了parent_id自动生成路径
let finalPath = nodePath || null;
if (!finalPath && parent_id) {
const parentResult = await db.query('SELECT path, title FROM brain_nodes WHERE id = $1', [parent_id]);
if (parentResult.rows.length > 0) {
const parentPath = parentResult.rows[0].path || '/' + parentResult.rows[0].title;
finalPath = parentPath + '/' + title;
}
}
if (!finalPath) {
finalPath = '/' + title;
}
await db.query(
`INSERT INTO brain_nodes (id, title, node_type, parent_id, path, tags, source, source_url, content_url, summary, content_hash, owner, owner_user_id, status, created_by)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,'active',$14)`,
[id, title, node_type, parent_id || null, finalPath,
JSON.stringify(tags || []), source, source_url || null,
content_url, summary, content_hash, owner,
input.owner_user_id || null, created_by || owner]
);
return { node_id: id, path: finalPath, content_url };
}
/**
* updateNode — 更新节点
*/
async function updateNode(input) {
const { node_id } = input;
const updates = [];
const values = [];
let paramIndex = 1;
const allowedFields = ['title', 'tags', 'status', 'summary', 'path', 'node_type', 'source_url'];
for (const field of allowedFields) {
if (input[field] !== undefined) {
const val = field === 'tags' ? JSON.stringify(input[field]) : input[field];
updates.push(`${field} = $${paramIndex}`);
values.push(val);
paramIndex++;
}
}
// 如果有content更新
if (input.content !== undefined) {
const content_hash = crypto.createHash('sha256').update(input.content).digest('hex').substring(0, 16);
updates.push(`content_hash = $${paramIndex}`);
values.push(content_hash);
paramIndex++;
updates.push(`version = version + 1`);
if (input.content.length <= COS_THRESHOLD) {
updates.push(`summary = $${paramIndex}`);
values.push(input.content);
paramIndex++;
} else {
updates.push(`summary = $${paramIndex}`);
values.push(input.content.substring(0, 256));
paramIndex++;
// 获取当前节点信息用于COS路径
const nodeResult = await db.query('SELECT owner, node_type FROM brain_nodes WHERE id = $1', [node_id]);
if (nodeResult.rows.length > 0) {
const { owner, node_type } = nodeResult.rows[0];
const cosKey = `brain/${owner}/${node_type}/${node_id}.md`;
const cosResult = await cos.write('hot', cosKey, input.content);
updates.push(`content_url = $${paramIndex}`);
values.push(cosResult.url);
paramIndex++;
}
}
}
if (updates.length === 0) {
return { node_id, updated_fields: [] };
}
values.push(node_id);
await db.query(
`UPDATE brain_nodes SET ${updates.join(', ')} WHERE id = $${paramIndex}`,
values
);
return {
node_id,
updated_fields: Object.keys(input).filter(k => k !== 'node_id')
};
}
/**
* deleteNode — 软删除节点
*/
async function deleteNode(input) {
const { node_id } = input;
const result = await db.query(
"UPDATE brain_nodes SET status = 'deleted' WHERE id = $1 AND status != 'deleted'",
[node_id]
);
return { success: result.rowCount > 0 };
}
/**
* queryNodes — 查询/搜索节点
*/
async function queryNodes(input) {
const conditions = ["status != 'deleted'"];
const values = [];
let paramIndex = 1;
if (input.keyword) {
conditions.push(`(title ILIKE $${paramIndex} OR summary ILIKE $${paramIndex})`);
values.push(`%${input.keyword}%`);
paramIndex++;
}
if (input.node_type) {
conditions.push(`node_type = $${paramIndex}`);
values.push(input.node_type);
paramIndex++;
}
if (input.tags && input.tags.length > 0) {
conditions.push(`tags ?| $${paramIndex}`);
values.push(input.tags);
paramIndex++;
}
if (input.owner) {
conditions.push(`owner = $${paramIndex}`);
values.push(input.owner);
paramIndex++;
}
if (input.parent_id) {
conditions.push(`parent_id = $${paramIndex}`);
values.push(input.parent_id);
paramIndex++;
}
if (input.path_prefix) {
conditions.push(`path LIKE $${paramIndex}`);
values.push(input.path_prefix + '%');
paramIndex++;
}
if (input.status) {
// 覆盖默认的 != deleted 条件
conditions[0] = `status = $${paramIndex}`;
values.push(input.status);
paramIndex++;
}
const limit = Math.min(input.limit || 50, 200);
values.push(limit);
const sql = `SELECT id, title, node_type, parent_id, path, tags, source, source_url, content_url, summary, status, owner, created_at, updated_at
FROM brain_nodes
WHERE ${conditions.join(' AND ')}
ORDER BY updated_at DESC
LIMIT $${paramIndex}`;
const result = await db.query(sql, values);
// 获取总数
const countSql = `SELECT COUNT(*) as total FROM brain_nodes WHERE ${conditions.join(' AND ')}`;
const countResult = await db.query(countSql, values);
return {
nodes: result.rows,
total: parseInt(countResult.rows[0].total, 10)
};
}
/**
* getNode — 获取单个节点详情含COS完整内容
*/
async function getNode(input) {
const { node_id } = input;
const result = await db.query('SELECT * FROM brain_nodes WHERE id = $1', [node_id]);
if (result.rows.length === 0) {
return { node: null };
}
const node = result.rows[0];
// 如果有COS内容拉取完整内容
if (node.content_url) {
try {
const cosKey = node.content_url.replace(/^https?:\/\/[^/]+\//, '');
const cosResult = await cos.read('hot', cosKey);
node.content = cosResult.content;
} catch (err) {
node.content = node.summary;
node.content_error = err.message;
}
} else {
node.content = node.summary;
}
return { node };
}
module.exports = { createNode, updateNode, deleteNode, queryNodes, getNode };