clodds 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/public/webchat/css/app.css +1807 -0
- package/public/webchat/index.html +310 -0
- package/public/webchat/js/app.js +750 -0
- package/public/webchat/js/chat.js +467 -0
- package/public/webchat/js/commands.js +356 -0
- package/public/webchat/js/sidebar.js +1012 -0
- package/public/webchat/js/storage.js +20 -0
- package/public/webchat/js/ws.js +165 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage wrapper for webchat state
|
|
3
|
+
*/
|
|
4
|
+
export const Storage = {
|
|
5
|
+
get(key) {
|
|
6
|
+
try { return localStorage.getItem(key); } catch { return null; }
|
|
7
|
+
},
|
|
8
|
+
set(key, val) {
|
|
9
|
+
try { localStorage.setItem(key, val); } catch { /* ignore */ }
|
|
10
|
+
},
|
|
11
|
+
remove(key) {
|
|
12
|
+
try { localStorage.removeItem(key); } catch { /* ignore */ }
|
|
13
|
+
},
|
|
14
|
+
getJSON(key) {
|
|
15
|
+
try { return JSON.parse(localStorage.getItem(key)); } catch { return null; }
|
|
16
|
+
},
|
|
17
|
+
setJSON(key, val) {
|
|
18
|
+
try { localStorage.setItem(key, JSON.stringify(val)); } catch { /* ignore */ }
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client with auto-reconnect and clean teardown
|
|
3
|
+
*/
|
|
4
|
+
export class WSClient {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ws = null;
|
|
7
|
+
this.handlers = { message: [], open: [], close: [], error: [] };
|
|
8
|
+
this.reconnectDelay = 1000;
|
|
9
|
+
this.maxReconnectDelay = 5000;
|
|
10
|
+
this.currentDelay = this.reconnectDelay;
|
|
11
|
+
this.shouldReconnect = true;
|
|
12
|
+
this.sessionId = null;
|
|
13
|
+
this.authenticated = false;
|
|
14
|
+
this._reconnectTimer = null;
|
|
15
|
+
this._pingTimer = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
connect(token, userId, sessionId) {
|
|
19
|
+
this.shouldReconnect = true;
|
|
20
|
+
this.sessionId = sessionId || null;
|
|
21
|
+
this._token = token;
|
|
22
|
+
this._userId = userId;
|
|
23
|
+
this._teardown();
|
|
24
|
+
this._doConnect();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_teardown() {
|
|
28
|
+
if (this._reconnectTimer) {
|
|
29
|
+
clearTimeout(this._reconnectTimer);
|
|
30
|
+
this._reconnectTimer = null;
|
|
31
|
+
}
|
|
32
|
+
if (this._pingTimer) {
|
|
33
|
+
clearInterval(this._pingTimer);
|
|
34
|
+
this._pingTimer = null;
|
|
35
|
+
}
|
|
36
|
+
if (this.ws) {
|
|
37
|
+
this.ws.onopen = null;
|
|
38
|
+
this.ws.onmessage = null;
|
|
39
|
+
this.ws.onclose = null;
|
|
40
|
+
this.ws.onerror = null;
|
|
41
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
42
|
+
this.ws.close();
|
|
43
|
+
}
|
|
44
|
+
this.ws = null;
|
|
45
|
+
}
|
|
46
|
+
this.authenticated = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_doConnect() {
|
|
50
|
+
// Clean up any leftover socket
|
|
51
|
+
if (this.ws) {
|
|
52
|
+
this.ws.onopen = null;
|
|
53
|
+
this.ws.onmessage = null;
|
|
54
|
+
this.ws.onclose = null;
|
|
55
|
+
this.ws.onerror = null;
|
|
56
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
57
|
+
this.ws.close();
|
|
58
|
+
}
|
|
59
|
+
this.ws = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
63
|
+
// Never include sessionId in URL — avoids server-side eviction loops.
|
|
64
|
+
// We send a 'switch' message after auth to set the desired session.
|
|
65
|
+
const url = `${proto}//${location.host}/chat`;
|
|
66
|
+
|
|
67
|
+
const ws = new WebSocket(url);
|
|
68
|
+
this.ws = ws;
|
|
69
|
+
this.authenticated = false;
|
|
70
|
+
|
|
71
|
+
ws.onopen = () => {
|
|
72
|
+
if (this.ws !== ws) return;
|
|
73
|
+
ws.send(JSON.stringify({
|
|
74
|
+
type: 'auth',
|
|
75
|
+
token: this._token || '',
|
|
76
|
+
userId: this._userId || 'web-' + Date.now(),
|
|
77
|
+
_wsVersion: 4,
|
|
78
|
+
}));
|
|
79
|
+
this._emit('open');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
ws.onmessage = (e) => {
|
|
83
|
+
if (this.ws !== ws) return;
|
|
84
|
+
try {
|
|
85
|
+
const msg = JSON.parse(e.data);
|
|
86
|
+
if (msg.type === 'authenticated') {
|
|
87
|
+
this.authenticated = true;
|
|
88
|
+
this.currentDelay = this.reconnectDelay;
|
|
89
|
+
clearInterval(this._pingTimer);
|
|
90
|
+
this._pingTimer = setInterval(() => {
|
|
91
|
+
if (this.ws === ws && ws.readyState === WebSocket.OPEN) {
|
|
92
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
93
|
+
} else {
|
|
94
|
+
clearInterval(this._pingTimer);
|
|
95
|
+
this._pingTimer = null;
|
|
96
|
+
}
|
|
97
|
+
}, 25000);
|
|
98
|
+
// After auth, switch to desired session (if any)
|
|
99
|
+
if (this.sessionId && ws.readyState === WebSocket.OPEN) {
|
|
100
|
+
ws.send(JSON.stringify({ type: 'switch', sessionId: this.sessionId }));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (msg.type === 'pong') return;
|
|
104
|
+
this._emit('message', msg);
|
|
105
|
+
} catch { /* ignore malformed */ }
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
ws.onclose = () => {
|
|
109
|
+
if (this.ws !== ws) return;
|
|
110
|
+
this.authenticated = false;
|
|
111
|
+
clearInterval(this._pingTimer);
|
|
112
|
+
this._pingTimer = null;
|
|
113
|
+
this._emit('close');
|
|
114
|
+
if (this.shouldReconnect) {
|
|
115
|
+
this._reconnectTimer = setTimeout(() => this._doConnect(), this.currentDelay);
|
|
116
|
+
this.currentDelay = Math.min(this.currentDelay * 1.5, this.maxReconnectDelay);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
ws.onerror = () => {
|
|
121
|
+
if (this.ws !== ws) return;
|
|
122
|
+
this._emit('error');
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
send(text, attachments) {
|
|
127
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
128
|
+
const msg = { type: 'message', text };
|
|
129
|
+
if (attachments?.length) msg.attachments = attachments;
|
|
130
|
+
this.ws.send(JSON.stringify(msg));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
switchSession(sessionId) {
|
|
135
|
+
this.sessionId = sessionId;
|
|
136
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
137
|
+
this.ws.send(JSON.stringify({ type: 'switch', sessionId }));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
disconnect() {
|
|
142
|
+
this.shouldReconnect = false;
|
|
143
|
+
this._teardown();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
on(event, fn) {
|
|
147
|
+
if (this.handlers[event]) this.handlers[event].push(fn);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
off(event, fn) {
|
|
151
|
+
if (this.handlers[event]) {
|
|
152
|
+
this.handlers[event] = this.handlers[event].filter(f => f !== fn);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_emit(event, data) {
|
|
157
|
+
for (const fn of (this.handlers[event] || [])) {
|
|
158
|
+
try { fn(data); } catch { /* ignore */ }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get connected() {
|
|
163
|
+
return this.ws?.readyState === WebSocket.OPEN && this.authenticated;
|
|
164
|
+
}
|
|
165
|
+
}
|