formatting code

This commit is contained in:
lanvent
2023-04-17 01:00:08 +08:00
parent 3b8972ce1f
commit 8f72e8c3e6
92 changed files with 1850 additions and 1188 deletions

View File

@@ -24,17 +24,17 @@
在本仓库中预置了一些插件,如果要安装其他仓库的插件,有两种方法。
- 第一种方法是在将下载的插件文件都解压到"plugins"文件夹的一个单独的文件夹,最终插件的代码都位于"plugins/PLUGIN_NAME/*"中。启动程序后,如果插件的目录结构正确,插件会自动被扫描加载。除此以外,注意你还需要安装文件夹中`requirements.txt`中的依赖。
- 第二种方法是`Godcmd`插件,它是预置的管理员插件,能够让程序在运行时就能安装插件,它能够自动安装依赖。
安装插件的命令是"#installp [仓库源](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/plugins/source.json)记录的插件名/仓库地址"。这是管理员命令,认证方法在[这里](https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/godcmd)。
- 安装[仓库源](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/plugins/source.json)记录的插件:#installp sdwebui
- 安装指定仓库的插件:#installp https://github.com/lanvent/plugin_sdwebui.git
在安装之后,需要执行"#scanp"命令来扫描加载新安装的插件(或者重新启动程序)。
安装插件后需要注意有些插件有自己的配置模板,一般要去掉".template"新建一个配置文件。
## 插件化实现
@@ -107,14 +107,14 @@
```
回复`Reply`的定义如下所示它允许Bot可以回复多类不同的消息。同时也加入了`INFO``ERROR`消息类型区分系统提示和系统错误。
```python
class ReplyType(Enum):
TEXT = 1 # 文本
VOICE = 2 # 音频文件
IMAGE = 3 # 图片文件
IMAGE_URL = 4 # 图片URL
INFO = 9
ERROR = 10
class Reply:
@@ -159,12 +159,12 @@
目前支持三类触发事件:
```
1.收到消息
---> `ON_HANDLE_CONTEXT`
2.产生回复
---> `ON_DECORATE_REPLY`
3.装饰回复
---> `ON_SEND_REPLY`
1.收到消息
---> `ON_HANDLE_CONTEXT`
2.产生回复
---> `ON_DECORATE_REPLY`
3.装饰回复
---> `ON_SEND_REPLY`
4.发送回复
```
@@ -268,6 +268,6 @@ class Hello(Plugin):
- 一个插件目录建议只注册一个插件类。建议使用单独的仓库维护插件,便于更新。
在测试调试好后提交`PR`,把自己的仓库加入到[仓库源](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/plugins/source.json)中。
- 插件的config文件、使用说明`README.md`、`requirement.txt`等放置在插件目录中。
- 默认优先级不要超过管理员插件`Godcmd`的优先级(999)`Godcmd`插件提供了配置管理、插件管理等功能。

View File

@@ -1,9 +1,9 @@
from .plugin_manager import PluginManager
from .event import *
from .plugin import *
from .plugin_manager import PluginManager
instance = PluginManager()
register = instance.register
register = instance.register
# load_plugins = instance.load_plugins
# emit_event = instance.emit_event

View File

@@ -1 +1 @@
from .banwords import *
from .banwords import *

View File

@@ -2,56 +2,67 @@
import json
import os
import plugins
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
import plugins
from plugins import *
from common.log import logger
from plugins import *
from .lib.WordsSearch import WordsSearch
@plugins.register(name="Banwords", desire_priority=100, hidden=True, desc="判断消息中是否有敏感词、决定是否回复。", version="1.0", author="lanvent")
@plugins.register(
name="Banwords",
desire_priority=100,
hidden=True,
desc="判断消息中是否有敏感词、决定是否回复。",
version="1.0",
author="lanvent",
)
class Banwords(Plugin):
def __init__(self):
super().__init__()
try:
curdir=os.path.dirname(__file__)
config_path=os.path.join(curdir,"config.json")
conf=None
curdir = os.path.dirname(__file__)
config_path = os.path.join(curdir, "config.json")
conf = None
if not os.path.exists(config_path):
conf={"action":"ignore"}
with open(config_path,"w") as f:
json.dump(conf,f,indent=4)
conf = {"action": "ignore"}
with open(config_path, "w") as f:
json.dump(conf, f, indent=4)
else:
with open(config_path,"r") as f:
conf=json.load(f)
with open(config_path, "r") as f:
conf = json.load(f)
self.searchr = WordsSearch()
self.action = conf["action"]
banwords_path = os.path.join(curdir,"banwords.txt")
with open(banwords_path, 'r', encoding='utf-8') as f:
words=[]
banwords_path = os.path.join(curdir, "banwords.txt")
with open(banwords_path, "r", encoding="utf-8") as f:
words = []
for line in f:
word = line.strip()
if word:
words.append(word)
self.searchr.SetKeywords(words)
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
if conf.get("reply_filter",True):
if conf.get("reply_filter", True):
self.handlers[Event.ON_DECORATE_REPLY] = self.on_decorate_reply
self.reply_action = conf.get("reply_action","ignore")
self.reply_action = conf.get("reply_action", "ignore")
logger.info("[Banwords] inited")
except Exception as e:
logger.warn("[Banwords] init failed, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/banwords .")
logger.warn(
"[Banwords] init failed, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/banwords ."
)
raise e
def on_handle_context(self, e_context: EventContext):
if e_context['context'].type not in [ContextType.TEXT,ContextType.IMAGE_CREATE]:
if e_context["context"].type not in [
ContextType.TEXT,
ContextType.IMAGE_CREATE,
]:
return
content = e_context['context'].content
content = e_context["context"].content
logger.debug("[Banwords] on_handle_context. content: %s" % content)
if self.action == "ignore":
f = self.searchr.FindFirst(content)
@@ -61,31 +72,34 @@ class Banwords(Plugin):
return
elif self.action == "replace":
if self.searchr.ContainsAny(content):
reply = Reply(ReplyType.INFO, "发言中包含敏感词,请重试: \n"+self.searchr.Replace(content))
e_context['reply'] = reply
reply = Reply(
ReplyType.INFO, "发言中包含敏感词,请重试: \n" + self.searchr.Replace(content)
)
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
return
def on_decorate_reply(self, e_context: EventContext):
if e_context['reply'].type not in [ReplyType.TEXT]:
def on_decorate_reply(self, e_context: EventContext):
if e_context["reply"].type not in [ReplyType.TEXT]:
return
reply = e_context['reply']
reply = e_context["reply"]
content = reply.content
if self.reply_action == "ignore":
f = self.searchr.FindFirst(content)
if f:
logger.info("[Banwords] %s in reply" % f["Keyword"])
e_context['reply'] = None
e_context["reply"] = None
e_context.action = EventAction.BREAK_PASS
return
elif self.reply_action == "replace":
if self.searchr.ContainsAny(content):
reply = Reply(ReplyType.INFO, "已替换回复中的敏感词: \n"+self.searchr.Replace(content))
e_context['reply'] = reply
reply = Reply(
ReplyType.INFO, "已替换回复中的敏感词: \n" + self.searchr.Replace(content)
)
e_context["reply"] = reply
e_context.action = EventAction.CONTINUE
return
def get_help_text(self, **kwargs):
return Banwords.desc
return Banwords.desc

View File

@@ -1,5 +1,5 @@
{
"action": "replace",
"reply_filter": true,
"reply_action": "ignore"
}
"action": "replace",
"reply_filter": true,
"reply_action": "ignore"
}

View File

@@ -24,7 +24,7 @@ see https://ai.baidu.com/unit/home#/home?track=61fe1b0d3407ce3face1d92cb5c291087
``` json
{
"service_id": "s...", #"机器人ID"
"api_key": "",
"api_key": "",
"secret_key": ""
}
```

View File

@@ -1 +1 @@
from .bdunit import *
from .bdunit import *

View File

