mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
parent
5ed88d7947
commit
3c4ef8a837
20 changed files with 1000 additions and 476 deletions
|
|
@ -5,20 +5,26 @@ alwaysApply: false
|
|||
|
||||
# Database Migrations Guide
|
||||
|
||||
## Step1: Generate migrations:
|
||||
## Step1: Generate migrations
|
||||
|
||||
```bash
|
||||
bun run db:generate
|
||||
```
|
||||
|
||||
this step will generate or update the following files:
|
||||
this step will generate following files:
|
||||
|
||||
- packages/database/migrations/0046_meaningless_file_name.sql
|
||||
- packages/database/migrations/0046_meaningless_file_name.sql
|
||||
|
||||
and update the following files:
|
||||
|
||||
- packages/database/migrations/0046_xxx.sql
|
||||
- packages/database/migrations/meta/\_journal.json
|
||||
- packages/database/src/core/migrations.json
|
||||
- docs/development/database-schema.dbml
|
||||
|
||||
## Step2: optimize the migration sql fileName
|
||||
|
||||
the migration sql file name is randomly generated, we need to optimize the file name to make it more readable and meaningful. For example, `0046_xxx.sql` -> `0046_better_auth.sql`
|
||||
the migration sql file name is randomly generated, we need to optimize the file name to make it more readable and meaningful. For example, `0046_meaningless_file_name.sql` -> `0046_user_add_avatar_column.sql`
|
||||
|
||||
## Step3: Defensive Programming - Use Idempotent Clauses
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
---
|
||||
description:
|
||||
description:
|
||||
globs: src/database/schemas/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Drizzle ORM Schema Style Guide for lobe-chat
|
||||
|
||||
This document outlines the conventions and best practices for defining PostgreSQL Drizzle ORM schemas within the lobe-chat project.
|
||||
|
|
@ -16,7 +17,8 @@ This document outlines the conventions and best practices for defining PostgreSQ
|
|||
|
||||
## Helper Functions
|
||||
|
||||
Commonly used column definitions, especially for timestamps, are centralized in [src/database/schemas/_helpers.ts](mdc:src/database/schemas/_helpers.ts):
|
||||
Commonly used column definitions, especially for timestamps, are centralized in [src/database/schemas/\_helpers.ts](mdc:src/database/schemas/_helpers.ts):
|
||||
|
||||
- `timestamptz(name: string)`: Creates a timestamp column with timezone
|
||||
- `createdAt()`, `updatedAt()`, `accessedAt()`: Helper functions for standard timestamp columns
|
||||
- `timestamps`: An object `{ createdAt, updatedAt, accessedAt }` for easy inclusion in table definitions
|
||||
|
|
@ -29,6 +31,7 @@ Commonly used column definitions, especially for timestamps, are centralized in
|
|||
## Column Definitions
|
||||
|
||||
### Primary Keys (PKs)
|
||||
|
||||
- Typically `text('id')` (or `varchar('id')` for some OIDC tables)
|
||||
- Often use `.$defaultFn(() => idGenerator('table_name'))` for automatic ID generation with meaningful prefixes
|
||||
- **ID Prefix Purpose**: Makes it easy for users and developers to distinguish different entity types at a glance
|
||||
|
|
@ -36,24 +39,29 @@ Commonly used column definitions, especially for timestamps, are centralized in
|
|||
- Composite PKs are defined using `primaryKey({ columns: [t.colA, t.colB] })`
|
||||
|
||||
### Foreign Keys (FKs)
|
||||
|
||||
- Defined using `.references(() => otherTable.id, { onDelete: 'cascade' | 'set null' | 'no action' })`
|
||||
- FK columns are usually named `related_table_singular_name_id` (e.g., `user_id` references `users.id`)
|
||||
- Most tables include a `user_id` column referencing `users.id` with `onDelete: 'cascade'`
|
||||
|
||||
### Timestamps
|
||||
- Consistently use the `...timestamps` spread from [_helpers.ts](mdc:src/database/schemas/_helpers.ts) for `created_at`, `updated_at`, and `accessed_at` columns
|
||||
|
||||
- Consistently use the `...timestamps` spread from [\_helpers.ts](mdc:src/database/schemas/_helpers.ts) for `created_at`, `updated_at`, and `accessed_at` columns
|
||||
|
||||
### Default Values
|
||||
|
||||
- `.$defaultFn(() => expression)` for dynamic defaults (e.g., `idGenerator()`, `randomSlug()`)
|
||||
- `.default(staticValue)` for static defaults (e.g., `boolean('enabled').default(true)`)
|
||||
|
||||
### Indexes
|
||||
|
||||
- Defined in the table's second argument: `pgTable('name', {...columns}, (t) => ({ indexName: indexType().on(...) }))`
|
||||
- Use `uniqueIndex()` for unique constraints and `index()` for non-unique indexes
|
||||
- Naming pattern: `table_name_column(s)_idx` or `table_name_column(s)_unique`
|
||||
- Many tables feature a `clientId: text('client_id')` column, often part of a composite unique index with `user_id`
|
||||
|
||||
### Data Types
|
||||
|
||||
- Common types: `text`, `varchar`, `jsonb`, `boolean`, `integer`, `uuid`, `pgTable`
|
||||
- For `jsonb` fields, specify the TypeScript type using `.$type<MyType>()` for better type safety
|
||||
|
||||
|
|
@ -97,9 +105,7 @@ export const agents = pgTable(
|
|||
...timestamps,
|
||||
},
|
||||
// return array instead of object, the object style is deprecated
|
||||
(t) => [
|
||||
uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId),
|
||||
],
|
||||
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
|
||||
);
|
||||
|
||||
export const insertAgentSchema = createInsertSchema(agents);
|
||||
|
|
@ -110,6 +116,7 @@ export type AgentItem = typeof agents.$inferSelect;
|
|||
## Common Patterns
|
||||
|
||||
### 1. userId + clientId Pattern (Legacy)
|
||||
|
||||
Some existing tables include both fields for different purposes:
|
||||
|
||||
```typescript
|
||||
|
|
@ -129,6 +136,7 @@ clientIdUnique: uniqueIndex('agents_client_id_user_id_unique').on(t.clientId, t.
|
|||
- **Note**: This pattern is being phased out for new features to simplify the schema
|
||||
|
||||
### 2. Junction Tables (Many-to-Many Relationships)
|
||||
|
||||
Use composite primary keys for relationship tables:
|
||||
|
||||
```typescript
|
||||
|
|
@ -136,21 +144,26 @@ Use composite primary keys for relationship tables:
|
|||
export const agentsKnowledgeBases = pgTable(
|
||||
'agents_knowledge_bases',
|
||||
{
|
||||
agentId: text('agent_id').references(() => agents.id, { onDelete: 'cascade' }).notNull(),
|
||||
knowledgeBaseId: text('knowledge_base_id').references(() => knowledgeBases.id, { onDelete: 'cascade' }).notNull(),
|
||||
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||
agentId: text('agent_id')
|
||||
.references(() => agents.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
knowledgeBaseId: text('knowledge_base_id')
|
||||
.references(() => knowledgeBases.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
enabled: boolean('enabled').default(true),
|
||||
...timestamps,
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.agentId, t.knowledgeBaseId] }),
|
||||
],
|
||||
(t) => [primaryKey({ columns: [t.agentId, t.knowledgeBaseId] })],
|
||||
);
|
||||
```
|
||||
|
||||
**Pattern**: `{entity1}Id` + `{entity2}Id` as composite PK, plus `userId` for ownership
|
||||
|
||||
### 3. OIDC Tables Special Patterns
|
||||
|
||||
OIDC tables use `varchar` IDs instead of `text` with custom generators:
|
||||
|
||||
```typescript
|
||||
|
|
@ -166,6 +179,7 @@ export const oidcAuthorizationCodes = pgTable('oidc_authorization_codes', {
|
|||
**Reason**: OIDC standards expect specific ID formats and lengths
|
||||
|
||||
### 4. File Processing with Async Tasks
|
||||
|
||||
File-related tables reference async task IDs for background processing:
|
||||
|
||||
```typescript
|
||||
|
|
@ -173,17 +187,21 @@ File-related tables reference async task IDs for background processing:
|
|||
export const files = pgTable('files', {
|
||||
// ... other fields
|
||||
chunkTaskId: uuid('chunk_task_id').references(() => asyncTasks.id, { onDelete: 'set null' }),
|
||||
embeddingTaskId: uuid('embedding_task_id').references(() => asyncTasks.id, { onDelete: 'set null' }),
|
||||
embeddingTaskId: uuid('embedding_task_id').references(() => asyncTasks.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
**Purpose**:
|
||||
**Purpose**:
|
||||
|
||||
- Track file chunking progress (breaking files into smaller pieces)
|
||||
- Track embedding generation progress (converting text to vectors)
|
||||
- Allow querying task status and handling failures
|
||||
|
||||
### 5. Slug Pattern (Legacy)
|
||||
|
||||
Some entities include auto-generated slugs - this is legacy code:
|
||||
|
||||
```typescript
|
||||
|
|
@ -195,8 +213,6 @@ slug: varchar('slug', { length: 100 })
|
|||
slugUserIdUnique: uniqueIndex('slug_user_id_unique').on(t.slug, t.userId),
|
||||
```
|
||||
|
||||
**Current usage**: Only used to identify default agents/sessions (legacy pattern)
|
||||
**Future refactor**: Will likely be replaced with `isDefault: boolean()` field
|
||||
**Note**: Avoid using slugs for new features - prefer explicit boolean flags for status tracking
|
||||
**Current usage**: Only used to identify default agents/sessions (legacy pattern) **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.
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
description: Explain how group chat works in LobeHub (Multi-agent orchestratoin)
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
This rule explains how group chat (multi-agent orchestration) works. Not confused with session group, which is a organization method to manage session.
|
||||
|
||||
## Key points
|
||||
|
||||
- A supervisor will devide who and how will speak next
|
||||
- Each agent will speak just like in single chat (if was asked to speak)
|
||||
- Not coufused with session group
|
||||
|
||||
## Related Files
|
||||
|
||||
- src/store/chat/slices/message/supervisor.ts
|
||||
- src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts
|
||||
- src/prompts/groupChat/index.ts (All prompts here)
|
||||
|
||||
## Snippets
|
||||
|
||||
```tsx
|
||||
// Detect whether in group chat
|
||||
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
||||
|
||||
// Member actions
|
||||
const addAgentsToGroup = useChatGroupStore((s) => s.addAgentsToGroup);
|
||||
const removeAgentFromGroup = useChatGroupStore((s) => s.removeAgentFromGroup);
|
||||
const persistReorder = useChatGroupStore((s) => s.reorderGroupMembers);
|
||||
|
||||
// Get group info
|
||||
const groupConfig = useChatGroupStore(chatGroupSelectors.currentGroupConfig);
|
||||
const currentGroupMemebers = useSessionStore(sessionSelectors.currentGroupAgents);
|
||||
```
|
||||
161
.cursor/rules/hotkey.mdc
Normal file
161
.cursor/rules/hotkey.mdc
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
alwaysApply: false
|
||||
---
|
||||
# 如何添加新的快捷键:开发者指南
|
||||
|
||||
本指南将带您一步步地向 LobeChat 添加一个新的快捷键功能。我们将通过一个完整示例,演示从定义到实现的整个过程。
|
||||
|
||||
## 示例场景
|
||||
|
||||
假设我们要添加一个新的快捷键功能:**快速清空聊天记录**,快捷键为 `Mod+Shift+Backspace`。
|
||||
|
||||
## 步骤 1:更新快捷键常量定义
|
||||
|
||||
首先,在 `src/types/hotkey.ts` 中更新 `HotkeyEnum`:
|
||||
|
||||
```typescript
|
||||
export const HotkeyEnum = {
|
||||
// 已有的快捷键...
|
||||
AddUserMessage: 'addUserMessage',
|
||||
EditMessage: 'editMessage',
|
||||
|
||||
// 新增快捷键
|
||||
ClearChat: 'clearChat', // 添加这一行
|
||||
|
||||
// 其他已有快捷键...
|
||||
} as const;
|
||||
```
|
||||
|
||||
## 步骤 2:注册默认快捷键
|
||||
|
||||
在 `src/const/hotkeys.ts` 中添加快捷键的默认配置:
|
||||
|
||||
```typescript
|
||||
import { KeyMapEnum as Key, combineKeys } from '@lobehub/ui';
|
||||
|
||||
// ...现有代码
|
||||
|
||||
export const HOTKEYS_REGISTRATION: HotkeyRegistration = [
|
||||
// 现有的快捷键配置...
|
||||
|
||||
// 添加新的快捷键配置
|
||||
{
|
||||
group: HotkeyGroupEnum.Conversation, // 归类到会话操作组
|
||||
id: HotkeyEnum.ClearChat,
|
||||
keys: combineKeys([Key.Mod, Key.Shift, Key.Backspace]),
|
||||
scopes: [HotkeyScopeEnum.Chat], // 在聊天作用域下生效
|
||||
},
|
||||
|
||||
// 其他现有快捷键...
|
||||
];
|
||||
```
|
||||
|
||||
## 步骤 3:添加国际化翻译
|
||||
|
||||
在 `src/locales/default/hotkey.ts` 中添加对应的文本描述:
|
||||
|
||||
```typescript
|
||||
import { HotkeyI18nTranslations } from '@/types/hotkey';
|
||||
|
||||
const hotkey: HotkeyI18nTranslations = {
|
||||
// 现有翻译...
|
||||
|
||||
// 添加新快捷键的翻译
|
||||
clearChat: {
|
||||
desc: '清空当前会话的所有消息记录',
|
||||
title: '清空聊天记录',
|
||||
},
|
||||
|
||||
// 其他现有翻译...
|
||||
};
|
||||
|
||||
export default hotkey;
|
||||
```
|
||||
|
||||
如需支持其他语言,还需要在相应的语言文件中添加对应翻译。
|
||||
|
||||
## 步骤 4:创建并注册快捷键 Hook
|
||||
|
||||
在 `src/hooks/useHotkeys/chatScope.ts` 中添加新的 Hook:
|
||||
|
||||
```typescript
|
||||
export const useClearChatHotkey = () => {
|
||||
const clearMessages = useChatStore((s) => s.clearMessages);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useHotkeyById(HotkeyEnum.ClearChat, showConfirm);
|
||||
};
|
||||
|
||||
// 注册聚合
|
||||
|
||||
export const useRegisterChatHotkeys = () => {
|
||||
const { enableScope, disableScope } = useHotkeysContext();
|
||||
|
||||
useOpenChatSettingsHotkey();
|
||||
// ...其他快捷键
|
||||
useClearChatHotkey();
|
||||
|
||||
useEffect(() => {
|
||||
enableScope(HotkeyScopeEnum.Chat);
|
||||
return () => disableScope(HotkeyScopeEnum.Chat);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
```
|
||||
|
||||
## 步骤 5:给相应 UI 元素添加 Tooltip 提示(可选)
|
||||
|
||||
如果有对应的 UI 按钮,可以添加快捷键提示:
|
||||
|
||||
```tsx
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from '@lobehub/ui';
|
||||
import { Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { HotkeyEnum } from '@/types/hotkey';
|
||||
|
||||
const ClearChatButton = () => {
|
||||
const { t } = useTranslation(['hotkey', 'chat']);
|
||||
const clearChatHotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ClearChat));
|
||||
|
||||
// 获取清空聊天的方法
|
||||
const clearMessages = useChatStore((s) => s.clearMessages);
|
||||
|
||||
return (
|
||||
<Tooltip hotkey={clearChatHotkey} title={t('clearChat.title', { ns: 'hotkey' })}>
|
||||
<Button icon={<DeleteOutlined />} onClick={clearMessages} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 步骤 6:测试新快捷键
|
||||
|
||||
1. 启动开发服务器
|
||||
2. 打开聊天页面
|
||||
3. 按下设置的快捷键组合(`Cmd+Shift+Backspace` 或 `Ctrl+Shift+Backspace`)
|
||||
4. 确认功能正常工作
|
||||
5. 检查快捷键设置面板中是否正确显示了新快捷键
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **作用域考虑**:根据功能决定快捷键应属于全局作用域还是聊天作用域
|
||||
2. **分组合理**:将快捷键放在合适的功能组中(System/Layout/Conversation)
|
||||
3. **冲突检查**:确保新快捷键不会与现有系统、浏览器或应用快捷键冲突
|
||||
4. **平台适配**:使用 `Key.Mod` 而非硬编码 `Ctrl` 或 `Cmd`,以适配不同平台
|
||||
5. **提供清晰描述**:为快捷键添加明确的标题和描述,帮助用户理解功能
|
||||
|
||||
按照以上步骤,您可以轻松地向系统添加新的快捷键功能,提升用户体验。如有特殊需求,如桌面专属快捷键,可以通过 `isDesktop` 标记进行区分处理。
|
||||
|
||||
## 常见问题排查
|
||||
|
||||
- **快捷键未生效**:检查作用域是否正确,以及是否在 RegisterHotkeys 中调用了对应的 hook
|
||||
- **快捷键设置面板未显示**:确认在 HOTKEYS_REGISTRATION 中正确配置了快捷键
|
||||
- **快捷键冲突**:在 HotkeyInput 组件中可以检测到冲突,用户会看到警告
|
||||
- **功能在某些页面失效**:确认是否注册在了正确的作用域,以及相关页面是否激活了该作用域
|
||||
|
||||
通过这些步骤,您可以确保新添加的快捷键功能稳定、可靠且用户友好。
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
globs: *.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# LobeChat Internationalization Guide
|
||||
|
||||
## Key Points
|
||||
|
|
@ -14,7 +15,7 @@ alwaysApply: false
|
|||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
```plaintext
|
||||
src/locales/
|
||||
├── default/ # Source language files (zh-CN)
|
||||
│ ├── index.ts # Namespace exports
|
||||
|
|
@ -176,7 +177,3 @@ export default {
|
|||
- Check if the key exists in src/locales/default/namespace.ts
|
||||
- Ensure the namespace is correctly imported in the component
|
||||
- Ensure new namespaces are exported in src/locales/default/index.ts
|
||||
|
||||
- 检查键是否存在于 src/locales/default/namespace.ts 中
|
||||
- 确保在组件中正确导入命名空间
|
||||
- 确保新命名空间已在 src/locales/default/index.ts 中导出
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ logo emoji: 🤯
|
|||
## Project Technologies Stack
|
||||
|
||||
- Next.js 16
|
||||
- implement spa inside nextjs with `react-router-dom`
|
||||
- react 19
|
||||
- TypeScript
|
||||
- `@lobehub/ui`, antd for component framework
|
||||
|
|
@ -29,8 +30,8 @@ logo emoji: 🤯
|
|||
- SWR for data fetch
|
||||
- aHooks for react hooks library
|
||||
- dayjs for time library
|
||||
- lodash-es for utility library
|
||||
- es-toolkit for utility library
|
||||
- TRPC for type safe backend
|
||||
- PGLite for client DB and Neon PostgreSQL for backend DB
|
||||
- Neon PostgreSQL for backend DB
|
||||
- Drizzle ORM
|
||||
- Vitest for testing
|
||||
|
|
|
|||
|
|
@ -25,22 +25,23 @@ lobe-chat/
|
|||
│ └── zh-CN/
|
||||
├── packages/
|
||||
│ ├── agent-runtime/
|
||||
│ ├── builtin-agents/
|
||||
│ ├── builtin-tool-*/ # builtin tool packages
|
||||
│ ├── const/
|
||||
│ ├── context-engine/
|
||||
│ ├── conversation-flow/
|
||||
│ ├── database/
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── models/
|
||||
│ │ │ ├── schemas/
|
||||
│ │ │ └── repositories/
|
||||
│ │ └── src/
|
||||
│ │ ├── models/
|
||||
│ │ ├── schemas/
|
||||
│ │ └── repositories/
|
||||
│ ├── desktop-bridge/
|
||||
│ ├── electron-client-ipc/
|
||||
│ ├── electron-server-ipc/
|
||||
│ ├── fetch-sse/
|
||||
│ ├── file-loaders/
|
||||
│ ├── memory-extract/
|
||||
│ ├── memory-user-memory/
|
||||
│ ├── model-bank/
|
||||
│ │ └── src/
|
||||
│ │ └── aiModels/
|
||||
│ ├── model-runtime/
|
||||
│ │ └── src/
|
||||
│ │ ├── core/
|
||||
|
|
@ -50,9 +51,6 @@ lobe-chat/
|
|||
│ ├── python-interpreter/
|
||||
│ ├── ssrf-safe-fetch/
|
||||
│ ├── types/
|
||||
│ │ └── src/
|
||||
│ │ ├── message/
|
||||
│ │ └── user/
|
||||
│ ├── utils/
|
||||
│ └── web-crawler/
|
||||
├── public/
|
||||
|
|
@ -61,24 +59,25 @@ lobe-chat/
|
|||
│ ├── app/
|
||||
│ │ ├── (backend)/
|
||||
│ │ │ ├── api/
|
||||
│ │ │ │ ├── auth/
|
||||
│ │ │ │ └── webhooks/
|
||||
│ │ │ ├── f/
|
||||
│ │ │ ├── market/
|
||||
│ │ │ ├── middleware/
|
||||
│ │ │ ├── oidc/
|
||||
│ │ │ ├── trpc/
|
||||
│ │ │ └── webapi/
|
||||
│ │ │ ├── chat/
|
||||
│ │ │ └── tts/
|
||||
│ │ ├── [variants]/
|
||||
│ │ │ ├── (auth)/
|
||||
│ │ │ ├── (main)/
|
||||
│ │ │ │ ├── chat/
|
||||
│ │ │ │ └── settings/
|
||||
│ │ │ └── @modal/
|
||||
│ │ └── manifest.ts
|
||||
│ │ │ ├── (mobile)/
|
||||
│ │ │ ├── onboarding/
|
||||
│ │ │ └── router/
|
||||
│ │ └── desktop/
|
||||
│ ├── components/
|
||||
│ ├── config/
|
||||
│ ├── const/
|
||||
│ ├── envs/
|
||||
│ ├── features/
|
||||
│ │ └── ChatInput/
|
||||
│ ├── helpers/
|
||||
│ ├── hooks/
|
||||
│ ├── layout/
|
||||
│ │ ├── AuthProvider/
|
||||
|
|
@ -90,23 +89,23 @@ lobe-chat/
|
|||
│ ├── locales/
|
||||
│ │ └── default/
|
||||
│ ├── server/
|
||||
│ │ ├── featureFlags/
|
||||
│ │ ├── globalConfig/
|
||||
│ │ ├── modules/
|
||||
│ │ ├── routers/
|
||||
│ │ │ ├── async/
|
||||
│ │ │ ├── desktop/
|
||||
│ │ │ ├── edge/
|
||||
│ │ │ └── lambda/
|
||||
│ │ │ ├── lambda/
|
||||
│ │ │ ├── mobile/
|
||||
│ │ │ └── tools/
|
||||
│ │ └── services/
|
||||
│ ├── services/
|
||||
│ │ ├── user/
|
||||
│ │ │ ├── client.ts
|
||||
│ │ │ └── server.ts
|
||||
│ │ └── message/
|
||||
│ ├── store/
|
||||
│ │ ├── agent/
|
||||
│ │ ├── chat/
|
||||
│ │ └── user/
|
||||
│ ├── styles/
|
||||
│ ├── tools/
|
||||
│ ├── types/
|
||||
│ └── utils/
|
||||
└── package.json
|
||||
```
|
||||
|
|
@ -116,25 +115,22 @@ lobe-chat/
|
|||
- UI Components: `src/components`, `src/features`
|
||||
- Global providers: `src/layout`
|
||||
- Zustand stores: `src/store`
|
||||
- Client Services: `src/services/` cross-platform services
|
||||
- clientDB: `src/services/<domain>/client.ts`
|
||||
- serverDB: `src/services/<domain>/server.ts`
|
||||
- Client Services: `src/services/`
|
||||
- API Routers:
|
||||
- `src/app/(backend)/webapi` (REST)
|
||||
- `src/server/routers/{edge|lambda|async|desktop|tools}` (tRPC)
|
||||
- `src/server/routers/{async|lambda|mobile|tools}` (tRPC)
|
||||
- Server:
|
||||
- Services(can access serverDB): `src/server/services` server-used-only services
|
||||
- Modules(can't access db): `src/server/modules` (Server only Third-party Service Module)
|
||||
- Services (can access serverDB): `src/server/services`
|
||||
- Modules (can't access db): `src/server/modules`
|
||||
- Feature Flags: `src/server/featureFlags`
|
||||
- Global Config: `src/server/globalConfig`
|
||||
- Database:
|
||||
- Schema (Drizzle): `packages/database/src/schemas`
|
||||
- Model (CRUD): `packages/database/src/models`
|
||||
- Repository (bff-queries): `packages/database/src/repositories`
|
||||
- Third-party Integrations: `src/libs` — analytics, oidc etc.
|
||||
- Builtin Tools: `src/tools`, `packages/builtin-tool-*`
|
||||
|
||||
## Data Flow Architecture
|
||||
|
||||
- **Web with ClientDB**: React UI → Client Service → Direct Model Access → PGLite (Web WASM)
|
||||
- **Web with ServerDB**: React UI → Client Service → tRPC Lambda → Server Services → PostgreSQL (Remote)
|
||||
- **Desktop**:
|
||||
- Cloud sync disabled: Electron UI → Client Service → tRPC Lambda → Local Server Services → PGLite (Node WASM)
|
||||
- Cloud sync enabled: Electron UI → Client Service → tRPC Lambda → Cloud Server Services → PostgreSQL (Remote)
|
||||
React UI → Store Actions → Client Service → TRPC Lambda → Server Services -> DB Model → PostgreSQL (Remote)
|
||||
|
|
|
|||
|
|
@ -1,173 +0,0 @@
|
|||
---
|
||||
description:
|
||||
globs: *.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# react component 编写指南
|
||||
|
||||
- 如果要写复杂样式的话用 antd-style ,简单的话可以用 style 属性直接写内联样式
|
||||
- 如果需要 flex 布局或者居中布局应该使用 react-layout-kit 的 Flexbox 和 Center 组件
|
||||
- 选择组件时优先顺序应该是 src/components > 安装的组件 package > lobe-ui > antd
|
||||
- 使用 selector 访问 zustand store 的数据,而不是直接从 store 获取
|
||||
|
||||
## antd-style token system
|
||||
|
||||
### 访问 token system 的两种方式
|
||||
|
||||
#### 使用 antd-style 的 useTheme hook
|
||||
|
||||
```tsx
|
||||
import { useTheme } from 'antd-style';
|
||||
|
||||
const MyComponent = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
color: theme.colorPrimary,
|
||||
backgroundColor: theme.colorBgContainer,
|
||||
padding: theme.padding,
|
||||
borderRadius: theme.borderRadius,
|
||||
}}
|
||||
>
|
||||
使用主题 token 的组件
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 使用 antd-style 的 createStyles
|
||||
|
||||
```tsx
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${token.colorBgContainer};
|
||||
border-radius: ${token.borderRadius}px;
|
||||
padding: ${token.padding}px;
|
||||
color: ${token.colorText};
|
||||
`,
|
||||
title: css`
|
||||
font-size: ${token.fontSizeLG}px;
|
||||
font-weight: ${token.fontWeightStrong};
|
||||
margin-bottom: ${token.marginSM}px;
|
||||
`,
|
||||
content: css`
|
||||
font-size: ${token.fontSize}px;
|
||||
line-height: ${token.lineHeight};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const Card: FC<CardProps> = ({ title, content }) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.content}>{content}</div>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 一些你经常会忘记使用的 token
|
||||
|
||||
请注意使用下面的 token 而不是 css 字面值。可以访问 https://ant.design/docs/react/customize-theme-cn 了解所有 token
|
||||
|
||||
- 动画类
|
||||
- token.motionDurationMid
|
||||
- token.motionEaseInOut
|
||||
- 包围盒属性
|
||||
- token.paddingSM
|
||||
- token.marginLG
|
||||
|
||||
## Lobe UI 包含的组件
|
||||
|
||||
- 不知道 `@lobehub/ui` 的组件怎么用,有哪些属性,就自己搜下这个项目其它地方怎么用的,不要瞎猜,大部分组件都是在 antd 的基础上扩展了属性
|
||||
- 具体用法不懂可以联网搜索,例如 ActionIcon 就爬取 https://ui.lobehub.com/components/action-icon
|
||||
- 可以阅读 `node_modules/@lobehub/ui/es/index.js` 了解有哪些组件,每个组件的属性是什么
|
||||
|
||||
- General
|
||||
- ActionIcon
|
||||
- ActionIconGroup
|
||||
- Block
|
||||
- Button
|
||||
- DownloadButton
|
||||
- Icon
|
||||
- Data Display
|
||||
- Avatar
|
||||
- AvatarGroup
|
||||
- GroupAvatar
|
||||
- Collapse
|
||||
- FileTypeIcon
|
||||
- FluentEmoji
|
||||
- GuideCard
|
||||
- Highlighter
|
||||
- Hotkey
|
||||
- Image
|
||||
- List
|
||||
- Markdown
|
||||
- SearchResultCards
|
||||
- MaterialFileTypeIcon
|
||||
- Mermaid
|
||||
- Typography
|
||||
- Text
|
||||
- Segmented
|
||||
- Snippet
|
||||
- SortableList
|
||||
- Tag
|
||||
- Tooltip
|
||||
- Video
|
||||
- Data Entry
|
||||
- AutoComplete
|
||||
- CodeEditor
|
||||
- ColorSwatches
|
||||
- CopyButton
|
||||
- DatePicker
|
||||
- EditableText
|
||||
- EmojiPicker
|
||||
- Form
|
||||
- FormModal
|
||||
- HotkeyInput
|
||||
- ImageSelect
|
||||
- Input
|
||||
- SearchBar
|
||||
- Select
|
||||
- SliderWithInput
|
||||
- ThemeSwitch
|
||||
- Feedback
|
||||
- Alert
|
||||
- Drawer
|
||||
- Modal
|
||||
- Layout
|
||||
- DraggablePanel
|
||||
- DraggablePanelBody
|
||||
- DraggablePanelContainer
|
||||
- DraggablePanelFooter
|
||||
- DraggablePanelHeader
|
||||
- Footer
|
||||
- Grid
|
||||
- Header
|
||||
- Layout
|
||||
- LayoutFooter
|
||||
- LayoutHeader
|
||||
- LayoutMain
|
||||
- LayoutSidebar
|
||||
- LayoutSidebarInner
|
||||
- LayoutToc
|
||||
- MaskShadow
|
||||
- ScrollShadow
|
||||
- Navigation
|
||||
- Burger
|
||||
- Dropdown
|
||||
- Menu
|
||||
- SideNav
|
||||
- Tabs
|
||||
- Toc
|
||||
- Theme
|
||||
- ConfigProvider
|
||||
- FontLoader
|
||||
- ThemeProvider
|
||||
155
.cursor/rules/react.mdc
Normal file
155
.cursor/rules/react.mdc
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
description:
|
||||
globs: *.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# React Component Writing Guide
|
||||
|
||||
- Use antd-style for complex styles; for simple cases, use the `style` attribute for inline styles
|
||||
- Use `Flexbox` and `Center` components from react-layout-kit for flex and centered layouts
|
||||
- Component selection priority: src/components > installed component packages > lobe-ui > antd
|
||||
- Use selectors to access zustand store data instead of accessing the store directly
|
||||
|
||||
## Lobe UI Components
|
||||
|
||||
- If unsure how to use `@lobehub/ui` components or what props they accept, search for existing usage in this project instead of guessing. Most components extend antd components with additional props
|
||||
- For specific usage, search online. For example, for ActionIcon visit <https://ui.lobehub.com/components/action-icon>
|
||||
- Read `node_modules/@lobehub/ui/es/index.js` to see all available components and their props
|
||||
|
||||
- General
|
||||
- ActionIcon
|
||||
- ActionIconGroup
|
||||
- Block
|
||||
- Button
|
||||
- Icon
|
||||
- Data Display
|
||||
- Accordion
|
||||
- Avatar
|
||||
- Collapse
|
||||
- Empty
|
||||
- FileTypeIcon
|
||||
- FluentEmoji
|
||||
- GroupAvatar
|
||||
- GuideCard
|
||||
- Highlighter
|
||||
- Hotkey
|
||||
- Image
|
||||
- List
|
||||
- Markdown
|
||||
- MaterialFileTypeIcon
|
||||
- Mermaid
|
||||
- Segmented
|
||||
- Skeleton
|
||||
- Snippet
|
||||
- SortableList
|
||||
- Tag
|
||||
- Tooltip
|
||||
- Video
|
||||
- Data Entry
|
||||
- AutoComplete
|
||||
- CodeEditor
|
||||
- ColorSwatches
|
||||
- CopyButton
|
||||
- DatePicker
|
||||
- DownloadButton
|
||||
- EditableText
|
||||
- EmojiPicker
|
||||
- Form
|
||||
- FormModal
|
||||
- HotkeyInput
|
||||
- ImageSelect
|
||||
- Input
|
||||
- SearchBar
|
||||
- Select
|
||||
- SliderWithInput
|
||||
- ThemeSwitch
|
||||
- Feedback
|
||||
- Alert
|
||||
- Drawer
|
||||
- Modal
|
||||
- Layout
|
||||
- DraggablePanel
|
||||
- Footer
|
||||
- Grid
|
||||
- Header
|
||||
- Layout
|
||||
- MaskShadow
|
||||
- ScrollShadow
|
||||
- Navigation
|
||||
- Burger
|
||||
- DraggableSideNav
|
||||
- Dropdown
|
||||
- Menu
|
||||
- SideNav
|
||||
- Tabs
|
||||
- Toc
|
||||
- Theme
|
||||
- ConfigProvider
|
||||
- FontLoader
|
||||
- ThemeProvider
|
||||
- Typography
|
||||
- Text
|
||||
|
||||
## Routing Architecture
|
||||
|
||||
This project uses a **hybrid routing architecture**: Next.js App Router for static pages + React Router DOM for the main SPA.
|
||||
|
||||
### Route Types
|
||||
|
||||
```plaintext
|
||||
+------------------+--------------------------------+--------------------------------+
|
||||
| Route Type | Use Case | Implementation |
|
||||
+------------------+--------------------------------+--------------------------------+
|
||||
| Next.js App | Auth pages (login, signup, | page.tsx file convention |
|
||||
| Router | oauth, reset-password, etc.) | src/app/[variants]/(auth)/ |
|
||||
+------------------+--------------------------------+--------------------------------+
|
||||
| React Router | Main SPA features | BrowserRouter + Routes |
|
||||
| DOM | (chat, discover, settings) | desktopRouter.config.tsx |
|
||||
| | | mobileRouter.config.tsx |
|
||||
+------------------+--------------------------------+--------------------------------+
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
- Entry point: `src/app/[variants]/page.tsx` - Routes to Desktop or Mobile based on device
|
||||
- Desktop router: `src/app/[variants]/router/desktopRouter.config.tsx`
|
||||
- Mobile router: `src/app/[variants]/(mobile)/router/mobileRouter.config.tsx`
|
||||
- Router utilities: `src/utils/router.tsx`
|
||||
|
||||
### Router Utilities
|
||||
|
||||
```tsx
|
||||
import { dynamicElement, redirectElement, ErrorBoundary, RouteConfig } from '@/utils/router';
|
||||
|
||||
// Lazy load a page component
|
||||
element: dynamicElement(() => import('./chat'), 'Desktop > Chat')
|
||||
|
||||
// Create a redirect
|
||||
element: redirectElement('/settings/profile')
|
||||
|
||||
// Error boundary for route
|
||||
errorElement: <ErrorBoundary resetPath="/chat" />
|
||||
```
|
||||
|
||||
### Adding New Routes
|
||||
|
||||
1. Add route config to `desktopRouter.config.tsx` or `mobileRouter.config.tsx`
|
||||
2. Create page component in the corresponding directory under `(main)/`
|
||||
3. Use `dynamicElement()` for lazy loading
|
||||
|
||||
### Navigation
|
||||
|
||||
```tsx
|
||||
// In components - use react-router-dom hooks
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
const navigate = useNavigate();
|
||||
navigate('/chat');
|
||||
|
||||
// From stores - use global navigate
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
const navigate = useGlobalStore.getState().navigate;
|
||||
navigate?.('/settings');
|
||||
```
|
||||
138
.cursor/rules/recent-data-usage.mdc
Normal file
138
.cursor/rules/recent-data-usage.mdc
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Recent Data 使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
Recent 数据(recentTopics, recentResources, recentPages)存储在 session store 中,可以在应用的任何地方访问。
|
||||
|
||||
## 数据初始化
|
||||
|
||||
在应用顶层(如 `RecentHydration.tsx`)中初始化所有 recent 数据:
|
||||
|
||||
```tsx
|
||||
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
|
||||
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
|
||||
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
|
||||
|
||||
const App = () => {
|
||||
// 初始化所有 recent 数据
|
||||
useInitRecentTopic();
|
||||
useInitRecentResource();
|
||||
useInitRecentPage();
|
||||
|
||||
return <YourComponents />;
|
||||
};
|
||||
```
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:直接从 Store 读取(推荐用于多处使用)
|
||||
|
||||
在任何组件中直接访问 store 中的数据:
|
||||
|
||||
```tsx
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { recentSelectors } from '@/store/session/selectors';
|
||||
|
||||
const Component = () => {
|
||||
// 读取数据
|
||||
const recentTopics = useSessionStore(recentSelectors.recentTopics);
|
||||
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
|
||||
|
||||
if (!isInit) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{recentTopics.map(topic => (
|
||||
<div key={topic.id}>{topic.title}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 方式二:使用 Hook 返回的数据(用于单一组件)
|
||||
|
||||
```tsx
|
||||
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
|
||||
|
||||
const Component = () => {
|
||||
const { data: recentTopics, isLoading } = useInitRecentTopic();
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
return <div>{/* 使用 recentTopics */}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## 可用的 Selectors
|
||||
|
||||
### Recent Topics (最近话题)
|
||||
|
||||
```tsx
|
||||
import { recentSelectors } from '@/store/session/selectors';
|
||||
|
||||
// 数据
|
||||
const recentTopics = useSessionStore(recentSelectors.recentTopics);
|
||||
// 类型: RecentTopic[]
|
||||
|
||||
// 初始化状态
|
||||
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
|
||||
// 类型: boolean
|
||||
```
|
||||
|
||||
**RecentTopic 类型:**
|
||||
```typescript
|
||||
interface RecentTopic {
|
||||
agent: {
|
||||
avatar: string | null;
|
||||
backgroundColor: string | null;
|
||||
id: string;
|
||||
title: string | null;
|
||||
} | null;
|
||||
id: string;
|
||||
title: string | null;
|
||||
updatedAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### Recent Resources (最近文件)
|
||||
|
||||
```tsx
|
||||
import { recentSelectors } from '@/store/session/selectors';
|
||||
|
||||
// 数据
|
||||
const recentResources = useSessionStore(recentSelectors.recentResources);
|
||||
// 类型: FileListItem[]
|
||||
|
||||
// 初始化状态
|
||||
const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
|
||||
// 类型: boolean
|
||||
```
|
||||
|
||||
### Recent Pages (最近页面)
|
||||
|
||||
```tsx
|
||||
import { recentSelectors } from '@/store/session/selectors';
|
||||
|
||||
// 数据
|
||||
const recentPages = useSessionStore(recentSelectors.recentPages);
|
||||
// 类型: any[]
|
||||
|
||||
// 初始化状态
|
||||
const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
|
||||
// 类型: boolean
|
||||
```
|
||||
|
||||
## 特性
|
||||
|
||||
1. **自动登录检测**:只有在用户登录时才会加载数据
|
||||
2. **数据缓存**:数据存储在 store 中,多处使用无需重复加载
|
||||
3. **自动刷新**:使用 SWR,在用户重新聚焦时自动刷新(5分钟间隔)
|
||||
4. **类型安全**:完整的 TypeScript 类型定义
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **初始化位置**:在应用顶层统一初始化所有 recent 数据
|
||||
2. **数据访问**:使用 selectors 从 store 读取数据
|
||||
3. **多处使用**:同一数据在多个组件中使用时,推荐使用方式一(直接从 store 读取)
|
||||
4. **性能优化**:使用 selector 确保只有相关数据变化时才重新渲染
|
||||
|
|
@ -14,7 +14,7 @@ All following rules are saved under `.cursor/rules/` directory:
|
|||
|
||||
## Frontend
|
||||
|
||||
- `react-component.mdc` – React component style guide and conventions
|
||||
- `react.mdc` – React component style guide and conventions
|
||||
- `i18n.mdc` – Internationalization guide using react-i18next
|
||||
- `typescript.mdc` – TypeScript code style guide
|
||||
- `packages/react-layout-kit.mdc` – Usage guide for react-layout-kit
|
||||
|
|
|
|||
285
.cursor/rules/testing-guide/agent-runtime-e2e.mdc
Normal file
285
.cursor/rules/testing-guide/agent-runtime-e2e.mdc
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
# Agent Runtime E2E 测试指南
|
||||
|
||||
本文档描述 Agent Runtime 端到端测试的核心原则和实施方法。
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 最小化 Mock 原则
|
||||
|
||||
E2E 测试的目标是尽可能接近真实运行环境。因此,我们只 Mock **三个外部依赖**:
|
||||
|
||||
| 依赖 | Mock 方式 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| **Database** | PGLite | 使用 `@lobechat/database/test-utils` 提供的内存数据库 |
|
||||
| **Redis** | InMemoryAgentStateManager | Mock `AgentStateManager` 使用内存实现 |
|
||||
| **Redis** | InMemoryStreamEventManager | Mock `StreamEventManager` 使用内存实现 |
|
||||
|
||||
**不 Mock 的部分:**
|
||||
|
||||
- `model-bank` - 使用真实的模型配置数据
|
||||
- `Mecha` (AgentToolsEngine, ContextEngineering) - 使用真实逻辑
|
||||
- `AgentRuntimeService` - 使用真实逻辑
|
||||
- `AgentRuntimeCoordinator` - 使用真实逻辑
|
||||
|
||||
### 2. 使用 vi.spyOn 而非 vi.mock
|
||||
|
||||
不同测试场景需要不同的 LLM 响应。使用 `vi.spyOn` 可以:
|
||||
|
||||
- 在每个测试中灵活控制返回值
|
||||
- 便于测试不同场景(纯文本、tool calls、错误等)
|
||||
- 避免全局 mock 导致的测试隔离问题
|
||||
|
||||
### 3. 默认模型使用 gpt-5
|
||||
|
||||
- `model-bank` 中肯定有该模型的数据
|
||||
- 避免短期内因模型更新需要修改测试
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 数据库设置
|
||||
|
||||
```typescript
|
||||
import { LobeChatDatabase } from '@lobechat/database';
|
||||
import { getTestDB } from '@lobechat/database/test-utils';
|
||||
|
||||
let testDB: LobeChatDatabase;
|
||||
|
||||
beforeEach(async () => {
|
||||
testDB = await getTestDB();
|
||||
});
|
||||
```
|
||||
|
||||
### OpenAI Response Mock Helper
|
||||
|
||||
创建一个 helper 函数来生成 OpenAI 格式的流式响应:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 创建 OpenAI 格式的流式响应
|
||||
*/
|
||||
export const createOpenAIStreamResponse = (options: {
|
||||
content?: string;
|
||||
toolCalls?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
}>;
|
||||
finishReason?: 'stop' | 'tool_calls';
|
||||
}) => {
|
||||
const { content, toolCalls, finishReason = 'stop' } = options;
|
||||
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// 发送内容 chunk
|
||||
if (content) {
|
||||
const chunk = {
|
||||
id: 'chatcmpl-mock',
|
||||
object: 'chat.completion.chunk',
|
||||
model: 'gpt-5',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content },
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
||||
}
|
||||
|
||||
// 发送 tool_calls chunk
|
||||
if (toolCalls) {
|
||||
for (const tool of toolCalls) {
|
||||
const chunk = {
|
||||
id: 'chatcmpl-mock',
|
||||
object: 'chat.completion.chunk',
|
||||
model: 'gpt-5',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: tool.id,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.name,
|
||||
arguments: tool.arguments,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
||||
}
|
||||
}
|
||||
|
||||
// 发送完成 chunk
|
||||
const finishChunk = {
|
||||
id: 'chatcmpl-mock',
|
||||
object: 'chat.completion.chunk',
|
||||
model: 'gpt-5',
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {},
|
||||
finish_reason: finishReason,
|
||||
},
|
||||
],
|
||||
};
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
|
||||
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{ headers: { 'content-type': 'text/event-stream' } },
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 内存状态管理
|
||||
|
||||
使用依赖注入替代 Redis:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
InMemoryAgentStateManager,
|
||||
InMemoryStreamEventManager,
|
||||
} from '@/server/modules/AgentRuntime';
|
||||
import { AgentRuntimeService } from '@/server/services/agentRuntime';
|
||||
|
||||
const stateManager = new InMemoryAgentStateManager();
|
||||
const streamEventManager = new InMemoryStreamEventManager();
|
||||
|
||||
const service = new AgentRuntimeService(serverDB, userId, {
|
||||
coordinatorOptions: {
|
||||
stateManager,
|
||||
streamEventManager,
|
||||
},
|
||||
queueService: null, // 禁用 QStash 队列,使用 executeSync
|
||||
streamEventManager,
|
||||
});
|
||||
```
|
||||
|
||||
### Mock OpenAI API
|
||||
|
||||
在测试中使用 `vi.spyOn` mock fetch:
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// 在测试文件顶部或 beforeEach 中
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
||||
|
||||
// 在具体测试中设置返回值
|
||||
it('should handle text response', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(createOpenAIStreamResponse({ content: '杭州今天天气晴朗' }));
|
||||
|
||||
// ... 执行测试
|
||||
});
|
||||
|
||||
it('should handle tool calls', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'call_123',
|
||||
name: 'lobe-web-browsing____search____builtin',
|
||||
arguments: JSON.stringify({ query: '杭州天气' }),
|
||||
},
|
||||
],
|
||||
finishReason: 'tool_calls',
|
||||
}),
|
||||
);
|
||||
|
||||
// ... 执行测试
|
||||
});
|
||||
```
|
||||
|
||||
## 测试场景
|
||||
|
||||
### 1. 基本对话测试
|
||||
|
||||
```typescript
|
||||
describe('Basic Chat', () => {
|
||||
it('should complete a simple conversation', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({ content: 'Hello! How can I help you?' }),
|
||||
);
|
||||
|
||||
const result = await service.createOperation({
|
||||
agentConfig: { model: 'gpt-5', provider: 'openai' },
|
||||
initialMessages: [{ role: 'user', content: 'Hi' }],
|
||||
// ...
|
||||
});
|
||||
|
||||
const finalState = await service.executeSync(result.operationId);
|
||||
expect(finalState.status).toBe('done');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Tool 调用测试
|
||||
|
||||
```typescript
|
||||
describe('Tool Calls', () => {
|
||||
it('should execute web-browsing tool', async () => {
|
||||
// 第一次调用:LLM 返回 tool_calls
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'call_123',
|
||||
name: 'lobe-web-browsing____search____builtin',
|
||||
arguments: JSON.stringify({ query: '杭州天气' }),
|
||||
},
|
||||
],
|
||||
finishReason: 'tool_calls',
|
||||
}),
|
||||
);
|
||||
|
||||
// 第二次调用:处理 tool 结果后的响应
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
createOpenAIStreamResponse({ content: '根据搜索结果,杭州今天...' }),
|
||||
);
|
||||
|
||||
// ... 执行测试
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 错误处理测试
|
||||
|
||||
```typescript
|
||||
describe('Error Handling', () => {
|
||||
it('should handle API errors gracefully', async () => {
|
||||
fetchSpy.mockRejectedValueOnce(new Error('API rate limit exceeded'));
|
||||
|
||||
// ... 执行测试并验证错误处理
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 文件组织
|
||||
|
||||
```
|
||||
src/server/routers/lambda/__tests__/integration/
|
||||
├── setup.ts # 测试设置工具
|
||||
├── aiAgent.integration.test.ts # 现有集成测试
|
||||
├── aiAgent.e2e.test.ts # E2E 测试
|
||||
└── helpers/
|
||||
└── openaiMock.ts # OpenAI mock helper
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **测试隔离**:每个测试后清理 `InMemoryAgentStateManager` 和 `InMemoryStreamEventManager`
|
||||
2. **超时设置**:E2E 测试可能需要更长的超时时间
|
||||
3. **调试**:使用 `DEBUG=lobe-server:*` 环境变量查看详细日志
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
description: Best practices for testing Zustand store actions
|
||||
globs: "src/store/**/*.test.ts"
|
||||
globs: 'src/store/**/*.test.ts'
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ import { act, renderHook } from '@testing-library/react';
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { messageService } from '@/services/message';
|
||||
|
||||
import { useChatStore } from '../../store';
|
||||
|
||||
// Keep zustand mock as it's needed globally
|
||||
|
|
@ -229,8 +230,7 @@ it('should handle topic creation flow', async () => {
|
|||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
// Spy on action dependencies
|
||||
const createTopicSpy = vi.spyOn(result.current, 'createTopic')
|
||||
.mockResolvedValue('new-topic-id');
|
||||
const createTopicSpy = vi.spyOn(result.current, 'createTopic').mockResolvedValue('new-topic-id');
|
||||
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleMessageLoading');
|
||||
|
||||
// Execute
|
||||
|
|
@ -251,9 +251,7 @@ When testing streaming responses, simulate the flow properly:
|
|||
```typescript
|
||||
it('should handle streaming chunks', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
const messages = [
|
||||
{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' },
|
||||
];
|
||||
const messages = [{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' }];
|
||||
|
||||
const streamSpy = vi
|
||||
.spyOn(chatService, 'createAssistantMessageStream')
|
||||
|
|
@ -287,9 +285,7 @@ Always test error scenarios:
|
|||
it('should handle errors gracefully', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
vi.spyOn(messageService, 'createMessage').mockRejectedValue(
|
||||
new Error('create message error'),
|
||||
);
|
||||
vi.spyOn(messageService, 'createMessage').mockRejectedValue(new Error('create message error'));
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
|
|
@ -330,8 +326,7 @@ it('should test something', async () => {
|
|||
it('should call internal methods', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
const internalMethodSpy = vi.spyOn(result.current, 'internal_method')
|
||||
.mockResolvedValue();
|
||||
const internalMethodSpy = vi.spyOn(result.current, 'internal_method').mockResolvedValue();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.publicMethod();
|
||||
|
|
@ -456,6 +451,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import { discoverService } from '@/services/discover';
|
||||
import { globalHelpers } from '@/store/global/helpers';
|
||||
|
||||
import { useDiscoverStore as useStore } from '../../store';
|
||||
|
||||
vi.mock('zustand/traditional');
|
||||
|
|
@ -486,6 +482,7 @@ describe('SWR Hook Actions', () => {
|
|||
```
|
||||
|
||||
**Key points**:
|
||||
|
||||
- **DO NOT mock useSWR** - let it use the real implementation
|
||||
- Only mock the **service methods** (fetchers)
|
||||
- Use `waitFor` from `@testing-library/react` to wait for async operations
|
||||
|
|
@ -559,21 +556,19 @@ it('should not fetch when required parameter is missing', () => {
|
|||
7. **Type assertions**: Use `as any` for test mock data where type definitions are strict
|
||||
|
||||
**Why this matters**:
|
||||
|
||||
- The fetcher (service method) is what we're testing - it must be called
|
||||
- Hardcoding the return value bypasses the actual fetcher logic
|
||||
- SWR returns Promises in real usage, tests should mirror this behavior
|
||||
|
||||
## Benefits of This Approach
|
||||
|
||||
✅ **Clear test layers** - Each test only spies on direct dependencies
|
||||
✅ **Correct mocks** - Mocks match actual implementation
|
||||
✅ **Better maintainability** - Changes to implementation require fewer test updates
|
||||
✅ **Improved coverage** - Structured approach ensures all branches are tested
|
||||
✅ **Reduced coupling** - Tests are independent and can run in any order
|
||||
✅ **Clear test layers** - Each test only spies on direct dependencies ✅ **Correct mocks** - Mocks match actual implementation ✅ **Better maintainability** - Changes to implementation require fewer test updates ✅ **Improved coverage** - Structured approach ensures all branches are tested ✅ **Reduced coupling** - Tests are independent and can run in any order
|
||||
|
||||
## Reference
|
||||
|
||||
See example implementation in:
|
||||
|
||||
- `src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts` (Regular actions)
|
||||
- `src/store/discover/slices/plugin/action.test.ts` (SWR hooks)
|
||||
- `src/store/discover/slices/mcp/action.test.ts` (SWR hooks)
|
||||
|
|
|
|||
|
|
@ -16,10 +16,6 @@ alwaysApply: false
|
|||
- prefer `@ts-expect-error` over `@ts-ignore` over `as any`
|
||||
- Avoid meaningless null/undefined parameters; design strict function contracts.
|
||||
|
||||
## Imports and Modules
|
||||
|
||||
- When importing a directory module, prefer the explicit index path like `@/db/index` instead of `@/db`.
|
||||
|
||||
## Asynchronous Patterns and Concurrency
|
||||
|
||||
- Prefer `async`/`await` over callbacks or chained `.then` promises.
|
||||
|
|
|
|||
|
|
@ -1,137 +1,126 @@
|
|||
---
|
||||
description:
|
||||
description:
|
||||
globs: src/store/**
|
||||
alwaysApply: false
|
||||
---
|
||||
# LobeChat Zustand Action 组织模式
|
||||
|
||||
本文档详细说明了 LobeChat 项目中 Zustand Action 的组织方式、命名规范和实现模式,特别关注乐观更新与后端服务的集成。
|
||||
# LobeChat Zustand Action Patterns
|
||||
|
||||
## Action 类型分层
|
||||
## Action Type Hierarchy
|
||||
|
||||
LobeChat 的 Action 采用分层架构,明确区分不同职责:
|
||||
LobeChat Actions use a layered architecture with clear separation of responsibilities:
|
||||
|
||||
### 1. Public Actions
|
||||
对外暴露的主要接口,供 UI 组件调用:
|
||||
- 命名:动词形式(`createTopic`, `sendMessage`, `updateTopicTitle`)
|
||||
- 职责:参数验证、流程编排、调用 internal actions
|
||||
- 示例:[src/store/chat/slices/topic/action.ts](mdc:src/store/chat/slices/topic/action.ts)
|
||||
|
||||
Main interfaces exposed for UI component consumption:
|
||||
|
||||
- Naming: Verb form (`createTopic`, `sendMessage`, `updateTopicTitle`)
|
||||
- Responsibilities: Parameter validation, flow orchestration, calling internal actions
|
||||
- Example: [src/store/chat/slices/topic/action.ts](mdc:src/store/chat/slices/topic/action.ts)
|
||||
|
||||
```typescript
|
||||
// Public Action 示例
|
||||
// Public Action example
|
||||
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 组件直接调用
|
||||
|
||||
Internal implementation details handling core business logic:
|
||||
|
||||
- Naming: `internal_` prefix + verb (`internal_createTopic`, `internal_updateMessageContent`)
|
||||
- Responsibilities: Optimistic updates, service calls, error handling, state synchronization
|
||||
- Should not be called directly by UI components
|
||||
|
||||
```typescript
|
||||
// Internal Action 示例 - 乐观更新模式
|
||||
// Internal Action example - Optimistic update pattern
|
||||
internal_createTopic: async (params) => {
|
||||
const tmpId = Date.now().toString();
|
||||
|
||||
// 1. 立即更新前端状态(乐观更新)
|
||||
|
||||
// 1. Immediately update frontend state (optimistic update)
|
||||
get().internal_dispatchTopic(
|
||||
{ type: 'addTopic', value: { ...params, id: tmpId } },
|
||||
'internal_createTopic',
|
||||
);
|
||||
get().internal_updateTopicLoading(tmpId, true);
|
||||
|
||||
// 2. 调用后端服务
|
||||
|
||||
// 2. Call backend service
|
||||
const topicId = await topicService.createTopic(params);
|
||||
get().internal_updateTopicLoading(tmpId, false);
|
||||
|
||||
// 3. 刷新数据确保一致性
|
||||
|
||||
// 3. Refresh data to ensure consistency
|
||||
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、处理状态对比
|
||||
|
||||
Methods dedicated to handling state updates:
|
||||
|
||||
- Naming: `internal_dispatch` + entity name (`internal_dispatchTopic`, `internal_dispatchMessage`)
|
||||
- Responsibilities: Calling reducers, updating Zustand store, handling state comparison
|
||||
|
||||
```typescript
|
||||
// Dispatch Method 示例
|
||||
// Dispatch Method example
|
||||
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`
|
||||
## When to Use Reducer Pattern vs. Simple `set`
|
||||
|
||||
### 使用 Reducer 模式的场景
|
||||
### Use Reducer Pattern When
|
||||
|
||||
适用于复杂的数据结构管理,特别是:
|
||||
- 管理对象列表或映射(如 `messagesMap`, `topicMaps`)
|
||||
- 需要乐观更新的场景
|
||||
- 状态转换逻辑复杂
|
||||
- 需要类型安全的 action payload
|
||||
Suitable for complex data structure management, especially:
|
||||
|
||||
- Managing object lists or maps (e.g., `messagesMap`, `topicMaps`)
|
||||
- Scenarios requiring optimistic updates
|
||||
- Complex state transition logic
|
||||
- Type-safe action payloads needed
|
||||
|
||||
```typescript
|
||||
// Reducer 模式示例 - 复杂消息状态管理
|
||||
// Reducer pattern example - Complex message state management
|
||||
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()
|
||||
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: {}
|
||||
});
|
||||
});
|
||||
// ...
|
||||
}
|
||||
// ...其他复杂状态转换
|
||||
// ...other complex state transitions
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 使用简单 `set` 的场景
|
||||
### Use Simple `set` When
|
||||
|
||||
适用于简单状态更新:
|
||||
- 切换布尔值
|
||||
- 更新简单字符串/数字
|
||||
- 设置单一状态字段
|
||||
Suitable for simple state updates:
|
||||
|
||||
- Toggling boolean values
|
||||
- Updating simple strings/numbers
|
||||
- Setting single state fields
|
||||
|
||||
```typescript
|
||||
// 简单 set 示例
|
||||
// Simple set example
|
||||
updateInputMessage: (message) => {
|
||||
if (isEqual(message, get().inputMessage)) return;
|
||||
set({ inputMessage: message }, false, n('updateInputMessage'));
|
||||
|
|
@ -142,45 +131,45 @@ togglePortal: (open?: boolean) => {
|
|||
},
|
||||
```
|
||||
|
||||
## 乐观更新实现模式
|
||||
## Optimistic Update Implementation Patterns
|
||||
|
||||
乐观更新是 LobeChat 中的核心模式,用于提供流畅的用户体验:
|
||||
Optimistic updates are a core pattern in LobeChat for providing smooth user experience:
|
||||
|
||||
### 标准乐观更新流程
|
||||
### Standard Optimistic Update Flow
|
||||
|
||||
```typescript
|
||||
// 完整的乐观更新示例
|
||||
// Complete optimistic update example
|
||||
internal_updateMessageContent: async (id, content, extra) => {
|
||||
const { internal_dispatchMessage, refreshMessages } = get();
|
||||
|
||||
// 1. 立即更新前端状态(乐观更新)
|
||||
// 1. Immediately update frontend state (optimistic update)
|
||||
internal_dispatchMessage({
|
||||
id,
|
||||
type: 'updateMessage',
|
||||
value: { content },
|
||||
});
|
||||
|
||||
// 2. 调用后端服务
|
||||
// 2. Call backend service
|
||||
await messageService.updateMessage(id, {
|
||||
content,
|
||||
tools: extra?.toolCalls ? internal_transformToolCalls(extra.toolCalls) : undefined,
|
||||
// ...其他字段
|
||||
// ...other fields
|
||||
});
|
||||
|
||||
// 3. 刷新确保数据一致性
|
||||
// 3. Refresh to ensure data consistency
|
||||
await refreshMessages();
|
||||
},
|
||||
```
|
||||
|
||||
### 创建操作的乐观更新
|
||||
### Optimistic Update for Create Operations
|
||||
|
||||
```typescript
|
||||
internal_createMessage: async (message, context) => {
|
||||
const { internal_createTmpMessage, refreshMessages, internal_toggleMessageLoading } = get();
|
||||
|
||||
|
||||
let tempId = context?.tempMessageId;
|
||||
if (!tempId) {
|
||||
// 创建临时消息用于乐观更新
|
||||
// Create temporary message for optimistic update
|
||||
tempId = internal_createTmpMessage(message);
|
||||
internal_toggleMessageLoading(true, tempId);
|
||||
}
|
||||
|
|
@ -194,7 +183,7 @@ internal_createMessage: async (message, context) => {
|
|||
return id;
|
||||
} catch (e) {
|
||||
internal_toggleMessageLoading(false, tempId);
|
||||
// 错误处理:更新消息错误状态
|
||||
// Error handling: update message error state
|
||||
internal_dispatchMessage({
|
||||
id: tempId,
|
||||
type: 'updateMessage',
|
||||
|
|
@ -204,96 +193,77 @@ internal_createMessage: async (message, context) => {
|
|||
},
|
||||
```
|
||||
|
||||
### 删除操作模式(不使用乐观更新)
|
||||
### Delete Operation Pattern (No Optimistic Update)
|
||||
|
||||
删除操作通常不适合乐观更新,因为:
|
||||
- 删除是破坏性操作,错误恢复复杂
|
||||
- 用户对删除操作的即时反馈期望较低
|
||||
- 删除失败时恢复原状态会造成困惑
|
||||
Delete operations typically don't suit optimistic updates because:
|
||||
|
||||
- Deletion is destructive; error recovery is complex
|
||||
- Users have lower expectations for immediate feedback on deletions
|
||||
- Restoring state on deletion failure causes confusion
|
||||
|
||||
```typescript
|
||||
// 删除操作的标准模式 - 无乐观更新
|
||||
// Standard delete operation pattern - No optimistic update
|
||||
removeGenerationTopic: async (id: string) => {
|
||||
const { internal_removeGenerationTopic } = get();
|
||||
await internal_removeGenerationTopic(id);
|
||||
},
|
||||
|
||||
internal_removeGenerationTopic: async (id: string) => {
|
||||
// 1. 显示加载状态
|
||||
// 1. Show loading state
|
||||
get().internal_updateGenerationTopicLoading(id, true);
|
||||
|
||||
|
||||
try {
|
||||
// 2. 直接调用后端服务
|
||||
// 2. Directly call backend service
|
||||
await generationTopicService.deleteTopic(id);
|
||||
|
||||
// 3. 刷新数据获取最新状态
|
||||
|
||||
// 3. Refresh data to get latest state
|
||||
await get().refreshGenerationTopics();
|
||||
} finally {
|
||||
// 4. 确保清除加载状态(无论成功或失败)
|
||||
// 4. Ensure loading state is cleared (whether success or failure)
|
||||
get().internal_updateGenerationTopicLoading(id, false);
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
删除操作的特点:
|
||||
- 直接调用服务,不预先更新状态
|
||||
- 依赖 loading 状态提供用户反馈
|
||||
- 操作完成后刷新整个列表确保一致性
|
||||
- 使用 `try/finally` 确保 loading 状态总是被清理
|
||||
Delete operation characteristics:
|
||||
|
||||
## 加载状态管理模式
|
||||
- Directly call service without pre-updating state
|
||||
- Rely on loading state for user feedback
|
||||
- Refresh entire list after operation to ensure consistency
|
||||
- Use `try/finally` to ensure loading state is always cleaned up
|
||||
|
||||
LobeChat 使用统一的加载状态管理模式:
|
||||
## Loading State Management Pattern
|
||||
|
||||
### 数组式加载状态
|
||||
LobeChat uses a unified loading state management pattern:
|
||||
|
||||
### Array-based Loading State
|
||||
|
||||
```typescript
|
||||
// 在 initialState.ts 中定义
|
||||
// Define in initialState.ts
|
||||
export interface ChatMessageState {
|
||||
messageLoadingIds: string[]; // 消息加载状态
|
||||
messageEditingIds: string[]; // 消息编辑状态
|
||||
chatLoadingIds: string[]; // 对话生成状态
|
||||
messageEditingIds: string[]; // Message editing state
|
||||
}
|
||||
|
||||
// 在 action 中管理
|
||||
internal_toggleMessageLoading: (loading, id) => {
|
||||
set({
|
||||
messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
|
||||
}, false, `internal_toggleMessageLoading/${loading ? 'start' : 'end'}`);
|
||||
},
|
||||
// Manage in action
|
||||
{
|
||||
toggleMessageEditing: (id, editing) => {
|
||||
set(
|
||||
{ messageEditingIds: toggleBooleanList(get().messageEditingIds, id, editing) },
|
||||
false,
|
||||
'toggleMessageEditing',
|
||||
);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 统一的加载状态工具
|
||||
## SWR Integration Pattern
|
||||
|
||||
LobeChat uses SWR for data fetching and cache management:
|
||||
|
||||
### Hook-based Data Fetching
|
||||
|
||||
```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
|
||||
// Define SWR hook in action.ts
|
||||
useFetchMessages: (enable, sessionId, activeTopicId) =>
|
||||
useClientDataSWR<ChatMessage[]>(
|
||||
enable ? [SWR_USE_FETCH_MESSAGES, sessionId, activeTopicId] : null,
|
||||
|
|
@ -304,57 +274,55 @@ useFetchMessages: (enable, sessionId, activeTopicId) =>
|
|||
...get().messagesMap,
|
||||
[messageMapKey(sessionId, activeTopicId)]: messages,
|
||||
};
|
||||
|
||||
|
||||
if (get().messagesInit && isEqual(nextMap, get().messagesMap)) return;
|
||||
|
||||
|
||||
set({ messagesInit: true, messagesMap: nextMap }, false, n('useFetchMessages'));
|
||||
},
|
||||
},
|
||||
),
|
||||
```
|
||||
|
||||
### 缓存失效和刷新
|
||||
### Cache Invalidation and Refresh
|
||||
|
||||
```typescript
|
||||
// 刷新数据的标准模式
|
||||
// Standard data refresh pattern
|
||||
refreshMessages: async () => {
|
||||
await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId]);
|
||||
},
|
||||
|
||||
refreshTopic: async () => {
|
||||
return mutate([SWR_USE_FETCH_TOPIC, get().activeId]);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 命名规范总结
|
||||
## Naming Convention Summary
|
||||
|
||||
### Action 命名模式
|
||||
- Public Actions: 动词形式,描述用户意图
|
||||
### Action Naming Patterns
|
||||
|
||||
- Public Actions: Verb form, describing user intent
|
||||
- `createTopic`, `sendMessage`, `regenerateMessage`
|
||||
- Internal Actions: `internal_` + 动词,描述内部操作
|
||||
- Internal Actions: `internal_` + verb, describing internal operation
|
||||
- `internal_createTopic`, `internal_updateMessageContent`
|
||||
- Dispatch Methods: `internal_dispatch` + 实体名
|
||||
- Dispatch Methods: `internal_dispatch` + entity name
|
||||
- `internal_dispatchTopic`, `internal_dispatchMessage`
|
||||
- Toggle Methods: `internal_toggle` + 状态名
|
||||
- Toggle Methods: `internal_toggle` + state name
|
||||
- `internal_toggleMessageLoading`, `internal_toggleChatLoading`
|
||||
|
||||
### 状态命名模式
|
||||
- ID 数组: `[entity]LoadingIds`, `[entity]EditingIds`
|
||||
- 映射结构: `[entity]Maps`, `[entity]Map`
|
||||
- 当前激活: `active[Entity]Id`
|
||||
- 初始化标记: `[entity]sInit`
|
||||
### State Naming Patterns
|
||||
|
||||
## 最佳实践
|
||||
- ID arrays: `[entity]LoadingIds`, `[entity]EditingIds`
|
||||
- Map structures: `[entity]Maps`, `[entity]Map`
|
||||
- Currently active: `active[Entity]Id`
|
||||
- Initialization flags: `[entity]sInit`
|
||||
|
||||
1. 合理使用乐观更新:
|
||||
- ✅ 适用:创建、更新操作(用户交互频繁)
|
||||
- ❌ 避免:删除操作(破坏性操作,错误恢复复杂)
|
||||
2. 加载状态管理:使用统一的加载状态数组管理并发操作
|
||||
3. 类型安全:为所有 action payload 定义 TypeScript 接口
|
||||
4. SWR 集成:使用 SWR 管理数据获取和缓存失效
|
||||
5. AbortController:为长时间运行的操作提供取消能力
|
||||
6. 操作模式选择:
|
||||
- 创建/更新:乐观更新 + 最终一致性
|
||||
- 删除:加载状态 + 服务调用 + 数据刷新
|
||||
## Best Practices
|
||||
|
||||
这套 Action 组织模式确保了代码的一致性、可维护性,并提供了优秀的用户体验。
|
||||
1. Use optimistic updates appropriately:
|
||||
- ✅ Suitable: Create, update operations (frequent user interaction)
|
||||
- ❌ Avoid: Delete operations (destructive, complex error recovery)
|
||||
2. Loading state management: Use unified loading state arrays to manage concurrent operations
|
||||
3. Type safety: Define TypeScript interfaces for all action payloads
|
||||
4. SWR integration: Use SWR to manage data fetching and cache invalidation
|
||||
5. AbortController: Provide cancellation capability for long-running operations
|
||||
6. Operation mode selection:
|
||||
- Create/Update: Optimistic update + eventual consistency
|
||||
- Delete: Loading state + service call + data refresh
|
||||
|
||||
This Action organization pattern ensures code consistency, maintainability, and provides excellent user experience.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
---
|
||||
description:
|
||||
description:
|
||||
globs: src/store/**
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# LobeChat Zustand Store Slice 组织架构
|
||||
|
||||
本文档描述了 LobeChat 项目中 Zustand Store 的模块化 Slice 组织方式,展示如何通过分片架构管理复杂的应用状态。
|
||||
|
|
@ -69,7 +70,7 @@ export const useChatStore = createWithEqualityFn<ChatStore>()(
|
|||
|
||||
每个 slice 位于 `src/store/chat/slices/[sliceName]/` 目录下:
|
||||
|
||||
```
|
||||
```plaintext
|
||||
src/store/chat/slices/
|
||||
└── [sliceName]/ # 例如 message, topic, aiChat, builtinTool
|
||||
├── action.ts # 定义 actions (或者是一个 actions/ 目录)
|
||||
|
|
@ -159,15 +160,16 @@ export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch
|
|||
// 典型的 selectors.ts 结构
|
||||
import { ChatStoreState } from '../../initialState';
|
||||
|
||||
const currentTopics = (s: ChatStoreState): ChatTopic[] | undefined =>
|
||||
s.topicMaps[s.activeId];
|
||||
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);
|
||||
const getTopicById =
|
||||
(id: string) =>
|
||||
(s: ChatStoreState): ChatTopic | undefined =>
|
||||
currentTopics(s)?.find((topic) => topic.id === id);
|
||||
|
||||
// 核心模式:使用 xxxSelectors 聚合导出
|
||||
export const topicSelectors = {
|
||||
|
|
@ -219,13 +221,15 @@ src/store/chat/slices/builtinTool/
|
|||
## 状态设计模式
|
||||
|
||||
### 1. Map 结构用于关联数据
|
||||
|
||||
```typescript
|
||||
// 以 sessionId 为 key,管理多个会话的数据
|
||||
topicMaps: Record<string, ChatTopic[]>
|
||||
messagesMap: Record<string, ChatMessage[]>
|
||||
topicMaps: Record<string, ChatTopic[]>;
|
||||
messagesMap: Record<string, ChatMessage[]>;
|
||||
```
|
||||
|
||||
### 2. 数组用于加载状态管理
|
||||
|
||||
```typescript
|
||||
// 管理多个并发操作的加载状态
|
||||
messageLoadingIds: string[]
|
||||
|
|
@ -234,6 +238,7 @@ chatLoadingIds: string[]
|
|||
```
|
||||
|
||||
### 3. 可选字段用于当前活动项
|
||||
|
||||
```typescript
|
||||
// 当前激活的实体 ID
|
||||
activeId: string
|
||||
|
|
@ -244,6 +249,7 @@ activeThreadId?: string
|
|||
## Slice 集成到顶层 Store
|
||||
|
||||
### 1. 状态聚合
|
||||
|
||||
```typescript
|
||||
// 在 initialState.ts 中
|
||||
export type ChatStoreState = ChatTopicState &
|
||||
|
|
@ -253,6 +259,7 @@ export type ChatStoreState = ChatTopicState &
|
|||
```
|
||||
|
||||
### 2. Action 接口聚合
|
||||
|
||||
```typescript
|
||||
// 在 store.ts 中
|
||||
export interface ChatStoreAction
|
||||
|
|
@ -263,6 +270,7 @@ export interface ChatStoreAction
|
|||
```
|
||||
|
||||
### 3. Selector 统一导出
|
||||
|
||||
```typescript
|
||||
// 在 selectors.ts 中 - 统一聚合 selectors
|
||||
export { chatSelectors } from './slices/message/selectors';
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ The project follows a well-organized monorepo structure:
|
|||
- `src/` - Main source code
|
||||
- `docs/` - Documentation
|
||||
- `.cursor/rules/` - Development rules and guidelines
|
||||
- PR titles starting with `✨ feat/` or `🐛 fix` will trigger the release workflow upon merge. Only use these prefixes for significant user-facing feature changes or bug fixes
|
||||
|
||||
## Development Workflow
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ The project follows a well-organized monorepo structure:
|
|||
|
||||
### Type Checking
|
||||
|
||||
- Use `bun run typecheck` to check for type errors
|
||||
- Use `bun run type-check` to check for type errors
|
||||
|
||||
### i18n
|
||||
|
||||
|
|
@ -83,7 +84,7 @@ All following rules are saved under `.cursor/rules/` directory:
|
|||
|
||||
### Frontend
|
||||
|
||||
- `react-component.mdc` – React component style guide and conventions
|
||||
- `react.mdc` – React component style guide and conventions
|
||||
- `i18n.mdc` – Internationalization guide using react-i18next
|
||||
- `typescript.mdc` – TypeScript code style guide
|
||||
- `packages/react-layout-kit.mdc` – Usage guide for react-layout-kit
|
||||
|
|
|
|||
13
CLAUDE.md
13
CLAUDE.md
|
|
@ -19,6 +19,7 @@ read @.cursor/rules/project-structure.mdc
|
|||
- git commit message should prefix with gitmoji
|
||||
- git branch name format example: tj/feat/feature-name
|
||||
- use .github/PULL_REQUEST_TEMPLATE.md to generate pull request description
|
||||
- PR titles starting with `✨ feat/` or `🐛 fix` will trigger the release workflow upon merge. Only use these prefixes for significant user-facing feature changes or bug fixes
|
||||
|
||||
### Package Management
|
||||
|
||||
|
|
@ -44,10 +45,12 @@ see @.cursor/rules/typescript.mdc
|
|||
- wrap the file path in single quotes to avoid shell expansion
|
||||
- Never run `bun run test` etc to run tests, this will run all tests and cost about 10mins
|
||||
- If trying to fix the same test twice, but still failed, stop and ask for help.
|
||||
- **Prefer `vi.spyOn` over `vi.mock`**: When mocking modules or functions, prefer using `vi.spyOn` to mock specific functions rather than `vi.mock` to mock entire modules. This approach is more targeted, easier to maintain, and allows for better control over mock behavior in individual tests.
|
||||
- **Tests must pass type check**: After writing or modifying tests, run `bun run type-check` to ensure there are no type errors. Tests should pass both runtime execution and TypeScript type checking.
|
||||
|
||||
### Typecheck
|
||||
|
||||
- use `bun run typecheck` to check type errors.
|
||||
- use `bun run type-check` to check type errors.
|
||||
|
||||
### i18n
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ see @.cursor/rules/typescript.mdc
|
|||
- **Dev**: Translate `locales/zh-CN/namespace.json` and `locales/en-US/namespace.json` locales file only for dev preview
|
||||
- DON'T run `pnpm i18n`, let CI auto handle it
|
||||
|
||||
## Linear Issue Management
|
||||
## Linear Issue Management (ignore if not installed linear mcp)
|
||||
|
||||
When working with Linear issues:
|
||||
|
||||
|
|
@ -64,6 +67,10 @@ When working with Linear issues:
|
|||
3. **Update issue status** when completing tasks using `mcp__linear-server__update_issue`
|
||||
4. **MUST add completion comment** using `mcp__linear-server__create_comment`
|
||||
|
||||
### Creating Issues
|
||||
|
||||
When creating new Linear issues using `mcp__linear-server__create_issue`, **MUST add the `claude code` label** to indicate the issue was created by Claude Code.
|
||||
|
||||
### Completion Comment (REQUIRED)
|
||||
|
||||
**Every time you complete an issue, you MUST add a comment summarizing the work done.** This is critical for:
|
||||
|
|
@ -79,7 +86,7 @@ When working with Linear issues:
|
|||
**Workflow for EACH individual issue:**
|
||||
|
||||
1. Complete the implementation for this specific issue
|
||||
2. Run type check: `bun run typecheck`
|
||||
2. Run type check: `bun run type-check`
|
||||
3. Run related tests if applicable
|
||||
4. Create PR if needed
|
||||
5. **IMMEDIATELY** update issue status to **"In Review"** (NOT "Done"): `mcp__linear-server__update_issue`
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ read @.cursor/rules/project-structure.mdc
|
|||
- git commit message should prefix with gitmoji
|
||||
- git branch name format example: tj/feat/feature-name
|
||||
- use .github/PULL_REQUEST_TEMPLATE.md to generate pull request description
|
||||
- PR titles starting with `✨ feat/` or `🐛 fix` will trigger the release workflow upon merge. Only use these prefixes for significant user-facing feature changes or bug fixes
|
||||
|
||||
### Package Management
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ see @.cursor/rules/typescript.mdc
|
|||
|
||||
### Typecheck
|
||||
|
||||
- use `bun run typecheck` to check type errors.
|
||||
- use `bun run type-check` to check type errors.
|
||||
|
||||
### i18n
|
||||
|
||||
|
|
|
|||
1
packages/memory-user-memory/.gitignore
vendored
Normal file
1
packages/memory-user-memory/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
datasets
|
||||
Loading…
Reference in a new issue