一个人的 AI 工作台:把 agent 搬到手机上


title: 一个人的 AI 工作台:把 agent 搬到手机上
tags: 工程实践,Agent,架构

这篇是内部文档的公开版:抹掉了内网地址、主机名和私有仓库地址,保留了架构、取舍与教训。
全程零 npm 依赖、单机运行,能改代码、发文章、推微信、存云盘、备份上 NAS。

一个人的"AI 工作台":手机发一句话 → 沙箱里的 coding agent 干活 → 结果推回手机。
全部自建、零 npm 依赖、单机运行;能改代码、发文章、推微信、存云盘、备份上 NAS。


一、全景

                        ┌────────────── 手机(浏览器 / PWA / 微信)──────────────┐  
                        │  聊天、任务清单、工具卡片、附件、@ 引用文件            │  
                        └──────────────────────┬───────────────────────────────┘  
                                               │ HTTP + SSE  
                                   家庭内网 / WireGuard  
                                               │  
        ┌──────────────────────────────────────▼───────────────────────────────────┐  
        │  控制端主机(WireGuard 节点 A)                                   │  
        │  · SSH 隧道 -L 8093 → 沙箱 8099(局域网桥,供 局域网设备 访问)         │  
        │  · launchd 托管隧道;Worker 部署(wrangler)在这里                       │  
        └──────────────────────────────────────┬───────────────────────────────────┘  
                                               │ WireGuard 私有网段  
        ┌──────────────────────────────────────▼───────────────────────────────────┐  
        │  工作容器(WireGuard 节点 B,8GB / 256GB)                                       │  
        │  pi-gw:8099 ──spawn──▶ pi --mode rpc(每会话一进程)──▶ 模型 API          │  
        │  serve-web:8080(/workspace + /root/mdviewer 只读浏览 + /__fetch 取文)   │  
        │  watchdog / keepalive / supervisor / mem-guard(四层保活)               │  
        └──────────────────────────────────────┬───────────────────────────────────┘  
                                               │ 每 10 分钟 rsync  
                                 ┌─────────────▼──────────────┐  
                                 │ NAS 备份 + iCloud 破窗包     │  
                                 └────────────────────────────┘  

持久化分层(这是整套东西最关键的约定):

路径 普通重启 容器重建 说明
/workspace ✅ ✅ 持久卷,随 NAS 每 10 分钟备份
/root(用户文件) ✅ ❌ overlay 层,重建即丢 → watchdog 从 /workspace 回填
/usr/local、/etc ❌ ❌ 每次重置

二、四层保活(这部分比功能更值钱)

层 组件 职责
L1 supervisor(pi-gw-supervisor.sh) 每 10 秒探活 8099,挂了立即拉起
L2 keepalive.sh 每 5 分钟健康报告:sshd / wg0 / watchdog / 内存 / CPU 节流 / 磁盘,异常静默告警
L3 watchdog.sh(24 个段落) 启动即自愈:WireGuard 配置、sshd、serve-web 回填、mdviewer 克隆、pi-gw 回填、DSH 运行时、密钥恢复…
L4 mem-guard.sh(cron 每 5 分钟) cgroup 内存守卫:清孤儿 pi / 孤儿浏览器 / 回收 page cache

破窗包:每天生成一份自包含恢复脚本推 iCloud —— 灾备"一条命令"级别。


三、pi-gw(网关 + 前端)

3.1 架构

  • 进程模型:每会话一个独立 pi --mode rpc 子进程;上限 2 个,LRU 回收(上下文已落盘,重连即恢复)
  • 协议:行分隔 JSON。请求带 id;响应 {type:'response',id,success,data};事件 message_update / tool_execution_start / tool_execution_end / message_end / agent_settled
  • 前端:单文件 HTML + 原生 JS(ES5 风格,无框架无构建)。老浏览器解析失败=整站白屏,所以禁箭头函数/?./\u{}
  • 规模:网关 1801 行、前端 4001 行、33 个端点、26 项自动化测试、零 npm 依赖

