Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

1 год назад
1 год назад
1 год назад
1 год назад
1 год назад
1 год назад
1 год назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. # 插件说明
  2. 本项目主体是调用ChatGPT接口的Wechat自动回复机器人。之前未插件化的代码耦合程度高,很难定制一些个性化功能(如流量控制、接入本地的NovelAI画图平台等),多个功能的优先级顺序也难以调度。
  3. **插件化**: 在保证主体功能是ChatGPT的前提下,推荐将主体功能外的功能分离成不同的插件。有个性化需求的用户仅需按照插件提供的接口编写插件,无需了解程序主体的代码结构,同时也方便代码的测试和调试。(插件调用目前仅支持 itchat)
  4. ## 插件触发时机
  5. ### 消息处理过程
  6. 了解插件触发时机前,首先需要了解程序收到消息后的执行过程。插件化版本的消息处理过程如下:
  7. ```
  8. 1.收到消息 ---> 2.产生回复 ---> 3.包装回复 ---> 4.发送回复
  9. ```
  10. 以下是它们的默认处理逻辑(太长不看,可跳过):
  11. - 1. 收到消息
  12. 本过程接收到用户消息,根据用户设置,判断本条消息是否触发。若触发,则判断该消息的命令类型,如声音、聊天、画图等。之后,将消息包装成如下的 Context 交付给下一个步骤。
  13. ```python
  14. class ContextType (Enum):
  15. TEXT = 1 # 文本消息
  16. VOICE = 2 # 音频消息
  17. IMAGE_CREATE = 3 # 创建图片命令
  18. class Context:
  19. def __init__(self, type : ContextType = None , content = None, kwargs = dict()):
  20. self.type = type
  21. self.content = content
  22. self.kwargs = kwargs
  23. def __getitem__(self, key):
  24. return self.kwargs[key]
  25. ```
  26. `Context`中除了存放消息类型和内容外,还存放了与会话相关的参数。一个例子是,当收到用户私聊消息时,还会存放以下的会话参数,`isgroup`标识`Context`是否时群聊消息,`msg`是`itchat`中原始的消息对象,`receiver`是应回复消息的对象ID,`session_id`是会话ID(一般是触发bot的消息发送方,群聊中如果`conf`里设置了`group_chat_in_one_session`,那么此处便是群聊的ID)
  27. ```
  28. context.kwargs = {'isgroup': False, 'msg': msg, 'receiver': other_user_id, 'session_id': other_user_id}
  29. ```
  30. 2. 产生回复
  31. 本过程用于处理消息。目前默认处理逻辑如下,它根据`Context`的类型交付给对应的bot。如果本过程未产生任何回复,则会跳过之后的处理阶段。
  32. ```python
  33. if context.type == ContextType.TEXT or context.type == ContextType.IMAGE_CREATE:
  34. reply = super().build_reply_content(context.content, context) #文字跟画图交付给chatgpt
  35. elif context.type == ContextType.VOICE: # 声音先进行语音转文字后,修改Context类型为文字后,再交付给chatgpt
  36. msg = context['msg']
  37. file_name = TmpDir().path() + context.content
  38. msg.download(file_name)
  39. reply = super().build_voice_to_text(file_name)
  40. if reply.type != ReplyType.ERROR and reply.type != ReplyType.INFO:
  41. context.content = reply.content # 语音转文字后,将文字内容作为新的context
  42. context.type = ContextType.TEXT
  43. reply = super().build_reply_content(context.content, context)
  44. if reply.type == ReplyType.TEXT:
  45. if conf().get('voice_reply_voice'):
  46. reply = super().build_text_to_voice(reply.content)
  47. ```
  48. Bot可产生的回复如下所示,它允许Bot可以回复多类不同的消息,未来可能不止能返回文字,而是能根据文字回复音频/图片,这时候便能派上用场。同时也加入了`INFO`和`ERROR`消息类型区分系统提示和系统错误。
  49. ```python
  50. class ReplyType(Enum):
  51. TEXT = 1 # 文本
  52. VOICE = 2 # 音频文件
  53. IMAGE = 3 # 图片文件
  54. IMAGE_URL = 4 # 图片URL
  55. INFO = 9
  56. ERROR = 10
  57. class Reply:
  58. def __init__(self, type : ReplyType = None , content = None):
  59. self.type = type
  60. self.content = content
  61. ```
  62. 3. 装饰回复
  63. 本过程根据`Context`和回复的类型,对回复的内容进行装饰。目前的装饰有以下两种,如果是文本回复,会根据是否在群聊中来决定是否艾特收方或添加回复前缀。
  64. 如果是`INFO`或`ERROR`类型,会在消息前添加对应字样。
  65. ```python
  66. if reply.type == ReplyType.TEXT:
  67. reply_text = reply.content
  68. if context['isgroup']:
  69. reply_text = '@' + context['msg']['ActualNickName'] + ' ' + reply_text.strip()
  70. reply_text = conf().get("group_chat_reply_prefix", "")+reply_text
  71. else:
  72. reply_text = conf().get("single_chat_reply_prefix", "")+reply_text
  73. reply.content = reply_text
  74. elif reply.type == ReplyType.ERROR or reply.type == ReplyType.INFO:
  75. reply.content = str(reply.type)+":\n" + reply.content
  76. ```
  77. 4. 发送回复
  78. 本过程根据回复的类型来发送回复给接收方`context["receiver"]`。
  79. ### 插件触发事件
  80. 主程序会在各消息处理过程之间触发插件事件,插件可以监听相应事件编写相应的处理逻辑。
  81. ```
  82. 1.收到消息 ---> 2.产生回复 ---> 3.包装回复 ---> 4.发送回复
  83. ```
  84. 目前加入了三类事件的触发:
  85. ```
  86. 1.收到消息
  87. ---> `ON_HANDLE_CONTEXT`
  88. 2.产生回复
  89. ---> `ON_DECORATE_REPLY`
  90. 3.包装回复
  91. ---> `ON_SEND_REPLY`
  92. 4.发送回复
  93. ```
  94. 触发事件会产生事件上下文`EventContext`,它包含了以下信息:
  95. `EventContext(Event事件类型, {'channel' : 消息channel, 'context': context, 'reply': reply})`
  96. 插件的处理函数可以修改`Context`和`Reply`的内容来定制化处理逻辑。
  97. ## 插件编写
  98. 以`plugins/hello`为例,它编写了一个简单`Hello`插件。
  99. 1. 创建插件
  100. 在`plugins`目录下创建一个插件文件夹,例如`hello`。然后,在该文件夹中创建一个与文件夹同名的`.py`文件,例如`hello.py`。
  101. ```
  102. plugins/
  103. └── hello
  104. ├── __init__.py
  105. └── hello.py
  106. ```
  107. 2. 编写插件类
  108. 在`hello.py`文件中,创建插件类,它继承自Plugin类。在类定义之前使用`@plugins.register`装饰器注册插件,并填写插件的相关信息,其中`desire_priority`表示插件默认的优先级,越大优先级越高,扫描到插件后可在`plugins/plugins.json`中修改插件优先级。并在`__init__`中绑定你编写的事件处理函数:
  109. ```python
  110. @plugins.register(name="Hello", desc="A simple plugin that says hello", version="0.1", author="lanvent", desire_priority= -1)
  111. class Hello(Plugin):
  112. def __init__(self):
  113. super().__init__()
  114. self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
  115. logger.info("[Hello] inited")
  116. ```
  117. 3. 编写事件处理函数
  118. 事件处理函数接收一个`EventContext`对象作为参数。`EventContext`对象包含了事件相关的信息,如消息内容和当前回复等。可以通过`e_context['key']`访问这些信息。
  119. 处理函数中,你可以修改`EventContext`对象的信息,比如更改回复内容。在处理函数结束时,需要设置`EventContext`对象的`action`属性,以决定如何继续处理事件。有以下三种处理方式:
  120. - `EventAction.CONTINUE`: 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑。
  121. - `EventAction.BREAK`: 事件结束,不再给下个插件处理,交付给默认的处理逻辑。
  122. - `EventAction.BREAK_PASS`: 事件结束,不再给下个插件处理,跳过默认的处理逻辑。
  123. 以`Hello`插件为例,它处理`Context`类型为`TEXT`的消息:
  124. - 如果内容是`Hello`,直接将回复设置为`Hello+用户昵称`,并跳过之后的插件和默认逻辑。
  125. - 如果内容是`End`,它会将`Context`的类型更改为`IMAGE_CREATE`,并让事件继续,如果最终交付到默认逻辑,会调用默认的画图Bot来画画。
  126. ```python
  127. def on_handle_context(self, e_context: EventContext):
  128. if e_context['context'].type != ContextType.TEXT:
  129. return
  130. content = e_context['context'].content
  131. if content == "Hello":
  132. reply = Reply()
  133. reply.type = ReplyType.TEXT
  134. msg = e_context['context']['msg']
  135. if e_context['context']['isgroup']:
  136. reply.content = "Hello, " + msg['ActualNickName'] + " from " + msg['User'].get('NickName', "Group")
  137. else:
  138. reply.content = "Hello, " + msg['User'].get('NickName', "My friend")
  139. e_context['reply'] = reply
  140. e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑
  141. if content == "End":
  142. # 如果是文本消息"End",将请求转换成"IMAGE_CREATE",并将content设置为"The World"
  143. e_context['context'].type = ContextType.IMAGE_CREATE
  144. content = "The World"
  145. e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑
  146. ```
  147. ## 插件设计规范
  148. - 个性化功能推荐设计为插件。
  149. - 一个插件目录建议只注册一个插件类。建议使用单独的仓库维护插件,便于更新。
  150. - 插件的config文件、使用说明`README.md`、`requirement.txt`放置在插件目录中。
  151. - 默认优先级不要超过管理员插件`Godcmd`的优先级(999),`Godcmd`插件提供了配置管理、插件管理等功能。