Compare commits

...

4 Commits

Author SHA1 Message Date
MoeFurina
8bb68accee ci(action): fix workflows which pnpm is not installed 2025-11-01 13:23:30 +08:00
MoeFurina
55858d89d9 ci(action): switch GitHub Actions to use pnpm and update actions 2025-11-01 13:16:50 +08:00
MoeFurina
597ac1498f fix(eslint): fix files which have eslint problems 2025-11-01 13:11:38 +08:00
MoeFurina
398f410109 fix: update music search keyword and test case ID
- Changed the default search keyword in the API documentation from "这么可爱真是抱歉" to "妖精小姐的魔法邀约".
- Updated the test case for fetching song URL to use the correct song ID (from 1969519579 to 2756058128).
2025-11-01 12:16:29 +08:00
16 changed files with 855 additions and 665 deletions

View File

@ -1,57 +0,0 @@
module.exports = {
root: true,
ignorePatterns: ['public/'],
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['html'],
extends: ['plugin:prettier/recommended'],
env: {
browser: true,
node: true,
},
rules: {
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
indent: ['error', 2, { SwitchCase: 1 }],
'space-infix-ops': ['error', { int32Hint: false }],
'key-spacing': [
2,
{
beforeColon: false,
afterColon: true,
},
],
'no-octal': 2,
'no-redeclare': 2,
'comma-spacing': 2,
'no-new-object': 2,
'arrow-spacing': 2,
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
},
overrides: [
{
files: ['**/*.ts'],
parser: '@typescript-eslint/parser',
extends: [
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
// 'prettier/@typescript-eslint',
],
},
],
}

View File

@ -2,9 +2,9 @@ name: Node.js CI
on: on:
pull_request: pull_request:
branches: [ main ] branches: [main]
push: push:
branches: [ main ] branches: [main]
permissions: permissions:
contents: read contents: read
@ -14,46 +14,43 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [14.x, 16.x, 18.x ] node-version: [16.x, 18.x, 22.x]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5 uses: actions/setup-node@v5
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- uses: actions/cache@v4 # Use built-in package manager cache for pnpm
with: cache: 'pnpm'
path: ~/.npm cache-dependency-path: '**/pnpm-lock.yaml'
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - name: Install pnpm
restore-keys: | run: npm install -g pnpm
${{ runner.os }}-node- - name: Install dependencies (pnpm)
- run: npm ci run: pnpm install --frozen-lockfile
name: Install dependencies - name: Test
- name: Test env:
env: NCM_API_TEST_LOGIN_COUNTRY_CODE: ${{ secrets.NCM_API_TEST_LOGIN_COUNTRY_CODE }}
NCM_API_TEST_LOGIN_COUNTRY_CODE: ${{ secrets.NCM_API_TEST_LOGIN_COUNTRY_CODE }} NCM_API_TEST_LOGIN_PHONE: ${{ secrets.NCM_API_TEST_LOGIN_PHONE }}
NCM_API_TEST_LOGIN_PHONE: ${{ secrets.NCM_API_TEST_LOGIN_PHONE }} NCM_API_TEST_LOGIN_PASSWORD: ${{ secrets.NCM_API_TEST_LOGIN_PASSWORD }}
NCM_API_TEST_LOGIN_PASSWORD: ${{ secrets.NCM_API_TEST_LOGIN_PASSWORD }} run: pnpm test
run: npm test
lint: lint:
name: Lint name: Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [14.x] node-version: [18.x]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v5 uses: actions/setup-node@v5
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- uses: actions/cache@v4 cache: 'pnpm'
with: cache-dependency-path: '**/pnpm-lock.yaml'
path: ~/.npm - name: Install pnpm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} run: npm install -g pnpm
restore-keys: | - name: Install dependencies (pnpm)
${{ runner.os }}-node- run: pnpm install --frozen-lockfile
- run: npm ci - name: Lint
name: Install dependencies run: pnpm run lint
- name: Lint
run: npm run lint

91
eslint.config.js Normal file
View File

