mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-08 18: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 { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { getCloudServiceClient } from '@/app/infra/http';
|
import { getCloudServiceClient } from '@/app/infra/http';
|
||||||
|
import { getActiveWorkspaceUuid } from '@/app/infra/http/workspaceContext';
|
||||||
import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic';
|
import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic';
|
||||||
import type { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
import type { PipelineConfigTab } from '@/app/infra/entities/pipeline';
|
||||||
import type { PluginV4 } from '@/app/infra/entities/plugin';
|
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_CATALOG_PAGE_SIZE = 100;
|
||||||
const RUNNER_INSTALL_TIMEOUT_MS = 120_000;
|
const RUNNER_INSTALL_TIMEOUT_MS = 120_000;
|
||||||
const RUNNER_REGISTRATION_TIMEOUT_MS = 60_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 =
|
export type AgentRunnerMarketplaceErrorCode =
|
||||||
| 'version-unavailable'
|
| 'version-unavailable'
|
||||||
@@ -32,6 +35,21 @@ export interface InstalledAgentRunner {
|
|||||||
runner: IDynamicFormItemOption;
|
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) {
|
function wait(ms: number) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
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}/`;
|
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> {
|
export async function loadAgentRunnerCatalog(): Promise<AgentRunnerCatalog> {
|
||||||
const cloudClient = await getCloudServiceClient();
|
const cloudClient = await getCloudServiceClient();
|
||||||
const [firstSearchResult, recommendationResult, installedResult] =
|
const [firstSearchResult, recommendationResult, installedResult] =
|
||||||
@@ -124,6 +210,7 @@ export async function loadAgentRunnerCatalog(): Promise<AgentRunnerCatalog> {
|
|||||||
|
|
||||||
export async function installMarketplaceAgentRunner(
|
export async function installMarketplaceAgentRunner(
|
||||||
plugin: PluginV4,
|
plugin: PluginV4,
|
||||||
|
options: InstallAgentRunnerOptions,
|
||||||
): Promise<InstalledAgentRunner> {
|
): Promise<InstalledAgentRunner> {
|
||||||
if (!plugin.latest_version) {
|
if (!plugin.latest_version) {
|
||||||
throw new AgentRunnerMarketplaceError('version-unavailable');
|
throw new AgentRunnerMarketplaceError('version-unavailable');
|
||||||
@@ -134,17 +221,51 @@ export async function installMarketplaceAgentRunner(
|
|||||||
plugin.name,
|
plugin.name,
|
||||||
plugin.latest_version,
|
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;
|
const installDeadline = Date.now() + RUNNER_INSTALL_TIMEOUT_MS;
|
||||||
let installCompleted = false;
|
let installCompleted = false;
|
||||||
while (Date.now() < installDeadline) {
|
while (true) {
|
||||||
const task = await httpClient.getAsyncTask(taskId);
|
const task = await httpClient.getAsyncTask(pending.taskId);
|
||||||
if (task.runtime.done) {
|
if (task.runtime.done) {
|
||||||
if (task.runtime.exception) {
|
if (task.runtime.exception) {
|
||||||
|
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
|
||||||
throw new Error(task.runtime.exception);
|
throw new Error(task.runtime.exception);
|
||||||
}
|
}
|
||||||
installCompleted = true;
|
installCompleted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (Date.now() >= installDeadline) break;
|
||||||
await wait(1000);
|
await wait(1000);
|
||||||
}
|
}
|
||||||
if (!installCompleted) {
|
if (!installCompleted) {
|
||||||
@@ -152,7 +273,10 @@ export async function installMarketplaceAgentRunner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const registrationDeadline = Date.now() + RUNNER_REGISTRATION_TIMEOUT_MS;
|
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) {
|
while (Date.now() < registrationDeadline) {
|
||||||
const metadata = await httpClient.getGeneralPipelineMetadata();
|
const metadata = await httpClient.getGeneralPipelineMetadata();
|
||||||
const configTab = metadata.configs.find((config) => config.name === 'ai');
|
const configTab = metadata.configs.find((config) => config.name === 'ai');
|
||||||
@@ -169,10 +293,20 @@ export async function installMarketplaceAgentRunner(
|
|||||||
pluginRunnerOptions[0];
|
pluginRunnerOptions[0];
|
||||||
|
|
||||||
if (configTab && runner) {
|
if (configTab && runner) {
|
||||||
|
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
|
||||||
return { configTab, runner };
|
return { configTab, runner };
|
||||||
}
|
}
|
||||||
await wait(1000);
|
await wait(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearPendingAgentRunnerInstall(pending.scope, pending.taskId);
|
||||||
throw new AgentRunnerMarketplaceError('registration-timeout');
|
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 { z } from 'zod';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
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 { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
|
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +21,13 @@ import {
|
|||||||
PipelineConfigTab,
|
PipelineConfigTab,
|
||||||
} from '@/app/infra/entities/pipeline';
|
} from '@/app/infra/entities/pipeline';
|
||||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
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 { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import {
|
import {
|
||||||
@@ -117,6 +124,8 @@ function AgentFormComponent(
|
|||||||
useState<ApiRespPluginSystemStatus | null>(null);
|
useState<ApiRespPluginSystemStatus | null>(null);
|
||||||
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
|
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
|
||||||
const [pluginStatusError, setPluginStatusError] = useState(false);
|
const [pluginStatusError, setPluginStatusError] = useState(false);
|
||||||
|
const [initialDataLoaded, setInitialDataLoaded] = useState(false);
|
||||||
|
const [runnerInstallRecovering, setRunnerInstallRecovering] = useState(false);
|
||||||
const [activeSection, setActiveSection] =
|
const [activeSection, setActiveSection] =
|
||||||
useState<AgentConfigSection>('runner');
|
useState<AgentConfigSection>('runner');
|
||||||
const isSavingRef = useRef(false);
|
const isSavingRef = useRef(false);
|
||||||
@@ -147,6 +156,35 @@ function AgentFormComponent(
|
|||||||
supported_event_patterns: ['*'],
|
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 savedSnapshotRef = useRef('');
|
||||||
const initializedStagesRef = useRef<Set<string>>(new Set());
|
const initializedStagesRef = useRef<Set<string>>(new Set());
|
||||||
@@ -189,6 +227,7 @@ function AgentFormComponent(
|
|||||||
form.reset(loadedValues);
|
form.reset(loadedValues);
|
||||||
savedSnapshotRef.current = JSON.stringify(loadedValues);
|
savedSnapshotRef.current = JSON.stringify(loadedValues);
|
||||||
initializedStagesRef.current.clear();
|
initializedStagesRef.current.clear();
|
||||||
|
setInitialDataLoaded(true);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error(t('agents.loadError') + err.msg);
|
toast.error(t('agents.loadError') + err.msg);
|
||||||
@@ -198,6 +237,40 @@ function AgentFormComponent(
|
|||||||
};
|
};
|
||||||
}, [agentId, form, t]);
|
}, [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 () => {
|
const loadPluginSystemStatus = useCallback(async () => {
|
||||||
setPluginStatusLoading(true);
|
setPluginStatusLoading(true);
|
||||||
setPluginStatusError(false);
|
setPluginStatusError(false);
|
||||||
@@ -425,7 +498,8 @@ function AgentFormComponent(
|
|||||||
label={extractI18nObject(config.label)}
|
label={extractI18nObject(config.label)}
|
||||||
value={String(field.value ?? '')}
|
value={String(field.value ?? '')}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
onMetadataRefresh={setRunnerConfigSchema}
|
installScope={runnerInstallScope}
|
||||||
|
onInstalled={applyInstalledRunner}
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
: undefined
|
: undefined
|
||||||
@@ -576,7 +650,17 @@ function AgentFormComponent(
|
|||||||
|
|
||||||
{activeSection === 'runner_config' && (
|
{activeSection === 'runner_config' && (
|
||||||
<div className="space-y-6">
|
<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)
|
renderDynamicStage(activeRunnerStage)
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { getCloudServiceClientSync, httpClient } from '@/app/infra/http';
|
import { getCloudServiceClientSync, httpClient } from '@/app/infra/http';
|
||||||
import type { IDynamicFormItemOption } from '@/app/infra/entities/form/dynamic';
|
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 type { PluginV4 } from '@/app/infra/entities/plugin';
|
||||||
import {
|
import {
|
||||||
AgentRunnerMarketplaceError,
|
AgentRunnerMarketplaceError,
|
||||||
@@ -14,7 +13,11 @@ import {
|
|||||||
loadAgentRunnerCatalog,
|
loadAgentRunnerCatalog,
|
||||||
marketplacePluginId,
|
marketplacePluginId,
|
||||||
runnerPluginPrefix,
|
runnerPluginPrefix,
|
||||||
|
readPendingAgentRunnerInstall,
|
||||||
|
subscribePendingAgentRunnerInstall,
|
||||||
|
type InstalledAgentRunner,
|
||||||
} from '@/app/home/agents/agent-runner-marketplace';
|
} from '@/app/home/agents/agent-runner-marketplace';
|
||||||
|
import { usePluginInstallTasks } from '@/app/home/plugins/components/plugin-install-task';
|
||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -109,21 +112,24 @@ export default function AgentRunnerSelect({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
onMetadataRefresh,
|
installScope,
|
||||||
|
onInstalled,
|
||||||
}: {
|
}: {
|
||||||
options: IDynamicFormItemOption[];
|
options: IDynamicFormItemOption[];
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
onValueChange: (value: string) => void;
|
onValueChange: (value: string) => void;
|
||||||
onMetadataRefresh: (configTab: PipelineConfigTab) => void;
|
installScope: string;
|
||||||
|
onInstalled: (installed: InstalledAgentRunner) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { addTask } = usePluginInstallTasks();
|
||||||
const [marketplaceRunners, setMarketplaceRunners] = useState<PluginV4[]>([]);
|
const [marketplaceRunners, setMarketplaceRunners] = useState<PluginV4[]>([]);
|
||||||
const [installedPluginIds, setInstalledPluginIds] = useState<string[]>([]);
|
const [installedPluginIds, setInstalledPluginIds] = useState<string[]>([]);
|
||||||
const [catalogLoading, setCatalogLoading] = useState(true);
|
const [catalogLoading, setCatalogLoading] = useState(true);
|
||||||
const [catalogError, setCatalogError] = useState(false);
|
const [catalogError, setCatalogError] = useState(false);
|
||||||
const [installingPlugin, setInstallingPlugin] = useState<PluginV4 | null>(
|
const [pendingInstall, setPendingInstall] = useState(() =>
|
||||||
null,
|
readPendingAgentRunnerInstall(installScope),
|
||||||
);
|
);
|
||||||
const [installError, setInstallError] = useState<string | null>(null);
|
const [installError, setInstallError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -146,6 +152,13 @@ export default function AgentRunnerSelect({
|
|||||||
void loadCatalog();
|
void loadCatalog();
|
||||||
}, [loadCatalog]);
|
}, [loadCatalog]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const syncPendingInstall = () =>
|
||||||
|
setPendingInstall(readPendingAgentRunnerInstall(installScope));
|
||||||
|
syncPendingInstall();
|
||||||
|
return subscribePendingAgentRunnerInstall(installScope, syncPendingInstall);
|
||||||
|
}, [installScope]);
|
||||||
|
|
||||||
const marketplaceOptions = useMemo(
|
const marketplaceOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
marketplaceRunners.filter((plugin) => {
|
marketplaceRunners.filter((plugin) => {
|
||||||
@@ -159,6 +172,11 @@ export default function AgentRunnerSelect({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectedOption = options.find((option) => option.name === value);
|
const selectedOption = options.find((option) => option.name === value);
|
||||||
|
const installingPlugin = pendingInstall
|
||||||
|
? (marketplaceRunners.find(
|
||||||
|
(plugin) => marketplacePluginId(plugin) === pendingInstall.pluginId,
|
||||||
|
) ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
const handleValueChange = useCallback(
|
const handleValueChange = useCallback(
|
||||||
async (nextValue: string) => {
|
async (nextValue: string) => {
|
||||||
@@ -172,14 +190,21 @@ export default function AgentRunnerSelect({
|
|||||||
const plugin = marketplaceRunners.find(
|
const plugin = marketplaceRunners.find(
|
||||||
(candidate) => marketplacePluginId(candidate) === pluginId,
|
(candidate) => marketplacePluginId(candidate) === pluginId,
|
||||||
);
|
);
|
||||||
if (!plugin || installingPlugin) return;
|
if (!plugin || pendingInstall) return;
|
||||||
|
|
||||||
setInstallingPlugin(plugin);
|
|
||||||
setInstallError(null);
|
setInstallError(null);
|
||||||
try {
|
try {
|
||||||
const installed = await installMarketplaceAgentRunner(plugin);
|
const installed = await installMarketplaceAgentRunner(plugin, {
|
||||||
onMetadataRefresh(installed.configTab);
|
scope: installScope,
|
||||||
onValueChange(installed.runner.name);
|
onTaskCreated: (taskId) =>
|
||||||
|
addTask({
|
||||||
|
taskId,
|
||||||
|
pluginName: marketplacePluginId(plugin),
|
||||||
|
source: 'marketplace',
|
||||||
|
extensionType: 'plugin',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
onInstalled(installed);
|
||||||
await loadCatalog();
|
await loadCatalog();
|
||||||
toast.success(
|
toast.success(
|
||||||
t('wizard.aiEngine.installSuccess', {
|
t('wizard.aiEngine.installSuccess', {
|
||||||
@@ -191,15 +216,17 @@ export default function AgentRunnerSelect({
|
|||||||
setInstallError(message);
|
setInstallError(message);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
setInstallingPlugin(null);
|
setPendingInstall(readPendingAgentRunnerInstall(installScope));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
installingPlugin,
|
addTask,
|
||||||
|
installScope,
|
||||||
loadCatalog,
|
loadCatalog,
|
||||||
marketplaceRunners,
|
marketplaceRunners,
|
||||||
onMetadataRefresh,
|
onInstalled,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
|
pendingInstall,
|
||||||
t,
|
t,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -208,7 +235,7 @@ export default function AgentRunnerSelect({
|
|||||||
<div className="w-full max-w-[22rem] space-y-2">
|
<div className="w-full max-w-[22rem] space-y-2">
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
value={value}
|
||||||
disabled={installingPlugin !== null}
|
disabled={pendingInstall !== null}
|
||||||
onValueChange={(nextValue) => void handleValueChange(nextValue)}
|
onValueChange={(nextValue) => void handleValueChange(nextValue)}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open && catalogError && !catalogLoading) void loadCatalog();
|
if (open && catalogError && !catalogLoading) void loadCatalog();
|
||||||
@@ -264,11 +291,22 @@ export default function AgentRunnerSelect({
|
|||||||
<SelectSeparator />
|
<SelectSeparator />
|
||||||
|
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel className="px-2 py-1 text-[11px] font-medium">
|
<SelectLabel className="flex items-center justify-between gap-2 px-2 py-1 text-[11px] font-medium">
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||||
<Store className="size-3.5" />
|
<Store className="size-3.5" />
|
||||||
{t('agents.marketplaceRunners')}
|
{t('agents.marketplaceRunners')}
|
||||||
</span>
|
</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>
|
</SelectLabel>
|
||||||
{catalogLoading && marketplaceOptions.length === 0 ? (
|
{catalogLoading && marketplaceOptions.length === 0 ? (
|
||||||
<div className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -58,6 +59,12 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import PipelineExtension from '@/app/home/pipelines/components/pipeline-extensions/PipelineExtension';
|
import PipelineExtension from '@/app/home/pipelines/components/pipeline-extensions/PipelineExtension';
|
||||||
import AgentRunnerSelect from '@/app/home/agents/components/AgentRunnerSelect';
|
import AgentRunnerSelect from '@/app/home/agents/components/AgentRunnerSelect';
|
||||||
|
import {
|
||||||
|
getErrorMessage,
|
||||||
|
readPendingAgentRunnerInstall,
|
||||||
|
resumePendingAgentRunnerInstall,
|
||||||
|
type InstalledAgentRunner,
|
||||||
|
} from '@/app/home/agents/agent-runner-marketplace';
|
||||||
|
|
||||||
interface PipelineFormComponentProps {
|
interface PipelineFormComponentProps {
|
||||||
pipelineId?: string;
|
pipelineId?: string;
|
||||||
@@ -204,6 +211,8 @@ const PipelineFormComponent = forwardRef<
|
|||||||
useState<PipelineConfigTab>();
|
useState<PipelineConfigTab>();
|
||||||
const [outputConfigTabSchema, setOutputConfigTabSchema] =
|
const [outputConfigTabSchema, setOutputConfigTabSchema] =
|
||||||
useState<PipelineConfigTab>();
|
useState<PipelineConfigTab>();
|
||||||
|
const [metadataLoaded, setMetadataLoaded] = useState(false);
|
||||||
|
const [pipelineLoaded, setPipelineLoaded] = useState(!isEditMode);
|
||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@@ -219,6 +228,36 @@ const PipelineFormComponent = forwardRef<
|
|||||||
output: {},
|
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(
|
const dynamicFormSystemContext = useMemo(
|
||||||
() => ({ pipeline_id: pipelineId }),
|
() => ({ pipeline_id: pipelineId }),
|
||||||
[pipelineId],
|
[pipelineId],
|
||||||
@@ -244,6 +283,8 @@ const PipelineFormComponent = forwardRef<
|
|||||||
}, [hasUnsavedChanges, onDirtyChange]);
|
}, [hasUnsavedChanges, onDirtyChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setMetadataLoaded(false);
|
||||||
|
setPipelineLoaded(!isEditMode);
|
||||||
// get config schema from metadata
|
// get config schema from metadata
|
||||||
httpClient.getGeneralPipelineMetadata().then((resp) => {
|
httpClient.getGeneralPipelineMetadata().then((resp) => {
|
||||||
for (const config of resp.configs) {
|
for (const config of resp.configs) {
|
||||||
@@ -257,6 +298,7 @@ const PipelineFormComponent = forwardRef<
|
|||||||
setOutputConfigTabSchema(config);
|
setOutputConfigTabSchema(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setMetadataLoaded(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
@@ -279,10 +321,48 @@ const PipelineFormComponent = forwardRef<
|
|||||||
form.reset(loadedValues);
|
form.reset(loadedValues);
|
||||||
savedSnapshotRef.current = JSON.stringify(loadedValues);
|
savedSnapshotRef.current = JSON.stringify(loadedValues);
|
||||||
initializedStagesRef.current.clear();
|
initializedStagesRef.current.clear();
|
||||||
|
setPipelineLoaded(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, isEditMode, pipelineId]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!isEditMode) {
|
if (!isEditMode) {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -523,7 +603,8 @@ const PipelineFormComponent = forwardRef<
|
|||||||
label={extractI18nObject(config.label)}
|
label={extractI18nObject(config.label)}
|
||||||
value={String(field.value ?? '')}
|
value={String(field.value ?? '')}
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
onMetadataRefresh={setAIConfigTabSchema}
|
installScope={runnerInstallScope}
|
||||||
|
onInstalled={applyInstalledRunner}
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ import {
|
|||||||
installMarketplaceAgentRunner,
|
installMarketplaceAgentRunner,
|
||||||
loadAgentRunnerCatalog,
|
loadAgentRunnerCatalog,
|
||||||
marketplacePluginId,
|
marketplacePluginId,
|
||||||
|
readPendingAgentRunnerInstall,
|
||||||
|
resumePendingAgentRunnerInstall,
|
||||||
runnerPluginPrefix,
|
runnerPluginPrefix,
|
||||||
} from '@/app/home/agents/agent-runner-marketplace';
|
} from '@/app/home/agents/agent-runner-marketplace';
|
||||||
import {
|
import {
|
||||||
@@ -104,6 +106,7 @@ import {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const TOTAL_STEPS = 4;
|
const TOTAL_STEPS = 4;
|
||||||
|
const WIZARD_RUNNER_INSTALL_SCOPE = 'wizard';
|
||||||
|
|
||||||
type WizardScenarioId =
|
type WizardScenarioId =
|
||||||
| 'message_reply'
|
| 'message_reply'
|
||||||
@@ -497,7 +500,9 @@ export default function WizardPage() {
|
|||||||
setRunnerInstallError(null);
|
setRunnerInstallError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const installed = await installMarketplaceAgentRunner(plugin);
|
const installed = await installMarketplaceAgentRunner(plugin, {
|
||||||
|
scope: WIZARD_RUNNER_INSTALL_SCOPE,
|
||||||
|
});
|
||||||
setAiConfigTab(installed.configTab);
|
setAiConfigTab(installed.configTab);
|
||||||
setInstalledPluginIds((current) =>
|
setInstalledPluginIds((current) =>
|
||||||
current.includes(pluginId) ? current : [...current, pluginId],
|
current.includes(pluginId) ? current : [...current, pluginId],
|
||||||
@@ -529,6 +534,46 @@ export default function WizardPage() {
|
|||||||
[handleSelectRunner, t],
|
[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 ----
|
// ---- Navigation helpers ----
|
||||||
|
|
||||||
const canProceed = useCallback((): boolean => {
|
const canProceed = useCallback((): boolean => {
|
||||||
|
|||||||
@@ -771,7 +771,10 @@ const enUS = {
|
|||||||
noRunnersAvailableDescription:
|
noRunnersAvailableDescription:
|
||||||
'Install and enable an AgentRunner extension before configuring this Agent.',
|
'Install and enable an AgentRunner extension before configuring this Agent.',
|
||||||
installedRunners: 'Installed AgentRunners',
|
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.',
|
noInstalledRunners: 'No AgentRunner extension is installed yet.',
|
||||||
installingRunner: 'Installing {{runner}}...',
|
installingRunner: 'Installing {{runner}}...',
|
||||||
selectedRunnerUnavailable: 'Selected runner is unavailable',
|
selectedRunnerUnavailable: 'Selected runner is unavailable',
|
||||||
|
|||||||
@@ -783,7 +783,10 @@ const jaJP = {
|
|||||||
noRunnersAvailableDescription:
|
noRunnersAvailableDescription:
|
||||||
'この Agent を設定する前に AgentRunner 拡張機能をインストールして有効にしてください。',
|
'この Agent を設定する前に AgentRunner 拡張機能をインストールして有効にしてください。',
|
||||||
installedRunners: 'インストール済み AgentRunner',
|
installedRunners: 'インストール済み AgentRunner',
|
||||||
marketplaceRunners: 'AgentRunner マーケットプレイス',
|
marketplaceRunners: 'マーケットプレイスの AgentRunner プラグイン',
|
||||||
|
viewMarketplace: '市場を見る',
|
||||||
|
restoringRunnerInstall:
|
||||||
|
'AgentRunner プラグインのインストールを復元し、ランナーを待機しています…',
|
||||||
noInstalledRunners:
|
noInstalledRunners:
|
||||||
'AgentRunner 拡張機能はまだインストールされていません。',
|
'AgentRunner 拡張機能はまだインストールされていません。',
|
||||||
installingRunner: '{{runner}} をインストールしています...',
|
installingRunner: '{{runner}} をインストールしています...',
|
||||||
|
|||||||
@@ -739,7 +739,9 @@ const zhHans = {
|
|||||||
noRunnersAvailableDescription:
|
noRunnersAvailableDescription:
|
||||||
'请先安装并启用 AgentRunner 扩展,再配置此 Agent。',
|
'请先安装并启用 AgentRunner 扩展,再配置此 Agent。',
|
||||||
installedRunners: '已安装的 AgentRunner',
|
installedRunners: '已安装的 AgentRunner',
|
||||||
marketplaceRunners: 'AgentRunner 插件市场',
|
marketplaceRunners: '插件市场中的 AgentRunner 插件',
|
||||||
|
viewMarketplace: '查看市场',
|
||||||
|
restoringRunnerInstall: '正在恢复 AgentRunner 插件安装并等待运行器就绪…',
|
||||||
noInstalledRunners: '尚未安装任何 AgentRunner 扩展。',
|
noInstalledRunners: '尚未安装任何 AgentRunner 扩展。',
|
||||||
installingRunner: '正在安装 {{runner}}...',
|
installingRunner: '正在安装 {{runner}}...',
|
||||||
selectedRunnerUnavailable: '所选运行器不可用',
|
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', () => {
|
test('keeps the 4.11 AgentRunner marketplace installation flow', () => {
|
||||||
assert.match(wizardSource, /loadAgentRunnerCatalog\(\)/);
|
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(
|
assert.match(
|
||||||
runnerMarketplaceSource,
|
runnerMarketplaceSource,
|
||||||
/RUNNER_COMPONENT_FILTER = 'AgentRunner'/,
|
/RUNNER_COMPONENT_FILTER = 'AgentRunner'/,
|
||||||
);
|
);
|
||||||
assert.match(runnerMarketplaceSource, /installPluginFromMarketplace\(/);
|
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, /registrationDeadline/);
|
||||||
|
assert.match(runnerMarketplaceSource, /sessionStorage\.setItem\(/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('requires the selected AgentRunner mandatory configuration before finishing', () => {
|
test('requires the selected AgentRunner mandatory configuration before finishing', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user