Agent 三件套:计划 / 确认门 / 产物
EbChatPlan · EbChatConfirmation · EbChatArtifact
三者共同的前提是:这不是一次问答,而是一次受控执行。宿主需要把执行计划摊开给用户看、在关键动作前拿到批准、把产出的文件交出去。三者都挂在 assistant 消息上,与 toolCalls / citations 同构——引擎只存不解析,视图只读不推断。
本页组件在独立包
@wil-works/evoke-chat里,安装与注册见 Chatbot 对话窗口顶部说明。
toolCalls 表达的是「这一步做了什么」;计划卡表达的是「整体还要做什么」。两者独立渲染,不做自动关联。
计划
message.plan = { title?, steps: [{ id, label, status, detail?, duration? }] },status 取 pending / running / done / error / skipped。
头部给标题、3/5 完成 进度与失败标记;执行中自动展开,结束后回到用户可控的折叠态(与思考块同一套「过程可见、完事让位」的约定)。
驱动计划的引擎方法与工具调用同构:
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),不与已拒绝混淆。
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。开右侧分屏的配方:
<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
ChatPlan Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| plan | { title?, steps: [{ id, label, status, detail?, duration? }] } | object | { steps: [] } |
| expanded | 受控折叠态;不传时执行中自动展开、结束后回到用户可控 | boolean | — |
ChatPlan Events
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| toggle | (plan, open) | (plan, open) => void | — |
| step-click | (step, index) | (step, index) => void | — |
ChatConfirmation Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| confirmation | { id, title, description?, actions, status, responseKey?, respondedAt? } | object | {} |
ChatConfirmation Events
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| respond | 点了某个动作;执行由宿主负责 | (confirmation, actionKey) => void | — |
ChatArtifact Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| artifacts | [{ id, title, type, language?, content?, url?, size? }] | array | [] |
| copyable | 整体开关复制动作(条目仍要求有 content) | boolean | true |
ChatArtifact Events
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| open | 查看产物;预览形态由宿主决定 | (artifact) => void | — |
| copy | 内容已写入剪贴板 | (artifact) => void | — |
三者的逐条接管插槽分别是 #step({ step, index, isCurrent })、#artifact({ artifact, index, itemProps }),沿用 ChatList#message 的 itemProps 回落约定。
刻意不做
- 不自动执行:确认门拿到选择就结束,批准后做什么与组件无关。
- 不与工具调用自动关联:不猜「这一步对应哪个 toolCall」。
- 不自持时钟:
expired由宿主标记,组件里没有定时器——否则视觉基线与测试都会被时钟驱动。