(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 || ''), }, } ); }, }, }; })();