From a898e1d86c55d5993a68852555861619f6aeefbe Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Wed, 5 Aug 2026 18:11:31 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BA=91?= =?UTF-8?q?=E8=B4=9D=E4=BB=BB=E5=8A=A1=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interface.d.ts | 31 +- module/ad_get.js | 21 +- module/ad_listening_rights_gain.js | 210 ++++++++- module/register_checktoken_v2.js | 46 +- module/register_checktoken_v3.js | 31 +- module/register_xeapikey.js | 4 +- module/yunbei_sign.js | 6 +- module/yunbei_task_finish_v1.js | 26 ++ module/yunbei_task_list_v1.js | 16 + module/yunbei_task_recommend_song.js | 20 + module_example/yunbei_ad_task.js | 48 +++ public/docs/home.md | 68 ++- public/index.html | 1 + public/yunbei_task.html | 622 +++++++++++++++++++++++++++ util/request.js | 12 +- 15 files changed, 1066 insertions(+), 96 deletions(-) create mode 100644 module/yunbei_task_finish_v1.js create mode 100644 module/yunbei_task_list_v1.js create mode 100644 module/yunbei_task_recommend_song.js create mode 100644 module_example/yunbei_ad_task.js create mode 100644 public/yunbei_task.html diff --git a/interface.d.ts b/interface.d.ts index 9c430ff..019ecaa 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -48,18 +48,26 @@ export function ad_listening_rights( export function ad_listening_rights_gain( params: { reqUid?: string - creativeType?: string | number + uid?: string | number exposureTime?: string | number clickTime?: string | number + extraRightsType?: string | number + playContinuously?: boolean | string + source?: string | number + creativeType?: string | number rightsGainMethod?: string | number - rightsGainDuration?: string | number extraRightsGainMethod?: string | number extraRightsGainDuration?: string | number nextRightsGainDuration?: string | number - source?: string + rightsGainType?: string | number + rightsGainDuration?: string | number + gainMethodStep?: string | number + generalRightsInfo?: string rightsExtJson?: string appInfo?: string + contextInfo?: string installed?: string | number + sniffTime?: string | number type_ids?: string } & RequestBaseConfig, ): Promise @@ -1662,6 +1670,23 @@ export function yunbei_task_finish( } & RequestBaseConfig, ): Promise +export function yunbei_ad_task_list( + params: RequestBaseConfig, +): Promise + +export function yunbei_ad_task_recommend_song( + params: { + offset?: number | string + limit?: number | string + } & RequestBaseConfig, +): Promise + +export function yunbei_ad_task_finish( + params: { + yunbeiAmount?: number | string + } & RequestBaseConfig, +): Promise + export function msg_recentcontact(params: RequestBaseConfig): Promise export function hug_comment( diff --git a/module/ad_get.js b/module/ad_get.js index 742107a..9b70c2d 100644 --- a/module/ad_get.js +++ b/module/ad_get.js @@ -17,9 +17,24 @@ module.exports = async (query, request) => { try { if (raw?.ads) { const ad = Object.values(raw.ads)[0] - if (ad?.extJson) { - const ext = JSON.parse(ad.extJson) - reqId = ext?.contextInfo?.req_id || '' + // 逆向 v9.5.61:客户端从 ad.adExtMap["req_id"] 取 reqUid + if (ad?.adExtMap) { + if (typeof ad.adExtMap === 'string') { + try { + reqId = JSON.parse(ad.adExtMap).req_id || '' + } catch (_) {} + } else { + reqId = ad.adExtMap.req_id || '' + } + } + // 兜底:adLogId.requestId / ad.reqId / extJson.contextInfo.req_id + if (!reqId && ad?.adLogId?.requestId) reqId = ad.adLogId.requestId + if (!reqId && ad?.reqId) reqId = ad.reqId + if (!reqId && ad?.extJson) { + try { + const ext = JSON.parse(ad.extJson) + reqId = ext?.contextInfo?.req_id || '' + } catch (_) {} } } } catch (_) {} diff --git a/module/ad_listening_rights_gain.js b/module/ad_listening_rights_gain.js index 9c7d108..2099c5d 100644 --- a/module/ad_listening_rights_gain.js +++ b/module/ad_listening_rights_gain.js @@ -1,65 +1,229 @@ // 看广告免费听歌 - 领取免费听权益 -// 请求流程(基于逆向网易云音乐 v9.5.45 RN Hermes 源码): +// 请求流程(基于逆向网易云音乐 v9.5.61 原生 Kotlin 源码,classes5/18/19.dex): // 1. 从广告平台拉广告 → 用户看完/点击广告 → 获取 ad 对象的 extJson.contextInfo.req_id 作为 reqUid -// 2. 调用本接口传入 reqUid 及相关权益参数,领取免费听权益 +// 2. 调用本接口传入 reqUid 及相关权益参数,领取权益 // 3. 服务器返回 gainFlag 等标识,用于展示领取结果 +// +// 调用链(逆向还原): +// AdDSLIncentiveVideoRightsHelper.requestRightsGainInner +// → AdDSLUtils.requestRightGain(ad, exposeTime, clickTime, creativeType, cb) +// → 构造 ListeningRightRequestParams(21 字段)→ JSON.stringify +// → body = { "reqParam": "..." } → POST /api/ad/listening/rights/gain +// (注意:客户端 AdDSLIncentiveVideoRightsHelper 写死 creativeType=36) +// +// 接口不只领取听歌时长,还包含"看视频得云贝"等权益: +// rightsGainMethod=6 (LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益) 时, +// 弹窗 AdLaxinSegmentedGuideDialog 展示 "获得X云贝"(2000 云贝等), +// 权益数值由广告下发的 AdLaxinSegmentedRightsGuidePopup.rightsValue 决定, +// extraRightsType 作为权益类型随本接口上传。 +// +// rightsGainMethod 枚举(h80/a): +// 1=EXPOSE 曝光, 2=EXPOSE_CLICK 曝光+点击, 3=EXPOSE_DOWNLOAD 曝光+下载, +// 4=CLICK_STAY 点击+停留, 5=EXPOSURE_OR_CLICK_STAY 曝光或点击停留, +// 6=LAXIN_EXPOSE_OR_DOWNLOAD 拉新曝光/下载分段权益 const createOption = require('../util/option.js') const adGet = require('./ad_get.js') +// 安全 JSON 解析(字符串字段可能是 JSON 文本) +function safeParse(str, fallback) { + if (typeof str !== 'string') return fallback + try { + return JSON.parse(str) + } catch (_) { + return fallback + } +} + module.exports = async (query, request) => { const time = Date.now() - // 如果未传入 reqUid,自动从 ad_get 获取最新广告的 req_id + // 从广告对象自动补齐请求字段。 + // 实测 v9.5.61 真实 API 返回(/ad/get)确认的字段路径: + // reqUid = ad.extJson.contextInfo.req_id(真实响应中唯一存在) + // contextInfo = ad.extJson.contextInfo(完整对象序列化) + // generalRightsInfo = ad.generalRightsInfo(字符串 JSON) + // creativeType = ad.creativeType + // rightsGainMethod / extraRightsType / rightsGainDuration / rightsGainType / + // extraRightsGainMethod / extraRightsGainDuration / nextRightsGainDuration / + // rightsExtJson / source / rightsUpperLimit / qualified + // = ad.generalRightsInfo(字符串)解析后取值 + // (逆向类字段 adExtMap/adLogId/listeningRightHintInfo 真实响应中不存在,仅兜底) + // sniffTime = method==3 或 6 时 currentTimeMillis let reqUid = query.reqUid || '' - if (!reqUid) { + let contextInfo = query.contextInfo + let creativeType = query.creativeType + let generalRightsInfo = query.generalRightsInfo + + // 广告 hint 配置:仅当调用方未显式传参时用广告下发值兜底 + const hint = {} + + if (!reqUid || contextInfo === undefined || creativeType === undefined) { try { const adRes = await adGet( { ...query, type_ids: query.type_ids || '["400002_0"]' }, request, ) - reqUid = adRes?.body?.extra?.reqId || '' + const ad = Object.values(adRes?.body?.ads || {})[0] + if (!ad) throw new Error('ads 为空(未登录或广告位无广告)') + + // reqUid:真实 API 返回中 req_id 在 ad.extJson.contextInfo.req_id(实测 v9.5.61) + // (逆向类字段 adExtMap/adLogId 在真实响应中不存在,仅作兜底) + if (!reqUid) { + const ext = + typeof ad?.extJson === 'string' + ? safeParse(ad.extJson, {}) + : ad?.extJson || {} + const extMap = + typeof ad?.adExtMap === 'string' + ? safeParse(ad.adExtMap, {}) + : ad?.adExtMap + reqUid = + ext?.contextInfo?.req_id || + extMap?.req_id || + ad?.adLogId?.requestId || + ad?.reqId || + adRes?.body?.extra?.reqId || + '' + } + // contextInfo:真实 API 返回中在 ad.extJson.contextInfo(实测 v9.5.61) + if (contextInfo === undefined) { + const ext = + typeof ad?.extJson === 'string' + ? safeParse(ad.extJson, {}) + : ad?.extJson || {} + const ci = + ext?.contextInfo || ad?.adLogId?.contextInfo || ad?.showContext + if (ci) { + contextInfo = typeof ci === 'string' ? ci : JSON.stringify(ci) + } + } + if (creativeType === undefined && ad?.creativeType !== undefined) { + creativeType = ad.creativeType + } + if (generalRightsInfo === undefined && ad?.generalRightsInfo) { + generalRightsInfo = + typeof ad.generalRightsInfo === 'string' + ? ad.generalRightsInfo + : JSON.stringify(ad.generalRightsInfo) + } + // 缓存广告下发的领取配置(后续用于兜底) + // 实测 v9.5.61:rightsGainMethod/rightsUpperLimit/qualified 等在 + // ad.generalRightsInfo(字符串)里,ad.listeningRightHintInfo 真实响应中不存在 + const hintCfg = + typeof ad?.listeningRightHintInfo === 'string' + ? safeParse(ad.listeningRightHintInfo, {}) + : ad?.listeningRightHintInfo || {} + const gri = + typeof ad?.generalRightsInfo === 'string' + ? safeParse(ad.generalRightsInfo, {}) + : ad?.generalRightsInfo || {} + Object.assign(hint, hintCfg, gri) console.log(`自动获取 reqUid: ${reqUid}`) } catch (e) { // 获取广告失败,后续请求会因缺少 reqUid 被拒绝 } } + const rightsGainMethod = query.rightsGainMethod + ? parseInt(query.rightsGainMethod) + : hint.rightsGainMethod || 2 + const rightsParam = { // 必填: 广告请求 ID,自动从 ad_get 获取 reqUid, - // 广告创意类型 (默认 1) - creativeType: parseInt(query.creativeType || 2), + // 曝光时间戳 + exposureTime: query.exposureTime ? parseInt(query.exposureTime) : time, - // 时间戳 - exposureTime: query.exposureTime || time, - clickTime: query.clickTime || time, + // 当前登录用户 ID(原版客户端从 Profile.getUserId() 获取) + userId: query.uid ? parseInt(query.uid) : undefined, - // 权益领取方式 - rightsGainMethod: parseInt(query.rightsGainMethod || 2), + // 点击时间戳 + clickTime: query.clickTime ? parseInt(query.clickTime) : time, - // 权益时长相关 - rightsGainDuration: query.rightsGainDuration - ? parseInt(query.rightsGainDuration) - : undefined, + // 额外权益类型(拉新分段权益等,决定发放云贝/时长/下载,来自广告配置) + extraRightsType: query.extraRightsType + ? parseInt(query.extraRightsType) + : hint.extraRightsType !== undefined + ? parseInt(hint.extraRightsType) + : undefined, + + // 是否连续播放(默认 false) + playContinuously: query.playContinuously ? true : false, + + // 来源标识(原版从 ad.listeningRightHintInfo.source 读取) + source: query.source + ? parseInt(query.source) + : hint.source !== undefined + ? parseInt(hint.source) + : undefined, + + // 广告创意类型(激励视频场景=36,优先取 query / 广告对象) + creativeType: creativeType !== undefined ? parseInt(creativeType) : 36, + + // 权益领取方式(1~6 枚举,见文件头注释) + rightsGainMethod, + + // 权益扩展方式与时长 extraRightsGainMethod: query.extraRightsGainMethod ? parseInt(query.extraRightsGainMethod) - : undefined, + : hint.extraRightsGainMethod !== undefined + ? parseInt(hint.extraRightsGainMethod) + : undefined, extraRightsGainDuration: query.extraRightsGainDuration ? parseInt(query.extraRightsGainDuration) - : undefined, + : hint.extraRightsGainDuration !== undefined + ? parseInt(hint.extraRightsGainDuration) + : undefined, nextRightsGainDuration: query.nextRightsGainDuration ? parseInt(query.nextRightsGainDuration) + : hint.nextRightsGainDuration !== undefined + ? parseInt(hint.nextRightsGainDuration) + : undefined, + + // 权益类型(含 RIGHTS_GAIN_TYPE_CURRENT_DAY 等取值) + rightsGainType: query.rightsGainType + ? parseInt(query.rightsGainType) + : hint.rightsGainType !== undefined + ? parseInt(hint.rightsGainType) + : undefined, + + // 权益时长 + rightsGainDuration: query.rightsGainDuration + ? parseInt(query.rightsGainDuration) + : hint.rightsGainDuration !== undefined + ? parseInt(hint.rightsGainDuration) + : undefined, + + // 领取步骤(UNGAIN/GAINING/GAIN_FINISHED 对应的值) + gainMethodStep: query.gainMethodStep + ? parseInt(query.gainMethodStep) : undefined, - // 来源标识 | 权益扩展信息 - source: query.source || undefined, - rightsExtJson: query.rightsExtJson || undefined, + // 通用权益信息(ad.generalRightsInfo 序列化 JSON) + generalRightsInfo, + + // 权益扩展信息 + rightsExtJson: query.rightsExtJson || hint.rightsExtJson || undefined, // 应用信息(下载类广告) appInfo: query.appInfo ? JSON.parse(query.appInfo) : undefined, + + // 广告上下文(ad.adLogId.contextInfo,客户端真实来源) + contextInfo, + + // 应用是否已安装(下载类广告) installed: query.installed ? parseInt(query.installed) : undefined, + + // 嗅探时间:逆向确认(classes5.dex AdDSLUtils.requestRightGain): + // rightsGainMethod==3(曝光+下载) 或 6(LAXIN) 时传 currentTimeMillis + sniffTime: + rightsGainMethod === 3 || rightsGainMethod === 6 + ? query.sniffTime + ? parseInt(query.sniffTime) + : time + : undefined, } // 清理 undefined 字段 @@ -67,7 +231,7 @@ module.exports = async (query, request) => { if (rightsParam[key] === undefined) delete rightsParam[key] }) - // 将参数序列化为 reqParam 字符串 (与 RN 源码完全一致) + // 将参数序列化为 reqParam 字符串 (与原生源码一致) const data = { reqParam: JSON.stringify(rightsParam), } @@ -75,7 +239,7 @@ module.exports = async (query, request) => { const res = await request( `/api/ad/listening/rights/gain`, data, - createOption(query, 'xeapi', 'v3'), + createOption(query, 'xeapi', 'v2'), ) return { diff --git a/module/register_checktoken_v2.js b/module/register_checktoken_v2.js index 86faf23..8302136 100644 --- a/module/register_checktoken_v2.js +++ b/module/register_checktoken_v2.js @@ -1,15 +1,16 @@ // 易盾反作弊 Token 注册端点 // 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用 // -// GET /register/checktoken → 返回当前 token(尚无则自动获取) -// POST /register/checktoken → 强制刷新 token -// GET /register/checktoken?refresh=1 → 强制刷新 +// GET /register/checktoken → 实时获取新 token(不缓存) +// POST /register/checktoken → 实时获取新 token +// +// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token, +// 避免反作弊 token 复用触发风控。 // const { default: axios } = require('axios') const { APP_CONF } = require('../util/config.json') const URL = APP_CONF.dunDomainV2 + '/v2/config/js?pn=YD00000558929251' -let _token = '' async function fetch() { const res = await axios.get(URL, { timeout: 10000 }) @@ -20,13 +21,13 @@ async function fetch() { throw new Error('易盾返回异常: ' + JSON.stringify(data).substring(0, 200)) } -// 端点处理 -module.exports = async (query) => { - const refresh = query.refresh === '1' || query.refresh === 'true' - let token = refresh ? null : _token - if (!token) { +// 端点处理:每次实时获取新 token +module.exports = async () => { + let token = '' + try { token = await fetch() - _token = token + } catch (e) { + // token 获取失败时返回空,由调用方决定是否重试 } return { status: 200, @@ -34,24 +35,11 @@ module.exports = async (query) => { } } -// 模块加载时自动获取一次 Token,确保 request.js 使用时不为空 -fetch() - .then((token) => { - _token = token - }) - .catch(() => { - // 静默失败,后续请求时会重试 - }) - -// 给 request.js 读取用 -module.exports.getToken = () => { - if (!_token) { - // 如果 Token 为空,异步触发获取 - fetch() - .then((token) => { - _token = token - }) - .catch(() => {}) +// 给 request.js 读取用:每次调用实时获取新 token,不缓存 +module.exports.getToken = async () => { + try { + return await fetch() + } catch (e) { + return '' } - return _token } diff --git a/module/register_checktoken_v3.js b/module/register_checktoken_v3.js index 66b28a2..a0deefa 100644 --- a/module/register_checktoken_v3.js +++ b/module/register_checktoken_v3.js @@ -1,15 +1,16 @@ // 易盾反作弊 Token 注册端点 // 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用 // -// GET /register/checktoken → 返回当前 token(尚无则自动获取) -// POST /register/checktoken → 强制刷新 token -// GET /register/checktoken?refresh=1 → 强制刷新 +// GET /register/checktoken/v3 → 实时获取新 token(不缓存) +// POST /register/checktoken/v3 → 实时获取新 token +// +// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token, +// 避免反作弊 token 复用触发风控。 // const { default: axios } = require('axios') const { APP_CONF } = require('../util/config.json') const URL = APP_CONF.dunDomainV3 + '/v3/b?pn=YD00000558929251' -let _token = '' async function fetch() { const res = await axios.get(URL, { timeout: 10000 }) @@ -19,13 +20,13 @@ async function fetch() { throw new Error('易盾返回异常: ' + body.substring(0, 100)) } -// 端点处理 -module.exports = async (query) => { - const refresh = query.refresh === '1' || query.refresh === 'true' - let token = refresh ? null : _token - if (!token) { +// 端点处理:每次实时获取新 token +module.exports = async () => { + let token = '' + try { token = await fetch() - _token = token + } catch (e) { + // token 获取失败时返回空,由调用方决定是否重试 } return { status: 200, @@ -33,5 +34,11 @@ module.exports = async (query) => { } } -// 给 request.js 读取用 -module.exports.getToken = () => _token +// 给 request.js 读取用:每次调用实时获取新 token,不缓存 +module.exports.getToken = async () => { + try { + return await fetch() + } catch (e) { + return '' + } +} diff --git a/module/register_xeapikey.js b/module/register_xeapikey.js index 9372f35..48e0953 100644 --- a/module/register_xeapikey.js +++ b/module/register_xeapikey.js @@ -17,7 +17,7 @@ module.exports = async (query, request) => { const currentKeyVersion = query.currentKeyVersion || '' const data = { - appVersion: '9.1.65', + appVersion: '9.5.61', currentKeyVersion, deviceId, nonce, @@ -35,7 +35,7 @@ module.exports = async (query, request) => { url: APP_CONF.apiDomain + '/api/gorilla/anti/crawler/security/key/get', headers: { 'User-Agent': - 'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)', + 'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)', Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '', }, data: new URLSearchParams(data).toString(), diff --git a/module/yunbei_sign.js b/module/yunbei_sign.js index 74dd462..c05d405 100644 --- a/module/yunbei_sign.js +++ b/module/yunbei_sign.js @@ -1,5 +1,9 @@ const createOption = require('../util/option.js') module.exports = (query, request) => { const data = {} - return request(`/api/pointmall/user/sign`, data, createOption(query, 'weapi')) + return request( + `/api/pointmall/user/sign`, + data, + createOption(query, 'xeapi', 'v3'), + ) } diff --git a/module/yunbei_task_finish_v1.js b/module/yunbei_task_finish_v1.js new file mode 100644 index 0000000..ff98bab --- /dev/null +++ b/module/yunbei_task_finish_v1.js @@ -0,0 +1,26 @@ +// 云贝广告任务 - 完成任务领取云贝 +// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js +// POST /api/ad/power/yunbei/distribution/create +// 参数: yunbeiAmount (单次可得云贝, 客户端从 list 接口的 singleAmount 取值, 当前为 150) +// 返回: true (领取成功) +// +// 实测验证 (2026-08-05): +// - 仅传 yunbeiAmount 即可成功领取, 无需真实听歌/看视频 +// - 单日上限 10 次 × 150 云贝 = 1500 云贝/天 +// - 超限返回 code:400 "单日完成任务数已达上限" +// - 无频率限制, 800ms 间隔连续调用均成功 +// +// 建议: 领取前先调 yunbei_ad_task_list 查询 times, 达到 10 次即停止 + +const createOption = require('../util/option.js') + +module.exports = (query, request) => { + const data = { + yunbeiAmount: query.yunbeiAmount || 150, + } + return request( + `/api/ad/power/yunbei/distribution/create`, + data, + createOption(query, 'weapi'), + ) +} diff --git a/module/yunbei_task_list_v1.js b/module/yunbei_task_list_v1.js new file mode 100644 index 0000000..18c2ae1 --- /dev/null +++ b/module/yunbei_task_list_v1.js @@ -0,0 +1,16 @@ +// 云贝广告任务 - 查询今日任务状态 +// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js +// GET /api/ad/power/yunbei/distribution/list +// 返回: { times: 今日已完成次数, amount: 今日累计云贝, singleAmount: 单次可得云贝 } +// 注意: 单日上限 10 次, 单次 150 云贝 (每天最多 1500) + +const createOption = require('../util/option.js') + +module.exports = (query, request) => { + const data = {} + return request( + `/api/ad/power/yunbei/distribution/list`, + data, + createOption(query, 'weapi'), + ) +} diff --git a/module/yunbei_task_recommend_song.js b/module/yunbei_task_recommend_song.js new file mode 100644 index 0000000..fc19ecb --- /dev/null +++ b/module/yunbei_task_recommend_song.js @@ -0,0 +1,20 @@ +// 云贝广告任务 - 获取推荐歌曲 +// 逆向来源: 云贝任务中心 H5 (st.music.163.com/yunbei-listen) main.js +// POST /api/ad/power/yunbei/distribution/recommend/song +// 参数: offset (默认 0), limit (默认 10, 客户端一次 10 首) +// 返回: 推荐歌曲数组 [{ songId, songName, artistName, albumUrl, songChorusStartTime, likeFlag, alg }] +// 注意: alg 均为 alg_payrec_yunBei_*, 为"听歌得云贝"任务专属推荐 + +const createOption = require('../util/option.js') + +module.exports = (query, request) => { + const data = { + offset: query.offset || 0, + limit: query.limit || 10, + } + return request( + `/api/ad/power/yunbei/distribution/recommend/song`, + data, + createOption(query, 'weapi'), + ) +} diff --git a/module_example/yunbei_ad_task.js b/module_example/yunbei_ad_task.js new file mode 100644 index 0000000..fcc5c86 --- /dev/null +++ b/module_example/yunbei_ad_task.js @@ -0,0 +1,48 @@ +const { + login_cellphone, + yunbei_ad_task_list, + yunbei_ad_task_recommend_song, + yunbei_ad_task_finish, +} = require('../main') + +// 云贝广告任务(听歌/看视频得云贝)完整流程示例 +// 1. list - 查询今日任务状态(次数/云贝) +// 2. finish - 领取云贝, 仅需传 yunbeiAmount(单次 150), 无需真实听歌/看视频 +// 3. recommend/song - 可选, 获取推荐歌曲列表 +// 注意: 单日上限 10 次 x 150 = 1500 云贝/天 + +async function main() { + const login = await login_cellphone({ + phone: '手机号', + password: '密码', + }) + const cookie = login.body.cookie + + // 1. 查询今日任务状态 + const list = await yunbei_ad_task_list({ cookie }) + const { times, amount, singleAmount } = list.body + console.log( + `今日已完成 ${times} 次, 累计 ${amount} 云贝, 单次可得 ${singleAmount} 云贝`, + ) + if (times >= 10) { + console.log('已达单日上限, 明天再来吧') + return + } + + // 2. 领取云贝(单次 150) + const finish = await yunbei_ad_task_finish({ + yunbeiAmount: singleAmount, + cookie, + }) + console.log('领取结果:', finish.body) + + // 3. 获取推荐歌曲(听歌任务专属推荐, 可选) + const rcmd = await yunbei_ad_task_recommend_song({ + offset: 0, + limit: 10, + cookie, + }) + console.log('推荐歌曲:', rcmd.body.map((s) => s.songName).join(', ')) +} + +main() diff --git a/public/docs/home.md b/public/docs/home.md index b01689c..f8dd23a 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -88,7 +88,6 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr 5. 直接点`Continue` 6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可 - ## 腾讯云 serverless 部署 因 `Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法 @@ -102,7 +101,6 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr 5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目 6. 启动文件填入: - ``` #!/bin/bash export PORT=9000 @@ -115,7 +113,6 @@ export PORT=9000 - 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费 - 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的 - ## 可以使用代理 在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy @@ -183,7 +180,6 @@ request 相关的环境变量 5. no_proxy 6. NO_PROXY - ```shell docker pull moefurina/ncm-api @@ -218,7 +214,6 @@ $ sudo docker run -d -p 3000:3000 netease-music-api - 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持 - 目前支持算法 有 `weapi`, `eapi`, `linuxapi` 和 `xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数) - ## 接口文档 ### 调用前须知 @@ -1264,7 +1259,6 @@ tags: 歌单标签 > 如果你设置 limit=50&offset=100,你就会得到第 101-150 首歌曲 - ### 歌单详情动态 说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数 @@ -1551,7 +1545,6 @@ tags: 歌单标签 - (可能存在)JSON 歌曲元数据 - ``` {"t":0,"c":[{"tx":"作曲: "},{"tx":"柳重言","li":"http://p1.music.126.net/Icj0IcaOjH2ZZpyAM-QGoQ==/6665239487822533.jpg","or":"orpheus://nm/artist/home?id=228547&type=artist"}]} {"t":5403,"c":[{"tx":"编曲: "},{"tx":"Alex San","li":"http://p1.music.126.net/pSbvYkrzZ1RFKqoh-fA9AQ==/109951166352922615.jpg","or":"orpheus://nm/artist/home?id=28984845&type=artist"}]} @@ -1568,7 +1561,6 @@ tags: 歌单标签 * 逐字歌词 - ``` [16210,3460](16210,670,0)还(16880,410,0)没... ~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...) @@ -2954,7 +2946,6 @@ type : 地区 - 适合 Vercel、Netlify 等有请求体限制的平台 - 需要前端配合实现 - #### 客户端直传相关接口 **获取上传凭证** @@ -3001,7 +2992,6 @@ type : 地区 - `artist`: 艺术家 - `album`: 专辑名 - #### 客户端直传流程 1. 客户端计算文件 MD5 @@ -3009,7 +2999,6 @@ type : 地区 3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl` 4. 调用 `/cloud/upload/complete` 完成导入 - ### 云盘歌曲信息匹配纠正 说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0 @@ -3584,6 +3573,12 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009` 说明 : 登录后调用此接口可进行云贝签到 +> ⚠️ 逆向 v9.5.61 实测:该接口已被网易云下线。v9.5.61 客户端 dex 中已无 +> `/api/pointmall/user/sign` 的任何调用(云贝中心已迁移为 RN 页面 `rn-cloudshell-center`), +> 服务端对该接口恒返回 `{"code":200,"data":{"sign":false,"yunbeiNum":0,"yunbeiAdInfo":null}}`, +> 即只返回「未签到」查询状态,不再执行签到、不会发放云贝。 +> v9.5.61 的云贝获取已迁移到广告任务体系,见下方 `/yunbei/ad/task/*` 接口(每天 10 次 × 150 = 1500 云贝)。 + **接口地址 :** `/yunbei/sign` **调用例子 :** `/yunbei/sign` @@ -3626,6 +3621,36 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009` **调用例子 :** `/yunbei/task/finish?userTaskId=5146243240&depositCode=0` +### 云贝广告任务 - 今日任务状态 + +说明 :登录后调用此接口可查询云贝广告任务("听歌/看视频得云贝")今日状态。逆向自云贝任务中心 H5 页面(st.music.163.com/yunbei-listen)。返回 `times`(今日已完成次数)、`amount`(今日累计云贝)、`singleAmount`(单次可得云贝)。单日上限 10 次。 + +**接口地址 :** `/yunbei/task/list/v1` + +**调用例子 :** `/yunbei/task/list/v1` + +### 云贝广告任务 - 获取推荐歌曲 + +说明 :登录后调用此接口可获取云贝广告任务的推荐歌曲列表。返回数组项含 `songId`、`songName`、`artistName`、`albumUrl`、`songChorusStartTime`、`likeFlag`、`alg`(均为 `alg_payrec_yunBei_*`)。 + +**可选参数 :** `offset`: 偏移数量,默认为 0 + +`limit`: 取出数量,默认为 10(客户端每次固定取 10 首) + +**接口地址 :** `/yunbei/task/recommend/song` + +**调用例子 :** `/yunbei/task/recommend/song` `/yunbei/task/recommend/song?offset=0&limit=10` + +### 云贝广告任务 - 完成任务领取云贝 + +说明 :登录后调用此接口可完成任务并领取云贝。实测仅需传 `yunbeiAmount`(单次云贝数,客户端从 `list` 接口的 `singleAmount` 取值,当前为 150)即可成功领取,无需真实听歌/看视频。单日上限 10 次 × 150 = 1500 云贝/天,超限返回 `code:400 "单日完成任务数已达上限"`。建议领取前先调用 `/yunbei/task/list/v1` 查询今日剩余次数。 + +**可选参数 :** `yunbeiAmount`: 单次云贝数,默认为 150 + +**接口地址 :** `/yunbei/task/finish/v1` + +**调用例子 :** `/yunbei/task/finish/v1?yunbeiAmount=150` + ### 云贝收入 说明 :登录后调用此接口可获取云贝收入 @@ -4316,7 +4341,6 @@ ONLINE 已发布 - `voiceFeeType: 0`:返回免费的声音 - `voiceFeeType: 1`:返回收费的声音 - ### 播客声音详情 说明: 获取播客里的声音详情 @@ -5194,7 +5218,6 @@ let data = encodeURIComponent( **调用例子:** `/broadcast/sub?id=5&t=1` - ### 用户的创建歌单列表 说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表 @@ -5269,7 +5292,6 @@ let data = encodeURIComponent( **调用例子 :** `/voicelist/my/created` - ### DIFM电台 - 分类 说明: 调用此接口, 获取DIFM电台分类 @@ -5448,7 +5470,7 @@ let data = encodeURIComponent( **接口地址 :** `/comment/report` -**调用例子 :* `/comment/report?id=2058263032&cid=123456789&reason=人身攻击` +*_调用例子 :_ `/comment/report?id=2058263032&cid=123456789&reason=人身攻击` ### 多级行政区划数据 @@ -5538,13 +5560,23 @@ let data = encodeURIComponent( **调用例子 :** `/ad/get` -### 获取30分钟免费听歌时长 +### 看广告领取权益(免费听歌时长 / 云贝等) -说明 : 登录后调用此接口, 获取30分钟免费听歌时长 +说明 : 登录后调用此接口, 领取广告权益。权益类型由广告平台下发的配置决定, 不仅限于 30 分钟免费听歌时长, 还包括"看视频获得最高 2000 云贝"等拉新分段权益(`rightsGainMethod=6`)。除下方常用参数外, 权益类型/时长/扩展权益等其余字段会自动从广告下发配置补齐, 无需手动传入。 !> 警告: 通过调取接口出现的任何问题由调用者自行承担 -**可选参数 :** `reqUid` 通过`/ad/get` 获取的广告ID +**可选参数 :** + +`reqUid` : 广告请求 ID, 通过 `/ad/get` 获取, 未传时自动获取 + +`uid` : 当前登录用户 ID, 不传时服务端从 Cookie 识别 + +`rightsGainMethod` : 权益领取方式, `1`: 曝光, `2`: 曝光+点击(默认), `3`: 曝光+下载, `4`: 点击+停留, `5`: 曝光或点击停留, `6`: 拉新曝光/下载分段权益(看视频得云贝) + +`type_ids` : 广告位类型, 默认 `["400002_0"]` + +`creativeType` : 广告创意类型, 激励视频场景为 `36`, 默认 `36` **接口地址 :** `/ad/listening/rights/gain` diff --git a/public/index.html b/public/index.html index 20752e7..e7fc033 100644 --- a/public/index.html +++ b/public/index.html @@ -99,6 +99,7 @@ curl -s {origin}/search?keywords=网易云 听歌打卡示例 · 获取 CheckToken · 免费听权益 · + 云贝广告任务 · 云小编任务中心 · 云小编每日抽奖 diff --git a/public/yunbei_task.html b/public/yunbei_task.html new file mode 100644 index 0000000..a1d02fd --- /dev/null +++ b/public/yunbei_task.html @@ -0,0 +1,622 @@ + + + + + + + 云贝广告任务 — 网易云音乐 API Enhanced + + + +
+ + + +
+
+
1
+
+
+

查询今日任务状态

+ 未开始 +
+

+ 调用 + /yunbei/task/list/v1,查看今日已完成次数、累计云贝与单次可得云贝 +

+
+ +
+ + +
(尚未执行)
+
+
+
+ + +
+
+
2
+
+
+

领取云贝

+ 未开始 +
+

+ 调用 /yunbei/task/finish/v1 完成任务并领取云贝。实测仅需传 + yunbeiAmount(单次 150),无需真实听歌/看视频。单日上限 10 次 × + 150 = 1500 云贝/天。 +

+
+ + +
+ +
(尚未执行)
+
+
+
+ + +
+
+
3
+
+
+

获取推荐歌曲

+ 未开始 +
+

+ 调用 + /yunbei/task/recommend/song,获取「听歌得云贝」任务专属推荐(alg + 均为 alg_payrec_yunBei_*) +

+
+ +
+ +
(尚未执行)
+
+
+
+ + +
+
+

一键运行

+ 就绪 +
+

+ 查询今日状态 → 领取一次云贝 → 获取推荐歌曲 +

+
+ + +
+
+ + +
+ + + + diff --git a/util/request.js b/util/request.js index a009cdc..1d2f505 100644 --- a/util/request.js +++ b/util/request.js @@ -17,7 +17,7 @@ const { generateRandomChineseIP, } = require('./index') const { URLSearchParams, URL } = require('url') -const { APP_CONF } = require('../util/config.json') +const { APP_CONF } = require('./config.json') const { getToken: antiCheatTokenV2, } = require('../module/register_checktoken_v2') @@ -114,7 +114,7 @@ const userAgentMap = { api: { pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.1.29.205117', android: - 'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)', + 'NeteaseMusic/9.5.61.260802021928(9005061);Dalvik/2.1.0 (Linux; U; Android 12; HBN-AL00 Build/cd737a2.0)', iphone: 'NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)', }, } @@ -187,14 +187,16 @@ const generateRequestId = () => { .padStart(4, '0')}` } -const createRequest = (uri, data, options) => { +const createRequest = async (uri, data, options) => { let token = '' switch (options.checkToken) { case 'v2': - token = antiCheatTokenV2() + // 每次实时获取反作弊 token,不缓存 + token = await antiCheatTokenV2() break case 'v3': - token = antiCheatTokenV3() + // 每次实时获取反作弊 token,不缓存 + token = await antiCheatTokenV3() break } From aaa6459e55d60dbee6b9c562a16587d8036d67ed Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Wed, 5 Aug 2026 18:13:14 +0800 Subject: [PATCH 2/7] remove disclaimer --- public/docs/home.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/public/docs/home.md b/public/docs/home.md index f8dd23a..2dbd794 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -3573,12 +3573,6 @@ type='1009' 获取其 id, 如`/search?keywords= 代码时间 &type=1009` 说明 : 登录后调用此接口可进行云贝签到 -> ⚠️ 逆向 v9.5.61 实测:该接口已被网易云下线。v9.5.61 客户端 dex 中已无 -> `/api/pointmall/user/sign` 的任何调用(云贝中心已迁移为 RN 页面 `rn-cloudshell-center`), -> 服务端对该接口恒返回 `{"code":200,"data":{"sign":false,"yunbeiNum":0,"yunbeiAdInfo":null}}`, -> 即只返回「未签到」查询状态,不再执行签到、不会发放云贝。 -> v9.5.61 的云贝获取已迁移到广告任务体系,见下方 `/yunbei/ad/task/*` 接口(每天 10 次 × 150 = 1500 云贝)。 - **接口地址 :** `/yunbei/sign` **调用例子 :** `/yunbei/sign` From 5859944563803b4eacf2cfd76129e879ccb6b057 Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Wed, 5 Aug 2026 18:31:49 +0800 Subject: [PATCH 3/7] format docs --- public/docs/home.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/public/docs/home.md b/public/docs/home.md index 2dbd794..13b0fb6 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -88,6 +88,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr 5. 直接点`Continue` 6. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可 + ## 腾讯云 serverless 部署 因 `Vercel` 在国内访问太慢(不绑定自己的域名的情况下),在此提供腾讯云 serverless 部署方法 @@ -101,6 +102,7 @@ v4.29.9 加入了生成随机中国 IP 功能, 在请求时加上 `randomCNIP=tr 5. 输入`应用名`,上传方式选择`代码仓库`,进行 GitHub 授权(如已授权可跳过这一步),代码仓库选择刚刚 fork 的项目 6. 启动文件填入: + ``` #!/bin/bash export PORT=9000 @@ -113,6 +115,7 @@ export PORT=9000 - 腾讯云 serverless 并不是免费的,前三个月有免费额度,之后收费 - 当前(2024-08-24), 用此法创建的话, 会`默认`关联一个"日志服务-日志主题"(创建过程中没有提醒), 此服务是计量收费的 + ## 可以使用代理 在 query 参数中加上 proxy=your-proxy 即可让这一次的请求使用 proxy @@ -180,6 +183,7 @@ request 相关的环境变量 5. no_proxy 6. NO_PROXY + ```shell docker pull moefurina/ncm-api @@ -214,6 +218,7 @@ $ sudo docker run -d -p 3000:3000 netease-music-api - 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持 - 目前支持算法 有 `weapi`, `eapi`, `linuxapi` 和 `xeapi` (xeapi 是一种不加密的特殊算法, 主要用于调试加密前的原始请求参数) + ## 接口文档 ### 调用前须知 @@ -1545,6 +1550,7 @@ tags: 歌单标签 - (可能存在)JSON 歌曲元数据 + ``` {"t":0,"c":[{"tx":"作曲: "},{"tx":"柳重言","li":"http://p1.music.126.net/Icj0IcaOjH2ZZpyAM-QGoQ==/6665239487822533.jpg","or":"orpheus://nm/artist/home?id=228547&type=artist"}]} {"t":5403,"c":[{"tx":"编曲: "},{"tx":"Alex San","li":"http://p1.music.126.net/pSbvYkrzZ1RFKqoh-fA9AQ==/109951166352922615.jpg","or":"orpheus://nm/artist/home?id=28984845&type=artist"}]} @@ -1561,6 +1567,7 @@ tags: 歌单标签 * 逐字歌词 + ``` [16210,3460](16210,670,0)还(16880,410,0)没... ~~~~1 ~~~2 ~~~~3 ~~4 5 ~6 (...) @@ -2946,6 +2953,7 @@ type : 地区 - 适合 Vercel、Netlify 等有请求体限制的平台 - 需要前端配合实现 + #### 客户端直传相关接口 **获取上传凭证** @@ -2992,6 +3000,7 @@ type : 地区 - `artist`: 艺术家 - `album`: 专辑名 + #### 客户端直传流程 1. 客户端计算文件 MD5 @@ -2999,6 +3008,7 @@ type : 地区 3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl` 4. 调用 `/cloud/upload/complete` 完成导入 + ### 云盘歌曲信息匹配纠正 说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0 @@ -4335,6 +4345,7 @@ ONLINE 已发布 - `voiceFeeType: 0`:返回免费的声音 - `voiceFeeType: 1`:返回收费的声音 + ### 播客声音详情 说明: 获取播客里的声音详情 From fc9f41839c64e8cb793d85ecfc554a900add6909 Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Wed, 5 Aug 2026 19:44:08 +0800 Subject: [PATCH 4/7] ci: test ci with workflow file --- .github/workflows/issue-manage.yml | 63 +++++-- .github/workflows/opencode.yml | 5 +- .github/workflows/reviewer.yml | 50 ++++++ package.json | 14 +- pnpm-lock.yaml | 270 ++++++++++++++--------------- 5 files changed, 241 insertions(+), 161 deletions(-) create mode 100644 .github/workflows/reviewer.yml diff --git a/.github/workflows/issue-manage.yml b/.github/workflows/issue-manage.yml index 653d120..6bae7ff 100644 --- a/.github/workflows/issue-manage.yml +++ b/.github/workflows/issue-manage.yml @@ -28,29 +28,60 @@ jobs: - 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk) - 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题 - handle-help-wanted: - if: contains(github.event.issue.labels.*.name, 'question') + triage: runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write steps: - - name: Add help resources - uses: peter-evans/create-or-update-comment@v5 + - name: Check account age + id: check + uses: actions/github-script@v7 with: - issue-number: ${{ github.event.issue.number }} - body: | - 🆘 需要帮助? + script: | + const user = await github.rest.users.getByUsername({ + username: context.payload.issue.user.login + }); + const created = new Date(user.data.created_at); + const days = (Date.now() - created) / (1000 * 60 * 60 * 24); + return days >= 30; + result-encoding: string - 这里有一些可能对您有用的资源: + - name: Generate bot token + id: token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} - 📚 **文档资源** - - [项目文档](https://neteasecloudmusicapienhanced.js.org) + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + token: ${{ steps.token.outputs.token }} - 🔍 **常见问题** - - 搜索[已关闭的issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues?q=is%3Aissue+is%3Aclosed) 看看是否有类似的问题已经被解答。 + - name: Configure git identity + run: | + git config --global user.name "takanashi-hoshino-agent[bot]" + git config --global user.email "takanashi-hoshino-agent[bot]@users.noreply.github.com" - 💬 **即时帮助** - - 加入QQ群:https://qm.qq.com/q/TpeP9Uv2yk - - 如果以上资源无法解决您的问题,请提供更多详细信息,我们会尽快为您解答! + - name: Run opencode + uses: anomalyco/opencode/github@latest + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GITHUB_TOKEN: ${{ steps.token.outputs.token }} + with: + model: opencode/deepseek-v4-flash-free + use_github_token: true + prompt: | + Review this issue. If there's a clear fix or relevant docs: + - Provide documentation links + - Add error handling guidance for code examples + Otherwise, do not comment. + ${{ secrets.OPENCODE_PROMPT }} stale-issues: if: github.event.action == 'opened' diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index c682552..5d289db 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -5,8 +5,6 @@ on: types: [created] pull_request_review_comment: types: [created] - issues: - types: [opened] permissions: contents: write @@ -51,4 +49,5 @@ jobs: with: model: opencode/deepseek-v4-flash-free use_github_token: true - prompt: ${{ secrets.OPENCODE_PROMPT }} \ No newline at end of file + prompt: | + ${{ secrets.OPENCODE_PROMPT }} \ No newline at end of file diff --git a/.github/workflows/reviewer.yml b/.github/workflows/reviewer.yml new file mode 100644 index 0000000..f6818eb --- /dev/null +++ b/.github/workflows/reviewer.yml @@ -0,0 +1,50 @@ +name: PR Reviewer + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: read + issues: read + steps: + - name: Generate bot token + id: token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + token: ${{ steps.token.outputs.token }} + + - name: Configure git identity + run: | + git config --global user.name "takanashi-hoshino-agent[bot]" + git config --global user.email "takanashi-hoshino-agent[bot]@users.noreply.github.com" + + - name: Run opencode + uses: anomalyco/opencode/github@latest + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GITHUB_TOKEN: ${{ steps.token.outputs.token }} + with: + model: opencode/deepseek-v4-flash-free + use_github_token: true + prompt: | + ${{ secrets.PR_REVIEW_PROMPT }} + ${{ secrets.OPENCODE_PROMPT }} \ No newline at end of file diff --git a/package.json b/package.json index 3657dd3..6ee638a 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,8 @@ "data" ], "dependencies": { - "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.3.4", - "axios": "^1.18.1", + "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.4.0", + "axios": "^1.19.0", "crypto-js": "^4.2.0", "dotenv": "^17.4.2", "express": "^5.2.1", @@ -89,7 +89,7 @@ "safe-decode-uri-component": "^1.2.1", "tunnel": "^0.0.6", "xml2js": "^0.6.2", - "yargs": "^18.0.0" + "yargs": "^18.1.0" }, "devDependencies": { "@eslint/eslintrc": "^3.3.6", @@ -98,17 +98,17 @@ "@types/express-fileupload": "^1.5.1", "@types/mocha": "^10.0.10", "@types/node": "25.9.2", - "@typescript-eslint/eslint-plugin": "^8.65.0", - "@typescript-eslint/parser": "^8.65.0", + "@typescript-eslint/eslint-plugin": "^8.66.0", + "@typescript-eslint/parser": "^8.66.0", "eslint": "^9.39.5", "eslint-config-prettier": "^10.1.8", "eslint-plugin-html": "^8.1.4", "eslint-plugin-prettier": "^5.5.6", - "globals": "^17.7.0", + "globals": "^17.9.0", "husky": "^9.1.7", "intelli-espower-loader": "^1.1.0", "lint-staged": "^16.4.0", - "mocha": "^11.7.6", + "mocha": "^11.8.0", "nodemon": "^3.1.14", "pkg": "^5.8.1", "power-assert": "^1.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f6b142..2a49b25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: dependencies: '@neteasecloudmusicapienhanced/unblockmusic-utils': - specifier: ^0.3.4 - version: 0.3.4 + specifier: ^0.4.0 + version: 0.4.0 axios: - specifier: ^1.18.1 - version: 1.18.1 + specifier: ^1.19.0 + version: 1.19.0 crypto-js: specifier: ^4.2.0 version: 4.2.0 @@ -51,8 +51,8 @@ importers: specifier: ^0.6.2 version: 0.6.2 yargs: - specifier: ^18.0.0 - version: 18.0.0 + specifier: ^18.1.0 + version: 18.1.0 devDependencies: '@eslint/eslintrc': specifier: ^3.3.6 @@ -73,11 +73,11 @@ importers: specifier: 25.9.2 version: 25.9.2 '@typescript-eslint/eslint-plugin': - specifier: ^8.65.0 - version: 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3) + specifier: ^8.66.0 + version: 8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^8.65.0 - version: 8.65.0(eslint@9.39.5)(typescript@5.9.3) + specifier: ^8.66.0 + version: 8.66.0(eslint@9.39.5)(typescript@5.9.3) eslint: specifier: ^9.39.5 version: 9.39.5 @@ -91,8 +91,8 @@ importers: specifier: ^5.5.6 version: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5))(eslint@9.39.5)(prettier@3.9.6) globals: - specifier: ^17.7.0 - version: 17.7.0 + specifier: ^17.9.0 + version: 17.9.0 husky: specifier: ^9.1.7 version: 9.1.7 @@ -103,8 +103,8 @@ importers: specifier: ^16.4.0 version: 16.4.0 mocha: - specifier: ^11.7.6 - version: 11.7.6 + specifier: ^11.8.0 + version: 11.8.0 nodemon: specifier: ^3.1.14 version: 3.1.14 @@ -222,8 +222,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@neteasecloudmusicapienhanced/unblockmusic-utils@0.3.4': - resolution: {integrity: sha512-3Lu/eYnDqf9/rHxa5G38TZxM7QVUBZpzKMWBmT6STHaMb6ztFRfrVrejIC1r7IUvtCWkoRulD+8UlllNTEsd6g==} + '@neteasecloudmusicapienhanced/unblockmusic-utils@0.4.0': + resolution: {integrity: sha512-KUDdBEe3IK4tPSt2UHdFweYbhoZUYGBxbt8FuqQu5bZdpymfpecZxLf+a8G/JvIYHph4H+KxYlq6+MD9oXCfjg==} hasBin: true '@nodelib/fs.scandir@2.1.5': @@ -271,8 +271,8 @@ packages: '@types/express-fileupload@1.5.1': resolution: {integrity: sha512-DllImBVI1lCyjl2klky/TEwk60mbNebgXv1669h66g9TfptWSrEFq5a/raHSutaFzjSm1tmn9ypdNfu4jPSixQ==} - '@types/express-serve-static-core@5.1.2': - resolution: {integrity: sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==} + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} @@ -301,63 +301,63 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + '@typescript-eslint/eslint-plugin@8.66.0': + resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.65.0 + '@typescript-eslint/parser': ^8.66.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.65.0': - resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + '@typescript-eslint/parser@8.66.0': + resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.65.0': - resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + '@typescript-eslint/project-service@8.66.0': + resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.65.0': - resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + '@typescript-eslint/scope-manager@8.66.0': + resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + '@typescript-eslint/tsconfig-utils@8.66.0': + resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.65.0': - resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + '@typescript-eslint/type-utils@8.66.0': + resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.65.0': - resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + '@typescript-eslint/types@8.66.0': + resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.65.0': - resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + '@typescript-eslint/typescript-estree@8.66.0': + resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.65.0': - resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + '@typescript-eslint/utils@8.66.0': + resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.65.0': - resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + '@typescript-eslint/visitor-keys@8.66.0': + resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unblockneteasemusic/server@0.28.0': @@ -386,8 +386,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -485,8 +485,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.18.1: - resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + axios@1.19.0: + resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -517,14 +517,14 @@ packages: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} - brace-expansion@1.1.16: - resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} - brace-expansion@2.1.2: - resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} braces@3.0.3: @@ -1153,8 +1153,8 @@ packages: flatstr@1.0.12: resolution: {integrity: sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==} - flatted@3.4.3: - resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} @@ -1262,8 +1262,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@17.7.0: - resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + globals@17.9.0: + resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} globalthis@1.0.4: @@ -1400,8 +1400,8 @@ packages: resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==} engines: {node: '>=10'} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + ip-address@10.4.0: + resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -1565,8 +1565,8 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true jsesc@2.5.2: @@ -1699,8 +1699,8 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -1720,8 +1720,8 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mocha@11.7.6: - resolution: {integrity: sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==} + mocha@11.8.0: + resolution: {integrity: sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true @@ -2158,8 +2158,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.6.0: - resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} engines: {node: '>=11.0.0'} secure-json-parse@2.7.0: @@ -2400,8 +2400,8 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} tinyglobby@0.2.17: @@ -2655,8 +2655,8 @@ packages: resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} - yargs@18.0.0: - resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} yocto-queue@0.1.0: @@ -2718,7 +2718,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -2772,10 +2772,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@neteasecloudmusicapienhanced/unblockmusic-utils@0.3.4': + '@neteasecloudmusicapienhanced/unblockmusic-utils@0.4.0': dependencies: '@unblockneteasemusic/server': 0.28.0 - axios: 1.18.1 + axios: 1.19.0 dotenv: 17.4.2 express: 4.22.2 https: 1.0.0 @@ -2831,7 +2831,7 @@ snapshots: '@types/busboy': 1.5.4 '@types/express': 5.0.6 - '@types/express-serve-static-core@5.1.2': + '@types/express-serve-static-core@5.1.3': dependencies: '@types/node': 25.9.2 '@types/qs': 6.15.1 @@ -2841,7 +2841,7 @@ snapshots: '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.2 + '@types/express-serve-static-core': 5.1.3 '@types/serve-static': 2.2.0 '@types/http-errors@2.0.5': {} @@ -2867,14 +2867,14 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 25.9.2 - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5)(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/parser': 8.66.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/type-utils': 8.66.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/utils': 8.66.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.66.0 eslint: 9.39.5 ignore: 7.0.6 natural-compare: 1.4.0 @@ -2883,41 +2883,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.9.3)': + '@typescript-eslint/parser@8.66.0(eslint@9.39.5)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3 eslint: 9.39.5 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.66.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@5.9.3) + '@typescript-eslint/types': 8.66.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.65.0': + '@typescript-eslint/scope-manager@8.66.0': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 - '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.66.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.66.0(eslint@9.39.5)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.66.0(eslint@9.39.5)(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.5 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -2925,16 +2925,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.65.0': {} + '@typescript-eslint/types@8.66.0': {} - '@typescript-eslint/typescript-estree@8.65.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.66.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.65.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/project-service': 8.66.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@5.9.3) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -2942,20 +2942,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(eslint@9.39.5)(typescript@5.9.3)': + '@typescript-eslint/utils@8.66.0(eslint@9.39.5)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@5.9.3) eslint: 9.39.5 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.65.0': + '@typescript-eslint/visitor-keys@8.66.0': dependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 '@unblockneteasemusic/server@0.28.0': @@ -2976,13 +2976,13 @@ snapshots: acorn-es7-plugin@1.1.7: {} - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 acorn@5.7.4: {} - acorn@8.17.0: {} + acorn@8.18.0: {} agent-base@6.0.2: dependencies: @@ -3072,7 +3072,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.18.1: + axios@1.19.0: dependencies: follow-redirects: 1.16.0 form-data: 4.0.6 @@ -3129,16 +3129,16 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@1.1.16: + brace-expansion@1.1.18: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.1.2: + brace-expansion@2.1.4: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.8: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -3765,8 +3765,8 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 4.2.1 esprima@2.7.3: {} @@ -3959,14 +3959,14 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.3 + flatted: 3.4.4 keyv: 4.5.4 flat@5.0.2: {} flatstr@1.0.12: {} - flatted@3.4.3: {} + flatted@3.4.4: {} follow-redirects@1.16.0: {} @@ -4085,7 +4085,7 @@ snapshots: globals@14.0.0: {} - globals@17.7.0: {} + globals@17.9.0: {} globalthis@1.0.4: dependencies: @@ -4219,7 +4219,7 @@ snapshots: from2: 2.3.0 p-is-promise: 3.0.0 - ip-address@10.2.0: {} + ip-address@10.4.0: {} ipaddr.js@1.9.1: {} @@ -4378,7 +4378,7 @@ snapshots: joycon@3.1.1: {} - js-yaml@4.3.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -4418,7 +4418,7 @@ snapshots: listr2: 9.0.5 picomatch: 4.0.5 string-argv: 0.3.2 - tinyexec: 1.2.4 + tinyexec: 1.3.0 yaml: 2.9.0 listr2@9.0.5: @@ -4498,17 +4498,17 @@ snapshots: mimic-response@3.1.0: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.16 + brace-expansion: 1.1.18 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.2 + brace-expansion: 2.1.4 minimist@1.2.8: {} @@ -4516,7 +4516,7 @@ snapshots: mkdirp-classic@0.5.3: {} - mocha@11.7.6: + mocha@11.8.0: dependencies: browser-stdout: 1.3.1 chokidar: 4.0.3 @@ -4527,7 +4527,7 @@ snapshots: glob: 10.5.0 he: 1.2.0 is-path-inside: 3.0.3 - js-yaml: 4.3.0 + js-yaml: 4.3.1 log-symbols: 4.1.0 minimatch: 9.0.9 ms: 2.1.3 @@ -4602,7 +4602,7 @@ snapshots: chokidar: 3.6.0 debug: 4.4.3(supports-color@5.5.0) ignore-by-default: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 pstree.remy: 1.1.8 semver: 7.8.5 simple-update-notifier: 2.0.0 @@ -5073,7 +5073,7 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.6.0: {} + sax@1.6.1: {} secure-json-parse@2.7.0: {} @@ -5233,7 +5233,7 @@ snapshots: socks@2.8.9: dependencies: - ip-address: 10.2.0 + ip-address: 10.4.0 smart-buffer: 4.2.0 sonic-boom@1.4.1: @@ -5387,7 +5387,7 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tinyexec@1.2.4: {} + tinyexec@1.3.0: {} tinyglobby@0.2.17: dependencies: @@ -5620,7 +5620,7 @@ snapshots: xml2js@0.6.2: dependencies: - sax: 1.6.0 + sax: 1.6.1 xmlbuilder: 11.0.1 xml@1.0.1: {} @@ -5687,12 +5687,12 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yargs@18.0.0: + yargs@18.1.0: dependencies: cliui: 9.0.1 escalade: 3.2.0 get-caller-file: 2.0.5 - string-width: 7.2.0 + string-width: 8.2.2 y18n: 5.0.8 yargs-parser: 22.0.0 From 5b780addbafe17d38a922a9c502e9a91f5bac86f Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Wed, 5 Aug 2026 20:44:21 +0800 Subject: [PATCH 5/7] ci: test ci with the workflow file --- .github/workflows/issue-manage.yml | 57 -------- .github/workflows/opencode-auto.yml | 59 ++++++++ .github/workflows/opencode.yml | 11 +- .github/workflows/reviewer.yml | 50 ------- scripts/opencode-automation.mjs | 204 ++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 111 deletions(-) create mode 100644 .github/workflows/opencode-auto.yml delete mode 100644 .github/workflows/reviewer.yml create mode 100644 scripts/opencode-automation.mjs diff --git a/.github/workflows/issue-manage.yml b/.github/workflows/issue-manage.yml index 6bae7ff..0467fd6 100644 --- a/.github/workflows/issue-manage.yml +++ b/.github/workflows/issue-manage.yml @@ -6,8 +6,6 @@ permissions: on: issues: types: [opened, labeled] - issue_comment: - types: [created] jobs: welcome-new-issues: @@ -28,61 +26,6 @@ jobs: - 💬 加入[QQ交流群](https://qm.qq.com/q/TpeP9Uv2yk) - 🔍 搜索[现有issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues) 看是否有类似问题 - triage: - runs-on: ubuntu-latest - permissions: - id-token: write - contents: write - pull-requests: write - issues: write - steps: - - name: Check account age - id: check - uses: actions/github-script@v7 - with: - script: | - const user = await github.rest.users.getByUsername({ - username: context.payload.issue.user.login - }); - const created = new Date(user.data.created_at); - const days = (Date.now() - created) / (1000 * 60 * 60 * 24); - return days >= 30; - result-encoding: string - - - name: Generate bot token - id: token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - token: ${{ steps.token.outputs.token }} - - - name: Configure git identity - run: | - git config --global user.name "takanashi-hoshino-agent[bot]" - git config --global user.email "takanashi-hoshino-agent[bot]@users.noreply.github.com" - - - name: Run opencode - uses: anomalyco/opencode/github@latest - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ steps.token.outputs.token }} - with: - model: opencode/deepseek-v4-flash-free - use_github_token: true - prompt: | - Review this issue. If there's a clear fix or relevant docs: - - Provide documentation links - - Add error handling guidance for code examples - Otherwise, do not comment. - ${{ secrets.OPENCODE_PROMPT }} - stale-issues: if: github.event.action == 'opened' runs-on: ubuntu-latest diff --git a/.github/workflows/opencode-auto.yml b/.github/workflows/opencode-auto.yml new file mode 100644 index 0000000..eadcf95 --- /dev/null +++ b/.github/workflows/opencode-auto.yml @@ -0,0 +1,59 @@ +name: OpenCode Automation + +on: + issues: + types: [opened] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: auto-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + automate: + # 排除机器人自己的触发,防止自我循环 + if: | + github.actor != 'takanashi-hoshino-agent[bot]' && + github.actor != 'github-actions[bot]' && + github.actor != 'opencode-agent[bot]' + runs-on: ubuntu-latest + steps: + - name: Generate bot token + id: token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Checkout + uses: actions/checkout@v6 + with: + token: ${{ steps.token.outputs.token }} + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Install opencode + run: npm i -g opencode-ai@latest + + - name: Run automation + env: + GITHUB_TOKEN: ${{ steps.token.outputs.token }} + MODEL: opencode/deepseek-v4-flash-free + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + ISSUE_PROMPT: ${{ secrets.OPENCODE_PROMPT }} + PR_PROMPT: | + ${{ secrets.OPENCODE_PROMPT }} + ${{ secrets.PR_REVIEW_PROMPT }} + run: node scripts/opencode-automation.mjs diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 5d289db..9972ac7 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -14,10 +14,13 @@ permissions: jobs: bot: if: | - contains(github.event.comment.body, ' /oc') || - startsWith(github.event.comment.body, '/oc') || - contains(github.event.comment.body, ' /opencode') || - startsWith(github.event.comment.body, '/opencode') + (contains(github.event.comment.body, ' /oc') || + startsWith(github.event.comment.body, '/oc') || + contains(github.event.comment.body, ' /opencode') || + startsWith(github.event.comment.body, '/opencode')) && + github.actor != 'opencode-agent[bot]' && + github.actor != 'takanashi-hoshino-agent[bot]' && + github.actor != 'github-actions[bot]' runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/reviewer.yml b/.github/workflows/reviewer.yml deleted file mode 100644 index f6818eb..0000000 --- a/.github/workflows/reviewer.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: PR Reviewer - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - review: - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - pull-requests: read - issues: read - steps: - - name: Generate bot token - id: token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - token: ${{ steps.token.outputs.token }} - - - name: Configure git identity - run: | - git config --global user.name "takanashi-hoshino-agent[bot]" - git config --global user.email "takanashi-hoshino-agent[bot]@users.noreply.github.com" - - - name: Run opencode - uses: anomalyco/opencode/github@latest - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ steps.token.outputs.token }} - with: - model: opencode/deepseek-v4-flash-free - use_github_token: true - prompt: | - ${{ secrets.PR_REVIEW_PROMPT }} - ${{ secrets.OPENCODE_PROMPT }} \ No newline at end of file diff --git a/scripts/opencode-automation.mjs b/scripts/opencode-automation.mjs new file mode 100644 index 0000000..654dc25 --- /dev/null +++ b/scripts/opencode-automation.mjs @@ -0,0 +1,204 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process' +import { readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const eventName = process.env.GITHUB_EVENT_NAME +const eventPath = process.env.GITHUB_EVENT_PATH +const token = process.env.GITHUB_TOKEN +const repo = process.env.GITHUB_REPOSITORY +const model = process.env.MODEL || 'opencode/deepseek-v4-flash-free' +const botLogin = process.env.BOT_LOGIN || 'takanashi-hoshino-agent[bot]' + +if (!eventPath || !token || !repo) { + console.error( + 'Missing required env: GITHUB_EVENT_PATH / GITHUB_TOKEN / GITHUB_REPOSITORY', + ) + process.exit(1) +} +if (!process.env.OPENCODE_API_KEY) { + console.error('Missing OPENCODE_API_KEY') + process.exit(1) +} + +const event = JSON.parse(readFileSync(eventPath, 'utf8')) +const [owner, repoName] = repo.split('/') +const api = `https://api.github.com/repos/${owner}/${repoName}` +const headers = { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'opencode-automation', +} + +async function get(path) { + const res = await fetch(`${api}${path}`, { headers }) + if (!res.ok) throw new Error(`GET ${path}: HTTP ${res.status}`) + return res.json() +} + +async function post(path, body) { + const res = await fetch(`${api}${path}`, { + method: 'POST', + headers, + body: JSON.stringify(body), + }) + const text = await res.text() + if (!res.ok) throw new Error(`POST ${path}: HTTP ${res.status} ${text}`) + return text ? JSON.parse(text) : null +} + +/** 无头模式跑 opencode,返回模型输出的文本 */ +function runOpencode(prompt, contextFile) { + const args = ['run', '--auto', '-m', model, '--format', 'default', prompt] + if (contextFile) args.push('-f', contextFile) + console.log( + `Running: opencode run --auto -m ${model} (prompt ${prompt.length} chars${contextFile ? ', context attached' : ''})`, + ) + const res = spawnSync('opencode', args, { + encoding: 'utf8', + maxBuffer: 128 * 1024 * 1024, + }) + if (res.error) throw res.error + if (res.status !== 0) { + if (res.stderr) console.error('opencode stderr:\n' + res.stderr) + throw new Error(`opencode exited with code ${res.status}`) + } + return (res.stdout || '').trim() +} + +/** 该 issue/PR 是否已有 bot 评论(用于去重,避免 synchronize 重复评论) */ +async function hasBotComment(target) { + const comments = await get(`/issues/${target}/comments?per_page=100`) + return comments.some((c) => c.user?.login === botLogin) +} + +async function comment(target, body) { + if (!body) { + console.log('No output from agent, skip comment') + return + } + if (body.length > 60000) body = body.slice(0, 60000) + '\n\n_...(truncated)_' + await post(`/issues/${target}/comments`, { body }) + console.log(`Commented on #${target}`) +} + +async function handleIssue(issue) { + const n = issue.number + console.log(`Handling issue #${n}`) + if (await hasBotComment(n)) { + console.log(`Already handled by bot, skip`) + return + } + + const comments = await get(`/issues/${n}/comments?per_page=100`) + const thread = comments + .filter((c) => c.user?.login !== botLogin) + .map( + (c) => + `- **${c.user?.login}** (${c.created_at}):\n ${(c.body || '').replace(/\n/g, '\n ')}`, + ) + .join('\n') + + const ctx = [ + `# Issue #${n}: ${issue.title}`, + ``, + `- State: ${issue.state}`, + `- Author: ${issue.user?.login}`, + `- Created: ${issue.created_at}`, + ``, + `## Body`, + ``, + issue.body || '(empty)', + ...(thread ? [`\n## Comments\n\n${thread}`] : []), + ].join('\n') + + const ctxFile = join(tmpdir(), 'opencode-issue-context.md') + writeFileSync(ctxFile, ctx) + + const prompt = + process.env.ISSUE_PROMPT || + [ + `You are a maintainer bot for the ${repo} repository.`, + `Read the attached file for the issue context.`, + `If the issue has a clear fix or points to relevant docs, reply with documentation links and/or`, + `error-handling guidance for code examples. If nothing actionable, reply with exactly "NO_ACTION".`, + `Do NOT modify any files, do NOT commit, do NOT push. Output only the comment text.`, + ].join('\n') + + const output = runOpencode(prompt, ctxFile) + if (output === 'NO_ACTION' || output.startsWith('NO_ACTION')) { + console.log('Agent decided no action needed') + return + } + await comment(n, output) +} + +async function handlePullRequest(pr) { + const n = pr.number + console.log(`Handling PR #${n}`) + if (await hasBotComment(n)) { + console.log(`Already handled by bot, skip`) + return + } + + let files = [] + try { + files = await get(`/pulls/${n}/files?per_page=100`) + } catch (e) { + console.warn(`Failed to fetch changed files: ${e.message}`) + } + const fileList = files + .map((f) => `- ${f.status} ${f.filename} (+${f.additions}/-${f.deletions})`) + .join('\n') + + const ctx = [ + `# PR #${n}: ${pr.title}`, + ``, + `- State: ${pr.state}`, + `- Author: ${pr.user?.login}`, + `- Base: ${pr.base?.ref} <- Head: ${pr.head?.ref}`, + `- Created: ${pr.created_at}`, + `- Stats: +${pr.additions}/-${pr.deletions}, ${pr.changed_files} files`, + ``, + `## Body`, + ``, + pr.body || '(empty)', + ...(fileList ? [`\n## Changed files\n\n${fileList}`] : []), + ].join('\n') + + const ctxFile = join(tmpdir(), 'opencode-pr-context.md') + writeFileSync(ctxFile, ctx) + + const prompt = + process.env.PR_PROMPT || + [ + `You are a maintainer bot for the ${repo} repository.`, + `Read the attached file for the PR context. The PR branch is already checked out.`, + `Run \`git diff HEAD^1 HEAD\` (or \`git diff origin/${pr.base?.ref}...HEAD\`) to see the changes.`, + `Review the code changes: point out bugs, risks and improvements, and suggest fixes for obvious issues.`, + `If the PR looks good, say so concisely.`, + `Do NOT modify any files, do NOT commit, do NOT push. Output only the review comment text.`, + ].join('\n') + + const output = runOpencode(prompt, ctxFile) + await comment(n, output) +} + +async function main() { + console.log(`Event: ${eventName} on ${repo}`) + if (eventName === 'issues') { + await handleIssue(event.issue) + } else if (eventName === 'pull_request') { + await handlePullRequest(event.pull_request) + } else { + console.log(`Unhandled event type: ${eventName}`) + } +} + +main().catch((e) => { + console.error(`Automation failed: ${e.message}`) + process.exit(1) +}) From 3357317a1d8bf9f5ccc1bce4e76a9de2eb5fa754 Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Thu, 6 Aug 2026 16:51:51 +0800 Subject: [PATCH 6/7] upd --- .github/ISSUE_TEMPLATE/feature_request.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 50dfc99..684fefa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -14,15 +14,16 @@ body: id: terms attributes: label: 确认事项 - description: 在提交功能请求前,请确认以下事项 + description: | + 在提交功能请求前,请确认以下事项 + + - 我已经搜索了现有的issues,确认这不是重复请求 + - 我已经查看了项目文档 + - 该项目符合该项目的目标和范围 + - 我确认我提供的接口请求是合理且有意义的 + options: - - label: 我已经搜索了现有的issues,确认这不是重复请求 - required: true - - label: 我已经查看了项目文档 - required: true - - label: 该项目符合该项目的目标和范围 - required: true - - label: 我确认我提供的接口请求是合理且有意义的 + - label: 我已确认以上事项 required: true From c1fe1eb925c7b06998c9a8424fd495e854c6501b Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Thu, 6 Aug 2026 16:55:27 +0800 Subject: [PATCH 7/7] Merge branch 'pr/235' --- .github/ISSUE_TEMPLATE/feature_request.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 50dfc99..684fefa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -14,15 +14,16 @@ body: id: terms attributes: label: 确认事项 - description: 在提交功能请求前,请确认以下事项 + description: | + 在提交功能请求前,请确认以下事项 + + - 我已经搜索了现有的issues,确认这不是重复请求 + - 我已经查看了项目文档 + - 该项目符合该项目的目标和范围 + - 我确认我提供的接口请求是合理且有意义的 + options: - - label: 我已经搜索了现有的issues,确认这不是重复请求 - required: true - - label: 我已经查看了项目文档 - required: true - - label: 该项目符合该项目的目标和范围 - required: true - - label: 我确认我提供的接口请求是合理且有意义的 + - label: 我已确认以上事项 required: true