mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-21 14:17:11 +08:00
Merge branch 'master' into feat-agent
This commit is contained in:
20
README.md
20
README.md
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
# 演示
|
# 演示
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/26161723/233777277-e3b9928e-b88f-43e2-b0e0-3cbc923bc799.mp4
|
https://github.com/zhayujie/chatgpt-on-wechat/assets/26161723/d5154020-36e3-41db-8706-40ce9f3f1b1e
|
||||||
|
|
||||||
Demo made by [Visionn](https://www.wangpc.cc/)
|
Demo made by [Visionn](https://www.wangpc.cc/)
|
||||||
|
|
||||||
@@ -50,11 +50,13 @@ Demo made by [Visionn](https://www.wangpc.cc/)
|
|||||||
|
|
||||||
## 准备
|
## 准备
|
||||||
|
|
||||||
### 1. OpenAI账号注册
|
### 1. 账号注册
|
||||||
|
|
||||||
前往 [OpenAI注册页面](https://beta.openai.com/signup) 创建账号,参考这篇 [教程](https://www.pythonthree.com/register-openai-chatgpt/) 可以通过虚拟手机号来接收验证码。创建完账号则前往 [API管理页面](https://beta.openai.com/account/api-keys) 创建一个 API Key 并保存下来,后面需要在项目中配置这个key。
|
项目默认使用OpenAI接口,需前往 [OpenAI注册页面](https://beta.openai.com/signup) 创建账号,创建完账号则前往 [API管理页面](https://beta.openai.com/account/api-keys) 创建一个 API Key 并保存下来,后面需要在项目中配置这个key。接口需要海外网络访问及绑定信用卡支付。
|
||||||
|
|
||||||
> 项目中默认使用的对话模型是 gpt3.5 turbo,计费方式是约每 500 汉字 (包含请求和回复) 消耗 $0.002,图片生成是每张消耗 $0.016。
|
> 默认对话模型是 openai 的 gpt-3.5-turbo,计费方式是约每 1000tokens (约750个英文单词 或 500汉字,包含请求和回复) 消耗 $0.002,图片生成是Dell E模型,每张消耗 $0.016。
|
||||||
|
|
||||||
|
项目同时也支持使用 LinkAI 接口,无需代理,可使用 文心、讯飞、GPT-3、GPT-4 等模型,支持 定制化知识库、联网搜索、MJ绘图、文档总结和对话等能力。修改配置即可一键切换,参考 [接入文档](https://link-ai.tech/platform/link-app/wechat)。
|
||||||
|
|
||||||
### 2.运行环境
|
### 2.运行环境
|
||||||
|
|
||||||
@@ -184,10 +186,10 @@ pip3 install azure-cognitiveservices-speech
|
|||||||
如果是开发机 **本地运行**,直接在项目根目录下执行:
|
如果是开发机 **本地运行**,直接在项目根目录下执行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 app.py
|
python3 app.py # windows环境下该命令通常为 python app.py
|
||||||
```
|
```
|
||||||
终端输出二维码后,使用微信进行扫码,当输出 "Start auto replying" 时表示自动回复程序已经成功运行了(注意:用于登录的微信需要在支付处已完成实名认证)。扫码登录后你的账号就成为机器人了,可以在微信手机端通过配置的关键词触发自动回复 (任意好友发送消息给你,或是自己发消息给好友),参考[#142](https://github.com/zhayujie/chatgpt-on-wechat/issues/142)。
|
|
||||||
|
|
||||||
|
终端输出二维码后,使用微信进行扫码,当输出 "Start auto replying" 时表示自动回复程序已经成功运行了(注意:用于登录的微信需要在支付处已完成实名认证)。扫码登录后你的账号就成为机器人了,可以在微信手机端通过配置的关键词触发自动回复 (任意好友发送消息给你,或是自己发消息给好友),参考[#142](https://github.com/zhayujie/chatgpt-on-wechat/issues/142)。
|
||||||
|
|
||||||
### 2.服务器部署
|
### 2.服务器部署
|
||||||
|
|
||||||
@@ -269,8 +271,4 @@ FAQs: <https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs>
|
|||||||
|
|
||||||
## 联系
|
## 联系
|
||||||
|
|
||||||
欢迎提交PR、Issues,以及Star支持一下。程序运行遇到问题可以查看 [常见问题列表](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs) ,其次前往 [Issues](https://github.com/zhayujie/chatgpt-on-wechat/issues) 中搜索。
|
欢迎提交PR、Issues,以及Star支持一下。程序运行遇到问题可以查看 [常见问题列表](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs) ,其次前往 [Issues](https://github.com/zhayujie/chatgpt-on-wechat/issues) 中搜索。参与更多讨论可加入技术交流群。
|
||||||
|
|
||||||
如果你想了解更多项目细节,与开发者们交流更多关于AI技术的实践,欢迎加入星球:
|
|
||||||
|
|
||||||
<a href="https://public.zsxq.com/groups/88885848842852.html"><img width="360" src="./docs/images/planet.jpg"></a>
|
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ class LinkAIBot(Bot, OpenAIImage):
|
|||||||
response = res.json()
|
response = res.json()
|
||||||
reply_content = response["choices"][0]["message"]["content"]
|
reply_content = response["choices"][0]["message"]["content"]
|
||||||
total_tokens = response["usage"]["total_tokens"]
|
total_tokens = response["usage"]["total_tokens"]
|
||||||
|
logger.info(f"[LINKAI] reply={reply_content}, total_tokens={total_tokens}")
|
||||||
|
self.sessions.session_reply(reply_content, session_id, total_tokens)
|
||||||
|
|
||||||
agent_suffix = self._fetch_agent_suffix(response)
|
agent_suffix = self._fetch_agent_suffix(response)
|
||||||
if agent_suffix:
|
if agent_suffix:
|
||||||
reply_content += agent_suffix
|
reply_content += agent_suffix
|
||||||
@@ -101,8 +104,6 @@ class LinkAIBot(Bot, OpenAIImage):
|
|||||||
knowledge_suffix = self._fetch_knowledge_search_suffix(response)
|
knowledge_suffix = self._fetch_knowledge_search_suffix(response)
|
||||||
if knowledge_suffix:
|
if knowledge_suffix:
|
||||||
reply_content += knowledge_suffix
|
reply_content += knowledge_suffix
|
||||||
logger.info(f"[LINKAI] reply={reply_content}, total_tokens={total_tokens}")
|
|
||||||
self.sessions.session_reply(reply_content, session_id, total_tokens)
|
|
||||||
return Reply(ReplyType.TEXT, reply_content)
|
return Reply(ReplyType.TEXT, reply_content)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class ContextType(Enum):
|
|||||||
SHARING = 6 # 分享信息
|
SHARING = 6 # 分享信息
|
||||||
|
|
||||||
IMAGE_CREATE = 10 # 创建图片命令
|
IMAGE_CREATE = 10 # 创建图片命令
|
||||||
|
ACCEPT_FRIEND = 19 # 同意好友请求
|
||||||
JOIN_GROUP = 20 # 加入群聊
|
JOIN_GROUP = 20 # 加入群聊
|
||||||
PATPAT = 21 # 拍了拍
|
PATPAT = 21 # 拍了拍
|
||||||
FUNCTION = 22 # 函数调用
|
FUNCTION = 22 # 函数调用
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ class ChatChannel(Channel):
|
|||||||
elif context.type == ContextType.IMAGE: # 图片消息,当前仅做下载保存到本地的逻辑
|
elif context.type == ContextType.IMAGE: # 图片消息,当前仅做下载保存到本地的逻辑
|
||||||
cmsg = context["msg"]
|
cmsg = context["msg"]
|
||||||
cmsg.prepare()
|
cmsg.prepare()
|
||||||
|
elif context.type == ContextType.SHARING: # 分享信息,当前无默认逻辑
|
||||||
|
pass
|
||||||
elif context.type == ContextType.FUNCTION or context.type == ContextType.FILE: # 文件消息及函数调用等,当前无默认逻辑
|
elif context.type == ContextType.FUNCTION or context.type == ContextType.FILE: # 文件消息及函数调用等,当前无默认逻辑
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ class WechatChannel(ChatChannel):
|
|||||||
@time_checker
|
@time_checker
|
||||||
@_check
|
@_check
|
||||||
def handle_single(self, cmsg: ChatMessage):
|
def handle_single(self, cmsg: ChatMessage):
|
||||||
|
# filter system message
|
||||||
|
if cmsg.other_user_id in ["weixin"]:
|
||||||
|
return
|
||||||
if cmsg.ctype == ContextType.VOICE:
|
if cmsg.ctype == ContextType.VOICE:
|
||||||
if conf().get("speech_recognition") != True:
|
if conf().get("speech_recognition") != True:
|
||||||
return
|
return
|
||||||
@@ -167,7 +170,7 @@ class WechatChannel(ChatChannel):
|
|||||||
logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content))
|
logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content))
|
||||||
elif cmsg.ctype == ContextType.IMAGE:
|
elif cmsg.ctype == ContextType.IMAGE:
|
||||||
logger.debug("[WX]receive image for group msg: {}".format(cmsg.content))
|
logger.debug("[WX]receive image for group msg: {}".format(cmsg.content))
|
||||||
elif cmsg.ctype in [ContextType.JOIN_GROUP, ContextType.PATPAT]:
|
elif cmsg.ctype in [ContextType.JOIN_GROUP, ContextType.PATPAT, ContextType.ACCEPT_FRIEND]:
|
||||||
logger.debug("[WX]receive note msg: {}".format(cmsg.content))
|
logger.debug("[WX]receive note msg: {}".format(cmsg.content))
|
||||||
elif cmsg.ctype == ContextType.TEXT:
|
elif cmsg.ctype == ContextType.TEXT:
|
||||||
# logger.debug("[WX]receive group msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg))
|
# logger.debug("[WX]receive group msg: {}, cmsg={}".format(json.dumps(cmsg._rawmsg, ensure_ascii=False), cmsg))
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ class WechatMessage(ChatMessage):
|
|||||||
self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[-1]
|
self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[-1]
|
||||||
elif "加入群聊" in itchat_msg["Content"]:
|
elif "加入群聊" in itchat_msg["Content"]:
|
||||||
self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0]
|
self.actual_user_nickname = re.findall(r"\"(.*?)\"", itchat_msg["Content"])[0]
|
||||||
|
elif "你已添加了" in itchat_msg["Content"]: #通过好友请求
|
||||||
|
self.ctype = ContextType.ACCEPT_FRIEND
|
||||||
|
self.content = itchat_msg["Content"]
|
||||||
elif "拍了拍我" in itchat_msg["Content"]:
|
elif "拍了拍我" in itchat_msg["Content"]:
|
||||||
self.ctype = ContextType.PATPAT
|
self.ctype = ContextType.PATPAT
|
||||||
self.content = itchat_msg["Content"]
|
self.content = itchat_msg["Content"]
|
||||||
|
|||||||
@@ -20,9 +20,7 @@
|
|||||||
"ChatGPT测试群"
|
"ChatGPT测试群"
|
||||||
],
|
],
|
||||||
"image_create_prefix": [
|
"image_create_prefix": [
|
||||||
"画",
|
"画"
|
||||||
"看",
|
|
||||||
"找"
|
|
||||||
],
|
],
|
||||||
"speech_recognition": false,
|
"speech_recognition": false,
|
||||||
"group_speech_recognition": false,
|
"group_speech_recognition": false,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import plugins
|
import plugins
|
||||||
from bridge.context import ContextType
|
from bridge.context import ContextType
|
||||||
from bridge.reply import Reply, ReplyType
|
from bridge.reply import Reply, ReplyType
|
||||||
from config import global_config
|
|
||||||
from plugins import *
|
from plugins import *
|
||||||
from .midjourney import MJBot
|
from .midjourney import MJBot
|
||||||
from .summary import LinkSummary
|
from .summary import LinkSummary
|
||||||
@@ -9,7 +8,7 @@ from bridge import bridge
|
|||||||
from common.expired_dict import ExpiredDict
|
from common.expired_dict import ExpiredDict
|
||||||
from common import const
|
from common import const
|
||||||
import os
|
import os
|
||||||
|
from .utils import Util
|
||||||
|
|
||||||
@plugins.register(
|
@plugins.register(
|
||||||
name="linkai",
|
name="linkai",
|
||||||
@@ -129,7 +128,7 @@ class LinkAI(Plugin):
|
|||||||
|
|
||||||
if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"):
|
if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"):
|
||||||
# 知识库开关指令
|
# 知识库开关指令
|
||||||
if not _is_admin(e_context):
|
if not Util.is_admin(e_context):
|
||||||
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
|
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
|
||||||
return
|
return
|
||||||
is_open = True
|
is_open = True
|
||||||
@@ -147,7 +146,7 @@ class LinkAI(Plugin):
|
|||||||
if not context.kwargs.get("isgroup"):
|
if not context.kwargs.get("isgroup"):
|
||||||
_set_reply_text("该指令需在群聊中使用", e_context, level=ReplyType.ERROR)
|
_set_reply_text("该指令需在群聊中使用", e_context, level=ReplyType.ERROR)
|
||||||
return
|
return
|
||||||
if not _is_admin(e_context):
|
if not Util.is_admin(e_context):
|
||||||
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
|
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
|
||||||
return
|
return
|
||||||
app_code = cmd[2]
|
app_code = cmd[2]
|
||||||
@@ -164,7 +163,7 @@ class LinkAI(Plugin):
|
|||||||
|
|
||||||
if len(cmd) == 3 and cmd[1] == "sum" and (cmd[2] == "open" or cmd[2] == "close"):
|
if len(cmd) == 3 and cmd[1] == "sum" and (cmd[2] == "open" or cmd[2] == "close"):
|
||||||
# 知识库开关指令
|
# 知识库开关指令
|
||||||
if not _is_admin(e_context):
|
if not Util.is_admin(e_context):
|
||||||
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
|
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
|
||||||
return
|
return
|
||||||
is_open = True
|
is_open = True
|
||||||
@@ -253,23 +252,6 @@ def _send_info(e_context: EventContext, content: str):
|
|||||||
channel = e_context["channel"]
|
channel = e_context["channel"]
|
||||||
channel.send(reply, e_context["context"])
|
channel.send(reply, e_context["context"])
|
||||||
|
|
||||||
# 静态方法
|
|
||||||
def _is_admin(e_context: EventContext) -> bool:
|
|
||||||
"""
|
|
||||||
判断消息是否由管理员用户发送
|
|
||||||
:param e_context: 消息上下文
|
|
||||||
:return: True: 是, False: 否
|
|
||||||
"""
|
|
||||||
context = e_context["context"]
|
|
||||||
if context["isgroup"]:
|
|
||||||
actual_user_id= context.kwargs.get("msg").actual_user_id
|
|
||||||
for admin_user in global_config["admin_users"]:
|
|
||||||
if actual_user_id and actual_user_id in admin_user:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return context["receiver"] in global_config["admin_users"]
|
|
||||||
|
|
||||||
|
|
||||||
def _find_user_id(context):
|
def _find_user_id(context):
|
||||||
if context["isgroup"]:
|
if context["isgroup"]:
|
||||||
@@ -290,7 +272,8 @@ def _find_sum_id(context):
|
|||||||
return USER_FILE_MAP.get(_find_user_id(context) + "-sum_id")
|
return USER_FILE_MAP.get(_find_user_id(context) + "-sum_id")
|
||||||
|
|
||||||
def _find_file_id(context):
|
def _find_file_id(context):
|
||||||
return USER_FILE_MAP.get(_find_user_id(context) + "-file_id")
|
user_id = _find_user_id(context)
|
||||||
|
if user_id:
|
||||||
|
return USER_FILE_MAP.get(user_id + "-file_id")
|
||||||
|
|
||||||
|
USER_FILE_MAP = ExpiredDict(conf().get("expires_in_seconds") or 60 * 30)
|
||||||
USER_FILE_MAP = ExpiredDict(conf().get("expires_in_seconds") or 60 * 60)
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from bridge.reply import Reply, ReplyType
|
|||||||
import asyncio
|
import asyncio
|
||||||
from bridge.context import ContextType
|
from bridge.context import ContextType
|
||||||
from plugins import EventContext, EventAction
|
from plugins import EventContext, EventAction
|
||||||
|
from .utils import Util
|
||||||
|
|
||||||
INVALID_REQUEST = 410
|
INVALID_REQUEST = 410
|
||||||
NOT_FOUND_ORIGIN_IMAGE = 461
|
NOT_FOUND_ORIGIN_IMAGE = 461
|
||||||
@@ -48,7 +49,7 @@ task_name_mapping = {
|
|||||||
|
|
||||||
|
|
||||||
class MJTask:
|
class MJTask:
|
||||||
def __init__(self, id, user_id: str, task_type: TaskType, raw_prompt=None, expires: int = 60 * 30,
|
def __init__(self, id, user_id: str, task_type: TaskType, raw_prompt=None, expires: int = 60 * 6,
|
||||||
status=Status.PENDING):
|
status=Status.PENDING):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
@@ -113,6 +114,9 @@ class MJBot:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"):
|
if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"):
|
||||||
|
if not Util.is_admin(e_context):
|
||||||
|
Util.set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR)
|
||||||
|
return
|
||||||
# midjourney 开关指令
|
# midjourney 开关指令
|
||||||
is_open = True
|
is_open = True
|
||||||
tips_text = "开启"
|
tips_text = "开启"
|
||||||
|
|||||||
28
plugins/linkai/utils.py
Normal file
28
plugins/linkai/utils.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from config import global_config
|
||||||
|
from bridge.reply import Reply, ReplyType
|
||||||
|
from plugins.event import EventContext, EventAction
|
||||||
|
|
||||||
|
|
||||||
|
class Util:
|
||||||
|
@staticmethod
|
||||||
|
def is_admin(e_context: EventContext) -> bool:
|
||||||
|
"""
|
||||||
|
判断消息是否由管理员用户发送
|
||||||
|
:param e_context: 消息上下文
|
||||||
|
:return: True: 是, False: 否
|
||||||
|
"""
|
||||||
|
context = e_context["context"]
|
||||||
|
if context["isgroup"]:
|
||||||
|
actual_user_id = context.kwargs.get("msg").actual_user_id
|
||||||
|
for admin_user in global_config["admin_users"]:
|
||||||
|
if actual_user_id and actual_user_id in admin_user:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return context["receiver"] in global_config["admin_users"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_reply_text(content: str, e_context: EventContext, level: ReplyType = ReplyType.ERROR):
|
||||||
|
reply = Reply(level, content)
|
||||||
|
e_context["reply"] = reply
|
||||||
|
e_context.action = EventAction.BREAK_PASS
|
||||||
Reference in New Issue
Block a user