# 前端（app/src/）

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

这是一个整合参考。请使用上面的目录（或阅读器的大纲）在各章节之间跳转。

## 快速参考

| 章节                            | 涵盖                                            |
| ----------------------------- | --------------------------------------------- |
| [架构](#architecture-overview)  | Provider 链、构建、布局、约定                           |
| [状态管理](#state-management)     | Redux Toolkit slice、选择器、持久化                   |
| [服务层](#services-layer)        | `apiClient`, `socketService`, `coreRpcClient` |
| [Providers](#providers)       | `用户`, `Socket`, `AI`, `技能` providers          |
| [页面与路由](#pages-routing)       | `HashRouter`、路由守卫、主路由                         |
| [组件](#components)             | UI / 设置组件模式                                   |
| [Hooks 与工具](#hooks-utilities) | 共享 hooks、辅助函数、配置                              |

## 规模

| 指标                                       | 值                                                                    |
| ---------------------------------------- | -------------------------------------------------------------------- |
| 位于以下目录下的 TypeScript / TSX 文件： `app/src/` | \~285 (`find app/src -name '*.ts' -o -name '*.tsx' \| wc -l` （刷新以更新） |
| 测试运行器                                    | Vitest（`app/test/vitest.config.ts`)                                  |

## 目录布局

```
app/src/
├── App.tsx                 # Provider 链 + HashRouter 外壳
├── AppRoutes.tsx           # 路由表 + 守卫
├── main.tsx                # 入口（Sentry、store、样式）
├── store/                  # Redux slices 和选择器
├── providers/              # UserProvider、SocketProvider、AIProvider、SkillProvider
├── services/               # apiClient、socketService、coreRpcClient、api/*
├── lib/                    # AI 加载器、MCP 辅助、技能同步等
├── pages/                  # 路由级页面
├── components/             # 共享 UI
├── hooks/                  # 应用 hooks
├── utils/                  # 配置、Tauri 辅助、路由工具
└── assets/                 # 图标和静态资源
```

## 架构概览

### 系统架构

OpenHuman 的桌面端 UI 是一个 **React 19** 应用（`app/src/`）其：

* 使用 **Redux Toolkit** 对与会话相关的状态进行持久化
* 通过 **REST** (`apiClient`）和 **Socket.io** (`socketService`)
* 调用 **Rust 核心** 进程，通过 **`coreRpcClient`** / Tauri **`core_rpc_relay`** （JSON-RPC 方法实现于仓库根目录 `src/openhuman/`，并通过 `core_server`)
* 加载 **AI 提示词** ，来源于打包内置的 `src/openhuman/agent/prompts` （仓库根目录）以及 Tauri 的 **`ai_get_config`** 在打包后获取
* 使用一个 **轻量级的 MCP 风格** 辅助层，位于 `lib/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`、引导和助记词门控           |

### Provider 链

```
Redux Provider
  └─ PersistGate
      └─ UserProvider
          └─ SocketProvider
              └─ AIProvider
                  └─ SkillProvider
                      └─ HashRouter
                          └─ AppRoutes（页面 + 设置）
```

**为什么这个顺序**

1. Redux 放在最外层，便于在全局使用 `useAppSelector` / dispatch。
2. `PersistGate` 在子组件假定认证状态稳定之前，先恢复已持久化的 slices。
3. `SocketProvider` 使用认证 token 连接 Socket.io。
4. `AIProvider` / `SkillProvider` 包裹依赖 socket 和 store 状态的功能。
5. `HashRouter` 为所有路由提供导航。

### 模块关系（简化）

```
App.tsx
  ├─ Redux store + persistor
  ├─ UserProvider - 用户资料 / 工作区上下文
  ├─ SocketProvider - token 存在时连接 socketService
  ├─ AIProvider - AI 会话 / 记忆客户端协调
  ├─ SkillProvider - 技能目录与同步
  └─ AppRoutes
       ├─ PublicRoute - 例如 `/` 上的 Welcome
       ├─ ProtectedRoute - 引导、主页、技能、设置等
       └─ DefaultRedirect - 未认证用户
```

### 服务层（概念）

```
services/
  ├─ apiClient        → 通过运行时解析的 URL 发起 REST 请求，URL 由 `services/backendUrl#getBackendUrl` 提供
  ├─ backendUrl       → 调用 `openhuman.config_resolve_api_url`；仅在 Tauri 之外回退到 VITE_BACKEND_URL
  ├─ socketService    → Socket.io；实时 + MCP 风格封装
  └─ coreRpcClient    → 到本地 openhuman core 的 HTTP（JSON-RPC），与 Tauri relay 配合使用
```

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

桌面应用不会把核心 RPC URL 或 API 主机写死进打包产物。运行时会按以下顺序解析（优先级从高到低）：

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

一旦 RPC 握手成功， `services/backendUrl` 会调用 `openhuman.config_resolve_api_url` 以从已加载的 core 中获取 `api_url` （以及其他安全的客户端字段） `Config`. `VITE_BACKEND_URL` 仅在应用运行于 Tauri 之外时作为 Web 回退值使用。

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

### 相关文档

* Rust 架构： [架构](https://github.com/tinyhumansai/openhuman/blob/main/gitbooks/developing/architecture.md)
* Tauri 外壳： [Tauri Shell](/openhuman/zh/kai-fa/architecture/tauri-shell.md)

## 状态管理

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

### Store 配置

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

```typescript
// 将所有 slices 与持久化结合
const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['auth', 'telegram'], // 已持久化的 slices
};
```

### Redux 状态结构

```typescript
RootState = {
  auth: {
    token: string | null, // JWT（已持久化）
    isOnboardedByUser: Record<string, boolean>, // 每个用户的标志（已持久化）
  },
  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 用户（已持久化）
  },
};
```

### Slices

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

管理 JWT token 和按用户划分的引导状态。

**状态：**

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

**动作：**

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

**选择器（`store/authSelectors.ts`):**

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

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

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

**状态：**

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

**动作：**

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

**选择器（`store/socketSelectors.ts`):**

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

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

存储用户资料数据。

**状态：**

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

**动作：**

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

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

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

**文件：**

* `index.ts` - slice 导出（actions、thunks）
* `types.ts` - 实体和状态接口
* `reducers.ts` - 同步 reducers
* `extraReducers.ts` - 异步 thunk 处理器
* `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` - 存储线程数据

**Thunks（`store/telegram/thunks.ts`):**

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

**选择器（`store/telegramSelectors.ts`):**

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

### 类型化 Hooks

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

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

### 持久化配置

#### 会持久化什么

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

#### 不会持久化什么

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

#### 存储后端

Redux-Persist 默认使用 localStorage 适配器。这是应用中唯一允许使用 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);
}
```

#### 分发动作

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

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

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

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

#### 使用选择器

```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. **始终使用类型化 hooks** - `useAppDispatch` 和 `useAppSelector`
2. **使用选择器获取派生状态** - 已记忆化且可测试
3. **将 thunks 放在单独文件中** - 更好的组织方式
4. **按用户划分状态作用域** - 按用户 ID 为状态分键
5. **避免使用 localStorage** - 改用 Redux-Persist

***

## 服务层

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

### 服务架构

```
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 core（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 端点（`services/api/`)

#### 认证 API（`services/api/authApi.ts`)

与身份验证相关的端点。

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

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

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

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

#### 用户 API（`services/api/userApi.ts`)

用户资料端点。

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

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

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

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

### Socket 服务（`services/socketService.ts`)

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

#### 特性

* 单例模式 - 每个应用单一连接
* 认证令牌通过 socket 传递 `auth` 对象
* 传输方式：先轮询，然后升级到 WebSocket
* 自动重连处理

#### API

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

// 使用认证令牌连接
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` 以了解完整流程（包括守护进程生命周期钩子）。

### 核心 RPC（`services/coreRpcClient.ts`)

桌面应用运行一个单独的 **`openhuman`** Rust 二进制文件（暂存于 `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, // 如果中继应确保 systemd/launchd 风格的服务，则为 true
});
```

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

### 与 provider 的服务集成

#### SocketProvider

`app/src/providers/SocketProvider.tsx` 在以下情况时连接： `auth.token` 存在。在 **Tauri**中，它优先使用 Rust 支持的 socket 路径；在 **web**中，它使用 JS Socket.io 客户端。有关日志记录和 `useDaemonLifecycle` 集成，请参阅源码。

#### UserProvider、AIProvider、SkillProvider

这些包装用户资料加载、AI/记忆客户端协调，以及技能目录/同步。它们位于 **内部** `PersistGate` 和 **外部** 或与路由器并列，如下所示： `App.tsx`.

### 最佳实践

1. **使用单例** - 永远不要创建多个服务实例
2. **在 Redux 中存储会话** - 不要用 localStorage
3. **在卸载时清理** - 在 useEffect 清理中断开连接
4. **优雅地处理错误** - 对暂时性失败进行重试
5. **通过正确的通道传递认证信息** - 使用 Socket auth 对象，而不是查询字符串

***

## Providers

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

### Provider 链

这些 provider 按特定顺序包装应用（`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>
```

(`路由器` 是 `HashRouter` 来自 `react-router-dom`.)

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

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

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

管理实时连接性： **web** 使用 JS Socket.io 客户端； **Tauri** 通过以下方式桥接到 Rust socket： `utils/tauriSocket.ts` 并将状态回报给 Redux。

#### 职责

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

#### 实现

参见 **`app/src/providers/SocketProvider.tsx`**。该文件根据 **`isTauri()`**&#x8FDB;行分支：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('已接收：', data);
    on('event-name', handler);
    return () => off('event-name', handler);
  }, [on, off]);

  const sendMessage = () => {
    emit('send-message', { text: '你好！' });
  };

  return (
    <div>
      <span>状态：{isConnected ? '已连接' : '未连接'}</span>
      <button onClick={sendMessage}>发送</button>
    </div>
  );
}
```

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

初始化 **memory**, **会话**, **工具注册表** （包括 memory + web-search 工具）， **实体管理器**, **LLM / 嵌入 provider**，以及 **constitution** 加载。向子组件暴露 `useAI()` 。主要逻辑位于 `app/src/lib/ai/`.

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

在挂载时（认证后），从 **QuickJS** 技能引擎通过 Tauri 辅助函数（`runtimeDiscoverSkills`）发现技能，将清单同步到 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>欢迎，{user.firstName}</span>;
}
```

### Provider 模式

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

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

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

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

#### Redux 集成

Provider 从 Redux 读取并向 Redux 分发：

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

// 分发 action
const dispatch = useAppDispatch();
dispatch(setStatus({ userId, status: "connected" }));
```

#### 并行初始化

`SkillProvider` 和 `AIProvider` 可能会在挂载时启动多个异步任务（技能发现、memory 初始化、constitution 加载）。关于顺序保证，优先阅读源码，而不要假设到处都并行使用 `Promise.all` 。

#### 会话恢复

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

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

### Context 与 Redux

| Context 用于          | Redux 用于      |
| ------------------- | ------------- |
| 服务实例（socket、client） | 可序列化状态（状态、数据） |
| 方法（emit、on、off）     | 持久化状态（会话、令牌）  |
| 派生值                 | 复杂状态逻辑        |

示例：

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

### 测试 Provider

#### 用于测试的 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 Effect

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

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

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

***

## Human 吉祥物界面

Human 页面（`app/src/features/human/HumanPage.tsx`）渲染主要的 `YellowMascot` 在对话侧边栏旁边。吉祥物的脸部仍然来自 `useHumanMascot`，它会订阅聊天生命周期事件以表示思考、说话、确认和错误状态。

子代理委派通过以下方式可视化： `SubMascotLayer`。它不会引入新的 socket 协议。相反，它读取所选或活动线程的 `chatRuntime.toolTimelineByThread` 条目，这些条目由 `ChatRuntimeProvider` 已经基于以下事件构建： `subagent_spawned`, `subagent_completed`, `subagent_failed`, `subagent_iteration_start`, `subagent_tool_call`，以及 `subagent_tool_result`.

生命周期映射：

| 运行时时间线状态 | 子吉祥物状态                |
| -------- | --------------------- |
| `正在运行的`  | 带有思考表情和简短活动气泡的小型彩色吉祥物 |
| `成功`     | 同一个吉祥物变为开心表情和完成气泡     |
| `错误`     | 同一个吉祥物变为担忧表情和失败气泡     |

活动气泡文本刻意保持紧凑：当前子工具调用、子迭代、委派提示摘录或最终状态。线程时间线仍然是权威的详细视图；子吉祥物只是主吉祥物周围可快速浏览的编排层。

***

## 页面与路由

应用使用 HashRouter，并带有受保护和公开的路由守卫。

### 路由结构

定义于 **`app/src/AppRoutes.tsx`** （HashRouter）。大致映射：

```
/                  → 欢迎页（公开包装器）
/onboarding        → 引导流程（已认证，引导未完成）
/mnemonic          → 助记词 / 加密设置（已认证）
/home              → 首页（已认证 + 已完成引导 + 加密密钥）
/intelligence      → 智能（已认证）
/skills            → 技能（已认证）
/conversations     → 会话（已认证）
/invites           → 邀请（已认证）
/agents            → 代理（已认证）
/settings/*        → 设置（已认证）
*                  → 默认重定向
```

存在 **否** 顶层 `/login` 路由，位于 `AppRoutes`；身份验证流程通过 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>

        {/* 受保护 + 已完成引导的路由 */}
        <Route element={<ProtectedRoute requireOnboarded />}>
          <Route path="/home" element={<Home />} />
        </Route>

        {/* 回退重定向 */}
        <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`)

强制执行认证，并可选择检查引导状态。

```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`)

根据认证状态进行重定向的回退路由。

```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 />;
}
```

### 页面

#### 欢迎页（`pages/Welcome.tsx`)

未认证用户的落地页。

**特性：**

* 应用介绍和品牌展示
* 登录/注册 CTA
* 公开路由（若已认证则重定向）

#### 登录页（`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>
  );
}
```

#### 主页（`pages/Home.tsx`)

身份验证后的主仪表板。

**特性：**

* 受保护路由（需要身份验证 + 已完成引导）
* 连接状态指示器
* 导航到设置弹窗
* 未来：聊天列表、消息等。

```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>欢迎，{user?.firstName}</h1>
        <button onClick={() => navigate("/settings")}>设置</button>
      </header>

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

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

