255 lines
7.3 KiB
JavaScript
255 lines
7.3 KiB
JavaScript
|
|
/**
|
|||
|
|
* ═══════════════════════════════════════════════════════════
|
|||
|
|
* 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 {
|
|||
|
|
// 长内容上传COS,summary取前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 };
|