diff --git a/bun.config.ts b/bun.config.ts index 3ed43b5..595836e 100644 --- a/bun.config.ts +++ b/bun.config.ts @@ -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 }); \ No newline at end of file diff --git a/keep.ts b/keep.ts new file mode 100644 index 0000000..ecabb36 --- /dev/null +++ b/keep.ts @@ -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, +}); \ No newline at end of file diff --git a/package.json b/package.json index 9fc6f54..719aee0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6bca87..b8e4a93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/workspace/keep-live.ts b/src/workspace/keep-live.ts index 2c7edbe..fdd0e89 100644 --- a/src/workspace/keep-live.ts +++ b/src/workspace/keep-live.ts @@ -1,5 +1,16 @@ // WebSocket Keep-Alive Client Library -import WebSocket from "ws"; +// 运行时检测:Bun 使用原生 WebSocket,Node.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 = 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);