DisPatch:面向本地 AI 智能体的自托管聊天应用

发布日期
2026年7月11日
更新日期
2026年7月11日
作者
Jacob Lloyd —— 项目完成后,在 AI 协助下撰写
阅读时长
约 12 分钟阅读

简单来说: DisPatch 三合一,完全跑在你自己的电脑上:一个用来和同样住在这台电脑里的 AI 助手聊天的应用、一个文件服务器,还有一个终端——你所有的设备随时都能用,没有任何数据流向云端。它的文件服务器能把任意设备上的文件搬到运行助手的那台电脑上,让助手能直接处理你上传的内容。在共用设备上,不输入 PIN 也能用一部分安全的助手;输入 PIN 之后就能解锁全部功能强大的助手。

DisPatch 这个应用是我做出来的,为的是不用再仅仅为了给自己的 AI 智能体发消息,就得维护一套 30 容器的 Rocket.Chat 部署。它最后变成了三合一——一个本地托管的 LLM 聊天应用、一个文件服务器,还有一个终端——我拥有的每一台设备随时都能用。它是一个约 720KB 的压缩包,免费,而且是我现在唯一在用的聊天前端。

TL;DR

  • 这是什么:三合一的自托管应用——为你自己那一批本地 AI 智能体(一个 OpenClaw 网关)准备的网页聊天、一个拖拽式文件服务器,以及一个真正的终端——在你局域网或 Tailscale 网络上的每一台设备上随时可用。
  • 费用:0 元。不用付 SaaS 席位费,不用注册账号。
  • 需要什么:Linux、Python 3.12+,以及 uv。如果想要智能体打字回复就需要一个 OpenClaw 网关——其他一切没有它也能用。
  • 最终得到什么:按机器人分组的会话线程、流式 Markdown、拖拽式文件服务器、应用内的编程智能体终端、可用 PIN 锁定的"安全模式"、供 cron 任务使用的推送 API,以及解压即可直接通过的 187 项测试。

最终会得到什么

在讲配置之前,先看看它实际工作时是什么样子。截图来自一份带假数据的沙盒副本——没必要让大家看到我真实的机器人日志。

1440px 宽度下的 DisPatch 桌面布局:左侧是一排彩色字母方块头像组成的机器人栏,中间是会话列表,右侧聊天面板显示着一份 Markdown 早间简报,里面有语法高亮的代码块和模型徽标。
桌面端,1440px:机器人栏、会话列表、聊天面板。简报和代码块都是渲染出来的 Markdown,不是截图粘贴上去的。
390px 宽度下的 DisPatch 移动端布局:同一段对话收进单一面板,底部是 Bots、Chats、Messages 的标签栏,每条消息都有复制、重新生成、删除操作。
同一段对话在 390px 下的样子。侧栏收进底部标签栏,每条消息都带复制 / 重新生成 / 删除操作。
设置 PIN 后处于安全模式的 DisPatch:机器人栏只剩三个已批准的机器人,底部有个琥珀色挂锁按钮用来解锁其余部分。
设置了 PIN 的安全模式:只显示被标记为"安全"的机器人,头像和解锁后的一样。这不是靠 CSS 过滤出来的——服务器压根就没把其余部分发过来。

它实际能做什么

  • 按机器人分组的会话线程,到哪儿都一样。聊到一半用手机打开,还是同一个线程、同一份历史记录,不用重新登录折腾。
  • 一个文件服务器——在任意设备和这台主机之间拖拽传文件。这是我用得最多的功能,下面会单独讲。
  • 一个终端(Terminex)——浏览器里的真实 shell,跑着一个编程智能体。同样在下面单独讲。
  • 流式 Markdown 回复,代码带语法高亮(自带复制按钮),图片/视频支持缩放灯箱,还能作为 PWA 安装。
  • 实时的"工作中"面板。点一下打字动画的小圆点,就能实时看到模型的工具调用和思考过程——这是在实时追踪智能体的记录文件,不是假装转个圈骗你。
  • 两级锁定。一个 PIN 把全功能智能体和一小部分安全、只提供常规信息的智能体区分开来——细节见下面的两级锁定小节。
  • 一个推送 API——POST /api/inject——让 cron 任务或另一个智能体能往某个线程里丢一条消息(或一张图片)。每日简报能不请自来地出现在你手机上,靠的就是这个。
  • 直接在线程里生成本地图片。让智能体帮你画张图,它就能调用本地的 ComfyUI 实例;结果会像其他媒体一样出现在聊天里,灯箱效果一应俱全。不走任何图像 API,也不上传给任何人。
  • 可选功能,默认关闭:一个 ComfyUI 面板,可以直接在聊天里启动或停止那个图像生成服务。

