mirror of
https://github.com/zhayujie/bot-on-anything.git
synced 2026-01-19 01:21:06 +08:00
add channel feishu
This commit is contained in:
42
README.md
42
README.md
@@ -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`: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。
|
||||
|
||||
@@ -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)
|
||||
|
||||
184
channel/feishu/feishu_channel.py
Normal file
184
channel/feishu/feishu_channel.py
Normal 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"[]({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
67
channel/feishu/store.py
Normal 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()
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ TELEGRAM = "telegram"
|
||||
SLACK = "slack"
|
||||
HTTP = "http"
|
||||
DINGTALK = "dingtalk"
|
||||
FEISHU = "feishu"
|
||||
|
||||
# model
|
||||
OPEN_AI = "openai"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user