--- title: LobeHub 功能开发完全指南 description: 了解如何在 LobeHub 中开发完整的功能需求,提升开发效率。 tags: - LobeHub - 功能开发 - 开发指南 - 开场设置 --- # LobeHub 功能开发完全指南 本文档旨在指导开发者了解如何在 LobeHub 中开发一块完整的功能需求。 我们将以 [RFC 021 - 自定义助手开场引导](https://github.com/lobehub/lobehub/discussions/891) 为例,阐述完整的实现流程。 ## 一、更新 Schema LobeHub 使用 PostgreSQL 数据库,项目使用 [Drizzle ORM](https://orm.drizzle.team/) 来操作数据库。 Schemas 统一放在 `packages/database/src/schemas/` 下,我们需要调整 `agents` 表增加两个配置项对应的字段: ```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().default([]), // ... tts: jsonb('tts').$type(), + openingMessage: text('opening_message'), + openingQuestions: text('opening_questions').array().default([]), ...timestamps, }, (t) => ({ // ... // !: update index here }), ); ``` 需要注意的是,有些时候我们可能还需要更新索引,但对于这个需求我们没有相关的性能瓶颈问题,所以不需要更新索引。 ### 数据库迁移 调整完 schema 后需要生成并优化迁移文件,详细步骤请参阅 [数据库迁移指南](https://github.com/lobehub/lobehub/blob/main/.agents/skills/drizzle/references/db-migrations.md)。 ## 二、更新数据模型 数据模型定义在 `packages/types/src/` 下,我们并没有直接使用 Drizzle schema 导出的类型(例如 `typeof agents.$inferInsert`),而是根据前端需求定义了独立的数据模型。 更新 `packages/types/src/agent/index.ts` 中 `LobeAgentConfig` 类型: ```diff export interface LobeAgentConfig { // ... chatConfig: LobeAgentChatConfig; /** * 角色所使用的语言模型 * @default gpt-4o-mini */ model: string; + /** + * 开场白 + */ + openingMessage?: string; + /** + * 开场问题 + */ + openingQuestions?: string[]; /** * 语言模型参数 */ params: LLMParams; // ... } ``` ## 三、Service / Model 各层实现 项目按职责分为前端和后端多层,完整的分层如下: ```plaintext +-------------------+--------------------------------------+------------------------------------------------------+ | Layer | Location | Responsibility | +-------------------+--------------------------------------+------------------------------------------------------+ | Client Service | src/services/ | 封装前端可复用的业务逻辑,一般涉及多个后端请求(tRPC) | | WebAPI | src/app/(backend)/webapi/ | REST API 端点 | | tRPC Router | src/server/routers/ | tRPC 入口,校验输入,路由到 service | | Server Service | src/server/services/ | 服务端业务逻辑,可访问数据库 | | Server Module | src/server/modules/ | 服务端模块,不直接访问数据库 | | Repository | packages/database/src/repositories/ | 封装复杂查询、跨表操作 | | DB Model | packages/database/src/models/ | 封装单表的 CRUD 操作 | +-------------------+--------------------------------------+------------------------------------------------------+ ``` **Client Service** 是前端代码,封装可复用的业务逻辑,通过 tRPC 客户端调用后端。例如 `src/services/session/index.ts`: ```typescript export class SessionService { updateSessionConfig = (id: string, config: PartialDeep, signal?: AbortSignal) => { return lambdaClient.session.updateSessionConfig.mutate({ id, value: config }, { signal }); }; } ``` **tRPC Router** 是后端入口(`src/server/routers/lambda/`),校验输入后调用 Server Service 处理业务逻辑: ```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); }), }); ``` 对于本次需求,`updateSessionConfig` 只是简单 merge config,并没有细粒度到具体字段,因此各层都不需要修改。 ## 四、前端实现 ### 数据流 Store 实现 LobeHub 使用 [zustand](https://zustand.docs.pmnd.rs/getting-started/introduction) 作为全局状态管理框架,对于状态管理的详细实践介绍,可以查阅 [📘 状态管理最佳实践](/zh/docs/development/state-management/state-management-intro)。 和 agent 相关的 store 有两个: - `src/features/AgentSetting/store` 服务于 agent 设置的局部 store - `src/store/agent` 用于获取当前会话 agent 的 store 后者通过 `src/features/AgentSetting/AgentSettings.tsx` 中 `AgentSettings` 组件的 `onConfigChange` 监听并更新当前会话的 agent 配置。 #### 更新 AgentSetting/store 首先我们更新 initialState,阅读 `src/features/AgentSetting/store/initialState.ts` 后得知初始 agent 配置保存在 `src/const/settings/agent.ts` 中的 `DEFAULT_AGENT_CONFIG`: ```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, }; ``` 其实你这里不更新都可以,因为 `openingQuestions` 类型本来就是可选的,`openingMessage` 我这里就不更新了。 因为我们增加了两个新字段,为了方便在 `src/features/AgentSetting/AgentOpening` 文件夹中组件访问和性能优化,我们在 `src/features/AgentSetting/store/selectors.ts` 增加相关的 selectors: ```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, }; ``` 这里我们就不增加额外的 action 用于更新 agent config 了,因为已有的代码也是直接使用统一的 `setAgentConfig`: ```typescript export const store: StateCreator = (set, get) => ({ setAgentConfig: (config) => { get().dispatchConfig({ config, type: 'update' }); }, }); ``` #### 更新 store/agent 在展示组件中我们使用 `src/store/agent` 获取当前 agent 配置,简单加两个 selectors: 更新 `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 处理 LobeHub 是国际化项目,使用 [react-i18next](https://github.com/i18next/react-i18next) 作为 i18n 框架。新增的 UI 文案需要: 1. 在 `src/locales/default/` 对应的 namespace 文件中添加 key(默认语言为英文): ```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. 如果新增了 namespace,需要在 `src/locales/default/index.ts` 中导出 3. 开发预览时手动翻译 `locales/zh-CN/` 和 `locales/en-US/` 对应的 JSON 文件 4. CI 会自动运行 `pnpm i18n` 生成其他语言的翻译 key 的命名规范为扁平的 dot notation:`{feature}.{context}.{action|status}`。 ### UI 实现和 action 绑定 我们这次要新增一个类别的设置。在 `src/features/AgentSetting` 中定义了 agent 的各种设置 UI 组件,增加一个文件夹 `AgentOpening` 存放开场设置相关的组件。 以子组件 `OpeningQuestions.tsx` 为例,展示关键逻辑(省略样式代码): ```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(''); // 使用 selector 访问对应配置 const openingQuestions = useStore(selectors.openingQuestions); // 使用 action 更新配置 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], ); // 渲染 Input + SortableList,具体 UI 参考组件库文档 // ... }); ``` 关键点: - 通过 `selectors` 读取 store 中的配置 - 通过 `setAgentConfig` action 更新配置 - 使用 `useTranslation('setting')` 获取 i18n 文案 同时我们需要将用户设置的开场配置展示出来,这个是在 chat 页面,对应组件在 `src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx`: ```typescript const WelcomeMessage = () => { const { t } = useTranslation('chat'); // 从 store/agent 获取当前开场配置 const openingMessage = useAgentStore(agentSelectors.openingMessage); const openingQuestions = useAgentStore(agentSelectors.openingQuestions); const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual); const message = useMemo(() => { // 用户设置了就用用户设置的 if (openingMessage) return openingMessage; return !!meta.description ? agentSystemRoleMsg : agentMsg; }, [openingMessage, agentSystemRoleMsg, agentMsg, meta.description]); return openingQuestions.length > 0 ? ( {/* 渲染引导性问题 */} ) : ( ); }; ``` ## 五、测试 项目使用 Vitest 进行单元测试,相关指南详见 [测试技能文档](https://github.com/lobehub/lobehub/blob/main/.agents/skills/testing/SKILL.md)。 **运行测试:** ```bash # 运行指定测试文件(不要运行 bun run test,全量测试耗时很长) bunx vitest run --silent='passed-only' '[file-path]' # database 包的测试 cd packages/database && bunx vitest run --silent='passed-only' '[file]' ``` **添加新功能的测试建议:** 由于我们目前两个新的配置字段都是可选的,理论上不更新测试也能跑通。但如果修改了默认配置(如 `DEFAULT_AGENT_CONFIG` 增加了 `openingQuestions` 字段),可能导致一些测试快照不匹配,需要更新。 建议先本地跑下相关测试,看哪些失败了再针对性更新。例如: ```bash bunx vitest run --silent='passed-only' 'src/store/agent/slices/chat/selectors/agent.test.ts' ``` 如果只是想确认现有测试是否通过而不想本地跑,也可以直接查看 GitHub Actions 的测试结果。 **更多测试场景指南:** - DB Model 测试:`.agents/skills/testing/references/db-model-test.md` - Zustand Store Action 测试:`.agents/skills/testing/references/zustand-store-action-test.md` - Electron IPC 测试:`.agents/skills/testing/references/electron-ipc-test.md` ## 总结 以上就是 LobeHub 开场设置功能的完整实现流程,涵盖了从数据库 schema → 数据模型 → Service/Model → Store → i18n → UI → 测试的全链路。开发者可以参考本文档进行相关功能的开发。