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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. mofox_plugin_dev_toolkit-0.2.1.dist-info/METADATA +409 -0
  2. mofox_plugin_dev_toolkit-0.2.1.dist-info/RECORD +43 -0
  3. mofox_plugin_dev_toolkit-0.2.1.dist-info/WHEEL +5 -0
  4. mofox_plugin_dev_toolkit-0.2.1.dist-info/entry_points.txt +2 -0
  5. mofox_plugin_dev_toolkit-0.2.1.dist-info/licenses/LICENSE +674 -0
  6. mofox_plugin_dev_toolkit-0.2.1.dist-info/top_level.txt +1 -0
  7. mpdt/__init__.py +15 -0
  8. mpdt/__main__.py +8 -0
  9. mpdt/cli.py +314 -0
  10. mpdt/commands/__init__.py +9 -0
  11. mpdt/commands/check.py +316 -0
  12. mpdt/commands/dev.py +550 -0
  13. mpdt/commands/generate.py +366 -0
  14. mpdt/commands/init.py +487 -0
  15. mpdt/dev/bridge_plugin/__init__.py +17 -0
  16. mpdt/dev/bridge_plugin/discovery_server.py +126 -0
  17. mpdt/dev/bridge_plugin/plugin.py +258 -0
  18. mpdt/templates/__init__.py +165 -0
  19. mpdt/templates/action_template.py +102 -0
  20. mpdt/templates/adapter_template.py +129 -0
  21. mpdt/templates/chatter_template.py +103 -0
  22. mpdt/templates/event_template.py +116 -0
  23. mpdt/templates/plus_command_template.py +150 -0
  24. mpdt/templates/prompt_template.py +92 -0
  25. mpdt/templates/router_template.py +175 -0
  26. mpdt/templates/tool_template.py +98 -0
  27. mpdt/utils/__init__.py +10 -0
  28. mpdt/utils/color_printer.py +99 -0
  29. mpdt/utils/config_loader.py +171 -0
  30. mpdt/utils/config_manager.py +297 -0
  31. mpdt/utils/file_ops.py +203 -0
  32. mpdt/utils/license_generator.py +980 -0
  33. mpdt/utils/plugin_parser.py +196 -0
  34. mpdt/utils/template_engine.py +112 -0
  35. mpdt/validators/__init__.py +26 -0
  36. mpdt/validators/auto_fix_validator.py +182 -0
  37. mpdt/validators/base.py +121 -0
  38. mpdt/validators/component_validator.py +415 -0
  39. mpdt/validators/config_validator.py +173 -0
  40. mpdt/validators/metadata_validator.py +125 -0
  41. mpdt/validators/structure_validator.py +70 -0
  42. mpdt/validators/style_validator.py +125 -0
  43. mpdt/validators/type_validator.py +223 -0
