feat(agent-runner): enforce 4.x host-owned execution

This commit is contained in:
huanghuoguoguo
2026-07-12 20:36:32 +08:00
parent 9aa71d54e3
commit d88d05e27c
170 changed files with 6952 additions and 5381 deletions
@@ -17,6 +17,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
const [agent, setAgent] = useState<Agent | null>(null);
const [loading, setLoading] = useState(!isCreateMode);
const [formDirty, setFormDirty] = useState(false);
const [formSaving, setFormSaving] = useState(false);
useEffect(() => {
if (isCreateMode) {
@@ -73,7 +74,11 @@ export default function AgentDetailContent({ id }: { id: string }) {
<div className="flex h-full flex-col">
<div className="flex items-center justify-between pb-4 shrink-0">
<h1 className="text-xl font-semibold">{t('agents.editAgent')}</h1>
<Button type="submit" form="agent-form" disabled={!formDirty}>
<Button
type="submit"
form="agent-form"
disabled={!formDirty || formSaving}
>
{t('common.save')}
</Button>
</div>
@@ -83,13 +88,13 @@ export default function AgentDetailContent({ id }: { id: string }) {
agentId={id}
onFinish={() => {
refreshPipelines();
setFormDirty(false);
}}
onDeleted={() => {
refreshPipelines();
navigate('/home/agents');
}}
onDirtyChange={setFormDirty}
onSavingChange={setFormSaving}
/>
</div>
</div>
@@ -62,6 +62,7 @@ interface AgentFormComponentProps {
onFinish: () => void;
onDeleted: () => void;
onDirtyChange?: (dirty: boolean) => void;
onSavingChange?: (saving: boolean) => void;
}
interface SectionItem {
@@ -75,6 +76,7 @@ export default function AgentFormComponent({
onFinish,
onDeleted,
onDirtyChange,
onSavingChange,
}: AgentFormComponentProps) {
const { t } = useTranslation();
const [activeSection, setActiveSection] =
@@ -86,6 +88,8 @@ export default function AgentFormComponent({
const [pluginStatusLoading, setPluginStatusLoading] = useState(true);
const [pluginStatusError, setPluginStatusError] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const isSavingRef = useRef(false);
const formSchema = z.object({
basic: z.object({
@@ -118,10 +122,10 @@ export default function AgentFormComponent({
const savedSnapshotRef = useRef('');
const initializedStagesRef = useRef<Set<string>>(new Set());
const watchedValues = form.watch();
const hasUnsavedChanges = useMemo(() => {
const hasUnsavedChanges = (() => {
if (!savedSnapshotRef.current) return false;
return JSON.stringify(watchedValues) !== savedSnapshotRef.current;
}, [watchedValues]);
})();
useEffect(() => {
onDirtyChange?.(hasUnsavedChanges);
@@ -388,6 +392,8 @@ export default function AgentFormComponent({
}
function handleSubmit(values: FormValues) {
if (isSavingRef.current) return;
const submittedSnapshot = JSON.stringify(values);
const runner = values.runner || {};
const agent: Partial<Agent> = {
name: values.basic.name,
@@ -404,16 +410,23 @@ export default function AgentFormComponent({
},
};
isSavingRef.current = true;
setIsSaving(true);
onSavingChange?.(true);
httpClient
.updateAgent(agentId, agent)
.then(() => {
const snapshotValues = form.getValues();
savedSnapshotRef.current = JSON.stringify(snapshotValues);
savedSnapshotRef.current = submittedSnapshot;
onFinish();
toast.success(t('agents.saveSuccess'));
})
.catch((err) => {
toast.error(t('agents.saveError') + err.msg);
})
.finally(() => {
isSavingRef.current = false;
setIsSaving(false);
onSavingChange?.(false);
});
}
@@ -574,6 +587,7 @@ export default function AgentFormComponent({
type="button"
variant="destructive"
size="sm"
disabled={isSaving}
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="size-4 mr-1.5" />
@@ -1561,7 +1561,11 @@ export default function EventBindingsEditor({
function toggleExpand(id: string) {
setExpandedIds((prev) => {
const s = new Set(prev);
s.has(id) ? s.delete(id) : s.add(id);
if (s.has(id)) {
s.delete(id);
} else {
s.add(id);
}
return s;
});
}
@@ -106,6 +106,25 @@ function getBoundPluginId(plugin: BoundPlugin) {
return `${plugin.author || ''}/${plugin.name}`;
}
function getGlobalExtensions(
plugins: AvailablePlugin[],
mcpServers: MCPServer[],
): PipelineExtensions {
return {
enable_all_plugins: true,
enable_all_mcp_servers: true,
enable_all_skills: true,
mcp_resource_agent_read_enabled: true,
bound_plugins: [],
available_plugins: plugins,
bound_mcp_servers: [],
available_mcp_servers: mcpServers,
bound_mcp_resources: [],
bound_skills: [],
available_skills: [],
};
}
function InfoTooltip({ label }: { label: string }) {
return (
<Tooltip>
@@ -348,19 +367,60 @@ export default function ToolResourceSelectors({
const [tempSelectedKBIds, setTempSelectedKBIds] = useState<string[]>([]);
useEffect(() => {
let cancelled = false;
setTools([]);
setKnowledgeBases([]);
setExtensions(null);
if (mode !== 'resources') {
backendClient.getTools(pipelineId).then((resp) => setTools(resp.tools));
backendClient
.getTools(pipelineId)
.then((resp) => {
if (!cancelled) setTools(resp.tools);
})
.catch(() => {
if (!cancelled) setTools([]);
});
}
if (mode !== 'tools') {
backendClient
.getKnowledgeBases()
.then((resp) => setKnowledgeBases(resp.bases));
.then((resp) => {
if (!cancelled) setKnowledgeBases(resp.bases);
})
.catch(() => {
if (!cancelled) setKnowledgeBases([]);
});
}
if (pipelineId) {
backendClient
.getPipelineExtensions(pipelineId)
.then((resp) => setExtensions(resp));
.then((resp) => {
if (!cancelled) setExtensions(resp);
})
.catch(() => {
if (!cancelled) setExtensions(null);
});
} else {
Promise.all([
backendClient
.getPlugins()
.catch(() => ({ plugins: [] as AvailablePlugin[] })),
backendClient
.getMCPServers()
.catch(() => ({ servers: [] as MCPServer[] })),
]).then(([pluginResp, mcpResp]) => {
if (!cancelled) {
setExtensions(
getGlobalExtensions(pluginResp.plugins, mcpResp.servers),
);
}
});
}
return () => {
cancelled = true;
};
}, [mode, pipelineId]);
const enableAllTools = value['enable-all-tools'] !== false;
@@ -480,11 +540,6 @@ export default function ToolResourceSelectors({
],
);
const availableToolNames = useMemo(
() => new Set(availableTools.map((tool) => tool.name)),
[availableTools],
);
const resourceServers = useMemo(() => {
return scopedMCPServers.filter(
(server) =>
@@ -513,6 +568,9 @@ export default function ToolResourceSelectors({
? availableMCPResourceKeys.has(getMCPResourceKey(server, resource.uri))
: false;
});
const unavailableSelectedMCPResources = selectedMCPResources.filter(
(resource) => !scopedSelectedMCPResources.includes(resource),
);
const selectedTools = selectedToolNames
.map((name: string) => availableTools.find((tool) => tool.name === name))
@@ -524,10 +582,10 @@ export default function ToolResourceSelectors({
const sourceLabels = useMemo<Record<string, string>>(
() => ({
builtin: t('pipelines.localAgent.builtinTools'),
plugin: t('pipelines.localAgent.pluginTools'),
mcp: t('pipelines.localAgent.mcpTools'),
skill: t('pipelines.localAgent.skillTools'),
builtin: t('pipelines.agentRunner.builtinTools'),
plugin: t('pipelines.agentRunner.pluginTools'),
mcp: t('pipelines.agentRunner.mcpTools'),
skill: t('pipelines.agentRunner.skillTools'),
}),
[t],
);
@@ -546,11 +604,7 @@ export default function ToolResourceSelectors({
};
const handleConfirmTools = () => {
onChange({
tools: tempSelectedToolNames.filter((name) =>
availableToolNames.has(name),
),
});
onChange({ tools: tempSelectedToolNames });
setToolsDialogOpen(false);
};
@@ -580,7 +634,9 @@ export default function ToolResourceSelectors({
: scopedSelectedMCPResources.filter(
(item) => !isSameMCPResource(item, server, resource.uri),
);
onChange({ 'mcp-resources': next });
onChange({
'mcp-resources': [...unavailableSelectedMCPResources, ...next],
});
};
const isMCPResourceSelected = (server: MCPServer, uri: string) =>
@@ -597,25 +653,27 @@ export default function ToolResourceSelectors({
<div>
<div className="flex items-center gap-1.5">
<h3 className="text-sm font-semibold">
{t('pipelines.localAgent.toolsTitle')}
{t('pipelines.agentRunner.toolsTitle')}
</h3>
<InfoTooltip
label={t('pipelines.localAgent.toolsScopeTooltip')}
/>
{pipelineId && (
<InfoTooltip
label={t('pipelines.agentRunner.toolsScopeTooltip')}
/>
)}
</div>
<p className="mt-1 text-sm text-muted-foreground">
{t('pipelines.localAgent.toolsDescription')}
{t('pipelines.agentRunner.toolsDescription')}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<Label
htmlFor="local-agent-enable-all-tools"
htmlFor="agent-runner-enable-all-tools"
className="cursor-pointer text-sm font-normal"
>
{t('pipelines.localAgent.enableAllTools')}
{t('pipelines.agentRunner.enableAllTools')}
</Label>
<Switch
id="local-agent-enable-all-tools"
id="agent-runner-enable-all-tools"
checked={enableAllTools}
onCheckedChange={handleToggleToolMode}
/>
@@ -625,13 +683,13 @@ export default function ToolResourceSelectors({
{enableAllTools ? (
<div className="flex h-24 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/30">
<p className="text-sm text-muted-foreground">
{t('pipelines.localAgent.allToolsEnabled')}
{t('pipelines.agentRunner.allToolsEnabled')}
</p>
</div>
) : selectedTools.length === 0 ? (
<div className="flex h-24 items-center justify-center rounded-lg border-2 border-dashed border-border">
<p className="text-sm text-muted-foreground">
{t('pipelines.localAgent.noToolsSelected')}
{t('pipelines.agentRunner.noToolsSelected')}
</p>
</div>
) : (
@@ -701,16 +759,12 @@ export default function ToolResourceSelectors({
className="w-full"
disabled={enableAllTools}
onClick={() => {
setTempSelectedToolNames(
selectedToolNames.filter((name: string) =>
availableToolNames.has(name),
),
);
setTempSelectedToolNames(selectedToolNames);
setToolsDialogOpen(true);
}}
>
<Plus className="mr-2 h-4 w-4" />
{t('pipelines.localAgent.editTools')}
{t('pipelines.agentRunner.editTools')}
</Button>
</div>
)}
@@ -719,10 +773,10 @@ export default function ToolResourceSelectors({
<div className="space-y-4 rounded-lg border p-4">
<div>
<h3 className="text-sm font-semibold">
{t('pipelines.localAgent.resourcesTitle')}
{t('pipelines.agentRunner.resourcesTitle')}
</h3>
<p className="mt-1 text-sm text-muted-foreground">
{t('pipelines.localAgent.resourcesDescription')}
{t('pipelines.agentRunner.resourcesDescription')}
</p>
</div>
@@ -731,7 +785,7 @@ export default function ToolResourceSelectors({
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">
{t('pipelines.localAgent.knowledgeBases')}
{t('pipelines.agentRunner.knowledgeBases')}
</span>
</div>
<Button
@@ -801,24 +855,26 @@ export default function ToolResourceSelectors({
<div className="flex items-center gap-2">
<Server className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">
{t('pipelines.localAgent.mcpResources')}
{t('pipelines.agentRunner.mcpResources')}
</span>
<InfoTooltip
label={t('pipelines.localAgent.mcpResourcesScopeTooltip')}
/>
{pipelineId && (
<InfoTooltip
label={t('pipelines.agentRunner.mcpResourcesScopeTooltip')}
/>
)}
</div>
<div className="flex items-center gap-2">
<Label
htmlFor="local-agent-mcp-resource-read"
htmlFor="agent-runner-mcp-resource-read"
className="cursor-pointer text-sm font-normal"
>
{t('pipelines.localAgent.enableMCPResourceRead')}
{t('pipelines.agentRunner.enableMCPResourceRead')}
</Label>
<InfoTooltip
label={t('pipelines.localAgent.mcpResourceReadTooltip')}
label={t('pipelines.agentRunner.mcpResourceReadTooltip')}
/>
<Switch
id="local-agent-mcp-resource-read"
id="agent-runner-mcp-resource-read"
checked={mcpResourceReadEnabled}
onCheckedChange={(checked) =>
onChange({ 'mcp-resource-agent-read-enabled': checked })
@@ -830,7 +886,7 @@ export default function ToolResourceSelectors({
{resourceServers.length === 0 ? (
<div className="flex h-20 items-center justify-center rounded-lg border-2 border-dashed border-border">
<p className="text-sm text-muted-foreground">
{t('pipelines.localAgent.noMCPResourcesAvailable')}
{t('pipelines.agentRunner.noMCPResourcesAvailable')}
</p>
</div>
) : (
@@ -900,7 +956,9 @@ export default function ToolResourceSelectors({
<Dialog open={toolsDialogOpen} onOpenChange={setToolsDialogOpen}>
<DialogContent className="flex max-h-[80vh] max-w-2xl flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>{t('pipelines.localAgent.selectTools')}</DialogTitle>
<DialogTitle>
{t('pipelines.agentRunner.selectTools')}
</DialogTitle>
</DialogHeader>
<div className="flex-1 space-y-5 overflow-y-auto pr-2">
{availableToolGroups.map((sourceGroup) => {
@@ -910,15 +968,17 @@ export default function ToolResourceSelectors({
<span className="text-sm font-semibold">
{sourceGroup.label}
</span>
{sourceGroup.key === 'mcp' && (
{pipelineId && sourceGroup.key === 'mcp' && (
<InfoTooltip
label={t('pipelines.localAgent.mcpToolsScopeTooltip')}
label={t(
'pipelines.agentRunner.mcpToolsScopeTooltip',
)}
/>
)}
{sourceGroup.key === 'skill' && (
<InfoTooltip
label={t(
'pipelines.localAgent.skillToolsScopeTooltip',
'pipelines.agentRunner.skillToolsScopeTooltip',
)}
/>
)}
@@ -987,7 +1047,7 @@ export default function ToolResourceSelectors({
{availableToolGroups.length === 0 && (
<div className="flex h-24 items-center justify-center rounded-lg border-2 border-dashed border-border">
<p className="text-sm text-muted-foreground">
{t('pipelines.localAgent.noToolsSelected')}
{t('pipelines.agentRunner.noToolsSelected')}
</p>
</div>
)}
@@ -1012,7 +1072,7 @@ export default function ToolResourceSelectors({
<DialogContent className="flex max-h-[80vh] max-w-2xl flex-col overflow-hidden">
<DialogHeader>
<DialogTitle>
{t('pipelines.localAgent.selectKnowledgeBases')}
{t('pipelines.agentRunner.selectKnowledgeBases')}
</DialogTitle>
</DialogHeader>
<div className="flex-1 space-y-2 overflow-y-auto pr-2">
@@ -9,10 +9,6 @@ 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(() => {
@@ -205,49 +201,45 @@ export function MessageDetailsCard({ details }: MessageDetailsCardProps) {
</div>
)}
{/* Query Variables Section - Only show for non-local-agent runners */}
{queryVariables &&
Object.keys(queryVariables).length > 0 &&
!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" />
{t('monitoring.queryVariables.title')}
</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
{Object.entries(queryVariables).map(([key, value]) => (
<div key={key} className="bg-background rounded p-2">
<div className="text-muted-foreground">{key}</div>
<div
className="font-medium text-foreground truncate"
title={
typeof value === 'string' ? value : JSON.stringify(value)
}
>
{value === null || value === undefined ? (
<span className="text-muted-foreground italic">null</span>
) : typeof value === 'string' ? (
value || (
<span className="text-muted-foreground italic">
empty
</span>
)
) : (
JSON.stringify(value)
)}
</div>
{/* Query Variables Section */}
{queryVariables && Object.keys(queryVariables).length > 0 && (
<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" />
{t('monitoring.queryVariables.title')}
</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
{Object.entries(queryVariables).map(([key, value]) => (
<div key={key} className="bg-background rounded p-2">
<div className="text-muted-foreground">{key}</div>
<div
className="font-medium text-foreground truncate"
title={
typeof value === 'string' ? value : JSON.stringify(value)
}
>
{value === null || value === undefined ? (
<span className="text-muted-foreground italic">null</span>
) : typeof value === 'string' ? (
value || (
<span className="text-muted-foreground italic">
empty
</span>
)
) : (
JSON.stringify(value)
)}
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
)}
{/* No data message */}
{(!details.llmCalls || details.llmCalls.length === 0) &&
(!details.errors || details.errors.length === 0) &&
(isLocalAgent ||
!queryVariables ||
Object.keys(queryVariables).length === 0) && (
(!queryVariables || Object.keys(queryVariables).length === 0) && (
<div className="text-sm text-muted-foreground text-center py-4">
{t('monitoring.messageDetails.noData')}
</div>
@@ -35,6 +35,7 @@ export default function PipelineDetailContent({
const [activeTab, setActiveTab] = useState('config');
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
const [formDirty, setFormDirty] = useState(false);
const [formSaving, setFormSaving] = useState(false);
function handleFinish() {
refreshPipelines();
@@ -53,7 +54,7 @@ export default function PipelineDetailContent({
<h1 className="text-xl font-semibold">
{t('pipelines.createPipeline')}
</h1>
<Button type="submit" form="pipeline-form">
<Button type="submit" form="pipeline-form" disabled={formSaving}>
{t('common.submit')}
</Button>
</div>
@@ -68,6 +69,7 @@ export default function PipelineDetailContent({
onFinish={handleFinish}
onNewPipelineCreated={handleNewPipelineCreated}
onDeletePipeline={() => {}}
onSavingChange={setFormSaving}
/>
</div>
</div>
@@ -89,7 +91,7 @@ export default function PipelineDetailContent({
<Button
type="submit"
form="pipeline-form"
disabled={!formDirty}
disabled={!formDirty || formSaving}
className={activeTab !== 'config' ? 'invisible' : ''}
>
{t('common.save')}
@@ -140,6 +142,7 @@ export default function PipelineDetailContent({
onDeletePipeline={handleDeletePipeline}
onCancel={() => navigate(routeBase)}
onDirtyChange={setFormDirty}
onSavingChange={setFormSaving}
/>
</TabsContent>
@@ -59,6 +59,7 @@ export default function PipelineFormComponent({
onDeletePipeline,
onCancel,
onDirtyChange,
onSavingChange,
}: {
pipelineId?: string;
isEditMode: boolean;
@@ -69,11 +70,14 @@ export default function PipelineFormComponent({
onDeletePipeline: () => void;
onCancel?: () => void;
onDirtyChange?: (dirty: boolean) => void;
onSavingChange?: (saving: boolean) => void;
}) {
const { t } = useTranslation();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showCopyConfirm, setShowCopyConfirm] = useState(false);
const [isDefaultPipeline, setIsDefaultPipeline] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState(false);
const isSavingRef = useRef(false);
const formSchema = isEditMode
? z.object({
@@ -176,16 +180,20 @@ export default function PipelineFormComponent({
output: {},
},
});
const dynamicFormSystemContext = useMemo(
() => ({ pipeline_id: pipelineId }),
[pipelineId],
);
// Track unsaved changes by comparing current form values against a saved snapshot
const savedSnapshotRef = useRef<string>('');
// Track which dynamic form stages have completed their initial mount emission.
const initializedStagesRef = useRef<Set<string>>(new Set());
const watchedValues = form.watch();
const hasUnsavedChanges = useMemo(() => {
const hasUnsavedChanges = (() => {
if (!isEditMode || !savedSnapshotRef.current) return false;
return JSON.stringify(watchedValues) !== savedSnapshotRef.current;
}, [isEditMode, watchedValues]);
})();
// Keep a ref so that non-reactive callbacks (handleDynamicFormEmit) can
// read the latest dirty state without stale closures.
const hasUnsavedChangesRef = useRef(hasUnsavedChanges);
@@ -257,12 +265,16 @@ export default function PipelineFormComponent({
}
function handleCreate(values: FormValues) {
if (isSavingRef.current) return;
const pipeline: Pipeline = {
config: {},
description: values.basic.description ?? '',
name: values.basic.name,
emoji: values.basic.emoji,
};
isSavingRef.current = true;
setIsSaving(true);
onSavingChange?.(true);
httpClient
.createPipeline(pipeline)
.then((resp) => {
@@ -272,10 +284,17 @@ export default function PipelineFormComponent({
})
.catch((err) => {
toast.error(t('pipelines.createError') + err.msg);
})
.finally(() => {
isSavingRef.current = false;
setIsSaving(false);
onSavingChange?.(false);
});
}
function handleModify(values: FormValues) {
if (isSavingRef.current) return;
const submittedSnapshot = JSON.stringify(values);
const realConfig = {
ai: values.ai,
trigger: values.trigger,
@@ -295,15 +314,23 @@ export default function PipelineFormComponent({
// uuid: pipelineId || '',
// is_default: false,
};
isSavingRef.current = true;
setIsSaving(true);
onSavingChange?.(true);
httpClient
.updatePipeline(pipelineId || '', pipeline)
.then(() => {
savedSnapshotRef.current = JSON.stringify(form.getValues());
savedSnapshotRef.current = submittedSnapshot;
onFinish();
toast.success(t('pipelines.saveSuccess'));
})
.catch((err) => {
toast.error(t('pipelines.saveError') + err.msg);
})
.finally(() => {
isSavingRef.current = false;
setIsSaving(false);
onSavingChange?.(false);
});
}
@@ -390,6 +417,7 @@ export default function PipelineFormComponent({
stage.name
] || {}
}
systemContext={dynamicFormSystemContext}
onSubmit={(values) => {
handleDynamicFormEmit(formName, stage.name, values);
}}
@@ -424,6 +452,7 @@ export default function PipelineFormComponent({
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={stageInitialValues}
systemContext={dynamicFormSystemContext}
onSubmit={(values) => {
handleRunnerConfigEmit(stage.name, values);
}}
@@ -451,6 +480,7 @@ export default function PipelineFormComponent({
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={stageInitialValues}
systemContext={dynamicFormSystemContext}
onSubmit={(values) => {
handleDynamicFormEmit(formName, stage.name, values);
}}
@@ -746,7 +776,7 @@ export default function PipelineFormComponent({
</Button>
)}
<Button type="submit" form="pipeline-form">
<Button type="submit" form="pipeline-form" disabled={isSaving}>
{isEditMode ? t('common.save') : t('common.submit')}
</Button>
</div>
+4 -5
View File
@@ -437,10 +437,6 @@ export interface SystemLimitation {
max_bots: number;
max_pipelines: number;
max_extensions: number;
/** When non-empty, every pipeline is forced to this Box sandbox-scope
* template (e.g. ``{global}``) and the per-pipeline "Sandbox Scope"
* selector is locked. Used by SaaS deployments. Empty = no restriction. */
force_box_session_id_template?: string;
}
export interface WizardProgress {
@@ -800,7 +796,10 @@ export interface ApiRespTools {
}
export interface ApiRespToolDetail {
tool: PluginTool;
tool: Omit<PluginTool, 'source' | 'source_id'> & {
source: NonNullable<PluginTool['source']>;
source_id: string | null;
};
}
// Skills
+8 -2
View File
@@ -982,8 +982,14 @@ export class BackendClient extends BaseHttpClient {
);
}
public getToolDetail(toolName: string): Promise<ApiRespToolDetail> {
return this.get(`/api/v1/tools/${toolName}`);
public getToolDetail(
toolName: string,
pipelineId?: string,
): Promise<ApiRespToolDetail> {
return this.get(
`/api/v1/tools/${encodeURIComponent(toolName)}`,
pipelineId ? { pipeline_uuid: pipelineId } : undefined,
);
}
public getMCPServer(serverName: string): Promise<ApiRespMCPServer> {
-48
View File
@@ -25,7 +25,6 @@ import {
import { httpClient } from '@/app/infra/http/HttpClient';
import {
userInfo,
systemInfo,
initializeUserInfo,
initializeSystemInfo,
@@ -888,24 +887,6 @@ export default function WizardPage() {
saveProgress,
]);
// ---- Space auth redirect ----
const handleSpaceAuth = useCallback(async () => {
try {
const callbackUrl = `${window.location.origin}/auth/space/callback`;
const resp = await httpClient.getSpaceAuthorizeUrl(callbackUrl);
window.location.href = resp.authorize_url;
} catch (err) {
console.error('Failed to get space authorize URL', err);
toast.error(t('wizard.spaceAuthError'));
}
}, [t]);
// ---- Check if local account ----
// Re-evaluated after remote data fetch (when userInfo is populated)
const isLocalAccount =
!isLoading && (!userInfo || userInfo.account_type === 'local');
// ---- Skip handler ----
const [showSkipConfirm, setShowSkipConfirm] = useState(false);
const [isSkipping, setIsSkipping] = useState(false);
@@ -1071,8 +1052,6 @@ export default function WizardPage() {
onSelect={handleSelectRunner}
onInstall={handleInstallRunner}
onRetryCatalog={loadRunnerCatalog}
isLocalAccount={isLocalAccount}
onSpaceAuth={handleSpaceAuth}
runnerConfigItems={selectedRunnerConfigItems}
runnerConfigValues={runnerConfig}
onRunnerConfigChange={setRunnerConfig}
@@ -1587,8 +1566,6 @@ function StepAIEngine({
onSelect,
onInstall,
onRetryCatalog,
isLocalAccount,
onSpaceAuth,
runnerConfigItems,
runnerConfigValues,
onRunnerConfigChange,
@@ -1604,8 +1581,6 @@ function StepAIEngine({
onSelect: (name: string) => void;
onInstall: (plugin: PluginV4) => void;
onRetryCatalog: () => void;
isLocalAccount: boolean;
onSpaceAuth: () => void;
runnerConfigItems: IDynamicFormItemSchema[];
runnerConfigValues: Record<string, unknown>;
onRunnerConfigChange: (v: Record<string, unknown>) => void;
@@ -1878,29 +1853,6 @@ function StepAIEngine({
</Card>
);
})}
{/* Space promotion banner */}
{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">
<div className="rounded-[calc(0.5rem-2px)] bg-background p-3 flex flex-col items-center gap-2 text-center">
<Sparkles className="w-6 h-6 text-purple-500 shrink-0" />
<p className="text-xs font-medium">
{t('wizard.spaceBanner.message')}
</p>
<Button
variant="outline"
size="sm"
onClick={onSpaceAuth}
className="w-full"
>
{t('wizard.spaceBanner.action')}
</Button>
</div>
</div>
</div>
)}
</div>
</div>
+4 -9
View File
@@ -1220,14 +1220,14 @@ const enUS = {
selectSkills: 'Select Skills',
noSkillsAvailable: 'No skills available',
mcpServersScopeTooltip:
'This only controls which MCP servers are bound to the pipeline. Choose exact MCP tools and resources in AI Feature > Local Agent.',
'This only controls which MCP servers are bound to the pipeline. Choose exact MCP tools and resources in AI Feature > Agent Runner.',
enableAllMCPServersTooltip:
'When enabled, all configured and enabled MCP servers become candidates for MCP tools and resources in AI Feature.',
},
localAgent: {
agentRunner: {
toolsTitle: 'Tools',
toolsDescription:
'Select plugin, MCP, skill, and built-in tools available to this Local Agent.',
'Select plugin, MCP, skill, and built-in tools available to this Agent Runner.',
toolsScopeTooltip:
'MCP tools only come from MCP servers bound in Extensions. Bind another MCP server there to make its tools selectable here.',
enableAllTools: 'Enable all tools',
@@ -1245,7 +1245,7 @@ const enUS = {
selectTools: 'Select tools',
resourcesTitle: 'Resources',
resourcesDescription:
'Select MCP resources and knowledge bases available to this Local Agent.',
'Select MCP resources and knowledge bases available to this Agent Runner.',
knowledgeBases: 'Knowledge bases',
mcpResources: 'MCP resources',
mcpResourcesScopeTooltip:
@@ -2011,11 +2011,6 @@ const enUS = {
registrationTimeout:
'The extension installed, but its Runner did not register. Check the plugin runtime and retry.',
},
spaceBanner: {
message:
'Connect to LangBot Space for free trial model credits and zero-config instant setup!',
action: 'Authorize with Space',
},
config: {
botInfo: 'Bot Information',
botNamePlaceholder: 'Enter bot name',
+4 -9
View File
@@ -1047,14 +1047,14 @@ const esES = {
selectSkills: 'Seleccionar skills',
noSkillsAvailable: 'No hay skills disponibles',
mcpServersScopeTooltip:
'Aquí solo se controla qué servidores MCP se vinculan al Pipeline. Las herramientas y recursos MCP concretos se eligen en AI Feature > Local Agent.',
'Aquí solo se controla qué servidores MCP se vinculan al Pipeline. Las herramientas y recursos MCP concretos se eligen en AI Feature > Agent Runner.',
enableAllMCPServersTooltip:
'Al activarlo, todos los servidores MCP configurados y habilitados serán candidatos para herramientas y recursos MCP en AI Feature.',
},
localAgent: {
agentRunner: {
toolsTitle: 'Herramientas',
toolsDescription:
'Selecciona las herramientas de plugins, MCP e integradas disponibles para este Local Agent.',
'Selecciona las herramientas de plugins, MCP e integradas disponibles para este Agent Runner.',
toolsScopeTooltip:
'Las herramientas MCP solo provienen de servidores MCP vinculados en Extensiones. Vincula allí otro servidor para poder seleccionarlo aquí.',
enableAllTools: 'Activar todas las herramientas',
@@ -1072,7 +1072,7 @@ const esES = {
selectTools: 'Seleccionar herramientas',
resourcesTitle: 'Recursos',
resourcesDescription:
'Selecciona los recursos MCP y bases de conocimiento disponibles para este Local Agent.',
'Selecciona los recursos MCP y bases de conocimiento disponibles para este Agent Runner.',
knowledgeBases: 'Bases de conocimiento',
mcpResources: 'Recursos MCP',
mcpResourcesScopeTooltip:
@@ -1717,11 +1717,6 @@ const esES = {
description:
'Elige el motor de IA que impulsará la inteligencia de tu Bot.',
},
spaceBanner: {
message:
'¡Conéctate a LangBot Space para obtener créditos de prueba gratuitos y configuración instantánea sin esfuerzo!',
action: 'Autorizar con Space',
},
config: {
botInfo: 'Información del Bot',
botNamePlaceholder: 'Introduce el nombre del Bot',
+4 -9
View File
@@ -1232,14 +1232,14 @@ const jaJP = {
selectSkills: 'スキルを選択',
noSkillsAvailable: '利用可能なスキルがありません',
mcpServersScopeTooltip:
'ここでは、このパイプラインに紐付ける MCP サーバーだけを管理します。個別の MCP ツールとリソースは AI 機能の Local Agent で選択します。',
'ここでは、このパイプラインに紐付ける MCP サーバーだけを管理します。個別の MCP ツールとリソースは AI 機能の Agent Runner で選択します。',
enableAllMCPServersTooltip:
'有効にすると、設定済みで有効なすべての MCP サーバーが AI 機能の MCP ツールとリソース候補になります。',
},
localAgent: {
agentRunner: {
toolsTitle: 'ツール',
toolsDescription:
'この Local Agent が使用できるプラグイン、MCP、組み込みツールを選択します。',
'この Agent Runner が使用できるプラグイン、MCP、組み込みツールを選択します。',
toolsScopeTooltip:
'MCP ツールは拡張機能で紐付けられた MCP サーバーからのみ表示されます。追加するには先に拡張機能でサーバーを紐付けてください。',
enableAllTools: 'すべてのツールを有効化',
@@ -1257,7 +1257,7 @@ const jaJP = {
selectTools: 'ツールを選択',
resourcesTitle: 'リソース',
resourcesDescription:
'この Local Agent が読み取れる MCP リソースとナレッジベースを選択します。',
'この Agent Runner が読み取れる MCP リソースとナレッジベースを選択します。',
knowledgeBases: 'ナレッジベース',
mcpResources: 'MCP リソース',
mcpResourcesScopeTooltip:
@@ -1934,11 +1934,6 @@ const jaJP = {
registrationTimeout:
'拡張機能はインストールされましたが、Runner が登録されませんでした。プラグインランタイムを確認して再試行してください。',
},
spaceBanner: {
message:
'LangBot Spaceに接続して、無料トライアルモデルクレジットとゼロ設定の即時セットアップを入手!',
action: 'Spaceで認証',
},
config: {
botInfo: 'ボット情報',
botNamePlaceholder: 'ボット名を入力',
+4 -9
View File
@@ -1038,14 +1038,14 @@ const ruRU = {
selectSkills: 'Выбрать навыки',
noSkillsAvailable: 'Нет доступных навыков',
mcpServersScopeTooltip:
'Здесь задаётся только привязка MCP-серверов к конвейеру. Конкретные MCP-инструменты и ресурсы выбираются в AI Feature > Local Agent.',
'Здесь задаётся только привязка MCP-серверов к конвейеру. Конкретные MCP-инструменты и ресурсы выбираются в AI Feature > Agent Runner.',
enableAllMCPServersTooltip:
'Если включено, все настроенные и включённые MCP-серверы станут кандидатами для инструментов и ресурсов MCP в AI Feature.',
},
localAgent: {
agentRunner: {
toolsTitle: 'Инструменты',
toolsDescription:
'Выберите инструменты плагинов, MCP и встроенные инструменты для этого Local Agent.',
'Выберите инструменты плагинов, MCP и встроенные инструменты для этого Agent Runner.',
toolsScopeTooltip:
'MCP-инструменты берутся только из MCP-серверов, привязанных в Расширениях. Чтобы добавить источник, сначала привяжите там сервер.',
enableAllTools: 'Включить все инструменты',
@@ -1063,7 +1063,7 @@ const ruRU = {
selectTools: 'Выбрать инструменты',
resourcesTitle: 'Ресурсы',
resourcesDescription:
'Выберите MCP-ресурсы и базы знаний для этого Local Agent.',
'Выберите MCP-ресурсы и базы знаний для этого Agent Runner.',
knowledgeBases: 'Базы знаний',
mcpResources: 'MCP-ресурсы',
mcpResourcesScopeTooltip:
@@ -1687,11 +1687,6 @@ const ruRU = {
description:
'Выберите ИИ-движок, который будет управлять интеллектом вашего бота.',
},
spaceBanner: {
message:
'Подключитесь к LangBot Space для бесплатных пробных кредитов и мгновенной настройки!',
action: 'Авторизация через Space',
},
config: {
botInfo: 'Информация о боте',
botNamePlaceholder: 'Введите имя бота',
+4 -9
View File
@@ -1013,14 +1013,14 @@ const thTH = {
selectSkills: 'เลือกสกิล',
noSkillsAvailable: 'ไม่มีสกิลที่พร้อมใช้งาน',
mcpServersScopeTooltip:
'ส่วนนี้ใช้ควบคุมว่า Pipeline ผูกกับเซิร์ฟเวอร์ MCP ใดเท่านั้น ส่วนเครื่องมือและทรัพยากร MCP รายตัวให้เลือกใน AI Feature > Local Agent',
'ส่วนนี้ใช้ควบคุมว่า Pipeline ผูกกับเซิร์ฟเวอร์ MCP ใดเท่านั้น ส่วนเครื่องมือและทรัพยากร MCP รายตัวให้เลือกใน AI Feature > Agent Runner',
enableAllMCPServersTooltip:
'เมื่อเปิดใช้ เซิร์ฟเวอร์ MCP ที่ตั้งค่าและเปิดใช้งานทั้งหมดจะเป็นตัวเลือกสำหรับเครื่องมือและทรัพยากร MCP ใน AI Feature',
},
localAgent: {
agentRunner: {
toolsTitle: 'เครื่องมือ',
toolsDescription:
'เลือกเครื่องมือจากปลั๊กอิน MCP และเครื่องมือในตัวสำหรับ Local Agent นี้',
'เลือกเครื่องมือจากปลั๊กอิน MCP และเครื่องมือในตัวสำหรับ Agent Runner นี้',
toolsScopeTooltip:
'เครื่องมือ MCP จะแสดงจากเซิร์ฟเวอร์ MCP ที่ผูกไว้ในส่วนขยายเท่านั้น หากต้องการเพิ่มแหล่งเครื่องมือ ให้ไปผูกเซิร์ฟเวอร์ที่นั่นก่อน',
enableAllTools: 'เปิดใช้เครื่องมือทั้งหมด',
@@ -1038,7 +1038,7 @@ const thTH = {
selectTools: 'เลือกเครื่องมือ',
resourcesTitle: 'ทรัพยากร',
resourcesDescription:
'เลือกทรัพยากร MCP และคลังความรู้สำหรับ Local Agent นี้',
'เลือกทรัพยากร MCP และคลังความรู้สำหรับ Agent Runner นี้',
knowledgeBases: 'คลังความรู้',
mcpResources: 'ทรัพยากร MCP',
mcpResourcesScopeTooltip:
@@ -1652,11 +1652,6 @@ const thTH = {
title: 'เลือกเครื่องมือ AI',
description: 'เลือกเครื่องมือ AI ที่จะขับเคลื่อนความฉลาดของ Bot',
},
spaceBanner: {
message:
'เชื่อมต่อกับ LangBot Space เพื่อรับเครดิตทดลองใช้โมเดลฟรีและตั้งค่าทันทีโดยไม่ต้องกำหนดค่า!',
action: 'ยืนยันสิทธิ์กับ Space',
},
config: {
botInfo: 'ข้อมูล Bot',
botNamePlaceholder: 'กรอกชื่อ Bot',
+4 -9
View File
@@ -1030,14 +1030,14 @@ const viVN = {
selectSkills: 'Chọn kỹ năng',
noSkillsAvailable: 'Không có kỹ năng khả dụng',
mcpServersScopeTooltip:
'Tại đây chỉ kiểm soát máy chủ MCP được liên kết với Pipeline. Công cụ và tài nguyên MCP cụ thể được chọn trong AI Feature > Local Agent.',
'Tại đây chỉ kiểm soát máy chủ MCP được liên kết với Pipeline. Công cụ và tài nguyên MCP cụ thể được chọn trong AI Feature > Agent Runner.',
enableAllMCPServersTooltip:
'Khi bật, mọi máy chủ MCP đã cấu hình và bật sẽ trở thành ứng viên cho công cụ và tài nguyên MCP trong AI Feature.',
},
localAgent: {
agentRunner: {
toolsTitle: 'Công cụ',
toolsDescription:
'Chọn công cụ plugin, MCP và công cụ tích hợp sẵn cho Local Agent này.',
'Chọn công cụ plugin, MCP và công cụ tích hợp sẵn cho Agent Runner này.',
toolsScopeTooltip:
'Công cụ MCP chỉ đến từ máy chủ MCP đã liên kết trong Tiện ích mở rộng. Hãy liên kết máy chủ tại đó trước nếu muốn chọn thêm tại đây.',
enableAllTools: 'Bật tất cả công cụ',
@@ -1055,7 +1055,7 @@ const viVN = {
selectTools: 'Chọn công cụ',
resourcesTitle: 'Tài nguyên',
resourcesDescription:
'Chọn tài nguyên MCP và kho tri thức cho Local Agent này.',
'Chọn tài nguyên MCP và kho tri thức cho Agent Runner này.',
knowledgeBases: 'Kho tri thức',
mcpResources: 'Tài nguyên MCP',
mcpResourcesScopeTooltip:
@@ -1678,11 +1678,6 @@ const viVN = {
title: 'Chọn công cụ AI',
description: 'Chọn công cụ AI sẽ cung cấp trí tuệ cho Bot của bạn.',
},
spaceBanner: {
message:
'Kết nối với LangBot Space để nhận tín dụng dùng thử mô hình miễn phí và thiết lập tức thì không cần cấu hình!',
action: 'Ủy quyền với Space',
},
config: {
botInfo: 'Thông tin Bot',
botNamePlaceholder: 'Nhập tên Bot',
+4 -7
View File
@@ -1172,10 +1172,10 @@ const zhHans = {
enableAllMCPServersTooltip:
'开启后,所有已配置且启用的 MCP 服务器都会进入 AI 能力里的 MCP 工具和资源候选范围。',
},
localAgent: {
agentRunner: {
toolsTitle: '工具',
toolsDescription:
'选择此内置 Agent 可以调用的插件、MCP、技能和内置工具。',
'选择当前 AgentRunner 可以调用的插件、MCP、技能和内置工具。',
toolsScopeTooltip:
'MCP 工具只会从扩展集成中已绑定的 MCP 服务器里出现;如需增加 MCP 工具来源,请先到扩展集成绑定对应服务器。',
enableAllTools: '启用所有工具',
@@ -1192,7 +1192,8 @@ const zhHans = {
'技能工具会在 LangBot 技能服务和 Box 沙箱后端可用时出现,用于让 Agent 激活或注册技能。',
selectTools: '选择工具',
resourcesTitle: '资源',
resourcesDescription: '选择此内置 Agent 可以读取的 MCP 资源和知识库。',
resourcesDescription:
'选择当前 AgentRunner 可以读取的 MCP 资源和知识库。',
knowledgeBases: '知识库',
mcpResources: 'MCP 资源',
mcpResourcesScopeTooltip:
@@ -1920,10 +1921,6 @@ const zhHans = {
registrationTimeout:
'扩展已安装,但运行器未完成注册。请检查插件运行时后重试。',
},
spaceBanner: {
message: '接入 LangBot Space,获取免费试用模型额度,零配置极速开箱!',
action: '前往授权登录',
},
config: {
botInfo: '机器人信息',
botNamePlaceholder: '请输入机器人名称',
+1 -5
View File
@@ -986,7 +986,7 @@ const zhHant = {
enableAllMCPServersTooltip:
'啟用後,所有已配置且啟用的 MCP 伺服器都會進入 AI 能力中的 MCP 工具和資源候選範圍。',
},
localAgent: {
agentRunner: {
toolsTitle: '工具',
toolsDescription: '選擇此內建 Agent 可以調用的插件、MCP 和內建工具。',
toolsScopeTooltip:
@@ -1603,10 +1603,6 @@ const zhHant = {
title: '選擇 AI 引擎',
description: '選擇驅動機器人智慧的 AI 引擎。',
},
spaceBanner: {
message: '接入 LangBot Space,取得免費試用模型額度,零配置極速開箱!',
action: '前往授權登入',
},
config: {
botInfo: '機器人資訊',
botNamePlaceholder: '請輸入機器人名稱',