From fbf916880b444f747480a8f18545122898e8cd16 Mon Sep 17 00:00:00 2001 From: ILoveBingLu Date: Thu, 2 Apr 2026 03:09:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E6=8E=A5=E5=85=A5=20AI=20=E4=B8=8E=20Telegram=20?= =?UTF-8?q?=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 59 ++++++++- README.md | 17 +++ package.json | 3 + scripts/README.md | 39 +++++- scripts/generate-release-body.js | 194 ++++++++++++++++++++++++++++ scripts/generate-release-context.js | 148 +++++++++++++++++++++ scripts/send-telegram-release.js | 175 +++++++++++++++++++++++++ 7 files changed, 628 insertions(+), 7 deletions(-) create mode 100644 scripts/generate-release-body.js create mode 100644 scripts/generate-release-context.js create mode 100644 scripts/send-telegram-release.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64e2291..24dc100 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,12 @@ jobs: runs-on: windows-latest environment: 软件发布 env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GLM_KEY: ${{ secrets.GLM_KEY }} + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TELEGRAM_CHAT_IDS: ${{ vars.TELEGRAM_CHAT_IDS }} + TELEGRAM_RELEASE_COVER_URL: ${{ vars.TELEGRAM_RELEASE_COVER_URL }} FORCE_UPDATE_MIN_VERSION: ${{ vars.FORCE_UPDATE_MIN_VERSION }} FORCE_UPDATE_BLOCKED_VERSIONS: ${{ vars.FORCE_UPDATE_BLOCKED_VERSIONS }} FORCE_UPDATE_TITLE: ${{ vars.FORCE_UPDATE_TITLE }} @@ -25,10 +30,10 @@ jobs: R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 22.12.0 cache: npm @@ -63,6 +68,14 @@ jobs: - name: Generate force update manifest run: npm run build:force-update-manifest + - name: Generate release context + env: + RELEASE_TAG: ${{ github.ref_name }} + run: npm run build:release-context + + - name: Generate AI release body + run: npm run build:release-body + - name: Ensure AWS CLI shell: pwsh run: | @@ -88,12 +101,20 @@ jobs: Write-Error "force-update.json not found" exit 1 } + if (-not (Test-Path "release/release-context.json")) { + Write-Error "release-context.json not found" + exit 1 + } + if (-not (Test-Path "release/release-body.md")) { + Write-Error "release-body.md not found" + exit 1 + } - name: Create or update GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.5.0 with: tag_name: ${{ github.ref_name }} - generate_release_notes: true + body_path: release/release-body.md fail_on_unmatched_files: false files: | release/CipherTalk-${{ steps.version.outputs.version }}-Setup.exe @@ -133,3 +154,33 @@ jobs: aws s3 cp "release/$currentInstaller" "$bucket/$currentInstaller" --endpoint-url $endpoint aws s3 cp "release/latest.yml" "$bucket/latest.yml" --endpoint-url $endpoint aws s3 cp "release/force-update.json" "$bucket/force-update.json" --endpoint-url $endpoint + + - name: Notify Telegram success + env: + RELEASE_VERSION: ${{ steps.version.outputs.version }} + TELEGRAM_NOTIFY_MODE: success + run: npm run notify:telegram + + notify-failure: + runs-on: windows-latest + environment: 软件发布 + if: failure() + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TELEGRAM_CHAT_IDS: ${{ vars.TELEGRAM_CHAT_IDS }} + TELEGRAM_RELEASE_COVER_URL: ${{ vars.TELEGRAM_RELEASE_COVER_URL }} + RELEASE_VERSION: ${{ github.ref_name }} + TELEGRAM_NOTIFY_MODE: failure + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.12.0 + cache: npm + + - name: Notify Telegram failure + run: npm run notify:telegram diff --git a/README.md b/README.md index e13d160..24043ec 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,23 @@ git push origin v2.2.14 - GitHub Release:安装包、`latest.yml`、`force-update.json` - Cloudflare R2:安装包、`latest.yml`、`force-update.json` +- GitHub Release body:由工作流自动生成标准化中文版本说明 +- Telegram:自动推送机器人风格的发布通知(支持多个频道/群) + +AI 生成说明的密钥来源: + +- GitHub Environment `软件发布` +- Secret 名称:`GLM_KEY` + +若 AI 不可用,工作流会自动回退为模板化 Release body,不影响正式发布。 + +如配置 Telegram Bot,发布成功后还会自动发送: + +- AI 摘要版发布通知 +- 强制更新提醒(如存在) +- Release / 安装包按钮链接 + +若发布失败,也会自动发送失败通知和 Actions 日志链接。 ### v1 工具 diff --git a/package.json b/package.json index d4f35c5..f58175d 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "build": "tsc && vite build && electron-builder && node scripts/add-size-to-yml.js", "build:mcp": "tsc && vite build", "build:force-update-manifest": "node scripts/generate-force-update-manifest.js", + "build:release-context": "node scripts/generate-release-context.js", + "build:release-body": "node scripts/generate-release-body.js", + "notify:telegram": "node scripts/send-telegram-release.js", "mcp": "node scripts/mcp-runner.js", "mcp:probe": "node scripts/mcp-probe.js", "preview": "vite preview", diff --git a/scripts/README.md b/scripts/README.md index 7ab7376..8fb5541 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -25,16 +25,19 @@ git push origin v2.2.14 2. 重新编译原生模块 3. 执行 `npm run build` 4. 生成 `release/force-update.json` -5. 创建/更新 GitHub Release -6. 上传以下文件到 GitHub Release: +5. 生成 `release/release-context.json` +6. 自动生成 `release/release-body.md`(AI 失败时自动降级为模板版) +7. 创建/更新 GitHub Release +8. 上传以下文件到 GitHub Release: - 安装包 - `latest.yml` - `force-update.json` - 若存在则上传 `.blockmap` -7. 将以下文件同步到 Cloudflare R2: +9. 将以下文件同步到 Cloudflare R2: - 安装包 - `latest.yml` - `force-update.json` +10. 向 Telegram 频道/群发送发布通知(AI 摘要 + 强制更新提醒) ## 版本要求 @@ -84,6 +87,36 @@ npm run build:force-update-manifest 不配置时,`force-update.json` 仍会生成,但只包含当前版本信息,不会强制用户升级。 +### AI Release Body Secret + +发布工作流会自动生成标准化 Release body。 + +需要在 GitHub Environment `软件发布` 中配置: + +- `GLM_KEY` + +用途: +- 调用智谱 `glm-4.7-flash` +- 自动生成中文 Release 说明 +- 若 AI 不可用,会自动降级为模板正文,不影响发版 + +### Telegram 通知配置 + +如果需要自动发 Telegram 通知,请在 GitHub Environment `软件发布` 中配置: + +- Secret: + - `TELEGRAM_BOT_TOKEN` + +- Variable: + - `TELEGRAM_CHAT_IDS` + - `TELEGRAM_RELEASE_COVER_URL`(可选) + +说明: +- `TELEGRAM_CHAT_IDS` 支持多个目标,用英文逗号分隔 +- 可填写频道用户名或群/频道 chat_id +- 成功发布时会发送 AI 摘要版通知 +- 发布失败时会发送失败通知 + ## 当前更新源角色 - **GitHub Release**:主更新源,负责安装包与 `latest.yml` diff --git a/scripts/generate-release-body.js b/scripts/generate-release-body.js new file mode 100644 index 0000000..c7e9964 --- /dev/null +++ b/scripts/generate-release-body.js @@ -0,0 +1,194 @@ +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 outputPath = path.join(releaseDir, 'release-body.md') +const glmKey = process.env.GLM_KEY || '' + +const PRIMARY_AUTHOR_LOGINS = new Set(['ILoveBingLu']) +const PRIMARY_AUTHOR_NAMES = new Set(['ILoveBingLu', 'BingLu', 'ILoveBinglu']) +const PRIMARY_AUTHOR_EMAILS = new Set(['aiqiji74@gmail.com', 'aiqji74@gmail.com']) + +function isPrimaryAuthor(person) { + if (!person) return false + const login = String(person.authorLogin || '').trim() + const name = String(person.authorName || '').trim() + const email = String(person.authorEmail || '').trim().toLowerCase() + return PRIMARY_AUTHOR_LOGINS.has(login) || PRIMARY_AUTHOR_NAMES.has(name) || PRIMARY_AUTHOR_EMAILS.has(email) +} + +function classifyCommit(subject) { + const normalized = String(subject || '').toLowerCase() + if (normalized.startsWith('feat')) return '新增' + if (normalized.startsWith('fix')) return '修复' + return '调整' +} + +function buildThanks(context) { + const lines = [] + + for (const pr of context.pullRequests || []) { + if (!isPrimaryAuthor({ authorLogin: pr.authorLogin, authorName: pr.authorName })) { + lines.push(`- 感谢 @${pr.authorLogin} 提交 PR #${pr.number}《${pr.title}》`) + } + } + + const prNumbers = new Set((context.pullRequests || []).map((pr) => pr.number)) + for (const commit of context.commits || []) { + const hasPrRef = /#(\d+)/.test(commit.subject || '') + if (hasPrRef) continue + if (!isPrimaryAuthor(commit)) { + lines.push(`- 感谢 ${commit.authorName} 提交改动《${commit.subject}》`) + } + } + + return Array.from(new Set(lines)) +} + +function buildReferences(context) { + const lines = [] + for (const pr of context.pullRequests || []) { + lines.push(`- PR #${pr.number}: [${pr.title}](${pr.url})`) + } + for (const commit of context.commits || []) { + lines.push(`- Commit [${commit.shortSha}](${commit.url}): ${commit.subject}`) + } + return lines +} + +function buildFallbackBody(context) { + const groups = { + 新增: [], + 修复: [], + 调整: [] + } + + for (const commit of context.commits || []) { + groups[classifyCommit(commit.subject)].push(`- ${commit.subject}(${commit.shortSha})`) + } + + const thanks = buildThanks(context) + const references = buildReferences(context) + const blockedVersions = context.forceUpdate?.blockedVersions || [] + const hasUpgradeReminder = Boolean(context.forceUpdate?.minimumSupportedVersion || blockedVersions.length > 0) + + return [ + `## CipherTalk ${context.tag}`, + '', + '### 概览', + `本版本包含 ${context.commits.length} 条提交,涉及 ${(context.pullRequests || []).length} 个 PR。`, + '', + '### 新增', + ...(groups.新增.length ? groups.新增 : ['- 无']), + '', + '### 修复', + ...(groups.修复.length ? groups.修复 : ['- 无']), + '', + '### 调整', + ...(groups.调整.length ? groups.调整 : ['- 无']), + '', + ...(hasUpgradeReminder ? [ + '### 升级提醒', + ...(context.forceUpdate.minimumSupportedVersion ? [`- 最低安全版本:${context.forceUpdate.minimumSupportedVersion}`] : []), + ...(blockedVersions.length ? [`- 封禁版本:${blockedVersions.join(', ')}`] : []), + '' + ] : []), + '### 感谢贡献者', + ...(thanks.length ? thanks : ['- 本版本无新增外部贡献']), + '', + '### 相关提交与 PR', + ...(references.length ? references : ['- 无']), + '' + ].join('\n') +} + +function isValidAiBody(body) { + if (!body) return false + return body.includes('## CipherTalk') && body.includes('### 感谢贡献者') && body.includes('### 相关提交与 PR') +} + +async function generateAiBody(context) { + if (!glmKey) { + throw new Error('GLM_KEY 未配置') + } + + const systemPrompt = [ + '你是一个发布说明撰写助手。', + '只能基于输入中的 commits 和 pull requests 生成,不得编造任何功能或修复。', + '输出必须是中文 Markdown。', + '必须包含以下章节:', + '## CipherTalk vX.Y.Z', + '### 概览', + '### 新增', + '### 修复', + '### 调整', + '### 感谢贡献者', + '### 相关提交与 PR', + '如果存在最低安全版本或封禁版本,增加 ### 升级提醒 章节。', + '有 PR 时优先引用 PR 标题;没有 PR 时才引用 commit 标题。', + '感谢规则:只有非主作者的 PR/commit 才出现在感谢段。', + '不要写模糊词,不要写猜测,不要写未在输入中出现的功能。' + ].join('\n') + + const userPrompt = `请根据以下发布上下文为 ${context.tag} 生成标准化发布说明:\n\n${JSON.stringify(context, null, 2)}` + + const response = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${glmKey}` + }, + body: JSON.stringify({ + model: 'glm-4.7-flash', + temperature: 0.2, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt } + ] + }) + }) + + if (!response.ok) { + throw new Error(`GLM 请求失败: ${response.status}`) + } + + const data = await response.json() + const content = data?.choices?.[0]?.message?.content + if (typeof content !== 'string' || !content.trim()) { + throw new Error('GLM 返回内容为空') + } + + const body = content.trim() + if (!isValidAiBody(body)) { + throw new Error('GLM 返回内容不符合格式要求') + } + + return body +} + +async function main() { + if (!fs.existsSync(contextPath)) { + throw new Error(`未找到 release context: ${contextPath}`) + } + + const context = JSON.parse(fs.readFileSync(contextPath, 'utf8')) + + let body + try { + body = await generateAiBody(context) + console.log('✅ 已生成 AI Release Body') + } catch (error) { + console.warn('⚠️ AI 生成失败,回退到模板正文:', String(error)) + body = buildFallbackBody(context) + } + + fs.writeFileSync(outputPath, `${body.trim()}\n`, 'utf8') + console.log(`✅ release-body.md 已生成: ${outputPath}`) +} + +main().catch((error) => { + console.error('❌ 生成 release-body.md 失败:', error) + process.exit(1) +}) diff --git a/scripts/generate-release-context.js b/scripts/generate-release-context.js new file mode 100644 index 0000000..68c1d2d --- /dev/null +++ b/scripts/generate-release-context.js @@ -0,0 +1,148 @@ +const fs = require('fs') +const path = require('path') +const { execSync } = require('child_process') + +const rootDir = path.resolve(__dirname, '..') +const releaseDir = path.join(rootDir, 'release') +const owner = process.env.GITHUB_REPOSITORY_OWNER || 'ILoveBingLu' +const repo = (process.env.GITHUB_REPOSITORY || `${owner}/CipherTalk`).split('/')[1] || 'CipherTalk' +const currentTag = process.env.RELEASE_TAG || process.env.GITHUB_REF_NAME || '' +const ghToken = process.env.GH_TOKEN || '' +const pkg = require(path.join(rootDir, 'package.json')) + +function runGit(command) { + return execSync(command, { + cwd: rootDir, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }).trim() +} + +function safeJsonParse(value, fallback) { + try { + return JSON.parse(value) + } catch { + return fallback + } +} + +function parseList(value) { + if (!value) return [] + return String(value) + .split(',') + .map((item) => item.trim()) + .filter(Boolean) +} + +function getPreviousTag() { + const tags = runGit('git tag --sort=-version:refname') + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + + if (!currentTag) return tags[0] || null + + const currentIndex = tags.indexOf(currentTag) + if (currentIndex === -1) return tags[0] || null + return tags[currentIndex + 1] || null +} + +function getCommitRange(previousTag, tag) { + if (!tag) return 'HEAD' + if (!previousTag || previousTag === tag) return tag + return `${previousTag}..${tag}` +} + +function extractPrNumbers(commits) { + const prNumbers = new Set() + for (const commit of commits) { + const matches = commit.subject.match(/#(\d+)/g) + if (!matches) continue + for (const match of matches) { + prNumbers.add(Number(match.slice(1))) + } + } + return Array.from(prNumbers) +} + +async function fetchPullRequest(prNumber) { + if (!ghToken) return null + + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, { + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${ghToken}`, + 'User-Agent': 'CipherTalk-Release-Context' + } + }) + + if (!response.ok) return null + const data = await response.json() + return { + number: data.number, + title: data.title, + url: data.html_url, + authorLogin: data.user?.login || null, + authorName: data.user?.login || null, + mergedBy: data.merged_by?.login || null + } +} + +async function main() { + if (!fs.existsSync(releaseDir)) { + fs.mkdirSync(releaseDir, { recursive: true }) + } + + const previousTag = getPreviousTag() + const commitRange = getCommitRange(previousTag, currentTag || 'HEAD') + + const commitLines = runGit(`git log ${commitRange} --pretty=format:"%H|%h|%an|%ae|%s"`) + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + + const commits = commitLines.map((line) => { + const [sha, shortSha, authorName, authorEmail, ...subjectParts] = line.split('|') + return { + sha, + shortSha, + url: `https://github.com/${owner}/${repo}/commit/${sha}`, + authorName, + authorEmail, + subject: subjectParts.join('|') + } + }) + + const prNumbers = extractPrNumbers(commits) + const prs = [] + for (const prNumber of prNumbers) { + const pr = await fetchPullRequest(prNumber) + if (pr) prs.push(pr) + } + + const context = { + version: pkg.version, + tag: currentTag || `v${pkg.version}`, + previousTag, + generatedAt: new Date().toISOString(), + repository: { + owner, + repo + }, + forceUpdate: { + minimumSupportedVersion: process.env.FORCE_UPDATE_MIN_VERSION || null, + blockedVersions: parseList(process.env.FORCE_UPDATE_BLOCKED_VERSIONS) + }, + commits, + pullRequests: prs + } + + const outputPath = path.join(releaseDir, 'release-context.json') + fs.writeFileSync(outputPath, `${JSON.stringify(context, null, 2)}\n`, 'utf8') + console.log(`✅ release-context.json 已生成: ${outputPath}`) +} + +main().catch((error) => { + console.error('❌ 生成 release-context.json 失败:', error) + process.exit(1) +}) diff --git a/scripts/send-telegram-release.js b/scripts/send-telegram-release.js new file mode 100644 index 0000000..37dcde8 --- /dev/null +++ b/scripts/send-telegram-release.js @@ -0,0 +1,175 @@ +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']) + for (const pr of context?.pullRequests || []) { + if (pr?.authorLogin && !primaryLogins.has(pr.authorLogin)) { + thanks.push(`🙏 感谢 @${pr.authorLogin} 提交 PR #${pr.number}`) + } + } + + 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) +})