我是怎么让 Claude Code 在沙箱里帮 SayCraft 写代码的

这篇记录一下 SayCraft 现在最核心的一块东西:我是怎么把 Claude Code 放进一个沙箱里,让它一边写代码,一边让用户在浏览器里看到页面实时长出来的。

如果只从外面看,SayCraft 像是一个“你说需求,AI 帮你做网页”的产品。但里面真正难的部分不是“调用一个大模型”,而是让这个过程稳定、可观察、可回放,而且用户不是开发者,不会帮你看 terminal,也不会知道什么时候该刷新页面。

我最后选的形态是:会议系统只负责理解和排计划;真正写代码的是 E2B 沙箱里的 Claude Code CLI;它直接改 /workspace,Vite 负责把变化实时展示给用户。

SayCraft Claude Code 沙箱执行架构图
SayCraft 里 Claude Code 的运行机制:配置、调度、沙箱执行、提交与回调。

为什么不是“直接让模型写代码”

一开始很容易把这件事想简单:用户说一句话,模型输出一些代码,我把文件写进去,预览一下就好了。

但真跑起来会发现,这个想法有点天真。一个真实网页不是一个单文件答案,它会经历很多细碎动作:建页面、加组件、补样式、接 mock data、修类型错误、看 Vite 日志、补图片、再改 router。更重要的是,用户正在看着屏幕等结果。

所以这里面有三个约束:

  • 代码必须真的落地到一个可运行项目里,不是停在聊天记录里。
  • 过程要可见,最好页面能一块一块出现,而不是五分钟后突然闪一下。
  • 边界要可靠,哪一步完成、哪一步失败、哪些文件改了,都要有系统记录。

Claude Code 刚好适合“在一个真实项目里动手做事”。它会读文件、写文件、跑命令、修错。于是我没有把它当成一个普通 LLM API,而是把它当成一个被产品调度的“代码执行进程”。


控制面:Coordinator 不写代码,只写计划

SayCraft 的会议里有一个 Coordinator。它的职责不是写代码,而是理解用户刚刚说了什么,更新产品意图、选择模板、拆出 plan items。

这一步有点像产品经理:它要判断“用户到底想做什么”,而不是马上打开编辑器开干。真正决定什么时候启动 Claude Code 的,是另一个普通 TypeScript 服务:Coding Reconciler

Reconciler 做的事很朴素:

  • 看数据库里有没有 pending 的 plan item。
  • 看这个 meeting 有没有正在跑的 foreground Claude。
  • 如果没有,就启动一个新的 Claude Code 进程。
  • 如果有,就先别打扰它,等这轮结束后再 --resume 下一批。

这里有一个很重要的经验:不要在 Claude 正跑到一半时继续往 stdin 里塞新需求。 我们实测过,mid-turn 的消息很容易被当前 run 合并、忽略,最后看起来像用户补充了需求,但 Claude 没真正处理。后来我干脆把规则改成:新需求先留在数据库里,等 SessionEnd 后用新的 --resume 进程处理。

这类系统里,“看起来更实时”的方案不一定更可靠。稳定的边界比一时的丝滑更重要。


执行面:一个 E2B 沙箱就是一个临时工作台

每个 meeting 会有一个 E2B sandbox。里面有一个真正的 Vite 项目,主目录是 /workspace

Claude Code 不是在我的 API 容器里本地跑,而是在这个 sandbox 里跑。这样它看到的文件、执行的命令、改出来的页面,都属于这次 meeting 的临时环境。用户看到的 live preview,也是这个 sandbox 里的 Vite 服务。

启动时大概会做这几件事:

  • 确认模板已经初始化到 /workspace
  • 确认 Vite preview 已经起来。
  • 构造本轮 Claude Code prompt。
  • 把 provider env、model、hook settings 传进去。
  • 用 wrapper 启动 claude -p --input-format stream-json

这里我没有直接用 Claude Agent SDK,而是自己用 CLI 拼了这一层。原因很现实:SDK 底层也是 spawn Claude Code 进程,而我们的进程必须跑在 E2B 里面,且还要配合 sandbox 的 stdout、hooks、文件 tail、pause/resume。也就是说,真正麻烦的不是“怎么调用 Claude”,而是“怎么管理这个跑在远端沙箱里的 Claude”。


