---
url: /chat/chat-agent.md
---
# Agent 三件套：计划 / 确认门 / 产物

`EbChatPlan` · `EbChatConfirmation` · `EbChatArtifact`

三者共同的前提是：这不是一次问答，而是一次**受控执行**。宿主需要把执行计划摊开给用户看、在关键动作前拿到批准、把产出的文件交出去。三者都挂在 assistant 消息上，与 `toolCalls` / `citations` 同构——引擎只存不解析，视图只读不推断。

> 本页组件在独立包 `@wil-works/evoke-chat` 里，安装与注册见 [Chatbot 对话窗口](/chat/chatbot)顶部说明。

`toolCalls` 表达的是「这一步做了什么」；计划卡表达的是「整体还要做什么」。两者独立渲染，不做自动关联。

## 计划

`message.plan = { title?, steps: [{ id, label, status, detail?, duration? }] }`，`status` 取 `pending / running / done / error / skipped`。

头部给标题、`3/5 完成` 进度与失败标记；执行中自动展开，结束后回到用户可控的折叠态（与思考块同一套「过程可见、完事让位」的约定）。

驱动计划的引擎方法与工具调用同构：

```js
const msg = engine.createAssistantMessage()
engine.setPlan(msg.id, { title: '对齐季度口径', steps: [
  { id: 's1', label: '拉取两张表' },
  { id: 's2', label: '比对差异' },
  { id: 's3', label: '输出报告' },
] })

engine.startPlanStep(msg.id, 's1')
engine.completePlanStep(msg.id, 's1', '拉到了 2 张表')   // 自动记耗时
engine.failPlanStep(msg.id, 's2', new Error('接口 500'))
engine.skipPlanStep(msg.id, 's2')
```

## 确认门

`message.confirmation = { id, title, description?, actions: [{ key, label, type?, icon?, status? }], status, responseKey? }`，`status` 取 `pending / approved / rejected / expired`。**响应后进入只读态**并显示选了哪一项，避免用户以为还能改。

`actions[].type` 取 `EbButton` 那套（`default / primary / success / warning / info / danger / text`），**按钮本身就是 `EbButton`（`size="small"`）**——外观、焦点环、禁用态都由它承担；动作可给 `icon`（库内图标名，如批准用 `check-circle`、拒绝用 `close-circle`），**响应后那颗挑中的自动换成对勾**（「选了哪个」比「这颗是什么动作」更该被看见），卡片另加「已选中」标记（响应后那颗挑中的保留主色描边 + 对勾，其余转禁用）。

动作的**语义由它自己声明**：`actions[].status` 写 `approved` 或 `rejected`。没写时按 `type` 兜底——`danger` 视为拒绝、其余视为批准；这个兜底只保证「点了拒绝不会显示成已批准」这个方向不出错，业务含义仍归你。超时是独立形态（`expired`），不与已拒绝混淆。

```js
engine.setConfirmation(msg.id, {
  id: 'c1',
  title: '即将删除 3 个文件',
  description: '此操作不可撤销',
  status: 'pending',
  actions: [
    { key: 'approve', label: '批准执行', type: 'primary', icon: 'check-circle', status: 'approved' },
    { key: 'reject', label: '拒绝', type: 'danger', icon: 'close-circle', status: 'rejected' },
  ],
})

// 事件回调里记下结果，真正要执行什么由你的代码决定
engine.respondConfirmation(msg.id, actionKey)
```

**确认门只负责拿到选择，不负责执行**。批准之后调什么接口、跑什么命令，由宿主决定——家族不发请求的原则在这里同样成立。

## 产物

`message.artifacts = [{ id, title, type, language?, content?, url?, size? }]`。

卡片给类型图标、标题、`语言 · 体积`；动作有查看（`open`）、复制内容（有 `content` 时）、下载（有 `url` 时）。类型认不出来时按扩展名回落，再不行用通用文件图标。

**组件不做内嵌预览**：预览是布局问题（分屏 / 抽屉 / 新页），它只抛 `open`。开右侧分屏的配方：

```vue
<eb-splitter>
  <template #left><eb-chatbot v-model="messages" @artifact-open="openArtifact" /></template>
  <template #right><pre class="preview">{{ activeArtifact?.content }}</pre></template>
</eb-splitter>
```

## API

三者的逐条接管插槽分别是 `#step`（`{ step, index, isCurrent }`）、`#artifact`（`{ artifact, index, itemProps }`），沿用 `ChatList#message` 的 `itemProps` 回落约定。

## 刻意不做

* **不自动执行**：确认门拿到选择就结束，批准后做什么与组件无关。
* **不与工具调用自动关联**：不猜「这一步对应哪个 toolCall」。
* **不自持时钟**：`expired` 由宿主标记，组件里没有定时器——否则视觉基线与测试都会被时钟驱动。
