mofox-plugin-dev-toolkit 0.3.3__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 (46) hide show
  1. mofox_plugin_dev_toolkit-0.3.3.dist-info/METADATA +730 -0
  2. mofox_plugin_dev_toolkit-0.3.3.dist-info/RECORD +46 -0
  3. mofox_plugin_dev_toolkit-0.3.3.dist-info/WHEEL +5 -0
  4. mofox_plugin_dev_toolkit-0.3.3.dist-info/entry_points.txt +2 -0
  5. mofox_plugin_dev_toolkit-0.3.3.dist-info/licenses/LICENSE +674 -0
  6. mofox_plugin_dev_toolkit-0.3.3.dist-info/top_level.txt +1 -0
  7. mpdt/__init__.py +15 -0
  8. mpdt/__main__.py +8 -0
  9. mpdt/cli.py +316 -0
  10. mpdt/commands/__init__.py +9 -0
  11. mpdt/commands/check.py +498 -0
  12. mpdt/commands/dev.py +318 -0
  13. mpdt/commands/generate.py +448 -0
  14. mpdt/commands/init.py +686 -0
  15. mpdt/dev/bridge_plugin/__init__.py +17 -0
  16. mpdt/dev/bridge_plugin/cleanup_handler.py +65 -0
  17. mpdt/dev/bridge_plugin/dev_config.py +24 -0
  18. mpdt/dev/bridge_plugin/file_watcher.py +169 -0
  19. mpdt/dev/bridge_plugin/plugin.py +219 -0
  20. mpdt/templates/__init__.py +165 -0
  21. mpdt/templates/action_template.py +102 -0
  22. mpdt/templates/adapter_template.py +129 -0
  23. mpdt/templates/chatter_template.py +103 -0
  24. mpdt/templates/event_template.py +116 -0
  25. mpdt/templates/plus_command_template.py +150 -0
  26. mpdt/templates/prompt_template.py +92 -0
  27. mpdt/templates/router_template.py +175 -0
  28. mpdt/templates/tool_template.py +98 -0
  29. mpdt/utils/__init__.py +10 -0
  30. mpdt/utils/code_parser.py +401 -0
  31. mpdt/utils/color_printer.py +99 -0
  32. mpdt/utils/config_loader.py +171 -0
  33. mpdt/utils/config_manager.py +297 -0
  34. mpdt/utils/file_ops.py +207 -0
  35. mpdt/utils/license_generator.py +980 -0
  36. mpdt/utils/plugin_parser.py +195 -0
  37. mpdt/utils/template_engine.py +112 -0
  38. mpdt/validators/__init__.py +26 -0
  39. mpdt/validators/auto_fix_validator.py +990 -0
  40. mpdt/validators/base.py +129 -0
  41. mpdt/validators/component_validator.py +842 -0
  42. mpdt/validators/config_validator.py +119 -0
  43. mpdt/validators/metadata_validator.py +107 -0
  44. mpdt/validators/structure_validator.py +72 -0
  45. mpdt/validators/style_validator.py +117 -0
  46. mpdt/validators/type_validator.py +206 -0
