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.
- mofox_plugin_dev_toolkit-0.3.3.dist-info/METADATA +730 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/RECORD +46 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/WHEEL +5 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/entry_points.txt +2 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/licenses/LICENSE +674 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/top_level.txt +1 -0
- mpdt/__init__.py +15 -0
- mpdt/__main__.py +8 -0
- mpdt/cli.py +316 -0
- mpdt/commands/__init__.py +9 -0
- mpdt/commands/check.py +498 -0
- mpdt/commands/dev.py +318 -0
- mpdt/commands/generate.py +448 -0
- mpdt/commands/init.py +686 -0
- mpdt/dev/bridge_plugin/__init__.py +17 -0
- mpdt/dev/bridge_plugin/cleanup_handler.py +65 -0
- mpdt/dev/bridge_plugin/dev_config.py +24 -0
- mpdt/dev/bridge_plugin/file_watcher.py +169 -0
- mpdt/dev/bridge_plugin/plugin.py +219 -0
- mpdt/templates/__init__.py +165 -0
- mpdt/templates/action_template.py +102 -0
- mpdt/templates/adapter_template.py +129 -0
- mpdt/templates/chatter_template.py +103 -0
- mpdt/templates/event_template.py +116 -0
- mpdt/templates/plus_command_template.py +150 -0
- mpdt/templates/prompt_template.py +92 -0
- mpdt/templates/router_template.py +175 -0
- mpdt/templates/tool_template.py +98 -0
- mpdt/utils/__init__.py +10 -0
- mpdt/utils/code_parser.py +401 -0
- mpdt/utils/color_printer.py +99 -0
- mpdt/utils/config_loader.py +171 -0
- mpdt/utils/config_manager.py +297 -0
- mpdt/utils/file_ops.py +207 -0
- mpdt/utils/license_generator.py +980 -0
- mpdt/utils/plugin_parser.py +195 -0
- mpdt/utils/template_engine.py +112 -0
- mpdt/validators/__init__.py +26 -0
- mpdt/validators/auto_fix_validator.py +990 -0
- mpdt/validators/base.py +129 -0
- mpdt/validators/component_validator.py +842 -0
- mpdt/validators/config_validator.py +119 -0
- mpdt/validators/metadata_validator.py +107 -0
- mpdt/validators/structure_validator.py +72 -0
- mpdt/validators/style_validator.py +117 -0
- 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
|