# Web 嵌入式对话 SDK(`sdk/embed`)评审报告 > 评审对象:`sdk/embed`(`@mao/chat-embed` v0.1.1,产物 `desktop/public/embed/mao-chat.js`) > 评审日期:2026-09-04 评审方式:全量源码通读 + 后端契约逐条核对 + desktop 参考实现对比 + 9 批 Playwright 运行时验证 > 参考文档:`docs/plan/2026-09-04-embed-sdk-technical-design.md` > 本报告只做评审,未修改任何 SDK 源码。 ## 0. 结论摘要 工程骨架与"轻量嵌入"这条主线做得扎实:Shadow DOM 隔离、包体、全局污染、销毁清理、多 tab 协调、上下文注入这些**嵌入式 SDK 特有的难点全部达标**,实测无一处翻车。 问题集中在**从 desktop 裁剪 WS 事件语义的那一层**。设计文档 §4.2 列了 5 条"必须继承的 desktop 行为",实际只完整继承了 2 条: | 设计文档 §4.2 要求 | 实现情况 | |---|---| | 30s 静默判定 + 心跳 | 完整继承,数值与 desktop 一致 | | 重连后全量 re-subscribe | 继承,但发了两次 subscribe | | executionId 去重(stale 丢弃) | 判定函数等价,但**终态后不记录 cancelledExecutionId**,旧轮次事件会被接受 | | cancel 后会话级事件抑制 | **时序倒置**(先抑制后发帧)+ **终态自解抑制** | | `session_snapshot` 终态对账(终结残留工具转圈) | **完全缺失** | 加上三处契约理解错误(会话失效判据、`tool_call_args_delta` 语义、追问 answers 形状),构成本次评审的 P0 主体。一个共性根因:**单测用人造 mock 固化了错误的契约假设**,反而让 bug 通过了测试(详见 §7)。 按用户可感知程度排序,最该先修的三件事: 1. `tool_call_args_delta` 累加导致工具参数渲染成乱码(每次工具调用必现) 2. localStorage 死会话 id 永不自愈(浏览器一旦命中就永久卡在错误横幅,只能手动清 storage) 3. 发送失败不回滚乐观气泡(断网发一次就留下一个永久闪烁的空气泡) ### 做得好的部分(实测确认) - **Shadow DOM 隔离有效**:宿主注入 `!important` 全局样式不侵入浮窗(input font-size 保持 14px);SDK 未向 `document.head` 注入任何样式,`css-injected-by-js` 正确注入到 Shadow Root 内。 - **全局污染最小**:只挂 `window.MaoChat` + `window.__maoChatInstance` 两个键。 - **单实例约束正确**:重复 `init()` 先 destroy 旧实例,DOM 中只留一个 host。 - **`destroy()` 干净**:移除 DOM、停重连、停心跳三项全部生效,无残留 timer。 - **多 tab 协调生效**:双 tab 同时初始化只 `POST /sessions` 一次(BroadcastChannel `inquire/claim` 按设计工作)。 - **上下文注入符合设计 §4.4**:首条消息带 `[页面上下文]`(url/title/data),用户气泡只显示纯净输入文本,上下文未变则不重复携带;8KB 截断不切断 UTF-8 多字节字符。 - **`position: 'left'` 与 `theme.primary` 生效**(`--mao-primary` 内联覆写路径正确)。 - **XSS 防护有效**:`javascript:` / `data:` 协议被清理,`onerror` / `onclick` 属性被清理,`