### 引导流程（`pages/onboarding/`)

多步骤引导流程。

#### 结构

```
pages/onboarding/
├── Onboarding.tsx           # 流程控制器
└── steps/
    ├── GetStartedStep.tsx   # 欢迎
    ├── PrivacyStep.tsx      # 隐私政策
    ├── AnalyticsStep.tsx    # 分析数据选择加入
    ├── ConnectStep.tsx      # Telegram 连接
    └── FeaturesStep.tsx     # 功能概览
```

#### 引导控制器（`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 {
      // 完成引导
      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>
  );
}
```

#### 步骤组件

每一步都会接收 `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>连接你的账号</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}>返回</button>
        <button onClick={onNext}>继续</button>
      </div>
    </div>
  );
}
```

### 设置弹窗路由

该设置弹窗使用基于 URL 的路由覆盖现有内容。

#### 弹窗检测

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

#### 子路由

```
/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}>关闭</button>
    </div>
  );
}
```

### HashRouter 与 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 命令交换令牌
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">设置</Link>;
```

#### 状态传递

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

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

***

## 组件

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

### 组件结构

```
components/
├── 路由守卫
│   ├── ProtectedRoute.tsx
│   ├── PublicRoute.tsx
│   └── DefaultRedirect.tsx
│
├── 身份验证
│   └── TelegramLoginButton.tsx
│
├── 连接状态
│   ├── ConnectionIndicator.tsx
│   ├── TelegramConnectionIndicator.tsx
│   ├── TelegramConnectionModal.tsx
│   └── GmailConnectionIndicator.tsx
│
├── 引导
│   ├── ProgressIndicator.tsx
│   └── LottieAnimation.tsx
│
├── 设置弹窗（16 个文件）
│   ├── SettingsModal.tsx
│   ├── SettingsLayout.tsx
│   ├── SettingsHome.tsx
│   ├── panels/
│   ├── components/
│   └── hooks/
│
└── 开发
    └── DesignSystemShowcase.tsx
```