@@ -2,21 +2,29 @@
import json
import os
import uuid
from uuid import getnode as get_mac
import requests
import plugins
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from common.log import logger
import plugins
from plugins import *
from uuid import getnode as get_mac
"""利用百度UNIT实现智能对话
如果命中意图,返回意图对应的回复,否则返回继续交付给下个插件处理
"""
@plugins.register(name="BDunit", desire_priority=0, hidden=True, desc="Baidu unit bot system", version="0.1", author="jackson")
@plugins.register(
name="BDunit",
desire_priority=0,
hidden=True,
desc="Baidu unit bot system",
version="0.1",
author="jackson",
)
class BDunit(Plugin):
def __init__(self):
super().__init__()
@@ -40,11 +48,10 @@ class BDunit(Plugin):
raise e
def on_handle_context(self, e_context: EventContext):
if e_context['context'].type != ContextType.TEXT:
if e_context["context"].type != ContextType.TEXT:
return
content = e_context['context'].content
content = e_context["context"].content
logger.debug("[BDunit] on_handle_context. content: %s" % content)
parsed = self.getUnit2(content)
intent = self.getIntent(parsed)
@@ -53,7 +60,7 @@ class BDunit(Plugin):
reply = Reply()
reply.type = ReplyType.TEXT
reply.content = self.getSay(parsed)
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
else:
e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑
@@ -70,17 +77,15 @@ class BDunit(Plugin):
string: access_token
"""
url = "https://aip.baidubce.com/oauth/2.0/token?client_id={}&client_secret={}&grant_type=client_credentials".format(
self.api_key, self.secret_key)
self.api_key, self.secret_key
)
payload = ""
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
response = requests.request("POST", url, headers=headers, data=payload)
# print(response.text)
return response.json()['access_token']
return response.json()["access_token"]
def getUnit(self, query):
"""
@@ -90,11 +95,14 @@ class BDunit(Plugin):
"""
url = (
'https://aip.baidubce.com/rpc/2.0/unit/service/v3/chat?access_token='
"https://aip.baidubce.com/rpc/2.0/unit/service/v3/chat?access_token="
+ self.access_token
)
request = {"query": query, "user_id": str(
get_mac())[:32], "terminal_id": "88888"}
request = {
"query": query,
"user_id": str(get_mac())[:32],
"terminal_id": "88888",
}
body = {
"log_id": str(uuid.uuid1()),
"version": "3.0",
@@ -142,11 +150,7 @@ class BDunit(Plugin):
:param parsed: UNIT 解析结果
:returns: 意图数组
"""
if (
parsed
and "result" in parsed
and "response_list" in parsed["result"]
):
if parsed and "result" in parsed and "response_list" in parsed["result"]:
try:
return parsed["result"]["response_list"][0]["schema"]["intent"]
except Exception as e:
@@ -163,11 +167,7 @@ class BDunit(Plugin):
:param intent: 意图的名称
:returns: True: 包含; False: 不包含
"""
if (
parsed
and "result" in parsed
and "response_list" in parsed["result"]
):
if parsed and "result" in parsed and "response_list" in parsed["result"]:
response_list = parsed["result"]["response_list"]
for response in response_list:
if (
@@ -189,11 +189,7 @@ class BDunit(Plugin):
:returns: 词槽列表。你可以通过 name 属性筛选词槽,
再通过 normalized_word 属性取出相应的值
"""
if (
parsed
and "result" in parsed
and "response_list" in parsed["result"]
):
if parsed and "result" in parsed and "response_list" in parsed["result"]:
response_list = parsed["result"]["response_list"]
if intent == "":
try:
@@ -236,11 +232,7 @@ class BDunit(Plugin):
:param parsed: UNIT 解析结果
:returns: UNIT 的回复文本
"""
if (
parsed
and "result" in parsed
and "response_list" in parsed["result"]
):
if parsed and "result" in parsed and "response_list" in parsed["result"]:
response_list = parsed["result"]["response_list"]
answer = {}
for response in response_list:
@@ -266,11 +258,7 @@ class BDunit(Plugin):
:param intent: 意图的名称
:returns: UNIT 的回复文本
"""
if (
parsed
and "result" in parsed
and "response_list" in parsed["result"]
):
if parsed and "result" in parsed and "response_list" in parsed["result"]:
response_list = parsed["result"]["response_list"]
if intent == "":
try:

View File

@@ -1,5 +1,5 @@
{
"service_id": "s...",
"api_key": "",
"secret_key": ""
}
"service_id": "s...",
"api_key": "",
"secret_key": ""
}

View File

@@ -1 +1 @@
from .dungeon import *
from .dungeon import *

View File

@@ -1,17 +1,18 @@
# encoding:utf-8
import plugins
from bridge.bridge import Bridge
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from common.expired_dict import ExpiredDict
from config import conf
import plugins
from plugins import *
from common.log import logger
from common import const
from common.expired_dict import ExpiredDict
from common.log import logger
from config import conf
from plugins import *
# https://github.com/bupticybee/ChineseAiDungeonChatGPT
class StoryTeller():
class StoryTeller:
def __init__(self, bot, sessionid, story):
self.bot = bot
self.sessionid = sessionid
@@ -27,67 +28,85 @@ class StoryTeller():
if user_action[-1] != "":
user_action = user_action + ""
if self.first_interact:
prompt = """现在来充当一个文字冒险游戏,描述时候注意节奏,不要太快,仔细描述各个人物的心情和周边环境。一次只需写四到六句话。
开头是,""" + self.story + " " + user_action
prompt = (
"""现在来充当一个文字冒险游戏,描述时候注意节奏,不要太快,仔细描述各个人物的心情和周边环境。一次只需写四到六句话。
开头是,"""
+ self.story
+ " "
+ user_action
)
self.first_interact = False
else:
prompt = """继续一次只需要续写四到六句话总共就只讲5分钟内发生的事情。""" + user_action
return prompt
@plugins.register(name="Dungeon", desire_priority=0, namecn="文字冒险", desc="A plugin to play dungeon game", version="1.0", author="lanvent")
@plugins.register(
name="Dungeon",
desire_priority=0,
namecn="文字冒险",
desc="A plugin to play dungeon game",
version="1.0",
author="lanvent",
)
class Dungeon(Plugin):
def __init__(self):
super().__init__()
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
logger.info("[Dungeon] inited")
# 目前没有设计session过期事件这里先暂时使用过期字典
if conf().get('expires_in_seconds'):
self.games = ExpiredDict(conf().get('expires_in_seconds'))
if conf().get("expires_in_seconds"):
self.games = ExpiredDict(conf().get("expires_in_seconds"))
else:
self.games = dict()
def on_handle_context(self, e_context: EventContext):
if e_context['context'].type != ContextType.TEXT:
if e_context["context"].type != ContextType.TEXT:
return
bottype = Bridge().get_bot_type("chat")
if bottype not in (const.CHATGPT, const.OPEN_AI):
return
bot = Bridge().get_bot("chat")
content = e_context['context'].content[:]
clist = e_context['context'].content.split(maxsplit=1)
sessionid = e_context['context']['session_id']
content = e_context["context"].content[:]
clist = e_context["context"].content.split(maxsplit=1)
sessionid = e_context["context"]["session_id"]
logger.debug("[Dungeon] on_handle_context. content: %s" % clist)
trigger_prefix = conf().get('plugin_trigger_prefix', "$")
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
if clist[0] == f"{trigger_prefix}停止冒险":
if sessionid in self.games:
self.games[sessionid].reset()
del self.games[sessionid]
reply = Reply(ReplyType.INFO, "冒险结束!")
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
elif clist[0] == f"{trigger_prefix}开始冒险" or sessionid in self.games:
if sessionid not in self.games or clist[0] == f"{trigger_prefix}开始冒险":
if len(clist)>1 :
if len(clist) > 1:
story = clist[1]
else:
story = "你在树林里冒险,指不定会从哪里蹦出来一些奇怪的东西,你握紧手上的手枪,希望这次冒险能够找到一些值钱的东西,你往树林深处走去。"
story = (
"你在树林里冒险,指不定会从哪里蹦出来一些奇怪的东西,你握紧手上的手枪,希望这次冒险能够找到一些值钱的东西,你往树林深处走去。"
)
self.games[sessionid] = StoryTeller(bot, sessionid, story)
reply = Reply(ReplyType.INFO, "冒险开始,你可以输入任意内容,让故事继续下去。故事背景是:" + story)
e_context['reply'] = reply
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
else:
prompt = self.games[sessionid].action(content)
e_context['context'].type = ContextType.TEXT
e_context['context'].content = prompt
e_context.action = EventAction.BREAK # 事件结束不跳过处理context的默认逻辑
e_context["context"].type = ContextType.TEXT
e_context["context"].content = prompt
e_context.action = EventAction.BREAK # 事件结束不跳过处理context的默认逻辑
def get_help_text(self, **kwargs):
help_text = "可以和机器人一起玩文字冒险游戏。\n"
if kwargs.get('verbose') != True:
if kwargs.get("verbose") != True:
return help_text
trigger_prefix = conf().get('plugin_trigger_prefix', "$")
help_text = f"{trigger_prefix}开始冒险 "+"背景故事: 开始一个基于{背景故事}的文字冒险,之后你的所有消息会协助完善这个故事。\n"+f"{trigger_prefix}停止冒险: 结束游戏。\n"
if kwargs.get('verbose') == True:
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
help_text = (
f"{trigger_prefix}开始冒险 "
+ "背景故事: 开始一个基于{背景故事}的文字冒险,之后你的所有消息会协助完善这个故事。\n"
+ f"{trigger_prefix}停止冒险: 结束游戏。\n"
)
if kwargs.get("verbose") == True:
help_text += f"\n命令例子: '{trigger_prefix}开始冒险 你在树林里冒险,指不定会从哪里蹦出来一些奇怪的东西,你握紧手上的手枪,希望这次冒险能够找到一些值钱的东西,你往树林深处走去。'"
return help_text
return help_text

