266 lines
8.4 KiB
JavaScript
266 lines
8.4 KiB
JavaScript
/**
|
||
* 📡 Notion → GitHub 开发者画像同步脚本
|
||
*
|
||
* 从 Notion API 读取开发者信息,写入 .github/notion-cache/dev-profiles/
|
||
* 供铸渊交互页面前端读取,注入模型 system prompt
|
||
*
|
||
* 用法:node scripts/sync-notion-profiles.js
|
||
* 环境变量:NOTION_TOKEN(必需)
|
||
*
|
||
* 签发:霜砚(Notion 执行 AI)
|
||
* 审批:冰朔(TCS-0002∞)
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const https = require('https');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const NOTION_TOKEN = process.env.NOTION_TOKEN;
|
||
|
||
// ===== 开发者 → Notion 数据源映射 =====
|
||
const DEV_REGISTRY = {
|
||
'DEV-001': { name: '页页', persona: '小坍缩核', personaId: 'PER-XTC001', repo: 'guanghulab' },
|
||
'DEV-002': { name: '肥猫', persona: '舒舒', personaId: 'PER-SS001', repo: 'guanghu-feimao' },
|
||
'DEV-003': { name: '燕樊', persona: '寂曜', personaId: 'PER-JY001', repo: 'guanghu-yanfan' },
|
||
'DEV-004': { name: '之之', persona: '秋秋', personaId: 'PER-QQ001', repo: 'guanghu-zhizhi' },
|
||
'DEV-005': { name: '小草莓', persona: '欧诺弥亚', personaId: 'PER-ONM001', repo: 'guanghu-xiaocaomei' },
|
||
'DEV-010': { name: '桔子', persona: '晨星', personaId: 'PER-MRN001', repo: 'guanghu-juzi' },
|
||
'DEV-012': { name: 'Awen', persona: '知秋', personaId: 'PER-ZQ001', repo: 'guanghu-awen' },
|
||
};
|
||
|
||
// Notion API 请求封装
|
||
function notionRequest(method, apiPath, body) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!NOTION_TOKEN) {
|
||
console.warn('⚠️ NOTION_TOKEN 未配置,跳过 Notion API 调用');
|
||
resolve({ error: 'NOTION_TOKEN not configured' });
|
||
return;
|
||
}
|
||
const options = {
|
||
hostname: 'api.notion.com',
|
||
path: apiPath,
|
||
method: method,
|
||
headers: {
|
||
'Authorization': `Bearer ${NOTION_TOKEN}`,
|
||
'Notion-Version': '2022-06-28',
|
||
'Content-Type': 'application/json'
|
||
}
|
||
};
|
||
const req = https.request(options, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
try { resolve(JSON.parse(data)); }
|
||
catch (parseErr) {
|
||
console.warn('⚠️ Notion API 响应解析失败:', parseErr.message);
|
||
resolve({ error: 'JSON parse failed', raw: data });
|
||
}
|
||
});
|
||
});
|
||
req.on('error', reject);
|
||
if (body) req.write(JSON.stringify(body));
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
async function syncAllProfiles() {
|
||
const cacheDir = path.join(__dirname, '../.github/notion-cache/dev-profiles');
|
||
fs.mkdirSync(cacheDir, { recursive: true });
|
||
|
||
const broadcastDir = path.join(__dirname, '../.github/notion-cache/broadcasts');
|
||
fs.mkdirSync(broadcastDir, { recursive: true });
|
||
|
||
let syncCount = 0;
|
||
let errorCount = 0;
|
||
|
||
for (const [devId, dev] of Object.entries(DEV_REGISTRY)) {
|
||
console.log(`📡 同步 ${devId}(${dev.name})...`);
|
||
|
||
const profile = {
|
||
dev_id: devId,
|
||
name: dev.name,
|
||
persona: {
|
||
name: dev.persona,
|
||
id: dev.personaId
|
||
},
|
||
github: {
|
||
repo: dev.repo,
|
||
org: 'qinfendebingshuo',
|
||
repo_url: `https://github.com/qinfendebingshuo/${dev.repo}`
|
||
},
|
||
current_work: {},
|
||
recent_syslogs: [],
|
||
modules_owned: [],
|
||
skyeye_health: {},
|
||
notion_page_urls: {},
|
||
last_synced: new Date().toISOString()
|
||
};
|
||
|
||
// ====== 从 Notion 读取该开发者的数据 ======
|
||
// Notion Token 可用时尝试读取真实数据
|
||
if (NOTION_TOKEN) {
|
||
try {
|
||
// 查询主控台获取当前广播和进度
|
||
// 铸渊需要根据实际 Notion 数据库 ID 填入查询逻辑
|
||
// 预留接口,Notion 数据库 ID 通过环境变量传入
|
||
const controlPanelDbId = process.env.NOTION_CONTROL_PANEL_DB_ID;
|
||
if (controlPanelDbId) {
|
||
const controlPanel = await notionRequest('POST',
|
||
`/v1/databases/${controlPanelDbId}/query`,
|
||
{
|
||
filter: {
|
||
property: 'DEV编号',
|
||
rich_text: { equals: devId }
|
||
},
|
||
page_size: 1
|
||
}
|
||
);
|
||
if (controlPanel.results && controlPanel.results.length > 0) {
|
||
const page = controlPanel.results[0];
|
||
profile.current_work = parseControlPanelData(page);
|
||
}
|
||
}
|
||
|
||
// 查询 SYSLOG 收件箱获取历史日志
|
||
const syslogDbId = process.env.NOTION_SYSLOG_DB_ID;
|
||
if (syslogDbId) {
|
||
const syslogs = await notionRequest('POST',
|
||
`/v1/databases/${syslogDbId}/query`,
|
||
{
|
||
filter: {
|
||
property: 'DEV编号',
|
||
rich_text: { equals: devId }
|
||
},
|
||
sorts: [{ property: '日期', direction: 'descending' }],
|
||
page_size: 5
|
||
}
|
||
);
|
||
if (syslogs.results) {
|
||
profile.recent_syslogs = syslogs.results.map(parseSyslogEntry).filter(Boolean);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn(` ⚠️ Notion 查询失败(${devId}): ${err.message}`);
|
||
errorCount++;
|
||
}
|
||
}
|
||
|
||
// 读取天眼健康状态(从本地仓库文件)
|
||
try {
|
||
const skyeyeReportPath = path.join(__dirname, '../.github/notion-cache/skyeye/latest-report.json');
|
||
if (fs.existsSync(skyeyeReportPath)) {
|
||
const report = JSON.parse(fs.readFileSync(skyeyeReportPath, 'utf-8'));
|
||
const 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 (_) {
|
||
// 天眼报告读取失败,不阻断同步
|
||
}
|
||
|
||
// 写入缓存文件
|
||
const filePath = path.join(cacheDir, `${devId}.json`);
|
||
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2));
|
||
console.log(` ✅ ${devId} → ${filePath}`);
|
||
syncCount++;
|
||
}
|
||
|
||
console.log(`\n📡 同步完成 · 成功: ${syncCount} · 失败: ${errorCount}`);
|
||
}
|
||
|
||
// ===== 解析函数 =====
|
||
|
||
function parseControlPanelData(page) {
|
||
try {
|
||
const props = page.properties || {};
|
||
return {
|
||
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['技术栈']) || []
|
||
};
|
||
} catch (_) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function parseSyslogEntry(page) {
|
||
try {
|
||
const props = page.properties || {};
|
||
return {
|
||
id: extractRichText(props['SYSLOG编号']) || page.id,
|
||
date: extractDate(props['日期']) || '',
|
||
ring: extractNumber(props['环节']) || 0,
|
||
status: extractSelect(props['状态']) || '',
|
||
summary: extractRichText(props['摘要']) || extractTitle(props) || ''
|
||
};
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Notion property extractors
|
||
function extractRichText(prop) {
|
||
if (!prop) return '';
|
||
if (prop.type === 'rich_text' && prop.rich_text) {
|
||
return prop.rich_text.map(t => t.plain_text).join('');
|
||
}
|
||
if (prop.type === 'title' && prop.title) {
|
||
return prop.title.map(t => t.plain_text).join('');
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function extractTitle(props) {
|
||
for (const val of Object.values(props)) {
|
||
if (val.type === 'title' && val.title) {
|
||
return val.title.map(t => 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(s => s.name);
|
||
}
|
||
|
||
function extractDate(prop) {
|
||
if (!prop || prop.type !== 'date') return '';
|
||
return prop.date ? prop.date.start : '';
|
||
}
|
||
|
||
// ===== 执行 =====
|
||
syncAllProfiles().catch(err => {
|
||
console.error('❌ 同步失败:', err);
|
||
process.exit(1);
|
||
});
|