@lobehub/lobehub 2.0.0-next.173 → 2.0.0-next.175

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.cursor/rules/db-migrations.mdc +10 -4
  2. package/.cursor/rules/drizzle-schema-style-guide.mdc +33 -17
  3. package/.cursor/rules/hotkey.mdc +161 -0
  4. package/.cursor/rules/i18n.mdc +2 -5
  5. package/.cursor/rules/project-introduce.mdc +3 -2
  6. package/.cursor/rules/project-structure.mdc +33 -37
  7. package/.cursor/rules/react.mdc +155 -0
  8. package/.cursor/rules/recent-data-usage.mdc +138 -0
  9. package/.cursor/rules/rules-index.mdc +1 -1
  10. package/.cursor/rules/testing-guide/agent-runtime-e2e.mdc +285 -0
  11. package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +11 -16
  12. package/.cursor/rules/typescript.mdc +0 -4
  13. package/.cursor/rules/zustand-action-patterns.mdc +137 -169
  14. package/.cursor/rules/zustand-slice-organization.mdc +16 -8
  15. package/.husky/pre-commit +1 -1
  16. package/.i18nrc.js +3 -1
  17. package/AGENTS.md +3 -2
  18. package/CHANGELOG.md +42 -0
  19. package/CLAUDE.md +10 -3
  20. package/GEMINI.md +2 -1
  21. package/README.md +9 -9
  22. package/README.zh-CN.md +12 -12
  23. package/changelog/v1.json +14 -0
  24. package/docs/development/database-schema.dbml +18 -3
  25. package/docs/development/state-management/state-management-selectors.mdx +1 -1
  26. package/glossary.json +8 -0
  27. package/package.json +3 -3
  28. package/packages/database/migrations/0063_add_columns_for_several_tables.sql +30 -0
  29. package/packages/database/migrations/0064_add_agents_session_group_id.sql +7 -0
  30. package/packages/database/migrations/meta/0063_snapshot.json +9113 -0
  31. package/packages/database/migrations/meta/0064_snapshot.json +9143 -0
  32. package/packages/database/migrations/meta/_journal.json +14 -0
  33. package/packages/database/src/core/migrations.json +39 -0
  34. package/packages/database/src/schemas/_helpers.ts +5 -1
  35. package/packages/database/src/schemas/agent.ts +8 -1
  36. package/packages/database/src/schemas/aiInfra.ts +18 -3
  37. package/packages/database/src/schemas/message.ts +11 -6
  38. package/packages/database/src/schemas/topic.ts +17 -2
  39. package/packages/database/src/schemas/user.ts +5 -2
  40. package/packages/database/src/schemas/userMemories.ts +9 -8
  41. package/packages/types/src/topic/thread.ts +24 -0
  42. package/packages/types/src/user/index.ts +1 -0
  43. package/packages/types/src/user/onboarding.ts +16 -0
  44. package/vitest.config.mts +3 -0
  45. package/.cursor/rules/group-chat.mdc +0 -35
  46. package/.cursor/rules/react-component.mdc +0 -173
