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' import { buildWithBun } from '@kevisual/code-builder'
await buildWithBun({ naming: 'opencode', entry: 'agent/opencode.ts', dts: true }); 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 }); 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", "name": "@kevisual/cnb",
"version": "0.0.25", "version": "0.0.26",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -22,9 +22,9 @@
"@kevisual/ai": "^0.0.24", "@kevisual/ai": "^0.0.24",
"@kevisual/code-builder": "^0.0.6", "@kevisual/code-builder": "^0.0.6",
"@kevisual/dts": "^0.0.3", "@kevisual/dts": "^0.0.3",
"@kevisual/context": "^0.0.4", "@kevisual/context": "^0.0.6",
"@kevisual/types": "^0.0.12", "@kevisual/types": "^0.0.12",
"@opencode-ai/plugin": "^1.2.1", "@opencode-ai/plugin": "^1.2.4",
"@types/bun": "^1.3.9", "@types/bun": "^1.3.9",
"@types/node": "^25.2.3", "@types/node": "^25.2.3",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",

28
pnpm-lock.yaml generated
View File

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

View File

@@ -1,5 +1,16 @@
// WebSocket Keep-Alive Client Library // 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 { export interface KeepAliveConfig {
wsUrl: string; wsUrl: string;
@@ -31,6 +42,7 @@ export class WSKeepAlive {
private pingTimer: NodeJS.Timeout | null = null; private pingTimer: NodeJS.Timeout | null = null;
private messageHandlers: Set<MessageHandler> = new Set(); private messageHandlers: Set<MessageHandler> = new Set();
private url: URL; private url: URL;
private readonly isBun: boolean;
constructor(config: KeepAliveConfig) { constructor(config: KeepAliveConfig) {
this.config = { this.config = {
@@ -48,6 +60,7 @@ export class WSKeepAlive {
debug: config.debug ?? false, debug: config.debug ?? false,
}; };
this.url = new URL(this.config.wsUrl); this.url = new URL(this.config.wsUrl);
this.isBun = typeof Bun !== 'undefined';
} }
private log(message: string) { private log(message: string) {
@@ -107,44 +120,91 @@ export class WSKeepAlive {
} }
}); });
this.ws.on("open", () => { if (this.isBun) {
debug && this.log("Connected!"); // Bun 环境:使用标准 Web API
this.reconnectAttempts = 0; const ws = this.ws as any;
this.config.onConnect(); ws.onopen = () => {
this.startPing(); debug && this.log("Connected!");
}); this.reconnectAttempts = 0;
this.config.onConnect();
this.startPing();
};
this.ws.on("message", (data: any) => { ws.onmessage = async (event: MessageEvent) => {
if (Buffer.isBuffer(data)) { let data: Buffer | string;
const parsed = this.parseMessage(data);
this.config.onMessage(parsed?.raw ?? data);
this.messageHandlers.forEach(handler => { if (event.data instanceof Blob) {
if (parsed) handler(parsed); data = Buffer.from(await event.data.arrayBuffer());
}); } else if (event.data instanceof ArrayBuffer) {
} else { data = Buffer.from(event.data);
this.config.onMessage(data); } else if (typeof event.data === 'string') {
} data = event.data;
}); } else {
data = Buffer.from(event.data);
}
this.ws.on("close", (code: number) => { this.handleMessage(data);
debug && this.log(`Disconnected (code: ${code})`); };
this.stopPing();
this.config.onDisconnect(code);
this.handleReconnect();
});
this.ws.on("error", (err: Error) => { ws.onclose = (event: CloseEvent) => {
debug && this.log(`Error: ${err.message}`); debug && this.log(`Disconnected (code: ${event.code})`);
this.config.onError(err); 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() { private startPing() {
this.stopPing(); this.stopPing();
this.pingTimer = setInterval(() => { this.pingTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) { 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.log("Sent ping");
} }
}, this.config.pingInterval); }, this.config.pingInterval);