mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat: add feishu websocket mode
This commit is contained in:
167
channel/feishu/README.md
Normal file
167
channel/feishu/README.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# 飞书Channel使用说明
|
||||||
|
|
||||||
|
飞书Channel支持两种事件接收模式,可以根据部署环境灵活选择。
|
||||||
|
|
||||||
|
## 模式对比
|
||||||
|
|
||||||
|
| 模式 | 适用场景 | 优点 | 缺点 |
|
||||||
|
|------|---------|------|------|
|
||||||
|
| **webhook** | 生产环境 | 稳定可靠,官方推荐 | 需要公网IP或域名 |
|
||||||
|
| **websocket** | 本地开发 | 无需公网IP,开发便捷 | 需要额外依赖 |
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### 基础配置
|
||||||
|
|
||||||
|
在 `config.json` 中添加以下配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channel_type": "feishu",
|
||||||
|
"feishu_app_id": "cli_xxxxx",
|
||||||
|
"feishu_app_secret": "your_app_secret",
|
||||||
|
"feishu_token": "your_verification_token",
|
||||||
|
"feishu_bot_name": "你的机器人名称",
|
||||||
|
"feishu_event_mode": "webhook",
|
||||||
|
"feishu_port": 9891
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置项说明
|
||||||
|
|
||||||
|
- `feishu_app_id`: 飞书应用的App ID
|
||||||
|
- `feishu_app_secret`: 飞书应用的App Secret
|
||||||
|
- `feishu_token`: 事件订阅的Verification Token
|
||||||
|
- `feishu_bot_name`: 机器人名称(用于群聊@判断)
|
||||||
|
- `feishu_event_mode`: 事件接收模式,可选值:
|
||||||
|
- `"websocket"`: 长连接模式(默认)
|
||||||
|
- `"webhook"`: HTTP服务器模式
|
||||||
|
- `feishu_port`: webhook模式下的HTTP服务端口(默认9891)
|
||||||
|
|
||||||
|
## 模式一: Webhook模式(推荐生产环境)
|
||||||
|
|
||||||
|
### 1. 配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"feishu_event_mode": "webhook",
|
||||||
|
"feishu_port": 9891
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
服务将在 `http://0.0.0.0:9891` 启动。
|
||||||
|
|
||||||
|
### 3. 配置飞书应用
|
||||||
|
|
||||||
|
1. 登录[飞书开放平台](https://open.feishu.cn/)
|
||||||
|
2. 进入应用详情 -> 事件订阅
|
||||||
|
3. 选择 **将事件发送至开发者服务器**
|
||||||
|
4. 填写请求地址: `http://your-domain:9891/`
|
||||||
|
5. 添加事件: `im.message.receive_v1` (接收消息v2.0)
|
||||||
|
6. 保存配置
|
||||||
|
|
||||||
|
### 4. 注意事项
|
||||||
|
|
||||||
|
- 需要有公网IP或域名
|
||||||
|
- 确保防火墙开放对应端口
|
||||||
|
- 建议使用HTTPS(需要配置反向代理)
|
||||||
|
|
||||||
|
## 模式二: WebSocket模式(推荐本地开发)
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install lark-oapi
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"feishu_event_mode": "websocket"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
程序将自动建立与飞书开放平台的长连接。
|
||||||
|
|
||||||
|
### 4. 配置飞书应用
|
||||||
|
|
||||||
|
1. 登录[飞书开放平台](https://open.feishu.cn/)
|
||||||
|
2. 进入应用详情 -> 事件订阅
|
||||||
|
3. 选择 **使用长连接接收事件**
|
||||||
|
4. 添加事件: `im.message.receive_v1` (接收消息v2.0)
|
||||||
|
5. 保存配置
|
||||||
|
|
||||||
|
### 5. 注意事项
|
||||||
|
|
||||||
|
- 无需公网IP
|
||||||
|
- 需要能访问公网(建立WebSocket连接)
|
||||||
|
- 每个应用最多50个连接
|
||||||
|
- 集群模式下消息随机分发到一个客户端
|
||||||
|
|
||||||
|
## 平滑迁移
|
||||||
|
|
||||||
|
从webhook模式切换到websocket模式(或反向切换):
|
||||||
|
|
||||||
|
1. 修改 `config.json` 中的 `feishu_event_mode`
|
||||||
|
2. 如果切换到websocket模式,安装 `lark-oapi` 依赖
|
||||||
|
3. 重启服务
|
||||||
|
4. 在飞书开放平台修改事件订阅方式
|
||||||
|
|
||||||
|
**重要**: 同一时间只能使用一种模式,否则会导致消息重复接收。
|
||||||
|
|
||||||
|
## 消息去重机制
|
||||||
|
|
||||||
|
两种模式都使用相同的消息去重机制:
|
||||||
|
|
||||||
|
- 使用 `ExpiredDict` 存储已处理的消息ID
|
||||||
|
- 过期时间: 7.1小时
|
||||||
|
- 确保消息不会重复处理
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### WebSocket模式连接失败
|
||||||
|
|
||||||
|
```
|
||||||
|
[FeiShu] lark_oapi not installed
|
||||||
|
```
|
||||||
|
|
||||||
|
**解决**: 安装依赖 `pip install lark-oapi`
|
||||||
|
|
||||||
|
### Webhook模式端口被占用
|
||||||
|
|
||||||
|
```
|
||||||
|
Address already in use
|
||||||
|
```
|
||||||
|
|
||||||
|
**解决**: 修改 `feishu_port` 配置或关闭占用端口的进程
|
||||||
|
|
||||||
|
### 收不到消息
|
||||||
|
|
||||||
|
1. 检查飞书应用的事件订阅配置
|
||||||
|
2. 确认已添加 `im.message.receive_v1` 事件
|
||||||
|
3. 检查应用权限: 需要 `im:message` 权限
|
||||||
|
4. 查看日志中的错误信息
|
||||||
|
|
||||||
|
## 开发建议
|
||||||
|
|
||||||
|
- **本地开发**: 使用websocket模式,快速迭代
|
||||||
|
- **测试环境**: 可以使用webhook模式 + 内网穿透工具(如ngrok)
|
||||||
|
- **生产环境**: 使用webhook模式,配置正式域名和HTTPS
|
||||||
|
|
||||||
|
## 参考文档
|
||||||
|
|
||||||
|
- [飞书开放平台 - 事件订阅](https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM)
|
||||||
|
- [飞书SDK - Python](https://github.com/larksuite/oapi-sdk-python)
|
||||||
@@ -1,48 +1,80 @@
|
|||||||
"""
|
"""
|
||||||
飞书通道接入
|
飞书通道接入
|
||||||
|
|
||||||
|
支持两种事件接收模式:
|
||||||
|
1. webhook模式: 通过HTTP服务器接收事件(需要公网IP)
|
||||||
|
2. websocket模式: 通过长连接接收事件(本地开发友好)
|
||||||
|
|
||||||
|
通过配置项 feishu_event_mode 选择模式: "webhook" 或 "websocket"
|
||||||
|
|
||||||
@author Saboteur7
|
@author Saboteur7
|
||||||
@Date 2023/11/19
|
@Date 2023/11/19
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
# -*- coding=utf-8 -*-
|
# -*- coding=utf-8 -*-
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import web
|
import web
|
||||||
from channel.feishu.feishu_message import FeishuMessage
|
|
||||||
from bridge.context import Context
|
from bridge.context import Context
|
||||||
|
from bridge.context import ContextType
|
||||||
from bridge.reply import Reply, ReplyType
|
from bridge.reply import Reply, ReplyType
|
||||||
|
from channel.chat_channel import ChatChannel, check_prefix
|
||||||
|
from channel.feishu.feishu_message import FeishuMessage
|
||||||
|
from common import utils
|
||||||
|
from common.expired_dict import ExpiredDict
|
||||||
from common.log import logger
|
from common.log import logger
|
||||||
from common.singleton import singleton
|
from common.singleton import singleton
|
||||||
from config import conf
|
from config import conf
|
||||||
from common.expired_dict import ExpiredDict
|
|
||||||
from bridge.context import ContextType
|
|
||||||
from channel.chat_channel import ChatChannel, check_prefix
|
|
||||||
from common import utils
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
URL_VERIFICATION = "url_verification"
|
URL_VERIFICATION = "url_verification"
|
||||||
|
|
||||||
|
# 尝试导入飞书SDK,如果未安装则websocket模式不可用
|
||||||
|
try:
|
||||||
|
import lark_oapi as lark
|
||||||
|
|
||||||
|
LARK_SDK_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
LARK_SDK_AVAILABLE = False
|
||||||
|
logger.warning(
|
||||||
|
"[FeiShu] lark_oapi not installed, websocket mode is not available. Install with: pip install lark-oapi")
|
||||||
|
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
class FeiShuChanel(ChatChannel):
|
class FeiShuChanel(ChatChannel):
|
||||||
feishu_app_id = conf().get('feishu_app_id')
|
feishu_app_id = conf().get('feishu_app_id')
|
||||||
feishu_app_secret = conf().get('feishu_app_secret')
|
feishu_app_secret = conf().get('feishu_app_secret')
|
||||||
feishu_token = conf().get('feishu_token')
|
feishu_token = conf().get('feishu_token')
|
||||||
|
feishu_event_mode = conf().get('feishu_event_mode', 'websocket') # webhook 或 websocket
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# 历史消息id暂存,用于幂等控制
|
# 历史消息id暂存,用于幂等控制
|
||||||
self.receivedMsgs = ExpiredDict(60 * 60 * 7.1)
|
self.receivedMsgs = ExpiredDict(60 * 60 * 7.1)
|
||||||
logger.info("[FeiShu] app_id={}, app_secret={} verification_token={}".format(
|
logger.info("[FeiShu] app_id={}, app_secret={}, verification_token={}, event_mode={}".format(
|
||||||
self.feishu_app_id, self.feishu_app_secret, self.feishu_token))
|
self.feishu_app_id, self.feishu_app_secret, self.feishu_token, self.feishu_event_mode))
|
||||||
# 无需群校验和前缀
|
# 无需群校验和前缀
|
||||||
conf()["group_name_white_list"] = ["ALL_GROUP"]
|
conf()["group_name_white_list"] = ["ALL_GROUP"]
|
||||||
conf()["single_chat_prefix"] = [""]
|
conf()["single_chat_prefix"] = [""]
|
||||||
|
|
||||||
|
# 验证配置
|
||||||
|
if self.feishu_event_mode == 'websocket' and not LARK_SDK_AVAILABLE:
|
||||||
|
logger.error("[FeiShu] websocket mode requires lark_oapi. Please install: pip install lark-oapi")
|
||||||
|
raise Exception("lark_oapi not installed")
|
||||||
|
|
||||||
def startup(self):
|
def startup(self):
|
||||||
|
if self.feishu_event_mode == 'websocket':
|
||||||
|
self._startup_websocket()
|
||||||
|
else:
|
||||||
|
self._startup_webhook()
|
||||||
|
|
||||||
|
def _startup_webhook(self):
|
||||||
|
"""启动HTTP服务器接收事件(webhook模式)"""
|
||||||
|
logger.info("[FeiShu] Starting in webhook mode...")
|
||||||
urls = (
|
urls = (
|
||||||
'/', 'channel.feishu.feishu_channel.FeishuController'
|
'/', 'channel.feishu.feishu_channel.FeishuController'
|
||||||
)
|
)
|
||||||
@@ -50,6 +82,109 @@ class FeiShuChanel(ChatChannel):
|
|||||||
port = conf().get("feishu_port", 9891)
|
port = conf().get("feishu_port", 9891)
|
||||||
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
||||||
|
|
||||||
|
def _startup_websocket(self):
|
||||||
|
"""启动长连接接收事件(websocket模式)"""
|
||||||
|
logger.info("[FeiShu] Starting in websocket mode...")
|
||||||
|
|
||||||
|
# 创建事件处理器
|
||||||
|
def handle_message_event(data: lark.im.v1.P2ImMessageReceiveV1) -> None:
|
||||||
|
"""处理接收消息事件 v2.0"""
|
||||||
|
try:
|
||||||
|
logger.debug(f"[FeiShu] websocket receive event: {lark.JSON.marshal(data, indent=2)}")
|
||||||
|
|
||||||
|
# 转换为标准的event格式
|
||||||
|
event_dict = json.loads(lark.JSON.marshal(data))
|
||||||
|
event = event_dict.get("event", {})
|
||||||
|
|
||||||
|
# 处理消息
|
||||||
|
self._handle_message_event(event)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[FeiShu] websocket handle message error: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# 构建事件分发器
|
||||||
|
event_handler = lark.EventDispatcherHandler.builder("", "") \
|
||||||
|
.register_p2_im_message_receive_v1(handle_message_event) \
|
||||||
|
.build()
|
||||||
|
|
||||||
|
# 创建长连接客户端
|
||||||
|
ws_client = lark.ws.Client(
|
||||||
|
self.feishu_app_id,
|
||||||
|
self.feishu_app_secret,
|
||||||
|
event_handler=event_handler,
|
||||||
|
log_level=lark.LogLevel.DEBUG if conf().get("debug") else lark.LogLevel.INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
# 在新线程中启动客户端,避免阻塞主线程
|
||||||
|
def start_client():
|
||||||
|
try:
|
||||||
|
logger.info("[FeiShu] Websocket client starting...")
|
||||||
|
ws_client.start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[FeiShu] Websocket client error: {e}", exc_info=True)
|
||||||
|
|
||||||
|
ws_thread = threading.Thread(target=start_client, daemon=True)
|
||||||
|
ws_thread.start()
|
||||||
|
|
||||||
|
# 保持主线程运行
|
||||||
|
logger.info("[FeiShu] Websocket mode started, waiting for events...")
|
||||||
|
ws_thread.join()
|
||||||
|
|
||||||
|
def _handle_message_event(self, event: dict):
|
||||||
|
"""
|
||||||
|
处理消息事件的核心逻辑
|
||||||
|
webhook和websocket模式共用此方法
|
||||||
|
"""
|
||||||
|
if not event.get("message") or not event.get("sender"):
|
||||||
|
logger.warning(f"[FeiShu] invalid message, event={event}")
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = event.get("message")
|
||||||
|
|
||||||
|
# 幂等判断
|
||||||
|
msg_id = msg.get("message_id")
|
||||||
|
if self.receivedMsgs.get(msg_id):
|
||||||
|
logger.warning(f"[FeiShu] repeat msg filtered, msg_id={msg_id}")
|
||||||
|
return
|
||||||
|
self.receivedMsgs[msg_id] = True
|
||||||
|
|
||||||
|
is_group = False
|
||||||
|
chat_type = msg.get("chat_type")
|
||||||
|
|
||||||
|
if chat_type == "group":
|
||||||
|
if not msg.get("mentions") and msg.get("message_type") == "text":
|
||||||
|
# 群聊中未@不响应
|
||||||
|
return
|
||||||
|
if msg.get("mentions") and msg.get("mentions")[0].get("name") != conf().get("feishu_bot_name") and msg.get(
|
||||||
|
"message_type") == "text":
|
||||||
|
# 不是@机器人,不响应
|
||||||
|
return
|
||||||
|
# 群聊
|
||||||
|
is_group = True
|
||||||
|
receive_id_type = "chat_id"
|
||||||
|
elif chat_type == "p2p":
|
||||||
|
receive_id_type = "open_id"
|
||||||
|
else:
|
||||||
|
logger.warning("[FeiShu] message ignore")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 构造飞书消息对象
|
||||||
|
feishu_msg = FeishuMessage(event, is_group=is_group, access_token=self.fetch_access_token())
|
||||||
|
if not feishu_msg:
|
||||||
|
return
|
||||||
|
|
||||||
|
context = self._compose_context(
|
||||||
|
feishu_msg.ctype,
|
||||||
|
feishu_msg.content,
|
||||||
|
isgroup=is_group,
|
||||||
|
msg=feishu_msg,
|
||||||
|
receive_id_type=receive_id_type,
|
||||||
|
no_need_at=True
|
||||||
|
)
|
||||||
|
if context:
|
||||||
|
self.produce(context)
|
||||||
|
logger.info(f"[FeiShu] query={feishu_msg.content}, type={feishu_msg.ctype}")
|
||||||
|
|
||||||
def send(self, reply: Reply, context: Context):
|
def send(self, reply: Reply, context: Context):
|
||||||
msg = context.get("msg")
|
msg = context.get("msg")
|
||||||
is_group = context["isgroup"]
|
is_group = context["isgroup"]
|
||||||
@@ -143,9 +278,39 @@ class FeiShuChanel(ChatChannel):
|
|||||||
os.remove(temp_name)
|
os.remove(temp_name)
|
||||||
return upload_response.json().get("data").get("image_key")
|
return upload_response.json().get("data").get("image_key")
|
||||||
|
|
||||||
|
def _compose_context(self, ctype: ContextType, content, **kwargs):
|
||||||
|
context = Context(ctype, content)
|
||||||
|
context.kwargs = kwargs
|
||||||
|
if "origin_ctype" not in context:
|
||||||
|
context["origin_ctype"] = ctype
|
||||||
|
|
||||||
|
cmsg = context["msg"]
|
||||||
|
context["session_id"] = cmsg.from_user_id
|
||||||
|
context["receiver"] = cmsg.other_user_id
|
||||||
|
|
||||||
|
if ctype == ContextType.TEXT:
|
||||||
|
# 1.文本请求
|
||||||
|
# 图片生成处理
|
||||||
|
img_match_prefix = check_prefix(content, conf().get("image_create_prefix"))
|
||||||
|
if img_match_prefix:
|
||||||
|
content = content.replace(img_match_prefix, "", 1)
|
||||||
|
context.type = ContextType.IMAGE_CREATE
|
||||||
|
else:
|
||||||
|
context.type = ContextType.TEXT
|
||||||
|
context.content = content.strip()
|
||||||
|
|
||||||
|
elif context.type == ContextType.VOICE:
|
||||||
|
# 2.语音请求
|
||||||
|
if "desire_rtype" not in context and conf().get("voice_reply_voice"):
|
||||||
|
context["desire_rtype"] = ReplyType.VOICE
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class FeishuController:
|
class FeishuController:
|
||||||
|
"""
|
||||||
|
HTTP服务器控制器,用于webhook模式
|
||||||
|
"""
|
||||||
# 类常量
|
# 类常量
|
||||||
FAILED_MSG = '{"success": false}'
|
FAILED_MSG = '{"success": false}'
|
||||||
SUCCESS_MSG = '{"success": true}'
|
SUCCESS_MSG = '{"success": true}'
|
||||||
@@ -175,80 +340,10 @@ class FeishuController:
|
|||||||
# 处理消息事件
|
# 处理消息事件
|
||||||
event = request.get("event")
|
event = request.get("event")
|
||||||
if header.get("event_type") == self.MESSAGE_RECEIVE_TYPE and event:
|
if header.get("event_type") == self.MESSAGE_RECEIVE_TYPE and event:
|
||||||
if not event.get("message") or not event.get("sender"):
|
channel._handle_message_event(event)
|
||||||
logger.warning(f"[FeiShu] invalid message, msg={request}")
|
|
||||||
return self.FAILED_MSG
|
|
||||||
msg = event.get("message")
|
|
||||||
|
|
||||||
# 幂等判断
|
|
||||||
if channel.receivedMsgs.get(msg.get("message_id")):
|
|
||||||
logger.warning(f"[FeiShu] repeat msg filtered, event_id={header.get('event_id')}")
|
|
||||||
return self.SUCCESS_MSG
|
|
||||||
channel.receivedMsgs[msg.get("message_id")] = True
|
|
||||||
|
|
||||||
is_group = False
|
|
||||||
chat_type = msg.get("chat_type")
|
|
||||||
if chat_type == "group":
|
|
||||||
if not msg.get("mentions") and msg.get("message_type") == "text":
|
|
||||||
# 群聊中未@不响应
|
|
||||||
return self.SUCCESS_MSG
|
|
||||||
if msg.get("mentions")[0].get("name") != conf().get("feishu_bot_name") and msg.get("message_type") == "text":
|
|
||||||
# 不是@机器人,不响应
|
|
||||||
return self.SUCCESS_MSG
|
|
||||||
# 群聊
|
|
||||||
is_group = True
|
|
||||||
receive_id_type = "chat_id"
|
|
||||||
elif chat_type == "p2p":
|
|
||||||
receive_id_type = "open_id"
|
|
||||||
else:
|
|
||||||
logger.warning("[FeiShu] message ignore")
|
|
||||||
return self.SUCCESS_MSG
|
|
||||||
# 构造飞书消息对象
|
|
||||||
feishu_msg = FeishuMessage(event, is_group=is_group, access_token=channel.fetch_access_token())
|
|
||||||
if not feishu_msg:
|
|
||||||
return self.SUCCESS_MSG
|
|
||||||
|
|
||||||
context = self._compose_context(
|
|
||||||
feishu_msg.ctype,
|
|
||||||
feishu_msg.content,
|
|
||||||
isgroup=is_group,
|
|
||||||
msg=feishu_msg,
|
|
||||||
receive_id_type=receive_id_type,
|
|
||||||
no_need_at=True
|
|
||||||
)
|
|
||||||
if context:
|
|
||||||
channel.produce(context)
|
|
||||||
logger.info(f"[FeiShu] query={feishu_msg.content}, type={feishu_msg.ctype}")
|
|
||||||
return self.SUCCESS_MSG
|
return self.SUCCESS_MSG
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return self.FAILED_MSG
|
return self.FAILED_MSG
|
||||||
|
|
||||||
def _compose_context(self, ctype: ContextType, content, **kwargs):
|
|
||||||
context = Context(ctype, content)
|
|
||||||
context.kwargs = kwargs
|
|
||||||
if "origin_ctype" not in context:
|
|
||||||
context["origin_ctype"] = ctype
|
|
||||||
|
|
||||||
cmsg = context["msg"]
|
|
||||||
context["session_id"] = cmsg.from_user_id
|
|
||||||
context["receiver"] = cmsg.other_user_id
|
|
||||||
|
|
||||||
if ctype == ContextType.TEXT:
|
|
||||||
# 1.文本请求
|
|
||||||
# 图片生成处理
|
|
||||||
img_match_prefix = check_prefix(content, conf().get("image_create_prefix"))
|
|
||||||
if img_match_prefix:
|
|
||||||
content = content.replace(img_match_prefix, "", 1)
|
|
||||||
context.type = ContextType.IMAGE_CREATE
|
|
||||||
else:
|
|
||||||
context.type = ContextType.TEXT
|
|
||||||
context.content = content.strip()
|
|
||||||
|
|
||||||
elif context.type == ContextType.VOICE:
|
|
||||||
# 2.语音请求
|
|
||||||
if "desire_rtype" not in context and conf().get("voice_reply_voice"):
|
|
||||||
context["desire_rtype"] = ReplyType.VOICE
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"@bot"
|
"@bot"
|
||||||
],
|
],
|
||||||
"group_name_white_list": [
|
"group_name_white_list": [
|
||||||
"ChatGPT测试群",
|
"Agent测试群",
|
||||||
"ChatGPT测试群2"
|
"ChatGPT测试群2"
|
||||||
],
|
],
|
||||||
"image_create_prefix": [
|
"image_create_prefix": [
|
||||||
|
|||||||
10
config.py
10
config.py
@@ -148,6 +148,7 @@ available_setting = {
|
|||||||
"feishu_app_secret": "", # 飞书机器人APP secret
|
"feishu_app_secret": "", # 飞书机器人APP secret
|
||||||
"feishu_token": "", # 飞书 verification token
|
"feishu_token": "", # 飞书 verification token
|
||||||
"feishu_bot_name": "", # 飞书机器人的名字
|
"feishu_bot_name": "", # 飞书机器人的名字
|
||||||
|
"feishu_event_mode": "websocket", # 飞书事件接收模式: webhook(HTTP服务器) 或 websocket(长连接)
|
||||||
# 钉钉配置
|
# 钉钉配置
|
||||||
"dingtalk_client_id": "", # 钉钉机器人Client ID
|
"dingtalk_client_id": "", # 钉钉机器人Client ID
|
||||||
"dingtalk_client_secret": "", # 钉钉机器人Client Secret
|
"dingtalk_client_secret": "", # 钉钉机器人Client Secret
|
||||||
@@ -199,12 +200,14 @@ class Config(dict):
|
|||||||
self.user_datas = {}
|
self.user_datas = {}
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
if key not in available_setting:
|
# 跳过以下划线开头的注释字段
|
||||||
|
if not key.startswith("_") and key not in available_setting:
|
||||||
raise Exception("key {} not in available_setting".format(key))
|
raise Exception("key {} not in available_setting".format(key))
|
||||||
return super().__getitem__(key)
|
return super().__getitem__(key)
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
if key not in available_setting:
|
# 跳过以下划线开头的注释字段
|
||||||
|
if not key.startswith("_") and key not in available_setting:
|
||||||
raise Exception("key {} not in available_setting".format(key))
|
raise Exception("key {} not in available_setting".format(key))
|
||||||
return super().__setitem__(key, value)
|
return super().__setitem__(key, value)
|
||||||
|
|
||||||
@@ -286,6 +289,9 @@ def load_config():
|
|||||||
# Some online deployment platforms (e.g. Railway) deploy project from github directly. So you shouldn't put your secrets like api key in a config file, instead use environment variables to override the default config.
|
# Some online deployment platforms (e.g. Railway) deploy project from github directly. So you shouldn't put your secrets like api key in a config file, instead use environment variables to override the default config.
|
||||||
for name, value in os.environ.items():
|
for name, value in os.environ.items():
|
||||||
name = name.lower()
|
name = name.lower()
|
||||||
|
# 跳过以下划线开头的注释字段
|
||||||
|
if name.startswith("_"):
|
||||||
|
continue
|
||||||
if name in available_setting:
|
if name in available_setting:
|
||||||
logger.info("[INIT] override config by environ args: {}={}".format(name, value))
|
logger.info("[INIT] override config by environ args: {}={}".format(name, value))
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ elevenlabs==1.0.3 # elevenlabs TTS
|
|||||||
#install plugin
|
#install plugin
|
||||||
dulwich
|
dulwich
|
||||||
|
|
||||||
# wechatmp && wechatcom
|
# wechatmp && wechatcom && feishu
|
||||||
web.py
|
web.py
|
||||||
wechatpy
|
wechatpy
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ websocket-client==1.2.0
|
|||||||
|
|
||||||
# claude bot
|
# claude bot
|
||||||
curl_cffi
|
curl_cffi
|
||||||
|
|
||||||
# claude API
|
# claude API
|
||||||
anthropic==0.25.0
|
anthropic==0.25.0
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ pre-commit
|
|||||||
web.py
|
web.py
|
||||||
linkai>=0.0.6.0
|
linkai>=0.0.6.0
|
||||||
agentmesh-sdk>=0.1.3
|
agentmesh-sdk>=0.1.3
|
||||||
|
|
||||||
|
# feishu websocket mode
|
||||||
|
lark-oapi
|
||||||
Reference in New Issue
Block a user