Files
web_huangguo2_h5/app/static/js/common/request.js
T
谢宇宁andClaude Opus 5.5 0fbdba5ccf Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:47:47 +07:00

326 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 前端请求封装。
* - 登录态走 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<string, any>,
* headers?: Record<string, string>,
* timeout?: number,
* trackerEvent?: false|string,
* trackerContext?: Record<string, any>,
* }} [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);