0%

【本文由 AI 辅助完成】

最近用 topcoat 0.6 写了一个本地单机便笺应用 Stickies:增删改查、置顶、标签、全文搜索、回收站(30 天自动清理),外加多窗口 WebSocket 实时同步。项目本身还在实验期,但开发过程几乎全程在跟框架的”年轻”搏斗,积累的经验值得单独成文。

topcoat 是 tokio-rs 出品的一个很年轻的 Rust 全栈框架:服务端渲染 + $(...) 运行时表达式做客户端响应式,#[page]/#[route]/#[shard]/#[procedure]/#[component] 五个宏驱动。文档稀少、API 不稳定,很多行为要靠读源码和试错。本文按技术层组织,每个坑给出”现象 → 根因 → 修法 → 通用教训”。

什么是 topcoat

先说清楚这个框架的模型,后面看坑才有上下文:

  • SSR + 客户端响应式:页面在服务端渲染成 HTML,$(...) 表达式在服务端求值、序列化成 JS 塞进 HTML,浏览器端靠这段 JS 建立响应式绑定。信号(signal)变化时,绑定的 DOM 自动更新,或者触发服务端重新渲染某个区块。
  • 五个宏#[page] 定义页面路由;#[route] 定义子路由;#[shard] 定义”服务端重查的视图块”——参数一变,浏览器请求服务端重渲染整块;#[procedure] 定义客户端可调用的服务端过程(类似 RPC);#[component] 复用 UI 片段。
  • 现状:0.6 版本,README 和示例都很少,API 尚未稳定,破坏性变更随时可能发生。用它做生产项目要谨慎,但作为”Rust 全栈还能长什么样”的探索,值得一试。

1. $(...) expr DSL 的词汇表边界

expr 是服务端求值、序列化成 JS 塞进 HTML 的 DSL,词汇表很小:只有 f64Stringbool 和信号/事件的 surrogate。大多数坑源于”想用 Rust 世界的类型/能力,但 DSL 里没有”。

1.1 u64 传参要过 f64

#[procedure] 参数里只有 f64 数字。便笺 id 是 u64,于是 signal id = note.id as f64;,服务端再 id as u64 转回。丑,但绕得过去。教训:DSL 边界上妥协类型,进服务端立刻还原,并记录下理由,避免后人看到 as f64 时把它”修”回去。

1.2 没有定时器 → raw! 兜底

防抖 500ms 自动保存需要 setTimeout/clearTimeout,词汇表里没有。用 raw!(js_string, rust_fallback) 塞原生 JS:

1
timer.set(raw!("setTimeout(${_save}, 500)", 0.0));

${_save} 是插值,其余是字面 JS。教训:把框架没提供的原生能力包在 raw! 里,${...} 负责桥接

1.3 raw! 的 JS 必须是表达式(两次翻车)

第一次把 raw!("if (...) { clearTimeout(...) }", 0.0); 当独立语句写,展开后是裸块 { into_surrogate(0.0) },编译报 E0308 expected (), found F64Surrogate。改成 let _cleared = raw!(...) 后能编译了,但浏览器Unexpected token 'if'——if 是语句不是表达式,出现在 let x = if(...) 里直接语法错误。最终包成 IIFE:

1
let _cleared = raw!("(() => { if (${timer}.get() !== -1) { clearTimeout(${timer}.get()); } })()", 0.0);

教训:raw! 的 JS 必须是表达式(IIFE / 逗号表达式都行),并且”能编译 ≠ 能跑”——这类错只在真实浏览器里暴露。

1.4 procedure 参数走 dehydrate():裸值要包 surrogate

自动保存广播需要把”来源窗口 id”传给 procedure。第一次写 let cid = raw!("window.__stickiesClientId || ''", String::new()),浏览器报 r.dehydrate is not a function。查了 runtime 源码才明白:浏览器端调用 procedure 时对每个参数调 .dehydrate() 序列化,raw 出来的是裸字符串,不是 surrogate。用 cx.hydrate(...) 包一下:

1
let cid = raw!("cx.hydrate(window.__stickiesClientId || '')", String::new());

教训:DSL 里任何要跨进程的值,都得是 surrogate 形态cx.hydrate 是造 surrogate 的万能入口(数字/字符串/bool 直接传字面值即可)。

1.5 闭包捕获循环变量 → E0597

标签 chips 行想在 for tag in tags 里给每个按钮写 @click=$(|_e| tag_id.set(tid))let tid = tag.id as f64; 在循环体内声明,闭包捕获它 → 借用生命周期 E0597(handler 存进 view 活得比局部变量久)。绕法:把 id 放进按钮 value 属性,handler 用 raw + parseFloat 读:

1
<button value=(tag.id) @click=$(|_e: Event| raw!("${tag_id}.set(parseFloat(${_e}.target.value))", 0.0))>

教训:循环/临时变量别想捕获进 handler;把值放 DOM 属性、事件里读,是通用出路。

