| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- import { RealtimeProbe, redact } from './realtime-probe.js';
- const $ = (id) => document.getElementById(id);
- const LABELS = {
- 'admin.orders.sport': '体彩订单', 'admin.orders.lhc': '六合彩 / 彩票',
- 'admin.orders.sport_game': '足球游戏订单', 'admin.orders.jisu_game': '极速游戏订单',
- 'admin.orders.third_game': '三方游戏订单', 'admin.orders.bet': '投注记录',
- 'admin.orders.recharge': 'USDT 充值', 'admin.orders.payment': '代收订单',
- 'admin.orders.third_party_deposit': '三方充值', 'admin.orders.payment_withdraw': '人民币提现 / 代付',
- 'admin.orders.withdraw': 'USDT 提现', 'admin.orders.manual_audit': '人工上下分',
- 'admin.users': '用户列表', 'admin.users.online': '在线用户',
- 'admin.stats.platform_count': '平台在线人数', 'admin.stats.unprocessed': '待处理角标',
- };
- const STATE_LABELS = { initialized: '已初始化', connecting: '正在连接', connected: '已连接',
- disconnected: '未连接', unavailable: '暂不可用', failed: '连接失败' };
- const CHANNEL_STATES = { idle: '待订阅', subscribing: '订阅中', authorizing: '授权中',
- subscribed: '已订阅', denied: '无权限', error: '失败' };
- const time = (timestamp) => new Date(timestamp).toLocaleTimeString('zh-CN', { hour12: false });
- const make = (tag, text, className) => {
- const element = document.createElement(tag);
- if (text !== undefined) element.textContent = text;
- if (className) element.className = className;
- return element;
- };
- const pretty = (value, maximum = 32768) => {
- const text = JSON.stringify(value, null, 2) ?? '';
- return text.length > maximum ? `${text.slice(0, maximum)}\n… 内容较长,已截断显示。` : text;
- };
- let records = [];
- let eventFilter = 'all';
- let recordId = 0;
- let shownConfig = null;
- let latestState;
- const selected = new Set();
- const probe = new RealtimeProbe({ Echo: window.Echo?.default || window.Echo, Pusher: window.Pusher,
- onChange: renderState, onLog: addLog, onEvent: addEvent });
- function showError(error) {
- $('global-error').textContent = redact(error.message || String(error), probe.token);
- $('global-error').hidden = false;
- }
- function check(id, status, note, label) {
- const element = $(`check-${id}`);
- element.dataset.status = status;
- $(`check-${id}-note`).textContent = note;
- element.querySelector('.check-state').textContent = label;
- element.querySelector('.check-icon').textContent = status === 'ok' ? '✓' : status === 'error' ? '!' : { api: '1', ws: '2', auth: '3', live: '4' }[id];
- }
- function renderState(state) {
- latestState = state;
- if (state.config !== shownConfig) {
- shownConfig = state.config;
- selected.clear();
- $('channel-list').replaceChildren();
- if (shownConfig) {
- for (const name of shownConfig.channels) {
- selected.add(name);
- const label = make('label', undefined, 'channel');
- label.title = name;
- const input = make('input'); input.type = 'checkbox'; input.checked = true; input.dataset.channel = name;
- input.setAttribute('aria-label', LABELS[name] || name);
- input.addEventListener('change', () => { input.checked ? selected.add(name) : selected.delete(name); renderControls(); });
- const title = make('span', LABELS[name] || name, 'channel-name');
- const status = make('span', '待订阅', 'channel-status'); status.dataset.channelStatus = name;
- label.append(input, title, status); $('channel-list').append(label);
- }
- if (!shownConfig.channels.length) $('channel-list').append(make('div', '当前管理员没有可订阅的频道,请检查账号的列表权限。', 'empty compact-empty'));
- const scheme = shownConfig.force_tls ? 'wss' : 'ws';
- $('ws-address').textContent = `${scheme}://${shownConfig.ws_host}:${shownConfig.ws_port}/app/…`;
- $('config-seq').textContent = String(shownConfig.last_seq);
- $('session-expiry').textContent = shownConfig.session_expires_at
- ? new Date(shownConfig.session_expires_at * 1000).toLocaleString('zh-CN', { hour12: false }) : '后端未提供';
- $('config-json').textContent = pretty(shownConfig);
- } else $('channel-list').append(make('div', '读取配置后,在这里选择有权限的频道。', 'empty compact-empty'));
- $('config-summary').hidden = !shownConfig;
- }
- $('connection-pill').dataset.status = state.expired ? 'failed' : state.connection;
- $('connection-label').textContent = state.expired ? '登录已过期' : STATE_LABELS[state.connection] || state.connection;
- $('live-count').textContent = String(state.liveCount);
- $('replay-count').textContent = String(state.replayCount);
- $('duplicate-count').textContent = String(state.duplicateCount);
- if (shownConfig) $('config-seq').textContent = String(state.lastSeq);
- for (const element of document.querySelectorAll('[data-channel-status]')) {
- const channel = state.channels.find((c) => c.name === element.dataset.channelStatus);
- element.dataset.status = channel?.status || 'idle';
- element.textContent = channel?.replaying ? '补发中' : CHANNEL_STATES[channel?.status || 'idle'];
- element.title = channel ? `续传点 ${channel.checkpoint} · 最近实时 SEQ ${channel.liveSeq}` : '';
- }
- check('api', state.api, state.api === 'ok' ? '配置接口已返回有效的连接参数'
- : state.api === 'loading' ? '正在请求后台配置…' : state.api === 'error' ? '请查看上方提示或连接日志' : '等待获取后台配置',
- { ok: '通过', loading: '检查中', error: '失败', idle: '待检查' }[state.api]);
- const connected = state.connection === 'connected';
- const failed = ['failed', 'unavailable'].includes(state.connection);
- check('ws', connected ? 'ok' : failed ? 'error' : state.connection === 'connecting' ? 'loading' : 'idle',
- connected ? `握手成功 · socket ${state.socketId}` : failed ? '检查公网域名、Nginx 反代、证书及 Reverb 进程' : '等待连接 Reverb',
- connected ? '通过' : failed ? '失败' : state.connection === 'connecting' ? '连接中' : '待检查');
- check('live', state.liveCount ? 'ok' : 'idle', state.liveCount ? `本次检测已收到 ${state.liveCount} 条实时消息`
- : state.replayCount ? '已收到补发消息,仍需等待新的实时事件' : '连接后,在另一后台页面触发一条真实业务变更', state.liveCount ? '通过' : '待验证');
- renderControls();
- }
- function renderControls() {
- const state = latestState;
- if (!state) return;
- const ready = state.api === 'ok';
- const connected = state.connection === 'connected';
- const subscribed = state.channels.filter((c) => selected.has(c.name) && c.status === 'subscribed').length;
- const denied = state.channels.some((c) => selected.has(c.name) && ['denied', 'error'].includes(c.status));
- const authorizing = state.channels.some((c) => selected.has(c.name) && ['authorizing', 'subscribing'].includes(c.status));
- check('auth', state.expired ? 'error' : subscribed && subscribed === selected.size ? 'ok' : denied ? 'error' : authorizing ? 'loading' : 'idle',
- state.expired ? '登录已过期,请更新 token 后重新获取配置' : subscribed ? `${subscribed} / ${selected.size} 个已选频道订阅成功`
- : denied ? '频道授权未通过,请查看连接日志' : '使用当前管理员的频道权限',
- state.expired ? '登录失效' : subscribed && subscribed === selected.size ? '通过' : denied ? '失败' : authorizing ? '授权中' : '待检查');
- $('load-config').disabled = state.api === 'loading';
- $('load-config').firstChild.textContent = state.api === 'loading' ? '正在读取配置 ' : '获取连接配置 ';
- $('connect').disabled = !ready || !selected.size || state.expired;
- $('connect').textContent = probe.echo ? '重新连接' : '连接 WebSocket';
- $('disconnect').disabled = !probe.echo;
- $('apply-channels').disabled = !connected;
- $('select-all').disabled = $('select-none').disabled = !ready;
- $('replay').disabled = !ready || !selected.size || !state.channels.some((c) => selected.has(c.name)) || state.expired
- || state.channels.some((c) => selected.has(c.name) && c.replaying);
- $('channel-count').textContent = ready ? `已选 ${selected.size} / ${state.config.channels.length}` : '尚未读取';
- const passed = Number(ready) + Number(connected) + Number(subscribed > 0 && subscribed === selected.size) + Number(state.liveCount > 0);
- $('progress-count').textContent = `${passed} / 4 通过`;
- const complete = passed === 4;
- $('verdict').dataset.status = complete ? 'ok' : '';
- $('verdict').textContent = complete ? '已收到实时消息,当前 API → 频道授权 → relay / Reverb → 浏览器链路验证通过。'
- : state.expired ? '登录已过期。重新登录后粘贴新 token,再获取配置。'
- : connected && subscribed ? '连接和订阅已通过。在另一后台页面触发一条业务变更,等待实时消息。'
- : '连接成功仅代表通道可用;收到实时事件后,才能确认业务 → relay → Reverb 的链路。';
- }
- function addLog(entry) {
- const logs = $('logs');
- if (!logs.querySelector('[data-level]')) logs.replaceChildren();
- const follow = logs.scrollHeight - logs.scrollTop - logs.clientHeight < 50;
- const row = make('li', undefined, 'log-entry'); row.dataset.level = entry.level;
- const message = entry.details ? `${entry.message} ${pretty(entry.details, 1200)}` : entry.message;
- row.append(make('time', time(entry.time)), make('span', message)); logs.append(row);
- while (logs.children.length > 120) logs.firstChild.remove();
- if (follow) logs.scrollTop = logs.scrollHeight;
- }
- function addEvent(message) {
- const event = message.event;
- records.unshift({ ...message, event: { channel: event.channel, event: event.event, action: event.action, seq: event.seq,
- key: event.key }, json: pretty(event), recordId: ++recordId });
- records = records.slice(0, 200);
- renderEvents();
- }
- function renderEvents() {
- $('event-rows').replaceChildren();
- const visible = records.filter((record) => eventFilter === 'all' || record.source === eventFilter);
- $('events-empty').hidden = visible.length > 0;
- for (const record of visible) {
- const e = record.event;
- const row = make('tr'); row.dataset.recordId = String(record.recordId);
- row.append(make('td', time(record.time)));
- const source = make('td'); source.append(make('span', record.source === 'live' ? '实时' : '补发', `tag ${record.source}`));
- if (record.duplicate) source.append(make('span', '重复', 'tag duplicate'));
- row.append(source);
- const channel = make('td', LABELS[e.channel] || e.channel);
- channel.append(make('span', e.event, 'subline')); channel.title = e.channel; row.append(channel);
- row.append(make('td', String(e.seq), 'seq'), make('td', JSON.stringify(e.key)));
- const action = make('td'); const button = make('button', '查看详情', 'text-button'); button.type = 'button';
- button.addEventListener('click', () => {
- for (const other of $('event-rows').children) other.classList.toggle('selected', other === row);
- $('event-detail').hidden = false; $('event-detail').open = true;
- $('detail-meta').textContent = `SEQ ${e.seq} · ${e.channel}`;
- $('event-json').textContent = record.json;
- });
- action.append(button); row.append(action); $('event-rows').append(row);
- }
- }
- function clearEvents() {
- records = []; $('event-detail').hidden = true; $('event-json').textContent = ''; renderEvents();
- }
- $('config-form').addEventListener('submit', async (event) => {
- event.preventDefault(); $('global-error').hidden = true; clearEvents();
- try { await probe.configure($('api-base').value, $('admin-token').value); } catch (error) { showError(error); }
- });
- for (const id of ['api-base', 'admin-token']) $(id).addEventListener('input', () => {
- $('global-error').hidden = true;
- probe.invalidate(); clearEvents();
- });
- $('toggle-token').addEventListener('click', () => {
- const show = $('admin-token').type === 'password'; $('admin-token').type = show ? 'text' : 'password';
- $('toggle-token').textContent = show ? '隐藏' : '显示';
- $('toggle-token').setAttribute('aria-label', show ? '隐藏 token' : '显示 token');
- $('toggle-token').setAttribute('aria-pressed', String(show));
- });
- $('connect').addEventListener('click', () => { $('global-error').hidden = true; try { probe.connect([...selected]); } catch (error) { showError(error); } });
- $('disconnect').addEventListener('click', () => probe.disconnect());
- $('apply-channels').addEventListener('click', () => { try { probe.subscribe([...selected]); } catch (error) { showError(error); } });
- $('replay').addEventListener('click', async () => {
- $('global-error').hidden = true;
- try { await probe.replay([...selected], $('after-seq').value === '' ? null : Number($('after-seq').value)); } catch (error) { showError(error); }
- });
- for (const [id, value] of [['select-all', true], ['select-none', false]]) $(id).addEventListener('click', () => {
- selected.clear();
- for (const checkbox of document.querySelectorAll('[data-channel]')) {
- checkbox.checked = value; if (value) selected.add(checkbox.dataset.channel);
- }
- renderControls();
- });
- for (const button of document.querySelectorAll('[data-filter]')) button.addEventListener('click', () => {
- eventFilter = button.dataset.filter;
- for (const other of document.querySelectorAll('[data-filter]')) other.setAttribute('aria-pressed', String(other === button));
- renderEvents();
- });
- $('clear-events').addEventListener('click', () => { clearEvents(); probe.clearCounts(); });
- $('clear-logs').addEventListener('click', () => { $('logs').replaceChildren(); addLog({ level: 'info', message: '日志已清空。', time: Date.now() }); });
- window.addEventListener('beforeunload', () => probe.disconnect({ quiet: true }));
- if (['http:', 'https:'].includes(location.protocol)) $('api-base').value = location.origin;
- probe.emit();
- if (location.protocol === 'file:') addLog({ level: 'warning', message: '建议将页面部署到 API 域名下打开。本地 file 页面可能被 Reverb 的来源限制拦截。', time: Date.now() });
|