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,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
插件名称解析器
|
|
3
|
+
使用 AST 解析插件文件,提取运行时插件名称
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract_plugin_name(plugin_path: Path) -> str | None:
|
|
12
|
+
"""从插件目录提取运行时插件名称
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
plugin_path: 插件目录路径
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
插件名称,如果解析失败返回 None
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> extract_plugin_name(Path("my_awesome_plugin"))
|
|
22
|
+
"awesome_plugin" # 从 plugin.py 中的 plugin_name 属性读取
|
|
23
|
+
"""
|
|
24
|
+
plugin_file = plugin_path / "plugin.py"
|
|
25
|
+
|
|
26
|
+
if not plugin_file.exists():
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with open(plugin_file, "r", encoding="utf-8") as f:
|
|
31
|
+
source = f.read()
|
|
32
|
+
|
|
33
|
+
tree = ast.parse(source)
|
|
34
|
+
|
|
35
|
+
# 查找 BasePlugin 的子类
|
|
36
|
+
for node in ast.walk(tree):
|
|
37
|
+
if isinstance(node, ast.ClassDef):
|
|
38
|
+
# 检查是否继承自 BasePlugin
|
|
39
|
+
is_base_plugin = False
|
|
40
|
+
for base in node.bases:
|
|
41
|
+
if isinstance(base, ast.Name) and base.id == "BasePlugin":
|
|
42
|
+
is_base_plugin = True
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if not is_base_plugin:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# 查找 plugin_name 属性
|
|
49
|
+
for item in node.body:
|
|
50
|
+
# 处理普通赋值: plugin_name = "xxx"
|
|
51
|
+
if isinstance(item, ast.Assign):
|
|
52
|
+
for target in item.targets:
|
|
53
|
+
if isinstance(target, ast.Name) and target.id == "plugin_name":
|
|
54
|
+
if isinstance(item.value, ast.Constant):
|
|
55
|
+
return item.value.value
|
|
56
|
+
|
|
57
|
+
# 处理带类型注解的赋值: plugin_name: str = "xxx"
|
|
58
|
+
elif isinstance(item, ast.AnnAssign):
|
|
59
|
+
if isinstance(item.target, ast.Name) and item.target.id == "plugin_name":
|
|
60
|
+
if item.value and isinstance(item.value, ast.Constant):
|
|
61
|
+
return item.value.value
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
except Exception:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_plugin_info(plugin_path: Path) -> dict:
|
|
70
|
+
"""获取插件详细信息
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
plugin_path: 插件目录路径
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
包含插件信息的字典:
|
|
77
|
+
{
|
|
78
|
+
"dir_name": "my_awesome_plugin",
|
|
79
|
+
"plugin_name": "awesome_plugin",
|
|
80
|
+
"class_name": "MyAwesomePlugin",
|
|
81
|
+
"path": "/path/to/plugin",
|
|
82
|
+
"has_plugin_file": True,
|
|
83
|
+
"parse_success": True
|
|
84
|
+
}
|
|
85
|
+
"""
|
|
86
|
+
info = {
|
|
87
|
+
"dir_name": plugin_path.name,
|
|
88
|
+
"plugin_name": None,
|
|
89
|
+
"class_name": None,
|
|
90
|
+
"path": str(plugin_path.absolute()),
|
|
91
|
+
"has_plugin_file": False,
|
|
92
|
+
"parse_success": False,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
plugin_file = plugin_path / "plugin.py"
|
|
96
|
+
|
|
97
|
+
if not plugin_file.exists():
|
|
98
|
+
return info
|
|
99
|
+
|
|
100
|
+
info["has_plugin_file"] = True
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with open(plugin_file, "r", encoding="utf-8") as f:
|
|
104
|
+
source = f.read()
|
|
105
|
+
|
|
106
|
+
tree = ast.parse(source)
|
|
107
|
+
|
|
108
|
+
# 查找 BasePlugin 的子类
|
|
109
|
+
for node in ast.walk(tree):
|
|
110
|
+
if isinstance(node, ast.ClassDef):
|
|
111
|
+
# 检查是否继承自 BasePlugin
|
|
112
|
+
is_base_plugin = False
|
|
113
|
+
for base in node.bases:
|
|
114
|
+
if isinstance(base, ast.Name) and base.id == "BasePlugin":
|
|
115
|
+
is_base_plugin = True
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
if not is_base_plugin:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
info["class_name"] = node.name
|
|
122
|
+
|
|
123
|
+
# 查找 plugin_name 属性
|
|
124
|
+
for item in node.body:
|
|
125
|
+
# 处理普通赋值: plugin_name = "xxx"
|
|
126
|
+
if isinstance(item, ast.Assign):
|
|
127
|
+
for target in item.targets:
|
|
128
|
+
if isinstance(target, ast.Name) and target.id == "plugin_name":
|
|
129
|
+
if isinstance(item.value, ast.Constant):
|
|
130
|
+
info["plugin_name"] = item.value.value
|
|
131
|
+
info["parse_success"] = True
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
# 处理带类型注解的赋值: plugin_name: str = "xxx"
|
|
135
|
+
elif isinstance(item, ast.AnnAssign):
|
|
136
|
+
if isinstance(item.target, ast.Name) and item.target.id == "plugin_name":
|
|
137
|
+
if item.value and isinstance(item.value, ast.Constant):
|
|
138
|
+
info["plugin_name"] = item.value.value
|
|
139
|
+
info["parse_success"] = True
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
# 找到 BasePlugin 子类后跳出
|
|
143
|
+
if info["class_name"]:
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
return info
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
info["error"] = str(e)
|
|
150
|
+
return info
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def validate_plugin_structure(plugin_path: Path) -> tuple[bool, list[str]]:
|
|
154
|
+
"""验证插件目录结构
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
plugin_path: 插件目录路径
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
(是否有效, 错误/警告消息列表)
|
|
161
|
+
"""
|
|
162
|
+
messages = []
|
|
163
|
+
|
|
164
|
+
if not plugin_path.is_dir():
|
|
165
|
+
return False, ["路径不是一个目录"]
|
|
166
|
+
|
|
167
|
+
# 检查必需文件
|
|
168
|
+
required_files = {"plugin.py": "插件主文件", "__init__.py": "包初始化文件"}
|
|
169
|
+
|
|
170
|
+
for filename, description in required_files.items():
|
|
171
|
+
file_path = plugin_path / filename
|
|
172
|
+
if not file_path.exists():
|
|
173
|
+
messages.append(f"缺少 {description}: {filename}")
|
|
174
|
+
|
|
175
|
+
# 如果缺少必需文件,直接返回
|
|
176
|
+
if messages:
|
|
177
|
+
return False, messages
|
|
178
|
+
|
|
179
|
+
# 检查 plugin.py 是否可以解析
|
|
180
|
+
plugin_name = extract_plugin_name(plugin_path)
|
|
181
|
+
if not plugin_name:
|
|
182
|
+
messages.append("无法从 plugin.py 中提取 plugin_name")
|
|
183
|
+
messages.append("请确保有一个继承自 BasePlugin 的类,并定义了 plugin_name 属性")
|
|
184
|
+
return False, messages
|
|
185
|
+
|
|
186
|
+
# 检查推荐文件
|
|
187
|
+
recommended_files = {"config.toml": "配置文件", "README.md": "说明文档"}
|
|
188
|
+
|
|
189
|
+
for filename, description in recommended_files.items():
|
|
190
|
+
file_path = plugin_path / filename
|
|
191
|
+
if not file_path.exists():
|
|
192
|
+
messages.append(f"建议添加 {description}: {filename}")
|
|
193
|
+
|
|
194
|
+
# 如果只有建议性消息,仍然返回有效
|
|
195
|
+
has_errors = any("缺少" in msg or "无法" in msg for msg in messages)
|
|
196
|
+
return not has_errors, messages
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
模板引擎
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from jinja2 import Environment, FileSystemLoader, Template
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TemplateEngine:
|
|
12
|
+
"""模板引擎类"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, template_dir: Path | str | None = None):
|
|
15
|
+
"""
|
|
16
|
+
初始化模板引擎
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
template_dir: 模板目录路径
|
|
20
|
+
"""
|
|
21
|
+
if template_dir:
|
|
22
|
+
self.template_dir = Path(template_dir)
|
|
23
|
+
self.env = Environment(
|
|
24
|
+
loader=FileSystemLoader(str(self.template_dir)),
|
|
25
|
+
trim_blocks=True,
|
|
26
|
+
lstrip_blocks=True,
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
# 使用默认模板目录
|
|
30
|
+
default_template_dir = Path(__file__).parent.parent / "templates"
|
|
31
|
+
self.template_dir = default_template_dir
|
|
32
|
+
|
|
33
|
+
if default_template_dir.exists():
|
|
34
|
+
self.env = Environment(
|
|
35
|
+
loader=FileSystemLoader(str(default_template_dir)),
|
|
36
|
+
trim_blocks=True,
|
|
37
|
+
lstrip_blocks=True,
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
self.env = None
|
|
41
|
+
|
|
42
|
+
def render_string(self, template_string: str, context: dict[str, Any]) -> str:
|
|
43
|
+
"""
|
|
44
|
+
渲染字符串模板
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
template_string: 模板字符串
|
|
48
|
+
context: 模板变量
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
渲染后的字符串
|
|
52
|
+
"""
|
|
53
|
+
template = Template(template_string, trim_blocks=True, lstrip_blocks=True)
|
|
54
|
+
return template.render(**context)
|
|
55
|
+
|
|
56
|
+
def render_file(self, template_name: str, context: dict[str, Any]) -> str:
|
|
57
|
+
"""
|
|
58
|
+
渲染文件模板
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
template_name: 模板文件名
|
|
62
|
+
context: 模板变量
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
渲染后的字符串
|
|
66
|
+
"""
|
|
67
|
+
if self.env is None:
|
|
68
|
+
raise ValueError("模板目录不存在,无法加载文件模板")
|
|
69
|
+
|
|
70
|
+
template = self.env.get_template(template_name)
|
|
71
|
+
return template.render(**context)
|
|
72
|
+
|
|
73
|
+
def render_to_file(self, template_name: str, context: dict[str, Any], output_path: Path | str) -> None:
|
|
74
|
+
"""
|
|
75
|
+
渲染模板并写入文件
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
template_name: 模板文件名
|
|
79
|
+
context: 模板变量
|
|
80
|
+
output_path: 输出文件路径
|
|
81
|
+
"""
|
|
82
|
+
content = self.render_file(template_name, context)
|
|
83
|
+
output_path = Path(output_path)
|
|
84
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
output_path.write_text(content, encoding="utf-8")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def prepare_common_context(**kwargs: Any) -> dict[str, Any]:
|
|
89
|
+
"""
|
|
90
|
+
准备通用模板变量
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
**kwargs: 额外的变量
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
模板变量字典
|
|
97
|
+
"""
|
|
98
|
+
from datetime import datetime
|
|
99
|
+
|
|
100
|
+
from mpdt.utils.file_ops import get_git_user_info
|
|
101
|
+
|
|
102
|
+
git_info = get_git_user_info()
|
|
103
|
+
|
|
104
|
+
context = {
|
|
105
|
+
"timestamp": datetime.now().isoformat(),
|
|
106
|
+
"year": datetime.now().year,
|
|
107
|
+
"author": git_info.get("name", ""),
|
|
108
|
+
"author_email": git_info.get("email", ""),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
context.update(kwargs)
|
|
112
|
+
return context
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
验证器模块
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .auto_fix_validator import AutoFixValidator
|
|
6
|
+
from .base import BaseValidator, ValidationIssue, ValidationLevel, ValidationResult
|
|
7
|
+
from .component_validator import ComponentValidator
|
|
8
|
+
from .config_validator import ConfigValidator
|
|
9
|
+
from .metadata_validator import MetadataValidator
|
|
10
|
+
from .structure_validator import StructureValidator
|
|
11
|
+
from .style_validator import StyleValidator
|
|
12
|
+
from .type_validator import TypeValidator
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BaseValidator",
|
|
16
|
+
"ValidationResult",
|
|
17
|
+
"ValidationIssue",
|
|
18
|
+
"ValidationLevel",
|
|
19
|
+
"StructureValidator",
|
|
20
|
+
"MetadataValidator",
|
|
21
|
+
"ComponentValidator",
|
|
22
|
+
"ConfigValidator",
|
|
23
|
+
"StyleValidator",
|
|
24
|
+
"TypeValidator",
|
|
25
|
+
"AutoFixValidator",
|
|
26
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""自动修复验证器
|
|
2
|
+
|
|
3
|
+
提供自动修复常见问题的功能
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .base import BaseValidator, ValidationResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AutoFixValidator(BaseValidator):
|
|
13
|
+
"""自动修复验证器
|
|
14
|
+
|
|
15
|
+
自动修复插件中的常见问题
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, plugin_path: Path):
|
|
19
|
+
super().__init__(plugin_path)
|
|
20
|
+
self.fixes_applied = []
|
|
21
|
+
|
|
22
|
+
def validate(self) -> ValidationResult:
|
|
23
|
+
"""执行自动修复(实际上是 fix 而非 validate)"""
|
|
24
|
+
result = ValidationResult(
|
|
25
|
+
validator_name="AutoFixValidator",
|
|
26
|
+
success=True
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
plugin_name = self._get_plugin_name()
|
|
30
|
+
if not plugin_name:
|
|
31
|
+
result.add_error("无法确定插件名称")
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
# 修复缺失的元数据
|
|
35
|
+
self._fix_missing_metadata(result)
|
|
36
|
+
|
|
37
|
+
# 修复导入顺序
|
|
38
|
+
self._fix_import_order(result)
|
|
39
|
+
|
|
40
|
+
# 汇总修复结果
|
|
41
|
+
if self.fixes_applied:
|
|
42
|
+
result.add_info(f"应用了 {len(self.fixes_applied)} 个自动修复")
|
|
43
|
+
for fix in self.fixes_applied:
|
|
44
|
+
result.add_info(fix)
|
|
45
|
+
else:
|
|
46
|
+
result.add_info("未发现可自动修复的问题")
|
|
47
|
+
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
def _fix_missing_metadata(self, result: ValidationResult) -> None:
|
|
51
|
+
"""修复缺失的元数据字段"""
|
|
52
|
+
init_file = self.plugin_path / "__init__.py"
|
|
53
|
+
if not init_file.exists():
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
content = init_file.read_text(encoding="utf-8")
|
|
58
|
+
tree = ast.parse(content)
|
|
59
|
+
|
|
60
|
+
# 查找 __plugin_meta__ 定义
|
|
61
|
+
meta_found = False
|
|
62
|
+
for node in ast.walk(tree):
|
|
63
|
+
if isinstance(node, ast.Assign):
|
|
64
|
+
for target in node.targets:
|
|
65
|
+
if isinstance(target, ast.Name) and target.id == "__plugin_meta__":
|
|
66
|
+
meta_found = True
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
if not meta_found:
|
|
70
|
+
# 添加基本的 __plugin_meta__ 定义
|
|
71
|
+
plugin_name = self._get_plugin_name()
|
|
72
|
+
meta_template = f'''
|
|
73
|
+
from src.plugin_system import PluginMetadata
|
|
74
|
+
|
|
75
|
+
__plugin_meta__ = PluginMetadata(
|
|
76
|
+
name="{plugin_name}",
|
|
77
|
+
description="插件描述",
|
|
78
|
+
usage="插件使用说明",
|
|
79
|
+
version="0.1.0",
|
|
80
|
+
author="",
|
|
81
|
+
license="MIT"
|
|
82
|
+
)
|
|
83
|
+
'''
|
|
84
|
+
# 在文件开头添加
|
|
85
|
+
new_content = meta_template + "\n" + content
|
|
86
|
+
init_file.write_text(new_content, encoding="utf-8")
|
|
87
|
+
self.fixes_applied.append("添加缺失的 __plugin_meta__ 定义到 __init__.py")
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
result.add_warning(f"无法自动修复元数据: {e}")
|
|
91
|
+
|
|
92
|
+
def _fix_import_order(self, result: ValidationResult) -> None:
|
|
93
|
+
"""修复导入顺序(使用 ruff --fix)"""
|
|
94
|
+
try:
|
|
95
|
+
import subprocess
|
|
96
|
+
|
|
97
|
+
# 查找所有 Python 文件
|
|
98
|
+
python_files = list(self.plugin_path.rglob("*.py"))
|
|
99
|
+
|
|
100
|
+
for py_file in python_files:
|
|
101
|
+
# 使用 ruff 的 isort 规则修复导入顺序
|
|
102
|
+
subprocess.run(
|
|
103
|
+
["ruff", "check", "--select", "I", "--fix", str(py_file)],
|
|
104
|
+
capture_output=True,
|
|
105
|
+
check=False # 不因错误退出而失败
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.fixes_applied.append("修复导入顺序")
|
|
109
|
+
|
|
110
|
+
except Exception:
|
|
111
|
+
# 如果 ruff 未安装,静默失败
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def fix_component_metadata(self, component_file: Path, component_class: str, missing_fields: list[str]) -> bool:
|
|
115
|
+
"""修复组件缺失的元数据字段
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
component_file: 组件文件路径
|
|
119
|
+
component_class: 组件类名
|
|
120
|
+
missing_fields: 缺失的字段列表
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
是否成功修复
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
content = component_file.read_text(encoding="utf-8")
|
|
127
|
+
tree = ast.parse(content)
|
|
128
|
+
|
|
129
|
+
# 查找组件类
|
|
130
|
+
for node in ast.walk(tree):
|
|
131
|
+
if isinstance(node, ast.ClassDef) and node.name == component_class:
|
|
132
|
+
# 在类定义中添加缺失的字段
|
|
133
|
+
indent = " " # 4 个空格缩进
|
|
134
|
+
fields_to_add = []
|
|
135
|
+
|
|
136
|
+
for field in missing_fields:
|
|
137
|
+
# 根据字段类型生成默认值
|
|
138
|
+
if field.endswith("_name"):
|
|
139
|
+
value = f'"{component_class.lower().replace("_", "-")}"'
|
|
140
|
+
elif field.endswith("_description"):
|
|
141
|
+
value = f'"{component_class} 组件"'
|
|
142
|
+
else:
|
|
143
|
+
value = '""'
|
|
144
|
+
|
|
145
|
+
fields_to_add.append(f"{indent}{field}: str = {value}")
|
|
146
|
+
|
|
147
|
+
# 找到类体的第一行(文档字符串后)
|
|
148
|
+
lines = content.split('\n')
|
|
149
|
+
class_line = None
|
|
150
|
+
for i, line in enumerate(lines):
|
|
151
|
+
if f"class {component_class}" in line:
|
|
152
|
+
class_line = i
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
if class_line is not None:
|
|
156
|
+
# 找到插入位置(类定义后,第一个方法前)
|
|
157
|
+
insert_line = class_line + 1
|
|
158
|
+
|
|
159
|
+
# 跳过文档字符串
|
|
160
|
+
if insert_line < len(lines) and '"""' in lines[insert_line]:
|
|
161
|
+
while insert_line < len(lines) and not lines[insert_line].strip().endswith('"""'):
|
|
162
|
+
insert_line += 1
|
|
163
|
+
insert_line += 1
|
|
164
|
+
|
|
165
|
+
# 插入字段
|
|
166
|
+
for field_def in fields_to_add:
|
|
167
|
+
lines.insert(insert_line, field_def)
|
|
168
|
+
insert_line += 1
|
|
169
|
+
|
|
170
|
+
# 写回文件
|
|
171
|
+
new_content = '\n'.join(lines)
|
|
172
|
+
component_file.write_text(new_content, encoding="utf-8")
|
|
173
|
+
|
|
174
|
+
self.fixes_applied.append(
|
|
175
|
+
f"为 {component_class} 添加缺失的元数据字段: {', '.join(missing_fields)}"
|
|
176
|
+
)
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
except Exception:
|
|
182
|
+
return False
|
mpdt/validators/base.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
验证器基类
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ValidationLevel(str, Enum):
|
|
12
|
+
"""验证级别"""
|
|
13
|
+
|
|
14
|
+
ERROR = "error"
|
|
15
|
+
WARNING = "warning"
|
|
16
|
+
INFO = "info"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ValidationIssue:
|
|
21
|
+
"""验证问题"""
|
|
22
|
+
|
|
23
|
+
level: ValidationLevel
|
|
24
|
+
message: str
|
|
25
|
+
file_path: str | None = None
|
|
26
|
+
line_number: int | None = None
|
|
27
|
+
suggestion: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ValidationResult:
|
|
32
|
+
"""验证结果"""
|
|
33
|
+
|
|
34
|
+
validator_name: str
|
|
35
|
+
issues: list[ValidationIssue] = field(default_factory=list)
|
|
36
|
+
success: bool = True
|
|
37
|
+
|
|
38
|
+
def add_error(self, message: str, file_path: str | None = None, line_number: int | None = None, suggestion: str | None = None) -> None:
|
|
39
|
+
"""添加错误"""
|
|
40
|
+
self.issues.append(
|
|
41
|
+
ValidationIssue(
|
|
42
|
+
level=ValidationLevel.ERROR,
|
|
43
|
+
message=message,
|
|
44
|
+
file_path=file_path,
|
|
45
|
+
line_number=line_number,
|
|
46
|
+
suggestion=suggestion,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
self.success = False
|
|
50
|
+
|
|
51
|
+
def add_warning(self, message: str, file_path: str | None = None, line_number: int | None = None, suggestion: str | None = None) -> None:
|
|
52
|
+
"""添加警告"""
|
|
53
|
+
self.issues.append(
|
|
54
|
+
ValidationIssue(
|
|
55
|
+
level=ValidationLevel.WARNING,
|
|
56
|
+
message=message,
|
|
57
|
+
file_path=file_path,
|
|
58
|
+
line_number=line_number,
|
|
59
|
+
suggestion=suggestion,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def add_info(self, message: str, file_path: str | None = None, line_number: int | None = None) -> None:
|
|
64
|
+
"""添加信息"""
|
|
65
|
+
self.issues.append(
|
|
66
|
+
ValidationIssue(
|
|
67
|
+
level=ValidationLevel.INFO,
|
|
68
|
+
message=message,
|
|
69
|
+
file_path=file_path,
|
|
70
|
+
line_number=line_number,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def error_count(self) -> int:
|
|
76
|
+
"""错误数量"""
|
|
77
|
+
return sum(1 for issue in self.issues if issue.level == ValidationLevel.ERROR)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def warning_count(self) -> int:
|
|
81
|
+
"""警告数量"""
|
|
82
|
+
return sum(1 for issue in self.issues if issue.level == ValidationLevel.WARNING)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def info_count(self) -> int:
|
|
86
|
+
"""信息数量"""
|
|
87
|
+
return sum(1 for issue in self.issues if issue.level == ValidationLevel.INFO)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BaseValidator(ABC):
|
|
91
|
+
"""验证器基类"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, plugin_path: Path):
|
|
94
|
+
"""初始化验证器
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
plugin_path: 插件路径
|
|
98
|
+
"""
|
|
99
|
+
self.plugin_path = plugin_path
|
|
100
|
+
self.result = ValidationResult(validator_name=self.__class__.__name__)
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def validate(self) -> ValidationResult:
|
|
104
|
+
"""执行验证
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
ValidationResult: 验证结果
|
|
108
|
+
"""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def _get_plugin_name(self) -> str | None:
|
|
112
|
+
"""获取插件名称(从目录名)
|
|
113
|
+
|
|
114
|
+
插件结构: my_plugin/plugin.py
|
|
115
|
+
"""
|
|
116
|
+
# 检查 plugin.py 是否在根目录
|
|
117
|
+
if (self.plugin_path / "plugin.py").exists():
|
|
118
|
+
# 返回插件路径的目录名
|
|
119
|
+
return self.plugin_path.name
|
|
120
|
+
|
|
121
|
+
return None
|