1673 lines
54 KiB
HTML
1673 lines
54 KiB
HTML
<!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, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
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>
|