2. 数据层:toasty 0.7 的现实

2.1 push_schema 非幂等 → 手动建表

toasty 0.7 的 sqlite 驱动 push_schema 不幂等,重跑会坏。绕法:不用它,在 db.rs 里手写 CREATE TABLE IF NOT EXISTS,建表语句和模型手维护同步。教训:新 ORM 的”自动迁移”别轻信,先验证幂等性

2.2 查询 DSL 限制 → repository 层内存过滤

搜索(LIKE 标题+正文)、标签过滤、回收站清理的表达式在 toasty DSL 里要么不支持要么写不出来。约定:所有数据访问收敛到 repository 层,需要复杂查询时先全量拉回、在 Rust 里过滤/排序。数据量小(本地单机)时这是合理的务实选择。

2.3 时间用 i64 epoch-ms

DateTime 类型在 toasty sqlite 驱动里未验证,直接用 i64 存毫秒,排序和 30 天清理阈值都好写。教训:框架类型未验证时,用最朴素的可比较类型,别赌

2.4 每请求拿句柄

toasty 的语句要 &mut Db,而 handler 只有 &Cx。用 app_context::<Db>(cx).clone()——Db 是池句柄,clone 便宜。

3. SSR + 响应式:组件 / shard 生命周期

3.1 shard 参数变化触发服务端重查

搜索框 @input 更新 query signal,note_wall(query, tag_id) shard 参数一变,浏览器请求服务端重渲染整块列表。这是”即时过滤”的实现基础。教训:shard 是”服务端重查的视图块”,用它做列表/过滤天然合适

3.2 shard 重渲染替换内部 DOM:隐藏字段会丢

ws.js 需要在所有 POST 表单里注入隐藏的 client 来源 id。第一版在 DOMContentLoaded 注入一次——但 note_wall 是 shard,搜索/标签过滤会重渲染出全新表单,注入字段丢了。提交时 source 为空 → 广播回来自己窗口不认 → 自我 reload。修法:改成提交时注入

1
2
3
4
5
6
7
8
document.addEventListener('submit', (event) => {
const form = event.target;
if (form && form.querySelector && !form.querySelector('input[name="client"]')) {
const input = document.createElement('input');
input.type = 'hidden'; input.name = 'client'; input.value = myId;
form.appendChild(input);
}
});

教训:凡是”外部脚本往 SSR 输出的 DOM 里塞东西”,都要考虑该 DOM 会被 shard/组件重渲染替换;在事件发生时注入,而不是在页面加载时注入。

3.3 表单按钮 stopPropagation

卡片整块 @click 进编辑态,卡片上的”置顶/删除”按钮会冒泡触发编辑态。给按钮所在 <form>@click=$(|e: Event| e.stop_propagation())。小坑,但容易漏。

4. 多窗口同步:WebSocket + 广播

4.1 自我广播竞态:核心一课

自动保存(procedure)广播 note_updated编辑窗口自己会收到location.reload() → 打断编辑。第一版 hack 是”表单提交后跳过下一条消息”(skipNext),不优雅且漏编辑场景。正解:给每次页面加载一个来源窗口 id,广播带 {source, type},客户端只刷新 source ≠ 自己的消息。

教训:任何”广播全量变化”的系统,必须给消息带来源身份,接收方排除自己;用”跳过下一条”这种时序 hack 迟早漏。

4.2 窗口 id 别用 sessionStorage

第一版把 id 存 sessionStorage 想跨刷新稳定——但复制标签页共享 sessionStorage,两个窗口会互认成”自己”,彼此跳过同步。改成每次页面加载 Math.random() 生成:id 只需在单页生命周期内稳定(广播在旧页面收到、导航前比对即可),不需要跨刷新。

4.3 表单 PRG 与广播的竞态

表单提交 → PRG 导航,同时广播到达。时序对就没事(消息由旧页面收到并比对),但”提交瞬间在表单里补隐藏字段”(3.2 的修法)必须做对,否则 source 为空、所有窗口都 reload。

5. 资源打包与样式

5.1 cargo build 不重建 assets:主题”改了不生效”的假象

target/debug/assets(runtime JS、tailwind.css、ws.js)由 topcoat dev 生成,cargo build 不重建。第一次集成主题时改完 build.rs 发现产物没变,以为是 @theme 语法不支持,其实是缓存旧 bundle。教训:先确认”改动到底有没有进产物”再怀疑框架;删掉 target/debug/assets 重建即真相大白。

5.2 topcoat ui 主题需要 Tailwind v4 CLI

topcoat ui init 装的 neutral 主题(styles.css)用了 Tailwind v4 的 @theme inline/@source/@custom-variant。topcoat-tailwind 默认 CLI 版本是 4.3.2,build.rs 里 .input("styles.css") 即可让 token 正常产出;页面再改用 bg-background/text-foreground/border-border 等 token 类。教训:@theme inline 是 v4 语法,确认构建管线用的 CLI 版本

