add channel feishu

This commit is contained in:
wujiyu
2023-04-07 13:06:23 +08:00
parent 7e1c112a15
commit 02d6d65d49
6 changed files with 308 additions and 1 deletions

View File

@@ -21,7 +21,7 @@
- [x] [Telegram](https://github.com/zhayujie/bot-on-anything#6telegram)
- [x] [QQ](https://github.com/zhayujie/bot-on-anything#5qq)
- [x] 钉钉
- [ ] 飞书
- [x] [飞书](https://github.com/zhayujie/bot-on-anything#11%E9%A3%9E%E4%B9%A6)
- [x] [Gmail](https://github.com/zhayujie/bot-on-anything#7gmail)
- [x] [Slack](https://github.com/zhayujie/bot-on-anything#8slack)
@@ -522,6 +522,46 @@ https://open.dingtalk.com/document/orgapp/custom-robot-access
地址: https://open-dev.dingtalk.com/fe/app#/corp/robot
添加机器人,在开发管理中设置服务器出口ip(在部署机执行curl ifconfig.me就可以得到)和消息接收地址(配置中的对外地址如 https://xx.xx.com:8081)
### 11.飞书
**依赖**
```bash
pip3 install requests flask
```
**配置**
```json
"channel": {
"type": "dingtalk",
"feishu": {
"image_create_prefix": [
"画",
"draw",
"Draw"
],
"port": "8082",//对外端口
"app_id": "xxx", //应用app_id
"app_secret": "xxx",//应用Secret
"verification_token": "xxx" //事件订阅 Verification Token
}
}
```
**生成机器人**
地址: https://open.feishu.cn/app/
1. 添加企业自建应用
2. 开通权限
- im:message
- im:message.group_at_msg
- im:message.group_at_msg:readonly
- im:message.p2p_msg
- im:message.p2p_msg:readonly
- im:message:send_as_bot
3. 订阅菜单添加事件(接收消息v2.0) 配置请求地址(配置中的对外地址如 https://xx.xx.com:8081)
4. 版本管理与发布中上架应用,app中会收到审核信息,通过审核后在群里添加自建应用
### 通用配置
+ `clear_memory_commands`: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。

View File

@@ -48,6 +48,10 @@ def create_channel(channel_type):
elif channel_type == const.DINGTALK:
from channel.dingtalk.dingtalk_channel import DingTalkChannel
return DingTalkChannel()
elif channel_type == const.FEISHU:
from channel.feishu.feishu_channel import FeiShuChannel
return FeiShuChannel()
else:
raise RuntimeError("unknown channel_type in config.json: " + channel_type)

View File

@@ -0,0 +1,184 @@
# encoding:utf-8
import json
import hmac
import hashlib
import base64
import time
import requests
from urllib.parse import quote_plus
from common import log
from flask import Flask, request, render_template, make_response
from common import const
from common import functions
from config import channel_conf
from config import channel_conf_val
from channel.channel import Channel
from urllib import request as url_request
from channel.feishu.store import MemoryStore
class FeiShuChannel(Channel):
def __init__(self):
self.app_id = channel_conf(
const.FEISHU).get('app_id')
self.app_secret = channel_conf(
const.FEISHU).get('app_secret')
self.verification_token = channel_conf(
const.FEISHU).get('verification_token')
log.info("[FeiShu] app_id={}, app_secret={} verification_token={}".format(
self.app_id, self.app_secret, self.verification_token))
self.memory_store = MemoryStore()
def startup(self):
http_app.run(host='0.0.0.0', port=channel_conf(
const.FEISHU).get('port'))
def get_tenant_access_token(self):
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
headers = {
"Content-Type": "application/json"
}
req_body = {
"app_id": self.app_id,
"app_secret": self.app_secret
}
data = bytes(json.dumps(req_body), encoding='utf8')
req = url_request.Request(url=url, data=data,
headers=headers, method='POST')
try:
response = url_request.urlopen(req)
except Exception as e:
print(e.read().decode())
return ""
rsp_body = response.read().decode('utf-8')
rsp_dict = json.loads(rsp_body)
code = rsp_dict.get("code", -1)
if code != 0:
print("get tenant_access_token error, code =", code)
return ""
return rsp_dict.get("tenant_access_token", "")
def notify_feishu(self, token, receive_type, receive_id, at_id, answer):
log.info("notify_feishu.receive_type = {} receive_id={}",
receive_type, receive_id)
url = "https://open.feishu.cn/open-apis/im/v1/messages"
params = {"receive_id_type": receive_type}
# text = at_id and "<at user_id=\"%s\">%s</at>" % (
# at_id, answer.lstrip()) or answer.lstrip()
text = answer.lstrip()
log.info("notify_feishu.text = {}", text)
msgContent = {
"text": text,
}
req = {
"receive_id": receive_id, # chat id
"msg_type": "text",
"content": json.dumps(msgContent),
}
payload = json.dumps(req)
headers = {
# your access token
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
}
response = requests.request(
"POST", url, params=params, headers=headers, data=payload
)
log.info("notify_feishu.response.content = {}", response.content)
def handle(self, message):
event = message["event"]
msg = event["message"]
messageId = msg["message_id"]
chat_type = msg["chat_type"]
sender_id = event["sender"]["sender_id"]["open_id"]
prompt = json.loads(msg["content"])["text"]
prompt = prompt.replace("@_user_1", "")
#重复
r, v = self.memory_store.get(messageId)
if v:
return {'ret': 200}
self.memory_store.set(messageId, True)
# 非文本不处理
message_type = msg["message_type"]
if message_type != "text":
return {'ret': 200}
if chat_type == "group":
mentions = msg["mentions"]
# 日常群沟通要@才生效
if not mentions:
return {'ret': 200}
receive_type = "chat_id"
receive_id = msg.get("chat_id")
at_id = sender_id
elif chat_type == "p2p":
receive_type = "open_id"
receive_id = sender_id
# 调用发消息 API 之前,先要获取 API 调用凭证tenant_access_token
access_token = self.get_tenant_access_token()
if access_token == "":
log.error("send message access_token is empty")
return {'ret': 204}
context = dict()
img_match_prefix = functions.check_prefix(
prompt, channel_conf_val(const.DINGTALK, 'image_create_prefix'))
if img_match_prefix:
prompt = prompt.split(img_match_prefix, 1)[1].strip()
context['type'] = 'IMAGE_CREATE'
context['from_user_id'] = str(sender_id)
reply = super().build_reply_content(prompt, context)
if img_match_prefix:
if not isinstance(reply, list):
return {'ret': 204}
images = ""
for url in reply:
images += f"[!['IMAGE_CREATE']({url})]({url})\n"
reply = images
# 机器人 echo 收到的消息
self.notify_feishu(access_token, receive_type,
receive_id, at_id, reply)
return {'ret': 200}
def handle_request_url_verify(self, post_obj):
# 原样返回 challenge 字段内容
challenge = post_obj.get("challenge", "")
return {'challenge': challenge}
feishu = FeiShuChannel()
http_app = Flask(__name__,)
@http_app.route("/", methods=['POST'])
def chat():
# log.info("[FeiShu] chat_headers={}".format(str(request.headers)))
log.info("[FeiShu] chat={}".format(str(request.data)))
obj = json.loads(request.data)
if not obj:
return {'ret': 201}
# 校验 verification token 是否匹配token 不匹配说明该回调并非来自开发平台
headers = obj.get("header")
if not headers:
return {'ret': 201}
token = headers.get("token", "")
if token != feishu.verification_token:
log.error("verification token not match, token = {}", token)
return {'ret': 201}
# 根据 type 处理不同类型事件
t = obj.get("type", "")
if "url_verification" == t: # 验证请求 URL 是否有效
return feishu.handle_request_url_verify(obj)
elif headers.get("event_type", None) == "im.message.receive_v1": # 事件回调
return feishu.handle(obj)
return {'ret': 202}

67
channel/feishu/store.py Normal file
View File

@@ -0,0 +1,67 @@
# -*- coding: UTF-8 -*-
import time
from threading import Lock
class Store(object):
"""
This is an interface to storage (Key, Value) pairs for sdk.
"""
def get(self, key): # type: (str) -> Tuple[bool, str]
return False, ''
def set(self, key, value, expire): # type: (str, str, int) -> None
"""
storage key, value into the store, value has an expire time.(unit: second)
"""
pass
class ExpireValue(object):
def __init__(self, value, expireTime): # type: (str, int) -> None
self.value = value
self.expireTime = expireTime
class MemoryStore(Store):
"""
This is an implement of `StoreInterface` which stores data in the memory
"""
def __init__(self): # type: () -> None
self.data = {} # type: Dict[str, ExpireValue]
self.mutex = Lock() # type: Lock
def get(self, key): # type: (str) -> Tuple[bool, str]
# print('get %s' % key)
self.mutex.acquire()
try:
val = self.data.get(key)
if val is None:
return False, ""
else:
if val.expireTime == -1:
return True, val.value
elif val.expireTime < int(time.time()):
self.data.pop(key)
return False, ""
else:
return True, val.value
finally:
self.mutex.release()
def set(self, key, value, expire=None): # type: (str, str, int) -> None
# print('put %s=%s, expire=%s' % (key, value, expire))
"""
storage key, value into the store, value has an expire time.(unit: second)
"""
self.mutex.acquire()
try:
self.data[key] = ExpireValue(
value, expire == None and -1 or int(time.time()) + expire)
finally:
self.mutex.release()

View File

@@ -9,6 +9,7 @@ TELEGRAM = "telegram"
SLACK = "slack"
HTTP = "http"
DINGTALK = "dingtalk"
FEISHU = "feishu"
# model
OPEN_AI = "openai"

View File

@@ -67,6 +67,17 @@
"dingtalk_token": "xx",
"dingtalk_post_token": "xx",
"dingtalk_secret": "xx"
},
"feishu": {
"image_create_prefix": [
"画",
"draw",
"Draw"
],
"port": "8082",
"app_id": "xxx",
"app_secret": "xxx",
"verification_token": "xxx"
}
},
"common": {