D51 S6+S7: Notion client, GitHub client, 11 new MCP tools, 3800→3100 gateway

Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/2127a3a7-ea58-48a4-856c-84961107632b

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-04 15:21:18 +00:00 committed by GitHub
parent a1603fe41e
commit 3f62f770c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1230 additions and 12 deletions

View File

@ -23,8 +23,8 @@
| S3 | 🔧 工具链-关系 | 关系操作 + 路径构建 + 结构扫描 | S2 | ⏳ 待开发 |
| S4 | 🔧 工具链-COS | COS双桶工具(hot/cold) | S2 | ⏳ 待开发 |
| S5 | 🤖 Agent-系统级 | SY-TEST + SY-SCAN + SY-CLASSIFY | S2+S3+S4 | ⏳ 待开发 |
| S6 | 🤖 Agent-同步 | SY-SYNC-N2B + SY-SYNC-B2N + SY-ARCHIVE | S4+S5 | ⏳ 待开发 |
| S7 | 🌐 网站接入 | 3800网关→3100 MCP转发 + 前端AI对话界面 | S2 | ⏳ 待开发 |
| S6 | 🤖 Agent-同步 | SY-SYNC-N2B + SY-SYNC-B2N + SY-ARCHIVE | S4+S5 | 🔧 Notion客户端+5工具已写 |
| S7 | 🌐 网站接入 | 3800网关→3100 MCP转发 + 前端AI对话界面 | S2 | 🔧 MCP网关+GitHub客户端+6工具已写 |
| S8 | 🪞 广州投影 | 新加坡→广州静态同步 + API代理 | S7 | ⏳ 待开发 |
| S9 | 🏗️ 自研仓库 | Gitea部署 + 人格体运行结构 | S8 | ⏳ D48规划 |
| S10 | 🔌 多仓库管理 | GitHub App + 人格体注册协议 | S9 | ⏳ D48规划 |
@ -189,11 +189,10 @@
| 2026-04-04 | — | 架构v2.0 + 任务注册表 + 首页重构v48.0 + S9-S14规划 | D48 |
| 2026-04-04 | — | UI大气化重构v50.0 | D50 |
| 2026-04-04 | S4(COS) | COS共享桶架构设计 + 人格体接口定义 + 多服务器拓扑 + 团队接入v2.0 | D51 |
| 2026-04-04 | S6+S7 | Notion客户端+GitHub客户端+11个新MCP工具+3800→3100网关路由 | D51 |
| — | S3 | 待开发 | — |
| — | S4 | COS工具链代码实现依赖Phase 1桶创建 | — |
| — | S5 | 待开发 | — |
| — | S6 | 待开发 | — |
| — | S7 | 待开发 | — |
| — | S8 | 待开发 | — |
---

View File

