@lobehub/chat 1.137.4 → 1.137.5

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,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.137.5](https://github.com/lobehub/lobe-chat/compare/v1.137.4...v1.137.5)
6
+
7
+ <sup>Released on **2025-10-14**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Add imagen model to vertex ai.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Add imagen model to vertex ai, closes [#9699](https://github.com/lobehub/lobe-chat/issues/9699) ([3b2a2c1](https://github.com/lobehub/lobe-chat/commit/3b2a2c1))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 1.137.4](https://github.com/lobehub/lobe-chat/compare/v1.137.3...v1.137.4)
6
31
 
7
32
  <sup>Released on **2025-10-14**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Add imagen model to vertex ai."
6
+ ]
7
+ },
8
+ "date": "2025-10-14",
9
+ "version": "1.137.5"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.137.4",
3
+ "version": "1.137.5",
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,5 +1,7 @@
1
1
  import { filesPrompts } from '@lobechat/prompts';
2
- import { imageUrlToBase64, isLocalUrl, parseDataUri } from '@lobechat/utils';
2
+ import { imageUrlToBase64 } from '@lobechat/utils/imageToBase64';
3
+ import { parseDataUri } from '@lobechat/utils/uriParser';
4
+ import { isDesktopLocalStaticServerUrl } from '@lobechat/utils/url';
3
5
  import debug from 'debug';
4
6
 
5
7
  import { BaseProcessor } from '../base/BaseProcessor';
