docs/development/basic/feature-development.zh-CN.mdx
本文档旨在指导开发者了解如何在 LobeHub 中开发一块完整的功能需求。
我们将以 RFC 021 - 自定义助手开场引导 为例,阐述完整的实现流程。
LobeHub 使用 PostgreSQL 数据库,项目使用 Drizzle ORM 来操作数据库。
Schemas 统一放在 packages/database/src/schemas/ 下,我们需要调整 agents 表增加两个配置项对应的字段:
// 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
}),
);
需要注意的是,有些时候我们可能还需要更新索引,但对于这个需求我们没有相关的性能瓶颈问题,所以不需要更新索引。
调整完 schema 后需要生成并优化迁移文件,详细步骤请参阅 数据库迁移指南。
数据模型定义在 packages/types/src/ 下,我们并没有直接使用 Drizzle schema 导出的类型(例如 typeof agents.$inferInsert),而是根据前端需求定义了独立的数据模型。
更新 packages/types/src/agent/index.ts 中 LobeAgentConfig 类型:
export interface LobeAgentConfig {
// ...
chatConfig: LobeAgentChatConfig;
/**
* 角色所使用的语言模型
* @default gpt-4o-mini
*/
model: string;
+ /**
+ * 开场白
+ */
+ openingMessage?: string;
+ /**
+ * 开场问题
+ */
+ openingQuestions?: string[];
/**
* 语言模型参数
*/
params: LLMParams;
// ...
}
项目按职责分为前端和后端多层,完整的分层如下:
+-------------------+--------------------------------------+------------------------------------------------------+
| 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:
export class SessionService {
updateSessionConfig = (id: string, config: PartialDeep<LobeAgentConfig>, signal?: AbortSignal) => {
return lambdaClient.session.updateSessionConfig.mutate({ id, value: config }, { signal });
};
}
tRPC Router 是后端入口(src/server/routers/lambda/),校验输入后调用 Server Service 处理业务逻辑:
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,并没有细粒度到具体字段,因此各层都不需要修改。
LobeHub 使用 zustand 作为全局状态管理框架,对于状态管理的详细实践介绍,可以查阅 📘 状态管理最佳实践。
和 agent 相关的 store 有两个:
src/features/AgentSetting/store 服务于 agent 设置的局部 storesrc/store/agent 用于获取当前会话 agent 的 store后者通过 src/features/AgentSetting/AgentSettings.tsx 中 AgentSettings 组件的 onConfigChange 监听并更新当前会话的 agent 配置。
首先我们更新 initialState,阅读 src/features/AgentSetting/store/initialState.ts 后得知初始 agent 配置保存在 src/const/settings/agent.ts 中的 DEFAULT_AGENT_CONFIG:
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:
+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:
export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, get) => ({
setAgentConfig: (config) => {
get().dispatchConfig({ config, type: 'update' });
},
});
在展示组件中我们使用 src/store/agent 获取当前 agent 配置,简单加两个 selectors:
更新 src/store/agent/slices/chat/selectors/agent.ts:
+const openingQuestions = (s: AgentStoreState) =>
+ currentAgentConfig(s).openingQuestions || DEFAULT_OPENING_QUESTIONS;
+const openingMessage = (s: AgentStoreState) => currentAgentConfig(s).openingMessage || '';
export const agentSelectors = {
// ...
isInboxSession,
+ openingMessage,
+ openingQuestions,
};
LobeHub 是国际化项目,使用 react-i18next 作为 i18n 框架。新增的 UI 文案需要:
src/locales/default/ 对应的 namespace 文件中添加 key(默认语言为英文):// 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',
};
src/locales/default/index.ts 中导出locales/zh-CN/ 和 locales/en-US/ 对应的 JSON 文件pnpm i18n 生成其他语言的翻译key 的命名规范为扁平的 dot notation:{feature}.{context}.{action|status}。
我们这次要新增一个类别的设置。在 src/features/AgentSetting 中定义了 agent 的各种设置 UI 组件,增加一个文件夹 AgentOpening 存放开场设置相关的组件。
以子组件 OpeningQuestions.tsx 为例,展示关键逻辑(省略样式代码):
// 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:
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 ? (
<Flexbox>
<ChatItem avatar={meta} message={message} placement="left" />
<OpeningQuestions questions={openingQuestions} />
</Flexbox>
) : (
<ChatItem avatar={meta} message={message} placement="left" />
);
};
项目使用 Vitest 进行单元测试,相关指南详见 测试技能文档。
运行测试:
# 运行指定测试文件(不要运行 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 字段),可能导致一些测试快照不匹配,需要更新。
建议先本地跑下相关测试,看哪些失败了再针对性更新。例如:
bunx vitest run --silent='passed-only' 'src/store/agent/slices/chat/selectors/agent.test.ts'
如果只是想确认现有测试是否通过而不想本地跑,也可以直接查看 GitHub Actions 的测试结果。
更多测试场景指南:
.agents/skills/testing/references/db-model-test.md.agents/skills/testing/references/zustand-store-action-test.md.agents/skills/testing/references/electron-ipc-test.md以上就是 LobeHub 开场设置功能的完整实现流程,涵盖了从数据库 schema → 数据模型 → Service/Model → Store → i18n → UI → 测试的全链路。开发者可以参考本文档进行相关功能的开发。