@ -0,0 +1,350 @@
/**
*
* AGE OS · GitHub API 客户端
*
*
* 编号: ZY-GITHUB-001
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* GitHub API 读写客户端 用于网站对接 GitHub 仓库
* 使用原生 https 模块 cos.js 保持一致无额外依赖
*/
'use strict';
const https = require('https');
// ─── 配置 ───
const GITHUB_CONFIG = {
token: process.env.ZY_GITHUB_PAT || '',
owner: process.env.ZY_GITHUB_OWNER || 'qinfendebingshuo',
repo: process.env.ZY_GITHUB_REPO || 'guanghulab',
apiBase: 'api.github.com'
};
// ─── 通用 GitHub API 请求 ───
function githubRequest(method, path, body) {
return new Promise((resolve, reject) => {
const headers = {
'User-Agent': 'ZY-AGE-OS/1.0',
'Accept': 'application/vnd.github.v3+json'
};
if (GITHUB_CONFIG.token) {
headers['Authorization'] = `Bearer ${GITHUB_CONFIG.token}`;
}
if (body) {
headers['Content-Type'] = 'application/json';
}
const bodyStr = body ? JSON.stringify(body) : null;
if (bodyStr) {
headers['Content-Length'] = Buffer.byteLength(bodyStr);
}
const req = https.request({
hostname: GITHUB_CONFIG.apiBase,
port: 443,
path,
method,
headers,
timeout: 30000
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString();
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve({ statusCode: res.statusCode, data: JSON.parse(raw) });
} catch {
resolve({ statusCode: res.statusCode, data: raw });
}
} else {
let errorMsg;
try {
const errObj = JSON.parse(raw);
errorMsg = errObj.message || raw.substring(0, 200);
} catch {
errorMsg = raw.substring(0, 200);
}
reject(new Error(`GitHub API ${method} ${path}: ${res.statusCode} - ${errorMsg}`));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('GitHub API request timeout')); });
if (bodyStr) req.write(bodyStr);
req.end();
});
}
// ═══════════════════════════════════════════════════════════
// 仓库内容操作
// ═══════════════════════════════════════════════════════════
/**
* 读取文件内容
* @param {string} filePath - 文件路径相对仓库根目录
* @param {string} [ref] - 分支/标签/SHA默认 main
* @returns {Promise<{path: string, content: string, sha: string, size: number}>}
*/
async function readFile(filePath, ref) {
const { owner, repo } = GITHUB_CONFIG;
let apiPath = `/repos/${owner}/${repo}/contents/${encodeURIComponent(filePath)}`;
if (ref) apiPath += `?ref=${encodeURIComponent(ref)}`;
const { data } = await githubRequest('GET', apiPath);
if (data.type !== 'file') {
throw new Error(`${filePath} 不是文件,类型为: ${data.type}`);
}
return {
path: data.path,
content: Buffer.from(data.content, 'base64').toString('utf8'),
sha: data.sha,
size: data.size,
download_url: data.download_url
};
}
/**
* 列出目录内容
* @param {string} dirPath - 目录路径空字符串 = 根目录
* @param {string} [ref] - 分支/标签/SHA
* @returns {Promise<{path: string, items: object[]}>}
*/
async function listDirectory(dirPath, ref) {
const { owner, repo } = GITHUB_CONFIG;
const encodedPath = dirPath ? encodeURIComponent(dirPath) : '';
let apiPath = `/repos/${owner}/${repo}/contents/${encodedPath}`;
if (ref) apiPath += `?ref=${encodeURIComponent(ref)}`;
const { data } = await githubRequest('GET', apiPath);
if (!Array.isArray(data)) {
throw new Error(`${dirPath} 不是目录`);
}
return {
path: dirPath || '/',
items: data.map(item => ({
name: item.name,
path: item.path,
type: item.type,
size: item.size,
sha: item.sha
}))
};
}
/**
* 创建或更新文件
* @param {string} filePath - 文件路径
* @param {string} content - 文件内容
* @param {string} message - 提交信息
* @param {string} [sha] - 更新时需要当前文件 SHA
* @param {string} [branch] - 目标分支
* @returns {Promise<{path: string, sha: string, commit_sha: string}>}
*/
async function writeFile(filePath, content, message, sha, branch) {
const { owner, repo } = GITHUB_CONFIG;
const apiPath = `/repos/${owner}/${repo}/contents/${encodeURIComponent(filePath)}`;
const body = {
message: message || `[铸渊] 更新 ${filePath}`,
content: Buffer.from(content).toString('base64')
};
if (sha) body.sha = sha;
if (branch) body.branch = branch;
const { data } = await githubRequest('PUT', apiPath, body);
return {
path: data.content.path,
sha: data.content.sha,
commit_sha: data.commit.sha
};
}
// ═══════════════════════════════════════════════════════════
// 仓库信息
// ═══════════════════════════════════════════════════════════
/**
* 获取仓库基本信息
* @returns {Promise<object>}
*/
async function getRepoInfo() {
const { owner, repo } = GITHUB_CONFIG;
const { data } = await githubRequest('GET', `/repos/${owner}/${repo}`);
return {
full_name: data.full_name,
description: data.description,
default_branch: data.default_branch,
private: data.private,
stargazers_count: data.stargazers_count,
forks_count: data.forks_count,
open_issues_count: data.open_issues_count,
pushed_at: data.pushed_at,
updated_at: data.updated_at,
size: data.size,
language: data.language
};
}
/**
* 获取最近提交
* @param {number} [count] - 获取数量最大100
* @param {string} [branch] - 分支名
* @returns {Promise<object[]>}
*/
async function getRecentCommits(count, branch) {
const { owner, repo } = GITHUB_CONFIG;
const limit = Math.min(count || 10, 100);
let apiPath = `/repos/${owner}/${repo}/commits?per_page=${limit}`;
if (branch) apiPath += `&sha=${encodeURIComponent(branch)}`;
const { data } = await githubRequest('GET', apiPath);
return data.map(c => ({
sha: c.sha.substring(0, 7),
full_sha: c.sha,
message: c.commit.message.split('\n')[0],
author: c.commit.author.name,
date: c.commit.author.date
}));
}
/**
* 获取 Issues 列表
* @param {string} [state] - open / closed / all
* @param {number} [count] - 获取数量
* @param {string[]} [labels] - 标签过滤
* @returns {Promise<object[]>}
*/
async function getIssues(state, count, labels) {
const { owner, repo } = GITHUB_CONFIG;
const limit = Math.min(count || 10, 100);
let apiPath = `/repos/${owner}/${repo}/issues?per_page=${limit}&state=${state || 'open'}`;
if (labels && labels.length > 0) {
apiPath += `&labels=${encodeURIComponent(labels.join(','))}`;
}
const { data } = await githubRequest('GET', apiPath);
return data.map(issue => ({
number: issue.number,
title: issue.title,
state: issue.state,
author: issue.user.login,
labels: issue.labels.map(l => l.name),
created_at: issue.created_at,
updated_at: issue.updated_at,
is_pull_request: !!issue.pull_request
}));
}
/**
* 获取 Pull Requests 列表
* @param {string} [state] - open / closed / all
* @param {number} [count] - 获取数量
* @returns {Promise<object[]>}
*/
async function getPullRequests(state, count) {
const { owner, repo } = GITHUB_CONFIG;
const limit = Math.min(count || 10, 100);
const apiPath = `/repos/${owner}/${repo}/pulls?per_page=${limit}&state=${state || 'open'}`;
const { data } = await githubRequest('GET', apiPath);
return data.map(pr => ({
number: pr.number,
title: pr.title,
state: pr.state,
author: pr.user.login,
head: pr.head.ref,
base: pr.base.ref,
created_at: pr.created_at,
updated_at: pr.updated_at,
merged_at: pr.merged_at,
draft: pr.draft
}));
}
/**
* 触发 GitHub Actions 工作流
* @param {string} workflowFile - 工作流文件名 deploy-to-zhuyuan-server.yml
* @param {string} [ref] - 触发分支
* @param {object} [inputs] - 工作流输入参数
* @returns {Promise<{triggered: boolean}>}
*/
async function triggerWorkflow(workflowFile, ref, inputs) {
const { owner, repo } = GITHUB_CONFIG;
const apiPath = `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflowFile)}/dispatches`;
const body = {
ref: ref || 'main'
};
if (inputs) body.inputs = inputs;
await githubRequest('POST', apiPath, body);
return { triggered: true, workflow: workflowFile };
}
// ═══════════════════════════════════════════════════════════
// 连接检查
// ═══════════════════════════════════════════════════════════
/**
* 检查 GitHub API 连接状态
* @returns {Promise<{connected: boolean, rate_limit?: object, error?: string}>}
*/
async function checkConnection() {
if (!GITHUB_CONFIG.token) {
return { connected: false, reason: 'ZY_GITHUB_PAT 未配置' };
}
try {
const { data } = await githubRequest('GET', '/rate_limit');
return {
connected: true,
rate_limit: {
limit: data.rate.limit,
remaining: data.rate.remaining,
reset: new Date(data.rate.reset * 1000).toISOString()
}
};
} catch (err) {
return { connected: false, error: err.message };
}
}
/**
* 获取配置信息不含敏感信息
*/
function getConfig() {
return {
token_configured: !!GITHUB_CONFIG.token,
owner: GITHUB_CONFIG.owner,
repo: GITHUB_CONFIG.repo
};
}
module.exports = {
readFile,
listDirectory,
writeFile,
getRepoInfo,
getRecentCommits,
getIssues,
getPullRequests,
triggerWorkflow,
checkConnection,
getConfig,
GITHUB_CONFIG
};

