feat: enhance WebSocket proxy with user connection management and status reporting

- Updated StudioOpts type to include infoList for user connection status.
- Added rendering of connection info in createStudioAppListHtml.
- Modified self-restart logic to use a specific app path.
- Improved WebSocket connection handling in wsProxyManager, including user registration and ID management.
- Implemented connection status checks and responses in UserV1Proxy.
- Introduced renderServerHtml function to inject server data into HTML responses.
- Refactored page-proxy request handling for better URL management.
This commit is contained in:
2026-03-05 03:58:46 +08:00
parent aaedcb881b
commit bbdf9f087d
9 changed files with 451 additions and 270 deletions

View File

@@ -1,7 +1,9 @@
import { nanoid } from 'nanoid';
import { customAlphabet } from 'nanoid';
import { WebSocket } from 'ws';
import { logger } from '../logger.ts';
import { EventEmitter } from 'eventemitter3';
const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const nanoid = customAlphabet(letters, 10);
class WsMessage {
ws: WebSocket;
@@ -9,11 +11,16 @@ class WsMessage {
emitter: EventEmitter;
private pingTimer?: NodeJS.Timeout;
private readonly PING_INTERVAL = 30000; // 30 秒发送一次 ping
constructor({ ws, user }: WssMessageOptions) {
id?: string;
status?: 'waiting' | 'connected' | 'closed';
manager: WsProxyManager;
constructor({ ws, user, id, isLogin, manager }: WssMessageOptions) {
this.ws = ws;
this.user = user;
this.id = id;
this.emitter = new EventEmitter();
this.manager = manager;
this.status = isLogin ? 'connected' : 'waiting';
this.startPing();
}
@@ -39,12 +46,44 @@ class WsMessage {
this.stopPing();
this.emitter.removeAllListeners();
}
isClosed() {
return this.ws.readyState === WebSocket.CLOSED;
}
async sendResponse(data: any) {
if (data.id) {
this.emitter.emit(data.id, data?.data);
}
}
async sendConnected() {
const id = this.id;
const user = this.user;
const data = { type: 'verified', user, id };
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
this.status = 'connected';
}
if (id.includes('-registry-')) {
const newId = id.split('-registry-')[0];
this.manager.changeId(id, newId);
const ws = this.ws;
// @ts-ignore
if (this.ws?.data) {
// @ts-ignore
this.ws.data.userApp = newId;
}
}
}
getInfo() {
const shortAppId = this.id ? this.id.split('--')[1] : '';
return {
user: this.user,
id: this.id,
status: this.status,
shortAppId,
pathname: this.id ? `/${this.user}/v1/${shortAppId}` : '',
};
}
async sendData(data: any, context?: any, opts?: { timeout?: number }) {
if (this.ws.readyState !== WebSocket.OPEN) {
return { code: 500, message: 'WebSocket is not open' };
@@ -64,6 +103,7 @@ class WsMessage {
const msg = { path: data?.path, key: data?.key, id: data?.id };
return new Promise((resolve) => {
const timer = setTimeout(() => {
console.log('ws-proxy sendData timeout', msg);
resolve({
code: 500,
message: `运行超时执行的id: ${id},参数是${JSON.stringify(msg)}`,
@@ -79,7 +119,12 @@ class WsMessage {
type WssMessageOptions = {
ws: WebSocket;
user?: string;
id?: string;
realId?: string;
isLogin?: boolean;
manager: WsProxyManager;
};
export class WsProxyManager {
wssMap: Map<string, WsMessage> = new Map();
PING_INTERVAL = 30000; // 30 秒检查一次连接状态
@@ -89,7 +134,7 @@ export class WsProxyManager {
}
this.checkConnceted();
}
register(id: string, opts?: { ws: WebSocket; user: string }) {
register(id: string, opts?: { ws: WebSocket; user: string, id?: string }) {
if (this.wssMap.has(id)) {
const value = this.wssMap.get(id);
if (value) {
@@ -100,8 +145,24 @@ export class WsProxyManager {
const [username, appId] = id.split('--');
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
console.log('WsProxyManager register', id, '访问地址', url.toString());
const value = new WsMessage({ ws: opts?.ws, user: opts?.user });
const value = new WsMessage({ ...opts, manager: this } as WssMessageOptions);
this.wssMap.set(id, value);
return value;
}
changeId(oldId: string, newId: string) {
const value = this.wssMap.get(oldId);
const originalValue = this.wssMap.get(newId);
if (originalValue) {
logger.debug(`WsProxyManager changeId: ${newId} already exists, close old connection`);
originalValue.ws.close();
originalValue.destroy();
}
if (value) {
this.wssMap.delete(oldId);
this.wssMap.set(newId, value);
value.id = newId;
logger.debug(`WsProxyManager changeId: ${oldId} -> ${newId}`);
}
}
unregister(id: string) {
const value = this.wssMap.get(id);
@@ -117,9 +178,33 @@ export class WsProxyManager {
}
return Array.from(this.wssMap.keys());
}
getIdsInfo(beginWith?: string) {
const ids = this.getIds(beginWith);
const infoList = this.getInfoList(ids);
return {
ids,
infoList,
};
}
getInfoList(ids: string[]) {
return ids.map(id => {
const value = this.wssMap.get(id);
if (value) {
return value.getInfo();
}
return null;
}).filter(Boolean);
}
get(id: string) {
return this.wssMap.get(id);
}
createId(id: string) {
if (!this.wssMap.has(id)) {
return id;
}
const newId = id + '-' + nanoid(6);
return newId;
}
checkConnceted() {
const that = this;
setTimeout(() => {
@@ -132,4 +217,40 @@ export class WsProxyManager {
that.checkConnceted();
}, this.PING_INTERVAL);
}
async createNewConnection(opts: { ws: any; user: string, userApp: string, isLogin?: boolean }) {
const id = opts.userApp;
let realId: string = id;
const isLogin = opts.isLogin || false;
const has = this.wssMap.has(id);
const registryId = '-registry-' + generateRegistryId(); // 生成一个随机六位字符串作为注册 ID
let isNeedVerify = !isLogin;
if (has) {
const value = this.wssMap.get(id);
if (value) {
if (value.isClosed()) {
// 短时间内还在, 等于简单重启了一下应用,不需要重新注册.
logger.debug('之前的连接已关闭,复用注册 ID 连接 ws', id);
this.unregister(id);
await new Promise(resolve => setTimeout(resolve, 100));
const wsMessage = this.register(id, { ws: opts.ws, user: opts.user, id });
wsMessage.sendConnected();
return { wsMessage, isNew: false, id: id };
} else {
// 没有关闭,需要重新注册鉴权一下, 生成新的 id 连接.
isNeedVerify = true;
}
}
}
// 没有连接, 直接注册新的连接.
if (isNeedVerify) {
realId = id + registryId;
logger.debug('未登录用户,使用临时注册 ID 连接 ws', realId);
}
const wsMessage = this.register(realId, { ws: opts.ws, user: opts.user, id: realId });
return { wsMessage, isNew: true, id: realId };
}
}
// 生成一个随机六位字符串作为注册 ID
const generateRegistryId = () => {
return Math.random().toString(36).substring(2, 8);
}