### 路由守卫组件

#### ProtectedRoute

需要身份验证，并可选地需要完成引导。

```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

根据身份验证状态进行路由的兜底方案。

```typescript
// 重定向到：
// - 未认证时重定向到 "/"
// - 已认证但未完成引导时重定向到 "/onboarding"
// - 已认证且已完成引导时重定向到 "/home"
```

### 身份验证组件

#### 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;
}

// 在引导/设置中使用
const [showModal, setShowModal] = useState(false);

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

**特性：**

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

#### GmailConnectionIndicator

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

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

### 引导组件

#### ProgressIndicator

引导步骤的可视化进度。

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

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

#### LottieAnimation

用于引导的 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 路由的弹窗系统。

#### 文件结构

```
components/settings/
├── SettingsModal.tsx          # 基于路由的容器
├── SettingsLayout.tsx         # Portal + 背景遮罩包装器
├── SettingsHome.tsx           # 带个人资料的主菜单
├── panels/
│   ├── ConnectionsPanel.tsx   # 连接管理
│   ├── MessagingPanel.tsx     # （未来）
│   ├── PrivacyPanel.tsx       # （未来）
│   ├── ProfilePanel.tsx       # （未来）
│   ├── AdvancedPanel.tsx      # （未来）
│   └── BillingPanel.tsx       # （未来）
├── components/
│   ├── SettingsHeader.tsx     # 用户资料部分
│   ├── SettingsMenuItem.tsx   # 菜单项组件
│   ├── SettingsBackButton.tsx # 返回导航
│   └── SettingsPanelLayout.tsx# 面板包装器
└── 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>
      {/* 路由到相应面板 */}
      {location.pathname === '/settings' && <SettingsHome />}
      {location.pathname === '/settings/connections' && <ConnectionsPanel />}
      {/* ... 更多面板 */}
    </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

