首页
/ Storybook MCP 按文件路径预览 Story:解析 914-preview-story-by-path 评测场景与 stories-preview 工具

Storybook MCP 按文件路径预览 Story:解析 914-preview-story-by-path 评测场景与 stories-preview 工具

2026-09-06 18:34:57作者:牧宁李

本篇围绕 Storybook 仓库中 agent-eval 评测套件里的 914-preview-story-by-path 场景展开:它给 AI 编码 Agent 下达了一个两步任务——先阅读 stories 文件并说明其导入内容,再调用 Storybook MCP 的 preview 工具按「文件路径 + 导出名」预览 Primary 与 Secondary 两个 Button story。读完本文,你能理解 path-based story 选择器的参数契约、评测如何断言 Agent 的工具调用,以及该场景与 storyId 选择方式(915-preview-story-by-id)之间的设计取舍。

评测场景的定位

Storybook 仓库在 agent-eval 目录下内置了一套 Agent 评测套件:它在沙箱中运行编码 Agent(Claude Code / Codex),让 Agent 对着 fixture 项目执行 Storybook 工作流(编写 story、预览或审阅 story、运行 story 测试),然后断言 Agent 确实遵循了仓库提供的 MCP 工具或 plugin 技能所定义的工作流。每个评测场景是一个独立目录,由三部分组成:

文件 作用
PROMPT.md 直接投喂给 Agent 的任务提示词
EVAL.ts vitest 测试,解析 Agent 的调用记录并断言工作流结果
package.json 声明该评测使用的 fixture 模板(本场景为 reshaped-storybook

914-preview-story-by-path 属于 9xx 系列的「MCP 工具行为」评测,其核心考察点是:当用户以故事文件路径(而非 storyId)指代 story 时,Agent 能否正确使用 stories-preview 工具的路径形态选择器

原始任务提示词(PROMPT.md)

PROMPT.md 全文如下,仅两句话、两步操作:

Read the stories file for the button at stories/Button.stories.tsx, and tell me what that file imports.

Then afterwards, show a preview of the Primary and Secondary button stories using the Storybook MCP preview tool.

拆解这个任务,它刻意包含两种不同性质的动作:

  1. 静态阅读:读取 stories/Button.stories.tsx,向用户报告该文件导入了什么。这一步不依赖 MCP,考察 Agent 的基础文件理解能力。
  2. 工具调用:使用 Storybook MCP 的 preview 工具,预览 PrimarySecondary 两个 story。关键在于提示词给出的是文件路径 + 导出名stories/Button.stories.tsx 中的 Primary/Secondary),而不是 button--primary 这类 storyId——因此 Agent 应当使用 preview 工具输入 schema 中的 path-based 形态。

Fixture 项目:任务中"正确答案"的来源

评测目录自带的 fixture 代码即任务的全部上下文。stories/Button.stories.tsx 完整内容如下:

import type { Meta, StoryObj } from '@storybook/react';
import Button from '../src/components/Button';

const meta = {
  title: 'Example/Button',
  component: Button,
  tags: ['test'],
  args: {
    label: 'Click me',
    disabled: false,
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    label: 'Primary',
  },
};

export const Secondary: Story = {
  args: {
    label: 'Secondary',
  },
};

对照提示词第一步,该文件共导入两处:@storybook/reactMetaStoryObj 类型(type-only 导入),以及相对路径 ../src/components/Button 的默认导出 Button 组件。这个组件本身极简——一个受 label / disabled props 驱动的 <button>,带 data-testid="button-component" 属性,供后续断言或测试定位使用:

type ButtonProps = {
  label: string;
  disabled?: boolean;
};

export default function Button({ label, disabled = false }: ButtonProps) {
  return (
    <button type="button" disabled={disabled} data-testid="button-component">
      {label}
    </button>
  );
}

meta.title'Example/Button',因此两个 story 在 Storybook 中的完整定位是 Example/Button 下的 PrimarySecondarytags: ['test'] 标记使其同时可被 story 测试工具链发现。fixture 的 package.json 只声明了 "template": "reshaped-storybook",说明沙箱会基于 agent-eval/templates/reshaped-storybook 模板搭建完整可运行的 Storybook 项目,再叠加这些 fixture 文件。

断言逻辑:EVAL.ts 如何验证 Agent

EVAL.ts 是该场景的验收测试,全文逻辑浓缩为四条断言:

const previewCalls = getWorkflowCalls('stories-preview');
expectWorkflowCalls(['stories-preview']);

expect(
  previewCalls.some((call) =>
    workflowCallIncludesStory(call, {
      absoluteStoryPath: 'stories/Button.stories.tsx',
      exportName: 'Primary',
    })
  )
).toBe(true);

expect(
  previewCalls.some((call) =>
    workflowCallIncludesStory(call, {
      absoluteStoryPath: 'stories/Button.stories.tsx',
      exportName: 'Secondary',
    })
  )
).toBe(true);

其中三个关键点值得注意:

  1. expectWorkflowCalls(['stories-preview']) 要求 Agent 的完整工作流恰好是调用 stories-preview 这一个工作流——不允许额外调用 find-story-ids 之类的探测工具来回绕,即提示词给的信息已足够,Agent 应直达预览。

  2. 逐 story 校验选择器形态workflowCallIncludesStory 断言的期望对象同时携带 absoluteStoryPathexportName,而非 storyId。测试里的注释明确说明了原因:

    Deliberately no storyId: this eval requires the path + export strategy, and workflowCallIncludesStory would accept a storyId match on its own.

    也就是说,如果 Agent 偷懒改用 storyId 形态预览,该断言会失败——这正是"by-path"与姊妹评测 915-preview-story-by-id 的分工所在。

  3. some() 语义:Primary 和 Secondary 可以出现在同一次调用的 stories 数组里,也可以分两次调用,只要每个 story 都被 path + export 形态覆盖即通过。

