充值h5
This commit is contained in:
parent
f776b87a01
commit
0a1221933e
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
var DESIGN_WIDTH = 1080;
|
var DESIGN_WIDTH = 1080;
|
||||||
var VIEWPORT_WIDTH = 375;
|
var VIEWPORT_WIDTH = 375;
|
||||||
|
var MAX_PAGE_WIDTH = 430;
|
||||||
var DEFAULT_DESIGN_HEIGHT = 3273;
|
var DEFAULT_DESIGN_HEIGHT = 3273;
|
||||||
var designHeight = DEFAULT_DESIGN_HEIGHT;
|
var designHeight = DEFAULT_DESIGN_HEIGHT;
|
||||||
var countdownEndAt = resolveCountdownEndAt();
|
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 params = getParams();
|
||||||
var explicitEnd = Date.parse(
|
var explicitEnd = Date.parse(
|
||||||
params.get('end_at') || params.get('endAt') || ''
|
params.get('end_at') || params.get('endAt') || ''
|
||||||
@ -124,23 +162,40 @@
|
|||||||
if (Number.isFinite(hours) && hours > 0)
|
if (Number.isFinite(hours) && hours > 0)
|
||||||
return Date.now() + hours * 60 * 60 * 1000;
|
return Date.now() + hours * 60 * 60 * 1000;
|
||||||
|
|
||||||
// 后端月累充接口当前只返回活动文案周期,不返回毫秒级结束时间;倒计时仍支持 URL 显式覆盖,默认只作为页面视觉计时。
|
return (
|
||||||
return Date.now() + 25 * 60 * 60 * 1000 + 61 * 1000;
|
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() {
|
function updateScale() {
|
||||||
var scale = Math.min(window.innerWidth / VIEWPORT_WIDTH, 1);
|
var pageWidth = getPageWidth();
|
||||||
var scaledHeight = Math.ceil(
|
var designScale = pageWidth / DESIGN_WIDTH;
|
||||||
designHeight * (VIEWPORT_WIDTH / DESIGN_WIDTH)
|
var scaledHeight = Math.ceil(designHeight * designScale);
|
||||||
);
|
|
||||||
|
|
||||||
|
// 视觉稿仍以 1080 坐标布局,只把整张背景和内容按真实 WebView 宽度等比缩放,避免 412px 等机型露出页面两侧空白。
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
'--app-scale',
|
'--page-width',
|
||||||
String(scale)
|
pageWidth + 'px'
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--design-scale',
|
||||||
|
String(designScale)
|
||||||
);
|
);
|
||||||
viewport.style.minHeight = scaledHeight + 'px';
|
viewport.style.minHeight = scaledHeight + 'px';
|
||||||
|
viewport.style.height = scaledHeight + 'px';
|
||||||
page.style.minHeight = 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() {
|
function updateCountdown() {
|
||||||
@ -518,6 +573,8 @@
|
|||||||
return loader
|
return loader
|
||||||
.then(function (payload) {
|
.then(function (payload) {
|
||||||
var statusData = payload.status || {};
|
var statusData = payload.status || {};
|
||||||
|
countdownEndAt = resolveCountdownEndAt(statusData);
|
||||||
|
updateCountdown();
|
||||||
state.user = normalizeUser(payload.profile, statusData);
|
state.user = normalizeUser(payload.profile, statusData);
|
||||||
state.rewards = normalizeRewards(statusData);
|
state.rewards = normalizeRewards(statusData);
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
@ -577,6 +634,7 @@
|
|||||||
claimReward(button.getAttribute('data-rule-id'));
|
claimReward(button.getAttribute('data-rule-id'));
|
||||||
});
|
});
|
||||||
window.addEventListener('resize', updateScale);
|
window.addEventListener('resize', updateScale);
|
||||||
|
window.addEventListener('orientationchange', updateScale);
|
||||||
window.addEventListener('hyapp:i18n-ready', function () {
|
window.addEventListener('hyapp:i18n-ready', function () {
|
||||||
renderPage();
|
renderPage();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--app-scale: 1;
|
--page-width: 375px;
|
||||||
--design-scale: 0.3472222222;
|
--design-scale: 0.3472222222;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: var(--hy-theme-bg);
|
background: #1b100d;
|
||||||
color: #f6e0b2;
|
color: #f6e0b2;
|
||||||
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
@ -25,16 +25,17 @@ body {
|
|||||||
|
|
||||||
.app-viewport {
|
.app-viewport {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 375px;
|
width: var(--page-width);
|
||||||
|
max-width: 430px;
|
||||||
min-height: 1137px;
|
min-height: 1137px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
transform: scale(var(--app-scale));
|
overflow: hidden;
|
||||||
transform-origin: top center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 375px;
|
width: var(--page-width);
|
||||||
|
max-width: 430px;
|
||||||
min-height: 1137px;
|
min-height: 1137px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #1b100d;
|
background: #1b100d;
|
||||||
|
|||||||
@ -16,7 +16,6 @@
|
|||||||
'req-sys-origin': 'origin=ATYOU;originChild=ATYOU',
|
'req-sys-origin': 'origin=ATYOU;originChild=ATYOU',
|
||||||
'req-version': 'V2',
|
'req-version': 'V2',
|
||||||
'req-zone': 'Asia/Shanghai',
|
'req-zone': 'Asia/Shanghai',
|
||||||
'Req-Atyou': 'true',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function currentParams() {
|
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) {
|
function request(path, options) {
|
||||||
var opts = options || {};
|
var opts = options || {};
|
||||||
var body = opts.body;
|
var body = opts.body;
|
||||||
@ -438,6 +444,7 @@
|
|||||||
}
|
}
|
||||||
return connectToApp().then(function () {
|
return connectToApp().then(function () {
|
||||||
var requestHeaders = Object.assign({}, headers, opts.headers || {});
|
var requestHeaders = Object.assign({}, headers, opts.headers || {});
|
||||||
|
stripAppEncryptedHeaders(requestHeaders);
|
||||||
if (!requestHeaders.authorization)
|
if (!requestHeaders.authorization)
|
||||||
delete requestHeaders.authorization;
|
delete requestHeaders.authorization;
|
||||||
if (body) {
|
if (body) {
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -8,9 +8,23 @@
|
|||||||
"name": "hyapp-h5",
|
"name": "hyapp-h5",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"playwright-core": "^1.61.1",
|
||||||
"prettier": "^3.0.0"
|
"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": {
|
"node_modules/prettier": {
|
||||||
"version": "3.8.3",
|
"version": "3.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
||||||
|
|||||||
@ -5,10 +5,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:h5": "python3 scripts/h5_dev_server.py --host 127.0.0.1 --port 8788 --directory .",
|
"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 .",
|
"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": "prettier --write \"**/*.{js,html,css,json,md}\"",
|
||||||
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"playwright-core": "^1.61.1",
|
||||||
"prettier": "^3.0.0"
|
"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