diff --git a/package.json b/package.json index 4ebbd5e..48a775f 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,12 @@ "@types/busboy": "^1.5.4", "@types/send": "^1.2.1", "@types/ws": "^8.18.1", - "bullmq": "^5.66.1", + "bullmq": "^5.66.2", "busboy": "^1.6.0", "commander": "^14.0.2", "cookie": "^1.1.1", "drizzle-orm": "^0.45.1", + "eventemitter3": "^5.0.1", "ioredis": "^5.8.2", "minio": "^8.0.6", "pg": "^8.16.3", @@ -68,7 +69,7 @@ "@kevisual/logger": "^0.0.4", "@kevisual/oss": "0.0.13", "@kevisual/permission": "^0.0.3", - "@kevisual/router": "0.0.40", + "@kevisual/router": "0.0.42", "@kevisual/types": "^0.0.10", "@kevisual/use-config": "^1.0.21", "@types/archiver": "^7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3383e2..fb58378 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: specifier: ^0.0.19 version: 0.0.19 '@kevisual/query': - specifier: ^0.0.32 - version: 0.0.32 + specifier: ^0.0.33 + version: 0.0.33 '@types/busboy': specifier: ^1.5.4 version: 1.5.4 @@ -28,8 +28,8 @@ importers: specifier: ^8.18.1 version: 8.18.1 bullmq: - specifier: ^5.66.0 - version: 5.66.0 + specifier: ^5.66.2 + version: 5.66.2 busboy: specifier: ^1.6.0 version: 1.6.0 @@ -42,6 +42,9 @@ importers: drizzle-orm: specifier: ^0.45.1 version: 0.45.1(pg@8.16.3) + eventemitter3: + specifier: ^5.0.1 + version: 5.0.1 ioredis: specifier: ^5.8.2 version: 5.8.2 @@ -83,8 +86,8 @@ importers: specifier: ^0.0.3 version: 0.0.3 '@kevisual/router': - specifier: 0.0.37 - version: 0.0.37 + specifier: 0.0.42 + version: 0.0.42 '@kevisual/types': specifier: ^0.0.10 version: 0.0.10 @@ -161,6 +164,27 @@ importers: specifier: ^4.2.1 version: 4.2.1 + wxmsg/pack-dist: + dependencies: + '@kevisual/context': + specifier: ^0.0.4 + version: 0.0.4 + '@kevisual/query': + specifier: ^0.0.29 + version: 0.0.29(@kevisual/ws@8.0.0)(zod@3.25.67) + '@kevisual/router': + specifier: 0.0.33 + version: 0.0.33 + '@types/node': + specifier: ^24.10.1 + version: 24.10.4 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + xml2js: + specifier: ^0.6.2 + version: 0.6.2 + packages: '@ioredis/commands@1.4.0': @@ -207,11 +231,14 @@ packages: '@kevisual/permission@0.0.3': resolution: {integrity: sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA==} + '@kevisual/query@0.0.29': + resolution: {integrity: sha512-rQZk0J073UuC1QGzuyq+pb4Y0hu8/Qx/xYHs9NbsmslM+RuMnd1zpXmvhXNj7Kn1MdYTH90ng2MlFLBkkQFaIg==} + '@kevisual/query@0.0.31': resolution: {integrity: sha512-bBdepjmMICLpcj/a9fnn82/0CGGYUZiCV+usWsJZKAwVlZcnj+WtKmbgKT09KpP6g3jjYzYOaXHiNFB8N0bQAQ==} - '@kevisual/query@0.0.32': - resolution: {integrity: sha512-9WN9cjmwSW8I5A0SqITdts9oxlLBGdPP7kJ8vwrxkaQteHS9FzxKuMBJxZzGKZdyte/zJDvdrE+lMf254BGbbg==} + '@kevisual/query@0.0.33': + resolution: {integrity: sha512-3w74bcLpwV3z483eg8n0DgkftfjWC6iLONXBvfyjW6IZf6jMOuouFaM4Rk+uEsTgElU6XGMKseNTp6dlQdWYkg==} '@kevisual/router@0.0.21': resolution: {integrity: sha512-XKTxbNO924cT18UOAGplWErZ+hMze8Y53F2jYCk18v4jsdsvjRho5uXXjJb6HSVsuITMtQR4R3rG0IcM3jkDKQ==} @@ -222,8 +249,11 @@ packages: '@kevisual/router@0.0.23': resolution: {integrity: sha512-W6ehlhAzNe58vq4QeQt2XFoO84Qaw34A0PVOByJsJ2ICj4YKBTclAt+rOAoISCvUeSbeNOIuhUE3sLyPfplzUw==} - '@kevisual/router@0.0.37': - resolution: {integrity: sha512-f/siDSqO0g6cQhBrWyPIVv8WMgxjC+olRS8GNxqzkBvAj5M4x3cmfAj1bxTn7neOejTjkGd+ZeoDQbhIpFKDZQ==} + '@kevisual/router@0.0.33': + resolution: {integrity: sha512-9z7TkSzCIGbXn9SuHPBdZpGwHlAuwA8iN5jNAZBUvbEvBRkBxlrbdCSe9fBYiAHueLm2AceFNrW74uulOiAkqA==} + + '@kevisual/router@0.0.42': + resolution: {integrity: sha512-6j254Hl1Q9uM4qKD4v6pcNSXVs7zwHZlyfSxUrNTWrgD7OCt/mrgBpzcNo0TM25/CsdrZCDs21kamienfYQ+lw==} '@kevisual/types@0.0.10': resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==} @@ -392,6 +422,9 @@ packages: '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + '@types/node@24.10.4': + resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} + '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} @@ -547,8 +580,8 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bullmq@5.66.0: - resolution: {integrity: sha512-LSe8yEiVTllOOq97Q0C/EhczKS5Yd0AUJleGJCIh0cyJE5nWUqEpGC/uZQuuAYniBSoMT8LqwrxE7N5MZVrLoQ==} + bullmq@5.66.2: + resolution: {integrity: sha512-0PrkpIakIntkBcPLltPIRWdLC1FTLUa/VhJkmEfobb5YUQjoUwJdmmf7HX+o/vMonS5048JpP+abf9lVRUFEjA==} busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} @@ -1331,6 +1364,18 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openai@5.23.2: + resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + p-queue@9.0.1: resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==} engines: {node: '>=20'} @@ -1574,6 +1619,10 @@ packages: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} + selfsigned@4.0.1: + resolution: {integrity: sha512-QVGzJryYPB7ctpYxoK4DDvH4kFf23wtBl0s6km/wN+JCWLqjutdyWaiXHwSg3B3ftDOFfu0B7FWRVNH0xNn6rw==} + engines: {node: '>=10'} + selfsigned@5.2.0: resolution: {integrity: sha512-QKF6fsJgdKn5Cy1SekTuwUQ6LOLUdnO4gVne1+TGSAyL/CD1aznNCmBna+bQB9xyon7DiYbDtTR1cLaxDXuaVA==} engines: {node: '>=15.6.0'} @@ -2059,9 +2108,16 @@ snapshots: '@kevisual/permission@0.0.3': {} + '@kevisual/query@0.0.29(@kevisual/ws@8.0.0)(zod@3.25.67)': + dependencies: + openai: 5.23.2(@kevisual/ws@8.0.0)(zod@3.25.67) + transitivePeerDependencies: + - ws + - zod + '@kevisual/query@0.0.31': {} - '@kevisual/query@0.0.32': {} + '@kevisual/query@0.0.33': {} '@kevisual/router@0.0.21': dependencies: @@ -2081,7 +2137,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@kevisual/router@0.0.37': + '@kevisual/router@0.0.33': + dependencies: + path-to-regexp: 8.3.0 + selfsigned: 4.0.1 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@kevisual/router@0.0.42': dependencies: path-to-regexp: 8.3.0 selfsigned: 5.2.0 @@ -2328,6 +2392,10 @@ snapshots: dependencies: '@types/node': 25.0.3 + '@types/node@24.10.4': + dependencies: + undici-types: 7.16.0 + '@types/node@25.0.3': dependencies: undici-types: 7.16.0 @@ -2482,7 +2550,7 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bullmq@5.66.0: + bullmq@5.66.2: dependencies: cron-parser: 4.9.0 ioredis: 5.8.2 @@ -3166,6 +3234,11 @@ snapshots: dependencies: wrappy: 1.0.2 + openai@5.23.2(@kevisual/ws@8.0.0)(zod@3.25.67): + optionalDependencies: + ws: '@kevisual/ws@8.0.0' + zod: 3.25.67 + p-queue@9.0.1: dependencies: eventemitter3: 5.0.1 @@ -3473,6 +3546,10 @@ snapshots: '@types/node-forge': 1.3.11 node-forge: 1.3.1 + selfsigned@4.0.1: + dependencies: + node-forge: 1.3.1 + selfsigned@5.2.0: dependencies: '@peculiar/x509': 1.14.2 diff --git a/src/index.ts b/src/index.ts index 1566f15..21a53d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,6 +57,6 @@ const main = () => { }); }; -setTimeout(() => { - main(); -}, 1200); \ No newline at end of file +// setTimeout(() => { +// main(); +// }, 1200); \ No newline at end of file diff --git a/src/modules/ws-proxy/index.ts b/src/modules/ws-proxy/index.ts index ace5eb8..e3dcac3 100644 --- a/src/modules/ws-proxy/index.ts +++ b/src/modules/ws-proxy/index.ts @@ -5,7 +5,7 @@ import { getLoginUser } from '@/modules/auth.ts'; import { logger } from '../logger.ts'; export const wsProxyManager = new WsProxyManager(); -export const upgrade = async (request: any, socket: any, head: any) => { +export const upgrade = (request: any, socket: any, head: any) => { const req = request as any; const url = new URL(req.url, 'http://localhost'); const id = url.searchParams.get('id'); @@ -13,6 +13,7 @@ export const upgrade = async (request: any, socket: any, head: any) => { console.log('upgrade', request.url, id); wss.handleUpgrade(req, socket, head, (ws) => { // 这里手动触发 connection 事件 + console.log('emitting connection event'); // @ts-ignore wss.emit('connection', ws, req); }); @@ -28,15 +29,21 @@ export const wss = new WebSocketServer({ wss.on('connection', async (ws, req) => { console.log('connected', req.url); const url = new URL(req.url, 'http://localhost'); - const id = url?.searchParams?.get('id') || nanoid(); + const _id = url?.searchParams?.get('id'); + const id = _id || nanoid(); const loginUser = await getLoginUser(req); if (!loginUser) { - ws.send(JSON.stringify({ code: 401, message: 'No Login' })); - ws.close(); + console.log('未登录,断开连接'); + ws.send(JSON.stringify({ code: 401, message: '未登录' })); + setTimeout(() => { + ws.close(); + }, 1000); return; } - const user = loginUser.tokenUser?.username; - wsProxyManager.register(id, { user, ws }); + const user = loginUser.tokenUser.username; + const userApp = user + '-' + id; + console.log('注册 ws 连接', userApp); + wsProxyManager.register(userApp, { user, ws }); ws.send( JSON.stringify({ type: 'connected', @@ -54,16 +61,103 @@ wss.on('connection', async (ws, req) => { }); ws.on('close', () => { logger.debug('ws closed'); - wsProxyManager.unregister(id, user); + wsProxyManager.unregister(userApp); }); }); export class WssApp { wss: WebSocketServer; + bunWSS = websocket; constructor() { this.wss = wss; } upgrade(request: any, socket: any, head: any) { - return upgrade(request, socket, head); + // return upgrade(request, socket, head); + return bunUpgrade(request); } } + +export const bunUpgrade = (request: Request) => { + const url = new URL(request.url, 'http://localhost'); + const isUpgrade = url.pathname === '/ws/proxy'; + if (isUpgrade) { + console.log('upgrade', request.url); + + // 使用 Bun 原生 WebSocket + new Response(null, { + status: 101, + headers: { + 'Upgrade': 'websocket', + }, + }); + return true; + } + + return false; +}; +// Bun WebSocket 处理器 +export const websocket = { + async open(ws: any) { + console.log('WebSocket opened'); + const { url, token } = ws.data; + + const urlObj = new URL(url, 'http://localhost'); + const _id = urlObj.searchParams.get('id'); + const id = _id || nanoid(); + + // 创建一个模拟的 request 对象用于认证 + const mockReq: any = { + url: url, + headers: { + authorization: token ? `Bearer ${token}` : undefined, + }, + }; + + const loginUser = await getLoginUser(mockReq); + + if (!loginUser) { + console.log('未登录,断开连接'); + ws.send(JSON.stringify({ code: 401, message: '未登录' })); + ws.close(); + return; + } + + const user = loginUser.tokenUser.username; + const userApp = user + '-' + id; + console.log('注册 ws 连接', userApp); + + ws.data.userApp = userApp; + ws.data.user = user; + + wsProxyManager.register(userApp, { user, ws }); + + ws.send( + JSON.stringify({ + type: 'connected', + user: user, + id, + }), + ); + }, + + async message(ws: any, message: string) { + try { + const data = JSON.parse(message); + logger.debug('message', data); + } catch (error) { + logger.error('Failed to parse message', error); + } + }, + + close(ws: any) { + const { userApp } = ws.data; + logger.debug('ws closed', userApp); + if (userApp) { + wsProxyManager.unregister(userApp); + } + }, + + error(ws: any, error: Error) { + console.error('WebSocket error:', error); + }, +}; \ No newline at end of file diff --git a/src/modules/ws-proxy/manager.ts b/src/modules/ws-proxy/manager.ts index 2edc3b2..4ea9324 100644 --- a/src/modules/ws-proxy/manager.ts +++ b/src/modules/ws-proxy/manager.ts @@ -1,12 +1,27 @@ import { nanoid } from 'nanoid'; import { WebSocket } from 'ws'; import { logger } from '../logger.ts'; +import { EventEmitter } from 'eventemitter3'; class WsMessage { ws: WebSocket; user?: string; + emitter: EventEmitter;; constructor({ ws, user }: WssMessageOptions) { this.ws = ws; this.user = user; + this.emitter = new EventEmitter(); + this.listenMessage(); + } + async listenMessage() { + this.ws.on('message', (event: Buffer) => { + const eventData = event.toString(); + if (!eventData) { + return; + } + const data = JSON.parse(eventData); + logger.debug('ws-proxy listenMessage', data); + this.emitter.emit(data.id, data.data); + }); } async sendData(data: any, opts?: { timeout?: number }) { if (this.ws.readyState !== WebSocket.OPEN) { @@ -21,23 +36,17 @@ class WsMessage { }); logger.info('ws-proxy sendData', message); this.ws.send(message); + const msg = { path: data?.path, key: data?.key, id: data?.id }; return new Promise((resolve) => { const timer = setTimeout(() => { resolve({ code: 500, - message: 'timeout', + message: `运行超时,执行的id: ${id},参数是${JSON.stringify(msg)}`, }); }, timeout); - this.ws.once('message', (event: Buffer) => { - const eventData = event.toString(); - if (!eventData) { - return; - } - const data = JSON.parse(eventData); - if (data.id === id) { - resolve(data.data); - clearTimeout(timer); - } + this.emitter.once(id, (data: any) => { + resolve(data); + clearTimeout(timer); }); }); } @@ -48,37 +57,28 @@ type WssMessageOptions = { }; export class WsProxyManager { wssMap: Map = new Map(); - constructor() {} - getId(id: string, user?: string) { - return id + '/' + user; - } + constructor() { } register(id: string, opts?: { ws: WebSocket; user: string }) { - const _id = this.getId(id, opts?.user || ''); - if (this.wssMap.has(_id)) { - const value = this.wssMap.get(_id); + if (this.wssMap.has(id)) { + const value = this.wssMap.get(id); if (value) { value.ws.close(); } } const value = new WsMessage({ ws: opts?.ws, user: opts?.user }); - this.wssMap.set(_id, value); + this.wssMap.set(id, value); } - unregister(id: string, user?: string) { - const _id = this.getId(id, user || ''); - const value = this.wssMap.get(_id); + unregister(id: string) { + const value = this.wssMap.get(id); if (value) { value.ws.close(); } - this.wssMap.delete(_id); + this.wssMap.delete(id); } getIds() { return Array.from(this.wssMap.keys()); } - get(id: string, user?: string) { - if (user) { - const _id = this.getId(id, user); - return this.wssMap.get(_id); - } + get(id: string) { return this.wssMap.get(id); } } diff --git a/src/modules/ws-proxy/proxy.ts b/src/modules/ws-proxy/proxy.ts index 70dd08b..ebe2a3c 100644 --- a/src/modules/ws-proxy/proxy.ts +++ b/src/modules/ws-proxy/proxy.ts @@ -27,7 +27,7 @@ export const UserV1Proxy = async (req: IncomingMessage, res: ServerResponse, opt return false; } logger.debug('data', data); - const client = wsProxyManager.get(userAppKey, user); + const client = wsProxyManager.get(userAppKey); const ids = wsProxyManager.getIds(); if (!client) { opts?.createNotFoundPage?.(`未找到应用, ${userAppKey}, ${ids.join(',')}`); diff --git a/src/route.ts b/src/route.ts index e656195..d4488b4 100644 --- a/src/route.ts +++ b/src/route.ts @@ -166,3 +166,14 @@ app } }) .addTo(app); + + +app.route({ + path: 'system', + key: 'version' +}).define(async (ctx) => { + ctx.body = { + version: '0.0.1', + name: 'KeVisual Backend System', + } +}).addTo(app); \ No newline at end of file