Skip to content

Commit

Permalink
Merge pull request #80 from Cassius0924/feat-wechatbot-webhook
Browse files Browse the repository at this point in the history
[Feature] 支持 wechatbot-webhook v2.6.1,集成 Bark 提醒
  • Loading branch information
Cassius0924 authored Mar 4, 2024
2 parents 379a3b9 + 837c30c commit 4ec4c07
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 84 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ python3 -m wechatter
## 支持的功能

- [x] **掉线提醒**:当机器人掉线时,通过 Bark 推送提醒消息。
- [x] **消息可引用回复**:用户可以通过引用并回复命令消息进一步获取消息内容。带`(可引用:***)`的机器人消息即为可进一步互动的可引用消息。
- [x] **消息转发**:转发用户或群的消息到其他用户或群,并支持引用回复转发消息。需进行[配置](#%EF%B8%8F-message-forwarding-配置)
![message_forwarding_and_quoted_reply_show](docs/images/message_forwarding_and_quoted_reply_show.png)
Expand Down Expand Up @@ -156,6 +157,7 @@ python3 -m wechatter
| --- | --- | --- |
| `admin_list` | 设置管理员,用于接收机器人状态变化通知 | 填入管理员微信名(不是备注) |
| `admin_group_list` |`admin_list` 同理,接收机器人状态变化通知 | 填入群名称(不是群备注) |
| `bark_url` | 用于接收机器人状态变化通知的 Bark URL | [Bark](https://github.com/Finb/Bark) 仅限 iOS 和 iPadOS |

### ⚙️ Bot 配置

Expand All @@ -168,7 +170,7 @@ python3 -m wechatter
| 配置项 | 解释 | 备注 |
| --- | --- | --- |
| `command_prefix` | 机器人命令前缀 | 默认为 `/` ,可以设置为`>>``!` 等任意字符 |
| `need_mentioned` | 群聊中的命令是否需要@机器人 | 默认为 `True` |
| `need_mentioned` | 群聊中的命令是否需要@机器人 | 默认为 `False` |

### ⚙️ LLM 配置

Expand Down Expand Up @@ -226,10 +228,10 @@ python3 -m wechatter

### ⚙️ Discord Message Forwarding 配置

| 配置项 | 解释 | 备注 |
| --- | --- | --- |
| `discord_message_forwarding_enabled` | 功能开关,是否开启 Discord 消息转发 | 默认为 `False` |
| `discord_message_forwarding_rule_list` | 消息规则列表,每个规则包含三个字段:`from_list``to_discord_webhook_url``to_discord_webhook_name` | |
| 配置项 | 子项 | 解释 | 备注 |
| --- | --- | --- | --- |
| `discord_message_forwarding_enabled` | | 功能开关,是否开启 Discord 消息转发 | 默认为 `False` |
| `discord_message_forwarding_rule_list` | | 消息规则列表,每个规则包含三个字段:`from_list``to_discord_webhook_url``to_discord_webhook_name` |
| | `from_list` | 消息转发来源列表,即消息发送者 | 可以填多个用户名称或群名称,若要转发所有消息则使用 `["%ALL"]` |
| | `from_list_exclude` | 消息转发来源排除列表,不转发此列表的用户和群 | 只在 `from_list``["%ALL"]` 时生效 |
| | `discord_webhook_url` | 消息转发目标 Discord Webhook URL | |
Expand Down
1 change: 1 addition & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ wx_webhook_token: "your_wx_webhook_token"
# Admin
admin_list: [ "文件传输助手", "AdminName" ]
admin_group_list: [ "AdminGroupName" ]
bark_url: your_bark_url


# Bot
Expand Down
54 changes: 27 additions & 27 deletions wechatter/app/routers/wechat.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import json
from typing import Union

from fastapi import APIRouter, Form, UploadFile
from loguru import logger

from wechatter.bot import BotInfo
# from wechatter.bot import BotInfo
from wechatter.commands import commands, quoted_handlers
from wechatter.config import config
from wechatter.database import (
Expand All @@ -22,26 +21,34 @@

router = APIRouter()

# 传入命令字典,构造消息处理器
message_handler = MessageHandler(
commands=commands, quoted_handlers=quoted_handlers, games=games
)


@router.post(config["wx_webhook_recv_api_path"])
async def recv_wechat_msg(
type: str = Form(),
content: Union[UploadFile, str] = Form(),
source: str = Form(),
is_mentioned: str = Form(alias="isMentioned"),
is_system_event: str = Form(alias="isSystemEvent"),
is_from_self: str = Form(alias="isMsgFromSelf"),
):
"""
接收Docker转发过来的消息的接口
用于接收 wxBotWebhook 转发过来的消息的接口
"""

# 更新机器人信息(id和name)
BotInfo.update_from_source(source)
# BotInfo.update_from_source(source)

if type == "unknown":
logger.info(f"收到未知消息:{content}")
return

# 判断是否是系统事件
if is_system_event == "1":
logger.warning(f"收到系统事件:{content}")
handle_system_event(content)
if type in ["system_event_login", "system_event_logout", "system_event_error"]:
logger.info(f"收到系统事件:{type}")
handle_system_event(type)
return

# 不是系统消息,则是用户发来的消息
Expand All @@ -51,47 +58,40 @@ async def recv_wechat_msg(

# 解析命令
# 构造消息对象
message = Message.from_api_msg(
message_obj = Message.from_api_msg(
type=type,
content=content,
source=source,
is_mentioned=is_mentioned,
is_from_self=is_from_self,
)
# 向群组表中添加该群组
add_group(message.group)
add_group(message_obj.group)
# 向用户表中添加该用户
add_person(message.person)
add_person(message_obj.person)
# 向消息表中添加该消息
message.id = add_message(message)
# TODO: 添加自己发送的消息,等待 wechatbot-webhook 支持
message_obj.id = add_message(message_obj)

# DEBUG
print(str(message))
print(str(message_obj))

# 传入命令字典,构造消息处理器
message_handler = MessageHandler(
commands=commands, quoted_handlers=quoted_handlers, games=games
)
# 用户发来的消息均送给消息解析器处理
message_handler.handle_message(message)
message_handler.handle_message(message_obj)

# 快捷回复
# return {"success": True, "data": {"type": "text", "content": "hello world!"}}


def handle_system_event(content: str) -> None:
def handle_system_event(event: str) -> None:
"""
判断系统事件类型,并调用相应的函数
"""
content_dict: dict = json.loads(content)
# 判断是否为机器人登录消息
if content_dict["event"] == "login":
logger.info("机器人登录成功")
if event == "system_event_login":
notifier.notify_logged_in()
elif content_dict["event"] == "logout":
logger.info("机器人已退出登录")
elif event == "system_event_logout":
notifier.notify_logged_out()
elif content_dict["event"] == "error":
elif event == "system_event_error":
pass
else:
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ def gpt4_remove_command_handler(
def _gptx(model: str, to: SendTo, message: str = "", message_obj=None) -> None:
person = to.person
# 获取文件夹下最新的对话记录
chat_info = CopilotGPT4.get_chatting_chat_info(person, model)
chat_info = OpenaiChatGPT.get_chatting_chat_info(person, model)
if message == "": # /gpt4
# 判断对话是否有效
sender.send_msg(to, "正在创建新对话...")
if chat_info is None or CopilotGPT4._is_chat_valid(chat_info):
CopilotGPT4.create_chat(person, model)
if chat_info is None or OpenaiChatGPT._is_chat_valid(chat_info):
OpenaiChatGPT.create_chat(person, model)
logger.info("创建新对话成功")
sender.send_msg(to, "创建新对话成功")
return
Expand All @@ -135,39 +135,39 @@ def _gptx(model: str, to: SendTo, message: str = "", message_obj=None) -> None:
# 如果没有对话记录,则创建新对话
sender.send_msg(to, f"正在调用 {model} 进行对话...")
if chat_info is None:
chat_info = CopilotGPT4.create_chat(person, model)
chat_info = OpenaiChatGPT.create_chat(person, model)
logger.info("无历史对话记录,创建新对话成功")
sender.send_msg(to, "无历史对话记录,创建新对话成功")
try:
response = CopilotGPT4.chat(
response = OpenaiChatGPT.chat(
chat_info, message=message, message_obj=message_obj
)
logger.info(response)
sender.send_msg(to, response)
except Exception as e:
error_message = f"调用Copilot-GPT4-Server失败,错误信息:{str(e)}"
error_message = f"调用 ChatGPT 服务失败,错误信息:{str(e)}"
logger.error(error_message)
sender.send_msg(to, error_message)


def _gptx_chats(model: str, to: SendTo, message: str = "", message_obj=None) -> None:
response = CopilotGPT4.get_chat_list_str(to.person, model)
response = OpenaiChatGPT.get_chat_list_str(to.person, model)
sender.send_msg(to, response)


def _gptx_record(model: str, to: SendTo, message: str = ""):
person = to.person
if message == "":
# 获取当前对话的对话记录
chat_info = CopilotGPT4.get_chatting_chat_info(person, model)
chat_info = OpenaiChatGPT.get_chatting_chat_info(person, model)
else:
# 获取指定对话的对话记录
chat_info = CopilotGPT4.get_chat_info(person, model, int(message))
chat_info = OpenaiChatGPT.get_chat_info(person, model, int(message))
if chat_info is None:
logger.warning("对话不存在")
sender.send_msg(to, "对话不存在")
return
response = CopilotGPT4.get_brief_conversation_str(chat_info)
response = OpenaiChatGPT.get_brief_conversation_str(chat_info)
logger.info(response)
sender.send_msg(to, response)

Expand All @@ -180,22 +180,22 @@ def _gptx_continue(model: str, to: SendTo, message: str = "") -> None:
sender.send_msg(to, "请输入对话记录编号")
return
sender.send_msg(to, f"正在切换到对话记录 {message}...")
chat_info = CopilotGPT4.continue_chat(
chat_info = OpenaiChatGPT.continue_chat(
person=person, model=model, chat_index=int(message)
)
if chat_info is None:
warning_message = "选择历史对话失败,对话不存在"
logger.warning(warning_message)
sender.send_msg(to, warning_message)
return
response = CopilotGPT4.get_brief_conversation_str(chat_info)
response = OpenaiChatGPT.get_brief_conversation_str(chat_info)
response += "====================\n"
response += "对话已选中,输入命令继续对话"
logger.info(response)
sender.send_msg(to, response)


class CopilotGPT4:
class OpenaiChatGPT:
chat_api = join_urls(config["openai_base_api"], "v1/chat/completions")
token = "Bearer " + config["openai_token"]

Expand All @@ -208,8 +208,8 @@ def create_chat(person: Person, model: str) -> GptChatInfo:
:return: 新的对话信息
"""
# 生成上一次对话的主题
CopilotGPT4._save_chatting_chat_topic(person, model)
CopilotGPT4._set_all_chats_not_chatting(person, model)
OpenaiChatGPT._save_chatting_chat_topic(person, model)
OpenaiChatGPT._set_all_chats_not_chatting(person, model)
gpt_chat_info = GptChatInfo(
person=person,
model=model,
Expand Down Expand Up @@ -237,18 +237,18 @@ def continue_chat(
:return: 对话信息
"""
# 读取对话记录文件
chat_info = CopilotGPT4.get_chat_info(person, model, chat_index)
chat_info = OpenaiChatGPT.get_chat_info(person, model, chat_index)
if chat_info is None:
return None
chatting_chat_info = CopilotGPT4.get_chatting_chat_info(person, model)
chatting_chat_info = OpenaiChatGPT.get_chatting_chat_info(person, model)
if chatting_chat_info:
if not CopilotGPT4._is_chat_valid(chatting_chat_info):
if not OpenaiChatGPT._is_chat_valid(chatting_chat_info):
# 如果对话无效,则删除该对话记录后再继续对话
CopilotGPT4._delete_chat(chatting_chat_info)
OpenaiChatGPT._delete_chat(chatting_chat_info)
else:
# 生成上一次对话的主题
CopilotGPT4._save_chatting_chat_topic(person, model)
CopilotGPT4._set_chatting_chat(person, model, chat_info)
OpenaiChatGPT._save_chatting_chat_topic(person, model)
OpenaiChatGPT._set_chatting_chat(person, model, chat_info)
return chat_info

@staticmethod
Expand All @@ -257,7 +257,7 @@ def _set_chatting_chat(person: Person, model: str, chat_info: GptChatInfo) -> No
设置正在进行中的对话记录
"""
# 先将所有对话记录的 is_chatting 字段设置为 False
CopilotGPT4._set_all_chats_not_chatting(person, model)
OpenaiChatGPT._set_all_chats_not_chatting(person, model)
with make_db_session() as session:
chat_info = session.query(DbGptChatInfo).filter_by(id=chat_info.id).first()
if chat_info is None:
Expand Down Expand Up @@ -349,7 +349,7 @@ def get_chat_list_str(person: Person, model: str) -> str:
:param model: 模型
:return: 对话记录
"""
chat_info_list = CopilotGPT4._list_chat_info(person, model)
chat_info_list = OpenaiChatGPT._list_chat_info(person, model)
chat_info_list_str = f"✨==={model}对话记录===✨\n"
if not chat_info_list:
chat_info_list_str += " 📭 无对话记录"
Expand All @@ -374,7 +374,7 @@ def get_chat_info(
:param chat_index: 对话记录索引(从1开始)
:return: 对话信息
"""
chat_info_id_list = CopilotGPT4._list_chat_info(person, model)
chat_info_id_list = OpenaiChatGPT._list_chat_info(person, model)
if not chat_info_id_list:
return None
if chat_index <= 0 or chat_index > len(chat_info_id_list):
Expand Down Expand Up @@ -409,7 +409,7 @@ def chat(chat_info: GptChatInfo, message: str, message_obj) -> str:
:return: GPT 回复
"""
# 对外暴露的对话方法,必须保存对话记录
return CopilotGPT4._chat(
return OpenaiChatGPT._chat(
chat_info=chat_info, message=message, message_obj=message_obj, is_save=True
)

Expand All @@ -426,23 +426,23 @@ def _chat(chat_info: GptChatInfo, message: str, message_obj, is_save: bool) -> s
newconv = [{"role": "user", "content": message}]
# 发送请求
headers = {
"Authorization": CopilotGPT4.token,
"Authorization": OpenaiChatGPT.token,
"Content-Type": "application/json",
}
json = {
"model": chat_info.model,
"messages": DEFAULT_CONVERSATION + chat_info.get_conversation() + newconv,
}
r_json = post_request_json(
url=CopilotGPT4.chat_api, headers=headers, json=json, timeout=60
url=OpenaiChatGPT.chat_api, headers=headers, json=json, timeout=60
)

# 判断是否有 error 或 code 字段
if "error" in r_json or "code" in r_json:
raise ValueError("Copilot-GPT4-Server返回值错误")
raise ValueError("ChatGPT 服务返回值错误")

msg = r_json["choices"][0]["message"]
msg_content = msg.get("content", "调用Copilot-GPT4-Server失败")
msg_content = msg.get("content", "调用 ChatGPT 服务失败")
# 将返回的 assistant 回复添加到对话记录中
if is_save is True:
newconv.append({"role": "assistant", "content": msg_content})
Expand All @@ -464,15 +464,15 @@ def _save_chatting_chat_topic(person: Person, model: str) -> None:
"""
生成正在进行的对话的主题
"""
chat_info = CopilotGPT4.get_chatting_chat_info(person, model)
if chat_info is None or CopilotGPT4._has_topic(chat_info):
chat_info = OpenaiChatGPT.get_chatting_chat_info(person, model)
if chat_info is None or OpenaiChatGPT._has_topic(chat_info):
return
# 生成对话主题
if not CopilotGPT4._is_chat_valid(chat_info):
if not OpenaiChatGPT._is_chat_valid(chat_info):
logger.error("对话记录长度小于1")
return

topic = CopilotGPT4._generate_chat_topic(chat_info)
topic = OpenaiChatGPT._generate_chat_topic(chat_info)
if not topic:
logger.error("生成对话主题失败")
raise ValueError("生成对话主题失败")
Expand All @@ -487,10 +487,10 @@ def _generate_chat_topic(chat_info: GptChatInfo) -> str:
"""
生成对话主题,用于保存对话记录
"""
assert CopilotGPT4._is_chat_valid(chat_info)
assert OpenaiChatGPT._is_chat_valid(chat_info)
# 通过一次对话生成对话主题,但这次对话不保存到对话记录中
prompt = "请用10个字以内总结一下这次对话的主题,不带任何标点符号"
topic = CopilotGPT4._chat(
topic = OpenaiChatGPT._chat(
chat_info=chat_info, message=prompt, message_obj=None, is_save=False
)
# 限制主题长度
Expand Down
Loading

0 comments on commit 4ec4c07

Please sign in to comment.