跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PythonAI

ZeroClaw Gateway + LM Studio + Reflex 本地 AI 管理面板搭建

记录了使用 Python Reflex 框架为 ZeroClaw 本地 AI 网关打造 Web 管理面板的全过程。主要解决了 ZeroClaw CLI 操作不便的问题,通过 HTTP 接口对接 LM Studio 实现本地模型推理。内容涵盖环境准备、Reflex 项目初始化、网关架构理解(POST /webhook)、Reflex 0.8.x 版本适配坑点(如 Setter 弃用、组件参数变化)、System Prompt 配置写入方案以及输出日志清洗。最终实现了包含网关控制、模型选择、对话交互及状态监控的本地 AI 控制台。

追风少年发布于 2026/4/6更新于 2026/9/873 浏览
ZeroClaw Gateway + LM Studio + Reflex 本地 AI 管理面板搭建

前言:为什么要给 ZeroClaw 做 Web UI?

ZeroClaw 是一个用 Rust 编写的高性能本地 AI 网关工具,设计目标是速度快、体积小、无依赖。但它本身只有命令行界面(CLI),每次使用都需要手动输入命令,管理起来不够直观。

ZeroClaw
https://github.com/zeroclaw-labs/zeroclaw

本文记录了从零开始,用 Python Reflex 框架 为 ZeroClaw 打造一个现代化 Web 管理面板的完整过程,包括踩过的所有坑和最终解决方案。

💡 ZeroClaw 架构:用户 → ZeroClaw Gateway (127.0.0.1:8080) → LM Studio API → 本地大模型

技术栈

组件说明
ZeroClawRust 编写的本地 AI 网关,提供 /webhook HTTP 接口
LM Studio本地大模型运行环境,提供 OpenAI 兼容 API
ReflexPython 全栈 Web 框架,前后端均用 Python 编写
llama.cpp底层推理引擎(可选)

第一步:环境准备

1.1 安装依赖

在 ZeroClaw 项目根目录,激活虚拟环境后安装所需 Python 包:

# 激活虚拟环境(Windows PowerShell)
.venv\Scripts\Activate.ps1

# 安装依赖
pip install reflex psutil python-dotenv requests pywin32

1.2 初始化 Reflex 项目

mkdir zeroclaw-reflex-ui
cd zeroclaw-reflex-ui
reflex init  # 选择模板 0(空白)

⚠️ Reflex init 会生成同名的 Python 包目录和入口文件,注意不要覆盖错位置。

1.3 放置主文件

将我们编写的 zeroclaw_reflex_ui.py 覆盖到 Reflex 自动生成的同名文件:

# Windows 命令
move zeroclaw_reflex_ui.py zeroclaw_reflex_ui\
# 提示覆盖时选 Yes(Y)

zeroclaw_reflex_ui.py 完整内容示例:

import re
import time
import tomllib
import reflex as rx
import requests
import subprocess
import os
 threading
 dotenv  load_dotenv
 typing  , , 


_ANSI_ESCAPE = re.()
load_dotenv()


GATEWAY_URL = 
ZEROCLAW_PATH = 
ZEROCLAW_CONFIG = os.path.expanduser()


