失败卡片与系统提示

适用范围:本页示例随 chatkit 仓库 main 源码维护,源码可能领先于已发布包。使用 npm 安装包时,请核对所安装版本的类型声明是否提供 renderers.errors 的 renderDefault/defaultText 和 settings.answerPending,并查阅版本与兼容承诺及对应版本的更新说明。

一轮失败时通过 renderers.errors 按错误码选择渲染器。渲染器可用内置样式覆盖标题和正文,也可换掉整张卡片;对话里的系统提示可按类型自定义。

失败卡片:renderers.errors

先在下面的在线示例中发送“开始一个会失败的任务”,再切换三种模式。Mock 的 run-failed 失败步骤给出 code: 'MODEL_OVERLOADED' 和原始说明,所以命中精确键 renderers.errors.MODEL_OVERLOADED;已登记精确键时不会走 *。

模式精确 renderer 返回值页面上看到什么
SDK 原样默认renderDefault()内置标题、正文和附加的 Mock 原始说明;不登记 renderer 或返回 undefined 也显示此卡片
覆盖默认卡片文案renderDefault({ title, content })沿用 SDK 卡片外观,显示宿主标题和 Markdown 正文;显式覆盖正文后不再自动附加原文
整卡自定义<BusyCard text={formattedContent} />宿主绘制整张卡片,并自行显示查表前的原始说明

运行下面的完整示例:左侧是可作为 Vite React 18 / 19 项目 src/App.tsx 的组件文件,已引入 Chatkit 样式并自带页面布局。按以下顺序准备;本地 tarball 使用仓库的版本字段,不能仅凭该字段认定公网同名版本包含相同接口。

  1. 用 Vite 的 react-ts 模板创建项目,保留生成的 index.html 和 src/main.tsx;将项目的 React、React DOM 及其类型包固定到同一个主版本(18.x 或 19.x)。
  2. 在 chatkit 仓库检出 main,用同一份源码构建 SDK 和取得示例:运行 pnpm install --frozen-lockfile、pnpm run build,再从 packages/chatkit 运行 pnpm pack --pack-destination <Vite 项目绝对路径>。在 Vite 项目中安装生成的本地 .tgz;若改用公网包,先按安装版本核对类型声明和版本说明。
  3. 将同一份源码中的 site/demos/src/demos/error-renderers.tsx 复制到 Vite 项目的 src/App.tsx,执行 pnpm exec tsc --noEmit -p tsconfig.app.json 和 pnpm run build,再启动页面并发送推荐输入。Mock 适配器、会话 ID 和三种回调都在该文件中;Vite 的入口负责将 App 挂到 #root。接真实服务时需替换 Mock 适配器。

此页面里的其他短代码块仅说明局部 API,不能作为完整应用单独运行。

error-renderers.tsx
import '@volcengine/chatkit/styles.css';

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

// Mock 的 run-failed 场景在失败时给出 code: MODEL_OVERLOADED。
const adapter = createMockAdapter({ getScenarioId: () => 'run-failed' });

// 本文件可作为 Vite 的 src/App.tsx 使用;页面布局不依赖文档站的 demo-* 样式。
const buttonStyle = (active = false): CSSProperties => ({
  padding: '6px 10px',
  border: '1px solid #b8b7c3',
  borderRadius: 8,
  background: active ? '#0b0b0f' : '#fff',
  color: active ? '#fff' : '#0b0b0f',
  cursor: 'pointer',
});

// 整卡自定义:按钮发送一条新消息,不重跑失败的原 run。
function BusyCard({ text }: { text: string }) {
  const { send } = useChatkitActions();
  return (
    <div style={{ padding: 12, border: '1px solid #d6d6dd', borderRadius: 8, background: '#fff' }}>
      <header>整卡自定义:现在用的人有点多</header>
      <span style={{ color: '#666' }}>{text}</span>
      <footer>
        <button type="button" style={buttonStyle()} onClick={() => void send('请再试一次')}>
          发送新消息
        </button>
      </footer>
    </div>
  );
}

