@lobehub/chat 1.136.13 → 1.137.0
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/.cursor/rules/add-setting-env.mdc +175 -0
- package/.cursor/rules/db-migrations.mdc +25 -0
- package/.env.example +7 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/docs/development/database-schema.dbml +1 -0
- package/docs/self-hosting/advanced/feature-flags.mdx +25 -15
- package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +25 -15
- package/docs/self-hosting/environment-variables/basic.mdx +12 -0
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +12 -0
- package/locales/ar/setting.json +8 -0
- package/locales/bg-BG/setting.json +8 -0
- package/locales/de-DE/setting.json +8 -0
- package/locales/en-US/setting.json +8 -0
- package/locales/es-ES/setting.json +8 -0
- package/locales/fa-IR/setting.json +8 -0
- package/locales/fr-FR/setting.json +8 -0
- package/locales/it-IT/setting.json +8 -0
- package/locales/ja-JP/setting.json +8 -0
- package/locales/ko-KR/setting.json +8 -0
- package/locales/nl-NL/setting.json +8 -0
- package/locales/pl-PL/setting.json +8 -0
- package/locales/pt-BR/setting.json +8 -0
- package/locales/ru-RU/setting.json +8 -0
- package/locales/tr-TR/setting.json +8 -0
- package/locales/vi-VN/setting.json +8 -0
- package/locales/zh-CN/setting.json +8 -0
- package/locales/zh-TW/setting.json +8 -0
- package/package.json +1 -1
- package/packages/const/src/settings/image.ts +8 -0
- package/packages/const/src/settings/index.ts +3 -0
- package/packages/context-engine/src/__tests__/pipeline.test.ts +485 -0
- package/packages/context-engine/src/base/__tests__/BaseProcessor.test.ts +381 -0
- package/packages/context-engine/src/base/__tests__/BaseProvider.test.ts +392 -0
- package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +346 -0
- package/packages/context-engine/src/processors/__tests__/ToolCall.test.ts +552 -0
- package/packages/database/migrations/0038_add_image_user_settings.sql +1 -0
- package/packages/database/migrations/meta/0038_snapshot.json +7580 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/core/migrations.json +6 -0
- package/packages/database/src/models/user.ts +3 -1
- package/packages/database/src/schemas/user.ts +1 -0
- package/packages/file-loaders/src/loaders/docx/index.test.ts +0 -1
- package/packages/file-loaders/src/loaders/excel/__snapshots__/index.test.ts.snap +30 -0
- package/packages/file-loaders/src/loaders/excel/index.test.ts +8 -0
- package/packages/file-loaders/src/loaders/pptx/index.test.ts +25 -0
- package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -0
- package/packages/file-loaders/vitest.config.mts +8 -0
- package/packages/types/src/serverConfig.ts +7 -1
- package/packages/types/src/user/settings/image.ts +3 -0
- package/packages/types/src/user/settings/index.ts +3 -0
- package/src/app/[variants]/(main)/settings/_layout/SettingsContent.tsx +3 -0
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +8 -3
- package/src/app/[variants]/(main)/settings/image/index.tsx +74 -0
- package/src/components/FormInput/FormSliderWithInput.tsx +40 -0
- package/src/components/FormInput/index.ts +1 -0
- package/src/envs/image.ts +27 -0
- package/src/hooks/useFetchAiImageConfig.ts +12 -17
- package/src/locales/default/setting.ts +8 -0
- package/src/server/globalConfig/index.ts +5 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/store/image/slices/generationConfig/action.test.ts +17 -0
- package/src/store/image/slices/generationConfig/action.ts +18 -21
- package/src/store/image/slices/generationConfig/initialState.ts +3 -2
- package/src/store/user/slices/common/action.ts +1 -0
- package/src/store/user/slices/settings/selectors/settings.ts +3 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { PipelineContext } from '../../types';
|
|
4
|
+
import { BaseProvider } from '../BaseProvider';
|
|
5
|
+
|
|
6
|
+
class TestProvider extends BaseProvider {
|
|
7
|
+
readonly name = 'TestProvider';
|
|
8
|
+
|
|
9
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
10
|
+
return context;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class ContextBuildingProvider extends BaseProvider {
|
|
15
|
+
readonly name = 'ContextBuildingProvider';
|
|
16
|
+
|
|
17
|
+
protected async buildContext(_context: PipelineContext): Promise<string | null> {
|
|
18
|
+
return 'Test context content';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
22
|
+
const content = await this.buildContext(context);
|
|
23
|
+
if (content && this.shouldInject(context)) {
|
|
24
|
+
const systemMessage = this.createSystemMessage(content);
|
|
25
|
+
return {
|
|
26
|
+
...context,
|
|
27
|
+
messages: [systemMessage, ...context.messages],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return context;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class ConditionalProvider extends BaseProvider {
|
|
35
|
+
readonly name = 'ConditionalProvider';
|
|
36
|
+
|
|
37
|
+
constructor(private shouldInjectFlag: boolean) {
|
|
38
|
+
super();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected shouldInject(_context: PipelineContext): boolean {
|
|
42
|
+
return this.shouldInjectFlag;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected async buildContext(_context: PipelineContext): Promise<string | null> {
|
|
46
|
+
return 'Conditional content';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
50
|
+
const content = await this.buildContext(context);
|
|
51
|
+
if (content && this.shouldInject(context)) {
|
|
52
|
+
const systemMessage = this.createSystemMessage(content);
|
|
53
|
+
return {
|
|
54
|
+
...context,
|
|
55
|
+
messages: [systemMessage, ...context.messages],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return context;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class NullContextProvider extends BaseProvider {
|
|
63
|
+
readonly name = 'NullContextProvider';
|
|
64
|
+
|
|
65
|
+
protected async buildContext(_context: PipelineContext): Promise<string | null> {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
70
|
+
const content = await this.buildContext(context);
|
|
71
|
+
if (content) {
|
|
72
|
+
const systemMessage = this.createSystemMessage(content);
|
|
73
|
+
return {
|
|
74
|
+
...context,
|
|
75
|
+
messages: [systemMessage, ...context.messages],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return context;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('BaseProvider', () => {
|
|
83
|
+
const createContext = (messages: any[] = []): PipelineContext => ({
|
|
84
|
+
initialState: {
|
|
85
|
+
messages: [],
|
|
86
|
+
model: 'test-model',
|
|
87
|
+
provider: 'test-provider',
|
|
88
|
+
},
|
|
89
|
+
isAborted: false,
|
|
90
|
+
messages,
|
|
91
|
+
metadata: {
|
|
92
|
+
maxTokens: 4000,
|
|
93
|
+
model: 'test-model',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('constructor', () => {
|
|
98
|
+
it('should initialize with options', () => {
|
|
99
|
+
const provider = new TestProvider({ debug: true });
|
|
100
|
+
expect(provider.name).toBe('TestProvider');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should initialize without options', () => {
|
|
104
|
+
const provider = new TestProvider();
|
|
105
|
+
expect(provider.name).toBe('TestProvider');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('buildContext', () => {
|
|
110
|
+
it('should return null by default', async () => {
|
|
111
|
+
const provider = new TestProvider();
|
|
112
|
+
const context = createContext();
|
|
113
|
+
|
|
114
|
+
// Access protected method through a derived class
|
|
115
|
+
class AccessibleProvider extends TestProvider {
|
|
116
|
+
async testBuildContext(ctx: PipelineContext) {
|
|
117
|
+
return this.buildContext(ctx);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const accessibleProvider = new AccessibleProvider();
|
|
122
|
+
const result = await accessibleProvider.testBuildContext(context);
|
|
123
|
+
|
|
124
|
+
expect(result).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should allow override to return content', async () => {
|
|
128
|
+
const provider = new ContextBuildingProvider();
|
|
129
|
+
const context = createContext([{ content: 'user message', role: 'user' }]);
|
|
130
|
+
|
|
131
|
+
const result = await provider.process(context);
|
|
132
|
+
|
|
133
|
+
expect(result.messages).toHaveLength(2);
|
|
134
|
+
expect(result.messages[0].role).toBe('system');
|
|
135
|
+
expect(result.messages[0].content).toBe('Test context content');
|
|
136
|
+
expect(result.messages[1].content).toBe('user message');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should allow returning null to skip injection', async () => {
|
|
140
|
+
const provider = new NullContextProvider();
|
|
141
|
+
const context = createContext([{ content: 'user message', role: 'user' }]);
|
|
142
|
+
|
|
143
|
+
const result = await provider.process(context);
|
|
144
|
+
|
|
145
|
+
expect(result.messages).toHaveLength(1);
|
|
146
|
+
expect(result.messages[0].content).toBe('user message');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('shouldInject', () => {
|
|
151
|
+
it('should return true by default', async () => {
|
|
152
|
+
const provider = new TestProvider();
|
|
153
|
+
const context = createContext();
|
|
154
|
+
|
|
155
|
+
// Access protected method
|
|
156
|
+
class AccessibleProvider extends TestProvider {
|
|
157
|
+
testShouldInject(ctx: PipelineContext) {
|
|
158
|
+
return this.shouldInject(ctx);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const accessibleProvider = new AccessibleProvider();
|
|
163
|
+
const result = accessibleProvider.testShouldInject(context);
|
|
164
|
+
|
|
165
|
+
expect(result).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should allow override to control injection', async () => {
|
|
169
|
+
const provider1 = new ConditionalProvider(true);
|
|
170
|
+
const provider2 = new ConditionalProvider(false);
|
|
171
|
+
|
|
172
|
+
const context = createContext([{ content: 'test', role: 'user' }]);
|
|
173
|
+
|
|
174
|
+
const result1 = await provider1.process(context);
|
|
175
|
+
const result2 = await provider2.process(context);
|
|
176
|
+
|
|
177
|
+
expect(result1.messages).toHaveLength(2);
|
|
178
|
+
expect(result1.messages[0].role).toBe('system');
|
|
179
|
+
|
|
180
|
+
expect(result2.messages).toHaveLength(1);
|
|
181
|
+
expect(result2.messages[0].role).toBe('user');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('createSystemMessage', () => {
|
|
186
|
+
it('should create system message with content', async () => {
|
|
187
|
+
const provider = new TestProvider();
|
|
188
|
+
|
|
189
|
+
// Access protected method
|
|
190
|
+
class AccessibleProvider extends TestProvider {
|
|
191
|
+
testCreateSystemMessage(content: string) {
|
|
192
|
+
return this.createSystemMessage(content);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const accessibleProvider = new AccessibleProvider();
|
|
197
|
+
const message = accessibleProvider.testCreateSystemMessage('Test system prompt');
|
|
198
|
+
|
|
199
|
+
expect(message).toEqual({
|
|
200
|
+
content: 'Test system prompt',
|
|
201
|
+
role: 'system',
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle empty content', async () => {
|
|
206
|
+
class AccessibleProvider extends TestProvider {
|
|
207
|
+
testCreateSystemMessage(content: string) {
|
|
208
|
+
return this.createSystemMessage(content);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const provider = new AccessibleProvider();
|
|
213
|
+
const message = provider.testCreateSystemMessage('');
|
|
214
|
+
|
|
215
|
+
expect(message).toEqual({
|
|
216
|
+
content: '',
|
|
217
|
+
role: 'system',
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle multiline content', async () => {
|
|
222
|
+
class AccessibleProvider extends TestProvider {
|
|
223
|
+
testCreateSystemMessage(content: string) {
|
|
224
|
+
return this.createSystemMessage(content);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const provider = new AccessibleProvider();
|
|
229
|
+
const content = 'Line 1\nLine 2\nLine 3';
|
|
230
|
+
const message = provider.testCreateSystemMessage(content);
|
|
231
|
+
|
|
232
|
+
expect(message).toEqual({
|
|
233
|
+
content,
|
|
234
|
+
role: 'system',
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('integration scenarios', () => {
|
|
240
|
+
it('should work with full provider pattern', async () => {
|
|
241
|
+
class FullProvider extends BaseProvider {
|
|
242
|
+
readonly name = 'FullProvider';
|
|
243
|
+
|
|
244
|
+
protected async buildContext(context: PipelineContext): Promise<string | null> {
|
|
245
|
+
if (context.initialState.systemRole) {
|
|
246
|
+
return `System: ${context.initialState.systemRole}`;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
protected shouldInject(context: PipelineContext): boolean {
|
|
252
|
+
return context.messages.length > 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
256
|
+
const cloned = this.cloneContext(context);
|
|
257
|
+
const content = await this.buildContext(context);
|
|
258
|
+
|
|
259
|
+
if (content && this.shouldInject(context)) {
|
|
260
|
+
const systemMessage = this.createSystemMessage(content);
|
|
261
|
+
cloned.messages = [systemMessage, ...cloned.messages];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return cloned;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const provider = new FullProvider();
|
|
269
|
+
const context = createContext([{ content: 'hello', role: 'user' }]);
|
|
270
|
+
context.initialState.systemRole = 'You are a helpful assistant';
|
|
271
|
+
|
|
272
|
+
const result = await provider.process(context);
|
|
273
|
+
|
|
274
|
+
expect(result.messages).toHaveLength(2);
|
|
275
|
+
expect(result.messages[0].role).toBe('system');
|
|
276
|
+
expect(result.messages[0].content).toBe('System: You are a helpful assistant');
|
|
277
|
+
expect(result.messages[1].content).toBe('hello');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should skip injection when shouldInject returns false', async () => {
|
|
281
|
+
class SkipProvider extends BaseProvider {
|
|
282
|
+
readonly name = 'SkipProvider';
|
|
283
|
+
|
|
284
|
+
protected async buildContext(_context: PipelineContext): Promise<string | null> {
|
|
285
|
+
return 'System prompt';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
protected shouldInject(context: PipelineContext): boolean {
|
|
289
|
+
return context.messages.length > 1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
293
|
+
const cloned = this.cloneContext(context);
|
|
294
|
+
const content = await this.buildContext(context);
|
|
295
|
+
|
|
296
|
+
if (content && this.shouldInject(context)) {
|
|
297
|
+
const systemMessage = this.createSystemMessage(content);
|
|
298
|
+
cloned.messages = [systemMessage, ...cloned.messages];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return cloned;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const provider = new SkipProvider();
|
|
306
|
+
const context = createContext([{ content: 'hello', role: 'user' }]);
|
|
307
|
+
|
|
308
|
+
const result = await provider.process(context);
|
|
309
|
+
|
|
310
|
+
expect(result.messages).toHaveLength(1);
|
|
311
|
+
expect(result.messages[0].role).toBe('user');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle complex context building', async () => {
|
|
315
|
+
class ComplexProvider extends BaseProvider {
|
|
316
|
+
readonly name = 'ComplexProvider';
|
|
317
|
+
|
|
318
|
+
protected async buildContext(context: PipelineContext): Promise<string | null> {
|
|
319
|
+
const { model, provider } = context.initialState;
|
|
320
|
+
const { maxTokens } = context.metadata;
|
|
321
|
+
|
|
322
|
+
return `Model: ${model}, Provider: ${provider}, Max Tokens: ${maxTokens}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
326
|
+
const cloned = this.cloneContext(context);
|
|
327
|
+
const content = await this.buildContext(context);
|
|
328
|
+
|
|
329
|
+
if (content && this.shouldInject(context)) {
|
|
330
|
+
const systemMessage = this.createSystemMessage(content);
|
|
331
|
+
cloned.messages = [systemMessage, ...cloned.messages];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return cloned;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const provider = new ComplexProvider();
|
|
339
|
+
const context = createContext([{ content: 'test', role: 'user' }]);
|
|
340
|
+
|
|
341
|
+
const result = await provider.process(context);
|
|
342
|
+
|
|
343
|
+
expect(result.messages).toHaveLength(2);
|
|
344
|
+
expect(result.messages[0].content).toBe(
|
|
345
|
+
'Model: test-model, Provider: test-provider, Max Tokens: 4000',
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('inheritance from BaseProcessor', () => {
|
|
351
|
+
it('should inherit all BaseProcessor functionality', async () => {
|
|
352
|
+
class InheritanceTestProvider extends BaseProvider {
|
|
353
|
+
readonly name = 'InheritanceTest';
|
|
354
|
+
|
|
355
|
+
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
356
|
+
const cloned = this.cloneContext(context);
|
|
357
|
+
|
|
358
|
+
// Use inherited methods
|
|
359
|
+
if (cloned.messages.length === 0) {
|
|
360
|
+
return this.abort(cloned, 'No messages');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const firstContent = cloned.messages[0].content;
|
|
364
|
+
if (this.isEmptyMessage(typeof firstContent === 'string' ? firstContent : undefined)) {
|
|
365
|
+
return this.abort(cloned, 'Empty message');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return this.markAsExecuted(cloned);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const provider = new InheritanceTestProvider();
|
|
373
|
+
|
|
374
|
+
// Test with empty messages
|
|
375
|
+
const context1 = createContext([]);
|
|
376
|
+
const result1 = await provider.process(context1);
|
|
377
|
+
expect(result1.isAborted).toBe(true);
|
|
378
|
+
expect(result1.abortReason).toBe('No messages');
|
|
379
|
+
|
|
380
|
+
// Test with empty content
|
|
381
|
+
const context2 = createContext([{ content: '', role: 'user' }]);
|
|
382
|
+
const result2 = await provider.process(context2);
|
|
383
|
+
expect(result2.isAborted).toBe(true);
|
|
384
|
+
expect(result2.abortReason).toBe('Empty message');
|
|
385
|
+
|
|
386
|
+
// Test with valid content
|
|
387
|
+
const context3 = createContext([{ content: 'hello', role: 'user' }]);
|
|
388
|
+
const result3 = await provider.process(context3);
|
|
389
|
+
expect(result3.isAborted).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|