业务卡片

后端想在回答里显示订单卡片、引用来源、图表时,用业务卡片:后端返回数据,界面按数据类型找登记的组件来显示。

custom-card.tsx
import '@volcengine/chatkit/styles.css';

import { Chatkit, type ChatkitDataPartRenderer, useChatRuntime } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import { useState } from 'react';

// Mock 的这个场景在回答里带一块类型为 acme.order-card 的数据;
// 接真实后端时,由适配器的 transformOutput 从后端的输出里取出同样的数据。
const adapter = createMockAdapter({ getScenarioId: () => 'custom-data' });

interface Order {
  orderId: string;
  item: string;
  count: number;
  amount: string;
  status: string;
}

// 按数据类型登记的组件:data 是后端给的,原样交过来
const OrderCard: ChatkitDataPartRenderer = ({ data }) => {
  const order = data as Order;
  return (
    <div className="demo-card">
      <header>
        订单 {order.orderId}
        <span className="demo-badge">{order.status}</span>
      </header>
      {order.item} × {order.count}
      <div>
        <strong>{order.amount}</strong>
      </div>
    </div>
  );
};

// 业务卡片:renderers.parts 按数据类型找组件。没有登记的类型不显示,不会把 JSON 显示给用户。
export default function CustomCard() {
  const runtime = useChatRuntime({ sessionId: 'custom-card', adapter });
  const [registered, setRegistered] = useState(true);
  if (!runtime) return null;
  return (
    <div className="demo-column">
      <div className="demo-bar">
        <label>
          <input type="checkbox" checked={registered} onChange={event => setRegistered(event.target.checked)} />
          登记 acme.order-card 的组件
        </label>
      </div>
      <div className="demo-main">
        <Chatkit
          runtime={runtime}
          settings={{ recommendations: ['帮我买两台云服务器'] }}
          renderers={{ parts: registered ? { 'acme.order-card': OrderCard } : {} }}
        />
      </div>
    </div>
  );
}
回答里的订单卡片:按数据类型找组件新窗口打开

一条链路

后端返回的业务数据
  └─ 适配器的 transformOutput 把它变成回答里的一块数据 { type: 'data', kind: 'acme.order', data }
       └─ renderers.parts['acme.order'] 把它显示出来

用法

import type { ChatkitDataPartRenderer } from '@volcengine/chatkit';

const OrderCard: ChatkitDataPartRenderer = ({ data, isRunning }) => (
  <MyOrderCard order={data as Order} pending={isRunning} />
);

<Chatkit sessions={sessions} renderers={{ parts: { 'acme.order': OrderCard } }} />;

组件拿到的参数

参数说明
kind数据类型,就是登记时用的名字
data后端给的数据,原样交过来
part当前数据片段,包含 kind、data 等字段
message所在的消息
partIndex这块数据在消息里的序号
isRunning所在的消息是否还在输出
run可选的运行信息;仅有消息的历史可能没有此字段

数据类型的命名

  • 形如 业务名.类型:至少两段,用点号分隔;每段以小写字母或数字开头,后续可含小写字母、数字、连字符。例如 acme.order、acme.citation-list。
  • chatkit. 开头的名字保留给内置显示(chatkit.run-status、chatkit.notice……)。
  • 没有登记的类型不显示,不会自动展示 JSON。关键结果不能只放在尚未登记的卡片里,否则用户看不到。
  • 登记成 null 表示不显示,也可以用来关掉某个内置显示。

适配器:transformOutput

内置适配器都有 transformOutput 选项:适配器处理完后端的一段输出之后调用,返回默认内容与业务变更,由运行时统一校验并应用。

adk.ts
import { createAdkAdapter } from '@volcengine/chatkit/adapters/adk';

// ADK:后端把订单放在事件的 customMetadata 里
export const adapter = createAdkAdapter({
  run: ({ body }) =>
    fetch('/api/chat/run', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }),
  transformOutput: (event, context) => {
    const parts = (() => {
      const order = (event.customMetadata as { order?: unknown } | undefined)?.order;
      return order ? [{ id: `order:${event.id}`, type: 'data' as const, kind: 'acme.order', data: order }] : undefined;
    })();
    return parts && context.messageId
      ? [
          ...context.defaultChanges,
          ...parts.map(part => ({ type: 'set-part' as const, messageId: context.messageId!, part })),
        ]
      : undefined;
  },
});
openai.ts
import { createOpenAiAdapter } from '@volcengine/chatkit/adapters/openai';

// OpenAI 兼容接口:代理在流块里加了 citations 字段
export const adapter = createOpenAiAdapter({
  run: ({ body }) =>
    fetch('/api/chat/completions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }),
  transformOutput: (chunk, context) => {
    const parts = (() => {
      const { citations } = chunk as { citations?: unknown };
      return Array.isArray(citations)
        ? [{ id: 'citations', type: 'data' as const, kind: 'acme.citations', data: citations }]
        : undefined;
    })();
    return parts && context.messageId
      ? [
          ...context.defaultChanges,
          ...parts.map(part => ({ type: 'set-part' as const, messageId: context.messageId!, part })),
        ]
      : undefined;
  },
});
适配器transformOutput 拿到的数据通常在哪不写 transformOutput 时
ADK每个 ADK 事件customMetadata、某个工具的结果不产出业务数据
A2A每条流事件协议自带的数据片段、事件的 metadata类型优先取合法且不以 chatkit. 开头的 metadata.kind,否则用 a2a.data;仍需登记相应渲染器
OpenAI 兼容接口每个流块兼容实现加的私有字段不产出业务数据
Hermes每个流事件事件里的业务字段不产出业务数据
Mock每一步脚本脚本的数据步骤数据步骤直接产出

transformOutput 返回内容变更,不直接返回片段。保留默认内容时展开 context.defaultChanges;undefined 保留默认内容,[] 隐藏本事件的默认内容。set-part 在已存在的消息里新增或替换 data,update-part 更新标题或附加数据。

函数必须同步且无副作用。独立接口异步返回的数据使用 runtime.updateContent。历史要保留业务字段,并使用稳定的片段 id。完整参数、返回值、失败处理和可运行示例见 适配器扩展 API。

配置项

配置项类型默认说明
renderers.partsRecord<string, ChatkitDataPartRenderer>—业务卡片:按数据类型(`产品名.类型`)登记;登记成 null 表示不显示。

说明

  • 业务卡片按消息里的片段顺序显示。transformOutput 和 runtime.updateContent 不负责持久保存业务数据;历史需要返回或重新构造同样的片段及稳定 id,否则刷新后卡片可能消失。
  • data 的类型是 unknown,类型断言不会校验后端数据。组件应检查必要字段,对不完整数据提供占位或返回 null。
  • 卡片里要发消息、操作输入框、打开面板,用公开 hooks,见 Hook 参考。
  • 卡片渲染抛错由所在助手消息的错误边界处理,可能使整条消息回退为文本预览,并调用 onError;不是逐卡片隔离。需要只替换单张卡片时,由业务组件自行提供错误边界。
  • 工具调用的显示不用业务卡片,见工具展示;对话里的系统提示见失败卡片与系统提示。