断言所依赖的工具函数定义在 agent-eval/lib/test-utils.ts,其中 StoryInputExpectation 类型精确刻画了两种可选的故事选择形态:

export type StoryInputExpectation = {
  absoluteStoryPath?: string;
  exportName?: string;
  storyId?: string;
};

该文件同时负责从 Agent 的 shell 命令记录或 MCP 调用记录中提取 StorybookWorkflowCall(解析逻辑见 agent-eval/lib/shell-parse.ts),并在 pluginmcp 两种集成方式间做归一化——因此同一份 EVAL.ts 同时约束 MCP 工具和 CLI plugin 两条路径。

工具侧实现:stories-preview 的输入契约

评测断言的"标准答案"来自 MCP 工具本身的 schema。核心定义在 code/core/src/shared/open-service/toolsets/stories/story-input.tsstoryInputSchema 是一个 union,提供两种互斥的故事选择形态。

形态一:路径 + 导出名(本评测考察的形态)

v.object({
  exportName: /* The export name of the story from the story file... */,
  explicitStoryName: v.optional(v.string()),
  absoluteStoryPath: /* Absolute path to the story file... */,
  ...storyInputProps,
})

各字段的用途(结合 schema 中的描述文本):

  • absoluteStoryPath:故事文件的绝对路径;描述明确写道"仅当已有 story 文件上下文时与 exportName 一起使用"。
  • exportName:story 在文件中的导出名(本例即 Primary / Secondary)。schema 描述特意引导 Agent:只有当你"已经在编辑某个 .stories.* 文件、知道其中的导出名"时才用这种形态,否则应优先 storyId。
  • explicitStoryName(可选):当 story 通过 name 属性设置了与导出名不同的显示名时使用,否则不设置。
  • props(可选):覆盖 story 默认 args 的自定义 props;
  • globals(可选):预览时的全局参数,如 theme'dark'/'light')、locale'en'/'fr')、backgrounds(如 { value: '#000' })等横切关注点。

形态二:storyId

v.object({
  storyId: /* for example "button--primary"... */,
  ...storyInputProps,
})

描述建议"不在具体 story 文件内工作时优先使用此形态",并提示 ID 应从 docs 工具(withStoryIds=true 或 show 操作)中获取。这正是 915-preview-story-by-id 评测对应的路径。

输出契约:预览成功/失败的结构定义在 code/core/src/shared/open-service/toolsets/stories/definition.ts 中。成功项必须返回 titlenamepreviewUrl 三个字段,其中 previewUrl 带有明确的 Agent 行为引导:

Direct URL to open the story preview. Include this URL in the final user-facing response so users can open it directly.

失败项则返回原始 inputerror 字符串,保证 Agent 能定位是哪个 story 的预览请求失败。

两种选择策略的对照:914 vs 915

914-preview-story-by-pathagent-eval/evals/915-preview-story-by-id/PROMPT.md 构成一组对照实验。915 的提示词是:

Show a preview of two existing Button stories: Primary and Secondary.

Do not modify any component or story files.

两者的差异揭示了工具 schema 的设计意图:

维度 914(by path) 915(by id)
提示词给的信息 文件路径 stories/Button.stories.tsx + 导出名 仅组件与 story 名,不含文件路径
期望选择器形态 absoluteStoryPath + exportName storyId
隐含前置动作 阅读 stories 文件(顺带回答导入问题) 通常需先从 story 索引中解析出 storyId
断言策略 显式排斥 storyId 匹配 期望 storyId 匹配

从 schema 描述文本看,官方对二者的使用边界是清晰的:已持有 story 文件上下文时用路径形态,否则优先 storyId。914 评测正是把 Agent 钉在"已有文件上下文"这一分支上,验证它不会退化到先查索引、再按 ID 预览的绕路行为。

如何本地运行该评测

评测套件在仓库根目录以 yarn workspace 方式运行(见 agent-eval/README.md):

# 预览将要运行的内容,不产生 API 调用
yarn workspace agent-eval run eval:dry

# 只调试这一个评测
EVAL_ONLY=914-preview-story-by-path yarn workspace agent-eval run eval

README 同时强调了两条工程约束:一是默认只跑第一个核心评测,需 EVAL_EXTRA_EVALS=1 才跑完整工作流评测线,EVAL_ONLY 则用于单个评测的调试;二是本地运行前需在仓库根目录重新编译沙箱注入的 MCP 构建(yarn nx run-many -t compile --projects mcp,addon-mcp),过期的 dist 会在 preset 加载时崩溃,表面症状是沙箱就绪超时而非构建错误。此外,评测按 Agent(Claude Code / Codex)× 集成方式(mcp / plugin)× 模型与推理档位组合成实验矩阵(见 agent-eval/experiments 下的实验定义文件),EVAL.ts 会依据运行环境自动适配两种集成方式的调用记录解析。

小结

914-preview-story-by-path 场景用一个三步链条完整刻画了"按路径预览 story"的工作流:提示词以文件路径指代 story(PROMPT.md)→ fixture 提供可静态核对的 stories 文件与组件(Button.stories.tsx)→ 断言强制 stories-preview 调用携带 absoluteStoryPath + exportName 而非 storyIdEVAL.ts)。而该链条的最终语义锚点落在 MCP 工具的输入 schema 上(story-input.ts):path 形态要求调用方已持有故事文件上下文,storyId 形态则是无文件上下文时的首选。理解了这组评测,也就理解了 Storybook 为 Agent 设计的"最小惊讶"原则——用户以何种坐标指代 story,Agent 就应使用与之对应的选择器形态。

登录后查看全文
热门项目推荐
相关项目推荐