mofox-plugin-dev-toolkit 0.5.0__tar.gz → 0.5.1__tar.gz

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 (65) hide show
  1. {mofox_plugin_dev_toolkit-0.5.0/mofox_plugin_dev_toolkit.egg-info → mofox_plugin_dev_toolkit-0.5.1}/PKG-INFO +1 -1
  2. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1/mofox_plugin_dev_toolkit.egg-info}/PKG-INFO +1 -1
  3. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mofox_plugin_dev_toolkit.egg-info/SOURCES.txt +1 -0
  4. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/commands/build.py +17 -4
  5. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/commands/check.py +14 -0
  6. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/commands/init.py +30 -2
  7. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/commands/market.py +14 -5
  8. mofox_plugin_dev_toolkit-0.5.1/mpdt/utils/manifest_metadata.py +199 -0
  9. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/metadata_validator.py +8 -0
  10. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/pyproject.toml +1 -1
  11. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/LICENSE +0 -0
  12. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/MANIFEST.in +0 -0
  13. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/README.md +0 -0
  14. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mofox_plugin_dev_toolkit.egg-info/dependency_links.txt +0 -0
  15. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mofox_plugin_dev_toolkit.egg-info/entry_points.txt +0 -0
  16. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mofox_plugin_dev_toolkit.egg-info/requires.txt +0 -0
  17. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mofox_plugin_dev_toolkit.egg-info/top_level.txt +0 -0
  18. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/__init__.py +0 -0
  19. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/__main__.py +0 -0
  20. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/cli.py +0 -0
  21. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/commands/__init__.py +0 -0
  22. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/commands/dev.py +0 -0
  23. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/commands/generate.py +0 -0
  24. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/dev/bridge_plugin/__init__.py +0 -0
  25. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/dev/bridge_plugin/cleanup_handler.py +0 -0
  26. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/dev/bridge_plugin/dev_config.py +0 -0
  27. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/dev/bridge_plugin/file_watcher.py +0 -0
  28. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/dev/bridge_plugin/manifest.json +0 -0
  29. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/dev/bridge_plugin/plugin.py +0 -0
  30. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/market/__init__.py +0 -0
  31. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/market/client.py +0 -0
  32. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/market/config.py +0 -0
  33. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/market/git.py +0 -0
  34. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/market/github.py +0 -0
  35. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/market/manifest.py +0 -0
  36. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/__init__.py +0 -0
  37. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/action_template.py +0 -0
  38. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/adapter_template.py +0 -0
  39. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/chatter_template.py +0 -0
  40. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/collection_template.py +0 -0
  41. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/config_template.py +0 -0
  42. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/event_template.py +0 -0
  43. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/plus_command_template.py +0 -0
  44. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/prompt_template.py +0 -0
  45. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/router_template.py +0 -0
  46. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/service_template.py +0 -0
  47. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/templates/tool_template.py +0 -0
  48. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/__init__.py +0 -0
  49. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/code_parser.py +0 -0
  50. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/color_printer.py +0 -0
  51. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/config_loader.py +0 -0
  52. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/config_manager.py +0 -0
  53. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/file_ops.py +0 -0
  54. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/license_generator.py +0 -0
  55. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/plugin_parser.py +0 -0
  56. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/utils/template_engine.py +0 -0
  57. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/__init__.py +0 -0
  58. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/auto_fix_validator.py +0 -0
  59. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/base.py +0 -0
  60. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/component_validator.py +0 -0
  61. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/config_validator.py +0 -0
  62. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/structure_validator.py +0 -0
  63. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/style_validator.py +0 -0
  64. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/mpdt/validators/type_validator.py +0 -0
  65. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mofox-plugin-dev-toolkit
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: 开发工具集,用于快速创建、开发和测试 MoFox-Bot 插件
5
5
  Author-email: MoFox-Studio <wwwww95915@qq.com>
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mofox-plugin-dev-toolkit
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: 开发工具集,用于快速创建、开发和测试 MoFox-Bot 插件
5
5
  Author-email: MoFox-Studio <wwwww95915@qq.com>
6
6
  License: GPL-3.0-or-later
@@ -49,6 +49,7 @@ mpdt/utils/config_loader.py
49
49
  mpdt/utils/config_manager.py