@@ -277,7 +279,7 @@ export class MessageContentProcessor extends BaseProcessor {
277
279
  const { type } = parseDataUri(image.url);
278
280
 
279
281
  let processedUrl = image.url;
280
- if (type === 'url' && isLocalUrl(image.url)) {
282
+ if (type === 'url' && isDesktopLocalStaticServerUrl(image.url)) {
281
283
  const { base64, mimeType } = await imageUrlToBase64(image.url);
282
284
  processedUrl = `data:${mimeType};base64,${base64}`;
283
285
  }
@@ -4,19 +4,16 @@ import { describe, expect, it, vi } from 'vitest';
4
4
  import type { PipelineContext } from '../../types';
5
5
  import { MessageContentProcessor } from '../MessageContent';
6
6
 
7
- vi.mock('@lobechat/utils', () => ({
8
- imageUrlToBase64: vi.fn().mockResolvedValue({
9
- base64: 'base64-data',
10
- mimeType: 'image/png',
11
- }),
12
- isLocalUrl: vi.fn((url: string) => url.includes('localhost') || url.includes('127.0.0.1')),
13
- parseDataUri: vi.fn((url: string) => {
14
- if (url.startsWith('data:')) {
15
- return { type: 'data' };
16
- }
17
- return { type: 'url' };
18
- }),
19
- }));
7
+ vi.mock('@lobechat/utils/imageToBase64', async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import('@lobechat/utils/imageToBase64')>();
9
+ return {
10
+ ...actual,
11
+ imageUrlToBase64: vi.fn().mockResolvedValue({
12
+ base64: 'base64-data',
13
+ mimeType: 'image/png',
14
+ }),
15
+ };
16
+ });
20
17
 
21
18
  const createContext = (messages: ChatMessage[]): PipelineContext => ({
22
19
  initialState: { messages: [] } as any,
@@ -138,7 +135,7 @@ describe('MessageContentProcessor', () => {
138
135
  role: 'user',
139
136
  content: 'Hello',
140
137
  imageList: [
141
- { url: 'http://localhost:3000/image.jpg', alt: '', id: 'test' } as ChatImageItem,
138
+ { url: 'http://127.0.0.1:3000/image.jpg', alt: '', id: 'test' } as ChatImageItem,
142
139
  ],
143
140
  createdAt: Date.now(),
144
141
  updatedAt: Date.now(),
@@ -820,7 +820,7 @@ const googleChatModels: AIChatModelCard[] = [
820
820
  ];
821
821
 
822
822
  // Common parameters for Imagen models
823
- const imagenBaseParameters: ModelParamsSchema = {
823
+ export const imagenGenParameters: ModelParamsSchema = {
824
824
  aspectRatio: {
825
825
  default: '1:1',
826
826
  enum: ['1:1', '16:9', '9:16', '3:4', '4:3'],
@@ -841,7 +841,7 @@ const NANO_BANANA_ASPECT_RATIOS = [
841
841
  '21:9', // 1536x672
842
842
  ];
843
843
 
844
- const nanoBananaParameters: ModelParamsSchema = {
844
+ export const nanoBananaParameters: ModelParamsSchema = {
845
845
  aspectRatio: {
846
846
  default: '1:1',
847
847
  enum: NANO_BANANA_ASPECT_RATIOS,
@@ -895,7 +895,7 @@ const googleImageModels: AIImageModelCard[] = [
895
895
  description: 'Imagen 4th generation text-to-image model series',
896
896
  organization: 'Deepmind',
897
897
  releasedAt: '2025-08-15',
898
- parameters: imagenBaseParameters,
898
+ parameters: imagenGenParameters,
899
899
  pricing: {
900
900
  units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
901
901
  },
@@ -908,7 +908,7 @@ const googleImageModels: AIImageModelCard[] = [
908
908
  description: 'Imagen 4th generation text-to-image model series Ultra version',
909
909
  organization: 'Deepmind',
910
910
  releasedAt: '2025-08-15',
911
- parameters: imagenBaseParameters,
911
+ parameters: imagenGenParameters,
912
912
  pricing: {
913
913
  units: [{ name: 'imageGeneration', rate: 0.06, strategy: 'fixed', unit: 'image' }],
914
914
  },
@@ -921,7 +921,7 @@ const googleImageModels: AIImageModelCard[] = [
921
921
  description: 'Imagen 4th generation text-to-image model series Fast version',
922
922
  organization: 'Deepmind',
923
923
  releasedAt: '2025-08-15',
924
- parameters: imagenBaseParameters,
924
+ parameters: imagenGenParameters,
925
925
  pricing: {
926
926
  units: [{ name: 'imageGeneration', rate: 0.02, strategy: 'fixed', unit: 'image' }],
927
927
  },
@@ -933,7 +933,7 @@ const googleImageModels: AIImageModelCard[] = [
933
933
  description: 'Imagen 4th generation text-to-image model series',
934
934
  organization: 'Deepmind',
935
935
  releasedAt: '2024-06-06',
936
- parameters: imagenBaseParameters,
936
+ parameters: imagenGenParameters,
937
937
  pricing: {
938
938
  units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
939
939
  },
@@ -945,7 +945,7 @@ const googleImageModels: AIImageModelCard[] = [
945
945
  description: 'Imagen 4th generation text-to-image model series Ultra version',
946
946
  organization: 'Deepmind',
947
947
  releasedAt: '2025-06-11',
948
- parameters: imagenBaseParameters,
948
+ parameters: imagenGenParameters,
949
949
  pricing: {
950
950
  units: [{ name: 'imageGeneration', rate: 0.06, strategy: 'fixed', unit: 'image' }],
951
951
  },
@@ -1,4 +1,5 @@
1
- import { AIChatModelCard } from '../types/aiModel';
1
+ import { AIChatModelCard, AIImageModelCard } from '../types/aiModel';
2
+ import { imagenGenParameters, nanoBananaParameters } from './google';
2
3
 
3
4
  // ref: https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models
4
5
  const vertexaiChatModels: AIChatModelCard[] = [
@@ -278,6 +279,66 @@ const vertexaiChatModels: AIChatModelCard[] = [
278
279
  },
279
280
  ];
280
281
 
281
- export const allModels = [...vertexaiChatModels];
282
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
283
+ const vertexaiImageModels: AIImageModelCard[] = [
284
+ {
285
+ displayName: 'Nano Banana',
286
+ id: 'gemini-2.5-flash-image:image',
287
+ enabled: true,
288
+ type: 'image',
289
+ description:
290
+ 'Nano Banana 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
291
+ releasedAt: '2025-08-26',
292
+ parameters: nanoBananaParameters,
293
+ pricing: {
294
+ units: [
295
+ { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
296
+ { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
297
+ { name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
298
+ ],
299
+ },
300
+ },
301
+ {
302
+ displayName: 'Imagen 4',
303
+ id: 'imagen-4.0-generate-001',
304
+ enabled: true,
305
+ type: 'image',
306
+ description: 'Imagen 4th generation text-to-image model series',
307
+ organization: 'Deepmind',
308
+ releasedAt: '2025-08-15',
309
+ parameters: imagenGenParameters,
310
+ pricing: {
311
+ units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
312
+ },
313
+ },
314
+ {
315
+ displayName: 'Imagen 4 Ultra',
316
+ id: 'imagen-4.0-ultra-generate-001',
317
+ enabled: true,
318
+ type: 'image',
319
+ description: 'Imagen 4th generation text-to-image model series Ultra version',
320
+ organization: 'Deepmind',
321
+ releasedAt: '2025-08-15',
322
+ parameters: imagenGenParameters,
323
+ pricing: {
324
+ units: [{ name: 'imageGeneration', rate: 0.06, strategy: 'fixed', unit: 'image' }],
325
+ },
326
+ },
327
+ {
328
+ displayName: 'Imagen 4 Fast',
329
+ id: 'imagen-4.0-fast-generate-001',
330
+ enabled: true,
331
+ type: 'image',
332
+ description: 'Imagen 4th generation text-to-image model series Fast version',
333
+ organization: 'Deepmind',
334
+ releasedAt: '2025-08-15',
335
+ parameters: imagenGenParameters,
336
+ pricing: {
337
+ units: [{ name: 'imageGeneration', rate: 0.02, strategy: 'fixed', unit: 'image' }],
338
+ },
339
+ },
340
+ ];
341
+
342
+ export const allModels = [...vertexaiChatModels, ...vertexaiImageModels];
282
343
 
283
344
  export default allModels;
@@ -6,7 +6,8 @@
6
6
  ".": "./src/index.ts",
7
7
  "./server": "./src/server/index.ts",
8
8
  "./client": "./src/client/index.ts",
9
- "./object": "./src/object.ts"
9
+ "./object": "./src/object.ts",
10
+ "./*": "./src/*.ts"
10
11
  },
11
12
  "scripts": {
12
13
  "test": "vitest",
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { pathString } from './url';
4
3
  import {
5
4
  inferContentTypeFromImageUrl,
6
5
  inferFileExtensionFromImageUrl,
6
+ isDesktopLocalStaticServerUrl,
7
7
  isLocalOrPrivateUrl,
8
- isLocalUrl,
8
+ pathString,
9
9
  } from './url';
10
10
 
11
11
  describe('pathString', () => {
@@ -404,36 +404,36 @@ describe('inferFileExtensionFromImageUrl', () => {
404
404
  });
405
405
  });
406
406
 
407
- describe('isLocalUrl', () => {
407
+ describe('isDesktopLocalStaticServerUrl', () => {
408
408
  it('should return true for 127.0.0.1', () => {
409
- expect(isLocalUrl('http://127.0.0.1')).toBe(true);
410
- expect(isLocalUrl('https://127.0.0.1')).toBe(true);
411
- expect(isLocalUrl('http://127.0.0.1:8080')).toBe(true);
412
- expect(isLocalUrl('http://127.0.0.1/path/to/resource')).toBe(true);
413
- expect(isLocalUrl('https://127.0.0.1/path?query=1#hash')).toBe(true);
409
+ expect(isDesktopLocalStaticServerUrl('http://127.0.0.1')).toBe(true);
410
+ expect(isDesktopLocalStaticServerUrl('https://127.0.0.1')).toBe(true);
411
+ expect(isDesktopLocalStaticServerUrl('http://127.0.0.1:8080')).toBe(true);
412
+ expect(isDesktopLocalStaticServerUrl('http://127.0.0.1/path/to/resource')).toBe(true);
413
+ expect(isDesktopLocalStaticServerUrl('https://127.0.0.1/path?query=1#hash')).toBe(true);
414
414
  });
415
415
 
416
416
  it('should return false for other 127.x.x.x addresses', () => {
417
- expect(isLocalUrl('http://127.0.0.2')).toBe(false);
418
- expect(isLocalUrl('http://127.1.1.1')).toBe(false);
419
- expect(isLocalUrl('http://127.255.255.255')).toBe(false);
417
+ expect(isDesktopLocalStaticServerUrl('http://127.0.0.2')).toBe(false);
418
+ expect(isDesktopLocalStaticServerUrl('http://127.1.1.1')).toBe(false);
419
+ expect(isDesktopLocalStaticServerUrl('http://127.255.255.255')).toBe(false);
420
420
  });
421
421
 
422
422
  it('should return false for localhost', () => {
423
- expect(isLocalUrl('http://localhost')).toBe(false);
424
- expect(isLocalUrl('http://localhost:3000')).toBe(false);
425
- expect(isLocalUrl('https://localhost/api')).toBe(false);
423
+ expect(isDesktopLocalStaticServerUrl('http://localhost')).toBe(false);
424
+ expect(isDesktopLocalStaticServerUrl('http://localhost:3000')).toBe(false);
425
+ expect(isDesktopLocalStaticServerUrl('https://localhost/api')).toBe(false);
426
426
  });
427
427
 
428
428
  it('should return false for domain names', () => {
429
- expect(isLocalUrl('https://example.com')).toBe(false);
430
- expect(isLocalUrl('http://www.google.com')).toBe(false);
429
+ expect(isDesktopLocalStaticServerUrl('https://example.com')).toBe(false);
430
+ expect(isDesktopLocalStaticServerUrl('http://www.google.com')).toBe(false);
431
431
  });
432
432
 
433
433
  it('should return false for malformed URLs', () => {
434
- expect(isLocalUrl('invalid-url')).toBe(false);
435
- expect(isLocalUrl('http://')).toBe(false);
436
- expect(isLocalUrl('')).toBe(false);
434
+ expect(isDesktopLocalStaticServerUrl('invalid-url')).toBe(false);
435
+ expect(isDesktopLocalStaticServerUrl('http://')).toBe(false);
436
+ expect(isDesktopLocalStaticServerUrl('')).toBe(false);
437
437
  });
438
438
  });
439
439
 
@@ -125,25 +125,21 @@ export function inferContentTypeFromImageUrl(url: string) {
125
125
  }
126
126
 
127
127
  /**
128
- * Check if a URL points to localhost (127.0.0.1)
129
128
  *
130
- * This function safely determines if the provided URL's hostname is '127.0.0.1'.
131
- * It handles malformed URLs gracefully by returning false instead of throwing errors.
132
- *
133
- * @param url - The URL string to check
134
- * @returns true if the URL's hostname is '127.0.0.1', false otherwise (including for malformed URLs)
129
+ * Check if a URL points to desktop local static server
135
130
  *
136
131
  * @example
137
132
  * ```typescript
138
- * isLocalUrl('http://127.0.0.1:8080/path') // true
139
- * isLocalUrl('https://example.com') // false
140
- * isLocalUrl('invalid-url') // false (instead of throwing)
141
- * isLocalUrl('') // false (instead of throwing)
133
+ * isDesktopLocalStaticServerUrl('http://127.0.0.1:8080/path') // true
134
+ * isDesktopLocalStaticServerUrl('http://localhost:8080/path') // false
135
+ * isDesktopLocalStaticServerUrl('https://example.com') // false
136
+ * isDesktopLocalStaticServerUrl('invalid-url') // false (instead of throwing)
137
+ * isDesktopLocalStaticServerUrl('') // false (instead of throwing)
142
138
  * ```
143
139
  *
144
140
  * check: apps/desktop/src/main/core/StaticFileServerManager.ts
145
141
  */
146
- export function isLocalUrl(url: string) {
142
+ export function isDesktopLocalStaticServerUrl(url: string) {
147
143
  try {
148
144
  return new URL(url).hostname === '127.0.0.1';
149
145
  } catch {
@@ -37,9 +37,13 @@ vi.mock('@/utils/fetch', async (importOriginal) => {
37
37
 
38
38
  return { ...(module as any), getMessageError: vi.fn() };
39
39
  });
40
- vi.mock('@lobechat/utils', () => ({
41
- isLocalUrl: vi.fn(),
40
+ vi.mock('@lobechat/utils/url', () => ({
41
+ isDesktopLocalStaticServerUrl: vi.fn(),
42
+ }));
43
+ vi.mock('@lobechat/utils/imageToBase64', () => ({
42
44
  imageUrlToBase64: vi.fn(),
45
+ }));
46
+ vi.mock('@lobechat/utils/uriParser', () => ({
43
47
  parseDataUri: vi.fn(),
44
48
  }));
45
49
 
@@ -280,9 +284,10 @@ describe('ChatService', () => {
280
284
  describe('should handle content correctly for vision models', () => {
281
285
  it('should include image content when with vision model', async () => {
282
286
  // Mock utility functions used in processImageList
283
- const { parseDataUri, isLocalUrl } = await import('@lobechat/utils');
287
+ const { parseDataUri } = await import('@lobechat/utils/uriParser');
288
+ const { isDesktopLocalStaticServerUrl } = await import('@lobechat/utils/url');
284
289
  vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
285
- vi.mocked(isLocalUrl).mockReturnValue(false); // Not a local URL
290
+ vi.mocked(isDesktopLocalStaticServerUrl).mockReturnValue(false); // Not a local URL
286
291
 
287
292
  const messages = [
288
293
  {
@@ -357,11 +362,13 @@ describe('ChatService', () => {
357
362
 
358
363
  describe('local image URL conversion', () => {
359
364
  it('should convert local image URLs to base64 and call processImageList', async () => {
360
- const { imageUrlToBase64, parseDataUri, isLocalUrl } = await import('@lobechat/utils');
365
+ const { imageUrlToBase64 } = await import('@lobechat/utils/imageToBase64');
366
+ const { parseDataUri } = await import('@lobechat/utils/uriParser');
367
+ const { isDesktopLocalStaticServerUrl } = await import('@lobechat/utils/url');
361
368
 
362
369
  // Mock for local URL
363
370
  vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
364
- vi.mocked(isLocalUrl).mockReturnValue(true); // This is a local URL
371
+ vi.mocked(isDesktopLocalStaticServerUrl).mockReturnValue(true); // This is a local URL
365
372
  vi.mocked(imageUrlToBase64).mockResolvedValue({
366
373
  base64: 'converted-base64-content',
367
374
  mimeType: 'image/png',
@@ -397,7 +404,9 @@ describe('ChatService', () => {
397
404
 
398
405
  // Verify the utility functions were called
399
406
  expect(parseDataUri).toHaveBeenCalledWith('http://127.0.0.1:3000/uploads/image.png');
400
- expect(isLocalUrl).toHaveBeenCalledWith('http://127.0.0.1:3000/uploads/image.png');
407
+ expect(isDesktopLocalStaticServerUrl).toHaveBeenCalledWith(
408
+ 'http://127.0.0.1:3000/uploads/image.png',
409
+ );
401
410
  expect(imageUrlToBase64).toHaveBeenCalledWith('http://127.0.0.1:3000/uploads/image.png');
402
411
 
403
412
  // Verify the final result contains base64 converted URL
@@ -428,11 +437,13 @@ describe('ChatService', () => {
428
437
  });
429
438
 
430
439
  it('should not convert remote URLs to base64 and call processImageList', async () => {
431
- const { imageUrlToBase64, parseDataUri, isLocalUrl } = await import('@lobechat/utils');
440
+ const { imageUrlToBase64 } = await import('@lobechat/utils/imageToBase64');
441
+ const { parseDataUri } = await import('@lobechat/utils/uriParser');
442
+ const { isDesktopLocalStaticServerUrl } = await import('@lobechat/utils/url');
432
443
 
433
444
  // Mock for remote URL
434
445
  vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
435
- vi.mocked(isLocalUrl).mockReturnValue(false); // This is NOT a local URL
446
+ vi.mocked(isDesktopLocalStaticServerUrl).mockReturnValue(false); // This is NOT a local URL
436
447
  vi.mocked(imageUrlToBase64).mockClear(); // Clear to ensure it's not called
437
448
 
438
449
  const messages = [
@@ -464,7 +475,9 @@ describe('ChatService', () => {
464
475
 
465
476
  // Verify the utility functions were called
466
477
  expect(parseDataUri).toHaveBeenCalledWith('https://example.com/remote-image.jpg');
467
- expect(isLocalUrl).toHaveBeenCalledWith('https://example.com/remote-image.jpg');
478
+ expect(isDesktopLocalStaticServerUrl).toHaveBeenCalledWith(
479
+ 'https://example.com/remote-image.jpg',
480
+ );
468
481
  expect(imageUrlToBase64).not.toHaveBeenCalled(); // Should NOT be called for remote URLs
469
482
 
470
483
  // Verify the final result preserves original URL
@@ -492,13 +505,15 @@ describe('ChatService', () => {
492
505
  });
493
506
 
494
507
  it('should handle mixed local and remote URLs correctly', async () => {
495
- const { imageUrlToBase64, parseDataUri, isLocalUrl } = await import('@lobechat/utils');
508
+ const { imageUrlToBase64 } = await import('@lobechat/utils/imageToBase64');
509
+ const { parseDataUri } = await import('@lobechat/utils/uriParser');
510
+ const { isDesktopLocalStaticServerUrl } = await import('@lobechat/utils/url');
496
511
 
497
512
  // Mock parseDataUri to always return url type
498
513
  vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
499
514
 
500
- // Mock isLocalUrl to return true only for 127.0.0.1 URLs
501
- vi.mocked(isLocalUrl).mockImplementation((url: string) => {
515
+ // Mock isDesktopLocalStaticServerUrl to return true only for 127.0.0.1 URLs
516
+ vi.mocked(isDesktopLocalStaticServerUrl).mockImplementation((url: string) => {
502
517
  return new URL(url).hostname === '127.0.0.1';
503
518
  });
504
519
 
@@ -544,10 +559,16 @@ describe('ChatService', () => {
544
559
  model: 'gpt-4-vision-preview',
545
560
  });
546
561
 
547
- // Verify isLocalUrl was called for each image
548
- expect(isLocalUrl).toHaveBeenCalledWith('http://127.0.0.1:3000/local1.jpg');
549
- expect(isLocalUrl).toHaveBeenCalledWith('https://example.com/remote1.png');
550
- expect(isLocalUrl).toHaveBeenCalledWith('http://127.0.0.1:8080/local2.gif');
562
+ // Verify isDesktopLocalStaticServerUrl was called for each image
563
+ expect(isDesktopLocalStaticServerUrl).toHaveBeenCalledWith(
564
+ 'http://127.0.0.1:3000/local1.jpg',
565
+ );
566
+ expect(isDesktopLocalStaticServerUrl).toHaveBeenCalledWith(
567
+ 'https://example.com/remote1.png',
568
+ );
569
+ expect(isDesktopLocalStaticServerUrl).toHaveBeenCalledWith(
570
+ 'http://127.0.0.1:8080/local2.gif',
571
+ );
551
572
 
552
573
  // Verify imageUrlToBase64 was called only for local URLs
553
574
  expect(imageUrlToBase64).toHaveBeenCalledWith('http://127.0.0.1:3000/local1.jpg');
@@ -46,7 +46,7 @@ vi.mock('@/utils/fetch', async (importOriginal) => {
46
46
 
47
47
  // Mock image processing utilities
48
48
  vi.mock('@/utils/url', () => ({
49
- isLocalUrl: vi.fn(),
49
+ isDesktopLocalStaticServerUrl: vi.fn(),
50
50
  }));
51
51
 
52
52
  vi.mock('@/utils/imageToBase64', () => ({
@@ -81,12 +81,12 @@ beforeEach(async () => {
81
81
  vi.clearAllMocks();
82
82
 
83
83
  // Set default mock return values for image processing utilities
84
- const { isLocalUrl } = await import('@/utils/url');
84
+ const { isDesktopLocalStaticServerUrl } = await import('@/utils/url');
85
85
  const { imageUrlToBase64 } = await import('@/utils/imageToBase64');
86
86
  const { parseDataUri } = await import('@lobechat/model-runtime');
87
87
 
88
88
  vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
89
- vi.mocked(isLocalUrl).mockReturnValue(false);
89
+ vi.mocked(isDesktopLocalStaticServerUrl).mockReturnValue(false);
90
90
  vi.mocked(imageUrlToBase64).mockResolvedValue({
91
91
  base64: 'mock-base64',
92
92
  mimeType: 'image/jpeg',