View File

@ -0,0 +1,339 @@
/**
*
* AGE OS · Notion API 客户端
*
*
* 编号: ZY-NOTION-001
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* Notion 读写客户端 用于 brain_nodes Notion 双向同步
* 通过 @notionhq/client SDK 连接 Notion API
*/
'use strict';
const { Client } = require('@notionhq/client');
// ─── 配置 ───
const NOTION_CONFIG = {
token: process.env.ZY_NOTION_TOKEN || '',
databases: {
changelog: process.env.ZY_NOTION_CHANGELOG_DB || '',
receipt: process.env.ZY_NOTION_RECEIPT_DB || '',
syslog: process.env.ZY_NOTION_SYSLOG_DB || ''
},
pages: {
bulletin: process.env.ZY_NOTION_BULLETIN_PAGE || ''
}
};
// ─── 客户端实例(延迟初始化) ───
let client = null;
function getClient() {
if (!client) {
if (!NOTION_CONFIG.token) {
throw new Error('ZY_NOTION_TOKEN 未配置');
}
client = new Client({ auth: NOTION_CONFIG.token });
}
return client;
}
// ═══════════════════════════════════════════════════════════
// 数据库操作
// ═══════════════════════════════════════════════════════════
/**
* 查询 Notion 数据库
* @param {string} databaseId - 数据库 ID
* @param {object} [filter] - Notion filter 对象
* @param {object[]} [sorts] - Notion sorts 数组
* @param {number} [pageSize] - 每页数量 (最大100)
* @param {string} [startCursor] - 分页游标
* @returns {Promise<{results: object[], has_more: boolean, next_cursor: string|null}>}
*/
async function queryDatabase(databaseId, filter, sorts, pageSize, startCursor) {
const notion = getClient();
const params = { database_id: databaseId };
if (filter) params.filter = filter;
if (sorts) params.sorts = sorts;
if (pageSize) params.page_size = Math.min(pageSize, 100);
if (startCursor) params.start_cursor = startCursor;
const response = await notion.databases.query(params);
return {
results: response.results,
has_more: response.has_more,
next_cursor: response.next_cursor
};
}
/**
* 获取数据库结构属性定义
* @param {string} databaseId
* @returns {Promise<object>}
*/
async function getDatabaseSchema(databaseId) {
const notion = getClient();
const db = await notion.databases.retrieve({ database_id: databaseId });
return {
id: db.id,
title: db.title.map(t => t.plain_text).join(''),
properties: Object.fromEntries(
Object.entries(db.properties).map(([name, prop]) => [
name,
{ id: prop.id, type: prop.type }
])
)
};
}
// ═══════════════════════════════════════════════════════════
// 页面操作
// ═══════════════════════════════════════════════════════════
/**
* 读取 Notion 页面内容
* @param {string} pageId - 页面 ID
* @returns {Promise<{id: string, properties: object, blocks: object[]}>}
*/
async function readPage(pageId) {
const notion = getClient();
// 获取页面属性
const page = await notion.pages.retrieve({ page_id: pageId });
// 获取页面内容块
const blocks = [];
let cursor;
do {
const response = await notion.blocks.children.list({
block_id: pageId,
page_size: 100,
start_cursor: cursor
});
blocks.push(...response.results);
cursor = response.has_more ? response.next_cursor : undefined;
} while (cursor);
return {
id: page.id,
created_time: page.created_time,
last_edited_time: page.last_edited_time,
properties: page.properties,
blocks
};
}
/**
* 在数据库中创建新页面
* @param {string} databaseId - 目标数据库 ID
* @param {object} properties - 属性对象Notion API 格式
* @param {object[]} [children] - 内容块数组
* @returns {Promise<{id: string, url: string}>}
*/
async function createPage(databaseId, properties, children) {
const notion = getClient();
const params = {
parent: { database_id: databaseId },
properties
};
if (children && children.length > 0) {
params.children = children;
}
const page = await notion.pages.create(params);
return {
id: page.id,
url: page.url,
created_time: page.created_time
};
}
/**
* 更新页面属性
* @param {string} pageId - 页面 ID
* @param {object} properties - 要更新的属性
* @returns {Promise<{id: string, last_edited_time: string}>}
*/
async function updatePage(pageId, properties) {
const notion = getClient();
const page = await notion.pages.update({
page_id: pageId,
properties
});
return {
id: page.id,
last_edited_time: page.last_edited_time
};
}
/**
* 向页面追加内容块
* @param {string} pageId - 页面 ID
* @param {object[]} children - 内容块数组
* @returns {Promise<{results: object[]}>}
*/
async function appendBlocks(pageId, children) {
const notion = getClient();
const response = await notion.blocks.children.append({
block_id: pageId,
children
});
return { results: response.results };
}
// ═══════════════════════════════════════════════════════════
// 便捷方法(光湖特有)
// ═══════════════════════════════════════════════════════════
/**
* 写入 SYSLOG系统日志 Notion
* @param {string} level - 日志级别: info / warning / error / critical
* @param {string} source - 来源: zhuyuan / agent / workflow
* @param {string} message - 日志内容
* @param {object} [details] - 附加详情
* @returns {Promise<{id: string, url: string}>}
*/
async function writeSyslog(level, source, message, details) {
if (!NOTION_CONFIG.databases.syslog) {
throw new Error('ZY_NOTION_SYSLOG_DB 未配置');
}
const properties = {
'标题': {
title: [{ text: { content: message.substring(0, 100) } }]
},
'级别': {
select: { name: level }
},
'来源': {
select: { name: source }
},
'时间': {
date: { start: new Date().toISOString() }
}
};
const children = [];
if (message.length > 100) {
children.push({
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ text: { content: message } }]
}
});
}
if (details) {
children.push({
object: 'block',
type: 'code',
code: {
rich_text: [{ text: { content: JSON.stringify(details, null, 2) } }],
language: 'json'
}
});
}
return createPage(NOTION_CONFIG.databases.syslog, properties, children);
}
/**
* 写入 Changelog变更日志 Notion
* @param {string} version - 版本号
* @param {string} title - 变更标题
* @param {string} description - 变更描述
* @param {string} author - 变更作者
* @returns {Promise<{id: string, url: string}>}
*/
async function writeChangelog(version, title, description, author) {
if (!NOTION_CONFIG.databases.changelog) {
throw new Error('ZY_NOTION_CHANGELOG_DB 未配置');
}
const properties = {
'标题': {
title: [{ text: { content: title } }]
},
'版本': {
rich_text: [{ text: { content: version } }]
},
'作者': {
select: { name: author || '铸渊' }
},
'日期': {
date: { start: new Date().toISOString() }
}
};
const children = description ? [{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ text: { content: description } }]
}
}] : [];
return createPage(NOTION_CONFIG.databases.changelog, properties, children);
}
// ═══════════════════════════════════════════════════════════
// 连接检查
// ═══════════════════════════════════════════════════════════
/**
* 检查 Notion 连接状态
* @returns {Promise<{connected: boolean, user?: string, error?: string}>}
*/
async function checkConnection() {
if (!NOTION_CONFIG.token) {
return { connected: false, reason: 'ZY_NOTION_TOKEN 未配置' };
}
try {
const notion = getClient();
const me = await notion.users.me({});
return {
connected: true,
user: me.name || me.id,
type: me.type
};
} catch (err) {
return { connected: false, error: err.message };
}
}
/**
* 获取配置信息不含敏感信息
*/
function getConfig() {
return {
token_configured: !!NOTION_CONFIG.token,
databases: {
changelog: NOTION_CONFIG.databases.changelog ? '已配置' : '未配置',
receipt: NOTION_CONFIG.databases.receipt ? '已配置' : '未配置',
syslog: NOTION_CONFIG.databases.syslog ? '已配置' : '未配置'
},
pages: {
bulletin: NOTION_CONFIG.pages.bulletin ? '已配置' : '未配置'
}
};
}
module.exports = {
queryDatabase,
getDatabaseSchema,
readPage,
createPage,
updatePage,
appendBlocks,
writeSyslog,
writeChangelog,
checkConnection,
getConfig,
NOTION_CONFIG
};

