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.
- mofox_plugin_dev_toolkit-0.2.1.dist-info/METADATA +409 -0
- mofox_plugin_dev_toolkit-0.2.1.dist-info/RECORD +43 -0
- mofox_plugin_dev_toolkit-0.2.1.dist-info/WHEEL +5 -0
- mofox_plugin_dev_toolkit-0.2.1.dist-info/entry_points.txt +2 -0
- mofox_plugin_dev_toolkit-0.2.1.dist-info/licenses/LICENSE +674 -0
- mofox_plugin_dev_toolkit-0.2.1.dist-info/top_level.txt +1 -0
- mpdt/__init__.py +15 -0
- mpdt/__main__.py +8 -0
- mpdt/cli.py +314 -0
- mpdt/commands/__init__.py +9 -0
- mpdt/commands/check.py +316 -0
- mpdt/commands/dev.py +550 -0
- mpdt/commands/generate.py +366 -0
- mpdt/commands/init.py +487 -0
- mpdt/dev/bridge_plugin/__init__.py +17 -0
- mpdt/dev/bridge_plugin/discovery_server.py +126 -0
- mpdt/dev/bridge_plugin/plugin.py +258 -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/color_printer.py +99 -0
- mpdt/utils/config_loader.py +171 -0
- mpdt/utils/config_manager.py +297 -0
- mpdt/utils/file_ops.py +203 -0
- mpdt/utils/license_generator.py +980 -0
- mpdt/utils/plugin_parser.py +196 -0
- mpdt/utils/template_engine.py +112 -0
- mpdt/validators/__init__.py +26 -0
- mpdt/validators/auto_fix_validator.py +182 -0
- mpdt/validators/base.py +121 -0
- mpdt/validators/component_validator.py +415 -0
- mpdt/validators/config_validator.py +173 -0
- mpdt/validators/metadata_validator.py +125 -0
- mpdt/validators/structure_validator.py +70 -0
- mpdt/validators/style_validator.py +125 -0
- 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 {}
|