3.2 队列语义(2026-11-05 重做)

场景 行为
空闲发送 新一轮(queued:false)
忙时发送(默认) 打断重开:先 /api/abort 中止当前轮,再作为新一轮发出
忙时长按发送 显式排队 follow_up(等这轮彻底结束)
排队面板 「清空排队」只丢队列不中止当前轮;方块按钮才中止这一轮

3.3 端点分组(33 个)

  • 会话/对话:health state sessions history send abort stream(SSE) queue clearqueue unqueue
  • 模型/上下文:models model default-model thinking compact stats
  • 文件:upload ls text files/** export find
  • 任务清单:todos(GET/POST;add/toggle/edit/del/clear)
  • 对外发布:knowly share blog ima ima/notes ima/url wechat
  • 杂项:sys ui-version

3.4 前端踩过的坑(全部有代码注释与绊线测试)

  1. 长按发送双计时器:pointerdown + touchstart 双绑 → 孤儿 timer → 每次轻点都弹 follow_up。现在二选一绑定 + 滑动 >10px 取消
  2. 状态不同源:pending 只听 SSE 的 idle 一帧 → 漏帧后永久"排队"。现在用服务端 busy/streaming 回写(带防抖)
  3. 服务端 busy 卡死:agent_settled 丢帧后永真 → 30 秒定时扫描,连续 3 分钟无输出即复位
  4. 正则死循环:同一个 /g 正则既 exec 又 replace → 页面冻死。循环内不复用正则
  5. 表情独占一行:装饰 emoji 打在气泡容器上 → 排在块级 <p> 前;改打首个子元素
  6. PWA 不重载文档:改了 HTML 用户可能几天看不到 → /api/ui-version 每 60 秒自检,变了亮"界面已更新"

3.5 发布

/workspace/ops/pi-gw/publish.sh --dry-run   # 看差异  
/workspace/ops/pi-gw/publish.sh             # 脱敏 → 泄露体检 → 推送  

仓库:私有仓库(publish.sh 一条命令发布)。
运行副本含密钥,绝不直接 push:知识库密码、IMA 知识库 id、四家后端域名、内网前缀、token 示例全部脱敏成 env/空串,每条替换带命中次数断言,末尾还有全目录泄露体检。


四、mdviewer(Markdown 编辑器 / PWA)

  • 版本 4.11,纯静态 PWA(md.example.com,GitHub Pages),零构建零运行时依赖
  • 本地优先:IndexedDB 文库 + localStorage 草稿,离线可用;支持原地保存(File System Access API)
  • 渲染管线:marked → DOMPurify → KaTeX / Mermaid / 代码高亮 → 快照导出
  • 公式保真(v4.11):渲染前把公式抽成占位符(跳过围栏代码/行内代码/价格误判),消毒后 katex.render 回填;导出 HTML 时等渲染完再快照,并把 katex.min.css + 20 个 woff2 字体内联(断网也正确)
  • 外部网址只读浏览(v4.10):?url= 或菜单「🌐 从网址打开」,走三道闸——取文安检(只 https/443、拒 IP 与内网名、逐跳复查、10s/2MB)、消毒严档(外链断 opener、远程图点击才加载、不给音频/mermaid)、状态隔离(不写草稿/文库/分享/发布,只读)
  • 测试:npm run check 共 103 项(含 12 项公式保护、14 项取文安检)

五、对外服务(Cloudflare)

服务 地址 作用
协作分享 Worker share.example.com R2 中转:URL 即密码的临时分享;写入闸门(只收 text/markdown、拒 Content-Encoding、8 位 id 白名单、2MB 上限)
取文端点 share.example.com/fetch-md 替浏览器取远端 Markdown。闸门与 serve-web 共用 lib/fetch-guard.js;限流用 Durable Object(KV 计数实测无效:读缓存 60s + 单 key 每秒 1 写)
静态站点 md.example.com mdviewer(GitHub Pages,git push 即部署)

沙箱侧 serve-web 提供 /__fetch 同源取文(LAN 用),guard 查找顺序:/root/mdviewer/lib/ → /workspace/ops/ → 同目录(持久区优先)。


六、测试体系

仓库 规模 内容
pi-gw 26 项 smoke 7(真起网关+桩 pi)、todo 4、ui-pure 6(从 HTML 抠纯函数跑真值表)、ui-invariants 9(结构绊线)
mdviewer 103 项 纯函数内核、正则护栏、分享/取文安检、公式保护、Worker 集成

分工要诚实:

  • ui-pure 是真验证(输入→输出)
  • ui-invariants 是便宜绊线(防"被改回某个形状"):会误报等价重构,也测不到运行期错误
  • 2026-11-05 的三个 bug(孤儿计时器、pending 不回写、正则死循环)这套测试在设计上测不到——要测必须真浏览器

七、安全假设(有意接受的代价)

项 现状 越界前必须补
pi-gw token 固定值,等价容器 root 失败退避 + 轮换 / OAuth
暴露面 家庭内网 + Mac SSH 隧道;--allow-ip 前缀白名单 TLS + 强鉴权 + WAF
FS_DENY 只挡直接翻文件(token/key 类) 真正隔离靠容器
速率限制 网关侧无(Cloudflare 侧有 DO 兜底) 令牌桶
并发 会话进程上限 2、内存 8GB 硬顶 内存水位保护
备份加密 密钥材料 AES 加密后随包推送 定期演练恢复

一句话:这是"个人威力工具",不是多租户服务。自有机器 + 内网里是杠杆,出边界就是风险敞口。


八、运维手册(常见故障 → 处理)

现象 处理
手机打不开 ① Mac 上 launchctl 隧道是否在跑 ② curl 127.0.0.1:8099/api/health ③ supervisor 日志
网关挂了 pi-gw-supervisor.sh 每 10 秒自动拉起;手动 reload-pi-gw.sh [延迟]
容器重建后功能缺失 watchdog 启动即自愈;手动兜底 /workspace/ops/pi-gw/restore.sh --force
页面改了没生效 看底部有没有亮「界面已更新」;否则 location.reload() 或彻底关掉 PWA 重开
内存吃紧 mem-guard.sh 每 5 分钟清孤儿;/api/sys 看 anon 与 cache 分别多少
改了 pi-gw 要发布 gen-doc.py → verify-embedded.py(必须 ALL OK)→ publish.sh
分享接口被刷 Worker 闸门 + DO 限流(30 次/分钟/IP);Cloudflare 面板的 WAF 需付费,Free 无入口

九、变更记录与教训

2026-11-05

  • pi-gw:队列语义重做(打断重开)、状态自愈三处、长按三修、IMA/博客/收藏链接、任务清单、界面版本自检、配色重构、测试 0→26、发布脚本脱敏
  • mdviewer:v4.11 公式保真(占位符方案)、v4.10 外部网址只读导入 + Worker DO 限流
  • 基础设施:watchdog 9b2 段自动回填 pi-gw、/workspace/AGENTS.md(清单协议)、安全假设成文

教训(按代价排序)

  1. 改前端要验"页面还活着",不只是验函数返回值——一个 /g 正则复用让整页冻死十几分钟
  2. 30 小时不崩不是防线好,是没动代码;一动手就连环翻车
  3. "修好了"与"用户看到修好了"是两件事——PWA 不重载文档,为此加了版本自检
  4. 测试要对得起"验证"二字:正则断言源码长相只是绊线,真验证要跑真值表或真浏览器
  5. 脱敏要成脚本:靠人记得"别把运行副本推上去"迟早出事

公开版整理:2026-11-05 | pi-gw 1801+4001 行 / 33 端点 / 26 测试 | mdviewer v4.11 / 103 测试 | 发布 24e9d0e