From 3f62f770c282714f49bcb1e8e6100f745ebad5a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:21:18 +0000 Subject: [PATCH] =?UTF-8?q?D51=20S6+S7:=20Notion=20client,=20GitHub=20clie?= =?UTF-8?q?nt,=2011=20new=20MCP=20tools,=203800=E2=86=923100=20gateway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/2127a3a7-ea58-48a4-856c-84961107632b Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- brain/age-os-landing/development-roadmap.md | 7 +- server/age-os/mcp-server/github-client.js | 350 +++++++++++++++++++ server/age-os/mcp-server/notion-client.js | 339 ++++++++++++++++++ server/age-os/mcp-server/server.js | 63 +++- server/age-os/mcp-server/tools/github-ops.js | 138 ++++++++ server/age-os/mcp-server/tools/notion-ops.js | 249 +++++++++++++ server/age-os/package.json | 3 +- server/app/server.js | 93 +++++ 8 files changed, 1230 insertions(+), 12 deletions(-) create mode 100644 server/age-os/mcp-server/github-client.js create mode 100644 server/age-os/mcp-server/notion-client.js create mode 100644 server/age-os/mcp-server/tools/github-ops.js create mode 100644 server/age-os/mcp-server/tools/notion-ops.js diff --git a/brain/age-os-landing/development-roadmap.md b/brain/age-os-landing/development-roadmap.md index 132ddf37..9619705c 100644 --- a/brain/age-os-landing/development-roadmap.md +++ b/brain/age-os-landing/development-roadmap.md @@ -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 | ๅพ…ๅผ€ๅ‘ | โ€” | --- diff --git a/server/age-os/mcp-server/github-client.js b/server/age-os/mcp-server/github-client.js new file mode 100644 index 00000000..2e610383 --- /dev/null +++ b/server/age-os/mcp-server/github-client.js @@ -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} + */ +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} + */ +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} + */ +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} + */ +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 +}; diff --git a/server/age-os/mcp-server/notion-client.js b/server/age-os/mcp-server/notion-client.js new file mode 100644 index 00000000..7906f09b --- /dev/null +++ b/server/age-os/mcp-server/notion-client.js @@ -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} + */ +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 +}; diff --git a/server/age-os/mcp-server/server.js b/server/age-os/mcp-server/server.js index c82f89b0..3d8e5368 100644 --- a/server/age-os/mcp-server/server.js +++ b/server/age-os/mcp-server/server.js @@ -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'; } diff --git a/server/age-os/mcp-server/tools/github-ops.js b/server/age-os/mcp-server/tools/github-ops.js new file mode 100644 index 00000000..e54e4157 --- /dev/null +++ b/server/age-os/mcp-server/tools/github-ops.js @@ -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 +}; diff --git a/server/age-os/mcp-server/tools/notion-ops.js b/server/age-os/mcp-server/tools/notion-ops.js new file mode 100644 index 00000000..5de34e29 --- /dev/null +++ b/server/age-os/mcp-server/tools/notion-ops.js @@ -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 +}; diff --git a/server/age-os/package.json b/server/age-os/package.json index 5d6a70b6..50dc637b 100644 --- a/server/age-os/package.json +++ b/server/age-os/package.json @@ -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" diff --git a/server/app/server.js b/server/app/server.js index b1f90700..5b28ffb9 100644 --- a/server/app/server.js +++ b/server/app/server.js @@ -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',