@lobehub/chat 1.102.3 → 1.103.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 +58 -0
- package/apps/desktop/README.md +322 -36
- package/apps/desktop/README.zh-CN.md +353 -0
- package/apps/desktop/package.json +1 -0
- package/apps/desktop/resources/tray-dark.png +0 -0
- package/apps/desktop/resources/tray-light.png +0 -0
- package/apps/desktop/resources/tray.png +0 -0
- package/apps/desktop/src/main/const/env.ts +25 -0
- package/apps/desktop/src/main/core/TrayManager.ts +7 -1
- package/changelog/v1.json +18 -0
- package/locales/ar/subscription.json +24 -0
- package/locales/bg-BG/subscription.json +24 -0
- package/locales/de-DE/subscription.json +24 -0
- package/locales/en-US/subscription.json +24 -0
- package/locales/es-ES/subscription.json +24 -0
- package/locales/fa-IR/subscription.json +24 -0
- package/locales/fr-FR/subscription.json +24 -0
- package/locales/it-IT/subscription.json +24 -0
- package/locales/ja-JP/subscription.json +24 -0
- package/locales/ko-KR/subscription.json +24 -0
- package/locales/nl-NL/subscription.json +24 -0
- package/locales/pl-PL/subscription.json +24 -0
- package/locales/pt-BR/subscription.json +24 -0
- package/locales/ru-RU/subscription.json +24 -0
- package/locales/tr-TR/subscription.json +24 -0
- package/locales/vi-VN/subscription.json +24 -0
- package/locales/zh-CN/subscription.json +24 -0
- package/locales/zh-TW/subscription.json +24 -0
- package/package.json +1 -1
- package/packages/electron-client-ipc/README.md +55 -30
- package/packages/electron-client-ipc/README.zh-CN.md +73 -0
- package/packages/electron-server-ipc/README.md +42 -20
- package/packages/electron-server-ipc/README.zh-CN.md +76 -0
- package/packages/file-loaders/README.md +77 -51
- package/packages/file-loaders/README.zh-CN.md +89 -0
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +11 -8
- package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +11 -8
- package/src/app/[variants]/(main)/chat/(workspace)/features/ShareButton/index.tsx +3 -0
- package/src/app/[variants]/(main)/chat/@session/_layout/Desktop/SessionHeader.tsx +3 -0
- package/src/config/aiModels/qwen.ts +22 -2
- package/src/features/PlanIcon/index.tsx +126 -0
- package/src/features/User/PlanTag.tsx +33 -25
- package/src/libs/model-runtime/qwen/createImage.test.ts +613 -0
- package/src/libs/model-runtime/qwen/createImage.ts +218 -0
- package/src/libs/model-runtime/qwen/index.ts +2 -0
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +19 -1
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/subscription.ts +24 -0
- package/src/types/subscription.ts +7 -0
- package/apps/desktop/resources/tray-icon.png +0 -0
@@ -0,0 +1,218 @@
|
|
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:qwen');
|
8
|
+
|
9
|
+
interface QwenImageTaskResponse {
|
10
|
+
output: {
|
11
|
+
error_message?: string;
|
12
|
+
results?: Array<{
|
13
|
+
url: string;
|
14
|
+
}>;
|
15
|
+
task_id: string;
|
16
|
+
task_status: 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED';
|
17
|
+
};
|
18
|
+
request_id: string;
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* Create an image generation task with Qwen API
|
23
|
+
*/
|
24
|
+
async function createImageTask(payload: CreateImagePayload, apiKey: string): Promise<string> {
|
25
|
+
const { model, params } = payload;
|
26
|
+
// I can only say that the design of Alibaba Cloud's API is really bad; each model has a different endpoint path.
|
27
|
+
const modelEndpointMap: Record<string, string> = {
|
28
|
+
'wanx2.1-t2i-turbo':
|
29
|
+
'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis',
|
30
|
+
};
|
31
|
+
|
32
|
+
const endpoint = modelEndpointMap[model];
|
33
|
+
if (!endpoint) {
|
34
|
+
throw new Error(`Unsupported model: ${model}`);
|
35
|
+
}
|
36
|
+
log('Creating image task with model: %s, endpoint: %s', model, endpoint);
|
37
|
+
|
38
|
+
const response = await fetch(endpoint, {
|
39
|
+
body: JSON.stringify({
|
40
|
+
input: {
|
41
|
+
prompt: params.prompt,
|
42
|
+
// negativePrompt is not part of standard parameters
|
43
|
+
// but can be supported by extending the params type if needed
|
44
|
+
},
|
45
|
+
model,
|
46
|
+
parameters: {
|
47
|
+
n: 1,
|
48
|
+
...(params.seed !== undefined ? { seed: params.seed } : {}),
|
49
|
+
...(params.width && params.height
|
50
|
+
? { size: `${params.width}*${params.height}` }
|
51
|
+
: { size: '1024*1024' }),
|
52
|
+
},
|
53
|
+
}),
|
54
|
+
headers: {
|
55
|
+
'Authorization': `Bearer ${apiKey}`,
|
56
|
+
'Content-Type': 'application/json',
|
57
|
+
'X-DashScope-Async': 'enable',
|
58
|
+
},
|
59
|
+
method: 'POST',
|
60
|
+
});
|
61
|
+
|
62
|
+
if (!response.ok) {
|
63
|
+
let errorData;
|
64
|
+
try {
|
65
|
+
errorData = await response.json();
|
66
|
+
} catch {
|
67
|
+
// Failed to parse JSON error response
|
68
|
+
}
|
69
|
+
throw new Error(
|
70
|
+
`Failed to create image task (${response.status}): ${errorData?.message || response.statusText}`,
|
71
|
+
);
|
72
|
+
}
|
73
|
+
|
74
|
+
const data: QwenImageTaskResponse = await response.json();
|
75
|
+
log('Task created with ID: %s', data.output.task_id);
|
76
|
+
|
77
|
+
return data.output.task_id;
|
78
|
+
}
|
79
|
+
|
80
|
+
/**
|
81
|
+
* Query the status of an image generation task
|
82
|
+
*/
|
83
|
+
async function queryTaskStatus(taskId: string, apiKey: string): Promise<QwenImageTaskResponse> {
|
84
|
+
const endpoint = `https://dashscope.aliyuncs.com/api/v1/tasks/${taskId}`;
|
85
|
+
|
86
|
+
log('Querying task status for: %s', taskId);
|
87
|
+
|
88
|
+
const response = await fetch(endpoint, {
|
89
|
+
headers: {
|
90
|
+
Authorization: `Bearer ${apiKey}`,
|
91
|
+
},
|
92
|
+
});
|
93
|
+
|
94
|
+
if (!response.ok) {
|
95
|
+
let errorData;
|
96
|
+
try {
|
97
|
+
errorData = await response.json();
|
98
|
+
} catch {
|
99
|
+
// Failed to parse JSON error response
|
100
|
+
}
|
101
|
+
throw new Error(
|
102
|
+
`Failed to query task status (${response.status}): ${errorData?.message || response.statusText}`,
|
103
|
+
);
|
104
|
+
}
|
105
|
+
|
106
|
+
return response.json();
|
107
|
+
}
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Create image using Qwen Wanxiang API
|
111
|
+
* This implementation uses async task creation and polling
|
112
|
+
*/
|
113
|
+
export async function createQwenImage(
|
114
|
+
payload: CreateImagePayload,
|
115
|
+
options: CreateImageOptions,
|
116
|
+
): Promise<CreateImageResponse> {
|
117
|
+
const { apiKey, provider } = options;
|
118
|
+
try {
|
119
|
+
// 1. Create image generation task
|
120
|
+
const taskId = await createImageTask(payload, apiKey);
|
121
|
+
|
122
|
+
// 2. Poll task status until completion
|
123
|
+
let taskStatus: QwenImageTaskResponse | null = null;
|
124
|
+
let retries = 0;
|
125
|
+
let consecutiveFailures = 0;
|
126
|
+
const maxConsecutiveFailures = 3; // Allow up to 3 consecutive query failures
|
127
|
+
// Using Infinity for maxRetries is safe because:
|
128
|
+
// 1. Vercel runtime has execution time limits
|
129
|
+
// 2. Qwen's API will eventually return FAILED status for timed-out tasks
|
130
|
+
// 3. Our exponential backoff ensures reasonable retry intervals
|
131
|
+
const maxRetries = Infinity;
|
132
|
+
const initialRetryInterval = 500; // 500ms initial interval
|
133
|
+
const maxRetryInterval = 5000; // 5 seconds max interval
|
134
|
+
const backoffMultiplier = 1.5; // exponential backoff multiplier
|
135
|
+
|
136
|
+
while (retries < maxRetries) {
|
137
|
+
try {
|
138
|
+
taskStatus = await queryTaskStatus(taskId, apiKey);
|
139
|
+
consecutiveFailures = 0; // Reset consecutive failures on success
|
140
|
+
} catch (error) {
|
141
|
+
consecutiveFailures++;
|
142
|
+
log(
|
143
|
+
'Failed to query task status (attempt %d/%d, consecutive failures: %d/%d): %O',
|
144
|
+
retries + 1,
|
145
|
+
maxRetries,
|
146
|
+
consecutiveFailures,
|
147
|
+
maxConsecutiveFailures,
|
148
|
+
error,
|
149
|
+
);
|
150
|
+
|
151
|
+
// If we've failed too many times in a row, give up
|
152
|
+
if (consecutiveFailures >= maxConsecutiveFailures) {
|
153
|
+
throw new Error(
|
154
|
+
`Failed to query task status after ${consecutiveFailures} consecutive attempts: ${error}`,
|
155
|
+
);
|
156
|
+
}
|
157
|
+
|
158
|
+
// Wait before retrying
|
159
|
+
const currentRetryInterval = Math.min(
|
160
|
+
initialRetryInterval * Math.pow(backoffMultiplier, retries),
|
161
|
+
maxRetryInterval,
|
162
|
+
);
|
163
|
+
await new Promise((resolve) => {
|
164
|
+
setTimeout(resolve, currentRetryInterval);
|
165
|
+
});
|
166
|
+
retries++;
|
167
|
+
continue; // Skip the rest of the loop and retry
|
168
|
+
}
|
169
|
+
|
170
|
+
// At this point, taskStatus should not be null since we just got it successfully
|
171
|
+
log(
|
172
|
+
'Task %s status: %s (attempt %d/%d)',
|
173
|
+
taskId,
|
174
|
+
taskStatus!.output.task_status,
|
175
|
+
retries + 1,
|
176
|
+
maxRetries,
|
177
|
+
);
|
178
|
+
|
179
|
+
if (taskStatus!.output.task_status === 'SUCCEEDED') {
|
180
|
+
if (!taskStatus!.output.results || taskStatus!.output.results.length === 0) {
|
181
|
+
throw new Error('Task succeeded but no images generated');
|
182
|
+
}
|
183
|
+
|
184
|
+
// Return the first generated image
|
185
|
+
const imageUrl = taskStatus!.output.results[0].url;
|
186
|
+
log('Image generated successfully: %s', imageUrl);
|
187
|
+
|
188
|
+
return { imageUrl };
|
189
|
+
} else if (taskStatus!.output.task_status === 'FAILED') {
|
190
|
+
throw new Error(taskStatus!.output.error_message || 'Image generation task failed');
|
191
|
+
}
|
192
|
+
|
193
|
+
// Calculate dynamic retry interval with exponential backoff
|
194
|
+
const currentRetryInterval = Math.min(
|
195
|
+
initialRetryInterval * Math.pow(backoffMultiplier, retries),
|
196
|
+
maxRetryInterval,
|
197
|
+
);
|
198
|
+
|
199
|
+
log('Waiting %dms before next retry', currentRetryInterval);
|
200
|
+
|
201
|
+
// Wait before retrying
|
202
|
+
await new Promise((resolve) => {
|
203
|
+
setTimeout(resolve, currentRetryInterval);
|
204
|
+
});
|
205
|
+
retries++;
|
206
|
+
}
|
207
|
+
|
208
|
+
throw new Error(`Image generation timeout after ${maxRetries} attempts`);
|
209
|
+
} catch (error) {
|
210
|
+
log('Error in createQwenImage: %O', error);
|
211
|
+
|
212
|
+
throw AgentRuntimeError.createImage({
|
213
|
+
error: error as any,
|
214
|
+
errorType: 'ProviderBizError',
|
215
|
+
provider,
|
216
|
+
});
|
217
|
+
}
|
218
|
+
}
|
@@ -2,6 +2,7 @@ import { ModelProvider } from '../types';
|
|
2
2
|
import { processMultiProviderModelList } from '../utils/modelParse';
|
3
3
|
import { createOpenAICompatibleRuntime } from '../utils/openaiCompatibleFactory';
|
4
4
|
import { QwenAIStream } from '../utils/streams';
|
5
|
+
import { createQwenImage } from './createImage';
|
5
6
|
|
6
7
|
export interface QwenModelCard {
|
7
8
|
id: string;
|
@@ -73,6 +74,7 @@ export const LobeQwenAI = createOpenAICompatibleRuntime({
|
|
73
74
|
},
|
74
75
|
handleStream: QwenAIStream,
|
75
76
|
},
|
77
|
+
createImage: createQwenImage,
|
76
78
|
debug: {
|
77
79
|
chatCompletion: () => process.env.DEBUG_QWEN_CHAT_COMPLETION === '1',
|
78
80
|
},
|
@@ -52,6 +52,10 @@ export const CHAT_MODELS_BLOCK_LIST = [
|
|
52
52
|
];
|
53
53
|
|
54
54
|
type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
|
55
|
+
export type CreateImageOptions = Omit<ClientOptions, 'apiKey'> & {
|
56
|
+
apiKey: string;
|
57
|
+
provider: string;
|
58
|
+
};
|
55
59
|
|
56
60
|
export interface CustomClientOptions<T extends Record<string, any> = any> {
|
57
61
|
createChatCompletionStream?: (
|
@@ -89,7 +93,10 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
|
|
89
93
|
noUserId?: boolean;
|
90
94
|
};
|
91
95
|
constructorOptions?: ConstructorOptions<T>;
|
92
|
-
createImage?: (
|
96
|
+
createImage?: (
|
97
|
+
payload: CreateImagePayload,
|
98
|
+
options: CreateImageOptions,
|
99
|
+
) => Promise<CreateImageResponse>;
|
93
100
|
customClient?: CustomClientOptions<T>;
|
94
101
|
debug?: {
|
95
102
|
chatCompletion: () => boolean;
|
@@ -178,6 +185,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
178
185
|
models,
|
179
186
|
customClient,
|
180
187
|
responses,
|
188
|
+
createImage: customCreateImage,
|
181
189
|
}: OpenAICompatibleFactoryOptions<T>) => {
|
182
190
|
const ErrorType = {
|
183
191
|
bizError: errorType?.bizError || AgentRuntimeErrorType.ProviderBizError,
|
@@ -317,6 +325,16 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
317
325
|
}
|
318
326
|
|
319
327
|
async createImage(payload: CreateImagePayload) {
|
328
|
+
// If custom createImage implementation is provided, use it
|
329
|
+
if (customCreateImage) {
|
330
|
+
return customCreateImage(payload, {
|
331
|
+
...this._options,
|
332
|
+
apiKey: this._options.apiKey!,
|
333
|
+
provider,
|
334
|
+
});
|
335
|
+
}
|
336
|
+
|
337
|
+
// Otherwise use default OpenAI compatible implementation
|
320
338
|
const { model, params } = payload;
|
321
339
|
const log = createDebug(`lobe-image:model-runtime`);
|
322
340
|
|
@@ -22,6 +22,7 @@ import portal from './portal';
|
|
22
22
|
import providers from './providers';
|
23
23
|
import ragEval from './ragEval';
|
24
24
|
import setting from './setting';
|
25
|
+
import subscription from './subscription';
|
25
26
|
import thread from './thread';
|
26
27
|
import tool from './tool';
|
27
28
|
import topic from './topic';
|
@@ -52,6 +53,7 @@ const resources = {
|
|
52
53
|
providers,
|
53
54
|
ragEval,
|
54
55
|
setting,
|
56
|
+
subscription,
|
55
57
|
thread,
|
56
58
|
tool,
|
57
59
|
topic,
|
@@ -0,0 +1,24 @@
|
|
1
|
+
export default {
|
2
|
+
plans: {
|
3
|
+
plan: {
|
4
|
+
enterprise: {
|
5
|
+
title: '企业版',
|
6
|
+
},
|
7
|
+
free: {
|
8
|
+
title: '免费版',
|
9
|
+
},
|
10
|
+
hobby: {
|
11
|
+
title: '自助版',
|
12
|
+
},
|
13
|
+
premium: {
|
14
|
+
title: '进阶版',
|
15
|
+
},
|
16
|
+
starter: {
|
17
|
+
title: '基础版',
|
18
|
+
},
|
19
|
+
ultimate: {
|
20
|
+
title: '专业版',
|
21
|
+
},
|
22
|
+
},
|
23
|
+
},
|
24
|
+
};
|
Binary file
|