5.3 Tailwind 只生成扫描到的类

内容扫描决定产物,没在源码里出现的类不会进 CSS。搜索框、卡片这些动态渲染的类名都在 .rs 里,扫描能覆盖;但任何”运行时拼出来的类名”都会丢——别用字符串拼接类名。

6. 测试策略

6.1 无 TestClient → 先 binary 集成,后迁 Playwright

topcoat 0.6 没有 TestClient。第一版按 spec 做了 binary-level HTTP 集成测试(起真实二进制 + reqwest 打 HTTP),验证 CRUD/回收站/标签。后来按需求全部迁到 Playwright:E2E 一律走真实浏览器,Rust 侧只留 repository 单测(内存 SQLite),删掉 reqwest dev-dep。教训:“E2E 归浏览器、单测归纯逻辑”的分层更干净,HTTP 层测试在浏览器 E2E 覆盖后是重复劳动。

6.2 E2E 直连 dev 服务

Playwright webServerreuseExistingServer: true 直接连 topcoat dev 的 3000 端口;无服务时才自动拉起带隔离测试库的实例。注意:webServer 先于 globalSetup 启动,删测试库只能在应用启动时做,所以应用在测试环境变量下先删库。

6.3 E2E 常见坑

  • strict mode violation:多张卡片有同名文本(每个卡片都有隐藏的”已保存”),getByText('已保存') 解析到多个元素 → 断言必须限定在卡片作用域内。
  • WS 自我 reload:见 4.1。
  • Windows 特有.cargo-build-lock 残留(删掉即可)、运行中的 exe 占用 target/debug/stickies.exe 导致 rebuild 报”拒绝访问”、CLI 要用全路径。

已知局限

  • toasty 自动迁移:仍用手写建表,等 toasty 0.7 的 push_schema 幂等后再考虑迁移。
  • WS 事件结构:目前是 {source, type} 最小 JSON;将来可以带 note_id/note 做增量更新,替代”整页 reload”。
  • 深色模式:neutral 主题自带 .dark token,但页面没接 dark 类切换,属未启用能力。

结语

topcoat 最大的特点是”思路新、文档少”,所以开发过程基本是”读源码 + 试错”双轨并行。对后来者的三条经验:

  1. 先读 runtime 的 browser/TS 源码再猜行为——dehydrate()cx.hydrate、shard 重渲染这些机制,源码一读就通,猜则踩坑。
  2. 能编译 ≠ 能跑:expr/raw! 生成的 JS 错只在真实浏览器暴露,尽早跑 E2E。
  3. 把”绕行”当成一等决策:DSL 限制、类型妥协、测试分层,每条都写进 spec/ADR,让未来的改动知道”这里为什么长这样”。

参考链接:

【本文由 AI 辅助完成】

上篇《Everyday CLI开发经验谈》记录到 v0.10–v0.11 时代。此后两周,Everyday CLI 从 v0.13.0 迭代到 v0.18.0,跨了 11 个版本点。本文按「版本时间线 + 主题深挖」双线梳理这段演进,最后单独成章,分享这期间最有代表性的一类经验——在 WorkBuddy 沙盒不稳定性下的工程实践。

版本时间线

版本 日期 主要内容
v0.13.0 08-09 WebDAV 同步(D001-D003)
v0.14.0 08-10 auth 环境变量凭证回退(R020)
v0.15.0 08-10 MCP server over stdio(F014)
v0.16.0–2 08-10 tracing 分级日志(F015)
v0.17.0 08-14 daemon 守护进程(F016)
v0.17.1 08-14 日期序列 ID + PID(R021)
v0.17.2 08-18 task 命令执行 + cron(F017)
v0.17.4 08-18 task/config/output 重构
v0.17.6 08-19 rss digest –since(F008 修订)
v0.17.7 08-21 日历时区修复(Utc → Local)
v0.18.0 08-24 mail search –cached + mail gc

两周 11 个版本点,节奏相当密集。以下按主题展开。

同步与凭证

v0.13.0 引入 WebDAV 同步(src/modules/sync/,D001-D003),跨设备数据打通,这是 daily 数据能跟随 agent 迁移的基础设施。

v0.14.0 的 R020 补强了凭证体系:[auth] env_credentials = trueEVERYDAY_ENV_CREDENTIALS=1 双通道,变量命名 EVERYDAY_<MODULE>_<ACCOUNT>_PASSWORD,读取链 keyring → env → 报错。这条规则解决了无钥匙串环境下 agent 取不到凭据的问题——CI 或容器里没有系统 keyring,环境变量回退是刚需。

MCP server over stdio

