feat: 更新WebSocket Keep-Alive客户端库以支持Bun和Node.js环境,添加统一的消息处理方法,并创建keep.ts文件以初始化KeepAlive

This commit is contained in:
2026-02-15 20:48:45 +08:00
parent d231f3748a
commit 47229c6db9
5 changed files with 120 additions and 47 deletions

View File

@@ -1,4 +1,4 @@
import { buildWithBun } from '@kevisual/code-builder'
await buildWithBun({ naming: 'opencode', entry: 'agent/opencode.ts', dts: true });
await buildWithBun({ naming: 'keep', entry: 'src/keep.ts', dts: true, external: ['ws'] });
await buildWithBun({ naming: 'keep', entry: 'src/keep.ts', dts: true, target: 'node' });
await buildWithBun({ naming: 'routes', entry: 'agent/index.ts', dts: true });

13
keep.ts Normal file
View File

@@ -0,0 +1,13 @@
import { createKeepAlive } from "@kevisual/cnb/keep";
const config = {
"wss": "wss://cnb-tmm-1jhgl3i0m-001.cnb.space:443/stable-3c0b449c6e6e37b44a8a7938c0d8a3049926a64c?reconnectionToken=26ba6a08-1c57-41cc-8099-1f6e64863bf6&reconnection=false&skipWebSocketFrames=false",
"cookie": "orange:workspace:cookie-session:cnb-tmm-1jhgl3i0m-001=93d7bc9b-9ca0-4867-963d-1928ad3038c7",
"url": "https://cnb-tmm-1jhgl3i0m-001.cnb.space/?folder=/workspace"
}
createKeepAlive({
wsUrl: config.wss,
cookie: config.cookie,
debug: true,
});

View File

@@ -1,6 +1,6 @@
{
"name": "@kevisual/cnb",
"version": "0.0.25",
"version": "0.0.26",
"description": "",
"main": "index.js",
"scripts": {
@@ -22,9 +22,9 @@
"@kevisual/ai": "^0.0.24",
"@kevisual/code-builder": "^0.0.6",
"@kevisual/dts": "^0.0.3",
"@kevisual/context": "^0.0.4",
"@kevisual/context": "^0.0.6",
"@kevisual/types": "^0.0.12",
"@opencode-ai/plugin": "^1.2.1",
"@opencode-ai/plugin": "^1.2.4",
"@types/bun": "^1.3.9",
"@types/node": "^25.2.3",
"@types/ws": "^8.18.1",

28
pnpm-lock.yaml generated
View File

@@ -43,8 +43,8 @@ importers:
specifier: ^0.0.6
version: 0.0.6
'@kevisual/context':
specifier: ^0.0.4
version: 0.0.4
specifier: ^0.0.6
version: 0.0.6
'@kevisual/dts':
specifier: ^0.0.3
version: 0.0.3(typescript@5.9.3)
@@ -52,8 +52,8 @@ importers:
specifier: ^0.0.12
version: 0.0.12
'@opencode-ai/plugin':
specifier: ^1.2.1
version: 1.2.1
specifier: ^1.2.4
version: 1.2.4
'@types/bun':
specifier: ^1.3.9
version: 1.3.9
@@ -90,8 +90,8 @@ packages:
resolution: {integrity: sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw==}
hasBin: true
'@kevisual/context@0.0.4':
resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==}
'@kevisual/context@0.0.6':
resolution: {integrity: sha512-w7HBOuO3JH37n6xT6W3FD7ykqHTwtyxOQzTzfEcKDCbsvGB1wVreSxFm2bvoFnnFLuxT/5QMpKlnPrwvmcTGnw==}
'@kevisual/dts@0.0.3':
resolution: {integrity: sha512-4T/m2LqhtwWEW+lWmg7jLxKFW7VtIAftsWFDDZvh10bZunqFf8iXxChHcVSQWikghJb4cq1IkWzPkvc2l+Asdw==}
@@ -127,11 +127,11 @@ packages:
resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==}
engines: {node: '>=10.0.0'}
'@opencode-ai/plugin@1.2.1':
resolution: {integrity: sha512-PVRz3Y0l7+xi4iNxvdC32zx5wrEMfCiVQQVh3wZ7r+g6kM+8pUguKhwxTcwcOx57XMPMhmuoxuRcLMn79gtQuA==}
'@opencode-ai/plugin@1.2.4':
resolution: {integrity: sha512-FfRybm1Ujzkt8EQDtxZKVEA88EI8XaAu3ikViC8DYSP3lJaF++8isN3vmlSqCi+A+O2/5xd2yQ0yq3tmJ2WVhw==}
'@opencode-ai/sdk@1.2.1':
resolution: {integrity: sha512-K5e15mIXTyAykBw0GX+8O28IJHlPMw1jI/m3SDu+hgUHjmg2refqLPqyuqv8hE2nRcuGi8HajhpDJjkO7H2S0A==}
'@opencode-ai/sdk@1.2.4':
resolution: {integrity: sha512-IPgtBpif46wTviC3HQxkjS4M/1tZSnRmD/6aEF3lL88MT+PAqKA30G+AhBlpvXBITq9EmjO4gjzM59ly2z7mYQ==}
'@rollup/plugin-commonjs@28.0.9':
resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==}
@@ -583,7 +583,7 @@ snapshots:
'@kevisual/code-builder@0.0.6': {}
'@kevisual/context@0.0.4': {}
'@kevisual/context@0.0.6': {}
'@kevisual/dts@0.0.3(typescript@5.9.3)':
dependencies:
@@ -625,12 +625,12 @@ snapshots:
'@kevisual/ws@8.19.0': {}
'@opencode-ai/plugin@1.2.1':
'@opencode-ai/plugin@1.2.4':
dependencies:
'@opencode-ai/sdk': 1.2.1
'@opencode-ai/sdk': 1.2.4
zod: 4.3.6
'@opencode-ai/sdk@1.2.1': {}
'@opencode-ai/sdk@1.2.4': {}
'@rollup/plugin-commonjs@28.0.9(rollup@4.57.1)':
dependencies:

