mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 14:41:23 +08:00
262 lines
7.4 KiB
JavaScript
262 lines
7.4 KiB
JavaScript
/**
|
|
* 统一运行时状态容器。
|
|
*
|
|
* 历史上 anonymous_token / xeapi_public_key 以“文件当 IPC”的方式存放在
|
|
* os.tmpdir()(/tmp)。这在可移植性上存在两个问题:
|
|
* 1. 并非所有平台都提供可写的 /tmp(如 Android / iOS / 嵌入式 / serverless);
|
|
* 2. main.js / request.js 会在 require 阶段就执行文件 I/O,只读文件系统上直接
|
|
* 导致模块加载失败。
|
|
*
|
|
* 但这些值本质上只是进程内的动态配置,读写双方都在同一个进程里,完全没有
|
|
* 落盘的必要。本模块把它们收敛到统一的内存状态:
|
|
* - 默认纯内存,get/set 绝不做任何文件 I/O(可在只读 FS / serverless 安全使用);
|
|
* - 可选地通过 loadSeed() 从环境变量或旧 /tmp 文件读取“种子”,仅用于平滑迁移;
|
|
* - 支持懒加载与按需刷新,适配 serverless 冷启动(不假设启动时已刷新过)。
|
|
*/
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const os = require('os')
|
|
|
|
/** @type {string} */
|
|
let anonymousToken = ''
|
|
/** @type {Record<string, any> | null} */
|
|
let xeapiPublicKey = null
|
|
|
|
/** 进行中的刷新 Promise,用于并发去重 */
|
|
let anonymousTokenTask = null
|
|
let xeapiPublicKeyTask = null
|
|
|
|
/**
|
|
* 防重入标记:刷新凭证本身也要走 util/request 发 HTTP 请求,
|
|
* 若不跳过 request 层的“缺失即刷新”逻辑,会等待自己造成死锁。
|
|
*/
|
|
let anonymousTokenRefreshing = false
|
|
let xeapiPublicKeyRefreshing = false
|
|
|
|
/** 旧版本遗留的磁盘路径,仅作为可选迁移种子读取,永不写入 */
|
|
const legacyTmpPath = (() => {
|
|
try {
|
|
return os.tmpdir()
|
|
} catch (_) {
|
|
return ''
|
|
}
|
|
})()
|
|
|
|
const safeReadFile = (filePath) => {
|
|
if (!filePath) return null
|
|
try {
|
|
return fs.readFileSync(filePath, 'utf-8')
|
|
} catch (_) {
|
|
// 文件不存在 / 不可读 / 只读文件系统:静默降级
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 读取 anonymous_token 的可选种子(环境变量优先,其次旧 /tmp 文件)。
|
|
* 仅在显式调用时读取,绝不在 require 阶段触发。
|
|
* @returns {string}
|
|
*/
|
|
const readAnonymousTokenSeed = () => {
|
|
const fromEnv = process.env.NETEASE_ANONYMOUS_TOKEN
|
|
if (fromEnv) return fromEnv.trim()
|
|
|
|
const fromFile = safeReadFile(
|
|
legacyTmpPath ? path.resolve(legacyTmpPath, 'anonymous_token') : '',
|
|
)
|
|
return fromFile ? fromFile.trim() : ''
|
|
}
|
|
|
|
/**
|
|
* 读取 xeapi_public_key 的可选种子(环境变量优先,其次旧 /tmp 文件)。
|
|
* @returns {Record<string, any> | null}
|
|
*/
|
|
const readXeapiPublicKeySeed = () => {
|
|
const fromEnv = process.env.NETEASE_XEAPI_PUBLIC_KEY
|
|
const raw =
|
|
fromEnv ||
|
|
safeReadFile(
|
|
legacyTmpPath ? path.resolve(legacyTmpPath, 'xeapi_public_key') : '',
|
|
)
|
|
if (!raw) return null
|
|
try {
|
|
return JSON.parse(raw)
|
|
} catch (_) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取当前 anonymous_token(纯内存,不触发任何 I/O)。
|
|
* @returns {string}
|
|
*/
|
|
const getAnonymousToken = () => anonymousToken
|
|
|
|
/**
|
|
* 写入 anonymous_token。
|
|
* @param {string} token
|
|
*/
|
|
const setAnonymousToken = (token) => {
|
|
if (typeof token === 'string' && token) {
|
|
anonymousToken = token
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 确保 anonymous_token 可用:优先返回内存值,缺失时刷新一次。
|
|
* 并发调用会共享同一个刷新任务,避免重复请求。
|
|
* @param {() => Promise<string>} refresh 刷新函数,返回新的 token
|
|
* @returns {Promise<string>}
|
|
*/
|
|
const ensureAnonymousToken = async (refresh) => {
|
|
const cached = getAnonymousToken()
|
|
if (cached) return cached
|
|
if (typeof refresh !== 'function') return cached
|
|
|
|
if (!anonymousTokenTask) {
|
|
anonymousTokenRefreshing = true
|
|
anonymousTokenTask = Promise.resolve()
|
|
.then(refresh)
|
|
.then((token) => {
|
|
setAnonymousToken(token)
|
|
return token || ''
|
|
})
|
|
.finally(() => {
|
|
anonymousTokenTask = null
|
|
anonymousTokenRefreshing = false
|
|
})
|
|
}
|
|
return anonymousTokenTask
|
|
}
|
|
|
|
/**
|
|
* 获取当前 xeapi public key 状态(纯内存,不触发任何 I/O)。
|
|
* @returns {Record<string, any> | null}
|
|
*/
|
|
const getXeapiPublicKey = () => xeapiPublicKey
|
|
|
|
/**
|
|
* 写入 xeapi public key 状态。
|
|
* @param {Record<string, any>} publicKey
|
|
*/
|
|
const setXeapiPublicKey = (publicKey) => {
|
|
if (publicKey && typeof publicKey === 'object') {
|
|
xeapiPublicKey = publicKey
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 合并并写入 xeapi public key,保留旧状态里的 sk(服务端并非每次下发 sk)。
|
|
* @param {Record<string, any>} publicKey
|
|
* @returns {Record<string, any>}
|
|
*/
|
|
const mergeXeapiPublicKey = (publicKey) => {
|
|
const previous = getXeapiPublicKey() || {}
|
|
const merged = { ...publicKey }
|
|
if (!merged.sk && previous.sk) {
|
|
merged.sk = previous.sk
|
|
}
|
|
xeapiPublicKey = merged
|
|
return merged
|
|
}
|
|
|
|
/**
|
|
* 确保 xeapi public key 可用。
|
|
*
|
|
* 语义:有缓存且未声明 force 时直接复用;否则调用 refresh 刷新,并发去重。
|
|
* “version 是否变化”由调用方判断并通过 options.force 表达。
|
|
*
|
|
* @param {() => Promise<Record<string, any>>} refresh 刷新函数,返回新的 public key 状态
|
|
* @param {{ force?: boolean }} [options]
|
|
* @returns {Promise<Record<string, any> | null>}
|
|
*/
|
|
const ensureXeapiPublicKey = async (refresh, options = {}) => {
|
|
const cached = getXeapiPublicKey()
|
|
if (cached && !options.force) {
|
|
return cached
|
|
}
|
|
if (typeof refresh !== 'function') {
|
|
return cached
|
|
}
|
|
|
|
if (!xeapiPublicKeyTask) {
|
|
xeapiPublicKeyRefreshing = true
|
|
xeapiPublicKeyTask = Promise.resolve()
|
|
.then(refresh)
|
|
.then((result) => mergeXeapiPublicKey(result || {}))
|
|
.finally(() => {
|
|
xeapiPublicKeyTask = null
|
|
xeapiPublicKeyRefreshing = false
|
|
})
|
|
}
|
|
return xeapiPublicKeyTask
|
|
}
|
|
|
|
/**
|
|
* 刷新类请求是否正在进行中。request 层据此跳过“缺失即刷新”,避免死锁。
|
|
* @returns {boolean}
|
|
*/
|
|
const isRefreshing = () => anonymousTokenRefreshing || xeapiPublicKeyRefreshing
|
|
|
|
/**
|
|
* 匿名 token 刷新是否进行中。
|
|
* @returns {boolean}
|
|
*/
|
|
const isAnonymousTokenRefreshing = () => anonymousTokenRefreshing
|
|
|
|
/**
|
|
* xeapi public key 刷新是否进行中。
|
|
* @returns {boolean}
|
|
*/
|
|
const isXeapiPublicKeyRefreshing = () => xeapiPublicKeyRefreshing
|
|
|
|
/**
|
|
* 清空内存状态。仅用于测试,或需要强制重新初始化的场景。
|
|
*/
|
|
const reset = () => {
|
|
anonymousToken = ''
|
|
xeapiPublicKey = null
|
|
anonymousTokenTask = null
|
|
xeapiPublicKeyTask = null
|
|
anonymousTokenRefreshing = false
|
|
xeapiPublicKeyRefreshing = false
|
|
}
|
|
|
|
/**
|
|
* 从环境变量 / 旧 /tmp 文件加载可选种子到内存。
|
|
* 默认不启用——由 app/generateConfig 等启动路径显式调用;
|
|
* 测试与只读 FS 场景可以不调用,从而完全避免文件 I/O。
|
|
*
|
|
* @returns {{ anonymousToken: string, xeapiPublicKey: Record<string, any> | null }}
|
|
*/
|
|
const loadSeed = () => {
|
|
const token = readAnonymousTokenSeed()
|
|
if (token && !anonymousToken) {
|
|
anonymousToken = token
|
|
}
|
|
const publicKey = readXeapiPublicKeySeed()
|
|
if (publicKey && !xeapiPublicKey) {
|
|
xeapiPublicKey = publicKey
|
|
}
|
|
return { anonymousToken, xeapiPublicKey }
|
|
}
|
|
|
|
module.exports = {
|
|
getAnonymousToken,
|
|
setAnonymousToken,
|
|
ensureAnonymousToken,
|
|
getXeapiPublicKey,
|
|
setXeapiPublicKey,
|
|
mergeXeapiPublicKey,
|
|
ensureXeapiPublicKey,
|
|
isRefreshing,
|
|
isAnonymousTokenRefreshing,
|
|
isXeapiPublicKeyRefreshing,
|
|
loadSeed,
|
|
reset,
|
|
// 供 generateConfig 等迁移逻辑按需加载种子
|
|
readAnonymousTokenSeed,
|
|
readXeapiPublicKeySeed,
|
|
}
|