@lobehub/chat 1.80.0 → 1.80.2
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.
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +21 -0
- package/docs/development/basic/feature-development.mdx +370 -619
- package/docs/development/basic/feature-development.zh-CN.mdx +368 -611
- package/package.json +1 -1
- package/src/app/[variants]/oauth/consent/[uid]/Client.tsx +36 -23
- package/src/app/[variants]/oauth/consent/[uid]/page.tsx +2 -0
- package/src/config/aiModels/azure.ts +79 -1
- package/src/config/aiModels/azureai.ts +181 -0
- package/src/config/aiModels/google.ts +36 -2
- package/src/config/aiModels/groq.ts +31 -3
- package/src/config/aiModels/hunyuan.ts +54 -18
- package/src/config/aiModels/moonshot.ts +17 -17
- package/src/config/aiModels/novita.ts +25 -30
- package/src/config/aiModels/siliconcloud.ts +80 -2
- package/src/config/aiModels/stepfun.ts +40 -31
- package/src/config/aiModels/tencentcloud.ts +7 -6
- package/src/config/aiModels/volcengine.ts +1 -0
- package/src/config/aiModels/zhipu.ts +91 -27
- package/src/const/settings/knowledge.ts +2 -2
- package/src/features/ChatInput/ActionBar/Upload/ClientMode.tsx +7 -6
- package/src/hooks/useModelSupportFiles.ts +15 -0
- package/src/libs/agent-runtime/stepfun/index.ts +7 -1
- package/src/libs/agent-runtime/zhipu/index.ts +17 -10
- package/src/libs/oidc-provider/config.ts +0 -3
- package/src/libs/trpc/edge/index.ts +0 -4
- package/src/libs/trpc/lambda/context.ts +90 -6
- package/src/libs/trpc/lambda/index.ts +2 -1
- package/src/libs/trpc/lambda/middleware/oidcAuth.ts +14 -0
- package/src/libs/trpc/middleware/userAuth.ts +2 -4
- package/src/server/services/oidc/index.ts +71 -0
- package/src/store/aiInfra/slices/aiModel/selectors.ts +7 -0
- package/src/utils/parseModels.test.ts +19 -3
- package/src/utils/server/__tests__/auth.test.ts +45 -1
- package/src/utils/server/auth.ts +26 -2
- package/docs/development/basic/feature-development-new.mdx +0 -465
- package/docs/development/basic/feature-development-new.zh-CN.mdx +0 -465
@@ -2,705 +2,462 @@
|
|
2
2
|
|
3
3
|
本文档旨在指导开发者了解如何在 LobeChat 中开发一块完整的功能需求。
|
4
4
|
|
5
|
-
我们将以
|
5
|
+
我们将以 [RFC 021 - 自定义助手开场引导](https://github.com/lobehub/lobe-chat/discussions/891) 为例,阐述完整的实现流程。
|
6
6
|
|
7
|
-
|
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
|
-
|
11
|
+
相比旧方案浏览器端使用 indexDB 来说,浏览器端和 server 端都使用 postgres 好处在于 model 层代码可以完全复用。
|
17
12
|
|
18
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
'
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
+
|
57
|
-
+
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
43
|
+
需要注意的是,有些时候我们可能还需要更新索引,但对于这个需求我们没有相关的性能瓶颈问题,所以不需要更新索引。
|
119
44
|
|
120
|
-
|
45
|
+
调整完 schema 后我们需要运行 `npm run db:generate,` 使用 drizzle-kit 自带的数据库迁移能力生成对应的用于迁移到最新 schema 的 sql 代码。执行后会产生四个文件:
|
121
46
|
|
122
|
-
|
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
|
-
|
52
|
+
注意脚本默认生成的迁移 sql 文件名不会像 `0021_add_agent_opening_settings.sql` 这样语义清晰,你需要自己手动对它重命名并且更新 `src/database/migrations/meta/_journal.json`。
|
125
53
|
|
126
|
-
|
54
|
+
以前客户端存储使用 indexDB 数据迁移相对麻烦,现在本地端使用 pglite 之后数据库迁移就是一条命令的事,非常简单快捷,你也可以检查生成的迁移 sql 是否有什么优化空间,手动调整。
|
127
55
|
|
128
|
-
|
56
|
+
## 二、更新数据模型
|
129
57
|
|
130
|
-
|
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
|
-
|
136
|
-
constructor() {
|
137
|
-
super('sessions', DB_SessionGroupSchema);
|
138
|
-
}
|
60
|
+
数据模型定义都放在 `src/types` 文件夹下,更新 `src/types/agent/index.ts` 中 `LobeAgentConfig` 类型:
|
139
61
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
72
|
+
+ /**
|
73
|
+
+ * 开场白
|
74
|
+
+ */
|
75
|
+
+ openingMessage?: string;
|
76
|
+
+ /**
|
77
|
+
+ * 开场问题
|
78
|
+
+ */
|
79
|
+
+ openingQuestions?: string[];
|
161
80
|
|
162
|
-
|
163
|
-
|
164
|
-
|
81
|
+
/**
|
82
|
+
* 语言模型参数
|
83
|
+
*/
|
84
|
+
params: LLMParams;
|
85
|
+
/**
|
86
|
+
* 启用的插件
|
87
|
+
*/
|
88
|
+
plugins?: string[];
|
165
89
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
}
|
90
|
+
/**
|
91
|
+
* 模型供应商
|
92
|
+
*/
|
93
|
+
provider?: string;
|
171
94
|
|
172
|
-
|
173
|
-
|
95
|
+
/**
|
96
|
+
* 系统角色
|
97
|
+
*/
|
98
|
+
systemRole: string;
|
174
99
|
|
175
|
-
|
100
|
+
/**
|
101
|
+
* 语音服务
|
102
|
+
*/
|
103
|
+
tts: LobeAgentTTSConfig;
|
176
104
|
}
|
177
105
|
```
|
178
106
|
|
179
|
-
##
|
180
|
-
|
181
|
-
在 LobeChat 应用中,Store 是用于管理应用前端状态的模块。其中的 Action 是触发状态更新的函数,通常会调用服务层的方法来执行实际的数据处理操作,然后更新 Store 中的状态。我们采用了 `zustand` 作为 Store 模块的底层依赖,对于状态管理的详细实践介绍,可以查阅 [📘 状态管理最佳实践](/zh/docs/development/state-management/state-management-intro)
|
107
|
+
## 三、Service 实现 / Model 实现
|
182
108
|
|
183
|
-
|
109
|
+
- `model` 层封装对 DB 的可复用操作
|
110
|
+
- `service` 层实现应用业务逻辑
|
184
111
|
|
185
|
-
|
112
|
+
在 `src` 目录下都有对应的顶层文件夹。
|
186
113
|
|
187
|
-
|
114
|
+
我们需要查看是否需要更新其实现,agent 配置在前端被抽象成 session 的配置,在 `src/services/session/server.ts` 中可以看到有个 service 函数 `updateSessionConfig`:
|
188
115
|
|
189
|
-
```
|
190
|
-
export
|
191
|
-
//
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
205
|
-
|
206
|
-
```
|
207
|
-
export const
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
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
|
-
|
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
|
-
|
148
|
+
## 四、前端实现
|
287
149
|
|
288
|
-
|
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
|
-
|
154
|
+
和 agent 相关的 store 有两个:
|
320
155
|
|
321
|
-
|
156
|
+
- `src/features/AgentSetting/store` 服务于 agent 设置的局部 store
|
157
|
+
- `src/store/agent` 用于获取当前会话 agent 的 store
|
322
158
|
|
323
|
-
|
159
|
+
后者通过 `src/features/AgentSetting/AgentSettings.tsx` 中 `AgentSettings` 组件的 `onConfigChange` 监听并更新当前会话的 agent 配置。
|
324
160
|
|
325
|
-
|
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
|
-
```
|
343
|
-
const
|
344
|
-
|
345
|
-
|
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
|
-
|
183
|
+
其实你这里不更新都可以,因为 `openingQuestions` 类型本来就是可选的,`openingMessage` 我这里就不更新了。
|
349
184
|
|
350
|
-
|
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
|
-
|
354
|
-
|
355
|
-
|
187
|
+
```diff
|
188
|
+
import { DEFAULT_AGENT_CHAT_CONFIG } from '@/const/settings';
|
189
|
+
import { LobeAgentChatConfig } from '@/types/agent';
|
356
190
|
|
357
|
-
|
358
|
-
const CreateGroupModal = () => {
|
359
|
-
// ... 其他逻辑
|
191
|
+
import { Store } from './action';
|
360
192
|
|
361
|
-
|
362
|
-
|
363
|
-
s.addSessionGroup,
|
364
|
-
]);
|
193
|
+
const chatConfig = (s: Store): LobeAgentChatConfig =>
|
194
|
+
s.config.chatConfig || DEFAULT_AGENT_CHAT_CONFIG;
|
365
195
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
392
|
-
|
393
|
-
|
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
|
-
|
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
|
-
|
218
|
+
这里因为只需要读取两个配置项,我们就简单的加两个 selectors 就好了:
|
403
219
|
|
404
|
-
|
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
|
-
|
413
|
-
+
|
414
|
-
|
415
|
-
|
416
|
-
const
|
417
|
-
|
418
|
-
|
419
|
-
+
|
420
|
-
+
|
421
|
-
|
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
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
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
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
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
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
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
|
-
|
497
|
-
|
327
|
+
// 处理拖拽排序后的逻辑
|
328
|
+
const handleSortEnd = useCallback(
|
329
|
+
(items: QuestionItem[]) => {
|
330
|
+
setQuestions(items.map((item) => item.content));
|
331
|
+
},
|
332
|
+
[setQuestions],
|
333
|
+
);
|
498
334
|
|
499
|
-
|
335
|
+
const items: QuestionItem[] = useMemo(() => {
|
336
|
+
return openingQuestions.map((item, index) => ({
|
337
|
+
content: item,
|
338
|
+
id: index,
|
339
|
+
}));
|
340
|
+
}, [openingQuestions]);
|
500
341
|
|
501
|
-
|
342
|
+
const isRepeat = openingQuestions.includes(questionInput.trim());
|
502
343
|
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
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
|
-
|
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
|
-
|
585
|
-
|
586
|
-
|
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
|
-
|
601
|
-
|
602
|
-
|
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
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
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
|
-
|
619
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
457
|
+
由于我们目前两个新的配置字段都是可选的,所以理论上你不更新测试也能跑通,不过由于我们把前面提到的默认配置 `DEFAULT_AGENT_CONFIG` 增加了 `openingQuestions` 字段,这导致很多测试计算出的配置都是有这个字段的,因此我们还是需要更新一部分测试的快照。
|
701
458
|
|
702
|
-
|
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
|
463
|
+
以上就是 LobeChat 开场设置功能的完整实现流程。开发者可以参考本文档进行相关功能的开发和测试。
|