_gateway_process: [subprocess.Popen] = 
_gateway_lock = threading.Lock()

 () -> subprocess.Popen:
    
    env = os.environ.copy()
    env[] = lm_url
    env[] = lm_url
    env[] = lm_key
    env[] = lm_url
    env[] = lm_key
    env[] = model
    proc = subprocess.Popen(
        [ZEROCLAW_PATH, ],
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        creationflags=subprocess.CREATE_NEW_PROCESS_GROUP  
    )
     proc

 () -> :
    
    :
        r = requests.get(, timeout=)
         r.status_code == 
     Exception:
         

 (rx.State):
    
    lm_studio_api_url:  = os.getenv(, )
    lm_studio_api_key:  = os.getenv(, )
    model_id:  = os.getenv(, )
    models: [] = []

    
    user_message:  = 
    system_prompt:  = 
    chat_history: [[, ]] = []
    is_loading:  = 

    
    gpu_usage:  = 
    lm_studio_status:  = 
    gateway_status:  = 
    zeroclaw_bin_status:  = 

    
    
    
     ():
        .lm_studio_api_url = value

     ():
        .lm_studio_api_key = value

     ():
        .model_id = value

     ():
        .user_message = value

     ():
        .system_prompt = value

     ():
         State.send_message

    
    
    
     ():
        
        :
             (ZEROCLAW_CONFIG, )  f:
                config = tomllib.load(f)
                .system_prompt = config.get(, .system_prompt)
         Exception:
              

     ():
        
        :
            
             (ZEROCLAW_CONFIG, , encoding=)  f:
                content = f.read()
            escaped = .system_prompt.replace(, ).replace(, )
            new_line = 
             re.search(, content, re.MULTILINE):
                
                content = re.sub(
                    , new_line, content, flags=re.MULTILINE
                )
            :
                
                content = new_line +  + content
             (ZEROCLAW_CONFIG, , encoding=)  f:
                f.write(content)
            
             State.stop_gateway
             State.start_gateway
             rx.toast.success()
         Exception  e:
             rx.toast.error()

    
    
    
     ():
        
         _gateway_process
         _gateway_lock:
             _check_gateway_alive():
                .gateway_status = 
                 rx.toast.info()
             _gateway_process  _gateway_process.poll()  :
                _gateway_process.kill()
            :
                _gateway_process = _start_gateway_process(
                    .lm_studio_api_url,
                    .lm_studio_api_key,
                    .model_id
                )
                 _  ():
                    time.sleep()
                     _check_gateway_alive():
                        .gateway_status = 
                         rx.toast.success()
                .gateway_status = 
                 rx.toast.error()
             FileNotFoundError:
                .gateway_status = 
                 rx.toast.error()
             Exception  e:
                .gateway_status = 
                 rx.toast.error()

     ():
        
         _gateway_process
         _gateway_lock:
             _gateway_process  _gateway_process.poll()  :
                _gateway_process.kill()
                _gateway_process = 
                .gateway_status = 
                 rx.toast.success()
            :
                .gateway_status = 
                 rx.toast.info()

    
    
    
     ():
        
        :
            response = requests.get(
                ,
                headers={: },
                timeout=
            )
             response.status_code == :
                data = response.json()
                .models = [model[]  model  data.get(, [])]
                .lm_studio_status = 
                  .model_id  .models:
                    .model_id = .models[]
            :
                .lm_studio_status = 
                .models = []
         Exception:
            .lm_studio_status = 
            .models = []

     ():
        
         (, )  f:
            f.write()
            f.write()
            f.write()
         rx.toast.success()

    
    
    
     ():
        
          .user_message.strip():
             rx.toast.error()
          _check_gateway_alive():
             rx.toast.error()

        user_text = .user_message
        .chat_history.append({: , : user_text})
        .is_loading = 
        .user_message = 
          

        :
            response = requests.post(
                ,
                json={: user_text, : .system_prompt},
                timeout=
            )
             response.status_code == :
                data = response.json()
                 (data, ):
                    raw = (
                        data.get()  
                         data.get()
                         data.get()
                         data.get()
                         (data)
                    )
                :
                    raw = (data)

                
                clean = _ANSI_ESCAPE.sub(, raw)
                
                lines = clean.splitlines()
                reply_lines = [
                    ln  ln  lines
                      re.(, ln)
                ]
                reply = .join(reply_lines).strip()  clean.strip()
                .chat_history.append({: , : reply})
            :
                .chat_history.append({
                    : ,
                    : 
                })
         requests.exceptions.Timeout:
            .chat_history.append({
                : ,
                : 
            })
         Exception  e:
            .chat_history.append({
                : ,
                : 
            })
        :
            .is_loading = 

     ():
        .chat_history = []

    
    
    
     ():
        
        .zeroclaw_bin_status =   os.path.exists(ZEROCLAW_PATH)  
        .gateway_status =   _check_gateway_alive()  
        :
            result = subprocess.run(
                [, , ],
                capture_output=,
                text=,
                timeout=
            )
            .gpu_usage =   result.returncode ==   
         Exception:
            .gpu_usage = 
        .fetch_lm_studio_models()

    
    
    
     () -> rx.Component:
         rx.box(
            rx.text(label, size=, color=, margin_bottom=),
            rx.text(value, size=, font_weight=),
            border_radius=,
            background_color=,
        )

     () -> rx.Component:
         rx.card(
            rx.vstack(
                rx.heading(, size=),
                rx.grid(
                    status_card(, State.zeroclaw_bin_status),
                    status_card(, State.gateway_status),
                    status_card(, State.lm_studio_status),
                    status_card(, State.gpu_usage),
                    columns=,
                    gap=
                ),
                rx.hstack(
                    rx.button(, on_click=State.start_gateway, color_scheme=, size=),
                    rx.button(, on_click=State.stop_gateway, color_scheme=, size=),
                    rx.button(, on_click=State.update_system_status, size=),
                    spacing=
                ),
                rx.callout(
                    rx.text(, size=),
                    color=,
                    size=
                ),
                spacing=,
            ),
            margin_bottom=
        )

     () -> rx.Component:
         rx.card(
            rx.vstack(
                rx.heading(, size=),
                rx.text(, size=, color=),
                rx.(
                    value=State.lm_studio_api_url,
                    on_change=State.set_lm_studio_api_url,
                    placeholder=,
                ),
                rx.text(, size=, color=),
                rx.(
                    value=State.lm_studio_api_key,
                    on_change=State.set_lm_studio_api_key,
                    placeholder=,
                    =,
                ),
                rx.text(, size=, color=),
                rx.select(
                    State.models,
                    value=State.model_id,
                    on_change=State.set_model_id,
                    placeholder=,
                ),
                rx.hstack(
                    rx.button(, on_click=State.fetch_lm_studio_models, size=),
                    rx.button(, on_click=State.save_config, color_scheme=, size=),
                    spacing=
                ),
                rx.divider(),
                rx.text(, size=, color=),
                rx.callout(
                    rx.text(, size=),
                    color=,
                    size=
                ),
                rx.text_area(
                    value=State.system_prompt,
                    on_change=State.set_system_prompt,
                    placeholder=,
                    rows=
                ),
                rx.button(
                    ,
                    on_click=State.save_system_prompt_to_config,
                    color_scheme=,
                    size=,
                ),
                spacing=,
            ),
            margin_bottom=
        )

     () -> rx.Component:
        is_user = msg[] == 
         rx.box(
            rx.hstack(
                rx.text(
                    rx.cond(is_user, , ),
                    font_weight=,
                    color=rx.cond(is_user, , ),
                    white_space=,
                    min_width=
                ),
                rx.text(, color=),
                rx.cond(
                    is_user,
                    rx.text(msg[], flex=),
                    rx.box(
                        rx.markdown(msg[]),
                        flex=,
                        class_name=
                    )
                ),
            ),
            background_color=rx.cond(is_user, , ),
            border_left=rx.cond(is_user, , ),
            border_radius=,
            margin_bottom=,
        )

     () -> rx.Component:
         rx.card(
            rx.vstack(
                rx.hstack(
                    rx.heading(, size=),
                    rx.spacer(),
                    rx.button(, on_click=State.clear_chat, size=, color_scheme=),
                ),
                rx.box(
                    rx.cond(
                        State.chat_history.length() == ,
                        rx.center(
                            rx.text(, color=, size=),
                        ),
                        rx.foreach(State.chat_history, chat_bubble)
                    ),
                    overflow_y=,
                    border_radius=,
                ),
                rx.form(
                    rx.hstack(
                        rx.(
                            placeholder=,
                            value=State.user_message,
                            on_change=State.set_user_message,
                            name=,
                            disabled=State.is_loading
                        ),
                        rx.button(
                            rx.cond(
                                State.is_loading,
                                rx.hstack(rx.spinner(size=), rx.text(), spacing=),
                                rx.text()
                            ),
                            =,
                            disabled=State.is_loading,
                            color_scheme=,
                            size=
                        ),
                        spacing=
                    ),
                    on_submit=State.handle_form_submit,
                ),
                spacing=,
            ),
        )

     () -> rx.Component:
         rx.container(
            rx.vstack(
                rx.heading(, size=, margin_bottom=),
                rx.text(, size=, color=, margin_bottom=),
                gateway_panel(),
                config_panel(),
                chat_interface(),
                max_width=,
                spacing=,
            )
        )