50
50
  mpdt/utils/file_ops.py
51
51
  mpdt/utils/license_generator.py
52
+ mpdt/utils/manifest_metadata.py
52
53
  mpdt/utils/plugin_parser.py
53
54
  mpdt/utils/template_engine.py
54
55
  mpdt/validators/__init__.py
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import json
18
18
  import re
19
+ import sys
19
20
  import zipfile
20
21
  from dataclasses import dataclass
21
22
  from hashlib import sha256
@@ -31,6 +32,7 @@ from mpdt.utils.color_printer import (
31
32
  print_success,
32
33
  print_warning,
33
34
  )
35
+ from mpdt.utils.manifest_metadata import ensure_manifest_metadata_interactive, metadata_errors
34
36
 
35
37
  # 构建时默认排除的文件/目录名称(精确匹配)
36
38
  _EXCLUDE_NAMES: set[str] = {
@@ -179,6 +181,18 @@ def _save_manifest(plugin_dir: Path, manifest: dict) -> None:
179
181
  json.dump(manifest, f, ensure_ascii=False, indent=4)
180
182
 
181
183
 
184
+ def _validate_manifest_metadata(manifest: dict) -> None:
185
+ """验证打包所需的 manifest 元数据。"""
186
+ required = ["name", "version", "description", "author", "entry_point"]
187
+ for field in required:
188
+ if field not in manifest:
189
+ raise ValueError(f"manifest.json 缺少必需字段: '{field}'")
190
+
191
+ errors = metadata_errors(manifest)
192
+ if errors:
193
+ raise ValueError(errors[0])
194
+
195
+
182
196
  def build_plugin(
183
197
  plugin_path: str = ".",
184
198
  output_dir: str = "dist",
@@ -263,10 +277,9 @@ def build_package(
263
277
  if manifest is None:
264
278
  return
265
279
 
266
- required = ["name", "version", "description", "author", "entry_point"]
267
- for field in required:
268
- if field not in manifest:
269
- raise ValueError(f"manifest.json 缺少必需字段: '{field}'")
280
+ manifest = ensure_manifest_metadata_interactive(plugin_dir, manifest)
281
+
282
+ _validate_manifest_metadata(manifest)
270
283
 
271
284
  plugin_name: str = manifest["name"]
272
285
  plugin_version: str = manifest["version"]
@@ -2,12 +2,15 @@
2
2
  静态检查命令实现
3
3
  """
4
4
 
5
+ import json
6
+
5
7
  from pathlib import Path
6
8
 
7
9
  from rich.panel import Panel
8
10
  from rich.table import Table
9
11
 
10
12
  from mpdt.utils.color_printer import console, print_error, print_info, print_success, print_warning
13
+ from mpdt.utils.manifest_metadata import ensure_manifest_metadata_interactive
11
14
  from mpdt.validators import (
12
15
  AutoFixValidator,
13
16
  ComponentValidator,
@@ -77,6 +80,17 @@ def check_plugin(
77
80
  # 元数据验证
78
81
  if not skip_metadata:
79
82
  print_info("正在检查插件元数据...")
83
+ manifest_path = path / "manifest.json"
84
+ if manifest_path.exists():
85
+ try:
86
+ with open(manifest_path, encoding="utf-8") as f:
87
+ manifest_data = json.load(f)
88
+ ensure_manifest_metadata_interactive(path, manifest_data)
89
+ except json.JSONDecodeError:
90
+ pass
91
+ except ValueError as e:
92
+ print_error(str(e))
93
+ return
80
94
  validator = MetadataValidator(path)
81
95
  result = validator.validate()
82
96
  all_results.append(result)
@@ -17,6 +17,7 @@ from typing import Any
17
17
 
18
18
  import questionary
19
19
 
20
+ from mpdt.utils.manifest_metadata import prompt_manifest_metadata
20
21
  from mpdt.utils.color_printer import (
21
22
  console,
22
23
  print_error,
@@ -64,12 +65,20 @@ def init_plugin(
64
65
  if not plugin_name:
65
66
  plugin_info = _interactive_init()
66
67
  plugin_name = plugin_info["plugin_name"]
68
+ description = plugin_info.get("description", "")
67
69
  template = plugin_info["template"]
68
70
  author = plugin_info.get("author")
69
71
  email = plugin_info.get("email")
70
72
  license_type = plugin_info["license"]
71
73
  with_docs = plugin_info.get("with_docs", False)
72
74
  init_git = plugin_info.get("init_git", False)
75
+ categories = plugin_info["categories"]
76
+ tags = plugin_info["tags"]
77
+ else:
78
+ description = ""
79
+ metadata = prompt_manifest_metadata()
80
+ categories = metadata["categories"]
81
+ tags = metadata["tags"]
73
82
 
74
83
  # 此时 plugin_name 必定不为 None
75
84
  assert plugin_name is not None
@@ -96,11 +105,14 @@ def init_plugin(
96
105
  _create_plugin_structure(
97
106
  plugin_dir=plugin_dir,
98
107
  plugin_name=plugin_name,
108
+ description=description,
99
109
  template=template,
100
110
  author=author,
101
111
  email=email,
102
112
  license_type=license_type,
103
113
  with_docs=with_docs,
114
+ categories=categories,
115
+ tags=tags,
104
116
  verbose=verbose,
105
117
  )
106
118
 
@@ -194,6 +206,10 @@ def _interactive_init() -> dict[str, Any]:
194
206
  ),
195
207
  ).ask()
196
208
 
209
+ metadata = prompt_manifest_metadata()
210
+ answers["categories"] = metadata["categories"]
211
+ answers["tags"] = metadata["tags"]
212
+
197
213
  return answers
198
214
 
199
215
  # ============================================================================
@@ -204,11 +220,14 @@ def _interactive_init() -> dict[str, Any]:
204
220
  def _create_plugin_structure(
205
221
  plugin_dir: Path,
206
222
  plugin_name: str,
223
+ description: str,
207
224
  template: str,
208
225
  author: str | None,
209
226
  email: str | None,
210
227
  license_type: str,
211
228
  with_docs: bool,
229
+ categories: list[str],
230
+ tags: list[str],
212
231
  verbose: bool,
213
232
  ) -> None:
214
233
  """创建插件目录结构"""
@@ -217,7 +236,7 @@ def _create_plugin_structure(
217
236
  ensure_dir(plugin_dir)
218
237
 
219
238
  # 创建 manifest.json
220
- manifest_content = _generate_manifest_file(plugin_name, author, template)
239
+ manifest_content = _generate_manifest_file(plugin_name, author, template, description, categories, tags)
221
240
  safe_write_file(plugin_dir / "manifest.json", manifest_content)
222
241
  if verbose:
223
242
  console.print("[dim]✓ 生成清单文件: manifest.json[/dim]")
@@ -281,7 +300,14 @@ def _create_plugin_structure(
281
300
  console.print(f"[dim]✓ 生成许可证文件: {license_type}[/dim]")
282
301
 
283
302
 
284
- def _generate_manifest_file(plugin_name: str, author: str | None, template: str, description: str = "") -> str:
303
+ def _generate_manifest_file(
304
+ plugin_name: str,
305
+ author: str | None,
306
+ template: str,
307
+ description: str = "",
308
+ categories: list[str] | None = None,
309
+ tags: list[str] | None = None,
310
+ ) -> str:
285
311
  """生成 manifest.json 文件内容
286
312
 
287
313
  Args:
@@ -346,6 +372,8 @@ def _generate_manifest_file(plugin_name: str, author: str | None, template: str,
346
372
  "version": "1.0.0",
347
373
  "description": description or f"{plugin_name} 插件",
348
374
  "author": author or "Your Name",
375
+ "categories": categories or ["tool"],
376
+ "tags": tags or [plugin_name],
349
377
  "dependencies": {"plugins": [], "components": []},
350
378
  "include": template_components.get(template, []),
351
379
  "entry_point": "plugin.py",
@@ -30,6 +30,7 @@ from mpdt.market.manifest import (
30
30
  version_payload,
31
31
  )
32
32
  from mpdt.utils.color_printer import console
33
+ from mpdt.utils.manifest_metadata import ensure_manifest_metadata_interactive_async
33
34
 
34
35
 
35
36
  def market_doctor(market_url: str | None = None, token: str | None = None) -> None:
@@ -49,7 +50,7 @@ def market_register(plugin_path: str = ".", market_url: str | None = None, token
49
50
  """Register a plugin in the market."""
50
51
 
51
52
  async def run() -> None:
52
- manifest = load_manifest(plugin_path)
53
+ manifest = await _load_manifest_for_market(plugin_path)
53
54
  payload = plugin_payload(manifest, repository_url=repository_url)
54
55
  result = await _client(market_url, token).register_plugin(payload)
55
56
  _print_ok(f"Plugin registered: {result['plugin_id']} ({result['status']})")
@@ -61,7 +62,7 @@ def market_update(plugin_path: str = ".", market_url: str | None = None, token:
61
62
  """Update plugin metadata in the market."""
62
63
 
63
64
  async def run() -> None:
64
- manifest = load_manifest(plugin_path)
65
+ manifest = await _load_manifest_for_market(plugin_path)
65
66
  payload = plugin_payload(manifest, repository_url=repository_url)
66
67
  plugin_id = payload.pop("plugin_id")
67
68
  result = await _client(market_url, token).update_plugin(plugin_id, payload)
@@ -98,7 +99,7 @@ def market_submit_version(
98
99
  """Build and submit a plugin version to the market."""
99
100
 
100
101
  async def run() -> None:
101
- manifest = load_manifest(plugin_path)
102
+ manifest = await _load_manifest_for_market(plugin_path)
102
103
  package = build_package(plugin_path=plugin_path, output_dir=output_dir, with_docs=with_docs, fmt="mfp", show_progress=False)
103
104
  if package is None:
104
105
  return
@@ -132,7 +133,7 @@ def market_sync(
132
133
  """Rebuild and sync version metadata."""
133
134
 
134
135
  async def run() -> None:
135
- manifest = load_manifest(plugin_path)
136
+ manifest = await _load_manifest_for_market(plugin_path)
136
137
  package = build_package(plugin_path=plugin_path, output_dir=output_dir, with_docs=with_docs, fmt="mfp", show_progress=False)
137
138
  if package is None:
138
139
  return
@@ -173,7 +174,7 @@ def market_publish(
173
174
 
174
175
  async def run() -> None:
175
176
  plugin_dir = Path(plugin_path).resolve()
176
- manifest = load_manifest(str(plugin_dir))
177
+ manifest = await _load_manifest_for_market(str(plugin_dir))
177
178
  package = build_package(plugin_path=str(plugin_dir), output_dir=output_dir, with_docs=with_docs, fmt="mfp", show_progress=False)
178
179
  if package is None:
179
180
  return
@@ -433,6 +434,14 @@ def _run_market(awaitable) -> None:
433
434
  _print_error(f"Market server connection failed: {e}")
434
435
 
435
436
 
437
+ async def _load_manifest_for_market(plugin_path: str) -> dict:
438
+ """Load manifest and repair required market metadata in async flows before proceeding."""
439
+ resolved_path = Path(plugin_path).resolve()
440
+ manifest = load_manifest(str(resolved_path))
441
+ await ensure_manifest_metadata_interactive_async(resolved_path, manifest)
442
+ return load_manifest(str(resolved_path))
443
+
444
+
436
445
  def _print_ok(message: str) -> None:
437
446
  """Print an ASCII success message."""
438
447
 
@@ -0,0 +1,199 @@
1
+ """Manifest metadata rules and interactive prompts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import questionary
12
+
13
+ from .color_printer import print_success, print_warning
14
+
15
+ ALLOWED_CATEGORIES = ("tool", "chat", "fun", "information", "moderation")
16
+
17
+
18
+ def get_categories(manifest: dict[str, Any]) -> list[str]:
19
+ """Return normalized categories from manifest."""
20
+ value = manifest.get("categories")
21
+ if not isinstance(value, list):
22
+ return []
23
+ return [item.strip() for item in value if isinstance(item, str) and item.strip()]
24
+
25
+
26
+ def get_tags(manifest: dict[str, Any]) -> list[str]:
27
+ """Return normalized tags from manifest."""
28
+ value = manifest.get("tags")
29
+ if not isinstance(value, list):
30
+ return []
31
+ normalized: list[str] = []
32
+ for item in value:
33
+ if not isinstance(item, str):
34
+ continue
35
+ tag = item.strip()
36
+ if tag and tag not in normalized:
37
+ normalized.append(tag)
38
+ return normalized
39
+
40
+
41
+ def normalize_tags(raw_tags: str) -> list[str]:
42
+ """Parse raw tag input into a de-duplicated tag list."""
43
+ tags: list[str] = []
44
+ normalized_input = raw_tags.replace(",", ",").replace("\n", ",")
45
+ for part in normalized_input.split(","):
46
+ tag = part.strip()
47
+ if tag and tag not in tags:
48
+ tags.append(tag)
49
+ return tags
50
+
51
+
52
+ def categories_are_valid(manifest: dict[str, Any]) -> bool:
53
+ """Return whether manifest categories satisfy the packaging rules."""
54
+ categories = get_categories(manifest)
55
+ return len(categories) == 1 and categories[0] in ALLOWED_CATEGORIES
56
+
57
+
58
+ def tags_are_valid(manifest: dict[str, Any]) -> bool:
59
+ """Return whether manifest tags satisfy the packaging rules."""
60
+ return bool(get_tags(manifest))
61
+
62
+
63
+ def metadata_errors(manifest: dict[str, Any]) -> list[str]:
64
+ """Return validation errors for required manifest metadata."""
65
+ errors: list[str] = []
66
+ if not categories_are_valid(manifest):
67
+ allowed = ", ".join(ALLOWED_CATEGORIES)
68
+ errors.append(f"manifest.json 的 categories 必须是只包含一个值的数组,且取值只能是: {allowed}")
69
+ if not tags_are_valid(manifest):
70
+ errors.append("manifest.json 的 tags 必须是至少包含一个非空字符串的数组")
71
+ return errors
72
+
73
+
74
+ def _is_interactive_terminal() -> bool:
75
+ """Return whether stdin/stdout are attached to a TTY."""
76
+ return sys.stdin.isatty() and sys.stdout.isatty()
77
+
78
+
79
+ def _has_running_event_loop() -> bool:
80
+ """Return whether the current thread is already inside a running event loop."""
81
+ try:
82
+ asyncio.get_running_loop()
83
+ except RuntimeError:
84
+ return False
85
+ return True
86
+
87
+
88
+ def _finalize_manifest_metadata(manifest: dict[str, Any]) -> dict[str, Any]:
89
+ """Normalize legacy single-value metadata keys."""
90
+ manifest.pop("category", None)
91
+ manifest.pop("tag", None)
92
+ return manifest
93
+
94
+
95
+ def _save_manifest(plugin_dir: Path, manifest: dict[str, Any]) -> None:
96
+ """Persist manifest.json after interactive metadata repair."""
97
+ manifest_path = plugin_dir / "manifest.json"
98
+ with open(manifest_path, "w", encoding="utf-8") as f:
99
+ json.dump(manifest, f, ensure_ascii=False, indent=4)
100
+
101
+
102
+ def prompt_manifest_metadata(existing_manifest: dict[str, Any] | None = None) -> dict[str, list[str]]:
103
+ """Collect required manifest metadata from the console."""
104
+ manifest = existing_manifest or {}
105
+ existing_categories = get_categories(manifest)
106
+ default_category = existing_categories[0] if existing_categories else ALLOWED_CATEGORIES[0]
107
+ existing_tags = ", ".join(get_tags(manifest))
108
+
109
+ category = questionary.select(
110
+ "选择插件分类 categories(只能选一个):",
111
+ choices=[questionary.Choice(item, value=item) for item in ALLOWED_CATEGORIES],
112
+ default=default_category,
113
+ ).ask()
114
+ if category is None:
115
+ raise ValueError("已取消填写 categories")
116
+
117
+ tags_text = questionary.text(
118
+ "填写插件 tags(多个标签用逗号分隔):",
119
+ default=existing_tags,
120
+ validate=lambda value: bool(normalize_tags(value)) or "至少填写一个 tag",
121
+ ).ask()
122
+ if tags_text is None:
123
+ raise ValueError("已取消填写 tags")
124
+
125
+ return {
126
+ "categories": [category],
127
+ "tags": normalize_tags(tags_text),
128
+ }
129
+
130
+
131
+ async def prompt_manifest_metadata_async(existing_manifest: dict[str, Any] | None = None) -> dict[str, list[str]]:
132
+ """Collect required manifest metadata from the console inside an event loop."""
133
+ manifest = existing_manifest or {}
134
+ existing_categories = get_categories(manifest)
135
+ default_category = existing_categories[0] if existing_categories else ALLOWED_CATEGORIES[0]
136
+ existing_tags = ", ".join(get_tags(manifest))
137
+
138
+ category = await questionary.select(
139
+ "选择插件分类 categories(只能选一个):",
140
+ choices=[questionary.Choice(item, value=item) for item in ALLOWED_CATEGORIES],
141
+ default=default_category,
142
+ ).ask_async()
143
+ if category is None:
144
+ raise ValueError("已取消填写 categories")
145
+
146
+ tags_text = await questionary.text(
147
+ "填写插件 tags(多个标签用逗号分隔):",
148
+ default=existing_tags,
149
+ validate=lambda value: bool(normalize_tags(value)) or "至少填写一个 tag",
150
+ ).ask_async()
151
+ if tags_text is None:
152
+ raise ValueError("已取消填写 tags")
153
+
154
+ return {
155
+ "categories": [category],
156
+ "tags": normalize_tags(tags_text),
157
+ }
158
+
159
+
160
+ def ensure_manifest_metadata_interactive(plugin_dir: Path, manifest: dict[str, Any]) -> dict[str, Any]:
161
+ """Prompt for missing manifest metadata and persist it when running in a TTY."""
162
+ errors = metadata_errors(manifest)
163
+ if not errors:
164
+ return _finalize_manifest_metadata(manifest)
165
+
166
+ if not _is_interactive_terminal() or _has_running_event_loop():
167
+ return manifest
168
+
169
+ print_warning("检测到 manifest.json 缺少或无效的 categories/tags,进入交互补全...")
170
+ for error in errors:
171
+ print_warning(error)
172
+
173
+ manifest.update(prompt_manifest_metadata(manifest))
174
+ _finalize_manifest_metadata(manifest)
175
+ _save_manifest(plugin_dir, manifest)
176
+
177
+ print_success("已更新 manifest.json 中的 categories 和 tags")
178
+ return manifest
179
+
180
+
181
+ async def ensure_manifest_metadata_interactive_async(plugin_dir: Path, manifest: dict[str, Any]) -> dict[str, Any]:
182
+ """Async prompt for missing manifest metadata and persist it when running in a TTY."""
183
+ errors = metadata_errors(manifest)
184
+ if not errors:
185
+ return _finalize_manifest_metadata(manifest)
186
+
187
+ if not _is_interactive_terminal():
188
+ return manifest
189
+
190
+ print_warning("检测到 manifest.json 缺少或无效的 categories/tags,进入交互补全...")
191
+ for error in errors:
192
+ print_warning(error)
193
+
194
+ manifest.update(await prompt_manifest_metadata_async(manifest))
195
+ _finalize_manifest_metadata(manifest)
196
+ _save_manifest(plugin_dir, manifest)
197
+
198
+ print_success("已更新 manifest.json 中的 categories 和 tags")
199
+ return manifest
@@ -4,6 +4,7 @@
4
4
 
5
5
  import json
6
6
 
7
+ from ..utils.manifest_metadata import metadata_errors
7
8
  from .base import BaseValidator, ValidationResult
8
9
 
9
10
 
@@ -71,6 +72,13 @@ class MetadataValidator(BaseValidator):
71
72
  else:
72
73
  self.result.add_info("所有必需的元数据字段都已提供")
73
74
 
75
+ for error in metadata_errors(manifest_data):
76
+ self.result.add_error(
77
+ error,
78
+ file_path="manifest.json",
79
+ suggestion='请填写 "categories": ["tool|chat|fun|information|moderation"] 和 "tags": ["..."]',
80
+ )
81
+
74
82
  # 检查推荐字段
75
83
  missing_recommended = []
76
84
  for field in self.RECOMMENDED_FIELDS:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mofox-plugin-dev-toolkit"
7
- version = "0.5.0"
7
+ version = "0.5.1"
8
8
  description = "开发工具集,用于快速创建、开发和测试 MoFox-Bot 插件"
9
9
  authors = [
10
10
  {name = "MoFox-Studio", email = "wwwww95915@qq.com"}