Storybook 9 技能体系深度解析:storybook-story-instructions.md 如何教会 AI Agent 编写高质量 Story
本篇指南聚焦 Storybook 仓库中 AI 技能(Skills)体系的核心模板文件 storybook-story-instructions.md——它是 Storybook 交付给 AI Agent(如 Claude Code、Codex 等编码助手)的"写 Story 操作手册"。读完后,你将掌握:Agent 编写 Story 的完整方法论(状态覆盖、交互测试、Mock、命名规范)、Storybook 9 相对旧版本的关键破坏性变更(包合并、initialGlobals、tags 驱动 autodocs、storybook/test 导入、Play Function 参数语义),以及这套模板如何被 build-story-instructions.ts 按项目实际配置动态渲染、经 MCP 或 CLI 两种通道下发到 Agent。
一、模板定位:write-story 技能的单一事实来源
1.1 技能 ID 与"冻结契约"
该模板文件在 Storybook 9 的 CLI/MCP 技能体系中对应 write-story 技能。技能 ID 定义在 skills.ts 中:
export const SKILL_IDS = ['stories', 'write-story', 'setup'] as const;
export const SKILLS: Record<SkillId, { blurb: string }> = {
// ...
'write-story': {
blurb:
'How to write, update, and test Storybook stories for this project: imports, patterns, and conventions.',
},
// ...
};
源码注释明确说明这些 ID 是"公开的 CLI 词汇——插件存根会引用它们,因此重命名视为破坏性变更"。write-story 技能的定位即"如何为本项目编写、更新和测试 Story:导入、模式与约定",强调项目特异性——这正是模板中需要占位符动态填充的原因。
1.2 双通道下发:MCP 工具与 CLI 命令
同一份模板有两条到达 Agent 的路径,映射关系定义在 skill-refs.ts:
/**
* Frozen contract: the name must match `GET_UI_BUILDING_INSTRUCTIONS_TOOL_NAME`
* in addon-mcp's `tools/tool-names.ts`.
*/
const MCP_SKILL_TOOL_NAMES: Partial<Record<SkillId, string>> = {
'write-story': 'get-storybook-story-instructions',
};
export function getSkillRef(transport: SkillTransport) {
return (id: SkillId): string =>
(transport === 'mcp' && MCP_SKILL_TOOL_NAMES[id]) || `npx storybook skills get ${id}`;
}
- MCP 通道:addon-mcp 注册了名为
get-storybook-story-instructions的工具(与 addon-mcp 中tool-names.ts的冻结常量一致,可参见 tool-names.ts 与 get-storybook-story-instructions.test.ts),Agent 在写 UI 前调用它获取完整指令。 - CLI 通道:用户可执行
npx storybook skills get write-story直接拉取渲染后的指令文本。
为什么"双通道必须表述同一工作流"?build-story-instructions.ts 中的注释解释了一处关键设计:MCP 客户端(如 Claude Code)会截断服务端指令(截断上限 2,048 字符),因此服务端指令只保留简略指针,而完整规则必须放在本模板输出里——因为该工具的输出"从不被截断,且是所有 Agent 在写 UI 前都会读取的唯一通道"。这一工程取舍直接体现在 build-server-instructions.ts 的 getFinalLinksGuidance 实现中。
二、模板正文:Agent 写 Story 的方法论
2.1 组件拆分与"必有 Story"原则
模板开篇即给出两条硬性规则:
- 编写 UI 时,优先把大组件拆分为更小的部分;
- 任何写出的组件都必须有 Story;编辑组件时,确保该组件的 Story 做了相应修改。
这是 Agent 工作流的总纲:Story 不是可选产物,而是组件交付物的一部分。
2.2 好 Story 的七项标准
模板将"什么是好 Story"拆解为可执行清单,核心目标是:覆盖组件能到达的每一段独立业务逻辑与状态(happy path、错误/边缘态、加载中、权限/角色、空态、props/context 带来的变体),同时避免展示相同逻辑的冗余 Story。七个维度如下:
| 维度 | 要求 |
|---|---|
| 交互性 | 组件可交互时,必须用 play function 编写 Interaction 测试,借助 fn、userEvent、expect 等 storybook/test 工具驱动 UI:模拟点击、输入、focus/blur、键盘导航、表单提交、异步响应、toggle/选择变化、分页/过滤等。特别强调:把 fn 作为回调 args 传入时,play function 必须断言该回调确实被调用 |
| 数据与搭建 | 提供真实的 props、state 与 mock 数据;包含有意义的标签/文本让行为可观测;用确定性 fixture 桩掉网络/服务,保证 Story 可重复渲染 |
| 断言 | 在 play function 中断言交互的可见结果(文本、aria 状态、启用/禁用、class/状态变化、触发的事件);优先使用基于 role/label 的查询 |
| 变体 | 只选取会改变行为的变体:默认/主题切换、loading/loaded/empty/error、合法/非法输入、权限/角色/能力、feature flag、改变逻辑的尺寸/密度/布局 |
| 可访问性 | 使用语义化 role/label;焦点与键盘交互在相关处必须有测试覆盖 |
| 命名与结构 | Story 名描述场景(如 "Error state after failed submit");相关变体逻辑分组,不重复 |
| 导入与格式 | Meta/StoryObj 从框架包导入;测试工具从 storybook/test 导入(而非 @storybook/test);Story 保持最小化,只含演示行为所需的元素 |
2.3 Storybook 9 关键变更(Story 编写视角)
模板的 "Storybook 9 Essential Changes for Story Writing" 一节是版本迁移的核心要点,Agent 写代码前必须先内化这些差异:
包合并:Meta / StoryObj 与测试工具导入
- import { Meta, StoryObj } from '@storybook/react';
+ import { Meta, StoryObj } from '@storybook/react-vite';
- import { fn } from '@storybook/test';
+ import { fn } from 'storybook/test';
注意第一个 diff 中的包名是占位符:{{FRAMEWORK}}(框架包,如 @storybook/react-vite)与 {{RENDERER}}(渲染器包,如 @storybook/react)由渲染器解析后填入,映射表见 framework-renderer.ts:
export const frameworkToRendererMap: Record<string, string> = {
'@storybook/react-vite': '@storybook/react',
'@storybook/react-webpack5': '@storybook/react',
'@storybook/nextjs': '@storybook/react',
'@storybook/vue3-vite': '@storybook/vue3',
'@storybook/sveltekit': '@storybook/svelte',
// ... angular / preact / web-components / html 等
};
从源码结构看,该映射并非完备(源码注释自述 "it's not complete"),未命中时 buildStoryInstructions 回退为 renderer ?? frameworkToRendererMap[framework] ?? framework,即直接把框架名当作渲染器名。
全局状态:globals 重命名为 initialGlobals
// .storybook/preview.js
export default {
- globals: { theme: 'light' }
+ initialGlobals: { theme: 'light' }
};
autodocs 配置:用 tags 取代 parameters.docs.autodocs
// .storybook/preview.js 或单个 story
export default {
tags: ['autodocs'], // 为所有 story 生成 autodocs
};
Storybook 中的 Mock:sb.mock 两步走
模板要求始终 Mock 外部依赖以保证 Story 渲染一致性,并给出两步流程:
- 在 preview 文件中注册模块 Mock(
sb.mock建议{ spy: true },保留原函数同时可覆盖、可监听):
import { sb } from 'storybook/test';
// Prefer spy mocks (keeps functions, but allows to override them and spy on them)
sb.mock(import('some-library'), { spy: true });
// Important: Use file extensions when referring to relative files!
sb.mock(import('./relative/module.ts'), { spy: true });
注意模板特别强调:引用相对文件时必须带文件扩展名——这是 Vite 环境下模块解析的硬性要求。
- 在 Story 中用
beforeEach+mocked()指定 Mock 值:
import { expect, mocked, fn } from 'storybook/test';
import { library } from 'some-library';
const meta = {
component: AuthButton,
beforeEach: async () => {
mocked(library).mockResolvedValue({ user: 'data' });
},
};
export const LoggedIn: Story = {
play: async ({ canvas }) => {
await expect(library).toHaveBeenCalled();
},
};
执行前提:对应的 import 必须已在 preview 文件中完成 sb.mock 注册(模板原文 "Before doing this ensure you have mocked the import in the preview file")。
Play Function 参数语义:canvas vs canvasElement
这是模板中纠正 Agent 高发错误的一节:
- play function 的
canvas参数可直接使用 testing-library 风格的查询方法(getByLabelText等); canvasElement才是真正的 DOM 元素;- 从
storybook/test导入的within把 DOM 元素转换成带查询方法的对象,与canvas等价。
绝对不要写 within(canvas)——canvas 已具备查询方法,它不是 DOM 元素:
// ✅ Correct: Use canvas directly
play: async ({ canvas }) => {
await canvas.getByLabelText('Submit').click();
};
// ⚠️ Also acceptable: Use `canvasElement` with `within`
import { within } from 'storybook/test';
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.getByLabelText('Submit').click();
};
// ❌ Wrong: Do NOT use within(canvas)
play: async ({ canvas }) => {
const screen = within(canvas); // Error!
};
环境硬性要求
- Node.js 20+、TypeScript 4.9+;
- React Native 项目使用
.rnstorybook目录(而非.storybook)。
2.4 文档工作流指引的注入点
模板第 6 行的 {{DOCS_WORKFLOW_GUIDANCE}} 占位符对应一段"设计系统优先"规则:启用文档工具集时,Agent 在创建或修改任何 UI 之前,必须先调用 docs.list 查看设计系统已提供的组件——基于现有组件构建而非重复造轮子——再对要使用的每个组件调用 docs.show 获取真实 props 与用法示例;应通过文档工具而非阅读 node_modules 里的库源码/类型定义来回答 props/用法问题,"Never assume or invent props"(绝不臆造 props)。未启用文档工具集时该段落整体置空。
三、渲染管线:占位符如何被项目配置填充
build-story-instructions.ts 是模板的"编译器",函数签名为:
export type StoryInstructionsInputs = {
transport: SkillTransport; // 'mcp' | 'cli'
framework: string; // 框架包名
renderer?: string; // 渲染器包名,缺省时按 frameworkToRendererMap 解析
changeDetectionEnabled: boolean; // features.changeDetection 是否开启
reviewEnabled: boolean; // experimental review 是否开启
testSupported: boolean; // 是否支持 story 测试
a11yEnabled: boolean; // a11y addon 是否启用
docsEnabled: boolean; // docs 工具集是否可用
};
占位符替换逻辑逐一对应模板:
let uiInstructions = storyInstructionsTemplate
.replace('{{FRAMEWORK}}', framework)
.replace('{{RENDERER}}', resolvedRenderer)
.replace('\n{{DOCS_WORKFLOW_GUIDANCE}}', docsEnabled ? docsWorkflowGuidance : '')
.replace('{{STORY_LINKING_WORKFLOW}}', storyLinkingWorkflow)
.replace('{{FINAL_LINKS_GUIDANCE}}', getFinalLinksGuidance(transport, reviewEnabled))
.replace('{{PREVIEW_STORIES}}', ref('stories.preview'))
.replace('{{CHANGED_STORY_FALLBACK_LINK_GUIDANCE}}', changedStoryFallbackLinkGuidance);
其中 ref 由 getToolName({ transport }) 生成,保证工具名在 MCP(如 stories-preview 类工具)与 CLI 两种词汇体系下各自正确。
3.1 链接工作流随 changeDetection / review 状态切换
{{STORY_LINKING_WORKFLOW}} 有三级降级策略,体现了模板对三种能力组合的适配:
| 能力组合 | 渲染后的工作流 |
|---|---|
changeDetectionEnabled && reviewEnabled |
修改组件/Story 后调用 stories.changed 发现受影响的 Story,Story ID 必须来自该调用(共享基础设施变更可用 stories.findByComponent 兜底)——严禁从文件名、导出名或记忆构造 ID;视觉可观测的变更将发现的 ID 传入 review.create,stories.preview 仅在迭代单个 Story 时使用 |
changeDetectionEnabled(无 review) |
先调 stories.changed,再用结果中选取的 storyId 调 stories.preview |
| 均未开启 | 修改 UI 后直接调 stories.preview 并分享最相关的链接 |
兜底链接指引 {{CHANGED_STORY_FALLBACK_LINK_GUIDANCE}} 同理:开启变更检测时,若未把所有变更 Story 都传入 preview,需附上 /?statuses=affected;modified;new 这条 Storybook 兜底链接,让用户能看到完整变更列表;未开启时仅提示"可能还存在其他相关 Story"。
3.2 最终回复的链接呈现规则
{{FINAL_LINKS_GUIDANCE}} 来自 build-server-instructions.ts 导出的 getFinalLinksGuidance,按 reviewToolAvailable 二选一:
- review 可用:最终回复只展示一组链接(review 与 preview 不可并存)。若已发布 review,回复末尾必须是独立的 review 区块(单独标题行、一句"该 review 展示与本次变更最相关的少量 Story、因由 AI 整理结果可能不准确或不完整"的说明、👉 前缀的 review 页 markdown 链接),其后不得再有任何内容,也不得再罗列单条 Story/preview URL;视觉可观测的变更在 review 发布前不算完成。仅在变更无可视影响时才可改用 preview URL。
- review 不可用:最终回复包含所有返回的 preview URL,顺序一致(先变更兜底链接、再具体 preview URL)。
模板中"向 {{PREVIEW_STORIES}} 最多传 5 个最相关 Story ID、并原样包含该调用返回的全部 URL"以及"修改 UI 组件后必须搜索相关 Story 并再次提供链接——即使会话中已经链接过,也必须重新提供链接"等条目,共同构成 Agent 交付环节的防漏机制。
3.3 测试与 a11y 指令的条件追加
testSupported 为真时,渲染器会在模板末尾追加两块内容:
- story-testing-instructions.md:要求"每次组件或 Story 变更后运行 Story 测试工具",且这是运行 Story 测试的唯一方式——绝不能用
npm run test:stories等 package.json 脚本替代;工作流为"变更 → 用相关 Story 聚焦运行 → 失败则修复重跑 → 直到全部通过";开发期优先聚焦运行(传stories参数),交付前/大范围重构后跑全量(省略stories参数)。 - a11y-instructions.md:仅当
a11yEnabled为真时追加,且测试指令中的修复提示会带上 "(see a11y guidelines below)" 后缀。
3.4 测试佐证:渲染行为如何被锁定
build-story-instructions.test.ts 与 skill-refs.test.ts 对渲染管线做了契约级验证,例如:
- MCP 传输下
write-story技能的交叉引用渲染为冻结工具名get-storybook-story-instructions;CLI 传输下渲染为npx storybook skills get write-story; - 框架回退行为:传入未知框架(如
@storybook/who-knows)时,渲染器占位符回退为框架名本身; - 占位符解析、review 感知链接指引、测试工具集与 a11y 的开关组合均有独立用例覆盖。
这些测试保证了模板占位符与 build-story-instructions.ts 的替换逻辑之间不会出现漂移——对维护"永不截断的 Agent 指令通道"而言,这是关键的质量护栏。
四、实践清单:从模板提炼的 Agent 写 Story 流程
综合模板全文与渲染逻辑,一次完整的"Agent 编写/修改 Story"流程可以归纳为:
- 查现有能力(若 docsEnabled):先
docs.list再docs.show,基于现有设计系统组件构建,不臆造 props; - 按九条变更点写代码:框架包导入
Meta/StoryObj、storybook/test导入测试工具、initialGlobals、tags: ['autodocs']、preview 中sb.mock注册(相对路径带扩展名)、Story 内mocked()覆盖; - 按七项标准完善 Story:状态全覆盖、确定性数据、play function 断言可见结果、role/label 查询、场景化命名、最小化;
- 正确操作画布:直接用
canvas查询;需要 DOM 操作时用within(canvasElement),禁用within(canvas); - 运行验证:每次变更后运行 Story 测试工具直至全绿,聚焦运行 + 交付前全量运行;
- 交付链接:按变更检测能力选择
stories.changed→review.create/stories.preview链路,遵守最终回复链接呈现规则,并在后续会话中重复提供 Story 链接。
五、小结
storybook-story-instructions.md 不是一份静态文档,而是 Storybook 9 AI 技能体系的"故事编写宪法":它以模板占位符与项目运行时配置(框架、变更检测、review、测试、a11y、docs 六项开关)解耦,经 build-story-instructions.ts 渲染后经 MCP 工具 get-storybook-story-instructions 或 npx storybook skills get write-story 双通道下发。模板本身承载了 Agent 写 Story 的完整方法论与 Storybook 9 的破坏性变更清单,而渲染管线确保每个项目拿到的是"贴合自身能力矩阵"的指令版本——这正是该文件在仓库中被大量 eval 用例(如 804-write-story-for-existing-component)反复引用的原因:它是 Agent 行为一致性的锚点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00