带有用户资料的主菜单。

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

  const menuItems = [
    { id: 'connections', label: '连接', icon: LinkIcon },
    { id: 'messaging', label: '消息', icon: MessageIcon },
    { id: 'privacy', label: '隐私', 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="连接" onBack={navigateBack}>
      {connections.map((conn) => (
        <ConnectionItem
          key={conn.id}
          {...conn}
          onConnect={() => conn.id === 'telegram' && setTelegramModalOpen(true)}
        />
      ))}

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

#### Settings Hooks

**useSettingsNavigation**

用于设置弹窗的基于 URL 的导航。

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

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

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

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

// 关闭弹窗
closeModal(); // → 上一个非设置路由
```

**useSettingsAnimation**

动画状态管理。

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

const { animationClass } = useSettingsAnimation();

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

#### 设置组件

**SettingsHeader**

设置顶部的用户资料部分。

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

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

**SettingsMenuItem**

带有图标和箭头的单个菜单项。

```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**

设置面板的包装器。

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

<SettingsPanelLayout title="连接" onBack={navigateBack}>
  {/* 面板内容 */}
</SettingsPanelLayout>
```

### 组件模式

#### 复用连接选项

这个 `connectOptions` 数组在引导和设置之间共享：

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

#### 通过 Portal 的弹窗

设置弹窗使用 `createPortal` 在组件树之外渲染：

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

#### 受控与非受控

连接弹窗是受控组件：

```typescript
// 父组件控制打开状态
const [isOpen, setIsOpen] = useState(false);

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

***

## Hooks 与工具

自定义 React hooks 和工具函数。

### 自定义 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`)

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

```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; // 导航到面板
  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")}>连接</button>
      <button onClick={() => navigateTo("privacy")}>隐私</button>
      <button onClick={closeModal}>关闭</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}`}>{/* 内容 */}</div>;
}
```

### 实用工具

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

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

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

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

**用法（仅构建时、特性标志、调试开关等）：**

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

if (DEBUG) {
  console.log('已启用调试');
}
```

> **不要** import `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
// 构建认证深层链接
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 前端中：
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. 监听来自 `onOpenUrl` Tauri 深层链接插件的事件
2. 解析 `openhuman://auth?token=...` URL
3. 调用 Rust `exchange_token` 命令（绕过 CORS）
4. 将会话存储到 Redux 中
5. 导航到 `/onboarding` 或 `/home`

**循环防止：**

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

// 下次加载时，清除标志
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 {
    // 回退到浏览器 API
    window.open(url, '_blank');
  }
}
```

### Polyfills (`polyfills.ts`)

面向浏览器环境的 Node.js polyfill。

这个 `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;
}

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

#### 引导类型 (`types/onboarding.ts`)

```typescript
// 引导步骤定义
interface OnboardingStep {
  id: string;
  title: string;
  component: React.ComponentType<StepProps>;
}

// 步骤组件属性
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; // "美国"
  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="电话号码" />
    </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('打开 URL 失败：', error);
  // 回退行为
}
```

#### 类型安全

对 API 调用使用 TypeScript 泛型：

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

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://tinyhumans.gitbook.io/openhuman/zh/kai-fa/architecture/frontend.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