文件服务器

在 DisPatch 的所有功能里,文件服务器是我用得最多的一个。它是同一台主机提供的一个拖拽式文件区域:在任意设备上打开 DisPatch,把文件拖进去,文件就到服务器上了——按天分组,图片和视频都有缩略图。

DisPatch 文件服务器视图:顶部是上传拖放区,下方是按天分组的文件,包括图片缩略图和文件类型卡片。
文件服务器:顶部是拖放区,下方是按天分组的上传内容。从手机上看,整个流程就这么简单。

让它不只是"方便"的关键在于文件落在哪里:和智能体住在同一台机器上。从手机上传一张照片,几秒钟后任何智能体就能从磁盘上读到它——描述它、归档它、放到网站上、喂给图像工具。它是"我手上这台设备里的文件"和"我的智能体能处理的文件"之间的桥梁,中间既没有云盘,也不用接线。

那些不起眼但重要的细节也都处理好了:上传的文件在被缓冲之前会先检查大小(手机没法把主机内存撑爆),有一个总的存储上限,写入是原子操作,所以一次被打断的上传绝不会留下半截文件,而且文件存储和聊天记录用的是同一套滚动备份方案。还有一个配置开关,可以把整个文件服务器变成单向的、只能上传的投递箱——设备只能加文件,不能浏览——适合这种场景:能打开这个应用的人,比该看到它归档内容的人要多。

Terminex:聊天里的终端

另一个每天都在发挥作用的功能:DisPatch 能把一个真正的终端放进浏览器。它像其他机器人一样出现在机器人栏里,但打开它会把你接到服务器端的一个 PTY 上,通过 WebSocket 流式传输到 xterm.js——这是主机上真实的 shell 会话,不是模拟出来的。

DisPatch 终端视图:聊天应用内的 xterm.js 终端面板,带有启动、重启、停止控件,正在运行一个编程智能体 CLI。
Terminex:聊天应用里的服务器端 PTY,带启动 / 重启 / 停止控件。

我自己的配置会直接启动 Reasonix,也就是我跑在 DeepSeek 上、Claude Code 风格的编程智能体——助手中心页面把这套配置叫做 Terminex。这样一来,一个聊天标签页就变成了一整个编程智能体控制台:把任务交给它,看它干活,任何能打开这个应用的设备都行。和文件服务器搭配使用,整台主机在手机上就能完全操控——不管你人在哪儿,先把文件丢进去,再打开终端,告诉智能体拿它做什么。

  • 仅限解锁模式。安全模式下终端根本不存在,而且除非你手动启用(LOCAL_CHAT_TERMINAL),否则它一直是关闭的。
  • 会话保存在内存里。可以在应用里启动 / 重启 / 停止;服务器重启就会终止它。不存在藏在后台的持久 shell。

为什么要做这个

它取代的是一整套 Rocket.Chat 部署——一个数据库加一堆容器,就为了让一个人能和几个本地模型对话。它是能用,但为了"发文字、收文字、到处同步"这么点事,这套基础设施未免太重了。

而"为什么连聊天应用都要自己搭"这个问题更深层的答案是:一旦你的模型都在本地跑了,聊天界面就是最后一个还在引诱你投向云服务的部件。自己托管它,就意味着你的提示词、智能体的输出、家人的消息永远不会离开家里,而且 UI、网关、模型、图像生成这一整套流程,就算拔了网线也照样能用。既然你已经决定用 本地 LLM 了,一个托管在云上的聊天前端反而是那个格格不入的存在。