View File

@@ -9,17 +9,17 @@ class Event(Enum):
e_context = { "channel": 消息channel, "context" : 本次消息的context}
"""
ON_HANDLE_CONTEXT = 2 # 处理消息前
ON_HANDLE_CONTEXT = 2 # 处理消息前
"""
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复,初始为空 }
"""
ON_DECORATE_REPLY = 3 # 得到回复后准备装饰
ON_DECORATE_REPLY = 3 # 得到回复后准备装饰
"""
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复 }
"""
ON_SEND_REPLY = 4 # 发送回复前
ON_SEND_REPLY = 4 # 发送回复前
"""
e_context = { "channel": 消息channel, "context" : 本次消息的context, "reply" : 目前的回复 }
"""
@@ -28,9 +28,9 @@ class Event(Enum):
class EventAction(Enum):
CONTINUE = 1 # 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑
BREAK = 2 # 事件结束,不再给下个插件处理,交付给默认的事件处理逻辑
BREAK_PASS = 3 # 事件结束,不再给下个插件处理,不交付给默认的事件处理逻辑
CONTINUE = 1 # 事件未结束,继续交给下个插件处理,如果没有下个插件,则交付给默认的事件处理逻辑
BREAK = 2 # 事件结束,不再给下个插件处理,交付给默认的事件处理逻辑
BREAK_PASS = 3 # 事件结束,不再给下个插件处理,不交付给默认的事件处理逻辑
class EventContext:

View File

@@ -1 +1 @@
from .finish import *
from .finish import *

View File

@@ -1,14 +1,21 @@
# encoding:utf-8
import plugins
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from config import conf
import plugins
from plugins import *
from common.log import logger
from config import conf
from plugins import *
@plugins.register(name="Finish", desire_priority=-999, hidden=True, desc="A plugin that check unknown command", version="1.0", author="js00000")
@plugins.register(
name="Finish",
desire_priority=-999,
hidden=True,
desc="A plugin that check unknown command",
version="1.0",
author="js00000",
)
class Finish(Plugin):
def __init__(self):
super().__init__()
@@ -16,19 +23,18 @@ class Finish(Plugin):
logger.info("[Finish] inited")
def on_handle_context(self, e_context: EventContext):
if e_context['context'].type != ContextType.TEXT:
if e_context["context"].type != ContextType.TEXT:
return
content = e_context['context'].content
content = e_context["context"].content
logger.debug("[Finish] on_handle_context. content: %s" % content)
trigger_prefix = conf().get('plugin_trigger_prefix',"$")
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
if content.startswith(trigger_prefix):
reply = Reply()
reply.type = ReplyType.ERROR
reply.content = "未知插件命令\n查看插件命令列表请输入#help 插件名\n"
e_context['reply'] = reply
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
def get_help_text(self, **kwargs):
return ""

View File

@@ -1 +1 @@
from .godcmd import *
from .godcmd import *

View File

@@ -1,4 +1,4 @@
{
"password": "",
"admin_users": []
}
"password": "",
"admin_users": []
}

View File

@@ -6,14 +6,16 @@ import random
import string
import traceback
from typing import Tuple
import plugins
from bridge.bridge import Bridge
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from config import conf, load_config
import plugins
from plugins import *
from common import const
from common.log import logger
from config import conf, load_config
from plugins import *
# 定义指令集
COMMANDS = {
"help": {
@@ -41,7 +43,7 @@ COMMANDS = {
},
"id": {
"alias": ["id", "用户"],
"desc": "获取用户id", # wechaty和wechatmp的用户id不会变化可用于绑定管理员
"desc": "获取用户id", # wechaty和wechatmp的用户id不会变化可用于绑定管理员
},
"reset": {
"alias": ["reset", "重置会话"],
@@ -114,18 +116,20 @@ ADMIN_COMMANDS = {
"desc": "开启机器调试日志",
},
}
# 定义帮助函数
def get_help_text(isadmin, isgroup):
help_text = "通用指令:\n"
for cmd, info in COMMANDS.items():
if cmd=="auth": #不提示认证指令
if cmd == "auth": # 不提示认证指令
continue
if cmd=="id" and conf().get("channel_type","wx") not in ["wxy","wechatmp"]:
if cmd == "id" and conf().get("channel_type", "wx") not in ["wxy", "wechatmp"]:
continue
alias=["#"+a for a in info['alias'][:1]]
alias = ["#" + a for a in info["alias"][:1]]
help_text += f"{','.join(alias)} "
if 'args' in info:
args=[a for a in info['args']]
if "args" in info:
args = [a for a in info["args"]]
help_text += f"{' '.join(args)}"
help_text += f": {info['desc']}\n"
@@ -135,39 +139,48 @@ def get_help_text(isadmin, isgroup):
for plugin in plugins:
if plugins[plugin].enabled and not plugins[plugin].hidden:
namecn = plugins[plugin].namecn
help_text += "\n%s:"%namecn
help_text += PluginManager().instances[plugin].get_help_text(verbose=False).strip()
help_text += "\n%s:" % namecn
help_text += (
PluginManager().instances[plugin].get_help_text(verbose=False).strip()
)
if ADMIN_COMMANDS and isadmin:
help_text += "\n\n管理员指令:\n"
for cmd, info in ADMIN_COMMANDS.items():
alias=["#"+a for a in info['alias'][:1]]
alias = ["#" + a for a in info["alias"][:1]]
help_text += f"{','.join(alias)} "
if 'args' in info:
args=[a for a in info['args']]
if "args" in info:
args = [a for a in info["args"]]
help_text += f"{' '.join(args)}"
help_text += f": {info['desc']}\n"
return help_text
@plugins.register(name="Godcmd", desire_priority=999, hidden=True, desc="为你的机器人添加指令集,有用户和管理员两种角色,加载顺序请放在首位,初次运行后插件目录会生成配置文件, 填充管理员密码后即可认证", version="1.0", author="lanvent")
class Godcmd(Plugin):
@plugins.register(
name="Godcmd",
desire_priority=999,
hidden=True,
desc="为你的机器人添加指令集,有用户和管理员两种角色,加载顺序请放在首位,初次运行后插件目录会生成配置文件, 填充管理员密码后即可认证",
version="1.0",
author="lanvent",
)
class Godcmd(Plugin):
def __init__(self):
super().__init__()
curdir=os.path.dirname(__file__)
config_path=os.path.join(curdir,"config.json")
gconf=None
curdir = os.path.dirname(__file__)
config_path = os.path.join(curdir, "config.json")
gconf = None
if not os.path.exists(config_path):
gconf={"password":"","admin_users":[]}
with open(config_path,"w") as f:
json.dump(gconf,f,indent=4)
gconf = {"password": "", "admin_users": []}
with open(config_path, "w") as f:
json.dump(gconf, f, indent=4)
else:
with open(config_path,"r") as f:
gconf=json.load(f)
with open(config_path, "r") as f:
gconf = json.load(f)
if gconf["password"] == "":
self.temp_password = "".join(random.sample(string.digits, 4))
logger.info("[Godcmd] 因未设置口令,本次的临时口令为%s"%self.temp_password)
logger.info("[Godcmd] 因未设置口令,本次的临时口令为%s" % self.temp_password)
else:
self.temp_password = None
custom_commands = conf().get("clear_memory_commands", [])
@@ -178,41 +191,42 @@ class Godcmd(Plugin):
COMMANDS["reset"]["alias"].append(custom_command)
self.password = gconf["password"]
self.admin_users = gconf["admin_users"] # 预存的管理员账号这些账号不需要认证。itchat的用户名每次都会变不可用
self.isrunning = True # 机器人是否运行中
self.admin_users = gconf[
"admin_users"
] # 预存的管理员账号这些账号不需要认证。itchat的用户名每次都会变不可用
self.isrunning = True # 机器人是否运行中
self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
logger.info("[Godcmd] inited")
def on_handle_context(self, e_context: EventContext):
context_type = e_context['context'].type
context_type = e_context["context"].type
if context_type != ContextType.TEXT:
if not self.isrunning:
e_context.action = EventAction.BREAK_PASS
return
content = e_context['context'].content
content = e_context["context"].content
logger.debug("[Godcmd] on_handle_context. content: %s" % content)
if content.startswith("#"):
# msg = e_context['context']['msg']
channel = e_context['channel']
user = e_context['context']['receiver']
session_id = e_context['context']['session_id']
isgroup = e_context['context'].get("isgroup", False)
channel = e_context["channel"]
user = e_context["context"]["receiver"]
session_id = e_context["context"]["session_id"]
isgroup = e_context["context"].get("isgroup", False)
bottype = Bridge().get_bot_type("chat")
bot = Bridge().get_bot("chat")
# 将命令和参数分割
command_parts = content[1:].strip().split()
cmd = command_parts[0]
args = command_parts[1:]
isadmin=False
isadmin = False
if user in self.admin_users:
isadmin=True
ok=False
result="string"
if any(cmd in info['alias'] for info in COMMANDS.values()):
cmd = next(c for c, info in COMMANDS.items() if cmd in info['alias'])
isadmin = True
ok = False
result = "string"
if any(cmd in info["alias"] for info in COMMANDS.values()):
cmd = next(c for c, info in COMMANDS.items() if cmd in info["alias"])
if cmd == "auth":
ok, result = self.authenticate(user, args, isadmin, isgroup)
elif cmd == "help" or cmd == "helpp":
@@ -224,10 +238,14 @@ class Godcmd(Plugin):
query_name = args[0].upper()
# search name and namecn
for name, plugincls in plugins.items():
if not plugincls.enabled :
if not plugincls.enabled:
continue
if query_name == name or query_name == plugincls.namecn:
ok, result = True, PluginManager().instances[name].get_help_text(isgroup=isgroup, isadmin=isadmin, verbose=True)
ok, result = True, PluginManager().instances[
name
].get_help_text(
isgroup=isgroup, isadmin=isadmin, verbose=True
)
break
if not ok:
result = "插件不存在或未启用"
@@ -236,14 +254,14 @@ class Godcmd(Plugin):
elif cmd == "set_openai_api_key":
if len(args) == 1:
user_data = conf().get_user_data(user)
user_data['openai_api_key'] = args[0]
user_data["openai_api_key"] = args[0]
ok, result = True, "你的OpenAI私有api_key已设置为" + args[0]
else:
ok, result = False, "请提供一个api_key"
elif cmd == "reset_openai_api_key":
try:
user_data = conf().get_user_data(user)
user_data.pop('openai_api_key')
user_data.pop("openai_api_key")
ok, result = True, "你的OpenAI私有api_key已清除"
except Exception as e:
ok, result = False, "你没有设置私有api_key"
@@ -255,12 +273,16 @@ class Godcmd(Plugin):
else:
ok, result = False, "当前对话机器人不支持重置会话"
logger.debug("[Godcmd] command: %s by %s" % (cmd, user))
elif any(cmd in info['alias'] for info in ADMIN_COMMANDS.values()):
elif any(cmd in info["alias"] for info in ADMIN_COMMANDS.values()):
if isadmin:
if isgroup:
ok, result = False, "群聊不可执行管理员指令"
else:
cmd = next(c for c, info in ADMIN_COMMANDS.items() if cmd in info['alias'])
cmd = next(
c
for c, info in ADMIN_COMMANDS.items()
if cmd in info["alias"]
)
if cmd == "stop":
self.isrunning = False
ok, result = True, "服务已暂停"
@@ -278,13 +300,13 @@ class Godcmd(Plugin):
else:
ok, result = False, "当前对话机器人不支持重置会话"
elif cmd == "debug":
logger.setLevel('DEBUG')
logger.setLevel("DEBUG")
ok, result = True, "DEBUG模式已开启"
elif cmd == "plist":
plugins = PluginManager().list_plugins()
ok = True
result = "插件列表:\n"
for name,plugincls in plugins.items():
for name, plugincls in plugins.items():
result += f"{plugincls.name}_v{plugincls.version} {plugincls.priority} - "
if plugincls.enabled:
result += "已启用\n"
@@ -294,16 +316,20 @@ class Godcmd(Plugin):
new_plugins = PluginManager().scan_plugins()
ok, result = True, "插件扫描完成"
PluginManager().activate_plugins()
if len(new_plugins) >0 :
if len(new_plugins) > 0:
result += "\n发现新插件:\n"
result += "\n".join([f"{p.name}_v{p.version}" for p in new_plugins])
else :
result +=", 未发现新插件"
result += "\n".join(
[f"{p.name}_v{p.version}" for p in new_plugins]
)
else:
result += ", 未发现新插件"
elif cmd == "setpri":
if len(args) != 2:
ok, result = False, "请提供插件名和优先级"
else:
ok = PluginManager().set_plugin_priority(args[0], int(args[1]))
ok = PluginManager().set_plugin_priority(
args[0], int(args[1])
)
if ok:
result = "插件" + args[0] + "优先级已设置为" + args[1]
else:
@@ -350,42 +376,42 @@ class Godcmd(Plugin):
else:
ok, result = False, "需要管理员权限才能执行该指令"
else:
trigger_prefix = conf().get('plugin_trigger_prefix',"$")
if trigger_prefix == "#": # 跟插件聊天指令前缀相同,继续递交
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
if trigger_prefix == "#": # 跟插件聊天指令前缀相同,继续递交
return
ok, result = False, f"未知指令:{cmd}\n查看指令列表请输入#help \n"
reply = Reply()
if ok:
reply.type = ReplyType.INFO
else:
reply.type = ReplyType.ERROR
reply.content = result
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
elif not self.isrunning:
e_context.action = EventAction.BREAK_PASS
def authenticate(self, userid, args, isadmin, isgroup) -> Tuple[bool,str] :
def authenticate(self, userid, args, isadmin, isgroup) -> Tuple[bool, str]:
if isgroup:
return False,"请勿在群聊中认证"
return False, "请勿在群聊中认证"
if isadmin:
return False,"管理员账号无需认证"
return False, "管理员账号无需认证"
if len(args) != 1:
return False,"请提供口令"
return False, "请提供口令"
password = args[0]
if password == self.password:
self.admin_users.append(userid)
return True,"认证成功"
return True, "认证成功"
elif password == self.temp_password:
self.admin_users.append(userid)
return True,"认证成功,请尽快设置口令"
return True, "认证成功,请尽快设置口令"
else:
return False,"认证失败"
return False, "认证失败"
def get_help_text(self, isadmin = False, isgroup = False, **kwargs):
return get_help_text(isadmin, isgroup)
def get_help_text(self, isadmin=False, isgroup=False, **kwargs):
return get_help_text(isadmin, isgroup)

View File

@@ -1 +1 @@
from .hello import *
from .hello import *

View File

@@ -1,14 +1,21 @@
# encoding:utf-8
import plugins
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from channel.chat_message import ChatMessage
import plugins
from plugins import *
from common.log import logger
from plugins import *
@plugins.register(name="Hello", desire_priority=-1, hidden=True, desc="A simple plugin that says hello", version="0.1", author="lanvent")
@plugins.register(
name="Hello",
desire_priority=-1,
hidden=True,
desc="A simple plugin that says hello",
version="0.1",
author="lanvent",
)
class Hello(Plugin):
def __init__(self):
super().__init__()
@@ -16,33 +23,34 @@ class Hello(Plugin):
logger.info("[Hello] inited")
def on_handle_context(self, e_context: EventContext):
if e_context['context'].type != ContextType.TEXT:
if e_context["context"].type != ContextType.TEXT:
return
content = e_context['context'].content
content = e_context["context"].content
logger.debug("[Hello] on_handle_context. content: %s" % content)
if content == "Hello":
reply = Reply()
reply.type = ReplyType.TEXT
msg:ChatMessage = e_context['context']['msg']
if e_context['context']['isgroup']:
reply.content = f"Hello, {msg.actual_user_nickname} from {msg.from_user_nickname}"
msg: ChatMessage = e_context["context"]["msg"]
if e_context["context"]["isgroup"]:
reply.content = (
f"Hello, {msg.actual_user_nickname} from {msg.from_user_nickname}"
)
else:
reply.content = f"Hello, {msg.from_user_nickname}"
e_context['reply'] = reply
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS # 事件结束并跳过处理context的默认逻辑
if content == "Hi":
reply = Reply()
reply.type = ReplyType.TEXT
reply.content = "Hi"
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK # 事件结束进入默认处理逻辑一般会覆写reply
if content == "End":
# 如果是文本消息"End",将请求转换成"IMAGE_CREATE"并将content设置为"The World"
e_context['context'].type = ContextType.IMAGE_CREATE
e_context["context"].type = ContextType.IMAGE_CREATE
content = "The World"
e_context.action = EventAction.CONTINUE # 事件继续,交付给下个插件或默认逻辑

View File

@@ -3,4 +3,4 @@ class Plugin:
self.handlers = {}
def get_help_text(self, **kwargs):
return "暂无帮助信息"
return "暂无帮助信息"

View File

@@ -5,17 +5,19 @@ import importlib.util
import json
import os
import sys
from common.log import logger
from common.singleton import singleton
from common.sorted_dict import SortedDict
from .event import *
from common.log import logger
from config import conf
from .event import *
@singleton
class PluginManager:
def __init__(self):
self.plugins = SortedDict(lambda k,v: v.priority,reverse=True)
self.plugins = SortedDict(lambda k, v: v.priority, reverse=True)
self.listening_plugins = {}
self.instances = {}
self.pconf = {}
@@ -26,17 +28,27 @@ class PluginManager:
def wrapper(plugincls):
plugincls.name = name
plugincls.priority = desire_priority
plugincls.desc = kwargs.get('desc')
plugincls.author = kwargs.get('author')
plugincls.desc = kwargs.get("desc")
plugincls.author = kwargs.get("author")
plugincls.path = self.current_plugin_path
plugincls.version = kwargs.get('version') if kwargs.get('version') != None else "1.0"
plugincls.namecn = kwargs.get('namecn') if kwargs.get('namecn') != None else name
plugincls.hidden = kwargs.get('hidden') if kwargs.get('hidden') != None else False
plugincls.version = (
kwargs.get("version") if kwargs.get("version") != None else "1.0"
)
plugincls.namecn = (
kwargs.get("namecn") if kwargs.get("namecn") != None else name
)
plugincls.hidden = (
kwargs.get("hidden") if kwargs.get("hidden") != None else False
)
plugincls.enabled = True
if self.current_plugin_path == None:
raise Exception("Plugin path not set")
self.plugins[name.upper()] = plugincls
logger.info("Plugin %s_v%s registered, path=%s" % (name, plugincls.version, plugincls.path))
logger.info(
"Plugin %s_v%s registered, path=%s"
% (name, plugincls.version, plugincls.path)
)
return wrapper
def save_config(self):
@@ -50,10 +62,12 @@ class PluginManager:
if os.path.exists("./plugins/plugins.json"):
with open("./plugins/plugins.json", "r", encoding="utf-8") as f:
pconf = json.load(f)
pconf['plugins'] = SortedDict(lambda k,v: v["priority"],pconf['plugins'],reverse=True)
pconf["plugins"] = SortedDict(
lambda k, v: v["priority"], pconf["plugins"], reverse=True
)
else:
modified = True
pconf = {"plugins": SortedDict(lambda k,v: v["priority"],reverse=True)}
pconf = {"plugins": SortedDict(lambda k, v: v["priority"], reverse=True)}
self.pconf = pconf
if modified:
self.save_config()
@@ -67,7 +81,7 @@ class PluginManager:
plugin_path = os.path.join(plugins_dir, plugin_name)
if os.path.isdir(plugin_path):
# 判断插件是否包含同名__init__.py文件
main_module_path = os.path.join(plugin_path,"__init__.py")
main_module_path = os.path.join(plugin_path, "__init__.py")
if os.path.isfile(main_module_path):
# 导入插件
import_path = "plugins.{}".format(plugin_name)
@@ -76,16 +90,26 @@ class PluginManager:
if plugin_path in self.loaded:
if self.loaded[plugin_path] == None:
logger.info("reload module %s" % plugin_name)
self.loaded[plugin_path] = importlib.reload(sys.modules[import_path])
dependent_module_names = [name for name in sys.modules.keys() if name.startswith( import_path+ '.')]
self.loaded[plugin_path] = importlib.reload(
sys.modules[import_path]
)
dependent_module_names = [
name
for name in sys.modules.keys()
if name.startswith(import_path + ".")
]
for name in dependent_module_names:
logger.info("reload module %s" % name)
importlib.reload(sys.modules[name])
else:
self.loaded[plugin_path] = importlib.import_module(import_path)
self.loaded[plugin_path] = importlib.import_module(
import_path
)
self.current_plugin_path = None
except Exception as e:
logger.exception("Failed to import plugin %s: %s" % (plugin_name, e))
logger.exception(
"Failed to import plugin %s: %s" % (plugin_name, e)
)
continue
pconf = self.pconf
news = [self.plugins[name] for name in self.plugins]
@@ -95,21 +119,28 @@ class PluginManager:
rawname = plugincls.name
if rawname not in pconf["plugins"]:
modified = True
logger.info("Plugin %s not found in pconfig, adding to pconfig..." % name)
pconf["plugins"][rawname] = {"enabled": plugincls.enabled, "priority": plugincls.priority}
logger.info(
"Plugin %s not found in pconfig, adding to pconfig..." % name
)
pconf["plugins"][rawname] = {
"enabled": plugincls.enabled,
"priority": plugincls.priority,
}
else:
self.plugins[name].enabled = pconf["plugins"][rawname]["enabled"]
self.plugins[name].priority = pconf["plugins"][rawname]["priority"]
self.plugins._update_heap(name) # 更新下plugins中的顺序
self.plugins._update_heap(name) # 更新下plugins中的顺序
if modified:
self.save_config()
return new_plugins
def refresh_order(self):
for event in self.listening_plugins.keys():
self.listening_plugins[event].sort(key=lambda name: self.plugins[name].priority, reverse=True)
self.listening_plugins[event].sort(
key=lambda name: self.plugins[name].priority, reverse=True
)
def activate_plugins(self): # 生成新开启的插件实例
def activate_plugins(self): # 生成新开启的插件实例
failed_plugins = []
for name, plugincls in self.plugins.items():
if plugincls.enabled:
@@ -129,7 +160,7 @@ class PluginManager:
self.refresh_order()
return failed_plugins
def reload_plugin(self, name:str):
def reload_plugin(self, name: str):
name = name.upper()
if name in self.instances:
for event in self.listening_plugins:
@@ -139,13 +170,13 @@ class PluginManager:
self.activate_plugins()
return True
return False
def load_plugins(self):
self.load_config()
self.scan_plugins()
pconf = self.pconf
logger.debug("plugins.json config={}".format(pconf))
for name,plugin in pconf["plugins"].items():
for name, plugin in pconf["plugins"].items():
if name.upper() not in self.plugins:
logger.error("Plugin %s not found, but found in plugins.json" % name)
self.activate_plugins()
@@ -153,13 +184,18 @@ class PluginManager:
def emit_event(self, e_context: EventContext, *args, **kwargs):
if e_context.event in self.listening_plugins:
for name in self.listening_plugins[e_context.event]:
if self.plugins[name].enabled and e_context.action == EventAction.CONTINUE:
logger.debug("Plugin %s triggered by event %s" % (name,e_context.event))
if (
self.plugins[name].enabled
and e_context.action == EventAction.CONTINUE
):
logger.debug(
"Plugin %s triggered by event %s" % (name, e_context.event)
)
instance = self.instances[name]
instance.handlers[e_context.event](e_context, *args, **kwargs)
return e_context
def set_plugin_priority(self, name:str, priority:int):
def set_plugin_priority(self, name: str, priority: int):
name = name.upper()
if name not in self.plugins:
return False
@@ -174,11 +210,11 @@ class PluginManager:
self.refresh_order()
return True
def enable_plugin(self, name:str):
def enable_plugin(self, name: str):
name = name.upper()
if name not in self.plugins:
return False, "插件不存在"
if not self.plugins[name].enabled :
if not self.plugins[name].enabled:
self.plugins[name].enabled = True
rawname = self.plugins[name].name
self.pconf["plugins"][rawname]["enabled"] = True
@@ -188,43 +224,47 @@ class PluginManager:
return False, "插件开启失败"
return True, "插件已开启"
return True, "插件已开启"
def disable_plugin(self, name:str):
def disable_plugin(self, name: str):
name = name.upper()
if name not in self.plugins:
return False
if self.plugins[name].enabled :
if self.plugins[name].enabled:
self.plugins[name].enabled = False
rawname = self.plugins[name].name
self.pconf["plugins"][rawname]["enabled"] = False
self.save_config()
return True
return True
def list_plugins(self):
return self.plugins
def install_plugin(self, repo:str):
def install_plugin(self, repo: str):
try:
import common.package_manager as pkgmgr
pkgmgr.check_dulwich()
except Exception as e:
logger.error("Failed to install plugin, {}".format(e))
return False, "无法导入dulwich安装插件失败"
import re
from dulwich import porcelain
logger.info("clone git repo: {}".format(repo))
match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
if not match:
try:
with open("./plugins/source.json","r", encoding="utf-8") as f:
with open("./plugins/source.json", "r", encoding="utf-8") as f:
source = json.load(f)
if repo in source["repo"]:
repo = source["repo"][repo]["url"]
match = re.match(r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo)
match = re.match(
r"^(https?:\/\/|git@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$", repo
)
if not match:
return False, "安装插件失败source中的仓库地址不合法"
else:
@@ -232,42 +272,53 @@ class PluginManager:
except Exception as e:
logger.error("Failed to install plugin, {}".format(e))
return False, "安装插件失败,请检查仓库地址是否正确"
dirname = os.path.join("./plugins",match.group(4))
dirname = os.path.join("./plugins", match.group(4))
try:
repo = porcelain.clone(repo, dirname, checkout=True)
if os.path.exists(os.path.join(dirname,"requirements.txt")):
if os.path.exists(os.path.join(dirname, "requirements.txt")):
logger.info("detect requirements.txtinstalling...")
pkgmgr.install_requirements(os.path.join(dirname,"requirements.txt"))
pkgmgr.install_requirements(os.path.join(dirname, "requirements.txt"))
return True, "安装插件成功,请使用 #scanp 命令扫描插件或重启程序,开启前请检查插件是否需要配置"
except Exception as e:
logger.error("Failed to install plugin, {}".format(e))
return False, "安装插件失败,"+str(e)
def update_plugin(self, name:str):
return False, "安装插件失败," + str(e)
def update_plugin(self, name: str):
try:
import common.package_manager as pkgmgr
pkgmgr.check_dulwich()
except Exception as e:
logger.error("Failed to install plugin, {}".format(e))
return False, "无法导入dulwich更新插件失败"
from dulwich import porcelain
name = name.upper()
if name not in self.plugins:
return False, "插件不存在"
if name in ["HELLO","GODCMD","ROLE","TOOL","BDUNIT","BANWORDS","FINISH","DUNGEON"]:
if name in [
"HELLO",
"GODCMD",
"ROLE",
"TOOL",
"BDUNIT",
"BANWORDS",
"FINISH",
"DUNGEON",
]:
return False, "预置插件无法更新,请更新主程序仓库"
dirname = self.plugins[name].path
try:
porcelain.pull(dirname, "origin")
if os.path.exists(os.path.join(dirname,"requirements.txt")):
if os.path.exists(os.path.join(dirname, "requirements.txt")):
logger.info("detect requirements.txtinstalling...")
pkgmgr.install_requirements(os.path.join(dirname,"requirements.txt"))
pkgmgr.install_requirements(os.path.join(dirname, "requirements.txt"))
return True, "更新插件成功,请重新运行程序"
except Exception as e:
logger.error("Failed to update plugin, {}".format(e))
return False, "更新插件失败,"+str(e)
def uninstall_plugin(self, name:str):
return False, "更新插件失败," + str(e)
def uninstall_plugin(self, name: str):
name = name.upper()
if name not in self.plugins:
return False, "插件不存在"
@@ -276,6 +327,7 @@ class PluginManager:
dirname = self.plugins[name].path
try:
import shutil
shutil.rmtree(dirname)
rawname = self.plugins[name].name
for event in self.listening_plugins:
@@ -288,4 +340,4 @@ class PluginManager:
return True, "卸载插件成功"
except Exception as e:
logger.error("Failed to uninstall plugin, {}".format(e))
return False, "卸载插件失败,请手动删除文件夹完成卸载,"+str(e)
return False, "卸载插件失败,请手动删除文件夹完成卸载," + str(e)

View File

@@ -1 +1 @@
from .role import *
from .role import *

View File

@@ -2,17 +2,18 @@
import json
import os
import plugins
from bridge.bridge import Bridge
from bridge.context import ContextType
from bridge.reply import Reply, ReplyType
from common import const
from config import conf
import plugins
from plugins import *
from common.log import logger
from config import conf
from plugins import *
class RolePlay():
class RolePlay:
def __init__(self, bot, sessionid, desc, wrapper=None):
self.bot = bot
self.sessionid = sessionid
@@ -25,12 +26,20 @@ class RolePlay():
def action(self, user_action):
session = self.bot.sessions.build_session(self.sessionid)
if session.system_prompt != self.desc: # 目前没有触发session过期事件这里先简单判断然后重置
if session.system_prompt != self.desc: # 目前没有触发session过期事件这里先简单判断然后重置
session.set_system_prompt(self.desc)
prompt = self.wrapper % user_action
return prompt
@plugins.register(name="Role", desire_priority=0, namecn="角色扮演", desc="为你的Bot设置预设角色", version="1.0", author="lanvent")
@plugins.register(
name="Role",
desire_priority=0,
namecn="角色扮演",
desc="为你的Bot设置预设角色",
version="1.0",
author="lanvent",
)
class Role(Plugin):
def __init__(self):
super().__init__()
@@ -39,7 +48,7 @@ class Role(Plugin):
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
self.tags = { tag:(desc,[]) for tag,desc in config["tags"].items()}
self.tags = {tag: (desc, []) for tag, desc in config["tags"].items()}
self.roles = {}
for role in config["roles"]:
self.roles[role["title"].lower()] = role
@@ -60,12 +69,16 @@ class Role(Plugin):
logger.info("[Role] inited")
except Exception as e:
if isinstance(e, FileNotFoundError):
logger.warn(f"[Role] init failed, {config_path} not found, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/role .")
logger.warn(
f"[Role] init failed, {config_path} not found, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/role ."
)
else:
logger.warn("[Role] init failed, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/role .")
logger.warn(
"[Role] init failed, ignore or see https://github.com/zhayujie/chatgpt-on-wechat/tree/master/plugins/role ."
)
raise e
def get_role(self, name, find_closest=True, min_sim = 0.35):
def get_role(self, name, find_closest=True, min_sim=0.35):
name = name.lower()
found_role = None
if name in self.roles:
@@ -75,6 +88,7 @@ class Role(Plugin):
def str_simularity(a, b):
return difflib.SequenceMatcher(None, a, b).ratio()
max_sim = min_sim
max_role = None
for role in self.roles:
@@ -86,25 +100,24 @@ class Role(Plugin):
return found_role
def on_handle_context(self, e_context: EventContext):
if e_context['context'].type != ContextType.TEXT:
if e_context["context"].type != ContextType.TEXT:
return
bottype = Bridge().get_bot_type("chat")
if bottype not in (const.CHATGPT, const.OPEN_AI):
return
bot = Bridge().get_bot("chat")
content = e_context['context'].content[:]
clist = e_context['context'].content.split(maxsplit=1)
content = e_context["context"].content[:]
clist = e_context["context"].content.split(maxsplit=1)
desckey = None
customize = False
sessionid = e_context['context']['session_id']
trigger_prefix = conf().get('plugin_trigger_prefix', "$")
sessionid = e_context["context"]["session_id"]
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
if clist[0] == f"{trigger_prefix}停止扮演":
if sessionid in self.roleplays:
self.roleplays[sessionid].reset()
del self.roleplays[sessionid]
reply = Reply(ReplyType.INFO, "角色扮演结束!")
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
return
elif clist[0] == f"{trigger_prefix}角色":
@@ -114,10 +127,10 @@ class Role(Plugin):
elif clist[0] == f"{trigger_prefix}设定扮演":
customize = True
elif clist[0] == f"{trigger_prefix}角色类型":
if len(clist) >1:
if len(clist) > 1:
tag = clist[1].strip()
help_text = "角色列表:\n"
for key,value in self.tags.items():
for key, value in self.tags.items():
if value[0] == tag:
tag = key
break
@@ -130,57 +143,75 @@ class Role(Plugin):
else:
help_text = f"未知角色类型。\n"
help_text += "目前的角色类型有: \n"
help_text += "".join([self.tags[tag][0] for tag in self.tags])+"\n"
help_text += (
"".join([self.tags[tag][0] for tag in self.tags]) + "\n"
)
else:
help_text = f"请输入角色类型。\n"
help_text += "目前的角色类型有: \n"
help_text += "".join([self.tags[tag][0] for tag in self.tags])+"\n"
help_text += "".join([self.tags[tag][0] for tag in self.tags]) + "\n"
reply = Reply(ReplyType.INFO, help_text)
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
return
elif sessionid not in self.roleplays:
return
logger.debug("[Role] on_handle_context. content: %s" % content)
if desckey is not None:
if len(clist) == 1 or (len(clist) > 1 and clist[1].lower() in ["help", "帮助"]):
if len(clist) == 1 or (
len(clist) > 1 and clist[1].lower() in ["help", "帮助"]
):
reply = Reply(ReplyType.INFO, self.get_help_text(verbose=True))
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
return
role = self.get_role(clist[1])
if role is None:
reply = Reply(ReplyType.ERROR, "角色不存在")
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
return
else:
self.roleplays[sessionid] = RolePlay(bot, sessionid, self.roles[role][desckey], self.roles[role].get("wrapper","%s"))
reply = Reply(ReplyType.INFO, f"预设角色为 {role}:\n"+self.roles[role][desckey])
e_context['reply'] = reply
self.roleplays[sessionid] = RolePlay(
bot,
sessionid,
self.roles[role][desckey],
self.roles[role].get("wrapper", "%s"),
)
reply = Reply(
ReplyType.INFO, f"预设角色为 {role}:\n" + self.roles[role][desckey]
)
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
elif customize == True:
self.roleplays[sessionid] = RolePlay(bot, sessionid, clist[1], "%s")
reply = Reply(ReplyType.INFO, f"角色设定为:\n{clist[1]}")
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
else:
prompt = self.roleplays[sessionid].action(content)
e_context['context'].type = ContextType.TEXT
e_context['context'].content = prompt
e_context["context"].type = ContextType.TEXT
e_context["context"].content = prompt
e_context.action = EventAction.BREAK
def get_help_text(self, verbose=False, **kwargs):
help_text = "让机器人扮演不同的角色。\n"
if not verbose:
return help_text
trigger_prefix = conf().get('plugin_trigger_prefix', "$")
help_text = f"使用方法:\n{trigger_prefix}角色"+" 预设角色名: 设定角色为{预设角色名}\n"+f"{trigger_prefix}role"+" 预设角色名: 同上,但使用英文设定。\n"
help_text += f"{trigger_prefix}设定扮演"+" 角色设定: 设定自定义角色人设为{角色设定}\n"
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
help_text = (
f"使用方法:\n{trigger_prefix}角色"
+ " 预设角色名: 设定角色为{预设角色名}\n"
+ f"{trigger_prefix}role"
+ " 预设角色名: 同上,但使用英文设定。\n"
)
help_text += f"{trigger_prefix}设定扮演" + " 角色设定: 设定自定义角色人设为{角色设定}\n"
help_text += f"{trigger_prefix}停止扮演: 清除设定的角色。\n"
help_text += f"{trigger_prefix}角色类型"+" 角色类型: 查看某类{角色类型}的所有预设角色,为所有时输出所有预设角色。\n"
help_text += (
f"{trigger_prefix}角色类型" + " 角色类型: 查看某类{角色类型}的所有预设角色,为所有时输出所有预设角色。\n"
)
help_text += "\n目前的角色类型有: \n"
help_text += "".join([self.tags[tag][0] for tag in self.tags])+"\n"
help_text += "".join([self.tags[tag][0] for tag in self.tags]) + "\n"
help_text += f"\n命令例子: \n{trigger_prefix}角色 写作助理\n"
help_text += f"{trigger_prefix}角色类型 所有\n"
help_text += f"{trigger_prefix}停止扮演\n"

View File

@@ -428,4 +428,4 @@
]
}
]
}
}

View File

@@ -1,16 +1,16 @@
{
"repo": {
"sdwebui": {
"url": "https://github.com/lanvent/plugin_sdwebui.git",
"desc": "利用stable-diffusion画图的插件"
},
"replicate": {
"url": "https://github.com/lanvent/plugin_replicate.git",
"desc": "利用replicate api画图的插件"
},
"summary": {
"url": "https://github.com/lanvent/plugin_summary.git",
"desc": "总结聊天记录的插件"
}
"repo": {
"sdwebui": {
"url": "https://github.com/lanvent/plugin_sdwebui.git",
"desc": "利用stable-diffusion画图的插件"
},
"replicate": {
"url": "https://github.com/lanvent/plugin_replicate.git",
"desc": "利用replicate api画图的插件"
},
"summary": {
"url": "https://github.com/lanvent/plugin_summary.git",
"desc": "总结聊天记录的插件"
}
}
}
}

View File

@@ -1,14 +1,14 @@
## 插件描述
一个能让chatgpt联网搜索数字运算的插件将赋予强大且丰富的扩展能力
一个能让chatgpt联网搜索数字运算的插件将赋予强大且丰富的扩展能力
使用该插件需在机器人回复你的前提下,在对话内容前加$tool仅输入$tool将返回tool插件帮助信息用于测试插件是否加载成功
### 本插件所有工具同步存放至专用仓库:[chatgpt-tool-hub](https://github.com/goldfishh/chatgpt-tool-hub)
## 使用说明
使用该插件后将默认使用4个工具, 无需额外配置长期生效:
### 1. python
使用该插件后将默认使用4个工具, 无需额外配置长期生效:
### 1. python
###### python解释器使用它来解释执行python指令可以配合你想要chatgpt生成的代码输出结果或执行事务
### 2. url-get
###### 往往用来获取某个网站具体内容,结果可能会被反爬策略影响
@@ -23,16 +23,16 @@
> meteo调优记录https://github.com/zhayujie/chatgpt-on-wechat/issues/776#issuecomment-1500771334
## 使用本插件对话prompt技巧
### 1. 有指引的询问
## 使用本插件对话prompt技巧
### 1. 有指引的询问
#### 例如:
- 总结这个链接的内容 https://github.com/goldfishh/chatgpt-tool-hub
- 总结这个链接的内容 https://github.com/goldfishh/chatgpt-tool-hub
- 使用Terminal执行curl cip.cc
- 使用python查询今天日期
### 2. 使用搜索引擎工具
- 如果有搜索工具就能让chatgpt获取到你的未传达清楚的上下文信息比如chatgpt不知道你的地理位置现在时间等所以无法查询到天气
## 其他工具
### 5. wikipedia
@@ -55,9 +55,9 @@
### 10. google-search *
###### google搜索引擎申请流程较bing-search繁琐
###### 注1带*工具需要获取api-key才能使用部分工具需要外网支持
###### 注1带*工具需要获取api-key才能使用部分工具需要外网支持
#### [申请方法](https://github.com/goldfishh/chatgpt-tool-hub/blob/master/docs/apply_optional_tool.md)
## config.json 配置说明
###### 默认工具无需配置,其它工具需手动配置,一个例子:
```json
@@ -71,15 +71,15 @@
}
```
config.json文件非必须未创建仍可使用本tool带*工具需在kwargs填入对应api-key键值对
config.json文件非必须未创建仍可使用本tool带*工具需在kwargs填入对应api-key键值对
- `tools`:本插件初始化时加载的工具, 目前可选集:["wikipedia", "wolfram-alpha", "bing-search", "google-search", "news", "morning-news"] & 默认工具除wikipedia工具之外均需要申请api-key
- `kwargs`:工具执行时的配置,一般在这里存放**api-key**,或环境配置
- `request_timeout`: 访问openai接口的超时时间默认与wechat-on-chatgpt配置一致可单独配置
- `no_default`: 用于配置默认加载4个工具的行为如果为true则仅使用tools列表工具不加载默认工具
- `top_k_results`: 控制所有有关搜索的工具返回条目数数字越高则参考信息越多但无用信息可能干扰判断该值一般为2
- `model_name`: 用于控制tool插件底层使用的llm模型目前暂未测试3.5以外的模型,一般保持默认
## 备注
- 强烈建议申请搜索工具搭配使用推荐bing-search
- 虽然我会有意加入一些限制,但请不要使用本插件做危害他人的事情,请提前了解清楚某些内容是否会违反相关规定,建议提前做好过滤

View File

@@ -1 +1 @@
from .tool import *
from .tool import *

View File

@@ -1,8 +1,13 @@
{
"tools": ["python", "url-get", "terminal", "meteo-weather"],
"tools": [
"python",
"url-get",
"terminal",
"meteo-weather"
],
"kwargs": {
"top_k_results": 2,
"no_default": false,
"model_name": "gpt-3.5-turbo"
"top_k_results": 2,
"no_default": false,
"model_name": "gpt-3.5-turbo"
}
}
}

View File

@@ -4,6 +4,7 @@ import os
from chatgpt_tool_hub.apps import load_app
from chatgpt_tool_hub.apps.app import App
from chatgpt_tool_hub.tools.all_tool_list import get_all_tool_names
import plugins
from bridge.bridge import Bridge
from bridge.context import ContextType
@@ -14,7 +15,13 @@ from config import conf
from plugins import *
@plugins.register(name="tool", desc="Arming your ChatGPT bot with various tools", version="0.3", author="goldfishh", desire_priority=0)
@plugins.register(
name="tool",
desc="Arming your ChatGPT bot with various tools",
version="0.3",
author="goldfishh",
desire_priority=0,
)
class Tool(Plugin):
def __init__(self):
super().__init__()
@@ -28,22 +35,26 @@ class Tool(Plugin):
help_text = "这是一个能让chatgpt联网搜索数字运算的插件将赋予强大且丰富的扩展能力。"
if not verbose:
return help_text
trigger_prefix = conf().get('plugin_trigger_prefix', "$")
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
help_text += "使用说明:\n"
help_text += f"{trigger_prefix}tool "+"命令: 根据给出的{命令}使用一些可用工具尽力为你得到结果。\n"
help_text += f"{trigger_prefix}tool " + "命令: 根据给出的{命令}使用一些可用工具尽力为你得到结果。\n"
help_text += f"{trigger_prefix}tool reset: 重置工具。\n"
return help_text
def on_handle_context(self, e_context: EventContext):
if e_context['context'].type != ContextType.TEXT:
if e_context["context"].type != ContextType.TEXT:
return
# 暂时不支持未来扩展的bot
if Bridge().get_bot_type("chat") not in (const.CHATGPT, const.OPEN_AI, const.CHATGPTONAZURE):
if Bridge().get_bot_type("chat") not in (
const.CHATGPT,
const.OPEN_AI,
const.CHATGPTONAZURE,
):
return
content = e_context['context'].content
content_list = e_context['context'].content.split(maxsplit=1)
content = e_context["context"].content
content_list = e_context["context"].content.split(maxsplit=1)
if not content or len(content_list) < 1:
e_context.action = EventAction.CONTINUE
@@ -52,13 +63,13 @@ class Tool(Plugin):
logger.debug("[tool] on_handle_context. content: %s" % content)
reply = Reply()
reply.type = ReplyType.TEXT
trigger_prefix = conf().get('plugin_trigger_prefix', "$")
trigger_prefix = conf().get("plugin_trigger_prefix", "$")
# todo: 有些工具必须要api-key需要修改config文件所以这里没有实现query增删tool的功能
if content.startswith(f"{trigger_prefix}tool"):
if len(content_list) == 1:
logger.debug("[tool]: get help")
reply.content = self.get_help_text()
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
return
elif len(content_list) > 1:
@@ -66,12 +77,14 @@ class Tool(Plugin):
logger.debug("[tool]: reset config")
self.app = self._reset_app()
reply.content = "重置工具成功"
e_context['reply'] = reply
e_context["reply"] = reply
e_context.action = EventAction.BREAK_PASS
return
elif content_list[1].startswith("reset"):
logger.debug("[tool]: remind")
e_context['context'].content = "请你随机用一种聊天风格提醒用户如果想重置tool插件reset之后不要加任何字符"
e_context[
"context"
].content = "请你随机用一种聊天风格提醒用户如果想重置tool插件reset之后不要加任何字符"
e_context.action = EventAction.BREAK
return
@@ -80,34 +93,35 @@ class Tool(Plugin):
# Don't modify bot name
all_sessions = Bridge().get_bot("chat").sessions
user_session = all_sessions.session_query(query, e_context['context']['session_id']).messages
user_session = all_sessions.session_query(
query, e_context["context"]["session_id"]
).messages
# chatgpt-tool-hub will reply you with many tools
logger.debug("[tool]: just-go")
try:
_reply = self.app.ask(query, user_session)
e_context.action = EventAction.BREAK_PASS
all_sessions.session_reply(_reply, e_context['context']['session_id'])
all_sessions.session_reply(
_reply, e_context["context"]["session_id"]
)
except Exception as e:
logger.exception(e)
logger.error(str(e))
e_context['context'].content = "请你随机用一种聊天风格提醒用户这个问题tool插件暂时无法处理"
e_context["context"].content = "请你随机用一种聊天风格提醒用户这个问题tool插件暂时无法处理"
reply.type = ReplyType.ERROR
e_context.action = EventAction.BREAK
return
reply.content = _reply
e_context['reply'] = reply
e_context["reply"] = reply
return
def _read_json(self) -> dict:
curdir = os.path.dirname(__file__)
config_path = os.path.join(curdir, "config.json")
tool_config = {
"tools": [],
"kwargs": {}
}
tool_config = {"tools": [], "kwargs": {}}
if not os.path.exists(config_path):
return tool_config
else:
@@ -123,7 +137,9 @@ class Tool(Plugin):
"proxy": conf().get("proxy", ""),
"request_timeout": conf().get("request_timeout", 60),
# note: 目前tool暂未对其他模型测试但这里仍对配置来源做了优先级区分一般插件配置可覆盖全局配置
"model_name": tool_model_name if tool_model_name else conf().get("model", "gpt-3.5-turbo"),
"model_name": tool_model_name
if tool_model_name
else conf().get("model", "gpt-3.5-turbo"),
"no_default": kwargs.get("no_default", False),
"top_k_results": kwargs.get("top_k_results", 2),
# for news tool
@@ -160,4 +176,7 @@ class Tool(Plugin):
# filter not support tool
tool_list = self._filter_tool_list(tool_config.get("tools", []))
return load_app(tools_list=tool_list, **self._build_tool_kwargs(tool_config.get("kwargs", {})))
return load_app(
tools_list=tool_list,
**self._build_tool_kwargs(tool_config.get("kwargs", {})),
)