@lobehub/lobehub 2.0.0-next.207 → 2.0.0-next.209
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 +52 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +27 -0
- package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +8 -1
- package/packages/model-runtime/src/utils/modelParse.ts +78 -78
- package/packages/model-runtime/src/utils/uriParser.ts +4 -4
- package/src/app/[variants]/(main)/home/_layout/Header/components/AddButton.tsx +2 -2
- package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +9 -9
- package/src/helpers/parserPlaceholder/index.test.ts +361 -2
- package/src/server/routers/lambda/userMemories.ts +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.209](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.208...v2.0.0-next.209)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-04**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **model-runtime**: Handle array content in anthropic assistant messages.
|
|
12
|
+
- **misc**: Use configured embedding provider instead of hardcoded OpenAI.
|
|
13
|
+
|
|
14
|
+
<br/>
|
|
15
|
+
|
|
16
|
+
<details>
|
|
17
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
18
|
+
|
|
19
|
+
#### What's fixed
|
|
20
|
+
|
|
21
|
+
- **model-runtime**: Handle array content in anthropic assistant messages, closes [#11206](https://github.com/lobehub/lobe-chat/issues/11206) ([b03845d](https://github.com/lobehub/lobe-chat/commit/b03845d))
|
|
22
|
+
- **misc**: Use configured embedding provider instead of hardcoded OpenAI, closes [#11133](https://github.com/lobehub/lobe-chat/issues/11133) ([503c3eb](https://github.com/lobehub/lobe-chat/commit/503c3eb))
|
|
23
|
+
|
|
24
|
+
</details>
|
|
25
|
+
|
|
26
|
+
<div align="right">
|
|
27
|
+
|
|
28
|
+
[](#readme-top)
|
|
29
|
+
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
## [Version 2.0.0-next.208](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.207...v2.0.0-next.208)
|
|
33
|
+
|
|
34
|
+
<sup>Released on **2026-01-04**</sup>
|
|
35
|
+
|
|
36
|
+
#### 🐛 Bug Fixes
|
|
37
|
+
|
|
38
|
+
- **misc**: Auto jump to group.
|
|
39
|
+
|
|
40
|
+
<br/>
|
|
41
|
+
|
|
42
|
+
<details>
|
|
43
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
44
|
+
|
|
45
|
+
#### What's fixed
|
|
46
|
+
|
|
47
|
+
- **misc**: Auto jump to group, closes [#11187](https://github.com/lobehub/lobe-chat/issues/11187) ([e43578a](https://github.com/lobehub/lobe-chat/commit/e43578a))
|
|
48
|
+
|
|
49
|
+
</details>
|
|
50
|
+
|
|
51
|
+
<div align="right">
|
|
52
|
+
|
|
53
|
+
[](#readme-top)
|
|
54
|
+
|
|
55
|
+
</div>
|
|
56
|
+
|
|
5
57
|
## [Version 2.0.0-next.207](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.206...v2.0.0-next.207)
|
|
6
58
|
|
|
7
59
|
<sup>Released on **2026-01-04**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"fixes": [
|
|
5
|
+
"Use configured embedding provider instead of hardcoded OpenAI."
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"date": "2026-01-04",
|
|
9
|
+
"version": "2.0.0-next.209"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"children": {
|
|
13
|
+
"fixes": [
|
|
14
|
+
"Auto jump to group."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"date": "2026-01-04",
|
|
18
|
+
"version": "2.0.0-next.208"
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"children": {
|
|
4
22
|
"fixes": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.209",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -216,6 +216,33 @@ describe('anthropicHelpers', () => {
|
|
|
216
216
|
role: 'assistant',
|
|
217
217
|
});
|
|
218
218
|
});
|
|
219
|
+
|
|
220
|
+
it('should correctly convert assistant message with array content but no tool_calls', async () => {
|
|
221
|
+
const message: OpenAIChatMessage = {
|
|
222
|
+
content: [
|
|
223
|
+
{ thinking: 'Let me think about this...', type: 'thinking', signature: 'sig123' },
|
|
224
|
+
{ type: 'text', text: 'Here is my response.' },
|
|
225
|
+
],
|
|
226
|
+
role: 'assistant',
|
|
227
|
+
};
|
|
228
|
+
const result = await buildAnthropicMessage(message);
|
|
229
|
+
expect(result).toEqual({
|
|
230
|
+
content: [
|
|
231
|
+
{ thinking: 'Let me think about this...', type: 'thinking', signature: 'sig123' },
|
|
232
|
+
{ type: 'text', text: 'Here is my response.' },
|
|
233
|
+
],
|
|
234
|
+
role: 'assistant',
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should return undefined for assistant message with empty array content', async () => {
|
|
239
|
+
const message: OpenAIChatMessage = {
|
|
240
|
+
content: [],
|
|
241
|
+
role: 'assistant',
|
|
242
|
+
};
|
|
243
|
+
const result = await buildAnthropicMessage(message);
|
|
244
|
+
expect(result).toBeUndefined();
|
|
245
|
+
});
|
|
219
246
|
});
|
|
220
247
|
|
|
221
248
|
describe('buildAnthropicMessages', () => {
|
|
@@ -118,8 +118,15 @@ export const buildAnthropicMessage = async (
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// or it's a plain assistant message
|
|
121
|
+
// Handle array content (e.g., content with thinking blocks)
|
|
122
|
+
if (Array.isArray(content)) {
|
|
123
|
+
const messageContent = await buildArrayContent(content);
|
|
124
|
+
if (messageContent.length === 0) return undefined;
|
|
125
|
+
return { content: messageContent, role: 'assistant' };
|
|
126
|
+
}
|
|
127
|
+
|
|
121
128
|
// Anthropic API requires non-empty content, filter out empty/whitespace-only content
|
|
122
|
-
const textContent =
|
|
129
|
+
const textContent = content?.trim();
|
|
123
130
|
if (!textContent) return undefined;
|
|
124
131
|
return { content: textContent, role: 'assistant' };
|
|
125
132
|
}
|
|
@@ -4,7 +4,7 @@ import { AIBaseModelCard } from 'model-bank';
|
|
|
4
4
|
import type { ModelProviderKey } from '../types';
|
|
5
5
|
|
|
6
6
|
export interface ModelProcessorConfig {
|
|
7
|
-
excludeKeywords?: readonly string[]; //
|
|
7
|
+
excludeKeywords?: readonly string[]; // Do not add tags to models that match
|
|
8
8
|
functionCallKeywords?: readonly string[];
|
|
9
9
|
imageOutputKeywords?: readonly string[];
|
|
10
10
|
reasoningKeywords?: readonly string[];
|
|
@@ -13,10 +13,10 @@ export interface ModelProcessorConfig {
|
|
|
13
13
|
visionKeywords?: readonly string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// Default keyword: any model ID containing -search is considered to support internet search
|
|
17
17
|
const DEFAULT_SEARCH_KEYWORDS = ['-search'] as const;
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// Model capability tag keyword configuration
|
|
20
20
|
export const MODEL_LIST_CONFIGS = {
|
|
21
21
|
anthropic: {
|
|
22
22
|
functionCallKeywords: ['claude'],
|
|
@@ -136,7 +136,7 @@ export const MODEL_LIST_CONFIGS = {
|
|
|
136
136
|
},
|
|
137
137
|
} as const;
|
|
138
138
|
|
|
139
|
-
//
|
|
139
|
+
// Model owner (provider) keyword configuration
|
|
140
140
|
export const MODEL_OWNER_DETECTION_CONFIG = {
|
|
141
141
|
anthropic: ['claude'],
|
|
142
142
|
comfyui: ['comfyui/'], // ComfyUI models detection - all ComfyUI models have comfyui/ prefix
|
|
@@ -159,7 +159,7 @@ export const MODEL_OWNER_DETECTION_CONFIG = {
|
|
|
159
159
|
zhipu: ['glm'],
|
|
160
160
|
} as const;
|
|
161
161
|
|
|
162
|
-
//
|
|
162
|
+
// Image model keyword configuration
|
|
163
163
|
export const IMAGE_MODEL_KEYWORDS = [
|
|
164
164
|
'dall-e',
|
|
165
165
|
'dalle',
|
|
@@ -173,25 +173,25 @@ export const IMAGE_MODEL_KEYWORDS = [
|
|
|
173
173
|
'wanxiang',
|
|
174
174
|
'DESCRIBE',
|
|
175
175
|
'UPSCALE',
|
|
176
|
-
'!gemini', //
|
|
176
|
+
'!gemini', // Exclude gemini models, they are chat models even if they contain -image
|
|
177
177
|
'-image',
|
|
178
178
|
'^V3',
|
|
179
179
|
'^V_2',
|
|
180
180
|
'^V_1',
|
|
181
181
|
] as const;
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// Embedding model keyword configuration
|
|
184
184
|
export const EMBEDDING_MODEL_KEYWORDS = ['embedding', 'embed', 'bge', 'm3e'] as const;
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
|
-
*
|
|
188
|
-
* @param modelId
|
|
189
|
-
* @param keywords
|
|
190
|
-
* - ^
|
|
191
|
-
* - !
|
|
192
|
-
* - re:
|
|
193
|
-
* -
|
|
194
|
-
* @returns
|
|
187
|
+
* Detect whether a keyword list matches a model ID (supports multiple matching patterns)
|
|
188
|
+
* @param modelId Model ID (lowercase)
|
|
189
|
+
* @param keywords Keyword list, supports the following prefixes:
|
|
190
|
+
* - ^ prefix: match only at the start of model ID
|
|
191
|
+
* - ! prefix: exclude match, highest priority
|
|
192
|
+
* - re: prefix: regular expression match (supports !re: for regex exclusion)
|
|
193
|
+
* - no prefix: contains match (default behavior)
|
|
194
|
+
* @returns Whether it matches (exclusion logic takes priority)
|
|
195
195
|
*/
|
|
196
196
|
const isKeywordListMatch = (modelId: string, keywords: readonly string[]): boolean => {
|
|
197
197
|
const matchKeyword = (keyword: string): boolean => {
|
|
@@ -212,7 +212,7 @@ const isKeywordListMatch = (modelId: string, keywords: readonly string[]): boole
|
|
|
212
212
|
return modelId.includes(rawKeyword);
|
|
213
213
|
};
|
|
214
214
|
|
|
215
|
-
//
|
|
215
|
+
// First check exclusion rules (starting with exclamation mark, including !re:)
|
|
216
216
|
const excludeKeywords = keywords.filter((keyword) => keyword.startsWith('!'));
|
|
217
217
|
const includeKeywords = keywords.filter((keyword) => !keyword.startsWith('!'));
|
|
218
218
|
|
|
@@ -222,15 +222,15 @@ const isKeywordListMatch = (modelId: string, keywords: readonly string[]): boole
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
//
|
|
225
|
+
// Check inclusion rules
|
|
226
226
|
return includeKeywords.some((keyword) => matchKeyword(keyword));
|
|
227
227
|
};
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
|
-
*
|
|
231
|
-
* @param modelId
|
|
232
|
-
* @param provider
|
|
233
|
-
* @returns
|
|
230
|
+
* Find the corresponding local model configuration based on provider type
|
|
231
|
+
* @param modelId Model ID
|
|
232
|
+
* @param provider Provider type
|
|
233
|
+
* @returns Matching local model configuration
|
|
234
234
|
*/
|
|
235
235
|
const findKnownModelByProvider = async (
|
|
236
236
|
modelId: string,
|
|
@@ -239,32 +239,32 @@ const findKnownModelByProvider = async (
|
|
|
239
239
|
const lowerModelId = modelId.toLowerCase();
|
|
240
240
|
|
|
241
241
|
try {
|
|
242
|
-
//
|
|
242
|
+
// Attempt to dynamically import the corresponding configuration file
|
|
243
243
|
const modules = await import('model-bank');
|
|
244
244
|
|
|
245
|
-
//
|
|
245
|
+
// If provider configuration file doesn't exist, skip
|
|
246
246
|
if (!(provider in modules)) {
|
|
247
247
|
return null;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
const providerModels = modules[provider as keyof typeof modules] as AIBaseModelCard[];
|
|
251
251
|
|
|
252
|
-
//
|
|
252
|
+
// If import succeeds and has data, perform search
|
|
253
253
|
if (Array.isArray(providerModels)) {
|
|
254
254
|
return providerModels.find((m) => m.id.toLowerCase() === lowerModelId);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
return null;
|
|
258
258
|
} catch {
|
|
259
|
-
//
|
|
259
|
+
// If import fails (file doesn't exist or other error), return null
|
|
260
260
|
return null;
|
|
261
261
|
}
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
|
-
*
|
|
266
|
-
* @param modelId
|
|
267
|
-
* @returns
|
|
265
|
+
* Detect the provider type of a single model
|
|
266
|
+
* @param modelId Model ID
|
|
267
|
+
* @returns Detected provider configuration key name, defaults to 'openai'
|
|
268
268
|
*/
|
|
269
269
|
export const detectModelProvider = (modelId: string): keyof typeof MODEL_LIST_CONFIGS => {
|
|
270
270
|
const lowerModelId = modelId.toLowerCase();
|
|
@@ -281,20 +281,20 @@ export const detectModelProvider = (modelId: string): keyof typeof MODEL_LIST_CO
|
|
|
281
281
|
};
|
|
282
282
|
|
|
283
283
|
/**
|
|
284
|
-
*
|
|
285
|
-
* @param timestamp
|
|
286
|
-
* @returns
|
|
284
|
+
* Convert timestamp to date string
|
|
285
|
+
* @param timestamp Timestamp (seconds)
|
|
286
|
+
* @returns Formatted date string (YYYY-MM-DD)
|
|
287
287
|
*/
|
|
288
288
|
const formatTimestampToDate = (timestamp: number): string | undefined => {
|
|
289
289
|
if (timestamp === null || timestamp === undefined || Number.isNaN(timestamp)) return undefined;
|
|
290
290
|
|
|
291
|
-
//
|
|
292
|
-
// -
|
|
293
|
-
// -
|
|
291
|
+
// Support both second-level and millisecond-level timestamps:
|
|
292
|
+
// - If millisecond-level (>= 1e12), use as milliseconds directly;
|
|
293
|
+
// - Otherwise treat as seconds, need to *1000 to convert to milliseconds
|
|
294
294
|
const msTimestamp = timestamp > 1e12 ? timestamp : timestamp * 1000;
|
|
295
295
|
const date = new Date(msTimestamp);
|
|
296
296
|
|
|
297
|
-
//
|
|
297
|
+
// Validate parsing result and year range (only accept 4-digit years to avoid exceeding varchar(10) YYYY-MM-DD)
|
|
298
298
|
const year = date.getUTCFullYear();
|
|
299
299
|
if (year < 1000 || year > 9999) return undefined;
|
|
300
300
|
|
|
@@ -303,22 +303,22 @@ const formatTimestampToDate = (timestamp: number): string | undefined => {
|
|
|
303
303
|
};
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
|
-
*
|
|
307
|
-
* @param model
|
|
308
|
-
* @param knownModel
|
|
309
|
-
* @returns
|
|
306
|
+
* Process releasedAt field
|
|
307
|
+
* @param model Model object
|
|
308
|
+
* @param knownModel Known model configuration
|
|
309
|
+
* @returns Processed releasedAt value
|
|
310
310
|
*/
|
|
311
311
|
const processReleasedAt = (model: any, knownModel?: any): string | undefined => {
|
|
312
|
-
//
|
|
312
|
+
// Check model.created first
|
|
313
313
|
if (model.created !== undefined && model.created !== null) {
|
|
314
|
-
//
|
|
314
|
+
// Check if it's in timestamp format
|
|
315
315
|
if (typeof model.created === 'number' && model.created > 1_630_000_000) {
|
|
316
|
-
// AiHubMix
|
|
316
|
+
// AiHubMix incorrect timestamp is 1626777600
|
|
317
317
|
return formatTimestampToDate(model.created);
|
|
318
318
|
}
|
|
319
|
-
//
|
|
319
|
+
// If created is a string and already in date format, return directly
|
|
320
320
|
if (typeof model.created === 'string') {
|
|
321
|
-
// Anthropic
|
|
321
|
+
// Anthropic: if it's '2025-02-19T00:00:00Z', only take the date part
|
|
322
322
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(model.created)) {
|
|
323
323
|
return model.created.split('T')[0];
|
|
324
324
|
}
|
|
@@ -326,17 +326,17 @@ const processReleasedAt = (model: any, knownModel?: any): string | undefined =>
|
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
//
|
|
329
|
+
// Fall back to original logic
|
|
330
330
|
return model.releasedAt ?? knownModel?.releasedAt ?? undefined;
|
|
331
331
|
};
|
|
332
332
|
|
|
333
333
|
/**
|
|
334
|
-
*
|
|
335
|
-
* @param displayName
|
|
336
|
-
* @returns
|
|
334
|
+
* Process model display name
|
|
335
|
+
* @param displayName Original display name
|
|
336
|
+
* @returns Processed display name
|
|
337
337
|
*/
|
|
338
338
|
const processDisplayName = (displayName: string): string => {
|
|
339
|
-
//
|
|
339
|
+
// If it contains "Gemini 2.5 Flash Image Preview", replace the corresponding part with "Nano Banana"
|
|
340
340
|
if (displayName.includes('Gemini 2.5 Flash Image Preview')) {
|
|
341
341
|
return displayName.replace('Gemini 2.5 Flash Image Preview', 'Nano Banana');
|
|
342
342
|
}
|
|
@@ -345,9 +345,9 @@ const processDisplayName = (displayName: string): string => {
|
|
|
345
345
|
};
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
*
|
|
349
|
-
* @param provider
|
|
350
|
-
* @returns
|
|
348
|
+
* Get the local configuration of the model provider
|
|
349
|
+
* @param provider Model provider
|
|
350
|
+
* @returns Local configuration of the model provider
|
|
351
351
|
*/
|
|
352
352
|
const getProviderLocalConfig = async (provider?: ModelProviderKey): Promise<any[] | null> => {
|
|
353
353
|
let providerLocalConfig: any[] | null = null;
|
|
@@ -357,7 +357,7 @@ const getProviderLocalConfig = async (provider?: ModelProviderKey): Promise<any[
|
|
|
357
357
|
|
|
358
358
|
providerLocalConfig = modules[provider];
|
|
359
359
|
} catch {
|
|
360
|
-
//
|
|
360
|
+
// If configuration file doesn't exist or import fails, keep as null
|
|
361
361
|
providerLocalConfig = null;
|
|
362
362
|
}
|
|
363
363
|
}
|
|
@@ -365,16 +365,16 @@ const getProviderLocalConfig = async (provider?: ModelProviderKey): Promise<any[
|
|
|
365
365
|
};
|
|
366
366
|
|
|
367
367
|
/**
|
|
368
|
-
*
|
|
369
|
-
* @param providerLocalConfig
|
|
370
|
-
* @param model
|
|
371
|
-
* @returns
|
|
368
|
+
* Get model local configuration
|
|
369
|
+
* @param providerLocalConfig Local configuration of the model provider
|
|
370
|
+
* @param model Model object
|
|
371
|
+
* @returns Model local configuration
|
|
372
372
|
*/
|
|
373
373
|
const getModelLocalEnableConfig = (
|
|
374
374
|
providerLocalConfig: any[],
|
|
375
375
|
model: { id: string },
|
|
376
376
|
): any | null => {
|
|
377
|
-
//
|
|
377
|
+
// If providerid is provided and has local configuration, try to get the model's enabled status from it
|
|
378
378
|
let providerLocalModelConfig = null;
|
|
379
379
|
if (providerLocalConfig && Array.isArray(providerLocalConfig)) {
|
|
380
380
|
providerLocalModelConfig = providerLocalConfig.find((m) => m.id === model.id);
|
|
@@ -383,7 +383,7 @@ const getModelLocalEnableConfig = (
|
|
|
383
383
|
};
|
|
384
384
|
|
|
385
385
|
/**
|
|
386
|
-
*
|
|
386
|
+
* Common logic for processing model cards
|
|
387
387
|
*/
|
|
388
388
|
const processModelCard = (
|
|
389
389
|
model: { [key: string]: any; id: string },
|
|
@@ -522,11 +522,11 @@ const processModelCard = (
|
|
|
522
522
|
};
|
|
523
523
|
|
|
524
524
|
/**
|
|
525
|
-
*
|
|
526
|
-
* @param modelList
|
|
527
|
-
* @param config
|
|
528
|
-
* @param provider
|
|
529
|
-
* @returns
|
|
525
|
+
* Process model list for a single provider
|
|
526
|
+
* @param modelList Model list
|
|
527
|
+
* @param config Provider configuration
|
|
528
|
+
* @param provider Provider type (optional, used to prioritize matching corresponding local configuration, will only attempt to override enabled from local configuration when provider is provided)
|
|
529
|
+
* @returns Processed model card list
|
|
530
530
|
*/
|
|
531
531
|
export const processModelList = async (
|
|
532
532
|
modelList: Array<{ id: string }>,
|
|
@@ -535,19 +535,19 @@ export const processModelList = async (
|
|
|
535
535
|
): Promise<ChatModelCard[]> => {
|
|
536
536
|
const { LOBE_DEFAULT_MODEL_LIST } = await import('model-bank');
|
|
537
537
|
|
|
538
|
-
//
|
|
538
|
+
// If provider is provided, try to get the local configuration for that provider
|
|
539
539
|
const providerLocalConfig = await getProviderLocalConfig(provider as ModelProviderKey);
|
|
540
540
|
|
|
541
541
|
return Promise.all(
|
|
542
542
|
modelList.map(async (model) => {
|
|
543
543
|
let knownModel: any = null;
|
|
544
544
|
|
|
545
|
-
//
|
|
545
|
+
// If provider is provided, prioritize using provider-specific configuration
|
|
546
546
|
if (provider) {
|
|
547
547
|
knownModel = await findKnownModelByProvider(model.id, provider);
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
-
//
|
|
550
|
+
// If not found, fall back to global configuration
|
|
551
551
|
if (!knownModel) {
|
|
552
552
|
knownModel = LOBE_DEFAULT_MODEL_LIST.find(
|
|
553
553
|
(m) => model.id.toLowerCase() === m.id.toLowerCase(),
|
|
@@ -556,13 +556,13 @@ export const processModelList = async (
|
|
|
556
556
|
|
|
557
557
|
const processedModel = processModelCard(model, config, knownModel);
|
|
558
558
|
|
|
559
|
-
//
|
|
559
|
+
// If provider is provided and has local configuration, try to get the model's enabled status from it
|
|
560
560
|
const providerLocalModelConfig = getModelLocalEnableConfig(
|
|
561
561
|
providerLocalConfig as any[],
|
|
562
562
|
model,
|
|
563
563
|
);
|
|
564
564
|
|
|
565
|
-
//
|
|
565
|
+
// If model is found in local configuration, use its enabled status
|
|
566
566
|
if (
|
|
567
567
|
processedModel &&
|
|
568
568
|
providerLocalModelConfig &&
|
|
@@ -577,10 +577,10 @@ export const processModelList = async (
|
|
|
577
577
|
};
|
|
578
578
|
|
|
579
579
|
/**
|
|
580
|
-
*
|
|
581
|
-
* @param modelList
|
|
582
|
-
* @param providerid
|
|
583
|
-
* @returns
|
|
580
|
+
* Process model list for mixed providers
|
|
581
|
+
* @param modelList Model list
|
|
582
|
+
* @param providerid Optional provider ID, used to get its local configuration file
|
|
583
|
+
* @returns Processed model card list
|
|
584
584
|
*/
|
|
585
585
|
export const processMultiProviderModelList = async (
|
|
586
586
|
modelList: Array<{ id: string }>,
|
|
@@ -588,7 +588,7 @@ export const processMultiProviderModelList = async (
|
|
|
588
588
|
): Promise<ChatModelCard[]> => {
|
|
589
589
|
const { LOBE_DEFAULT_MODEL_LIST } = await import('model-bank');
|
|
590
590
|
|
|
591
|
-
//
|
|
591
|
+
// If providerid is provided, try to get the local configuration for that provider
|
|
592
592
|
const providerLocalConfig = await getProviderLocalConfig(providerid);
|
|
593
593
|
|
|
594
594
|
return Promise.all(
|
|
@@ -596,17 +596,17 @@ export const processMultiProviderModelList = async (
|
|
|
596
596
|
const detectedProvider = detectModelProvider(model.id);
|
|
597
597
|
const config = MODEL_LIST_CONFIGS[detectedProvider];
|
|
598
598
|
|
|
599
|
-
//
|
|
599
|
+
// Prioritize using provider-specific configuration
|
|
600
600
|
let knownModel = await findKnownModelByProvider(model.id, detectedProvider);
|
|
601
601
|
|
|
602
|
-
//
|
|
602
|
+
// If not found, fall back to global configuration
|
|
603
603
|
if (!knownModel) {
|
|
604
604
|
knownModel = LOBE_DEFAULT_MODEL_LIST.find(
|
|
605
605
|
(m) => model.id.toLowerCase() === m.id.toLowerCase(),
|
|
606
606
|
);
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
-
//
|
|
609
|
+
// If providerid is provided and has local configuration, try to get the model's enabled status from it
|
|
610
610
|
const providerLocalModelConfig = getModelLocalEnableConfig(
|
|
611
611
|
providerLocalConfig as any[],
|
|
612
612
|
model,
|
|
@@ -614,7 +614,7 @@ export const processMultiProviderModelList = async (
|
|
|
614
614
|
|
|
615
615
|
const processedModel = processModelCard(model, config, knownModel);
|
|
616
616
|
|
|
617
|
-
//
|
|
617
|
+
// If model is found in local configuration, use its enabled status
|
|
618
618
|
if (
|
|
619
619
|
processedModel &&
|
|
620
620
|
providerLocalModelConfig &&
|
|
@@ -5,20 +5,20 @@ interface UriParserResult {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export const parseDataUri = (dataUri: string): UriParserResult => {
|
|
8
|
-
//
|
|
8
|
+
// Regular expression to match the entire Data URI structure
|
|
9
9
|
const dataUriMatch = dataUri.match(/^data:([^;]+);base64,(.+)$/);
|
|
10
10
|
|
|
11
11
|
if (dataUriMatch) {
|
|
12
|
-
//
|
|
12
|
+
// If it's a valid Data URI
|
|
13
13
|
return { base64: dataUriMatch[2], mimeType: dataUriMatch[1], type: 'base64' };
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
new URL(dataUri);
|
|
18
|
-
//
|
|
18
|
+
// If it's a valid URL
|
|
19
19
|
return { base64: null, mimeType: null, type: 'url' };
|
|
20
20
|
} catch {
|
|
21
|
-
//
|
|
21
|
+
// Neither a Data URI nor a valid URL
|
|
22
22
|
return { base64: null, mimeType: null, type: null };
|
|
23
23
|
}
|
|
24
24
|
};
|
|
@@ -18,7 +18,7 @@ const AddButton = memo(() => {
|
|
|
18
18
|
createGroupChatMenuItem,
|
|
19
19
|
createPageMenuItem,
|
|
20
20
|
createAgent,
|
|
21
|
-
|
|
21
|
+
isMutatingAgent,
|
|
22
22
|
isCreatingGroup,
|
|
23
23
|
} = useCreateMenuItems();
|
|
24
24
|
|
|
@@ -39,7 +39,7 @@ const AddButton = memo(() => {
|
|
|
39
39
|
<Flexbox horizontal>
|
|
40
40
|
<ActionIcon
|
|
41
41
|
icon={CreateBotIcon}
|
|
42
|
-
loading={
|
|
42
|
+
loading={isMutatingAgent || isCreatingGroup}
|
|
43
43
|
onClick={handleMainIconClick}
|
|
44
44
|
size={DESKTOP_HEADER_ICON_SIZE}
|
|
45
45
|
title={tChat('newAgent')}
|
|
@@ -6,10 +6,10 @@ import { BotIcon, FileTextIcon, FolderCogIcon, FolderPlus } from 'lucide-react';
|
|
|
6
6
|
import { useCallback, useState } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { useNavigate } from 'react-router-dom';
|
|
9
|
+
import useSWRMutation from 'swr/mutation';
|
|
9
10
|
|
|
10
11
|
import { useGroupTemplates } from '@/components/ChatGroupWizard/templates';
|
|
11
12
|
import { DEFAULT_CHAT_GROUP_CHAT_CONFIG } from '@/const/settings';
|
|
12
|
-
import { useActionSWR } from '@/libs/swr';
|
|
13
13
|
import { type GroupMemberConfig, chatGroupService } from '@/services/chatGroup';
|
|
14
14
|
import { useAgentStore } from '@/store/agent';
|
|
15
15
|
import { useAgentGroupStore } from '@/store/agentGroup';
|
|
@@ -51,22 +51,22 @@ export const useCreateMenuItems = () => {
|
|
|
51
51
|
const [isCreatingSessionGroup, setIsCreatingSessionGroup] = useState(false);
|
|
52
52
|
|
|
53
53
|
// SWR-based agent creation with auto navigation to profile
|
|
54
|
-
const {
|
|
54
|
+
const { trigger: mutateAgent, isMutating: isMutatingAgent } = useSWRMutation(
|
|
55
55
|
'agent.createAgent',
|
|
56
56
|
async () => {
|
|
57
57
|
const result = await storeCreateAgent({});
|
|
58
|
-
navigate(`/agent/${result.agentId}/profile`);
|
|
59
58
|
return result;
|
|
60
59
|
},
|
|
61
60
|
{
|
|
62
|
-
onSuccess: async () => {
|
|
61
|
+
onSuccess: async (result) => {
|
|
62
|
+
navigate(`/agent/${result.agentId}/profile`);
|
|
63
63
|
await refreshAgentList();
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
66
|
);
|
|
67
67
|
|
|
68
68
|
// SWR-based group creation with auto navigation to profile
|
|
69
|
-
const {
|
|
69
|
+
const { trigger: mutateGroup, isMutating: isMutatingGroup } = useSWRMutation(
|
|
70
70
|
'group.createGroup',
|
|
71
71
|
async () => {
|
|
72
72
|
const groupId = await createGroup(
|
|
@@ -77,11 +77,11 @@ export const useCreateMenuItems = () => {
|
|
|
77
77
|
[],
|
|
78
78
|
true, // silent mode - don't switch session, we'll navigate instead
|
|
79
79
|
);
|
|
80
|
-
navigate(`/group/${groupId}/profile`);
|
|
81
80
|
return groupId;
|
|
82
81
|
},
|
|
83
82
|
{
|
|
84
|
-
onSuccess: async () => {
|
|
83
|
+
onSuccess: async (groupId) => {
|
|
84
|
+
navigate(`/group/${groupId}/profile`);
|
|
85
85
|
await refreshAgentList();
|
|
86
86
|
await loadGroups();
|
|
87
87
|
},
|
|
@@ -329,7 +329,7 @@ export const useCreateMenuItems = () => {
|
|
|
329
329
|
// Loading states
|
|
330
330
|
isCreatingGroup,
|
|
331
331
|
isCreatingSessionGroup,
|
|
332
|
-
isLoading:
|
|
333
|
-
|
|
332
|
+
isLoading: isMutatingAgent || isMutatingGroup || isCreatingGroup || isCreatingSessionGroup,
|
|
333
|
+
isMutatingAgent,
|
|
334
334
|
};
|
|
335
335
|
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
VARIABLE_GENERATORS,
|
|
5
|
+
parsePlaceholderVariables,
|
|
6
|
+
parsePlaceholderVariablesMessages,
|
|
7
|
+
} from './index';
|
|
4
8
|
|
|
5
9
|
// Mock dependencies
|
|
6
10
|
vi.mock('@lobechat/utils', () => ({
|
|
@@ -48,7 +52,7 @@ vi.mock('@/store/chat/selectors', () => ({
|
|
|
48
52
|
},
|
|
49
53
|
}));
|
|
50
54
|
|
|
51
|
-
vi.mock('
|
|
55
|
+
vi.mock('../GlobalAgentContextManager', () => ({
|
|
52
56
|
globalAgentContextManager: {
|
|
53
57
|
getContext: () => ({
|
|
54
58
|
homePath: '/Users/test',
|
|
@@ -330,3 +334,358 @@ describe('parsePlaceholderVariablesMessages', () => {
|
|
|
330
334
|
});
|
|
331
335
|
});
|
|
332
336
|
});
|
|
337
|
+
|
|
338
|
+
describe('parsePlaceholderVariables', () => {
|
|
339
|
+
beforeEach(() => {
|
|
340
|
+
vi.useFakeTimers();
|
|
341
|
+
vi.setSystemTime(new Date('2025-06-06T06:06:06.666Z'));
|
|
342
|
+
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
afterEach(() => {
|
|
346
|
+
vi.useRealTimers();
|
|
347
|
+
vi.restoreAllMocks();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('basic variable replacement', () => {
|
|
351
|
+
it('should replace a single variable', () => {
|
|
352
|
+
const text = 'Hello {{username}}!';
|
|
353
|
+
const result = parsePlaceholderVariables(text);
|
|
354
|
+
expect(result).toBe('Hello testuser!');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should replace multiple variables', () => {
|
|
358
|
+
const text = 'User: {{username}}, Email: {{email}}';
|
|
359
|
+
const result = parsePlaceholderVariables(text);
|
|
360
|
+
expect(result).toBe('User: testuser, Email: test@example.com');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should return original text if no variables present', () => {
|
|
364
|
+
const text = 'Hello world!';
|
|
365
|
+
const result = parsePlaceholderVariables(text);
|
|
366
|
+
expect(result).toBe('Hello world!');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should handle empty string', () => {
|
|
370
|
+
const result = parsePlaceholderVariables('');
|
|
371
|
+
expect(result).toBe('');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should preserve unknown variables', () => {
|
|
375
|
+
const text = 'Hello {{unknown_var}}!';
|
|
376
|
+
const result = parsePlaceholderVariables(text);
|
|
377
|
+
expect(result).toBe('Hello {{unknown_var}}!');
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('recursive depth handling', () => {
|
|
382
|
+
it('should handle default depth of 2', () => {
|
|
383
|
+
const text = 'Test {{username}}';
|
|
384
|
+
const result = parsePlaceholderVariables(text);
|
|
385
|
+
expect(result).toBe('Test testuser');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should handle custom depth', () => {
|
|
389
|
+
const text = 'Test {{username}}';
|
|
390
|
+
const result = parsePlaceholderVariables(text, 1);
|
|
391
|
+
expect(result).toBe('Test testuser');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should handle depth of 0', () => {
|
|
395
|
+
const text = 'Test {{username}}';
|
|
396
|
+
const result = parsePlaceholderVariables(text, 0);
|
|
397
|
+
// With depth 0, no replacements should occur
|
|
398
|
+
expect(result).toBe('Test {{username}}');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should stop early if no more replacements needed', () => {
|
|
402
|
+
const text = 'Static text';
|
|
403
|
+
const result = parsePlaceholderVariables(text, 10);
|
|
404
|
+
expect(result).toBe('Static text');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('special characters and edge cases', () => {
|
|
409
|
+
it('should handle variables with spaces', () => {
|
|
410
|
+
const text = 'Hello {{ username }}!';
|
|
411
|
+
const result = parsePlaceholderVariables(text);
|
|
412
|
+
// The regex trims spaces, so this should work
|
|
413
|
+
expect(result).toBe('Hello testuser!');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should handle consecutive variables', () => {
|
|
417
|
+
const text = '{{username}}{{email}}';
|
|
418
|
+
const result = parsePlaceholderVariables(text);
|
|
419
|
+
expect(result).toBe('testusertest@example.com');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should handle variables at start and end', () => {
|
|
423
|
+
const text = '{{username}} middle {{email}}';
|
|
424
|
+
const result = parsePlaceholderVariables(text);
|
|
425
|
+
expect(result).toBe('testuser middle test@example.com');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should handle malformed brackets', () => {
|
|
429
|
+
const text = '{username} or {{{username}}}';
|
|
430
|
+
const result = parsePlaceholderVariables(text);
|
|
431
|
+
// Only {{username}} should be replaced
|
|
432
|
+
expect(result).toContain('{username}');
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe('VARIABLE_GENERATORS', () => {
|
|
438
|
+
describe('time-related variables', () => {
|
|
439
|
+
beforeEach(() => {
|
|
440
|
+
vi.useFakeTimers();
|
|
441
|
+
vi.setSystemTime(new Date('2025-06-06T06:06:06.666Z'));
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
afterEach(() => {
|
|
445
|
+
vi.useRealTimers();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should generate year', () => {
|
|
449
|
+
expect(VARIABLE_GENERATORS.year()).toBe('2025');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should generate month with padding', () => {
|
|
453
|
+
expect(VARIABLE_GENERATORS.month()).toBe('06');
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should generate day with padding', () => {
|
|
457
|
+
expect(VARIABLE_GENERATORS.day()).toBe('06');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should generate hour with padding', () => {
|
|
461
|
+
expect(VARIABLE_GENERATORS.hour()).toBe('06');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should generate minute with padding', () => {
|
|
465
|
+
expect(VARIABLE_GENERATORS.minute()).toBe('06');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should generate second with padding', () => {
|
|
469
|
+
expect(VARIABLE_GENERATORS.second()).toBe('06');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should generate ISO timestamp', () => {
|
|
473
|
+
expect(VARIABLE_GENERATORS.iso()).toBe('2025-06-06T06:06:06.666Z');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should generate timestamp', () => {
|
|
477
|
+
const result = VARIABLE_GENERATORS.timestamp();
|
|
478
|
+
expect(result).toBe(Date.now().toString());
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should generate date string', () => {
|
|
482
|
+
const result = VARIABLE_GENERATORS.date();
|
|
483
|
+
expect(result).toBeTruthy();
|
|
484
|
+
expect(typeof result).toBe('string');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should generate time string', () => {
|
|
488
|
+
const result = VARIABLE_GENERATORS.time();
|
|
489
|
+
expect(result).toBeTruthy();
|
|
490
|
+
expect(typeof result).toBe('string');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('should generate datetime string', () => {
|
|
494
|
+
const result = VARIABLE_GENERATORS.datetime();
|
|
495
|
+
expect(result).toBeTruthy();
|
|
496
|
+
expect(typeof result).toBe('string');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should generate weekday', () => {
|
|
500
|
+
const result = VARIABLE_GENERATORS.weekday();
|
|
501
|
+
expect(result).toBe('Friday');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should generate locale', () => {
|
|
505
|
+
const result = VARIABLE_GENERATORS.locale();
|
|
506
|
+
expect(result).toBeTruthy();
|
|
507
|
+
expect(typeof result).toBe('string');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should generate timezone', () => {
|
|
511
|
+
const result = VARIABLE_GENERATORS.timezone();
|
|
512
|
+
expect(result).toBeTruthy();
|
|
513
|
+
expect(typeof result).toBe('string');
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe('random value variables', () => {
|
|
518
|
+
beforeEach(() => {
|
|
519
|
+
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
afterEach(() => {
|
|
523
|
+
vi.restoreAllMocks();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should generate random number', () => {
|
|
527
|
+
expect(VARIABLE_GENERATORS.random()).toBe('500001');
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('should generate random boolean', () => {
|
|
531
|
+
expect(VARIABLE_GENERATORS.random_bool()).toBe('false');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('should generate random float', () => {
|
|
535
|
+
expect(VARIABLE_GENERATORS.random_float()).toBe('50.00');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should generate random integer', () => {
|
|
539
|
+
expect(VARIABLE_GENERATORS.random_int()).toBe('51');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should generate random hex color', () => {
|
|
543
|
+
const result = VARIABLE_GENERATORS.random_hex();
|
|
544
|
+
// Math.floor(0.5 * 16777215) = 8388607 = 0x7fffff
|
|
545
|
+
expect(result).toBe('7fffff');
|
|
546
|
+
expect(result.length).toBe(6);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('should generate random string', () => {
|
|
550
|
+
const result = VARIABLE_GENERATORS.random_string();
|
|
551
|
+
expect(result).toBeTruthy();
|
|
552
|
+
expect(typeof result).toBe('string');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('should generate random digit', () => {
|
|
556
|
+
expect(VARIABLE_GENERATORS.random_digit()).toBe('5');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should generate different random booleans', () => {
|
|
560
|
+
vi.spyOn(Math, 'random').mockReturnValue(0.6);
|
|
561
|
+
expect(VARIABLE_GENERATORS.random_bool()).toBe('true');
|
|
562
|
+
|
|
563
|
+
vi.spyOn(Math, 'random').mockReturnValue(0.4);
|
|
564
|
+
expect(VARIABLE_GENERATORS.random_bool()).toBe('false');
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
describe('UUID variables', () => {
|
|
569
|
+
it('should generate full UUID', () => {
|
|
570
|
+
expect(VARIABLE_GENERATORS.uuid()).toBe('mocked-uuid-12345');
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should generate short UUID', () => {
|
|
574
|
+
expect(VARIABLE_GENERATORS.uuid_short()).toBe('mocked');
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
describe('user information variables', () => {
|
|
579
|
+
it('should get username', () => {
|
|
580
|
+
expect(VARIABLE_GENERATORS.username()).toBe('testuser');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should get nickname', () => {
|
|
584
|
+
expect(VARIABLE_GENERATORS.nickname()).toBe('Test User');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should get email', () => {
|
|
588
|
+
expect(VARIABLE_GENERATORS.email()).toBe('test@example.com');
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
describe('model information variables', () => {
|
|
593
|
+
it('should get current model', () => {
|
|
594
|
+
expect(VARIABLE_GENERATORS.model()).toBe('gpt-4');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should get current provider', () => {
|
|
598
|
+
expect(VARIABLE_GENERATORS.provider()).toBe('openai');
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
describe('desktop path variables', () => {
|
|
603
|
+
it('should get home path', () => {
|
|
604
|
+
expect(VARIABLE_GENERATORS.homePath()).toBe('/Users/test');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should get desktop path', () => {
|
|
608
|
+
expect(VARIABLE_GENERATORS.desktopPath()).toBe('/Users/test/Desktop');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should get documents path', () => {
|
|
612
|
+
expect(VARIABLE_GENERATORS.documentsPath()).toBe('/Users/test/Documents');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('should get downloads path', () => {
|
|
616
|
+
expect(VARIABLE_GENERATORS.downloadsPath()).toBe('/Users/test/Downloads');
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should return empty string for missing music path', () => {
|
|
620
|
+
expect(VARIABLE_GENERATORS.musicPath()).toBe('');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('should return empty string for missing pictures path', () => {
|
|
624
|
+
expect(VARIABLE_GENERATORS.picturesPath()).toBe('');
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('should return empty string for missing videos path', () => {
|
|
628
|
+
expect(VARIABLE_GENERATORS.videosPath()).toBe('');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('should return empty string for missing userData path', () => {
|
|
632
|
+
expect(VARIABLE_GENERATORS.userDataPath()).toBe('');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('should return default message for working directory when not specified', () => {
|
|
636
|
+
const result = VARIABLE_GENERATORS.workingDirectory();
|
|
637
|
+
expect(result).toBe('(not specified, use user Desktop directory as default)');
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
describe('platform variables', () => {
|
|
642
|
+
const originalNavigator = global.navigator;
|
|
643
|
+
|
|
644
|
+
beforeEach(() => {
|
|
645
|
+
Object.defineProperty(global, 'navigator', {
|
|
646
|
+
writable: true,
|
|
647
|
+
configurable: true,
|
|
648
|
+
value: {
|
|
649
|
+
language: 'en-US',
|
|
650
|
+
platform: 'MacIntel',
|
|
651
|
+
userAgent:
|
|
652
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
afterEach(() => {
|
|
658
|
+
Object.defineProperty(global, 'navigator', {
|
|
659
|
+
writable: true,
|
|
660
|
+
configurable: true,
|
|
661
|
+
value: originalNavigator,
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should get language', () => {
|
|
666
|
+
expect(VARIABLE_GENERATORS.language()).toBe('en-US');
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should get platform', () => {
|
|
670
|
+
expect(VARIABLE_GENERATORS.platform()).toBe('MacIntel');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('should get user agent', () => {
|
|
674
|
+
const result = VARIABLE_GENERATORS.user_agent();
|
|
675
|
+
expect(result).toContain('Mozilla/5.0');
|
|
676
|
+
expect(result).toContain('Chrome/132.0.0.0');
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('should return empty string when navigator is undefined', () => {
|
|
680
|
+
Object.defineProperty(global, 'navigator', {
|
|
681
|
+
writable: true,
|
|
682
|
+
configurable: true,
|
|
683
|
+
value: undefined,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
expect(VARIABLE_GENERATORS.language()).toBe('');
|
|
687
|
+
expect(VARIABLE_GENERATORS.platform()).toBe('');
|
|
688
|
+
expect(VARIABLE_GENERATORS.user_agent()).toBe('');
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
});
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
} from '@lobechat/memory-user-memory';
|
|
16
16
|
import { LayersEnum, type SearchMemoryResult, searchMemorySchema } from '@lobechat/types';
|
|
17
17
|
import { type SQL, and, asc, eq, gte, lte } from 'drizzle-orm';
|
|
18
|
-
import { ModelProvider } from 'model-bank';
|
|
19
18
|
import pMap from 'p-map';
|
|
20
19
|
import { z } from 'zod';
|
|
21
20
|
|
|
@@ -135,11 +134,14 @@ const searchUserMemories = async (
|
|
|
135
134
|
};
|
|
136
135
|
|
|
137
136
|
const getEmbeddingRuntime = async (serverDB: LobeChatDatabase, userId: string) => {
|
|
138
|
-
const provider
|
|
139
|
-
// Read user's provider config from database
|
|
140
|
-
const agentRuntime = await initModelRuntimeFromDB(serverDB, userId, provider);
|
|
141
|
-
const { model: embeddingModel } =
|
|
137
|
+
const { provider, model: embeddingModel } =
|
|
142
138
|
getServerDefaultFilesConfig().embeddingModel || DEFAULT_USER_MEMORY_EMBEDDING_MODEL_ITEM;
|
|
139
|
+
// Read user's provider config from database
|
|
140
|
+
const agentRuntime = await initModelRuntimeFromDB(
|
|
141
|
+
serverDB,
|
|
142
|
+
userId,
|
|
143
|
+
ENABLE_BUSINESS_FEATURES ? BRANDING_PROVIDER : provider,
|
|
144
|
+
);
|
|
143
145
|
|
|
144
146
|
return { agentRuntime, embeddingModel };
|
|
145
147
|
};
|