diff --git a/.github/workflows/notion-connectivity-test.yml b/.github/workflows/notion-connectivity-test.yml new file mode 100644 index 00000000..c8d25bd3 --- /dev/null +++ b/.github/workflows/notion-connectivity-test.yml @@ -0,0 +1,53 @@ +name: 铸渊 · Notion 连通性测试 + +# YM-TEST-20260307-001 · 铸渊→Notion 数据桥连通性测试 +# +# 三项测试: +# 1. 写入「📡 跨平台信号日志」(SIGNAL_LOG_DB_ID) +# 2. 写入「📋 GitHub 变更日志」(CHANGES_DB_ID,有默认值) +# 3. 写入「📥 GitHub SYSLOG 收件箱」(SYSLOG_DB_ID,有默认值) +# +# 依赖 Secrets: +# NOTION_TOKEN Notion Integration Token +# +# 可选 Secrets / Variables: +# SIGNAL_LOG_DB_ID 信号日志 database_id(Secrets 或 Variables) +# CHANGES_DB_ID 变更日志 database_id(有默认值,可覆盖) +# SYSLOG_DB_ID SYSLOG 收件箱 database_id(有默认值,可覆盖) + +on: + workflow_dispatch: + +jobs: + connectivity-test: + name: 🧪 Notion 连通性测试 + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 🧪 执行连通性测试 + id: test + env: + NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} + SIGNAL_LOG_DB_ID: ${{ vars.SIGNAL_LOG_DB_ID }} + CHANGES_DB_ID: ${{ secrets.CHANGES_DB_ID }} + SYSLOG_DB_ID: ${{ secrets.SYSLOG_DB_ID }} + run: node scripts/notion-connectivity-test.js + + - name: 提交信号日志 + if: always() + run: | + git config user.name "铸渊 (ZhùYuān)" + git config user.email "zhuyuan@guanghulab.com" + git add signal-log/ + git diff --cached --quiet || git commit -m "🧪 连通性测试信号日志 · YM-TEST-20260307-001" + git push || true diff --git a/scripts/notion-connectivity-test.js b/scripts/notion-connectivity-test.js new file mode 100644 index 00000000..4b009c4a --- /dev/null +++ b/scripts/notion-connectivity-test.js @@ -0,0 +1,326 @@ +// scripts/notion-connectivity-test.js +// 铸渊 → Notion 连通性测试(YM-TEST-20260307-001) +// +// 用法: +// node scripts/notion-connectivity-test.js +// +// 必需环境变量: +// NOTION_TOKEN Notion Integration Token(GitHub Secret) +// +// 可选环境变量(有内置默认值): +// SIGNAL_LOG_DB_ID 「📡 跨平台信号日志」database_id +// CHANGES_DB_ID 「📋 GitHub 变更日志」database_id(默认已内置) +// SYSLOG_DB_ID 「📥 GitHub SYSLOG 收件箱」database_id(默认已内置) + +'use strict'; + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +// ══════════════════════════════════════════════════════════ +// 常量 +// ══════════════════════════════════════════════════════════ + +const NOTION_VERSION = '2022-06-28'; +const NOTION_API_HOSTNAME = 'api.notion.com'; + +// Notion 数据库 ID(默认值与 notion-bridge.js / notion-signal-bridge.js 一致) +const DEFAULT_SYSLOG_DB_ID = '330ab17507d542c9bbb96d0749b41197'; +const DEFAULT_CHANGES_DB_ID = 'e740b77aa6bd4ac0a2e8a75f678fba98'; + +const SIGNAL_LOG_DIR = path.join(__dirname, '../signal-log'); +const SIGNAL_INDEX_PATH = path.join(SIGNAL_LOG_DIR, 'index.json'); + +// ══════════════════════════════════════════════════════════ +// Notion API 基础调用(复用 notion-bridge.js 模式) +// ══════════════════════════════════════════════════════════ + +function notionPost(endpoint, body, token) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body); + const opts = { + hostname: NOTION_API_HOSTNAME, + port: 443, + path: endpoint, + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json', + 'Notion-Version': NOTION_VERSION, + 'Content-Length': Buffer.byteLength(payload), + }, + }; + + const req = https.request(opts, res => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const parsed = JSON.parse(data); + resolve({ statusCode: res.statusCode, body: parsed }); + } catch (e) { + resolve({ statusCode: res.statusCode, body: null, raw: data }); + } + }); + }); + + req.on('error', reject); + req.setTimeout(30000, () => { req.destroy(new Error('Notion API 请求超时')); }); + req.write(payload); + req.end(); + }); +} + +// ══════════════════════════════════════════════════════════ +// 本地信号日志(复用 notion-signal-bridge.js 模式) +// ══════════════════════════════════════════════════════════ + +function loadSignalIndex() { + if (!fs.existsSync(SIGNAL_INDEX_PATH)) { + return { + description: '铸渊信号日志目录索引 · AGE OS 信号协议(Notion API 直连)', + last_updated: new Date().toISOString(), + total_count: 0, + signals: [] + }; + } + return JSON.parse(fs.readFileSync(SIGNAL_INDEX_PATH, 'utf8')); +} + +function writeLocalSignalLog(signal) { + const dateStr = signal.timestamp.slice(0, 7); + const monthDir = path.join(SIGNAL_LOG_DIR, dateStr); + fs.mkdirSync(monthDir, { recursive: true }); + + const filePath = path.join(monthDir, `${signal.signal_id}.json`); + fs.writeFileSync(filePath, JSON.stringify(signal, null, 2)); + + const index = loadSignalIndex(); + index.total_count += 1; + index.signals.unshift({ + signal_id: signal.signal_id, + trace_id: signal.trace_id, + type: signal.signal_type, + timestamp: signal.timestamp, + summary: signal.summary, + related_dev: null, + file: `${dateStr}/${signal.signal_id}.json` + }); + index.last_updated = new Date().toISOString(); + fs.writeFileSync(SIGNAL_INDEX_PATH, JSON.stringify(index, null, 2)); +} + +// ══════════════════════════════════════════════════════════ +// 三项连通性测试 +// ══════════════════════════════════════════════════════════ + +async function testSignalLog(token, dbId) { + return notionPost('/v1/pages', { + parent: { database_id: dbId }, + properties: { + '信号编号': { title: [{ text: { content: 'SIG-TEST-20260307-001' } }] }, + '信号类型': { select: { name: 'GL-ACK' } }, + '发送方': { select: { name: '铸渊' } }, + '接收方': { select: { name: '霜砚' } }, + '方向': { select: { name: 'GitHub→Notion' } }, + '关联DEV': { select: { name: '系统' } }, + '关联模块': { select: { name: '全局' } }, + '执行结果': { status: { name: '成功' } }, + '摘要': { rich_text: [{ text: { content: '🧪 铸渊→Notion API 连通性测试 · NOTION_TOKEN 验证 · 数据桥首次握手' } }] }, + '备注': { rich_text: [{ text: { content: '这是一条自动化测试信号。如果你在 Notion 信号日志里看到这条记录,说明 GitHub→Notion 数据桥已打通。' } }] } + } + }, token); +} + +async function testChangeLog(token, dbId) { + return notionPost('/v1/pages', { + parent: { database_id: dbId }, + properties: { + '标题': { title: [{ text: { content: '🧪 连通性测试 · 铸渊首次握手 · 2026-03-07' } }] }, + '变更类型': { select: { name: 'Commit' } }, + '提交者': { select: { name: '铸渊' } }, + '分支': { rich_text: [{ text: { content: 'main' } }] }, + 'commit_sha': { rich_text: [{ text: { content: 'test-00000000' } }] }, + '变更文件': { rich_text: [{ text: { content: '.github/test/bridge-connectivity-test.md' } }] }, + '霜砚已读': { checkbox: false } + } + }, token); +} + +async function testSyslogInbox(token, dbId) { + return notionPost('/v1/pages', { + parent: { database_id: dbId }, + properties: { + '标题': { title: [{ text: { content: '🧪 SYSLOG连通性测试 · 2026-03-07' } }] }, + 'DEV编号': { select: { name: 'DEV-001' } }, + '处理状态': { status: { name: '待处理' } }, + '推送方': { rich_text: [{ text: { content: '铸渊' } }] }, + '来源路径': { rich_text: [{ text: { content: 'syslog-inbox/test/bridge-test-20260307.md' } }] }, + '文件内容': { rich_text: [{ text: { content: '这是一条连通性测试SYSLOG。如果霜砚能在收件箱看到这条,说明铸渊→Notion SYSLOG推送管线已打通。' } }] }, + 'commit_sha': { rich_text: [{ text: { content: 'test-00000000' } }] } + } + }, token); +} + +// ══════════════════════════════════════════════════════════ +// 主执行 +// ══════════════════════════════════════════════════════════ + +async function main() { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🧪 YM-TEST-20260307-001 · 铸渊→Notion 连通性测试'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + const token = process.env.NOTION_TOKEN; + if (!token) { + console.error('❌ NOTION_TOKEN 未设置,无法执行连通性测试'); + console.error(' 请在 GitHub Secrets 中配置 NOTION_TOKEN'); + process.exit(1); + } + console.log('✅ NOTION_TOKEN 已检测到'); + + const signalLogDbId = process.env.SIGNAL_LOG_DB_ID || ''; + const changesDbId = process.env.CHANGES_DB_ID || DEFAULT_CHANGES_DB_ID; + const syslogDbId = process.env.SYSLOG_DB_ID || DEFAULT_SYSLOG_DB_ID; + + const results = { + signalLog: { ok: false, pageId: null, error: null }, + changeLog: { ok: false, pageId: null, error: null }, + syslogInbox: { ok: false, pageId: null, error: null }, + }; + + // ── 测试一:信号日志 ────────────────────────────────── + console.log('\n── 测试一:写入「📡 跨平台信号日志」──'); + if (!signalLogDbId) { + console.log('⚠️ SIGNAL_LOG_DB_ID 未配置,跳过此测试'); + results.signalLog.error = 'SIGNAL_LOG_DB_ID 未配置'; + } else { + console.log(` Database ID: ${signalLogDbId}`); + try { + const res = await testSignalLog(token, signalLogDbId); + if (res.statusCode >= 200 && res.statusCode < 300) { + results.signalLog.ok = true; + results.signalLog.pageId = res.body.id; + console.log(` ✅ 写入成功 · page_id: ${res.body.id}`); + } else { + results.signalLog.error = `HTTP ${res.statusCode}: ${res.body?.message || JSON.stringify(res.body)}`; + console.log(` ❌ 写入失败 · ${results.signalLog.error}`); + } + } catch (err) { + results.signalLog.error = err.message; + console.log(` ❌ 请求异常 · ${err.message}`); + } + } + + // ── 测试二:变更日志 ────────────────────────────────── + console.log('\n── 测试二:写入「📋 GitHub 变更日志」──'); + console.log(` Database ID: ${changesDbId}`); + try { + const res = await testChangeLog(token, changesDbId); + if (res.statusCode >= 200 && res.statusCode < 300) { + results.changeLog.ok = true; + results.changeLog.pageId = res.body.id; + console.log(` ✅ 写入成功 · page_id: ${res.body.id}`); + } else { + results.changeLog.error = `HTTP ${res.statusCode}: ${res.body?.message || JSON.stringify(res.body)}`; + console.log(` ❌ 写入失败 · ${results.changeLog.error}`); + } + } catch (err) { + results.changeLog.error = err.message; + console.log(` ❌ 请求异常 · ${err.message}`); + } + + // ── 测试三:SYSLOG 收件箱 ───────────────────────────── + console.log('\n── 测试三:写入「📥 GitHub SYSLOG 收件箱」──'); + console.log(` Database ID: ${syslogDbId}`); + try { + const res = await testSyslogInbox(token, syslogDbId); + if (res.statusCode >= 200 && res.statusCode < 300) { + results.syslogInbox.ok = true; + results.syslogInbox.pageId = res.body.id; + console.log(` ✅ 写入成功 · page_id: ${res.body.id}`); + } else { + results.syslogInbox.error = `HTTP ${res.statusCode}: ${res.body?.message || JSON.stringify(res.body)}`; + console.log(` ❌ 写入失败 · ${results.syslogInbox.error}`); + } + } catch (err) { + results.syslogInbox.error = err.message; + console.log(` ❌ 请求异常 · ${err.message}`); + } + + // ── 写入本地信号日志 ────────────────────────────────── + const now = new Date(); + const allPassed = results.signalLog.ok && results.changeLog.ok && results.syslogInbox.ok; + const signalLogSkipped = !signalLogDbId; + const partialPassed = results.changeLog.ok || results.syslogInbox.ok || results.signalLog.ok; + + let bridgeStatus; + if (allPassed) { + bridgeStatus = '全通'; + } else if (signalLogSkipped && results.changeLog.ok && results.syslogInbox.ok) { + bridgeStatus = '全通(信号日志DB未配置,其余全通)'; + } else if (partialPassed) { + bridgeStatus = '部分通'; + } else { + bridgeStatus = '不通'; + } + + const signal = { + signal_id: 'SIG-TEST-20260307-001', + trace_id: 'TRC-TEST-20260307-YM001', + timestamp: now.toISOString(), + signal_type: 'GL-ACK', + direction: 'GitHub→Notion', + sender: '铸渊', + receiver: '霜砚', + summary: `🧪 连通性测试完成 · 数据桥状态 = ${bridgeStatus}`, + result: allPassed || (signalLogSkipped && results.changeLog.ok && results.syslogInbox.ok) ? '成功' : '失败', + payload: { + test_id: 'YM-TEST-20260307-001', + signal_log: results.signalLog, + change_log: results.changeLog, + syslog_inbox: results.syslogInbox, + bridge_status: bridgeStatus + } + }; + + try { + writeLocalSignalLog(signal); + console.log(`\n📝 本地信号日志已写入: ${signal.signal_id}`); + } catch (err) { + console.error(`\n⚠️ 本地信号日志写入失败: ${err.message}`); + } + + // ── 输出汇总 ────────────────────────────────────────── + const fmt = (r) => r.ok ? `✅(page_id: ${r.pageId})` : `❌(${r.error})`; + + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('✅ YM-TEST-20260307-001 连通性测试完成'); + console.log(`· 信号日志写入:${signalLogSkipped ? '⚠️ 跳过(SIGNAL_LOG_DB_ID 未配置)' : fmt(results.signalLog)}`); + console.log(`· 变更日志写入:${fmt(results.changeLog)}`); + console.log(`· SYSLOG收件箱写入:${fmt(results.syslogInbox)}`); + console.log(`· 总结:数据桥状态 = ${bridgeStatus}`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + // GitHub Actions: 输出到 GITHUB_OUTPUT(如有) + if (process.env.GITHUB_OUTPUT) { + const outputLines = [ + `signal_log_ok=${results.signalLog.ok}`, + `change_log_ok=${results.changeLog.ok}`, + `syslog_inbox_ok=${results.syslogInbox.ok}`, + `bridge_status=${bridgeStatus}`, + ]; + fs.appendFileSync(process.env.GITHUB_OUTPUT, outputLines.join('\n') + '\n'); + } + + // 如果没有全通(且不是因为跳过信号日志),以非零退出码结束 + if (!allPassed && !(signalLogSkipped && results.changeLog.ok && results.syslogInbox.ok)) { + process.exit(1); + } +} + +main().catch(err => { + console.error('❌ 连通性测试异常退出:', err.message); + process.exit(1); +});