@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 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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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": [
@@ -220,7 +220,7 @@
220
220
  },
221
221
  "inputTemplate": {
222
222
  "desc": "سيتم ملء أحدث رسالة من المستخدم في هذا القالب",
223
- "placeholder": "القالب المُعالج مسبقًا {{text}} سيتم استبداله بالمعلومات المُدخلة في الوقت الحقيقي",
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": "Шаблонът за предварителна обработка {{text}} ще бъде заменен с информация за въвеждане в реално време",
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-{{text}} werden durch Echtzeit-Eingabeinformationen ersetzt",
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 {{text}} will be replaced with real-time input information",
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 {{text}} se reemplazará por la información de entrada en tiempo real",
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": "قالب پیش‌پردازش {{text}} با اطلاعات ورودی لحظه‌ای جایگزین می‌شود",
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 {{text}} sera remplacé par les informations d'entrée en temps réel",
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 {{text}} verrà sostituito con le informazioni in tempo reale",
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": "入力テンプレート {{text}} はリアルタイムの入力情報に置き換えられます",
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": "입력 템플릿 {{text}}은 실시간 입력 정보로 대체됩니다",
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 {{text}} wordt vervangen door realtime invoer",
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 {{text}} zostanie zastąpiony rzeczywistą wiadomością",
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 {{text}} será substituído pela entrada em tempo real",
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": "Шаблон ввода {{text}} будет заменен на реальные данные",
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 {{text}}, gerçek zamanlı giriş bilgileri ile değiştirilir",
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 {{text}} sẽ được thay thế bằng thông tin nhập thời gian thự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",
@@ -220,7 +220,7 @@
220
220
  },
221
221
  "inputTemplate": {
222
222
  "desc": "用户最新的一条消息会填充到此模板",
223
- "placeholder": "预处理模版 {{text}} 将替换为实时输入信息",
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": "預處理模板 {{text}} 將替換為實時輸入資訊",
223
+ "placeholder": "預處理模板 {{input_template}} 將替換為實時輸入資訊",
224
224
  "title": "使用者輸入預處理"
225
225
  },
226
226
  "submit": "更新聊天偏好",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.91.3",
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: !config.chatConfig.enableMaxTokens,
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
- {isModelHasBuiltinSearchConfig && <ModelBuiltinSearch />}
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 {
@@ -224,7 +224,7 @@ export default {
224
224
  },
225
225
  inputTemplate: {
226
226
  desc: '用户最新的一条消息会填充到此模板',
227
- placeholder: '预处理模版 {{text}} 将替换为实时输入信息',
227
+ placeholder: '预处理模版 {{input_template}} 将替换为实时输入信息',
228
228
  title: '用户输入预处理',
229
229
  },
230
230
  submit: '更新聊天偏好',
@@ -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
- )(getAiInfraStoreState());
184
+ )(aiInfraStoreState);
181
185
 
182
- const useModelSearch = isModelHasBuiltinSearch && chatConfig.useModelBuiltinSearch;
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 messages ============ //
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
- // ============ 2. preprocess tools ============ //
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
- // ============ 3. process extend params ============ //
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
- )(getAiInfraStoreState());
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
- )(getAiInfraStoreState());
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 { model, provider, chatConfig } = agentSelectors.currentAgentConfig(getAgentStoreState());
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(getAgentStoreState()),
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
- )(getAiInfraStoreState());
362
- const isAgentEnableSearch = agentChatConfigSelectors.isAgentEnableSearch(getAgentStoreState());
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(getAgentStoreState());
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(getAgentStoreState());
474
+ const historyCount = agentChatConfigSelectors.historyCount(agentStoreState);
464
475
 
465
476
  if (
466
- agentChatConfigSelectors.enableHistoryCount(getAgentStoreState()) &&
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. replace inputMessage template
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
- // 4. handle max_tokens
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
+ });