feat: add plugin instructions and fix some issuesmaster
@@ -54,3 +54,9 @@ class Bridge(object): | |||||
def fetch_translate(self, text, from_lang="", to_lang="en") -> Reply: | def fetch_translate(self, text, from_lang="", to_lang="en") -> Reply: | ||||
return self.get_bot("translate").translate(text, from_lang, to_lang) | return self.get_bot("translate").translate(text, from_lang, to_lang) | ||||
def reset_bot(self): | |||||
""" | |||||
重置bot路由 | |||||
""" | |||||
self.__init__() |
@@ -108,8 +108,12 @@ class ChatChannel(Channel): | |||||
if not conf().get("group_at_off", False): | if not conf().get("group_at_off", False): | ||||
flag = True | flag = True | ||||
pattern = f"@{re.escape(self.name)}(\u2005|\u0020)" | pattern = f"@{re.escape(self.name)}(\u2005|\u0020)" | ||||
content = re.sub(pattern, r"", content) | |||||
subtract_res = re.sub(pattern, r"", content) | |||||
if subtract_res == content and context["msg"].self_display_name: | |||||
# 前缀移除后没有变化,使用群昵称再次移除 | |||||
pattern = f"@{re.escape(context['msg'].self_display_name)}(\u2005|\u0020)" | |||||
subtract_res = re.sub(pattern, r"", content) | |||||
content = subtract_res | |||||
if not flag: | if not flag: | ||||
if context["origin_ctype"] == ContextType.VOICE: | if context["origin_ctype"] == ContextType.VOICE: | ||||
logger.info("[WX]receive group voice, but checkprefix didn't match") | logger.info("[WX]receive group voice, but checkprefix didn't match") | ||||
@@ -24,9 +24,7 @@ is_at: 是否被at | |||||
- (群消息时,一般会存在实际发送者,是群内某个成员的id和昵称,下列项仅在群消息时存在) | - (群消息时,一般会存在实际发送者,是群内某个成员的id和昵称,下列项仅在群消息时存在) | ||||
actual_user_id: 实际发送者id (群聊必填) | actual_user_id: 实际发送者id (群聊必填) | ||||
actual_user_nickname:实际发送者昵称 | actual_user_nickname:实际发送者昵称 | ||||
self_display_name: 自身的展示名,设置群昵称时,该字段表示群昵称 | |||||
_prepare_fn: 准备函数,用于准备消息的内容,比如下载图片等, | _prepare_fn: 准备函数,用于准备消息的内容,比如下载图片等, | ||||
_prepared: 是否已经调用过准备函数 | _prepared: 是否已经调用过准备函数 | ||||
@@ -49,6 +47,7 @@ class ChatMessage(object): | |||||
other_user_id = None | other_user_id = None | ||||
other_user_nickname = None | other_user_nickname = None | ||||
my_msg = False | my_msg = False | ||||
self_display_name = None | |||||
is_group = False | is_group = False | ||||
is_at = False | is_at = False | ||||
@@ -58,7 +58,7 @@ def _check(func): | |||||
if conf().get("hot_reload") == True and int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息 | if conf().get("hot_reload") == True and int(create_time) < int(time.time()) - 60: # 跳过1分钟前的历史消息 | ||||
logger.debug("[WX]history message {} skipped".format(msgId)) | logger.debug("[WX]history message {} skipped".format(msgId)) | ||||
return | return | ||||
if cmsg.my_msg: | |||||
if cmsg.my_msg and not cmsg.is_group: | |||||
logger.debug("[WX]my message {} skipped".format(msgId)) | logger.debug("[WX]my message {} skipped".format(msgId)) | ||||
return | return | ||||
return func(self, cmsg) | return func(self, cmsg) | ||||
@@ -57,7 +57,8 @@ class WechatMessage(ChatMessage): | |||||
self.from_user_nickname = nickname | self.from_user_nickname = nickname | ||||
if self.to_user_id == user_id: | if self.to_user_id == user_id: | ||||
self.to_user_nickname = nickname | self.to_user_nickname = nickname | ||||
try: # 陌生人时候, 'User'字段可能不存在 | |||||
try: # 陌生人时候, User字段可能不存在 | |||||
# my_msg 为True是表示是自己发送的消息 | |||||
self.my_msg = itchat_msg["ToUserName"] == itchat_msg["User"]["UserName"] and \ | self.my_msg = itchat_msg["ToUserName"] == itchat_msg["User"]["UserName"] and \ | ||||
itchat_msg["ToUserName"] != itchat_msg["FromUserName"] | itchat_msg["ToUserName"] != itchat_msg["FromUserName"] | ||||
self.other_user_id = itchat_msg["User"]["UserName"] | self.other_user_id = itchat_msg["User"]["UserName"] | ||||
@@ -66,6 +67,9 @@ class WechatMessage(ChatMessage): | |||||
self.from_user_nickname = self.other_user_nickname | self.from_user_nickname = self.other_user_nickname | ||||
if self.other_user_id == self.to_user_id: | if self.other_user_id == self.to_user_id: | ||||
self.to_user_nickname = self.other_user_nickname | self.to_user_nickname = self.other_user_nickname | ||||
if itchat_msg["User"].get("Self"): | |||||
# 自身的展示名,当设置了群昵称时,该字段表示群昵称 | |||||
self.self_display_name = itchat_msg["User"].get("Self").get("DisplayName") | |||||
except KeyError as e: # 处理偶尔没有对方信息的情况 | except KeyError as e: # 处理偶尔没有对方信息的情况 | ||||
logger.warn("[WX]get other_user_id failed: " + str(e)) | logger.warn("[WX]get other_user_id failed: " + str(e)) | ||||
if self.from_user_id == user_id: | if self.from_user_id == user_id: | ||||
@@ -33,7 +33,7 @@ | |||||
## 插件使用 | ## 插件使用 | ||||
> 使用插件中的知识库管理功能需要首先开启`linkai`对话,依赖全局 `config.json` 中的 `use_linkai` 和 `linkai_api_key` 配置;而midjourney绘画功能则只需填写 `linkai_api_key` 配置。具体可参考 [详细文档](https://link-ai.tech/platform/link-app/wechat)。 | |||||
> 使用插件中的知识库管理功能需要首先开启`linkai`对话,依赖全局 `config.json` 中的 `use_linkai` 和 `linkai_api_key` 配置;而midjourney绘画功能则只需填写 `linkai_api_key` 配置,`use_linkai` 无论是否关闭均可使用。具体可参考 [详细文档](https://link-ai.tech/platform/link-app/wechat)。 | |||||
完成配置后运行项目,会自动运行插件,输入 `#help linkai` 可查看插件功能。 | 完成配置后运行项目,会自动运行插件,输入 `#help linkai` 可查看插件功能。 | ||||
@@ -1,19 +1,10 @@ | |||||
import asyncio | |||||
import json | |||||
import threading | |||||
from concurrent.futures import ThreadPoolExecutor | |||||
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 channel.chat_message import ChatMessage | |||||
from common.log import logger | |||||
from config import conf, global_config | |||||
from config import global_config | |||||
from plugins import * | from plugins import * | ||||
from .midjourney import MJBot, TaskType | |||||
# 任务线程池 | |||||
task_thread_pool = ThreadPoolExecutor(max_workers=4) | |||||
from .midjourney import MJBot | |||||
from bridge import bridge | |||||
@plugins.register( | @plugins.register( | ||||
@@ -66,11 +57,28 @@ class LinkAI(Plugin): | |||||
if len(cmd) == 1 or (len(cmd) == 2 and cmd[1] == "help"): | if len(cmd) == 1 or (len(cmd) == 2 and cmd[1] == "help"): | ||||
_set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO) | _set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO) | ||||
return | return | ||||
if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"): | |||||
# 知识库开关指令 | |||||
if not _is_admin(e_context): | |||||
_set_reply_text("需要管理员权限执行", e_context, level=ReplyType.ERROR) | |||||
return | |||||
is_open = True | |||||
tips_text = "开启" | |||||
if cmd[1] == "close": | |||||
tips_text = "关闭" | |||||
is_open = False | |||||
conf()["use_linkai"] = is_open | |||||
bridge.Bridge().reset_bot() | |||||
_set_reply_text(f"知识库功能已{tips_text}", e_context, level=ReplyType.INFO) | |||||
return | |||||
if len(cmd) == 3 and cmd[1] == "app": | if len(cmd) == 3 and cmd[1] == "app": | ||||
# 知识库应用切换指令 | |||||
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 context.kwargs.get("msg").actual_user_id not in global_config["admin_users"]: | |||||
if not _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] | ||||
@@ -84,7 +92,8 @@ class LinkAI(Plugin): | |||||
super().save_config(self.config) | super().save_config(self.config) | ||||
_set_reply_text(f"应用设置成功: {app_code}", e_context, level=ReplyType.INFO) | _set_reply_text(f"应用设置成功: {app_code}", e_context, level=ReplyType.INFO) | ||||
else: | else: | ||||
_set_reply_text(f"指令错误,请输入{_get_trigger_prefix()}linkai help 获取帮助", e_context, level=ReplyType.INFO) | |||||
_set_reply_text(f"指令错误,请输入{_get_trigger_prefix()}linkai help 获取帮助", e_context, | |||||
level=ReplyType.INFO) | |||||
return | return | ||||
# LinkAI 对话任务处理 | # LinkAI 对话任务处理 | ||||
@@ -127,6 +136,19 @@ class LinkAI(Plugin): | |||||
# 静态方法 | # 静态方法 | ||||
def _is_admin(e_context: EventContext) -> bool: | |||||
""" | |||||
判断消息是否由管理员用户发送 | |||||
:param e_context: 消息上下文 | |||||
:return: True: 是, False: 否 | |||||
""" | |||||
context = e_context["context"] | |||||
if context["isgroup"]: | |||||
return context.kwargs.get("msg").actual_user_id in global_config["admin_users"] | |||||
else: | |||||
return context["receiver"] in global_config["admin_users"] | |||||
def _set_reply_text(content: str, e_context: EventContext, level: ReplyType = ReplyType.ERROR): | def _set_reply_text(content: str, e_context: EventContext, level: ReplyType = ReplyType.ERROR): | ||||
reply = Reply(level, content) | reply = Reply(level, content) | ||||
e_context["reply"] = reply | e_context["reply"] = reply | ||||
@@ -69,7 +69,7 @@ class MJBot: | |||||
:param e_context: 上下文 | :param e_context: 上下文 | ||||
:return: 任务类型枚举 | :return: 任务类型枚举 | ||||
""" | """ | ||||
if not self.config or not self.config.get("enabled"): | |||||
if not self.config: | |||||
return None | return None | ||||
trigger_prefix = conf().get("plugin_trigger_prefix", "$") | trigger_prefix = conf().get("plugin_trigger_prefix", "$") | ||||
context = e_context['context'] | context = e_context['context'] | ||||
@@ -92,9 +92,26 @@ class MJBot: | |||||
session_id = context["session_id"] | session_id = context["session_id"] | ||||
cmd = context.content.split(maxsplit=1) | cmd = context.content.split(maxsplit=1) | ||||
if len(cmd) == 1 and context.type == ContextType.TEXT: | if len(cmd) == 1 and context.type == ContextType.TEXT: | ||||
# midjourney 帮助指令 | |||||
self._set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO) | self._set_reply_text(self.get_help_text(verbose=True), e_context, level=ReplyType.INFO) | ||||
return | return | ||||
if len(cmd) == 2 and (cmd[1] == "open" or cmd[1] == "close"): | |||||
# midjourney 开关指令 | |||||
is_open = True | |||||
tips_text = "开启" | |||||
if cmd[1] == "close": | |||||
tips_text = "关闭" | |||||
is_open = False | |||||
self.config["enabled"] = is_open | |||||
self._set_reply_text(f"Midjourney绘画已{tips_text}", e_context, level=ReplyType.INFO) | |||||
return | |||||
if not self.config.get("enabled"): | |||||
logger.warn("Midjourney绘画未开启,请查看 plugins/linkai/config.json 中的配置") | |||||
self._set_reply_text(f"Midjourney绘画未开启", e_context, level=ReplyType.INFO) | |||||
return | |||||
if not self._check_rate_limit(session_id, e_context): | if not self._check_rate_limit(session_id, e_context): | ||||
logger.warn("[MJ] midjourney task exceed rate limit") | logger.warn("[MJ] midjourney task exceed rate limit") | ||||
return | return | ||||
@@ -19,7 +19,7 @@ class Plugin: | |||||
# 全局配置不存在 或者 未开启全局配置开关,则获取插件目录下的配置 | # 全局配置不存在 或者 未开启全局配置开关,则获取插件目录下的配置 | ||||
plugin_config_path = os.path.join(self.path, "config.json") | plugin_config_path = os.path.join(self.path, "config.json") | ||||
if os.path.exists(plugin_config_path): | if os.path.exists(plugin_config_path): | ||||
with open(plugin_config_path, "r") as f: | |||||
with open(plugin_config_path, "r", encoding="utf-8") as f: | |||||
plugin_conf = json.load(f) | plugin_conf = json.load(f) | ||||
logger.debug(f"loading plugin config, plugin_name={self.name}, conf={plugin_conf}") | logger.debug(f"loading plugin config, plugin_name={self.name}, conf={plugin_conf}") | ||||
return plugin_conf | return plugin_conf | ||||