WechatOnCloud/.github/scripts/tg-notify.mjs
Kris e35f1d64cf feat: 初始化项目,添加完整的微信云部署方案与面板实现
包含以下核心内容:
1.  完整的Docker容器化部署方案,支持多架构amd64/arm64
2.  自研Web管理面板,包含登录认证、实例管理、权限控制
3.  微信实例容器镜像,内置中文字体与输入法修复
4.  飞牛OS应用打包适配
5.  完整的文档与运维指南
2026-07-05 03:50:30 +08:00

181 lines
8.0 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

// 发布 / 新 issue → Telegram 群通知。把 GitHub Markdown 转成 Telegram 支持的 HTML 再发,
// 失败则回退纯文本,保证必达。无服务器,跑在 GitHub Actionstelegram-notify.yml
//
// 事件来源env EVENT
// release —— 发布事件,用 release 的 payload
// issues —— 新 issue 事件,用 issue 的 payload
// workflow_dispatch —— 手动触发,拉「最新 release」渲染发送用于测试渲染效果
const TG = process.env.TG_TOKEN;
const CHAT = process.env.TG_CHAT;
const EVENT = process.env.EVENT || '';
if (!TG || !CHAT) {
console.log('TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID 未配置,跳过');
process.exit(0);
}
const esc = (s) => (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// 行内 Markdown → Telegram HTML在已 HTML-escape 的文本上运行)
function inlineMd(s) {
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, t, u) => `<a href="${u.replace(/"/g, '&quot;')}">${t}</a>`);
s = s.replace(/\*\*([^*]+?)\*\*/g, '<b>$1</b>');
s = s.replace(/__([^_]+?)__/g, '<b>$1</b>');
s = s.replace(/(^|[^*])\*([^*\n]+?)\*(?!\*)/g, '$1<i>$2</i>');
return s;
}
// GitHub Markdown → Telegram HTML。Telegram 不支持标题/表格/列表标记,转成粗体/·/• 等。
function mdToHtml(md) {
const blocks = [];
md = md.replace(/```[^\n]*\n([\s\S]*?)```/g, (_, code) => {
blocks.push('<pre>' + esc(code.replace(/\n$/, '')) + '</pre>');
return `B${blocks.length - 1}`;
});
const codes = [];
md = md.replace(/`([^`\n]+)`/g, (_, c) => {
codes.push('<code>' + esc(c) + '</code>');
return `C${codes.length - 1}`;
});
const out = [];
for (const line of md.split('\n')) {
// 表格分隔行(|---|---|)跳过
if (/^\s*\|[\s:|-]+\|\s*$/.test(line) && line.includes('-')) continue;
// 分割线
if (/^\s*([-*_])\1{2,}\s*$/.test(line)) continue;
let m;
if ((m = line.match(/^\s{0,3}#{1,6}\s+(.*)$/))) {
out.push('<b>' + inlineMd(esc(m[1])) + '</b>');
} else if ((m = line.match(/^\s*>\s?(.*)$/))) {
out.push('▎ ' + inlineMd(esc(m[1])));
} else if ((m = line.match(/^(\s*)[-*]\s+(.*)$/))) {
out.push(m[1] + '• ' + inlineMd(esc(m[2])));
} else if ((m = line.match(/^(\s*)(\d+)\.\s+(.*)$/))) {
out.push(m[1] + m[2] + '. ' + inlineMd(esc(m[3])));
} else if (/^\s*\|.*\|\s*$/.test(line)) {
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => inlineMd(esc(c.trim())));
out.push(cells.filter(Boolean).join(' · '));
} else {
out.push(inlineMd(esc(line)));
}
}
let res = out.join('\n').replace(/C(\d+)/g, (_, i) => codes[+i]).replace(/B(\d+)/g, (_, i) => blocks[+i]);
return res.replace(/\n{3,}/g, '\n\n').trim();
}
const stripTags = (s) => s.replace(/<[^>]+>/g, '').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/&quot;/g, '"');
async function ghLatestRelease() {
const repo = process.env.REPO;
const headers = { accept: 'application/vnd.github+json', 'user-agent': 'woc-tg' };
if (process.env.GH_TOKEN) headers.authorization = `Bearer ${process.env.GH_TOKEN}`;
const r = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { headers });
if (!r.ok) throw new Error('latest release ' + r.status);
return r.json();
}
async function tgCall(method, params) {
const r = await fetch(`https://api.telegram.org/bot${TG}/${method}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(params),
});
return r.json();
}
const send = (text, mode) =>
tgCall('sendMessage', { chat_id: CHAT, text, parse_mode: mode || undefined, disable_web_page_preview: false });
// 发布通知自动置顶:置顶新消息,并取消「上一个 release」的置顶。
// 用 getChat 读出群当前置顶消息(即上一个 release无需任何持久化存储。
// 失败不影响通知本身(需机器人是群管理员且有「置顶消息」权限)。
async function pinAndUnpinPrev(newId) {
// 先读当前置顶(= 上一个 release 的置顶消息),必须在置顶新消息之前
const chat = await tgCall('getChat', { chat_id: CHAT });
const prevPinned = chat?.result?.pinned_message?.message_id;
// 置顶新消息(静音,避免二次提醒——消息本身已经提醒过)
const pin = await tgCall('pinChatMessage', { chat_id: CHAT, message_id: newId, disable_notification: true });
if (!pin.ok) {
console.error('置顶失败(机器人需为群管理员且有「置顶消息」权限):', JSON.stringify(pin));
return;
}
console.log('已置顶新发布消息');
if (prevPinned && prevPinned !== newId) {
const unpin = await tgCall('unpinChatMessage', { chat_id: CHAT, message_id: prevPinned });
console.log(unpin.ok ? '已取消上一个 release 的置顶' : '取消旧置顶失败:' + JSON.stringify(unpin));
}
}
(async () => {
// PR 的评论也走 issue_comment 事件;只通知真正的 issue 回复C_PR 非空 = PR 评论,跳过)
if (EVENT === 'issue_comment' && (process.env.C_PR || '').trim()) {
console.log('PR 评论,跳过');
return;
}
let htmlText;
let plainText;
if (EVENT === 'issues') {
const num = process.env.I_NUM || '';
const title = process.env.I_TITLE || '';
const url = process.env.I_URL || '';
const user = process.env.I_USER || '';
const body = (process.env.I_BODY || '').slice(0, 800);
htmlText = `🐛 <b>新反馈 Issue #${esc(num)}${esc(title)}</b>\n👤 by ${esc(user)}\n\n${mdToHtml(body)}\n\n🔗 ${esc(url)}`;
plainText = `🐛 新反馈 Issue #${num}${title}\n👤 by ${user}\n\n${body}\n\n🔗 ${url}`;
} else if (EVENT === 'issue_comment') {
// issue 新回复:简短推送
const num = process.env.C_NUM || '';
const title = process.env.C_TITLE || '';
const url = process.env.C_URL || '';
const user = process.env.C_USER || '';
const body = (process.env.C_BODY || '').slice(0, 400);
htmlText = `💬 <b>Issue #${esc(num)} 新回复</b> · ${esc(title)}\n👤 ${esc(user)}\n\n${mdToHtml(body)}\n\n🔗 ${esc(url)}`;
plainText = `💬 Issue #${num} 新回复 · ${title}\n👤 ${user}\n\n${body}\n\n🔗 ${url}`;
} else {
// release 或 手动触发(拉最新 release
let tag = process.env.R_TAG || '';
let name = process.env.R_NAME || '';
let url = process.env.R_URL || '';
let body = process.env.R_BODY || '';
if (!tag) {
const rel = await ghLatestRelease();
tag = rel.tag_name || '';
name = rel.name || '';
url = rel.html_url || '';
body = rel.body || '';
}
body = body.slice(0, 3500);
const titleTxt = `🚀 云微 WechatOnCloud ${tag}${name && name !== tag ? ' — ' + name : ''} 已发布`;
htmlText = `<b>${esc(titleTxt)}</b>\n\n${mdToHtml(body)}\n\n🔗 ${esc(url)}\n⬆️ 升级: <code>docker compose pull &amp;&amp; docker compose up -d</code>`;
plainText = `${titleTxt}\n\n${body}\n\n🔗 ${url}\n⬆️ 升级: docker compose pull && docker compose up -d`;
}
// 发送HTML失败回退纯文本
let sent = await send(htmlText, 'HTML');
if (sent.ok) {
console.log('sent (HTML)');
} else {
console.error('HTML 发送失败,回退纯文本:', JSON.stringify(sent));
sent = await send(plainText.length > 4000 ? plainText.slice(0, 4000) : plainText);
if (!sent.ok) {
console.error('纯文本也失败:', JSON.stringify(sent));
process.exit(1);
}
console.log('sent (plain fallback)');
}
// 发布通知release 事件或手动触发)自动置顶 + 取消上一个 release 的置顶issue / 回复通知不置顶。
if (EVENT !== 'issues' && EVENT !== 'issue_comment' && sent.result?.message_id) {
try {
await pinAndUnpinPrev(sent.result.message_id);
} catch (e) {
console.error('置顶流程出错(已忽略,不影响通知):', e?.message || e);
}
}
})().catch((e) => {
console.error(e);
process.exit(1);
});