@@ -0,0 +1,138 @@
1
+ # Recent Data 使用指南
2
+
3
+ ## 概述
4
+
5
+ Recent 数据(recentTopics, recentResources, recentPages)存储在 session store 中,可以在应用的任何地方访问。
6
+
7
+ ## 数据初始化
8
+
9
+ 在应用顶层(如 `RecentHydration.tsx`)中初始化所有 recent 数据:
10
+
11
+ ```tsx
12
+ import { useInitRecentPage } from '@/hooks/useInitRecentPage';
13
+ import { useInitRecentResource } from '@/hooks/useInitRecentResource';
14
+ import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
15
+
16
+ const App = () => {
17
+ // 初始化所有 recent 数据
18
+ useInitRecentTopic();
19
+ useInitRecentResource();
20
+ useInitRecentPage();
21
+
22
+ return <YourComponents />;
23
+ };
24
+ ```
25
+
26
+ ## 使用方式
27
+
28
+ ### 方式一:直接从 Store 读取(推荐用于多处使用)
29
+
30
+ 在任何组件中直接访问 store 中的数据:
31
+
32
+ ```tsx
33
+ import { useSessionStore } from '@/store/session';
34
+ import { recentSelectors } from '@/store/session/selectors';
35
+
36
+ const Component = () => {
37
+ // 读取数据
38
+ const recentTopics = useSessionStore(recentSelectors.recentTopics);
39
+ const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
40
+
41
+ if (!isInit) return <div>Loading...</div>;
42
+
43
+ return (
44
+ <div>
45
+ {recentTopics.map(topic => (
46
+ <div key={topic.id}>{topic.title}</div>
47
+ ))}
48
+ </div>
49
+ );
50
+ };
51
+ ```
52
+
53
+ ### 方式二:使用 Hook 返回的数据(用于单一组件)
54
+
55
+ ```tsx
56
+ import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
57
+
58
+ const Component = () => {
59
+ const { data: recentTopics, isLoading } = useInitRecentTopic();
60
+
61
+ if (isLoading) return <div>Loading...</div>;
62
+
63
+ return <div>{/* 使用 recentTopics */}</div>;
64
+ };
65
+ ```
66
+
67
+ ## 可用的 Selectors
68
+
69
+ ### Recent Topics (最近话题)
70
+
71
+ ```tsx
72
+ import { recentSelectors } from '@/store/session/selectors';
73
+
74
+ // 数据
75
+ const recentTopics = useSessionStore(recentSelectors.recentTopics);
76
+ // 类型: RecentTopic[]
77
+
78
+ // 初始化状态
79
+ const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
80
+ // 类型: boolean
81
+ ```
82
+
83
+ **RecentTopic 类型:**
84
+ ```typescript
85
+ interface RecentTopic {
86
+ agent: {
87
+ avatar: string | null;
88
+ backgroundColor: string | null;
89
+ id: string;
90
+ title: string | null;
91
+ } | null;
92
+ id: string;
93
+ title: string | null;
94
+ updatedAt: Date;
95
+ }
96
+ ```
97
+
98
+ ### Recent Resources (最近文件)
99
+
100
+ ```tsx
101
+ import { recentSelectors } from '@/store/session/selectors';
102
+
103
+ // 数据
104
+ const recentResources = useSessionStore(recentSelectors.recentResources);
105
+ // 类型: FileListItem[]
106
+
107
+ // 初始化状态
108
+ const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
109
+ // 类型: boolean
110
+ ```
111
+
112
+ ### Recent Pages (最近页面)
113
+
114
+ ```tsx
115
+ import { recentSelectors } from '@/store/session/selectors';
116
+
117
+ // 数据
118
+ const recentPages = useSessionStore(recentSelectors.recentPages);
119
+ // 类型: any[]
120
+
121
+ // 初始化状态
122
+ const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
123
+ // 类型: boolean
124
+ ```
125
+
126
+ ## 特性
127
+
128
+ 1. **自动登录检测**:只有在用户登录时才会加载数据
129
+ 2. **数据缓存**:数据存储在 store 中,多处使用无需重复加载
130
+ 3. **自动刷新**:使用 SWR,在用户重新聚焦时自动刷新(5分钟间隔)
131
+ 4. **类型安全**:完整的 TypeScript 类型定义
132
+
133
+ ## 最佳实践
134
+
135
+ 1. **初始化位置**:在应用顶层统一初始化所有 recent 数据
136
+ 2. **数据访问**:使用 selectors 从 store 读取数据
137
+ 3. **多处使用**:同一数据在多个组件中使用时,推荐使用方式一(直接从 store 读取)
138
+ 4. **性能优化**:使用 selector 确保只有相关数据变化时才重新渲染
@@ -14,7 +14,7 @@ All following rules are saved under `.cursor/rules/` directory:
14
14
 
