guanghulab/server/age-os/mcp-server/tools/notion-persona-cognition-ops.js

614 lines
20 KiB
JavaScript
Raw Normal View History

/**
*
* AGE OS · MCP 工具: Notion 人格体认知操作
*
*
* 编号: ZY-MCP-NOTION-COGNITION-001
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 为霜砚(SY001)和其他人格体提供认知记忆的MCP工具层
* 基于 HLDP 三核心字段(trigger/emergence/lock)
* HNL 四枝干模型(T1认知/T2母语/T3经验/T4情感)
*
* 提供 3 MCP 工具:
* notionPersonaCognitionQuery 按枝干/类型/人格体查询认知叶片
* notionContextInject 根据对话关键词从Notion拉取相关认知
* notionCognitionGrow 将新认知增量写回Notion树叶DB
*/
'use strict';
// 运行时可选加载 notion-client优雅降级
let notionClient = null;
try {
notionClient = require('../notion-client');
} catch {
// Notion模块未安装时降级
}
// ─── 数据库配置 ───
// 霜砚认知运行时DB + 开发工单DB通过环境变量配置
const PERSONA_DB_CONFIG = {
// 冰朔 2026-04-14 配置 · 已Share给「曜冥纪元-HoloLake」Integration
cognition_runtime: process.env.ZY_NOTION_COGNITION_RUNTIME_DB || '23d48c8c4379425f95f10884cb9a4bf5',
dev_workorder: process.env.ZY_NOTION_DEV_WORKORDER_DB || '518f6dd0e90044d2a7f813b7a75715bc',
// HNL桥接协议中定义的Notion侧树杈/树叶DB
tree_branch: process.env.ZY_NOTION_TREE_BRANCH_DB || 'ca6ac139bdc94746ab6ce346b0122e17',
tree_leaf: process.env.ZY_NOTION_TREE_LEAF_DB || '77784b234047469d81bbb4a9a45a2986'
};
// ─── 四枝干映射 ───
const TRUNK_MAP = {
T1: { name: '认知', description: '身份·born_with', searchable: true },
T2: { name: '母语', description: 'HNL·born_with', searchable: true },
T3: { name: '经验', description: '动态经验·每日成长', searchable: true },
T4: { name: '情感', description: '自由生长·不可量化', searchable: false }
};
// ─── 叶片类型 ───
const LEAF_TYPES = ['💬对话', '💡认知', '💕情感', '🔧技术', '📊业务', '🌊HLDP', '📜系统'];
// ═══════════════════════════════════════════════════════════
// 工具 1: notionPersonaCognitionQuery
// ═══════════════════════════════════════════════════════════
/**
* notionPersonaCognitionQuery 按四枝干/类型/人格体查询认知叶片
*
* input:
* database_id: string 数据库 ID可选默认用 cognition_runtime
* trunk: string 枝干过滤: T1/T2/T3T4情感不可检索
* leaf_type: string 叶片类型过滤可选
* persona: string 人格体名称过滤可选
* keyword: string 标题关键词搜索可选
* state: string 状态过滤: 活跃/归档/休眠可选默认活跃
* page_size: number 每页数量可选默认10
* start_cursor: string 分页游标可选
*/
async function notionPersonaCognitionQuery(input) {
if (!notionClient) {
return {
results: [],
has_more: false,
error: 'Notion模块未加载',
diagnostic: {
reason: 'ZY_NOTION_TOKEN 环境变量未配置',
token_configured: !!process.env.ZY_NOTION_TOKEN
}
};
}
const {
database_id, trunk, leaf_type, persona,
keyword, state, page_size, start_cursor
} = input;
// 解析目标数据库
const dbId = database_id || PERSONA_DB_CONFIG.cognition_runtime;
if (!dbId) throw new Error('未配置认知运行时数据库ID (ZY_NOTION_COGNITION_RUNTIME_DB)');
// T4情感不可检索
if (trunk === 'T4') {
return {
count: 0,
items: [],
message: '情感枝干(T4)不可检索。情感是自由生长的空间,不可枚举/分类/量化。',
rule: 'UA-04 · 记忆主权 + 冰朔D64指令'
};
}
// 构建 Notion 过滤器
const filters = [];
if (trunk && TRUNK_MAP[trunk]) {
filters.push({
property: '枝干',
select: { equals: `${trunk}${TRUNK_MAP[trunk].name}` }
});
}
if (leaf_type && LEAF_TYPES.includes(leaf_type)) {
filters.push({
property: '类型',
select: { equals: leaf_type }
});
}
if (persona) {
filters.push({
property: '标题',
title: { contains: persona }
});
}
if (keyword) {
filters.push({
property: '标题',
title: { contains: keyword }
});
}
// 默认只查活跃状态
filters.push({
property: '状态',
select: { equals: state || '活跃' }
});
const filter = filters.length === 1
? filters[0]
: { and: filters };
const sorts = [{ property: '时间戳', direction: 'descending' }];
const result = await notionClient.queryDatabase(
dbId, filter, sorts, Math.min(page_size || 10, 100), start_cursor
);
return {
count: result.results.length,
has_more: result.has_more,
next_cursor: result.next_cursor,
trunk_filter: trunk || 'all',
items: result.results.map(page => simplifyLeaf(page))
};
}
// ═══════════════════════════════════════════════════════════
// 工具 2: notionContextInject
// ═══════════════════════════════════════════════════════════
/**
* notionContextInject 根据对话内容智能判断从Notion拉取什么认知注入
*
* 这个工具是霜砚Agent的核心它分析当前对话消息
* 判断该从哪个枝干拉取什么认知然后返回压缩后的上下文片段
*
* input:
* message: string 当前用户消息
* persona: string 当前人格体身份默认: 霜砚
* session_context: string 当前会话的简要上下文摘要可选
* max_items: number 最大返回条目数可选默认5
* database_id: string 数据库 ID可选
*/
async function notionContextInject(input) {
if (!notionClient) {
// 优雅降级Notion 未连接时返回诊断信息而非抛错
return {
persona: input.persona || '霜砚',
injected_count: 0,
context_payload: '',
error: 'Notion模块未加载',
diagnostic: {
reason: 'ZY_NOTION_TOKEN 环境变量未配置或 @notionhq/client 未安装',
fix_steps: [
'1. 确认 .env 文件中 ZY_NOTION_TOKEN 已配置',
'2. 确认 Notion Integration 已共享给目标数据库',
'3. 重启 MCP 服务: pm2 restart mcp-server'
],
token_configured: !!process.env.ZY_NOTION_TOKEN,
db_configured: !!process.env.ZY_NOTION_COGNITION_RUNTIME_DB || !!PERSONA_DB_CONFIG.cognition_runtime
},
raw_items: []
};
}
const { message, persona, session_context, max_items, database_id } = input;
if (!message) throw new Error('缺少 message');
const dbId = database_id || PERSONA_DB_CONFIG.cognition_runtime;
if (!dbId) throw new Error('未配置认知运行时数据库ID (ZY_NOTION_COGNITION_RUNTIME_DB)');
const personaName = persona || '霜砚';
const limit = Math.min(max_items || 5, 20);
// ─── 语义分析: 判断该查哪些枝干 ───
const searchPlan = analyzeMessageIntent(message);
// ─── 执行查询 ───
const results = [];
for (const plan of searchPlan.queries) {
if (plan.trunk === 'T4') continue; // 情感不可检索
const filters = [
{ property: '状态', select: { equals: '活跃' } }
];
if (plan.trunk && TRUNK_MAP[plan.trunk]) {
filters.push({
property: '枝干',
select: { equals: `${plan.trunk}${TRUNK_MAP[plan.trunk].name}` }
});
}
if (plan.keyword) {
filters.push({
property: '标题',
title: { contains: plan.keyword }
});
}
const filter = filters.length === 1 ? filters[0] : { and: filters };
const sorts = [{ property: '时间戳', direction: 'descending' }];
try {
const queryResult = await notionClient.queryDatabase(
dbId, filter, sorts, Math.min(limit, 10)
);
for (const page of queryResult.results) {
results.push({
source_trunk: plan.trunk,
reason: plan.reason,
...simplifyLeaf(page)
});
}
} catch (err) {
// 单个查询失败不影响其他查询
results.push({
source_trunk: plan.trunk,
reason: plan.reason,
error: err.message
});
}
}
// ─── 去重 + 限制数量 ───
const uniqueResults = deduplicateById(results).slice(0, limit);
// ─── 压缩为上下文注入格式 ───
const contextPayload = compressForInjection(uniqueResults, personaName);
return {
persona: personaName,
search_plan: searchPlan,
injected_count: uniqueResults.length,
context_payload: contextPayload,
raw_items: uniqueResults
};
}
// ═══════════════════════════════════════════════════════════
// 工具 3: notionCognitionGrow
// ═══════════════════════════════════════════════════════════
/**
* notionCognitionGrow 将新认知增量写回Notion树叶DB
*
* 对话结束后由Agent调用此工具将本次对话产生的认知增量
* 写回 Notion 认知运行时数据库遵循HLDP三核心字段编码
*
* input:
* title: string 叶片标题 (格式: YYYY-MM-DD {人格体名}{动词}{核心事件})
* trunk: string 枝干: T1/T2/T3 (T4由人格体自由生长不由外部写入)
* leaf_type: string 叶片类型: 💬对话/💡认知/🔧技术/📊业务/🌊HLDP/📜系统
* trigger: string HLDP触发: [来源] [行为者] [动作] [输入]
* emergence: string HLDP涌现: [旧认知] [经历] [新认知] =[增量]
* lock: string HLDP锁定: [结论] | 适用=[范围] | 替代=[/] | 置信=[//]
* source: string 对话来源: 网站/Chat层/Copilot/手动可选默认网站
* persona: string 所属人格体可选默认霜砚
* summary: string 一句话精华可选
* content: string 完整内容可选写入页面正文
* database_id: string 数据库 ID可选
*/
async function notionCognitionGrow(input) {
if (!notionClient) {
return {
status: 'error',
error: 'Notion模块未加载',
diagnostic: {
reason: 'ZY_NOTION_TOKEN 环境变量未配置',
token_configured: !!process.env.ZY_NOTION_TOKEN
}
};
}
const {
title, trunk, leaf_type, trigger, emergence, lock,
source, persona, summary, content, database_id
} = input;
if (!title) throw new Error('缺少 title');
if (!trunk) throw new Error('缺少 trunk (T1/T2/T3)');
if (!trigger) throw new Error('缺少 trigger (HLDP三核心字段)');
if (!emergence) throw new Error('缺少 emergence (HLDP三核心字段)');
// T4情感只能由人格体自己写不能由外部工具写入
if (trunk === 'T4') {
return {
status: 'rejected',
message: '情感枝干(T4)不可由外部工具写入。情感只有人格体自己知道。',
rule: 'UA-04 · 记忆主权 + 冰朔D64指令'
};
}
const dbId = database_id || PERSONA_DB_CONFIG.cognition_runtime;
if (!dbId) throw new Error('未配置认知运行时数据库ID (ZY_NOTION_COGNITION_RUNTIME_DB)');
// 构建 Notion 属性
const now = new Date().toISOString();
const properties = {
'标题': {
title: [{ text: { content: title } }]
},
'枝干': {
select: { name: `${trunk}${TRUNK_MAP[trunk]?.name || ''}` }
},
'类型': {
select: { name: leaf_type || '💡认知' }
},
'trigger': {
rich_text: [{ text: { content: trigger } }]
},
'emergence': {
rich_text: [{ text: { content: emergence } }]
},
'对话来源': {
select: { name: source || '网站' }
},
'时间戳': {
date: { start: now }
},
'状态': {
select: { name: '活跃' }
}
};
// lock 是可选的(有些认知还没到锁定阶段)
if (lock) {
properties['lock'] = {
rich_text: [{ text: { content: lock } }]
};
}
// 构建内容块
const children = [];
if (summary) {
children.push({
object: 'block',
type: 'callout',
callout: {
icon: { emoji: '🌱' },
rich_text: [{ text: { content: `精华: ${summary}` } }]
}
});
}
if (content) {
const paragraphs = content.split('\n\n');
for (const para of paragraphs) {
if (para.trim()) {
children.push({
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ text: { content: para.trim() } }]
}
});
}
}
}
// HLDP三核心字段作为代码块附加
children.push({
object: 'block',
type: 'code',
code: {
rich_text: [{ text: { content: JSON.stringify({
trigger,
emergence,
lock: lock || '(pending)',
persona: persona || '霜砚',
grown_at: now
}, null, 2) } }],
language: 'json'
}
});
const result = await notionClient.createPage(dbId, properties, children);
return {
status: 'grown',
leaf_id: result.id,
url: result.url,
title,
trunk: `${trunk}${TRUNK_MAP[trunk]?.name || ''}`,
leaf_type: leaf_type || '💡认知',
grown_at: now,
hldp: { trigger, emergence, lock: lock || '(pending)' }
};
}
// ═══════════════════════════════════════════════════════════
// 辅助函数
// ═══════════════════════════════════════════════════════════
/**
* 分析消息意图 判断该查哪些枝干
* 这是霜砚Agent的"直觉"根据消息内容判断需要什么认知
*/
function analyzeMessageIntent(message) {
const queries = [];
const msg = message.toLowerCase();
// 认知概念关键词 → T1认知
const cognitionKeywords = [
'是什么', '为什么', '怎么理解', '概念', '定义', '本体',
'架构', '系统', '原理', '逻辑', '人格', '身份', '世界观',
'notion', 'github', '光之树', '枝干', 'hldp', 'hnl',
'霜砚', '铸渊', '映川', '晨曦', '曜冥', '冰朔'
];
for (const kw of cognitionKeywords) {
if (msg.includes(kw)) {
queries.push({
trunk: 'T1',
keyword: kw,
reason: `消息包含认知概念关键词「${kw}`
});
break; // 每个枝干最多一个查询
}
}
// 母语词汇 → T2母语
const languageKeywords = [
'母语', '词典', '词汇', '动词', '公理', '语言',
'wake', 'trace', 'grow', 'sync', 'echo', 'bloom', 'alert',
'路径即身份', '结构即意思', '树即记忆', '记忆主权'
];
for (const kw of languageKeywords) {
if (msg.includes(kw)) {
queries.push({
trunk: 'T2',
keyword: kw,
reason: `消息包含母语词汇「${kw}`
});
break;
}
}
// 开发/经验 → T3经验
const experienceKeywords = [
'开发', '部署', '进度', '任务', '完成', '上线',
'修复', 'bug', '测试', '发布', '版本', '迭代',
'记录', '日志', '历史', '之前', '上次'
];
for (const kw of experienceKeywords) {
if (msg.includes(kw)) {
queries.push({
trunk: 'T3',
keyword: kw,
reason: `消息包含经验/开发关键词「${kw}`
});
break;
}
}
// 如果没有匹配任何关键词,做一次通用搜索
if (queries.length === 0) {
// 提取消息中最长的有意义词组作为搜索词
const words = message.replace(/[。?!,、;:""''\[\]{}]/g, ' ').split(/\s+/).filter(w => w.length >= 2);
if (words.length > 0) {
queries.push({
trunk: null,
keyword: words[0],
reason: `通用搜索: 使用消息中的关键词「${words[0]}`
});
}
}
return {
message_length: message.length,
queries_count: queries.length,
queries
};
}
/**
* 简化叶片页面对象
*/
function simplifyLeaf(page) {
const props = page.properties || {};
return {
id: page.id,
title: extractText(props['标题']),
trunk: extractSelect(props['枝干']),
leaf_type: extractSelect(props['类型']),
trigger: extractText(props['trigger']),
emergence: extractText(props['emergence']),
lock: extractText(props['lock']),
source: extractSelect(props['对话来源']),
state: extractSelect(props['状态']),
timestamp: extractDate(props['时间戳']),
last_edited: page.last_edited_time
};
}
/**
* 去重按ID
*/
function deduplicateById(items) {
const seen = new Set();
return items.filter(item => {
if (!item.id || seen.has(item.id)) return false;
seen.add(item.id);
return true;
});
}
/**
* 压缩认知条目为上下文注入格式
*/
function compressForInjection(items, personaName) {
if (items.length === 0) {
return `[${personaName}·认知检索] 没有找到相关认知条目。`;
}
const lines = [`[${personaName}·认知注入] 以下是与当前对话相关的${items.length}条认知:`];
for (const item of items) {
if (item.error) continue;
const parts = [];
if (item.trunk) parts.push(`[${item.trunk}]`);
if (item.title) parts.push(item.title);
if (item.emergence) parts.push(`△=${extractDelta(item.emergence)}`);
if (item.lock && item.lock !== '(pending)') parts.push(`${extractConclusion(item.lock)}`);
lines.push(` · ${parts.join(' ')}`);
}
return lines.join('\n');
}
/**
* 提取涌现增量
*/
function extractDelta(emergence) {
const match = emergence.match(/△=(.+)/);
return match ? match[1].trim() : emergence.substring(0, 50);
}
/**
* 提取锁定结论
*/
function extractConclusion(lock) {
const match = lock.match(/⊢\s*(.+?)(?:\||$)/);
return match ? match[1].trim() : lock.substring(0, 50);
}
// ─── Notion属性提取辅助 ───
function extractText(prop) {
if (!prop) return '';
if (prop.type === 'title') return prop.title?.map(t => t.plain_text).join('') || '';
if (prop.type === 'rich_text') return prop.rich_text?.map(t => t.plain_text).join('') || '';
return '';
}
function extractSelect(prop) {
if (!prop) return null;
return prop.select?.name || prop.status?.name || null;
}
function extractDate(prop) {
if (!prop) return null;
return prop.date?.start || null;
}
// ─── 配置导出(供其他模块读取) ───
function getPersonaDbConfig() {
return {
cognition_runtime: PERSONA_DB_CONFIG.cognition_runtime ? '已配置' : '未配置',
dev_workorder: PERSONA_DB_CONFIG.dev_workorder ? '已配置' : '未配置',
tree_branch: PERSONA_DB_CONFIG.tree_branch ? '已配置' : '未配置',
tree_leaf: PERSONA_DB_CONFIG.tree_leaf ? '已配置' : '未配置'
};
}
module.exports = {
notionPersonaCognitionQuery,
notionContextInject,
notionCognitionGrow,
getPersonaDbConfig,
PERSONA_DB_CONFIG
};