nonebot-plugin-keyreply 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonebot-plugin-keyreply
3
+ Version: 0.1.4
4
+ Summary: 基于NoneBot2的轻量级关键词自动回复插件
5
+ Keywords: nonebot,nonebot2,keyreply,reply
6
+ Author: yuexps@qq.com
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: nonebot-adapter-onebot (>=2.4.0,<3.0.0)
17
+ Requires-Dist: nonebot-plugin-localstore (>=0.6.0,<1.0.0)
18
+ Requires-Dist: nonebot2 (>=2.2.0,<3.0.0)
19
+ Project-URL: Homepage, https://github.com/yuexps/nonebot-plugin-keyreply
20
+ Project-URL: Repository, https://github.com/yuexps/nonebot-plugin-keyreply
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center">
24
+ <a href="https://adapter-onebot.netlify.app/"><img src="https://img.shields.io/badge/nonebot2-plugin-red.svg" alt="nonebot2"></a>
25
+ <a href="https://pypi.org/project/nonebot-plugin-keyreply"><img src="https://img.shields.io/pypi/v/nonebot-plugin-keyreply.svg" alt="pypi"></a>
26
+ <img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
27
+ </p>
28
+
29
+ # nonebot-plugin-keyreply
30
+
31
+ 基于 NoneBot2 的轻量级关键词自动回复插件。
32
+
33
+ ## 特性
34
+
35
+ - **群聊隔离**:各群聊及私聊数据完全独立,互不干扰。
36
+ - **多种匹配**:支持 精确匹配(默认)、模糊匹配、正则匹配。
37
+ - **权限受控**:管理指令限超级用户(`SUPERUSER`)或群管理(`GROUP_ADMIN | GROUP_OWNER`)执行。
38
+
39
+ ## 安装
40
+
41
+ 推荐使用 `nb-cli` 安装:
42
+ ```bash
43
+ nb plugin install nonebot-plugin-keyreply
44
+ ```
45
+ <details>
46
+ <summary>或使用包管理器</summary>
47
+
48
+ ```bash
49
+ pip install nonebot-plugin-keyreply
50
+ ```
51
+ 并在 `pyproject.toml` 或 `bot.py` 中加载插件:
52
+ ```toml
53
+ plugins = ["nonebot_plugin_keyreply"]
54
+ ```
55
+ </details>
56
+
57
+ ## 指令说明
58
+
59
+ 指令前缀默认为 `/reply`(实际取决于 `COMMAND_START` 配置)。
60
+
61
+ | 指令格式 | 参数说明 | 匹配范围参数 | 示例 |
62
+ | :--- | :--- | :--- | :--- |
63
+ | `/reply add <关键词> <回复>` | `-f` 模糊匹配<br>`-r` 正则匹配 | `-g` 全局词条<br>`-p` 私聊词条 | `/reply add 菜单 "群菜单内容"`<br>`/reply add -f -g 帮助 帮助文档`<br>`/reply add -r -p "hi\|hello" 你好` |
64
+ | `/reply edit <关键词> <新回复>` | - | `-g` 全局 / `-p` 私聊 | `/reply edit 菜单 "新菜单"` |
65
+ | `/reply del <关键词>` | - | `-g` 全局 / `-p` 私聊 | `/reply del 菜单` |
66
+ | `/reply list [关键词]` | - | `-g` 全局 / `-p` 私聊 | `/reply list`<br>`/reply list 菜单` |
67
+
68
+ > [!IMPORTANT]
69
+ > 添加、修改和删除全局(`-g`)与私聊(`-p`)规则仅限超级用户(`SUPERUSER`)执行,查询列表(`list`)不受此限制。
70
+ > 如果关键词或回复内容**包含空格**,请使用双引号包裹,例如:`/reply add "早上 好" "您好!"`
71
+
@@ -0,0 +1,48 @@
1
+ <p align="center">
2
+ <a href="https://adapter-onebot.netlify.app/"><img src="https://img.shields.io/badge/nonebot2-plugin-red.svg" alt="nonebot2"></a>
3
+ <a href="https://pypi.org/project/nonebot-plugin-keyreply"><img src="https://img.shields.io/pypi/v/nonebot-plugin-keyreply.svg" alt="pypi"></a>
4
+ <img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
5
+ </p>
6
+
7
+ # nonebot-plugin-keyreply
8
+
9
+ 基于 NoneBot2 的轻量级关键词自动回复插件。
10
+
11
+ ## 特性
12
+
13
+ - **群聊隔离**:各群聊及私聊数据完全独立,互不干扰。
14
+ - **多种匹配**:支持 精确匹配(默认)、模糊匹配、正则匹配。
15
+ - **权限受控**:管理指令限超级用户(`SUPERUSER`)或群管理(`GROUP_ADMIN | GROUP_OWNER`)执行。
16
+
17
+ ## 安装
18
+
19
+ 推荐使用 `nb-cli` 安装:
20
+ ```bash
21
+ nb plugin install nonebot-plugin-keyreply
22
+ ```
23
+ <details>
24
+ <summary>或使用包管理器</summary>
25
+
26
+ ```bash
27
+ pip install nonebot-plugin-keyreply
28
+ ```
29
+ 并在 `pyproject.toml` 或 `bot.py` 中加载插件:
30
+ ```toml
31
+ plugins = ["nonebot_plugin_keyreply"]
32
+ ```
33
+ </details>
34
+
35
+ ## 指令说明
36
+
37
+ 指令前缀默认为 `/reply`(实际取决于 `COMMAND_START` 配置)。
38
+
39
+ | 指令格式 | 参数说明 | 匹配范围参数 | 示例 |
40
+ | :--- | :--- | :--- | :--- |
41
+ | `/reply add <关键词> <回复>` | `-f` 模糊匹配<br>`-r` 正则匹配 | `-g` 全局词条<br>`-p` 私聊词条 | `/reply add 菜单 "群菜单内容"`<br>`/reply add -f -g 帮助 帮助文档`<br>`/reply add -r -p "hi\|hello" 你好` |
42
+ | `/reply edit <关键词> <新回复>` | - | `-g` 全局 / `-p` 私聊 | `/reply edit 菜单 "新菜单"` |
43
+ | `/reply del <关键词>` | - | `-g` 全局 / `-p` 私聊 | `/reply del 菜单` |
44
+ | `/reply list [关键词]` | - | `-g` 全局 / `-p` 私聊 | `/reply list`<br>`/reply list 菜单` |
45
+
46
+ > [!IMPORTANT]
47
+ > 添加、修改和删除全局(`-g`)与私聊(`-p`)规则仅限超级用户(`SUPERUSER`)执行,查询列表(`list`)不受此限制。
48
+ > 如果关键词或回复内容**包含空格**,请使用双引号包裹,例如:`/reply add "早上 好" "您好!"`
@@ -0,0 +1,277 @@
1
+ import shlex
2
+ from nonebot import on_command, on_message, require
3
+ from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent
4
+ from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
5
+ from nonebot.params import CommandArg
6
+ from nonebot.permission import SUPERUSER
7
+ from nonebot.plugin import PluginMetadata
8
+
9
+ require("nonebot_plugin_localstore")
10
+ import nonebot_plugin_localstore as store
11
+
12
+ from .rule_manager import RuleManager
13
+
14
+ __plugin_meta__ = PluginMetadata(
15
+ name="KeyReply",
16
+ description="基于 NoneBot2 的轻量级关键词自动回复插件",
17
+ usage=(
18
+ "管理指令:/reply\n"
19
+ "1. 添加:/reply add [-f 模糊 | -r 正则] [-g 全局 | -p 私聊] <关键词> <回复内容>\n"
20
+ "2. 修改:/reply edit [-g 全局 | -p 私聊] <关键词> <新回复内容>\n"
21
+ "3. 删除:/reply del [-g 全局 | -p 私聊] <关键词>\n"
22
+ "4. 列表/查看:/reply list [-g 全局 | -p 私聊] [关键词]"
23
+ ),
24
+ type="application",
25
+ homepage="https://github.com/yuexps/nonebot-plugin-keyreply",
26
+ supported_adapters={"~onebot.v11"},
27
+ )
28
+
29
+ rule_manager = RuleManager(store.get_plugin_data_file("rules.json"))
30
+
31
+ # 管理命令
32
+ reply_cmd = on_command(
33
+ "reply",
34
+ permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER,
35
+ priority=5,
36
+ block=True,
37
+ )
38
+
39
+
40
+ @reply_cmd.handle()
41
+ async def handle_reply(bot: Bot, event: MessageEvent, command_arg: Message = CommandArg()):
42
+ # 提取参数文本
43
+ args_str = command_arg.extract_plain_text().strip()
44
+ if not args_str:
45
+ await reply_cmd.finish(__plugin_meta__.usage)
46
+
47
+ # 解析参数
48
+ try:
49
+ args = shlex.split(args_str)
50
+ except ValueError as e:
51
+ await reply_cmd.finish(f"参数解析错误:{str(e)}(请检查双引号是否闭合)")
52
+
53
+ sub_cmd = args[0].lower()
54
+ is_superuser = await SUPERUSER(bot, event)
55
+
56
+ if sub_cmd == "add":
57
+ match_type = "exact"
58
+ is_global = False
59
+ is_private = False
60
+ clean_args = []
61
+ for arg in args[1:]:
62
+ if arg in ("-f", "--fuzzy"):
63
+ match_type = "fuzzy"
64
+ elif arg in ("-r", "--regex"):
65
+ match_type = "regex"
66
+ elif arg in ("-g", "--global"):
67
+ is_global = True
68
+ elif arg in ("-p", "--private"):
69
+ is_private = True
70
+ else:
71
+ clean_args.append(arg)
72
+
73
+ if len(clean_args) < 2:
74
+ await reply_cmd.finish("格式错误:reply add [-f|-r] [-g|-p] <关键词> <回复内容>")
75
+
76
+ if is_global and is_private:
77
+ await reply_cmd.finish("格式错误:不能同时指定全局(-g)和私聊(-p)")
78
+
79
+ if (is_global or is_private) and not is_superuser:
80
+ await reply_cmd.finish("权限不足:仅超级用户有权配置全局/私聊词条")
81
+
82
+ if is_global:
83
+ group_id = "global"
84
+ elif is_private:
85
+ group_id = "private"
86
+ else:
87
+ group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else "private"
88
+
89
+ key = clean_args[0]
90
+ reply = " ".join(clean_args[1:])
91
+
92
+ match_types = {"exact": "精确匹配", "fuzzy": "模糊匹配", "regex": "正则匹配"}
93
+ rule_manager.add_rule(key, reply, match_type, group_id)
94
+ scope_map = {"global": "全局", "private": "私聊"}
95
+ scope = scope_map.get(group_id, f"群 {group_id}")
96
+ await reply_cmd.finish(f"添加成功![{scope}] 关键词「{key}」-> 「{reply}」[类型: {match_types.get(match_type, match_type)}]")
97
+
98
+ elif sub_cmd == "edit":
99
+ is_global = False
100
+ is_private = False
101
+ clean_args = []
102
+ for arg in args[1:]:
103
+ if arg in ("-g", "--global"):
104
+ is_global = True
105
+ elif arg in ("-p", "--private"):
106
+ is_private = True
107
+ else:
108
+ clean_args.append(arg)
109
+
110
+ if len(clean_args) < 2:
111
+ await reply_cmd.finish("格式错误:reply edit [-g|-p] <关键词> <新回复内容>")
112
+
113
+ if is_global and is_private:
114
+ await reply_cmd.finish("格式错误:不能同时指定全局(-g)和私聊(-p)")
115
+
116
+ if (is_global or is_private) and not is_superuser:
117
+ await reply_cmd.finish("权限不足:仅超级用户有权配置全局/私聊词条")
118
+
119
+ if is_global:
120
+ group_id = "global"
121
+ elif is_private:
122
+ group_id = "private"
123
+ else:
124
+ group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else "private"
125
+
126
+ key = clean_args[0]
127
+ reply = " ".join(clean_args[1:])
128
+
129
+ success = rule_manager.edit_rule(key, reply, group_id)
130
+ if success:
131
+ scope_map = {"global": "全局", "private": "私聊"}
132
+ scope = scope_map.get(group_id, f"群 {group_id}")
133
+ await reply_cmd.finish(f"修改成功!已覆盖 [{scope}] 关键词「{key}」的回复")
134
+ else:
135
+ await reply_cmd.finish(f"修改失败:未找到该范围内对应的关键词「{key}」")
136
+
137
+ elif sub_cmd == "del":
138
+ is_global = False
139
+ is_private = False
140
+ clean_args = []
141
+ for arg in args[1:]:
142
+ if arg in ("-g", "--global"):
143
+ is_global = True
144
+ elif arg in ("-p", "--private"):
145
+ is_private = True
146
+ else:
147
+ clean_args.append(arg)
148
+
149
+ if len(clean_args) < 1:
150
+ await reply_cmd.finish("格式错误:reply del [-g|-p] <关键词>")
151
+
152
+ if is_global and is_private:
153
+ await reply_cmd.finish("格式错误:不能同时指定全局(-g)和私聊(-p)")
154
+
155
+ if (is_global or is_private) and not is_superuser:
156
+ await reply_cmd.finish("权限不足:仅超级用户有权配置全局/私聊词条")
157
+
158
+ if is_global:
159
+ group_id = "global"
160
+ elif is_private:
161
+ group_id = "private"
162
+ else:
163
+ group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else "private"
164
+
165
+ key = clean_args[0]
166
+
167
+ success = rule_manager.del_rule(key, group_id)
168
+ if success:
169
+ scope_map = {"global": "全局", "private": "私聊"}
170
+ scope = scope_map.get(group_id, f"群 {group_id}")
171
+ await reply_cmd.finish(f"删除成功!已移除 [{scope}] 关键词「{key}」")
172
+ else:
173
+ await reply_cmd.finish(f"删除失败:未找到该范围内对应的关键词「{key}」")
174
+
175
+ elif sub_cmd == "list":
176
+ is_global = False
177
+ is_private = False
178
+ clean_args = []
179
+ for arg in args[1:]:
180
+ if arg in ("-g", "--global"):
181
+ is_global = True
182
+ elif arg in ("-p", "--private"):
183
+ is_private = True
184
+ else:
185
+ clean_args.append(arg)
186
+
187
+ if is_global and is_private:
188
+ await reply_cmd.finish("格式错误:不能同时指定全局(-g)和私聊(-p)")
189
+
190
+ if is_global:
191
+ group_id = "global"
192
+ elif is_private:
193
+ group_id = "private"
194
+ else:
195
+ group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else "private"
196
+
197
+ if len(clean_args) == 0:
198
+ if is_global or group_id == "global":
199
+ rules = rule_manager.get_rules_for_group("global")
200
+ if not rules:
201
+ await reply_cmd.finish("当前无生效的全局词条")
202
+ lines = [f"- {r.key}" for r in rules]
203
+ await reply_cmd.finish("当前生效的全局词条关键词列表:\n" + "\n".join(lines))
204
+ elif is_private or group_id == "private":
205
+ private_rules = rule_manager.get_rules_for_group("private")
206
+ global_rules = rule_manager.get_rules_for_group("global")
207
+ if not private_rules and not global_rules:
208
+ await reply_cmd.finish("当前无生效的私聊词条")
209
+
210
+ msg_parts = []
211
+ if private_rules:
212
+ msg_parts.append("【私聊词条】")
213
+ msg_parts.extend(f"- {r.key}" for r in private_rules)
214
+ if global_rules:
215
+ if msg_parts:
216
+ msg_parts.append("")
217
+ msg_parts.append("【全局词条】")
218
+ msg_parts.extend(f"- {r.key}" for r in global_rules)
219
+
220
+ await reply_cmd.finish("当前生效的词条关键词列表:\n" + "\n".join(msg_parts))
221
+ else:
222
+ group_rules = rule_manager.get_rules_for_group(group_id)
223
+ global_rules = rule_manager.get_rules_for_group("global")
224
+ if not group_rules and not global_rules:
225
+ await reply_cmd.finish("当前无生效的词条")
226
+
227
+ msg_parts = []
228
+ if group_rules:
229
+ msg_parts.append("【本群词条】")
230
+ msg_parts.extend(f"- {r.key}" for r in group_rules)
231
+ if global_rules:
232
+ if msg_parts:
233
+ msg_parts.append("")
234
+ msg_parts.append("【全局词条】")
235
+ msg_parts.extend(f"- {r.key}" for r in global_rules)
236
+
237
+ await reply_cmd.finish("当前生效的词条关键词列表:\n" + "\n".join(msg_parts))
238
+ else:
239
+ # 查询指定关键词
240
+ key = clean_args[0]
241
+ rule = rule_manager.get_rule(key, group_id)
242
+ if not rule and group_id != "global":
243
+ # 在当前上下文没找到时,也去全局找一下
244
+ rule = rule_manager.get_rule(key, "global")
245
+
246
+ if rule:
247
+ scope_map = {"global": "全局", "private": "私聊"}
248
+ scope = scope_map.get(rule.group_id, f"群 {rule.group_id}")
249
+ match_types = {"exact": "精确匹配", "fuzzy": "模糊匹配", "regex": "正则匹配"}
250
+ reply_text = (
251
+ f"词条信息:\n"
252
+ f"关键词:{rule.key}\n"
253
+ f"回复内容:{rule.reply}\n"
254
+ f"匹配类型:{match_types.get(rule.match_type, rule.match_type)}\n"
255
+ f"生效范围:{scope}"
256
+ )
257
+ await reply_cmd.finish(reply_text)
258
+ else:
259
+ await reply_cmd.finish(f"未找到对应的关键词「{key}」")
260
+ else:
261
+ await reply_cmd.finish(f"未知子命令:{sub_cmd}\n{__plugin_meta__.usage}")
262
+
263
+
264
+ # 自动回复监听器
265
+ message_reply = on_message(priority=99, block=False)
266
+
267
+
268
+ @message_reply.handle()
269
+ async def handle_message(bot: Bot, event: MessageEvent):
270
+ text = event.get_plaintext().strip()
271
+ if not text:
272
+ return
273
+
274
+ group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else "private"
275
+ matched = rule_manager.match(text, group_id)
276
+ if matched:
277
+ await message_reply.send(message=matched.reply)
@@ -0,0 +1,136 @@
1
+ import json
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Dict, List, Literal, Optional
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class ReplyRule(BaseModel):
9
+ key: str
10
+ reply: str
11
+ match_type: Literal["exact", "fuzzy", "regex"] = "exact"
12
+ group_id: str = "global"
13
+
14
+
15
+ class RuleManager:
16
+ def __init__(self, data_path: Path):
17
+ self.data_path = data_path
18
+ self._rules: Dict[str, List[ReplyRule]] = {}
19
+ self.load()
20
+
21
+ @property
22
+ def rules(self) -> List[ReplyRule]:
23
+ """获取扁平化的所有规则列表"""
24
+ all_rules = []
25
+ for group_rules in self._rules.values():
26
+ all_rules.extend(group_rules)
27
+ return all_rules
28
+
29
+ def load(self) -> None:
30
+ """从本地文件加载规则字典"""
31
+ if not self.data_path.exists():
32
+ self._rules = {}
33
+ return
34
+ try:
35
+ with open(self.data_path, "r", encoding="utf-8") as f:
36
+ data = json.load(f)
37
+ self._rules = {}
38
+ if isinstance(data, dict):
39
+ for group_id, items in data.items():
40
+ rules_list = []
41
+ for item in items:
42
+ item["group_id"] = group_id
43
+ rules_list.append(ReplyRule(**item))
44
+ self._rules[group_id] = rules_list
45
+ except Exception:
46
+ self._rules = {}
47
+
48
+ def save(self) -> None:
49
+ """保存规则字典到本地文件"""
50
+ try:
51
+ self.data_path.parent.mkdir(parents=True, exist_ok=True)
52
+ with open(self.data_path, "w", encoding="utf-8") as f:
53
+ data_to_save = {}
54
+ for group_id, rules in self._rules.items():
55
+ group_rules = []
56
+ for r in rules:
57
+ if hasattr(r, "model_dump"):
58
+ r_dict = r.model_dump()
59
+ else:
60
+ r_dict = r.dict()
61
+ r_dict.pop("group_id", None)
62
+ group_rules.append(r_dict)
63
+ data_to_save[group_id] = group_rules
64
+ json.dump(data_to_save, f, ensure_ascii=False, indent=2)
65
+ except Exception:
66
+ pass
67
+
68
+ def add_rule(self, key: str, reply: str, match_type: Literal["exact", "fuzzy", "regex"], group_id: str) -> None:
69
+ """添加或覆盖规则"""
70
+ if group_id not in self._rules:
71
+ self._rules[group_id] = []
72
+ self._rules[group_id] = [r for r in self._rules[group_id] if r.key != key]
73
+ self._rules[group_id].append(ReplyRule(key=key, reply=reply, match_type=match_type, group_id=group_id))
74
+ self.save()
75
+
76
+ def edit_rule(self, key: str, reply: str, group_id: str) -> bool:
77
+ """修改规则,若不存在则返回 False"""
78
+ if group_id in self._rules:
79
+ for r in self._rules[group_id]:
80
+ if r.key == key:
81
+ r.reply = reply
82
+ self.save()
83
+ return True
84
+ return False
85
+
86
+ def del_rule(self, key: str, group_id: str) -> bool:
87
+ """删除指定规则,若不存在则返回 False"""
88
+ if group_id not in self._rules:
89
+ return False
90
+ original_len = len(self._rules[group_id])
91
+ self._rules[group_id] = [r for r in self._rules[group_id] if r.key != key]
92
+ if len(self._rules[group_id]) < original_len:
93
+ if not self._rules[group_id]:
94
+ del self._rules[group_id]
95
+ self.save()
96
+ return True
97
+ return False
98
+
99
+ def get_rule(self, key: str, group_id: str) -> Optional[ReplyRule]:
100
+ """获取特定的规则"""
101
+ if group_id in self._rules:
102
+ for r in self._rules[group_id]:
103
+ if r.key == key:
104
+ return r
105
+ return None
106
+
107
+ def get_rules_for_group(self, group_id: str) -> List[ReplyRule]:
108
+ """获取指定群/范围生效的规则"""
109
+ return self._rules.get(group_id, [])
110
+
111
+ def _match_rules(self, text: str, rules: List[ReplyRule]) -> Optional[ReplyRule]:
112
+ """匹配规则列表"""
113
+ for rule in rules:
114
+ if rule.match_type == "exact":
115
+ if text == rule.key:
116
+ return rule
117
+ elif rule.match_type == "fuzzy":
118
+ if rule.key in text:
119
+ return rule
120
+ elif rule.match_type == "regex":
121
+ try:
122
+ if re.search(rule.key, text):
123
+ return rule
124
+ except re.error:
125
+ continue
126
+ return None
127
+
128
+ def match(self, text: str, group_id: str) -> Optional[ReplyRule]:
129
+ """匹配规则,群聊未中时回退全局"""
130
+ matched = self._match_rules(text, self.get_rules_for_group(group_id))
131
+ if matched:
132
+ return matched
133
+ if group_id != "global":
134
+ return self._match_rules(text, self.get_rules_for_group("global"))
135
+ return None
136
+
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "nonebot-plugin-keyreply"
3
- version = "0.1.3"
4
- description = "根据设定好的关键词进行自动回复词条的插件"
3
+ version = "0.1.4"
4
+ description = "基于NoneBot2的轻量级关键词自动回复插件"
5
5
  authors = ["yuexps@qq.com"]