View File

@@ -1,5 +1,16 @@
// WebSocket Keep-Alive Client Library
import WebSocket from "ws";
// 运行时检测Bun 使用原生 WebSocketNode.js 使用 ws 库
let WebSocketModule: any;
if (typeof Bun !== 'undefined') {
// Bun 环境:使用原生 WebSocket
WebSocketModule = { WebSocket: globalThis.WebSocket };
} else {
// Node.js 环境:使用 ws 库
WebSocketModule = await import('ws');
}
const WebSocket = WebSocketModule.WebSocket;
export interface KeepAliveConfig {
wsUrl: string;
@@ -31,6 +42,7 @@ export class WSKeepAlive {
private pingTimer: NodeJS.Timeout | null = null;
private messageHandlers: Set<MessageHandler> = new Set();
private url: URL;
private readonly isBun: boolean;
constructor(config: KeepAliveConfig) {
this.config = {
@@ -48,6 +60,7 @@ export class WSKeepAlive {
debug: config.debug ?? false,
};
this.url = new URL(this.config.wsUrl);
this.isBun = typeof Bun !== 'undefined';
}
private log(message: string) {
@@ -107,44 +120,91 @@ export class WSKeepAlive {
}
});
this.ws.on("open", () => {
debug && this.log("Connected!");
this.reconnectAttempts = 0;
this.config.onConnect();
this.startPing();
});
if (this.isBun) {
// Bun 环境:使用标准 Web API
const ws = this.ws as any;
ws.onopen = () => {
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);
ws.onmessage = async (event: MessageEvent) => {
let data: Buffer | string;
this.messageHandlers.forEach(handler => {
if (parsed) handler(parsed);
});
} else {
this.config.onMessage(data);
}
});
if (event.data instanceof Blob) {
data = Buffer.from(await event.data.arrayBuffer());
} else if (event.data instanceof ArrayBuffer) {
data = Buffer.from(event.data);
} else if (typeof event.data === 'string') {
data = event.data;
} else {
data = Buffer.from(event.data);
}
this.ws.on("close", (code: number) => {
debug && this.log(`Disconnected (code: ${code})`);
this.stopPing();
this.config.onDisconnect(code);
this.handleReconnect();
});
this.handleMessage(data);
};
this.ws.on("error", (err: Error) => {
debug && this.log(`Error: ${err.message}`);
this.config.onError(err);
});
ws.onclose = (event: CloseEvent) => {
debug && this.log(`Disconnected (code: ${event.code})`);
this.stopPing();
this.config.onDisconnect(event.code);
this.handleReconnect();
};
ws.onerror = (event: Event) => {
debug && this.log(`Error: ${event}`);
this.config.onError(new Error("WebSocket error"));
};
} else {
// Node.js (ws 库):使用 EventEmitter 模式
const ws = this.ws as any;
ws.on("open", () => {
debug && this.log("Connected!");
this.reconnectAttempts = 0;
this.config.onConnect();
this.startPing();
});
ws.on("message", (data: any) => {
this.handleMessage(data);
});
ws.on("close", (code: number) => {
debug && this.log(`Disconnected (code: ${code})`);
this.stopPing();
this.config.onDisconnect(code);
this.handleReconnect();
});
ws.on("error", (err: Error) => {
debug && this.log(`Error: ${err.message}`);
this.config.onError(err);
});
}
}
// 统一的消息处理方法
private handleMessage(data: Buffer | string) {
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);
}
}
private startPing() {
this.stopPing();
this.pingTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
// 使用 JSON 格式的 ping 消息,兼容 Bun 和 Node.js
this.ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
this.log("Sent ping");
}
}, this.config.pingInterval);