Bläddra i källkod

Merge branch 'master' into feat-client

zhayujie 2 år sedan
förälder
incheckning
40a10ee926

+ 1 - 0
.github/workflows/deploy-image-arm.yml

@@ -19,6 +19,7 @@ env:
 
 jobs:
   build-and-push-image:
+    if: github.repository == 'zhayujie/chatgpt-on-wechat'
     runs-on: ubuntu-latest
     permissions:
       contents: read

+ 1 - 0
.github/workflows/deploy-image.yml

@@ -19,6 +19,7 @@ env:
 
 jobs:
   build-and-push-image:
+    if: github.repository == 'zhayujie/chatgpt-on-wechat'
     runs-on: ubuntu-latest
     permissions:
       contents: read

+ 3 - 1
README.md

@@ -5,7 +5,7 @@
 最新版本支持的功能如下:
 
 - [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信、微信公众号和、业微信、飞书等部署方式
-- [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4, claude, 文心一言, 讯飞星火
+- [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4, claude, Gemini, 文心一言, 讯飞星火, 通义千问
 - [x] **语音能力:** 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai(whisper/tts) 等多种语音模型
 - [x] **图像能力:** 支持图片生成、图片识别、图生图(如照片修复),可选择 Dall-E-3, stable diffusion, replicate, midjourney, vision模型
 - [x] **丰富插件:** 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结、文档总结和对话等插件
@@ -28,6 +28,8 @@ Demo made by [Visionn](https://www.wangpc.cc/)
 
 # 更新日志
 
+>**2023.11.11:** [1.5.3版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.5.3) 和 [1.5.4版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.5.4),新增Google Gemini、通义千问模型
+
 >**2023.11.10:** [1.5.2版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.5.2),新增飞书通道、图像识别对话、黑名单配置
 
 >**2023.11.10:** [1.5.0版本](https://github.com/zhayujie/chatgpt-on-wechat/releases/tag/1.5.0),新增 `gpt-4-turbo`, `dall-e-3`, `tts` 模型接入,完善图像理解&生成、语音识别&生成的多模态能力

+ 1 - 1
app.py

@@ -44,7 +44,7 @@ def run():
             # os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:9001'
 
         channel = channel_factory.create_channel(channel_name)
-        if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service", "wechatcom_app", "wework", const.FEISHU]:
+        if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service", "wechatcom_app", "wework", const.FEISHU,const.DINGTALK]:
             PluginManager().load_plugins()
 
         if conf().get("use_linkai"):

+ 55 - 26
bot/tongyi/tongyi_qwen_bot.py → bot/ali/ali_qwen_bot.py

@@ -10,31 +10,48 @@ import broadscope_bailian
 from broadscope_bailian import ChatQaMessage
 
 from bot.bot import Bot
-from bot.baidu.baidu_wenxin_session import BaiduWenxinSession
+from bot.ali.ali_qwen_session import AliQwenSession
 from bot.session_manager import SessionManager
 from bridge.context import ContextType
 from bridge.reply import Reply, ReplyType
 from common.log import logger
+from common import const
 from config import conf, load_config
 
-class TongyiQwenBot(Bot):
+class AliQwenBot(Bot):
     def __init__(self):
         super().__init__()
-        self.access_key_id = conf().get("qwen_access_key_id")
-        self.access_key_secret = conf().get("qwen_access_key_secret")
-        self.agent_key = conf().get("qwen_agent_key")
-        self.app_id = conf().get("qwen_app_id")
-        self.node_id = conf().get("qwen_node_id") or ""
-        self.api_key_client = broadscope_bailian.AccessTokenClient(access_key_id=self.access_key_id, access_key_secret=self.access_key_secret)
         self.api_key_expired_time = self.set_api_key()
-        self.sessions = SessionManager(BaiduWenxinSession, model=conf().get("model") or "qwen")
-        self.temperature = conf().get("temperature", 0.2) # 值在[0,1]之间,越大表示回复越具有不确定性
-        self.top_p = conf().get("top_p", 1)
+        self.sessions = SessionManager(AliQwenSession, model=conf().get("model", const.QWEN))
+
+    def api_key_client(self):
+        return broadscope_bailian.AccessTokenClient(access_key_id=self.access_key_id(), access_key_secret=self.access_key_secret())
+
+    def access_key_id(self):
+        return conf().get("qwen_access_key_id")
+
+    def access_key_secret(self):
+        return conf().get("qwen_access_key_secret")
+
+    def agent_key(self):
+        return conf().get("qwen_agent_key")
+
+    def app_id(self):
+        return conf().get("qwen_app_id")
+
+    def node_id(self):
+        return conf().get("qwen_node_id", "")
+
+    def temperature(self):
+        return conf().get("temperature", 0.2 )
+
+    def top_p(self):
+        return conf().get("top_p", 1)
 
     def reply(self, query, context=None):
         # acquire reply content
         if context.type == ContextType.TEXT:
-            logger.info("[TONGYI] query={}".format(query))
+            logger.info("[QWEN] query={}".format(query))
 
             session_id = context["session_id"]
             reply = None
@@ -51,11 +68,11 @@ class TongyiQwenBot(Bot):
             if reply:
                 return reply
             session = self.sessions.session_query(query, session_id)
-            logger.debug("[TONGYI] session query={}".format(session.messages))
+            logger.debug("[QWEN] session query={}".format(session.messages))
 
             reply_content = self.reply_text(session)
             logger.debug(
-                "[TONGYI] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format(
+                "[QWEN] new_query={}, session_id={}, reply_cont={}, completion_tokens={}".format(
                     session.messages,
                     session_id,
                     reply_content["content"],
@@ -69,14 +86,14 @@ class TongyiQwenBot(Bot):
                 reply = Reply(ReplyType.TEXT, reply_content["content"])
             else:
                 reply = Reply(ReplyType.ERROR, reply_content["content"])
-                logger.debug("[TONGYI] reply {} used 0 tokens.".format(reply_content))
+                logger.debug("[QWEN] reply {} used 0 tokens.".format(reply_content))
             return reply
 
         else:
             reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type))
             return reply
 
-    def reply_text(self, session: BaiduWenxinSession, retry_count=0) -> dict:
+    def reply_text(self, session: AliQwenSession, retry_count=0) -> dict:
         """
         call bailian's ChatCompletion to get the answer
         :param session: a conversation session
@@ -86,9 +103,9 @@ class TongyiQwenBot(Bot):
         try:
             prompt, history = self.convert_messages_format(session.messages)
             self.update_api_key_if_expired()
-            # NOTE 阿里百炼的call()函数参数比较奇怪, top_k参数表示top_p, top_p参数表示temperature, 可以参考文档 https://help.aliyun.com/document_detail/2587502.htm
-            response = broadscope_bailian.Completions().call(app_id=self.app_id, prompt=prompt, history=history, top_k=self.top_p, top_p=self.temperature)
-            completion_content = self.get_completion_content(response, self.node_id)
+            # NOTE 阿里百炼的call()函数未提供temperature参数,考虑到temperature和top_p参数作用相同,取两者较小的值作为top_p参数传入,详情见文档 https://help.aliyun.com/document_detail/2587502.htm
+            response = broadscope_bailian.Completions().call(app_id=self.app_id(), prompt=prompt, history=history, top_p=min(self.temperature(), self.top_p()))
+            completion_content = self.get_completion_content(response, self.node_id())
             completion_tokens, total_tokens = self.calc_tokens(session.messages, completion_content)
             return {
                 "total_tokens": total_tokens,
@@ -99,39 +116,40 @@ class TongyiQwenBot(Bot):
             need_retry = retry_count < 2
             result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"}
             if isinstance(e, openai.error.RateLimitError):
-                logger.warn("[TONGYI] RateLimitError: {}".format(e))
+                logger.warn("[QWEN] RateLimitError: {}".format(e))
                 result["content"] = "提问太快啦,请休息一下再问我吧"
                 if need_retry:
                     time.sleep(20)
             elif isinstance(e, openai.error.Timeout):
-                logger.warn("[TONGYI] Timeout: {}".format(e))
+                logger.warn("[QWEN] Timeout: {}".format(e))
                 result["content"] = "我没有收到你的消息"
                 if need_retry:
                     time.sleep(5)
             elif isinstance(e, openai.error.APIError):
-                logger.warn("[TONGYI] Bad Gateway: {}".format(e))
+                logger.warn("[QWEN] Bad Gateway: {}".format(e))
                 result["content"] = "请再问我一次"
                 if need_retry:
                     time.sleep(10)
             elif isinstance(e, openai.error.APIConnectionError):
-                logger.warn("[TONGYI] APIConnectionError: {}".format(e))
+                logger.warn("[QWEN] APIConnectionError: {}".format(e))
                 need_retry = False
                 result["content"] = "我连接不到你的网络"
             else:
-                logger.exception("[TONGYI] Exception: {}".format(e))
+                logger.exception("[QWEN] Exception: {}".format(e))
                 need_retry = False
                 self.sessions.clear_session(session.session_id)
 
             if need_retry:
-                logger.warn("[TONGYI] 第{}次重试".format(retry_count + 1))
+                logger.warn("[QWEN] 第{}次重试".format(retry_count + 1))
                 return self.reply_text(session, retry_count + 1)
             else:
                 return result
 
     def set_api_key(self):
-        api_key, expired_time = self.api_key_client.create_token(agent_key=self.agent_key)
+        api_key, expired_time = self.api_key_client().create_token(agent_key=self.agent_key())
         broadscope_bailian.api_key = api_key
         return expired_time
+
     def update_api_key_if_expired(self):
         if time.time() > self.api_key_expired_time:
             self.api_key_expired_time = self.set_api_key()
@@ -140,6 +158,7 @@ class TongyiQwenBot(Bot):
         history = []
         user_content = ''
         assistant_content = ''
+        system_content = ''
         for message in messages:
             role = message.get('role')
             if role == 'user':
@@ -149,11 +168,21 @@ class TongyiQwenBot(Bot):
                 history.append(ChatQaMessage(user_content, assistant_content))
                 user_content = ''
                 assistant_content = ''
+            elif role =='system':
+                system_content += message.get('content')
         if user_content == '':
             raise Exception('no user message')
+        if system_content != '':
+            # NOTE 模拟系统消息,测试发现人格描述以"你需要扮演ChatGPT"开头能够起作用,而以"你是ChatGPT"开头模型会直接否认
+            system_qa = ChatQaMessage(system_content, '好的,我会严格按照你的设定回答问题')
+            history.insert(0, system_qa)
+        logger.debug("[QWEN] converted qa messages: {}".format([item.to_dict() for item in history]))
+        logger.debug("[QWEN] user content as prompt: {}".format(user_content))
         return user_content, history
 
     def get_completion_content(self, response, node_id):
+        if not response['Success']:
+            return f"[ERROR]\n{response['Code']}:{response['Message']}"
         text = response['Data']['Text']
         if node_id == '':
             return text

+ 62 - 0
bot/ali/ali_qwen_session.py

@@ -0,0 +1,62 @@
+from bot.session_manager import Session
+from common.log import logger
+
+"""
+    e.g.
+    [
+        {"role": "system", "content": "You are a helpful assistant."},
+        {"role": "user", "content": "Who won the world series in 2020?"},
+        {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
+        {"role": "user", "content": "Where was it played?"}
+    ]
+"""
+
+class AliQwenSession(Session):
+    def __init__(self, session_id, system_prompt=None, model="qianwen"):
+        super().__init__(session_id, system_prompt)
+        self.model = model
+        self.reset()
+
+    def discard_exceeding(self, max_tokens, cur_tokens=None):
+        precise = True
+        try:
+            cur_tokens = self.calc_tokens()
+        except Exception as e:
+            precise = False
+            if cur_tokens is None:
+                raise e
+            logger.debug("Exception when counting tokens precisely for query: {}".format(e))
+        while cur_tokens > max_tokens:
+            if len(self.messages) > 2:
+                self.messages.pop(1)
+            elif len(self.messages) == 2 and self.messages[1]["role"] == "assistant":
+                self.messages.pop(1)
+                if precise:
+                    cur_tokens = self.calc_tokens()
+                else:
+                    cur_tokens = cur_tokens - max_tokens
+                break
+            elif len(self.messages) == 2 and self.messages[1]["role"] == "user":
+                logger.warn("user message exceed max_tokens. total_tokens={}".format(cur_tokens))
+                break
+            else:
+                logger.debug("max_tokens={}, total_tokens={}, len(messages)={}".format(max_tokens, cur_tokens, len(self.messages)))
+                break
+            if precise:
+                cur_tokens = self.calc_tokens()
+            else:
+                cur_tokens = cur_tokens - max_tokens
+        return cur_tokens
+
+    def calc_tokens(self):
+        return num_tokens_from_messages(self.messages, self.model)
+
+def num_tokens_from_messages(messages, model):
+    """Returns the number of tokens used by a list of messages."""
+    # 官方token计算规则:"对于中文文本来说,1个token通常对应一个汉字;对于英文文本来说,1个token通常对应3至4个字母或1个单词"
+    # 详情请产看文档:https://help.aliyun.com/document_detail/2586397.html
+    # 目前根据字符串长度粗略估计token数,不影响正常使用
+    tokens = 0
+    for msg in messages:
+        tokens += len(msg["content"])
+    return tokens

+ 7 - 2
bot/bot_factory.py

@@ -45,6 +45,11 @@ def create_bot(bot_type):
         return ClaudeAIBot()
 
     elif bot_type == const.QWEN:
-        from bot.tongyi.tongyi_qwen_bot import TongyiQwenBot
-        return TongyiQwenBot()
+        from bot.ali.ali_qwen_bot import AliQwenBot
+        return AliQwenBot()
+
+    elif bot_type == const.GEMINI:
+        from bot.gemini.google_gemini_bot import GoogleGeminiBot
+        return GoogleGeminiBot()
+
     raise RuntimeError

+ 1 - 1
bot/chatgpt/chat_gpt_session.py

@@ -57,7 +57,7 @@ class ChatGPTSession(Session):
 def num_tokens_from_messages(messages, model):
     """Returns the number of tokens used by a list of messages."""
 
-    if model in ["wenxin", "xunfei"]:
+    if model in ["wenxin", "xunfei", const.GEMINI]:
         return num_tokens_by_character(messages)
 
     import tiktoken

+ 75 - 0
bot/gemini/google_gemini_bot.py

@@ -0,0 +1,75 @@
+"""
+Google gemini bot
+
+@author zhayujie
+@Date 2023/12/15
+"""
+# encoding:utf-8
+
+from bot.bot import Bot
+import google.generativeai as genai
+from bot.session_manager import SessionManager
+from bridge.context import ContextType, Context
+from bridge.reply import Reply, ReplyType
+from common.log import logger
+from config import conf
+from bot.baidu.baidu_wenxin_session import BaiduWenxinSession
+
+
+# OpenAI对话模型API (可用)
+class GoogleGeminiBot(Bot):
+
+    def __init__(self):
+        super().__init__()
+        self.api_key = conf().get("gemini_api_key")
+        # 复用文心的token计算方式
+        self.sessions = SessionManager(BaiduWenxinSession, model=conf().get("model") or "gpt-3.5-turbo")
+
+    def reply(self, query, context: Context = None) -> Reply:
+        try:
+            if context.type != ContextType.TEXT:
+                logger.warn(f"[Gemini] Unsupported message type, type={context.type}")
+                return Reply(ReplyType.TEXT, None)
+            logger.info(f"[Gemini] query={query}")
+            session_id = context["session_id"]
+            session = self.sessions.session_query(query, session_id)
+            gemini_messages = self._convert_to_gemini_messages(self._filter_messages(session.messages))
+            genai.configure(api_key=self.api_key)
+            model = genai.GenerativeModel('gemini-pro')
+            response = model.generate_content(gemini_messages)
+            reply_text = response.text
+            self.sessions.session_reply(reply_text, session_id)
+            logger.info(f"[Gemini] reply={reply_text}")
+            return Reply(ReplyType.TEXT, reply_text)
+        except Exception as e:
+            logger.error("[Gemini] fetch reply error, may contain unsafe content")
+            logger.error(e)
+
+    def _convert_to_gemini_messages(self, messages: list):
+        res = []
+        for msg in messages:
+            if msg.get("role") == "user":
+                role = "user"
+            elif msg.get("role") == "assistant":
+                role = "model"
+            else:
+                continue
+            res.append({
+                "role": role,
+                "parts": [{"text": msg.get("content")}]
+            })
+        return res
+
+    def _filter_messages(self, messages: list):
+        res = []
+        turn = "user"
+        for i in range(len(messages) - 1, -1, -1):
+            message = messages[i]
+            if message.get("role") != turn:
+                continue
+            res.insert(0, message)
+            if turn == "user":
+                turn = "assistant"
+            elif turn == "assistant":
+                turn = "user"
+        return res

+ 10 - 2
bot/linkai/link_ai_bot.py

@@ -1,10 +1,9 @@
 # access LinkAI knowledge base platform
 # docs: https://link-ai.tech/platform/link-app/wechat
 
+import re
 import time
-
 import requests
-
 import config
 from bot.bot import Bot
 from bot.chatgpt.chat_gpt_session import ChatGPTSession
@@ -141,6 +140,7 @@ class LinkAIBot(Bot):
                     thread.start()
                     if response["choices"][0].get("text_content"):
                         reply_content = response["choices"][0].get("text_content")
+                reply_content = self._process_url(reply_content)
                 return Reply(ReplyType.TEXT, reply_content)
 
             else:
@@ -371,6 +371,14 @@ class LinkAIBot(Bot):
         except Exception as e:
             logger.exception(e)
 
+    def _process_url(self, text):
+        try:
+            url_pattern = re.compile(r'\[(.*?)\]\((http[s]?://.*?)\)')
+            def replace_markdown_url(match):
+                return f"{match.group(2)}"
+            return url_pattern.sub(replace_markdown_url, text)
+        except Exception as e:
+            logger.error(e)
 
     def _send_image(self, channel, context, image_urls):
         if not image_urls:

+ 4 - 0
bridge/bridge.py

@@ -29,12 +29,16 @@ class Bridge(object):
             self.btype["chat"] = const.XUNFEI
         if model_type in [const.QWEN]:
             self.btype["chat"] = const.QWEN
+        if model_type in [const.GEMINI]:
+            self.btype["chat"] = const.GEMINI
+
         if conf().get("use_linkai") and conf().get("linkai_api_key"):
             self.btype["chat"] = const.LINKAI
             if not conf().get("voice_to_text") or conf().get("voice_to_text") in ["openai"]:
                 self.btype["voice_to_text"] = const.LINKAI
             if not conf().get("text_to_voice") or conf().get("text_to_voice") in ["openai", const.TTS_1, const.TTS_1_HD]:
                 self.btype["text_to_voice"] = const.LINKAI
+
         if model_type in ["claude"]:
             self.btype["chat"] = const.CLAUDEAI
         self.bots = {}

+ 3 - 0
channel/channel_factory.py

@@ -36,6 +36,9 @@ def create_channel(channel_type) -> Channel:
     elif channel_type == const.FEISHU:
         from channel.feishu.feishu_channel import FeiShuChanel
         ch = FeiShuChanel()
+    elif channel_type == const.DINGTALK:
+        from channel.dingtalk.dingtalk_channel import DingTalkChanel
+        ch = DingTalkChanel()
     else:
         raise RuntimeError
     ch.channel_type = channel_type

+ 164 - 0
channel/dingtalk/dingtalk_channel.py

@@ -0,0 +1,164 @@
+"""
+钉钉通道接入
+
+@author huiwen
+@Date 2023/11/28
+"""
+
+# -*- coding=utf-8 -*-
+import uuid
+
+import requests
+import web
+from channel.dingtalk.dingtalk_message import DingTalkMessage
+from bridge.context import Context
+from bridge.reply import Reply, ReplyType
+from common.log import logger
+from common.singleton import singleton
+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
+
+
+
+import argparse
+import logging
+from dingtalk_stream import AckMessage
+import dingtalk_stream
+
+@singleton
+class DingTalkChanel(ChatChannel,dingtalk_stream.ChatbotHandler):
+    dingtalk_client_id = conf().get('dingtalk_client_id')
+    dingtalk_client_secret = conf().get('dingtalk_client_secret')
+    
+    def setup_logger(self):
+        logger = logging.getLogger()
+        handler = logging.StreamHandler()
+        handler.setFormatter(
+            logging.Formatter('%(asctime)s %(name)-8s %(levelname)-8s %(message)s [%(filename)s:%(lineno)d]'))
+        logger.addHandler(handler)
+        logger.setLevel(logging.INFO)
+        return logger
+    def __init__(self):
+        super().__init__()
+        super(dingtalk_stream.ChatbotHandler, self).__init__()
+        
+        self.logger = self.setup_logger()
+        # 历史消息id暂存,用于幂等控制
+        self.receivedMsgs = ExpiredDict(60 * 60 * 7.1)
+        
+        logger.info("[dingtalk] client_id={}, client_secret={} ".format(
+            self.dingtalk_client_id, self.dingtalk_client_secret))
+        # 无需群校验和前缀
+        conf()["group_name_white_list"] = ["ALL_GROUP"]
+        
+        
+
+    def startup(self):
+       
+        credential = dingtalk_stream.Credential( self.dingtalk_client_id, self.dingtalk_client_secret)
+        client = dingtalk_stream.DingTalkStreamClient(credential)
+        client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC,self)
+        client.start_forever()
+
+    def handle_single(self, cmsg:DingTalkMessage):
+        # 处理单聊消息
+        #  
+    
+        if cmsg.ctype == ContextType.VOICE:
+           
+            logger.debug("[dingtalk]receive voice msg: {}".format(cmsg.content))
+        elif cmsg.ctype == ContextType.IMAGE:
+            logger.debug("[dingtalk]receive image msg: {}".format(cmsg.content))
+        elif cmsg.ctype == ContextType.PATPAT:
+            logger.debug("[dingtalk]receive patpat msg: {}".format(cmsg.content))
+        elif cmsg.ctype == ContextType.TEXT:
+            expression = cmsg.my_msg
+            
+        cmsg.content = conf()["single_chat_prefix"][0] + cmsg.content
+        
+        context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=False, msg=cmsg)
+        
+        if context:
+            self.produce(context)
+
+    def handle_group(self, cmsg:DingTalkMessage):
+        # 处理群聊消息
+        #  
+    
+        if cmsg.ctype == ContextType.VOICE:
+           
+            logger.debug("[dingtalk]receive voice msg: {}".format(cmsg.content))
+        elif cmsg.ctype == ContextType.IMAGE:
+            logger.debug("[dingtalk]receive image msg: {}".format(cmsg.content))
+        elif cmsg.ctype == ContextType.PATPAT:
+            logger.debug("[dingtalk]receive patpat msg: {}".format(cmsg.content))
+        elif cmsg.ctype == ContextType.TEXT:
+            expression = cmsg.my_msg
+            
+        cmsg.content = conf()["group_chat_prefix"][0] + cmsg.content
+        context = self._compose_context(cmsg.ctype, cmsg.content, isgroup=True, msg=cmsg)
+        context['no_need_at']=True
+        if context:
+            self.produce(context)
+
+
+    async def process(self, callback: dingtalk_stream.CallbackMessage):
+       
+
+
+        try:
+            
+            incoming_message = dingtalk_stream.ChatbotMessage.from_dict(callback.data)
+            dingtalk_msg = DingTalkMessage(incoming_message)
+            if incoming_message.conversation_type == '1':
+                self.handle_single(dingtalk_msg)
+            else:
+                self.handle_group(dingtalk_msg)   
+            return AckMessage.STATUS_OK, 'OK'
+        except Exception as e:
+            logger.error(e)
+            return self.FAILED_MSG
+
+
+    def send(self, reply: Reply, context: Context):
+
+
+        incoming_message = context.kwargs['msg'].incoming_message
+        self.reply_text(reply.content, incoming_message)
+       
+        
+
+
+
+    # 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

+ 40 - 0
channel/dingtalk/dingtalk_message.py

@@ -0,0 +1,40 @@
+from bridge.context import ContextType
+from channel.chat_message import ChatMessage
+import json
+import requests
+from common.log import logger
+from common.tmp_dir import TmpDir
+from common import utils
+from dingtalk_stream import ChatbotMessage
+
+class DingTalkMessage(ChatMessage):
+    def __init__(self, event: ChatbotMessage):
+        super().__init__(event)
+        
+        self.msg_id = event.message_id
+        msg_type = event.message_type
+        self.incoming_message =event
+        self.sender_staff_id = event.sender_staff_id
+        
+        self.create_time = event.create_at
+        if event.conversation_type=="1":
+            self.is_group = False
+        else:
+            self.is_group = True
+        
+
+        if msg_type == "text":
+            self.ctype = ContextType.TEXT
+            
+            self.content = event.text.content.strip()
+        
+        self.from_user_id = event.sender_id
+        self.to_user_id = event.chatbot_user_id
+        self.other_user_nickname = event.conversation_title
+        
+        user_id = event.sender_id
+        nickname =event.sender_nick
+
+        
+
+      

+ 0 - 29
channel/feishu/feishu_message.py

@@ -46,35 +46,6 @@ class FeishuMessage(ChatMessage):
                 else:
                     logger.info(f"[FeiShu] Failed to download file, key={file_key}, res={response.text}")
             self._prepare_fn = _download_file
-
-        # elif msg.type == "voice":
-        #     self.ctype = ContextType.VOICE
-        #     self.content = TmpDir().path() + msg.media_id + "." + msg.format  # content直接存临时目录路径
-        #
-        #     def download_voice():
-        #         # 如果响应状态码是200,则将响应内容写入本地文件
-        #         response = client.media.download(msg.media_id)
-        #         if response.status_code == 200:
-        #             with open(self.content, "wb") as f:
-        #                 f.write(response.content)
-        #         else:
-        #             logger.info(f"[wechatcom] Failed to download voice file, {response.content}")
-        #
-        #     self._prepare_fn = download_voice
-        # elif msg.type == "image":
-        #     self.ctype = ContextType.IMAGE
-        #     self.content = TmpDir().path() + msg.media_id + ".png"  # content直接存临时目录路径
-        #
-        #     def download_image():
-        #         # 如果响应状态码是200,则将响应内容写入本地文件
-        #         response = client.media.download(msg.media_id)
-        #         if response.status_code == 200:
-        #             with open(self.content, "wb") as f:
-        #                 f.write(response.content)
-        #         else:
-        #             logger.info(f"[wechatcom] Failed to download image file, {response.content}")
-        #
-        #     self._prepare_fn = download_image
         else:
             raise NotImplementedError("Unsupported message type: Type:{} ".format(msg_type))
 

+ 1 - 1
channel/wework/wework_channel.py

@@ -120,7 +120,7 @@ def _check(func):
 
 
 @wework.msg_register(
-    [ntwork.MT_RECV_TEXT_MSG, ntwork.MT_RECV_IMAGE_MSG, 11072, ntwork.MT_RECV_VOICE_MSG])
+    [ntwork.MT_RECV_TEXT_MSG, ntwork.MT_RECV_IMAGE_MSG, 11072, ntwork.MT_RECV_LINK_CARD_MSG,ntwork.MT_RECV_FILE_MSG, ntwork.MT_RECV_VOICE_MSG])
 def all_msg_handler(wework_instance: ntwork.WeWork, message):
     logger.debug(f"收到消息: {message}")
     if 'data' in message:

+ 13 - 0
channel/wework/wework_message.py

@@ -128,6 +128,18 @@ class WeworkMessage(ChatMessage):
                 self.ctype = ContextType.IMAGE
                 self.content = os.path.join(current_dir, "tmp", file_name)
                 self._prepare_fn = lambda: cdn_download(wework, wework_msg, file_name)
+            elif wework_msg["type"] == 11045:  # 文件消息
+                print("文件消息")
+                print(wework_msg)
+                file_name = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
+                file_name = file_name + wework_msg['data']['cdn']['file_name']
+                current_dir = os.getcwd()
+                self.ctype = ContextType.FILE
+                self.content = os.path.join(current_dir, "tmp", file_name)
+                self._prepare_fn = lambda: cdn_download(wework, wework_msg, file_name)
+            elif wework_msg["type"] == 11047:  # 链接消息
+                self.ctype = ContextType.SHARING
+                self.content = wework_msg['data']['url']
             elif wework_msg["type"] == 11072:  # 新成员入群通知
                 self.ctype = ContextType.JOIN_GROUP
                 member_list = wework_msg['data']['member_list']
@@ -179,6 +191,7 @@ class WeworkMessage(ChatMessage):
                 if conversation_id:
                     room_info = get_room_info(wework=wework, conversation_id=conversation_id)
                     self.other_user_nickname = room_info.get('nickname', None) if room_info else None
+                    self.from_user_nickname = room_info.get('nickname', None) if room_info else None
                     at_list = data.get('at_list', [])
                     tmp_list = []
                     for at in at_list:

+ 3 - 1
common/const.py

@@ -7,6 +7,7 @@ CHATGPTONAZURE = "chatGPTOnAzure"
 LINKAI = "linkai"
 CLAUDEAI = "claude"
 QWEN = "qwen"
+GEMINI = "gemini"
 
 # model
 GPT35 = "gpt-3.5-turbo"
@@ -17,7 +18,8 @@ WHISPER_1 = "whisper-1"
 TTS_1 = "tts-1"
 TTS_1_HD = "tts-1-hd"
 
-MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "wenxin-4", "xunfei", "claude", "gpt-4-turbo", GPT4_TURBO_PREVIEW, QWEN]
+MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "wenxin-4", "xunfei", "claude", "gpt-4-turbo", GPT4_TURBO_PREVIEW, QWEN, GEMINI]
 
 # channel
 FEISHU = "feishu"
+DINGTALK = "dingtalk"   

+ 7 - 1
config.py

@@ -73,6 +73,8 @@ available_setting = {
     "qwen_agent_key": "",
     "qwen_app_id": "",
     "qwen_node_id": "",  # 流程编排模型用到的id,如果没有用到qwen_node_id,请务必保持为空字符串
+    # Google Gemini Api Key
+    "gemini_api_key": "",
     # wework的通用配置
     "wework_smart": True,  # 配置wework是否使用已登录的企业微信,False为多开
     # 语音设置
@@ -130,7 +132,11 @@ available_setting = {
     "feishu_app_secret": "",  # 飞书机器人APP secret
     "feishu_token": "",  # 飞书 verification token
     "feishu_bot_name": "",  # 飞书机器人的名字
-
+    
+    # 钉钉配置
+    "dingtalk_client_id": "",  # 钉钉机器人Client ID 
+    "dingtalk_client_secret": "",  # 钉钉机器人Client Secret 
+    
     # chatgpt指令自定义触发词
     "clear_memory_commands": ["#清除记忆"],  # 重置会话指令,必须以#开头
     # channel配置

+ 2 - 2
plugins/godcmd/godcmd.py

@@ -313,7 +313,7 @@ class Godcmd(Plugin):
                     except Exception as e:
                         ok, result = False, "你没有设置私有GPT模型"
                 elif cmd == "reset":
-                    if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, const.BAIDU, const.XUNFEI]:
+                    if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI, const.BAIDU, const.XUNFEI, const.QWEN, const.GEMINI]:
                         bot.sessions.clear_session(session_id)
                         if Bridge().chat_bots.get(bottype):
                             Bridge().chat_bots.get(bottype).sessions.clear_session(session_id)
@@ -339,7 +339,7 @@ class Godcmd(Plugin):
                             ok, result = True, "配置已重载"
                         elif cmd == "resetall":
                             if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI,
-                                           const.BAIDU, const.XUNFEI]:
+                                           const.BAIDU, const.XUNFEI, const.QWEN, const.GEMINI]:
                                 channel.cancel_all_session()
                                 bot.sessions.clear_all_session()
                                 ok, result = True, "重置所有会话成功"

+ 3 - 0
requirements-optional.txt

@@ -33,3 +33,6 @@ curl_cffi
 
 # tongyi qwen
 broadscope_bailian
+
+# google
+google-generativeai