249 lines
8.8 KiB
JavaScript
249 lines
8.8 KiB
JavaScript
// 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); });
|