iter(g148): MCP 暴露组合配方检索
新增 list_recipes / get_recipe 两个工具,配方解析统一走 recipes.md。 顺带把误嵌在 renderAgentDocs 内部的 loadRecipes 提到顶层, 并给 verify 加配方结构校验:编号连续、代码骨架、要点、通用约定段。
This commit is contained in:
@@ -1540,6 +1540,14 @@
|
|||||||
"acceptance": "recipes.md 补配方 7-9(筛选栏、评论输入、详情页排版),只消费对外 API,随 npm run docs 进入 llms-full.txt;npm run check 全绿",
|
"acceptance": "recipes.md 补配方 7-9(筛选栏、评论输入、详情页排版),只消费对外 API,随 npm run docs 进入 llms-full.txt;npm run check 全绿",
|
||||||
"notes": "自补:Agent 文档完善",
|
"notes": "自补:Agent 文档完善",
|
||||||
"status": "done"
|
"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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T07:43:07.847Z",
|
"generatedAt": "2026-10-26T07:47:10.174Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -3586,8 +3586,7 @@
|
|||||||
"查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
|
"查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
|
||||||
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码",
|
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码",
|
||||||
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
|
"改完清单跑 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 / get_tokens / list_recipes / get_recipe"
|
||||||
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components"
|
|
||||||
],
|
],
|
||||||
"components": []
|
"components": []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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`
|
2. **CLI**:`node packages/tools/cli/bin/ua.js list | info <name> | index | docs | style | verify | changelog`
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup
|
|||||||
|
|
||||||
- 静态:`llms.txt`(速览)/ **`llms-full.txt`(全量:逐组件参数 / 回调 / 手把手 / 令牌)**
|
- 静态:`llms.txt`(速览)/ **`llms-full.txt`(全量:逐组件参数 / 回调 / 手把手 / 令牌)**
|
||||||
- 命令行:`node packages/tools/cli/bin/ua.js list | info <包名|组件id> | docs | tokens | verify | mcp`
|
- 命令行:`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`(自动迭代协议)。
|
规范见 `AGENTS.md`(接入手册)、`ARCHITECTURE.md`(分层与依赖方向)、`CODING.md`(编码规范)、`ITERATION.md`(自动迭代协议)。
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @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)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -44,8 +44,7 @@
|
|||||||
2. 查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)
|
2. 查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)
|
||||||
3. 查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码
|
3. 查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码
|
||||||
4. 改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)
|
4. 改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)
|
||||||
5. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens
|
5. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe
|
||||||
6. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components
|
|
||||||
|
|
||||||
## @ui-arch/ui(skin-dom)v0.1.0
|
## @ui-arch/ui(skin-dom)v0.1.0
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const INDEX_DIR = path.join(ROOT, '.ua');
|
|||||||
const INDEX_FILE = path.join(INDEX_DIR, 'index.json');
|
const INDEX_FILE = path.join(INDEX_DIR, 'index.json');
|
||||||
const LLMS_FILE = path.join(ROOT, 'llms.txt');
|
const LLMS_FILE = path.join(ROOT, 'llms.txt');
|
||||||
const LLMS_FULL = path.join(ROOT, 'llms-full.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/ 两层目录) ***/
|
/*** 汇总各包 ua.manifest.json 与 package.json 版本(扫描 packages/ 两层目录) ***/
|
||||||
function collectManifests() {
|
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):无图形纯文本,逐组件参数 / 回调 / 步骤 / 约定全量收录 ***/
|
/*** 渲染 Agent 文档(llms-full.txt):无图形纯文本,逐组件参数 / 回调 / 步骤 / 约定全量收录 ***/
|
||||||
function renderAgentDocs(index, tokens) {
|
function renderAgentDocs(index, tokens) {
|
||||||
const lines = [
|
const lines = [
|
||||||
@@ -363,12 +390,6 @@ function renderAgentDocs(index, tokens) {
|
|||||||
}
|
}
|
||||||
lines.push('---', '');
|
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(含字号、间距复合值)
|
const tokenList = loadTokenList(); // 令牌唯一来源:tokens.css(含字号、间距复合值)
|
||||||
if (tokenList.length) {
|
if (tokenList.length) {
|
||||||
lines.push('## 设计令牌(--ua-*)', '');
|
lines.push('## 设计令牌(--ua-*)', '');
|
||||||
@@ -439,7 +460,7 @@ function cmdTokens(args) {
|
|||||||
|
|
||||||
// ---- 内置 stdio MCP server(JSON-RPC 2.0,按行分帧) ----
|
// ---- 内置 stdio MCP server(JSON-RPC 2.0,按行分帧) ----
|
||||||
|
|
||||||
/*** 启动 MCP server:向 Agent 暴露 list / get / search / tokens 四个查询工具 ***/
|
/*** 启动 MCP server:向 Agent 暴露 list / get / search / tokens / recipes 六类查询工具 ***/
|
||||||
function cmdMcp() {
|
function cmdMcp() {
|
||||||
if (!fs.existsSync(INDEX_FILE)) cmdIndex();
|
if (!fs.existsSync(INDEX_FILE)) cmdIndex();
|
||||||
const { packages } = loadIndex();
|
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',
|
description: 'Get the --ua-* design token list (cssVar / value / dark), so styles never hardcode colors or spacing',
|
||||||
inputSchema: { type: 'object', properties: {} },
|
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');
|
const send = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
|
||||||
|
|
||||||
/*** 执行 MCP 工具调用 ***/
|
/*** 执行 MCP 工具调用 ***/
|
||||||
@@ -505,6 +547,14 @@ function cmdMcp() {
|
|||||||
if (!list.length) throw new Error('tokens.json not found');
|
if (!list.length) throw new Error('tokens.json not found');
|
||||||
return list;
|
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}`);
|
throw new Error(`unknown tool: ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,6 +919,22 @@ function checkIterations() {
|
|||||||
return issues;
|
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 必须含每个区块锚点,证明演示代码真的编进了产物(防「只改源码、产物过期」) ***/
|
/*** 演示产物校验:打包后的 demo.html 必须含每个区块锚点,证明演示代码真的编进了产物(防「只改源码、产物过期」) ***/
|
||||||
function checkDemoBundle(anchors) {
|
function checkDemoBundle(anchors) {
|
||||||
const issues = [];
|
const issues = [];
|
||||||
@@ -958,6 +1024,7 @@ function cmdVerify() {
|
|||||||
const tokenVars = new Set(loadTokenList().map((t) => t.cssVar)); // 令牌白名单:tokens.json 拍平结果
|
const tokenVars = new Set(loadTokenList().map((t) => t.cssVar)); // 令牌白名单:tokens.json 拍平结果
|
||||||
if (demoSource) issues.push(...checkDemoBundle(anchors)); // 产物与源码同步校验
|
if (demoSource) issues.push(...checkDemoBundle(anchors)); // 产物与源码同步校验
|
||||||
if (!demoSource) console.log('verify: 未找到 apps/demo/main.ts,跳过 demo 锚点校验');
|
if (!demoSource) console.log('verify: 未找到 apps/demo/main.ts,跳过 demo 锚点校验');
|
||||||
|
issues.push(...checkRecipes()); // 配方结构:编号连续 + 骨架 + 要点,保证 MCP 取得到
|
||||||
|
|
||||||
for (const pkg of pkgs) {
|
for (const pkg of pkgs) {
|
||||||
const comps = pkg.docs?.components ?? []; // 清单已被 collectManifests 摊平
|
const comps = pkg.docs?.components ?? []; // 清单已被 collectManifests 摊平
|
||||||
@@ -1006,7 +1073,7 @@ function cmdVerify() {
|
|||||||
console.error(`${issues.length} violation(s)`);
|
console.error(`${issues.length} violation(s)`);
|
||||||
process.exit(1);
|
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 ***/
|
/*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/
|
||||||
|
|||||||
@@ -39,8 +39,7 @@
|
|||||||
"查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
|
"查组件:ua list 看全量,ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
|
||||||
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码",
|
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码",
|
||||||
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
|
"改完清单跑 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 / get_tokens / list_recipes / get_recipe"
|
||||||
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components"
|
|
||||||
],
|
],
|
||||||
"components": []
|
"components": []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user