fix(plugins): 插件管理 - 修正官方应用插件的分类与状态显示

AI 审美分析和剪辑工作台在管理页显示为"已停用",原因是三处插件
分类清单不一致:

- 后端 officialApplicationPolicies 缺少 editor-shell
- 管理页 officialApplicationIds 同时缺少 ai-art-critique 和 editor-shell
- 用户插件页另有一份重复清单

后端把这两个插件当作系统协议处理(Kind=protocol),platformAvailable
回退为 false,于是返回"管理员已停用该插件",管理页据此渲染成"已停用"。

修复:

- 后端补上 editor-shell 应用策略(权威修复,决定平台可用性)
- 前端抽取唯一清单 OFFICIAL_APPLICATION_PLUGIN_IDS,管理页与用户插件页
  共用,删除两份重复实现
- 补前后端回归测试,锁定这 8 个官方应用插件

验证:tsc --noEmit 通过;前端 17 项测试通过;后端 TestEditorShell* 与
既有插件测试通过;前后端镜像构建通过。
This commit is contained in:
jay
2026-09-12 21:16:42 +08:00
parent 4eeef3cfd8
commit fbd6962d8f
7 changed files with 139 additions and 15 deletions

View File

@@ -8,9 +8,8 @@ import { useEffect, useMemo, useState } from "react";
import { PaginationBar } from "@/components/layout/workspace-page";
import "@/lib/plugins/builtin";
import { EAGLE_PLUGIN_ID } from "@/lib/plugins/builtin/eagle";
import { PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer";
import { COMFYUI_PLUGIN_ID, RUNNINGHUB_PLUGIN_ID } from "@/lib/plugins/builtin/workflows";
import { MEDIA_CONVERSION_PLUGIN_ID } from "@/lib/plugins/builtin/media-conversion";
import { isOfficialApplicationPluginId } from "@/lib/plugins/official-applications";
import { listRegisteredPlugins } from "@/lib/plugins/plugin-registry";
import type { PluginManifest, PluginManifestV2 } from "@/lib/plugins/plugin-types";
import { fetchAdminPlugins, setPluginPlatformAvailability, uninstallPlugin, uploadPlugin, type AdminPluginState, type BackendPlugin, type PluginManagement } from "@/services/api/plugins";
@@ -27,8 +26,6 @@ type AdminPluginItem = {
error?: string;
};
const officialApplicationIds = new Set([RUNNINGHUB_PLUGIN_ID, COMFYUI_PLUGIN_ID, EAGLE_PLUGIN_ID, PROMPT_OPTIMIZER_PLUGIN_ID, "portrait-clearance", MEDIA_CONVERSION_PLUGIN_ID]);
export default function AdminPluginsPage() {
const { message, modal } = App.useApp();
const [plugins, setPlugins] = useState<BackendPlugin[]>([]);
@@ -363,7 +360,7 @@ function PluginBrandIcon({ pluginId }: { pluginId: string }) {
function mergePlugins(remote: BackendPlugin[]): AdminPluginItem[] {
const byId = new Map<string, AdminPluginItem>();
for (const plugin of listRegisteredPlugins()) {
const application = officialApplicationIds.has(plugin.manifest.id);
const application = isOfficialApplicationPluginId(plugin.manifest.id);
byId.set(plugin.manifest.id, {
manifest: plugin.manifest,
source: plugin.source || "bundled",

View File

@@ -10,8 +10,7 @@ import "@/lib/plugins/builtin";
import { EAGLE_PLUGIN_ID } from "@/lib/plugins/builtin/eagle";
import { PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer";
import { COMFYUI_PLUGIN_ID, RUNNINGHUB_PLUGIN_ID } from "@/lib/plugins/builtin/workflows";
import { MEDIA_CONVERSION_PLUGIN_ID } from "@/lib/plugins/builtin/media-conversion";
import { ART_CRITIQUE_PLUGIN_ID } from "@/lib/art-critique/contracts";
import { isOfficialApplicationPluginId } from "@/lib/plugins/official-applications";
import type { PluginManifest, PluginManifestV2, RegisteredPlugin } from "@/lib/plugins/plugin-types";
import { getEagleLibrary, type EagleFolder } from "@/services/api/eagle";
import { fetchPlugins, setUserPluginEnabled, type BackendPlugin, type PluginState } from "@/services/api/plugins";
@@ -142,7 +141,7 @@ export default function PluginsPage() {
const normalizedSearch = search.trim().toLocaleLowerCase();
return registeredPlugins.filter((plugin) => {
const state = pluginStates[plugin.manifest.id];
const isApplicationPlugin = backendPluginById.get(plugin.manifest.id)?.management.kind === "application" || isOfficialApplicationPlugin(plugin.manifest.id);
const isApplicationPlugin = backendPluginById.get(plugin.manifest.id)?.management.kind === "application" || isOfficialApplicationPluginId(plugin.manifest.id);
if (user?.role !== "admin" && !features.systemPluginsVisibleToUsers && !isApplicationPlugin) return false;
const installation = installations.find((item) => item.manifest.id === plugin.manifest.id);
const enabled = state?.effectiveEnabled ?? Boolean(installation?.enabled);
@@ -186,7 +185,7 @@ export default function PluginsPage() {
}, [pluginSections, scrollTarget]);
const categoryCounts = useMemo(() => {
const visiblePlugins = registeredPlugins.filter((plugin) => user?.role === "admin" || features.systemPluginsVisibleToUsers || isOfficialApplicationPlugin(plugin.manifest.id));
const visiblePlugins = registeredPlugins.filter((plugin) => user?.role === "admin" || features.systemPluginsVisibleToUsers || isOfficialApplicationPluginId(plugin.manifest.id));
const counts: Record<string, number> = { all: visiblePlugins.length, text: 0, image: 0, video: 0, audio: 0, payment: 0, other: 0 };
for (const plugin of visiblePlugins) {
for (const section of protocolSectionMeta) {
@@ -627,14 +626,10 @@ function toRegisteredPlugin(plugin: BackendPlugin): RegisteredPlugin {
return { manifest: plugin.manifest, source: plugin.source };
}
function isOfficialApplicationPlugin(pluginId: string) {
return [RUNNINGHUB_PLUGIN_ID, COMFYUI_PLUGIN_ID, EAGLE_PLUGIN_ID, PROMPT_OPTIMIZER_PLUGIN_ID, "portrait-clearance", ART_CRITIQUE_PLUGIN_ID, MEDIA_CONVERSION_PLUGIN_ID].includes(pluginId);
}
function pluginSourceLabel(plugin: RegisteredPlugin, state?: PluginState) {
if (plugin.source === "uploaded") return "自定义插件";
if (plugin.source === "system") return "系统插件";
if (state?.canToggle || isOfficialApplicationPlugin(plugin.manifest.id)) return "官方插件";
if (state?.canToggle || isOfficialApplicationPluginId(plugin.manifest.id)) return "官方插件";
return "系统插件";
}