Align marketplace categories with Codex taxonomy
Constraint: Keep one EAPIL marketplace while using official Codex category metadata and stable plugin IDs. Rejected: One marketplace per category | Codex groups the UI by marketplace source, which would require users to add multiple markets and change install selectors. Confidence: high Scope-risk: moderate Directive: Keep localized plugins in the same functional category and distinguish them with a language suffix such as -zh. Tested: Marketplace validation, all manifest/category consistency checks, KeyInfo market tests, frontend lint and production build.
This commit is contained in:
@@ -503,9 +503,34 @@ function Merge-Hashtable {
|
||||
function Get-SourceCategory {
|
||||
param([object]$Source)
|
||||
if ($Source.PSObject.Properties.Name -contains "marketplace" -and $Source.marketplace -and $Source.marketplace.category) {
|
||||
return $Source.marketplace.category
|
||||
return ConvertTo-OfficialCategory $Source.marketplace.category
|
||||
}
|
||||
return $Source.category
|
||||
return ConvertTo-OfficialCategory $Source.category
|
||||
}
|
||||
|
||||
function ConvertTo-OfficialCategory {
|
||||
param([string]$Category)
|
||||
$aliases = @{
|
||||
"文档处理" = "Productivity"
|
||||
"工具" = "Productivity"
|
||||
"翻译与本地化" = "Productivity"
|
||||
"开发工具" = "Developer Tools"
|
||||
"测试与质量" = "Developer Tools"
|
||||
"MCP" = "Developer Tools"
|
||||
"设计" = "Creativity"
|
||||
"多媒体与生成" = "Creativity"
|
||||
"数据分析" = "Data & Analytics"
|
||||
"知识库与检索" = "Education & Research"
|
||||
}
|
||||
$official = @(
|
||||
"Productivity", "Developer Tools", "Engineering", "Creativity",
|
||||
"Data & Analytics", "Education & Research", "Communication", "Finance",
|
||||
"Business & Operations", "Travel", "Security", "Other"
|
||||
)
|
||||
if ($official -contains $Category) { return $Category }
|
||||
if ($aliases.ContainsKey($Category)) { return $aliases[$Category] }
|
||||
if ([string]::IsNullOrWhiteSpace($Category)) { return "" }
|
||||
return "Other"
|
||||
}
|
||||
|
||||
function ConvertTo-YamlDoubleQuoted {
|
||||
@@ -566,12 +591,15 @@ function Apply-LocalOverrides {
|
||||
[object]$Source
|
||||
)
|
||||
|
||||
$manifestPath = Join-Path $PluginDir ".codex-plugin\plugin.json"
|
||||
$manifest = ConvertTo-Hashtable (Read-JsonFile $manifestPath)
|
||||
if ($Source.PSObject.Properties.Name -contains "manifestOverrides" -and $Source.manifestOverrides) {
|
||||
$manifestPath = Join-Path $PluginDir ".codex-plugin\plugin.json"
|
||||
$manifest = ConvertTo-Hashtable (Read-JsonFile $manifestPath)
|
||||
$manifest = Merge-Hashtable -Base $manifest -Override $Source.manifestOverrides
|
||||
Write-JsonFile -Path $manifestPath -Value $manifest
|
||||
}
|
||||
if ($manifest.interface -and $manifest.interface.category) {
|
||||
$manifest.interface.category = ConvertTo-OfficialCategory ([string]$manifest.interface.category)
|
||||
}
|
||||
Write-JsonFile -Path $manifestPath -Value $manifest
|
||||
|
||||
if ($Source.PSObject.Properties.Name -contains "skillDescriptions" -and $Source.skillDescriptions) {
|
||||
$skillsRoot = Join-Path $PluginDir "skills"
|
||||
@@ -923,6 +951,7 @@ function Update-Marketplace {
|
||||
$plugins = @()
|
||||
foreach ($plugin in $marketplace.plugins) {
|
||||
if ($sourceNames -notcontains $plugin.name) {
|
||||
$plugin.category = ConvertTo-OfficialCategory ([string]$plugin.category)
|
||||
$plugins += $plugin
|
||||
}
|
||||
}
|
||||
@@ -940,7 +969,23 @@ function Update-Marketplace {
|
||||
category = (Get-SourceCategory $source)
|
||||
}
|
||||
}
|
||||
$marketplace.plugins = $plugins
|
||||
$categoryOrder = @{
|
||||
"Productivity" = 0
|
||||
"Developer Tools" = 1
|
||||
"Engineering" = 2
|
||||
"Creativity" = 3
|
||||
"Data & Analytics" = 4
|
||||
"Education & Research" = 5
|
||||
"Communication" = 6
|
||||
"Finance" = 7
|
||||
"Business & Operations" = 8
|
||||
"Travel" = 9
|
||||
"Security" = 10
|
||||
"Other" = 11
|
||||
}
|
||||
$marketplace.plugins = @($plugins | Sort-Object `
|
||||
@{ Expression = { if ($categoryOrder.ContainsKey([string]$_.category)) { $categoryOrder[[string]$_.category] } else { 99 } } }, `
|
||||
@{ Expression = { [string]$_.name } })
|
||||
Write-JsonFile -Path $MarketplacePath -Value $marketplace
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,35 @@ MARKETPLACES = [
|
||||
]
|
||||
SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?$")
|
||||
PLUGIN_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
||||
OFFICIAL_CATEGORIES = {
|
||||
"Productivity",
|
||||
"Developer Tools",
|
||||
"Engineering",
|
||||
"Creativity",
|
||||
"Data & Analytics",
|
||||
"Education & Research",
|
||||
"Communication",
|
||||
"Finance",
|
||||
"Business & Operations",
|
||||
"Travel",
|
||||
"Security",
|
||||
"Other",
|
||||
}
|
||||
CATEGORY_ORDER = [
|
||||
"Productivity",
|
||||
"Developer Tools",
|
||||
"Engineering",
|
||||
"Creativity",
|
||||
"Data & Analytics",
|
||||
"Education & Research",
|
||||
"Communication",
|
||||
"Finance",
|
||||
"Business & Operations",
|
||||
"Travel",
|
||||
"Security",
|
||||
"Other",
|
||||
]
|
||||
CATEGORY_INDEX = {category: index for index, category in enumerate(CATEGORY_ORDER)}
|
||||
|
||||
|
||||
def load_json(path: Path) -> object:
|
||||
@@ -132,7 +161,7 @@ def validate_interface_asset(plugin_dir: Path, manifest_path: Path, value: objec
|
||||
validate_manifest_path(plugin_dir, manifest_path, value, f"interface.{field}")
|
||||
|
||||
|
||||
def validate_manifest(plugin_dir: Path, expected_name: str) -> None:
|
||||
def validate_manifest(plugin_dir: Path, expected_name: str, expected_category: str) -> None:
|
||||
manifest_path = plugin_dir / ".codex-plugin" / "plugin.json"
|
||||
require(manifest_path.exists(), f"Missing manifest: {manifest_path}")
|
||||
manifest = load_json(manifest_path)
|
||||
@@ -160,6 +189,8 @@ def validate_manifest(plugin_dir: Path, expected_name: str) -> None:
|
||||
require(isinstance(interface, dict), f"{manifest_path} missing interface object")
|
||||
for key in ["displayName", "shortDescription", "longDescription", "developerName", "category"]:
|
||||
require(bool(interface.get(key)), f"{manifest_path} missing interface.{key}")
|
||||
require(interface.get("category") in OFFICIAL_CATEGORIES, f"{manifest_path} uses unsupported category")
|
||||
require(interface.get("category") == expected_category, f"{manifest_path} category must match marketplace entry")
|
||||
capabilities = interface.get("capabilities")
|
||||
require(isinstance(capabilities, list), f"{manifest_path} interface.capabilities must be a list")
|
||||
validate_interface_asset(plugin_dir, manifest_path, interface.get("logo"), "logo")
|
||||
@@ -182,6 +213,11 @@ def validate_marketplace(marketplace_path: Path) -> None:
|
||||
require(isinstance(marketplace, dict), f"Marketplace must be an object: {marketplace_path}")
|
||||
require(bool(marketplace.get("name")), f"{marketplace_path} missing name")
|
||||
require(isinstance(marketplace.get("plugins"), list), f"{marketplace_path} plugins must be a list")
|
||||
expected_order = sorted(
|
||||
marketplace["plugins"],
|
||||
key=lambda entry: (CATEGORY_INDEX.get(str(entry.get("category") or ""), len(CATEGORY_INDEX)), str(entry.get("name") or "")),
|
||||
)
|
||||
require(marketplace["plugins"] == expected_order, f"{marketplace_path} plugins must be sorted by category and name")
|
||||
|
||||
seen: set[str] = set()
|
||||
for entry in marketplace["plugins"]:
|
||||
@@ -202,9 +238,10 @@ def validate_marketplace(marketplace_path: Path) -> None:
|
||||
require(isinstance(policy, dict), f"{marketplace_path} {name} missing policy")
|
||||
require(policy.get("installation") in {"AVAILABLE", "INSTALLED_BY_DEFAULT", "NOT_AVAILABLE"}, f"{marketplace_path} {name} invalid installation policy")
|
||||
require(policy.get("authentication") in {"ON_INSTALL", "ON_USE"}, f"{marketplace_path} {name} invalid authentication policy")
|
||||
require(bool(entry.get("category")), f"{marketplace_path} {name} missing category")
|
||||
category = entry.get("category")
|
||||
require(category in OFFICIAL_CATEGORIES, f"{marketplace_path} {name} uses unsupported category: {category}")
|
||||
|
||||
validate_manifest(plugin_dir, name)
|
||||
validate_manifest(plugin_dir, name, str(category))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
Reference in New Issue
Block a user