app = rx.App()
app.add_page(
    index,
    title=,
    on_load=[State.update_system_status, State.load_system_prompt_from_config]
)

 __name__ == :
    app.run()
import
from
import
from
import
Dict
List
Optional
# 过滤 ANSI 终端控制码(颜色、粗体、日志前缀等)
compile
r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07'
".env"
# ZeroClaw 网关地址(固定,由 zeroclaw gateway 命令启动)
"http://127.0.0.1:8080"
"J:\\PythonProjects4\\zeroclaw\\target\\release\\zeroclaw.exe"
"~\\.zeroclaw\\config.toml"
# 全局持有网关进程(跨请求共享)
Optional
None
def
_start_gateway_process
lm_url: str, lm_key: str, model: str
"""在后台启动 zeroclaw gateway 进程"""
"OPENAI_API_BASE"
"OPENAI_BASE_URL"
"OPENAI_API_KEY"
"LM_STUDIO_API_URL"
"LM_STUDIO_API_KEY"
"MODEL_ID"
"gateway"
# Windows:独立进程组,方便终止
return
def
_check_gateway_alive
bool
"""检查网关是否响应"""
try
f"{GATEWAY_URL}/health"
2
return
200
except
return
False
class
State
# LM Studio 配置
str
"LM_STUDIO_API_URL"
"http://127.0.0.1:1234/v1"
str
"LM_STUDIO_API_KEY"
"sk-local-lmstudio-2026-zeroclaw"
str
"MODEL_ID"
""
List
str
# 对话状态
str
""
str
"你是一个本地运行的 AI 助手,基于开源大模型。请不要声称自己是 ChatGPT 或 GPT-4。"
List
Dict
str
str
bool
False
# 系统状态
str
"检测中..."
str
"未连接"
str
"未启动"
str
"未检测"
# --------------------------
# Setters
# --------------------------
def
set_lm_studio_api_url
self, value: str
self
def
set_lm_studio_api_key
self, value: str
self
def
set_model_id
self, value: str
self
def
set_user_message
self, value: str
self
def
set_system_prompt
self, value: str
self
def
handle_form_submit
self, form_data: dict
yield
# --------------------------
# System Prompt 配置文件读写
# --------------------------
def
load_system_prompt_from_config
self
"""从 config.toml 读取 system_prompt 字段"""
try
with
open
"rb"
as
self
"system_prompt"
self
except
pass
# 文件不存在或解析失败时保留当前值
def
save_system_prompt_to_config
self
"""将 system_prompt 写入 config.toml,然后重启网关生效"""
try
# 读取原始文件内容(保留格式和注释)
with
open
"r"
"utf-8"
as
self
"\\"
"\\\\"
'"'
'\\"'
f'system_prompt = "{escaped}"'
if
r'^system_prompt\s*='
# 替换已有的 system_prompt 行
r'^system_prompt\s*=.*$'
else
# 插入到文件顶部(第一个 [section] 之前)
"\n"
with
open
"w"
"utf-8"
as
# 保存成功后重启网关使其生效
yield
yield
yield
"System Prompt 已保存,网关已重启!"
except
as
yield
f"保存失败:{str(e)}"
# --------------------------
# 网关管理
# --------------------------
def
start_gateway
self
"""启动 ZeroClaw 网关"""
global
with
if
self
"✅ 运行中"
return
"网关已在运行中!"
if
and
is
None
try
self
self
self
for
in
range
6
0.5
if
self
"✅ 运行中"
return
"网关启动成功!"
self
"⚠️ 启动超时"
return
"网关启动超时,请检查路径和配置"
except
self
"❌ 找不到 zeroclaw.exe"
return
f"找不到:{ZEROCLAW_PATH}"
except
as
self
f"❌ 异常"
return
f"启动失败:{str(e)}"
def
stop_gateway
self
"""停止 ZeroClaw 网关"""
global
with
if
and
is
None
None
self
"⛔ 已停止"
return
"网关已停止"
else
self
"⛔ 未运行"
return
"网关当前未运行"
# --------------------------
# LM Studio
# --------------------------
def
fetch_lm_studio_models
self
"""获取 LM Studio 可用模型列表"""
try
f"{self.lm_studio_api_url}/models"
"Authorization"
f"Bearer {self.lm_studio_api_key}"
5
if
200
self
"id"
for
in
"data"
self
"✅ 已连接"
if
not
self
and
self
self
self
0
else
self
f"❌ 失败({response.status_code})"
self
except
self
"❌ 连接异常"
self
def
save_config
self
"""保存配置到 .env"""
with
open
".env"
"w"
as
f'LM_STUDIO_API_URL="{self.lm_studio_api_url}"\n'
f'LM_STUDIO_API_KEY="{self.lm_studio_api_key}"\n'
f'MODEL_ID="{self.model_id}"\n'
return
"配置已保存!"
# --------------------------
# 对话:直接 POST 到网关 /webhook
# --------------------------
def
send_message
self
"""通过 ZeroClaw 网关 /webhook 发送消息"""
if
not
self
return
"请输入消息!"
if
not
return
"网关未启动!请先点击「▶ 启动网关」"
self
self
"role"
"user"
"content"
self
True
self
""
yield
# 立即刷新 UI
try
f"{GATEWAY_URL}/webhook"
"message"
"system_prompt"
self
120
if
200
if
isinstance
dict
"response"
# ZeroClaw 网关实际返回字段
or
"reply"
or
"message"
or
"content"
or
str
else
str
# 去除 ANSI 控制码
""
# 去除 zeroclaw 日志行(以时间戳或 INFO/WARN 开头的行)
for
in
if
not
match
r'^\s*(INFO|WARN|ERROR|DEBUG|\d{4}-\d{2}-\d{2})'
"\n"
or
self
"role"
"assistant"
"content"
else
self
"role"
"assistant"
"content"
f"❌ 网关返回错误 {response.status_code}:{response.text[:300]}"
except
self
"role"
"assistant"
"content"
"⏱️ 请求超时,模型响应过慢,请稍后重试"
except
as
self
"role"
"assistant"
"content"
f"❌ 请求异常:{str(e)}"
finally
self
False
def
clear_chat
self
self
# --------------------------
# 系统状态刷新
# --------------------------
def
update_system_status
self
"""刷新所有系统状态"""
self
"✅ 已找到"
if
else
"❌ 未找到"
self
"✅ 运行中"
if
else
"⛔ 未运行"
try
"nvidia-smi"
"--query-gpu=utilization.gpu"
"--format=csv,noheader,nounits"
True
True
3
self
f"{result.stdout.strip()}%"
if
0
else
"无法读取"
except
self
"不支持"
self
# --------------------------
# UI 组件
# --------------------------
def
status_card
label: str, value
return
"1"
"#6b7280"
"0.2em"
"3"
"600"
"0.5em"
"#f9fafb"
def
gateway_panel
return
"ZeroClaw 网关控制"
"5"
"zeroclaw.exe"
"网关状态"
"LM Studio"
"GPU 使用率"
"2"
"0.75em"
"▶ 启动网关"
"green"
"2"
"■ 停止网关"
"red"
"2"
"↻ 刷新状态"
"2"
"3"
"发送消息前请确保网关显示「✅ 运行中」。启动网关前请先配置好 LM Studio 并选择模型。"
"2"
"blue"
"1"
"4"
"1em"
def
config_panel
return
"LM Studio 配置"
"5"
"API 地址(带 /v1)"
"2"
"#6b7280"
input
"http://127.0.0.1:1234/v1"
"API 密钥"
"2"
"#6b7280"
input
"sk-local-xxx"
type
"password"
"选择本地模型"
"2"
"#6b7280"
"点击「刷新模型列表」加载..."
"↻ 刷新模型列表"
"2"
"💾 保存配置"
"green"
"2"
"3"
"系统提示词(System Prompt)"
"2"
"#6b7280"
"修改后需点击「保存并重启网关」才能生效,网关会自动重启。"
"2"
"amber"
"1"
"在此输入系统提示词,约束模型的身份和行为..."
"4"
"💾 保存 System Prompt 并重启网关"
"amber"
"2"
"4"
"1em"
def
chat_bubble
msg
"role"
"user"
return
"你"
"AI"
"700"
"#1d4ed8"
"#065f46"
"nowrap"
"1.8em"
":"
"#9ca3af"
"content"
"1"
"content"
"1"
"markdown-body"
"#eff6ff"
"#f0fdf4"
"3px solid #3b82f6"
"3px solid #22c55e"
"0.4em"
"0.5em"
def
chat_interface
return
"ZeroClaw 对话窗口"
"5"
"🗑 清空对话"
"1"
"gray"
0
"还没有对话,输入消息开始吧~"
"#9ca3af"
"2"
"auto"
"0.5em"
input
"输入消息,按 Enter 或点击发送..."
"message"
"2"
"等待中"
"2"
"发送"
type
"submit"
"blue"
"2"
"2"
"4"
def
index
return
"🦀 ZeroClaw 本地管理面板"
"7"
"0.2em"
"ZeroClaw Gateway + LM Studio 本地 AI 控制台"
"2"
"#6b7280"
"0.5em"
"820px"
"4"
"ZeroClaw 本地管理面板"
if
"__main__"