@ -0,0 +1,91 @@
const { defineConfig, globalIgnores } = require('eslint/config')
const html = require('eslint-plugin-html')
const globals = require('globals')
const tsParser = require('@typescript-eslint/parser')
const js = require('@eslint/js')
const { FlatCompat } = require('@eslint/eslintrc')
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
module.exports = defineConfig([
{
languageOptions: {
ecmaVersion: 2018,
sourceType: 'module',
parserOptions: {
parser: 'babel-eslint',
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
html,
},
extends: compat.extends('plugin:prettier/recommended'),
rules: {
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
indent: [
'error',
2,
{
SwitchCase: 1,
},
],
'space-infix-ops': [
'error',
{
int32Hint: false,
},
],
'key-spacing': [
2,
{
beforeColon: false,
afterColon: true,
},
],
'no-octal': 2,
'no-redeclare': 2,
'comma-spacing': 2,
'no-new-object': 2,
'arrow-spacing': 2,
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
},
},
globalIgnores(['**/public/']),
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
},
extends: compat.extends('plugin:@typescript-eslint/recommended'),
},
])

View File

@ -5,31 +5,34 @@ const createOption = require('../util/option.js')
const logger = require('../util/logger.js') const logger = require('../util/logger.js')
module.exports = async (query, request) => { module.exports = async (query, request) => {
try { try {
const match = require("@unblockneteasemusic/server") const match = require('@unblockneteasemusic/server')
const source = query.source const source = query.source
? query.source.split(',') : ['pyncmd', 'bodian','kuwo', 'qq', 'migu', 'kugou'] ? query.source.split(',')
const server = query.server ? query.server.split(',') : query.server : ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou']
const result = await match(query.id, !server? source : server) const server = query.server ? query.server.split(',') : query.server
const proxy = process.env.PROXY_URL; const result = await match(query.id, !server ? source : server)
logger.info("开始解灰", query.id, result) const proxy = process.env.PROXY_URL
const useProxy = process.env.ENABLE_PROXY || "false" logger.info('开始解灰', query.id, result)
if (result.url.includes('kuwo')) { result.proxyUrl = useProxy === 'true' ? proxy + result.url : result.url } const useProxy = process.env.ENABLE_PROXY || 'false'
return { if (result.url.includes('kuwo')) {
status: 200, result.proxyUrl = useProxy === 'true' ? proxy + result.url : result.url
body: {
code: 200,
data: result,
},
}
} catch (e) {
return {
status: 500,
body: {
code: 500,
msg: e.message || 'unblock error',
data: [],
},
}
} }
} return {
status: 200,
body: {
code: 200,
data: result,
},
}
} catch (e) {
return {
status: 500,
body: {
code: 500,
msg: e.message || 'unblock error',
data: [],
},
}
}
}

View File