// 同一个 Mock 失败码、同一个精确 renderer:比较原样默认、覆盖文案和整卡自定义。
export default function ErrorRenderers() {
  const [mode, setMode] = useState<'sdk' | 'patch' | 'custom'>('sdk');
  const runtime = useChatRuntime({ sessionId: 'error-renderers', adapter });
  if (!runtime) return null;
  return (
    <div style={{ position: 'fixed', inset: 0, display: 'flex', flexDirection: 'column', background: '#fff' }}>
      <div
        style={{
          display: 'flex',
          flexWrap: 'wrap',
          gap: 8,
          alignItems: 'center',
          padding: 12,
          borderBottom: '1px solid #e8e8ec',
        }}
      >
        <strong>
          Mock <code>run-failed</code> → <code>MODEL_OVERLOADED</code> → 精确键命中
        </strong>
        <button
          type="button"
          style={buttonStyle(mode === 'sdk')}
          aria-pressed={mode === 'sdk'}
          onClick={() => setMode('sdk')}
        >
          SDK 原样默认
        </button>
        <button
          type="button"
          style={buttonStyle(mode === 'patch')}
          aria-pressed={mode === 'patch'}
          onClick={() => setMode('patch')}
        >
          覆盖默认卡片文案
        </button>
        <button
          type="button"
          style={buttonStyle(mode === 'custom')}
          aria-pressed={mode === 'custom'}
          onClick={() => setMode('custom')}
        >
          整卡自定义
        </button>
      </div>
      <div style={{ flex: 1, minHeight: 0, minWidth: 0 }}>
        <Chatkit
          runtime={runtime}
          settings={{ recommendations: ['开始一个会失败的任务'] }}
          renderers={{
            errors: {
              MODEL_OVERLOADED: ({ formattedContent, renderDefault }) =>
                mode === 'sdk' ? (
                  renderDefault()
                ) : mode === 'patch' ? (
                  renderDefault({
                    title: 'renderDefault:模型暂时繁忙',
                    content: '**正文已由宿主覆盖。**\n\n卡片样式仍由 Chatkit 绘制。',
                  })
                ) : (
                  <BusyCard text={formattedContent} />
                ),
              '*': ({ defaultNode, code }) => (
                <>
                  {defaultNode}
                  <small style={{ color: '#666' }}>错误码 {code},可以反馈给管理员</small>
                </>
              ),
            },
          }}
        />
      </div>
    </div>
  );
}
Mock 错误码命中精确键:SDK 原样默认 / 覆盖文案 / 整卡自定义新窗口打开

默认模式返回 renderDefault();第二种模式显式覆盖标题和正文,正文的加粗文字由安全 Markdown 渲染;第三种模式由 BusyCard 绘制并显示 formattedContent。整卡按钮调用 send('请再试一次') 发送新消息,不重跑失败的原 run;Mock 场景仍会返回同样的失败。需要原输入重试时,应使用支持的重新生成能力并核对当前运行状态。

没有宿主渲染器时,UI 按原始错误码和当前语言生成默认标题与正文。首批只登记以下六个精确码(大小写敏感):

错误码zh-CN 标题 / 正文en-US 标题 / 正文
UNSUPPORTED_ATTACHMENT附件格式不受支持 / 当前对话不支持此附件类型。Unsupported attachment / This conversation does not support this attachment type.
MAX_STEPS_EXCEEDED、max_iterations_reached执行次数已达上限 / 本轮对话已达到配置的执行次数上限。Execution limit reached / This conversation has reached its configured execution limit.
MODEL_OVERLOADED模型服务繁忙 / 模型服务当前负载较高。Model service busy / The model service is under heavy load.
rate_limit_exceeded、rate_limited请求受限 / 当前请求受到调用限制。Request limited / This request is subject to a usage limit.

命中内置码时,非空且与内置正文按 trim() 比较不同的原始说明会原样附在正文后,以空行和加粗的「原始说明 / Original details」标签分隔。相同内容只显示一次,空说明不加标签。未知码仍使用当前语言的通用标题和原始正文。语言切换会重算同一张卡片的 SDK 文案,不改写运行错误或历史消息。

需要原样默认卡片时,回调返回 renderDefault();它与直接返回 defaultNode、不登记 renderers.errors、或回调返回 undefined 的显示结果相同。只改文字时,返回 renderDefault({ title, content })。缺省字段或显式 undefined 保留内置值,空字符串清空该字段。默认卡片继续使用安全 Markdown 渲染正文;标题按普通文本显示。默认错误卡片为 p、a、ul、ol、code 使用自己的安全组件,这些同名组件会覆盖 renderers.markdown 的全局配置;需要自己的链接策略或整卡画法时,用 renderers.errors 返回自定义节点。