v0.15.0(F014)让 everyday 可直接作为 MCP server 被 WorkBuddy 等客户端拉起。这个版本定下的契约后来成为所有模块被 agent 稳定调用的基础:

  • stdout 专供 JSON-RPC,任何日志、调试信息一律走 stderr;
  • JSON 输出三系形状 {"_log"} / {"_warning"} / {"_error"} 不可变。

日志与可观测

v0.16.0–2 落地 tracing 分级日志(F015),默认 WARN 静音。原因很实际:agent 会话里如果默认 INFO,调试噪音会淹没真正的问题;需要细粒度追踪时再显式调级别。

daemon 守护进程

v0.17.0(F016)是走向「个人 Info Agent」的关键一步。daemon run [--once] / daemon status,周期性同步 timeline + mail + rss,状态落在 ~/.config/everyday/daemon-state.json。设计上值得注意的两点:

  • pid 存活检测防重入:同一时刻只允许一个 daemon 实例;
  • 同步 = timeline run_sync + mail 全 folders 增量(每周期 IMAP LIST)+ rss。

编号体系与 task 子系统

v0.17.1 定稿 R021 编号规则:id = {前缀}{YYYYMMDD}-{当日序号}(n/t/b/m/ev/mc/ri),按天重置、每前缀独立计数;旧格式共存不迁移。

v0.17.2(F017)给 task 模块加入命令执行与 cron 能力,v0.17.4 又对 task/config/output 做了一轮重构(Scheduler 上下文)——这是 v0.17.x 里真正的 SOLID 依赖反转对象。

rss 与日历

v0.17.6 修订 F008:rss digest --since 支持时间窗口聚合。

v0.17.7 修了一个隐蔽的日历时区 bug:cal list 对 UTC 存储事件直接输出 UTC 字符串、不转本地,导致日程显示早 8 小时。修复是 format_date_perhaps_timedate_perhaps_time_to_naiveUtc 变体先转 Local 再输出。更深一层是服务端数据缺陷:QQ 日历对手动创建的事件按「钟面时间直接标 Z」存储(naive-as-UTC),对同步导入事件存正确 UTC,两者从 ICS 无法区分——这个只能留作已知限制。

mail 收尾

v0.18.0 补齐 mail 模块最后两块拼图:mail search --cached(M006,信封缓存内搜索)与 mail gc(M007,缓存回收)。

开发经验:WorkBuddy 沙盒下的工程实践

v0.17.x 期间踩得最深的一类坑,来自开发环境本身——WorkBuddy 沙盒在 Windows 上对命令执行有各种不稳定性。沉淀出三条铁律:

1. 输出落盘 + 哨兵行,日志是唯一事实来源

1
cmd > log 2>&1; echo "EXIT=$?" >> log

vitest 等工具经管道输出时退出码会误报(管道吞掉真实 exit code);\r 进度条刷新符在管道捕获层被吞,工具返回空但命令其实已跑完。做法:所有测试/构建/安装命令重定向到日志文件,末尾追加哨兵行,只以日志为判断依据,绝不凭管道退出码下结论。

2. 工具空返回 ≠ 命令失败,先读日志再决定

「命令执行完 ≠ 工具感知到」。工具返回空或超时,先读日志、再查进程是否存活(tasklist),最后才决定是否重跑——盲重跑不仅浪费,还会掩盖真实的间歇性失败。

3. 怀疑命令执行错误时,用 Python subprocess 落盘验证

工具层报 Permission denied / os error 5 / 空返回,先写一次性 Python 脚本用 subprocess 把 stdout/stderr 落盘跑一遍。沙盒文件层写入受限而命令本身可正常执行,是 Windows 沙盒下的常见情况——工具报错不一定是命令真的失败。

附一个 Rust 专属边注:多次强杀 cargo 后,沙盒文件层可能持有 .cargo-build-lock 句柄,导致所有 target 构建失败(WinError 5)。cargo clean 直接根治,比重启应用快得多。

现状与下一步

当前 v0.18.0:13 个模块 + MCP server + WebDAV 同步,350+ 测试,冷启动 <100ms。下一步重心在 timeline 聚合的深化,以及 system/fs/network 模块缺口的评估。

参考链接:

【本文由 AI 辅助完成】

一次完整的「看图建模 → 视觉验证 → 云端发布」体验记录:把一张东方明珠电视塔的照片,变成可交互的体素 3D 模型。

最近体验了 DeepSeek-V4-Flash-Vision-Exp,一个有视觉能力的模型。我给它出了一个有点意思的端到端任务:只看一张东方明珠电视塔的照片,用 three.js 做一个体素(voxel)风格的 3D 模型,再用 Playwright 截图做视觉验证,最后部署到 Cloudflare Pages,并加上可交互的相机。 这篇文章记录这个过程里模型的表现、我的观察,以及一些值得注意的细节。

任务与背景

东方明珠电视塔的造型很有辨识度:三根斜柱撑起一座大球,向上是细细的中柱,柱上挂着一排水平观测舱,顶部是小球和逐渐收窄的天线。把它做成体素的样子,就是用一堆小立方体堆出这个剪影。

