diff --git a/dashboard/src/lib/plugin-api/marketplace.ts b/dashboard/src/lib/plugin-api/marketplace.ts index 4e243410..d347e1c8 100644 --- a/dashboard/src/lib/plugin-api/marketplace.ts +++ b/dashboard/src/lib/plugin-api/marketplace.ts @@ -21,6 +21,7 @@ interface PluginApiResponse { id: string manifest: { manifest_version: number + id?: string name: string version: string description: string @@ -56,6 +57,7 @@ function normalizePluginManifest(manifest: PluginApiResponse['manifest']): Plugi return { manifest_version: manifest.manifest_version || 1, + id: manifest.id, name: manifest.name, version: manifest.version, description: manifest.description || '', @@ -104,10 +106,15 @@ export async function fetchPluginList(): Promise> { const pluginList = data .filter(item => { - if (!item?.id || !item?.manifest) { + if (!item?.manifest) { console.warn('跳过无效插件数据:', item) return false } + const pluginId = item.manifest.id || item.id + if (!pluginId) { + console.warn('跳过缺少 ID 的插件:', item) + return false + } if (!item.manifest.name || !item.manifest.version) { console.warn('跳过缺少必需字段的插件:', item.id) return false @@ -115,7 +122,7 @@ export async function fetchPluginList(): Promise> { return true }) .map((item) => ({ - id: item.id, + id: item.manifest.id || item.id, manifest: normalizePluginManifest(item.manifest), downloads: 0, rating: 0, diff --git a/dashboard/src/lib/plugin-api/types.ts b/dashboard/src/lib/plugin-api/types.ts index fe0148f8..aaafe19d 100644 --- a/dashboard/src/lib/plugin-api/types.ts +++ b/dashboard/src/lib/plugin-api/types.ts @@ -25,6 +25,7 @@ export interface InstalledPlugin { id: string manifest: { manifest_version: number + id?: string name: string version: string description: string diff --git a/dashboard/src/routes/plugin-detail.tsx b/dashboard/src/routes/plugin-detail.tsx index a2cff384..c0372659 100644 --- a/dashboard/src/routes/plugin-detail.tsx +++ b/dashboard/src/routes/plugin-detail.tsx @@ -104,21 +104,23 @@ export function PluginDetailPage() { } const pluginList = JSON.parse(result.data) - const foundPlugin = pluginList.find((p: any) => p.id === search.pluginId) + const foundPlugin = pluginList.find((p: any) => (p.manifest?.id || p.id) === search.pluginId) if (!foundPlugin) { throw new Error('未找到该插件') } const rawManifest = foundPlugin.manifest || {} + const pluginId = rawManifest.id || foundPlugin.id const repositoryUrl = rawManifest.repository_url || rawManifest.urls?.repository const homepageUrl = rawManifest.homepage_url || rawManifest.urls?.homepage // 转换为 PluginInfo 格式 const pluginInfo: PluginInfo = { - id: foundPlugin.id, + id: pluginId, manifest: { ...rawManifest, + id: pluginId, homepage_url: homepageUrl, repository_url: repositoryUrl, default_locale: rawManifest.default_locale || rawManifest.i18n?.default_locale || 'zh-CN', @@ -170,8 +172,8 @@ export function PluginDetailPage() { return } - setIsInstalled(checkPluginInstalled(search.pluginId, installedPlugins.data)) - setInstalledVersion(getInstalledPluginVersion(search.pluginId, installedPlugins.data)) + setIsInstalled(checkPluginInstalled(pluginId, installedPlugins.data)) + setInstalledVersion(getInstalledPluginVersion(pluginId, installedPlugins.data)) } catch (err) { setError(err instanceof Error ? err.message : '加载失败') } finally { @@ -196,7 +198,7 @@ export function PluginDetailPage() { // 如果插件已安装,优先尝试从本地读取 README if (isInstalled && search.pluginId) { try { - const localResponse = await fetchWithAuth(`/api/webui/plugins/local-readme/${search.pluginId}`) + const localResponse = await fetchWithAuth(`/api/webui/plugins/local-readme/${plugin.id}`) if (localResponse.ok) { const localResult = await localResponse.json() diff --git a/dashboard/src/routes/plugins/index.tsx b/dashboard/src/routes/plugins/index.tsx index 64ef02de..f2f5c1c1 100644 --- a/dashboard/src/routes/plugins/index.tsx +++ b/dashboard/src/routes/plugins/index.tsx @@ -220,6 +220,7 @@ function PluginsPageContent() { id: installedPlugin.id, manifest: { manifest_version: installedPlugin.manifest.manifest_version || 1, + id: installedPlugin.manifest.id || installedPlugin.id, name: installedPlugin.manifest.name, version: installedPlugin.manifest.version, description: installedPlugin.manifest.description || '', diff --git a/dashboard/src/types/plugin.ts b/dashboard/src/types/plugin.ts index db38853e..9bc7e0ef 100644 --- a/dashboard/src/types/plugin.ts +++ b/dashboard/src/types/plugin.ts @@ -20,6 +20,8 @@ export interface HostApplication { export interface PluginManifest { /** 清单文件版本 */ manifest_version: number + /** Manifest 声明的插件唯一标识 */ + id?: string /** 插件名称 */ name: string /** 插件版本 */ diff --git a/pytests/webui/test_plugin_management_routes.py b/pytests/webui/test_plugin_management_routes.py index 8b47b640..132cee86 100644 --- a/pytests/webui/test_plugin_management_routes.py +++ b/pytests/webui/test_plugin_management_routes.py @@ -47,3 +47,17 @@ def test_installed_plugins_only_scan_plugins_dir_and_exclude_a_memorix(client: T assert ids == ["test.demo"] assert "a-dawn.a-memorix" not in ids assert all("/src/plugins/built_in/" not in plugin["path"] for plugin in payload["plugins"]) + + +def test_resolve_installed_plugin_path_falls_back_to_manifest_id(client: TestClient): + plugin_path = support_module.resolve_installed_plugin_path("test.demo") + + assert plugin_path is not None + assert plugin_path.name == "demo_plugin" + + +def test_resolve_installed_plugin_path_accepts_manifest_id_case_mismatch(client: TestClient): + plugin_path = support_module.resolve_installed_plugin_path("Test.Demo") + + assert plugin_path is not None + assert plugin_path.name == "demo_plugin" diff --git a/src/webui/routers/plugin/support.py b/src/webui/routers/plugin/support.py index 04aa14a1..f7a3c827 100644 --- a/src/webui/routers/plugin/support.py +++ b/src/webui/routers/plugin/support.py @@ -213,7 +213,7 @@ def resolve_installed_plugin_path(plugin_id: str) -> Optional[Path]: return _resolve_safe_plugin_directory(new_format_path, plugins_dir, strict=True) if old_format_path.exists(): return _resolve_safe_plugin_directory(old_format_path, plugins_dir, strict=True) - return None + return find_plugin_path_by_id(plugin_id) def parse_repository_url(repository_url: str) -> Tuple[str, str, str]: @@ -256,11 +256,29 @@ def iter_plugin_directories() -> List[Path]: def find_plugin_path_by_id(plugin_id: str) -> Optional[Path]: + casefold_matched_path: Optional[Path] = None + normalized_plugin_id = plugin_id.casefold() + for plugin_path in iter_plugin_directories(): manifest_path = resolve_plugin_file_path(plugin_path, "_manifest.json") manifest = load_manifest_json(manifest_path) - if manifest is not None and (manifest.get("id") == plugin_id or plugin_path.name == plugin_id): + if manifest is None: + continue + + manifest_id = str(manifest.get("id", "")) + if manifest_id == plugin_id or plugin_path.name == plugin_id: return plugin_path + + if ( + casefold_matched_path is None + and (manifest_id.casefold() == normalized_plugin_id or plugin_path.name.casefold() == normalized_plugin_id) + ): + casefold_matched_path = plugin_path + + if casefold_matched_path is not None: + logger.warning(f"插件 ID 大小写不一致,已按大小写不敏感匹配: {plugin_id} -> {casefold_matched_path}") + return casefold_matched_path + return None