diff --git a/channel/wechat/wechat_channel.py b/channel/wechat/wechat_channel.py index bc5faf9..58b6f8d 100644 --- a/channel/wechat/wechat_channel.py +++ b/channel/wechat/wechat_channel.py @@ -176,7 +176,7 @@ class WechatChannel(Channel): image_storage.write(block) image_storage.seek(0) itchat.send_image(image_storage, toUserName=receiver) - logger.info('[WX] sendImage url=, receiver={}'.format(img_url,receiver)) + logger.info('[WX] sendImage url={}, receiver={}'.format(img_url,receiver)) elif reply.type == ReplyType.IMAGE: # 从文件读取图片 image_storage = reply.content image_storage.seek(0) diff --git a/plugins/README.md b/plugins/README.md index 62b10b6..c81f94d 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1,19 +1,37 @@ -# 插件说明 -本项目主体是调用ChatGPT接口的Wechat自动回复机器人。之前未插件化的代码耦合程度高,很难定制一些个性化功能(如流量控制、接入本地的NovelAI画图平台等),多个功能的优先级顺序也难以调度。 -**插件化**: 在保证主体功能是ChatGPT的前提下,推荐将主体功能外的功能分离成不同的插件。有个性化需求的用户仅需按照插件提供的接口编写插件,无需了解程序主体的代码结构,同时也方便代码的测试和调试。(插件调用目前仅支持 itchat) +## 插件化初衷 +之前未插件化的代码耦合程度高,如果要定制一些个性化功能(如流量控制、接入`NovelAI`画图平台等),需要了解代码主体,避免影响到其他的功能。在实现多个功能后,不但无法调整功能的优先级顺序,功能的配置项也会变得非常混乱。 -## 插件触发时机 +此时插件化应声而出。 + +**插件化**: 在保证主体功能是ChatGPT的前提下,我们推荐将主体功能外的功能利用插件的方式实现。 + +- [x] 可根据功能需要,下载不同插件。 +- [x] 插件开发成本低,仅需了解插件触发事件,并按照插件定义接口编写插件。 +- [x] 插件化能够自由开关和调整优先级。 +- [x] 每个插件可在插件文件夹内维护独立的配置文件,方便代码的测试和调试,可以在独立的仓库开发插件。 + +PS: 插件目前仅支持`itchat` + +## 插件化实现 + +插件化实现是在收到消息到发送回复的各个步骤之间插入触发事件实现的。 ### 消息处理过程 -了解插件触发时机前,首先需要了解程序收到消息后的执行过程。插件化版本的消息处理过程如下: + +在了解插件触发事件前,首先需要了解程序收到消息到发送回复的整个过程。 + +插件化版本中,消息处理过程可以分为4个步骤: ``` 1.收到消息 ---> 2.产生回复 ---> 3.包装回复 ---> 4.发送回复 ``` + 以下是它们的默认处理逻辑(太长不看,可跳过): -- 1. 收到消息 - 本过程接收到用户消息,根据用户设置,判断本条消息是否触发。若触发,则判断该消息的命令类型,如声音、聊天、画图等。之后,将消息包装成如下的 Context 交付给下一个步骤。 - ```python +#### 1. 收到消息 + +负责接收用户消息,根据用户的配置,判断本条消息是否触发机器人。如果触发,则会判断该消息的类型(声音、文本、画图命令等),将消息包装成如下的`Context`交付给下一个步骤。 + +```python class ContextType (Enum): TEXT = 1 # 文本消息 VOICE = 2 # 音频消息 @@ -25,14 +43,26 @@ self.kwargs = kwargs def __getitem__(self, key): return self.kwargs[key] - ``` - `Context`中除了存放消息类型和内容外,还存放了与会话相关的参数。一个例子是,当收到用户私聊消息时,还会存放以下的会话参数,`isgroup`标识`Context`是否时群聊消息,`msg`是`itchat`中原始的消息对象,`receiver`是应回复消息的对象ID,`session_id`是会话ID(一般是触发bot的消息发送方,群聊中如果`conf`里设置了`group_chat_in_one_session`,那么此处便是群聊的ID) - ``` - context.kwargs = {'isgroup': False, 'msg': msg, 'receiver': other_user_id, 'session_id': other_user_id} - ``` -2. 产生回复 - 本过程用于处理消息。目前默认处理逻辑如下,它根据`Context`的类型交付给对应的bot。如果本过程未产生任何回复,则会跳过之后的处理阶段。 - ```python +``` + +`Context`中除了存放消息类型和内容外,还存放了一些与会话相关的参数。 + +例如,当收到用户私聊消息时,会存放以下的会话参数。 + +```python + context.kwargs = {'isgroup': False, 'msg': msg, 'receiver': other_user_id, 'session_id': other_user_id} +``` + +- `isgroup`: `Context`是否是群聊消息。 +- `msg`: `itchat`中原始的消息对象。 +- `receiver`: 需要回复消息的对象ID。 +- `session_id`: 会话ID(一般是发送触发bot消息的用户ID,如果在群聊中并且`conf`里设置了`group_chat_in_one_session`,那么此处便是群聊ID) + +#### 2. 产生回复 + +处理消息并产生回复。目前默认处理逻辑是根据`Context`的类型交付给对应的bot,并产生回复`Reply`。 如果本步骤没有产生任何回复,那么会跳过之后的所有步骤。 + +```python if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE: reply = super().build_reply_content(context.content, context) #文字跟画图交付给chatgpt elif context.type == ContextType.VOICE: # 声音先进行语音转文字后,修改Context类型为文字后,再交付给chatgpt @@ -47,9 +77,11 @@ if reply.type == ReplyType.TEXT: if conf().get('voice_reply_voice'): reply = super().build_text_to_voice(reply.content) - ``` - Bot可产生的回复如下所示,它允许Bot可以回复多类不同的消息,未来可能不止能返回文字,而是能根据文字回复音频/图片,这时候便能派上用场。同时也加入了`INFO`和`ERROR`消息类型区分系统提示和系统错误。 - ```python +``` + +回复`Reply`的定义如下所示,它允许Bot可以回复多类不同的消息。同时也加入了`INFO`和`ERROR`消息类型区分系统提示和系统错误。 + +```python class ReplyType(Enum): TEXT = 1 # 文本 VOICE = 2 # 音频文件 @@ -62,11 +94,19 @@ def __init__(self, type : ReplyType = None , content = None): self.type = type self.content = content - ``` -3. 装饰回复 - 本过程根据`Context`和回复的类型,对回复的内容进行装饰。目前的装饰有以下两种,如果是文本回复,会根据是否在群聊中来决定是否艾特收方或添加回复前缀。 - 如果是`INFO`或`ERROR`类型,会在消息前添加对应字样。 - ```python +``` + +#### 3. 装饰回复 + +根据`Context`和回复`Reply`的类型,对回复的内容进行装饰。目前的装饰有以下两种: + +- `TEXT`文本回复,根据是否在群聊中来决定是艾特接收方还是添加回复的前缀。 + +- `INFO`或`ERROR`类型,会在消息前添加对应的系统提示字样。 + +如下是默认逻辑的代码: + +```python if reply.type == ReplyType.TEXT: reply_text = reply.content if context['isgroup']: @@ -77,36 +117,40 @@ reply.content = reply_text elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO: reply.content = str(reply.type)+":\n" + reply.content - ``` -4. 发送回复 - 本过程根据回复的类型来发送回复给接收方`context["receiver"]`。 +``` + +#### 4. 发送回复 + +根据`Reply`的类型,默认逻辑调用不同的发送函数发送回复给接收方`context["receiver"]`。 ### 插件触发事件 -主程序会在各消息处理过程之间触发插件事件,插件可以监听相应事件编写相应的处理逻辑。 -``` - 1.收到消息 ---> 2.产生回复 ---> 3.包装回复 ---> 4.发送回复 -``` -目前加入了三类事件的触发: +主程序目前会在各个消息步骤间触发事件,监听相应事件的插件会按照优先级,顺序调用事件处理函数。 + +目前支持三类触发事件: ``` 1.收到消息 ---> `ON_HANDLE_CONTEXT` 2.产生回复 ---> `ON_DECORATE_REPLY` -3.包装回复 +3.装饰回复 ---> `ON_SEND_REPLY` 4.发送回复 ``` -触发事件会产生事件上下文`EventContext`,它包含了以下信息: -`EventContext(Event事件类型, {'channel' : 消息channel, 'context': context, 'reply': reply})` -插件的处理函数可以修改`Context`和`Reply`的内容来定制化处理逻辑。 +触发事件会产生事件的上下文`EventContext`,它包含了以下信息: + +`EventContext(Event事件类型, {'channel' : 消息channel, 'context': Context, 'reply': Reply})` + +插件处理函数可通过修改`EventContext`中的`context`和`reply`来实现功能。 + +## 插件编写示例 -## 插件编写 -以`plugins/hello`为例,它编写了一个简单`Hello`插件。 +以`plugins/hello`为例,其中编写了一个简单的`Hello`插件。 -1. 创建插件 -在`plugins`目录下创建一个插件文件夹,例如`hello`。然后,在该文件夹中创建一个与文件夹同名的`.py`文件,例如`hello.py`。 +### 1. 创建插件 + +在`plugins`目录下创建一个插件文件夹`hello`。然后,在该文件夹中创建一个与文件夹同名的`.py`文件`hello.py`。 ``` plugins/ └── hello @@ -114,8 +158,14 @@ plugins/ └── hello.py ``` -2. 编写插件类 -在`hello.py`文件中,创建插件类,它继承自Plugin类。在类定义之前使用`@plugins.register`装饰器注册插件,并填写插件的相关信息,其中`desire_priority`表示插件默认的优先级,越大优先级越高,扫描到插件后可在`plugins/plugins.json`中修改插件优先级。并在`__init__`中绑定你编写的事件处理函数: +### 2. 编写插件类 + +在`hello.py`文件中,创建插件类,它继承自`Plugin`类。 + +在类定义之前需要使用`@plugins.register`装饰器注册插件,并填写插件的相关信息,其中`desire_priority`表示插件默认的优先级,越大优先级越高。初次加载插件后可在`plugins/plugins.json`中修改插件优先级。 + +并在`__init__`中绑定你编写的事件处理函数。 + ```python @plugins.register(name="Hello", desc="A simple plugin that says hello", version="0.1", author="lanvent", desire_priority= -1) class Hello(Plugin): @@ -125,17 +175,29 @@ class Hello(Plugin): logger.info("[Hello] inited") ``` -3. 编写事件处理函数 -事件处理函数接收一个`EventContext`对象作为参数。`EventContext`对象包含了事件相关的信息,如消息内容和当前回复等。可以通过`e_context['key']`访问这些信息。 +### 3. 编写事件处理函数 + +#### 修改事件上下文 + +事件处理函数接收一个`EventContext`对象`e_context`作为参数。`e_context`包含了事件相关信息,利用`e_context['key']`来访问这些信息。 + +`EventContext(Event事件类型, {'channel' : 消息channel, 'context': Context, 'reply': Reply})` + +处理函数中通过修改`e_context`对象中的事件相关信息来实现所需功能,比如更改`e_context['reply']`中的内容可以修改回复。 + +#### 决定是否交付给下个插件或默认逻辑 + +在处理函数结束时,还需要设置`e_context`对象的`action`属性,它决定如何继续处理事件。目前有以下三种处理方式: -处理函数中,你可以修改`EventContext`对象的信息,比如更改回复内容。在处理函数结束时,需要设置`EventContext`对象的`action`属性,以决定如何继续处理事件。有以下三种处理方式: - `EventAction.CONTINUE`: 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑。 - `EventAction.BREAK`: 事件结束,不再给下个插件处理,交付给默认的处理逻辑。 - `EventAction.BREAK_PASS`: 事件结束,不再给下个插件处理,跳过默认的处理逻辑。 以`Hello`插件为例,它处理`Context`类型为`TEXT`的消息: + - 如果内容是`Hello`,直接将回复设置为`Hello+用户昵称`,并跳过之后的插件和默认逻辑。 - 如果内容是`End`,它会将`Context`的类型更改为`IMAGE_CREATE`,并让事件继续,如果最终交付到默认逻辑,会调用默认的画图Bot来画画。 + ```python def on_handle_context(self, e_context: EventContext): if e_context['context'].type != ContextType.TEXT: @@ -158,8 +220,9 @@ class Hello(Plugin): e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑 ``` -## 插件设计规范 -- 个性化功能推荐设计为插件。 +## 插件设计建议 + +- 尽情将你想要的个性化功能设计为插件。 - 一个插件目录建议只注册一个插件类。建议使用单独的仓库维护插件,便于更新。 -- 插件的config文件、使用说明`README.md`、`requirement.txt`放置在插件目录中。 -- 默认优先级不要超过管理员插件`Godcmd`的优先级(999),`Godcmd`插件提供了配置管理、插件管理等功能。 \ No newline at end of file +- 插件的config文件、使用说明`README.md`、`requirement.txt`等放置在插件目录中。 +- 默认优先级不要超过管理员插件`Godcmd`的优先级(999),`Godcmd`插件提供了配置管理、插件管理等功能。 diff --git a/plugins/banwords/README.md b/plugins/banwords/README.md index 9c7e498..c423a36 100644 --- a/plugins/banwords/README.md +++ b/plugins/banwords/README.md @@ -1,9 +1,9 @@ -### 说明 +## 插件描述 简易的敏感词插件,暂不支持分词,请自行导入词库到插件文件夹中的`banwords.txt`,每行一个词,一个参考词库是[1](https://github.com/cjh0613/tencent-sensitive-words/blob/main/sensitive_words_lines.txt)。 `config.json`中能够填写默认的处理行为,目前行为有: - `ignore` : 无视这条消息。 - `replace` : 将消息中的敏感词替换成"*",并回复违规。 -### 致谢 +## 致谢 搜索功能实现来自https://github.com/toolgood/ToolGood.Words \ No newline at end of file diff --git a/plugins/dungeon/README.md b/plugins/dungeon/README.md index e3fa61a..2c2e8cd 100644 --- a/plugins/dungeon/README.md +++ b/plugins/dungeon/README.md @@ -1,3 +1,4 @@ 玩地牢游戏的聊天插件,触发方法如下: + - `$开始冒险 <背景故事>` - 以<背景故事>开始一个地牢游戏,不填写会使用默认背景故事。之后聊天中你的所有消息会帮助ai完善这个故事。 -- `$停止冒险` - 停止一个地牢游戏,回归正常的ai。 \ No newline at end of file +- `$停止冒险` - 停止一个地牢游戏,回归正常的ai。 diff --git a/plugins/godcmd/README.md b/plugins/godcmd/README.md index e93b854..cb8a1ea 100644 --- a/plugins/godcmd/README.md +++ b/plugins/godcmd/README.md @@ -1,2 +1,3 @@ 管理员插件 + `#help` - 输出帮助文档。 \ No newline at end of file diff --git a/plugins/role/README.md b/plugins/role/README.md index 59ddb75..f53e957 100644 --- a/plugins/role/README.md +++ b/plugins/role/README.md @@ -1,18 +1,14 @@ 用于让Bot扮演指定角色的聊天插件,触发方法如下: + - `$角色/$role help/帮助` - 打印目前支持的角色列表。 - `$角色/$role <角色名>` - 让AI扮演该角色,角色名支持模糊匹配。 - `$停止扮演` - 停止角色扮演。 添加自定义角色请在`roles/roles.json`中添加。 -(大部分prompt来自https://github.com/rockbenben/ChatGPT-Shortcut/blob/main/src/data/users.tsx) -以下为例子, -- `title`是角色名。 -- `description`是使用`$role`触发的英语prompt。 -- `descn`是使用`$角色`触发的中文prompt。 -- `wrapper`用于包装你的消息,可以起到强调的作用。 -- `remark`简短的描述该角色,在打印帮助时显示。 +(大部分prompt来自https://github.com/rockbenben/ChatGPT-Shortcut/blob/main/src/data/users.tsx) +以下为例子: ```json { "title": "写作助理", @@ -22,3 +18,9 @@ "remark": "最常使用的角色,用于优化文本的语法、清晰度和简洁度,提高可读性。" } ``` + +- `title`: 角色名。 +- `description`: 使用`$role`触发时,使用英语prompt。 +- `descn`: 使用`$角色`触发时,使用中文prompt。 +- `wrapper`: 用于包装用户消息,可起到强调作用,避免回复离题。 +- `remark`: 简短描述该角色,在打印帮助文档时显示。 diff --git a/plugins/sdwebui/readme.md b/plugins/sdwebui/readme.md index bb8c62c..cd6291e 100644 --- a/plugins/sdwebui/readme.md +++ b/plugins/sdwebui/readme.md @@ -1,31 +1,43 @@ -### 插件描述 +## 插件描述 + 本插件用于将画图请求转发给stable diffusion webui。 -### 环境要求 +## 环境要求 + 使用前先安装stable diffusion webui,并在它的启动参数中添加 "--api"。 + 具体信息,请参考[文章](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/API)。 请**安装**本插件的依赖包```webuiapi``` + ``` - ```pip install webuiapi``` +pip install webuiapi ``` -### 使用说明 + +## 使用说明 + 请将`config.json.template`复制为`config.json`,并修改其中的参数和规则。 -#### 画图请求格式 +### 画图请求格式 + 用户的画图请求格式为: + ``` <画图触发词><关键词1> <关键词2> ... <关键词n>: ``` + - 本插件会对画图触发词后的关键词进行逐个匹配,如果触发了规则中的关键词,则会在画图请求中重载对应的参数。 -- 规则的匹配顺序参考`config.json`中的顺序,每个关键词最多被匹配到1次,如果多个关键词触发了重复的参数,重复参数以最后一个关键词为准: +- 规则的匹配顺序参考`config.json`中的顺序,每个关键词最多被匹配到1次,如果多个关键词触发了重复的参数,重复参数以最后一个关键词为准。 - 关键词中包含`help`或`帮助`,会打印出帮助文档。 + 第一个"**:**"号之后的内容会作为附加的**prompt**,接在最终的prompt后 例如: 画横版 高清 二次元:cat + 会触发三个关键词 "横版", "高清", "二次元",prompt为"cat" + 若默认参数是: -``` +```json "width": 512, "height": 512, "enable_hr": false, @@ -35,25 +47,30 @@ ``` "横版"触发的规则参数为: -``` +```json "width": 640, "height": 384, ``` + "高清"触发的规则参数为: -``` +```json "enable_hr": true, "hr_scale": 1.6, ``` + "二次元"触发的规则参数为: -``` +```json "negative_prompt": "(low quality, worst quality:1.4),(bad_prompt:0.8), (monochrome:1.1), (greyscale)", "steps": 20, "prompt": "masterpiece, best quality", "sd_model_checkpoint": "meinamix_meinaV8" ``` -最后将第一个":"后的内容cat连接在prompt后,得到最终参数为: -``` + +以上这些规则的参数会和默认参数合并。第一个":"后的内容cat会连接在prompt后。 + +得到最终参数为: +```json "width": 640, "height": 384, "enable_hr": true, @@ -64,6 +81,8 @@ "sd_model_checkpoint": "meinamix_meinaV8" ``` -PS: 参数分为两部分: -- 一部分是params,为画画的参数;参数名**必须**与webuiapi包中[txt2img api](https://github.com/mix1009/sdwebuiapi/blob/fb2054e149c0a4e25125c0cd7e7dca06bda839d4/webuiapi/webuiapi.py#L163)的参数名一致 -- 另一部分是options,指sdwebui的设置,使用的模型和vae需要写在里面。它和http://127.0.0.1:7860/sdapi/v1/options所返回的键一致。 \ No newline at end of file + +PS: 实际参数分为两部分: + +- 一部分是`params`,为画画的参数;参数名**必须**与webuiapi包中[txt2img api](https://github.com/mix1009/sdwebuiapi/blob/fb2054e149c0a4e25125c0cd7e7dca06bda839d4/webuiapi/webuiapi.py#L163)的参数名一致 +- 另一部分是`options`,指sdwebui的设置,使用的模型和vae需写在里面。它和(http://127.0.0.1:7860/sdapi/v1/options)所返回的键一致。