有一个设计选择值得单独提一下:DisPatch 刻意不设构建步骤。前端是原生 JS(ES 模块)加普通的 CSS/HTML。没有打包工具,没有 npm run build。我当初真正的需求是"以后 AI 智能体要能轻松改动它"。改一个 JS 文件,刷新一下,就完事了。如果重新来一次,我还是会这么选。

技术栈

内容
后端Python 3.12+、FastAPI + uvicorn、aiosqlite(单个 SQLite 文件,WAL 模式)、Pillow,用 uv 管理
后端规模backend/app/ 中约 7,200 行(仅 main.py 就约 3,600 行),另有约 2,700 行测试代码
前端无需构建的原生 JS + HTML/CSS——JS 约 4,900 行,CSS/HTML/service-worker 约 2,100 行
内置库marked、highlight.js、DOMPurify、xterm.js——不调用任何 CDN
数据代码之外,存放在 ~/.local/share/local-chat/:数据库、config.yamlsecurity.yaml、media/、files/,以及滚动的数据库备份

部署方法

以下内容浓缩自压缩包内的 README-SETUP.md(下载框见下方)。

unzip dispatch-2026-07.zip
cd dispatch
./install.sh
./run.sh

这样它就会在 http://0.0.0.0:8765 上启动,并打印出局域网和 Tailscale 的地址,方便你直接在手机上点开。接下来:

  • 编辑 ~/.local/share/local-chat/config.yaml(自动生成,说明文档在 config.yaml.example),让机器人的 id 和你的 OpenClaw 智能体 id 对上。它会热重载。
  • 可选:./install.sh --systemd 会生成一个用户级 systemd 单元。
  • 可选:从齿轮图标 → Security 设置 PIN。security.yaml.example 里说明了这个文件的内容,包括从主机外访问 /api/inject 需要的 api_token
  • 环境变量旋钮:LOCAL_CHAT_HOST/PORT/DATA_DIR/AGENT_TIMEOUT/MAX_CONCURRENCY,以及用来关闭可选面板的 LOCAL_CHAT_COMFY=0 / LOCAL_CHAT_TERMINAL=0

openclaw CLI 本身没有包含在内——DisPatch 假定某个它能连到的地方已经有一个 OpenClaw 网关在运行。还没有网关?应用照样能启动,只是会在日志里提醒你,除了打字回复以外的一切都能用:会话线程、inject API、文件服务器、PIN 和安全模式。我当初就是这么冒烟测试这个压缩包的——全新解压、uv sync、随便找个空闲端口、没有网关——打包好的测试套件跑出来是187 passed in 8.4s

输入的一条消息背后到底发生了什么

你打了点字,按下发送。它走的路径是这样的:

这是理想情况。真正花心思的工程都在处理不理想的情况:CLI 超时了、崩溃了,或者子智能体十分钟后才姗姗来迟地回答。四个独立的来源——实时监视器、CLI 的 JSON 返回内容、一个在每轮结束后重新扫描记录文件的核对器、以及一个再多追踪约 30 分钟的"追随者"——都汇入同一个投递漏斗,并按轮次去重(基于规范化文本加媒体指纹)。任何一条路径失败,回复依然恰好只会送达一次。就算 CLI 报错了,核对器也会先跑一遍,因为模型往往在挂掉之前已经把答案写进了记录文件。

两级锁定:普通模式与安全模式

这个区分很重要,因为解锁那一侧不是玩具。那些智能体是真能干活的:编辑源代码、把改动推送到我正在运营的网站上、运行各种工具、管理它们所在的那台机器。锁定那一侧只提供常规信息——依然是真正能用的,你可以提问、聊天、获得实际的帮助——但被刻意隔绝在那些权限之外。原因很简单:我家有个三岁小孩,家里那台手机也不是随时都在大人手上。想象一下,一个三岁小孩拿着我的手机、拥有完整权限能干出什么事——网站大概会被清一色的《汪汪队立大功》视频占领。所以安全模式其实是服务器强制执行的"防熊孩子"机制。还有一个小小的体验改进:锁定状态下的主屏幕现在显示的机器人头像和解锁那一侧一样,而不是过去那种字母方块占位图。下面是当一个请求到达、但还没输入 PIN 时会发生的事:

