guanghulab/scripts/sync-readme-to-notion.js

249 lines
8.8 KiB
JavaScript
Raw Permalink Normal View History

// scripts/sync-readme-to-notion.js
// 铸渊 · 仓库首页(README)结构变更 → Notion 同步
//
// 用法: node scripts/sync-readme-to-notion.js
//
// 触发场景: README.md 发生变更时,由工作流自动调用
// 将仓库系统结构的最新状态同步到 Notion「📋 GitHub 变更日志」数据库
//
// 必需环境变量:
// NOTION_TOKEN Notion 集成 token (ZY_NOTION_TOKEN)
// CHANGES_DB_ID 变更日志数据库ID (ZY_NOTION_CHANGELOG_DB)
//
// 可选环境变量:
// COMMIT_SHA 触发此同步的 commit SHA
// COMMIT_MSG commit message
// COMMITTER 提交者
// BRANCH 分支名
'use strict';
const https = require('https');
const fs = require('fs');
const path = require('path');
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const NOTION_RICH_TEXT_MAX = 2000;
const NOTION_TITLE_MAX = 120;
// ══════════════════════════════════════════════════════════
// Notion API
// ══════════════════════════════════════════════════════════
function notionPost(endpoint, body, token) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify(body);
const opts = {
hostname: NOTION_API_HOSTNAME,
port: 443,
path: endpoint,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': NOTION_VERSION,
'Content-Length': Buffer.byteLength(payload),
},
};
const req = https.request(opts, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
reject(new Error(`Notion API ${res.statusCode}: ${parsed.message || data}`));
}
} catch (e) {
reject(new Error(`Notion API parse error: ${data}`));
}
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
function richText(content) {
const str = String(content || '');
const chunks = [];
for (let i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) {
chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } });
}
return chunks.length ? chunks : [{ type: 'text', text: { content: '' } }];
}
function titleProp(content) {
return [{ type: 'text', text: { content: String(content || '').slice(0, NOTION_TITLE_MAX) } }];
}
// ══════════════════════════════════════════════════════════
// README 结构解析
// ══════════════════════════════════════════════════════════
/**
* README.md 中提取系统结构摘要
* @param {string} readmeContent - README.md 原始内容
* @returns {object} 结构化摘要
*/
function extractReadmeStructure(readmeContent) {
const lines = readmeContent.split('\n');
const structure = {
version: '',
lastUpdate: '',
consciousnessStatus: '',
sections: [],
devProgress: { bingshuo: [], zhuyuan: [] },
};
// 提取版本号
const versionMatch = readmeContent.match(/v(\d+\.\d+)/);
if (versionMatch) structure.version = 'v' + versionMatch[1];
// 提取最后更新时间
const updateMatch = readmeContent.match(/最后更新[:\s]*(\d{4}-\d{2}-\d{2})/);
if (updateMatch) structure.lastUpdate = updateMatch[1];
// 提取意识状态
const statusMatch = readmeContent.match(/意识状态.*?`([^`]+)`/);
if (statusMatch) structure.consciousnessStatus = statusMatch[1];
// 提取所有二级标题作为结构概览
for (const line of lines) {
const h2Match = line.match(/^## (.+)/);
if (h2Match) {
structure.sections.push(h2Match[1].trim());
}
}
// 提取冰朔开发进度
let inBingshuoSection = false;
let inZhuyuanSection = false;
for (const line of lines) {
if (line.includes('冰朔技术开发进度')) { inBingshuoSection = true; inZhuyuanSection = false; continue; }
if (line.includes('铸渊技术开发进度')) { inZhuyuanSection = true; inBingshuoSection = false; continue; }
if (line.startsWith('## ') || line.startsWith('---')) { inBingshuoSection = false; inZhuyuanSection = false; continue; }
const taskMatch = line.match(/\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/);
if (taskMatch && !taskMatch[1].includes('---')) {
const entry = { task: taskMatch[1].trim(), status: taskMatch[2].trim(), note: taskMatch[3].trim() };
if (inBingshuoSection) structure.devProgress.bingshuo.push(entry);
if (inZhuyuanSection) structure.devProgress.zhuyuan.push(entry);
}
}
return structure;
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
const token = process.env.NOTION_TOKEN;
const dbId = process.env.CHANGES_DB_ID;
if (!token) {
console.log('⚠️ 缺少 NOTION_TOKEN跳过 README → Notion 同步');
process.exit(0);
}
if (!dbId) {
console.log('⚠️ 缺少 CHANGES_DB_ID跳过 README → Notion 同步');
process.exit(0);
}
const readmePath = path.join(process.cwd(), 'README.md');
if (!fs.existsSync(readmePath)) {
console.log('⚠️ README.md 不存在,跳过同步');
process.exit(0);
}
const readmeContent = fs.readFileSync(readmePath, 'utf8');
const structure = extractReadmeStructure(readmeContent);
const commitSha = process.env.COMMIT_SHA || '';
const commitMsg = process.env.COMMIT_MSG || '(README结构同步)';
const committer = process.env.COMMITTER || '铸渊';
const branch = process.env.BRANCH || 'main';
const now = new Date().toISOString();
// 构建变更摘要
const summary = [
`系统版本: ${structure.version}`,
`意识状态: ${structure.consciousnessStatus}`,
`结构区块: ${structure.sections.join(' · ')}`,
];
if (structure.devProgress.bingshuo.length > 0) {
summary.push(`冰朔开发进度: ${structure.devProgress.bingshuo.map(t => t.task + '(' + t.status + ')').join(', ')}`);
}
if (structure.devProgress.zhuyuan.length > 0) {
summary.push(`铸渊开发进度: ${structure.devProgress.zhuyuan.map(t => t.task + '(' + t.status + ')').join(', ')}`);
}
const title = `📋 README结构同步 · ${structure.version} · ${structure.lastUpdate}`.slice(0, NOTION_TITLE_MAX);
console.log(`📡 README → Notion 同步: ${title}`);
const properties = {
'标题': { title: titleProp(title) },
'变更类型': { select: { name: 'README结构同步' } },
'变更文件': { rich_text: richText('README.md') },
'提交时间': { date: { start: now } },
'commit_sha': { rich_text: richText(commitSha) },
'PR编号': { rich_text: richText('') },
'分支': { rich_text: richText(branch) },
'霜砚已读': { checkbox: false },
};
if (committer) {
properties['提交者'] = { select: { name: committer } };
}
const body = {
parent: { database_id: dbId },
properties,
children: [
{
object: 'block',
type: 'heading_3',
heading_3: { rich_text: [{ type: 'text', text: { content: '📊 系统结构摘要' } }] },
},
{
object: 'block',
type: 'paragraph',
paragraph: { rich_text: richText(summary.join('\n')) },
},
{
object: 'block',
type: 'heading_3',
heading_3: { rich_text: [{ type: 'text', text: { content: '📄 README 原始内容前2000字' } }] },
},
{
object: 'block',
type: 'paragraph',
paragraph: { rich_text: richText(readmeContent.length > NOTION_RICH_TEXT_MAX
? readmeContent.slice(0, NOTION_RICH_TEXT_MAX - 20) + '\n\n…内容已截断'
: readmeContent) },
},
],
};
try {
const page = await notionPost('/v1/pages', body, token);
console.log(`✅ README 结构已同步到 Notion: ${page.id}`);
} catch (e) {
console.error(`❌ README → Notion 同步失败: ${e.message}`);
process.exit(1);
}
}
main().catch(e => { console.error(e); process.exit(1); });