feat(agent-runner): finalize 4.x processor integration

This commit is contained in:
huanghuoguoguo
2026-07-12 15:44:05 +08:00
parent 29689962f3
commit e6384aae5d
109 changed files with 2200 additions and 1204 deletions
@@ -1,13 +1,25 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type React from 'react';
import { Link } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Brain, FileJson2, Info, Power, Trash2 } from 'lucide-react';
import {
Brain,
CircleAlert,
CircleCheck,
FileJson2,
Info,
LoaderCircle,
Power,
RefreshCw,
Trash2,
Unplug,
} from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Agent } from '@/app/infra/entities/api';
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import {
PipelineConfigStage,
PipelineConfigTab,
@@ -43,6 +55,7 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
interface AgentFormComponentProps {
agentId: string;
@@ -68,6 +81,10 @@ export default function AgentFormComponent({
useState<SectionItem['name']>('basic');
const [runnerConfigSchema, setRunnerConfigSchema] =
useState<PipelineConfigTab | null>(null);
const [pluginSystemStatus, setPluginSystemStatus] =
useState<ApiRespPluginSystemStatus | null>(null);
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
const [pluginStatusError, setPluginStatusError] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const formSchema = z.object({
@@ -145,6 +162,23 @@ export default function AgentFormComponent({
};
}, [agentId, form, t]);
const loadPluginSystemStatus = useCallback(async () => {
setPluginStatusLoading(true);
setPluginStatusError(false);
try {
setPluginSystemStatus(await httpClient.getPluginSystemStatus());
} catch {
setPluginSystemStatus(null);
setPluginStatusError(true);
} finally {
setPluginStatusLoading(false);
}
}, []);
useEffect(() => {
void loadPluginSystemStatus();
}, [loadPluginSystemStatus]);
const sections: SectionItem[] = [
{ label: t('agents.basicInfo'), name: 'basic', icon: Info },
{ label: t('agents.runnerSettings'), name: 'runner', icon: Brain },
@@ -152,6 +186,128 @@ export default function AgentFormComponent({
];
const currentRunner = (form.watch('runner') as Record<string, any>)?.id;
const runnerOptions = useMemo(() => {
const runnerStage = runnerConfigSchema?.stages.find(
(stage) => stage.name === 'runner',
);
return (
runnerStage?.config.find((item) => item.name === 'id')?.options ?? []
);
}, [runnerConfigSchema]);
const selectedRunnerOption = runnerOptions.find(
(option) => option.name === currentRunner,
);
function renderRunnerStatusActions(showRetry = true) {
return (
<div className="mt-3 flex flex-wrap gap-2">
{showRetry && (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => void loadPluginSystemStatus()}
>
<RefreshCw className="size-4" />
{t('common.retry')}
</Button>
)}
<Button type="button" variant="outline" size="sm" asChild>
<Link to="/home/extensions">{t('plugins.title')}</Link>
</Button>
</div>
);
}
function renderRunnerStatus() {
if (pluginStatusLoading) {
return (
<Alert>
<LoaderCircle className="animate-spin" />
<AlertTitle>{t('agents.runnerStatusLoading')}</AlertTitle>
</Alert>
);
}
if (pluginStatusError || !pluginSystemStatus) {
return (
<Alert variant="destructive">
<CircleAlert />
<AlertTitle>{t('agents.runnerStatusCheckFailed')}</AlertTitle>
<AlertDescription>
{t('agents.runnerStatusCheckFailedDescription')}
{renderRunnerStatusActions()}
</AlertDescription>
</Alert>
);
}
if (!pluginSystemStatus.is_enable) {
return (
<Alert variant="destructive">
<Power />
<AlertTitle>{t('plugins.systemDisabled')}</AlertTitle>
<AlertDescription>
{t('plugins.systemDisabledDesc')}
{renderRunnerStatusActions(false)}
</AlertDescription>
</Alert>
);
}
if (!pluginSystemStatus.is_connected) {
return (
<Alert variant="destructive">
<Unplug />
<AlertTitle>{t('plugins.connectionError')}</AlertTitle>
<AlertDescription>
{t('plugins.connectionErrorDesc')}
{renderRunnerStatusActions()}
</AlertDescription>
</Alert>
);
}
if (runnerOptions.length === 0) {
return (
<Alert variant="destructive">
<CircleAlert />
<AlertTitle>{t('agents.noRunnersAvailable')}</AlertTitle>
<AlertDescription>
{t('agents.noRunnersAvailableDescription')}
{renderRunnerStatusActions()}
</AlertDescription>
</Alert>
);
}
if (!currentRunner || !selectedRunnerOption) {
return (
<Alert variant="destructive">
<CircleAlert />
<AlertTitle>{t('agents.selectedRunnerUnavailable')}</AlertTitle>
<AlertDescription>
{t('agents.selectedRunnerUnavailableDescription', {
runner: currentRunner || t('agents.noRunnerSelected'),
})}
{renderRunnerStatusActions()}
</AlertDescription>
</Alert>
);
}
return (
<Alert className="border-emerald-600/40 bg-emerald-500/5 text-emerald-950 dark:text-emerald-100">
<CircleCheck className="text-emerald-600" />
<AlertTitle>{t('agents.runnerReady')}</AlertTitle>
<AlertDescription>
{t('agents.runnerReadyDescription', {
runner: extractI18nObject(selectedRunnerOption.label),
})}
</AlertDescription>
</Alert>
);
}
function updateSnapshotIfInitial(stageKey: string) {
if (!initializedStagesRef.current.has(stageKey)) {
@@ -431,6 +587,7 @@ export default function AgentFormComponent({
{activeSection === 'runner' && (
<div className="space-y-6">
{renderRunnerStatus()}
{runnerConfigSchema?.stages.map((stage) =>
renderDynamicStage(stage),
)}
@@ -111,6 +111,7 @@ function getValueSchema(spec: DynamicFormValueSpec) {
case DynamicFormItemType.BOOLEAN:
return z.boolean();
case DynamicFormItemType.STRING:
case DynamicFormItemType.SECRET:
return z.string();
case DynamicFormItemType.STRING_ARRAY:
return z.array(z.string());
@@ -43,6 +43,7 @@ import {
Plus,
X,
Eye,
EyeOff,
Wrench,
Trash2,
Sparkles,
@@ -137,6 +138,7 @@ export default function DynamicFormItemComponent({
const [bots, setBots] = useState<Bot[]>([]);
const [tools, setTools] = useState<PluginTool[]>([]);
const [uploading, setUploading] = useState<boolean>(false);
const [secretVisible, setSecretVisible] = useState(false);
const [kbDialogOpen, setKbDialogOpen] = useState(false);
const [tempSelectedKBIds, setTempSelectedKBIds] = useState<string[]>([]);
const [toolsDialogOpen, setToolsDialogOpen] = useState(false);
@@ -385,6 +387,44 @@ export default function DynamicFormItemComponent({
/>
);
case DynamicFormItemType.SECRET:
return (
<div className="relative w-full max-w-md">
<Input
type={secretVisible ? 'text' : 'password'}
autoComplete="new-password"
className="pr-10"
{...field}
value={field.value ?? ''}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-1 top-1/2 size-8 -translate-y-1/2 text-muted-foreground"
aria-label={
secretVisible
? t('common.hideSecret')
: t('common.showSecret')
}
onClick={() => setSecretVisible((visible) => !visible)}
>
{secretVisible ? (
<EyeOff className="size-4" />
) : (
<Eye className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
{secretVisible ? t('common.hideSecret') : t('common.showSecret')}
</TooltipContent>
</Tooltip>
</div>
);
case DynamicFormItemType.TEXT:
return (
<Textarea
@@ -9,6 +9,10 @@ interface MessageDetailsCardProps {
export function MessageDetailsCard({ details }: MessageDetailsCardProps) {
const { t } = useTranslation();
const isLocalAgent = [
'local-agent',
'plugin:langbot-team/LocalAgent/default',
].includes(details.message?.runnerName ?? '');
// Parse query variables JSON string
const queryVariables = useMemo(() => {
@@ -204,7 +208,7 @@ export function MessageDetailsCard({ details }: MessageDetailsCardProps) {
{/* Query Variables Section - Only show for non-local-agent runners */}
{queryVariables &&
Object.keys(queryVariables).length > 0 &&
details.message?.runnerName !== 'local-agent' && (
!isLocalAgent && (
<div className="bg-muted rounded-lg p-3">
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center">
<Braces className="w-4 h-4 mr-2" />
@@ -241,7 +245,7 @@ export function MessageDetailsCard({ details }: MessageDetailsCardProps) {
{/* No data message */}
{(!details.llmCalls || details.llmCalls.length === 0) &&
(!details.errors || details.errors.length === 0) &&
(details.message?.runnerName === 'local-agent' ||
(isLocalAgent ||
!queryVariables ||
Object.keys(queryVariables).length === 0) && (
<div className="text-sm text-muted-foreground text-center py-4">
@@ -51,6 +51,7 @@ export enum DynamicFormItemType {
FLOAT = 'float',
BOOLEAN = 'boolean',
STRING = 'string',
SECRET = 'secret',
TEXT = 'text',
STRING_ARRAY = 'array[string]',
FILE = 'file',
+403 -7
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { UUID } from 'uuidjs';
import { toast } from 'sonner';
@@ -18,6 +18,9 @@ import {
UserPlus,
X,
ExternalLink,
Download,
RefreshCw,
CircleAlert,
} from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
@@ -26,6 +29,8 @@ import {
systemInfo,
initializeUserInfo,
initializeSystemInfo,
getCloudServiceClient,
getCloudServiceClientSync,
} from '@/app/infra/http';
import {
Adapter,
@@ -55,6 +60,7 @@ import {
} from '@/app/infra/entities/adapter-categories';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import i18n from 'i18next';
import { PluginV4 } from '@/app/infra/entities/plugin';
import { Button } from '@/components/ui/button';
import {
@@ -81,6 +87,30 @@ import {
// ---------------------------------------------------------------------------
const TOTAL_STEPS = 4;
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;
function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getErrorMessage(error: unknown): string {
if (error && typeof error === 'object') {
const value = error as { msg?: string; message?: string };
return value.msg || value.message || '';
}
return typeof error === 'string' ? error : '';
}
function marketplacePluginId(plugin: Pick<PluginV4, 'author' | 'name'>) {
return `${plugin.author}/${plugin.name}`;
}
function runnerPluginPrefix(plugin: Pick<PluginV4, 'author' | 'name'>) {
return `plugin:${plugin.author}/${plugin.name}/`;
}
type WizardScenarioId =
| 'message_reply'
@@ -174,12 +204,108 @@ export default function WizardPage() {
const [aiConfigTab, setAiConfigTab] = useState<PipelineConfigTab | null>(
null,
);
const [marketplaceRunners, setMarketplaceRunners] = useState<PluginV4[]>([]);
const [installedPluginIds, setInstalledPluginIds] = useState<string[]>([]);
const [isRunnerCatalogLoading, setIsRunnerCatalogLoading] = useState(true);
const [runnerCatalogError, setRunnerCatalogError] = useState(false);
const [installingRunnerPluginId, setInstallingRunnerPluginId] = useState<
string | null
>(null);
const [runnerInstallError, setRunnerInstallError] = useState<string | null>(
null,
);
const [isLoading, setIsLoading] = useState(true);
const [isCreatingBot, setIsCreatingBot] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSavingBot, setIsSavingBot] = useState(false);
const [botSaved, setBotSaved] = useState(false);
const loadRunnerCatalog = useCallback(async () => {
setIsRunnerCatalogLoading(true);
setRunnerCatalogError(false);
try {
const cloudClient = await getCloudServiceClient();
const [firstSearchResult, recommendationResult, installedResult] =
await Promise.all([
cloudClient.searchMarketplaceExtensions({
query: '',
page: 1,
page_size: RUNNER_CATALOG_PAGE_SIZE,
type_filter: 'plugin',
component_filter: RUNNER_COMPONENT_FILTER,
}),
cloudClient.getRecommendationLists().catch(() => ({ lists: [] })),
httpClient.getPlugins().catch(() => ({ plugins: [] })),
]);
const remainingPageCount = Math.max(
0,
Math.ceil((firstSearchResult.total || 0) / RUNNER_CATALOG_PAGE_SIZE) -
1,
);
const remainingResults = await Promise.all(
Array.from({ length: remainingPageCount }, (_, index) =>
cloudClient.searchMarketplaceExtensions({
query: '',
page: index + 2,
page_size: RUNNER_CATALOG_PAGE_SIZE,
type_filter: 'plugin',
component_filter: RUNNER_COMPONENT_FILTER,
}),
),
);
const catalogPlugins = [
...(firstSearchResult.plugins || []),
...remainingResults.flatMap((result) => result.plugins || []),
];
const recommendationOrder = new Map<string, number>();
let nextOrder = 0;
for (const list of recommendationResult.lists || []) {
for (const plugin of list.plugins || []) {
if (!plugin.components?.[RUNNER_COMPONENT_FILTER]) continue;
const id = marketplacePluginId(plugin);
if (!recommendationOrder.has(id)) {
recommendationOrder.set(id, nextOrder);
nextOrder += 1;
}
}
}
const runners = catalogPlugins
.filter((plugin) => plugin.components?.[RUNNER_COMPONENT_FILTER])
.sort((left, right) => {
const leftOrder = recommendationOrder.get(marketplacePluginId(left));
const rightOrder = recommendationOrder.get(
marketplacePluginId(right),
);
if (leftOrder !== undefined && rightOrder !== undefined) {
return leftOrder - rightOrder;
}
if (leftOrder !== undefined) return -1;
if (rightOrder !== undefined) return 1;
return right.install_count - left.install_count;
});
setMarketplaceRunners(runners);
setInstalledPluginIds(
installedResult.plugins.map((plugin) => {
const metadata = plugin.manifest.manifest.metadata;
return `${metadata.author ?? ''}/${metadata.name}`;
}),
);
} catch (error) {
console.error('Failed to load AgentRunner catalog', error);
setRunnerCatalogError(true);
} finally {
setIsRunnerCatalogLoading(false);
}
}, []);
useEffect(() => {
void loadRunnerCatalog();
}, [loadRunnerCatalog]);
// ---- Helper: persist wizard progress to backend (fire-and-forget) ----
const saveProgress = useCallback(
(overrides: Partial<WizardProgress> = {}) => {
@@ -358,9 +484,9 @@ export default function WizardPage() {
// ---- Runner selection with progress saving ----
const handleSelectRunner = useCallback(
(runner: string) => {
(runner: string, configTab: PipelineConfigTab | null = aiConfigTab) => {
setSelectedRunner(runner);
const configStage = aiConfigTab?.stages.find((s) => s.name === runner);
const configStage = configTab?.stages.find((s) => s.name === runner);
const defaults = configStage ? getDefaultValues(configStage.config) : {};
const promptKey = selectedScenario
? WIZARD_SCENARIO_PROMPT_KEYS[selectedScenario]
@@ -377,6 +503,90 @@ export default function WizardPage() {
[aiConfigTab, saveProgress, selectedScenario, t],
);
const handleInstallRunner = useCallback(
async (plugin: PluginV4) => {
const pluginId = marketplacePluginId(plugin);
setInstallingRunnerPluginId(pluginId);
setRunnerInstallError(null);
try {
if (!plugin.latest_version) {
throw new Error(t('wizard.aiEngine.versionUnavailable'));
}
const { task_id: taskId } =
await httpClient.installPluginFromMarketplace(
plugin.author,
plugin.name,
plugin.latest_version,
);
const installDeadline = Date.now() + RUNNER_INSTALL_TIMEOUT_MS;
let installCompleted = false;
while (Date.now() < installDeadline) {
const task = await httpClient.getAsyncTask(taskId);
if (task.runtime.done) {
if (task.runtime.exception) {
throw new Error(task.runtime.exception);
}
installCompleted = true;
break;
}
await wait(1000);
}
if (!installCompleted) {
throw new Error(t('wizard.aiEngine.installTimeout'));
}
const registrationDeadline =
Date.now() + RUNNER_REGISTRATION_TIMEOUT_MS;
const prefix = runnerPluginPrefix(plugin);
while (Date.now() < registrationDeadline) {
const metadata = await httpClient.getGeneralPipelineMetadata();
const nextAiTab =
metadata.configs.find((config) => config.name === 'ai') ?? null;
const nextRunnerStage = nextAiTab?.stages.find(
(stage) => stage.name === 'runner',
);
const nextRunnerOptions =
nextRunnerStage?.config.find((item) => item.name === 'id')
?.options ?? [];
const pluginRunnerOptions = nextRunnerOptions.filter((option) =>
option.name.startsWith(prefix),
);
const preferredRunner =
pluginRunnerOptions.find((option) =>
option.name.endsWith('/default'),
) ?? pluginRunnerOptions[0];
if (nextAiTab && preferredRunner) {
setAiConfigTab(nextAiTab);
setInstalledPluginIds((current) =>
current.includes(pluginId) ? current : [...current, pluginId],
);
handleSelectRunner(preferredRunner.name, nextAiTab);
toast.success(
t('wizard.aiEngine.installSuccess', {
runner: extractI18nObject(plugin.label),
}),
);
return;
}
await wait(1000);
}
throw new Error(t('wizard.aiEngine.registrationTimeout'));
} catch (error) {
const message =
getErrorMessage(error) || t('wizard.aiEngine.installFailed');
setRunnerInstallError(message);
toast.error(message);
} finally {
setInstallingRunnerPluginId(null);
}
},
[handleSelectRunner, t],
);
// ---- Navigation helpers ----
const canProceed = useCallback((): boolean => {
@@ -843,8 +1053,16 @@ export default function WizardPage() {
{currentStep === 2 && (
<StepAIEngine
runnerOptions={runnerOptions}
marketplaceRunners={marketplaceRunners}
installedPluginIds={installedPluginIds}
isRunnerCatalogLoading={isRunnerCatalogLoading}
runnerCatalogError={runnerCatalogError}
installingRunnerPluginId={installingRunnerPluginId}
runnerInstallError={runnerInstallError}
selected={selectedRunner}
onSelect={handleSelectRunner}
onInstall={handleInstallRunner}
onRetryCatalog={loadRunnerCatalog}
isLocalAccount={isLocalAccount}
onSpaceAuth={handleSpaceAuth}
runnerConfigItems={selectedRunnerConfigItems}
@@ -1351,8 +1569,16 @@ function StepBotConfig({
function StepAIEngine({
runnerOptions,
marketplaceRunners,
installedPluginIds,
isRunnerCatalogLoading,
runnerCatalogError,
installingRunnerPluginId,
runnerInstallError,
selected,
onSelect,
onInstall,
onRetryCatalog,
isLocalAccount,
onSpaceAuth,
runnerConfigItems,
@@ -1360,8 +1586,16 @@ function StepAIEngine({
onRunnerConfigChange,
}: {
runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[];
marketplaceRunners: PluginV4[];
installedPluginIds: string[];
isRunnerCatalogLoading: boolean;
runnerCatalogError: boolean;
installingRunnerPluginId: string | null;
runnerInstallError: string | null;
selected: string | null;
onSelect: (name: string) => void;
onInstall: (plugin: PluginV4) => void;
onRetryCatalog: () => void;
isLocalAccount: boolean;
onSpaceAuth: () => void;
runnerConfigItems: IDynamicFormItemSchema[];
@@ -1383,6 +1617,20 @@ function StepAIEngine({
return r ? extractI18nObject(r.label) : (selected ?? '');
}, [runnerOptions, selected]);
const marketplaceRunnerIds = useMemo(
() => new Set(marketplaceRunners.map(marketplacePluginId)),
[marketplaceRunners],
);
const standaloneRunnerOptions = useMemo(
() =>
runnerOptions.filter((option) => {
if (!option.name.startsWith('plugin:')) return true;
const pluginId = option.name.slice('plugin:'.length).split('/');
return !marketplaceRunnerIds.has(`${pluginId[0]}/${pluginId[1]}`);
}),
[marketplaceRunnerIds, runnerOptions],
);
// Before any runner is selected: centered grid layout
if (!selected) {
return (
@@ -1395,11 +1643,136 @@ function StepAIEngine({
{t('wizard.aiEngine.description')}
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{runnerOptions.map((opt) => (
{runnerCatalogError && (
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-4">
<div className="flex items-start gap-3">
<CircleAlert className="mt-0.5 size-5 shrink-0 text-destructive" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">
{t('wizard.aiEngine.catalogUnavailable')}
</p>
<p className="mt-1 text-sm text-muted-foreground">
{t('wizard.aiEngine.catalogUnavailableDescription')}
</p>
<Button
type="button"
variant="outline"
size="sm"
className="mt-3"
onClick={onRetryCatalog}
>
<RefreshCw className="size-4" />
{t('common.retry')}
</Button>
</div>
</div>
</div>
)}
{runnerInstallError && (
<div className="rounded-md border border-destructive/40 bg-destructive/5 px-4 py-3 text-sm text-destructive">
{runnerInstallError}
</div>
)}
{isRunnerCatalogLoading && marketplaceRunners.length === 0 && (
<div className="flex min-h-32 items-center justify-center rounded-md border border-dashed">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="size-4 animate-spin" />
{t('wizard.aiEngine.loadingCatalog')}
</div>
</div>
)}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{marketplaceRunners.map((plugin) => {
const pluginId = marketplacePluginId(plugin);
const prefix = runnerPluginPrefix(plugin);
const registeredOptions = runnerOptions.filter((option) =>
option.name.startsWith(prefix),
);
const preferredOption =
registeredOptions.find((option) =>
option.name.endsWith('/default'),
) ?? registeredOptions[0];
const isInstalled = installedPluginIds.includes(pluginId);
const isInstalling = installingRunnerPluginId === pluginId;
const iconUrl =
getCloudServiceClientSync().resolveMarketplaceIconURL(
plugin.type,
plugin.author,
plugin.name,
plugin.icon,
);
return (
<Card key={pluginId} className="flex min-h-52 flex-col">
<CardHeader className="flex flex-row items-start gap-3 pb-3">
<img
src={iconUrl}
alt=""
className="size-10 shrink-0 rounded-md border bg-muted object-cover"
/>
<div className="min-w-0 flex-1">
<CardTitle className="text-base">
{extractI18nObject(plugin.label) || plugin.name}
</CardTitle>
<CardDescription className="mt-1 text-xs font-mono">
{plugin.author}/{plugin.name}
</CardDescription>
</div>
</CardHeader>
<CardContent className="flex flex-1 flex-col">
<p className="line-clamp-3 text-sm text-muted-foreground">
{extractI18nObject(plugin.description)}
</p>
<div className="mt-auto pt-4">
{preferredOption ? (
<Button
type="button"
className="w-full"
onClick={() => onSelect(preferredOption.name)}
>
<Check className="size-4" />
{t('wizard.aiEngine.useInstalled')}
</Button>
) : isInstalled ? (
<Button
type="button"
variant="outline"
disabled
className="w-full"
>
<CircleAlert className="size-4" />
{t('wizard.aiEngine.installedUnavailable')}
</Button>
) : (
<Button
type="button"
className="w-full"
disabled={installingRunnerPluginId !== null}
onClick={() => onInstall(plugin)}
>
{isInstalling ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Download className="size-4" />
)}
{isInstalling
? t('wizard.aiEngine.installing')
: t('wizard.aiEngine.installAndContinue')}
</Button>
)}
</div>
</CardContent>
</Card>
);
})}
{standaloneRunnerOptions.map((opt) => (
<Card
key={opt.name}
className="cursor-pointer transition-all hover:shadow-md hover:border-primary/50"
className="min-h-40 cursor-pointer transition-all hover:border-primary/50 hover:shadow-md"
onClick={() => onSelect(opt.name)}
>
<CardHeader className="flex flex-row items-center gap-3">
@@ -1415,6 +1788,29 @@ function StepAIEngine({
</Card>
))}
</div>
{!isRunnerCatalogLoading &&
marketplaceRunners.length === 0 &&
standaloneRunnerOptions.length === 0 &&
!runnerCatalogError && (
<div className="rounded-md border border-dashed p-6 text-center">
<p className="font-medium">
{t('wizard.aiEngine.noMarketplaceRunners')}
</p>
<p className="mt-1 text-sm text-muted-foreground">
{t('wizard.aiEngine.noMarketplaceRunnersDescription')}
</p>
</div>
)}
<div className="flex justify-center">
<Button variant="outline" size="sm" asChild>
<Link to="/home/extensions?type=plugin&component=AgentRunner">
{t('wizard.aiEngine.browseRunners')}
<ExternalLink className="size-4" />
</Link>
</Button>
</div>
</div>
);
}
@@ -1476,7 +1872,7 @@ function StepAIEngine({
})}
{/* Space promotion banner */}
{selected === 'plugin:langbot/local-agent/default' &&
{selected === 'plugin:langbot-team/LocalAgent/default' &&
isLocalAccount && (
<div className="animate-in fade-in slide-in-from-left-2 duration-300">
<div className="relative rounded-lg p-[2px] bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500">
+39 -3
View File
@@ -29,6 +29,8 @@ const enUS = {
loginLoadErrorDesc:
'Unable to connect to the LangBot backend. Please make sure the service is running and try again.',
retry: 'Retry',
showSecret: 'Show secret',
hideSecret: 'Hide secret',
enterEmail: 'Enter email address',
enterPassword: 'Enter password',
invalidEmail: 'Please enter a valid email address',
@@ -364,7 +366,7 @@ const enUS = {
'Bind the pipeline that processes messages for this bot',
eventRouting: 'Event Routing',
eventRoutingDescription:
'Choose which processor handles each event received by this bot. Edit the processing logic on the Agent page. Pipelines only support message events.',
'Choose which processor handles each event received by this bot. Edit the logic in the corresponding Agent or Pipeline configuration. Pipelines only support message events.',
eventBindings: 'Event Routes',
addEventBinding: 'Add Route',
addBehavior: 'Add behavior',
@@ -650,7 +652,7 @@ const enUS = {
},
},
agents: {
title: 'Agent',
title: 'Processors',
description: 'Create reusable processors and use them in bot event routing',
create: 'Create Processor',
editAgent: 'Edit Agent',
@@ -664,7 +666,7 @@ const enUS = {
groupByKind: 'Group by type',
groupByKindShort: 'Group',
pipelineTypeDescription:
'Keep the existing no-code message pipeline for backward compatibility. It only handles message events.',
'Use a visual multi-stage flow to control message preprocessing, AI, postprocessing, extensions, and output. Message events only.',
allEvents: 'Supports all events',
messageEventsOnly: 'Message events only',
basicInfo: 'Basic Information',
@@ -695,6 +697,20 @@ const enUS = {
deleteAgentHint:
'Once deleted, events bound to it can no longer be executed.',
noRunnerMetadata: 'No AgentRunner metadata is currently available.',
runnerStatusLoading: 'Checking runner status',
runnerStatusCheckFailed: 'Runner status could not be checked',
runnerStatusCheckFailedDescription:
'Retry the check. If it still fails, verify the backend and plugin runtime.',
noRunnersAvailable: 'No runners are available',
noRunnersAvailableDescription:
'Install and enable an AgentRunner extension before configuring this Agent.',
selectedRunnerUnavailable: 'Selected runner is unavailable',
selectedRunnerUnavailableDescription:
'{{runner}} is not currently registered. Select another runner or restore its extension.',
noRunnerSelected: 'No runner selected',
runnerReady: 'Runner ready',
runnerReadyDescription:
'{{runner}} is registered and the plugin runtime is connected.',
},
plugins: {
title: 'Extensions',
@@ -1974,6 +1990,26 @@ const enUS = {
title: 'Select an AI Engine',
description:
"Choose the AI engine that will power your bot's intelligence.",
loadingCatalog: 'Loading AgentRunner extensions...',
catalogUnavailable: 'Runner catalog is unavailable',
catalogUnavailableDescription:
'Installed runners are still available. Retry the catalog or browse Extensions.',
noMarketplaceRunners: 'No AgentRunner extensions are published yet',
noMarketplaceRunnersDescription:
'Retry after runner extensions are published to the configured Marketplace.',
browseRunners: 'Browse Runner Extensions',
installAndContinue: 'Install & Continue',
installing: 'Installing...',
useInstalled: 'Use This Runner',
installedUnavailable: 'Installed, Runner Unavailable',
installSuccess: '{{runner}} installed and selected',
installFailed: 'Failed to install the Runner extension',
versionUnavailable:
'The Marketplace did not return an installable version.',
installTimeout:
'Runner installation timed out. Check the task in Extensions.',
registrationTimeout:
'The extension installed, but its Runner did not register. Check the plugin runtime and retry.',
},
spaceBanner: {
message:
+3 -3
View File
@@ -375,7 +375,7 @@ const esES = {
'Vincula el Pipeline que procesa los mensajes de este Bot',
eventRouting: 'Enrutamiento de eventos',
eventRoutingDescription:
'Elige qué procesador maneja cada evento recibido por este Bot. Edita la lógica de procesamiento en la página Agent. Los Pipelines solo admiten eventos de mensaje.',
'Elige qué procesador maneja cada evento recibido por este Bot. Edita la lógica en la configuración del Agent o Pipeline correspondiente. Los Pipelines solo admiten eventos de mensaje.',
routingRules: 'Reglas de enrutamiento condicional',
routingRulesDescription:
'Las reglas se evalúan en orden; la primera coincidencia enruta a su pipeline. Si ninguna coincide, se usa el pipeline predeterminado.',
@@ -484,7 +484,7 @@ const esES = {
},
},
agents: {
title: 'Agent',
title: 'Procesadores',
description:
'Crea procesadores reutilizables y úsalos en el enrutamiento de eventos del bot',
create: 'Crear procesador',
@@ -499,7 +499,7 @@ const esES = {
groupByKind: 'Agrupar por tipo',
groupByKindShort: 'Agrupar',
pipelineTypeDescription:
'Mantiene el pipeline de mensajes sin código para compatibilidad con versiones anteriores. Solo procesa eventos de mensaje.',
'Usa un flujo visual de varias etapas para controlar el preprocesamiento, la IA, el posprocesamiento, las extensiones y la salida. Solo procesa eventos de mensaje.',
allEvents: 'Compatible con todos los eventos',
messageEventsOnly: 'Solo eventos de mensaje',
basicInfo: 'Información básica',
+39 -3
View File
@@ -30,6 +30,8 @@ const jaJP = {
loginLoadErrorDesc:
'LangBot バックエンドに接続できません。サービスが起動していることを確認してから再試行してください。',
retry: '再試行',
showSecret: 'シークレットを表示',
hideSecret: 'シークレットを隠す',
enterEmail: 'メールアドレスを入力',
enterPassword: 'パスワードを入力',
invalidEmail: '有効なメールアドレスを入力してください',
@@ -370,7 +372,7 @@ const jaJP = {
'このボットのメッセージを処理するパイプラインを紐付け',
eventRouting: 'イベントルーティング',
eventRoutingDescription:
'このボットが受信した各イベントをどのプロセッサーに渡すかを選択します。処理ロジックは Agent ページで編集します。Pipeline はメッセージイベントのみ対応します。',
'このボットが受信した各イベントをどのプロセッサーに渡すかを選択します。対応する Agent または Pipeline の設定で処理ロジックを編集します。Pipeline はメッセージイベントのみ対応します。',
eventBindings: 'イベントルート',
addEventBinding: 'ルートを追加',
addBehavior: '動作を追加',
@@ -663,7 +665,7 @@ const jaJP = {
},
},
agents: {
title: 'Agent',
title: 'プロセッサー',
description:
'再利用可能なプロセッサーを作成し、ボットのイベントルーティングで使用します',
create: 'プロセッサーを作成',
@@ -679,7 +681,7 @@ const jaJP = {
groupByKind: 'タイプ別にグループ化',
groupByKindShort: 'グループ',
pipelineTypeDescription:
'既存のノーコードメッセージ Pipeline を互換性のため保持します。メッセージイベントのみ処理できます。',
'ビジュアルな多段フローで、メッセージの前処理、AI、後処理、拡張、出力を制御します。メッセージイベントのみ処理できます。',
allEvents: 'すべてのイベントに対応',
messageEventsOnly: 'メッセージイベントのみ',
basicInfo: '基本情報',
@@ -709,6 +711,20 @@ const jaJP = {
deleteAgentAction: 'この Agent を削除',
deleteAgentHint: '削除すると、紐付けられたイベントは実行できなくなります。',
noRunnerMetadata: '現在利用可能な AgentRunner メタデータはありません。',
runnerStatusLoading: 'Runner の状態を確認しています',
runnerStatusCheckFailed: 'Runner の状態を確認できませんでした',
runnerStatusCheckFailedDescription:
'再試行してください。失敗が続く場合は、バックエンドとプラグインランタイムを確認してください。',
noRunnersAvailable: '利用可能な Runner がありません',
noRunnersAvailableDescription:
'この Agent を設定する前に AgentRunner 拡張機能をインストールして有効にしてください。',
selectedRunnerUnavailable: '選択した Runner は利用できません',
selectedRunnerUnavailableDescription:
'{{runner}} は現在登録されていません。別の Runner を選択するか、対応する拡張機能を復元してください。',
noRunnerSelected: 'Runner が選択されていません',
runnerReady: 'Runner の準備完了',
runnerReadyDescription:
'{{runner}} は登録済みで、プラグインランタイムに接続されています。',
},
plugins: {
title: '拡張機能',
@@ -1897,6 +1913,26 @@ const jaJP = {
title: 'AIエンジンを選択',
description:
'ボットのインテリジェンスを駆動するAIエンジンを選択してください。',
loadingCatalog: 'AgentRunner 拡張機能を読み込んでいます...',
catalogUnavailable: 'Runner カタログを読み込めません',
catalogUnavailableDescription:
'インストール済みの Runner は引き続き使用できます。再試行するか、拡張機能を確認してください。',
noMarketplaceRunners: 'AgentRunner 拡張機能はまだ公開されていません',
noMarketplaceRunnersDescription:
'設定済みのマーケットプレイスに Runner 拡張機能が公開された後、再試行してください。',
browseRunners: 'Runner 拡張機能を見る',
installAndContinue: 'インストールして続行',
installing: 'インストール中...',
useInstalled: 'この Runner を使用',
installedUnavailable: 'インストール済み、Runner は利用不可',
installSuccess: '{{runner}} をインストールして選択しました',
installFailed: 'Runner 拡張機能のインストールに失敗しました',
versionUnavailable:
'マーケットプレイスからインストール可能なバージョンが返されませんでした。',
installTimeout:
'Runner のインストールがタイムアウトしました。拡張機能のタスクを確認してください。',
registrationTimeout:
'拡張機能はインストールされましたが、Runner が登録されませんでした。プラグインランタイムを確認して再試行してください。',
},
spaceBanner: {
message:
+3 -3
View File
@@ -374,7 +374,7 @@ const ruRU = {
'Привяжите конвейер, обрабатывающий сообщения для этого бота',
eventRouting: 'Маршрутизация событий',
eventRoutingDescription:
'Выберите, какой обработчик будет получать каждое событие этого бота. Логика обработки редактируется на странице Agent. Pipeline поддерживает только события сообщений.',
'Выберите обработчик для каждого события бота. Изменяйте логику в настройках соответствующего Agent или Pipeline. Pipeline поддерживает только события сообщений.',
routingRules: 'Правила условной маршрутизации',
routingRulesDescription:
'Правила проверяются по порядку; первое совпадение направляет в соответствующий конвейер. При отсутствии совпадений используется конвейер по умолчанию.',
@@ -482,7 +482,7 @@ const ruRU = {
},
},
agents: {
title: 'Agent',
title: 'Обработчики',
description:
'Создавайте переиспользуемые обработчики и используйте их в маршрутизации событий бота',
create: 'Создать обработчик',
@@ -497,7 +497,7 @@ const ruRU = {
groupByKind: 'Группировать по типу',
groupByKindShort: 'Группа',
pipelineTypeDescription:
'Сохраняет существующий no-code конвейер сообщений для обратной совместимости. Обрабатывает только события сообщений.',
'Визуальный многоэтапный процесс управляет предобработкой сообщений, AI, постобработкой, расширениями и выводом. Только события сообщений.',
allEvents: 'Поддерживает все события',
messageEventsOnly: 'Только события сообщений',
basicInfo: 'Основная информация',
+3 -3
View File
@@ -361,7 +361,7 @@ const thTH = {
'ผูก Pipeline ที่ประมวลผลข้อความสำหรับ Bot นี้',
eventRouting: 'การกำหนดเส้นทางเหตุการณ์',
eventRoutingDescription:
'เลือกตัวประมวลผลที่จะจัดการแต่ละเหตุการณ์ที่ Bot นี้ได้รับ แก้ไขตรรกะการประมวลผลในหน้า Agent และ Pipeline รองรับเฉพาะเหตุการณ์ข้อความ',
'เลือกตัวประมวลผลสำหรับแต่ละเหตุการณ์ของ Bot และแก้ไขตรรกะในการตั้งค่า Agent หรือ Pipeline ที่เกี่ยวข้อง โดย Pipeline รองรับเฉพาะเหตุการณ์ข้อความ',
routingRules: 'กฎการกำหนดเส้นทางตามเงื่อนไข',
routingRulesDescription:
'กฎจะถูกประเมินตามลำดับ การจับคู่แรกจะกำหนดเส้นทางไปยัง Pipeline ที่เกี่ยวข้อง หากไม่ตรงกันจะใช้ Pipeline เริ่มต้นด้านบน',
@@ -469,7 +469,7 @@ const thTH = {
},
},
agents: {
title: 'Agent',
title: 'ตัวประมวลผล',
description: 'สร้างตัวประมวลผลที่ใช้ซ้ำได้และใช้ในเส้นทางเหตุการณ์ของบอท',
create: 'สร้างตัวประมวลผล',
editAgent: 'แก้ไข Agent',
@@ -483,7 +483,7 @@ const thTH = {
groupByKind: 'จัดกลุ่มตามประเภท',
groupByKindShort: 'จัดกลุ่ม',
pipelineTypeDescription:
'คงไว้ซึ่ง pipeline ข้อความแบบไม่ต้องเขียนโค้ดเพื่อความเข้ากันได้ย้อนหลัง รองรับเฉพาะเหตุการณ์ข้อความ',
'ใช้โฟลว์แบบหลายขั้นตอนที่มองเห็นได้เพื่อควบคุมการประมวลผลก่อน AI การประมวลผลหลัง ส่วนขยาย และผลลัพธ์ รองรับเฉพาะเหตุการณ์ข้อความ',
allEvents: 'รองรับทุกเหตุการณ์',
messageEventsOnly: 'เฉพาะเหตุการณ์ข้อความ',
basicInfo: 'ข้อมูลพื้นฐาน',
+3 -3
View File
@@ -370,7 +370,7 @@ const viVN = {
'Liên kết Pipeline xử lý tin nhắn cho Bot này',
eventRouting: 'Định tuyến sự kiện',
eventRoutingDescription:
'Chọn bộ xử lý sẽ nhận từng sự kiện của Bot này. Chỉnh sửa logic xử lý trong trang Agent. Pipeline chỉ hỗ trợ sự kiện tin nhắn.',
'Chọn bộ xử lý cho từng sự kiện của Bot. Chỉnh sửa logic trong cấu hình Agent hoặc Pipeline tương ứng. Pipeline chỉ hỗ trợ sự kiện tin nhắn.',
routingRules: 'Quy tắc định tuyến có điều kiện',
routingRulesDescription:
'Các quy tắc được đánh giá theo thứ tự; kết quả khớp đầu tiên sẽ định tuyến đến pipeline tương ứng. Nếu không khớp, pipeline mặc định ở trên sẽ được sử dụng.',
@@ -478,7 +478,7 @@ const viVN = {
},
},
agents: {
title: 'Agent',
title: 'Bộ xử lý',
description:
'Tạo bộ xử lý có thể tái sử dụng và dùng chúng trong định tuyến sự kiện của bot',
create: 'Tạo bộ xử lý',
@@ -493,7 +493,7 @@ const viVN = {
groupByKind: 'Nhóm theo loại',
groupByKindShort: 'Nhóm',
pipelineTypeDescription:
'Giữ lại pipeline tin nhắn không cần mã hiện có để tương thích ngược. Chỉ xử lý sự kiện tin nhắn.',
'Dùng luồng trực quan nhiều giai đoạn để kiểm soát tiền xử lý, AI, hậu xử lý, tiện ích mở rộng và đầu ra. Chỉ xử lý sự kiện tin nhắn.',
allEvents: 'Hỗ trợ tất cả sự kiện',
messageEventsOnly: 'Chỉ sự kiện tin nhắn',
basicInfo: 'Thông tin cơ bản',
+36 -3
View File
@@ -28,6 +28,8 @@ const zhHans = {
loginLoadError: '无法连接到服务器',
loginLoadErrorDesc: '无法连接到 LangBot 后端服务,请确认服务已启动后重试。',
retry: '重试',
showSecret: '显示密钥',
hideSecret: '隐藏密钥',
enterEmail: '输入邮箱地址',
enterPassword: '输入密码',
invalidEmail: '请输入有效的邮箱地址',
@@ -349,7 +351,7 @@ const zhHans = {
routingConnectionDescription: '绑定处理此机器人消息的流水线',
eventRouting: '事件路由',
eventRoutingDescription:
'选择此机器人收到不同事件时交给哪个处理器。具体处理逻辑在 Agent 页面编辑,Pipeline 仅支持消息事件。',
'选择此机器人收到不同事件时交给哪个处理器。在对应的 Agent 或 Pipeline 配置中编辑处理逻辑;Pipeline 仅支持消息事件。',
eventBindings: '事件路由',
addEventBinding: '添加路由',
addBehavior: '添加行为',
@@ -624,7 +626,7 @@ const zhHans = {
},
},
agents: {
title: 'Agent',
title: '处理器',
description: '创建可复用的处理器,并在机器人事件路由中使用',
create: '创建处理器',
editAgent: '编辑 Agent',
@@ -637,7 +639,7 @@ const zhHans = {
groupByKind: '按类型分组',
groupByKindShort: '分组',
pipelineTypeDescription:
'保留现有无代码消息流水线,兼容旧配置,只能处理消息事件。',
'通过可视化多阶段流程控制消息的预处理、AI、后处理、扩展和输出;仅处理消息事件。',
allEvents: '支持全部事件',
messageEventsOnly: '仅支持消息事件',
basicInfo: '基础信息',
@@ -666,6 +668,19 @@ const zhHans = {
deleteAgentAction: '删除此 Agent',
deleteAgentHint: '删除后,绑定到它的事件将无法继续执行。',
noRunnerMetadata: '当前没有可用的 AgentRunner 元数据。',
runnerStatusLoading: '正在检查运行器状态',
runnerStatusCheckFailed: '无法检查运行器状态',
runnerStatusCheckFailedDescription:
'请重试检查;如果仍然失败,请确认后端和插件运行时正常。',
noRunnersAvailable: '没有可用的运行器',
noRunnersAvailableDescription:
'请先安装并启用 AgentRunner 扩展,再配置此 Agent。',
selectedRunnerUnavailable: '所选运行器不可用',
selectedRunnerUnavailableDescription:
'{{runner}} 当前未注册。请选择其他运行器,或恢复对应扩展。',
noRunnerSelected: '尚未选择运行器',
runnerReady: '运行器已就绪',
runnerReadyDescription: '{{runner}} 已注册,插件运行时连接正常。',
},
plugins: {
title: '插件扩展',
@@ -1886,6 +1901,24 @@ const zhHans = {
aiEngine: {
title: '选择 AI 引擎',
description: '选择驱动机器人智能的 AI 引擎。',
loadingCatalog: '正在加载 AgentRunner 扩展...',
catalogUnavailable: '无法加载运行器目录',
catalogUnavailableDescription:
'已安装的运行器仍可使用。你可以重试,或前往扩展页面查看。',
noMarketplaceRunners: '市场暂未发布 AgentRunner 扩展',
noMarketplaceRunnersDescription:
'请在运行器扩展发布到当前配置的市场后重试。',
browseRunners: '浏览运行器扩展',
installAndContinue: '安装并继续',
installing: '正在安装...',
useInstalled: '使用此运行器',
installedUnavailable: '已安装,运行器不可用',
installSuccess: '{{runner}} 已安装并选中',
installFailed: '运行器扩展安装失败',
versionUnavailable: '扩展市场未返回可安装版本。',
installTimeout: '运行器安装超时,请前往扩展页面检查任务。',
registrationTimeout:
'扩展已安装,但运行器未完成注册。请检查插件运行时后重试。',
},
spaceBanner: {
message: '接入 LangBot Space,获取免费试用模型额度,零配置极速开箱!',
+3 -3
View File
@@ -349,7 +349,7 @@ const zhHant = {
routingConnectionDescription: '綁定處理此機器人訊息的流程線',
eventRouting: '事件路由',
eventRoutingDescription:
'選擇此機器人收到不同事件時交給哪個處理器。具體處理邏輯在 Agent 頁面編輯,Pipeline 僅支援訊息事件。',
'選擇此機器人收到不同事件時交給哪個處理器。在對應的 Agent 或 Pipeline 設定中編輯處理邏輯;Pipeline 僅支援訊息事件。',
routingRules: '條件路由規則',
routingRulesDescription:
'按順序匹配,命中第一條規則後路由到對應流程線;都不匹配時使用上方預設流程線',
@@ -454,7 +454,7 @@ const zhHant = {
},
},
agents: {
title: 'Agent',
title: '處理器',
description: '建立可重用的處理器,並在機器人事件路由中使用',
create: '建立處理器',
editAgent: '編輯 Agent',
@@ -467,7 +467,7 @@ const zhHant = {
groupByKind: '依類型分組',
groupByKindShort: '分組',
pipelineTypeDescription:
'保留現有無程式碼訊息流水線,相容舊設定,只能處理訊息事件。',
'透過視覺化多階段流程控制訊息的前處理、AI、後處理、擴充與輸出;僅處理訊息事件。',
allEvents: '支援全部事件',
messageEventsOnly: '僅支援訊息事件',
basicInfo: '基本資訊',