第二步:理解 ZeroClaw 网关架构

2.1 正确的通信方式

这是本项目最关键的发现。ZeroClaw 提供了一个 HTTP 网关服务,支持以下接口:

接口说明
POST /webhook{"message": "你的提问"} → AI 回复
GET /health健康检查(用于检测网关是否在线)
POST /pair配对新客户端

错误做法(最初的方案): 直接调用 zeroclaw.exe agent 命令行

# ❌ 错误 — agent 子命令不支持 --api-base 等参数
zeroclaw.exe agent --message "你好" --model xxx --api-base http://...

正确做法: 启动网关后,直接 POST 到 /webhook 接口

# ✅ 正确 — 通过 HTTP 与网关通信
import requests
response = requests.post(
    "http://127.0.0.1:8080/webhook",
    json={"message": "你好"},
    timeout=120
)
reply = response.json().get("response", "")

2.2 网关启动方式

ZeroClaw 网关通过以下命令启动,API 配置通过环境变量传入:

zeroclaw gateway

手动启动方式

zeroclaw gateway

输出示例:

🚀 Starting ZeroClaw Gateway on 127.0.0.1:8080

POST /webhook — {"message": "your prompt"}

GET /health — health check

在 Reflex UI 里,我们用 subprocess.Popen 在后台启动网关进程,通过环境变量注入配置:

