Install any skill in seconds. Free to start, no credit card required.
Get Started Free →当 ai-shifu 聊天流从 block 粒度向 element 粒度演进,或历史记录与 SSE 渲染一致性出现问题时使用本技能。统一 element_bid 渲染键、兼容旧字段并收敛 AskBlock 归并逻辑。
.claude/skills/ai-shifu-chat-element-streaming/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 288% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 85% | 0% |
element_bid 作为聊天项稳定渲染 key。generated_block_bid、parent_block_bid 兼容字段。contentList。type=element 时按 element_bid 覆盖更新,不做重复拼接。element_type=answer,让 AskBlock 按答案流增量更新。element_type=ask/answer 时,归并到 anchor_element_bid 对应的 AskBlock.ask_list。sequence)为准,不强制锚定在 anchor 内容后。type=element + type=done,需按 element_bid 覆盖更新元素项,并将 done 作为唯一收尾信号。type/event_type 与 content/data 两套命名,避免后端灰度期间前端丢流。done 但出现多个 element_bid 时,以“新 element 到达”作为前一个 element 的结束信号,并立即补一个追问块(like-status/ask入口)。type=done 时,优先读取 done.is_terminal 判断它是“当前分段结束”还是“真正终态”;字段缺失时才回退到旧的前端推断。done 收尾补追问块时,锚点应取“最近的可操作 element(content 或 interaction)”,否则最后一项为 interaction 时会漏补追问块。done 与后续 element 乱序,边界判定不能只依赖 streaming ref,需增加“从当前列表尾部推断上一个可操作 element”的兜底逻辑,避免中间 element 漏补追问块。done.is_terminal=true,则必须停止续拉。type=done 时,不要默认立即 stopPreview;仅当 done.is_terminal=true 才关闭当前预览流。关闭后若最后一个可操作块不是 interaction,应继续拉取下一段流。ai-shifu 本地稳定复现 run SSE 问题时,优先在 getRunMessage 层增加显式调试开关;mock_run_sse_fixture=stuck 回放由 data.json 截断得到的卡住 fixture,mock_run_sse_fixture=test 回放 mock-fixtures/test/data.json 的完整测试流,并保持 message/readystatechange/close 契约不变;不要把页面组件改成只兼容 mock 数据。run 流若在 3 秒内没有任何新事件,前端应在消费层主动判定为超时:关闭当前流与 loading 状态、向 items/elementList 追加 type=error 的错误项,并复用同一份国际化文案弹 destructive toast。Slide 遇到超时错误时,只把它当作“关闭内部 loading overlay”的信号,不要把错误项渲染成单独一页;用户侧错误提示统一交给 toast。html 视觉 marker 更新时容易把 loading 插到两个 element 的视觉边界之间;但当 loading 思考占位仍在列表中时,不要同时显示点状 loading。markdown-flow-ui 的 ContentRender 打开打字机效果,应只对“非历史记录、且 element_type === text”的实时正文块开启;html、interaction 和历史正文继续即时渲染,优先在 ContentBlock 这类渲染包装层集中判断,不要把条件散落到列表组装逻辑里。element 被 done 或“下一个 element 到达”收尾时,要同步把本地 ChatContentItem.is_final 置为 true;不要让列表状态长期停留在早先 SSE 快照里的 false。NewChatComp 上层维护 readModeItems 的可见列表缓存和按 element_bid 存储的打字机完成态;只有前一个 text 同时满足“is_final=true 且当前 content 的打字机回调已完成”时,才放行下一个 element。element 收尾时给正文 content 追加追问按钮或把块改成 isHistory=true,不要让这个“收尾态 content”继续复用同一条 typewriter gate 缓存;否则缓存会因 content 变化被重置成未完成,而 ContentRender 又因为不再启用打字机而不会补发 onTypeFinished,最终把后续 element 永久卡住。isHistory 只表示“来自学习记录/历史回放的数据”,不要把它当作运行时 UI 控制位复用;run SSE 覆盖已有历史项时,也不要从 previousItem 继承 isHistory,否则实时流会被误判成历史数据。elementList 可见性 gate 不要直接依赖 isHistory;应使用独立的 typewriter 候选标记,仅让“真正参与实时打字机的 text element”进入缓存与阻塞链路,避免历史首块被 SSE 覆盖后误拦住后续 element。done 或“下一个 element 到达”把某个元素标记为 is_final=true,后续同 element_bid 的 SSE element 快照不能再用旧的 is_final=false/undefined 覆盖回去;本地 finalize 语义优先级应高于早先快照字段。custom-button-after-content 只允许作为“打字结束后的静态尾部 UI”出现;打字机阶段传给渲染器的 content 必须先剥掉这段 markup,避免把追问按钮当正文逐字输出。text 的富内容块(如 html、svg、sandbox 相关内容)若需要尾部追问按钮,不要依赖内容字符串内部的 custom-button-after-content 首屏渲染;应把按钮从 content 中剥离后在外层独立渲染,避免正文未稳定时按钮先于正文或以异常结构闪现。StudyRecordItem 这类共享记录类型被 SSE、历史记录和渲染层共同消费,就必须把 is_final 等流式收尾字段声明在共享接口里;不要仅在局部 UI 类型上兜底,否则 next build 很容易在跨层调用处报类型缺失。cache.isFinished 决定同一个 element_bid 是否继续打字;若当前归一化 content 已经比缓存快照更长,即使旧 cache 标记过完成,也必须继续给 ContentRender 打开 typewriter,否则后续增量首帧会直接整段落地。text element 在 is_final !== true 的等待阶段,即使当前 chunk 已经打完,也不要先把 enableTypewriter 关掉;否则后续增量到达时会经历 false -> true 切换,ContentRender 会把已显示文本清空并从头重打。text_end/done 后会自动续拉下一段,不能只依赖内存里的 currentContentRef 作为正文基线;下一轮 content 到达时应优先回填同一 element_bid 已渲染正文,并同时兼容“纯 delta”与“累计快照”两种 payload 语义,否则续流会把前一段正文覆盖掉并触发整段重打。AskBlock 里的追问 answer 要把 ContentRender.enableTypewriter 当成“整条 answer 的会话状态”来保活,而不是把每个 done/break 都当成终态。非终态 done(text_end) 不能立刻 finalize + close,终态收尾时也不要顺手把 shouldUseTypewriter 改成 false,否则同一条 answer 后续再到 chunk 时就会从某一句开始整段直出。AskBlock 本地 store 里若已经追加出比父层 ask_list 更长的追问历史,后续 hydrateAskList 不能再用更短的旧列表覆盖它;展开/收起、列表重排或重新渲染时都要优先保留较新的本地追问记录,否则收起后再展开会丢失刚刚流出来的追问问答。text_end/done 与下一段同 element_bid 文本续流之间存在等待期,尾部可见的那个 text element 不能先把 enableTypewriter 关掉;至少在本轮输出仍未结束时要保活这次打字机会话,避免后续追加文本到达时触发 false -> true 切换并把已显示正文清空重打。custom-button-after-content 从正文字符串里剥离后,如果改成在宿主层单独渲染追问按钮,按钮的横向布局与 img/span 对齐样式也要一起在宿主层显式声明;不要只依赖原先 custom element 场景下的 descendant selector,否则按钮容易在外层 DOM 变化后出现图标与文案错位或换行。isOutputInProgress;还要记录“当前这轮输出已经实际流到过哪个 element”。新一轮交互 onSend 刚启动、首个 element 尚未到达时,必须先清空上一轮的 keep-alive bid,避免旧正文被误判为当前流的一部分而再次整段打字。interaction/html 抢走;它应绑定到“当前输出会话里最近一次参与打字机的 text element”。新一轮输出开始前先清空锚点,后续只有新的 text element 才能切换锚点,这样既能避免旧会话串场,也能避免 interaction 到达后把前一个 text 关掉再重开,触发整段重打。like-status / interaction / ask 之类的非 text 交互块,前一个 text 就不再是尾项,必须立即停止 keep-alive,避免交互块出现后前文再次整段打字。text_end/done 或“下一个 element 到达”收尾的实时 element,应该立刻在数据层打上“切回阅读模式按历史态展示”的独立标记;阅读模式组装 readModeItems 时再把它映射成 isHistory=true 和 shouldUseTypewriter=false,不要等用户切模式时再临时回填,也不要直接污染实时流本身的 isHistory 语义。finalize/build item 这类依赖当前模式的逻辑,都不能直接读取创建 stream 时 closure 里的 isListenMode;应改读实时 ref,否则“听课模式发起、阅读模式收尾”的交互链路会把新正文误标成 history-like,触发 typewriter 中途关闭后再重开,造成闪烁或整段重打。AskBlock 的 streaming answer 若在打字途中被收起/关闭,不能把这条 answer 的 shouldUseTypewriter 立即改成 false,也不要卸载承载它的 ContentRender 面板节点;应让流式会话在隐藏状态下继续保活,否则再次打开时同一条 answer 会因重挂载从错误进度重新打字,表现为速度异常、跳字或像被“叠速”一样。done/text_end(is_terminal=true) 并完成收尾,就应立即把该 answer 的 shouldUseTypewriter 置为 false。这样再次打开时直接展示静态最终文本,不要再重放一次打字机。document.visibilityState === 'hidden' 时,应立即绕过 element 可见性 gate,并把所有已收到的实时 text element 标记为静态直出;这些 element_bid 回到前台后也要保持静态,避免 ContentRender.enableTypewriter 经历 false -> true 后从头重播。后台期间收到的同 bid 增量继续直出,回到前台后新出现的 text element 才重新使用打字机。element_bid 相同,必须覆盖同一条消息。*BlockBid* 到 *ElementBid* 后,消费方解构与依赖数组必须同步改名。| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 23,477 | 21,403 | -9% | 1 | 1 | 0% | 4,687 | 7,744 | +65% | 0 | 0 | — |
case-07 | pass→pass | 14,956 | 5,084 | -66% | 1 | 1 | 0% | 2,158 | 4,238 | +96% | 0 | 0 | — |
case-02 | fail→pass | 48,850 | 18,100 | -63% | 1 | 1 | 0% | 1,718 | 6,663 | +288% | 0 | 0 | — |
case-03 | fail→pass | 29,888 | 28,300 | -5% | 1 | 1 | 0% | 6,212 | 9,230 | +49% | 0 | 0 | — |
case-04 | fail→pass | 18,209 | 8,046 | -56% | 1 | 1 | 0% | 2,543 | 4,807 | +89% | 0 | 0 | — |
case-05 | fail→pass | 15,054 | 5,041 | -67% | 1 | 1 | 0% | 2,262 | 4,186 | +85% | 0 | 0 | — |
case-06 | fail→pass | 11,458 | 4,670 | -59% | 1 | 1 | 0% | 1,664 | 4,127 | +148% | 0 | 0 | — |
case-08 | fail→pass | 11,037 | 3,837 | -65% | 1 | 1 | 0% | 1,840 | 3,916 | +113% | 0 | 0 | — |
case-09 | fail→pass | 17,061 | 7,845 | -54% | 1 | 1 | 0% | 2,931 | 4,624 | +58% | 0 | 0 | — |
case-10 | fail→pass | 12,991 | 4,480 | -66% | 1 | 1 | 0% | 2,177 | 4,070 | +87% | 0 | 0 | — |
case-11 | pass→pass | 28,478 | 5,921 | -79% | 1 | 1 | 0% | 2,277 | 4,473 | +96% | 0 | 0 | — |
case-12 | fail→pass | 12,300 | 5,563 | -55% | 1 | 1 | 0% | 1,870 | 4,201 | +125% | 0 | 0 | — |
case-13 | fail→pass | 11,867 | 4,139 | -65% | 1 | 1 | 0% | 1,950 | 3,997 | +105% | 0 | 0 | — |
case-14 | fail→pass | 12,269 | 3,977 | -68% | 1 | 1 | 0% | 1,880 | 3,941 | +110% | 0 | 0 | — |
case-15 | fail→pass | 9,137 | 5,283 | -42% | 1 | 1 | 0% | 1,425 | 4,031 | +183% | 0 | 0 | — |
case-16 | pass→pass | 17,271 | 6,725 | -61% | 1 | 1 | 0% | 2,719 | 4,461 | +64% | 0 | 0 | — |
case-17 | pass→pass | 12,183 | 5,470 | -55% | 1 | 1 | 0% | 1,943 | 4,291 | +121% | 0 | 0 | — |
case-18 | pass→pass | 19,725 | 8,611 | -56% | 1 | 1 | 0% | 2,961 | 4,769 | +61% | 0 | 0 | — |
case-19 | pass→pass | 12,139 | 5,041 | -58% | 1 | 1 | 0% | 1,839 | 4,218 | +129% | 0 | 0 | — |
case-20 | fail→pass | 13,238 | 5,295 | -60% | 1 | 1 | 0% | 2,047 | 4,228 | +107% | 0 | 0 | — |
case-21 | pass→pass | 11,058 | 9,340 | -16% | 1 | 1 | 0% | 1,969 | 4,963 | +152% | 0 | 0 | — |
case-22 | pass→fail | 15,815 | 14,650 | -7% | 1 | 1 | 0% | 2,946 | 5,859 | +99% | 0 | 0 | — |
case-23 | pass→pass | 15,613 | 16,458 | +5% | 1 | 1 | 0% | 2,706 | 6,239 | +131% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 22 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +57 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.