发送前处理 onSend

通过聊天界面或公开操作 hook 发送之前,onSend 可以改写输入,或者拦下这次发送。它是界面侧的发送回调,不是所有后端请求的统一拦截器。

on-send.tsx
import '@volcengine/chatkit/styles.css';

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

const adapter = createMockAdapter({});

const BLOCKED_WORD = '密码';

// onSend 在每次发送之前调用:返回新的输入表示改写,返回 false 表示拦下(草稿保留)。
// 右侧显示改写之后、交给适配器的输入。
export default function OnSend() {
  const runtime = useChatRuntime({ sessionId: 'on-send', adapter });
  const [sent, setSent] = useState<ChatkitSendInput>();
  const [blocked, setBlocked] = useState(false);
  if (!runtime) return null;
  return (
    <div className="demo-row">
      <div className="demo-main">
        <Chatkit
          runtime={runtime}
          settings={{
            welcome: '发送前先处理',
            placeholder: `带「${BLOCKED_WORD}」两个字的消息会被拦下`,
            recommendations: ['帮我查一下 chatkit 怎么接入', '我的密码是多少'],
          }}
          onSend={input => {
            const text = input.content.map(part => (part.type === 'text' ? part.text : part.type === 'reference' ? part.label : '')).join('');
            setBlocked(text.includes(BLOCKED_WORD));
            if (text.includes(BLOCKED_WORD)) return false;
            const next = { ...input, metadata: { ...input.metadata, knowledgeBaseId: 'kb-handbook', page: '/docs' } };
            setSent(next);
            return next;
          }}
        />
      </div>
      <aside className="demo-side">
        <h4>后端收到</h4>
        {blocked ? <p>这次发送被拦下了,没有到后端。</p> : null}
        {sent ? <pre>{JSON.stringify(sent, null, 2)}</pre> : <p className="demo-muted">发一句话看看</p>}
      </aside>
    </div>
  );
}
发送时带上知识库与页面信息;带「密码」两个字的消息被拦下新窗口打开

用法

<Chatkit
  sessions={sessions}
  onSend={input => {
    if (containsSensitiveWords(input)) {
      toast('内容包含敏感词');
      return false; // 拦下:不发送,草稿保留
    }
    return { ...input, metadata: { ...input.metadata, knowledgeBaseId } }; // 改写
  }}
/>

返回值

返回值效果
完整的新输入用它整体替换本次输入,不会自动与旧输入合并;保留其他字段时使用 { ...input, … }
false拦下这次发送,输入框里的草稿保留
不返回原样发送
Promise等它完成后按上面三种处理

拿到的输入

字段说明
content文字、结构化引用和附件出现位置,按原顺序。附件位置是 { type: 'attachment', attachmentId, placement }
attachments完整资源表:{ id, name, mimeType, size, source, metadata? };source 是 URL、base64 内联数据或稳定资源引用
metadata附加信息,由适配器决定是否以及如何带给后端

常见用法

带上业务信息。 知识库、连接器、当前页面的上下文放进 metadata:

onSend={input => ({ ...input, metadata: { ...input.metadata, knowledgeBaseIds, pageUrl: location.href } })}

拦截指令。 /clear 这类只在前端处理的指令不发给后端:

下面匹配的是手动输入的纯文本 /clear。从 / 候选项插入的标签可能编码成文本标记或结构化引用,应按候选项约定读取 content,或在候选项的 onSelect 里直接处理。

onSend={input => {
  const text = input.content.map(part => (part.type === 'text' ? part.text : '')).join('').trim();
  if (text === '/clear') {
    clearLocalState();
    return false;
  }
}}

拦下之后草稿保留;要清空输入框时用 useComposer().setText('')。

发送前确认。 返回 Promise:

onSend={async input => ((await confirmCost(input)) ? input : false)}

metadata 怎么到后端

metadata 怎么进请求由适配器决定,各内置适配器的默认做法见各协议页面的「输入怎么放进请求」,入口在接入后端。后端要的字段位置不一样时,用适配器上的选项调整(各内置适配器都叫 transformInput)。

配置项

配置项类型默认说明
onSendChatkitSendHandler—发送前处理:可以改写输入,返回 `false` 拦下这次发送(草稿保留),不返回表示原样发送。

说明

  • 输入框、推荐问题和同一配置上下文中的 useChatkitActions().send 会经过 onSend;直接调用 runtime.actions.send、Core 或适配器不会。重新生成与编辑后重发也不经过。
  • onSend 抛错会中止这次发送。输入框提交会保留草稿并交给 onError;程序调用需要处理返回 Promise 的拒绝,不能假定所有调用入口都自动报告错误。异步确认还需由业务方处理取消、重复点击和会话切换。
  • 当前推荐问题入口没有捕获发送 Promise 的拒绝。共用异步校验时,应在 onSend 中捕获预期异常、显示提示并返回 false,避免产生未处理的拒绝。
  • onSend 不提供当前会话 id 参数;需要按会话补业务信息时,应从所在页面或 Provider 的会话状态读取。
  • onSend 只处理输入,不能改变发送之后的流程(排队、停止、结论判定)。
  • 输入变成后端请求的那一步在适配器里,不在 onSend 里。