From 2f0fc370127f16279703aea48f9518d6ca2f51d4 Mon Sep 17 00:00:00 2001 From: wujiyu115 Date: Wed, 29 Mar 2023 10:05:08 +0800 Subject: [PATCH 1/5] add dingding channel --- README.md | 29 +++++++++++ channel/channel_factory.py | 4 ++ channel/dd/dd_channel.py | 102 +++++++++++++++++++++++++++++++++++++ common/const.py | 1 + config-template.json | 8 +++ requirements.txt | 3 +- 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 channel/dd/dd_channel.py diff --git a/README.md b/README.md index 8351f3a..f310419 100644 --- a/README.md +++ b/README.md @@ -492,6 +492,35 @@ pip3 install PyJWT flask 服务器运行:部署后访问 `http://公网域名或IP:端口` +### 10.钉钉 + +**依赖** + +```bash +pip3 install requests flask +``` +**配置** + +```bash +"channel": { + "type": "dingding", + "dingding": { + "image_create_prefix": ["画", "draw", "Draw"], + "port": "8081", //对外端口 + "dd_token": "xx", //webhook地址的access_token + "dd_post_token": "xx", //钉钉post回消息时header中带的检验token + "dd_secret": "xx"// 安全加密加签串,群机器人中 + } + } +``` +钉钉开放平台说明: https://open.dingtalk.com/document/robots/customize-robot-security-settin.dingtalk.com/robot/send?access_token=906dadcbc7750fef5ff60d3445b66d5bbca32804f40fbdb59039a29b20b9a3f0gs + +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) ### 通用配置 diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 7b53e80..2c0bde3 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -45,5 +45,9 @@ def create_channel(channel_type): from channel.http.http_channel import HttpChannel return HttpChannel() + elif channel_type == const.DINGDING: + from channel.dd.dd_channel import DDChannel + return DDChannel() + else: raise RuntimeError("unknown channel_type in config.json: " + channel_type) diff --git a/channel/dd/dd_channel.py b/channel/dd/dd_channel.py new file mode 100644 index 0000000..6664daa --- /dev/null +++ b/channel/dd/dd_channel.py @@ -0,0 +1,102 @@ +# 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 + +class DDChannel(Channel): + def __init__(self): + self.dd_token = channel_conf(const.DINGDING).get('dd_token') + self.dd_post_token = channel_conf(const.DINGDING).get('dd_post_token') + self.dd_secret = channel_conf(const.DINGDING).get('dd_secret') + log.info("[DingDing] dd_secret={}, dd_token={} dd_post_token={}".format(self.dd_secret, self.dd_token, self.dd_post_token)) + + def startup(self): + + http_app.run(host='0.0.0.0', port=channel_conf(const.DINGDING).get('port')) + + def notify_dingding(self, answer): + data = { + "msgtype": "text", + "text": { + "content": answer + }, + + "at": { + "atMobiles": [ + "" + ], + "isAtAll": False + } + } + + timestamp = round(time.time() * 1000) + secret_enc = bytes(self.dd_secret, encoding='utf-8') + string_to_sign = '{}\n{}'.format(timestamp, self.dd_secret) + string_to_sign_enc = bytes(string_to_sign, encoding='utf-8') + hmac_code = hmac.new(secret_enc, string_to_sign_enc, + digestmod=hashlib.sha256).digest() + sign = quote_plus(base64.b64encode(hmac_code)) + + notify_url = f"https://oapi.dingtalk.com/robot/send?access_token={self.dd_token}×tamp={timestamp}&sign={sign}" + try: + r = requests.post(notify_url, json=data) + reply = r.json() + # log.info("[DingDing] reply={}".format(str(reply))) + except Exception as e: + log.error(e) + + def handle(self, data): + prompt = data['text']['content'] + conversation_id = data['conversationId'] + sender_id = data['senderId'] + context = dict() + img_match_prefix = functions.check_prefix( + prompt, channel_conf_val(const.DINGDING, 'image_create_prefix')) + if img_match_prefix: + prompt = prompt.split(img_match_prefix, 1)[1].strip() + context['type'] = 'IMAGE_CREATE' + id = sender_id + context['from_user_id'] = str(id) + reply = super().build_reply_content(prompt, context) + if img_match_prefix: + if not isinstance(reply, list): + return reply + images = "" + for url in reply: + images += f"[!['IMAGE_CREATE']({url})]({url})\n" + reply = images + return reply + + +dd = DDChannel() +http_app = Flask(__name__,) + + +@http_app.route("/", methods=['POST']) +def chat(): + # log.info("[DingDing] chat_headers={}".format(str(request.headers))) + log.info("[DingDing] chat={}".format(str(request.data))) + token = request.headers.get('token') + if dd.dd_post_token and token != dd.dd_post_token: + return {'ret': 203} + #TODO: Verify identity + data = json.loads(request.data) + if data: + content = data['text']['content'] + if not content: + return + reply_text = dd.handle(data=data) + dd.notify_dingding(reply_text) + return {'ret': 200} + return {'ret': 201} diff --git a/common/const.py b/common/const.py index ad2a061..95e10bc 100644 --- a/common/const.py +++ b/common/const.py @@ -8,6 +8,7 @@ GMAIL = "gmail" TELEGRAM = "telegram" SLACK = "slack" HTTP = "http" +DINGDING = "dingding" # model OPEN_AI = "openai" diff --git a/config-template.json b/config-template.json index e494383..d416001 100644 --- a/config-template.json +++ b/config-template.json @@ -59,6 +59,14 @@ "http_auth_secret_key": "6d25a684-9558-11e9-aa94-efccd7a0659b", "http_auth_password": "6.67428e-11", "port": "80" + }, + + "dingding": { + "image_create_prefix": ["画", "draw", "Draw"], + "port": "8081", + "dd_token": "xx", + "dd_post_token": "xx", + "dd_secret": "xx" } }, "common": { diff --git a/requirements.txt b/requirements.txt index 57a56a2..23dfbc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ PyJWT flask itchat-uos==1.5.0.dev0 -openai \ No newline at end of file +openai +requests \ No newline at end of file From 94802c1e2b79f1cfa41d622b972723756410b29c Mon Sep 17 00:00:00 2001 From: wujiyu115 Date: Wed, 29 Mar 2023 11:28:44 +0800 Subject: [PATCH 2/5] remove config --- config-template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-template.json b/config-template.json index d416001..4c2f393 100644 --- a/config-template.json +++ b/config-template.json @@ -3,7 +3,7 @@ "type" : "chatgpt", "openai": { "api_key": "YOUR API KEY", - "api_base": "https://api.xxx.com/v1", + "api_base": "", "model": "gpt-3.5-turbo", "proxy": "", "conversation_max_tokens": 1000, From 7e1c112a15787406efe80e18c8cbe406b7221693 Mon Sep 17 00:00:00 2001 From: wujiyu115 Date: Thu, 30 Mar 2023 11:39:12 +0800 Subject: [PATCH 3/5] rename dingding to dingtalk --- README.md | 12 +++---- channel/channel_factory.py | 6 ++-- .../dingtalk_channel.py} | 35 +++++++++---------- common/const.py | 2 +- config-template.json | 8 ++--- 5 files changed, 31 insertions(+), 32 deletions(-) rename channel/{dd/dd_channel.py => dingtalk/dingtalk_channel.py} (69%) diff --git a/README.md b/README.md index f310419..247ba7a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - [ ] 企业微信 - [x] [Telegram](https://github.com/zhayujie/bot-on-anything#6telegram) - [x] [QQ](https://github.com/zhayujie/bot-on-anything#5qq) - - [ ] 钉钉 + - [x] 钉钉 - [ ] 飞书 - [x] [Gmail](https://github.com/zhayujie/bot-on-anything#7gmail) - [x] [Slack](https://github.com/zhayujie/bot-on-anything#8slack) @@ -503,13 +503,13 @@ pip3 install requests flask ```bash "channel": { - "type": "dingding", - "dingding": { + "type": "dingtalk", + "dingtalk": { "image_create_prefix": ["画", "draw", "Draw"], "port": "8081", //对外端口 - "dd_token": "xx", //webhook地址的access_token - "dd_post_token": "xx", //钉钉post回消息时header中带的检验token - "dd_secret": "xx"// 安全加密加签串,群机器人中 + "dingtalk_token": "xx", //webhook地址的access_token + "dingtalk_post_token": "xx", //钉钉post回消息时header中带的检验token + "dingtalk_secret": "xx"// 安全加密加签串,群机器人中 } } ``` diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 2c0bde3..78a1a49 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -45,9 +45,9 @@ def create_channel(channel_type): from channel.http.http_channel import HttpChannel return HttpChannel() - elif channel_type == const.DINGDING: - from channel.dd.dd_channel import DDChannel - return DDChannel() + elif channel_type == const.DINGTALK: + from channel.dingtalk.dingtalk_channel import DingTalkChannel + return DingTalkChannel() else: raise RuntimeError("unknown channel_type in config.json: " + channel_type) diff --git a/channel/dd/dd_channel.py b/channel/dingtalk/dingtalk_channel.py similarity index 69% rename from channel/dd/dd_channel.py rename to channel/dingtalk/dingtalk_channel.py index 6664daa..47c5d7f 100644 --- a/channel/dd/dd_channel.py +++ b/channel/dingtalk/dingtalk_channel.py @@ -14,18 +14,18 @@ from config import channel_conf from config import channel_conf_val from channel.channel import Channel -class DDChannel(Channel): +class DingTalkChannel(Channel): def __init__(self): - self.dd_token = channel_conf(const.DINGDING).get('dd_token') - self.dd_post_token = channel_conf(const.DINGDING).get('dd_post_token') - self.dd_secret = channel_conf(const.DINGDING).get('dd_secret') - log.info("[DingDing] dd_secret={}, dd_token={} dd_post_token={}".format(self.dd_secret, self.dd_token, self.dd_post_token)) + self.dingtalk_token = channel_conf(const.DINGTALK).get('dingtalk_token') + self.dingtalk_post_token = channel_conf(const.DINGTALK).get('dingtalk_post_token') + self.dingtalk_secret = channel_conf(const.DINGTALK).get('dingtalk_secret') + log.info("[DingTalk] dingtalk_secret={}, dingtalk_token={} dingtalk_post_token={}".format(self.dingtalk_secret, self.dingtalk_token, self.dingtalk_post_token)) def startup(self): - http_app.run(host='0.0.0.0', port=channel_conf(const.DINGDING).get('port')) + http_app.run(host='0.0.0.0', port=channel_conf(const.DINGTALK).get('port')) - def notify_dingding(self, answer): + def notify_dingtalk(self, answer): data = { "msgtype": "text", "text": { @@ -41,18 +41,18 @@ class DDChannel(Channel): } timestamp = round(time.time() * 1000) - secret_enc = bytes(self.dd_secret, encoding='utf-8') - string_to_sign = '{}\n{}'.format(timestamp, self.dd_secret) + secret_enc = bytes(self.dingtalk_secret, encoding='utf-8') + string_to_sign = '{}\n{}'.format(timestamp, self.dingtalk_secret) string_to_sign_enc = bytes(string_to_sign, encoding='utf-8') hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() sign = quote_plus(base64.b64encode(hmac_code)) - notify_url = f"https://oapi.dingtalk.com/robot/send?access_token={self.dd_token}×tamp={timestamp}&sign={sign}" + notify_url = f"https://oapi.dingtalk.com/robot/send?access_token={self.dingtalk_token}×tamp={timestamp}&sign={sign}" try: r = requests.post(notify_url, json=data) reply = r.json() - # log.info("[DingDing] reply={}".format(str(reply))) + # log.info("[DingTalk] reply={}".format(str(reply))) except Exception as e: log.error(e) @@ -62,7 +62,7 @@ class DDChannel(Channel): sender_id = data['senderId'] context = dict() img_match_prefix = functions.check_prefix( - prompt, channel_conf_val(const.DINGDING, 'image_create_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' @@ -79,24 +79,23 @@ class DDChannel(Channel): return reply -dd = DDChannel() +dd = DingTalkChannel() http_app = Flask(__name__,) @http_app.route("/", methods=['POST']) def chat(): - # log.info("[DingDing] chat_headers={}".format(str(request.headers))) - log.info("[DingDing] chat={}".format(str(request.data))) + # log.info("[DingTalk] chat_headers={}".format(str(request.headers))) + log.info("[DingTalk] chat={}".format(str(request.data))) token = request.headers.get('token') - if dd.dd_post_token and token != dd.dd_post_token: + if dd.dingtalk_post_token and token != dd.dingtalk_post_token: return {'ret': 203} - #TODO: Verify identity data = json.loads(request.data) if data: content = data['text']['content'] if not content: return reply_text = dd.handle(data=data) - dd.notify_dingding(reply_text) + dd.notify_dingtalk(reply_text) return {'ret': 200} return {'ret': 201} diff --git a/common/const.py b/common/const.py index 95e10bc..6324c82 100644 --- a/common/const.py +++ b/common/const.py @@ -8,7 +8,7 @@ GMAIL = "gmail" TELEGRAM = "telegram" SLACK = "slack" HTTP = "http" -DINGDING = "dingding" +DINGTALK = "dingtalk" # model OPEN_AI = "openai" diff --git a/config-template.json b/config-template.json index 4c2f393..93e88c2 100644 --- a/config-template.json +++ b/config-template.json @@ -61,12 +61,12 @@ "port": "80" }, - "dingding": { + "dingtalk": { "image_create_prefix": ["画", "draw", "Draw"], "port": "8081", - "dd_token": "xx", - "dd_post_token": "xx", - "dd_secret": "xx" + "dingtalk_token": "xx", + "dingtalk_post_token": "xx", + "dingtalk_secret": "xx" } }, "common": { From 02d6d65d4989dd38ba7105ea74292acf9b3570bf Mon Sep 17 00:00:00 2001 From: wujiyu Date: Fri, 7 Apr 2023 13:06:23 +0800 Subject: [PATCH 4/5] add channel feishu --- README.md | 42 ++++++- channel/channel_factory.py | 4 + channel/feishu/feishu_channel.py | 184 +++++++++++++++++++++++++++++++ channel/feishu/store.py | 67 +++++++++++ common/const.py | 1 + config-template.json | 11 ++ 6 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 channel/feishu/feishu_channel.py create mode 100644 channel/feishu/store.py diff --git a/README.md b/README.md index 247ba7a..07f0530 100644 --- a/README.md +++ b/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`: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。 diff --git a/channel/channel_factory.py b/channel/channel_factory.py index 78a1a49..482bc57 100644 --- a/channel/channel_factory.py +++ b/channel/channel_factory.py @@ -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) diff --git a/channel/feishu/feishu_channel.py b/channel/feishu/feishu_channel.py new file mode 100644 index 0000000..8aa6102 --- /dev/null +++ b/channel/feishu/feishu_channel.py @@ -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 "%s" % ( + # 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} + diff --git a/channel/feishu/store.py b/channel/feishu/store.py new file mode 100644 index 0000000..e9caf52 --- /dev/null +++ b/channel/feishu/store.py @@ -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() + + diff --git a/common/const.py b/common/const.py index 6324c82..bd5d829 100644 --- a/common/const.py +++ b/common/const.py @@ -9,6 +9,7 @@ TELEGRAM = "telegram" SLACK = "slack" HTTP = "http" DINGTALK = "dingtalk" +FEISHU = "feishu" # model OPEN_AI = "openai" diff --git a/config-template.json b/config-template.json index 93e88c2..ccebe0b 100644 --- a/config-template.json +++ b/config-template.json @@ -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": { From 63ae7acbdc3681ca051405f588dd9d20ffa7b7dc Mon Sep 17 00:00:00 2001 From: wujiyu Date: Fri, 7 Apr 2023 13:38:42 +0800 Subject: [PATCH 5/5] add channel feishu --- channel/feishu/feishu_channel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/channel/feishu/feishu_channel.py b/channel/feishu/feishu_channel.py index 8aa6102..07877d8 100644 --- a/channel/feishu/feishu_channel.py +++ b/channel/feishu/feishu_channel.py @@ -121,6 +121,7 @@ class FeiShuChannel(Channel): elif chat_type == "p2p": receive_type = "open_id" receive_id = sender_id + at_id = None # 调用发消息 API 之前,先要获取 API 调用凭证:tenant_access_token access_token = self.get_tenant_access_token()