下例只是已有 runtime 的组件中 <Chatkit> 的配置片段,runtime 需先由 useChatRuntime 等入口取得,不能单独复制运行。这里的 quota 正文匹配是宿主自己的业务规则,不是 SDK 内置配额码或升级策略。套餐、额度、恢复时间和升级链接由宿主自行提供。

<Chatkit
  runtime={runtime}
  renderers={{
    errors: {
      '*': ({ formattedContent, locale, renderDefault }) => {
        // 实际后端错误码取决于适配器;这里按可见说明匹配。
        if (!formattedContent.includes('quota')) return undefined;
        return renderDefault({
          title: locale === 'zh-CN' ? '额度已用完' : 'Quota exhausted',
          content: formattedContent + '\n\n[查看额度](https://example.com/usage)',
        });
      },
    },
  }}
/>

error 是当前渲染态的 status.error,不保证是网络原始载荷。code 优先取 run.error.code,其次取对象型 status.error.code,否则为 unknown。formattedContent 始终是查内置表前提取的原文,供宿主已有正文规则使用;defaultText、defaultTitle 和 defaultNode 则是新的默认展示。调用 renderDefault 本身不会更改这些字段;只有把返回的节点作为渲染器返回值,才会显示覆盖后的文字。宿主显式覆盖 content 时,SDK 不再追加原文。渲染器可能因重渲染重复执行,应保持同步且无副作用。

非法补丁、回调同步抛错或直接返回 Promise 时回退到默认错误卡片;Promise rejection 会被消费。有 onError 时在卡片提交后上报,并对同一消息、原始错误和异常跨宿主重渲染及虚拟列表重挂去重;原始错误变化、会话切换,或一次成功渲染后再次失败则可以重新上报。没有 onError 时仍回退,但这条同步失败路径不会额外写控制台警告。若回调已返回 <CustomCard />,而这个组件随后在 React render 中抛错,则由消息级错误边界显示消息快照,不会回到默认错误卡片。

按错误码登记,* 是兜底。下例同样只是已有 runtime 的组件内的配置片段;若要整卡自定义,精确键改为返回 <BusyCard text={formattedContent} />,如上方完整示例:

<Chatkit
  runtime={runtime}
  renderers={{
    errors: {
      MODEL_OVERLOADED: ({ renderDefault }) => renderDefault({ title: '模型暂时繁忙', content: '**请稍后再试。**' }),
      '*': ({ defaultNode, code }) => (
        <>
          {defaultNode}
          <small>错误码 {code},可以反馈给管理员</small>
        </>
      ),
    },
  }}
/>
参数说明
code错误码;后端没给时是 unknown
error当前渲染态的 status.error
locale当前界面语言
defaultText只读的默认展示文字;已知码含本地化文案与必要的原始说明
renderDefault用内置样式生成覆盖文字后的卡片;须返回生成的节点
formattedContent查内置表前的原始错误文字,供宿主正文规则使用
defaultTitle默认的标题
defaultNode默认的失败卡片,可以直接用或在外面包一层
message失败的那条消息
run可选的运行信息;存在时可读 run.error?.retryable,仅表示重试建议
返回值效果
节点显示这个节点
undefined用默认的卡片
null不显示失败卡片

查找顺序:先找和错误码完全相同的键;只有没有登记精确键时才用 *。若精确键的回调返回 undefined,直接显示默认卡片,不再试 *;两种键都没有时也显示默认卡片。码的匹配大小写敏感。

ChatkitProvider 上的 renderers.errors 可以被里面的 <Chatkit /> 继承;子组件一旦自己传了 errors,就整张 map 替换 Provider 的 map,不会按错误码深合并。需要保留 Provider 的规则时,在子组件的 map 中显式加入它们。

错误码来自适配器或运行时,既可能保留后端错误码,也可能是本地执行错误码。retryable 不会自动重试;自定义「重试」按钮仍需要后端提供相应能力,且不能对仍在远端运行的任务直接重复执行。

回答正文前的等待提示

默认在助手回答首个可见正文文字出现前显示中文“正在生成回答…”或英文“Generating an answer…”,语言切换时默认文字随之切换。用局部值型配置改文案或隐藏,不必接管整条消息:

以下两段分别说明直接 Chatkit 与 Provider 的配置位置,假定组件已有由 useChatRuntime 取得的 runtime;它们不是两个需要同时渲染的应用入口。

