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 58fa42a..4cb73cd 100644 --- a/config-template.json +++ b/config-template.json @@ -58,6 +58,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": {