mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-04-30 19:14:07 +08:00
758 lines
22 KiB
HTML
758 lines
22 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>OpenIM WS Demo</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f2efe8;
|
||
--panel: rgba(255, 250, 242, 0.94);
|
||
--panel-2: #fffdf8;
|
||
--border: #d5c6ab;
|
||
--text: #1e1a14;
|
||
--muted: #665c4f;
|
||
--accent: #b45736;
|
||
--accent-2: #23506a;
|
||
--ok: #1c6a57;
|
||
--err: #a2352d;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||
color: var(--text);
|
||
background:
|
||
radial-gradient(circle at top left, rgba(180, 87, 54, 0.13), transparent 28%),
|
||
radial-gradient(circle at bottom right, rgba(35, 80, 106, 0.16), transparent 30%),
|
||
linear-gradient(180deg, #f7f2ea, var(--bg));
|
||
}
|
||
|
||
main {
|
||
max-width: 1280px;
|
||
margin: 0 auto;
|
||
padding: 24px 16px 40px;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0 0 8px;
|
||
font-size: clamp(30px, 4vw, 46px);
|
||
letter-spacing: -0.04em;
|
||
line-height: 1;
|
||
}
|
||
|
||
.lead {
|
||
margin: 0 0 18px;
|
||
color: var(--muted);
|
||
line-height: 1.5;
|
||
max-width: 900px;
|
||
}
|
||
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: 380px minmax(0, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.panel {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 22px;
|
||
box-shadow: 0 16px 40px rgba(31, 27, 22, 0.06);
|
||
}
|
||
|
||
.sidebar {
|
||
padding: 18px;
|
||
}
|
||
|
||
.content {
|
||
display: grid;
|
||
grid-template-rows: auto auto auto minmax(420px, 1fr);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.section + .section {
|
||
margin-top: 18px;
|
||
}
|
||
|
||
h2 {
|
||
margin: 0 0 12px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
input,
|
||
textarea {
|
||
width: 100%;
|
||
border: 1px solid var(--border);
|
||
background: var(--panel-2);
|
||
border-radius: 14px;
|
||
padding: 11px 12px;
|
||
color: var(--text);
|
||
font: inherit;
|
||
}
|
||
|
||
textarea {
|
||
min-height: 100px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.grid {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.grid.two {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
button {
|
||
border: 0;
|
||
border-radius: 999px;
|
||
padding: 10px 16px;
|
||
color: white;
|
||
background: var(--accent);
|
||
font: inherit;
|
||
cursor: pointer;
|
||
}
|
||
|
||
button.alt {
|
||
background: var(--accent-2);
|
||
}
|
||
|
||
button.ghost {
|
||
background: #706451;
|
||
}
|
||
|
||
button:disabled {
|
||
opacity: 0.6;
|
||
cursor: progress;
|
||
}
|
||
|
||
.status {
|
||
margin-top: 12px;
|
||
padding: 12px 14px;
|
||
border-radius: 14px;
|
||
background: #f4ecdf;
|
||
line-height: 1.5;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.status.ok {
|
||
color: var(--ok);
|
||
}
|
||
|
||
.status.err {
|
||
color: var(--err);
|
||
}
|
||
|
||
.note {
|
||
color: var(--muted);
|
||
line-height: 1.5;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 16px 18px;
|
||
border-bottom: 1px solid rgba(213, 198, 171, 0.75);
|
||
}
|
||
|
||
.toolbar .meta {
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.block {
|
||
padding: 16px 18px;
|
||
border-bottom: 1px solid rgba(213, 198, 171, 0.65);
|
||
}
|
||
|
||
.block:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.console {
|
||
overflow: auto;
|
||
padding: 18px;
|
||
min-height: 420px;
|
||
max-height: 68vh;
|
||
background: rgba(255, 252, 247, 0.75);
|
||
font-family: "SFMono-Regular", "Consolas", monospace;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.line {
|
||
padding: 10px 12px;
|
||
border-radius: 12px;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
border: 1px solid rgba(213, 198, 171, 0.5);
|
||
margin-bottom: 10px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.line.in {
|
||
border-left: 4px solid #23506a;
|
||
}
|
||
|
||
.line.out {
|
||
border-left: 4px solid #b45736;
|
||
}
|
||
|
||
.line.sys {
|
||
border-left: 4px solid #7a6a56;
|
||
}
|
||
|
||
.line.err {
|
||
border-left: 4px solid #a2352d;
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
.layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.grid.two {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
<script src="https://cdn.jsdelivr.net/npm/protobufjs@7.4.0/dist/protobuf.min.js"></script>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>OpenIM WS Demo</h1>
|
||
<p class="lead">
|
||
这个页面是真正走 WebSocket 的最小 OpenIM 浏览器客户端。它使用浏览器原生
|
||
<code>new WebSocket(url)</code> 握手,再用 OpenIM 的 JSON envelope +
|
||
protobuf 数据体发二进制帧。
|
||
</p>
|
||
|
||
<div class="layout">
|
||
<aside class="panel sidebar">
|
||
<div class="section">
|
||
<h2>连接参数</h2>
|
||
<div class="grid">
|
||
<div>
|
||
<label for="apiBase">API Base URL</label>
|
||
<input id="apiBase" value="http://127.0.0.1:10002" />
|
||
</div>
|
||
<div>
|
||
<label for="wsBase">WS URL</label>
|
||
<input id="wsBase" value="ws://127.0.0.1:10001/" />
|
||
</div>
|
||
<div>
|
||
<label for="operationID">Operation ID</label>
|
||
<input id="operationID" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>管理员</h2>
|
||
<div class="grid two">
|
||
<div>
|
||
<label for="adminUserID">Admin User ID</label>
|
||
<input id="adminUserID" value="imAdmin" />
|
||
</div>
|
||
<div>
|
||
<label for="adminSecret">Secret</label>
|
||
<input id="adminSecret" value="openIM123" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>当前登录用户</h2>
|
||
<div class="grid two">
|
||
<div>
|
||
<label for="userID">User ID</label>
|
||
<input id="userID" value="user_1" />
|
||
</div>
|
||
<div>
|
||
<label for="peerUserID">Peer User ID</label>
|
||
<input id="peerUserID" value="user_2" />
|
||
</div>
|
||
</div>
|
||
<div class="grid two" style="margin-top: 12px">
|
||
<div>
|
||
<label for="platformID">Platform ID</label>
|
||
<input id="platformID" value="5" />
|
||
</div>
|
||
<div>
|
||
<label for="sdkType">SDK Type</label>
|
||
<input id="sdkType" value="js" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h2>文本消息</h2>
|
||
<textarea id="textContent">hello from native browser websocket</textarea>
|
||
<div class="actions">
|
||
<button id="prepareBtn">拿 Token</button>
|
||
<button class="alt" id="connectBtn">连接 WS</button>
|
||
<button class="ghost" id="disconnectBtn">断开</button>
|
||
</div>
|
||
<div class="actions">
|
||
<button id="getSeqBtn">WSGetNewestSeq</button>
|
||
<button class="alt" id="sendMsgBtn">WSSendMsg</button>
|
||
</div>
|
||
<div class="note" style="margin-top: 10px">
|
||
这里不是 HTTP fallback。点击“连接 WS”后,会真的对
|
||
<code>ws://127.0.0.1:10001</code> 发浏览器原生 WebSocket 握手请求。
|
||
</div>
|
||
<div id="status" class="status"><pre>Ready.</pre></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="panel content">
|
||
<div class="toolbar">
|
||
<div>
|
||
<strong>连接状态</strong>
|
||
<div class="meta" id="connectionMeta">未连接</div>
|
||
</div>
|
||
<div class="meta" id="reqMeta">msgIncr=0</div>
|
||
</div>
|
||
|
||
<div class="block">
|
||
<h2>Last URL</h2>
|
||
<div id="lastURL" class="note">尚未构造</div>
|
||
</div>
|
||
|
||
<div class="block">
|
||
<h2>Raw Envelope</h2>
|
||
<div id="rawEnvelope" class="note">尚未发送</div>
|
||
</div>
|
||
|
||
<div class="console" id="console"></div>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
const els = {
|
||
apiBase: $("apiBase"),
|
||
wsBase: $("wsBase"),
|
||
operationID: $("operationID"),
|
||
adminUserID: $("adminUserID"),
|
||
adminSecret: $("adminSecret"),
|
||
userID: $("userID"),
|
||
peerUserID: $("peerUserID"),
|
||
platformID: $("platformID"),
|
||
sdkType: $("sdkType"),
|
||
textContent: $("textContent"),
|
||
prepareBtn: $("prepareBtn"),
|
||
connectBtn: $("connectBtn"),
|
||
disconnectBtn: $("disconnectBtn"),
|
||
getSeqBtn: $("getSeqBtn"),
|
||
sendMsgBtn: $("sendMsgBtn"),
|
||
status: $("status"),
|
||
connectionMeta: $("connectionMeta"),
|
||
reqMeta: $("reqMeta"),
|
||
lastURL: $("lastURL"),
|
||
rawEnvelope: $("rawEnvelope"),
|
||
console: $("console"),
|
||
};
|
||
|
||
const state = {
|
||
adminToken: "",
|
||
userToken: "",
|
||
ws: null,
|
||
msgIncr: 0,
|
||
proto: null,
|
||
};
|
||
|
||
const SDKWS_PROTO = `
|
||
syntax = "proto3";
|
||
package openim.sdkws;
|
||
|
||
enum PullOrder {
|
||
PullOrderAsc = 0;
|
||
PullOrderDesc = 1;
|
||
}
|
||
|
||
message GetMaxSeqReq {
|
||
string userID = 1;
|
||
}
|
||
|
||
message GetMaxSeqResp {
|
||
map<string, int64> maxSeqs = 1;
|
||
map<string, int64> minSeqs = 2;
|
||
}
|
||
|
||
message PullMsgs {
|
||
repeated MsgData Msgs = 1;
|
||
bool isEnd = 2;
|
||
int64 endSeq = 3;
|
||
}
|
||
|
||
message MsgData {
|
||
string sendID = 1;
|
||
string recvID = 2;
|
||
string groupID = 3;
|
||
string clientMsgID = 4;
|
||
string serverMsgID = 5;
|
||
int32 senderPlatformID = 6;
|
||
string senderNickname = 7;
|
||
string senderFaceURL = 8;
|
||
int32 sessionType = 9;
|
||
int32 msgFrom = 10;
|
||
int32 contentType = 11;
|
||
bytes content = 12;
|
||
int64 seq = 14;
|
||
int64 sendTime = 15;
|
||
int64 createTime = 16;
|
||
int32 status = 17;
|
||
bool isRead = 18;
|
||
map<string, bool> options = 19;
|
||
string attachedInfo = 22;
|
||
string ex = 23;
|
||
}
|
||
|
||
message PushMessages {
|
||
map<string, PullMsgs> msgs = 1;
|
||
map<string, PullMsgs> notificationMsgs = 2;
|
||
}
|
||
`;
|
||
|
||
const MSG_PROTO = `
|
||
syntax = "proto3";
|
||
package openim.msg;
|
||
|
||
message SendMsgResp {
|
||
string serverMsgID = 1;
|
||
string clientMsgID = 2;
|
||
int64 sendTime = 3;
|
||
}
|
||
`;
|
||
|
||
function ensureOperationID() {
|
||
if (!els.operationID.value.trim()) {
|
||
els.operationID.value = `ws-${Date.now()}`;
|
||
}
|
||
return els.operationID.value.trim();
|
||
}
|
||
|
||
function setStatus(message, type = "ok") {
|
||
els.status.className = `status ${type}`;
|
||
els.status.innerHTML = `<pre>${message}</pre>`;
|
||
}
|
||
|
||
function logLine(type, title, payload) {
|
||
const line = document.createElement("div");
|
||
line.className = `line ${type}`;
|
||
const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
||
line.textContent = `${title}\n${text}`;
|
||
els.console.prepend(line);
|
||
}
|
||
|
||
function setConnectionMeta(text) {
|
||
els.connectionMeta.textContent = text;
|
||
}
|
||
|
||
function nextMsgIncr() {
|
||
state.msgIncr += 1;
|
||
els.reqMeta.textContent = `msgIncr=${state.msgIncr}`;
|
||
return String(state.msgIncr);
|
||
}
|
||
|
||
async function postJSON(path, body, token = "") {
|
||
const operationID = ensureOperationID();
|
||
const url = `${els.apiBase.value.replace(/\/$/, "")}${path}`;
|
||
const headers = {
|
||
"Content-Type": "application/json",
|
||
operationID,
|
||
...(token ? { token } : {}),
|
||
};
|
||
const response = await fetch(url, {
|
||
method: "POST",
|
||
headers,
|
||
body: JSON.stringify(body),
|
||
});
|
||
const data = await response.json();
|
||
if (!response.ok || data.errCode) {
|
||
throw new Error(data.errDlt || data.errMsg || `HTTP ${response.status}`);
|
||
}
|
||
return data.data;
|
||
}
|
||
|
||
async function prepareTokens() {
|
||
const admin = await postJSON("/auth/get_admin_token", {
|
||
secret: els.adminSecret.value.trim(),
|
||
userID: els.adminUserID.value.trim(),
|
||
});
|
||
state.adminToken = admin.token;
|
||
|
||
try {
|
||
await postJSON(
|
||
"/user/user_register",
|
||
{
|
||
users: [
|
||
{ userID: els.userID.value.trim(), nickname: els.userID.value.trim() },
|
||
{ userID: els.peerUserID.value.trim(), nickname: els.peerUserID.value.trim() },
|
||
],
|
||
},
|
||
state.adminToken
|
||
);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
if (!message.includes("registered already")) {
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
const user = await postJSON(
|
||
"/auth/get_user_token",
|
||
{
|
||
userID: els.userID.value.trim(),
|
||
platformID: Number(els.platformID.value),
|
||
},
|
||
state.adminToken
|
||
);
|
||
state.userToken = user.token;
|
||
}
|
||
|
||
function initProto() {
|
||
if (state.proto) return state.proto;
|
||
const root = new protobuf.Root();
|
||
protobuf.parse(SDKWS_PROTO, root);
|
||
protobuf.parse(MSG_PROTO, root);
|
||
state.proto = {
|
||
root,
|
||
GetMaxSeqReq: root.lookupType("openim.sdkws.GetMaxSeqReq"),
|
||
GetMaxSeqResp: root.lookupType("openim.sdkws.GetMaxSeqResp"),
|
||
MsgData: root.lookupType("openim.sdkws.MsgData"),
|
||
PushMessages: root.lookupType("openim.sdkws.PushMessages"),
|
||
SendMsgResp: root.lookupType("openim.msg.SendMsgResp"),
|
||
};
|
||
return state.proto;
|
||
}
|
||
|
||
function buildWSURL() {
|
||
const url = new URL(els.wsBase.value);
|
||
url.searchParams.set("sendID", els.userID.value.trim());
|
||
url.searchParams.set("token", state.userToken);
|
||
url.searchParams.set("operationID", ensureOperationID());
|
||
url.searchParams.set("platformID", els.platformID.value.trim());
|
||
url.searchParams.set("sdkType", els.sdkType.value.trim());
|
||
url.searchParams.set("isBackground", "false");
|
||
url.searchParams.set("isMsgResp", "true");
|
||
return url.toString();
|
||
}
|
||
|
||
function base64FromBytes(bytes) {
|
||
let binary = "";
|
||
for (const b of bytes) binary += String.fromCharCode(b);
|
||
return btoa(binary);
|
||
}
|
||
|
||
function bytesFromBase64(base64) {
|
||
const binary = atob(base64);
|
||
const bytes = new Uint8Array(binary.length);
|
||
for (let i = 0; i < binary.length; i += 1) {
|
||
bytes[i] = binary.charCodeAt(i);
|
||
}
|
||
return bytes;
|
||
}
|
||
|
||
function sendEnvelope(reqIdentifier, payloadBytes) {
|
||
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) {
|
||
throw new Error("websocket is not connected");
|
||
}
|
||
const envelope = {
|
||
reqIdentifier,
|
||
token: state.userToken,
|
||
sendID: els.userID.value.trim(),
|
||
operationID: ensureOperationID(),
|
||
msgIncr: nextMsgIncr(),
|
||
data: base64FromBytes(payloadBytes),
|
||
};
|
||
els.rawEnvelope.textContent = JSON.stringify(envelope, null, 2);
|
||
logLine("out", `WS Send ${reqIdentifier}`, envelope);
|
||
const jsonBytes = new TextEncoder().encode(JSON.stringify(envelope));
|
||
state.ws.send(jsonBytes);
|
||
}
|
||
|
||
async function connectWS() {
|
||
if (!state.userToken) {
|
||
throw new Error("user token is missing, click '拿 Token' first");
|
||
}
|
||
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
||
throw new Error("websocket already connected");
|
||
}
|
||
|
||
const url = buildWSURL();
|
||
els.lastURL.textContent = url;
|
||
|
||
const ws = new WebSocket(url);
|
||
ws.binaryType = "arraybuffer";
|
||
|
||
ws.onopen = () => {
|
||
setConnectionMeta(`OPEN as ${els.userID.value.trim()}`);
|
||
setStatus("WebSocket connected.", "ok");
|
||
logLine("sys", "WS Open", url);
|
||
};
|
||
|
||
ws.onclose = (event) => {
|
||
setConnectionMeta(`CLOSED code=${event.code}`);
|
||
logLine("sys", "WS Close", { code: event.code, reason: event.reason });
|
||
};
|
||
|
||
ws.onerror = () => {
|
||
setStatus("WebSocket error.", "err");
|
||
logLine("err", "WS Error", "See browser console/network panel for handshake details.");
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
if (typeof event.data === "string") {
|
||
logLine("in", "WS Text", event.data);
|
||
return;
|
||
}
|
||
const text = new TextDecoder().decode(new Uint8Array(event.data));
|
||
let envelope;
|
||
try {
|
||
envelope = JSON.parse(text);
|
||
} catch {
|
||
logLine("err", "WS Binary Parse Error", text);
|
||
return;
|
||
}
|
||
logLine("in", `WS Recv ${envelope.reqIdentifier}`, envelope);
|
||
decodeEnvelope(envelope);
|
||
};
|
||
|
||
state.ws = ws;
|
||
}
|
||
|
||
function disconnectWS() {
|
||
if (state.ws) {
|
||
state.ws.close();
|
||
state.ws = null;
|
||
}
|
||
}
|
||
|
||
function decodeEnvelope(envelope) {
|
||
const proto = initProto();
|
||
if (!envelope.data) return;
|
||
const bytes = bytesFromBase64(envelope.data);
|
||
try {
|
||
switch (envelope.reqIdentifier) {
|
||
case 1001: {
|
||
const decoded = proto.GetMaxSeqResp.decode(bytes);
|
||
logLine("sys", "Decoded GetMaxSeqResp", proto.GetMaxSeqResp.toObject(decoded, { longs: String }));
|
||
break;
|
||
}
|
||
case 1003: {
|
||
const decoded = proto.SendMsgResp.decode(bytes);
|
||
logLine("sys", "Decoded SendMsgResp", proto.SendMsgResp.toObject(decoded, { longs: String }));
|
||
break;
|
||
}
|
||
case 2001: {
|
||
const decoded = proto.PushMessages.decode(bytes);
|
||
logLine("sys", "Decoded PushMessages", proto.PushMessages.toObject(decoded, { longs: String, bytes: String }));
|
||
break;
|
||
}
|
||
default:
|
||
logLine("sys", "Unknown Proto Payload", { reqIdentifier: envelope.reqIdentifier, base64: envelope.data });
|
||
}
|
||
} catch (error) {
|
||
logLine("err", "Proto Decode Error", error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
function sendGetNewestSeq() {
|
||
const proto = initProto();
|
||
const message = proto.GetMaxSeqReq.create({
|
||
userID: els.userID.value.trim(),
|
||
});
|
||
const bytes = proto.GetMaxSeqReq.encode(message).finish();
|
||
sendEnvelope(1001, bytes);
|
||
}
|
||
|
||
function makeClientMsgID() {
|
||
return `browser-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
||
}
|
||
|
||
function sendTextMessage() {
|
||
const proto = initProto();
|
||
const contentBytes = new TextEncoder().encode(
|
||
JSON.stringify({
|
||
content: els.textContent.value,
|
||
})
|
||
);
|
||
const now = Date.now();
|
||
const message = proto.MsgData.create({
|
||
sendID: els.userID.value.trim(),
|
||
recvID: els.peerUserID.value.trim(),
|
||
clientMsgID: makeClientMsgID(),
|
||
senderPlatformID: Number(els.platformID.value),
|
||
sessionType: 1,
|
||
msgFrom: 100,
|
||
contentType: 101,
|
||
content: contentBytes,
|
||
sendTime: now,
|
||
createTime: now,
|
||
});
|
||
const bytes = proto.MsgData.encode(message).finish();
|
||
sendEnvelope(1003, bytes);
|
||
}
|
||
|
||
async function run(button, fn, okText) {
|
||
try {
|
||
setBusy(button, true);
|
||
setStatus("Running...", "ok");
|
||
await fn();
|
||
if (okText) {
|
||
setStatus(okText, "ok");
|
||
}
|
||
} catch (error) {
|
||
setStatus(error instanceof Error ? error.message : String(error), "err");
|
||
logLine("err", "Action Error", error instanceof Error ? error.message : String(error));
|
||
} finally {
|
||
setBusy(button, false);
|
||
}
|
||
}
|
||
|
||
function setBusy(button, busy) {
|
||
button.disabled = busy;
|
||
}
|
||
|
||
ensureOperationID();
|
||
initProto();
|
||
|
||
els.prepareBtn.addEventListener("click", () => run(els.prepareBtn, prepareTokens, "Tokens prepared."));
|
||
els.connectBtn.addEventListener("click", () => run(els.connectBtn, connectWS, ""));
|
||
els.disconnectBtn.addEventListener("click", () => run(els.disconnectBtn, async () => disconnectWS(), "Disconnected."));
|
||
els.getSeqBtn.addEventListener("click", () => run(els.getSeqBtn, async () => sendGetNewestSeq(), "WSGetNewestSeq sent."));
|
||
els.sendMsgBtn.addEventListener("click", () => run(els.sendMsgBtn, async () => sendTextMessage(), "WSSendMsg sent."));
|
||
</script>
|
||
</body>
|
||
</html>
|