From aba5d172bdbca4b74dcb15d25e76d5210caedf96 Mon Sep 17 00:00:00 2001 From: buua436 Date: Tue, 23 Jun 2026 17:45:31 +0800 Subject: [PATCH] feat: add whatsapp web qr chat channel (#16238) Adds a WhatsApp chat channel backed by a QR-based web login flow so users can connect without manual token setup. --- api/apps/restful_apis/chat_channel_api.py | 44 +- api/channels/bootstrap.py | 28 +- api/channels/whatsapp/__init__.py | 1 + api/channels/whatsapp/channel.py | 393 ++++ api/channels/whatsapp/gateway-node/README.md | 34 + api/channels/whatsapp/gateway-node/index.js | 533 ++++++ .../whatsapp/gateway-node/package-lock.json | 1677 +++++++++++++++++ .../whatsapp/gateway-node/package.json | 12 + api/channels/whatsapp/gateway.py | 173 ++ .../chat-channel/add-channel-modal.tsx | 90 +- .../component/added-channel-card.tsx | 115 +- .../chat-channel/constant/index.tsx | 55 +- .../pages/user-setting/chat-channel/hooks.ts | 9 +- .../pages/user-setting/chat-channel/index.tsx | 1 + web/src/services/chat-channel-service.ts | 3 + web/src/utils/api.ts | 2 + 16 files changed, 3160 insertions(+), 10 deletions(-) create mode 100644 api/channels/whatsapp/__init__.py create mode 100644 api/channels/whatsapp/channel.py create mode 100644 api/channels/whatsapp/gateway-node/README.md create mode 100644 api/channels/whatsapp/gateway-node/index.js create mode 100644 api/channels/whatsapp/gateway-node/package-lock.json create mode 100644 api/channels/whatsapp/gateway-node/package.json create mode 100644 api/channels/whatsapp/gateway.py diff --git a/api/apps/restful_apis/chat_channel_api.py b/api/apps/restful_apis/chat_channel_api.py index f39a2597e2..93e389a8aa 100644 --- a/api/apps/restful_apis/chat_channel_api.py +++ b/api/apps/restful_apis/chat_channel_api.py @@ -33,7 +33,7 @@ def _chat_channel_auth_error(channel_id: str, user_id: str): @manager.route("/chat-channels", methods=["POST"]) # noqa: F821 @login_required -@validate_request("name", "channel", "config") +@validate_request("name", "channel") async def create_chat_channel(): """Create a chat channel bot owned by the current tenant.""" req = await get_request_json() @@ -42,7 +42,7 @@ async def create_chat_channel(): "tenant_id": current_user.id, "name": req["name"], "channel": req["channel"], - "config": req["config"], + "config": req.get("config") or {}, "chat_id": req.get("chat_id") or None } ChatChannelService.insert(**channel) @@ -115,3 +115,43 @@ def rm_chat_channel(channel_id): ChatChannelService.delete_by_id(channel_id) return get_json_result(data=True) + + +@manager.route("/chat-channels//runtime", methods=["GET"]) # noqa: F821 +@login_required +def get_chat_channel_runtime(channel_id): + """Return live runtime metadata for a running chat channel.""" + if not ChatChannelService.accessible(channel_id, current_user.id): + return _chat_channel_auth_error(channel_id, current_user.id) + + e, conn = ChatChannelService.get_by_id(channel_id) + if not e: + return get_data_error_result(message="Can't find this chat channel!") + + if conn.channel != "whatsapp": + return get_data_error_result(message="Runtime snapshot is only available for WhatsApp.") + + try: + from api.channels.whatsapp.channel import get_runtime_snapshot + except Exception as ex: + LOGGER.error("failed to load whatsapp runtime helper: %s", ex, exc_info=True) + return get_data_error_result(message="WhatsApp runtime is unavailable.") + + snapshot = get_runtime_snapshot(channel_id) + if snapshot is None: + return get_json_result( + data={ + "account_id": channel_id, + "session_key": channel_id, + "status": "waiting", + "connected_at": None, + "qr_updated_at": None, + "qr_data_url": None, + "last_error": None, + "session_id": None, + "last_snapshot_at": None, + "gateway_base_url": None, + "event_cursor": 0, + } + ) + return get_json_result(data=snapshot) diff --git a/api/channels/bootstrap.py b/api/channels/bootstrap.py index db86c2dc03..212e137d2d 100644 --- a/api/channels/bootstrap.py +++ b/api/channels/bootstrap.py @@ -33,7 +33,16 @@ import threading LOGGER = logging.getLogger(__name__) # Channel packages bundled under api/channels that self-register on import. -_BUNDLED_CHANNELS = ("feishu", "discord", "telegram", "line", "wecom", "qqbot", "dingtalk") +_BUNDLED_CHANNELS = ( + "feishu", + "discord", + "telegram", + "line", + "wecom", + "qqbot", + "dingtalk", + "whatsapp", +) # How often (seconds) to reconcile running channels against the database. _RECONCILE_INTERVAL_SECS = 10 @@ -144,7 +153,10 @@ def _make_chat_handler(ch): answer_text = "" try: - async for ans in async_chat(dia, history, False, quote=False): + chat_kwargs = {"quote": False} + if "{knowledge}" in (dia.prompt_config or {}).get("system", ""): + chat_kwargs["knowledge"] = "" + async for ans in async_chat(dia, history, False, **chat_kwargs): structure_answer(conv, ans, message_id, conv.id) answer_text = (ans or {}).get("answer", "") or "" ConversationService.update_by_id(conv.id, conv.to_dict()) @@ -228,6 +240,18 @@ async def _reconcile(running: dict, failed: dict) -> None: if account_id not in desired or desired[account_id][2] != failed[account_id]: failed.pop(account_id, None) + active_whatsapp = any(channel == "whatsapp" for channel, _, _ in desired.values()) + if not active_whatsapp: + active_whatsapp = any( + entry["ch"].channel_id == "whatsapp" for entry in running.values() + ) + from api.channels.whatsapp.gateway import sync_whatsapp_gateway + + try: + await sync_whatsapp_gateway(active_whatsapp) + except Exception: + LOGGER.exception("failed to sync WhatsApp gateway enabled=%s", active_whatsapp) + # Start channels that are new (skip ones already known to fail with this config). for account_id, (channel, credential, fp) in desired.items(): if account_id in running or failed.get(account_id) == fp: diff --git a/api/channels/whatsapp/__init__.py b/api/channels/whatsapp/__init__.py new file mode 100644 index 0000000000..0b15b0677b --- /dev/null +++ b/api/channels/whatsapp/__init__.py @@ -0,0 +1 @@ +from . import channel # noqa: F401 diff --git a/api/channels/whatsapp/channel.py b/api/channels/whatsapp/channel.py new file mode 100644 index 0000000000..20988cfb39 --- /dev/null +++ b/api/channels/whatsapp/channel.py @@ -0,0 +1,393 @@ +from __future__ import annotations + +import asyncio +import json +import logging +import time +from dataclasses import dataclass +from typing import Any, Optional + +import aiohttp + +from ..core.base import Channel, IncomingMessage, OutgoingMessage +from ..core.registry import register_channel + +LOGGER = logging.getLogger(__name__) + +WHATSAPP_DEFAULT_TIMEOUT_SECS = 30 +WHATSAPP_DEFAULT_SESSION_KEY = "default" +WHATSAPP_MESSAGE_TTL_SECS = 3600 +WHATSAPP_GATEWAY_START_RETRY_SECS = 30 +WHATSAPP_GATEWAY_RECONNECT_SECS = 3 + + +@dataclass +class WhatsAppAccount: + account_id: str + gateway_base_url: str + gateway_token: str = "" + session_key: str = "" + timeout_secs: int = WHATSAPP_DEFAULT_TIMEOUT_SECS + + +_live_channels: dict[str, "WhatsAppChannel"] = {} + + +def _default_gateway_base_url() -> str: + return "http://127.0.0.1:3005" + + +def get_runtime_snapshot(account_id: str) -> dict[str, Any] | None: + channel = _live_channels.get(account_id) + if channel is None: + return None + return channel.get_status_snapshot() + + +class WhatsAppChannel(Channel): + channel_id = "whatsapp" + + def __init__(self, account: WhatsAppAccount) -> None: + super().__init__() + self.account = account + self.account_id = account.account_id + self._task: Optional[asyncio.Task] = None + self._stop_event = asyncio.Event() + self._lifecycle_lock = asyncio.Lock() + self._http_session: Optional[aiohttp.ClientSession] = None + self._status: str = "stopped" + self._last_error: str = "" + self._qr_data_url: str = "" + self._qr_updated_at: float = 0.0 + self._connected_at: float = 0.0 + self._last_snapshot_at: float = 0.0 + self._session_id: str = "" + self._event_cursor: int = 0 + self._seen_message_ids: dict[str, float] = {} + + def _session_key(self) -> str: + value = str(self.account.session_key or "").strip() + return value or WHATSAPP_DEFAULT_SESSION_KEY + + def _gateway_base_url(self) -> str: + return str(self.account.gateway_base_url or "").strip().rstrip("/") + + def _gateway_url(self, path: str) -> str: + base_url = self._gateway_base_url() + if not base_url: + raise ValueError("WhatsApp gateway_base_url is required") + return f"{base_url}/{path.lstrip('/')}" + + def _events_ws_url(self) -> str: + base_url = self._gateway_base_url() + if not base_url: + raise ValueError("WhatsApp gateway_base_url is required") + if base_url.startswith("http://"): + ws_base = f"ws://{base_url.removeprefix('http://')}" + elif base_url.startswith("https://"): + ws_base = f"wss://{base_url.removeprefix('https://')}" + else: + ws_base = base_url + return ( + f"{ws_base}/whatsapp/{self._session_key()}/events/ws" + f"?after={self._event_cursor}" + ) + + def _gateway_headers(self) -> dict[str, str]: + token = str(self.account.gateway_token or "").strip() + if not token: + return {} + if token.lower().startswith("bearer "): + return {"Authorization": token} + return {"Authorization": f"Bearer {token}"} + + async def _ensure_http_session(self) -> aiohttp.ClientSession: + if self._http_session is not None and not self._http_session.closed: + return self._http_session + timeout = aiohttp.ClientTimeout(total=max(int(self.account.timeout_secs), 1)) + self._http_session = aiohttp.ClientSession(timeout=timeout) + return self._http_session + + async def _request_json( + self, + method: str, + path: str, + payload: dict[str, Any] | None = None, + ) -> dict[str, Any]: + session = await self._ensure_http_session() + url = self._gateway_url(path) + async with session.request( + method, + url, + json=payload, + headers=self._gateway_headers(), + ) as resp: + text = await resp.text() + if resp.status >= 400: + raise RuntimeError(f"status: {resp.status}, response: {text}") + if not text.strip(): + return {} + content_type = resp.headers.get("content-type", "") + if "application/json" not in content_type.lower(): + raise RuntimeError( + f"unexpected response content-type: {content_type or 'unknown'}, response: {text[:200]}" + ) + try: + return await resp.json() + except Exception as ex: + raise RuntimeError(f"invalid json response: {text[:200]}") from ex + + async def _request_json_with_retry( + self, + method: str, + path: str, + payload: dict[str, Any] | None = None, + ) -> dict[str, Any]: + deadline = time.time() + WHATSAPP_GATEWAY_START_RETRY_SECS + last_error: Exception | None = None + while time.time() < deadline and not self._stop_event.is_set(): + try: + return await self._request_json(method, path, payload) + except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as ex: + last_error = ex + await asyncio.sleep(0.5) + if last_error is not None: + raise last_error + return await self._request_json(method, path, payload) + + async def start(self) -> None: + async with self._lifecycle_lock: + if self._task and not self._task.done(): + return + self._stop_event.clear() + _live_channels[self.account_id] = self + self._task = asyncio.create_task(self._run(), name=f"whatsapp-{self.account_id}") + LOGGER.info("[whatsapp:%s] starting gateway client", self.account_id) + + async def stop(self) -> None: + async with self._lifecycle_lock: + self._stop_event.set() + task = self._task + if task and not task.done(): + task.cancel() + try: + await task + except (asyncio.CancelledError, Exception): + pass + try: + await self._request_json( + "POST", + f"whatsapp/{self._session_key()}/stop", + {"account_id": self.account_id, "session_key": self._session_key()}, + ) + except Exception: + LOGGER.debug("[whatsapp:%s] gateway stop failed", self.account_id, exc_info=True) + + try: + if self._http_session is not None and not self._http_session.closed: + await self._http_session.close() + finally: + self._http_session = None + + _live_channels.pop(self.account_id, None) + self._task = None + self._status = "stopped" + self._last_error = "" + self._qr_data_url = "" + self._qr_updated_at = 0.0 + self._connected_at = 0.0 + self._last_snapshot_at = 0.0 + self._session_id = "" + self._event_cursor = 0 + self._seen_message_ids.clear() + + async def send(self, message: OutgoingMessage) -> None: + if not message.chat_id: + LOGGER.error("[whatsapp:%s] missing chat_id; cannot send", self.account_id) + return + try: + LOGGER.info( + "[whatsapp:%s] sending reply chat_id=%s reply_to=%s text_preview=%r", + self.account_id, + message.chat_id, + message.reply_to_message_id, + message.text[:120], + ) + await self._request_json( + "POST", + f"whatsapp/{self._session_key()}/send", + { + "account_id": self.account_id, + "session_key": self._session_key(), + "chat_id": message.chat_id, + "text": message.text, + "reply_to_message_id": message.reply_to_message_id, + }, + ) + LOGGER.info( + "[whatsapp:%s] message sent chat_id=%s reply_to=%s", + self.account_id, + message.chat_id, + message.reply_to_message_id, + ) + except Exception: + LOGGER.error("[whatsapp:%s] send failed", self.account_id, exc_info=True) + + async def _run(self) -> None: + self._status = "connecting" + self._last_error = "" + try: + await self._request_json_with_retry( + "POST", + f"whatsapp/{self._session_key()}/start", + { + "account_id": self.account_id, + "session_key": self._session_key(), + "gateway_base_url": self._gateway_base_url(), + }, + ) + while not self._stop_event.is_set(): + try: + await self._run_events_ws() + except asyncio.CancelledError: + raise + except Exception as ex: + self._status = "error" + self._last_error = str(ex) + LOGGER.error("[whatsapp:%s] gateway event loop error", self.account_id, exc_info=True) + if not self._stop_event.is_set(): + await asyncio.sleep(WHATSAPP_GATEWAY_RECONNECT_SECS) + except asyncio.CancelledError: + pass + except Exception as ex: + self._status = "error" + self._last_error = str(ex) + LOGGER.error("[whatsapp:%s] gateway runtime error", self.account_id, exc_info=True) + finally: + if not self._stop_event.is_set() and self._status != "error": + self._status = "disconnected" + if not self._last_error: + self._last_error = "WhatsApp gateway stopped unexpectedly." + self._last_snapshot_at = time.time() + + def _apply_snapshot(self, snapshot: dict[str, Any]) -> None: + self._last_snapshot_at = time.time() + self._status = str(snapshot.get("status") or "connecting") + self._last_error = str(snapshot.get("last_error") or "") + self._qr_data_url = str(snapshot.get("qr_data_url") or "") + self._qr_updated_at = float(snapshot.get("qr_updated_at") or 0.0) + self._connected_at = float(snapshot.get("connected_at") or 0.0) + self._session_id = str(snapshot.get("session_id") or self._session_id or "") + self._event_cursor = max(self._event_cursor, int(snapshot.get("event_cursor") or 0)) + if self._status == "connected": + self._last_error = "" + + async def _run_events_ws(self) -> None: + session = await self._ensure_http_session() + url = self._events_ws_url() + async with session.ws_connect(url, headers=self._gateway_headers(), heartbeat=None) as ws: + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + await self._handle_ws_payload(msg.data) + elif msg.type == aiohttp.WSMsgType.ERROR: + raise RuntimeError(f"whatsapp events websocket error: {ws.exception()}") + LOGGER.warning("[whatsapp:%s] events websocket closed", self.account_id) + + async def _handle_ws_payload(self, payload: str) -> None: + try: + obj = json.loads(payload) + except json.JSONDecodeError: + LOGGER.warning( + "[whatsapp:%s] ignored invalid websocket payload: %r", + self.account_id, + payload[:200], + ) + return + + kind = str(obj.get("type") or "").strip() + data = obj.get("data") + if kind == "snapshot" and isinstance(data, dict): + self._apply_snapshot(data) + return + if kind == "event" and isinstance(data, dict): + await self._handle_event_item(data) + return + + async def _handle_event_item(self, item: dict[str, Any]) -> None: + seq = int(item.get("seq") or 0) + if seq > self._event_cursor: + self._event_cursor = seq + if item.get("kind") != "message": + return + message_id = str(item.get("message_id") or "").strip() + if not message_id: + return + self._prune_seen_message_ids() + if self._is_seen_message(message_id): + return + self._seen_message_ids[message_id] = time.time() + incoming = IncomingMessage( + channel=self.channel_id, + account_id=self.account_id, + chat_id=str(item.get("chat_id") or ""), + chat_type=str(item.get("chat_type") or "p2p"), + message_id=message_id, + sender_id=str(item.get("sender_id") or ""), + text=str(item.get("text") or ""), + raw=item.get("raw"), + ) + LOGGER.info( + "[whatsapp:%s] inbound message_id=%s chat_id=%s", + self.account_id, + message_id, + incoming.chat_id, + ) + await self._dispatch(incoming) + + def _is_seen_message(self, message_id: str) -> bool: + return message_id in self._seen_message_ids + + def _prune_seen_message_ids(self) -> None: + cutoff = time.time() - WHATSAPP_MESSAGE_TTL_SECS + stale = [key for key, ts in self._seen_message_ids.items() if ts < cutoff] + for key in stale: + self._seen_message_ids.pop(key, None) + + def get_status_snapshot(self) -> dict[str, Any]: + return { + "account_id": self.account_id, + "session_key": self._session_key(), + "status": self._status, + "connected_at": self._connected_at or None, + "qr_updated_at": self._qr_updated_at or None, + "qr_data_url": self._qr_data_url or None, + "last_error": self._last_error or None, + "session_id": self._session_id or None, + "last_snapshot_at": self._last_snapshot_at or None, + "gateway_base_url": self._gateway_base_url() or None, + "event_cursor": self._event_cursor, + } + + +def _build(account_id: str, cfg: dict) -> Channel: + gateway_base_url = str( + cfg.get("gateway_base_url") + or cfg.get("gateway_url") + or cfg.get("control_url") + or _default_gateway_base_url() + ) + gateway_token = str(cfg.get("gateway_token") or cfg.get("token") or "") + session_key = str(cfg.get("session_key") or cfg.get("session_id") or account_id) + timeout_secs = int(cfg.get("timeout_secs") or WHATSAPP_DEFAULT_TIMEOUT_SECS) + return WhatsAppChannel( + WhatsAppAccount( + account_id=account_id, + gateway_base_url=gateway_base_url, + gateway_token=gateway_token, + session_key=session_key, + timeout_secs=timeout_secs, + ) + ) + + +register_channel("whatsapp", _build) diff --git a/api/channels/whatsapp/gateway-node/README.md b/api/channels/whatsapp/gateway-node/README.md new file mode 100644 index 0000000000..94adcda720 --- /dev/null +++ b/api/channels/whatsapp/gateway-node/README.md @@ -0,0 +1,34 @@ +# RAGFlow WhatsApp Gateway + +This directory contains a minimal WhatsApp gateway built on top of +`@whiskeysockets/baileys`. + +## Install + +```bash +cd api/channels/whatsapp/gateway-node +npm install +``` + +## Run + +```bash +WHATSAPP_GATEWAY_PORT=3005 \ +WHATSAPP_GATEWAY_DATA_DIR=~/.ragflow/whatsapp-gateway \ +npm start +``` + +## API + +- `POST /whatsapp/:sessionKey/start` +- `GET /whatsapp/:sessionKey/status` +- `GET /whatsapp/:sessionKey/events/ws?after=` (WebSocket) +- `POST /whatsapp/:sessionKey/send` +- `POST /whatsapp/:sessionKey/stop` + +## Notes + +- Authentication state is persisted under `WHATSAPP_GATEWAY_DATA_DIR`. +- Scan the QR code exposed in `status.qr_data_url`. +- RAGFlow polls `status` and `events` and forwards inbound messages to the + connected assistant. diff --git a/api/channels/whatsapp/gateway-node/index.js b/api/channels/whatsapp/gateway-node/index.js new file mode 100644 index 0000000000..aed2cc1f3b --- /dev/null +++ b/api/channels/whatsapp/gateway-node/index.js @@ -0,0 +1,533 @@ +import http from 'node:http'; +import crypto from 'node:crypto'; +import os from 'node:os'; +import path from 'node:path'; +import { mkdir } from 'node:fs/promises'; + +import makeWASocket, { + Browsers, + DisconnectReason, + fetchLatestBaileysVersion, + useMultiFileAuthState, +} from 'baileys'; +import QRCode from 'qrcode'; + +const PORT = Number.parseInt(process.env.WHATSAPP_GATEWAY_PORT || '3005', 10); +const HOST = process.env.WHATSAPP_GATEWAY_HOST || '127.0.0.1'; +const AUTH_TOKEN = String(process.env.WHATSAPP_GATEWAY_TOKEN || '').trim(); +const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; +const DATA_DIR = + process.env.WHATSAPP_GATEWAY_DATA_DIR || + path.join(os.homedir(), '.ragflow', 'whatsapp-gateway'); + +function now() { + return Date.now() / 1000; +} + +function normalizeJid(chatId) { + const raw = String(chatId || '').trim(); + if (!raw) { + return ''; + } + if (raw.includes('@')) { + return raw; + } + const digits = raw.replace(/\D/g, ''); + if (!digits) { + return ''; + } + return `${digits}@s.whatsapp.net`; +} + +function detectChatType(jid) { + if (jid.endsWith('@g.us')) { + return 'group'; + } + if (jid.endsWith('@newsletter')) { + return 'channel'; + } + return 'dm'; +} + +function extractText(message) { + if (!message) { + return ''; + } + return ( + message.conversation || + message.extendedTextMessage?.text || + message.imageMessage?.caption || + message.videoMessage?.caption || + message.documentMessage?.caption || + message.buttonsResponseMessage?.selectedButtonId || + message.listResponseMessage?.title || + '' + ).trim(); +} + +function safeMessageKey(message) { + return { + remoteJid: message.key?.remoteJid || '', + fromMe: Boolean(message.key?.fromMe), + id: message.key?.id || '', + participant: message.key?.participant || '', + }; +} + +function buildWsFrame(text) { + const data = Buffer.from(String(text), 'utf8'); + let header; + if (data.length < 126) { + header = Buffer.alloc(2); + header[1] = data.length; + } else if (data.length < 65536) { + header = Buffer.alloc(4); + header[1] = 126; + header.writeUInt16BE(data.length, 2); + } else { + header = Buffer.alloc(10); + header[1] = 127; + header.writeBigUInt64BE(BigInt(data.length), 2); + } + header[0] = 0x81; + return Buffer.concat([header, data]); +} + +function sendWsText(socket, payload) { + socket.write(buildWsFrame(JSON.stringify(payload))); +} + +function isAuthorized(req) { + if (!AUTH_TOKEN) { + return true; + } + const auth = String(req.headers.authorization || '').trim(); + return auth === `Bearer ${AUTH_TOKEN}`; +} + +class WhatsAppSession { + constructor(sessionKey) { + this.sessionKey = sessionKey; + this.sessionDir = path.join(DATA_DIR, sessionKey); + this.status = 'stopped'; + this.lastError = ''; + this.qrDataUrl = ''; + this.qrUpdatedAt = 0; + this.connectedAt = 0; + this.sessionId = ''; + this.authRegistered = false; + this.lastSnapshotAt = 0; + this.eventSeq = 0; + this.events = []; + this.messageStore = new Map(); + this.sock = null; + this.saveCreds = null; + this.starting = null; + this.stopping = false; + this.restartTimer = null; + this.subscribers = new Set(); + } + + addSubscriber(socket, afterSeq) { + const subscriber = { socket }; + this.subscribers.add(subscriber); + socket.on('close', () => { + this.subscribers.delete(subscriber); + }); + socket.on('error', () => { + this.subscribers.delete(subscriber); + }); + + sendWsText(socket, { type: 'snapshot', data: this.snapshot() }); + const backlog = this.listEvents(afterSeq); + for (const event of backlog.items) { + sendWsText(socket, { type: 'event', data: event }); + } + } + + broadcast(message) { + for (const subscriber of this.subscribers) { + try { + sendWsText(subscriber.socket, message); + } catch { + this.subscribers.delete(subscriber); + } + } + } + + snapshot() { + return { + session_key: this.sessionKey, + status: this.status, + last_error: this.lastError || null, + qr_data_url: this.qrDataUrl || null, + qr_updated_at: this.qrUpdatedAt || null, + connected_at: this.connectedAt || null, + session_id: this.sessionId || null, + auth_registered: this.authRegistered, + last_snapshot_at: this.lastSnapshotAt || null, + event_cursor: this.eventSeq, + event_queue_size: this.events.length, + }; + } + + listEvents(afterSeq) { + const after = Number.isFinite(afterSeq) ? afterSeq : 0; + return { + next_cursor: this.eventSeq, + items: this.events.filter((event) => event.seq > after), + }; + } + + async start() { + if (this.starting) { + return this.starting; + } + if (this.sock) { + return; + } + this.stopping = false; + this.starting = this._start().finally(() => { + this.starting = null; + }); + return this.starting; + } + + async _start() { + await mkdir(this.sessionDir, { recursive: true }); + this.status = 'connecting'; + this.lastError = ''; + const { state, saveCreds } = await useMultiFileAuthState(this.sessionDir); + this.saveCreds = saveCreds; + this.authRegistered = Boolean(state?.creds?.registered); + if (!this.authRegistered) { + this.status = 'qr'; + } + const { version } = await fetchLatestBaileysVersion(); + const sock = makeWASocket({ + auth: state, + version, + browser: Browsers.ubuntu('RAGFlow'), + printQRInTerminal: false, + markOnlineOnConnect: false, + syncFullHistory: false, + getMessage: async (key) => { + if (!key?.id) { + return undefined; + } + return this.messageStore.get(key.id); + }, + }); + this.sock = sock; + + sock.ev.on('creds.update', this.saveCreds); + sock.ev.on('connection.update', (update) => { + void this._handleConnectionUpdate(update); + }); + sock.ev.on('messages.upsert', (update) => { + void this._handleMessagesUpsert(update); + }); + + this.lastSnapshotAt = now(); + } + + async _handleConnectionUpdate(update) { + if (update.qr) { + this.status = 'qr'; + this.lastError = ''; + this.qrUpdatedAt = now(); + this.qrDataUrl = await QRCode.toDataURL(update.qr, { + errorCorrectionLevel: 'M', + margin: 2, + scale: 8, + }); + this.lastSnapshotAt = now(); + this.broadcast({ type: 'snapshot', data: this.snapshot() }); + } + + if (update.connection === 'open') { + this.status = 'connected'; + this.lastError = ''; + this.connectedAt = now(); + this.qrDataUrl = ''; + this.sessionId = this.sock?.user?.id || this.sessionId; + this.authRegistered = true; + this.lastSnapshotAt = now(); + this.broadcast({ type: 'snapshot', data: this.snapshot() }); + return; + } + + if (update.connection === 'close') { + const reason = update.lastDisconnect?.error?.output?.statusCode; + const loggedOut = reason === DisconnectReason.loggedOut; + this.status = loggedOut ? 'error' : 'disconnected'; + this.lastError = + update.lastDisconnect?.error?.message || + (loggedOut ? 'WhatsApp session logged out.' : 'WhatsApp session disconnected.'); + this.lastSnapshotAt = now(); + this.sock = null; + this.saveCreds = null; + this.authRegistered = false; + this.broadcast({ type: 'snapshot', data: this.snapshot() }); + + if (!this.stopping && !loggedOut) { + clearTimeout(this.restartTimer); + this.restartTimer = setTimeout(() => { + void this.start(); + }, 3000); + } + } + } + + async _handleMessagesUpsert(update) { + for (const message of update.messages || []) { + const key = safeMessageKey(message); + if (key.fromMe || !key.id || !key.remoteJid) { + continue; + } + const text = extractText(message.message); + if (!text) { + continue; + } + const jid = key.remoteJid; + const event = { + seq: ++this.eventSeq, + kind: 'message', + message_id: key.id, + chat_id: jid, + chat_type: detectChatType(jid), + sender_id: key.participant || jid, + text, + raw: { + key, + message: message.message, + pushName: message.pushName || '', + messageTimestamp: message.messageTimestamp || 0, + }, + }; + this.events.push(event); + this.messageStore.set(key.id, message); + this.broadcast({ type: 'event', data: event }); + if (this.events.length > 1000) { + const dropped = this.events.slice(0, this.events.length - 500); + this.events = this.events.slice(-500); + for (const oldEvent of dropped) { + if (oldEvent.kind === 'message' && oldEvent.message_id) { + this.messageStore.delete(oldEvent.message_id); + } + } + } + this.lastSnapshotAt = now(); + } + } + + async send(payload) { + if (!this.sock) { + throw new Error('WhatsApp session is not running.'); + } + const jid = normalizeJid(payload.chat_id); + if (!jid) { + throw new Error(`Invalid chat_id: ${payload.chat_id}`); + } + const text = String(payload.text || ''); + const options = {}; + if (payload.reply_to_message_id) { + const quoted = this.messageStore.get(String(payload.reply_to_message_id)); + if (quoted) { + options.quoted = quoted; + } + } + await this.sock.sendMessage(jid, { text }, options); + this.lastSnapshotAt = now(); + } + + async stop() { + this.stopping = true; + clearTimeout(this.restartTimer); + this.restartTimer = null; + const sock = this.sock; + this.sock = null; + this.saveCreds = null; + if (sock) { + try { + sock.end?.(undefined); + } catch { + try { + sock.ws?.close?.(); + } catch { + // ignore + } + } + } + this.status = 'stopped'; + this.lastError = ''; + this.qrDataUrl = ''; + this.qrUpdatedAt = 0; + this.connectedAt = 0; + this.sessionId = ''; + this.lastSnapshotAt = now(); + this.subscribers.clear(); + this.events = []; + this.messageStore.clear(); + } +} + +const sessions = new Map(); + +function getSession(sessionKey) { + const key = String(sessionKey || '').trim() || 'default'; + let session = sessions.get(key); + if (!session) { + session = new WhatsAppSession(key); + sessions.set(key, session); + } + return session; +} + +function getExistingSession(sessionKey) { + const key = String(sessionKey || '').trim() || 'default'; + return sessions.get(key) || null; +} + +async function readBody(req) { + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + } + if (!chunks.length) { + return {}; + } + const raw = Buffer.concat(chunks).toString('utf8'); + if (!raw.trim()) { + return {}; + } + return JSON.parse(raw); +} + +function sendJson(res, statusCode, payload) { + const body = JSON.stringify(payload); + res.writeHead(statusCode, { + 'content-type': 'application/json; charset=utf-8', + 'content-length': Buffer.byteLength(body), + }); + res.end(body); +} + +function sendError(res, statusCode, message) { + sendJson(res, statusCode, { code: statusCode, message, data: null }); +} + +const server = http.createServer(async (req, res) => { + try { + const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); + const parts = url.pathname.split('/').filter(Boolean); + + if (req.method === 'GET' && url.pathname === '/health') { + return sendJson(res, 200, { ok: true }); + } + + if (parts[0] !== 'whatsapp' || parts.length < 2) { + return sendError(res, 404, 'not found'); + } + + if (!isAuthorized(req)) { + return sendError(res, 401, 'unauthorized'); + } + + const sessionKey = decodeURIComponent(parts[1]); + + if (req.method === 'POST' && parts.length === 3 && parts[2] === 'start') { + const session = getSession(sessionKey); + await session.start(); + return sendJson(res, 200, { code: 0, message: '', data: session.snapshot() }); + } + + if (req.method === 'GET' && parts.length === 3 && parts[2] === 'status') { + const session = getExistingSession(sessionKey); + if (!session) { + return sendError(res, 404, 'session not found'); + } + return sendJson(res, 200, { code: 0, message: '', data: session.snapshot() }); + } + + if (req.method === 'POST' && parts.length === 3 && parts[2] === 'send') { + const session = getExistingSession(sessionKey); + if (!session) { + return sendError(res, 404, 'session not found'); + } + const body = await readBody(req); + await session.send(body); + return sendJson(res, 200, { code: 0, message: '', data: true }); + } + + if (req.method === 'POST' && parts.length === 3 && parts[2] === 'stop') { + const session = getExistingSession(sessionKey); + if (!session) { + return sendError(res, 404, 'session not found'); + } + await session.stop(); + sessions.delete(sessionKey); + return sendJson(res, 200, { code: 0, message: '', data: true }); + } + + return sendError(res, 404, 'not found'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(error); + return sendJson(res, 500, { code: 500, message, data: null }); + } +}); + +server.on('upgrade', (req, socket) => { + try { + const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); + const parts = url.pathname.split('/').filter(Boolean); + if (parts[0] !== 'whatsapp' || parts.length !== 4 || parts[2] !== 'events' || parts[3] !== 'ws') { + socket.destroy(); + return; + } + if (!isAuthorized(req)) { + socket.destroy(); + return; + } + const sessionKey = decodeURIComponent(parts[1]); + const key = req.headers['sec-websocket-key']; + if (!key) { + socket.destroy(); + return; + } + const accept = crypto.createHash('sha1').update(`${key}${WS_MAGIC}`).digest('base64'); + socket.write( + [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${accept}`, + '', + '', + ].join('\r\n'), + ); + socket.setNoDelay(true); + const session = getExistingSession(sessionKey); + if (!session) { + socket.destroy(); + return; + } + const after = Number.parseInt(url.searchParams.get('after') || '0', 10) || 0; + session.addSubscriber(socket, after); + socket.on('end', () => { + session.subscribers.forEach((subscriber) => { + if (subscriber.socket === socket) { + session.subscribers.delete(subscriber); + } + }); + }); + } catch (error) { + console.error(error); + socket.destroy(); + } +}); + +server.listen(PORT, HOST, () => { + console.log(`WhatsApp gateway listening on http://${HOST}:${PORT}`); +}); diff --git a/api/channels/whatsapp/gateway-node/package-lock.json b/api/channels/whatsapp/gateway-node/package-lock.json new file mode 100644 index 0000000000..eac4c7ebc5 --- /dev/null +++ b/api/channels/whatsapp/gateway-node/package-lock.json @@ -0,0 +1,1677 @@ +{ + "name": "ragflow-whatsapp-gateway", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ragflow-whatsapp-gateway", + "dependencies": { + "baileys": "7.0.0-rc13", + "qrcode": "^1.5.4" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz", + "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==", + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.4.1", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/node-cache": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", + "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", + "license": "MIT", + "dependencies": { + "cacheable": "^2.3.1", + "hookified": "^1.14.0", + "keyv": "^5.5.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", + "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", + "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", + "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", + "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", + "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", + "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", + "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", + "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", + "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", + "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", + "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", + "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", + "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", + "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", + "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", + "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", + "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", + "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", + "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", + "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", + "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", + "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", + "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", + "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", + "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", + "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/baileys": { + "version": "7.0.0-rc13", + "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc13.tgz", + "integrity": "sha512-v8k74K8B5R7WNYGa26MyJAYEu3Wc4BSuK01QaK8lr30lhE8Nga31nWNu8KN0NDDt+Fsvkq4SQFFI8Q13ghjKmA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cacheable/node-cache": "^1.4.0", + "@hapi/boom": "^9.1.3", + "async-mutex": "^0.5.0", + "libsignal": "^6.0.0", + "lru-cache": "^11.1.0", + "music-metadata": "^11.12.3", + "p-queue": "^9.0.0", + "pino": "^9.6", + "protobufjs": "^7.5.6", + "whatsapp-rust-bridge": "0.5.4", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "audio-decode": "^2.1.3", + "jimp": "^1.6.1", + "link-preview-js": "^3.0.0", + "sharp": "*" + }, + "peerDependenciesMeta": { + "audio-decode": { + "optional": true + }, + "jimp": { + "optional": true + }, + "link-preview-js": { + "optional": true + } + } + }, + "node_modules/cacheable": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", + "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.1", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/curve25519-js": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", + "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/libsignal": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/libsignal/-/libsignal-6.0.0.tgz", + "integrity": "sha512-d/5V3YFtDljbFMufz4ncyUYGYhJl+vzAe+c2EFFBQ6bz1h8Q3IOMEGXYMzlibU60I+e8GagMMpji18iez3P1hA==", + "license": "GPL-3.0", + "dependencies": { + "curve25519-js": "^0.0.4", + "protobufjs": "^7.5.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/media-typer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-2.0.0.tgz", + "integrity": "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/music-metadata": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.13.0.tgz", + "integrity": "sha512-uXRaov9dfjSpQufXIU7sMxVZnh+FilCQv2mXn+K5EJ/decP3dTWrgvPYa5r6MtRbieNSCE708Da4J0u1UGfQIw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.2", + "@tokenizer/token": "^0.3.0", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "file-type": "^21.3.4", + "media-typer": "^2.0.0", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-queue": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", + "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.2", + "@img/sharp-darwin-x64": "0.35.2", + "@img/sharp-freebsd-wasm32": "0.35.2", + "@img/sharp-libvips-darwin-arm64": "1.3.1", + "@img/sharp-libvips-darwin-x64": "1.3.1", + "@img/sharp-libvips-linux-arm": "1.3.1", + "@img/sharp-libvips-linux-arm64": "1.3.1", + "@img/sharp-libvips-linux-ppc64": "1.3.1", + "@img/sharp-libvips-linux-riscv64": "1.3.1", + "@img/sharp-libvips-linux-s390x": "1.3.1", + "@img/sharp-libvips-linux-x64": "1.3.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", + "@img/sharp-libvips-linuxmusl-x64": "1.3.1", + "@img/sharp-linux-arm": "0.35.2", + "@img/sharp-linux-arm64": "0.35.2", + "@img/sharp-linux-ppc64": "0.35.2", + "@img/sharp-linux-riscv64": "0.35.2", + "@img/sharp-linux-s390x": "0.35.2", + "@img/sharp-linux-x64": "0.35.2", + "@img/sharp-linuxmusl-arm64": "0.35.2", + "@img/sharp-linuxmusl-x64": "0.35.2", + "@img/sharp-webcontainers-wasm32": "0.35.2", + "@img/sharp-win32-arm64": "0.35.2", + "@img/sharp-win32-ia32": "0.35.2", + "@img/sharp-win32-x64": "0.35.2" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/whatsapp-rust-bridge": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.4.tgz", + "integrity": "sha512-yYO1qSs0Fe7tGtnxOFHomocUD6IZtoAgmA4oDFyGIRZ67D3QZk3w7swA6XXFXNQngiyrg2k7tul6IrM3eUFh7A==", + "license": "MIT" + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/win-guid": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", + "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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 + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/api/channels/whatsapp/gateway-node/package.json b/api/channels/whatsapp/gateway-node/package.json new file mode 100644 index 0000000000..e14da7e753 --- /dev/null +++ b/api/channels/whatsapp/gateway-node/package.json @@ -0,0 +1,12 @@ +{ + "name": "ragflow-whatsapp-gateway", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "baileys": "7.0.0-rc13", + "qrcode": "^1.5.4" + } +} diff --git a/api/channels/whatsapp/gateway.py b/api/channels/whatsapp/gateway.py new file mode 100644 index 0000000000..533b2879a8 --- /dev/null +++ b/api/channels/whatsapp/gateway.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import asyncio +import logging +import os +import shlex +import shutil +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +LOGGER = logging.getLogger(__name__) +_missing_command_warned = False +_deps_install_warned = False + + +def _env_flag(name: str, default: bool = False) -> bool: + raw = os.getenv(name) + if raw is None: + return default + return raw.strip().lower() in {"1", "true", "yes", "on"} + + +def _default_gateway_command() -> list[str]: + raw = os.getenv("WHATSAPP_GATEWAY_COMMAND", "").strip() + if raw: + return shlex.split(raw) + gateway_entry = Path(__file__).resolve().parent / "gateway-node" / "index.js" + node = shutil.which("node") + if node and gateway_entry.exists(): + return [node, str(gateway_entry)] + return [] + + +def _gateway_dir() -> Path: + return Path(__file__).resolve().parent / "gateway-node" + + +@dataclass +class WhatsAppGatewayConfig: + command: list[str] + cwd: str + enabled: bool + + +class WhatsAppGatewayRuntime: + def __init__(self) -> None: + self._process: Optional[asyncio.subprocess.Process] = None + self._lock = asyncio.Lock() + self._install_lock = asyncio.Lock() + self._sync_generation = 0 + + def _config(self) -> WhatsAppGatewayConfig: + workdir = os.getenv("WHATSAPP_GATEWAY_WORKDIR", "").strip() + return WhatsAppGatewayConfig( + command=_default_gateway_command(), + cwd=workdir or str(_gateway_dir()), + enabled=_env_flag("WHATSAPP_GATEWAY_ENABLED", True), + ) + + def is_running(self) -> bool: + return bool(self._process and self._process.returncode is None) + + async def sync(self, enabled: bool) -> None: + cfg = self._config() + should_run = bool(enabled and cfg.enabled and cfg.command) + async with self._lock: + self._sync_generation += 1 + generation = self._sync_generation + if not should_run: + await self._stop_locked() + return + if self.is_running(): + return + + await self._ensure_dependencies(cfg) + + async with self._lock: + if generation != self._sync_generation: + return + if not should_run: + await self._stop_locked() + return + await self._start_locked(cfg) + + async def _ensure_dependencies(self, cfg: WhatsAppGatewayConfig) -> None: + global _deps_install_warned + if not _env_flag("WHATSAPP_GATEWAY_AUTO_INSTALL", True): + return + + async with self._install_lock: + gateway_dir = Path(cfg.cwd) + node_modules = gateway_dir / "node_modules" + if node_modules.exists(): + return + + npm = shutil.which("npm") + if not npm: + if not _deps_install_warned: + LOGGER.warning( + "npm is not available; WhatsApp gateway dependencies cannot be installed automatically" + ) + _deps_install_warned = True + return + + package_json = gateway_dir / "package.json" + if not package_json.exists(): + LOGGER.warning("WhatsApp gateway package.json not found in %s", gateway_dir) + return + + LOGGER.info("installing WhatsApp gateway dependencies in %s", gateway_dir) + proc = await asyncio.create_subprocess_exec( + npm, + "install", + "--no-fund", + "--no-audit", + cwd=str(gateway_dir), + ) + try: + code = await asyncio.wait_for(proc.wait(), timeout=300) + except asyncio.TimeoutError as ex: + proc.kill() + await proc.wait() + raise RuntimeError("npm install timed out after 300s") from ex + if code != 0: + raise RuntimeError(f"npm install failed with exit code {code}") + _deps_install_warned = False + + async def _start_locked(self, cfg: WhatsAppGatewayConfig) -> None: + global _missing_command_warned + if self.is_running(): + return + if not cfg.command: + if not _missing_command_warned: + LOGGER.warning("WhatsApp gateway command is not configured; gateway will not start") + _missing_command_warned = True + return + _missing_command_warned = False + + env = os.environ.copy() + env.setdefault("PYTHONUNBUFFERED", "1") + LOGGER.info("starting WhatsApp gateway: %s", " ".join(cfg.command)) + self._process = await asyncio.create_subprocess_exec( + *cfg.command, + cwd=cfg.cwd, + env=env, + ) + + async def _stop_locked(self) -> None: + proc = self._process + if proc is None: + return + + if proc.returncode is None: + LOGGER.info("stopping WhatsApp gateway") + proc.terminate() + try: + await asyncio.wait_for(proc.wait(), timeout=10) + except asyncio.TimeoutError: + LOGGER.warning("WhatsApp gateway did not stop in time; killing it") + proc.kill() + await proc.wait() + except Exception: + LOGGER.debug("WhatsApp gateway stop failed", exc_info=True) + + self._process = None + + +_gateway_runtime = WhatsAppGatewayRuntime() + + +async def sync_whatsapp_gateway(enabled: bool) -> None: + await _gateway_runtime.sync(enabled) diff --git a/web/src/pages/user-setting/chat-channel/add-channel-modal.tsx b/web/src/pages/user-setting/chat-channel/add-channel-modal.tsx index a498fa88d0..9fc1e8e080 100644 --- a/web/src/pages/user-setting/chat-channel/add-channel-modal.tsx +++ b/web/src/pages/user-setting/chat-channel/add-channel-modal.tsx @@ -1,16 +1,24 @@ import { DynamicForm, FormFieldConfig } from '@/components/dynamic-form'; import { Modal } from '@/components/ui/modal/modal'; import { IModalProps } from '@/interfaces/common'; -import { useEffect, useMemo, useState } from 'react'; +import { fetchChatChannelRuntime } from '@/services/chat-channel-service'; +import { QrCode } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FieldValues } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { ChatChannelFormDefaultValues, + ChatChannelKey, getChatChannelFields, + getChatChannelRuntimeStatusClass, + getChatChannelRuntimeStatusText, mergeChatChannelFormValues, } from './constant'; import { IChatChannel, IChatChannelInfo } from './interface'; +const getRuntimeSnapshot = (payload: any) => + payload?.data?.data ?? payload?.data ?? payload?.runtime ?? payload ?? {}; + const AddChatChannelModal = ({ visible, hideModal, @@ -24,6 +32,10 @@ const AddChatChannelModal = ({ }) => { const { t } = useTranslation(); const [fields, setFields] = useState([]); + const [runtimeStatus, setRuntimeStatus] = useState(''); + const [runtimeError, setRuntimeError] = useState(''); + const [runtimeQr, setRuntimeQr] = useState(''); + const runtimePollingInFlightRef = useRef(false); useEffect(() => { if (channel) { @@ -31,6 +43,42 @@ const AddChatChannelModal = ({ } }, [channel]); + const refreshRuntime = useCallback(async () => { + if (channel?.id !== ChatChannelKey.WHATSAPP || !record?.id) { + return; + } + if (runtimePollingInFlightRef.current) { + return; + } + runtimePollingInFlightRef.current = true; + try { + const { data } = await fetchChatChannelRuntime(record.id); + const snapshot = getRuntimeSnapshot(data); + setRuntimeStatus(snapshot?.status || ''); + setRuntimeError(snapshot?.last_error || ''); + setRuntimeQr(snapshot?.qr_data_url || ''); + } catch (error: any) { + setRuntimeError(error?.message || 'Failed to load QR.'); + setRuntimeQr(''); + } finally { + runtimePollingInFlightRef.current = false; + } + }, [channel?.id, record?.id]); + + useEffect(() => { + if (channel?.id !== ChatChannelKey.WHATSAPP || !record?.id) { + return; + } + setRuntimeStatus(''); + setRuntimeError(''); + setRuntimeQr(''); + void refreshRuntime(); + const timer = window.setInterval(() => { + void refreshRuntime(); + }, 1000); + return () => window.clearInterval(timer); + }, [channel?.id, record?.id, refreshRuntime]); + const defaultValues = useMemo(() => { const base = channel ? ChatChannelFormDefaultValues[channel.id] : undefined; return mergeChatChannelFormValues(base, record) as FieldValues; @@ -63,6 +111,46 @@ const AddChatChannelModal = ({ defaultValues={defaultValues} labelClassName="font-normal" > + {channel?.id === ChatChannelKey.WHATSAPP && ( +
+ {record?.id ? ( +
+
+
+ + + {getChatChannelRuntimeStatusText(runtimeStatus)} + +
+
+ {runtimeError ? ( +
{runtimeError}
+ ) : null} + {runtimeQr && runtimeStatus !== 'connected' ? ( + WhatsApp QR + ) : runtimeStatus === 'connected' ? ( +
+ Channel is connected. +
+ ) : !runtimeStatus ? ( +
+ QR will appear after the channel starts. +
+ ) : null} +
+ ) : ( +
+ Save this WhatsApp channel first, then scan the QR code here. +
+ )} +
+ )}
{ diff --git a/web/src/pages/user-setting/chat-channel/component/added-channel-card.tsx b/web/src/pages/user-setting/chat-channel/component/added-channel-card.tsx index 3e1c8867b7..70863641b8 100644 --- a/web/src/pages/user-setting/chat-channel/component/added-channel-card.tsx +++ b/web/src/pages/user-setting/chat-channel/component/added-channel-card.tsx @@ -1,15 +1,25 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Modal } from '@/components/ui/modal/modal'; import { useSetModalState } from '@/hooks/common-hooks'; -import { Link2, Settings, Trash2 } from 'lucide-react'; -import { useState } from 'react'; +import { fetchChatChannelRuntime } from '@/services/chat-channel-service'; +import { Link2, QrCode, RefreshCw, Settings, Trash2 } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useChatChannelInfo } from '../constant'; +import { + ChatChannelKey, + getChatChannelRuntimeStatusClass, + getChatChannelRuntimeStatusText, + useChatChannelInfo, +} from '../constant'; import { useDeleteChatChannel, useFetchChatChannelDetail } from '../hooks'; import { IChatChannel, IChatChannelBase, IChatChannelInfo } from '../interface'; import ConnectDialogModal from './connect-dialog-modal'; import { delChannelModal } from './delete-channel-modal'; +const getRuntimeSnapshot = (payload: any) => + payload?.data?.data ?? payload?.data ?? payload?.runtime ?? payload ?? {}; + export type IAddedChannelCardProps = IChatChannelInfo & { list: IChatChannelBase[]; onEdit: (channel: IChatChannelInfo, record: IChatChannel) => void; @@ -29,6 +39,13 @@ export const AddedChannelCard = (props: IAddedChannelCardProps) => { const [connectTarget, setConnectTarget] = useState< IChatChannelBase | undefined >(undefined); + const [qrVisible, setQrVisible] = useState(false); + const [qrChannelId, setQrChannelId] = useState(''); + const [qrLoading, setQrLoading] = useState(false); + const [qrData, setQrData] = useState(''); + const [qrStatus, setQrStatus] = useState(''); + const [qrError, setQrError] = useState(''); + const qrPollingInFlightRef = useRef(false); const handleEdit = async (id: string) => { const record = await fetchDetail(id); @@ -42,6 +59,42 @@ export const AddedChannelCard = (props: IAddedChannelCardProps) => { showConnectModal(); }; + const loadQr = useCallback(async () => { + if (!qrChannelId) { + return; + } + if (qrPollingInFlightRef.current) { + return; + } + qrPollingInFlightRef.current = true; + setQrLoading(true); + try { + const { data } = await fetchChatChannelRuntime(qrChannelId); + const snapshot = getRuntimeSnapshot(data); + setQrData(snapshot?.qr_data_url || ''); + setQrStatus(snapshot?.status || ''); + setQrError(snapshot?.last_error || ''); + } catch (error: any) { + setQrData(''); + setQrStatus(''); + setQrError(error?.message || 'Failed to load QR.'); + } finally { + qrPollingInFlightRef.current = false; + setQrLoading(false); + } + }, [qrChannelId]); + + useEffect(() => { + if (!qrVisible) { + return; + } + void loadQr(); + const timer = window.setInterval(() => { + void loadQr(); + }, 1000); + return () => window.clearInterval(timer); + }, [qrVisible, loadQr]); + return ( @@ -85,6 +138,22 @@ export const AddedChannelCard = (props: IAddedChannelCardProps) => { > + {channel.channel === ChatChannelKey.WHATSAPP && ( + + )} + +
+ } + > +
+
+ + {getChatChannelRuntimeStatusText(qrStatus)} +
+ {qrError ? ( +
{qrError}
+ ) : null} + {qrData ? ( + WhatsApp QR + ) : ( +
+ QR is not ready yet. +
+ )} +
+ ); }; diff --git a/web/src/pages/user-setting/chat-channel/constant/index.tsx b/web/src/pages/user-setting/chat-channel/constant/index.tsx index 2bc697bf64..6e16ce2def 100644 --- a/web/src/pages/user-setting/chat-channel/constant/index.tsx +++ b/web/src/pages/user-setting/chat-channel/constant/index.tsx @@ -93,6 +93,60 @@ export const useChatChannelInfo = () => { return { chatChannelInfo }; }; +export const getChatChannelRuntimeStatusClass = (status?: string) => { + const normalized = (status || '').toLowerCase(); + if (normalized === 'connected') { + return 'bg-state-success/10 text-state-success border-state-success/20'; + } + if ( + normalized === 'connecting' || + normalized === 'reconnecting' || + normalized === 'qr' + ) { + return 'bg-state-warning/10 text-state-warning border-state-warning/20'; + } + if (normalized === 'waiting') { + return 'bg-state-warning/10 text-state-warning border-state-warning/20'; + } + if ( + normalized === 'error' || + normalized === 'disconnected' || + normalized === 'stopped' + ) { + return 'bg-state-error/10 text-state-error border-state-error/20'; + } + return 'bg-gray-500/10 text-text-secondary border-border-button'; +}; + +export const getChatChannelRuntimeStatusText = (status?: string) => { + const normalized = (status || '').toLowerCase(); + if (normalized === 'connected') { + return 'Connected'; + } + if (normalized === 'connecting') { + return 'Connecting...'; + } + if (normalized === 'reconnecting') { + return 'Reconnecting...'; + } + if (normalized === 'qr') { + return 'Scan the QR code below'; + } + if (normalized === 'waiting') { + return 'Waiting for the channel to start'; + } + if (normalized === 'error') { + return 'Runtime error'; + } + if (normalized === 'disconnected') { + return 'Disconnected'; + } + if (normalized === 'stopped') { + return 'Stopped'; + } + return 'Waiting for runtime...'; +}; + const isPlainObject = (value: unknown): value is Record => typeof value === 'object' && value !== null && !Array.isArray(value); @@ -697,7 +751,6 @@ ChatChannelFormDefaultValues[ ].config.credential.connection_type = 'webhook'; ChatChannelFormDefaultValues[ChatChannelKey.FEISHU].config.credential.domain = 'feishu'; - export const getChatChannelFields = ( key?: ChatChannelKey, ): FormFieldConfig[] => { diff --git a/web/src/pages/user-setting/chat-channel/hooks.ts b/web/src/pages/user-setting/chat-channel/hooks.ts index b5c9be6157..679ce6db83 100644 --- a/web/src/pages/user-setting/chat-channel/hooks.ts +++ b/web/src/pages/user-setting/chat-channel/hooks.ts @@ -102,9 +102,16 @@ export const useAddChatChannel = () => { queryClient.invalidateQueries({ queryKey: ChatChannelKeys.detail(values.id), }); + } else if (values.channel === ChatChannelKey.WHATSAPP) { + setEditingRecord(res.data as IChatChannel); + queryClient.invalidateQueries({ + queryKey: ChatChannelKeys.detail(res.data?.id), + }); } message.success(t('message.operated')); - hideModal(); + if (isEdit || values.channel !== ChatChannelKey.WHATSAPP) { + hideModal(); + } } } finally { setLoading(false); diff --git a/web/src/pages/user-setting/chat-channel/index.tsx b/web/src/pages/user-setting/chat-channel/index.tsx index a530642e3d..4e19fa5c58 100644 --- a/web/src/pages/user-setting/chat-channel/index.tsx +++ b/web/src/pages/user-setting/chat-channel/index.tsx @@ -66,6 +66,7 @@ const ChatChannel = () => { ChatChannelKey.TELEGRAM, ChatChannelKey.QQBOT, ChatChannelKey.WECOM, + ChatChannelKey.WHATSAPP, ].includes(id), // Show only selected chat channels ) .map((id) => ({ diff --git a/web/src/services/chat-channel-service.ts b/web/src/services/chat-channel-service.ts index 28d9bfd9eb..cf9b544d94 100644 --- a/web/src/services/chat-channel-service.ts +++ b/web/src/services/chat-channel-service.ts @@ -28,4 +28,7 @@ export const updateChatChannel = (id: string, data: Record) => export const deleteChatChannel = (id: string) => request.delete(api.chatChannelDel(id)); +export const fetchChatChannelRuntime = (id: string) => + request.get(api.chatChannelRuntime(id)); + export default chatChannelService; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index d0786396e2..6c9d98c3f9 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -102,6 +102,8 @@ export default { chatChannelDetail: (id: string) => `${restAPIv1}/chat-channels/${id}`, chatChannelUpdate: (id: string) => `${restAPIv1}/chat-channels/${id}`, chatChannelDel: (id: string) => `${restAPIv1}/chat-channels/${id}`, + chatChannelRuntime: (id: string) => + `${restAPIv1}/chat-channels/${id}/runtime`, // plugin llmTools: `${restAPIv1}/plugin/tools`,