mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
♻️ 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:
90
.agents/skills/add-provider-doc/SKILL.md
Normal file
90
.agents/skills/add-provider-doc/SKILL.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: add-provider-doc
|
||||
description: Guide for adding new AI provider documentation. Use when adding documentation for a new AI provider (like OpenAI, Anthropic, etc.), including usage docs, environment variables, Docker config, and image resources. Triggers on provider documentation tasks.
|
||||
---
|
||||
|
||||
# Adding New AI Provider Documentation
|
||||
|
||||
Complete workflow for adding documentation for a new AI provider.
|
||||
|
||||
## Overview
|
||||
|
||||
1. Create usage documentation (EN + CN)
|
||||
2. Add environment variable documentation (EN + CN)
|
||||
3. Update Docker configuration files
|
||||
4. Update .env.example
|
||||
5. Prepare image resources
|
||||
|
||||
## Step 1: Create Provider Usage Documentation
|
||||
|
||||
### Required Files
|
||||
|
||||
- `docs/usage/providers/{provider-name}.mdx` (English)
|
||||
- `docs/usage/providers/{provider-name}.zh-CN.mdx` (Chinese)
|
||||
|
||||
### Key Requirements
|
||||
|
||||
- 5-6 screenshots showing the process
|
||||
- Cover image for the provider
|
||||
- Real registration and dashboard URLs
|
||||
- Pricing information callout
|
||||
- **Never include real API keys** - use placeholders
|
||||
|
||||
Reference: `docs/usage/providers/fal.mdx`
|
||||
|
||||
## Step 2: Update Environment Variables Documentation
|
||||
|
||||
### Files to Update
|
||||
|
||||
- `docs/self-hosting/environment-variables/model-provider.mdx` (EN)
|
||||
- `docs/self-hosting/environment-variables/model-provider.zh-CN.mdx` (CN)
|
||||
|
||||
### Content Format
|
||||
|
||||
```markdown
|
||||
### `{PROVIDER}_API_KEY`
|
||||
- Type: Required
|
||||
- Description: API key from {Provider Name}
|
||||
- Example: `{api-key-format}`
|
||||
|
||||
### `{PROVIDER}_MODEL_LIST`
|
||||
- Type: Optional
|
||||
- Description: Control model list. Use `+` to add, `-` to hide
|
||||
- Example: `-all,+model-1,+model-2=Display Name`
|
||||
```
|
||||
|
||||
## Step 3: Update Docker Files
|
||||
|
||||
Update all Dockerfiles at the **end** of ENV section:
|
||||
|
||||
- `Dockerfile`
|
||||
- `Dockerfile.database`
|
||||
- `Dockerfile.pglite`
|
||||
|
||||
```dockerfile
|
||||
# {New Provider}
|
||||
{PROVIDER}_API_KEY="" {PROVIDER}_MODEL_LIST=""
|
||||
```
|
||||
|
||||
## Step 4: Update .env.example
|
||||
|
||||
```bash
|
||||
### {Provider Name} ###
|
||||
# {PROVIDER}_API_KEY={prefix}-xxxxxxxx
|
||||
```
|
||||
|
||||
## Step 5: Image Resources
|
||||
|
||||
- Cover image
|
||||
- 3-4 API dashboard screenshots
|
||||
- 2-3 LobeChat configuration screenshots
|
||||
- Host on LobeHub CDN: `hub-apac-1.lobeobjects.space`
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] EN + CN usage docs
|
||||
- [ ] EN + CN env var docs
|
||||
- [ ] All 3 Dockerfiles updated
|
||||
- [ ] .env.example updated
|
||||
- [ ] All images prepared
|
||||
- [ ] No real API keys in docs
|
||||
106
.agents/skills/add-setting-env/SKILL.md
Normal file
106
.agents/skills/add-setting-env/SKILL.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: add-setting-env
|
||||
description: Guide for adding environment variables to configure user settings. Use when implementing server-side environment variables that control default values for user settings. Triggers on env var configuration or setting default value tasks.
|
||||
---
|
||||
|
||||
# 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 (if new domain)
|
||||
|
||||
Add to `packages/types/src/serverConfig.ts`:
|
||||
|
||||
```typescript
|
||||
import { User<Domain>Config } from './user/settings';
|
||||
|
||||
export interface GlobalServerConfig {
|
||||
<domain>?: PartialDeep<User<Domain>Config>;
|
||||
}
|
||||
```
|
||||
|
||||
**Prefer reusing existing types** from `packages/types/src/user/settings`.
|
||||
|
||||
### 3. Assemble Server Config (if new domain)
|
||||
|
||||
In `src/server/globalConfig/index.ts`:
|
||||
|
||||
```typescript
|
||||
import { <domain>Env } from '@/envs/<domain>';
|
||||
|
||||
export const getServerGlobalConfig = async () => {
|
||||
const config: GlobalServerConfig = {
|
||||
<domain>: cleanObject({
|
||||
<settingName>: <domain>Env.YOUR_ENV_VAR,
|
||||
}),
|
||||
};
|
||||
return config;
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Merge to User Store (if new domain)
|
||||
|
||||
In `src/store/user/slices/common/action.ts`:
|
||||
|
||||
```typescript
|
||||
const serverSettings: PartialDeep<UserSettings> = {
|
||||
<domain>: serverConfig.<domain>,
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Update .env.example
|
||||
|
||||
```bash
|
||||
# <Description> (range/options, default: X)
|
||||
# YOUR_ENV_VAR=<example>
|
||||
```
|
||||
|
||||
### 6. Update Documentation
|
||||
|
||||
- `docs/self-hosting/environment-variables/basic.mdx` (EN)
|
||||
- `docs/self-hosting/environment-variables/basic.zh-CN.mdx` (CN)
|
||||
|
||||
## Example: AI_IMAGE_DEFAULT_IMAGE_NUM
|
||||
|
||||
```typescript
|
||||
// src/envs/image.ts
|
||||
AI_IMAGE_DEFAULT_IMAGE_NUM: z.coerce.number().min(1).max(20).optional(),
|
||||
|
||||
// packages/types/src/serverConfig.ts
|
||||
image?: PartialDeep<UserImageConfig>;
|
||||
|
||||
// src/server/globalConfig/index.ts
|
||||
image: cleanObject({ defaultImageNum: imageEnv.AI_IMAGE_DEFAULT_IMAGE_NUM }),
|
||||
|
||||
// src/store/user/slices/common/action.ts
|
||||
image: serverConfig.image,
|
||||
|
||||
// .env.example
|
||||
# AI_IMAGE_DEFAULT_IMAGE_NUM=4
|
||||
```
|
||||
66
.agents/skills/debug/SKILL.md
Normal file
66
.agents/skills/debug/SKILL.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: debug
|
||||
description: Debug package usage guide. Use when adding debug logging, understanding log namespaces, or implementing debugging features. Triggers on debug logging requests or logging implementation.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Debug Package Usage Guide
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import debug from 'debug';
|
||||
|
||||
// Format: lobe-[module]:[submodule]
|
||||
const log = debug('lobe-server:market');
|
||||
|
||||
log('Simple message');
|
||||
log('With variable: %O', object);
|
||||
log('Formatted number: %d', number);
|
||||
```
|
||||
|
||||
## Namespace Conventions
|
||||
|
||||
- Desktop: `lobe-desktop:[module]`
|
||||
- Server: `lobe-server:[module]`
|
||||
- Client: `lobe-client:[module]`
|
||||
- Router: `lobe-[type]-router:[module]`
|
||||
|
||||
## Format Specifiers
|
||||
|
||||
- `%O` - Object expanded (recommended for complex objects)
|
||||
- `%o` - Object
|
||||
- `%s` - String
|
||||
- `%d` - Number
|
||||
|
||||
## Enable Debug Output
|
||||
|
||||
### Browser
|
||||
|
||||
```javascript
|
||||
localStorage.debug = 'lobe-*';
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```bash
|
||||
DEBUG=lobe-* npm run dev
|
||||
DEBUG=lobe-* pnpm dev
|
||||
```
|
||||
|
||||
### Electron
|
||||
|
||||
```typescript
|
||||
process.env.DEBUG = 'lobe-*';
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```typescript
|
||||
// src/server/routers/edge/market/index.ts
|
||||
import debug from 'debug';
|
||||
|
||||
const log = debug('lobe-edge-router:market');
|
||||
|
||||
log('getAgent input: %O', input);
|
||||
```
|
||||
78
.agents/skills/desktop/SKILL.md
Normal file
78
.agents/skills/desktop/SKILL.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: desktop
|
||||
description: Electron desktop development guide. Use when implementing desktop features, IPC handlers, controllers, preload scripts, window management, menu configuration, or Electron-specific functionality. Triggers on desktop app development, Electron IPC, or desktop local tools implementation.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# Desktop Development Guide
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
LobeChat desktop is built on Electron with main-renderer architecture:
|
||||
|
||||
1. **Main Process** (`apps/desktop/src/main`): App lifecycle, system APIs, window management
|
||||
2. **Renderer Process**: Reuses web code from `src/`
|
||||
3. **Preload Scripts** (`apps/desktop/src/preload`): Securely expose main process to renderer
|
||||
|
||||
## Adding New Desktop Features
|
||||
|
||||
### 1. Create Controller
|
||||
Location: `apps/desktop/src/main/controllers/`
|
||||
|
||||
```typescript
|
||||
import { ControllerModule, IpcMethod } from '@/controllers';
|
||||
|
||||
export default class NewFeatureCtr extends ControllerModule {
|
||||
static override readonly groupName = 'newFeature';
|
||||
|
||||
@IpcMethod()
|
||||
async doSomething(params: SomeParams): Promise<SomeResult> {
|
||||
// Implementation
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register in `apps/desktop/src/main/controllers/registry.ts`.
|
||||
|
||||
### 2. Define IPC Types
|
||||
Location: `packages/electron-client-ipc/src/types.ts`
|
||||
|
||||
```typescript
|
||||
export interface SomeParams { /* ... */ }
|
||||
export interface SomeResult { success: boolean; error?: string }
|
||||
```
|
||||
|
||||
### 3. Create Renderer Service
|
||||
Location: `src/services/electron/`
|
||||
|
||||
```typescript
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
||||
|
||||
const ipc = ensureElectronIpc();
|
||||
|
||||
export const newFeatureService = async (params: SomeParams) => {
|
||||
return ipc.newFeature.doSomething(params);
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Implement Store Action
|
||||
Location: `src/store/`
|
||||
|
||||
### 5. Add Tests
|
||||
Location: `apps/desktop/src/main/controllers/__tests__/`
|
||||
|
||||
## Detailed Guides
|
||||
|
||||
See `references/` for specific topics:
|
||||
- **Feature implementation**: `references/feature-implementation.md`
|
||||
- **Local tools workflow**: `references/local-tools.md`
|
||||
- **Menu configuration**: `references/menu-config.md`
|
||||
- **Window management**: `references/window-management.md`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Security**: Validate inputs, limit exposed APIs
|
||||
2. **Performance**: Use async methods, batch data transfers
|
||||
3. **UX**: Add progress indicators, provide error feedback
|
||||
4. **Code organization**: Follow existing patterns, add documentation
|
||||
99
.agents/skills/desktop/references/feature-implementation.md
Normal file
99
.agents/skills/desktop/references/feature-implementation.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Desktop Feature Implementation Guide
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```plaintext
|
||||
Main Process Renderer Process
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ Controller │◄──IPC───►│ Service Layer │
|
||||
│ (IPC Handler) │ │ │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ System APIs │ │ Store Actions │
|
||||
│ (fs, network) │ │ (UI State) │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## Step-by-Step Implementation
|
||||
|
||||
### 1. Create Controller
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/controllers/NotificationCtr.ts
|
||||
import type { ShowDesktopNotificationParams, DesktopNotificationResult } 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:', error);
|
||||
return { error: error instanceof Error ? error.message : 'Unknown error', success: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Define IPC Types
|
||||
|
||||
```typescript
|
||||
// packages/electron-client-ipc/src/types.ts
|
||||
export interface ShowDesktopNotificationParams {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface DesktopNotificationResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Service Layer
|
||||
|
||||
```typescript
|
||||
// src/services/electron/notificationService.ts
|
||||
import type { ShowDesktopNotificationParams } from '@lobechat/electron-client-ipc';
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
||||
|
||||
const ipc = ensureElectronIpc();
|
||||
|
||||
export const notificationService = {
|
||||
show: (params: ShowDesktopNotificationParams) =>
|
||||
ipc.notification.showDesktopNotification(params),
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Implement Store Action
|
||||
|
||||
```typescript
|
||||
// src/store/.../actions.ts
|
||||
showNotification: async (title: string, body: string) => {
|
||||
if (!isElectron) return;
|
||||
|
||||
const result = await notificationService.show({ title, body });
|
||||
if (!result.success) {
|
||||
console.error('Notification failed:', result.error);
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Security**: Validate inputs, limit exposed APIs
|
||||
2. **Performance**: Use async methods for heavy operations
|
||||
3. **Error handling**: Always return structured results
|
||||
4. **UX**: Provide loading states and error feedback
|
||||
133
.agents/skills/desktop/references/local-tools.md
Normal file
133
.agents/skills/desktop/references/local-tools.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Desktop Local Tools Implementation
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
1. Define tool interface (Manifest)
|
||||
2. Define related types
|
||||
3. Implement Store Action
|
||||
4. Implement Service Layer
|
||||
5. Implement Controller (IPC Handler)
|
||||
6. Update Agent documentation
|
||||
|
||||
## Step 1: Define Tool Interface (Manifest)
|
||||
|
||||
Location: `src/tools/[tool_category]/index.ts`
|
||||
|
||||
```typescript
|
||||
// src/tools/local-files/index.ts
|
||||
export const LocalFilesApiName = {
|
||||
RenameFile: 'renameFile',
|
||||
MoveFile: 'moveFile',
|
||||
} as const;
|
||||
|
||||
export const LocalFilesManifest = {
|
||||
api: [
|
||||
{
|
||||
name: LocalFilesApiName.RenameFile,
|
||||
description: 'Rename a local file',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
oldPath: { type: 'string', description: 'Current file path' },
|
||||
newName: { type: 'string', description: 'New file name' },
|
||||
},
|
||||
required: ['oldPath', 'newName'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Step 2: Define Types
|
||||
|
||||
```typescript
|
||||
// packages/electron-client-ipc/src/types.ts
|
||||
export interface RenameLocalFileParams {
|
||||
oldPath: string;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
// src/tools/local-files/type.ts
|
||||
export interface LocalRenameFileState {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
oldPath: string;
|
||||
newPath: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Implement Store Action
|
||||
|
||||
```typescript
|
||||
// src/store/chat/slices/builtinTool/actions/localFile.ts
|
||||
renameLocalFile: async (id: string, params: RenameLocalFileParams) => {
|
||||
const { toggleLocalFileLoading, updatePluginState, internal_updateMessageContent } = get();
|
||||
|
||||
toggleLocalFileLoading(id, true);
|
||||
|
||||
try {
|
||||
const result = await localFileService.renameFile(params);
|
||||
|
||||
if (result.success) {
|
||||
updatePluginState(id, { success: true, ...result });
|
||||
internal_updateMessageContent(id, JSON.stringify({ success: true }));
|
||||
} else {
|
||||
updatePluginState(id, { success: false, error: result.error });
|
||||
internal_updateMessageContent(id, JSON.stringify({ error: result.error }));
|
||||
}
|
||||
|
||||
return result.success;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
updatePluginState(id, { success: false, error: e.message });
|
||||
return false;
|
||||
} finally {
|
||||
toggleLocalFileLoading(id, false);
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
## Step 4: Implement Service Layer
|
||||
|
||||
```typescript
|
||||
// src/services/electron/localFileService.ts
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
||||
|
||||
const ipc = ensureElectronIpc();
|
||||
|
||||
export const localFileService = {
|
||||
renameFile: (params: RenameLocalFileParams) => ipc.localFiles.renameFile(params),
|
||||
};
|
||||
```
|
||||
|
||||
## Step 5: Implement Controller
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/controllers/LocalFileCtr.ts
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { ControllerModule, IpcMethod } from '@/controllers';
|
||||
|
||||
export default class LocalFileCtr extends ControllerModule {
|
||||
static override readonly groupName = 'localFiles';
|
||||
|
||||
@IpcMethod()
|
||||
async renameFile(params: RenameLocalFileParams) {
|
||||
const { oldPath, newName } = params;
|
||||
const newPath = path.join(path.dirname(oldPath), newName);
|
||||
|
||||
try {
|
||||
await fs.rename(oldPath, newPath);
|
||||
return { success: true, newPath };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 6: Update Agent Documentation
|
||||
|
||||
Location: `src/tools/[tool_category]/systemRole.ts`
|
||||
|
||||
Add tool description to `<core_capabilities>` and usage guidelines to `<tool_usage_guidelines>`.
|
||||
103
.agents/skills/desktop/references/menu-config.md
Normal file
103
.agents/skills/desktop/references/menu-config.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Desktop Menu Configuration Guide
|
||||
|
||||
## Menu Types
|
||||
|
||||
1. **App Menu**: Top of window (macOS) or title bar (Windows/Linux)
|
||||
2. **Context Menu**: Right-click menus
|
||||
3. **Tray Menu**: System tray icon menus
|
||||
|
||||
## File Structure
|
||||
|
||||
```plaintext
|
||||
apps/desktop/src/main/
|
||||
├── menus/
|
||||
│ ├── appMenu.ts # App menu config
|
||||
│ ├── contextMenu.ts # Context menu config
|
||||
│ └── factory.ts # Menu factory functions
|
||||
├── controllers/
|
||||
│ ├── MenuCtr.ts # Menu controller
|
||||
│ └── TrayMenuCtr.ts # Tray menu controller
|
||||
```
|
||||
|
||||
## App Menu Configuration
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/menus/appMenu.ts
|
||||
import { BrowserWindow, Menu, MenuItemConstructorOptions } from 'electron';
|
||||
|
||||
export const createAppMenu = (win: BrowserWindow) => {
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{ label: 'New', accelerator: 'CmdOrCtrl+N', click: () => { /* ... */ } },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' },
|
||||
],
|
||||
},
|
||||
// ...
|
||||
];
|
||||
|
||||
return Menu.buildFromTemplate(template);
|
||||
};
|
||||
|
||||
// Register in MenuCtr.ts
|
||||
Menu.setApplicationMenu(menu);
|
||||
```
|
||||
|
||||
## Context Menu
|
||||
|
||||
```typescript
|
||||
export const createContextMenu = () => {
|
||||
const template = [
|
||||
{ label: 'Copy', role: 'copy' },
|
||||
{ label: 'Paste', role: 'paste' },
|
||||
];
|
||||
return Menu.buildFromTemplate(template);
|
||||
};
|
||||
|
||||
// Show on right-click
|
||||
const menu = createContextMenu();
|
||||
menu.popup();
|
||||
```
|
||||
|
||||
## Tray Menu
|
||||
|
||||
```typescript
|
||||
// TrayMenuCtr.ts
|
||||
this.tray = new Tray(trayIconPath);
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Show Window', click: this.showMainWindow },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
]);
|
||||
this.tray.setContextMenu(contextMenu);
|
||||
```
|
||||
|
||||
## i18n Support
|
||||
|
||||
```typescript
|
||||
import { i18n } from '../locales';
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: i18n.t('menu.file'),
|
||||
submenu: [
|
||||
{ label: i18n.t('menu.new'), click: createNew },
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Use standard roles (`role: 'copy'`) for native behavior
|
||||
2. Use `CmdOrCtrl` for cross-platform shortcuts
|
||||
3. Use `{ type: 'separator' }` to group related items
|
||||
4. Handle platform differences with `process.platform`
|
||||
|
||||
```typescript
|
||||
if (process.platform === 'darwin') {
|
||||
template.unshift({ role: 'appMenu' });
|
||||
}
|
||||
```
|
||||
143
.agents/skills/desktop/references/window-management.md
Normal file
143
.agents/skills/desktop/references/window-management.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Desktop Window Management Guide
|
||||
|
||||
## Window Management Overview
|
||||
|
||||
1. Window creation and configuration
|
||||
2. Window state management (size, position, maximize)
|
||||
3. Multi-window coordination
|
||||
4. Window event handling
|
||||
|
||||
## File Structure
|
||||
|
||||
```plaintext
|
||||
apps/desktop/src/main/
|
||||
├── appBrowsers.ts # Core window management
|
||||
├── controllers/
|
||||
│ └── BrowserWindowsCtr.ts # Window controller
|
||||
└── modules/
|
||||
└── browserWindowManager.ts # Window manager module
|
||||
```
|
||||
|
||||
## Window Creation
|
||||
|
||||
```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');
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../../renderer/index.html'));
|
||||
}
|
||||
|
||||
return mainWindow;
|
||||
};
|
||||
```
|
||||
|
||||
## Window State Persistence
|
||||
|
||||
```typescript
|
||||
const saveWindowState = (window: BrowserWindow) => {
|
||||
if (!window.isMinimized() && !window.isMaximized()) {
|
||||
const [x, y] = window.getPosition();
|
||||
const [width, height] = window.getSize();
|
||||
settings.set('windowState', { x, y, width, height });
|
||||
}
|
||||
};
|
||||
|
||||
const restoreWindowState = (window: BrowserWindow) => {
|
||||
const state = settings.get('windowState');
|
||||
if (state) {
|
||||
window.setBounds({ x: state.x, y: state.y, width: state.width, height: state.height });
|
||||
}
|
||||
};
|
||||
|
||||
window.on('close', () => saveWindowState(window));
|
||||
```
|
||||
|
||||
## Multi-Window Management
|
||||
|
||||
```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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Window IPC Controller
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
|
||||
export default class BrowserWindowsCtr extends ControllerModule {
|
||||
static override readonly groupName = 'windows';
|
||||
|
||||
@IpcMethod()
|
||||
minimizeWindow() {
|
||||
BrowserWindow.getFocusedWindow()?.minimize();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
maximizeWindow() {
|
||||
const win = BrowserWindow.getFocusedWindow();
|
||||
win?.isMaximized() ? win.restore() : win?.maximize();
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Renderer Service
|
||||
|
||||
```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(),
|
||||
};
|
||||
```
|
||||
|
||||
## Frameless Window
|
||||
|
||||
```typescript
|
||||
const window = new BrowserWindow({
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden',
|
||||
});
|
||||
```
|
||||
|
||||
```css
|
||||
.titlebar { -webkit-app-region: drag; }
|
||||
.titlebar-button { -webkit-app-region: no-drag; }
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Use `show: false` initially, show after content loads
|
||||
2. Always set secure `webPreferences`
|
||||
3. Handle `webContents.on('crashed')` for recovery
|
||||
4. Clean up resources on `window.on('closed')`
|
||||
129
.agents/skills/drizzle/SKILL.md
Normal file
129
.agents/skills/drizzle/SKILL.md
Normal file
@@ -0,0 +1,129 @@
|
||||
---
|
||||
name: drizzle
|
||||
description: Drizzle ORM schema and database guide. Use when working with database schemas (src/database/schemas/*), defining tables, creating migrations, or database model code. Triggers on Drizzle schema definition, database migrations, or ORM usage questions.
|
||||
---
|
||||
|
||||
# Drizzle ORM Schema Style Guide
|
||||
|
||||
## Configuration
|
||||
|
||||
- Config: `drizzle.config.ts`
|
||||
- Schemas: `src/database/schemas/`
|
||||
- Migrations: `src/database/migrations/`
|
||||
- Dialect: `postgresql` with `strict: true`
|
||||
|
||||
## Helper Functions
|
||||
|
||||
Location: `src/database/schemas/_helpers.ts`
|
||||
|
||||
- `timestamptz(name)`: Timestamp with timezone
|
||||
- `createdAt()`, `updatedAt()`, `accessedAt()`: Standard timestamp columns
|
||||
- `timestamps`: Object with all three for easy spread
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Tables**: Plural snake_case (`users`, `session_groups`)
|
||||
- **Columns**: snake_case (`user_id`, `created_at`)
|
||||
|
||||
## Column Definitions
|
||||
|
||||
### Primary Keys
|
||||
|
||||
```typescript
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => idGenerator('agents'))
|
||||
.notNull(),
|
||||
```
|
||||
|
||||
ID prefixes make entity types distinguishable. For internal tables, use `uuid`.
|
||||
|
||||
### Foreign Keys
|
||||
|
||||
```typescript
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
```
|
||||
|
||||
### Timestamps
|
||||
|
||||
```typescript
|
||||
...timestamps, // Spread from _helpers.ts
|
||||
```
|
||||
|
||||
### Indexes
|
||||
|
||||
```typescript
|
||||
// Return array (object style deprecated)
|
||||
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
|
||||
```
|
||||
|
||||
## Type Inference
|
||||
|
||||
```typescript
|
||||
export const insertAgentSchema = createInsertSchema(agents);
|
||||
export type NewAgent = typeof agents.$inferInsert;
|
||||
export type AgentItem = typeof agents.$inferSelect;
|
||||
```
|
||||
|
||||
## Example Pattern
|
||||
|
||||
```typescript
|
||||
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,
|
||||
},
|
||||
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
|
||||
);
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Junction Tables (Many-to-Many)
|
||||
|
||||
```typescript
|
||||
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] })],
|
||||
);
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
See `references/db-migrations.md` for detailed migration guide.
|
||||
|
||||
```bash
|
||||
# Generate migrations
|
||||
bun run db:generate
|
||||
|
||||
# After modifying SQL (e.g., adding IF NOT EXISTS)
|
||||
bun run db:generate:client
|
||||
```
|
||||
|
||||
### Migration Best Practices
|
||||
|
||||
```sql
|
||||
-- ✅ 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");
|
||||
|
||||
-- ❌ Non-idempotent
|
||||
ALTER TABLE "users" ADD COLUMN "avatar" text;
|
||||
```
|
||||
|
||||
Rename migration files meaningfully: `0046_meaningless.sql` → `0046_user_add_avatar.sql`
|
||||
50
.agents/skills/drizzle/references/db-migrations.md
Normal file
50
.agents/skills/drizzle/references/db-migrations.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Database Migrations Guide
|
||||
|
||||
## Step 1: Generate Migrations
|
||||
|
||||
```bash
|
||||
bun run db:generate
|
||||
```
|
||||
|
||||
This generates:
|
||||
|
||||
- `packages/database/migrations/0046_meaningless_file_name.sql`
|
||||
|
||||
And updates:
|
||||
|
||||
- `packages/database/migrations/meta/_journal.json`
|
||||
- `packages/database/src/core/migrations.json`
|
||||
- `docs/development/database-schema.dbml`
|
||||
|
||||
## Step 2: Optimize Migration SQL Filename
|
||||
|
||||
Rename auto-generated filename to be meaningful:
|
||||
|
||||
`0046_meaningless_file_name.sql` → `0046_user_add_avatar_column.sql`
|
||||
|
||||
## Step 3: Use Idempotent Clauses (Defensive Programming)
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
bun run db:generate:client
|
||||
```
|
||||
|
||||
This updates the hash in `packages/database/src/core/migrations.json`.
|
||||
90
.agents/skills/hotkey/SKILL.md
Normal file
90
.agents/skills/hotkey/SKILL.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: hotkey
|
||||
description: Guide for adding keyboard shortcuts. Use when implementing new hotkeys, registering shortcuts, or working with keyboard interactions. Triggers on hotkey implementation or keyboard shortcut tasks.
|
||||
---
|
||||
|
||||
# Adding Keyboard Shortcuts Guide
|
||||
|
||||
## Steps to Add a New Hotkey
|
||||
|
||||
### 1. Update Hotkey Constant
|
||||
|
||||
In `src/types/hotkey.ts`:
|
||||
|
||||
```typescript
|
||||
export const HotkeyEnum = {
|
||||
// existing...
|
||||
ClearChat: 'clearChat', // Add new
|
||||
} as const;
|
||||
```
|
||||
|
||||
### 2. Register Default Hotkey
|
||||
|
||||
In `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. Add i18n Translation
|
||||
|
||||
In `src/locales/default/hotkey.ts`:
|
||||
|
||||
```typescript
|
||||
const hotkey: HotkeyI18nTranslations = {
|
||||
clearChat: {
|
||||
desc: '清空当前会话的所有消息记录',
|
||||
title: '清空聊天记录',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Create and Register Hook
|
||||
|
||||
In `src/hooks/useHotkeys/chatScope.ts`:
|
||||
|
||||
```typescript
|
||||
export const useClearChatHotkey = () => {
|
||||
const clearMessages = useChatStore((s) => s.clearMessages);
|
||||
return useHotkeyById(HotkeyEnum.ClearChat, clearMessages);
|
||||
};
|
||||
|
||||
export const useRegisterChatHotkeys = () => {
|
||||
useClearChatHotkey();
|
||||
// ...other hotkeys
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Add Tooltip (Optional)
|
||||
|
||||
```tsx
|
||||
const clearChatHotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ClearChat));
|
||||
|
||||
<Tooltip hotkey={clearChatHotkey} title={t('clearChat.title', { ns: 'hotkey' })}>
|
||||
<Button icon={<DeleteOutlined />} onClick={clearMessages} />
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Scope**: Choose global or chat scope based on functionality
|
||||
2. **Grouping**: Place in appropriate group (System/Layout/Conversation)
|
||||
3. **Conflict check**: Ensure no conflict with system/browser shortcuts
|
||||
4. **Platform**: Use `Key.Mod` instead of hardcoded `Ctrl` or `Cmd`
|
||||
5. **Clear description**: Provide title and description for users
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Not working**: Check scope and RegisterHotkeys hook
|
||||
- **Not in settings**: Verify HOTKEYS_REGISTRATION config
|
||||
- **Conflict**: HotkeyInput component shows warnings
|
||||
- **Page-specific**: Ensure correct scope activation
|
||||
@@ -1,15 +1,14 @@
|
||||
---
|
||||
globs: *.tsx
|
||||
alwaysApply: false
|
||||
name: i18n
|
||||
description: Internationalization guide using react-i18next. Use when adding translations, creating i18n keys, or working with localized text in React components (.tsx files). Triggers on translation tasks, locale management, or i18n implementation.
|
||||
---
|
||||
|
||||
# LobeChat Internationalization Guide
|
||||
|
||||
## Key Points
|
||||
|
||||
- Default language: Chinese (zh-CN), Framework: react-i18next
|
||||
- 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)
|
||||
- Run `pnpm i18n` to generate translations (or manually translate zh-CN/en-US for dev preview)
|
||||
|
||||
## Key Naming Convention
|
||||
|
||||
@@ -19,37 +18,30 @@ alwaysApply: false
|
||||
// ✅ Correct
|
||||
export default {
|
||||
'alert.cloud.action': '立即体验',
|
||||
'clientDB.error.desc': '数据库初始化遇到问题',
|
||||
'sync.actions.sync': '立即同步',
|
||||
'sync.status.ready': '已连接',
|
||||
};
|
||||
|
||||
// ❌ Avoid: Nested objects
|
||||
// ❌ 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
|
||||
**Patterns:** `{feature}.{context}.{action|status}`
|
||||
|
||||
**Parameters:** Use `{{variableName}}` syntax
|
||||
|
||||
```typescript
|
||||
'alert.cloud.desc': '我们提供 {{credit}} 额度积分',
|
||||
```
|
||||
|
||||
**Avoid key conflicts:** Don't use both a leaf key and its parent path
|
||||
|
||||
**Avoid key conflicts:**
|
||||
```typescript
|
||||
// ❌ Conflict: clientDB.solve exists as both leaf and parent
|
||||
// ❌ Conflict
|
||||
'clientDB.solve': '自助解决',
|
||||
'clientDB.solve.backup.title': '数据备份',
|
||||
|
||||
// ✅ Solution: Use different suffixes
|
||||
// ✅ Solution
|
||||
'clientDB.solve.action': '自助解决',
|
||||
'clientDB.solve.backup.title': '数据备份',
|
||||
```
|
||||
@@ -68,17 +60,16 @@ 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
|
||||
## Common Namespaces
|
||||
|
||||
**Most used:** `common` (shared UI), `chat` (chat features), `setting` (settings)
|
||||
|
||||
Others: auth, changelog, components, discover, editor, electron, error, file, hotkey, knowledgeBase, memory, models, plugin, portal, providers, tool, topic
|
||||
51
.agents/skills/linear/SKILL.md
Normal file
51
.agents/skills/linear/SKILL.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: linear
|
||||
description: Linear issue management guide. Use when working with Linear issues, creating issues, updating status, or adding comments. Triggers on Linear issue references (LOBE-xxx), issue tracking, or project management tasks. Requires Linear MCP tools to be available.
|
||||
---
|
||||
|
||||
# Linear Issue Management
|
||||
|
||||
Before using Linear workflows, search for `linear` MCP tools. If not found, treat as not installed.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Retrieve issue details** before starting: `mcp__linear-server__get_issue`
|
||||
2. **Check for sub-issues**: Use `mcp__linear-server__list_issues` with `parentId` filter
|
||||
3. **Update issue status** when completing: `mcp__linear-server__update_issue`
|
||||
4. **Add completion comment** (REQUIRED): `mcp__linear-server__create_comment`
|
||||
|
||||
## Creating Issues
|
||||
|
||||
When creating issues with `mcp__linear-server__create_issue`, **MUST add the `claude code` label**.
|
||||
|
||||
## Completion Comment (REQUIRED)
|
||||
|
||||
Every completed issue MUST have a comment summarizing work done. This is critical for:
|
||||
- Team visibility
|
||||
- Code review context
|
||||
- Future reference
|
||||
|
||||
## PR Association (REQUIRED)
|
||||
|
||||
When creating PRs for Linear issues, include magic keywords in PR body:
|
||||
- `Fixes LOBE-123`
|
||||
- `Closes LOBE-123`
|
||||
- `Resolves LOBE-123`
|
||||
|
||||
## Per-Issue Completion Rule
|
||||
|
||||
When working on multiple issues, update EACH issue IMMEDIATELY after completing it:
|
||||
|
||||
1. Complete implementation
|
||||
2. Run `bun run type-check`
|
||||
3. Run related tests
|
||||
4. Create PR if needed
|
||||
5. Update status to **"In Review"** (NOT "Done")
|
||||
6. Add completion comment
|
||||
7. Move to next issue
|
||||
|
||||
**Note:** Status → "In Review" when PR created. "Done" only after PR merged.
|
||||
|
||||
**❌ Wrong:** Complete all → Update all statuses → Add all comments
|
||||
|
||||
**✅ Correct:** Complete A → Update A → Comment A → Complete B → ...
|
||||
83
.agents/skills/microcopy/SKILL.md
Normal file
83
.agents/skills/microcopy/SKILL.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: microcopy
|
||||
description: UI copy and microcopy guidelines. Use when writing UI text, buttons, error messages, empty states, onboarding, or any user-facing copy. Triggers on i18n translation, UI text writing, or copy improvement tasks. Supports both Chinese and English.
|
||||
---
|
||||
|
||||
# LobeHub UI Microcopy Guidelines
|
||||
|
||||
Brand: **Where Agents Collaborate** - Focus on collaborative agent system, not just "generation".
|
||||
|
||||
## Fixed Terminology
|
||||
|
||||
| Chinese | English |
|
||||
|---------|---------|
|
||||
| 空间 | Workspace |
|
||||
| 助理 | Agent |
|
||||
| 群组 | Group |
|
||||
| 上下文 | Context |
|
||||
| 记忆 | Memory |
|
||||
| 连接器 | Integration |
|
||||
| 技能 | Skill |
|
||||
| 助理档案 | Agent Profile |
|
||||
| 话题 | Topic |
|
||||
| 文稿 | Page |
|
||||
| 社区 | Community |
|
||||
| 资源 | Resource |
|
||||
| 库 | Library |
|
||||
| 模型服务商 | Provider |
|
||||
|
||||
## Brand Principles
|
||||
|
||||
1. **Create**: One sentence → usable Agent; clear next step
|
||||
2. **Collaborate**: Multi-agent; shared Context; controlled
|
||||
3. **Evolve**: Remember with consent; explainable; replayable
|
||||
|
||||
## Writing Rules
|
||||
|
||||
1. **Clarity first**: Short sentences, strong verbs, minimal adjectives
|
||||
2. **Layered**: Main line (simple) + optional detail (precise)
|
||||
3. **Consistent verbs**: Create / Connect / Run / Pause / Retry / View details
|
||||
4. **Actionable**: Every message tells next step; avoid generic "OK/Cancel"
|
||||
|
||||
## Human Warmth (Balanced)
|
||||
|
||||
Default: **80% information, 20% warmth**
|
||||
Key moments: **70/30** (first-time, empty state, failures, long waits)
|
||||
|
||||
**Hard cap**: At most half sentence of warmth, followed by clear next step.
|
||||
|
||||
**Order**:
|
||||
1. Acknowledge situation (no judgment)
|
||||
2. Restore control (pause/replay/edit/undo/clear Memory)
|
||||
3. Provide next action
|
||||
|
||||
**Avoid**: Preachy encouragement, grand narratives, over-anthropomorphizing
|
||||
|
||||
## Patterns
|
||||
|
||||
**Getting started**:
|
||||
- "Starting with one sentence is enough. Describe your goal."
|
||||
- "Not sure where to begin? Tell me the outcome."
|
||||
|
||||
**Long wait**:
|
||||
- "Running… You can switch tasks—I'll notify you when done."
|
||||
- "This may take a few minutes. To speed up: reduce Context / switch model."
|
||||
|
||||
**Failure**:
|
||||
- "That didn't run through. Retry, or view details to fix."
|
||||
- "Connection failed. Re-authorize in Settings, or try again later."
|
||||
|
||||
**Collaboration**:
|
||||
- "Align everyone to the same Context."
|
||||
- "Different opinions are fine. Write the goal first."
|
||||
|
||||
## Errors/Exceptions
|
||||
|
||||
Must include:
|
||||
1. **What happened**
|
||||
2. (Optional) **Why**
|
||||
3. **What user can do next**
|
||||
|
||||
Provide: Retry / View details / Go to Settings / Contact support / Copy logs
|
||||
|
||||
Never blame user. Put error codes in "Details".
|
||||
102
.agents/skills/modal/SKILL.md
Normal file
102
.agents/skills/modal/SKILL.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: modal
|
||||
description: Modal imperative API guide. Use when creating modal dialogs using createModal from @lobehub/ui. Triggers on modal component implementation or dialog creation tasks.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Modal Imperative API Guide
|
||||
|
||||
Use `createModal` from `@lobehub/ui` for imperative modal dialogs.
|
||||
|
||||
## Why Imperative?
|
||||
|
||||
| Mode | Characteristics | Recommended |
|
||||
|------|-----------------|-------------|
|
||||
| Declarative | Need `open` state, render `<Modal />` | ❌ |
|
||||
| Imperative | Call function directly, no state | ✅ |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
features/
|
||||
└── MyFeatureModal/
|
||||
├── index.tsx # Export createXxxModal
|
||||
└── MyFeatureContent.tsx # Modal content
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Content Component (`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(); // Optional: get close method
|
||||
|
||||
return <div>{/* Modal content */}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Export createModal (`index.tsx`)
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { createModal } from '@lobehub/ui';
|
||||
import { t } from 'i18next'; // Note: use i18next, not 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. Usage
|
||||
|
||||
```tsx
|
||||
import { createMyFeatureModal } from '@/features/MyFeatureModal';
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
createMyFeatureModal();
|
||||
}, []);
|
||||
|
||||
return <Button onClick={handleOpen}>Open</Button>;
|
||||
```
|
||||
|
||||
## i18n Handling
|
||||
|
||||
- **Content component**: `useTranslation` hook (React context)
|
||||
- **createModal params**: `import { t } from 'i18next'` (non-hook, imperative)
|
||||
|
||||
## useModalContext Hook
|
||||
|
||||
```tsx
|
||||
const { close, setCanDismissByClickOutside } = useModalContext();
|
||||
```
|
||||
|
||||
## Common Config
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `allowFullscreen` | `boolean` | Allow fullscreen mode |
|
||||
| `destroyOnHidden` | `boolean` | Destroy content on close |
|
||||
| `footer` | `ReactNode \| null` | Footer content |
|
||||
| `width` | `string \| number` | Modal width |
|
||||
|
||||
## Examples
|
||||
|
||||
- `src/features/SkillStore/index.tsx`
|
||||
- `src/features/LibraryModal/CreateNew/index.tsx`
|
||||
@@ -1,19 +1,50 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
name: project-overview
|
||||
description: Complete project architecture and structure guide. Use when exploring the codebase, understanding project organization, finding files, or needing comprehensive architectural context. Triggers on architecture questions, directory navigation, or project overview needs.
|
||||
---
|
||||
|
||||
# LobeChat Project Structure
|
||||
# LobeChat Project Overview
|
||||
|
||||
## Project Description
|
||||
|
||||
Open-source, modern-design AI Agent Workspace: **LobeHub** (previously LobeChat).
|
||||
|
||||
**Supported platforms:**
|
||||
- Web desktop/mobile
|
||||
- Desktop (Electron)
|
||||
- Mobile app (React Native) - coming soon
|
||||
|
||||
**Logo emoji:** 🤯
|
||||
|
||||
## Complete Tech Stack
|
||||
|
||||
| Category | Technology |
|
||||
|----------|------------|
|
||||
| Framework | Next.js 16 + React 19 |
|
||||
| Routing | SPA inside Next.js with `react-router-dom` |
|
||||
| Language | TypeScript |
|
||||
| UI Components | `@lobehub/ui`, antd |
|
||||
| CSS-in-JS | antd-style |
|
||||
| Icons | lucide-react, `@ant-design/icons` |
|
||||
| i18n | react-i18next |
|
||||
| State | zustand |
|
||||
| URL Params | nuqs |
|
||||
| Data Fetching | SWR |
|
||||
| React Hooks | aHooks |
|
||||
| Date/Time | dayjs |
|
||||
| Utilities | es-toolkit |
|
||||
| API | TRPC (type-safe) |
|
||||
| Database | Neon PostgreSQL + Drizzle ORM |
|
||||
| Testing | Vitest |
|
||||
|
||||
## Complete Project Structure
|
||||
|
||||
This project uses common monorepo structure. The workspace packages name use `@lobechat/` namespace.
|
||||
Monorepo using `@lobechat/` namespace for workspace packages.
|
||||
|
||||
**note**: some not very important files are not shown for simplicity.
|
||||
|
||||
```plaintext
|
||||
```
|
||||
lobe-chat/
|
||||
├── apps/
|
||||
│ └── desktop/
|
||||
│ └── desktop/ # Electron desktop app
|
||||
├── docs/
|
||||
│ ├── changelog/
|
||||
│ ├── development/
|
||||
@@ -23,10 +54,10 @@ lobe-chat/
|
||||
│ ├── en-US/
|
||||
│ └── zh-CN/
|
||||
├── packages/
|
||||
│ ├── agent-runtime/
|
||||
│ ├── agent-runtime/ # Agent runtime
|
||||
│ ├── builtin-agents/
|
||||
│ ├── builtin-tool-*/ # builtin tool packages
|
||||
│ ├── business/ # cloud-only business logic packages
|
||||
│ ├── builtin-tool-*/ # Builtin tool packages
|
||||
│ ├── business/ # Cloud-only business logic
|
||||
│ │ ├── config/
|
||||
│ │ ├── const/
|
||||
│ │ └── model-runtime/
|
||||
@@ -59,8 +90,6 @@ lobe-chat/
|
||||
│ ├── types/
|
||||
│ ├── utils/
|
||||
│ └── web-crawler/
|
||||
├── public/
|
||||
├── scripts/
|
||||
├── src/
|
||||
│ ├── app/
|
||||
│ │ ├── (backend)/
|
||||
@@ -78,7 +107,7 @@ lobe-chat/
|
||||
│ │ │ ├── onboarding/
|
||||
│ │ │ └── router/
|
||||
│ │ └── desktop/
|
||||
│ ├── business/ # cloud-only business logic (client/server)
|
||||
│ ├── business/ # Cloud-only (client/server)
|
||||
│ │ ├── client/
|
||||
│ │ ├── locales/
|
||||
│ │ └── server/
|
||||
@@ -117,33 +146,32 @@ lobe-chat/
|
||||
│ ├── tools/
|
||||
│ ├── types/
|
||||
│ └── utils/
|
||||
└── package.json
|
||||
└── e2e/ # E2E tests (Cucumber + Playwright)
|
||||
```
|
||||
|
||||
## 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/*`
|
||||
| Layer | Location |
|
||||
|-------|----------|
|
||||
| UI Components | `src/components`, `src/features` |
|
||||
| Global Providers | `src/layout` |
|
||||
| Zustand Stores | `src/store` |
|
||||
| Client Services | `src/services/` |
|
||||
| REST API | `src/app/(backend)/webapi` |
|
||||
| tRPC Routers | `src/server/routers/{async\|lambda\|mobile\|tools}` |
|
||||
| Server Services | `src/server/services` (can access DB) |
|
||||
| Server Modules | `src/server/modules` (no DB access) |
|
||||
| Feature Flags | `src/server/featureFlags` |
|
||||
| Global Config | `src/server/globalConfig` |
|
||||
| DB Schema | `packages/database/src/schemas` |
|
||||
| DB Model | `packages/database/src/models` |
|
||||
| DB Repository | `packages/database/src/repositories` |
|
||||
| Third-party | `src/libs` (analytics, oidc, etc.) |
|
||||
| Builtin Tools | `src/tools`, `packages/builtin-tool-*` |
|
||||
| Cloud-only | `src/business/*`, `packages/business/*` |
|
||||
|
||||
## Data Flow Architecture
|
||||
## Data Flow
|
||||
|
||||
React UI → Store Actions → Client Service → TRPC Lambda → Server Services -> DB Model → PostgreSQL (Remote)
|
||||
```
|
||||
React UI → Store Actions → Client Service → TRPC Lambda → Server Services → DB Model → PostgreSQL
|
||||
```
|
||||
73
.agents/skills/react/SKILL.md
Normal file
73
.agents/skills/react/SKILL.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: react
|
||||
description: React component development guide. Use when working with React components (.tsx files), creating UI, using @lobehub/ui components, implementing routing, or building frontend features. Triggers on React component creation, modification, layout implementation, or navigation tasks.
|
||||
---
|
||||
|
||||
# React Component Writing Guide
|
||||
|
||||
- Use antd-style for complex styles; for simple cases, use inline `style` attribute
|
||||
- Use `Flexbox` and `Center` from `@lobehub/ui` for layouts (see `references/layout-kit.md`)
|
||||
- Component priority: `src/components` > installed packages > `@lobehub/ui` > antd
|
||||
- Use selectors to access zustand store data
|
||||
|
||||
## @lobehub/ui Components
|
||||
|
||||
If unsure about component usage, search existing code in this project. Most components extend antd with additional props.
|
||||
|
||||
Reference: `node_modules/@lobehub/ui/es/index.mjs` for all available components.
|
||||
|
||||
**Common Components:**
|
||||
- General: ActionIcon, ActionIconGroup, Block, Button, Icon
|
||||
- Data Display: Avatar, Collapse, Empty, Highlighter, Markdown, Tag, Tooltip
|
||||
- Data Entry: CodeEditor, CopyButton, EditableText, Form, FormModal, Input, SearchBar, Select
|
||||
- Feedback: Alert, Drawer, Modal
|
||||
- Layout: Center, DraggablePanel, Flexbox, Grid, Header, MaskShadow
|
||||
- Navigation: Burger, Dropdown, Menu, SideNav, Tabs
|
||||
|
||||
## Routing Architecture
|
||||
|
||||
Hybrid routing: Next.js App Router (static pages) + React Router DOM (main SPA).
|
||||
|
||||
| Route Type | Use Case | Implementation |
|
||||
|------------|----------|----------------|
|
||||
| Next.js App Router | Auth pages (login, signup, oauth) | `src/app/[variants]/(auth)/` |
|
||||
| React Router DOM | Main SPA (chat, settings) | `desktopRouter.config.tsx` |
|
||||
|
||||
### Key Files
|
||||
- Entry: `src/app/[variants]/page.tsx`
|
||||
- Desktop router: `src/app/[variants]/router/desktopRouter.config.tsx`
|
||||
- Mobile router: `src/app/[variants]/(mobile)/router/mobileRouter.config.tsx`
|
||||
- Router utilities: `src/utils/router.tsx`
|
||||
|
||||
### Router Utilities
|
||||
|
||||
```tsx
|
||||
import { dynamicElement, redirectElement, ErrorBoundary } from '@/utils/router';
|
||||
|
||||
element: dynamicElement(() => import('./chat'), 'Desktop > Chat');
|
||||
element: redirectElement('/settings/profile');
|
||||
errorElement: <ErrorBoundary resetPath="/chat" />;
|
||||
```
|
||||
|
||||
### Navigation
|
||||
|
||||
**Important**: For SPA pages, use `Link` from `react-router-dom`, NOT `next/link`.
|
||||
|
||||
```tsx
|
||||
// ❌ Wrong
|
||||
import Link from 'next/link';
|
||||
<Link href="/">Home</Link>
|
||||
|
||||
// ✅ Correct
|
||||
import { Link } from 'react-router-dom';
|
||||
<Link to="/">Home</Link>
|
||||
|
||||
// In components
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
const navigate = useNavigate();
|
||||
navigate('/chat');
|
||||
|
||||
// From stores
|
||||
const navigate = useGlobalStore.getState().navigate;
|
||||
navigate?.('/settings');
|
||||
```
|
||||
100
.agents/skills/react/references/layout-kit.md
Normal file
100
.agents/skills/react/references/layout-kit.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Flexbox Layout Components Guide
|
||||
|
||||
`@lobehub/ui` provides `Flexbox` and `Center` components for creating flexible layouts.
|
||||
|
||||
## Flexbox Component
|
||||
|
||||
Flexbox is the most commonly used layout component, similar to CSS `display: flex`.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```jsx
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
|
||||
// Default vertical layout
|
||||
<Flexbox>
|
||||
<div>Child 1</div>
|
||||
<div>Child 2</div>
|
||||
</Flexbox>
|
||||
|
||||
// Horizontal layout
|
||||
<Flexbox horizontal>
|
||||
<div>Left</div>
|
||||
<div>Right</div>
|
||||
</Flexbox>
|
||||
```
|
||||
|
||||
### Common Props
|
||||
|
||||
- `horizontal`: Boolean, set horizontal direction layout
|
||||
- `flex`: Number or string, controls flex property
|
||||
- `gap`: Number, spacing between children
|
||||
- `align`: Alignment like 'center', 'flex-start', etc.
|
||||
- `justify`: Main axis alignment like 'space-between', 'center', etc.
|
||||
- `padding`: Padding value
|
||||
- `paddingInline`: Horizontal padding
|
||||
- `paddingBlock`: Vertical padding
|
||||
- `width/height`: Set dimensions, typically '100%' or specific pixels
|
||||
- `style`: Custom style object
|
||||
|
||||
### Layout Example
|
||||
|
||||
```jsx
|
||||
// Classic three-column layout
|
||||
<Flexbox horizontal height={'100%'} width={'100%'}>
|
||||
{/* Left sidebar */}
|
||||
<Flexbox
|
||||
width={260}
|
||||
style={{
|
||||
borderRight: `1px solid ${theme.colorBorderSecondary}`,
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<SidebarContent />
|
||||
</Flexbox>
|
||||
|
||||
{/* Center content */}
|
||||
<Flexbox flex={1} style={{ height: '100%' }}>
|
||||
<Flexbox flex={1} padding={24} style={{ overflowY: 'auto' }}>
|
||||
<MainContent />
|
||||
</Flexbox>
|
||||
|
||||
{/* Footer */}
|
||||
<Flexbox
|
||||
style={{
|
||||
borderTop: `1px solid ${theme.colorBorderSecondary}`,
|
||||
padding: '16px 24px',
|
||||
}}
|
||||
>
|
||||
<Footer />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
```
|
||||
|
||||
## Center Component
|
||||
|
||||
Center wraps Flexbox with horizontal and vertical centering.
|
||||
|
||||
```jsx
|
||||
import { Center } from '@lobehub/ui';
|
||||
|
||||
<Center width={'100%'} height={'100%'}>
|
||||
<Content />
|
||||
</Center>
|
||||
|
||||
// Icon centered
|
||||
<Center className={styles.icon} flex={'none'} height={40} width={40}>
|
||||
<Icon icon={icon} size={24} />
|
||||
</Center>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Use `flex={1}` to fill available space
|
||||
- Use `gap` instead of margin for spacing
|
||||
- Nest Flexbox for complex layouts
|
||||
- Set `overflow: 'auto'` for scrollable content
|
||||
- Use `horizontal` for horizontal layout (default is vertical)
|
||||
- Combine with `useTheme` hook for theme-responsive layouts
|
||||
108
.agents/skills/recent-data/SKILL.md
Normal file
108
.agents/skills/recent-data/SKILL.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
name: recent-data
|
||||
description: Guide for using Recent Data (topics, resources, pages). Use when working with recently accessed items, implementing recent lists, or accessing session store recent data. Triggers on recent data usage or implementation tasks.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# Recent Data Usage Guide
|
||||
|
||||
Recent data (recentTopics, recentResources, recentPages) is stored in session store.
|
||||
|
||||
## Initialization
|
||||
|
||||
In app top-level (e.g., `RecentHydration.tsx`):
|
||||
|
||||
```tsx
|
||||
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
|
||||
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
|
||||
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
|
||||
|
||||
const App = () => {
|
||||
useInitRecentTopic();
|
||||
useInitRecentResource();
|
||||
useInitRecentPage();
|
||||
return <YourComponents />;
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Method 1: Read from Store (Recommended)
|
||||
|
||||
```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>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Method 2: Use Hook Return (Single component)
|
||||
|
||||
```tsx
|
||||
const { data: recentTopics, isLoading } = useInitRecentTopic();
|
||||
```
|
||||
|
||||
## Available Selectors
|
||||
|
||||
### Recent Topics
|
||||
|
||||
```tsx
|
||||
const recentTopics = useSessionStore(recentSelectors.recentTopics);
|
||||
// Type: RecentTopic[]
|
||||
|
||||
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
|
||||
// Type: boolean
|
||||
```
|
||||
|
||||
**RecentTopic type:**
|
||||
```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
|
||||
const recentResources = useSessionStore(recentSelectors.recentResources);
|
||||
// Type: FileListItem[]
|
||||
|
||||
const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
|
||||
```
|
||||
|
||||
### Recent Pages
|
||||
|
||||
```tsx
|
||||
const recentPages = useSessionStore(recentSelectors.recentPages);
|
||||
const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
1. **Auto login detection**: Only loads when user is logged in
|
||||
2. **Data caching**: Stored in store, no repeated loading
|
||||
3. **Auto refresh**: SWR refreshes on focus (5-minute interval)
|
||||
4. **Type safe**: Full TypeScript types
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Initialize all recent data at app top-level
|
||||
2. Use selectors to read from store
|
||||
3. For multi-component use, prefer Method 1
|
||||
4. Use selectors for render optimization
|
||||
89
.agents/skills/testing/SKILL.md
Normal file
89
.agents/skills/testing/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: testing
|
||||
description: Testing guide using Vitest. Use when writing tests (.test.ts, .test.tsx), fixing failing tests, improving test coverage, or debugging test issues. Triggers on test creation, test debugging, mock setup, or test-related questions.
|
||||
---
|
||||
|
||||
# LobeChat Testing Guide
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
# Run specific test file
|
||||
bunx vitest run --silent='passed-only' '[file-path]'
|
||||
|
||||
# Database package (client)
|
||||
cd packages/database && bunx vitest run --silent='passed-only' '[file]'
|
||||
|
||||
# Database package (server)
|
||||
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' '[file]'
|
||||
```
|
||||
|
||||
**Never run** `bun run test` - it runs all 3000+ tests (~10 minutes).
|
||||
|
||||
## Test Categories
|
||||
|
||||
| Category | Location | Config |
|
||||
|----------|----------|--------|
|
||||
| Webapp | `src/**/*.test.ts(x)` | `vitest.config.ts` |
|
||||
| Packages | `packages/*/**/*.test.ts` | `packages/*/vitest.config.ts` |
|
||||
| Desktop | `apps/desktop/**/*.test.ts` | `apps/desktop/vitest.config.ts` |
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Prefer `vi.spyOn` over `vi.mock`** - More targeted, easier to maintain
|
||||
2. **Tests must pass type check** - Run `bun run type-check` after writing tests
|
||||
3. **After 1-2 failed fix attempts, stop and ask for help**
|
||||
4. **Test behavior, not implementation details**
|
||||
|
||||
## Basic Test Structure
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('ModuleName', () => {
|
||||
describe('functionName', () => {
|
||||
it('should handle normal case', () => {
|
||||
// Arrange → Act → Assert
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mock Patterns
|
||||
|
||||
```typescript
|
||||
// ✅ Spy on direct dependencies
|
||||
vi.spyOn(messageService, 'createMessage').mockResolvedValue('id');
|
||||
|
||||
// ✅ Use vi.stubGlobal for browser APIs
|
||||
vi.stubGlobal('Image', mockImage);
|
||||
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock');
|
||||
|
||||
// ❌ Avoid mocking entire modules globally
|
||||
vi.mock('@/services/chat'); // Too broad
|
||||
```
|
||||
|
||||
## Detailed Guides
|
||||
|
||||
See `references/` for specific testing scenarios:
|
||||
- **Database Model testing**: `references/db-model-test.md`
|
||||
- **Electron IPC testing**: `references/electron-ipc-test.md`
|
||||
- **Zustand Store Action testing**: `references/zustand-store-action-test.md`
|
||||
- **Agent Runtime E2E testing**: `references/agent-runtime-e2e.md`
|
||||
- **Desktop Controller testing**: `references/desktop-controller-test.md`
|
||||
|
||||
## Common Issues
|
||||
|
||||
1. **Module pollution**: Use `vi.resetModules()` when tests fail mysteriously
|
||||
2. **Mock not working**: Check setup position and use `vi.clearAllMocks()` in beforeEach
|
||||
3. **Test data pollution**: Clean database state in beforeEach/afterEach
|
||||
4. **Async issues**: Wrap state changes in `act()` for React hooks
|
||||
126
.agents/skills/testing/references/agent-runtime-e2e.md
Normal file
126
.agents/skills/testing/references/agent-runtime-e2e.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Agent Runtime E2E Testing Guide
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Minimal Mock Principle
|
||||
|
||||
Only mock **three external dependencies**:
|
||||
|
||||
| Dependency | Mock | Description |
|
||||
|------------|------|-------------|
|
||||
| Database | PGLite | In-memory database from `@lobechat/database/test-utils` |
|
||||
| Redis | InMemoryAgentStateManager | Memory implementation |
|
||||
| Redis | InMemoryStreamEventManager | Memory implementation |
|
||||
|
||||
**NOT mocked:**
|
||||
- `model-bank` - Uses real model config
|
||||
- `Mecha` (AgentToolsEngine, ContextEngineering)
|
||||
- `AgentRuntimeService`
|
||||
- `AgentRuntimeCoordinator`
|
||||
|
||||
### Use vi.spyOn, not vi.mock
|
||||
|
||||
Different tests need different LLM responses. `vi.spyOn` provides:
|
||||
- Flexible return values per test
|
||||
- Easy testing of different scenarios
|
||||
- Better test isolation
|
||||
|
||||
### Default Model: gpt-5
|
||||
|
||||
- Always available in `model-bank`
|
||||
- Stable across model updates
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Database Setup
|
||||
|
||||
```typescript
|
||||
import { LobeChatDatabase } from '@lobechat/database';
|
||||
import { getTestDB } from '@lobechat/database/test-utils';
|
||||
|
||||
let testDB: LobeChatDatabase;
|
||||
|
||||
beforeEach(async () => {
|
||||
testDB = await getTestDB();
|
||||
});
|
||||
```
|
||||
|
||||
### OpenAI Stream Response Helper
|
||||
|
||||
```typescript
|
||||
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();
|
||||
|
||||
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 handling
|
||||
// ... finish chunk
|
||||
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{ headers: { 'content-type': 'text/event-stream' } }
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```typescript
|
||||
import { InMemoryAgentStateManager, InMemoryStreamEventManager } from '@/server/modules/AgentRuntime';
|
||||
|
||||
const stateManager = new InMemoryAgentStateManager();
|
||||
const streamEventManager = new InMemoryStreamEventManager();
|
||||
|
||||
const service = new AgentRuntimeService(serverDB, userId, {
|
||||
coordinatorOptions: { stateManager, streamEventManager },
|
||||
queueService: null,
|
||||
streamEventManager,
|
||||
});
|
||||
```
|
||||
|
||||
### Mock OpenAI API
|
||||
|
||||
```typescript
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
||||
|
||||
it('should handle text response', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(createOpenAIStreamResponse({ content: 'Response text' }));
|
||||
// ... execute test
|
||||
});
|
||||
|
||||
it('should handle tool calls', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(createOpenAIStreamResponse({
|
||||
toolCalls: [{
|
||||
id: 'call_123',
|
||||
name: 'lobe-web-browsing____search____builtin',
|
||||
arguments: JSON.stringify({ query: 'weather' }),
|
||||
}],
|
||||
finishReason: 'tool_calls',
|
||||
}));
|
||||
// ... execute test
|
||||
});
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Test isolation**: Clean `InMemoryAgentStateManager` and `InMemoryStreamEventManager` after each test
|
||||
2. **Timeout**: E2E tests may need longer timeouts
|
||||
3. **Debug**: Use `DEBUG=lobe-server:*` for detailed logs
|
||||
124
.agents/skills/testing/references/db-model-test.md
Normal file
124
.agents/skills/testing/references/db-model-test.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Database Model Testing Guide
|
||||
|
||||
Test `packages/database` Model layer.
|
||||
|
||||
## Dual Environment Verification (Required)
|
||||
|
||||
```bash
|
||||
# 1. Client environment (fast)
|
||||
cd packages/database && TEST_SERVER_DB=0 bunx vitest run --silent='passed-only' '[file]'
|
||||
|
||||
# 2. Server environment (compatibility)
|
||||
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' '[file]'
|
||||
```
|
||||
|
||||
## User Permission Check - Security First 🔒
|
||||
|
||||
**Critical security requirement**: All user data operations must include permission checks.
|
||||
|
||||
```typescript
|
||||
// ❌ DANGEROUS: Missing permission check
|
||||
update = async (id: string, data: Partial<MyModel>) => {
|
||||
return this.db.update(myTable).set(data)
|
||||
.where(eq(myTable.id, id)) // Only checks ID
|
||||
.returning();
|
||||
};
|
||||
|
||||
// ✅ SECURE: Permission check included
|
||||
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) // ✅ Permission check
|
||||
))
|
||||
.returning();
|
||||
};
|
||||
```
|
||||
|
||||
## Test File Structure
|
||||
|
||||
```typescript
|
||||
// @vitest-environment node
|
||||
describe('MyModel', () => {
|
||||
describe('create', () => { /* ... */ });
|
||||
describe('queryAll', () => { /* ... */ });
|
||||
describe('update', () => {
|
||||
it('should update own records');
|
||||
it('should NOT update other users records'); // 🔒 Security
|
||||
});
|
||||
describe('delete', () => {
|
||||
it('should delete own records');
|
||||
it('should NOT delete other users records'); // 🔒 Security
|
||||
});
|
||||
describe('user isolation', () => {
|
||||
it('should enforce user data isolation'); // 🔒 Core security
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Security Test Example
|
||||
|
||||
```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' });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
const unchanged = await serverDB.query.myTable.findFirst({
|
||||
where: eq(myTable.id, otherUserRecord.id),
|
||||
});
|
||||
expect(unchanged?.data).toBe('original');
|
||||
});
|
||||
```
|
||||
|
||||
## Data Management
|
||||
|
||||
```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);
|
||||
});
|
||||
```
|
||||
|
||||
## Foreign Key Handling
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong: Invalid foreign key
|
||||
const testData = { asyncTaskId: 'invalid-uuid', fileId: 'non-existent' };
|
||||
|
||||
// ✅ Correct: Use null
|
||||
const testData = { asyncTaskId: null, fileId: null };
|
||||
|
||||
// ✅ Or: Create referenced record first
|
||||
beforeEach(async () => {
|
||||
const [asyncTask] = await serverDB.insert(asyncTasks)
|
||||
.values({ id: 'valid-id', status: 'pending' }).returning();
|
||||
testData.asyncTaskId = asyncTask.id;
|
||||
});
|
||||
```
|
||||
|
||||
## Predictable Sorting
|
||||
|
||||
```typescript
|
||||
// ✅ Use explicit timestamps
|
||||
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 },
|
||||
]);
|
||||
|
||||
// ❌ Don't rely on insert order
|
||||
await serverDB.insert(table).values([data1, data2]); // Unpredictable
|
||||
```
|
||||
124
.agents/skills/testing/references/desktop-controller-test.md
Normal file
124
.agents/skills/testing/references/desktop-controller-test.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Desktop Controller Unit Testing Guide
|
||||
|
||||
## Testing Framework & Directory Structure
|
||||
|
||||
LobeChat Desktop uses Vitest as the test framework. Controller unit tests should be placed in the `__tests__` directory adjacent to the controller file, named with the original controller filename plus `.test.ts`.
|
||||
|
||||
```plaintext
|
||||
apps/desktop/src/main/controllers/
|
||||
├── __tests__/
|
||||
│ ├── index.test.ts
|
||||
│ ├── MenuCtr.test.ts
|
||||
│ └── ...
|
||||
├── McpCtr.ts
|
||||
├── MenuCtr.ts
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Basic Test File Structure
|
||||
|
||||
```typescript
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { App } from '@/core/App';
|
||||
|
||||
import YourController from '../YourControllerName';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('dependency-module', () => ({
|
||||
dependencyFunction: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock App instance
|
||||
const mockApp = {
|
||||
// Mock necessary App properties and methods as needed
|
||||
} as unknown as App;
|
||||
|
||||
describe('YourController', () => {
|
||||
let controller: YourController;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
controller = new YourController(mockApp);
|
||||
});
|
||||
|
||||
describe('methodName', () => {
|
||||
it('test scenario description', async () => {
|
||||
// Prepare test data
|
||||
|
||||
// Execute method under test
|
||||
const result = await controller.methodName(params);
|
||||
|
||||
// Verify results
|
||||
expect(result).toMatchObject(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking External Dependencies
|
||||
|
||||
### Module Functions
|
||||
|
||||
```typescript
|
||||
const mockFunction = vi.fn();
|
||||
|
||||
vi.mock('module-name', () => ({
|
||||
functionName: mockFunction,
|
||||
}));
|
||||
```
|
||||
|
||||
### Node.js Core Modules
|
||||
|
||||
Example: mocking `child_process.exec` and `util.promisify`:
|
||||
|
||||
```typescript
|
||||
const mockExecImpl = vi.fn();
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
exec: vi.fn((cmd, callback) => {
|
||||
return mockExecImpl(cmd, callback);
|
||||
}),
|
||||
}));
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
}),
|
||||
}));
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Isolate tests**: Use `beforeEach` to reset mocks and state
|
||||
2. **Comprehensive coverage**: Test normal flows, edge cases, and error handling
|
||||
3. **Clear naming**: Test names should describe content and expected results
|
||||
4. **Avoid implementation details**: Test behavior, not implementation
|
||||
5. **Mock external dependencies**: Use `vi.mock()` for all external dependencies
|
||||
|
||||
## Example: Testing IPC Event Handler
|
||||
|
||||
```typescript
|
||||
it('should handle IPC event correctly', async () => {
|
||||
mockSomething.mockReturnValue({ result: 'success' });
|
||||
|
||||
const result = await controller.ipcMethodName({
|
||||
param1: 'value1',
|
||||
param2: 'value2',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
data: { result: 'success' },
|
||||
});
|
||||
|
||||
expect(mockSomething).toHaveBeenCalledWith('value1', 'value2');
|
||||
});
|
||||
```
|
||||
63
.agents/skills/testing/references/electron-ipc-test.md
Normal file
63
.agents/skills/testing/references/electron-ipc-test.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Electron IPC Testing Strategy
|
||||
|
||||
For Electron IPC tests, use **Mock return values** instead of real Electron environment.
|
||||
|
||||
## Basic Mock Setup
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
|
||||
|
||||
vi.mock('@/server/modules/ElectronIPCClient', () => ({
|
||||
electronIpcClient: {
|
||||
getFilePathById: vi.fn(),
|
||||
deleteFiles: vi.fn(),
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
## Setting Mock Behavior
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue('/path/to/file.txt');
|
||||
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Different Scenarios
|
||||
|
||||
```typescript
|
||||
it('should handle successful file deletion', async () => {
|
||||
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('should handle file deletion failure', async () => {
|
||||
vi.mocked(electronIpcClient.deleteFiles).mockRejectedValue(new Error('Delete failed'));
|
||||
|
||||
const result = await service.deleteFiles(['desktop://file1.txt']);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
## Advantages
|
||||
|
||||
1. **Environment simplification**: No complex Electron setup
|
||||
2. **Controlled testing**: Precise control over IPC return values
|
||||
3. **Scenario coverage**: Easy to test success/failure cases
|
||||
4. **Speed**: Mock calls are faster than real IPC
|
||||
|
||||
## Notes
|
||||
|
||||
- Ensure mock behavior matches real IPC interface
|
||||
- Use `vi.mocked()` for type safety
|
||||
- Reset mocks in `beforeEach` to avoid test interference
|
||||
- Verify both return values and that IPC methods were called correctly
|
||||
150
.agents/skills/testing/references/zustand-store-action-test.md
Normal file
150
.agents/skills/testing/references/zustand-store-action-test.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Zustand Store Action Testing Guide
|
||||
|
||||
## Basic Structure
|
||||
|
||||
```typescript
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useChatStore } from '../../store';
|
||||
|
||||
vi.mock('zustand/traditional');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useChatStore.setState({
|
||||
activeId: 'test-session-id',
|
||||
messagesMap: {},
|
||||
loadingIds: [],
|
||||
}, false);
|
||||
|
||||
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
refreshMessages: vi.fn(),
|
||||
internal_coreProcessMessage: vi.fn(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
### 1. Spy Direct Dependencies Only
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Spy on direct dependency
|
||||
const fetchAIChatSpy = vi.spyOn(result.current, 'internal_fetchAIChatMessage')
|
||||
.mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
|
||||
|
||||
// ❌ Bad: Spy on lower-level implementation
|
||||
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
|
||||
.mockImplementation(...);
|
||||
```
|
||||
|
||||
### 2. Minimize Global Spies
|
||||
|
||||
```typescript
|
||||
// ✅ Spy only when needed
|
||||
it('should process message', async () => {
|
||||
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
|
||||
.mockImplementation(...);
|
||||
// test logic
|
||||
streamSpy.mockRestore();
|
||||
});
|
||||
|
||||
// ❌ Don't setup all spies globally
|
||||
beforeEach(() => {
|
||||
vi.spyOn(chatService, 'createAssistantMessageStream').mockResolvedValue({});
|
||||
vi.spyOn(fileService, 'uploadFile').mockResolvedValue({});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Use act() for Async Operations
|
||||
|
||||
```typescript
|
||||
it('should send message', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.sendMessage({ message: 'Hello' });
|
||||
});
|
||||
|
||||
expect(messageService.createMessage).toHaveBeenCalled();
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Test Organization
|
||||
|
||||
```typescript
|
||||
describe('sendMessage', () => {
|
||||
describe('validation', () => {
|
||||
it('should not send when session is inactive');
|
||||
it('should not send when message is empty');
|
||||
});
|
||||
describe('message creation', () => {
|
||||
it('should create user message and trigger AI processing');
|
||||
});
|
||||
describe('error handling', () => {
|
||||
it('should handle message creation errors gracefully');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Streaming Response Mock
|
||||
|
||||
```typescript
|
||||
it('should handle streaming chunks', async () => {
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
|
||||
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
|
||||
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({...});
|
||||
});
|
||||
|
||||
streamSpy.mockRestore();
|
||||
});
|
||||
```
|
||||
|
||||
## SWR Hook Testing
|
||||
|
||||
```typescript
|
||||
it('should fetch data', async () => {
|
||||
const mockData = [{ id: '1', name: 'Item 1' }];
|
||||
vi.spyOn(discoverService, 'getPluginCategories').mockResolvedValue(mockData);
|
||||
|
||||
const { result } = renderHook(() => useStore.getState().usePluginCategories(params));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Key points for SWR:**
|
||||
- DO NOT mock useSWR - let it use real implementation
|
||||
- Only mock service methods (fetchers)
|
||||
- Use `waitFor` for async operations
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
```typescript
|
||||
// ❌ Don't mock entire store
|
||||
vi.mock('../../store', () => ({ useChatStore: vi.fn(() => ({...})) }));
|
||||
|
||||
// ❌ Don't test internal state structure
|
||||
expect(result.current.messagesMap).toHaveProperty('test-session');
|
||||
|
||||
// ✅ Test behavior instead
|
||||
expect(result.current.refreshMessages).toHaveBeenCalled();
|
||||
```
|
||||
52
.agents/skills/typescript/SKILL.md
Normal file
52
.agents/skills/typescript/SKILL.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: typescript
|
||||
description: TypeScript code style and optimization guidelines. Use when writing TypeScript code (.ts, .tsx, .mts files), reviewing code quality, or implementing type-safe patterns. Triggers on TypeScript development, type safety questions, or code style discussions.
|
||||
---
|
||||
|
||||
# TypeScript Code Style Guide
|
||||
|
||||
## Types and Type Safety
|
||||
|
||||
- Avoid explicit type annotations when TypeScript can infer
|
||||
- Avoid implicitly `any`; explicitly type when necessary
|
||||
- Use accurate types: prefer `Record<PropertyKey, unknown>` over `object` or `any`
|
||||
- Prefer `interface` for object shapes (e.g., React props); use `type` for unions/intersections
|
||||
- Prefer `as const satisfies XyzInterface` over plain `as const`
|
||||
- Prefer `@ts-expect-error` over `@ts-ignore` over `as any`
|
||||
- Avoid meaningless null/undefined parameters; design strict function contracts
|
||||
|
||||
## Async Patterns
|
||||
|
||||
- Prefer `async`/`await` over callbacks or `.then()` chains
|
||||
- Prefer async APIs over sync ones (avoid `*Sync`)
|
||||
- Use promise-based variants: `import { readFile } from 'fs/promises'`
|
||||
- Use `Promise.all`, `Promise.race` for concurrent operations where safe
|
||||
|
||||
## Code Structure
|
||||
|
||||
- Prefer object destructuring
|
||||
- Use consistent, descriptive naming; avoid obscure abbreviations
|
||||
- Replace magic numbers/strings with well-named constants
|
||||
- Defer formatting to tooling
|
||||
|
||||
## UI and Theming
|
||||
|
||||
- Use `@lobehub/ui`, Ant Design components instead of raw HTML tags
|
||||
- Design for dark mode and mobile responsiveness
|
||||
- Use `antd-style` token system instead of hard-coded colors
|
||||
|
||||
## Performance
|
||||
|
||||
- Prefer `for…of` loops over index-based `for` loops
|
||||
- Reuse existing utils in `packages/utils` or installed npm packages
|
||||
- Query only required columns from database
|
||||
|
||||
## Time Consistency
|
||||
|
||||
- Assign `Date.now()` to a constant once and reuse for consistency
|
||||
|
||||
## Logging
|
||||
|
||||
- Never log user private information (API keys, etc.)
|
||||
- Don't use `import { log } from 'debug'` directly (logs to console)
|
||||
- Use `console.error` in catch blocks instead of debug package
|
||||
78
.agents/skills/zustand/SKILL.md
Normal file
78
.agents/skills/zustand/SKILL.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: zustand
|
||||
description: Zustand state management guide. Use when working with store code (src/store/**), implementing actions, managing state, or creating slices. Triggers on Zustand store development, state management questions, or action implementation.
|
||||
---
|
||||
|
||||
# LobeChat Zustand State Management
|
||||
|
||||
## Action Type Hierarchy
|
||||
|
||||
### 1. Public Actions
|
||||
Main interfaces for UI components:
|
||||
- Naming: Verb form (`createTopic`, `sendMessage`)
|
||||
- Responsibilities: Parameter validation, flow orchestration
|
||||
|
||||
### 2. Internal Actions (`internal_*`)
|
||||
Core business logic implementation:
|
||||
- Naming: `internal_` prefix (`internal_createTopic`)
|
||||
- Responsibilities: Optimistic updates, service calls, error handling
|
||||
- Should not be called directly by UI
|
||||
|
||||
### 3. Dispatch Methods (`internal_dispatch*`)
|
||||
State update handlers:
|
||||
- Naming: `internal_dispatch` + entity (`internal_dispatchTopic`)
|
||||
- Responsibilities: Calling reducers, updating store
|
||||
|
||||
## When to Use Reducer vs Simple `set`
|
||||
|
||||
**Use Reducer Pattern:**
|
||||
- Managing object lists/maps (`messagesMap`, `topicMaps`)
|
||||
- Optimistic updates
|
||||
- Complex state transitions
|
||||
|
||||
**Use Simple `set`:**
|
||||
- Toggling booleans
|
||||
- Updating simple values
|
||||
- Setting single state fields
|
||||
|
||||
## Optimistic Update Pattern
|
||||
|
||||
```typescript
|
||||
internal_createTopic: async (params) => {
|
||||
const tmpId = Date.now().toString();
|
||||
|
||||
// 1. Immediately update frontend (optimistic)
|
||||
get().internal_dispatchTopic(
|
||||
{ type: 'addTopic', value: { ...params, id: tmpId } },
|
||||
'internal_createTopic'
|
||||
);
|
||||
|
||||
// 2. Call backend service
|
||||
const topicId = await topicService.createTopic(params);
|
||||
|
||||
// 3. Refresh for consistency
|
||||
await get().refreshTopic();
|
||||
return topicId;
|
||||
},
|
||||
```
|
||||
|
||||
**Delete operations**: Don't use optimistic updates (destructive, complex recovery)
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
**Actions:**
|
||||
- Public: `createTopic`, `sendMessage`
|
||||
- Internal: `internal_createTopic`, `internal_updateMessageContent`
|
||||
- Dispatch: `internal_dispatchTopic`
|
||||
- Toggle: `internal_toggleMessageLoading`
|
||||
|
||||
**State:**
|
||||
- ID arrays: `messageLoadingIds`, `topicEditingIds`
|
||||
- Maps: `topicMaps`, `messagesMap`
|
||||
- Active: `activeTopicId`
|
||||
- Init flags: `topicsInit`
|
||||
|
||||
## Detailed Guides
|
||||
|
||||
- Action patterns: `references/action-patterns.md`
|
||||
- Slice organization: `references/slice-organization.md`
|
||||
125
.agents/skills/zustand/references/action-patterns.md
Normal file
125
.agents/skills/zustand/references/action-patterns.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Zustand Action Patterns
|
||||
|
||||
## Optimistic Update Implementation
|
||||
|
||||
### Standard Flow
|
||||
|
||||
```typescript
|
||||
internal_updateMessageContent: async (id, content, extra) => {
|
||||
const { internal_dispatchMessage, refreshMessages } = get();
|
||||
|
||||
// 1. Immediately update frontend
|
||||
internal_dispatchMessage({
|
||||
id,
|
||||
type: 'updateMessage',
|
||||
value: { content },
|
||||
});
|
||||
|
||||
// 2. Call backend
|
||||
await messageService.updateMessage(id, { content });
|
||||
|
||||
// 3. Refresh for consistency
|
||||
await refreshMessages();
|
||||
},
|
||||
```
|
||||
|
||||
### Create Operations
|
||||
|
||||
```typescript
|
||||
internal_createMessage: async (message, context) => {
|
||||
let tempId = context?.tempMessageId;
|
||||
if (!tempId) {
|
||||
tempId = internal_createTmpMessage(message);
|
||||
internal_toggleMessageLoading(true, tempId);
|
||||
}
|
||||
|
||||
try {
|
||||
const id = await messageService.createMessage(message);
|
||||
await refreshMessages();
|
||||
internal_toggleMessageLoading(false, tempId);
|
||||
return id;
|
||||
} catch (e) {
|
||||
internal_toggleMessageLoading(false, tempId);
|
||||
internal_dispatchMessage({
|
||||
id: tempId,
|
||||
type: 'updateMessage',
|
||||
value: { error: { type: ChatErrorType.CreateMessageError } },
|
||||
});
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
### Delete Operations (No Optimistic Update)
|
||||
|
||||
```typescript
|
||||
internal_removeGenerationTopic: async (id: string) => {
|
||||
get().internal_updateGenerationTopicLoading(id, true);
|
||||
|
||||
try {
|
||||
await generationTopicService.deleteTopic(id);
|
||||
await get().refreshGenerationTopics();
|
||||
} finally {
|
||||
get().internal_updateGenerationTopicLoading(id, false);
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
## Loading State Management
|
||||
|
||||
```typescript
|
||||
// Define in initialState.ts
|
||||
export interface ChatMessageState {
|
||||
messageEditingIds: string[];
|
||||
}
|
||||
|
||||
// Manage in action
|
||||
toggleMessageEditing: (id, editing) => {
|
||||
set(
|
||||
{ messageEditingIds: toggleBooleanList(get().messageEditingIds, id, editing) },
|
||||
false,
|
||||
'toggleMessageEditing'
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## SWR Integration
|
||||
|
||||
```typescript
|
||||
useFetchMessages: (enable, sessionId, activeTopicId) =>
|
||||
useClientDataSWR<ChatMessage[]>(
|
||||
enable ? [SWR_USE_FETCH_MESSAGES, sessionId, activeTopicId] : null,
|
||||
async ([, sessionId, topicId]) => messageService.getMessages(sessionId, topicId),
|
||||
{
|
||||
onSuccess: (messages) => {
|
||||
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
|
||||
refreshMessages: async () => {
|
||||
await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId]);
|
||||
};
|
||||
```
|
||||
|
||||
## Reducer Pattern
|
||||
|
||||
```typescript
|
||||
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(),
|
||||
});
|
||||
});
|
||||
}
|
||||
// ...other cases
|
||||
}
|
||||
};
|
||||
```
|
||||
125
.agents/skills/zustand/references/slice-organization.md
Normal file
125
.agents/skills/zustand/references/slice-organization.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Zustand Slice Organization
|
||||
|
||||
## Top-Level Store Structure
|
||||
|
||||
Key aggregation files:
|
||||
- `src/store/chat/initialState.ts`: Aggregate all slice initial states
|
||||
- `src/store/chat/store.ts`: Define top-level `ChatStore`, combine all slice actions
|
||||
- `src/store/chat/selectors.ts`: Export all slice selectors
|
||||
- `src/store/chat/helpers.ts`: Chat helper functions
|
||||
|
||||
## Store Aggregation Pattern
|
||||
|
||||
```typescript
|
||||
// src/store/chat/initialState.ts
|
||||
import { ChatTopicState, initialTopicState } from './slices/topic/initialState';
|
||||
import { ChatMessageState, initialMessageState } from './slices/message/initialState';
|
||||
|
||||
export type ChatStoreState = ChatTopicState & ChatMessageState & ...
|
||||
|
||||
export const initialState: ChatStoreState = {
|
||||
...initialMessageState,
|
||||
...initialTopicState,
|
||||
...
|
||||
};
|
||||
|
||||
// src/store/chat/store.ts
|
||||
export interface ChatStoreAction
|
||||
extends ChatMessageAction, ChatTopicAction, ...
|
||||
|
||||
const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...params) => ({
|
||||
...initialState,
|
||||
...chatMessage(...params),
|
||||
...chatTopic(...params),
|
||||
});
|
||||
|
||||
export const useChatStore = createWithEqualityFn<ChatStore>()(
|
||||
subscribeWithSelector(devtools(createStore)),
|
||||
shallow
|
||||
);
|
||||
```
|
||||
|
||||
## Single Slice Structure
|
||||
|
||||
```plaintext
|
||||
src/store/chat/slices/
|
||||
└── [sliceName]/
|
||||
├── action.ts # Define actions (or actions/ directory)
|
||||
├── initialState.ts # State structure and initial values
|
||||
├── reducer.ts # (Optional) Reducer pattern
|
||||
├── selectors.ts # Define selectors
|
||||
└── index.ts # (Optional) Re-exports
|
||||
```
|
||||
|
||||
### initialState.ts
|
||||
|
||||
```typescript
|
||||
export interface ChatTopicState {
|
||||
activeTopicId?: string;
|
||||
topicMaps: Record<string, ChatTopic[]>;
|
||||
topicsInit: boolean;
|
||||
topicLoadingIds: string[];
|
||||
}
|
||||
|
||||
export const initialTopicState: ChatTopicState = {
|
||||
activeTopicId: undefined,
|
||||
topicMaps: {},
|
||||
topicsInit: false,
|
||||
topicLoadingIds: [],
|
||||
};
|
||||
```
|
||||
|
||||
### selectors.ts
|
||||
|
||||
```typescript
|
||||
const currentTopics = (s: ChatStoreState): ChatTopic[] | undefined => s.topicMaps[s.activeId];
|
||||
|
||||
const getTopicById = (id: string) => (s: ChatStoreState): ChatTopic | undefined =>
|
||||
currentTopics(s)?.find((topic) => topic.id === id);
|
||||
|
||||
// Core pattern: Use xxxSelectors aggregate
|
||||
export const topicSelectors = {
|
||||
currentTopics,
|
||||
getTopicById,
|
||||
};
|
||||
```
|
||||
|
||||
## Complex Actions Sub-directory
|
||||
|
||||
```plaintext
|
||||
src/store/chat/slices/aiChat/
|
||||
├── actions/
|
||||
│ ├── generateAIChat.ts
|
||||
│ ├── rag.ts
|
||||
│ ├── memory.ts
|
||||
│ └── index.ts
|
||||
├── initialState.ts
|
||||
└── selectors.ts
|
||||
```
|
||||
|
||||
## State Design Patterns
|
||||
|
||||
### Map Structure for Associated Data
|
||||
```typescript
|
||||
topicMaps: Record<string, ChatTopic[]>;
|
||||
messagesMap: Record<string, ChatMessage[]>;
|
||||
```
|
||||
|
||||
### Arrays for Loading State
|
||||
```typescript
|
||||
messageLoadingIds: string[]
|
||||
topicLoadingIds: string[]
|
||||
```
|
||||
|
||||
### Optional Fields for Active Items
|
||||
```typescript
|
||||
activeId: string
|
||||
activeTopicId?: string
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Slice division**: By functional domain (message, topic, aiChat)
|
||||
2. **File naming**: camelCase for directories, consistent patterns
|
||||
3. **State structure**: Flat, avoid deep nesting
|
||||
4. **Type safety**: Clear TypeScript interfaces for each slice
|
||||
@@ -1 +1 @@
|
||||
../.agents
|
||||
../.agents/skills
|
||||
@@ -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`
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 语法问题
|
||||
- 纠正错别字
|
||||
- 指出示例与说明不一致之处
|
||||
@@ -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`.
|
||||
@@ -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-*';
|
||||
```
|
||||
@@ -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' }` 在逻辑上分隔不同组的菜单项
|
||||
@@ -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.
|
||||
@@ -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 组件中可以检测到冲突,用户会看到警告
|
||||
- **功能在某些页面失效**:确认是否注册在了正确的作用域,以及相关页面是否激活了该作用域
|
||||
|
||||
通过这些步骤,您可以确保新添加的快捷键功能稳定、可靠且用户友好。
|
||||
@@ -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 → ...
|
||||
@@ -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) 错误/异常/权限/付费:硬规则
|
||||
|
||||
- 必须包含:**发生了什么 +(可选)原因 + 你可以怎么做**
|
||||
- 必须提供可操作选项:**重试 / 查看详情 / 去设置 / 联系支持 / 复制日志**(按场景取舍)
|
||||
- 不责备用户;不只给错误码;错误码可放在“详情”里
|
||||
- 涉及数据与安全:语气更中性更完整,温度通过“尊重与解释”体现,而不是煽
|
||||
@@ -1,148 +0,0 @@
|
||||
---
|
||||
globs: src/locales/default/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
You are **LobeHub’s 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 (“don’t worry”, “stay positive”)
|
||||
- grand narratives
|
||||
- overly anthropomorphic claims (“I understand you”, “I’ll 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 I’ll help you set up the first Agent.”
|
||||
- “Not sure where to begin? Tell me the outcome—we’ll break it down together.”
|
||||
- **Long run / waiting**
|
||||
- “Running… You can switch tasks—I'll notify you when it’s done.”
|
||||
- “This may take a few minutes. To speed up: reduce Context / switch model / disable Auto-run.”
|
||||
- **Failure / retry**
|
||||
- “That didn’t 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. Don’t 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 LobeHub’s 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.
|
||||
@@ -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` hook(React 上下文)
|
||||
- **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 控制示例
|
||||
@@ -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 配合使用创建主题响应式的布局
|
||||
@@ -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
|
||||
@@ -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');
|
||||
```
|
||||
@@ -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 确保只有相关数据变化时才重新渲染
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user