跳到主要内容

01 · Gradio 外壳:组件即状态、字典即参数、yield 即实时

本章讲清三件让后面所有代码成立的“地基”:UI 怎么拼、状态存在哪、回调凭什么能拿到所有设置并实时刷新。读懂这章,03 章那个几百行的运行循环就不再吓人。

1.1 它要解决的小问题

Gradio 的常规写法是“一个回调列出它需要的几个输入组件”。但本项目的运行回调需要几乎所有 tab 的设置(模型、温度、浏览器路径、窗口大小……几十个)。如果按常规写,函数签名会爆炸,且加一个设置项就得改一长串地方。

它的解法: 不按名传参,而是把全部组件作为输入,回调内部凭“字符串 id”现取现用。

1.2 顶层结构:5 个 tab 拼成一个 Blocks

create_ui 就是装配工(src/webui/interface.py:22):先建一个 WebuiManager()(全局状态),再在 gr.Blocks 里逐个 tab 调各自的 create_*_tab(ui_manager),把 manager 传进去让它们自我注册。

create_ui()
├─ ui_manager = WebuiManager() # 全局状态 + 注册表
└─ gr.Blocks
├─ ⚙️ Agent Settings → create_agent_settings_tab(ui_manager)
├─ 🌐 Browser Settings → create_browser_settings_tab(ui_manager)
├─ 🤖 Run Agent → create_browser_use_agent_tab(ui_manager)
├─ 🎁 Marketplace → create_deep_research_agent_tab(ui_manager)
└─ 📁 Load & Save → create_load_save_config_tab(ui_manager)

注意两个细节(都在 interface.py):

  • 启动默认强制暗色——靠注入一段 JS js_func,发现 URL 没有 __theme=dark 就改 URL 重载(src/webui/interface.py:46-55)。
  • 主题可选(theme_map,8 种),--theme 决定用哪个(src/webui/interface.py:10-19)。

1.3 WebuiManager:注册表 + 状态容器

WebuiManager 是整个 UI 的“中枢”,干两类事。

第一类:组件↔id 双向注册表。 每个 tab 创建完组件后调 add_components(tab_name, {...}),它给每个组件生成 "{tab}.{name}" 的 id,并存两张表(src/webui/webui_manager.py:53-60):

# 真实逻辑,src/webui/webui_manager.py:57-60
comp_id = f"{tab_name}.{comp_name}"
self.id_to_component[comp_id] = component # id → 组件
self.component_to_id[component] = comp_id # 组件 → id

有了这两张表,任何回调都能:拿 id 取组件(get_component_by_id)、或拿组件反查 id(get_id_by_component)。这就是“凭 id 现取现用”的底座。

第二类:全局可变状态。 比如浏览器、agent 实例、聊天记录、求助事件等,全挂在 manager 实例属性上,由 init_browser_use_agent() 一次性初始化(src/webui/webui_manager.py:30-42):

# 真实字段(节选),src/webui/webui_manager.py:34-42
self.bu_agent: Optional[Agent] = None # 当前 agent
self.bu_browser: Optional[CustomBrowser] = None # 当前浏览器
self.bu_chat_history: List[Dict] = [] # 聊天记录(UI 与回调共享)
self.bu_response_event: Optional[asyncio.Event] = None # 人在回路:等用户回复
self.bu_current_task: Optional[asyncio.Task] = None # 后台运行的 agent 任务

重点看 bu_chat_historybu_response_event——它们是“后台 agent 回调”和“前端刷新循环”之间的共享内存。回调往 bu_chat_history 追加消息,循环发现它变长就刷新聊天框(03 章详述)。

1.4 “全部组件当输入”如何接线

看 Run Agent tab 的接线(src/webui/components/browser_use_agent_tab.py:1042-1075):