四类配置同时生效

Claude Code 启动前,不是只吃一个 prompt。它同时受到几类配置影响:

  • 模板里的 CLAUDE.md:告诉它这个项目的目录结构、依赖、组件库、哪些文件不要改、图片 slot 怎么写。
  • Langfuse 里的 code-session-foreground prompt:告诉它这轮怎么构建,尤其是“先搭结构,再一块一块填 section”。
  • 每次 spawn 写入的 /tmp/cs-settings...:配置 Stop hook / SessionEnd hook,并关闭自动 Co-Authored-By。
  • Provider env:比如 ANTHROPIC_BASE_URLCLAUDE_CODE_MODEL,决定实际走哪个模型和网关。

这个分层挺重要。CLAUDE.md 是模板知识;Langfuse prompt 是运行策略;settings 是进程级回调;env 是模型和网络。它们混在一起会非常难维护,分开以后心智负担小很多。


为什么既让 Claude 自己 commit,又有 Stop hook 兜底

这块我一开始也绕过弯。

现在的 prompt 会要求 Claude 在页面生长的过程中主动 commit:先 scaffold,再每个 section,再 Phase 2。这样做的好处是 replay 可以看到网页一段一段长出来,image-filler 也能在每个阶段后开始替换真实图片。

但只靠 Claude 自己 commit 又不够可靠。模型可能忘了,可能 type-check 后没提交,也可能刚好有一点尾巴没收干净。所以 Stop hook 会在每个 agent run 边界做一次兜底:

git add -A
if git diff --cached --quiet; then
  # 没有新 diff,说明 Claude 已经提交过了
else
  git commit -m "claude run boundary"
fi

这就形成了一个比较舒服的关系:Claude 的 commit 是主动的阶段 checkpoint;系统的 commit 是保险丝。 如果 Claude 已经提交,hook 就 no-op;如果还有漏网之鱼,hook 帮它补上。


两个踩坑:SessionStart 和 secret

有两个小坑我印象很深。

第一个是 SessionStart hook。我们曾经想在 SessionStart 时把项目快照注入给 Claude,听起来很聪明:少读文件,启动更快。结果 Claude Code 的 Write 工具有自己的规则:写某个已存在文件前,需要通过 Read 工具读过它。系统 reminder 里塞了文件内容,并不等于工具层面“读过”。最后出现了“模型以为自己看过,工具认为没看过”的错位。

所以 SessionStart hook 后来被删了。现在只保留 Stop 和 SessionEnd。

第二个是 hook settings 不能放在 /workspace。因为 Stop hook 会 git add -A,如果 settings 里有真实的 hook secret,而文件又在 workspace 里,那就有可能被提交进仓库。这个听起来很低级,但这种系统性低级错误才最吓人。现在 settings 一律写到 /tmp


我现在对这套架构的理解

这套东西看起来像是“AI 写代码”,但我越来越觉得,它更像一个小型操作系统:

  • Coordinator 是产品层的调度器。
  • Reconciler 是任务队列和状态机。
  • E2B sandbox 是进程隔离和临时文件系统。
  • Claude Code CLI 是真正的代码执行 worker。
  • hooks 是进程生命周期事件。
  • git commit 是可回放、可归档、可恢复的边界。

如果把 AI 产品只理解成“发 prompt,收回答”,很多问题会被藏起来。真正上线以后,问题反而都在 prompt 外面:进程怎么死、状态怎么恢复、用户怎么看到进度、secret 会不会漏、模型忘记 commit 怎么办。

做 AI coding 产品,模型能力当然重要,但更重要的是把模型放进一个有边界、有回调、有审计的运行时里。

SayCraft 现在还远没到我满意的程度,但这套“会议计划 → reconciler → E2B Claude Code → live preview → hook commit”的形态,至少已经让我觉得方向是对的。它不是最简单的实现,但它解释了为什么用户能“说一句话,然后看着网页真的长出来”。

产品在这里:SayCraft。之前录过一个 demo,也可以看:YouTube demo

感谢您的收看 祝你天天开心~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