const fs = require('fs') const path = require('path') const rootDir = path.resolve(__dirname, '..') const releaseDir = path.join(rootDir, 'release') const contextPath = path.join(releaseDir, 'release-context.json') const releaseBodyPath = path.join(releaseDir, 'release-body.md') const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '' const TELEGRAM_CHAT_IDS = String(process.env.TELEGRAM_CHAT_IDS || '') .split(',') .map((item) => item.trim()) .filter(Boolean) const TELEGRAM_RELEASE_COVER_URL = process.env.TELEGRAM_RELEASE_COVER_URL || '' const mode = process.env.TELEGRAM_NOTIFY_MODE || 'success' function escapeHtml(text) { return String(text || '') .replace(/&/g, '&') .replace(//g, '>') } function markdownToPlainSummary(markdown) { return String(markdown || '') .replace(/^#+\s*/gm, '') .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') .replace(/[*_`>-]/g, '') .replace(/\n{3,}/g, '\n\n') .trim() } function getContext() { if (!fs.existsSync(contextPath)) return null return JSON.parse(fs.readFileSync(contextPath, 'utf8')) } function getReleaseBody() { if (!fs.existsSync(releaseBodyPath)) return '' return fs.readFileSync(releaseBodyPath, 'utf8') } function buildButtons(version) { const releaseUrl = `https://github.com/ILoveBingLu/CipherTalk/releases/tag/v${version}` const installerUrl = `https://github.com/ILoveBingLu/CipherTalk/releases/download/v${version}/CipherTalk-${version}-Setup.exe` return { inline_keyboard: [ [ { text: '📦 查看 Release', url: releaseUrl }, { text: '⬇️ 下载安装包', url: installerUrl } ] ] } } function buildSuccessMessage(context, releaseBody) { const version = context?.version || process.env.RELEASE_VERSION || 'unknown' const blockedVersions = context?.forceUpdate?.blockedVersions || [] const minimumSupportedVersion = context?.forceUpdate?.minimumSupportedVersion || '' const hasForceUpdate = Boolean(minimumSupportedVersion || blockedVersions.length > 0) const summary = markdownToPlainSummary(releaseBody) .split('\n') .filter(Boolean) .slice(0, 8) .join('\n') const thanks = [] const primaryLogins = new Set(['ILoveBingLu']) const primaryNames = new Set(['ILoveBingLu', 'BingLu', 'ILoveBinglu']) for (const pr of context?.pullRequests || []) { if (pr?.authorLogin && !primaryLogins.has(pr.authorLogin)) { thanks.push(`🙏 感谢 @${pr.authorLogin} 提交 PR #${pr.number}`) } } for (const commit of context?.commits || []) { const hasPrRef = /#(\d+)/.test(commit.subject || '') const authorName = String(commit.authorName || '').trim() if (!hasPrRef && authorName && !primaryNames.has(authorName)) { thanks.push(`🙏 感谢 ${authorName} 提交改动《${commit.subject}》`) } } const lines = [ `🚀 CipherTalk v${escapeHtml(version)} 已发布`, '', '📝 本次更新摘要', escapeHtml(summary || '本次版本已完成发布,可点击下方按钮查看完整说明。'), ] if (hasForceUpdate) { lines.push('', '⚠️ 强制更新提醒') if (minimumSupportedVersion) { lines.push(`- 最低安全版本:${escapeHtml(minimumSupportedVersion)}`) } if (blockedVersions.length) { lines.push(`- 封禁版本:${escapeHtml(blockedVersions.join(', '))}`) } } lines.push('', '🔗 相关链接', `- GitHub Release:查看发布说明`) if (thanks.length) { lines.push('', '🌟 感谢贡献者', ...thanks.map((line) => escapeHtml(line))) } return lines.join('\n') } function buildFailureMessage() { const workflowUrl = process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` : '' const version = process.env.RELEASE_VERSION || process.env.GITHUB_REF_NAME || 'unknown' const lines = [ `❌ CipherTalk ${escapeHtml(version)} 发布失败`, '', '请尽快检查 GitHub Actions 日志。' ] if (workflowUrl) { lines.push('', `🔗 查看失败日志`) } return lines.join('\n') } async function sendTelegramMessage(chatId, text, replyMarkup) { const body = { chat_id: chatId, text, parse_mode: 'HTML', disable_web_page_preview: false, reply_markup: replyMarkup } const endpoint = TELEGRAM_RELEASE_COVER_URL ? 'sendPhoto' : 'sendMessage' const payload = TELEGRAM_RELEASE_COVER_URL ? { chat_id: chatId, photo: TELEGRAM_RELEASE_COVER_URL, caption: text, parse_mode: 'HTML', reply_markup: replyMarkup } : body const response = await fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) if (!response.ok) { const raw = await response.text() throw new Error(`Telegram 发送失败 (${response.status}): ${raw}`) } } async function main() { if (!TELEGRAM_BOT_TOKEN || TELEGRAM_CHAT_IDS.length === 0) { console.log('ℹ️ Telegram 未配置,跳过通知') return } const context = getContext() const releaseBody = getReleaseBody() const version = context?.version || process.env.RELEASE_VERSION || 'unknown' const text = mode === 'failure' ? buildFailureMessage() : buildSuccessMessage(context, releaseBody) const replyMarkup = mode === 'failure' ? undefined : buildButtons(version) for (const chatId of TELEGRAM_CHAT_IDS) { await sendTelegramMessage(chatId, text, replyMarkup) } console.log(`✅ 已发送 Telegram 通知到 ${TELEGRAM_CHAT_IDS.length} 个目标`) } main().catch((error) => { console.error('❌ Telegram 通知失败:', error) process.exit(1) })