充值h5
This commit is contained in:
parent
f776b87a01
commit
0a1221933e
@ -14,6 +14,7 @@
|
||||
|
||||
var DESIGN_WIDTH = 1080;
|
||||
var VIEWPORT_WIDTH = 375;
|
||||
var MAX_PAGE_WIDTH = 430;
|
||||
var DEFAULT_DESIGN_HEIGHT = 3273;
|
||||
var designHeight = DEFAULT_DESIGN_HEIGHT;
|
||||
var countdownEndAt = resolveCountdownEndAt();
|
||||
@ -112,7 +113,44 @@
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCountdownEndAt() {
|
||||
function monthlyResetEndAt() {
|
||||
var now = new Date();
|
||||
return new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth() + 1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
}
|
||||
|
||||
function activityTimeEndAt(activityTime) {
|
||||
var matches = String(activityTime || '').match(
|
||||
/(\d{1,2})\s*\/\s*(\d{1,2})\s*$/
|
||||
);
|
||||
if (!matches) return 0;
|
||||
|
||||
var now = new Date();
|
||||
var day = Number(matches[1]);
|
||||
var month = Number(matches[2]);
|
||||
if (!Number.isFinite(day) || !Number.isFinite(month)) return 0;
|
||||
|
||||
// 后端月累充返回的 activityTime 是 DD/MM - DD/MM,不带年份;结束日次日 00:00 就是月度清空点。
|
||||
var endAt = new Date(
|
||||
now.getFullYear(),
|
||||
month - 1,
|
||||
day + 1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
).getTime();
|
||||
return Number.isFinite(endAt) && endAt > Date.now() ? endAt : 0;
|
||||
}
|
||||
|
||||
function resolveCountdownEndAt(statusData) {
|
||||
var params = getParams();
|
||||
var explicitEnd = Date.parse(
|
||||
params.get('end_at') || params.get('endAt') || ''
|
||||
@ -124,23 +162,40 @@
|
||||
if (Number.isFinite(hours) && hours > 0)
|
||||
return Date.now() + hours * 60 * 60 * 1000;
|
||||
|
||||
// 后端月累充接口当前只返回活动文案周期,不返回毫秒级结束时间;倒计时仍支持 URL 显式覆盖,默认只作为页面视觉计时。
|
||||
return Date.now() + 25 * 60 * 60 * 1000 + 61 * 1000;
|
||||
return (
|
||||
activityTimeEndAt(statusData && statusData.activityTime) ||
|
||||
monthlyResetEndAt()
|
||||
);
|
||||
}
|
||||
|
||||
function getPageWidth() {
|
||||
var viewportWidth =
|
||||
window.innerWidth ||
|
||||
document.documentElement.clientWidth ||
|
||||
VIEWPORT_WIDTH;
|
||||
return Math.min(Math.max(viewportWidth, 1), MAX_PAGE_WIDTH);
|
||||
}
|
||||
|
||||
function updateScale() {
|
||||
var scale = Math.min(window.innerWidth / VIEWPORT_WIDTH, 1);
|
||||
var scaledHeight = Math.ceil(
|
||||
designHeight * (VIEWPORT_WIDTH / DESIGN_WIDTH)
|
||||
);
|
||||
var pageWidth = getPageWidth();
|
||||
var designScale = pageWidth / DESIGN_WIDTH;
|
||||
var scaledHeight = Math.ceil(designHeight * designScale);
|
||||
|
||||
// 视觉稿仍以 1080 坐标布局,只把整张背景和内容按真实 WebView 宽度等比缩放,避免 412px 等机型露出页面两侧空白。
|
||||
document.documentElement.style.setProperty(
|
||||
'--app-scale',
|
||||
String(scale)
|
||||
'--page-width',
|
||||
pageWidth + 'px'
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--design-scale',
|
||||
String(designScale)
|
||||
);
|
||||
viewport.style.minHeight = scaledHeight + 'px';
|
||||
viewport.style.height = scaledHeight + 'px';
|
||||
page.style.minHeight = scaledHeight + 'px';
|
||||
document.body.style.minHeight = Math.ceil(scaledHeight * scale) + 'px';
|
||||
page.style.height = scaledHeight + 'px';
|
||||
document.body.style.minHeight =
|
||||
Math.max(scaledHeight, window.innerHeight || 0) + 'px';
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
@ -518,6 +573,8 @@
|
||||
return loader
|
||||
.then(function (payload) {
|
||||
var statusData = payload.status || {};
|
||||
countdownEndAt = resolveCountdownEndAt(statusData);
|
||||
updateCountdown();
|
||||
state.user = normalizeUser(payload.profile, statusData);
|
||||
state.rewards = normalizeRewards(statusData);
|
||||
state.loading = false;
|
||||
@ -577,6 +634,7 @@
|
||||
claimReward(button.getAttribute('data-rule-id'));
|
||||
});
|
||||
window.addEventListener('resize', updateScale);
|
||||
window.addEventListener('orientationchange', updateScale);
|
||||
window.addEventListener('hyapp:i18n-ready', function () {
|
||||
renderPage();
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--app-scale: 1;
|
||||
--page-width: 375px;
|
||||
--design-scale: 0.3472222222;
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ body {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
background: var(--hy-theme-bg);
|
||||
background: #1b100d;
|
||||
color: #f6e0b2;
|
||||
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
}
|
||||
@ -25,16 +25,17 @@ body {
|
||||
|
||||
.app-viewport {
|
||||
position: relative;
|
||||
width: 375px;
|
||||
width: var(--page-width);
|
||||
max-width: 430px;
|
||||
min-height: 1137px;
|
||||
margin: 0 auto;
|
||||
transform: scale(var(--app-scale));
|
||||
transform-origin: top center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
width: 375px;
|
||||
width: var(--page-width);
|
||||
max-width: 430px;
|
||||
min-height: 1137px;
|
||||
overflow: hidden;
|
||||
background: #1b100d;
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
'req-sys-origin': 'origin=ATYOU;originChild=ATYOU',
|
||||
'req-version': 'V2',
|
||||
'req-zone': 'Asia/Shanghai',
|
||||
'Req-Atyou': 'true',
|
||||
};
|
||||
|
||||
function currentParams() {
|
||||
@ -430,6 +429,13 @@
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
@ -438,6 +444,7 @@
|
||||
}
|
||||
return connectToApp().then(function () {
|
||||
var requestHeaders = Object.assign({}, headers, opts.headers || {});
|
||||
stripAppEncryptedHeaders(requestHeaders);
|
||||
if (!requestHeaders.authorization)
|
||||
delete requestHeaders.authorization;
|
||||
if (body) {
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -8,9 +8,23 @@
|
||||
"name": "hyapp-h5",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"playwright-core": "^1.61.1",
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
||||
|
||||
@ -5,10 +5,12 @@
|
||||
"scripts": {
|
||||
"dev:h5": "python3 scripts/h5_dev_server.py --host 127.0.0.1 --port 8788 --directory .",
|
||||
"dev:manager-center": "python3 scripts/h5_dev_server.py --host 127.0.0.1 --port 8788 --directory .",
|
||||
"aslan:webview": "node scripts/aslan_flutter_webview.js",
|
||||
"format": "prettier --write \"**/*.{js,html,css,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"playwright-core": "^1.61.1",
|
||||
"prettier": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
511
scripts/aslan_flutter_webview.js
Executable file
511
scripts/aslan_flutter_webview.js
Executable file
@ -0,0 +1,511 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const childProcess = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const http = require('node:http');
|
||||
const path = require('node:path');
|
||||
const readline = require('node:readline/promises');
|
||||
const { stdin, stdout } = require('node:process');
|
||||
const { chromium } = require('playwright-core');
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
||||
const DEFAULT_HOST = '127.0.0.1';
|
||||
const DEFAULT_PORT = 8788;
|
||||
const DEFAULT_API_BASE = 'https://api.atuchat.com/';
|
||||
const DEFAULT_URL =
|
||||
'/aslan/recharge-activity/index.html?app_code=aslan&codex_webview=1';
|
||||
const DEFAULT_CHROME_PATHS = [
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
account: '',
|
||||
password: '',
|
||||
token: '',
|
||||
url: '',
|
||||
apiBase: DEFAULT_API_BASE,
|
||||
host: DEFAULT_HOST,
|
||||
port: DEFAULT_PORT,
|
||||
origin: 'ATYOU',
|
||||
originChild: '',
|
||||
lang: 'en',
|
||||
chrome: '',
|
||||
headless: false,
|
||||
devtools: false,
|
||||
disableWebSecurity: true,
|
||||
navigationPrompt: true,
|
||||
closeAfter: 0,
|
||||
screenshot: '',
|
||||
};
|
||||
const aliases = {
|
||||
'-a': 'account',
|
||||
'--account': 'account',
|
||||
'-p': 'password',
|
||||
'--password': 'password',
|
||||
'--pwd': 'password',
|
||||
'--token': 'token',
|
||||
'-u': 'url',
|
||||
'--url': 'url',
|
||||
'--api-base': 'apiBase',
|
||||
'--host': 'host',
|
||||
'--port': 'port',
|
||||
'--origin': 'origin',
|
||||
'--origin-child': 'originChild',
|
||||
'--lang': 'lang',
|
||||
'--chrome': 'chrome',
|
||||
'--close-after': 'closeAfter',
|
||||
'--screenshot': 'screenshot',
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const raw = argv[i];
|
||||
const [flag, inlineValue] = raw.split(/=(.*)/s);
|
||||
if (flag === '--help' || flag === '-h') {
|
||||
options.help = true;
|
||||
continue;
|
||||
}
|
||||
if (flag === '--headless') {
|
||||
options.headless = true;
|
||||
continue;
|
||||
}
|
||||
if (flag === '--devtools') {
|
||||
options.devtools = true;
|
||||
continue;
|
||||
}
|
||||
if (flag === '--no-disable-web-security') {
|
||||
options.disableWebSecurity = false;
|
||||
continue;
|
||||
}
|
||||
if (flag === '--no-nav-prompt') {
|
||||
options.navigationPrompt = false;
|
||||
continue;
|
||||
}
|
||||
const key = aliases[flag];
|
||||
if (!key) {
|
||||
throw new Error(`Unknown argument: ${raw}`);
|
||||
}
|
||||
const value = inlineValue !== undefined ? inlineValue : argv[++i];
|
||||
if (value === undefined) throw new Error(`Missing value for ${raw}`);
|
||||
options[key] =
|
||||
key === 'port' || key === 'closeAfter' ? Number(value) : value;
|
||||
}
|
||||
options.originChild = options.originChild || options.origin;
|
||||
return options;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
'Usage:',
|
||||
' npm run aslan:webview',
|
||||
' npm run aslan:webview -- --account 6666 --password 123456 --url /aslan/recharge-activity/index.html?app_code=aslan',
|
||||
'',
|
||||
'Options:',
|
||||
' -a, --account <value> Aslan login account or special ID',
|
||||
' -p, --password <value> Aslan login password',
|
||||
' --token <value> Existing Aslan token; skips account password login',
|
||||
' -u, --url <value> Target H5 URL. Relative paths use the local H5 dev server.',
|
||||
' --api-base <url> API base, default https://api.atuchat.com/',
|
||||
' --origin <value> Req-Sys-Origin origin, default ATYOU',
|
||||
' --origin-child <value> Req-Sys-Origin originChild, default same as origin',
|
||||
' --lang <value> Req-Lang, default en',
|
||||
' --chrome <path> Chrome executable path',
|
||||
' --headless Run browser headless',
|
||||
' --devtools Open DevTools',
|
||||
' --no-disable-web-security Keep browser web security enabled',
|
||||
' --no-nav-prompt Do not prompt for more URLs after opening',
|
||||
' --close-after <seconds> Auto close, useful for smoke tests',
|
||||
' --screenshot <path> Save a screenshot after initial load',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function normalizeBaseURL(value) {
|
||||
return String(value || DEFAULT_API_BASE).replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function resolveChromePath(explicitPath) {
|
||||
if (explicitPath) return explicitPath;
|
||||
return (
|
||||
DEFAULT_CHROME_PATHS.find((candidate) => fs.existsSync(candidate)) || ''
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeTargetURL(input, host, port) {
|
||||
const value = String(input || DEFAULT_URL).trim() || DEFAULT_URL;
|
||||
if (/^https?:\/\//i.test(value)) return value;
|
||||
const cleanPath = value.startsWith('/')
|
||||
? value
|
||||
: `/${value.replace(/^\.?\//, '')}`;
|
||||
return `http://${host}:${port}${cleanPath}`;
|
||||
}
|
||||
|
||||
function isLocalTarget(url, host, port) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return (
|
||||
parsed.hostname === host &&
|
||||
String(parsed.port || 80) === String(port)
|
||||
);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function healthURL(host, port) {
|
||||
return `http://${host}:${port}/__h5_dev_server__/health`;
|
||||
}
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function requestHealth(url) {
|
||||
return new Promise((resolve) => {
|
||||
const req = http.get(url, (res) => {
|
||||
res.resume();
|
||||
resolve(res.statusCode === 200);
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
req.setTimeout(800, () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureLocalServer(host, port) {
|
||||
const url = healthURL(host, port);
|
||||
if (await requestHealth(url)) return null;
|
||||
const server = childProcess.spawn(
|
||||
'python3',
|
||||
[
|
||||
path.join(PROJECT_ROOT, 'scripts/h5_dev_server.py'),
|
||||
'--host',
|
||||
host,
|
||||
'--port',
|
||||
String(port),
|
||||
'--directory',
|
||||
PROJECT_ROOT,
|
||||
],
|
||||
{
|
||||
cwd: PROJECT_ROOT,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}
|
||||
);
|
||||
server.stdout.on('data', (chunk) =>
|
||||
process.stdout.write(`[h5-server] ${chunk}`)
|
||||
);
|
||||
server.stderr.on('data', (chunk) =>
|
||||
process.stderr.write(`[h5-server] ${chunk}`)
|
||||
);
|
||||
for (let i = 0; i < 30; i += 1) {
|
||||
if (await requestHealth(url)) return server;
|
||||
await wait(200);
|
||||
}
|
||||
server.kill();
|
||||
throw new Error(`Failed to start local H5 server: ${url}`);
|
||||
}
|
||||
|
||||
async function askRequired(rl, label, currentValue) {
|
||||
if (currentValue) return currentValue;
|
||||
const value = (await rl.question(label)).trim();
|
||||
if (!value)
|
||||
throw new Error(`${label.replace(/[::]\s*$/, '')} is required`);
|
||||
return value;
|
||||
}
|
||||
|
||||
async function askPassword(rl, currentValue) {
|
||||
if (currentValue) return currentValue;
|
||||
if (!stdin.isTTY) return askRequired(rl, 'Password: ', currentValue);
|
||||
return new Promise((resolve, reject) => {
|
||||
let value = '';
|
||||
stdout.write('Password: ');
|
||||
stdin.setRawMode(true);
|
||||
stdin.resume();
|
||||
const onData = (buffer) => {
|
||||
const text = buffer.toString('utf8');
|
||||
if (text === '\u0003') {
|
||||
stdin.setRawMode(false);
|
||||
stdin.off('data', onData);
|
||||
reject(new Error('Cancelled'));
|
||||
return;
|
||||
}
|
||||
if (text === '\r' || text === '\n') {
|
||||
stdin.setRawMode(false);
|
||||
stdin.off('data', onData);
|
||||
stdout.write('\n');
|
||||
resolve(value);
|
||||
return;
|
||||
}
|
||||
if (text === '\u007f') {
|
||||
if (value.length > 0) {
|
||||
value = value.slice(0, -1);
|
||||
stdout.write('\b \b');
|
||||
}
|
||||
return;
|
||||
}
|
||||
value += text;
|
||||
stdout.write('*');
|
||||
};
|
||||
stdin.on('data', onData);
|
||||
});
|
||||
}
|
||||
|
||||
function buildReqAppIntel(model, channel, imei) {
|
||||
return [
|
||||
'build=1',
|
||||
'version=1.0.0',
|
||||
`model=${model}`,
|
||||
'sysVersion=web',
|
||||
`channel=${channel}`,
|
||||
`Req-Imei=${imei}`,
|
||||
].join(';');
|
||||
}
|
||||
|
||||
async function login(options) {
|
||||
const imei = 'codex-aslan-webview';
|
||||
const response = await fetch(
|
||||
`${normalizeBaseURL(options.apiBase)}/auth/account/login`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'req-lang': options.lang,
|
||||
'req-client': 'Android',
|
||||
'req-imei': imei,
|
||||
'req-app-intel': buildReqAppIntel(
|
||||
'CodexWebViewLogin',
|
||||
'Codex',
|
||||
imei
|
||||
),
|
||||
'req-sys-origin': `origin=${options.origin};originChild=${options.originChild}`,
|
||||
'req-version': 'V2',
|
||||
'req-zone': 'Asia/Shanghai',
|
||||
'user-agent': 'curl/8.7.1',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
account: options.account,
|
||||
pwd: options.password,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const text = await response.text();
|
||||
let payload;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : {};
|
||||
} catch (_) {
|
||||
throw new Error(
|
||||
`Login returned non-JSON response: HTTP ${response.status} ${text.slice(0, 200)}`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!response.ok ||
|
||||
payload.status === false ||
|
||||
!payload.body ||
|
||||
!payload.body.token
|
||||
) {
|
||||
throw new Error(
|
||||
`Login failed: HTTP ${response.status} ${payload.errorMsg || payload.message || text.slice(0, 200)}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
token: payload.body.token,
|
||||
profile: payload.body.userProfile || {},
|
||||
};
|
||||
}
|
||||
|
||||
function buildInitScriptPayload(options, token) {
|
||||
const imei = 'codex-aslan-webview';
|
||||
return {
|
||||
token,
|
||||
lang: options.lang,
|
||||
origin: options.origin,
|
||||
originChild: options.originChild,
|
||||
reqAppIntel: buildReqAppIntel('CodexFlutterWebView', 'Codex', imei),
|
||||
};
|
||||
}
|
||||
|
||||
async function openWebView(options, token, firstURL) {
|
||||
const chromePath = resolveChromePath(options.chrome);
|
||||
if (!chromePath) {
|
||||
throw new Error(
|
||||
'Chrome executable not found. Pass --chrome /path/to/chrome.'
|
||||
);
|
||||
}
|
||||
const launchArgs = ['--window-size=390,844'];
|
||||
if (options.disableWebSecurity) {
|
||||
launchArgs.push('--disable-web-security');
|
||||
launchArgs.push('--disable-features=IsolateOrigins,site-per-process');
|
||||
}
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromePath,
|
||||
headless: options.headless,
|
||||
devtools: options.devtools,
|
||||
args: launchArgs,
|
||||
});
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 390, height: 844 },
|
||||
deviceScaleFactor: 3,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Linux; Android 14; Aslan Flutter WebView) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.0.0 Mobile Safari/537.36',
|
||||
});
|
||||
await context.addInitScript(
|
||||
(config) => {
|
||||
window.FlutterApp = window.FlutterApp || {
|
||||
postMessage(data) {
|
||||
console.log('[FlutterApp.postMessage]', data);
|
||||
},
|
||||
};
|
||||
window.FlutterPageControl = window.FlutterPageControl || {
|
||||
postMessage(data) {
|
||||
console.log('[FlutterPageControl.postMessage]', data);
|
||||
},
|
||||
};
|
||||
window.app = window.app || {};
|
||||
window.app.getAccessOrigin = function getAccessOrigin() {
|
||||
return JSON.stringify({
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
'Req-Lang': config.lang,
|
||||
'Req-App-Intel': config.reqAppIntel,
|
||||
'Req-Sys-Origin': `origin=${config.origin};originChild=${config.originChild}`,
|
||||
});
|
||||
};
|
||||
window.app.getAuth = function getAuth() {
|
||||
return window.app.getAccessOrigin();
|
||||
};
|
||||
window.app.sendToFlutter = function sendToFlutter(data) {
|
||||
window.FlutterApp.postMessage(data);
|
||||
};
|
||||
},
|
||||
buildInitScriptPayload(options, token)
|
||||
);
|
||||
|
||||
const page = await context.newPage();
|
||||
page.on('console', (message) => {
|
||||
const type = message.type();
|
||||
if (type === 'error' || type === 'warning') {
|
||||
console.log(`[webview:${type}] ${message.text()}`);
|
||||
}
|
||||
});
|
||||
await page.goto(firstURL, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000,
|
||||
});
|
||||
await page
|
||||
.waitForLoadState('networkidle', { timeout: 15000 })
|
||||
.catch(() => {});
|
||||
return { browser, page };
|
||||
}
|
||||
|
||||
async function promptNavigationLoop(rl, page, options) {
|
||||
console.log(
|
||||
'输入新的 URL 回车可跳转;输入 q 退出。相对路径会走本地 H5 服务。'
|
||||
);
|
||||
while (!page.isClosed()) {
|
||||
const next = (await rl.question('WebView URL> ')).trim();
|
||||
if (!next) continue;
|
||||
if (next.toLowerCase() === 'q' || next.toLowerCase() === 'quit') break;
|
||||
const targetURL = normalizeTargetURL(next, options.host, options.port);
|
||||
if (isLocalTarget(targetURL, options.host, options.port)) {
|
||||
await ensureLocalServer(options.host, options.port);
|
||||
}
|
||||
console.log(`Navigate: ${targetURL}`);
|
||||
await page.goto(targetURL, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000,
|
||||
});
|
||||
await page
|
||||
.waitForLoadState('networkidle', { timeout: 15000 })
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
console.log(usage());
|
||||
return;
|
||||
}
|
||||
options.account = options.account || process.env.ASLAN_ACCOUNT || '';
|
||||
options.password = options.password || process.env.ASLAN_PASSWORD || '';
|
||||
options.token = options.token || process.env.ASLAN_TOKEN || '';
|
||||
options.url = options.url || process.env.ASLAN_WEBVIEW_URL || '';
|
||||
|
||||
const rl = readline.createInterface({ input: stdin, output: stdout });
|
||||
let localServer = null;
|
||||
let browser = null;
|
||||
try {
|
||||
if (!options.token) {
|
||||
options.account = await askRequired(
|
||||
rl,
|
||||
'Account: ',
|
||||
options.account
|
||||
);
|
||||
options.password = await askPassword(rl, options.password);
|
||||
}
|
||||
options.url =
|
||||
options.url ||
|
||||
(await rl.question(`URL [${DEFAULT_URL}]: `)).trim() ||
|
||||
DEFAULT_URL;
|
||||
|
||||
const targetURL = normalizeTargetURL(
|
||||
options.url,
|
||||
options.host,
|
||||
options.port
|
||||
);
|
||||
if (isLocalTarget(targetURL, options.host, options.port)) {
|
||||
localServer = await ensureLocalServer(options.host, options.port);
|
||||
}
|
||||
|
||||
const loginResult = options.token
|
||||
? { token: options.token, profile: { account: 'token' } }
|
||||
: await login(options);
|
||||
const displayAccount =
|
||||
loginResult.profile.account ||
|
||||
loginResult.profile.id ||
|
||||
options.account;
|
||||
console.log(
|
||||
`${options.token ? 'Token loaded' : 'Login OK'}: ${loginResult.profile.userNickname || '-'} / ${displayAccount}`
|
||||
);
|
||||
console.log(`Open WebView: ${targetURL}`);
|
||||
|
||||
const result = await openWebView(options, loginResult.token, targetURL);
|
||||
browser = result.browser;
|
||||
|
||||
if (options.screenshot) {
|
||||
await result.page.screenshot({
|
||||
path: path.resolve(options.screenshot),
|
||||
fullPage: true,
|
||||
});
|
||||
console.log(
|
||||
`Screenshot saved: ${path.resolve(options.screenshot)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (options.closeAfter > 0) {
|
||||
await wait(options.closeAfter * 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.navigationPrompt && !options.headless) {
|
||||
await promptNavigationLoop(rl, result.page, options);
|
||||
return;
|
||||
}
|
||||
|
||||
await result.page.waitForEvent('close').catch(() => {});
|
||||
} finally {
|
||||
rl.close();
|
||||
if (browser) await browser.close().catch(() => {});
|
||||
if (localServer) localServer.kill();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.message || error);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user