我的 LLM 会话让我困惑,所以我构建了 with-my-dashboards
AI 如何精简了我的数字生活
构建你自己的 Web 服务器
我通往更高效数字生活的道路,始于我让 Claude Code (CC) 为我构建一个 Web 服务器。它叫 with-my-dashboards (WMD),一个单文件的全栈 Web 应用,已经发展成了我整个桌面的控制面板。它的通用核心在 GitHub 上,你可以基于它构建自己的版本。
它始于阅读。长回答在 CC 里很难读,所以我让它把回答写进一个 markdown 文件,然后在浏览器里打开。CC 自己就能做到。它无法选择的是页面出现在哪里,而这就是 WMD 解决的问题:我希望每个页面都在一个已知的浏览器窗口里,位于我当前所在的工作区。
从那时起它不断成长。一个“个人代理”,也就是那种在你不在时出去替你办事的东西,并不是我想要的。但我确实喜欢这类代理承诺的便利,而 WMD 是我获得这种便利的重要部分。任何我发现自己频繁做的事情都会被精简。有时是一个小小的 ~/bin 命令,有时是 WMD 里的一个按钮或页面,有时只是 CC 为我遵循的一条规则,比如在修改文件前总是先备份。
当我从命令行启动 WMD 时,它会打开几个浏览器窗口,每个窗口里有很多标签页。我目前有三个关注领域,所以我在三个工作区之间工作。我唯一没能自动化的一步是把每个窗口移到它对应的工作区,但这只需要几秒钟。
关于视频:它们是屏幕录制,制作时 WMD 还叫 mdserve,里面的文字很小。打开一个面板,然后在按播放前进入全屏。YouTube 会根据播放器的大小选择画质,所以小播放器会得到模糊的画面。
一切都在标签页上
正如你在视频里看到的,我从工作区 2 启动 WMD。那是我用于控制、以及任何非编程项目的工作区。思路是让 WMD 引导整台机器。之后,我需要做的所有工作都在某个 WMD 浏览器标签页上。
当我想在 WMD 本身上工作时,我会启动一个新的 CC 会话,让它读取 WMD.md,它的文档就在那里。如果你没和 CC 或类似的编码代理(比如 Codex CLI 或 Gemini CLI)合作过:像这样的维护文档是由 AI 自己写的,来自你在其提示符处与它的对话。
浏览器标签页就是 URL。启动时 WMD 从一个 EDN 文件读取它们,而那个文件在我与 CC 的对话过程中会被修改。
一个标签页通过三种方式之一出现:
- 始终需要。这些通常是完整的程序,比如我项目的仪表板,或者像 Gmail 和 Slack 这样的普通网站,它们每次 WMD 启动时都会回来。
- 按需请求。我已经让 CC“把它放上去”。这通常是 markdown 文档。
- 由另一个标签页打开。另一个标签页上的按钮被按下,通常是为了更仔细地查看某些东西,比如日志。
每个标签页还会得到一个彩色圆点作为图标,这样我一眼就能看出它是什么类型的标签页。普通网站保留它们自己的图标,但它们同样是 WMD 的一部分:被记住并和其他标签页一起重新打开,CC 也能像放文档一样轻松地把一个网站放到正确的窗口里。思路是每个工作区只保留一个浏览器窗口。
会话
我经常打开十几个以上的 CC 会话,每个都在其项目工作区的一个终端标签页里。Sessions 标签页列出该工作区的会话,每行一个:它的标题、一行说明它现在在做什么,以及它的上下文有多满。每行还显示会话是忙碌、空闲还是在等我。

