249 lines
6.8 KiB
JavaScript
249 lines
6.8 KiB
JavaScript
/**
|
||
* scripts/dc-notion-usage.js
|
||
* DC-01 · Notion 读写分析器
|
||
*
|
||
* 采集时机:每日自检结束时附带写入
|
||
* 存储位置:data/dc-reports/notion-usage-YYYY-MM-DD.json
|
||
*
|
||
* 采集方式:
|
||
* 读取天眼报告中的 Notion 相关诊断数据,
|
||
* 统计当日 API 调用次数并写入标准格式。
|
||
* 当 Notion API 可用时,额外查询数据库元信息。
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const https = require('https');
|
||
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
const DC_DIR = path.join(ROOT, 'data/dc-reports');
|
||
const SKYEYE_DIR = path.join(ROOT, 'data/skyeye-reports');
|
||
|
||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||
const now = new Date();
|
||
const today = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString().split('T')[0];
|
||
|
||
// ━━━ 工具函数 ━━━
|
||
|
||
function loadJson(filePath) {
|
||
try {
|
||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function notionGet(apiPath, token) {
|
||
return new Promise((resolve, reject) => {
|
||
const options = {
|
||
hostname: 'api.notion.com',
|
||
path: apiPath,
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': 'Bearer ' + token,
|
||
'Notion-Version': '2022-06-28',
|
||
'Content-Type': 'application/json',
|
||
'User-Agent': 'dc-notion-usage'
|
||
},
|
||
timeout: 15000
|
||
};
|
||
|
||
const req = https.request(options, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
try {
|
||
resolve({ statusCode: res.statusCode, body: JSON.parse(data) });
|
||
} catch {
|
||
resolve({ statusCode: res.statusCode, body: data });
|
||
}
|
||
});
|
||
});
|
||
req.on('error', reject);
|
||
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// ━━━ 从天眼报告中提取 Notion 相关数据 ━━━
|
||
|
||
function extractFromSkyeye() {
|
||
const info = {
|
||
skyeye_available: false,
|
||
bridges_status: null,
|
||
notion_connected: false,
|
||
api_hint_calls: 0
|
||
};
|
||
|
||
// 查找最近的天眼报告
|
||
try {
|
||
const files = fs.readdirSync(SKYEYE_DIR)
|
||
.filter(f => f.startsWith('skyeye-') && f.endsWith('.json'))
|
||
.sort()
|
||
.reverse();
|
||
|
||
if (files.length === 0) return info;
|
||
|
||
const report = loadJson(path.join(SKYEYE_DIR, files[0]));
|
||
if (!report) return info;
|
||
|
||
info.skyeye_available = true;
|
||
|
||
// 提取 bridge 扫描中的 Notion 连接信息
|
||
if (report.bridges) {
|
||
const bridges = typeof report.bridges === 'string'
|
||
? loadJson(path.join(SKYEYE_DIR, 'bridge-health.json'))
|
||
: report.bridges;
|
||
|
||
if (bridges && bridges.notion_api) {
|
||
info.notion_connected = bridges.notion_api.connected === true;
|
||
info.bridges_status = bridges.notion_api.status || null;
|
||
}
|
||
}
|
||
} catch {
|
||
// 天眼报告不可用,跳过
|
||
}
|
||
|
||
return info;
|
||
}
|
||
|
||
// ━━━ 查询 Notion 数据库元信息 ━━━
|
||
|
||
async function queryNotionDatabases(token) {
|
||
const databases = [];
|
||
|
||
try {
|
||
// search 需要 POST,此处使用已知数据库 ID 直接查询
|
||
} catch {
|
||
// Notion API 不可达
|
||
}
|
||
|
||
// 尝试查询环境变量中的已知数据库
|
||
const knownDbs = [];
|
||
|
||
if (process.env.NOTION_DB_SYSLOG) {
|
||
knownDbs.push({ id: process.env.NOTION_DB_SYSLOG, name: 'SYSLOG收件箱' });
|
||
}
|
||
if (process.env.NOTION_TICKET_DB_ID) {
|
||
knownDbs.push({ id: process.env.NOTION_TICKET_DB_ID, name: '人格协作工单簿' });
|
||
}
|
||
if (process.env.BRIDGE_QUEUE_DB_ID) {
|
||
knownDbs.push({ id: process.env.BRIDGE_QUEUE_DB_ID, name: '调度队列' });
|
||
}
|
||
|
||
for (const db of knownDbs) {
|
||
try {
|
||
const resp = await notionGet('/v1/databases/' + db.id, token);
|
||
if (resp.statusCode === 200 && resp.body && resp.body.properties) {
|
||
const props = Object.keys(resp.body.properties);
|
||
databases.push({
|
||
name: resp.body.title?.[0]?.plain_text || db.name,
|
||
daily_reads: 0,
|
||
daily_writes: 0,
|
||
active_fields: props.slice(0, 20),
|
||
unused_fields: []
|
||
});
|
||
}
|
||
} catch {
|
||
// 该数据库不可访问
|
||
}
|
||
}
|
||
|
||
return databases;
|
||
}
|
||
|
||
// ━━━ 读取 writeback log 统计当日调用次数 ━━━
|
||
|
||
function countTodayApiCalls() {
|
||
let totalCalls = 0;
|
||
let peakHourMap = {};
|
||
|
||
// 检查 notion-writeback-log.json
|
||
const wbLog = loadJson(path.join(ROOT, 'data/notion-writeback-log.json'));
|
||
if (wbLog && Array.isArray(wbLog)) {
|
||
for (const entry of wbLog) {
|
||
if (entry.timestamp && entry.timestamp.startsWith(today)) {
|
||
totalCalls++;
|
||
const hour = entry.timestamp.substring(11, 13);
|
||
peakHourMap[hour] = (peakHourMap[hour] || 0) + 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查 bridge-logs
|
||
const bridgeLogDir = path.join(ROOT, 'data/bridge-logs');
|
||
if (fs.existsSync(bridgeLogDir)) {
|
||
try {
|
||
const logFiles = fs.readdirSync(bridgeLogDir)
|
||
.filter(f => f.includes(today) && f.endsWith('.json'));
|
||
for (const lf of logFiles) {
|
||
const log = loadJson(path.join(bridgeLogDir, lf));
|
||
if (log && log.notion_calls) {
|
||
totalCalls += log.notion_calls;
|
||
}
|
||
}
|
||
} catch {
|
||
// bridge-logs 不可读
|
||
}
|
||
}
|
||
|
||
// 找出峰值小时
|
||
let peakHour = null;
|
||
let peakCount = 0;
|
||
for (const [hour, count] of Object.entries(peakHourMap)) {
|
||
if (count > peakCount) {
|
||
peakCount = count;
|
||
const h = parseInt(hour, 10);
|
||
peakHour = `${String(h).padStart(2, '0')}:00-${String((h + 1) % 24).padStart(2, '0')}:00`;
|
||
}
|
||
}
|
||
|
||
return { totalCalls, peakHour: peakHour || 'N/A' };
|
||
}
|
||
|
||
// ━━━ 主流程 ━━━
|
||
|
||
async function main() {
|
||
console.log(`📦 DC-01 · Notion 读写分析器 · ${today}`);
|
||
|
||
fs.mkdirSync(DC_DIR, { recursive: true });
|
||
|
||
const skyeyeInfo = extractFromSkyeye();
|
||
const { totalCalls, peakHour } = countTodayApiCalls();
|
||
|
||
const notionToken = process.env.NOTION_TOKEN;
|
||
let databases = [];
|
||
|
||
if (notionToken) {
|
||
console.log(' → Notion Token 可用,查询数据库元信息...');
|
||
databases = await queryNotionDatabases(notionToken);
|
||
console.log(` → 查询到 ${databases.length} 个数据库`);
|
||
} else {
|
||
console.log(' ⚠️ 缺少 NOTION_TOKEN,跳过 Notion API 查询');
|
||
}
|
||
|
||
const report = {
|
||
date: today,
|
||
databases: databases,
|
||
total_api_calls: totalCalls,
|
||
peak_hour: peakHour,
|
||
_meta: {
|
||
generated_by: 'dc-notion-usage.js',
|
||
generated_at: now.toISOString(),
|
||
skyeye_available: skyeyeInfo.skyeye_available,
|
||
notion_connected: skyeyeInfo.notion_connected
|
||
}
|
||
};
|
||
|
||
const outputFile = path.join(DC_DIR, `notion-usage-${today}.json`);
|
||
fs.writeFileSync(outputFile, JSON.stringify(report, null, 2));
|
||
console.log(` ✅ DC-01 报告已写入: ${path.relative(ROOT, outputFile)}`);
|
||
}
|
||
|
||
main().catch(err => {
|
||
console.error('❌ DC-01 采集失败:', err.message);
|
||
process.exit(1);
|
||
});
|