立项约束:

  • 技术栈:pnpm + TypeScript + Vite
  • 不使用任何 Web 框架(three.js 是库,不是框架)
  • 产物放在 ~/lab
  • 用 Playwright 截图做视觉验证,拿参考图对比迭代
  • 最后部署到 Cloudflare Pages

看图:模型「看懂」了参考图

把参考图交给模型后,它先描述了建筑结构,并把每个部分量化为可程序化的参数:大球中心约在 1/3 高度、半径约为全高的 10%;小球在上方约 78% 高度;天线从 83% 一路收窄到针尖。它不是照着描,而是先理解结构,再转成几何与比例。 这一点让它后面的建模有了骨架。

建模:程序化体素生成器

核心是一个 sample(point) 函数:对空间的每个格子点判断「此处是否有一块体素」,以及该用哪种颜色。塔体由几组几何体组合而成:

  • 三根斜柱:从底部(半径 20)向中心收拢到与大球相接处
  • 大球:r=12.5,中心在 y=37,上半部分做了红/玻璃的色带
  • 中柱:贯穿全塔
  • 一排观测舱:水平伸出的小胶囊
  • 小球 + 渐细天线针

每个体素对应 InstancedMesh 里一个立方体实例,按区域着色并加一点随机的亮度扰动,形成体素特有的斑驳质感。

1
2
3
4
5
6
7
// 斜柱:线性内插出一条从底部到中心的圆柱
for (const a of LEG.angles) {
const t = clamp((y - LEG.y0) / (LEG.y1 - LEG.y0), 0, 1);
const rr = LEG.baseR + (LEG.topR - LEG.baseR) * t;
const cx = rr * Math.cos(a), cz = rr * Math.sin(a);
if (y >= LEG.y0 - 2 && y <= LEG.y1 && Math.hypot(x - cx, z - cz) < LEG.r) c = 1;
}

视觉验证:截图驱动的迭代

模型没有「画完就完」,而是搭了一个 Playwright 截图环:起一个静态服务器托管 dist/,用 Chromium 加载页面,等 WebGL 渲染出第一帧后再截图,然后拿截图和参考图对比。

这样迭代了几轮,问题逐个暴露又逐个被修掉:

  1. 初版底座是一整块大圆盘,喧宾夺主 → 改成薄板
  2. 斜柱太粗、上下比例不对 → 收细
  3. 顶部的「天线」和上球糊成一团 → 重新分层

最值得一提的,是一个很隐蔽的 bug:斜柱的插值在收敛点之上会把 t 钳到 1,导致三根柱子在 y 大于收敛高度之后无限向上延伸,在塔顶形成一根「很粗的柱子」,把天线都淹没了。肉眼从正面很难看出来,但模型不是只靠猜——它写了一个逐层转储体素几何的调试脚本,发现 y=89~99 高度上出现了本不该有的柱体颜色,从而定位到是斜柱没有在上限处截断,一行 && y <= LEG.y1 就修好了。

这种「用数据说话、不只凭肉眼」的排查方式,是这次体验里最打动我的一点。

修完后,四个角度(正、三分之四、侧、背)的截图都干净、一致,剪影一眼就能认出是东方明珠。

发布:云上一键可达

接着让模型把 dist/ 部署到 Cloudflare Pages:

1
2
npx wrangler pages project create tower --production-branch main
npx wrangler pages deploy dist --project-name tower --branch main

部署后用 Invoke-WebRequest 确认 HTTP 200、JS 资源可访问,再用 Playwright 打开线上真实页面截图验证 WebGL 场景确实跑起来、0 个控制台错误,而不只是「文件能访问」。

现在的线上地址(项目名 tower):https://tower-81m.pages.dev/

交互:让镜头活起来

发布后模型又发现一个问题:页面相机是写死的(只能靠 URL 参数调)。于是加了 three.js 的 OrbitControls,支持左键旋转、滚轮缩放、右键平移,并用 maxPolarAngle 约束相机不要钻到地下。加完后又写了一个脚本模拟拖动 + 滚轮,确认视图确实变了、无报错,才重新部署。含交互相机的版本也已上线,同样在 https://tower-81m.pages.dev/

小结

DeepSeek-V4-Flash-Vision-Exp 在这次体验里展现出几项很实际的能力:

  • 视觉理解:能「看懂」参考图的构图与比例,并转成可执行的几何参数
  • 完整工程链:脚手架、建模、构建、测试、发布一路做下来,不卡壳
  • 工具调用的纪律:用 Playwright 做视觉回归、用调试脚本做数据级定位,而不是停在「看起来对」
  • 迭代与纠错:遇到反直觉的现象会去查根因,而不是绕过去

