@lobehub/chat 1.120.7 → 1.121.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.
Files changed (32) hide show
  1. package/.cursor/rules/project-structure.mdc +54 -42
  2. package/.cursor/rules/testing-guide/testing-guide.mdc +28 -17
  3. package/.env.development +122 -0
  4. package/.vscode/settings.json +0 -1
  5. package/CHANGELOG.md +25 -0
  6. package/CLAUDE.md +3 -4
  7. package/changelog/v1.json +9 -0
  8. package/docker-compose/local/init_data.json +981 -1024
  9. package/docker-compose.development.yml +40 -0
  10. package/docs/development/basic/work-with-server-side-database.mdx +77 -0
  11. package/docs/development/basic/work-with-server-side-database.zh-CN.mdx +77 -0
  12. package/docs/self-hosting/advanced/s3/cloudflare-r2.mdx +1 -1
  13. package/docs/self-hosting/advanced/s3/cloudflare-r2.zh-CN.mdx +2 -2
  14. package/locales/zh-CN/common.json +7 -0
  15. package/package.json +2 -1
  16. package/packages/database/src/repositories/aiInfra/index.ts +3 -1
  17. package/packages/model-runtime/src/RouterRuntime/createRuntime.test.ts +6 -91
  18. package/packages/model-runtime/src/RouterRuntime/createRuntime.ts +6 -28
  19. package/packages/model-runtime/src/openrouter/index.ts +15 -12
  20. package/packages/model-runtime/src/openrouter/type.ts +10 -0
  21. package/packages/model-runtime/src/utils/modelParse.test.ts +66 -0
  22. package/packages/model-runtime/src/utils/modelParse.ts +15 -3
  23. package/packages/model-runtime/src/utils/postProcessModelList.ts +1 -0
  24. package/packages/utils/src/detectChinese.test.ts +37 -0
  25. package/packages/utils/src/detectChinese.ts +12 -0
  26. package/packages/utils/src/index.ts +1 -0
  27. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +33 -18
  28. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +12 -0
  29. package/src/features/ChatInput/useSend.ts +14 -2
  30. package/src/hooks/useGeminiChineseWarning.tsx +91 -0
  31. package/src/locales/default/common.ts +7 -0
  32. package/src/store/global/initialState.ts +2 -0
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Detect if text contains Chinese characters
3
+ * @param text - The text to check
4
+ * @returns true if text contains Chinese characters, false otherwise
5
+ */
6
+ export const containsChinese = (text: string): boolean => {
7
+ // Enhanced regex to cover more Chinese character ranges:
8
+ // \u4e00-\u9fa5: CJK Unified Ideographs (basic)
9
+ // \u3400-\u4dbf: CJK Unified Ideographs Extension A
10
+ // \uf900-\ufaff: CJK Compatibility Ideographs
11
+ return /[\u3400-\u4DBF\u4E00-\u9FA5\uF900-\uFAFF]/.test(text);
12
+ };
@@ -1,4 +1,5 @@
1
1
  export * from './client/cookie';
2
+ export * from './detectChinese';
2
3
  export * from './format';
3
4
  export * from './imageToBase64';
4
5
  export * from './parseModels';
@@ -7,10 +7,27 @@ import { useUserStore } from '@/store/user';
7
7
 
8
8
  import InputArea from './TextArea';
9
9
 
10
+ let sendMessageMock: () => Promise<void>;
11
+
12
+ // Mock the useSendMessage hook to return our mock function
13
+ vi.mock('@/features/ChatInput/useSend', () => ({
14
+ useSendMessage: () => ({
15
+ send: sendMessageMock,
16
+ canSend: true,
17
+ }),
18
+ }));
19
+
20
+ // Mock the Chinese warning hook to always allow sending
21
+ vi.mock('@/hooks/useGeminiChineseWarning', () => ({
22
+ useGeminiChineseWarning: () => () => Promise.resolve(true),
23
+ }));
24
+
10
25
  let onSendMock: () => void;
11
26
 
12
27
  beforeEach(() => {
13
28
  onSendMock = vi.fn();
29
+ sendMessageMock = vi.fn().mockResolvedValue(undefined);
30
+ vi.clearAllMocks();
14
31
  });
15
32
 
