hyapp-h5/common_aslan/aslan-common.js
2026-07-18 11:13:12 +08:00

670 lines
23 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.

(function () {
'use strict';
var PROD_API_BASE = 'https://api.atuchat.com/';
var LOCAL_API_BASE = 'http://127.0.0.1:1100/';
var APP_CODE = 'aslan';
var DEFAULT_ORIGIN = 'ATYOU';
var LOOPBACK_API_HOSTS = ['127.0.0.1', 'localhost', '[::1]'];
var readyPromise = null;
var headers = {
authorization: '',
'req-app-intel':
'version=1.0.0;build=1;model=H5;sysVersion=web;channel=Web',
'req-client': 'H5',
'req-imei': 'H5-STATIC',
'req-lang': 'en',
'req-sys-origin': 'origin=ATYOU;originChild=ATYOU',
'req-version': 'V2',
'req-zone': 'Asia/Shanghai',
};
function currentParams() {
var params = new URLSearchParams(window.location.search || '');
var hash = window.location.hash || '';
if (hash.indexOf('?') >= 0) {
new URLSearchParams(hash.split('?').slice(1).join('?')).forEach(
function (value, key) {
if (!params.has(key)) params.set(key, value);
}
);
}
return params;
}
function query(name) {
return currentParams().get(name);
}
function normalizeBaseURL(value) {
return String(value || '').replace(/\/+$/, '');
}
function resolveEnv() {
var env = String(query('env') || '')
.trim()
.toLowerCase();
return env === 'local' || env === 'test' || env === 'dev' ? env : '';
}
function nonProductionBaseURL(value) {
var config = window.AslanCommonConfig || {};
var trustedHosts = Array.isArray(config.trustedApiHosts)
? config.trustedApiHosts.map(function (host) {
return String(host || '')
.trim()
.toLowerCase();
})
: [];
var parsed;
try {
parsed = new URL(String(value || ''));
} catch (_) {
throw new Error('aslan_test_api_invalid');
}
var host = String(parsed.hostname || '').toLowerCase();
var isLoopback = LOOPBACK_API_HOSTS.indexOf(host) >= 0;
var isTrusted = trustedHosts.indexOf(host) >= 0;
// App authorization is attached to every request. Test/dev may use only a
// build-time configured HTTP(S) origin whose hostname is explicitly
// allowlisted; URL parameters are intentionally never an API override.
if (
!/^https?:$/.test(parsed.protocol) ||
!(isLoopback || (parsed.protocol === 'https:' && isTrusted))
) {
throw new Error('aslan_test_api_untrusted');
}
return normalizeBaseURL(parsed.toString());
}
function baseURL() {
var env = resolveEnv();
// Production must always use the compiled service origin: accepting an
// apiBase query here would forward the App bearer token to an attacker URL.
if (!env) return normalizeBaseURL(PROD_API_BASE);
if (env === 'local') return normalizeBaseURL(LOCAL_API_BASE);
if (env === 'test' || env === 'dev') {
var testBase =
window.ASLAN_TEST_API_BASE_URL ||
(window.AslanCommonConfig && window.AslanCommonConfig.testApi);
// Test/dev 请求携带真实 App 登录态,未注入独立测试地址时必须失败,
// 不能把 `?env=test` 静默回落到生产域名并产生抽奖或领奖写入。
if (!testBase) throw new Error('aslan_test_api_not_configured');
return nonProductionBaseURL(testBase);
}
}
function buildURL(path, queryData) {
var url = new URL(
String(path || '').replace(/^\/+/, ''),
baseURL() + '/'
);
Object.keys(queryData || {}).forEach(function (key) {
var value = queryData[key];
if (value === undefined || value === null || value === '') return;
url.searchParams.set(key, value);
});
return url.toString();
}
function normalizeAuthorization(value) {
var text = String(value || '').trim();
if (!text) return '';
return /^Bearer\s+/i.test(text) ? text : 'Bearer ' + text;
}
function splitHeaderPairs(value) {
var result = {};
String(value || '')
.split(';')
.forEach(function (item) {
var index = item.indexOf('=');
if (index > -1) {
result[item.slice(0, index).trim()] = item
.slice(index + 1)
.trim();
}
});
return result;
}
function putHeader(target, key, value) {
if (
value === undefined ||
value === null ||
value === '' ||
target[key]
) {
return;
}
target[key] = String(value);
}
function normalizeHeaderCandidate(value, depth) {
depth = depth || 0;
if (depth > 4 || value === null || value === undefined) return {};
if (typeof value === 'string') {
var trimmed = value.trim();
if (!trimmed) return {};
if (trimmed.charAt(0) === '{' || trimmed.charAt(0) === '[') {
try {
return normalizeHeaderCandidate(
JSON.parse(trimmed),
depth + 1
);
} catch (_) {
return {};
}
}
return { authorization: trimmed };
}
if (Array.isArray(value)) {
return value.reduce(function (result, item) {
return Object.assign(
result,
normalizeHeaderCandidate(item, depth + 1)
);
}, {});
}
if (typeof value !== 'object') return {};
var appIntel = splitHeaderPairs(
value['Req-App-Intel'] || value['req-app-intel']
);
var sysOrigin = splitHeaderPairs(
value['Req-Sys-Origin'] || value['req-sys-origin']
);
var normalized = {};
putHeader(
normalized,
'authorization',
value.Authorization ||
value.authorization ||
value.token ||
value.accessToken
);
putHeader(
normalized,
'reqLang',
value['Req-Lang'] ||
value['req-lang'] ||
value.reqLang ||
value.lang
);
putHeader(
normalized,
'appVersion',
appIntel.version || value.appVersion || value['App-Version']
);
putHeader(
normalized,
'buildVersion',
appIntel.build || value.buildVersion
);
putHeader(normalized, 'model', appIntel.model || value.model);
putHeader(normalized, 'channel', appIntel.channel || value.channel);
putHeader(
normalized,
'reqImei',
appIntel['Req-Imei'] ||
appIntel.imei ||
value['Req-Imei'] ||
value['req-imei'] ||
value.reqImei
);
putHeader(
normalized,
'origin',
sysOrigin.origin || value.sysOrigin || value.origin
);
putHeader(
normalized,
'child',
sysOrigin.originChild ||
sysOrigin.child ||
value.sysOriginChild ||
value.child
);
['headerInfo', 'headers', 'auth', 'data', 'state'].forEach(
function (key) {
Object.assign(
normalized,
normalizeHeaderCandidate(value[key], depth + 1)
);
}
);
return normalized;
}
function setHeadersFromApp(rawOrObject) {
var data = normalizeHeaderCandidate(rawOrObject || {});
var next = {};
if (data.authorization) {
next.authorization = normalizeAuthorization(data.authorization);
}
if (data.reqLang) next['req-lang'] = data.reqLang;
if (
data.appVersion ||
data.buildVersion ||
data.model ||
data.channel
) {
next['req-app-intel'] = [
data.appVersion ? 'version=' + data.appVersion : '',
data.buildVersion ? 'build=' + data.buildVersion : '',
data.model ? 'model=' + data.model : '',
data.channel ? 'channel=' + data.channel : '',
]
.filter(Boolean)
.join(';');
}
if (data.reqImei) next['req-imei'] = data.reqImei;
if (data.origin) {
next['req-sys-origin'] = [
'origin=' + data.origin,
'originChild=' + (data.child || data.origin),
].join(';');
}
headers = Object.assign(headers, next);
return headers;
}
function discoverQueryHeaders() {
return {
authorization:
query('authorization') ||
query('Authorization') ||
query('token') ||
query('accessToken') ||
query('access_token'),
reqLang: query('lang') || query('language') || query('locale'),
origin: query('sysOrigin') || query('origin') || DEFAULT_ORIGIN,
child:
query('sysOriginChild') ||
query('originChild') ||
DEFAULT_ORIGIN,
channel: query('channel') || query('appChannel'),
};
}
function readStorageHeaders(storage) {
var collected = {};
if (!storage) return collected;
[
'authorization',
'Authorization',
'token',
'accessToken',
'headerInfo',
'headersFromApp',
'appHeaders',
'user',
'userInfo',
].forEach(function (key) {
try {
Object.assign(
collected,
normalizeHeaderCandidate(storage.getItem(key))
);
} catch (_) {}
});
return collected;
}
function applyDiscoveredHeaders() {
setHeadersFromApp(discoverQueryHeaders());
setHeadersFromApp(window.__APP_HEADERS__);
setHeadersFromApp(window.__HEADER_INFO__);
setHeadersFromApp(window.headerInfo);
try {
setHeadersFromApp(readStorageHeaders(window.localStorage));
setHeadersFromApp(readStorageHeaders(window.sessionStorage));
} catch (_) {}
}
function ensureAslanAppCode() {
document.documentElement.dataset.hyAppCode = APP_CODE;
var params = currentParams();
if (
params.get('app_code') ||
params.get('app') ||
params.get('appCode') ||
!window.history ||
!window.history.replaceState
) {
return;
}
var url = new URL(window.location.href);
url.searchParams.set('app_code', APP_CODE);
window.history.replaceState(null, '', url.toString());
}
function isAppEnvironment() {
return Boolean(
window.app ||
window.FlutterApp ||
window.FlutterPageControl ||
window.webkit
);
}
function requestAccessOrigin() {
return new Promise(function (resolve) {
var settled = false;
var timeoutId = null;
function finish(raw) {
if (raw) setHeadersFromApp(raw);
if (settled) return;
settled = true;
if (timeoutId) window.clearTimeout(timeoutId);
resolve(raw || null);
}
window.renderData = finish;
window.getIosAccessOriginParam = finish;
if (!isAppEnvironment()) {
finish(null);
return;
}
// Aslan Flutter 在页面完成后注入 window.app.getAccessOrigin这里轮询短时间等待避免首屏请求早于注入而丢登录态。
if (
window.app &&
typeof window.app.getAccessOrigin === 'function'
) {
try {
finish(window.app.getAccessOrigin());
return;
} catch (_) {}
}
if (
window.FlutterApp &&
typeof window.FlutterApp.postMessage === 'function'
) {
try {
window.FlutterApp.postMessage('requestAccessOrigin');
} catch (_) {}
}
var attempt = 0;
var poll = function () {
attempt += 1;
if (
window.app &&
typeof window.app.getAccessOrigin === 'function'
) {
try {
finish(window.app.getAccessOrigin());
return;
} catch (_) {}
}
if (!settled && attempt < 30) window.setTimeout(poll, 80);
};
poll();
timeoutId = window.setTimeout(function () {
finish(null);
}, 2400);
});
}
function connectToApp() {
if (readyPromise) return readyPromise;
ensureAslanAppCode();
applyDiscoveredHeaders();
readyPromise = requestAccessOrigin().then(function () {
return {
baseURL: baseURL(),
headers: Object.assign({}, headers),
authenticated: Boolean(headers.authorization),
};
});
return readyPromise;
}
function parseResponse(response) {
return response.text().then(function (text) {
var payload = text ? JSON.parse(text) : {};
if (!response.ok) {
var httpError = new Error(
payload.message ||
payload.errorMsg ||
response.statusText ||
'request_failed'
);
httpError.status = response.status;
httpError.payload = payload;
throw httpError;
}
if (payload && payload.status === false) {
var apiError = new Error(
payload.message ||
payload.errorMsg ||
payload.error ||
'api_request_failed'
);
apiError.payload = payload;
apiError.code = payload.errorCode || payload.code;
throw apiError;
}
if (
payload &&
Object.prototype.hasOwnProperty.call(payload, 'body')
) {
return payload.body;
}
if (
payload &&
Object.prototype.hasOwnProperty.call(payload, 'data')
) {
return payload.data;
}
return payload;
});
}
function stripAppEncryptedHeaders(target) {
// H5 WebView 只复用 App 登录态,不参与 App Dio 加密;带 Req-Atyou 会让网关按密文解码普通 H5 请求。
Object.keys(target || {}).forEach(function (key) {
if (String(key).toLowerCase() === 'req-atyou') delete target[key];
});
}
function request(path, options) {
var opts = options || {};
var body = opts.body;
if (body && typeof body === 'object' && !(body instanceof FormData)) {
body = JSON.stringify(body);
}
return connectToApp().then(function () {
var requestHeaders = Object.assign({}, headers, opts.headers || {});
stripAppEncryptedHeaders(requestHeaders);
if (!requestHeaders.authorization)
delete requestHeaders.authorization;
if (body) {
requestHeaders['content-type'] =
requestHeaders['content-type'] || 'application/json';
}
return window
.fetch(buildURL(path, opts.query), {
method: opts.method || 'GET',
headers: requestHeaders,
body: body,
credentials: 'omit',
cache: 'no-store',
})
.then(parseResponse);
});
}
function back() {
if (
window.FlutterPageControl &&
window.FlutterPageControl.postMessage
) {
window.FlutterPageControl.postMessage('close_page');
return;
}
if (window.HyAppBridge && window.HyAppBridge.back) {
window.HyAppBridge.back();
return;
}
window.history.back();
}
window.AslanCommon = {
baseURL: baseURL,
buildURL: buildURL,
headers: function () {
return Object.assign({}, headers);
},
connectToApp: connectToApp,
setHeadersFromApp: setHeadersFromApp,
request: request,
get: function (path, queryData) {
return request(path, { method: 'GET', query: queryData });
},
post: function (path, body, queryData) {
return request(path, {
method: 'POST',
body: body,
query: queryData,
});
},
back: back,
user: {
profile: function () {
return request('/user/user-profile/user-id', { method: 'GET' });
},
},
rechargeReward: {
status: function () {
return request(
'/activity/cumulative-recharge/recharge-reward',
{
method: 'GET',
}
);
},
claim: function (ruleId) {
return request('/activity/cumulative-recharge', {
method: 'POST',
body: { id: String(ruleId || '') },
});
},
},
gameKing: {
current: function () {
return request('/activity/game/king/aslan/detail', {
method: 'GET',
}).then(function (detail) {
var activity =
detail && detail.activity
? detail.activity
: detail || {};
var activityId = String(
activity.activityId ||
activity.id ||
detail.activityId ||
detail.id ||
''
);
if (!activityId) return activity;
return request('/activity/game/king/aslan/me', {
method: 'GET',
query: { activityId: activityId },
}).then(function (me) {
// detail 的 prizes/rankRewards/activityStatus 位于外层,而
// activity 只携带基础配置;保留两个层级再叠加 me避免页面
// 因合并顺序丢掉七个奖项或榜单奖励配置。
return Object.assign(
{},
detail || {},
activity,
me || {},
{
activityId: activityId,
}
);
});
});
},
draw: function (activityId, requestId) {
// requestId 由页面为“一次用户意图”稳定生成;网络超时后重试仍携带同一值,避免重复扣减抽奖次数。
return request('/activity/game/king/aslan/draw', {
method: 'POST',
body: {
activityId: String(activityId || ''),
requestId: String(requestId || ''),
},
});
},
ranking: function (activityId, rankingType, period, limit) {
return request('/activity/game/king/aslan/ranking', {
method: 'GET',
query: {
activityId: String(activityId || ''),
rankingType: String(rankingType || 'TYCOON'),
period: String(period || 'DAILY'),
limit: Number(limit) || 30,
},
});
},
records: function (activityId, limit) {
return request('/activity/game/king/aslan/draw-records', {
method: 'GET',
query: {
activityId: String(activityId || ''),
limit: Number(limit) || 50,
},
});
},
},
giftChallenge: {
current: function () {
return request('/activity/gift-challenge/aslan/detail', {
method: 'GET',
});
},
enter: function (activityId) {
// 进入任务按“活动 + 活动时区自然日 + 用户”由服务端幂等;请求体只传
// 后端契约声明的 activityId用户和统计日始终从网关上下文推导。
return request('/activity/gift-challenge/aslan/enter', {
method: 'POST',
body: {
activityId: String(activityId || ''),
},
});
},
ranking: function (activityId, period, date, limit) {
return request('/activity/gift-challenge/aslan/ranking', {
method: 'GET',
query: {
activityId: String(activityId || ''),
period: String(period || 'DAILY'),
statDate: date ? String(date) : '',
limit: Number(limit) || 30,
},
});
},
claimTask: function (activityId, taskCode, requestId) {
// taskCode 进入 URL 前编码,避免后台配置值意外改变请求路径;用户 ID
// 从 App Bearer 登录态读取,页面从不接受或上送可伪造的 userId。
return request(
'/activity/gift-challenge/aslan/tasks/' +
encodeURIComponent(String(taskCode || '')) +
'/claim',
{
method: 'POST',
body: {
activityId: String(activityId || ''),
requestId: String(requestId || ''),
},
}
);
},
},
};
})();