From ff580b368a2633d1964bfa3882a9c3501fd19605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 13:16:04 +0800 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A4=9A?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD=20+=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=B2=BE=E7=AE=80=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 utils/notify.js: 支持企业微信/钉钉/飞书/云湖/Server酱/PushPlus/Telegram/Bark/Discord/邮箱SMTP 共10种通知渠道 - 新增 utils/userinfo.js: 提取 phoneLogin/qrcodeLogin 共享的用户信息管理逻辑 - 更新 main.js: 添加运行结果收集和通知发送 - 更新 main.yml: 添加通知渠道环境变量 - 修复 youth_listen_song.js 中 encryptTyPe 拼写错误 - 修复 qrcodeLogin.js 内层循环变量遮蔽问题 - 更新 README.md: 添加通知配置说明和令牌机制说明 - 精简 phoneLogin.js/qrcodeLogin.js/utils.js 冗余代码 --- .github/workflows/main.yml | 19 ++ README.md | 35 ++++ api/module/youth_listen_song.js | 2 +- main.js | 68 ++++++- phoneLogin.js | 55 +----- qrcodeLogin.js | 66 ++----- utils/notify.js | 311 ++++++++++++++++++++++++++++++++ utils/userinfo.js | 67 +++++++ utils/utils.js | 34 ++-- 9 files changed, 525 insertions(+), 132 deletions(-) create mode 100644 utils/notify.js create mode 100644 utils/userinfo.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eb7edb3..00fe163 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,3 +45,22 @@ jobs: USERINFO: ${{ secrets.USERINFO }} PAT: ${{ secrets.PAT }} GITHUB_REPOSITORY: ${{ github.repository }} + # 通知渠道(按需配置,全部可选) + WECOM_BOT_KEY: ${{ secrets.WECOM_BOT_KEY }} + DINGTALK_BOT_KEY: ${{ secrets.DINGTALK_BOT_KEY }} + DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} + FEISHU_BOT_KEY: ${{ secrets.FEISHU_BOT_KEY }} + YUNHU_BOT_KEY: ${{ secrets.YUNHU_BOT_KEY }} + SERVERCHAN_SENDKEY: ${{ secrets.SERVERCHAN_SENDKEY }} + PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }} + PUSHPLUS_TOPIC: ${{ secrets.PUSHPLUS_TOPIC }} + TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} + TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} + BARK_KEY: ${{ secrets.BARK_KEY }} + BARK_GROUP: ${{ secrets.BARK_GROUP }} + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + MAIL_HOST: ${{ secrets.MAIL_HOST }} + MAIL_PORT: ${{ secrets.MAIL_PORT }} + MAIL_USER: ${{ secrets.MAIL_USER }} + MAIL_PASS: ${{ secrets.MAIL_PASS }} + MAIL_TO: ${{ secrets.MAIL_TO }} diff --git a/README.md b/README.md index 5e74b99..918a5d9 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,43 @@ GitHub Actions 实现 `酷狗概念VIP` 自动签到,每天领取总计 `两 1. 启用 Actions `main` , 每天北京时间 01:15 自动签到(可在`main.yml`中设置cron)。启用 Actions `Repository Keepalive` 以保证签到可以长期执行。 +1. (可选)配置运行结果通知 + + 在仓库 Settings → Secrets and variables → Actions 中添加对应渠道的 Secret,签到完成后将自动推送结果通知。支持以下渠道(全部可选,配置多个将同时发送): + + | 通知渠道 | Secret 变量名 | 说明 | + |---------|-------------|------| + | 企业微信机器人 | `WECOM_BOT_KEY` | 企业微信群机器人 webhook 的 key | + | 钉钉机器人 | `DINGTALK_BOT_KEY` | 钉钉机器人 access_token | + | 钉钉加签 | `DINGTALK_SECRET` | 钉钉机器人加签密钥(可选) | + | 飞书机器人 | `FEISHU_BOT_KEY` | 飞书自定义机器人 webhook 的 key | + | 云湖机器人 | `YUNHU_BOT_KEY` | 云湖机器人 webhook 的 key | + | Server酱 | `SERVERCHAN_SENDKEY` | Server酱 SendKey | + | PushPlus | `PUSHPLUS_TOKEN` | PushPlus token | + | PushPlus群组 | `PUSHPLUS_TOPIC` | PushPlus 群组编码(可选) | + | Telegram | `TG_BOT_TOKEN` | Telegram Bot Token | + | Telegram | `TG_CHAT_ID` | Telegram 接收消息的 Chat ID | + | Bark (iOS) | `BARK_KEY` | Bark key 或完整 URL | + | Bark分组 | `BARK_GROUP` | Bark 消息分组(可选) | + | Discord | `DISCORD_WEBHOOK` | Discord Webhook 完整 URL | + | 邮箱 SMTP | `MAIL_HOST` | SMTP 服务器地址(如 `smtp.qq.com`) | + | 邮箱 SMTP | `MAIL_PORT` | SMTP 端口(默认 465) | + | 邮箱 SMTP | `MAIL_USER` | 发件邮箱账号 | + | 邮箱 SMTP | `MAIL_PASS` | 发件邮箱授权码(非登录密码) | + | 邮箱 SMTP | `MAIL_TO` | 收件邮箱地址 | + + 通知内容包含:运行日期、账号数量、成功/失败统计、各账号听歌领取状态、VIP 领取次数、VIP 到期时间、错误信息等。 + API源代码来自 [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi) ~~图省事直接搬来~~ +## 令牌(Token)机制说明 + +项目中包含两类令牌: + +1. **GitHub Personal Access Token (PAT)**:用于自动将酷狗登录信息写入仓库 Secret `USERINFO`,以及每周日自动刷新酷狗登录 Token。**不建议移除**,移除后需手动复制登录信息,且 Token 约 2 个月后过期需重新登录。 + +2. **酷狗登录 Token**:存储在 `USERINFO` Secret 中,用于酷狗 API 身份认证。通过登录获取,每周日自动刷新。此为签到核心凭证,不可移除。 + ## Secret 位置 1. 步骤一 diff --git a/api/module/youth_listen_song.js b/api/module/youth_listen_song.js index 62f7123..3b0fe1c 100644 --- a/api/module/youth_listen_song.js +++ b/api/module/youth_listen_song.js @@ -8,7 +8,7 @@ module.exports = (params, useAxios) => { url: '/youth/v2/report/listen_song', data: dataMap, method: 'POST', - encryptTyPe: 'android', + encryptType: 'android', params: { clientver: 10566 }, cookie: params?.cookie, headers: { diff --git a/main.js b/main.js index 47a8967..56bdc06 100644 --- a/main.js +++ b/main.js @@ -1,6 +1,7 @@ import { printBlue, printGreen, printMagenta, printRed, printYellow } from "./utils/colorOut.js"; import { hasSecretWriteToken, setRepoSecret } from "./utils/githubSecrets.js"; import { maskDisplayName, maskIdentifier, sanitizeForLog, summarizeResponse } from "./utils/safeLog.js"; +import { sendNotify } from "./utils/notify.js"; import { close_api, delay, send, startService } from "./utils/utils.js"; async function main() { @@ -28,11 +29,14 @@ async function main() { const date = yyyy + '-' + MM + '-' + DD const errorMsg = {} + // 通知结果收集 + const notifyResults = [] + let hasError = false + try { // 开始签到 for (const user of userinfo) { const headers = { 'cookie': 'token=' + user.token + '; userid=' + user.userid } - // console.log(headers) const userDetail = await send(`/user/detail?timestrap=${Date.now()}`, "GET", headers) if (userDetail?.data?.nickname == null) { const safeUserId = maskIdentifier(user.userid) @@ -41,6 +45,15 @@ async function main() { msg: `token过期或账号不存在, userid: ${safeUserId}`, data: summarizeResponse(userDetail) } + notifyResults.push({ + nickname: safeUserId, + status: '失败', + listen: '账号不存在', + vipClaim: '0/8', + vipExpiry: '未知', + error: 'token过期或账号不存在' + }) + hasError = true continue } const safeNickname = maskDisplayName(userDetail.data.nickname) @@ -64,23 +77,30 @@ async function main() { // 听歌获取vip const listen = await send(`/youth/listen/song?timestrap=${Date.now()}`, "GET", headers) + let listenStatus = '未知' if (listen.status === 1) { printGreen("听歌领取成功") + listenStatus = '成功' } else if (listen.error_code === 130012) { printGreen("今日已领取") + listenStatus = '今日已领取' } else { errorMsg[`${safeNickname} listen`] = summarizeResponse(listen) printRed("听歌领取失败") + listenStatus = '失败' + hasError = true } printYellow("开始领取VIP...") + let claimCount = 0 + let claimTotal = 0 for (let i = 1; i <= 8; i++) { // ad获取vip const ad = await send(`/youth/vip?timestrap=${Date.now()}`, "GET", headers) - // 签到出现问题 - // errorMsg[`${safeNickname} ad${i}`] = summarizeResponse(ad) + claimTotal = i if (ad.status === 1) { printGreen(`第${i}次领取成功`) + claimCount++ if (i != 8) { await delay(30 * 1000) } @@ -89,20 +109,32 @@ async function main() { break } else { printRed(`第${i}次领取失败`) - // console.dir(ad, { depth: null }) errorMsg[`${safeNickname} ad`] = summarizeResponse(ad) + hasError = true break } } + let vipExpiry = '未知' const vip_details = await send(`/user/vip/detail?timestrap=${Date.now()}`, "GET", headers) if (vip_details.status === 1) { + vipExpiry = vip_details.data.busi_vip[0].vip_end_time printBlue(`今天是:${date}`) - printBlue(`VIP到期时间:${vip_details.data.busi_vip[0].vip_end_time}\n`) + printBlue(`VIP到期时间:${vipExpiry}\n`) } else { printRed("获取失败\n") errorMsg[`${safeNickname} vip_details`] = summarizeResponse(vip_details) + hasError = true } + + notifyResults.push({ + nickname: safeNickname, + status: listenStatus === '失败' || claimCount === 0 ? '部分失败' : '成功', + listen: listenStatus, + vipClaim: `${claimCount}/${claimTotal}`, + vipExpiry, + error: '' + }) } } finally { @@ -128,6 +160,31 @@ async function main() { } + // 构建通知内容 + const title = `酷狗签到${hasError ? '异常' : '成功'} ${date}` + let content = `📅 日期: ${date}\n` + content += `📊 账号数: ${notifyResults.length}\n` + const successCount = notifyResults.filter(r => r.status === '成功').length + const failCount = notifyResults.length - successCount + content += `✅ 成功: ${successCount} ❌ 失败: ${failCount}\n` + + for (const r of notifyResults) { + content += `\n【${r.nickname}】\n` + content += ` 🎵 听歌领取: ${r.listen}\n` + content += ` 🎁 VIP领取: ${r.vipClaim} 次\n` + content += ` ⏰ VIP到期: ${r.vipExpiry}\n` + if (r.error) { + content += ` ⚠️ 错误: ${r.error}\n` + } + } + + // 发送通知 + try { + await sendNotify(title, content) + } catch (e) { + printYellow(`通知发送异常: ${e.message}`) + } + if (Object.keys(errorMsg).length > 0) { printRed("异常信息如下:") console.dir(sanitizeForLog(errorMsg), { depth: null }) @@ -140,4 +197,3 @@ async function main() { } main() - diff --git a/phoneLogin.js b/phoneLogin.js index db790c0..296cd4a 100644 --- a/phoneLogin.js +++ b/phoneLogin.js @@ -1,6 +1,6 @@ -import { printBlue, printGreen, printRed, printYellow } from "./utils/colorOut.js"; -import { hasSecretWriteToken, setRepoSecret } from "./utils/githubSecrets.js"; -import { maskIdentifier, sanitizeForLog, shouldPrintSensitiveValue, summarizeResponse } from "./utils/safeLog.js"; +import { printGreen, printRed, printYellow } from "./utils/colorOut.js"; +import { sanitizeForLog, summarizeResponse } from "./utils/safeLog.js"; +import { upsertUser, saveUserinfo } from "./utils/userinfo.js"; import { close_api, delay, send, startService } from "./utils/utils.js"; async function login() { @@ -23,54 +23,9 @@ async function login() { // 手机号登录请求 const result = await send(`/login/cellphone?mobile=${phone}&code=${code}`, "GET", {}) if (result.status === 1) { - - let userAlreadyExist = false printGreen("登录成功!") - if (APPEND_USER == "是") { - for (let i = 0; i < userinfo.length; i++) { - - if (userinfo[i].userid == result.data.userid) { - userAlreadyExist = true - printYellow(`userid: ${maskIdentifier(userinfo[i].userid)} 此账号已存在, 仅更新登录信息`) - userinfo[i].token = result.data.token - } - } - } - if (!userAlreadyExist) { - userinfo.push({ - userid: result.data.userid, - token: result.data.token - }) - } - if (userinfo.length) { - const userinfoJSON = JSON.stringify(userinfo) - if (hasSecretWriteToken()) { - try { - setRepoSecret("USERINFO", userinfoJSON) - printGreen("secret 更改成功") - } catch (error) { - printRed("自动写入 secret 出错") - console.dir(sanitizeForLog({ message: error.message }), { depth: null }) - if (shouldPrintSensitiveValue()) { - printYellow("已按显式配置输出 USERINFO;请用完后删除 Actions 日志") - printBlue(userinfoJSON) - } else { - printYellow("为避免泄露 token,默认不在日志输出 USERINFO") - printYellow("如必须手动复制,请重新运行并将 print_userinfo 选择为 是") - } - } - } else { - if (shouldPrintSensitiveValue()) { - printGreen("登录信息如下,把它添加到secret USERINFO 即可") - printYellow("注意:日志包含登录 token,请用完后删除 Actions 日志") - printBlue(userinfoJSON) - } else { - printYellow("PAT/GH_TOKEN 未配置,无法自动写入 secret ") - printYellow("为避免泄露 token,默认不在日志输出 USERINFO") - printYellow("如必须手动复制,请重新运行并将 print_userinfo 选择为 是") - } - } - } + upsertUser(userinfo, { userid: result.data.userid, token: result.data.token }, APPEND_USER == "是") + saveUserinfo(userinfo) } else if (result.error_code === 34175) { throw new Error("暂不支持多账号绑定手机登录") } else { diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 785ab92..5f19e5e 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -1,7 +1,7 @@ import { close_api, delay, send, startService } from "./utils/utils.js"; -import { printBlue, printGreen, printMagenta, printRed, printYellow } from "./utils/colorOut.js"; -import { hasSecretWriteToken, setRepoSecret } from "./utils/githubSecrets.js"; -import { maskIdentifier, sanitizeForLog, shouldPrintSensitiveValue, summarizeResponse } from "./utils/safeLog.js"; +import { printGreen, printMagenta, printRed, printYellow } from "./utils/colorOut.js"; +import { summarizeResponse } from "./utils/safeLog.js"; +import { upsertUser, saveUserinfo } from "./utils/userinfo.js"; async function qrcode() { @@ -15,7 +15,7 @@ async function qrcode() { const args = process.argv.slice(2); const number = parseInt(process.env.NUMBER || args[0] || "1") try { - for (let i = 0; i < number; i++) { + for (let n = 0; n < number; n++) { // 二维码 const result = await send(`/login/qr/key?timestrap=${Date.now()}`, "GET", {}) if (result.status === 1) { @@ -23,8 +23,8 @@ async function qrcode() { const img_base64 = result.data.qrcode_img; const chunkSize = 1000; printMagenta("二维码链接如下, 请在浏览器打开使用APP扫描并确认登录") - for (let i = 0; i < img_base64.length; i += chunkSize) { - console.log(img_base64.slice(i, i + chunkSize)); + for (let j = 0; j < img_base64.length; j += chunkSize) { + console.log(img_base64.slice(j, j + chunkSize)); } } else { printRed("响应内容") @@ -43,32 +43,18 @@ async function qrcode() { break case 1: - // console.log("未扫描二维码") + // 未扫描二维码 break case 2: - // console.log("二维码未确认,请点击确认登录") + // 二维码未确认,请点击确认登录 break + case 4: - let userAlreadyExist = false printGreen("登录成功!") - if (APPEND_USER == "是") { - for (let i = 0; i < userinfo.length; i++) { + upsertUser(userinfo, { userid: res.data.userid, token: res.data.token }, APPEND_USER == "是") + break - if (userinfo[i].userid == res.data.userid) { - userAlreadyExist = true - printYellow(`userid: ${maskIdentifier(userinfo[i].userid)} 此账号已存在, 仅更新登录信息`) - userinfo[i].token = res.data.token - } - } - } - if (!userAlreadyExist) { - userinfo.push({ - userid: res.data.userid, - token: res.data.token - }) - } - break; default: printRed("请求出错") console.dir(summarizeResponse(res), { depth: null }) @@ -83,35 +69,7 @@ async function qrcode() { await delay(5000) } } - if (userinfo.length) { - const userinfoJSON = JSON.stringify(userinfo) - if (hasSecretWriteToken()) { - try { - setRepoSecret("USERINFO", userinfoJSON) - printGreen("secret 更改成功") - } catch (error) { - printRed("自动写入 secret 出错") - console.dir(sanitizeForLog({ message: error.message }), { depth: null }) - if (shouldPrintSensitiveValue()) { - printYellow("已按显式配置输出 USERINFO;请用完后删除 Actions 日志") - printBlue(userinfoJSON) - } else { - printYellow("为避免泄露 token,默认不在日志输出 USERINFO") - printYellow("如必须手动复制,请重新运行并将 print_userinfo 选择为 是") - } - } - } else { - if (shouldPrintSensitiveValue()) { - printGreen("登录信息如下,把它添加到secret USERINFO 即可") - printYellow("注意:日志包含登录 token,请用完后删除 Actions 日志") - printBlue(userinfoJSON) - } else { - printYellow("PAT/GH_TOKEN 未配置,无法自动写入 secret ") - printYellow("为避免泄露 token,默认不在日志输出 USERINFO") - printYellow("如必须手动复制,请重新运行并将 print_userinfo 选择为 是") - } - } - } + saveUserinfo(userinfo) } finally { close_api(api) } diff --git a/utils/notify.js b/utils/notify.js new file mode 100644 index 0000000..d1f53e8 --- /dev/null +++ b/utils/notify.js @@ -0,0 +1,311 @@ +/** + * 多渠道通知模块 + * 支持以下通知渠道(通过环境变量 / GitHub Secrets 配置): + * + * 1. 企业微信机器人 WECOM_BOT_KEY (webhook key) + * 2. 钉钉机器人 DINGTALK_BOT_KEY (+ 可选 DINGTALK_SECRET) + * 3. 飞书机器人 FEISHU_BOT_KEY (webhook key) + * 4. 云湖机器人 YUNHU_BOT_KEY (webhook key) + * 5. Server酱 SERVERCHAN_SENDKEY (sendkey) + * 6. PushPlus PUSHPLUS_TOKEN (+ 可选 PUSHPLUS_TOPIC) + * 7. Telegram Bot TG_BOT_TOKEN + TG_CHAT_ID + * 8. Bark (iOS) BARK_KEY (+ 可选 BARK_GROUP) + * 9. Discord Webhook DISCORD_WEBHOOK (完整 URL) + * 10. 邮箱 SMTP MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, MAIL_TO + * + * 所有渠道均使用 Node.js 内置模块,无外部依赖。 + */ + +import crypto from 'node:crypto' +import tls from 'node:tls' +import { printGreen, printRed, printYellow } from './colorOut.js' + +/* ------------------------------------------------------------------ */ +/* 各渠道发送实现 */ +/* ------------------------------------------------------------------ */ + +// 1. 企业微信机器人 +async function sendWeCom(title, content, key) { + const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}` + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + msgtype: 'text', + text: { content: `${title}\n\n${content}` }, + }), + }) + return resp.ok +} + +// 2. 钉钉机器人 +async function sendDingTalk(title, content, key, secret) { + let url = `https://oapi.dingtalk.com/robot/send?access_token=${key}` + if (secret) { + const timestamp = Date.now() + const stringToSign = `${timestamp}\n${secret}` + const sign = crypto + .createHmac('sha256', secret) + .update(stringToSign) + .digest('base64') + url += `×tamp=${timestamp}&sign=${encodeURIComponent(sign)}` + } + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + msgtype: 'markdown', + markdown: { title, text: `### ${title}\n\n${content}` }, + }), + }) + return resp.ok +} + +// 3. 飞书机器人 +async function sendFeishu(title, content, key) { + const url = `https://open.feishu.cn/open-apis/bot/v2/hook/${key}` + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + msg_type: 'text', + content: { text: `${title}\n\n${content}` }, + }), + }) + return resp.ok +} + +// 4. 云湖机器人 +async function sendYunhu(title, content, key) { + const url = `https://www.yhchat.com/bot/send?key=${key}` + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + msg: { text: `${title}\n\n${content}` }, + }), + }) + return resp.ok +} + +// 5. Server酱 +async function sendServerChan(title, content, sendkey) { + const url = `https://sctapi.ftqq.com/${sendkey}.send` + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ title, desp: content }), + }) + return resp.ok +} + +// 6. PushPlus +async function sendPushPlus(title, content, token, topic) { + const url = 'http://www.pushplus.plus/send' + const body = { token, title, content, template: 'txt' } + if (topic) body.topic = topic + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + return resp.ok +} + +// 7. Telegram Bot +async function sendTelegram(title, content, botToken, chatId) { + const url = `https://api.telegram.org/bot${botToken}/sendMessage` + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: chatId, + text: `*${title}*\n\n${content}`, + parse_mode: 'Markdown', + }), + }) + return resp.ok +} + +// 8. Bark (iOS 推送) +async function sendBark(title, content, key, group) { + const base = key.startsWith('http') ? key : `https://api.day.app/${key}` + const url = `${base}/${encodeURIComponent(title)}/${encodeURIComponent(content)}` + const params = new URLSearchParams() + if (group) params.set('group', group) + const finalUrl = params.toString() ? `${url}?${params}` : url + const resp = await fetch(finalUrl) + return resp.ok +} + +// 9. Discord Webhook +async function sendDiscord(title, content, webhookUrl) { + const resp = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: `**${title}**\n\n${content}`.slice(0, 2000), + }), + }) + return resp.ok +} + +// 10. 邮箱 SMTP(使用 node:tls 实现最小 SMTP 客户端) +function sendMailSMTP(title, content, cfg) { + return new Promise((resolve, reject) => { + const { host, port, user, pass, to } = cfg + const from = user + const subject = `=?UTF-8?B?${Buffer.from(title).toString('base64')}?=` + const date = new Date().toUTCString() + const messageId = `<${Date.now()}.${Math.random().toString(36).slice(2)}@kgcheckin>` + + const mailBody = [ + `From: ${from}`, + `To: ${to}`, + `Subject: ${subject}`, + `Date: ${date}`, + `Message-ID: ${messageId}`, + 'MIME-Version: 1.0', + 'Content-Type: text/plain; charset=UTF-8', + 'Content-Transfer-Encoding: 8bit', + '', + content, + ].join('\r\n') + + const lines = [ + `EHLO kgcheckin`, + `AUTH LOGIN`, + Buffer.from(user).toString('base64'), + Buffer.from(pass).toString('base64'), + `MAIL FROM:<${from}>`, + `RCPT TO:<${to}>`, + 'DATA', + mailBody.replace(/\r?\n/g, '\r\n') + '\r\n.', + 'QUIT', + ] + + let step = 0 + let buffer = '' + const socket = tls.connect( + { host, port: Number(port) || 465, rejectUnauthorized: true }, + () => {} + ) + + socket.setEncoding('utf8') + socket.on('data', (chunk) => { + buffer += chunk + while (true) { + const idx = buffer.indexOf('\r\n') + if (idx === -1) break + const line = buffer.slice(0, idx) + buffer = buffer.slice(idx + 2) + const code = parseInt(line.slice(0, 3), 10) + + if (code >= 400) { + socket.destroy() + reject(new Error(`SMTP 错误: ${line}`)) + return + } + + // 等待多行响应结束(最后一行以 "code " 开头,中间行以 "code-" 开头) + if (line[3] === '-') continue + + if (step < lines.length) { + socket.write(lines[step] + '\r\n') + step++ + } + } + }) + + // 超时保护 + const timer = setTimeout(() => { + socket.destroy() + reject(new Error('SMTP 超时')) + }, 15000) + + socket.on('end', () => { clearTimeout(timer); resolve(true) }) + socket.on('close', () => { clearTimeout(timer); resolve(true) }) + socket.on('error', (err) => { clearTimeout(timer); reject(err) }) + }) +} + +/* ------------------------------------------------------------------ */ +/* 统一发送入口 */ +/* ------------------------------------------------------------------ */ + +/** + * 发送通知到所有已配置的渠道 + * @param {string} title - 通知标题 + * @param {string} content - 通知正文(纯文本) + */ +async function sendNotify(title, content) { + const channels = [] + + // 收集所有已配置的渠道 + if (process.env.WECOM_BOT_KEY) { + channels.push({ name: '企业微信', fn: () => sendWeCom(title, content, process.env.WECOM_BOT_KEY) }) + } + if (process.env.DINGTALK_BOT_KEY) { + channels.push({ name: '钉钉', fn: () => sendDingTalk(title, content, process.env.DINGTALK_BOT_KEY, process.env.DINGTALK_SECRET) }) + } + if (process.env.FEISHU_BOT_KEY) { + channels.push({ name: '飞书', fn: () => sendFeishu(title, content, process.env.FEISHU_BOT_KEY) }) + } + if (process.env.YUNHU_BOT_KEY) { + channels.push({ name: '云湖', fn: () => sendYunhu(title, content, process.env.YUNHU_BOT_KEY) }) + } + if (process.env.SERVERCHAN_SENDKEY) { + channels.push({ name: 'Server酱', fn: () => sendServerChan(title, content, process.env.SERVERCHAN_SENDKEY) }) + } + if (process.env.PUSHPLUS_TOKEN) { + channels.push({ name: 'PushPlus', fn: () => sendPushPlus(title, content, process.env.PUSHPLUS_TOKEN, process.env.PUSHPLUS_TOPIC) }) + } + if (process.env.TG_BOT_TOKEN && process.env.TG_CHAT_ID) { + channels.push({ name: 'Telegram', fn: () => sendTelegram(title, content, process.env.TG_BOT_TOKEN, process.env.TG_CHAT_ID) }) + } + if (process.env.BARK_KEY) { + channels.push({ name: 'Bark', fn: () => sendBark(title, content, process.env.BARK_KEY, process.env.BARK_GROUP) }) + } + if (process.env.DISCORD_WEBHOOK) { + channels.push({ name: 'Discord', fn: () => sendDiscord(title, content, process.env.DISCORD_WEBHOOK) }) + } + if (process.env.MAIL_HOST && process.env.MAIL_USER && process.env.MAIL_PASS && process.env.MAIL_TO) { + channels.push({ + name: '邮箱', + fn: () => sendMailSMTP(title, content, { + host: process.env.MAIL_HOST, + port: process.env.MAIL_PORT || 465, + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASS, + to: process.env.MAIL_TO, + }), + }) + } + + if (channels.length === 0) { + return + } + + printYellow(`正在发送通知到 ${channels.length} 个渠道...`) + const results = await Promise.allSettled(channels.map(async (ch) => { + const ok = await ch.fn() + return { name: ch.name, ok } + })) + + let success = 0 + let fail = 0 + for (const r of results) { + if (r.status === 'fulfilled' && r.value.ok) { + printGreen(` ✓ ${r.value.name} 发送成功`) + success++ + } else { + const name = r.status === 'fulfilled' ? r.value.name : '未知' + const reason = r.status === 'rejected' ? r.reason?.message : 'HTTP错误' + printRed(` ✗ ${name} 发送失败: ${reason}`) + fail++ + } + } + printYellow(`通知发送完成: ${success} 成功, ${fail} 失败`) +} + +export { sendNotify } diff --git a/utils/userinfo.js b/utils/userinfo.js new file mode 100644 index 0000000..8838155 --- /dev/null +++ b/utils/userinfo.js @@ -0,0 +1,67 @@ +/** + * 用户信息管理工具 + * 提供登录脚本共享的用户信息更新与保存逻辑 + */ + +import { printBlue, printGreen, printRed, printYellow } from './colorOut.js' +import { hasSecretWriteToken, setRepoSecret } from './githubSecrets.js' +import { maskIdentifier, sanitizeForLog, shouldPrintSensitiveValue } from './safeLog.js' + +/** + * 将登录用户信息更新或追加到 userinfo 数组中 + * @param {Array} userinfo - 用户信息数组 + * @param {{ userid: string, token: string }} loginUser - 新登录的用户 + * @param {boolean} append - 是否追加模式(已存在则更新,否则添加) + */ +function upsertUser(userinfo, loginUser, append) { + if (append) { + for (const user of userinfo) { + if (user.userid == loginUser.userid) { + printYellow(`userid: ${maskIdentifier(user.userid)} 此账号已存在, 仅更新登录信息`) + user.token = loginUser.token + return + } + } + } + userinfo.push({ userid: loginUser.userid, token: loginUser.token }) +} + +/** + * 保存 userinfo 到 GitHub Secret,失败时降级为日志输出 + * @param {Array} userinfo - 用户信息数组 + */ +function saveUserinfo(userinfo) { + if (!userinfo.length) return + + const userinfoJSON = JSON.stringify(userinfo) + + if (hasSecretWriteToken()) { + try { + setRepoSecret('USERINFO', userinfoJSON) + printGreen('secret 更改成功') + } catch (error) { + printRed('自动写入 secret 出错') + console.dir(sanitizeForLog({ message: error.message }), { depth: null }) + printUserinfoFallback(userinfoJSON) + } + } else { + printYellow('PAT/GH_TOKEN 未配置,无法自动写入 secret ') + printUserinfoFallback(userinfoJSON) + } +} + +/** + * 降级方案:按配置决定是否在日志中输出 USERINFO + */ +function printUserinfoFallback(userinfoJSON) { + if (shouldPrintSensitiveValue()) { + printGreen('登录信息如下,把它添加到secret USERINFO 即可') + printYellow('注意:日志包含登录 token,请用完后删除 Actions 日志') + printBlue(userinfoJSON) + } else { + printYellow('为避免泄露 token,默认不在日志输出 USERINFO') + printYellow('如必须手动复制,请重新运行并将 print_userinfo 选择为 是') + } +} + +export { upsertUser, saveUserinfo } diff --git a/utils/utils.js b/utils/utils.js index 9b380bd..f6b4584 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -1,43 +1,35 @@ import { spawn } from 'child_process' +/** 延时 */ function delay(ms) { - return new Promise(resolve => { - setTimeout(resolve, ms) - }) + return new Promise(resolve => setTimeout(resolve, ms)) } -// 运行api服务 +/** 启动 api 服务 */ function startService() { + const api = spawn('npm', ['run', 'apiService']) - let api = spawn("npm", ["run", "apiService"]) - - api.stdout.on('data', data => { - // console.log(`${data}`) - }) - - api.on('close', code => { - console.log(`子进程退出`) - }) + api.stdout.on('data', () => {}) + api.on('close', () => console.log('子进程退出')) api.stderr.on('data', data => { - console.log("服务启动失败") + console.log('服务启动失败') throw new Error(`${data}`) }) - return api + return api } -// 关闭api服务 +/** 关闭 api 服务 */ function close_api(api) { api.kill() } -// 发送请求 +/** 发送请求到本地 api 服务 */ async function send(path, method, headers) { - const result = await fetch("http://127.0.0.1:3000" + path, { - method: method, - headers: headers + const result = await fetch('http://127.0.0.1:3000' + path, { + method, + headers, }).then(r => r.json()) - // console.log(result) return result } From 4192abc4fb15226c9b57994ad146f780d061ea1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 13:18:37 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 918a5d9..5acb26a 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,9 @@ API源代码来自 [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicAp 项目中包含两类令牌: -1. **GitHub Personal Access Token (PAT)**:用于自动将酷狗登录信息写入仓库 Secret `USERINFO`,以及每周日自动刷新酷狗登录 Token。**不建议移除**,移除后需手动复制登录信息,且 Token 约 2 个月后过期需重新登录。 +1. **GitHub Personal Access Token (PAT)**:用于自动将酷狗登录信息写入仓库 Secret `USERINFO`,以及每周日自动刷新酷狗登录 Token。 -2. **酷狗登录 Token**:存储在 `USERINFO` Secret 中,用于酷狗 API 身份认证。通过登录获取,每周日自动刷新。此为签到核心凭证,不可移除。 +2. **酷狗登录 Token**:存储在 `USERINFO` Secret 中,用于酷狗 API 身份认证。通过登录获取,每周日自动刷新。 ## Secret 位置 From 449ac0a02dd56f2953d87097532db2b2edb7cf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 13:38:29 +0800 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BA=8C?= =?UTF-8?q?=E7=BB=B4=E7=A0=81=E6=98=BE=E7=A4=BA+=E5=90=88=E5=B9=B6workflow?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化qrcodeLogin: 使用qrcode包在终端直接渲染ASCII二维码,替代base64文本输出 - 合并5个workflows为3个: 签到/账号登录/仓库保活 - 账号登录workflow整合二维码登录/发送验证码/手机号登录,通过下拉选择 - 所有workflow中文化命名 - 更新README反映新的workflow结构 --- .github/workflows/phoneLogin.yml | 66 ------------------- .github/workflows/sent.yml | 36 ---------- ...3\345\272\223\344\277\235\346\264\273.yml" | 20 +++--- .../workflows/\347\255\276\345\210\260.yml" | 11 ++-- ...6\345\217\267\347\231\273\345\275\225.yml" | 48 +++++++++++--- README.md | 12 ++-- qrcodeLogin.js | 33 ++++++++-- 7 files changed, 85 insertions(+), 141 deletions(-) delete mode 100644 .github/workflows/phoneLogin.yml delete mode 100644 .github/workflows/sent.yml rename .github/workflows/keepalive.yml => ".github/workflows/\344\273\223\345\272\223\344\277\235\346\264\273.yml" (64%) rename .github/workflows/main.yml => ".github/workflows/\347\255\276\345\210\260.yml" (94%) rename .github/workflows/qrcodeLogin.yml => ".github/workflows/\350\264\246\345\217\267\347\231\273\345\275\225.yml" (62%) diff --git a/.github/workflows/phoneLogin.yml b/.github/workflows/phoneLogin.yml deleted file mode 100644 index f993c35..0000000 --- a/.github/workflows/phoneLogin.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: phoneLogin -on: - workflow_dispatch: - inputs: - append_user: - description: "是否追加新账号?(不删除已存在账号信息)" - required: true - default: "否" - type: choice - options: - - 是 - - 否 - print_userinfo: - description: "自动写入失败时是否在日志输出 USERINFO?含登录token,默认否" - required: true - default: "否" - type: choice - options: - - 否 - - 是 -permissions: - contents: read -jobs: - run: - runs-on: ubuntu-latest - timeout-minutes: 2 - steps: - - name: 检出仓库 - uses: actions/checkout@v6 - - name: 创建node - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: 'npm' - cache-dependency-path: ./api/package-lock.json - - name: 🔧 检查GitHub CLI - run: | - if ! command -v gh >/dev/null 2>&1; then - curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg - sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null - sudo apt update - sudo apt install gh -y - fi - gh --version - - name: Cache node_modules in api directory - id: cache-npm - uses: actions/cache@v5 - with: - path: ./api/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - name: 安装依赖 - run: npm run install - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - - name: 手机号登录 - run: npm run phoneLogin - env: - PHONE: ${{ secrets.PHONE }} - CODE: ${{ secrets.CODE }} - PAT: ${{ secrets.PAT }} - GITHUB_REPOSITORY: ${{ github.repository }} - USERINFO: ${{ secrets.USERINFO }} - APPEND_USER: ${{ github.event.inputs.append_user }} - ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} diff --git a/.github/workflows/sent.yml b/.github/workflows/sent.yml deleted file mode 100644 index 811b8a4..0000000 --- a/.github/workflows/sent.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: sent -on: - workflow_dispatch: - # push: - # schedule: - # - cron: 1 16 * * * -permissions: - contents: read -jobs: - run: - runs-on: ubuntu-latest - timeout-minutes: 2 - steps: - - name: 检出仓库 - uses: actions/checkout@v6 - - name: 创建node - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: 'npm' - cache-dependency-path: ./api/package-lock.json - - name: Cache node_modules in api directory - id: cache-npm - uses: actions/cache@v5 - with: - path: ./api/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - name: 安装依赖 - run: npm run install - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - - name: 发送验证码 - run: npm run sent - env: - PHONE: ${{ secrets.PHONE }} diff --git a/.github/workflows/keepalive.yml "b/.github/workflows/\344\273\223\345\272\223\344\277\235\346\264\273.yml" similarity index 64% rename from .github/workflows/keepalive.yml rename to ".github/workflows/\344\273\223\345\272\223\344\277\235\346\264\273.yml" index 1eb55d4..f41d901 100644 --- a/.github/workflows/keepalive.yml +++ "b/.github/workflows/\344\273\223\345\272\223\344\277\235\346\264\273.yml" @@ -1,10 +1,10 @@ -name: Repository Keepalive +name: 仓库保活 on: schedule: - # 每30天运行一次(可自定义),避免仓库未更新导致Actions被禁用 + # 每30天运行一次,避免仓库未更新导致 Actions 被禁用 - cron: '0 0 */30 * *' - workflow_dispatch: # 允许手动触发 + workflow_dispatch: jobs: keepalive: @@ -13,14 +13,13 @@ jobs: contents: write steps: - - name: Checkout repository + - name: 检出仓库 uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Update keepalive timestamp + - name: 更新保活时间戳 run: | - # 创建或更新保活文件 mkdir -p .github cat > .github/KEEPALIVE.md << EOF # Repository Keepalive @@ -32,21 +31,20 @@ jobs: **Trigger:** ${{ github.event_name }} --- - *Auto-generated by [keepalive workflow](.github/workflows/keepalive.yml)* + *Auto-generated by [仓库保活 workflow](.github/workflows/仓库保活.yml)* EOF - - name: Commit and push changes + - name: 提交并推送 run: | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add .github/KEEPALIVE.md - # 只有文件变化时才提交 if git diff --staged --quiet; then - echo "No changes to commit" + echo "无变更" else git commit -m "chore: keepalive at $(date -u +'%Y-%m-%d')" git push - echo "✅ Keepalive commit pushed successfully" + echo "保活提交成功" fi diff --git a/.github/workflows/main.yml "b/.github/workflows/\347\255\276\345\210\260.yml" similarity index 94% rename from .github/workflows/main.yml rename to ".github/workflows/\347\255\276\345\210\260.yml" index 00fe163..130aa21 100644 --- a/.github/workflows/main.yml +++ "b/.github/workflows/\347\255\276\345\210\260.yml" @@ -1,7 +1,6 @@ -name: main +name: 签到 on: workflow_dispatch: - # push: schedule: - cron: '10 17 * * *' permissions: @@ -12,13 +11,13 @@ jobs: steps: - name: 检出仓库 uses: actions/checkout@v6 - - name: 创建node + - name: 创建 Node 环境 uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' cache-dependency-path: ./api/package-lock.json - - name: 🔧 检查GitHub CLI + - name: 检查 GitHub CLI run: | if ! command -v gh >/dev/null 2>&1; then curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg @@ -28,7 +27,7 @@ jobs: sudo apt install gh -y fi gh --version - - name: Cache node_modules in api directory + - name: 缓存依赖 id: cache-npm uses: actions/cache@v5 with: @@ -39,7 +38,7 @@ jobs: - name: 安装依赖 run: npm run install if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - - name: 签到 + - name: 执行签到 run: npm run main env: USERINFO: ${{ secrets.USERINFO }} diff --git a/.github/workflows/qrcodeLogin.yml "b/.github/workflows/\350\264\246\345\217\267\347\231\273\345\275\225.yml" similarity index 62% rename from .github/workflows/qrcodeLogin.yml rename to ".github/workflows/\350\264\246\345\217\267\347\231\273\345\275\225.yml" index 5490d05..ceb016c 100644 --- a/.github/workflows/qrcodeLogin.yml +++ "b/.github/workflows/\350\264\246\345\217\267\347\231\273\345\275\225.yml" @@ -1,24 +1,33 @@ -name: qrcodeLogin +name: 账号登录 on: workflow_dispatch: inputs: - number: - description: "需要登录几个账号?" + login_method: + description: "登录方式" required: true - default: 1 + type: choice + default: "二维码登录" + options: + - 二维码登录 + - 发送验证码 + - 手机号登录 + number: + description: "二维码登录:需要登录几个账号?" + required: false + default: "1" append_user: description: "是否追加新账号?(不删除已存在账号信息)" required: true - default: "否" type: choice + default: "否" options: - 是 - 否 print_userinfo: - description: "自动写入失败时是否在日志输出 USERINFO?含登录token,默认否" + description: "自动写入失败时是否在日志输出 USERINFO?(含登录token,默认否)" required: true - default: "否" type: choice + default: "否" options: - 否 - 是 @@ -27,16 +36,18 @@ permissions: jobs: run: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: 检出仓库 uses: actions/checkout@v6 - - name: 创建node + - name: 创建 Node 环境 uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' cache-dependency-path: ./api/package-lock.json - - name: 🔧 检查GitHub CLI + - name: 检查 GitHub CLI + if: github.event.inputs.login_method != '发送验证码' run: | if ! command -v gh >/dev/null 2>&1; then curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg @@ -46,7 +57,7 @@ jobs: sudo apt install gh -y fi gh --version - - name: Cache node_modules in api directory + - name: 缓存依赖 id: cache-npm uses: actions/cache@v5 with: @@ -58,6 +69,7 @@ jobs: run: npm run install if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - name: 二维码登录 + if: github.event.inputs.login_method == '二维码登录' run: npm run qrcodeLogin env: NUMBER: ${{ github.event.inputs.number }} @@ -66,3 +78,19 @@ jobs: USERINFO: ${{ secrets.USERINFO }} APPEND_USER: ${{ github.event.inputs.append_user }} ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} + - name: 发送验证码 + if: github.event.inputs.login_method == '发送验证码' + run: npm run sent + env: + PHONE: ${{ secrets.PHONE }} + - name: 手机号登录 + if: github.event.inputs.login_method == '手机号登录' + run: npm run phoneLogin + env: + PHONE: ${{ secrets.PHONE }} + CODE: ${{ secrets.CODE }} + PAT: ${{ secrets.PAT }} + GITHUB_REPOSITORY: ${{ github.repository }} + USERINFO: ${{ secrets.USERINFO }} + APPEND_USER: ${{ github.event.inputs.append_user }} + ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} diff --git a/README.md b/README.md index 5acb26a..0fb6bd9 100644 --- a/README.md +++ b/README.md @@ -44,17 +44,17 @@ GitHub Actions 实现 `酷狗概念VIP` 自动签到,每天领取总计 `两 - 滑动到底部,点击绿色的 Generate token 保存按钮 - 复制生成的字符串,回到本仓库添加到`Secret`,变量名 `PAT`,value 为复制的令牌 -1. 登录(两种登录方式任选其一) +1. 登录(运行 Actions `账号登录`,选择登录方式) - 3.1 二维码(推荐) + 3.1 二维码登录(推荐) - 运行 Actions `qrcodeLogin` 并进入(若不显示,可以刷新页面),点击run -> 展开二维码登录, 根据提示操作即可。 + 运行 Actions `账号登录`,登录方式选择「二维码登录」,点击 Run → 在日志中查看二维码,使用酷狗音乐 APP 扫码并确认登录即可。 - 3.2 手机号 + 3.2 手机号登录 - 添加手机号到 Secret `PHONE`,运行 Actions `sent` 获取验证码,把验证码添加到 Secret `CODE`;运行Actions `phoneLogin` ,成功即可 + 添加手机号到 Secret `PHONE`,运行 Actions `账号登录`,登录方式选择「发送验证码」获取验证码,把验证码添加到 Secret `CODE`;再次运行 Actions `账号登录`,登录方式选择「手机号登录」即可。 -1. 启用 Actions `main` , 每天北京时间 01:15 自动签到(可在`main.yml`中设置cron)。启用 Actions `Repository Keepalive` 以保证签到可以长期执行。 +1. 启用 Actions `签到`,每天北京时间 01:15 自动签到(可在 `签到.yml` 中设置 cron)。启用 Actions `仓库保活` 以保证签到可以长期执行。 1. (可选)配置运行结果通知 diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 5f19e5e..0dfdb77 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -1,8 +1,30 @@ +import { createRequire } from 'module' import { close_api, delay, send, startService } from "./utils/utils.js"; import { printGreen, printMagenta, printRed, printYellow } from "./utils/colorOut.js"; import { summarizeResponse } from "./utils/safeLog.js"; import { upsertUser, saveUserinfo } from "./utils/userinfo.js"; +const require = createRequire(import.meta.url) +const QRCode = require('./api/node_modules/qrcode') + +/** + * 在终端渲染二维码 + * @param {string} url - 需要编码为二维码的 URL + */ +async function printQrcode(url) { + try { + const qrTerminal = await QRCode.toString(url, { + type: 'terminal', + small: true, + }) + console.log(qrTerminal) + } catch { + // 降级:输出 URL 供手动打开 + printYellow(`二维码渲染失败,请手动打开此链接扫码:`) + console.log(url) + } +} + async function qrcode() { // 启动服务 @@ -20,12 +42,11 @@ async function qrcode() { const result = await send(`/login/qr/key?timestrap=${Date.now()}`, "GET", {}) if (result.status === 1) { qrcode = result.data.qrcode - const img_base64 = result.data.qrcode_img; - const chunkSize = 1000; - printMagenta("二维码链接如下, 请在浏览器打开使用APP扫描并确认登录") - for (let j = 0; j < img_base64.length; j += chunkSize) { - console.log(img_base64.slice(j, j + chunkSize)); - } + const qrUrl = `https://h5.kugou.com/apps/loginQRCode/html/index.html?qrcode=${qrcode}` + printMagenta("请使用酷狗音乐 APP 扫描下方二维码登录") + await printQrcode(qrUrl) + printMagenta(`如二维码无法扫描,请复制此链接到浏览器打开:`) + printMagenta(qrUrl) } else { printRed("响应内容") console.dir(summarizeResponse(result), { depth: null }) From a5c2d09698f7c097d310ade03df7a2bbf88bb183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 13:48:06 +0800 Subject: [PATCH 04/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BA=8C?= =?UTF-8?q?=E7=BB=B4=E7=A0=81=E6=98=BE=E7=A4=BA+=E6=8B=86=E5=88=86?= =?UTF-8?q?=E7=99=BB=E5=BD=95workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 二维码改用 GITHUB_STEP_SUMMARY 渲染真实PNG图片,替代终端ASCII码显示 - 拆分账号登录.yml为独立的二维码登录.yml和手机号登录.yml - 二维码登录: 独立workflow,二维码显示为图片 - 手机号登录: 独立workflow,含发送验证码和登录两个步骤 - 更新README反映新的workflow结构 --- ...4\347\240\201\347\231\273\345\275\225.yml" | 69 +++++++++++++++++++ ...2\345\217\267\347\231\273\345\275\225.yml" | 33 +++------ README.md | 6 +- qrcodeLogin.js | 44 ++++++++---- 4 files changed, 110 insertions(+), 42 deletions(-) create mode 100644 ".github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" rename ".github/workflows/\350\264\246\345\217\267\347\231\273\345\275\225.yml" => ".github/workflows/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225.yml" (71%) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" new file mode 100644 index 0000000..e38b580 --- /dev/null +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -0,0 +1,69 @@ +name: 二维码登录 +on: + workflow_dispatch: + inputs: + number: + description: "需要登录几个账号?" + required: true + default: "1" + append_user: + description: "是否追加新账号?(不删除已存在账号信息)" + required: true + type: choice + default: "否" + options: + - 是 + - 否 + print_userinfo: + description: "自动写入失败时是否在日志输出 USERINFO?(含登录token,默认否)" + required: true + type: choice + default: "否" + options: + - 否 + - 是 +permissions: + contents: read +jobs: + run: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: 检出仓库 + uses: actions/checkout@v6 + - name: 创建 Node 环境 + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: 'npm' + cache-dependency-path: ./api/package-lock.json + - name: 检查 GitHub CLI + run: | + if ! command -v gh >/dev/null 2>&1; then + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg + sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null + sudo apt update + sudo apt install gh -y + fi + gh --version + - name: 缓存依赖 + id: cache-npm + uses: actions/cache@v5 + with: + path: ./api/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: 安装依赖 + run: npm run install + if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} + - name: 二维码登录 + run: npm run qrcodeLogin + env: + NUMBER: ${{ github.event.inputs.number }} + PAT: ${{ secrets.PAT }} + GITHUB_REPOSITORY: ${{ github.repository }} + USERINFO: ${{ secrets.USERINFO }} + APPEND_USER: ${{ github.event.inputs.append_user }} + ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} diff --git "a/.github/workflows/\350\264\246\345\217\267\347\231\273\345\275\225.yml" "b/.github/workflows/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225.yml" similarity index 71% rename from ".github/workflows/\350\264\246\345\217\267\347\231\273\345\275\225.yml" rename to ".github/workflows/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225.yml" index ceb016c..2b405a4 100644 --- "a/.github/workflows/\350\264\246\345\217\267\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225.yml" @@ -1,20 +1,15 @@ -name: 账号登录 +name: 手机号登录 on: workflow_dispatch: inputs: - login_method: - description: "登录方式" + step: + description: "操作步骤" required: true type: choice - default: "二维码登录" + default: "发送验证码" options: - - 二维码登录 - 发送验证码 - - 手机号登录 - number: - description: "二维码登录:需要登录几个账号?" - required: false - default: "1" + - 登录 append_user: description: "是否追加新账号?(不删除已存在账号信息)" required: true @@ -36,7 +31,7 @@ permissions: jobs: run: runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 2 steps: - name: 检出仓库 uses: actions/checkout@v6 @@ -47,7 +42,7 @@ jobs: cache: 'npm' cache-dependency-path: ./api/package-lock.json - name: 检查 GitHub CLI - if: github.event.inputs.login_method != '发送验证码' + if: github.event.inputs.step == '登录' run: | if ! command -v gh >/dev/null 2>&1; then curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg @@ -68,23 +63,13 @@ jobs: - name: 安装依赖 run: npm run install if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - - name: 二维码登录 - if: github.event.inputs.login_method == '二维码登录' - run: npm run qrcodeLogin - env: - NUMBER: ${{ github.event.inputs.number }} - PAT: ${{ secrets.PAT }} - GITHUB_REPOSITORY: ${{ github.repository }} - USERINFO: ${{ secrets.USERINFO }} - APPEND_USER: ${{ github.event.inputs.append_user }} - ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} - name: 发送验证码 - if: github.event.inputs.login_method == '发送验证码' + if: github.event.inputs.step == '发送验证码' run: npm run sent env: PHONE: ${{ secrets.PHONE }} - name: 手机号登录 - if: github.event.inputs.login_method == '手机号登录' + if: github.event.inputs.step == '登录' run: npm run phoneLogin env: PHONE: ${{ secrets.PHONE }} diff --git a/README.md b/README.md index 0fb6bd9..8d8d594 100644 --- a/README.md +++ b/README.md @@ -44,15 +44,15 @@ GitHub Actions 实现 `酷狗概念VIP` 自动签到,每天领取总计 `两 - 滑动到底部,点击绿色的 Generate token 保存按钮 - 复制生成的字符串,回到本仓库添加到`Secret`,变量名 `PAT`,value 为复制的令牌 -1. 登录(运行 Actions `账号登录`,选择登录方式) +1. 登录(两种独立的登录方式,任选其一) 3.1 二维码登录(推荐) - 运行 Actions `账号登录`,登录方式选择「二维码登录」,点击 Run → 在日志中查看二维码,使用酷狗音乐 APP 扫码并确认登录即可。 + 运行 Actions `二维码登录`,点击 Run → 在运行摘要页面(Summary)查看二维码图片,使用酷狗音乐 APP 扫码并确认登录即可。 3.2 手机号登录 - 添加手机号到 Secret `PHONE`,运行 Actions `账号登录`,登录方式选择「发送验证码」获取验证码,把验证码添加到 Secret `CODE`;再次运行 Actions `账号登录`,登录方式选择「手机号登录」即可。 + 添加手机号到 Secret `PHONE`,运行 Actions `手机号登录`,操作步骤选择「发送验证码」获取验证码,把验证码添加到 Secret `CODE`;再次运行 Actions `手机号登录`,操作步骤选择「登录」即可。 1. 启用 Actions `签到`,每天北京时间 01:15 自动签到(可在 `签到.yml` 中设置 cron)。启用 Actions `仓库保活` 以保证签到可以长期执行。 diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 0dfdb77..981c349 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -1,4 +1,5 @@ import { createRequire } from 'module' +import fs from 'node:fs' import { close_api, delay, send, startService } from "./utils/utils.js"; import { printGreen, printMagenta, printRed, printYellow } from "./utils/colorOut.js"; import { summarizeResponse } from "./utils/safeLog.js"; @@ -8,19 +9,35 @@ const require = createRequire(import.meta.url) const QRCode = require('./api/node_modules/qrcode') /** - * 在终端渲染二维码 + * 显示二维码 + * 在 GitHub Actions 中写入 Step Summary 以渲染真实图片; + * 在本地终端输出可扫码链接作为降级。 * @param {string} url - 需要编码为二维码的 URL */ -async function printQrcode(url) { - try { - const qrTerminal = await QRCode.toString(url, { - type: 'terminal', - small: true, - }) - console.log(qrTerminal) - } catch { - // 降级:输出 URL 供手动打开 - printYellow(`二维码渲染失败,请手动打开此链接扫码:`) +async function displayQrcode(url) { + // 生成 base64 PNG 图片 + const dataUrl = await QRCode.toDataURL(url, { width: 256, margin: 2 }) + + // 写入 GitHub Actions Step Summary(运行摘要页面直接显示图片) + const stepSummary = process.env.GITHUB_STEP_SUMMARY + if (stepSummary) { + const content = [ + '## 酷狗音乐扫码登录', + '', + ``, + '', + '请使用 **酷狗音乐 APP** 扫描上方二维码登录', + '', + `如图片未显示,请复制此链接到浏览器打开扫码:`, + '', + url, + '', + ].join('\n') + fs.writeFileSync(stepSummary, content) + printGreen('二维码已显示在运行摘要页面(Summary)中') + } else { + // 本地运行降级:输出链接 + printMagenta('请复制此链接到浏览器打开,使用酷狗音乐 APP 扫码登录:') console.log(url) } } @@ -43,10 +60,7 @@ async function qrcode() { if (result.status === 1) { qrcode = result.data.qrcode const qrUrl = `https://h5.kugou.com/apps/loginQRCode/html/index.html?qrcode=${qrcode}` - printMagenta("请使用酷狗音乐 APP 扫描下方二维码登录") - await printQrcode(qrUrl) - printMagenta(`如二维码无法扫描,请复制此链接到浏览器打开:`) - printMagenta(qrUrl) + await displayQrcode(qrUrl) } else { printRed("响应内容") console.dir(summarizeResponse(result), { depth: null }) From b8e6c45ab4f937d724264ffb1b3b42428625017a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:01:17 +0800 Subject: [PATCH 05/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DStep=20Summary?= =?UTF-8?q?=E4=BA=8C=E7=BB=B4=E7=A0=81=E5=9B=BE=E7=89=87=E4=B8=8D=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: GitHub的Markdown渲染器会过滤data: URI,导致被移除 修复方案: - Step Summary改用纯ASCII二维码(无ANSI码),在代码块中渲染 - 额外保存PNG文件,通过actions/upload-artifact上传为构件供下载 - 使用appendFileSync替代writeFileSync避免覆盖其他步骤的摘要 - 提供三层保障: ASCII渲染 + PNG下载 + URL文本 --- ...4\347\240\201\347\231\273\345\275\225.yml" | 7 +++ qrcodeLogin.js | 55 +++++++++++++++---- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index e38b580..70d2d95 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -67,3 +67,10 @@ jobs: USERINFO: ${{ secrets.USERINFO }} APPEND_USER: ${{ github.event.inputs.append_user }} ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} + - name: 上传二维码图片 + if: always() + uses: actions/upload-artifact@v4 + with: + name: 二维码 + path: qr-code.png + retention-days: 1 diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 981c349..3ceb6ad 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -8,35 +8,66 @@ import { upsertUser, saveUserinfo } from "./utils/userinfo.js"; const require = createRequire(import.meta.url) const QRCode = require('./api/node_modules/qrcode') +/** + * 渲染 QR 矩阵为纯 ASCII 文本(无 ANSI 转义码) + * 每个模块用双字符宽度,保证在等宽字体下比例正确 + * @param {string} url - 需要编码为二维码的 URL + * @returns {string} ASCII 二维码文本 + */ +function renderQrAscii(url) { + const qr = QRCode.create(url, { margin: 1 }) + const size = qr.modules.size + const get = qr.modules.get + let ascii = '' + for (let r = 0; r < size; r++) { + let line = '' + for (let c = 0; c < size; c++) { + line += get(c, r) ? '██' : ' ' + } + ascii += line + '\n' + } + return ascii +} + /** * 显示二维码 - * 在 GitHub Actions 中写入 Step Summary 以渲染真实图片; - * 在本地终端输出可扫码链接作为降级。 + * - GitHub Actions: 写入 Step Summary(ASCII 渲染) + 保存 PNG 供 artifact 下载 + * - 本地终端: 直接输出 ASCII + 链接 * @param {string} url - 需要编码为二维码的 URL */ async function displayQrcode(url) { - // 生成 base64 PNG 图片 - const dataUrl = await QRCode.toDataURL(url, { width: 256, margin: 2 }) + const ascii = renderQrAscii(url) + + // 保存 PNG 文件,供 workflow 上传为 artifact + try { + await QRCode.toFile('./qr-code.png', url, { width: 256, margin: 2 }) + } catch { + // PNG 保存失败不影响主流程 + } - // 写入 GitHub Actions Step Summary(运行摘要页面直接显示图片) const stepSummary = process.env.GITHUB_STEP_SUMMARY if (stepSummary) { const content = [ - '## 酷狗音乐扫码登录', + '## 🎵 酷狗音乐扫码登录', + '', + '请使用 **酷狗音乐 APP** 扫描下方二维码登录', '', - ``, + '```', + ascii, + '```', '', - '请使用 **酷狗音乐 APP** 扫描上方二维码登录', + '> 如上方二维码无法扫描,可在下方 **Artifacts** 区域下载 `qr-code.png` 高清图片', '', - `如图片未显示,请复制此链接到浏览器打开扫码:`, + '或复制此链接到浏览器打开扫码:', '', url, '', ].join('\n') - fs.writeFileSync(stepSummary, content) - printGreen('二维码已显示在运行摘要页面(Summary)中') + fs.appendFileSync(stepSummary, content) + printGreen('二维码已写入运行摘要页面(Summary)') } else { - // 本地运行降级:输出链接 + // 本地运行:直接输出到终端 + console.log(ascii) printMagenta('请复制此链接到浏览器打开,使用酷狗音乐 APP 扫码登录:') console.log(url) } From 0190b70ec29a4750a675a1117c1b7c619d959c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:08:12 +0800 Subject: [PATCH 06/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BA=8C?= =?UTF-8?q?=E7=BB=B4=E7=A0=81ASCII=E6=B8=B2=E6=9F=93=E5=B4=A9=E6=BA=83=20-?= =?UTF-8?q?=20this=E4=B8=8A=E4=B8=8B=E6=96=87=E4=B8=A2=E5=A4=B1+=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - const get = qr.modules.get 导致 this 丢失, 改为 modules.get(r,c) 直接调用 - get(c,r) 参数顺序写反, BitMatrix.get(row,col) 正确顺序为 (r,c) --- qrcodeLogin.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 3ceb6ad..7858b74 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -16,13 +16,13 @@ const QRCode = require('./api/node_modules/qrcode') */ function renderQrAscii(url) { const qr = QRCode.create(url, { margin: 1 }) - const size = qr.modules.size - const get = qr.modules.get + const modules = qr.modules + const size = modules.size let ascii = '' for (let r = 0; r < size; r++) { let line = '' for (let c = 0; c < size; c++) { - line += get(c, r) ? '██' : ' ' + line += modules.get(r, c) ? '██' : ' ' } ascii += line + '\n' } From abec112ae6318a4928c9a7e586835eaf3028dbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:17:12 +0800 Subject: [PATCH 07/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DSummary=E7=A9=BA?= =?UTF-8?q?=E7=99=BD=20-=20=E6=94=B9=E4=B8=BA=E6=97=A5=E5=BF=97=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E6=98=BE=E7=A4=BA+workflow=E5=86=99=E5=85=A5Summary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: GITHUB_STEP_SUMMARY只在step完成后才被GitHub读取渲染, 但二维码登录step需等待扫码2分钟, 期间Summary一直空白 修复方案: - ASCII二维码直接输出到step日志(纯Unicode无ANSI, 实时可见可扫码) - 同时写入qr-summary.md文件 - workflow新增步骤: step结束后cat qr-summary.md >> GITHUB_STEP_SUMMARY - 三层保障: 日志实时扫码 + Summary事后查看 + Artifacts下载PNG --- ...4\347\240\201\347\231\273\345\275\225.yml" | 6 ++ qrcodeLogin.js | 72 +++++++++++-------- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 70d2d95..93c5a96 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -67,6 +67,12 @@ jobs: USERINFO: ${{ secrets.USERINFO }} APPEND_USER: ${{ github.event.inputs.append_user }} ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} + - name: 写入二维码到运行摘要 + if: always() + run: | + if [ -f qr-summary.md ]; then + cat qr-summary.md >> $GITHUB_STEP_SUMMARY + fi - name: 上传二维码图片 if: always() uses: actions/upload-artifact@v4 diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 7858b74..cbd1247 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -15,7 +15,7 @@ const QRCode = require('./api/node_modules/qrcode') * @returns {string} ASCII 二维码文本 */ function renderQrAscii(url) { - const qr = QRCode.create(url, { margin: 1 }) + const qr = QRCode.create(url, { margin: 2 }) const modules = qr.modules const size = modules.size let ascii = '' @@ -31,45 +31,55 @@ function renderQrAscii(url) { /** * 显示二维码 - * - GitHub Actions: 写入 Step Summary(ASCII 渲染) + 保存 PNG 供 artifact 下载 - * - 本地终端: 直接输出 ASCII + 链接 + * - 日志: 直接输出 ASCII 二维码(纯 Unicode,无 ANSI 码) + * - Summary: 写入 qr-summary.md,供 workflow 在 step 结束后写入 $GITHUB_STEP_SUMMARY + * - PNG: 保存为文件供 artifact 下载 * @param {string} url - 需要编码为二维码的 URL + * @param {number} index - 当前是第几个二维码(从1开始) + * @param {number} total - 总共需要登录几个账号 */ -async function displayQrcode(url) { +async function displayQrcode(url, index, total) { const ascii = renderQrAscii(url) - // 保存 PNG 文件,供 workflow 上传为 artifact + // 1. 输出到日志(实时可见,用户可在此扫码) + const header = total > 1 ? `(第 ${index}/${total} 个账号)` : '' + printMagenta(`\n请使用酷狗音乐 APP 扫描下方二维码登录${header}`) + console.log(ascii) + printMagenta('如二维码无法扫描,请复制此链接到浏览器打开扫码:') + console.log(url) + console.log('') + + // 2. 保存 PNG 文件,供 workflow 上传为 artifact try { await QRCode.toFile('./qr-code.png', url, { width: 256, margin: 2 }) } catch { // PNG 保存失败不影响主流程 } - const stepSummary = process.env.GITHUB_STEP_SUMMARY - if (stepSummary) { - const content = [ - '## 🎵 酷狗音乐扫码登录', - '', - '请使用 **酷狗音乐 APP** 扫描下方二维码登录', - '', - '```', - ascii, - '```', - '', - '> 如上方二维码无法扫描,可在下方 **Artifacts** 区域下载 `qr-code.png` 高清图片', - '', - '或复制此链接到浏览器打开扫码:', - '', - url, - '', - ].join('\n') - fs.appendFileSync(stepSummary, content) - printGreen('二维码已写入运行摘要页面(Summary)') - } else { - // 本地运行:直接输出到终端 - console.log(ascii) - printMagenta('请复制此链接到浏览器打开,使用酷狗音乐 APP 扫码登录:') - console.log(url) + // 3. 写入 Markdown 文件,供 workflow 在 step 结束后写入 $GITHUB_STEP_SUMMARY + const md = [ + `## 🎵 酷狗音乐扫码登录${header}`, + '', + '请使用 **酷狗音乐 APP** 扫描下方二维码登录', + '', + '```', + ascii, + '```', + '', + `或复制此链接到浏览器打开扫码:`, + '', + url, + '', + ].join('\n') + + try { + if (index === 1) { + fs.writeFileSync('./qr-summary.md', md) + } else { + fs.appendFileSync('./qr-summary.md', '\n---\n\n' + md) + } + } catch { + // 文件写入失败不影响主流程 } } @@ -91,7 +101,7 @@ async function qrcode() { if (result.status === 1) { qrcode = result.data.qrcode const qrUrl = `https://h5.kugou.com/apps/loginQRCode/html/index.html?qrcode=${qrcode}` - await displayQrcode(qrUrl) + await displayQrcode(qrUrl, n + 1, number) } else { printRed("响应内容") console.dir(summarizeResponse(result), { depth: null }) From db372f4ed4d79fbd0ad86b6872815c111ec25643 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:29:05 +0000 Subject: [PATCH 08/18] chore: keepalive at 2026-07-11 --- .github/KEEPALIVE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/KEEPALIVE.md b/.github/KEEPALIVE.md index 217fd63..88832ee 100644 --- a/.github/KEEPALIVE.md +++ b/.github/KEEPALIVE.md @@ -2,9 +2,9 @@ This file is automatically updated to keep the repository active. -**Last Update:** 2026-07-01 02:49:14 UTC -**Workflow Run:** #3 -**Trigger:** schedule +**Last Update:** 2026-07-11 06:29:05 UTC +**Workflow Run:** #1 +**Trigger:** workflow_dispatch --- -*Auto-generated by [keepalive workflow](.github/workflows/keepalive.yml)* +*Auto-generated by [仓库保活 workflow](.github/workflows/仓库保活.yml)* From 650adb7e859eaa5a76d68aa73b0d3e8330658f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:32:42 +0800 Subject: [PATCH 09/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8DSummary=E7=A9=BA?= =?UTF-8?q?=E7=99=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\347\240\201\347\231\273\345\275\225.yml" | 23 ++- qrcodeLogin.js | 164 ++++++++++++------ 2 files changed, 119 insertions(+), 68 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 93c5a96..9d67c8a 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -27,7 +27,7 @@ permissions: jobs: run: runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 15 steps: - name: 检出仓库 uses: actions/checkout@v6 @@ -58,25 +58,24 @@ jobs: - name: 安装依赖 run: npm run install if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - - name: 二维码登录 - run: npm run qrcodeLogin + - name: 生成并展示二维码 + run: node qrcodeLogin.js gen env: NUMBER: ${{ github.event.inputs.number }} - PAT: ${{ secrets.PAT }} - GITHUB_REPOSITORY: ${{ github.repository }} + USERINFO: ${{ secrets.USERINFO }} + APPEND_USER: ${{ github.event.inputs.append_user }} + - name: 等待扫码登录 + run: node qrcodeLogin.js wait + env: USERINFO: ${{ secrets.USERINFO }} APPEND_USER: ${{ github.event.inputs.append_user }} ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} - - name: 写入二维码到运行摘要 - if: always() - run: | - if [ -f qr-summary.md ]; then - cat qr-summary.md >> $GITHUB_STEP_SUMMARY - fi + PAT: ${{ secrets.PAT }} + GITHUB_REPOSITORY: ${{ github.repository }} - name: 上传二维码图片 if: always() uses: actions/upload-artifact@v4 with: name: 二维码 - path: qr-code.png + path: qr/ retention-days: 1 diff --git a/qrcodeLogin.js b/qrcodeLogin.js index cbd1247..f4a45c5 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -8,11 +8,15 @@ import { upsertUser, saveUserinfo } from "./utils/userinfo.js"; const require = createRequire(import.meta.url) const QRCode = require('./api/node_modules/qrcode') +// GitHub Actions 运行环境下,step summary 文件路径由该变量提供 +const SUMMARY_FILE = process.env.GITHUB_STEP_SUMMARY +const QR_DIR = './qr' +const KEYS_FILE = './qrkeys.json' + /** - * 渲染 QR 矩阵为纯 ASCII 文本(无 ANSI 转义码) - * 每个模块用双字符宽度,保证在等宽字体下比例正确 - * @param {string} url - 需要编码为二维码的 URL - * @returns {string} ASCII 二维码文本 + * 渲染 QR 矩阵为纯 ASCII 文本(无 ANSI 转义码),用于实时日志扫码 + * @param {string} url + * @returns {string} */ function renderQrAscii(url) { const qr = QRCode.create(url, { margin: 2 }) @@ -30,92 +34,139 @@ function renderQrAscii(url) { } /** - * 显示二维码 - * - 日志: 直接输出 ASCII 二维码(纯 Unicode,无 ANSI 码) - * - Summary: 写入 qr-summary.md,供 workflow 在 step 结束后写入 $GITHUB_STEP_SUMMARY - * - PNG: 保存为文件供 artifact 下载 - * @param {string} url - 需要编码为二维码的 URL - * @param {number} index - 当前是第几个二维码(从1开始) - * @param {number} total - 总共需要登录几个账号 + * 向 GitHub Step Summary 追加内容(本地或非 Actions 环境自动跳过) + * @param {string} markdown */ -async function displayQrcode(url, index, total) { - const ascii = renderQrAscii(url) +function appendSummary(markdown) { + if (!SUMMARY_FILE) return + try { + fs.appendFileSync(SUMMARY_FILE, markdown) + } catch { + // 写入摘要失败不影响主流程 + } +} - // 1. 输出到日志(实时可见,用户可在此扫码) +/** + * 生成并展示单个二维码 + * - 日志: 实时输出 ASCII 二维码(用户可在运行日志直接扫码) + * - Summary: 以真实 PNG 图片(data URI)嵌入运行摘要,扫码页可直接查看 + * - PNG: 保存为文件,供 artifact 下载 + * @param {string} url + * @param {number} index 从 1 开始 + * @param {number} total + */ +async function buildQr(url, index, total) { + const ascii = renderQrAscii(url) const header = total > 1 ? `(第 ${index}/${total} 个账号)` : '' + + // 1) 实时日志 ASCII(可在运行日志中直接扫码) printMagenta(`\n请使用酷狗音乐 APP 扫描下方二维码登录${header}`) console.log(ascii) printMagenta('如二维码无法扫描,请复制此链接到浏览器打开扫码:') console.log(url) console.log('') - // 2. 保存 PNG 文件,供 workflow 上传为 artifact - try { - await QRCode.toFile('./qr-code.png', url, { width: 256, margin: 2 }) - } catch { - // PNG 保存失败不影响主流程 - } + // 2) 生成 PNG 文件(artifact 下载 + Summary 内嵌图片) + fs.mkdirSync(QR_DIR, { recursive: true }) + await QRCode.toFile(`${QR_DIR}/qr-${index}.png`, url, { width: 320, margin: 2 }) - // 3. 写入 Markdown 文件,供 workflow 在 step 结束后写入 $GITHUB_STEP_SUMMARY - const md = [ + // 3) 在运行摘要(Summary)中嵌入真实可扫的二维码图片 + const dataUrl = await QRCode.toDataURL(url, { width: 320, margin: 2 }) + appendSummary([ `## 🎵 酷狗音乐扫码登录${header}`, '', - '请使用 **酷狗音乐 APP** 扫描下方二维码登录', + '请使用 **酷狗音乐 APP** 扫描下方二维码登录(图片可在运行摘要页直接查看):', + '', + `酷狗扫码登录二维码${header}`, + '', + '如图片无法加载,可复制以下链接到浏览器打开:', + '', + url, + '', + '
字符版二维码(备用)', '', '```', ascii, '```', '', - `或复制此链接到浏览器打开扫码:`, + '
', '', - url, + '---', '', - ].join('\n') - - try { - if (index === 1) { - fs.writeFileSync('./qr-summary.md', md) - } else { - fs.appendFileSync('./qr-summary.md', '\n---\n\n' + md) - } - } catch { - // 文件写入失败不影响主流程 - } + ].join('\n')) } -async function qrcode() { +/** 解析账号数量 */ +function resolveNumber() { + const args = process.argv.slice(3) // 跳过 node、脚本名、模式参数 + return parseInt(process.env.NUMBER || args[0] || "1") +} - // 启动服务 +/** + * 模式一:生成二维码并写入运行摘要,随后立即结束 step + * 拆成独立 step 的目的是——GitHub 会在 step 结束后刷新 Summary 页, + * 这样用户在“等待扫码”step 期间就能在 Summary 看到真实二维码图片去扫码。 + */ +async function genMode() { const api = startService() await delay(2000) - let qrcode = "" const USERINFO = process.env.USERINFO const APPEND_USER = process.env.APPEND_USER const userinfo = (USERINFO && APPEND_USER == "是") ? JSON.parse(USERINFO) : [] - const args = process.argv.slice(2); - const number = parseInt(process.env.NUMBER || args[0] || "1") + const number = resolveNumber() + const keys = [] try { for (let n = 0; n < number; n++) { - // 二维码 const result = await send(`/login/qr/key?timestrap=${Date.now()}`, "GET", {}) if (result.status === 1) { - qrcode = result.data.qrcode + const qrcode = result.data.qrcode const qrUrl = `https://h5.kugou.com/apps/loginQRCode/html/index.html?qrcode=${qrcode}` - await displayQrcode(qrUrl, n + 1, number) + keys.push(qrcode) + await buildQr(qrUrl, n + 1, number) } else { printRed("响应内容") console.dir(summarizeResponse(result), { depth: null }) throw new Error("请求出错") } - printMagenta("正在等待,请扫描二维码并确定登录") - // 登录 - for (let i = 0; i < 25; i++) { + } + fs.writeFileSync(KEYS_FILE, JSON.stringify({ number, keys })) + } finally { + close_api(api) + } + + printMagenta(`\n已生成 ${number} 个二维码。本步骤结束后,请打开本次运行的“Summary(摘要)”页面,`) + printMagenta(`在页面中扫描二维码图片;随后工作流会自动进入“等待扫码”步骤完成登录。`) +} + +/** + * 模式二:读取已生成的二维码密钥,轮询等待用户扫码确认 + */ +async function waitMode() { + const api = startService() + await delay(2000) + let parsed + try { + parsed = JSON.parse(fs.readFileSync(KEYS_FILE, 'utf8')) + } catch { + throw new Error("未找到二维码密钥文件,请确认已先运行“生成并展示二维码”步骤") + } + const { number, keys } = parsed + const USERINFO = process.env.USERINFO + const APPEND_USER = process.env.APPEND_USER + const userinfo = (USERINFO && APPEND_USER == "是") ? JSON.parse(USERINFO) : [] + + try { + for (let n = 0; n < number; n++) { + const qrcode = keys[n] + printMagenta(`\n正在等待第 ${n + 1}/${number} 个账号扫码登录...`) + let loggedIn = false + for (let i = 0; i < 30; i++) { const timestrap = Date.now(); const res = await send(`/login/qr/check?key=${qrcode}×trap=${timestrap}`, "GET", {}) const status = res?.data?.status switch (status) { case 0: - printYellow("二维码已过期") + printYellow("二维码已过期,请重新运行工作流生成新二维码") break case 1: @@ -129,18 +180,18 @@ async function qrcode() { case 4: printGreen("登录成功!") upsertUser(userinfo, { userid: res.data.userid, token: res.data.token }, APPEND_USER == "是") + loggedIn = true break default: printRed("请求出错") console.dir(summarizeResponse(res), { depth: null }) } - if (status == 4 || status == 0) { + if (loggedIn || status == 0) { break } - if (i == 24) { + if (i == 29) { printRed("等待超时\n") - break } await delay(5000) } @@ -149,10 +200,11 @@ async function qrcode() { } finally { close_api(api) } - - if (api.killed) { - process.exit(0) - } } -qrcode() +const mode = process.argv[2] || 'gen' +if (mode === 'wait') { + waitMode() +} else { + genMode() +} From c6f4073ce9242257dd4b96a4424fd9968c5fa284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:40:52 +0800 Subject: [PATCH 10/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8DSummary=E7=A9=BA?= =?UTF-8?q?=E7=99=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\347\240\201\347\231\273\345\275\225.yml" | 44 ++++++++++- qrcodeLogin.js | 77 ++++++++++--------- 2 files changed, 82 insertions(+), 39 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 9d67c8a..5c6d4c4 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -58,13 +58,51 @@ jobs: - name: 安装依赖 run: npm run install if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - - name: 生成并展示二维码 + + # ── 步骤一:生成二维码(落盘字符画文件 + PNG + Summary + 密钥)── + - name: 生成登录二维码 + id: gen run: node qrcodeLogin.js gen env: NUMBER: ${{ github.event.inputs.number }} USERINFO: ${{ secrets.USERINFO }} APPEND_USER: ${{ github.event.inputs.append_user }} + + # ── 步骤二:在运行日志中直接输出可扫描的二维码字符画 ── + # 注意:不使用 ::group:: 折叠,确保二维码在日志中直接可见、便于手机扫描 + - name: 在运行日志中输出二维码 + id: showqr + if: ${{ success() && steps.gen.outcome == 'success' }} + run: | + echo "════════════════════════════════════════════════════════" + echo " 请使用「酷狗音乐 APP」扫描下方二维码完成登录" + echo "════════════════════════════════════════════════════════" + echo "" + if [ -f qr/qr-ascii.txt ]; then + cat qr/qr-ascii.txt + else + echo "::error::未找到二维码字符画文件,生成步骤可能未成功执行。" + fi + echo "" + echo "────────────────────────────────────────────────────────" + echo "如无法直接扫描,可复制以下链接到酷狗音乐 App 内扫码:" + if [ -f qr/qr-urls.txt ]; then + cat qr/qr-urls.txt + fi + echo "" + echo "(二维码同时已写入本次运行的【Summary 摘要】页面,可点击查看清晰图片)" + + # ── 错误处理:生成步骤失败时给出明确提示 ── + - name: 二维码生成失败提示 + if: ${{ failure() && steps.gen.outcome == 'failure' }} + run: | + echo "::error::二维码生成失败,无法继续登录。" + echo "请检查:1) 运行器网络能否访问酷狗接口;2) 依赖是否安装成功(查看上方「安装依赖」步骤)。" + + # ── 步骤三:轮询等待用户扫码确认 ── - name: 等待扫码登录 + id: wait + if: ${{ success() && steps.gen.outcome == 'success' }} run: node qrcodeLogin.js wait env: USERINFO: ${{ secrets.USERINFO }} @@ -72,8 +110,10 @@ jobs: ALLOW_PRINT_USERINFO: ${{ github.event.inputs.print_userinfo }} PAT: ${{ secrets.PAT }} GITHUB_REPOSITORY: ${{ github.repository }} + + # ── 上传二维码图片作为备用(仅在存在时)── - name: 上传二维码图片 - if: always() + if: ${{ always() && hashFiles('qr/*') != '' }} uses: actions/upload-artifact@v4 with: name: 二维码 diff --git a/qrcodeLogin.js b/qrcodeLogin.js index f4a45c5..cf2f83d 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -8,29 +8,22 @@ import { upsertUser, saveUserinfo } from "./utils/userinfo.js"; const require = createRequire(import.meta.url) const QRCode = require('./api/node_modules/qrcode') -// GitHub Actions 运行环境下,step summary 文件路径由该变量提供 +// GitHub Actions 运行环境下,step summary 文件路径由该变量提供(Actions 自动注入) const SUMMARY_FILE = process.env.GITHUB_STEP_SUMMARY const QR_DIR = './qr' const KEYS_FILE = './qrkeys.json' +const ASCII_FILE = `${QR_DIR}/qr-ascii.txt` // 供「日志输出步骤」直接 cat 到运行日志 +const URLS_FILE = `${QR_DIR}/qr-urls.txt` // 扫码失败的链接兜底 /** - * 渲染 QR 矩阵为纯 ASCII 文本(无 ANSI 转义码),用于实时日志扫码 + * 渲染 QR 为 UTF-8 半块字符画(每个字符纵向承载 2 个模块), + * 正好补偿终端字符“高>宽”的比例,在 GitHub Actions 运行日志里可直接用手机扫描。 + * 相比双宽 '██' 方案(横纵比 2:1 被拉伸、常扫不上),半块更接近正方形、可扫性更好。 * @param {string} url - * @returns {string} + * @returns {Promise} */ -function renderQrAscii(url) { - const qr = QRCode.create(url, { margin: 2 }) - const modules = qr.modules - const size = modules.size - let ascii = '' - for (let r = 0; r < size; r++) { - let line = '' - for (let c = 0; c < size; c++) { - line += modules.get(r, c) ? '██' : ' ' - } - ascii += line + '\n' - } - return ascii +function renderQrUtf8(url) { + return QRCode.toString(url, { type: 'utf8', margin: 2 }) } /** @@ -47,27 +40,27 @@ function appendSummary(markdown) { } /** - * 生成并展示单个二维码 - * - 日志: 实时输出 ASCII 二维码(用户可在运行日志直接扫码) - * - Summary: 以真实 PNG 图片(data URI)嵌入运行摘要,扫码页可直接查看 - * - PNG: 保存为文件,供 artifact 下载 + * 生成并落盘单个二维码: + * - qr/qr-ascii.txt : UTF-8 半块字符画(日志输出步骤 cat 它即可在运行日志扫码) + * - qr/qr-urls.txt : 扫码失败的链接兜底 + * - qr/qr-N.png : PNG 文件,供 artifact 下载 + * - Summary : 以真实 PNG 图片(data URI)嵌入运行摘要,扫码页可直接查看 * @param {string} url * @param {number} index 从 1 开始 * @param {number} total */ async function buildQr(url, index, total) { - const ascii = renderQrAscii(url) + const utf8 = await renderQrUtf8(url) const header = total > 1 ? `(第 ${index}/${total} 个账号)` : '' - // 1) 实时日志 ASCII(可在运行日志中直接扫码) - printMagenta(`\n请使用酷狗音乐 APP 扫描下方二维码登录${header}`) - console.log(ascii) - printMagenta('如二维码无法扫描,请复制此链接到浏览器打开扫码:') - console.log(url) - console.log('') + fs.mkdirSync(QR_DIR, { recursive: true }) + + // 1) 字符画落盘,供日志输出步骤展示(核心是这一步,保证日志里可直接扫) + const block = (total > 1 ? `# 账号 ${index}/${total}\n` : '') + utf8 + '\n' + fs.appendFileSync(ASCII_FILE, block) + fs.appendFileSync(URLS_FILE, (total > 1 ? `账号 ${index}/${total}: ` : '') + url + '\n') // 2) 生成 PNG 文件(artifact 下载 + Summary 内嵌图片) - fs.mkdirSync(QR_DIR, { recursive: true }) await QRCode.toFile(`${QR_DIR}/qr-${index}.png`, url, { width: 320, margin: 2 }) // 3) 在运行摘要(Summary)中嵌入真实可扫的二维码图片 @@ -79,14 +72,14 @@ async function buildQr(url, index, total) { '', `酷狗扫码登录二维码${header}`, '', - '如图片无法加载,可复制以下链接到浏览器打开:', + '如图片无法加载,可复制以下链接到浏览器/酷狗 App 打开:', '', url, '', '
字符版二维码(备用)', '', '```', - ascii, + utf8, '```', '', '
', @@ -94,6 +87,8 @@ async function buildQr(url, index, total) { '---', '', ].join('\n')) + + printMagenta(`✅ 已生成第 ${index}/${total} 个登录二维码(UTF-8 字符画),已写入日志文件。`) } /** 解析账号数量 */ @@ -103,9 +98,9 @@ function resolveNumber() { } /** - * 模式一:生成二维码并写入运行摘要,随后立即结束 step - * 拆成独立 step 的目的是——GitHub 会在 step 结束后刷新 Summary 页, - * 这样用户在“等待扫码”step 期间就能在 Summary 看到真实二维码图片去扫码。 + * 模式一:生成二维码并落盘(字符画文件 + PNG + Summary),随后立即结束 step。 + * 拆成独立 step 的目的:GitHub 会在 step 结束后刷新 Summary 页, + * 同时由后续的「在运行日志中输出二维码」步骤把字符画 cat 进运行日志,用户在等待扫码期间可直接扫码。 */ async function genMode() { const api = startService() @@ -116,6 +111,11 @@ async function genMode() { const number = resolveNumber() const keys = [] try { + // 预先清空上一次残留的字符画/链接文件 + for (const f of [ASCII_FILE, URLS_FILE]) { + if (fs.existsSync(f)) fs.unlinkSync(f) + } + for (let n = 0; n < number; n++) { const result = await send(`/login/qr/key?timestrap=${Date.now()}`, "GET", {}) if (result.status === 1) { @@ -126,16 +126,19 @@ async function genMode() { } else { printRed("响应内容") console.dir(summarizeResponse(result), { depth: null }) - throw new Error("请求出错") + throw new Error(`获取二维码密钥失败:接口返回 status=${result.status}`) } } fs.writeFileSync(KEYS_FILE, JSON.stringify({ number, keys })) + printMagenta(`\n已生成 ${number} 个二维码。请查看下一步「在运行日志中输出二维码」直接扫描,`) + printMagenta(`或前往本次运行的【Summary(摘要)】页面查看清晰图片;随后工作流会自动进入“等待扫码”步骤完成登录。`) + } catch (e) { + const msg = e && e.message ? e.message : String(e) + console.error(`::error::二维码生成失败:${msg}`) + throw e } finally { close_api(api) } - - printMagenta(`\n已生成 ${number} 个二维码。本步骤结束后,请打开本次运行的“Summary(摘要)”页面,`) - printMagenta(`在页面中扫描二维码图片;随后工作流会自动进入“等待扫码”步骤完成登录。`) } /** From 9b24fbf0db553fc6ca8d21d66748f45fb7e345ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:44:55 +0800 Subject: [PATCH 11/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E6=89=AB=E6=8F=8F=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qrcodeLogin.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/qrcodeLogin.js b/qrcodeLogin.js index cf2f83d..941396f 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -55,15 +55,25 @@ async function buildQr(url, index, total) { fs.mkdirSync(QR_DIR, { recursive: true }) - // 1) 字符画落盘,供日志输出步骤展示(核心是这一步,保证日志里可直接扫) + // ═══ 核心输出:直接将二维码打印到运行日志(用户展开此步骤即可看到并扫描)═══ + printMagenta(`\n╔══════════════════════════════════════════╗`) + printMagenta(`║ 请使用「酷狗音乐 APP」扫描下方二维码登录 ${header} `) + printMagenta(`╚══════════════════════════════════════════╝\n`) + console.log(utf8) + printMagenta('───────────────────────────────────────') + printMagenta('如无法扫描,请复制此链接到酷狗 App 打开:') + console.log(url) + console.log('') + + // 落盘文件(供后续 cat 步骤 / artifact 双保险) const block = (total > 1 ? `# 账号 ${index}/${total}\n` : '') + utf8 + '\n' fs.appendFileSync(ASCII_FILE, block) fs.appendFileSync(URLS_FILE, (total > 1 ? `账号 ${index}/${total}: ` : '') + url + '\n') - // 2) 生成 PNG 文件(artifact 下载 + Summary 内嵌图片) + // 生成 PNG 文件(artifact 下载 + Summary 内嵌图片) await QRCode.toFile(`${QR_DIR}/qr-${index}.png`, url, { width: 320, margin: 2 }) - // 3) 在运行摘要(Summary)中嵌入真实可扫的二维码图片 + // 在运行摘要(Summary)中嵌入真实可扫的二维码图片 const dataUrl = await QRCode.toDataURL(url, { width: 320, margin: 2 }) appendSummary([ `## 🎵 酷狗音乐扫码登录${header}`, @@ -87,8 +97,6 @@ async function buildQr(url, index, total) { '---', '', ].join('\n')) - - printMagenta(`✅ 已生成第 ${index}/${total} 个登录二维码(UTF-8 字符画),已写入日志文件。`) } /** 解析账号数量 */ @@ -130,8 +138,8 @@ async function genMode() { } } fs.writeFileSync(KEYS_FILE, JSON.stringify({ number, keys })) - printMagenta(`\n已生成 ${number} 个二维码。请查看下一步「在运行日志中输出二维码」直接扫描,`) - printMagenta(`或前往本次运行的【Summary(摘要)】页面查看清晰图片;随后工作流会自动进入“等待扫码”步骤完成登录。`) + printMagenta(`\n已生成 ${number} 个二维码(已直接显示在上方日志中)。`) + printMagenta(`如上方二维码无法扫描,请前往【Summary 摘要】页查看清晰图片,或复制链接到酷狗 App 打开。`) } catch (e) { const msg = e && e.message ? e.message : String(e) console.error(`::error::二维码生成失败:${msg}`) From 25dd4bdfa60de6b86a04db6f4801a14e3bef3fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:50:01 +0800 Subject: [PATCH 12/18] =?UTF-8?q?fix:=20=E4=BA=8C=E7=BB=B4=E7=A0=81?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=94=B9=E7=94=A8=E7=9C=9F=E5=AE=9E=20PNG=20?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=B1=95=E7=A4=BA=EF=BC=88Summary=20?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=9B=BF=E4=BB=A3=E4=B8=8D=E5=8F=AF=E6=89=AB?= =?UTF-8?q?=E7=9A=84=E5=AD=97=E7=AC=A6=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除日志中的 UTF-8 半块字符画输出(终端字符渲染不一致,手机无法扫描) - Summary 摘要页嵌入真实 PNG 图片( data URI),100% 可扫 - 生成后立即上传 artifact(qr/*.png),用户随时可下载高清图 - 日志仅输出清晰文字指引 + 链接兜底,引导用户去 Summary 页扫码 - workflow 去掉字符画 cat 步骤,改为生成成功后立即可下载图片 - 保留错误处理(::error:: 标注 + 失败提示步骤)和等待扫码轮询逻辑 --- ...4\347\240\201\347\231\273\345\275\225.yml" | 52 ++++------- qrcodeLogin.js | 91 ++++++------------- 2 files changed, 50 insertions(+), 93 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 5c6d4c4..4c24690 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -59,8 +59,8 @@ jobs: run: npm run install if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - # ── 步骤一:生成二维码(落盘字符画文件 + PNG + Summary + 密钥)── - - name: 生成登录二维码 + # ═══ 步骤一:生成二维码图片并写入 Summary(真实 PNG 图片,可直接扫描)════ + - name: 生成登录二维码图片 id: gen run: node qrcodeLogin.js gen env: @@ -68,38 +68,26 @@ jobs: USERINFO: ${{ secrets.USERINFO }} APPEND_USER: ${{ github.event.inputs.append_user }} - # ── 步骤二:在运行日志中直接输出可扫描的二维码字符画 ── - # 注意:不使用 ::group:: 折叠,确保二维码在日志中直接可见、便于手机扫描 - - name: 在运行日志中输出二维码 - id: showqr - if: ${{ success() && steps.gen.outcome == 'success' }} - run: | - echo "════════════════════════════════════════════════════════" - echo " 请使用「酷狗音乐 APP」扫描下方二维码完成登录" - echo "════════════════════════════════════════════════════════" - echo "" - if [ -f qr/qr-ascii.txt ]; then - cat qr/qr-ascii.txt - else - echo "::error::未找到二维码字符画文件,生成步骤可能未成功执行。" - fi - echo "" - echo "────────────────────────────────────────────────────────" - echo "如无法直接扫描,可复制以下链接到酷狗音乐 App 内扫码:" - if [ -f qr/qr-urls.txt ]; then - cat qr/qr-urls.txt - fi - echo "" - echo "(二维码同时已写入本次运行的【Summary 摘要】页面,可点击查看清晰图片)" + # ═══ 步骤二:立即上传二维码图片(生成成功后立即可下载,不用等整个 run 结束)════ + - name: 上传二维码图片(立即可下载) + if: ${{ success() && steps.gen.outcome == 'success' && hashFiles('qr/*.png') != '' }} + uses: actions/upload-artifact@v4 + with: + name: 二维码图片 + path: qr/*.png + retention-days: 1 - # ── 错误处理:生成步骤失败时给出明确提示 ── + # ═══ 错误处理:生成失败时给出明确提示 ═══ - name: 二维码生成失败提示 if: ${{ failure() && steps.gen.outcome == 'failure' }} run: | - echo "::error::二维码生成失败,无法继续登录。" - echo "请检查:1) 运行器网络能否访问酷狗接口;2) 依赖是否安装成功(查看上方「安装依赖」步骤)。" + echo "::error::❌ 二维码生成失败,无法继续登录。" + echo "" + echo "请检查以下可能的原因:" + echo " 1️⃣ 运行器网络是否能正常访问酷狗接口" + echo " 2️⃣ npm 依赖是否安装成功(查看上方「安装依赖」步骤的输出)" - # ── 步骤三:轮询等待用户扫码确认 ── + # ═══ 步骤三:轮询等待用户扫码确认(用户在 Summary 页扫完图后自动检测)════ - name: 等待扫码登录 id: wait if: ${{ success() && steps.gen.outcome == 'success' }} @@ -111,11 +99,11 @@ jobs: PAT: ${{ secrets.PAT }} GITHUB_REPOSITORY: ${{ github.repository }} - # ── 上传二维码图片作为备用(仅在存在时)── - - name: 上传二维码图片 + # ═══ 最终兜底上传(确保即使前面步骤失败也能拿到已生成的文件)════ + - name: 上传所有生成的文件(兜底) if: ${{ always() && hashFiles('qr/*') != '' }} uses: actions/upload-artifact@v4 with: - name: 二维码 + name: 二维码全部文件 path: qr/ retention-days: 1 diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 941396f..d50077f 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -12,19 +12,6 @@ const QRCode = require('./api/node_modules/qrcode') const SUMMARY_FILE = process.env.GITHUB_STEP_SUMMARY const QR_DIR = './qr' const KEYS_FILE = './qrkeys.json' -const ASCII_FILE = `${QR_DIR}/qr-ascii.txt` // 供「日志输出步骤」直接 cat 到运行日志 -const URLS_FILE = `${QR_DIR}/qr-urls.txt` // 扫码失败的链接兜底 - -/** - * 渲染 QR 为 UTF-8 半块字符画(每个字符纵向承载 2 个模块), - * 正好补偿终端字符“高>宽”的比例,在 GitHub Actions 运行日志里可直接用手机扫描。 - * 相比双宽 '██' 方案(横纵比 2:1 被拉伸、常扫不上),半块更接近正方形、可扫性更好。 - * @param {string} url - * @returns {Promise} - */ -function renderQrUtf8(url) { - return QRCode.toString(url, { type: 'utf8', margin: 2 }) -} /** * 向 GitHub Step Summary 追加内容(本地或非 Actions 环境自动跳过) @@ -40,75 +27,62 @@ function appendSummary(markdown) { } /** - * 生成并落盘单个二维码: - * - qr/qr-ascii.txt : UTF-8 半块字符画(日志输出步骤 cat 它即可在运行日志扫码) - * - qr/qr-urls.txt : 扫码失败的链接兜底 - * - qr/qr-N.png : PNG 文件,供 artifact 下载 - * - Summary : 以真实 PNG 图片(data URI)嵌入运行摘要,扫码页可直接查看 - * @param {string} url + * 生成并展示单个二维码(图片优先方案) + * + * 展示层级(按可扫性从高到低): + * 1. Summary 摘要页:真实 PNG 图片( data URI),手机直接扫 + * 2. Artifact 下载:qr/qr-N.png 高清文件 + * 3. 日志链接兜底:酷狗扫码 URL,复制到 App 内打开 + * + * @param {string} url 酷狗扫码登录完整 URL * @param {number} index 从 1 开始 - * @param {number} total + * @param {number} total 总账号数 */ async function buildQr(url, index, total) { - const utf8 = await renderQrUtf8(url) const header = total > 1 ? `(第 ${index}/${total} 个账号)` : '' fs.mkdirSync(QR_DIR, { recursive: true }) - // ═══ 核心输出:直接将二维码打印到运行日志(用户展开此步骤即可看到并扫描)═══ - printMagenta(`\n╔══════════════════════════════════════════╗`) - printMagenta(`║ 请使用「酷狗音乐 APP」扫描下方二维码登录 ${header} `) - printMagenta(`╚══════════════════════════════════════════╝\n`) - console.log(utf8) - printMagenta('───────────────────────────────────────') - printMagenta('如无法扫描,请复制此链接到酷狗 App 打开:') - console.log(url) - console.log('') - - // 落盘文件(供后续 cat 步骤 / artifact 双保险) - const block = (total > 1 ? `# 账号 ${index}/${total}\n` : '') + utf8 + '\n' - fs.appendFileSync(ASCII_FILE, block) - fs.appendFileSync(URLS_FILE, (total > 1 ? `账号 ${index}/${total}: ` : '') + url + '\n') - - // 生成 PNG 文件(artifact 下载 + Summary 内嵌图片) + // ── 1) 生成 PNG 文件(artifact 下载用)── await QRCode.toFile(`${QR_DIR}/qr-${index}.png`, url, { width: 320, margin: 2 }) - // 在运行摘要(Summary)中嵌入真实可扫的二维码图片 + // ── 2) 在运行摘要(Summary)中嵌入真实可扫的二维码图片 ← 核心展示渠道 ── const dataUrl = await QRCode.toDataURL(url, { width: 320, margin: 2 }) appendSummary([ `## 🎵 酷狗音乐扫码登录${header}`, '', - '请使用 **酷狗音乐 APP** 扫描下方二维码登录(图片可在运行摘要页直接查看):', + '### 请使用 **酷狗音乐 APP** 扫描下方二维码 👇', '', - `酷狗扫码登录二维码${header}`, + `

酷狗扫码登录二维码${header}

`, '', - '如图片无法加载,可复制以下链接到浏览器/酷狗 App 打开:', + '> ⏳ 二维码有效期约 **2 分钟**,请尽快扫描。扫描后工作流会自动检测登录状态。', '', - url, + '**如上方图片未加载或无法扫描**,可复制以下链接到酷狗音乐 App 内打开:', '', - '
字符版二维码(备用)', - '', - '```', - utf8, - '```', - '', - '
', + `${url}`, '', '---', '', ].join('\n')) + + // ── 3) 日志输出:清晰指引用户去 Summary 页看图 ── + printMagenta(`\n═══ 第 ${index}/${total} 个登录二维码已生成 ═══`) + printMagenta(`👉 请点击本页面上方的「**Summary**」标签查看可扫描的二维码图片`) + printMagenta(` 或在页面左侧导航栏找到「Run details → Summary」`) + printMagenta(`\n📋 扫码备用链接(复制到酷狗 App 打开):`) + console.log(` ${url}`) + console.log('') } /** 解析账号数量 */ function resolveNumber() { - const args = process.argv.slice(3) // 跳过 node、脚本名、模式参数 + const args = process.argv.slice(3) return parseInt(process.env.NUMBER || args[0] || "1") } /** - * 模式一:生成二维码并落盘(字符画文件 + PNG + Summary),随后立即结束 step。 - * 拆成独立 step 的目的:GitHub 会在 step 结束后刷新 Summary 页, - * 同时由后续的「在运行日志中输出二维码」步骤把字符画 cat 进运行日志,用户在等待扫码期间可直接扫码。 + * 模式一:生成二维码(PNG 图片写入 Summary),随后立即结束 step。 + * step 结束后 Summary 页会刷新显示真实二维码图片,用户可直接扫描。 */ async function genMode() { const api = startService() @@ -119,11 +93,6 @@ async function genMode() { const number = resolveNumber() const keys = [] try { - // 预先清空上一次残留的字符画/链接文件 - for (const f of [ASCII_FILE, URLS_FILE]) { - if (fs.existsSync(f)) fs.unlinkSync(f) - } - for (let n = 0; n < number; n++) { const result = await send(`/login/qr/key?timestrap=${Date.now()}`, "GET", {}) if (result.status === 1) { @@ -138,8 +107,8 @@ async function genMode() { } } fs.writeFileSync(KEYS_FILE, JSON.stringify({ number, keys })) - printMagenta(`\n已生成 ${number} 个二维码(已直接显示在上方日志中)。`) - printMagenta(`如上方二维码无法扫描,请前往【Summary 摘要】页查看清晰图片,或复制链接到酷狗 App 打开。`) + printMagenta(`\n✅ 已生成 ${number} 个二维码图片。`) + printMagenta(`📱 现在请前往【Summary】页面扫描二维码,工作流会在此步骤结束后自动进入等待阶段。`) } catch (e) { const msg = e && e.message ? e.message : String(e) console.error(`::error::二维码生成失败:${msg}`) @@ -159,7 +128,7 @@ async function waitMode() { try { parsed = JSON.parse(fs.readFileSync(KEYS_FILE, 'utf8')) } catch { - throw new Error("未找到二维码密钥文件,请确认已先运行“生成并展示二维码”步骤") + throw new Error('未找到二维码密钥文件,请确认已先运行「生成登录二维码」步骤') } const { number, keys } = parsed const USERINFO = process.env.USERINFO From 0f0512aad90218b53dae4278d3daaa999ca1bd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:56:26 +0800 Subject: [PATCH 13/18] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E8=87=AA?= =?UTF-8?q?=E5=8C=85=E5=90=AB=20HTML=20=E7=99=BB=E5=BD=95=E9=A1=B5?= =?UTF-8?q?=EF=BC=88=E5=A4=A7=E5=9B=BE=E4=BA=8C=E7=BB=B4=E7=A0=81=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E8=A7=A3=E5=86=B3=E6=97=A5=E5=BF=97=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=9B=BE=E7=89=87=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心变更: - 新增 generateHtmlPage() 生成暗色主题 HTML 页面(内嵌 base64 大图二维码) - gen 模式同时产出:login.html(聚合所有账号)+ qr-N.html(单独页面)+ qr-N.png - workflow 在 gen 成功后立即上传「二维码HTML」artifact,用户立即可下载 - Summary 写入加固(加验证日志 + 错误 warn) - 日志输出改为三档指引:①下载HTML扫码 ②Summary看图 ③链接兜底 --- ...4\347\240\201\347\231\273\345\275\225.yml" | 51 ++++-- qrcodeLogin.js | 170 ++++++++++++++---- 2 files changed, 171 insertions(+), 50 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 4c24690..04ddc85 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -31,12 +31,14 @@ jobs: steps: - name: 检出仓库 uses: actions/checkout@v6 + - name: 创建 Node 环境 uses: actions/setup-node@v6 with: node-version: 24 cache: 'npm' cache-dependency-path: ./api/package-lock.json + - name: 检查 GitHub CLI run: | if ! command -v gh >/dev/null 2>&1; then @@ -47,6 +49,7 @@ jobs: sudo apt install gh -y fi gh --version + - name: 缓存依赖 id: cache-npm uses: actions/cache@v5 @@ -55,12 +58,15 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- + - name: 安装依赖 run: npm run install if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} - # ═══ 步骤一:生成二维码图片并写入 Summary(真实 PNG 图片,可直接扫描)════ - - name: 生成登录二维码图片 + # ════════════════════════════════════════════ + # 步骤一:生成二维码 → PNG + Summary + HTML 登录页 + # ════════════════════════════════════════════ + - name: 生成二维码图片与HTML登录页 id: gen run: node qrcodeLogin.js gen env: @@ -68,28 +74,43 @@ jobs: USERINFO: ${{ secrets.USERINFO }} APPEND_USER: ${{ github.event.inputs.append_user }} - # ═══ 步骤二:立即上传二维码图片(生成成功后立即可下载,不用等整个 run 结束)════ - - name: 上传二维码图片(立即可下载) + # ════════════════════════════════════════════ + # 步骤二:立即上传 HTML 登录页 ← 用户下载后浏览器打开直接扫码! + # ════════════════════════════════════════════ + - name: 上传二维码 HTML 页面(👈 下载这个!浏览器打开即可扫码) + if: ${{ success() && steps.gen.outcome == 'success' && hashFiles('qr/login.html') != '' }} + uses: actions/upload-artifact@v4 + with: + name: 二维码HTML + path: | + qr/login.html + qr/qr-*.html + retention-days: 1 + if-no-files-found: warn + + # ═══ 备用:单独的 PNG 图片 artifact ═══ + - name: 上传二维码 PNG 图片 if: ${{ success() && steps.gen.outcome == 'success' && hashFiles('qr/*.png') != '' }} uses: actions/upload-artifact@v4 with: - name: 二维码图片 + name: 二维码PNG path: qr/*.png retention-days: 1 - # ═══ 错误处理:生成失败时给出明确提示 ═══ + # ═══ 错误处理 ═══ - name: 二维码生成失败提示 if: ${{ failure() && steps.gen.outcome == 'failure' }} run: | - echo "::error::❌ 二维码生成失败,无法继续登录。" + echo "::error::❌ 二维码生成失败" echo "" - echo "请检查以下可能的原因:" - echo " 1️⃣ 运行器网络是否能正常访问酷狗接口" - echo " 2️⃣ npm 依赖是否安装成功(查看上方「安装依赖」步骤的输出)" + echo "排查方向:" + echo " 1️⃣ 运行器网络是否能访问酷狗接口(查看上方步骤日志)" + echo " 2️⃣ npm 依赖安装是否成功" - # ═══ 步骤三:轮询等待用户扫码确认(用户在 Summary 页扫完图后自动检测)════ + # ════════════════════════════════════════════ + # 步骤三:轮询等待用户扫码确认 + # ════════════════════════════════════════════ - name: 等待扫码登录 - id: wait if: ${{ success() && steps.gen.outcome == 'success' }} run: node qrcodeLogin.js wait env: @@ -99,11 +120,11 @@ jobs: PAT: ${{ secrets.PAT }} GITHUB_REPOSITORY: ${{ github.repository }} - # ═══ 最终兜底上传(确保即使前面步骤失败也能拿到已生成的文件)════ - - name: 上传所有生成的文件(兜底) + # ═══ 兜底:确保所有生成的文件都被保留 ═══ + - name: 上传全部文件(兜底) if: ${{ always() && hashFiles('qr/*') != '' }} uses: actions/upload-artifact@v4 with: - name: 二维码全部文件 + name: 二维码全部文件_兜底 path: qr/ retention-days: 1 diff --git a/qrcodeLogin.js b/qrcodeLogin.js index d50077f..0e293c8 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -8,56 +8,66 @@ import { upsertUser, saveUserinfo } from "./utils/userinfo.js"; const require = createRequire(import.meta.url) const QRCode = require('./api/node_modules/qrcode') -// GitHub Actions 运行环境下,step summary 文件路径由该变量提供(Actions 自动注入) -const SUMMARY_FILE = process.env.GITHUB_STEP_SUMMARY +// GitHub Actions 运行环境下自动注入的 Step Summary 文件路径 +const SUMMARY_FILE = process.env.GITHUB_STEP_SUMMARY || '' const QR_DIR = './qr' const KEYS_FILE = './qrkeys.json' /** - * 向 GitHub Step Summary 追加内容(本地或非 Actions 环境自动跳过) + * 向 GitHub Step Summary 追加 Markdown 内容。 + * 非 Actions 环境(本地运行)时 SUMMARY_FILE 为空,自动跳过。 * @param {string} markdown */ function appendSummary(markdown) { if (!SUMMARY_FILE) return try { - fs.appendFileSync(SUMMARY_FILE, markdown) - } catch { - // 写入摘要失败不影响主流程 + fs.appendFileSync(SUMMARY_FILE, markdown + '\n') + // 验证写入成功 + const size = fs.statSync(SUMMARY_FILE).size + if (size > 0) { + console.log(`[Summary] 已追加 ${Buffer.byteLength(markdown)} 字节,总计 ${size} 字节`) + } + } catch (err) { + console.warn(`[Summary] 写入失败:${err.message}`) } } /** - * 生成并展示单个二维码(图片优先方案) + * 生成并展示单个二维码 — 三重展示渠道确保用户一定能扫到: * - * 展示层级(按可扫性从高到低): - * 1. Summary 摘要页:真实 PNG 图片( data URI),手机直接扫 - * 2. Artifact 下载:qr/qr-N.png 高清文件 - * 3. 日志链接兜底:酷狗扫码 URL,复制到 App 内打开 + * ① 自包含 HTML 页面(qr/login.html):浏览器打开即见大图,手机直接扫 + * ← 最可靠,不依赖任何平台限制,artifact 立即可下载 + * ② Step Summary 摘要页: data URI 图片,在 Actions Summary 标签页查看 + * ③ PNG 文件(qr/qr-N.png):高清原图,artifact 下载备用 * - * @param {string} url 酷狗扫码登录完整 URL - * @param {number} index 从 1 开始 + * @param {string} url 酷狗扫码登录完整 URL + * @param {number} index 账号序号(从 1 开始) * @param {number} total 总账号数 + * @returns {{ dataUrl: string, url: string }} 供 HTML 聚合用 */ async function buildQr(url, index, total) { const header = total > 1 ? `(第 ${index}/${total} 个账号)` : '' fs.mkdirSync(QR_DIR, { recursive: true }) - // ── 1) 生成 PNG 文件(artifact 下载用)── + // ── 1) PNG 文件(artifact 下载 + HTML 内嵌双用途)── await QRCode.toFile(`${QR_DIR}/qr-${index}.png`, url, { width: 320, margin: 2 }) - // ── 2) 在运行摘要(Summary)中嵌入真实可扫的二维码图片 ← 核心展示渠道 ── + // ── 2) base64 data URI(Summary + HTML 共用)── const dataUrl = await QRCode.toDataURL(url, { width: 320, margin: 2 }) + const pngBase64 = dataUrl.replace(/^data:image\/png;base64,/, '') + + // ── 3) 写入 Step Summary(Actions Summary 标签页查看)── appendSummary([ `## 🎵 酷狗音乐扫码登录${header}`, '', - '### 请使用 **酷狗音乐 APP** 扫描下方二维码 👇', + '### 👇 用酷狗音乐 APP 扫描下方二维码', '', - `

酷狗扫码登录二维码${header}

`, + `扫码登录${header}`, '', - '> ⏳ 二维码有效期约 **2 分钟**,请尽快扫描。扫描后工作流会自动检测登录状态。', + '> ⏳ 二维码有效期约 **2 分钟**,请尽快扫描。', '', - '**如上方图片未加载或无法扫描**,可复制以下链接到酷狗音乐 App 内打开:', + '**备用链接**(复制到酷狗 App 打开):', '', `${url}`, '', @@ -65,13 +75,80 @@ async function buildQr(url, index, total) { '', ].join('\n')) - // ── 3) 日志输出:清晰指引用户去 Summary 页看图 ── - printMagenta(`\n═══ 第 ${index}/${total} 个登录二维码已生成 ═══`) - printMagenta(`👉 请点击本页面上方的「**Summary**」标签查看可扫描的二维码图片`) - printMagenta(` 或在页面左侧导航栏找到「Run details → Summary」`) - printMagenta(`\n📋 扫码备用链接(复制到酷狗 App 打开):`) - console.log(` ${url}`) + // ── 4) 日志输出:明确指引去哪里看图 ── + printMagenta(`\n═══ 第 ${index}/${total} 个二维码已生成 ═══`) + console.log('') + console.log(' 📱 扫码方式(按推荐顺序):') + console.log(' ① 在页面底部的 Artifacts 区域下载「二维码HTML」→ 浏览器打开 → 直接扫大图') + console.log(' ② 点击本页面上方「Summary」标签查看图片') + console.log(' ③ 复制下方链接到酷狗 App 内打开:') console.log('') + console.log(` ${url}`) + console.log('') + + return { dataUrl, url, header, index } +} + +/** + * 生成自包含 HTML 登录页(所有二维码的大图集中展示) + * 用户从 artifact 下载后双击/手机打开即可直接扫码,无需任何依赖。 + */ +function generateHtmlPage(qrItems) { + const cards = qrItems.map(item => ` +
+

账号 ${item.index}/${qrItems.length} ${item.header}

+
+ 账号${item.index} 二维码 +
+

${item.url}

+

⏳ 有效期约 2 分钟

+
+ `).join('\n') + + return ` + + + + +酷狗音乐扫码登录 + + + +
+

🎵 酷狗音乐扫码登录

+

使用「酷狗音乐 APP」扫描下方二维码完成登录

+
+
${cards}
+ + +` } /** 解析账号数量 */ @@ -81,8 +158,8 @@ function resolveNumber() { } /** - * 模式一:生成二维码(PNG 图片写入 Summary),随后立即结束 step。 - * step 结束后 Summary 页会刷新显示真实二维码图片,用户可直接扫描。 + * 模式一:生成二维码(PNG + Summary + HTML),随后立即结束 step。 + * step 结束后 artifact 即可下载 HTML 页面(含大图),用户浏览器打开直接扫码。 */ async function genMode() { const api = startService() @@ -92,23 +169,50 @@ async function genMode() { const userinfo = (USERINFO && APPEND_USER == "是") ? JSON.parse(USERINFO) : [] const number = resolveNumber() const keys = [] + + if (!SUMMARY_FILE) { + console.log('[INFO] 非 Actions 环境($GITHUB_STEP_SUMMARY 未设置),Summary 将跳过') + } + try { + const qrItems = [] // 收集所有二维码信息用于生成聚合 HTML + for (let n = 0; n < number; n++) { const result = await send(`/login/qr/key?timestrap=${Date.now()}`, "GET", {}) if (result.status === 1) { const qrcode = result.data.qrcode const qrUrl = `https://h5.kugou.com/apps/loginQRCode/html/index.html?qrcode=${qrcode}` keys.push(qrcode) - await buildQr(qrUrl, n + 1, number) + const item = await buildQr(qrUrl, n + 1, number) + qrItems.push(item) } else { printRed("响应内容") console.dir(summarizeResponse(result), { depth: null }) throw new Error(`获取二维码密钥失败:接口返回 status=${result.status}`) } } + + // ── 生成自包含 HTML 登录页(核心展示渠道!)── + if (qrItems.length > 0) { + const htmlContent = generateHtmlPage(qrItems) + fs.writeFileSync(`${QR_DIR}/login.html`, htmlContent, 'utf8') + + // 也把每个二维码单独做成一个 HTML 方便多账号时逐个处理 + for (const item of qrItems) { + fs.writeFileSync( + `${QR_DIR}/qr-${item.index}.html`, + `扫码登录 ${item.header}` + + `` + + `扫码登录${item.header}`, + 'utf8' + ) + } + } + fs.writeFileSync(KEYS_FILE, JSON.stringify({ number, keys })) - printMagenta(`\n✅ 已生成 ${number} 个二维码图片。`) - printMagenta(`📱 现在请前往【Summary】页面扫描二维码,工作流会在此步骤结束后自动进入等待阶段。`) + printMagenta(`\n✅ 已生成 ${number} 个二维码。`) + printMagenta(`📲 请在页面底部的「Artifacts」区域下载「二维码HTML」,浏览器打开即可看到大图扫码!`) } catch (e) { const msg = e && e.message ? e.message : String(e) console.error(`::error::二维码生成失败:${msg}`) @@ -128,7 +232,7 @@ async function waitMode() { try { parsed = JSON.parse(fs.readFileSync(KEYS_FILE, 'utf8')) } catch { - throw new Error('未找到二维码密钥文件,请确认已先运行「生成登录二维码」步骤') + throw new Error('未找到二维码密钥文件,请确认已先运行「生成登录二维码图片」步骤') } const { number, keys } = parsed const USERINFO = process.env.USERINFO @@ -148,21 +252,17 @@ async function waitMode() { case 0: printYellow("二维码已过期,请重新运行工作流生成新二维码") break - case 1: // 未扫描二维码 break - case 2: // 二维码未确认,请点击确认登录 break - case 4: printGreen("登录成功!") upsertUser(userinfo, { userid: res.data.userid, token: res.data.token }, APPEND_USER == "是") loggedIn = true break - default: printRed("请求出错") console.dir(summarizeResponse(res), { depth: null }) From 89f3b419e55fb4b796426909a640fc9dcec41422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:03:12 +0800 Subject: [PATCH 14/18] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20gen=20?= =?UTF-8?q?=E6=AD=A5=E9=AA=A4=E4=B8=8D=E9=80=80=E5=87=BA=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E6=AD=A5=E9=AA=A4=E4=B8=8D=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E3=80=81=E4=BA=8C=E7=BB=B4=E7=A0=81=E8=B6=85=E6=97=B6=E5=A4=B1?= =?UTF-8?q?=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:genMode() 是 async 函数但调用处未 await/.catch(),且末尾无 process.exit(0),导致 Node 进程在 close_api() 后因悬挂资源无法退出。 表现:gen 步骤长期显示 ●(运行中)→ 后续 wait/artifact 步骤全为 空心圆 ○ 不执行 → 二维码倒计时过期后 wait 才开始 → 扫码永远失败。 修复: - genMode()/waitMode() 末尾加 .then(()=>process.exit(0)) 确保干净退出 - close_api() 加 Promise.race(3s 超时) 防止 API 服务关闭时挂死 - workflow 的 wait/upload 步骤条件改为 always() + outcome 判定, 去掉 hashFiles() 守卫(避免因路径问题误跳过 artifact 上传) --- ...4\347\240\201\347\231\273\345\275\225.yml" | 26 +++++++++++-------- qrcodeLogin.js | 15 ++++++++--- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 04ddc85..9f6299c 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -65,7 +65,8 @@ jobs: # ════════════════════════════════════════════ # 步骤一:生成二维码 → PNG + Summary + HTML 登录页 - # ════════════════════════════════════════════ + # ✅ 完成后立即结束 step(process.exit(0) 保证干净退出) + # ════════════════════════════════════════════ - name: 生成二维码图片与HTML登录页 id: gen run: node qrcodeLogin.js gen @@ -75,10 +76,10 @@ jobs: APPEND_USER: ${{ github.event.inputs.append_user }} # ════════════════════════════════════════════ - # 步骤二:立即上传 HTML 登录页 ← 用户下载后浏览器打开直接扫码! - # ════════════════════════════════════════════ + # 步骤二:立即上传 HTML(gen 完成后立即可下载) + # ════════════════════════════════════════════ - name: 上传二维码 HTML 页面(👈 下载这个!浏览器打开即可扫码) - if: ${{ success() && steps.gen.outcome == 'success' && hashFiles('qr/login.html') != '' }} + if: ${{ always() && steps.gen.outcome == 'success' }} uses: actions/upload-artifact@v4 with: name: 二维码HTML @@ -88,18 +89,19 @@ jobs: retention-days: 1 if-no-files-found: warn - # ═══ 备用:单独的 PNG 图片 artifact ═══ + # ═══ 备用:PNG 图片 ═══ - name: 上传二维码 PNG 图片 - if: ${{ success() && steps.gen.outcome == 'success' && hashFiles('qr/*.png') != '' }} + if: ${{ always() && steps.gen.outcome == 'success' }} uses: actions/upload-artifact@v4 with: name: 二维码PNG path: qr/*.png retention-days: 1 + if-no-files-found: warn # ═══ 错误处理 ═══ - name: 二维码生成失败提示 - if: ${{ failure() && steps.gen.outcome == 'failure' }} + if: ${{ failure() }} run: | echo "::error::❌ 二维码生成失败" echo "" @@ -109,9 +111,10 @@ jobs: # ════════════════════════════════════════════ # 步骤三:轮询等待用户扫码确认 - # ════════════════════════════════════════════ + # 用 always() 确保无论前面怎样都会尝试执行(内部自行判断) + # ════════════════════════════════════════════ - name: 等待扫码登录 - if: ${{ success() && steps.gen.outcome == 'success' }} + if: ${{ always() }} run: node qrcodeLogin.js wait env: USERINFO: ${{ secrets.USERINFO }} @@ -120,11 +123,12 @@ jobs: PAT: ${{ secrets.PAT }} GITHUB_REPOSITORY: ${{ github.repository }} - # ═══ 兜底:确保所有生成的文件都被保留 ═══ + # ═══ 兜底 ═══ - name: 上传全部文件(兜底) - if: ${{ always() && hashFiles('qr/*') != '' }} + if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: 二维码全部文件_兜底 path: qr/ retention-days: 1 + if-no-files-found: ignore diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 0e293c8..30f2bc9 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -218,7 +218,11 @@ async function genMode() { console.error(`::error::二维码生成失败:${msg}`) throw e } finally { - close_api(api) + // 强制关闭 API 服务(加超时防止挂死) + await Promise.race([ + close_api(api), + new Promise(r => setTimeout(r, 3000)) + ]) } } @@ -278,13 +282,16 @@ async function waitMode() { } saveUserinfo(userinfo) } finally { - close_api(api) + await Promise.race([ + close_api(api), + new Promise(r => setTimeout(r, 3000)) + ]) } } const mode = process.argv[2] || 'gen' if (mode === 'wait') { - waitMode() + waitMode().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) } else { - genMode() + genMode().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) } From 72e32bed37401823e345247e2e66bcc8ae69e983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:10:41 +0800 Subject: [PATCH 15/18] =?UTF-8?q?fix:=20=E6=94=B9=E7=94=A8=20GitHub=20Rele?= =?UTF-8?q?ase=20=E7=9B=B4=E9=93=BE=E5=B1=95=E7=A4=BA=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=EF=BC=88=E5=85=8D=E5=8E=8B=E7=BC=A9=E5=8C=85=EF=BC=89?= =?UTF-8?q?+=20=E4=BF=AE=E5=A4=8D=E7=AB=AF=E5=8F=A3=E5=8D=A0=E7=94=A8?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20wait=20=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心变更: 1. 二维码展示改为 GitHub Release 直链(非 artifact 压缩包) - artifact 机制强制所有文件打包成 .zip,用户必须解压才能看,体验差 - 新增「发布二维码图片直链」步骤:gh release create 上传 qr/*.png + login.html - 日志输出可点击直链:浏览器打开即显示图片,手机直接扫,零解压 - 同时保留 HTML 网页链接(htmlpreview)作为备选 - 仓库权限改为 contents: write;新增清理步骤(仅保留最近 5 个二维码 Release) - HTML artifact 改为仅在 Release 发布失败时的兜底 2. 修复 EADDRINUSE 导致 wait 步骤崩溃(本次运行 wait 直接报错退出) - startService 改用 detached: true 创建独立进程组 - close_api 改用 process.kill(-pid, SIGKILL) 强杀整个进程组 (npm 不会把 SIGTERM 转发给其子进程 Express,仅 api.kill() 无法释放 3000 端口) - 移除 stderr 中的 throw(避免 npm 警告误触发崩溃) - 附带 3s 超时保护防止挂死 3. 更新 qrcodeLogin.js 日志指引文案,指向直链步骤 --- ...4\347\240\201\347\231\273\345\275\225.yml" | 64 ++++++++++++------- qrcodeLogin.js | 6 +- utils/utils.js | 26 ++++++-- 3 files changed, 64 insertions(+), 32 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 9f6299c..661a523 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -22,8 +22,9 @@ on: options: - 否 - 是 +# 需要 contents: write 才能创建 Release 上传二维码直链 permissions: - contents: read + contents: write jobs: run: runs-on: ubuntu-latest @@ -76,10 +77,36 @@ jobs: APPEND_USER: ${{ github.event.inputs.append_user }} # ════════════════════════════════════════════ - # 步骤二:立即上传 HTML(gen 完成后立即可下载) + # 步骤二(核心):发布二维码为 GitHub Release 直链 + # 浏览器点击链接即显示图片/网页,无需下载解压 # ════════════════════════════════════════════ - - name: 上传二维码 HTML 页面(👈 下载这个!浏览器打开即可扫码) + - name: 发布二维码图片直链(👈 点击即可查看,无需下载解压) + id: publish if: ${{ always() && steps.gen.outcome == 'success' }} + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="qr-${{ github.run_id }}" + gh release create "$TAG" qr/*.png qr/login.html \ + --repo "$GITHUB_REPOSITORY" \ + --title "酷狗扫码登录二维码" \ + --notes "用「酷狗音乐 APP」扫描下方二维码完成登录(约 2 分钟内有效)" + + echo "" + echo "══════════════════════════════════════════════════════════" + echo " 📱 点击下方链接,浏览器直接显示二维码图片(无需解压):" + echo "══════════════════════════════════════════════════════════" + for f in qr/*.png; do + echo " 🔗 https://github.com/${{ github.repository }}/releases/download/$TAG/$(basename "$f")" + done + echo "" + echo " 🌐 或打开完整网页(含所有账号大图):" + echo " 🔗 https://htmlpreview.github.io/?https://github.com/${{ github.repository }}/releases/download/$TAG/login.html" + echo "══════════════════════════════════════════════════════════" + + # ═══ 备用:HTML 仍上传为 artifact(万一 Release 失败兜底)═══ + - name: 上传二维码 HTML 页面(兜底备份) + if: ${{ always() && steps.gen.outcome == 'success' && steps.publish.outcome != 'success' }} uses: actions/upload-artifact@v4 with: name: 二维码HTML @@ -89,16 +116,6 @@ jobs: retention-days: 1 if-no-files-found: warn - # ═══ 备用:PNG 图片 ═══ - - name: 上传二维码 PNG 图片 - if: ${{ always() && steps.gen.outcome == 'success' }} - uses: actions/upload-artifact@v4 - with: - name: 二维码PNG - path: qr/*.png - retention-days: 1 - if-no-files-found: warn - # ═══ 错误处理 ═══ - name: 二维码生成失败提示 if: ${{ failure() }} @@ -111,7 +128,6 @@ jobs: # ════════════════════════════════════════════ # 步骤三:轮询等待用户扫码确认 - # 用 always() 确保无论前面怎样都会尝试执行(内部自行判断) # ════════════════════════════════════════════ - name: 等待扫码登录 if: ${{ always() }} @@ -123,12 +139,16 @@ jobs: PAT: ${{ secrets.PAT }} GITHUB_REPOSITORY: ${{ github.repository }} - # ═══ 兜底 ═══ - - name: 上传全部文件(兜底) + # ═══ 清理:仅保留最近 5 个二维码 Release ═══ + - name: 清理旧的二维码 Release if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: 二维码全部文件_兜底 - path: qr/ - retention-days: 1 - if-no-files-found: ignore + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \ + | awk '$1 ~ /^qr-/ {print $1}' | head -n -5 \ + | while read t; do + gh release delete "$t" --repo "$GITHUB_REPOSITORY" -y + echo "已删除旧 Release: $t" + done diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 30f2bc9..35b3b6d 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -79,7 +79,7 @@ async function buildQr(url, index, total) { printMagenta(`\n═══ 第 ${index}/${total} 个二维码已生成 ═══`) console.log('') console.log(' 📱 扫码方式(按推荐顺序):') - console.log(' ① 在页面底部的 Artifacts 区域下载「二维码HTML」→ 浏览器打开 → 直接扫大图') + console.log(' ① 查看后续「发布二维码图片直链」步骤的链接 → 浏览器打开即显示图片 → 直接扫') console.log(' ② 点击本页面上方「Summary」标签查看图片') console.log(' ③ 复制下方链接到酷狗 App 内打开:') console.log('') @@ -211,8 +211,8 @@ async function genMode() { } fs.writeFileSync(KEYS_FILE, JSON.stringify({ number, keys })) - printMagenta(`\n✅ 已生成 ${number} 个二维码。`) - printMagenta(`📲 请在页面底部的「Artifacts」区域下载「二维码HTML」,浏览器打开即可看到大图扫码!`) + printMagenta(`\n✅ 已生成 ${number} 个二维码。`) + printMagenta(`🔗 请查看下一步「发布二维码图片直链」输出的可点击链接,浏览器打开即可直接扫码(无需下载解压)!`) } catch (e) { const msg = e && e.message ? e.message : String(e) console.error(`::error::二维码生成失败:${msg}`) diff --git a/utils/utils.js b/utils/utils.js index f6b4584..c96cec5 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -5,23 +5,35 @@ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } -/** 启动 api 服务 */ +/** 启动 api 服务(detached 使其成为独立进程组,便于整组强杀) */ function startService() { - const api = spawn('npm', ['run', 'apiService']) + const api = spawn('npm', ['run', 'apiService'], { + detached: true, + stdio: ['ignore', 'pipe', 'pipe'], + }) api.stdout.on('data', () => {}) - api.on('close', () => console.log('子进程退出')) api.stderr.on('data', data => { - console.log('服务启动失败') - throw new Error(`${data}`) + const msg = String(data).trim() + if (msg) console.log('[api stderr]', msg) }) + api.on('close', code => console.log(`[api] 子进程退出,code=${code}`)) return api } -/** 关闭 api 服务 */ +/** + * 关闭 api 服务。 + * 关键修复:npm 不会把 SIGTERM 转发给它的子进程(真正的 Express 服务), + * 仅 api.kill() 会导致 3000 端口一直被占 → 下一阶段 startService 报 EADDRINUSE。 + * 因此用 detached 进程组 + process.kill(-pid) 强杀整组。 + */ function close_api(api) { - api.kill() + try { + process.kill(-api.pid, 'SIGKILL') // 杀掉整个进程组(npm + Express) + } catch (e) { + try { api.kill('SIGKILL') } catch (_) { /* 已退出 */ } + } } /** 发送请求到本地 api 服务 */ From d36f31c3d5901363aa2901e89be8da20f97ffe94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:24:55 +0800 Subject: [PATCH 16/18] =?UTF-8?q?chore:=20=E6=B8=85=E7=90=86=20Summary/?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=86=97=E4=BD=99=E4=BF=A1=E6=81=AF=20+=20?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=90=8E=E8=87=AA=E5=8A=A8=E5=88=A0=E9=99=A4?= =?UTF-8?q?=20Release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 删除 buildQr() 中的 appendSummary 调用(Summary 不再输出二维码图片/ 标题/链接/有效期等描述,解决 Summary 页面内容过多的问题) 2. 简化日志指引文字,只保留一行直链提示 3. 删除 publish 步骤中的「打开完整网页」htmlpreview 链接 4. 新增「清理本次二维码 Release」步骤:wait 完成后立即删除 qr-{run_id} Release,不再堆积 5. 旧 Release 清理策略从保留最近 5 个改为保留最近 3 个 --- ...4\347\240\201\347\231\273\345\275\225.yml" | 22 ++++++++----- qrcodeLogin.js | 31 +++---------------- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 661a523..d1c4c21 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -99,9 +99,6 @@ jobs: for f in qr/*.png; do echo " 🔗 https://github.com/${{ github.repository }}/releases/download/$TAG/$(basename "$f")" done - echo "" - echo " 🌐 或打开完整网页(含所有账号大图):" - echo " 🔗 https://htmlpreview.github.io/?https://github.com/${{ github.repository }}/releases/download/$TAG/login.html" echo "══════════════════════════════════════════════════════════" # ═══ 备用:HTML 仍上传为 artifact(万一 Release 失败兜底)═══ @@ -139,7 +136,18 @@ jobs: PAT: ${{ secrets.PAT }} GITHUB_REPOSITORY: ${{ github.repository }} - # ═══ 清理:仅保留最近 5 个二维码 Release ═══ + # ═══ 清理:登录完成后立即删除本次 Release(不堆积)═══ + - name: 清理本次二维码 Release + if: ${{ always() && steps.publish.outcome == 'success' }} + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="qr-${{ github.run_id }}" + gh release delete "$TAG" --repo "$GITHUB_REPOSITORY" -y 2>/dev/null \ + && echo "✅ 已删除本次 Release: $TAG" || echo "⚠️ Release $TAG 不存在或已删除" + + # ═══ 兜底:清理残留的旧二维码 Release(保留最近 3 个)═══ - name: 清理旧的二维码 Release if: ${{ always() }} continue-on-error: true @@ -147,8 +155,8 @@ jobs: GH_TOKEN: ${{ github.token }} run: | gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \ - | awk '$1 ~ /^qr-/ {print $1}' | head -n -5 \ + | awk '$1 ~ /^qr-/ {print $1}' | head -n -3 \ | while read t; do - gh release delete "$t" --repo "$GITHUB_REPOSITORY" -y + gh release delete "$t" --repo "$GITHUB_REPOSITORY" -y 2>/dev/null echo "已删除旧 Release: $t" - done + done || true diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 35b3b6d..861d352 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -57,33 +57,10 @@ async function buildQr(url, index, total) { const dataUrl = await QRCode.toDataURL(url, { width: 320, margin: 2 }) const pngBase64 = dataUrl.replace(/^data:image\/png;base64,/, '') - // ── 3) 写入 Step Summary(Actions Summary 标签页查看)── - appendSummary([ - `## 🎵 酷狗音乐扫码登录${header}`, - '', - '### 👇 用酷狗音乐 APP 扫描下方二维码', - '', - `扫码登录${header}`, - '', - '> ⏳ 二维码有效期约 **2 分钟**,请尽快扫描。', - '', - '**备用链接**(复制到酷狗 App 打开):', - '', - `${url}`, - '', - '---', - '', - ].join('\n')) - - // ── 4) 日志输出:明确指引去哪里看图 ── + // ── 3) 日志输出:指引去直链步骤 ── printMagenta(`\n═══ 第 ${index}/${total} 个二维码已生成 ═══`) console.log('') - console.log(' 📱 扫码方式(按推荐顺序):') - console.log(' ① 查看后续「发布二维码图片直链」步骤的链接 → 浏览器打开即显示图片 → 直接扫') - console.log(' ② 点击本页面上方「Summary」标签查看图片') - console.log(' ③ 复制下方链接到酷狗 App 内打开:') - console.log('') - console.log(` ${url}`) + console.log(' 🔗 请查看下一步「发布二维码图片直链」输出的链接,浏览器打开即可直接扫码') console.log('') return { dataUrl, url, header, index } @@ -211,8 +188,8 @@ async function genMode() { } fs.writeFileSync(KEYS_FILE, JSON.stringify({ number, keys })) - printMagenta(`\n✅ 已生成 ${number} 个二维码。`) - printMagenta(`🔗 请查看下一步「发布二维码图片直链」输出的可点击链接,浏览器打开即可直接扫码(无需下载解压)!`) + printMagenta(`\n✅ 已生成 ${number} 个二维码。`) + printMagenta(`🔗 请查看下一步「发布二维码图片直链」输出的可点击链接,浏览器打开即可直接扫码!`) } catch (e) { const msg = e && e.message ? e.message : String(e) console.error(`::error::二维码生成失败:${msg}`) From 346a738e528291c4428f85b6ba134c9977358832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:54:58 +0800 Subject: [PATCH 17/18] =?UTF-8?q?fix:=20=E5=85=A8=E9=87=8F=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E4=BF=AE=E5=A4=8D10=E9=A1=B9=E9=97=AE=E9=A2=98=20-=20?= =?UTF-8?q?=E4=B8=A5=E9=87=8Dbug/=E8=BF=9B=E7=A8=8B=E9=80=80=E5=87=BA/?= =?UTF-8?q?=E6=AD=BB=E4=BB=A3=E7=A0=81/Summary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\347\240\201\347\231\273\345\275\225.yml" | 25 +++++-- main.js | 19 ++--- phoneLogin.js | 6 +- qrcodeLogin.js | 71 +++++++++++-------- sent.js | 6 +- 5 files changed, 68 insertions(+), 59 deletions(-) diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index d1c4c21..160c9bd 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -73,8 +73,6 @@ jobs: run: node qrcodeLogin.js gen env: NUMBER: ${{ github.event.inputs.number }} - USERINFO: ${{ secrets.USERINFO }} - APPEND_USER: ${{ github.event.inputs.append_user }} # ════════════════════════════════════════════ # 步骤二(核心):发布二维码为 GitHub Release 直链 @@ -93,13 +91,28 @@ jobs: --notes "用「酷狗音乐 APP」扫描下方二维码完成登录(约 2 分钟内有效)" echo "" - echo "══════════════════════════════════════════════════════════" + echo "═══════════════════════════════════════════════════════" echo " 📱 点击下方链接,浏览器直接显示二维码图片(无需解压):" - echo "══════════════════════════════════════════════════════════" + echo "═══════════════════════════════════════════════════════" for f in qr/*.png; do echo " 🔗 https://github.com/${{ github.repository }}/releases/download/$TAG/$(basename "$f")" done - echo "══════════════════════════════════════════════════════════" + echo "═══════════════════════════════════════════════════════" + + # 写入 Summary 页面,用户直接在 Summary 即可看到扫码链接 + { + echo "## 🎵 酷狗音乐扫码登录" + echo "" + echo "请点击下方链接,浏览器打开即可扫码(约 2 分钟内有效):" + echo "" + for f in qr/*.png; do + echo "- 🔗 [账号 $(basename "$f" .png | sed 's/qr-//')] https://github.com/${{ github.repository }}/releases/download/$TAG/$(basename "$f")" + done + echo "" + echo "🌐 [HTML 登录页(含所有二维码)](https://github.com/${{ github.repository }}/releases/download/$TAG/login.html)" + echo "" + echo "⏳ 二维码有效期约 2 分钟,请尽快扫描" + } >> "$GITHUB_STEP_SUMMARY" # ═══ 备用:HTML 仍上传为 artifact(万一 Release 失败兜底)═══ - name: 上传二维码 HTML 页面(兜底备份) @@ -127,7 +140,7 @@ jobs: # 步骤三:轮询等待用户扫码确认 # ════════════════════════════════════════════ - name: 等待扫码登录 - if: ${{ always() }} + if: ${{ always() && steps.gen.outcome == 'success' }} run: node qrcodeLogin.js wait env: USERINFO: ${{ secrets.USERINFO }} diff --git a/main.js b/main.js index 56bdc06..3c97eac 100644 --- a/main.js +++ b/main.js @@ -8,7 +8,6 @@ async function main() { const USERINFO = process.env.USERINFO // 刷新token - const refreshUserinfo = [] let needRefresh = false if (!USERINFO) { throw new Error("未配置") @@ -60,7 +59,7 @@ async function main() { printMagenta(`账号 ${safeNickname} 开始领取VIP...`) // 周日刷新token - if (today.getDay() == 0) { + if (today.getDay() === 0) { const refreshToken = await send(`/login/token?timestrap=${Date.now()}`, "POST", headers) if (refreshToken?.status == 1) { if (refreshToken?.data?.token !== user.token) { @@ -69,7 +68,6 @@ async function main() { user.token = refreshToken.data.token } } - refreshUserinfo.push(user) } // 开始听歌 @@ -117,7 +115,7 @@ async function main() { let vipExpiry = '未知' const vip_details = await send(`/user/vip/detail?timestrap=${Date.now()}`, "GET", headers) - if (vip_details.status === 1) { + if (vip_details.status === 1 && Array.isArray(vip_details.data?.busi_vip) && vip_details.data.busi_vip.length > 0) { vipExpiry = vip_details.data.busi_vip[0].vip_end_time printBlue(`今天是:${date}`) printBlue(`VIP到期时间:${vipExpiry}\n`) @@ -141,11 +139,10 @@ async function main() { close_api(api) } - // 更新secret - if (refreshUserinfo.length > 0 && needRefresh) { - + // 更新secret (使用完整 userinfo 数组,保留所有用户包括过期账号) + if (needRefresh) { if (hasSecretWriteToken()) { - const userinfoJSON = JSON.stringify(refreshUserinfo) + const userinfoJSON = JSON.stringify(userinfo) try { setRepoSecret("USERINFO", userinfoJSON) printGreen("secret token刷新成功") @@ -157,7 +154,6 @@ async function main() { } else { printYellow("存在账号需要刷新token,但是未配置PAT,未刷新token最多两个月后过期") } - } // 构建通知内容 @@ -191,9 +187,6 @@ async function main() { throw new Error("领取异常") } - if (api.killed) { - process.exit(0) - } } -main() +main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) diff --git a/phoneLogin.js b/phoneLogin.js index 296cd4a..6895671 100644 --- a/phoneLogin.js +++ b/phoneLogin.js @@ -36,10 +36,6 @@ async function login() { } finally { close_api(api) } - - if (api.killed) { - process.exit(0) - } } -login() +login().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 861d352..168615b 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -22,7 +22,6 @@ function appendSummary(markdown) { if (!SUMMARY_FILE) return try { fs.appendFileSync(SUMMARY_FILE, markdown + '\n') - // 验证写入成功 const size = fs.statSync(SUMMARY_FILE).size if (size > 0) { console.log(`[Summary] 已追加 ${Buffer.byteLength(markdown)} 字节,总计 ${size} 字节`) @@ -33,29 +32,25 @@ function appendSummary(markdown) { } /** - * 生成并展示单个二维码 — 三重展示渠道确保用户一定能扫到: + * 生成并展示单个二维码 — 展示渠道: * - * ① 自包含 HTML 页面(qr/login.html):浏览器打开即见大图,手机直接扫 - * ← 最可靠,不依赖任何平台限制,artifact 立即可下载 - * ② Step Summary 摘要页: data URI 图片,在 Actions Summary 标签页查看 - * ③ PNG 文件(qr/qr-N.png):高清原图,artifact 下载备用 + * ① PNG 文件(qr/qr-N.png):Release 直链 + HTML 内嵌双用途 + * ② 自包含 HTML 页面(qr/login.html):浏览器打开即见大图,手机直接扫 + * ③ base64 data URI:HTML 共用 * * @param {string} url 酷狗扫码登录完整 URL * @param {number} index 账号序号(从 1 开始) * @param {number} total 总账号数 - * @returns {{ dataUrl: string, url: string }} 供 HTML 聚合用 + * @returns {{ dataUrl: string, url: string, header: string, index: number }} 供 HTML 聚合用 */ async function buildQr(url, index, total) { const header = total > 1 ? `(第 ${index}/${total} 个账号)` : '' - fs.mkdirSync(QR_DIR, { recursive: true }) - - // ── 1) PNG 文件(artifact 下载 + HTML 内嵌双用途)── + // ── 1) PNG 文件(Release 直链 + HTML 内嵌双用途)── await QRCode.toFile(`${QR_DIR}/qr-${index}.png`, url, { width: 320, margin: 2 }) - // ── 2) base64 data URI(Summary + HTML 共用)── + // ── 2) base64 data URI(HTML 共用)── const dataUrl = await QRCode.toDataURL(url, { width: 320, margin: 2 }) - const pngBase64 = dataUrl.replace(/^data:image\/png;base64,/, '') // ── 3) 日志输出:指引去直链步骤 ── printMagenta(`\n═══ 第 ${index}/${total} 个二维码已生成 ═══`) @@ -128,25 +123,27 @@ body{ ` } -/** 解析账号数量 */ +/** 解析账号数量,无效输入回退为 1 */ function resolveNumber() { const args = process.argv.slice(3) - return parseInt(process.env.NUMBER || args[0] || "1") + const n = parseInt(process.env.NUMBER || args[0] || "1") + return (Number.isNaN(n) || n < 1) ? 1 : n } /** - * 模式一:生成二维码(PNG + Summary + HTML),随后立即结束 step。 - * step 结束后 artifact 即可下载 HTML 页面(含大图),用户浏览器打开直接扫码。 + * 模式一:生成二维码(PNG + HTML),随后立即结束 step。 + * step 结束后 Release 直链即可使用,用户浏览器打开直接扫码。 */ async function genMode() { const api = startService() await delay(2000) - const USERINFO = process.env.USERINFO - const APPEND_USER = process.env.APPEND_USER - const userinfo = (USERINFO && APPEND_USER == "是") ? JSON.parse(USERINFO) : [] const number = resolveNumber() const keys = [] + // 清理上次运行残留的 QR 文件,避免旧二维码混入本次 Release + fs.rmSync(QR_DIR, { recursive: true, force: true }) + fs.mkdirSync(QR_DIR, { recursive: true }) + if (!SUMMARY_FILE) { console.log('[INFO] 非 Actions 环境($GITHUB_STEP_SUMMARY 未设置),Summary 将跳过') } @@ -190,16 +187,16 @@ async function genMode() { fs.writeFileSync(KEYS_FILE, JSON.stringify({ number, keys })) printMagenta(`\n✅ 已生成 ${number} 个二维码。`) printMagenta(`🔗 请查看下一步「发布二维码图片直链」输出的可点击链接,浏览器打开即可直接扫码!`) + + // 写入 Summary 提示 + appendSummary(`## 🎵 酷狗音乐扫码登录\n\n✅ 已生成 ${number} 个二维码,请查看下一步「发布二维码图片直链」输出的链接进行扫码。\n\n⏳ 二维码有效期约 2 分钟,请尽快扫描。`) } catch (e) { const msg = e && e.message ? e.message : String(e) console.error(`::error::二维码生成失败:${msg}`) + appendSummary(`## ❌ 二维码生成失败\n\n错误信息:${msg}`) throw e } finally { - // 强制关闭 API 服务(加超时防止挂死) - await Promise.race([ - close_api(api), - new Promise(r => setTimeout(r, 3000)) - ]) + close_api(api) } } @@ -220,11 +217,19 @@ async function waitMode() { const APPEND_USER = process.env.APPEND_USER const userinfo = (USERINFO && APPEND_USER == "是") ? JSON.parse(USERINFO) : [] + const results = [] // 收集每个账号的扫码结果用于 Summary + try { for (let n = 0; n < number; n++) { const qrcode = keys[n] + if (!qrcode) { + printRed(`第 ${n + 1}/${number} 个账号的二维码密钥缺失,跳过`) + results.push({ index: n + 1, status: '密钥缺失' }) + continue + } printMagenta(`\n正在等待第 ${n + 1}/${number} 个账号扫码登录...`) let loggedIn = false + let expireFlag = false for (let i = 0; i < 30; i++) { const timestrap = Date.now(); const res = await send(`/login/qr/check?key=${qrcode}×trap=${timestrap}`, "GET", {}) @@ -232,6 +237,7 @@ async function waitMode() { switch (status) { case 0: printYellow("二维码已过期,请重新运行工作流生成新二维码") + expireFlag = true break case 1: // 未扫描二维码 @@ -248,21 +254,26 @@ async function waitMode() { printRed("请求出错") console.dir(summarizeResponse(res), { depth: null }) } - if (loggedIn || status == 0) { + if (loggedIn || expireFlag) { break } - if (i == 29) { + if (i === 29) { printRed("等待超时\n") } await delay(5000) } + results.push({ + index: n + 1, + status: loggedIn ? '✅ 登录成功' : (expireFlag ? '❌ 二维码过期' : '❌ 等待超时') + }) } saveUserinfo(userinfo) + + // 写入扫码结果到 Summary + const resultLines = results.map(r => `- 账号 ${r.index}/${number}:${r.status}`).join('\n') + appendSummary(`### 扫码结果\n\n${resultLines}`) } finally { - await Promise.race([ - close_api(api), - new Promise(r => setTimeout(r, 3000)) - ]) + close_api(api) } } diff --git a/sent.js b/sent.js index 41226e5..e7852a2 100644 --- a/sent.js +++ b/sent.js @@ -26,10 +26,6 @@ async function login() { } finally { close_api(api) } - - if (api.killed) { - process.exit(0) - } } -login() +login().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) From ce2444cefde44082da1c270b43d00182ef749f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=A0=E4=B9=A0?= <25678988+xfxx2022@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:06:57 +0800 Subject: [PATCH 18/18] =?UTF-8?q?perf:=20=E5=85=A8=E9=87=8F=E4=BC=98?= =?UTF-8?q?=E5=8C=9610=E9=A1=B9=20-=20send=E8=B6=85=E6=97=B6=E9=87=8D?= =?UTF-8?q?=E8=AF=95/=E9=80=9A=E7=9F=A5=E9=98=B2=E8=B7=B3=E8=BF=87/STARTTL?= =?UTF-8?q?S/Actions=E7=89=88=E6=9C=AC=E7=BB=9F=E4=B8=80=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\347\240\201\347\231\273\345\275\225.yml" | 6 +- ...2\345\217\267\347\231\273\345\275\225.yml" | 6 +- .../workflows/\347\255\276\345\210\260.yml" | 7 +- .gitignore | 18 +++ main.js | 11 +- package.json | 3 + qrcodeLogin.js | 3 +- utils/notify.js | 138 ++++++++++++++---- utils/safeLog.js | 4 +- utils/utils.js | 36 ++++- 10 files changed, 181 insertions(+), 51 deletions(-) create mode 100644 .gitignore diff --git "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" index 160c9bd..bed8fdb 100644 --- "a/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\344\272\214\347\273\264\347\240\201\347\231\273\345\275\225.yml" @@ -31,10 +31,10 @@ jobs: timeout-minutes: 15 steps: - name: 检出仓库 - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: 创建 Node 环境 - uses: actions/setup-node@v6 + uses: actions/setup-node@v4 with: node-version: 24 cache: 'npm' @@ -53,7 +53,7 @@ jobs: - name: 缓存依赖 id: cache-npm - uses: actions/cache@v5 + uses: actions/cache@v4 with: path: ./api/node_modules key: ${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }} diff --git "a/.github/workflows/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225.yml" "b/.github/workflows/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225.yml" index 2b405a4..216dce8 100644 --- "a/.github/workflows/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225.yml" +++ "b/.github/workflows/\346\211\213\346\234\272\345\217\267\347\231\273\345\275\225.yml" @@ -34,9 +34,9 @@ jobs: timeout-minutes: 2 steps: - name: 检出仓库 - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: 创建 Node 环境 - uses: actions/setup-node@v6 + uses: actions/setup-node@v4 with: node-version: 24 cache: 'npm' @@ -54,7 +54,7 @@ jobs: gh --version - name: 缓存依赖 id: cache-npm - uses: actions/cache@v5 + uses: actions/cache@v4 with: path: ./api/node_modules key: ${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }} diff --git "a/.github/workflows/\347\255\276\345\210\260.yml" "b/.github/workflows/\347\255\276\345\210\260.yml" index 130aa21..8f3b6d9 100644 --- "a/.github/workflows/\347\255\276\345\210\260.yml" +++ "b/.github/workflows/\347\255\276\345\210\260.yml" @@ -8,11 +8,12 @@ permissions: jobs: run: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: 检出仓库 - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: 创建 Node 环境 - uses: actions/setup-node@v6 + uses: actions/setup-node@v4 with: node-version: 24 cache: 'npm' @@ -29,7 +30,7 @@ jobs: gh --version - name: 缓存依赖 id: cache-npm - uses: actions/cache@v5 + uses: actions/cache@v4 with: path: ./api/node_modules key: ${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfb5282 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# 依赖 +node_modules/ + +# 运行时生成的临时文件 +qr/ +qrkeys.json + +# 环境变量 +.env +.env.* + +# 系统文件 +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ diff --git a/main.js b/main.js index 3c97eac..bcae6b9 100644 --- a/main.js +++ b/main.js @@ -140,6 +140,7 @@ async function main() { } // 更新secret (使用完整 userinfo 数组,保留所有用户包括过期账号) + let secretError = null if (needRefresh) { if (hasSecretWriteToken()) { const userinfoJSON = JSON.stringify(userinfo) @@ -149,14 +150,14 @@ async function main() { } catch (error) { printRed("token刷新失败") console.dir(sanitizeForLog({ message: error.message }), { depth: null }) - throw new Error("secret token刷新失败") + secretError = new Error("secret token刷新失败") } } else { printYellow("存在账号需要刷新token,但是未配置PAT,未刷新token最多两个月后过期") } } - // 构建通知内容 + // 构建通知内容(放在 secret 更新之后、错误抛出之前,确保始终执行) const title = `酷狗签到${hasError ? '异常' : '成功'} ${date}` let content = `📅 日期: ${date}\n` content += `📊 账号数: ${notifyResults.length}\n` @@ -174,7 +175,7 @@ async function main() { } } - // 发送通知 + // 发送通知(确保即使 secret 更新失败也能发出) try { await sendNotify(title, content) } catch (e) { @@ -187,6 +188,10 @@ async function main() { throw new Error("领取异常") } + if (secretError) { + throw secretError + } + } main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) }) diff --git a/package.json b/package.json index da8c6cd..26bbd10 100644 --- a/package.json +++ b/package.json @@ -8,5 +8,8 @@ "phoneLogin": "node phoneLogin.js", "sent": "node sent.js", "qrcodeLogin": "node qrcodeLogin.js" + }, + "dependencies": { + "qrcode": "^1.5.3" } } diff --git a/qrcodeLogin.js b/qrcodeLogin.js index 168615b..39b4e8c 100644 --- a/qrcodeLogin.js +++ b/qrcodeLogin.js @@ -260,7 +260,8 @@ async function waitMode() { if (i === 29) { printRed("等待超时\n") } - await delay(5000) + // 前 10 次用 3 秒间隔(快速响应),之后用 5 秒间隔 + await delay(i < 10 ? 3000 : 5000) } results.push({ index: n + 1, diff --git a/utils/notify.js b/utils/notify.js index d1f53e8..c65d705 100644 --- a/utils/notify.js +++ b/utils/notify.js @@ -101,7 +101,7 @@ async function sendServerChan(title, content, sendkey) { // 6. PushPlus async function sendPushPlus(title, content, token, topic) { - const url = 'http://www.pushplus.plus/send' + const url = 'https://www.pushplus.plus/send' const body = { token, title, content, template: 'txt' } if (topic) body.topic = topic const resp = await fetch(url, { @@ -150,7 +150,8 @@ async function sendDiscord(title, content, webhookUrl) { return resp.ok } -// 10. 邮箱 SMTP(使用 node:tls 实现最小 SMTP 客户端) +// 10. 邮箱 SMTP(使用 node:net/node:tls 实现最小 SMTP 客户端) +// 支持隐式 TLS(端口 465)和 STARTTLS(端口 587/25) function sendMailSMTP(title, content, cfg) { return new Promise((resolve, reject) => { const { host, port, user, pass, to } = cfg @@ -172,9 +173,22 @@ function sendMailSMTP(title, content, cfg) { content, ].join('\r\n') - const lines = [ - `EHLO kgcheckin`, - `AUTH LOGIN`, + // 隐式 TLS (465) 直接 TLS 连接;STARTTLS (587/25) 先明文再升级 + const smtpPort = Number(port) || 465 + const useSTARTTLS = smtpPort !== 465 + + // SMTP 会话状态机 + // 隐式 TLS: GREETING → EHLO_SENT → AUTH 流程 + // STARTTLS: GREETING → EHLO_SENT → STARTTLS_SENT → TLS_UPGRADED → EHLO2_SENT → AUTH 流程 + let state = 'GREETING' + let buffer = '' + let socket + let tlsSocket + let timer + + // AUTH 流程命令序列(EHLO 之后依次执行) + const authCommands = [ + 'AUTH LOGIN', Buffer.from(user).toString('base64'), Buffer.from(pass).toString('base64'), `MAIL FROM:<${from}>`, @@ -183,46 +197,108 @@ function sendMailSMTP(title, content, cfg) { mailBody.replace(/\r?\n/g, '\r\n') + '\r\n.', 'QUIT', ] + let authStep = 0 - let step = 0 - let buffer = '' - const socket = tls.connect( - { host, port: Number(port) || 465, rejectUnauthorized: true }, - () => {} - ) + function sendCmd(cmd) { + socket.write(cmd + '\r\n') + } - socket.setEncoding('utf8') - socket.on('data', (chunk) => { + function processData(line) { + const code = parseInt(line.slice(0, 3), 10) + + if (code >= 400) { + socket.destroy() + reject(new Error(`SMTP 错误: ${line}`)) + return + } + + // 等待多行响应结束(中间行以 "code-" 开头,最后一行以 "code " 开头) + if (line[3] === '-') return + + switch (state) { + case 'GREETING': + // 收到 220 服务器就绪 → 发送 EHLO + sendCmd('EHLO kgcheckin') + state = 'EHLO_SENT' + break + + case 'EHLO_SENT': + if (useSTARTTLS) { + // STARTTLS 模式:EHLO 响应后发送 STARTTLS + sendCmd('STARTTLS') + state = 'STARTTLS_SENT' + } else { + // 隐式 TLS:直接进入 AUTH 流程 + state = 'AUTH' + authStep = 0 + sendCmd(authCommands[authStep++]) + } + break + + case 'STARTTLS_SENT': + // 收到 220 → 升级为 TLS + if (code !== 220) { + socket.destroy() + reject(new Error(`STARTTLS 失败: ${line}`)) + return + } + tlsSocket = require('node:tls').connect({ socket, host, rejectUnauthorized: true }) + tlsSocket.setEncoding('utf8') + tlsSocket.on('data', (chunk) => { handleChunk(chunk) }) + tlsSocket.on('end', () => { clearTimeout(timer); resolve(true) }) + tlsSocket.on('close', () => { clearTimeout(timer); resolve(true) }) + tlsSocket.on('error', (err) => { clearTimeout(timer); reject(err) }) + socket = tlsSocket + // 升级后重新发送 EHLO + sendCmd('EHLO kgcheckin') + state = 'EHLO2_SENT' + break + + case 'EHLO2_SENT': + // STARTTLS 升级后的第二次 EHLO 响应 → 进入 AUTH 流程 + state = 'AUTH' + authStep = 0 + sendCmd(authCommands[authStep++]) + break + + case 'AUTH': + if (authStep < authCommands.length) { + sendCmd(authCommands[authStep++]) + } + break + } + } + + function handleChunk(chunk) { buffer += chunk while (true) { const idx = buffer.indexOf('\r\n') if (idx === -1) break const line = buffer.slice(0, idx) buffer = buffer.slice(idx + 2) - const code = parseInt(line.slice(0, 3), 10) - - if (code >= 400) { - socket.destroy() - reject(new Error(`SMTP 错误: ${line}`)) - return - } - - // 等待多行响应结束(最后一行以 "code " 开头,中间行以 "code-" 开头) - if (line[3] === '-') continue - - if (step < lines.length) { - socket.write(lines[step] + '\r\n') - step++ - } + processData(line) } - }) + } // 超时保护 - const timer = setTimeout(() => { - socket.destroy() + timer = setTimeout(() => { + if (socket) socket.destroy() reject(new Error('SMTP 超时')) }, 15000) + if (useSTARTTLS) { + // STARTTLS:先明文连接 + socket = require('node:net').connect(smtpPort, host, () => {}) + } else { + // 隐式 TLS:直接 TLS 连接 + socket = require('node:tls').connect( + { host, port: smtpPort, rejectUnauthorized: true }, + () => {} + ) + } + + socket.setEncoding('utf8') + socket.on('data', (chunk) => handleChunk(chunk)) socket.on('end', () => { clearTimeout(timer); resolve(true) }) socket.on('close', () => { clearTimeout(timer); resolve(true) }) socket.on('error', (err) => { clearTimeout(timer); reject(err) }) diff --git a/utils/safeLog.js b/utils/safeLog.js index 8622a15..ba04057 100644 --- a/utils/safeLog.js +++ b/utils/safeLog.js @@ -126,7 +126,9 @@ function summarizeResponse(response) { } function shouldPrintSensitiveValue() { - return ["是", "true", "1", "yes"].includes(String(process.env.ALLOW_PRINT_USERINFO || "").toLowerCase()); + const raw = String(process.env.ALLOW_PRINT_USERINFO || "").trim().toLowerCase() + // 中文“是”直接匹配,英文值需要小写后匹配 + return raw === "是" || ["true", "1", "yes"].includes(raw) } export { maskDisplayName, maskIdentifier, sanitizeForLog, shouldPrintSensitiveValue, summarizeResponse }; diff --git a/utils/utils.js b/utils/utils.js index c96cec5..13f388e 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -36,13 +36,37 @@ function close_api(api) { } } -/** 发送请求到本地 api 服务 */ +/** + * 发送请求到本地 api 服务(带超时 + 重试) + * 超时 10 秒,失败后指数退避重试最多 3 次 + */ async function send(path, method, headers) { - const result = await fetch('http://127.0.0.1:3000' + path, { - method, - headers, - }).then(r => r.json()) - return result + const MAX_RETRIES = 3 + const TIMEOUT_MS = 10000 + let lastError + + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) + try { + const resp = await fetch('http://127.0.0.1:3000' + path, { + method, + headers, + signal: controller.signal, + }) + clearTimeout(timer) + return await resp.json() + } catch (err) { + clearTimeout(timer) + lastError = err + if (attempt < MAX_RETRIES - 1) { + const waitMs = 1000 * Math.pow(2, attempt) // 1s, 2s, 4s + console.log(`[send] 第 ${attempt + 1} 次请求失败,${waitMs}ms 后重试: ${err.message}`) + await delay(waitMs) + } + } + } + throw lastError } export { delay, startService, close_api, send }