【从0开发类OpenClaw】Day 2:后端出生了,顺手跟 UI 干了一架

从零写一个类 OpenClaw 的 AI 助手框架,全程记录踩坑。今天是第二天:NestJS 后端 + 配置落盘 + 联动校验 + 1286px 惨案。


昨天把架子立起来了:WebUI 三个配置页能点能存,但说白了是个"无源之水"——配置全存浏览器 localStorage,换个浏览器、换台电脑,配置就没了。今天目标很明确:给配置找个真正的家

后端:捡现成的 NestJS

写后端框架这事,我没纠结太久。Express 太裸,得自己搭一堆轮子;NestJS 开箱自带依赖注入、模块化、校验管道,还跟 TypeScript 天生一对。直接 git clone typescript-starter 塞进 apps/api,第一天的经验告诉我们:能用现成的就别手搓

当然,捡现成的也有现成的坑,今天第一个坑就栽在这:

根 package.json 是 "type": "module"(ESM)
NestJS 默认编译成 CommonJS
→ 两个体系打架,dist 跑起来直接报错

解法就是让 apps/api 自己声明 "type": "commonjs",把这个包从 ESM 的世界里摘出去。这种"父子 package.json 声明打架"的破事,写单仓库的人一辈子遇不上,写 monorepo 的隔三差五来一回。

顺手把克隆带来的脏东西清掉:嵌套的 .git(不能有子仓库)、自带的 lockfile(monorepo 只认根目录那一把锁)。

配置落盘:一个 JSON 文件当数据库

后端第一件事,是决定配置住哪。SQLite?Redis?为仨配置页上数据库有点小题大做。就一个 JSON 文件吧——CyberClaw.json,三个 key:agents / models / tools

文件当数据库,也得有点数据库的尊严:

  • 原子写入:先写临时文件再 rename,写一半断电也不会得到半个 JSON
  • 串行写队列:多个请求同时改配置,排队来,别互相踩

文件里躺着 apiKey 这种敏感货,所以 CyberClaw.json 进了 .gitignore,只提交一个 CyberClaw.example.json 当模板。密码学第一原则:秘密不进版本库。

第一个真接口:AgentsController

从 Hello World 到真正的 CRUD,中间隔着一个 AgentsController

GET    /api/claw/agents          看全部
GET    /api/claw/agents/:id      看单个
POST   /api/claw/agents          建
PUT    /api/claw/agents/:id      改
DELETE /api/claw/agents/:id      删

id 自动生成、重名 409、查无此人 404,该有的规矩都有。DTO 用 class-validator 把门,传进来的东西先验明正身再说。

重头戏:Models/Tools 跟上 + 联动校验

三个配置页不是三个孤岛。智能体要干活,必须绑一个模型、带几个工具——这就是联动

今天被一条需求点醒:智能体必须关联大模型,否则不许创建。这是用户的原话,也是产品该有的样子——一个没有模型的智能体,就是个空壳子,建了也是废的。

于是联动规则定下来了:

  • 建智能体:modelId 必填,且必须存在于 models;工具名必须存在于 tools,否则 422 直接拒
  • 删模型/工具:被智能体用着?409,不许删,还会告诉你"模型「xxx」正被 1 个智能体使用(演示多工具智能体),请先解除关联"
  • 内置工具:删不掉、改不了名(8 个内置是地基,想动它得先过我这关)
  • 默认模型:第一个自动当选,删掉默认的自动扶正下一个

前端也配了套:卡片上显示"被 N 个智能体使用",你删模型之前先掂量掂量。

前后端打通:localStorage 兜底正式退役

后端活了,前端就别再躲 localStorage 里了。webui 的 dev 代理把 /api/ 转发到 localhost:3000,页面点保存 = 真往 CyberClaw.json 里写。第一天的"先走本地缓存、后端出生后无缝切换"的伏笔,今天兑现了。

跟 UI 干架:1286px 惨案

后端消停了,UI 开始找事。

测试的时候发现,一个超长 Base URL 能把模型卡片撑到 1286px 宽——列宽才三百多像素,一个卡片顶仨。工具一多,Tag 一长,卡片高度也参差不齐,整个列表跟狗啃的似的。

