diff --git a/.agents/skills/add-provider-doc/SKILL.md b/.agents/skills/add-provider-doc/SKILL.md new file mode 100644 index 0000000000..82568080ab --- /dev/null +++ b/.agents/skills/add-provider-doc/SKILL.md @@ -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 diff --git a/.agents/skills/add-setting-env/SKILL.md b/.agents/skills/add-setting-env/SKILL.md new file mode 100644 index 0000000000..d3e4de3a64 --- /dev/null +++ b/.agents/skills/add-setting-env/SKILL.md @@ -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/.ts`: + +```typescript +import { createEnv } from '@t3-oss/env-nextjs'; +import { z } from 'zod'; + +export const getConfig = () => { + 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 Env = getConfig(); +``` + +### 2. Update Type (if new domain) + +Add to `packages/types/src/serverConfig.ts`: + +```typescript +import { UserConfig } from './user/settings'; + +export interface GlobalServerConfig { + ?: PartialDeepConfig>; +} +``` + +**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 { Env } from '@/envs/'; + +export const getServerGlobalConfig = async () => { + const config: GlobalServerConfig = { + : cleanObject({ + : 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 = { + : serverConfig., +}; +``` + +### 5. Update .env.example + +```bash +# (range/options, default: X) +# YOUR_ENV_VAR= +``` + +### 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; + +// 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 +``` diff --git a/.agents/skills/debug/SKILL.md b/.agents/skills/debug/SKILL.md new file mode 100644 index 0000000000..ed28ae77f8 --- /dev/null +++ b/.agents/skills/debug/SKILL.md @@ -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); +``` diff --git a/.agents/skills/desktop/SKILL.md b/.agents/skills/desktop/SKILL.md new file mode 100644 index 0000000000..01f9650179 --- /dev/null +++ b/.agents/skills/desktop/SKILL.md @@ -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 { + // 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 diff --git a/.agents/skills/desktop/references/feature-implementation.md b/.agents/skills/desktop/references/feature-implementation.md new file mode 100644 index 0000000000..cfc155edf8 --- /dev/null +++ b/.agents/skills/desktop/references/feature-implementation.md @@ -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 { + 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 diff --git a/.agents/skills/desktop/references/local-tools.md b/.agents/skills/desktop/references/local-tools.md new file mode 100644 index 0000000000..ca6952d005 --- /dev/null +++ b/.agents/skills/desktop/references/local-tools.md @@ -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 `` and usage guidelines to ``. diff --git a/.agents/skills/desktop/references/menu-config.md b/.agents/skills/desktop/references/menu-config.md new file mode 100644 index 0000000000..769eaf0f4f --- /dev/null +++ b/.agents/skills/desktop/references/menu-config.md @@ -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' }); +} +``` diff --git a/.agents/skills/desktop/references/window-management.md b/.agents/skills/desktop/references/window-management.md new file mode 100644 index 0000000000..b8d9a7f8b3 --- /dev/null +++ b/.agents/skills/desktop/references/window-management.md @@ -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 = 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')` diff --git a/.agents/skills/drizzle/SKILL.md b/.agents/skills/drizzle/SKILL.md new file mode 100644 index 0000000000..a44389cef8 --- /dev/null +++ b/.agents/skills/drizzle/SKILL.md @@ -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(), + ...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` diff --git a/.agents/skills/drizzle/references/db-migrations.md b/.agents/skills/drizzle/references/db-migrations.md new file mode 100644 index 0000000000..a5afec8275 --- /dev/null +++ b/.agents/skills/drizzle/references/db-migrations.md @@ -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`. diff --git a/.agents/skills/hotkey/SKILL.md b/.agents/skills/hotkey/SKILL.md new file mode 100644 index 0000000000..2f96149aa8 --- /dev/null +++ b/.agents/skills/hotkey/SKILL.md @@ -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)); + + + ; +``` + +## 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` diff --git a/.cursor/rules/project-structure.mdc b/.agents/skills/project-overview/SKILL.md similarity index 54% rename from .cursor/rules/project-structure.mdc rename to .agents/skills/project-overview/SKILL.md index dd051d6890..342b504f14 100644 --- a/.cursor/rules/project-structure.mdc +++ b/.agents/skills/project-overview/SKILL.md @@ -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 +``` diff --git a/.agents/skills/react/SKILL.md b/.agents/skills/react/SKILL.md new file mode 100644 index 0000000000..205b028b0f --- /dev/null +++ b/.agents/skills/react/SKILL.md @@ -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: ; +``` + +### Navigation + +**Important**: For SPA pages, use `Link` from `react-router-dom`, NOT `next/link`. + +```tsx +// ❌ Wrong +import Link from 'next/link'; +Home + +// ✅ Correct +import { Link } from 'react-router-dom'; +Home + +// In components +import { useNavigate } from 'react-router-dom'; +const navigate = useNavigate(); +navigate('/chat'); + +// From stores +const navigate = useGlobalStore.getState().navigate; +navigate?.('/settings'); +``` diff --git a/.agents/skills/react/references/layout-kit.md b/.agents/skills/react/references/layout-kit.md new file mode 100644 index 0000000000..ba1a14b494 --- /dev/null +++ b/.agents/skills/react/references/layout-kit.md @@ -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 + +
Child 1
+
Child 2
+
+ +// Horizontal layout + +
Left
+
Right
+
+``` + +### 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 + + {/* Left sidebar */} + + + + + {/* Center content */} + + + + + + {/* Footer */} + +