16
33
  describe('<InputArea />', () => {
@@ -194,9 +211,8 @@ describe('<InputArea />', () => {
194
211
 
195
212
  describe('message sending behavior', () => {
196
213
  it('does not send message when loading or shift key is pressed', () => {
197
- const sendMessageMock = vi.fn();
198
214
  act(() => {
199
- useChatStore.setState({ chatLoadingIds: ['123'], sendMessage: sendMessageMock });
215
+ useChatStore.setState({ chatLoadingIds: ['123'] });
200
216
  });
201
217
 
202
218
  render(<InputArea onSend={onSendMock} />);
@@ -206,13 +222,11 @@ describe('<InputArea />', () => {
206
222
  expect(sendMessageMock).not.toHaveBeenCalled();
207
223
  });
208
224
 
209
- it('sends message on Enter press when not loading and no shift key', () => {
210
- const sendMessageMock = vi.fn();
225
+ it('sends message on Enter press when not loading and no shift key', async () => {
211
226
  act(() => {
212
227
  useChatStore.setState({
213
228
  chatLoadingIds: [],
214
229
  inputMessage: 'abc',
215
- sendMessage: sendMessageMock,
216
230
  });
217
231
  });
218
232
 
@@ -221,17 +235,18 @@ describe('<InputArea />', () => {
221
235
  fireEvent.change(textArea, { target: { value: 'Test message' } });
222
236
 
223
237
  fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });
224
- expect(sendMessageMock).toHaveBeenCalled();
238
+
239
+ await vi.waitFor(() => {
240
+ expect(sendMessageMock).toHaveBeenCalled();
241
+ });
225
242
  });
226
243
 
227
244
  describe('metaKey behavior for sending messages', () => {
228
- it('windows: sends message on ctrl + enter when useCmdEnterToSend is true', () => {
229
- const sendMessageMock = vi.fn();
245
+ it('windows: sends message on ctrl + enter when useCmdEnterToSend is true', async () => {
230
246
  act(() => {
231
247
  useChatStore.setState({
232
248
  chatLoadingIds: [],
233
249
  inputMessage: '123',
234
- sendMessage: sendMessageMock,
235
250
  });
236
251
  useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
237
252
  });
@@ -240,17 +255,18 @@ describe('<InputArea />', () => {
240
255
  const textArea = screen.getByRole('textbox');
241
256
 
242
257
  fireEvent.keyDown(textArea, { code: 'Enter', ctrlKey: true, key: 'Enter' });
243
- expect(sendMessageMock).toHaveBeenCalled();
258
+
259
+ await vi.waitFor(() => {
260
+ expect(sendMessageMock).toHaveBeenCalled();
261
+ });
244
262
  });
245
263
 
246
264
  it('windows: inserts a new line on ctrl + enter when useCmdEnterToSend is false', () => {
247
- const sendMessageMock = vi.fn();
248
265
  const updateInputMessageMock = vi.fn();
249
266
  act(() => {
250
267
  useChatStore.setState({
251
268
  chatLoadingIds: [],
252
269
  inputMessage: 'Test',
253
- sendMessage: sendMessageMock,
254
270
  updateInputMessage: updateInputMessageMock,
255
271
  });
256
272
  useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
@@ -264,17 +280,15 @@ describe('<InputArea />', () => {
264
280
  expect(sendMessageMock).not.toHaveBeenCalled(); // sendMessage should not be called
265
281
  });
266
282
 
267
- it('macOS: sends message on cmd + enter when useCmdEnterToSend is true', () => {
283
+ it('macOS: sends message on cmd + enter when useCmdEnterToSend is true', async () => {
268
284
  vi.stubGlobal('navigator', {
269
285
  userAgent:
270
286
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
271
287
  });
272
- const sendMessageMock = vi.fn();
273
288
  act(() => {
274
289
  useChatStore.setState({
275
290
  chatLoadingIds: [],
276
291
  inputMessage: '123',
277
- sendMessage: sendMessageMock,
278
292
  });
279
293
  useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
280
294
  });
@@ -283,7 +297,10 @@ describe('<InputArea />', () => {
283
297
  const textArea = screen.getByRole('textbox');
284
298
 
285
299
  fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', metaKey: true });
286
- expect(sendMessageMock).toHaveBeenCalled();
300
+
301
+ await vi.waitFor(() => {
302
+ expect(sendMessageMock).toHaveBeenCalled();
303
+ });
287
304
  vi.restoreAllMocks();
288
305
  });
289
306
 
@@ -292,13 +309,11 @@ describe('<InputArea />', () => {
292
309
  userAgent:
293
310
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
294
311
  });
295
- const sendMessageMock = vi.fn();
296
312
  const updateInputMessageMock = vi.fn();
297
313
  act(() => {
298
314
  useChatStore.setState({
299
315
  chatLoadingIds: [],
300
316
  inputMessage: 'Test',
301
- sendMessage: sendMessageMock,
302
317
  updateInputMessage: updateInputMessageMock,
303
318
  });
304
319
  useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
@@ -8,9 +8,11 @@ import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
10
10
  import { loginRequired } from '@/components/Error/loginRequiredNotification';
11
+ import { useGeminiChineseWarning } from '@/hooks/useGeminiChineseWarning';
11
12
  import { useImageStore } from '@/store/image';
12
13
  import { createImageSelectors } from '@/store/image/selectors';
13
14
  import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
15
+ import { imageGenerationConfigSelectors } from '@/store/image/slices/generationConfig/selectors';
14
16
  import { useUserStore } from '@/store/user';
15
17
  import { authSelectors } from '@/store/user/slices/auth/selectors';
16
18
 
@@ -49,13 +51,23 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
49
51
  const { value, setValue } = useGenerationConfigParam('prompt');
50
52
  const isCreating = useImageStore(createImageSelectors.isCreating);
51
53
  const createImage = useImageStore((s) => s.createImage);
54
+ const currentModel = useImageStore(imageGenerationConfigSelectors.model);
52
55
  const isLogin = useUserStore(authSelectors.isLogin);
56
+ const checkGeminiChineseWarning = useGeminiChineseWarning();
53
57
 
54
58
  const handleGenerate = async () => {
55
59
  if (!isLogin) {
56
60
  loginRequired.redirect({ timeout: 2000 });
57
61
  return;
58
62
  }
63
+ // Check for Chinese text warning with Gemini model
64
+ const shouldContinue = await checkGeminiChineseWarning({
65
+ model: currentModel,
66
+ prompt: value,
67
+ scenario: 'image',
68
+ });
69
+
70
+ if (!shouldContinue) return;
59
71
 
60
72
  await createImage();
61
73
  };
@@ -1,6 +1,7 @@
1
1
  import { useAnalytics } from '@lobehub/analytics/react';
2
2
  import { useCallback, useMemo } from 'react';
3
3
 
4
+ import { useGeminiChineseWarning } from '@/hooks/useGeminiChineseWarning';
4
5
  import { getAgentStoreState } from '@/store/agent';
5
6
  import { agentSelectors } from '@/store/agent/selectors';
6
7
  import { useChatStore } from '@/store/chat';
@@ -20,6 +21,7 @@ export const useSendMessage = () => {
20
21
  s.updateInputMessage,
21
22
  ]);
22
23
  const { analytics } = useAnalytics();
24
+ const checkGeminiChineseWarning = useGeminiChineseWarning();
23
25
 
24
26
  const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
25
27
 
@@ -28,7 +30,7 @@ export const useSendMessage = () => {
28
30
 
29
31
  const canSend = !isUploadingFiles && !isSendButtonDisabledByMessage;
30
32
 
31
- const send = useCallback((params: UseSendMessageParams = {}) => {
33
+ const send = useCallback(async (params: UseSendMessageParams = {}) => {
32
34
  const store = useChatStore.getState();
33
35
  if (chatSelectors.isAIGenerating(store)) return;
34
36
 
@@ -45,6 +47,17 @@ export const useSendMessage = () => {
45
47
  // if there is no message and no image, then we should not send the message
46
48
  if (!store.inputMessage && fileList.length === 0) return;
47
49
 
50
+ // Check for Chinese text warning with Gemini model
51
+ const agentStore = getAgentStoreState();
52
+ const currentModel = agentSelectors.currentAgentModel(agentStore);
53
+ const shouldContinue = await checkGeminiChineseWarning({
54
+ model: currentModel,
55
+ prompt: store.inputMessage,
56
+ scenario: 'chat',
57
+ });
58
+
59
+ if (!shouldContinue) return;
60
+
48
61
  sendMessage({
49
62
  files: fileList,
50
63
  message: store.inputMessage,
@@ -56,7 +69,6 @@ export const useSendMessage = () => {
56
69
 
57
70
  // 获取分析数据
58
71
  const userStore = getUserStoreState();
59
- const agentStore = getAgentStoreState();
60
72
 
61
73
  // 直接使用现有数据结构判断消息类型
62
74
  const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
@@ -0,0 +1,91 @@
1
+ import { containsChinese } from '@lobechat/utils';
2
+ import { App, Checkbox } from 'antd';
3
+ import React, { useCallback } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { useGlobalStore } from '@/store/global';
7
+ import { systemStatusSelectors } from '@/store/global/selectors';
8
+
9
+ const shouldShowChineseWarning = (
10
+ model: string,
11
+ prompt: string,
12
+ hasWarningBeenDismissed: boolean,
13
+ ): boolean => {
14
+ return (
15
+ model.includes('gemini-2.5-flash-image-preview') &&
16
+ !hasWarningBeenDismissed &&
17
+ Boolean(prompt) &&
18
+ containsChinese(prompt)
19
+ );
20
+ };
21
+
22
+ interface UseGeminiChineseWarningOptions {
23
+ model: string;
24
+ prompt: string;
25
+ scenario?: 'chat' | 'image';
26
+ }
27
+
28
+ export const useGeminiChineseWarning = () => {
29
+ const { t } = useTranslation('common');
30
+ const { modal } = App.useApp();
31
+
32
+ const [hideGeminiChineseWarning, updateSystemStatus] = useGlobalStore((s) => [
33
+ systemStatusSelectors.systemStatus(s).hideGemini2_5FlashImagePreviewChineseWarning ?? false,
34
+ s.updateSystemStatus,
35
+ ]);
36
+
37
+ const checkWarning = useCallback(
38
+ async ({
39
+ model,
40
+ prompt,
41
+ scenario = 'chat',
42
+ }: UseGeminiChineseWarningOptions): Promise<boolean> => {
43
+ if (!shouldShowChineseWarning(model, prompt, hideGeminiChineseWarning)) {
44
+ return true;
45
+ }
46
+
47
+ return new Promise<boolean>((resolve) => {
48
+ let doNotShowAgain = false;
49
+
50
+ // 根据场景选择不同的按钮文案
51
+ const continueText =
52
+ scenario === 'image'
53
+ ? t('geminiImageChineseWarning.continueGenerate')
54
+ : t('geminiImageChineseWarning.continueSend');
55
+
56
+ modal.confirm({
57
+ cancelText: t('cancel', { ns: 'common' }),
58
+ centered: true,
59
+ content: (
60
+ <div>
61
+ <p>{t('geminiImageChineseWarning.content')}</p>
62
+ <div style={{ marginTop: 16 }}>
63
+ <Checkbox
64
+ onChange={(e) => {
65
+ doNotShowAgain = e.target.checked;
66
+ }}
67
+ >
68
+ {t('geminiImageChineseWarning.doNotShowAgain')}
69
+ </Checkbox>
70
+ </div>
71
+ </div>
72
+ ),
73
+ okText: continueText,
74
+ onCancel: () => {
75
+ resolve(false);
76
+ },
77
+ onOk: () => {
78
+ if (doNotShowAgain) {
79
+ updateSystemStatus({ hideGemini2_5FlashImagePreviewChineseWarning: true });
80
+ }
81
+ resolve(true);
82
+ },
83
+ title: t('geminiImageChineseWarning.title'),
84
+ });
85
+ });
86
+ },
87
+ [modal, t, hideGeminiChineseWarning, updateSystemStatus],
88
+ );
89
+
90
+ return checkWarning;
91
+ };
@@ -185,6 +185,13 @@ export default {
185
185
  title: '喜欢我们的产品?',
186
186
  },
187
187
  fullscreen: '全屏模式',
188
+ geminiImageChineseWarning: {
189
+ content: 'Nano Banana 使用中文有概率性生成图片失败。建议使用英文以获得更好的效果。',
190
+ continueGenerate: '继续生成',
191
+ continueSend: '继续发送',
192
+ doNotShowAgain: '不再提示',
193
+ title: '中文输入提示',
194
+ },
188
195
  historyRange: '历史范围',
189
196
  import: '导入',
190
197
  importData: '导入数据',
@@ -50,6 +50,7 @@ export interface SystemStatus {
50
50
  // which sessionGroup should expand
51
51
  expandSessionGroupKeys: string[];
52
52
  filePanelWidth: number;
53
+ hideGemini2_5FlashImagePreviewChineseWarning?: boolean;
53
54
  hidePWAInstaller?: boolean;
54
55
  hideThreadLimitAlert?: boolean;
55
56
  imagePanelWidth: number;
@@ -108,6 +109,7 @@ export interface GlobalState {
108
109
  export const INITIAL_STATUS = {
109
110
  expandSessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default],
110
111
  filePanelWidth: 320,
112
+ hideGemini2_5FlashImagePreviewChineseWarning: false,
111
113
  hidePWAInstaller: false,
112
114
  hideThreadLimitAlert: false,
113
115
  imagePanelWidth: 320,