mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-06 17:47:14 +00:00
fix(web): harden agent runner marketplace flows
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { getCloudServiceClient } from '@/app/infra/http';
|
||||
import { getActiveWorkspaceUuid } from '@/app/infra/http/workspaceContext';
|
||||
import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic';
|
||||
import type { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
||||
import type { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
@@ -9,6 +10,8 @@ export const RUNNER_COMPONENT_FILTER = 'AgentRunner';
|
||||
const RUNNER_CATALOG_PAGE_SIZE = 100;
|
||||
const RUNNER_INSTALL_TIMEOUT_MS = 120_000;
|
||||
const RUNNER_REGISTRATION_TIMEOUT_MS = 60_000;
|
||||
const RUNNER_INSTALL_INTENT_KEY_PREFIX = 'langbot-agent-runner-install';
|
||||
const RUNNER_INSTALL_INTENT_EVENT = 'langbot-agent-runner-install-change';
|
||||
|
||||
export type AgentRunnerMarketplaceErrorCode =
|
||||
| 'version-unavailable'
|
||||
@@ -32,6 +35,21 @@ export interface InstalledAgentRunner {
|
||||
runner: IDynamicFormItemOption;
|
||||
}
|
||||
|
||||
export interface PendingAgentRunnerInstall {
|
||||
taskId: number;
|
||||
pluginId: string;
|
||||
pluginAuthor: string;
|
||||
pluginName: string;
|
||||
pluginLabel: string;
|
||||
scope: string;
|
||||
startedAt: number;
|
||||
}
|
||||
|
||||
interface InstallAgentRunnerOptions {
|
||||
scope: string;
|
||||
onTaskCreated?: (taskId: number) => void;
|
||||
}
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
@@ -52,6 +70,74 @@ export function runnerPluginPrefix(plugin: Pick<PluginV4, 'author' | 'name'>) {
|
||||
return `plugin:${plugin.author}/${plugin.name}/`;
|
||||
}
|
||||
|
||||
function installIntentStorageKey(scope: string) {
|
||||
return `${RUNNER_INSTALL_INTENT_KEY_PREFIX}:${getActiveWorkspaceUuid() || 'default'}:${scope}`;
|
||||
}
|
||||
|
||||
function emitInstallIntentChange(scope: string) {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(RUNNER_INSTALL_INTENT_EVENT, { detail: { scope } }),
|
||||
);
|
||||
}
|
||||
|
||||
export function readPendingAgentRunnerInstall(
|
||||
scope: string,
|
||||
): PendingAgentRunnerInstall | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = sessionStorage.getItem(installIntentStorageKey(scope));
|
||||
if (!raw) return null;
|
||||
const value = JSON.parse(raw) as Partial<PendingAgentRunnerInstall>;
|
||||
if (
|
||||
value.scope !== scope ||
|
||||
typeof value.taskId !== 'number' ||
|
||||
typeof value.pluginId !== 'string' ||
|
||||
typeof value.pluginAuthor !== 'string' ||
|
||||
typeof value.pluginName !== 'string' ||
|
||||
typeof value.pluginLabel !== 'string' ||
|
||||
typeof value.startedAt !== 'number'
|
||||
) {
|
||||
sessionStorage.removeItem(installIntentStorageKey(scope));
|
||||
return null;
|
||||
}
|
||||
return value as PendingAgentRunnerInstall;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writePendingAgentRunnerInstall(intent: PendingAgentRunnerInstall) {
|
||||
if (typeof window === 'undefined') return;
|
||||
sessionStorage.setItem(
|
||||
installIntentStorageKey(intent.scope),
|
||||
JSON.stringify(intent),
|
||||
);
|
||||
emitInstallIntentChange(intent.scope);
|
||||
}
|
||||
|
||||
export function clearPendingAgentRunnerInstall(scope: string, taskId?: number) {
|
||||
if (typeof window === 'undefined') return;
|
||||
const current = readPendingAgentRunnerInstall(scope);
|
||||
if (taskId !== undefined && current?.taskId !== taskId) return;
|
||||
sessionStorage.removeItem(installIntentStorageKey(scope));
|
||||
emitInstallIntentChange(scope);
|
||||
}
|
||||
|
||||
export function subscribePendingAgentRunnerInstall(
|
||||
scope: string,
|
||||
listener: () => void,
|
||||
) {
|
||||
if (typeof window === 'undefined') return () => undefined;
|
||||
const handleChange = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ scope?: string }>).detail;
|
||||
if (detail?.scope === scope) listener();
|
||||
};
|
||||
window.addEventListener(RUNNER_INSTALL_INTENT_EVENT, handleChange);
|
||||
return () =>
|
||||
window.removeEventListener(RUNNER_INSTALL_INTENT_EVENT, handleChange);
|
||||
}
|
||||
|
||||
export async function loadAgentRunnerCatalog(): Promise<AgentRunnerCatalog> {
|
||||
const cloudClient = await getCloudServiceClient();
|
||||
const [firstSearchResult, recommendationResult, installedResult] =
|
||||
@@ -124,6 +210,7 @@ export async function loadAgentRunnerCatalog(): Promise<AgentRunnerCatalog> {
|
||||
|
||||
export async function installMarketplaceAgentRunner(
|
||||
plugin: PluginV4,
|
||||
options: InstallAgentRunnerOptions,
|
||||
): Promise<InstalledAgentRunner> {
|
||||
if (!plugin.latest_version) {
|
||||
throw new AgentRunnerMarketplaceError('version-unavailable');
|
||||
@@ -134,17 +221,51 @@ export async function installMarketplaceAgentRunner(
|
||||
plugin.name,
|
||||
plugin.latest_version,
|
||||
);
|
||||
const pending: PendingAgentRunnerInstall = {
|
||||
taskId,
|
||||
pluginId: marketplacePluginId(plugin),
|
||||
pluginAuthor: plugin.author,
|
||||
pluginName: plugin.name,
|
||||
pluginLabel: extractPluginLabel(plugin),
|
||||
scope: options.scope,
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
writePendingAgentRunnerInstall(pending);
|
||||
options.onTaskCreated?.(taskId);
|
||||
return finishAgentRunnerInstall(pending);
|
||||
}
|
||||
|
||||
function extractPluginLabel(plugin: PluginV4) {
|
||||
const label = plugin.label;
|
||||
if (typeof label === 'string') return label || plugin.name;
|
||||
if (label && typeof label === 'object') {
|
||||
const localized = Object.values(label).find(
|
||||
(value): value is string => typeof value === 'string' && value.length > 0,
|
||||
);
|
||||
if (localized) return localized;
|
||||
}
|
||||
return plugin.name;
|
||||
}
|
||||
|
||||
async function finishAgentRunnerInstall(
|
||||
pending: PendingAgentRunnerInstall,
|
||||
): Promise<InstalledAgentRunner> {
|
||||
// A refreshed page receives a fresh observation window. The backend task is
|
||||
// authoritative; `startedAt` is display metadata, not a reason to abandon a
|
||||
// still-running installation immediately after recovery.
|
||||
const installDeadline = Date.now() + RUNNER_INSTALL_TIMEOUT_MS;
|
||||
let installCompleted = false;
|
||||
while (Date.now() < installDeadline) {
|
||||
const task = await httpClient.getAsyncTask(taskId);
|
||||
while (true) {
|
||||
const task = await httpClient.getAsyncTask(pending.taskId);
|
||||
if (task.runtime.done) {
|
||||
if (task.runtime.exception) {
|
||||
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
|
||||
throw new Error(task.runtime.exception);
|
||||
}
|
||||
installCompleted = true;
|
||||
break;
|
||||
}
|
||||
if (Date.now() >= installDeadline) break;
|
||||
await wait(1000);
|
||||
}
|
||||
if (!installCompleted) {
|
||||
@@ -152,7 +273,10 @@ export async function installMarketplaceAgentRunner(
|
||||
}
|
||||
|
||||
const registrationDeadline = Date.now() + RUNNER_REGISTRATION_TIMEOUT_MS;
|
||||
const prefix = runnerPluginPrefix(plugin);
|
||||
const prefix = runnerPluginPrefix({
|
||||
author: pending.pluginAuthor,
|
||||
name: pending.pluginName,
|
||||
});
|
||||
while (Date.now() < registrationDeadline) {
|
||||
const metadata = await httpClient.getGeneralPipelineMetadata();
|
||||
const configTab = metadata.configs.find((config) => config.name === 'ai');
|
||||
@@ -169,10 +293,20 @@ export async function installMarketplaceAgentRunner(
|
||||
pluginRunnerOptions[0];
|
||||
|
||||
if (configTab && runner) {
|
||||
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
|
||||
return { configTab, runner };
|
||||
}
|
||||
await wait(1000);
|
||||
}
|
||||
|
||||
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
|
||||
throw new AgentRunnerMarketplaceError('registration-timeout');
|
||||
}
|
||||
|
||||
export async function resumePendingAgentRunnerInstall(
|
||||
scope: string,
|
||||
): Promise<InstalledAgentRunner | null> {
|
||||
const pending = readPendingAgentRunnerInstall(scope);
|
||||
if (!pending) return null;
|
||||
return finishAgentRunnerInstall(pending);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Bot, SlidersHorizontal, Zap } from 'lucide-react';
|
||||
import { Bot, Loader2, SlidersHorizontal, Zap } from 'lucide-react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
|
||||
import {
|
||||
@@ -21,6 +21,13 @@ import {
|
||||
PipelineConfigTab,
|
||||
} from '@/app/infra/entities/pipeline';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import { getDefaultValues } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
||||
import {
|
||||
getErrorMessage,
|
||||
readPendingAgentRunnerInstall,
|
||||
resumePendingAgentRunnerInstall,
|
||||
type InstalledAgentRunner,
|
||||
} from '@/app/home/agents/agent-runner-marketplace';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
@@ -117,6 +124,8 @@ function AgentFormComponent(
|
||||
useState<ApiRespPluginSystemStatus | null>(null);
|
||||
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
|
||||
const [pluginStatusError, setPluginStatusError] = useState(false);
|
||||
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
|
||||
const [runnerInstallRecovering, setRunnerInstallRecovering] = useState(false);
|
||||
const [activeSection, setActiveSection] =
|
||||
useState<AgentConfigSection>('runner');
|
||||
const isSavingRef = useRef(false);
|
||||
@@ -147,6 +156,35 @@ function AgentFormComponent(
|
||||
supported_event_patterns: ['*'],
|
||||
},
|
||||
});
|
||||
const runnerInstallScope = `agent:${agentId}`;
|
||||
|
||||
const applyInstalledRunner = useCallback(
|
||||
(installed: InstalledAgentRunner) => {
|
||||
setRunnerConfigSchema(installed.configTab);
|
||||
const currentRunner = form.getValues('runner') || {};
|
||||
const currentConfigs = form.getValues('runner_config') || {};
|
||||
const runnerName = installed.runner.name;
|
||||
const runnerStage = installed.configTab.stages.find(
|
||||
(stage) => stage.name === runnerName,
|
||||
);
|
||||
form.setValue(
|
||||
'runner',
|
||||
{ ...currentRunner, id: runnerName },
|
||||
{ shouldDirty: true },
|
||||
);
|
||||
if (!(runnerName in currentConfigs) && runnerStage) {
|
||||
form.setValue(
|
||||
'runner_config',
|
||||
{
|
||||
...currentConfigs,
|
||||
[runnerName]: getDefaultValues(runnerStage.config),
|
||||
},
|
||||
{ shouldDirty: true },
|
||||
);
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const savedSnapshotRef = useRef('');
|
||||
const initializedStagesRef = useRef<Set<string>>(new Set());
|
||||
@@ -189,6 +227,7 @@ function AgentFormComponent(
|
||||
form.reset(loadedValues);
|
||||
savedSnapshotRef.current = JSON.stringify(loadedValues);
|
||||
initializedStagesRef.current.clear();
|
||||
setInitialDataLoaded(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(t('agents.loadError') + err.msg);
|
||||
@@ -198,6 +237,40 @@ function AgentFormComponent(
|
||||
};
|
||||
}, [agentId, form, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!initialDataLoaded ||
|
||||
!readPendingAgentRunnerInstall(runnerInstallScope)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setRunnerInstallRecovering(true);
|
||||
void resumePendingAgentRunnerInstall(runnerInstallScope)
|
||||
.then((installed) => {
|
||||
if (cancelled || !installed) return;
|
||||
applyInstalledRunner(installed);
|
||||
toast.success(
|
||||
t('wizard.aiEngine.installSuccess', {
|
||||
runner: extractI18nObject(installed.runner.label),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
toast.error(
|
||||
getErrorMessage(error) || t('wizard.aiEngine.installFailed'),
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setRunnerInstallRecovering(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [applyInstalledRunner, initialDataLoaded, runnerInstallScope, t]);
|
||||
|
||||
const loadPluginSystemStatus = useCallback(async () => {
|
||||
setPluginStatusLoading(true);
|
||||
setPluginStatusError(false);
|
||||
@@ -425,7 +498,8 @@ function AgentFormComponent(
|
||||
label={extractI18nObject(config.label)}
|
||||
value={String(field.value ?? '')}
|
||||
onValueChange={field.onChange}
|
||||
onMetadataRefresh={setRunnerConfigSchema}
|
||||
installScope={runnerInstallScope}
|
||||
onInstalled={applyInstalledRunner}
|
||||
/>
|
||||
) : undefined
|
||||
: undefined
|
||||
@@ -576,7 +650,17 @@ function AgentFormComponent(
|
||||
|
||||
{activeSection === 'runner_config' && (
|
||||
<div className="space-y-6">
|
||||
{activeRunnerStage ? (
|
||||
{runnerInstallRecovering ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('agents.runnerSettings')}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
{t('agents.restoringRunnerInstall')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
) : activeRunnerStage ? (
|
||||
renderDynamicStage(activeRunnerStage)
|
||||
) : (
|
||||
<Card>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Bot, Download, Loader2, Store } from 'lucide-react';
|
||||
import { Bot, Download, ExternalLink, Loader2, Store } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { getCloudServiceClientSync, httpClient } from '@/app/infra/http';
|
||||
import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic';
|
||||
import type { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
||||
import type { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import {
|
||||
AgentRunnerMarketplaceError,
|
||||
@@ -14,7 +13,11 @@ import {
|
||||
loadAgentRunnerCatalog,
|
||||
marketplacePluginId,
|
||||
runnerPluginPrefix,
|
||||
readPendingAgentRunnerInstall,
|
||||
subscribePendingAgentRunnerInstall,
|
||||
type InstalledAgentRunner,
|
||||
} from '@/app/home/agents/agent-runner-marketplace';
|
||||
import { usePluginInstallTasks } from '@/app/home/plugins/components/plugin-install-task';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import {
|
||||
Select,
|
||||
@@ -109,21 +112,24 @@ export default function AgentRunnerSelect({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
onMetadataRefresh,
|
||||
installScope,
|
||||
onInstalled,
|
||||
}: {
|
||||
options: IDynamicFormItemOption[];
|
||||
label: string;
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
onMetadataRefresh: (configTab: PipelineConfigTab) => void;
|
||||
installScope: string;
|
||||
onInstalled: (installed: InstalledAgentRunner) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { addTask } = usePluginInstallTasks();
|
||||
const [marketplaceRunners, setMarketplaceRunners] = useState<PluginV4[]>([]);
|
||||
const [installedPluginIds, setInstalledPluginIds] = useState<string[]>([]);
|
||||
const [catalogLoading, setCatalogLoading] = useState(true);
|
||||
const [catalogError, setCatalogError] = useState(false);
|
||||
const [installingPlugin, setInstallingPlugin] = useState<PluginV4 | null>(
|
||||
null,
|
||||
const [pendingInstall, setPendingInstall] = useState(() =>
|
||||
readPendingAgentRunnerInstall(installScope),
|
||||
);
|
||||
const [installError, setInstallError] = useState<string | null>(null);
|
||||
|
||||
@@ -146,6 +152,13 @@ export default function AgentRunnerSelect({
|
||||
void loadCatalog();
|
||||
}, [loadCatalog]);
|
||||
|
||||
useEffect(() => {
|
||||
const syncPendingInstall = () =>
|
||||
setPendingInstall(readPendingAgentRunnerInstall(installScope));
|
||||
syncPendingInstall();
|
||||
return subscribePendingAgentRunnerInstall(installScope, syncPendingInstall);
|
||||
}, [installScope]);
|
||||
|
||||
const marketplaceOptions = useMemo(
|
||||
() =>
|
||||
marketplaceRunners.filter((plugin) => {
|
||||
@@ -159,6 +172,11 @@ export default function AgentRunnerSelect({
|
||||
);
|
||||
|
||||
const selectedOption = options.find((option) => option.name === value);
|
||||
const installingPlugin = pendingInstall
|
||||
? (marketplaceRunners.find(
|
||||
(plugin) => marketplacePluginId(plugin) === pendingInstall.pluginId,
|
||||
) ?? null)
|
||||
: null;
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
async (nextValue: string) => {
|
||||
@@ -172,14 +190,21 @@ export default function AgentRunnerSelect({
|
||||
const plugin = marketplaceRunners.find(
|
||||
(candidate) => marketplacePluginId(candidate) === pluginId,
|
||||
);
|
||||
if (!plugin || installingPlugin) return;
|
||||
if (!plugin || pendingInstall) return;
|
||||
|
||||
setInstallingPlugin(plugin);
|
||||
setInstallError(null);
|
||||
try {
|
||||
const installed = await installMarketplaceAgentRunner(plugin);
|
||||
onMetadataRefresh(installed.configTab);
|
||||
onValueChange(installed.runner.name);
|
||||
const installed = await installMarketplaceAgentRunner(plugin, {
|
||||
scope: installScope,
|
||||
onTaskCreated: (taskId) =>
|
||||
addTask({
|
||||
taskId,
|
||||
pluginName: marketplacePluginId(plugin),
|
||||
source: 'marketplace',
|
||||
extensionType: 'plugin',
|
||||
}),
|
||||
});
|
||||
onInstalled(installed);
|
||||
await loadCatalog();
|
||||
toast.success(
|
||||
t('wizard.aiEngine.installSuccess', {
|
||||
@@ -191,15 +216,17 @@ export default function AgentRunnerSelect({
|
||||
setInstallError(message);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setInstallingPlugin(null);
|
||||
setPendingInstall(readPendingAgentRunnerInstall(installScope));
|
||||
}
|
||||
},
|
||||
[
|
||||
installingPlugin,
|
||||
addTask,
|
||||
installScope,
|
||||
loadCatalog,
|
||||
marketplaceRunners,
|
||||
onMetadataRefresh,
|
||||
onInstalled,
|
||||
onValueChange,
|
||||
pendingInstall,
|
||||
t,
|
||||
],
|
||||
);
|
||||
@@ -208,7 +235,7 @@ export default function AgentRunnerSelect({
|
||||
<div className="w-full max-w-[22rem] space-y-2">
|
||||
<Select
|
||||
value={value}
|
||||
disabled={installingPlugin !== null}
|
||||
disabled={pendingInstall !== null}
|
||||
onValueChange={(nextValue) => void handleValueChange(nextValue)}
|
||||
onOpenChange={(open) => {
|
||||
if (open && catalogError && !catalogLoading) void loadCatalog();
|
||||
@@ -264,11 +291,22 @@ export default function AgentRunnerSelect({
|
||||
<SelectSeparator />
|
||||
|
||||
<SelectGroup>
|
||||
<SelectLabel className="px-2 py-1 text-[11px] font-medium">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<SelectLabel className="flex items-center justify-between gap-2 px-2 py-1 text-[11px] font-medium">
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||
<Store className="size-3.5" />
|
||||
{t('agents.marketplaceRunners')}
|
||||
</span>
|
||||
<a
|
||||
href="https://space.langbot.app/market?type=plugin&component=AgentRunner"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex shrink-0 items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] font-medium text-foreground hover:bg-accent"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{t('agents.viewMarketplace')}
|
||||
<ExternalLink className="size-3" />
|
||||
</a>
|
||||
</SelectLabel>
|
||||
{catalogLoading && marketplaceOptions.length === 0 ? (
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
@@ -58,6 +59,12 @@ import {
|
||||
} from 'lucide-react';
|
||||
import PipelineExtension from '@/app/home/pipelines/components/pipeline-extensions/PipelineExtension';
|
||||
import AgentRunnerSelect from '@/app/home/agents/components/AgentRunnerSelect';
|
||||
import {
|
||||
getErrorMessage,
|
||||
readPendingAgentRunnerInstall,
|
||||
resumePendingAgentRunnerInstall,
|
||||
type InstalledAgentRunner,
|
||||
} from '@/app/home/agents/agent-runner-marketplace';
|
||||
|
||||
interface PipelineFormComponentProps {
|
||||
pipelineId?: string;
|
||||
@@ -204,6 +211,8 @@ const PipelineFormComponent = forwardRef<
|
||||
useState<PipelineConfigTab>();
|
||||
const [outputConfigTabSchema, setOutputConfigTabSchema] =
|
||||
useState<PipelineConfigTab>();
|
||||
const [metadataLoaded, setMetadataLoaded] = useState(false);
|
||||
const [pipelineLoaded, setPipelineLoaded] = useState(!isEditMode);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -219,6 +228,36 @@ const PipelineFormComponent = forwardRef<
|
||||
output: {},
|
||||
},
|
||||
});
|
||||
const runnerInstallScope = `pipeline:${pipelineId || 'new'}`;
|
||||
const applyInstalledRunner = useCallback(
|
||||
(installed: InstalledAgentRunner) => {
|
||||
setAIConfigTabSchema(installed.configTab);
|
||||
const currentAI = (form.getValues('ai') || {}) as Record<string, any>;
|
||||
const currentRunner =
|
||||
currentAI.runner && typeof currentAI.runner === 'object'
|
||||
? currentAI.runner
|
||||
: {};
|
||||
const currentConfigs =
|
||||
currentAI.runner_config && typeof currentAI.runner_config === 'object'
|
||||
? currentAI.runner_config
|
||||
: {};
|
||||
const runnerName = installed.runner.name;
|
||||
const runnerStage = installed.configTab.stages.find(
|
||||
(stage) => stage.name === runnerName,
|
||||
);
|
||||
form.setValue('ai', {
|
||||
...currentAI,
|
||||
runner: { ...currentRunner, id: runnerName },
|
||||
runner_config: {
|
||||
...currentConfigs,
|
||||
...(runnerStage && !(runnerName in currentConfigs)
|
||||
? { [runnerName]: getDefaultValues(runnerStage.config) }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
[form],
|
||||
);
|
||||
const dynamicFormSystemContext = useMemo(
|
||||
() => ({ pipeline_id: pipelineId }),
|
||||
[pipelineId],
|
||||
@@ -244,6 +283,8 @@ const PipelineFormComponent = forwardRef<
|
||||
}, [hasUnsavedChanges, onDirtyChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMetadataLoaded(false);
|
||||
setPipelineLoaded(!isEditMode);
|
||||
// get config schema from metadata
|
||||
httpClient.getGeneralPipelineMetadata().then((resp) => {
|
||||
for (const config of resp.configs) {
|
||||
@@ -257,6 +298,7 @@ const PipelineFormComponent = forwardRef<
|
||||
setOutputConfigTabSchema(config);
|
||||
}
|
||||
}
|
||||
setMetadataLoaded(true);
|
||||
});
|
||||
|
||||
if (isEditMode) {
|
||||
@@ -279,10 +321,48 @@ const PipelineFormComponent = forwardRef<
|
||||
form.reset(loadedValues);
|
||||
savedSnapshotRef.current = JSON.stringify(loadedValues);
|
||||
initializedStagesRef.current.clear();
|
||||
setPipelineLoaded(true);
|
||||
});
|
||||
}
|
||||
}, [form, isEditMode, pipelineId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!metadataLoaded ||
|
||||
!pipelineLoaded ||
|
||||
!readPendingAgentRunnerInstall(runnerInstallScope)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void resumePendingAgentRunnerInstall(runnerInstallScope)
|
||||
.then((installed) => {
|
||||
if (cancelled || !installed) return;
|
||||
applyInstalledRunner(installed);
|
||||
toast.success(
|
||||
t('wizard.aiEngine.installSuccess', {
|
||||
runner: extractI18nObject(installed.runner.label),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
toast.error(
|
||||
getErrorMessage(error) || t('wizard.aiEngine.installFailed'),
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
applyInstalledRunner,
|
||||
metadataLoaded,
|
||||
pipelineLoaded,
|
||||
runnerInstallScope,
|
||||
t,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditMode) {
|
||||
form.reset({
|
||||
@@ -523,7 +603,8 @@ const PipelineFormComponent = forwardRef<
|
||||
label={extractI18nObject(config.label)}
|
||||
value={String(field.value ?? '')}
|
||||
onValueChange={field.onChange}
|
||||
onMetadataRefresh={setAIConfigTabSchema}
|
||||
installScope={runnerInstallScope}
|
||||
onInstalled={applyInstalledRunner}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ import {
|
||||
installMarketplaceAgentRunner,
|
||||
loadAgentRunnerCatalog,
|
||||
marketplacePluginId,
|
||||
readPendingAgentRunnerInstall,
|
||||
resumePendingAgentRunnerInstall,
|
||||
runnerPluginPrefix,
|
||||
} from '@/app/home/agents/agent-runner-marketplace';
|
||||
import {
|
||||
@@ -104,6 +106,7 @@ import {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TOTAL_STEPS = 4;
|
||||
const WIZARD_RUNNER_INSTALL_SCOPE = 'wizard';
|
||||
|
||||
type WizardScenarioId =
|
||||
| 'message_reply'
|
||||
@@ -497,7 +500,9 @@ export default function WizardPage() {
|
||||
setRunnerInstallError(null);
|
||||
|
||||
try {
|
||||
const installed = await installMarketplaceAgentRunner(plugin);
|
||||
const installed = await installMarketplaceAgentRunner(plugin, {
|
||||
scope: WIZARD_RUNNER_INSTALL_SCOPE,
|
||||
});
|
||||
setAiConfigTab(installed.configTab);
|
||||
setInstalledPluginIds((current) =>
|
||||
current.includes(pluginId) ? current : [...current, pluginId],
|
||||
@@ -529,6 +534,46 @@ export default function WizardPage() {
|
||||
[handleSelectRunner, t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
const pending = readPendingAgentRunnerInstall(WIZARD_RUNNER_INSTALL_SCOPE);
|
||||
if (!pending) return;
|
||||
|
||||
let cancelled = false;
|
||||
setInstallingRunnerPluginId(pending.pluginId);
|
||||
setRunnerInstallError(null);
|
||||
void resumePendingAgentRunnerInstall(WIZARD_RUNNER_INSTALL_SCOPE)
|
||||
.then((installed) => {
|
||||
if (cancelled || !installed) return;
|
||||
setAiConfigTab(installed.configTab);
|
||||
setInstalledPluginIds((current) =>
|
||||
current.includes(pending.pluginId)
|
||||
? current
|
||||
: [...current, pending.pluginId],
|
||||
);
|
||||
handleSelectRunner(installed.runner.name, installed.configTab);
|
||||
toast.success(
|
||||
t('wizard.aiEngine.installSuccess', {
|
||||
runner: pending.pluginLabel,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
const message =
|
||||
getErrorMessage(error) || t('wizard.aiEngine.installFailed');
|
||||
setRunnerInstallError(message);
|
||||
toast.error(message);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setInstallingRunnerPluginId(null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [handleSelectRunner, isLoading, t]);
|
||||
|
||||
// ---- Navigation helpers ----
|
||||
|
||||
const canProceed = useCallback((): boolean => {
|
||||
|
||||
@@ -771,7 +771,10 @@ const enUS = {
|
||||
noRunnersAvailableDescription:
|
||||
'Install and enable an AgentRunner extension before configuring this Agent.',
|
||||
installedRunners: 'Installed AgentRunners',
|
||||
marketplaceRunners: 'AgentRunner Marketplace',
|
||||
marketplaceRunners: 'AgentRunner plugins in Marketplace',
|
||||
viewMarketplace: 'View market',
|
||||
restoringRunnerInstall:
|
||||
'Restoring the AgentRunner plugin installation and waiting for the runner…',
|
||||
noInstalledRunners: 'No AgentRunner extension is installed yet.',
|
||||
installingRunner: 'Installing {{runner}}...',
|
||||
selectedRunnerUnavailable: 'Selected runner is unavailable',
|
||||
|
||||
@@ -783,7 +783,10 @@ const jaJP = {
|
||||
noRunnersAvailableDescription:
|
||||
'この Agent を設定する前に AgentRunner 拡張機能をインストールして有効にしてください。',
|
||||
installedRunners: 'インストール済み AgentRunner',
|
||||
marketplaceRunners: 'AgentRunner マーケットプレイス',
|
||||
marketplaceRunners: 'マーケットプレイスの AgentRunner プラグイン',
|
||||
viewMarketplace: '市場を見る',
|
||||
restoringRunnerInstall:
|
||||
'AgentRunner プラグインのインストールを復元し、ランナーを待機しています…',
|
||||
noInstalledRunners:
|
||||
'AgentRunner 拡張機能はまだインストールされていません。',
|
||||
installingRunner: '{{runner}} をインストールしています...',
|
||||
|
||||
@@ -739,7 +739,9 @@ const zhHans = {
|
||||
noRunnersAvailableDescription:
|
||||
'请先安装并启用 AgentRunner 扩展,再配置此 Agent。',
|
||||
installedRunners: '已安装的 AgentRunner',
|
||||
marketplaceRunners: 'AgentRunner 插件市场',
|
||||
marketplaceRunners: '插件市场中的 AgentRunner 插件',
|
||||
viewMarketplace: '查看市场',
|
||||
restoringRunnerInstall: '正在恢复 AgentRunner 插件安装并等待运行器就绪…',
|
||||
noInstalledRunners: '尚未安装任何 AgentRunner 扩展。',
|
||||
installingRunner: '正在安装 {{runner}}...',
|
||||
selectedRunnerUnavailable: '所选运行器不可用',
|
||||
|
||||
@@ -68,14 +68,20 @@ test('binds every message-reply bot to its provisional pipeline before verificat
|
||||
|
||||
test('keeps the 4.11 AgentRunner marketplace installation flow', () => {
|
||||
assert.match(wizardSource, /loadAgentRunnerCatalog\(\)/);
|
||||
assert.match(wizardSource, /installMarketplaceAgentRunner\(plugin\)/);
|
||||
assert.match(
|
||||
wizardSource,
|
||||
/installMarketplaceAgentRunner\(plugin, \{[\s\S]*?scope: WIZARD_RUNNER_INSTALL_SCOPE/,
|
||||
);
|
||||
assert.match(wizardSource, /resumePendingAgentRunnerInstall\(/);
|
||||
assert.match(
|
||||
runnerMarketplaceSource,
|
||||
/RUNNER_COMPONENT_FILTER = 'AgentRunner'/,
|
||||
);
|
||||
assert.match(runnerMarketplaceSource, /installPluginFromMarketplace\(/);
|
||||
assert.match(runnerMarketplaceSource, /runnerPluginPrefix\(plugin\)/);
|
||||
assert.match(runnerMarketplaceSource, /const prefix = runnerPluginPrefix\(\{/);
|
||||
assert.match(runnerMarketplaceSource, /option\.name\.startsWith\(prefix\)/);
|
||||
assert.match(runnerMarketplaceSource, /registrationDeadline/);
|
||||
assert.match(runnerMarketplaceSource, /sessionStorage\.setItem\(/);
|
||||
});
|
||||
|
||||
test('requires the selected AgentRunner mandatory configuration before finishing', () => {
|
||||
|
||||
Reference in New Issue
Block a user