diff --git a/plugins/nodejs-websocket-bridge/GUIDE.md b/plugins/nodejs-websocket-bridge/GUIDE.md new file mode 100644 index 000000000000..c5b21d407381 --- /dev/null +++ b/plugins/nodejs-websocket-bridge/GUIDE.md @@ -0,0 +1,151 @@ +# FastGPT 前端工具调用集成指南 + +## 概述 + +本指南介绍如何实现工作流来调用前端工具,以达到Agent真的在帮助用户进行页面操作,填写表单等效果。 + +## 架构说明 + +1. **集成流程**: + - FastGPT通过iframe嵌入到您的页面 + - 工作流中的工具调用组件可调用前端功能(如读取DOM、触发点击) + - Proxy组件通过chatId维持前后端连接 + - 工作流通过HTTP调用Proxy,Proxy通过WebSocket与前端通信 + +2. **数据流向**: + - 工作流 → HTTP → Proxy → WebSocket → share页面 + - share页面 → postMessage → 父页面执行函数 → postMessage回传 → WebSocket → Proxy → HTTP → 工作流 + +## 实现步骤 + +### 1. 实现消息处理器 + +在您的父页面中添加以下消息处理代码: + +```javascript +const messageHandler = (event) => { + // 验证消息来源(根据实际域名修改) + // const trustedOrigin = 'http://localhost:3000'; + // if (event.origin !== trustedOrigin) return; + + // 处理不同类型的消息 + const iframe = document.querySelector('iframe'); + switch (event.data.type) { + case 'functionCall': + function makeResponse(result, requestId) { + iframe?.contentWindow?.postMessage({ + type: 'functionCallResponse', + requestId, + result + }, '*'); // 生产环境应替换为实际的trustedOrigin + } + try { + let obj = event.data.value; + if (typeof obj === 'string') { + obj = JSON.parse(obj); + } + const requestId = obj.requestId; + const tool_name = obj.tool_name; + const tool_param = obj.tool_param; + + // 示例工具调用处理 + if (tool_name === 'get_dom') { + try { + const pageContent = document.querySelector(tool_param); + if (pageContent) { + const DOM = pageContent.innerHTML; + makeResponse(DOM, requestId); + } + } catch (error) { + makeResponse('Error processing page content: ' + error, requestId); + } + } + + // 添加其他工具处理逻辑 + // else if (tool_name === '其他工具名') {...} + + } catch (error) { + console.error('Error parsing JSON:', error); + } + break; + } +}; + +window.addEventListener('message', messageHandler); +``` + +### 2. 配置Nginx转发 + +添加以下配置到您的Nginx(share页面将通过该接口建立websocket连接): + +```nginx +location /ws-proxy { + # 将/ws-proxy路径重写为根路径,以便WebSocket服务器能正确处理 + rewrite ^/ws-proxy(/.*)$ $1 break; + + proxy_pass http://localhost:3003; + proxy_http_version 1.1; + + # WebSocket特定设置 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # WebSocket连接不应该被Nginx缓存服务器自动关闭 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} +``` + +### 3. 嵌入FastGPT iframe + +将FastGPT的iframe嵌入到您的页面: + + +### 4. 工作流中配置HTTP节点 + +在FastGPT工作流中创建HTTP节点,配置如下: + +- 请求方式: POST +- 请求地址: http://localhost:3003/execute-tool +- 请求内容: +```json +{ + "chatId": "{{$VARIABLE_NODE_ID.chatId$}}", // 选择变量,当前对话ID + "tool_name": "get_dom", // 工具名称 + "tool_param": "#root" // 工具参数,此处为DOM选择器 +} +``` + +工作流将通过HTTP节点将请求发送到proxy服务,proxy再通过WebSocket与前端通信,实现工具调用。 + +## 注意事项 + +1. 目前proxy设置的HTTP超时设为5分钟,请确保在此时间内完成响应 +2. 生产环境中请设置正确的trustedOrigin以增强安全性 +3. 根据需要扩展messageHandler以支持更多工具调用类型 + +## 工具实现示例 + +### DOM读取工具 +如上述代码所示,实现了get_dom工具 + +### 按钮点击工具示例 +```javascript +if (tool_name === 'click_button') { + try { + const buttonSelector = tool_param.selector; + const button = document.querySelector(buttonSelector); + if (button) { + button.click(); + makeResponse('Button clicked successfully', requestId); + } else { + makeResponse('Button not found', requestId); + } + } catch (error) { + makeResponse('Error clicking button: ' + error, requestId); + } +} +``` diff --git a/plugins/nodejs-websocket-bridge/README.md b/plugins/nodejs-websocket-bridge/README.md new file mode 100644 index 000000000000..4d18a8e91795 --- /dev/null +++ b/plugins/nodejs-websocket-bridge/README.md @@ -0,0 +1,269 @@ +# WebSocket Bridge - Complete Guide + +This document provides a detailed guide on how to use the WebSocket Bridge application to communicate between your server and web clients. + +## Project Structure + +``` +nodejs-websocket-bridge/ +├── public/ # Static files +│ ├── index.html # Simple test client +│ ├── example.html # Advanced example integration +│ └── client-library.js # Client-side library for WebSocket Bridge +├── server.js # Main server implementation +├── test-client.js # Command-line tool for testing +├── package.json # Node.js package configuration +└── README.md # Project overview +``` + +## Architecture Overview + +The WebSocket Bridge acts as a mediator between backend systems and web clients: + +``` +┌────────────┐ ┌───────────────────┐ ┌─────────────┐ +│ │ │ │ │ │ +│ Backend │──────▶│ WebSocket Bridge │──────▶│ Web Client │ +│ System │ │ Server │ │ │ +│ │◀──────│ │◀──────│ │ +└────────────┘ └───────────────────┘ └─────────────┘ + HTTP API WebSocket Server Browser Tab +``` + +1. Backend systems send tool execution requests to the HTTP API +2. The server routes these requests to the appropriate web client via WebSocket +3. The web client executes the tool and sends the result back +4. The server returns the result to the backend through the HTTP response + +## Server Setup + +### Starting the Server + +```bash +npm start +``` + +The server runs on port 3003 by default. You can change this by setting the `PORT` environment variable: + +```bash +PORT=8080 npm start +``` + +### HTTP API + +#### Execute Tool + +Send a tool execution request to a specific web client: + +**Endpoint:** `POST /execute-tool` + +**Request Body:** +```json +{ + "chatId": "unique-chat-identifier", + "tool_name": "name-of-tool-to-execute", + "tool_param": { + "param1": "value1", + "param2": "value2" + } +} +``` + +**Response:** +```json +{ + "result": { + // Tool execution result returned from the web client + } +} +``` + +**Error Response:** +```json +{ + "error": "Error message" +} +``` + +Common error codes: +- `400` - Missing required parameters +- `404` - No active connection found for chatId +- `504` - Request timed out (default timeout: 30 seconds) + +#### Connection Status + +Get information about active WebSocket connections: + +**Endpoint:** `GET /connections` + +**Response:** +```json +{ + "totalConnections": 5, + "activeChats": 3, + "connections": { + "chat-id-1": 2, + "chat-id-2": 1, + "chat-id-3": 2 + } +} +``` + +## Web Client Integration + +### WebSocket Connection + +Web clients need to connect to the WebSocket server with a `chatId` parameter: + +```javascript +const ws = new WebSocket('ws://localhost:3003?chatId=unique-chat-id'); +``` + +### Using the Client Library + +The easiest way to integrate is using the provided client library: + +```html + + +``` + +### Client Library Methods + +- `connect()` - Connect to the WebSocket server +- `disconnect()` - Disconnect from the WebSocket server +- `registerToolHandler(toolName, handler)` - Register a handler for a specific tool + +### Tool Handler Function + +Tool handler functions receive parameters from the server and should return a result: + +```javascript +async function myToolHandler(params) { + // Do something with the parameters + const result = { + success: true, + message: 'Tool executed successfully', + data: { + // Tool-specific result data + } + }; + + return result; +} +``` + +The handler function can also throw an error, which will be sent back to the server: + +```javascript +async function errorHandler(params) { + throw new Error('Something went wrong'); +} +``` + +## Testing + +### Browser Test Client + +A browser-based test client is available at http://localhost:3003/. + +1. Open the page in a browser +2. Enter a Chat ID and click "Connect" +3. Once connected, the server can send tool execution requests to this client + +### Command-line Test Client + +A command-line test client is included for testing the HTTP API: + +```bash +node test-client.js +``` + +This client will prompt for: +1. Chat ID +2. Tool name +3. Tool parameters (as JSON) + +It will then send a request to the server and display the response. + +### Example Integration + +An advanced example integration is available at http://localhost:3003/example.html. + +This example demonstrates: +1. Connection management +2. Tool registration +3. Error handling +4. Example tool implementations + +## Advanced Configuration + +### WebSocket Connection Timeouts + +The server has a 30-second timeout for tool execution. This can be modified in the server.js file: + +```javascript +// Change timeout for tool execution (in milliseconds) +const timeout = 60000; // 60 seconds +``` + +### Client Reconnection + +The client library automatically attempts to reconnect if the connection is lost. You can configure this behavior: + +```javascript +const client = new WebSocketBridgeClient({ + chatId: 'unique-chat-id', + reconnectInterval: 5000, // Time between reconnection attempts (ms) + maxReconnectAttempts: 10 // Maximum number of reconnection attempts +}); +``` + +## Handling High Concurrency + +The server is designed to handle thousands of simultaneous connections. For very high loads, consider: + +1. Using a load balancer +2. Implementing a Redis backend for connection tracking +3. Horizontal scaling with sticky sessions + +## Security Considerations + +1. **Authentication**: The current implementation does not include authentication. In production, implement a token-based auth system. +2. **HTTPS/WSS**: Always use secure connections in production. +3. **Input Validation**: Always validate input parameters both on server and client. +4. **Rate Limiting**: Implement rate limiting to prevent abuse. + +## Deployment + +For production deployment: + +1. Use process manager like PM2 + ```bash + npm install -g pm2 + pm2 start server.js + ``` + +2. Set up a reverse proxy (Nginx or Apache) +3. Use HTTPS for all connections +4. Set up proper monitoring and logging diff --git a/plugins/nodejs-websocket-bridge/package-lock.json b/plugins/nodejs-websocket-bridge/package-lock.json new file mode 100644 index 000000000000..3b23a2d80f07 --- /dev/null +++ b/plugins/nodejs-websocket-bridge/package-lock.json @@ -0,0 +1,912 @@ +{ + "name": "nodejs-websocket-bridge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nodejs-websocket-bridge", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.21.2", + "node-fetch": "^2.7.0", + "uuid": "^11.1.0", + "ws": "^8.18.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/plugins/nodejs-websocket-bridge/package.json b/plugins/nodejs-websocket-bridge/package.json new file mode 100644 index 000000000000..465c6d903228 --- /dev/null +++ b/plugins/nodejs-websocket-bridge/package.json @@ -0,0 +1,19 @@ +{ + "name": "nodejs-websocket-bridge", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "express": "^4.21.2", + "node-fetch": "^2.7.0", + "uuid": "^11.1.0", + "ws": "^8.18.1" + } +} diff --git a/plugins/nodejs-websocket-bridge/public/architecture.html b/plugins/nodejs-websocket-bridge/public/architecture.html new file mode 100644 index 000000000000..1f89a212a5cb --- /dev/null +++ b/plugins/nodejs-websocket-bridge/public/architecture.html @@ -0,0 +1,173 @@ + + + + + + WebSocket Bridge Architecture + + + +
+