env = os.environ.copy()
env["OPENAI_API_BASE"] = lm_studio_url
env["OPENAI_API_KEY"]  = lm_studio_key
proc = subprocess.Popen([ZEROCLAW_PATH, "gateway"], env=env,
    creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)

第三步:Reflex 开发关键踩坑记录

Reflex 0.8.x 版本变化较大,以下是本次开发中遇到的所有报错及解决方案:

3.1 Button size 参数

❌ 旧写法✅ 新写法(0.8.x)
size="sm"size="2"
size="lg"size="3"
is_disabled=Truedisabled=True

3.2 自动 Setter 弃用

错误: DeprecationWarning: state_auto_setters defaulting to True

Reflex 0.8.9+ 不再自动生成 set_xxx 方法,需要手动在 State 类里定义:

class State(rx.State):
    lm_studio_api_url:

    # ✅ 必须显式定义 setter
    def set_lm_studio_api_url(self, value: str):
        self.lm_studio_api_url = value

3.3 rx.foreach 里不能用 Python if/else

错误: VarTypeError: Cannot convert Var to bool

在 rx.foreach 的 lambda 里,变量是 Reflex 响应式 Var,不能用 Python 原生条件:

# ❌ 错误写法
"你" if msg["role"] == "user" else "AI"

# ✅ 正确写法 — 使用 rx.cond()
rx.cond(msg["role"] == "user", "你", "AI")