@@ -0,0 +1,125 @@
1
+ """
2
+ 插件元数据验证器
3
+ """
4
+
5
+ import ast
6
+
7
+ from .base import BaseValidator, ValidationResult
8
+
9
+
10
+ class MetadataValidator(BaseValidator):
11
+ """插件元数据验证器
12
+
13
+ 检查 plugin.py 中的 PluginMetadata 是否完整
14
+ """
15
+
16
+ # 必需的元数据字段
17
+ REQUIRED_FIELDS = ["name", "description", "usage"]
18
+
19
+ # 推荐的元数据字段
20
+ RECOMMENDED_FIELDS = ["version", "author", "license"]
21
+
22
+ def validate(self) -> ValidationResult:
23
+ """执行元数据验证
24
+
25
+ Returns:
26
+ ValidationResult: 验证结果
27
+ """
28
+ # 获取插件名称
29
+ plugin_name = self._get_plugin_name()
30
+ if not plugin_name:
31
+ self.result.add_error("无法确定插件名称")
32
+ return self.result
33
+
34
+ # 元数据在 __init__.py 中
35
+ init_file = self.plugin_path / "__init__.py"
36
+ if not init_file.exists():
37
+ self.result.add_error(
38
+ "__init__.py 文件不存在",
39
+ suggestion="请创建 __init__.py 文件并定义 __plugin_meta__",
40
+ )
41
+ return self.result
42
+
43
+ # 读取并解析 __init__.py
44
+ try:
45
+ with open(init_file, encoding="utf-8") as f:
46
+ tree = ast.parse(f.read(), filename=str(init_file))
47
+ except SyntaxError as e:
48
+ self.result.add_error(
49
+ f"__init__.py 存在语法错误: {e.msg}",
50
+ file_path="__init__.py",
51
+ line_number=e.lineno,
52
+ )
53
+ return self.result
54
+ except Exception as e:
55
+ self.result.add_error(f"读取 __init__.py 失败: {e}")
56
+ return self.result
57
+
58
+ # 查找 __plugin_meta__ 变量赋值
59
+ metadata_found = False
60
+ metadata_node = None
61
+
62
+ for node in ast.walk(tree):
63
+ # 查找 __plugin_meta__ = PluginMetadata(...) 赋值
64
+ if isinstance(node, ast.Assign):
65
+ for target in node.targets:
66
+ if isinstance(target, ast.Name) and target.id == "__plugin_meta__":
67
+ if isinstance(node.value, ast.Call):
68
+ if isinstance(node.value.func, ast.Name) and node.value.func.id == "PluginMetadata":
69
+ metadata_found = True
70
+ metadata_node = node.value
71
+ break
72
+
73
+ if not metadata_found:
74
+ self.result.add_error(
75
+ "未找到 __plugin_meta__ 变量或 PluginMetadata 实例",
76
+ file_path="__init__.py",
77
+ suggestion="请在 __init__.py 中定义: __plugin_meta__ = PluginMetadata(...)",
78
+ )
79
+ return self.result
80
+
81
+ # 提取元数据字段
82
+ metadata_fields = {}
83
+ if metadata_node:
84
+ # 处理关键字参数
85
+ for keyword in metadata_node.keywords:
86
+ if keyword.arg:
87
+ # 尝试获取值
88
+ value = self._extract_value(keyword.value)
89
+ metadata_fields[keyword.arg] = value
90
+
91
+ # 检查必需字段
92
+ for field in self.REQUIRED_FIELDS:
93
+ if field not in metadata_fields:
94
+ self.result.add_error(
95
+ f"PluginMetadata 缺少必需字段: {field}",
96
+ file_path="__init__.py",
97
+ )
98
+ elif not metadata_fields[field]:
99
+ self.result.add_warning(
100
+ f"PluginMetadata 字段 {field} 为空",
101
+ file_path="__init__.py",
102
+ )
103
+
104
+ # 检查推荐字段
105
+ for field in self.RECOMMENDED_FIELDS:
106
+ if field not in metadata_fields:
107
+ self.result.add_warning(
108
+ f"PluginMetadata 缺少推荐字段: {field}",
109
+ file_path="__init__.py",
110
+ suggestion=f"建议添加 {field} 字段",
111
+ )
112
+
113
+ return self.result
114
+
115
+ def _extract_value(self, node: ast.AST) -> str | None:
116
+ """提取 AST 节点的值"""
117
+ if isinstance(node, ast.Constant):
118
+ return str(node.value) if node.value else None
119
+ elif isinstance(node, ast.Str): # Python 3.7 兼容
120
+ return str(node.s) if node.s else None
121
+ elif isinstance(node, ast.List):
122
+ return "[...]" # 列表类型
123
+ elif isinstance(node, ast.Dict):
124
+ return "{...}" # 字典类型
125
+ return None
@@ -0,0 +1,70 @@
1
+ """
2
+ 插件结构验证器
3
+ """
4
+
5
+ from .base import BaseValidator, ValidationResult
6
+
7
+
8
+ class StructureValidator(BaseValidator):
9
+ """插件结构验证器
10
+
11
+ 检查插件的目录结构和必需文件
12
+ """
13
+
14
+ # 必需的文件
15
+ REQUIRED_FILES = ["__init__.py", "plugin.py"]
16
+
17
+ # 推荐的文件
18
+ RECOMMENDED_FILES = ["README.md", "pyproject.toml", "requirements.txt"]
19
+
20
+ # 推荐的目录
21
+ RECOMMENDED_DIRS = ["tests", "docs"]
22
+
23
+ def validate(self) -> ValidationResult:
24
+ """执行结构验证
25
+
26
+ Returns:
27
+ ValidationResult: 验证结果
28
+ """
29
+ # 获取插件名称
30
+ plugin_name = self._get_plugin_name()
31
+ if not plugin_name:
32
+ self.result.add_error(
33
+ "无法确定插件名称,请确保插件目录结构正确",
34
+ suggestion="插件应该有 plugin.py 文件",
35
+ )
36
+ return self.result
37
+
38
+ # 插件代码目录就是根目录
39
+ plugin_code_dir = self.plugin_path
40
+
41
+ # 检查必需的文件
42
+ for file_name in self.REQUIRED_FILES:
43
+ file_path = plugin_code_dir / file_name
44
+ if not file_path.exists():
45
+ self.result.add_error(
46
+ f"缺少必需文件: {file_name}",
47
+ file_path=str(file_path.relative_to(self.plugin_path)),
48
+ )
49
+
50
+ # 检查推荐的文件
51
+ for file_name in self.RECOMMENDED_FILES:
52
+ file_path = self.plugin_path / file_name
53
+ if not file_path.exists():
54
+ self.result.add_warning(
55
+ f"缺少推荐文件: {file_name}",
56
+ file_path=file_name,
57
+ suggestion=f"建议添加 {file_name} 以提供更好的文档或依赖管理",
58
+ )
59
+
60
+ # 检查推荐的目录
61
+ for dir_name in self.RECOMMENDED_DIRS:
62
+ dir_path = self.plugin_path / dir_name
63
+ if not dir_path.exists():
64
+ self.result.add_warning(
65
+ f"缺少推荐目录: {dir_name}/",
66
+ file_path=dir_name,
67
+ suggestion=f"建议添加 {dir_name}/ 目录",
68
+ )
69
+
70
+ return self.result
@@ -0,0 +1,125 @@
1
+ """代码风格验证器
2
+
3
+ 使用 ruff 检查代码风格问题
4
+ """
5
+
6
+ import json
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .base import BaseValidator, ValidationResult
12
+
13
+
14
+ class StyleValidator(BaseValidator):
15
+ """代码风格验证器
16
+
17
+ 使用 ruff 检查代码风格和代码质量问题
18
+ """
19
+
20
+ def __init__(self, plugin_path: Path, auto_fix: bool = False):
21
+ super().__init__(plugin_path)
22
+ self.auto_fix = auto_fix
23
+
24
+ def validate(self) -> ValidationResult:
25
+ """执行代码风格检查"""
26
+ result = ValidationResult(
27
+ validator_name="StyleValidator",
28
+ success=True
29
+ )
30
+
31
+ plugin_name = self._get_plugin_name()
32
+ if not plugin_name:
33
+ result.add_error("无法确定插件名称")
34
+ return result
35
+
36
+ # 检查 ruff 是否安装
37
+ if not self._is_ruff_installed():
38
+ result.add_warning(
39
+ "未安装 ruff,跳过代码风格检查",
40
+ suggestion="运行 'pip install ruff' 安装"
41
+ )
42
+ return result
43
+
44
+ # 运行 ruff check
45
+ issues = self._run_ruff_check()
46
+
47
+ if issues:
48
+ for issue in issues:
49
+ result.add_warning(
50
+ issue["message"],
51
+ file_path=issue.get("file"),
52
+ line_number=issue.get("line"),
53
+ suggestion=issue.get("suggestion")
54
+ )
55
+ else:
56
+ result.add_info("代码风格检查通过,未发现问题")
57
+
58
+ return result
59
+
60
+ def _is_ruff_installed(self) -> bool:
61
+ """检查 ruff 是否安装"""
62
+ try:
63
+ subprocess.run(
64
+ ["ruff", "--version"],
65
+ capture_output=True,
66
+ check=True
67
+ )
68
+ return True
69
+ except (subprocess.CalledProcessError, FileNotFoundError):
70
+ return False
71
+
72
+ def _run_ruff_check(self) -> list[dict[str, Any]]:
73
+ """运行 ruff 检查
74
+
75
+ Returns:
76
+ 问题列表
77
+ """
78
+ issues = []
79
+
80
+ try:
81
+ # 构建命令
82
+ cmd = ["ruff", "check"]
83
+ if self.auto_fix:
84
+ cmd.append("--fix")
85
+ cmd.extend(["--output-format", "json", str(self.plugin_path)])
86
+
87
+ # 运行 ruff
88
+ result = subprocess.run(
89
+ cmd,
90
+ capture_output=True,
91
+ text=True,
92
+ encoding='utf-8'
93
+ )
94
+
95
+ # 解析输出
96
+ if result.stdout.strip():
97
+ try:
98
+ ruff_output = json.loads(result.stdout)
99
+ for item in ruff_output:
100
+ issues.append({
101
+ "file": str(Path(item["filename"]).relative_to(self.plugin_path)),
102
+ "line": item["location"]["row"],
103
+ "message": f"{item['code']}: {item['message']}",
104
+ "suggestion": self._get_fix_suggestion(item)
105
+ })
106
+ except json.JSONDecodeError:
107
+ # 如果不是 JSON 格式,尝试解析纯文本
108
+ pass
109
+
110
+ except Exception as e:
111
+ # 不抛出异常,只记录问题
112
+ issues.append({
113
+ "file": None,
114
+ "line": None,
115
+ "message": f"运行 ruff 时出错: {e}",
116
+ "suggestion": None
117
+ })
118
+
119
+ return issues
120
+
121
+ def _get_fix_suggestion(self, item: dict) -> str | None:
122
+ """获取修复建议"""
123
+ if item.get("fix"):
124
+ return "可自动修复,使用 --auto-fix 选项"
125
+ return None
@@ -0,0 +1,223 @@
1
+ """类型检查验证器
2
+
3
+ 使用 mypy 进行类型检查
4
+ """
5
+
6
+ import re
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ from .base import BaseValidator, ValidationResult
11
+
12
+
13
+ class TypeValidator(BaseValidator):
14
+ """类型检查验证器
15
+
16
+ 使用 mypy 进行静态类型检查
17
+ """
18
+
19
+ def __init__(self, plugin_path: Path):
20
+ super().__init__(plugin_path)
21
+ # 尝试找到 MoFox 主项目路径
22
+ self.MoFox_root = self._find_mofox_root()
23
+
24
+ def validate(self) -> ValidationResult:
25
+ """执行类型检查"""
26
+ result = ValidationResult(
27
+ validator_name="TypeValidator",
28
+ success=True
29
+ )
30
+
31
+ plugin_name = self._get_plugin_name()
32
+ if not plugin_name:
33
+ result.add_error("无法确定插件名称")
34
+ return result
35
+
36
+ # 检查 mypy 是否安装
37
+ if not self._is_mypy_installed():
38
+ result.add_warning(
39
+ "未安装 mypy,跳过类型检查",
40
+ suggestion="运行 'pip install mypy' 安装"
41
+ )
42
+ return result
43
+
44
+ # 运行 mypy
45
+ issues = self._run_mypy_check()
46
+
47
+ if issues:
48
+ for issue in issues:
49
+ # 根据严重程度决定是错误还是警告
50
+ if issue.get("severity") == "error":
51
+ result.add_error(
52
+ issue["message"],
53
+ file_path=issue.get("file"),
54
+ line_number=issue.get("line"),
55
+ suggestion=issue.get("suggestion")
56
+ )
57
+ else:
58
+ result.add_warning(
59
+ issue["message"],
60
+ file_path=issue.get("file"),
61
+ line_number=issue.get("line"),
62
+ suggestion=issue.get("suggestion")
63
+ )
64
+ else:
65
+ result.add_info("类型检查通过,未发现类型问题")
66
+
67
+ return result
68
+
69
+ def _is_mypy_installed(self) -> bool:
70
+ """检查 mypy 是否安装"""
71
+ try:
72
+ subprocess.run(
73
+ ["mypy", "--version"],
74
+ capture_output=True,
75
+ check=True
76
+ )
77
+ return True
78
+ except (subprocess.CalledProcessError, FileNotFoundError):
79
+ return False
80
+
81
+ def _find_mofox_root(self) -> Path | None:
82
+ """尝试找到 MoFox 主项目根目录
83
+
84
+ 从插件路径向上查找,直到找到包含 src/ 目录的父目录
85
+ """
86
+ current = self.plugin_path.parent
87
+
88
+ # 最多向上查找 5 层
89
+ for _ in range(5):
90
+ if not current or current == current.parent:
91
+ break
92
+
93
+ # 检查是否存在 src/ 目录且包含 plugin_system 等模块
94
+ src_dir = current / "src"
95
+ if src_dir.exists() and src_dir.is_dir():
96
+ # 验证是否是 MMC 项目
97
+ if (src_dir / "plugin_system").exists() or (src_dir / "common").exists():
98
+ return current
99
+
100
+ current = current.parent
101
+
102
+ return None
103
+
104
+ def _run_mypy_check(self) -> list[dict]:
105
+ """运行 mypy 检查
106
+
107
+ Returns:
108
+ 问题列表
109
+ """
110
+ issues = []
111
+
112
+ try:
113
+ # 构建命令
114
+ cmd = [
115
+ "mypy",
116
+ str(self.plugin_path),
117
+ "--no-error-summary",
118
+ "--show-column-numbers",
119
+ "--show-error-codes",
120
+ "--no-namespace-packages", # 避免包命名空间问题
121
+ ]
122
+
123
+ # 如果找到了 MMC 主项目,添加到 Python 路径
124
+ if self.MoFox_root:
125
+ cmd.extend(["--python-path", str(self.MoFox_root)])
126
+
127
+ # 运行 mypy
128
+ result = subprocess.run(
129
+ cmd,
130
+ capture_output=True,
131
+ text=True
132
+ )
133
+
134
+ # 解析输出
135
+ if result.stdout.strip():
136
+ for line in result.stdout.strip().split('\n'):
137
+ issue = self._parse_mypy_line(line)
138
+ if issue:
139
+ # 如果找不到 MoFox 根目录,过滤掉所有 src.* 模块的导入错误
140
+ if not self.MoFox_root:
141
+ # 检查是否是 src.* 模块的导入错误
142
+ if "Cannot find implementation" in issue["message"] and '"src.' in issue["message"]:
143
+ continue # 跳过这个错误
144
+ # 检查是否是因为基类是 Any 导致的错误(因为导入失败)
145
+ if "has type \"Any\"" in issue["message"]:
146
+ continue # 跳过这个错误
147
+ issues.append(issue)
148
+
149
+ except Exception as e:
150
+ issues.append({
151
+ "file": None,
152
+ "line": None,
153
+ "message": f"运行 mypy 时出错: {e}",
154
+ "severity": "error",
155
+ "suggestion": None
156
+ })
157
+
158
+ return issues
159
+
160
+ def _parse_mypy_line(self, line: str) -> dict | None:
161
+ """解析 mypy 输出的一行
162
+
163
+ 格式: file.py:123:45: error: Message [error-code]
164
+
165
+ Args:
166
+ line: mypy 输出的一行
167
+
168
+ Returns:
169
+ 解析后的问题字典,如果解析失败返回 None
170
+ """
171
+ # 匹配 mypy 输出格式
172
+ pattern = r'^(.+?):(\d+):(?:\d+:)?\s+(error|warning|note):\s+(.+?)(?:\s+\[(.+?)\])?$'
173
+ match = re.match(pattern, line)
174
+
175
+ if not match:
176
+ return None
177
+
178
+ file_path, line_num, severity, message, error_code = match.groups()
179
+
180
+ try:
181
+ # 转换为相对路径
182
+ rel_path = str(Path(file_path).relative_to(self.plugin_path))
183
+ except ValueError:
184
+ rel_path = file_path
185
+
186
+ issue = {
187
+ "file": rel_path,
188
+ "line": int(line_num),
189
+ "message": message.strip(),
190
+ "severity": severity,
191
+ "suggestion": None
192
+ }
193
+
194
+ # 添加错误代码到消息中
195
+ if error_code:
196
+ issue["message"] = f"[{error_code}] {issue['message']}"
197
+
198
+ # 根据常见错误提供建议
199
+ issue["suggestion"] = self._get_type_hint_suggestion(message, error_code)
200
+
201
+ return issue
202
+
203
+ def _get_type_hint_suggestion(self, message: str, error_code: str | None) -> str | None:
204
+ """根据错误消息提供类型提示建议"""
205
+ if not error_code:
206
+ return None
207
+
208
+ suggestions = {
209
+ "no-untyped-def": "为函数添加类型注解",
210
+ "no-untyped-call": "被调用的函数缺少类型注解",
211
+ "assignment": "检查赋值的类型是否匹配",
212
+ "return-value": "检查返回值类型是否与声明一致",
213
+ "arg-type": "检查参数类型是否正确",
214
+ "attr-defined": "检查属性是否存在",
215
+ "name-defined": "检查名称是否定义",
216
+ "import": "检查导入是否正确"
217
+ }
218
+
219
+ for code, suggestion in suggestions.items():
220
+ if code in error_code:
221
+ return suggestion
222
+
223
+ return None