chatapp-h5/h5/baishun-debug.html
2026-04-16 12:13:43 +08:00

1673 lines
54 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<title>BAISHUN 调试页</title>
<style>
:root {
--bg: #120b08;
--bg-soft: #25130f;
--panel: rgba(31, 19, 15, 0.88);
--panel-strong: rgba(46, 28, 21, 0.96);
--line: rgba(255, 255, 255, 0.1);
--text: #fff5ef;
--text-soft: rgba(255, 245, 239, 0.7);
--accent: #ff6a3d;
--accent-2: #ff935f;
--accent-3: #ffd2bf;
--danger: #ff5b74;
--success: #2ad28d;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
--radius-xl: 28px;
--radius-lg: 20px;
--radius-md: 14px;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
font-family: "SF Pro Display", "PingFang SC", "Helvetica Neue",
sans-serif;
background:
radial-gradient(circle at top right, rgba(255, 146, 92, 0.24), transparent 30%),
radial-gradient(circle at top left, rgba(255, 89, 56, 0.18), transparent 34%),
linear-gradient(180deg, #1f100c 0%, #120b08 45%, #0c0807 100%);
color: var(--text);
}
body {
padding: 24px 16px 48px;
}
a {
color: inherit;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
cursor: pointer;
border: 0;
}
.app {
width: min(1180px, 100%);
margin: 0 auto;
}
.hero {
position: relative;
overflow: hidden;
padding: 24px 22px;
border: 1px solid var(--line);
border-radius: 32px;
background:
linear-gradient(135deg, rgba(255, 122, 71, 0.2), rgba(255, 196, 157, 0.08)),
rgba(24, 13, 10, 0.9);
box-shadow: var(--shadow);
}
.hero::after {
content: "";
position: absolute;
inset: auto -40px -60px auto;
width: 220px;
height: 220px;
border-radius: 999px;
background: radial-gradient(circle, rgba(255, 130, 78, 0.42), transparent 68%);
pointer-events: none;
}
.hero-kicker {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
color: var(--accent-3);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero h1 {
margin: 14px 0 8px;
font-size: clamp(28px, 5vw, 42px);
line-height: 1.06;
}
.hero p {
max-width: 760px;
margin: 0;
color: var(--text-soft);
line-height: 1.65;
}
.layout {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 18px;
margin-top: 18px;
}
.column {
display: flex;
flex-direction: column;
gap: 18px;
}
.card {
border: 1px solid var(--line);
border-radius: var(--radius-xl);
background: var(--panel);
box-shadow: var(--shadow);
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 18px 18px 0;
}
.card-title {
font-size: 18px;
font-weight: 700;
}
.card-subtitle {
margin: 4px 0 0;
color: var(--text-soft);
font-size: 13px;
line-height: 1.5;
}
.card-body {
padding: 18px;
}
.group {
display: grid;
gap: 12px;
}
.group + .group {
margin-top: 16px;
}
.label {
display: block;
margin-bottom: 7px;
color: var(--text-soft);
font-size: 12px;
}
.field,
.select,
.textarea {
width: 100%;
padding: 13px 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
background: rgba(255, 255, 255, 0.04);
color: var(--text);
outline: none;
transition: border-color 0.18s ease, transform 0.18s ease;
}
.field:focus,
.select:focus,
.textarea:focus {
border-color: rgba(255, 146, 92, 0.68);
transform: translateY(-1px);
}
.textarea {
min-height: 106px;
resize: vertical;
}
.row {
display: grid;
gap: 12px;
}
.row.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.tabs {
display: inline-flex;
gap: 8px;
padding: 6px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
}
.tab {
padding: 10px 14px;
border-radius: 999px;
background: transparent;
color: var(--text-soft);
font-weight: 600;
}
.tab.active {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: #2e120a;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 44px;
padding: 0 16px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.06);
color: var(--text);
font-weight: 700;
}
.button.primary {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: #2e120a;
}
.button.ghost {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.button.danger {
background: rgba(255, 91, 116, 0.16);
color: #ffd6de;
}
.button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.hint {
font-size: 12px;
color: var(--text-soft);
line-height: 1.6;
}
.status {
display: grid;
gap: 10px;
}
.badge-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 36px;
padding: 0 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
color: var(--text-soft);
}
.badge b {
color: var(--text);
}
.empty {
padding: 22px;
border-radius: 18px;
border: 1px dashed rgba(255, 255, 255, 0.12);
color: var(--text-soft);
text-align: center;
}
.games {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
gap: 14px;
}
.game {
display: grid;
gap: 12px;
padding: 14px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03)),
rgba(18, 12, 10, 0.88);
}
.game-cover {
position: relative;
aspect-ratio: 1.46 / 1;
border-radius: 16px;
overflow: hidden;
background:
linear-gradient(135deg, rgba(255, 110, 71, 0.2), rgba(255, 210, 191, 0.05)),
#241512;
}
.game-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.game-cover span {
position: absolute;
inset: auto 10px 10px auto;
padding: 5px 9px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.58);
font-size: 11px;
}
.game h3 {
margin: 0;
font-size: 16px;
}
.game-meta {
display: grid;
gap: 4px;
color: var(--text-soft);
font-size: 12px;
}
.shell {
position: relative;
min-height: 620px;
border-radius: 26px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
background: #080605;
}
.shell-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)),
rgba(17, 10, 8, 0.96);
}
.shell-title {
display: grid;
gap: 4px;
}
.shell-title strong {
font-size: 16px;
}
.shell-title span {
color: var(--text-soft);
font-size: 12px;
}
.shell-frame {
width: 100%;
height: 560px;
border: 0;
display: block;
background: #0b0908;
}
.shell-overlay {
position: absolute;
inset: 57px 0 0;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(8, 6, 5, 0.7);
text-align: center;
}
.shell-note {
max-width: 560px;
padding: 22px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(19, 12, 10, 0.9);
line-height: 1.7;
}
.code {
display: block;
margin-top: 10px;
padding: 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.04);
color: #ffd3c5;
font-family: "SF Mono", "Consolas", monospace;
font-size: 12px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
}
.log {
min-height: 220px;
max-height: 360px;
overflow: auto;
padding: 16px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.03);
color: #ffd8cb;
font-family: "SF Mono", "Consolas", monospace;
font-size: 12px;
line-height: 1.65;
white-space: pre-wrap;
word-break: break-word;
}
.text-good {
color: var(--success);
}
.text-bad {
color: var(--danger);
}
.footer-note {
margin-top: 18px;
color: var(--text-soft);
font-size: 12px;
line-height: 1.75;
}
@media (max-width: 980px) {
.layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
body {
padding-inline: 12px;
}
.hero,
.card,
.shell {
border-radius: 24px;
}
.row.two {
grid-template-columns: 1fr;
}
.shell-frame {
height: 520px;
}
}
</style>
</head>
<body>
<div class="app">
<section class="hero">
<div class="hero-kicker">CHATAPP3 · BAISHUN · WEB DEBUG</div>
<h1>网页登录 + 房间游戏拉起测试</h1>
<p>
这个页面按 Flutter 和 Golang 现有链路做了一层轻量测试壳:先登录拿
<code>token</code>,再请求
<code>/app/game/room/list</code>
<code>/app/game/baishun/launch</code>,最后把 BAISHUN 游戏页包进一个可注入
<code>NativeBridge</code> 的浏览器容器里,方便你脱离 app 做联调。
</p>
</section>
<main class="layout">
<div class="column">
<section class="card">
<div class="card-header">
<div>
<div class="card-title">基础配置</div>
<p class="card-subtitle">默认已指向 Flutter 配置里的线上地址。</p>
</div>
</div>
<div class="card-body">
<div class="group">
<div>
<label class="label" for="apiHost">API Host</label>
<input
id="apiHost"
class="field"
value="https://jvapi.haiyihy.com"
autocomplete="off"
/>
</div>
<div class="row two">
<div>
<label class="label" for="roomId">Room ID</label>
<input
id="roomId"
class="field"
placeholder="例如20240101"
autocomplete="off"
/>
</div>
<div>
<label class="label" for="sceneMode">Scene Mode</label>
<input
id="sceneMode"
class="field"
type="number"
value="0"
min="0"
/>
</div>
</div>
<div class="row two">
<div>
<label class="label" for="sysOrigin">Req Sys Origin</label>
<input
id="sysOrigin"
class="field"
value="LIKEI"
autocomplete="off"
/>
</div>
<div>
<label class="label" for="originChild">Origin Child</label>
<input
id="originChild"
class="field"
value="LIKEI"
autocomplete="off"
/>
</div>
</div>
<div class="row two">
<div>
<label class="label" for="reqClient">Req Client</label>
<select id="reqClient" class="select">
<option value="Android" selected>Android</option>
<option value="iOS">iOS</option>
</select>
</div>
<div>
<label class="label" for="clientOrigin">Client Origin</label>
<select id="clientOrigin" class="select">
<option value="ANDROID" selected>ANDROID</option>
<option value="IOS">IOS</option>
<option value="WEB">WEB</option>
</select>
</div>
</div>
</div>
</div>
</section>
<section class="card">
<div class="card-header">
<div>
<div class="card-title">登录</div>
<p class="card-subtitle">
优先尝试账号密码;如果线上登录口有额外网关约束,可以直接粘贴现成 Token。
</p>
</div>
<div class="tabs" id="loginTabs">
<button class="tab active" data-mode="account">账号</button>
<button class="tab" data-mode="token">Token</button>
</div>
</div>
<div class="card-body">
<div id="accountPane">
<div class="group">
<div>
<label class="label" for="account">Account</label>
<input
id="account"
class="field"
placeholder="输入 app 账号"
autocomplete="username"
/>
</div>
<div>
<label class="label" for="password">Password</label>
<input
id="password"
class="field"
type="password"
placeholder="输入 app 密码"
autocomplete="current-password"
/>
</div>
<div class="actions">
<button id="loginButton" class="button primary">
账号登录
</button>
<button id="prefillButton" class="button ghost">
回填上次输入
</button>
</div>
<div class="hint">
页面会模拟 Flutter 的请求头:
<code>req-client</code>
<code>req-app-intel</code>
<code>req-sys-origin</code>
如果登录口返回 406 或 decoder 错误,可切换到 Token 模式继续测游戏。
</div>
</div>
</div>
<div id="tokenPane" hidden>
<div class="group">
<div>
<label class="label" for="tokenText">Bearer Token</label>
<textarea
id="tokenText"
class="textarea"
placeholder="支持直接粘贴 token或带 Bearer 前缀一起粘贴"
></textarea>
</div>
<div class="actions">
<button id="useTokenButton" class="button primary">
使用 Token
</button>
<button id="clearTokenButton" class="button ghost">
清空 Token
</button>
</div>
</div>
</div>
<div class="group">
<div class="card-title" style="font-size: 15px">当前会话</div>
<div class="status" id="sessionStatus">
<div class="badge-list">
<div class="badge"><span>状态</span><b>未登录</b></div>
<div class="badge"><span>User</span><b>-</b></div>
</div>
</div>
<div class="actions">
<button id="logoutButton" class="button danger">
清空会话
</button>
</div>
</div>
</div>
</section>
<section class="card">
<div class="card-header">
<div>
<div class="card-title">调试日志</div>
<p class="card-subtitle">
会记录登录、列表、launch、桥接事件和页面注入结果。
</p>
</div>
<button id="clearLogButton" class="button ghost">清空</button>
</div>
<div class="card-body">
<pre id="log" class="log"></pre>
</div>
</section>
</div>
<div class="column">
<section class="card">
<div class="card-header">
<div>
<div class="card-title">房间游戏</div>
<p class="card-subtitle">
读取 <code>/app/game/room/list</code>
<code>/app/game/baishun/state</code>
</p>
</div>
</div>
<div class="card-body">
<div class="actions" style="margin-bottom: 14px">
<button id="loadGamesButton" class="button primary">
加载房间游戏
</button>
<button id="loadStateButton" class="button ghost">
查询房间状态
</button>
</div>
<div id="roomStateBox" class="empty">还没有查询房间状态</div>
<div style="height: 14px"></div>
<div id="gamesBox" class="empty">登录后输入 Room ID再加载游戏列表</div>
</div>
</section>
<section class="card">
<div class="card-header">
<div>
<div class="card-title">游戏壳</div>
<p class="card-subtitle">
尝试抓取入口 HTML注入与 Flutter 一致的
<code>NativeBridge</code>,方便浏览器里直接调 BAISHUN。
</p>
</div>
<div class="actions">
<button id="walletUpdateButton" class="button ghost">
walletUpdate
</button>
<button id="closeGameButton" class="button danger">
close
</button>
</div>
</div>
<div class="card-body">
<div class="shell">
<div class="shell-toolbar">
<div class="shell-title">
<strong id="shellTitle">等待拉起游戏</strong>
<span id="shellMeta">这里会显示 launch / bridge 的关键信息</span>
</div>
</div>
<iframe
id="gameFrame"
class="shell-frame"
title="BAISHUN Game Shell"
referrerpolicy="no-referrer"
></iframe>
<div id="shellOverlay" class="shell-overlay">
<div class="shell-note">
登录后先加载游戏列表,然后点击任意 BAISHUN 游戏的“启动”按钮。
<span class="code"
>当前实现会优先用 srcdoc 方式注入 bridge。
如果入口页不允许抓取,页面会退化成直接打开远程地址,并在日志里告诉你哪里失败了。</span
>
</div>
</div>
</div>
<div class="footer-note">
说明:
<br />
1. 账号登录完全走线上地址 <code>https://jvapi.haiyihy.com</code>
<br />
2. 如果游戏页调用 <code>gameRecharge</code>,这里会把它记进日志,并允许你手动触发
<code>walletUpdate</code>
<br />
3. 如果入口页是 zip 或跨域禁止抓取,日志会显示当前候选 URL方便你继续排查。
</div>
</div>
</section>
</div>
</main>
</div>
<script>
(function () {
const STORAGE_KEY = "baishun_debug_page_v1";
const state = {
loginMode: "account",
token: "",
user: null,
launch: null,
selectedGame: null,
roomGames: [],
lastState: null,
gameShellReady: false,
};
const els = {
apiHost: document.getElementById("apiHost"),
roomId: document.getElementById("roomId"),
sceneMode: document.getElementById("sceneMode"),
sysOrigin: document.getElementById("sysOrigin"),
originChild: document.getElementById("originChild"),
reqClient: document.getElementById("reqClient"),
clientOrigin: document.getElementById("clientOrigin"),
account: document.getElementById("account"),
password: document.getElementById("password"),
tokenText: document.getElementById("tokenText"),
accountPane: document.getElementById("accountPane"),
tokenPane: document.getElementById("tokenPane"),
sessionStatus: document.getElementById("sessionStatus"),
roomStateBox: document.getElementById("roomStateBox"),
gamesBox: document.getElementById("gamesBox"),
log: document.getElementById("log"),
gameFrame: document.getElementById("gameFrame"),
shellOverlay: document.getElementById("shellOverlay"),
shellTitle: document.getElementById("shellTitle"),
shellMeta: document.getElementById("shellMeta"),
loginTabs: document.getElementById("loginTabs"),
};
function nowTime() {
return new Date().toLocaleTimeString("zh-CN", {
hour12: false,
});
}
function log(title, payload, level) {
const prefix = `[${nowTime()}] ${title}`;
const raw =
payload === undefined
? ""
: typeof payload === "string"
? payload
: JSON.stringify(payload, null, 2);
const marker = level === "error" ? " [ERROR]" : level === "success" ? " [OK]" : "";
els.log.textContent = `${prefix}${marker}\n${raw}\n\n${els.log.textContent}`;
}
function saveLocal() {
const data = {
apiHost: els.apiHost.value.trim(),
roomId: els.roomId.value.trim(),
sceneMode: els.sceneMode.value.trim(),
sysOrigin: els.sysOrigin.value.trim(),
originChild: els.originChild.value.trim(),
reqClient: els.reqClient.value,
clientOrigin: els.clientOrigin.value,
account: els.account.value.trim(),
token: state.token,
loginMode: state.loginMode,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function loadLocal() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return;
}
const data = JSON.parse(raw);
if (data.apiHost) els.apiHost.value = data.apiHost;
if (data.roomId) els.roomId.value = data.roomId;
if (data.sceneMode) els.sceneMode.value = data.sceneMode;
if (data.sysOrigin) els.sysOrigin.value = data.sysOrigin;
if (data.originChild) els.originChild.value = data.originChild;
if (data.reqClient) els.reqClient.value = data.reqClient;
if (data.clientOrigin) els.clientOrigin.value = data.clientOrigin;
if (data.account) els.account.value = data.account;
if (data.token) {
state.token = normalizeBearer(data.token);
els.tokenText.value = state.token;
}
if (data.loginMode) {
state.loginMode = data.loginMode;
}
} catch (error) {
log("loadLocal failed", String(error), "error");
}
}
function normalizeBearer(rawToken) {
const token = String(rawToken || "").trim();
if (!token) return "";
return token.toLowerCase().startsWith("bearer ")
? token
: `Bearer ${token}`;
}
function getTokenOnly() {
return state.token.replace(/^Bearer\s+/i, "").trim();
}
function getApiHost() {
return els.apiHost.value.trim().replace(/\/+$/, "");
}
function buildHeaders(withAuth) {
const sysOrigin = (els.sysOrigin.value || "LIKEI").trim() || "LIKEI";
const originChild =
(els.originChild.value || sysOrigin).trim() || sysOrigin;
const client = els.reqClient.value || "Android";
const browserModel = navigator.platform || "Browser";
const sysVersion = navigator.userAgent || "Browser";
const headers = {
"Content-Type": "application/json; charset=UTF-8",
"req-lang": "en",
"req-client": client,
"req-imei": "baishun-web-debug-device",
"req-app-intel": `version=1.0.0;build=1;model=${browserModel};sysVersion=${sysVersion};channel=web`,
"req-sys-origin": `origin=${sysOrigin};originChild=${originChild}`,
"X-Forwarded-Proto": "http",
};
if (withAuth && state.token) {
headers.Authorization = state.token;
}
return headers;
}
async function requestJSON(path, options) {
const config = options || {};
const url = `${getApiHost()}${path}`;
const fetchOptions = {
method: config.method || "GET",
headers: {
...buildHeaders(config.auth !== false),
...(config.headers || {}),
},
};
if (config.body !== undefined) {
fetchOptions.body = JSON.stringify(config.body);
}
const response = await fetch(url, fetchOptions);
const rawText = await response.text();
let json = null;
try {
json = rawText ? JSON.parse(rawText) : null;
} catch (error) {
log("requestJSON parse error", { url, rawText }, "error");
}
const body =
json && typeof json === "object"
? json.body !== undefined
? json.body
: json.data !== undefined
? json.data
: json
: json;
if (!response.ok) {
const message =
(json && (json.errorMsg || json.message || json.errorCodeName)) ||
`HTTP ${response.status}`;
const err = new Error(message);
err.response = json;
err.status = response.status;
throw err;
}
if (json && json.status === false) {
const err = new Error(json.errorMsg || json.message || "request failed");
err.response = json;
err.status = response.status;
throw err;
}
return {
response,
json,
body,
rawText,
};
}
function setBusy(button, busy, label) {
if (!button) return;
if (busy) {
button.dataset.originalText = button.textContent;
button.disabled = true;
button.textContent = label || "处理中...";
return;
}
button.disabled = false;
button.textContent = button.dataset.originalText || button.textContent;
}
function renderSessionStatus() {
const userProfile = state.user && state.user.userProfile;
const credential = state.user && state.user.userCredential;
const nickname =
(userProfile && (userProfile.userNickname || userProfile.account || userProfile.id)) ||
"-";
const userId =
(credential && credential.userId) ||
(userProfile && userProfile.id) ||
"-";
const tokenShort = getTokenOnly();
const tokenView = tokenShort
? `${tokenShort.slice(0, 16)}...${tokenShort.slice(-10)}`
: "-";
els.sessionStatus.innerHTML = `
<div class="badge-list">
<div class="badge"><span>状态</span><b>${state.token ? "已登录" : "未登录"}</b></div>
<div class="badge"><span>User</span><b>${escapeHtml(String(nickname))}</b></div>
<div class="badge"><span>User ID</span><b>${escapeHtml(String(userId))}</b></div>
</div>
<div class="badge-list">
<div class="badge"><span>Token</span><b>${escapeHtml(tokenView)}</b></div>
</div>
`;
}
function renderRoomState() {
if (!state.lastState) {
els.roomStateBox.className = "empty";
els.roomStateBox.textContent = "还没有查询房间状态";
return;
}
const item = state.lastState;
els.roomStateBox.className = "";
els.roomStateBox.innerHTML = `
<div class="badge-list">
<div class="badge"><span>状态</span><b>${escapeHtml(item.state || "-")}</b></div>
<div class="badge"><span>Session</span><b>${escapeHtml(item.gameSessionId || "-")}</b></div>
<div class="badge"><span>Game</span><b>${escapeHtml(item.currentGameName || item.currentGameId || "-")}</b></div>
</div>
`;
}
function renderGames() {
if (!state.roomGames.length) {
els.gamesBox.className = "empty";
els.gamesBox.textContent = "这个房间当前没有可用的 BAISHUN 游戏";
return;
}
els.gamesBox.className = "games";
els.gamesBox.innerHTML = state.roomGames
.map((game) => {
const cover = escapeAttribute(game.cover || "");
const title = escapeHtml(game.name || game.gameId || "Unnamed Game");
const meta = [
`gameId: ${game.gameId || "-"}`,
`vendorGameId: ${game.vendorGameId || "-"}`,
`launchMode: ${game.launchMode || "-"}`,
`orientation: ${game.orientation || 0}`,
`safeHeight: ${game.safeHeight || 0}`,
];
return `
<article class="game">
<div class="game-cover">
${
cover
? `<img src="${cover}" alt="${title}" referrerpolicy="no-referrer" />`
: ""
}
<span>${escapeHtml(game.vendorType || "BAISHUN")}</span>
</div>
<div>
<h3>${title}</h3>
<div class="game-meta">${meta.map((line) => `<div>${escapeHtml(line)}</div>`).join("")}</div>
</div>
<div class="actions">
<button class="button primary" data-launch="${escapeAttribute(
game.gameId || ""
)}">启动</button>
</div>
</article>
`;
})
.join("");
}
function setShellIdle(message) {
els.shellTitle.textContent = "等待拉起游戏";
els.shellMeta.textContent = "这里会显示 launch / bridge 的关键信息";
els.shellOverlay.hidden = false;
els.shellOverlay.innerHTML = `
<div class="shell-note">
${escapeHtml(message || "登录后先加载游戏列表,然后点击“启动”")}
</div>
`;
els.gameFrame.removeAttribute("src");
els.gameFrame.setAttribute("srcdoc", "");
state.launch = null;
state.selectedGame = null;
state.gameShellReady = false;
}
function setShellInfo(title, meta) {
els.shellTitle.textContent = title;
els.shellMeta.textContent = meta;
}
function showShellNote(message, detail) {
els.shellOverlay.hidden = false;
els.shellOverlay.innerHTML = `
<div class="shell-note">
${escapeHtml(message)}
${
detail
? `<span class="code">${escapeHtml(typeof detail === "string" ? detail : JSON.stringify(detail, null, 2))}</span>`
: ""
}
</div>
`;
}
function hideShellNote() {
els.shellOverlay.hidden = true;
}
async function loginByAccount(button) {
const account = els.account.value.trim();
const password = els.password.value;
if (!account || !password) {
alert("账号和密码不能为空");
return;
}
setBusy(button, true, "登录中...");
saveLocal();
try {
const result = await requestJSON("/auth/account/login", {
method: "POST",
auth: false,
body: {
account,
pwd: password,
},
});
state.user = result.body || null;
state.token = normalizeBearer(result.body && result.body.token);
els.tokenText.value = state.token;
localStorage.setItem("Login_Account", account);
localStorage.setItem("Login_Pwd", password);
saveLocal();
renderSessionStatus();
log("account login success", {
userId:
(state.user &&
state.user.userCredential &&
state.user.userCredential.userId) ||
null,
account:
(state.user &&
state.user.userProfile &&
state.user.userProfile.account) ||
account,
}, "success");
} catch (error) {
log(
"account login failed",
error && error.response ? error.response : String(error),
"error"
);
alert(
"账号登录失败,已把返回写入日志。\n如果线上登录口仍有额外限制请切到 Token 模式继续测试。"
);
} finally {
renderSessionStatus();
setBusy(button, false);
}
}
function useToken() {
const raw = els.tokenText.value.trim();
if (!raw) {
alert("请先粘贴 Token");
return;
}
state.token = normalizeBearer(raw);
state.user = state.user || null;
saveLocal();
renderSessionStatus();
log("token applied", { tokenPreview: `${getTokenOnly().slice(0, 12)}...` }, "success");
}
function logout() {
state.token = "";
state.user = null;
state.launch = null;
state.selectedGame = null;
state.roomGames = [];
state.lastState = null;
state.gameShellReady = false;
els.tokenText.value = "";
renderSessionStatus();
renderRoomState();
renderGames();
setShellIdle("会话已清空");
saveLocal();
log("session cleared", {}, "success");
}
function ensureReadyForAuthCall() {
if (!state.token) {
throw new Error("请先登录或先粘贴 Bearer Token");
}
const roomId = els.roomId.value.trim();
if (!roomId) {
throw new Error("请先填写 Room ID");
}
return roomId;
}
async function loadGames(button) {
setBusy(button, true, "加载中...");
try {
const roomId = ensureReadyForAuthCall();
const result = await requestJSON(
`/app/game/room/list?roomId=${encodeURIComponent(roomId)}`,
{
method: "GET",
}
);
const list = (result.body && result.body.items) || [];
state.roomGames = Array.isArray(list) ? list : [];
renderGames();
log("room game list", state.roomGames, "success");
} catch (error) {
state.roomGames = [];
renderGames();
log(
"load room games failed",
error && error.response ? error.response : String(error),
"error"
);
alert(error.message || "加载游戏列表失败");
} finally {
setBusy(button, false);
}
}
async function loadRoomState(button) {
setBusy(button, true, "查询中...");
try {
const roomId = ensureReadyForAuthCall();
const result = await requestJSON(
`/app/game/baishun/state?roomId=${encodeURIComponent(roomId)}`,
{
method: "GET",
}
);
state.lastState = result.body || null;
renderRoomState();
log("room state", state.lastState, "success");
} catch (error) {
log(
"load room state failed",
error && error.response ? error.response : String(error),
"error"
);
alert(error.message || "查询房间状态失败");
} finally {
setBusy(button, false);
}
}
async function launchGame(gameId, button) {
setBusy(button, true, "启动中...");
try {
const roomId = ensureReadyForAuthCall();
const sceneMode = Number(els.sceneMode.value || 0) || 0;
const clientOrigin = els.clientOrigin.value || "ANDROID";
const result = await requestJSON("/app/game/baishun/launch", {
method: "POST",
body: {
roomId,
gameId,
sceneMode,
clientOrigin,
},
});
state.launch = result.body;
state.selectedGame = state.roomGames.find((item) => item.gameId === gameId) || {
gameId,
name: gameId,
};
setShellInfo(
state.selectedGame.name || gameId,
`session=${state.launch.gameSessionId || "-"} · entry=${state.launch.entry && state.launch.entry.entryUrl ? state.launch.entry.entryUrl : "-"}`
);
log("launch success", state.launch, "success");
await mountGameShell();
} catch (error) {
log(
"launch game failed",
error && error.response ? error.response : String(error),
"error"
);
showShellNote("游戏拉起失败", error && error.response ? error.response : String(error));
alert(error.message || "启动游戏失败");
} finally {
setBusy(button, false);
}
}
function collectCandidateUrls(entry) {
const values = [];
if (!entry) return values;
[entry.entryUrl, entry.previewUrl, entry.downloadUrl].forEach((item) => {
const value = String(item || "").trim();
if (value && !values.includes(value) && !value.startsWith("mock://")) {
values.push(value);
}
});
return values;
}
async function fetchHtmlCandidate(url) {
const response = await fetch(url, {
method: "GET",
mode: "cors",
credentials: "omit",
});
const contentType = response.headers.get("content-type") || "";
const text = await response.text();
const looksLikeHtml =
/text\/html|application\/xhtml\+xml/i.test(contentType) ||
/^\s*<!doctype html/i.test(text) ||
/^\s*<html/i.test(text);
return {
ok: response.ok,
contentType,
text,
looksLikeHtml,
url,
};
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function escapeAttribute(value) {
return escapeHtml(value);
}
function buildInjectedHtml(sourceHtml, baseUrl) {
const launch = state.launch || {};
const bridgeConfig = JSON.stringify((launch && launch.bridgeConfig) || {});
const bridgeBootstrap = `
<base href="${escapeAttribute(baseUrl)}" />
<script>
(function() {
if (window.__baishunDebugBridgeReady) return;
window.__baishunDebugBridgeReady = true;
var config = ${bridgeConfig};
window.__baishunLastConfig = config;
function emit(action, payload) {
try {
parent.postMessage({
source: "baishun-debug-frame",
action: action,
payload: payload || {}
}, "*");
} catch (error) {}
}
window.NativeBridge = window.NativeBridge || {};
window.NativeBridge.getConfig = function(callback) {
if (typeof callback === "function") {
try { callback(config); } catch (error) {}
}
emit("getConfig", config);
return config;
};
window.NativeBridge.destroy = function(payload) {
emit("destroy", payload || {});
};
window.NativeBridge.gameRecharge = function(payload) {
emit("gameRecharge", payload || {});
};
window.NativeBridge.gameLoaded = function(payload) {
emit("gameLoaded", payload || {});
};
window.baishunChannel = window.baishunChannel || {};
window.baishunChannel.walletUpdate = function(payload) {
window.__baishunLastWalletPayload = payload || {};
if (typeof window.walletUpdate === "function") {
try { window.walletUpdate(payload || {}); } catch (error) {}
}
if (typeof window.onWalletUpdate === "function") {
try { window.onWalletUpdate(payload || {}); } catch (error) {}
}
if (typeof window.dispatchEvent === "function") {
try {
window.dispatchEvent(new CustomEvent("walletUpdate", { detail: payload || {} }));
} catch (error) {}
}
emit("walletUpdate", payload || {});
};
window.addEventListener("error", function(event) {
emit("frameError", {
message: (event && event.message) || "unknown frame error",
filename: (event && event.filename) || "",
lineno: (event && event.lineno) || 0
});
});
emit("bridgeReady", {
baseUrl: "${escapeAttribute(baseUrl)}"
});
})();
<\/script>`;
if (/<head[^>]*>/i.test(sourceHtml)) {
return sourceHtml.replace(/<head([^>]*)>/i, `<head$1>${bridgeBootstrap}`);
}
if (/<html[^>]*>/i.test(sourceHtml)) {
return sourceHtml.replace(/<html([^>]*)>/i, `<html$1><head>${bridgeBootstrap}</head>`);
}
return `<!DOCTYPE html><html><head>${bridgeBootstrap}</head><body>${sourceHtml}</body></html>`;
}
async function mountGameShell() {
if (!state.launch || !state.launch.entry) {
showShellNote("当前没有可加载的 launch 结果");
return;
}
state.gameShellReady = false;
showShellNote("正在尝试抓取入口页并注入 BAISHUN Bridge...");
const candidates = collectCandidateUrls(state.launch.entry);
log("game entry candidates", candidates);
if (!candidates.length) {
showShellNote("launch 返回里没有可用入口地址", state.launch.entry);
return;
}
for (const url of candidates) {
try {
const fetched = await fetchHtmlCandidate(url);
log("candidate fetch result", {
url,
statusOk: fetched.ok,
contentType: fetched.contentType,
looksLikeHtml: fetched.looksLikeHtml,
textPreview: String(fetched.text || "").slice(0, 180),
});
if (!fetched.ok) {
continue;
}
if (!fetched.looksLikeHtml) {
continue;
}
const html = buildInjectedHtml(
fetched.text,
url.replace(/[^/]*([?#].*)?$/, "")
);
els.gameFrame.srcdoc = html;
hideShellNote();
state.gameShellReady = true;
setShellInfo(
`${state.selectedGame && state.selectedGame.name ? state.selectedGame.name : "BAISHUN Game"}`,
`srcdoc 注入成功 · ${url}`
);
log("game shell mounted with srcdoc", { url }, "success");
return;
} catch (error) {
log(
"candidate fetch failed",
{
url,
error: String(error),
},
"error"
);
}
}
const fallback = candidates[0];
els.gameFrame.removeAttribute("srcdoc");
els.gameFrame.src = fallback;
showShellNote(
"入口页无法被浏览器抓取注入,已退化成直接打开远程地址。这个模式下游戏如果强依赖 NativeBridge可能无法完整工作。",
{
fallback,
entry: state.launch.entry,
}
);
setShellInfo(
`${state.selectedGame && state.selectedGame.name ? state.selectedGame.name : "BAISHUN Game"}`,
`fallback 远程直开 · ${fallback}`
);
log("game shell fallback to remote src", { fallback }, "error");
}
async function notifyWalletUpdate() {
if (!state.launch) {
alert("还没有启动游戏");
return;
}
const payload = {
timestamp: Date.now(),
roomId: (state.launch.bridgeConfig && state.launch.bridgeConfig.roomId) || els.roomId.value.trim(),
source: "baishun-debug-page",
};
try {
const frameWindow = els.gameFrame.contentWindow;
if (
frameWindow &&
frameWindow.baishunChannel &&
typeof frameWindow.baishunChannel.walletUpdate === "function"
) {
frameWindow.baishunChannel.walletUpdate(payload);
log("walletUpdate dispatched", payload, "success");
return;
}
if (frameWindow) {
frameWindow.postMessage(
{
source: "baishun-debug-parent",
action: "walletUpdate",
payload,
},
"*"
);
}
log("walletUpdate posted by message", payload, "success");
} catch (error) {
log("walletUpdate failed", String(error), "error");
alert("walletUpdate 触发失败,详情看日志");
}
}
async function closeGame() {
if (!state.launch) {
setShellIdle("当前没有游戏会话");
return;
}
try {
const roomId = els.roomId.value.trim();
const result = await requestJSON("/app/game/baishun/close", {
method: "POST",
body: {
roomId,
gameSessionId: state.launch.gameSessionId || "",
reason: "web_debug_close",
},
});
log("close game response", result.body || result.json, "success");
} catch (error) {
log(
"close game failed",
error && error.response ? error.response : String(error),
"error"
);
} finally {
setShellIdle("游戏已关闭");
}
}
function switchLoginMode(mode) {
state.loginMode = mode === "token" ? "token" : "account";
const tabs = els.loginTabs.querySelectorAll(".tab");
tabs.forEach((tab) => {
tab.classList.toggle("active", tab.dataset.mode === state.loginMode);
});
els.accountPane.hidden = state.loginMode !== "account";
els.tokenPane.hidden = state.loginMode !== "token";
saveLocal();
}
function attachEvents() {
document.getElementById("loginButton").addEventListener("click", function () {
loginByAccount(this);
});
document.getElementById("prefillButton").addEventListener("click", function () {
els.account.value = localStorage.getItem("Login_Account") || els.account.value;
els.password.value = localStorage.getItem("Login_Pwd") || els.password.value;
log("prefill credentials", { account: els.account.value || "" }, "success");
});
document.getElementById("useTokenButton").addEventListener("click", useToken);
document.getElementById("clearTokenButton").addEventListener("click", function () {
els.tokenText.value = "";
state.token = "";
renderSessionStatus();
saveLocal();
});
document.getElementById("logoutButton").addEventListener("click", logout);
document.getElementById("loadGamesButton").addEventListener("click", function () {
loadGames(this);
});
document.getElementById("loadStateButton").addEventListener("click", function () {
loadRoomState(this);
});
document.getElementById("walletUpdateButton").addEventListener("click", notifyWalletUpdate);
document.getElementById("closeGameButton").addEventListener("click", closeGame);
document.getElementById("clearLogButton").addEventListener("click", function () {
els.log.textContent = "";
});
els.loginTabs.addEventListener("click", function (event) {
const button = event.target.closest(".tab");
if (!button) return;
switchLoginMode(button.dataset.mode);
});
els.gamesBox.addEventListener("click", function (event) {
const button = event.target.closest("[data-launch]");
if (!button) return;
launchGame(button.dataset.launch, button);
});
[
els.apiHost,
els.roomId,
els.sceneMode,
els.sysOrigin,
els.originChild,
els.reqClient,
els.clientOrigin,
els.account,
els.tokenText,
].forEach((element) => {
element.addEventListener("change", saveLocal);
element.addEventListener("blur", saveLocal);
});
window.addEventListener("message", function (event) {
const data = event.data;
if (!data || typeof data !== "object") return;
if (data.source === "baishun-debug-frame") {
log(`bridge event: ${data.action}`, data.payload || {}, data.action === "frameError" ? "error" : "success");
if (data.action === "gameLoaded") {
hideShellNote();
}
if (data.action === "destroy") {
closeGame();
}
if (data.action === "gameRecharge") {
showShellNote("游戏页触发了 gameRecharge。你可以点击上方 walletUpdate 手动回推余额变更事件。");
}
return;
}
if (data.source === "baishun-debug-parent" && data.action === "walletUpdate") {
log("walletUpdate loopback", data.payload || {}, "success");
}
});
}
loadLocal();
renderSessionStatus();
renderRoomState();
renderGames();
setShellIdle("登录后先加载游戏列表,然后点击“启动”");
switchLoginMode(state.loginMode);
attachEvents();
if (state.token) {
log("restored token from localStorage", { tokenPreview: `${getTokenOnly().slice(0, 12)}...` }, "success");
}
log("page ready", {
apiHost: getApiHost(),
defaultClientOrigin: els.clientOrigin.value,
defaultReqClient: els.reqClient.value,
}, "success");
})();
</script>
</body>
</html>