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