mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-03-28 05:58:33 +08:00
Merge pull request #1675 from zhayujie/feat-client
feat: channel client
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -29,4 +29,5 @@ plugins/banwords/lib/__pycache__
|
|||||||
!plugins/hello
|
!plugins/hello
|
||||||
!plugins/role
|
!plugins/role
|
||||||
!plugins/keyword
|
!plugins/keyword
|
||||||
!plugins/linkai
|
!plugins/linkai
|
||||||
|
client_config.json
|
||||||
|
|||||||
9
app.py
9
app.py
@@ -8,6 +8,7 @@ from channel import channel_factory
|
|||||||
from common import const
|
from common import const
|
||||||
from config import load_config
|
from config import load_config
|
||||||
from plugins import *
|
from plugins import *
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
def sigterm_handler_wrap(_signo):
|
def sigterm_handler_wrap(_signo):
|
||||||
@@ -46,8 +47,16 @@ def run():
|
|||||||
if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service", "wechatcom_app", "wework", const.FEISHU,const.DINGTALK]:
|
if channel_name in ["wx", "wxy", "terminal", "wechatmp", "wechatmp_service", "wechatcom_app", "wework", const.FEISHU,const.DINGTALK]:
|
||||||
PluginManager().load_plugins()
|
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
|
# startup channel
|
||||||
channel.startup()
|
channel.startup()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("App startup failed!")
|
logger.error("App startup failed!")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import threading
|
|||||||
from common import memory, utils
|
from common import memory, utils
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
||||||
class LinkAIBot(Bot):
|
class LinkAIBot(Bot):
|
||||||
# authentication failed
|
# authentication failed
|
||||||
AUTH_FAILED_CODE = 401
|
AUTH_FAILED_CODE = 401
|
||||||
@@ -83,7 +82,6 @@ class LinkAIBot(Bot):
|
|||||||
if session_message[0].get("role") == "system":
|
if session_message[0].get("role") == "system":
|
||||||
if app_code or model == "wenxin":
|
if app_code or model == "wenxin":
|
||||||
session_message.pop(0)
|
session_message.pop(0)
|
||||||
|
|
||||||
body = {
|
body = {
|
||||||
"app_code": app_code,
|
"app_code": app_code,
|
||||||
"messages": session_message,
|
"messages": session_message,
|
||||||
@@ -92,7 +90,25 @@ class LinkAIBot(Bot):
|
|||||||
"top_p": conf().get("top_p", 1),
|
"top_p": conf().get("top_p", 1),
|
||||||
"frequency_penalty": conf().get("frequency_penalty", 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容
|
"frequency_penalty": conf().get("frequency_penalty", 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容
|
||||||
"presence_penalty": conf().get("presence_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")
|
file_id = context.kwargs.get("file_id")
|
||||||
if file_id:
|
if file_id:
|
||||||
body["file_id"] = file_id
|
body["file_id"] = file_id
|
||||||
@@ -230,7 +246,7 @@ class LinkAIBot(Bot):
|
|||||||
}
|
}
|
||||||
if self.args.get("max_tokens"):
|
if self.args.get("max_tokens"):
|
||||||
body["max_tokens"] = 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
|
# do http request
|
||||||
base_url = conf().get("linkai_api_base", "https://api.link-ai.chat")
|
base_url = conf().get("linkai_api_base", "https://api.link-ai.chat")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from bridge.reply import *
|
|||||||
|
|
||||||
|
|
||||||
class Channel(object):
|
class Channel(object):
|
||||||
|
channel_type = ""
|
||||||
NOT_SUPPORT_REPLYTYPE = [ReplyType.VOICE, ReplyType.IMAGE]
|
NOT_SUPPORT_REPLYTYPE = [ReplyType.VOICE, ReplyType.IMAGE]
|
||||||
|
|
||||||
def startup(self):
|
def startup(self):
|
||||||
|
|||||||
@@ -2,46 +2,44 @@
|
|||||||
channel factory
|
channel factory
|
||||||
"""
|
"""
|
||||||
from common import const
|
from common import const
|
||||||
|
from .channel import Channel
|
||||||
|
|
||||||
def create_channel(channel_type):
|
|
||||||
|
def create_channel(channel_type) -> Channel:
|
||||||
"""
|
"""
|
||||||
create a channel instance
|
create a channel instance
|
||||||
:param channel_type: channel type code
|
:param channel_type: channel type code
|
||||||
:return: channel instance
|
:return: channel instance
|
||||||
"""
|
"""
|
||||||
|
ch = Channel()
|
||||||
if channel_type == "wx":
|
if channel_type == "wx":
|
||||||
from channel.wechat.wechat_channel import WechatChannel
|
from channel.wechat.wechat_channel import WechatChannel
|
||||||
|
ch = WechatChannel()
|
||||||
return WechatChannel()
|
|
||||||
elif channel_type == "wxy":
|
elif channel_type == "wxy":
|
||||||
from channel.wechat.wechaty_channel import WechatyChannel
|
from channel.wechat.wechaty_channel import WechatyChannel
|
||||||
|
ch = WechatyChannel()
|
||||||
return WechatyChannel()
|
|
||||||
elif channel_type == "terminal":
|
elif channel_type == "terminal":
|
||||||
from channel.terminal.terminal_channel import TerminalChannel
|
from channel.terminal.terminal_channel import TerminalChannel
|
||||||
|
ch = TerminalChannel()
|
||||||
return TerminalChannel()
|
|
||||||
elif channel_type == "wechatmp":
|
elif channel_type == "wechatmp":
|
||||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||||
|
ch = WechatMPChannel(passive_reply=True)
|
||||||
return WechatMPChannel(passive_reply=True)
|
|
||||||
elif channel_type == "wechatmp_service":
|
elif channel_type == "wechatmp_service":
|
||||||
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
from channel.wechatmp.wechatmp_channel import WechatMPChannel
|
||||||
|
ch = WechatMPChannel(passive_reply=False)
|
||||||
return WechatMPChannel(passive_reply=False)
|
|
||||||
elif channel_type == "wechatcom_app":
|
elif channel_type == "wechatcom_app":
|
||||||
from channel.wechatcom.wechatcomapp_channel import WechatComAppChannel
|
from channel.wechatcom.wechatcomapp_channel import WechatComAppChannel
|
||||||
|
ch = WechatComAppChannel()
|
||||||
return WechatComAppChannel()
|
|
||||||
elif channel_type == "wework":
|
elif channel_type == "wework":
|
||||||
from channel.wework.wework_channel import WeworkChannel
|
from channel.wework.wework_channel import WeworkChannel
|
||||||
return WeworkChannel()
|
ch = WeworkChannel()
|
||||||
|
|
||||||
elif channel_type == const.FEISHU:
|
elif channel_type == const.FEISHU:
|
||||||
from channel.feishu.feishu_channel import FeiShuChanel
|
from channel.feishu.feishu_channel import FeiShuChanel
|
||||||
return FeiShuChanel()
|
ch = FeiShuChanel()
|
||||||
elif channel_type == const.DINGTALK:
|
elif channel_type == const.DINGTALK:
|
||||||
from channel.dingtalk.dingtalk_channel import DingTalkChanel
|
from channel.dingtalk.dingtalk_channel import DingTalkChanel
|
||||||
return DingTalkChanel()
|
ch = DingTalkChanel()
|
||||||
|
else:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
ch.channel_type = channel_type
|
||||||
|
return ch
|
||||||
|
|||||||
@@ -51,10 +51,14 @@ class FeiShuChanel(ChatChannel):
|
|||||||
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
|
||||||
|
|
||||||
def send(self, reply: Reply, context: Context):
|
def send(self, reply: Reply, context: Context):
|
||||||
msg = context["msg"]
|
msg = context.get("msg")
|
||||||
is_group = context["isgroup"]
|
is_group = context["isgroup"]
|
||||||
|
if msg:
|
||||||
|
access_token = msg.access_token
|
||||||
|
else:
|
||||||
|
access_token = self.fetch_access_token()
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + msg.access_token,
|
"Authorization": "Bearer " + access_token,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
msg_type = "text"
|
msg_type = "text"
|
||||||
@@ -63,7 +67,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
content_key = "text"
|
content_key = "text"
|
||||||
if reply.type == ReplyType.IMAGE_URL:
|
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:
|
if not reply_content:
|
||||||
logger.warning("[FeiShu] upload file failed")
|
logger.warning("[FeiShu] upload file failed")
|
||||||
return
|
return
|
||||||
@@ -79,7 +83,7 @@ class FeiShuChanel(ChatChannel):
|
|||||||
res = requests.post(url=url, headers=headers, json=data, timeout=(5, 10))
|
res = requests.post(url=url, headers=headers, json=data, timeout=(5, 10))
|
||||||
else:
|
else:
|
||||||
url = "https://open.feishu.cn/open-apis/im/v1/messages"
|
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 = {
|
data = {
|
||||||
"receive_id": context.get("receiver"),
|
"receive_id": context.get("receiver"),
|
||||||
"msg_type": msg_type,
|
"msg_type": msg_type,
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ class WechatChannel(ChatChannel):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.receivedMsgs = ExpiredDict(60 * 60)
|
self.receivedMsgs = ExpiredDict(60 * 60)
|
||||||
|
self.auto_login_times = 0
|
||||||
|
|
||||||
def startup(self):
|
def startup(self):
|
||||||
itchat.instance.receivingRetryCount = 600 # 修改断线超时时间
|
itchat.instance.receivingRetryCount = 600 # 修改断线超时时间
|
||||||
@@ -120,6 +121,8 @@ class WechatChannel(ChatChannel):
|
|||||||
hotReload=hotReload,
|
hotReload=hotReload,
|
||||||
statusStorageDir=status_path,
|
statusStorageDir=status_path,
|
||||||
qrCallback=qrCallback,
|
qrCallback=qrCallback,
|
||||||
|
exitCallback=self.exitCallback,
|
||||||
|
loginCallback=self.loginCallback
|
||||||
)
|
)
|
||||||
self.user_id = itchat.instance.storageClass.userName
|
self.user_id = itchat.instance.storageClass.userName
|
||||||
self.name = itchat.instance.storageClass.nickName
|
self.name = itchat.instance.storageClass.nickName
|
||||||
@@ -127,6 +130,14 @@ class WechatChannel(ChatChannel):
|
|||||||
# start message listener
|
# start message listener
|
||||||
itchat.run()
|
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和发送回复
|
# handle_* 系列函数处理收到的消息后构造Context,然后传入produce函数中处理Context和发送回复
|
||||||
# Context包含了消息的所有信息,包括以下属性
|
# Context包含了消息的所有信息,包括以下属性
|
||||||
# type 消息类型, 包括TEXT、VOICE、IMAGE_CREATE
|
# type 消息类型, 包括TEXT、VOICE、IMAGE_CREATE
|
||||||
|
|||||||
28
common/linkai_client.py
Normal file
28
common/linkai_client.py
Normal 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()
|
||||||
Reference in New Issue
Block a user