第一轮修:给文本加省略号。没用。
第二轮修:换 CSS 硬截断。还没用。
第三轮终于摸到根了:flex 布局的子元素默认 min-width: auto,遇到不换行的长 URL,它能自己膨胀到内容那么宽,max-width 都拦不住。解法就一行:min-width: 0

后来这活用户亲自下场了,global.less 里一行全局样式,从布局根上把病除了。我反思了一下:前两轮我是在"给症状贴膏药",第三轮才算治病。修 UI 先找根因,这个教训今天记下了。

工具 Tag 也按"定长 + 省略 + 悬浮看全"的标准整改:每个 Tag 固定 88px,超了显示省略号,鼠标悬上去看完整名字。长 URL 也一样,悬停弹完整地址。

孤儿引用:全量体检的教训

中途用户报了个诡异的 bug:删模型,提示"关联的大模型不存在"。删的是模型,关模型什么事?

排查半天,真相是:配置里有个"孤儿"——某个智能体还指着一个已经不存在了的模型 ID(多半是以前手改文件留下的)。而后端的校验是全量体检:每次写配置,把所有智能体的引用查一遍,只要有一根孤儿,整体作废。

这就好比全班体检,一个同学漏检,全班体检单作废——你想给 A 同学办个手续,也被"体检没过"堵在门口。体验极差。

方案 A(最终采用):单资源操作只做定向检查。建智能体就查新智能体的引用,删模型就查有没有智能体用它。全量体检只保留在"整包提交配置"这一条路上。孤儿从此只挡它自己,不拖累别人。

顺手补了三个测试:配置里有孤儿的时候,建智能体、改智能体、建模型都该畅通无阻。绿灯。

自己坑自己的两个瞬间

诚实交代,今天两次被自己人(也就是我)背刺:

1. JSDoc 里写 */,577 个编译错误。

我在注释里写"create*/update*/delete*“想表达"创建更新删除等操作”,结果 */ 是注释的结束符——注释提前被掐断,后面全变代码,577 个错误当场爆炸。更冤的是第一次跑还显示 0 错误(增量编译缓存给的假象),清了缓存才现原形。教训:注释里别写 */,写"create、update、delete 系列"不香吗

2. 一条 curl 把用户配置清空了。

测试接口时手滑发了 -d '{}'——一个空对象。后端一看:好家伙,空配置,给它补全成默认值存盘吧。用户的数据当场蒸发。好在测试前留了备份,一条 cp 救了命。

这件事的直接后果:给全量保存接口加了 DTO 校验,空对象直接 400 拒绝,外加三个 e2e 测试。文件当数据库,备份和防手滑,一条都不能少。

今天的成果

* 9ab028e fix(api/webui): orphan-reference tolerance (plan A) + UI truncation + empty-config guard
* f0c2f9c fix(webui): model delete via single-resource API + hard truncation
* e465e4a fix(webui): enforce single-line truncation in config cards
* f2fe304 fix(webui): ellipsis long content in config cards with hover tooltips
* 01859be feat(api): add Models/Tools CRUD + agent-model-tool linkage validation
* a3fe67b feat(api): add full config endpoints + webui dev proxy
* 9443e91 feat(api): add AgentsController for CyberClaw.json agents config
* 20bceda feat(api): init NestJS backend app from typescript-starter
  • 后端出生:三套 CRUD(Agents/Models/Tools)全齐,配置真落盘
  • 联动校验上线:模型必绑、删除保护、被引用提示
  • 前端联调:localStorage 兜底退役,页面保存 = 真写盘
  • UI 整改:1286px 惨案结案,Tag 定长省略 + 悬浮看全
  • 孤儿引用方案 A 落地 + 空对象防护
  • 54 个单元测试 + 4 个 e2e 全绿

明天干点啥

原计划:packages/core 的 LLM 多 provider 客户端(DeepSeek、OpenAI、Ollama 一家通吃)。昨天说今天干,今天又鸽了——后端这摊子事太大了。明天争取动工,如果明天还鸽,那就后天。

另外方案 B 还欠着:把 webui 的操作全部从"整包提交"改成走单资源 API,职责更干净。不急,等 core 那边忙完再说。

今天到这儿。跟 UI 干仗干得血压高,但后端终于能独当一面了。


仓库地址:https://github.com/Li-Esquire-codeverse/CyberClaw(欢迎 star 监督进度)

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