@lobehub/lobehub 2.0.0-next.353 → 2.0.0-next.354
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.
- package/.agents/skills/add-provider-doc/SKILL.md +90 -0
- package/.agents/skills/add-setting-env/SKILL.md +106 -0
- package/.agents/skills/debug/SKILL.md +66 -0
- package/.agents/skills/desktop/SKILL.md +78 -0
- package/.agents/skills/desktop/references/feature-implementation.md +99 -0
- package/.agents/skills/desktop/references/local-tools.md +133 -0
- package/.agents/skills/desktop/references/menu-config.md +103 -0
- package/.agents/skills/desktop/references/window-management.md +143 -0
- package/.agents/skills/drizzle/SKILL.md +129 -0
- package/.agents/skills/drizzle/references/db-migrations.md +50 -0
- package/.agents/skills/hotkey/SKILL.md +90 -0
- package/{.cursor/rules/i18n.mdc → .agents/skills/i18n/SKILL.md} +14 -23
- package/.agents/skills/linear/SKILL.md +51 -0
- package/.agents/skills/microcopy/SKILL.md +83 -0
- package/.agents/skills/modal/SKILL.md +102 -0
- package/{.cursor/rules/project-structure.mdc → .agents/skills/project-overview/SKILL.md} +65 -37
- package/.agents/skills/react/SKILL.md +73 -0
- package/.agents/skills/react/references/layout-kit.md +100 -0
- package/.agents/skills/recent-data/SKILL.md +108 -0
- package/.agents/skills/testing/SKILL.md +89 -0
- package/.agents/skills/testing/references/agent-runtime-e2e.md +126 -0
- package/.agents/skills/testing/references/db-model-test.md +124 -0
- package/.agents/skills/testing/references/desktop-controller-test.md +124 -0
- package/.agents/skills/testing/references/electron-ipc-test.md +63 -0
- package/.agents/skills/testing/references/zustand-store-action-test.md +150 -0
- package/.agents/skills/typescript/SKILL.md +52 -0
- package/.agents/skills/zustand/SKILL.md +78 -0
- package/.agents/skills/zustand/references/action-patterns.md +125 -0
- package/.agents/skills/zustand/references/slice-organization.md +125 -0
- package/AGENTS.md +42 -55
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +57 -46
- package/GEMINI.md +47 -39
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/src/features/FileViewer/Renderer/PDF/index.tsx +2 -3
- package/src/features/ShareModal/SharePdf/PdfPreview.tsx +1 -2
- package/src/libs/pdfjs/index.tsx +25 -0
- package/src/store/test-coverage.md +5 -5
- package/.cursor/rules/add-provider-doc.mdc +0 -183
- package/.cursor/rules/add-setting-env.mdc +0 -175
- package/.cursor/rules/cursor-rules.mdc +0 -28
- package/.cursor/rules/db-migrations.mdc +0 -46
- package/.cursor/rules/debug-usage.mdc +0 -86
- package/.cursor/rules/desktop-controller-tests.mdc +0 -189
- package/.cursor/rules/desktop-feature-implementation.mdc +0 -155
- package/.cursor/rules/desktop-local-tools-implement.mdc +0 -81
- package/.cursor/rules/desktop-menu-configuration.mdc +0 -209
- package/.cursor/rules/desktop-window-management.mdc +0 -301
- package/.cursor/rules/drizzle-schema-style-guide.mdc +0 -218
- package/.cursor/rules/hotkey.mdc +0 -162
- package/.cursor/rules/linear.mdc +0 -53
- package/.cursor/rules/microcopy-cn.mdc +0 -158
- package/.cursor/rules/microcopy-en.mdc +0 -148
- package/.cursor/rules/modal-imperative.mdc +0 -162
- package/.cursor/rules/packages/react-layout-kit.mdc +0 -122
- package/.cursor/rules/project-introduce.mdc +0 -36
- package/.cursor/rules/react.mdc +0 -169
- package/.cursor/rules/recent-data-usage.mdc +0 -139
- package/.cursor/rules/rules-index.mdc +0 -44
- package/.cursor/rules/testing-guide/agent-runtime-e2e.mdc +0 -285
- package/.cursor/rules/testing-guide/db-model-test.mdc +0 -455
- package/.cursor/rules/testing-guide/electron-ipc-test.mdc +0 -80
- package/.cursor/rules/testing-guide/testing-guide.mdc +0 -534
- package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +0 -574
- package/.cursor/rules/typescript.mdc +0 -55
- package/.cursor/rules/zustand-action-patterns.mdc +0 -328
- package/.cursor/rules/zustand-slice-organization.mdc +0 -308
- package/src/libs/pdfjs/pdf.worker.ts +0 -1
- package/src/libs/pdfjs/worker.ts +0 -12
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/AGENTS.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/SKILL.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-event-handler-refs.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-use-latest.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-api-routes.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-defer-await.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-parallel.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-suspense-boundaries.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-barrel-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-conditional.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-defer-third-party.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-dynamic-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-preload.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-localstorage-schema.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-passive-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-swr-dedup.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-batch-dom-css.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-function-results.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-property-access.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-storage.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-combine-iterations.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-early-exit.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-hoist-regexp.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-index-maps.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-length-check-first.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-min-max-loop.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-set-map-lookups.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-tosorted-immutable.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-activity.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-animate-svg-wrapper.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-conditional-render.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-content-visibility.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hoist-jsx.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hydration-no-flicker.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-svg-precision.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-defer-reads.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-derived-state.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-functional-setstate.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-lazy-state-init.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-memo.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-transitions.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-after-nonblocking.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-lru.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-react.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-parallel-fetching.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-serialization.md +0 -0
|
@@ -1,139 +0,0 @@
|
|
|
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
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
interface RecentTopic {
|
|
87
|
-
agent: {
|
|
88
|
-
avatar: string | null;
|
|
89
|
-
backgroundColor: string | null;
|
|
90
|
-
id: string;
|
|
91
|
-
title: string | null;
|
|
92
|
-
} | null;
|
|
93
|
-
id: string;
|
|
94
|
-
title: string | null;
|
|
95
|
-
updatedAt: Date;
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Recent Resources (最近文件)
|
|
100
|
-
|
|
101
|
-
```tsx
|
|
102
|
-
import { recentSelectors } from '@/store/session/selectors';
|
|
103
|
-
|
|
104
|
-
// 数据
|
|
105
|
-
const recentResources = useSessionStore(recentSelectors.recentResources);
|
|
106
|
-
// 类型: FileListItem[]
|
|
107
|
-
|
|
108
|
-
// 初始化状态
|
|
109
|
-
const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
|
|
110
|
-
// 类型: boolean
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Recent Pages (最近页面)
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
import { recentSelectors } from '@/store/session/selectors';
|
|
117
|
-
|
|
118
|
-
// 数据
|
|
119
|
-
const recentPages = useSessionStore(recentSelectors.recentPages);
|
|
120
|
-
// 类型: any[]
|
|
121
|
-
|
|
122
|
-
// 初始化状态
|
|
123
|
-
const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
|
|
124
|
-
// 类型: boolean
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## 特性
|
|
128
|
-
|
|
129
|
-
1. **自动登录检测**:只有在用户登录时才会加载数据
|
|
130
|
-
2. **数据缓存**:数据存储在 store 中,多处使用无需重复加载
|
|
131
|
-
3. **自动刷新**:使用 SWR,在用户重新聚焦时自动刷新(5分钟间隔)
|
|
132
|
-
4. **类型安全**:完整的 TypeScript 类型定义
|
|
133
|
-
|
|
134
|
-
## 最佳实践
|
|
135
|
-
|
|
136
|
-
1. **初始化位置**:在应用顶层统一初始化所有 recent 数据
|
|
137
|
-
2. **数据访问**:使用 selectors 从 store 读取数据
|
|
138
|
-
3. **多处使用**:同一数据在多个组件中使用时,推荐使用方式一(直接从 store 读取)
|
|
139
|
-
4. **性能优化**:使用 selector 确保只有相关数据变化时才重新渲染
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description:
|
|
3
|
-
globs:
|
|
4
|
-
alwaysApply: true
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Available project rules index
|
|
8
|
-
|
|
9
|
-
All following rules are saved under `.cursor/rules/` directory:
|
|
10
|
-
|
|
11
|
-
## Backend
|
|
12
|
-
|
|
13
|
-
- `drizzle-schema-style-guide.mdc` – Style guide for defining Drizzle ORM schemas
|
|
14
|
-
|
|
15
|
-
## Frontend
|
|
16
|
-
|
|
17
|
-
- `react.mdc` – React component style guide and conventions
|
|
18
|
-
- `i18n.mdc` – Internationalization guide using react-i18next
|
|
19
|
-
- `typescript.mdc` – TypeScript code style guide
|
|
20
|
-
- `packages/react-layout-kit.mdc` – Usage guide for react-layout-kit
|
|
21
|
-
- `modal-imperative.mdc` – Modal imperative API usage guide (createRawModal/createModal)
|
|
22
|
-
|
|
23
|
-
## State Management
|
|
24
|
-
|
|
25
|
-
- `zustand-action-patterns.mdc` – Recommended patterns for organizing Zustand actions
|
|
26
|
-
- `zustand-slice-organization.mdc` – Best practices for structuring Zustand slices
|
|
27
|
-
|
|
28
|
-
## Desktop (Electron)
|
|
29
|
-
|
|
30
|
-
- `desktop-feature-implementation.mdc` – Implementing new Electron desktop features
|
|
31
|
-
- `desktop-controller-tests.mdc` – Desktop controller unit testing guide
|
|
32
|
-
- `desktop-local-tools-implement.mdc` – Workflow to add new desktop local tools
|
|
33
|
-
- `desktop-menu-configuration.mdc` – Desktop menu configuration guide
|
|
34
|
-
- `desktop-window-management.mdc` – Desktop window management guide
|
|
35
|
-
|
|
36
|
-
## Debugging
|
|
37
|
-
|
|
38
|
-
- `debug-usage.mdc` – Using the debug package and namespace conventions
|
|
39
|
-
|
|
40
|
-
## Testing
|
|
41
|
-
|
|
42
|
-
- `testing-guide/testing-guide.mdc` – Comprehensive testing guide for Vitest
|
|
43
|
-
- `testing-guide/electron-ipc-test.mdc` – Electron IPC interface testing strategy
|
|
44
|
-
- `testing-guide/db-model-test.mdc` – Database Model testing guide
|
|
@@ -1,285 +0,0 @@
|
|
|
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:*` 环境变量查看详细日志
|