mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
feat: support wecom websocket channel (#16175)
Added WeCom chat channel websocket mode alongside the existing webhook mode, plus frontend support for selecting the connection type.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
@@ -18,16 +19,19 @@ from ..core.registry import register_channel
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
WECOM_API_BASE = "https://qyapi.weixin.qq.com/cgi-bin"
|
||||
WECOM_WS_URL = "wss://openws.work.weixin.qq.com"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeComAccount:
|
||||
account_id: str
|
||||
corp_id: str
|
||||
agent_id: int
|
||||
secret: str
|
||||
token: str
|
||||
aes_key: str
|
||||
connection_type: str = "webhook"
|
||||
corp_id: str = ""
|
||||
agent_id: int = 0
|
||||
secret: str = ""
|
||||
token: str = ""
|
||||
aes_key: str = ""
|
||||
bot_id: str = ""
|
||||
webhook_host: str = "0.0.0.0"
|
||||
webhook_port: int = 3002
|
||||
|
||||
@@ -143,15 +147,39 @@ class WeComChannel(Channel):
|
||||
super().__init__()
|
||||
self.account = account
|
||||
self.account_id = account.account_id
|
||||
self.crypto = WeChatCrypto(
|
||||
account.token, account.aes_key, account.corp_id
|
||||
self.connection_type = (account.connection_type or "webhook").strip().lower()
|
||||
self.crypto = (
|
||||
WeChatCrypto(account.token, account.aes_key, account.corp_id)
|
||||
if self.connection_type == "webhook"
|
||||
else None
|
||||
)
|
||||
self._server: Optional[_SharedWebhookServer] = None
|
||||
self._access_token: Optional[str] = None
|
||||
self._access_token_expires_at: float = 0.0
|
||||
self._access_token_lock = asyncio.Lock()
|
||||
self._ws_task: Optional[asyncio.Task] = None
|
||||
self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
|
||||
self._ws_send_lock: Optional[asyncio.Lock] = None
|
||||
self._heartbeat_task: Optional[asyncio.Task] = None
|
||||
self._stop_requested = False
|
||||
|
||||
async def start(self) -> None:
|
||||
self._stop_requested = False
|
||||
if self.connection_type == "websocket":
|
||||
if self._ws_task and not self._ws_task.done():
|
||||
return
|
||||
self._ws_send_lock = asyncio.Lock()
|
||||
self._ws_task = asyncio.create_task(
|
||||
self._run_websocket(),
|
||||
name=f"wecom-ws-{self.account_id}",
|
||||
)
|
||||
LOGGER.info(
|
||||
"[wecom:%s] starting websocket client (bot_id=%s)",
|
||||
self.account_id,
|
||||
self.account.bot_id,
|
||||
)
|
||||
return
|
||||
|
||||
self._server = await _acquire_server(
|
||||
self.account.webhook_host, self.account.webhook_port
|
||||
)
|
||||
@@ -164,6 +192,27 @@ class WeComChannel(Channel):
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
self._stop_requested = True
|
||||
if self._heartbeat_task and not self._heartbeat_task.done():
|
||||
self._heartbeat_task.cancel()
|
||||
try:
|
||||
await self._heartbeat_task
|
||||
except BaseException:
|
||||
pass
|
||||
self._heartbeat_task = None
|
||||
if self._ws is not None and not self._ws.closed:
|
||||
try:
|
||||
await self._ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
if self._ws_task and not self._ws_task.done():
|
||||
self._ws_task.cancel()
|
||||
try:
|
||||
await self._ws_task
|
||||
except BaseException:
|
||||
pass
|
||||
self._ws_task = None
|
||||
self._ws = None
|
||||
if self._server is not None:
|
||||
self._server.channels.pop(self.account_id, None)
|
||||
await _release_server(
|
||||
@@ -171,27 +220,235 @@ class WeComChannel(Channel):
|
||||
)
|
||||
self._server = None
|
||||
|
||||
async def handle_decrypted_message(self, msg) -> None:
|
||||
async def _handle_text_message(
|
||||
self,
|
||||
*,
|
||||
chat_id: str,
|
||||
sender_id: str,
|
||||
message_id: str,
|
||||
text: str,
|
||||
raw: Any,
|
||||
chat_type: str = "p2p",
|
||||
) -> None:
|
||||
try:
|
||||
if not (text or "").strip():
|
||||
return
|
||||
incoming = IncomingMessage(
|
||||
channel=self.channel_id,
|
||||
account_id=self.account_id,
|
||||
chat_id=chat_id,
|
||||
chat_type=chat_type,
|
||||
message_id=message_id,
|
||||
sender_id=sender_id,
|
||||
text=text,
|
||||
raw=raw,
|
||||
)
|
||||
await self._dispatch(incoming)
|
||||
except Exception:
|
||||
LOGGER.error(
|
||||
"[wecom:%s] inbound message handling error",
|
||||
self.account_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
async def handle_decrypted_message(self, msg) -> None:
|
||||
# Short-connection webhook mode.
|
||||
try:
|
||||
# Only handle plain text events; ignore image/voice/event etc.
|
||||
if getattr(msg, "type", "") != "text":
|
||||
return
|
||||
user_id = str(getattr(msg, "source", "") or "")
|
||||
if not user_id:
|
||||
return
|
||||
incoming = IncomingMessage(
|
||||
channel=self.channel_id,
|
||||
account_id=self.account_id,
|
||||
await self._handle_text_message(
|
||||
chat_id=user_id,
|
||||
chat_type="p2p",
|
||||
message_id=str(getattr(msg, "id", "") or ""),
|
||||
sender_id=user_id,
|
||||
message_id=str(getattr(msg, "id", "") or ""),
|
||||
text=getattr(msg, "content", "") or "",
|
||||
raw=msg,
|
||||
chat_type="p2p",
|
||||
)
|
||||
await self._dispatch(incoming)
|
||||
except Exception:
|
||||
LOGGER.error("[wecom:%s] inbound message handling error", self.account_id, exc_info=True)
|
||||
LOGGER.error(
|
||||
"[wecom:%s] inbound message handling error",
|
||||
self.account_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
async def _run_websocket(self) -> None:
|
||||
while not self._stop_requested:
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.ws_connect(WECOM_WS_URL, heartbeat=None) as ws:
|
||||
self._ws = ws
|
||||
LOGGER.info(
|
||||
"[wecom:%s] websocket connected",
|
||||
self.account_id,
|
||||
)
|
||||
await self._subscribe_websocket(ws)
|
||||
self._heartbeat_task = asyncio.create_task(
|
||||
self._heartbeat_loop(ws)
|
||||
)
|
||||
async for msg in ws:
|
||||
if self._stop_requested:
|
||||
break
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
await self._handle_ws_payload(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
await self._handle_ws_payload(
|
||||
msg.data.decode("utf-8", "ignore")
|
||||
)
|
||||
elif msg.type == aiohttp.WSMsgType.PONG:
|
||||
LOGGER.debug("[wecom:%s] websocket pong", self.account_id)
|
||||
elif msg.type in (
|
||||
aiohttp.WSMsgType.CLOSE,
|
||||
aiohttp.WSMsgType.CLOSED,
|
||||
aiohttp.WSMsgType.ERROR,
|
||||
):
|
||||
break
|
||||
except PermissionError as ex:
|
||||
self._stop_requested = True
|
||||
LOGGER.error(
|
||||
"[wecom:%s] websocket auth failed; stop reconnecting: %s",
|
||||
self.account_id,
|
||||
ex,
|
||||
)
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception:
|
||||
LOGGER.error(
|
||||
"[wecom:%s] websocket loop error",
|
||||
self.account_id,
|
||||
exc_info=True,
|
||||
)
|
||||
finally:
|
||||
if self._heartbeat_task and not self._heartbeat_task.done():
|
||||
self._heartbeat_task.cancel()
|
||||
try:
|
||||
await self._heartbeat_task
|
||||
except BaseException:
|
||||
pass
|
||||
self._heartbeat_task = None
|
||||
self._ws = None
|
||||
if not self._stop_requested:
|
||||
await asyncio.sleep(3)
|
||||
|
||||
async def _heartbeat_loop(self, ws: aiohttp.ClientWebSocketResponse) -> None:
|
||||
try:
|
||||
while not self._stop_requested and not ws.closed:
|
||||
await asyncio.sleep(25)
|
||||
await ws.ping()
|
||||
LOGGER.debug("[wecom:%s] websocket ping", self.account_id)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception:
|
||||
LOGGER.error("[wecom:%s] websocket heartbeat failed", self.account_id, exc_info=True)
|
||||
|
||||
async def _subscribe_websocket(self, ws: aiohttp.ClientWebSocketResponse) -> None:
|
||||
if not self.account.bot_id:
|
||||
raise RuntimeError(f"wecom account '{self.account_id}' missing bot_id")
|
||||
payload = {
|
||||
"cmd": "aibot_subscribe",
|
||||
"headers": {"req_id": f"req-{time.time_ns()}"},
|
||||
"body": {
|
||||
"bot_id": self.account.bot_id,
|
||||
"secret": self.account.secret,
|
||||
},
|
||||
}
|
||||
await ws.send_json(payload)
|
||||
LOGGER.info("[wecom:%s] websocket subscribe sent", self.account_id)
|
||||
try:
|
||||
resp = await asyncio.wait_for(ws.receive_json(), timeout=10)
|
||||
except Exception as err:
|
||||
raise RuntimeError("wecom websocket subscribe ack timeout") from err
|
||||
if not isinstance(resp, dict):
|
||||
raise RuntimeError(f"wecom websocket subscribe response invalid: {resp!r}")
|
||||
if resp.get("cmd") != "aibot_subscribe":
|
||||
LOGGER.warning(
|
||||
"[wecom:%s] unexpected subscribe response: %s",
|
||||
self.account_id,
|
||||
resp,
|
||||
)
|
||||
errcode = int(resp.get("errcode", 0) or 0)
|
||||
if errcode != 0:
|
||||
if errcode == 853000:
|
||||
raise PermissionError(
|
||||
f"wecom websocket subscribe failed: invalid bot_id or secret: {resp}"
|
||||
)
|
||||
raise RuntimeError(f"wecom websocket subscribe failed: {resp}")
|
||||
LOGGER.info("[wecom:%s] websocket subscribed", self.account_id)
|
||||
|
||||
async def _handle_ws_payload(self, payload: str) -> None:
|
||||
if not payload:
|
||||
return
|
||||
try:
|
||||
obj = json.loads(payload)
|
||||
except json.JSONDecodeError:
|
||||
LOGGER.error(
|
||||
"[wecom:%s] invalid websocket payload: %r",
|
||||
self.account_id,
|
||||
payload[:200],
|
||||
)
|
||||
return
|
||||
cmd = str(obj.get("cmd") or "")
|
||||
headers = obj.get("headers") or {}
|
||||
body = obj.get("body") or {}
|
||||
if cmd == "aibot_msg_callback":
|
||||
await self._handle_ws_message(headers, body, obj)
|
||||
return
|
||||
if cmd == "aibot_event_callback":
|
||||
await self._handle_ws_event(headers, body, obj)
|
||||
return
|
||||
if cmd in ("aibot_subscribe", "aibot_respond_msg", "aibot_respond_welcome_msg"):
|
||||
errcode = obj.get("errcode")
|
||||
if errcode not in (None, 0, "0"):
|
||||
LOGGER.error("[wecom:%s] websocket response error: %s", self.account_id, obj)
|
||||
else:
|
||||
LOGGER.debug("[wecom:%s] websocket response: %s", self.account_id, obj)
|
||||
return
|
||||
LOGGER.debug("[wecom:%s] websocket ignored cmd=%s", self.account_id, cmd)
|
||||
|
||||
async def _handle_ws_message(self, headers: Any, body: Any, raw: Any) -> None:
|
||||
if not isinstance(body, dict):
|
||||
return
|
||||
msgtype = str(body.get("msgtype") or "")
|
||||
if msgtype != "text":
|
||||
return
|
||||
sender = body.get("from") or {}
|
||||
sender_id = str(sender.get("userid") or "")
|
||||
if not sender_id:
|
||||
return
|
||||
chat_type = str(body.get("chattype") or "")
|
||||
chat_id = str(body.get("chatid") or sender_id or "")
|
||||
req_id = str((headers or {}).get("req_id") or body.get("msgid") or "")
|
||||
content = str((body.get("text") or {}).get("content") or "")
|
||||
await self._handle_text_message(
|
||||
chat_id=chat_id or sender_id,
|
||||
sender_id=sender_id,
|
||||
message_id=req_id,
|
||||
text=content,
|
||||
raw=raw,
|
||||
chat_type="group" if chat_type == "group" else "p2p",
|
||||
)
|
||||
|
||||
async def _handle_ws_event(self, headers: Any, body: Any, raw: Any) -> None:
|
||||
if not isinstance(body, dict):
|
||||
return
|
||||
event = body.get("event") or {}
|
||||
event_type = str(event.get("eventtype") or "")
|
||||
req_id = str((headers or {}).get("req_id") or body.get("msgid") or "")
|
||||
LOGGER.info(
|
||||
"[wecom:%s] websocket event=%s req_id=%s",
|
||||
self.account_id,
|
||||
event_type or "unknown",
|
||||
req_id,
|
||||
)
|
||||
if event_type == "disconnected_event":
|
||||
self._stop_requested = True
|
||||
if self._ws is not None and not self._ws.closed:
|
||||
await self._ws.close()
|
||||
return
|
||||
# Other events are accepted but do not trigger the RAG handler.
|
||||
|
||||
async def _get_access_token(self) -> str:
|
||||
async with self._access_token_lock:
|
||||
@@ -217,6 +474,10 @@ class WeComChannel(Channel):
|
||||
return self._access_token
|
||||
|
||||
async def send(self, message: OutgoingMessage) -> None:
|
||||
if self.connection_type == "websocket":
|
||||
await self._send_websocket_message(message)
|
||||
return
|
||||
|
||||
if not message.chat_id:
|
||||
LOGGER.error("[wecom:%s] missing chat_id; cannot send", self.account_id)
|
||||
return
|
||||
@@ -252,35 +513,76 @@ class WeComChannel(Channel):
|
||||
self._access_token_expires_at = 0.0
|
||||
LOGGER.error("[wecom:%s] send failed: %s", self.account_id, data)
|
||||
|
||||
async def _send_websocket_message(self, message: OutgoingMessage) -> None:
|
||||
if not message.text:
|
||||
LOGGER.error("[wecom:%s] empty websocket reply text", self.account_id)
|
||||
return
|
||||
if self._ws is None or self._ws.closed:
|
||||
LOGGER.error("[wecom:%s] websocket is not connected", self.account_id)
|
||||
return
|
||||
payload = {
|
||||
"cmd": "aibot_send_msg",
|
||||
"headers": {"req_id": f"req-{time.time_ns()}"},
|
||||
"body": {
|
||||
"chatid": message.chat_id,
|
||||
"msgtype": "markdown",
|
||||
"markdown": {"content": message.text},
|
||||
},
|
||||
}
|
||||
try:
|
||||
if self._ws_send_lock is None:
|
||||
self._ws_send_lock = asyncio.Lock()
|
||||
async with self._ws_send_lock:
|
||||
await self._ws.send_json(payload)
|
||||
LOGGER.info(
|
||||
"[wecom:%s] websocket reply sent chat_id=%s",
|
||||
self.account_id,
|
||||
message.chat_id,
|
||||
)
|
||||
except Exception:
|
||||
LOGGER.error("[wecom:%s] websocket send failed", self.account_id, exc_info=True)
|
||||
|
||||
|
||||
def _build(account_id: str, cfg: dict) -> Channel:
|
||||
required = ("corp_id", "agent_id", "secret", "token", "aes_key")
|
||||
connection_type = str(cfg.get("connection_type") or "webhook").strip().lower()
|
||||
if connection_type == "websocket":
|
||||
required = ("bot_id", "secret")
|
||||
else:
|
||||
required = ("corp_id", "agent_id", "secret", "token", "aes_key")
|
||||
missing = [k for k in required if not cfg.get(k)]
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"wecom account '{account_id}' missing required fields: {missing}"
|
||||
)
|
||||
try:
|
||||
agent_id = int(cfg["agent_id"])
|
||||
except (TypeError, ValueError) as err:
|
||||
raise ValueError(
|
||||
f"wecom account '{account_id}' agent_id must be int: {err}"
|
||||
) from err
|
||||
# WeCom EncodingAESKey is always 43 characters; reject placeholders early so
|
||||
# the failure is a clear message instead of a base64 "Incorrect padding" error.
|
||||
aes_key = str(cfg["aes_key"])
|
||||
if len(aes_key) != 43:
|
||||
raise ValueError(
|
||||
f"wecom account '{account_id}' aes_key (EncodingAESKey) must be 43 characters, got {len(aes_key)}"
|
||||
)
|
||||
agent_id = 0
|
||||
aes_key = ""
|
||||
corp_id = str(cfg.get("corp_id") or "")
|
||||
token = str(cfg.get("token") or "")
|
||||
bot_id = str(cfg.get("bot_id") or "")
|
||||
if connection_type == "webhook":
|
||||
try:
|
||||
agent_id = int(cfg["agent_id"])
|
||||
except (TypeError, ValueError) as err:
|
||||
raise ValueError(
|
||||
f"wecom account '{account_id}' agent_id must be int: {err}"
|
||||
) from err
|
||||
# WeCom EncodingAESKey is always 43 characters; reject placeholders early so
|
||||
# the failure is a clear message instead of a base64 "Incorrect padding" error.
|
||||
aes_key = str(cfg["aes_key"])
|
||||
if len(aes_key) != 43:
|
||||
raise ValueError(
|
||||
f"wecom account '{account_id}' aes_key (EncodingAESKey) must be 43 characters, got {len(aes_key)}"
|
||||
)
|
||||
return WeComChannel(
|
||||
WeComAccount(
|
||||
account_id=account_id,
|
||||
corp_id=str(cfg["corp_id"]),
|
||||
connection_type=connection_type,
|
||||
corp_id=corp_id,
|
||||
agent_id=agent_id,
|
||||
secret=str(cfg["secret"]),
|
||||
token=str(cfg["token"]),
|
||||
aes_key=str(cfg["aes_key"]),
|
||||
token=token,
|
||||
aes_key=aes_key,
|
||||
bot_id=bot_id,
|
||||
webhook_host=str(cfg.get("webhook_host", "0.0.0.0")),
|
||||
webhook_port=int(cfg.get("webhook_port", 3002)),
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#07C160" d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1781754070064" class="icon" viewBox="0 0 1229 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2789" xmlns:xlink="http://www.w3.org/1999/xlink" width="240.0390625" height="200"><path d="M690.8 828.8c-72 28.8-148.8 33.6-225.6 28.8-33.6-4.8-67.2-9.6-100.8-19.2-4.8 0-9.6 0-14.4 4.8-43.2 19.2-86.4 43.2-124.8 62.4-14.4 9.6-28.8 9.6-43.2 0s-14.4-24-14.4-43.2c9.6-33.6 9.6-67.2 14.4-100.8 0-4.8-4.8-9.6-4.8-14.4-48-48-86.4-96-115.2-158.4-48-115.2-38.4-230.4 28.8-336C158 137.6 263.6 75.2 388.4 46.4S633.2 32 748.4 89.6c105.6 52.8 182.4 134.4 216 249.6 14.4 43.2 19.2 86.4 14.4 129.6-24-24-52.8-28.8-81.6-14.4 0-28.8 0-57.6-9.6-86.4-19.2-67.2-57.6-120-105.6-163.2-81.6-67.2-182.4-96-288-96-110.4 9.6-206.4 48-283.2 124.8-62.4 62.4-96 139.2-91.2 230.4 4.8 76.8 38.4 139.2 86.4 192l38.4 38.4c19.2 14.4 24 28.8 14.4 48-4.8 19.2-9.6 43.2-14.4 62.4 0 4.8-4.8 9.6 0 9.6 4.8 4.8 9.6 0 9.6 0 24-14.4 52.8-28.8 76.8-48 14.4-9.6 28.8-9.6 48-4.8 81.6 24 168 24 249.6 0 4.8 0 9.6-4.8 9.6 4.8 9.6 28.8 24 48 52.8 62.4z" fill="#0082EF" p-id="2790"></path><path d="M1170.8 732.8c0 33.6-24 57.6-52.8 62.4-48 9.6-86.4 28.8-120 62.4-9.6 9.6-14.4 9.6-24 4.8-4.8-4.8-4.8-14.4 0-24 33.6-33.6 52.8-76.8 62.4-120 4.8-33.6 38.4-52.8 72-52.8 38.4 4.8 62.4 33.6 62.4 67.2z" fill="#0081EE" p-id="2791"></path><path d="M926 992c-33.6 0-62.4-24-67.2-52.8-4.8-48-28.8-86.4-62.4-115.2-4.8-4.8-9.6-9.6-4.8-19.2 4.8-14.4 14.4-14.4 24-9.6 9.6 4.8 14.4 14.4 19.2 19.2 28.8 24 62.4 38.4 96 43.2 33.6 4.8 57.6 38.4 52.8 72 4.8 33.6-24 62.4-57.6 62.4z" fill="#FA6202" p-id="2792"></path><path d="M671.6 742.4c0-33.6 19.2-57.6 52.8-67.2 48-9.6 86.4-28.8 120-62.4 9.6-9.6 19.2-9.6 24 0 4.8 4.8 4.8 14.4-4.8 24-28.8 28.8-48 62.4-57.6 105.6 0 4.8 0 14.4-4.8 19.2-9.6 33.6-38.4 52.8-72 48-33.6-4.8-57.6-33.6-57.6-67.2z" fill="#FECD00" p-id="2793"></path><path d="M1002.8 574.4c14.4 28.8 28.8 52.8 48 72 9.6 9.6 9.6 19.2 4.8 24-4.8 9.6-14.4 9.6-24 0-24-28.8-57.6-48-91.2-57.6-9.6-4.8-19.2-4.8-28.8-4.8-19.2-4.8-38.4-14.4-43.2-38.4-9.6-24-9.6-48 9.6-67.2 19.2-24 43.2-28.8 67.2-24 24 9.6 43.2 24 48 52.8 0 14.4 4.8 28.8 9.6 43.2z" fill="#2CBD00" p-id="2794"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.2 KiB |
1
web/src/assets/svg/chat-channel/wexin.svg
Normal file
1
web/src/assets/svg/chat-channel/wexin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="#07C160" d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -540,12 +540,41 @@ export const ChatChannelFormFields: Record<ChatChannelKey, FormFieldConfig[]> =
|
||||
},
|
||||
],
|
||||
[ChatChannelKey.WECOM]: [
|
||||
{
|
||||
label: 'Connection Type',
|
||||
name: 'config.credential.connection_type',
|
||||
type: FormFieldType.Select,
|
||||
required: true,
|
||||
defaultValue: 'webhook',
|
||||
options: [
|
||||
{ label: 'Webhook', value: 'webhook' },
|
||||
{ label: 'WebSocket', value: 'websocket' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Bot ID',
|
||||
name: 'config.credential.bot_id',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'AIBOTID',
|
||||
shouldRender: (values: any) =>
|
||||
values?.config?.credential?.connection_type === 'websocket',
|
||||
},
|
||||
{
|
||||
label: 'Secret',
|
||||
name: 'config.credential.secret',
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
placeholder: 'App Secret / Long-connection Secret',
|
||||
},
|
||||
{
|
||||
label: 'Corp ID',
|
||||
name: 'config.credential.corp_id',
|
||||
type: FormFieldType.Text,
|
||||
required: true,
|
||||
placeholder: 'ww1234567890abcdef',
|
||||
shouldRender: (values: any) =>
|
||||
values?.config?.credential?.connection_type !== 'websocket',
|
||||
},
|
||||
{
|
||||
label: 'Agent ID',
|
||||
@@ -553,18 +582,16 @@ export const ChatChannelFormFields: Record<ChatChannelKey, FormFieldConfig[]> =
|
||||
type: FormFieldType.Number,
|
||||
required: true,
|
||||
placeholder: '1000001',
|
||||
},
|
||||
{
|
||||
label: 'Secret',
|
||||
name: 'config.credential.secret',
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
shouldRender: (values: any) =>
|
||||
values?.config?.credential?.connection_type !== 'websocket',
|
||||
},
|
||||
{
|
||||
label: 'Token',
|
||||
name: 'config.credential.token',
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
shouldRender: (values: any) =>
|
||||
values?.config?.credential?.connection_type !== 'websocket',
|
||||
},
|
||||
{
|
||||
label: 'AES Key',
|
||||
@@ -572,6 +599,8 @@ export const ChatChannelFormFields: Record<ChatChannelKey, FormFieldConfig[]> =
|
||||
type: FormFieldType.Password,
|
||||
required: true,
|
||||
placeholder: '43 chars',
|
||||
shouldRender: (values: any) =>
|
||||
values?.config?.credential?.connection_type !== 'websocket',
|
||||
},
|
||||
],
|
||||
[ChatChannelKey.WHATSAPP]: [],
|
||||
@@ -646,6 +675,9 @@ export const ChatChannelFormDefaultValues: Record<
|
||||
// googlechat carries a non-credential discriminator (auth_mode).
|
||||
ChatChannelFormDefaultValues[ChatChannelKey.GOOGLECHAT].config.auth_mode =
|
||||
'webhook_url';
|
||||
ChatChannelFormDefaultValues[
|
||||
ChatChannelKey.WECOM
|
||||
].config.credential.connection_type = 'webhook';
|
||||
|
||||
export const getChatChannelFields = (
|
||||
key?: ChatChannelKey,
|
||||
|
||||
@@ -64,6 +64,7 @@ const ChatChannel = () => {
|
||||
ChatChannelKey.FEISHU,
|
||||
ChatChannelKey.TELEGRAM,
|
||||
ChatChannelKey.QQBOT,
|
||||
ChatChannelKey.WECOM,
|
||||
].includes(id), // Show only selected chat channels
|
||||
)
|
||||
.map((id) => ({
|
||||
|
||||
Reference in New Issue
Block a user