mofox-plugin-dev-toolkit 0.2.1__py3-none-any.whl

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.
Files changed (43) hide show
  1. mofox_plugin_dev_toolkit-0.2.1.dist-info/METADATA +409 -0
  2. mofox_plugin_dev_toolkit-0.2.1.dist-info/RECORD +43 -0
  3. mofox_plugin_dev_toolkit-0.2.1.dist-info/WHEEL +5 -0
  4. mofox_plugin_dev_toolkit-0.2.1.dist-info/entry_points.txt +2 -0
  5. mofox_plugin_dev_toolkit-0.2.1.dist-info/licenses/LICENSE +674 -0
  6. mofox_plugin_dev_toolkit-0.2.1.dist-info/top_level.txt +1 -0
  7. mpdt/__init__.py +15 -0
  8. mpdt/__main__.py +8 -0
  9. mpdt/cli.py +314 -0
  10. mpdt/commands/__init__.py +9 -0
  11. mpdt/commands/check.py +316 -0
  12. mpdt/commands/dev.py +550 -0
  13. mpdt/commands/generate.py +366 -0
  14. mpdt/commands/init.py +487 -0
  15. mpdt/dev/bridge_plugin/__init__.py +17 -0
  16. mpdt/dev/bridge_plugin/discovery_server.py +126 -0
  17. mpdt/dev/bridge_plugin/plugin.py +258 -0
  18. mpdt/templates/__init__.py +165 -0
  19. mpdt/templates/action_template.py +102 -0
  20. mpdt/templates/adapter_template.py +129 -0
  21. mpdt/templates/chatter_template.py +103 -0
  22. mpdt/templates/event_template.py +116 -0
  23. mpdt/templates/plus_command_template.py +150 -0
  24. mpdt/templates/prompt_template.py +92 -0
  25. mpdt/templates/router_template.py +175 -0
  26. mpdt/templates/tool_template.py +98 -0
  27. mpdt/utils/__init__.py +10 -0
  28. mpdt/utils/color_printer.py +99 -0
  29. mpdt/utils/config_loader.py +171 -0
  30. mpdt/utils/config_manager.py +297 -0
  31. mpdt/utils/file_ops.py +203 -0
  32. mpdt/utils/license_generator.py +980 -0
  33. mpdt/utils/plugin_parser.py +196 -0
  34. mpdt/utils/template_engine.py +112 -0
  35. mpdt/validators/__init__.py +26 -0
  36. mpdt/validators/auto_fix_validator.py +182 -0
  37. mpdt/validators/base.py +121 -0
  38. mpdt/validators/component_validator.py +415 -0
  39. mpdt/validators/config_validator.py +173 -0
  40. mpdt/validators/metadata_validator.py +125 -0
  41. mpdt/validators/structure_validator.py +70 -0
  42. mpdt/validators/style_validator.py +125 -0
  43. mpdt/validators/type_validator.py +223 -0