# 属性也一样
background_color=rx.cond(msg["role"] == "user", "#f0f", "#0ff")

3.4 rx.input 不支持 on_submit

错误: ValueError: TextFieldRoot does not take in an on_submit event trigger

Reflex 的 rx.input 组件不支持 on_submit。解决方案是用 rx.form 包裹,通过表单提交触发:

# ✅ 用 rx.form 包裹支持回车发送
rx.form(
    rx.hstack(
        rx.input(value=State.user_message, on_change=State.set_user_message),
        rx.button("发送", type="submit")
    ),
    on_submit=State.handle_form_submit  # 接收 dict 参数
)

# State 里定义:
def handle_form_submit(self, form_data: dict):
    yield State.send_message

3.5 rx.select 的正确用法

Reflex 的 rx.select 将选项列表作为第一个位置参数传入,不用 options= 关键字:

# ❌ 错误
rx.select(label="模型", options=State.models, value=State.model_id)

# ✅ 正确
rx.select(State.models, value=State.model_id, on_change=State.set_model_id)

第四步:System Prompt 的实现

4.1 网关不支持动态传入 system_prompt

通过分析 ZeroClaw 源码(src/gateway/mod.rs),发现网关的 /webhook 接口参数签名为:

// Rust 源码片段
_system_prompt: Option<&str>, // 下划线前缀 = 故意忽略该参数

