Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

支持回复图片视频,配置全局禁用插件,增加插件转换微信分享 #33

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ plugins/banwords/lib/__pycache__
!plugins/linkai
!plugins/jina_sum
!plugins/jina_sum/**/
!plugins/transform_sharing
client_config.json
22 changes: 18 additions & 4 deletions bot/dify/dify_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __init__(self):

def reply(self, query, context: Context=None):
# acquire reply content
if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE:
if context.type in [ContextType.TEXT, ContextType.IMAGE_CREATE, ContextType.SHARING]:
if context.type == ContextType.IMAGE_CREATE:
query = conf().get('image_create_prefix', ['画'])[0] + query
logger.info("[DIFY] query={}".format(query))
Expand Down Expand Up @@ -110,9 +110,23 @@ def _handle_chatbot(self, query: str, session: DifySession):
# }
rsp_data = response.json()
logger.debug("[DIFY] usage {}".format(rsp_data.get('metadata', {}).get('usage', 0)))
# TODO: 处理返回的图片文件
# {"answer": "![image](/files/tools/dbf9cd7c-2110-4383-9ba8-50d9fd1a4815.png?timestamp=1713970391&nonce=0d5badf2e39466042113a4ba9fd9bf83&sign=OVmdCxCEuEYwc9add3YNFFdUpn4VdFKgl84Cg54iLnU=)"}
reply = Reply(ReplyType.TEXT, rsp_data['answer'])
reply = Reply()
reply_text = rsp_data['answer']
if (reply_text.startswith("http://") or reply_text.startswith("https://")) and any(reply_text.endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".gif", ".img"]):
# 如果是以 http:// 或 https:// 开头,且".jpg", ".jpeg", ".png", ".gif", ".img"结尾,则认为是图片 URL。
reply = Reply()
reply.type = ReplyType.IMAGE_URL
reply.content = reply_text
elif (reply_text.startswith("http://") or reply_text.startswith("https://")) and any(reply_text.endswith(ext) for ext in [".mp4"]):
# 如果是以 http:// 或 https:// 开头,且".mp4"结尾,则下载视频到tmp目录并发送给用户
reply = Reply()
reply.type = ReplyType.VIDEO_URL
reply.content = reply_text
else:
# 否则认为是普通文本
reply = Reply()
reply.type = ReplyType.TEXT
reply.content = reply_text
# 设置dify conversation_id, 依靠dify管理上下文
if session.get_conversation_id() == '':
session.set_conversation_id(rsp_data['conversation_id'])
Expand Down
17 changes: 14 additions & 3 deletions channel/chat_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ def _compose_context(self, ctype: ContextType, content, **kwargs):
logger.debug("[WX]self message skipped")
return None

# 处理群分享
if ctype == ContextType.SHARING and context.get("isgroup", False):
# 如果开启群分享白名单,则直接处理
group_name = context["msg"].other_user_nickname
group_name_sharing_white_list = config.get("group_name_sharing_white_list", [])
if ctype == ContextType.SHARING and any(
[
"ALL_GROUP" in group_name_sharing_white_list,
check_contain(group_name, group_name_sharing_white_list),
]
):
return context

# 消息内容匹配过程,并处理content
if ctype == ContextType.TEXT:
if first_in and "」\n- - - - - - -" in content: # 初次匹配 过滤引用消息
Expand Down Expand Up @@ -188,7 +201,7 @@ def _generate_reply(self, context: Context, reply: Reply = Reply()) -> Reply:
reply = e_context["reply"]
if not e_context.is_pass():
logger.debug("[WX] ready to handle context: type={}, content={}".format(context.type, context.content))
if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: # 文字和图片消息
if context.type in [ContextType.TEXT, ContextType.IMAGE_CREATE, ContextType.SHARING]: # 文字、分享和图片消息
context["channel"] = e_context["channel"]
reply = super().build_reply_content(context.content, context)
elif context.type == ContextType.VOICE: # 语音消息
Expand Down Expand Up @@ -225,8 +238,6 @@ def _generate_reply(self, context: Context, reply: Reply = Reply()) -> Reply:
}
elif context.type == ContextType.ACCEPT_FRIEND: # 好友申请,匹配字符串
reply = self._build_friend_request_reply(context)
elif context.type == ContextType.SHARING: # 分享信息,当前无默认逻辑
pass
elif context.type == ContextType.FUNCTION or context.type == ContextType.FILE: # 文件消息及函数调用等,当前无默认逻辑
pass
else:
Expand Down
13 changes: 13 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,19 @@
"linkai_api_key": "",
"linkai_app_code": "",
"linkai_api_base": "https://api.link-ai.chat", # linkAI服务地址,若国内无法访问或延迟较高可改为 https://api.link-ai.tech
# 插件开关
"use_jinasum": True, # 是否使用JinaSum插件
"use_godcmd": True, # 是否使用管理员指令插件
"use_finish": True, # 是否使用未知指令兜底插件
"use_dungeon": True, # 是否使用文字冒险插件
"use_bdunit": True, # 是否使用百度UNIT插件
"use_banwords": True, # 是否使用敏感词过滤插件
"use_hello": True, # 是否使用入群自动迎新插件
"use_keyword": True, # 是否使用关键词匹配过滤插件
"use_role": True, # 是否使用角色扮演插件
"use_tool": True, # 是否使用工具插件,
"use_transformsharing": True, # 是否使用TransformSharing插件
"group_name_sharing_white_list": ["ALL_GROUP"], # 开启处理分享的群名称列表
}


