You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

440 lines
19KB

  1. # encoding:utf-8
  2. import json
  3. import os
  4. import random
  5. import string
  6. import traceback
  7. from typing import Tuple
  8. import plugins
  9. from bridge.bridge import Bridge
  10. from bridge.context import ContextType
  11. from bridge.reply import Reply, ReplyType
  12. from common import const
  13. from common.log import logger
  14. from config import conf, load_config, global_config
  15. from plugins import *
  16. # 定义指令集
  17. COMMANDS = {
  18. "help": {
  19. "alias": ["help", "帮助"],
  20. "desc": "回复此帮助",
  21. },
  22. "helpp": {
  23. "alias": ["help", "帮助"], # 与help指令共用别名,根据参数数量区分
  24. "args": ["插件名"],
  25. "desc": "回复指定插件的详细帮助",
  26. },
  27. "auth": {
  28. "alias": ["auth", "认证"],
  29. "args": ["口令"],
  30. "desc": "管理员认证",
  31. },
  32. "set_openai_api_key": {
  33. "alias": ["set_openai_api_key"],
  34. "args": ["api_key"],
  35. "desc": "设置你的OpenAI私有api_key",
  36. },
  37. "reset_openai_api_key": {
  38. "alias": ["reset_openai_api_key"],
  39. "desc": "重置为默认的api_key",
  40. },
  41. "set_gpt_model": {
  42. "alias": ["set_gpt_model"],
  43. "desc": "设置你的私有模型",
  44. },
  45. "reset_gpt_model": {
  46. "alias": ["reset_gpt_model"],
  47. "desc": "重置你的私有模型",
  48. },
  49. "gpt_model": {
  50. "alias": ["gpt_model"],
  51. "desc": "查询你使用的模型",
  52. },
  53. "id": {
  54. "alias": ["id", "用户"],
  55. "desc": "获取用户id", # wechaty和wechatmp的用户id不会变化,可用于绑定管理员
  56. },
  57. "reset": {
  58. "alias": ["reset", "重置会话"],
  59. "desc": "重置会话",
  60. },
  61. }
  62. ADMIN_COMMANDS = {
  63. "resume": {
  64. "alias": ["resume", "恢复服务"],
  65. "desc": "恢复服务",
  66. },
  67. "stop": {
  68. "alias": ["stop", "暂停服务"],
  69. "desc": "暂停服务",
  70. },
  71. "reconf": {
  72. "alias": ["reconf", "重载配置"],
  73. "desc": "重载配置(不包含插件配置)",
  74. },
  75. "resetall": {
  76. "alias": ["resetall", "重置所有会话"],
  77. "desc": "重置所有会话",
  78. },
  79. "scanp": {
  80. "alias": ["scanp", "扫描插件"],
  81. "desc": "扫描插件目录是否有新插件",
  82. },
  83. "plist": {
  84. "alias": ["plist", "插件"],
  85. "desc": "打印当前插件列表",
  86. },
  87. "setpri": {
  88. "alias": ["setpri", "设置插件优先级"],
  89. "args": ["插件名", "优先级"],
  90. "desc": "设置指定插件的优先级,越大越优先",
  91. },
  92. "reloadp": {
  93. "alias": ["reloadp", "重载插件"],
  94. "args": ["插件名"],
  95. "desc": "重载指定插件配置",
  96. },
  97. "enablep": {
  98. "alias": ["enablep", "启用插件"],
  99. "args": ["插件名"],
  100. "desc": "启用指定插件",
  101. },
  102. "disablep": {
  103. "alias": ["disablep", "禁用插件"],
  104. "args": ["插件名"],
  105. "desc": "禁用指定插件",
  106. },
  107. "installp": {
  108. "alias": ["installp", "安装插件"],
  109. "args": ["仓库地址或插件名"],
  110. "desc": "安装指定插件",
  111. },
  112. "uninstallp": {
  113. "alias": ["uninstallp", "卸载插件"],
  114. "args": ["插件名"],
  115. "desc": "卸载指定插件",
  116. },
  117. "updatep": {
  118. "alias": ["updatep", "更新插件"],
  119. "args": ["插件名"],
  120. "desc": "更新指定插件",
  121. },
  122. "debug": {
  123. "alias": ["debug", "调试模式", "DEBUG"],
  124. "desc": "开启机器调试日志",
  125. },
  126. }
  127. # 定义帮助函数
  128. def get_help_text(isadmin, isgroup):
  129. help_text = "通用指令:\n"
  130. for cmd, info in COMMANDS.items():
  131. if cmd == "auth": # 不提示认证指令
  132. continue
  133. if cmd == "id" and conf().get("channel_type", "wx") not in ["wxy", "wechatmp"]:
  134. continue
  135. alias = ["#" + a for a in info["alias"][:1]]
  136. help_text += f"{','.join(alias)} "
  137. if "args" in info:
  138. args = [a for a in info["args"]]
  139. help_text += f"{' '.join(args)}"
  140. help_text += f": {info['desc']}\n"
  141. # 插件指令
  142. plugins = PluginManager().list_plugins()
  143. help_text += "\n目前可用插件有:"
  144. for plugin in plugins:
  145. if plugins[plugin].enabled and not plugins[plugin].hidden:
  146. namecn = plugins[plugin].namecn
  147. help_text += "\n%s:" % namecn
  148. help_text += PluginManager().instances[plugin].get_help_text(verbose=False).strip()
  149. if ADMIN_COMMANDS and isadmin:
  150. help_text += "\n\n管理员指令:\n"
  151. for cmd, info in ADMIN_COMMANDS.items():
  152. alias = ["#" + a for a in info["alias"][:1]]
  153. help_text += f"{','.join(alias)} "
  154. if "args" in info:
  155. args = [a for a in info["args"]]
  156. help_text += f"{' '.join(args)}"
  157. help_text += f": {info['desc']}\n"
  158. return help_text
  159. @plugins.register(
  160. name="Godcmd",
  161. desire_priority=999,
  162. hidden=True,
  163. desc="为你的机器人添加指令集,有用户和管理员两种角色,加载顺序请放在首位,初次运行后插件目录会生成配置文件, 填充管理员密码后即可认证",
  164. version="1.0",
  165. author="lanvent",
  166. )
  167. class Godcmd(Plugin):
  168. def __init__(self):
  169. super().__init__()
  170. config_path = os.path.join(os.path.dirname(__file__), "config.json")
  171. gconf = super().load_config()
  172. if not gconf:
  173. if not os.path.exists(config_path):
  174. gconf = {"password": "", "admin_users": []}
  175. with open(config_path, "w") as f:
  176. json.dump(gconf, f, indent=4)
  177. if gconf["password"] == "":
  178. self.temp_password = "".join(random.sample(string.digits, 4))
  179. logger.info("[Godcmd] 因未设置口令,本次的临时口令为%s。" % self.temp_password)
  180. else:
  181. self.temp_password = None
  182. custom_commands = conf().get("clear_memory_commands", [])
  183. for custom_command in custom_commands:
  184. if custom_command and custom_command.startswith("#"):
  185. custom_command = custom_command[1:]
  186. if custom_command and custom_command not in COMMANDS["reset"]["alias"]:
  187. COMMANDS["reset"]["alias"].append(custom_command)
  188. self.password = gconf["password"]
  189. self.admin_users = gconf["admin_users"] # 预存的管理员账号,这些账号不需要认证。itchat的用户名每次都会变,不可用
  190. self.isrunning = True # 机器人是否运行中
  191. self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context
  192. logger.info("[Godcmd] inited")
  193. def on_handle_context(self, e_context: EventContext):
  194. context_type = e_context["context"].type
  195. if context_type != ContextType.TEXT:
  196. if not self.isrunning:
  197. e_context.action = EventAction.BREAK_PASS
  198. return
  199. content = e_context["context"].content
  200. logger.debug("[Godcmd] on_handle_context. content: %s" % content)
  201. if content.startswith("#"):
  202. if len(content) == 1:
  203. reply = Reply()
  204. reply.type = ReplyType.ERROR
  205. reply.content = f"空指令,输入#help查看指令列表\n"
  206. e_context["reply"] = reply
  207. e_context.action = EventAction.BREAK_PASS
  208. return
  209. # msg = e_context['context']['msg']
  210. channel = e_context["channel"]
  211. user = e_context["context"]["receiver"]
  212. session_id = e_context["context"]["session_id"]
  213. isgroup = e_context["context"].get("isgroup", False)
  214. bottype = Bridge().get_bot_type("chat")
  215. bot = Bridge().get_bot("chat")
  216. # 将命令和参数分割
  217. command_parts = content[1:].strip().split()
  218. cmd = command_parts[0]
  219. args = command_parts[1:]
  220. isadmin = False
  221. if user in self.admin_users:
  222. isadmin = True
  223. ok = False
  224. result = "string"
  225. if any(cmd in info["alias"] for info in COMMANDS.values()):
  226. cmd = next(c for c, info in COMMANDS.items() if cmd in info["alias"])
  227. if cmd == "auth":
  228. ok, result = self.authenticate(user, args, isadmin, isgroup)
  229. elif cmd == "help" or cmd == "helpp":
  230. if len(args) == 0:
  231. ok, result = True, get_help_text(isadmin, isgroup)
  232. else:
  233. # This can replace the helpp command
  234. plugins = PluginManager().list_plugins()
  235. query_name = args[0].upper()
  236. # search name and namecn
  237. for name, plugincls in plugins.items():
  238. if not plugincls.enabled:
  239. continue
  240. if query_name == name or query_name == plugincls.namecn:
  241. ok, result = True, PluginManager().instances[name].get_help_text(isgroup=isgroup, isadmin=isadmin, verbose=True)
  242. break
  243. if not ok:
  244. result = "插件不存在或未启用"
  245. elif cmd == "id":
  246. ok, result = True, user
  247. elif cmd == "set_openai_api_key":
  248. if len(args) == 1:
  249. user_data = conf().get_user_data(user)
  250. user_data["openai_api_key"] = args[0]
  251. ok, result = True, "你的OpenAI私有api_key已设置为" + args[0]
  252. else:
  253. ok, result = False, "请提供一个api_key"
  254. elif cmd == "reset_openai_api_key":
  255. try:
  256. user_data = conf().get_user_data(user)
  257. user_data.pop("openai_api_key")
  258. ok, result = True, "你的OpenAI私有api_key已清除"
  259. except Exception as e:
  260. ok, result = False, "你没有设置私有api_key"
  261. elif cmd == "set_gpt_model":
  262. if len(args) == 1:
  263. user_data = conf().get_user_data(user)
  264. user_data["gpt_model"] = args[0]
  265. ok, result = True, "你的GPT模型已设置为" + args[0]
  266. else:
  267. ok, result = False, "请提供一个GPT模型"
  268. elif cmd == "gpt_model":
  269. user_data = conf().get_user_data(user)
  270. model = conf().get("model")
  271. if "gpt_model" in user_data:
  272. model = user_data["gpt_model"]
  273. ok, result = True, "你的GPT模型为" + str(model)
  274. elif cmd == "reset_gpt_model":
  275. try:
  276. user_data = conf().get_user_data(user)
  277. user_data.pop("gpt_model")
  278. ok, result = True, "你的GPT模型已重置"
  279. except Exception as e:
  280. ok, result = False, "你没有设置私有GPT模型"
  281. elif cmd == "reset":
  282. if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI]:
  283. bot.sessions.clear_session(session_id)
  284. channel.cancel_session(session_id)
  285. ok, result = True, "会话已重置"
  286. else:
  287. ok, result = False, "当前对话机器人不支持重置会话"
  288. logger.debug("[Godcmd] command: %s by %s" % (cmd, user))
  289. elif any(cmd in info["alias"] for info in ADMIN_COMMANDS.values()):
  290. if isadmin:
  291. if isgroup:
  292. ok, result = False, "群聊不可执行管理员指令"
  293. else:
  294. cmd = next(c for c, info in ADMIN_COMMANDS.items() if cmd in info["alias"])
  295. if cmd == "stop":
  296. self.isrunning = False
  297. ok, result = True, "服务已暂停"
  298. elif cmd == "resume":
  299. self.isrunning = True
  300. ok, result = True, "服务已恢复"
  301. elif cmd == "reconf":
  302. load_config()
  303. ok, result = True, "配置已重载"
  304. elif cmd == "resetall":
  305. if bottype in [const.OPEN_AI, const.CHATGPT, const.CHATGPTONAZURE, const.LINKAI]:
  306. channel.cancel_all_session()
  307. bot.sessions.clear_all_session()
  308. ok, result = True, "重置所有会话成功"
  309. else:
  310. ok, result = False, "当前对话机器人不支持重置会话"
  311. elif cmd == "debug":
  312. logger.setLevel("DEBUG")
  313. ok, result = True, "DEBUG模式已开启"
  314. elif cmd == "plist":
  315. plugins = PluginManager().list_plugins()
  316. ok = True
  317. result = "插件列表:\n"
  318. for name, plugincls in plugins.items():
  319. result += f"{plugincls.name}_v{plugincls.version} {plugincls.priority} - "
  320. if plugincls.enabled:
  321. result += "已启用\n"
  322. else:
  323. result += "未启用\n"
  324. elif cmd == "scanp":
  325. new_plugins = PluginManager().scan_plugins()
  326. ok, result = True, "插件扫描完成"
  327. PluginManager().activate_plugins()
  328. if len(new_plugins) > 0:
  329. result += "\n发现新插件:\n"
  330. result += "\n".join([f"{p.name}_v{p.version}" for p in new_plugins])
  331. else:
  332. result += ", 未发现新插件"
  333. elif cmd == "setpri":
  334. if len(args) != 2:
  335. ok, result = False, "请提供插件名和优先级"
  336. else:
  337. ok = PluginManager().set_plugin_priority(args[0], int(args[1]))
  338. if ok:
  339. result = "插件" + args[0] + "优先级已设置为" + args[1]
  340. else:
  341. result = "插件不存在"
  342. elif cmd == "reloadp":
  343. if len(args) != 1:
  344. ok, result = False, "请提供插件名"
  345. else:
  346. ok = PluginManager().reload_plugin(args[0])
  347. if ok:
  348. result = "插件配置已重载"
  349. else:
  350. result = "插件不存在"
  351. elif cmd == "enablep":
  352. if len(args) != 1:
  353. ok, result = False, "请提供插件名"
  354. else:
  355. ok, result = PluginManager().enable_plugin(args[0])
  356. elif cmd == "disablep":
  357. if len(args) != 1:
  358. ok, result = False, "请提供插件名"
  359. else:
  360. ok = PluginManager().disable_plugin(args[0])
  361. if ok:
  362. result = "插件已禁用"
  363. else:
  364. result = "插件不存在"
  365. elif cmd == "installp":
  366. if len(args) != 1:
  367. ok, result = False, "请提供插件名或.git结尾的仓库地址"
  368. else:
  369. ok, result = PluginManager().install_plugin(args[0])
  370. elif cmd == "uninstallp":
  371. if len(args) != 1:
  372. ok, result = False, "请提供插件名"
  373. else:
  374. ok, result = PluginManager().uninstall_plugin(args[0])
  375. elif cmd == "updatep":
  376. if len(args) != 1:
  377. ok, result = False, "请提供插件名"
  378. else:
  379. ok, result = PluginManager().update_plugin(args[0])
  380. logger.debug("[Godcmd] admin command: %s by %s" % (cmd, user))
  381. else:
  382. ok, result = False, "需要管理员权限才能执行该指令"
  383. else:
  384. trigger_prefix = conf().get("plugin_trigger_prefix", "$")
  385. if trigger_prefix == "#": # 跟插件聊天指令前缀相同,继续递交
  386. return
  387. ok, result = False, f"未知指令:{cmd}\n查看指令列表请输入#help \n"
  388. reply = Reply()
  389. if ok:
  390. reply.type = ReplyType.INFO
  391. else:
  392. reply.type = ReplyType.ERROR
  393. reply.content = result
  394. e_context["reply"] = reply
  395. e_context.action = EventAction.BREAK_PASS # 事件结束,并跳过处理context的默认逻辑
  396. elif not self.isrunning:
  397. e_context.action = EventAction.BREAK_PASS
  398. def authenticate(self, userid, args, isadmin, isgroup) -> Tuple[bool, str]:
  399. if isgroup:
  400. return False, "请勿在群聊中认证"
  401. if isadmin:
  402. return False, "管理员账号无需认证"
  403. if len(args) != 1:
  404. return False, "请提供口令"
  405. password = args[0]
  406. if password == self.password:
  407. self.admin_users.append(userid)
  408. global_config["admin_users"].append(userid)
  409. return True, "认证成功"
  410. elif password == self.temp_password:
  411. self.admin_users.append(userid)
  412. global_config["admin_users"].append(userid)
  413. return True, "认证成功,请尽快设置口令"
  414. else:
  415. return False, "认证失败"
  416. def get_help_text(self, isadmin=False, isgroup=False, **kwargs):
  417. return get_help_text(isadmin, isgroup)