这意味着通过 /webhook 传入的 system_prompt 字段会被直接丢弃。

4.2 正确方式:写入配置文件

ZeroClaw 的 system_prompt 在 config.toml 里配置,网关启动时读取:

# 配置文件位置:C:\Users\{用户名}\\.zeroclaw\config.toml

# 添加这一行:
system_prompt = "你是一个本地运行的 AI 助手,基于开源大模型。"

在 UI 里实现了「保存 System Prompt 并重启网关」功能,通过正则替换写入配置文件后自动重启网关:

def save_system_prompt_to_config(self):
    with open(ZEROCLAW_CONFIG, "r", encoding="utf-8") as f:
        content = f.read()
    new_line = f'system_prompt = "{self.system_prompt}"'
    if re.search(r'^system_prompt', content, re.MULTILINE):
        content = re.sub(r'^system_prompt.*$', new_line,
                         content, flags=re.MULTILINE)
    else:
        content = new_line + "\n" + content
    with open(ZEROCLAW_CONFIG, "w", encoding="utf-8") as f:
        f.write(content)
    yield State.stop_gateway
    yield State.start_gateway

第五步:清理模型输出的 ANSI 控制码

ZeroClaw 网关返回的 response 字段有时会包含终端 ANSI 控制码和日志行,需要过滤:

import re

# 过滤 ANSI 控制码
_ANSI_ESCAPE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07')

def clean_reply(raw: str) -> str:
    clean = _ANSI_ESCAPE.sub("", raw)
    # 过滤 zeroclaw 日志行(时间戳 / INFO / WARN 开头)
    lines = clean.splitlines()
    reply_lines = [
        ln for ln in lines
        if not re.match(r'^\s*(INFO|WARN|ERROR|\d{4}-\d{2}-\d{2})', ln)
    ]
    return "\n".join(reply_lines).strip() or clean.strip()

第六步:完整使用流程

每次启动顺序

  1. 启动 LM Studio,加载模型并开启本地服务器(默认端口 1234)
  2. 进入项目目录,激活虚拟环境
cd zeroclaw-reflex-ui
.venv\Scripts\Activate.ps1
reflex run
  1. 浏览器打开 http://localhost:3000
  2. 在配置面板填写 LM Studio API 地址,点击「刷新模型列表」选择模型
  3. 点击「▶ 启动网关」,等待状态显示「✅ 运行中」
  4. 在对话窗口输入消息,开始聊天

⚡ 网关启动后可以直接对话,不需要每次重启 Reflex 服务。配置修改后才需要重启网关。

UI 功能一览

功能模块说明
▶ 启动网关在后台启动 zeroclaw gateway,自动注入 LM Studio 配置
■ 停止网关终止网关进程
↻ 刷新状态检测网关心跳、GPU 使用率、LM Studio 连接状态
刷新模型列表从 LM Studio API 获取当前加载的模型列表
💾 保存配置将 API 地址、密钥、模型 ID 保存到 .env 文件
System Prompt修改并写入 config.toml,自动重启网关生效
对话窗口支持 Markdown 渲染,回复显示 AI/用户气泡样式
🗑 清空对话清除当前会话历史记录

总结