15
15
  ## Frontend
16
16
 
17
- - `react-component.mdc` – React component style guide and conventions
17
+ - `react.mdc` – React component style guide and conventions
18
18
  - `i18n.mdc` – Internationalization guide using react-i18next
19
19
  - `typescript.mdc` – TypeScript code style guide
20
20
  - `packages/react-layout-kit.mdc` – Usage guide for react-layout-kit
@@ -0,0 +1,285 @@
1
+ # Agent Runtime E2E 测试指南
2
+
3
+ 本文档描述 Agent Runtime 端到端测试的核心原则和实施方法。
4
+
5
+ ## 核心原则
6
+
7
+ ### 1. 最小化 Mock 原则
8
+
9
+ E2E 测试的目标是尽可能接近真实运行环境。因此,我们只 Mock **三个外部依赖**:
10
+
11
+ | 依赖 | Mock 方式 | 说明 |
12
+ | --- | --- | --- |
13
+ | **Database** | PGLite | 使用 `@lobechat/database/test-utils` 提供的内存数据库 |
14
+ | **Redis** | InMemoryAgentStateManager | Mock `AgentStateManager` 使用内存实现 |
15
+ | **Redis** | InMemoryStreamEventManager | Mock `StreamEventManager` 使用内存实现 |
16
+
17
+ **不 Mock 的部分:**
18
+
19
+ - `model-bank` - 使用真实的模型配置数据
20
+ - `Mecha` (AgentToolsEngine, ContextEngineering) - 使用真实逻辑
21
+ - `AgentRuntimeService` - 使用真实逻辑
22
+ - `AgentRuntimeCoordinator` - 使用真实逻辑
23
+
24
+ ### 2. 使用 vi.spyOn 而非 vi.mock
25
+
26
+ 不同测试场景需要不同的 LLM 响应。使用 `vi.spyOn` 可以:
27
+
28
+ - 在每个测试中灵活控制返回值
29
+ - 便于测试不同场景(纯文本、tool calls、错误等)
30
+ - 避免全局 mock 导致的测试隔离问题
31
+
32
+ ### 3. 默认模型使用 gpt-5
33
+
34
+ - `model-bank` 中肯定有该模型的数据
35
+ - 避免短期内因模型更新需要修改测试
36
+
37
+ ## 技术实现
38
+
39
+ ### 数据库设置
40
+
41
+ ```typescript
42
+ import { LobeChatDatabase } from '@lobechat/database';
43
+ import { getTestDB } from '@lobechat/database/test-utils';
44
+
45
+ let testDB: LobeChatDatabase;
46
+
47
+ beforeEach(async () => {
48
+ testDB = await getTestDB();
49
+ });
50
+ ```
51
+
52
+ ### OpenAI Response Mock Helper
53
+
54
+ 创建一个 helper 函数来生成 OpenAI 格式的流式响应:
55
+
56
+ ```typescript
57
+ /**
58
+ * 创建 OpenAI 格式的流式响应
59
+ */
60
+ export const createOpenAIStreamResponse = (options: {
61
+ content?: string;
62
+ toolCalls?: Array<{
63
+ id: string;
64
+ name: string;
65
+ arguments: string;
66
+ }>;
67
+ finishReason?: 'stop' | 'tool_calls';
68
+ }) => {
69
+ const { content, toolCalls, finishReason = 'stop' } = options;
70
+
71
+ return new Response(
72
+ new ReadableStream({
73
+ start(controller) {
74
+ const encoder = new TextEncoder();
75
+
76
+ // 发送内容 chunk
77
+ if (content) {
78
+ const chunk = {
79
+ id: 'chatcmpl-mock',
80
+ object: 'chat.completion.chunk',
81
+ model: 'gpt-5',
82
+ choices: [
83
+ {
84
+ index: 0,
85
+ delta: { content },
86
+ finish_reason: null,
87
+ },
88
+ ],
89
+ };
90
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
91
+ }
92
+
93
+ // 发送 tool_calls chunk
94
+ if (toolCalls) {
95
+ for (const tool of toolCalls) {
96
+ const chunk = {
97
+ id: 'chatcmpl-mock',
98
+ object: 'chat.completion.chunk',
99
+ model: 'gpt-5',
100
+ choices: [
101
+ {
102
+ index: 0,
103
+ delta: {
104
+ tool_calls: [
105
+ {
106
+ index: 0,
107
+ id: tool.id,
108
+ type: 'function',
109
+ function: {
110
+ name: tool.name,
111
+ arguments: tool.arguments,
112
+ },
113
+ },
114
+ ],
115
+ },
116
+ finish_reason: null,
117
+ },
118
+ ],
119
+ };
120
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
121
+ }
122
+ }
123
+
124
+ // 发送完成 chunk
125
+ const finishChunk = {
126
+ id: 'chatcmpl-mock',
127
+ object: 'chat.completion.chunk',
128
+ model: 'gpt-5',
129
+ choices: [
130
+ {
131
+ index: 0,
132
+ delta: {},
133
+ finish_reason: finishReason,
134
+ },
135
+ ],
136
+ };
137
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
138
+ controller.enqueue(encoder.encode('data: [DONE]\n\n'));
139
+ controller.close();
140
+ },
141
+ }),
142
+ { headers: { 'content-type': 'text/event-stream' } },
143
+ );
144
+ };
145
+ ```
146
+
147
+ ### 内存状态管理
148
+
149
+ 使用依赖注入替代 Redis:
150
+
151
+ ```typescript
152
+ import {
153
+ InMemoryAgentStateManager,
154
+ InMemoryStreamEventManager,
155
+ } from '@/server/modules/AgentRuntime';
156
+ import { AgentRuntimeService } from '@/server/services/agentRuntime';
157
+
158
+ const stateManager = new InMemoryAgentStateManager();
159
+ const streamEventManager = new InMemoryStreamEventManager();
160
+
161
+ const service = new AgentRuntimeService(serverDB, userId, {
162
+ coordinatorOptions: {
163
+ stateManager,
164
+ streamEventManager,
165
+ },
166
+ queueService: null, // 禁用 QStash 队列,使用 executeSync
167
+ streamEventManager,
168
+ });
169
+ ```
170
+
171
+ ### Mock OpenAI API
172
+
173
+ 在测试中使用 `vi.spyOn` mock fetch:
174
+
175
+ ```typescript
176
+ import { vi } from 'vitest';
177
+
178
+ // 在测试文件顶部或 beforeEach 中
179
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
180
+
181
+ // 在具体测试中设置返回值
182
+ it('should handle text response', async () => {
183
+ fetchSpy.mockResolvedValueOnce(createOpenAIStreamResponse({ content: '杭州今天天气晴朗' }));
184
+
185
+ // ... 执行测试
186
+ });
187
+
188
+ it('should handle tool calls', async () => {
189
+ fetchSpy.mockResolvedValueOnce(
190
+ createOpenAIStreamResponse({
191
+ toolCalls: [
192
+ {
193
+ id: 'call_123',
194
+ name: 'lobe-web-browsing____search____builtin',
195
+ arguments: JSON.stringify({ query: '杭州天气' }),
196
+ },
197
+ ],
198
+ finishReason: 'tool_calls',
199
+ }),
200
+ );
201
+
202
+ // ... 执行测试
203
+ });
204
+ ```
205
+
206
+ ## 测试场景
207
+
208
+ ### 1. 基本对话测试
209
+
210
+ ```typescript
211
+ describe('Basic Chat', () => {
212
+ it('should complete a simple conversation', async () => {
213
+ fetchSpy.mockResolvedValueOnce(
214
+ createOpenAIStreamResponse({ content: 'Hello! How can I help you?' }),
215
+ );
216
+
217
+ const result = await service.createOperation({
218
+ agentConfig: { model: 'gpt-5', provider: 'openai' },
219
+ initialMessages: [{ role: 'user', content: 'Hi' }],
220
+ // ...
221
+ });
222
+
223
+ const finalState = await service.executeSync(result.operationId);
224
+ expect(finalState.status).toBe('done');
225
+ });
226
+ });
227
+ ```
228
+
229
+ ### 2. Tool 调用测试
230
+
231
+ ```typescript
232
+ describe('Tool Calls', () => {
233
+ it('should execute web-browsing tool', async () => {
234
+ // 第一次调用:LLM 返回 tool_calls
235
+ fetchSpy.mockResolvedValueOnce(
236
+ createOpenAIStreamResponse({
237
+ toolCalls: [
238
+ {
239
+ id: 'call_123',
240
+ name: 'lobe-web-browsing____search____builtin',
241
+ arguments: JSON.stringify({ query: '杭州天气' }),
242
+ },
243
+ ],
244
+ finishReason: 'tool_calls',
245
+ }),
246
+ );
247
+
248
+ // 第二次调用:处理 tool 结果后的响应
249
+ fetchSpy.mockResolvedValueOnce(
250
+ createOpenAIStreamResponse({ content: '根据搜索结果,杭州今天...' }),
251
+ );
252
+
253
+ // ... 执行测试
254
+ });
255
+ });
256
+ ```
257
+
258
+ ### 3. 错误处理测试
259
+
260
+ ```typescript
261
+ describe('Error Handling', () => {
262
+ it('should handle API errors gracefully', async () => {
263
+ fetchSpy.mockRejectedValueOnce(new Error('API rate limit exceeded'));
264
+
265
+ // ... 执行测试并验证错误处理
266
+ });
267
+ });
268
+ ```
269
+
270
+ ## 文件组织
271
+
272
+ ```
273
+ src/server/routers/lambda/__tests__/integration/
274
+ ├── setup.ts # 测试设置工具
275
+ ├── aiAgent.integration.test.ts # 现有集成测试
276
+ ├── aiAgent.e2e.test.ts # E2E 测试
277
+ └── helpers/
278
+ └── openaiMock.ts # OpenAI mock helper
279
+ ```
280
+
281
+ ## 注意事项
282
+
283
+ 1. **测试隔离**:每个测试后清理 `InMemoryAgentStateManager` 和 `InMemoryStreamEventManager`
284
+ 2. **超时设置**:E2E 测试可能需要更长的超时时间
285
+ 3. **调试**:使用 `DEBUG=lobe-server:*` 环境变量查看详细日志
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Best practices for testing Zustand store actions
3
- globs: "src/store/**/*.test.ts"
3
+ globs: 'src/store/**/*.test.ts'
4
4
  alwaysApply: false