@@ -0,0 +1,842 @@
1
+ """
2
+ 组件验证器
3
+ """
4
+
5
+ import ast
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from ..utils.code_parser import CodeParser
10
+ from .base import BaseValidator, ValidationResult
11
+
12
+
13
+ class ComponentValidator(BaseValidator):
14
+ """组件验证器
15
+
16
+ 通过解析 plugin.py 中的 get_plugin_components() 方法,
17
+ 找到所有组件类,然后检查每个组件类是否有必需的元数据和方法。
18
+ """
19
+
20
+ # 不同组件类型的必需元数据
21
+ # 注意:根据 MMC 基类定义,各组件使用不同的属性名:
22
+ # - BaseTool: name, description
23
+ # - BaseCommand/PlusCommand: command_name, command_description
24
+ # - BaseAction: action_name, action_description
25
+ # - BaseEventHandler: handler_name, handler_description
26
+ # - BaseAdapter: adapter_name, adapter_description
27
+ # - BasePrompt: prompt_name (无 prompt_description)
28
+ # - BaseRouterComponent: component_name, component_description
29
+ COMPONENT_REQUIRED_FIELDS = {
30
+ "Action": ["action_name", "action_description"],
31
+ "BaseAction": ["action_name", "action_description"],
32
+ "Command": ["command_name", "command_description"],
33
+ "BaseCommand": ["command_name", "command_description"],
34
+ "PlusCommand": ["command_name", "command_description"],
35
+ "Tool": ["name", "description"],
36
+ "BaseTool": ["name", "description"],
37
+ "EventHandler": ["handler_name", "handler_description"],
38
+ "BaseEventHandler": ["handler_name", "handler_description"],
39
+ "Adapter": ["adapter_name", "adapter_description"],
40
+ "BaseAdapter": ["adapter_name", "adapter_description"],
41
+ "Prompt": ["prompt_name"],
42
+ "BasePrompt": ["prompt_name"],
43
+ "Chatter": ["chatter_name", "chatter_description"],
44
+ "BaseChatter": ["chatter_name", "chatter_description"],
45
+ "Router": ["component_name", "component_description"],
46
+ "BaseRouterComponent": ["component_name", "component_description"],
47
+ }
48
+
49
+ # 不同组件类型的必需方法
50
+ # 格式: {基类名: [必需方法名列表]}
51
+ COMPONENT_REQUIRED_METHODS = {
52
+ "BaseAction": ["execute", "go_activate"],
53
+ "BaseCommand": ["execute"],
54
+ "PlusCommand": ["execute"],
55
+ "BaseTool": ["execute"],
56
+ "BaseEventHandler": ["execute"],
57
+ "BaseAdapter": ["from_platform_message"],
58
+ "BasePrompt": ["execute"],
59
+ "BaseRouterComponent": ["register_endpoints"],
60
+ }
61
+
62
+ # 方法签名要求
63
+ # 格式: {基类名: {方法名: {"params": [...], "return_type": "..."}}}
64
+ COMPONENT_METHOD_SIGNATURES = {
65
+ "BaseAction": {
66
+ "execute": {
67
+ "params": [], # async def execute(self)
68
+ "return_type": "tuple[bool, str]",
69
+ "is_async": True,
70
+ },
71
+ "go_activate": {
72
+ "params": [("llm_judge_model", "optional")], # async def go_activate(self, llm_judge_model=None)
73
+ "return_type": "bool",
74
+ "is_async": True,
75
+ },
76
+ },
77
+ "BaseCommand": {
78
+ "execute": {
79
+ "params": [], # async def execute(self)
80
+ "return_type": "tuple[bool, str | None, bool]",
81
+ "is_async": True,
82
+ },
83
+ },
84
+ "PlusCommand": {
85
+ "execute": {
86
+ "params": [("args", "CommandArgs")], # async def execute(self, args: CommandArgs)
87
+ "return_type": "tuple[bool, str | None, bool]",
88
+ "is_async": True,
89
+ },
90
+ },
91
+ "BaseTool": {
92
+ "execute": {
93
+ "params": [("function_args", "dict[str, Any]")], # async def execute(self, function_args: dict[str, Any])
94
+ "return_type": "dict[str, Any]",
95
+ "is_async": True,
96
+ },
97
+ },
98
+ "BaseEventHandler": {
99
+ "execute": {
100
+ "params": [("kwargs", "dict | None")], # async def execute(self, kwargs: dict | None)
101
+ "return_type": "tuple[bool, bool, str | None]",
102
+ "is_async": True,
103
+ },
104
+ },
105
+ "BaseAdapter": {
106
+ "from_platform_message": {
107
+ "params": [("raw", "Any")], # async def from_platform_message(self, raw: Any)
108
+ "return_type": "MessageEnvelope",
109
+ "is_async": True,
110
+ },
111
+ },
112
+ "BasePrompt": {
113
+ "execute": {
114
+ "params": [], # async def execute(self)
115
+ "return_type": "str",
116
+ "is_async": True,
117
+ },
118
+ },
119
+ "BaseRouterComponent": {
120
+ "register_endpoints": {
121
+ "params": [], # def register_endpoints(self)
122
+ "return_type": "None",
123
+ "is_async": False,
124
+ },
125
+ },
126
+ }
127
+
128
+ def validate(self) -> ValidationResult:
129
+ """执行组件验证
130
+
131
+ Returns:
132
+ ValidationResult: 验证结果
133
+ """
134
+ # 获取插件名称
135
+ plugin_name = self._get_plugin_name()
136
+ if not plugin_name:
137
+ self.result.add_error("无法确定插件名称")
138
+ return self.result
139
+
140
+ plugin_dir = self.plugin_path
141
+ plugin_file = plugin_dir / "plugin.py"
142
+
143
+ if not plugin_file.exists():
144
+ self.result.add_error("插件文件不存在: plugin.py")
145
+ return self.result
146
+
147
+ # 验证插件类本身的元数据
148
+ self._validate_plugin_class(plugin_file, plugin_name)
149
+
150
+ # 解析 plugin.py 获取组件信息
151
+ components = self._extract_components_from_plugin(plugin_file, plugin_name)
152
+
153
+ if not components:
154
+ self.result.add_warning(
155
+ "未找到任何组件注册",
156
+ file_path="plugin.py",
157
+ suggestion="请在 get_plugin_components() 方法中注册组件",
158
+ )
159
+ return self.result
160
+
161
+ # 验证每个组件
162
+ for component_info in components:
163
+ self._validate_component(component_info, plugin_dir, plugin_name)
164
+
165
+ return self.result
166
+
167
+ def _validate_plugin_class(self, plugin_file: Path, plugin_name: str) -> None:
168
+ """验证插件类本身的元数据
169
+
170
+ 检查 plugin.py 中的插件主类是否定义了必需的属性
171
+
172
+ Args:
173
+ plugin_file: plugin.py 文件路径
174
+ plugin_name: 插件名称
175
+ """
176
+ try:
177
+ parser = CodeParser.from_file(plugin_file)
178
+ except Exception as e:
179
+ self.result.add_error(f"解析 plugin.py 失败: {e}")
180
+ return
181
+
182
+ # 查找继承自 BasePlugin 的类
183
+ plugin_classes = parser.find_class(base_class="BasePlugin")
184
+
185
+ if not plugin_classes:
186
+ self.result.add_warning(
187
+ "未找到继承自 BasePlugin 的插件类",
188
+ file_path="plugin.py",
189
+ suggestion="插件主类应该继承自 BasePlugin",
190
+ )
191
+ return
192
+
193
+ plugin_class = plugin_classes[0]
194
+ class_name = plugin_class.name.value
195
+
196
+ # 提取类属性
197
+ class_attributes = parser.find_all_class_attributes(base_class="BasePlugin")
198
+
199
+ # 检查必需的类属性
200
+ # plugin_name 是必需的
201
+ if "plugin_name" not in class_attributes:
202
+ self.result.add_error(
203
+ f"插件类 {class_name} 缺少必需的类属性: plugin_name",
204
+ file_path="plugin.py",
205
+ suggestion="在类中添加: plugin_name = '...' | 可运行 'mpdt check --fix' 自动修复",
206
+ )
207
+ elif not class_attributes["plugin_name"]:
208
+ self.result.add_error(
209
+ f"插件类 {class_name} 的 plugin_name 属性为空",
210
+ file_path="plugin.py",
211
+ )
212
+
213
+ # config_file_name 必需有
214
+ if "config_file_name" not in class_attributes:
215
+ self.result.add_error(
216
+ f"插件类 {class_name} 未定义 config_file_name",
217
+ file_path="plugin.py",
218
+ suggestion="在类中添加: config_file_name = 'config.toml' | 可运行 'mpdt check --fix' 自动修复",
219
+ )
220
+
221
+ # 检查 enable_plugin 属性(有默认值,但可以检查是否自定义)
222
+ if "enable_plugin" in class_attributes:
223
+ enable_value = class_attributes["enable_plugin"]
224
+ if enable_value and str(enable_value).lower() not in ["true", "false"]:
225
+ self.result.add_warning(
226
+ f"插件类 {class_name} 的 enable_plugin 应该是布尔值",
227
+ file_path="plugin.py",
228
+ )
229
+
230
+ def _extract_components_from_plugin(self, plugin_file: Path, plugin_name: str) -> list[dict]:
231
+ """从 plugin.py 中提取组件信息
232
+
233
+ Args:
234
+ plugin_file: plugin.py 文件路径
235
+ plugin_name: 插件名称
236
+
237
+ Returns:
238
+ 组件信息列表,每个元素包含: {
239
+ 'class_name': 组件类名,
240
+ 'base_class': 基类名称,
241
+ 'import_from': 导入来源(相对路径)
242
+ }
243
+ """
244
+ try:
245
+ with open(plugin_file, encoding="utf-8") as f:
246
+ tree = ast.parse(f.read(), filename=str(plugin_file))
247
+ except Exception as e:
248
+ self.result.add_error(f"解析 plugin.py 失败: {e}")
249
+ return []
250
+
251
+ components = []
252
+
253
+ # 收集所有导入的组件类
254
+ imports = self._collect_imports(tree, plugin_name)
255
+
256
+ # 查找 get_plugin_components 方法
257
+ for node in ast.walk(tree):
258
+ if isinstance(node, ast.FunctionDef) and node.name == "get_plugin_components":
259
+ # 分析函数体,查找 components.append() 调用
260
+ components.extend(self._extract_components_from_function(node, imports))
261
+
262
+ return components
263
+
264
+ def _extract_components_from_function(self, func_node: ast.FunctionDef, imports: dict[str, str]) -> list[dict]:
265
+ """从 get_plugin_components 函数中提取组件信息
266
+
267
+ Args:
268
+ func_node: 函数定义节点
269
+ imports: 导入映射
270
+
271
+ Returns:
272
+ 组件信息列表
273
+ """
274
+ components = []
275
+
276
+ # 递归遍历所有语句节点,包括 if/for 等块内的语句
277
+ def walk_statements(statements):
278
+ for stmt in statements:
279
+ # 情况1: components.append((ComponentInfo, ComponentClass))
280
+ if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
281
+ call = stmt.value
282
+ # 检查是否是 .append() 调用
283
+ if isinstance(call.func, ast.Attribute) and call.func.attr == "append":
284
+ # 获取 append 的参数(应该是一个元组)
285
+ if call.args:
286
+ component = self._extract_component_from_tuple(call.args[0], imports)
287
+ if component:
288
+ components.append(component)
289
+
290
+ # 情况2: return [(...), (...), ...]
291
+ elif isinstance(stmt, ast.Return) and stmt.value:
292
+ if isinstance(stmt.value, ast.List):
293
+ for element in stmt.value.elts:
294
+ component = self._extract_component_from_tuple(element, imports)
295
+ if component:
296
+ components.append(component)
297
+
298
+ # 情况3: if 语句块内
299
+ elif isinstance(stmt, ast.If):
300
+ # 递归检查 if 块
301
+ walk_statements(stmt.body)
302
+ # 递归检查 else/elif 块
303
+ walk_statements(stmt.orelse)
304
+
305
+ # 情况4: for/while 循环块内
306
+ elif isinstance(stmt, (ast.For, ast.While)):
307
+ walk_statements(stmt.body)
308
+ walk_statements(stmt.orelse)
309
+
310
+ # 情况5: with 语句块内
311
+ elif isinstance(stmt, ast.With):
312
+ walk_statements(stmt.body)
313
+
314
+ # 情况6: try-except 块内
315
+ elif isinstance(stmt, ast.Try):
316
+ walk_statements(stmt.body)
317
+ for handler in stmt.handlers:
318
+ walk_statements(handler.body)
319
+ walk_statements(stmt.orelse)
320
+ walk_statements(stmt.finalbody)
321
+
322
+ # 从函数体开始遍历
323
+ walk_statements(func_node.body)
324
+
325
+ return components
326
+
327
+ def _collect_imports(self, tree: ast.AST, plugin_name: str) -> dict[str, str]:
328
+ """收集导入信息
329
+
330
+ Args:
331
+ tree: AST 树
332
+ plugin_name: 插件名称
333
+
334
+ Returns:
335
+ 导入映射: {类名: 导入路径}
336
+ """
337
+ imports = {}
338
+
339
+ for node in ast.walk(tree):
340
+ # from xxx import yyy
341
+ if isinstance(node, ast.ImportFrom):
342
+ if node.module and node.module.startswith("."):
343
+ # 相对导入
344
+ for alias in node.names:
345
+ imports[alias.name] = node.module
346
+ elif node.module and node.module.startswith(plugin_name):
347
+ # 绝对导入
348
+ for alias in node.names:
349
+ # 转换为相对路径
350
+ relative_module = "." + node.module[len(plugin_name) :]
351
+ imports[alias.name] = relative_module
352
+
353
+ return imports
354
+
355
+ def _extract_components_from_return(self, return_node: ast.AST, imports: dict[str, str]) -> list[dict]:
356
+ """从 return 语句中提取组件信息
357
+
358
+ Args:
359
+ return_node: return 语句的 AST 节点
360
+ imports: 导入映射
361
+
362
+ Returns:
363
+ 组件信息列表
364
+ """
365
+ components = []
366
+
367
+ if isinstance(return_node, ast.List):
368
+ for element in return_node.elts:
369
+ component = self._extract_component_from_tuple(element, imports)
370
+ if component:
371
+ components.append(component)
372
+
373
+ return components
374
+
375
+ def _extract_component_from_tuple(self, tuple_node: ast.AST, imports: dict[str, str]) -> dict | None:
376
+ """从元组中提取组件信息
377
+
378
+ Args:
379
+ tuple_node: 元组节点
380
+ imports: 导入映射
381
+
382
+ Returns:
383
+ 组件信息字典
384
+ """
385
+ if not isinstance(tuple_node, ast.Tuple) or len(tuple_node.elts) < 2:
386
+ return None
387
+
388
+ # 第二个元素应该是组件类
389
+ class_node = tuple_node.elts[1]
390
+
391
+ if isinstance(class_node, ast.Name):
392
+ class_name = class_node.id
393
+ import_from = imports.get(class_name, "")
394
+
395
+ return {"class_name": class_name, "import_from": import_from}
396
+
397
+ return None
398
+
399
+ def _validate_component(self, component_info: dict, plugin_dir: Path, plugin_name: str) -> None:
400
+ """验证单个组件
401
+
402
+ Args:
403
+ component_info: 组件信息
404
+ plugin_dir: 插件目录
405
+ plugin_name: 插件名称
406
+ """
407
+ class_name = component_info["class_name"]
408
+ import_from = component_info["import_from"]
409
+
410
+ # 根据导入路径找到组件文件
411
+ component_file = self._resolve_component_file(import_from, class_name, plugin_dir)
412
+
413
+ if not component_file:
414
+ self.result.add_warning(
415
+ f"无法定位组件 {class_name} 的源文件",
416
+ file_path=f"{plugin_name}/plugin.py",
417
+ )
418
+ return
419
+
420
+ # 解析组件文件
421
+ try:
422
+ with open(component_file, encoding="utf-8") as f:
423
+ tree = ast.parse(f.read(), filename=str(component_file))
424
+ except Exception as e:
425
+ self.result.add_error(
426
+ f"解析组件文件失败: {component_file.name} - {e}",
427
+ file_path=str(component_file.relative_to(self.plugin_path)),
428
+ )
429
+ return
430
+
431
+ # 查找组件类定义
432
+ class_node = self._find_class_definition(tree, class_name)
433
+ if not class_node:
434
+ self.result.add_error(
435
+ f"在文件中未找到类定义: {class_name}",
436
+ file_path=str(component_file.relative_to(self.plugin_path)),
437
+ )
438
+ return
439
+
440
+ # 确定组件基类
441
+ base_class = self._get_base_class(class_node)
442
+
443
+ # 获取该组件类型需要的字段
444
+ required_fields = self.COMPONENT_REQUIRED_FIELDS.get(base_class, [])
445
+
446
+ if not required_fields:
447
+ # 未知的组件类型
448
+ self.result.add_error(
449
+ f"组件 {class_name} 的基类 {base_class} 不在已知类型列表中",
450
+ file_path=str(component_file.relative_to(self.plugin_path)),
451
+ )
452
+ return
453
+ # 检查必需字段
454
+ class_attributes = self._extract_class_attributes(class_node)
455
+
456
+ for field in required_fields:
457
+ if field not in class_attributes:
458
+ self.result.add_error(
459
+ f"组件 {class_name} 缺少必需的类属性: {field}",
460
+ file_path=str(component_file.relative_to(self.plugin_path)),
461
+ suggestion=f"在类中添加: {field} = '...' | 可运行 'mpdt check --fix' 自动修复",
462
+ )
463
+ elif not class_attributes[field]:
464
+ self.result.add_warning(
465
+ f"组件 {class_name} 的类属性 {field} 为空",
466
+ file_path=str(component_file.relative_to(self.plugin_path)),
467
+ )
468
+
469
+ # 检查必需方法
470
+ required_methods = self.COMPONENT_REQUIRED_METHODS.get(base_class, [])
471
+ if required_methods:
472
+ self._validate_required_methods(class_node, class_name, required_methods, component_file)
473
+
474
+ def _resolve_component_file(self, import_from: str, class_name: str, plugin_dir: Path) -> Path | None:
475
+ """解析组件文件路径
476
+
477
+ Args:
478
+ import_from: 导入路径(如 ".actions.my_action")
479
+ class_name: 类名
480
+ plugin_dir: 插件目录
481
+
482
+ Returns:
483
+ 组件文件路径,如果找不到返回 None
484
+ """
485
+ # 如果没有导入路径,说明组件类在 plugin.py 中定义
486
+ if not import_from:
487
+ plugin_file = plugin_dir / "plugin.py"
488
+ if plugin_file.exists():
489
+ return plugin_file
490
+ return None
491
+
492
+ # 转换相对导入路径为文件路径
493
+ # ".actions.my_action" -> "actions/my_action.py"
494
+ module_path = import_from.lstrip(".").replace(".", "/")
495
+ component_file = plugin_dir / f"{module_path}.py"
496
+
497
+ if component_file.exists():
498
+ return component_file
499
+
500
+ # 尝试查找 __init__.py 中的定义
501
+ init_file = plugin_dir / module_path / "__init__.py"
502
+ if init_file.exists():
503
+ return init_file
504
+
505
+ # 搜索整个插件目录
506
+ for py_file in plugin_dir.rglob("*.py"):
507
+ if py_file.name == "__init__.py":
508
+ continue
509
+ try:
510
+ with open(py_file, encoding="utf-8") as f:
511
+ content = f.read()
512
+ # 简单的正则匹配
513
+ if re.search(rf"class\s+{re.escape(class_name)}\s*\(", content):
514
+ return py_file
515
+ except Exception:
516
+ continue
517
+
518
+ return None
519
+
520
+ def _find_class_definition(self, tree: ast.AST, class_name: str) -> ast.ClassDef | None:
521
+ """查找类定义
522
+
523
+ Args:
524
+ tree: AST 树
525
+ class_name: 类名
526
+
527
+ Returns:
528
+ 类定义节点
529
+ """
530
+ for node in ast.walk(tree):
531
+ if isinstance(node, ast.ClassDef) and node.name == class_name:
532
+ return node
533
+ return None
534
+
535
+ def _get_base_class(self, class_node: ast.ClassDef) -> str:
536
+ """获取组件的基类名称
537
+
538
+ Args:
539
+ class_node: 类定义节点
540
+
541
+ Returns:
542
+ 基类名称
543
+ """
544
+ if not class_node.bases:
545
+ return ""
546
+
547
+ # 获取第一个基类
548
+ base = class_node.bases[0]
549
+ if isinstance(base, ast.Name):
550
+ return base.id
551
+ elif isinstance(base, ast.Attribute):
552
+ return base.attr
553
+
554
+ return ""
555
+
556
+ def _extract_class_attributes(self, class_node: ast.ClassDef) -> dict[str, str | None]:
557
+ """提取类的属性
558
+
559
+ Args:
560
+ class_node: 类定义节点
561
+
562
+ Returns:
563
+ 属性字典 {属性名: 属性值}
564
+ """
565
+ attributes = {}
566
+
567
+ for node in class_node.body:
568
+ if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
569
+ # 类型注解的赋值: name: str = "value"
570
+ attr_name = node.target.id
571
+ attr_value = self._extract_value(node.value) if node.value else None
572
+ attributes[attr_name] = attr_value
573
+ elif isinstance(node, ast.Assign):
574
+ # 普通赋值: name = "value"
575
+ for target in node.targets:
576
+ if isinstance(target, ast.Name):
577
+ attr_name = target.id
578
+ attr_value = self._extract_value(node.value)
579
+ attributes[attr_name] = attr_value
580
+
581
+ return attributes
582
+
583
+ def _extract_value(self, node: ast.AST) -> str | None:
584
+ """提取 AST 节点的值"""
585
+ if isinstance(node, ast.Constant):
586
+ return str(node.value) if node.value else None
587
+ elif isinstance(node, ast.Str): # Python 3.7 兼容
588
+ return str(node.s) if node.s else None
589
+ elif isinstance(node, ast.List):
590
+ return "[...]"
591
+ elif isinstance(node, ast.Dict):
592
+ return "{...}"
593
+ return None
594
+
595
+ def _validate_required_methods(
596
+ self, class_node: ast.ClassDef, class_name: str, required_methods: list[str], component_file: Path
597
+ ) -> None:
598
+ """验证组件类是否实现了所有必需的方法
599
+
600
+ Args:
601
+ class_node: 类定义节点
602
+ class_name: 类名
603
+ required_methods: 必需方法列表
604
+ component_file: 组件文件路径
605
+ """
606
+ # 提取类中定义的所有方法
607
+ defined_methods = {}
608
+ for node in class_node.body:
609
+ if isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
610
+ defined_methods[node.name] = node
611
+
612
+ # 获取基类名以查找签名要求
613
+ base_class = self._get_base_class(class_node)
614
+ method_signatures = self.COMPONENT_METHOD_SIGNATURES.get(base_class, {})
615
+
616
+ # 检查每个必需方法
617
+ for method_name in required_methods:
618
+ if method_name not in defined_methods:
619
+ self.result.add_error(
620
+ f"组件 {class_name} 缺少必需的方法: {method_name}",
621
+ file_path=str(component_file.relative_to(self.plugin_path)),
622
+ suggestion=f"在类中实现方法:\n async def {method_name}(self, ...):\n ... | 可运行 'mpdt check --fix' 自动修复",
623
+ )
624
+ else:
625
+ method_node = defined_methods[method_name]
626
+
627
+ # 检查方法是否为空实现
628
+ self._check_method_implementation(class_node, method_name, class_name, component_file)
629
+
630
+ # 检查方法签名(如果有签名要求)
631
+ if method_name in method_signatures:
632
+ signature_spec = method_signatures[method_name]
633
+ self._check_method_signature(
634
+ method_node, class_name, method_name, signature_spec, component_file
635
+ )
636
+
637
+ def _check_method_implementation(
638
+ self, class_node: ast.ClassDef, method_name: str, class_name: str, component_file: Path
639
+ ) -> None:
640
+ """检查方法是否为空实现
641
+
642
+ Args:
643
+ class_node: 类定义节点
644
+ method_name: 方法名
645
+ class_name: 类名
646
+ component_file: 组件文件路径
647
+ """
648
+ # 找到方法定义
649
+ method_node = None
650
+ for node in class_node.body:
651
+ if (isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef)) and node.name == method_name:
652
+ method_node = node
653
+ break
654
+
655
+ if not method_node:
656
+ return
657
+
658
+ # 检查方法体
659
+ if not method_node.body:
660
+ self.result.add_warning(
661
+ f"组件 {class_name} 的方法 {method_name} 为空",
662
+ file_path=str(component_file.relative_to(self.plugin_path)),
663
+ )
664
+ return
665
+
666
+ # 检查是否只有 pass 或 raise NotImplementedError
667
+ is_stub = True
668
+ for stmt in method_node.body:
669
+ # 跳过文档字符串
670
+ if isinstance(stmt, ast.Expr) and isinstance(stmt.value, (ast.Str, ast.Constant)):
671
+ continue
672
+
673
+ # 检查是否为 pass
674
+ if isinstance(stmt, ast.Pass):
675
+ continue
676
+
677
+ # 检查是否为 raise NotImplementedError
678
+ if isinstance(stmt, ast.Raise):
679
+ if isinstance(stmt.exc, ast.Call):
680
+ if isinstance(stmt.exc.func, ast.Name) and stmt.exc.func.id == "NotImplementedError":
681
+ continue
682
+
683
+ # 如果有其他语句,说明不是空实现
684
+ is_stub = False
685
+ break
686
+
687
+ if is_stub:
688
+ self.result.add_warning(
689
+ f"组件 {class_name} 的方法 {method_name} 只包含 pass 或 raise NotImplementedError,可能未实现",
690
+ file_path=str(component_file.relative_to(self.plugin_path)),
691
+ suggestion=f"请实现方法 {method_name} 的具体逻辑",
692
+ )
693
+
694
+ def _check_method_signature(
695
+ self,
696
+ method_node: ast.FunctionDef | ast.AsyncFunctionDef,
697
+ class_name: str,
698
+ method_name: str,
699
+ signature_spec: dict,
700
+ component_file: Path,
701
+ ) -> None:
702
+ """检查方法签名是否符合要求
703
+
704
+ Args:
705
+ method_node: 方法定义节点
706
+ class_name: 类名
707
+ method_name: 方法名
708
+ signature_spec: 签名规范
709
+ component_file: 组件文件路径
710
+ """
711
+ # 检查是否为异步方法
712
+ is_async_required = signature_spec.get("is_async", False)
713
+ is_async_actual = isinstance(method_node, ast.AsyncFunctionDef)
714
+
715
+ if is_async_required and not is_async_actual:
716
+ self.result.add_error(
717
+ f"组件 {class_name} 的方法 {method_name} 应该是异步方法(使用 async def)",
718
+ file_path=str(component_file.relative_to(self.plugin_path)),
719
+ suggestion=f"将 'def {method_name}' 改为 'async def {method_name}' | 可运行 'mpdt check --fix' 自动修复",
720
+ )
721
+ elif not is_async_required and is_async_actual:
722
+ self.result.add_warning(
723
+ f"组件 {class_name} 的方法 {method_name} 不应该是异步方法",
724
+ file_path=str(component_file.relative_to(self.plugin_path)),
725
+ suggestion=f"将 'async def {method_name}' 改为 'def {method_name}' | 可运行 'mpdt check --fix' 自动修复",
726
+ )
727
+
728
+ # 检查参数(排除 self)
729
+ required_params = signature_spec.get("params", [])
730
+ actual_args = method_node.args.args[1:] # 跳过 self
731
+
732
+ # 检查参数数量
733
+ min_params = sum(1 for param in required_params if param[1] != "optional")
734
+ max_params = len(required_params)
735
+
736
+ if len(actual_args) < min_params:
737
+ param_names = [param[0] for param in required_params if param[1] != "optional"]
738
+ self.result.add_error(
739
+ f"组件 {class_name} 的方法 {method_name} 缺少必需参数,应包含: {', '.join(param_names)}",
740
+ file_path=str(component_file.relative_to(self.plugin_path)),
741
+ suggestion=f"方法签名应为: {'async ' if is_async_required else ''}def {method_name}(self, {', '.join(param_names)}) | 可运行 'mpdt check --fix' 自动修复",
742
+ )
743
+ elif len(actual_args) > max_params and not method_node.args.vararg and not method_node.args.kwarg:
744
+ # 如果参数过多且没有 *args 或 **kwargs
745
+ expected_params = [param[0] for param in required_params]
746
+ self.result.add_warning(
747
+ f"组件 {class_name} 的方法 {method_name} 参数过多,预期: {', '.join(expected_params) if expected_params else '无参数'}",
748
+ file_path=str(component_file.relative_to(self.plugin_path)),
749
+ suggestion="可运行 'mpdt check --fix' 尝试自动修复",
750
+ )
751
+
752
+ # 检查返回类型注解
753
+ expected_return = signature_spec.get("return_type")
754
+ if expected_return and method_node.returns:
755
+ actual_return = self._extract_return_annotation(method_node.returns)
756
+ if actual_return and not self._compare_type_annotations(actual_return, expected_return):
757
+ self.result.add_warning(
758
+ f"组件 {class_name} 的方法 {method_name} 返回类型注解不匹配,预期: {expected_return},实际: {actual_return}",
759
+ file_path=str(component_file.relative_to(self.plugin_path)),
760
+ suggestion=f"建议修改返回类型注解为: -> {expected_return}",
761
+ )
762
+ elif expected_return and not method_node.returns:
763
+ self.result.add_warning(
764
+ f"组件 {class_name} 的方法 {method_name} 缺少返回类型注解,建议添加: -> {expected_return}",
765
+ file_path=str(component_file.relative_to(self.plugin_path)),
766
+ )
767
+
768
+ def _extract_return_annotation(self, node: ast.AST) -> str:
769
+ """提取返回类型注解的字符串表示
770
+
771
+ Args:
772
+ node: 返回类型注解节点
773
+
774
+ Returns:
775
+ 返回类型的字符串表示
776
+ """
777
+ if isinstance(node, ast.Name):
778
+ return node.id
779
+ elif isinstance(node, ast.Constant):
780
+ return str(node.value)
781
+ elif isinstance(node, ast.Subscript):
782
+ # 处理泛型类型,如 tuple[bool, str]
783
+ value = self._extract_return_annotation(node.value)
784
+ if isinstance(node.slice, ast.Tuple):
785
+ slice_parts = [self._extract_return_annotation(elt) for elt in node.slice.elts]
786
+ return f"{value}[{', '.join(slice_parts)}]"
787
+ else:
788
+ slice_str = self._extract_return_annotation(node.slice)
789
+ return f"{value}[{slice_str}]"
790
+ elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
791
+ # 处理联合类型,如 str | None
792
+ left = self._extract_return_annotation(node.left)
793
+ right = self._extract_return_annotation(node.right)
794
+ return f"{left} | {right}"
795
+ elif isinstance(node, ast.Attribute):
796
+ # 处理 module.Type 形式
797
+ return node.attr
798
+ return ""
799
+
800
+ def _compare_type_annotations(self, actual: str, expected: str) -> bool:
801
+ """比较两个类型注解是否匹配(宽松比较)
802
+
803
+ Args:
804
+ actual: 实际的类型注解
805
+ expected: 期望的类型注解
806
+
807
+ Returns:
808
+ 是否匹配
809
+ """
810
+ # 标准化类型字符串(移除空格)
811
+ actual = actual.replace(" ", "")
812
+ expected = expected.replace(" ", "")
813
+
814
+ # 直接比较
815
+ if actual == expected:
816
+ return True
817
+
818
+ # 处理可选类型的不同写法
819
+ # Optional[str] vs str | None
820
+ if "Optional" in actual or "Optional" in expected:
821
+ actual = actual.replace("Optional[", "").replace("]", "|None")
822
+ expected = expected.replace("Optional[", "").replace("]", "|None")
823
+ if actual == expected:
824
+ return True
825
+
826
+ # 宽松匹配:泛型基类型匹配
827
+ # 例如 tuple 可以匹配 tuple[bool, str],dict 可以匹配 dict[str, Any]
828
+ actual_base = actual.split("[")[0]
829
+ expected_base = expected.split("[")[0]
830
+
831
+ if actual_base == expected_base:
832
+ return True
833
+
834
+ # 处理 Union 和 | 的不同写法
835
+ if "Union" in actual or "Union" in expected or "|" in actual or "|" in expected:
836
+ # 简化比较:提取基础类型
837
+ actual_types = set(re.findall(r'\w+', actual))
838
+ expected_types = set(re.findall(r'\w+', actual))
839
+ if actual_types == expected_types:
840
+ return True
841
+
842
+ return False