Expand Down
6 changes: 5 additions & 1 deletion plugins/plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@ def refresh_order(self):

def activate_plugins(self): # 生成新开启的插件实例
failed_plugins = []
for name, plugincls in self.plugins.items():
for name, plugincls in self.plugins.items(): # 如果config中有配置 use_<plugin_name> 的则禁用插件
if conf().get("use_" + name.lower(), True) == False:
logger.warn("插件 %s 已被禁用" % (name))
self.disable_plugin(name)
continue
if plugincls.enabled:
if name not in self.instances:
try:
Expand Down
2 changes: 2 additions & 0 deletions plugins/transform_sharing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .transform_sharing import *

4 changes: 4 additions & 0 deletions plugins/transform_sharing/config.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"white_url_list": [],
"black_url_list": ["https://support.weixin.qq.com", "https://channels-aladin.wxqcloud.qq.com"]
}
97 changes: 97 additions & 0 deletions plugins/transform_sharing/transform_sharing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# encoding:utf-8
import json
import os
import html
from urllib.parse import urlparse

import requests

import plugins
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from common.log import logger
from plugins import *

@plugins.register(
name="TransformSharing",
desire_priority=5,
hidden=False,
desc="转换分享链接",
version="0.0.1",
author="gadzan",
)
class TransformSharing(Plugin):

white_url_list = []
black_url_list = [
"https://support.weixin.qq.com", # 视频号视频
"https://channels-aladin.wxqcloud.qq.com", # 视频号音乐
"https://mp.weixin.qq.com/mp/waerrpage" # 小程序
]

def __init__(self):
super().__init__()
try:
self.config = super().load_config()
if not self.config:
self.config = self._load_config_template()
self.white_url_list = self.config.get("white_url_list", self.white_url_list)
self.black_url_list = self.config.get("black_url_list", self.black_url_list)
logger.info(f"[TransformSharing] inited, config={self.config}")
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
except Exception as e:
logger.error(f"[TransformSharing] 初始化异常:{e}")
raise "[TransformSharing] init failed, ignore "

def on_handle_context(self, e_context: EventContext):
try:
context = e_context["context"]
content = context.content
if context.type != ContextType.SHARING:
return
if not self._check_url(content):
logger.debug(f"[TransformSharing] {content} is not a valid url, skip")
e_context.action = EventAction.BREAK_PASS
return
logger.debug("[TransformSharing] on_handle_context. content: %s" % content)
reply = Reply(ReplyType.TEXT, "🧐正在阅读您的分享,请稍候...")
channel = e_context["channel"]
channel.send(reply, context)

target_url = html.unescape(content) # 解决公众号卡片链接校验问题,参考 https://github.com/fatwang2/sum4all/commit/b983c49473fc55f13ba2c44e4d8b226db3517c45
context.content = target_url
logger.debug(f"[TransformSharing] 转换分享为链接: {context.content}")
except Exception as e:
logger.exception(f"[TransformSharing] {str(e)}")
reply = Reply(ReplyType.ERROR, "我暂时无法处理分享内容,请稍后再试")
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS

def _load_config_template(self):
logger.debug("No plugin config.json, use plugins/transform_sharing/config.json.template")
try:
plugin_config_path = os.path.join(self.path, "config.json.template")
if os.path.exists(plugin_config_path):
with open(plugin_config_path, "r", encoding="utf-8") as f:
plugin_conf = json.load(f)
return plugin_conf
except Exception as e:
logger.exception(e)

def _check_url(self, target_url: str):
stripped_url = target_url.strip()
# 简单校验是否是url
if not stripped_url.startswith("http://") and not stripped_url.startswith("https://"):
return False

# 检查白名单
if len(self.white_url_list):
if not any(stripped_url.startswith(white_url) for white_url in self.white_url_list):
return False

# 排除黑名单,黑名单优先级>白名单
for black_url in self.black_url_list:
if stripped_url.startswith(black_url):
return False

return True