跳到主要内容

第 4 章 · store 层:AssistantClient、useAui、useAuiState

本章讲什么: 下面有了框架无关的运行时(资源),上面的 React 组件怎么读到它、怎么对它发指令?这是 @assistant-ui/store 的活:它把 tap 资源桥接进 React,给你两类入口——发动作(useAui)和读状态(useAuiState)。

4.1 一句话定位

仓库的架构图说得直接(AGENTS.md:5):

@assistant-ui/store —— 把 tap 桥接到 React(useAuiuseAuiStateAuiProvider)。」

所以 store 不含业务逻辑,它是胶水:让第 1 章的 resource 能被 React 组件订阅、让组件能调到运行时的方法。

4.2 AssistantClient:带作用域的动作入口

你拿到的 aui 是一个 AssistantClient。它按**作用域(scope)**组织:aui.thread()aui.composer()aui.message()…… 每个作用域是一个返回「该作用域方法集」的访问器。

useAui 的文档注释(packages/store/src/useAui.tsuseAui 的 JSDoc)给了典型用法:

// 来自 useAui 的 JSDoc 示例
const aui = useAui();
const onSend = () => aui.composer().send();
const onCancel = () => aui.thread().cancelRun();

几个要点:

  • 在 provider 外用会报错。 没有 AuiProvider/AssistantRuntimeProvider 时,作用域访问器一被调用就抛描述性错误(useAui.ts 的 JSDoc 与实现末尾的 parent === null 检查)。
  • 作用域可扩展。 useAui(clients) 的高级重载允许往 client 上挂新作用域(比如自定义 provider 注册一个 message/part 作用域),供其后代读取。这就是 primitives 内部用来「把当前这条消息/这个片段」注入上下文的机制。
  • 作用域注册是类型安全的。 scope 的方法/事件 schema 通过模块增强(packages/store/src/types/client.tsScopeRegistry 接口,client.ts:43-58 的示例)声明,所以 aui.thread() 上有哪些方法是有类型的。

4.3 client 怎么和 tap 缝起来

useAui.ts 里能看到桥接的真身:useRootClientAccessorResource(useAui.ts:65-105)用 useTapRoot 把一个 client 资源跑起来,然后用 store.subscribe(...) 把它的更新接到一个 NotificationManager(useAui.ts:78-86)。

再往上,useAssistantClient(useAui.ts:296-352)用 Object.create(proto) 把各个作用域字段拼到一个 client 对象上,并装上 subscribe / on / 一个被代理的状态对象(useAui.ts:329-345)。

直觉:每个作用域是一个 tap 资源,client 只是这些资源的「门面」。订阅 client 就等于订阅底下那些资源的变化——这正是把第 1 章的「脱离 React 的响应式资源」接回 React 世界的接缝。事件还能冒泡到父 client(useAui.tson(...) 实现里既订本地、又订 clientRef.parent.on,useAui.ts:160-205),所以嵌套的 assistant 上下文能逐层传播事件。

4.4 useAuiState:用 selector 订阅「状态切片」

读状态不要整坨拿,要选一小块useAuiState(packages/store/src/useAuiState.ts)就是干这个的:

// 真实源码:useAuiState.ts(useAuiState 实现)
export const useAuiState = <T>(selector: (state: AssistantState) => T): T => {
const aui = useAui();
const proxiedState = getProxiedAssistantState(aui);
const slice = useSyncExternalStore(
aui.subscribe,
() => selector(proxiedState),
() => selector(proxiedState),
);
// ...
return slice;
};

机制(useAuiState.ts 的 JSDoc 讲得很细):

  • 基于 React 的 useSyncExternalStore,订阅源就是 aui.subscribe
  • selector 每次更新都被调用,返回值用 Object.is 比较;只有选中的那块变了才重渲染。
  • 不准返回整个 state 对象——会在运行时抛错(useAuiState.ts 实现里 slice === proxiedState 的检查)。要选具体字段,或多调用几次 useAuiState
  • 别返回新建的对象/数组字面量(包括把 s.thread 展开成新对象),否则每次更新都触发重渲染。

典型用法(来自 JSDoc):

// 运行中禁用发送按钮
const isRunning = useAuiState((s) => s.thread.isRunning);
// 多个 selector 优于一个返回对象字面量的 selector
const text = useAuiState((s) => s.composer.text);
const canSend = useAuiState((s) => s.composer.canSend);

这个「细粒度 selector 订阅」是性能关键:一条流式消息每秒变几十次,但只有真正读了变化字段的组件才重渲染,其余原地不动。

4.5 把这一切装进 React 树:AssistantRuntimeProvider

用户只需包一层 AssistantRuntimeProvider(packages/core/src/react/AssistantRuntimeProvider.tsx)。它的 JSDoc 说明:内部自动装好 AuiProvider,所以后代不用额外设置就能用 useAui/useAuiState/primitives(AssistantRuntimeProvider.tsx 顶部 JSDoc)。它接收任意运行时 hook 的产物(useLocalRuntimeuseExternalStoreRuntime…),可选 aui 父 client 用于嵌套。

4.6 代码地图

主题文件符号
取 client / 扩展作用域packages/store/src/useAui.tsuseAuiuseAssistantClientuseRootClientAccessorResource
selector 订阅状态packages/store/src/useAuiState.tsuseAuiState
作用域类型注册packages/store/src/types/client.tsScopeRegistryClientSchemaAssistantState
装载到 React 树packages/core/src/react/AssistantRuntimeProvider.tsxAssistantRuntimeProvider
事件类型packages/store/src/types/events.tsAssistantEventNameAssistantEventSelector