mofox-plugin-dev-toolkit 0.5.0__tar.gz → 0.5.2__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.2}/PKG-INFO +2 -1
  2. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2/mofox_plugin_dev_toolkit.egg-info}/PKG-INFO +2 -1
  3. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mofox_plugin_dev_toolkit.egg-info/SOURCES.txt +1 -0
  4. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mofox_plugin_dev_toolkit.egg-info/requires.txt +1 -0
  5. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/commands/build.py +17 -4
  6. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/commands/check.py +14 -0
  7. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/commands/init.py +32 -2
  8. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/commands/market.py +17 -8
  9. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/market/manifest.py +58 -2
  10. mofox_plugin_dev_toolkit-0.5.2/mpdt/utils/manifest_metadata.py +251 -0
  11. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/metadata_validator.py +8 -0
  12. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/pyproject.toml +2 -1
  13. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/LICENSE +0 -0
  14. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/MANIFEST.in +0 -0
  15. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/README.md +0 -0
  16. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mofox_plugin_dev_toolkit.egg-info/dependency_links.txt +0 -0
  17. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mofox_plugin_dev_toolkit.egg-info/entry_points.txt +0 -0
  18. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mofox_plugin_dev_toolkit.egg-info/top_level.txt +0 -0
  19. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/__init__.py +0 -0
  20. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/__main__.py +0 -0
  21. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/cli.py +0 -0
  22. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/commands/__init__.py +0 -0
  23. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/commands/dev.py +0 -0
  24. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/commands/generate.py +0 -0
  25. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/dev/bridge_plugin/__init__.py +0 -0
  26. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/dev/bridge_plugin/cleanup_handler.py +0 -0
  27. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/dev/bridge_plugin/dev_config.py +0 -0
  28. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/dev/bridge_plugin/file_watcher.py +0 -0
  29. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/dev/bridge_plugin/manifest.json +0 -0
  30. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/dev/bridge_plugin/plugin.py +0 -0
  31. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/market/__init__.py +0 -0
  32. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/market/client.py +0 -0
  33. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/market/config.py +0 -0
  34. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/market/git.py +0 -0
  35. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/market/github.py +0 -0
  36. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/__init__.py +0 -0
  37. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/action_template.py +0 -0
  38. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/adapter_template.py +0 -0
  39. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/chatter_template.py +0 -0
  40. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/collection_template.py +0 -0
  41. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/config_template.py +0 -0
  42. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/event_template.py +0 -0
  43. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/plus_command_template.py +0 -0
  44. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/prompt_template.py +0 -0
  45. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/router_template.py +0 -0
  46. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/service_template.py +0 -0
  47. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/templates/tool_template.py +0 -0
  48. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/__init__.py +0 -0
  49. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/code_parser.py +0 -0
  50. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/color_printer.py +0 -0
  51. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/config_loader.py +0 -0
  52. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/config_manager.py +0 -0
  53. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/file_ops.py +0 -0
  54. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/license_generator.py +0 -0
  55. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/plugin_parser.py +0 -0
  56. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/utils/template_engine.py +0 -0
  57. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/__init__.py +0 -0
  58. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/auto_fix_validator.py +0 -0
  59. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/base.py +0 -0
  60. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/component_validator.py +0 -0
  61. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/config_validator.py +0 -0
  62. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/structure_validator.py +0 -0
  63. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/style_validator.py +0 -0
  64. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/mpdt/validators/type_validator.py +0 -0
  65. {mofox_plugin_dev_toolkit-0.5.0 → mofox_plugin_dev_toolkit-0.5.2}/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.2
4
4
  Summary: 开发工具集,用于快速创建、开发和测试 MoFox-Bot 插件
5
5
  Author-email: MoFox-Studio <wwwww95915@qq.com>
6
6
  License: GPL-3.0-or-later
@@ -30,6 +30,7 @@ Requires-Dist: watchdog>=3.0.0
30
30
  Requires-Dist: websockets>=12.0
31
31
  Requires-Dist: libcst>=1.8.6
32
32
  Requires-Dist: aiohttp>=3.9.0
