/** * sync-dev-status.js * 从 Notion 主控台同步 dev-status.json 到 GitHub 仓库 * 机制:霜砚签发制 v1.0 (shuangyan-signed-v1.0) * 指令溯源:ZY-DEVSYNC-REBUILD-2026-0325-001 * * 使用方式: * NOTION_TOKEN=xxx DEV_STATUS_DB_ID=xxx node scripts/sync-dev-status.js * * 如果 Notion 环境变量不可用,将基于本地文件刷新 last_sync 时间戳 */ const fs = require('fs'); const path = require('path'); const https = require('https'); const crypto = require('crypto'); const DEV_STATUS_PATH = path.join('.github', 'persona-brain', 'dev-status.json'); const ROOT_STATUS_PATH = 'dev-status.json'; const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; const DEV_STATUS_DB_ID = process.env.DEV_STATUS_DB_ID || ''; const INSTRUCTION_REF = 'ZY-DEVSYNC-REBUILD-2026-0325-001'; const SYNC_MECHANISM = 'shuangyan-signed-v1.0'; // ── Notion API 请求封装 ── function notionRequest(method, endpoint, body) { return new Promise((resolve, reject) => { const data = body ? JSON.stringify(body) : null; const options = { hostname: 'api.notion.com', path: `/v1/${endpoint}`, method, headers: { 'Authorization': `Bearer ${NOTION_TOKEN}`, 'Notion-Version': '2022-06-28', 'Content-Type': 'application/json', }, }; if (data) options.headers['Content-Length'] = Buffer.byteLength(data); const req = https.request(options, (res) => { let result = ''; res.on('data', (chunk) => result += chunk); res.on('end', () => { try { resolve({ status: res.statusCode, data: JSON.parse(result) }); } catch { resolve({ status: res.statusCode, data: result }); } }); }); req.on('error', reject); if (data) req.write(data); req.end(); }); } // ── 从 Notion 数据库查询开发者状态 ── async function fetchDevStatusFromNotion() { if (!NOTION_TOKEN || !DEV_STATUS_DB_ID) { console.log('⚠️ NOTION_TOKEN 或 DEV_STATUS_DB_ID 未配置,跳过 Notion 同步'); return null; } console.log('📡 正在从 Notion 查询开发者状态...'); const response = await notionRequest('POST', `databases/${DEV_STATUS_DB_ID}/query`, { sorts: [{ property: '编号', direction: 'ascending' }], }); if (response.status !== 200) { console.error(`❌ Notion API 返回 ${response.status}:`, JSON.stringify(response.data).slice(0, 500)); return null; } const pages = response.data.results || []; if (pages.length === 0) { console.log('⚠️ Notion 数据库为空,跳过同步'); return null; } console.log(`✅ 从 Notion 获取到 ${pages.length} 条开发者记录`); // 解析 Notion 页面属性为 dev-status 格式(霜砚签发制格式) const team = pages.map(page => { const props = page.properties || {}; const syslogProp = props['最后SYSLOG']; const lastSyslog = getNotionDate(syslogProp) || getNotionText(syslogProp); return { dev_id: getNotionText(props['编号']) || getNotionTitle(props['Name']) || '', name: getNotionText(props['昵称']) || getNotionTitle(props['Name']) || '', module: getNotionText(props['当前模块']) || '', status: mapNotionStatus(getNotionSelect(props['状态'])), current_task: getNotionText(props['当前广播']) || getNotionText(props['当前进度']) || '', streak: getNotionNumber(props['连胜']) || 0, waiting_for: getNotionText(props['等待项']) || null, last_syslog: lastSyslog || null, alert: computeAlert(lastSyslog), }; }).filter(d => d.dev_id); return team; } // ── 根据 last_syslog 计算告警级别 ── function computeAlert(lastSyslog) { if (!lastSyslog) return null; try { const last = new Date(lastSyslog); if (isNaN(last.getTime())) return null; const diffHours = (Date.now() - last.getTime()) / (1000 * 60 * 60); if (diffHours > 168) return 'over_7d'; if (diffHours > 72) return 'over_72h'; return null; } catch { return null; } } // ── Notion 属性解析辅助函数 ── function getNotionText(prop) { if (!prop) return ''; if (prop.type === 'rich_text') return (prop.rich_text || []).map(t => t.plain_text).join(''); if (prop.type === 'title') return (prop.title || []).map(t => t.plain_text).join(''); return ''; } function getNotionTitle(prop) { if (!prop) return ''; if (prop.type === 'title') return (prop.title || []).map(t => t.plain_text).join(''); return ''; } function getNotionSelect(prop) { if (!prop || prop.type !== 'select') return ''; return prop.select?.name || ''; } function getNotionNumber(prop) { if (!prop || prop.type !== 'number') return 0; return prop.number || 0; } function getNotionDate(prop) { if (!prop || prop.type !== 'date') return ''; return prop.date?.start || ''; } function mapNotionStatus(status) { const map = { '活跃': 'active', '等待SYSLOG': 'waiting_syslog', '等待广播': 'waiting_broadcast', '暂停': 'paused', '已毕业': 'graduated', }; return map[status] || status || 'active'; } // ── 生成汇总信息 ── function generateSummary(team) { const activeCount = team.filter(d => d.status === 'active').length; const waitingSyslog = team.filter(d => d.status === 'waiting_syslog').length; const topStreakDev = team.length > 0 ? team.reduce((max, d) => d.streak > max.streak ? d : max, team[0]) : null; const alerts = team .filter(d => d.alert || (d.waiting_for && typeof d.waiting_for === 'string' && d.waiting_for.includes('⚠️'))) .map(d => { if (d.alert === 'over_7d') return `${d.dev_id} ${d.name}: 超7天未回执`; if (d.alert === 'over_72h') return `${d.dev_id} ${d.name}: 超72h未回执`; return `${d.dev_id} ${d.name}: ${d.waiting_for}`; }); return { total_devs: team.length, active_waiting_syslog: waitingSyslog, active_normal: activeCount, top_streak: topStreakDev ? `${topStreakDev.dev_id} ${topStreakDev.name} ${topStreakDev.streak}连胜` : '无', alerts, }; } // ── 计算签名哈希 ── function computeSignatureHash(developers) { const devJson = JSON.stringify(developers); const hash = crypto.createHash('sha256').update(devJson).digest('hex'); return `sha256:${hash}`; } // ── 主流程 ── async function main() { const now = new Date(); const bjTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); const timestamp = bjTime.toISOString().replace('Z', '+08:00'); const syncDate = timestamp.split('T')[0]; console.log('═══ dev-status.json 同步 · 霜砚签发制 v1.0 ═══'); console.log(`同步时间:${timestamp}`); // 读取当前状态 let currentStatus = {}; try { currentStatus = JSON.parse(fs.readFileSync(DEV_STATUS_PATH, 'utf8')); } catch { console.log('⚠️ 当前 dev-status.json 不存在或格式错误,将创建新文件'); } // 尝试从 Notion 获取数据 const notionTeam = await fetchDevStatusFromNotion(); let team; if (notionTeam) { // Notion 同步成功 team = notionTeam; console.log(`✅ Notion 同步成功 · ${notionTeam.length} 位开发者`); } else { // Notion 不可用时,保持现有数据 console.log('📌 Notion 不可用,保留当前数据,更新同步时间戳'); team = currentStatus.developers || currentStatus.team || []; } // 组装霜砚签发包 const signatureHash = computeSignatureHash(team); const syncPackage = { // ─── 签发元数据 ─── signed_by: 'AG-SY-01', signed_at: timestamp, sync_date: syncDate, instruction_ref: INSTRUCTION_REF, // ─── 同步元数据 ─── last_sync: timestamp, sync_source: notionTeam ? 'notion-mastercontrol-v3' : (currentStatus.sync_source || 'local-refresh'), sync_mechanism: SYNC_MECHANISM, // ─── 团队数据 ─── team_count: team.length, developers: team, // ─── 汇总 ─── summary: generateSummary(team), // ─── 签名校验 ─── signature_hash: signatureHash, }; // 写入 persona-brain 版本 fs.writeFileSync(DEV_STATUS_PATH, JSON.stringify(syncPackage, null, 2) + '\n'); console.log(`✅ 已写入 ${DEV_STATUS_PATH}`); // 同步写入根目录版本 fs.writeFileSync(ROOT_STATUS_PATH, JSON.stringify(syncPackage, null, 2) + '\n'); console.log(`✅ 已写入 ${ROOT_STATUS_PATH}`); // 输出摘要 const summary = syncPackage.summary; console.log('\n📊 团队状态摘要:'); console.log(` 总开发者: ${summary.total_devs}`); console.log(` 等待SYSLOG: ${summary.active_waiting_syslog}`); console.log(` 最高连胜: ${summary.top_streak}`); console.log(` 签名哈希: ${signatureHash.slice(0, 19)}...`); if (summary.alerts?.length > 0) { console.log(` ⚠️ 告警: ${summary.alerts.length} 条`); summary.alerts.forEach(a => console.log(` - ${a}`)); } console.log(`\n✅ dev-status.json 同步完成 · 霜砚签发制 v1.0 · ${timestamp}`); } main().catch(err => { console.error('❌ 同步失败:', err.message); process.exit(1); });