diff --git a/.ua/backlog.json b/.ua/backlog.json index 6f1bb06..d688d29 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -3284,6 +3284,15 @@ "motivation": "按钮 / 徽标 / 页签 / 分页 / 步骤条这类控件的文案是「动作」不是「内容」:双击或拖选会把按钮文字选中变蓝,观感割裂且打断连点(现全库只有 8 处零散 user-select,交互控件基本没盖住);移动端浏览器默认点按灰闪(tap highlight)与令牌皮肤同层突兀,按下反馈应由 :active 态承担。", "acceptance": "① ui.css 全局区新增一条控件清单式 user-select: none(button/tag/badge/fab/backtop/page-btn/tab/seg-btn/step/rate-star/checkbox/radio/switch/menu-item/crumb),内容型组件(代码块 / 表格 / 描述列表 / 链接)不入选、照常可复制;② 全站 ua- 类 -webkit-tap-highlight-color: transparent,不引入任何颜色字面量;③ audit:visual / audit:contrast / audit:surface / audit:motion 全绿,npm run check 通过。", "notes": "" + }, + { + "id": "g322", + "domain": "tooling", + "status": "open", + "title": "MCP get_component 支持逗号批量,与 ua info 对齐", + "motivation": "g319 给 `ua info` 加了逗号批量,但 Agent 查组件的主通道是 MCP(get_component 一次仍只能查一个);拼一个界面要 3–5 次工具往返,MCP 侧同样需要批量,并抽出 CLI / MCP 共用的名字解析函数避免两处口径漂移。", + "acceptance": "① get_component 的 name 支持逗号分隔多值:json 返回数组、md 返回以水平线分隔的多段片段,单值输出结构不变;② 不存在的 id 报错并标明序号;③ 名字解析抽成 CLI / MCP 共用函数;④ 工具 description 同步更新;⑤ npm run check 通过。", + "notes": "" } ] } diff --git a/AGENTS.md b/AGENTS.md index 801a2e2..48975ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ } ``` -可用工具:`list_components` / `get_component`(传 `format: "md"` 直接拿 Markdown 片段,等同 `ua info --md`)/ `search_components` / `get_tokens`(返回 --ua-* 令牌清单,避免硬编码颜色间距)/ `list_recipes` / `get_recipe`(取跨组件组合配方:骨架代码 + 要点 + 用到的组件 id,配好再逐个 `get_component` 查参数)。 +可用工具:`list_components` / `get_component`(name 支持逗号分隔批量一次取多个;传 `format: "md"` 直接拿 Markdown 片段,批量片段以水平线分隔,等同 `ua info --md`)/ `search_components` / `get_tokens`(返回 --ua-* 令牌清单,避免硬编码颜色间距)/ `list_recipes` / `get_recipe`(取跨组件组合配方:骨架代码 + 要点 + 用到的组件 id,配好再逐个 `get_component` 查参数)。 2. **CLI**:`node packages/tools/cli/bin/ua.js loop [--json](迭代循环诊断:锁心跳 / 上次留痕 / 待办数) | iter done (迭代收尾:置 done + 写留痕,ts 由本机时钟生成) | iter add <条目json>(补自选题) | list [--json|--components] | search <关键词> | info [--json|--md](name 支持逗号分隔一次查多个;--md 出可粘贴的 Markdown 片段:标题 + 参数表 + 用法 + 示例,批量片段以水平线分隔;--json 单值为对象、多值为数组) | recipes [编号] [--json] | index | docs | style | verify | changelog`(`--json` 给脚本与 Agent 用,不必解析文本);`search` 覆盖 id / 标题 / 参数名 / 参数说明 / 步骤五层 + 配方三层(标题 / 配方组件 / 要点,命中行直接给 `ua recipes <编号>` 指路),命中非 id 标题时会标出命中字段(如「← 参数说明含『处理中』」),`--json` 输出 `{ components, recipes }` 两数组、各项带 `matched` 字段 diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index 3d6d103..c081742 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -227,6 +227,14 @@ function nearIds(name) { .map(([id]) => id); } +/*** 名字 → 包或组件条目:包名 / @ui-arch 前缀 / 组件 id 三种写法都认;CLI info 与 MCP get_component 共用,防止两处解析口径漂移 ***/ +function resolveEntry(packages, n) { + const pkg = packages.find((x) => x.name === n || x.name === `@ui-arch/${n}`); + if (pkg) return { kind: 'pkg', pkg }; + const hit = collectComponents(packages).find((x) => x.comp.id === n); + return hit ? { kind: 'comp', hit } : null; +} + /*** 组件的可检索字段:权重从高到低(id > 标题 > 参数名 > 参数说明 > 步骤),search 与 MCP 共用一份 ***/ function searchFields(comp) { const props = comp.props ?? []; @@ -321,13 +329,7 @@ function cmdInfo(name, args = []) { const asJson = args.includes('--json'); const asMd = args.includes('--md'); const names = name.split(',').map((s) => s.trim()).filter(Boolean); // 批量:逗号分隔多组件一次拿全,省 Agent 往返 - const comps = collectComponents(packages); - const resolve = (n) => { // 单个名字 → 包或组件条目;都没命中返回 null 交由调用方报错 - const pkg = packages.find((x) => x.name === n || x.name === `@ui-arch/${n}`); - if (pkg) return { kind: 'pkg', pkg }; - const hit = comps.find((x) => x.comp.id === n); - return hit ? { kind: 'comp', hit } : null; - }; + const resolve = (n) => resolveEntry(packages, n); // 单个名字 → 包或组件条目;都没命中返回 null 交由调用方报错 if (names.length === 1) { const r = resolve(names[0]); if (!r) { @@ -799,12 +801,12 @@ function cmdMcp() { }, { name: 'get_component', - description: 'Get one package by name, or one component by its id; format=md returns a paste-ready Markdown block instead of raw JSON', + description: 'Get one package by name, or one or more components by comma-separated ids (e.g. "button,select"); format=md returns paste-ready Markdown blocks (multi-query blocks are separated by horizontal rules)', inputSchema: { type: 'object', properties: { - name: { type: 'string', description: 'component id (e.g. inputTag) or package name (e.g. @ui-arch/table-core)' }, - format: { type: 'string', enum: ['json', 'md'], description: 'json (default) returns the manifest entry; md returns a paste-ready Markdown block (title + props table + usage + example)' }, + name: { type: 'string', description: 'component id (e.g. inputTag), package name (e.g. @ui-arch/table-core), or comma-separated ids for batch query (e.g. button,select)' }, + format: { type: 'string', enum: ['json', 'md'], description: 'json (default) returns the manifest entry (array for batch); md returns a paste-ready Markdown block (title + props table + usage + example)' }, }, required: ['name'], }, @@ -855,19 +857,24 @@ function cmdMcp() { return packages.map((p) => ({ name: p.name, version: p.version, type: p.type, description: p.description })); } if (name === 'get_component') { - const p = packages.find((x) => x.name === args.name || x.name === `@ui-arch/${args.name}`); - if (p) return p; - const hit = collectComponents(packages).find((x) => x.comp.id === args.name); // 退化到组件级查询 - if (!hit) throw new Error(`not found: ${args.name}`); - if (args.format === 'md') { - // 与 ua info --md 共用同一渲染:Agent 拿到就能直接贴进回复 / 文档,不必自己排参数表 - const related = recipesFor(hit.comp.id); - const tail = related.length ? ` - -出现在配方:${related.map((r) => `${r.id}(${r.title})`).join('、')}` : ''; - return formatComponentMd(hit.pkg, hit.comp) + tail; // 直接给字符串:上层原样作为 text 返回 + const names = String(args.name).split(',').map((s) => s.trim()).filter(Boolean); // 批量:逗号分隔一次拿全多个组件,与 ua info 同口径 + const resolved = names.map((n) => resolveEntry(packages, n)); + const bad = names.filter((n, i) => !resolved[i]); + if (bad.length) throw new Error(`not found: ${bad.join(' / ')}(第 ${names.indexOf(bad[0]) + 1} 个,包名与组件 id 都没命中)`); + const compMd = (r) => { // md 模式组件片段 + 配方尾注;包条目退化为标题行 + if (r.kind === 'pkg') return `## ${r.pkg.name}(${r.pkg.type})\n\n${r.pkg.description}`; + const related = recipesFor(r.hit.comp.id); + const tail = related.length ? `\n\n出现在配方:${related.map((x) => `${x.id}(${x.title})`).join('、')}` : ''; + return formatComponentMd(r.hit.pkg, r.hit.comp) + tail; + }; + if (resolved.length > 1) { + if (args.format === 'md') return resolved.map(compMd).join('\n\n---\n\n'); // 多段片段以水平线分隔 + return resolved.map((r) => (r.kind === 'pkg' ? r.pkg : { package: r.hit.pkg.name, ...r.hit.comp, recipes: recipesFor(r.hit.comp.id) })); } - return { package: hit.pkg.name, ...hit.comp, recipes: recipesFor(hit.comp.id) }; + const r = resolved[0]; + if (r.kind === 'pkg') return r.pkg; + if (args.format === 'md') return compMd(r); // 与 ua info --md 共用同一渲染:Agent 拿到就能直接贴进回复 / 文档,不必自己排参数表 + return { package: r.hit.pkg.name, ...r.hit.comp, recipes: recipesFor(r.hit.comp.id) }; } if (name === 'search_components') { const q = String(args.query ?? '').toLowerCase().trim();