AI 写 wot-ui 老踩坑?我把组件库文档做成了离线知识库 + MCP + CLI
大家好,我是不如摸鱼去,wot-ui 的发起人,欢迎来到我的 AI Coding 分享专栏。
好久不见了,各位,最近忙着在做 wot-ui v2,好久没写文章了,今天分享一下我们为 wot-ui v2 提供的 cli 工具,wot-ui v2 即将迎来正式版,敬请期待。
这篇文章想聊的,不是“我又写了一个命令行工具”,而是另一个我最近越来越强烈的感受:
很多时候,AI 写不好 UI,不是因为它不会写,而是因为它根本不知道你这套组件库到底能不能这么写。
这个问题我见得太多了。大模型能很快把页面搭出来,但经常会在这些地方翻车:
- props 记错了
- events 名字猜错了
- slot 用法写得像那么回事,其实压根不对
- 主题变量默认值写成“模型想象中的样子”
- 最难受的是,最后还没法快速定位你项目里到底哪一行有问题
说白了,光靠“问 AI”这条路,太飘了。
直到我看到 ant-design 的 cli 工具 ant-design-cli,原来可以这样玩:与其继续堆 prompt,不如直接把组件库知识做成工具。
这也就是 @wot-ui/cli 想做的事。它把 wot-ui v2 的组件知识整理成离线数据集,再以 CLI 和 MCP Server 两种形态提供出来。仓库定位可以先看 README.md。
换句话说,我不是想让模型“更会猜”,而是想让它少猜一点,最好别猜。
为什么要做这个东西
如果你平时也会拿 AI 写页面,应该能很快共鸣。
让模型生成一个按钮、弹窗、表单,不难。难的是让它写出来的代码,真的符合你项目正在用的那一版组件库约束。组件文档分散、示例零碎、版本持续变化,这些信息对人类开发者来说还能边查边补,对模型来说就很容易“凭印象发挥”。
最典型的几个坑:
- 文档里有 props,但 demo 里又藏着另一套更真实的用法
- CSS 变量默认值在文档没写全,实际要去 SCSS 源码里找
- 在线抓文档这件事本身就不稳定,今天抓到的和明天抓到的可能都不一样
- 即使问答答对了,也解决不了“你本地项目里具体哪一行写错”这个问题
所以 @wot-ui/cli 从一开始就不是单纯的“知识查询器”,它更像一条完整链路:
- 先把组件知识离线化
- 再把这些知识变成稳定 API
- 最后给人和 Agent 都准备好可调用入口
这里的两个入口分别是:
- CLI:给开发者、脚本、CI 用,方便复现和回归
- MCP Server:给 AI 客户端和 Agent 用,走结构化输入输出
我比较喜欢把这件事理解成一句话:
文档是给人看的,工具才是给机器稳定调用的。
在 @wot-ui/cli 里,数据集不是副产物
很多项目会把“抽出来的数据”当成构建过程里的中间结果,但在 @wot-ui/cli 里,我反而把它当成核心 API。
原因很简单。只要你希望模型稳定地理解一个组件库,那最不该依赖的,就是临时抓取网页和上下文拼接。
所以这里有几个很明确的原则:
- 离线优先:运行时不依赖网络,也不依赖上游仓库当下是什么状态
- 结构化优先:能用 JSON 表达的内容,就别再让模型二次概括一遍 markdown
- 一套内核,多入口复用:CLI 和 MCP 共用同一套加载、查询、扫描逻辑
- 输出可控:同一能力支持
text/json/markdown三种格式,既照顾人,也照顾机器
元数据和报告结构定义在 types.ts,里面几个核心对象基本就能把整个设计思路串起来:
ComponentMeta:组件知识本体,包含 props、events、slots、cssVars、demos、docLintReport、UsageReport、DoctorReport:项目分析阶段的结构化输出
如果用一句更直白的话来说,就是:
先把“组件库知识”压成一份可靠的数据,再讨论怎么给人和 AI 用。
整体架构,其实就三层
我把 @wot-ui/cli 拆成了三块:数据生成、运行时内核、产品入口。
1. 数据生成:把上游文档和源码吃进去
离线数据来自 wot-ui/wot-ui 仓库,提取范围见 README.md:
docs/component/*.mddocs/guide/changelog.mdsrc/uni_modules/wot-ui/components/*/index.scss
提取脚本入口是 scripts/extract.ts,最后输出到:
data/v2.jsondata/versions.json
这两个文件会跟着 npm 包一起发布,配置见 package.json。
2. 运行时内核:版本解析、数据加载、组件查询
这层其实很克制,没有做太多花活,链路非常短:
resolveVersion:处理v2/latest这类 alias,见 version.tsloadMetadataFile:读取离线数据文件,见 loader.tsfindComponent/listComponents:组件查找和列表能力,见 metadata.ts
3. 产品入口:CLI 和 MCP 一起对外
- CLI 入口:
src/index.ts,见 index.ts - 命令注册:
src/app.ts,见 app.ts - MCP Server:
src/mcp/server.ts,见 server.ts - MCP tools:
src/mcp/tools.ts,见 tools.ts
对应关系画出来就是下面这样:
flowchart LR
subgraph Upstream[wot-ui/wot-ui]
A1[docs/component/*.md]
A2[docs/guide/changelog.md]
A3[src/.../components/*/index.scss]
end
Upstream --> B[scripts/extract.ts]
B --> C[data/v2.json]
B --> D[data/versions.json]
subgraph Runtime[open-wot runtime core]
E[resolveVersion]
F[loadMetadataFile]
G[findComponent / listComponents]
H[analyzeUsage / lintProject / diagnoseProject]
end
C --> F
D --> E
E --> F
F --> G
subgraph Products[product surfaces]
P1[CLI: wot ...]
P2[MCP tools: wot_info/wot_lint/...]
end
P1 --> Runtime
P2 --> Runtime这个结构有个我自己很喜欢的点:产品入口可以继续长,但底下那套数据和查询内核不用反复重造。
最关键的一步:把“文档 + 源码”整理成结构化知识
这一块才是整个项目最值钱的地方。
因为如果数据本身不靠谱,CLI 做得再花、MCP schema 写得再漂亮,最后也只是把错误包装得更规整一点而已。
所以提取脚本的原则很明确:能从源码拿真值,就别先信文档;文档可以补充,但尽量别当唯一事实来源。
从 markdown 里提取 props、events、slots 和 demo
脚本会解析 markdown 的章节和表格,把这些常见信息抽出来:
Attributes / Events / Slots转成结构化数组- code fence 抽成
DemoMeta[] - 原始文档内容保留在
doc字段里,供doc命令和 MCP 的wot_doc直接输出
相关实现集中在 extract.ts,比如 parseMarkdownTable、parseProps、parseEvents、parseSlots、parseDemos。
CSS 变量优先从 SCSS 拿,不跟文档“赌运气”
这一点我还挺在意的。
因为很多 UI 组件库里,CSS 变量默认值往往比 props 更容易滞后。文档会补,但不一定总是第一时间补齐;而真正能决定主题效果的,往往还是源码里那行 var(--wot-xxx, defaultValue)。
所以 @wot-ui/cli 的策略是:
- 先从组件
index.scss里解析var(--wot-xxx, defaultValue) - 再结合源码注释生成描述
- 如果 SCSS 里没拿到,再回退到 markdown 的
CSS 变量表格
对应实现可以看:
- SCSS 变量解析:extract.ts
- 回退策略:extract.ts
这一步解决的,其实不是“抽数据”本身,而是让主题相关知识从“可能对”变成“尽量有据可查”。
为什么我还是很看重 CLI
很多人一提 AI 工具链,第一反应就是 MCP、Agent、协议、工作流。CLI 反倒容易被看成“传统配角”。
但我自己做下来越来越觉得,CLI 不能少,而且最好先做好。
因为它天然就有三个价值:
- 它是验证入口,不用依赖任何 MCP 客户端就能复现结果
- 它是回归入口,输出稳定,适合测试和 diff
- 它是自动化入口,本地脚本和 CI 都能直接接
@wot-ui/cli 的 CLI 命令清单可以在 README.md 里看到,主要包括:
- 组件知识查询:
list、info、doc、demo、token、changelog - 项目分析:
doctor、usage、lint - MCP 启动:
wot mcp
一个我觉得很实用的小设计:统一输出格式
同一类能力支持三种输出:
--format text--format json--format markdown
对应类型定义在 types.ts,README 说明见 README.md。
这个设计看起来不大,但挺省心。人眼排查的时候用 text,脚本消费的时候用 json,想直接塞到文档或对话上下文里时用 markdown,不用来回再包一层。
看一个真实的 token JSON 输出
相比 info 或 list,token 的 JSON 更适合放到文章里,因为它足够小,但也足够能说明“结构化输出”这件事到底有多实用。
pnpm exec tsx src/index.ts token Button --format json输出节选如下:
{
"name": "Button",
"cssVars": [
{
"name": "--wot-button-primary-bg",
"defaultValue": "$primary-6",
"description": "主类型背景色"
},
{
"name": "--wot-button-primary-bg-active",
"defaultValue": "$primary-7",
"description": "主类型背景激活色"
}
]
}对应实现见 token.ts。
对我来说,这种输出最大的意义在于:模型不用再看一大段 prose 去猜“这个 token 大概是什么意思”,它直接拿 JSON 就行。
MCP 解决的,不是“换个壳”,而是“让能力可协议化”
如果只从表面看,MCP Server 好像只是把 CLI 包了一层。
但它真正有价值的地方,其实是把工具调用这件事变成了可以被客户端理解、可以被约束、可以被安全声明的行为。
在 @wot-ui/cli 里,MCP tools 的定义和实现都放在 tools.ts,里面几个比较核心的工具包括:
wot_info:返回组件的 props、events、slots、cssVarswot_doc:直接输出原始 markdown 文档,避免模型转述时失真wot_demo:列出 demo,或者返回指定 demo 代码,做 few-shot 很稳wot_lint:扫描本地项目并返回结构化报告
MCP 这层最重要的几个点,我会总结成这样:
- 输入是 schema 化的,比如
component、version、dir - 输出可以是严格 JSON,也可以是原始 markdown
- 工具行为可以显式声明,比如只读还是 open-world
最小 MCP 配置
把下面配置加到支持 MCP 的客户端里,就能通过 stdio 启动:
{
"mcpServers": {
"wot-ui": {
"command": "wot",
"args": ["mcp"]
}
}
}示例也可以参考 README.md。
为什么专门标注 open-world
wot_lint 会访问本地文件系统,所以它在 MCP annotations 里标成了 open-world,相关实现见 tools.ts。
这件事听起来像个细节,但其实很重要。因为一旦工具开始“看你的本地项目”,那它就不再只是普通知识查询了,客户端在权限和安全层面应该有明确预期。
我挺希望 MCP 生态里这类标注能被越来越认真地使用起来。否则很多工具看起来都叫“查一查”,实际有的只是读离线包,有的已经在扫你整个工程目录了,这两者差别还是很大的。
真正让我觉得它像“工具链”的,是后面的 usage、lint、doctor
如果 @wot-ui/cli 只做到组件知识查询,其实已经能解决一部分问题了。
但还不够。
因为 AI 最容易出问题的地方,常常不是“它不知道”,而是“它已经改进项目了,但改得不对”。这时候你需要的,不是再问一遍文档,而是基于你本地工程的事实做检查。
这也是为什么我后来把 usage、lint、doctor 一并补上了。
usage:先看看项目里到底怎么用的
usage 会扫描 .vue 文件,做几件事:
- 用
@vue/compiler-sfc解析 SFC - 统计模板中的
<wd-*>标签出现次数和所在文件 - 收集脚本区里和 wot 相关的 import
实现见 scanner.ts。
这个命令很适合做摸底。比如你准备升级、做规范治理,或者只是单纯想知道“项目里哪些组件被用得最多”,它都挺顺手。
lint:把元数据真正用起来
lint 的意思不是“再造一个 ESLint”,而是把组件元数据转成更贴近 wot-ui 使用约束的规则。
当前内置规则包括:
unknown-component:模板里有标签,但元数据里查不到button-content:wd-button既没有可见文字,也没有icondeprecated-prop:基于元数据标出的废弃 prop,目前先落在wd-button
实现见 scanner.ts,输出结构定义在 types.ts。
真实输出长这样:
{
"scannedFiles": 1,
"issues": [
{
"file": "pages/index.vue",
"line": 3,
"rule": "unknown-component",
"severity": "warning",
"message": "Unknown wot-ui component tag: wd-not-exist"
},
{
"file": "pages/index.vue",
"line": 2,
"rule": "button-content",
"severity": "warning",
"message": "wd-button should include visible text content or an icon attribute."
}
]
}有了这种报告,AI 才算真的有机会从“生成代码”走到“对着文件和行号继续修代码”。
doctor:别上来就分析,先检查前提对不对
doctor 负责做基础环境诊断,比如:
- 有没有
package.json - 有没有安装
@wot-ui/ui - vue、uni-app、typescript 这些依赖是否齐全
node_modules是否存在
实现见 project.ts。
这个命令的作用很朴素,但很有必要。很多分析工具看起来“没扫出结果”,最后并不是规则有问题,而是目录压根就不满足运行前提。
我最想支持的,其实是这一条闭环
如果把 @wot-ui/cli 当成一个 AI Coding 工作流里的部件,我最希望它支持的是下面这个过程:
- 组件名不确定,先跑
wot_list或wot list拿候选集合 - 组件约束不确定,先用
wot_info看 props、events、slots、cssVars - 用法还没把握,继续用
wot_demo拉对应 demo 做参考 - AI 根据约束和 demo 生成或修改代码
- 改完以后,用
wot_lint或wot lint去扫本地项目 - 根据 lint 报告继续修,直到结果收敛
这一套下来,AI 才不是“答完问题就跑了”,而是真的参与了一轮可回放、可定位、可迭代的工程过程。
对应的闭环图如下:
flowchart LR
A[约束获取: wot_info / wot_token] --> B[代码生成/修改]
B --> C[事实校验: wot_lint]
C -->|issues| B
C -->|clean| D[合入/发布]顺手附一段我自己会用的 Agent 提示词
如果你在支持 MCP 的客户端里配项目规则,下面这段提示词基本可以直接拿去改一改:
你是 wot-ui v2 的工程助手。写组件相关代码前必须先通过工具获取确定性信息,不得凭记忆猜测 props、事件、slot、CSS 变量。
规则:
1) 组件名不确定时,先调用 wot_list 获取候选组件。
2) 编写/修改某组件用法前,先调用 wot_info(component) 获取 props/events/slots 约束。
3) 如果要写示例或不确定用法,先调用 wot_demo(component) 获取最接近目标的 demo 代码。
4) 如果涉及样式主题或 CSS 变量,先调用 wot_token(component) 获取变量与默认值,再给出修改建议。
5) 修改完成后必须调用 wot_lint(dir) 扫描项目,基于报告定位并修复问题;除非用户明确要求跳过。
输出要求:
优先给出可复制的代码片段与修改点列表;遇到冲突时以工具返回信息为准。我自己会比较喜欢这种“工具优先”的约束方式。不是因为 prompt 不重要,而是因为 prompt 说到底还是在提醒模型“你要认真点”,工具调用才是在真正给它确定性信息。
现在的边界,也顺便说清楚
@wot-ui/cli 现在还很克制,边界在 README.md 里也写得比较明确:
- 当前只支持
wot-ui v2 usage和lint主要聚焦.vue文件里的<wd-*>标签及相关 import- CSS 变量提取优先走 SCSS,必要时才回退到 markdown 表格
如果后面继续扩,我觉得方向还是那句老话:继续围绕“数据集就是 API”来做。
比如这些方向都挺自然:
- 让多版本数据集并存,不再只盯着
v2 - 把元数据驱动的 lint 规则扩到更多组件
- 给提取脚本和 loader 增加更稳的回归校验,早点暴露上游结构变化
我不太想把它做成一个什么都想包进去的大而全平台。至少现在阶段,我更看重的是这条链路够不够稳,输出契约够不够清楚。
最后
回过头看,@wot-ui/cli 真正想解决的问题,其实一直只有一个:
怎么把组件库知识,从“人类读得懂的文档”,变成“机器也能稳定调用的能力”。
CLI 解决的是复现和回归,MCP 解决的是协议化调用,usage/lint/doctor 解决的是和本地工程事实闭环。它们放在一起,才比较像一套完整工具链。
如果你也在做组件库、AI Coding 工具,或者正在折腾“怎么让模型别乱猜 API”,那我觉得这个方向还是挺值得试一试的。
至少对我来说,这比继续和 prompt 斗智斗勇省心多了。

