mirror of
https://github.com/zhayujie/bot-on-anything.git
synced 2026-03-01 16:39:09 +08:00
增加插件功能,新增模型选择插件和生成图片插件
This commit is contained in:
3
app.py
3
app.py
@@ -6,6 +6,8 @@ from channel import channel_factory
|
||||
from common import log, const
|
||||
from multiprocessing import Pool
|
||||
|
||||
from plugins.plugin_manager import PluginManager
|
||||
|
||||
|
||||
# 启动通道
|
||||
def start_process(channel_type, config_path):
|
||||
@@ -28,6 +30,7 @@ def main():
|
||||
model_type = config.conf().get("model").get("type")
|
||||
channel_type = config.conf().get("channel").get("type")
|
||||
|
||||
PluginManager()
|
||||
# 1.单个字符串格式配置时,直接启动
|
||||
if not isinstance(channel_type, list):
|
||||
start_process(channel_type, args.config)
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
from model import model_factory
|
||||
import config
|
||||
from plugins.event import Event, EventContext
|
||||
from plugins.plugin_manager import PluginManager
|
||||
|
||||
|
||||
class Bridge(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def fetch_reply_content(self, query, context):
|
||||
return model_factory.create_bot(config.conf().get("model").get("type")).reply(query, context)
|
||||
econtext = PluginManager().emit_event(EventContext(
|
||||
Event.ON_BRIDGE_HANDLE_CONTEXT, {'context': query, 'args': context}))
|
||||
type = econtext['args'].get('model') or config.conf().get("model").get("type")
|
||||
query = econtext.econtext.get("context", None)
|
||||
reply = econtext.econtext.get("reply", "无回复")
|
||||
if not econtext.is_pass() and query:
|
||||
return model_factory.create_bot(type).reply(query, context)
|
||||
else:
|
||||
return reply
|
||||
|
||||
async def fetch_reply_stream(self, query, context):
|
||||
bot=model_factory.create_bot(config.conf().get("model").get("type"))
|
||||
async for final,response in bot.reply_text_stream(query, context):
|
||||
yield final,response
|
||||
econtext = PluginManager().emit_event(EventContext(
|
||||
Event.ON_BRIDGE_HANDLE_CONTEXT, {'context': query, 'args': context}))
|
||||
type = econtext['args'].get('model') or config.conf().get("model").get("type")
|
||||
query = econtext.get("context", None)
|
||||
reply = econtext.get("reply", "无回复")
|
||||
bot = model_factory.create_bot(type)
|
||||
if not econtext.is_pass() and query:
|
||||
async for final, response in bot.reply_text_stream(query, context):
|
||||
yield final, response
|
||||
else:
|
||||
yield True, reply
|
||||
|
||||
@@ -12,6 +12,7 @@ from config import channel_conf_val
|
||||
from channel.channel import Channel
|
||||
from flask_socketio import SocketIO
|
||||
from common import log
|
||||
from plugins.plugin_manager import *
|
||||
|
||||
http_app = Flask(__name__,)
|
||||
socketio = SocketIO(http_app, close_timeout=5)
|
||||
@@ -26,13 +27,13 @@ http_app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1)
|
||||
async def return_stream(data):
|
||||
async for final, response in HttpChannel().handle_stream(data=data):
|
||||
try:
|
||||
if(final):
|
||||
if (final):
|
||||
socketio.server.emit(
|
||||
'disconnect', {'result': response, 'final': final}, request.sid, namespace="/chat")
|
||||
'disconnect', {'result': response, 'final': final}, request.sid, namespace="/chat")
|
||||
disconnect()
|
||||
else:
|
||||
socketio.server.emit(
|
||||
'message', {'result': response, 'final': final}, request.sid, namespace="/chat")
|
||||
'message', {'result': response, 'final': final}, request.sid, namespace="/chat")
|
||||
except Exception as e:
|
||||
disconnect()
|
||||
log.warn("[http]emit:{}", e)
|
||||
@@ -51,7 +52,8 @@ def stream(data):
|
||||
data["msg"], channel_conf_val(const.HTTP, 'image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
reply_text = HttpChannel().handle(data=data)
|
||||
socketio.emit('disconnect', {'result': reply_text}, namespace='/chat')
|
||||
socketio.emit(
|
||||
'disconnect', {'result': reply_text}, namespace='/chat')
|
||||
disconnect()
|
||||
return
|
||||
asyncio.run(return_stream(data))
|
||||
@@ -66,7 +68,7 @@ def connect():
|
||||
@socketio.on('disconnect', namespace='/chat')
|
||||
def disconnect():
|
||||
log.info('disconnect')
|
||||
socketio.server.disconnect(request.sid,namespace="/chat")
|
||||
socketio.server.disconnect(request.sid, namespace="/chat")
|
||||
|
||||
|
||||
@http_app.route("/chat", methods=['POST'])
|
||||
@@ -114,26 +116,30 @@ class HttpChannel(Channel):
|
||||
|
||||
def handle(self, data):
|
||||
context = dict()
|
||||
img_match_prefix = functions.check_prefix(
|
||||
data["msg"], channel_conf_val(const.HTTP, 'image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
data["msg"] = data["msg"].split(img_match_prefix, 1)[1].strip()
|
||||
context['type'] = 'IMAGE_CREATE'
|
||||
query = data["msg"]
|
||||
id = data["id"]
|
||||
context['from_user_id'] = str(id)
|
||||
reply = super().build_reply_content(data["msg"], context)
|
||||
if img_match_prefix:
|
||||
if not isinstance(reply, list):
|
||||
return reply
|
||||
images = ""
|
||||
for url in reply:
|
||||
images += f"[]({url})\n"
|
||||
reply = images
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {
|
||||
'channel': self, 'context': query, "args": context}))
|
||||
reply = e_context['reply']
|
||||
if not e_context.is_pass():
|
||||
reply = super().build_reply_content(e_context["context"], e_context["args"])
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {
|
||||
'channel': self, 'context': context, 'reply': reply, "args": context}))
|
||||
reply = e_context['reply']
|
||||
return reply
|
||||
|
||||
async def handle_stream(self, data):
|
||||
context = dict()
|
||||
id = data["id"]
|
||||
context['from_user_id'] = str(id)
|
||||
async for final, reply in super().build_reply_stream(data["msg"], context):
|
||||
yield final, reply
|
||||
context['stream'] = True
|
||||
context['origin'] = data["msg"]
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {
|
||||
'channel': self, 'context': data["msg"], 'reply': data["msg"], "args": context}))
|
||||
reply = e_context['reply']
|
||||
if not e_context.is_pass():
|
||||
async for final, reply in super().build_reply_stream(data["msg"], context):
|
||||
yield final, reply
|
||||
else:
|
||||
yield True, reply
|
||||
|
||||
@@ -14,7 +14,7 @@ from common.log import logger
|
||||
from common import const
|
||||
from config import channel_conf_val
|
||||
import requests
|
||||
|
||||
from plugins.plugin_manager import *
|
||||
from common.sensitive_word import SensitiveWord
|
||||
|
||||
import io
|
||||
@@ -81,25 +81,14 @@ class WechatChannel(Channel):
|
||||
str_list = content.split(match_prefix, 1)
|
||||
if len(str_list) == 2:
|
||||
content = str_list[1].strip()
|
||||
|
||||
img_match_prefix = self.check_prefix(content, channel_conf_val(const.WECHAT, 'image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
thread_pool.submit(self._do_send_img, content, from_user_id)
|
||||
else:
|
||||
thread_pool.submit(self._do_send, content, from_user_id)
|
||||
thread_pool.submit(self._do_send, content, from_user_id)
|
||||
|
||||
elif to_user_id == other_user_id and match_prefix:
|
||||
# 自己给好友发送消息
|
||||
str_list = content.split(match_prefix, 1)
|
||||
if len(str_list) == 2:
|
||||
content = str_list[1].strip()
|
||||
img_match_prefix = self.check_prefix(content, channel_conf_val(const.WECHAT, 'image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
thread_pool.submit(self._do_send_img, content, to_user_id)
|
||||
else:
|
||||
thread_pool.submit(self._do_send, content, to_user_id)
|
||||
thread_pool.submit(self._do_send, content, to_user_id)
|
||||
|
||||
|
||||
def handle_group(self, msg):
|
||||
@@ -137,13 +126,7 @@ class WechatChannel(Channel):
|
||||
group_white_list = channel_conf_val(const.WECHAT, 'group_name_white_list')
|
||||
|
||||
if ('ALL_GROUP' in group_white_list or group_name in group_white_list or self.check_contain(group_name, channel_conf_val(const.WECHAT, 'group_name_keyword_white_list'))) and match_prefix:
|
||||
|
||||
img_match_prefix = self.check_prefix(content, channel_conf_val(const.WECHAT, 'image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
content = content.split(img_match_prefix, 1)[1].strip()
|
||||
thread_pool.submit(self._do_send_img, content, group_id)
|
||||
else:
|
||||
thread_pool.submit(self._do_send_group, content, msg)
|
||||
thread_pool.submit(self._do_send_group, content, msg)
|
||||
return None
|
||||
|
||||
def send(self, msg, receiver):
|
||||
@@ -156,18 +139,25 @@ class WechatChannel(Channel):
|
||||
return
|
||||
context = dict()
|
||||
context['from_user_id'] = reply_user_id
|
||||
reply_text = super().build_reply_content(query, context)
|
||||
if reply_text:
|
||||
self.send(channel_conf_val(const.WECHAT, "single_chat_reply_prefix") + reply_text, reply_user_id)
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {
|
||||
'channel': self, 'context': query, "args": context}))
|
||||
|
||||
reply = e_context['reply']
|
||||
if not e_context.is_pass():
|
||||
reply = super().build_reply_content(e_context["context"], e_context["args"])
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {
|
||||
'channel': self, 'context': context, 'reply': reply, "args": e_context["args"]}))
|
||||
reply = e_context['reply']
|
||||
if reply:
|
||||
self.send(channel_conf_val(const.WECHAT, "single_chat_reply_prefix") + reply, reply_user_id)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
def _do_send_img(self, query, reply_user_id):
|
||||
def _do_send_img(self, query, context):
|
||||
try:
|
||||
if not query:
|
||||
return
|
||||
context = dict()
|
||||
context['type'] = 'IMAGE_CREATE'
|
||||
reply_user_id=context['from_user_id']
|
||||
img_urls = super().build_reply_content(query, context)
|
||||
if not img_urls:
|
||||
return
|
||||
@@ -192,12 +182,19 @@ class WechatChannel(Channel):
|
||||
if not query:
|
||||
return
|
||||
context = dict()
|
||||
context['from_user_id'] = msg['ActualUserName']
|
||||
reply_text = super().build_reply_content(query, context)
|
||||
if reply_text:
|
||||
reply_text = '@' + msg['ActualNickName'] + ' ' + reply_text.strip()
|
||||
self.send(channel_conf_val(const.WECHAT, "group_chat_reply_prefix", "") + reply_text, msg['User']['UserName'])
|
||||
|
||||
context['from_user_id'] = msg['User']['UserName']
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_HANDLE_CONTEXT, {
|
||||
'channel': self, 'context': query, "args": context}))
|
||||
reply = e_context['reply']
|
||||
if not e_context.is_pass():
|
||||
context['from_user_id'] = msg['ActualUserName']
|
||||
reply = super().build_reply_content(e_context["context"], e_context["args"])
|
||||
e_context = PluginManager().emit_event(EventContext(Event.ON_DECORATE_REPLY, {
|
||||
'channel': self, 'context': context, 'reply': reply, "args": e_context["args"]}))
|
||||
reply = e_context['reply']
|
||||
if reply:
|
||||
reply = '@' + msg['ActualNickName'] + ' ' + reply.strip()
|
||||
self.send(channel_conf_val(const.WECHAT, "group_chat_reply_prefix", "") + reply, msg['User']['UserName'])
|
||||
|
||||
def check_prefix(self, content, prefix_list):
|
||||
for prefix in prefix_list:
|
||||
|
||||
@@ -1,4 +1,31 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from common import log
|
||||
|
||||
def singleton(cls):
|
||||
instances = {}
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
if cls not in instances:
|
||||
instances[cls] = cls(*args, **kwargs)
|
||||
return instances[cls]
|
||||
|
||||
return get_instance
|
||||
|
||||
def load_json_file(curdir: str, file: str = 'config.json'):
|
||||
config_path = os.path.join(curdir, file)
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config
|
||||
except Exception as e:
|
||||
if isinstance(e, FileNotFoundError):
|
||||
log.warn(
|
||||
f"[common]load json file failed, {config_path}\{file} not found")
|
||||
else:
|
||||
log.warn("[common]load json file failed")
|
||||
raise e
|
||||
|
||||
|
||||
def contain_chinese(str):
|
||||
@@ -11,7 +38,9 @@ def contain_chinese(str):
|
||||
|
||||
|
||||
def check_prefix(content, prefix_list):
|
||||
if(len(prefix_list)==0):
|
||||
return True
|
||||
for prefix in prefix_list:
|
||||
if content.startswith(prefix):
|
||||
return prefix
|
||||
return None
|
||||
return False
|
||||
|
||||
122
plugins/README.md
Normal file
122
plugins/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# 简介
|
||||
|
||||
按 **[chatgpt-on-wechat](https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins)** 插件的思路对 **bot-on-anything** 进行插件化,期望能实现插件的共享使用,但是由于两个项目的架构存在较大差异,只能尽最大可能兼容 **[chatgpt-on-wechat](https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins)** 的插件,部分功能还需进行适配。
|
||||
|
||||
|
||||
|
||||
## **插件监听的事件:**
|
||||
|
||||
事件顺序为 1、ON_HANDLE_CONTEXT --> 2、ON_BRIDGE_HANDLE_CONTEXT(ON_BRIDGE_HANDLE_STREAM_CONTEXT) --> 3、ON_DECORATE_REPLY
|
||||
|
||||
|
||||
触发事件会产生事件的上下文EventContext,它可能包含了以下信息:
|
||||
|
||||
EventContext(Event事件类型, {'channel' : 本次消息的context, 'context': 本次消息用户的提问, 'reply': 当前AI回复, "args": 其他上下文参数})
|
||||
|
||||
插件处理函数可通过修改EventContext中的context、reply、args或者调用channel中对应的方法来实现功能。
|
||||
|
||||
|
||||
```
|
||||
class Event(Enum):
|
||||
|
||||
ON_HANDLE_CONTEXT = 2 # 对应通道处理消息前
|
||||
"""
|
||||
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 }
|
||||
"""
|
||||
|
||||
ON_DECORATE_REPLY = 3 # 得到回复后准备装饰
|
||||
"""
|
||||
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复 , "args": 其他上下文参数 }
|
||||
"""
|
||||
|
||||
ON_SEND_REPLY = 4 # 发送回复前
|
||||
"""
|
||||
bot-on-anything 不支持ON_SEND_REPLY事件,请使用ON_BRIDGE_HANDLE_CONTEXT或者ON_BRIDGE_HANDLE_STREAM_CONTEXT事件
|
||||
"""
|
||||
|
||||
ON_BRIDGE_HANDLE_CONTEXT = 6 # 模型桥处理消息前
|
||||
"""
|
||||
e_context = { "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 , 模型桥会调用args.model指定的AI模型来进行回复 }
|
||||
"""
|
||||
|
||||
ON_BRIDGE_HANDLE_STREAM_CONTEXT = 7 # 模型桥处理流式消息前,流式对话的消息处理仅支持一次性返回,请直接返回结果
|
||||
"""
|
||||
e_context = { "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 , 模型桥会调用args.model指定的AI模型来进行回复 }
|
||||
"""
|
||||
|
||||
```
|
||||
|
||||
## 插件编写示例
|
||||
|
||||
以`plugins/selector`为例,其中编写了一个通过判断前缀调用不同模型的`Selector`插件。
|
||||
|
||||
### 1. 创建插件
|
||||
|
||||
在`plugins`目录下创建一个插件文件夹`selector`。然后,在该文件夹中创建同名``selector.py``文件。
|
||||
|
||||
```
|
||||
plugins/
|
||||
└── selector
|
||||
└── selector.py
|
||||
```
|
||||
|
||||
### 2. 编写功能
|
||||
|
||||
在`selector.py`文件中,创建插件类`Selector`,它继承自`Plugin`。
|
||||
|
||||
在类定义之前需要使用`@plugins.register`装饰器注册插件,并填写插件的相关信息,其中`desire_priority`表示插件默认的优先级,越大优先级越高。`Selector`插件加载时读取了同目录下的`selector.json`文件,从中取出对应的模型和触发前缀,`Selector`插件为事件`ON_HANDLE_CONTEXT`和`ON_BRIDGE_HANDLE_STREAM_CONTEXT`绑定了一个处理函数`select_model`,它表示在模型桥调用指定模型之前,都会先由`select_model`函数预处理。
|
||||
|
||||
```python
|
||||
@plugins.register(name="Selector", desire_priority=99, hidden=True, desc="A model selector", version="0.1", author="RegimenArsenic")
|
||||
class Selector(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
curdir = os.path.dirname(__file__)
|
||||
try:
|
||||
self.config = functions.load_json_file(curdir, "selector.json")
|
||||
except Exception as e:
|
||||
log.warn("[Selector] init failed")
|
||||
raise e
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.select_model
|
||||
self.handlers[Event.ON_BRIDGE_HANDLE_STREAM_CONTEXT] = self.select_model
|
||||
log.info("[Selector] inited")
|
||||
```
|
||||
|
||||
### 3. 编写事件处理函数
|
||||
|
||||
#### 修改事件上下文
|
||||
|
||||
事件处理函数接收一个`EventContext`对象`e_context`作为参数。`e_context`包含了事件相关信息,利用`e_context['key']`来访问这些信息。
|
||||
|
||||
`EventContext(Event事件类型, {'channel' : 消息channel, 'context': Context, 'reply': Reply , "args": 其他上下文参数})`
|
||||
|
||||
处理函数中通过修改`e_context`对象中的事件相关信息来实现所需功能,比如更改`e_context['reply']`中的内容可以修改回复,更改`e_context['context']`中的内容可以修改用户提问。
|
||||
|
||||
#### 决定是否交付给下个插件或默认逻辑
|
||||
|
||||
在处理函数结束时,还需要设置`e_context`对象的`action`属性,它决定如何继续处理事件。目前有以下三种处理方式:
|
||||
|
||||
- `EventAction.CONTINUE`: 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑。
|
||||
- `EventAction.BREAK`: 事件结束,不再给下个插件处理,交付给默认的处理逻辑。
|
||||
- `EventAction.BREAK_PASS`: 事件结束,不再给下个插件处理,跳过默认的处理逻辑。
|
||||
|
||||
#### 示例处理函数
|
||||
|
||||
`Selector`插件通过判断前缀,如有`@bing`前缀,则修改调用模型为bing模型,若前缀为`@gpt`,则修改调用模型为chatgpt,否则就使用app里配置的原始模型插件,同时删去提问的前缀`@bing`或者`@gpt`
|
||||
|
||||
```python
|
||||
def select_model(self, e_context: EventContext):
|
||||
model=e_context['args'].get('model')
|
||||
for selector in self.config.get("selector", []):
|
||||
prefix = selector.get('prefix', [])
|
||||
check_prefix=functions.check_prefix(e_context["context"], prefix)
|
||||
if (check_prefix):
|
||||
model=selector.get('model')
|
||||
if isinstance(check_prefix, str):
|
||||
e_context["context"] = e_context["context"].split(check_prefix, 1)[1].strip()
|
||||
break
|
||||
log.debug(f"[Selector] select model {model}")
|
||||
e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑
|
||||
e_context['args']['model']=model
|
||||
return e_context
|
||||
```
|
||||
10
plugins/__init__.py
Normal file
10
plugins/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# encoding:utf-8
|
||||
from .event import *
|
||||
from .plugin import *
|
||||
from plugins.plugin_registry import PluginRegistry
|
||||
|
||||
instance = PluginRegistry()
|
||||
|
||||
register = instance.register
|
||||
# load_plugins = instance.load_plugins
|
||||
# emit_event = instance.emit_event
|
||||
66
plugins/createimg/createimg.py
Normal file
66
plugins/createimg/createimg.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# encoding:utf-8
|
||||
|
||||
from channel.http.http_channel import HttpChannel
|
||||
from channel.wechat.wechat_channel import WechatChannel
|
||||
import plugins
|
||||
from plugins import *
|
||||
from common import functions
|
||||
from config import channel_conf
|
||||
from config import channel_conf_val
|
||||
from common import const
|
||||
|
||||
|
||||
@plugins.register(name="CreateImg", desire_priority=90, hidden=True, desc="A simple plugin that create images from model", version="0.1", author="RegimenArseic")
|
||||
class Createimg(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.handles = {HttpChannel: self.handle_http}
|
||||
self.channel_types = {HttpChannel: const.HTTP,
|
||||
WechatChannel: const.WECHAT}
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.handle_query
|
||||
self.handlers[Event.ON_DECORATE_REPLY] = self.send_images
|
||||
|
||||
def get_events(self):
|
||||
return self.handlers
|
||||
|
||||
def handle_query(self, e_context: EventContext):
|
||||
channel = e_context['channel']
|
||||
channel_type = self.channel_types.get(type(channel), None)
|
||||
if (channel_type):
|
||||
query = e_context['context']
|
||||
if (query):
|
||||
img_match_prefix = functions.check_prefix(
|
||||
query, channel_conf_val(channel_type, 'image_create_prefix'))
|
||||
if img_match_prefix:
|
||||
if (channel_type == const.HTTP) and e_context['args'].get('stream', False):
|
||||
e_context['reply'] = channel.handle(
|
||||
{'msg': e_context['args']['origin'], 'id': e_context['args']['from_user_id']})
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
else:
|
||||
query = query.split(img_match_prefix, 1)[1].strip()
|
||||
e_context['args']['type'] = 'IMAGE_CREATE'
|
||||
if (channel_type == const.WECHAT):
|
||||
channel._do_send_img(
|
||||
query, e_context['args'])
|
||||
e_context.action = EventAction.BREAK_PASS
|
||||
else:
|
||||
e_context.action = EventAction.CONTINUE
|
||||
return e_context
|
||||
|
||||
def handle_http(self, e_context: EventContext):
|
||||
reply = e_context["reply"]
|
||||
if e_context['args'].get('type', '') == 'IMAGE_CREATE':
|
||||
if isinstance(reply, list):
|
||||
images = ""
|
||||
for url in reply:
|
||||
images += f"[]({url})\n\n"
|
||||
e_context["reply"] = images
|
||||
return e_context
|
||||
|
||||
def send_images(self, e_context: EventContext):
|
||||
channel = e_context['channel']
|
||||
method = self.handles.get(type(channel), None)
|
||||
if (method):
|
||||
e_context = method(e_context)
|
||||
e_context.action = EventAction.BREAK_PASS # 事件结束,不再给下个插件处理,不交付给默认的事件处理逻辑
|
||||
return e_context
|
||||
59
plugins/event.py
Normal file
59
plugins/event.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# encoding:utf-8
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Event(Enum):
|
||||
# ON_RECEIVE_MESSAGE = 1 # 收到消息
|
||||
|
||||
ON_HANDLE_CONTEXT = 2 # 对应通道处理消息前
|
||||
"""
|
||||
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 }
|
||||
"""
|
||||
|
||||
ON_DECORATE_REPLY = 3 # 得到回复后准备装饰
|
||||
"""
|
||||
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复 , "args": 其他上下文参数 }
|
||||
"""
|
||||
|
||||
ON_SEND_REPLY = 4 # 发送回复前
|
||||
"""
|
||||
bot-on-anything 不支持ON_SEND_REPLY事件,请使用ON_BRIDGE_HANDLE_CONTEXT或者ON_BRIDGE_HANDLE_STREAM_CONTEXT事件
|
||||
"""
|
||||
|
||||
# AFTER_SEND_REPLY = 5 # 发送回复后
|
||||
|
||||
ON_BRIDGE_HANDLE_CONTEXT = 6 # 模型桥处理消息前
|
||||
"""
|
||||
e_context = { "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 }
|
||||
"""
|
||||
|
||||
ON_BRIDGE_HANDLE_STREAM_CONTEXT = 7 # 模型桥处理流式消息前,流式对话的消息处理仅支持一次性返回,请直接返回结果
|
||||
"""
|
||||
e_context = { "context" : 本次消息的context, "reply" : 目前的回复,初始为空 , "args": 其他上下文参数 }
|
||||
"""
|
||||
|
||||
|
||||
class EventAction(Enum):
|
||||
CONTINUE = 1 # 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑
|
||||
BREAK = 2 # 事件结束,不再给下个插件处理,交付给默认的事件处理逻辑
|
||||
BREAK_PASS = 3 # 事件结束,不再给下个插件处理,不交付给默认的事件处理逻辑
|
||||
|
||||
|
||||
class EventContext:
|
||||
def __init__(self, event, econtext=dict()):
|
||||
self.event = event
|
||||
self.econtext = econtext
|
||||
self.action = EventAction.CONTINUE
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.econtext.get(key,"")
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.econtext[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.econtext[key]
|
||||
|
||||
def is_pass(self):
|
||||
return self.action == EventAction.BREAK_PASS
|
||||
7
plugins/plugin.py
Normal file
7
plugins/plugin.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# encoding:utf-8
|
||||
class Plugin:
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
|
||||
def get_help_text(self, **kwargs):
|
||||
return "暂无帮助信息"
|
||||
41
plugins/plugin_manager.py
Normal file
41
plugins/plugin_manager.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# encoding:utf-8
|
||||
import os
|
||||
import importlib.util
|
||||
from plugins.event import EventAction, EventContext,Event
|
||||
from plugins.plugin_registry import PluginRegistry
|
||||
from common import functions
|
||||
|
||||
@functions.singleton
|
||||
class PluginManager:
|
||||
def __init__(self, plugins_dir="./plugins/"):
|
||||
self.plugins_dir = plugins_dir
|
||||
self.plugin_registry = PluginRegistry()
|
||||
self.load_plugins()
|
||||
|
||||
def load_plugins(self):
|
||||
for plugin_name in self.find_plugin_names():
|
||||
if os.path.exists(f"./plugins/{plugin_name}/{plugin_name}.py"):
|
||||
plugin_module = self.load_plugin_module(plugin_name)
|
||||
self.plugin_registry.register_from_module(plugin_module)
|
||||
|
||||
def find_plugin_names(self):
|
||||
plugin_names = []
|
||||
for entry in os.scandir(self.plugins_dir):
|
||||
if entry.is_dir():
|
||||
plugin_names.append(entry.name)
|
||||
return plugin_names
|
||||
|
||||
def load_plugin_module(self, plugin_name):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
plugin_name, os.path.join(self.plugins_dir, plugin_name, f"{plugin_name}.py")
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
def emit_event(self, e_context: EventContext, *args, **kwargs):
|
||||
for plugin in self.plugin_registry.list_plugins():
|
||||
if plugin.enabled and e_context.action == EventAction.CONTINUE:
|
||||
if(e_context.event in plugin.handlers):
|
||||
plugin.handlers[e_context.event](e_context, *args, **kwargs)
|
||||
return e_context
|
||||
51
plugins/plugin_registry.py
Normal file
51
plugins/plugin_registry.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# encoding:utf-8
|
||||
|
||||
import inspect
|
||||
from plugins.plugin import Plugin
|
||||
from common.log import logger
|
||||
from common import functions
|
||||
|
||||
@functions.singleton
|
||||
class PluginRegistry:
|
||||
def __init__(self):
|
||||
self.plugins = []
|
||||
|
||||
def register(self, name: str, desire_priority: int = 0, **kwargs):
|
||||
def wrapper(plugin_cls):
|
||||
plugin_cls.name = name
|
||||
plugin_cls.priority = desire_priority
|
||||
plugin_cls.desc = kwargs.get('desc')
|
||||
plugin_cls.author = kwargs.get('author')
|
||||
plugin_cls.version = kwargs.get('version') or "1.0"
|
||||
plugin_cls.namecn = kwargs.get('namecn') or name
|
||||
plugin_cls.hidden = kwargs.get('hidden') or False
|
||||
plugin_cls.enabled = kwargs.get('enabled') or True
|
||||
logger.info(f"Plugin {name}_v{plugin_cls.version} registered")
|
||||
return plugin_cls
|
||||
return wrapper
|
||||
|
||||
def register_from_module(self, module):
|
||||
plugins = []
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj) and issubclass(obj, Plugin) and obj != Plugin:
|
||||
plugin_name = getattr(obj, "name", None)
|
||||
if plugin_name:
|
||||
plugin = obj()
|
||||
plugin.name = plugin_name
|
||||
plugin.priority = getattr(obj, "priority", 0)
|
||||
plugin.desc = getattr(obj, "desc", None)
|
||||
plugin.author = getattr(obj, "author", None)
|
||||
plugin.version = getattr(obj, "version", "1.0")
|
||||
plugin.namecn = getattr(obj, "namecn", plugin_name)
|
||||
plugin.hidden = getattr(obj, "hidden", False)
|
||||
plugin.enabled = getattr(obj, "enabled", True)
|
||||
# Sort the list of plugins by priority
|
||||
self.plugins.append(plugin)
|
||||
self.plugins.sort(key=lambda x: x.priority, reverse=True)
|
||||
|
||||
def get_plugin(self, name):
|
||||
plugin = next((p for p in self.plugins if p.name.upper() == name.upper()), None)
|
||||
return plugin
|
||||
|
||||
def list_plugins(self):
|
||||
return [plugin for plugin in self.plugins]
|
||||
12
plugins/selector/selector.json
Normal file
12
plugins/selector/selector.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"selector":[
|
||||
{
|
||||
"model":"bing",
|
||||
"prefix":["@bing"]
|
||||
},
|
||||
{
|
||||
"model":"chatgpt",
|
||||
"prefix":["@gpt"]
|
||||
}
|
||||
]
|
||||
}
|
||||
40
plugins/selector/selector.py
Normal file
40
plugins/selector/selector.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# encoding:utf-8
|
||||
|
||||
import os
|
||||
import plugins
|
||||
from plugins import *
|
||||
from common import log
|
||||
from common import functions
|
||||
|
||||
|
||||
@plugins.register(name="Selector", desire_priority=99, hidden=True, desc="A model selector", version="0.1", author="RegimenArsenic")
|
||||
class Selector(Plugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
curdir = os.path.dirname(__file__)
|
||||
try:
|
||||
self.config = functions.load_json_file(curdir, "selector.json")
|
||||
except Exception as e:
|
||||
log.warn("[Selector] init failed")
|
||||
raise e
|
||||
self.handlers[Event.ON_HANDLE_CONTEXT] = self.select_model
|
||||
self.handlers[Event.ON_BRIDGE_HANDLE_STREAM_CONTEXT] = self.select_model
|
||||
log.info("[Selector] inited")
|
||||
|
||||
def get_events(self):
|
||||
return self.handlers
|
||||
|
||||
def select_model(self, e_context: EventContext):
|
||||
model=e_context['args'].get('model')
|
||||
for selector in self.config.get("selector", []):
|
||||
prefix = selector.get('prefix', [])
|
||||
check_prefix=functions.check_prefix(e_context["context"], prefix)
|
||||
if (check_prefix):
|
||||
model=selector.get('model')
|
||||
if isinstance(check_prefix, str):
|
||||
e_context["context"] = e_context["context"].split(check_prefix, 1)[1].strip()
|
||||
break
|
||||
log.debug(f"[Selector] select model {model}")
|
||||
e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑
|
||||
e_context['args']['model']=model
|
||||
return e_context
|
||||
Reference in New Issue
Block a user