@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/apps/desktop/README.md +322 -36
  3. package/apps/desktop/README.zh-CN.md +353 -0
  4. package/apps/desktop/package.json +1 -0
  5. package/apps/desktop/resources/tray-dark.png +0 -0
  6. package/apps/desktop/resources/tray-light.png +0 -0
  7. package/apps/desktop/resources/tray.png +0 -0
  8. package/apps/desktop/src/main/const/env.ts +25 -0
  9. package/apps/desktop/src/main/core/TrayManager.ts +7 -1
  10. package/changelog/v1.json +18 -0
  11. package/locales/ar/subscription.json +24 -0
  12. package/locales/bg-BG/subscription.json +24 -0
  13. package/locales/de-DE/subscription.json +24 -0
  14. package/locales/en-US/subscription.json +24 -0
  15. package/locales/es-ES/subscription.json +24 -0
  16. package/locales/fa-IR/subscription.json +24 -0
  17. package/locales/fr-FR/subscription.json +24 -0
  18. package/locales/it-IT/subscription.json +24 -0
  19. package/locales/ja-JP/subscription.json +24 -0
  20. package/locales/ko-KR/subscription.json +24 -0
  21. package/locales/nl-NL/subscription.json +24 -0
  22. package/locales/pl-PL/subscription.json +24 -0
  23. package/locales/pt-BR/subscription.json +24 -0
  24. package/locales/ru-RU/subscription.json +24 -0
  25. package/locales/tr-TR/subscription.json +24 -0
  26. package/locales/vi-VN/subscription.json +24 -0
  27. package/locales/zh-CN/subscription.json +24 -0
  28. package/locales/zh-TW/subscription.json +24 -0
  29. package/package.json +1 -1
  30. package/packages/electron-client-ipc/README.md +55 -30
  31. package/packages/electron-client-ipc/README.zh-CN.md +73 -0
  32. package/packages/electron-server-ipc/README.md +42 -20
  33. package/packages/electron-server-ipc/README.zh-CN.md +76 -0
  34. package/packages/file-loaders/README.md +77 -51
  35. package/packages/file-loaders/README.zh-CN.md +89 -0
  36. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +11 -8
  37. package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +11 -8
  38. package/src/app/[variants]/(main)/chat/(workspace)/features/ShareButton/index.tsx +3 -0
  39. package/src/app/[variants]/(main)/chat/@session/_layout/Desktop/SessionHeader.tsx +3 -0
  40. package/src/config/aiModels/qwen.ts +22 -2
  41. package/src/features/PlanIcon/index.tsx +126 -0
  42. package/src/features/User/PlanTag.tsx +33 -25
  43. package/src/libs/model-runtime/qwen/createImage.test.ts +613 -0
  44. package/src/libs/model-runtime/qwen/createImage.ts +218 -0
  45. package/src/libs/model-runtime/qwen/index.ts +2 -0
  46. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +19 -1
  47. package/src/locales/default/index.ts +2 -0
  48. package/src/locales/default/subscription.ts +24 -0
  49. package/src/types/subscription.ts +7 -0
  50. 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?: (payload: CreateImagePayload & { client: OpenAI }) => Promise<CreateImageResponse>;
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
+ };
@@ -0,0 +1,7 @@
1
+ export enum Plans {
2
+ Free = 'free',
3
+ Hobby = 'hobby',
4
+ Premium = 'premium',
5
+ Starter = 'starter',
6
+ Ultimate = 'ultimate',
7
+ }