@lobehub/chat 1.79.10 → 1.80.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/changelog/v1.json +27 -0
  3. package/docs/development/basic/feature-development.mdx +370 -619
  4. package/docs/development/basic/feature-development.zh-CN.mdx +368 -611
  5. package/docs/development/database-schema.dbml +2 -0
  6. package/locales/ar/setting.json +16 -0
  7. package/locales/bg-BG/setting.json +16 -0
  8. package/locales/de-DE/setting.json +16 -0
  9. package/locales/en-US/setting.json +16 -0
  10. package/locales/es-ES/setting.json +16 -0
  11. package/locales/fa-IR/setting.json +16 -0
  12. package/locales/fr-FR/setting.json +16 -0
  13. package/locales/it-IT/setting.json +16 -0
  14. package/locales/ja-JP/setting.json +16 -0
  15. package/locales/ko-KR/setting.json +16 -0
  16. package/locales/nl-NL/setting.json +16 -0
  17. package/locales/pl-PL/setting.json +16 -0
  18. package/locales/pt-BR/setting.json +16 -0
  19. package/locales/ru-RU/setting.json +16 -0
  20. package/locales/tr-TR/setting.json +16 -0
  21. package/locales/vi-VN/setting.json +16 -0
  22. package/locales/zh-CN/setting.json +16 -0
  23. package/locales/zh-TW/setting.json +16 -0
  24. package/package.json +1 -1
  25. package/scripts/generate-oidc-jwk.mjs +2 -1
  26. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +78 -0
  27. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx +24 -4
  28. package/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/CategoryContent/useCategory.tsx +6 -1
  29. package/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/index.tsx +2 -0
  30. package/src/const/settings/agent.ts +1 -0
  31. package/src/database/_deprecated/schemas/session.ts +2 -0
  32. package/src/database/client/migrations.json +9 -0
  33. package/src/database/migrations/0021_add_agent_opening_settings.sql +2 -0
  34. package/src/database/migrations/meta/0021_snapshot.json +4988 -0
  35. package/src/database/migrations/meta/_journal.json +7 -0
  36. package/src/database/repositories/dataImporter/deprecated/__tests__/fixtures/messages.json +2 -0
  37. package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +19 -0
  38. package/src/database/schemas/agent.ts +3 -0
  39. package/src/features/AgentSetting/AgentOpening/OpeningMessage.tsx +80 -0
  40. package/src/features/AgentSetting/AgentOpening/OpeningQuestions.tsx +144 -0
  41. package/src/features/AgentSetting/AgentOpening/index.tsx +52 -0
  42. package/src/features/AgentSetting/store/selectors.ts +3 -0
  43. package/src/features/ChatInput/ActionBar/Upload/ClientMode.tsx +7 -6
  44. package/src/hooks/useModelSupportFiles.ts +15 -0
  45. package/src/libs/agent-runtime/stepfun/index.ts +7 -1
  46. package/src/libs/agent-runtime/zhipu/index.ts +17 -10
  47. package/src/locales/default/setting.ts +16 -0
  48. package/src/migrations/FromV5ToV6/types/v6.ts +2 -0
  49. package/src/server/routers/lambda/session.ts +8 -1
  50. package/src/services/import/client.test.ts +18 -0
  51. package/src/services/session/server.test.ts +2 -0
  52. package/src/store/agent/slices/chat/selectors/__snapshots__/agent.test.ts.snap +1 -0
  53. package/src/store/agent/slices/chat/selectors/agent.ts +7 -0
  54. package/src/store/aiInfra/slices/aiModel/selectors.ts +7 -0
  55. package/src/store/global/initialState.ts +1 -0
  56. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +2 -0
  57. package/src/types/agent/index.ts +12 -0
@@ -2,705 +2,462 @@
2
2
 
3
3
  本文档旨在指导开发者了解如何在 LobeChat 中开发一块完整的功能需求。
4
4
 
