From a898e1d86c55d5993a68852555861619f6aeefbe Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Wed, 5 Aug 2026 18:11:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BA=91=E8=B4=9D?= =?UTF-8?q?=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 }