唯一需要人工兜底的是外部凭证与网络(Cloudflare 登录、GitHub 推送这类的访问令牌),这类敏感信息交给模型前还是要谨慎。整体而言,把「看图 → 建模 → 验证 → 上线」交给一个有视觉的模型,是一次很顺畅的体验。

我最近发现我在Workbuddy配置的“晨间提醒”定时任务执行时长有点久,经脚本化改造后得到了显著的改进。

改造前状态

“晨间提醒”定时任务的大致工作内容如下:

  • 执行everyday timeline sync命令同步操作后,通过everyday cli工具获取24小时内的邮件、当天的日历事件和待办任务。
  • 通过腾讯新闻 Skill获取热点新闻和当地天气信息。
  • 通过github cli工具获取前一天的Github动态。
  • 整理上述信息后,生成一份晨间提醒报告,发送到我的个人微信。
  • 总体上该这是一个结构相对稳定、信息源相互独立的任务。

AI Agent执行“晨间提醒”定时任务的效率瓶颈主要集中以下两个方面:

  1. 获取工具用法:AI Agent在每次执行任务时,往往都会重新通过everyday --helpgh --help命令获取命令行工具的用法说明。
    类似地,AI Agent也会重新加载所需的Agent Skill并理解其意图和使用方式,做了很多重复的工作。

  2. 串行执行命令:虽然各个信息源的获取是相互独立的,但AI Agent还是会按照顺序一个接一个地执行这些命令,导致整体执行时间较长。某些Agent支持运行并行子Agent执行多个子任务,但由语言模型进行任务的分派和结果汇总的效率也不高,并且会消耗大量的Token。

脚本化改造

考虑到我配置的“晨间提醒”定时任务的工作内容是相对稳定的,并且其涉及到的信息本质都可以执行命令行工具来获取,我让Workbuddy编写了一个Python脚本用于并行执行这些命令行工具获取原始信息,然后将结果汇总后再进行处理和生成报告,从而有效提升了执行效率并节省了大量Token。

Read more »

我近期开发了一款Rust命令行工具Everyday CLI
配合自带的Skill
可为 AI Agent 提供便捷的邮箱、日历、RSS 订阅、笔记、待办事项及书签管理能力。。
我几乎全程在腾讯推出的 WorkBuddy 上进行开发,期间不断切换模型与工作流,推动项目逐步迭代至较完善版本,也积累了不少实践经验。

WorkBuddy 除腾讯混元模型外,还集成了多款国产开源模型,其中包括业界公认的领先模型 GLM-5.2。
Everyday CLI项目的基础搭建和前期开发使用的就是GLM 5.2模型,总体质量令人满意。
然而 GLM-5.2 调用成本较高,迅速消耗了我大部分的积分余额。此前我因参与 WorkBuddy 早期营销活动累计获赠七千余积分,仅不到两天就由 GLM-5.2 耗去了三四千。
这一消耗速度相当惊人——WorkBuddy 每月 70 元的基础订阅仅赠送 4000 积分,重度使用下往往一两天便告罄。

积分告急后,我转而使用腾讯自研的 HY3 模型(近期刚由 HY3-Preview 升级为正式版,目前在 WorkBuddy 及 OpenRouter 平台限时免费)。
HY3的总体参数量和GLM 5.2还是有一定的差距,不过在成熟的 Harness 框架支撑下,项目推进依然顺畅。
此外,我还另行订阅了 MiniMax Token Plan,并在 WorkBuddy 中手动接入了 MiniMax M3 模型。
体验时发现M3模型有些过度思考的倾向,在很多场景下可能还不如HY3模型的体验好。
针对这一问题,我手动安装并启用了 Caveman
Skill,强制压缩输出风格,取得了显著的优化效果。

我认为 Rust 在 Code Agent 场景下具备独特优势:其质量门禁(rust fmt与 Clippy)及编译器反馈均十分明确,能有效弥补模型能力的波动。

此前使用 GitHub Copilot 时,我习惯于先通过 /plan规划模式制定方案,确认后再着手开发。
鉴于 WorkBuddy 未内置规划模式,我改为使用planning-with-files skill,
依托 task_plan.mdprogress.mdfindings.md三类文档,分别追踪项目计划、实施进度及关键决策与技术要点。

本周末我接触到了grill-with-docs Skill,
该技能会在方案制定前深入质询各项关键设计决策,并将其固化为 ADR(架构决策记录,过程中虽消耗较多 Token),最终助力我顺利完成了 Timeline 模块的开发。

Everyday CLI 目前仍在高频迭代中,欢迎各位关注、试用并提出宝贵意见与建议。

参考链接:

微软近期正式发布WSL container CLI(wslc.exe),可以直接在windows上管理Linux 容器,不必再安装Docker Destop或在某个wsl实例里安装docker ce。

wslc.exe随最新的wsl预览版发布,可通过wsl --update --pre-release命令升级。

wslc.exe的命令行语法与docker高度一致,开发者(以及AI Agent)可以使用熟悉的命令。