5
5
  ---
6
6
 
@@ -15,6 +15,7 @@ import { act, renderHook } from '@testing-library/react';
15
15
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
16
16
 
17
17
  import { messageService } from '@/services/message';
18
+
18
19
  import { useChatStore } from '../../store';
19
20
 
20
21
  // Keep zustand mock as it's needed globally
@@ -229,8 +230,7 @@ it('should handle topic creation flow', async () => {
229
230
  const { result } = renderHook(() => useChatStore());
230
231
 
231
232
  // Spy on action dependencies
232
- const createTopicSpy = vi.spyOn(result.current, 'createTopic')
233
- .mockResolvedValue('new-topic-id');
233
+ const createTopicSpy = vi.spyOn(result.current, 'createTopic').mockResolvedValue('new-topic-id');
234
234
  const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleMessageLoading');
235
235
 
236
236
  // Execute
@@ -251,9 +251,7 @@ When testing streaming responses, simulate the flow properly:
251
251
  ```typescript
252
252
  it('should handle streaming chunks', async () => {
253
253
  const { result } = renderHook(() => useChatStore());
254
- const messages = [
255
- { id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' },
256
- ];
254
+ const messages = [{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' }];
257
255
 
258
256
  const streamSpy = vi
259
257
  .spyOn(chatService, 'createAssistantMessageStream')
@@ -287,9 +285,7 @@ Always test error scenarios:
287
285
  it('should handle errors gracefully', async () => {
288
286
  const { result } = renderHook(() => useChatStore());
289
287
 
290
- vi.spyOn(messageService, 'createMessage').mockRejectedValue(
291
- new Error('create message error'),
292
- );
288
+ vi.spyOn(messageService, 'createMessage').mockRejectedValue(new Error('create message error'));
293
289
 
294
290
  await act(async () => {
295
291
  try {
@@ -330,8 +326,7 @@ it('should test something', async () => {
330
326
  it('should call internal methods', async () => {
331
327
  const { result } = renderHook(() => useChatStore());
332
328
 
333
- const internalMethodSpy = vi.spyOn(result.current, 'internal_method')
334
- .mockResolvedValue();
329
+ const internalMethodSpy = vi.spyOn(result.current, 'internal_method').mockResolvedValue();
335
330
 
336
331
  await act(async () => {
337
332
  await result.current.publicMethod();
@@ -456,6 +451,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
456
451
 
457
452
  import { discoverService } from '@/services/discover';
458
453
  import { globalHelpers } from '@/store/global/helpers';
454
+
459
455
  import { useDiscoverStore as useStore } from '../../store';
460
456
 
461
457
  vi.mock('zustand/traditional');
@@ -486,6 +482,7 @@ describe('SWR Hook Actions', () => {
486
482
  ```
487
483
 
488
484
  **Key points**:
485
+
489
486
  - **DO NOT mock useSWR** - let it use the real implementation
490
487
  - Only mock the **service methods** (fetchers)
491
488
  - Use `waitFor` from `@testing-library/react` to wait for async operations
@@ -559,21 +556,19 @@ it('should not fetch when required parameter is missing', () => {
559
556
  7. **Type assertions**: Use `as any` for test mock data where type definitions are strict
560
557
 
561
558
  **Why this matters**:
559
+
562
560
  - The fetcher (service method) is what we're testing - it must be called
563
561
  - Hardcoding the return value bypasses the actual fetcher logic
564
562
  - SWR returns Promises in real usage, tests should mirror this behavior
565
563
 
566
564
  ## Benefits of This Approach
567
565
 
568
- ✅ **Clear test layers** - Each test only spies on direct dependencies
569
- ✅ **Correct mocks** - Mocks match actual implementation
570
- ✅ **Better maintainability** - Changes to implementation require fewer test updates
571
- ✅ **Improved coverage** - Structured approach ensures all branches are tested
572
- ✅ **Reduced coupling** - Tests are independent and can run in any order
566
+ ✅ **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
573
567
 
574
568
  ## Reference
575
569
 
576
570
  See example implementation in:
571
+
577
572
  - `src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts` (Regular actions)
578
573
  - `src/store/discover/slices/plugin/action.test.ts` (SWR hooks)
579
574
  - `src/store/discover/slices/mcp/action.test.ts` (SWR hooks)
@@ -16,10 +16,6 @@ alwaysApply: false
16
16
  - prefer `@ts-expect-error` over `@ts-ignore` over `as any`
17
17
  - Avoid meaningless null/undefined parameters; design strict function contracts.
18
18
 
19
- ## Imports and Modules
20
-
21
- - When importing a directory module, prefer the explicit index path like `@/db/index` instead of `@/db`.
22
-
23
19
  ## Asynchronous Patterns and Concurrency
24
20
 
25
21
  - Prefer `async`/`await` over callbacks or chained `.then` promises.