本项目从零到能跑,主要经历了以下几个阶段:

  • 架构误解纠正: 从「调用 CLI 命令」改为「HTTP 调用 /webhook 接口」
  • Reflex API 适配: 解决了 5+ 个 0.8.x 版本的 API 变更问题
  • System Prompt 实现: 通过写入 config.toml + 重启网关的方式生效
  • 输出清洗: 过滤 ANSI 控制码和日志行,让 AI 回复干净呈现
  • Markdown 渲染: 使用 rx.markdown() 组件,AI 回复支持表格、代码块等格式

完整代码见 zeroclaw_reflex_ui.py,单文件约 350 行,涵盖了网关管理、对话、配置保存的完整功能。

🦀 ZeroClaw 本身的设计哲学:零依赖、极速、小体积。配合 Reflex UI,终于有了一个对人类友好的操作界面。

目录

  1. 前言:为什么要给 ZeroClaw 做 Web UI?
  2. 技术栈
  3. 第一步:环境准备
  4. 1.1 安装依赖
  5. 激活虚拟环境(Windows PowerShell)
  6. 安装依赖
  7. 1.2 初始化 Reflex 项目
  8. 1.3 放置主文件
  9. Windows 命令
  10. 提示覆盖时选 Yes(Y)
  11. 过滤 ANSI 终端控制码(颜色、粗体、日志前缀等)
  12. ZeroClaw 网关地址(固定,由 zeroclaw gateway 命令启动)
  13. 全局持有网关进程(跨请求共享)
  14. 第二步:理解 ZeroClaw 网关架构
  15. 2.1 正确的通信方式
  16. ❌ 错误 — agent 子命令不支持 --api-base 等参数
  17. ✅ 正确 — 通过 HTTP 与网关通信
  18. 2.2 网关启动方式
  19. 第三步:Reflex 开发关键踩坑记录
  20. 3.1 Button size 参数
  21. 3.2 自动 Setter 弃用
  22. 3.3 rx.foreach 里不能用 Python if/else
  23. ❌ 错误写法
  24. ✅ 正确写法 — 使用 rx.cond()
  25. 属性也一样
  26. 3.4 rx.input 不支持 on_submit
  27. ✅ 用 rx.form 包裹支持回车发送
  28. State 里定义:
  29. 3.5 rx.select 的正确用法
  30. ❌ 错误
  31. ✅ 正确
  32. 第四步:System Prompt 的实现
  33. 4.1 网关不支持动态传入 system_prompt
  34. 4.2 正确方式:写入配置文件
  35. 配置文件位置:C:\Users\{用户名}\\.zeroclaw\config.toml
  36. 添加这一行:
  37. 第五步:清理模型输出的 ANSI 控制码
  38. 过滤 ANSI 控制码
  39. 第六步:完整使用流程
  40. 每次启动顺序
  41. UI 功能一览
  42. 总结

更多推荐文章

查看全部
  • YOLO26 实时目标检测:关键架构改进与性能基准测试
  • 在鸿蒙游戏开发中接入 AI NPC 的体验与实践
  • Java 基本语法
  • VR 大空间技术在文旅产业的创新应用与体验升级
  • 主流 AI 编程工具对比与开发者选型指南
  • 7 款主流自动化渗透测试工具及特点分析
  • DeepSeek-R1 开源大模型推理优化实战方案
  • Go 语言结合 DeepSeek 大模型构建智能监控系统
  • Android 程序员就业现状分析与进阶建议
  • 国产 AI 大模型进入“群模时代”:技术突破与产业落地
  • TypeScript 高级技巧:提升代码类型安全
  • 渗透测试全流程思路与方法总结
  • Android 插件化框架 DynamicLoadApk 源码解析(下)
  • JWT(JSON Web Token)结构化知识体系
  • Windows 安装 Docker Desktop 实战指南
  • 6 款实测有效的论文降 AI 工具评测
  • AI 游戏 Agent 训练案例深度拆解
  • AI 助力六花直装 V8.3.9 自动化开发与测试
  • 解决 npm 安装 OpenClaw 时的 Git 报错问题
  • Stable Diffusion 3.5-FP8 环境配置与部署指南

相关免费在线工具

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online