@feng3d/ctc 0.0.10 → 0.0.12
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/README.md +17 -32
- package/dist/admin-server.d.ts +65 -12
- package/dist/admin-server.d.ts.map +1 -1
- package/dist/admin-server.js +291 -564
- package/dist/admin-server.js.map +1 -1
- package/dist/cli.d.ts +12 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +931 -311
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -11
- package/dist/config.js.map +1 -1
- package/dist/controller.d.ts +27 -124
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +77 -139
- package/dist/controller.js.map +1 -1
- package/dist/data-channel.d.ts +76 -0
- package/dist/data-channel.d.ts.map +1 -0
- package/dist/data-channel.js +222 -0
- package/dist/data-channel.js.map +1 -0
- package/dist/forward-proxy.d.ts +100 -0
- package/dist/forward-proxy.d.ts.map +1 -0
- package/dist/forward-proxy.js +407 -0
- package/dist/forward-proxy.js.map +1 -0
- package/dist/handlers/unified-handler.d.ts +33 -75
- package/dist/handlers/unified-handler.d.ts.map +1 -1
- package/dist/handlers/unified-handler.js +191 -315
- package/dist/handlers/unified-handler.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +135 -36
- package/dist/index.js.map +1 -1
- package/dist/ipc-handler.d.ts +52 -0
- package/dist/ipc-handler.d.ts.map +1 -0
- package/dist/ipc-handler.js +110 -0
- package/dist/ipc-handler.js.map +1 -0
- package/dist/proxy-manager.d.ts +13 -6
- package/dist/proxy-manager.d.ts.map +1 -1
- package/dist/proxy-manager.js +58 -23
- package/dist/proxy-manager.js.map +1 -1
- package/package.json +9 -5
package/dist/admin-server.js
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* @module admin-server
|
|
3
3
|
* @description 客户端管理页面 HTTP 服务器模块。
|
|
4
4
|
* 提供一个本地 HTTP 服务,用于查看客户端状态和管理代理映射。
|
|
5
|
+
* 支持反向代理模式和正向穿透模式。
|
|
5
6
|
*/
|
|
6
7
|
import { createServer } from 'http';
|
|
8
|
+
import { readFile } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
7
11
|
/**
|
|
8
12
|
* 管理页面服务器类
|
|
9
13
|
*
|
|
@@ -16,18 +20,43 @@ export class AdminServer {
|
|
|
16
20
|
*
|
|
17
21
|
* @param config - 服务器配置
|
|
18
22
|
* @param getStatus - 获取状态的回调函数
|
|
19
|
-
* @param addProxy -
|
|
20
|
-
* @param removeProxy -
|
|
23
|
+
* @param addProxy - 添加反向代理的回调函数
|
|
24
|
+
* @param removeProxy - 删除反向代理的回调函数
|
|
25
|
+
* @param addForwardProxy - 添加正向穿透的回调函数
|
|
26
|
+
* @param removeForwardProxy - 删除正向穿透的回调函数
|
|
27
|
+
* @param registerClient - 注册客户端的回调函数
|
|
28
|
+
* @param getClientList - 获取客户端列表的回调函数
|
|
29
|
+
* @param reconnect - 触发重连的回调函数
|
|
21
30
|
*/
|
|
22
|
-
constructor(config, getStatus, addProxy, removeProxy) {
|
|
31
|
+
constructor(config, getStatus, addProxy, removeProxy, addForwardProxy, removeForwardProxy, registerClient, getClientList, sendMessage, reconnect) {
|
|
32
|
+
/** 正向穿透代理列表(用于存储运行时的正向穿透配置) */
|
|
33
|
+
this.forwardProxies = new Map();
|
|
23
34
|
this.port = config.port;
|
|
24
35
|
this.host = config.host;
|
|
25
36
|
this.startedAt = Date.now();
|
|
26
37
|
this.getStatusCallback = getStatus;
|
|
27
38
|
this.addProxyCallback = addProxy;
|
|
28
39
|
this.removeProxyCallback = removeProxy;
|
|
40
|
+
this.addForwardProxyCallback = addForwardProxy;
|
|
41
|
+
this.removeForwardProxyCallback = removeForwardProxy;
|
|
42
|
+
this.registerClientCallback = registerClient;
|
|
43
|
+
this.getClientListCallback = getClientList;
|
|
44
|
+
this.sendMessageCallback = sendMessage;
|
|
45
|
+
this.reconnectCallback = reconnect;
|
|
29
46
|
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
30
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 设置发送消息的回调
|
|
50
|
+
*/
|
|
51
|
+
setSendMessageCallback(callback) {
|
|
52
|
+
this.sendMessageCallback = callback;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 设置重连回调
|
|
56
|
+
*/
|
|
57
|
+
setReconnectCallback(callback) {
|
|
58
|
+
this.reconnectCallback = callback;
|
|
59
|
+
}
|
|
31
60
|
/**
|
|
32
61
|
* 启动服务器
|
|
33
62
|
*
|
|
@@ -49,17 +78,56 @@ export class AdminServer {
|
|
|
49
78
|
* 处理 HTTP 请求
|
|
50
79
|
*
|
|
51
80
|
* 提供以下端点:
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
81
|
+
* 反向代理:
|
|
82
|
+
* - `GET /` - 管理页面
|
|
83
|
+
* - `GET /_ctc/status` - 获取状态
|
|
84
|
+
* - `POST /_ctc/proxies` - 添加反向代理
|
|
85
|
+
* - `DELETE /_ctc/proxies/:port` - 删除反向代理
|
|
86
|
+
* - `POST /_ctc/test-proxy?port=xxx` - 测试反向代理连接
|
|
87
|
+
* 正向穿透:
|
|
88
|
+
* - `GET /_ctc/forward/list` - 获取正向穿透列表
|
|
89
|
+
* - `POST /_ctc/forward/add` - 添加正向穿透
|
|
90
|
+
* - `POST /_ctc/forward/remove` - 删除正向穿透
|
|
91
|
+
* - `GET /_ctc/forward/clients` - 获取客户端列表
|
|
92
|
+
* - `POST /_ctc/forward/register` - 注册到服务器
|
|
56
93
|
*/
|
|
57
|
-
handleRequest(req, res) {
|
|
94
|
+
async handleRequest(req, res) {
|
|
58
95
|
const url = req.url ?? '/';
|
|
59
|
-
//
|
|
96
|
+
// 静态文件服务 - 首页读取模板文件
|
|
60
97
|
if (url === '/' && req.method === 'GET') {
|
|
61
|
-
|
|
62
|
-
|
|
98
|
+
readFile(AdminServer.TEMPLATE_PATH, 'utf-8', (err, data) => {
|
|
99
|
+
if (err) {
|
|
100
|
+
console.error('模板文件读取错误:', err);
|
|
101
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
102
|
+
res.end('Error loading page');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
106
|
+
res.end(data);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// 处理静态文件请求 (支持 .js, .css 等静态资源直接从根路径访问)
|
|
112
|
+
if (req.method === 'GET' && url !== '/' && !url.startsWith('/_ctc/')) {
|
|
113
|
+
const fileName = url.slice(1); // 去掉开头的 /
|
|
114
|
+
const filePath = join(AdminServer.STATIC_DIR, fileName);
|
|
115
|
+
readFile(filePath, 'utf-8', (err, data) => {
|
|
116
|
+
if (err || !data) {
|
|
117
|
+
console.error('静态文件读取错误:', err);
|
|
118
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
119
|
+
res.end('File not found');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const ext = fileName.split('.').pop() || 'html';
|
|
123
|
+
const contentType = ext === 'css' ? 'text/css; charset=utf-8' :
|
|
124
|
+
ext === 'js' ? 'text/javascript; charset=utf-8' : 'text/html; charset=utf-8';
|
|
125
|
+
res.writeHead(200, {
|
|
126
|
+
'Content-Type': contentType,
|
|
127
|
+
'Cache-Control': 'public, max-age=3600'
|
|
128
|
+
});
|
|
129
|
+
res.end(data);
|
|
130
|
+
});
|
|
63
131
|
return;
|
|
64
132
|
}
|
|
65
133
|
// 状态 API
|
|
@@ -69,7 +137,8 @@ export class AdminServer {
|
|
|
69
137
|
res.end(JSON.stringify(status));
|
|
70
138
|
return;
|
|
71
139
|
}
|
|
72
|
-
//
|
|
140
|
+
// ==================== 反向代理 API ====================
|
|
141
|
+
// 添加反向代理 API
|
|
73
142
|
if (url === '/_ctc/proxies' && req.method === 'POST') {
|
|
74
143
|
let body = '';
|
|
75
144
|
req.on('data', (chunk) => { body += chunk; });
|
|
@@ -88,7 +157,7 @@ export class AdminServer {
|
|
|
88
157
|
});
|
|
89
158
|
return;
|
|
90
159
|
}
|
|
91
|
-
//
|
|
160
|
+
// 删除反向代理 API
|
|
92
161
|
if (url.startsWith('/_ctc/proxies/') && req.method === 'DELETE') {
|
|
93
162
|
const port = parseInt(url.split('/').pop(), 10);
|
|
94
163
|
if (isNaN(port)) {
|
|
@@ -108,6 +177,209 @@ export class AdminServer {
|
|
|
108
177
|
});
|
|
109
178
|
return;
|
|
110
179
|
}
|
|
180
|
+
// 测试反向代理 API
|
|
181
|
+
if (url.startsWith('/_ctc/test-proxy') && req.method === 'POST') {
|
|
182
|
+
const urlObj = new URL(url, `http://${req.headers.host}`);
|
|
183
|
+
const portParam = urlObj.searchParams.get('port');
|
|
184
|
+
const port = portParam ? parseInt(portParam, 10) : NaN;
|
|
185
|
+
if (isNaN(port)) {
|
|
186
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
187
|
+
res.end(JSON.stringify({ error: '无效的端口号' }));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// 通过 Controller 发送测试请求到服务端,然后测试连接
|
|
191
|
+
try {
|
|
192
|
+
// 简单测试:尝试连接本地端口来验证代理是否工作
|
|
193
|
+
// 实际测试需要通过服务端发起请求,这里只返回成功状态
|
|
194
|
+
const status = this.getStatusCallback();
|
|
195
|
+
const proxy = status.proxies.find((p) => p.remotePort === port);
|
|
196
|
+
if (!proxy) {
|
|
197
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
198
|
+
res.end(JSON.stringify({ success: false, error: '端口未注册' }));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// 测试本地端口是否可连接
|
|
202
|
+
const net = await import('net');
|
|
203
|
+
const testClient = new net.Socket();
|
|
204
|
+
let connected = false;
|
|
205
|
+
let error = null;
|
|
206
|
+
await new Promise((resolve) => {
|
|
207
|
+
testClient.connect({ port: proxy.localPort, host: proxy.localHost || 'localhost' }, () => {
|
|
208
|
+
connected = true;
|
|
209
|
+
testClient.destroy();
|
|
210
|
+
resolve();
|
|
211
|
+
});
|
|
212
|
+
testClient.on('error', (err) => {
|
|
213
|
+
error = err.message;
|
|
214
|
+
testClient.destroy();
|
|
215
|
+
resolve();
|
|
216
|
+
});
|
|
217
|
+
// 2秒超时
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
if (!connected) {
|
|
220
|
+
error = '连接超时';
|
|
221
|
+
testClient.destroy();
|
|
222
|
+
resolve();
|
|
223
|
+
}
|
|
224
|
+
}, 2000);
|
|
225
|
+
});
|
|
226
|
+
if (connected) {
|
|
227
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
228
|
+
res.end(JSON.stringify({
|
|
229
|
+
success: true,
|
|
230
|
+
message: `本地端口 ${proxy.localPort} 连接成功`,
|
|
231
|
+
proxy: { remotePort: port, localPort: proxy.localPort }
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
236
|
+
res.end(JSON.stringify({
|
|
237
|
+
success: false,
|
|
238
|
+
error: `本地端口 ${proxy.localPort} 连接失败: ${error}`
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
244
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
245
|
+
res.end(JSON.stringify({ success: false, error: errorMessage }));
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// ==================== 正向穿透 API ====================
|
|
250
|
+
// forward list - 列出正向穿透代理
|
|
251
|
+
if (url === '/_ctc/forward/list' && req.method === 'GET') {
|
|
252
|
+
const proxies = Array.from(this.forwardProxies.entries()).map(([key, value]) => ({
|
|
253
|
+
localPort: value.localPort,
|
|
254
|
+
targetClientId: value.targetClientId,
|
|
255
|
+
targetPort: value.targetPort,
|
|
256
|
+
enabled: true,
|
|
257
|
+
}));
|
|
258
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
259
|
+
res.end(JSON.stringify({ proxies }));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// forward add - 添加正向穿透代理
|
|
263
|
+
if (url === '/_ctc/forward/add' && req.method === 'POST') {
|
|
264
|
+
let body = '';
|
|
265
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
266
|
+
req.on('end', async () => {
|
|
267
|
+
try {
|
|
268
|
+
const data = JSON.parse(body);
|
|
269
|
+
const key = `${data.localPort}`;
|
|
270
|
+
if (this.addForwardProxyCallback) {
|
|
271
|
+
await this.addForwardProxyCallback({
|
|
272
|
+
...data,
|
|
273
|
+
enabled: true,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
this.forwardProxies.set(key, {
|
|
277
|
+
localPort: data.localPort,
|
|
278
|
+
targetClientId: data.targetClientId,
|
|
279
|
+
targetPort: data.targetPort,
|
|
280
|
+
});
|
|
281
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
282
|
+
res.end(JSON.stringify({ success: true }));
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
286
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
287
|
+
res.end(JSON.stringify({ error: errorMessage }));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// forward remove - 移除正向穿透代理
|
|
293
|
+
if (url === '/_ctc/forward/remove' && req.method === 'POST') {
|
|
294
|
+
let body = '';
|
|
295
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
296
|
+
req.on('end', async () => {
|
|
297
|
+
try {
|
|
298
|
+
const data = JSON.parse(body);
|
|
299
|
+
const key = `${data.localPort}`;
|
|
300
|
+
const deleted = this.forwardProxies.delete(key);
|
|
301
|
+
if (this.removeForwardProxyCallback) {
|
|
302
|
+
await this.removeForwardProxyCallback(data.localPort);
|
|
303
|
+
}
|
|
304
|
+
if (deleted) {
|
|
305
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
306
|
+
res.end(JSON.stringify({ success: true }));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
310
|
+
res.end(JSON.stringify({ error: '代理不存在' }));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
315
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
316
|
+
res.end(JSON.stringify({ error: errorMessage }));
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// forward clients - 获取客户端列表
|
|
322
|
+
if (url === '/_ctc/forward/clients' && req.method === 'GET') {
|
|
323
|
+
if (!this.getClientListCallback) {
|
|
324
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
325
|
+
res.end(JSON.stringify({ error: '服务未就绪' }));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const result = await this.getClientListCallback();
|
|
330
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
331
|
+
res.end(JSON.stringify(result));
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
335
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
336
|
+
res.end(JSON.stringify({ error: errorMessage }));
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// forward register - 注册到服务器
|
|
341
|
+
if (url === '/_ctc/forward/register' && req.method === 'POST') {
|
|
342
|
+
let body = '';
|
|
343
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
344
|
+
req.on('end', async () => {
|
|
345
|
+
try {
|
|
346
|
+
const data = JSON.parse(body);
|
|
347
|
+
if (!this.registerClientCallback) {
|
|
348
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
349
|
+
res.end(JSON.stringify({ error: '服务未就绪' }));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const result = await this.registerClientCallback(data.description);
|
|
353
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
354
|
+
res.end(JSON.stringify(result));
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
358
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
359
|
+
res.end(JSON.stringify({ error: errorMessage }));
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// reconnect - 主动重连到服务器
|
|
365
|
+
if (url === '/_ctc/reconnect' && req.method === 'POST') {
|
|
366
|
+
if (!this.reconnectCallback) {
|
|
367
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
368
|
+
res.end(JSON.stringify({ error: '重连服务未就绪' }));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
await this.reconnectCallback();
|
|
373
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
374
|
+
res.end(JSON.stringify({ success: true, message: '正在重连...' }));
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
378
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
379
|
+
res.end(JSON.stringify({ error: errorMessage }));
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
111
383
|
// 404
|
|
112
384
|
res.writeHead(404);
|
|
113
385
|
res.end('Not Found');
|
|
@@ -125,556 +397,11 @@ export class AdminServer {
|
|
|
125
397
|
}
|
|
126
398
|
}
|
|
127
399
|
/**
|
|
128
|
-
*
|
|
400
|
+
* 静态文件路径常量
|
|
129
401
|
*/
|
|
130
|
-
AdminServer.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<title>穿透客户端管理</title>
|
|
136
|
-
<style>
|
|
137
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
138
|
-
body {
|
|
139
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
140
|
-
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
141
|
-
min-height: 100vh;
|
|
142
|
-
color: #e0e0e0;
|
|
143
|
-
padding: 20px;
|
|
144
|
-
}
|
|
145
|
-
.container {
|
|
146
|
-
max-width: 900px;
|
|
147
|
-
margin: 0 auto;
|
|
148
|
-
}
|
|
149
|
-
.header {
|
|
150
|
-
text-align: center;
|
|
151
|
-
margin-bottom: 30px;
|
|
152
|
-
padding: 30px 20px;
|
|
153
|
-
background: rgba(255,255,255,0.05);
|
|
154
|
-
border-radius: 16px;
|
|
155
|
-
backdrop-filter: blur(10px);
|
|
156
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
157
|
-
}
|
|
158
|
-
.header h1 {
|
|
159
|
-
font-size: 28px;
|
|
160
|
-
margin-bottom: 8px;
|
|
161
|
-
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
|
162
|
-
-webkit-background-clip: text;
|
|
163
|
-
-webkit-text-fill-color: transparent;
|
|
164
|
-
}
|
|
165
|
-
.status {
|
|
166
|
-
display: inline-flex;
|
|
167
|
-
align-items: center;
|
|
168
|
-
gap: 8px;
|
|
169
|
-
padding: 6px 16px;
|
|
170
|
-
border-radius: 20px;
|
|
171
|
-
font-size: 14px;
|
|
172
|
-
font-weight: 500;
|
|
173
|
-
}
|
|
174
|
-
.status.running {
|
|
175
|
-
background: rgba(0, 255, 136, 0.15);
|
|
176
|
-
color: #00ff88;
|
|
177
|
-
}
|
|
178
|
-
.status.running::before {
|
|
179
|
-
content: "";
|
|
180
|
-
width: 8px;
|
|
181
|
-
height: 8px;
|
|
182
|
-
border-radius: 50%;
|
|
183
|
-
background: #00ff88;
|
|
184
|
-
animation: pulse 1.5s infinite;
|
|
185
|
-
}
|
|
186
|
-
.status.stopped {
|
|
187
|
-
background: rgba(255, 77, 77, 0.15);
|
|
188
|
-
color: #ff4d4d;
|
|
189
|
-
}
|
|
190
|
-
@keyframes pulse {
|
|
191
|
-
0%, 100% { opacity: 1; }
|
|
192
|
-
50% { opacity: 0.4; }
|
|
193
|
-
}
|
|
194
|
-
.grid {
|
|
195
|
-
display: grid;
|
|
196
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
197
|
-
gap: 16px;
|
|
198
|
-
margin-bottom: 20px;
|
|
199
|
-
}
|
|
200
|
-
.card {
|
|
201
|
-
background: rgba(255,255,255,0.05);
|
|
202
|
-
border-radius: 12px;
|
|
203
|
-
padding: 20px;
|
|
204
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
205
|
-
backdrop-filter: blur(10px);
|
|
206
|
-
}
|
|
207
|
-
.card-label {
|
|
208
|
-
font-size: 12px;
|
|
209
|
-
color: #888;
|
|
210
|
-
margin-bottom: 6px;
|
|
211
|
-
text-transform: uppercase;
|
|
212
|
-
letter-spacing: 0.5px;
|
|
213
|
-
}
|
|
214
|
-
.card-value {
|
|
215
|
-
font-size: 20px;
|
|
216
|
-
font-weight: 600;
|
|
217
|
-
color: #fff;
|
|
218
|
-
}
|
|
219
|
-
.card-value .unit {
|
|
220
|
-
font-size: 14px;
|
|
221
|
-
color: #888;
|
|
222
|
-
font-weight: 400;
|
|
223
|
-
}
|
|
224
|
-
.proxies-section {
|
|
225
|
-
background: rgba(255,255,255,0.05);
|
|
226
|
-
border-radius: 12px;
|
|
227
|
-
padding: 20px;
|
|
228
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
229
|
-
margin-top: 20px;
|
|
230
|
-
}
|
|
231
|
-
.section-header {
|
|
232
|
-
display: flex;
|
|
233
|
-
justify-content: space-between;
|
|
234
|
-
align-items: center;
|
|
235
|
-
margin-bottom: 16px;
|
|
236
|
-
}
|
|
237
|
-
.section-title {
|
|
238
|
-
font-size: 14px;
|
|
239
|
-
color: #888;
|
|
240
|
-
text-transform: uppercase;
|
|
241
|
-
letter-spacing: 0.5px;
|
|
242
|
-
}
|
|
243
|
-
.btn {
|
|
244
|
-
padding: 8px 16px;
|
|
245
|
-
border-radius: 8px;
|
|
246
|
-
border: none;
|
|
247
|
-
font-size: 13px;
|
|
248
|
-
cursor: pointer;
|
|
249
|
-
transition: all 0.2s;
|
|
250
|
-
}
|
|
251
|
-
.btn-primary {
|
|
252
|
-
background: linear-gradient(135deg, #00d9ff, #00ff88);
|
|
253
|
-
color: #000;
|
|
254
|
-
font-weight: 500;
|
|
255
|
-
}
|
|
256
|
-
.btn-primary:hover {
|
|
257
|
-
opacity: 0.9;
|
|
258
|
-
transform: translateY(-1px);
|
|
259
|
-
}
|
|
260
|
-
.btn-danger {
|
|
261
|
-
background: rgba(255, 77, 77, 0.2);
|
|
262
|
-
color: #ff4d4d;
|
|
263
|
-
padding: 4px 10px;
|
|
264
|
-
font-size: 12px;
|
|
265
|
-
}
|
|
266
|
-
.btn-danger:hover {
|
|
267
|
-
background: rgba(255, 77, 77, 0.3);
|
|
268
|
-
}
|
|
269
|
-
.proxy-item {
|
|
270
|
-
display: flex;
|
|
271
|
-
justify-content: space-between;
|
|
272
|
-
align-items: center;
|
|
273
|
-
padding: 12px 16px;
|
|
274
|
-
background: rgba(0,0,0,0.2);
|
|
275
|
-
border-radius: 8px;
|
|
276
|
-
margin-bottom: 8px;
|
|
277
|
-
font-size: 14px;
|
|
278
|
-
}
|
|
279
|
-
.proxy-item:last-child {
|
|
280
|
-
margin-bottom: 0;
|
|
281
|
-
}
|
|
282
|
-
.proxy-info {
|
|
283
|
-
display: flex;
|
|
284
|
-
align-items: center;
|
|
285
|
-
gap: 12px;
|
|
286
|
-
}
|
|
287
|
-
.proxy-protocol {
|
|
288
|
-
padding: 2px 8px;
|
|
289
|
-
border-radius: 4px;
|
|
290
|
-
font-size: 11px;
|
|
291
|
-
font-weight: 500;
|
|
292
|
-
text-transform: uppercase;
|
|
293
|
-
}
|
|
294
|
-
.proxy-protocol.http {
|
|
295
|
-
background: rgba(0, 217, 255, 0.2);
|
|
296
|
-
color: #00d9ff;
|
|
297
|
-
}
|
|
298
|
-
.proxy-protocol.websocket {
|
|
299
|
-
background: rgba(255, 165, 0, 0.2);
|
|
300
|
-
color: #ffa500;
|
|
301
|
-
}
|
|
302
|
-
.proxy-remote {
|
|
303
|
-
color: #00d9ff;
|
|
304
|
-
font-family: monospace;
|
|
305
|
-
}
|
|
306
|
-
.proxy-arrow {
|
|
307
|
-
color: #666;
|
|
308
|
-
}
|
|
309
|
-
.proxy-local {
|
|
310
|
-
color: #888;
|
|
311
|
-
font-family: monospace;
|
|
312
|
-
}
|
|
313
|
-
.empty-state {
|
|
314
|
-
text-align: center;
|
|
315
|
-
padding: 30px;
|
|
316
|
-
color: #666;
|
|
317
|
-
font-size: 14px;
|
|
318
|
-
}
|
|
319
|
-
.add-form {
|
|
320
|
-
display: none;
|
|
321
|
-
background: rgba(0,0,0,0.3);
|
|
322
|
-
border-radius: 12px;
|
|
323
|
-
padding: 20px;
|
|
324
|
-
margin-bottom: 16px;
|
|
325
|
-
}
|
|
326
|
-
.add-form.show {
|
|
327
|
-
display: block;
|
|
328
|
-
}
|
|
329
|
-
.form-row {
|
|
330
|
-
display: grid;
|
|
331
|
-
grid-template-columns: repeat(4, 1fr) auto;
|
|
332
|
-
gap: 12px;
|
|
333
|
-
align-items: end;
|
|
334
|
-
}
|
|
335
|
-
.form-group {
|
|
336
|
-
display: flex;
|
|
337
|
-
flex-direction: column;
|
|
338
|
-
gap: 6px;
|
|
339
|
-
}
|
|
340
|
-
.form-group label {
|
|
341
|
-
font-size: 11px;
|
|
342
|
-
color: #888;
|
|
343
|
-
text-transform: uppercase;
|
|
344
|
-
}
|
|
345
|
-
.form-group input, .form-group select {
|
|
346
|
-
padding: 10px 14px;
|
|
347
|
-
background: rgba(255,255,255,0.05);
|
|
348
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
349
|
-
border-radius: 8px;
|
|
350
|
-
color: #fff;
|
|
351
|
-
font-size: 14px;
|
|
352
|
-
}
|
|
353
|
-
.form-group input:focus, .form-group select:focus {
|
|
354
|
-
outline: none;
|
|
355
|
-
border-color: #00d9ff;
|
|
356
|
-
}
|
|
357
|
-
.form-actions {
|
|
358
|
-
display: flex;
|
|
359
|
-
gap: 8px;
|
|
360
|
-
}
|
|
361
|
-
.modal {
|
|
362
|
-
display: none;
|
|
363
|
-
position: fixed;
|
|
364
|
-
top: 0;
|
|
365
|
-
left: 0;
|
|
366
|
-
right: 0;
|
|
367
|
-
bottom: 0;
|
|
368
|
-
background: rgba(0,0,0,0.7);
|
|
369
|
-
align-items: center;
|
|
370
|
-
justify-content: center;
|
|
371
|
-
z-index: 1000;
|
|
372
|
-
}
|
|
373
|
-
.modal.show {
|
|
374
|
-
display: flex;
|
|
375
|
-
}
|
|
376
|
-
.modal-content {
|
|
377
|
-
background: #1a1a2e;
|
|
378
|
-
border-radius: 16px;
|
|
379
|
-
padding: 30px;
|
|
380
|
-
max-width: 400px;
|
|
381
|
-
width: 90%;
|
|
382
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
383
|
-
}
|
|
384
|
-
.modal-title {
|
|
385
|
-
font-size: 18px;
|
|
386
|
-
margin-bottom: 20px;
|
|
387
|
-
text-align: center;
|
|
388
|
-
}
|
|
389
|
-
.modal-actions {
|
|
390
|
-
display: flex;
|
|
391
|
-
gap: 12px;
|
|
392
|
-
margin-top: 20px;
|
|
393
|
-
}
|
|
394
|
-
.modal-actions .btn {
|
|
395
|
-
flex: 1;
|
|
396
|
-
}
|
|
397
|
-
.btn-secondary {
|
|
398
|
-
background: rgba(255,255,255,0.1);
|
|
399
|
-
color: #fff;
|
|
400
|
-
}
|
|
401
|
-
.btn-secondary:hover {
|
|
402
|
-
background: rgba(255,255,255,0.15);
|
|
403
|
-
}
|
|
404
|
-
.last-update {
|
|
405
|
-
text-align: center;
|
|
406
|
-
color: #666;
|
|
407
|
-
font-size: 12px;
|
|
408
|
-
margin-top: 20px;
|
|
409
|
-
}
|
|
410
|
-
.footer {
|
|
411
|
-
text-align: center;
|
|
412
|
-
margin-top: 30px;
|
|
413
|
-
padding: 20px;
|
|
414
|
-
color: #666;
|
|
415
|
-
font-size: 12px;
|
|
416
|
-
}
|
|
417
|
-
.footer a {
|
|
418
|
-
color: #00d9ff;
|
|
419
|
-
text-decoration: none;
|
|
420
|
-
}
|
|
421
|
-
.toast {
|
|
422
|
-
position: fixed;
|
|
423
|
-
bottom: 20px;
|
|
424
|
-
right: 20px;
|
|
425
|
-
padding: 12px 20px;
|
|
426
|
-
border-radius: 8px;
|
|
427
|
-
font-size: 14px;
|
|
428
|
-
transform: translateY(100px);
|
|
429
|
-
opacity: 0;
|
|
430
|
-
transition: all 0.3s;
|
|
431
|
-
}
|
|
432
|
-
.toast.show {
|
|
433
|
-
transform: translateY(0);
|
|
434
|
-
opacity: 1;
|
|
435
|
-
}
|
|
436
|
-
.toast.success {
|
|
437
|
-
background: rgba(0, 255, 136, 0.2);
|
|
438
|
-
color: #00ff88;
|
|
439
|
-
border: 1px solid rgba(0, 255, 136, 0.3);
|
|
440
|
-
}
|
|
441
|
-
.toast.error {
|
|
442
|
-
background: rgba(255, 77, 77, 0.2);
|
|
443
|
-
color: #ff4d4d;
|
|
444
|
-
border: 1px solid rgba(255, 77, 77, 0.3);
|
|
445
|
-
}
|
|
446
|
-
</style>
|
|
447
|
-
</head>
|
|
448
|
-
<body>
|
|
449
|
-
<div class="container">
|
|
450
|
-
<div class="header">
|
|
451
|
-
<h1>🔌 feng3d-ctc 穿透客户端</h1>
|
|
452
|
-
<div class="status running" id="status">运行中</div>
|
|
453
|
-
</div>
|
|
454
|
-
|
|
455
|
-
<div class="grid">
|
|
456
|
-
<div class="card">
|
|
457
|
-
<div class="card-label">服务器</div>
|
|
458
|
-
<div class="card-value" id="server">-</div>
|
|
459
|
-
</div>
|
|
460
|
-
<div class="card">
|
|
461
|
-
<div class="card-label">连接状态</div>
|
|
462
|
-
<div class="card-value" id="connection">-</div>
|
|
463
|
-
</div>
|
|
464
|
-
<div class="card">
|
|
465
|
-
<div class="card-label">运行时长</div>
|
|
466
|
-
<div class="card-value"><span id="uptime">-</span></div>
|
|
467
|
-
</div>
|
|
468
|
-
<div class="card">
|
|
469
|
-
<div class="card-label">代理数量</div>
|
|
470
|
-
<div class="card-value"><span id="proxyCount">0</span><span class="unit"> 个</span></div>
|
|
471
|
-
</div>
|
|
472
|
-
<div class="card">
|
|
473
|
-
<div class="card-label">重连次数</div>
|
|
474
|
-
<div class="card-value"><span id="reconnectCount">0</span><span class="unit"> 次</span></div>
|
|
475
|
-
</div>
|
|
476
|
-
</div>
|
|
477
|
-
|
|
478
|
-
<div class="proxies-section">
|
|
479
|
-
<div class="section-header">
|
|
480
|
-
<div class="section-title">代理映射</div>
|
|
481
|
-
<button class="btn btn-primary" id="showAddForm">+ 添加代理</button>
|
|
482
|
-
</div>
|
|
483
|
-
|
|
484
|
-
<div class="add-form" id="addForm">
|
|
485
|
-
<div class="form-row">
|
|
486
|
-
<div class="form-group">
|
|
487
|
-
<label>远程端口</label>
|
|
488
|
-
<input type="number" id="newRemotePort" placeholder="8080" min="1" max="65535">
|
|
489
|
-
</div>
|
|
490
|
-
<div class="form-group">
|
|
491
|
-
<label>协议</label>
|
|
492
|
-
<select id="newProtocol">
|
|
493
|
-
<option value="http">HTTP</option>
|
|
494
|
-
<option value="websocket">WebSocket</option>
|
|
495
|
-
</select>
|
|
496
|
-
</div>
|
|
497
|
-
<div class="form-group">
|
|
498
|
-
<label>本地端口</label>
|
|
499
|
-
<input type="number" id="newLocalPort" placeholder="3000" min="1" max="65535">
|
|
500
|
-
</div>
|
|
501
|
-
<div class="form-group">
|
|
502
|
-
<label>本地地址</label>
|
|
503
|
-
<input type="text" id="newLocalHost" placeholder="localhost">
|
|
504
|
-
</div>
|
|
505
|
-
<div class="form-actions">
|
|
506
|
-
<button class="btn btn-primary" id="addProxy">添加</button>
|
|
507
|
-
<button class="btn btn-secondary" id="cancelAdd">取消</button>
|
|
508
|
-
</div>
|
|
509
|
-
</div>
|
|
510
|
-
</div>
|
|
511
|
-
|
|
512
|
-
<div id="proxiesList"></div>
|
|
513
|
-
</div>
|
|
514
|
-
|
|
515
|
-
<div class="last-update">最后更新: <span id="lastUpdate">-</span></div>
|
|
516
|
-
|
|
517
|
-
<div class="footer">
|
|
518
|
-
<a href="https://github.com/feng3d/chuantou" target="_blank">feng3d-ctc</a>
|
|
519
|
-
— 内网穿透客户端
|
|
520
|
-
</div>
|
|
521
|
-
</div>
|
|
522
|
-
|
|
523
|
-
<div class="modal" id="deleteModal">
|
|
524
|
-
<div class="modal-content">
|
|
525
|
-
<div class="modal-title">确认删除代理</div>
|
|
526
|
-
<p style="color: #888; text-align: center;">确定要删除此代理映射吗?</p>
|
|
527
|
-
<div class="modal-actions">
|
|
528
|
-
<button class="btn btn-secondary" id="cancelDelete">取消</button>
|
|
529
|
-
<button class="btn btn-danger" id="confirmDelete">删除</button>
|
|
530
|
-
</div>
|
|
531
|
-
</div>
|
|
532
|
-
</div>
|
|
533
|
-
|
|
534
|
-
<div class="toast" id="toast"></div>
|
|
535
|
-
|
|
536
|
-
<script>
|
|
537
|
-
let deletePort = null;
|
|
538
|
-
|
|
539
|
-
function formatUptime(ms) {
|
|
540
|
-
const seconds = Math.floor(ms / 1000);
|
|
541
|
-
const minutes = Math.floor(seconds / 60);
|
|
542
|
-
const hours = Math.floor(minutes / 60);
|
|
543
|
-
const days = Math.floor(hours / 24);
|
|
544
|
-
if (days > 0) return \`\${days}天 \${hours % 24}小时\`;
|
|
545
|
-
if (hours > 0) return \`\${hours}小时 \${minutes % 60}分钟\`;
|
|
546
|
-
if (minutes > 0) return \`\${minutes}分钟 \${seconds % 60}秒\`;
|
|
547
|
-
return \`\${seconds}秒\`;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function showToast(message, type = 'success') {
|
|
551
|
-
const toast = document.getElementById('toast');
|
|
552
|
-
toast.textContent = message;
|
|
553
|
-
toast.className = \`toast \${type} show\`;
|
|
554
|
-
setTimeout(() => toast.classList.remove('show'), 3000);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
async function updateStatus() {
|
|
558
|
-
try {
|
|
559
|
-
const res = await fetch('/_ctc/status');
|
|
560
|
-
const data = await res.json();
|
|
561
|
-
|
|
562
|
-
const statusEl = document.getElementById('status');
|
|
563
|
-
if (data.running) {
|
|
564
|
-
statusEl.textContent = data.authenticated ? '已连接' : (data.connected ? '认证中...' : '连接中...');
|
|
565
|
-
statusEl.className = 'status running';
|
|
566
|
-
} else {
|
|
567
|
-
statusEl.textContent = '已停止';
|
|
568
|
-
statusEl.className = 'status stopped';
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
document.getElementById('server').textContent = data.serverUrl.replace('ws://', '').replace('wss://', '');
|
|
572
|
-
document.getElementById('connection').textContent = data.authenticated ? '已认证' : (data.connected ? '已连接' : '未连接');
|
|
573
|
-
document.getElementById('uptime').textContent = formatUptime(data.uptime);
|
|
574
|
-
document.getElementById('proxyCount').textContent = data.proxies.length;
|
|
575
|
-
document.getElementById('reconnectCount').textContent = data.reconnectAttempts;
|
|
576
|
-
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('zh-CN');
|
|
577
|
-
|
|
578
|
-
// 更新代理列表
|
|
579
|
-
const listEl = document.getElementById('proxiesList');
|
|
580
|
-
if (data.proxies.length === 0) {
|
|
581
|
-
listEl.innerHTML = '<div class="empty-state">暂无代理映射,点击上方按钮添加</div>';
|
|
582
|
-
} else {
|
|
583
|
-
listEl.innerHTML = data.proxies.map(p => {
|
|
584
|
-
const protocol = p.protocol === 'websocket' ? 'websocket' : 'http';
|
|
585
|
-
return \`
|
|
586
|
-
<div class="proxy-item">
|
|
587
|
-
<div class="proxy-info">
|
|
588
|
-
<span class="proxy-protocol \${protocol}">\${protocol === 'websocket' ? 'WS' : 'HTTP'}</span>
|
|
589
|
-
<span class="proxy-remote">:\${p.remotePort}</span>
|
|
590
|
-
<span class="proxy-arrow">→</span>
|
|
591
|
-
<span class="proxy-local">\${p.localHost || 'localhost'}:\${p.localPort}</span>
|
|
592
|
-
</div>
|
|
593
|
-
<button class="btn btn-danger" onclick="showDeleteModal(\${p.remotePort})">删除</button>
|
|
594
|
-
</div>
|
|
595
|
-
\`;
|
|
596
|
-
}).join('');
|
|
597
|
-
}
|
|
598
|
-
} catch (e) {
|
|
599
|
-
console.error('获取状态失败:', e);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function showDeleteModal(port) {
|
|
604
|
-
deletePort = port;
|
|
605
|
-
document.getElementById('deleteModal').classList.add('show');
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
document.getElementById('cancelDelete').addEventListener('click', () => {
|
|
609
|
-
document.getElementById('deleteModal').classList.remove('show');
|
|
610
|
-
deletePort = null;
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
document.getElementById('confirmDelete').addEventListener('click', async () => {
|
|
614
|
-
if (deletePort) {
|
|
615
|
-
try {
|
|
616
|
-
const res = await fetch(\`/_ctc/proxies/\${deletePort}\`, { method: 'DELETE' });
|
|
617
|
-
if (res.ok) {
|
|
618
|
-
showToast('代理已删除');
|
|
619
|
-
updateStatus();
|
|
620
|
-
} else {
|
|
621
|
-
const data = await res.json();
|
|
622
|
-
showToast(\`删除失败: \${data.error}\`, 'error');
|
|
623
|
-
}
|
|
624
|
-
} catch (e) {
|
|
625
|
-
showToast('删除失败: 网络错误', 'error');
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
document.getElementById('deleteModal').classList.remove('show');
|
|
629
|
-
deletePort = null;
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
document.getElementById('showAddForm').addEventListener('click', () => {
|
|
633
|
-
document.getElementById('addForm').classList.add('show');
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
document.getElementById('cancelAdd').addEventListener('click', () => {
|
|
637
|
-
document.getElementById('addForm').classList.remove('show');
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
document.getElementById('addProxy').addEventListener('click', async () => {
|
|
641
|
-
const remotePort = parseInt(document.getElementById('newRemotePort').value);
|
|
642
|
-
const protocol = document.getElementById('newProtocol').value;
|
|
643
|
-
const localPort = parseInt(document.getElementById('newLocalPort').value);
|
|
644
|
-
const localHost = document.getElementById('newLocalHost').value || 'localhost';
|
|
645
|
-
|
|
646
|
-
if (!remotePort || !localPort) {
|
|
647
|
-
showToast('请填写完整信息', 'error');
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
try {
|
|
652
|
-
const res = await fetch('/_ctc/proxies', {
|
|
653
|
-
method: 'POST',
|
|
654
|
-
headers: { 'Content-Type': 'application/json' },
|
|
655
|
-
body: JSON.stringify({ remotePort, protocol, localPort, localHost })
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
if (res.ok) {
|
|
659
|
-
showToast('代理已添加');
|
|
660
|
-
document.getElementById('addForm').classList.remove('show');
|
|
661
|
-
document.getElementById('newRemotePort').value = '';
|
|
662
|
-
document.getElementById('newLocalPort').value = '';
|
|
663
|
-
document.getElementById('newLocalHost').value = 'localhost';
|
|
664
|
-
updateStatus();
|
|
665
|
-
} else {
|
|
666
|
-
const data = await res.json();
|
|
667
|
-
showToast(\`添加失败: \${data.error}\`, 'error');
|
|
668
|
-
}
|
|
669
|
-
} catch (e) {
|
|
670
|
-
showToast('添加失败: 网络错误', 'error');
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
updateStatus();
|
|
675
|
-
setInterval(updateStatus, 3000);
|
|
676
|
-
</script>
|
|
677
|
-
</body>
|
|
678
|
-
</html>
|
|
679
|
-
`;
|
|
402
|
+
AdminServer.STATIC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'admin-ui', 'dist');
|
|
403
|
+
/**
|
|
404
|
+
* HTML 模板文件路径
|
|
405
|
+
*/
|
|
406
|
+
AdminServer.TEMPLATE_PATH = join(AdminServer.STATIC_DIR, 'index.html');
|
|
680
407
|
//# sourceMappingURL=admin-server.js.map
|