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

@@ -109,6 +109,7 @@ export enum Operator {
StringTransform = 'StringTransform',
SearXNG = 'SearXNG',
DocGenerator = 'DocGenerator',
Browser = 'Browser',
Placeholder = 'Placeholder',
DataOperations = 'DataOperations',
ListOperations = 'ListOperations',

View File

@@ -1836,6 +1836,20 @@ Best for: Documents with flowing, contextually connected content — such as boo
maxRounds: 'Max reflection rounds',
delayAfterError: 'Delay after error',
maxRetries: 'Max retry rounds',
maxSteps: 'Max steps',
headless: 'Headless',
enableDefaultExtensions: 'Enable default extensions',
enableDefaultExtensionsTip:
'Enable browser-use default extensions (uBlock, cookie handling, ClearURLs). Disable this to avoid runtime extension downloads.',
chromiumSandbox: 'Chromium sandbox',
chromiumSandboxTip:
'Whether to enable Chromium sandbox. In Docker root environments this is usually disabled; on regular hosts it is recommended to enable.',
persistSession: 'Persist session',
persistSessionTip:
'When enabled, this Browser node reuses its browser session to avoid repeated logins.',
uploadSources: 'Upload sources',
uploadSourcesTip:
'Supports file IDs, file URLs, or variables. You can separate multiple values with commas or use a JSON array format (for example ["id1","https://example.com/a.pdf"]).',
advancedSettings: 'Advanced settings',
addTools: 'Add tools',
sysPromptDefaultValue: `
@@ -1938,6 +1952,9 @@ Best for: Documents with flowing, contextually connected content — such as boo
'A component that searches via your provided SearXNG instance URL. Specify TopN and the instance URL.',
docGenerator: 'Doc Generator',
docGeneratorDescription: `Generate a file from Markdown content.`,
browser: 'Browser',
browserDescription:
'Automate browser tasks. Supports model configuration and prompt-driven actions. Upload sources support file IDs and URLs, and downloaded files can be saved to a target folder.',
subtitle: 'Subtitle',
logoImage: 'Logo Image',
logoPosition: 'Logo Position',

View File

@@ -208,16 +208,19 @@ export default {
'Geçersiz beceri: "name" küçük harfli ve URL uyumlu olmalıdır (yalnızca harf, rakam ve tire).',
invalid_version:
'Geçersiz beceri: "version" geçerli semver olmalıdır (örn. 1.0.0).',
invalid_metadata: 'Geçersiz beceri: meta veriler geçersiz alanlar içeriyor.',
invalid_metadata:
'Geçersiz beceri: meta veriler geçersiz alanlar içeriyor.',
invalid_file_type:
'Geçersiz beceri: Yalnızca metin tabanlı dosyalara izin verilir.',
invalid_path: 'Geçersiz beceri: Dosya yolu geçersiz karakterler içeriyor.',
invalid_path:
'Geçersiz beceri: Dosya yolu geçersiz karakterler içeriyor.',
file_too_large:
'Geçersiz beceri: Tekil dosya boyutu 5MB sınırınııyor.',
total_size_exceeded:
'Geçersiz beceri: Toplam paket boyutu 50MB sınırınııyor.',
no_files: 'Dosya seçilmedi. Lütfen bir beceri klasörü seçin.',
noValidFiles: 'Geçerli dosya bulunamadı. Lütfen seçiminizi kontrol edin.',
noValidFiles:
'Geçerli dosya bulunamadı. Lütfen seçiminizi kontrol edin.',
junkFilesFound:
'Geçici dosyalar algılandı (örn. .DS_Store). Lütfen yüklemeden önce bunları kaldırın.',
read_failed: 'Geçersiz beceri: SKILL.md dosyası okunamadı.',
@@ -232,8 +235,8 @@ export default {
upload: 'Yükle',
importFromGit: "Git'ten içe aktar",
gitPlatform: 'Platform',
repoUrl: 'Depo URL\'si',
repoUrlHelp: 'İsteğe bağlı yol içeren depo URL\'sini destekler',
repoUrl: "Depo URL'si",
repoUrlHelp: "İsteğe bağlı yol içeren depo URL'sini destekler",
accessToken: 'Erişim Tokenı',
githubTokenHelp:
'Özel depolar veya daha yüksek hız limitleri için (5000 istek/saat)',
@@ -250,7 +253,8 @@ export default {
},
skillSearch: {
configTitle: 'Beceri Arama Yapılandırması',
configDesc: 'Becerilerin nasıl indeksleneceğini ve aranacağını yapılandırın',
configDesc:
'Becerilerin nasıl indeksleneceğini ve aranacağını yapılandırın',
embeddingModel: 'Embedding Modeli',
embeddingModelPlaceholder: 'Bir embedding modeli seçin',
vectorSimilarityWeight: 'Vektör Benzerlik Ağırlığı',

View File

@@ -1529,6 +1529,20 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
maxRounds: '最大反思轮数',
delayAfterError: '错误后延迟',
maxRetries: '最大重试轮数',
maxSteps: '最大步数',
headless: '无头模式',
enableDefaultExtensions: '启用默认扩展',
enableDefaultExtensionsTip:
'启用 browser-use 默认扩展uBlock、Cookie 处理、ClearURLs。关闭后可避免运行时下载扩展。',
chromiumSandbox: 'Chromium 沙箱',
chromiumSandboxTip:
'是否启用 Chromium 沙箱。Docker root 环境通常需要关闭,普通宿主机环境建议开启。',
persistSession: '持久化登录态',
persistSessionTip:
'开启后将复用该 Browser 节点的浏览器会话,避免重复登录。',
uploadSources: '上传来源',
uploadSourcesTip:
'支持文件 ID、文件 URL 或变量。可用逗号分隔多个值,也支持 JSON 数组格式(如 ["id1","https://example.com/a.pdf"])。',
advancedSettings: '高级设置',
addTools: '添加工具',
sysPromptDefultValue: `
@@ -1643,6 +1657,9 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
'该组件通过您提供的 SearXNG 实例地址进行搜索。请设置 Top N 和实例 URL。',
docGenerator: '文档生成器',
docGeneratorDescription: `从 Markdown 内容生成文件。`,
browser: 'Browser',
browserDescription:
'使用 Browser 自动执行浏览器任务。支持模型与提示词配置,上传来源支持文件 ID 与 URL并可将下载文件保存到指定目录。',
subtitle: '副标题',
logoImage: '标志图片',
logoPosition: '标志位置',

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>