feat: 重构 WebSocket Keep-Alive 客户端,添加连接和消息处理功能,更新依赖版本,增加 keep.ts 文件
This commit is contained in:
@@ -1,19 +1,23 @@
|
||||
import { resolvePath } from '@kevisual/use-config';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const entry = 'agent/opencode.ts';
|
||||
const naming = 'opencode';
|
||||
const external: string[] = ["bun"];
|
||||
await Bun.build({
|
||||
target: 'node',
|
||||
format: 'esm',
|
||||
entrypoints: [resolvePath(entry, { meta: import.meta })],
|
||||
outdir: resolvePath('./dist', { meta: import.meta }),
|
||||
naming: {
|
||||
entry: `${naming}.js`,
|
||||
},
|
||||
external,
|
||||
});
|
||||
const buildFn = async (opts: { entry?: string, naming?: string }) => {
|
||||
const entry = opts.entry || 'agent/opencode.ts';
|
||||
const naming = opts.naming || 'opencode';
|
||||
const external: string[] = ["bun"];
|
||||
await Bun.build({
|
||||
target: 'node',
|
||||
format: 'esm',
|
||||
entrypoints: [resolvePath(entry, { meta: import.meta })],
|
||||
outdir: resolvePath('./dist', { meta: import.meta }),
|
||||
naming: {
|
||||
entry: `${naming}.js`,
|
||||
},
|
||||
external,
|
||||
});
|
||||
const cmd = `dts -i ${entry} -o ${naming}.d.ts`;
|
||||
execSync(cmd);
|
||||
};
|
||||
|
||||
const cmd = 'dts -i agent/opencode.ts -o opencode.d.ts';
|
||||
execSync(cmd);
|
||||
await buildFn({ naming: 'opencode', entry: 'agent/opencode.ts' });
|
||||
await buildFn({ naming: 'keep', entry: 'src/keep.ts' });
|
||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kevisual/cnb",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -21,10 +21,12 @@
|
||||
"@kevisual/ai": "^0.0.24",
|
||||
"@kevisual/context": "^0.0.4",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@opencode-ai/plugin": "^1.1.39",
|
||||
"@types/bun": "^1.3.7",
|
||||
"@opencode-ai/plugin": "^1.1.44",
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/node": "^25.1.0",
|
||||
"dotenv": "^17.2.3"
|
||||
"@types/ws": "^8.18.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"ws": "npm:@kevisual/ws"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -40,6 +42,7 @@
|
||||
"exports": {
|
||||
".": "./mod.ts",
|
||||
"./opencode": "./dist/opencode.js",
|
||||
"./keep": "./dist/keep.js",
|
||||
"./src/*": "./src/*",
|
||||
"./agent/*": "./agent/*"
|
||||
}
|
||||
|
||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@@ -37,17 +37,23 @@ importers:
|
||||
specifier: ^0.0.12
|
||||
version: 0.0.12
|
||||
'@opencode-ai/plugin':
|
||||
specifier: ^1.1.39
|
||||
version: 1.1.39
|
||||
specifier: ^1.1.44
|
||||
version: 1.1.44
|
||||
'@types/bun':
|
||||
specifier: ^1.3.7
|
||||
version: 1.3.7
|
||||
specifier: ^1.3.8
|
||||
version: 1.3.8
|
||||
'@types/node':
|
||||
specifier: ^25.1.0
|
||||
version: 25.1.0
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
dotenv:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
ws:
|
||||
specifier: npm:@kevisual/ws
|
||||
version: '@kevisual/ws@8.19.0'
|
||||
|
||||
packages:
|
||||
|
||||
@@ -95,11 +101,15 @@ packages:
|
||||
peerDependencies:
|
||||
dotenv: ^17
|
||||
|
||||
'@opencode-ai/plugin@1.1.39':
|
||||
resolution: {integrity: sha512-PAdVYNZeRW9pCi4+t+Dp88hoPgEIiS5uJT31hUXLhZEfBvgaLNP38WhXwRNWbwSaNXudWOu/a5DzYNbU84uvHQ==}
|
||||
'@kevisual/ws@8.19.0':
|
||||
resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@opencode-ai/sdk@1.1.39':
|
||||
resolution: {integrity: sha512-EUYBZAci0bzG9+a7JVINmqAqis71ipG2/D3juvmvvKFyu0YBIT/6b+g3+p82Eb5CU2dujxpPdJJCaexZ1389eQ==}
|
||||
'@opencode-ai/plugin@1.1.44':
|
||||
resolution: {integrity: sha512-5w66Dq2Fugwgr2yrd8obvnlIEjBOuya82UgfR/3z3EzlyNDi2sitQSYbz7CcOtwd89eZ0n/tH/JX2KDGVuzxTQ==}
|
||||
|
||||
'@opencode-ai/sdk@1.1.44':
|
||||
resolution: {integrity: sha512-coQgtSSCbY46/GY+M5zG0rChiLSJWSjPERRt5L1hbjvDWvErelVV0ILPbd1+3CwJLFTedBYgotby2TcO8U0IfQ==}
|
||||
|
||||
'@rollup/plugin-commonjs@28.0.9':
|
||||
resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==}
|
||||
@@ -279,8 +289,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@types/bun@1.3.7':
|
||||
resolution: {integrity: sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA==}
|
||||
'@types/bun@1.3.8':
|
||||
resolution: {integrity: sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
@@ -291,8 +301,11 @@ packages:
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
|
||||
bun-types@1.3.7:
|
||||
resolution: {integrity: sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ==}
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
bun-types@1.3.8:
|
||||
resolution: {integrity: sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q==}
|
||||
|
||||
commondir@1.0.1:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
@@ -467,12 +480,14 @@ snapshots:
|
||||
'@kevisual/load': 0.0.6
|
||||
dotenv: 17.2.3
|
||||
|
||||
'@opencode-ai/plugin@1.1.39':
|
||||
'@kevisual/ws@8.19.0': {}
|
||||
|
||||
'@opencode-ai/plugin@1.1.44':
|
||||
dependencies:
|
||||
'@opencode-ai/sdk': 1.1.39
|
||||
'@opencode-ai/sdk': 1.1.44
|
||||
zod: 4.1.8
|
||||
|
||||
'@opencode-ai/sdk@1.1.39': {}
|
||||
'@opencode-ai/sdk@1.1.44': {}
|
||||
|
||||
'@rollup/plugin-commonjs@28.0.9(rollup@4.57.0)':
|
||||
dependencies:
|
||||
@@ -588,9 +603,9 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.57.0':
|
||||
optional: true
|
||||
|
||||
'@types/bun@1.3.7':
|
||||
'@types/bun@1.3.8':
|
||||
dependencies:
|
||||
bun-types: 1.3.7
|
||||
bun-types: 1.3.8
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
@@ -600,7 +615,11 @@ snapshots:
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
bun-types@1.3.7:
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
|
||||
bun-types@1.3.8:
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
|
||||
|
||||
1
src/keep.ts
Normal file
1
src/keep.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './workspace/keep-live.ts';
|
||||
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;
|
||||
}
|
||||
19
test/keep.ts
Normal file
19
test/keep.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// WebSocket Keep-Alive Client with node+ws
|
||||
import { createKeepAlive } from "../src/workspace/keep-live.ts";
|
||||
|
||||
const WS_URL = "wss://cnb-l6o-1jg7aoevl-001.cnb.space/stable-3c0b449c6e6e37b44a8a7938c0d8a3049926a64c?reconnectionToken=a6517530-9911-406b-a65f-0d9d4b3f0d6f&reconnection=false&skipWebSocketFrames=false";
|
||||
const COOKIE = "orange:workspace:cookie-session:cnb-l6o-1jg7aoevl-001=1ba3d696-1805-4c6b-b109-222738be570f";
|
||||
|
||||
// 使用库创建客户端
|
||||
const client = createKeepAlive({
|
||||
wsUrl: WS_URL,
|
||||
cookie: COOKIE,
|
||||
debug: true,
|
||||
});
|
||||
|
||||
// 监听解析后的消息
|
||||
client.onMessage((msg) => {
|
||||
console.log(`[Received] ${msg.raw.length} bytes`);
|
||||
});
|
||||
|
||||
console.log("开始激活 WebSocket 连接...");
|
||||
Reference in New Issue
Block a user