02 渲染第一个节点组件
2026/3/30大约 3 分钟
02 渲染第一个节点组件
这一篇只关心前端交互:
- 调 A2A
- 收事件
- 渲染一个最小组件
本文内容对照提交:
data-agent-frontend/src/App.vuedata-agent-frontend/src/components/toy-hello-node.vue
1. 本文目标
- 前端通过 Agent Card 建立 A2A 客户端
- 发送一条消息并消费流式事件
- 把
TOY_HELLO_NODE渲染为一个独立 Vue 组件
2. 最小数据结构
建议先用最简单的消息结构:
interface ToyStep {
id: string
name: string
content: string
status: 'pending' | 'success'
}提交里实际就是用这个结构的一个 reactive 对象来保存当前步骤:
const currentStep = reactive<ToyStep>({
content: '',
id: '',
name: '',
status: 'pending',
})只保留 UI 渲染必需字段,这样第一版不会被复杂状态拖住。
3. 建立 A2A 客户端并发送
当前提交不是 createFromUrl(...),而是先通过项目里的 api.a2acontroller.agentJson() 拿到 Agent Card,再用 createFromAgentCard(...) 创建客户端:
const factory = new ClientFactory()
client.value = await factory.createFromAgentCard(
(await api.a2acontroller.agentJson()) as AgentCard,
)这样做的好处是:
- Agent Card 获取走现有前端 API 封装
- 不需要手写发现 URL 拼接
- 前后端联调时更容易统一代理路径
发送消息的代码则和最小示例一致:
const stream = client.sendMessageStream({
message: {
messageId: crypto.randomUUID(),
role: 'user',
kind: 'message',
parts: [{ kind: 'text', text: userInput.value }],
},
})这里第一版只传一个 text part,后面接 SQL Agent 或 Graph 的时候再扩展 message parts。
4. 消费流式事件
核心只处理两类:
artifact-update:更新步骤内容status-update(completed):结束 loading
示例:
for await (const event of stream) {
if (event.kind === 'artifact-update') {
const artifact = event.artifact
if (artifact.name !== 'TOY_HELLO_NODE') continue
const textPart = artifact.parts.find((p) => p.kind === 'text')
if (textPart) {
currentStep.name = artifact.name
currentStep.content += textPart.text
currentStep.status = 'pending'
}
}
if (event.kind === 'status-update' && event.status.state === 'completed') {
currentStep.status = 'success'
}
}这里和提交保持一致,有两个值得注意的点:
currentStep.name = artifact.name
这是后面模板里 v-if="currentStep.name === 'TOY_HELLO_NODE'" 的前提,不写这句页面不会渲染节点组件。
currentStep.content += textPart.text
这里故意用累加而不是覆盖,是在给后续真正的流式增量输出留接口。现在 toy demo 可能只来一段文本,但改成逐 token 或逐片段推送时,这段代码不用改。
如果你想更稳一点,发送前可以顺手重置状态:
currentStep.name = ''
currentStep.content = ''
currentStep.status = 'pending'这样连续发送多轮消息时不会把上一轮内容混进来。
5. 渲染第一个组件
新增组件 toy-hello-node.vue:
<script setup lang="ts">
defineProps<{ content: string; status: 'pending' | 'success' }>()
</script>
<template>
<div class="toy-node">
<div class="toy-node__title">Toy Hello Node</div>
<div class="toy-node__content">{{ content || '等待数据...' }}</div>
<div class="toy-node__status">{{ status }}</div>
</div>
</template>父组件里实际是按“当前步骤名是否匹配”来渲染:
<toy-hello-node
v-if="currentStep.name === 'TOY_HELLO_NODE'"
:content="currentStep.content"
:status="currentStep.status"
/>这就是为什么后端和前端都要统一使用 TOY_HELLO_NODE 这个名字。
这个节点名本质上就是第一版 UI 路由键。
当前提交里的完整页面骨架其实非常短:
<template>
<div>
<toy-hello-node
v-if="currentStep.name === 'TOY_HELLO_NODE'"
:content="currentStep.content"
:status="currentStep.status"
/>
<el-input
v-model="userInput"
placeholder="请输入内容"
@keydown.enter="handleSend"
/>
</div>
</template>这版的重点不是样式,而是先证明:
A2A 事件 -> 前端状态 -> Vue 组件渲染
这条链路已经通了。
6. 验收标准
满足以下 3 条即完成:
- 输入一句话后,页面出现
Toy Hello Node - 组件里能看到后端返回文本
- 请求结束后状态从
pending变成success
7. 常见问题
1) 页面没有任何节点
artifact.name与前端判断不一致- 流里没有命中
artifact-update currentStep.name没有在事件处理中赋值
2) 一直 pending
- 没收到
status-update(completed) - 事件循环异常提前结束
- 你引入了 loading 状态,但完成时没有同步关闭
3) 文本为空
- 后端 artifact 不是
text part - 前端读取 part 时过滤条件写错
- 发送前没有重置
currentStep.content,导致你误判了本轮结果