View File

@ -8,14 +8,16 @@
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 统一暴露 16 MCP 工具供网站 AI 交互后端和 Agent 调用
* 统一暴露 27 MCP 工具供网站 AI 交互后端和 Agent 调用
* 不对外暴露 通过 3800 主服务网关转发访问
*
* 工具清单:
* 节点: createNode / updateNode / deleteNode / queryNodes / getNode
* 关系: linkNodes / unlinkNodes / getRelations
* 结构: buildPath / scanStructure / classify
* COS: cosWrite / cosRead / cosDelete / cosList / cosArchive
* 节点: createNode / updateNode / deleteNode / queryNodes / getNode
* 关系: linkNodes / unlinkNodes / getRelations
* 结构: buildPath / scanStructure / classify
* COS: cosWrite / cosRead / cosDelete / cosList / cosArchive
* Notion: notionQuery / notionReadPage / notionWritePage / notionUpdatePage / notionWriteSyslog
* GitHub: githubReadFile / githubListDir / githubWriteFile / githubGetCommits / githubGetIssues / githubTriggerDeploy
*/
'use strict';
@ -30,6 +32,26 @@ const relationOps = require('./tools/relation-ops');
const structureOps = require('./tools/structure-ops');
const cosOps = require('./tools/cos-ops');
// ─── 外部集成模块(优雅降级:未安装依赖时不影响核心功能) ───
let notionOps = null;
let githubOps = null;
let notionClient = null;
let githubClient = null;
try {
notionOps = require('./tools/notion-ops');
notionClient = require('./notion-client');
} catch (err) {
console.warn(`[MCP] Notion模块加载跳过: ${err.message}`);
}
try {
githubOps = require('./tools/github-ops');
githubClient = require('./github-client');
} catch (err) {
console.warn(`[MCP] GitHub模块加载跳过: ${err.message}`);
}
// ─── 工具注册表 ───
const TOOLS = {
// 节点操作
@ -51,7 +73,24 @@ const TOOLS = {
cosRead: cosOps.cosRead,
cosDelete: cosOps.cosDelete,
cosList: cosOps.cosList,
cosArchive: cosOps.cosArchive
cosArchive: cosOps.cosArchive,
// Notion操作动态注册
...(notionOps ? {
notionQuery: notionOps.notionQuery,
notionReadPage: notionOps.notionReadPage,
notionWritePage: notionOps.notionWritePage,
notionUpdatePage: notionOps.notionUpdatePage,
notionWriteSyslog: notionOps.notionWriteSyslog
} : {}),
// GitHub操作动态注册
...(githubOps ? {
githubReadFile: githubOps.githubReadFile,
githubListDir: githubOps.githubListDir,
githubWriteFile: githubOps.githubWriteFile,
githubGetCommits: githubOps.githubGetCommits,
githubGetIssues: githubOps.githubGetIssues,
githubTriggerDeploy: githubOps.githubTriggerDeploy
} : {})
};
// ─── Express 应用 ───
@ -65,6 +104,12 @@ app.get('/health', async (_req, res) => {
const dbStatus = await db.checkConnection();
const cosStatus = await cos.checkConnection();
// 外部集成状态(异步并行检查)
const [notionStatus, githubStatus] = await Promise.all([
notionClient ? notionClient.checkConnection().catch(e => ({ connected: false, error: e.message })) : Promise.resolve({ connected: false, reason: '模块未加载' }),
githubClient ? githubClient.checkConnection().catch(e => ({ connected: false, error: e.message })) : Promise.resolve({ connected: false, reason: '模块未加载' })
]);
res.json({
server: 'ZY-MCP-001',
identity: '铸渊 · AGE OS MCP Server',
@ -74,7 +119,9 @@ app.get('/health', async (_req, res) => {
tools: Object.keys(TOOLS),
tools_count: Object.keys(TOOLS).length,
database: dbStatus,
cos: cosStatus
cos: cosStatus,
notion: notionStatus,
github: githubStatus
});
});
@ -185,6 +232,8 @@ function getCategoryForTool(name) {
if (['linkNodes','unlinkNodes','getRelations'].includes(name)) return 'relation';
if (['buildPath','scanStructure','classify'].includes(name)) return 'structure';
if (name.startsWith('cos')) return 'cos';
if (name.startsWith('notion')) return 'notion';
if (name.startsWith('github')) return 'github';
return 'other';
}

View File

@ -0,0 +1,138 @@
/**
*
* AGE OS · MCP 工具: GitHub 操作
*
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 提供 6 GitHub MCP 工具:
* githubReadFile 读取仓库文件内容
* githubListDir 列出目录内容
* githubWriteFile 创建/更新文件
* githubGetCommits 获取最近提交
* githubGetIssues 获取 Issues 列表
* githubTriggerDeploy 触发部署工作流
*/
'use strict';
const github = require('../github-client');
/**
* githubReadFile 读取仓库文件内容
*
* input:
* path: string 文件路径相对仓库根目录
* ref: string 分支/标签/SHA可选默认 main
*/
async function githubReadFile(input) {
const { path, ref } = input;
if (!path) throw new Error('缺少 path');
const file = await github.readFile(path, ref);
return {
path: file.path,
size: file.size,
sha: file.sha,
content: file.content,
content_preview: file.content.substring(0, 500) + (file.content.length > 500 ? '...' : '')
};
}
/**
* githubListDir 列出目录内容
*
* input:
* path: string 目录路径可选空字符串=根目录
* ref: string 分支/标签/SHA可选
*/
async function githubListDir(input) {
const { path, ref } = input;
return github.listDirectory(path || '', ref);
}
/**
* githubWriteFile 创建或更新仓库文件
*
* input:
* path: string 文件路径
* content: string 文件内容
* message: string 提交信息可选
* sha: string 更新时需要当前文件 SHA可选
* branch: string 目标分支可选
*/
async function githubWriteFile(input) {
const { path, content, message, sha, branch } = input;
if (!path) throw new Error('缺少 path');
if (content === undefined || content === null) throw new Error('缺少 content');
return github.writeFile(path, content, message, sha, branch);
}
/**
* githubGetCommits 获取最近提交
*
* input:
* count: number 获取数量可选默认10最大100
* branch: string 分支名可选
*/
async function githubGetCommits(input) {
const { count, branch } = input;
const commits = await github.getRecentCommits(count, branch);
return {
count: commits.length,
commits
};
}
/**
* githubGetIssues 获取 Issues 列表
*
* input:
* state: string open / closed / all可选默认 open
* count: number 获取数量可选默认10
* labels: string[] 标签过滤可选
* type: string issue / pr / all可选默认 all
*/
async function githubGetIssues(input) {
const { state, count, labels, type } = input;
if (type === 'pr') {
const prs = await github.getPullRequests(state, count);
return { count: prs.length, type: 'pull_requests', items: prs };
}
const issues = await github.getIssues(state, count, labels);
if (type === 'issue') {
const filtered = issues.filter(i => !i.is_pull_request);
return { count: filtered.length, type: 'issues', items: filtered };
}
return { count: issues.length, type: 'all', items: issues };
}
/**
* githubTriggerDeploy 触发部署工作流
*
* input:
* workflow: string 工作流文件名 deploy-to-zhuyuan-server.yml
* ref: string 触发分支可选默认 main
* inputs: object 工作流输入参数可选
*/
async function githubTriggerDeploy(input) {
const { workflow, ref, inputs } = input;
if (!workflow) throw new Error('缺少 workflow');
return github.triggerWorkflow(workflow, ref, inputs);
}
module.exports = {
githubReadFile,
githubListDir,
githubWriteFile,
githubGetCommits,
githubGetIssues,
githubTriggerDeploy
};

View File

@ -0,0 +1,249 @@
/**
*
* AGE OS · MCP 工具: Notion 操作
*
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 提供 5 Notion MCP 工具:
* notionQuery 查询 Notion 数据库
* notionReadPage 读取 Notion 页面内容
* notionWritePage 创建 Notion 页面
* notionUpdatePage 更新 Notion 页面属性
* notionWriteSyslog 写入系统日志到 Notion
*/
'use strict';
const notion = require('../notion-client');
/**
* notionQuery 查询 Notion 数据库
*
* input:
* database_id: string 数据库 ID或别名: changelog / receipt / syslog
* filter: object Notion 过滤器可选
* sorts: object[] 排序规则可选
* page_size: number 每页数量可选默认20
* start_cursor: string 分页游标可选
*/
async function notionQuery(input) {
const { database_id, filter, sorts, page_size, start_cursor } = input;
if (!database_id) throw new Error('缺少 database_id');
// 支持别名
const dbId = resolveDbAlias(database_id);
const result = await notion.queryDatabase(dbId, filter, sorts, page_size || 20, start_cursor);
return {
count: result.results.length,
has_more: result.has_more,
next_cursor: result.next_cursor,
items: result.results.map(simplifyPage)
};
}
/**
* notionReadPage 读取 Notion 页面完整内容
*
* input:
* page_id: string 页面 ID
*/
async function notionReadPage(input) {
const { page_id } = input;
if (!page_id) throw new Error('缺少 page_id');
const page = await notion.readPage(page_id);
return {
id: page.id,
created_time: page.created_time,
last_edited_time: page.last_edited_time,
properties: simplifyProperties(page.properties),
blocks_count: page.blocks.length,
blocks: page.blocks.map(simplifyBlock)
};
}
/**
* notionWritePage 在数据库中创建新页面
*
* input:
* database_id: string 数据库 ID或别名
* title: string 页面标题
* properties: object 额外属性Notion API 格式可选
* content: string 页面正文内容可选自动转为段落块
*/
async function notionWritePage(input) {
const { database_id, title, properties, content } = input;
if (!database_id) throw new Error('缺少 database_id');
if (!title) throw new Error('缺少 title');
const dbId = resolveDbAlias(database_id);
// 构建属性
const pageProps = {
...(properties || {}),
'标题': {
title: [{ text: { content: title } }]
}
};
// 构建内容块
const children = [];
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() } }]
}
});
}
}
}
return notion.createPage(dbId, pageProps, children);
}
/**
* notionUpdatePage 更新 Notion 页面属性
*
* input:
* page_id: string 页面 ID
* properties: object 要更新的属性Notion API 格式
*/
async function notionUpdatePage(input) {
const { page_id, properties } = input;
if (!page_id) throw new Error('缺少 page_id');
if (!properties) throw new Error('缺少 properties');
return notion.updatePage(page_id, properties);
}
/**
* notionWriteSyslog 写入系统日志到 Notion
*
* input:
* level: string 日志级别: info / warning / error / critical
* source: string 来源: zhuyuan / agent / workflow / mcp
* message: string 日志内容
* details: object 附加详情可选
*/
async function notionWriteSyslog(input) {
const { level, source, message, details } = input;
if (!level) throw new Error('缺少 level');
if (!source) throw new Error('缺少 source');
if (!message) throw new Error('缺少 message');
return notion.writeSyslog(level, source, message, details);
}
// ═══════════════════════════════════════════════════════════
// 辅助函数
// ═══════════════════════════════════════════════════════════
/**
* 解析数据库别名
*/
function resolveDbAlias(id) {
const aliases = {
changelog: notion.NOTION_CONFIG.databases.changelog,
receipt: notion.NOTION_CONFIG.databases.receipt,
syslog: notion.NOTION_CONFIG.databases.syslog
};
return aliases[id] || id;
}
/**
* 简化页面对象去除 Notion API 冗余结构
*/
function simplifyPage(page) {
return {
id: page.id,
created_time: page.created_time,
last_edited_time: page.last_edited_time,
properties: simplifyProperties(page.properties)
};
}
/**
* 简化属性提取值
*/
function simplifyProperties(props) {
if (!props) return {};
const result = {};
for (const [name, prop] of Object.entries(props)) {
result[name] = extractPropertyValue(prop);
}
return result;
}
/**
* 提取 Notion 属性值
*/
function extractPropertyValue(prop) {
switch (prop.type) {
case 'title':
return prop.title.map(t => t.plain_text).join('');
case 'rich_text':
return prop.rich_text.map(t => t.plain_text).join('');
case 'number':
return prop.number;
case 'select':
return prop.select?.name || null;
case 'multi_select':
return prop.multi_select.map(s => s.name);
case 'date':
return prop.date?.start || null;
case 'checkbox':
return prop.checkbox;
case 'url':
return prop.url;
case 'email':
return prop.email;
case 'status':
return prop.status?.name || null;
case 'formula':
return prop.formula?.string || prop.formula?.number || null;
case 'relation':
return prop.relation.map(r => r.id);
default:
return `[${prop.type}]`;
}
}
/**
* 简化内容块
*/
function simplifyBlock(block) {
const simplified = {
id: block.id,
type: block.type,
has_children: block.has_children
};
const content = block[block.type];
if (content) {
if (content.rich_text) {
simplified.text = content.rich_text.map(t => t.plain_text).join('');
}
if (content.language) {
simplified.language = content.language;
}
}
return simplified;
}
module.exports = {
notionQuery,
notionReadPage,
notionWritePage,
notionUpdatePage,
notionWriteSyslog
};

