Compare commits

..

25 Commits

Author SHA1 Message Date
zhayujie eae95dfef5 fix: api base bug 2024-01-17 18:25:57 +08:00
zhayujie 11f6e98874 Merge branch 'master' of github.com:zhayujie/chatgpt-on-wechat 2024-01-16 23:22:10 +08:00
zhayujie 2609e595f4 fix: client host 2024-01-16 22:38:33 +08:00
zhayujie ac6e41abc8 Merge pull request #1644 from PoseidonLi0514/master
Image generation supports custom endpoint
2024-01-16 22:35:57 +08:00
zhayujie 9c17e16d0a fix: optimize code format 2024-01-16 19:17:32 +08:00
zhayujie 91cabd7d49 Merge pull request #1628 from huiwenTT/dingdinggpt
添加语音发送消息
2024-01-15 22:45:46 +08:00
zhayujie 7456950530 Merge pull request #1658 from I-E-E-E/patch-1
fixed a typo
2024-01-15 22:41:12 +08:00
zhayujie 8fcdda625d Merge pull request #1675 from zhayujie/feat-client
feat: channel client
2024-01-15 22:37:53 +08:00
zhayujie 40a10ee926 Merge branch 'master' into feat-client 2024-01-15 22:37:47 +08:00
zhayujie c3f7e2645c feat: channel client 2024-01-15 22:35:30 +08:00
I-E-E-E b264af1892 fixed a typo 2024-01-08 17:51:15 +08:00
Haikui Yang 43e93e8e22 Update open_ai_image.py 2024-01-01 22:43:03 +08:00
Haikui Yang d6c4789688 Merge branch 'zhayujie:master' into master 2024-01-01 22:42:10 +08:00
惠文 cb31ee6f01 Merge branch 'dingdinggpt' of github.com:huiwenTT/chatgpt-on-wechat-1 into dingdinggpt 2023-12-26 15:56:35 +08:00
huiwen f7b694ac56 添加语音发送消息和修复上下文的关联 2023-12-26 14:48:54 +08:00
zhayujie eb809055d4 Merge pull request #1559 from huiwenTT/dingdinggpt
钉钉机器人
2023-12-25 18:15:33 +08:00
zhayujie 78d9be82b2 fix: add gemini dependency 2023-12-19 11:47:33 +08:00
Haikui Yang 76a95c0226 Update open_ai_image.py 2023-12-17 19:50:06 +08:00
huiwen d3ab8fb04a Merge branch 'dingdinggpt' of 47.98.110.173:/opt/python_app/gpt into dingdinggpt 2023-12-17 09:52:24 +08:00
huiwen f7a0b63a00 Merge branch 'zhayujie:master' into dingdinggpt 2023-12-17 09:27:30 +08:00
huiwen a21dd97786 钉钉app_id,变更为_client_id,和逻辑优化 2023-12-17 09:23:15 +08:00
zhayujie 04943c0bfa Update README.md 2023-12-16 01:11:05 +08:00
zhayujie 203d4d8bfb Update README.md 2023-12-15 19:16:13 +08:00
huiwen 32a8a847fc 修复小bug 2023-11-30 12:09:03 +08:00
惠文 f6bee3aa58 新增钉钉机器人(Stream模式) 2023-11-30 10:41:34 +08:00
16 changed files with 264 additions and 42 deletions
+2 -1
View File
@@ -29,4 +29,5 @@ plugins/banwords/lib/__pycache__
!plugins/hello
!plugins/role
!plugins/keyword
!plugins/linkai
!plugins/linkai
client_config.json
+4 -2
View File
@@ -4,8 +4,8 @@
最新版本支持的功能如下:
- [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信、微信公众号和、业微信、飞书等部署方式
- [x] **基础对话:** 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3.5, GPT-4, claude, 文心一言, 讯飞星火
- [x] **多端部署:** 有多种部署方式可选择且功能完备,目前已支持个人微信、微信公众号和、业微信、飞书等部署方式
- [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` 模型接入,完善图像理解&生成、语音识别&生成的多模态能力
+10 -1
View File
@@ -8,6 +8,7 @@ from channel import channel_factory
from common import const
from config import load_config
from plugins import *
import threading
def sigterm_handler_wrap(_signo):
@@ -43,11 +44,19 @@ 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"):
try:
from common import linkai_client
threading.Thread(target=linkai_client.start, args=(channel, )).start()
except Exception as e:
pass
# startup channel
channel.startup()
except Exception as e:
logger.error("App startup failed!")
logger.exception(e)
+19 -3
View File
@@ -16,7 +16,6 @@ import threading
from common import memory, utils
import base64
class LinkAIBot(Bot):
# authentication failed
AUTH_FAILED_CODE = 401
@@ -83,7 +82,6 @@ class LinkAIBot(Bot):
if session_message[0].get("role") == "system":
if app_code or model == "wenxin":
session_message.pop(0)
body = {
"app_code": app_code,
"messages": session_message,
@@ -92,7 +90,25 @@ class LinkAIBot(Bot):
"top_p": conf().get("top_p", 1),
"frequency_penalty": conf().get("frequency_penalty", 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容
"presence_penalty": conf().get("presence_penalty", 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容
"session_id": session_id,
"channel_type": conf().get("channel_type")
}
try:
from linkai import LinkAIClient
client_id = LinkAIClient.fetch_client_id()
if client_id:
body["client_id"] = client_id
# start: client info deliver
if context.kwargs.get("msg"):
body["session_id"] = context.kwargs.get("msg").from_user_id
if context.kwargs.get("msg").is_group:
body["is_group"] = True
body["group_name"] = context.kwargs.get("msg").from_user_nickname
body["sender_name"] = context.kwargs.get("msg").actual_user_nickname
else:
body["sender_name"] = context.kwargs.get("msg").from_user_nickname
except Exception as e:
pass
file_id = context.kwargs.get("file_id")
if file_id:
body["file_id"] = file_id
@@ -230,7 +246,7 @@ class LinkAIBot(Bot):
}
if self.args.get("max_tokens"):
body["max_tokens"] = self.args.get("max_tokens")
headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")}
headers = {"Authorization": "Bearer " + conf().get("linkai_api_key")}
# do http request
base_url = conf().get("linkai_api_base", "https://api.link-ai.chat")
+1 -1
View File
@@ -15,7 +15,7 @@ class OpenAIImage(object):
if conf().get("rate_limit_dalle"):
self.tb4dalle = TokenBucket(conf().get("rate_limit_dalle", 50))
def create_img(self, query, retry_count=0, api_key=None):
def create_img(self, query, retry_count=0, api_key=None, api_base=None):
try:
if conf().get("rate_limit_dalle") and not self.tb4dalle.get_token():
return False, "请求太快了,请休息一下再问我吧"
+1
View File
@@ -8,6 +8,7 @@ from bridge.reply import *
class Channel(object):
channel_type = ""
NOT_SUPPORT_REPLYTYPE = [ReplyType.VOICE, ReplyType.IMAGE]
def startup(self):
+19 -18
View File
@@ -2,43 +2,44 @@
channel factory
"""
from common import const
from .channel import Channel
def create_channel(channel_type):
def create_channel(channel_type) -> Channel:
"""
create a channel instance
:param channel_type: channel type code
:return: channel instance
"""
ch = Channel()
if channel_type == "wx":
from channel.wechat.wechat_channel import WechatChannel
return WechatChannel()
ch = WechatChannel()
elif channel_type == "wxy":
from channel.wechat.wechaty_channel import WechatyChannel
return WechatyChannel()
ch = WechatyChannel()
elif channel_type == "terminal":
from channel.terminal.terminal_channel import TerminalChannel
return TerminalChannel()
ch = TerminalChannel()
elif channel_type == "wechatmp":
from channel.wechatmp.wechatmp_channel import WechatMPChannel
return WechatMPChannel(passive_reply=True)
ch = WechatMPChannel(passive_reply=True)
elif channel_type == "wechatmp_service":
from channel.wechatmp.wechatmp_channel import WechatMPChannel
return WechatMPChannel(passive_reply=False)
ch = WechatMPChannel(passive_reply=False)
elif channel_type == "wechatcom_app":
from channel.wechatcom.wechatcomapp_channel import WechatComAppChannel
return WechatComAppChannel()
ch = WechatComAppChannel()
elif channel_type == "wework":
from channel.wework.wework_channel import WeworkChannel
return WeworkChannel()
ch = WeworkChannel()
elif channel_type == const.FEISHU:
from channel.feishu.feishu_channel import FeiShuChanel
return FeiShuChanel()
raise RuntimeError
ch = FeiShuChanel()
elif channel_type == const.DINGTALK:
from channel.dingtalk.dingtalk_channel import DingTalkChanel
ch = DingTalkChanel()
else:
raise RuntimeError
ch.channel_type = channel_type
return ch
+100
View File
@@ -0,0 +1,100 @@
"""
钉钉通道接入
@author huiwen
@Date 2023/11/28
"""
# -*- coding=utf-8 -*-
from channel.dingtalk.dingtalk_message import DingTalkMessage
from bridge.context import Context
from bridge.reply import Reply
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
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)
+44
View File
@@ -0,0 +1,44 @@
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.other_user_id = event.conversation_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()
elif msg_type == "audio":
# 钉钉支持直接识别语音,所以此处将直接提取文字,当文字处理
self.content = event.extensions['content']['recognition'].strip()
self.ctype = ContextType.TEXT
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
+8 -4
View File
@@ -51,10 +51,14 @@ class FeiShuChanel(ChatChannel):
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
def send(self, reply: Reply, context: Context):
msg = context["msg"]
msg = context.get("msg")
is_group = context["isgroup"]
if msg:
access_token = msg.access_token
else:
access_token = self.fetch_access_token()
headers = {
"Authorization": "Bearer " + msg.access_token,
"Authorization": "Bearer " + access_token,
"Content-Type": "application/json",
}
msg_type = "text"
@@ -63,7 +67,7 @@ class FeiShuChanel(ChatChannel):
content_key = "text"
if reply.type == ReplyType.IMAGE_URL:
# 图片上传
reply_content = self._upload_image_url(reply.content, msg.access_token)
reply_content = self._upload_image_url(reply.content, access_token)
if not reply_content:
logger.warning("[FeiShu] upload file failed")
return
@@ -79,7 +83,7 @@ class FeiShuChanel(ChatChannel):
res = requests.post(url=url, headers=headers, json=data, timeout=(5, 10))
else:
url = "https://open.feishu.cn/open-apis/im/v1/messages"
params = {"receive_id_type": context.get("receive_id_type")}
params = {"receive_id_type": context.get("receive_id_type") or "open_id"}
data = {
"receive_id": context.get("receiver"),
"msg_type": msg_type,
+11
View File
@@ -109,6 +109,7 @@ class WechatChannel(ChatChannel):
def __init__(self):
super().__init__()
self.receivedMsgs = ExpiredDict(60 * 60)
self.auto_login_times = 0
def startup(self):
itchat.instance.receivingRetryCount = 600 # 修改断线超时时间
@@ -120,6 +121,8 @@ class WechatChannel(ChatChannel):
hotReload=hotReload,
statusStorageDir=status_path,
qrCallback=qrCallback,
exitCallback=self.exitCallback,
loginCallback=self.loginCallback
)
self.user_id = itchat.instance.storageClass.userName
self.name = itchat.instance.storageClass.nickName
@@ -127,6 +130,14 @@ class WechatChannel(ChatChannel):
# start message listener
itchat.run()
def exitCallback(self):
self.auto_login_times += 1
if self.auto_login_times < 100:
self.startup()
def loginCallback(self):
pass
# handle_* 系列函数处理收到的消息后构造Context,然后传入produce函数中处理Context和发送回复
# Context包含了消息的所有信息,包括以下属性
# type 消息类型, 包括TEXT、VOICE、IMAGE_CREATE
+1
View File
@@ -22,3 +22,4 @@ MODEL_LIST = ["gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-4", "wenxin", "wenxin-4
# channel
FEISHU = "feishu"
DINGTALK = "dingtalk"
+28
View File
@@ -0,0 +1,28 @@
from bridge.context import Context, ContextType
from bridge.reply import Reply, ReplyType
from common.log import logger
from linkai import LinkAIClient, PushMsg
from config import conf
class ChatClient(LinkAIClient):
def __init__(self, api_key, host, channel):
super().__init__(api_key, host)
self.channel = channel
self.client_type = channel.channel_type
def on_message(self, push_msg: PushMsg):
session_id = push_msg.session_id
msg_content = push_msg.msg_content
logger.info(f"receive msg push, session_id={session_id}, msg_content={msg_content}")
context = Context()
context.type = ContextType.TEXT
context["receiver"] = session_id
context["isgroup"] = push_msg.is_group
self.channel.send(Reply(ReplyType.TEXT, content=msg_content), context)
def start(channel):
client = ChatClient(api_key=conf().get("linkai_api_key"),
host="link-ai.chat", channel=channel)
client.start()
+1 -5
View File
@@ -19,9 +19,6 @@
"ChatGPT测试群",
"ChatGPT测试群2"
],
"group_chat_in_one_session": [
"ChatGPT测试群"
],
"image_create_prefix": [
"画"
],
@@ -32,8 +29,7 @@
"expires_in_seconds": 3600,
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。",
"temperature": 0.7,
"top_p": 1,
"subscribe_msg": "感谢您的关注!\n这里是ChatGPT,可以自由对话。\n支持语音对话。\n支持图片输入。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持tool、角色扮演和文字冒险等丰富的插件。\n输入{trigger_prefix}#help 查看详细指令。",
"subscribe_msg": "感谢您的关注!\n这里是AI智能助手,可以自由对话。\n支持语音对话。\n支持图片输入。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持tool、角色扮演和文字冒险等丰富的插件。\n输入{trigger_prefix}#help 查看详细指令。",
"use_linkai": false,
"linkai_api_key": "",
"linkai_app_code": ""
+5 -1
View File
@@ -132,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配置
+10 -6
View File
@@ -13,12 +13,7 @@ langid # language detect
#install plugin
dulwich
# wechaty
wechaty>=0.10.7
wechaty_puppet>=0.4.23
# pysilk_mod>=1.6.0 # needed by send voice only in wechaty
# wechatmp wechatcom
# wechatmp && wechatcom
web.py
wechatpy
@@ -33,3 +28,12 @@ curl_cffi
# tongyi qwen
broadscope_bailian
# google
google-generativeai
# linkai
linkai
# dingtalk
dingtalk_stream