Feat(browser control):Add new agent component 'browser' to control browser by AI (#14888)

### What problem does this PR solve?
This PR adds a new `Browser` operator to Agent workflows, enabling
prompt-driven browser automation in RAGFlow.Technically based
‘Browser-Use’

It includes:
- Backend browser component execution with tenant LLM integration
- Upload source support (file IDs, URLs, variables, CSV/JSON array)
- Downloaded file persistence to RAGFlow storage
- Frontend node/operator integration, form config, icon, and i18n
updates
- Unit tests for upload/download and ID parsing logic
- Dependency and Docker updates for browser-use runtime support

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
天海蒼灆
2026-05-21 14:09:06 +08:00
committed by Jin Hai
parent da112e3db0
commit 3e5b11a523
16 changed files with 4033 additions and 29 deletions

View File

@@ -123,6 +123,7 @@ export function AccordionOperators({
Operator.WenCai,
Operator.SearXNG,
Operator.DocGenerator,
Operator.Browser,
]}
isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition}

View File

@@ -698,6 +698,7 @@ export const RestrictedUpstreamMap = {
[Operator.LoopStart]: [Operator.Begin],
[Operator.ExitLoop]: [Operator.Begin],
[Operator.DocGenerator]: [Operator.Begin],
[Operator.Browser]: [Operator.Begin],
};
export const NodeMap = {
@@ -749,6 +750,7 @@ export const NodeMap = {
[Operator.ExitLoop]: 'exitLoopNode',
[Operator.ExcelProcessor]: 'ragNode',
[Operator.DocGenerator]: 'ragNode',
[Operator.Browser]: 'ragNode',
};
export enum BeginQueryType {
@@ -980,6 +982,21 @@ export const initialDocGeneratorValues = {
},
};
export const initialBrowserValues = {
...initialLlmBaseValues,
prompts: `{${AgentGlobals.SysQuery}}`,
max_steps: 30,
headless: true,
enable_default_extensions: false,
chromium_sandbox: false,
persist_session: true,
upload_sources: '',
outputs: {
content: { type: 'string', value: '' },
downloaded_files: { type: 'Array<Object>', value: [] },
},
};
export enum WebhookMethod {
Post = 'POST',
Get = 'GET',

View File

@@ -3,6 +3,7 @@ import AgentForm from '../form/agent-form';
import ArXivForm from '../form/arxiv-form';
import BeginForm from '../form/begin-form';
import BingForm from '../form/bing-form';
import BrowserForm from '../form/browser-use-form';
import CategorizeForm from '../form/categorize-form';
import CodeForm from '../form/code-form';
import CrawlerForm from '../form/crawler-form';
@@ -114,6 +115,9 @@ export const FormConfigMap = {
[Operator.DocGenerator]: {
component: DocGeneratorForm,
},
[Operator.Browser]: {
component: BrowserForm,
},
[Operator.Note]: {
component: () => <></>,
},

View File

@@ -0,0 +1,117 @@
import { NextLLMSelect } from '@/components/llm-select/next';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Form } from '@/components/ui/form';
import { NumberInput } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { initialBrowserValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { PromptEditor } from '../components/prompt-editor';
const FormSchema = z.object({
llm_id: z.string(),
prompts: z.string(),
max_steps: z.coerce.number().min(1),
headless: z.boolean(),
enable_default_extensions: z.boolean(),
chromium_sandbox: z.boolean(),
persist_session: z.boolean(),
upload_sources: z.string().optional(),
});
type FormSchemaType = z.infer<typeof FormSchema>;
function BrowserForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const defaultValues = useFormValues(initialBrowserValues, node);
const form = useForm<FormSchemaType>({
defaultValues,
resolver: zodResolver(FormSchema),
mode: 'onChange',
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<RAGFlowFormItem label={t('chat.model')} name="llm_id">
<NextLLMSelect></NextLLMSelect>
</RAGFlowFormItem>
<RAGFlowFormItem label={t('flow.userPrompt')} name="prompts">
<PromptEditor showToolbar={true}></PromptEditor>
</RAGFlowFormItem>
<RAGFlowFormItem label={t('flow.maxSteps')} name="max_steps">
{(field) => <NumberInput min={1} {...field}></NumberInput>}
</RAGFlowFormItem>
<RAGFlowFormItem label={t('flow.headless')} name="headless">
{(field) => (
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
)}
</RAGFlowFormItem>
<RAGFlowFormItem
label={t('flow.enableDefaultExtensions')}
tooltip={t('flow.enableDefaultExtensionsTip')}
name="enable_default_extensions"
>
{(field) => (
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
)}
</RAGFlowFormItem>
<RAGFlowFormItem
label={t('flow.chromiumSandbox')}
tooltip={t('flow.chromiumSandboxTip')}
name="chromium_sandbox"
>
{(field) => (
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
)}
</RAGFlowFormItem>
<RAGFlowFormItem
label={t('flow.persistSession')}
tooltip={t('flow.persistSessionTip')}
name="persist_session"
>
{(field) => (
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
)}
</RAGFlowFormItem>
<RAGFlowFormItem
label={t('flow.uploadSources')}
tooltip={t('flow.uploadSourcesTip')}
name="upload_sources"
>
{(field) => (
<PromptEditor
{...field}
showToolbar
multiLine={false}
placeholder="file_id,https://example.com/a.pdf,{node@files.0.id}"
></PromptEditor>
)}
</RAGFlowFormItem>
</FormWrapper>
</Form>
);
}
export default memo(BrowserForm);

View File

@@ -13,6 +13,7 @@ import {
initialArXivValues,
initialBeginValues,
initialBingValues,
initialBrowserValues,
initialCategorizeValues,
initialCodeValues,
initialCrawlerValues,
@@ -181,6 +182,7 @@ export const useInitializeOperatorParams = () => {
[Operator.LoopStart]: {},
[Operator.ExitLoop]: {},
[Operator.DocGenerator]: initialDocGeneratorValues,
[Operator.Browser]: { ...initialBrowserValues, llm_id: llmId },
[Operator.ExcelProcessor]: {},
};
}, [llmId]);

View File

@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils';
import {
FileCode,
FileText,
Globe,
HousePlus,
Infinity as InfinityIcon,
LogOut,
@@ -57,6 +58,7 @@ export const LucideIconMap = {
[Operator.Loop]: InfinityIcon,
[Operator.ExitLoop]: LogOut,
[Operator.DocGenerator]: FileText,
[Operator.Browser]: Globe,
};
const Empty = () => {

View File

@@ -132,7 +132,11 @@ const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({
<FormItem>
<FormLabel>{t('chunk.chunk')}</FormLabel>
<FormControl>
<Textarea {...field} autoSize={{ minRows: 4, maxRows: 10 }} resize="vertical" />
<Textarea
{...field}
autoSize={{ minRows: 4, maxRows: 10 }}
resize="vertical"
/>
</FormControl>
<FormMessage />
</FormItem>