wslc-example
wslc-nginx

近期微信推出了官方的 Clawbot 插件和官方的 Openclaw Plugin,用于配置 Openclaw 连接。与此同时,其他 Openclaw 类工具也纷纷接入了微信 Clawbot。

我使用的 Zeroclaw 暂时还没有官方的微信 Clawbot 接入方案,不过可以通过 OpeniLink Hub 来间接实现 Zeroclaw 与微信 Clawbot 的连接。

OpeniLink Hub 是一个基于微信官方 iLink 协议的开源微信机器人(Bot)管理平台,同时也是一个 App 应用市场。

它主要提供以下能力:

  • 支持多账号扫码绑定
  • 提供内置应用市场,可一键扩展飞书、Slack、Notion 等 20 多种工具
  • 支持 AI 自动回复能力

OpeniLink Hub

我将自己的微信 Clawbot 连接到了 OpeniLink Hub,并安装了两个内置应用:

  1. MCP Server

    • 提供向微信发消息的能力
    • 配置到 Zeroclaw 中后,可以让 Zeroclaw 向微信发送消息
  2. Runner

    • 提供斜杠命令能力
    • 可以在微信中通过 /agent 触发 Zeroclaw 中的功能

clawbot

虽然微信的 Clawbot 对于 Markdown 消息、流式消息等功能的支持还不完善,但通过 OpeniLink Hub 的中转,Zeroclaw 的功能可以在微信中得到一定程度的展现,目前可以满足一些简单的消息交互需求。

在技术领域,2026年的第一季度属于OpenClaw,近期我也开始了自己的“养龙虾”之旅。
我个人非常不愿意在个人设备上给予AI过多的权限(比如屏幕读取和文件系统权限),所以选择了在一台云服务器安装“龙虾”。作为一个TypeScript实现的node.js服务,原版的OpenClaw需要消耗相当高的服务器资源(运行时占用1GB以上的内存)。考虑到我的云服务器只有4GB内存,我最终选择安装配置了zeroclaw(一个rust实现的轻量级OpenClaw同类产品)。

运行ZeroClaw服务仅需一个约15MB大小的二进制文件,安装配置流程极为简便,运行时只需要不到10MB的内存,并且可以通过zeroclaw service install命令自动配置systemd服务。

ZeroClaw支持多种AI服务提供商,我使用的火山引擎的方舟Coding Plan(支持豆包模型以及Deepseek、GLM、Minimax等国产主流模型)。经过我的一番测试,我发现Doubao 2.0 Pro模型的表现比GLM 4.7和Minimax M2.5好一点。

OpenClaw类产品比起Claude Codede的一个显著优势就是可以通过即时通信软件直接和AI进行交互,支持用户随时随地通过个人设备调用AI执行自动化任务。

我给自己的ZeroClaw服务配置了QQ机器人的接入,值得注意的是我的ZeroClaw服务是通过Cloudflare Tunnel暴露到公网上的,域名没有进行备案,所以只能通过WebSocket方式接入QQ机器人,不能通过配置Webhook的方式接入(腾讯最近推出的 Workbuddy 和 QClaw 服务接入 QQ 机器人,提供了官方 Webhook 链接用于配置。)。

由于没有运行在个人设备上,无法管理个人文档和数据,我主要利用ZeroClaw进行一些定时的自动化信息获取任务。我配置了一个每天早上的“晨间播报”定时任务,推送当天的天气情况,待办事项和日程安排等信息。

我还配置了一个每天获取当天的AI资讯的定时任务,并且迭代了很多次。

一开始是直接通过ZeroClaw自己的搜索工具获取AI相关资讯,但发现获取到的资讯质量很不可控。
后来编写了一个Python脚本,直接从RSS源获取相关资讯,让ZeroClaw定时执行这个脚本,测试时发现 AI 竟然在无监督的情况下‘自作主张’修改了脚本中的 RSS 源列表,替换成了36氪和其他部分国内媒体的RSS源,这些媒体有相当一部分的报道是带有商业推广倾向的(即通常所说的“软文”),质量参差不齐。

我从这个案例学到一个深刻的教训,和使用Github Copilot或Claude Code等工具进行开发工作不同,在具备写权限和执行权限的环境中不能随意让AI在无监督的情况下运行AI自生成的代码(或者修改现有代码)。

最终我选择了使用crontab定时运行Python脚本获取RSS源的更新,将内容写到文本文件中,在定时任务中让ZeroClaw直接读取文本文件中的信息,精选出当天高价值的资讯推送给我。

总的来说,ZeroClaw 是一个非常有潜力的个人 AI 基础设施。这次‘养龙虾’之旅不仅帮我建立了一个趁手的自动化助手,也让我对 Agent 时代的权限隔离和确定性工程有了更直观的体悟。未来我会继续探索和利用这个工具来提升我的工作效率和生活品质。