WebSocket Bridge Architecture

+ +
+ +
+

Backend System

+

Sends HTTP requests with tool execution parameters

+
+ +
+

WebSocket Bridge Server

+

Routes tool execution requests to web clients and returns results

+
+ +
+

Web Client

+

Executes tools and returns results via WebSocket

+
+ + +
+
+
HTTP POST
+ + +
+
+
WebSocket
+ + +
+
+
Result
+ + +
+
+
HTTP Response
+ + +
+ chatId identifies the connection +
+
+ + +
+ requestId tracks the request/response +
+
+
+ +
+

How It Works

+ +
+ 1. Connection Establishment: Web clients connect to the WebSocket server with a unique chatId. +
+ +
+ 2. Tool Execution Request: The backend system sends an HTTP POST request to the /execute-tool endpoint with chatId, tool_name, and tool_param. +
+ +
+ 3. Message Routing: The server generates a unique requestId, finds the WebSocket connection matching the chatId, and forwards the request. +
+ +
+ 4. Tool Execution: The web client executes the requested tool with the provided parameters. +
+ +
+ 5. Result Return: The web client sends the result back to the server via the WebSocket connection, including the requestId. +
+ +
+ 6. Response Completion: The server matches the requestId to the pending HTTP request and sends the result as the HTTP response. +
+
+
+ + diff --git a/plugins/nodejs-websocket-bridge/public/client-library.js b/plugins/nodejs-websocket-bridge/public/client-library.js new file mode 100644 index 000000000000..8f219b786aeb --- /dev/null +++ b/plugins/nodejs-websocket-bridge/public/client-library.js @@ -0,0 +1,255 @@ +/** + * WebSocket Bridge Client Library + * + * This library provides an easy way to connect web applications to the WebSocket Bridge server. + * It handles connection management, reconnection, message processing, and tool execution. + */ +class WebSocketBridgeClient { + /** + * Create a new WebSocket Bridge client + * @param {Object} config - Configuration options + * @param {string} config.chatId - Unique identifier for this chat session + * @param {string} [config.url] - WebSocket server URL (defaults to current host) + * @param {number} [config.reconnectInterval=3000] - Time in ms between reconnection attempts + * @param {number} [config.maxReconnectAttempts=5] - Maximum number of reconnection attempts + * @param {Function} [config.onConnected] - Callback when connection is established + * @param {Function} [config.onDisconnected] - Callback when connection is lost + * @param {Function} [config.onReconnecting] - Callback when attempting to reconnect + * @param {Function} [config.onError] - Callback for connection errors + */ + constructor(config) { + if (!config || !config.chatId) { + throw new Error('ChatId is required'); + } + + // Configuration + this.chatId = config.chatId; + this.serverUrl = config.url || this._getDefaultServerUrl(); + this.reconnectInterval = config.reconnectInterval || 3000; + this.maxReconnectAttempts = config.maxReconnectAttempts || 5; + + // Callbacks + this.onConnected = config.onConnected || (() => {}); + this.onDisconnected = config.onDisconnected || (() => {}); + this.onReconnecting = config.onReconnecting || (() => {}); + this.onError = config.onError || (() => {}); + + // State + this.ws = null; + this.isConnected = false; + this.reconnectAttempts = 0; + this.reconnectTimer = null; + this.toolHandlers = {}; + + // Bind methods to this + this.connect = this.connect.bind(this); + this.disconnect = this.disconnect.bind(this); + this.registerToolHandler = this.registerToolHandler.bind(this); + this._handleMessage = this._handleMessage.bind(this); + this._reconnect = this._reconnect.bind(this); + } + + /** + * Get the default WebSocket server URL based on the current page location + * @private + * @returns {string} WebSocket URL + */ + _getDefaultServerUrl() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const host = window.location.hostname; + const port = window.location.port || (protocol === 'wss:' ? '443' : '80'); + return `${protocol}//${host}:${port}`; + } + + /** + * Connect to the WebSocket server + * @returns {Promise} Resolves when connected, rejects on error + */ + connect() { + return new Promise((resolve, reject) => { + if (this.ws && this.isConnected) { + resolve(); + return; + } + + // Clean up any existing connection + if (this.ws) { + this.ws.close(); + this.ws = null; + } + + try { + const url = `${this.serverUrl}?chatId=${encodeURIComponent(this.chatId)}`; + this.ws = new WebSocket(url); + + // Set up a timeout for initial connection + const connectionTimeout = setTimeout(() => { + if (!this.isConnected) { + reject(new Error('Connection timeout')); + this.ws.close(); + } + }, 10000); + + // Connection opened + this.ws.addEventListener('open', () => { + clearTimeout(connectionTimeout); + this.isConnected = true; + this.reconnectAttempts = 0; + console.log(`[WebSocketBridge] Connected with chatId: ${this.chatId}`); + this.onConnected(); + resolve(); + }); + + // Connection closed + this.ws.addEventListener('close', () => { + clearTimeout(connectionTimeout); + const wasConnected = this.isConnected; + this.isConnected = false; + + if (wasConnected) { + console.log('[WebSocketBridge] Connection closed'); + this.onDisconnected(); + } + + this._reconnect(); + }); + + // Connection error + this.ws.addEventListener('error', (error) => { + console.error('[WebSocketBridge] Connection error:', error); + this.onError(error); + + // Only reject if we're in the initial connection + if (!this.isConnected) { + clearTimeout(connectionTimeout); + reject(error); + } + }); + + // Listen for messages + this.ws.addEventListener('message', (event) => { + this._handleMessage(event.data); + }); + } catch (error) { + console.error('[WebSocketBridge] Failed to create WebSocket:', error); + reject(error); + this._reconnect(); + } + }); + } + + /** + * Disconnect from the WebSocket server + */ + disconnect() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + + if (this.ws) { + this.ws.close(); + this.ws = null; + } + + this.isConnected = false; + } + + /** + * Attempt to reconnect to the server after connection loss + * @private + */ + _reconnect() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + } + + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.log('[WebSocketBridge] Max reconnection attempts reached'); + return; + } + + this.reconnectAttempts++; + console.log(`[WebSocketBridge] Reconnecting (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); + this.onReconnecting(this.reconnectAttempts); + + this.reconnectTimer = setTimeout(() => { + this.connect().catch(() => { + // If connection fails, _reconnect will be called again by the close event + }); + }, this.reconnectInterval); + } + + /** + * Register a handler for a specific tool + * @param {string} toolName - Name of the tool to handle + * @param {Function} handler - Function that takes tool parameters and returns a promise + */ + registerToolHandler(toolName, handler) { + if (typeof handler !== 'function') { + throw new Error('Tool handler must be a function'); + } + this.toolHandlers[toolName] = handler; + } + + /** + * Handle incoming WebSocket messages + * @private + * @param {string} data - Raw message data + */ + _handleMessage(data) { + try { + const message = JSON.parse(data); + + // Check if this is a tool execution request + if (message.requestId && message.tool_name) { + this._handleToolExecution(message); + } + } catch (error) { + console.error('[WebSocketBridge] Error parsing message:', error); + } + } + + /** + * Handle tool execution requests + * @private + * @param {Object} request - Tool execution request + */ + async _handleToolExecution(request) { + const { requestId, tool_name, tool_param } = request; + console.log(`[WebSocketBridge] Tool execution request: ${tool_name}`); + + // Prepare the response structure + const response = { + requestId + }; + + try { + // Check if we have a handler for this tool + if (this.toolHandlers[tool_name]) { + // Execute the tool handler with the provided parameters + const result = await this.toolHandlers[tool_name](tool_param); + response.result = result; + } else { + throw new Error(`No handler registered for tool: ${tool_name}`); + } + } catch (error) { + console.error(`[WebSocketBridge] Error executing tool ${tool_name}:`, error); + response.error = error.message || 'Tool execution failed'; + } + + // Send the response if we're connected + if (this.isConnected && this.ws) { + this.ws.send(JSON.stringify(response)); + } else { + console.error('[WebSocketBridge] Cannot send response: not connected'); + } + } +} + +// Export for both browser and Node.js environments +if (typeof module !== 'undefined' && module.exports) { + module.exports = WebSocketBridgeClient; +} else { + window.WebSocketBridgeClient = WebSocketBridgeClient; +} diff --git a/plugins/nodejs-websocket-bridge/public/example.html b/plugins/nodejs-websocket-bridge/public/example.html new file mode 100644 index 000000000000..abaaedae420f --- /dev/null +++ b/plugins/nodejs-websocket-bridge/public/example.html @@ -0,0 +1,316 @@ + + + + + + WebSocket Bridge Example Integration + + + +

WebSocket Bridge Example

+ +
+ Disconnected +
+ +
+

Connection Settings

+
+ + + + +
+
+ +
+

Registered Tools

+
+

The following tools are registered and can be executed by the server:

+ +

getTime

+

Returns the current browser time.

+ +
+ +

calculateSum

+

Adds two numbers together.

+ +
+ +

fetchUserDetails

+

Simulates fetching user details (with delay).

+ +
+
+
+ +
+

Event Log

+
+
+ + + + + diff --git a/plugins/nodejs-websocket-bridge/public/index.html b/plugins/nodejs-websocket-bridge/public/index.html new file mode 100644 index 000000000000..ce27af065ccb --- /dev/null +++ b/plugins/nodejs-websocket-bridge/public/index.html @@ -0,0 +1,215 @@ + + + + + + WebSocket Tool Client + + + +
+

WebSocket Tool Client

+ +
+

Connection

+
Disconnected
+ +
+ + +
+ + + +
+ +
+

Tool Execution Log

+
+
+
+ + + + diff --git a/plugins/nodejs-websocket-bridge/server.js b/plugins/nodejs-websocket-bridge/server.js new file mode 100644 index 000000000000..3ba5ad73ba29 --- /dev/null +++ b/plugins/nodejs-websocket-bridge/server.js @@ -0,0 +1,170 @@ +const express = require('express'); +const http = require('http'); +const WebSocket = require('ws'); +const { v4: uuidv4 } = require('uuid'); +const url = require('url'); +const path = require('path'); + +// Create Express application +const app = express(); +app.use(express.json()); + +// Serve static files from the "public" directory +app.use(express.static(path.join(__dirname, 'public'))); + +// Create HTTP server +const server = http.createServer(app); + +// Create WebSocket server +const wss = new WebSocket.Server({ server }); + +// Store connections by chatId +const connections = {}; + +// Store pending requests with their response handlers +const pendingRequests = {}; + +// WebSocket connection handling +wss.on('connection', (ws, req) => { + // Extract chatId from query parameters + const parameters = url.parse(req.url, true).query; + const chatId = parameters.chatId; + + if (!chatId) { + console.log('Connection attempt without chatId - closing connection'); + ws.close(); + return; + } + + console.log(`New WebSocket connection established for chatId: ${chatId}`); + + // Store the connection with its chatId + if (!connections[chatId]) { + connections[chatId] = []; + } + connections[chatId].push(ws); + + // Handle incoming messages from the web client + ws.on('message', (message) => { + try { + const data = JSON.parse(message); + const { requestId, result, error } = data; + + // Check if there's a pending request with this ID + if (pendingRequests[requestId]) { + const { res, timer } = pendingRequests[requestId]; + + // Clear the timeout timer + clearTimeout(timer); + + // Send the response back through the HTTP endpoint + if (error) { + res.status(500).json({ error }); + } else { + res.json({ result }); + } + + // Remove the pending request + delete pendingRequests[requestId]; + } else { + console.log(`Received response for unknown request: ${requestId}`); + } + } catch (error) { + console.error('Error processing WebSocket message:', error); + } + }); + + // Handle connection close + ws.on('close', () => { + console.log(`WebSocket connection closed for chatId: ${chatId}`); + + // Remove the connection from the store + if (connections[chatId]) { + const index = connections[chatId].indexOf(ws); + if (index !== -1) { + connections[chatId].splice(index, 1); + } + + // Clean up if no connections left for this chatId + if (connections[chatId].length === 0) { + delete connections[chatId]; + } + } + }); + + // Handle errors + ws.on('error', (error) => { + console.error(`WebSocket error for chatId ${chatId}:`, error); + }); +}); + +// HTTP endpoint to trigger tool execution +app.post('/execute-tool', (req, res) => { + const { chatId, tool_name, tool_param } = req.body; + + // Validate required parameters + if (!chatId || !tool_name) { + return res.status(400).json({ error: 'Missing required parameters: chatId and tool_name are required' }); + } + + // Check if there's a WebSocket connection for this chatId + if (!connections[chatId] || connections[chatId].length === 0) { + return res.status(404).json({ error: `No active connection found for chatId: ${chatId}` }); + } + + // Generate a unique request ID + const requestId = uuidv4(); + + // Prepare the message to send to the client + const message = JSON.stringify({ + requestId, + tool_name, + tool_param + }); + + // Set a timeout for the response (30 seconds) + const timeout = 300000; + const timer = setTimeout(() => { + // If the request times out, remove it and send an error response + if (pendingRequests[requestId]) { + res.status(504).json({ error: 'Request timed out' }); + delete pendingRequests[requestId]; + } + }, timeout); + + // Store the pending request with its response handler + pendingRequests[requestId] = { res, timer }; + + // Send the message to all connections for this chatId + // (typically there should be only one, but handling multiple just in case) + connections[chatId].forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(message); + } + }); + + console.log(`Tool execution request sent to chatId ${chatId}: ${tool_name}`); + + // Note: The response will be sent when we receive the result from the WebSocket +}); + +// Get information about active connections +app.get('/connections', (req, res) => { + const connectionInfo = {}; + + for (const chatId in connections) { + connectionInfo[chatId] = connections[chatId].length; + } + + res.json({ + totalConnections: Object.values(connections).reduce((sum, conns) => sum + conns.length, 0), + activeChats: Object.keys(connections).length, + connections: connectionInfo + }); +}); + +// Start the server +const PORT = process.env.WS_PORT || 3003; +server.listen(PORT, () => { + console.log(`WebSocket Bridge Server running on port ${PORT}`); +}); diff --git a/plugins/nodejs-websocket-bridge/test-client.js b/plugins/nodejs-websocket-bridge/test-client.js new file mode 100644 index 000000000000..6869456fdf1f --- /dev/null +++ b/plugins/nodejs-websocket-bridge/test-client.js @@ -0,0 +1,88 @@ +const fetch = require('node-fetch'); +const readline = require('readline'); + +const API_URL = 'http://localhost:3003/execute-tool'; + +// Create readline interface for command line input +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// Function to execute a tool +async function executeTool(chatId, toolName, toolParams) { + try { + console.log(`Executing tool "${toolName}" for chat "${chatId}" with params:`, toolParams); + + const response = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + chatId, + tool_name: toolName, + tool_param: toolParams + }) + }); + + const data = await response.json(); + + if (response.ok) { + console.log('\nTool execution response:'); + console.log(JSON.stringify(data, null, 2)); + } else { + console.error('\nError executing tool:'); + console.error(JSON.stringify(data, null, 2)); + } + } catch (error) { + console.error('\nRequest failed:', error.message); + } +} + +// Main function to prompt for input and execute tool +function startPrompt() { + rl.question('\nEnter chat ID: ', (chatId) => { + if (!chatId) { + console.log('Chat ID is required. Please try again.'); + return startPrompt(); + } + + rl.question('Enter tool name: ', (toolName) => { + if (!toolName) { + console.log('Tool name is required. Please try again.'); + return startPrompt(); + } + + rl.question('Enter tool parameters as JSON (or press enter for none): ', (paramsStr) => { + let toolParams = {}; + + if (paramsStr) { + try { + toolParams = JSON.parse(paramsStr); + } catch (error) { + console.log('Invalid JSON. Using empty parameters.'); + } + } + + executeTool(chatId, toolName, toolParams) + .then(() => { + rl.question('\nDo you want to execute another tool? (y/n): ', (answer) => { + if (answer.toLowerCase() === 'y') { + startPrompt(); + } else { + console.log('Exiting...'); + rl.close(); + } + }); + }); + }); + }); + }); +} + +console.log('=== WebSocket Bridge Test Client ==='); +console.log('Make sure the server is running at http://localhost:3003'); + +// Start the prompt +startPrompt(); diff --git a/projects/app/Dockerfile b/projects/app/Dockerfile index 5142d120798f..0838eb3a1f37 100644 --- a/projects/app/Dockerfile +++ b/projects/app/Dockerfile @@ -77,6 +77,10 @@ COPY --from=maindeps /app/node_modules/@zilliz/milvus2-sdk-node ./node_modules/@ # copy package.json to version file COPY --from=builder /app/projects/app/package.json ./package.json +# copy nodejs-websocket-bridge +COPY ./plugins/nodejs-websocket-bridge /app/plugins/nodejs-websocket-bridge +RUN cd /app/plugins/nodejs-websocket-bridge && npm install + # copy config COPY ./projects/app/data /app/data RUN chown -R nextjs:nodejs /app/data @@ -86,12 +90,17 @@ RUN chown -R nextjs:nodejs /app/data ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=3000 +ENV WS_PORT=3003 ENV NEXT_PUBLIC_BASE_URL=$base_url +ENV serverPath=./projects/app/server.js EXPOSE 3000 +EXPOSE 3003 -USER nextjs +# Copy the start script +COPY ./projects/app/start.sh /app/start.sh +RUN chmod +x /app/start.sh -ENV serverPath=./projects/app/server.js +USER nextjs -ENTRYPOINT ["sh","-c","node --max-old-space-size=4096 ${serverPath}"] \ No newline at end of file +ENTRYPOINT ["/app/start.sh"] \ No newline at end of file diff --git a/projects/app/src/pages/chat/share.tsx b/projects/app/src/pages/chat/share.tsx index 5cfd52856b73..8ba8176ac856 100644 --- a/projects/app/src/pages/chat/share.tsx +++ b/projects/app/src/pages/chat/share.tsx @@ -54,6 +54,77 @@ type Props = { showNodeStatus: boolean; }; +const getWebSocketServerURL = () => { + if (typeof window !== 'undefined') { + // 兼容HTTP和HTTPS协议 + return window.location.origin.replace(/^http/, 'ws').replace(/^https/, 'wss') + '/ws-proxy'; + } + return ''; +}; + +// WebSocket hook for managing the connection +const useWebSocketConnection = (chatId: string) => { + const [socket, setSocket] = useState(null); + const [isConnected, setIsConnected] = useState(false); + + // Function to handle sending messages through WebSocket + const sendMessage = useCallback( + (data: any) => { + if (socket && isConnected) { + socket.send(JSON.stringify(data)); + return true; + } + return false; + }, + [socket, isConnected] + ); + + // Connect to WebSocket when chatId changes + useEffect(() => { + if (!chatId) return; + + // 获取WebSocket服务器URL + const wsUrl = getWebSocketServerURL(); + if (!wsUrl) return; // 如果URL为空,不建立连接 + + // Close previous connection if exists + if (socket) { + socket.close(); + setSocket(null); + setIsConnected(false); + } + + // Create new WebSocket connection + const ws = new WebSocket(`${wsUrl}?chatId=${chatId}`); + + ws.onopen = () => { + console.log(`WebSocket connected for chatId: ${chatId}`); + setIsConnected(true); + }; + + ws.onclose = () => { + console.log(`WebSocket disconnected for chatId: ${chatId}`); + setIsConnected(false); + }; + + ws.onerror = (error) => { + console.error('WebSocket error:', error); + setIsConnected(false); + }; + + setSocket(ws); + + // Cleanup function to close WebSocket when component unmounts or chatId changes + return () => { + if (ws) { + ws.close(); + } + }; + }, [chatId]); + + return { socket, isConnected, sendMessage }; +}; + const OutLink = (props: Props) => { const { t } = useTranslation(); const router = useRouter(); @@ -74,6 +145,9 @@ const OutLink = (props: Props) => { const { isPc } = useSystem(); const { outLinkAuthData, appId, chatId } = useChatStore(); + // Initialize WebSocket connection + const { socket, isConnected, sendMessage } = useWebSocketConnection(chatId); + const isOpenSlider = useContextSelector(ChatContext, (v) => v.isOpenSlider); const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); const forbidLoadChat = useContextSelector(ChatContext, (v) => v.forbidLoadChat); @@ -129,6 +203,54 @@ const OutLink = (props: Props) => { } }, [data, isChatRecordsLoaded]); + // Handle WebSocket messages + useEffect(() => { + if (!socket) return; + + socket.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + console.log('WebSocket message received:', data); + + // Convert WebSocket message to postMessage + if (window !== top) { + window.top?.postMessage( + { + type: 'functionCall', + value: data + }, + '*' + ); + } + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + }, [socket]); + + // Handle postMessage events + useEffect(() => { + if (window !== top) { + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === 'functionCallResponse') { + // Handle response to WebSocket message + if (isConnected && sendMessage) { + sendMessage({ + type: 'tool_response', + result: event.data.result, + requestId: event.data.requestId + }); + } + } + }; + + window.addEventListener('message', handleMessage); + return () => { + window.removeEventListener('message', handleMessage); + }; + } + }, [isConnected, sendMessage]); + const startChat = useCallback( async ({ messages, diff --git a/projects/app/start.sh b/projects/app/start.sh new file mode 100755 index 000000000000..689f1ec12bbd --- /dev/null +++ b/projects/app/start.sh @@ -0,0 +1,3 @@ +#!/bin/sh +cd /app/plugins/nodejs-websocket-bridge && node server.js & +node --max-old-space-size=4096 ${serverPath}