项目正在快速迭代中,API 与视觉细节可能随版本调整,请勿用于生产环境。

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 完成 进度与失败标记;执行中自动展开,结束后回到用户可控的折叠态(与思考块同一套「过程可见、完事让位」的约定)。

我
我

把上季度报表口径对齐一下

A
AI助手
  1. 完成820ms
  2. 完成1.2s
  3. 执行中
  4. 等待

按三步走,前两步已完成,第三步在跑。

点某一步试试;这个演示里计划是静态数据

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

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),不与已拒绝混淆。

A
AI助手

即将删除 3 个历史文件

此操作不可撤销,删除后只能从备份恢复。

等待确认

确认后由你的代码执行相应动作

这一步会改动数据,先请你确认。

点批准或拒绝;点完按钮进入只读态
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 时)。类型认不出来时按扩展名回落,再不行用通用文件图标。

A
AI助手

报告已生成。

口径对齐报告.md46 B
diff.csv20.0 KB
sync.tstypescript · 28 B
点「查看」——预览形态由你的页面决定

组件不做内嵌预览:预览是布局问题(分屏 / 抽屉 / 新页),它只抛 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 ​

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)booleantrue

ChatArtifact Events

参数说明类型默认值
open查看产物;预览形态由宿主决定(artifact) => void—
copy内容已写入剪贴板(artifact) => void—

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

刻意不做 ​

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