@@ -0,0 +1,415 @@
1
+ """
2
+ 组件验证器
3
+ """
4
+
5
+ import ast
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from .base import BaseValidator, ValidationResult
10
+
11
+
12
+ class ComponentValidator(BaseValidator):
13
+ """组件验证器
14
+
15
+ 通过解析 plugin.py 中的 get_plugin_components() 方法,
16
+ 找到所有组件类,然后检查每个组件类是否有必需的元数据。
17
+ """
18
+
19
+ # 不同组件类型的必需元数据
20
+ # 注意:根据 MMC 基类定义,各组件使用不同的属性名:
21
+ # - BaseTool: name, description
22
+ # - BaseCommand/PlusCommand: command_name, command_description
23
+ # - BaseAction: action_name, action_description
24
+ # - BaseEventHandler: handler_name, handler_description
25
+ # - BaseAdapter: adapter_name, adapter_description
26
+ # - BasePrompt: prompt_name (无 prompt_description)
27
+ # - BaseRouterComponent: component_name, component_description
28
+ COMPONENT_REQUIRED_FIELDS = {
29
+ "Action": ["action_name", "action_description"],
30
+ "BaseAction": ["action_name", "action_description"],
31
+ "Command": ["command_name", "command_description"],
32
+ "BaseCommand": ["command_name", "command_description"],
33
+ "PlusCommand": ["command_name", "command_description"],
34
+ "Tool": ["name", "description"],
35
+ "BaseTool": ["name", "description"],
36
+ "EventHandler": ["handler_name", "handler_description"],
37
+ "BaseEventHandler": ["handler_name", "handler_description"],
38
+ "Adapter": ["adapter_name", "adapter_description"],
39
+ "BaseAdapter": ["adapter_name", "adapter_description"],
40
+ "Prompt": ["prompt_name"],
41
+ "BasePrompt": ["prompt_name"],
42
+ "Chatter": ["chatter_name", "chatter_description"],
43
+ "BaseChatter": ["chatter_name", "chatter_description"],
44
+ "Router": ["component_name", "component_description"],
45
+ "BaseRouterComponent": ["component_name", "component_description"],
46
+ }
47
+
48
+ def validate(self) -> ValidationResult:
49
+ """执行组件验证
50
+
51
+ Returns:
52
+ ValidationResult: 验证结果
53
+ """
54
+ # 获取插件名称
55
+ plugin_name = self._get_plugin_name()
56
+ if not plugin_name:
57
+ self.result.add_error("无法确定插件名称")
58
+ return self.result
59
+
60
+ plugin_dir = self.plugin_path
61
+ plugin_file = plugin_dir / "plugin.py"
62
+
63
+ if not plugin_file.exists():
64
+ self.result.add_error("插件文件不存在: plugin.py")
65
+ return self.result
66
+
67
+ # 解析 plugin.py 获取组件信息
68
+ components = self._extract_components_from_plugin(plugin_file, plugin_name)
69
+
70
+ if not components:
71
+ self.result.add_warning(
72
+ "未找到任何组件注册",
73
+ file_path="plugin.py",
74
+ suggestion="请在 get_plugin_components() 方法中注册组件",
75
+ )
76
+ return self.result
77
+
78
+ # 验证每个组件
79
+ for component_info in components:
80
+ self._validate_component(component_info, plugin_dir, plugin_name)
81
+
82
+ return self.result
83
+
84
+ def _extract_components_from_plugin(self, plugin_file: Path, plugin_name: str) -> list[dict]:
85
+ """从 plugin.py 中提取组件信息
86
+
87
+ Args:
88
+ plugin_file: plugin.py 文件路径
89
+ plugin_name: 插件名称
90
+
91
+ Returns:
92
+ 组件信息列表,每个元素包含: {
93
+ 'class_name': 组件类名,
94
+ 'base_class': 基类名称,
95
+ 'import_from': 导入来源(相对路径)
96
+ }
97
+ """
98
+ try:
99
+ with open(plugin_file, encoding="utf-8") as f:
100
+ tree = ast.parse(f.read(), filename=str(plugin_file))
101
+ except Exception as e:
102
+ self.result.add_error(f"解析 plugin.py 失败: {e}")
103
+ return []
104
+
105
+ components = []
106
+
107
+ # 收集所有导入的组件类
108
+ imports = self._collect_imports(tree, plugin_name)
109
+
110
+ # 查找 get_plugin_components 方法
111
+ for node in ast.walk(tree):
112
+ if isinstance(node, ast.FunctionDef) and node.name == "get_plugin_components":
113
+ # 分析函数体,查找 components.append() 调用
114
+ components.extend(self._extract_components_from_function(node, imports))
115
+
116
+ return components
117
+
118
+ def _extract_components_from_function(self, func_node: ast.FunctionDef, imports: dict[str, str]) -> list[dict]:
119
+ """从 get_plugin_components 函数中提取组件信息
120
+
121
+ Args:
122
+ func_node: 函数定义节点
123
+ imports: 导入映射
124
+
125
+ Returns:
126
+ 组件信息列表
127
+ """
128
+ components = []
129
+
130
+ # 遍历函数体,查找 components.append(...) 或直接 return [...]
131
+ for stmt in func_node.body:
132
+ # 情况1: components.append((ComponentInfo, ComponentClass))
133
+ if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
134
+ call = stmt.value
135
+ # 检查是否是 .append() 调用
136
+ if isinstance(call.func, ast.Attribute) and call.func.attr == "append":
137
+ # 获取 append 的参数(应该是一个元组)
138
+ if call.args:
139
+ component = self._extract_component_from_tuple(call.args[0], imports)
140
+ if component:
141
+ components.append(component)
142
+
143
+ # 情况2: return [(...), (...), ...]
144
+ elif isinstance(stmt, ast.Return) and stmt.value:
145
+ if isinstance(stmt.value, ast.List):
146
+ for element in stmt.value.elts:
147
+ component = self._extract_component_from_tuple(element, imports)
148
+ if component:
149
+ components.append(component)
150
+
151
+ return components
152
+
153
+ def _collect_imports(self, tree: ast.AST, plugin_name: str) -> dict[str, str]:
154
+ """收集导入信息
155
+
156
+ Args:
157
+ tree: AST 树
158
+ plugin_name: 插件名称
159
+
160
+ Returns:
161
+ 导入映射: {类名: 导入路径}
162
+ """
163
+ imports = {}
164
+
165
+ for node in ast.walk(tree):
166
+ # from xxx import yyy
167
+ if isinstance(node, ast.ImportFrom):
168
+ if node.module and node.module.startswith("."):
169
+ # 相对导入
170
+ for alias in node.names:
171
+ imports[alias.name] = node.module
172
+ elif node.module and node.module.startswith(plugin_name):
173
+ # 绝对导入
174
+ for alias in node.names:
175
+ # 转换为相对路径
176
+ relative_module = "." + node.module[len(plugin_name) :]
177
+ imports[alias.name] = relative_module
178
+
179
+ return imports
180
+
181
+ def _extract_components_from_return(self, return_node: ast.AST, imports: dict[str, str]) -> list[dict]:
182
+ """从 return 语句中提取组件信息
183
+
184
+ Args:
185
+ return_node: return 语句的 AST 节点
186
+ imports: 导入映射
187
+
188
+ Returns:
189
+ 组件信息列表
190
+ """
191
+ components = []
192
+
193
+ if isinstance(return_node, ast.List):
194
+ for element in return_node.elts:
195
+ component = self._extract_component_from_tuple(element, imports)
196
+ if component:
197
+ components.append(component)
198
+
199
+ return components
200
+
201
+ def _extract_component_from_tuple(self, tuple_node: ast.AST, imports: dict[str, str]) -> dict | None:
202
+ """从元组中提取组件信息
203
+
204
+ Args:
205
+ tuple_node: 元组节点
206
+ imports: 导入映射
207
+
208
+ Returns:
209
+ 组件信息字典
210
+ """
211
+ if not isinstance(tuple_node, ast.Tuple) or len(tuple_node.elts) < 2:
212
+ return None
213
+
214
+ # 第二个元素应该是组件类
215
+ class_node = tuple_node.elts[1]
216
+
217
+ if isinstance(class_node, ast.Name):
218
+ class_name = class_node.id
219
+ import_from = imports.get(class_name, "")
220
+
221
+ return {"class_name": class_name, "import_from": import_from}
222
+
223
+ return None
224
+
225
+ def _validate_component(self, component_info: dict, plugin_dir: Path, plugin_name: str) -> None:
226
+ """验证单个组件
227
+
228
+ Args:
229
+ component_info: 组件信息
230
+ plugin_dir: 插件目录
231
+ plugin_name: 插件名称
232
+ """
233
+ class_name = component_info["class_name"]
234
+ import_from = component_info["import_from"]
235
+
236
+ # 根据导入路径找到组件文件
237
+ component_file = self._resolve_component_file(import_from, class_name, plugin_dir)
238
+
239
+ if not component_file:
240
+ self.result.add_warning(
241
+ f"无法定位组件 {class_name} 的源文件",
242
+ file_path=f"{plugin_name}/plugin.py",
243
+ )
244
+ return
245
+
246
+ # 解析组件文件
247
+ try:
248
+ with open(component_file, encoding="utf-8") as f:
249
+ tree = ast.parse(f.read(), filename=str(component_file))
250
+ except Exception as e:
251
+ self.result.add_error(
252
+ f"解析组件文件失败: {component_file.name} - {e}",
253
+ file_path=str(component_file.relative_to(self.plugin_path)),
254
+ )
255
+ return
256
+
257
+ # 查找组件类定义
258
+ class_node = self._find_class_definition(tree, class_name)
259
+ if not class_node:
260
+ self.result.add_error(
261
+ f"在文件中未找到类定义: {class_name}",
262
+ file_path=str(component_file.relative_to(self.plugin_path)),
263
+ )
264
+ return
265
+
266
+ # 确定组件基类
267
+ base_class = self._get_base_class(class_node)
268
+
269
+ # 获取该组件类型需要的字段
270
+ required_fields = self.COMPONENT_REQUIRED_FIELDS.get(base_class, [])
271
+
272
+ if not required_fields:
273
+ # 未知的组件类型
274
+ self.result.add_info(
275
+ f"组件 {class_name} 的基类 {base_class} 不在已知类型列表中",
276
+ file_path=str(component_file.relative_to(self.plugin_path)),
277
+ )
278
+ return
279
+
280
+ # 检查必需字段
281
+ class_attributes = self._extract_class_attributes(class_node)
282
+
283
+ for field in required_fields:
284
+ if field not in class_attributes:
285
+ self.result.add_error(
286
+ f"组件 {class_name} 缺少必需的类属性: {field}",
287
+ file_path=str(component_file.relative_to(self.plugin_path)),
288
+ suggestion=f"在类中添加: {field} = '...'",
289
+ )
290
+ elif not class_attributes[field]:
291
+ self.result.add_warning(
292
+ f"组件 {class_name} 的类属性 {field} 为空",
293
+ file_path=str(component_file.relative_to(self.plugin_path)),
294
+ )
295
+
296
+ def _resolve_component_file(self, import_from: str, class_name: str, plugin_dir: Path) -> Path | None:
297
+ """解析组件文件路径
298
+
299
+ Args:
300
+ import_from: 导入路径(如 ".actions.my_action")
301
+ class_name: 类名
302
+ plugin_dir: 插件目录
303
+
304
+ Returns:
305
+ 组件文件路径,如果找不到返回 None
306
+ """
307
+ # 如果没有导入路径,说明组件类在 plugin.py 中定义
308
+ if not import_from:
309
+ plugin_file = plugin_dir / "plugin.py"
310
+ if plugin_file.exists():
311
+ return plugin_file
312
+ return None
313
+
314
+ # 转换相对导入路径为文件路径
315
+ # ".actions.my_action" -> "actions/my_action.py"
316
+ module_path = import_from.lstrip(".").replace(".", "/")
317
+ component_file = plugin_dir / f"{module_path}.py"
318
+
319
+ if component_file.exists():
320
+ return component_file
321
+
322
+ # 尝试查找 __init__.py 中的定义
323
+ init_file = plugin_dir / module_path / "__init__.py"
324
+ if init_file.exists():
325
+ return init_file
326
+
327
+ # 搜索整个插件目录
328
+ for py_file in plugin_dir.rglob("*.py"):
329
+ if py_file.name == "__init__.py":
330
+ continue
331
+ try:
332
+ with open(py_file, encoding="utf-8") as f:
333
+ content = f.read()
334
+ # 简单的正则匹配
335
+ if re.search(rf"class\s+{re.escape(class_name)}\s*\(", content):
336
+ return py_file
337
+ except Exception:
338
+ continue
339
+
340
+ return None
341
+
342
+ def _find_class_definition(self, tree: ast.AST, class_name: str) -> ast.ClassDef | None:
343
+ """查找类定义
344
+
345
+ Args:
346
+ tree: AST 树
347
+ class_name: 类名
348
+
349
+ Returns:
350
+ 类定义节点
351
+ """
352
+ for node in ast.walk(tree):
353
+ if isinstance(node, ast.ClassDef) and node.name == class_name:
354
+ return node
355
+ return None
356
+
357
+ def _get_base_class(self, class_node: ast.ClassDef) -> str:
358
+ """获取组件的基类名称
359
+
360
+ Args:
361
+ class_node: 类定义节点
362
+
363
+ Returns:
364
+ 基类名称
365
+ """
366
+ if not class_node.bases:
367
+ return ""
368
+
369
+ # 获取第一个基类
370
+ base = class_node.bases[0]
371
+ if isinstance(base, ast.Name):
372
+ return base.id
373
+ elif isinstance(base, ast.Attribute):
374
+ return base.attr
375
+
376
+ return ""
377
+
378
+ def _extract_class_attributes(self, class_node: ast.ClassDef) -> dict[str, str | None]:
379
+ """提取类的属性
380
+
381
+ Args:
382
+ class_node: 类定义节点
383
+
384
+ Returns:
385
+ 属性字典 {属性名: 属性值}
386
+ """
387
+ attributes = {}
388
+
389
+ for node in class_node.body:
390
+ if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
391
+ # 类型注解的赋值: name: str = "value"
392
+ attr_name = node.target.id
393
+ attr_value = self._extract_value(node.value) if node.value else None
394
+ attributes[attr_name] = attr_value
395
+ elif isinstance(node, ast.Assign):
396
+ # 普通赋值: name = "value"
397
+ for target in node.targets:
398
+ if isinstance(target, ast.Name):
399
+ attr_name = target.id
400
+ attr_value = self._extract_value(node.value)
401
+ attributes[attr_name] = attr_value
402
+
403
+ return attributes
404
+
405
+ def _extract_value(self, node: ast.AST) -> str | None:
406
+ """提取 AST 节点的值"""
407
+ if isinstance(node, ast.Constant):
408
+ return str(node.value) if node.value else None
409
+ elif isinstance(node, ast.Str): # Python 3.7 兼容
410
+ return str(node.s) if node.s else None
411
+ elif isinstance(node, ast.List):
412
+ return "[...]"
413
+ elif isinstance(node, ast.Dict):
414
+ return "{...}"
415
+ return None
@@ -0,0 +1,173 @@
1
+ """
2
+ 配置文件验证器
3
+ """
4
+
5
+ import ast
6
+
7
+ from .base import BaseValidator, ValidationResult
8
+
9
+
10
+ class ConfigValidator(BaseValidator):
11
+ """配置文件验证器
12
+
13
+ 仅检查插件类中定义的 config_schema 是否正确
14
+ 不验证 config.toml 文件(因为它会在运行时自动生成)
15
+ """
16
+
17
+ def validate(self) -> ValidationResult:
18
+ """执行配置验证
19
+
20
+ Returns:
21
+ ValidationResult: 验证结果
22
+ """
23
+ # 获取插件名称
24
+ plugin_name = self._get_plugin_name()
25
+ if not plugin_name:
26
+ self.result.add_error("无法确定插件名称")
27
+ return self.result
28
+
29
+ plugin_file = self.plugin_path / "plugin.py"
30
+
31
+ if not plugin_file.exists():
32
+ self.result.add_error("插件文件不存在: plugin.py")
33
+ return self.result
34
+
35
+ # 解析 plugin.py 查找 config_schema
36
+ config_schema = self._extract_config_schema(plugin_file, plugin_name)
37
+
38
+ if config_schema is None:
39
+ # 没有定义 config_schema,这是正常的
40
+ self.result.add_info("插件未定义配置 schema")
41
+ return self.result
42
+
43
+ # 验证 config_schema 的结构
44
+ if not config_schema:
45
+ self.result.add_warning(
46
+ "config_schema 已定义但为空",
47
+ file_path="plugin.py",
48
+ suggestion="如果不需要配置,可以删除 config_schema 定义",
49
+ )
50
+ return self.result
51
+
52
+ # 检查是否定义了 config_file_name
53
+ has_config_file_name = self._check_config_file_name(plugin_file, plugin_name)
54
+ if not has_config_file_name:
55
+ self.result.add_warning(
56
+ "定义了 config_schema 但未定义 config_file_name",
57
+ file_path="plugin.py",
58
+ suggestion="请在插件类中添加: config_file_name = 'config.toml'",
59
+ )
60
+
61
+ # 验证每个配置节
62
+ for section_name, section_content in config_schema.items():
63
+ if not section_name:
64
+ self.result.add_error(
65
+ "config_schema 中存在空的配置节名",
66
+ file_path="plugin.py",
67
+ )
68
+ elif not isinstance(section_content, dict):
69
+ self.result.add_warning(
70
+ f"config_schema 中的 [{section_name}] 节格式不正确",
71
+ file_path="plugin.py",
72
+ suggestion="每个配置节应该是一个字典,包含 ConfigField 定义",
73
+ )
74
+
75
+ self.result.add_info(f"config_schema 定义了 {len(config_schema)} 个配置节")
76
+ return self.result
77
+
78
+ def _check_config_file_name(self, plugin_file, plugin_name: str) -> bool:
79
+ """检查是否定义了 config_file_name
80
+
81
+ Args:
82
+ plugin_file: plugin.py 文件路径
83
+ plugin_name: 插件名称
84
+
85
+ Returns:
86
+ 是否定义了 config_file_name
87
+ """
88
+ try:
89
+ with open(plugin_file, encoding="utf-8") as f:
90
+ tree = ast.parse(f.read(), filename=str(plugin_file))
91
+ except Exception:
92
+ return False
93
+
94
+ # 查找插件类和 config_file_name 定义
95
+ for node in ast.walk(tree):
96
+ if isinstance(node, ast.ClassDef):
97
+ # 检查是否继承自 BasePlugin
98
+ if any(
99
+ (isinstance(base, ast.Name) and base.id == "BasePlugin")
100
+ or (isinstance(base, ast.Attribute) and base.attr == "BasePlugin")
101
+ for base in node.bases
102
+ ):
103
+ # 在类中查找 config_file_name
104
+ for item in node.body:
105
+ if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
106
+ if item.target.id == "config_file_name":
107
+ return True
108
+ elif isinstance(item, ast.Assign):
109
+ for target in item.targets:
110
+ if isinstance(target, ast.Name) and target.id == "config_file_name":
111
+ return True
112
+
113
+ return False
114
+
115
+ def _extract_config_schema(self, plugin_file, plugin_name: str) -> dict | None:
116
+ """从 plugin.py 中提取 config_schema 定义
117
+
118
+ Args:
119
+ plugin_file: plugin.py 文件路径
120
+ plugin_name: 插件名称
121
+
122
+ Returns:
123
+ config_schema 字典,如果未定义返回 None
124
+ """
125
+ try:
126
+ with open(plugin_file, encoding="utf-8") as f:
127
+ tree = ast.parse(f.read(), filename=str(plugin_file))
128
+ except Exception as e:
129
+ self.result.add_error(f"解析 plugin.py 失败: {e}")
130
+ return None
131
+
132
+ # 查找插件类和 config_schema 定义
133
+ for node in ast.walk(tree):
134
+ if isinstance(node, ast.ClassDef):
135
+ # 检查是否继承自 BasePlugin
136
+ if any(
137
+ (isinstance(base, ast.Name) and base.id == "BasePlugin")
138
+ or (isinstance(base, ast.Attribute) and base.attr == "BasePlugin")
139
+ for base in node.bases
140
+ ):
141
+ # 在类中查找 config_schema
142
+ for item in node.body:
143
+ if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
144
+ if item.target.id == "config_schema" and item.value:
145
+ return self._extract_schema_structure(item.value)
146
+ elif isinstance(item, ast.Assign):
147
+ for target in item.targets:
148
+ if isinstance(target, ast.Name) and target.id == "config_schema":
149
+ return self._extract_schema_structure(item.value)
150
+
151
+ return None
152
+
153
+ def _extract_schema_structure(self, node: ast.AST) -> dict:
154
+ """提取 config_schema 的结构(只提取节名)
155
+
156
+ Args:
157
+ node: config_schema 的赋值节点
158
+
159
+ Returns:
160
+ 包含节名的字典
161
+ """
162
+ if isinstance(node, ast.Dict):
163
+ schema = {}
164
+ for key in node.keys:
165
+ if isinstance(key, ast.Constant):
166
+ section_name = str(key.value)
167
+ schema[section_name] = {}
168
+ elif isinstance(key, ast.Str): # Python 3.7 兼容
169
+ section_name = str(key.s)
170
+ schema[section_name] = {}
171
+ return schema
172
+
173
+ return {}