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