Files
lobehub/docs/development/basic/feature-development.mdx
YuTengjing 463d6c8762 📝 docs: improve development guides to reflect current architecture (#12174)
* 🔧 chore(vscode): add typescript.tsdk and disable mdx server

Fix MDX extension crash caused by Cursor's bundled TypeScript version

* 🔧 chore(claude): add skills symlink to .claude directory

* 📝 docs: update development guides with current tech stack and architecture

- Update tech stack: Next.js 16 + React 19, hybrid routing (App Router + React Router DOM), tRPC, Drizzle ORM + PostgreSQL, react-i18next
- Update directory structure to reflect monorepo layout (apps/, packages/, e2e/, locales/)
- Expand src/server/ with detailed subdirectory descriptions
- Add complete SPA routing architecture with desktop and mobile route tables
- Add tRPC router grouping details (lambda, async, tools, mobile)
- Add data flow diagram
- Simplify dev setup section to link to setup-development guide
- Fix i18n default language description (English, not Chinese)
- Sync all changes between zh-CN and English versions

* 📝 docs: expand data flow diagram in folder structure guide

Replace the single-line data flow with a detailed layer-by-layer
flow diagram showing each layer's location and responsibility.

* 📝 docs: modernize feature development guide

- Remove outdated clientDB/pglite/indexDB references
- Update schema path to packages/database/src/schemas/
- Update types path to packages/types/src/
- Replace inline migration steps with link to db-migrations guide
- Add complete layered architecture table (Client Service, WebAPI,
  tRPC Router, Server Service, Server Module, Repository, DB Model)
- Clarify Client Service as frontend code
- Add i18n handling section with workflow and key naming convention
- Remove verbose CSS style code, keep core business logic only
- Expand testing section with commands, skill refs, and CI tip

* 🔥 docs: remove outdated frontend feature development guide

Content is superseded by the comprehensive feature-development guide
which covers the full chain from schema to testing.

* 📝 docs: add LobeHub ecosystem and community resources

Add official ecosystem packages (LobeUI, LobeIcons, LobeCharts,
LobeEditor, LobeTTS, LobeLint, Lobe i18n, MCP Mark) and community
platforms (Agent Market, MCP Market, YouTube, X, Discord).

* 📝 docs: improve contributing guidelines and resources

- Clarify semantic release triggers (feat/fix vs style/chore)
- Add testing section with Vitest/E2E/CI requirements
- Update contribution steps to include CI check
- Add LobeHub ecosystem packages and community platforms to resources

* 📝 docs: rewrite architecture guide to reflect current platform design

* 📝 docs: add code quality tools to architecture guide

* 📝 docs: rewrite chat-api guide to reflect current architecture

- Update sequence diagram with Agent Runtime loop as core execution engine
- Replace PluginGateway with ToolExecution layer (Builtin/MCP/Plugin)
- Update all path references (model-runtime, agent-runtime, fetch-sse packages)
- Split old AgentRuntime section into Model Runtime + Agent Runtime
- Add tool calling taxonomy: Builtin, MCP, and Plugin (deprecated)
- Add client-side vs server-side execution section
- Remove outdated adapter pseudo-code examples

* 📝 docs: update file paths in add-new-image-model guide

- src/libs/standard-parameters/ → packages/model-bank/src/standard-parameters/
- src/config/aiModels/ → packages/model-bank/src/aiModels/
- src/libs/model-runtime/ → packages/model-runtime/src/providers/

* 📝 docs: restore S3_PUBLIC_DOMAIN in deployment guides

The S3_PUBLIC_DOMAIN env var was incorrectly removed from all
documentation in commit 4a87b31. This variable is still required
by the code (src/server/services/file/impls/s3.ts) to generate
public URLs for uploaded files. Without it, image URLs sent to
vision models are just S3 keys instead of full URLs.

Closes #12161

* 📦 chore: pin @lobehub/ui to 4.33.4 to fix SortableList type errors

@lobehub/ui 4.34.0 introduced breaking type changes in SortableList
where SortableListItem became strict, causing type incompatibility
in onChange and renderItem callbacks across 6 files. Pin to 4.33.4
via pnpm overrides to enforce consistent version across monorepo.

* 🐛 fix: correct ReadableStream type annotations and add dom.asynciterable

- Add dom.asynciterable to tsconfig lib for ReadableStream async iteration
- Fix createCallbacksTransformer return type: TransformStream<string, Uint8Array>
- Update stream function return types from ReadableStream<string> to
  ReadableStream<Uint8Array> (llama.ts, ollama.ts, claude.ts)
- Remove @ts-ignore from for-await loops in test files
- Add explicit string[] type for chunks arrays

* Revert "📝 docs: restore S3_PUBLIC_DOMAIN in deployment guides"

This reverts commit 24073f83d3.
2026-02-07 22:29:14 +08:00

366 lines
14 KiB
Plaintext

---
title: LobeHub Feature Development Complete Guide
description: A comprehensive guide for developers on implementing features in LobeHub.
tags:
- LobeHub
- Feature Development
- Developer Guide
- Postgres
- Drizzle ORM
---
# LobeHub Feature Development Complete Guide
This document aims to guide developers on how to develop a complete feature in LobeHub.
We will use [RFC 021 - Custom Assistant Opening Guidance](https://github.com/lobehub/lobehub/discussions/891) as an example to illustrate the complete implementation process.
## 1. Update Schema
LobeHub uses a PostgreSQL database, with [Drizzle ORM](https://orm.drizzle.team/) to operate the database.
All schemas are located in `packages/database/src/schemas/`. We need to adjust the `agents` table to add two fields corresponding to the configuration items:
```diff
// packages/database/src/schemas/agent.ts
export const agents = pgTable(
'agents',
{
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('agents'))
.notNull(),
avatar: text('avatar'),
backgroundColor: text('background_color'),
plugins: jsonb('plugins').$type<string[]>().default([]),
// ...
tts: jsonb('tts').$type<LobeAgentTTSConfig>(),
+ openingMessage: text('opening_message'),
+ openingQuestions: text('opening_questions').array().default([]),
...timestamps,
},
(t) => ({
// ...
// !: update index here
}),
);
```
Note that sometimes we may also need to update the index, but for this feature, we don't have any related performance bottleneck issues, so we don't need to update the index.
### Database Migration
After adjusting the schema, you need to generate and optimize migration files. See the [Database Migration Guide](https://github.com/lobehub/lobe-chat/blob/main/.agents/skills/drizzle/references/db-migrations.md) for detailed steps.
## 2. Update Data Model
Data models are defined in `packages/types/src/`. We don't directly use the types exported from the Drizzle schema (e.g., `typeof agents.$inferInsert`), but instead define independent data models based on frontend requirements.
Update the `LobeAgentConfig` type in `packages/types/src/agent/index.ts`:
```diff
export interface LobeAgentConfig {
// ...
chatConfig: LobeAgentChatConfig;
/**
* The language model used by the agent
* @default gpt-4o-mini
*/
model: string;
+ /**
+ * Opening message
+ */
+ openingMessage?: string;
+ /**
+ * Opening questions
+ */
+ openingQuestions?: string[];
/**
* Language model parameters
*/
params: LLMParams;
// ...
}
```
## 3. Service / Model Layer Implementation
The project is divided into multiple frontend and backend layers by responsibility:
```plaintext
+-------------------+--------------------------------------+------------------------------------------------------+
| Layer | Location | Responsibility |
+-------------------+--------------------------------------+------------------------------------------------------+
| Client Service | src/services/ | Reusable frontend business logic, often multiple tRPC |
| WebAPI | src/app/(backend)/webapi/ | REST API endpoints |
| tRPC Router | src/server/routers/ | tRPC entry, validates input, routes to service |
| Server Service | src/server/services/ | Server-side business logic, with DB access |
| Server Module | src/server/modules/ | Server-side modules, no direct DB access |
| Repository | packages/database/src/repositories/ | Complex queries, cross-table operations |
| DB Model | packages/database/src/models/ | Single-table CRUD operations |
+-------------------+--------------------------------------+------------------------------------------------------+
```
**Client Service** is frontend code that encapsulates reusable business logic, calling the backend via tRPC client. For example, `src/services/session/index.ts`:
```typescript
export class SessionService {
updateSessionConfig = (id: string, config: PartialDeep<LobeAgentConfig>, signal?: AbortSignal) => {
return lambdaClient.session.updateSessionConfig.mutate({ id, value: config }, { signal });
};
}
```
**tRPC Router** is the backend entry point (`src/server/routers/lambda/`), validates input and calls Server Service for business logic:
```typescript
export const sessionRouter = router({
updateSessionConfig: sessionProcedure
.input(
z.object({
id: z.string(),
value: z.object({}).passthrough().partial(),
}),
)
.mutation(async ({ input, ctx }) => {
const session = await ctx.sessionModel.findByIdOrSlug(input.id);
// ...
const mergedValue = merge(session.agent, input.value);
return ctx.sessionModel.updateConfig(session.agent.id, mergedValue);
}),
});
```
For this feature, `updateSessionConfig` simply merges config without field-level granularity, so none of the layers need modification.
## 4. Frontend Implementation
### Data Flow Store Implementation
LobeHub uses [zustand](https://zustand.docs.pmnd.rs/getting-started/introduction) as the global state management framework. For detailed practices on state management, refer to [State Management Best Practices](/docs/development/state-management/state-management-intro).
There are two stores related to the agent:
- `src/features/AgentSetting/store` serves the local store for agent settings
- `src/store/agent` is used to get the current session agent's store
The latter listens for and updates the current session's agent configuration through the `onConfigChange` in the `AgentSettings` component in `src/features/AgentSetting/AgentSettings.tsx`.
#### Update AgentSetting/store
First, we update the initialState. After reading `src/features/AgentSetting/store/initialState.ts`, we learn that the initial agent configuration is saved in `DEFAULT_AGENT_CONFIG` in `src/const/settings/agent.ts`:
```diff
export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
chatConfig: DEFAULT_AGENT_CHAT_CONFIG,
model: DEFAULT_MODEL,
+ openingQuestions: [],
params: {
frequency_penalty: 0,
presence_penalty: 0,
temperature: 1,
top_p: 1,
},
plugins: [],
provider: DEFAULT_PROVIDER,
systemRole: '',
tts: DEFAUTT_AGENT_TTS_CONFIG,
};
```
Actually, you don't even need to update this since the `openingQuestions` type is already optional. I'm not updating `openingMessage` here.
Because we've added two new fields, to facilitate component access in `src/features/AgentSetting/AgentOpening` and for performance optimization, we add related selectors in `src/features/AgentSetting/store/selectors.ts`:
```diff
+export const DEFAULT_OPENING_QUESTIONS: string[] = [];
export const selectors = {
chatConfig,
+ openingMessage: (s: Store) => s.config.openingMessage,
+ openingQuestions: (s: Store) => s.config.openingQuestions || DEFAULT_OPENING_QUESTIONS,
};
```
We won't add additional actions to update the agent config here, as existing code also directly uses the unified `setAgentConfig`:
```typescript
export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, get) => ({
setAgentConfig: (config) => {
get().dispatchConfig({ config, type: 'update' });
},
});
```
#### Update store/agent
In the display component we use `src/store/agent` to get the current agent configuration. Simply add two selectors:
Update `src/store/agent/slices/chat/selectors/agent.ts`:
```diff
+const openingQuestions = (s: AgentStoreState) =>
+ currentAgentConfig(s).openingQuestions || DEFAULT_OPENING_QUESTIONS;
+const openingMessage = (s: AgentStoreState) => currentAgentConfig(s).openingMessage || '';
export const agentSelectors = {
// ...
isInboxSession,
+ openingMessage,
+ openingQuestions,
};
```
### i18n Handling
LobeHub is an internationalized project using [react-i18next](https://github.com/i18next/react-i18next). Newly added UI text needs to:
1. Add keys to the corresponding namespace file in `src/locales/default/` (default language is English):
```typescript
// src/locales/default/setting.ts
export default {
// ...
'settingOpening.title': 'Opening Settings',
'settingOpening.openingMessage.title': 'Opening Message',
'settingOpening.openingMessage.placeholder': 'Enter a custom opening message...',
'settingOpening.openingQuestions.title': 'Opening Questions',
'settingOpening.openingQuestions.placeholder': 'Enter a guiding question',
'settingOpening.openingQuestions.empty': 'No opening questions yet',
'settingOpening.openingQuestions.repeat': 'Question already exists',
};
```
2. If a new namespace is added, export it in `src/locales/default/index.ts`
3. For dev preview: manually translate the corresponding JSON files in `locales/zh-CN/` and `locales/en-US/`
4. CI will automatically run `pnpm i18n` to generate translations for other languages
Key naming convention uses flat dot notation: `{feature}.{context}.{action|status}`.
### UI Implementation and Action Binding
We're adding a new category of settings. In `src/features/AgentSetting`, various UI components for agent settings are defined. We'll add an `AgentOpening` folder for opening settings components.
Taking the subcomponent `OpeningQuestions.tsx` as an example, here's the key logic (style code omitted):
```typescript
// src/features/AgentSetting/AgentOpening/OpeningQuestions.tsx
'use client';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useStore } from '../store';
import { selectors } from '../store/selectors';
const OpeningQuestions = memo(() => {
const { t } = useTranslation('setting');
const [questionInput, setQuestionInput] = useState('');
// Use selector to access corresponding configuration
const openingQuestions = useStore(selectors.openingQuestions);
// Use action to update configuration
const updateConfig = useStore((s) => s.setAgentConfig);
const setQuestions = useCallback(
(questions: string[]) => {
updateConfig({ openingQuestions: questions });
},
[updateConfig],
);
const addQuestion = useCallback(() => {
if (!questionInput.trim()) return;
setQuestions([...openingQuestions, questionInput.trim()]);
setQuestionInput('');
}, [openingQuestions, questionInput, setQuestions]);
const removeQuestion = useCallback(
(content: string) => {
const newQuestions = [...openingQuestions];
const index = newQuestions.indexOf(content);
newQuestions.splice(index, 1);
setQuestions(newQuestions);
},
[openingQuestions, setQuestions],
);
// Render Input + SortableList, see component library docs for UI details
// ...
});
```
Key points:
- Read store config via `selectors`
- Update config via the `setAgentConfig` action
- Use `useTranslation('setting')` for i18n text
We also need to display the opening configuration set by the user on the chat page. The corresponding component is in `src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx`:
```typescript
const WelcomeMessage = () => {
const { t } = useTranslation('chat');
// Get current opening configuration from store/agent
const openingMessage = useAgentStore(agentSelectors.openingMessage);
const openingQuestions = useAgentStore(agentSelectors.openingQuestions);
const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual);
const message = useMemo(() => {
// Use user-set message if available
if (openingMessage) return openingMessage;
return !!meta.description ? agentSystemRoleMsg : agentMsg;
}, [openingMessage, agentSystemRoleMsg, agentMsg, meta.description]);
return openingQuestions.length > 0 ? (
<Flexbox>
<ChatItem avatar={meta} message={message} placement="left" />
{/* Render guiding questions */}
<OpeningQuestions questions={openingQuestions} />
</Flexbox>
) : (
<ChatItem avatar={meta} message={message} placement="left" />
);
};
```
## 5. Testing
The project uses Vitest for unit testing. See the [Testing Skill Guide](https://github.com/lobehub/lobe-chat/blob/main/.agents/skills/testing/SKILL.md) for details.
**Running tests:**
```bash
# Run specific test file (never run bun run test — full suite is very slow)
bunx vitest run --silent='passed-only' '[file-path]'
# Database package tests
cd packages/database && bunx vitest run --silent='passed-only' '[file]'
```
**Testing suggestions for new features:**
Since our two new configuration fields are both optional, theoretically tests would pass without updates. However, if you modified default config (e.g., added `openingQuestions` to `DEFAULT_AGENT_CONFIG`), some test snapshots may become stale and need updating.
It's recommended to run related tests locally first to see which fail, then update as needed. For example:
```bash
bunx vitest run --silent='passed-only' 'src/store/agent/slices/chat/selectors/agent.test.ts'
```
If you just want to check whether existing tests pass without running locally, you can also check the GitHub Actions test results directly.
**More testing scenario guides:**
- DB Model testing: `.agents/skills/testing/references/db-model-test.md`
- Zustand Store Action testing: `.agents/skills/testing/references/zustand-store-action-test.md`
- Electron IPC testing: `.agents/skills/testing/references/electron-ipc-test.md`
## Summary
The above is the complete implementation process for the LobeHub opening settings feature, covering the full chain from database schema → data model → Service/Model → Store → i18n → UI → testing. Developers can refer to this document for developing related features.