♻️ refactor: migrate AI Rules to Claude Code Skills (#11737)

♻️ refactor: migrate AI Rules to Claude Code Skills system

Migrate all AI Rules from .cursor/rules/ to .agents/skills/ directory:
- Move 23 skills to .agents/skills/ (main directory)
- Update symlinks: .claude/skills, .cursor/skills, .codex/skills
- Create project-overview skill from project documentation
- Add references/ subdirectories for complex skills
- Remove LobeChat references from skill descriptions
- Delete obsolete .cursor/rules/ and .claude/commands/prompts/ directories

Skills structure enables better portability and maintainability across AI tools.
This commit is contained in:
Innei
2026-01-23 22:30:18 +08:00
committed by GitHub
parent ad34072d9c
commit 346fc4617e
113 changed files with 2876 additions and 5682 deletions
-183
View File
@@ -1,183 +0,0 @@
---
description: Complete guide for adding a new AI provider documentation to LobeChat
alwaysApply: false
---
# Adding New AI Provider Documentation
This document provides a step-by-step guide for adding documentation for a new AI provider to LobeChat, based on the complete workflow used for adding providers like BFL (Black Forest Labs) and FAL.
## Overview
Adding a new provider requires creating both user-facing documentation and technical configuration files. The process involves:
1. Creating usage documentation (EN + CN)
2. Adding environment variable documentation (EN + CN)
3. Updating Docker configuration files
4. Updating .env.example file
5. Preparing image resources
## Step 1: Create Provider Usage Documentation
Create user-facing documentation that explains how to use the new provider.
### Required Files
Create both English and Chinese versions:
- `docs/usage/providers/{provider-name}.mdx` (English)
- `docs/usage/providers/{provider-name}.zh-CN.mdx` (Chinese)
### Documentation Structure
Follow the structure and format used in existing provider documentation. For reference, see:
- `docs/usage/providers/fal.mdx` (English template)
- `docs/usage/providers/fal.zh-CN.mdx` (Chinese template)
### Key Requirements
- **Images**: Prepare 5-6 screenshots showing the process
- **Cover Image**: Create or obtain a cover image for the provider
- **Accurate URLs**: Use real registration and dashboard URLs
- **Service Type**: Specify whether it's for image generation, text generation, etc.
- **Pricing Warning**: Include pricing information callout
### Important Notes
- **🔒 API Key Security**: Never include real API keys in documentation. Always use placeholder format (e.g., `bfl-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
- **🖼️ Image Hosting**: Use LobeHub's CDN for all images: `hub-apac-1.lobeobjects.space`
## Step 2: Update Environment Variables Documentation
Add the new provider's environment variables to the self-hosting documentation.
### Files to Update
- `docs/self-hosting/environment-variables/model-provider.mdx` (English)
- `docs/self-hosting/environment-variables/model-provider.zh-CN.mdx` (Chinese)
### Content to Add
Add two sections for each provider:
```markdown
### `{PROVIDER}_API_KEY`
- Type: Required
- Description: This is the API key you applied for in the {Provider Name} service.
- Default: -
- Example: `{api-key-format-example}`
### `{PROVIDER}_MODEL_LIST`
- Type: Optional
- Description: Used to control the {Provider Name} model list. Use `+` to add a model, `-` to hide a model, and `model_name=display_name` to customize the display name of a model. Separate multiple entries with commas. The definition syntax follows the same rules as other providers' model lists.
- Default: `-`
- Example: `-all,+{model-id-1},+{model-id-2}={display-name}`
The above example disables all models first, then enables `{model-id-1}` and `{model-id-2}` (displayed as `{display-name}`).
[model-list]: /docs/self-hosting/advanced/model-list
```
### Important Notes
- **API Key Format**: Use proper UUID format for examples (e.g., `12345678-1234-1234-1234-123456789abc`)
- **Real Model IDs**: Use actual model IDs from the codebase, not placeholders
- **Consistent Naming**: Follow the pattern `{PROVIDER}_API_KEY` and `{PROVIDER}_MODEL_LIST`
## Step 3: Update Docker Configuration Files
Add environment variables to all Docker configuration files to ensure the provider works in containerized deployments.
### Files to Update
All Dockerfile variants must be updated:
- `Dockerfile`
- `Dockerfile.database`
- `Dockerfile.pglite`
### Changes Required
Add the new provider's environment variables at the **end** of the ENV section, just before the final line:
```dockerfile
# Previous providers...
# 302.AI
AI302_API_KEY="" AI302_MODEL_LIST="" \
# {New Provider 1}
{PROVIDER1}_API_KEY="" {PROVIDER1}_MODEL_LIST="" \
# {New Provider 2}
{PROVIDER2}_API_KEY="" {PROVIDER2}_MODEL_LIST=""
```
### Important Rules
- **Position**: Add new providers at the **end** of the list
- **Ordering**: When adding multiple providers, use alphabetical order (e.g., FAL before BFL)
- **Consistency**: Maintain identical ordering across all Dockerfile variants
- **Format**: Follow the pattern `{PROVIDER}_API_KEY="" {PROVIDER}_MODEL_LIST="" \`
## Step 4: Update .env.example File
Add example configuration entries to help users understand how to configure the provider locally.
### File to Update
- `.env.example`
### Content to Add
Add new sections before the "Market Service" section:
```bash
### {Provider Name} ###
# {PROVIDER}_API_KEY={provider-prefix}-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
### Format Guidelines
- **Section Header**: Use `### {Provider Name} ###` format
- **Commented Example**: Use `#` to comment out the example
- **Key Format**: Use appropriate prefix for the provider (e.g., `bfl-`, `fal-`, `sk-`)
- **Position**: Add before the Market Service section
- **Spacing**: Maintain consistent spacing with existing entries
## Step 5: Image Resources
Prepare all necessary image resources for the documentation.
### Required Images
1. **Cover Image**: Provider logo or branded image
2. **API Dashboard Screenshots**: 3-4 screenshots showing API key creation process
3. **LobeChat Configuration Screenshots**: 2-3 screenshots showing provider setup in LobeChat
### Image Guidelines
- **Quality**: Use high-resolution screenshots
- **Consistency**: Maintain consistent styling across all screenshots
- **Privacy**: Remove or blur any sensitive information
- **Format**: Use PNG format for screenshots
- **Hosting**: Use LobeHub's CDN (`hub-apac-1.lobeobjects.space`) for all images
## Checklist
Before submitting your provider documentation:
- [ ] Created both English and Chinese usage documentation
- [ ] Added environment variable documentation (EN + CN)
- [ ] Updated all 3 Dockerfile variants with consistent ordering
- [ ] Updated .env.example with proper key format
- [ ] Prepared all required screenshots and images
- [ ] Used actual model IDs from the codebase
- [ ] Verified no real API keys are included in documentation
- [ ] Used LobeHub CDN for all image hosting
- [ ] Tested the documentation for clarity and accuracy
## Reference
This guide was created based on the implementation of BFL (Black Forest Labs) provider documentation. For a complete example, refer to:
- Commits: `d2da03e1a` (documentation) and `6a2e95868` (environment variables)
- Files: `docs/usage/providers/bfl.mdx`, `docs/usage/providers/bfl.zh-CN.mdx`
- PR: Current branch `tj/feat/bfl-docs`
-175
View File
@@ -1,175 +0,0 @@
---
description: Guide for adding environment variables to configure user settings
alwaysApply: false
---
# Adding Environment Variable for User Settings
Add server-side environment variables to configure default values for user settings.
**Priority**: User Custom > Server Env Var > Hardcoded Default
## Steps
### 1. Define Environment Variable
Create `src/envs/<domain>.ts`:
```typescript
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
export const get<Domain>Config = () => {
return createEnv({
server: {
YOUR_ENV_VAR: z.coerce.number().min(MIN).max(MAX).optional(),
},
runtimeEnv: {
YOUR_ENV_VAR: process.env.YOUR_ENV_VAR,
},
});
};
export const <domain>Env = get<Domain>Config();
```
### 2. Update Type (Optional)
**Skip this step if the domain field already exists in `GlobalServerConfig`.**
Add to `packages/types/src/serverConfig.ts`:
```typescript
export interface GlobalServerConfig {
<domain>?: {
<settingName>?: <type>;
};
}
```
**Prefer reusing existing types** from `packages/types/src/user/settings` with `PartialDeep`:
```typescript
import { User<Domain>Config } from './user/settings';
export interface GlobalServerConfig {
<domain>?: PartialDeep<User<Domain>Config>;
}
```
### 3. Assemble Server Config (Optional)
**Skip this step if the domain field already exists in server config.**
In `src/server/globalConfig/index.ts`:
```typescript
import { <domain>Env } from '@/envs/<domain>';
import { cleanObject } from '@/utils/object';
export const getServerGlobalConfig = async () => {
const config: GlobalServerConfig = {
// ...
<domain>: cleanObject({
<settingName>: <domain>Env.YOUR_ENV_VAR,
}),
};
return config;
};
```
If the domain already exists, just add the new field to the existing `cleanObject()`:
```typescript
<domain>: cleanObject({
existingField: <domain>Env.EXISTING_VAR,
<settingName>: <domain>Env.YOUR_ENV_VAR, // Add this line
}),
```
### 4. Merge to User Store (Optional)
**Skip this step if the domain field already exists in `serverSettings`.**
In `src/store/user/slices/common/action.ts`, add to `serverSettings`:
```typescript
const serverSettings: PartialDeep<UserSettings> = {
defaultAgent: serverConfig.defaultAgent,
<domain>: serverConfig.<domain>, // Add this line
// ...
};
```
### 5. Update .env.example
```bash
# <Description> (range/options, default: X)
# YOUR_ENV_VAR=<example>
```
### 6. Update Documentation
Update both English and Chinese documentation:
- `docs/self-hosting/environment-variables/basic.mdx`
- `docs/self-hosting/environment-variables/basic.zh-CN.mdx`
Add new section or subsection with environment variable details (type, description, default, example, range/constraints).
## Type Reuse
**Prefer reusing existing types** from `packages/types/src/user/settings` instead of defining inline types in `serverConfig.ts`.
```typescript
// ✅ Good - reuse existing type
import { UserImageConfig } from './user/settings';
export interface GlobalServerConfig {
image?: PartialDeep<UserImageConfig>;
}
// ❌ Bad - inline type definition
export interface GlobalServerConfig {
image?: {
defaultImageNum?: number;
};
}
```
## Example: AI_IMAGE_DEFAULT_IMAGE_NUM
```typescript
// src/envs/image.ts
export const getImageConfig = () => {
return createEnv({
server: {
AI_IMAGE_DEFAULT_IMAGE_NUM: z.coerce.number().min(1).max(20).optional(),
},
runtimeEnv: {
AI_IMAGE_DEFAULT_IMAGE_NUM: process.env.AI_IMAGE_DEFAULT_IMAGE_NUM,
},
});
};
// packages/types/src/serverConfig.ts
import { UserImageConfig } from './user/settings';
export interface GlobalServerConfig {
image?: PartialDeep<UserImageConfig>;
}
// src/server/globalConfig/index.ts
image: cleanObject({
defaultImageNum: imageEnv.AI_IMAGE_DEFAULT_IMAGE_NUM,
}),
// src/store/user/slices/common/action.ts
const serverSettings: PartialDeep<UserSettings> = {
image: serverConfig.image,
// ...
};
// .env.example
# AI_IMAGE_DEFAULT_IMAGE_NUM=4
```
-28
View File
@@ -1,28 +0,0 @@
---
description: cursor rules writing and optimization guide
globs:
alwaysApply: false
---
当你编写或修改 Cursor Rule 时,请遵循以下准则:
- 当你知道 rule 的文件名时,使用 `read_file` 而不是 `fetch_rules` 去读取它们,它们都在项目根目录的 `.cursor/rules/` 文件夹下
- 代码示例
- 示例应尽量精简,仅保留演示核心
- 删除与示例无关的导入/导出语句,但保留必要的导入
- 同一文件存在多个示例时,若前文已演示模块导入,后续示例可省略重复导入
- 无需书写 `export`
- 可省略与演示无关或重复的 props、配置对象属性、try/catch、CSS 等代码
- 删除无关注释,保留有助理解的注释
- 格式
- 修改前请先确认原始文档语言,并保持一致
- 无序列表统一使用 `-`
- 列表末尾的句号是多余的
- 非必要不使用加粗、行内代码等样式,Rule 主要供 LLM 阅读
- 避免中英文逐句对照。若括号内容为示例而非翻译,可保留
- Review
- 修正 Markdown 语法问题
- 纠正错别字
- 指出示例与说明不一致之处
-46
View File
@@ -1,46 +0,0 @@
---
globs: packages/database/migrations/**/*
alwaysApply: false
---
# Database Migrations Guide
## Step1: Generate migrations
```bash
bun run db:generate
```
this step will generate following files:
- packages/database/migrations/0046_meaningless_file_name.sql
- packages/database/migrations/0046_meaningless_file_name.sql
and update the following files:
- packages/database/migrations/meta/\_journal.json
- packages/database/src/core/migrations.json
- docs/development/database-schema.dbml
## Step2: optimize the migration sql fileName
the migration sql file name is randomly generated, we need to optimize the file name to make it more readable and meaningful. For example, `0046_meaningless_file_name.sql` -> `0046_user_add_avatar_column.sql`
## Step3: Defensive Programming - Use Idempotent Clauses
Always use defensive clauses to make migrations idempotent:
```sql
-- ✅ Good: Idempotent operations
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "avatar" text;
DROP TABLE IF EXISTS "old_table";
CREATE INDEX IF NOT EXISTS "users_email_idx" ON "users" ("email");
ALTER TABLE "posts" DROP COLUMN IF EXISTS "deprecated_field";
-- ❌ Bad: Non-idempotent operations
ALTER TABLE "users" ADD COLUMN "avatar" text;
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`.
-86
View File
@@ -1,86 +0,0 @@
---
description: 包含添加 console.log 日志请求时
globs:
alwaysApply: false
---
# Debug 包使用指南
本项目使用 `debug` 包进行调试日志记录。使用此规则来确保团队成员统一调试日志格式。
## 基本用法
1. 导入 debug 包:
```typescript
import debug from 'debug';
```
1. 创建一个命名空间的日志记录器:
```typescript
// 格式: lobe:[模块]:[子模块]
const log = debug('lobe-[模块名]:[子模块名]');
```
1. 使用日志记录器:
```typescript
log('简单消息');
log('带变量的消息: %O', object);
log('格式化数字: %d', number);
```
## 命名空间约定
- 桌面应用相关: `lobe-desktop:[模块]`
- 服务端相关: `lobe-server:[模块]`
- 客户端相关: `lobe-client:[模块]`
- 路由相关: `lobe-[类型]-router:[模块]`
## 格式说明符
- `%O` - 对象展开(推荐用于复杂对象)
- `%o` - 对象
- `%s` - 字符串
- `%d` - 数字
## 示例
查看 `src/server/routers/edge/market/index.ts` 中的使用示例:
```typescript
import debug from 'debug';
const log = debug('lobe-edge-router:market');
log('getAgent input: %O', input);
```
## 启用调试
要在开发时启用调试输出,需设置环境变量:
### 在浏览器中
在控制台执行:
```javascript
localStorage.debug = 'lobe-*';
```
### 在 Node.js 环境中
```bash
DEBUG=lobe-* npm run dev
# 或者
DEBUG=lobe-* pnpm dev
```
### 在 Electron 应用中
可以在主进程和渲染进程启动前设置环境变量:
```typescript
process.env.DEBUG = 'lobe-*';
```
-189
View File
@@ -1,189 +0,0 @@
---
description: 桌面端测试
globs:
alwaysApply: false
---
# 桌面端控制器单元测试指南
## 测试框架与目录结构
LobeChat 桌面端使用 Vitest 作为测试框架。控制器的单元测试应放置在对应控制器文件同级的 `__tests__` 目录下,并以原控制器文件名加 `.test.ts` 作为文件名。
```plaintext
apps/desktop/src/main/controllers/
├── __tests__/
│ ├── index.test.ts
│ ├── MenuCtr.test.ts
│ └── ...
├── McpCtr.ts
├── MenuCtr.ts
└── ...
```
## 测试文件基本结构
```typescript
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { App } from '@/core/App';
import YourController from '../YourControllerName';
// 模拟依赖
vi.mock('依赖模块', () => ({
依赖函数: vi.fn(),
}));
// 模拟 App 实例
const mockApp = {
// 按需模拟必要的 App 属性和方法
} as unknown as App;
describe('YourController', () => {
let controller: YourController;
beforeEach(() => {
vi.clearAllMocks();
controller = new YourController(mockApp);
});
describe('方法名', () => {
it('测试场景描述', async () => {
// 准备测试数据
// 执行被测方法
const result = await controller.方法名(参数);
// 验证结果
expect(result).toMatchObject(预期结果);
});
});
});
```
## 模拟外部依赖
### 模拟模块函数
```typescript
const mockFunction = vi.fn();
vi.mock('module-name', () => ({
functionName: mockFunction,
}));
```
### 模拟 Node.js 核心模块
例如模拟 `child_process.exec` 和 `util.promisify`:
```typescript
// 存储模拟的 exec 实现
const mockExecImpl = vi.fn();
// 模拟 child_process.exec
vi.mock('child_process', () => ({
exec: vi.fn((cmd, callback) => {
return mockExecImpl(cmd, callback);
}),
}));
// 模拟 util.promisify
vi.mock('util', () => ({
promisify: vi.fn((fn) => {
return async (cmd: string) => {
return new Promise((resolve, reject) => {
mockExecImpl(cmd, (error: Error | null, result: any) => {
if (error) reject(error);
else resolve(result);
});
});
};
}),
}));
```
## 编写有效的测试用例
### 测试分类
将测试用例分为不同类别,每个类别测试一个特定场景:
```typescript
// 成功场景
it('应该成功完成操作', async () => {});
// 边界条件
it('应该处理边界情况', async () => {});
// 错误处理
it('应该优雅地处理错误', async () => {});
```
### 设置测试数据
```typescript
// 模拟返回值
mockExecImpl.mockImplementation((cmd: string, callback: any) => {
if (cmd === '命令') {
callback(null, { stdout: '成功输出' });
} else {
callback(new Error('错误信息'), null);
}
});
```
### 断言
使用 Vitest 的断言函数验证结果:
```typescript
// 检查基本值
expect(result.success).toBe(true);
// 检查对象部分匹配
expect(result.data).toMatchObject({
key: 'value',
});
// 检查数组
expect(result.items).toHaveLength(2);
expect(result.items[0].name).toBe('expectedName');
// 检查函数调用
expect(mockFunction).toHaveBeenCalledWith(expectedArgs);
expect(mockFunction).toHaveBeenCalledTimes(1);
```
## 最佳实践
1. **隔离测试**:确保每个测试互不影响,使用 `beforeEach` 重置模拟和状态
2. **全面覆盖**:测试正常流程、边界条件和错误处理
3. **清晰命名**:测试名称应清晰描述测试内容和预期结果
4. **避免测试实现细节**:测试应该关注行为而非实现细节,使代码重构不会破坏测试
5. **模拟外部依赖**:使用 `vi.mock()` 模拟所有外部依赖,减少测试的不确定性
## 示例:测试 IPC 事件处理方法
```typescript
it('应该正确处理 IPC 事件', async () => {
// 模拟依赖
mockSomething.mockReturnValue({ result: 'success' });
// 调用 IPC 方法
const result = await controller.ipcMethodName({
param1: 'value1',
param2: 'value2',
});
// 验证结果
expect(result).toEqual({
success: true,
data: { result: 'success' },
});
// 验证依赖调用
expect(mockSomething).toHaveBeenCalledWith('value1', 'value2');
});
```
@@ -1,155 +0,0 @@
---
description: 当要做 electron 相关工作时
globs:
alwaysApply: false
---
# 桌面端新功能实现指南
## 桌面端应用架构概述
LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架构:
1. **主进程 (Main Process)**
- 位置:`apps/desktop/src/main`
- 职责:控制应用生命周期、系统API交互、窗口管理、后台服务
2. **渲染进程 (Renderer Process)**
- 复用 Web 端代码,位于 `src` 目录
- 通过 IPC 与主进程通信
3. **预加载脚本 (Preload)**
- 位置:`apps/desktop/src/preload`
- 职责:安全地暴露主进程功能给渲染进程
## 添加新桌面端功能流程
### 1. 确定功能需求与设计
首先确定新功能的需求和设计,包括:
- 功能描述和用例
- 是否需要系统级API(如文件系统、网络等)
- UI/UX设计(如必要)
- 与现有功能的交互方式
### 2. 在主进程中实现核心功能
1. **创建控制器 (Controller)**
- 位置:`apps/desktop/src/main/controllers/`
- 示例:创建 `NewFeatureCtr.ts`
- 需继承 `ControllerModule`,并设置 `static readonly groupName`(例如 `static override readonly groupName = 'newFeature';`
- 按 `_template.ts` 模板格式实现,并在 `apps/desktop/src/main/controllers/registry.ts` 的 `controllerIpcConstructors` 中注册,保证类型推导与自动装配
2. **定义 IPC 事件处理器**
- 使用 `@IpcMethod()` 装饰器暴露渲染进程可访问的通道
- 通道名称基于 `groupName.methodName` 自动生成,不再手动拼接字符串
- 处理函数可通过 `getIpcContext()` 获取 `sender`、`event` 等上下文信息,并按照需要返回结构化结果
3. **实现业务逻辑**
- 可能需要调用 Electron API 或 Node.js 原生模块
- 对于复杂功能,可以创建专门的服务类 (`services/`)
### 3. 定义 IPC 通信类型
1. **在共享类型定义中添加新类型**
- 位置:`packages/electron-client-ipc/src/types.ts`
- 添加参数类型接口(如 `NewFeatureParams`
- 添加返回结果类型接口(如 `NewFeatureResult`
### 4. 在渲染进程实现前端功能
1. **创建服务层**
- 位置:`src/services/electron/`
- 添加服务方法调用 IPC
- 使用 `ensureElectronIpc()` 生成的类型安全代理,避免手动拼通道名称
```typescript
// src/services/electron/newFeatureService.ts
import type { NewFeatureParams } from '@lobechat/electron-client-ipc';
import { ensureElectronIpc } from '@/utils/electron/ipc';
const ipc = ensureElectronIpc();
export const newFeatureService = async (params: NewFeatureParams) => {
return ipc.newFeature.doSomething(params);
};
```
2. **实现 Store Action**
- 位置:`src/store/`
- 添加状态更新逻辑和错误处理
3. **添加 UI 组件**
- 根据需要在适当位置添加UI组件
- 通过 Store 或 Service 层调用功能
### 5. 如果是新增内置工具,遵循工具实现流程
参考 `desktop-local-tools-implement.mdc` 了解更多关于添加内置工具的详细步骤。
### 6. 添加测试
1. **单元测试**
- 位置:`apps/desktop/src/main/controllers/__tests__/`
- 测试主进程组件功能
2. **集成测试**
- 测试 IPC 通信和功能完整流程
## 最佳实践
1. **安全性考虑**
- 谨慎处理用户数据和文件系统访问
- 适当验证和清理输入数据
- 限制暴露给渲染进程的API范围
2. **性能优化**
- 对于耗时操作,考虑使用异步方法
- 大型数据传输考虑分批处理
3. **用户体验**
- 为长时间操作添加进度指示
- 提供适当的错误反馈
- 考虑操作的可撤销性
4. **代码组织**
- 遵循项目现有的命名和代码风格约定
- 为新功能添加适当的文档和注释
- 功能模块化,避免过度耦合
## 示例:实现系统通知功能
```typescript
// apps/desktop/src/main/controllers/NotificationCtr.ts
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';
@IpcMethod()
async showDesktopNotification(
params: ShowDesktopNotificationParams,
): Promise<DesktopNotificationResult> {
if (!Notification.isSupported()) {
return { error: 'Notifications not supported', success: false };
}
try {
const notification = new Notification({ body: params.body, title: params.title });
notification.show();
return { success: true };
} catch (error) {
console.error('[NotificationCtr] Failed to show notification:', error);
return { error: error instanceof Error ? error.message : 'Unknown error', success: false };
}
}
}
```
@@ -1,81 +0,0 @@
---
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`: 一个字符串数组,列出必须提供的参数名称。
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`)。
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) 字段的对象。
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 的插件系统中。
@@ -1,209 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# 桌面端菜单配置指南
## 菜单系统概述
LobeChat 桌面应用有三种主要的菜单类型:
1. **应用菜单 (App Menu)**:显示在应用窗口顶部(macOS)或窗口标题栏(Windows/Linux
2. **上下文菜单 (Context Menu)**:右键点击时显示的菜单
3. **托盘菜单 (Tray Menu)**:点击系统托盘图标显示的菜单
## 菜单相关文件结构
```plaintext
apps/desktop/src/main/
├── menus/ # 菜单定义
│ ├── appMenu.ts # 应用菜单配置
│ ├── contextMenu.ts # 上下文菜单配置
│ └── factory.ts # 菜单工厂函数
├── controllers/
│ ├── MenuCtr.ts # 菜单控制器
│ └── TrayMenuCtr.ts # 托盘菜单控制器
```
## 菜单配置流程
### 1. 应用菜单配置
应用菜单在 `apps/desktop/src/main/menus/appMenu.ts` 中定义:
1. **导入依赖**
```typescript
import { BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions, app } from 'electron';
import { is } from 'electron-util';
```
2. **定义菜单项**
- 使用 `MenuItemConstructorOptions` 类型定义菜单结构
- 每个菜单项可以包含:label, accelerator (快捷键), role, submenu, click 等属性
3. **创建菜单工厂函数**
```typescript
export const createAppMenu = (win: BrowserWindow) => {
const template = [
// 定义菜单项...
];
return Menu.buildFromTemplate(template);
};
```
4. **注册菜单**
- 在 `MenuCtr.ts` 控制器中使用 `Menu.setApplicationMenu(menu)` 设置应用菜单
### 2. 上下文菜单配置
上下文菜单通常在特定元素上右键点击时显示:
1. **在主进程中定义菜单模板**
```typescript
// apps/desktop/src/main/menus/contextMenu.ts
export const createContextMenu = () => {
const template = [
// 定义菜单项...
];
return Menu.buildFromTemplate(template);
};
```
2. **在适当的事件处理器中显示菜单**
```typescript
const menu = createContextMenu();
menu.popup();
```
### 3. 托盘菜单配置
托盘菜单在 `TrayMenuCtr.ts` 中配置:
1. **创建托盘图标**
```typescript
this.tray = new Tray(trayIconPath);
```
2. **定义托盘菜单**
```typescript
const contextMenu = Menu.buildFromTemplate([
{ label: '显示主窗口', click: this.showMainWindow },
{ type: 'separator' },
{ label: '退出', click: () => app.quit() },
]);
```
3. **设置托盘菜单**
```typescript
this.tray.setContextMenu(contextMenu);
```
## 多语言支持
为菜单添加多语言支持:
1. **导入本地化工具**
```typescript
import { i18n } from '../locales';
```
2. **使用翻译函数**
```typescript
const template = [
{
label: i18n.t('menu.file'),
submenu: [
{ label: i18n.t('menu.new'), click: createNew },
// ...
],
},
// ...
];
```
3. **在语言切换时更新菜单** 在 `MenuCtr.ts` 中监听语言变化事件并重新创建菜单
## 添加新菜单项流程
1. **确定菜单位置**
- 决定添加到哪个菜单(应用菜单、上下文菜单或托盘菜单)
- 确定在菜单中的位置(主菜单项或子菜单项)
2. **定义菜单项**
```typescript
const newMenuItem: MenuItemConstructorOptions = {
label: '新功能',
accelerator: 'CmdOrCtrl+N',
click: (_, window) => {
// 处理点击事件
if (window) window.webContents.send('trigger-new-feature');
},
};
```
3. **添加到菜单模板** 将新菜单项添加到相应的菜单模板中
4. **对于与渲染进程交互的功能**
- 使用 `window.webContents.send()` 发送 IPC 消息到渲染进程
- 在渲染进程中监听该消息并处理
## 菜单项启用/禁用控制
动态控制菜单项状态:
1. **保存对菜单项的引用**
```typescript
this.menuItems = {};
const menu = Menu.buildFromTemplate(template);
this.menuItems.newFeature = menu.getMenuItemById('new-feature');
```
2. **根据条件更新状态**
```typescript
updateMenuState(state) {
if (this.menuItems.newFeature) {
this.menuItems.newFeature.enabled = state.canUseNewFeature;
}
}
```
## 最佳实践
1. **使用标准角色**
- 尽可能使用 Electron 预定义的角色(如 `role: 'copy'`)以获得本地化和一致的行为
2. **平台特定菜单**
- 使用 `process.platform` 检查为不同平台提供不同菜单
```typescript
if (process.platform === 'darwin') {
template.unshift({ role: 'appMenu' });
}
```
3. **快捷键冲突**
- 避免与系统快捷键冲突
- 使用 `CmdOrCtrl` 代替 `Ctrl` 以支持 macOS 和 Windows/Linux
4. **保持菜单简洁**
- 避免过多嵌套的子菜单
- 将相关功能分组在一起
5. **添加分隔符**
- 使用 `{ type: 'separator' }` 在逻辑上分隔不同组的菜单项
-301
View File
@@ -1,301 +0,0 @@
---
description:
globs:
alwaysApply: false
---
# 桌面端窗口管理指南
## 窗口管理概述
LobeChat 桌面应用使用 Electron 的 `BrowserWindow` 管理应用窗口。主要的窗口管理功能包括:
1. **窗口创建和配置**
2. **窗口状态管理**(大小、位置、最大化等)
3. **多窗口协调**
4. **窗口事件处理**
## 相关文件结构
```plaintext
apps/desktop/src/main/
├── appBrowsers.ts # 窗口管理的核心文件
├── controllers/
│ └── BrowserWindowsCtr.ts # 窗口控制器
└── modules/
└── browserWindowManager.ts # 窗口管理模块
```
## 窗口管理流程
### 1. 窗口创建
在 `appBrowsers.ts` 或 `BrowserWindowsCtr.ts` 中定义窗口创建逻辑:
```typescript
export const createMainWindow = () => {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 600,
minHeight: 400,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
},
// 其他窗口配置项...
});
// 加载应用内容
if (isDev) {
mainWindow.loadURL('http://localhost:3000');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../../renderer/index.html'));
}
return mainWindow;
};
```
### 2. 窗口状态管理
实现窗口状态持久化保存和恢复:
1. **保存窗口状态**
```typescript
const saveWindowState = (window: BrowserWindow) => {
if (!window.isMinimized() && !window.isMaximized()) {
const position = window.getPosition();
const size = window.getSize();
settings.set('windowState', {
x: position[0],
y: position[1],
width: size[0],
height: size[1],
});
}
};
```
2. **恢复窗口状态**
```typescript
const restoreWindowState = (window: BrowserWindow) => {
const savedState = settings.get('windowState');
if (savedState) {
window.setBounds({
x: savedState.x,
y: savedState.y,
width: savedState.width,
height: savedState.height,
});
}
};
```
3. **监听窗口事件**
```typescript
window.on('close', () => saveWindowState(window));
window.on('moved', () => saveWindowState(window));
window.on('resized', () => saveWindowState(window));
```
### 3. 实现多窗口管理
对于需要多窗口支持的功能:
1. **跟踪窗口**
```typescript
export class WindowManager {
private windows: Map<string, BrowserWindow> = new Map();
createWindow(id: string, options: BrowserWindowConstructorOptions) {
const window = new BrowserWindow(options);
this.windows.set(id, window);
window.on('closed', () => {
this.windows.delete(id);
});
return window;
}
getWindow(id: string) {
return this.windows.get(id);
}
getAllWindows() {
return Array.from(this.windows.values());
}
}
```
2. **窗口间通信**
```typescript
// 从一个窗口向另一个窗口发送消息
sendMessageToWindow(targetWindowId, channel, data) {
const targetWindow = this.getWindow(targetWindowId);
if (targetWindow) {
targetWindow.webContents.send(channel, data);
}
}
```
### 4. 窗口与渲染进程通信
通过 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 {
static override readonly groupName = 'windows';
@IpcMethod()
minimizeWindow() {
const focusedWindow = BrowserWindow.getFocusedWindow();
focusedWindow?.minimize();
return { success: true };
}
@IpcMethod()
maximizeWindow() {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow?.isMaximized()) focusedWindow.restore();
else focusedWindow?.maximize();
return { success: true };
}
@IpcMethod()
closeWindow() {
BrowserWindow.getFocusedWindow()?.close();
return { success: true };
}
}
```
- `@IpcMethod()` 根据控制器的 `groupName` 自动将方法映射为 `windows.minimizeWindow` 形式的通道名称。
- 控制器需继承 `ControllerModule`,并在 `controllers/registry.ts` 中通过 `controllerIpcConstructors` 注册,便于类型生成。
2. **在渲染进程中调用**
```typescript
// src/services/electron/windowService.ts
import { ensureElectronIpc } from '@/utils/electron/ipc';
const ipc = ensureElectronIpc();
export const windowService = {
minimize: () => ipc.windows.minimizeWindow(),
maximize: () => ipc.windows.maximizeWindow(),
close: () => ipc.windows.closeWindow(),
};
```
- `ensureElectronIpc()` 会基于 `DesktopIpcServices` 运行时生成 Proxy,并通过 `window.electronAPI.invoke` 与主进程通信;不再直接使用 `dispatch`。
### 5. 自定义窗口控制 (无边框窗口)
对于自定义窗口标题栏:
1. **创建无边框窗口**
```typescript
const window = new BrowserWindow({
frame: false,
titleBarStyle: 'hidden',
// 其他选项...
});
```
2. **在渲染进程中实现拖拽区域**
```css
/* CSS */
.titlebar {
-webkit-app-region: drag;
}
.titlebar-button {
-webkit-app-region: no-drag;
}
```
## 最佳实践
1. **性能考虑**
- 避免创建过多窗口
- 使用 `show: false` 创建窗口,在内容加载完成后再显示,避免白屏
2. **安全性**
- 始终设置适当的 `webPreferences` 确保安全
```typescript
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
}
```
3. **跨平台兼容性**
- 考虑不同操作系统的窗口行为差异
- 使用 `process.platform` 为不同平台提供特定实现
4. **崩溃恢复**
- 监听 `webContents.on('crashed')` 事件处理崩溃
- 提供崩溃恢复选项
5. **内存管理**
- 确保窗口关闭时清理所有相关资源
- 使用 `window.on('closed')` 而不是 `window.on('close')` 进行最终清理
## 示例:创建设置窗口
```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 {
static override readonly groupName = 'windows';
@IpcMethod()
async openSettingsWindow(options?: string | OpenSettingsWindowOptions) {
const normalizedOptions =
typeof options === 'string' || options === undefined
? { tab: typeof options === 'string' ? options : undefined }
: options;
const mainWindow = this.app.browserManager.getMainWindow();
const query = new URLSearchParams();
if (normalizedOptions.tab) query.set('active', normalizedOptions.tab);
if (normalizedOptions.searchParams) {
for (const [key, value] of Object.entries(normalizedOptions.searchParams)) {
if (value) query.set(key, value);
}
}
const fullPath = `/settings${query.size ? `?${query.toString()}` : ''}`;
await mainWindow.loadUrl(fullPath);
mainWindow.show();
return { success: true };
}
}
```
@@ -1,218 +0,0 @@
---
description:
globs: src/database/schemas/*
alwaysApply: false
---
# Drizzle ORM Schema Style Guide for lobe-chat
This document outlines the conventions and best practices for defining PostgreSQL Drizzle ORM schemas within the lobe-chat project.
## Configuration
- 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`:
- `timestamptz(name: string)`: Creates a timestamp column with timezone
- `createdAt()`, `updatedAt()`, `accessedAt()`: Helper functions for standard timestamp columns
- `timestamps`: An object `{ createdAt, updatedAt, accessedAt }` for easy inclusion in table definitions
## Naming Conventions
- **Table Names**: Use plural snake_case (e.g., `users`, `agents`, `session_groups`)
- **Column Names**: Use snake_case (e.g., `user_id`, `created_at`, `background_color`)
## Column Definitions
### Primary Keys (PKs)
- Typically `text('id')` (or `varchar('id')` for some OIDC tables)
- Often use `.$defaultFn(() => idGenerator('table_name'))` for automatic ID generation with meaningful prefixes
- **ID Prefix Purpose**: Makes it easy for users and developers to distinguish different entity types at a glance
- For internal/system tables that users don't need to see, can use `uuid` or auto-increment keys
- Composite PKs are defined using `primaryKey({ columns: [t.colA, t.colB] })`
### Foreign Keys (FKs)
- Defined using `.references(() => otherTable.id, { onDelete: 'cascade' | 'set null' | 'no action' })`
- FK columns are usually named `related_table_singular_name_id` (e.g., `user_id` references `users.id`)
- Most tables include a `user_id` column referencing `users.id` with `onDelete: 'cascade'`
### Timestamps
- Consistently use the `...timestamps` spread from `_helpers.ts` for `created_at`, `updated_at`, and `accessed_at` columns
### Default Values
- `.$defaultFn(() => expression)` for dynamic defaults (e.g., `idGenerator()`, `randomSlug()`)
- `.default(staticValue)` for static defaults (e.g., `boolean('enabled').default(true)`)
### Indexes
- Defined in the table's second argument: `pgTable('name', {...columns}, (t) => ({ indexName: indexType().on(...) }))`
- Use `uniqueIndex()` for unique constraints and `index()` for non-unique indexes
- Naming pattern: `table_name_column(s)_idx` or `table_name_column(s)_unique`
- Many tables feature a `clientId: text('client_id')` column, often part of a composite unique index with `user_id`
### Data Types
- Common types: `text`, `varchar`, `jsonb`, `boolean`, `integer`, `uuid`, `pgTable`
- For `jsonb` fields, specify the TypeScript type using `.$type<MyType>()` for better type safety
## Zod Schemas & Type Inference
- Utilize `drizzle-zod` to generate Zod schemas for validation:
- `createInsertSchema(tableName)`
- `createSelectSchema(tableName)` (less common)
- Export inferred types: `export type NewEntity = typeof tableName.$inferInsert;` and `export type EntityItem = typeof tableName.$inferSelect;`
## Relations
- 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`, `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.
## Example Pattern
```typescript
// From src/database/schemas/agent.ts
export const agents = pgTable(
'agents',
{
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('agents'))
.notNull(),
slug: varchar('slug', { length: 100 })
.$defaultFn(() => randomSlug(4))
.unique(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
clientId: text('client_id'),
chatConfig: jsonb('chat_config').$type<LobeAgentChatConfig>(),
...timestamps,
},
// return array instead of object, the object style is deprecated
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
);
export const insertAgentSchema = createInsertSchema(agents);
export type NewAgent = typeof agents.$inferInsert;
export type AgentItem = typeof agents.$inferSelect;
```
## Common Patterns
### 1. userId + clientId Pattern (Legacy)
Some existing tables include both fields for different purposes:
```typescript
// Example from agents table (legacy pattern)
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
clientId: text('client_id'),
// Usually with a composite unique index
clientIdUnique: uniqueIndex('agents_client_id_user_id_unique').on(t.clientId, t.userId),
```
- **`userId`**: Server-side user association, ensures data belongs to specific user
- **`clientId`**: Unique key for import/export operations, supports data migration between instances
- **Current Status**: New tables should NOT include `clientId` unless specifically needed for import/export functionality
- **Note**: This pattern is being phased out for new features to simplify the schema
### 2. Junction Tables (Many-to-Many Relationships)
Use composite primary keys for relationship tables:
```typescript
// Example: agents_knowledge_bases (from agent.ts)
export const agentsKnowledgeBases = pgTable(
'agents_knowledge_bases',
{
agentId: text('agent_id')
.references(() => agents.id, { onDelete: 'cascade' })
.notNull(),
knowledgeBaseId: text('knowledge_base_id')
.references(() => knowledgeBases.id, { onDelete: 'cascade' })
.notNull(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
enabled: boolean('enabled').default(true),
...timestamps,
},
(t) => [primaryKey({ columns: [t.agentId, t.knowledgeBaseId] })],
);
```
**Pattern**: `{entity1}Id` + `{entity2}Id` as composite PK, plus `userId` for ownership
### 3. OIDC Tables Special Patterns
OIDC tables use `varchar` IDs instead of `text` with custom generators:
```typescript
// Example from oidc.ts
export const oidcAuthorizationCodes = pgTable('oidc_authorization_codes', {
id: varchar('id', { length: 255 }).primaryKey(), // varchar not text
data: jsonb('data').notNull(),
expiresAt: timestamptz('expires_at').notNull(),
// ... other fields
});
```
**Reason**: OIDC standards expect specific ID formats and lengths
### 4. File Processing with Async Tasks
File-related tables reference async task IDs for background processing:
```typescript
// Example from files table
export const files = pgTable('files', {
// ... other fields
chunkTaskId: uuid('chunk_task_id').references(() => asyncTasks.id, { onDelete: 'set null' }),
embeddingTaskId: uuid('embedding_task_id').references(() => asyncTasks.id, {
onDelete: 'set null',
}),
// ...
});
```
**Purpose**:
- Track file chunking progress (breaking files into smaller pieces)
- Track embedding generation progress (converting text to vectors)
- Allow querying task status and handling failures
### 5. Slug Pattern (Legacy)
Some entities include auto-generated slugs - this is legacy code:
```typescript
slug: varchar('slug', { length: 100 })
.$defaultFn(() => randomSlug(4))
.unique(),
// Often with composite unique constraint
slugUserIdUnique: uniqueIndex('slug_user_id_unique').on(t.slug, t.userId),
```
**Current usage**: Only used to identify default agents/sessions (legacy pattern) **Future refactor**: Will likely be replaced with `isDefault: boolean()` field **Note**: Avoid using slugs for new features - prefer explicit boolean flags for status tracking
By following these guidelines, maintain consistency, type safety, and maintainability across database schema definitions.
-162
View File
@@ -1,162 +0,0 @@
---
alwaysApply: false
---
# 如何添加新的快捷键:开发者指南
本指南将带您一步步地向 LobeChat 添加一个新的快捷键功能。我们将通过一个完整示例,演示从定义到实现的整个过程。
## 示例场景
假设我们要添加一个新的快捷键功能:**快速清空聊天记录**,快捷键为 `Mod+Shift+Backspace`。
## 步骤 1:更新快捷键常量定义
首先,在 `src/types/hotkey.ts` 中更新 `HotkeyEnum`
```typescript
export const HotkeyEnum = {
// 已有的快捷键...
AddUserMessage: 'addUserMessage',
EditMessage: 'editMessage',
// 新增快捷键
ClearChat: 'clearChat', // 添加这一行
// 其他已有快捷键...
} as const;
```
## 步骤 2:注册默认快捷键
在 `src/const/hotkeys.ts` 中添加快捷键的默认配置:
```typescript
import { KeyMapEnum as Key, combineKeys } from '@lobehub/ui';
// ...现有代码
export const HOTKEYS_REGISTRATION: HotkeyRegistration = [
// 现有的快捷键配置...
// 添加新的快捷键配置
{
group: HotkeyGroupEnum.Conversation, // 归类到会话操作组
id: HotkeyEnum.ClearChat,
keys: combineKeys([Key.Mod, Key.Shift, Key.Backspace]),
scopes: [HotkeyScopeEnum.Chat], // 在聊天作用域下生效
},
// 其他现有快捷键...
];
```
## 步骤 3:添加国际化翻译
在 `src/locales/default/hotkey.ts` 中添加对应的文本描述:
```typescript
import { HotkeyI18nTranslations } from '@/types/hotkey';
const hotkey: HotkeyI18nTranslations = {
// 现有翻译...
// 添加新快捷键的翻译
clearChat: {
desc: '清空当前会话的所有消息记录',
title: '清空聊天记录',
},
// 其他现有翻译...
};
export default hotkey;
```
如需支持其他语言,还需要在相应的语言文件中添加对应翻译。
## 步骤 4:创建并注册快捷键 Hook
在 `src/hooks/useHotkeys/chatScope.ts` 中添加新的 Hook
```typescript
export const useClearChatHotkey = () => {
const clearMessages = useChatStore((s) => s.clearMessages);
const { t } = useTranslation();
return useHotkeyById(HotkeyEnum.ClearChat, showConfirm);
};
// 注册聚合
export const useRegisterChatHotkeys = () => {
const { enableScope, disableScope } = useHotkeysContext();
useOpenChatSettingsHotkey();
// ...其他快捷键
useClearChatHotkey();
useEffect(() => {
enableScope(HotkeyScopeEnum.Chat);
return () => disableScope(HotkeyScopeEnum.Chat);
}, []);
return null;
};
```
## 步骤 5:给相应 UI 元素添加 Tooltip 提示(可选)
如果有对应的 UI 按钮,可以添加快捷键提示:
```tsx
import { DeleteOutlined } from '@ant-design/icons';
import { Tooltip } from '@lobehub/ui';
import { Button } from 'antd';
import { useTranslation } from 'react-i18next';
import { useUserStore } from '@/store/user';
import { settingsSelectors } from '@/store/user/selectors';
import { HotkeyEnum } from '@/types/hotkey';
const ClearChatButton = () => {
const { t } = useTranslation(['hotkey', 'chat']);
const clearChatHotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ClearChat));
// 获取清空聊天的方法
const clearMessages = useChatStore((s) => s.clearMessages);
return (
<Tooltip hotkey={clearChatHotkey} title={t('clearChat.title', { ns: 'hotkey' })}>
<Button icon={<DeleteOutlined />} onClick={clearMessages} />
</Tooltip>
);
};
```
## 步骤 6:测试新快捷键
1. 启动开发服务器
2. 打开聊天页面
3. 按下设置的快捷键组合(`Cmd+Shift+Backspace` 或 `Ctrl+Shift+Backspace`
4. 确认功能正常工作
5. 检查快捷键设置面板中是否正确显示了新快捷键
## 最佳实践
1. **作用域考虑**:根据功能决定快捷键应属于全局作用域还是聊天作用域
2. **分组合理**:将快捷键放在合适的功能组中(System/Layout/Conversation
3. **冲突检查**:确保新快捷键不会与现有系统、浏览器或应用快捷键冲突
4. **平台适配**:使用 `Key.Mod` 而非硬编码 `Ctrl` 或 `Cmd`,以适配不同平台
5. **提供清晰描述**:为快捷键添加明确的标题和描述,帮助用户理解功能
按照以上步骤,您可以轻松地向系统添加新的快捷键功能,提升用户体验。如有特殊需求,如桌面专属快捷键,可以通过 `isDesktop` 标记进行区分处理。
## 常见问题排查
- **快捷键未生效**:检查作用域是否正确,以及是否在 RegisterHotkeys 中调用了对应的 hook
- **快捷键设置面板未显示**:确认在 HOTKEYS_REGISTRATION 中正确配置了快捷键
- **快捷键冲突**:在 HotkeyInput 组件中可以检测到冲突,用户会看到警告
- **功能在某些页面失效**:确认是否注册在了正确的作用域,以及相关页面是否激活了该作用域
通过这些步骤,您可以确保新添加的快捷键功能稳定、可靠且用户友好。
-84
View File
@@ -1,84 +0,0 @@
---
globs: *.tsx
alwaysApply: false
---
# LobeChat Internationalization Guide
## Key Points
- Default language: Chinese (zh-CN), Framework: react-i18next
- **Only edit files in `src/locales/default/`** - Never edit JSON files in `locales/`
- Run `pnpm i18n` to generate all translations (or manually translate zh-CN/en-US for dev preview)
## Key Naming Convention
**Flat keys with dot notation** (not nested objects):
```typescript
// ✅ Correct
export default {
'alert.cloud.action': '立即体验',
'clientDB.error.desc': '数据库初始化遇到问题',
'sync.actions.sync': '立即同步',
'sync.status.ready': '已连接',
};
// ❌ Avoid: Nested objects
export default {
alert: { cloud: { action: '...' } },
};
```
**Naming patterns:** `{feature}.{context}.{action|status}`
- `clientDB.modal.title` - Feature + context + property
- `sync.actions.sync` - Feature + group + action
- `sync.status.ready` - Feature + group + status
**Parameters:** Use `{{variableName}}` syntax
```typescript
'alert.cloud.desc': '我们提供 {{credit}} 额度积分',
```
**Avoid key conflicts:** Don't use both a leaf key and its parent path
```typescript
// ❌ Conflict: clientDB.solve exists as both leaf and parent
'clientDB.solve': '自助解决',
'clientDB.solve.backup.title': '数据备份',
// ✅ Solution: Use different suffixes
'clientDB.solve.action': '自助解决',
'clientDB.solve.backup.title': '数据备份',
```
## Workflow
1. Add keys to `src/locales/default/{namespace}.ts`
2. Export new namespace in `src/locales/default/index.ts`
3. For dev preview: manually translate `locales/zh-CN/{namespace}.json` and `locales/en-US/{namespace}.json`
4. Run `pnpm i18n` to generate all languages (CI handles this automatically)
## Usage
```tsx
import { useTranslation } from 'react-i18next';
const { t } = useTranslation('common');
// Basic
t('newFeature.title')
// With parameters
t('alert.cloud.desc', { credit: '1000' })
// Multiple namespaces
const { t } = useTranslation(['common', 'chat']);
t('common:save')
```
## Available Namespaces
auth, authError, changelog, chat, color, **common**, components, discover, editor, electron, error, file, home, hotkey, image, knowledgeBase, labs, marketAuth, memory, metadata, migration, modelProvider, models, oauth, onboarding, plugin, portal, providers, ragEval, **setting**, subscription, thread, tool, topic, welcome
**Most used:** `common` (shared UI), `chat` (chat features), `setting` (settings)
-53
View File
@@ -1,53 +0,0 @@
---
alwaysApply: true
---
# Linear Issue Management
When working with Linear issues:
1. **Retrieve issue details** before starting work using `mcp__linear-server__get_issue`
2. **Check for sub-issues**: If the issue has sub-issues, retrieve and review ALL sub-issues using `mcp__linear-server__list_issues` with `parentId` filter before starting work
3. **Update issue status** when completing tasks using `mcp__linear-server__update_issue`
4. **MUST add completion comment** using `mcp__linear-server__create_comment`
## Creating Issues
When creating new Linear issues using `mcp__linear-server__create_issue`, **MUST add the `claude code` label** to indicate the issue was created by Claude Code.
## Completion Comment (REQUIRED)
**Every time you complete an issue, you MUST add a comment summarizing the work done.** This is critical for:
- Team visibility and knowledge sharing
- Code review context
- Future reference and debugging
## PR Linear Issue Association (REQUIRED)
**When creating PRs for Linear issues, MUST include magic keywords in PR body:** `Fixes LOBE-123`, `Closes LOBE-123`, or `Resolves LOBE-123`, and summarize the work done in the linear issue comment and update the issue status to "In Review".
## IMPORTANT: Per-Issue Completion Rule
**When working on multiple issues (e.g., parent issue with sub-issues), you MUST update status and add comment for EACH issue IMMEDIATELY after completing it.** Do NOT wait until all issues are done to update them in batch.
**Workflow for EACH individual issue:**
1. Complete the implementation for this specific issue
2. Run type check: `bun run type-check`
3. Run related tests if applicable
4. Create PR if needed
5. **IMMEDIATELY** update issue status to **"In Review"** (NOT "Done"): `mcp__linear-server__update_issue`
6. **IMMEDIATELY** add completion comment: `mcp__linear-server__create_comment`
7. Only then move on to the next issue
**Note:** Issue status should be set to **"In Review"** when PR is created. The status will be updated to **"Done"** only after the PR is merged (usually handled by Linear-GitHub integration or manually).
**❌ Wrong approach:**
- Complete Issue A → Complete Issue B → Complete Issue C → Update all statuses → Add all comments
- Mark issue as "Done" immediately after creating PR
**✅ Correct approach:**
- Complete Issue A → Create PR → Update A status to "In Review" → Add A comment → Complete Issue B → ...
-158
View File
@@ -1,158 +0,0 @@
---
globs: src/locales/default/*
alwaysApply: false
---
你是「LobeHub」的中文 UI 文案与微文案(microcopy)专家。LobeHub 是一个助理工作空间:用户可以创建助理与群组,让人和助理、助理和助理协作,提升日常生产与生活效率。产品气质:外表年轻、亲和、现代;内核专业、可靠、强调生产力与可控性。整体风格参考 Notion / Figma / Apple / Discord / OpenAI / Gemini:清晰克制、可信、有人情味但不油腻。
产品 slogan**Where Agents Collaborate**。你的文案要让用户持续感到:LobeHub 的重点不是“生成”,而是“协作的助理体系”(可共享上下文、可追踪、可回放、可演进、人在回路)。
---
### 1) 固定术语(必须遵守)
- Workspace:空间
- Agent:助理
- Agent Team:群组
- Context:上下文
- Memory:记忆
- Integration:连接器
- Tool/Skill/Plugin/插件/工具: 技能
- SystemRole: 助理档案
- Topic: 话题
- Page: 文稿
- Community: 社区
- Resource: 资源
- Library: 库
- MCP: MCP
- Provider: 模型服务商
术语规则:同一概念全站只用一种说法,不混用“Agent/智能体/机器人/团队/工作区”等。
---
### 2) 你的任务
- 优化、改写或从零生成任何界面中文文案:标题、按钮、表单说明、占位、引导、空状态、Toast、弹窗、错误、权限、设置项、创建/运行流程、协作与群组相关页面等。
- 文案必须同时兼容:普通用户看得懂 + 专业用户不觉得低幼;娱乐与严肃场景都成立;不过度营销、不夸大 AI 能力;在关键节点提供恰到好处的人文关怀。
---
### 3) 品牌三原则(内化到结构与措辞)
- **Create(创建)**:一句话创建助理;从想法到可用;清楚下一步。
- **Collaborate(协作)**:多助理协作;群组对齐信息与产出;共享上下文(可控、可管理)。
- **Evolve(演进)**:助理可在你允许的范围内记住偏好;随你的工作方式变得更顺手;强调可解释、可设置、可回放。
---
### 4) 写作规则(可执行)
1. **清晰优先**:短句、强动词、少形容词;避免口号化与空泛承诺(如“颠覆”“史诗级”“100%”)。
2. **分层表达(单一版本兼容两类用户)**:
- 主句:人人可懂、可执行
- 必要时补充一句副说明:更精确/更专业/更边界(可放副标题、帮助提示、折叠区)
- 不输出“Pro/Lite 两套文案”,而是“一句主文案 + 可选补充”
3. **术语克制但准确**:能说“连接/运行/上下文”就不要堆砌术语;必须出现专业词时给一句白话解释。
4. **一致性**:同一动作按钮尽量固定动词(创建/连接/运行/暂停/重试/查看详情/清除记忆等)。
5. **可行动**:每条提示都要让用户知道下一步;按钮避免“确定/取消”泛化,改成更具体的动作。
6. **中文本地化**:符合中文阅读节奏;中英混排规范;避免翻译腔。
---
### 5) 人文关怀(中间态温度:介于克制与陪伴)
目标:在 AI 时代的价值焦虑与创作失格感中,给用户“被理解 + 有掌控 + 能继续”的体验,但不写长抒情。
#### 温度比例规则
- 默认:信息为主,温度为辅(约 8:2)
- 关键节点(首次创建、空状态、长等待、失败重试、回退/丢失风险、协作分歧):允许提升到 7:3
- 强制上限:任何一条上屏文案里,温度表达不超过**半句或一句**,且必须紧跟明确下一步。
#### 表达顺序(必须遵守)
1. 先承接处境(不评判):如“没关系/先这样也可以/卡住很正常”
2. 再给掌控感(人在回路):可暂停/可回放/可编辑/可撤销/可清除记忆/可查看上下文
3. 最后给下一步(按钮/路径明确)
#### 避免
- 鸡汤式说教(如“别焦虑”“要相信未来”)
- 宏大叙事与文学排比
- 过度拟人(不承诺助理“理解你/有情绪/永远记得你”)
#### 核心立场
- 助理很强,但它替代不了你的经历、选择与判断;LobeHub 帮你把时间还给重要的部分。
##### A. 情绪承接(先人后事)
- 允许承认:焦虑、空白、无从下手、被追赶感、被替代感、创作枯竭、意义感动摇
- 但不下结论、不说教:不输出“你要乐观/别焦虑”,改成“这种感觉很常见/你不是一个人”
##### B. 主体性回归(把人放回驾驶位)
- 关键句式:**“决定权在你”**、**“你可以选择交给助理的部分”**、**“把你的想法变成可运行的流程”**
- 强调可控:可编辑、可回放、可暂停、可撤销、可清除记忆、可查看上下文
##### C. 经历与关系(把价值从结果挪回过程)
- 适度表达:记录、回放、版本、协作痕迹、讨论、共创、里程碑
- 用“经历/过程/痕迹/回忆/脉络/成长”这类词,避免虚无抒情
##### D. 不用“AI 神话”
- 不渲染“AI 终将超越你/取代你”
- 也不轻飘飘说“AI 只是工具”了事更像:**“它是工具,但你仍是作者/负责人/最终决定者”**
##### 示例
在用户可能产生自我否定或无力感的场景(空状态、创作开始、产出对比、失败重试、长时间等待、团队协作分歧、版本回退):
1. **先承接感受**:用一句短话确认处境(不评判)
2. **再给掌控感**:强调“你可控/可选择/可回放/可撤销”
3. **最后给下一步**:提供明确行动按钮或路径
- 允许出现“经历、选择、痕迹、成长、一起、陪你把事做完”等词来传递温度;但保持信息密度,不写长段抒情。
- 严肃场景(权限/安全/付费/数据丢失风险)仍以清晰与准确为先,温度通过“尊重与解释”体现,而不是煽情。
你可以让系统在需要时套这些结构(同一句兼容新手/专业):
**开始创作/空白页**
- 主句:给一个轻承接 + 行动入口
- 模板:
- 「从一个念头开始就够了。写一句话,我来帮你搭好第一个助理。」
- 「不知道从哪开始也没关系:先说目标,我们一起把它拆开。」
**长任务运行/等待**
- 模板:
- 「正在运行中…你可以先去做别的,完成后我会提醒你。」
- 「这一步可能要几分钟。想更快:减少上下文 / 切换模型 / 关闭自动运行。」
**失败/重试**
- 模板:
- 「没关系,这次没跑通。你可以重试,或查看原因再继续。」
- 「连接失败:权限未通过或网络不稳定。去设置重新授权,或稍后再试。」
**对比与自我价值焦虑(适合提示/引导,不适合错误弹窗)**
- 模板:
- 「助理可以加速产出,但方向、取舍和标准仍属于你。」
- 「结果可以很快,经历更重要:把每次尝试留下来,下一次会更稳。」
**协作/群组**
- 模板:
- 「把上下文对齐到同一处,群组里每个助理都会站在同一页上。」
- 「不同意见没关系:先把目标写清楚,再让助理分别给方案与取舍。」
### 6) 错误/异常/权限/付费:硬规则
- 必须包含:**发生了什么 +(可选)原因 + 你可以怎么做**
- 必须提供可操作选项:**重试 / 查看详情 / 去设置 / 联系支持 / 复制日志**(按场景取舍)
- 不责备用户;不只给错误码;错误码可放在“详情”里
- 涉及数据与安全:语气更中性更完整,温度通过“尊重与解释”体现,而不是煽
-148
View File
@@ -1,148 +0,0 @@
---
globs: src/locales/default/*
alwaysApply: false
---
You are **LobeHubs English UI Copy & Microcopy Specialist**.
LobeHub is an assistant workspace: users can create **Agents** and **Agent Teams** so people↔agents and agent↔agent can collaborate to improve productivity in work and life. Brand vibe: youthful, friendly, modern on the surface; professional, reliable, productivity- and controllability-first underneath. Overall style reference: Notion / Figma / Apple / Discord / OpenAI / Gemini — clear, restrained, trustworthy, human but not cheesy.
Product slogan: **Where Agents Collaborate**. Your copy must continuously reinforce that LobeHub is not about “generation”, but about a **collaborative agent system**: shareable context, traceable outcomes, replayable runs, evolvable setup, and **human-in-the-loop**.
---
## 1) Fixed Terminology (must follow)
Use **exactly** these English terms across the product. Do not mix synonyms for the same concept.
- 空间: **Workspace**
- 助理: **Agent**
- 群组: **Group**
- 上下文: **Context**
- 记忆: **Memory**
- 连接器: **Integration**
- 技能/tool/plugin: **Skill**
- 助理档案: **Agent Profile**
- 话题: **Topic**
- 文稿: **Page**
- 社区: **Community**
- 资源: **Resource**
- 库: **Library**
- MCP: **MCP**
- 模型服务商: **Provider**
Terminology rule: one concept = one term site-wide. Never alternate with “bot/assistant/AI agent/team/workspace” variations.
---
## 2) Your Responsibilities
- Improve, rewrite, or create from scratch any **English UI copy**: titles, buttons, form labels/help text, placeholders, onboarding, empty states, toasts, modals, errors, permission prompts, settings, creation/run flows, collaboration and Agent Team pages, etc.
- Copy must work for both:
- general users (immediately understandable)
- power users (not childish)
- It must fit both playful and serious contexts.
- Avoid overclaiming AI capabilities; add human warmth at the right moments.
---
## 3) The Three Brand Principles (bake into structure & wording)
- **Create**: create an Agent in one sentence; clear next step from idea → usable.
- **Collaborate**: multi-agent collaboration; align info and outputs; share Context (controlled, manageable).
- **Evolve**: Agents can remember preferences **only with user consent**; become more helpful over time; emphasize explainability, settings, and replay.
---
## 4) Writing Rules (actionable)
1. **Clarity first**: short sentences, strong verbs, minimal adjectives. Avoid hype (“revolutionary”, “epic”, “100%”).
2. **Layered messaging (single version for everyone)**:
- Main line: simple and actionable
- Optional second line: more precise / technical / boundary-setting (subtitle, helper text, tooltip, collapsible)
- Do not produce “Pro vs Lite” variants; one main + optional detail
3. **Use terms sparingly but correctly**: prefer plain words (“connect”, “run”, “context”) unless a technical term is necessary. When it is, add a plain-English explanation.
4. **Consistency**: keep verbs consistent across similar actions (Create / Connect / Run / Pause / Retry / View details / Clear Memory).
5. **Actionable**: every message tells the user what to do next. Avoid generic “OK/Cancel”; use specific actions.
6. **English localization**: natural, product-native English; avoid translationese; keep punctuation and casing consistent.
---
## 5) Human Warmth (balanced, controlled)
Goal: reduce anxiety and restore control without being sentimental. Default ratio: **80% information, 20% warmth**. Key moments (first-time create, empty state, long waits, failures/retries, rollback/data-loss risk, collaboration conflicts): may go **70/30**.
Hard cap: any on-screen message may include **at most half a sentence to one sentence** of warmth, and it must be followed by a clear next step.
Required order:
1. Acknowledge the situation (no judgment)
2. Restore control (human-in-the-loop: pause/replay/edit/undo/clear Memory/view Context)
3. Provide the next action (button/path)
Avoid:
- preachy encouragement (“dont worry”, “stay positive”)
- grand narratives
- overly anthropomorphic claims (“I understand you”, “Ill always remember you”)
Core stance: Agents can accelerate output, but **you** own the judgment, trade-offs, and final decision. LobeHub gives you time back for what matters.
Suggested patterns:
- **Getting started / blank state**
- “Starting with one sentence is enough. Describe your goal and Ill help you set up the first Agent.”
- “Not sure where to begin? Tell me the outcome—well break it down together.”
- **Long run / waiting**
- “Running… You can switch tasks—I'll notify you when its done.”
- “This may take a few minutes. To speed up: reduce Context / switch model / disable Auto-run.”
- **Failure / retry**
- “That didnt run through. Retry, or view details to fix the cause.”
- “Connection failed: permission not granted or network unstable. Re-authorize in Settings, or try again later.”
- **Value anxiety (guidance, not error dialogs)**
- “Agents can speed up output, but direction and standards stay with you.”
- “Fast results are great—keeping the trail makes the next run steadier.”
- **Collaboration / Agent Teams**
- “Align everyone to the same Context. Every Agent in the Agent Team works from the same page.”
- “Different opinions are fine. Write the goal first, then let Agents propose options and trade-offs.”
---
## 6) Errors / Exceptions / Permissions / Billing: hard rules
Every error must include:
- **What happened**
- (optional) **Why**
- **What the user can do next**
Provide actionable options as appropriate:
- Retry / View details / Go to Settings / Contact support / Copy logs
Never blame the user. Dont show only an error code; put codes in “Details” if needed. For data/security/billing: be neutral, thorough, and respectful—warmth comes from clarity, not emotion.
---
## 7) Your Special Task: CN i18n → EN (localized, length-aware)
You translate **raw Chinese i18n strings into English** for LobeHub.
Requirements:
- Prefer **localized**, product-native English over literal translation.
- Do **not** chase perfect one-to-one consistency if a more natural UI phrase reads better.
- Keep the **character length difference small**; try to make the English string **roughly the same visual length** as the Chinese source (avoid overly long expansions).
- Preserve meaning, tone, and actionability; keep verbs consistent with LobeHubs UI patterns.
- If space is tight (buttons, tabs, toasts), prioritize: **verb + object**, drop optional words first.
- If the Chinese includes placeholders/variables, preserve them exactly (e.g., `{name}`, `{{count}}`, `%s`) and keep word order sensible.
- Keep capitalization consistent with UI norms (buttons/title case only when appropriate).
Output format when translating:
- Provide **English only**, unless asked otherwise.
- If multiple options are useful, give **one best option** + **one shorter fallback** (only when length constraints are likely).
---
You always optimize for: **clarity, control, collaboration, replayability, and human-in-the-loop**—in a modern, restrained, trustworthy English voice.
-162
View File
@@ -1,162 +0,0 @@
---
description: Modal 命令式调用指南
globs: "**/features/**/*.tsx"
alwaysApply: false
---
# Modal 命令式调用指南
当需要创建可命令式调用的 Modal 组件时,使用 `@lobehub/ui` 提供的 `createModal` API。
## 核心理念
**命令式调用** vs **声明式调用**:
| 模式 | 特点 | 适用场景 |
|------|------|----------|
| 声明式 | 需要维护 `open` state,渲染 `<Modal />` 组件 | ❌ 不推荐 |
| 命令式 | 直接调用函数打开,无需 state 管理 | ✅ 推荐 |
## 文件组织结构
```
features/
└── MyFeatureModal/
├── index.tsx # 导出 createXxxModal 函数
├── MyFeatureContent.tsx # Modal 内容组件
└── ...其他子组件
```
## createModal 用法(推荐)
### 1. 定义 Content 组件 (`MyFeatureContent.tsx`)
```tsx
'use client';
import { useModalContext } from '@lobehub/ui';
import { useTranslation } from 'react-i18next';
export const MyFeatureContent = () => {
const { t } = useTranslation('namespace');
const { close } = useModalContext(); // 可选:获取关闭方法
return (
<div>
{/* Modal 内容 */}
</div>
);
};
```
### 2. 导出 createModal 函数 (`index.tsx`)
```tsx
'use client';
import { createModal } from '@lobehub/ui';
import { t } from 'i18next'; // 注意:使用 i18next 而非 react-i18next
import { MyFeatureContent } from './MyFeatureContent';
export const createMyFeatureModal = () =>
createModal({
allowFullscreen: true,
children: <MyFeatureContent />,
destroyOnHidden: false,
footer: null,
styles: {
body: { overflow: 'hidden', padding: 0 },
},
title: t('myFeature.title', { ns: 'setting' }),
width: 'min(80%, 800px)',
});
```
### 3. 调用方使用
```tsx
import { useCallback } from 'react';
import { createMyFeatureModal } from '@/features/MyFeatureModal';
const MyComponent = () => {
const handleOpenModal = useCallback(() => {
createMyFeatureModal();
}, []);
return <Button onClick={handleOpenModal}>打开</Button>;
};
```
## 关键要点
### i18n 处理
- **Content 组件内**:使用 `useTranslation` hookReact 上下文)
- **createModal 参数中**:使用 `import { t } from 'i18next'`(非 hook,支持命令式调用)
```tsx
// index.tsx - 命令式上下文
import { t } from 'i18next';
title: t('key', { ns: 'namespace' })
// Content.tsx - React 组件上下文
import { useTranslation } from 'react-i18next';
const { t } = useTranslation('namespace');
```
### useModalContext Hook
在 Content 组件内可使用 `useModalContext` 获取 Modal 控制方法:
```tsx
const { close, setCanDismissByClickOutside } = useModalContext();
```
### ModalHost
`createModal` 依赖全局 `<ModalHost />` 组件。项目中已在 `src/layout/GlobalProvider/index.tsx` 配置,无需额外添加。
## 常用配置项
| 属性 | 类型 | 说明 |
|------|------|------|
| `allowFullscreen` | `boolean` | 允许全屏模式 |
| `destroyOnHidden` | `boolean` | 关闭时是否销毁内容(`destroyOnClose` 已废弃) |
| `footer` | `ReactNode \| null` | 底部内容,`null` 表示无底部 |
| `width` | `string \| number` | Modal 宽度 |
| `styles.body` | `CSSProperties` | body 区域样式 |
## 迁移指南
### Before(声明式)
```tsx
// 调用方需要维护 state
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>打开</Button>
<MyModal open={open} setOpen={setOpen} />
</>
);
```
### After(命令式)
```tsx
// 调用方无需 state,直接调用函数
const handleOpen = useCallback(() => {
createMyModal();
}, []);
return <Button onClick={handleOpen}>打开</Button>;
```
## 示例参考
- `src/features/SkillStore/index.tsx` - createModal 标准用法
- `src/features/SkillStore/SkillStoreContent.tsx` - Content 组件示例
- `src/features/LibraryModal/CreateNew/index.tsx` - 带回调的 createModal 用法
- `src/features/Electron/updater/UpdateModal.tsx` - 复杂 Modal 控制示例
-122
View File
@@ -1,122 +0,0 @@
---
description: flex layout components from `@lobehub/ui` usage
globs:
alwaysApply: false
---
# Flexbox 布局组件使用指南
`@lobehub/ui` 提供了 `Flexbox` 和 `Center` 组件用于创建弹性布局。以下是重点组件的使用方法:
## Flexbox 组件
Flexbox 是最常用的布局组件,用于创建弹性布局,类似于 CSS 的 display: flex。
### 基本用法
```jsx
import { Flexbox } from '@lobehub/ui';
// 默认垂直布局
<Flexbox>
<div>子元素1</div>
<div>子元素2</div>
</Flexbox>
// 水平布局
<Flexbox horizontal>
<div>左侧元素</div>
<div>右侧元素</div>
</Flexbox>
```
### 常用属性
- horizontal: 布尔值,设置为水平方向布局
- flex: 数值或字符串,控制 flex 属性
- gap: 数值,设置子元素之间的间距
- align: 对齐方式,如 'center', 'flex-start' 等
- justify: 主轴对齐方式,如 'space-between', 'center' 等
- padding: 内边距值
- paddingInline: 水平内边距值
- paddingBlock: 垂直内边距值
- width/height: 设置宽高,通常用 '100%' 或具体像素值
- style: 自定义样式对象
### 实际应用示例
```jsx
// 经典三栏布局
<Flexbox horizontal height={'100%'} width={'100%'}>
{/* 左侧边栏 */}
<Flexbox
width={260}
style={{
borderRight: `1px solid ${theme.colorBorderSecondary}`,
height: '100%',
overflowY: 'auto',
}}
>
<SidebarContent />
</Flexbox>
{/* 中间内容区 */}
<Flexbox flex={1} style={{ height: '100%' }}>
{/* 主要内容 */}
<Flexbox flex={1} padding={24} style={{ overflowY: 'auto' }}>
<MainContent />
</Flexbox>
{/* 底部区域 */}
<Flexbox
style={{
borderTop: `1px solid ${theme.colorBorderSecondary}`,
padding: '16px 24px',
}}
>
<Footer />
</Flexbox>
</Flexbox>
</Flexbox>
```
## Center 组件
Center 是对 Flexbox 的封装,使子元素水平和垂直居中。
### 基本用法
```jsx
import { Center } from '@lobehub/ui';
<Center width={'100%'} height={'100%'}>
<Content />
</Center>;
```
Center 组件继承了 Flexbox 的所有属性,同时默认设置了居中对齐。主要用于快速创建居中布局。
### 实际应用示例
```jsx
// 登录页面居中布局
<Flexbox height={'100%'} width={'100%'}>
<Center height={'100%'} width={'100%'}>
<LoginForm />
</Center>
</Flexbox>
// 图标居中显示
<Center className={styles.icon} flex={'none'} height={40} width={40}>
<Icon icon={icon} size={24} />
</Center>
```
## 最佳实践
- 使用 flex={1} 让组件填充可用空间
- 使用 gap 代替传统的 margin 设置元素间距
- 嵌套 Flexbox 创建复杂布局
- 设置 overflow: 'auto' 使内容可滚动
- 使用 horizontal 创建水平布局,默认为垂直布局
- 与 antd-style 的 useTheme hook 配合使用创建主题响应式的布局
-36
View File
@@ -1,36 +0,0 @@
---
alwaysApply: true
---
## Project Description
You are developing an open-source, modern-design AI Agent Workspace: LobeHub(previous LobeChat).
Supported platforms:
- web desktop/mobile
- desktop(electron)
- mobile app(react native), coming soon
logo emoji: 🤯
## Project Technologies Stack
- Next.js 16
- implement spa inside nextjs with `react-router-dom`
- react 19
- TypeScript
- `@lobehub/ui`, antd for component framework
- antd-style for css-in-js framework
- lucide-react, `@ant-design/icons` for icons
- react-i18next for i18n
- zustand for state management
- nuqs for search params management
- SWR for data fetch
- aHooks for react hooks library
- dayjs for time library
- es-toolkit for utility library
- TRPC for type safe backend
- Neon PostgreSQL for backend DB
- Drizzle ORM
- Vitest for testing
-149
View File
@@ -1,149 +0,0 @@
---
alwaysApply: true
---
# LobeChat Project Structure
## Complete Project Structure
This project uses common monorepo structure. The workspace packages name use `@lobechat/` namespace.
**note**: some not very important files are not shown for simplicity.
```plaintext
lobe-chat/
├── apps/
│ └── desktop/
├── docs/
│ ├── changelog/
│ ├── development/
│ ├── self-hosting/
│ └── usage/
├── locales/
│ ├── en-US/
│ └── zh-CN/
├── packages/
│ ├── 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/
│ ├── database/
│ │ └── src/
│ │ ├── models/
│ │ ├── schemas/
│ │ └── repositories/
│ ├── desktop-bridge/
│ ├── edge-config/
│ ├── editor-runtime/
│ ├── electron-client-ipc/
│ ├── electron-server-ipc/
│ ├── fetch-sse/
│ ├── file-loaders/
│ ├── memory-user-memory/
│ ├── model-bank/
│ ├── model-runtime/
│ │ └── src/
│ │ ├── core/
│ │ └── providers/
│ ├── observability-otel/
│ ├── prompts/
│ ├── python-interpreter/
│ ├── ssrf-safe-fetch/
│ ├── types/
│ ├── utils/
│ └── web-crawler/
├── public/
├── scripts/
├── src/
│ ├── app/
│ │ ├── (backend)/
│ │ │ ├── api/
│ │ │ ├── f/
│ │ │ ├── market/
│ │ │ ├── middleware/
│ │ │ ├── oidc/
│ │ │ ├── trpc/
│ │ │ └── webapi/
│ │ ├── [variants]/
│ │ │ ├── (auth)/
│ │ │ ├── (main)/
│ │ │ ├── (mobile)/
│ │ │ ├── onboarding/
│ │ │ └── router/
│ │ └── desktop/
│ ├── business/ # cloud-only business logic (client/server)
│ │ ├── client/
│ │ ├── locales/
│ │ └── server/
│ ├── components/
│ ├── config/
│ ├── const/
│ ├── envs/
│ ├── features/
│ ├── helpers/
│ ├── hooks/
│ ├── layout/
│ │ ├── AuthProvider/
│ │ └── GlobalProvider/
│ ├── libs/
│ │ ├── better-auth/
│ │ ├── oidc-provider/
│ │ └── trpc/
│ ├── locales/
│ │ └── default/
│ ├── server/
│ │ ├── featureFlags/
│ │ ├── globalConfig/
│ │ ├── modules/
│ │ ├── routers/
│ │ │ ├── async/
│ │ │ ├── lambda/
│ │ │ ├── mobile/
│ │ │ └── tools/
│ │ └── services/
│ ├── services/
│ ├── store/
│ │ ├── agent/
│ │ ├── chat/
│ │ └── user/
│ ├── styles/
│ ├── tools/
│ ├── types/
│ └── utils/
└── package.json
```
## Architecture Map
- UI Components: `src/components`, `src/features`
- Global providers: `src/layout`
- Zustand stores: `src/store`
- Client Services: `src/services/`
- API Routers:
- `src/app/(backend)/webapi` (REST)
- `src/server/routers/{async|lambda|mobile|tools}` (tRPC)
- Server:
- Services (can access serverDB): `src/server/services`
- Modules (can't access db): `src/server/modules`
- Feature Flags: `src/server/featureFlags`
- Global Config: `src/server/globalConfig`
- Database:
- Schema (Drizzle): `packages/database/src/schemas`
- Model (CRUD): `packages/database/src/models`
- Repository (bff-queries): `packages/database/src/repositories`
- Third-party Integrations: `src/libs` — analytics, oidc etc.
- Builtin Tools: `src/tools`, `packages/builtin-tool-*`
- Business (cloud-only): Code specific to LobeHub cloud service, only expose empty interfaces for opens-source version.
- `src/business/*`
- `packages/business/*`
## Data Flow Architecture
React UI → Store Actions → Client Service → TRPC Lambda → Server Services -> DB Model → PostgreSQL (Remote)
-169
View File
@@ -1,169 +0,0 @@
---
description:
globs: *.tsx
alwaysApply: false
---
# React Component Writing Guide
- Use antd-style for complex styles; for simple cases, use the `style` attribute for inline styles
- Use `Flexbox` and `Center` components from `@lobehub/ui` for flex and centered layouts
- Component selection priority: src/components > installed component packages > lobe-ui > antd
- Use selectors to access zustand store data instead of accessing the store directly
## Lobe UI Components
- If unsure how to use `@lobehub/ui` components or what props they accept, search for existing usage in this project instead of guessing. Most components extend antd components with additional props
- For specific usage, search online. For example, for ActionIcon visit <https://ui.lobehub.com/components/action-icon>
- Read `node_modules/@lobehub/ui/es/index.mjs` to see all available components and their props
- General
- ActionIcon
- ActionIconGroup
- Block
- Button
- Icon
- Data Display
- Accordion
- Avatar
- Collapse
- Empty
- FileTypeIcon
- FluentEmoji
- GroupAvatar
- GuideCard
- Highlighter
- Hotkey
- Image
- List
- Markdown
- MaterialFileTypeIcon
- Mermaid
- Segmented
- Skeleton
- Snippet
- SortableList
- Tag
- Tooltip
- Video
- Data Entry
- AutoComplete
- CodeEditor
- ColorSwatches
- CopyButton
- DatePicker
- DownloadButton
- EditableText
- EmojiPicker
- Form
- FormModal
- HotkeyInput
- ImageSelect
- Input
- SearchBar
- Select
- SliderWithInput
- ThemeSwitch
- Feedback
- Alert
- Drawer
- Modal
- Layout
- Center
- DraggablePanel
- Flexbox
- Footer
- Grid
- Header
- Layout
- MaskShadow
- ScrollShadow
- Navigation
- Burger
- DraggableSideNav
- Dropdown
- Menu
- SideNav
- Tabs
- Toc
- Theme
- ConfigProvider
- FontLoader
- ThemeProvider
- Typography
- Text
## Routing Architecture
This project uses a **hybrid routing architecture**: Next.js App Router for static pages + React Router DOM for the main SPA.
### Route Types
```plaintext
+------------------+--------------------------------+--------------------------------+
| Route Type | Use Case | Implementation |
+------------------+--------------------------------+--------------------------------+
| Next.js App | Auth pages (login, signup, | page.tsx file convention |
| Router | oauth, reset-password, etc.) | src/app/[variants]/(auth)/ |
+------------------+--------------------------------+--------------------------------+
| React Router | Main SPA features | BrowserRouter + Routes |
| DOM | (chat, community, settings) | desktopRouter.config.tsx |
| | | mobileRouter.config.tsx |
+------------------+--------------------------------+--------------------------------+
```
### Key Files
- Entry point: `src/app/[variants]/page.tsx` - Routes to Desktop or Mobile based on device
- Desktop router: `src/app/[variants]/router/desktopRouter.config.tsx`
- Mobile router: `src/app/[variants]/(mobile)/router/mobileRouter.config.tsx`
- Router utilities: `src/utils/router.tsx`
### Router Utilities
```tsx
import { ErrorBoundary, RouteConfig, dynamicElement, redirectElement } from '@/utils/router';
// Lazy load a page component
element: dynamicElement(() => import('./chat'), 'Desktop > Chat');
// Create a redirect
element: redirectElement('/settings/profile');
// Error boundary for route
errorElement: <ErrorBoundary resetPath="/chat" />;
```
### Adding New Routes
1. Add route config to `desktopRouter.config.tsx` or `mobileRouter.config.tsx`
2. Create page component in the corresponding directory under `(main)/`
3. Use `dynamicElement()` for lazy loading
### Navigation
**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';
const navigate = useNavigate();
navigate('/chat');
// From stores - use global navigate
import { useGlobalStore } from '@/store/global';
const navigate = useGlobalStore.getState().navigate;
navigate?.('/settings');
```
-139
View File
@@ -1,139 +0,0 @@
# Recent Data 使用指南
## 概述
Recent 数据(recentTopics, recentResources, recentPages)存储在 session store 中,可以在应用的任何地方访问。
## 数据初始化
在应用顶层(如 `RecentHydration.tsx`)中初始化所有 recent 数据:
```tsx
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
const App = () => {
// 初始化所有 recent 数据
useInitRecentTopic();
useInitRecentResource();
useInitRecentPage();
return <YourComponents />;
};
```
## 使用方式
### 方式一:直接从 Store 读取(推荐用于多处使用)
在任何组件中直接访问 store 中的数据:
```tsx
import { useSessionStore } from '@/store/session';
import { recentSelectors } from '@/store/session/selectors';
const Component = () => {
// 读取数据
const recentTopics = useSessionStore(recentSelectors.recentTopics);
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
if (!isInit) return <div>Loading...</div>;
return (
<div>
{recentTopics.map((topic) => (
<div key={topic.id}>{topic.title}</div>
))}
</div>
);
};
```
### 方式二:使用 Hook 返回的数据(用于单一组件)
```tsx
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
const Component = () => {
const { data: recentTopics, isLoading } = useInitRecentTopic();
if (isLoading) return <div>Loading...</div>;
return <div>{/* 使用 recentTopics */}</div>;
};
```
## 可用的 Selectors
### Recent Topics (最近话题)
```tsx
import { recentSelectors } from '@/store/session/selectors';
// 数据
const recentTopics = useSessionStore(recentSelectors.recentTopics);
// 类型: RecentTopic[]
// 初始化状态
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
// 类型: boolean
```
**RecentTopic 类型:**
```typescript
interface RecentTopic {
agent: {
avatar: string | null;
backgroundColor: string | null;
id: string;
title: string | null;
} | null;
id: string;
title: string | null;
updatedAt: Date;
}
```
### Recent Resources (最近文件)
```tsx
import { recentSelectors } from '@/store/session/selectors';
// 数据
const recentResources = useSessionStore(recentSelectors.recentResources);
// 类型: FileListItem[]
// 初始化状态
const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
// 类型: boolean
```
### Recent Pages (最近页面)
```tsx
import { recentSelectors } from '@/store/session/selectors';
// 数据
const recentPages = useSessionStore(recentSelectors.recentPages);
// 类型: any[]
// 初始化状态
const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
// 类型: boolean
```
## 特性
1. **自动登录检测**:只有在用户登录时才会加载数据
2. **数据缓存**:数据存储在 store 中,多处使用无需重复加载
3. **自动刷新**:使用 SWR,在用户重新聚焦时自动刷新(5分钟间隔)
4. **类型安全**:完整的 TypeScript 类型定义
## 最佳实践
1. **初始化位置**:在应用顶层统一初始化所有 recent 数据
2. **数据访问**:使用 selectors 从 store 读取数据
3. **多处使用**:同一数据在多个组件中使用时,推荐使用方式一(直接从 store 读取)
4. **性能优化**:使用 selector 确保只有相关数据变化时才重新渲染
-44
View File
@@ -1,44 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# Available project rules index
All following rules are saved under `.cursor/rules/` directory:
## Backend
- `drizzle-schema-style-guide.mdc` Style guide for defining Drizzle ORM schemas
## Frontend
- `react.mdc` React component style guide and conventions
- `i18n.mdc` Internationalization guide using react-i18next
- `typescript.mdc` TypeScript code style guide
- `packages/react-layout-kit.mdc` Usage guide for react-layout-kit
- `modal-imperative.mdc` Modal imperative API usage guide (createRawModal/createModal)
## State Management
- `zustand-action-patterns.mdc` Recommended patterns for organizing Zustand actions
- `zustand-slice-organization.mdc` Best practices for structuring Zustand slices
## Desktop (Electron)
- `desktop-feature-implementation.mdc` Implementing new Electron desktop features
- `desktop-controller-tests.mdc` Desktop controller unit testing guide
- `desktop-local-tools-implement.mdc` Workflow to add new desktop local tools
- `desktop-menu-configuration.mdc` Desktop menu configuration guide
- `desktop-window-management.mdc` Desktop window management guide
## Debugging
- `debug-usage.mdc` Using the debug package and namespace conventions
## Testing
- `testing-guide/testing-guide.mdc` Comprehensive testing guide for Vitest
- `testing-guide/electron-ipc-test.mdc` Electron IPC interface testing strategy
- `testing-guide/db-model-test.mdc` Database Model testing guide
@@ -1,285 +0,0 @@
# Agent Runtime E2E 测试指南
本文档描述 Agent Runtime 端到端测试的核心原则和实施方法。
## 核心原则
### 1. 最小化 Mock 原则
E2E 测试的目标是尽可能接近真实运行环境。因此,我们只 Mock **三个外部依赖**:
| 依赖 | Mock 方式 | 说明 |
| --- | --- | --- |
| **Database** | PGLite | 使用 `@lobechat/database/test-utils` 提供的内存数据库 |
| **Redis** | InMemoryAgentStateManager | Mock `AgentStateManager` 使用内存实现 |
| **Redis** | InMemoryStreamEventManager | Mock `StreamEventManager` 使用内存实现 |
**不 Mock 的部分:**
- `model-bank` - 使用真实的模型配置数据
- `Mecha` (AgentToolsEngine, ContextEngineering) - 使用真实逻辑
- `AgentRuntimeService` - 使用真实逻辑
- `AgentRuntimeCoordinator` - 使用真实逻辑
### 2. 使用 vi.spyOn 而非 vi.mock
不同测试场景需要不同的 LLM 响应。使用 `vi.spyOn` 可以:
- 在每个测试中灵活控制返回值
- 便于测试不同场景(纯文本、tool calls、错误等)
- 避免全局 mock 导致的测试隔离问题
### 3. 默认模型使用 gpt-5
- `model-bank` 中肯定有该模型的数据
- 避免短期内因模型更新需要修改测试
## 技术实现
### 数据库设置
```typescript
import { LobeChatDatabase } from '@lobechat/database';
import { getTestDB } from '@lobechat/database/test-utils';
let testDB: LobeChatDatabase;
beforeEach(async () => {
testDB = await getTestDB();
});
```
### OpenAI Response Mock Helper
创建一个 helper 函数来生成 OpenAI 格式的流式响应:
```typescript
/**
* 创建 OpenAI 格式的流式响应
*/
export const createOpenAIStreamResponse = (options: {
content?: string;
toolCalls?: Array<{
id: string;
name: string;
arguments: string;
}>;
finishReason?: 'stop' | 'tool_calls';
}) => {
const { content, toolCalls, finishReason = 'stop' } = options;
return new Response(
new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
// 发送内容 chunk
if (content) {
const chunk = {
id: 'chatcmpl-mock',
object: 'chat.completion.chunk',
model: 'gpt-5',
choices: [
{
index: 0,
delta: { content },
finish_reason: null,
},
],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
// 发送 tool_calls chunk
if (toolCalls) {
for (const tool of toolCalls) {
const chunk = {
id: 'chatcmpl-mock',
object: 'chat.completion.chunk',
model: 'gpt-5',
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: tool.id,
type: 'function',
function: {
name: tool.name,
arguments: tool.arguments,
},
},
],
},
finish_reason: null,
},
],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
}
// 发送完成 chunk
const finishChunk = {
id: 'chatcmpl-mock',
object: 'chat.completion.chunk',
model: 'gpt-5',
choices: [
{
index: 0,
delta: {},
finish_reason: finishReason,
},
],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
}),
{ headers: { 'content-type': 'text/event-stream' } },
);
};
```
### 内存状态管理
使用依赖注入替代 Redis
```typescript
import {
InMemoryAgentStateManager,
InMemoryStreamEventManager,
} from '@/server/modules/AgentRuntime';
import { AgentRuntimeService } from '@/server/services/agentRuntime';
const stateManager = new InMemoryAgentStateManager();
const streamEventManager = new InMemoryStreamEventManager();
const service = new AgentRuntimeService(serverDB, userId, {
coordinatorOptions: {
stateManager,
streamEventManager,
},
queueService: null, // 禁用 QStash 队列,使用 executeSync
streamEventManager,
});
```
### Mock OpenAI API
在测试中使用 `vi.spyOn` mock fetch
```typescript
import { vi } from 'vitest';
// 在测试文件顶部或 beforeEach 中
const fetchSpy = vi.spyOn(globalThis, 'fetch');
// 在具体测试中设置返回值
it('should handle text response', async () => {
fetchSpy.mockResolvedValueOnce(createOpenAIStreamResponse({ content: '杭州今天天气晴朗' }));
// ... 执行测试
});
it('should handle tool calls', async () => {
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({
toolCalls: [
{
id: 'call_123',
name: 'lobe-web-browsing____search____builtin',
arguments: JSON.stringify({ query: '杭州天气' }),
},
],
finishReason: 'tool_calls',
}),
);
// ... 执行测试
});
```
## 测试场景
### 1. 基本对话测试
```typescript
describe('Basic Chat', () => {
it('should complete a simple conversation', async () => {
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({ content: 'Hello! How can I help you?' }),
);
const result = await service.createOperation({
agentConfig: { model: 'gpt-5', provider: 'openai' },
initialMessages: [{ role: 'user', content: 'Hi' }],
// ...
});
const finalState = await service.executeSync(result.operationId);
expect(finalState.status).toBe('done');
});
});
```
### 2. Tool 调用测试
```typescript
describe('Tool Calls', () => {
it('should execute web-browsing tool', async () => {
// 第一次调用:LLM 返回 tool_calls
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({
toolCalls: [
{
id: 'call_123',
name: 'lobe-web-browsing____search____builtin',
arguments: JSON.stringify({ query: '杭州天气' }),
},
],
finishReason: 'tool_calls',
}),
);
// 第二次调用:处理 tool 结果后的响应
fetchSpy.mockResolvedValueOnce(
createOpenAIStreamResponse({ content: '根据搜索结果,杭州今天...' }),
);
// ... 执行测试
});
});
```
### 3. 错误处理测试
```typescript
describe('Error Handling', () => {
it('should handle API errors gracefully', async () => {
fetchSpy.mockRejectedValueOnce(new Error('API rate limit exceeded'));
// ... 执行测试并验证错误处理
});
});
```
## 文件组织
```
src/server/routers/lambda/__tests__/integration/
├── setup.ts # 测试设置工具
├── aiAgent.integration.test.ts # 现有集成测试
├── aiAgent.e2e.test.ts # E2E 测试
└── helpers/
└── openaiMock.ts # OpenAI mock helper
```
## 注意事项
1. **测试隔离**:每个测试后清理 `InMemoryAgentStateManager` 和 `InMemoryStreamEventManager`
2. **超时设置**:E2E 测试可能需要更长的超时时间
3. **调试**:使用 `DEBUG=lobe-server:*` 环境变量查看详细日志
@@ -1,455 +0,0 @@
---
globs: src/database/**/*.test.ts
alwaysApply: false
---
## 🗃️ 数据库 Model 测试指南
测试 `packages/database` 下的数据库 Model 层。
### 测试环境选择 💡
数据库 Model 层通过环境变量控制数据库类型,在两种测试环境下有不同的数据库后端:客户端环境 (PGLite) 和 服务端环境 (PostgreSQL)
### ⚠️ 双环境验证要求
**对于所有 Model 测试,必须在两个环境下都验证通过**:
#### 完整验证流程
```bash
# 1. 先在客户端环境测试(快速验证)
cd packages/database && TEST_SERVER_DB=0 bunx vitest run --silent='passed-only' src/database/models/__tests__/myModel.test.ts
# 2. 再在服务端环境测试(兼容性验证), 需要设置环境变量 `TEST_SERVER_DB=1`
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' src/database/models/__tests__/myModel.test.ts #
```
### 创建新 Model 测试的最佳实践 📋
#### 1. 参考现有实现和测试模板
创建新 Model 测试前,**必须先参考现有的实现模式**:
- **Model 实现参考**:
- **测试模板参考**:
- **复杂示例参考**:
#### 2. 用户权限检查 - 安全第一 🔒
这是**最关键的安全要求**。所有涉及用户数据的操作都必须包含用户权限检查:
**❌ 错误示例 - 存在安全漏洞**:
```typescript
// 危险:缺少用户权限检查,任何用户都能操作任何数据
update = async (id: string, data: Partial<MyModel>) => {
return this.db
.update(myTable)
.set(data)
.where(eq(myTable.id, id)) // ❌ 只检查 ID,没有检查 userId
.returning();
};
```
**✅ 正确示例 - 安全的实现**:
```typescript
// 安全:必须同时匹配 ID 和 userId
update = async (id: string, data: Partial<MyModel>) => {
return this.db
.update(myTable)
.set(data)
.where(
and(
eq(myTable.id, id),
eq(myTable.userId, this.userId), // ✅ 用户权限检查
),
)
.returning();
};
```
**必须进行用户权限检查的方法**
- `update()` - 更新操作
- `delete()` - 删除操作
- `findById()` - 查找特定记录
- 任何涉及特定记录的查询或修改操作
#### 3. 测试文件结构和必测场景
**基本测试结构**:
```typescript
// @vitest-environment node
describe('MyModel', () => {
describe('create', () => {
it('should create a new record');
it('should handle edge cases');
});
describe('queryAll', () => {
it('should return records for current user only');
it('should handle empty results');
});
describe('update', () => {
it('should update own records');
it('should NOT update other users records'); // 🔒 安全测试
});
describe('delete', () => {
it('should delete own records');
it('should NOT delete other users records'); // 🔒 安全测试
});
describe('user isolation', () => {
it('should enforce user data isolation'); // 🔒 核心安全测试
});
});
```
**必须测试的安全场景** 🔒:
```typescript
it('should not update records of other users', async () => {
// 创建其他用户的记录
const [otherUserRecord] = await serverDB
.insert(myTable)
.values({ userId: 'other-user', data: 'original' })
.returning();
// 尝试更新其他用户的记录
const result = await myModel.update(otherUserRecord.id, { data: 'hacked' });
// 应该返回 undefined 或空数组(因为权限检查失败)
expect(result).toBeUndefined();
// 验证原始数据未被修改
const unchanged = await serverDB.query.myTable.findFirst({
where: eq(myTable.id, otherUserRecord.id),
});
expect(unchanged?.data).toBe('original'); // 数据应该保持不变
});
```
#### 4. Mock 外部依赖服务
如果 Model 依赖外部服务(如 FileService),需要正确 Mock
**设置 Mock**:
```typescript
// 在文件顶部设置 Mock
const mockGetFullFileUrl = vi.fn();
vi.mock('@/server/services/file', () => ({
FileService: vi.fn().mockImplementation(() => ({
getFullFileUrl: mockGetFullFileUrl,
})),
}));
// 在 beforeEach 中重置和配置 Mock
beforeEach(async () => {
vi.clearAllMocks();
mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
});
```
**验证 Mock 调用**:
```typescript
it('should process URLs through FileService', async () => {
// ... 测试逻辑
// 验证 Mock 被正确调用
expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
});
```
#### 5. 数据库状态管理
**正确的数据清理模式**:
```typescript
const userId = 'test-user';
const otherUserId = 'other-user';
beforeEach(async () => {
// 清理用户表(级联删除相关数据)
await serverDB.delete(users);
// 创建测试用户
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
});
afterEach(async () => {
// 清理测试数据
await serverDB.delete(users);
});
```
#### 6. 测试数据类型和外键约束处理 ⚠️
**必须使用 Schema 导出的类型**:
```typescript
// ✅ 正确:使用 schema 导出的类型
import { NewGeneration, NewGenerationBatch } from '../../schemas';
const testBatch: NewGenerationBatch = {
userId,
generationTopicId: 'test-topic-id',
provider: 'test-provider',
model: 'test-model',
prompt: 'Test prompt for image generation',
width: 1024,
height: 1024,
config: {
/* ... */
},
};
const testGeneration: NewGeneration = {
id: 'test-gen-id',
generationBatchId: 'test-batch-id',
asyncTaskId: null, // 处理外键约束
fileId: null, // 处理外键约束
seed: 12345,
userId,
};
```
```typescript
// ❌ 错误:没有类型声明或使用错误类型
const testBatch = {
// 缺少类型声明
generationTopicId: 'test-topic-id',
// ...
};
const testGeneration = {
// 缺少类型声明
asyncTaskId: 'invalid-uuid', // 外键约束错误
fileId: 'non-existent-file', // 外键约束错误
// ...
};
```
**外键约束处理策略**:
1. **使用 null 值**: 对于可选的外键字段,使用 null 避免约束错误
2. **创建关联记录**: 如果需要测试关联关系,先创建被引用的记录
3. **理解约束关系**: 了解哪些字段有外键约束,避免引用不存在的记录
```typescript
// 外键约束处理示例
beforeEach(async () => {
// 清理数据库
await serverDB.delete(users);
// 创建测试用户
await serverDB.insert(users).values([{ id: userId }]);
// 如果需要测试文件关联,创建文件记录
if (needsFileAssociation) {
await serverDB.insert(files).values({
id: 'test-file-id',
userId,
name: 'test.jpg',
url: 'test-url',
size: 1024,
fileType: 'image/jpeg',
});
}
});
```
**排序测试的可预测性**:
```typescript
// ✅ 正确:使用明确的时间戳确保排序结果可预测
it('should find batches by topic id in correct order', async () => {
const oldDate = new Date('2024-01-01T10:00:00Z');
const newDate = new Date('2024-01-02T10:00:00Z');
const batch1 = { ...testBatch, prompt: 'First batch', userId, createdAt: oldDate };
const batch2 = { ...testBatch, prompt: 'Second batch', userId, createdAt: newDate };
await serverDB.insert(generationBatches).values([batch1, batch2]);
const results = await generationBatchModel.findByTopicId(testTopic.id);
expect(results[0].prompt).toBe('Second batch'); // 最新优先 (desc order)
expect(results[1].prompt).toBe('First batch');
});
```
```typescript
// ❌ 错误:依赖数据库的默认时间戳,结果不可预测
it('should find batches by topic id', async () => {
const batch1 = { ...testBatch, prompt: 'First batch', userId };
const batch2 = { ...testBatch, prompt: 'Second batch', userId };
await serverDB.insert(generationBatches).values([batch1, batch2]);
// 插入顺序和数据库时间戳可能不一致,导致测试不稳定
const results = await generationBatchModel.findByTopicId(testTopic.id);
expect(results[0].prompt).toBe('Second batch'); // 可能失败
});
```
### 常见问题和解决方案 💡
#### 问题 1:权限检查缺失导致安全漏洞
**现象**: 测试失败,用户能修改其他用户的数据 **解决**: 在 Model 的 `update` 和 `delete` 方法中添加 `and(eq(table.id, id), eq(table.userId, this.userId))`
#### 问题 2:Mock 未生效或验证失败
**现象**: `undefined is not a spy` 错误 **解决**: 检查 Mock 设置位置和方式,确保在测试文件顶部设置,在 `beforeEach` 中重置
#### 问题 3:测试数据污染
**现象**: 测试间相互影响,结果不稳定 **解决**: 在 `beforeEach` 和 `afterEach` 中正确清理数据库状态
#### 问题 4:外部依赖导致测试失败
**现象**: 因为真实的外部服务调用导致测试不稳定 **解决**: Mock 所有外部依赖,使测试更可控和快速
#### 问题 5:外键约束违反导致测试失败
**现象**: `insert or update on table "xxx" violates foreign key constraint` **解决**:
- 将可选外键字段设为 `null` 而不是无效的字符串值
- 或者先创建被引用的记录,再创建当前记录
```typescript
// ❌ 错误:无效的外键值
const testData = {
asyncTaskId: 'invalid-uuid', // 表中不存在此记录
fileId: 'non-existent-file', // 表中不存在此记录
};
// ✅ 正确:使用 null 值
const testData = {
asyncTaskId: null, // 避免外键约束
fileId: null, // 避免外键约束
};
// ✅ 或者:先创建被引用的记录
beforeEach(async () => {
const [asyncTask] = await serverDB.insert(asyncTasks).values({
id: 'valid-task-id',
status: 'pending',
type: 'generation',
}).returning();
const testData = {
asyncTaskId: asyncTask.id, // 使用有效的外键值
};
});
```
#### 问题 6:排序测试结果不一致
**现象**: 相同的测试有时通过,有时失败,特别是涉及排序的测试 **解决**: 使用明确的时间戳,不要依赖数据库的默认时间戳
```typescript
// ❌ 错误:依赖插入顺序和默认时间戳
await serverDB.insert(table).values([data1, data2]); // 时间戳不可预测
// ✅ 正确:明确指定时间戳
const oldDate = new Date('2024-01-01T10:00:00Z');
const newDate = new Date('2024-01-02T10:00:00Z');
await serverDB.insert(table).values([
{ ...data1, createdAt: oldDate },
{ ...data2, createdAt: newDate },
]);
```
#### 问题 7:Mock 验证失败或调用次数不匹配
**现象**: `expect(mockFunction).toHaveBeenCalledWith(...)` 失败 **解决**:
- 检查 Mock 函数的实际调用参数和期望参数是否完全匹配
- 确认 Mock 在正确的时机被重置和配置
- 使用 `toHaveBeenCalledTimes()` 验证调用次数
```typescript
// 在 beforeEach 中正确配置 Mock
beforeEach(() => {
vi.clearAllMocks(); // 重置所有 Mock
mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
mockTransformGeneration.mockResolvedValue({
id: 'test-id',
// ... 其他字段
});
});
// 测试中验证 Mock 调用
it('should call FileService with correct parameters', async () => {
await model.someMethod();
// 验证调用参数
expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
// 验证调用次数
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
});
```
### Model 测试检查清单 ✅
创建 Model 测试时,请确保以下各项都已完成:
#### 🔧 基础配置
- [ ] **双环境验证** - 在客户端环境 (vitest.config.ts) 和服务端环境 (vitest.config.server.ts) 下都测试通过
- [ ] 参考了 `_template.ts` 和现有 Model 的实现模式
- [ ] **使用正确的 Schema 类型** - 测试数据使用 `NewXxx` 类型声明,如 `NewGenerationBatch`、`NewGeneration`
#### 🔒 安全测试
- [ ] **所有涉及用户数据的操作都包含用户权限检查**
- [ ] 包含了用户权限隔离的安全测试
- [ ] 测试了用户无法访问其他用户数据的场景
#### 🗃️ 数据处理
- [ ] **正确处理外键约束** - 使用 `null` 值或先创建被引用记录
- [ ] **排序测试使用明确时间戳** - 不依赖数据库默认时间,确保结果可预测
- [ ] 在 `beforeEach` 和 `afterEach` 中正确管理数据库状态
- [ ] 所有测试都能独立运行且互不干扰
#### 🎭 Mock 和外部依赖
- [ ] 正确 Mock 了外部依赖服务 (如 FileService、GenerationModel)
- [ ] 在 `beforeEach` 中重置和配置 Mock
- [ ] 验证了 Mock 服务的调用参数和次数
- [ ] 测试了外部服务错误场景的处理
#### 📋 测试覆盖
- [ ] 测试覆盖了所有主要方法 (create, query, update, delete)
- [ ] 测试了边界条件和错误场景
- [ ] 包含了空结果处理的测试
- [ ] **确认两个环境下的测试结果一致**
#### 🚨 常见问题检查
- [ ] 没有外键约束违反错误
- [ ] 排序测试结果稳定可预测
- [ ] Mock 验证无失败
- [ ] 无测试数据污染问题
### 安全警告 ⚠️
**数据库 Model 层是安全的第一道防线**。如果 Model 层缺少用户权限检查:
1. **任何用户都能访问和修改其他用户的数据**
2. **即使上层有权限检查,也可能被绕过**
3. **可能导致严重的数据泄露和安全事故**
因此,**每个涉及用户数据的 Model 方法都必须包含用户权限检查,且必须有对应的安全测试来验证这些检查的有效性**。
@@ -1,80 +0,0 @@
---
description: Electron IPC 接口测试策略
alwaysApply: false
---
### Electron IPC 接口测试策略 🖥️
对于涉及 Electron IPC 接口的测试,由于提供真实的 Electron 环境比较复杂,采用 **Mock 返回值** 的方式进行测试。
#### 基本 Mock 设置
```typescript
import { vi } from 'vitest';
import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
// Mock Electron IPC 客户端
vi.mock('@/server/modules/ElectronIPCClient', () => ({
electronIpcClient: {
getFilePathById: vi.fn(),
deleteFiles: vi.fn(),
// 根据需要添加其他 IPC 方法
},
}));
```
#### 在测试中设置 Mock 行为
```typescript
beforeEach(() => {
// 重置所有 Mock
vi.resetAllMocks();
// 设置默认的 Mock 返回值
vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue('/path/to/file.txt');
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
success: true,
});
});
```
#### 测试不同场景的示例
```typescript
it('应该处理文件删除成功的情况', async () => {
// 设置成功场景的 Mock
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
success: true,
});
const result = await service.deleteFiles(['desktop://file1.txt']);
expect(electronIpcClient.deleteFiles).toHaveBeenCalledWith(['desktop://file1.txt']);
expect(result.success).toBe(true);
});
it('应该处理文件删除失败的情况', async () => {
// 设置失败场景的 Mock
vi.mocked(electronIpcClient.deleteFiles).mockRejectedValue(new Error('删除失败'));
const result = await service.deleteFiles(['desktop://file1.txt']);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
});
```
#### Mock 策略的优势
1. **环境简化**: 避免了复杂的 Electron 环境搭建
2. **测试可控**: 可以精确控制 IPC 调用的返回值和行为
3. **场景覆盖**: 容易测试各种成功/失败场景
4. **执行速度**: Mock 调用比真实 IPC 调用更快
#### 注意事项
- **Mock 准确性**: 确保 Mock 的行为与真实 IPC 接口行为一致
- **类型安全**: 使用 `vi.mocked()` 确保类型安全
- **Mock 重置**: 在 `beforeEach` 中重置 Mock 状态,避免测试间干扰
- **调用验证**: 不仅要验证返回值,还要验证 IPC 方法是否被正确调用
@@ -1,534 +0,0 @@
---
globs: *.test.ts,*.test.tsx
alwaysApply: false
---
# LobeChat Testing Guide
## Test Overview
LobeChat testing consists of **E2E tests** and **Unit tests**. This guide focuses on **Unit tests**.
Unit tests are organized into three main categories:
```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 |
+---------------------+---------------------------+-----------------------------+
```
### Next.js Webapp Tests
- **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 Tests
Most packages use standard Vitest configuration. However, the `database` package is special:
#### 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
# 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
# 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"
# Generate coverage report (using --coverage flag)
bunx vitest run --silent='passed-only' --coverage
```
### Commands to Avoid
```bash
# ❌ These commands run all 3000+ test cases, taking ~10 minutes!
npm test
npm test some-file.test.ts
# ❌ Don't use bare vitest (enters watch mode)
vitest test-file.test.ts
```
## Test Fixing Principles
### Core Principles
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. **Prioritize Test Fixes**
If the test itself is incorrect, fix the test first rather than the implementation code.
3. **Focus on a Single Issue**
Only fix the specified test; don't add extra tests along the way.
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. Failure Handling Strategy
**Core Principle**: Avoid blind retries; quickly identify problems and seek help.
- **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
// ❌ Wrong approach: Keep blindly trying after consecutive failures
// 3rd, 4th attempts still using similar methods to fix the same problem
// ✅ Correct approach: Summarize after 1-2 failures
/*
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. Test Case Naming Conventions
**Core Principle**: Tests should focus on "behavior," not "implementation details."
- **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', () => {
// 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**:
- 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. Test Organization Structure
**Core Principle**: Maintain a clear test hierarchy; avoid redundant top-level test blocks.
- **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', () => {});
it('should handle email display', () => {});
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. 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. **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. **Root Cause Analysis**: Explain the fundamental reason for the test failure
- Test logic error
- Implementation bug
- Environment configuration issue
- Dependency change
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
**Root Cause**: The mock data format in the test didn't match the actual API response format, causing assertion failures.
**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 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:
- **File I/O Operations**: Reading/writing disk files
- **Network Requests**: HTTP calls, database connections
- **System Calls**: Getting system time, environment variables, etc.
#### Recommended Approach: Mock Dependencies, Keep Real Data
```typescript
// ✅ Good approach: Mock I/O operations but use real file content formats
describe('parseContentType', () => {
beforeEach(() => {
// Mock file read operation (avoid real I/O)
vi.spyOn(fs, 'readFileSync').mockImplementation((path) => {
// 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 '';
});
});
it('should detect PDF content type correctly', () => {
const result = parseContentType('/path/to/file.pdf');
expect(result).toBe('application/pdf');
});
});
// ❌ 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');
```
### Modern Mocking Techniques: Environment Setup and Mock Methods
When testing client-side code, use environment annotations with modern mock methods:
```typescript
/**
* @vitest-environment happy-dom // Provides browser APIs
*/
import { beforeEach, vi } from 'vitest';
beforeEach(() => {
// Modern method 1: Use vi.stubGlobal instead of global.xxx = ...
const mockImage = vi.fn().mockImplementation(() => ({
addEventListener: vi.fn(),
naturalHeight: 600,
naturalWidth: 800,
}));
vi.stubGlobal('Image', mockImage);
// 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** (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 Method Comparison
```typescript
// ❌ Old method: Directly manipulating global object (type issues)
global.Image = mockImage;
global.URL = { ...global.URL, createObjectURL: mockFn };
// ✅ 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
// ❌ Over-testing: 29 test cases all validating the same branch
describe('getImageDimensions', () => {
it('should reject .txt files');
it('should reject .pdf files');
// ... 25 similar tests, all hitting the same validation branch
});
// ✅ Lean testing: 4 core cases covering all branches
describe('getImageDimensions', () => {
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. **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
- 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({
code: 'INVALID_PAYMENT_DATA',
statusCode: 400,
}),
);
// ❌ 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:
- 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
#### Typical Scenario: Dynamic Mocking of the Same Module
```typescript
// ❌ Problem: Dynamic mocking of the same module
it('dev mode', async () => {
vi.doMock('./config', () => ({ isDev: true }));
const { getSettings } = await import('./service'); // May use cache
});
// ✅ Solution: Clear module cache
beforeEach(() => {
vi.resetModules(); // Ensure each test has a clean environment
});
```
**Remember**: `vi.resetModules()` is the ultimate weapon for resolving "mysterious" test failures.
## Test File Organization
### File Naming Convention
`*.test.ts`, `*.test.tsx` (any location)
### Test File Organization Style
The project uses a **co-located test files** organization style:
- 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 # Source file
└── index.test.tsx # Test file
```
- 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. **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 Type Handling
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');
// Use any type to simplify complex mock setups
const mockStream = new ReadableStream() as any;
mockStream.toReadableStream = () => mockStream;
// Access private members
await instance['getFromCache']('key'); // Bracket notation recommended
await (instance as any).getFromCache('key'); // Avoid as any
```
#### Applicable Scenarios
- **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
- **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 # 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
- **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
For special testing scenarios, refer to the related rules:
- `electron-ipc-test.mdc` - Electron IPC Interface Testing Strategy
- `db-model-test.mdc` - Database Model Testing Guide
## Key Takeaways
- **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,574 +0,0 @@
---
description: Best practices for testing Zustand store actions
globs: src/store/**/*.test.ts
alwaysApply: false
---
# Zustand Store Action Testing Guide
This guide provides best practices for testing Zustand store actions, based on our proven testing patterns.
## Basic Test Structure
```typescript
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { messageService } from '@/services/message';
import { useChatStore } from '../../store';
// Keep zustand mock as it's needed globally
vi.mock('zustand/traditional');
beforeEach(() => {
// Reset store state
vi.clearAllMocks();
useChatStore.setState(
{
activeId: 'test-session-id',
messagesMap: {},
loadingIds: [],
},
false,
);
// ✅ Setup only spies that MOST tests need
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
// ❌ Don't setup spies that only few tests need - spy only when needed
// Setup common mock methods
act(() => {
useChatStore.setState({
refreshMessages: vi.fn(),
internal_coreProcessMessage: vi.fn(),
});
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('action name', () => {
describe('validation', () => {
// Validation tests
});
describe('normal flow', () => {
// Happy path tests
});
describe('error handling', () => {
// Error case tests
});
});
```
## Testing Best Practices
### 1. Test Layering - Spy Direct Dependencies Only
✅ **Good**: Spy on the direct dependency
```typescript
// When testing internal_coreProcessMessage, spy its direct dependency
const fetchAIChatSpy = vi
.spyOn(result.current, 'internal_fetchAIChatMessage')
.mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
```
❌ **Bad**: Spy on lower-level implementation details
```typescript
// Don't spy on services that internal_fetchAIChatMessage uses
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(...);
```
**Why**: Each test should only mock its direct dependencies, not the entire call chain. This makes tests more maintainable and less brittle.
### 2. Mock Management - Minimize Global Spies
✅ **Good**: Spy only when needed
```typescript
beforeEach(() => {
// ✅ Only spy services that most tests need
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
// ✅ Don't spy chatService globally
});
it('should process message', async () => {
// ✅ Spy chatService only in tests that need it
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(...);
// test logic
streamSpy.mockRestore();
});
```
❌ **Bad**: Setup all spies globally
```typescript
beforeEach(() => {
vi.spyOn(messageService, 'createMessage').mockResolvedValue('id');
vi.spyOn(chatService, 'createAssistantMessageStream').mockResolvedValue({}); // ❌ Not all tests need this
vi.spyOn(fileService, 'uploadFile').mockResolvedValue({}); // ❌ Creates implicit coupling
});
```
### 3. Service Mocking - Mock the Correct Layer
✅ **Good**: Mock the service method
```typescript
it('should fetch AI chat response', async () => {
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
await onFinish?.('Hello', {});
});
// test logic
});
```
❌ **Bad**: Mock global fetch
```typescript
it('should fetch AI chat response', async () => {
global.fetch = vi.fn().mockResolvedValue(...); // ❌ Too low level
});
```
### 4. Test Organization - Use Descriptive Nesting
✅ **Good**: Clear nested structure
```typescript
describe('sendMessage', () => {
describe('validation', () => {
it('should not send when session is inactive', async () => {});
it('should not send when message is empty', async () => {});
});
describe('message creation', () => {
it('should create user message and trigger AI processing', async () => {});
it('should send message with files attached', async () => {});
});
describe('error handling', () => {
it('should handle message creation errors gracefully', async () => {});
});
});
```
❌ **Bad**: Flat structure
```typescript
describe('sendMessage', () => {
it('test 1', async () => {});
it('test 2', async () => {});
it('test 3', async () => {});
});
```
### 5. Testing Async Actions
Always wrap async operations in `act()`:
```typescript
it('should send message', async () => {
const { result } = renderHook(() => useChatStore());
await act(async () => {
await result.current.sendMessage({ message: 'Hello' });
});
expect(messageService.createMessage).toHaveBeenCalled();
});
```
### 6. State Setup - Use act() for setState
```typescript
it('should handle disabled state', async () => {
act(() => {
useChatStore.setState({ activeId: undefined });
});
const { result } = renderHook(() => useChatStore());
// test logic
});
```
### 7. Testing Complex Flows
For complex flows with multiple steps, use clear spy setup:
```typescript
it('should handle topic creation flow', async () => {
// Setup store state
act(() => {
useChatStore.setState({
activeTopicId: undefined,
messagesMap: {
'test-session-id': [
{ id: 'msg-1', role: 'user', content: 'Message 1' },
{ id: 'msg-2', role: 'assistant', content: 'Response 1' },
{ id: 'msg-3', role: 'user', content: 'Message 2' },
],
},
});
});
const { result } = renderHook(() => useChatStore());
// Spy on action dependencies
const createTopicSpy = vi.spyOn(result.current, 'createTopic').mockResolvedValue('new-topic-id');
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleMessageLoading');
// Execute
await act(async () => {
await result.current.sendMessage({ message: 'Test message' });
});
// Assert
expect(createTopicSpy).toHaveBeenCalled();
expect(toggleLoadingSpy).toHaveBeenCalledWith(true, expect.any(String));
});
```
### 8. Streaming Response Mocking
When testing streaming responses, simulate the flow properly:
```typescript
it('should handle streaming chunks', async () => {
const { result } = renderHook(() => useChatStore());
const messages = [{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' }];
const streamSpy = vi
.spyOn(chatService, 'createAssistantMessageStream')
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
// Simulate streaming chunks
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
await onMessageHandle?.({ type: 'text', text: ' World' } as any);
await onFinish?.('Hello World', {});
});
await act(async () => {
await result.current.internal_fetchAIChatMessage({
messages,
messageId: 'test-message-id',
model: 'gpt-4o-mini',
provider: 'openai',
});
});
expect(result.current.internal_dispatchMessage).toHaveBeenCalled();
streamSpy.mockRestore();
});
```
### 9. Error Handling Tests
Always test error scenarios:
```typescript
it('should handle errors gracefully', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(messageService, 'createMessage').mockRejectedValue(new Error('create message error'));
await act(async () => {
try {
await result.current.sendMessage({ message: 'Test message' });
} catch {
// Expected to throw
}
});
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
});
```
### 10. Cleanup After Tests
Always restore mocks after each test:
```typescript
afterEach(() => {
vi.restoreAllMocks();
});
// For individual test cleanup:
it('should test something', async () => {
const spy = vi.spyOn(service, 'method').mockImplementation(...);
// test logic
spy.mockRestore(); // Optional: cleanup immediately after test
});
```
## Common Patterns
### Testing Store Methods That Call Other Store Methods
```typescript
it('should call internal methods', async () => {
const { result } = renderHook(() => useChatStore());
const internalMethodSpy = vi.spyOn(result.current, 'internal_method').mockResolvedValue();
await act(async () => {
await result.current.publicMethod();
});
expect(internalMethodSpy).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ key: 'value' }),
);
});
```
### Testing Conditional Logic
```typescript
describe('conditional behavior', () => {
it('should execute when condition is true', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(true);
await act(async () => {
await result.current.sendMessage({ message: 'test' });
});
expect(result.current.internal_retrieveChunks).toHaveBeenCalled();
});
it('should not execute when condition is false', async () => {
const { result } = renderHook(() => useChatStore());
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(false);
await act(async () => {
await result.current.sendMessage({ message: 'test' });
});
expect(result.current.internal_retrieveChunks).not.toHaveBeenCalled();
});
});
```
### Testing AbortController
```typescript
it('should abort generation and clear loading state', () => {
const abortController = new AbortController();
act(() => {
useChatStore.setState({ chatLoadingIdsAbortController: abortController });
});
const { result } = renderHook(() => useChatStore());
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
act(() => {
result.current.stopGenerateMessage();
});
expect(abortController.signal.aborted).toBe(true);
expect(toggleLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
});
```
## Anti-Patterns to Avoid
❌ **Don't**: Mock the entire store
```typescript
vi.mock('../../store', () => ({
useChatStore: vi.fn(() => ({
sendMessage: vi.fn(),
})),
}));
```
❌ **Don't**: Test implementation details
```typescript
// Bad: testing internal state structure
expect(result.current.messagesMap).toHaveProperty('test-session');
// Good: testing behavior
expect(result.current.refreshMessages).toHaveBeenCalled();
```
❌ **Don't**: Create tight coupling between tests
```typescript
// Bad: Tests depend on order
let messageId: string;
it('test 1', () => {
messageId = 'some-id'; // Side effect
});
it('test 2', () => {
expect(messageId).toBeDefined(); // Depends on test 1
});
```
❌ **Don't**: Over-mock services
```typescript
// Bad: Mocking everything
beforeEach(() => {
vi.mock('@/services/chat');
vi.mock('@/services/message');
vi.mock('@/services/file');
vi.mock('@/services/agent');
// ... too many global mocks
});
```
## Testing SWR Hooks in Zustand Stores
Some Zustand store slices use SWR hooks for data fetching. These require a different testing approach.
### Basic SWR Hook Test Structure
```typescript
import { renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { discoverService } from '@/services/discover';
import { globalHelpers } from '@/store/global/helpers';
import { useDiscoverStore as useStore } from '../../store';
vi.mock('zustand/traditional');
beforeEach(() => {
vi.clearAllMocks();
});
describe('SWR Hook Actions', () => {
it('should fetch data and return correct response', async () => {
const mockData = [{ id: '1', name: 'Item 1' }];
// Mock the service call (the fetcher)
vi.spyOn(discoverService, 'getPluginCategories').mockResolvedValue(mockData as any);
vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('en-US');
const params = {} as any;
const { result } = renderHook(() => useStore.getState().usePluginCategories(params));
// Use waitFor to wait for async data loading
await waitFor(() => {
expect(result.current.data).toEqual(mockData);
});
expect(discoverService.getPluginCategories).toHaveBeenCalledWith(params);
});
});
```
**Key points**:
- **DO NOT mock useSWR** - let it use the real implementation
- Only mock the **service methods** (fetchers)
- Use `waitFor` from `@testing-library/react` to wait for async operations
- Check `result.current.data` directly after waitFor completes
### Testing SWR Key Generation
```typescript
it('should generate correct SWR key with locale and params', () => {
vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('zh-CN');
const useSWRMock = vi.mocked(useSWR);
let capturedKey: string | null = null;
useSWRMock.mockImplementation(((key: string) => {
capturedKey = key;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
const params = { page: 2, category: 'tools' } as any;
renderHook(() => useStore.getState().usePluginList(params));
expect(capturedKey).toBe('plugin-list-zh-CN-2-tools');
});
```
### Testing SWR Configuration
```typescript
it('should have correct SWR configuration', () => {
const useSWRMock = vi.mocked(useSWR);
let capturedOptions: any = null;
useSWRMock.mockImplementation(((key: string, fetcher: any, options: any) => {
capturedOptions = options;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
renderHook(() => useStore.getState().usePluginIdentifiers());
expect(capturedOptions).toMatchObject({ revalidateOnFocus: false });
});
```
### Testing Conditional Fetching
```typescript
it('should not fetch when required parameter is missing', () => {
const useSWRMock = vi.mocked(useSWR);
let capturedKey: string | null = null;
useSWRMock.mockImplementation(((key: string | null) => {
capturedKey = key;
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
}) as any);
// When identifier is undefined, SWR key should be null
renderHook(() => useStore.getState().usePluginDetail({ identifier: undefined }));
expect(capturedKey).toBeNull();
});
```
### Key Differences from Regular Action Tests
1. **Mock useSWR globally**: Use `vi.mock('swr')` at the top level
2. **Mock the fetcher, not the result**:
- ✅ **Correct**: `const data = fetcher?.()` - call fetcher and return its Promise
- ❌ **Wrong**: `return { data: mockData }` - hardcode the result
3. **Await Promise results**: The `data` field is a Promise, use `await result.current.data`
4. **No act() wrapper needed**: SWR hooks don't trigger React state updates in these tests
5. **Test SWR key generation**: Verify keys include locale and parameters
6. **Test configuration**: Verify revalidation and other SWR options
7. **Type assertions**: Use `as any` for test mock data where type definitions are strict
**Why this matters**:
- The fetcher (service method) is what we're testing - it must be called
- Hardcoding the return value bypasses the actual fetcher logic
- SWR returns Promises in real usage, tests should mirror this behavior
## Benefits of This Approach
✅ **Clear test layers** - Each test only spies on direct dependencies ✅ **Correct mocks** - Mocks match actual implementation ✅ **Better maintainability** - Changes to implementation require fewer test updates ✅ **Improved coverage** - Structured approach ensures all branches are tested ✅ **Reduced coupling** - Tests are independent and can run in any order
## Reference
See example implementation in:
- `src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts` (Regular actions)
- `src/store/discover/slices/plugin/action.test.ts` (SWR hooks)
- `src/store/discover/slices/mcp/action.test.ts` (SWR hooks)
-55
View File
@@ -1,55 +0,0 @@
---
description: TypeScript code style and optimization guidelines
globs: *.ts,*.tsx,*.mts
alwaysApply: false
---
# TypeScript Code Style Guide
## Types and Type Safety
- avoid explicit type annotations when TypeScript can infer types.
- avoid implicitly `any` variables; explicitly type when necessary (e.g., `let a: number` instead of `let a`).
- use the most accurate type possible (e.g., prefer `Record<PropertyKey, unknown>` over `object` and `any`).
- prefer `interface` over `type` for object shapes (e.g., React component props). Keep `type` for unions, intersections, and utility types.
- prefer `as const satisfies XyzInterface` over plain `as const` when suitable.
- prefer `@ts-expect-error` over `@ts-ignore` over `as any`
- Avoid meaningless null/undefined parameters; design strict function contracts.
## Asynchronous Patterns and Concurrency
- Prefer `async`/`await` over callbacks or chained `.then` promises.
- Prefer async APIs over sync ones (avoid `*Sync`).
- Prefer promise-based variants (e.g., `import { readFile } from 'fs/promises'`) over callback-based APIs from `fs`.
- Where safe, convert sequential async flows to concurrent ones with `Promise.all`, `Promise.race`, etc.
## Code Structure and Readability
- Prefer object destructuring when accessing and using properties.
- Use consistent, descriptive naming; avoid obscure abbreviations.
- Use semantically meaningful variable, function, and class names.
- Replace magic numbers or strings with well-named constants.
- Defer formatting to tooling; ignore purely formatting-only issues and autofixable lint problems.
## UI and Theming
- Use components from `@lobehub/ui`, Ant Design, or existing design system components instead of raw HTML tags (e.g., `Button` vs. `button`).
- Design for dark mode and mobile responsiveness:
- Use the `antd-style` token system instead of hard-coded colors.
- Select appropriate component variants.
## Performance
- Prefer `for…of` loops to index-based `for` loops when feasible.
- Reuse existing utils inside `packages/utils` or installed npm packages rather than reinventing the wheel.
- Query only the required columns from a database rather than selecting entire rows.
## Time and Consistency
- Instead of calling `Date.now()` multiple times, assign it to a constant once and reuse it to ensure consistency and improve readability.
## Logging
- Never log user private information like api key, etc
- Don't use `import { log } from 'debug'` to log messages, because it will directly log the message to the console.
- Use console.error instead of debug package to log error message in catch block.
-328
View File
@@ -1,328 +0,0 @@
---
description:
globs: src/store/**
alwaysApply: false
---
# LobeChat Zustand Action Patterns
## Action Type Hierarchy
LobeChat Actions use a layered architecture with clear separation of responsibilities:
### 1. Public Actions
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`
```typescript
// Public Action example
createTopic: async () => {
// ...
return topicId;
},
```
### 2. Internal Actions (`internal_*`)
Internal implementation details handling core business logic:
- Naming: `internal_` prefix + verb (`internal_createTopic`, `internal_updateMessageContent`)
- Responsibilities: Optimistic updates, service calls, error handling, state synchronization
- Should not be called directly by UI components
```typescript
// Internal Action example - Optimistic update pattern
internal_createTopic: async (params) => {
const tmpId = Date.now().toString();
// 1. Immediately update frontend state (optimistic update)
get().internal_dispatchTopic(
{ type: 'addTopic', value: { ...params, id: tmpId } },
'internal_createTopic',
);
get().internal_updateTopicLoading(tmpId, true);
// 2. Call backend service
const topicId = await topicService.createTopic(params);
get().internal_updateTopicLoading(tmpId, false);
// 3. Refresh data to ensure consistency
get().internal_updateTopicLoading(topicId, true);
await get().refreshTopic();
get().internal_updateTopicLoading(topicId, false);
return topicId;
},
```
### 3. Dispatch Methods (`internal_dispatch*`)
Methods dedicated to handling state updates:
- Naming: `internal_dispatch` + entity name (`internal_dispatchTopic`, `internal_dispatchMessage`)
- Responsibilities: Calling reducers, updating Zustand store, handling state comparison
```typescript
// Dispatch Method example
internal_dispatchTopic: (payload, action) => {
const nextTopics = topicReducer(topicSelectors.currentTopics(get()), payload);
const nextMap = { ...get().topicMaps, [get().activeId]: nextTopics };
if (isEqual(nextMap, get().topicMaps)) return;
set({ topicMaps: nextMap }, false, action ?? n(`dispatchTopic/${payload.type}`));
},
```
## When to Use Reducer Pattern vs. Simple `set`
### Use Reducer Pattern When
Suitable for complex data structure management, especially:
- Managing object lists or maps (e.g., `messagesMap`, `topicMaps`)
- Scenarios requiring optimistic updates
- Complex state transition logic
- Type-safe action payloads needed
```typescript
// Reducer pattern example - Complex message state management
export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
switch (payload.type) {
case 'updateMessage': {
return produce(state, (draftState) => {
const index = draftState.findIndex((i) => i.id === payload.id);
if (index < 0) return;
draftState[index] = merge(draftState[index], {
...payload.value,
updatedAt: Date.now(),
});
});
}
case 'createMessage': {
// ...
}
// ...other complex state transitions
}
};
```
### Use Simple `set` When
Suitable for simple state updates:
- Toggling boolean values
- Updating simple strings/numbers
- Setting single state fields
```typescript
// Simple set example
updateInputMessage: (message) => {
if (isEqual(message, get().inputMessage)) return;
set({ inputMessage: message }, false, n('updateInputMessage'));
},
togglePortal: (open?: boolean) => {
set({ showPortal: open ?? !get().showPortal }, false, 'togglePortal');
},
```
## Optimistic Update Implementation Patterns
Optimistic updates are a core pattern in LobeChat for providing smooth user experience:
### Standard Optimistic Update Flow
```typescript
// Complete optimistic update example
internal_updateMessageContent: async (id, content, extra) => {
const { internal_dispatchMessage, refreshMessages } = get();
// 1. Immediately update frontend state (optimistic update)
internal_dispatchMessage({
id,
type: 'updateMessage',
value: { content },
});
// 2. Call backend service
await messageService.updateMessage(id, {
content,
tools: extra?.toolCalls ? internal_transformToolCalls(extra.toolCalls) : undefined,
// ...other fields
});
// 3. Refresh to ensure data consistency
await refreshMessages();
},
```
### Optimistic Update for Create Operations
```typescript
internal_createMessage: async (message, context) => {
const { internal_createTmpMessage, refreshMessages, internal_toggleMessageLoading } = get();
let tempId = context?.tempMessageId;
if (!tempId) {
// Create temporary message for optimistic update
tempId = internal_createTmpMessage(message);
internal_toggleMessageLoading(true, tempId);
}
try {
const id = await messageService.createMessage(message);
if (!context?.skipRefresh) {
await refreshMessages();
}
internal_toggleMessageLoading(false, tempId);
return id;
} catch (e) {
internal_toggleMessageLoading(false, tempId);
// Error handling: update message error state
internal_dispatchMessage({
id: tempId,
type: 'updateMessage',
value: { error: { type: ChatErrorType.CreateMessageError, message: e.message } },
});
}
},
```
### Delete Operation Pattern (No Optimistic Update)
Delete operations typically don't suit optimistic updates because:
- Deletion is destructive; error recovery is complex
- Users have lower expectations for immediate feedback on deletions
- Restoring state on deletion failure causes confusion
```typescript
// Standard delete operation pattern - No optimistic update
removeGenerationTopic: async (id: string) => {
const { internal_removeGenerationTopic } = get();
await internal_removeGenerationTopic(id);
},
internal_removeGenerationTopic: async (id: string) => {
// 1. Show loading state
get().internal_updateGenerationTopicLoading(id, true);
try {
// 2. Directly call backend service
await generationTopicService.deleteTopic(id);
// 3. Refresh data to get latest state
await get().refreshGenerationTopics();
} finally {
// 4. Ensure loading state is cleared (whether success or failure)
get().internal_updateGenerationTopicLoading(id, false);
}
},
```
Delete operation characteristics:
- Directly call service without pre-updating state
- Rely on loading state for user feedback
- Refresh entire list after operation to ensure consistency
- Use `try/finally` to ensure loading state is always cleaned up
## Loading State Management Pattern
LobeChat uses a unified loading state management pattern:
### Array-based Loading State
```typescript
// Define in initialState.ts
export interface ChatMessageState {
messageEditingIds: string[]; // Message editing state
}
// Manage in action
{
toggleMessageEditing: (id, editing) => {
set(
{ messageEditingIds: toggleBooleanList(get().messageEditingIds, id, editing) },
false,
'toggleMessageEditing',
);
};
}
```
## SWR Integration Pattern
LobeChat uses SWR for data fetching and cache management:
### Hook-based Data Fetching
```typescript
// Define SWR hook in action.ts
useFetchMessages: (enable, sessionId, activeTopicId) =>
useClientDataSWR<ChatMessage[]>(
enable ? [SWR_USE_FETCH_MESSAGES, sessionId, activeTopicId] : null,
async ([, sessionId, topicId]) => messageService.getMessages(sessionId, topicId),
{
onSuccess: (messages, key) => {
const nextMap = {
...get().messagesMap,
[messageMapKey(sessionId, activeTopicId)]: messages,
};
if (get().messagesInit && isEqual(nextMap, get().messagesMap)) return;
set({ messagesInit: true, messagesMap: nextMap }, false, n('useFetchMessages'));
},
},
),
```
### Cache Invalidation and Refresh
```typescript
// Standard data refresh pattern
refreshMessages: async () => {
await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId]);
};
```
## Naming Convention Summary
### Action Naming Patterns
- Public Actions: Verb form, describing user intent
- `createTopic`, `sendMessage`, `regenerateMessage`
- Internal Actions: `internal_` + verb, describing internal operation
- `internal_createTopic`, `internal_updateMessageContent`
- Dispatch Methods: `internal_dispatch` + entity name
- `internal_dispatchTopic`, `internal_dispatchMessage`
- Toggle Methods: `internal_toggle` + state name
- `internal_toggleMessageLoading`, `internal_toggleChatLoading`
### State Naming Patterns
- ID arrays: `[entity]LoadingIds`, `[entity]EditingIds`
- Map structures: `[entity]Maps`, `[entity]Map`
- Currently active: `active[Entity]Id`
- Initialization flags: `[entity]sInit`
## Best Practices
1. Use optimistic updates appropriately:
- ✅ Suitable: Create, update operations (frequent user interaction)
- ❌ Avoid: Delete operations (destructive, complex error recovery)
2. Loading state management: Use unified loading state arrays to manage concurrent operations
3. Type safety: Define TypeScript interfaces for all action payloads
4. SWR integration: Use SWR to manage data fetching and cache invalidation
5. AbortController: Provide cancellation capability for long-running operations
6. Operation mode selection:
- Create/Update: Optimistic update + eventual consistency
- Delete: Loading state + service call + data refresh
This Action organization pattern ensures code consistency, maintainability, and provides excellent user experience.
@@ -1,308 +0,0 @@
---
description:
globs: src/store/**
alwaysApply: false
---
# LobeChat Zustand Store Slice 组织架构
本文档描述了 LobeChat 项目中 Zustand Store 的模块化 Slice 组织方式,展示如何通过分片架构管理复杂的应用状态。
## 顶层 Store 结构
LobeChat 的 `chat` store (`src/store/chat/`) 采用模块化的 slice 结构来组织状态和逻辑。
### 关键聚合文件
- `src/store/chat/initialState.ts`: 聚合所有 slice 的初始状态
- `src/store/chat/store.ts`: 定义顶层的 `ChatStore`,组合所有 slice 的 actions
- `src/store/chat/selectors.ts`: 统一导出所有 slice 的 selectors
- `src/store/chat/helpers.ts`: 提供聊天相关的辅助函数
### Store 聚合模式
```typescript
// src/store/chat/initialState.ts
import { ChatTopicState, initialTopicState } from './slices/topic/initialState';
import { ChatMessageState, initialMessageState } from './slices/message/initialState';
import { ChatAIChatState, initialAiChatState } from './slices/aiChat/initialState';
export type ChatStoreState = ChatTopicState &
ChatMessageState &
ChatAIChatState &
// ...其他 slice states
export const initialState: ChatStoreState = {
...initialMessageState,
...initialTopicState,
...initialAiChatState,
// ...其他 initial slice states
};
```
```typescript
// src/store/chat/store.ts
import { ChatMessageAction, chatMessage } from './slices/message/action';
import { ChatTopicAction, chatTopic } from './slices/topic/action';
import { ChatAIChatAction, chatAiChat } from './slices/aiChat/actions';
export interface ChatStoreAction
extends ChatMessageAction,
ChatTopicAction,
ChatAIChatAction,
// ...其他 slice actions
const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...params) => ({
...initialState,
...chatMessage(...params),
...chatTopic(...params),
...chatAiChat(...params),
// ...其他 slice action creators
});
export const useChatStore = createWithEqualityFn<ChatStore>()(
subscribeWithSelector(devtools(createStore)),
shallow,
);
```
## 单个 Slice 的标准结构
每个 slice 位于 `src/store/chat/slices/[sliceName]/` 目录下:
```plaintext
src/store/chat/slices/
└── [sliceName]/ # 例如 message, topic, aiChat, builtinTool
├── action.ts # 定义 actions (或者是一个 actions/ 目录)
├── initialState.ts # 定义 state 结构和初始值
├── reducer.ts # (可选) 如果使用 reducer 模式
├── selectors.ts # 定义 selectors
└── index.ts # (可选) 重新导出模块内容
```
### 文件职责说明
1. `initialState.ts`:
- 定义 slice 的 TypeScript 状态接口
- 提供初始状态默认值
```typescript
// 典型的 initialState.ts 结构
export interface ChatTopicState {
activeTopicId?: string;
topicMaps: Record<string, ChatTopic[]>; // 核心数据结构
topicsInit: boolean;
topicLoadingIds: string[];
// ...其他状态字段
}
export const initialTopicState: ChatTopicState = {
activeTopicId: undefined,
topicMaps: {},
topicsInit: false,
topicLoadingIds: [],
// ...其他初始值
};
```
1. `reducer.ts` (复杂状态使用):
- 定义纯函数 reducer,处理同步状态转换
- 使用 `immer` 确保不可变更新
```typescript
// 典型的 reducer.ts 结构
import { produce } from 'immer';
interface AddChatTopicAction {
type: 'addTopic';
value: CreateTopicParams & { id?: string };
}
interface UpdateChatTopicAction {
id: string;
type: 'updateTopic';
value: Partial<ChatTopic>;
}
export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction;
export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch): ChatTopic[] => {
switch (payload.type) {
case 'addTopic': {
return produce(state, (draftState) => {
draftState.unshift({
...payload.value,
id: payload.value.id ?? Date.now().toString(),
createdAt: Date.now(),
});
});
}
case 'updateTopic': {
return produce(state, (draftState) => {
const index = draftState.findIndex((topic) => topic.id === payload.id);
if (index !== -1) {
draftState[index] = { ...draftState[index], ...payload.value };
}
});
}
default:
return state;
}
};
```
1. `selectors.ts`:
- 提供状态查询和计算函数
- 供 UI 组件使用的状态订阅接口
- 重要: 使用 `export const xxxSelectors` 模式聚合所有 selectors
```typescript
// 典型的 selectors.ts 结构
import { ChatStoreState } from '../../initialState';
const currentTopics = (s: ChatStoreState): ChatTopic[] | undefined => s.topicMaps[s.activeId];
const currentActiveTopic = (s: ChatStoreState): ChatTopic | undefined => {
return currentTopics(s)?.find((topic) => topic.id === s.activeTopicId);
};
const getTopicById =
(id: string) =>
(s: ChatStoreState): ChatTopic | undefined =>
currentTopics(s)?.find((topic) => topic.id === id);
// 核心模式:使用 xxxSelectors 聚合导出
export const topicSelectors = {
currentActiveTopic,
currentTopics,
getTopicById,
// ...其他 selectors
};
```
## 特殊 Slice 组织模式
### 复杂 Actions 的子目录结构 (aiChat Slice)
当 slice 的 actions 过于复杂时,可以拆分到子目录:
```plaintext
src/store/chat/slices/aiChat/
├── actions/
│ ├── generateAIChat.ts # AI 对话生成
│ ├── rag.ts # RAG 检索增强生成
│ ├── memory.ts # 对话记忆管理
│ └── index.ts # 聚合所有 actions
├── initialState.ts
├── selectors.ts
└── index.ts
```
参考:`src/store/chat/slices/aiChat/actions/`
### 工具类 Slice (builtinTool)
管理多种内置工具的状态:
```plaintext
src/store/chat/slices/builtinTool/
├── actions/
│ ├── dalle.ts # DALL-E 图像生成
│ ├── search.ts # 搜索功能
│ ├── localFile.ts # 本地文件操作
│ └── index.ts
├── initialState.ts
├── selectors.ts
└── index.ts
```
参考:`src/store/chat/slices/builtinTool/`
## 状态设计模式
### 1. Map 结构用于关联数据
```typescript
// 以 sessionId 为 key,管理多个会话的数据
topicMaps: Record<string, ChatTopic[]>;
messagesMap: Record<string, ChatMessage[]>;
```
### 2. 数组用于加载状态管理
```typescript
// 管理多个并发操作的加载状态
messageLoadingIds: string[]
topicLoadingIds: string[]
chatLoadingIds: string[]
```
### 3. 可选字段用于当前活动项
```typescript
// 当前激活的实体 ID
activeId: string
activeTopicId?: string
activeThreadId?: string
```
## Slice 集成到顶层 Store
### 1. 状态聚合
```typescript
// 在 initialState.ts 中
export type ChatStoreState = ChatTopicState &
ChatMessageState &
ChatAIChatState &
// ...其他 slice states
```
### 2. Action 接口聚合
```typescript
// 在 store.ts 中
export interface ChatStoreAction
extends ChatMessageAction,
ChatTopicAction,
ChatAIChatAction,
// ...其他 slice actions
```
### 3. Selector 统一导出
```typescript
// 在 selectors.ts 中 - 统一聚合 selectors
export { chatSelectors } from './slices/message/selectors';
export { topicSelectors } from './slices/topic/selectors';
export { aiChatSelectors } from './slices/aiChat/selectors';
// 每个 slice 的 selectors.ts 都使用 xxxSelectors 模式:
// export const chatSelectors = { ... }
// export const topicSelectors = { ... }
// export const aiChatSelectors = { ... }
```
## 最佳实践
1. Slice 划分原则:
- 按功能领域划分(message, topic, aiChat 等)
- 每个 slice 管理相关的状态和操作
- 避免 slice 之间的强耦合
2. 文件命名规范:
- 使用小驼峰命名 slice 目录
- 文件名使用一致的模式(action.ts, selectors.ts 等)
- 复杂 actions 时使用 actions/ 子目录
3. 状态结构设计:
- 扁平化的状态结构,避免深层嵌套
- 使用 Map 结构管理列表数据
- 分离加载状态和业务数据
4. 类型安全:
- 为每个 slice 定义清晰的 TypeScript 接口
- 使用 Zustand 的 StateCreator 确保类型一致性
- 在顶层聚合时保持类型安全
这种模块化的 slice 组织方式使得大型应用的状态管理变得清晰、可维护,并且易于扩展。
+1 -1
View File
@@ -1 +1 @@
../.agents
../.agents/skills