---
description: >-
  React + Vite 前端 (`app/src/`) —— 架构、状态、服务、
  提供商、路由、组件、hook。
icon: browsers
---

# 前端 (app/src/)

OpenHuman 桌面 UI：`app/src/` 下的 Vite + React 19 树（Yarn workspace `openhuman-app`）。它使用 Redux Toolkit 配合持久化来管理会话状态，通过 REST + Socket.io 与后端通信，并通过 JSON-RPC 调用 Rust core sidecar（`coreRpcClient` / Tauri `core_rpc_relay`）。重逻辑在核心中，不在此处。

这是一份整合的参考。使用上方目录（或你的阅读器大纲）在章节间跳转。

## 快速参考

| 章节 | 涵盖内容 |
| ------------------------------------------------- | --------------------------------------------- |
| [架构](frontend.zh-CN.md#architecture-overview) | Provider 链、构建、布局、规范 |
| [状态管理](frontend.zh-CN.md#state-management) | Redux Toolkit slice、selector、持久化 |
| [服务层](frontend.zh-CN.md#services-layer) | `apiClient`、`socketService`、`coreRpcClient` |
| [Providers](frontend.zh-CN.md#providers) | `User`、`Socket`、`AI`、`Skill` providers |
| [页面与路由](frontend.zh-CN.md#pages-routing) | `HashRouter`、路由守卫、主路由 |
| [组件](frontend.zh-CN.md#components) | UI / 设置组件模式 |
| [Hook 与工具](frontend.zh-CN.md#hooks-utilities) | 共享 hook、辅助函数、配置 |

## 规模

| 指标 | 值 |
| --------------------------------------- | ------------------------------------------------------------------------ |
| `app/src/` 下的 TypeScript / TSX 文件 | \~285 (`find app/src -name '*.ts' -o -name '*.tsx' \| wc -l` 刷新) |
| 测试 runner | Vitest (`app/test/vitest.config.ts`) |

## 目录布局

```text
app/src/
├── App.tsx                 # Provider 链 + HashRouter shell
├── AppRoutes.tsx           # 路由表 + 守卫
├── main.tsx                # 入口 (Sentry、store、样式)
├── store/                  # Redux slice 和 selector
├── providers/              # UserProvider、SocketProvider、AIProvider、SkillProvider
├── services/               # apiClient、socketService、coreRpcClient、api/*
├── lib/                    # AI loader、MCP 辅助函数、技能同步等
├── pages/                  # 路由级页面
├── components/             # 共享 UI
├── hooks/                  # 应用 hook
├── utils/                  # 配置、Tauri 辅助函数、路由工具
└── assets/                 # 图标和静态资源
```

## 架构概览

### 系统架构

OpenHuman 的桌面 UI 是一个 **React 19** 应用 (`app/src/`)，它：

* 使用 **Redux Toolkit** 配合持久化来管理与会话相关的状态
* 通过 **REST** (`apiClient`) 和 **Socket.io** (`socketService`) 连接后端
* 通过 **`coreRpcClient`** / Tauri **`core_rpc_relay`** 调用 **Rust 核心**进程（JSON-RPC 方法实现在仓库根目录 `src/openhuman/` 中，通过 `core_server` 暴露）
* 从捆绑的 `src/openhuman/agent/prompts`（仓库根目录）和打包时的 Tauri **`ai_get_config`** 加载 **AI 提示**
* 在 `lib/mcp/` 下使用 **最小 MCP 风格**辅助层（传输、验证），而非大型的仓库内 Telegram MCP 工具包

### 入口点

| 文件 | 用途 |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `app/src/main.tsx` | React 根节点、Sentry 边界、store、全局样式 |
| `app/src/App.tsx` | Provider 链：Redux → PersistGate → User → Socket → AI → Skill → Router |
| `app/src/AppRoutes.tsx` | `HashRouter` 路由、`ProtectedRoute` / `PublicRoute`、onboarding 和 mnemonic 门禁 |

### Provider 链

```text
Redux Provider
  └─ PersistGate
      └─ UserProvider
          └─ SocketProvider
              └─ AIProvider
                  └─ SkillProvider
                      └─ HashRouter
                          └─ AppRoutes (pages + settings)
```

**为什么是这个顺序**

1. Redux 在最外层，以便到处使用 `useAppSelector` / dispatch。
2. `PersistGate` 在子组件假设稳定认证前重新水合持久化的 slice。
3. `SocketProvider` 使用 auth token 进行 Socket.io。
4. `AIProvider` / `SkillProvider` 包装依赖 socket 和 store 状态的功能。
5. `HashRouter` 为所有路由提供导航。

### 模块关系（简化）

```text
App.tsx
  ├─ Redux store + persistor
  ├─ UserProvider - 用户 profile / workspace 上下文
  ├─ SocketProvider - token 存在时连接 socketService
  ├─ AIProvider - AI 会话 / 记忆客户端协调
  ├─ SkillProvider - 技能目录和同步
  └─ AppRoutes
       ├─ PublicRoute - 例如 `/` 上的 Welcome
       ├─ ProtectedRoute - onboarding、home、skills、settings、…
       └─ DefaultRedirect - 未认证用户
```

### 服务层（概念性）

```text
services/
  ├─ apiClient        → 通过运行时解析的 URL 的 REST，使用 `services/backendUrl#getBackendUrl`
  ├─ backendUrl       → 调用 `openhuman.config_resolve_api_url`；仅在 Tauri 外 fallback 到 VITE_BACKEND_URL
  ├─ socketService    → Socket.io；实时 + MCP 风格信封
  └─ coreRpcClient    → 本地 openhuman 核心的 HTTP (JSON-RPC)，配合 Tauri relay 使用
```

#### 运行时配置优先级

桌面应用不会将核心 RPC URL 或 API 主机作为硬性要求烘焙到 bundle 中。运行时应用按此顺序解析它们（最高优先）：

1. **登录屏幕 RPC URL 字段**，通过 `utils/configPersistence` 保存并在下次启动时恢复。终端用户在此配置 sidecar 地址，而非手动编辑 `config.toml` 或 `.env` 文件。
2. **Tauri `core_rpc_url` 命令**，bundled sidecar 为本进程监听的端口。
3. **`VITE_OPENHUMAN_CORE_RPC_URL`**，开发时的构建时 fallback。
4. 硬编码的 `http://127.0.0.1:7788/rpc` 默认值。

RPC 握手成功后，`services/backendUrl` 调用 `openhuman.config_resolve_api_url` 从加载的核心 `Config` 中拉取 `api_url`（和其他安全客户端字段）。`VITE_BACKEND_URL` 仅在应用运行在 Tauri 外时作为 Web fallback 使用。

需要后端 URL 的组件应调用 `useBackendUrl()`（或非 React 代码调用 `getBackendUrl()`），它们绝不能从 `utils/config` 导入静态的 `BACKEND_URL` 常量，那只代表构建时值。

### 相关文档

* Rust 架构：[架构](../architecture.zh-CN.md)
* Tauri 壳层：[Tauri Shell](tauri-shell.zh-CN.md)

## 状态管理

应用使用 Redux Toolkit 配合 Redux-Persist 进行健壮的状态管理。

### Store 配置

**文件：** `store/index.ts`

```typescript
// 合并所有 slice 并持久化
const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['auth', 'telegram'], // 持久化的 slice
};
```

### Redux 状态结构

```typescript
RootState = {
  auth: {
    token: string | null, // JWT (持久化)
    isOnboardedByUser: Record<string, boolean>, // 每用户 flag (持久化)
  },
  socket: {
    byUser: Record<
      string,
      {
        // 每用户 ID
        status: 'connecting' | 'connected' | 'disconnected';
        socketId: string | null;
      }
    >,
  },
  user: { profile: User | null, loading: boolean, error: string | null },
  telegram: {
    byUser: Record<string, TelegramState>, // 每 Telegram 用户 (持久化)
  },
};
```

### Slice

#### Auth Slice (`store/authSlice.ts`)

管理 JWT token 和每用户 onboarding 状态。

**状态：**

```typescript
interface AuthState {
  token: string | null;
  isOnboardedByUser: Record<string, boolean>;
}
```

**Actions：**

* `setToken(token: string)` - 登录后存储 JWT
* `clearToken()` - 登出时移除 token
* `setOnboarded({ userId, isOnboarded })` - 将用户标记为已 onboard

**Selectors (`store/authSelectors.ts`)：**

* `selectToken` - 获取当前 JWT
* `selectIsOnboarded(userId)` - 检查用户是否完成 onboarding

#### Socket Slice (`store/socketSlice.ts`)

跟踪每用户的 Socket.io 连接状态。

**状态：**

```typescript
interface SocketState {
  byUser: Record<
    string,
    { status: 'connecting' | 'connected' | 'disconnected'; socketId: string | null }
  >;
}
```

**Actions：**

* `setSocketStatus({ userId, status })` - 更新连接状态
* `setSocketId({ userId, socketId })` - 存储 socket ID
* `clearSocketState(userId)` - 清除用户 socket 状态

**Selectors (`store/socketSelectors.ts`)：**

* `selectSocketStatus(userId)` - 获取连接状态
* `selectIsSocketConnected(userId)` - 布尔连接检查

#### User Slice (`store/userSlice.ts`)

存储用户 profile 数据。

**状态：**

```typescript
interface UserState {
  profile: User | null;
  loading: boolean;
  error: string | null;
}
```

**Actions：**

* `setUser(user)` - 存储用户 profile
* `setUserLoading(loading)` - 设置加载状态
* `setUserError(error)` - 设置错误状态
* `clearUser()` - 登出时清除 profile

#### Telegram Slice (`store/telegram/`)

Telegram 集成的复杂嵌套状态管理。

**文件：**

* `index.ts` - Slice 导出（actions、thunks）
* `types.ts` - 实体和状态接口
* `reducers.ts` - 同步 reducers
* `extraReducers.ts` - 异步 thunk handlers
* `thunks.ts` - 异步操作

**状态结构：**

```typescript
telegram.byUser[telegramUserId] = {
  connectionStatus: "disconnected" | "connecting" | "connected" | "error",
  authStatus: "not_authenticated" | "authenticating" | "authenticated" | "error",
  currentUser: TelegramUser | null,
  sessionString: string | null,              // 存储在这里，而非 localStorage
  chats: Record<string, TelegramChat>,
  chatsOrder: string[],
  messages: Record<chatId, Record<msgId, TelegramMessage>>,
  threads: Record<chatId, TelegramThread[]>
}
```

**Reducers：**

* `setCurrentUser` - 存储已认证的 Telegram 用户
* `setSessionString` - 存储 MTProto 会话（用于持久化）
* `setConnectionStatus` - 更新连接状态
* `setAuthStatus` - 更新认证状态
* `addChat` / `updateChat` - 管理聊天列表
* `addMessage` / `updateMessage` - 管理消息历史
* `setThreads` - 存储 thread 数据

**Thunks (`store/telegram/thunks.ts`)：**

* `initializeTelegram(userId)` - 初始化 MTProto 客户端
* `connectTelegram(userId)` - 建立 Telegram 连接
* `fetchChats(userId)` - 加载聊天列表
* `fetchMessages({ userId, chatId })` - 加载消息历史
* `disconnectTelegram(userId)` - 干净断开

**Selectors (`store/telegramSelectors.ts`)：**

* `selectTelegramState(userId)` - 获取完整 Telegram 状态
* `selectTelegramConnectionStatus(userId)` - 获取连接状态
* `selectTelegramAuthStatus(userId)` - 获取 auth 状态
* `selectTelegramChats(userId)` - 获取聊天列表
* `selectTelegramMessages(userId, chatId)` - 获取聊天的消息

### Typed Hooks

**文件：** `store/hooks.ts`

```typescript
// 使用这些代替普通的 useDispatch/useSelector
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```

### 持久化配置

#### 什么被持久化

* `auth.token` - 用于认证的 JWT
* `auth.isOnboardedByUser` - 每用户 onboarding 状态
* `telegram.byUser` - Telegram 状态（会话、聊天等）

#### 什么**不**被持久化

* `socket` - 连接状态（应用启动时重连）
* `user.loading` / `user.error` - 瞬态 UI 状态
* Telegram 加载/错误状态

#### 存储后端

Redux-Persist 默认使用 localStorage adapter。这是应用中唯一可接受的 localStorage 使用。

### 使用示例

#### 读取状态

```typescript
import { useAppSelector } from '../store/hooks';

function MyComponent() {
  const token = useAppSelector(state => state.auth.token);
  const isConnected = useAppSelector(state => state.socket.byUser[userId]?.status === 'connected');
  const chats = useAppSelector(state => state.telegram.byUser[userId]?.chats);
}
```

#### Dispatch Actions

```typescript
import { clearToken, setToken } from '../store/authSlice';
import { useAppDispatch } from '../store/hooks';
import { initializeTelegram } from '../store/telegram/thunks';

function MyComponent() {
  const dispatch = useAppDispatch();

  // 同步 action
  const handleLogin = (token: string) => {
    dispatch(setToken(token));
  };

  // 异步 thunk
  const handleConnect = async () => {
    await dispatch(initializeTelegram(userId)).unwrap();
  };
}
```

#### 使用 Selectors

```typescript
import { selectIsOnboarded } from '../store/authSelectors';
import { useAppSelector } from '../store/hooks';
import { selectTelegramConnectionStatus } from '../store/telegramSelectors';

function MyComponent({ userId }) {
  const isOnboarded = useAppSelector(state => selectIsOnboarded(state, userId));
  const connectionStatus = useAppSelector(state => selectTelegramConnectionStatus(state, userId));
}
```

### 最佳实践

1. **始终使用 typed hooks** - `useAppDispatch` 和 `useAppSelector`
2. **使用 selector 处理派生状态** - 可记忆且可测试
3. **将 thunks 放在单独文件中** - 更好的组织
4. **每用户状态作用域** - 按用户 ID 键控状态
5. **避免 localStorage** - 改用 Redux-Persist

***

## 服务层

应用使用单例服务进行外部通信。这防止连接泄漏并提供一致的 API 访问。

### 服务架构

```text
app/src/services/
  ├─ apiClient (HTTP REST)
  │   ├─ 从 Redux 读取 auth.token
  ��   └─ 调用 VITE_BACKEND_URL（见 utils/config.ts）
  ├─ socketService (Socket.io)
  │   ├─ web: JS 客户端
  │   └─ Tauri: 通过 utils/tauriSocket.ts 与 Rust 端 socket 协调
  ├─ coreRpcClient.ts
  │   └─ invoke('core_rpc_relay', …) → 本地 openhuman 核心 (JSON-RPC)
  └─ services/api/* - 领域 REST 模块 (auth、user、teams、…)
```

### API Client (`services/apiClient.ts`)

用于后端通信的 HTTP REST 客户端。

#### 特性

* 基于 Fetch 的实现
* 自动从 Redux store 注入 JWT
* 类型化的请求/响应处理
* 带类型错误的错误处理

#### 用法

```typescript
import apiClient from "../services/apiClient";

// GET 请求
const user = await apiClient.get<User>("/users/me");

// POST 请求
const result = await apiClient.post<LoginResponse>("/auth/login", {
  email,
  password,
});

// 带自定义头
const data = await apiClient.get<Data>("/endpoint", {
  headers: { "X-Custom": "value" },
});
```

#### 配置

从环境读取 `VITE_BACKEND_URL` 或使用默认值：

```typescript
const BACKEND_URL =
  import.meta.env.VITE_BACKEND_URL || "https://api.example.com";
```

### API Endpoints (`services/api/`)

#### Auth API (`services/api/authApi.ts`)

认证相关端点。

```typescript
import { authApi } from "../services/api/authApi";

// 登录
const { token, user } = await authApi.login(credentials);

// Token 交换（用于深度链接流程）
const { sessionToken, user } = await authApi.exchangeToken(loginToken);

// 登出
await authApi.logout();
```

#### User API (`services/api/userApi.ts`)

用户 profile 端点。

```typescript
import { userApi } from "../services/api/userApi";

// 获取当前用户
const user = await userApi.getCurrentUser();

// 更新 profile
const updated = await userApi.updateProfile({ firstName, lastName });

// 获取设置
const settings = await userApi.getSettings();
```

### Socket Service (`services/socketService.ts`)

用于实时通信的 Socket.io 客户端单例。

#### 特性

* 单例模式 - 每应用一个连接
* Auth token 通过 socket `auth` 对象传递
* 传输：先 polling，然后 WebSocket 升级
* 自动重连处理

#### API

```typescript
import socketService from "../services/socketService";

// 用 auth token 连接
socketService.connect(token);

// 断开
socketService.disconnect();

// 发射事件
socketService.emit("event-name", data);

// 监听事件
socketService.on("event-name", (data) => {
  // 处理事件
});

// 移除监听器
socketService.off("event-name", handler);

// 一次性监听器
socketService.once("event-name", (data) => {
  // 处理一次
});

// 获取 socket 实例
const socket = socketService.getSocket();

// 检查连接状态
const isConnected = socketService.isConnected();
```

#### 连接流程

```typescript
// 在 SocketProvider.tsx 中
useEffect(() => {
  if (token) {
    socketService.connect(token);

    socketService.on("connect", () => {
      dispatch(setSocketStatus({ userId, status: "connected" }));
      dispatch(setSocketId({ userId, socketId: socket.id }));
      // 初始化 MCP 服务器
      initMCPServer(socketService.getSocket());
    });

    socketService.on("disconnect", () => {
      dispatch(setSocketStatus({ userId, status: "disconnected" }));
    });
  }

  return () => {
    socketService.disconnect();
  };
}, [token]);
```

#### 配置

```typescript
const socket = io(BACKEND_URL, {
  auth: { token },
  transports: ["polling", "websocket"],
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 1000,
});
```

#### Socket 事件契约 (Tauri)

在 Tauri 模式下，连接和事件通过 **`utils/tauriSocket.ts`** (`setupTauriSocketListeners`、`connectRustSocket` 等) 桥接。见 `providers/SocketProvider.tsx` 获取完整流程（包括 daemon 生命周期 hook）。

### Core RPC (`services/coreRpcClient.ts`)

桌面应用运行一个单独的 **`openhuman`** Rust 二进制文件（staging 在 `app/src-tauri/binaries/` 下）。UI 通过 Tauri 调用该进程上的 JSON-RPC 方法：

```typescript
import { callCoreRpc } from "../services/coreRpcClient";

const result = await callCoreRpc<MyType>({
  method: "some.openhuman.method",
  params: {
    /* … */
  },
  serviceManaged: false, // true 如果 relay 应确保 systemd/launchd 风格服务
});
```

实现：`invoke('core_rpc_relay', { request: { method, params, serviceManaged } })` → `app/src-tauri/src/commands/core_relay.rs` → `app/src-tauri/src/core_rpc.rs` 中的 HTTP 客户端。

### 服务与 provider 集成

#### SocketProvider

`app/src/providers/SocketProvider.tsx` 在 `auth.token` 存在时连接。在 **Tauri** 中，它优先使用 Rust-backed socket 路径；在 **web** 中，它使用 JS Socket.io 客户端。见源码获取日志和 `useDaemonLifecycle` 集成。

#### UserProvider、AIProvider、SkillProvider

这些包装用户 profile 加载、AI/记忆客户端协调和技能目录/同步。它们位于 `PersistGate` **内部** 和路由器旁边或外部，如 `App.tsx` 所示。

### 最佳实践

1. **使用单例** - 永远不要创建多个服务实例
2. **在 Redux 中存储会话** - 不用 localStorage
3. **卸载时清理** - 在 useEffect cleanup 中断开连接
4. **优雅处理错误** - 瞬态失败时重试
5. **通过正确通道传递 auth** - Socket auth 对象，而非 query string

***

## Providers

React context providers 管理服务生命周期并提供共享状态。

### Provider 链

providers 按特定顺序包装应用 (`app/src/App.tsx`)：

```tsx
<Sentry.ErrorBoundary>
  <Provider store={store}>
    <PersistGate persistor={persistor} onBeforeLift={...}>
      <UserProvider>
        <SocketProvider>
          <AIProvider>
            <SkillProvider>
              <Router>
                <AppRoutes />
              </Router>
            </SkillProvider>
          </AIProvider>
        </SocketProvider>
      </UserProvider>
    </PersistGate>
  </Provider>
</Sentry.ErrorBoundary>
```

(`Router` 是 `react-router-dom` 的 `HashRouter`。)

**顺序重要，因为：**

1. Redux 在最外层用于 store 访问。
2. `PersistGate` 在子组件依赖 auth 前重新水合持久化的 slice。
3. `SocketProvider` 使用 store 中的 JWT。
4. `AIProvider` / `SkillProvider` 依赖 socket 和 store-backed 功能。
5. 路由器为所有路由提供导航。

### SocketProvider (`app/src/providers/SocketProvider.tsx`)

管理实时连接：**web** 使用 JS Socket.io 客户端；**Tauri** 通过 `utils/tauriSocket.ts` 桥接到 Rust socket 并向 Redux 报告状态。

#### 职责

* `auth.token` 可用时连接；清除时断开
* Tauri 中：安装监听器一次，连接 Rust socket，协调 daemon 生命周期 (`useDaemonLifecycle`)
* 更新 Redux socket slice / 连接状态

#### 实现

见 **`app/src/providers/SocketProvider.tsx`**。文件在 **`isTauri()`** 上分叉：web 模式直接使用 `socketService`；Tauri 设置 `tauriSocket` 监听器和 `connectRustSocket` / `disconnectRustSocket`。不要将下方的伪代码视为实时实现。

#### 用法

```typescript
import { useSocket } from '../providers/SocketProvider';

function MyComponent() {
  const { socket, isConnected, emit, on, off } = useSocket();

  useEffect(() => {
    const handler = (data) => console.log('Received:', data);
    on('event-name', handler);
    return () => off('event-name', handler);
  }, [on, off]);

  const sendMessage = () => {
    emit('send-message', { text: 'Hello!' });
  };

  return (
    <div>
      <span>Status: {isConnected ? 'Connected' : 'Disconnected'}</span>
      <button onClick={sendMessage}>Send</button>
    </div>
  );
}
```

### AIProvider (`app/src/providers/AIProvider.tsx`)

初始化 **memory**、**sessions**、**tool registry**（包括 memory + web-search 工具）、**entity manager**、**LLM / embedding providers** 和 **constitution** 加载。为子组件暴露 `useAI()`。重逻辑位于 `app/src/lib/ai/` 下。

### SkillProvider (`app/src/providers/SkillProvider.tsx`)

挂载时（认证后），通过 Tauri 辅助函数 (`runtimeDiscoverSkills`) 从 **QuickJS** 技能引擎发现技能，将 manifest 同步到 Redux，监听技能相关的 Tauri 事件，并可以在开发中自动启动配置的技能。

### UserProvider (`providers/UserProvider.tsx`)

最小用户 context provider（大多数用户状态在 Redux 中）。

#### 职责

* 兼容性用的遗留用户 context
* 可能弃用，改为 Redux

#### 实现

```typescript
interface UserContextValue {
  user: User | null;
  loading: boolean;
}

export function UserProvider({ children }) {
  const user = useAppSelector((state) => state.user.profile);
  const loading = useAppSelector((state) => state.user.loading);

  return (
    <UserContext.Provider value={{ user, loading }}>
      {children}
    </UserContext.Provider>
  );
}
```

#### 用法

```typescript
import { useUserContext } from '../providers/UserProvider';

function Header() {
  const { user, loading } = useUserContext();

  if (loading) return <Skeleton />;
  if (!user) return null;

  return <span>Welcome, {user.firstName}</span>;
}
```

### Provider 模式

#### 基于 Effect 的生命周期

Providers 使用 `useEffect` 管理服务生命周期：

```typescript
useEffect(() => {
  // 挂载或依赖变更时设置
  service.connect();

  // 卸载或依赖变更时清理
  return () => {
    service.disconnect();
  };
}, [dependencies]);
```

#### Redux 集成

Providers 从 Redux 读取并 dispatch：

```typescript
// 读取状态
const token = useAppSelector((state) => state.auth.token);

// Dispatch actions
const dispatch = useAppDispatch();
dispatch(setStatus({ userId, status: "connected" }));
```

#### 并行初始化

`SkillProvider` 和 `AIProvider` 可能在挂载时启动多个异步任务（技能发现、记忆初始化、constitution 加载）。优先阅读源码获取排序保证，而非假设到处都是并行 `Promise.all`。

#### 会话恢复

Providers 在挂载时恢复持久化状态：

```typescript
useEffect(() => {
  if (persistedSession) {
    service.restoreSession(persistedSession);
  }
}, [persistedSession]);
```

### Context vs Redux

| 使用 Context 用于 | 使用 Redux 用于 |
| ---------------------------------- | ---------------------------------- |
| 服务实例 (socket、client) | 可序列化状态 (status、data) |
| 方法 (emit、on、off) | 持久化状态 (sessions、tokens) |
| 派生值 | 复杂状态逻辑 |

示例：

* `SocketContext` 提供 `socket` 实例和 `emit` 方法
* Redux 存储 `socketStatus` 和 `socketId`

### 测试 Providers

#### 测试用的 Mock Provider

```typescript
// test-utils.tsx
const mockSocketContext: SocketContextValue = {
  socket: null,
  isConnected: true,
  emit: jest.fn(),
  on: jest.fn(),
  off: jest.fn()
};

export function TestProviders({ children }) {
  return (
    <Provider store={testStore}>
      <SocketContext.Provider value={mockSocketContext}>
        {children}
      </SocketContext.Provider>
    </Provider>
  );
}
```

#### 测试 Provider Effects

```typescript
test('SocketProvider 在 token 可用时连接', () => {
  const store = createTestStore({ auth: { token: 'test-token' } });

  render(
    <Provider store={store}>
      <SocketProvider>
        <TestComponent />
      </SocketProvider>
    </Provider>
  );

  expect(socketService.connect).toHaveBeenCalledWith('test-token');
});
```

***

## Human Mascot 表面

Human 页面 (`app/src/features/human/HumanPage.tsx`) 在对话侧边栏旁渲染主
`YellowMascot`。mascot  face 仍然来自 `useHumanMascot`，它订阅聊天生命周期事件以获取 thinking、
speaking、acknowledgement 和 error 状态。

子智能体委托由 `SubMascotLayer` 可视化。它不引入新的 socket 协议。相反，它读取已选或活跃 thread 的
`chatRuntime.toolTimelineByThread` 条目，`ChatRuntimeProvider` 已经从
`subagent_spawned`、`subagent_completed`、`subagent_failed`、
`subagent_iteration_start`、`subagent_tool_call` 和 `subagent_tool_result` 构建了这些条目。

生命周期映射：

| Runtime timeline 状态 | Sub-mascot 状态 |
| ---------------------- | ---------------- |
| `running` | 带 thinking face 和短活动气泡的小型彩色 mascot |
| `success` | 相同 mascot 解析为 happy face 和完成气泡 |
| `error` | 相同 mascot 解析为 concerned face 和失败气泡 |

活动气泡文本有意保持紧凑：当前子工具调用、子迭代、委托提示摘录或最终状态。Thread timeline 仍然是权威的详细视图；sub-mascot 只是主 mascot 周围可一瞥的编排层。

***

## 页面与路由

应用使用 HashRouter 配合受保护和公共路由守卫。

### 路由结构

在 **`app/src/AppRoutes.tsx`** (HashRouter) 中定义。近似映射：

```
/                  → Welcome (公共包装器)
/onboarding        → Onboarding (auth，onboarding 未完成)
/mnemonic          → Mnemonic / 加密设置 (auth)
/home              → Home (auth + onboarding + 加密密钥)
/intelligence      → Intelligence (auth)
/skills            → Skills (auth)
/conversations     → Conversations (auth)
/invites           → Invites (auth)
/agents            → Agents (auth)
/settings/*        → Settings (auth)
*                  → DefaultRedirect
```

`AppRoutes` 中**没有**顶级 `/login` 路由；认证流程通过 welcome/onboarding 和后端重定向处理。

### 路由配置 (`AppRoutes.tsx`)

```typescript
export function AppRoutes() {
  return (
    <>
      <Routes>
        {/* 公共路由 - 已认证时重定向 */}
        <Route element={<PublicRoute />}>
          <Route path="/" element={<Welcome />} />
          <Route path="/login" element={<Login />} />
        </Route>

        {/* 受保护路由 - 需要认证 */}
        <Route element={<ProtectedRoute />}>
          <Route path="/onboarding/*" element={<Onboarding />} />
        </Route>

        {/* 受保护 + 已 onboard 路由 */}
        <Route element={<ProtectedRoute requireOnboarded />}>
          <Route path="/home" element={<Home />} />
        </Route>

        {/* Fallback 重定向 */}
        <Route path="*" element={<DefaultRedirect />} />
      </Routes>

      {/* 设置模态覆盖层 - 在路由之上渲染 */}
      <SettingsModal />
    </>
  );
}
```

### 路由守卫

#### PublicRoute (`components/PublicRoute.tsx`)

将已认证用户从公共页面重定向走。

```typescript
export function PublicRoute() {
  const token = useAppSelector((state) => state.auth.token);
  const isOnboarded = useAppSelector((state) =>
    selectIsOnboarded(state, userId),
  );

  if (token) {
    // 已认证 - 重定向到适当页面
    return <Navigate to={isOnboarded ? "/home" : "/onboarding"} replace />;
  }

  return <Outlet />;
}
```

#### ProtectedRoute (`components/ProtectedRoute.tsx`)

强制执行认证和可选的 onboarding 状态。

```typescript
interface ProtectedRouteProps {
  requireOnboarded?: boolean;
}

export function ProtectedRoute({ requireOnboarded = false }) {
  const token = useAppSelector((state) => state.auth.token);
  const isOnboarded = useAppSelector((state) =>
    selectIsOnboarded(state, userId),
  );

  if (!token) {
    return <Navigate to="/login" replace />;
  }

  if (requireOnboarded && !isOnboarded) {
    return <Navigate to="/onboarding" replace />;
  }

  return <Outlet />;
}
```

#### DefaultRedirect (`components/DefaultRedirect.tsx`)

基于 auth 状态的 fallback 路由。

```typescript
export function DefaultRedirect() {
  const token = useAppSelector((state) => state.auth.token);
  const isOnboarded = useAppSelector((state) =>
    selectIsOnboarded(state, userId),
  );

  if (!token) {
    return <Navigate to="/" replace />;
  }

  if (!isOnboarded) {
    return <Navigate to="/onboarding" replace />;
  }

  return <Navigate to="/home" replace />;
}
```

### 页面

#### Welcome 页面 (`pages/Welcome.tsx`)

未认证用户的落地页。

**特性：**

* 应用介绍和品牌
* 登录/注册 CTA
* 公共路由（已认证时重定向）

#### Login 页面 (`pages/Login.tsx`)

认证页面。

**特性：**

* Telegram OAuth 按钮
* 在浏览器中打开 `/auth/telegram?platform=desktop`
* 处理深度链接回调

```typescript
export function Login() {
  const handleTelegramLogin = () => {
    // 在系统浏览器中打开 Telegram OAuth
    openUrl(`${BACKEND_URL}/auth/telegram?platform=desktop`);
  };

  return (
    <div className="login-page">
      <TelegramLoginButton onClick={handleTelegramLogin} />
    </div>
  );
}
```

#### Home 页面 (`pages/Home.tsx`)

认证后的主仪表板。

**特性：**

* 受保护路由（需要 auth + onboarded）
* 连接状态指示器
* 导航到设置模态
* 未来：聊天列表、消息等

```typescript
export function Home() {
  const navigate = useNavigate();
  const user = useAppSelector((state) => state.user.profile);
  const telegramStatus = useAppSelector((state) =>
    selectTelegramConnectionStatus(state, user?.id),
  );

  return (
    <div className="home-page">
      <header>
        <h1>Welcome, {user?.firstName}</h1>
        <button onClick={() => navigate("/settings")}>Settings</button>
      </header>

      <TelegramConnectionIndicator status={telegramStatus} />
      <ConnectionIndicator />

      {/* 主内容 */}
    </div>
  );
}
```

### Onboarding 流程 (`pages/onboarding/`)

多步 onboarding 流程。

#### 结构

```text
pages/onboarding/
├── Onboarding.tsx           # 流程控制器
└── steps/
    ├── GetStartedStep.tsx   # Welcome
    ├── PrivacyStep.tsx      # 隐私政策
    ├── AnalyticsStep.tsx    # Analytics 选择加入
    ├── ConnectStep.tsx      # Telegram 连接
    └── FeaturesStep.tsx     # 特性概览
```

#### Onboarding 控制器 (`Onboarding.tsx`)

```typescript
const STEPS = [
  { id: "get-started", component: GetStartedStep },
  { id: "privacy", component: PrivacyStep },
  { id: "analytics", component: AnalyticsStep },
  { id: "connect", component: ConnectStep },
  { id: "features", component: FeaturesStep },
];

export function Onboarding() {
  const [currentStep, setCurrentStep] = useState(0);
  const dispatch = useAppDispatch();
  const navigate = useNavigate();

  const handleNext = () => {
    if (currentStep < STEPS.length - 1) {
      setCurrentStep(currentStep + 1);
    } else {
      // 完成 onboarding
      dispatch(setOnboarded({ userId, isOnboarded: true }));
      navigate("/home");
    }
  };

  const handleBack = () => {
    if (currentStep > 0) {
      setCurrentStep(currentStep - 1);
    }
  };

  const StepComponent = STEPS[currentStep].component;

  return (
    <div className="onboarding">
      <ProgressIndicator current={currentStep} total={STEPS.length} />
      <StepComponent onNext={handleNext} onBack={handleBack} />
    </div>
  );
}
```

#### Step 组件

每个 step 接收 `onNext` 和 `onBack` 回调：

```typescript
interface StepProps {
  onNext: () => void;
  onBack: () => void;
}

export function ConnectStep({ onNext, onBack }: StepProps) {
  const [showModal, setShowModal] = useState(false);
  const telegramStatus = useAppSelector(/* ... */);

  return (
    <div className="step">
      <h2>Connect Your Accounts</h2>

      {connectOptions.map((option) => (
        <ConnectionOption
          key={option.id}
          {...option}
          onClick={() => option.id === "telegram" && setShowModal(true)}
        />
      ))}

      <TelegramConnectionModal
        isOpen={showModal}
        onClose={() => setShowModal(false)}
      />

      <div className="actions">
        <button onClick={onBack}>Back</button>
        <button onClick={onNext}>Continue</button>
      </div>
    </div>
  );
}
```

### 设置模态路由

设置模态使用基于 URL 的路由覆盖现有内容。

#### 模态检测

```typescript
// 在 SettingsModal.tsx 中
const location = useLocation();
const isOpen = location.pathname.startsWith("/settings");
```

#### 子路由

```text
/settings              → SettingsHome (主菜单)
/settings/connections  → ConnectionsPanel
/settings/messaging    → MessagingPanel (未来)
/settings/privacy      → PrivacyPanel (未来)
/settings/profile      → ProfilePanel (未来)
/settings/advanced     → AdvancedPanel (未来)
/settings/billing      → BillingPanel (未来)
```

#### 导航

```typescript
import { useSettingsNavigation } from "./hooks/useSettingsNavigation";

function SettingsHome() {
  const { navigateTo, closeModal } = useSettingsNavigation();

  return (
    <div>
      <SettingsMenuItem
        label="Connections"
        onClick={() => navigateTo("connections")}
      />
      <button onClick={closeModal}>Close</button>
    </div>
  );
}
```

### HashRouter vs BrowserRouter

应用使用 HashRouter 以兼容桌面：

```typescript
// App.tsx
import { HashRouter } from "react-router-dom";

// URL 看起来像这样：app://localhost/#/home
// 而不是：app://localhost/home
```

**为什么用 HashRouter：**

1. Tauri 深度链接与基于 hash 的 URL 配合工作
2. 不需要服务器配置
3. 与 file:// 协议配合工作
4. 防止直接 URL 访问时的 404

### 深度链接处理

深度链接在路由前处理：

```typescript
// main.tsx
import("./utils/desktopDeepLinkListener").then((m) => {
  m.setupDesktopDeepLinkListener().catch(console.error);
});
```

监听器拦截 `openhuman://auth?token=...` 并：

1. 通过 Rust 命令交换 token
2. 在 Redux 中存储会话
3. 导航到 `/onboarding` 或 `/home`

### 导航模式

#### 程序化导航

```typescript
import { useNavigate } from "react-router-dom";

const navigate = useNavigate();

// 导航到路由
navigate("/home");

// 替换历史条目
navigate("/login", { replace: true });

// 返回
navigate(-1);
```

#### Link 组件

```typescript
import { Link } from "react-router-dom";

<Link to="/settings">Settings</Link>;
```

#### 状态传递

```typescript
// 向路由传递状态
navigate("/details", { state: { itemId: 123 } });

// 接收状态
const location = useLocation();
const { itemId } = location.state;
```

***

## 组件

按功能组织的可复用 React 组件。

### 组件结构

```text
components/
├── Route Guards
│   ├── ProtectedRoute.tsx
│   ├── PublicRoute.tsx
│   └── DefaultRedirect.tsx
│
├── Authentication
│   └── TelegramLoginButton.tsx
│
├── Connection Status
│   ├── ConnectionIndicator.tsx
│   ├── TelegramConnectionIndicator.tsx
│   ├── TelegramConnectionModal.tsx
│   └── GmailConnectionIndicator.tsx
│
├── Onboarding
│   ├── ProgressIndicator.tsx
│   └── LottieAnimation.tsx
│
├── Settings Modal (16 files)
│   ├── SettingsModal.tsx
│   ├── SettingsLayout.tsx
│   ├── SettingsHome.tsx
│   ├── panels/
│   ├── components/
│   └── hooks/
│
└── Development
    └── DesignSystemShowcase.tsx
```

### 路由守卫组件

#### ProtectedRoute

需要认证和可选的 onboarding。

```typescript
interface ProtectedRouteProps {
  requireOnboarded?: boolean;
}

// 在 AppRoutes.tsx 中的用法
<Route element={<ProtectedRoute />}>
  <Route path="/onboarding/*" element={<Onboarding />} />
</Route>

<Route element={<ProtectedRoute requireOnboarded />}>
  <Route path="/home" element={<Home />} />
</Route>
```

#### PublicRoute

将已认证用户重定向走。

```typescript
// 在 AppRoutes.tsx 中的用法
<Route element={<PublicRoute />}>
  <Route path="/" element={<Welcome />} />
  <Route path="/login" element={<Login />} />
</Route>
```

#### DefaultRedirect

基于 auth 状态的 fallback。

```typescript
// 重定向到：
// - "/" 如果未认证
// - "/onboarding" 如果已认证但未 onboard
// - "/home" 如果已认证且已 onboard
```

### 认证组件

#### TelegramLoginButton

Telegram 的 OAuth 登录按钮。

```typescript
interface TelegramLoginButtonProps {
  onClick: () => void;
  disabled?: boolean;
}

// 用法
<TelegramLoginButton
  onClick={() => openUrl(`${BACKEND_URL}/auth/telegram?platform=desktop`)}
/>
```

### 连接状态组件

#### ConnectionIndicator

通用连接状态徽章。

```typescript
interface ConnectionIndicatorProps {
  status: 'connected' | 'connecting' | 'disconnected' | 'error';
  label?: string;
}

<ConnectionIndicator status="connected" label="Socket" />
```

#### TelegramConnectionIndicator

Telegram 特定的状态显示。

```typescript
interface TelegramConnectionIndicatorProps {
  status: 'connected' | 'connecting' | 'disconnected' | 'error';
}

// 配合 Redux 状态使用
const telegramStatus = useAppSelector((state) =>
  selectTelegramConnectionStatus(state, userId)
);

<TelegramConnectionIndicator status={telegramStatus} />
```

#### TelegramConnectionModal

设置 Telegram 连接的模态。

```typescript
interface TelegramConnectionModalProps {
  isOpen: boolean;
  onClose: () => void;
}

// 在 onboarding/settings 中的用法
const [showModal, setShowModal] = useState(false);

<TelegramConnectionModal
  isOpen={showModal}
  onClose={() => setShowModal(false)}
/>
```

**特性：**

* QR 码登录流程
* 手机号登录流程
* 连接状态显示
* 错误处理

#### GmailConnectionIndicator

Gmail 状态徽章（未来集成）。

```typescript
<GmailConnectionIndicator status="coming-soon" />
```

### Onboarding 组件

#### ProgressIndicator

通过 onboarding step 的视觉进度。

```typescript
interface ProgressIndicatorProps {
  current: number;
  total: number;
}

<ProgressIndicator current={2} total={5} />
```

#### LottieAnimation

Onboarding 的 Lottie 动画播放器。

```typescript
interface LottieAnimationProps {
  animationData: object;
  loop?: boolean;
  autoplay?: boolean;
  className?: string;
}

import welcomeAnimation from '../assets/animations/welcome.json';

<LottieAnimation
  animationData={welcomeAnimation}
  loop={true}
  autoplay={true}
/>
```

### 设置模态系统

带基于 URL 路由的完整模态系统。

#### 文件结构

```text
components/settings/
├── SettingsModal.tsx          # 基于路由的容器
├── SettingsLayout.tsx         # Portal + 背景包装器
├── SettingsHome.tsx           # 带 profile 的主菜单
├── panels/
│   ├── ConnectionsPanel.tsx   # 连接管理
│   ├── MessagingPanel.tsx     # (未来)
│   ├── PrivacyPanel.tsx       # (未来)
│   ├── ProfilePanel.tsx       # (未来)
│   ├── AdvancedPanel.tsx      # (未来)
│   └── BillingPanel.tsx       # (未来)
├── components/
│   ├── SettingsHeader.tsx     # 用户 profile 部分
│   ├── SettingsMenuItem.tsx   # 菜单项组件
│   ├── SettingsBackButton.tsx # 返回导航
│   └── SettingsPanelLayout.tsx# Panel 包装器
└── hooks/
    ├── useSettingsNavigation.ts # URL 路由
    └── useSettingsAnimation.ts  # 动画状态
```

#### SettingsModal

基于 URL 渲染的主容器。

```typescript
export function SettingsModal() {
  const location = useLocation();
  const isOpen = location.pathname.startsWith('/settings');

  if (!isOpen) return null;

  return (
    <SettingsLayout>
      {/* 路由到适当的 panel */}
      {location.pathname === '/settings' && <SettingsHome />}
      {location.pathname === '/settings/connections' && <ConnectionsPanel />}
      {/* ... 更多 panels */}
    </SettingsLayout>
  );
}
```

#### SettingsLayout

基于 Portal 的模态包装器。

```typescript
export function SettingsLayout({ children }) {
  const { closeModal } = useSettingsNavigation();

  return createPortal(
    <div className="fixed inset-0 z-50">
      {/* 背景 */}
      <div
        className="absolute inset-0 bg-black/50 backdrop-blur-sm"
        onClick={closeModal}
      />

      {/* 模态 */}
      <div className="absolute inset-4 flex items-center justify-center">
        <div className="bg-white rounded-2xl w-full max-w-[520px] shadow-xl">
          {children}
        </div>
      </div>
    </div>,
    document.body
  );
}
```

#### SettingsHome

带用户 profile 的主菜单。

```typescript
export function SettingsHome() {
  const { navigateTo, closeModal } = useSettingsNavigation();
  const user = useAppSelector((state) => state.user.profile);

  const menuItems = [
    { id: 'connections', label: 'Connections', icon: LinkIcon },
    { id: 'messaging', label: 'Messaging', icon: MessageIcon },
    { id: 'privacy', label: 'Privacy', icon: ShieldIcon },
    // ... 更多项
  ];

  return (
    <div>
      <SettingsHeader user={user} onClose={closeModal} />

      {menuItems.map((item) => (
        <SettingsMenuItem
          key={item.id}
          {...item}
          onClick={() => navigateTo(item.id)}
        />
      ))}
    </div>
  );
}
```

#### ConnectionsPanel

连接管理界面。

```typescript
export function ConnectionsPanel() {
  const { navigateBack } = useSettingsNavigation();
  const [telegramModalOpen, setTelegramModalOpen] = useState(false);

  const telegramStatus = useAppSelector((state) =>
    selectTelegramConnectionStatus(state, userId)
  );

  // 复用 onboarding 中的 connectOptions
  const connections = connectOptions.map((opt) => ({
    ...opt,
    status: opt.id === 'telegram' ? telegramStatus : 'coming-soon'
  }));

  return (
    <SettingsPanelLayout title="Connections" onBack={navigateBack}>
      {connections.map((conn) => (
        <ConnectionItem
          key={conn.id}
          {...conn}
          onConnect={() => conn.id === 'telegram' && setTelegramModalOpen(true)}
        />
      ))}

      <TelegramConnectionModal
        isOpen={telegramModalOpen}
        onClose={() => setTelegramModalOpen(false)}
      />
    </SettingsPanelLayout>
  );
}
```

#### 设置 Hooks

**useSettingsNavigation**

设置模态的基于 URL 导航。

```typescript
interface UseSettingsNavigationReturn {
  currentRoute: string;
  navigateTo: (panel: string) => void;
  navigateBack: () => void;
  closeModal: () => void;
}

const { navigateTo, navigateBack, closeModal } = useSettingsNavigation();

// 导航到 panel
navigateTo('connections'); // → /settings/connections

// 返回
navigateBack(); // → /settings

// 关闭模态
closeModal(); // → 之前的非设置路由
```

**useSettingsAnimation**

设置模态的动画状态管理。

```typescript
interface UseSettingsAnimationReturn {
  isEntering: boolean;
  isExiting: boolean;
  animationClass: string;
}

const { animationClass } = useSettingsAnimation();

<div className={`modal ${animationClass}`}>{/* Content */}</div>
```

#### 设置组件

**SettingsHeader**

设置顶部的用户 profile 部分。

```typescript
interface SettingsHeaderProps {
  user: User | null;
  onClose: () => void;
}

<SettingsHeader user={user} onClose={handleClose} />
```

**SettingsMenuItem**

带图标和 chevron 的单个菜单项。

```typescript
interface SettingsMenuItemProps {
  label: string;
  icon: React.ComponentType;
  onClick: () => void;
  badge?: string;
  disabled?: boolean;
}

<SettingsMenuItem
  label="Connections"
  icon={LinkIcon}
  onClick={() => navigateTo('connections')}
  badge="2"
/>
```

**SettingsBackButton**

返回导航按钮。

```typescript
interface SettingsBackButtonProps {
  onClick: () => void;
}

<SettingsBackButton onClick={navigateBack} />
```

**SettingsPanelLayout**

设置 panel 的包装器。

```typescript
interface SettingsPanelLayoutProps {
  title: string;
  onBack: () => void;
  children: React.ReactNode;
}

<SettingsPanelLayout title="Connections" onBack={navigateBack}>
  {/* Panel content */}
</SettingsPanelLayout>
```

### 组件模式

#### 复用连接选项

`connectOptions` 数组在 onboarding 和 settings 之间共享：

```typescript
// 在 ConnectStep.tsx 中定义，在其他地方导入
export const connectOptions = [
  {
    id: 'telegram',
    label: 'Telegram',
    icon: TelegramIcon,
    description: 'Connect your Telegram account',
  },
  {
    id: 'gmail',
    label: 'Gmail',
    icon: GmailIcon,
    description: 'Connect your Gmail account',
    comingSoon: true,
  },
];
```

#### 通过 Portal 的模态

设置模态使用 `createPortal` 在组件树外部渲染：

```typescript
return createPortal(
  <div className="modal-container">
    {/* 模态内容 */}
  </div>,
  document.body
);
```

#### 受控 vs 非受控

连接模态是受控组件：

```typescript
// 父级控制 open 状态
const [isOpen, setIsOpen] = useState(false);

<TelegramConnectionModal
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
/>
```

***

## Hook 与工具

自定义 React hook 和工具函数。

### 自定义 Hooks

#### useSocket (`hooks/useSocket.ts`)

从任何组件访问 Socket.io 功能。

```typescript
interface UseSocketReturn {
  socket: Socket | null;
  isConnected: boolean;
  emit: (event: string, data: unknown) => void;
  on: (event: string, handler: Function) => void;
  off: (event: string, handler: Function) => void;
  once: (event: string, handler: Function) => void;
}

function useSocket(): UseSocketReturn;
```

**用法：**

```typescript
import { useSocket } from "../hooks/useSocket";

function ChatInput() {
  const { emit, isConnected } = useSocket();

  const sendMessage = (text: string) => {
    if (isConnected) {
      emit("chat:message", { text });
    }
  };

  return (
    <input
      disabled={!isConnected}
      onKeyDown={(e) => e.key === "Enter" && sendMessage(e.target.value)}
    />
  );
}
```

**配合事件监听器：**

```typescript
function Notifications() {
  const { on, off } = useSocket();
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    const handler = (notification) => {
      setNotifications((prev) => [...prev, notification]);
    };

    on("notification", handler);
    return () => off("notification", handler);
  }, [on, off]);

  return <NotificationList items={notifications} />;
}
```

#### useUser (`hooks/useUser.ts`)

访问用户 profile 数据和加载状态。

```typescript
interface UseUserReturn {
  user: User | null;
  loading: boolean;
  error: string | null;
  refetch: () => Promise<void>;
}

function useUser(): UseUserReturn;
```

**用法：**

```typescript
import { useUser } from "../hooks/useUser";

function ProfileHeader() {
  const { user, loading, error, refetch } = useUser();

  if (loading) return <Skeleton />;
  if (error) return <Error message={error} onRetry={refetch} />;
  if (!user) return null;

  return (
    <div className="profile">
      <Avatar src={user.avatar} />
      <span>
        {user.firstName} {user.lastName}
      </span>
    </div>
  );
}
```

#### 设置模态 Hooks

**useSettingsNavigation (`components/settings/hooks/useSettingsNavigation.ts`)**

设置模态的基于 URL 导航。

```typescript
interface UseSettingsNavigationReturn {
  currentRoute: string; // 当前设置路径
  navigateTo: (panel: string) => void; // 导航到 panel
  navigateBack: () => void; // 返回一级
  closeModal: () => void; // 完全关闭设置
}

function useSettingsNavigation(): UseSettingsNavigationReturn;
```

**用法：**

```typescript
import { useSettingsNavigation } from "./hooks/useSettingsNavigation";

function SettingsMenu() {
  const { navigateTo, closeModal } = useSettingsNavigation();

  return (
    <nav>
      <button onClick={() => navigateTo("connections")}>Connections</button>
      <button onClick={() => navigateTo("privacy")}>Privacy</button>
      <button onClick={closeModal}>Close</button>
    </nav>
  );
}
```

**useSettingsAnimation (`components/settings/hooks/useSettingsAnimation.ts`)**

设置模态的动画状态管理。

```typescript
interface UseSettingsAnimationReturn {
  isEntering: boolean; // 模态正在动画进入
  isExiting: boolean; // 模态正在动画退出
  animationClass: string; // 当前状态的 CSS 类
}

function useSettingsAnimation(): UseSettingsAnimationReturn;
```

**用法：**

```typescript
import { useSettingsAnimation } from "./hooks/useSettingsAnimation";

function SettingsModal() {
  const { animationClass, isExiting } = useSettingsAnimation();

  return <div className={`modal ${animationClass}`}>{/* Content */}</div>;
}
```

### 工具

#### 配置 (`utils/config.ts`)

构建时环境变量访问。这些常量只携带烘焙到 bundle 中的值，对于应用实际通信的**运行时** URL，见 `services/backendUrl` 和下方的 `hooks/useBackendUrl`。

```typescript
// 仅构建时 fallback（在 Tauri 外使用）。
export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.example.com';

// 调试模式
export const DEBUG = import.meta.env.VITE_DEBUG === 'true';
```

**用法（仅构建时、feature flag、调试开关、…）：**

```typescript
import { DEBUG } from '../utils/config';

if (DEBUG) {
  console.log('debug enabled');
}
```

> **不要**直接导入 `BACKEND_URL` 来发起 API 调用。在运行时解析 URL，以便核心 sidecar 的 `api_url`（通过登录屏幕上的 `openhuman.config_resolve_api_url` 设置）生效：
>
> ```typescript
> // React 组件
> import { useBackendUrl } from '../hooks/useBackendUrl';
> const backendUrl = useBackendUrl();
>
> // 非 React 代码
> import { getBackendUrl } from '../services/backendUrl';
> const backendUrl = await getBackendUrl();
> ```

#### 深度链接 (`utils/deeplink.ts`)

为认证交接构建深度链接 URL。

```typescript
// 构建 auth 深度链接
function buildAuthDeepLink(token: string): string;

// 解析深度链接 URL
function parseDeepLink(url: string): { path: string; params: URLSearchParams };
```

**用法：**

```typescript
import { buildAuthDeepLink } from '../utils/deeplink';

// 为浏览器重定向构建 URL
const deepLink = buildAuthDeepLink(loginToken);
// → "openhuman://auth?token=abc123"

// 在 Web 前端 auth 后：
window.location.href = deepLink;
```

#### 桌面深度链接监听器 (`utils/desktopDeepLinkListener.ts`)

在桌面应用中处理传入的深度链接。

```typescript
// 设置深度链接事件监听器
async function setupDesktopDeepLinkListener(): Promise<void>;
```

**在 main.tsx 中调用：**

```typescript
// 懒加载以确保 Tauri IPC 就绪
import('./utils/desktopDeepLinkListener').then(m => {
  m.setupDesktopDeepLinkListener().catch(console.error);
});
```

**它做什么：**

1. 监听来自 Tauri 深度链接插件的 `onOpenUrl` 事件
2. 解析 `openhuman://auth?token=...` URL
3. 调用 Rust `exchange_token` 命令（绕过 CORS）
4. 在 Redux 中存储会话
5. 导航到 `/onboarding` 或 `/home`

**循环预防：**

```typescript
// 导航前设置 flag 以防止重新处理
localStorage.setItem('deepLinkHandled', 'true');
window.location.replace('/');

// 下次加载时，清除 flag
if (localStorage.getItem('deepLinkHandled') === 'true') {
  localStorage.removeItem('deepLinkHandled');
  return; // 不再处理
}
```

#### URL 打开器 (`utils/openUrl.ts`)

跨平台 URL 打开。

```typescript
// 在系统浏览器中打开 URL
async function openUrl(url: string): Promise<void>;
```

**用法：**

```typescript
import { openUrl } from '../utils/openUrl';

// 在系统浏览器中打开（非应用内 WebView）
await openUrl('https://telegram.org/auth');
```

**实现：**

```typescript
export async function openUrl(url: string): Promise<void> {
  try {
    // 先尝试 Tauri opener 插件
    const { open } = await import('@tauri-apps/plugin-opener');
    await open(url);
  } catch {
    // Fallback 到浏览器 API
    window.open(url, '_blank');
  }
}
```

### Polyfills (`polyfills.ts`)

浏览器环境的 Node.js polyfills。

`telegram` npm 包需要 Node.js API。这些被 polyfill：

```typescript
// polyfills.ts
import { Buffer } from 'buffer';
import process from 'process';
import util from 'util';

window.Buffer = Buffer;
window.process = process;
window.util = util;
```

**在应用入口导入：**

```typescript
// main.tsx
import './polyfills';

// ... 应用的其余部分
```

**Vite 配置：**

```typescript
// vite.config.ts
export default defineConfig({
  resolve: { alias: { buffer: 'buffer', process: 'process/browser', util: 'util' } },
  define: { 'process.env': {}, global: 'globalThis' },
});
```

### 类型

#### API 类型 (`types/api.ts`)

```typescript
// API 响应包装器
interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

// API 错误
interface ApiError {
  code: string;
  message: string;
  details?: unknown;
}

// User 接口
interface User {
  id: string;
  firstName: string;
  lastName?: string;
  username?: string;
  email?: string;
  avatar?: string;
  telegramId?: string;
  subscription?: SubscriptionInfo;
  usage?: UsageInfo;
  createdAt: string;
  updatedAt: string;
}
```

#### Onboarding 类型 (`types/onboarding.ts`)

```typescript
// Onboarding step 定义
interface OnboardingStep {
  id: string;
  title: string;
  component: React.ComponentType<StepProps>;
}

// Step 组件 props
interface StepProps {
  onNext: () => void;
  onBack: () => void;
}

// 连接选项
interface ConnectionOption {
  id: string;
  label: string;
  icon: React.ComponentType;
  description: string;
  comingSoon?: boolean;
}
```

### 静态数据

#### 国家 (`data/countries.ts`)

手机号输入的国家列表。

```typescript
interface Country {
  code: string; // "US"
  name: string; // "United States"
  dialCode: string; // "+1"
  flag: string; // "🇺🇸"
}

export const countries: Country[];
```

**用法：**

```typescript
import { countries } from "../data/countries";

function PhoneInput() {
  const [country, setCountry] = useState(countries[0]);

  return (
    <div>
      <select
        value={country.code}
        onChange={(e) =>
          setCountry(countries.find((c) => c.code === e.target.value))
        }
      >
        {countries.map((c) => (
          <option key={c.code} value={c.code}>
            {c.flag} {c.name} ({c.dialCode})
          </option>
        ))}
      </select>
      <input placeholder="Phone number" />
    </div>
  );
}
```

### 最佳实践

#### Hook 依赖

始终在 useEffect 中包含依赖：

```typescript
// 好
useEffect(() => {
  on('event', handler);
  return () => off('event', handler);
}, [on, off, handler]);

// 坏 - 缺失依赖
useEffect(() => {
  on('event', handler);
  return () => off('event', handler);
}, []);
```

#### 清理函数

始终清理订阅：

```typescript
useEffect(() => {
  const subscription = subscribe();
  return () => subscription.unsubscribe();
}, []);
```

#### 错误边界

将工具调用包装在 try-catch 中：

```typescript
try {
  await openUrl(url);
} catch (error) {
  console.error('Failed to open URL:', error);
  // Fallback 行为
}
```

#### 类型安全

对 API 调用使用 TypeScript 泛型：

```typescript
const user = await apiClient.get<User>('/users/me');
// user 被类型化为 User
```

***
