- mssdk.ts:进程内 bdms.node 加载移除;优先 QS_SIGN_SERVER 调签名服务 (兼容 qs-signer 仓库 server.mjs 与 mssdk-sign 两个实现,/sign 契约互通), 无服务时 QS_HELIOS/QS_MEDUSA 或 data/qs-sign.json 固定值兜底,都没有走安卓自签 - musicSearch:PC 通道签名失败/请求异常/风控空结果自动降级安卓通道(原为同通道重试) - signer/、tools/mssdk-sign/、scripts/qs-local-login.mjs 迁至 qs-signer (gitea.4i.hk/hhhge/qs-signer),soda-mssdk/ 二进制目录一并挪走
178 lines
7.1 KiB
TypeScript
178 lines
7.1 KiB
TypeScript
// 汽水 PC 通道签名(X-Helios / X-Medusa)—— 两种供给方式,按优先级:
|
||
// 1) 签名服务器:环境变量 QS_SIGN_SERVER 指向签名服务(如 http://127.0.0.1:8899,
|
||
// 可选 QS_SIGN_TOKEN 配 Bearer 鉴权),HTTP POST /sign。
|
||
// 契约同时兼容独立仓库 qs-signer(gitea.4i.hk/hhhge/qs-signer,本地与 MusicServer 同级)
|
||
// 的两个服务(请求 {url, headers, body?},响应 {ok, headers, added?}):
|
||
// - server.mjs (Wine + bdms.node)
|
||
// - mssdk-sign/ (metasecml.dll 独立进程,见其 README)
|
||
// 2) 固定值兜底:无签名服务器时,两个加密头直接用抓包固定值 ——
|
||
// 环境变量 QS_HELIOS / QS_MEDUSA,或 data/qs-sign.json:
|
||
// { "helios": "…", "medusa": "…", "device_id": "…", "install_id": "…" }
|
||
// 签名与设备/URL 绑定:固定值必须搭配抓包时的同一套 device_id/install_id
|
||
// (json 里带上即自动对齐 URL 参数),时效自负,被风控时走安卓通道兜底。
|
||
// 两者都没有:PC 通道不可用,搜索/取流自动走安卓自签通道。
|
||
// 官方只对 qishui.com 且 **非** /passport/、非 /ttwid/ 的请求签名 —— 登录链路用不到本模块。
|
||
// 签名覆盖 URL + 全部请求头;POST 的 x-ss-stub(body 的 MD5 大写)既是签名输入也要随请求发出。
|
||
import { existsSync, readFileSync } from 'node:fs'
|
||
import { dirname, join } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { getDeviceIds } from './device.ts'
|
||
|
||
// 仓库根(本文件在 sdk/qs/ 下)——不依赖 process.cwd(),从任何目录启动都能找到
|
||
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||
const FIXED_SIGN_FILE = join(REPO_ROOT, 'data', 'qs-sign.json')
|
||
const SIGN_TIMEOUT = 5000
|
||
|
||
/** 固定值配置(helios/medusa 必填;device_id/install_id 应与抓包时同一套) */
|
||
interface FixedSignConfig {
|
||
helios: string
|
||
medusa: string
|
||
deviceId: string
|
||
installId: string
|
||
}
|
||
|
||
let fixedConfigWarned = false
|
||
|
||
/** 读固定值:环境变量 QS_HELIOS/QS_MEDUSA 优先,其次 data/qs-sign.json;没有返回 null */
|
||
const readFixedConfig = (): FixedSignConfig | null => {
|
||
const envHelios = process.env.QS_HELIOS?.trim()
|
||
const envMedusa = process.env.QS_MEDUSA?.trim()
|
||
if (envHelios && envMedusa) {
|
||
return {
|
||
helios: envHelios,
|
||
medusa: envMedusa,
|
||
deviceId: process.env.QS_SIGN_DEVICE_ID?.trim() ?? '',
|
||
installId: process.env.QS_SIGN_INSTALL_ID?.trim() ?? '',
|
||
}
|
||
}
|
||
if (!existsSync(FIXED_SIGN_FILE)) return null
|
||
try {
|
||
const data = JSON.parse(readFileSync(FIXED_SIGN_FILE, 'utf8')) as Record<string, unknown>
|
||
const helios = String(data.helios ?? '').trim()
|
||
const medusa = String(data.medusa ?? '').trim()
|
||
if (helios && medusa) {
|
||
return {
|
||
helios,
|
||
medusa,
|
||
deviceId: String(data.device_id ?? '').trim(),
|
||
installId: String(data.install_id ?? '').trim(),
|
||
}
|
||
}
|
||
if (!fixedConfigWarned) {
|
||
fixedConfigWarned = true
|
||
console.log(`[qs/mssdk] ${FIXED_SIGN_FILE} 缺少 helios/medusa 字段,固定值模式不启用`)
|
||
}
|
||
} catch (err: any) {
|
||
if (!fixedConfigWarned) {
|
||
fixedConfigWarned = true
|
||
console.log(`[qs/mssdk] 固定值配置解析失败: ${err?.message ?? err}`)
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
type SignMode =
|
||
| { kind: 'server'; server: string; token: string }
|
||
| { kind: 'fixed'; fixed: FixedSignConfig }
|
||
|
||
/** 当前签名供给方式:签名服务器 > 固定值 > 不可用(null) */
|
||
const loadSignMode = (): SignMode | null => {
|
||
const server = process.env.QS_SIGN_SERVER?.trim().replace(/\/+$/, '')
|
||
if (server) return { kind: 'server', server, token: process.env.QS_SIGN_TOKEN?.trim() ?? '' }
|
||
const fixed = readFixedConfig()
|
||
return fixed ? { kind: 'fixed', fixed } : null
|
||
}
|
||
|
||
/** PC 签名是否可用(只看配置;服务器宕机等运行期失败由签名调用返回 null、调用方降级处理) */
|
||
export const mssdkAvailable = (): boolean => Boolean(loadSignMode())
|
||
|
||
const signViaServer = async (
|
||
server: string,
|
||
token: string,
|
||
url: string,
|
||
headers: Record<string, string>,
|
||
body?: string,
|
||
): Promise<Record<string, string> | null> => {
|
||
try {
|
||
const res = await fetch(`${server}/sign`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'content-type': 'application/json',
|
||
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||
},
|
||
body: JSON.stringify({ url, headers, body }),
|
||
signal: AbortSignal.timeout(SIGN_TIMEOUT),
|
||
})
|
||
const json = await res.json().catch(() => null) as any
|
||
if (!res.ok || !json?.ok) {
|
||
console.log(`[qs/mssdk] 签名服务返回异常: HTTP ${res.status} ${json?.error ?? ''}`.trim())
|
||
return null
|
||
}
|
||
// signer/server.mjs 把 x-ss-stub 并进 headers,mssdk-sign 放 added —— 合并即可
|
||
const signed: Record<string, string> = { ...(json.headers ?? {}), ...(json.added ?? {}) }
|
||
if (!signed['X-Helios'] && !signed['x-helios']) {
|
||
console.log('[qs/mssdk] 签名结果缺少 X-Helios')
|
||
return null
|
||
}
|
||
return signed
|
||
} catch (err: any) {
|
||
console.log(`[qs/mssdk] 签名服务不可达: ${err?.message ?? err}`)
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 给一个 qishui.com 请求签名
|
||
* @param headers 必须是「真实要发出去的那一份」(含 cookie / x-ss-stub)
|
||
* @param body POST body(给了且 headers 没带 x-ss-stub 时服务端会自动补)
|
||
* @returns 需追加的头;不可用时返回 null(调用方走安卓通道)
|
||
*/
|
||
export const signQishuiRequest = async (
|
||
url: string,
|
||
headers: Record<string, string>,
|
||
body?: string,
|
||
): Promise<Record<string, string> | null> => {
|
||
const mode = loadSignMode()
|
||
if (!mode) return null
|
||
if (mode.kind === 'fixed') return { 'X-Helios': mode.fixed.helios, 'X-Medusa': mode.fixed.medusa }
|
||
return signViaServer(mode.server, mode.token, url, headers, body)
|
||
}
|
||
|
||
/** 官方 PC 端 UA(签名与 UA 绑定同一客户端版本) */
|
||
export const PC_UA = 'LunaPC/3.7.0(452316191)'
|
||
|
||
/** 官方 PC 端公共查询参数(与签名设备成套;track_v2 / search 等 luna_pc 接口共用) */
|
||
export const pcCommonParams = (): Record<string, string> => {
|
||
const mode = loadSignMode()
|
||
// 固定值是针对抓包设备签的,必须用配套设备;服务器现签则用本地设备(URL 带什么签什么)
|
||
const fixedDevice = mode?.kind === 'fixed' && mode.fixed.deviceId ? mode.fixed : null
|
||
const { deviceId, installId } = fixedDevice
|
||
? { deviceId: fixedDevice.deviceId, installId: fixedDevice.installId }
|
||
: getDeviceIds()
|
||
return {
|
||
aid: '386088',
|
||
app_name: 'luna_pc',
|
||
region: 'cn',
|
||
geo_region: 'cn',
|
||
os_region: 'cn',
|
||
sim_region: '',
|
||
device_id: deviceId,
|
||
cdid: '',
|
||
iid: installId,
|
||
version_name: '3.7.0',
|
||
version_code: '30070000',
|
||
channel: 'official',
|
||
build_mode: 'master',
|
||
network_carrier: '',
|
||
ac: 'wifi',
|
||
tz_name: 'Asia/Shanghai',
|
||
resolution: '',
|
||
device_platform: 'windows',
|
||
device_type: 'Windows',
|
||
os_version: 'Windows 11',
|
||
fp: deviceId,
|
||
}
|
||
}
|
||
|
||
export default { mssdkAvailable, signQishuiRequest, pcCommonParams, PC_UA }
|