@@ -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/) | |||
@@ -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.运行环境 | |||
@@ -184,10 +186,10 @@ pip3 install azure-cognitiveservices-speech | |||
如果是开发机 **本地运行**,直接在项目根目录下执行: | |||
```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.服务器部署 | |||
@@ -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) 中搜索。 | |||
如果你想了解更多项目细节,与开发者们交流更多关于AI技术的实践,欢迎加入星球: | |||
<a href="https://public.zsxq.com/groups/88885848842852.html"><img width="360" src="./docs/images/planet.jpg"></a> | |||
欢迎提交PR、Issues,以及Star支持一下。程序运行遇到问题可以查看 [常见问题列表](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs) ,其次前往 [Issues](https://github.com/zhayujie/chatgpt-on-wechat/issues) 中搜索。参与更多讨论可加入技术交流群。 |
@@ -94,6 +94,9 @@ class LinkAIBot(Bot, OpenAIImage): | |||
response = res.json() | |||
reply_content = response["choices"][0]["message"]["content"] | |||
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) | |||
if agent_suffix: | |||
reply_content += agent_suffix | |||
@@ -101,8 +104,6 @@ class LinkAIBot(Bot, OpenAIImage): | |||
knowledge_suffix = self._fetch_knowledge_search_suffix(response) | |||
if 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) | |||
else: | |||
@@ -12,6 +12,7 @@ class ContextType(Enum): | |||
SHARING = 6 # 分享信息 | |||
IMAGE_CREATE = 10 # 创建图片命令 | |||
ACCEPT_FRIEND = 19 # 同意好友请求 | |||
JOIN_GROUP = 20 # 加入群聊 | |||
PATPAT = 21 # 拍了拍 | |||
FUNCTION = 22 # 函数调用 | |||
@@ -205,6 +205,8 @@ class ChatChannel(Channel): | |||
elif context.type == ContextType.IMAGE: # 图片消息,当前仅做下载保存到本地的逻辑 | |||
cmsg = context["msg"] | |||
cmsg.prepare() | |||
elif context.type == ContextType.SHARING: # 分享信息,当前无默认逻辑 | |||
pass | |||
elif context.type == ContextType.FUNCTION or context.type == ContextType.FILE: # 文件消息及函数调用等,当前无默认逻辑 | |||
pass | |||
else: | |||
@@ -142,6 +142,9 @@ class WechatChannel(ChatChannel): | |||
@time_checker | |||
@_check | |||
def handle_single(self, cmsg: ChatMessage): | |||
# filter system message | |||
if cmsg.other_user_id in ["weixin"]: | |||
return | |||
if cmsg.ctype == ContextType.VOICE: | |||
if conf().get("speech_recognition") != True: | |||
return | |||
@@ -167,7 +170,7 @@ class WechatChannel(ChatChannel): | |||
logger.debug("[WX]receive voice for group msg: {}".format(cmsg.content)) | |||
elif cmsg.ctype == ContextType.IMAGE: | |||
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)) | |||
elif cmsg.ctype == ContextType.TEXT: | |||
# 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] | |||
elif "加入群聊" in itchat_msg["Content"]: | |||
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"]: | |||
self.ctype = ContextType.PATPAT | |||
self.content = itchat_msg["Content"] | |||
@@ -20,9 +20,7 @@ | |||
"ChatGPT测试群" | |||
], | |||
"image_create_prefix": [ | |||
"画", | |||
"看", | |||
"找" | |||
"画" | |||
], | |||
"speech_recognition": false, | |||
"group_speech_recognition": false, | |||
@@ -1,7 +1,6 @@ | |||
import plugins | |||
from bridge.context import ContextType | |||
from bridge.reply import Reply, ReplyType | |||
from config import global_config | |||
from plugins import * | |||
from .midjourney import MJBot | |||
from .summary import LinkSummary | |||
@@ -9,7 +8,7 @@ from bridge import bridge | |||
from common.expired_dict import ExpiredDict | |||
from common import const | |||
import os | |||
from .utils import Util | |||
@plugins.register( | |||
name="linkai", | |||
@@ -129,7 +128,7 @@ class LinkAI(Plugin): | |||
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) | |||
return | |||
is_open = True | |||
@@ -147,7 +146,7 @@ class LinkAI(Plugin): | |||
if not context.kwargs.get("isgroup"): | |||
_set_reply_text("该指令需在群聊中使用", e_context, level=ReplyType.ERROR) | |||
return | |||
if not _is_admin(e_context): | |||
if not Util.is_admin(e_context): | |||
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR) | |||
return | |||
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 not _is_admin(e_context): | |||
if not Util.is_admin(e_context): | |||
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR) | |||
return | |||
is_open = True | |||
@@ -253,23 +252,6 @@ def _send_info(e_context: EventContext, content: str): | |||
channel = e_context["channel"] | |||
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): | |||
if context["isgroup"]: | |||
@@ -290,7 +272,8 @@ def _find_sum_id(context): | |||
return USER_FILE_MAP.get(_find_user_id(context) + "-sum_id") | |||
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 * 60) | |||
USER_FILE_MAP = ExpiredDict(conf().get("expires_in_seconds") or 60 * 30) |
@@ -8,6 +8,7 @@ from bridge.reply import Reply, ReplyType | |||
import asyncio | |||
from bridge.context import ContextType | |||
from plugins import EventContext, EventAction | |||
from .utils import Util | |||
INVALID_REQUEST = 410 | |||
NOT_FOUND_ORIGIN_IMAGE = 461 | |||
@@ -48,7 +49,7 @@ task_name_mapping = { | |||
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): | |||
self.id = id | |||
self.user_id = user_id | |||
@@ -113,6 +114,9 @@ class MJBot: | |||
return | |||
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 开关指令 | |||
is_open = True | |||
tips_text = "开启" | |||
@@ -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 |