guanghulab/scripts/bridge/distribute.js
2026-05-10 13:12:44 +08:00

231 lines
7.7 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/bridge/distribute.js
// 🌉 桥接·广播分发到 IM
//
// 根据分发清单,查询人格体注册表获取通知渠道,
// 将 PDF 下载链接推送到飞书/钉钉/邮箱
//
// 环境变量:
// NOTION_TOKEN Notion API token查询人格体注册表
// DIST_MANIFEST dist-manifest.json 路径
// FEISHU_APP_ID 飞书 App ID
// FEISHU_APP_SECRET 飞书 App Secret
// DINGTALK_TOKEN 钉钉机器人 Token
// SMTP_USER 邮件发送者
// SMTP_PASS 邮件授权码
'use strict';
const https = require('https');
const fs = require('fs');
const path = require('path');
const OUTPUT_DIR = path.join('data', 'broadcasts', 'pdf');
// ══════════════════════════════════════════════════════════
// HTTP 请求辅助
// ══════════════════════════════════════════════════════════
function httpsPost(hostname, apiPath, body, headers) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify(body);
const opts = {
hostname,
port: 443,
path: apiPath,
method: 'POST',
headers: Object.assign({
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
}, headers || {}),
};
const req = https.request(opts, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
resolve({ status: res.statusCode, body: data });
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// 飞书分发
// ══════════════════════════════════════════════════════════
async function getFeishuToken() {
const appId = process.env.FEISHU_APP_ID;
const appSecret = process.env.FEISHU_APP_SECRET;
if (!appId || !appSecret) return null;
const result = await httpsPost(
'open.feishu.cn',
'/open-apis/auth/v3/tenant_access_token/internal',
{ app_id: appId, app_secret: appSecret }
);
const parsed = JSON.parse(result.body);
return parsed.tenant_access_token || null;
}
async function sendFeishuMessage(token, chatId, title, downloadUrl) {
const text = `📡 新广播已生成\n\n📋 ${title}\n\n📎 下载链接: ${downloadUrl}\n\n${new Date().toISOString()}`;
return httpsPost(
'open.feishu.cn',
'/open-apis/im/v1/messages?receive_id_type=chat_id',
{
receive_id: chatId,
msg_type: 'text',
content: JSON.stringify({ text }),
},
{ 'Authorization': 'Bearer ' + token }
);
}
// ══════════════════════════════════════════════════════════
// 钉钉分发
// ══════════════════════════════════════════════════════════
async function sendDingtalkMessage(webhookToken, title, downloadUrl) {
const text = `📡 新广播已生成\n\n📋 ${title}\n\n📎 下载链接: ${downloadUrl}`;
return httpsPost(
'oapi.dingtalk.com',
`/robot/send?access_token=${webhookToken}`,
{
msgtype: 'text',
text: { content: text },
}
);
}
// ══════════════════════════════════════════════════════════
// 邮件分发
// ══════════════════════════════════════════════════════════
async function sendEmailNotification(to, title, downloadUrl) {
const smtpUser = process.env.SMTP_USER;
const smtpPass = process.env.SMTP_PASS;
if (!smtpUser || !smtpPass) {
console.log(' ⚠️ SMTP 未配置,跳过邮件通知');
return;
}
try {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: 'smtp.qq.com',
port: 465,
secure: true,
auth: { user: smtpUser, pass: smtpPass },
});
await transporter.sendMail({
from: `"光湖广播" <${smtpUser}>`,
to: to,
subject: `📡 ${title}`,
html: `
<h2>📡 新广播已生成</h2>
<p><strong>标题:</strong> ${title}</p>
<p><strong>下载链接:</strong> <a href="${downloadUrl}">${downloadUrl}</a></p>
<p><em>— 光湖广播系统 · guanghulab.com</em></p>
`,
});
console.log(` ✅ 邮件已发送: ${to}`);
} catch (e) {
console.error(` ❌ 邮件发送失败: ${e.message}`);
}
}
// ══════════════════════════════════════════════════════════
// 主逻辑
// ══════════════════════════════════════════════════════════
async function main() {
const manifestFile = process.env.DIST_MANIFEST ||
path.join(OUTPUT_DIR, 'dist-manifest.json');
if (!fs.existsSync(manifestFile)) {
console.log('📭 无 dist-manifest.json跳过分发');
process.exit(0);
}
const items = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
if (items.length === 0) {
console.log('📭 分发列表为空,跳过');
process.exit(0);
}
console.log(`📢 开始分发 ${items.length} 条广播…`);
// 飞书 Token一次性获取
let feishuToken = null;
if (process.env.FEISHU_APP_ID) {
try {
feishuToken = await getFeishuToken();
if (feishuToken) console.log(' 🔑 飞书 Token 获取成功');
} catch (e) {
console.error(` ⚠️ 飞书 Token 获取失败: ${e.message}`);
}
}
const dingtalkToken = process.env.DINGTALK_TOKEN;
let distributed = 0;
for (const item of items) {
const title = item.title || '光湖广播';
const downloadUrl = item.download_url || '';
if (!downloadUrl) {
console.log(` ⚠️ ${title}: 无下载链接,跳过`);
continue;
}
console.log(`\n📡 分发: ${title}`);
// 飞书分发(广播到默认群)
if (feishuToken && process.env.FEISHU_BROADCAST_CHAT_ID) {
try {
await sendFeishuMessage(feishuToken, process.env.FEISHU_BROADCAST_CHAT_ID, title, downloadUrl);
console.log(' ✅ 飞书群已推送');
} catch (e) {
console.error(` ❌ 飞书推送失败: ${e.message}`);
}
}
// 钉钉分发
if (dingtalkToken) {
try {
await sendDingtalkMessage(dingtalkToken, title, downloadUrl);
console.log(' ✅ 钉钉已推送');
} catch (e) {
console.error(` ❌ 钉钉推送失败: ${e.message}`);
}
}
// 邮件分发(发送给冰朔)
if (process.env.SMTP_USER && process.env.ALERT_EMAIL) {
await sendEmailNotification(process.env.ALERT_EMAIL, title, downloadUrl);
}
distributed++;
}
console.log(`\n✅ 分发完成 · ${distributed}/${items.length} 条广播已推送`);
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT,
`distributed_count=${distributed}\n`
);
}
}
main().catch(e => { console.error(e); process.exit(1); });