AI 工具调用
工具调用卡 .ai-tool:kind 修饰类(--shell/--read/--edit/--mcp/--rm…,kind 注册表驱动 --ai-item-tint 与图标)+ 7 态状态机(失败条目自动展开)+ 审批内联按钮(icen:ai-approve / icen:ai-reject,detail {id, kind})。renderAiToolCall(el, model) 动态建卡,返回 { el, update(patch) } 做流式状态流转;输入/输出走注册表 summarize 或压缩 JSON。
输入
bun run build
输出
tsup ✓ · build-css ✓(3.2s)
输入
{ "file": "src/utils/date.ts", "range": [1, 50] }输出
export function formatDate(…)
输入
{ "repo": "icen/ui", "title": "ai-chat 上滚后 jump 钮计数不对" }输入
{ "file": "src/config.ts", "old": "timeout: 3000", "new": "timeout: 8000" }修改运行配置需要人工确认:超时时间 3s → 8s
输入
rm dist/cache.json
输出
rm: dist/cache.json: No such file or directory
<div style="display:flex;flex-direction:column;gap:8px;width:100%"> <div class="ai-tool ai-tool--shell is-done" data-ai-id="demo-t1" data-ai-kind="shell"> <button class="ai-tool-head" type="button" aria-expanded="false"> <span class="ai-item-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m4 17 6-6-6-6"/><path d="M12 19h8"/></svg></span> <span class="ai-item-main"><span class="ai-item-title">Shell</span><span class="ai-item-sub">bun run build</span></span> <span class="ai-item-status" role="img" aria-label="完成"></span> <span class="ai-item-meta">3.2s</span> </button> <div class="ai-tool-body" hidden> <div class="ai-tool-io"><div class="ai-tool-io-label">输入</div><pre class="ai-tool-io-content">bun run build</pre></div> <div class="ai-tool-io"><div class="ai-tool-io-label">输出</div><pre class="ai-tool-io-content">tsup ✓ · build-css ✓(3.2s)</pre></div> </div> </div> <div class="ai-tool ai-tool--read is-done" data-ai-id="demo-t2" data-ai-kind="read"> <button class="ai-tool-head" type="button" aria-expanded="false"> <span class="ai-item-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg></span> <span class="ai-item-main"><span class="ai-item-title">Read</span><span class="ai-item-sub">src/utils/date.ts:1-50</span></span> <span class="ai-item-status" role="img" aria-label="完成"></span> <span class="ai-item-meta">0.4s</span> </button> <div class="ai-tool-body" hidden> <div class="ai-tool-io"><div class="ai-tool-io-label">输入</div><pre class="ai-tool-io-content">{ "file": "src/utils/date.ts", "range": [1, 50] }</pre></div> <div class="ai-tool-io"><div class="ai-tool-io-label">输出</div><pre class="ai-tool-io-content">export function formatDate(…)</pre></div> </div> </div> <div class="ai-tool ai-tool--mcp is-running" data-ai-id="demo-t3" data-ai-kind="mcp"> <button class="ai-tool-head" type="button" aria-expanded="false"> <span class="ai-item-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01"/><path d="M6 18h.01"/></svg></span> <span class="ai-item-main"><span class="ai-item-title">MCP</span><span class="ai-item-sub">github · create_issue</span></span> <span class="ai-item-status" role="img" aria-label="执行中"></span> <span class="ai-item-meta">2.8s</span> </button> <div class="ai-tool-body" hidden> <div class="ai-tool-io"><div class="ai-tool-io-label">输入</div><pre class="ai-tool-io-content">{ "repo": "icen/ui", "title": "ai-chat 上滚后 jump 钮计数不对" }</pre></div> </div> </div> <div class="ai-tool ai-tool--edit is-approval" data-ai-id="demo-t4" data-ai-kind="edit"> <button class="ai-tool-head" type="button" aria-expanded="false"> <span class="ai-item-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></span> <span class="ai-item-main"><span class="ai-item-title">Edit</span><span class="ai-item-sub">src/config.ts:12</span></span> <span class="ai-item-status" role="img" aria-label="待人确认"></span> <span class="ai-item-meta"></span> </button> <div class="ai-tool-body" hidden> <div class="ai-tool-io"><div class="ai-tool-io-label">输入</div><pre class="ai-tool-io-content">{ "file": "src/config.ts", "old": "timeout: 3000", "new": "timeout: 8000" }</pre></div> <div class="ai-tool-approval"> <div class="ai-tool-approval-reason">修改运行配置需要人工确认:超时时间 3s → 8s</div> <button class="btn btn-sm btn-primary" data-ai-approve type="button">允许</button> <button class="btn btn-sm" data-ai-reject type="button">拒绝</button> </div> </div> </div> <div class="ai-tool ai-tool--rm is-error" data-ai-id="demo-t5" data-ai-kind="rm"> <button class="ai-tool-head" type="button" aria-expanded="false"> <span class="ai-item-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></span> <span class="ai-item-main"><span class="ai-item-title">Rm</span><span class="ai-item-sub">dist/cache.json</span></span> <span class="ai-item-status" role="img" aria-label="失败"></span> <span class="ai-item-meta">0.1s</span> </button> <div class="ai-tool-body" hidden> <div class="ai-tool-io"><div class="ai-tool-io-label">输入</div><pre class="ai-tool-io-content">rm dist/cache.json</pre></div> <div class="ai-tool-io"><div class="ai-tool-io-label">输出</div><pre class="ai-tool-io-content">rm: dist/cache.json: No such file or directory</pre></div> </div> </div></div><div class="toolbar" style="margin-top:12px"> <button class="btn btn-sm btn-primary" type="button" id="ai-tool-run">模拟执行(pending → running → done)</button> <span class="toolbar-label" id="ai-tool-log">rm 失败卡自动展开 · 审批卡展开后允许 / 拒绝</span></div><div id="ai-tool-live" style="margin-top:8px"></div> 安装
bun add @icen.ai/ui pnpm add @icen.ai/ui npm install @icen.ai/ui yarn add @icen.ai/ui 一行引入该组件(CSS 与 behavior 自动带上,无需关心内部 CSS 文件名):
import { initAiTool } from '@icen.ai/ui/kit/ai-tool-call';initAiTool(); 也可以用脚手架直接打印引入行:
bunx --bun @icen.ai/ui add ai-tool-call
Astro 项目注意:kit 入口里的 CSS import 在页面 <script> 里会被 Astro 的 client bundle 摇掉——请改在布局 frontmatter 里引 CSS(import '@icen.ai/ui/components/ai-tool-call.css';),JS 行为仍可走 kit。Vite SPA / webpack 项目无此问题。
用法
import { initAiTool, renderAiToolCall } from '@icen.ai/ui/kit/ai-tool-call';initAiTool(); // 展开/折叠 + 审批按钮委托(icen:ai-approve / icen:ai-reject,detail {id, kind}) <!-- 静态卡:kind 修饰类 + 7 态;失败 is-error 首次渲染自动展开 --><div class="ai-tool ai-tool--edit is-approval" data-ai-id="t-1" data-ai-kind="edit"> <button class="ai-tool-head" aria-expanded="false">…</button> <div class="ai-tool-body" hidden> <div class="ai-tool-approval"> <button class="btn btn-sm btn-primary" data-ai-approve>允许</button> <button class="btn btn-sm" data-ai-reject>拒绝</button> </div> </div></div> // 动态建卡(流式场景),update(patch) 做状态流转const h = renderAiToolCall(el, { id: 't-2', name: 'Shell', kind: 'shell', status: 'pending', input: { cmd: 'bun run build' }, // 摘要行:注册表 summarize 或压缩 JSON});h.update({ status: 'running' });h.update({ status: 'done', output: '…', durationMs: 3200 });