feat: 重构 WebSocket Keep-Alive 客户端,添加连接和消息处理功能,更新依赖版本,增加 keep.ts 文件

This commit is contained in:
2026-01-30 21:16:06 +08:00
parent 1d4a27d1b2
commit d7a4bcf58f
6 changed files with 270 additions and 37 deletions

187
src/workspace/keep-live.ts Normal file
View 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;
}