diff --git a/danmu_api/utils/aiyifan-util.js b/danmu_api/utils/aiyifan-util.js index 68e46a2..2d655b5 100644 --- a/danmu_api/utils/aiyifan-util.js +++ b/danmu_api/utils/aiyifan-util.js @@ -12,8 +12,34 @@ const DEFAULT_USER_AGENT = ( export const AIYIFAN_SIGNING_CONFIG_TTL_MS = 60 * 1000; +// 安全获取对象属性(替代可选链操作符) +function safeGet(obj, path, defaultValue) { + if (obj == null) return defaultValue; + const keys = path.split('.'); + let result = obj; + for (let i = 0; i < keys.length; i++) { + if (result == null) return defaultValue; + // 处理数组索引,如 config[0] + const key = keys[i]; + const arrayMatch = key.match(/^(.+)\[(\d+)\]$/); + if (arrayMatch) { + const arrKey = arrayMatch[1]; + const index = parseInt(arrayMatch[2], 10); + result = result[arrKey]; + if (Array.isArray(result) && index < result.length) { + result = result[index]; + } else { + return defaultValue; + } + } else { + result = result[key]; + } + } + return result !== undefined ? result : defaultValue; +} + function extractAssignedObjectLiteral(html, variableName) { - const assignmentPattern = new RegExp(`\\b(?:var|let|const)\\s+${variableName}\\s*=\\s*`); + const assignmentPattern = new RegExp('\\b(?:var|let|const)\\s+' + variableName + '\\s*=\\s*'); const match = assignmentPattern.exec(html); if (!match) { return null; @@ -74,8 +100,8 @@ function parseFallbackPConfig(html) { let privateKeys = []; try { - privateKeys = JSON.parse(`[${match[2]}]`); - } catch { + privateKeys = JSON.parse('[' + match[2] + ']'); + } catch (e) { return null; } @@ -90,9 +116,14 @@ function parseFallbackPConfig(html) { } export function extractPConfigFromInjectJson(injectJson) { - const config = injectJson?.config?.[0]?.pConfig; - const publicKey = config?.publicKey; - const privateKey = Array.isArray(config?.privateKey) ? config.privateKey[0] : config?.privateKey; + // 使用安全获取替代可选链 + const config = safeGet(injectJson, 'config[0]', null); + const pConfig = config ? config.pConfig : null; + + if (!pConfig) return null; + + const publicKey = pConfig.publicKey; + const privateKey = Array.isArray(pConfig.privateKey) ? pConfig.privateKey[0] : pConfig.privateKey; if (!publicKey || !privateKey) { return null; @@ -111,7 +142,7 @@ export function extractPConfigFromHtml(html) { return signingConfig; } } catch (error) { - log("warn", `[Aiyifan] 解析 injectJson 失败,回退到 pConfig 提取: ${error.message}`); + log("warn", '[Aiyifan] 解析 injectJson 失败,回退到 pConfig 提取: ' + (error.message || '未知错误')); } } @@ -137,12 +168,22 @@ function splitQueryString(queryString) { return queryString .split("&") .filter(Boolean) - .map(pair => { + .map(function(pair) { const equalsIndex = pair.indexOf("="); const rawKey = equalsIndex === -1 ? pair : pair.slice(0, equalsIndex); const rawValue = equalsIndex === -1 ? "" : pair.slice(equalsIndex + 1); - const key = decodeURIComponent(rawKey.replace(/\+/g, "%20")); - const value = decodeURIComponent(rawValue.replace(/\+/g, "%20")); + + // 手动解码,替代 decodeURIComponent 的异常处理 + function safeDecode(str) { + try { + return decodeURIComponent(str.replace(/\+/g, "%20")); + } catch (e) { + return str; + } + } + + const key = safeDecode(rawKey); + const value = safeDecode(rawValue); return [key, value]; }); } @@ -163,32 +204,63 @@ function getQueryEntries(input) { if (queryIndex !== -1) { const hashIndex = trimmed.indexOf("#", queryIndex); queryString = trimmed.slice(queryIndex + 1, hashIndex === -1 ? undefined : hashIndex); - } else if (trimmed.startsWith("?")) { + } else if (trimmed.charAt(0) === "?") { queryString = trimmed.slice(1); } return splitQueryString(queryString); } - if (input instanceof URLSearchParams) { - return Array.from(input.entries()); + // 移除 URLSearchParams 检查,统一按普通对象处理 + if (typeof input === "object" && input !== null) { + // 如果 input 有 entries 方法且返回数组,使用它 + if (typeof input.entries === "function") { + try { + var entries = input.entries(); + if (Array.isArray(entries)) { + return entries; + } + // 处理迭代器情况 + var result = []; + // 尝试作为 Map 或类似对象处理 + if (typeof input.forEach === "function") { + input.forEach(function(value, key) { + result.push([key, normalizeQueryValue(value)]); + }); + return result.filter(function(item) { + return item[1] !== null; + }); + } + } catch (e) { + // 失败则回退到 Object.entries + } + } + + // 标准对象处理 + return Object.keys(input).map(function(key) { + return [key, normalizeQueryValue(input[key])]; + }).filter(function(item) { + return item[1] !== null; + }); } - return Object.entries(input) - .map(([key, value]) => [key, normalizeQueryValue(value)]) - .filter(([, value]) => value !== null); + return []; } export function buildCanonicalQuery(input) { return getQueryEntries(input) - .filter(([key]) => !isSigningParam(key)) - .map(([key, value]) => `${key}=${value}`) + .filter(function(item) { + return !isSigningParam(item[0]); + }) + .map(function(item) { + return item[0] + "=" + item[1]; + }) .join("&"); } export function computeAiyifanVv(input, signingConfig) { const query = buildCanonicalQuery(input); - const raw = `${signingConfig.publicKey}&${query.toLowerCase()}&${signingConfig.privateKey}`; + const raw = signingConfig.publicKey + "&" + query.toLowerCase() + "&" + signingConfig.privateKey; return md5(raw); } @@ -200,89 +272,108 @@ function normalizeJsonPayload(data) { } function isSignedRequestSuccessful(payload) { - return payload?.ret === 200 && payload?.data?.code === 0; + // 使用安全获取替代可选链 + var ret = safeGet(payload, 'ret', null); + var code = safeGet(payload, 'data.code', null); + return ret === 200 && code === 0; } function getFailureMessage(payload, status) { - return payload?.data?.msg || payload?.msg || `HTTP ${status}`; + // 使用安全获取替代可选链 + var msg = safeGet(payload, 'data.msg', null) || safeGet(payload, 'msg', null); + return msg || ('HTTP ' + status); } export class AiyifanSigningProvider { - constructor(options = {}) { - this.request = options.request || httpGet; - this.proxyUrlBuilder = options.proxyUrlBuilder || (url => globals.makeProxyUrl(url)); + constructor(options) { + options = options || {}; + this.proxyUrlBuilder = options.proxyUrlBuilder || function(url) { + return globals.makeProxyUrl(url); + }; this.userAgent = options.userAgent || DEFAULT_USER_AGENT; this.configPageUrl = options.configPageUrl || DEFAULT_CONFIG_PAGE_URL; this.ttlMs = options.ttlMs || AIYIFAN_SIGNING_CONFIG_TTL_MS; - this.now = options.now || (() => Date.now()); + this.now = options.now || function() { return Date.now(); }; this.signingConfig = null; this.signingConfigFetchedAt = 0; } - async getSigningConfig(forceRefresh = false) { - const now = this.now(); - const cacheValid = this.signingConfig && (now - this.signingConfigFetchedAt) < this.ttlMs; + async getSigningConfig(forceRefresh) { + forceRefresh = forceRefresh || false; + var now = this.now(); + var cacheValid = this.signingConfig && (now - this.signingConfigFetchedAt) < this.ttlMs; if (!forceRefresh && cacheValid) { return this.signingConfig; } - const response = await this.request(this.proxyUrlBuilder(this.configPageUrl), { + var response = await httpGet(this.proxyUrlBuilder(this.configPageUrl), { headers: { "User-Agent": this.userAgent, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } }); - const html = typeof response.data === "string" ? response.data : String(response.data ?? ""); - const signingConfig = extractPConfigFromHtml(html); + var html = typeof response.data === "string" ? response.data : String(response.data || ""); + var signingConfig = extractPConfigFromHtml(html); if (!signingConfig) { throw new Error("未能从桌面站页面解析到 pConfig"); } this.signingConfig = signingConfig; this.signingConfigFetchedAt = now; - log("info", `[Aiyifan] 已更新桌面站签名配置: ${signingConfig.publicKey.slice(0, 12)}...`); + log("info", '[Aiyifan] 已更新桌面站签名配置: ' + signingConfig.publicKey.slice(0, 12) + '...'); return signingConfig; } buildSignedParams(baseParams, signingConfig) { - return { - ...baseParams, - vv: computeAiyifanVv(baseParams, signingConfig), - pub: signingConfig.publicKey - }; + var result = {}; + // 手动复制对象,替代展开运算符 + for (var key in baseParams) { + if (baseParams.hasOwnProperty(key)) { + result[key] = baseParams[key]; + } + } + result.vv = computeAiyifanVv(baseParams, signingConfig); + result.pub = signingConfig.publicKey; + return result; } - async signedGetJson(api, baseParams, headers = {}, logPrefix = "Aiyifan", forceRefresh = false) { - const signingConfig = await this.getSigningConfig(forceRefresh); - const signedParams = this.buildSignedParams(baseParams, signingConfig); - const requestUrl = updateQueryString(api, signedParams); - const response = await this.request(this.proxyUrlBuilder(requestUrl), { headers }); + async signedGetJson(api, baseParams, headers, logPrefix, forceRefresh) { + headers = headers || {}; + logPrefix = logPrefix || "Aiyifan"; + forceRefresh = forceRefresh || false; + + var signingConfig = await this.getSigningConfig(forceRefresh); + var signedParams = this.buildSignedParams(baseParams, signingConfig); + var requestUrl = updateQueryString(api, signedParams); + var response = await httpGet(this.proxyUrlBuilder(requestUrl), { headers: headers }); - let payload; + var payload; try { payload = normalizeJsonPayload(response.data); } catch (error) { if (!forceRefresh) { - log("warn", `[${logPrefix}] 响应无法解析为 JSON,刷新签名配置后重试: ${error.message}`); + log("warn", '[' + logPrefix + '] 响应无法解析为 JSON,刷新签名配置后重试: ' + (error.message || '未知错误')); return this.signedGetJson(api, baseParams, headers, logPrefix, true); } throw error; } - if (response.status !== 200 || !isSignedRequestSuccessful(payload)) { + // 安全获取状态码,如果不存在则默认为200 + var statusCode = response.status != null ? response.status : 200; + if (statusCode !== 200 || !isSignedRequestSuccessful(payload)) { if (!forceRefresh) { - log("warn", `[${logPrefix}] 当前签名请求失败,刷新 pConfig 后重试: ${getFailureMessage(payload, response.status)}`); + log("warn", '[' + logPrefix + '] 当前签名请求失败,刷新 pConfig 后重试: ' + getFailureMessage(payload, statusCode)); return this.signedGetJson(api, baseParams, headers, logPrefix, true); } - throw new Error(getFailureMessage(payload, response.status)); + throw new Error(getFailureMessage(payload, statusCode)); } return { data: payload, vv: signedParams.vv, - signingConfig + signingConfig: signingConfig }; } -} +} \ No newline at end of file