失败卡片与系统提示
适用范围:本页示例随 chatkit 仓库 main 源码维护,源码可能领先于已发布包。使用 npm 安装包时,请核对所安装版本的类型声明是否提供 renderers.errors 的 renderDefault/defaultText 和 settings.answerPending,并查阅版本与兼容承诺及对应版本的更新说明。
一轮失败时通过 renderers.errors 按错误码选择渲染器。渲染器可用内置样式覆盖标题和正文,也可换掉整张卡片;对话里的系统提示可按类型自定义。
失败卡片:renderers.errors
先在下面的在线示例中发送“开始一个会失败的任务”,再切换三种模式。Mock 的 run-failed 失败步骤给出 code: 'MODEL_OVERLOADED' 和原始说明,所以命中精确键 renderers.errors.MODEL_OVERLOADED;已登记精确键时不会走 *。
运行下面的完整示例:左侧是可作为 Vite React 18 / 19 项目 src/App.tsx 的组件文件,已引入 Chatkit 样式并自带页面布局。按以下顺序准备;本地 tarball 使用仓库的版本字段,不能仅凭该字段认定公网同名版本包含相同接口。
- 用 Vite 的
react-ts 模板创建项目,保留生成的 index.html 和 src/main.tsx;将项目的 React、React DOM 及其类型包固定到同一个主版本(18.x 或 19.x)。
- 在 chatkit 仓库检出 main,用同一份源码构建 SDK 和取得示例:运行
pnpm install --frozen-lockfile、pnpm run build,再从 packages/chatkit 运行 pnpm pack --pack-destination <Vite 项目绝对路径>。在 Vite 项目中安装生成的本地 .tgz;若改用公网包,先按安装版本核对类型声明和版本说明。
- 将同一份源码中的
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 按原始错误码和当前语言生成默认标题与正文。首批只登记以下六个精确码(大小写敏感):
命中内置码时,非空且与内置正文按 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>
</>
),
},
}}
/>
查找顺序:先找和错误码完全相同的键;只有没有登记精确键时才用 *。若精确键的回调返回 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: ['继续我们刚才的讨论'] }} />;
}
上下文压缩:插在用户消息与回答之间的提示条新窗口打开
对话里不属于任何一方发言的内容:上下文已压缩、日期分隔线、模型已切换……它们没有头像、没有操作栏。
内置两种:
登记自己的类型,或换掉内置的。下方是已有 runtime 的组件内的配置片段;data 的结构由宿主定义:
<Chatkit
runtime={runtime}
renderers={{
events: {
'acme.agent-handoff': ({ data }) => <pre>{JSON.stringify(data, null, 2)}</pre>,
'chatkit.date-divider': null, // 不显示日期分隔线
},
}}
/>
后端怎么产生
适配器发一条角色为 system 的消息,里面放一块带类型的数据:
await ingress.emitMessage({
eventId,
sessionId,
runId,
message: {
id: `compaction:${runId}`,
role: 'system',
parts: [
{
id: 'event',
type: 'data',
kind: 'chatkit.notice',
data: { title: '上下文已压缩', description: '较早的对话已被摘要' },
},
],
},
});
配置项
配置项类型默认说明
renderers.eventsRecord<string, ConversationEventRenderer>—系统提示:按类型登记;登记成 null 表示不显示。
怎么区分系统提示与业务卡片
判断标准:删掉它,前后两条消息的意思变不变。不变的是系统提示。
说明
- 类型名形如
产品名.类型,chatkit. 开头的保留给内置。
- 系统提示不进左侧的轮次导航,不能复制和赞踩。
- 没有登记的类型不显示。
system 消息里的非空文本会按 chatkit.notice 显示。内置提示类型不会自动生成日期分隔线或压缩记录,需要适配器实际提供消息。