@lobehub/chat 1.106.0 → 1.106.1
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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +5 -2
- package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +9 -0
- package/src/config/aiModels/minimax.ts +43 -2
- package/src/libs/model-runtime/minimax/createImage.test.ts +553 -0
- package/src/libs/model-runtime/minimax/createImage.ts +105 -0
- package/src/libs/model-runtime/minimax/index.ts +2 -0
- package/src/store/image/slices/generationTopic/action.test.ts +3 -3
- package/src/store/image/slices/generationTopic/action.ts +4 -7
- package/src/hooks/useFetchGenerationTopics.ts +0 -13
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.106.1](https://github.com/lobehub/lobe-chat/compare/v1.106.0...v1.106.1)
|
6
|
+
|
7
|
+
<sup>Released on **2025-07-29**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Support Minimax T2I models.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Support Minimax T2I models, closes [#8583](https://github.com/lobehub/lobe-chat/issues/8583) ([f8a01aa](https://github.com/lobehub/lobe-chat/commit/f8a01aa))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
## [Version 1.106.0](https://github.com/lobehub/lobe-chat/compare/v1.105.6...v1.106.0)
|
6
31
|
|
7
32
|
<sup>Released on **2025-07-29**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.106.
|
3
|
+
"version": "1.106.1",
|
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",
|
@@ -5,15 +5,18 @@ import { useSize } from 'ahooks';
|
|
5
5
|
import { memo, useRef } from 'react';
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
7
7
|
|
8
|
-
import { useFetchGenerationTopics } from '@/hooks/useFetchGenerationTopics';
|
9
8
|
import { useImageStore } from '@/store/image';
|
10
9
|
import { generationTopicSelectors } from '@/store/image/selectors';
|
10
|
+
import { useUserStore } from '@/store/user';
|
11
|
+
import { authSelectors } from '@/store/user/slices/auth/selectors';
|
11
12
|
|
12
13
|
import NewTopicButton from './NewTopicButton';
|
13
14
|
import TopicItem from './TopicItem';
|
14
15
|
|
15
16
|
const TopicsList = memo(() => {
|
16
|
-
|
17
|
+
const isLogin = useUserStore(authSelectors.isLogin);
|
18
|
+
const useFetchGenerationTopics = useImageStore((s) => s.useFetchGenerationTopics);
|
19
|
+
useFetchGenerationTopics(!!isLogin);
|
17
20
|
const ref = useRef(null);
|
18
21
|
const { width = 80 } = useSize(ref) || {};
|
19
22
|
const [parent] = useAutoAnimate();
|
@@ -7,9 +7,12 @@ import type { KeyboardEvent } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
8
8
|
import { Flexbox } from 'react-layout-kit';
|
9
9
|
|
10
|
+
import { loginRequired } from '@/components/Error/loginRequiredNotification';
|
10
11
|
import { useImageStore } from '@/store/image';
|
11
12
|
import { createImageSelectors } from '@/store/image/selectors';
|
12
13
|
import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
|
14
|
+
import { useUserStore } from '@/store/user';
|
15
|
+
import { authSelectors } from '@/store/user/slices/auth/selectors';
|
13
16
|
|
14
17
|
import PromptTitle from './Title';
|
15
18
|
|
@@ -46,8 +49,14 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
|
46
49
|
const { value, setValue } = useGenerationConfigParam('prompt');
|
47
50
|
const isCreating = useImageStore(createImageSelectors.isCreating);
|
48
51
|
const createImage = useImageStore((s) => s.createImage);
|
52
|
+
const isLogin = useUserStore(authSelectors.isLogin);
|
49
53
|
|
50
54
|
const handleGenerate = async () => {
|
55
|
+
if (!isLogin) {
|
56
|
+
loginRequired.redirect({ timeout: 2000 });
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
|
51
60
|
await createImage();
|
52
61
|
};
|
53
62
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AIChatModelCard } from '@/types/aiModel';
|
1
|
+
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
2
2
|
|
3
3
|
const minimaxChatModels: AIChatModelCard[] = [
|
4
4
|
{
|
@@ -51,6 +51,47 @@ const minimaxChatModels: AIChatModelCard[] = [
|
|
51
51
|
},
|
52
52
|
];
|
53
53
|
|
54
|
-
|
54
|
+
const minimaxImageModels: AIImageModelCard[] = [
|
55
|
+
{
|
56
|
+
description:
|
57
|
+
'全新图像生成模型,画面表现细腻,支持文生图、图生图',
|
58
|
+
displayName: 'Image 01',
|
59
|
+
enabled: true,
|
60
|
+
id: 'image-01',
|
61
|
+
parameters: {
|
62
|
+
aspectRatio: {
|
63
|
+
default: '1:1',
|
64
|
+
enum: ['1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9'],
|
65
|
+
},
|
66
|
+
prompt: {
|
67
|
+
default: '',
|
68
|
+
},
|
69
|
+
seed: { default: null },
|
70
|
+
},
|
71
|
+
releasedAt: '2025-02-28',
|
72
|
+
type: 'image',
|
73
|
+
},
|
74
|
+
{
|
75
|
+
description:
|
76
|
+
'图像生成模型,画面表现细腻,支持文生图并进行画风设置',
|
77
|
+
displayName: 'Image 01 Live',
|
78
|
+
enabled: true,
|
79
|
+
id: 'image-01-live',
|
80
|
+
parameters: {
|
81
|
+
aspectRatio: {
|
82
|
+
default: '1:1',
|
83
|
+
enum: ['1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9'],
|
84
|
+
},
|
85
|
+
prompt: {
|
86
|
+
default: '',
|
87
|
+
},
|
88
|
+
seed: { default: null },
|
89
|
+
},
|
90
|
+
releasedAt: '2025-02-28',
|
91
|
+
type: 'image',
|
92
|
+
},
|
93
|
+
];
|
94
|
+
|
95
|
+
export const allModels = [...minimaxChatModels, ...minimaxImageModels];
|
55
96
|
|
56
97
|
export default allModels;
|
@@ -0,0 +1,553 @@
|
|
1
|
+
// @vitest-environment edge-runtime
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { CreateImagePayload } from '../types/image';
|
5
|
+
import { CreateImageOptions } from '../utils/openaiCompatibleFactory';
|
6
|
+
import { createMiniMaxImage } from './createImage';
|
7
|
+
|
8
|
+
// Mock the console.error to avoid polluting test output
|
9
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
10
|
+
|
11
|
+
const mockOptions: CreateImageOptions = {
|
12
|
+
apiKey: 'test-api-key',
|
13
|
+
baseURL: 'https://api.minimaxi.com/v1',
|
14
|
+
provider: 'minimax',
|
15
|
+
};
|
16
|
+
|
17
|
+
beforeEach(() => {
|
18
|
+
// Reset all mocks before each test
|
19
|
+
vi.clearAllMocks();
|
20
|
+
});
|
21
|
+
|
22
|
+
afterEach(() => {
|
23
|
+
vi.clearAllMocks();
|
24
|
+
});
|
25
|
+
|
26
|
+
describe('createMiniMaxImage', () => {
|
27
|
+
describe('Success scenarios', () => {
|
28
|
+
it('should successfully generate image with basic prompt', async () => {
|
29
|
+
const mockImageUrl = 'https://minimax-cdn.com/images/generated/test-image.jpg';
|
30
|
+
|
31
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
32
|
+
ok: true,
|
33
|
+
json: async () => ({
|
34
|
+
base_resp: {
|
35
|
+
status_code: 0,
|
36
|
+
status_msg: 'success',
|
37
|
+
},
|
38
|
+
data: {
|
39
|
+
image_urls: [mockImageUrl],
|
40
|
+
},
|
41
|
+
id: 'img-123456',
|
42
|
+
metadata: {
|
43
|
+
failed_count: '0',
|
44
|
+
success_count: '1',
|
45
|
+
},
|
46
|
+
}),
|
47
|
+
});
|
48
|
+
|
49
|
+
const payload: CreateImagePayload = {
|
50
|
+
model: 'image-01',
|
51
|
+
params: {
|
52
|
+
prompt: 'A beautiful sunset over the mountains',
|
53
|
+
},
|
54
|
+
};
|
55
|
+
|
56
|
+
const result = await createMiniMaxImage(payload, mockOptions);
|
57
|
+
|
58
|
+
expect(fetch).toHaveBeenCalledWith(
|
59
|
+
'https://api.minimaxi.com/v1/image_generation',
|
60
|
+
{
|
61
|
+
method: 'POST',
|
62
|
+
headers: {
|
63
|
+
'Authorization': 'Bearer test-api-key',
|
64
|
+
'Content-Type': 'application/json',
|
65
|
+
},
|
66
|
+
body: JSON.stringify({
|
67
|
+
aspect_ratio: undefined,
|
68
|
+
model: 'image-01',
|
69
|
+
n: 1,
|
70
|
+
prompt: 'A beautiful sunset over the mountains',
|
71
|
+
}),
|
72
|
+
},
|
73
|
+
);
|
74
|
+
|
75
|
+
expect(result).toEqual({
|
76
|
+
imageUrl: mockImageUrl,
|
77
|
+
});
|
78
|
+
});
|
79
|
+
|
80
|
+
it('should handle custom aspect ratio', async () => {
|
81
|
+
const mockImageUrl = 'https://minimax-cdn.com/images/generated/custom-ratio.jpg';
|
82
|
+
|
83
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
84
|
+
ok: true,
|
85
|
+
json: async () => ({
|
86
|
+
base_resp: {
|
87
|
+
status_code: 0,
|
88
|
+
status_msg: 'success',
|
89
|
+
},
|
90
|
+
data: {
|
91
|
+
image_urls: [mockImageUrl],
|
92
|
+
},
|
93
|
+
id: 'img-custom-ratio',
|
94
|
+
metadata: {
|
95
|
+
failed_count: '0',
|
96
|
+
success_count: '1',
|
97
|
+
},
|
98
|
+
}),
|
99
|
+
});
|
100
|
+
|
101
|
+
const payload: CreateImagePayload = {
|
102
|
+
model: 'image-01',
|
103
|
+
params: {
|
104
|
+
prompt: 'Abstract digital art',
|
105
|
+
aspectRatio: '16:9',
|
106
|
+
},
|
107
|
+
};
|
108
|
+
|
109
|
+
const result = await createMiniMaxImage(payload, mockOptions);
|
110
|
+
|
111
|
+
expect(fetch).toHaveBeenCalledWith(
|
112
|
+
'https://api.minimaxi.com/v1/image_generation',
|
113
|
+
expect.objectContaining({
|
114
|
+
body: JSON.stringify({
|
115
|
+
aspect_ratio: '16:9',
|
116
|
+
model: 'image-01',
|
117
|
+
n: 1,
|
118
|
+
prompt: 'Abstract digital art',
|
119
|
+
}),
|
120
|
+
}),
|
121
|
+
);
|
122
|
+
|
123
|
+
expect(result).toEqual({
|
124
|
+
imageUrl: mockImageUrl,
|
125
|
+
});
|
126
|
+
});
|
127
|
+
|
128
|
+
it('should handle seed value correctly', async () => {
|
129
|
+
const mockImageUrl = 'https://minimax-cdn.com/images/generated/seeded-image.jpg';
|
130
|
+
|
131
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
132
|
+
ok: true,
|
133
|
+
json: async () => ({
|
134
|
+
base_resp: {
|
135
|
+
status_code: 0,
|
136
|
+
status_msg: 'success',
|
137
|
+
},
|
138
|
+
data: {
|
139
|
+
image_urls: [mockImageUrl],
|
140
|
+
},
|
141
|
+
id: 'img-seeded',
|
142
|
+
metadata: {
|
143
|
+
failed_count: '0',
|
144
|
+
success_count: '1',
|
145
|
+
},
|
146
|
+
}),
|
147
|
+
});
|
148
|
+
|
149
|
+
const payload: CreateImagePayload = {
|
150
|
+
model: 'image-01',
|
151
|
+
params: {
|
152
|
+
prompt: 'Reproducible image with seed',
|
153
|
+
seed: 42,
|
154
|
+
},
|
155
|
+
};
|
156
|
+
|
157
|
+
const result = await createMiniMaxImage(payload, mockOptions);
|
158
|
+
|
159
|
+
expect(fetch).toHaveBeenCalledWith(
|
160
|
+
'https://api.minimaxi.com/v1/image_generation',
|
161
|
+
expect.objectContaining({
|
162
|
+
body: JSON.stringify({
|
163
|
+
aspect_ratio: undefined,
|
164
|
+
model: 'image-01',
|
165
|
+
n: 1,
|
166
|
+
prompt: 'Reproducible image with seed',
|
167
|
+
seed: 42,
|
168
|
+
}),
|
169
|
+
}),
|
170
|
+
);
|
171
|
+
|
172
|
+
expect(result).toEqual({
|
173
|
+
imageUrl: mockImageUrl,
|
174
|
+
});
|
175
|
+
});
|
176
|
+
|
177
|
+
it('should handle seed value of 0 correctly', async () => {
|
178
|
+
const mockImageUrl = 'https://minimax-cdn.com/images/generated/zero-seed.jpg';
|
179
|
+
|
180
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
181
|
+
ok: true,
|
182
|
+
json: async () => ({
|
183
|
+
base_resp: {
|
184
|
+
status_code: 0,
|
185
|
+
status_msg: 'success',
|
186
|
+
},
|
187
|
+
data: {
|
188
|
+
image_urls: [mockImageUrl],
|
189
|
+
},
|
190
|
+
id: 'img-zero-seed',
|
191
|
+
metadata: {
|
192
|
+
failed_count: '0',
|
193
|
+
success_count: '1',
|
194
|
+
},
|
195
|
+
}),
|
196
|
+
});
|
197
|
+
|
198
|
+
const payload: CreateImagePayload = {
|
199
|
+
model: 'image-01',
|
200
|
+
params: {
|
201
|
+
prompt: 'Image with seed 0',
|
202
|
+
seed: 0,
|
203
|
+
},
|
204
|
+
};
|
205
|
+
|
206
|
+
await createMiniMaxImage(payload, mockOptions);
|
207
|
+
|
208
|
+
// Verify that seed: 0 is included in the request
|
209
|
+
expect(fetch).toHaveBeenCalledWith(
|
210
|
+
'https://api.minimaxi.com/v1/image_generation',
|
211
|
+
expect.objectContaining({
|
212
|
+
body: JSON.stringify({
|
213
|
+
aspect_ratio: undefined,
|
214
|
+
model: 'image-01',
|
215
|
+
n: 1,
|
216
|
+
prompt: 'Image with seed 0',
|
217
|
+
seed: 0,
|
218
|
+
}),
|
219
|
+
}),
|
220
|
+
);
|
221
|
+
});
|
222
|
+
|
223
|
+
it('should handle multiple generated images and return the first one', async () => {
|
224
|
+
const mockImageUrls = [
|
225
|
+
'https://minimax-cdn.com/images/generated/image-1.jpg',
|
226
|
+
'https://minimax-cdn.com/images/generated/image-2.jpg',
|
227
|
+
];
|
228
|
+
|
229
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
230
|
+
ok: true,
|
231
|
+
json: async () => ({
|
232
|
+
base_resp: {
|
233
|
+
status_code: 0,
|
234
|
+
status_msg: 'success',
|
235
|
+
},
|
236
|
+
data: {
|
237
|
+
image_urls: mockImageUrls,
|
238
|
+
},
|
239
|
+
id: 'img-multiple',
|
240
|
+
metadata: {
|
241
|
+
failed_count: '0',
|
242
|
+
success_count: '2',
|
243
|
+
},
|
244
|
+
}),
|
245
|
+
});
|
246
|
+
|
247
|
+
const payload: CreateImagePayload = {
|
248
|
+
model: 'image-01',
|
249
|
+
params: {
|
250
|
+
prompt: 'Multiple images test',
|
251
|
+
},
|
252
|
+
};
|
253
|
+
|
254
|
+
const result = await createMiniMaxImage(payload, mockOptions);
|
255
|
+
|
256
|
+
expect(result).toEqual({
|
257
|
+
imageUrl: mockImageUrls[0], // Should return the first image
|
258
|
+
});
|
259
|
+
});
|
260
|
+
|
261
|
+
it('should handle partial failures gracefully', async () => {
|
262
|
+
const mockImageUrl = 'https://minimax-cdn.com/images/generated/partial-success.jpg';
|
263
|
+
|
264
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
265
|
+
ok: true,
|
266
|
+
json: async () => ({
|
267
|
+
base_resp: {
|
268
|
+
status_code: 0,
|
269
|
+
status_msg: 'success',
|
270
|
+
},
|
271
|
+
data: {
|
272
|
+
image_urls: [mockImageUrl],
|
273
|
+
},
|
274
|
+
id: 'img-partial',
|
275
|
+
metadata: {
|
276
|
+
failed_count: '2',
|
277
|
+
success_count: '1',
|
278
|
+
},
|
279
|
+
}),
|
280
|
+
});
|
281
|
+
|
282
|
+
const payload: CreateImagePayload = {
|
283
|
+
model: 'image-01',
|
284
|
+
params: {
|
285
|
+
prompt: 'Test partial failure',
|
286
|
+
},
|
287
|
+
};
|
288
|
+
|
289
|
+
const result = await createMiniMaxImage(payload, mockOptions);
|
290
|
+
|
291
|
+
expect(result).toEqual({
|
292
|
+
imageUrl: mockImageUrl,
|
293
|
+
});
|
294
|
+
});
|
295
|
+
});
|
296
|
+
|
297
|
+
describe('Error scenarios', () => {
|
298
|
+
it('should handle HTTP error responses', async () => {
|
299
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
300
|
+
ok: false,
|
301
|
+
status: 400,
|
302
|
+
statusText: 'Bad Request',
|
303
|
+
json: async () => ({
|
304
|
+
base_resp: {
|
305
|
+
status_code: 1001,
|
306
|
+
status_msg: 'Invalid prompt format',
|
307
|
+
},
|
308
|
+
}),
|
309
|
+
});
|
310
|
+
|
311
|
+
const payload: CreateImagePayload = {
|
312
|
+
model: 'image-01',
|
313
|
+
params: {
|
314
|
+
prompt: 'Invalid prompt',
|
315
|
+
},
|
316
|
+
};
|
317
|
+
|
318
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
319
|
+
expect.objectContaining({
|
320
|
+
errorType: 'ProviderBizError',
|
321
|
+
provider: 'minimax',
|
322
|
+
}),
|
323
|
+
);
|
324
|
+
});
|
325
|
+
|
326
|
+
it('should handle non-JSON error responses', async () => {
|
327
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
328
|
+
ok: false,
|
329
|
+
status: 500,
|
330
|
+
statusText: 'Internal Server Error',
|
331
|
+
json: async () => {
|
332
|
+
throw new Error('Failed to parse JSON');
|
333
|
+
},
|
334
|
+
});
|
335
|
+
|
336
|
+
const payload: CreateImagePayload = {
|
337
|
+
model: 'image-01',
|
338
|
+
params: {
|
339
|
+
prompt: 'Test prompt',
|
340
|
+
},
|
341
|
+
};
|
342
|
+
|
343
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
344
|
+
expect.objectContaining({
|
345
|
+
errorType: 'ProviderBizError',
|
346
|
+
provider: 'minimax',
|
347
|
+
}),
|
348
|
+
);
|
349
|
+
});
|
350
|
+
|
351
|
+
it('should handle API error status codes', async () => {
|
352
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
353
|
+
ok: true,
|
354
|
+
json: async () => ({
|
355
|
+
base_resp: {
|
356
|
+
status_code: 1002,
|
357
|
+
status_msg: 'Content policy violation',
|
358
|
+
},
|
359
|
+
data: {
|
360
|
+
image_urls: [],
|
361
|
+
},
|
362
|
+
id: 'img-error',
|
363
|
+
metadata: {
|
364
|
+
failed_count: '1',
|
365
|
+
success_count: '0',
|
366
|
+
},
|
367
|
+
}),
|
368
|
+
});
|
369
|
+
|
370
|
+
const payload: CreateImagePayload = {
|
371
|
+
model: 'image-01',
|
372
|
+
params: {
|
373
|
+
prompt: 'Inappropriate content',
|
374
|
+
},
|
375
|
+
};
|
376
|
+
|
377
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
378
|
+
expect.objectContaining({
|
379
|
+
errorType: 'ProviderBizError',
|
380
|
+
provider: 'minimax',
|
381
|
+
}),
|
382
|
+
);
|
383
|
+
});
|
384
|
+
|
385
|
+
it('should handle empty image URLs array', async () => {
|
386
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
387
|
+
ok: true,
|
388
|
+
json: async () => ({
|
389
|
+
base_resp: {
|
390
|
+
status_code: 0,
|
391
|
+
status_msg: 'success',
|
392
|
+
},
|
393
|
+
data: {
|
394
|
+
image_urls: [],
|
395
|
+
},
|
396
|
+
id: 'img-empty',
|
397
|
+
metadata: {
|
398
|
+
failed_count: '1',
|
399
|
+
success_count: '0',
|
400
|
+
},
|
401
|
+
}),
|
402
|
+
});
|
403
|
+
|
404
|
+
const payload: CreateImagePayload = {
|
405
|
+
model: 'image-01',
|
406
|
+
params: {
|
407
|
+
prompt: 'Empty result test',
|
408
|
+
},
|
409
|
+
};
|
410
|
+
|
411
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
412
|
+
expect.objectContaining({
|
413
|
+
errorType: 'ProviderBizError',
|
414
|
+
provider: 'minimax',
|
415
|
+
}),
|
416
|
+
);
|
417
|
+
});
|
418
|
+
|
419
|
+
it('should handle missing data field', async () => {
|
420
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
421
|
+
ok: true,
|
422
|
+
json: async () => ({
|
423
|
+
base_resp: {
|
424
|
+
status_code: 0,
|
425
|
+
status_msg: 'success',
|
426
|
+
},
|
427
|
+
id: 'img-no-data',
|
428
|
+
metadata: {
|
429
|
+
failed_count: '0',
|
430
|
+
success_count: '1',
|
431
|
+
},
|
432
|
+
}),
|
433
|
+
});
|
434
|
+
|
435
|
+
const payload: CreateImagePayload = {
|
436
|
+
model: 'image-01',
|
437
|
+
params: {
|
438
|
+
prompt: 'Missing data test',
|
439
|
+
},
|
440
|
+
};
|
441
|
+
|
442
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
443
|
+
expect.objectContaining({
|
444
|
+
errorType: 'ProviderBizError',
|
445
|
+
provider: 'minimax',
|
446
|
+
}),
|
447
|
+
);
|
448
|
+
});
|
449
|
+
|
450
|
+
it('should handle null/empty image URL', async () => {
|
451
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
452
|
+
ok: true,
|
453
|
+
json: async () => ({
|
454
|
+
base_resp: {
|
455
|
+
status_code: 0,
|
456
|
+
status_msg: 'success',
|
457
|
+
},
|
458
|
+
data: {
|
459
|
+
image_urls: [''], // Empty string URL
|
460
|
+
},
|
461
|
+
id: 'img-empty-url',
|
462
|
+
metadata: {
|
463
|
+
failed_count: '0',
|
464
|
+
success_count: '1',
|
465
|
+
},
|
466
|
+
}),
|
467
|
+
});
|
468
|
+
|
469
|
+
const payload: CreateImagePayload = {
|
470
|
+
model: 'image-01',
|
471
|
+
params: {
|
472
|
+
prompt: 'Empty URL test',
|
473
|
+
},
|
474
|
+
};
|
475
|
+
|
476
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
477
|
+
expect.objectContaining({
|
478
|
+
errorType: 'ProviderBizError',
|
479
|
+
provider: 'minimax',
|
480
|
+
}),
|
481
|
+
);
|
482
|
+
});
|
483
|
+
|
484
|
+
it('should handle network errors', async () => {
|
485
|
+
global.fetch = vi.fn().mockRejectedValueOnce(new Error('Network connection failed'));
|
486
|
+
|
487
|
+
const payload: CreateImagePayload = {
|
488
|
+
model: 'image-01',
|
489
|
+
params: {
|
490
|
+
prompt: 'Network error test',
|
491
|
+
},
|
492
|
+
};
|
493
|
+
|
494
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
495
|
+
expect.objectContaining({
|
496
|
+
errorType: 'ProviderBizError',
|
497
|
+
provider: 'minimax',
|
498
|
+
}),
|
499
|
+
);
|
500
|
+
});
|
501
|
+
|
502
|
+
it('should handle unauthorized access', async () => {
|
503
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
504
|
+
ok: false,
|
505
|
+
status: 401,
|
506
|
+
statusText: 'Unauthorized',
|
507
|
+
json: async () => ({
|
508
|
+
base_resp: {
|
509
|
+
status_code: 1003,
|
510
|
+
status_msg: 'Invalid API key',
|
511
|
+
},
|
512
|
+
}),
|
513
|
+
});
|
514
|
+
|
515
|
+
const payload: CreateImagePayload = {
|
516
|
+
model: 'image-01',
|
517
|
+
params: {
|
518
|
+
prompt: 'Unauthorized test',
|
519
|
+
},
|
520
|
+
};
|
521
|
+
|
522
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
523
|
+
expect.objectContaining({
|
524
|
+
errorType: 'ProviderBizError',
|
525
|
+
provider: 'minimax',
|
526
|
+
}),
|
527
|
+
);
|
528
|
+
});
|
529
|
+
|
530
|
+
it('should handle malformed JSON response', async () => {
|
531
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
532
|
+
ok: true,
|
533
|
+
json: async () => {
|
534
|
+
throw new Error('Unexpected token in JSON');
|
535
|
+
},
|
536
|
+
});
|
537
|
+
|
538
|
+
const payload: CreateImagePayload = {
|
539
|
+
model: 'image-01',
|
540
|
+
params: {
|
541
|
+
prompt: 'JSON error test',
|
542
|
+
},
|
543
|
+
};
|
544
|
+
|
545
|
+
await expect(createMiniMaxImage(payload, mockOptions)).rejects.toEqual(
|
546
|
+
expect.objectContaining({
|
547
|
+
errorType: 'ProviderBizError',
|
548
|
+
provider: 'minimax',
|
549
|
+
}),
|
550
|
+
);
|
551
|
+
});
|
552
|
+
});
|
553
|
+
});
|
@@ -0,0 +1,105 @@
|
|
1
|
+
import createDebug from 'debug';
|
2
|
+
|
3
|
+
import { CreateImagePayload, CreateImageResponse } from '../types/image';
|
4
|
+
import { AgentRuntimeError } from '../utils/createError';
|
5
|
+
import { CreateImageOptions } from '../utils/openaiCompatibleFactory';
|
6
|
+
|
7
|
+
const log = createDebug('lobe-image:minimax');
|
8
|
+
|
9
|
+
interface MiniMaxImageResponse {
|
10
|
+
base_resp: {
|
11
|
+
status_code: number;
|
12
|
+
status_msg: string;
|
13
|
+
};
|
14
|
+
data: {
|
15
|
+
image_urls: string[];
|
16
|
+
};
|
17
|
+
id: string;
|
18
|
+
metadata: {
|
19
|
+
failed_count: string;
|
20
|
+
success_count: string;
|
21
|
+
};
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Create image using MiniMax API
|
26
|
+
*/
|
27
|
+
export async function createMiniMaxImage(
|
28
|
+
payload: CreateImagePayload,
|
29
|
+
options: CreateImageOptions,
|
30
|
+
): Promise<CreateImageResponse> {
|
31
|
+
const { apiKey, baseURL, provider } = options;
|
32
|
+
const { model, params } = payload;
|
33
|
+
|
34
|
+
try {
|
35
|
+
const endpoint = `${baseURL}/image_generation`;
|
36
|
+
|
37
|
+
const response = await fetch(endpoint, {
|
38
|
+
body: JSON.stringify({
|
39
|
+
aspect_ratio: params.aspectRatio,
|
40
|
+
model,
|
41
|
+
n: 1,
|
42
|
+
prompt: params.prompt,
|
43
|
+
//prompt_optimizer: true, // 开启 prompt 自动优化
|
44
|
+
...(typeof params.seed === 'number' ? { seed: params.seed } : {}),
|
45
|
+
}),
|
46
|
+
headers: {
|
47
|
+
'Authorization': `Bearer ${apiKey}`,
|
48
|
+
'Content-Type': 'application/json',
|
49
|
+
},
|
50
|
+
method: 'POST',
|
51
|
+
});
|
52
|
+
|
53
|
+
if (!response.ok) {
|
54
|
+
let errorData;
|
55
|
+
try {
|
56
|
+
errorData = await response.json();
|
57
|
+
} catch {
|
58
|
+
// Failed to parse JSON error response
|
59
|
+
}
|
60
|
+
|
61
|
+
throw new Error(
|
62
|
+
`MiniMax API error (${response.status}): ${errorData?.base_resp || response.statusText}`
|
63
|
+
);
|
64
|
+
}
|
65
|
+
|
66
|
+
const data: MiniMaxImageResponse = await response.json();
|
67
|
+
|
68
|
+
log('Image generation response: %O', data);
|
69
|
+
|
70
|
+
// Check API response status
|
71
|
+
if (data.base_resp.status_code !== 0) {
|
72
|
+
throw new Error(`MiniMax API error: ${data.base_resp.status_msg}`);
|
73
|
+
}
|
74
|
+
|
75
|
+
// Check if we have valid image data
|
76
|
+
if (!data.data?.image_urls || data.data.image_urls.length === 0) {
|
77
|
+
throw new Error('No images generated in response');
|
78
|
+
}
|
79
|
+
|
80
|
+
// Log generation statistics
|
81
|
+
const successCount = parseInt(data.metadata.success_count);
|
82
|
+
const failedCount = parseInt(data.metadata.failed_count);
|
83
|
+
log('Image generation completed: %d successful, %d failed', successCount, failedCount);
|
84
|
+
|
85
|
+
// Return the first generated image URL
|
86
|
+
const imageUrl = data.data.image_urls[0];
|
87
|
+
|
88
|
+
if (!imageUrl) {
|
89
|
+
throw new Error('No valid image URL in response');
|
90
|
+
}
|
91
|
+
|
92
|
+
log('Image generated successfully: %s', imageUrl);
|
93
|
+
|
94
|
+
return { imageUrl };
|
95
|
+
|
96
|
+
} catch (error) {
|
97
|
+
log('Error in createMiniMaxImage: %O', error);
|
98
|
+
|
99
|
+
throw AgentRuntimeError.createImage({
|
100
|
+
error: error as any,
|
101
|
+
errorType: 'ProviderBizError',
|
102
|
+
provider,
|
103
|
+
});
|
104
|
+
}
|
105
|
+
}
|
@@ -2,6 +2,7 @@ import minimaxChatModels from '@/config/aiModels/minimax';
|
|
2
2
|
|
3
3
|
import { ModelProvider } from '../types';
|
4
4
|
import { createOpenAICompatibleRuntime } from '../utils/openaiCompatibleFactory';
|
5
|
+
import { createMiniMaxImage } from './createImage';
|
5
6
|
|
6
7
|
export const getMinimaxMaxOutputs = (modelId: string): number | undefined => {
|
7
8
|
const model = minimaxChatModels.find((model) => model.id === modelId);
|
@@ -34,6 +35,7 @@ export const LobeMinimaxAI = createOpenAICompatibleRuntime({
|
|
34
35
|
} as any;
|
35
36
|
},
|
36
37
|
},
|
38
|
+
createImage: createMiniMaxImage,
|
37
39
|
debug: {
|
38
40
|
chatCompletion: () => process.env.DEBUG_MINIMAX_CHAT_COMPLETION === '1',
|
39
41
|
},
|
@@ -405,7 +405,7 @@ describe('GenerationTopicAction', () => {
|
|
405
405
|
const { result } = renderHook(() => {
|
406
406
|
const store = useImageStore();
|
407
407
|
// Actually call the SWR hook to trigger the service call
|
408
|
-
const swrResult = store.useFetchGenerationTopics(true
|
408
|
+
const swrResult = store.useFetchGenerationTopics(true);
|
409
409
|
|
410
410
|
// Simulate the SWR onSuccess callback behavior
|
411
411
|
React.useEffect(() => {
|
@@ -426,7 +426,7 @@ describe('GenerationTopicAction', () => {
|
|
426
426
|
});
|
427
427
|
|
428
428
|
it('should not fetch when disabled', async () => {
|
429
|
-
const { result } = renderHook(() => useImageStore().useFetchGenerationTopics(false
|
429
|
+
const { result } = renderHook(() => useImageStore().useFetchGenerationTopics(false));
|
430
430
|
|
431
431
|
expect(result.current.data).toBeUndefined();
|
432
432
|
expect(generationTopicService.getAllGenerationTopics).not.toHaveBeenCalled();
|
@@ -455,7 +455,7 @@ describe('GenerationTopicAction', () => {
|
|
455
455
|
await result.current.refreshGenerationTopics();
|
456
456
|
});
|
457
457
|
|
458
|
-
expect(mutate).toHaveBeenCalledWith(['fetchGenerationTopics'
|
458
|
+
expect(mutate).toHaveBeenCalledWith(['fetchGenerationTopics']);
|
459
459
|
});
|
460
460
|
});
|
461
461
|
|
@@ -25,10 +25,7 @@ const n = setNamespace('generationTopic');
|
|
25
25
|
export interface GenerationTopicAction {
|
26
26
|
createGenerationTopic: (prompts: string[]) => Promise<string>;
|
27
27
|
removeGenerationTopic: (id: string) => Promise<void>;
|
28
|
-
useFetchGenerationTopics: (
|
29
|
-
enabled: boolean,
|
30
|
-
isLogin: boolean | undefined,
|
31
|
-
) => SWRResponse<ImageGenerationTopic[]>;
|
28
|
+
useFetchGenerationTopics: (enabled: boolean) => SWRResponse<ImageGenerationTopic[]>;
|
32
29
|
summaryGenerationTopicTitle: (topicId: string, prompts: string[]) => Promise<string>;
|
33
30
|
refreshGenerationTopics: () => Promise<void>;
|
34
31
|
switchGenerationTopic: (topicId: string) => void;
|
@@ -216,9 +213,9 @@ export const createGenerationTopicSlice: StateCreator<
|
|
216
213
|
);
|
217
214
|
},
|
218
215
|
|
219
|
-
useFetchGenerationTopics: (enabled
|
216
|
+
useFetchGenerationTopics: (enabled) =>
|
220
217
|
useClientDataSWR<ImageGenerationTopic[]>(
|
221
|
-
enabled ? [FETCH_GENERATION_TOPICS_KEY
|
218
|
+
enabled ? [FETCH_GENERATION_TOPICS_KEY] : null,
|
222
219
|
() => generationTopicService.getAllGenerationTopics(),
|
223
220
|
{
|
224
221
|
suspense: true,
|
@@ -231,7 +228,7 @@ export const createGenerationTopicSlice: StateCreator<
|
|
231
228
|
),
|
232
229
|
|
233
230
|
refreshGenerationTopics: async () => {
|
234
|
-
await mutate([FETCH_GENERATION_TOPICS_KEY
|
231
|
+
await mutate([FETCH_GENERATION_TOPICS_KEY]);
|
235
232
|
},
|
236
233
|
|
237
234
|
removeGenerationTopic: async (id: string) => {
|
@@ -1,13 +0,0 @@
|
|
1
|
-
import { useGlobalStore } from '@/store/global';
|
2
|
-
import { systemStatusSelectors } from '@/store/global/selectors';
|
3
|
-
import { useImageStore } from '@/store/image';
|
4
|
-
import { useUserStore } from '@/store/user';
|
5
|
-
import { authSelectors } from '@/store/user/slices/auth/selectors';
|
6
|
-
|
7
|
-
export const useFetchGenerationTopics = () => {
|
8
|
-
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
9
|
-
const isLogin = useUserStore(authSelectors.isLogin);
|
10
|
-
const useFetchGenerationTopics = useImageStore((s) => s.useFetchGenerationTopics);
|
11
|
-
|
12
|
-
useFetchGenerationTopics(isDBInited, isLogin);
|
13
|
-
};
|