更多功能,以及你自己的 Windows Explorer
关于改进 WMD 的视频展示了用于显示内存使用量的“Refresh”,以及保存打开标签页的“Remember Tabs”。
Claude 为我遵循的一条规则是:在修改 markdown 文件之前始终先备份。这会让备份文件变得很乱,当然这些文件是可以清理的。
下一个视频展示了清理过程。
想象一下,每个工作区都有你自己可编程版本的 Windows 资源管理器。你刚才在上面的视频里已经看到了!每个工作区里至少会有一个这样的标签页。视频没有展示这一点,但点击文件会打开一个新标签页,显示该类型的文件。Markdown 文件最重要,所以它们按最近修改时间排序,最新的在前。
WMD 的许多按钮本身并不执行工作。它们会运行我 ~/bin 目录里的一个小命令,和我在 shell 提示符下输入的命令一样,也是 CC 可以替我运行的命令。视频里的 “Flotsam” 按钮运行的是 archive-flotsam,这个脚本会把备份文件和截图移到一个带时间戳的归档文件夹里。大多数按钮背后的命令只有几行 bash。少数按钮,比如记忆相关的 “Refresh” 和 “Remember Tabs”,则内置于 WMD 本身。
更少的终端
作为开发者或支持人员,你最终会到处开着终端,有些是临时的,有些则永久运行,它们会把桌面弄得乱七八糟。整理它们的方法有很多:tmux 或它更新的同类 Zellij、i3 这类平铺窗口管理器,以及在 macOS 上的 iTerm2,或为同时运行多个编码 agent 而打造的 cmux。
把我桌面弄得乱七八糟的终端大多是长期运行的进程:Web 服务器、构建监视器之类。这些现在都在后台运行,每个终端都被仪表盘标签页上的一排按钮(“Launch”、“Stop” 和 “Log”)取代,可以一次启动一个,也可以整组一起操作。“Log” 会在另一个标签页中打开输出,我用完后会关掉它。我只保留每个工作区一个带标签页的终端窗口,用于我的 CC 会话和偶尔的一次性终端会话。
tmux 和 i3 这类工具是在安排终端,而 WMD 则消除了对它们的需要,并顺带解决了两个问题。一个是就近性:这些控件就在同一个窗口的标签页上,和我正在处理的文档、仪表盘和日志在一起。另一个是定制化。通用工具给每个人的都是同样的东西,所以它并不完全适合任何人。而我仪表盘上的每个按钮,都是我向 CC 要求的,并按我的工作方式分组。
使用 runbook
我提到的那种偶尔的一次性终端会话,通常是一个 runbook,而且常常在另一台机器上。
有些工作需要由我而不是 CC 来输入命令,因为它们需要我的密码或控制台。对于这些工作,CC 会写一个 runbook:一个纯文本的 shell 命令文件,带有注释说明每一步为什么存在。我按 Ctrl-x n,下一步就会出现在我的提示符处,已经输入好,不按 Enter 就什么都不会运行。再也不用从聊天里复制命令了。这个工具叫 cmdq,它是一个 gist,一分钟就能装好。
每一步的输出都会被保存下来,供 CC 回读,我的进度也会被保存,所以一个任务可以跨越重启。当任务在另一台机器上时,CC 会把 runbook 复制过去,我登录后继续按 Ctrl-x n。每个 runbook 都是一个带日期的文件,和它的输出保存在一起,所以每台机器上运行过什么都有历史记录。
复杂部署
按钮不只用于小任务。我们的销售点系统(POSES)是五个 webapp(chat、till、prods、custs 和 pos),运行在一台生产服务器上,部署它们是一项有多个步骤的工作。它现在位于 WMD 中 POSES 仪表盘标签页的 Deploy 面板里,是五个按使用顺序排列的按钮:
- Plan 会逐个应用比较线上版本与最新代码(无论是否已提交),并显示一次部署会发布什么。
- Ship 会为每个有改动的应用构建并上传一个 uberjar,遇到第一个失败就停止。
- Release 实际上什么都不做!相反,它会打印出供人工部署者在服务器上运行的步骤。
- Record 会检查每个已发布的 app 是否真的在线,如果有一个不在线就大声报错,从而捕捉到服务器上漏掉的一步。然后它会记录哪些已经上线,这样下一个 Plan 就能根据这条记录工作,而不必查询在线服务器。这也是 Plan 知道生产环境中共享库版本的唯一方式,因为服务器不会说明。
- Confirm 会以只读方式再次运行该计划,并且应该找不到任何仍需部署的内容。它的日志结尾是:“Confirm 完成。如果上面的每个 app 都显示 'up-to-date',部署就完成了。” 然后它会重启 Ship 不得不停止的开发构建监视器。
每个步骤都有一个彩色圆点,显示它是否已运行以及运行得如何,还有一个用于查看其输出的“Log”按钮。
五个仓库加上一个共享库就是 polyrepo,而 polyrepo 有个名声:每多一个仓库,就多一样要启动、保持同步和部署的东西。POSES 标签页把这三件事都处理了:
- 一起运行它们。一个页面显示每个 app,每个都有自己的 Start、Stop、Restart 和 Log 按钮,而“Start all”会启动全部。每个 app 需要什么,比如数据库,会显示在它旁边,共享服务有自己的一行。
- 一个地址。在我的开发机器上,Caddy 把所有 app 放在一个地址后面,就像生产环境中的 web 服务器那样。因此开发环境的行为就像真实系统,app 并排运行,而不是分散在零散的端口上。
- 部署它们。Deploy 面板会逐个仓库算出需要发布什么,并跟踪共享库的哪个版本在线。
所以我不再犹豫要不要新建一个仓库。
生活小技巧
同样的习惯在离开桌子后也管用。五个例子,以及每个例子如何运作。
在沙发上阅读。当我说“把它放到平板上”时,CC 会把一个文档加入阅读列表,第二个特意做得很小的 web 服务器会把它显示在我的平板上。那个服务器只能显示列表和列表上的文档。WMD 本身只监听机器自己的内部地址,所以局域网上的任何东西都访问不到它。终端和编辑器页面在平板的服务器上根本不存在,因此我以后加到 WMD 里的任何东西都不会意外泄漏到网络上。网页也可以加入列表;它只是指向真实站点的链接。
大屏幕。说“把它放到大屏幕上”会把一个页面发送到电视上,它会在那里等到我下次坐下来阅读。这句话背后是 bigscreen,我桌面上的一个小命令:给它一个网址,页面就会到电视上。CC 在我要求时运行它,我自己也可以输入。电视实际上只是一台备用 Linux 机器的 HDMI 屏幕,我称这台机器为媒体机器。这胜过智能电视:媒体机器是一台普通计算机,所以它可以运行 web 服务器和完全可控的 Chrome。如果不能在沙发上滚动电视上的页面,那页面就没用,所以媒体机器运行它自己的小 web 服务器 tvremote,而我的手机就是遥控器。在阅读模式下,它可以上下滚动页面、放大或缩小文字、重新加载页面,并播放页面里的 YouTube 视频;同一个遥控器也能操作 Netflix。tvremote 会在一个单独的 Chrome 中显示页面,该 Chrome 有自己的配置文件,因为 Chrome 不会让程序控制你日常使用的那个。如果你有一台备用机器和一台电视,它
tvremote 能工作的原因,是 WMD 使用的同一个技巧,只不过用在两台设备上。在 WMD 里,我点击的页面和被驱动的浏览器是同一个:页面上的一个按钮向 WMD 发出请求,WMD 驱动那个浏览器。到了 tvremote 这里,有两个浏览器:手机的浏览器,以及媒体机器上的 Chrome。一个 Web 服务器夹在两者之间。当我在手机浏览器上按下一个按钮,这次按下会通过 Wi-Fi 传到 tvremote(媒体机器上的 Web 服务器)。tvremote 和那个 Chrome 都运行在媒体机器上,所以 tvremote 可以直接驱动它。于是,一次按下从浏览器到 Web 服务器,再到浏览器。某种程度上,媒体机器上的 Chrome 也在充当 Web 服务器,从 tvremote 接收命令。手机浏览器对电视没有任何控制力;它只能去请求一个拥有控制力的 Web 服务器。局域网上的任何设备都可以当遥控器,而我桌面上的 bigscreen 只是其中又一个。
同样的事再说一遍,这次用技术语言。媒体机器上的 Chrome 是带着自己的 profile 和 Chrome 的远程调试开关启动的。以这种方式启动后,Chrome 会在自己的某个端口上监听命令。它监听的命令是 Chrome DevTools Protocol。这个端口只在 localhost 上,所以局域网上的其他机器(包括手机)都够不到 Chrome。手机浏览器上的每个按钮都会通过 HTTP 调用 tvremote 上的一个端点。tvremote 也通过 HTTP 向 Chrome 的端口询问标签页列表。然后它向正确的标签页打开一个 WebSocket,并发送一条命令。该命令会运行一行 JavaScript 来滚动或调整文本大小,按一个键来播放和暂停,或者打开一个地址。Web 服务器无法独自命令浏览器。Chrome 之所以接受命令,只是因为它带着那个开关启动,而 WebSocket 只是 DevTools Protocol 用来承载命令的东西。
电子邮件和消息。当我需要写一封邮件时,CC 会直接把它起草到 Gmail 里,并留在我的屏幕上打开:一个新邮件,或者在正确会话末尾的一封回复。Slack 消息也是同样的方式。这背后没有电子邮件集成。Gmail 和 Slack 只是 WMD 浏览器里的标签页,已经登录,而 CC 通过 Chrome DevTools Protocol 在它们内部工作,这和 WMD 用来打开自己窗口的远程控制是同一套。
购物。网购又能用了。Lazada 和 Shopee 之类的商店会拒绝机器人。机器人很容易被识别。curl 和 wget 这类程序会在每个请求里报出自己的名字。它们不运行页面的 JavaScript,也不保留页面的 cookie。CC 自己的 web-fetch 工具看起来也差不多,所以商店会返回一个空页面或拦截页面。但 WMD 的浏览器是真正的 Chrome。于是 CC 在那里打开一个临时标签页,读取商品列表,比较价格,然后关闭标签页。对商店来说,那只是我在浏览。
跑腿。我的第二喜欢的事(排在电视遥控器之后):我需要从当地几家照相馆中的一家拍一张证件照。CC 给我做了一份单页指南,里面有要问的问题和规格,还有照相馆按顺序列出,每家都有一个按钮可以打开指向它的 Google Maps。当我一家一家骑过去时,下一家总在点一下就能到的地方。这个没法从家里提供:我在那里的 Web 服务器只能到达我自己的机器和我的局域网,而骑车在外时我的手机用的是移动数据。所以 CC 把它发布到我在互联网上租用的 Web 服务器上,也就是托管这个博客的那台,作为我自己域名下的一个临时页面。对 CC 来说这只是一条命令,而且这个页面不需要登录就能看。
明天试试这个
你不需要从 WMD 开始。下面每一条都是可以对你自己的编程代理说的话,而且每一条都独立成立。
- “别在这里给我长篇回答。把它们写到一个 markdown 文件里,并在我的浏览器中打开。” WMD 就是从这里开始的,而且完全不需要 Web 服务器。
- “在修改文件之前,总是先备份它。” 这只是你代理的规则文件里的一行,你就再也不会丢版本了。
- “对此运行 falsifier。” falsifier 是一种 skill(你的 agent 加载的指令文件),用于检查 spec、请求或代码中的矛盾、隐藏规则和错误假设,并报告一份简短的、按优先级排序的待修复项列表。
- “给我要按 runbook 运行的命令。” 那意味着一个文件,而不是聊天里的列表。要逐步执行它,cmdq 就是 runbooks 部分里的 Ctrl-x n 工具。再也不需要输入命令,或复制粘贴它们。
- 想要浏览器里不止有 markdown?with-my-dashboards-starter 是 WMD 的通用核心,可以直接在上面构建你自己的。
- 有一台闲置机器和一台电视?tvremote 就是上文“The big screen”里的手机遥控器。把它安装到闲置机器上,在手机上打开它的页面,你就能在电视上看 Netflix、YouTube 和阅读内容。阅读针对的是你的 agent 发来的页面:它的 bigscreen 命令会把页面放到电视上,你可以在沙发上用手机滚动页面。
结论
AI,或者更确切地说,是它能快速构建的软件,终于让我摆脱了控制计算机时的大部分摩擦。
想象你作为开发者或生产支持人员到新岗位上班,被告知从命令行运行一样东西。该角色需要的一切都出现了:仪表盘、日志、runbook、文档,各归其位。每个角色都可以有自己的 WMD。长久以来想要的、开箱即用式的岗位入门,终于变得可行。
下一阶段当然是,把其中一些事情放在循环里做,而不需要人来驱动每一步。这在软件开发中已经发生:agent 做多次尝试,保留最好的。现在有两个即将推出的产品就是这样工作的。两者都采用演化式方法。
samizdat 是面向 Clojure 的 agentic harness,它把较大的任务拆分给多个 agent,每个子 agent 都会拿到一份契约以及作为其 spec 的测试。多个尝试可以并行运行,获胜者通过根据经过核验的证据对每次尝试进行排名来选出。循环本身是一个写成数据的状态机,agent 可以在其运行时重写它,但要在 validate-and-rollback 门禁之后。
simmis 是一个自托管应用,人和 agent 共享同一份记忆,包括代码仓库,其核心思想是低成本分叉。agent 对共享知识、文档或代码所做的每一处更改,都会落到它自己的写时复制分支上,因此 agent 可以并行地对同一件事做多次尝试。谁拥有合并权限,无论是人还是另一个 agent,都会审查差异并合并值得保留的那些。这正是让 agent 在循环中处理软件变得更安全的原因。有两样东西不进入分支:聊天,人和 agent 在其中交谈并提出工作需求;以及具有外部影响的操作,例如发送电子邮件,分支无法撤销这类操作。simmis 是早期公开发布版本;概览和路线图在 simm.is。
瀑布式会预先规划一切,然后构建;敏捷式则小步构建,并在学习过程中调整方向。瀑布式的弱点在于往回走:一旦开始构建,修改 spec 的代价很高。演化式方法兼顾两者,并消除了这个弱点。每次尝试都可以像瀑布式那样从一份 spec 开始,而尝试所揭示的内容会在下一轮之前回到 spec 中,就像敏捷式那样。samizdat 给每个子 agent 一份契约和作为其 spec 的测试,并根据证据对尝试进行排名;在 simmis 中,尝试留在分叉上,直到拥有合并权限的人或 agent 保留好的那些。而当代码的构建以小时而不是月计时,往回走的代价就很低。
术语表
- Agentic harness:在循环中运行 AI agent 的软件,把任务交给它们,检查它们的工作并反馈结果,使它们无需人驱动每一步也能持续工作。
- 敏捷:一种以短步骤运行软件项目的方式,每一步都交付可工作的成果,因此计划可以随着你了解更多而改变。它在很大程度上取代了瀑布式。
文章标题:AI 简化了我的数字生活
正文:
- Babashka:一种快速启动的方式,让你像写日常脚本一样运行 Clojure,也就是那些你原本会用 shell 语言写的小程序。babashka.org
- Bot:一种背后无人操作、自动访问网站的程序。搜索引擎用 bot 来索引网络,另一些则用来抓取价格或内容。许多商店会屏蔽它们。
- Caddy:一个易于配置的 web 服务器。在这里,它充当唯一的入口,把每个请求转发给正确的应用。caddyserver.com
- Chrome DevTools Protocol:Chrome 内置的远程控制接口。开发者工具以及像 WMD 这样的程序用它来打开标签页、读取页面并点击它们。
- Clojure / ClojureScript:一种属于 Lisp 家族的编程语言,在处理数据方面很受欢迎。ClojureScript 是同一门语言编译后在浏览器中运行。
- 上下文(Context):AI 模型在一次对话中能同时看到的一切:到目前为止的消息,以及它读取过的文件和结果。它的容量是固定的。当它被填满时,CC 会把较早的部分压缩成摘要,细节就会丢失。
- 写时复制分支(Copy-on-write branch):一份只存储变化内容的廉价数据副本。它可以被保留、合并回去或直接丢弃,而不会影响原始数据。
- curl:一个小型命令行程序,用于发送单个 web 请求,方式与浏览器类似,但没有窗口。
- 仪表盘(Dashboard):WMD 自己提供的一个标签页,与 Gmail 这类外部网站相对。它显示正在发生的事情,许多还带有可执行操作的按钮,比如启动一个服务器。GitHub 上 WMD 的通用核心自带几个通用的仪表盘;大多数是我自己的,为某个项目或我的控制工作区而构建。
- EDN:Clojure 的数据文件格式,是 JSON 的近亲。edn-format
- 端点(Endpoint):web 服务器上的一个地址,负责一项工作,比如“向下滚动”。网页上的一个按钮往往只是对某个端点的一次调用。
- HTTP:浏览器和 web 服务器用来相互通信的语言。
- 局域网(LAN):你家里或办公室内部的网络,通常就是你的 Wi-Fi,把你自己的设备互相连接起来。互联网上的设备无法访问进来。
- Localhost:一台机器与自己通信。只监听 localhost 的 web 服务器无法被其他任何东西访问,即使在同一个家庭网络内也不行。
- Markdown:带有少量简单格式标记的纯文本,比如用 # 表示标题、用 * 表示强调。易于书写,也易于转换成网页。
- 多仓库(Polyrepo):代码分散在多个仓库中的项目,通常每个应用一个仓库。相反的是单仓库(monorepo),所有内容都放在一个仓库里。人们常用单体(monolith)和微服务(microservices)来描述这种区别,但那些词说的是运行中的软件如何拆分,而不是代码如何存储。POSES 两者兼有:多个独立的应用,每个都在自己的仓库里。
- REPL:运行中程序内部的一个实时会话,你可以在其中输入代码并立即看到结果,在程序运行时读取和修改它。Epupp 把一个 Clojure REPL 放进一个实时浏览器标签页,这样代理就能看到页面实际绘制出的内容,而不只是发送给它的 HTML,甚至可以修改页面,直到刷新才恢复原状。clojure.org
- 规格(Spec):书面规格说明,对软件必须做什么的精确描述,在写代码之前写就。
- 终端(终端标签页):一个文本窗口,你在其中向计算机输入命令,而不是点击操作。在终端程序中,每一个都是一个标签页,与浏览器的标签页相互独立。
- 终端程序(带标签页的终端窗口):容纳你的终端的程序,以标签页形式放在一个窗口里。我用的是 Ptyxis;其他还有 Linux 上的 GNOME Terminal 和 Konsole、macOS 上的 Terminal 和 iTerm2,以及 Windows Terminal。
- tmux:一个让终端会话在后台持续运行、并允许你把一个终端窗口分成多个的程序。Zellij 做同样的工作,而且更容易上手。
- Uberjar:一个单一文件,把 Java 或 Clojure 程序及其所需的一切打包在一起,可以直接在服务器上运行。
- Waterfall:运行软件项目的旧方式,严格分阶段进行:先写完整的规格说明,再设计,再构建,再测试,几乎不回头。它之所以失宠,是因为规格中的错误发现得晚,修复代价高昂。
- Webapp:在网页浏览器中使用的应用。你看到并点击的部分在浏览器本身运行,它与存储数据、承担繁重工作的 web 服务器通信。
- WebSocket:两个程序(通常是网页和 web 服务器)之间保持打开的连接,因此任一方随时都能发送消息,而不是一次请求、一次回复。
- wget:一个下载网页或文件的命令行程序,很像 curl。和 curl 一样,它在每个请求中都会报出自己的名字,因此网站能把它和浏览器区分开。
- Workspace:一个完全独立的桌面,有自己的窗口,用一次按键切换。Windows 称之为虚拟桌面,macOS 称之为 Spaces。
有评论或问题?给我发邮件至(开启 JavaScript 以查看我的地址)。
发布于:2026-10-08
标签:Clojure AI