@lobehub/chat 1.91.3 → 1.92.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/CHANGELOG.md +41 -0
- package/changelog/v1.json +15 -0
- package/locales/ar/setting.json +1 -1
- package/locales/bg-BG/setting.json +1 -1
- package/locales/de-DE/setting.json +1 -1
- package/locales/en-US/setting.json +1 -1
- package/locales/es-ES/setting.json +1 -1
- package/locales/fa-IR/setting.json +1 -1
- package/locales/fr-FR/setting.json +1 -1
- package/locales/it-IT/setting.json +1 -1
- package/locales/ja-JP/setting.json +1 -1
- package/locales/ko-KR/setting.json +1 -1
- package/locales/nl-NL/setting.json +1 -1
- package/locales/pl-PL/setting.json +1 -1
- package/locales/pt-BR/setting.json +1 -1
- package/locales/ru-RU/setting.json +1 -1
- package/locales/tr-TR/setting.json +1 -1
- package/locales/vi-VN/setting.json +1 -1
- package/locales/zh-CN/setting.json +1 -1
- package/locales/zh-TW/setting.json +1 -1
- package/package.json +1 -1
- package/src/features/AgentSetting/AgentModal/index.tsx +3 -2
- package/src/features/ChatInput/ActionBar/Search/Controls.tsx +6 -2
- package/src/libs/model-runtime/utils/streams/vertex-ai.ts +12 -0
- package/src/locales/default/setting.ts +1 -1
- package/src/services/chat.ts +17 -9
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +23 -31
- package/src/store/user/slices/auth/selectors.test.ts +18 -0
- package/src/store/user/slices/auth/selectors.ts +1 -0
- package/src/utils/client/parserPlaceholder.test.ts +326 -0
- package/src/utils/client/parserPlaceholder.ts +190 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,47 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
## [Version 1.92.0](https://github.com/lobehub/lobe-chat/compare/v1.91.3...v1.92.0)
|
6
|
+
|
7
|
+
<sup>Released on **2025-06-06**</sup>
|
8
|
+
|
9
|
+
#### ✨ Features
|
10
|
+
|
11
|
+
- **misc**: Support placeholder variables in prompts and input.
|
12
|
+
|
13
|
+
#### 🐛 Bug Fixes
|
14
|
+
|
15
|
+
- **misc**: Some web search bugs.
|
16
|
+
|
17
|
+
#### 💄 Styles
|
18
|
+
|
19
|
+
- **misc**: Support Vertex AI thought summaries.
|
20
|
+
|
21
|
+
<br/>
|
22
|
+
|
23
|
+
<details>
|
24
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
25
|
+
|
26
|
+
#### What's improved
|
27
|
+
|
28
|
+
- **misc**: Support placeholder variables in prompts and input, closes [#8060](https://github.com/lobehub/lobe-chat/issues/8060) ([3752739](https://github.com/lobehub/lobe-chat/commit/3752739))
|
29
|
+
|
30
|
+
#### What's fixed
|
31
|
+
|
32
|
+
- **misc**: Some web search bugs, closes [#8068](https://github.com/lobehub/lobe-chat/issues/8068) ([bebe7a3](https://github.com/lobehub/lobe-chat/commit/bebe7a3))
|
33
|
+
|
34
|
+
#### Styles
|
35
|
+
|
36
|
+
- **misc**: Support Vertex AI thought summaries, closes [#8090](https://github.com/lobehub/lobe-chat/issues/8090) ([1355a2e](https://github.com/lobehub/lobe-chat/commit/1355a2e))
|
37
|
+
|
38
|
+
</details>
|
39
|
+
|
40
|
+
<div align="right">
|
41
|
+
|
42
|
+
[](#readme-top)
|
43
|
+
|
44
|
+
</div>
|
45
|
+
|
5
46
|
### [Version 1.91.3](https://github.com/lobehub/lobe-chat/compare/v1.91.2...v1.91.3)
|
6
47
|
|
7
48
|
<sup>Released on **2025-06-05**</sup>
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,19 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"features": [
|
5
|
+
"Support placeholder variables in prompts and input."
|
6
|
+
],
|
7
|
+
"fixes": [
|
8
|
+
"Some web search bugs."
|
9
|
+
],
|
10
|
+
"improvements": [
|
11
|
+
"Support Vertex AI thought summaries."
|
12
|
+
]
|
13
|
+
},
|
14
|
+
"date": "2025-06-06",
|
15
|
+
"version": "1.92.0"
|
16
|
+
},
|
2
17
|
{
|
3
18
|
"children": {
|
4
19
|
"fixes": [
|
package/locales/ar/setting.json
CHANGED
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "سيتم ملء أحدث رسالة من المستخدم في هذا القالب",
|
223
|
-
"placeholder": "القالب المُعالج مسبقًا {{
|
223
|
+
"placeholder": "القالب المُعالج مسبقًا {{input_template}} سيتم استبداله بالمعلومات المُدخلة في الوقت الحقيقي",
|
224
224
|
"title": "معالجة مُدخلات المستخدم"
|
225
225
|
},
|
226
226
|
"submit": "تحديث تفضيلات الدردشة",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "Последното съобщение на потребителя ще бъде попълнено в този шаблон",
|
223
|
-
"placeholder": "Шаблонът за предварителна обработка {{
|
223
|
+
"placeholder": "Шаблонът за предварителна обработка {{input_template}} ще бъде заменен с информация за въвеждане в реално време",
|
224
224
|
"title": "Предварителна обработка на потребителския вход"
|
225
225
|
},
|
226
226
|
"submit": "Актуализиране на предпочитанията за чат",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "Die neueste Benutzernachricht wird in dieses Template eingefügt",
|
223
|
-
"placeholder": "Vorlagen-{{
|
223
|
+
"placeholder": "Vorlagen-{{input_template}} werden durch Echtzeit-Eingabeinformationen ersetzt",
|
224
224
|
"title": "Benutzereingabe-Vorverarbeitung"
|
225
225
|
},
|
226
226
|
"submit": "Chat-Präferenzen aktualisieren",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "The user's latest message will be filled into this template",
|
223
|
-
"placeholder": "Preprocessing template {{
|
223
|
+
"placeholder": "Preprocessing template {{input_template}} will be replaced with real-time input information",
|
224
224
|
"title": "User Input Preprocessing"
|
225
225
|
},
|
226
226
|
"submit": "Update Chat Preferences",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "El último mensaje del usuario se completará en esta plantilla",
|
223
|
-
"placeholder": "La plantilla de preprocesamiento {{
|
223
|
+
"placeholder": "La plantilla de preprocesamiento {{input_template}} se reemplazará por la información de entrada en tiempo real",
|
224
224
|
"title": "Preprocesamiento de entrada del usuario"
|
225
225
|
},
|
226
226
|
"submit": "Actualizar preferencias de chat",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "آخرین پیام کاربر در این قالب پر میشود",
|
223
|
-
"placeholder": "قالب پیشپردازش {{
|
223
|
+
"placeholder": "قالب پیشپردازش {{input_template}} با اطلاعات ورودی لحظهای جایگزین میشود",
|
224
224
|
"title": "پیشپردازش ورودی کاربر"
|
225
225
|
},
|
226
226
|
"submit": "بهروزرسانی ترجیحات چت",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "Le dernier message de l'utilisateur sera rempli dans ce modèle",
|
223
|
-
"placeholder": "Le modèle de prétraitement {{
|
223
|
+
"placeholder": "Le modèle de prétraitement {{input_template}} sera remplacé par les informations d'entrée en temps réel",
|
224
224
|
"title": "Modèle de prétraitement de l'entrée utilisateur"
|
225
225
|
},
|
226
226
|
"submit": "Mettre à jour les préférences de chat",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "Il template verrà popolato con l'ultimo messaggio dell'utente",
|
223
|
-
"placeholder": "Il modello di input {{
|
223
|
+
"placeholder": "Il modello di input {{input_template}} verrà sostituito con le informazioni in tempo reale",
|
224
224
|
"title": "Pre-elaborazione dell'input dell'utente"
|
225
225
|
},
|
226
226
|
"submit": "Aggiorna preferenze chat",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "ユーザーの最新メッセージがこのテンプレートに埋め込まれます",
|
223
|
-
"placeholder": "入力テンプレート {{
|
223
|
+
"placeholder": "入力テンプレート {{input_template}} はリアルタイムの入力情報に置き換えられます",
|
224
224
|
"title": "ユーザー入力のプリプロセス"
|
225
225
|
},
|
226
226
|
"submit": "チャットの好みを更新",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "사용자의 최신 메시지가이 템플릿에 채워집니다",
|
223
|
-
"placeholder": "입력 템플릿 {{
|
223
|
+
"placeholder": "입력 템플릿 {{input_template}}은 실시간 입력 정보로 대체됩니다",
|
224
224
|
"title": "사용자 입력 전처리"
|
225
225
|
},
|
226
226
|
"submit": "채팅 선호도 업데이트",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "De meest recente gebruikersboodschap wordt ingevuld in dit sjabloon",
|
223
|
-
"placeholder": "Voorbewerkingssjabloon {{
|
223
|
+
"placeholder": "Voorbewerkingssjabloon {{input_template}} wordt vervangen door realtime invoer",
|
224
224
|
"title": "Voorbewerking van gebruikersinvoer"
|
225
225
|
},
|
226
226
|
"submit": "Chatvoorkeuren bijwerken",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "Ostatnia wiadomość użytkownika zostanie wypełniona w tym szablonie",
|
223
|
-
"placeholder": "Szablon wejściowy {{
|
223
|
+
"placeholder": "Szablon wejściowy {{input_template}} zostanie zastąpiony rzeczywistą wiadomością",
|
224
224
|
"title": "Szablon wejściowy"
|
225
225
|
},
|
226
226
|
"submit": "Zaktualizuj preferencje czatu",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "A última mensagem do usuário será preenchida neste modelo",
|
223
|
-
"placeholder": "O modelo de pré-processamento {{
|
223
|
+
"placeholder": "O modelo de pré-processamento {{input_template}} será substituído pela entrada em tempo real",
|
224
224
|
"title": "Pré-processamento de entrada do usuário"
|
225
225
|
},
|
226
226
|
"submit": "Atualizar preferências de chat",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "Последнее сообщение пользователя будет использовано в этом шаблоне",
|
223
|
-
"placeholder": "Шаблон ввода {{
|
223
|
+
"placeholder": "Шаблон ввода {{input_template}} будет заменен на реальные данные",
|
224
224
|
"title": "Шаблон ввода пользователя"
|
225
225
|
},
|
226
226
|
"submit": "Обновить предпочтения чата",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "Kullanıcının son mesajı bu şablona doldurulur",
|
223
|
-
"placeholder": "Ön işleme şablonu {{
|
223
|
+
"placeholder": "Ön işleme şablonu {{input_template}}, gerçek zamanlı giriş bilgileri ile değiştirilir",
|
224
224
|
"title": "Kullanıcı Girişi Ön İşleme"
|
225
225
|
},
|
226
226
|
"submit": "Sohbet tercihlerini güncelle",
|
@@ -220,7 +220,7 @@
|
|
220
220
|
},
|
221
221
|
"inputTemplate": {
|
222
222
|
"desc": "Tin nhắn mới nhất của người dùng sẽ được điền vào mẫu này",
|
223
|
-
"placeholder": "Mẫu xử lý trước {{
|
223
|
+
"placeholder": "Mẫu xử lý trước {{input_template}} sẽ được thay thế bằng thông tin nhập thời gian thực",
|
224
224
|
"title": "Mẫu xử lý đầu vào của người dùng"
|
225
225
|
},
|
226
226
|
"submit": "Cập nhật sở thích trò chuyện",
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.92.0",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot 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",
|
@@ -1,7 +1,7 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
3
|
import { Form, type FormGroupItemType, SliderWithInput } from '@lobehub/ui';
|
4
|
-
import { Switch } from 'antd';
|
4
|
+
import { Form as AntdForm, Switch } from 'antd';
|
5
5
|
import isEqual from 'fast-deep-equal';
|
6
6
|
import { memo } from 'react';
|
7
7
|
import { useTranslation } from 'react-i18next';
|
@@ -15,6 +15,7 @@ import { selectors, useStore } from '../store';
|
|
15
15
|
const AgentModal = memo(() => {
|
16
16
|
const { t } = useTranslation('setting');
|
17
17
|
const [form] = Form.useForm();
|
18
|
+
const enableMaxTokens = AntdForm.useWatch(['chatConfig', 'enableMaxTokens'], form);
|
18
19
|
const config = useStore(selectors.currentAgentConfig, isEqual);
|
19
20
|
|
20
21
|
const updateConfig = useStore((s) => s.setAgentConfig);
|
@@ -69,7 +70,7 @@ const AgentModal = memo(() => {
|
|
69
70
|
children: <SliderWithInput max={32_000} min={0} step={100} unlimitedInput={true} />,
|
70
71
|
desc: t('settingModel.maxTokens.desc'),
|
71
72
|
divider: false,
|
72
|
-
hidden: !
|
73
|
+
hidden: !enableMaxTokens,
|
73
74
|
label: t('settingModel.maxTokens.title'),
|
74
75
|
name: ['params', 'max_tokens'],
|
75
76
|
tag: 'max_tokens',
|
@@ -9,7 +9,7 @@ import { Center, Flexbox } from 'react-layout-kit';
|
|
9
9
|
|
10
10
|
import { useAgentStore } from '@/store/agent';
|
11
11
|
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/slices/chat';
|
12
|
-
import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
|
12
|
+
import { aiModelSelectors, aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
13
13
|
import { SearchMode } from '@/types/search';
|
14
14
|
|
15
15
|
import FCSearchModel from './FCSearchModel';
|
@@ -99,6 +99,9 @@ const Controls = memo(() => {
|
|
99
99
|
]);
|
100
100
|
|
101
101
|
const supportFC = useAiInfraStore(aiModelSelectors.isModelSupportToolUse(model, provider));
|
102
|
+
const isProviderHasBuiltinSearchConfig = useAiInfraStore(
|
103
|
+
aiProviderSelectors.isProviderHasBuiltinSearchConfig(provider),
|
104
|
+
);
|
102
105
|
const isModelHasBuiltinSearchConfig = useAiInfraStore(
|
103
106
|
aiModelSelectors.isModelHasBuiltinSearchConfig(model, provider),
|
104
107
|
);
|
@@ -119,6 +122,7 @@ const Controls = memo(() => {
|
|
119
122
|
];
|
120
123
|
|
121
124
|
const showDivider = isModelHasBuiltinSearchConfig || !supportFC;
|
125
|
+
const showModelBuiltinSearch = isModelHasBuiltinSearchConfig || isProviderHasBuiltinSearchConfig;
|
122
126
|
|
123
127
|
return (
|
124
128
|
<Flexbox gap={4}>
|
@@ -126,7 +130,7 @@ const Controls = memo(() => {
|
|
126
130
|
<Item {...option} key={option.value} />
|
127
131
|
))}
|
128
132
|
{showDivider && <Divider style={{ margin: 0 }} />}
|
129
|
-
{
|
133
|
+
{showModelBuiltinSearch && <ModelBuiltinSearch />}
|
130
134
|
{!supportFC && <FCSearchModel />}
|
131
135
|
</Flexbox>
|
132
136
|
);
|
@@ -48,6 +48,18 @@ const transformVertexAIStream = (
|
|
48
48
|
);
|
49
49
|
}
|
50
50
|
|
51
|
+
if (
|
52
|
+
candidate && // 首先检查是否为 reasoning 内容 (thought: true)
|
53
|
+
Array.isArray(candidate.content.parts) &&
|
54
|
+
candidate.content.parts.length > 0
|
55
|
+
) {
|
56
|
+
for (const part of candidate.content.parts) {
|
57
|
+
if (part && part.text && (part as any).thought === true) {
|
58
|
+
return { data: part.text, id: context.id, type: 'reasoning' };
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
51
63
|
const candidates = chunk.candidates;
|
52
64
|
if (!candidates)
|
53
65
|
return {
|
package/src/services/chat.ts
CHANGED
@@ -41,6 +41,7 @@ import { createErrorResponse } from '@/utils/errorResponse';
|
|
41
41
|
import { FetchSSEOptions, fetchSSE, getMessageError } from '@/utils/fetch';
|
42
42
|
import { genToolCallingName } from '@/utils/toolCall';
|
43
43
|
import { createTraceHeader, getTraceId } from '@/utils/trace';
|
44
|
+
import { parsePlaceholderVariablesMessages } from '@/utils/client/parserPlaceholder';
|
44
45
|
|
45
46
|
import { createHeaderWithAuth, createPayloadWithKeyVaults } from './_auth';
|
46
47
|
import { API_ENDPOINTS } from './_url';
|
@@ -172,14 +173,18 @@ class ChatService {
|
|
172
173
|
|
173
174
|
// =================== 0. process search =================== //
|
174
175
|
const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
|
175
|
-
|
176
|
+
const aiInfraStoreState = getAiInfraStoreState();
|
176
177
|
const enabledSearch = chatConfig.searchMode !== 'off';
|
178
|
+
const isProviderHasBuiltinSearch = aiProviderSelectors.isProviderHasBuiltinSearch(
|
179
|
+
payload.provider!,
|
180
|
+
)(aiInfraStoreState);
|
177
181
|
const isModelHasBuiltinSearch = aiModelSelectors.isModelHasBuiltinSearch(
|
178
182
|
payload.model,
|
179
183
|
payload.provider!,
|
180
|
-
)(
|
184
|
+
)(aiInfraStoreState);
|
181
185
|
|
182
|
-
const useModelSearch =
|
186
|
+
const useModelSearch =
|
187
|
+
(isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && chatConfig.useModelBuiltinSearch;
|
183
188
|
|
184
189
|
const useApplicationBuiltinSearchTool = enabledSearch && !useModelSearch;
|
185
190
|
|
@@ -189,11 +194,14 @@ class ChatService {
|
|
189
194
|
pluginIds.push(WebBrowsingManifest.identifier);
|
190
195
|
}
|
191
196
|
|
192
|
-
// ============ 1. preprocess
|
197
|
+
// ============ 1. preprocess placeholder variables ============ //
|
198
|
+
const parsedMessages = parsePlaceholderVariablesMessages(messages);
|
199
|
+
|
200
|
+
// ============ 2. preprocess messages ============ //
|
193
201
|
|
194
202
|
const oaiMessages = this.processMessages(
|
195
203
|
{
|
196
|
-
messages,
|
204
|
+
messages: parsedMessages,
|
197
205
|
model: payload.model,
|
198
206
|
provider: payload.provider!,
|
199
207
|
tools: pluginIds,
|
@@ -201,28 +209,28 @@ class ChatService {
|
|
201
209
|
options,
|
202
210
|
);
|
203
211
|
|
204
|
-
// ============
|
212
|
+
// ============ 3. preprocess tools ============ //
|
205
213
|
|
206
214
|
const tools = this.prepareTools(pluginIds, {
|
207
215
|
model: payload.model,
|
208
216
|
provider: payload.provider!,
|
209
217
|
});
|
210
218
|
|
211
|
-
// ============
|
219
|
+
// ============ 4. process extend params ============ //
|
212
220
|
|
213
221
|
let extendParams: Record<string, any> = {};
|
214
222
|
|
215
223
|
const isModelHasExtendParams = aiModelSelectors.isModelHasExtendParams(
|
216
224
|
payload.model,
|
217
225
|
payload.provider!,
|
218
|
-
)(
|
226
|
+
)(aiInfraStoreState);
|
219
227
|
|
220
228
|
// model
|
221
229
|
if (isModelHasExtendParams) {
|
222
230
|
const modelExtendParams = aiModelSelectors.modelExtendParams(
|
223
231
|
payload.model,
|
224
232
|
payload.provider!,
|
225
|
-
)(
|
233
|
+
)(aiInfraStoreState);
|
226
234
|
// if model has extended params, then we need to check if the model can use reasoning
|
227
235
|
|
228
236
|
if (modelExtendParams!.includes('enableReasoning')) {
|
@@ -1,7 +1,6 @@
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
2
2
|
// Disable the auto sort key eslint rule to make the code more logic and readable
|
3
3
|
import { produce } from 'immer';
|
4
|
-
import { template } from 'lodash-es';
|
5
4
|
import { StateCreator } from 'zustand/vanilla';
|
6
5
|
|
7
6
|
import { LOADING_FLAT, MESSAGE_CANCEL_FLAT } from '@/const/message';
|
@@ -13,7 +12,7 @@ import { messageService } from '@/services/message';
|
|
13
12
|
import { useAgentStore } from '@/store/agent';
|
14
13
|
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
|
15
14
|
import { getAgentStoreState } from '@/store/agent/store';
|
16
|
-
import { aiModelSelectors } from '@/store/aiInfra';
|
15
|
+
import { aiModelSelectors, aiProviderSelectors } from '@/store/aiInfra';
|
17
16
|
import { getAiInfraStoreState } from '@/store/aiInfra/store';
|
18
17
|
import { chatHelpers } from '@/store/chat/helpers';
|
19
18
|
import { ChatStore } from '@/store/chat/store';
|
@@ -299,7 +298,8 @@ export const generateAIChat: StateCreator<
|
|
299
298
|
// create a new array to avoid the original messages array change
|
300
299
|
const messages = [...originalMessages];
|
301
300
|
|
302
|
-
const
|
301
|
+
const agentStoreState = getAgentStoreState();
|
302
|
+
const { model, provider, chatConfig } = agentSelectors.currentAgentConfig(agentStoreState);
|
303
303
|
|
304
304
|
let fileChunks: MessageSemanticSearchChunk[] | undefined;
|
305
305
|
let ragQueryId;
|
@@ -323,7 +323,7 @@ export const generateAIChat: StateCreator<
|
|
323
323
|
chunks,
|
324
324
|
userQuery: lastMsg.content,
|
325
325
|
rewriteQuery,
|
326
|
-
knowledge: agentSelectors.currentEnabledKnowledge(
|
326
|
+
knowledge: agentSelectors.currentEnabledKnowledge(agentStoreState),
|
327
327
|
});
|
328
328
|
|
329
329
|
// 3. add the retrieve context messages to the messages history
|
@@ -355,14 +355,25 @@ export const generateAIChat: StateCreator<
|
|
355
355
|
if (!assistantId) return;
|
356
356
|
|
357
357
|
// 3. place a search with the search working model if this model is not support tool use
|
358
|
+
const aiInfraStoreState = getAiInfraStoreState();
|
358
359
|
const isModelSupportToolUse = aiModelSelectors.isModelSupportToolUse(
|
359
360
|
model,
|
360
361
|
provider!,
|
361
|
-
)(
|
362
|
-
const
|
362
|
+
)(aiInfraStoreState);
|
363
|
+
const isProviderHasBuiltinSearch = aiProviderSelectors.isProviderHasBuiltinSearch(provider!)(
|
364
|
+
aiInfraStoreState,
|
365
|
+
);
|
366
|
+
const isModelHasBuiltinSearch = aiModelSelectors.isModelHasBuiltinSearch(
|
367
|
+
model,
|
368
|
+
provider!,
|
369
|
+
)(aiInfraStoreState);
|
370
|
+
const useModelBuiltinSearch = agentChatConfigSelectors.useModelBuiltinSearch(agentStoreState);
|
371
|
+
const useModelSearch =
|
372
|
+
(isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && useModelBuiltinSearch;
|
373
|
+
const isAgentEnableSearch = agentChatConfigSelectors.isAgentEnableSearch(agentStoreState);
|
363
374
|
|
364
|
-
if (isAgentEnableSearch && !isModelSupportToolUse) {
|
365
|
-
const { model, provider } = agentChatConfigSelectors.searchFCModel(
|
375
|
+
if (isAgentEnableSearch && !useModelSearch && !isModelSupportToolUse) {
|
376
|
+
const { model, provider } = agentChatConfigSelectors.searchFCModel(agentStoreState);
|
366
377
|
|
367
378
|
let isToolsCalling = false;
|
368
379
|
let isError = false;
|
@@ -460,10 +471,10 @@ export const generateAIChat: StateCreator<
|
|
460
471
|
}
|
461
472
|
|
462
473
|
// 6. summary history if context messages is larger than historyCount
|
463
|
-
const historyCount = agentChatConfigSelectors.historyCount(
|
474
|
+
const historyCount = agentChatConfigSelectors.historyCount(agentStoreState);
|
464
475
|
|
465
476
|
if (
|
466
|
-
agentChatConfigSelectors.enableHistoryCount(
|
477
|
+
agentChatConfigSelectors.enableHistoryCount(agentStoreState) &&
|
467
478
|
chatConfig.enableCompressHistory &&
|
468
479
|
originalMessages.length > historyCount
|
469
480
|
) {
|
@@ -495,8 +506,6 @@ export const generateAIChat: StateCreator<
|
|
495
506
|
const agentConfig = agentSelectors.currentAgentConfig(getAgentStoreState());
|
496
507
|
const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
|
497
508
|
|
498
|
-
const compiler = template(chatConfig.inputTemplate, { interpolate: /{{([\S\s]+?)}}/g });
|
499
|
-
|
500
509
|
// ================================== //
|
501
510
|
// messages uniformly preprocess //
|
502
511
|
// ================================== //
|
@@ -511,29 +520,12 @@ export const generateAIChat: StateCreator<
|
|
511
520
|
historyCount,
|
512
521
|
});
|
513
522
|
|
514
|
-
// 2.
|
515
|
-
preprocessMsgs = !chatConfig.inputTemplate
|
516
|
-
? preprocessMsgs
|
517
|
-
: preprocessMsgs.map((m) => {
|
518
|
-
if (m.role === 'user') {
|
519
|
-
try {
|
520
|
-
return { ...m, content: compiler({ text: m.content }) };
|
521
|
-
} catch (error) {
|
522
|
-
console.error(error);
|
523
|
-
|
524
|
-
return m;
|
525
|
-
}
|
526
|
-
}
|
527
|
-
|
528
|
-
return m;
|
529
|
-
});
|
530
|
-
|
531
|
-
// 3. add systemRole
|
523
|
+
// 2. add systemRole
|
532
524
|
if (agentConfig.systemRole) {
|
533
525
|
preprocessMsgs.unshift({ content: agentConfig.systemRole, role: 'system' } as ChatMessage);
|
534
526
|
}
|
535
527
|
|
536
|
-
//
|
528
|
+
// 3. handle max_tokens
|
537
529
|
agentConfig.params.max_tokens = chatConfig.enableMaxTokens
|
538
530
|
? agentConfig.params.max_tokens
|
539
531
|
: undefined;
|
@@ -33,6 +33,24 @@ afterEach(() => {
|
|
33
33
|
});
|
34
34
|
|
35
35
|
describe('userProfileSelectors', () => {
|
36
|
+
describe('fullName', () => {
|
37
|
+
it('should return user fullName if exist', () => {
|
38
|
+
const store: UserStore = {
|
39
|
+
user: { fullName: 'John Doe' },
|
40
|
+
} as UserStore;
|
41
|
+
|
42
|
+
expect(userProfileSelectors.fullName(store)).toBe('John Doe');
|
43
|
+
});
|
44
|
+
|
45
|
+
it('should return empty string if not exist', () => {
|
46
|
+
const store: UserStore = {
|
47
|
+
user: { fullName: undefined },
|
48
|
+
} as UserStore;
|
49
|
+
|
50
|
+
expect(userProfileSelectors.fullName(store)).toBe('');
|
51
|
+
});
|
52
|
+
});
|
53
|
+
|
36
54
|
describe('nickName', () => {
|
37
55
|
it('should return default nickname when auth is disabled and not desktop', () => {
|
38
56
|
enableAuth = false;
|
@@ -36,6 +36,7 @@ const username = (s: UserStore) => {
|
|
36
36
|
};
|
37
37
|
|
38
38
|
export const userProfileSelectors = {
|
39
|
+
fullName: (s: UserStore): string => s.user?.fullName || '',
|
39
40
|
nickName,
|
40
41
|
userAvatar: (s: UserStore): string => s.user?.avatar || '',
|
41
42
|
userId: (s: UserStore) => s.user?.id,
|
@@ -0,0 +1,326 @@
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
2
|
+
import { parsePlaceholderVariablesMessages, VARIABLE_GENERATORS } from './parserPlaceholder';
|
3
|
+
|
4
|
+
// Mock dependencies
|
5
|
+
vi.mock('@/utils/uuid', () => ({
|
6
|
+
uuid: () => 'mocked-uuid-12345'
|
7
|
+
}));
|
8
|
+
|
9
|
+
vi.mock('@/store/user', () => ({
|
10
|
+
useUserStore: {
|
11
|
+
getState: () => ({})
|
12
|
+
}
|
13
|
+
}));
|
14
|
+
|
15
|
+
vi.mock('@/store/user/selectors', () => ({
|
16
|
+
userProfileSelectors: {
|
17
|
+
nickName: () => 'Test User',
|
18
|
+
username: () => 'testuser',
|
19
|
+
fullName: () => 'Test Full Name'
|
20
|
+
}
|
21
|
+
}));
|
22
|
+
|
23
|
+
vi.mock('@/store/agent/store', () => ({
|
24
|
+
getAgentStoreState: () => ({})
|
25
|
+
}));
|
26
|
+
|
27
|
+
vi.mock('@/store/agent/selectors', () => ({
|
28
|
+
agentChatConfigSelectors: {
|
29
|
+
currentChatConfig: () => ({
|
30
|
+
inputTemplate: 'Hello {{username}}!'
|
31
|
+
})
|
32
|
+
}
|
33
|
+
}));
|
34
|
+
|
35
|
+
describe('parsePlaceholderVariablesMessages', () => {
|
36
|
+
beforeEach(() => {
|
37
|
+
// Mock Date for consistent testing
|
38
|
+
vi.useFakeTimers();
|
39
|
+
vi.setSystemTime(new Date('2025-06-06T06:06:06.666Z'));
|
40
|
+
|
41
|
+
// Mock Math.random for consistent random values
|
42
|
+
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
43
|
+
});
|
44
|
+
|
45
|
+
afterEach(() => {
|
46
|
+
vi.useRealTimers();
|
47
|
+
vi.restoreAllMocks();
|
48
|
+
});
|
49
|
+
|
50
|
+
describe('string content messages', () => {
|
51
|
+
it('should replace template variables in string content', () => {
|
52
|
+
const messages = [
|
53
|
+
{
|
54
|
+
id: '1',
|
55
|
+
content: 'Hello {{username}}, today is {{date}}'
|
56
|
+
}
|
57
|
+
];
|
58
|
+
|
59
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
60
|
+
|
61
|
+
expect(result[0].content).toContain('testuser');
|
62
|
+
expect(result[0].content).toContain(new Date().toLocaleDateString());
|
63
|
+
});
|
64
|
+
|
65
|
+
it('should handle multiple variables in one message', () => {
|
66
|
+
const messages = [
|
67
|
+
{
|
68
|
+
id: '1',
|
69
|
+
content: 'Time: {{time}}, Date: {{date}}, User: {{nickname}}'
|
70
|
+
}
|
71
|
+
];
|
72
|
+
|
73
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
74
|
+
|
75
|
+
expect(result[0].content).toContain('Test User');
|
76
|
+
expect(result[0].content).toMatch(/Time: .+, Date: .+, User: Test User/);
|
77
|
+
});
|
78
|
+
|
79
|
+
it('should preserve message structure when replacing variables', () => {
|
80
|
+
const messages = [
|
81
|
+
{
|
82
|
+
id: '1',
|
83
|
+
role: 'user',
|
84
|
+
content: 'Hello {{username}}'
|
85
|
+
}
|
86
|
+
];
|
87
|
+
|
88
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
89
|
+
|
90
|
+
expect(result[0]).toEqual({
|
91
|
+
id: '1',
|
92
|
+
role: 'user',
|
93
|
+
content: 'Hello testuser'
|
94
|
+
});
|
95
|
+
});
|
96
|
+
});
|
97
|
+
|
98
|
+
describe('array content messages', () => {
|
99
|
+
it('should replace variables in text type array elements', () => {
|
100
|
+
const messages = [
|
101
|
+
{
|
102
|
+
id: '1',
|
103
|
+
content: [
|
104
|
+
{
|
105
|
+
type: 'text',
|
106
|
+
text: 'Hello {{username}}'
|
107
|
+
},
|
108
|
+
{
|
109
|
+
type: 'image_url',
|
110
|
+
image_url: 'image.jpg'
|
111
|
+
}
|
112
|
+
]
|
113
|
+
}
|
114
|
+
];
|
115
|
+
|
116
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
117
|
+
|
118
|
+
expect(result[0].content[0].text).toBe('Hello testuser');
|
119
|
+
expect(result[0].content[1]).toEqual({
|
120
|
+
type: 'image_url',
|
121
|
+
image_url: 'image.jpg'
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
it('should handle multiple text elements with variables', () => {
|
126
|
+
const messages = [
|
127
|
+
{
|
128
|
+
id: '1',
|
129
|
+
content: [
|
130
|
+
{
|
131
|
+
type: 'text',
|
132
|
+
text: 'Date: {{date}}'
|
133
|
+
},
|
134
|
+
{
|
135
|
+
type: 'text',
|
136
|
+
text: 'Time: {{time}}'
|
137
|
+
},
|
138
|
+
{
|
139
|
+
type: 'image_url',
|
140
|
+
image_url: 'test.jpg'
|
141
|
+
}
|
142
|
+
]
|
143
|
+
}
|
144
|
+
];
|
145
|
+
|
146
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
147
|
+
|
148
|
+
expect(result[0].content[0].text).toContain(new Date().toLocaleDateString());
|
149
|
+
expect(result[0].content[1].text).toContain(new Date().toLocaleTimeString());
|
150
|
+
expect(result[0].content[2]).toEqual({
|
151
|
+
type: 'image_url',
|
152
|
+
image_url: 'test.jpg'
|
153
|
+
});
|
154
|
+
});
|
155
|
+
|
156
|
+
it('should preserve non-text array elements unchanged', () => {
|
157
|
+
const messages = [
|
158
|
+
{
|
159
|
+
id: '1',
|
160
|
+
content: [
|
161
|
+
{
|
162
|
+
type: 'image_url',
|
163
|
+
image_url: 'image.jpg',
|
164
|
+
},
|
165
|
+
{
|
166
|
+
type: 'image_url',
|
167
|
+
name: 'image2.jpg'
|
168
|
+
}
|
169
|
+
]
|
170
|
+
}
|
171
|
+
];
|
172
|
+
|
173
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
174
|
+
|
175
|
+
expect(result[0].content).toEqual([
|
176
|
+
{
|
177
|
+
type: 'image_url',
|
178
|
+
image_url: 'image.jpg'
|
179
|
+
},
|
180
|
+
{
|
181
|
+
type: 'image_url',
|
182
|
+
name: 'image2.jpg'
|
183
|
+
}
|
184
|
+
]);
|
185
|
+
});
|
186
|
+
});
|
187
|
+
|
188
|
+
describe('edge cases', () => {
|
189
|
+
it('should handle empty messages array', () => {
|
190
|
+
const result = parsePlaceholderVariablesMessages([]);
|
191
|
+
expect(result).toEqual([]);
|
192
|
+
});
|
193
|
+
|
194
|
+
it('should handle messages without content', () => {
|
195
|
+
const messages = [
|
196
|
+
{ id: '1' },
|
197
|
+
{ id: '2', content: null },
|
198
|
+
{ id: '3', content: undefined }
|
199
|
+
];
|
200
|
+
|
201
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
202
|
+
|
203
|
+
expect(result).toEqual([
|
204
|
+
{ id: '1' },
|
205
|
+
{ id: '2', content: null },
|
206
|
+
{ id: '3', content: undefined }
|
207
|
+
]);
|
208
|
+
});
|
209
|
+
|
210
|
+
it('should handle empty string content', () => {
|
211
|
+
const messages = [
|
212
|
+
{ id: '1', content: '' }
|
213
|
+
];
|
214
|
+
|
215
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
216
|
+
|
217
|
+
expect(result[0].content).toBe('');
|
218
|
+
});
|
219
|
+
|
220
|
+
it('should handle content without variables', () => {
|
221
|
+
const messages = [
|
222
|
+
{ id: '1', content: 'Hello world!' },
|
223
|
+
{
|
224
|
+
id: '2',
|
225
|
+
content: [
|
226
|
+
{ type: 'text', text: 'No variables here' },
|
227
|
+
{ type: 'image_url', image_url: 'test.jpg' }
|
228
|
+
]
|
229
|
+
}
|
230
|
+
];
|
231
|
+
|
232
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
233
|
+
|
234
|
+
expect(result[0].content).toBe('Hello world!');
|
235
|
+
expect(result[1].content[0].text).toBe('No variables here');
|
236
|
+
});
|
237
|
+
|
238
|
+
it('should handle unknown variable types', () => {
|
239
|
+
const messages = [
|
240
|
+
{ id: '1', content: 'Hello {{unknown_variable}}!' }
|
241
|
+
];
|
242
|
+
|
243
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
244
|
+
|
245
|
+
// Unknown variables should remain unchanged
|
246
|
+
expect(result[0].content).toBe('Hello {{unknown_variable}}!');
|
247
|
+
});
|
248
|
+
|
249
|
+
it('should handle nested variables (input_template)', () => {
|
250
|
+
const messages = [
|
251
|
+
{ id: '1', content: 'Template: {{input_template}}' }
|
252
|
+
];
|
253
|
+
|
254
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
255
|
+
|
256
|
+
// Should resolve nested variables in input_template
|
257
|
+
expect(result[0].content).toBe('Template: Hello testuser!');
|
258
|
+
});
|
259
|
+
});
|
260
|
+
|
261
|
+
describe('specific variable types', () => {
|
262
|
+
it('should handle time variables', () => {
|
263
|
+
const messages = [
|
264
|
+
{
|
265
|
+
id: '1',
|
266
|
+
content: 'Year: {{year}}, Month: {{month}}, Day: {{day}}'
|
267
|
+
}
|
268
|
+
];
|
269
|
+
|
270
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
271
|
+
|
272
|
+
expect(result[0].content).toContain('Year: 2025');
|
273
|
+
expect(result[0].content).toContain('Month: 06');
|
274
|
+
expect(result[0].content).toContain('Day: 06');
|
275
|
+
});
|
276
|
+
|
277
|
+
it('should handle random variables', () => {
|
278
|
+
const messages = [
|
279
|
+
{
|
280
|
+
id: '1',
|
281
|
+
content: 'Random: {{random}}, Bool: {{random_bool}}, UUID: {{uuid}}'
|
282
|
+
}
|
283
|
+
];
|
284
|
+
|
285
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
286
|
+
|
287
|
+
expect(result[0].content).toContain('Random: 500001'); // Math.random() * 1000000 + 1 with 0.5
|
288
|
+
expect(result[0].content).toContain('Bool: false'); // Math.random() > 0.5 with 0.5
|
289
|
+
expect(result[0].content).toContain('UUID: mocked-uuid-12345');
|
290
|
+
});
|
291
|
+
|
292
|
+
it('should handle user variables', () => {
|
293
|
+
const messages = [
|
294
|
+
{
|
295
|
+
id: '1',
|
296
|
+
content: 'User: {{username}}, Nickname: {{nickname}}'
|
297
|
+
}
|
298
|
+
];
|
299
|
+
|
300
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
301
|
+
|
302
|
+
expect(result[0].content).toBe('User: testuser, Nickname: Test User');
|
303
|
+
});
|
304
|
+
});
|
305
|
+
|
306
|
+
describe('multiple messages', () => {
|
307
|
+
it('should process multiple messages correctly', () => {
|
308
|
+
const messages = [
|
309
|
+
{ id: '1', content: 'Hello {{username}}' },
|
310
|
+
{
|
311
|
+
id: '2',
|
312
|
+
content: [
|
313
|
+
{ type: 'text', text: 'Today is {{date}}' }
|
314
|
+
]
|
315
|
+
},
|
316
|
+
{ id: '3', content: 'Time: {{time}}' }
|
317
|
+
];
|
318
|
+
|
319
|
+
const result = parsePlaceholderVariablesMessages(messages);
|
320
|
+
|
321
|
+
expect(result[0].content).toBe('Hello testuser');
|
322
|
+
expect(result[1].content[0].text).toContain(new Date().toLocaleDateString());
|
323
|
+
expect(result[2].content).toContain(new Date().toLocaleTimeString());
|
324
|
+
});
|
325
|
+
});
|
326
|
+
});
|
@@ -0,0 +1,190 @@
|
|
1
|
+
import { template } from 'lodash-es';
|
2
|
+
|
3
|
+
import { uuid } from '@/utils/uuid';
|
4
|
+
|
5
|
+
import { useUserStore } from '@/store/user';
|
6
|
+
import { userProfileSelectors } from '@/store/user/selectors';
|
7
|
+
|
8
|
+
import { getAgentStoreState } from '@/store/agent/store';
|
9
|
+
import { agentChatConfigSelectors } from '@/store/agent/selectors';
|
10
|
+
|
11
|
+
const placeholderVariablesRegex = /{{(.*?)}}/g;
|
12
|
+
|
13
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
14
|
+
export const VARIABLE_GENERATORS = {
|
15
|
+
/**
|
16
|
+
* 时间类模板变量
|
17
|
+
*
|
18
|
+
* | Value | Example |
|
19
|
+
* |-------|---------|
|
20
|
+
* | `{{date}}` | 12/25/2023 |
|
21
|
+
* | `{{datetime}}` | 12/25/2023, 2:30:45 PM |
|
22
|
+
* | `{{day}}` | 25 |
|
23
|
+
* | `{{hour}}` | 14 |
|
24
|
+
* | `{{iso}}` | 2023-12-25T14:30:45.123Z |
|
25
|
+
* | `{{locale}}` | zh-CN |
|
26
|
+
* | `{{minute}}` | 30 |
|
27
|
+
* | `{{month}}` | 12 |
|
28
|
+
* | `{{second}}` | 45 |
|
29
|
+
* | `{{time}}` | 2:30:45 PM |
|
30
|
+
* | `{{timestamp}}` | 1703538645123 |
|
31
|
+
* | `{{timezone}}` | America/New_York |
|
32
|
+
* | `{{weekday}}` | Monday |
|
33
|
+
* | `{{year}}` | 2023 |
|
34
|
+
*
|
35
|
+
*/
|
36
|
+
date: () => new Date().toLocaleDateString(),
|
37
|
+
datetime: () => new Date().toLocaleString(),
|
38
|
+
day: () => new Date().getDate().toString().padStart(2, '0'),
|
39
|
+
hour: () => new Date().getHours().toString().padStart(2, '0'),
|
40
|
+
iso: () => new Date().toISOString(),
|
41
|
+
locale: () => Intl.DateTimeFormat().resolvedOptions().locale,
|
42
|
+
minute: () => new Date().getMinutes().toString().padStart(2, '0'),
|
43
|
+
month: () => (new Date().getMonth() + 1).toString().padStart(2, '0'),
|
44
|
+
second: () => new Date().getSeconds().toString().padStart(2, '0'),
|
45
|
+
time: () => new Date().toLocaleTimeString(),
|
46
|
+
timestamp: () => Date.now().toString(),
|
47
|
+
timezone: () => Intl.DateTimeFormat().resolvedOptions().timeZone,
|
48
|
+
weekday: () => new Date().toLocaleDateString('en-US', { weekday: 'long' }),
|
49
|
+
year: () => new Date().getFullYear().toString(),
|
50
|
+
|
51
|
+
/**
|
52
|
+
* 用户信息类模板变量
|
53
|
+
*
|
54
|
+
* | Value | Example |
|
55
|
+
* |-------|---------|
|
56
|
+
* | `{{nickname}}` | 社区版用户 |
|
57
|
+
* | `{{username}}` | LobeChat |
|
58
|
+
*
|
59
|
+
*/
|
60
|
+
nickname: () => userProfileSelectors.nickName(useUserStore.getState()) ?? '',
|
61
|
+
username: () => userProfileSelectors.username(useUserStore.getState()) ?? userProfileSelectors.fullName(useUserStore.getState()) ?? '',
|
62
|
+
|
63
|
+
/**
|
64
|
+
* 随机值类模板变量
|
65
|
+
*
|
66
|
+
* | Value | Example |
|
67
|
+
* |-------|---------|
|
68
|
+
* | `{{random}}` | 100041 |
|
69
|
+
* | `{{random_bool}}` | true |
|
70
|
+
* | `{{random_float}}` | 76.02 |
|
71
|
+
* | `{{random_hex}}` | de0dbd |
|
72
|
+
* | `{{random_int}}` | 68 |
|
73
|
+
* | `{{random_string}}` | wqn9zfrqe7h |
|
74
|
+
*
|
75
|
+
*/
|
76
|
+
random: () => Math.floor(Math.random() * 1_000_000 + 1).toString(),
|
77
|
+
random_bool: () => (Math.random() > 0.5 ? 'true' : 'false'),
|
78
|
+
random_float: () => (Math.random() * 100).toFixed(2),
|
79
|
+
random_hex: () => Math.floor(Math.random() * 16_777_215).toString(16).padStart(6, '0'),
|
80
|
+
random_int: () => Math.floor(Math.random() * 100 + 1).toString(),
|
81
|
+
random_string: () => Math.random().toString(36).slice(2, 15),
|
82
|
+
random_digit: () => Math.floor(Math.random() * 10).toString(),
|
83
|
+
|
84
|
+
/**
|
85
|
+
* UUID 类模板变量
|
86
|
+
*
|
87
|
+
* | Value | Example |
|
88
|
+
* |-------|---------|
|
89
|
+
* | `{{uuid}}` | dd90b35-669f-4e87-beb8-ac6877f6995d |
|
90
|
+
* | `{{uuid_short}}` | dd90b35 |
|
91
|
+
*
|
92
|
+
*/
|
93
|
+
uuid: () => uuid(),
|
94
|
+
uuid_short: () => uuid().split('-')[0],
|
95
|
+
|
96
|
+
/**
|
97
|
+
* 平台类模板变量
|
98
|
+
*
|
99
|
+
* | Value | Example |
|
100
|
+
* |-------|---------|
|
101
|
+
* | `{{language}}` | zh-CN |
|
102
|
+
* | `{{platform}}` | MacIntel |
|
103
|
+
* | `{{user_agent}}` | 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 Edg/132.0.0.0 |
|
104
|
+
*
|
105
|
+
*/
|
106
|
+
language: () => typeof navigator !== 'undefined' ? navigator.language : '',
|
107
|
+
platform: () => typeof navigator !== 'undefined' ? navigator.platform : '',
|
108
|
+
user_agent: () => typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
109
|
+
|
110
|
+
/**
|
111
|
+
* LobeChat 模板变量
|
112
|
+
*
|
113
|
+
* | Value | Example |
|
114
|
+
* |-------|---------|
|
115
|
+
* | `{{input_template}}` | Some contents |
|
116
|
+
*
|
117
|
+
*/
|
118
|
+
input_template: () => agentChatConfigSelectors.currentChatConfig(getAgentStoreState()).inputTemplate || '',
|
119
|
+
} as Record<string, () => string>;
|
120
|
+
|
121
|
+
/**
|
122
|
+
* 从文本中提取所有 {{variable}} 占位符的变量名
|
123
|
+
* @param text 包含模板变量的字符串
|
124
|
+
* @returns 变量名数组,如 ['date', 'nickname']
|
125
|
+
*/
|
126
|
+
const extractPlaceholderVariables = (text: string): string[] => {
|
127
|
+
const matches = [...text.matchAll(placeholderVariablesRegex)];
|
128
|
+
return matches.map(m => m[1].trim());
|
129
|
+
};
|
130
|
+
|
131
|
+
/**
|
132
|
+
* 将模板变量替换为实际值,并支持递归解析嵌套变量
|
133
|
+
* @param text - 含变量的原始文本
|
134
|
+
* @param depth - 递归深度,默认 1,设置更高可支持 {{input_template}} 中的 {{date}} 等
|
135
|
+
* @returns 替换后的文本
|
136
|
+
*/
|
137
|
+
export const parsePlaceholderVariables = (text: string, depth = 2): string => {
|
138
|
+
let result = text;
|
139
|
+
|
140
|
+
// 递归解析,用于处理如 {{input_template}} 存在额外预设变量
|
141
|
+
for (let i = 0; i < depth; i++) {
|
142
|
+
try {
|
143
|
+
const variables = Object.fromEntries(
|
144
|
+
extractPlaceholderVariables(result)
|
145
|
+
.map((key) => [key, VARIABLE_GENERATORS[key]?.()])
|
146
|
+
.filter(([, value]) => value !== undefined)
|
147
|
+
);
|
148
|
+
|
149
|
+
const replaced = template(result, { interpolate: placeholderVariablesRegex })(variables);
|
150
|
+
if (replaced === result) break;
|
151
|
+
|
152
|
+
result = replaced;
|
153
|
+
} catch {
|
154
|
+
break;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
return result;
|
159
|
+
};
|
160
|
+
|
161
|
+
/**
|
162
|
+
* 解析消息内容,替换占位符变量
|
163
|
+
* @param messages 原始消息数组
|
164
|
+
* @returns 处理后的消息数组
|
165
|
+
*/
|
166
|
+
export const parsePlaceholderVariablesMessages = (messages: any[]): any[] =>
|
167
|
+
messages.map(message => {
|
168
|
+
if (!message?.content) return message;
|
169
|
+
|
170
|
+
const { content } = message;
|
171
|
+
|
172
|
+
// 字符串类型直接处理
|
173
|
+
if (typeof content === 'string') {
|
174
|
+
return { ...message, content: parsePlaceholderVariables(content) };
|
175
|
+
}
|
176
|
+
|
177
|
+
// 数组类型处理其中的 text 元素
|
178
|
+
if (Array.isArray(content)) {
|
179
|
+
return {
|
180
|
+
...message,
|
181
|
+
content: content.map(item =>
|
182
|
+
item?.type === 'text'
|
183
|
+
? { ...item, text: parsePlaceholderVariables(item.text) }
|
184
|
+
: item
|
185
|
+
)
|
186
|
+
};
|
187
|
+
}
|
188
|
+
|
189
|
+
return message;
|
190
|
+
});
|