mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
📝 docs: add more cursor rules (#8047)
* 📝 docs: add some more cursor rules * 📝 docs: fix greptile reported issue
This commit is contained in:
@@ -29,37 +29,40 @@ LobeChat 的后端设计注重模块化、可测试性和灵活性,以适应
|
||||
其主要分层如下:
|
||||
|
||||
1. **客户端服务层 (`src/services`)**:
|
||||
* 位于 [src/services/](mdc:src/services)。
|
||||
* 位于 src/services/。
|
||||
* 这是客户端业务逻辑的核心层,负责封装各种业务操作和数据处理逻辑。
|
||||
* **环境适配**: 根据不同的运行环境,服务层会选择合适的数据访问方式:
|
||||
* **本地数据库模式**: 直接调用 `Model` 层进行数据操作,适用于浏览器 PGLite 和本地 Electron 应用。
|
||||
* **远程数据库模式**: 通过 `tRPC` 客户端调用服务端 API,适用于需要云同步的场景。
|
||||
* **类型转换**: 对于简单的数据类型转换,直接在此层进行类型断言,如 `this.pluginModel.query() as Promise<LobeTool[]>`(参见 [src/services/plugin/client.ts](mdc:src/services/plugin/client.ts))。
|
||||
* 每个服务模块通常包含 `client.ts`(本地模式)、`server.ts`(远程模式)和 `type.ts`(接口定义)文件。
|
||||
* **类型转换**: 对于简单的数据类型转换,直接在此层进行类型断言,如 `this.pluginModel.query() as Promise<LobeTool[]>`
|
||||
* 每个服务模块通常包含 `client.ts`(本地模式)、`server.ts`(远程模式)和 `type.ts`(接口定义)文件,在实现时应该确保本地模式和远程模式业务逻辑实现一致,只是数据库不同。
|
||||
|
||||
2. **API 接口层 (`TRPC`)**:
|
||||
* 位于 [src/server/routers/](mdc:src/server/routers)。
|
||||
* 位于 src/server/routers/
|
||||
* 使用 `tRPC` 构建类型安全的 API。Router 根据运行时环境(如 Edge Functions, Node.js Lambda)进行组织。
|
||||
* 负责接收客户端请求,并将其路由到相应的 `Service` 层进行处理。
|
||||
* 新建 lambda 端点时可以参考 src/server/routers/lambda/_template.ts
|
||||
|
||||
3. **服务端服务层 (`server/services`)**:
|
||||
* 位于 [src/server/services/](mdc:src/server/services)。
|
||||
* 位于 src/server/services/。
|
||||
* 核心职责是封装独立的、可复用的业务逻辑单元。这些服务应易于测试。
|
||||
* **平台差异抽象**: 一个关键特性是通过其内部的 `impls` 子目录(例如 [src/server/services/file/impls/](mdc:src/server/services/file/impls) 包含 `s3.ts` 和 `local.ts`)来抹平不同运行环境带来的差异(例如云端使用 S3 存储,桌面版使用本地文件系统)。这使得上层(如 `tRPC` routers)无需关心底层具体实现。
|
||||
* **平台差异抽象**: 一个关键特性是通过其内部的 `impls` 子目录(例如 src/server/services/file/impls 包含 s3.ts 和 local.ts)来抹平不同运行环境带来的差异(例如云端使用 S3 存储,桌面版使用本地文件系统)。这使得上层(如 `tRPC` routers)无需关心底层具体实现。
|
||||
* 目标是使 `tRPC` router 层的逻辑尽可能纯粹,专注于请求处理和业务流程编排。
|
||||
* 服务会调用 `Repository` 层或直接调用 `Model` 层进行数据持久化和检索,也可能调用其他服务。
|
||||
|
||||
4. **仓库层 (`Repositories`)**:
|
||||
* 位于 [src/database/repositories/](mdc:src/database/repositories)。
|
||||
* 位于 src/database/repositories/。
|
||||
* 主要处理**复杂的跨表查询和数据聚合**逻辑,特别是当需要从**多个 `Model`** 获取数据并进行组合时。
|
||||
* 与 `Model` 层不同,`Repository` 层专注于复杂的业务查询场景,而不涉及简单的领域模型转换。
|
||||
* 当业务逻辑涉及多表关联、复杂的数据统计或需要事务处理时,会使用 `Repository` 层。
|
||||
* 如果数据操作简单(仅涉及单个 `Model`),则通常直接在 `src/services` 层调用 `Model` 并进行简单的类型断言。
|
||||
|
||||
5. **模型层 (`Models`)**:
|
||||
* 位于 [src/database/models/](mdc:src/database/models) (例如 [src/database/models/plugin.ts](mdc:src/database/models/plugin.ts) 和 [src/database/models/document.ts](mdc:src/database/models/document.ts))。
|
||||
* 提供对数据库中各个表(由 [src/database/schemas/](mdc:src/database/schemas) 中的 Drizzle ORM schema 定义)的基本 CRUD (创建、读取、更新、删除) 操作和简单的查询能力。
|
||||
* 位于 src/database/models/ (例如 src/database/models/plugin.ts 和 src/database/models/document.ts)。
|
||||
* 提供对数据库中各个表(由 src/database/schemas/ 中的 Drizzle ORM schema 定义)的基本 CRUD (创建、读取、更新、删除) 操作和简单的查询能力。
|
||||
* `Model` 类专注于单个数据表的直接操作,**不涉及复杂的领域模型转换**,这些转换通常在上层的 `src/services` 中通过类型断言完成。
|
||||
* model(例如 Topic) 层接口经常需要从对应的 schema 层导入 NewTopic 和 TopicItem
|
||||
* 创建新的 model 时可以参考 src/database/models/_template.ts
|
||||
|
||||
6. **数据库 (`Database`)**:
|
||||
* **客户端模式 (浏览器/PWA)**: 使用 PGLite (基于 WASM 的 PostgreSQL),数据存储在用户浏览器本地。
|
||||
|
||||
@@ -24,4 +24,4 @@ Example:
|
||||
| 2 | Bob | User |
|
||||
| 3 | Charlie | Moderator |
|
||||
+----+---------+-----------+
|
||||
```
|
||||
```
|
||||
|
||||
@@ -5,4 +5,4 @@ alwaysApply: false
|
||||
---
|
||||
1. first read [lobe-chat-backend-architecture.mdc](mdc:.cursor/rules/lobe-chat-backend-architecture.mdc)
|
||||
2. refer to the [_template.ts](mdc:src/database/models/_template.ts) to create new model
|
||||
3. if an operation involves multiple models or complex queries, consider defining it in the `repositories` layer under `src/database/repositories/`
|
||||
3. if an operation involves multiple models or complex queries, consider defining it in the `repositories` layer under `src/database/repositories/`
|
||||
|
||||
@@ -199,4 +199,4 @@ slugUserIdUnique: uniqueIndex('slug_user_id_unique').on(t.slug, t.userId),
|
||||
**Future refactor**: Will likely be replaced with `isDefault: boolean()` field
|
||||
**Note**: Avoid using slugs for new features - prefer explicit boolean flags for status tracking
|
||||
|
||||
By following these guidelines, maintain consistency, type safety, and maintainability across database schema definitions.
|
||||
By following these guidelines, maintain consistency, type safety, and maintainability across database schema definitions.
|
||||
|
||||
@@ -180,4 +180,4 @@ export default {
|
||||
|
||||
- Check if the key exists in src/locales/default/namespace.ts
|
||||
- Ensure proper namespace import in component
|
||||
- Ensure new namespaces are exported in [src/locales/default/index.ts](mdc:src/locales/default/index.ts)
|
||||
- Ensure new namespaces are exported in [src/locales/default/index.ts](mdc:src/locales/default/index.ts)
|
||||
|
||||
@@ -34,6 +34,7 @@ The project uses the following technologies:
|
||||
- aHooks for react hooks library
|
||||
- dayjs for date and time library
|
||||
- lodash-es for utility library
|
||||
- fast-deep-equal for deep comparison of JavaScript objects
|
||||
- zod for data validation
|
||||
- TRPC for type safe backend
|
||||
- PGLite for client DB and PostgreSQL for backend DB
|
||||
@@ -43,4 +44,4 @@ The project uses the following technologies:
|
||||
- ESLint for code linting
|
||||
- Cursor AI for code editing and AI coding assistance
|
||||
|
||||
Note: All tools and libraries used are the latest versions. The application only needs to be compatible with the latest browsers;
|
||||
Note: All tools and libraries used are the latest versions. The application only needs to be compatible with the latest browsers;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
## System Role
|
||||
|
||||
You are an expert in full-stack Web development, proficient in JavaScript, TypeScript, CSS, React, Node.js, Next.js, Postgresql, all kinds of network protocols.
|
||||
|
||||
You are an expert in LLM and Ai art. In Ai image generation, you are proficient in Stable Diffusion and ComfyUI's architectural principles, workflows, model structures, parameter configurations, training methods, and inference optimization.
|
||||
|
||||
You are an expert in UI/UX design, proficient in web interaction patterns, responsive design, accessibility, and user behavior optimization. You excel at improving user retention and paid conversion rates through various interaction details.
|
||||
|
||||
|
||||
## Problem Solving
|
||||
|
||||
- When modifying existing code, clearly describe the differences and reasons for the changes
|
||||
- Provide alternative solutions that may be better overall or superior in specific aspects
|
||||
- Always consider using the latest technologies, standards, and APIs to strive for code optimization, not just the conventional wisdom
|
||||
- Provide optimization suggestions for deprecated API usage
|
||||
- Cite sources whenever possible at the end, not inline
|
||||
- When you provide multiple solutions, provide the recommended solution first, and note it as `Recommended`
|
||||
- Express uncertainty when there might not be a correct answer
|
||||
- Admit when you don't know something instead of guessing
|
||||
|
||||
## Code Implementation
|
||||
|
||||
- Write minimal code changes that are ONLY directly related to the requirements
|
||||
- Write correct, up-to-date, bug-free, fully functional, secure, maintainable and efficient code
|
||||
- First, think step-by-step: describe your plan in detailed pseudocode before implementation
|
||||
- Confirm the plan before writing code
|
||||
- Focus on maintainable over being performant
|
||||
- Leave NO TODOs, placeholders, or missing pieces
|
||||
- Be sure to reference file names
|
||||
- Please respect my prettier preferences when you provide code
|
||||
- When you notice I have manually modified the code, that was definitely on purpose and do not revert them
|
||||
- Don't remove meaningful code comments, be sure to keep original comments when providing applied code
|
||||
- Update the code comments when needed after you modify the related code
|
||||
- If documentation links or required files are missing, ask for them before proceeding with the task rather than making assumptions
|
||||
- If you're unable to access or retrieve content from websites, please inform me immediately and request the specific information needed rather than making assumptions
|
||||
- Sometimes ESLint errors may not be reasonable, and making changes could introduce logical bugs. If you find an ESLint rule unreasonable, disable it directly. For example, with the 'prefer-dom-node-text-content' rule, there are actual differences between innerText and textContent
|
||||
- You can use emojis, npm packages like `chalk`/`chalk-animation`/`terminal-link`/`gradient-string`/`log-symbols`/`boxen`/`consola`/`@clack/prompts` to create beautiful terminal output
|
||||
@@ -0,0 +1,318 @@
|
||||
---
|
||||
description:
|
||||
globs: src/store/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# LobeChat Zustand Action 组织模式
|
||||
|
||||
本文档详细说明了 LobeChat 项目中 Zustand Action 的组织方式、命名规范和实现模式,特别关注乐观更新与后端服务的集成。
|
||||
|
||||
## Action 类型分层
|
||||
|
||||
LobeChat 的 Action 采用分层架构,明确区分不同职责:
|
||||
|
||||
### 1. Public Actions
|
||||
对外暴露的主要接口,供 UI 组件调用:
|
||||
- 命名:动词形式(`createTopic`, `sendMessage`, `updateTopicTitle`)
|
||||
- 职责:参数验证、流程编排、调用 internal actions
|
||||
- 示例:[src/store/chat/slices/topic/action.ts](mdc:src/store/chat/slices/topic/action.ts)
|
||||
|
||||
```typescript
|
||||
// Public Action 示例
|
||||
createTopic: async () => {
|
||||
const { activeId, internal_createTopic } = get();
|
||||
const messages = chatSelectors.activeBaseChats(get());
|
||||
|
||||
if (messages.length === 0) return;
|
||||
|
||||
const topicId = await internal_createTopic({
|
||||
sessionId: activeId,
|
||||
title: t('defaultTitle', { ns: 'topic' }),
|
||||
messages: messages.map((m) => m.id),
|
||||
});
|
||||
|
||||
return topicId;
|
||||
},
|
||||
```
|
||||
|
||||
### 2. Internal Actions (`internal_*`)
|
||||
内部实现细节,处理核心业务逻辑:
|
||||
- 命名:`internal_` 前缀 + 动词(`internal_createTopic`, `internal_updateMessageContent`)
|
||||
- 职责:乐观更新、服务调用、错误处理、状态同步
|
||||
- 不应该被 UI 组件直接调用
|
||||
|
||||
```typescript
|
||||
// Internal Action 示例 - 乐观更新模式
|
||||
internal_createTopic: async (params) => {
|
||||
const tmpId = Date.now().toString();
|
||||
|
||||
// 1. 立即更新前端状态(乐观更新)
|
||||
get().internal_dispatchTopic(
|
||||
{ type: 'addTopic', value: { ...params, id: tmpId } },
|
||||
'internal_createTopic',
|
||||
);
|
||||
get().internal_updateTopicLoading(tmpId, true);
|
||||
|
||||
// 2. 调用后端服务
|
||||
const topicId = await topicService.createTopic(params);
|
||||
get().internal_updateTopicLoading(tmpId, false);
|
||||
|
||||
// 3. 刷新数据确保一致性
|
||||
get().internal_updateTopicLoading(topicId, true);
|
||||
await get().refreshTopic();
|
||||
get().internal_updateTopicLoading(topicId, false);
|
||||
|
||||
return topicId;
|
||||
},
|
||||
```
|
||||
|
||||
### 3. Dispatch Methods (`internal_dispatch*`)
|
||||
专门处理状态更新的方法:
|
||||
- 命名:`internal_dispatch` + 实体名(`internal_dispatchTopic`, `internal_dispatchMessage`)
|
||||
- 职责:调用 reducer、更新 Zustand store、处理状态对比
|
||||
|
||||
```typescript
|
||||
// Dispatch Method 示例
|
||||
internal_dispatchTopic: (payload, action) => {
|
||||
const nextTopics = topicReducer(topicSelectors.currentTopics(get()), payload);
|
||||
const nextMap = { ...get().topicMaps, [get().activeId]: nextTopics };
|
||||
|
||||
if (isEqual(nextMap, get().topicMaps)) return;
|
||||
|
||||
set({ topicMaps: nextMap }, false, action ?? n(`dispatchTopic/${payload.type}`));
|
||||
},
|
||||
```
|
||||
|
||||
## 何时使用 Reducer 模式 vs. 简单 `set`
|
||||
|
||||
### 使用 Reducer 模式的场景
|
||||
|
||||
**适用于复杂的数据结构管理**,特别是:
|
||||
- 管理对象列表或映射(如 `messagesMap`, `topicMaps`)
|
||||
- 需要乐观更新的场景
|
||||
- 状态转换逻辑复杂
|
||||
- 需要类型安全的 action payload
|
||||
|
||||
```typescript
|
||||
// Reducer 模式示例 - 复杂消息状态管理
|
||||
export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
|
||||
switch (payload.type) {
|
||||
case 'updateMessage': {
|
||||
return produce(state, (draftState) => {
|
||||
const index = draftState.findIndex((i) => i.id === payload.id);
|
||||
if (index < 0) return;
|
||||
draftState[index] = merge(draftState[index], {
|
||||
...payload.value,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
});
|
||||
}
|
||||
case 'createMessage': {
|
||||
return produce(state, (draftState) => {
|
||||
draftState.push({
|
||||
...payload.value,
|
||||
id: payload.id,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
meta: {}
|
||||
});
|
||||
});
|
||||
}
|
||||
// ...其他复杂状态转换
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 使用简单 `set` 的场景
|
||||
|
||||
**适用于简单状态更新**:
|
||||
- 切换布尔值
|
||||
- 更新简单字符串/数字
|
||||
- 设置单一状态字段
|
||||
|
||||
```typescript
|
||||
// 简单 set 示例
|
||||
updateInputMessage: (message) => {
|
||||
if (isEqual(message, get().inputMessage)) return;
|
||||
set({ inputMessage: message }, false, n('updateInputMessage'));
|
||||
},
|
||||
|
||||
togglePortal: (open?: boolean) => {
|
||||
set({ showPortal: open ?? !get().showPortal }, false, 'togglePortal');
|
||||
},
|
||||
```
|
||||
|
||||
## 乐观更新实现模式
|
||||
|
||||
乐观更新是 LobeChat 中的核心模式,用于提供流畅的用户体验:
|
||||
|
||||
### 标准乐观更新流程
|
||||
|
||||
```typescript
|
||||
// 完整的乐观更新示例
|
||||
internal_updateMessageContent: async (id, content, extra) => {
|
||||
const { internal_dispatchMessage, refreshMessages } = get();
|
||||
|
||||
// 1. 立即更新前端状态(乐观更新)
|
||||
internal_dispatchMessage({
|
||||
id,
|
||||
type: 'updateMessage',
|
||||
value: { content },
|
||||
});
|
||||
|
||||
// 2. 调用后端服务
|
||||
await messageService.updateMessage(id, {
|
||||
content,
|
||||
tools: extra?.toolCalls ? internal_transformToolCalls(extra.toolCalls) : undefined,
|
||||
// ...其他字段
|
||||
});
|
||||
|
||||
// 3. 刷新确保数据一致性
|
||||
await refreshMessages();
|
||||
},
|
||||
```
|
||||
|
||||
### 创建操作的乐观更新
|
||||
|
||||
```typescript
|
||||
internal_createMessage: async (message, context) => {
|
||||
const { internal_createTmpMessage, refreshMessages, internal_toggleMessageLoading } = get();
|
||||
|
||||
let tempId = context?.tempMessageId;
|
||||
if (!tempId) {
|
||||
// 创建临时消息用于乐观更新
|
||||
tempId = internal_createTmpMessage(message);
|
||||
internal_toggleMessageLoading(true, tempId);
|
||||
}
|
||||
|
||||
try {
|
||||
const id = await messageService.createMessage(message);
|
||||
if (!context?.skipRefresh) {
|
||||
await refreshMessages();
|
||||
}
|
||||
internal_toggleMessageLoading(false, tempId);
|
||||
return id;
|
||||
} catch (e) {
|
||||
internal_toggleMessageLoading(false, tempId);
|
||||
// 错误处理:更新消息错误状态
|
||||
internal_dispatchMessage({
|
||||
id: tempId,
|
||||
type: 'updateMessage',
|
||||
value: { error: { type: ChatErrorType.CreateMessageError, message: e.message } },
|
||||
});
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
## 加载状态管理模式
|
||||
|
||||
LobeChat 使用统一的加载状态管理模式:
|
||||
|
||||
### 数组式加载状态
|
||||
|
||||
```typescript
|
||||
// 在 initialState.ts 中定义
|
||||
export interface ChatMessageState {
|
||||
messageLoadingIds: string[]; // 消息加载状态
|
||||
messageEditingIds: string[]; // 消息编辑状态
|
||||
chatLoadingIds: string[]; // 对话生成状态
|
||||
}
|
||||
|
||||
// 在 action 中管理
|
||||
internal_toggleMessageLoading: (loading, id) => {
|
||||
set({
|
||||
messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
|
||||
}, false, `internal_toggleMessageLoading/${loading ? 'start' : 'end'}`);
|
||||
},
|
||||
```
|
||||
|
||||
### 统一的加载状态工具
|
||||
|
||||
```typescript
|
||||
// 通用的加载状态切换工具
|
||||
internal_toggleLoadingArrays: (key, loading, id, action) => {
|
||||
const abortControllerKey = `${key}AbortController`;
|
||||
|
||||
if (loading) {
|
||||
const abortController = new AbortController();
|
||||
set({
|
||||
[abortControllerKey]: abortController,
|
||||
[key]: toggleBooleanList(get()[key] as string[], id!, loading),
|
||||
}, false, action);
|
||||
return abortController;
|
||||
} else {
|
||||
set({
|
||||
[abortControllerKey]: undefined,
|
||||
[key]: id ? toggleBooleanList(get()[key] as string[], id, loading) : [],
|
||||
}, false, action);
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
## SWR 集成模式
|
||||
|
||||
LobeChat 使用 SWR 进行数据获取和缓存管理:
|
||||
|
||||
### Hook 式数据获取
|
||||
|
||||
```typescript
|
||||
// 在 action.ts 中定义 SWR hook
|
||||
useFetchMessages: (enable, sessionId, activeTopicId) =>
|
||||
useClientDataSWR<ChatMessage[]>(
|
||||
enable ? [SWR_USE_FETCH_MESSAGES, sessionId, activeTopicId] : null,
|
||||
async ([, sessionId, topicId]) => messageService.getMessages(sessionId, topicId),
|
||||
{
|
||||
onSuccess: (messages, key) => {
|
||||
const nextMap = {
|
||||
...get().messagesMap,
|
||||
[messageMapKey(sessionId, activeTopicId)]: messages,
|
||||
};
|
||||
|
||||
if (get().messagesInit && isEqual(nextMap, get().messagesMap)) return;
|
||||
|
||||
set({ messagesInit: true, messagesMap: nextMap }, false, n('useFetchMessages'));
|
||||
},
|
||||
},
|
||||
),
|
||||
```
|
||||
|
||||
### 缓存失效和刷新
|
||||
|
||||
```typescript
|
||||
// 刷新数据的标准模式
|
||||
refreshMessages: async () => {
|
||||
await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId]);
|
||||
},
|
||||
|
||||
refreshTopic: async () => {
|
||||
return mutate([SWR_USE_FETCH_TOPIC, get().activeId]);
|
||||
},
|
||||
```
|
||||
|
||||
## 命名规范总结
|
||||
|
||||
### Action 命名模式
|
||||
- **Public Actions**: 动词形式,描述用户意图
|
||||
- `createTopic`, `sendMessage`, `regenerateMessage`
|
||||
- **Internal Actions**: `internal_` + 动词,描述内部操作
|
||||
- `internal_createTopic`, `internal_updateMessageContent`
|
||||
- **Dispatch Methods**: `internal_dispatch` + 实体名
|
||||
- `internal_dispatchTopic`, `internal_dispatchMessage`
|
||||
- **Toggle Methods**: `internal_toggle` + 状态名
|
||||
- `internal_toggleMessageLoading`, `internal_toggleChatLoading`
|
||||
|
||||
### 状态命名模式
|
||||
- **ID 数组**: `[entity]LoadingIds`, `[entity]EditingIds`
|
||||
- **映射结构**: `[entity]Maps`, `[entity]Map`
|
||||
- **当前激活**: `active[Entity]Id`
|
||||
- **初始化标记**: `[entity]sInit`
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **始终实现乐观更新**:对于用户交互频繁的操作
|
||||
2. **加载状态管理**:使用统一的加载状态数组管理并发操作
|
||||
3. **类型安全**:为所有 action payload 定义 TypeScript 接口
|
||||
4. **SWR 集成**:使用 SWR 管理数据获取和缓存失效
|
||||
5. **AbortController**:为长时间运行的操作提供取消能力
|
||||
|
||||
这套 Action 组织模式确保了代码的一致性、可维护性,并提供了优秀的用户体验。
|
||||
@@ -0,0 +1,300 @@
|
||||
---
|
||||
description:
|
||||
globs: src/store/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# LobeChat Zustand Store Slice 组织架构
|
||||
|
||||
本文档描述了 LobeChat 项目中 Zustand Store 的模块化 Slice 组织方式,展示如何通过分片架构管理复杂的应用状态。
|
||||
|
||||
## 顶层 Store 结构
|
||||
|
||||
LobeChat 的 `chat` store (`src/store/chat/`) 采用模块化的 slice 结构来组织状态和逻辑。
|
||||
|
||||
### 关键聚合文件
|
||||
|
||||
- **`src/store/chat/initialState.ts`**: 聚合所有 slice 的初始状态
|
||||
- **`src/store/chat/store.ts`**: 定义顶层的 `ChatStore`,组合所有 slice 的 actions
|
||||
- **`src/store/chat/selectors.ts`**: 统一导出所有 slice 的 selectors
|
||||
- **`src/store/chat/helpers.ts`**: 提供聊天相关的辅助函数
|
||||
|
||||
### Store 聚合模式
|
||||
|
||||
```typescript
|
||||
// src/store/chat/initialState.ts
|
||||
import { ChatTopicState, initialTopicState } from './slices/topic/initialState';
|
||||
import { ChatMessageState, initialMessageState } from './slices/message/initialState';
|
||||
import { ChatAIChatState, initialAiChatState } from './slices/aiChat/initialState';
|
||||
|
||||
export type ChatStoreState = ChatTopicState &
|
||||
ChatMessageState &
|
||||
ChatAIChatState &
|
||||
// ...其他 slice states
|
||||
|
||||
export const initialState: ChatStoreState = {
|
||||
...initialMessageState,
|
||||
...initialTopicState,
|
||||
...initialAiChatState,
|
||||
// ...其他 initial slice states
|
||||
};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/store/chat/store.ts
|
||||
import { ChatMessageAction, chatMessage } from './slices/message/action';
|
||||
import { ChatTopicAction, chatTopic } from './slices/topic/action';
|
||||
import { ChatAIChatAction, chatAiChat } from './slices/aiChat/actions';
|
||||
|
||||
export interface ChatStoreAction
|
||||
extends ChatMessageAction,
|
||||
ChatTopicAction,
|
||||
ChatAIChatAction,
|
||||
// ...其他 slice actions
|
||||
|
||||
const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...params) => ({
|
||||
...initialState,
|
||||
...chatMessage(...params),
|
||||
...chatTopic(...params),
|
||||
...chatAiChat(...params),
|
||||
// ...其他 slice action creators
|
||||
});
|
||||
|
||||
export const useChatStore = createWithEqualityFn<ChatStore>()(
|
||||
subscribeWithSelector(devtools(createStore)),
|
||||
shallow,
|
||||
);
|
||||
```
|
||||
|
||||
## 单个 Slice 的标准结构
|
||||
|
||||
每个 slice 位于 `src/store/chat/slices/[sliceName]/` 目录下:
|
||||
|
||||
```
|
||||
src/store/chat/slices/
|
||||
└── [sliceName]/ # 例如 message, topic, aiChat, builtinTool
|
||||
├── action.ts # 定义 actions (或者是一个 actions/ 目录)
|
||||
├── initialState.ts # 定义 state 结构和初始值
|
||||
├── reducer.ts # (可选) 如果使用 reducer 模式
|
||||
├── selectors.ts # 定义 selectors
|
||||
└── index.ts # (可选) 重新导出模块内容
|
||||
```
|
||||
|
||||
### 文件职责说明
|
||||
|
||||
1. **`initialState.ts`**:
|
||||
- 定义 slice 的 TypeScript 状态接口
|
||||
- 提供初始状态默认值
|
||||
|
||||
```typescript
|
||||
// 典型的 initialState.ts 结构
|
||||
export interface ChatTopicState {
|
||||
activeTopicId?: string;
|
||||
topicMaps: Record<string, ChatTopic[]>; // 核心数据结构
|
||||
topicsInit: boolean;
|
||||
topicLoadingIds: string[];
|
||||
// ...其他状态字段
|
||||
}
|
||||
|
||||
export const initialTopicState: ChatTopicState = {
|
||||
activeTopicId: undefined,
|
||||
topicMaps: {},
|
||||
topicsInit: false,
|
||||
topicLoadingIds: [],
|
||||
// ...其他初始值
|
||||
};
|
||||
```
|
||||
|
||||
2. **`reducer.ts`** (复杂状态使用):
|
||||
- 定义纯函数 reducer,处理同步状态转换
|
||||
- 使用 `immer` 确保不可变更新
|
||||
|
||||
```typescript
|
||||
// 典型的 reducer.ts 结构
|
||||
import { produce } from 'immer';
|
||||
|
||||
interface AddChatTopicAction {
|
||||
type: 'addTopic';
|
||||
value: CreateTopicParams & { id?: string };
|
||||
}
|
||||
|
||||
interface UpdateChatTopicAction {
|
||||
id: string;
|
||||
type: 'updateTopic';
|
||||
value: Partial<ChatTopic>;
|
||||
}
|
||||
|
||||
export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction;
|
||||
|
||||
export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch): ChatTopic[] => {
|
||||
switch (payload.type) {
|
||||
case 'addTopic': {
|
||||
return produce(state, (draftState) => {
|
||||
draftState.unshift({
|
||||
...payload.value,
|
||||
id: payload.value.id ?? Date.now().toString(),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
});
|
||||
}
|
||||
case 'updateTopic': {
|
||||
return produce(state, (draftState) => {
|
||||
const index = draftState.findIndex((topic) => topic.id === payload.id);
|
||||
if (index !== -1) {
|
||||
draftState[index] = { ...draftState[index], ...payload.value };
|
||||
}
|
||||
});
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. **`selectors.ts`**:
|
||||
- 提供状态查询和计算函数
|
||||
- 供 UI 组件使用的状态订阅接口
|
||||
- **重要**: 使用 `export const xxxSelectors` 模式聚合所有 selectors
|
||||
|
||||
```typescript
|
||||
// 典型的 selectors.ts 结构
|
||||
import { ChatStoreState } from '../../initialState';
|
||||
|
||||
const currentTopics = (s: ChatStoreState): ChatTopic[] | undefined =>
|
||||
s.topicMaps[s.activeId];
|
||||
|
||||
const currentActiveTopic = (s: ChatStoreState): ChatTopic | undefined => {
|
||||
return currentTopics(s)?.find((topic) => topic.id === s.activeTopicId);
|
||||
};
|
||||
|
||||
const getTopicById = (id: string) => (s: ChatStoreState): ChatTopic | undefined =>
|
||||
currentTopics(s)?.find((topic) => topic.id === id);
|
||||
|
||||
// 核心模式:使用 xxxSelectors 聚合导出
|
||||
export const topicSelectors = {
|
||||
currentActiveTopic,
|
||||
currentTopics,
|
||||
getTopicById,
|
||||
// ...其他 selectors
|
||||
};
|
||||
```
|
||||
|
||||
## 特殊 Slice 组织模式
|
||||
|
||||
### 复杂 Actions 的子目录结构 (aiChat Slice)
|
||||
|
||||
当 slice 的 actions 过于复杂时,可以拆分到子目录:
|
||||
|
||||
```
|
||||
src/store/chat/slices/aiChat/
|
||||
├── actions/
|
||||
│ ├── generateAIChat.ts # AI 对话生成
|
||||
│ ├── rag.ts # RAG 检索增强生成
|
||||
│ ├── memory.ts # 对话记忆管理
|
||||
│ └── index.ts # 聚合所有 actions
|
||||
├── initialState.ts
|
||||
├── selectors.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
参考:`src/store/chat/slices/aiChat/actions/`
|
||||
|
||||
### 工具类 Slice (builtinTool)
|
||||
|
||||
管理多种内置工具的状态:
|
||||
|
||||
```
|
||||
src/store/chat/slices/builtinTool/
|
||||
├── actions/
|
||||
│ ├── dalle.ts # DALL-E 图像生成
|
||||
│ ├── search.ts # 搜索功能
|
||||
│ ├── localFile.ts # 本地文件操作
|
||||
│ └── index.ts
|
||||
├── initialState.ts
|
||||
├── selectors.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
参考:`src/store/chat/slices/builtinTool/`
|
||||
|
||||
## 状态设计模式
|
||||
|
||||
### 1. Map 结构用于关联数据
|
||||
```typescript
|
||||
// 以 sessionId 为 key,管理多个会话的数据
|
||||
topicMaps: Record<string, ChatTopic[]>
|
||||
messagesMap: Record<string, ChatMessage[]>
|
||||
```
|
||||
|
||||
### 2. 数组用于加载状态管理
|
||||
```typescript
|
||||
// 管理多个并发操作的加载状态
|
||||
messageLoadingIds: string[]
|
||||
topicLoadingIds: string[]
|
||||
chatLoadingIds: string[]
|
||||
```
|
||||
|
||||
### 3. 可选字段用于当前活动项
|
||||
```typescript
|
||||
// 当前激活的实体 ID
|
||||
activeId: string
|
||||
activeTopicId?: string
|
||||
activeThreadId?: string
|
||||
```
|
||||
|
||||
## Slice 集成到顶层 Store
|
||||
|
||||
### 1. 状态聚合
|
||||
```typescript
|
||||
// 在 initialState.ts 中
|
||||
export type ChatStoreState = ChatTopicState &
|
||||
ChatMessageState &
|
||||
ChatAIChatState &
|
||||
// ...其他 slice states
|
||||
```
|
||||
|
||||
### 2. Action 接口聚合
|
||||
```typescript
|
||||
// 在 store.ts 中
|
||||
export interface ChatStoreAction
|
||||
extends ChatMessageAction,
|
||||
ChatTopicAction,
|
||||
ChatAIChatAction,
|
||||
// ...其他 slice actions
|
||||
```
|
||||
|
||||
### 3. Selector 统一导出
|
||||
```typescript
|
||||
// 在 selectors.ts 中 - 统一聚合 selectors
|
||||
export { chatSelectors } from './slices/message/selectors';
|
||||
export { topicSelectors } from './slices/topic/selectors';
|
||||
export { aiChatSelectors } from './slices/aiChat/selectors';
|
||||
|
||||
// 每个 slice 的 selectors.ts 都使用 xxxSelectors 模式:
|
||||
// export const chatSelectors = { ... }
|
||||
// export const topicSelectors = { ... }
|
||||
// export const aiChatSelectors = { ... }
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **Slice 划分原则**:
|
||||
- 按功能领域划分(message, topic, aiChat 等)
|
||||
- 每个 slice 管理相关的状态和操作
|
||||
- 避免 slice 之间的强耦合
|
||||
|
||||
2. **文件命名规范**:
|
||||
- 使用小驼峰命名 slice 目录
|
||||
- 文件名使用一致的模式(action.ts, selectors.ts 等)
|
||||
- 复杂 actions 时使用 actions/ 子目录
|
||||
|
||||
3. **状态结构设计**:
|
||||
- 扁平化的状态结构,避免深层嵌套
|
||||
- 使用 Map 结构管理列表数据
|
||||
- 分离加载状态和业务数据
|
||||
|
||||
4. **类型安全**:
|
||||
- 为每个 slice 定义清晰的 TypeScript 接口
|
||||
- 使用 Zustand 的 StateCreator 确保类型一致性
|
||||
- 在顶层聚合时保持类型安全
|
||||
|
||||
这种模块化的 slice 组织方式使得大型应用的状态管理变得清晰、可维护,并且易于扩展。
|
||||
Reference in New Issue
Block a user