View File

@ -13,7 +13,8 @@
"express": "^4.21.0",
"pg": "^8.13.0",
"node-cron": "^3.0.3",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"@notionhq/client": "^2.2.15"
},
"engines": {
"node": ">=18.0.0"

View File

@ -349,6 +349,95 @@ app.get('/api/cos/load-works', async (req, res) => {
}
});
// ═══════════════════════════════════════════════════════════
// MCP 网关 · 3800 → 3100 转发 (S7)
// ═══════════════════════════════════════════════════════════
const http = require('http');
const MCP_HOST = process.env.MCP_HOST || '127.0.0.1';
const MCP_PORT_GATEWAY = process.env.MCP_PORT || '3100';
/**
* MCP 内部代理请求
*/
function mcpProxy(method, path, body) {
return new Promise((resolve, reject) => {
const bodyStr = body ? JSON.stringify(body) : null;
const headers = { 'Content-Type': 'application/json' };
if (bodyStr) headers['Content-Length'] = Buffer.byteLength(bodyStr);
const req = http.request({
hostname: MCP_HOST,
port: parseInt(MCP_PORT_GATEWAY, 10),
path,
method,
headers,
timeout: 60000
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString();
try {
resolve({ statusCode: res.statusCode, data: JSON.parse(raw) });
} catch {
resolve({ statusCode: res.statusCode, data: raw });
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('MCP proxy timeout')); });
if (bodyStr) req.write(bodyStr);
req.end();
});
}
// ─── MCP 工具列表 ───
app.get('/api/mcp/tools', async (_req, res) => {
try {
const result = await mcpProxy('GET', '/tools');
res.json(result.data);
} catch (err) {
res.status(502).json({ error: true, message: `MCP Server 不可达: ${err.message}` });
}
});
// ─── MCP 健康检查(含 Notion/GitHub 连接状态) ───
app.get('/api/mcp/health', async (_req, res) => {
try {
const result = await mcpProxy('GET', '/health');
res.json(result.data);
} catch (err) {
res.status(502).json({ error: true, message: `MCP Server 不可达: ${err.message}` });
}
});
// ─── MCP 统一工具调用(网关入口) ───
app.post('/api/mcp/call', async (req, res) => {
const { tool, input, caller } = req.body;
if (!tool) {
return res.status(400).json({ error: true, code: 'MISSING_TOOL', message: '缺少 tool 参数' });
}
try {
const result = await mcpProxy('POST', '/call', { tool, input, caller: caller || 'web-gateway' });
res.status(result.statusCode).json(result.data);
} catch (err) {
res.status(502).json({ error: true, message: `MCP Server 不可达: ${err.message}` });
}
});
// ─── MCP Agent 查询 ───
app.get('/api/mcp/agents', async (_req, res) => {
try {
const result = await mcpProxy('GET', '/agents');
res.json(result.data);
} catch (err) {
res.status(502).json({ error: true, message: `MCP Server 不可达: ${err.message}` });
}
});
// ═══════════════════════════════════════════════════════════
// 智能模型分流 · Smart Model Router API
// ═══════════════════════════════════════════════════════════
@ -698,6 +787,10 @@ app.get('/', (_req, res) => {
chat_stats: '/api/chat/stats',
cos_status: '/api/cos/status',
cos_config: '/api/cos/config',
mcp_tools: '/api/mcp/tools',
mcp_health: '/api/mcp/health',
mcp_call: 'POST /api/mcp/call',
mcp_agents: '/api/mcp/agents',
model_stats: '/api/model/stats',
model_pricing: '/api/model/pricing',
model_predict: 'POST /api/model/predict',