327 lines
15 KiB
JavaScript
327 lines
15 KiB
JavaScript
|
|
// 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 formatResult = (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 未配置)' : formatResult(results.signalLog)}`);
|
|||
|
|
console.log(`· 变更日志写入:${formatResult(results.changeLog)}`);
|
|||
|
|
console.log(`· SYSLOG收件箱写入:${formatResult(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);
|
|||
|
|
});
|