fix(agent-runner): align plugin runner runtime boundaries

This commit is contained in:
huanghuoguoguo
2026-06-05 09:35:17 +08:00
committed by huanghuoguoguo
parent c0f5f30f57
commit e916c2e463
14 changed files with 210 additions and 117 deletions

View File

@@ -6,7 +6,6 @@ import {
PipelineConfigStage,
} from '@/app/infra/entities/pipeline';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import N8nAuthFormComponent from '@/app/home/components/dynamic-form/N8nAuthFormComponent';
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
import { systemInfo } from '@/app/infra/http';
import { Button } from '@/components/ui/button';
@@ -238,7 +237,7 @@ export default function PipelineFormComponent({
initializedStagesRef.current.clear();
});
}
}, []);
}, [form, isEditMode, pipelineId]);
useEffect(() => {
if (!isEditMode) {
@@ -311,7 +310,7 @@ export default function PipelineFormComponent({
});
}
// Called from DynamicFormComponent/N8nAuthFormComponent onSubmit callbacks.
// Called from DynamicFormComponent onSubmit callbacks.
// On the first emission for a stage (mount-time default filling), the
// snapshot is synchronously re-captured so that hasUnsavedChanges stays false.
// However, if the form is already dirty (the user has made real changes),
@@ -326,8 +325,7 @@ export default function PipelineFormComponent({
const isFirstEmission = !initializedStagesRef.current.has(stageKey);
const currentValues =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(form.getValues(formName) as Record<string, any>) || {};
(form.getValues(formName) as Record<string, unknown>) || {};
form.setValue(formName, {
...currentValues,
[stageName]: values,
@@ -350,12 +348,23 @@ export default function PipelineFormComponent({
stage: PipelineConfigStage,
formName: keyof FormValues,
) {
const forcedBoxTemplate =
systemInfo.limitation?.force_box_session_id_template || '';
const boxScopeForced = !!forcedBoxTemplate;
const isLocalAgentRunner =
stage.name === 'local-agent' ||
stage.name === 'plugin:langbot/local-agent/default';
const stageSystemContext = isLocalAgentRunner
? {
box_available: boxAvailable,
box_scope_editable: boxAvailable && !boxScopeForced,
}
: undefined;
// Special handling for AI config section
if (formName === 'ai') {
// Get the currently selected runner (use 'id' for new format, fallback to 'runner' for old)
const runnerConfig = (form.watch('ai.runner') as any) || {};
const currentRunner = runnerConfig.id || runnerConfig.runner;
const currentRunner = runnerConfig.id;
// If this is the runner selector stage, render it directly
if (stage.name === 'runner') {
@@ -373,9 +382,9 @@ export default function PipelineFormComponent({
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
{}
(form.watch(formName) as Record<string, unknown>)?.[
stage.name
] || {}
}
onSubmit={(values) => {
handleDynamicFormEmit(formName, stage.name, values);
@@ -391,42 +400,20 @@ export default function PipelineFormComponent({
return null;
}
// For old n8n built-in runner config, use N8nAuthFormComponent for form linkage
// New plugin:langbot/n8n-agent/default follows normal plugin runner path below
if (stage.name === 'n8n-service-api') {
return (
<Card key={stage.name}>
<CardHeader>
<CardTitle>{extractI18nObject(stage.label)}</CardTitle>
{stage.description && (
<CardDescription>
{extractI18nObject(stage.description)}
</CardDescription>
)}
</CardHeader>
<CardContent className="space-y-6">
<N8nAuthFormComponent
itemConfigList={stage.config}
initialValues={
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
{}
}
onSubmit={(values) => {
handleDynamicFormEmit(formName, stage.name, values);
}}
/>
</CardContent>
</Card>
);
}
// For plugin runner configs, store in ai.runner_config[runnerId]
const isPluginRunner =
currentRunner && currentRunner.startsWith('plugin:');
if (isPluginRunner) {
const runnerConfigs = (form.watch('ai.runner_config') as any) || {};
const stageInitialValues = runnerConfigs[stage.name] || {};
const effectiveInitialValues =
isLocalAgentRunner && boxScopeForced
? {
...stageInitialValues,
'box-session-id-template': forcedBoxTemplate,
}
: stageInitialValues;
return (
<Card key={stage.name}>
<CardHeader>
@@ -440,7 +427,7 @@ export default function PipelineFormComponent({
<CardContent className="space-y-6">
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={runnerConfigs[stage.name] || {}}
initialValues={effectiveInitialValues}
onSubmit={(values) => {
// Store in ai.runner_config[stage.name]
@@ -457,6 +444,7 @@ export default function PipelineFormComponent({
savedSnapshotRef.current = JSON.stringify(form.getValues());
}
}}
systemContext={stageSystemContext}
/>
</CardContent>
</Card>
@@ -469,23 +457,6 @@ export default function PipelineFormComponent({
// opt-in via ``disable_if`` + ``disabled_tooltip`` rather than every page
// hard-coding a banner. Field-level gating keeps unrelated fields
// untouched.
//
// ``box_scope_editable`` folds the two reasons the Sandbox Scope selector
// can be locked into a single flag the yaml ``disable_if`` consumes:
// 1. Box sandbox is unavailable, or
// 2. the deployment pins all pipelines to a fixed scope via
// ``system.limitation.force_box_session_id_template`` (SaaS).
const forcedBoxTemplate =
systemInfo.limitation?.force_box_session_id_template || '';
const boxScopeForced = !!forcedBoxTemplate;
const stageSystemContext =
stage.name === 'local-agent'
? {
box_available: boxAvailable,
box_scope_editable: boxAvailable && !boxScopeForced,
}
: undefined;
// When the deployment pins every pipeline to a fixed sandbox scope (SaaS
// ``force_box_session_id_template``), the Sandbox Scope selector is locked.
// The runtime already overrides the scope on every exec, but the stored
@@ -497,7 +468,7 @@ export default function PipelineFormComponent({
const stageInitialValues: Record<string, any> =
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
const effectiveInitialValues =
stage.name === 'local-agent' && boxScopeForced
isLocalAgentRunner && boxScopeForced
? {
...stageInitialValues,
'box-session-id-template': forcedBoxTemplate,

View File

@@ -86,7 +86,6 @@ export default function WizardPage() {
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
const [botName, setBotName] = useState('');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [botDescription, _setBotDescription] = useState('');
const [adapterConfig, setAdapterConfig] = useState<Record<string, unknown>>(
{},
@@ -202,9 +201,7 @@ export default function WizardPage() {
const runnerOptions = useMemo(() => {
if (!runnerStage) return [];
const runnerField =
runnerStage.config.find((c) => c.name === 'id') ??
runnerStage.config.find((c) => c.name === 'runner');
const runnerField = runnerStage.config.find((c) => c.name === 'id');
return runnerField?.options ?? [];
}, [runnerStage]);
@@ -1139,8 +1136,7 @@ function StepAIEngine({
})}
{/* Space promotion banner */}
{(selected === 'local-agent' ||
selected === 'plugin:langbot/local-agent/default') &&
{selected === 'plugin:langbot/local-agent/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">