在现有架构下,我主要维护的是一个运行在 AlmaLinux ECS 实例上的后端服务:基于 FastAPI 构建,通过 Docker Compose 进行容器化部署。服务中包含多条 AI 对话类 SSE(Server-Sent Events)长连接接口。

随着用户规模增加,我逐渐发现一个现实问题:Docker Compose 在滚动发布和长连接共存场景下,几乎无法实现真正的零停机更新。容器重建过程中,SSE 连接被强制断开,用户体验受到明显影响。

在与 AI 进行多轮技术探讨后,我重新审视了技术选型。结论是:在当前规模与复杂度下,并不需要直接迁移到 Kubernetes。采用 Docker Swarm 即可满足零停机更新与滚动发布的需求,同时保持架构复杂度可控。

经过半天实践,我成功将服务从 Docker Compose 迁移至 Docker Swarm。以下是关键迁移步骤与问题记录。

一、初始化 Swarm 集群

在 ECS 主机上执行:

docker swarm init

该命令会将当前 Docker Engine 切换为 Swarm 模式,并初始化为单节点集群(Manager)。对于单机部署场景,这已经足够。

二、调整 Compose 文件结构

Swarm 兼容 Compose 文件格式,但需要引入 deploy 段定义编排策略,例如:

  • replicas:副本数量
  • update_config:更新策略(并行数、延迟、顺序等)
  • restart_policy:重启策略

例如可配置:

  • 每次只更新一个副本
  • 新副本就绪后再停止旧副本(start-first 策略)

这一步是实现“零停机滚动更新”的核心。

三、使用 Stack 部署

Swarm 不再使用 docker-compose up,而是通过 Stack 进行编排部署:

docker stack deploy -c docker-compose.yml your_stack_name

Stack 会将服务转换为 Swarm Service,并交由调度器管理。

四、迁移过程中遇到的问题

  1. 本地构建镜像无法拉取

由于镜像是本地构建,未推送至 Docker Hub 或私有仓库,Swarm 默认会尝试从远程仓库解析镜像并拉取,导致部署失败。

解决方式是在部署时添加:

–resolve-image never

该参数指示 Swarm 跳过远程镜像解析,直接使用本地镜像。

  1. Swarm 网络无法直接访问宿主机服务

在 Compose 模式下,容器可以通过 bridge 网络访问宿主机服务(如本地数据库或 Redis)。但在 Swarm overlay 网络中,默认无法直接访问宿主机 127.0.0.1。

解决方案是将服务地址改为:

host.docker.internal

或在 Linux 环境中使用:

host-gateway

并在 compose 文件中通过 extra_hosts 显式声明 host-gateway 映射。这样容器即可访问宿主机网络资源。

五、迁移结果与技术判断

迁移至 Docker Swarm 后:

  • 支持多副本运行
  • 支持滚动更新与回滚
  • SSE 长连接在发布期间不中断
  • 架构复杂度远低于 Kubernetes

在当前业务规模下,Swarm 提供了一个性价比极高的“轻量级编排层”。它弥补了 Docker Compose 在生产环境部署能力上的不足,同时避免了 Kubernetes 带来的学习成本和运维复杂度。

对于单机或小规模集群的 AI 服务而言,这是一个务实且工程上合理的选择。

在近期的 AI 辅助开发实践中,我逐步形成了一套相对稳定、可复用的工作流,核心目标是:让 AI 可规划、可执行、可追踪、可审查

整体流程如下:

首先,为每个项目建立专属的 agents.md,用于描述项目背景、技术约束以及 AI 开发规范,例如强制要求在代码生成后执行静态检查和单元测试。这一步相当于为 AI 提供“项目宪法”。

其次,提出足够明确、结构化的需求,并使用 Copilot 的 Planning 模式生成开发计划,避免直接进入无序实现。

在实施阶段,切换至 Copilot 的 Agent 模式执行计划,并明确要求使用 PRD.mdprocess.md 跟踪进度:前者记录需求,后者记录每次代码变更与当前状态。通过外部文件系统作为长期记忆,降低上下文丢失的风险。

完成初步实现后,由人工对功能正确性进行验证,确保需求被真实满足,而非“看起来能跑”。

最后,引入我自定义的Linus 风格 Code Review Agent进行代码审查,集中指出设计、可维护性和工程质量问题,再由人工或 AI 执行针对性的重构。

值得一提的是,近期社区中出现了一个名为 planning-with-files 的 Claude Skill,其思路与上述流程高度相似,同样通过外部文件作为 AI 的长期记忆,并额外引入 findings.md 用于沉淀调研结论和经验知识。这类模式进一步验证了“文件即上下文”的工程价值。

1
2
# Skill安装命令
npx skills add https://github.com/othmanadi/planning-with-files --skill planning-with-files

总体来看,AI 开发正在从“对话式生成”走向“工程化协作”,而可追踪的计划、状态与知识载体,是这一转变的关键基础。