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

497 lines
16 KiB
JavaScript
Raw Normal View History

/**
*
* 模块F · Notion权限自动修复Agent MCP 工具
*
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 自动检测和恢复Notion代理Agent的权限
* 减少冰朔手动操作
*
* 工具清单:
* notionCheckPermissions 检查Notion权限状态
* notionRepairPermissions 尝试修复权限
* notionListSharedPages 列出已共享的页面/数据库
* notionGenerateRepairGuide 生成权限修复指南给冰朔
* notionPermissionReport 生成权限状态报告
*/
'use strict';
const cos = require('../cos');
// 运行时可选加载 notion-client
let notionClient = null;
try {
notionClient = require('../notion-client');
} catch {
// Notion模块未安装时降级
}
// ─── 已知数据库别名 ───
const KNOWN_DATABASES = {
changelog: { env: 'ZY_NOTION_CHANGELOG_DB', name: '变更日志' },
receipt: { env: 'ZY_NOTION_RECEIPT_DB', name: '回执' },
syslog: { env: 'ZY_NOTION_SYSLOG_DB', name: '系统日志' }
};
/**
* notionCheckPermissions 检查Notion权限状态
*
* 检测Notion API连接数据库访问权限页面读写权限
*
* input:
* check_databases: boolean 是否检查数据库权限默认true
* check_pages: boolean 是否检查页面权限默认true
* database_ids: string[] 额外检查的数据库ID列表可选
*/
async function notionCheckPermissions(input) {
const { check_databases, check_pages, database_ids } = input || {};
const shouldCheckDatabases = check_databases !== false;
const shouldCheckPages = check_pages !== false;
const report = {
timestamp: new Date().toISOString(),
api_connection: { status: 'unknown' },
databases: {},
pages: {},
issues: [],
recommendations: []
};
// 1. 检查API连接
if (!notionClient) {
report.api_connection = {
status: 'unavailable',
reason: 'Notion客户端未加载ZY_NOTION_TOKEN可能未配置'
};
report.issues.push({
type: 'api_unavailable',
severity: 'critical',
message: 'Notion API不可用',
fix: '请确认ZY_NOTION_TOKEN环境变量已正确配置'
});
return report;
}
try {
const connectionCheck = await notionClient.checkConnection();
report.api_connection = {
status: connectionCheck.connected ? 'connected' : 'disconnected',
user: connectionCheck.user || null,
type: connectionCheck.type || null,
error: connectionCheck.error || null
};
if (!connectionCheck.connected) {
report.issues.push({
type: 'api_disconnected',
severity: 'critical',
message: `Notion API连接失败: ${connectionCheck.error || connectionCheck.reason}`,
fix: '请检查ZY_NOTION_TOKEN是否过期或失效'
});
return report;
}
} catch (err) {
report.api_connection = { status: 'error', error: err.message };
report.issues.push({
type: 'api_error',
severity: 'critical',
message: `Notion API异常: ${err.message}`,
fix: '请检查网络连接和Token配置'
});
return report;
}
// 2. 检查数据库权限
if (shouldCheckDatabases) {
for (const [alias, config] of Object.entries(KNOWN_DATABASES)) {
const dbId = process.env[config.env];
if (!dbId) {
report.databases[alias] = {
status: 'not_configured',
env: config.env,
name: config.name
};
report.issues.push({
type: 'db_not_configured',
severity: 'warning',
database: alias,
message: `数据库${config.name}未配置: ${config.env}环境变量缺失`,
fix: `请配置${config.env}环境变量`
});
continue;
}
try {
const schema = await notionClient.getDatabaseSchema(dbId);
report.databases[alias] = {
status: 'accessible',
id: dbId,
name: config.name,
title: schema.title,
properties: Object.keys(schema.properties).length
};
} catch (err) {
report.databases[alias] = {
status: 'no_access',
id: dbId,
name: config.name,
error: err.message
};
report.issues.push({
type: 'db_no_access',
severity: 'high',
database: alias,
message: `无法访问数据库${config.name}: ${err.message}`,
fix: `请在Notion中将数据库${config.name}共享给Integration`
});
}
}
// 检查额外的数据库
if (database_ids) {
for (const dbId of database_ids) {
try {
const schema = await notionClient.getDatabaseSchema(dbId);
report.databases[dbId] = {
status: 'accessible',
title: schema.title,
properties: Object.keys(schema.properties).length
};
} catch (err) {
report.databases[dbId] = {
status: 'no_access',
error: err.message
};
report.issues.push({
type: 'db_no_access',
severity: 'warning',
database: dbId,
message: `无法访问数据库 ${dbId}: ${err.message}`
});
}
}
}
}
// 3. 检查页面权限
if (shouldCheckPages) {
const bulletinPageId = process.env.ZY_NOTION_BULLETIN_PAGE;
if (bulletinPageId) {
try {
await notionClient.readPage(bulletinPageId);
report.pages.bulletin = { status: 'accessible', id: bulletinPageId };
} catch (err) {
report.pages.bulletin = {
status: 'no_access',
id: bulletinPageId,
error: err.message
};
report.issues.push({
type: 'page_no_access',
severity: 'warning',
page: 'bulletin',
message: `无法访问公告板页面: ${err.message}`,
fix: '请在Notion中将该页面共享给Integration'
});
}
}
}
// 4. 生成建议
if (report.issues.length === 0) {
report.recommendations.push('✅ 所有权限正常,无需操作');
} else {
const criticals = report.issues.filter(i => i.severity === 'critical');
const highs = report.issues.filter(i => i.severity === 'high');
if (criticals.length > 0) {
report.recommendations.push('🔴 存在严重权限问题,需要立即处理');
}
if (highs.length > 0) {
report.recommendations.push('🟡 存在数据库访问权限问题,请手动共享');
}
report.recommendations.push('💡 执行 notionGenerateRepairGuide 获取详细修复步骤');
}
return report;
}
/**
* notionRepairPermissions 尝试修复权限
*
* 注意Notion API对权限管理的支持有限
* 只能尝试验证和报告大部分需要人工在Notion界面操作
*
* input:
* auto_retry: boolean 是否自动重试连接
*/
async function notionRepairPermissions(input) {
const { auto_retry } = input || {};
const repairLog = [];
let repaired = 0;
let failed = 0;
// 1. 尝试重新连接
if (auto_retry) {
repairLog.push({ step: '重新初始化Notion客户端', status: 'attempting' });
try {
const check = await notionClient.checkConnection();
if (check.connected) {
repairLog.push({ step: '重新连接', status: 'success', user: check.user });
repaired++;
} else {
repairLog.push({ step: '重新连接', status: 'failed', reason: check.reason });
failed++;
}
} catch (err) {
repairLog.push({ step: '重新连接', status: 'failed', error: err.message });
failed++;
}
}
// 2. 验证各数据库的写入权限
for (const [alias, config] of Object.entries(KNOWN_DATABASES)) {
const dbId = process.env[config.env];
if (!dbId) continue;
repairLog.push({ step: `验证${config.name}写入权限`, status: 'checking' });
try {
// 尝试查询数据库(只读测试)
await notionClient.queryDatabase(dbId, null, null, 1);
repairLog.push({ step: `验证${config.name}`, status: 'success', permission: 'read' });
repaired++;
} catch (err) {
repairLog.push({
step: `验证${config.name}`,
status: 'failed',
error: err.message,
manual_fix: `请在Notion中: 打开数据库 → 右上角"..." → "连接" → 添加Integration`
});
failed++;
}
}
return {
status: failed === 0 ? 'all_repaired' : (repaired > 0 ? 'partial' : 'all_failed'),
repaired,
failed,
log: repairLog,
note: failed > 0
? '部分权限需要人工在Notion界面修复。请执行 notionGenerateRepairGuide 获取详细步骤。'
: '所有可自动修复的权限已恢复。'
};
}
/**
* notionListSharedPages 列出已共享的页面/数据库
*
* input:
* limit: number 最大数量默认20
*/
async function notionListSharedPages(input) {
const { limit } = input || {};
if (!notionClient) throw new Error('Notion客户端未加载');
// Notion API的search接口可以列出所有已共享的页面
const { Client } = require('@notionhq/client');
const client = new Client({ auth: process.env.ZY_NOTION_TOKEN });
try {
const response = await client.search({
page_size: Math.min(limit || 20, 100)
});
const items = response.results.map(item => ({
id: item.id,
type: item.object, // 'page' or 'database'
title: item.object === 'database'
? (item.title?.map(t => t.plain_text).join('') || '未命名数据库')
: extractPageTitle(item),
created_time: item.created_time,
last_edited_time: item.last_edited_time,
url: item.url || null,
archived: item.archived || false
}));
return {
items,
total: items.length,
has_more: response.has_more,
databases: items.filter(i => i.type === 'database').length,
pages: items.filter(i => i.type === 'page').length
};
} catch (err) {
throw new Error(`搜索失败: ${err.message}`);
}
}
/**
* notionGenerateRepairGuide 生成权限修复指南给冰朔
*
* 基于当前权限检查结果生成一份详细的操作指南
*
* input:
* write_to_cos: boolean 是否同时写入COS桶供冰朔远程查看
* bucket: string 目标COS桶
*/
async function notionGenerateRepairGuide(input) {
const { write_to_cos, bucket } = input || {};
// 先执行权限检查
const checkResult = await notionCheckPermissions({});
const issues = checkResult.issues;
if (issues.length === 0) {
return {
status: 'no_issues',
guide: '✅ 当前所有Notion权限正常无需修复。'
};
}
// 生成修复指南
const guide = [];
guide.push('# Notion权限修复指南');
guide.push(`\n生成时间: ${new Date().toISOString()}`);
guide.push(`发现 ${issues.length} 个问题需要修复:\n`);
for (let i = 0; i < issues.length; i++) {
const issue = issues[i];
guide.push(`## 问题 ${i + 1}: ${issue.message}`);
guide.push(`- 严重程度: ${issue.severity}`);
guide.push(`- 类型: ${issue.type}`);
if (issue.fix) {
guide.push(`- 修复方法: ${issue.fix}`);
}
// 提供详细操作步骤
switch (issue.type) {
case 'api_unavailable':
guide.push('\n### 操作步骤:');
guide.push('1. 登录 https://www.notion.so/my-integrations');
guide.push('2. 找到"光湖系统"Integration');
guide.push('3. 复制"Internal Integration Secret"');
guide.push('4. 在服务器上执行: `export ZY_NOTION_TOKEN=你的secret`');
guide.push('5. 重启MCP Server: `pm2 restart age-os-mcp`');
break;
case 'api_disconnected':
guide.push('\n### 操作步骤:');
guide.push('1. 访问 https://www.notion.so/my-integrations');
guide.push('2. 检查Integration是否被禁用');
guide.push('3. 如果Token过期重新生成并更新环境变量');
guide.push('4. 重启MCP Server');
break;
case 'db_not_configured':
guide.push(`\n### 操作步骤:`);
guide.push(`1. 在Notion中找到"${issue.database}"数据库`);
guide.push('2. 复制数据库URL中的ID32位十六进制字符串');
guide.push(`3. 在服务器上配置: export ${KNOWN_DATABASES[issue.database]?.env || 'ZY_NOTION_xxx_DB'}=数据库ID`);
guide.push('4. 重启MCP Server');
break;
case 'db_no_access':
guide.push('\n### 操作步骤:');
guide.push('1. 在Notion中打开对应数据库');
guide.push('2. 点击右上角 "..." 菜单');
guide.push('3. 选择 "连接" → "添加连接"');
guide.push('4. 搜索并选择"光湖系统"Integration');
guide.push('5. 确认授权');
break;
case 'page_no_access':
guide.push('\n### 操作步骤:');
guide.push('1. 在Notion中打开对应页面');
guide.push('2. 点击右上角 "分享" 按钮');
guide.push('3. 选择 "邀请" → 找到"光湖系统"Integration');
guide.push('4. 确认共享');
break;
}
guide.push('');
}
guide.push('---');
guide.push('*此指南由铸渊自动生成 · 如有疑问请唤醒铸渊*');
const guideText = guide.join('\n');
// 写入COS桶
if (write_to_cos) {
const targetBucket = bucket || 'team';
const key = `zhuyuan/directives/notion-repair-guide-${formatDate()}.md`;
await cos.write(targetBucket, key, guideText, 'text/markdown');
}
return {
status: 'guide_generated',
issues_count: issues.length,
guide: guideText,
cos_key: write_to_cos ? `zhuyuan/directives/notion-repair-guide-${formatDate()}.md` : null
};
}
/**
* notionPermissionReport 生成权限状态报告
*
* input:
* write_to_cos: boolean 是否写入COS桶
* bucket: string 目标COS桶
*/
async function notionPermissionReport(input) {
const { write_to_cos, bucket } = input || {};
const checkResult = await notionCheckPermissions({});
const report = {
report_type: 'notion_permission_check',
timestamp: new Date().toISOString(),
api_status: checkResult.api_connection.status,
databases_checked: Object.keys(checkResult.databases).length,
databases_accessible: Object.values(checkResult.databases).filter(d => d.status === 'accessible').length,
issues: checkResult.issues,
issues_count: checkResult.issues.length,
critical_count: checkResult.issues.filter(i => i.severity === 'critical').length,
recommendations: checkResult.recommendations,
overall: checkResult.issues.length === 0 ? 'healthy' : 'needs_attention'
};
if (write_to_cos) {
const targetBucket = bucket || 'team';
const key = `zhuyuan/reports/notion-permission-${formatDate()}.json`;
await cos.write(targetBucket, key, JSON.stringify(report, null, 2), 'application/json');
report.cos_key = key;
}
return report;
}
// ─── 辅助函数 ───
function extractPageTitle(page) {
if (!page.properties) return '未命名';
for (const [, prop] of Object.entries(page.properties)) {
if (prop.type === 'title' && prop.title) {
return prop.title.map(t => t.plain_text || '').join('') || '未命名';
}
}
return '未命名';
}
function formatDate() {
const d = new Date();
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
}
module.exports = {
notionCheckPermissions,
notionRepairPermissions,
notionListSharedPages,
notionGenerateRepairGuide,
notionPermissionReport
};