万一把自己锁在外面,有一份恢复码文件加一个可编辑的 YAML 兜底,不用去开工单求助。而且如果 security.yaml 在写入过程中损坏了,失败模式是"保持锁定状态",而不是"把一切都开放给局域网上的任何人"。这是刻意做出的设计决定。

踩过的坑

  • API 字段是 content,不是 textPOST /api/inject 要的是 {"bot_id": "...", "content": "..."}。这是集成时最常见的一个坑,连我自己都踩过好几次。
  • 不要把长消息当作 CLI 参数传递。内核把 argv 限制在 128KB(MAX_ARG_STRLEN);一条很长的回复会让子进程启动时因 E2BIG 崩溃。代码用的是 --message-file <tmpfile>——不要把它"简化"回参数形式。
  • 机器人 id 只在一个特定位置区分大小写。OpenClaw 的网关会在自己的索引里把会话键统一转成小写,但你的 id 可能是大小写混合的(DoxyDS_Brain)。区分大小写的查找会悄无声息地弄坏工作面板和延迟回答追随者,而其他一切照常运行——非常难发现。做法是先精确匹配,匹配不上再回退到小写。
  • CLI 只返回最后一块内容。智能体在工具调用之间说的任何话,只存在于它的记录文件里,不会出现在 CLI 的返回值中——这就是为什么需要监视器和核对器,也是为什么要特别处理智能体在等待子智能体时"这是我目前找到的内容"这类中间反馈。
  • 跨站 WebSocket 劫持通过 Origin 校验来阻止。两条 WS 路由都会拒绝那些 Origin 主机和 Host 不一致的握手请求;非浏览器客户端不发送 Origin,所以能正常通过。
  • SVG 被刻意设计为不能作为媒体提供。浏览器会把内联 SVG 当作同源脚本处理——把它当图片返回等于开了一个存储型 XSS 的洞。媒体响应改用带沙箱效果的 CSP 加 nosniff。
  • Service worker 更新后,旧标签页会卡在旧版 JS 上。表面看起来完全就是个回归 bug,直到你意识到那个标签页压根没接收到新的 worker。解决办法是监听 controllerchange 自动刷新。

如果你想复刻这个思路

就算你永远不会跑我这个压缩包,这套架构也值得借鉴。整体上是四个相互独立、都可以替换的部分:

  • 一个掌管历史记录的聊天后端:一个小型 web 服务器(这里是 FastAPI 加 WAL 模式下的单个 SQLite 文件),配一个用于实时更新的 WebSocket,和一个用来推送消息进来的普通 HTTP 端点。这就是"数据库加一堆容器"的完整替代品。
  • 一个智能体网关,后端通过 shell 调用或 HTTP 请求它——这里用的是 OpenClaw,但任何"输入会话 id + 消息,输出文本"的东西都行。让它保持独立进程,意味着即使智能体栈崩溃、升级,或者被整个换掉,聊天应用都能照常存活。
  • 网关背后的本地模型服务器(LM StudiovLLMOllama——这是网关要操心的事,不是聊天应用的事)。
  • 可选的媒体服务,比如用于图像生成的本地 ComfyUI,智能体把它当作工具调用,后端只需把输出结果当作媒体存起来。

不管用什么技术栈,可以带走的经验是:安全/锁定模式要在服务器端强制执行,绝不能靠 CSS;把"模型的回复恰好送达用户一次"当作一个真实的分布式系统问题来对待,而不是想当然地假设理想路径;如果你打算让 AI 智能体来维护前端,那就让它保持无需构建。

下载

仅限个人使用免费。如果它替你省下了一下午的时间,欢迎点旁边的咖啡按钮支持一下。


← 更多AI 与本地 LLM