5
- 我们将以 sessionGroup 的实现为示例:[ feat: add session group manager](https://github.com/lobehub/lobe-chat/pull/1055) , 通过以下六个主要部分来阐述完整的实现流程:
5
+ 我们将以 [RFC 021 - 自定义助手开场引导](https://github.com/lobehub/lobe-chat/discussions/891) 为例,阐述完整的实现流程。
6
6
 
7
- 1. [数据模型 / 数据库定义](#一数据模型--数据库定义)
8
- 2. [Service 实现 / Model 实现](#二service-实现--model-实现)
9
- 3. [前端数据流 Store 实现](#三前端数据流-store-实现)
10
- 4. [UI 实现与 action 绑定](#四ui-实现与-action-绑定)
11
- 5. [数据迁移](#五数据迁移)
12
- 6. [数据导入导出](#六数据导入导出)
7
+ ## 一、更新 schema
13
8
 
14
- ## 一、数据模型 / 数据库定义
9
+ lobe-chat 使用 postgres 数据库,浏览器端本地数据库使用 [pglite](https://pglite.dev/)(wasm 版本 postgres)。项目还使用了 [drizzle](https://orm.drizzle.team/) ORM 用来操作数据库。
15
10
 
16
- 为了实现 Session Group 功能,首先需要在数据库层面定义相关的数据模型和索引。
11
+ 相比旧方案浏览器端使用 indexDB 来说,浏览器端和 server 端都使用 postgres 好处在于 model 层代码可以完全复用。
17
12
 
18
- 定义一个新的 sessionGroup 表,分 3 步:
19
-
20
- ### 1. 建立数据模型 schema
21
-
22
- 在 `src/database/schema/sessionGroup.ts` 中定义 `DB_SessionGroup` 的数据模型:
23
-
24
- ```ts
25
- import { z } from 'zod';
26
-
27
- export const DB_SessionGroupSchema = z.object({
28
- name: z.string(),
29
- sort: z.number().optional(),
30
- });
31
-
32
- export type DB_SessionGroup = z.infer<typeof DB_SessionGroupSchema>;
33
- ```
34
-
35
- ### 2. 创建数据库索引
36
-
37
- 由于要新增一个表,所以需要在在数据库 Schema 中,为 `sessionGroup` 表添加索引。
38
-
39
- 在 `src/database/core/schema.ts` 中添加 `dbSchemaV4`:
13
+ schemas 都统一放在 `src/database/schemas`,我们需要调整 `agents` 表增加两个配置项对应的字段:
40
14
 
41
15
  ```diff
42
- // ... 前面的一些实现
43
-
44
- // ************************************** //
45
- // ******* Version 3 - 2023-12-06 ******* //
46
- // ************************************** //
47
- // - Added `plugin` table
48
-
49
- export const dbSchemaV3 = {
50
- ...dbSchemaV2,
51
- plugins:
52
- '&identifier, type, manifest.type, manifest.meta.title, manifest.meta.description, manifest.meta.author, createdAt, updatedAt',
53
- };
54
-
55
- + // ************************************** //
56
- + // ******* Version 4 - 2024-01-21 ******* //
57
- + // ************************************** //
58
- + // - Added `sessionGroup` table
59
-
60
- + export const dbSchemaV4 = {
61
- + ...dbSchemaV3,
62
- + sessionGroups: '&id, name, sort, createdAt, updatedAt',
63
- + sessions: '&id, type, group, pinned, meta.title, meta.description, meta.tags, createdAt, updatedAt',
64
- };
65
- ```
66
-
67
- > \[!Note]
68
- >
69
- > 除了 `sessionGroups` 外,此处也修改了 `sessions` 的定义,原因是存在数据迁移的情况。但由于本节只关注 schema 定义,不展开数据迁移部分实现,详情可见第五节。
70
-
71
- > \[!Important]
72
- >
73
- > 如果你不了解为何此处需要创建索引,以及不了解此处的 schema 的定义语法。你可能需要提前了解下 Dexie.js 相关的基础知识。
74
-
75
- ### 3. 在本地 DB 中加入 sessionGroups 表
76
-
77
- 扩展本地数据库类以包含新的 `sessionGroups` 表:
78
-
79
- ```diff
80
-
81
- import { dbSchemaV1, dbSchemaV2, dbSchemaV3, dbSchemaV4 } from './schemas';
82
-
83
- interface LobeDBSchemaMap {
84
- files: DB_File;
85
- messages: DB_Message;
86
- plugins: DB_Plugin;
87
- + sessionGroups: DB_SessionGroup;
88
- sessions: DB_Session;
89
- topics: DB_Topic;
90
- }
16
+ // src/database/schemas/agent.ts
17
+ export const agents = pgTable(
18
+ 'agents',
19
+ {
20
+ id: text('id')
21
+ .primaryKey()
22
+ .$defaultFn(() => idGenerator('agents'))
23
+ .notNull(),
24
+ avatar: text('avatar'),
25
+ backgroundColor: text('background_color'),
26
+ plugins: jsonb('plugins').$type<string[]>().default([]),
27
+ // ...
28
+ tts: jsonb('tts').$type<LobeAgentTTSConfig>(),
29
+
30
+ + openingMessage: text('opening_message'),
31
+ + openingQuestions: text('opening_questions').array().default([]),
32
+
33
+ ...timestamps,
34
+ },
35
+ (t) => ({
36
+ // ...
37
+ // !: update index here
38
+ }),
39
+ );
91
40
 
92
- // Define a local DB
93
- export class LocalDB extends Dexie {
94
- public files: LobeDBTable<'files'>;
95
- public sessions: LobeDBTable<'sessions'>;
96
- public messages: LobeDBTable<'messages'>;
97
- public topics: LobeDBTable<'topics'>;
98
- public plugins: LobeDBTable<'plugins'>;
99
- + public sessionGroups: LobeDBTable<'sessionGroups'>;
100
-
101
- constructor() {
102
- super(LOBE_CHAT_LOCAL_DB_NAME);
103
- this.version(1).stores(dbSchemaV1);
104
- this.version(2).stores(dbSchemaV2);
105
- this.version(3).stores(dbSchemaV3);
106
- + this.version(4).stores(dbSchemaV4);
107
-
108
- this.files = this.table('files');
109
- this.sessions = this.table('sessions');
110
- this.messages = this.table('messages');
111
- this.topics = this.table('topics');
112
- this.plugins = this.table('plugins');
113
- + this.sessionGroups = this.table('sessionGroups');
114
- }
115
- }
116
41
  ```
117
42
 
118
- 如此一来,你就可以通过在 `Application` -> `Storage` -> `IndexedDB` 中查看到 `LOBE_CHAT_DB` 里的 `sessionGroups` 表了。
43
+ 需要注意的是,有些时候我们可能还需要更新索引,但对于这个需求我们没有相关的性能瓶颈问题,所以不需要更新索引。
119
44
 
120
- ![](https://github.com/lobehub/lobe-chat/assets/28616219/aea50f66-4060-4a32-88c8-b3c672d05be8)
45
+ 调整完 schema 后我们需要运行 `npm run db:generate,` 使用 drizzle-kit 自带的数据库迁移能力生成对应的用于迁移到最新 schema 的 sql 代码。执行后会产生四个文件:
121
46
 
122
- ## 二、Service 实现 / Model 实现
47
+ - src/database/migrations/meta/\_journal.json:保存每次迁移的相关信息
48
+ - src/database/migrations/0021\_add\_agent\_opening\_settings.sql:此次迁移的 sql 命令
49
+ - src/database/client/migrations.json:pglite 使用的此次迁移的 sql 命令
50
+ - src/database/migrations/meta/0021\_snapshot.json:当前最新的完整数据库快照
123
51
 
124
- ### 定义 Model
52
+ 注意脚本默认生成的迁移 sql 文件名不会像 `0021_add_agent_opening_settings.sql` 这样语义清晰,你需要自己手动对它重命名并且更新 `src/database/migrations/meta/_journal.json`。
125
53
 
126
- 在构建 LobeChat 应用时,Model 负责与数据库的交互,它定义了如何读取、插入、更新和删除数据库的数据,定义具体的业务逻辑。
54
+ 以前客户端存储使用 indexDB 数据迁移相对麻烦,现在本地端使用 pglite 之后数据库迁移就是一条命令的事,非常简单快捷,你也可以检查生成的迁移 sql 是否有什么优化空间,手动调整。
127
55
 
128
- `src/database/model/sessionGroup.ts` 中定义 `SessionGroupModel`:
56
+ ## 二、更新数据模型
129
57
 
130
- ```ts
131
- import { BaseModel } from '@/database/client/core';
132
- import { DB_SessionGroup, DB_SessionGroupSchema } from '@/database/client/schemas/sessionGroup';
133
- import { nanoid } from '@/utils/uuid';
58
+ 在 `src/types` 下定义了我们项目中使用到的各种数据模型,我们并没有直接使用 drizzle schema 导出的类型,例如 `export type NewAgent = typeof agents.$inferInsert;`,而是根据前端需求和 db schema 定义中对应字段数据类型定义了对应的数据模型。
134
59
 
135
- class _SessionGroupModel extends BaseModel {
136
- constructor() {
137
- super('sessions', DB_SessionGroupSchema);
138
- }
60
+ 数据模型定义都放在 `src/types` 文件夹下,更新 `src/types/agent/index.ts` 中 `LobeAgentConfig` 类型:
139
61
 
140
- async create(name: string, sort?: number, id = nanoid()) {
141
- return this._add({ name, sort }, id);
142
- }
143
-
144
- // ... 其他 CRUD 方法的实现
145
- }
146
-
147
- export const SessionGroupModel = new _SessionGroupModel();
148
- ```
149
-
150
- ### Service 实现
151
-
152
- 在 LobeChat 中,Service 层主要负责与后端服务进行通信,封装业务逻辑,并提供数据给前端的其他层使用。`SessionService` 是一个专门处理与会话(Session)相关业务逻辑的服务类,它封装了创建会话、查询会话、更新会话等操作。
153
-
154
- 为了保持代码的可维护性和可扩展性,我们将会话分组相关的服务逻辑放在 `SessionService` 中,这样可以使会话领域的业务逻辑保持内聚。当业务需求增加或变化时,我们可以更容易地在这个领域内进行修改和扩展。
155
-
156
- `SessionService` 通过调用 `SessionGroupModel` 的方法来实现对会话分组的管理。 在 `sessionService` 中实现 Session Group 相关的请求逻辑:
62
+ ```diff
63
+ export interface LobeAgentConfig {
64
+ // ...
65
+ chatConfig: LobeAgentChatConfig;
66
+ /**
67
+ * 角色所使用的语言模型
68
+ * @default gpt-4o-mini
69
+ */
70
+ model: string;
157
71
 
158
- ```ts
159
- class SessionService {
160
- // ... 省略 session 业务逻辑
72
+ + /**
73
+ + * 开场白
74
+ + */
75
+ + openingMessage?: string;
76
+ + /**
77
+ + * 开场问题
78
+ + */
79
+ + openingQuestions?: string[];
161
80
 
162
- // ************************************** //
163
- // *********** SessionGroup *********** //
164
- // ************************************** //
81
+ /**
82
+ * 语言模型参数
83
+ */
84
+ params: LLMParams;
85
+ /**
86
+ * 启用的插件
87
+ */
88
+ plugins?: string[];
165
89
 
166
- async createSessionGroup(name: string, sort?: number) {
167
- const item = await SessionGroupModel.create(name, sort);
168
- if (!item) {
169
- throw new Error('session group create Error');
170
- }
90
+ /**
91
+ * 模型供应商
92
+ */
93
+ provider?: string;
171
94
 
172
- return item.id;
173
- }
95
+ /**
96
+ * 系统角色
97
+ */
98
+ systemRole: string;
174
99
 
175
- // ... 其他 SessionGroup 相关的实现
100
+ /**
101
+ * 语音服务
102
+ */
103
+ tts: LobeAgentTTSConfig;
176
104
  }
177
105
  ```
178
106
 
179
- ## 三、前端数据流 Store 实现
180
-
181
- 在 LobeChat 应用中,Store 是用于管理应用前端状态的模块。其中的 Action 是触发状态更新的函数,通常会调用服务层的方法来执行实际的数据处理操作,然后更新 Store 中的状态。我们采用了 `zustand` 作为 Store 模块的底层依赖,对于状态管理的详细实践介绍,可以查阅 [📘 状态管理最佳实践](/zh/docs/development/state-management/state-management-intro)
107
+ ## 三、Service 实现 / Model 实现
182
108
 
183
- ### sessionGroup CRUD
109
+ - `model` 层封装对 DB 的可复用操作
110
+ - `service` 层实现应用业务逻辑
184
111
 
185
- 会话组的 CRUD 操作是管理会话组数据的核心行为。在 `src/store/session/slice/sessionGroup` 中,我们将实现与会话组相关的状态逻辑,包括添加、删除、更新会话组及其排序。
112
+ `src` 目录下都有对应的顶层文件夹。
186
113
 
187
- 以下是 `action.ts` 文件中需要实现的 `SessionGroupAction` 接口方法:
114
+ 我们需要查看是否需要更新其实现,agent 配置在前端被抽象成 session 的配置,在 `src/services/session/server.ts` 中可以看到有个 service 函数 `updateSessionConfig`:
188
115
 
189
- ```ts
190
- export interface SessionGroupAction {
191
- // 增加会话组
192
- addSessionGroup: (name: string) => Promise<string>;
193
- // 删除会话组
194
- removeSessionGroup: (id: string) => Promise<void>;
195
- // 更新会话的会话组 ID
196
- updateSessionGroupId: (sessionId: string, groupId: string) => Promise<void>;
197
- // 更新会话组名称
198
- updateSessionGroupName: (id: string, name: string) => Promise<void>;
199
- // 更新会话组排序
200
- updateSessionGroupSort: (items: SessionGroupItem[]) => Promise<void>;
116
+ ```typescript
117
+ export class ServerService implements ISessionService {
118
+ // ...
119
+ updateSessionConfig: ISessionService['updateSessionConfig'] = (id, config, signal) => {
120
+ return lambdaClient.session.updateSessionConfig.mutate({ id, value: config }, { signal });
121
+ };
201
122
  }
202
123
  ```
203
124
 
204
- `addSessionGroup` 方法为例,我们首先调用 `sessionService` `createSessionGroup` 方法来创建新的会话组,然后使用 `refreshSessions` 方法来刷新 sessions 状态:
205
-
206
- ```ts
207
- export const createSessionGroupSlice: StateCreator<
208
- SessionStore,
209
- [['zustand/devtools', never]],
210
- [],
211
- SessionGroupAction
212
- > = (set, get) => ({
213
- // 实现添加会话组的逻辑
214
- addSessionGroup: async (name) => {
215
- // 调用服务层的 createSessionGroup 方法并传入会话组名称
216
- const id = await sessionService.createSessionGroup(name);
217
- // 调用 get 方法获取当前的 Store 状态并执行 refreshSessions 方法刷新会话数据
218
- await get().refreshSessions();
219
- // 返回新创建的会话组 ID
220
- return id;
221
- },
222
- // ... 其他 action 实现
223
- });
224
- ```
225
-
226
- 通过以上的实现,我们可以确保在添加新的会话组后,应用的状态会及时更新,且相关的组件会收到最新的状态并重新渲染。这种方式提高了数据流的可预测性和可维护性,同时也简化了组件之间的通信。
227
-
228
- ### Sessions 分组逻辑改造
229
-
230
- 本次需求改造需要对 Sessions 进行升级,从原来的单一列表变成了三个不同的分组:`pinnedSessions`(置顶列表)、`customSessionGroups`(自定义分组)和 `defaultSessions`(默认列表)。
231
-
232
- 为了处理这些分组,我们需要改造 `useFetchSessions` 的实现逻辑。以下是关键的改动点:
233
-
234
- 1. 使用 `sessionService.getGroupedSessions` 方法负责调用后端接口来获取分组后的会话数据;
235
- 2. 将获取后的数据保存为三到不同的状态字段中:`pinnedSessions`、`customSessionGroups` 和 `defaultSessions`;
236
-
237
- #### `useFetchSessions` 方法
238
-
239
- 该方法在 `createSessionSlice` 中定义,如下所示:
240
-
241
- ```ts
242
- export const createSessionSlice: StateCreator<
243
- SessionStore,
244
- [['zustand/devtools', never]],
245
- [],
246
- SessionAction
247
- > = (set, get) => ({
248
- // ... 其他方法
249
- useFetchSessions: () =>
250
- useSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getGroupedSessions, {
251
- onSuccess: (data) => {
252
- set(
253
- {
254
- customSessionGroups: data.customGroup,
255
- defaultSessions: data.default,
256
- isSessionsFirstFetchFinished: true,
257
- pinnedSessions: data.pinned,
258
- sessions: data.all,
259
- },
260
- false,
261
- n('useFetchSessions/onSuccess', data),
262
- );
263
- },
125
+ 跳转 `lambdaClient.session.updateSessionConfig` 实现,发现它只是简单的 **merge** 了新的 config 和旧的 config。
126
+
127
+ ```typescript
128
+ export const sessionRouter = router({
129
+ // ..
130
+ updateSessionConfig: sessionProcedure
131
+ .input(
132
+ z.object({
133
+ id: z.string(),
134
+ value: z.object({}).passthrough().partial(),
135
+ }),
136
+ )
137
+ .mutation(async ({ input, ctx }) => {
138
+ const session = await ctx.sessionModel.findByIdOrSlug(input.id);
139
+ // ...
140
+ const mergedValue = merge(session.agent, input.value);
141
+ return ctx.sessionModel.updateConfig(session.agent.id, mergedValue);
264
142
  }),
265
143
  });
266
144
  ```
267
145
 
268
- 在成功获取数据后,我们使用 `set` 方法来更新 `customSessionGroups`、`defaultSessions`、`pinnedSessions` `sessions` 状态。这将保证状态与最新的会话数据同步。
269
-
270
- #### getGroupedSessions
271
-
272
- 使用 `sessionService.getGroupedSessions` 方法负责调用后端接口 `SessionModel.queryWithGroups()`
273
-
274
- ```ts
275
- class SessionService {
276
- // ... 其他 SessionGroup 相关的实现
277
-
278
- async getGroupedSessions(): Promise<ChatSessionList> {
279
- return SessionModel.queryWithGroups();
280
- }
281
- }
282
- ```
283
-
284
- #### `SessionModel.queryWithGroups` 方法
146
+ 可以预想的到,我们前端会增加两个输入,用户修改的时候去调用这个 `updateSessionConfig`,而目前的时候都没细粒度到 config 中的具体字段,因此,service 层和 model 层不需要修改。
285
147
 
286
- 此方法是 `sessionService.getGroupedSessions` 调用的核心方法,它负责查询和组织会话数据,代码如下:
148
+ ## 四、前端实现
287
149
 
288
- ```ts
289
- class _SessionModel extends BaseModel {
290
- // ... 其他方法
150
+ ### 数据流 store 实现
291
151
 
292
- /**
293
- * 查询会话数据,并根据会话组将会话分类。
294
- * @returns {Promise<ChatSessionList>} 返回一个对象,其中包含所有会话以及分为不同组的会话列表。
295
- */
296
- async queryWithGroups(): Promise<ChatSessionList> {
297
- // 查询会话组数据
298
- const groups = await SessionGroupModel.query();
299
- // 根据会话组ID查询自定义会话组
300
- const customGroups = await this.queryByGroupIds(groups.map((item) => item.id));
301
- // 查询默认会话列表
302
- const defaultItems = await this.querySessionsByGroupId(SessionDefaultGroup.Default);
303
- // 查询置顶的会话
304
- const pinnedItems = await this.getPinnedSessions();
305
-
306
- // 查询所有会话
307
- const all = await this.query();
308
- // 组合并返回所有会话及其分组信息
309
- return {
310
- all, // 包含所有会话的数组
311
- customGroup: groups.map((group) => ({ ...group, children: customGroups[group.id] })), // 自定义分组
312
- default: defaultItems, // 默认会话列表
313
- pinned: pinnedItems, // 置顶会话列表
314
- };
315
- }
316
- }
317
- ```
152
+ lobe-chat 使用 [zustand](https://zustand.docs.pmnd.rs/getting-started/introduction) 作为全局状态管理框架,对于状态管理的详细实践介绍,可以查阅 [📘 状态管理最佳实践](/zh/docs/development/state-management/state-management-intro)。
318
153
 
319
- 方法 `queryWithGroups` 首先查询所有会话组,然后基于这些组的 ID 查询自定义会话组,同时查询默认和固定的会话。最后,它返回一个包含所有会话和按组分类的会话列表对象。
154
+ agent 相关的 store 有两个:
320
155
 
321
- ### sessions selectors 调整
156
+ - `src/features/AgentSetting/store` 服务于 agent 设置的局部 store
157
+ - `src/store/agent` 用于获取当前会话 agent 的 store
322
158
 
323
- 由于 sessions 中关于分组的逻辑发生了变化,因此我们需要调整 `sessions` selectors 逻辑,以确保它们能够正确地处理新的数据结构。
159
+ 后者通过 `src/features/AgentSetting/AgentSettings.tsx` `AgentSettings` 组件的 `onConfigChange` 监听并更新当前会话的 agent 配置。
324
160
 
325
- 原有的 selectors:
326
-
327
- ```ts
328
- // 默认分组
329
- const defaultSessions = (s: SessionStore): LobeSessions => s.sessions;
330
-
331
- // 置顶分组
332
- const pinnedSessionList = (s: SessionStore) =>
333
- defaultSessions(s).filter((s) => s.group === SessionGroupDefaultKeys.Pinned);
334
-
335
- // 未置顶分组
336
- const unpinnedSessionList = (s: SessionStore) =>
337
- defaultSessions(s).filter((s) => s.group === SessionGroupDefaultKeys.Default);
338
- ```
161
+ #### 更新 AgentSetting/store
339
162
 
340
- 修改后:
163
+ 首先我们更新 initialState,阅读 `src/features/AgentSetting/store/initialState.ts` 后得知初始 agent 配置保存在 `src/const/settings/agent.ts` 中的 `DEFAULT_AGENT_CONFIG`:
341
164
 
342
- ```ts
343
- const defaultSessions = (s: SessionStore): LobeSessions => s.defaultSessions;
344
- const pinnedSessions = (s: SessionStore): LobeSessions => s.pinnedSessions;
345
- const customSessionGroups = (s: SessionStore): CustomSessionGroup[] => s.customSessionGroups;
165
+ ```diff
166
+ export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
167
+ chatConfig: DEFAULT_AGENT_CHAT_CONFIG,
168
+ model: DEFAULT_MODEL,
169
+ + openingQuestions: [],
170
+ params: {
171
+ frequency_penalty: 0,
172
+ presence_penalty: 0,
173
+ temperature: 1,
174
+ top_p: 1,
175
+ },
176
+ plugins: [],
177
+ provider: DEFAULT_PROVIDER,
178
+ systemRole: '',
179
+ tts: DEFAUTT_AGENT_TTS_CONFIG,
180
+ };
346
181
  ```
347
182
 
348
- 由于在 UI 中的取数全部是通过 `useSessionStore(sessionSelectors.defaultSessions)` 这样的写法实现的,因此我们只需要修改 `defaultSessions` 的选择器实现,即可完成数据结构的变更。 UI 层的取数代码完全不用变更,可以大大降低重构的成本和风险。
183
+ 其实你这里不更新都可以,因为 `openingQuestions` 类型本来就是可选的,`openingMessage` 我这里就不更新了。
349
184
 
350
- > !\[Important]
351
- > 如果你对 Selectors 的概念和功能不太了解,可以查阅 [📘 数据存储取数模块](/zh/docs/development/state-management/state-management-selectors) 部分了解相关内容。
185
+ 因为我们增加了两个新字段,为了方便在 `src/features/AgentSetting/AgentOpening` 文件夹中组件访问和性能优化,我们在 `src/features/AgentSetting/store/selectors.ts` 增加相关的 selectors:
352
186
 
353
- ## 四、UI 实现与 action 绑定
354
-
355
- UI 组件中绑定 Store Action 实现交互逻辑,例如 `CreateGroupModal`:
187
+ ```diff
188
+ import { DEFAULT_AGENT_CHAT_CONFIG } from '@/const/settings';
189
+ import { LobeAgentChatConfig } from '@/types/agent';
356
190
 
357
- ```tsx
358
- const CreateGroupModal = () => {
359
- // ... 其他逻辑
191
+ import { Store } from './action';
360
192
 
361
- const [updateSessionGroup, addCustomGroup] = useSessionStore((s) => [
362
- s.updateSessionGroupId,
363
- s.addSessionGroup,
364
- ]);
193
+ const chatConfig = (s: Store): LobeAgentChatConfig =>
194
+ s.config.chatConfig || DEFAULT_AGENT_CHAT_CONFIG;
365
195
 
366
- return (
367
- <Modal
368
- onOk={async () => {
369
- // ... 其他逻辑
370
- const groupId = await addCustomGroup(name);
371
- await updateSessionGroup(sessionId, groupId);
372
- }}
373
- >
374
- {/* ... */}
375
- </Modal>
376
- );
196
+ +export const DEFAULT_OPENING_QUESTIONS: string[] = [];
197
+ export const selectors = {
198
+ chatConfig,
199
+ + openingMessage: (s: Store) => s.config.openingMessage,
200
+ + openingQuestions: (s: Store) => s.config.openingQuestions || DEFAULT_OPENING_QUESTIONS,
377
201
  };
378
202
  ```
379
203
 
380
- ## 五、数据迁移
381
-
382
- 在软件开发过程中,数据迁移是一个不可避免的问题,尤其是当现有的数据结构无法满足新的业务需求时。对于本次 SessionGroup 的迭代,我们需要处理 `session` 的 `group` 字段的迁移,这是一个典型的数据迁移案例。
383
-
384
- ### 旧数据结构的问题
385
-
386
- 在旧的数据结构中,`group` 字段被用来标记会话是否为 `pinned`(置顶)或属于某个 `default`(默认)分组。但是当需要支持多个会话分组时,原有的数据结构就显得不够灵活了。
204
+ 这里我们就不增加额外的 action 用于更新 agent config 了,因为我观察到已有的其它代码也是直接使用现有代码中统一的 `setChatConfig`:
387
205
 
388
- 例如:
389
-
390
- ```
391
- before pin: group = abc
392
- after pin: group = pinned
393
- after unpin: group = default
206
+ ```typescript
207
+ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, get) => ({
208
+ setAgentConfig: (config) => {
209
+ get().dispatchConfig({ config, type: 'update' });
210
+ },
211
+ });
394
212
  ```
395
213
 
396
- 从上述示例中可以看出,一旦会话从置顶状态(`pinned`)取消置顶(`unpin`),`group` 字段将无法恢复为原来的 `abc` 值。这是因为我们没有一个独立的字段来维护置顶状态。因此,我们引入了一个新的字段 `pinned` 来表示会话是否被置顶,而 `group` 字段将仅用于标识会话分组。
397
-
398
- ### 迁移策略
214
+ #### 更新 store/agent
399
215
 
400
- 本次迁移的核心逻辑只有一条:
216
+ 在组件 `src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx` 我们使用了这个 store 用于获取当前 agent 配置,用来渲染用户自定义的开场消息和引导性问题。
401
217
 
402
- - 当用户的 `group` 字段为 `pinned` 时,将其 `pinned` 字段置为 `true`,同时将 group 设为 `default`;
218
+ 这里因为只需要读取两个配置项,我们就简单的加两个 selectors 就好了:
403
219
 
404
- LobeChat 中的数据迁移通常涉及到 **配置文件迁移** 和 **数据库迁移** 两个部分。所以上述逻辑会需要分别在两块实现迁移。
405
-
406
- #### 配置文件迁移
407
-
408
- 对于配置文件迁移,我们建议先于数据库迁移进行,因为配置文件迁移通常更容易进行测试和验证。LobeChat 的文件迁移配置位于 `src/migrations/index.ts` 文件中,其中定义了配置文件迁移的各个版本及对应的迁移脚本。
220
+ 更新 `src/store/agent/slices/chat/selectors/agent.ts`:
409
221
 
410
222
  ```diff
411
- // 当前最新的版本号
412
- - export const CURRENT_CONFIG_VERSION = 2;
413
- + export const CURRENT_CONFIG_VERSION = 3;
414
-
415
- // 历史记录版本升级模块
416
- const ConfigMigrations = [
417
- + /**
418
- + * 2024.01.22
419
- + * from `group = pinned` to `pinned:true`
420
- + */
421
- + MigrationV2ToV3,
422
- /**
423
- * 2023.11.27
424
- * 从单 key 数据库转换为基于 dexie 的关系型结构
425
- */
426
- MigrationV1ToV2,
427
- /**
428
- * 2023.07.11
429
- * just the first version, Nothing to do
430
- */
431
- MigrationV0ToV1,
432
- ];
223
+ // ...
224
+ +const openingQuestions = (s: AgentStoreState) =>
225
+ + currentAgentConfig(s).openingQuestions || DEFAULT_OPENING_QUESTIONS;
226
+ +const openingMessage = (s: AgentStoreState) => currentAgentConfig(s).openingMessage || '';
227
+
228
+ export const agentSelectors = {
229
+ // ...
230
+ isInboxSession,
231
+ + openingMessage,
232
+ + openingQuestions,
233
+ };
433
234
  ```
434
235
 
435
- 本次的配置文件迁移逻辑定义在 `src/migrations/FromV2ToV3/index.ts` 中,简化如下:
436
-
437
- ```ts
438
- export class MigrationV2ToV3 implements Migration {
439
- // 指定从该版本开始向上升级
440
- version = 2;
441
-
442
- migrate(data: MigrationData<V2ConfigState>): MigrationData<V3ConfigState> {
443
- const { sessions } = data.state;
444
-
445
- return {
446
- ...data,
447
- state: {
448
- ...data.state,
449
- sessions: sessions.map((s) => this.migrateSession(s)),
450
- },
451
- };
452
- }
453
-
454
- migrateSession = (session: V2Session): V3Session => {
455
- return {
456
- ...session,
457
- group: 'default',
458
- pinned: session.group === 'pinned',
459
- };
460
- };
236
+ ### UI 实现和 action 绑定
237
+
238
+ 我们这次要新增一个类别的设置, 在 `src/features/AgentSetting` 中定义了 agent 的各种设置的 UI 组件,这次我们要增加一个设置类型:开场设置。增加一个文件夹 `AgentOpening` 存放开场设置相关的组件。项目使用了:
239
+
240
+ - [ant-design](https://ant.design/) 和 [lobe-ui:](https://github.com/lobehub/lobe-ui)组件库
241
+ - [antd-style](https://ant-design.github.io/antd-style) : css-in-js 方案
242
+ - [react-layout-kit](https://github.com/arvinxx/react-layout-kit):响应式布局组件
243
+ - [@ant-design/icons](https://ant.design/components/icon-cn) 和 [lucide](https://lucide.dev/icons/): 图标库
244
+ - [react-i18next](https://github.com/i18next/react-i18next) [lobe-i18n](https://github.com/lobehub/lobe-cli-toolbox/tree/master/packages/lobe-i18n) :i18n 框架和多语言自动翻译工具
245
+
246
+ lobe-chat 是个国际化项目,新加的文案需要更新默认的 `locale` 文件: `src/locales/default/setting.ts` 。
247
+
248
+ 我们以子组件 `OpeningQuestion.tsx` 为例,组件实现:
249
+
250
+ ```typescript
251
+ // src/features/AgentSetting/AgentOpening/OpeningQuestions.tsx
252
+ 'use client';
253
+
254
+ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
255
+ import { SortableList } from '@lobehub/ui';
256
+ import { Button, Empty, Input } from 'antd';
257
+ import { createStyles } from 'antd-style';
258
+ import { memo, useCallback, useMemo, useState } from 'react';
259
+ import { useTranslation } from 'react-i18next';
260
+ import { Flexbox } from 'react-layout-kit';
261
+
262
+ import { useStore } from '../store';
263
+ import { selectors } from '../store/selectors';
264
+
265
+ const useStyles = createStyles(({ css, token }) => ({
266
+ empty: css`
267
+ margin-block: 24px;
268
+ margin-inline: 0;
269
+ `,
270
+ questionItemContainer: css`
271
+ margin-block-end: 8px;
272
+ padding-block: 2px;
273
+ padding-inline: 10px 0;
274
+ background: ${token.colorBgContainer};
275
+ `,
276
+ questionItemContent: css`
277
+ flex: 1;
278
+ `,
279
+ questionsList: css`
280
+ width: 100%;
281
+ margin-block-start: 16px;
282
+ `,
283
+ repeatError: css`
284
+ margin: 0;
285
+ color: ${token.colorErrorText};
286
+ `,
287
+ }));
288
+
289
+ interface QuestionItem {
290
+ content: string;
291
+ id: number;
461
292
  }
462
- ```
463
-
464
- 可以看到迁移的实现非常简单。但重要的是,我们需要保证迁移的正确性,因此需要编写对应的测试用例 `src/migrations/FromV2ToV3/migrations.test.ts`:
465
-
466
- ```ts
467
- import { MigrationData, VersionController } from '@/migrations/VersionController';
468
-
469
- import { MigrationV1ToV2 } from '../FromV1ToV2';
470
- import inputV1Data from '../FromV1ToV2/fixtures/input-v1-session.json';
471
- import inputV2Data from './fixtures/input-v2-session.json';
472
- import outputV3DataFromV1 from './fixtures/output-v3-from-v1.json';
473
- import outputV3Data from './fixtures/output-v3.json';
474
- import { MigrationV2ToV3 } from './index';
475
-
476
- describe('MigrationV2ToV3', () => {
477
- let migrations;
478
- let versionController: VersionController<any>;
479
293
 
480
- beforeEach(() => {
481
- migrations = [MigrationV2ToV3];
482
- versionController = new VersionController(migrations, 3);
483
- });
484
-
485
- it('should migrate data correctly through multiple versions', () => {
486
- const data: MigrationData = inputV2Data;
487
-
488
- const migratedData = versionController.migrate(data);
294
+ const OpeningQuestions = memo(() => {
295
+ const { t } = useTranslation('setting');
296
+ const { styles } = useStyles();
297
+ const [questionInput, setQuestionInput] = useState('');
298
+
299
+ // 使用 selector 访问对应配置
300
+ const openingQuestions = useStore(selectors.openingQuestions);
301
+ // 使用 action 更新配置
302
+ const updateConfig = useStore((s) => s.setAgentConfig);
303
+ const setQuestions = useCallback(
304
+ (questions: string[]) => {
305
+ updateConfig({ openingQuestions: questions });
306
+ },
307
+ [updateConfig],
308
+ );
489
309
 
490
- expect(migratedData.version).toEqual(outputV3Data.version);
491
- expect(migratedData.state.sessions).toEqual(outputV3Data.state.sessions);
492
- expect(migratedData.state.topics).toEqual(outputV3Data.state.topics);
493
- expect(migratedData.state.messages).toEqual(outputV3Data.state.messages);
494
- });
310
+ const addQuestion = useCallback(() => {
311
+ if (!questionInput.trim()) return;
312
+
313
+ setQuestions([...openingQuestions, questionInput.trim()]);
314
+ setQuestionInput('');
315
+ }, [openingQuestions, questionInput, setQuestions]);
316
+
317
+ const removeQuestion = useCallback(
318
+ (content: string) => {
319
+ const newQuestions = [...openingQuestions];
320
+ const index = newQuestions.indexOf(content);
321
+ newQuestions.splice(index, 1);
322
+ setQuestions(newQuestions);
323
+ },
324
+ [openingQuestions, setQuestions],
325
+ );
495
326
 
496
- it('should work correct from v1 to v3', () => {
497
- const data: MigrationData = inputV1Data;
327
+ // 处理拖拽排序后的逻辑
328
+ const handleSortEnd = useCallback(
329
+ (items: QuestionItem[]) => {
330
+ setQuestions(items.map((item) => item.content));
331
+ },
332
+ [setQuestions],
333
+ );
498
334
 
499
- versionController = new VersionController([MigrationV2ToV3, MigrationV1ToV2], 3);
335
+ const items: QuestionItem[] = useMemo(() => {
336
+ return openingQuestions.map((item, index) => ({
337
+ content: item,
338
+ id: index,
339
+ }));
340
+ }, [openingQuestions]);
500
341
 
501
- const migratedData = versionController.migrate(data);
342
+ const isRepeat = openingQuestions.includes(questionInput.trim());
502
343
 
503
- expect(migratedData.version).toEqual(outputV3DataFromV1.version);
504
- expect(migratedData.state.sessions).toEqual(outputV3DataFromV1.state.sessions);
505
- expect(migratedData.state.topics).toEqual(outputV3DataFromV1.state.topics);
506
- expect(migratedData.state.messages).toEqual(outputV3DataFromV1.state.messages);
507
- });
344
+ return (
345
+ <Flexbox gap={8}>
346
+ <Flexbox gap={4}>
347
+ <Input
348
+ addonAfter={
349
+ <Button
350
+ // don't allow repeat
351
+ disabled={openingQuestions.includes(questionInput.trim())}
352
+ icon={<PlusOutlined />}
353
+ onClick={addQuestion}
354
+ size="small"
355
+ type="text"
356
+ />
357
+ }
358
+ onChange={(e) => setQuestionInput(e.target.value)}
359
+ onPressEnter={addQuestion}
360
+ placeholder={t('settingOpening.openingQuestions.placeholder')}
361
+ value={questionInput}
362
+ />
363
+
364
+ {isRepeat && (
365
+ <p className={styles.repeatError}>{t('settingOpening.openingQuestions.repeat')}</p>
366
+ )}
367
+ </Flexbox>
368
+
369
+ <div className={styles.questionsList}>
370
+ {openingQuestions.length > 0 ? (
371
+ <SortableList
372
+ items={items}
373
+ onChange={handleSortEnd}
374
+ renderItem={(item) => (
375
+ <SortableList.Item className={styles.questionItemContainer} id={item.id}>
376
+ <SortableList.DragHandle />
377
+ <div className={styles.questionItemContent}>{item.content}</div>
378
+ <Button
379
+ icon={<DeleteOutlined />}
380
+ onClick={() => removeQuestion(item.content)}
381
+ type="text"
382
+ />
383
+ </SortableList.Item>
384
+ )}
385
+ />
386
+ ) : (
387
+ <Empty
388
+ className={styles.empty}
389
+ description={t('settingOpening.openingQuestions.empty')}
390
+ />
391
+ )}
392
+ </div>
393
+ </Flexbox>
394
+ );
508
395
  });
509
- ```
510
-
511
- 单测需要使用 `fixtures` 来固定测试数据,测试用例包含了两个部分的验证逻辑: 1) 单次迁移(v2 -> v3)和 2) 完整迁移(v1 -> v3)的正确性。
512
-
513
- > \[!Important]
514
- >
515
- > 配置文件的版本号可能与数据库版本号不一致,因为数据库版本的更新不总是伴随数据结构的变化(如新增表或字段),而配置文件的版本更新则通常涉及到数据迁移。
516
-
517
- #### 数据库迁移
518
396
 
519
- 数据库迁移则需要在 `LocalDB` 类中实施,该类定义在 `src/database/core/db.ts` 文件中。迁移过程涉及到为 `sessions` 表的每条记录添加新的 `pinned` 字段,并重置 `group` 字段:
520
-
521
- ```diff
522
- export class LocalDB extends Dexie {
523
- public files: LobeDBTable<'files'>;
524
- public sessions: LobeDBTable<'sessions'>;
525
- public messages: LobeDBTable<'messages'>;
526
- public topics: LobeDBTable<'topics'>;
527
- public plugins: LobeDBTable<'plugins'>;
528
- public sessionGroups: LobeDBTable<'sessionGroups'>;
529
-
530
- constructor() {
531
- super(LOBE_CHAT_LOCAL_DB_NAME);
532
- this.version(1).stores(dbSchemaV1);
533
- this.version(2).stores(dbSchemaV2);
534
- this.version(3).stores(dbSchemaV3);
535
- this.version(4)
536
- .stores(dbSchemaV4)
537
- + .upgrade((trans) => this.upgradeToV4(trans));
538
-
539
- this.files = this.table('files');
540
- this.sessions = this.table('sessions');
541
- this.messages = this.table('messages');
542
- this.topics = this.table('topics');
543
- this.plugins = this.table('plugins');
544
- this.sessionGroups = this.table('sessionGroups');
545
- }
546
-
547
- + /**
548
- + * 2024.01.22
549
- + *
550
- + * DB V3 to V4
551
- + * from `group = pinned` to `pinned:true`
552
- + */
553
- + upgradeToV4 = async (trans: Transaction) => {
554
- + const sessions = trans.table('sessions');
555
- + await sessions.toCollection().modify((session) => {
556
- + // translate boolean to number
557
- + session.pinned = session.group === 'pinned' ? 1 : 0;
558
- + session.group = 'default';
559
- + });
560
- + };
561
- }
397
+ export default OpeningQuestions;
562
398
  ```
563
399
 
564
- 以上就是我们的数据迁移策略。在进行迁移时,务必确保迁移脚本的正确性,并通过充分的测试验证迁移结果。
565
-
566
- ## 六、数据导入导出
567
-
568
- 在 LobeChat 中,数据导入导出功能是为了确保用户可以在不同设备之间迁移他们的数据。这包括会话、话题、消息和设置等数据。在本次的 Session Group 功能实现中,我们也需要对数据导入导出进行处理,以确保当完整导出的数据在其他设备上可以一模一样恢复。
569
-
570
- 数据导入导出的核心实现在 `src/service/config.ts` 的 `ConfigService` 中,其中的关键方法如下:
571
-
572
- | 方法名称 | 描述 |
573
- | --------------------- | -------- |
574
- | `importConfigState` | 导入配置数据 |
575
- | `exportAgents` | 导出所有助理数据 |
576
- | `exportSessions` | 导出所有会话数据 |
577
- | `exportSingleSession` | 导出单个会话数据 |
578
- | `exportSingleAgent` | 导出单个助理数据 |
579
- | `exportSettings` | 导出设置数据 |
580
- | `exportAll` | 导出所有数据 |
400
+ 同时我们需要将用户设置的开场配置展示出来,这个是在 chat 页面,对应组件在 `src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx`:
581
401
 
582
- ### 数据导出
402
+ ```typescript
403
+ const WelcomeMessage = () => {
404
+ const { t } = useTranslation('chat');
583
405
 
584
- LobeChat 中,当用户选择导出数据时,会将当前的会话、话题、消息和设置等数据打包成一个 JSON 文件并提供给用户下载。这个 JSON 文件的标准结构如下:
585
-
586
- ```json
587
- {
588
- "exportType": "sessions",
589
- "state": {
590
- "sessions": [],
591
- "topics": [],
592
- "messages": []
593
- },
594
- "version": 3
595
- }
596
- ```
406
+ // 获取当前开场配置
407
+ const openingMessage = useAgentStore(agentSelectors.openingMessage);
408
+ const openingQuestions = useAgentStore(agentSelectors.openingQuestions);
597
409
 
598
- 其中:
410
+ const meta = useSessionStore(sessionMetaSelectors.currentAgentMeta, isEqual);
411
+ const { isAgentEditable } = useServerConfigStore(featureFlagsSelectors);
412
+ const activeId = useChatStore((s) => s.activeId);
599
413
 
600
- - `exportType`: 标识导出数据的类型,目前有 `sessions`、 `agent` 、 `settings` 和 `all` 四种;
601
- - `state`: 存储实际的数据,不同 `exportType` 的数据类型也不同;
602
- - `version`: 标识数据的版本。
603
-
604
- 在 Session Group 功能实现中,我们需要在 `state` 字段中添加 `sessionGroups` 数据。这样,当用户导出数据时,他们的 Session Group 数据也会被包含在内。
605
-
606
- 以导出 sessions 为例,导出数据的相关实现代码修改如下:
607
-
608
- ```diff
609
- class ConfigService {
610
- // ... 省略其他
414
+ const agentSystemRoleMsg = t('agentDefaultMessageWithSystemRole', {
415
+ name: meta.title || t('defaultAgent'),
416
+ systemRole: meta.description,
417
+ });
611
418
 
612
- exportSessions = async () => {
613
- const sessions = await sessionService.getAllSessions();
614
- + const sessionGroups = await sessionService.getSessionGroups();
615
- const messages = await messageService.getAllMessages();
616
- const topics = await topicService.getAllTopics();
419
+ const agentMsg = t(isAgentEditable ? 'agentDefaultMessage' : 'agentDefaultMessageWithoutEdit', {
420
+ name: meta.title || t('defaultAgent'),
421
+ url: `/chat/settings?session=${activeId}`,
422
+ });
617
423
 
618
- - const config = createConfigFile('sessions', { messages, sessions, topics });
619
- + const config = createConfigFile('sessions', { messages, sessionGroups, sessions, topics });
424
+ const message = useMemo(() => {
425
+ // 用户设置了就用用户设置的
426
+ if (openingMessage) return openingMessage;
427
+ return !!meta.description ? agentSystemRoleMsg : agentMsg;
428
+ }, [openingMessage, agentSystemRoleMsg, agentMsg, meta.description]);
429
+
430
+ const chatItem = (
431
+ <ChatItem
432
+ avatar={meta}
433
+ editing={false}
434
+ message={message}
435
+ placement={'left'}
436
+ type={type === 'chat' ? 'block' : 'pure'}
437
+ />
438
+ );
620
439
 
621
- exportConfigFile(config, 'sessions');
622
- };
623
- }
440
+ return openingQuestions.length > 0 ? (
441
+ <Flexbox>
442
+ {chatItem}
443
+ {/* 渲染引导性问题 */}
444
+ <OpeningQuestions mobile={mobile} questions={openingQuestions} />
445
+ </Flexbox>
446
+ ) : (
447
+ chatItem
448
+ );
449
+ };
450
+ export default WelcomeMessage;
624
451
  ```
625
452
 
626
- ### 数据导入
627
-
628
- 数据导入的功能是通过 `ConfigService.importConfigState` 来实现的。当用户选择导入数据时,他们需要提供一个由 符合上述结构规范的 JSON 文件。`importConfigState` 方法接受配置文件的数据,并将其导入到应用中。
629
-
630
- 在 Session Group 功能实现中,我们需要在导入数据的过程中处理 `sessionGroups` 数据。这样,当用户导入数据时,他们的 Session Group 数据也会被正确地导入。
631
-
632
- 以下是 `importConfigState` 中导入实现的变更代码:
453
+ ## 五、测试
633
454
 
634
- ```diff
635
- class ConfigService {
636
- // ... 省略其他代码
637
-
638
- + importSessionGroups = async (sessionGroups: SessionGroupItem[]) => {
639
- + return sessionService.batchCreateSessionGroups(sessionGroups);
640
- + };
641
-
642
- importConfigState = async (config: ConfigFile): Promise<ImportResults | undefined> => {
643
- switch (config.exportType) {
644
- case 'settings': {
645
- await this.importSettings(config.state.settings);
646
-
647
- break;
648
- }
649
-
650
- case 'agents': {
651
- + const sessionGroups = await this.importSessionGroups(config.state.sessionGroups);
652
-
653
- const data = await this.importSessions(config.state.sessions);
654
- return {
655
- + sessionGroups: this.mapImportResult(sessionGroups),
656
- sessions: this.mapImportResult(data),
657
- };
658
- }
659
-
660
- case 'all': {
661
- await this.importSettings(config.state.settings);
662
-
663
- + const sessionGroups = await this.importSessionGroups(config.state.sessionGroups);
664
-
665
- const [sessions, messages, topics] = await Promise.all([
666
- this.importSessions(config.state.sessions),
667
- this.importMessages(config.state.messages),
668
- this.importTopics(config.state.topics),
669
- ]);
670
-
671
- return {
672
- messages: this.mapImportResult(messages),
673
- + sessionGroups: this.mapImportResult(sessionGroups),
674
- sessions: this.mapImportResult(sessions),
675
- topics: this.mapImportResult(topics),
676
- };
677
- }
678
-
679
- case 'sessions': {
680
- + const sessionGroups = await this.importSessionGroups(config.state.sessionGroups);
681
-
682
- const [sessions, messages, topics] = await Promise.all([
683
- this.importSessions(config.state.sessions),
684
- this.importMessages(config.state.messages),
685
- this.importTopics(config.state.topics),
686
- ]);
687
-
688
- return {
689
- messages: this.mapImportResult(messages),
690
- + sessionGroups: this.mapImportResult(sessionGroups),
691
- sessions: this.mapImportResult(sessions),
692
- topics: this.mapImportResult(topics),
693
- };
694
- }
695
- }
696
- };
697
- }
698
- ```
455
+ 项目使用 vitest 进行单元测试。
699
456
 
700
- 上述修改的一个要点是先进行 sessionGroup 的导入,因为如果先导入 session 时,如果没有在当前数据库中查到相应的 SessionGroup Id,那么这个 session 的 group 会兜底修改为默认值。这样就无法正确地将 sessionGroup 的 ID 与 session 进行关联。
457
+ 由于我们目前两个新的配置字段都是可选的,所以理论上你不更新测试也能跑通,不过由于我们把前面提到的默认配置 `DEFAULT_AGENT_CONFIG` 增加了 `openingQuestions` 字段,这导致很多测试计算出的配置都是有这个字段的,因此我们还是需要更新一部分测试的快照。
701
458
 
702
- 以上就是 LobeChat Session Group 功能在数据导入导出部分的实现。通过这种方式,我们可以确保用户的 Session Group 数据在导入导出过程中能够被正确地处理。
459
+ 对于当前这个场景,我建议是本地直接跑下测试,看哪些测试失败了,针对需要更新,例如测试文件 `src/store/agent/slices/chat/selectors/agent.test.ts` 需要执行一下 `npx vitest -u src/store/agent/slices/chat/selectors/agent.test.ts` 更新快照。
703
460
 
704
461
  ## 总结
705
462
 
706
- 以上就是 LobeChat Session Group 功能的完整实现流程。开发者可以参考本文档进行相关功能的开发和测试。
463
+ 以上就是 LobeChat 开场设置功能的完整实现流程。开发者可以参考本文档进行相关功能的开发和测试。