guanghulab/scripts/push-broadcast.js

301 lines
11 KiB
JavaScript
Raw Normal View History

// scripts/push-broadcast.js
// 铸渊 · 广播推送脚本
//
// Notion 广播页面 → 飞书文档B追加到顶部
//
// 环境变量:
// NOTION_TOKEN Notion API token
// BROADCAST_PAGE_ID Notion 广播页面 ID由 dispatch payload 提供)
// FEISHU_APP_ID 飞书应用 App ID
// FEISHU_APP_SECRET 飞书应用 App Secret
// FEISHU_DOC_B_ID 飞书文档B的 document_id
'use strict';
const https = require('https');
// ══════════════════════════════════════════════════════════
// 常量
// ══════════════════════════════════════════════════════════
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const FEISHU_API_HOSTNAME = 'open.feishu.cn';
// ══════════════════════════════════════════════════════════
// HTTP 请求工具
// ══════════════════════════════════════════════════════════
function httpsRequest(options, body) {
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : null;
if (payload) {
options.headers = options.headers || {};
options.headers['Content-Length'] = Buffer.byteLength(payload);
}
const req = https.request(options, 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(`HTTP ${res.statusCode}: ${parsed.message || parsed.msg || data}`));
}
} catch (e) {
reject(new Error(`Parse error: ${data}`));
}
});
});
req.on('error', reject);
if (payload) req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// Notion API
// ══════════════════════════════════════════════════════════
function notionGet(endpoint, token) {
return httpsRequest({
hostname: NOTION_API_HOSTNAME,
port: 443,
path: endpoint,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Notion-Version': NOTION_VERSION,
},
});
}
async function getNotionPage(pageId, token) {
return notionGet('/v1/pages/' + pageId, token);
}
async function getNotionPageBlocks(pageId, token) {
const blocks = [];
let cursor = undefined;
do {
const qs = cursor ? '?start_cursor=' + cursor : '';
const result = await notionGet('/v1/blocks/' + pageId + '/children' + qs, token);
blocks.push(...(result.results || []));
cursor = result.has_more ? result.next_cursor : undefined;
} while (cursor);
return blocks;
}
// ══════════════════════════════════════════════════════════
// 飞书 API
// ══════════════════════════════════════════════════════════
async function getFeishuToken(appId, appSecret) {
const result = await httpsRequest({
hostname: FEISHU_API_HOSTNAME,
port: 443,
path: '/open-apis/auth/v3/tenant_access_token/internal',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}, { app_id: appId, app_secret: appSecret });
return result.tenant_access_token;
}
async function addFeishuDocBlocks(token, docId, parentBlockId, children, index) {
return httpsRequest({
hostname: FEISHU_API_HOSTNAME,
port: 443,
path: '/open-apis/docx/v1/documents/' + docId + '/blocks/' + parentBlockId + '/children',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
}, { children, index });
}
// ══════════════════════════════════════════════════════════
// Notion → 飞书 格式转换
// ══════════════════════════════════════════════════════════
function notionRichTextToFeishu(richTextArray) {
if (!richTextArray || richTextArray.length === 0) return [];
return richTextArray.map(rt => {
const elem = { content: rt.plain_text || '' };
if (rt.annotations) {
const style = {};
if (rt.annotations.bold) style.bold = true;
if (rt.annotations.italic) style.italic = true;
if (rt.annotations.strikethrough) style.strikethrough = true;
if (rt.annotations.underline) style.underline = true;
if (rt.annotations.code) style.inline_code = true;
if (Object.keys(style).length > 0) elem.text_element_style = style;
}
if (rt.href) {
elem.text_element_style = elem.text_element_style || {};
elem.text_element_style.link = { url: rt.href };
}
return { text_run: elem };
});
}
function notionBlockToFeishu(block) {
const type = block.type;
const data = block[type];
if (!data) return null;
switch (type) {
case 'paragraph':
return {
block_type: 2,
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_1':
return {
block_type: 4,
heading1: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_2':
return {
block_type: 5,
heading2: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'heading_3':
return {
block_type: 6,
heading3: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'bulleted_list_item':
return {
block_type: 12,
bullet: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'numbered_list_item':
return {
block_type: 13,
ordered: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
case 'to_do':
return {
block_type: 14,
todo: {
elements: notionRichTextToFeishu(data.rich_text),
style: { done: !!data.checked },
},
};
case 'code':
return {
block_type: 15,
code: {
elements: notionRichTextToFeishu(data.rich_text),
style: { language: 1 },
},
};
case 'divider':
return { block_type: 22, horizontal_rule: {} };
case 'callout':
return {
block_type: 2,
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
default:
if (data.rich_text) {
return {
block_type: 2,
text: { elements: notionRichTextToFeishu(data.rich_text), style: {} },
};
}
return null;
}
}
function extractBroadcastId(page) {
// 尝试从 Notion 页面属性中提取广播编号
const props = page.properties || {};
for (const key of Object.keys(props)) {
const prop = props[key];
if (prop.type === 'title' && prop.title) {
const text = prop.title.map(t => t.plain_text).join('');
const match = text.match(/BC-\w+-\d+-\d+/);
if (match) return match[0];
}
if (prop.type === 'rich_text' && prop.rich_text) {
const text = prop.rich_text.map(t => t.plain_text).join('');
const match = text.match(/BC-\w+-\d+-\d+/);
if (match) return match[0];
}
}
return null;
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
const notionToken = process.env.NOTION_TOKEN;
const pageId = process.env.BROADCAST_PAGE_ID;
const feishuAppId = process.env.FEISHU_APP_ID;
const feishuSecret = process.env.FEISHU_APP_SECRET;
const docBId = process.env.FEISHU_DOC_B_ID;
if (!notionToken || !pageId || !feishuAppId || !feishuSecret || !docBId) {
console.error('❌ 缺少必要环境变量');
console.error(' 需要: NOTION_TOKEN, BROADCAST_PAGE_ID, FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_DOC_B_ID');
process.exit(1);
}
// 1. 读取 Notion 广播页面元信息
console.log('📖 读取 Notion 广播页面...');
const page = await getNotionPage(pageId, notionToken);
const broadcastId = extractBroadcastId(page) || 'UNKNOWN';
const dateStr = new Date().toISOString().split('T')[0];
console.log(' → 广播编号: ' + broadcastId);
console.log(' → 签发日期: ' + dateStr);
// 2. 读取 Notion 广播页面内容
const blocks = await getNotionPageBlocks(pageId, notionToken);
console.log(' → 获取到 ' + blocks.length + ' 个内容块');
// 3. 转换为飞书文档格式
const feishuBlocks = blocks
.map(notionBlockToFeishu)
.filter(b => b !== null);
// 4. 构建追加内容(带分隔线+广播编号+日期头)
const headerBlocks = [
// 分隔线
{ block_type: 22, horizontal_rule: {} },
// 广播编号 + 日期
{
block_type: 5, // heading2
heading2: {
elements: [
{ text_run: { content: '📡 ' + broadcastId + ' · ' + dateStr } },
],
style: {},
},
},
];
const allBlocks = headerBlocks.concat(feishuBlocks);
console.log(' → 共 ' + allBlocks.length + ' 个飞书内容块(含头部)');
// 5. 获取飞书 token
console.log('🔑 获取飞书 access token...');
const feishuToken = await getFeishuToken(feishuAppId, feishuSecret);
// 6. 追加到飞书文档B顶部index=0 表示插入到最前面)
console.log('✍️ 追加广播到飞书文档B顶部...');
await addFeishuDocBlocks(feishuToken, docBId, docBId, allBlocks, 0);
console.log('✅ 广播推送完成: ' + broadcastId);
}
main().catch(err => {
console.error('❌ 推送失败: ' + err.message);
process.exit(1);
});