@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 前端请求封装。
|
||||
* - 登录态走 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);
|
||||
Reference in New Issue
Block a user