会话与历史从哪来

会话目录(有哪些会话、标题、排序)由应用自己的后端提供,应用渲染 SessionList 展示;适配器不提供会话目录能力。某个会话里的消息由适配器的 history 能力从后端读取。本页是两者的完整说明;界面效果见会话列表。

会话目录和路由由应用控制

SessionInfo 是会话摘要:id 必填,title、createdAt、updatedAt、status、metadata 可选。时间统一为毫秒。业务字段可放 metadata;SDK 不解释置顶、归档、权限等业务含义。

SessionList 接收 readonly ChatkitSessionView[],它与 SessionInfo 的摘要字段相容。status 为 idle / running / completed / error / unknown。组件不会修改数组或发后端请求。

SessionList prop类型与行为
sessions必填,外部会话摘要数组
activeSessionId可选,当前高亮 ID,由外部控制
onSelect必填,(id) => void 或 Promise<void>;一般更新路由
loading / hasMore可选,列表加载与分页状态,缺省 false
onCreate / onLoadMore可选,() => void 或 Promise<void>;未提供则隐藏入口
onRename可选,(id, title) => void/boolean 或相应 Promise
onRemove可选,(id) => void/boolean 或相应 Promise;false 表示拒绝
itemActions / itemBadge自定义业务操作与角标,例如 fork、收藏、归档
header / footer列表头尾内容
onError操作或渲染异常的报告回调;放在 ChatkitProvider 里没写时用 Provider 的
locale / className / style语言与布局
const [items, setItems] = useState<SessionInfo[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string>();
const chat = useChatSessions({ adapter, activeSessionId });
const opened = useSyncExternalStore(chat.store.subscribe, chat.store.getSnapshot, chat.store.getSnapshot);
const visibleItems = items.map(item => ({
  ...item,
  status: opened.openedSessions.find(open => open.id === item.id)?.status ?? item.status,
}));

<SessionList sessions={visibleItems} activeSessionId={activeSessionId} onSelect={setActiveSessionId} />;
<Chatkit sessions={chat} />;

实际应用可以直接从 URL 读取 activeSessionId,onSelect 执行导航。浏览器前进后退、深链接和 fork 后跳转都走同一条路。SDK 不自动选择列表首项,不创建虚拟草稿 ID;没有 activeSessionId 时 runtime 为空。

列表筛选掉一个会话不意味着它已被删除,也不会释放其 runtime。SDK 不回写标题或目录数组;应用根据业务接口或运行事件刷新元数据。后台运行和未查看结果从 chat.store 的 openedSessions 合并进列表。

running 表示回答尚未完全返回;completed / error 表示后台结束且用户尚未查看。选中后两种状态会确认结果并变成 idle。SessionList 不为 idle 显示状态图标,但会继续显示目录项的 updatedAt / createdAt 相对时间。该确认状态默认只在当前页面内存中保存,跨刷新或跨设备同步由应用负责。未打开的会话不在 openedSessions 中,应使用目录接口返回的状态;无法判断时可传 unknown。

和单个对话 useChatRuntime 一起用

useChatRuntime 只管一个对话。SessionList 也能用来切换它的 sessionId,但每次切换都会释放旧运行时并重新取历史,旧会话不在后台继续接收。需要多会话切换时用 useChatSessions。

useChatSessions 的资源生命周期

参数:adapter 必填且引用稳定;activeSessionId 可选并持续受控;onError、eventAttributes、stallThresholdMs 与单会话 hook 相同。

返回项契约
runtime当前选中会话的 ChatkitRuntime,初始化前、未选择或已释放时为空
actions.open(id)Promise<ChatkitRuntime>;打开或复用并等待历史初始化,不改变外部选择;已释放则拒绝。历史/连接错误经 onError 报告,不保证已获取完整历史
actions.release(id)boolean;释放指定本地 runtime 和接收资源;不存在返回 false,不删除后端会话,不请求远端取消
store.getSnapshot(){ activeSessionId, openedSessions: readonly { id, status }[] };不包含目录
store.subscribe(listener)订阅资源与运行状态变化,返回退订函数
events汇总全部已打开会话的事件,各自保留 sessionId
capabilities当前 adapter 的执行能力

切换保留旧 runtime,后台继续执行;直到显式 release、adapter 替换或 hook 卸载才释放。已释放的当前 ID 不会因普通重渲染自动重新打开,可显式 open 或切换 ID。应用负责选择下一个会话以及控制缓存数量;SDK 不内置 LRU 或空闲超时。

新建:应用调用后端 create,更新目录并导航到新 ID。首发前先完成创建;如果先发再导航,用 await chat.actions.open(id) 获取新 runtime,再调用其 actions.send,避免误发到旧会话。

删除:SessionList 先显示确认弹层,再调用应用的 onRemove。应用调用后端 delete,成功后更新目录、chat.actions.release(id),再决定下一条路由;返回 false 表示拒绝删除。若需要远端停止,应由后端删除语义或显式取消操作保证。

fork:应用调用后端 fork({ sessionId, messageId }),后端复制上下文并返回新 ID;应用更新目录并导航。SDK 通过 history 读取新会话。复制前端消息不等于后端 fork。单个 session 内的消息树分支切换不在本契约范围内。

历史、恢复与错误

HistoryResult 必含 messages 和 hasMore,可含 cursor、session、runSnapshots、activeRun。session 是摘要,不是业务目录;runSnapshots 只恢复明确提供的终态。activeRun 对象包含 runId/eventId,null 表示确认无执行,省略表示未知。会话 status:running 不能代替可靠 runId 和恢复位置。

history 取快照;subscribe 接实时;resume 补收已知轮次的缺失输出。三者不可互相替代。有订阅时先接收并缓冲,再加载历史;从匹配快照的 eventId 恢复,重放顺序和完整性由后端保证。没有游标不能承诺精确续接,不重新发送旧问题。

输入转换抛错使当前命令失败;输出转换抛错保留默认内容。协议/网络错误通过现有 onError 与运行事件报告;本地 IO 异常不伪造远端失败。取消一个会话的资源不影响其他会话。

对话界面被销毁重建时对话会不会丢

运行时跟着 useChatSessions / useChatRuntime 所在的组件:组件卸载时运行时释放,本地已收到的内容随之丢弃。chatkit 不在浏览器里持久化对话。

  • 适配器给了 history:再次打开同一 sessionId 时重新从后端读取已保存的内容;历史里有进行中的轮次、适配器又给了 resume 时尝试续接。
  • 没给 history:对话会清空。要保留,就把 hook 写在不会卸载的上层组件里(例如路由布局),或者隐藏而不是移除对话界面。
// 写在路由布局里,切换子页面不会卸载
function ChatLayout() {
  const chat = useChatSessions({ adapter, activeSessionId });
  return (
    <>
      <Outlet />
      <div hidden={!chatVisible}>
        <Chatkit sessions={chat} />
      </div>
    </>
  );
}