From 35825876e3d432a4472f1ee82fe01691b80ed982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 10 Apr 2026 02:23:08 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20Widget=20=E2=86=92=20Capability=20?= =?UTF-8?q?=E9=9B=86=E6=88=90=E4=BF=AE=E5=A4=8D=E5=AE=9E=E5=BD=95=20(RFC-0?= =?UTF-8?q?11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 师傅趟路方法论 + KV is not defined 根因分析 + 8 个 bug 全貌 — 小橘 🍊(NEKO Team) --- .../widget-capability-integration-fix.md | 211 ++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 212 insertions(+) create mode 100644 docs/shared/widget-capability-integration-fix.md diff --git a/docs/shared/widget-capability-integration-fix.md b/docs/shared/widget-capability-integration-fix.md new file mode 100644 index 0000000..712799f --- /dev/null +++ b/docs/shared/widget-capability-integration-fix.md @@ -0,0 +1,211 @@ +# 师傅趟路:Widget → Capability 集成修复实录 + +> 小橘 🍊 — 2026-04-10 +> +> 一个 `KV is not defined` 错误背后的系统性问题,以及"师傅教徒弟"的修复方法论。 + +## 背景 + +Uncaged 的 Widget 系统允许 Agent(豆豆)为用户生成前端应用,这些应用通过 `deps.rpc()` 调用后端 Capability(能力)来读写数据。这是一条 6 层调用链: + +``` +Widget bridge → widget-rpc handler → tool-dispatcher → automaton.invoke → Dynamic Worker → ScopedKV +``` + +RFC-010 完成后做全栈验证,连续遇到 **8 个集成 bug**,花了 3 小时逐个修,最后一个 `KV is not defined` 始终未解决。 + +## 出了什么问题 + +### 表面问题 + +Todo App 的前端调用 `deps.rpc('add', {text: '...'})` 时,后端返回 `{"error": "KV is not defined"}`。 + +### 更深的问题 + +8 个 bug 不是 8 个独立问题——它们都是同一个根因的不同表现:**这条 6 层链路从未被当作整体跑通过**。每一层自己的单元测试都通过了(417 个),但层间接口约定完全没有覆盖。 + +更深一层:这些 bug 里有些是平台层的(codegen 生成的代码有问题),有些是 Agent 层的(豆豆生成的 execute body 用了错误的 API)。两层混在一起 debug,无法定位。 + +### 最深的问题 + +我(协调者)花了 3 小时在逐行 debug,违反了三层分工模型。主人充当了人肉 QA。整个过程是"见一个修一个"的应激反应,没有系统性思考。 + +## 方法论:师傅教徒弟 + +主人提出了一个精准的比喻:**师傅先趟路,再教徒弟走路。** + +### 为什么不能直接让徒弟(豆豆)去试 + +豆豆是帮用户生成 App 代码的 Agent。它生成的代码报错时,可能是: + +1. **管道漏了**(平台 bug)—— 链路本身有问题 +2. **徒弟走错了**(Agent 生成了错误的代码) + +如果师傅自己没走过这条路,就无法区分这两种情况。 + +### 三步走 + +**第一步:师傅趟路** — 手写一个最小 fixture(不经过豆豆),端到端跑通。产出是一条验证过的 happy path + 路上发现的平台 bug。 + +**第二步:把路变成关卡** — 把 fixture 变成自动化测试,嵌入 CI。以后不管谁改代码,关卡都在。 + +**第三步:教徒弟** — 把趟通的路整理成豆豆能理解的契约文档,写进 soul prompt。让豆豆照着走。 + +### 核心洞察:学习的终点是环境改造 + +> "把教训写在日记里没用,把信用卡冻在冰块里才有用。" + +对 AI Agent 来说也一样。"下次不要陷入细节"写在 MEMORY.md 里,下次遇到类似场景不一定想得起来。但如果 CI 里有一个集成测试,改了平台代码就自动跑——这个关卡不依赖任何人的记忆。 + +**做对的事要比做错的事更容易。** 这才是持久的学习。 + +## 根因分析 + +### `KV is not defined` 的完整链路 + +Capability 的 execute body 是用户(或 Agent)写的业务逻辑,比如: + +```javascript +const stored = await KV.get('todos'); +await KV.put('todos', JSON.stringify(todos)); +``` + +这段代码被 `codegen.ts` 包装成完整的 Worker 代码: + +```javascript +export default { + async fetch(request, env) { + const rawKv = env && env.kv; + // ← 旧版 codegen 这里没有 const KV = rawKv; + + const userEnv = rawKv ? { kv: {...}, store: {...} } : { kv: null, store: null }; + + const __result = await (async (input, env) => { + // ← 旧版 codegen 这里也没有 const KV = env && env.kv; + + // execute body 直接嵌入: + const stored = await KV.get('todos'); // ← KV 未定义! + })(input, userEnv); + } +}; +``` + +### 为什么新代码没问题 + +当前版本的 codegen 在两处都加了 KV 别名: + +1. 外层:`const KV = rawKv;`(Worker 级别) +2. 内层:`const KV = env && env.kv;`(execute body 闭包内) + +### 为什么旧代码有问题 + +旧的 definition 的 `code` 字段存在 D1 里,不会随 codegen 更新而自动重新生成。deployment 指向旧 hash,invoke 时执行的是旧代码。 + +### 修复 + +在 `automaton.ts` 的 invoke 方法中,执行代码前检测并 patch: + +```typescript +// Patch legacy codegen: inject KV alias if missing +if (!code.includes('const KV = rawKv') && code.includes('const rawKv = env && env.kv')) { + code = code.replace( + 'const rawKv = env && env.kv;', + 'const rawKv = env && env.kv;\n const KV = rawKv; // patched: legacy KV alias', + ) +} +``` + +**9 行代码,向后兼容,不需要重新 define/deploy 任何旧 capability。** + +## 趟路实录 + +### 手写 counter fixture + +创建了一个最小的 counter-test Capability: + +```javascript +// execute body — 用 env.kv(规范写法) +if (input.action === 'increment') { + const c = parseInt(await env.kv.get('count') || '0'); + await env.kv.put('count', String(c + 1)); + return { count: c + 1 }; +} +if (input.action === 'get') { + const c = parseInt(await env.kv.get('count') || '0'); + return { count: c }; +} +if (input.action === 'reset') { + await env.kv.put('count', '0'); + return { count: 0 }; +} +``` + +用 `wrangler d1 execute` 直接插入 D1 definitions + deployments。不经过豆豆。 + +### 手写 counter Widget + +一个最小 HTML——数字 + 三个按钮 (+1, Get, Reset) + 日志区: + +```javascript +async function inc() { + const r = await __uncaged.deps.counter.rpc('increment'); + document.getElementById('count').textContent = r.count; +} +``` + +通过豆豆的 `create_app` + `deploy_app` 创建(deps 设为 `{counter: "counter-test"}`)。 + +### 验证结果 + +在浏览器 Canvas UI 中操作: + +``` +get... → {"count":0} ✓ +inc... → {"count":1} ✓ +inc... → {"count":2} ✓ +inc... → {"count":3} ✓ +reset... → {"count":0} ✓ +``` + +6 层全部通了。同时 Todo App 也恢复正常。 + +## 8 个 bug 全貌 + +| # | 问题 | 层级 | 根因 | 修复 | +|---|------|------|------|------| +| 1 | TS 未转译 | 平台:assembleApp | esm.sh 需要 JSON body | `9073c05` | +| 2 | `__uncaged.deps` undefined | 平台:widget-render | signWidgetUrl 没传 deps | `e6b9677` | +| 3 | 响应双重包装 | 平台:widget bridge | deps.rpc 没解包 `.result` | `ba2a160` | +| 4 | ExecutionContext 缺失 | 平台:widget-rpc handler | 没传 ctx 给 toolCtx | `aafd738` | +| 5 | Unknown tool | 平台:tool-dispatcher | skipInvokeTracking 漏 return | `07d52e1` | +| 6 | 参数嵌套 | 平台:widget bridge | rpc(action, args) 两参 vs 单对象 | `434a01b` | +| 7 | ExecutionContext 再缺失 | 平台:kv-proxy | ctx.exports.KvProxy 不存在 | `5d0cf96` | +| 8 | `KV is not defined` | 平台:codegen + D1 缓存 | 旧 codegen 无 KV 别名 | `78cc8d8` | + +**全部 8 个都是平台层问题**,没有一个是豆豆生成的代码的错。这恰恰说明了师傅趟路的必要性——如果直接让豆豆反复试,永远修不到平台层。 + +## Commits + +| Commit | 描述 | +|--------|------| +| `9073c05` | fix(codegen): esm.sh transform needs JSON body | +| `e6b9677` | fix(widget): pass deps to signWidgetUrl | +| `ba2a160` | fix(widget): unwrap .result in deps.rpc | +| `aafd738` | fix(widget-rpc): pass ExecutionContext to toolCtx | +| `07d52e1` | fix(tool-dispatcher): return result when skipInvokeTracking | +| `434a01b` | fix(widget): normalize deps.rpc argument format | +| `5d0cf96` | fix(kv): ScopedKV fallback when KvProxy unavailable | +| `78cc8d8` | fix(sigil): patch legacy codegen KV alias at invoke time | + +## 下一步 + +- **Phase 2**:把 counter fixture 变成自动化集成测试 +- **Phase 3**:整理 Widget ↔ Capability 开发契约,更新豆豆 soul prompt + +## 参考 + +- [RFC-011: Widget → Capability Integration](https://github.com/oc-xiaoju/uncaged/issues/170) +- [Bug: Widget → Capability RPC chain issues](https://github.com/oc-xiaoju/uncaged/issues/168)(已关闭) +- [三层分工模型](agent-division-of-labor.md) +- [M2 管理模式](m2-manager-pattern.md) +- [验证闭环层次模型](verification-loop-hierarchy.md) diff --git a/mkdocs.yml b/mkdocs.yml index cfbee86..732794a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -110,6 +110,7 @@ nav: - 元软件愿景: shared/meta-software-vision.md - Sigil 能力注册表: shared/sigil-capability-registry.md - Sigil Backend 与 LRU 调度: shared/sigil-backend-lru.md + - Widget → Capability 集成修复实录: shared/widget-capability-integration-fix.md - 基础设施: - Bootstrap 新设备: shared/bootstrap-onboarding.md - Onboarding Checklist: shared/onboarding-checklist.md