llmapi-v2 2.1.0
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/.env.example +40 -0
- package/Dockerfile +17 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/converter/request.d.ts +6 -0
- package/dist/converter/request.js +184 -0
- package/dist/converter/request.js.map +1 -0
- package/dist/converter/response.d.ts +6 -0
- package/dist/converter/response.js +76 -0
- package/dist/converter/response.js.map +1 -0
- package/dist/converter/stream.d.ts +54 -0
- package/dist/converter/stream.js +318 -0
- package/dist/converter/stream.js.map +1 -0
- package/dist/converter/types.d.ts +239 -0
- package/dist/converter/types.js +6 -0
- package/dist/converter/types.js.map +1 -0
- package/dist/data/posts.d.ts +19 -0
- package/dist/data/posts.js +462 -0
- package/dist/data/posts.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/api-key-auth.d.ts +6 -0
- package/dist/middleware/api-key-auth.js +76 -0
- package/dist/middleware/api-key-auth.js.map +1 -0
- package/dist/middleware/quota-guard.d.ts +10 -0
- package/dist/middleware/quota-guard.js +27 -0
- package/dist/middleware/quota-guard.js.map +1 -0
- package/dist/middleware/rate-limiter.d.ts +5 -0
- package/dist/middleware/rate-limiter.js +50 -0
- package/dist/middleware/rate-limiter.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +6 -0
- package/dist/middleware/request-logger.js +37 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/middleware/session-auth.d.ts +19 -0
- package/dist/middleware/session-auth.js +99 -0
- package/dist/middleware/session-auth.js.map +1 -0
- package/dist/providers/aliyun.d.ts +13 -0
- package/dist/providers/aliyun.js +20 -0
- package/dist/providers/aliyun.js.map +1 -0
- package/dist/providers/base-provider.d.ts +36 -0
- package/dist/providers/base-provider.js +133 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/deepseek.d.ts +11 -0
- package/dist/providers/deepseek.js +18 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/registry.d.ts +18 -0
- package/dist/providers/registry.js +98 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +17 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/routes/admin.d.ts +1 -0
- package/dist/routes/admin.js +153 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.js +318 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/blog.d.ts +1 -0
- package/dist/routes/blog.js +29 -0
- package/dist/routes/blog.js.map +1 -0
- package/dist/routes/dashboard.d.ts +1 -0
- package/dist/routes/dashboard.js +184 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/messages.d.ts +1 -0
- package/dist/routes/messages.js +309 -0
- package/dist/routes/messages.js.map +1 -0
- package/dist/routes/models.d.ts +1 -0
- package/dist/routes/models.js +39 -0
- package/dist/routes/models.js.map +1 -0
- package/dist/routes/payment.d.ts +1 -0
- package/dist/routes/payment.js +150 -0
- package/dist/routes/payment.js.map +1 -0
- package/dist/routes/sitemap.d.ts +1 -0
- package/dist/routes/sitemap.js +38 -0
- package/dist/routes/sitemap.js.map +1 -0
- package/dist/services/alipay.d.ts +27 -0
- package/dist/services/alipay.js +106 -0
- package/dist/services/alipay.js.map +1 -0
- package/dist/services/database.d.ts +4 -0
- package/dist/services/database.js +170 -0
- package/dist/services/database.js.map +1 -0
- package/dist/services/health-checker.d.ts +13 -0
- package/dist/services/health-checker.js +95 -0
- package/dist/services/health-checker.js.map +1 -0
- package/dist/services/mailer.d.ts +3 -0
- package/dist/services/mailer.js +91 -0
- package/dist/services/mailer.js.map +1 -0
- package/dist/services/metrics.d.ts +56 -0
- package/dist/services/metrics.js +94 -0
- package/dist/services/metrics.js.map +1 -0
- package/dist/services/remote-control.d.ts +20 -0
- package/dist/services/remote-control.js +209 -0
- package/dist/services/remote-control.js.map +1 -0
- package/dist/services/remote-ws.d.ts +5 -0
- package/dist/services/remote-ws.js +143 -0
- package/dist/services/remote-ws.js.map +1 -0
- package/dist/services/usage.d.ts +13 -0
- package/dist/services/usage.js +39 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.js +48 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +14 -0
- package/dist/utils/logger.js.map +1 -0
- package/docker-compose.yml +19 -0
- package/package.json +39 -0
- package/public/robots.txt +8 -0
- package/src/config.ts +140 -0
- package/src/converter/request.ts +207 -0
- package/src/converter/response.ts +85 -0
- package/src/converter/stream.ts +373 -0
- package/src/converter/types.ts +257 -0
- package/src/data/posts.ts +474 -0
- package/src/index.ts +219 -0
- package/src/middleware/api-key-auth.ts +82 -0
- package/src/middleware/quota-guard.ts +28 -0
- package/src/middleware/rate-limiter.ts +61 -0
- package/src/middleware/request-logger.ts +36 -0
- package/src/middleware/session-auth.ts +91 -0
- package/src/providers/aliyun.ts +16 -0
- package/src/providers/base-provider.ts +148 -0
- package/src/providers/deepseek.ts +14 -0
- package/src/providers/registry.ts +111 -0
- package/src/providers/types.ts +26 -0
- package/src/routes/admin.ts +169 -0
- package/src/routes/auth.ts +369 -0
- package/src/routes/blog.ts +28 -0
- package/src/routes/dashboard.ts +208 -0
- package/src/routes/messages.ts +346 -0
- package/src/routes/models.ts +37 -0
- package/src/routes/payment.ts +189 -0
- package/src/routes/sitemap.ts +40 -0
- package/src/services/alipay.ts +116 -0
- package/src/services/database.ts +187 -0
- package/src/services/health-checker.ts +115 -0
- package/src/services/mailer.ts +90 -0
- package/src/services/metrics.ts +104 -0
- package/src/services/remote-control.ts +226 -0
- package/src/services/remote-ws.ts +145 -0
- package/src/services/usage.ts +57 -0
- package/src/types/express.d.ts +46 -0
- package/src/utils/errors.ts +44 -0
- package/src/utils/logger.ts +8 -0
- package/tsconfig.json +17 -0
- package/views/pages/404.ejs +14 -0
- package/views/pages/admin.ejs +307 -0
- package/views/pages/blog-post.ejs +378 -0
- package/views/pages/blog.ejs +148 -0
- package/views/pages/dashboard.ejs +441 -0
- package/views/pages/docs.ejs +807 -0
- package/views/pages/index.ejs +416 -0
- package/views/pages/login.ejs +170 -0
- package/views/pages/orders.ejs +111 -0
- package/views/pages/pricing.ejs +379 -0
- package/views/pages/register.ejs +397 -0
- package/views/pages/remote.ejs +334 -0
- package/views/pages/settings.ejs +373 -0
- package/views/partials/header.ejs +70 -0
- package/views/partials/nav.ejs +140 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<%- include('../partials/header', { pageTitle: 'Remote Terminal' }) %>
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
html, body { height: 100%; margin: 0; }
|
|
5
|
+
body.terminal-active { overflow: hidden; }
|
|
6
|
+
body.terminal-active nav { display: none; }
|
|
7
|
+
|
|
8
|
+
.status-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
|
9
|
+
.status-dot.ready { background: #22c55e; box-shadow: 0 0 6px #22c55e80; }
|
|
10
|
+
.status-dot.error { background: #ef4444; box-shadow: 0 0 6px #ef444480; }
|
|
11
|
+
.status-dot.connecting { background: #eab308; box-shadow: 0 0 6px #eab30880; }
|
|
12
|
+
.status-dot.waiting { background: #eab308; box-shadow: 0 0 6px #eab30880; animation: pulse-dot 1.5s infinite; }
|
|
13
|
+
@keyframes pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
14
|
+
|
|
15
|
+
#terminal-container {
|
|
16
|
+
flex: 1;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
background: #1A1915;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#terminal-container .xterm {
|
|
22
|
+
height: 100%;
|
|
23
|
+
padding: 4px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.top-bar {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
padding: 6px 12px;
|
|
31
|
+
background: #232019;
|
|
32
|
+
border-bottom: 1px solid #3a362e;
|
|
33
|
+
min-height: 40px;
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
}
|
|
36
|
+
.top-bar .left { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
37
|
+
.top-bar .title { color: #e8e4de; font-size: 13px; font-weight: 600; }
|
|
38
|
+
.top-bar .session-id { color: #8a8578; font-size: 11px; font-family: 'JetBrains Mono', monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; }
|
|
39
|
+
.top-bar .right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
40
|
+
.top-bar .status-label { color: #8a8578; font-size: 11px; }
|
|
41
|
+
.top-bar .disconnect-btn {
|
|
42
|
+
font-size: 11px; padding: 4px 10px; border-radius: 6px;
|
|
43
|
+
background: #3a362e; color: #e8e4de; border: none; cursor: pointer;
|
|
44
|
+
font-weight: 500; transition: background 0.15s;
|
|
45
|
+
}
|
|
46
|
+
.top-bar .disconnect-btn:hover { background: #4a4639; }
|
|
47
|
+
</style>
|
|
48
|
+
|
|
49
|
+
<%- include('../partials/nav') %>
|
|
50
|
+
|
|
51
|
+
<!-- ==================== SESSION INPUT VIEW ==================== -->
|
|
52
|
+
<div id="session-select-view" class="hidden">
|
|
53
|
+
<div class="min-h-[calc(100vh-64px)] flex items-center justify-center px-4">
|
|
54
|
+
<div class="w-full max-w-md">
|
|
55
|
+
<div class="bg-white rounded-2xl shadow-lg border border-gray-100 p-8">
|
|
56
|
+
<div class="flex justify-center mb-6">
|
|
57
|
+
<div class="w-14 h-14 rounded-xl bg-claude-orange/10 flex items-center justify-center">
|
|
58
|
+
<svg class="w-7 h-7 text-claude-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
59
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
60
|
+
</svg>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<h1 class="text-xl font-bold text-claude-dark text-center mb-2">Remote Terminal</h1>
|
|
65
|
+
<p class="text-sm text-gray-500 text-center mb-6">Enter a session ID to connect to your Claude Code terminal</p>
|
|
66
|
+
|
|
67
|
+
<form id="session-form" onsubmit="connectToSession(event)">
|
|
68
|
+
<div class="mb-4">
|
|
69
|
+
<label for="session-id-input" class="block text-sm font-medium text-gray-700 mb-1.5">Session ID</label>
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
id="session-id-input"
|
|
73
|
+
placeholder="e.g. abc123-def456"
|
|
74
|
+
autocomplete="off"
|
|
75
|
+
spellcheck="false"
|
|
76
|
+
class="w-full px-4 py-3 rounded-xl border border-gray-200 text-sm focus:outline-none focus:ring-2 focus:ring-claude-orange/30 focus:border-claude-orange transition-all font-mono"
|
|
77
|
+
>
|
|
78
|
+
</div>
|
|
79
|
+
<button
|
|
80
|
+
type="submit"
|
|
81
|
+
class="w-full py-3 px-4 bg-claude-orange text-white text-sm font-medium rounded-xl hover:bg-claude-orange-hover transition-all shadow-sm hover:shadow"
|
|
82
|
+
>Connect</button>
|
|
83
|
+
</form>
|
|
84
|
+
|
|
85
|
+
<p class="text-xs text-gray-400 text-center mt-5">
|
|
86
|
+
Get a session ID by typing <code class="text-claude-orange font-mono bg-claude-orange/5 px-1.5 py-0.5 rounded">remote control</code> in Claude Code
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<p class="text-center text-xs text-gray-400 mt-4">
|
|
91
|
+
Need help? See the <a href="/docs" class="text-claude-orange hover:underline">documentation</a>
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- ==================== TERMINAL VIEW ==================== -->
|
|
98
|
+
<div id="terminal-view" class="hidden" style="display: none; flex-direction: column; height: 100vh; height: 100dvh;">
|
|
99
|
+
<div class="top-bar">
|
|
100
|
+
<div class="left">
|
|
101
|
+
<span id="status-dot" class="status-dot connecting"></span>
|
|
102
|
+
<span class="title">Remote Terminal</span>
|
|
103
|
+
<span id="session-id-display" class="session-id"></span>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="right">
|
|
106
|
+
<span id="status-text" class="status-label">Connecting...</span>
|
|
107
|
+
<button class="disconnect-btn" onclick="disconnectSession()">Disconnect</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div id="terminal-container"></div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<script>
|
|
114
|
+
// Session selection handler (global scope)
|
|
115
|
+
function connectToSession(e) {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
var id = document.getElementById('session-id-input').value.trim();
|
|
118
|
+
if (!id) return;
|
|
119
|
+
window.location.href = '/remote?s=' + encodeURIComponent(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function disconnectSession() {
|
|
123
|
+
window.location.href = '/remote';
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<script>
|
|
128
|
+
(function() {
|
|
129
|
+
var sessionId = new URLSearchParams(window.location.search).get('s');
|
|
130
|
+
|
|
131
|
+
if (!sessionId) {
|
|
132
|
+
document.getElementById('session-select-view').classList.remove('hidden');
|
|
133
|
+
setTimeout(function() {
|
|
134
|
+
var inp = document.getElementById('session-id-input');
|
|
135
|
+
if (inp) inp.focus();
|
|
136
|
+
}, 100);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Show terminal view
|
|
141
|
+
var termView = document.getElementById('terminal-view');
|
|
142
|
+
termView.classList.remove('hidden');
|
|
143
|
+
termView.style.display = 'flex';
|
|
144
|
+
document.body.classList.add('terminal-active');
|
|
145
|
+
|
|
146
|
+
// Show session ID
|
|
147
|
+
var display = document.getElementById('session-id-display');
|
|
148
|
+
display.textContent = sessionId.length > 24 ? sessionId.slice(0, 8) + '...' + sessionId.slice(-8) : sessionId;
|
|
149
|
+
display.title = sessionId;
|
|
150
|
+
|
|
151
|
+
// Load xterm.js from CDN
|
|
152
|
+
var cssLink = document.createElement('link');
|
|
153
|
+
cssLink.rel = 'stylesheet';
|
|
154
|
+
cssLink.href = 'https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css';
|
|
155
|
+
document.head.appendChild(cssLink);
|
|
156
|
+
|
|
157
|
+
function loadScript(src, cb) {
|
|
158
|
+
var s = document.createElement('script');
|
|
159
|
+
s.src = src;
|
|
160
|
+
s.onload = cb;
|
|
161
|
+
document.head.appendChild(s);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
loadScript('https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js', function() {
|
|
165
|
+
loadScript('https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js', function() {
|
|
166
|
+
initTerminal();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
var term, fitAddon, ws;
|
|
171
|
+
var reconnectAttempts = 0;
|
|
172
|
+
var maxReconnects = 5;
|
|
173
|
+
var reconnectInterval = 3000;
|
|
174
|
+
var reconnectTimer = null;
|
|
175
|
+
var bridgeConnected = false;
|
|
176
|
+
|
|
177
|
+
function setStatus(state, text) {
|
|
178
|
+
var dot = document.getElementById('status-dot');
|
|
179
|
+
var label = document.getElementById('status-text');
|
|
180
|
+
dot.className = 'status-dot ' + state;
|
|
181
|
+
label.textContent = text;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function initTerminal() {
|
|
185
|
+
term = new Terminal({
|
|
186
|
+
cursorBlink: true,
|
|
187
|
+
fontSize: 14,
|
|
188
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace",
|
|
189
|
+
theme: {
|
|
190
|
+
background: '#1A1915',
|
|
191
|
+
foreground: '#e8e4de',
|
|
192
|
+
cursor: '#D97757',
|
|
193
|
+
cursorAccent: '#1A1915',
|
|
194
|
+
selectionBackground: '#D9775740',
|
|
195
|
+
black: '#1A1915',
|
|
196
|
+
red: '#ef4444',
|
|
197
|
+
green: '#22c55e',
|
|
198
|
+
yellow: '#eab308',
|
|
199
|
+
blue: '#3b82f6',
|
|
200
|
+
magenta: '#a855f7',
|
|
201
|
+
cyan: '#06b6d4',
|
|
202
|
+
white: '#e8e4de',
|
|
203
|
+
brightBlack: '#6b7280',
|
|
204
|
+
brightRed: '#f87171',
|
|
205
|
+
brightGreen: '#4ade80',
|
|
206
|
+
brightYellow: '#facc15',
|
|
207
|
+
brightBlue: '#60a5fa',
|
|
208
|
+
brightMagenta: '#c084fc',
|
|
209
|
+
brightCyan: '#22d3ee',
|
|
210
|
+
brightWhite: '#ffffff'
|
|
211
|
+
},
|
|
212
|
+
allowProposedApi: true,
|
|
213
|
+
scrollback: 5000
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
fitAddon = new FitAddon.FitAddon();
|
|
217
|
+
term.loadAddon(fitAddon);
|
|
218
|
+
|
|
219
|
+
var container = document.getElementById('terminal-container');
|
|
220
|
+
term.open(container);
|
|
221
|
+
fitAddon.fit();
|
|
222
|
+
|
|
223
|
+
window.addEventListener('resize', function() {
|
|
224
|
+
if (fitAddon) fitAddon.fit();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Resize observer for container
|
|
228
|
+
if (window.ResizeObserver) {
|
|
229
|
+
new ResizeObserver(function() {
|
|
230
|
+
if (fitAddon) fitAddon.fit();
|
|
231
|
+
}).observe(container);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
term.write('\x1b[1;33m Connecting to remote session...\x1b[0m\r\n');
|
|
235
|
+
connectWebSocket();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function connectWebSocket() {
|
|
239
|
+
var proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
240
|
+
var wsUrl = proto + '//' + window.location.host + '/remote-ws?session=' + encodeURIComponent(sessionId) + '&role=browser';
|
|
241
|
+
|
|
242
|
+
setStatus('connecting', 'Connecting...');
|
|
243
|
+
ws = new WebSocket(wsUrl);
|
|
244
|
+
|
|
245
|
+
ws.onopen = function() {
|
|
246
|
+
reconnectAttempts = 0;
|
|
247
|
+
if (bridgeConnected) {
|
|
248
|
+
setStatus('ready', 'Connected');
|
|
249
|
+
term.write('\x1b[1;32m Reconnected!\x1b[0m\r\n');
|
|
250
|
+
} else {
|
|
251
|
+
setStatus('waiting', 'Waiting for bridge...');
|
|
252
|
+
term.write('\x1b[1;33m Waiting for bridge daemon to connect...\x1b[0m\r\n');
|
|
253
|
+
term.write('\x1b[90m The bridge should start automatically from Claude Code.\x1b[0m\r\n');
|
|
254
|
+
term.write('\x1b[90m If it doesn\'t, run: llmapi-remote start --session ' + sessionId.slice(0, 8) + '...\x1b[0m\r\n\r\n');
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
ws.onmessage = function(evt) {
|
|
259
|
+
try {
|
|
260
|
+
var msg = JSON.parse(evt.data);
|
|
261
|
+
if (msg.type === 'output') {
|
|
262
|
+
term.write(atob(msg.data));
|
|
263
|
+
} else if (msg.type === 'bridge_connected') {
|
|
264
|
+
bridgeConnected = true;
|
|
265
|
+
setStatus('ready', 'Connected');
|
|
266
|
+
term.write('\r\n\x1b[1;32m Bridge connected! Starting Claude Code...\x1b[0m\r\n\r\n');
|
|
267
|
+
} else if (msg.type === 'bridge_disconnected') {
|
|
268
|
+
bridgeConnected = false;
|
|
269
|
+
setStatus('error', 'Bridge disconnected');
|
|
270
|
+
term.write('\r\n\x1b[1;31m Bridge disconnected.\x1b[0m\r\n');
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
// Non-JSON message: write raw
|
|
274
|
+
term.write(evt.data);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
ws.onclose = function(evt) {
|
|
279
|
+
if (evt.code === 4003) {
|
|
280
|
+
setStatus('error', 'Auth failed');
|
|
281
|
+
term.write('\r\n\x1b[1;31m Authentication failed. Session may have expired.\x1b[0m\r\n');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (evt.code === 4001) {
|
|
285
|
+
setStatus('error', 'Invalid params');
|
|
286
|
+
term.write('\r\n\x1b[1;31m Connection rejected: invalid parameters.\x1b[0m\r\n');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
bridgeConnected = false;
|
|
291
|
+
setStatus('error', 'Disconnected');
|
|
292
|
+
term.write('\r\n\x1b[1;31m Connection lost.\x1b[0m');
|
|
293
|
+
|
|
294
|
+
if (reconnectAttempts < maxReconnects) {
|
|
295
|
+
reconnectAttempts++;
|
|
296
|
+
term.write(' \x1b[33mReconnecting (' + reconnectAttempts + '/' + maxReconnects + ')...\x1b[0m\r\n');
|
|
297
|
+
reconnectTimer = setTimeout(function() {
|
|
298
|
+
connectWebSocket();
|
|
299
|
+
}, reconnectInterval);
|
|
300
|
+
} else {
|
|
301
|
+
term.write(' \x1b[31mMax reconnection attempts reached. Refresh to try again.\x1b[0m\r\n');
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
ws.onerror = function() {
|
|
306
|
+
// onclose will handle reconnection
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Terminal input -> WebSocket
|
|
310
|
+
term.onData(function(data) {
|
|
311
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
312
|
+
ws.send(JSON.stringify({ type: 'input', data: btoa(data) }));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Terminal resize -> WebSocket
|
|
317
|
+
term.onResize(function(size) {
|
|
318
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
319
|
+
ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Cleanup on page unload
|
|
325
|
+
window.addEventListener('beforeunload', function() {
|
|
326
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
327
|
+
maxReconnects = 0; // prevent reconnect
|
|
328
|
+
if (ws) ws.close();
|
|
329
|
+
});
|
|
330
|
+
})();
|
|
331
|
+
</script>
|
|
332
|
+
|
|
333
|
+
</body>
|
|
334
|
+
</html>
|