6
6
  readme = "README.md"
7
7
  homepage = "https://github.com/yuexps/nonebot-plugin-keyreply"
@@ -23,6 +23,7 @@ packages = [
23
23
  python = "^3.9"
24
24
  nonebot2 = "^2.2.0"
25
25
  nonebot-adapter-onebot = "^2.4.0"
26
+ nonebot-plugin-localstore = ">=0.6.0,<1.0.0"
26
27
 
27
28
  [build-system]
28
29
  requires = ["poetry-core>=1.0.0"]
@@ -1,103 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: nonebot-plugin-keyreply
3
- Version: 0.1.3
4
- Summary: 根据设定好的关键词进行自动回复词条的插件
5
- Keywords: nonebot,nonebot2,keyreply,reply
6
- Author: yuexps@qq.com
7
- Requires-Python: >=3.9,<4.0
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.9
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Classifier: Programming Language :: Python :: 3.14
16
- Requires-Dist: nonebot-adapter-onebot (>=2.4.0,<3.0.0)
17
- Requires-Dist: nonebot2 (>=2.2.0,<3.0.0)
18
- Project-URL: Homepage, https://github.com/yuexps/nonebot-plugin-keyreply
19
- Project-URL: Repository, https://github.com/yuexps/nonebot-plugin-keyreply
20
- Description-Content-Type: text/markdown
21
-
22
- # KeyReply 插件
23
-
24
- Nonebot2 根据关键词自动回复设定词条的插件。
25
-
26
- ## 安装
27
-
28
- ### 使用 nb-cli 安装 (推荐)
29
- 在你的 NoneBot 项目根目录下运行:
30
- ```bash
31
- nb plugin install nonebot-plugin-keyreply
32
- ```
33
-
34
- <details>
35
- <summary><b>使用包管理器安装</b></summary>
36
-
37
- 根据你使用的包管理器,在 NoneBot 项目中运行:
38
-
39
- * **pip**:
40
- ```bash
41
- pip install nonebot-plugin-keyreply
42
- ```
43
- * **pdm**:
44
- ```bash
45
- pdm add nonebot-plugin-keyreply
46
- ```
47
- * **poetry**:
48
- ```bash
49
- poetry add nonebot-plugin-keyreply
50
- ```
51
-
52
- 随后在项目的配置文件中(如 `pyproject.toml` 的 `plugins` 列表中)添加:
53
- ```toml
54
- plugins = ["nonebot_plugin_keyreply"]
55
- ```
56
- </details>
57
-
58
- ## 核心特性
59
-
60
- - **群聊隔离**:群聊自动回复仅匹配当前群配置的专属词条,各群数据完全隔离,不互相干扰。
61
- - **多种匹配模式**:支持精确匹配(默认)、模糊匹配(包含匹配)以及正则表达式匹配。
62
- - **管理权限受控**:词条的添加、修改和删除指令仅限超级用户(`SUPERUSER`)或群管理员/群主(`GROUP_ADMIN | GROUP_OWNER`)执行。
63
-
64
- ## 指令说明
65
-
66
- 所有指令前缀默认为 `/reply`(实际前缀取决于您的 Nonebot 配置文件中的 `COMMAND_START` 设定)。
67
-
68
- ### 1. 添加词条
69
- * **指令格式**:`/reply add [-f|-r] [-g] <关键词> <回复内容>`
70
- * **参数说明**:
71
- - `-f` / `--fuzzy`:设置为模糊匹配(即消息中包含该关键词即可触发回复)。
72
- - `-r` / `--regex`:设置为正则表达式匹配(消息内容符合该正则表达式即可触发回复)。
73
- - `-g` / `--global`:设置为全局词条(仅超级用户可配置)。若在群聊中不加此参数,词条将仅在当前群生效;若在私聊中配置,默认即为全局生效。
74
- * **双引号规范**:
75
- - 如果关键词或回复内容**不含空格**,直接以空格分隔即可:
76
- `/reply add 测试 收到`
77
- - 如果关键词或回复内容中**包含空格**,请使用双引号包裹:
78
- `/reply add "早上 好" "您好!今天也是元气满满的一天!"`
79
-
80
- ### 2. 修改词条(覆盖)
81
- * **指令格式**:`/reply edit [-g] <关键词> <新回复内容>`
82
- * **说明**:覆盖修改指定关键词的回复内容。
83
-
84
- ### 3. 删除词条
85
- * **指令格式**:`/reply del [-g] <关键词>`
86
- * **说明**:删除指定关键词的回复规则。
87
-
88
- ### 4. 列表与详情查询
89
- * **指令格式**:
90
- - `/reply list [-g]`:列出当前生效的所有词条关键词列表。
91
- - `/reply list [-g] <关键词>`:查看指定关键词的匹配规则、具体回复内容与生效范围等详情。
92
-
93
- ---
94
-
95
- ## 插件配置项
96
-
97
- 您可以在 Nonebot2 的 `.env.*` 配置文件中添加以下配置:
98
-
99
- ```env
100
- # 词条规则保存的文件路径(相对于项目根目录)
101
- KEYREPLY_DATA_PATH="data/keyreply/rules.json"
102
- ```
103
-
@@ -1,81 +0,0 @@
1
- # KeyReply 插件
2
-
3
- Nonebot2 根据关键词自动回复设定词条的插件。
4
-
5
- ## 安装
6
-
7
- ### 使用 nb-cli 安装 (推荐)
8
- 在你的 NoneBot 项目根目录下运行:
9
- ```bash
10
- nb plugin install nonebot-plugin-keyreply
11
- ```
12
-
13
- <details>
14
- <summary><b>使用包管理器安装</b></summary>
15
-
16
- 根据你使用的包管理器,在 NoneBot 项目中运行:
17
-
18
- * **pip**:
19
- ```bash
20
- pip install nonebot-plugin-keyreply
21
- ```
22
- * **pdm**:
23
- ```bash
24
- pdm add nonebot-plugin-keyreply
25
- ```
26
- * **poetry**:
27
- ```bash
28
- poetry add nonebot-plugin-keyreply
29
- ```
30
-
31
- 随后在项目的配置文件中(如 `pyproject.toml` 的 `plugins` 列表中)添加:
32
- ```toml
33
- plugins = ["nonebot_plugin_keyreply"]
34
- ```
35
- </details>
36
-
37
- ## 核心特性
38
-
39
- - **群聊隔离**:群聊自动回复仅匹配当前群配置的专属词条,各群数据完全隔离,不互相干扰。
40
- - **多种匹配模式**:支持精确匹配(默认)、模糊匹配(包含匹配)以及正则表达式匹配。
41
- - **管理权限受控**:词条的添加、修改和删除指令仅限超级用户(`SUPERUSER`)或群管理员/群主(`GROUP_ADMIN | GROUP_OWNER`)执行。
42
-
43
- ## 指令说明
44
-
45
- 所有指令前缀默认为 `/reply`(实际前缀取决于您的 Nonebot 配置文件中的 `COMMAND_START` 设定)。
46
-
47
- ### 1. 添加词条
48
- * **指令格式**:`/reply add [-f|-r] [-g] <关键词> <回复内容>`
49
- * **参数说明**:
50
- - `-f` / `--fuzzy`:设置为模糊匹配(即消息中包含该关键词即可触发回复)。
51
- - `-r` / `--regex`:设置为正则表达式匹配(消息内容符合该正则表达式即可触发回复)。
52
- - `-g` / `--global`:设置为全局词条(仅超级用户可配置)。若在群聊中不加此参数,词条将仅在当前群生效;若在私聊中配置,默认即为全局生效。
53
- * **双引号规范**:
54
- - 如果关键词或回复内容**不含空格**,直接以空格分隔即可:
55
- `/reply add 测试 收到`
56
- - 如果关键词或回复内容中**包含空格**,请使用双引号包裹:
57
- `/reply add "早上 好" "您好!今天也是元气满满的一天!"`
58
-
59
- ### 2. 修改词条(覆盖)
60
- * **指令格式**:`/reply edit [-g] <关键词> <新回复内容>`
61
- * **说明**:覆盖修改指定关键词的回复内容。
62
-
63
- ### 3. 删除词条
64
- * **指令格式**:`/reply del [-g] <关键词>`
65
- * **说明**:删除指定关键词的回复规则。
66
-
67
- ### 4. 列表与详情查询
68
- * **指令格式**:
69
- - `/reply list [-g]`:列出当前生效的所有词条关键词列表。
70
- - `/reply list [-g] <关键词>`:查看指定关键词的匹配规则、具体回复内容与生效范围等详情。
71
-
72
- ---
73
-
74
- ## 插件配置项
75
-
76
- 您可以在 Nonebot2 的 `.env.*` 配置文件中添加以下配置:
77
-
78
- ```env
79
- # 词条规则保存的文件路径(相对于项目根目录)
80
- KEYREPLY_DATA_PATH="data/keyreply/rules.json"
81
- ```
@@ -1,200 +0,0 @@
1
- import shlex
2
- from nonebot import get_plugin_config, on_command, on_message
3
- from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent
4
- from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
5
- from nonebot.params import CommandArg
6
- from nonebot.permission import SUPERUSER
7
- from nonebot.plugin import PluginMetadata
8
-
9
- from .config import Config
10
- from .rule_manager import RuleManager
11
-
12
- __plugin_meta__ = PluginMetadata(
13
- name="KeyReply",
14
- description="根据设定好的关键词进行自动回复词条的插件",
15
- usage=(
16
- "管理指令:/reply\n"
17
- "1. 添加:/reply add [-f 模糊 | -r 正则] [-g 全局] <关键词> <回复内容>\n"
18
- "2. 修改:/reply edit [-g 全局] <关键词> <新回复内容>\n"
19
- "3. 删除:/reply del [-g 全局] <关键词>\n"
20
- "4. 列表/查看:/reply list [-g 全局] [关键词]"
21
- ),
22
- type="application",
23
- homepage="https://github.com/yuexps/nonebot-plugin-keyreply",
24
- config=Config,
25
- supported_adapters={"~onebot.v11"},
26
- )
27
-
28
- config = get_plugin_config(Config)
29
- rule_manager = RuleManager(config.keyreply_data_path)
30
-
31
- # 管理命令
32
- reply_cmd = on_command(
33
- "reply",
34
- permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER,
35
- priority=5,
36
- block=True,
37
- )
38
-
39
-
40
- @reply_cmd.handle()
41
- async def handle_reply(bot: Bot, event: MessageEvent, command_arg: Message = CommandArg()):
42
- # 提取参数文本
43
- args_str = command_arg.extract_plain_text().strip()
44
- if not args_str:
45
- await reply_cmd.finish(__plugin_meta__.usage)
46
-
47
- # 解析参数
48
- try:
49
- args = shlex.split(args_str)
50
- except ValueError as e:
51
- await reply_cmd.finish(f"参数解析错误:{str(e)}(请检查双引号是否闭合)")
52
-
53
- sub_cmd = args[0].lower()
54
- is_superuser = await SUPERUSER(bot, event)
55
-
56
- if sub_cmd == "add":
57
- # 解析参数
58
- match_type = "exact"
59
- is_global = False
60
- clean_args = []
61
- for arg in args[1:]:
62
- if arg in ("-f", "--fuzzy"):
63
- match_type = "fuzzy"
64
- elif arg in ("-r", "--regex"):
65
- match_type = "regex"
66
- elif arg in ("-g", "--global"):
67
- is_global = True
68
- else:
69
- clean_args.append(arg)
70
-
71
- if len(clean_args) < 2:
72
- await reply_cmd.finish("格式错误:reply add [-f|-r] [-g] <关键词> <回复内容>")
73
-
74
- if is_global and not is_superuser:
75
- await reply_cmd.finish("权限不足:仅超级用户有权配置全局词条")
76
-
77
- group_id = "global" if (is_global or not isinstance(event, GroupMessageEvent)) else str(event.group_id)
78
- key = clean_args[0]
79
- reply = " ".join(clean_args[1:])
80
-
81
- match_types = {"exact": "精确匹配", "fuzzy": "模糊匹配", "regex": "正则匹配"}
82
- rule_manager.add_rule(key, reply, match_type, group_id)
83
- scope = "全局" if group_id == "global" else f"群 {group_id}"
84
- await reply_cmd.finish(f"添加成功![{scope}] 关键词「{key}」-> 「{reply}」[类型: {match_types.get(match_type, match_type)}]")
85
-
86
- elif sub_cmd == "edit":
87
- is_global = False
88
- clean_args = []
89
- for arg in args[1:]:
90
- if arg in ("-g", "--global"):
91
- is_global = True
92
- else:
93
- clean_args.append(arg)
94
-
95
- if len(clean_args) < 2:
96
- await reply_cmd.finish("格式错误:reply edit [-g] <关键词> <新回复内容>")
97
-
98
- if is_global and not is_superuser:
99
- await reply_cmd.finish("权限不足:仅超级用户有权配置全局词条")
100
-
101
- group_id = "global" if (is_global or not isinstance(event, GroupMessageEvent)) else str(event.group_id)
102
- key = clean_args[0]
103
- reply = " ".join(clean_args[1:])
104
-
105
- success = rule_manager.edit_rule(key, reply, group_id)
106
- if success:
107
- scope = "全局" if group_id == "global" else f"群 {group_id}"
108
- await reply_cmd.finish(f"修改成功!已覆盖 [{scope}] 关键词「{key}」的回复")
109
- else:
110
- await reply_cmd.finish(f"修改失败:未找到该范围内对应的关键词「{key}」")
111
-
112
- elif sub_cmd == "del":
113
- is_global = False
114
- clean_args = []
115
- for arg in args[1:]:
116
- if arg in ("-g", "--global"):
117
- is_global = True
118
- else:
119
- clean_args.append(arg)
120
-
121
- if len(clean_args) < 1:
122
- await reply_cmd.finish("格式错误:reply del [-g] <关键词>")
123
-
124
- if is_global and not is_superuser:
125
- await reply_cmd.finish("权限不足:仅超级用户有权配置全局词条")
126
-
127
- group_id = "global" if (is_global or not isinstance(event, GroupMessageEvent)) else str(event.group_id)
128
- key = clean_args[0]
129
-
130
- success = rule_manager.del_rule(key, group_id)
131
- if success:
132
- scope = "全局" if group_id == "global" else f"群 {group_id}"
133
- await reply_cmd.finish(f"删除成功!已移除 [{scope}] 关键词「{key}」")
134
- else:
135
- await reply_cmd.finish(f"删除失败:未找到该范围内对应的关键词「{key}」")
136
-
137
- elif sub_cmd == "list":
138
- is_global = False
139
- clean_args = []
140
- for arg in args[1:]:
141
- if arg in ("-g", "--global"):
142
- is_global = True
143
- else:
144
- clean_args.append(arg)
145
-
146
- group_id = "global" if (is_global or not isinstance(event, GroupMessageEvent)) else str(event.group_id)
147
-
148
- if len(clean_args) == 0:
149
- # 获取当前上下文生效的词条列表
150
- if is_global:
151
- rules = [r for r in rule_manager.rules if r.group_id == "global"]
152
- else:
153
- rules = rule_manager.get_rules_for_group(group_id)
154
-
155
- if not rules:
156
- await reply_cmd.finish("当前无生效 of 词条")
157
-
158
- lines = []
159
- for r in rules:
160
- lines.append(f"- {r.key}")
161
- await reply_cmd.finish("当前生效的词条关键词列表:\n" + "\n".join(lines))
162
- else:
163
- # 查询指定关键词
164
- key = clean_args[0]
165
- rule = rule_manager.get_rule(key, group_id)
166
- if not rule and group_id != "global":
167
- # 在当前群没找到时,也去全局找一下
168
- rule = rule_manager.get_rule(key, "global")
169
-
170
- if rule:
171
- scope = "全局" if rule.group_id == "global" else f"群 {rule.group_id}"
172
- match_types = {"exact": "精确匹配", "fuzzy": "模糊匹配", "regex": "正则匹配"}
173
- reply_text = (
174
- f"词条信息:\n"
175
- f"关键词:{rule.key}\n"
176
- f"回复内容:{rule.reply}\n"
177
- f"匹配类型:{match_types.get(rule.match_type, rule.match_type)}\n"
178
- f"生效范围:{scope}"
179
- )
180
- await reply_cmd.finish(reply_text)
181
- else:
182
- await reply_cmd.finish(f"未找到对应的关键词「{key}」")
183
- else:
184
- await reply_cmd.finish(f"未知子命令:{sub_cmd}\n{__plugin_meta__.usage}")
185
-
186
-
187
- # 自动回复监听器
188
- message_reply = on_message(priority=99, block=False)
189
-
190
-
191
- @message_reply.handle()
192
- async def handle_message(bot: Bot, event: MessageEvent):
193
- text = event.get_plaintext().strip()
194
- if not text:
195
- return
196
-
197
- group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else "global"
198
- matched = rule_manager.match(text, group_id)
199
- if matched:
200
- await message_reply.send(message=matched.reply)
@@ -1,7 +0,0 @@
1
- from pydantic import BaseModel
2
- from pathlib import Path
3
-
4
-
5
- class Config(BaseModel):
6
- """Plugin Config Here"""
7
- keyreply_data_path: Path = Path("data/keyreply/rules.json")
@@ -1,102 +0,0 @@
1
- import json
2
- import re
3
- from pathlib import Path
4
- from typing import List, Literal, Optional
5
- from pydantic import BaseModel
6
-
7
-
8
- class ReplyRule(BaseModel):
9
- key: str
10
- reply: str
11
- match_type: Literal["exact", "fuzzy", "regex"] = "exact"
12
- group_id: str = "global"
13
-
14
-
15
- class RuleManager:
16
- def __init__(self, data_path: Path):
17
- self.data_path = data_path
18
- self.rules: List[ReplyRule] = []
19
- self.load()
20
-
21
- def load(self) -> None:
22
- """从本地文件加载规则列表"""
23
- if not self.data_path.exists():
24
- self.rules = []
25
- return
26
- try:
27
- with open(self.data_path, "r", encoding="utf-8") as f:
28
- data = json.load(f)
29
- self.rules = [ReplyRule(**item) for item in data]
30
- except Exception:
31
- self.rules = []
32
-
33
- def save(self) -> None:
34
- """保存规则列表到本地文件"""
35
- try:
36
- self.data_path.parent.mkdir(parents=True, exist_ok=True)
37
- with open(self.data_path, "w", encoding="utf-8") as f:
38
- # 兼容 Pydantic v1 & v2
39
- rules_data = []
40
- for r in self.rules:
41
- if hasattr(r, "model_dump"):
42
- rules_data.append(r.model_dump())
43
- else:
44
- rules_data.append(r.dict())
45
- json.dump(rules_data, f, ensure_ascii=False, indent=2)
46
- except Exception:
47
- pass
48
-
49
- def add_rule(self, key: str, reply: str, match_type: Literal["exact", "fuzzy", "regex"], group_id: str) -> None:
50
- """添加或覆盖规则"""
51
- # 移除已有的同群同关键词规则
52
- self.rules = [r for r in self.rules if not (r.key == key and r.group_id == group_id)]
53
- self.rules.append(ReplyRule(key=key, reply=reply, match_type=match_type, group_id=group_id))
54
- self.save()
55
-
56
- def edit_rule(self, key: str, reply: str, group_id: str) -> bool:
57
- """修改规则,若不存在则返回 False"""
58
- for r in self.rules:
59
- if r.key == key and r.group_id == group_id:
60
- r.reply = reply
61
- self.save()
62
- return True
63
- return False
64
-
65
- def del_rule(self, key: str, group_id: str) -> bool:
66
- """删除指定规则,若不存在则返回 False"""
67
- original_len = len(self.rules)
68
- self.rules = [r for r in self.rules if not (r.key == key and r.group_id == group_id)]
69
- if len(self.rules) < original_len:
70
- self.save()
71
- return True
72
- return False
73
-
74
- def get_rule(self, key: str, group_id: str) -> Optional[ReplyRule]:
75
- """获取特定的规则"""
76
- for r in self.rules:
77
- if r.key == key and r.group_id == group_id:
78
- return r
79
- return None
80
-
81
- def get_rules_for_group(self, group_id: str) -> List[ReplyRule]:
82
- """获取指定群/范围生效的规则"""
83
- return [r for r in self.rules if r.group_id == group_id]
84
-
85
- def match(self, text: str, group_id: str) -> Optional[ReplyRule]:
86
- """根据输入文本匹配首个符合条件的规则"""
87
- active_rules = self.get_rules_for_group(group_id)
88
- for rule in active_rules:
89
- if rule.match_type == "exact":
90
- if text == rule.key:
91
- return rule
92
- elif rule.match_type == "fuzzy":
93
- if rule.key in text:
94
- return rule
95
- elif rule.match_type == "regex":
96
- try:
97
- if re.search(rule.key, text):
98
- return rule
99
- except re.error:
100
- # 忽略无效的正则表达式
101
- continue
102
- return None