iter(g148): MCP 暴露组合配方检索

新增 list_recipes / get_recipe 两个工具,配方解析统一走 recipes.md。
顺带把误嵌在 renderAgentDocs 内部的 loadRecipes 提到顶层,
并给 verify 加配方结构校验:编号连续、代码骨架、要点、通用约定段。
This commit is contained in:
HIsheR
2026-10-26 15:48:21 +08:00
parent 38284ac425
commit 4a655d2dea
7 changed files with 90 additions and 18 deletions
+8
View File
@@ -1540,6 +1540,14 @@
"acceptance": "recipes.md 补配方 7-9(筛选栏、评论输入、详情页排版),只消费对外 API,随 npm run docs 进入 llms-full.txt;npm run check 全绿",
"notes": "自补:Agent 文档完善",
"status": "done"
},
{
"id": "g148",
"title": "MCP 暴露组合配方检索(list_recipes / get_recipe)",
"motivation": "recipes.md 已有 9 条跨组件配方,但只写在文档里:Agent 通过 MCP 只能查单组件参数,拿不到「怎么拼」,容易各自造轮子或漏掉约定。",
"acceptance": "ua mcp 新增 list_recipes(id/title/components)与 get_recipe(骨架代码 + 要点 + 用到的组件)两个工具;配方解析复用 recipes.md 单一来源;verify 增加配方结构校验(编号连续、每条有代码块与要点、有通用约定段);AGENTS/README/CLI 清单文档同步。",
"notes": "",
"status": "open"
}
]
}
+2 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T07:43:07.847Z",
"generatedAt": "2026-10-26T07:47:10.174Z",
"repoRoot": ".",
"packages": [
{
@@ -3586,8 +3586,7 @@
"查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码",
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens",
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components"
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"
],
"components": []
},
+1 -1
View File
@@ -48,7 +48,7 @@
}
```
可用工具:`list_components` / `get_component` / `search_components` / `get_tokens`(返回 --ua-* 令牌清单,避免硬编码颜色间距)。
可用工具:`list_components` / `get_component` / `search_components` / `get_tokens`(返回 --ua-* 令牌清单,避免硬编码颜色间距)/ `list_recipes` / `get_recipe`(取跨组件组合配方:骨架代码 + 要点 + 用到的组件 id,配好再逐个 `get_component` 查参数)。
2. **CLI**:`node packages/tools/cli/bin/ua.js list | info <name> | index | docs | style | verify | changelog`
+1 -1
View File
@@ -48,7 +48,7 @@ accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup
- 静态:`llms.txt`(速览)/ **`llms-full.txt`(全量:逐组件参数 / 回调 / 手把手 / 令牌)**
- 命令行:`node packages/tools/cli/bin/ua.js list | info <包名|组件id> | docs | tokens | verify | mcp`
- MCP:在 `~/.workbuddy/mcp.json` 注册 `ua mcp`,可用 `list_components` / `get_component` / `search_components` / `get_tokens`
- MCP:在 `~/.workbuddy/mcp.json` 注册 `ua mcp`,可用 `list_components` / `get_component` / `search_components` / `get_tokens` / `list_recipes` / `get_recipe`
规范见 `AGENTS.md`(接入手册)、`ARCHITECTURE.md`(分层与依赖方向)、`CODING.md`(编码规范)、`ITERATION.md`(自动迭代协议)。
+2 -3
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T07:43:07.847Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 自动生成于 2026-10-26T07:47:10.174Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手
@@ -44,8 +44,7 @@
2. 查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)
3. 查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码
4. 改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)
5. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens
6. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components
5. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe
## @ui-arch/ui(skin-dom)v0.1.0
+75 -8
View File
@@ -25,6 +25,7 @@ const INDEX_DIR = path.join(ROOT, '.ua');
const INDEX_FILE = path.join(INDEX_DIR, 'index.json');
const LLMS_FILE = path.join(ROOT, 'llms.txt');
const LLMS_FULL = path.join(ROOT, 'llms-full.txt');
const RECIPES_FILE = path.join(ROOT, 'packages', 'tools', 'cli', 'recipes.md'); // 组合配方单一来源(人类版与 MCP 共用)
/*** 汇总各包 ua.manifest.json 与 package.json 版本(扫描 packages/ 两层目录) ***/
function collectManifests() {
@@ -311,6 +312,32 @@ document.getElementById('q').addEventListener('input',function(e){
`;
}
/*** 读取组合配方(与 CLI 同目录的 recipes.md),缺失时返回空串 ***/
function loadRecipes() {
return fs.existsSync(RECIPES_FILE) ? fs.readFileSync(RECIPES_FILE, 'utf8').trim() : '';
}
/*** 把 recipes.md 拆成结构化配方:每个「## 配方 N:标题」一段,切出首个代码块与要点行 ***/
function parseRecipes() {
const text = loadRecipes().replace(/\r\n?/g, '\n'); // 归一化行尾:CRLF 的 \r 会让 `.+\$` 与标题目录匹配整体失效
if (!text) return [];
const out = [];
const chunks = text.split(/^## /m).slice(1); // 首段是文件标题与引言,无编号直接跳过
for (const chunk of chunks) {
const [headLine, ...rest] = chunk.split('\n');
const head = headLine.match(/^配方\s*(\d+)\s*[::]\s*(.+)$/); // 只认编号配方,「通用约定」等说明段不算
if (!head) continue;
const body = rest.join('\n').trim();
const code = body.match(/```[a-z]*\n([\s\S]*?)```/); // 首个代码块即最小可运行骨架
const notes = body
.split('\n')
.filter((line) => /^[-*]\s+/.test(line))
.map((line) => line.replace(/^[-*]\s+/, '').trim());
out.push({ id: head[1], title: head[2].trim(), code: code ? code[1].trim() : '', notes, text: body });
}
return out;
}
/*** 渲染 Agent 文档(llms-full.txt):无图形纯文本,逐组件参数 / 回调 / 步骤 / 约定全量收录 ***/
function renderAgentDocs(index, tokens) {
const lines = [
@@ -363,12 +390,6 @@ function renderAgentDocs(index, tokens) {
}
lines.push('---', '');
}
/*** 读取组合配方(与 CLI 同目录的 recipes.md),缺失时返回空串 ***/
function loadRecipes() {
const p = path.join(ROOT, 'packages', 'tools', 'cli', 'recipes.md');
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').trim() : '';
}
const tokenList = loadTokenList(); // 令牌唯一来源:tokens.css(含字号、间距复合值)
if (tokenList.length) {
lines.push('## 设计令牌(--ua-*)', '');
@@ -439,7 +460,7 @@ function cmdTokens(args) {
// ---- 内置 stdio MCP server(JSON-RPC 2.0,按行分帧) ----
/*** 启动 MCP server:向 Agent 暴露 list / get / search / tokens 四个查询工具 ***/
/*** 启动 MCP server:向 Agent 暴露 list / get / search / tokens / recipes 六类查询工具 ***/
function cmdMcp() {
if (!fs.existsSync(INDEX_FILE)) cmdIndex();
const { packages } = loadIndex();
@@ -473,8 +494,29 @@ function cmdMcp() {
description: 'Get the --ua-* design token list (cssVar / value / dark), so styles never hardcode colors or spacing',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'list_recipes',
description: 'List multi-component recipes (how to compose components together) with id/title/components',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'get_recipe',
description: 'Get one recipe by id: full markdown body, the minimal code skeleton and the components it uses',
inputSchema: {
type: 'object',
properties: { id: { type: 'string', description: 'recipe id, e.g. 3(对应「配方 3」)' } },
required: ['id'],
},
},
];
const recipes = parseRecipes(); // 配方清单:MCP 侧只读一次
const compIds = collectComponents(packages).map((x) => x.comp.id); // 组件全集:把配方里的组件名回指到注册表
/*** 从配方正文里认出用到的组件 id(按词边界匹配,避免 toast 命中 toastError) ***/
const recipeComponents = (r) =>
compIds.filter((id) => new RegExp(`\\b${id}\\b`).test(`${r.code}\n${r.text}`)).sort();
const send = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
/*** 执行 MCP 工具调用 ***/
@@ -505,6 +547,14 @@ function cmdMcp() {
if (!list.length) throw new Error('tokens.json not found');
return list;
}
if (name === 'list_recipes') {
return recipes.map((r) => ({ id: r.id, title: r.title, components: recipeComponents(r) }));
}
if (name === 'get_recipe') {
const hit = recipes.find((r) => r.id === String(args.id ?? '').trim());
if (!hit) throw new Error(`recipe not found: ${args.id}(可用 ${recipes.map((r) => r.id).join('/')})`);
return { id: hit.id, title: hit.title, components: recipeComponents(hit), code: hit.code, notes: hit.notes, markdown: hit.text };
}
throw new Error(`unknown tool: ${name}`);
}
@@ -869,6 +919,22 @@ function checkIterations() {
return issues;
}
/*** 配方结构校验:编号连续不重复、每条都有最小代码块与要点、通用约定段存在(MCP 与 llms-full 都按这个结构取用) ***/
function checkRecipes() {
const issues = [];
const text = loadRecipes();
if (!text) return ['recipes.md 缺失或为空:组合配方是 Agent 侧的主要拼装依据'];
const list = parseRecipes();
if (!list.length) issues.push('recipes.md 里没有任何「## 配方 N:标题」段落,MCP 的 list_recipes 会返回空表');
list.forEach((r, i) => {
if (r.id !== String(i + 1)) issues.push(`配方编号不连续:第 ${i + 1} 条是「配方 ${r.id}」,应为「配方 ${i + 1}」`);
if (!r.code) issues.push(`配方 ${r.id}(${r.title})缺少 ts 代码块:Agent 拿不到最小骨架`);
if (!r.notes.length) issues.push(`配方 ${r.id}(${r.title})缺少要点行(以 - 开头的说明)`);
});
if (!/^## 通用约定/m.test(text)) issues.push('recipes.md 缺少「## 通用约定」段:跨配方的共同约束没地方落');
return issues;
}
/*** 演示产物校验:打包后的 demo.html 必须含每个区块锚点,证明演示代码真的编进了产物(防「只改源码、产物过期」) ***/
function checkDemoBundle(anchors) {
const issues = [];
@@ -958,6 +1024,7 @@ function cmdVerify() {
const tokenVars = new Set(loadTokenList().map((t) => t.cssVar)); // 令牌白名单:tokens.json 拍平结果
if (demoSource) issues.push(...checkDemoBundle(anchors)); // 产物与源码同步校验
if (!demoSource) console.log('verify: 未找到 apps/demo/main.ts,跳过 demo 锚点校验');
issues.push(...checkRecipes()); // 配方结构:编号连续 + 骨架 + 要点,保证 MCP 取得到
for (const pkg of pkgs) {
const comps = pkg.docs?.components ?? []; // 清单已被 collectManifests 摊平
@@ -1006,7 +1073,7 @@ function cmdVerify() {
console.error(`${issues.length} violation(s)`);
process.exit(1);
}
console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌,README 组件清单与分组同步,backlog ↔ 留痕一致)');
console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌,README 组件清单与分组同步,backlog ↔ 留痕一致,组合配方结构完整)');
}
/*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/
+1 -2
View File
@@ -39,8 +39,7 @@
"查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码",
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens",
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components"
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"
],
"components": []
}