201 lines
6.0 KiB
JavaScript
201 lines
6.0 KiB
JavaScript
// scripts/skyeye/scan-external-bridges.js
|
||
// 天眼·扫描模块D · 外部桥接状态扫描
|
||
//
|
||
// 扫描内容:
|
||
// ① Notion API 连通性(用 NOTION_TOKEN 测试)
|
||
// ② 服务器 SSH 连通性(检测 DEPLOY_HOST 配置)
|
||
// ③ GitHub API 有效性 + 配额
|
||
// ④ Secrets 完整性(NOTION_TOKEN, DEPLOY_HOST, DEPLOY_USER, DEPLOY_KEY, SMTP_USER, SMTP_PASS)
|
||
//
|
||
// 输出:JSON → stdout
|
||
//
|
||
// 注意:此脚本在 GitHub Actions 中运行,通过环境变量获取 secrets 存在性
|
||
|
||
'use strict';
|
||
|
||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||
const now = new Date();
|
||
|
||
// ━━━ 检查 Secrets 完整性(仅检查环境变量是否被设置) ━━━
|
||
function checkSecrets() {
|
||
const requiredSecrets = [
|
||
'NOTION_TOKEN',
|
||
'DEPLOY_HOST',
|
||
'DEPLOY_USER',
|
||
'DEPLOY_KEY',
|
||
'SMTP_USER',
|
||
'SMTP_PASS'
|
||
];
|
||
|
||
const results = [];
|
||
let allPresent = true;
|
||
|
||
for (const secret of requiredSecrets) {
|
||
const present = !!process.env[secret] && process.env[secret].length > 0;
|
||
if (!present) allPresent = false;
|
||
results.push({ name: secret, present });
|
||
}
|
||
|
||
return {
|
||
complete: allPresent,
|
||
secrets: results,
|
||
missing: results.filter(s => !s.present).map(s => s.name)
|
||
};
|
||
}
|
||
|
||
// ━━━ 检查 Notion API 连通性 ━━━
|
||
async function checkNotionAPI() {
|
||
const token = process.env.NOTION_TOKEN;
|
||
if (!token) {
|
||
return { status: '🔴', detail: 'NOTION_TOKEN 未设置', connected: false };
|
||
}
|
||
|
||
try {
|
||
// Use native https to avoid dependency on axios
|
||
const result = await new Promise((resolve, reject) => {
|
||
const https = require('https');
|
||
const options = {
|
||
hostname: 'api.notion.com',
|
||
path: '/v1/users/me',
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + token,
|
||
'Notion-Version': '2022-06-28'
|
||
},
|
||
timeout: 10000
|
||
};
|
||
|
||
const req = https.request(options, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
resolve({ statusCode: res.statusCode, body: data });
|
||
});
|
||
});
|
||
|
||
req.on('error', (e) => reject(e));
|
||
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||
req.end();
|
||
});
|
||
|
||
if (result.statusCode === 200) {
|
||
return { status: '🟢', detail: 'Notion API 连通', connected: true };
|
||
} else {
|
||
return { status: '🟡', detail: `Notion API 返回 ${result.statusCode}`, connected: false };
|
||
}
|
||
} catch (e) {
|
||
return { status: '🔴', detail: 'Notion API 请求失败: ' + e.message, connected: false };
|
||
}
|
||
}
|
||
|
||
// ━━━ 检查 GitHub API ━━━
|
||
async function checkGitHubAPI() {
|
||
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
||
|
||
try {
|
||
const https = require('https');
|
||
const result = await new Promise((resolve, reject) => {
|
||
const options = {
|
||
hostname: 'api.github.com',
|
||
path: '/rate_limit',
|
||
method: 'GET',
|
||
headers: {
|
||
'User-Agent': 'skyeye-scanner',
|
||
'Accept': 'application/vnd.github.v3+json'
|
||
},
|
||
timeout: 10000
|
||
};
|
||
|
||
if (token) {
|
||
options.headers['Authorization'] = 'Bearer ' + token;
|
||
}
|
||
|
||
const req = https.request(options, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
resolve({ statusCode: res.statusCode, body: data });
|
||
});
|
||
});
|
||
|
||
req.on('error', (e) => reject(e));
|
||
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||
req.end();
|
||
});
|
||
|
||
if (result.statusCode === 200) {
|
||
try {
|
||
const rateData = JSON.parse(result.body);
|
||
const core = rateData.resources && rateData.resources.core;
|
||
return {
|
||
status: '🟢',
|
||
detail: 'GitHub API 连通',
|
||
connected: true,
|
||
rate_limit: core ? { remaining: core.remaining, limit: core.limit } : null
|
||
};
|
||
} catch (e) {
|
||
return { status: '🟢', detail: 'GitHub API 连通(无法解析配额)', connected: true };
|
||
}
|
||
} else {
|
||
return { status: '🟡', detail: `GitHub API 返回 ${result.statusCode}`, connected: false };
|
||
}
|
||
} catch (e) {
|
||
return { status: '🔴', detail: 'GitHub API 请求失败: ' + e.message, connected: false };
|
||
}
|
||
}
|
||
|
||
// ━━━ 检查服务器 SSH ━━━
|
||
function checkServerSSH() {
|
||
const host = process.env.DEPLOY_HOST;
|
||
const user = process.env.DEPLOY_USER;
|
||
const key = process.env.DEPLOY_KEY;
|
||
|
||
if (!host) {
|
||
return { status: '🟡', detail: 'DEPLOY_HOST 未设置', connected: false };
|
||
}
|
||
|
||
// SSH connectivity check would require actually connecting
|
||
// In CI, we just verify the credentials are available
|
||
const hasCredentials = !!(host && user && key);
|
||
return {
|
||
status: hasCredentials ? '🟡' : '🔴',
|
||
detail: hasCredentials
|
||
? '服务器凭证已配置(SSH 连通性需运行时验证)'
|
||
: '服务器凭证不完整',
|
||
connected: null, // Cannot verify without actual SSH
|
||
credentials_present: hasCredentials
|
||
};
|
||
}
|
||
|
||
// ━━━ 主扫描 ━━━
|
||
async function scanExternalBridges() {
|
||
const result = {
|
||
scan_time: new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00',
|
||
notion_api: { status: '⏳', detail: '检查中...' },
|
||
github_api: { status: '⏳', detail: '检查中...' },
|
||
server_ssh: checkServerSSH(),
|
||
secrets: checkSecrets(),
|
||
// Summary
|
||
all_bridges_ok: false
|
||
};
|
||
|
||
// Parallel API checks
|
||
const [notionResult, githubResult] = await Promise.all([
|
||
checkNotionAPI().catch(e => ({ status: '🔴', detail: '检查异常: ' + e.message, connected: false })),
|
||
checkGitHubAPI().catch(e => ({ status: '🔴', detail: '检查异常: ' + e.message, connected: false }))
|
||
]);
|
||
|
||
result.notion_api = notionResult;
|
||
result.github_api = githubResult;
|
||
|
||
// Summary
|
||
result.all_bridges_ok =
|
||
result.notion_api.status === '🟢' &&
|
||
result.github_api.status === '🟢' &&
|
||
result.secrets.complete;
|
||
|
||
console.log(JSON.stringify(result, null, 2));
|
||
}
|
||
|
||
scanExternalBridges();
|