33
+ Requires-Dist: pillow>=11.2.0
33
34
  Requires-Dist: uvicorn>=0.24.0
34
35
  Requires-Dist: fastapi>=0.104.0
35
36
  Requires-Dist: ruff>=0.1.6
@@ -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.2
4
4
  Summary: 开发工具集,用于快速创建、开发和测试 MoFox-Bot 插件
5
5
  Author-email: MoFox-Studio <wwwww95915@qq.com>
6
6
  License: GPL-3.0-or-later
@@ -30,6 +30,7 @@ Requires-Dist: watchdog>=3.0.0
30
30
  Requires-Dist: websockets>=12.0
31
31
  Requires-Dist: libcst>=1.8.6
32
32
  Requires-Dist: aiohttp>=3.9.0
33
+ Requires-Dist: pillow>=11.2.0
33
34
  Requires-Dist: uvicorn>=0.24.0
34
35
  Requires-Dist: fastapi>=0.104.0
35
36
  Requires-Dist: ruff>=0.1.6
@@ -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
@@ -10,6 +10,7 @@ watchdog>=3.0.0
10
10
  websockets>=12.0
11
11
  libcst>=1.8.6
12
12
  aiohttp>=3.9.0
13
+ pillow>=11.2.0
13
14
  uvicorn>=0.24.0
14
15
  fastapi>=0.104.0
15
16
  ruff>=0.1.6
