mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-08-12 17:10:37 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7c0db9455 | |||
| c1d14a86d9 | |||
| a2a2a40d71 | |||
| 543660fcdf | |||
| 98a95002b2 | |||
| c4a3d83abe | |||
| 7753b5de41 | |||
| c1fe1eb925 | |||
| 3357317a1d | |||
| a08f425ae7 | |||
| 5b780addba | |||
| fc9f41839c | |||
| 5859944563 | |||
| aaa6459e55 | |||
| a898e1d86c | |||
| 4045f1ad3f | |||
| e6214a3f73 | |||
| 686e8bf969 | |||
|
|
9214cb945f | ||
| a79a2d9ef6 | |||
|
|
9608da9b96 | ||
|
|
93b3e624d8 | ||
| 15f515c1ba | |||
|
|
e255e8f334 | ||
| 16aa61f98e | |||
| a939003b92 | |||
| 8f4873f2e2 | |||
| a8b45a8b8b | |||
| 1a70281c76 | |||
|
|
f5f4ff6eda | ||
|
|
eb1b5ba0ea | ||
| 6732fc7c32 | |||
| 6ce6b84015 | |||
| 63d89aa906 | |||
| 8c212be86b | |||
| 89de56d2a2 | |||
| d298025d7c | |||
| a3287a0180 | |||
| 9adfd50c58 | |||
| fda7c9e5b3 | |||
| a8266c6d7b | |||
|
|
64a6b9677a | ||
| 20d3a1a7a5 | |||
| 4826c65042 | |||
| 7822ab08d3 | |||
| 41bd6d82ce | |||
|
|
6f8dd1d971 | ||
|
|
ddd0af5474 | ||
|
|
fbd2e06419 | ||
|
|
4442fb4341 | ||
| 35d1c61cb4 | |||
| a183e153ef | |||
| 2fa78c3826 | |||
|
|
5ae1eab14c | ||
| 321c25bd7d | |||
| f527c1ef73 |
5
.codegraph/.gitignore
vendored
Normal file
5
.codegraph/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# CodeGraph data files — local to each machine, not for committing.
|
||||
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||
*
|
||||
!.gitignore
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -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
|
||||
|
||||
|
||||
|
||||
67
.github/workflows/ci-check.yml
vendored
Normal file
67
.github/workflows/ci-check.yml
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Lint / Unit / Docs
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18, 22, 24]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: pnpm
|
||||
|
||||
# frozen-lockfile: 顺带校验 package.json 与 pnpm-lock.yaml 是否一致
|
||||
- name: Install dependencies (frozen lockfile)
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
# 纯单测,不需要网络,稳定必过
|
||||
- name: Unit tests
|
||||
run: pnpm exec mocha -r intelli-espower-loader -t 60000 main.test.js --exit
|
||||
|
||||
- name: Docs format check
|
||||
run: pnpm docs:check
|
||||
|
||||
integration:
|
||||
# server.test.js 会启动真实服务器并请求真实网易云 API,
|
||||
# 可能因上游风控/限流偶发失败,因此不阻塞合并
|
||||
name: Integration tests (real NetEase API)
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies (frozen lockfile)
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run full test suite
|
||||
run: pnpm test
|
||||
31
.github/workflows/issue-manage.yml
vendored
31
.github/workflows/issue-manage.yml
vendored
@ -6,8 +6,6 @@ permissions:
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
welcome-new-issues:
|
||||
@ -19,38 +17,15 @@ jobs:
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
## 快速链接
|
||||
感谢您在该仓库提出问题!我们非常重视您的反馈,并希望尽快为您提供帮助。
|
||||
|
||||
在我们处理您的问题之前,您可以先查看以下资源:
|
||||
如果您在一周左右的时间里没有收到我们的回复,请在下方留言,以便我们注意到。我们的大部分审核都是志愿者,有时难免会有疏漏。
|
||||
|
||||
你可能会想要尝试以下方法来获得更快的帮助:
|
||||
- 📖 查看[项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||
- 💬 加入[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')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add help resources
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
🆘 需要帮助?
|
||||
|
||||
这里有一些可能对您有用的资源:
|
||||
|
||||
📚 **文档资源**
|
||||
- [项目文档](https://neteasecloudmusicapienhanced.js.org)
|
||||
|
||||
🔍 **常见问题**
|
||||
- 搜索[已关闭的issues](https://github.com/neteasecloudmusicapienhanced/api-enhanced/issues?q=is%3Aissue+is%3Aclosed) 看看是否有类似的问题已经被解答。
|
||||
|
||||
💬 **即时帮助**
|
||||
- 加入QQ群:https://qm.qq.com/q/TpeP9Uv2yk
|
||||
|
||||
如果以上资源无法解决您的问题,请提供更多详细信息,我们会尽快为您解答!
|
||||
|
||||
stale-issues:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
59
.github/workflows/opencode-auto.yml
vendored
Normal file
59
.github/workflows/opencode-auto.yml
vendored
Normal file
@ -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
|
||||
56
.github/workflows/opencode.yml
vendored
Normal file
56
.github/workflows/opencode.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
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')) &&
|
||||
github.actor != 'opencode-agent[bot]' &&
|
||||
github.actor != 'takanashi-hoshino-agent[bot]' &&
|
||||
github.actor != 'github-actions[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: 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.OPENCODE_PROMPT }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@ anonymous_token
|
||||
.vercel
|
||||
.env
|
||||
IFLOW.md
|
||||
precompiled
|
||||
55
AGENTS.md
55
AGENTS.md
@ -1,23 +1,40 @@
|
||||
# Agent Instructions for NeteaseCloudMusicApiEnhanced
|
||||
# NeteaseCloudMusicApiEnhanced Agent 说明
|
||||
|
||||
## Quick Start
|
||||
- **Package Manager**: Use `pnpm` (not npm or yarn).
|
||||
- **Node Version**: Requires Node.js 18 or later.
|
||||
## 快速开始
|
||||
- **包管理器**:开发与 CI 使用 `pnpm`,`Dockerfile` 也用 pnpm(`pnpm@9 --frozen-lockfile`,匹配 `pnpm-lock.yaml`)。仓库没有 `yarn.lock`,不要引入 yarn。
|
||||
- **Node 版本**:README 推荐 Node 22+;`package.json` 的 `engines` 声明 `>=12`;CI/打包在 Node 18–24 上运行。现代 Node 均可。
|
||||
- **环境变量**:`server.js` 调用了 `dotenv.config()`,本地 `.env` 会被自动加载;所有支持的变量见 `.env.prod.example`。
|
||||
|
||||
## Developer Commands
|
||||
- **Install dependencies**: `pnpm i`
|
||||
- **Start server**: `pnpm start` or `node app.js`
|
||||
- **Start dev server**: `pnpm dev` (uses nodemon)
|
||||
- **Run tests**: `pnpm test` (uses Mocha)
|
||||
- **Linting**: `pnpm lint` (check) or `pnpm lint-fix` (auto-fix)
|
||||
## 常用命令
|
||||
- 安装依赖:`pnpm i`
|
||||
- 启动服务:`pnpm start`(等价 `node app.js`);热重载开发:`pnpm dev`(nodemon)
|
||||
- 跑测试:`pnpm test`(Mocha,超时 60s)
|
||||
- Lint:`pnpm lint`;自动修复:`pnpm lint-fix`
|
||||
- 文档格式化检查/修复:`pnpm docs:check` / `pnpm docs:format`
|
||||
- 打包独立二进制:`pnpm pkgwin` / `pkglinux` / `pkgmacos`
|
||||
|
||||
## Architecture & Entrypoints
|
||||
- **Executable Server**: `app.js` is the main entrypoint for running the API server.
|
||||
- **Module Exports**: `main.js` is the entrypoint when the project is imported as a Node.js dependency.
|
||||
- **API Endpoints**: Located in the `module/` directory. Each file typically corresponds to an API route.
|
||||
- **Core Utilities**: Request handling, encryption, and core utilities are found in the `util/` directory.
|
||||
## 架构
|
||||
- `app.js`(也是 `bin`)——服务入口。先确保 `os.tmpdir()` 里存在 `anonymous_token`,执行 `generateConfig()` 刷新匿名 cookie 与 xeapi 公钥,再调用 `server.serveNcmApi()`。
|
||||
- `server.js`——Express 工厂。`constructServer()` 自动扫描 `module/*.js`,每个文件注册一条路由(文件名 `_` 转 `/`,如 `album_new.js` → `/album/new`;特例 `daily_signin`/`fm_trash`/`personal_fm` 硬编码在 `server.js` 的 `special` 对象里)。`serveNcmApi()` 监听 `PORT`(默认 3000)/`HOST`。
|
||||
- `main.js`——作为依赖被引入时的入口(`main` 字段)。把每个 `module/*` 导出为同名函数 `name(data)`,另导出 `server`、`serveNcmApi`、`getModulesDefinitions`。
|
||||
- `module/*.js`——每个接口一个文件,标准写法:`module.exports = (query, request) => request(path, data, createOption(query))`。`createOption` 在 `util/option.js`,负责 crypto、cookie(回退到 `NETEASE_COOKIE`)、proxy、realIP/randomCNIP、headers、timeout。
|
||||
- `util/request.js`——唯一的对外 HTTP 层(axios)。按 `crypto`(`api`/`eapi`/`weapi`/`linuxapi`/`xeapi`)加密并设置 IP 头;在 require 时同步读取 `os.tmpdir()` 里的 `anonymous_token` 与 `xeapi_public_key`。
|
||||
- `util/config.json`——运行时配置:网易域名 + `APP_CONF.encrypt: true`(默认走 eapi 加密)。已被 git 跟踪,改动会改变全局默认行为。
|
||||
- `index.js` / `index.mjs`——`require('./app.js')` 的薄包装,供 Vercel(`vercel.json`)和 ESM 导入使用。
|
||||
|
||||
## Important Gotchas & Quirks
|
||||
- **Environment Variables**: The server defaults to port 3000 but can be overridden with the `PORT` environment variable.
|
||||
- **Proxy Variables**: Be very careful with proxy environment variables (`http_proxy`, `https_proxy`, `no_proxy`). The request library (like axios) will automatically pick these up. If they point to an unavailable proxy (especially common in Docker environments), requests will fail silently or throw connection errors.
|
||||
- **Code Style**: The project uses ESLint and Prettier. Always run `pnpm lint-fix` before committing changes to ensure formatting consistency.
|
||||
## 新增/修改接口
|
||||
- 新建 `module/xxx.js` 会自动挂载路由,无需注册;**文件名即路由**。
|
||||
- 照抄同目录模块的写法(选对 `crypto`),用 `createOption(query)` 生成请求选项。
|
||||
- 改文件名/路由会破坏已有客户端,尽量保持旧路径兼容。
|
||||
|
||||
## 测试
|
||||
- `pnpm test` 跑 `server.test.js` + `main.test.js`。`server.test.js` 在 `before()` 里启动真实服务器,`test/*.test.js` 全部请求**真实网易云 API**——必须联网,且可能因上游风控/限流偶发失败。`main.test.js` 是纯单测。
|
||||
- 测试使用 `power-assert`(经 `intelli-espower-loader`),普通 `assert` 写法也会输出详细 diff。
|
||||
- 只跑单个用例:`pnpm exec mocha -r intelli-espower-loader -t 60000 --grep "<describe/it 名字>" server.test.js main.test.js --exit`
|
||||
|
||||
## 坑与注意
|
||||
- **改 `package.json` 的 `version` 会触发自动发布**:push 到 `main` 后会自动打 GitHub Release(`pkg` 二进制)、构建并推送 Docker 镜像(Docker Hub + GHCR)、`pnpm publish` 到 npm。别顺手改版本号。
|
||||
- **没有实际 git hooks**:`package.json` 里配了 `lint-staged`,但 `.husky/` 下没有真正的 hook,commit 时不会自动跑任何检查,自己记得 `pnpm lint-fix`。
|
||||
- **代理环境变量已失效**:README 里关于 `http_proxy`/`https_proxy` 的警告来自旧 `request` 库时代;现在 `util/request.js` 用 axios + 自定义 keep-alive agent,且显式 `proxy: false`,环境变量代理不会生效。按请求走 `query.proxy` 参数(支持 PAC 和 http 隧道)。
|
||||
- **启动令牌在系统临时目录**:`anonymous_token`、`xeapi_public_key` 存放在 `os.tmpdir()`,`util/request.js` 在 require 时同步读取。文件过期或被清空就重启服务(或调用 `generateConfig()`);首次启动先写空文件再刷新。
|
||||
- **ESLint 9 flat config**:`eslint.config.js`,风格由 `eslint-plugin-prettier` 强制(2 空格缩进、单引号、分号、`endOfLine: auto`)。
|
||||
|
||||
@ -2,14 +2,19 @@ FROM node:lts-alpine
|
||||
|
||||
RUN apk add --no-cache tini
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm install -g pnpm@9
|
||||
|
||||
USER node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node . ./
|
||||
|
||||
RUN yarn --network-timeout=100000
|
||||
# --prod 模式下 husky 不会被安装,prepare 脚本会因找不到 husky 而失败,
|
||||
# 故显式跳过生命周期脚本;本项目生产运行也不依赖任何 postinstall 步骤。
|
||||
RUN pnpm install --frozen-lockfile --prod --ignore-scripts
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
294
interface.d.ts
vendored
294
interface.d.ts
vendored
@ -37,6 +37,41 @@ export const enum SubAction {
|
||||
unsub = 0,
|
||||
}
|
||||
|
||||
export function ad_get(
|
||||
params: { type_ids?: string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function ad_listening_rights(
|
||||
params: RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function ad_listening_rights_gain(
|
||||
params: {
|
||||
reqUid?: string
|
||||
uid?: string | number
|
||||
exposureTime?: string | number
|
||||
clickTime?: string | number
|
||||
extraRightsType?: string | number
|
||||
playContinuously?: boolean | string
|
||||
source?: string | number
|
||||
creativeType?: string | number
|
||||
rightsGainMethod?: string | number
|
||||
extraRightsGainMethod?: string | number
|
||||
extraRightsGainDuration?: string | number
|
||||
nextRightsGainDuration?: string | number
|
||||
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<Response>
|
||||
|
||||
export function activate_init_profile(
|
||||
params: { nickname: string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
@ -119,6 +154,14 @@ export function album_sublist(
|
||||
params: MultiPageConfig & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function api(
|
||||
params: {
|
||||
uri: string
|
||||
data?: string | Record<string, unknown>
|
||||
crypto?: string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function artist_album(
|
||||
params: { id: string | number } & MultiPageConfig & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
@ -271,10 +314,21 @@ export function batch(
|
||||
params: { [index: string]: unknown } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function captcha_safe_sent(
|
||||
params: { ctcode?: number | string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function captcha_sent(
|
||||
params: { phone: string; ctcode?: number | string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function captcha_sent_v1(
|
||||
params: {
|
||||
phone: number | string
|
||||
ctcode?: number | string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function captcha_verify(
|
||||
params: {
|
||||
ctcode?: number | string
|
||||
@ -387,6 +441,14 @@ export function comment(
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function comment_add(
|
||||
params: {
|
||||
id: string | number
|
||||
type: CommentType
|
||||
content: string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function comment_album(
|
||||
params: {
|
||||
id: string | number
|
||||
@ -395,6 +457,14 @@ export function comment_album(
|
||||
RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function comment_delete(
|
||||
params: {
|
||||
id: string | number
|
||||
type: CommentType
|
||||
cid: string | number
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function comment_dj(
|
||||
params: {
|
||||
id: string | number
|
||||
@ -469,6 +539,15 @@ export function comment_playlist(
|
||||
RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function comment_reply(
|
||||
params: {
|
||||
id: string | number
|
||||
type: CommentType
|
||||
cid: string | number
|
||||
content: string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function comment_video(
|
||||
params: {
|
||||
id: string | number
|
||||
@ -490,6 +569,21 @@ export function daily_signin(
|
||||
params: { type?: DailySigninType } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function decrypt(
|
||||
params: {
|
||||
crypto?: string
|
||||
data?: string
|
||||
hexString?: string
|
||||
isReq?: boolean | string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function device_kickoff(
|
||||
params: { deviceKey: string | number; captcha?: string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function device_list(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function digitalAlbum_ordering(
|
||||
params: {
|
||||
payment: string
|
||||
@ -618,6 +712,10 @@ export function dj_toplist_popular(
|
||||
params: { limit?: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function eapi_decrypt(
|
||||
params: { hexString: string; isReq?: boolean | string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function event(
|
||||
params: { pagesize?: number; lasttime?: number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
@ -626,6 +724,20 @@ export function event_del(
|
||||
params: { evId: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export const enum EventPrivacy {
|
||||
everyone = 0,
|
||||
following = 1,
|
||||
onlyMe = 2,
|
||||
mutualFollowing = 6,
|
||||
}
|
||||
|
||||
export function event_privacy(
|
||||
params: {
|
||||
evId: string | number
|
||||
privacy: EventPrivacy
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function event_forward(
|
||||
params: {
|
||||
forwords: string
|
||||
@ -665,6 +777,8 @@ export function hot_topic(
|
||||
params: MultiPageConfig & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function inner_version(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function like(
|
||||
params: {
|
||||
like?: 'true' | 'false' | boolean
|
||||
@ -678,6 +792,10 @@ export function likelist(
|
||||
params: { uid: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function listentogether_status(
|
||||
params: RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function login(
|
||||
params: { email: string; password: string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
@ -724,6 +842,17 @@ export function lyric_new(
|
||||
params: { id: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function middle_play_do_lottery(
|
||||
params: {
|
||||
activityId?: string | number
|
||||
drawCount?: string | number
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function middle_play_lottery_remain_chance(
|
||||
params: { activityId?: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function msg_comments(
|
||||
params: {
|
||||
uid: string | number
|
||||
@ -946,6 +1075,18 @@ export function register_cellphone(
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function register_checktoken_v2(
|
||||
params: { refresh?: boolean | string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function register_checktoken_v3(
|
||||
params: { refresh?: boolean | string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function register_xeapikey(
|
||||
params: { deviceId?: string; currentKeyVersion?: string } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function related_allvideo(
|
||||
params: { id: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
@ -954,6 +1095,67 @@ export function related_playlist(
|
||||
params: { id: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function relay_play_state_submit(
|
||||
params: {
|
||||
id: string | number
|
||||
sessionId?: string
|
||||
progress?: string | number
|
||||
playMode?: string
|
||||
type?: string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function rep_ugc_activity_collect(
|
||||
params: { activityId?: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function rep_ugc_activity_get(
|
||||
params: RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function rep_ugc_exam_info_get(
|
||||
params: { examType: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function rep_ugc_exam_question_single_get(
|
||||
params: {
|
||||
examType: string | number
|
||||
taskId: string | number
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function rep_ugc_exam_result_get(
|
||||
params: {
|
||||
examType: string | number
|
||||
taskId: string | number
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function rep_ugc_exam_start(
|
||||
params: { examType: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function rep_ugc_exam_submit(
|
||||
params: {
|
||||
examType: string | number
|
||||
taskId: string | number
|
||||
questionId: string | number
|
||||
answer: string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
declare const rep_ugc_user_collect_vip: (
|
||||
params: { activityId?: string | number } & RequestBaseConfig,
|
||||
) => Promise<Response>
|
||||
|
||||
export { rep_ugc_user_collect_vip as 'rep_ugc_user_collect-vip' }
|
||||
|
||||
export function rep_ugc_user_get(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function rep_ugc_user_sign(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function rep_ugc_user_vip(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export const enum ResourceType {
|
||||
mv = 1,
|
||||
dj = 4,
|
||||
@ -979,6 +1181,22 @@ export function scrobble(
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function scrobble_v1(
|
||||
params: {
|
||||
id: string | number
|
||||
time: string | number
|
||||
total?: string | number
|
||||
sourceid?: string | number
|
||||
sourceId?: string | number
|
||||
source?: string
|
||||
name?: string
|
||||
artist?: string
|
||||
bitrate?: string | number
|
||||
level?: string
|
||||
vip?: boolean | string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function search(
|
||||
params: {
|
||||
keywords: string
|
||||
@ -1267,6 +1485,25 @@ export function user_event(
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export interface UserEventAllResponse {
|
||||
code: number
|
||||
events: Record<string, unknown>[]
|
||||
/** Upstream account statistic; it can include events no longer returned. */
|
||||
size: number | null
|
||||
/** Number of unique events in `events`. */
|
||||
retrievedCount: number
|
||||
/** Positive difference between `size` and `retrievedCount`. */
|
||||
unavailableCount: number | null
|
||||
sizeMismatch: boolean | null
|
||||
pageCount: number
|
||||
more: false
|
||||
lasttime: string | number | null
|
||||
}
|
||||
|
||||
export function user_event_all(
|
||||
params: RequestBaseConfig,
|
||||
): Promise<Response<UserEventAllResponse>>
|
||||
|
||||
export function user_followeds(
|
||||
params: {
|
||||
uid: string | number
|
||||
@ -1433,6 +1670,23 @@ export function yunbei_task_finish(
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function yunbei_ad_task_list(
|
||||
params: RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function yunbei_ad_task_recommend_song(
|
||||
params: {
|
||||
offset?: number | string
|
||||
limit?: number | string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function yunbei_ad_task_finish(
|
||||
params: {
|
||||
yunbeiAmount?: number | string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function msg_recentcontact(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function hug_comment(
|
||||
@ -2292,6 +2546,10 @@ export function song_chorus(
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function song_cloud_download(
|
||||
params: { id: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function song_creators(
|
||||
params: {
|
||||
id: string | number
|
||||
@ -2403,6 +2661,18 @@ export function threshold_detail_get(
|
||||
params: RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function thinktank_audit_resource_detail(
|
||||
params: { type?: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function thinktank_audit_resource_update(
|
||||
params: {
|
||||
type?: string | number
|
||||
taskId: string | number
|
||||
judgement: string | number
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function toplist_detail_v2(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function ugc_album_get(
|
||||
@ -2529,6 +2799,14 @@ export function verify_qrcodestatus(
|
||||
|
||||
export function vip_sign(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function vip_sign_detail(
|
||||
params: { timestamp: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function vip_sign_history(
|
||||
params: { type?: string | number } & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
export function vip_sign_info(params: RequestBaseConfig): Promise<Response>
|
||||
|
||||
export function vip_tasks_v1(
|
||||
@ -2549,6 +2827,22 @@ export function voice_upload(
|
||||
name: string
|
||||
data: string | Buffer
|
||||
}
|
||||
imgFile?: {
|
||||
name: string
|
||||
data: string | Buffer
|
||||
}
|
||||
voiceListId: string | number
|
||||
coverImgId?: string | number
|
||||
categoryId: string | number
|
||||
secondCategoryId: string | number
|
||||
description: string
|
||||
songName?: string
|
||||
privacy?: string | number
|
||||
publishTime?: string | number
|
||||
autoPublish?: string | number
|
||||
autoPublishText?: string
|
||||
orderNo?: string | number
|
||||
composedSongs?: string
|
||||
} & RequestBaseConfig,
|
||||
): Promise<Response>
|
||||
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
// 获取广告
|
||||
// 基于逆向网易云音乐 v9.5.45 RN Hermes 源码:
|
||||
// - adService.getAd() → Network.nativeRequest → /api/ad/get
|
||||
// - 需要 X-antiCheatToken 易盾反作弊头
|
||||
// - 使用 xeapi 加密
|
||||
//
|
||||
// 返回 extra.reqId 可用于 ad_listening_rights_gain 的 reqUid
|
||||
//
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
@ -13,20 +7,34 @@ module.exports = async (query, request) => {
|
||||
type_ids: query.type_ids || '["400002_0"]',
|
||||
}
|
||||
|
||||
const option = createOption(query, 'xeapi')
|
||||
option.checkToken = true
|
||||
const option = createOption(query, 'xeapi', 'v3')
|
||||
|
||||
const res = await request(`/api/ad/get`, data, option)
|
||||
const raw = res.body
|
||||
|
||||
// 提取广告中的 req_id (用于领取权益)
|
||||
// 提取广告中的 req_id
|
||||
let reqId = ''
|
||||
try {
|
||||
if (raw?.ads) {
|
||||
const ad = Object.values(raw.ads)[0]
|
||||
if (ad?.extJson) {
|
||||
// 逆向 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 (_) {}
|
||||
|
||||
13
module/ad_listening_rights.js
Normal file
13
module/ad_listening_rights.js
Normal file
@ -0,0 +1,13 @@
|
||||
// 获取免费听时长状态
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
entrance: 'FREE_LISTEN_RN',
|
||||
}
|
||||
return request(
|
||||
`/api/ad/homepage/free/tab/extend/v2`,
|
||||
data,
|
||||
createOption(query, 'xeapi'),
|
||||
)
|
||||
}
|
||||
@ -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)
|
||||
// 额外权益类型(拉新分段权益等,决定发放云贝/时长/下载,来自广告配置)
|
||||
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)
|
||||
: hint.extraRightsGainMethod !== undefined
|
||||
? parseInt(hint.extraRightsGainMethod)
|
||||
: undefined,
|
||||
extraRightsGainDuration: query.extraRightsGainDuration
|
||||
? parseInt(query.extraRightsGainDuration)
|
||||
: hint.extraRightsGainDuration !== undefined
|
||||
? parseInt(hint.extraRightsGainDuration)
|
||||
: undefined,
|
||||
nextRightsGainDuration: query.nextRightsGainDuration
|
||||
? parseInt(query.nextRightsGainDuration)
|
||||
: hint.nextRightsGainDuration !== undefined
|
||||
? parseInt(hint.nextRightsGainDuration)
|
||||
: undefined,
|
||||
|
||||
// 来源标识 | 权益扩展信息
|
||||
source: query.source || undefined,
|
||||
rightsExtJson: query.rightsExtJson || 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,
|
||||
|
||||
// 通用权益信息(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,16 +231,16 @@ module.exports = async (query, request) => {
|
||||
if (rightsParam[key] === undefined) delete rightsParam[key]
|
||||
})
|
||||
|
||||
// 将参数序列化为 reqParam 字符串 (与 RN 源码完全一致)
|
||||
// 将参数序列化为 reqParam 字符串 (与原生源码一致)
|
||||
const data = {
|
||||
reqParam: JSON.stringify(rightsParam),
|
||||
}
|
||||
|
||||
const option = createOption(query, 'xeapi')
|
||||
// 关键: 开启 X-antiCheatToken 头
|
||||
option.checkToken = true
|
||||
|
||||
const res = await request(`/api/ad/listening/rights/gain`, data, option)
|
||||
const res = await request(
|
||||
`/api/ad/listening/rights/gain`,
|
||||
data,
|
||||
createOption(query, 'xeapi', 'v3'),
|
||||
)
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
|
||||
13
module/captcha_safe_sent.js
Normal file
13
module/captcha_safe_sent.js
Normal file
@ -0,0 +1,13 @@
|
||||
// 发送安全验证码
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
ctcode: query.ctcode || '86',
|
||||
}
|
||||
return request(
|
||||
`/api/sms/captcha/safe/sent`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
16
module/captcha_sent_v1.js
Normal file
16
module/captcha_sent_v1.js
Normal file
@ -0,0 +1,16 @@
|
||||
// 发送验证码
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
ctcode: query.ctcode || '86',
|
||||
secrete: 'music_middleuser_pclogin',
|
||||
cellphone: query.phone,
|
||||
scene: '0',
|
||||
}
|
||||
return request(
|
||||
`/api/middle/captcha/sent/v1`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@ -6,5 +6,9 @@ module.exports = (query, request) => {
|
||||
cellphone: query.phone,
|
||||
countrycode: query.countrycode,
|
||||
}
|
||||
return request(`/api/cellphone/existence/check`, data, createOption(query))
|
||||
return request(
|
||||
`/api/cellphone/existence/check`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
|
||||
@ -25,6 +25,6 @@ module.exports = (query, request) => {
|
||||
return request(
|
||||
`/api/resource/comments/${query.t}`,
|
||||
data,
|
||||
createOption(query, 'weapi'),
|
||||
createOption(query, 'eapi', 'v2'),
|
||||
)
|
||||
}
|
||||
|
||||
18
module/comment_add.js
Normal file
18
module/comment_add.js
Normal file
@ -0,0 +1,18 @@
|
||||
const { resourceTypeMap } = require('../util/config.json')
|
||||
// 发送评论
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
threadId: resourceTypeMap[query.type] + query.id,
|
||||
content: query.content,
|
||||
resourceType: '0',
|
||||
expressionPicId: '-1',
|
||||
bubbleId: '-1',
|
||||
}
|
||||
return request(
|
||||
`/api/resource/comments/add`,
|
||||
data,
|
||||
createOption(query, 'xeapi', 'v3'),
|
||||
)
|
||||
}
|
||||
15
module/comment_delete.js
Normal file
15
module/comment_delete.js
Normal file
@ -0,0 +1,15 @@
|
||||
const { resourceTypeMap } = require('../util/config.json')
|
||||
// 删除评论
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
commentId: query.cid,
|
||||
threadId: resourceTypeMap[query.type] + query.id,
|
||||
}
|
||||
return request(
|
||||
`/api/resource/comments/delete`,
|
||||
data,
|
||||
createOption(query, 'xeapi'),
|
||||
)
|
||||
}
|
||||
17
module/comment_reply.js
Normal file
17
module/comment_reply.js
Normal file
@ -0,0 +1,17 @@
|
||||
const { resourceTypeMap } = require('../util/config.json')
|
||||
// 发送评论
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
threadId: resourceTypeMap[query.type] + query.id,
|
||||
commentId: query.cid,
|
||||
content: query.content,
|
||||
resourceType: '0',
|
||||
}
|
||||
return request(
|
||||
`/api/v1/resource/comments/reply`,
|
||||
data,
|
||||
createOption(query, 'xeapi', 'v3'),
|
||||
)
|
||||
}
|
||||
@ -48,7 +48,7 @@ module.exports = async (query, request) => {
|
||||
case 'linuxapi': {
|
||||
if (isReq) {
|
||||
const pureHex = data.replace(/\s/g, '')
|
||||
const decrypted = aesDecrypt(pureHex, linuxapiKey, '', 'hex')
|
||||
const decrypted = aesDecrypt(pureHex, 'ecb', linuxapiKey, '', 'hex')
|
||||
result = JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
|
||||
} else {
|
||||
result = typeof data === 'string' ? JSON.parse(data) : data
|
||||
|
||||
14
module/device_kickoff.js
Normal file
14
module/device_kickoff.js
Normal file
@ -0,0 +1,14 @@
|
||||
// 强制下线设备
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
key: query.deviceKey,
|
||||
captcha: query.captcha || '',
|
||||
}
|
||||
return request(
|
||||
`/api/middle/user/security/device/kickoff`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
13
module/device_list.js
Normal file
13
module/device_list.js
Normal file
@ -0,0 +1,13 @@
|
||||
// 登录设备列表
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
excStatus: '9',
|
||||
}
|
||||
return request(
|
||||
`/api/middle/user/device/list`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
34
module/event_privacy.js
Normal file
34
module/event_privacy.js
Normal file
@ -0,0 +1,34 @@
|
||||
// 修改本人动态的可见权限
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
const PRIVACY_VALUES = new Set([0, 1, 2, 6])
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const eventId = String(query.evId ?? '').trim()
|
||||
const rawPrivacy = String(query.privacy ?? '').trim()
|
||||
const privacy = Number(rawPrivacy)
|
||||
|
||||
if (
|
||||
!eventId ||
|
||||
!rawPrivacy ||
|
||||
!Number.isInteger(privacy) ||
|
||||
!PRIVACY_VALUES.has(privacy)
|
||||
) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: 'evId is required and privacy must be one of 0, 1, 2, 6',
|
||||
},
|
||||
cookie: [],
|
||||
})
|
||||
}
|
||||
|
||||
const data = {
|
||||
eventId,
|
||||
privacy,
|
||||
}
|
||||
|
||||
return request(`/api/event/privacy/op`, data, createOption(query))
|
||||
}
|
||||
@ -14,6 +14,7 @@ module.exports = async (query, request) => {
|
||||
? query.captcha
|
||||
: query.md5_password || CryptoJS.MD5(query.password).toString(),
|
||||
remember: 'true',
|
||||
secureCaptcha: query.sca || '',
|
||||
}
|
||||
let result = await request(
|
||||
`/api/w/login/cellphone`,
|
||||
|
||||
24
module/middle_play_do_lottery.js
Normal file
24
module/middle_play_do_lottery.js
Normal file
@ -0,0 +1,24 @@
|
||||
// 云小编每日抽奖
|
||||
//
|
||||
// activityId:
|
||||
// 默认 6501202
|
||||
//
|
||||
// drawCount:
|
||||
// 默认 1
|
||||
//
|
||||
// checkToken:
|
||||
// 易盾反作弊 Token
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
activityId: query.activityId || '6501202',
|
||||
drawCount: query.drawCount || '1',
|
||||
}
|
||||
return request(
|
||||
`/api/middle/play/do/lottery`,
|
||||
data,
|
||||
createOption(query, 'eapi', 'v2'),
|
||||
)
|
||||
}
|
||||
17
module/middle_play_lottery_remain_chance.js
Normal file
17
module/middle_play_lottery_remain_chance.js
Normal file
@ -0,0 +1,17 @@
|
||||
// 云小编抽奖剩余次数查询
|
||||
//
|
||||
// activityId:
|
||||
// 默认 6501202
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
activityId: query.activityId || '6501202',
|
||||
}
|
||||
return request(
|
||||
`/api/middle/play/lottery/remain/chance`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@ -9,6 +9,6 @@ module.exports = (query, request) => {
|
||||
? { checkToken: query.checkToken || APP_CONF.checkToken }
|
||||
: {}),
|
||||
}
|
||||
query.checkToken = true // 强制开启checkToken
|
||||
query.checkToken = 'v2' // 强制开启checkToken
|
||||
return request(`/api/playlist/${path}`, data, createOption(query, 'eapi'))
|
||||
}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
// 易盾反作弊 Token 注册端点
|
||||
// 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用
|
||||
//
|
||||
// GET /register/checktoken → 返回当前 token(尚无则自动获取)
|
||||
// POST /register/checktoken → 强制刷新 token
|
||||
// GET /register/checktoken?refresh=1 → 强制刷新
|
||||
//
|
||||
const { default: axios } = require('axios')
|
||||
const { APP_CONF } = require('../util/config.json')
|
||||
|
||||
const URL = APP_CONF.dunDomain + '/v3/b?pn=YD00000558929251'
|
||||
let _token = ''
|
||||
|
||||
async function fetch() {
|
||||
const res = await axios.get(URL, { timeout: 10000 })
|
||||
const body = String(res.data)
|
||||
const m = body.match(/null\(\[(\d+),\d+,\"([^\"]+)\"\]\)/)
|
||||
if (m && m[1] === '200') return m[2]
|
||||
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 = await fetch()
|
||||
_token = token
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: { code: 200, token, registered: !!token },
|
||||
}
|
||||
}
|
||||
|
||||
// 给 request.js 读取用
|
||||
module.exports.getToken = () => _token
|
||||
158
module/register_checktoken_v2.js
Normal file
158
module/register_checktoken_v2.js
Normal file
@ -0,0 +1,158 @@
|
||||
// 易盾反作弊 Token 注册端点
|
||||
// 通过易盾官方 Watchman SDK(Web 版,跑在 jsdom 模拟的浏览器环境里)
|
||||
// 实时调用 getToken(businessId) 获取反作弊 token,供后续带 checkToken 的请求使用
|
||||
//
|
||||
// GET /register/checktoken/v2 → 实时获取新 token(不缓存)
|
||||
// POST /register/checktoken/v2 → 实时获取新 token
|
||||
//
|
||||
// 注意:每次获取都不缓存,模拟真实客户端每次请求使用新鲜 token,
|
||||
// 避免反作弊 token 复用触发风控。
|
||||
//
|
||||
// 注:jsdom 固定用 v24(engines >=18),其依赖链为纯 CJS,可被 pkg 静态
|
||||
// 分析自动打包;v30+ 引入纯 ESM 依赖且要求 Node >=22,无法在 CI(18-24) 与
|
||||
// pkg(node18) 目标下运行。
|
||||
//
|
||||
const { JSDOM, VirtualConsole } = require('jsdom')
|
||||
const { default: axios } = require('axios')
|
||||
const { APP_CONF } = require('../util/config.json')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
// 网易云音乐在易盾的 productNumber 与 businessId
|
||||
const PRODUCT_NUMBER = 'YD00000558929251'
|
||||
const BUSINESS_ID = 'bd5d2f973ef74cd2a61325a412ae54d9'
|
||||
const TOOL_JS_URL = `${APP_CONF.dunStaticDomain}/tool.min.js`
|
||||
|
||||
// 最小 HTML 外壳,模拟网页环境
|
||||
const HTML =
|
||||
'<!doctype html><html><head><meta charset="UTF-8"></head><body></body></html>'
|
||||
|
||||
let toolJs = ''
|
||||
let wm = null // Watchman 实例(进程内复用,可反复 getToken)
|
||||
let dom = null // jsdom 实例(失败时 close 释放活动句柄,避免泄漏)
|
||||
let initPromise = null
|
||||
|
||||
// 获取 tool.min.js(内存缓存,避免每次初始化重复下载)
|
||||
async function getToolJs() {
|
||||
if (toolJs) return toolJs
|
||||
const res = await axios.get(TOOL_JS_URL, { timeout: 10000 })
|
||||
toolJs = String(res.data)
|
||||
return toolJs
|
||||
}
|
||||
|
||||
// 初始化 Watchman(进程内只初始化一次,实例可反复 getToken)
|
||||
async function ensureWatchman() {
|
||||
if (wm) return wm
|
||||
if (initPromise) return initPromise
|
||||
|
||||
initPromise = (async () => {
|
||||
const js = await getToolJs()
|
||||
const virtualConsole = new VirtualConsole()
|
||||
virtualConsole.on('jsdomError', () => {})
|
||||
dom = new JSDOM(HTML, {
|
||||
url: 'https://music.163.com/',
|
||||
referrer: 'https://music.163.com/',
|
||||
contentType: 'text/html',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable', // 允许动态加载 watchman.min.js / JSONP
|
||||
pretendToBeVisual: true,
|
||||
virtualConsole,
|
||||
beforeParse(window) {
|
||||
// 抹掉 headless 特征,避免易盾风控误判
|
||||
Object.defineProperty(window.navigator, 'webdriver', {
|
||||
get: () => undefined,
|
||||
})
|
||||
window.chrome = { runtime: {} }
|
||||
window.navigator.languages = ['zh-CN', 'zh']
|
||||
window.navigator.plugins = [1, 2, 3, 4, 5]
|
||||
},
|
||||
})
|
||||
const script = dom.window.document.createElement('script')
|
||||
script.textContent = js
|
||||
dom.window.document.body.appendChild(script)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
reject(new Error('watchman 初始化超时'))
|
||||
}, 15000)
|
||||
dom.window.initWatchman({
|
||||
auto: true,
|
||||
productNumber: PRODUCT_NUMBER,
|
||||
onload(instance) {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
wm = instance
|
||||
resolve(instance)
|
||||
},
|
||||
onerror(...args) {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
reject(new Error('watchman 初始化失败'))
|
||||
},
|
||||
})
|
||||
})
|
||||
})()
|
||||
|
||||
try {
|
||||
return await initPromise
|
||||
} catch (e) {
|
||||
// 失败路径:关闭 jsdom 释放 rAF/子资源/定时器等活动句柄,防止每次失败泄漏约 30MB
|
||||
if (dom) {
|
||||
dom.window.close()
|
||||
dom = null
|
||||
}
|
||||
initPromise = null
|
||||
wm = null
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// 获取新 token
|
||||
async function fetchToken() {
|
||||
const instance = await ensureWatchman()
|
||||
const raw = instance.getInstance()
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const timer = setTimeout(() => resolve(), 15000)
|
||||
raw.I(() => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => resolve(''), 15000)
|
||||
instance.getToken(BUSINESS_ID, (tk) => {
|
||||
clearTimeout(timer)
|
||||
resolve(typeof tk === 'string' ? tk : '')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 端点处理:每次实时获取新 token
|
||||
module.exports = async () => {
|
||||
let token = ''
|
||||
try {
|
||||
token = await fetchToken()
|
||||
} catch (e) {
|
||||
logger.warn('[checkToken v2]', e.message)
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: { code: 200, token, registered: !!token },
|
||||
}
|
||||
}
|
||||
|
||||
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
|
||||
module.exports.getToken = async () => {
|
||||
try {
|
||||
return await fetchToken()
|
||||
} catch (e) {
|
||||
logger.warn('[checkToken v2]', e.message)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
44
module/register_checktoken_v3.js
Normal file
44
module/register_checktoken_v3.js
Normal file
@ -0,0 +1,44 @@
|
||||
// 易盾反作弊 Token 注册端点
|
||||
// 调用后获取实时 token 并存入共享存储,供后续带 checkToken 的请求使用
|
||||
//
|
||||
// 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'
|
||||
|
||||
async function fetch() {
|
||||
const res = await axios.get(URL, { timeout: 10000 })
|
||||
const body = String(res.data)
|
||||
const m = body.match(/null\(\[(\d+),\d+,\"([^\"]+)\"\]\)/)
|
||||
if (m && m[1] === '200') return m[2]
|
||||
throw new Error('易盾返回异常: ' + body.substring(0, 100))
|
||||
}
|
||||
|
||||
// 端点处理:每次实时获取新 token
|
||||
module.exports = async () => {
|
||||
let token = ''
|
||||
try {
|
||||
token = await fetch()
|
||||
} catch (e) {
|
||||
// token 获取失败时返回空,由调用方决定是否重试
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: { code: 200, token, registered: !!token },
|
||||
}
|
||||
}
|
||||
|
||||
// 给 request.js 读取用:每次调用实时获取新 token,不缓存
|
||||
module.exports.getToken = async () => {
|
||||
try {
|
||||
return await fetch()
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
|
||||
17
module/rep_ugc_activity_collect.js
Normal file
17
module/rep_ugc_activity_collect.js
Normal file
@ -0,0 +1,17 @@
|
||||
// 云小编领取任务积分
|
||||
//
|
||||
// activityId:
|
||||
// 调用 rep/ugc/activity/get 获取
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
activityId: query.activityId || '5001',
|
||||
}
|
||||
return request(
|
||||
`/api/rep/ugc/activity/collect`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
7
module/rep_ugc_activity_get.js
Normal file
7
module/rep_ugc_activity_get.js
Normal file
@ -0,0 +1,7 @@
|
||||
// 云小编活动信息
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/activity/get`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
28
module/rep_ugc_exam_info_get.js
Normal file
28
module/rep_ugc_exam_info_get.js
Normal file
@ -0,0 +1,28 @@
|
||||
// 云小编考试状态
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
}
|
||||
return request(
|
||||
'/api/rep/ugc/exam/info/get',
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
32
module/rep_ugc_exam_question_single_get.js
Normal file
32
module/rep_ugc_exam_question_single_get.js
Normal file
@ -0,0 +1,32 @@
|
||||
// 云小编考试取题
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
//
|
||||
// taskId:
|
||||
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType || !query.taskId)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
taskId: query.taskId,
|
||||
}
|
||||
return request(
|
||||
'/api/rep/ugc/exam/question/single/get',
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
32
module/rep_ugc_exam_result_get.js
Normal file
32
module/rep_ugc_exam_result_get.js
Normal file
@ -0,0 +1,32 @@
|
||||
// 云小编考试结果
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
//
|
||||
// taskId:
|
||||
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType || !query.taskId)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
taskId: query.taskId,
|
||||
}
|
||||
return request(
|
||||
'/api/rep/ugc/exam/result/get',
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
24
module/rep_ugc_exam_start.js
Normal file
24
module/rep_ugc_exam_start.js
Normal file
@ -0,0 +1,24 @@
|
||||
// 云小编考试开始
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
}
|
||||
return request('/api/rep/ugc/exam/start', data, createOption(query, 'eapi'))
|
||||
}
|
||||
36
module/rep_ugc_exam_submit.js
Normal file
36
module/rep_ugc_exam_submit.js
Normal file
@ -0,0 +1,36 @@
|
||||
// 云小编考试提交
|
||||
//
|
||||
// examType:
|
||||
// 1. 歌曲曲风审核: musicalStyleEnter
|
||||
// 2. 歌曲语种审核: languageEnter
|
||||
// 3. 歌曲原唱审核: oriSingerEnter
|
||||
// 4. 情绪标签审核: emotionEnter
|
||||
//
|
||||
// taskId:
|
||||
// 首次调用 rep/ugc/exam/start 获取,之后调用 rep/ugc/exam/info/get 获取
|
||||
//
|
||||
// questionId:
|
||||
// 调用 rep/ugc/exam/question/single/get 获取
|
||||
//
|
||||
// answer:
|
||||
// A 对, B 错
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.examType || !query.taskId || !query.questionId || !query.answer)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
examType: query.examType,
|
||||
taskId: query.taskId,
|
||||
questionId: query.questionId,
|
||||
answer: query.answer,
|
||||
}
|
||||
return request('/api/rep/ugc/exam/submit', data, createOption(query, 'eapi'))
|
||||
}
|
||||
16
module/rep_ugc_user_collect-vip.js
Normal file
16
module/rep_ugc_user_collect-vip.js
Normal file
@ -0,0 +1,16 @@
|
||||
// 云小编领取一日会员
|
||||
//
|
||||
// 注:前提条件见 rep/ugc/user/vip
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
activityId: query.activityId || '5001',
|
||||
}
|
||||
return request(
|
||||
`/api/rep/ugc/user/collect-vip`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
7
module/rep_ugc_user_get.js
Normal file
7
module/rep_ugc_user_get.js
Normal file
@ -0,0 +1,7 @@
|
||||
// 云小编获取用户详情
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/user/get`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
7
module/rep_ugc_user_sign.js
Normal file
7
module/rep_ugc_user_sign.js
Normal file
@ -0,0 +1,7 @@
|
||||
// 云小编每日签到
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/user/sign`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
12
module/rep_ugc_user_vip.js
Normal file
12
module/rep_ugc_user_vip.js
Normal file
@ -0,0 +1,12 @@
|
||||
// 云小编查询会员任务状态
|
||||
//
|
||||
// 状态 (data.status)
|
||||
// 10: 用户积分达50,可免费领取1日黑胶会员
|
||||
// 20: 用户积分已达50,可免费领取1日黑胶会员
|
||||
// 30: 已领取1日黑胶会员,明天再来吧~
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
return request(`/api/rep/ugc/user/vip`, {}, createOption(query, 'eapi'))
|
||||
}
|
||||
@ -7,5 +7,9 @@ module.exports = (query, request) => {
|
||||
msg: query.msg || '',
|
||||
id: query.id || '',
|
||||
}
|
||||
return request(`/api/share/friends/resource`, data, createOption(query))
|
||||
return request(
|
||||
`/api/share/friends/resource`,
|
||||
data,
|
||||
createOption(query, 'xeapi', 'v3'),
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// 此版本不再采用 br 作为音质区分的标准
|
||||
// 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
|
||||
// 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
|
||||
// 当level为sky时, 可通过 immerseType 选择沉浸声类型, 支持 c51(c51类型)、ste(环绕立体声类型)、aac(aac类型), 默认为 c51
|
||||
|
||||
const logger = require('../util/logger.js')
|
||||
const createOption = require('../util/option.js')
|
||||
@ -51,7 +52,7 @@ module.exports = async (query, request) => {
|
||||
}
|
||||
}
|
||||
if (data.level == 'sky') {
|
||||
data.immerseType = 'c51'
|
||||
data.immerseType = query.immerseType || 'c51'
|
||||
}
|
||||
return request(
|
||||
`/api/song/enhance/player/url/v1`,
|
||||
|
||||
20
module/thinktank_audit_resource_detail.js
Normal file
20
module/thinktank_audit_resource_detail.js
Normal file
@ -0,0 +1,20 @@
|
||||
// 云小编获取任务
|
||||
//
|
||||
// type:
|
||||
// 1: 歌曲曲风审核 musicalStyleEnter
|
||||
// 2: 歌曲语种审核 languageEnter
|
||||
// 3: 歌曲原唱审核 oriSingerEnter
|
||||
// 4: 情绪标签审核 emotionEnter
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
type: query.type || '4',
|
||||
}
|
||||
return request(
|
||||
`/api/thinktank/audit/resource/detail`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
36
module/thinktank_audit_resource_update.js
Normal file
36
module/thinktank_audit_resource_update.js
Normal file
@ -0,0 +1,36 @@
|
||||
// 云小编提交任务
|
||||
//
|
||||
// type:
|
||||
// 1: 歌曲曲风审核 musicalStyleEnter
|
||||
// 2: 歌曲语种审核 languageEnter
|
||||
// 3: 歌曲原唱审核 oriSingerEnter
|
||||
// 4: 情绪标签审核 emotionEnter
|
||||
//
|
||||
// taskId:
|
||||
// 调用 thinktank/audit/resource/detail 获取
|
||||
//
|
||||
// judgement:
|
||||
// 1: 同意, 2: 否决, 3: 跳过 (不算次数)
|
||||
|
||||
const createOption = require('../util/option.js')
|
||||
|
||||
module.exports = (query, request) => {
|
||||
if (!query.taskId || !query.judgement)
|
||||
return Promise.reject({
|
||||
status: 400,
|
||||
body: {
|
||||
code: 400,
|
||||
message: '参数不足',
|
||||
},
|
||||
})
|
||||
const data = {
|
||||
type: query.type || '4',
|
||||
taskId: query.taskId,
|
||||
judgement: query.judgement,
|
||||
}
|
||||
return request(
|
||||
`/api/thinktank/audit/resource/update`,
|
||||
data,
|
||||
createOption(query, 'eapi'),
|
||||
)
|
||||
}
|
||||
@ -4,9 +4,10 @@ const createOption = require('../util/option.js')
|
||||
module.exports = (query, request) => {
|
||||
const data = {
|
||||
getcounts: true,
|
||||
time: query.lasttime || -1,
|
||||
limit: query.limit || 30,
|
||||
time: query.lasttime ?? -1,
|
||||
limit: query.limit ?? 30,
|
||||
total: false,
|
||||
fromRN: 'true',
|
||||
}
|
||||
return request(`/api/event/get/${query.uid}`, data, createOption(query))
|
||||
}
|
||||
|
||||
135
module/user_event_all.js
Normal file
135
module/user_event_all.js
Normal file
@ -0,0 +1,135 @@
|
||||
// 获取当前登录用户可被上游枚举的全部动态
|
||||
|
||||
const userAccount = require('./user_account.js')
|
||||
const userEvent = require('./user_event.js')
|
||||
|
||||
const PAGE_SIZE = 100
|
||||
const MAX_PAGES = 1000
|
||||
|
||||
const errorResponse = (message, cookie = []) => ({
|
||||
status: 502,
|
||||
body: {
|
||||
code: 502,
|
||||
message,
|
||||
},
|
||||
cookie,
|
||||
})
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const accountResult = await userAccount(query, request)
|
||||
const uid =
|
||||
accountResult.body?.account?.id || accountResult.body?.profile?.userId
|
||||
|
||||
if (!uid) {
|
||||
if (
|
||||
accountResult.status !== 200 ||
|
||||
(accountResult.body?.code && accountResult.body.code !== 200)
|
||||
) {
|
||||
return accountResult
|
||||
}
|
||||
|
||||
return {
|
||||
status: 401,
|
||||
body: {
|
||||
code: 401,
|
||||
message: 'A valid login cookie is required',
|
||||
},
|
||||
cookie: accountResult.cookie || [],
|
||||
}
|
||||
}
|
||||
|
||||
const cookies = [...(accountResult.cookie || [])]
|
||||
const events = []
|
||||
const eventIds = new Set()
|
||||
const cursors = new Set()
|
||||
let lasttime = -1
|
||||
let more = true
|
||||
let pageCount = 0
|
||||
let size = null
|
||||
|
||||
while (more) {
|
||||
pageCount += 1
|
||||
|
||||
const pageResult = await userEvent(
|
||||
{
|
||||
...query,
|
||||
uid,
|
||||
lasttime,
|
||||
limit: PAGE_SIZE,
|
||||
},
|
||||
request,
|
||||
)
|
||||
|
||||
cookies.push(...(pageResult.cookie || []))
|
||||
|
||||
if (pageResult.status !== 200 || pageResult.body?.code !== 200) {
|
||||
return {
|
||||
...pageResult,
|
||||
cookie: cookies,
|
||||
}
|
||||
}
|
||||
|
||||
if (pageCount === 1) {
|
||||
const reportedSize = pageResult.body.size
|
||||
const numericSize = Number(reportedSize)
|
||||
size =
|
||||
reportedSize != null && Number.isFinite(numericSize)
|
||||
? numericSize
|
||||
: null
|
||||
}
|
||||
|
||||
for (const event of pageResult.body.events || []) {
|
||||
if (event?.id == null) {
|
||||
events.push(event)
|
||||
continue
|
||||
}
|
||||
|
||||
const eventId = String(event.id)
|
||||
if (!eventIds.has(eventId)) {
|
||||
eventIds.add(eventId)
|
||||
events.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
more = Boolean(pageResult.body.more)
|
||||
lasttime = pageResult.body.lasttime
|
||||
|
||||
if (more) {
|
||||
const cursor = String(lasttime ?? '')
|
||||
if (!cursor || cursors.has(cursor)) {
|
||||
return errorResponse(
|
||||
'Upstream event pagination cursor stalled',
|
||||
cookies,
|
||||
)
|
||||
}
|
||||
cursors.add(cursor)
|
||||
}
|
||||
|
||||
if (more && pageCount >= MAX_PAGES) {
|
||||
return errorResponse(
|
||||
`Upstream event pagination exceeded ${MAX_PAGES} pages`,
|
||||
cookies,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const retrievedCount = events.length
|
||||
const unavailableCount =
|
||||
size == null ? null : Math.max(size - retrievedCount, 0)
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
events,
|
||||
size,
|
||||
retrievedCount,
|
||||
unavailableCount,
|
||||
sizeMismatch: size == null ? null : size !== retrievedCount,
|
||||
pageCount,
|
||||
more: false,
|
||||
lasttime: lasttime ?? null,
|
||||
},
|
||||
cookie: cookies,
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ const { default: axios } = require('axios')
|
||||
const fs = require('fs')
|
||||
var xml2js = require('xml2js')
|
||||
|
||||
const uploadPlugin = require('../plugins/upload')
|
||||
const createOption = require('../util/option.js')
|
||||
const { getFileExtension, readFileChunk } = require('../util/fileHelper')
|
||||
|
||||
@ -19,15 +20,7 @@ function createDupkey() {
|
||||
return s.join('')
|
||||
}
|
||||
|
||||
module.exports = async (query, request) => {
|
||||
const ext = getFileExtension(query.songFile.name)
|
||||
const filename =
|
||||
query.songName ||
|
||||
query.songFile.name
|
||||
.replace('.' + ext, '')
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '_')
|
||||
|
||||
module.exports = async (query, request, dependencies = {}) => {
|
||||
if (!query.songFile) {
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
@ -38,6 +31,19 @@ module.exports = async (query, request) => {
|
||||
})
|
||||
}
|
||||
|
||||
const axiosRequest = dependencies.axios || axios
|
||||
const uploadImage = dependencies.uploadPlugin || uploadPlugin
|
||||
const ext = getFileExtension(query.songFile.name)
|
||||
const filename =
|
||||
query.songName ||
|
||||
query.songFile.name
|
||||
.replace('.' + ext, '')
|
||||
.replace(/\s/g, '')
|
||||
.replace(/\./g, '_')
|
||||
const coverImgId = query.imgFile
|
||||
? (await uploadImage(query, request)).imgId
|
||||
: query.coverImgId
|
||||
|
||||
const tokenRes = await request(
|
||||
`/api/nos/token/alloc`,
|
||||
{
|
||||
@ -53,7 +59,7 @@ module.exports = async (query, request) => {
|
||||
|
||||
const objectKey = tokenRes.body.result.objectKey.replace(/\//g, '%2F')
|
||||
const docId = tokenRes.body.result.docId
|
||||
const res = await axios({
|
||||
const res = await axiosRequest({
|
||||
method: 'post',
|
||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploads`,
|
||||
headers: {
|
||||
@ -94,7 +100,7 @@ module.exports = async (query, request) => {
|
||||
)
|
||||
}
|
||||
|
||||
const res3 = await axios({
|
||||
const res3 = await axiosRequest({
|
||||
method: 'put',
|
||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?partNumber=${blockIndex}&uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||
headers: {
|
||||
@ -117,7 +123,7 @@ module.exports = async (query, request) => {
|
||||
}
|
||||
completeStr += '</CompleteMultipartUpload>'
|
||||
|
||||
await axios({
|
||||
await axiosRequest({
|
||||
method: 'post',
|
||||
url: `https://ymusic.nos-hz.163yun.com/${objectKey}?uploadId=${res2.InitiateMultipartUploadResult.UploadId[0]}`,
|
||||
headers: {
|
||||
@ -128,29 +134,29 @@ module.exports = async (query, request) => {
|
||||
data: completeStr,
|
||||
})
|
||||
|
||||
await request(
|
||||
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
||||
{
|
||||
dupkey: createDupkey(),
|
||||
voiceData: JSON.stringify([
|
||||
const voiceData = JSON.stringify([
|
||||
{
|
||||
name: filename,
|
||||
autoPublish: query.autoPublish == 1 ? true : false,
|
||||
autoPublishText: query.autoPublishText || '',
|
||||
description: query.description,
|
||||
voiceListId: query.voiceListId,
|
||||
coverImgId: query.coverImgId,
|
||||
coverImgId,
|
||||
dfsId: docId,
|
||||
categoryId: query.categoryId,
|
||||
secondCategoryId: query.secondCategoryId,
|
||||
composedSongs: query.composedSongs
|
||||
? query.composedSongs.split(',')
|
||||
: [],
|
||||
composedSongs: query.composedSongs ? query.composedSongs.split(',') : [],
|
||||
privacy: query.privacy == 1 ? true : false,
|
||||
publishTime: query.publishTime || 0,
|
||||
orderNo: query.orderNo || 1,
|
||||
},
|
||||
]),
|
||||
])
|
||||
|
||||
await request(
|
||||
`/api/voice/workbench/voice/batch/upload/preCheck`,
|
||||
{
|
||||
dupkey: createDupkey(),
|
||||
voiceData,
|
||||
},
|
||||
{
|
||||
...createOption(query),
|
||||
@ -163,25 +169,7 @@ module.exports = async (query, request) => {
|
||||
`/api/voice/workbench/voice/batch/upload/v2`,
|
||||
{
|
||||
dupkey: createDupkey(),
|
||||
voiceData: JSON.stringify([
|
||||
{
|
||||
name: filename,
|
||||
autoPublish: query.autoPublish == 1 ? true : false,
|
||||
autoPublishText: query.autoPublishText || '',
|
||||
description: query.description,
|
||||
voiceListId: query.voiceListId,
|
||||
coverImgId: query.coverImgId,
|
||||
dfsId: docId,
|
||||
categoryId: query.categoryId,
|
||||
secondCategoryId: query.secondCategoryId,
|
||||
composedSongs: query.composedSongs
|
||||
? query.composedSongs.split(',')
|
||||
: [],
|
||||
privacy: query.privacy == 1 ? true : false,
|
||||
publishTime: query.publishTime || 0,
|
||||
orderNo: query.orderNo || 1,
|
||||
},
|
||||
]),
|
||||
voiceData,
|
||||
},
|
||||
{
|
||||
...createOption(query),
|
||||
|
||||
@ -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'),
|
||||
)
|
||||
}
|
||||
|
||||
26
module/yunbei_task_finish_v1.js
Normal file
26
module/yunbei_task_finish_v1.js
Normal file
@ -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'),
|
||||
)
|
||||
}
|
||||
16
module/yunbei_task_list_v1.js
Normal file
16
module/yunbei_task_list_v1.js
Normal file
@ -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'),
|
||||
)
|
||||
}
|
||||
20
module/yunbei_task_recommend_song.js
Normal file
20
module/yunbei_task_recommend_song.js
Normal file
@ -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'),
|
||||
)
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import { banner, lyric } from '@neteasecloudmusicapienhanced/api'
|
||||
import { logger } from '../util/logger.js'
|
||||
banner({ type: 0 }).then((res) => {
|
||||
import type { Response } from '@neteasecloudmusicapienhanced/api'
|
||||
import logger from '../util/logger.js'
|
||||
banner({ type: 0 }).then((res: Response) => {
|
||||
logger.info(res)
|
||||
})
|
||||
lyric({
|
||||
id: '33894312',
|
||||
}).then((res) => {
|
||||
}).then((res: Response) => {
|
||||
logger.info(res)
|
||||
})
|
||||
|
||||
48
module_example/yunbei_ad_task.js
Normal file
48
module_example/yunbei_ad_task.js
Normal file
@ -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()
|
||||
19
package.json
19
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neteasecloudmusicapienhanced/api",
|
||||
"version": "4.37.0",
|
||||
"version": "4.40.0",
|
||||
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
|
||||
"scripts": {
|
||||
"dev": "nodemon app.js",
|
||||
@ -75,13 +75,14 @@
|
||||
"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",
|
||||
"express-fileupload": "^1.5.2",
|
||||
"gzip": "^0.1.0",
|
||||
"jsdom": "^24.1.3",
|
||||
"music-metadata": "^11.14.0",
|
||||
"node-forge": "^1.4.0",
|
||||
"pac-proxy-agent": "^7.2.0",
|
||||
@ -89,7 +90,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,21 +99,21 @@
|
||||
"@types/express-fileupload": "^1.5.1",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "25.9.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.64.0",
|
||||
"@typescript-eslint/parser": "^8.64.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",
|
||||
"prettier": "^3.9.5",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
612
pnpm-lock.yaml
generated
612
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -255,9 +255,13 @@
|
||||
}
|
||||
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
|
||||
for (var song of resp.data.result) {
|
||||
logs.write(
|
||||
`[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
|
||||
)
|
||||
var a = document.createElement('a');
|
||||
a.href = 'https://music.163.com/song?id=' + encodeURIComponent(song.song.id);
|
||||
a.target = '_blank';
|
||||
a.textContent = song.song.name + ' - ' + song.song.album.name + ' (' + (song.startTime / 1000) + 's)';
|
||||
logs.appendChild(document.createTextNode('[result] '));
|
||||
logs.appendChild(a);
|
||||
logs.appendChild(document.createElement('br'));
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@ -283,7 +283,9 @@ AI 生成的图,仅供娱乐()
|
||||
|
||||
`md5_password`: md5 加密后的密码,传入后 `password` 参数将失效
|
||||
|
||||
`captcha`: 验证码,使用 [`/captcha/sent`](#发送验证码)接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 `password` 参数将失效
|
||||
`captcha`: 验证码,使用 `/captcha/sent` 或 `/captcha/sent/v1`接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录,传入后 `password` 参数将失效
|
||||
|
||||
`sca`: 网易易盾滑块验证token, 获取方式未知
|
||||
|
||||
**接口地址 :** `/login/cellphone`
|
||||
|
||||
@ -397,6 +399,19 @@ body {
|
||||
|
||||
**调用例子 :** `/captcha/sent?phone=13xxx`
|
||||
|
||||
### 新版发送验证码
|
||||
|
||||
说明 : 调用此接口 ,传入手机号码, 可发送验证码
|
||||
|
||||
**必选参数 :** `phone`: 手机号码
|
||||
|
||||
**可选参数 :**
|
||||
`ctcode`: 国家区号,默认 86 即中国
|
||||
|
||||
**接口地址 :** `/captcha/sent/v1`
|
||||
|
||||
**调用例子 :** `/captcha/sent/v1?phone=13xxx`
|
||||
|
||||
### 验证验证码
|
||||
|
||||
说明 : 调用此接口 ,传入手机号码和验证码, 可校验验证码是否正确
|
||||
@ -811,6 +826,8 @@ tags: 歌单标签
|
||||
|
||||
`lasttime` : 返回数据的 `lasttime` ,默认-1,传入上一次返回结果的 lasttime,将会返回下一页的数据
|
||||
|
||||
接口会传入官方客户端使用的 `fromRN=true`,登录用户读取自己的动态时可获得网易云允许本人查看的非公开动态。返回的 `size` 是网易云账户侧统计值,不保证等于分页后实际可获取的动态数量。
|
||||
|
||||
**接口地址 :** `/user/event`
|
||||
|
||||
**调用例子 :** `/user/event?uid=32953014` `/user/event?uid=32953014&limit=1&lasttime=1558011138743`
|
||||
@ -828,6 +845,38 @@ tags: 歌单标签
|
||||
41、21 分享视频
|
||||
```
|
||||
|
||||
### 获取当前登录用户的全部可枚举动态
|
||||
|
||||
说明 : 登录后调用此接口,会组合 `/user/account` 与 `/user/event` 的原子能力:先从 Cookie 取得当前用户 id,再自动跟随 `lasttime` 游标读取至 `more=false`。接口返回网易云允许当前用户本人查看的公开及非公开动态,不能读取其他用户的私密动态。
|
||||
|
||||
本接口不接受 `limit` 或 `lasttime`,一次请求会完成全部上游分页。账号动态较多时,请预留足够的请求时间。
|
||||
|
||||
数量字段说明:
|
||||
|
||||
- `size`:网易云返回的账户统计数量,可能包含已删除、被屏蔽、资源失效或旧类型且不再下发的记录。
|
||||
- `retrievedCount`:本次实际取得的唯一动态数量,始终等于 `events.length`。
|
||||
- `unavailableCount`:`size - retrievedCount` 的正差值;缺失记录没有返回 id,无法继续读取或修改。
|
||||
- `sizeMismatch`:`size` 与 `retrievedCount` 是否不一致。若上游未返回 `size`,相关字段为 `null`。
|
||||
- `pageCount`:本次实际请求的上游分页数量。
|
||||
|
||||
每条动态的 `privacySetting` 表示当前可见权限:`0` 为所有人,`1` 为我关注的人,`2` 为仅自己,`6` 为互相关注的人。
|
||||
|
||||
**接口地址 :** `/user/event/all`
|
||||
|
||||
**调用例子 :** `/user/event/all`
|
||||
|
||||
### 修改动态可见权限
|
||||
|
||||
说明 : 登录后调用此接口,可以修改当前账号本人发布的单条动态的可见权限。此接口只负责一次原子修改;上游没有批量修改接口。如需批量操作,可先调用 `/user/event/all` 并按 `privacySetting` 筛选,再由调用方逐条调用本接口,同时自行处理限速、失败重试和部分成功。
|
||||
|
||||
**必选参数 :** `evId` : 动态 id
|
||||
|
||||
`privacy` : 目标可见权限。`0` 为所有人,`1` 为我关注的人,`2` 为仅自己,`6` 为互相关注的人
|
||||
|
||||
**接口地址 :** `/event/privacy`
|
||||
|
||||
**调用例子 :** `/event/privacy?evId=6712917601&privacy=0`
|
||||
|
||||
### 转发用户动态
|
||||
|
||||
说明 : 登录后调用此接口 ,可以转发用户动态
|
||||
@ -1215,7 +1264,6 @@ tags: 歌单标签
|
||||
|
||||
> 如果你设置 limit=50&offset=100,你就会得到第 101-150 首歌曲
|
||||
|
||||
|
||||
### 歌单详情动态
|
||||
|
||||
说明 : 调用后可获取歌单详情动态部分,如评论数,是否收藏,播放数
|
||||
@ -1259,9 +1307,11 @@ tags: 歌单标签
|
||||
`lossless`=>`无损`, `hires`=>`Hi-Res`, `jyeffect` => `高清环绕声`, `sky` => `沉浸环绕声`, `dolby` => `杜比全景声`, `jymaster` => `超清母带`
|
||||
`unblock`: 是否使用使用歌曲解锁, 分为`true`和`false`
|
||||
|
||||
**可选参数 :** `immerseType`: 沉浸声环绕声类型, 分为 `c51` => `c51类型`, `ste` => `环绕立体声类型`, `aac` => `aac类型`, 仅在 `level=sky` 时生效, 默认为 `c51`
|
||||
|
||||
**接口地址 :** `/song/url/v1`
|
||||
|
||||
**调用例子 :** `/song/url/v1?id=1969519579&level=exhigh` `/song/url/v1?id=1969519579,33894312&level=lossless`
|
||||
**调用例子 :** `/song/url/v1?id=1969519579&level=exhigh` `/song/url/v1?id=1969519579,33894312&level=lossless` `/song/url/v1?id=1969519579&level=sky&immerseType=ste`
|
||||
|
||||
说明:`杜比全景声`音质需要设备支持,不同的设备可能会返回不同码率的 url。cookie 需要传入`os=pc`保证返回正常码率的 url。
|
||||
|
||||
@ -1395,8 +1445,6 @@ tags: 歌单标签
|
||||
|
||||
说明 : 调用此接口 , 传入类型和歌单 id 可收藏歌单或者取消收藏歌单
|
||||
|
||||
!> 警告: 在`v4.29.7`版本后, 在网易云登陆后请求要带上`timestamp`字段, 否则会导致请求不合法
|
||||
|
||||
**必选参数 :**
|
||||
|
||||
`t` : 类型,1:收藏,2:取消收藏
|
||||
@ -3577,6 +3625,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`
|
||||
|
||||
### 云贝收入
|
||||
|
||||
说明 :登录后调用此接口可获取云贝收入
|
||||
@ -4322,27 +4400,33 @@ ONLINE 已发布
|
||||
|
||||
### 播客上传声音
|
||||
|
||||
说明: 可以上传声音到播客,例子在 `/public/voice_upload.html` 访问地址: <a href="/voice_upload.html" target="_blank">/voice_upload.html</a>
|
||||
说明: 登录后调用此接口,使用`'Content-Type': 'multipart/form-data'`上传声音文件 formData(name 为`songFile`),可通过 formData(name 为`imgFile`)同时上传声音封面。例子在 `/public/voice_upload.html` 访问地址: <a href="/voice_upload.html" target="_blank">/voice_upload.html</a>
|
||||
|
||||
**接口地址:** `/voice/upload`
|
||||
|
||||
**必选参数:**
|
||||
`voiceListId`: 播客 id
|
||||
|
||||
`coverImgId`: 播客封面
|
||||
`songFile`: 声音文件
|
||||
|
||||
`voiceListId`: 播客 id
|
||||
|
||||
`categoryId`: 分类 id
|
||||
|
||||
`secondCategoryId`:次级分类 id
|
||||
`secondCategoryId`: 次级分类 id
|
||||
|
||||
`description`: 声音介绍
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`imgFile`: 声音封面图片文件,上传后会自动生成图片 id。与`coverImgId`同时传入时,优先使用`imgFile`
|
||||
|
||||
`coverImgId`: 已上传的声音封面图片 id,未传入`imgFile`时使用该值
|
||||
|
||||
`songName`: 声音名称
|
||||
|
||||
`privacy`: 设为隐私声音,播客如果是隐私博客,则必须设为 1
|
||||
`privacy`: 设为隐私声音,播客如果是隐私播客,则必须设为 1
|
||||
|
||||
`publishTime`:默认立即发布,定时发布的话需传入时间戳
|
||||
`publishTime`: 默认立即发布,定时发布的话需传入时间戳
|
||||
|
||||
`autoPublish`: 是否发布动态,是则传入 1
|
||||
|
||||
@ -5139,7 +5223,6 @@ let data = encodeURIComponent(
|
||||
|
||||
**调用例子:** `/broadcast/sub?id=5&t=1`
|
||||
|
||||
|
||||
### 用户的创建歌单列表
|
||||
|
||||
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
|
||||
@ -5214,7 +5297,6 @@ let data = encodeURIComponent(
|
||||
|
||||
**调用例子 :** `/voicelist/my/created`
|
||||
|
||||
|
||||
### DIFM电台 - 分类
|
||||
|
||||
说明: 调用此接口, 获取DIFM电台分类
|
||||
@ -5393,7 +5475,7 @@ let data = encodeURIComponent(
|
||||
|
||||
**接口地址 :** `/comment/report`
|
||||
|
||||
**调用例子 :* `/comment/report?id=2058263032&cid=123456789&reason=人身攻击`
|
||||
*_调用例子 :_ `/comment/report?id=2058263032&cid=123456789&reason=人身攻击`
|
||||
|
||||
### 多级行政区划数据
|
||||
|
||||
@ -5473,7 +5555,284 @@ let data = encodeURIComponent(
|
||||
|
||||
**接口地址 :** `/song/cloud/download`
|
||||
|
||||
**调用例子 :** `/song/cloud/download?id=123456789`
|
||||
**调用例子 :** `/song/cloud/download?id=123456789`'
|
||||
|
||||
### 获取广告
|
||||
|
||||
说明 : 调用此接口, 可获取广告
|
||||
|
||||
**接口地址 :** `/ad/get`
|
||||
|
||||
**调用例子 :** `/ad/get`
|
||||
|
||||
### 看广告领取权益(免费听歌时长 / 云贝等)
|
||||
|
||||
说明 : 登录后调用此接口, 领取广告权益。权益类型由广告平台下发的配置决定, 不仅限于 30 分钟免费听歌时长, 还包括"看视频获得最高 2000 云贝"等拉新分段权益(`rightsGainMethod=6`)。除下方常用参数外, 权益类型/时长/扩展权益等其余字段会自动从广告下发配置补齐, 无需手动传入。
|
||||
|
||||
!> 警告: 通过调取接口出现的任何问题由调用者自行承担
|
||||
|
||||
**可选参数 :**
|
||||
|
||||
`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`
|
||||
|
||||
**调用例子 :** `/ad/listening/rights/gain`
|
||||
|
||||
### 获取免费听时长状态
|
||||
|
||||
说明 : 登录后调用此接口, 获取免费听剩余时长
|
||||
|
||||
**接口地址 :** `/ad/listening/rights`
|
||||
|
||||
**调用例子 :** `/ad/listening/rights`
|
||||
|
||||
### 云小编 - 获取用户详情
|
||||
|
||||
说明: 登录后调用此接口, 获取云小编用户详情
|
||||
|
||||
**接口地址:** `/rep/ugc/user/get`
|
||||
|
||||
**调用例子:** `/rep/ugc/user/get`
|
||||
|
||||
### 云小编 - 每日签到
|
||||
|
||||
说明: 登录后调用此接口, 进行云小编签到, 领取 5 积分, 签到后 `/rep/ugc/user/get` 返回 `data.signed = 1`
|
||||
|
||||
**接口地址:** `/rep/ugc/user/sign`
|
||||
|
||||
**调用例子:** `/rep/ugc/user/sign`
|
||||
|
||||
### 云小编 - 查询会员任务状态
|
||||
|
||||
说明: 登录后调用此接口, 查询云小编会员任务状态, 当 `data.status = 20` 时, 可调用 `/rep/ugc/user/collect-vip` 领取会员
|
||||
|
||||
**接口地址:** `/rep/ugc/user/vip`
|
||||
|
||||
**调用例子:** `/rep/ugc/user/vip`
|
||||
|
||||
### 云小编 - 活动信息
|
||||
|
||||
说明: 登录后调用此接口, 查询云小编会员活动信息
|
||||
|
||||
**接口地址:** `/rep/ugc/activity/get`
|
||||
|
||||
**调用例子:** `/rep/ugc/activity/get`
|
||||
|
||||
### 云小编 - 获取任务
|
||||
|
||||
> 注意: 调用前请先使用官方客户端完成云小编“情绪标签审核”入站考试
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`type`: 任务类型, 1: 歌曲曲风审核, 2: 歌曲语种审核, 3: 歌曲原唱审核, 4: 情绪标签审核, 默认 `4`
|
||||
|
||||
**接口地址:** `/thinktank/audit/resource/detail`
|
||||
|
||||
**调用例子:** `/thinktank/audit/resource/detail?type=4`
|
||||
|
||||
### 云小编 - 提交任务
|
||||
|
||||
> 注意: 投票结果会在后台审核, 一致 +3 积分, 不一致 -2 积分
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`type`: 任务类型, 1: 歌曲曲风审核, 2: 歌曲语种审核, 3: 歌曲原唱审核, 4: 情绪标签审核, 默认 `4`
|
||||
|
||||
**必选参数:**
|
||||
|
||||
`taskId`: 任务 ID, 调用 `/thinktank/audit/resource/detail` 获取 `data.taskId`
|
||||
|
||||
`judgement`: 审核结果, 1: 同意, 2: 否决, 3: 跳过 (不算次数)
|
||||
|
||||
**接口地址:** `/thinktank/audit/resource/update`
|
||||
|
||||
**调用例子:** `/thinktank/audit/resource/update?type=4&taskId=123456&judgement=1`
|
||||
|
||||
### 云小编 - 领取任务积分
|
||||
|
||||
说明: 完成任务后调用此接口, 领取云小编任务积分
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`activityId`: 活动 ID, 调用 `/rep/ugc/activity/get` 获取, 默认 `5001`
|
||||
|
||||
**接口地址:** `/rep/ugc/activity/collect`
|
||||
|
||||
**调用例子:** `/rep/ugc/activity/collect?activityId=5001`
|
||||
|
||||
### 云小编 - 领取一日会员
|
||||
|
||||
说明: 达成领取条件 (`/rep/ugc/user/vip`) 后调用此接口, 领取一日会员
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`activityId`: 活动 ID, 调用 `/rep/ugc/activity/get` 获取, 默认 `5001`
|
||||
|
||||
**接口地址:** `/rep/ugc/user/collect-vip`
|
||||
|
||||
**调用例子:** `/rep/ugc/user/collect-vip?activityId=5001`
|
||||
|
||||
### 云小编 - 剩余抽奖次数
|
||||
|
||||
说明: 登录后调用此接口, 获取今日云小编抽奖剩余次数
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`activityId`: 活动 ID, 默认 `6501202`
|
||||
|
||||
**接口地址:** `/middle/play/lottery/remain/chance`
|
||||
|
||||
**调用例子:** `/middle/play/lottery/remain/chance?activityId=6501202`
|
||||
|
||||
### 云小编 - 每日抽奖
|
||||
|
||||
说明: 登录后调用此接口, 消耗 200 积分进行抽奖, 每日最多抽 3 次
|
||||
|
||||
> 注意: 抽奖失败也消耗每日次数, 请先调用 `/rep/ugc/user/get` 查询可用积分
|
||||
|
||||
**可选参数:**
|
||||
|
||||
`activityId`: 活动 ID, 默认 `6501202`
|
||||
|
||||
`drawCount`: 未知, 默认 `1`
|
||||
|
||||
`checkToken`: 易盾反作弊 Token, 默认自动获取
|
||||
|
||||
**接口地址:** `/middle/play/do/lottery`
|
||||
|
||||
**调用例子:** `/middle/play/do/lottery?activityId=6501202&drawCount=1`
|
||||
|
||||
### 发送/删除评论
|
||||
|
||||
说明 : 调用此接口,可发送评论或者删除评论
|
||||
|
||||
1. 发送评论
|
||||
|
||||
**必选参数**
|
||||
|
||||
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
|
||||
|
||||
```
|
||||
0: 歌曲
|
||||
|
||||
1: mv
|
||||
|
||||
2: 歌单
|
||||
|
||||
3: 专辑
|
||||
|
||||
4: 电台
|
||||
|
||||
5: 视频
|
||||
|
||||
6: 动态
|
||||
```
|
||||
|
||||
`id`: 对应资源 id
|
||||
|
||||
`content`: 要发送的内容
|
||||
|
||||
**调用例子** : `/comment/add?type=1&id=5436712&content=test` (往广岛之恋 mv 发送评论: test)
|
||||
|
||||
2. 回复评论
|
||||
|
||||
**必选参数**
|
||||
|
||||
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
|
||||
|
||||
```
|
||||
0: 歌曲
|
||||
|
||||
1: mv
|
||||
|
||||
2: 歌单
|
||||
|
||||
3: 专辑
|
||||
|
||||
4: 电台
|
||||
|
||||
5: 视频
|
||||
|
||||
6: 动态
|
||||
```
|
||||
|
||||
`id`: 对应资源 id
|
||||
|
||||
`cid`: 评论 id
|
||||
|
||||
`content`: 要发送的内容
|
||||
|
||||
**调用例子** : `/comment/add?type=1&id=5436712&cid=1535550516319&content=test` (往广岛之恋 mv 回复test评论: test)
|
||||
|
||||
3. 删除评论
|
||||
|
||||
**必选参数**
|
||||
|
||||
`type`: 数字,资源类型,对应歌曲,mv,专辑,歌单,电台,视频对应以下类型
|
||||
|
||||
```
|
||||
0: 歌曲
|
||||
|
||||
1: mv
|
||||
|
||||
2: 歌单
|
||||
|
||||
3: 专辑
|
||||
|
||||
4: 电台节目
|
||||
|
||||
5: 视频
|
||||
|
||||
6: 动态
|
||||
|
||||
7: 电台
|
||||
|
||||
```
|
||||
|
||||
`id`: 对应资源 id
|
||||
|
||||
`cid`: 评论 id
|
||||
|
||||
**调用例子** : `/comment?type=1&id=5436712&cid=1535550516319` (在广岛之恋 mv 删除评论)
|
||||
|
||||
### 获取在线设备列表
|
||||
|
||||
说明: 登录后调用此接口, 获取在线设备列表
|
||||
|
||||
**接口地址:** `/device/list`
|
||||
|
||||
**调用例子:** `/device/list`
|
||||
|
||||
### 发送安全验证码
|
||||
|
||||
说明: 登录后调用此接口, 传入手机号, 可发送安全验证码
|
||||
|
||||
**必选参数 :** `phone`: 手机号
|
||||
|
||||
**接口地址 :** `/captcha/safe/sent`
|
||||
|
||||
**调用例子 :** `/captcha/safe/sent?phone=13XXXXXXXXX`
|
||||
|
||||
### 强制下线设备
|
||||
|
||||
说明: 登录后调用此接口, 传入设备 id, 可强制下线设备的登录会话
|
||||
|
||||
**必选参数 :** `key`: 设备的 `deviceKey`, 可通过 `/device/list` 获取
|
||||
|
||||
`captcha`: 安全验证码, 可通过 `/captcha/safe/sent` 获取
|
||||
|
||||
**接口地址 :** `/device/kickoff`
|
||||
|
||||
**调用例子 :** `/device/kickoff?key=00ALDFGEXXXXXXXXXXXXXXXXX&captcha=1234`
|
||||
|
||||
## 离线访问此文档
|
||||
|
||||
|
||||
@ -56,6 +56,15 @@
|
||||
footer.site-footer a:hover { border-bottom: 1px dotted var(--fg); }
|
||||
.info-grid { display: grid; grid-template-columns: 80px 1fr; gap: 4px 12px; font-size: 13px; margin-top: 8px; }
|
||||
.info-grid .lbl { color: var(--muted); }
|
||||
|
||||
/* ---- 剩余时长计时器 ---- */
|
||||
.timer-wrap { text-align: center; padding: 12px 0 4px; }
|
||||
.timer-row { display: flex; align-items: baseline; justify-content: center; gap: 2px; font-variant-numeric: tabular-nums; }
|
||||
.timer-num { font-size: 36px; font-weight: 700; letter-spacing: 1px; line-height: 1.2; font-family: 'SF Mono', 'Courier New', monospace; min-width: 2ch; text-align: center; }
|
||||
.timer-label { font-size: 13px; color: var(--muted); margin-right: 12px; }
|
||||
.timer-label:last-child { margin-right: 0; }
|
||||
.timer-sep { font-size: 28px; font-weight: 300; color: #bbb; margin: 0 2px; }
|
||||
.timer-unit { display: flex; flex-direction: column; align-items: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -141,6 +150,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 剩余时长计时器 -->
|
||||
<div class="block" id="rightsBlock">
|
||||
<div class="flex sb">
|
||||
<h3 id="rightsTitleDisplay">🕐 免费听时长剩余</h3>
|
||||
<span id="rightsTag" class="tag no">未查询</span>
|
||||
</div>
|
||||
<p class="desc" id="rightsSubDesc">查询你的免费听权益剩余时长</p>
|
||||
<div class="timer-wrap">
|
||||
<div class="timer-row" id="timerRow">
|
||||
<div class="timer-unit">
|
||||
<span class="timer-num" id="timerDays">--</span>
|
||||
<span class="timer-label">天</span>
|
||||
</div>
|
||||
<span class="timer-sep">:</span>
|
||||
<div class="timer-unit">
|
||||
<span class="timer-num" id="timerHours">--</span>
|
||||
<span class="timer-label">时</span>
|
||||
</div>
|
||||
<span class="timer-sep">:</span>
|
||||
<div class="timer-unit">
|
||||
<span class="timer-num" id="timerMinutes">--</span>
|
||||
<span class="timer-label">分</span>
|
||||
</div>
|
||||
<span class="timer-sep">:</span>
|
||||
<div class="timer-unit">
|
||||
<span class="timer-num" id="timerSeconds">--</span>
|
||||
<span class="timer-label">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="justify-content:center">
|
||||
<button id="queryRightsBtn" class="primary">⟳ 查询剩余时长</button>
|
||||
<button id="claim30Btn">领取30分钟</button>
|
||||
</div>
|
||||
<div class="info-grid" id="rightsInfo" style="display:none">
|
||||
<div class="lbl">状态</div><div id="rightsStatus">-</div>
|
||||
<div class="lbl">结束时间</div><div id="rightsEndTimeDisplay">-</div>
|
||||
<div class="lbl">今日已覆盖</div><div id="rightsCoverToday">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<a href="/">← 返回首页</a>
|
||||
</footer>
|
||||
@ -172,7 +222,7 @@
|
||||
setTag('s1tag', 'busy', '注册中…')
|
||||
setNum('s1num', false)
|
||||
try {
|
||||
const res = await fetch(nocache(base + '/register/checktoken?refresh=1'))
|
||||
const res = await fetch(nocache(base + '/register/checktoken/v3?refresh=1'))
|
||||
const data = await res.json()
|
||||
$('s1pre').textContent = JSON.stringify(data, null, 2)
|
||||
if (data.token) {
|
||||
@ -260,6 +310,8 @@
|
||||
$('s3info').style.display = 'grid'
|
||||
const body = data.data || {}
|
||||
$('s3result').textContent = JSON.stringify(body).substring(0, 200)
|
||||
// 领取成功后自动刷新剩余时长
|
||||
queryRights()
|
||||
return true
|
||||
}
|
||||
setTag('s3tag', 'no', '领取失败')
|
||||
@ -306,7 +358,139 @@
|
||||
$('step3').disabled = true
|
||||
setTag('allTag', 'no', '就绪')
|
||||
$('runAll').disabled = false
|
||||
|
||||
// 重置计时器
|
||||
stopCountdown()
|
||||
$('timerDays').textContent = '--'
|
||||
$('timerHours').textContent = '--'
|
||||
$('timerMinutes').textContent = '--'
|
||||
$('timerSeconds').textContent = '--'
|
||||
setTag('rightsTag', 'no', '未查询')
|
||||
$('rightsInfo').style.display = 'none'
|
||||
$('rightsTitleDisplay').textContent = '🕐 免费听时长剩余'
|
||||
$('rightsSubDesc').textContent = '查询你的免费听权益剩余时长'
|
||||
$('runAll').disabled = true
|
||||
}
|
||||
|
||||
// ===== 剩余时长计时器 =====
|
||||
let countdownTimer = null
|
||||
let remainingMs = 0
|
||||
|
||||
/** 毫秒 → { d, h, m, s } */
|
||||
function parseMs(ms) {
|
||||
if (ms <= 0) return { d: 0, h: 0, m: 0, s: 0 }
|
||||
const totalSec = Math.floor(ms / 1000)
|
||||
return {
|
||||
d: Math.floor(totalSec / 86400),
|
||||
h: Math.floor((totalSec % 86400) / 3600),
|
||||
m: Math.floor((totalSec % 3600) / 60),
|
||||
s: totalSec % 60
|
||||
}
|
||||
}
|
||||
|
||||
function renderTimer(ms) {
|
||||
const t = parseMs(ms)
|
||||
$('timerDays').textContent = String(t.d).padStart(2, '0')
|
||||
$('timerHours').textContent = String(t.h).padStart(2, '0')
|
||||
$('timerMinutes').textContent = String(t.m).padStart(2, '0')
|
||||
$('timerSeconds').textContent = String(t.s).padStart(2, '0')
|
||||
}
|
||||
|
||||
function stopCountdown() {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown(ms) {
|
||||
stopCountdown()
|
||||
remainingMs = Math.max(0, ms)
|
||||
renderTimer(remainingMs)
|
||||
if (remainingMs <= 0) {
|
||||
setTag('rightsTag', 'no', '已用完')
|
||||
return
|
||||
}
|
||||
countdownTimer = setInterval(() => {
|
||||
remainingMs = Math.max(0, remainingMs - 1000)
|
||||
renderTimer(remainingMs)
|
||||
if (remainingMs <= 0) {
|
||||
stopCountdown()
|
||||
setTag('rightsTag', 'no', '已过期')
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function queryRights() {
|
||||
const btn = $('queryRightsBtn')
|
||||
btn.disabled = true
|
||||
setTag('rightsTag', 'busy', '查询中…')
|
||||
try {
|
||||
const res = await fetch(nocache(base + '/ad/listening/rights'))
|
||||
const data = await res.json()
|
||||
if (data.code === 200 && data.data) {
|
||||
const d = data.data
|
||||
$('rightsTitleDisplay').textContent = '🕐 ' + (d.title || '免费听时长剩余')
|
||||
$('rightsSubDesc').textContent = d.vipInfoContent || '查询你的免费听权益剩余时长'
|
||||
$('rightsInfo').style.display = 'grid'
|
||||
|
||||
// 状态
|
||||
const status = d.status || ''
|
||||
$('rightsStatus').textContent = status
|
||||
|
||||
// 结束时间
|
||||
if (d.rightsEndTime) {
|
||||
const endDate = new Date(d.rightsEndTime)
|
||||
$('rightsEndTimeDisplay').textContent = endDate.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
||||
}
|
||||
|
||||
// 今日已覆盖
|
||||
$('rightsCoverToday').textContent = d.rightsCoverToday ? '是 ✓' : '否'
|
||||
|
||||
// 倒计时
|
||||
if (d.rightsRemainingTime > 0) {
|
||||
startCountdown(d.rightsRemainingTime)
|
||||
setTag('rightsTag', 'ok', '已解锁 ✓')
|
||||
} else {
|
||||
renderTimer(0)
|
||||
setTag('rightsTag', 'no', '已用完')
|
||||
}
|
||||
|
||||
// 领取按钮(仅当 step3 可用时启用)
|
||||
if (d.cardContent && d.cardContent.actionTitle && $('step3').disabled === false) {
|
||||
$('claim30Btn').disabled = false
|
||||
$('claim30Btn').textContent = d.cardContent.actionTitle
|
||||
} else {
|
||||
$('claim30Btn').disabled = true
|
||||
}
|
||||
|
||||
if (d.rightsUpperLimit) {
|
||||
$('rightsStatus').textContent += ' (已达上限)'
|
||||
}
|
||||
|
||||
return data.data
|
||||
}
|
||||
setTag('rightsTag', 'no', '查询失败')
|
||||
return null
|
||||
} catch (e) {
|
||||
setTag('rightsTag', 'no', e.message)
|
||||
return null
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
$('queryRightsBtn').onclick = queryRights
|
||||
|
||||
$('claim30Btn').onclick = async function () {
|
||||
const ok = await step3()
|
||||
if (ok) await queryRights()
|
||||
}
|
||||
|
||||
// 页面加载后自动查询一次
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(queryRights, 300)
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -98,7 +98,10 @@ curl -s {origin}/search?keywords=网易云</code></pre>
|
||||
<a href="/avatar_update.html">头像更新示例</a> ·
|
||||
<a href="/scrobble.html">听歌打卡示例</a> ·
|
||||
<a href="/yidun.html">获取 CheckToken</a> ·
|
||||
<a href="/free_listen.html">免费听权益</a>
|
||||
<a href="/free_listen.html">免费听权益</a> ·
|
||||
<a href="/yunbei_task.html">云贝广告任务</a> ·
|
||||
<a href="/ugc.html">云小编任务中心</a> ·
|
||||
<a href="/ugc_lottery.html">云小编每日抽奖</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
882
public/ugc.html
Normal file
882
public/ugc.html
Normal file
@ -0,0 +1,882 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="docs/netease.png" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="docs/netease.png" />
|
||||
<title>云小编任务中心 - 网易云音乐 API Enhanced</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ---- Section ---- */
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.section:last-of-type {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* ---- Form ---- */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
label .tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
label .tag.r {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
label .tag.o {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
label .tag.g {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="number"]:focus,
|
||||
textarea:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.row .form-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-fill {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.quick-fill span {
|
||||
color: #0066cc;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.quick-fill span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding: 6px 16px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.btn-sm:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-sm.green {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-sm.green:hover {
|
||||
background: #047857;
|
||||
}
|
||||
|
||||
.btn-sm.green:disabled {
|
||||
background: #a7f3d0;
|
||||
}
|
||||
|
||||
.btn-sm.red {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-sm.red:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.btn-sm.red:disabled {
|
||||
background: #fca5a5;
|
||||
}
|
||||
|
||||
.btn-sm.gray {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.btn-sm.gray:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.btn-sm.gray:disabled {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.result.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.result.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.result.info {
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer-link a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ---- Info Card ---- */
|
||||
.info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-card .avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.info-card .info-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-card .btn-sm {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#userPoint {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ---- Song Review ---- */
|
||||
.song-review {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.song-review .cover {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
background: #e5e7eb;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.song-review .song-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.song-review audio {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.song-review .song-tag {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.song-review .btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.song-review .lyric {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
background: #fff;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
white-space: pre-wrap;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>云小编任务中心</h1>
|
||||
<p class="subtitle">管理云音乐审核任务、签到与会员权益领取</p>
|
||||
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<!-- Cookie -->
|
||||
<div class="form-group">
|
||||
<label for="cookie">Cookie <span class="tag o">可选</span></label>
|
||||
<textarea id="cookie" placeholder="留空则默认读取本地存储的登录态" rows="2"></textarea>
|
||||
<div class="quick-fill">
|
||||
<span onclick="loadLocalCookie()">读取本地 Cookie</span>
|
||||
<span onclick="clearCookie()">清除 Cookie</span>
|
||||
<span onclick="saveCookie()">保存 Cookie</span>
|
||||
<span onclick="loadAll()">一键载入</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">用户信息</span>
|
||||
<button class="btn-sm" id="userGet">获取用户详情</button>
|
||||
</div>
|
||||
<div class="info-card" id="user">
|
||||
<img src="docs/netease.png" alt="头像" class="avatar" id="userAvatar" />
|
||||
<span class="info-text" id="userName">未登录</span>
|
||||
<span id="userPoint">0</span>
|
||||
<button class="btn-sm green" id="userSign" disabled>签到</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">会员领取</span>
|
||||
<button class="btn-sm" id="vipGet">获取会员详情</button>
|
||||
</div>
|
||||
<div class="info-card" id="vip">
|
||||
<span class="info-text" id="vipStatus">未载入</span>
|
||||
<button class="btn-sm" id="vipCollect" disabled>领取</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">审核任务</span>
|
||||
<button class="btn-sm" id="taskGet">获取任务详情</button>
|
||||
</div>
|
||||
<div class="info-card" id="task">
|
||||
<span class="info-text" id="taskInfo">[<span id="taskCount">0</span>/<span id="taskGoal">0</span>] 审核歌曲赚贡献分 (+<span id="taskPoint">0</span>)</span>
|
||||
<button class="btn-sm" id="taskStart" disabled>去完成</button>
|
||||
</div>
|
||||
|
||||
<!-- Song Review -->
|
||||
<div class="song-review" id="song" style="display: none;">
|
||||
<img alt="封面" class="cover" id="songCover" />
|
||||
<div class="song-title" id="songTitle">未载入</div>
|
||||
<audio controls id="songAudio"></audio>
|
||||
<div class="song-tag" id="songTag"></div>
|
||||
<div class="btn-group">
|
||||
<button class="btn-sm green" id="songApprove" disabled>同意</button>
|
||||
<button class="btn-sm red" id="songReject" disabled>否决</button>
|
||||
<button class="btn-sm gray" id="songSkip" disabled>跳过 (不计次数)</button>
|
||||
</div>
|
||||
<div class="lyric" id="songLyric">[00:00.00] 暂无歌词</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="result" class="result" style="display: none;"></div>
|
||||
|
||||
<div class="footer-link">
|
||||
<a href="/">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var k_cookie = 'ncm_cookie';
|
||||
var cookie = localStorage.getItem(k_cookie) || '';
|
||||
|
||||
/* ---- DOM refs ---- */
|
||||
var cookieInput = document.getElementById('cookie');
|
||||
var resultDiv = document.getElementById('result');
|
||||
|
||||
/* ---- Init ---- */
|
||||
if (cookie) {
|
||||
cookieInput.value = cookie;
|
||||
}
|
||||
|
||||
/* ---- Cookie ---- */
|
||||
window.loadLocalCookie = function () {
|
||||
var local = localStorage.getItem(k_cookie);
|
||||
if (local) {
|
||||
cookie = local;
|
||||
cookieInput.value = local;
|
||||
showResult('已成功读取本地 Cookie', 'success');
|
||||
} else {
|
||||
showResult('未在本地发现登录 Cookie,请先登录或手动粘贴', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
window.clearCookie = function () {
|
||||
cookie = '';
|
||||
cookieInput.value = '';
|
||||
localStorage.removeItem(k_cookie);
|
||||
showResult('已清除本地 Cookie', 'info');
|
||||
};
|
||||
|
||||
window.saveCookie = function () {
|
||||
var ck = getCookie()
|
||||
if (ck) localStorage.setItem(k_cookie, ck)
|
||||
showResult('已保存本地 Cookie', 'info');
|
||||
}
|
||||
|
||||
/* ---- Result ---- */
|
||||
function hideResult() {
|
||||
resultDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
function showResult(message, type) {
|
||||
resultDiv.textContent = message;
|
||||
resultDiv.className = 'result ' + type;
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function getCookie() {
|
||||
var ck = cookieInput.value.trim();
|
||||
if (ck) cookie = ck;
|
||||
return ck || cookie || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起请求 (兼容旧版)
|
||||
* @param {string} url
|
||||
* @param {Record<string,string>} params
|
||||
* @param {(resp:Record<string,any>)=>void} done
|
||||
*/
|
||||
function request(url, params, done) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.timeout = 1e4;
|
||||
xhr.onload = function () {
|
||||
var r = xhr.response;
|
||||
if (r.code !== 200) {
|
||||
showResult('错误 ' + r.code + ': ' + (r.message || '未知错误'), 'error');
|
||||
} else {
|
||||
done(r);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
showResult('网络请求失败: ' + xhr.statusText, 'error');
|
||||
};
|
||||
var ck = getCookie();
|
||||
if (ck) params.cookie = ck;
|
||||
var s = '?_=' + Date.now();
|
||||
for (var k in params) {
|
||||
s += '&' + k + '=' + encodeURIComponent(params[k]);
|
||||
}
|
||||
xhr.open('GET', url + s);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
/// 用户信息 ///
|
||||
var userAvatar = document.getElementById('userAvatar');
|
||||
var userName = document.getElementById('userName');
|
||||
var userPoint = document.getElementById('userPoint');
|
||||
var userSign = document.getElementById('userSign');
|
||||
var userGet = document.getElementById('userGet');
|
||||
|
||||
userGet.onclick = function () {
|
||||
request('rep/ugc/user/get', {}, function (res) {
|
||||
var d = res.data;
|
||||
if (d.imgUrl) userAvatar.src = d.imgUrl + '?param=50y50';
|
||||
userName.textContent = d.name || '游客' + d.id;
|
||||
userPoint.textContent = d.availablePoints;
|
||||
userSign.disabled = d.signed;
|
||||
showResult('用户信息加载成功', 'success');
|
||||
});
|
||||
};
|
||||
|
||||
userSign.onclick = function () {
|
||||
if (!userSign.disabled) {
|
||||
userSign.disabled = true;
|
||||
request('rep/ugc/user/sign', {}, function (res) {
|
||||
var d = res.data;
|
||||
userSign.disabled = d;
|
||||
showResult('签到' + (d ? '成功 ✓' : '失败 ✗'), d ? 'success' : 'error');
|
||||
if (d) userGet.click();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// 会员领取 ///
|
||||
var vipStatus = document.getElementById('vipStatus');
|
||||
var vipCollect = document.getElementById('vipCollect');
|
||||
var vipGet = document.getElementById('vipGet');
|
||||
|
||||
vipGet.onclick = function () {
|
||||
request('rep/ugc/user/vip', {}, function (res) {
|
||||
var d = res.data;
|
||||
vipStatus.textContent = d.status + ': ' + d.title;
|
||||
vipCollect.disabled = d.status !== 20;
|
||||
showResult('会员详情加载成功', 'success');
|
||||
});
|
||||
};
|
||||
|
||||
vipCollect.onclick = function () {
|
||||
if (!vipCollect.disabled) {
|
||||
vipCollect.disabled = true;
|
||||
request('rep/ugc/user/collect-vip', {}, function (res) {
|
||||
var d = res.data;
|
||||
vipCollect.disabled = d;
|
||||
showResult('领取' + (d ? '成功 ✓' : '失败 ✗'), d ? 'success' : 'error');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// 审核任务 ///
|
||||
var taskCount = document.getElementById('taskCount');
|
||||
var taskGoal = document.getElementById('taskGoal');
|
||||
var taskPoint = document.getElementById('taskPoint');
|
||||
var taskStartBtn = document.getElementById('taskStart');
|
||||
var taskGetBtn = document.getElementById('taskGet');
|
||||
|
||||
taskGetBtn.onclick = function () {
|
||||
request('rep/ugc/activity/get', {}, function (res) {
|
||||
var d = res.data;
|
||||
taskCount.textContent = d.count;
|
||||
taskGoal.textContent = d.goalCount;
|
||||
taskPoint.textContent = d.points;
|
||||
// 注:超过 100 可能有 bug,导致 thinktank/audit/resource/detail 获取的任务 ID 一直是 5002,目标数量也不对,暂时限制前两个任务
|
||||
taskStartBtn.disabled = d.activityId !== 5001 && d.activityId !== 5002; // 5001: 1-50, 5002: 51-100
|
||||
showResult('任务详情加载成功', 'success');
|
||||
// 任务完成,领取积分
|
||||
if (d.count >= d.goalCount) {
|
||||
request('rep/ugc/activity/collect', { activityId: d.activityId }, function (res) {
|
||||
var d = res.data;
|
||||
showResult('积分领取' + (d ? '成功,可以兑换会员了!' : '失败'), d ? 'success' : 'error');
|
||||
// 刷新任务状态
|
||||
if (d) loadAll();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
taskStartBtn.onclick = function () {
|
||||
if (!taskStartBtn.disabled) {
|
||||
songAudio.pause();
|
||||
examInit();
|
||||
}
|
||||
};
|
||||
|
||||
/// 歌曲审核区域 ///
|
||||
var song = document.getElementById('song');
|
||||
var songCover = document.getElementById('songCover');
|
||||
var songTitle = document.getElementById('songTitle');
|
||||
var songAudio = document.getElementById('songAudio');
|
||||
var songTag = document.getElementById('songTag');
|
||||
var songApprove = document.getElementById('songApprove');
|
||||
var songReject = document.getElementById('songReject');
|
||||
var songSkip = document.getElementById('songSkip');
|
||||
var songLyric = document.getElementById('songLyric');
|
||||
|
||||
var examType = 'emotionEnter';
|
||||
// 考试初始化、检查状态
|
||||
function examInit() {
|
||||
request('rep/ugc/exam/info/get', { examType: examType }, function (res) {
|
||||
var d = res.data;
|
||||
switch (d.process) {
|
||||
case 'whole_exam_end':
|
||||
// 通过考试
|
||||
if (d.hasPassExamination) {
|
||||
taskStart();
|
||||
return;
|
||||
}
|
||||
request('rep/ugc/exam/result/get', { examType: examType, taskId: d.taskId }, function (res) {
|
||||
var d = res.data;
|
||||
showResult('考试未通过:正确数:' + d.wrightNum + ',正确率:' + d.wrightRate, 'error');
|
||||
});
|
||||
case 'no':
|
||||
// 未通过或未开始
|
||||
request('rep/ugc/exam/start', { examType: examType }, function (res) {
|
||||
examStart(res.data.taskId);
|
||||
});
|
||||
break;
|
||||
case 'process':
|
||||
// 正在考试
|
||||
examStart(d.taskId);
|
||||
break;
|
||||
default:
|
||||
showResult('未知考试进度:' + d.process, 'error');
|
||||
return;
|
||||
}
|
||||
// 在上个信息不为异常时显示考试中提示
|
||||
if (d.process !== 'whole_exam_end' && !resultDiv.className.endsWith('error')) showResult('正在考试,请认真审题!', 'info');
|
||||
});
|
||||
}
|
||||
// 考试取题开始
|
||||
function examStart(taskId) {
|
||||
request('rep/ugc/exam/question/single/get', { examType: examType, taskId: taskId }, function (res) {
|
||||
var d = res.data;
|
||||
|
||||
songCover.src = d.coverUrl + '?param=100y100';
|
||||
songTitle.textContent = '[' + d.resId + '] ' + d.resName + ' - ' + d.artists;
|
||||
songTag.textContent = '[' + (d.auditCount + 1) + '/' + d.auditTaskCount + '] 当前结果: ' + d.questionContent + '?';
|
||||
songLyric.textContent = d.lyric ? d.transLyric ? lrctran(d.lyric, d.transLyric) : d.lyric : '[00:00.00] 暂无歌词';
|
||||
songLyric.scrollTop = 0;
|
||||
// 有时候就是没有播放链接,只能根据歌词硬审
|
||||
if (d.songUrl) {
|
||||
songAudio.src = d.songUrl;
|
||||
songAudio.play().catch(function () { /* 自动播放被浏览器阻止 */ });
|
||||
} else {
|
||||
songAudio.src = ''; // 清理播放器
|
||||
}
|
||||
songApprove.onclick = function () { if (!songApprove.disabled) examSubmit(taskId, d.questionId, 'A') };
|
||||
songReject.onclick = function () { if (!songReject.disabled) examSubmit(taskId, d.questionId, 'B') };
|
||||
songSkip.onclick = null;
|
||||
|
||||
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||
song.style.display = 'block';
|
||||
|
||||
setTimeout(function () {
|
||||
songApprove.disabled = songReject.disabled = false;
|
||||
}, 3e3);
|
||||
});
|
||||
}
|
||||
// 考试提交
|
||||
function examSubmit(taskId, questionId, answer) {
|
||||
songAudio.pause();
|
||||
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||
request(
|
||||
'rep/ugc/exam/submit',
|
||||
{ examType: examType, taskId: taskId, questionId: questionId, answer: answer },
|
||||
function (res) {
|
||||
var d = res.data;
|
||||
if (d.result) {
|
||||
showResult('回答正确,加载下一首...', 'success');
|
||||
} else {
|
||||
showResult('回答错误:' + d.analysis, 'error');
|
||||
}
|
||||
examInit();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务
|
||||
*/
|
||||
function taskStart() {
|
||||
request('thinktank/audit/resource/detail', { type: '4' }, function (res) {
|
||||
var d = res.data;
|
||||
|
||||
if (d.auditCount >= 50) {
|
||||
request('rep/ugc/activity/collect', { activityId: '5001' }, function (res) {
|
||||
var d = res.data;
|
||||
showResult('积分领取' + (d ? '成功,可以兑换会员了!' : '失败'), d ? 'success' : 'error');
|
||||
// 刷新任务状态
|
||||
if (d) {
|
||||
vipGet.click();
|
||||
taskGetBtn.click();
|
||||
}
|
||||
});
|
||||
songAudio.pause()
|
||||
return;
|
||||
}
|
||||
|
||||
songCover.src = d.coverUrl + '?param=100y100';
|
||||
songTitle.textContent = '[' + d.resId + '] ' + d.resName + ' - ' + d.artists;
|
||||
songTag.textContent = '[' + (d.auditCount + 1) + '/' + d.auditTaskCount + '] 当前结果: ' + d.initResult + '?';
|
||||
songLyric.textContent = d.lyric ? d.transLyric ? lrctran(d.lyric, d.transLyric) : d.lyric : '[00:00.00] 暂无歌词';
|
||||
songLyric.scrollTop = 0;
|
||||
songAudio.src = d.songUrl;
|
||||
songAudio.play().catch(function () { /* 自动播放被浏览器阻止 */ });
|
||||
|
||||
songApprove.onclick = function () { if (!songApprove.disabled) taskSubmit(d.taskId, '1'); };
|
||||
songReject.onclick = function () { if (!songReject.disabled) taskSubmit(d.taskId, '2'); };
|
||||
songSkip.onclick = function () { if (!songSkip.disabled) taskSubmit(d.taskId, '3'); };
|
||||
|
||||
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||
song.style.display = 'block';
|
||||
|
||||
setTimeout(function () {
|
||||
songApprove.disabled = songReject.disabled = songSkip.disabled = false;
|
||||
}, 3e3);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交任务
|
||||
* @param {string} taskId
|
||||
* @param {'1'|'2'|'3'} judgement
|
||||
*/
|
||||
function taskSubmit(taskId, judgement) {
|
||||
songAudio.pause();
|
||||
songApprove.disabled = songReject.disabled = songSkip.disabled = true;
|
||||
request(
|
||||
'thinktank/audit/resource/update',
|
||||
{ type: '4', taskId: taskId, judgement: judgement },
|
||||
function (res) {
|
||||
var d = res.data;
|
||||
if (d.result) {
|
||||
showResult((d.test ? '抽查' : '审核') + '提交成功,加载下一首...', 'success');
|
||||
taskStart();
|
||||
} else if (d.test) {
|
||||
showResult('抽查失败: [' + d.dayErrorNum + '/' + d.dayErrorNumStandard + '] ' + d.errorAnalysis, 'error');
|
||||
taskStart();
|
||||
} else {
|
||||
showResult('未知状态: ' + JSON.stringify(d), 'error');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**解析歌词时间戳和文本 */
|
||||
function lrctrim(lyrics) {
|
||||
var lines = lyrics.split('\n');
|
||||
var data = [];
|
||||
lines.forEach(function (line, index) {
|
||||
var matches = line.match(/\[(\d{2}):(\d{2}[\.:]?\d*)\]/);
|
||||
if (matches) {
|
||||
var minutes = parseInt(matches[1], 10);
|
||||
var seconds = parseFloat(matches[2].replace('.', ':')) || 0;
|
||||
var timestamp = minutes * 6e4 + seconds * 1e3;
|
||||
var text = line.replace(/\[\d{2}:\d{2}[\.:]?\d*\]/g, '').trim();
|
||||
text = text.replace(/\s\s+/g, ' '); // Replace multiple spaces with a single space
|
||||
data.push([timestamp, index, text]);
|
||||
}
|
||||
});
|
||||
data.sort(function (a, b) {
|
||||
return a[0] - b[0];
|
||||
});
|
||||
return data;
|
||||
}
|
||||
/**合并原文歌词和翻译歌词 */
|
||||
function lrctran(lyric, tlyric) {
|
||||
lyric = lrctrim(lyric);
|
||||
tlyric = lrctrim(tlyric);
|
||||
var len1 = lyric.length;
|
||||
var len2 = tlyric.length;
|
||||
var result = '';
|
||||
for (var i = 0, j = 0; i < len1 && j < len2; i++) {
|
||||
while (lyric[i][0] > tlyric[j][0] && j + 1 < len2) {
|
||||
j++;
|
||||
}
|
||||
if (lyric[i][0] === tlyric[j][0]) {
|
||||
tlyric[j][2] = tlyric[j][2].replace('/', '');
|
||||
if (tlyric[j][2]) {
|
||||
lyric[i][2] += ' (' + tlyric[j][2] + ')';
|
||||
}
|
||||
j++;
|
||||
}
|
||||
}
|
||||
for (var k = 0; k < len1; k++) {
|
||||
var t = lyric[k][0];
|
||||
result += '['.concat(
|
||||
String(Math.floor(t / 6e4)).padStart(2, '0'),
|
||||
':',
|
||||
String(Math.floor((t % 6e4) / 1e3)).padStart(2, '0'),
|
||||
'.',
|
||||
String(t % 1e3).padStart(3, '0'),
|
||||
']',
|
||||
lyric[k][2],
|
||||
'\n',
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 一键载入 ///
|
||||
window.loadAll = function () {
|
||||
userGet.click();
|
||||
vipGet.click();
|
||||
taskGetBtn.click();
|
||||
showResult('正在载入所有数据...', 'info');
|
||||
};
|
||||
|
||||
window.loadLocalCookie();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
670
public/ugc_lottery.html
Normal file
670
public/ugc_lottery.html
Normal file
@ -0,0 +1,670 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="docs/netease.png" />
|
||||
<title>云小编每日抽奖 - 网易云音乐 API Enhanced</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 520px;
|
||||
margin: 40px auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle b {
|
||||
color: #d63031;
|
||||
}
|
||||
|
||||
/* ---- Section ---- */
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.section:last-of-type {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* ---- Form Elements ---- */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
label .tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
label .tag.o {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="number"]:focus,
|
||||
textarea:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding: 6px 16px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.btn-sm:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-sm.gray {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.btn-sm.gray:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.btn-sm.gray:disabled {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.btn-sm.green {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-sm.green:hover {
|
||||
background: #047857;
|
||||
}
|
||||
|
||||
.btn-sm.green:disabled {
|
||||
background: #a7f3d0;
|
||||
}
|
||||
|
||||
.quick-fill {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.quick-fill span {
|
||||
color: #0066cc;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.quick-fill span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ---- Lottery Display ---- */
|
||||
.lottery-display {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eee;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.rolling-display {
|
||||
font-size: 2.4rem;
|
||||
font-weight: 800;
|
||||
color: #333;
|
||||
min-height: 4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 1px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.rolling-display.idle {
|
||||
color: #999;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- Winner Result ---- */
|
||||
.winner-display {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #86efac;
|
||||
border-radius: 8px;
|
||||
padding: 20px 16px;
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.winner-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #065f46;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.winner-name {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
color: #dc2626;
|
||||
letter-spacing: 1px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.confetti-row {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.confetti-emoji {
|
||||
font-size: 1.6rem;
|
||||
animation: bounce 0.6s ease infinite alternate;
|
||||
}
|
||||
|
||||
.confetti-emoji:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.confetti-emoji:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Status ---- */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: #f9fafb;
|
||||
border-radius: 6px;
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #059669;
|
||||
flex-shrink: 0;
|
||||
animation: pulse 1.5s ease infinite;
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.status-dot.idle {
|
||||
background: #9ca3af;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ---- Footer ---- */
|
||||
.footer-link {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer-link a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ---- Responsive ---- */
|
||||
@media (max-width: 500px) {
|
||||
.container {
|
||||
padding: 20px 16px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.rolling-display {
|
||||
font-size: 1.8rem;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.winner-name {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>云小编每日抽奖</h1>
|
||||
<p class="subtitle">每次抽奖消耗 <b>200 个曲库编辑部积分</b>,每日最多可抽奖 3 次</p>
|
||||
|
||||
<!-- Cookie 管理 -->
|
||||
<div class="form-group">
|
||||
<label for="cookieInput">令牌 <span class="tag o">可选</span></label>
|
||||
<input type="text" id="cookieInput" placeholder="留空则默认读取本地存储的令牌" autocomplete="off" />
|
||||
<div class="quick-fill">
|
||||
<span onclick="readLocalCookie()">读取本地令牌</span>
|
||||
<span onclick="clearCookie()">清除令牌</span>
|
||||
<span onclick="saveCookie()">保存令牌</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 抽奖区域 -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">每日抽奖</span>
|
||||
</div>
|
||||
|
||||
<div class="lottery-display">
|
||||
<div class="rolling-display idle" id="rollingDisplay">等待抽奖</div>
|
||||
<div id="resultContainer"></div>
|
||||
</div>
|
||||
|
||||
<button class="btn" id="lotteryBtn">
|
||||
🎲 开始抽奖
|
||||
</button>
|
||||
|
||||
<div class="status-bar" id="statusBar">
|
||||
<span class="status-dot idle" id="statusDot"></span>
|
||||
<span class="status-text" id="statusText">就绪,点击按钮开始抽奖</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<div class="footer-link">
|
||||
<a href="/">← 返回首页</a>
|
||||
<span style="margin: 0 8px; color: #ddd;">·</span>
|
||||
<a href="/ugc.html">云小编任务中心</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// ============ DOM 元素 ============
|
||||
const cookieInput = document.getElementById('cookieInput');
|
||||
const rollingDisplay = document.getElementById('rollingDisplay');
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const lotteryBtn = document.getElementById('lotteryBtn');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
|
||||
// ============ 状态 ============
|
||||
let isDrawing = false;
|
||||
let rollingTimer = null;
|
||||
|
||||
// ============ 状态栏更新 ============
|
||||
function setStatus(type, message) {
|
||||
statusText.textContent = message;
|
||||
statusDot.className = 'status-dot';
|
||||
if (type === 'error') {
|
||||
statusDot.classList.add('error');
|
||||
} else if (type === 'info') {
|
||||
statusDot.style.background = '#059669';
|
||||
} else {
|
||||
statusDot.classList.add('idle');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 重置结果 ============
|
||||
function resetResult() {
|
||||
resultContainer.innerHTML = '';
|
||||
rollingDisplay.textContent = '等待抽奖';
|
||||
rollingDisplay.className = 'rolling-display idle';
|
||||
setStatus('idle', '就绪,点击按钮开始抽奖');
|
||||
}
|
||||
|
||||
// ============ 本地令牌管理 ============
|
||||
const cookieKey = 'ncm_cookie';
|
||||
let cookie = localStorage.getItem(cookieKey) || '';
|
||||
if (cookie) {
|
||||
cookieInput.value = cookie;
|
||||
}
|
||||
|
||||
window.readLocalCookie = function () {
|
||||
const local = localStorage.getItem(cookieKey);
|
||||
if (local) {
|
||||
cookie = local;
|
||||
cookieInput.value = local;
|
||||
setStatus('info', '已读取本地令牌');
|
||||
} else {
|
||||
setStatus('error', '未在本地发现令牌,请手动输入');
|
||||
}
|
||||
};
|
||||
|
||||
window.clearCookie = function () {
|
||||
cookie = '';
|
||||
cookieInput.value = '';
|
||||
localStorage.removeItem(cookieKey);
|
||||
setStatus('info', '已清除本地令牌');
|
||||
};
|
||||
|
||||
window.saveCookie = function () {
|
||||
const ck = cookieInput.value.trim();
|
||||
if (ck) {
|
||||
cookie = ck;
|
||||
localStorage.setItem(cookieKey, ck);
|
||||
setStatus('info', '已保存本地令牌');
|
||||
} else {
|
||||
setStatus('error', '令牌内容为空,未保存');
|
||||
}
|
||||
};
|
||||
|
||||
function getCookie() {
|
||||
const ck = cookieInput.value.trim();
|
||||
if (ck) cookie = ck;
|
||||
return ck || cookie || '';
|
||||
}
|
||||
|
||||
// ============ 调用后端接口 ============
|
||||
async function request(url, params) {
|
||||
try {
|
||||
url += '?timestamp=' + Date.now() + params;
|
||||
const ck = getCookie();
|
||||
if (ck) url += '&cookie=' + encodeURIComponent(ck);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await response.json();
|
||||
} catch (error) {
|
||||
console.error('数据解析错误:', error);
|
||||
throw new Error('接口请求错误: ' + response.status + ': ' + response.statusText);
|
||||
}
|
||||
|
||||
if (res.code !== 200) {
|
||||
throw new Error('接口返回错误: ' + res.code + ': ' + (res.message || '未知错误'));
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('接口请求失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLotteryResult() {
|
||||
const user = await request('rep/ugc/user/get', '');
|
||||
|
||||
if ((user.data?.availablePoints ?? 0) < 200) {
|
||||
throw new Error('用户剩余积分不足,请先完成任务获取积分');
|
||||
}
|
||||
|
||||
const chance = await request('middle/play/lottery/remain/chance', '&activityId=6501202');
|
||||
|
||||
if (chance.data === 0) {
|
||||
throw new Error('今日机会已用完,明天再来吧');
|
||||
}
|
||||
|
||||
const res = await request('middle/play/do/lottery', '&activityId=6501202&drawCount=1');
|
||||
|
||||
const winner = res.data?.prizeDetailInfoMap?.['8186002']?.prizeName;
|
||||
if (!winner) {
|
||||
throw new Error('无法解析响应数据');
|
||||
}
|
||||
|
||||
return [winner, res.data.restChance];
|
||||
}
|
||||
|
||||
// ============ 滚动动画 ============
|
||||
function startRollingAnimation(finalWinner) {
|
||||
const placeholderNames = ['🎰', '🎲', '🎯', '✨', '💫', '🌟', '🎪', '🎭'];
|
||||
const totalDuration = 2800;
|
||||
const startInterval = 50;
|
||||
const endInterval = 280;
|
||||
const startTime = Date.now();
|
||||
|
||||
rollingDisplay.className = 'rolling-display';
|
||||
resultContainer.innerHTML = '';
|
||||
|
||||
function roll() {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / totalDuration, 1.0);
|
||||
const easeOutProgress = 1 - Math.pow(1 - progress, 3);
|
||||
const currentInterval = startInterval + (endInterval - startInterval) * easeOutProgress;
|
||||
|
||||
const randomPlaceholder = placeholderNames[Math.floor(Math.random() * placeholderNames.length)];
|
||||
rollingDisplay.textContent = randomPlaceholder;
|
||||
|
||||
if (progress >= 1.0) {
|
||||
clearTimeout(rollingTimer);
|
||||
rollingDisplay.textContent = finalWinner;
|
||||
showWinnerResult(finalWinner);
|
||||
finishDrawing();
|
||||
return;
|
||||
}
|
||||
|
||||
rollingTimer = setTimeout(roll, currentInterval);
|
||||
}
|
||||
|
||||
roll();
|
||||
}
|
||||
|
||||
function showWinnerResult(winner) {
|
||||
resultContainer.innerHTML =
|
||||
'<div class="winner-display">' +
|
||||
'<div class="winner-label">🏆 恭喜中奖</div>' +
|
||||
'<div class="winner-name">' + escapeHTML(winner) + '</div>' +
|
||||
'<div class="confetti-row">' +
|
||||
'<span class="confetti-emoji">🎉</span>' +
|
||||
'<span class="confetti-emoji">🎊</span>' +
|
||||
'<span class="confetti-emoji">✨</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function finishDrawing() {
|
||||
isDrawing = false;
|
||||
lotteryBtn.disabled = false;
|
||||
}
|
||||
|
||||
// ============ 主抽奖流程 ============
|
||||
async function startLottery() {
|
||||
if (isDrawing) return;
|
||||
|
||||
isDrawing = true;
|
||||
lotteryBtn.disabled = true;
|
||||
resultContainer.innerHTML = '';
|
||||
rollingDisplay.className = 'rolling-display';
|
||||
rollingDisplay.textContent = '🎰';
|
||||
setStatus('info', '正在获取抽奖结果...');
|
||||
|
||||
try {
|
||||
const res = await fetchLotteryResult();
|
||||
setStatus('info', '抽奖成功,剩余次数 ' + res[1]);
|
||||
startRollingAnimation(res[0]);
|
||||
} catch (error) {
|
||||
rollingDisplay.textContent = '抽奖失败';
|
||||
rollingDisplay.className = 'rolling-display idle';
|
||||
setStatus('error', '抽奖失败: ' + error.message);
|
||||
isDrawing = false;
|
||||
lotteryBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 事件绑定 ============
|
||||
lotteryBtn.addEventListener('click', startLottery);
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === ' ' && document.activeElement !== cookieInput) {
|
||||
e.preventDefault();
|
||||
if (!isDrawing && !lotteryBtn.disabled) {
|
||||
startLottery();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 回车键在输入框中触发保存
|
||||
cookieInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
saveCookie();
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 初始化 ============
|
||||
resetResult();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -217,8 +217,13 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择文件</label>
|
||||
<input type="file" name="songFile" accept="audio/*" />
|
||||
<label for="songFile">选择声音文件</label>
|
||||
<input id="songFile" type="file" name="songFile" accept="audio/*" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="imgFile">选择声音封面(可选)</label>
|
||||
<input id="imgFile" type="file" name="imgFile" accept="image/*" />
|
||||
</div>
|
||||
|
||||
<button class="btn" @click="submit">上传</button>
|
||||
@ -251,29 +256,36 @@
|
||||
methods: {
|
||||
submit() {
|
||||
console.info('submit')
|
||||
const file = document.querySelector('input[type=file]').files[0]
|
||||
if (!file) {
|
||||
alert('请选择文件')
|
||||
const songFile = document.querySelector('input[name=songFile]').files[0]
|
||||
const imgFile = document.querySelector('input[name=imgFile]').files[0]
|
||||
if (!songFile) {
|
||||
alert('请选择声音文件')
|
||||
return
|
||||
}
|
||||
this.upload(file)
|
||||
this.upload(songFile, imgFile)
|
||||
},
|
||||
|
||||
async getData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
|
||||
url: `/voicelist/my/created?limit=100&cookie=${localStorage.getItem(
|
||||
'cookie',
|
||||
)}`,
|
||||
})
|
||||
|
||||
console.info(res.data.data)
|
||||
this.voicelist = res.data.data.list || []
|
||||
this.voicelist = res.data.data.data || []
|
||||
this.voicelist.forEach(async (i) => {
|
||||
try {
|
||||
const res2 = await axios({
|
||||
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
|
||||
url: `/voicelist/list?voiceListId=${
|
||||
i.voiceListId
|
||||
}&limit=5&cookie=${localStorage.getItem('cookie')}`,
|
||||
})
|
||||
i.voiceListData = res2.data.data.list || []
|
||||
i.voiceListData = res2.data.data
|
||||
? res2.data.data.list || []
|
||||
: []
|
||||
console.info(res2)
|
||||
} catch (err) {
|
||||
console.error('获取播客详情失败:', err)
|
||||
@ -286,14 +298,17 @@
|
||||
}
|
||||
},
|
||||
|
||||
upload(file) {
|
||||
upload(songFile, imgFile) {
|
||||
if (!this.currentVoice) {
|
||||
alert('请先选择播客列表')
|
||||
return
|
||||
}
|
||||
|
||||
var formData = new FormData()
|
||||
formData.append('songFile', file)
|
||||
formData.append('songFile', songFile)
|
||||
if (imgFile) {
|
||||
formData.append('imgFile', imgFile)
|
||||
}
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
@ -312,7 +327,7 @@
|
||||
data: formData,
|
||||
})
|
||||
.then((res) => {
|
||||
alert(`${file.name} 上传成功`)
|
||||
alert(`${songFile.name} 上传成功`)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('上传失败:', err)
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Token 注册器 — 网易云音乐 API Enhanced</title>
|
||||
<title>易盾 Watchman 调试器 — 网易云音乐 API Enhanced</title>
|
||||
<style>
|
||||
:root {
|
||||
--fg: #333;
|
||||
@ -17,7 +17,7 @@
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
||||
.container { max-width: 720px; margin: 40px auto; padding: 0 20px; }
|
||||
.container { max-width: 760px; margin: 40px auto; padding: 0 20px; }
|
||||
@media (max-width: 480px) {
|
||||
.container { margin: 20px auto; padding: 0 16px; }
|
||||
header.site-header h1 { font-size: 22px; }
|
||||
@ -28,8 +28,12 @@
|
||||
.sub { margin-top: 4px; color: var(--muted); font-size: 14px; }
|
||||
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
|
||||
.block h3 { margin: 0 0 12px; font-size: 15px; font-weight: 600; }
|
||||
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-family: 'Courier New', monospace; font-size: 13px; max-height: 160px; overflow: auto; }
|
||||
.row { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
|
||||
.field { margin-bottom: 10px; }
|
||||
.field label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; }
|
||||
input[type="text"] { width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Courier New', monospace; color: var(--fg); background: #fafafa; }
|
||||
input[type="text"]:focus { outline: none; border-color: #888; background: #fff; }
|
||||
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-family: 'Courier New', monospace; font-size: 13px; max-height: 220px; overflow: auto; }
|
||||
.row { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; align-items: center; }
|
||||
button { padding: 8px 18px; border-radius: 8px; border: 1px solid var(--border); font-size: 14px; font-weight: 500; cursor: pointer; background: var(--panel); color: var(--fg); transition: all .15s; }
|
||||
button:hover { background: #eee; }
|
||||
button.primary { background: var(--fg); color: var(--panel); border-color: var(--fg); }
|
||||
@ -41,32 +45,65 @@
|
||||
.tag.busy { background: #fff3cd; color: #856404; border-color: #ffeeba; }
|
||||
.flex { display: flex; align-items: center; gap: 10px; }
|
||||
.flex.sb { justify-content: space-between; }
|
||||
.mt-2 { margin-top: 14px; }
|
||||
.hint { font-size: 12px; color: var(--muted); margin-top: 8px; }
|
||||
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; font-size: 13px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="site-header">
|
||||
<h1>🔑 Token 注册器</h1>
|
||||
<p class="sub">从易盾获取反作弊 token 并注册到服务器,供广告等需要 checkToken 的接口使用</p>
|
||||
<h1>🛡️ 易盾 Watchman 调试器</h1>
|
||||
<p class="sub">
|
||||
直接调用易盾官方 Web SDK(tool.min.js)获取反作弊 token。
|
||||
与 <code>module/register_checktoken_v2.js</code> 内部(jsdom 模拟同一套 SDK)逻辑一致,可用于对照验证。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Watchman 初始化 -->
|
||||
<div class="block">
|
||||
<div class="flex sb">
|
||||
<h3>当前 Token</h3>
|
||||
<span id="statusTag" class="tag no">未注册</span>
|
||||
<h3>① 初始化 Watchman</h3>
|
||||
<span id="initTag" class="tag no">未初始化</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="pnInput">productNumber(易盾产品号)</label>
|
||||
<input type="text" id="pnInput" value="YD00000558929251" spellcheck="false">
|
||||
</div>
|
||||
<pre id="tokenDisplay">(空)</pre>
|
||||
<div class="row">
|
||||
<button id="btnGet" class="primary">获取 Token</button>
|
||||
<button id="btnRefresh">强制刷新</button>
|
||||
<button id="btnInit" class="primary">初始化</button>
|
||||
<button id="btnReset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 获取 Token -->
|
||||
<div class="block">
|
||||
<h3>服务器响应</h3>
|
||||
<pre id="respDisplay">{}</pre>
|
||||
<div class="flex sb">
|
||||
<h3>② 获取 Token</h3>
|
||||
<span id="getTag" class="tag no">—</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="bidInput">businessId(业务 ID,来自网易 clientcfg 下发)</label>
|
||||
<input type="text" id="bidInput" value="bd5d2f973ef74cd2a61325a412ae54d9" spellcheck="false">
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="btnGet" class="primary" disabled>获取 Token</button>
|
||||
<button id="btnCopy" disabled>复制</button>
|
||||
<span class="hint">覆盖 /api/playlist/subscribe、/api/song/like、/api/user/follow 等接口</span>
|
||||
</div>
|
||||
<pre id="tokenDisplay">(尚未获取)</pre>
|
||||
</div>
|
||||
|
||||
<!-- 服务器端对照 -->
|
||||
<div class="block">
|
||||
<div class="flex sb">
|
||||
<h3>③ 服务器端对照(/register/checktoken/v2)</h3>
|
||||
<span id="srvTag" class="tag no">—</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="btnSrv">获取服务器端 Token</button>
|
||||
<span class="hint">node + jsdom 模拟同一套 SDK 的实时结果</span>
|
||||
</div>
|
||||
<pre id="srvDisplay">(尚未获取)</pre>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
@ -74,46 +111,121 @@
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script src="https://acstatic-dun.126.net/tool.min.js"></script>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id)
|
||||
const base = location.origin
|
||||
|
||||
async function call(refresh) {
|
||||
const btn = $('btnGet')
|
||||
const btnR = $('btnRefresh')
|
||||
btn.disabled = btnR.disabled = true
|
||||
const DEFAULTS = {
|
||||
pn: 'YD00000558929251',
|
||||
bid: 'bd5d2f973ef74cd2a61325a412ae54d9',
|
||||
}
|
||||
|
||||
const tag = $('statusTag')
|
||||
tag.className = 'tag busy'
|
||||
tag.textContent = '获取中…'
|
||||
let wm = null // Watchman 实例
|
||||
|
||||
try {
|
||||
const url = base + '/register/checktoken' + (refresh ? '?refresh=1' : '')
|
||||
const res = await fetch(url)
|
||||
const data = await res.json()
|
||||
$('respDisplay').textContent = JSON.stringify(data, null, 2)
|
||||
const setTag = (id, text, state) => {
|
||||
const tag = $(id)
|
||||
tag.textContent = text
|
||||
tag.className = 'tag ' + state // ok / no / busy
|
||||
}
|
||||
|
||||
if (data.token) {
|
||||
$('tokenDisplay').textContent = data.token
|
||||
tag.className = 'tag ok'
|
||||
tag.textContent = '已注册'
|
||||
// ① 初始化
|
||||
function initShield() {
|
||||
if (typeof window.initWatchman !== 'function') {
|
||||
alert('未找到 initWatchman:请确认 tool.min.js 已加载(CDN 是否可访问)')
|
||||
return
|
||||
}
|
||||
const pn = $('pnInput').value.trim() || DEFAULTS.pn
|
||||
setTag('initTag', '初始化中…', 'busy')
|
||||
$('btnInit').disabled = true
|
||||
window.initWatchman({
|
||||
auto: true,
|
||||
productNumber: pn,
|
||||
onload(instance) {
|
||||
wm = instance
|
||||
setTag('initTag', '已就绪', 'ok')
|
||||
$('btnInit').disabled = false
|
||||
$('btnGet').disabled = false
|
||||
},
|
||||
onerror() {
|
||||
wm = null
|
||||
setTag('initTag', '初始化失败', 'no')
|
||||
$('btnInit').disabled = false
|
||||
$('btnGet').disabled = true
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function resetShield() {
|
||||
wm = null
|
||||
setTag('initTag', '未初始化', 'no')
|
||||
setTag('getTag', '—', 'no')
|
||||
$('btnGet').disabled = true
|
||||
$('btnCopy').disabled = true
|
||||
$('tokenDisplay').textContent = '(尚未获取)'
|
||||
}
|
||||
|
||||
// ② 获取 token
|
||||
function getToken() {
|
||||
if (!wm) {
|
||||
alert('请先完成初始化')
|
||||
return
|
||||
}
|
||||
const bid = $('bidInput').value.trim() || DEFAULTS.bid
|
||||
setTag('getTag', '获取中…', 'busy')
|
||||
$('btnGet').disabled = true
|
||||
wm.getToken(bid, (tk) => {
|
||||
if (typeof tk === 'string' && tk) {
|
||||
$('tokenDisplay').textContent = tk
|
||||
setTag('getTag', 'OK', 'ok')
|
||||
$('btnCopy').disabled = false
|
||||
} else {
|
||||
$('tokenDisplay').textContent = '(获取失败)'
|
||||
tag.className = 'tag no'
|
||||
tag.textContent = '失败'
|
||||
setTag('getTag', '失败', 'no')
|
||||
$('btnCopy').disabled = true
|
||||
}
|
||||
$('btnGet').disabled = false
|
||||
})
|
||||
}
|
||||
|
||||
// ③ 服务器端对照
|
||||
async function fetchServerToken() {
|
||||
setTag('srvTag', '获取中…', 'busy')
|
||||
$('btnSrv').disabled = true
|
||||
try {
|
||||
const res = await fetch(
|
||||
location.origin + '/register/checktoken/v2?t=' + Date.now(),
|
||||
)
|
||||
const data = await res.json()
|
||||
$('srvDisplay').textContent = data.token
|
||||
? data.token
|
||||
: JSON.stringify(data, null, 2)
|
||||
setTag('srvTag', data.token ? 'OK' : '失败', data.token ? 'ok' : 'no')
|
||||
} catch (e) {
|
||||
$('respDisplay').textContent = JSON.stringify({ error: e.message }, null, 2)
|
||||
tag.className = 'tag no'
|
||||
tag.textContent = e.message
|
||||
$('srvDisplay').textContent = JSON.stringify({ error: e.message }, null, 2)
|
||||
setTag('srvTag', e.message, 'no')
|
||||
}
|
||||
$('btnSrv').disabled = false
|
||||
}
|
||||
|
||||
btn.disabled = btnR.disabled = false
|
||||
// 复制
|
||||
async function copyToken() {
|
||||
const txt = $('tokenDisplay').textContent
|
||||
try {
|
||||
await navigator.clipboard.writeText(txt)
|
||||
setTag('getTag', '已复制', 'ok')
|
||||
} catch (e) {
|
||||
setTag('getTag', '复制失败', 'no')
|
||||
}
|
||||
}
|
||||
|
||||
$('btnGet').onclick = () => call(false)
|
||||
$('btnRefresh').onclick = () => call(true)
|
||||
call(false)
|
||||
$('btnInit').onclick = initShield
|
||||
$('btnReset').onclick = resetShield
|
||||
$('btnGet').onclick = getToken
|
||||
$('btnCopy').onclick = copyToken
|
||||
$('btnSrv').onclick = fetchServerToken
|
||||
|
||||
// 自动初始化
|
||||
initShield()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
622
public/yunbei_task.html
Normal file
622
public/yunbei_task.html
Normal file
@ -0,0 +1,622 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>云贝广告任务 — 网易云音乐 API Enhanced</title>
|
||||
<style>
|
||||
:root {
|
||||
--fg: #333;
|
||||
--muted: #666;
|
||||
--border: #ddd;
|
||||
--bg: #f5f5f5;
|
||||
--panel: #ffffff;
|
||||
--accent: #333;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
margin: 20px auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
header.site-header h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
.block {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
header.site-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
header.site-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
.sub {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
.block {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.block h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.block .desc {
|
||||
margin: 0 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
pre {
|
||||
margin: 0;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
pre.data {
|
||||
max-height: 360px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: var(--panel);
|
||||
color: var(--fg);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
button:hover {
|
||||
background: #eee;
|
||||
}
|
||||
button.primary {
|
||||
background: var(--fg);
|
||||
color: var(--panel);
|
||||
border-color: var(--fg);
|
||||
}
|
||||
button.primary:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
button.danger {
|
||||
background: #a11;
|
||||
color: #fff;
|
||||
border-color: #a11;
|
||||
}
|
||||
button.danger:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.tag.ok {
|
||||
background: #e6f7e6;
|
||||
color: #1a7a1a;
|
||||
border-color: #b7e6b7;
|
||||
}
|
||||
.tag.no {
|
||||
background: #ffe6e6;
|
||||
color: #a11;
|
||||
border-color: #f5c6c6;
|
||||
}
|
||||
.tag.busy {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border-color: #ffeeba;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.flex.sb {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.step .num {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--fg);
|
||||
color: var(--panel);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.step .num.done {
|
||||
background: #1a7a1a;
|
||||
}
|
||||
.step .body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr;
|
||||
gap: 4px 12px;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.info-grid .lbl {
|
||||
color: var(--muted);
|
||||
}
|
||||
footer.site-footer {
|
||||
margin-top: 32px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
footer.site-footer a {
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
footer.site-footer a:hover {
|
||||
border-bottom: 1px dotted var(--fg);
|
||||
}
|
||||
|
||||
/* ---- 今日进度条 ---- */
|
||||
.progress-wrap {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background: #eee;
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
border-radius: 5px;
|
||||
background: #1a7a1a;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
.progress-text {
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---- 歌曲列表 ---- */
|
||||
.song-list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.song-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.song-item img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
background: #eee;
|
||||
}
|
||||
.song-item .song-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.song-item .song-artist {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.song-item .song-like {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="site-header">
|
||||
<h1>🪙 云贝广告任务 · 看视频得云贝</h1>
|
||||
<p class="sub">
|
||||
云贝任务中心「看广告视频得云贝」— 查询今日状态、领取云贝、获取推荐歌曲
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- 步骤 1: 查询今日任务状态 -->
|
||||
<div class="block">
|
||||
<div class="step">
|
||||
<div class="num" id="s1num">1</div>
|
||||
<div class="body">
|
||||
<div class="flex sb">
|
||||
<h3>查询今日任务状态</h3>
|
||||
<span id="s1tag" class="tag no">未开始</span>
|
||||
</div>
|
||||
<p class="desc">
|
||||
调用
|
||||
/yunbei/task/list/v1,查看今日已完成次数、累计云贝与单次可得云贝
|
||||
</p>
|
||||
<div class="row">
|
||||
<button id="step1" class="primary">① 查询状态</button>
|
||||
</div>
|
||||
<div class="info-grid" id="s1info" style="display: none">
|
||||
<div class="lbl">已完成</div>
|
||||
<div id="s1times">-</div>
|
||||
<div class="lbl">累计云贝</div>
|
||||
<div id="s1amount">-</div>
|
||||
<div class="lbl">单次可得</div>
|
||||
<div id="s1single">-</div>
|
||||
</div>
|
||||
<div class="progress-wrap" id="s1progress" style="display: none">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="s1fill"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="s1ptext"></div>
|
||||
</div>
|
||||
<pre id="s1pre">(尚未执行)</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 2: 领取云贝 -->
|
||||
<div class="block">
|
||||
<div class="step">
|
||||
<div class="num" id="s2num">2</div>
|
||||
<div class="body">
|
||||
<div class="flex sb">
|
||||
<h3>领取云贝</h3>
|
||||
<span id="s2tag" class="tag no">未开始</span>
|
||||
</div>
|
||||
<p class="desc">
|
||||
调用 /yunbei/task/finish/v1 完成任务并领取云贝。实测仅需传
|
||||
yunbeiAmount(单次 150),无需真实听歌/看视频。单日上限 10 次 ×
|
||||
150 = 1500 云贝/天。
|
||||
</p>
|
||||
<div class="row">
|
||||
<button id="step2" class="primary" disabled>
|
||||
② 领取一次 (+150)
|
||||
</button>
|
||||
<button id="step2x" disabled>连续领取至上限</button>
|
||||
</div>
|
||||
<div class="info-grid" id="s2info" style="display: none">
|
||||
<div class="lbl">本次结果</div>
|
||||
<div id="s2result">-</div>
|
||||
<div class="lbl">今日累计</div>
|
||||
<div id="s2amount">-</div>
|
||||
</div>
|
||||
<pre id="s2pre">(尚未执行)</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 3: 获取推荐歌曲 -->
|
||||
<div class="block">
|
||||
<div class="step">
|
||||
<div class="num" id="s3num">3</div>
|
||||
<div class="body">
|
||||
<div class="flex sb">
|
||||
<h3>获取推荐歌曲</h3>
|
||||
<span id="s3tag" class="tag no">未开始</span>
|
||||
</div>
|
||||
<p class="desc">
|
||||
调用
|
||||
/yunbei/task/recommend/song,获取「听歌得云贝」任务专属推荐(alg
|
||||
均为 alg_payrec_yunBei_*)
|
||||
</p>
|
||||
<div class="row">
|
||||
<button id="step3" class="primary" disabled>③ 获取推荐</button>
|
||||
</div>
|
||||
<div class="song-list" id="s3list" style="display: none"></div>
|
||||
<pre id="s3pre">(尚未执行)</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 一键全流程 -->
|
||||
<div class="block">
|
||||
<div class="flex sb">
|
||||
<h3>一键运行</h3>
|
||||
<span id="allTag" class="tag no">就绪</span>
|
||||
</div>
|
||||
<p class="desc" style="margin-top: 4px">
|
||||
查询今日状态 → 领取一次云贝 → 获取推荐歌曲
|
||||
</p>
|
||||
<div class="row">
|
||||
<button id="runAll" class="primary">▶ 一键运行</button>
|
||||
<button id="clearAll" class="danger">✕ 清空</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<a href="/">← 返回首页</a>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id)
|
||||
const base = location.origin
|
||||
|
||||
function setTag(id, type, text) {
|
||||
const el = $(id)
|
||||
el.className = 'tag ' + type
|
||||
el.textContent = text
|
||||
}
|
||||
|
||||
function setNum(id, done) {
|
||||
$(id).className = 'num' + (done ? ' done' : '')
|
||||
}
|
||||
|
||||
function nocache(url) {
|
||||
const sep = url.indexOf('?') > -1 ? '&' : '?'
|
||||
return url + sep + '_t=' + Date.now()
|
||||
}
|
||||
|
||||
/** 今日进度展示: times/10 */
|
||||
function renderProgress(times, singleAmount) {
|
||||
const pct = Math.min(100, Math.round((times / 10) * 100))
|
||||
$('s1fill').style.width = pct + '%'
|
||||
$('s1ptext').textContent =
|
||||
'今日进度: ' +
|
||||
times +
|
||||
' / 10 次' +
|
||||
(times >= 10
|
||||
? '(已达上限,明天再来吧)'
|
||||
: '(剩余 ' + (10 - times) + ' 次 × ' + singleAmount + ' 云贝)')
|
||||
}
|
||||
|
||||
// ===== 步骤 1: 查询状态 =====
|
||||
async function step1() {
|
||||
const btn = $('step1')
|
||||
btn.disabled = true
|
||||
setTag('s1tag', 'busy', '查询中…')
|
||||
setNum('s1num', false)
|
||||
try {
|
||||
const res = await fetch(nocache(base + '/yunbei/task/list/v1'))
|
||||
const data = await res.json()
|
||||
$('s1pre').textContent = JSON.stringify(data, null, 2)
|
||||
const body = data.data || {}
|
||||
if (data.code === 200 && typeof body.times === 'number') {
|
||||
setTag('s1tag', 'ok', '已查询')
|
||||
setNum('s1num', true)
|
||||
$('s1info').style.display = 'grid'
|
||||
$('s1times').textContent = body.times
|
||||
$('s1amount').textContent = body.amount + ' 云贝'
|
||||
$('s1single').textContent = body.singleAmount + ' 云贝'
|
||||
$('s1progress').style.display = 'block'
|
||||
renderProgress(body.times, body.singleAmount)
|
||||
|
||||
$('step2').disabled = body.times >= 10
|
||||
$('step2x').disabled = body.times >= 10
|
||||
$('step3').disabled = false
|
||||
window.__singleAmount = body.singleAmount || 150
|
||||
return body
|
||||
}
|
||||
setTag('s1tag', 'no', '查询失败')
|
||||
return null
|
||||
} catch (e) {
|
||||
$('s1pre').textContent = JSON.stringify({ error: e.message }, null, 2)
|
||||
setTag('s1tag', 'no', e.message)
|
||||
return null
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
}
|
||||
}
|
||||
$('step1').onclick = step1
|
||||
|
||||
// ===== 步骤 2: 领取云贝 =====
|
||||
async function claimOnce() {
|
||||
const btn = $('step2')
|
||||
btn.disabled = true
|
||||
$('step2x').disabled = true
|
||||
setTag('s2tag', 'busy', '领取中…')
|
||||
setNum('s2num', false)
|
||||
try {
|
||||
const amount = window.__singleAmount || 150
|
||||
const res = await fetch(
|
||||
nocache(base + '/yunbei/task/finish/v1?yunbeiAmount=' + amount),
|
||||
)
|
||||
const data = await res.json()
|
||||
$('s2pre').textContent = JSON.stringify(data, null, 2)
|
||||
if (data.code === 200) {
|
||||
setTag('s2tag', 'ok', '已领取 +' + amount)
|
||||
setNum('s2num', true)
|
||||
$('s2info').style.display = 'grid'
|
||||
$('s2result').textContent = String(data.data)
|
||||
await step1()
|
||||
return true
|
||||
}
|
||||
setTag('s2tag', 'no', '领取失败: ' + (data.message || data.code))
|
||||
$('s2info').style.display = 'none'
|
||||
return false
|
||||
} catch (e) {
|
||||
$('s2pre').textContent = JSON.stringify({ error: e.message }, null, 2)
|
||||
setTag('s2tag', 'no', e.message)
|
||||
return false
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
$('step2x').disabled = false
|
||||
}
|
||||
}
|
||||
$('step2').onclick = claimOnce
|
||||
|
||||
// 连续领取至上限
|
||||
$('step2x').onclick = async function () {
|
||||
const btn = this
|
||||
btn.disabled = true
|
||||
$('step2').disabled = true
|
||||
setTag('s2tag', 'busy', '连续领取中…')
|
||||
let okCount = 0
|
||||
let failed = false
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const state = await step1()
|
||||
if (!state) {
|
||||
failed = true
|
||||
break
|
||||
}
|
||||
if (state.times >= 10) break
|
||||
const ok = await claimOnce()
|
||||
if (!ok) {
|
||||
failed = true
|
||||
break
|
||||
}
|
||||
okCount++
|
||||
}
|
||||
setTag(
|
||||
's2tag',
|
||||
failed ? 'no' : 'ok',
|
||||
failed ? '中断' : '已领满 ' + okCount + ' 次',
|
||||
)
|
||||
btn.disabled = false
|
||||
$('step2').disabled = false
|
||||
}
|
||||
|
||||
// ===== 步骤 3: 获取推荐歌曲 =====
|
||||
$('step3').onclick = async function () {
|
||||
const btn = this
|
||||
btn.disabled = true
|
||||
setTag('s3tag', 'busy', '获取中…')
|
||||
setNum('s3num', false)
|
||||
$('s3list').style.display = 'none'
|
||||
try {
|
||||
const res = await fetch(
|
||||
nocache(base + '/yunbei/task/recommend/song?offset=0&limit=10'),
|
||||
)
|
||||
const data = await res.json()
|
||||
$('s3pre').textContent = JSON.stringify(data, null, 2)
|
||||
const list = data.data || []
|
||||
if (data.code === 200 && Array.isArray(list) && list.length) {
|
||||
setTag('s3tag', 'ok', '共 ' + list.length + ' 首')
|
||||
setNum('s3num', true)
|
||||
const box = $('s3list')
|
||||
box.style.display = 'block'
|
||||
box.innerHTML = ''
|
||||
list.forEach((s) => {
|
||||
const item = document.createElement('div')
|
||||
item.className = 'song-item'
|
||||
item.innerHTML =
|
||||
'<img src="' +
|
||||
(s.albumUrl || '') +
|
||||
'" alt="" onerror="this.style.visibility=\'hidden\'">' +
|
||||
'<div><div class="song-name">' +
|
||||
(s.songName || '-') +
|
||||
'</div>' +
|
||||
'<div class="song-artist">' +
|
||||
(s.artistName || '-') +
|
||||
'</div></div>' +
|
||||
'<div class="song-like">' +
|
||||
(s.likeFlag ? '♥ 已喜欢' : '♡ 未喜欢') +
|
||||
'</div>'
|
||||
box.appendChild(item)
|
||||
})
|
||||
return list
|
||||
}
|
||||
setTag('s3tag', 'no', '获取失败')
|
||||
return null
|
||||
} catch (e) {
|
||||
$('s3pre').textContent = JSON.stringify({ error: e.message }, null, 2)
|
||||
setTag('s3tag', 'no', e.message)
|
||||
return null
|
||||
} finally {
|
||||
btn.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 一键运行 =====
|
||||
$('runAll').onclick = async function () {
|
||||
const btn = this
|
||||
btn.disabled = true
|
||||
setTag('allTag', 'busy', '运行中…')
|
||||
await step1()
|
||||
const state = window.__singleAmount ? true : false
|
||||
if (state) await claimOnce()
|
||||
await $('step3').onclick()
|
||||
setTag('allTag', 'ok', '完成')
|
||||
btn.disabled = false
|
||||
}
|
||||
|
||||
// ===== 清空 =====
|
||||
$('clearAll').onclick = function () {
|
||||
const steps = ['s1', 's2', 's3']
|
||||
const tags = { s1: '未开始', s2: '未开始', s3: '未开始' }
|
||||
steps.forEach((s) => {
|
||||
setTag(s + 'tag', 'no', tags[s])
|
||||
setNum(s + 'num', false)
|
||||
$(s + 'pre').textContent = '(尚未执行)'
|
||||
const info = $(s + 'info')
|
||||
if (info) info.style.display = 'none'
|
||||
})
|
||||
$('s1progress').style.display = 'none'
|
||||
$('s3list').style.display = 'none'
|
||||
$('step1').disabled = false
|
||||
$('step2').disabled = true
|
||||
$('step2x').disabled = true
|
||||
$('step3').disabled = true
|
||||
setTag('allTag', 'no', '就绪')
|
||||
$('runAll').disabled = false
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
204
scripts/opencode-automation.mjs
Normal file
204
scripts/opencode-automation.mjs
Normal file
@ -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)
|
||||
})
|
||||
34
test/event_privacy.test.js
Normal file
34
test/event_privacy.test.js
Normal file
@ -0,0 +1,34 @@
|
||||
const assert = require('assert')
|
||||
const eventPrivacy = require('../module/event_privacy')
|
||||
|
||||
describe('event privacy modules', () => {
|
||||
it('updates event privacy with the official client parameters', async () => {
|
||||
let captured
|
||||
const expected = { status: 200, body: { code: 200 }, cookie: [] }
|
||||
const result = await eventPrivacy(
|
||||
{ evId: '123456789012345678', privacy: '0', cookie: { MUSIC_U: 'x' } },
|
||||
async (uri, data, options) => {
|
||||
captured = { uri, data, options }
|
||||
return expected
|
||||
},
|
||||
)
|
||||
|
||||
assert.strictEqual(result, expected)
|
||||
assert.strictEqual(captured.uri, '/api/event/privacy/op')
|
||||
assert.deepStrictEqual(captured.data, {
|
||||
eventId: '123456789012345678',
|
||||
privacy: 0,
|
||||
})
|
||||
assert.strictEqual(captured.options.crypto, '')
|
||||
})
|
||||
|
||||
it('rejects unknown event privacy values before making a request', async () => {
|
||||
let requested = false
|
||||
const result = await eventPrivacy({ evId: '1', privacy: '3' }, async () => {
|
||||
requested = true
|
||||
})
|
||||
|
||||
assert.strictEqual(requested, false)
|
||||
assert.strictEqual(result.status, 400)
|
||||
})
|
||||
})
|
||||
145
test/user_event_all.test.js
Normal file
145
test/user_event_all.test.js
Normal file
@ -0,0 +1,145 @@
|
||||
const assert = require('assert')
|
||||
const userEventAll = require('../module/user_event_all')
|
||||
|
||||
describe('all current user events module', () => {
|
||||
it('resolves the current user and aggregates every upstream page', async () => {
|
||||
const calls = []
|
||||
const pages = [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
events: [{ id: 1 }, { id: 2 }],
|
||||
size: 5,
|
||||
more: true,
|
||||
lasttime: 100,
|
||||
},
|
||||
cookie: ['page=1'],
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
events: [{ id: 2 }, { id: 3 }],
|
||||
size: 5,
|
||||
more: true,
|
||||
lasttime: 50,
|
||||
},
|
||||
cookie: ['page=2'],
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
events: [{ id: 4 }],
|
||||
size: 1,
|
||||
more: false,
|
||||
lasttime: 0,
|
||||
},
|
||||
cookie: ['page=3'],
|
||||
},
|
||||
]
|
||||
|
||||
const result = await userEventAll(
|
||||
{ cookie: { MUSIC_U: 'x' } },
|
||||
async (uri, data, options) => {
|
||||
calls.push({ uri, data, options })
|
||||
if (uri === '/api/nuser/account/get') {
|
||||
return {
|
||||
status: 200,
|
||||
body: { code: 200, account: { id: 42 } },
|
||||
cookie: ['account=1'],
|
||||
}
|
||||
}
|
||||
return pages.shift()
|
||||
},
|
||||
)
|
||||
|
||||
assert.deepStrictEqual(
|
||||
result.body.events.map((event) => event.id),
|
||||
[1, 2, 3, 4],
|
||||
)
|
||||
assert.strictEqual(result.body.size, 5)
|
||||
assert.strictEqual(result.body.retrievedCount, 4)
|
||||
assert.strictEqual(result.body.unavailableCount, 1)
|
||||
assert.strictEqual(result.body.sizeMismatch, true)
|
||||
assert.strictEqual(result.body.pageCount, 3)
|
||||
assert.strictEqual(result.body.more, false)
|
||||
assert.deepStrictEqual(result.cookie, [
|
||||
'account=1',
|
||||
'page=1',
|
||||
'page=2',
|
||||
'page=3',
|
||||
])
|
||||
|
||||
assert.strictEqual(calls[0].uri, '/api/nuser/account/get')
|
||||
assert.strictEqual(calls[0].options.crypto, 'weapi')
|
||||
assert.deepStrictEqual(
|
||||
calls.slice(1).map((call) => call.data),
|
||||
[
|
||||
{
|
||||
getcounts: true,
|
||||
time: -1,
|
||||
limit: 100,
|
||||
total: false,
|
||||
fromRN: 'true',
|
||||
},
|
||||
{
|
||||
getcounts: true,
|
||||
time: 100,
|
||||
limit: 100,
|
||||
total: false,
|
||||
fromRN: 'true',
|
||||
},
|
||||
{
|
||||
getcounts: true,
|
||||
time: 50,
|
||||
limit: 100,
|
||||
total: false,
|
||||
fromRN: 'true',
|
||||
},
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
it('requires a valid login cookie', async () => {
|
||||
const result = await userEventAll({}, async () => ({
|
||||
status: 200,
|
||||
body: { code: 200, account: null, profile: null },
|
||||
cookie: [],
|
||||
}))
|
||||
|
||||
assert.strictEqual(result.status, 401)
|
||||
assert.strictEqual(result.body.code, 401)
|
||||
})
|
||||
|
||||
it('fails instead of returning a partial list when the cursor stalls', async () => {
|
||||
let page = 0
|
||||
const result = await userEventAll({}, async (uri) => {
|
||||
if (uri === '/api/nuser/account/get') {
|
||||
return {
|
||||
status: 200,
|
||||
body: { code: 200, account: { id: 42 } },
|
||||
cookie: [],
|
||||
}
|
||||
}
|
||||
|
||||
page += 1
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
events: [],
|
||||
size: 1,
|
||||
more: true,
|
||||
lasttime: 100,
|
||||
},
|
||||
cookie: [],
|
||||
}
|
||||
})
|
||||
|
||||
assert.strictEqual(page, 2)
|
||||
assert.strictEqual(result.status, 502)
|
||||
assert.match(result.body.message, /cursor stalled/)
|
||||
})
|
||||
})
|
||||
129
test/voice_upload.test.js
Normal file
129
test/voice_upload.test.js
Normal file
@ -0,0 +1,129 @@
|
||||
const assert = require('assert')
|
||||
const voiceUpload = require('../module/voice_upload')
|
||||
|
||||
function createVoiceUploadHarness(uploadPlugin) {
|
||||
const requestCalls = []
|
||||
|
||||
return {
|
||||
requestCalls,
|
||||
dependencies: {
|
||||
uploadPlugin,
|
||||
axios: async (options) => {
|
||||
if (options.method === 'post' && options.url.endsWith('?uploads')) {
|
||||
return {
|
||||
data: '<InitiateMultipartUploadResult><UploadId>upload-id</UploadId></InitiateMultipartUploadResult>',
|
||||
}
|
||||
}
|
||||
|
||||
if (options.method === 'put') {
|
||||
return { headers: { etag: 'etag-1' } }
|
||||
}
|
||||
|
||||
return { data: {} }
|
||||
},
|
||||
},
|
||||
request: async (uri, data, options) => {
|
||||
requestCalls.push({ uri, data, options })
|
||||
|
||||
if (uri === '/api/nos/token/alloc') {
|
||||
return {
|
||||
body: {
|
||||
result: {
|
||||
objectKey: 'voice/audio.mp3',
|
||||
docId: 'audio-doc-id',
|
||||
token: 'nos-token',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (uri === '/api/voice/workbench/voice/batch/upload/v2') {
|
||||
return { body: { data: { voiceId: 'voice-id' } } }
|
||||
}
|
||||
|
||||
return { body: { code: 200 } }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createVoiceUploadQuery(overrides = {}) {
|
||||
const data = Buffer.from('audio')
|
||||
|
||||
return {
|
||||
songFile: {
|
||||
name: 'episode.mp3',
|
||||
mimetype: 'audio/mpeg',
|
||||
size: data.length,
|
||||
data,
|
||||
},
|
||||
voiceListId: 'voice-list-id',
|
||||
categoryId: 'category-id',
|
||||
secondCategoryId: 'second-category-id',
|
||||
description: 'episode description',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('voice upload cover', () => {
|
||||
it('uses the uploaded image as the cover for every voice submission', async () => {
|
||||
let uploadedQuery
|
||||
const harness = createVoiceUploadHarness(async (query) => {
|
||||
uploadedQuery = query
|
||||
return { imgId: 'uploaded-cover-id' }
|
||||
})
|
||||
const query = createVoiceUploadQuery({
|
||||
imgFile: {
|
||||
name: 'cover.jpg',
|
||||
mimetype: 'image/jpeg',
|
||||
data: Buffer.from('image'),
|
||||
},
|
||||
coverImgId: 'fallback-cover-id',
|
||||
})
|
||||
|
||||
const result = await voiceUpload(
|
||||
query,
|
||||
harness.request,
|
||||
harness.dependencies,
|
||||
)
|
||||
const voiceCalls = harness.requestCalls.filter((call) =>
|
||||
call.uri.startsWith('/api/voice/workbench/voice/batch/upload'),
|
||||
)
|
||||
|
||||
assert.strictEqual(uploadedQuery, query)
|
||||
assert.strictEqual(voiceCalls.length, 2)
|
||||
voiceCalls.forEach((call) => {
|
||||
const [voiceData] = JSON.parse(call.data.voiceData)
|
||||
assert.strictEqual(voiceData.coverImgId, 'uploaded-cover-id')
|
||||
})
|
||||
assert.deepStrictEqual(result, {
|
||||
status: 200,
|
||||
body: {
|
||||
code: 200,
|
||||
data: { voiceId: 'voice-id' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps using coverImgId when no image file is uploaded', async () => {
|
||||
let uploaded = false
|
||||
const harness = createVoiceUploadHarness(async () => {
|
||||
uploaded = true
|
||||
return { imgId: 'unexpected-cover-id' }
|
||||
})
|
||||
|
||||
await voiceUpload(
|
||||
createVoiceUploadQuery({ coverImgId: 'existing-cover-id' }),
|
||||
harness.request,
|
||||
harness.dependencies,
|
||||
)
|
||||
|
||||
const voiceCalls = harness.requestCalls.filter((call) =>
|
||||
call.uri.startsWith('/api/voice/workbench/voice/batch/upload'),
|
||||
)
|
||||
assert.strictEqual(uploaded, false)
|
||||
voiceCalls.forEach((call) => {
|
||||
const [voiceData] = JSON.parse(call.data.voiceData)
|
||||
assert.strictEqual(voiceData.coverImgId, 'existing-cover-id')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,15 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "commonjs",
|
||||
"module": "node16",
|
||||
"experimentalDecorators": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"esnext",
|
||||
"esnext.asynciterable",
|
||||
"dom"
|
||||
],
|
||||
"moduleResolution": "node16",
|
||||
"lib": ["esnext", "esnext.asynciterable", "dom"],
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"sourceMap": true,
|
||||
@ -17,15 +12,10 @@
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"./*"
|
||||
],
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["./*"],
|
||||
"@neteasecloudmusicapienhanced/api": ["./interface.d.ts"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@ -11,11 +11,13 @@
|
||||
},
|
||||
"APP_CONF": {
|
||||
"apiDomain": "https://interface.music.163.com",
|
||||
"eapiDomain": "https://interfacepc.music.163.com",
|
||||
"xeapiDomain": "https://interface3.music.163.com",
|
||||
"domain": "https://music.163.com",
|
||||
"clDomian": "https://clientlog.music.163.com",
|
||||
"clDomian3": "https://clientlog3.music.163.com",
|
||||
"dunDomain": "https://ac.dun.163yun.com",
|
||||
"dunDomainV3": "https://ac.dun.163yun.com",
|
||||
"dunStaticDomain": "https://acstatic-dun.126.net",
|
||||
"encrypt": true,
|
||||
"encryptResponse": false,
|
||||
"clientSign": "18:C0:4D:B9:8F:FE@@@453832335F384641365F424635335F303030315F303031425F343434415F343643365F333638332@@@@@@6ff673ef74955b38bce2fa8562d95c976ed4758b1227c4e9ee345987cee17bc9",
|
||||
|
||||
@ -34,12 +34,12 @@ const aesEncrypt = (text, mode, key, iv, format = 'base64') => {
|
||||
|
||||
return encrypted.ciphertext.toString().toUpperCase()
|
||||
}
|
||||
const aesDecrypt = (ciphertext, key, iv, format = 'base64') => {
|
||||
const aesDecrypt = (ciphertext, mode, key, iv, format = 'base64') => {
|
||||
let bytes
|
||||
if (format === 'base64') {
|
||||
bytes = CryptoJS.AES.decrypt(ciphertext, CryptoJS.enc.Utf8.parse(key), {
|
||||
iv: CryptoJS.enc.Utf8.parse(iv),
|
||||
mode: CryptoJS.mode.ECB,
|
||||
mode: CryptoJS.mode[mode.toUpperCase()],
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
})
|
||||
} else {
|
||||
@ -48,7 +48,7 @@ const aesDecrypt = (ciphertext, key, iv, format = 'base64') => {
|
||||
CryptoJS.enc.Utf8.parse(key),
|
||||
{
|
||||
iv: CryptoJS.enc.Utf8.parse(iv),
|
||||
mode: CryptoJS.mode.ECB,
|
||||
mode: CryptoJS.mode[mode.toUpperCase()],
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
},
|
||||
)
|
||||
@ -97,7 +97,7 @@ const eapi = (url, object) => {
|
||||
const eapiResDecrypt = (encryptedParams, aeapi = false) => {
|
||||
// 使用aesDecrypt解密参数
|
||||
try {
|
||||
const decrypted = aesDecrypt(encryptedParams, eapiKey, '', 'hex') // WordArray
|
||||
const decrypted = aesDecrypt(encryptedParams, 'ecb', eapiKey, '', 'hex') // WordArray
|
||||
|
||||
if (aeapi) {
|
||||
// 带压缩的解密:先转 Base64 再解压
|
||||
@ -120,6 +120,7 @@ const eapiReqDecrypt = (encryptedParams) => {
|
||||
// 使用 aesDecrypt 解密参数
|
||||
const decryptedData = aesDecrypt(
|
||||
encryptedParams,
|
||||
'ecb',
|
||||
eapiKey,
|
||||
'',
|
||||
'hex',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const createOption = (query, crypto = '') => {
|
||||
const createOption = (query, crypto = '', checkToken = false) => {
|
||||
return {
|
||||
crypto: query.crypto || crypto || '',
|
||||
cookie: query.cookie || process.env.NETEASE_COOKIE,
|
||||
@ -11,7 +11,7 @@ const createOption = (query, crypto = '') => {
|
||||
: ['true', true].includes(query.randomCNIP),
|
||||
e_r: query.e_r || undefined,
|
||||
domain: query.domain || '',
|
||||
checkToken: query.checkToken || false,
|
||||
checkToken: query.checkToken || checkToken,
|
||||
headers: query.headers || {},
|
||||
timeout: query.timeout || 0,
|
||||
}
|
||||
|
||||
@ -17,8 +17,13 @@ const {
|
||||
generateRandomChineseIP,
|
||||
} = require('./index')
|
||||
const { URLSearchParams, URL } = require('url')
|
||||
const { APP_CONF } = require('../util/config.json')
|
||||
const { getToken: antiCheatToken } = require('../module/register_checktoken')
|
||||
const { APP_CONF } = require('./config.json')
|
||||
const {
|
||||
getToken: antiCheatTokenV2,
|
||||
} = require('../module/register_checktoken_v2')
|
||||
const {
|
||||
getToken: antiCheatTokenV3,
|
||||
} = require('../module/register_checktoken_v3')
|
||||
|
||||
// 预先读取匿名token并缓存
|
||||
const anonymous_token = fs.readFileSync(
|
||||
@ -107,9 +112,9 @@ const userAgentMap = {
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||
},
|
||||
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.0.18.203152',
|
||||
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)',
|
||||
},
|
||||
}
|
||||
@ -117,6 +122,7 @@ const userAgentMap = {
|
||||
// 预先定义常量
|
||||
const DOMAIN = APP_CONF.domain
|
||||
const API_DOMAIN = APP_CONF.apiDomain
|
||||
const EAPI_DOMAIN = APP_CONF.eapiDomain
|
||||
const XEAPI_DOMAIN = APP_CONF.xeapiDomain
|
||||
const ENCRYPT_RESPONSE = APP_CONF.encryptResponse
|
||||
const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
|
||||
@ -181,8 +187,18 @@ const generateRequestId = () => {
|
||||
.padStart(4, '0')}`
|
||||
}
|
||||
|
||||
const createRequest = (uri, data, options) => {
|
||||
const token = options.checkToken ? antiCheatToken() : ''
|
||||
const createRequest = async (uri, data, options) => {
|
||||
let token = ''
|
||||
switch (options.checkToken) {
|
||||
case 'v2':
|
||||
// 每次实时获取反作弊 token,不缓存
|
||||
token = await antiCheatTokenV2()
|
||||
break
|
||||
case 'v3':
|
||||
// 每次实时获取反作弊 token,不缓存
|
||||
token = await antiCheatTokenV3()
|
||||
break
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 变量声明和初始化
|
||||
@ -229,6 +245,9 @@ const createRequest = (uri, data, options) => {
|
||||
headers['Referer'] = options.domain || DOMAIN
|
||||
headers['User-Agent'] = options.ua || chooseUserAgent('weapi')
|
||||
data.csrf_token = csrfToken
|
||||
if (options.checkToken) {
|
||||
headers['X-antiCheatToken'] = token
|
||||
}
|
||||
encryptData = encrypt.weapi(data)
|
||||
url = (options.domain || DOMAIN) + '/weapi/' + uri.substr(5)
|
||||
break
|
||||
@ -329,7 +348,7 @@ const createRequest = (uri, data, options) => {
|
||||
data.header = header
|
||||
|
||||
encryptData = encrypt.eapi(uri, data)
|
||||
url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5)
|
||||
url = (options.domain || EAPI_DOMAIN) + '/eapi/' + uri.substr(5)
|
||||
} else if (crypto === 'api') {
|
||||
url = (options.domain || API_DOMAIN) + uri
|
||||
encryptData = data
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user