feat(share): add topic sharing functionality (#11448)

This commit is contained in:
YuTengjing
2026-01-13 22:10:48 +08:00
committed by GitHub
parent 97a091d358
commit ddca1652bb
70 changed files with 12470 additions and 696 deletions
+1 -1
View File
@@ -43,4 +43,4 @@ DROP TABLE "old_table";
CREATE INDEX "users_email_idx" ON "users" ("email");
```
**Important**: After modifying migration SQL (e.g., adding `IF NOT EXISTS` clauses), run `bun run db:generate-client` to update the hash in `packages/database/src/core/migrations.json`.
**Important**: After modifying migration SQL (e.g., adding `IF NOT EXISTS` clauses), run `bun run db:generate:client` to update the hash in `packages/database/src/core/migrations.json`.
+7 -5
View File
@@ -3,9 +3,10 @@ description: 包含添加 console.log 日志请求时
globs:
alwaysApply: false
---
# Debug 包使用指南
本项目使用 [debug](mdc:https:/github.com/debug-js/debug) 包进行调试日志记录。使用此规则来确保团队成员统一调试日志格式。
本项目使用 `debug` 包进行调试日志记录。使用此规则来确保团队成员统一调试日志格式。
## 基本用法
@@ -15,14 +16,14 @@ alwaysApply: false
import debug from 'debug';
```
2. 创建一个命名空间的日志记录器:
1. 创建一个命名空间的日志记录器:
```typescript
// 格式: lobe:[模块]:[子模块]
const log = debug('lobe-[模块名]:[子模块名]');
```
3. 使用日志记录器:
1. 使用日志记录器:
```typescript
log('简单消息');
@@ -46,7 +47,7 @@ log('格式化数字: %d', number);
## 示例
查看 [market/index.ts](mdc:src/server/routers/edge/market/index.ts) 中的使用示例:
查看 `src/server/routers/edge/market/index.ts` 中的使用示例:
```typescript
import debug from 'debug';
@@ -63,8 +64,9 @@ log('getAgent input: %O', input);
### 在浏览器中
在控制台执行:
```javascript
localStorage.debug = 'lobe-*'
localStorage.debug = 'lobe-*';
```
### 在 Node.js 环境中
+2 -1
View File
@@ -3,13 +3,14 @@ description: 桌面端测试
globs:
alwaysApply: false
---
# 桌面端控制器单元测试指南
## 测试框架与目录结构
LobeChat 桌面端使用 Vitest 作为测试框架。控制器的单元测试应放置在对应控制器文件同级的 `__tests__` 目录下,并以原控制器文件名加 `.test.ts` 作为文件名。
```
```plaintext
apps/desktop/src/main/controllers/
├── __tests__/
│ ├── index.test.ts
@@ -3,7 +3,8 @@ description: 当要做 electron 相关工作时
globs:
alwaysApply: false
---
**桌面端新功能实现指南**
# 桌面端新功能实现指南
## 桌面端应用架构概述
@@ -26,6 +27,7 @@ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架
### 1. 确定功能需求与设计
首先确定新功能的需求和设计,包括:
- 功能描述和用例
- 是否需要系统级API(如文件系统、网络等)
- UI/UX设计(如必要)
@@ -64,9 +66,10 @@ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架
```typescript
// src/services/electron/newFeatureService.ts
import { ensureElectronIpc } from '@/utils/electron/ipc';
import type { NewFeatureParams } from '@lobechat/electron-client-ipc';
import { ensureElectronIpc } from '@/utils/electron/ipc';
const ipc = ensureElectronIpc();
export const newFeatureService = async (params: NewFeatureParams) => {
@@ -84,7 +87,7 @@ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架
### 5. 如果是新增内置工具,遵循工具实现流程
参考 [desktop-local-tools-implement.mdc](mdc:desktop-local-tools-implement.mdc) 了解更多关于添加内置工具的详细步骤。
参考 `desktop-local-tools-implement.mdc` 了解更多关于添加内置工具的详细步骤。
### 6. 添加测试
@@ -120,12 +123,13 @@ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架
```typescript
// apps/desktop/src/main/controllers/NotificationCtr.ts
import { Notification } from 'electron';
import { ControllerModule, IpcMethod } from '@/controllers';
import type {
DesktopNotificationResult,
ShowDesktopNotificationParams,
} from '@lobechat/electron-client-ipc';
import { Notification } from 'electron';
import { ControllerModule, IpcMethod } from '@/controllers';
export default class NotificationCtr extends ControllerModule {
static override readonly groupName = 'notification';
+67 -66
View File
@@ -3,78 +3,79 @@ description:
globs:
alwaysApply: false
---
**新增桌面端工具流程:**
1. **定义工具接口 (Manifest):**
* **文件:** `src/tools/[tool_category]/index.ts` (例如: `src/tools/local-files/index.ts`)
* **操作:**
* 在 `ApiName` 对象(例如 `LocalFilesApiName`)中添加一个新的、唯一的 API 名称。
* 在 `Manifest` 对象(例如 `LocalFilesManifest`)的 `api` 数组中,新增一个对象来定义新工具的接口。
* **关键字段:**
* `name`: 使用上一步定义的 API 名称。
* `description`: 清晰描述工具的功能,供 Agent 理解和向用户展示。
* `parameters`: 使用 JSON Schema 定义工具所需的输入参数。
* `type`: 通常是 'object'。
* `properties`: 定义每个参数的名称、`description`、`type` (string, number, boolean, array, etc.),使用英文。
* `required`: 一个字符串数组,列出必须提供的参数名称。
1. **定义工具接口 (Manifest):**
- **文件:** `src/tools/[tool_category]/index.ts` (例如: `src/tools/local-files/index.ts`)
- **操作:**
- 在 `ApiName` 对象(例如 `LocalFilesApiName`)中添加一个新的、唯一的 API 名称。
- 在 `Manifest` 对象(例如 `LocalFilesManifest`)的 `api` 数组中,新增一个对象来定义新工具的接口。
- **关键字段:**
- `name`: 使用上一步定义的 API 名称。
- `description`: 清晰描述工具的功能,供 Agent 理解和向用户展示。
- `parameters`: 使用 JSON Schema 定义工具所需的输入参数。
- `type`: 通常是 'object'。
- `properties`: 定义每个参数的名称、`description`、`type` (string, number, boolean, array, etc.),使用英文。
- `required`: 一个字符串数组,列出必须提供的参数名称。
2. **定义相关类型:**
* **文件 1:** `packages/electron-client-ipc/src/types.ts` (或类似的共享 IPC 类型文件)
* **操作:** 定义传递给 IPC 事件的参数类型接口 (例如: `RenameLocalFileParams`, `MoveLocalFileParams`)。确保与 Manifest 中定义的 `parameters` 一致。
* **文件 2:** `src/tools/[tool_category]/type.ts` (例如: `src/tools/local-files/type.ts`)
* **操作:** 定义此工具执行后,存储在前端 Zustand Store 中的状态类型接口 (例如: `LocalRenameFileState`, `LocalMoveFileState`)。这通常包含操作结果(成功/失败)、错误信息以及相关数据(如旧路径、新路径等)。
2. **定义相关类型:**
- **文件 1:** `packages/electron-client-ipc/src/types.ts` (或类似的共享 IPC 类型文件)
- **操作:** 定义传递给 IPC 事件的参数类型接口 (例如: `RenameLocalFileParams`, `MoveLocalFileParams`)。确保与 Manifest 中定义的 `parameters` 一致。
- **文件 2:** `src/tools/[tool_category]/type.ts` (例如: `src/tools/local-files/type.ts`)
- **操作:** 定义此工具执行后,存储在前端 Zustand Store 中的状态类型接口 (例如: `LocalRenameFileState`, `LocalMoveFileState`)。这通常包含操作结果(成功/失败)、错误信息以及相关数据(如旧路径、新路径等)。
3. **实现前端状态管理 (Store Action):**
* **文件:** `src/store/chat/slices/builtinTool/actions/[tool_category].ts` (例如: `src/store/chat/slices/builtinTool/actions/localFile.ts`)
* **操作:**
* 导入在步骤 2 中定义的 IPC 参数类型和状态类型。
* 在 Action 接口 (例如: `LocalFileAction`) 中添加新 Action 的方法签名,使用对应的 IPC 参数类型。
* 在 `createSlice` (例如: `localFileSlice`) 中实现该 Action 方法:
* 接收 `id` (消息 ID) 和 `params` (符合 IPC 参数类型)。
* 设置加载状态 (`toggleLocalFileLoading(id, true)`)。
* 调用对应的 `Service` 层方法 (见步骤 4),传递 `params`。
* 使用 `try...catch` 处理 `Service` 调用可能发生的错误。
* **成功时:**
* 调用 `updatePluginState(id, {...})` 更新插件状态,使用步骤 2 中定义的状态类型。
* 调用 `internal_updateMessageContent(id, JSON.stringify({...}))` 更新消息内容,通常包含成功确认信息。
* **失败时:**
* 记录错误 (`console.error`)。
* 调用 `updatePluginState(id, {...})` 更新插件状态,包含错误信息。
* 调用 `internal_updateMessagePluginError(id, {...})` 设置消息的错误状态。
* 调用 `internal_updateMessageContent(id, JSON.stringify({...}))` 更新消息内容,包含错误信息。
* 在 `finally` 块中取消加载状态 (`toggleLocalFileLoading(id, false)`)。
* 返回操作是否成功 (`boolean`)。
3. **实现前端状态管理 (Store Action):**
- **文件:** `src/store/chat/slices/builtinTool/actions/[tool_category].ts` (例如: `src/store/chat/slices/builtinTool/actions/localFile.ts`)
- **操作:**
- 导入在步骤 2 中定义的 IPC 参数类型和状态类型。
- 在 Action 接口 (例如: `LocalFileAction`) 中添加新 Action 的方法签名,使用对应的 IPC 参数类型。
- 在 `createSlice` (例如: `localFileSlice`) 中实现该 Action 方法:
- 接收 `id` (消息 ID) 和 `params` (符合 IPC 参数类型)。
- 设置加载状态 (`toggleLocalFileLoading(id, true)`)。
- 调用对应的 `Service` 层方法 (见步骤 4),传递 `params`。
- 使用 `try...catch` 处理 `Service` 调用可能发生的错误。
- **成功时:**
- 调用 `updatePluginState(id, {...})` 更新插件状态,使用步骤 2 中定义的状态类型。
- 调用 `internal_updateMessageContent(id, JSON.stringify({...}))` 更新消息内容,通常包含成功确认信息。
- **失败时:**
- 记录错误 (`console.error`)。
- 调用 `updatePluginState(id, {...})` 更新插件状态,包含错误信息。
- 调用 `internal_updateMessagePluginError(id, {...})` 设置消息的错误状态。
- 调用 `internal_updateMessageContent(id, JSON.stringify({...}))` 更新消息内容,包含错误信息。
- 在 `finally` 块中取消加载状态 (`toggleLocalFileLoading(id, false)`)。
- 返回操作是否成功 (`boolean`)。
4. **实现 Service 层 (调用 IPC):**
* **文件:** `src/services/electron/[tool_category]Service.ts` (例如: `src/services/electron/localFileService.ts`)
* **操作:**
* 导入在步骤 2 中定义的 IPC 参数类型。
* 添加一个新的 `async` 方法,方法名通常与 Action 名称对应 (例如: `renameLocalFile`)。
* 方法接收 `params` (符合 IPC 参数类型)。
* 通过 `ensureElectronIpc()` 获取 IPC 代理 (`const ipc = ensureElectronIpc();`),调用与 Manifest 中 `name` 字段匹配的链式方法,并将 `params` 传递过去。
* 定义方法的返回类型,通常是 `Promise<{ success: boolean; error?: string }>`,与后端 Controller 返回的结构一致。
4. **实现 Service 层 (调用 IPC):**
- **文件:** `src/services/electron/[tool_category]Service.ts` (例如: `src/services/electron/localFileService.ts`)
- **操作:**
- 导入在步骤 2 中定义的 IPC 参数类型。
- 添加一个新的 `async` 方法,方法名通常与 Action 名称对应 (例如: `renameLocalFile`)。
- 方法接收 `params` (符合 IPC 参数类型)。
- 通过 `ensureElectronIpc()` 获取 IPC 代理 (`const ipc = ensureElectronIpc();`),调用与 Manifest 中 `name` 字段匹配的链式方法,并将 `params` 传递过去。
- 定义方法的返回类型,通常是 `Promise<{ success: boolean; error?: string }>`,与后端 Controller 返回的结构一致。
5. **实现后端逻辑 (Controller / IPC Handler):**
* **文件:** `apps/desktop/src/main/controllers/[ToolName]Ctr.ts` (例如: `apps/desktop/src/main/controllers/LocalFileCtr.ts`)
* **操作:**
* 导入 Node.js 相关模块 (`fs`, `path` 等) 和 IPC 相关依赖 (`ControllerModule`, `IpcMethod`、参数类型等)。
* 添加一个新的 `async` 方法,方法名通常以 `handle` 开头 (例如: `handleRenameFile`)。
* 使用 `@IpcMethod()` 装饰器将此方法注册为对应 IPC 事件的处理器,确保方法名与 Manifest 中的 `name` 以及 Service 层的链式调用一致。
* 方法的参数应解构自 Service 层传递过来的对象,类型与步骤 2 中定义的 IPC 参数类型匹配。
* 实现核心业务逻辑:
* 进行必要的输入验证。
* 执行文件系统操作或其他后端任务 (例如: `fs.promises.rename`)。
* 使用 `try...catch` 捕获执行过程中的错误。
* 处理特定错误码 (`error.code`) 以提供更友好的错误消息。
* 返回一个包含 `success` (boolean) 和可选 `error` (string) 字段的对象。
5. **实现后端逻辑 (Controller / IPC Handler):**
- **文件:** `apps/desktop/src/main/controllers/[ToolName]Ctr.ts` (例如: `apps/desktop/src/main/controllers/LocalFileCtr.ts`)
- **操作:**
- 导入 Node.js 相关模块 (`fs`, `path` 等) 和 IPC 相关依赖 (`ControllerModule`, `IpcMethod`、参数类型等)。
- 添加一个新的 `async` 方法,方法名通常以 `handle` 开头 (例如: `handleRenameFile`)。
- 使用 `@IpcMethod()` 装饰器将此方法注册为对应 IPC 事件的处理器,确保方法名与 Manifest 中的 `name` 以及 Service 层的链式调用一致。
- 方法的参数应解构自 Service 层传递过来的对象,类型与步骤 2 中定义的 IPC 参数类型匹配。
- 实现核心业务逻辑:
- 进行必要的输入验证。
- 执行文件系统操作或其他后端任务 (例如: `fs.promises.rename`)。
- 使用 `try...catch` 捕获执行过程中的错误。
- 处理特定错误码 (`error.code`) 以提供更友好的错误消息。
- 返回一个包含 `success` (boolean) 和可选 `error` (string) 字段的对象。
6. **更新 Agent 文档 (System Role):**
* **文件:** `src/tools/[tool_category]/systemRole.ts` (例如: `src/tools/local-files/systemRole.ts`)
* **操作:**
* 在 `<core_capabilities>` 部分添加新工具的简要描述。
* 如果需要,更新 `<workflow>`。
* 在 `<tool_usage_guidelines>` 部分为新工具添加详细的使用说明,解释其参数、用途和预期行为。
* 如有必要,更新 `<security_considerations>`。
* 如有必要(例如工具返回了新的数据结构或路径),更新 `<response_format>` 中的示例。
6. **更新 Agent 文档 (System Role):**
- **文件:** `src/tools/[tool_category]/systemRole.ts` (例如: `src/tools/local-files/systemRole.ts`)
- **操作:**
- 在 `<core_capabilities>` 部分添加新工具的简要描述。
- 如果需要,更新 `<workflow>`。
- 在 `<tool_usage_guidelines>` 部分为新工具添加详细的使用说明,解释其参数、用途和预期行为。
- 如有必要,更新 `<security_considerations>`。
- 如有必要(例如工具返回了新的数据结构或路径),更新 `<response_format>` 中的示例。
通过遵循这些步骤,可以系统地将新的桌面端工具集成到 LobeChat 的插件系统中。
+21 -9
View File
@@ -3,7 +3,8 @@ description:
globs:
alwaysApply: false
---
**桌面端菜单配置指南**
# 桌面端菜单配置指南
## 菜单系统概述
@@ -15,7 +16,7 @@ LobeChat 桌面应用有三种主要的菜单类型:
## 菜单相关文件结构
```
```plaintext
apps/desktop/src/main/
├── menus/ # 菜单定义
│ ├── appMenu.ts # 应用菜单配置
@@ -33,8 +34,9 @@ apps/desktop/src/main/
应用菜单在 `apps/desktop/src/main/menus/appMenu.ts` 中定义:
1. **导入依赖**
```typescript
import { app, BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
import { BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions, app } from 'electron';
import { is } from 'electron-util';
```
@@ -43,6 +45,7 @@ apps/desktop/src/main/
- 每个菜单项可以包含:label, accelerator (快捷键), role, submenu, click 等属性
3. **创建菜单工厂函数**
```typescript
export const createAppMenu = (win: BrowserWindow) => {
const template = [
@@ -61,6 +64,7 @@ apps/desktop/src/main/
上下文菜单通常在特定元素上右键点击时显示:
1. **在主进程中定义菜单模板**
```typescript
// apps/desktop/src/main/menus/contextMenu.ts
export const createContextMenu = () => {
@@ -73,6 +77,7 @@ apps/desktop/src/main/
```
2. **在适当的事件处理器中显示菜单**
```typescript
const menu = createContextMenu();
menu.popup();
@@ -83,11 +88,13 @@ apps/desktop/src/main/
托盘菜单在 `TrayMenuCtr.ts` 中配置:
1. **创建托盘图标**
```typescript
this.tray = new Tray(trayIconPath);
```
2. **定义托盘菜单**
```typescript
const contextMenu = Menu.buildFromTemplate([
{ label: '显示主窗口', click: this.showMainWindow },
@@ -97,6 +104,7 @@ apps/desktop/src/main/
```
3. **设置托盘菜单**
```typescript
this.tray.setContextMenu(contextMenu);
```
@@ -106,11 +114,13 @@ apps/desktop/src/main/
为菜单添加多语言支持:
1. **导入本地化工具**
```typescript
import { i18n } from '../locales';
```
2. **使用翻译函数**
```typescript
const template = [
{
@@ -118,14 +128,13 @@ apps/desktop/src/main/
submenu: [
{ label: i18n.t('menu.new'), click: createNew },
// ...
]
],
},
// ...
];
```
3. **在语言切换时更新菜单**
在 `MenuCtr.ts` 中监听语言变化事件并重新创建菜单
3. **在语言切换时更新菜单** 在 `MenuCtr.ts` 中监听语言变化事件并重新创建菜单
## 添加新菜单项流程
@@ -134,6 +143,7 @@ apps/desktop/src/main/
- 确定在菜单中的位置(主菜单项或子菜单项)
2. **定义菜单项**
```typescript
const newMenuItem: MenuItemConstructorOptions = {
label: '新功能',
@@ -141,12 +151,11 @@ apps/desktop/src/main/
click: (_, window) => {
// 处理点击事件
if (window) window.webContents.send('trigger-new-feature');
}
},
};
```
3. **添加到菜单模板**
将新菜单项添加到相应的菜单模板中
3. **添加到菜单模板** 将新菜单项添加到相应的菜单模板中
4. **对于与渲染进程交互的功能**
- 使用 `window.webContents.send()` 发送 IPC 消息到渲染进程
@@ -157,6 +166,7 @@ apps/desktop/src/main/
动态控制菜单项状态:
1. **保存对菜单项的引用**
```typescript
this.menuItems = {};
const menu = Menu.buildFromTemplate(template);
@@ -164,6 +174,7 @@ apps/desktop/src/main/
```
2. **根据条件更新状态**
```typescript
updateMenuState(state) {
if (this.menuItems.newFeature) {
@@ -179,6 +190,7 @@ apps/desktop/src/main/
2. **平台特定菜单**
- 使用 `process.platform` 检查为不同平台提供不同菜单
```typescript
if (process.platform === 'darwin') {
template.unshift({ role: 'appMenu' });
+17 -2
View File
@@ -3,7 +3,8 @@ description:
globs:
alwaysApply: false
---
**桌面端窗口管理指南**
# 桌面端窗口管理指南
## 窗口管理概述
@@ -16,7 +17,7 @@ LobeChat 桌面应用使用 Electron 的 `BrowserWindow` 管理应用窗口。
## 相关文件结构
```
```plaintext
apps/desktop/src/main/
├── appBrowsers.ts # 窗口管理的核心文件
├── controllers/
@@ -63,6 +64,7 @@ export const createMainWindow = () => {
实现窗口状态持久化保存和恢复:
1. **保存窗口状态**
```typescript
const saveWindowState = (window: BrowserWindow) => {
if (!window.isMinimized() && !window.isMaximized()) {
@@ -80,6 +82,7 @@ export const createMainWindow = () => {
```
2. **恢复窗口状态**
```typescript
const restoreWindowState = (window: BrowserWindow) => {
const savedState = settings.get('windowState');
@@ -96,6 +99,7 @@ export const createMainWindow = () => {
```
3. **监听窗口事件**
```typescript
window.on('close', () => saveWindowState(window));
window.on('moved', () => saveWindowState(window));
@@ -107,6 +111,7 @@ export const createMainWindow = () => {
对于需要多窗口支持的功能:
1. **跟踪窗口**
```typescript
export class WindowManager {
private windows: Map<string, BrowserWindow> = new Map();
@@ -133,6 +138,7 @@ export const createMainWindow = () => {
```
2. **窗口间通信**
```typescript
// 从一个窗口向另一个窗口发送消息
sendMessageToWindow(targetWindowId, channel, data) {
@@ -148,9 +154,11 @@ export const createMainWindow = () => {
通过 IPC 实现窗口操作:
1. **在主进程中注册 IPC 处理器**
```typescript
// apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
import { BrowserWindow } from 'electron';
import { ControllerModule, IpcMethod } from '@/controllers';
export default class BrowserWindowsCtr extends ControllerModule {
@@ -178,10 +186,12 @@ export const createMainWindow = () => {
}
}
```
- `@IpcMethod()` 根据控制器的 `groupName` 自动将方法映射为 `windows.minimizeWindow` 形式的通道名称。
- 控制器需继承 `ControllerModule`,并在 `controllers/registry.ts` 中通过 `controllerIpcConstructors` 注册,便于类型生成。
2. **在渲染进程中调用**
```typescript
// src/services/electron/windowService.ts
import { ensureElectronIpc } from '@/utils/electron/ipc';
@@ -194,6 +204,7 @@ export const createMainWindow = () => {
close: () => ipc.windows.closeWindow(),
};
```
- `ensureElectronIpc()` 会基于 `DesktopIpcServices` 运行时生成 Proxy,并通过 `window.electronAPI.invoke` 与主进程通信;不再直接使用 `dispatch`。
### 5. 自定义窗口控制 (无边框窗口)
@@ -201,6 +212,7 @@ export const createMainWindow = () => {
对于自定义窗口标题栏:
1. **创建无边框窗口**
```typescript
const window = new BrowserWindow({
frame: false,
@@ -210,6 +222,7 @@ export const createMainWindow = () => {
```
2. **在渲染进程中实现拖拽区域**
```css
/* CSS */
.titlebar {
@@ -229,6 +242,7 @@ export const createMainWindow = () => {
2. **安全性**
- 始终设置适当的 `webPreferences` 确保安全
```typescript
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
@@ -255,6 +269,7 @@ export const createMainWindow = () => {
```typescript
// apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
import type { OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc';
import { ControllerModule, IpcMethod } from '@/controllers';
export default class BrowserWindowsCtr extends ControllerModule {
+6 -6
View File
@@ -10,14 +10,14 @@ This document outlines the conventions and best practices for defining PostgreSQ
## Configuration
- Drizzle configuration is managed in [drizzle.config.ts](mdc:drizzle.config.ts)
- Drizzle configuration is managed in `drizzle.config.ts`
- Schema files are located in the src/database/schemas/ directory
- Migration files are output to `src/database/migrations/`
- The project uses `postgresql` dialect with `strict: true`
## 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`:
- `timestamptz(name: string)`: Creates a timestamp column with timezone
- `createdAt()`, `updatedAt()`, `accessedAt()`: Helper functions for standard timestamp columns
@@ -46,7 +46,7 @@ Commonly used column definitions, especially for timestamps, are centralized in
### 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` for `created_at`, `updated_at`, and `accessed_at` columns
### Default Values
@@ -74,12 +74,12 @@ Commonly used column definitions, especially for timestamps, are centralized in
## Relations
- Table relationships are defined centrally in [src/database/schemas/relations.ts](mdc:src/database/schemas/relations.ts) using the `relations()` utility from `drizzle-orm`
- Table relationships are defined centrally in `src/database/schemas/relations.ts` using the `relations()` utility from `drizzle-orm`
## Code Style & Structure
- **File Organization**: Each main database entity typically has its own schema file (e.g., [user.ts](mdc:src/database/schemas/user.ts), [agent.ts](mdc:src/database/schemas/agent.ts))
- All schemas are re-exported from [src/database/schemas/index.ts](mdc:src/database/schemas/index.ts)
- **File Organization**: Each main database entity typically has its own schema file (e.g., `user.ts`, `agent.ts`)
- All schemas are re-exported from `src/database/schemas/index.ts`
- **ESLint**: Files often start with `/* eslint-disable sort-keys-fix/sort-keys-fix */`
- **Comments**: Use JSDoc-style comments to explain the purpose of tables and complex columns, fields that are self-explanatory do not require jsdoc explanations, such as id, user_id, etc.
+1
View File
@@ -1,6 +1,7 @@
---
alwaysApply: false
---
# 如何添加新的快捷键:开发者指南
本指南将带您一步步地向 LobeChat 添加一个新的快捷键功能。我们将通过一个完整示例,演示从定义到实现的整个过程。
+1
View File
@@ -37,6 +37,7 @@ export default {
- `sync.status.ready` - Feature + group + status
**Parameters:** Use `{{variableName}}` syntax
```typescript
'alert.cloud.desc': '我们提供 {{credit}} 额度积分',
```
+16 -3
View File
@@ -1,6 +1,5 @@
---
description: Project directory structure overview
alwaysApply: false
alwaysApply: true
---
# LobeChat Project Structure
@@ -27,6 +26,11 @@ lobe-chat/
│ ├── agent-runtime/
│ ├── builtin-agents/
│ ├── builtin-tool-*/ # builtin tool packages
│ ├── business/ # cloud-only business logic packages
│ │ ├── config/
│ │ ├── const/
│ │ └── model-runtime/
│ ├── config/
│ ├── const/
│ ├── context-engine/
│ ├── conversation-flow/
@@ -36,6 +40,8 @@ lobe-chat/
│ │ ├── schemas/
│ │ └── repositories/
│ ├── desktop-bridge/
│ ├── edge-config/
│ ├── editor-runtime/
│ ├── electron-client-ipc/
│ ├── electron-server-ipc/
│ ├── fetch-sse/
@@ -46,7 +52,7 @@ lobe-chat/
│ │ └── src/
│ │ ├── core/
│ │ └── providers/
│ ├── obervability-otel/
│ ├── observability-otel/
│ ├── prompts/
│ ├── python-interpreter/
│ ├── ssrf-safe-fetch/
@@ -72,6 +78,10 @@ lobe-chat/
│ │ │ ├── onboarding/
│ │ │ └── router/
│ │ └── desktop/
│ ├── business/ # cloud-only business logic (client/server)
│ │ ├── client/
│ │ ├── locales/
│ │ └── server/
│ ├── components/
│ ├── config/
│ ├── const/
@@ -130,6 +140,9 @@ lobe-chat/
- Repository (bff-queries): `packages/database/src/repositories`
- Third-party Integrations: `src/libs` — analytics, oidc etc.
- Builtin Tools: `src/tools`, `packages/builtin-tool-*`
- Business (cloud-only): Code specific to LobeHub cloud service, only expose empty interfaces for opens-source version.
- `src/business/*`
- `packages/business/*`
## Data Flow Architecture
+17 -5
View File
@@ -107,7 +107,7 @@ This project uses a **hybrid routing architecture**: Next.js App Router for stat
| Router | oauth, reset-password, etc.) | src/app/[variants]/(auth)/ |
+------------------+--------------------------------+--------------------------------+
| React Router | Main SPA features | BrowserRouter + Routes |
| DOM | (chat, discover, settings) | desktopRouter.config.tsx |
| DOM | (chat, community, settings) | desktopRouter.config.tsx |
| | | mobileRouter.config.tsx |
+------------------+--------------------------------+--------------------------------+
```
@@ -122,16 +122,16 @@ This project uses a **hybrid routing architecture**: Next.js App Router for stat
### Router Utilities
```tsx
import { dynamicElement, redirectElement, ErrorBoundary, RouteConfig } from '@/utils/router';
import { ErrorBoundary, RouteConfig, dynamicElement, redirectElement } from '@/utils/router';
// Lazy load a page component
element: dynamicElement(() => import('./chat'), 'Desktop > Chat')
element: dynamicElement(() => import('./chat'), 'Desktop > Chat');
// Create a redirect
element: redirectElement('/settings/profile')
element: redirectElement('/settings/profile');
// Error boundary for route
errorElement: <ErrorBoundary resetPath="/chat" />
errorElement: <ErrorBoundary resetPath="/chat" />;
```
### Adding New Routes
@@ -142,6 +142,18 @@ errorElement: <ErrorBoundary resetPath="/chat" />
### Navigation
**Important**: For SPA pages (React Router DOM routes), use `Link` from `react-router-dom`, NOT from `next/link`.
```tsx
// ❌ Wrong - next/link in SPA pages
import Link from 'next/link';
<Link href="/">Home</Link>
// ✅ Correct - react-router-dom Link in SPA pages
import { Link } from 'react-router-dom';
<Link to="/">Home</Link>
```
```tsx
// In components - use react-router-dom hooks
import { useNavigate, useParams } from 'react-router-dom';
+2 -1
View File
@@ -42,7 +42,7 @@ const Component = () => {
return (
<div>
{recentTopics.map(topic => (
{recentTopics.map((topic) => (
<div key={topic.id}>{topic.title}</div>
))}
</div>
@@ -81,6 +81,7 @@ const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
```
**RecentTopic 类型:**
```typescript
interface RecentTopic {
agent: {
+262 -238
View File
@@ -3,173 +3,199 @@ globs: *.test.ts,*.test.tsx
alwaysApply: false
---
# 测试指南 - LobeChat Testing Guide
# LobeChat Testing Guide
## 测试环境概览
## Test Overview
LobeChat 项目使用 Vitest 测试库,配置了两种不同的测试环境:
LobeChat testing consists of **E2E tests** and **Unit tests**. This guide focuses on **Unit tests**.
### 客户端数据库测试环境 (DOM Environment)
Unit tests are organized into three main categories:
- **配置文件**: [vitest.config.ts](mdc:vitest.config.ts)
- **环境**: Happy DOM (浏览器环境模拟)
- **数据库**: PGLite (浏览器环境的 PostgreSQL)
- **用途**: 测试前端组件、客户端逻辑、React 组件等
- **设置文件**: [tests/setup.ts](mdc:tests/setup.ts)
```plaintext
+---------------------+---------------------------+-----------------------------+
| Category | Location | Config File |
+---------------------+---------------------------+-----------------------------+
| Next.js Webapp | src/**/*.test.ts(x) | vitest.config.ts |
| Packages | packages/*/**/*.test.ts | packages/*/vitest.config.ts |
| Desktop App | apps/desktop/**/*.test.ts | apps/desktop/vitest.config.ts |
+---------------------+---------------------------+-----------------------------+
```
### 服务端数据库测试环境 (Node Environment)
### Next.js Webapp Tests
目前只有 `packages/database` 下的测试可以通过配置 `TEST_SERVER_DB=1` 环境变量来使用服务端数据库测试
- **Config File**: `vitest.config.ts`
- **Environment**: Happy DOM (browser environment simulation)
- **Database**: PGLite (PostgreSQL for browser environments)
- **Setup File**: `tests/setup.ts`
- **Purpose**: Testing React components, hooks, stores, utilities, and client-side logic
- **配置文件**: [packages/database/vitest.config.mts](mdc:packages/database/vitest.config.mts) 并且设置环境变量 `TEST_SERVER_DB=1`
- **环境**: Node.js
- **数据库**: 真实的 PostgreSQL 数据库
- **并发限制**: 单线程运行 (`singleFork: true`)
- **用途**: 测试数据库模型、服务端逻辑、API 端点等
- **设置文件**: [packages/database/tests/setup-db.ts](mdc:packages/database/tests/setup-db.ts)
### Packages Tests
## 测试运行命令
Most packages use standard Vitest configuration. However, the `database` package is special:
** 性能警告**: 项目包含 3000+ 测试用例,完整运行需要约 10 分钟。务必使用文件过滤或测试名称过滤。
#### Database Package (Special Case)
### 正确的命令格式
The database package supports **dual-environment testing**:
| Environment | Database | Config | Use Case |
|------------------|-----------------|---------------------------------------|-----------------------------------|
| Client (Default) | PGLite | `packages/database/vitest.config.mts` | Fast local development |
| Server | Real PostgreSQL | Set `TEST_SERVER_DB=1` | CI/CD, compatibility verification |
Server environment details:
- **Concurrency**: Single-threaded (`singleFork: true`)
- **Setup File**: `packages/database/tests/setup-db.ts`
- **Requirement**: `DATABASE_TEST_URL` environment variable must be set
### Desktop App Tests
- **Config File**: `apps/desktop/vitest.config.ts`
- **Environment**: Node.js
- **Purpose**: Testing Electron main process controllers, IPC handlers, and desktop-specific logic
## Test Commands
**Performance Warning**: The project contains 3000+ test cases. A full run takes approximately 10 minutes. Always use file filtering or test name filtering.
### Recommended Command Format
```bash
# 运行所有客户端/服务端测试
bunx vitest run --silent='passed-only' # 客户端测试
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' # 服务端测试
# Run all client/server tests
bunx vitest run --silent='passed-only' # Client tests
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' # Server tests
# 运行特定测试文件 (支持模糊匹配)
# Run specific test file (supports fuzzy matching)
bunx vitest run --silent='passed-only' user.test.ts
# 运行特定测试用例名称 (使用 -t 参数)
# Run specific test case by name (using -t flag)
bunx vitest run --silent='passed-only' -t "test case name"
# 组合使用文件和测试名称过滤
# Combine file and test name filtering
bunx vitest run --silent='passed-only' filename.test.ts -t "specific test"
# 生成覆盖率报告 (使用 --coverage 参数)
# Generate coverage report (using --coverage flag)
bunx vitest run --silent='passed-only' --coverage
```
### 避免的命令格式
### Commands to Avoid
```bash
# 这些命令会运行所有 3000+ 测试用例,耗时约 10 分钟!
# ❌ These commands run all 3000+ test cases, taking ~10 minutes!
npm test
npm test some-file.test.ts
# 不要使用裸 vitest (会进入 watch 模式)
# ❌ Don't use bare vitest (enters watch mode)
vitest test-file.test.ts
```
## 测试修复原则
## Test Fixing Principles
### 核心原则
### Core Principles
1. **收集足够的上下文**
在修复测试之前,务必做到:
- 完整理解测试的意图和实现
- 强烈建议阅读当前的 git diff PR diff
1. **Gather Sufficient Context**
Before fixing tests, ensure you:
- Fully understand the test's intent and implementation
- Strongly recommended: review the current git diff and PR diff
2. **测试优先修复**
如果是测试本身写错了,应优先修改测试,而不是实现代码。
2. **Prioritize Test Fixes**
If the test itself is incorrect, fix the test first rather than the implementation code.
3. **专注单一问题**
只修复指定的测试,不要顺带添加额外测试。
3. **Focus on a Single Issue**
Only fix the specified test; don't add extra tests along the way.
4. **不自作主张**
发现其他问题时,不要直接修改,需先提出并讨论。
4. **Don't Act Unilaterally**
When discovering other issues, don't modify them directly—raise and discuss first.
### 测试协作最佳实践
### Testing Collaboration Best Practices
基于实际开发经验总结的重要协作原则:
Important collaboration principles based on real development experience:
#### 1. 失败处理策略
#### 1. Failure Handling Strategy
**核心原则**: 避免盲目重试,快速识别问题并寻求帮助。
**Core Principle**: Avoid blind retries; quickly identify problems and seek help.
- **失败阈值**: 当连续尝试修复测试 1-2 次都失败后,应立即停止继续尝试
- **问题总结**: 分析失败原因,整理已尝试的解决方案及其失败原因
- **寻求帮助**: 带着清晰的问题摘要和尝试记录向团队寻求帮助
- **避免陷阱**: 不要陷入"不断尝试相同或类似方法"的循环
- **Failure Threshold**: After 1-2 consecutive failed fix attempts, stop immediately
- **Problem Summary**: Analyze failure reasons and document attempted solutions with their failure causes
- **Seek Help**: Approach the team with a clear problem summary and attempt history
- **Avoid the Trap**: Don't fall into the loop of repeatedly trying the same or similar approaches
```typescript
// 错误做法:连续失败后继续盲目尝试
// 第3次、第4次仍在用相似的方法修复同一个问题
// ❌ Wrong approach: Keep blindly trying after consecutive failures
// 3rd, 4th attempts still using similar methods to fix the same problem
// 正确做法:失败1-2次后总结问题
// ✅ Correct approach: Summarize after 1-2 failures
/*
问题总结:
1. 尝试过的方法:修改 mock 数据结构
2. 失败原因:仍然提示类型不匹配
3. 具体错误:Expected 'UserData' but received 'UserProfile'
4. 需要帮助:不确定最新的 UserData 接口定义
Problem Summary:
1. Attempted method: Modified mock data structure
2. Failure reason: Still getting type mismatch error
3. Specific error: Expected 'UserData' but received 'UserProfile'
4. Help needed: Unsure about the latest UserData interface definition
*/
```
#### 2. 测试用例命名规范
#### 2. Test Case Naming Conventions
**核心原则**: 测试应该关注"行为",而不是"实现细节"。
**Core Principle**: Tests should focus on "behavior," not "implementation details."
- **描述业务场景**: `describe` `it` 的标题应该描述具体的业务场景和预期行为
- **避免实现绑定**: 不要在测试名称中提及具体的代码行号、覆盖率目标或实现细节
- **保持稳定性**: 测试名称应该在代码重构后仍然有意义
- **Describe Business Scenarios**: `describe` and `it` titles should describe specific business scenarios and expected behaviors
- **Avoid Implementation Binding**: Don't mention specific line numbers, coverage goals, or implementation details in test names
- **Maintain Stability**: Test names should remain meaningful after code refactoring
```typescript
// 错误的测试命名
// ❌ Poor test naming
describe('User component coverage', () => {
it('covers line 45-50 in getUserData', () => {
// 为了覆盖第45-50行而写的测试
// Test written just to cover lines 45-50
});
it('tests the else branch', () => {
// 仅为了测试某个分支而存在
// Exists only to test a specific branch
});
});
// 正确的测试命名
// ✅ Good test naming
describe('<UserAvatar />', () => {
it('should render fallback icon when image url is not provided', () => {
// 测试具体的业务场景,自然会覆盖相关代码分支
// Tests a specific business scenario, naturally covering relevant code branches
});
it('should display user initials when avatar image fails to load', () => {
// 描述用户行为和预期结果
// Describes user behavior and expected outcome
});
});
```
**覆盖率提升的正确思路**:
**The Right Approach to Improving Coverage**:
- 通过设计各种业务场景(正常流程、边缘情况、错误处理)来自然提升覆盖率
- 不要为了达到覆盖率数字而写测试,更不要在测试中注释"为了覆盖 xxx 行"
- Naturally improve coverage by designing various business scenarios (happy paths, edge cases, error handling)
- Don't write tests just to hit coverage numbers, and never comment "to cover line xxx" in tests
#### 3. 测试组织结构
#### 3. Test Organization Structure
**核心原则**: 维护清晰的测试层次结构,避免冗余的顶级测试块。
**Core Principle**: Maintain a clear test hierarchy; avoid redundant top-level test blocks.
- **复用现有结构**: 添加新测试时,优先在现有的 `describe` 块中寻找合适的位置
- **逻辑分组**: 相关的测试用例应该组织在同一个 `describe` 块内
- **避免碎片化**: 不要为了单个测试用例就创建新的顶级 `describe` 块
- **Reuse Existing Structure**: When adding new tests, first look for an appropriate place in existing `describe` blocks
- **Logical Grouping**: Related test cases should be organized within the same `describe` block
- **Avoid Fragmentation**: Don't create a new top-level `describe` block for a single test case
```typescript
// 错误的组织方式:创建过多顶级块
// ❌ Poor organization: Too many top-level blocks
describe('<UserProfile />', () => {
it('should render user name', () => {});
});
describe('UserProfile new prop test', () => {
// 不必要的新块
// Unnecessary new block
it('should handle email display', () => {});
});
describe('UserProfile edge cases', () => {
// 不必要的新块
// Unnecessary new block
it('should handle missing avatar', () => {});
});
// 正确的组织方式:合并相关测试
// ✅ Good organization: Merge related tests
describe('<UserProfile />', () => {
it('should render user name', () => {});
@@ -178,78 +204,78 @@ describe('<UserProfile />', () => {
it('should handle missing avatar', () => {});
describe('when user data is incomplete', () => {
// 只有在有多个相关子场景时才创建子组
// Only create sub-groups when there are multiple related sub-scenarios
it('should show placeholder for missing name', () => {});
it('should hide email section when email is undefined', () => {});
});
});
```
**组织决策流程**:
**Organization Decision Flow**:
1. 是否存在逻辑相关的现有 `describe` 块? → 如果有,添加到其中
2. 是否有多个(3个以上)相关的测试用例? → 如果有,可以考虑创建新的子 `describe`
3. 是否是独立的、无关联的功能模块? → 如果是,才考虑创建新的顶级 `describe`
1. Is there a logically related existing `describe` block? → If yes, add to it
2. Are there multiple (3+) related test cases? → If yes, consider creating a new sub-`describe`
3. Is it an independent, unrelated feature module? → Only then consider creating a new top-level `describe`
### 测试修复流程
### Test Fixing Workflow
1. **复现问题**: 定位并运行失败的测试,确认能在本地复现
2. **分析原因**: 阅读测试代码、错误日志和相关文件的 Git 修改历史
3. **建立假设**: 判断问题出在测试逻辑、实现代码还是环境配置
4. **修复验证**: 根据假设进行修复,重新运行测试确认通过
5. **扩大验证**: 运行当前文件内所有测试,确保没有引入新问题
6. **撰写总结**: 说明错误原因和修复方法
1. **Reproduce the Issue**: Locate and run the failing test; confirm it can be reproduced locally
2. **Analyze the Cause**: Read test code, error logs, and Git history of related files
3. **Form a Hypothesis**: Determine if the problem is in test logic, implementation code, or environment configuration
4. **Fix and Verify**: Apply the fix based on your hypothesis; rerun the test to confirm it passes
5. **Expand Verification**: Run all tests in the current file to ensure no new issues were introduced
6. **Write a Summary**: Document the error cause and fix method
### 修复完成后的总结
### Post-Fix Summary
测试修复完成后,应该提供简要说明,包括:
After completing a test fix, provide a brief explanation including:
1. **错误原因分析**: 说明测试失败的根本原因
- 测试逻辑错误
- 实现代码bug
- 环境配置问题
- 依赖变更导致的问题
1. **Root Cause Analysis**: Explain the fundamental reason for the test failure
- Test logic error
- Implementation bug
- Environment configuration issue
- Dependency change
2. **修复方法说明**: 简述采用的修复方式
- 修改了哪些文件
- 采用了什么解决方案
- 为什么选择这种修复方式
2. **Fix Description**: Briefly describe the fix approach
- Which files were modified
- What solution was applied
- Why this fix approach was chosen
**示例格式**:
**Example Format**:
```markdown
## 测试修复总结
## Test Fix Summary
**错误原因**: 测试中的 mock 数据格式与实际 API 返回格式不匹配,导致断言失败。
**Root Cause**: The mock data format in the test didn't match the actual API response format, causing assertion failures.
**修复方法**: 更新了测试文件中的 mock 数据结构,使其与最新的 API 响应格式保持一致。具体修改了 `user.test.ts` 中的 `mockUserData` 对象结构。
**Fix**: Updated the mock data structure in the test file to match the latest API response format. Specifically modified the `mockUserData` object structure in `user.test.ts`.
```
## 测试编写最佳实践
## Test Writing Best Practices
### Mock 数据策略:追求"低成本的真实性"
### Mock Data Strategy: Aim for "Low-Cost Authenticity"
**核心原则**: 测试数据应默认追求真实性,只有在引入"高昂的测试成本"时才进行简化。
**Core Principle**: Test data should default to authenticity; only simplify when it introduces "high testing costs."
#### 什么是"高昂的测试成本"?
#### What Are "High Testing Costs"?
"高成本"指的是测试中引入了外部依赖,使测试变慢、不稳定或复杂:
"High cost" refers to introducing external dependencies in tests that make them slow, unstable, or complex:
- **文件 I/O 操作**:读写硬盘文件
- **网络请求**:HTTP 调用、数据库连接
- **系统调用**:获取系统时间、环境变量等
- **File I/O Operations**: Reading/writing disk files
- **Network Requests**: HTTP calls, database connections
- **System Calls**: Getting system time, environment variables, etc.
#### 推荐做法:Mock 依赖,保留真实数据
#### Recommended Approach: Mock Dependencies, Keep Real Data
```typescript
// 好的做法:Mock I/O 操作,但使用真实的文件内容格式
// ✅ Good approach: Mock I/O operations but use real file content formats
describe('parseContentType', () => {
beforeEach(() => {
// Mock 文件读取操作(避免真实 I/O
// Mock file read operation (avoid real I/O)
vi.spyOn(fs, 'readFileSync').mockImplementation((path) => {
// 但返回真实的文件内容格式
if (path.includes('.pdf')) return '%PDF-1.4\n%âãÏÓ'; // 真实 PDF 文件头
if (path.includes('.png')) return '\x89PNG\r\n\x1a\n'; // 真实 PNG 文件头
// But return real file content formats
if (path.includes('.pdf')) return '%PDF-1.4\n%âãÏÓ'; // Real PDF header
if (path.includes('.png')) return '\x89PNG\r\n\x1a\n'; // Real PNG header
return '';
});
});
@@ -260,40 +286,38 @@ describe('parseContentType', () => {
});
});
// 过度简化:使用不真实的数据
// ❌ Over-simplified: Using unrealistic data
describe('parseContentType', () => {
it('should detect PDF content type correctly', () => {
// 这种简化数据没有测试价值
// This simplified data has no test value
const result = parseContentType('fake-pdf-content');
expect(result).toBe('application/pdf');
});
});
```
#### 真实标识符的价值
#### The Value of Real Identifiers
```typescript
// ✅ 使用真实标识符
// ✅ Use real identifiers
const result = parseModelString('openai', '+gpt-4,+gpt-3.5-turbo');
// ❌ 使用占位符(价值较低)
// ❌ Use placeholders (lower value)
const result = parseModelString('test-provider', '+model1,+model2');
```
### 现代化Mock技巧:环境设置与Mock方法
### Modern Mocking Techniques: Environment Setup and Mock Methods
**环境设置 + Mock方法结合使用**
客户端代码测试时,推荐使用环境注释配合现代化Mock方法:
When testing client-side code, use environment annotations with modern mock methods:
```typescript
/**
* @vitest-environment happy-dom // 提供浏览器API
* @vitest-environment happy-dom // Provides browser APIs
*/
import { beforeEach, vi } from 'vitest';
beforeEach(() => {
// 现代方法1:使用vi.stubGlobal替代global.xxx = ...
// Modern method 1: Use vi.stubGlobal instead of global.xxx = ...
const mockImage = vi.fn().mockImplementation(() => ({
addEventListener: vi.fn(),
naturalHeight: 600,
@@ -301,72 +325,72 @@ beforeEach(() => {
}));
vi.stubGlobal('Image', mockImage);
// 现代方法2:使用vi.spyOn保留原功能,只mock特定方法
// Modern method 2: Use vi.spyOn to preserve original functionality, only mock specific methods
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url');
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
});
```
**环境选择优先级**
#### Environment Selection Priority
1. **@vitest-environment happy-dom** (推荐) - 轻量、快速,项目已安装
2. **@vitest-environment jsdom** - 功能完整,但需要额外安装jsdom包
3. **不设置环境** - Node.js环境,需要手动mock所有浏览器API
1. **@vitest-environment happy-dom** (Recommended) - Lightweight, fast, already installed in the project
2. **@vitest-environment jsdom** - Full-featured, but requires additional jsdom package installation
3. **No environment set** - Node.js environment, requires manually mocking all browser APIs
**Mock方法对比**
#### Mock Method Comparison
```typescript
// ❌ 旧方法:直接操作global对象(类型问题)
// ❌ Old method: Directly manipulating global object (type issues)
global.Image = mockImage;
global.URL = { ...global.URL, createObjectURL: mockFn };
// ✅ 现代方法:类型安全的vi API
vi.stubGlobal('Image', mockImage); // 完全替换全局对象
vi.spyOn(URL, 'createObjectURL'); // 部分mock,保留其他功能
// ✅ Modern method: Type-safe vi API
vi.stubGlobal('Image', mockImage); // Completely replace global object
vi.spyOn(URL, 'createObjectURL'); // Partial mock, preserve other functionality
```
### 测试覆盖率原则:代码分支优于用例数量
### Test Coverage Principles: Code Branches Over Test Quantity
**核心原则**: 优先覆盖所有代码分支,而非编写大量重复用例
**Core Principle**: Prioritize covering all code branches rather than writing many repetitive test cases.
```typescript
// ❌ 过度测试:29个测试用例都验证相同分支
// ❌ Over-testing: 29 test cases all validating the same branch
describe('getImageDimensions', () => {
it('should reject .txt files');
it('should reject .pdf files');
// ... 25个类似测试,都走相同的验证分支
// ... 25 similar tests, all hitting the same validation branch
});
// ✅ 精简测试:4个核心用例覆盖所有分支
// ✅ Lean testing: 4 core cases covering all branches
describe('getImageDimensions', () => {
it('should return dimensions for valid File object'); // 成功路径 - File
it('should return dimensions for valid data URI'); // 成功路径 - String
it('should return undefined for invalid inputs'); // 输入验证分支
it('should return undefined when image fails to load'); // 错误处理分支
it('should return dimensions for valid File object'); // Success path - File
it('should return dimensions for valid data URI'); // Success path - String
it('should return undefined for invalid inputs'); // Input validation branch
it('should return undefined when image fails to load'); // Error handling branch
});
```
**分支覆盖策略**
#### Branch Coverage Strategy
1. **成功路径** - 每种输入类型1个测试即可
2. **边界条件** - 合并类似场景到单个测试
3. **错误处理** - 测试代表性错误即可
4. **业务逻辑** - 覆盖所有if/else分支
1. **Success Paths** - One test per input type is sufficient
2. **Boundary Conditions** - Consolidate similar scenarios into a single test
3. **Error Handling** - Test representative errors only
4. **Business Logic** - Cover all if/else branches
**合理测试数量**
#### Reasonable Test Counts
- 简单工具函数:2-5个测试
- 复杂业务逻辑:5-10个测试
- 核心安全功能:适当增加,但避免重复路径
- Simple utility functions: 2-5 tests
- Complex business logic: 5-10 tests
- Core security features: Add more as needed, but avoid duplicate paths
### 错误处理测试:测试"行为"而非"文本"
### Error Handling Tests: Test "Behavior" Not "Text"
**核心原则**: 测试应该验证程序在错误发生时的行为是可预测的,而不是验证易变的错误信息文本。
**Core Principle**: Tests should verify that program behavior is predictable when errors occur, not verify error message text that may change.
#### 推荐的错误测试方式
#### Recommended Error Testing Approach
```typescript
// ✅ 测试错误类型和属性
// ✅ Test error types and properties
expect(() => validateUser({})).toThrow(ValidationError);
expect(() => processPayment({})).toThrow(
expect.objectContaining({
@@ -375,136 +399,136 @@ expect(() => processPayment({})).toThrow(
}),
);
// ❌ 避免测试具体错误文本
expect(() => processUser({})).toThrow('用户数据不能为空,请检查输入参数');
// ❌ Avoid testing specific error text
expect(() => processUser({})).toThrow('User data cannot be empty, please check input parameters');
```
### 疑难解答:警惕模块污染
### Troubleshooting: Beware of Module Pollution
**识别信号**: 当你的测试出现以下"灵异"现象时,优先怀疑模块污染:
**Warning Signs**: When your tests exhibit these "mysterious" behaviors, suspect module pollution first:
- 单独运行某个测试通过,但和其他测试一起运行就失败
- 测试的执行顺序影响结果
- Mock 设置看起来正确,但实际使用的是旧的 Mock 版本
- A test passes when run alone but fails when run with other tests
- Test execution order affects results
- Mock setup appears correct but actually uses an old mock version
#### 典型场景:动态 Mock 同一模块
#### Typical Scenario: Dynamic Mocking of the Same Module
```typescript
// ❌ 问题:动态Mock同一模块
// ❌ Problem: Dynamic mocking of the same module
it('dev mode', async () => {
vi.doMock('./config', () => ({ isDev: true }));
const { getSettings } = await import('./service'); // 可能使用缓存
const { getSettings } = await import('./service'); // May use cache
});
// ✅ 解决:清除模块缓存
// ✅ Solution: Clear module cache
beforeEach(() => {
vi.resetModules(); // 确保每个测试都是干净环境
vi.resetModules(); // Ensure each test has a clean environment
});
```
**记住**: `vi.resetModules()` 是解决测试"灵异"失败的终极武器。
**Remember**: `vi.resetModules()` is the ultimate weapon for resolving "mysterious" test failures.
## 测试文件组织
## Test File Organization
### 文件命名约定
### File Naming Convention
`*.test.ts`, `*.test.tsx` (任意位置)
`*.test.ts`, `*.test.tsx` (any location)
### 测试文件组织风格
### Test File Organization Style
项目采用 **测试文件与源文件同目录** 的组织风格:
The project uses a **co-located test files** organization style:
- 测试文件放在对应源文件的同一目录下
- 命名格式:`原文件名.test.ts` 或 `原文件名.test.tsx`
- Test files are placed in the same directory as the corresponding source files
- Naming format: `originalFileName.test.ts` or `originalFileName.test.tsx`
例如:
Example:
```plaintext
src/components/Button/
├── index.tsx # 源文件
└── index.test.tsx # 测试文件
├── index.tsx # Source file
└── index.test.tsx # Test file
```
- 也有少数情况会统一放到 `__tests__` 文件夹, 例如 `packages/database/src/models/__tests__`
- 测试使用的辅助文件放到 fixtures 文件夹
- In some cases, tests are consolidated in a `__tests__` folder, e.g., `packages/database/src/models/__tests__`
- Test helper files are placed in a fixtures folder
## 测试调试技巧
## Test Debugging Tips
### 测试调试步骤
### Test Debugging Steps
1. **确定测试环境**: 根据文件路径选择正确的配置文件
2. **隔离问题**: 使用 `-t` 参数只运行失败的测试用例
3. **分析错误**: 仔细阅读错误信息、堆栈跟踪和最近的文件修改记录
4. **添加调试**: 在测试中添加 `console.log` 了解执行流程
1. **Determine Test Environment**: Select the correct config file based on file path
2. **Isolate the Problem**: Use the `-t` flag to run only the failing test case
3. **Analyze the Error**: Carefully read error messages, stack traces, and recent file modification history
4. **Add Debugging**: Add `console.log` statements in tests to understand execution flow
### TypeScript 类型处理
### TypeScript Type Handling
在测试中,为了提高编写效率和可读性,可以适当放宽 TypeScript 类型检测:
In tests, you can relax TypeScript type checking to improve writing efficiency and readability:
#### 推荐的类型放宽策略
#### Recommended Type Relaxation Strategies
```typescript
// 使用非空断言访问测试中确定存在的属性
// Use non-null assertion to access properties you're certain exist in tests
const result = await someFunction();
expect(result!.data).toBeDefined();
expect(result!.status).toBe('success');
// 使用 any 类型简化复杂的 Mock 设置
// Use any type to simplify complex mock setups
const mockStream = new ReadableStream() as any;
mockStream.toReadableStream = () => mockStream;
// 访问私有成员
await instance['getFromCache']('key'); // 推荐中括号
await (instance as any).getFromCache('key'); // 避免as any
// Access private members
await instance['getFromCache']('key'); // Bracket notation recommended
await (instance as any).getFromCache('key'); // Avoid as any
```
#### 适用场景
#### Applicable Scenarios
- **Mock 对象**: 对于测试用的 Mock 数据,使用 `as any` 避免复杂的类型定义
- **第三方库**: 处理复杂的第三方库类型时,适当使用 `any` 提高效率
- **测试断言**: 在确定对象存在的测试场景中,使用 `!` 非空断言
- **私有成员访问**: 优先使用中括号 `instance['privateMethod']()` 而不是 `(instance as any).privateMethod()`
- **临时调试**: 快速编写测试时,先用 `any` 保证功能,后续可选择性地优化类型
- **Mock Objects**: Use `as any` for test mock data to avoid complex type definitions
- **Third-Party Libraries**: Use `any` appropriately when handling complex third-party library types
- **Test Assertions**: Use `!` non-null assertion in test scenarios where you're certain the object exists
- **Private Member Access**: Prefer bracket notation `instance['privateMethod']()` over `(instance as any).privateMethod()`
- **Temporary Debugging**: When quickly writing tests, use `any` first to ensure functionality, then optionally optimize types later
#### 注意事项
#### Important Notes
- **适度使用**: 不要过度依赖 `any`,核心业务逻辑的类型仍应保持严格
- **私有成员访问优先级**: 中括号访问 > `as any` 转换,保持更好的类型安全性
- **文档说明**: 对于使用 `any` 的复杂场景,添加注释说明原因
- **测试覆盖**: 确保即使使用了 `any`,测试仍能有效验证功能正确性
- **Use Moderately**: Don't over-rely on `any`; core business logic types should remain strict
- **Private Member Access Priority**: Bracket notation > `as any` casting for better type safety
- **Documentation**: Add comments explaining the reason for complex `any` usage scenarios
- **Test Coverage**: Ensure tests still effectively verify correctness even when using `any`
### 检查最近修改记录
### Checking Recent Modifications
**核心原则**:测试突然失败时,优先检查最近的代码修改。
**Core Principle**: When tests suddenly fail, first check recent code changes.
#### 快速检查方法
#### Quick Check Methods
```bash
git status # 查看当前修改状态
git diff HEAD -- '*.test.*' # 检查测试文件改动
git diff main...HEAD # 对比主分支差异
gh pr diff # 查看PR中的所有改动
git status # View current modification status
git diff HEAD -- '*.test.*' # Check test file changes
git diff main...HEAD # Compare with main branch
gh pr diff # View all changes in the PR
```
#### 常见原因与解决
#### Common Causes and Solutions
- **最新提交引入bug** → 检查并修复实现代码
- **分支代码滞后** → `git rebase main` 同步主分支
- **Latest commit introduced a bug** → Check and fix the implementation code
- **Branch code is outdated** → `git rebase main` to sync with main branch
## 特殊场景的测试
## Special Testing Scenarios
针对一些特殊场景的测试,需要阅读相关 rules
For special testing scenarios, refer to the related rules:
- [Electron IPC 接口测试策略](mdc:./electron-ipc-test.mdc)
- [数据库 Model 测试指南](mdc:./db-model-test.mdc)
- `electron-ipc-test.mdc` - Electron IPC Interface Testing Strategy
- `db-model-test.mdc` - Database Model Testing Guide
## 核心要点
## Key Takeaways
- **命令格式**: 使用 `bunx vitest run --silent='passed-only'` 并指定文件过滤
- **修复原则**: 失败1-2次后寻求帮助,测试命名关注行为而非实现细节
- **调试流程**: 复现 → 分析 → 假设 → 修复 → 验证 → 总结
- **文件组织**: 优先在现有 `describe` 块中添加测试,避免创建冗余顶级块
- **数据策略**: 默认追求真实性,只有高成本(I/O、网络等)时才简化
- **错误测试**: 测试错误类型和行为,避免依赖具体的错误信息文本
- **模块污染**: 测试"灵异"失败时,优先怀疑模块污染,使用 `vi.resetModules()` 解决
- **安全要求**: Model 测试必须包含权限检查,并在双环境下验证通过
- **Command Format**: Use `bunx vitest run --silent='passed-only'` with file filtering
- **Fix Principles**: Seek help after 1-2 failures; focus test naming on behavior, not implementation details
- **Debug Workflow**: Reproduce → Analyze → Hypothesize → Fix → Verify → Summarize
- **File Organization**: Prefer adding tests to existing `describe` blocks; avoid creating redundant top-level blocks
- **Data Strategy**: Default to authenticity; only simplify for high-cost scenarios (I/O, network, etc.)
- **Error Testing**: Test error types and behavior; avoid depending on specific error message text
- **Module Pollution**: When tests fail "mysteriously," suspect module pollution first; use `vi.resetModules()` to resolve
- **Security Requirements**: Model tests must include permission checks and pass in both environments
@@ -1,6 +1,6 @@
---
description: Best practices for testing Zustand store actions
globs: 'src/store/**/*.test.ts'
globs: src/store/**/*.test.ts
alwaysApply: false
---
+1 -1
View File
@@ -16,7 +16,7 @@ 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)
- Example: `src/store/chat/slices/topic/action.ts`
```typescript
// Public Action example
+4 -4
View File
@@ -105,7 +105,7 @@ export const initialTopicState: ChatTopicState = {
};
```
2. `reducer.ts` (复杂状态使用):
1. `reducer.ts` (复杂状态使用):
- 定义纯函数 reducer,处理同步状态转换
- 使用 `immer` 确保不可变更新
@@ -151,7 +151,7 @@ export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch
};
```
3. `selectors.ts`:
1. `selectors.ts`:
- 提供状态查询和计算函数
- 供 UI 组件使用的状态订阅接口
- 重要: 使用 `export const xxxSelectors` 模式聚合所有 selectors
@@ -186,7 +186,7 @@ export const topicSelectors = {
当 slice 的 actions 过于复杂时,可以拆分到子目录:
```
```plaintext
src/store/chat/slices/aiChat/
├── actions/
│ ├── generateAIChat.ts # AI 对话生成
@@ -204,7 +204,7 @@ src/store/chat/slices/aiChat/
管理多种内置工具的状态:
```
```plaintext
src/store/chat/slices/builtinTool/
├── actions/
│ ├── dalle.ts # DALL-E 图像生成