@ -5,71 +5,73 @@
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
module.exports = async (query, request) => { module.exports = async (query, request) => {
try { try {
const { id, br = "320" } = query; const { id, br = '320' } = query
if (!id) { if (!id) {
return { return {
status: 400, status: 400,
body: { body: {
code: 400, code: 400,
message: "缺少必要参数 id", message: '缺少必要参数 id',
data: [], data: [],
}, },
}; }
}
const validBR = ["128", "192", "320", "740", "999"];
// const covertBR = ["128000", "192000", "320000","740000", "999000"];
if (!validBR.includes(br)) {
return {
status: 400,
body: {
code: 400,
message: "无效音质参数",
allowed_values: validBR,
data: [],
},
};
}
const apiUrl = new URL("https://music-api.gdstudio.xyz/api.php");
apiUrl.searchParams.append("types", "url");
apiUrl.searchParams.append("id", id);
apiUrl.searchParams.append("br", br);
const response = await fetch(apiUrl.toString());
if (!response.ok) throw new Error(`API 响应状态: ${response.status}`);
const result = await response.json();
// 代理逻辑
const useProxy = process.env.ENABLE_PROXY || false;
const proxy = process.env.PROXY_URL;
if (useProxy && result.url && result.url.includes("kuwo")) {
result.proxyUrl = proxy + result.url.replace(/^http:\/\//, "http/");
}
return {
status: 200,
body: {
code: 200,
message: "请求成功",
data: {
id,
br,
url: result.url,
...(proxy && result.proxyUrl ? { proxyUrl: result.proxyUrl } : {})
}
}
};
} catch (error) {
console.error("Error in song_url_ncmget:", error);
return {
status: 500,
body: {
code: 500,
message: "服务器处理请求失败",
...(process.env.NODE_ENV === "development" ? { error: error.message } : {}),
data: [],
},
};
} }
const validBR = ['128', '192', '320', '740', '999']
// const covertBR = ['128000', '192000', '320000','740000', '999000']
if (!validBR.includes(br)) {
return {
status: 400,
body: {
code: 400,
message: '无效音质参数',
allowed_values: validBR,
data: [],
},
}
}
const apiUrl = new URL('https://music-api.gdstudio.xyz/api.php')
apiUrl.searchParams.append('types', 'url')
apiUrl.searchParams.append('id', id)
apiUrl.searchParams.append('br', br)
const response = await fetch(apiUrl.toString())
if (!response.ok) throw new Error(`API 响应状态: ${response.status}`)
const result = await response.json()
// 代理逻辑
const useProxy = process.env.ENABLE_PROXY || false
const proxy = process.env.PROXY_URL
if (useProxy && result.url && result.url.includes('kuwo')) {
result.proxyUrl = proxy + result.url.replace(/^http:\/\//, 'http/')
}
return {
status: 200,
body: {
code: 200,
message: '请求成功',
data: {
id,
br,
url: result.url,
...(proxy && result.proxyUrl ? { proxyUrl: result.proxyUrl } : {}),
},
},
}
} catch (error) {
console.error('Error in song_url_ncmget:', error)
return {
status: 500,
body: {
code: 500,
message: '服务器处理请求失败',
...(process.env.NODE_ENV === 'development'
? { error: error.message }
: {}),
data: [],
},
}
}
} }

View File

@ -5,31 +5,34 @@ const createOption = require('../util/option.js')
const logger = require('../util/logger.js') const logger = require('../util/logger.js')
module.exports = async (query, request) => { module.exports = async (query, request) => {
try { try {
const match = require("@unblockneteasemusic/server") const match = require('@unblockneteasemusic/server')
const source = query.source const source = query.source
? query.source.split(',') : ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou'] ? query.source.split(',')
const server = query.server ? query.server.split(',') : query.server : ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou']
const result = await match(query.id, !server? source : server) const server = query.server ? query.server.split(',') : query.server
const proxy = process.env.PROXY_URL; const result = await match(query.id, !server ? source : server)
logger.info("开始解灰", query.id, result) const proxy = process.env.PROXY_URL
const useProxy = process.env.ENABLE_PROXY || "false" logger.info('开始解灰', query.id, result)
if (result.url.includes('kuwo') && useProxy === "true") { result.proxyUrl = proxy + result.url } const useProxy = process.env.ENABLE_PROXY || 'false'
return { if (result.url.includes('kuwo')) {
status: 200, result.proxyUrl = useProxy === 'true' ? proxy + result.url : result.url
body: {
code: 200,
data: result,
},
}
} catch (e) {
return {
status: 500,
body: {
code: 500,
msg: e.message || 'unblock error',
data: [],
},
}
} }
} return {
status: 200,
body: {
code: 200,
data: result,
},
}
} catch (e) {
return {
status: 500,
body: {
code: 500,
msg: e.message || 'unblock error',
data: [],
},
}
}
}

View File

@ -18,12 +18,24 @@ module.exports = async (query, request) => {
try { try {
const result = await match(query.id, source) const result = await match(query.id, source)
logger.info('开始解灰', query.id, result) logger.info('开始解灰', query.id, result)
if (result.url.includes('kuwo')) { // avoid optional chaining for compatibility
const useProxy = process.env.ENABLE_PROXY || 'false' let url
var proxyUrl = useProxy === 'true' ? process.env.PROXY_URL + result.url : result.url if (Array.isArray(result)) {
url = result[0] && result[0].url ? result[0].url : result[0]
} else {
url = result && result.url ? result.url : result
} }
let url = Array.isArray(result) ? (result[0]?.url || result[0]) : (result.url || result) // decide proxyUrl after we resolved the actual url value
let proxyUrl = ''
if (url) { if (url) {
if (url.includes('kuwo')) {
const useProxy = process.env.ENABLE_PROXY || 'false'
if (useProxy === 'true' && process.env.PROXY_URL) {
proxyUrl = process.env.PROXY_URL + url
} else {
proxyUrl = url
}
}
return { return {
status: 200, status: 200,
body: { body: {

View File

@ -1,8 +1,9 @@
{ {
"name": "@neteasecloudmusicapienhanced/api", "name": "@neteasecloudmusicapienhanced/api",
"version": "4.29.12", "version": "4.29.13",
"description": "为停更的网易云音乐 NodeJs API 提供持续的维护!", "description": "为停更的网易云音乐 NodeJs API 提供持续的维护!",
"scripts": { "scripts": {
"dev": "nodemon app.js",
"start": "node app.js", "start": "node app.js",
"test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit", "test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit",
"lint": "eslint \"**/*.{js,ts}\"", "lint": "eslint \"**/*.{js,ts}\"",
@ -66,7 +67,7 @@
], ],
"dependencies": { "dependencies": {
"@unblockneteasemusic/server": "^0.28.0", "@unblockneteasemusic/server": "^0.28.0",
"axios": "^1.12.2", "axios": "^1.13.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.1.0", "express": "^5.1.0",
@ -82,20 +83,24 @@
"yargs": "^18.0.0" "yargs": "^18.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.4", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.39.0",
"@types/express": "^5.0.5",
"@types/express-fileupload": "^1.5.1", "@types/express-fileupload": "^1.5.1",
"@types/mocha": "^10.0.10", "@types/mocha": "^10.0.10",
"@types/node": "24.9.1", "@types/node": "24.9.1",
"@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "5.62.0", "@typescript-eslint/parser": "8.46.2",
"eslint": "8.7.0", "eslint": "9.39.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-plugin-html": "8.1.3", "eslint-plugin-html": "8.1.3",
"eslint-plugin-prettier": "5.5.4", "eslint-plugin-prettier": "5.5.4",
"globals": "^16.4.0",
"husky": "9.1.7", "husky": "9.1.7",
"intelli-espower-loader": "1.1.0", "intelli-espower-loader": "1.1.0",
"lint-staged": "16.2.6", "lint-staged": "16.2.6",
"mocha": "11.7.4", "mocha": "11.7.4",
"nodemon": "^3.1.10",
"pkg": "^5.8.1", "pkg": "^5.8.1",
"power-assert": "1.6.1", "power-assert": "1.6.1",
"prettier": "3.6.2", "prettier": "3.6.2",

769
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -60,7 +60,7 @@
<section class="block"> <section class="block">
<h2>常用接口</h2> <h2>常用接口</h2>
<ul class="links"> <ul class="links">
<li><a href="/search?keywords=这么可爱真是抱歉">搜索音乐: <code>GET /search</code></a></li> <li><a href="/search?keywords=妖精小姐的魔法邀约">搜索音乐: <code>GET /search</code></a></li>
<li><a href="/song/detail?ids=2756058128">获取音乐详情: <code>GET /song/detail</code></a></li> <li><a href="/song/detail?ids=2756058128">获取音乐详情: <code>GET /song/detail</code></a></li>
<li><a href="/comment/music?id=2756058128&limit=1">获取音乐评论: <code>GET /comment/music</code></a></li> <li><a href="/comment/music?id=2756058128&limit=1">获取音乐评论: <code>GET /comment/music</code></a></li>
<li><a href="/song/url/v1?id=2756058128&level=exhigh">获取音乐播放链接: <code>GET /song/url/v1</code></a></li> <li><a href="/song/url/v1?id=2756058128&level=exhigh">获取音乐播放链接: <code>GET /song/url/v1</code></a></li>
@ -71,7 +71,7 @@
<h2>调试部分</h2> <h2>调试部分</h2>
<pre><code>curl -s {origin}/inner/version <pre><code>curl -s {origin}/inner/version
curl -s {origin}/search?keywords=网易云</code></pre> curl -s {origin}/search?keywords=网易云</code></pre>
<p style="margin-top:10px"> · <a href="/api.html">交互式调试</a> · <a href="/qrlogin.html">二维码登录示例</a> · <a href="/unblock_test.html">解灰测试</a></p> · <a href="/audio_match_demo/index.html">听歌识曲 Demo</a></p> · <a href="/unblock_test.html">云盘上传</a></p> · <a href="/playlist_import.html">歌单导入</a></p> · <a href="/eapi_decrypt.html">EAPI 解密</p> <p style="margin-top:10px"> · <a href="/api.html">交互式调试</a> · <a href="/qrlogin.html">二维码登录示例</a> · <a href="/unblock_test.html">解灰测试</a></p> · <a href="/audio_match_demo/index.html">听歌识曲 Demo</a></p> · <a href="/cloud.html">云盘上传</a></p> · <a href="/playlist_import.html">歌单导入</a></p> · <a href="/eapi_decrypt.html">EAPI 解密</p>
</section> </section>
<footer class="site-footer"> <footer class="site-footer">

View File

@ -1,4 +1,4 @@
require("dotenv").config(); require('dotenv').config()
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const express = require('express') const express = require('express')
@ -248,20 +248,28 @@ async function consturctServer(moduleDefs) {
(req.baseUrl === '/song/url/v1' || req.baseUrl === '/song/url') && (req.baseUrl === '/song/url/v1' || req.baseUrl === '/song/url') &&
process.env.ENABLE_GENERAL_UNBLOCK === 'true' process.env.ENABLE_GENERAL_UNBLOCK === 'true'
) { ) {
const song = moduleResponse['body']['data'][0] const song = moduleResponse.body.data[0]
if (song.freeTrialInfo !== null || !song.url || [1, 4].includes(song.fee)) { if (
const match = require('@unblockneteasemusic/server') song.freeTrialInfo !== null ||
const source = process.env.UNBLOCK_SOURCE ? process.env.UNBLOCK_SOURCE.split(',') : ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou'] !song.url ||
logger.info("开始解灰", source) [1, 4].includes(song.fee)
const { url } = await match(req.query.id, source) ) {
song.url = url const match = require('@unblockneteasemusic/server')
song.freeTrialInfo = 'null' const source = process.env.UNBLOCK_SOURCE
logger.info("解灰成功!") ? process.env.UNBLOCK_SOURCE.split(',')
: ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou']
logger.info('开始解灰', source)
const { url } = await match(req.query.id, source)
song.url = url
song.freeTrialInfo = 'null'
logger.info('解灰成功!')
} }
if (song.url.includes('kuwo')) { if (song.url && song.url.includes('kuwo')) {
const proxy = process.env.PROXY_URL; const proxy = process.env.PROXY_URL
const useProxy = process.env.ENABLE_PROXY || 'false' const useProxy = process.env.ENABLE_PROXY || 'false'
if (useProxy === 'true' && proxy) {song.proxyUrl = proxy + song.url} if (useProxy === 'true' && proxy) {
song.proxyUrl = proxy + song.url
}
} }
} }

View File

@ -4,7 +4,7 @@ const host = global.host || 'http://localhost:3000'
describe('测试获取歌曲是否正常', () => { describe('测试获取歌曲是否正常', () => {
it('歌曲的 url 不应该为空', (done) => { it('歌曲的 url 不应该为空', (done) => {
const qs = { const qs = {
id: 1969519579, id: 2756058128,
br: 999000, br: 999000,
realIP: global.cnIp, realIP: global.cnIp,
} }

View File

@ -155,9 +155,12 @@ function ApiCache() {
} }
// add automatic cache clearing from duration, includes max limit on setTimeout // add automatic cache clearing from duration, includes max limit on setTimeout
timers[key] = setTimeout(function () { timers[key] = setTimeout(
instance.clear(key, true) function () {
}, Math.min(duration, 2147483647)) instance.clear(key, true)
},
Math.min(duration, 2147483647),
)
} }
function accumulateContent(res, content) { function accumulateContent(res, content) {

View File

@ -1,5 +1,5 @@
const crypto = require("crypto"); const crypto = require('crypto')
const os = require("os"); const os = require('os')
class AdvancedClientSignGenerator { class AdvancedClientSignGenerator {
/** /**
@ -7,25 +7,25 @@ class AdvancedClientSignGenerator {
*/ */
static getRealMacAddress() { static getRealMacAddress() {
try { try {
const interfaces = os.networkInterfaces(); const interfaces = os.networkInterfaces()
for (let interfaceName in interfaces) { for (let interfaceName in interfaces) {
const interface = interfaces[interfaceName]; const networkInterface = interfaces[interfaceName]
for (let i = 0; i < interface.length; i++) { for (let i = 0; i < networkInterface.length; i++) {
const alias = interface[i]; const alias = networkInterface[i]
// 排除内部地址和无效地址 // 排除内部地址和无效地址
if ( if (
alias.mac && alias.mac &&
alias.mac !== "00:00:00:00:00:00" && alias.mac !== '00:00:00:00:00:00' &&
!alias.internal !alias.internal
) { ) {
return alias.mac.toUpperCase(); return alias.mac.toUpperCase()
} }
} }
} }
return null; return null
} catch (error) { } catch (error) {
console.warn("获取MAC地址失败:", error.message); console.warn('获取MAC地址失败:', error.message)
return null; return null
} }
} }
@ -33,108 +33,108 @@ class AdvancedClientSignGenerator {
* 生成随机MAC地址 * 生成随机MAC地址
*/ */
static generateRandomMac() { static generateRandomMac() {
const chars = "0123456789ABCDEF"; const chars = '0123456789ABCDEF'
let mac = ""; let mac = ''
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
if (i > 0) mac += ":"; if (i > 0) mac += ':'
mac += mac +=
chars[Math.floor(Math.random() * 16)] + chars[Math.floor(Math.random() * 16)] +
chars[Math.floor(Math.random() * 16)]; chars[Math.floor(Math.random() * 16)]
} }
// 确保第一个字节是单播地址最低位为0 // 确保第一个字节是单播地址最低位为0
const firstByte = parseInt(mac.substring(0, 2), 16); const firstByte = parseInt(mac.substring(0, 2), 16)
const unicastFirstByte = (firstByte & 0xfe) const unicastFirstByte = (firstByte & 0xfe)
.toString(16) .toString(16)
.padStart(2, "0") .padStart(2, '0')
.toUpperCase(); .toUpperCase()
return unicastFirstByte + mac.substring(2); return unicastFirstByte + mac.substring(2)
} }
/** /**
* 获取MAC地址优先真实否则随机 * 获取MAC地址优先真实否则随机
*/ */
static getMacAddress() { static getMacAddress() {
const realMac = this.getRealMacAddress(); const realMac = this.getRealMacAddress()
if (realMac) { if (realMac) {
return realMac; return realMac
} }
console.warn("无法获取真实MAC地址使用随机生成"); console.warn('无法获取真实MAC地址使用随机生成')
return this.generateRandomMac(); return this.generateRandomMac()
} }
/** /**
* 字符串转HEX编码 * 字符串转HEX编码
*/ */
static stringToHex(str) { static stringToHex(str) {
return Buffer.from(str, "utf8").toString("hex").toUpperCase(); return Buffer.from(str, 'utf8').toString('hex').toUpperCase()
} }
/** /**
* SHA-256哈希 * SHA-256哈希
*/ */
static sha256(data) { static sha256(data) {
return crypto.createHash("sha256").update(data, "utf8").digest("hex"); return crypto.createHash('sha256').update(data, 'utf8').digest('hex')
} }
/** /**
* 生成随机设备ID * 生成随机设备ID
*/ */
static generateRandomDeviceId() { static generateRandomDeviceId() {
const partLengths = [4, 4, 4, 4, 4, 4, 4, 5]; // 各部分长度 const partLengths = [4, 4, 4, 4, 4, 4, 4, 5] // 各部分长度
const chars = "0123456789ABCDEF"; const chars = '0123456789ABCDEF'
const parts = partLengths.map((length) => { const parts = partLengths.map((length) => {
let part = ""; let part = ''
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
part += chars[Math.floor(Math.random() * 16)]; part += chars[Math.floor(Math.random() * 16)]
} }
return part; return part
}); })
return parts.join("_"); return parts.join('_')
} }
/** /**
* 生成随机clientSign优先使用真实MAC否则随机 * 生成随机clientSign优先使用真实MAC否则随机
*/ */
static generateRandomClientSign(secretKey = "") { static generateRandomClientSign(secretKey = '') {
// 获取MAC地址优先真实否则随机 // 获取MAC地址优先真实否则随机
const macAddress = this.getMacAddress(); const macAddress = this.getMacAddress()
// 生成随机设备ID // 生成随机设备ID
const deviceId = this.generateRandomDeviceId(); const deviceId = this.generateRandomDeviceId()
// 转换设备ID为HEX // 转换设备ID为HEX
const hexDeviceId = this.stringToHex(deviceId); const hexDeviceId = this.stringToHex(deviceId)
// 构造签名字符串 // 构造签名字符串
const signString = `${macAddress}@@@${hexDeviceId}`; const signString = `${macAddress}@@@${hexDeviceId}`
// 生成哈希 // 生成哈希
const hash = this.sha256(signString + secretKey); const hash = this.sha256(signString + secretKey)
return `${signString}@@@@@@${hash}`; return `${signString}@@@@@@${hash}`
} }
/** /**
* 批量生成多个随机签名 * 批量生成多个随机签名
*/ */
static generateMultipleRandomSigns(count, secretKey = "") { static generateMultipleRandomSigns(count, secretKey = '') {
const signs = []; const signs = []
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
signs.push(this.generateRandomClientSign(secretKey)); signs.push(this.generateRandomClientSign(secretKey))
} }
return signs; return signs
} }
/** /**
* 使用指定参数生成签名 * 使用指定参数生成签名
*/ */
static generateWithCustomDeviceId(macAddress, deviceId, secretKey = "") { static generateWithCustomDeviceId(macAddress, deviceId, secretKey = '') {
const hexDeviceId = this.stringToHex(deviceId); const hexDeviceId = this.stringToHex(deviceId)
const signString = `${macAddress}@@@${hexDeviceId}`; const signString = `${macAddress}@@@${hexDeviceId}`
const hash = this.sha256(signString + secretKey); const hash = this.sha256(signString + secretKey)
return `${signString}@@@@@@${hash}`; return `${signString}@@@@@@${hash}`
} }
/** /**
@ -142,28 +142,28 @@ class AdvancedClientSignGenerator {
*/ */
static validateClientSign(clientSign) { static validateClientSign(clientSign) {
try { try {
const parts = clientSign.split("@@@@@@"); const parts = clientSign.split('@@@@@@')
if (parts.length !== 2) return false; if (parts.length !== 2) return false
const [infoPart, hash] = parts; const [infoPart, hash] = parts
const infoParts = infoPart.split("@@@"); const infoParts = infoPart.split('@@@')
if (infoParts.length !== 2) return false; if (infoParts.length !== 2) return false
const [mac, hexDeviceId] = infoParts; const [mac, hexDeviceId] = infoParts
// 验证MAC地址格式 // 验证MAC地址格式
const macRegex = /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/; const macRegex = /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/
if (!macRegex.test(mac)) return false; if (!macRegex.test(mac)) return false
// 验证哈希格式 // 验证哈希格式
const hashRegex = /^[0-9a-f]{64}$/; const hashRegex = /^[0-9a-f]{64}$/
if (!hashRegex.test(hash)) return false; if (!hashRegex.test(hash)) return false
return true; return true
} catch (error) { } catch (error) {
return false; return false
} }
} }
} }
module.exports = AdvancedClientSignGenerator; module.exports = AdvancedClientSignGenerator

View File

@ -1,28 +1,42 @@
// ANSI 颜色代码 // ANSI 颜色代码
const colors = { const colors = {
reset: '\x1b[0m', reset: '\x1b[0m',
bright: '\x1b[1m', bright: '\x1b[1m',
dim: '\x1b[2m', dim: '\x1b[2m',
black: '\x1b[30m', black: '\x1b[30m',
red: '\x1b[31m', red: '\x1b[31m',
green: '\x1b[32m', green: '\x1b[32m',
yellow: '\x1b[33m', yellow: '\x1b[33m',
blue: '\x1b[34m', blue: '\x1b[34m',
magenta: '\x1b[35m', magenta: '\x1b[35m',
cyan: '\x1b[36m', cyan: '\x1b[36m',
white: '\x1b[37m', white: '\x1b[37m',
bgRed: '\x1b[41m', bgRed: '\x1b[41m',
bgGreen: '\x1b[42m', bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m' bgYellow: '\x1b[43m',
}; }
const logger = { const logger = {
debug: (msg, ...args) => console.info(`${colors.cyan}[DEBUG]${colors.reset}`, msg, ...args), debug: (msg, ...args) =>
info: (msg, ...args) => console.info(`${colors.green}[INFO]${colors.reset}`, msg, ...args), console.info(`${colors.cyan}[DEBUG]${colors.reset}`, msg, ...args),
warn: (msg, ...args) => console.info(`${colors.yellow}[WARN]${colors.reset}`, msg, ...args), info: (msg, ...args) =>
error: (msg, ...args) => console.error(`${colors.red}[ERROR]${colors.reset}`, msg, ...args), console.info(`${colors.green}[INFO]${colors.reset}`, msg, ...args),
success: (msg, ...args) => console.log(`${colors.bright}${colors.green}[SUCCESS]${colors.reset}`, msg, ...args), warn: (msg, ...args) =>
critical: (msg, ...args) => console.error(`${colors.bright}${colors.bgRed}[CRITICAL]${colors.reset}`, msg, ...args) console.info(`${colors.yellow}[WARN]${colors.reset}`, msg, ...args),
}; error: (msg, ...args) =>
console.error(`${colors.red}[ERROR]${colors.reset}`, msg, ...args),
success: (msg, ...args) =>
console.log(
`${colors.bright}${colors.green}[SUCCESS]${colors.reset}`,
msg,
...args,
),
critical: (msg, ...args) =>
console.error(
`${colors.bright}${colors.bgRed}[CRITICAL]${colors.reset}`,
msg,
...args,
),
}
module.exports = logger; module.exports = logger

View File

@ -101,7 +101,7 @@ const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
// chooseUserAgent函数 // chooseUserAgent函数
const chooseUserAgent = (crypto, uaType = 'pc') => { const chooseUserAgent = (crypto, uaType = 'pc') => {
return userAgentMap[crypto]?.[uaType] || '' return (userAgentMap[crypto] && userAgentMap[crypto][uaType]) || ''
} }
// cookie处理 // cookie处理
@ -153,15 +153,17 @@ const createHeaderCookie = (header) => {
const generateRequestId = () => { const generateRequestId = () => {
return `${now()}_${floor(random() * 1000) return `${now()}_${floor(random() * 1000)
.toString() .toString()
.padStart(4, "0")}`; .padStart(4, '0')}`
} }
const createRequest = (uri, data, options) => { const createRequest = (uri, data, options) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 变量声明和初始化 // 变量声明和初始化
const headers = options.headers ? { ...options.headers } : {} const headers = options.headers ? { ...options.headers } : {}
const ip = options.realIP || options.ip || (options.randomCNIP ? generateRandomChineseIP() : '') const ip =
options.realIP ||
options.ip ||
(options.randomCNIP ? generateRandomChineseIP() : '')
// IP头设置 // IP头设置
if (ip) { if (ip) {
headers['X-Real-IP'] = ip headers['X-Real-IP'] = ip
@ -243,8 +245,8 @@ const createRequest = (uri, data, options) => {
options.e_r !== undefined options.e_r !== undefined
? options.e_r ? options.e_r
: data.e_r !== undefined : data.e_r !== undefined
? data.e_r ? data.e_r
: ENCRYPT_RESPONSE, : ENCRYPT_RESPONSE,
) )
encryptData = encrypt.eapi(uri, data) encryptData = encrypt.eapi(uri, data)
url = API_DOMAIN + '/eapi/' + uri.substr(5) url = API_DOMAIN + '/eapi/' + uri.substr(5)