guanghulab/scripts/cache/sync-notion-cache.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

425 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 📡 Notion → GitHub 全量缓存同步脚本
*
* 同步内容:
* 1. 开发者画像 → .github/notion-cache/dev-profiles/
* 2. 数据库快照 → .github/notion-cache/databases/
* 3. 活跃广播 → .github/notion-cache/broadcasts/
*
* 用法node scripts/cache/sync-notion-cache.js
* 环境变量:
* NOTION_TOKEN — Notion API 密钥(必需)
* NOTION_CONTROL_PANEL_DB_ID — 主控台数据库 ID可选
* NOTION_SYSLOG_DB_ID — SYSLOG 收件箱 ID可选
* NOTION_AGENT_REGISTRY_DB_ID — Agent 注册表 ID可选
* NOTION_TICKET_DB_ID — 工单簿 ID可选
* NOTION_MAINTENANCE_DB_ID — 维护日志 ID可选
*
* 签发霜砚AG-SY-01
* 审批冰朔TCS-0002∞
* 版权:国作登字-2026-A-00037559
*/
'use strict';
const https = require('https');
const fs = require('fs');
const path = require('path');
const NOTION_TOKEN = process.env.NOTION_TOKEN;
const CACHE_DIR = path.join(__dirname, '../../.github/notion-cache');
// ====== Notion 数据库 ID 映射(通过环境变量传入)======
const DB_IDS = {
controlPanel: process.env.NOTION_CONTROL_PANEL_DB_ID || '',
syslogInbox: process.env.NOTION_SYSLOG_DB_ID || '',
agentRegistry: process.env.NOTION_AGENT_REGISTRY_DB_ID || '',
ticketBook: process.env.NOTION_TICKET_DB_ID || '',
maintenanceLog: process.env.NOTION_MAINTENANCE_DB_ID || '',
receiptTracker: process.env.RECEIPT_DB_ID || '',
};
// ====== 开发者编号 → 信息映射 ======
const DEVS = [
{ id: 'DEV-001', name: '页页', persona: '小坍缩核', personaId: 'PER-XTC001', repo: 'guanghulab', modules: ['backend/', 'src/'] },
{ id: 'DEV-002', name: '肥猫', persona: '舒舒', personaId: 'PER-SS001', repo: 'guanghu-feimao', modules: ['frontend/', 'persona-selector/', 'chat-bubble/'] },
{ id: 'DEV-003', name: '燕樊', persona: '寂曜', personaId: 'PER-JY001', repo: 'guanghu-yanfan', modules: ['settings/', 'cloud-drive/'] },
{ id: 'DEV-004', name: '之之', persona: '秋秋', personaId: 'PER-QQ001', repo: 'guanghu-zhizhi', modules: ['dingtalk-bot/'] },
{ id: 'DEV-005', name: '小草莓', persona: '欧诺弥亚', personaId: 'PER-ONM001', repo: 'guanghu-xiaocaomei', modules: ['status-board/'] },
{ id: 'DEV-009', name: '花尔', persona: '花尔', personaId: 'PER-HE001', repo: 'guanghulab', modules: ['user-center/'] },
{ id: 'DEV-010', name: '桔子', persona: '晨星', personaId: 'PER-MRN001', repo: 'guanghu-juzi', modules: ['ticket-system/', 'data-stats/', 'dynamic-comic/'] },
{ id: 'DEV-011', name: '匆匆那年', persona: '匆匆那年', personaId: 'PER-CCN001', repo: 'guanghulab', modules: ['writing-workspace/'] },
{ id: 'DEV-012', name: 'Awen', persona: '知秋', personaId: 'PER-ZQ001', repo: 'guanghu-awen', modules: ['notification-center/'] },
];
// ====== Notion API 封装(纯 https无外部依赖======
function notionRequest(method, apiPath, body) {
return new Promise(function(resolve, reject) {
if (!NOTION_TOKEN) {
console.warn('⚠️ NOTION_TOKEN 未配置,跳过 Notion API 调用');
resolve({ error: 'NOTION_TOKEN not configured' });
return;
}
var bodyStr = body ? JSON.stringify(body) : '';
var options = {
hostname: 'api.notion.com',
port: 443,
path: apiPath,
method: method,
headers: {
'Authorization': 'Bearer ' + NOTION_TOKEN,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(bodyStr)
}
};
var req = https.request(options, function(res) {
var data = '';
res.on('data', function(chunk) { data += chunk; });
res.on('end', function() {
try { resolve(JSON.parse(data)); }
catch (e) {
console.warn('⚠️ Notion API 响应解析失败:', e.message);
resolve({ error: 'JSON parse failed', raw: data });
}
});
});
req.on('error', reject);
if (bodyStr) req.write(bodyStr);
req.end();
});
}
function queryDatabase(dbId, filter, sorts, pageSize) {
pageSize = pageSize || 100;
var body = { page_size: pageSize };
if (filter) body.filter = filter;
if (sorts) body.sorts = sorts;
return notionRequest('POST', '/v1/databases/' + dbId + '/query', body);
}
// ====== Notion 属性提取器 ======
function extractRichText(prop) {
if (!prop) return '';
if (prop.type === 'rich_text' && prop.rich_text) {
return prop.rich_text.map(function(t) { return t.plain_text; }).join('');
}
if (prop.type === 'title' && prop.title) {
return prop.title.map(function(t) { return t.plain_text; }).join('');
}
return '';
}
function extractTitle(props) {
for (var key in props) {
var val = props[key];
if (val && val.type === 'title' && val.title) {
return val.title.map(function(t) { return t.plain_text; }).join('');
}
}
return '';
}
function extractNumber(prop) {
if (!prop || prop.type !== 'number') return null;
return prop.number;
}
function extractSelect(prop) {
if (!prop || prop.type !== 'select') return '';
return prop.select ? prop.select.name : '';
}
function extractMultiSelect(prop) {
if (!prop || prop.type !== 'multi_select') return [];
return (prop.multi_select || []).map(function(s) { return s.name; });
}
function extractDate(prop) {
if (!prop || prop.type !== 'date') return '';
return prop.date ? prop.date.start : '';
}
// ====== 同步开发者画像 ======
async function syncDevProfiles() {
var dir = path.join(CACHE_DIR, 'dev-profiles');
fs.mkdirSync(dir, { recursive: true });
var syncCount = 0;
var errorCount = 0;
for (var i = 0; i < DEVS.length; i++) {
var dev = DEVS[i];
console.log('📡 同步 ' + dev.id + '' + dev.name + '...');
var profile = {
dev_id: dev.id,
name: dev.name,
persona: { name: dev.persona, id: dev.personaId },
github: {
repo: dev.repo,
org: 'qinfendebingshuo',
repo_url: 'https://github.com/qinfendebingshuo/' + dev.repo
},
modules_owned: dev.modules.map(function(m) { return { path: m }; }),
current_work: {},
recent_syslogs: [],
skyeye_health: {},
notion_page_urls: {},
last_synced: new Date().toISOString()
};
// 从 Notion 读取该开发者的真实数据
if (NOTION_TOKEN) {
try {
// 查询主控台获取当前广播和进度
if (DB_IDS.controlPanel) {
var controlPanel = await queryDatabase(DB_IDS.controlPanel, {
property: 'DEV编号',
rich_text: { equals: dev.id }
}, null, 1);
if (controlPanel.results && controlPanel.results.length > 0) {
var page = controlPanel.results[0];
var props = page.properties || {};
profile.current_work = {
broadcast_id: extractRichText(props['广播编号']) || '',
broadcast_title: extractRichText(props['广播标题']) || extractTitle(props) || '',
current_ring: extractNumber(props['当前环节']) || 0,
total_rings: extractNumber(props['总环节']) || 0,
status: extractRichText(props['状态']) || extractSelect(props['状态']) || '',
module_path: extractRichText(props['模块路径']) || '',
tech_stack: extractMultiSelect(props['技术栈']) || []
};
}
}
// 查询 SYSLOG 收件箱
if (DB_IDS.syslogInbox) {
var syslogs = await queryDatabase(DB_IDS.syslogInbox, {
property: 'DEV编号',
rich_text: { equals: dev.id }
}, [{ property: '日期', direction: 'descending' }], 5);
if (syslogs.results) {
profile.recent_syslogs = syslogs.results.map(function(p) {
try {
var sp = p.properties || {};
return {
id: extractRichText(sp['SYSLOG编号']) || p.id,
date: extractDate(sp['日期']) || '',
ring: extractNumber(sp['环节']) || 0,
status: extractSelect(sp['状态']) || '',
summary: extractRichText(sp['摘要']) || extractTitle(sp) || ''
};
} catch (_) { return null; }
}).filter(Boolean);
}
}
} catch (err) {
console.warn(' ⚠️ Notion 查询失败(' + dev.id + ': ' + err.message);
errorCount++;
}
}
// 读取天眼健康状态(从本地仓库文件)
try {
var skyeyeReportPath = path.join(CACHE_DIR, 'skyeye/latest-report.json');
if (fs.existsSync(skyeyeReportPath)) {
var report = JSON.parse(fs.readFileSync(skyeyeReportPath, 'utf-8'));
var subRepoData = report.sub_repos && report.sub_repos[dev.repo];
if (subRepoData) {
profile.skyeye_health = {
last_scan: report.timestamp,
status: subRepoData.status || 'unknown',
issues: subRepoData.issues || [],
warnings: subRepoData.warnings || []
};
} else {
profile.skyeye_health = {
last_scan: report.timestamp || '',
status: 'not_scanned',
issues: [],
warnings: []
};
}
}
} catch (_) {
// 天眼报告读取失败,不阻断同步
}
fs.writeFileSync(
path.join(dir, dev.id + '.json'),
JSON.stringify(profile, null, 2)
);
console.log(' ✅ ' + dev.id + ' → ' + dev.id + '.json');
syncCount++;
}
// 写入开发者索引
var index = DEVS.map(function(d) {
return { id: d.id, name: d.name, persona: d.persona, personaId: d.personaId };
});
fs.writeFileSync(path.join(dir, 'index.json'), JSON.stringify(index, null, 2));
console.log('✅ dev-profiles 同步完成 · 成功: ' + syncCount + ' · 失败: ' + errorCount);
}
// ====== 同步数据库快照 ======
async function syncDatabases() {
var dir = path.join(CACHE_DIR, 'databases');
fs.mkdirSync(dir, { recursive: true });
if (!NOTION_TOKEN) {
console.warn('⚠️ NOTION_TOKEN 未配置,跳过数据库同步');
return;
}
// Agent 注册表
if (DB_IDS.agentRegistry) {
try {
var result = await queryDatabase(DB_IDS.agentRegistry);
if (result.results) {
fs.writeFileSync(
path.join(dir, 'agent-registry.json'),
JSON.stringify({
synced: new Date().toISOString(),
count: result.results.length,
data: result.results
}, null, 2)
);
console.log('✅ agent-registry 同步完成 · ' + result.results.length + ' 条');
}
} catch (e) { console.error('❌ agent-registry 同步失败:', e.message); }
} else {
console.log(' NOTION_AGENT_REGISTRY_DB_ID 未配置,跳过 agent-registry');
}
// SYSLOG 最近50条
if (DB_IDS.syslogInbox) {
try {
var result = await queryDatabase(DB_IDS.syslogInbox, null,
[{ property: 'Created time', direction: 'descending' }], 50);
if (result.results) {
fs.writeFileSync(
path.join(dir, 'syslog-recent.json'),
JSON.stringify({
synced: new Date().toISOString(),
count: result.results.length,
data: result.results
}, null, 2)
);
console.log('✅ syslog-recent 同步完成 · ' + result.results.length + ' 条');
}
} catch (e) { console.error('❌ syslog-recent 同步失败:', e.message); }
} else {
console.log(' NOTION_SYSLOG_DB_ID 未配置,跳过 syslog-recent');
}
// 活跃工单
if (DB_IDS.ticketBook) {
try {
var result = await queryDatabase(DB_IDS.ticketBook, {
property: '状态',
select: { does_not_equal: '已完成' }
}, [{ property: 'Created time', direction: 'descending' }], 50);
if (result.results) {
fs.writeFileSync(
path.join(dir, 'tickets-active.json'),
JSON.stringify({
synced: new Date().toISOString(),
count: result.results.length,
data: result.results
}, null, 2)
);
console.log('✅ tickets-active 同步完成 · ' + result.results.length + ' 条');
}
} catch (e) { console.error('❌ tickets-active 同步失败:', e.message); }
} else {
console.log(' NOTION_TICKET_DB_ID 未配置,跳过 tickets-active');
}
// 维护日志
if (DB_IDS.maintenanceLog) {
try {
var result = await queryDatabase(DB_IDS.maintenanceLog, null,
[{ property: 'Created time', direction: 'descending' }], 20);
if (result.results) {
fs.writeFileSync(
path.join(dir, 'maintenance-log.json'),
JSON.stringify({
synced: new Date().toISOString(),
count: result.results.length,
data: result.results
}, null, 2)
);
console.log('✅ maintenance-log 同步完成 · ' + result.results.length + ' 条');
}
} catch (e) { console.error('❌ maintenance-log 同步失败:', e.message); }
} else {
console.log(' NOTION_MAINTENANCE_DB_ID 未配置,跳过 maintenance-log');
}
}
// ====== 同步活跃广播 ======
async function syncBroadcasts() {
var dir = path.join(CACHE_DIR, 'broadcasts');
fs.mkdirSync(dir, { recursive: true });
// 从本地广播目录读取活跃广播
var broadcastsOutbox = path.join(__dirname, '../../.github/broadcasts');
var activeBroadcasts = [];
try {
if (fs.existsSync(broadcastsOutbox)) {
var files = fs.readdirSync(broadcastsOutbox).filter(function(f) {
return f.endsWith('.json') && !f.startsWith('.');
});
for (var i = 0; i < files.length; i++) {
try {
var content = JSON.parse(fs.readFileSync(path.join(broadcastsOutbox, files[i]), 'utf-8'));
if (content.status !== 'completed' && content.status !== 'cancelled') {
activeBroadcasts.push(content);
}
} catch (_) {}
}
}
} catch (_) {}
fs.writeFileSync(
path.join(dir, 'active.json'),
JSON.stringify({
broadcasts: activeBroadcasts,
last_synced: new Date().toISOString()
}, null, 2)
);
console.log('✅ broadcasts 同步完成 · ' + activeBroadcasts.length + ' 条活跃广播');
}
// ====== 主入口 ======
async function main() {
console.log('═══════════════════════════════════════');
console.log('📡 Notion 全量缓存同步 · 开始');
console.log('═══════════════════════════════════════');
console.log('缓存目录: ' + CACHE_DIR);
console.log('Notion Token: ' + (NOTION_TOKEN ? '✅ 已配置' : '❌ 未配置'));
console.log('');
fs.mkdirSync(CACHE_DIR, { recursive: true });
await syncDevProfiles();
console.log('');
await syncDatabases();
console.log('');
await syncBroadcasts();
console.log('');
console.log('═══════════════════════════════════════');
console.log('📡 Notion 全量缓存同步 · 完成');
console.log('═══════════════════════════════════════');
}
main().catch(function(e) {
console.error('💥 同步失败:', e);
process.exit(1);
});