guanghulab/scripts/sync-readme-to-notion.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

249 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

// 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); });