# 真实代码(节选),browser_use_agent_tab.py:1042-1071
all_managed_components = set(webui_manager.get_components()) # 拿到全部组件
run_button.click(
fn=submit_wrapper,
inputs=all_managed_components, # 整个界面的组件都喂进去
outputs=run_tab_outputs,
trigger_mode="multiple",
)

Gradio 调回调时,会把 inputs 里每个组件的当前值打包成一个 {组件对象: 值} 的字典传进来。于是回调内部用一个小帮手按前缀取值即可(browser_use_agent_tab.py:327-329):

# 真实代码,browser_use_agent_tab.py:327-329
def get_setting(key, default=None):
comp = webui_manager.id_to_component.get(f"agent_settings.{key}")
return components.get(comp, default) if comp else default

这就是全章的核心套路: “我要 agent_settings.llm_provider 的值” → 用 id 从注册表取组件对象 → 再用组件对象从 Gradio 传来的字典里取值。加新设置项时,回调签名一行不改。

1.5 yield 即实时:async generator 流式刷新

Gradio 支持回调是 async 生成器——每 yield 一个 {组件: gr.update(...)} 字典,前端就立刻按这个字典更新对应组件。本项目所有“跑起来后界面动态变化”都靠这个。

最小心智模型(示意,非源码):

# 示意,非源码:解释“yield 即一次界面刷新”
async def run(components):
yield {run_button: gr.update(value="⏳ 运行中...", interactive=False)} # 立即变灰
task = asyncio.create_task(agent.run()) # 后台真正干活
while not task.done(): # 轮询
yield {chatbot: gr.update(value=chat_history)} # 每轮把最新聊天推给前端
await asyncio.sleep(0.1)
yield {run_button: gr.update(value="▶️ 提交", interactive=True)} # 收尾恢复

真实版就是 run_agent_task(03 章逐段拆)。这里只需记住:生成器没结束 = 任务还在跑 = 界面还在被一帧帧刷新webui.py 里那句 demo.queue() 正是为了让这种长流式回调排队执行。

1.6 配置存取:遍历组件即序列化

借助注册表,存配置就是“遍历所有组件、把 id→值 写成 JSON”(src/webui/webui_manager.py:80-95):跳过按钮、文件、以及 interactive=False 的只读组件,其余按 comp_id: 值 落盘到带时间戳的 json。

加载则反过来:读 json,对每个 id 找回组件、用 comp.__class__(value=...) 重建并回填(src/webui/webui_manager.py:97-122)。有个小巧思——遇到 planner_llm_provider先 yield 一次再 sleep(0.1)(webui_manager.py:112-114),给 Gradio 的联动回调(选 provider 自动刷新模型下拉)留出执行窗口,避免后填的模型值被联动覆盖。

巧妙之处

  • 注册表解耦签名:回调不耦合“具体有哪些设置”,只耦合“id 命名约定”,UI 可自由增删项(add_components, get_setting)。
  • 聊天记录当共享总线:后台回调与前端循环不直接通信,只通过 bu_chat_history 这块共享列表(webui_manager.py:38),长度变化即“有新内容”。
  • 加载顺序的 0.1s 让位:用 yield + sleep 显式给联动回调让路,解决“provider 与 model 先后赋值打架”(webui_manager.py:112-114)。

边界与坑

  • 状态挂在单个 WebuiManager 实例上 → 天然单会话;多用户并发会互相串。
  • _get_config_value / get_setting 找不到组件时只 logger.warning 后返回默认(browser_use_agent_tab.py:69-95)——拼错 id 不会报错,只会静默拿到默认值,调试时要警惕。

代码地图

主题文件符号
UI 装配 + 暗色注入src/webui/interface.pycreate_ui, js_func, theme_map
注册表 + 状态src/webui/webui_manager.pyadd_components, get_component_by_id, init_browser_use_agent
配置存取src/webui/webui_manager.pysave_config, load_config
“全部组件当输入”接线src/webui/components/browser_use_agent_tab.pycreate_browser_use_agent_tab, get_setting