feat: 重构 WebSocket Keep-Alive 客户端,添加连接和消息处理功能,更新依赖版本,增加 keep.ts 文件
This commit is contained in:
187
src/workspace/keep-live.ts
Normal file
187
src/workspace/keep-live.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
// WebSocket Keep-Alive Client Library
|
||||
import WebSocket from "ws";
|
||||
|
||||
export interface KeepAliveConfig {
|
||||
wsUrl: string;
|
||||
cookie: string;
|
||||
reconnectInterval?: number;
|
||||
maxReconnectAttempts?: number;
|
||||
pingInterval?: number;
|
||||
onMessage?: (data: Buffer | string) => void;
|
||||
onConnect?: () => void;
|
||||
onDisconnect?: (code: number) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onSign?: (data: { type: string; data: string; signedData: string }) => void;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface ParsedMessage {
|
||||
type: string;
|
||||
raw: Buffer;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
type MessageHandler = (msg: ParsedMessage) => void;
|
||||
|
||||
export class WSKeepAlive {
|
||||
private ws: WebSocket | null = null;
|
||||
private config: Required<KeepAliveConfig>;
|
||||
private reconnectAttempts = 0;
|
||||
private pingTimer: NodeJS.Timeout | null = null;
|
||||
private messageHandlers: Set<MessageHandler> = new Set();
|
||||
private url: URL;
|
||||
|
||||
constructor(config: KeepAliveConfig) {
|
||||
this.config = {
|
||||
wsUrl: config.wsUrl,
|
||||
cookie: config.cookie,
|
||||
reconnectInterval: config.reconnectInterval ?? 5000,
|
||||
maxReconnectAttempts: config.maxReconnectAttempts ?? 3,
|
||||
pingInterval: config.pingInterval ?? 30000,
|
||||
onMessage: config.onMessage ?? (() => {}),
|
||||
onConnect: config.onConnect ?? (() => {}),
|
||||
onDisconnect: config.onDisconnect ?? (() => {}),
|
||||
onError: config.onError ?? (() => {}),
|
||||
onSign: config.onSign ?? (() => {}),
|
||||
debug: config.debug ?? false,
|
||||
};
|
||||
this.url = new URL(this.config.wsUrl);
|
||||
}
|
||||
|
||||
private log(message: string) {
|
||||
if (!this.config.debug) return;
|
||||
const timestamp = new Date().toISOString();
|
||||
const msg = `[${timestamp}] ${message}`;
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
private parseMessage(data: Buffer): ParsedMessage | null {
|
||||
const result: ParsedMessage = { type: "unknown", raw: data };
|
||||
|
||||
if (data.length < 14) {
|
||||
result.type = "raw";
|
||||
return result;
|
||||
}
|
||||
|
||||
const prefix = data.slice(0, 13);
|
||||
const msgType = prefix[0];
|
||||
const jsonStart = data.indexOf(0x71); // 0x71 = 'q'
|
||||
|
||||
if (jsonStart !== -1) {
|
||||
try {
|
||||
const jsonStr = data.slice(jsonStart + 1).toString();
|
||||
const payload = JSON.parse(jsonStr);
|
||||
result.type = `binary(0x${msgType.toString(16)})`;
|
||||
result.payload = payload;
|
||||
|
||||
// 特殊处理 sign 类型
|
||||
if (payload.type === "sign" && this.config.onSign) {
|
||||
this.config.onSign(payload);
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
result.type = "binary(json-parse-error)";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.type = "raw";
|
||||
return result;
|
||||
}
|
||||
|
||||
connect() {
|
||||
const { wsUrl, cookie, debug } = this.config;
|
||||
this.log(`Connecting to ${wsUrl}...`);
|
||||
|
||||
this.ws = new WebSocket(wsUrl, {
|
||||
headers: {
|
||||
"Origin": this.url.origin,
|
||||
"Cookie": cookie,
|
||||
"Cache-Control": "no-cache",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Pragma": "no-cache",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Sec-WebSocket-Extensions": "permessage-deflate",
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on("open", () => {
|
||||
debug && this.log("Connected!");
|
||||
this.reconnectAttempts = 0;
|
||||
this.config.onConnect();
|
||||
this.startPing();
|
||||
});
|
||||
|
||||
this.ws.on("message", (data: any) => {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
const parsed = this.parseMessage(data);
|
||||
this.config.onMessage(parsed?.raw ?? data);
|
||||
|
||||
this.messageHandlers.forEach(handler => {
|
||||
if (parsed) handler(parsed);
|
||||
});
|
||||
} else {
|
||||
this.config.onMessage(data);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on("close", (code: number) => {
|
||||
debug && this.log(`Disconnected (code: ${code})`);
|
||||
this.stopPing();
|
||||
this.config.onDisconnect(code);
|
||||
this.handleReconnect();
|
||||
});
|
||||
|
||||
this.ws.on("error", (err: Error) => {
|
||||
debug && this.log(`Error: ${err.message}`);
|
||||
this.config.onError(err);
|
||||
});
|
||||
}
|
||||
|
||||
private startPing() {
|
||||
this.stopPing();
|
||||
this.pingTimer = setInterval(() => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.ping();
|
||||
this.log("Sent ping");
|
||||
}
|
||||
}, this.config.pingInterval);
|
||||
}
|
||||
|
||||
private stopPing() {
|
||||
if (this.pingTimer) {
|
||||
clearInterval(this.pingTimer);
|
||||
this.pingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleReconnect() {
|
||||
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
||||
this.log(`Max reconnect attempts (${this.config.maxReconnectAttempts}) reached. Giving up.`);
|
||||
return;
|
||||
}
|
||||
this.reconnectAttempts++;
|
||||
this.log(`Reconnecting in ${this.config.reconnectInterval}ms... (attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts})`);
|
||||
setTimeout(() => this.connect(), this.config.reconnectInterval);
|
||||
}
|
||||
|
||||
onMessage(handler: MessageHandler) {
|
||||
this.messageHandlers.add(handler);
|
||||
return () => this.messageHandlers.delete(handler);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.stopPing();
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 便捷函数:快速创建并启动
|
||||
export function createKeepAlive(config: KeepAliveConfig): WSKeepAlive {
|
||||
const client = new WSKeepAlive(config);
|
||||
client.connect();
|
||||
return client;
|
||||
}
|
||||
Reference in New Issue
Block a user