Skip to content

第13篇:状态管理 · 第2节 AppState — 全局应用状态组织

本节将「会话 / 工具 / UI / 配置」四类关注点收拢到一棵可类型化的状态树,并说明与 createStore 的衔接方式。


学习目标

能力项说明
建模为 CLI/Agent 应用划分 sessiontoolsuiconfig 四大域及边界
类型用 TypeScript 表达嵌套 AppState,并定义各 slice 的 action 命名空间
选择器编写 memo 友好的 selector,避免无关重渲染或重复计算
安全识别哪些字段属于敏感数据(令牌、路径),在日志与快照中脱敏
演进预留 versionmeta 字段,为迁移节铺路

生活类比:移动指挥所里的四张桌子

一次野外救援,指挥所有四张桌子:人事签到本(谁在场上)、工具清单(铲子无人机是否可用)、大屏 UI(当前展示哪块地图)、操作手册与规章(配置)。四张桌子上的信息彼此有关但不能混成一本乱账:人事变动可能影响 UI 上显示的队长名,但规章本子不该被临时涂改——对应配置与运行时态分离AppState 就是把四张桌子的台账结构印在一张总表上,值班长(reducer)按规矩更新,任何人抬头都能看到一致的全局图景


AppState 形状(教学示意)

typescript
// state/appState.ts — 教学示意
export interface SessionSlice {
  sessionId: string;
  cwd: string;
  startedAt: number;
  lastUserMessageAt?: number;
  /** 续接会话时由 History 注入 */
  resumedFromCheckpoint?: string;
}

export interface ToolInvocation {
  id: string;
  name: string;
  status: "pending" | "running" | "success" | "error";
  startedAt: number;
  endedAt?: number;
  /** 仅调试;生产日志应脱敏 */
  argsDigest?: string;
}

export interface ToolsSlice {
  registryVersion: string;
  active: ToolInvocation[];
  lastError?: { code: string; message: string };
}

export interface UiSlice {
  theme: "dark" | "light" | "system";
  layout: "compact" | "comfortable";
  modalStack: string[];
  /** TUI:当前焦点面板 id */
  focusPaneId?: string;
}

export interface ConfigSlice {
  /** 与磁盘 profile 对齐的版本号 */
  schemaVersion: number;
  model: string;
  approvalPolicy: "off" | "ask" | "strict";
  experimental: Record<string, boolean>;
}

export interface AppState {
  session: SessionSlice;
  tools: ToolsSlice;
  ui: UiSlice;
  config: ConfigSlice;
}

根 reducer 组合

typescript
import { combineReducers, Action, Reducer } from "../store/createStore";

const appReducer: Reducer<AppState> = combineReducers({
  session: sessionReducer,
  tools: toolsReducer,
  ui: uiReducer,
  config: configReducer,
});

export function createAppStore(preloaded?: Partial<AppState>) {
  const initial: AppState = {
    session: defaultSession,
    tools: defaultTools,
    ui: defaultUi,
    config: defaultConfig,
    ...preloaded,
  };
  return createStore(appReducer, initial);
}

Action 命名空间约定

前缀含义示例 type
session/会话生命周期session/SET_CWD
tools/工具注册与调用tools/INVOCATION_END
ui/界面与交互ui/PUSH_MODAL
config/持久化相关配置config/PATCH

统一前缀便于日志过滤与遥测聚合(见第14篇)。


Mermaid:四域依赖(只读视角)

图2:dispatch 在各 slice 间的广播


选择器与派生状态

typescript
export const selectActiveTool = (s: AppState) =>
  s.tools.active.find((t) => t.status === "running");

export const selectIsModalOpen = (name: string) => (s: AppState) =>
  s.ui.modalStack.includes(name);

/** 派生:是否处于「需要用户注意」状态 */
export const selectNeedsAttention = (s: AppState) =>
  !!s.tools.lastError || s.ui.modalStack.length > 0;
实践说明
派生放 selector避免在 reducer 存重复字段导致双写
昂贵计算 memo大列表 + 过滤时引入 LRU 或显式缓存键
禁止 selector 副作用与 reducer 同样保持纯函数

敏感字段与脱敏策略

字段示例风险处理
sessionId关联用户轨迹日志只打 hash 前缀
tools.active[].argsDigest泄露路径/密钥默认关闭;开启时加盐 hash
config 内 API 相关合规从不进入匿名遥测 payload

与 createStore / 副作用 / 持久化的衔接

组件关系
createStore承载整棵 AppState
副作用同步subscribe 监听 configsession 变更触发 I/O
Memdir用户偏好可能映射到 config.experimental 或独立 mem 文件
Historysession.resumedFromCheckpoint 与检查点元数据
Migrationsconfig.schemaVersion 驱动升级脚本
持久化仅部分 slice 写入 ~/.claude/(见第7节)

表:slice 职责边界

Slice典型 action 来源是否默认持久化
session进程启动、用户输入部分元数据
toolsMCP/内置工具回调通常不
ui键盘/配置切换部分
config设置页、环境变量合并

小结

AppState运行时真相结构化:会话提供上下文,工具承载 Agent 能力状态,UI 承载人机交互壳层,配置承载策略与模型选择。四域通过 action 广播 + selector 派生 协作,而敏感数据与持久化边界需在类型与文档层显式标出。


自测

  1. 为何 ui 不应直接写 session.sessionId?应通过哪种 action?
  2. tools.lastError 清除动作应放在哪个 reducer?是否可能跨 slice?
  3. 若新增 permissions slice,对 combineReducers 与迁移脚本各有什么影响?

上一节index.md · 下一节03-side-effects.md

本项目仅用于教育学习目的。Claude Code 源码版权归 Anthropic, PBC 所有。