From ec3a396f1dc3069ad17195bd9fbe8465cf63c0ae Mon Sep 17 00:00:00 2001 From: Hyu Date: Thu, 8 Oct 2026 21:21:09 +0800 Subject: [PATCH] feat(web): install event processor plugins from selector --- .../agents/PluginProcessorDetailContent.tsx | 19 ++ .../components/PluginProcessorSettings.tsx | 148 +++------------ .../home/agents/components/RunnerSelect.tsx | 117 ++++++++++-- web/src/app/home/agents/runner-marketplace.ts | 66 ++++++- .../e2e/event-processor-marketplace.spec.ts | 179 ++++++++++++++++++ 5 files changed, 389 insertions(+), 140 deletions(-) create mode 100644 web/tests/e2e/event-processor-marketplace.spec.ts diff --git a/web/src/app/home/agents/PluginProcessorDetailContent.tsx b/web/src/app/home/agents/PluginProcessorDetailContent.tsx index 53e08f708..40a3a3b30 100644 --- a/web/src/app/home/agents/PluginProcessorDetailContent.tsx +++ b/web/src/app/home/agents/PluginProcessorDetailContent.tsx @@ -152,6 +152,23 @@ export default function PluginProcessorDetailContent({ [t, available, hasParameters, canOperate], ); + const applyInstalledProcessor = useCallback((component: RunnerDescriptor) => { + setComponents((current) => [ + ...current.filter((item) => item.id !== component.id), + component, + ]); + setComponentRef(component.id); + setParameters( + Object.fromEntries( + (component.config_schema ?? []) + .filter((field) => field.default !== undefined) + .map((field) => [field.name, field.default]), + ), + ); + setActiveTab('config'); + validate.current = null; + }, []); + const load = useCallback(async () => { setLoading(true); setFailed(false); @@ -469,6 +486,8 @@ export default function PluginProcessorDetailContent({ } titleControls={ - {iconURL ? ( - - ) : ( - - )} - - {label} - - {option && ( - - {pluginId} - - )} - - ); -} +import RunnerSelect from './RunnerSelect'; export default function PluginProcessorSettings({ components, value, onChange, disabled = false, + installScope, + onInstalled, }: { components: RunnerDescriptor[]; value: string; onChange: (value: string) => void; disabled?: boolean; + installScope: string; + onInstalled: (component: RunnerDescriptor) => void; }) { const { t } = useTranslation(); - const selected = components.find((item) => item.id === value); + const options = components.map((component) => ({ + name: component.id, + label: { en_US: component.id, zh_Hans: component.id, ...component.label }, + })); + if (value && !components.some((component) => component.id === value)) + options.push({ + name: value, + label: { + en_US: t('agents.eventProcessor.unavailable'), + zh_Hans: t('agents.eventProcessor.unavailable'), + }, + }); return ( -
- - +
+
); } diff --git a/web/src/app/home/agents/components/RunnerSelect.tsx b/web/src/app/home/agents/components/RunnerSelect.tsx index 08f6b06e7..906bec6e0 100644 --- a/web/src/app/home/agents/components/RunnerSelect.tsx +++ b/web/src/app/home/agents/components/RunnerSelect.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Bot, ExternalLink, Loader2, Store } from 'lucide-react'; +import { Bot, Puzzle, ExternalLink, Loader2, Store } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; @@ -7,10 +7,13 @@ import { getCloudServiceClientSync } from '@/app/infra/http'; import { useInstalledPluginIcon } from '@/app/infra/hooks/useInstalledPluginIcon'; import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic'; import type { PluginV4 } from '@/app/infra/entities/plugin'; +import type { RunnerDescriptor } from '@/app/infra/entities/api'; import { RunnerMarketplaceError, getErrorMessage, installMarketplaceRunner, + installMarketplaceEventProcessor, + resumePendingEventProcessorInstall, loadRunnerCatalog, marketplacePluginId, runnerPluginPrefix, @@ -63,9 +66,12 @@ function pluginOptionParts(option: IDynamicFormItemOption): { function InstalledRunnerContent({ option, + eventProcessor = false, }: { option: IDynamicFormItemOption; + eventProcessor?: boolean; }) { + const Icon = eventProcessor ? Puzzle : Bot; const parts = pluginOptionParts(option); const iconURL = useInstalledPluginIcon(parts?.author, parts?.name); @@ -78,7 +84,7 @@ function InstalledRunnerContent({ className="size-5 shrink-0 rounded object-cover" /> ) : ( - + )} {extractI18nObject(option.label)} @@ -88,10 +94,13 @@ function InstalledRunnerContent({ function InstalledRunnerOptionContent({ option, description, + eventProcessor = false, }: { option: IDynamicFormItemOption; description: string; + eventProcessor?: boolean; }) { + const Icon = eventProcessor ? Puzzle : Bot; const parts = pluginOptionParts(option); const iconURL = useInstalledPluginIcon(parts?.author, parts?.name); @@ -104,7 +113,7 @@ function InstalledRunnerOptionContent({ className="row-span-2 size-7 shrink-0 rounded-md object-cover" /> ) : ( - + )} {extractI18nObject(option.label)} @@ -202,13 +211,19 @@ export default function RunnerSelect({ onValueChange, installScope, onInstalled, + usage = 'agent', + disabled = false, + onEventProcessorInstalled, }: { options: IDynamicFormItemOption[]; label: string; value: string; onValueChange: (value: string) => void; installScope: string; - onInstalled: (installed: InstalledRunner) => void; + onInstalled?: (installed: InstalledRunner) => void; + usage?: 'agent' | 'event'; + disabled?: boolean; + onEventProcessorInstalled?: (component: RunnerDescriptor) => void; }) { const { t } = useTranslation(); const { addTask, tasks } = usePluginInstallTasks(); @@ -230,7 +245,7 @@ export default function RunnerSelect({ setCatalogLoading(true); setCatalogError(false); try { - const catalog = await loadRunnerCatalog('agent'); + const catalog = await loadRunnerCatalog(usage); setMarketplaceRunners(catalog.marketplaceRunners); setInstalledPluginIds(catalog.installedPluginIds); setInstalledPluginDescriptions(catalog.installedPluginDescriptions); @@ -240,7 +255,37 @@ export default function RunnerSelect({ } finally { setCatalogLoading(false); } - }, []); + }, [usage]); + + useEffect(() => { + if ( + usage !== 'event' || + disabled || + !readPendingRunnerInstall(installScope) + ) + return; + let cancelled = false; + void resumePendingEventProcessorInstall(installScope) + .then((component) => { + if (!cancelled && component) { + onEventProcessorInstalled?.(component); + void loadCatalog(); + } + }) + .catch((error) => { + if (!cancelled) setInstallError(installErrorMessage(error, t)); + }); + return () => { + cancelled = true; + }; + }, [ + installScope, + usage, + disabled, + onEventProcessorInstalled, + loadCatalog, + t, + ]); useEffect(() => { void loadCatalog(); @@ -289,22 +334,28 @@ export default function RunnerSelect({ const handleInstall = useCallback( async (plugin: PluginV4) => { const pluginId = marketplacePluginId(plugin); - if (pendingInstall || installingPluginId) return; + if (disabled || pendingInstall || installingPluginId) return; setInstallingPluginId(pluginId); setInstallError(null); try { - const installed = await installMarketplaceRunner(plugin, { + const installOptions = { scope: installScope, - onTaskCreated: (taskId) => + onTaskCreated: (taskId: number) => addTask({ taskId, pluginName: marketplacePluginId(plugin), source: 'marketplace', extensionType: 'plugin', }), - }); - onInstalled(installed); + }; + if (usage === 'event') { + onEventProcessorInstalled?.( + await installMarketplaceEventProcessor(plugin, installOptions), + ); + } else { + onInstalled?.(await installMarketplaceRunner(plugin, installOptions)); + } await loadCatalog(); toast.success( t('agents.runnerInstallSuccess', { @@ -329,12 +380,16 @@ export default function RunnerSelect({ onInstalled, pendingInstall, t, + usage, + disabled, + onEventProcessorInstalled, ], ); return (