@@ -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, plugin_dir: Path) -> 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, plugin_dir)
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, plugin_dir)
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:
@@ -343,9 +369,13 @@ def _generate_manifest_file(plugin_name: str, author: str | None, template: str,
343
369
 
344
370
  manifest = {
345
371
  "name": plugin_name,
372
+ "display_name": plugin_name,
346
373
  "version": "1.0.0",
347
374
  "description": description or f"{plugin_name} 插件",
348
375
  "author": author or "Your Name",
376
+ "icon": "icon.png",
377
+ "categories": categories or ["tool"],
378
+ "tags": tags or [plugin_name],
349
379
  "dependencies": {"plugins": [], "components": []},
350
380
  "include": template_components.get(template, []),
351
381
  "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,8 +50,8 @@ 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
- payload = plugin_payload(manifest, repository_url=repository_url)
53
+ manifest = await _load_manifest_for_market(plugin_path)
54
+ payload = plugin_payload(manifest, repository_url=repository_url, plugin_dir=plugin_path)
54
55
  result = await _client(market_url, token).register_plugin(payload)
55
56
  _print_ok(f"Plugin registered: {result['plugin_id']} ({result['status']})")
56
57
 
@@ -61,8 +62,8 @@ 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
- payload = plugin_payload(manifest, repository_url=repository_url)
65
+ manifest = await _load_manifest_for_market(plugin_path)
66
+ payload = plugin_payload(manifest, repository_url=repository_url, plugin_dir=plugin_path)
66
67
  plugin_id = payload.pop("plugin_id")
67
68
  result = await _client(market_url, token).update_plugin(plugin_id, payload)
68
69
  _print_ok(f"Plugin updated: {result['plugin_id']} ({result['status']})")
@@ -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
@@ -231,7 +232,7 @@ def market_publish(
231
232
  asset_url = str(asset.get("browser_download_url") or asset_download_url_for(plugin_id, version, package.package_path.name))
232
233
 
233
234
  market = _client(market_url, token or github_api_token)
234
- plugin_registration_payload = plugin_payload(manifest, repository_url=repo_html_url)
235
+ plugin_registration_payload = plugin_payload(manifest, repository_url=repo_html_url, plugin_dir=plugin_dir)
235
236
  try:
236
237
  plugin = await market.register_plugin(plugin_registration_payload)
237
238
  _print_ok(f"Plugin registered: {plugin['plugin_id']} ({plugin['status']})")
@@ -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
 
@@ -2,10 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
6
+ import io
5
7
  import json
6
8
  from pathlib import Path
7
9
  from typing import Any
8
10
 
11
+ from PIL import Image, ImageOps
12
+
9
13
 
10
14
  def load_manifest(plugin_path: str = ".") -> dict[str, Any]:
11
15
  """Load manifest.json from a plugin directory."""
@@ -20,16 +24,24 @@ def load_manifest(plugin_path: str = ".") -> dict[str, Any]:
20
24
  return data
21
25
 
22
26
 
23
- def plugin_payload(manifest: dict[str, Any], repository_url: str | None = None) -> dict[str, Any]:
27
+ def plugin_payload(
28
+ manifest: dict[str, Any],
29
+ repository_url: str | None = None,
30
+ plugin_dir: str | Path | None = None,
31
+ ) -> dict[str, Any]:
24
32
  """Convert MPDT manifest.json to market plugin registration payload."""
25
33
 
26
34
  plugin_id = str(manifest.get("name") or "").strip()
27
35
  if not plugin_id:
28
36
  raise ValueError("manifest.json 缺少 name 字段")
37
+ display_name = str(manifest.get("display_name") or "").strip()
38
+ if not display_name:
39
+ raise ValueError("manifest.json 缺少 display_name 字段")
29
40
  repo = repository_url or manifest.get("repository_url") or f"https://github.com/MoFox-Studio/{plugin_id}"
41
+ resolved_plugin_dir = Path(plugin_dir).resolve() if plugin_dir is not None else None
30
42
  return {
31
43
  "plugin_id": plugin_id,
32
- "display_name": str(manifest.get("display_name") or plugin_id),
44
+ "display_name": display_name,
33
45
  "summary": str(manifest.get("summary") or manifest.get("description") or f"{plugin_id} 插件"),
34
46
  "description": str(manifest.get("description") or ""),
35
47
  "homepage": manifest.get("homepage") or repo,
@@ -38,6 +50,9 @@ def plugin_payload(manifest: dict[str, Any], repository_url: str | None = None)
38
50
  "categories": list(manifest.get("categories") or []),
39
51
  "tags": list(manifest.get("tags") or []),
40
52
  "maintainers": _maintainers(manifest),
53
+ "plugin_dependencies": list(((manifest.get("dependencies") or {}).get("plugins") or [])),
54
+ "icon_png_base64": _icon_png_base64(manifest, resolved_plugin_dir),
55
+ "readme_markdown": _readme_markdown(resolved_plugin_dir),
41
56
  }
42
57
 
43
58
 
@@ -100,3 +115,44 @@ def _maintainers(manifest: dict[str, Any]) -> list[str]:
100
115
  return [str(item) for item in maintainers]
101
116
  author = str(manifest.get("author") or "mock-author").strip()
102
117
  return [author or "mock-author"]
118
+
119
+
120
+ def _icon_png_base64(manifest: dict[str, Any], plugin_dir: Path | None) -> str | None:
121
+ """Return a normalized base64 PNG icon for upload, if configured."""
122
+
123
+ raw_icon = str(manifest.get("icon") or "").strip()
124
+ if not raw_icon:
125
+ return None
126
+ if plugin_dir is None:
127
+ raise ValueError("manifest.json 包含 icon 字段,但当前上下文缺少插件目录")
128
+ icon_path = (plugin_dir / raw_icon).resolve() if not Path(raw_icon).is_absolute() else Path(raw_icon)
129
+ if not icon_path.exists() or not icon_path.is_file():
130
+ raise ValueError(f"manifest.json 的 icon 指向的文件不存在: {icon_path}")
131
+ if icon_path.suffix.lower() != ".png":
132
+ raise ValueError("manifest.json 的 icon 必须指向 PNG 文件")
133
+
134
+ with Image.open(icon_path) as image:
135
+ if (image.format or "").upper() != "PNG":
136
+ raise ValueError("manifest.json 的 icon 必须是有效的 PNG 图片")
137
+ working = ImageOps.contain(image.convert("RGBA"), (512, 512), method=Image.Resampling.LANCZOS)
138
+
139
+ canvas = Image.new("RGBA", (512, 512), (0, 0, 0, 0))
140
+ offset = ((512 - working.width) // 2, (512 - working.height) // 2)
141
+ canvas.paste(working, offset, working)
142
+ normalized = canvas.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
143
+
144
+ buffer = io.BytesIO()
145
+ normalized.save(buffer, format="PNG", optimize=True)
146
+ return base64.b64encode(buffer.getvalue()).decode("ascii")
147
+
148
+
149
+ def _readme_markdown(plugin_dir: Path | None) -> str | None:
150
+ """Load README.md contents for market detail rendering when present."""
151
+
152
+ if plugin_dir is None:
153
+ return None
154
+ readme_path = plugin_dir / "README.md"
155
+ if not readme_path.exists() or not readme_path.is_file():
156
+ return None
157
+ content = readme_path.read_text(encoding="utf-8").strip()
158
+ return content or None
@@ -0,0 +1,251 @@
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_display_name(manifest: dict[str, Any]) -> str:
19
+ """Return normalized display_name from manifest."""
20
+
21
+ value = manifest.get("display_name")
22
+ if not isinstance(value, str):
23
+ return ""
24
+ return value.strip()
25
+
26
+
27
+ def get_categories(manifest: dict[str, Any]) -> list[str]:
28
+ """Return normalized categories from manifest."""
29
+ value = manifest.get("categories")
30
+ if not isinstance(value, list):
31
+ return []
32
+ return [item.strip() for item in value if isinstance(item, str) and item.strip()]
33
+
34
+
35
+ def get_tags(manifest: dict[str, Any]) -> list[str]:
36
+ """Return normalized tags from manifest."""
37
+ value = manifest.get("tags")
38
+ if not isinstance(value, list):
39
+ return []
40
+ normalized: list[str] = []
41
+ for item in value:
42
+ if not isinstance(item, str):
43
+ continue
44
+ tag = item.strip()
45
+ if tag and tag not in normalized:
46
+ normalized.append(tag)
47
+ return normalized
48
+
49
+
50
+ def normalize_tags(raw_tags: str) -> list[str]:
51
+ """Parse raw tag input into a de-duplicated tag list."""
52
+ tags: list[str] = []
53
+ normalized_input = raw_tags.replace(",", ",").replace("\n", ",")
54
+ for part in normalized_input.split(","):
55
+ tag = part.strip()
56
+ if tag and tag not in tags:
57
+ tags.append(tag)
58
+ return tags
59
+
60
+
61
+ def categories_are_valid(manifest: dict[str, Any]) -> bool:
62
+ """Return whether manifest categories satisfy the packaging rules."""
63
+ categories = get_categories(manifest)
64
+ return len(categories) == 1 and categories[0] in ALLOWED_CATEGORIES
65
+
66
+
67
+ def tags_are_valid(manifest: dict[str, Any]) -> bool:
68
+ """Return whether manifest tags satisfy the packaging rules."""
69
+ return bool(get_tags(manifest))
70
+
71
+
72
+ def metadata_errors(manifest: dict[str, Any], plugin_dir: Path | None = None) -> list[str]:
73
+ """Return validation errors for required manifest metadata."""
74
+ errors: list[str] = []
75
+ if not get_display_name(manifest):
76
+ errors.append("manifest.json 的 display_name 必须是非空字符串")
77
+ if not categories_are_valid(manifest):
78
+ allowed = ", ".join(ALLOWED_CATEGORIES)
79
+ errors.append(f"manifest.json 的 categories 必须是只包含一个值的数组,且取值只能是: {allowed}")
80
+ if not tags_are_valid(manifest):
81
+ errors.append("manifest.json 的 tags 必须是至少包含一个非空字符串的数组")
82
+ icon_error = _icon_error(manifest, plugin_dir)
83
+ if icon_error:
84
+ errors.append(icon_error)
85
+ return errors
86
+
87
+
88
+ def _is_interactive_terminal() -> bool:
89
+ """Return whether stdin/stdout are attached to a TTY."""
90
+ return sys.stdin.isatty() and sys.stdout.isatty()
91
+
92
+
93
+ def _has_running_event_loop() -> bool:
94
+ """Return whether the current thread is already inside a running event loop."""
95
+ try:
96
+ asyncio.get_running_loop()
97
+ except RuntimeError:
98
+ return False
99
+ return True
100
+
101
+
102
+ def _finalize_manifest_metadata(manifest: dict[str, Any]) -> dict[str, Any]:
103
+ """Normalize legacy single-value metadata keys."""
104
+ manifest.pop("category", None)
105
+ manifest.pop("tag", None)
106
+ return manifest
107
+
108
+
109
+ def _save_manifest(plugin_dir: Path, manifest: dict[str, Any]) -> None:
110
+ """Persist manifest.json after interactive metadata repair."""
111
+ manifest_path = plugin_dir / "manifest.json"
112
+ with open(manifest_path, "w", encoding="utf-8") as f:
113
+ json.dump(manifest, f, ensure_ascii=False, indent=4)
114
+
115
+
116
+ def prompt_manifest_metadata(existing_manifest: dict[str, Any] | None = None) -> dict[str, Any]:
117
+ """Collect required manifest metadata from the console."""
118
+ manifest = existing_manifest or {}
119
+ display_name = get_display_name(manifest) or str(manifest.get("name") or "").strip()
120
+ existing_categories = get_categories(manifest)
121
+ default_category = existing_categories[0] if existing_categories else ALLOWED_CATEGORIES[0]
122
+ existing_tags = ", ".join(get_tags(manifest))
123
+
124
+ resolved_display_name = questionary.text(
125
+ "填写插件展示名 display_name:",
126
+ default=display_name,
127
+ validate=lambda value: bool(str(value).strip()) or "display_name 不能为空",
128
+ ).ask()
129
+ if resolved_display_name is None:
130
+ raise ValueError("已取消填写 display_name")
131
+
132
+ category = questionary.select(
133
+ "选择插件分类 categories(只能选一个):",
134
+ choices=[questionary.Choice(item, value=item) for item in ALLOWED_CATEGORIES],
135
+ default=default_category,
136
+ ).ask()
137
+ if category is None:
138
+ raise ValueError("已取消填写 categories")
139
+
140
+ tags_text = questionary.text(
141
+ "填写插件 tags(多个标签用逗号分隔):",
142
+ default=existing_tags,
143
+ validate=lambda value: bool(normalize_tags(value)) or "至少填写一个 tag",
144
+ ).ask()
145
+ if tags_text is None:
146
+ raise ValueError("已取消填写 tags")
147
+
148
+ return {
149
+ "display_name": str(resolved_display_name).strip(),
150
+ "categories": [category],
151
+ "tags": normalize_tags(tags_text),
152
+ }
153
+
154
+
155
+ async def prompt_manifest_metadata_async(existing_manifest: dict[str, Any] | None = None) -> dict[str, Any]:
156
+ """Collect required manifest metadata from the console inside an event loop."""
157
+ manifest = existing_manifest or {}
158
+ display_name = get_display_name(manifest) or str(manifest.get("name") or "").strip()
159
+ existing_categories = get_categories(manifest)
160
+ default_category = existing_categories[0] if existing_categories else ALLOWED_CATEGORIES[0]
161
+ existing_tags = ", ".join(get_tags(manifest))
162
+
163
+ resolved_display_name = await questionary.text(
164
+ "填写插件展示名 display_name:",
165
+ default=display_name,
166
+ validate=lambda value: bool(str(value).strip()) or "display_name 不能为空",
167
+ ).ask_async()
168
+ if resolved_display_name is None:
169
+ raise ValueError("已取消填写 display_name")
170
+
171
+ category = await questionary.select(
172
+ "选择插件分类 categories(只能选一个):",
173
+ choices=[questionary.Choice(item, value=item) for item in ALLOWED_CATEGORIES],
174
+ default=default_category,
175
+ ).ask_async()
176
+ if category is None:
177
+ raise ValueError("已取消填写 categories")
178
+
179
+ tags_text = await questionary.text(
180
+ "填写插件 tags(多个标签用逗号分隔):",
181
+ default=existing_tags,
182
+ validate=lambda value: bool(normalize_tags(value)) or "至少填写一个 tag",
183
+ ).ask_async()
184
+ if tags_text is None:
185
+ raise ValueError("已取消填写 tags")
186
+
187
+ return {
188
+ "display_name": str(resolved_display_name).strip(),
189
+ "categories": [category],
190
+ "tags": normalize_tags(tags_text),
191
+ }
192
+
193
+
194
+ def ensure_manifest_metadata_interactive(plugin_dir: Path, manifest: dict[str, Any]) -> dict[str, Any]:
195
+ """Prompt for missing manifest metadata and persist it when running in a TTY."""
196
+ errors = metadata_errors(manifest, plugin_dir)
197
+ if not errors:
198
+ return _finalize_manifest_metadata(manifest)
199
+
200
+ if not _is_interactive_terminal() or _has_running_event_loop():
201
+ return manifest
202
+
203
+ print_warning("检测到 manifest.json 缺少或无效的市场元数据,进入交互补全...")
204
+ for error in errors:
205
+ print_warning(error)
206
+
207
+ manifest.update(prompt_manifest_metadata(manifest))
208
+ _finalize_manifest_metadata(manifest)
209
+ _save_manifest(plugin_dir, manifest)
210
+
211
+ print_success("已更新 manifest.json 中的市场元数据")
212
+ return manifest
213
+
214
+
215
+ async def ensure_manifest_metadata_interactive_async(plugin_dir: Path, manifest: dict[str, Any]) -> dict[str, Any]:
216
+ """Async prompt for missing manifest metadata and persist it when running in a TTY."""
217
+ errors = metadata_errors(manifest, plugin_dir)
218
+ if not errors:
219
+ return _finalize_manifest_metadata(manifest)
220
+
221
+ if not _is_interactive_terminal():
222
+ return manifest
223
+
224
+ print_warning("检测到 manifest.json 缺少或无效的市场元数据,进入交互补全...")
225
+ for error in errors:
226
+ print_warning(error)
227
+
228
+ manifest.update(await prompt_manifest_metadata_async(manifest))
229
+ _finalize_manifest_metadata(manifest)
230
+ _save_manifest(plugin_dir, manifest)
231
+
232
+ print_success("已更新 manifest.json 中的市场元数据")
233
+ return manifest
234
+
235
+
236
+ def _icon_error(manifest: dict[str, Any], plugin_dir: Path | None) -> str | None:
237
+ """Return a validation error for the optional icon field, if any."""
238
+
239
+ icon = manifest.get("icon")
240
+ if icon is None or str(icon).strip() == "":
241
+ return None
242
+ if not isinstance(icon, str):
243
+ return "manifest.json 的 icon 必须是字符串路径"
244
+ if plugin_dir is None:
245
+ return None
246
+ icon_path = plugin_dir / icon
247
+ if not icon_path.exists() or not icon_path.is_file():
248
+ return f"manifest.json 的 icon 指向的文件不存在: {icon}"
249
+ if icon_path.suffix.lower() != ".png":
250
+ return "manifest.json 的 icon 必须指向 .png 文件"
251
+ return None
@@ -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, self.plugin_path):
76
+ self.result.add_error(
77
+ error,
78
+ file_path="manifest.json",
79
+ suggestion='请填写 display_name,并补全 categories/tags;如果配置了 icon,请确保它指向有效的 PNG 文件',
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.2"
8
8
  description = "开发工具集,用于快速创建、开发和测试 MoFox-Bot 插件"
9
9
  authors = [
10
10
  {name = "MoFox-Studio", email = "wwwww95915@qq.com"}
@@ -35,6 +35,7 @@ dependencies = [
35
35
  "websockets>=12.0",
36
36
  "libcst>=1.8.6",
37
37
  "aiohttp>=3.9.0",
38
+ "pillow>=11.2.0",
38
39
  "uvicorn>=0.24.0",
39
40
  "fastapi>=0.104.0",
40
41
  "ruff>=0.1.6",