refactors

This commit is contained in:
Takanashi Hoshino
2026-09-13 12:04:37 +08:00
parent c478367c6c
commit ddfd680301
15 changed files with 196 additions and 651 deletions
+45 -250
View File
@@ -1,261 +1,56 @@
/**
* 统一运行时状态容器。
*
* 历史上 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 冷启动(不假设启动时已刷新过)。
*/
// 统一的运行时动态状态存储(仅内存,不落盘)
//
// 存放需要在进程内共享、且会在运行期间变化的动态配置,
// 目前包括匿名令牌(MUSIC_A)与 xeapi 公钥。
//
// 设计约定:
// - 只存在于内存中,不做任何文件 I/O —— 在没有 /tmp 或临时目录
// 不可写的平台(Android / iOS / 嵌入式 / serverless)上同样可用
// - 写入方(module/register_anonimous.js、module/register_xeapikey.js)
// 在成功获取新值后立即写入,读取方(util/request.js)每次请求实时读取,
// 进程内即时生效,无需重启
// - 凭证缺失时可通过 ensure() 按需刷新,并发调用共享同一次刷新任务
// - 以后新增的动态配置请放在这里,不要再往 os.tmpdir() 写文件
const fs = require('fs')
const path = require('path')
const os = require('os')
const runtimeState = {
/** 匿名令牌(MUSIC_A cookie),由 module/register_anonimous.js 成功后写入 */
anonymousToken: '',
/** @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
}
/**
* xeapi 公钥(含 sk / version / deviceId 等字段),
* 由 module/register_xeapikey.js 成功后写入
*/
xeapiPublicKey: 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() : ''
}
/** 进行中的按需刷新任务:并发调用共享同一次刷新,避免重复请求 */
const refreshTasks = new Map()
/**
* 读取 xeapi_public_key 的可选种子(环境变量优先,其次旧 /tmp 文件)。
* @returns {Record<string, any> | null}
* 确保某项动态状态可用:已有值时直接返回;缺失时执行 refresh 刷新
* (refresh 成功后需自行把新值写入 runtimeState[key])。
* 并发调用会共享同一个进行中的刷新任务。
*
* @param {string} key runtimeState 上的字段名
* @param {() => Promise} refresh 刷新函数
* @returns {Promise<*>} 该状态当前的值(刷新失败时抛出错误)
*/
const readXeapiPublicKeySeed = () => {
const fromEnv = process.env.NETEASE_XEAPI_PUBLIC_KEY
const raw =
fromEnv ||
safeReadFile(
legacyTmpPath ? path.resolve(legacyTmpPath, 'xeapi_public_key') : '',
runtimeState.ensure = async (key, refresh) => {
if (runtimeState[key]) return runtimeState[key]
if (!refreshTasks.has(key)) {
refreshTasks.set(
key,
Promise.resolve()
.then(refresh)
.finally(() => {
refreshTasks.delete(key)
}),
)
if (!raw) return null
try {
return JSON.parse(raw)
} catch (_) {
return null
}
await refreshTasks.get(key)
return runtimeState[key]
}
/**
* 获取当前 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,
}
module.exports = runtimeState