diff --git a/.ua/backlog.json b/.ua/backlog.json index 111c227..221bbcc 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -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" } ] } diff --git a/.ua/index.json b/.ua/index.json index fdc95ca..761d11a 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -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": [] }, diff --git a/AGENTS.md b/AGENTS.md index 7e5b60d..955ccbf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 | index | docs | style | verify | changelog` diff --git a/README.md b/README.md index 70f8cec..19cdd7d 100644 --- a/README.md +++ b/README.md @@ -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`(自动迭代协议)。 diff --git a/llms-full.txt b/llms-full.txt index c41a577..c54e975 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -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 diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index dc6d4e1..4f410ef 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -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 ***/ diff --git a/packages/tools/cli/ua.manifest.json b/packages/tools/cli/ua.manifest.json index bbffe8b..667efb2 100644 --- a/packages/tools/cli/ua.manifest.json +++ b/packages/tools/cli/ua.manifest.json @@ -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": [] }