<Chatkit runtime={runtime} settings={{ answerPending: { text: '正在准备回复…' } }} />

<ChatkitProvider runtime={runtime} settings={{ answerPending: { visible: false } }}>
  <Chatkit />
</ChatkitProvider>

settings.answerPending.text 不传时沿用当前语言的默认文字;传入字符串后原样使用,不自动翻译。visible: false 只隐藏这条等待提示,不改变运行状态、停止或重试行为。纯 data 业务卡片和自定义 part 不算普通正文首字,因此默认提示可能与卡片同时显示;此类宿主可关闭提示或在自己的卡片里提供反馈。子 Chatkit 自己设置 answerPending 时按该对象整体覆盖 Provider 的同名设置。

系统提示:renderers.events

conversation-events.tsx
import { Chatkit, useChatRuntime } from '@volcengine/chatkit';
import { createMockAdapter } from '@volcengine/chatkit/adapters/mock';
import '@volcengine/chatkit/styles.css';

const adapter = createMockAdapter({ getScenarioId: () => 'context-compaction' });

// 系统提示(上下文已压缩):没有头像、没有操作栏的提示条,插在用户消息与回答之间。
// 内置的 chatkit.notice 不用登记;自己的类型登记在 renderers.events 里。
export default function ConversationEvents() {
  const runtime = useChatRuntime({ sessionId: 'conversation-events', adapter });
  if (!runtime) return null;
  return <Chatkit runtime={runtime} settings={{ recommendations: ['继续我们刚才的讨论'] }} />;
}
上下文压缩:插在用户消息与回答之间的提示条新窗口打开

对话里不属于任何一方发言的内容:上下文已压缩、日期分隔线、模型已切换……它们没有头像、没有操作栏。

内置两种:

类型显示数据
chatkit.notice提示条:标题、说明、可折叠的详情{ title, description?, details?, tone? }
chatkit.date-divider日期分隔线{ label?, timestamp? }

登记自己的类型,或换掉内置的。下方是已有 runtime 的组件内的配置片段;data 的结构由宿主定义:

<Chatkit
  runtime={runtime}
  renderers={{
    events: {
      'acme.agent-handoff': ({ data }) => <pre>{JSON.stringify(data, null, 2)}</pre>,
      'chatkit.date-divider': null, // 不显示日期分隔线
    },
  }}
/>
参数说明
kind类型,就是登记时用的名字
data后端给的数据
message所在的消息
emitAction(actionId: string) => void,报告系统提示上的动作,由 onEvent 收到 conversation_event_action;不自动执行后端操作

后端怎么产生

适配器发一条角色为 system 的消息,里面放一块带类型的数据:

await ingress.emitMessage({
  eventId,
  sessionId,
  runId,
  message: {
    id: `compaction:${runId}`,
    role: 'system',
    parts: [
      {
        id: 'event',
        type: 'data',
        kind: 'chatkit.notice',
        data: { title: '上下文已压缩', description: '较早的对话已被摘要' },
      },
    ],
  },
});

配置项

配置项类型默认说明
settings.answerPending{ visible?: boolean; text?: string; }—回答正文首字出现前的提示;只控制这条提示,不改变运行状态或业务卡片。
settings.answerPending.visiblebooleantrue是否显示提示;纯业务卡片可关闭,由宿主自行提供等待反馈。
settings.answerPending.textstring当前语言的内置文案提示文字;不传时随 `locale` 使用内置中英文。
renderers.errorsRecord<string, ChatkitErrorRenderer>—失败卡片:按错误码登记,`*` 兜底。
renderers.eventsRecord<string, ConversationEventRenderer>—系统提示:按类型登记;登记成 null 表示不显示。

怎么区分系统提示与业务卡片

内容用什么
属于某一条回答(订单卡片、引用来源)业务卡片
不属于任何一方的发言(已压缩、已切换)系统提示

判断标准:删掉它,前后两条消息的意思变不变。不变的是系统提示。

说明

  • 类型名形如 产品名.类型,chatkit. 开头的保留给内置。
  • 系统提示不进左侧的轮次导航,不能复制和赞踩。
  • 没有登记的类型不显示。
  • system 消息里的非空文本会按 chatkit.notice 显示。内置提示类型不会自动生成日期分隔线或压缩记录,需要适配器实际提供消息。