/** * 前端请求封装。 * - 登录态走 cookie(credentials: include) * - 浏览器无法改写 User-Agent;X-Real-IP 由服务端反代上游时注入 * - 同源业务请求携带 SDK 设备字段;短剧服务端事件额外携带完整公共字段快照 * - token 重试复用同一份 SDK Header、埋点上下文和 request_id * - request_id 只用于请求关联;最终 event_id 由服务端按每条事件的最终字段生成 * - 直连上游时若需自定义头,请走本站 Go 代理接口 */ (function (global) { var DEFAULT_TIMEOUT_MS = 30000; var TRACKER_CONTEXT_TIMEOUT_MS = 1500; var DRAMA_SERVER_EVENT_ROUTES = { "/api/playlet/chapter/like/set": "drama_like", "/api/playlet/like/set": "drama_like", "/api/playlet/chapter/collect/set": "drama_collect", "/api/playlet/collect/set": "drama_collect", "/api/comment/send": "drama_comment", "/api/playlet/chapter/unlock": "drama_purchase", }; function randomRequestId() { var bytes = new Uint8Array(16); if (global.crypto && typeof global.crypto.getRandomValues === "function") { global.crypto.getRandomValues(bytes); return Array.prototype.map.call(bytes, function (value) { return (value + 256).toString(16).slice(-2); }).join(""); } var fallback = (Date.now().toString(16) + Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2)).slice(0, 32); while (fallback.length < 32) fallback += "0"; return fallback; } function cleanHeaderPart(value) { return String(value == null ? "" : value).replace(/[^\x20-\x7E]|[;=]/g, " ").trim(); } function sameOrigin(url) { try { return new URL(url, global.location.href).origin === global.location.origin; } catch (error) { return true; } } function dramaServerEvent(url, method, options) { if (method === "GET" || method === "HEAD" || !sameOrigin(url)) return ""; if (options.trackerEvent === false) return ""; if (typeof options.trackerEvent === "string") { var explicit = options.trackerEvent.trim(); return /^(drama_like|drama_comment|drama_collect|drama_purchase)$/.test(explicit) ? explicit : ""; } try { return DRAMA_SERVER_EVENT_ROUTES[new URL(url, global.location.href).pathname] || ""; } catch (error) { return ""; } } function commonFields(options) { if (options._trackerCommonFieldsPromise) return options._trackerCommonFieldsPromise; var getter = global.tracker && typeof global.tracker.getCommonFields === "function" ? global.tracker.getCommonFields : global.WebSDK && typeof global.WebSDK.getCommonFields === "function" ? global.WebSDK.getCommonFields.bind(global.WebSDK) : null; options._trackerCommonFieldsPromise = getter ? Promise.resolve().then(function () { return getter(); }).catch(function () { return {}; }) : Promise.resolve({}); return options._trackerCommonFieldsPromise; } function currentDramaContext(options) { var context; if (options && options.trackerContext && typeof options.trackerContext === "object") { context = options.trackerContext; } else if (global.tracker && typeof global.tracker.getDramaContext === "function") { context = global.tracker.getDramaContext() || {}; } else { context = {}; } return { media_id: String(context.media_id == null ? "" : context.media_id).trim(), drama_id: String(context.drama_id == null ? "" : context.drama_id).trim(), episode: Math.max(1, parseInt(context.episode, 10) || 1), video_id: String(context.video_id == null ? "" : context.video_id).trim(), recommend_trace_id: String(context.recommend_trace_id == null ? "" : context.recommend_trace_id).trim(), }; } function base64URLJSON(value) { try { var json = JSON.stringify(value); var bytes = encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, function (_, hex) { return String.fromCharCode(parseInt(hex, 16)); }); return global.btoa(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } catch (error) { return ""; } } function trackerHeaders(url, method, options) { var eventName = dramaServerEvent(url, method, options); if (!eventName) return Promise.resolve({}); if (!options._trackerContextPromise) { if (!options._trackerRequestId) options._trackerRequestId = randomRequestId(); var drama = currentDramaContext(options); if (!drama || !String(drama.drama_id || "").trim()) { options._trackerContextPromise = Promise.resolve({}); } else { options._trackerContextPromise = commonFields(options).then(function (fields) { var packedContext = base64URLJSON({ v: 1, request_id: options._trackerRequestId, event_name: eventName, common: fields, drama: drama, }); if (!packedContext) return {}; var result = { "X-Tracker-Request-Id": options._trackerRequestId, "X-Tracker-Event-Name": eventName, "X-DC-Request-Context": packedContext, }; result["X-User-Agent"] = [ "DevID=" + cleanHeaderPart(fields.device_id), "DevType=" + cleanHeaderPart(fields.device || "OTHER"), "SysType=" + (fields.device === "Android" ? "h5_android" : fields.device === "iOS" ? "h5_ios" : "h5_pc"), "Ver=" + cleanHeaderPart(fields.app_version || "1.0.0"), "BuildID=web", "device_brand=" + cleanHeaderPart(fields.device_brand), "device_model=" + cleanHeaderPart(fields.device_model), "sid=" + cleanHeaderPart(fields.sid), ].join(";"); return result; }); } } return new Promise(function (resolve) { var settled = false; var timer = global.setTimeout(function () { if (settled) return; settled = true; resolve({}); }, TRACKER_CONTEXT_TIMEOUT_MS); options._trackerContextPromise.then(function (headers) { if (settled) return; settled = true; global.clearTimeout(timer); resolve(headers); }); }); } function buildQuery(params) { if (!params) return ""; var pairs = []; Object.keys(params).forEach(function (key) { var value = params[key]; if (value == null) return; pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(value))); }); return pairs.length ? "?" + pairs.join("&") : ""; } /** * @param {string} url * @param {{ * method?: string, * data?: any, * params?: Record, * headers?: Record, * timeout?: number, * trackerEvent?: false|string, * trackerContext?: Record, * }} [options] */ function request(url, options) { options = options || {}; var method = String(options.method || "GET").toUpperCase(); var headers = Object.assign( { Accept: "application/json" }, options.headers || {} ); var finalUrl = url + buildQuery(options.params); var body; var controller = typeof AbortController !== "undefined" ? new AbortController() : null; var timer; if (options.data != null && method !== "GET" && method !== "HEAD") { if (typeof FormData !== "undefined" && options.data instanceof FormData) { body = options.data; } else { headers["Content-Type"] = headers["Content-Type"] || "application/json"; body = headers["Content-Type"].indexOf("application/json") >= 0 ? JSON.stringify(options.data) : options.data; } } var timeoutMs = typeof options.timeout === "number" ? options.timeout : DEFAULT_TIMEOUT_MS; function sdkCommonHeader() { if (headers["X-SDK-Common"] || options._skipSDKCommon || !sameOrigin(finalUrl)) { return Promise.resolve(""); } if (!options._sdkCommonHeaderPromise) { options._sdkCommonHeaderPromise = Promise.resolve() .then(function () { if ( global.tracker && typeof global.tracker.getSDKCommonHeader === "function" ) { return global.tracker.getSDKCommonHeader(); } return ""; }) .catch(function () { return ""; }); } return options._sdkCommonHeaderPromise; } return Promise.all([ trackerHeaders(finalUrl, method, options).catch(function () { return {}; }), sdkCommonHeader(), ]) .then(function (prepared) { var tracking = prepared[0] || {}; Object.keys(tracking).forEach(function (key) { if (headers[key] == null) headers[key] = tracking[key]; }); if (!headers["X-SDK-Common"] && prepared[1]) { headers["X-SDK-Common"] = prepared[1]; } var fetchOpts = { method: method, headers: headers, credentials: "include", signal: controller ? controller.signal : undefined, }; if (body != null) fetchOpts.body = body; if (controller && timeoutMs > 0) { timer = setTimeout(function () { controller.abort(); }, timeoutMs); } return fetch(finalUrl, fetchOpts); }) .then(function (res) { var contentType = res.headers.get("content-type") || ""; var parse = contentType.indexOf("application/json") >= 0 ? res.json() : res.text(); return parse.then(function (data) { if (!res.ok) { var err = new Error( (data && data.msg) || (data && data.message) || res.statusText || "request failed" ); err.status = res.status; err.code = data && data.code; err.data = data; throw err; } return data; }); }) .then(function (data) { return handleTokenExpiredResponse(url, options, data); }) .finally(function () { if (timer) clearTimeout(timer); }); } /** * 业务码 1000:token 失效。 * 游客 → 重新游客登录并重试一次;注册账号 → 打开登录弹窗/跳转登录页。 */ function handleTokenExpiredResponse(url, options, data) { options = options || {}; var expiredCode = (global.AuthAPI && global.AuthAPI.TOKEN_EXPIRED_CODE) || 1000; if (!data || Number(data.code) !== Number(expiredCode)) { return data; } if (options._skipTokenRecovery || options._tokenRetried) { return data; } if ( global.AuthAPI && typeof global.AuthAPI.isAuthEndpoint === "function" && global.AuthAPI.isAuthEndpoint(url) ) { return data; } if (!global.AuthAPI || typeof global.AuthAPI.handleTokenExpired !== "function") { return data; } return global.AuthAPI.handleTokenExpired().then(function (result) { if (result && result.ok && result.guest) { return request( url, Object.assign({}, options, { _tokenRetried: true }) ); } return data; }); } global.HttpRequest = { request: request, get: function (url, options) { return request(url, Object.assign({}, options, { method: "GET" })); }, post: function (url, data, options) { return request(url, Object.assign({}, options, { method: "POST", data: data })); }, }; })(typeof window !== "undefined" ? window : this);