Compare commits

..
Author SHA1 Message Date
Takanashi Hoshino 1bdc85998b bump 5.0.0 2026-10-17 20:21:42 +08:00
Takanashi Hoshino 49c7800afe build(package): 更换pkg依赖并提升node要求至22 2026-10-17 19:48:29 +08:00
19 changed files with 1175 additions and 735 deletions
+4 -4
View File
@@ -14,15 +14,15 @@ jobs:
include: include:
- os: ubuntu-latest - os: ubuntu-latest
platform: linux platform: linux
target: node18-linux-x64 target: node22-linux-x64
output: precompiled/app output: precompiled/app
- os: windows-latest - os: windows-latest
platform: win platform: win
target: node18-win-x64 target: node22-win-x64
output: precompiled/app.exe output: precompiled/app.exe
- os: macos-latest - os: macos-latest
platform: macos platform: macos
target: node18-macos-x64 target: node22-macos-x64
output: precompiled/app output: precompiled/app
steps: steps:
@@ -32,7 +32,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: '18' node-version: '24'
- name: Setup pnpm - name: Setup pnpm
run: npm install -g pnpm@9 run: npm install -g pnpm@9
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [18, 22, 24] node-version: [22, 24]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
+1 -1
View File
@@ -59,7 +59,7 @@ jobs:
GITHUB_TOKEN: ${{ steps.token.outputs.token }} GITHUB_TOKEN: ${{ steps.token.outputs.token }}
OPENCODE_PERMISSION: '{"permission":{"*":"allow","question":"deny"}}' OPENCODE_PERMISSION: '{"permission":{"*":"allow","question":"deny"}}'
with: with:
model: opencode/glm-5.3-flash-free model: opencode/muse-spark-1.3-contributor-free
use_github_token: true use_github_token: true
prompt: | prompt: |
${{ steps.cfg.outputs.opencode_prompt }} ${{ steps.cfg.outputs.opencode_prompt }}
@@ -78,17 +78,17 @@ jobs:
include: include:
- os: ubuntu-latest - os: ubuntu-latest
platform: linux platform: linux
target: node18-linux-x64 target: node22-linux-x64
output: ncm-api-linux-x64 output: ncm-api-linux-x64
- os: windows-latest - os: windows-latest
platform: windows platform: windows
target: node18-win-x64 target: node22-win-x64
output: ncm-api-win-x64.exe output: ncm-api-win-x64.exe
- os: macos-latest - os: macos-latest
platform: macos platform: macos
target: node18-macos-x64 target: node22-macos-x64
output: ncm-api-macos-x64 output: ncm-api-macos-x64
steps: steps:
+2 -2
View File
@@ -2,7 +2,7 @@
## 快速开始 ## 快速开始
- **包管理器**:开发与 CI 使用 `pnpm`,`Dockerfile` 也用 pnpm(`pnpm@9 --frozen-lockfile`,匹配 `pnpm-lock.yaml`)。仓库没有 `yarn.lock`,不要引入 yarn。 - **包管理器**:开发与 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 均可。 - **Node 版本**:`package.json` 的 `engines` 声明 `>=22`;CI/打包在 Node 22–24 上运行。现代 Node 均可。
- **环境变量**:`server.js` 调用了 `dotenv.config()`,本地 `.env` 会被自动加载;所有支持的变量见 `.env.prod.example`。 - **环境变量**:`server.js` 调用了 `dotenv.config()`,本地 `.env` 会被自动加载;所有支持的变量见 `.env.prod.example`。
## 常用命令 ## 常用命令
@@ -11,7 +11,7 @@
- 跑测试:`pnpm test`(Mocha,超时 60s) - 跑测试:`pnpm test`(Mocha,超时 60s)
- Lint:`pnpm lint`;自动修复:`pnpm lint-fix` - Lint:`pnpm lint`;自动修复:`pnpm lint-fix`
- 文档格式化检查/修复:`pnpm docs:check` / `pnpm docs:format` - 文档格式化检查/修复:`pnpm docs:check` / `pnpm docs:format`
- 打包独立二进制:`pnpm pkgwin` / `pkglinux` / `pkgmacos` - 打包独立二进制:`pnpm pkgwin` / `pkglinux` / `pkgmacos`(@yao-pkg/pkg,node22 目标,`--no-bytecode` + `--public` 规避 node22 bytecode 回归)
## 目录内非路由文件 ## 目录内非路由文件
- `plugins/`——内部上传辅助(`upload.js`、`songUpload.js`),被 `module/*` require,本身不是路由。 - `plugins/`——内部上传辅助(`upload.js`、`songUpload.js`),被 `module/*` require,本身不是路由。
+1 -1
View File
@@ -163,7 +163,7 @@ docker run -d -p 3000:3000 moefurina/ncm-api:latest
```bash ```bash
#!/bin/bash #!/bin/bash
export PORT=9000 export PORT=9000
/var/lang/node16/bin/node app.js /var/lang/node22/bin/node app.js
``` ```
4. 完成部署后,访问 API 网关的 URL 即可 4. 完成部署后,访问 API 网关的 URL 即可
+11
View File
@@ -3,6 +3,17 @@ const fs = require('fs')
const path = require('path') const path = require('path')
const tmpPath = require('os').tmpdir() const tmpPath = require('os').tmpdir()
// engines 字段只在 npm/pnpm 安装时生效,直接 node app.js 不会被拦截,
// 这里给出友好的最低版本检查(>=22,与 package.json engines 保持一致)
const [major] = process.versions.node.split('.').map(Number)
if (major < 22) {
console.error(
`[${new Date().toLocaleString()}] Node.js >= 22 is required (current: ${process.versions.node}). ` +
'请升级 Node.js 后重试 / Please upgrade Node.js to v22 or newer.',
)
process.exit(1)
}
async function start() { async function start() {
// 检测是否存在 anonymous_token 文件,没有则生成 // 检测是否存在 anonymous_token 文件,没有则生成
if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) { if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) {
-1
View File
@@ -1,6 +1,5 @@
export interface RequestBaseConfig { export interface RequestBaseConfig {
cookie?: string cookie?: string
headers?: Record<string, string | string[]>
realIP?: string // IPv4/IPv6 filled in X-Real-IP realIP?: string // IPv4/IPv6 filled in X-Real-IP
proxy?: string // HTTP proxy proxy?: string // HTTP proxy
randomCNIP?: boolean // Whether to use a random Chinese IP address filled in X-Real-IP randomCNIP?: boolean // Whether to use a random Chinese IP address filled in X-Real-IP
+3 -1
View File
@@ -25,7 +25,9 @@ moduleFiles.forEach((file) => {
obj[fn] = function (data = {}) { obj[fn] = function (data = {}) {
const cookie = const cookie =
typeof data.cookie === 'string' ? cookieToJson(data.cookie) : data.cookie typeof data.cookie === 'string'
? cookieToJson(data.cookie)
: data.cookie || {}
return fileModule( return fileModule(
{ {
+2 -7
View File
@@ -1,15 +1,10 @@
const QRCode = require('qrcode') const QRCode = require('qrcode')
const { generateChainId, getCookieHeader, hasCookie } = require('../util/index') const { generateChainId } = require('../util/index')
module.exports = (query) => { module.exports = (query) => {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
const platform = query.platform || 'pc' const platform = query.platform || 'pc'
const headerCookie = getCookieHeader(query.headers) const cookie = query.cookie || ''
const cookie = hasCookie(query.cookie)
? query.cookie
: hasCookie(headerCookie)
? headerCookie
: ''
// 构建基础URL // 构建基础URL
let url = `https://music.163.com/login?codekey=${query.key}` let url = `https://music.163.com/login?codekey=${query.key}`
+2 -10
View File
@@ -1,26 +1,18 @@
// 听歌打卡 // 听歌打卡
const createOption = require('../util/option.js') const createOption = require('../util/option.js')
const { getCookieHeader, hasCookie } = require('../util/index')
const { APP_CONF } = require('../util/config.json') const { APP_CONF } = require('../util/config.json')
const DOMAIN = APP_CONF.clDomian const DOMAIN = APP_CONF.clDomian
module.exports = async (query, request) => { module.exports = async (query, request) => {
// 注入 os=osx 的 cookie // 注入 os=osx 的 cookie
const headerCookie = getCookieHeader(query.headers) let cookie = query.cookie || ''
let cookie = hasCookie(query.cookie)
? query.cookie
: hasCookie(headerCookie)
? headerCookie
: process.env.NETEASE_COOKIE || ''
if (typeof cookie === 'object') { if (typeof cookie === 'object') {
cookie = Object.assign({ os: 'osx' }, cookie) cookie = Object.assign({ os: 'osx' }, cookie)
} else if (typeof cookie === 'string') { } else if (typeof cookie === 'string') {
if (cookie.indexOf('os=') > -1) { if (cookie.indexOf('os=') > -1) {
cookie = cookie.replace(/os=[^;]+/g, 'os=osx') cookie = cookie.replace(/os=[^;]+/g, 'os=osx')
} else if (cookie) {
cookie = cookie + '; os=osx'
} else { } else {
cookie = 'os=osx' cookie = cookie + '; os=osx'
} }
} else { } else {
cookie = 'os=osx' cookie = 'os=osx'
+1 -7
View File
@@ -13,7 +13,6 @@ const {
buildMetaJson, buildMetaJson,
doUpload, doUpload,
} = require('../util/ncbl') } = require('../util/ncbl')
const { getCookieHeader, hasCookie } = require('../util/index')
module.exports = async (query, request) => { module.exports = async (query, request) => {
// --- 参数校验 --- // --- 参数校验 ---
@@ -33,12 +32,7 @@ module.exports = async (query, request) => {
const sourceName = query.source || 'list' const sourceName = query.source || 'list'
// --- 解析认证上下文 --- // --- 解析认证上下文 ---
const headerCookie = getCookieHeader(query.headers) const rawCookie = query.cookie || ''
const rawCookie = hasCookie(query.cookie)
? query.cookie
: hasCookie(headerCookie)
? headerCookie
: process.env.NETEASE_COOKIE || ''
const cookieObj = parseCookie(rawCookie) const cookieObj = parseCookie(rawCookie)
cookieObj.os = 'pc' cookieObj.os = 'pc'
const ctx = extractContext(cookieObj) const ctx = extractContext(cookieObj)
+12 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "@neteasecloudmusicapienhanced/api", "name": "@neteasecloudmusicapienhanced/api",
"version": "4.41.1", "version": "5.0.0",
"description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced)", "description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced)",
"scripts": { "scripts": {
"dev": "nodemon app.js", "dev": "nodemon app.js",
@@ -11,9 +11,9 @@
"prepare": "husky install", "prepare": "husky install",
"docs:format": "node scripts/format-docs.js", "docs:format": "node scripts/format-docs.js",
"docs:check": "node scripts/format-docs.js --check", "docs:check": "node scripts/format-docs.js --check",
"pkgwin": "node scripts/build-pkg.js . -t node18-win-x64 -C GZip -o precompiled/app", "pkgwin": "node scripts/build-pkg.js . -t node22-win-x64 -C GZip -o precompiled/app --no-bytecode --public-packages \"*\" --public",
"pkglinux": "node scripts/build-pkg.js . -t node18-linux-x64 -C GZip -o precompiled/app", "pkglinux": "node scripts/build-pkg.js . -t node22-linux-x64 -C GZip -o precompiled/app --no-bytecode --public-packages \"*\" --public",
"pkgmacos": "node scripts/build-pkg.js . -t node18-macos-x64 -C GZip -o precompiled/app" "pkgmacos": "node scripts/build-pkg.js . -t node22-macos-x64 -C GZip -o precompiled/app --no-bytecode --public-packages \"*\" --public"
}, },
"bin": "./app.js", "bin": "./app.js",
"pkg": { "pkg": {
@@ -52,7 +52,7 @@
}, },
"homepage": "https://neteasecloudmusicapienhanced.js.org/", "homepage": "https://neteasecloudmusicapienhanced.js.org/",
"engines": { "engines": {
"node": ">=12" "node": ">=22"
}, },
"lint-staged": { "lint-staged": {
"*.js": [ "*.js": [
@@ -79,7 +79,7 @@
"axios": "^1.20.0", "axios": "^1.20.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.3.0", "express": "^5.2.1",
"express-fileupload": "^1.5.2", "express-fileupload": "^1.5.2",
"fzstd": "^0.1.1", "fzstd": "^0.1.1",
"gzip": "^0.1.0", "gzip": "^0.1.0",
@@ -100,21 +100,21 @@
"@types/express-fileupload": "^1.5.1", "@types/express-fileupload": "^1.5.1",
"@types/mocha": "^10.0.10", "@types/mocha": "^10.0.10",
"@types/node": "25.9.2", "@types/node": "25.9.2",
"@typescript-eslint/eslint-plugin": "^8.72.0", "@typescript-eslint/eslint-plugin": "^8.71.0",
"@typescript-eslint/parser": "^8.72.0", "@typescript-eslint/parser": "^8.71.0",
"eslint": "^9.39.5", "eslint": "^9.39.5",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-html": "^8.2.1", "eslint-plugin-html": "^8.2.1",
"eslint-plugin-prettier": "^5.5.7", "eslint-plugin-prettier": "^5.5.6",
"globals": "^17.13.0", "globals": "^17.13.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"intelli-espower-loader": "^1.1.0", "intelli-espower-loader": "^1.1.0",
"lint-staged": "^16.4.0", "lint-staged": "^16.4.0",
"mocha": "^12.0.3", "mocha": "^11.8.0",
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"pkg": "^5.8.1", "@yao-pkg/pkg": "^6.23.0",
"power-assert": "^1.6.1", "power-assert": "^1.6.1",
"prettier": "^3.9.10", "prettier": "^3.9.9",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"publishConfig": { "publishConfig": {
+1094 -603
View File
File diff suppressed because it is too large Load Diff
+16 -8
View File
@@ -1,10 +1,10 @@
/** /**
* Invoke the pkg packager (cross-platform, independent of node_modules layout). * Invoke the pkg packager (cross-platform, independent of node_modules layout).
* *
* Background: on Windows, pnpm links node_modules/pkg to * Background: on Windows, pnpm links node_modules/@yao-pkg/pkg to
* node_modules/.pnpm/pkg@<ver>/node_modules/pkg via a junction. Some CI * node_modules/.pnpm/@yao-pkg+pkg@<ver>/node_modules/@yao-pkg/pkg via a junction.
* environments (pnpm 11 + Windows runner) may fail to create that link, * Some CI environments (pnpm 11 + Windows runner) may fail to create that link,
* so executing node_modules/pkg/lib-es5/bin.js directly throws * so executing node_modules/@yao-pkg/pkg/lib-es5/bin.js directly throws
* MODULE_NOT_FOUND. * MODULE_NOT_FOUND.
* *
* This script first resolves pkg via require.resolve and falls back to * This script first resolves pkg via require.resolve and falls back to
@@ -17,16 +17,24 @@ const path = require('path')
function resolvePkgBin() { function resolvePkgBin() {
try { try {
return require.resolve('pkg/lib-es5/bin.js') return require.resolve('@yao-pkg/pkg/lib-es5/bin.js')
} catch (err) { } catch (err) {
// require.resolve failed (e.g. missing node_modules/pkg link): scan .pnpm // require.resolve failed (e.g. missing node_modules/@yao-pkg/pkg link): scan .pnpm
const pnpmDir = path.join(process.cwd(), 'node_modules', '.pnpm') const pnpmDir = path.join(process.cwd(), 'node_modules', '.pnpm')
if (fs.existsSync(pnpmDir)) { if (fs.existsSync(pnpmDir)) {
const candidates = fs const candidates = fs
.readdirSync(pnpmDir) .readdirSync(pnpmDir)
.filter((dir) => dir.startsWith('pkg@')) .filter((dir) => dir.startsWith('@yao-pkg+pkg@'))
.map((dir) => .map((dir) =>
path.join(pnpmDir, dir, 'node_modules', 'pkg', 'lib-es5', 'bin.js'), path.join(
pnpmDir,
dir,
'node_modules',
'@yao-pkg',
'pkg',
'lib-es5',
'bin.js',
),
) )
.filter((file) => fs.existsSync(file)) .filter((file) => fs.existsSync(file))
if (candidates.length) { if (candidates.length) {
+10 -17
View File
@@ -6,7 +6,7 @@ const request = require('./util/request')
const packageJSON = require('./package.json') const packageJSON = require('./package.json')
const exec = require('child_process').exec const exec = require('child_process').exec
const cache = require('./util/apicache').middleware const cache = require('./util/apicache').middleware
const { cookieToJson, getCookieHeader, hasCookie } = require('./util/index') const { cookieToJson } = require('./util/index')
const fileUpload = require('express-fileupload') const fileUpload = require('express-fileupload')
const decode = require('safe-decode-uri-component') const decode = require('safe-decode-uri-component')
const logger = require('./util/logger.js') const logger = require('./util/logger.js')
@@ -225,9 +225,8 @@ async function constructServer(moduleDefs) {
*/ */
app.use((req, _, next) => { app.use((req, _, next) => {
req.cookies = {} req.cookies = {}
getCookieHeader(req.headers) //;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { // Polynomial regular expression //
.split(';') ;(req.headers.cookie || '').split(/;\s+|(?<!\s)\s+$/g).forEach((pair) => {
.forEach((pair) => {
let crack = pair.indexOf('=') let crack = pair.indexOf('=')
if (crack < 1 || crack == pair.length - 1) return if (crack < 1 || crack == pair.length - 1) return
req.cookies[decode(pair.slice(0, crack)).trim()] = decode( req.cookies[decode(pair.slice(0, crack)).trim()] = decode(
@@ -292,19 +291,13 @@ async function constructServer(moduleDefs) {
} }
}) })
const queryCookie = req.query && req.query.cookie let query = Object.assign(
const bodyCookie = req.body && req.body.cookie {},
const fileCookie = req.files && req.files.cookie { cookie: req.cookies },
const cookie = hasCookie(fileCookie) req.query,
? fileCookie req.body,
: hasCookie(bodyCookie) req.files,
? bodyCookie )
: hasCookie(queryCookie)
? queryCookie
: req.cookies
const query = Object.assign({}, req.query, req.body, req.files, {
cookie,
})
try { try {
let usedCrypto = '' let usedCrypto = ''
+4 -33
View File
@@ -94,42 +94,15 @@ module.exports = {
// 优化:使用for循环替代forEach,性能更好 // 优化:使用for循环替代forEach,性能更好
for (let i = 0, len = cookieArr.length; i < len; i++) { for (let i = 0, len = cookieArr.length; i < len; i++) {
let item = cookieArr[i] let item = cookieArr[i]
let separator = item.indexOf('=') let arr = item.split('=')
// 只按第一个等号拆分,保留 Cookie 值中的等号 // 优化:使用严格等于
if (separator > 0) { if (arr.length === 2) {
obj[item.slice(0, separator).trim()] = item.slice(separator + 1).trim() obj[arr[0].trim()] = arr[1].trim()
} }
} }
return obj return obj
}, },
hasCookie(cookie) {
if (typeof cookie === 'string') return cookie.length > 0
return (
cookie !== null &&
typeof cookie === 'object' &&
Object.keys(cookie).length > 0
)
},
getCookieHeader(headers) {
if (!headers || typeof headers !== 'object') return ''
if (typeof headers.get === 'function') {
const cookie = headers.get('cookie')
if (typeof cookie === 'string') return cookie
}
const headerName = Object.keys(headers).find(
(name) => name.toLowerCase() === 'cookie',
)
if (!headerName) return ''
const cookie = headers[headerName]
if (Array.isArray(cookie)) return cookie.join('; ')
return typeof cookie === 'string' ? cookie : ''
},
cookieObjToString(cookie) { cookieObjToString(cookie) {
// 优化:使用预绑定的keys函数和for循环 // 优化:使用预绑定的keys函数和for循环
const cookieKeys = keys(cookie) const cookieKeys = keys(cookie)
@@ -253,8 +226,6 @@ const cookieToJsonOptimized = (function() {
function getCookieValue(cookieStr, name) { function getCookieValue(cookieStr, name) {
if (!cookieStr) return '' if (!cookieStr) return ''
if (typeof cookieStr === 'object') return cookieStr[name] || ''
const cookies = '; ' + cookieStr const cookies = '; ' + cookieStr
const parts = cookies.split('; ' + name + '=') const parts = cookies.split('; ' + name + '=')
if (parts.length === 2) return parts.pop().split(';').shift() if (parts.length === 2) return parts.pop().split(';').shift()
+1 -9
View File
@@ -1,15 +1,7 @@
const { getCookieHeader, hasCookie } = require('./index')
const createOption = (query, crypto = '', checkToken = false) => { const createOption = (query, crypto = '', checkToken = false) => {
const headerCookie = getCookieHeader(query.headers)
return { return {
crypto: query.crypto || crypto || '', crypto: query.crypto || crypto || '',
cookie: hasCookie(query.cookie) cookie: query.cookie || process.env.NETEASE_COOKIE,
? query.cookie
: hasCookie(headerCookie)
? headerCookie
: process.env.NETEASE_COOKIE,
ua: query.ua || '', ua: query.ua || '',
proxy: query.proxy, proxy: query.proxy,
realIP: query.realIP, realIP: query.realIP,
+1 -9
View File
@@ -13,8 +13,6 @@ const tmpPath = require('os').tmpdir()
const { const {
cookieToJson, cookieToJson,
cookieObjToString, cookieObjToString,
getCookieHeader,
hasCookie,
toBoolean, toBoolean,
generateRandomChineseIP, generateRandomChineseIP,
} = require('./index') } = require('./index')
@@ -245,19 +243,13 @@ const createRequest = async (uri, data, options) => {
headers['X-Forwarded-For'] = ip headers['X-Forwarded-For'] = ip
} }
let cookie = hasCookie(options.cookie) let cookie = options.cookie || {}
? options.cookie
: getCookieHeader(options.headers)
if (!hasCookie(cookie)) cookie = {}
if (typeof cookie === 'string') { if (typeof cookie === 'string') {
cookie = cookieToJson(cookie) cookie = cookieToJson(cookie)
} }
if (typeof cookie === 'object') { if (typeof cookie === 'object') {
cookie = processCookieObject(cookie, crypto) cookie = processCookieObject(cookie, crypto)
Object.keys(headers).forEach((name) => {
if (name.toLowerCase() === 'cookie') delete headers[name]
})
headers['Cookie'] = cookieObjToString(cookie) headers['Cookie'] = cookieObjToString(cookie)
} }
let url = '' let url = ''