@lobehub/lobehub 2.0.0-next.40 → 2.0.0-next.42

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 (45) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/chat.json +1 -0
  4. package/locales/bg-BG/chat.json +1 -0
  5. package/locales/de-DE/chat.json +1 -0
  6. package/locales/en-US/chat.json +1 -0
  7. package/locales/es-ES/chat.json +1 -0
  8. package/locales/fa-IR/chat.json +1 -0
  9. package/locales/fr-FR/chat.json +1 -0
  10. package/locales/it-IT/chat.json +1 -0
  11. package/locales/ja-JP/chat.json +1 -0
  12. package/locales/ko-KR/chat.json +1 -0
  13. package/locales/nl-NL/chat.json +1 -0
  14. package/locales/pl-PL/chat.json +1 -0
  15. package/locales/pt-BR/chat.json +1 -0
  16. package/locales/ru-RU/chat.json +1 -0
  17. package/locales/tr-TR/chat.json +1 -0
  18. package/locales/vi-VN/chat.json +1 -0
  19. package/locales/zh-CN/chat.json +1 -0
  20. package/locales/zh-TW/chat.json +1 -0
  21. package/package.json +1 -1
  22. package/packages/database/src/models/__tests__/messages/message.create.test.ts +549 -0
  23. package/packages/database/src/models/__tests__/messages/message.delete.test.ts +481 -0
  24. package/packages/database/src/models/__tests__/messages/message.query.test.ts +1187 -0
  25. package/packages/database/src/models/__tests__/messages/message.stats.test.ts +633 -0
  26. package/packages/database/src/models/__tests__/messages/message.update.test.ts +757 -0
  27. package/packages/database/src/models/message.ts +5 -55
  28. package/packages/utils/src/clientIP.ts +6 -6
  29. package/packages/utils/src/compressImage.ts +3 -3
  30. package/packages/utils/src/fetch/fetchSSE.ts +15 -15
  31. package/packages/utils/src/format.ts +2 -2
  32. package/packages/utils/src/merge.ts +3 -3
  33. package/packages/utils/src/parseModels.ts +3 -3
  34. package/packages/utils/src/sanitizeUTF8.ts +4 -4
  35. package/packages/utils/src/toolManifest.ts +4 -4
  36. package/packages/utils/src/trace.test.ts +359 -0
  37. package/packages/utils/src/uriParser.ts +4 -4
  38. package/src/features/ChatItem/components/Title.tsx +20 -16
  39. package/src/features/Conversation/Messages/Assistant/index.tsx +3 -2
  40. package/src/features/Conversation/Messages/Group/index.tsx +10 -3
  41. package/src/server/services/message/index.ts +14 -4
  42. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +8 -2
  43. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -4
  44. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +1 -1
  45. package/packages/database/src/models/__tests__/message.test.ts +0 -2632
@@ -14,7 +14,6 @@ import {
14
14
  UIChatMessage,
15
15
  UpdateMessageParams,
16
16
  UpdateMessageRAGParams,
17
- UpdateMessageResult,
18
17
  } from '@lobechat/types';
19
18
  import type { HeatmapsProps } from '@lobehub/charts';
20
19
  import dayjs from 'dayjs';
@@ -213,7 +212,7 @@ export class MessageModel {
213
212
  .from(messageQueries)
214
213
  .where(inArray(messageQueries.messageId, messageIds));
215
214
 
216
- const mappedMessages = result.map(
215
+ return result.map(
217
216
  ({ model, provider, translate, ttsId, ttsFile, ttsContentMd5, ttsVoice, ...item }) => {
218
217
  const messageQuery = messageQueriesList.find((relation) => relation.messageId === item.id);
219
218
  return {
@@ -222,7 +221,7 @@ export class MessageModel {
222
221
  .filter((relation) => relation.messageId === item.id)
223
222
  .map((c) => ({
224
223
  ...c,
225
- similarity: Number(c.similarity) ?? undefined,
224
+ similarity: c.similarity === null ? undefined : Number(c.similarity),
226
225
  })),
227
226
 
228
227
  extra: {
@@ -267,8 +266,6 @@ export class MessageModel {
267
266
  } as unknown as UIChatMessage;
268
267
  },
269
268
  );
270
-
271
- return mappedMessages;
272
269
  };
273
270
 
274
271
  findById = async (id: string) => {
@@ -424,7 +421,7 @@ export class MessageModel {
424
421
  for (const item of result) {
425
422
  if (item?.date) {
426
423
  const dateStr = dayjs(item.date as string).format('YYYY-MM-DD');
427
- dateCountMap.set(dateStr, Number(item.count) || 0);
424
+ dateCountMap.set(dateStr, item.count);
428
425
  }
429
426
  }
430
427
 
@@ -550,13 +547,7 @@ export class MessageModel {
550
547
  update = async (
551
548
  id: string,
552
549
  { imageList, ...message }: Partial<UpdateMessageParams>,
553
- options?: {
554
- groupAssistantMessages?: boolean;
555
- postProcessUrl?: (path: string | null, file: { fileType: string }) => Promise<string>;
556
- sessionId?: string | null;
557
- topicId?: string | null;
558
- },
559
- ): Promise<UpdateMessageResult> => {
550
+ ): Promise<{ success: boolean }> => {
560
551
  try {
561
552
  await this.db.transaction(async (trx) => {
562
553
  // 1. insert message files
@@ -574,22 +565,6 @@ export class MessageModel {
574
565
  .where(and(eq(messages.id, id), eq(messages.userId, this.userId)));
575
566
  });
576
567
 
577
- // if sessionId or topicId provided, return the updated message list
578
- if (options?.sessionId !== undefined || options?.topicId !== undefined) {
579
- const messageList = await this.query(
580
- {
581
- sessionId: options.sessionId,
582
- topicId: options.topicId,
583
- },
584
- {
585
- groupAssistantMessages: options.groupAssistantMessages ?? false,
586
- postProcessUrl: options.postProcessUrl,
587
- },
588
- );
589
-
590
- return { messages: messageList, success: true };
591
- }
592
-
593
568
  return { success: true };
594
569
  } catch (error) {
595
570
  console.error('Update message error:', error);
@@ -610,16 +585,7 @@ export class MessageModel {
610
585
  .where(and(eq(messages.userId, this.userId), eq(messages.id, id)));
611
586
  };
612
587
 
613
- updatePluginState = async (
614
- id: string,
615
- state: Record<string, any>,
616
- options?: {
617
- groupAssistantMessages?: boolean;
618
- postProcessUrl?: (path: string | null, file: { fileType: string }) => Promise<string>;
619
- sessionId?: string | null;
620
- topicId?: string | null;
621
- },
622
- ): Promise<UpdateMessageResult> => {
588
+ updatePluginState = async (id: string, state: Record<string, any>): Promise<void> => {
623
589
  const item = await this.db.query.messagePlugins.findFirst({
624
590
  where: eq(messagePlugins.id, id),
625
591
  });
@@ -629,22 +595,6 @@ export class MessageModel {
629
595
  .update(messagePlugins)
630
596
  .set({ state: merge(item.state || {}, state) })
631
597
  .where(eq(messagePlugins.id, id));
632
-
633
- // Return updated messages if sessionId or topicId is provided
634
- if (options?.sessionId !== undefined || options?.topicId !== undefined) {
635
- const messageList = await this.query(
636
- {
637
- sessionId: options.sessionId,
638
- topicId: options.topicId,
639
- },
640
- {
641
- groupAssistantMessages: options.groupAssistantMessages ?? false,
642
- postProcessUrl: options.postProcessUrl,
643
- },
644
- );
645
- return { messages: messageList, success: true };
646
- }
647
- return { success: true };
648
598
  };
649
599
 
650
600
  updateMessagePlugin = async (id: string, value: Partial<MessagePluginItem>) => {
@@ -1,16 +1,16 @@
1
1
  /**
2
- * 获取客户端 IP
3
- * @param headers HTTP 请求头
2
+ * Get client IP address
3
+ * @param headers HTTP request headers
4
4
  */
5
5
  export const getClientIP = (headers: Headers): string => {
6
- // 按优先级顺序检查各种 IP
6
+ // Check various IP headers in priority order
7
7
  const ipHeaders = [
8
8
  'cf-connecting-ip', // Cloudflare
9
9
  'x-real-ip', // Nginx proxy
10
- 'x-forwarded-for', // 标准代理头
10
+ 'x-forwarded-for', // Standard proxy header
11
11
  'x-client-ip', // Apache
12
12
  'true-client-ip', // Akamai and Cloudflare
13
- 'x-cluster-client-ip', // 负载均衡
13
+ 'x-cluster-client-ip', // Load balancer
14
14
  'forwarded', // RFC 7239
15
15
  'fastly-client-ip', // Fastly CDN
16
16
  'x-forwarded', // General forward
@@ -21,7 +21,7 @@ export const getClientIP = (headers: Headers): string => {
21
21
  const value = headers.get(header);
22
22
  if (!value) continue;
23
23
 
24
- // 处理可能包含多个 IP 的情况(比如 x-forwarded-for
24
+ // Handle cases where multiple IPs may be present (e.g., x-forwarded-for)
25
25
  if (header.toLowerCase() === 'x-forwarded-for') {
26
26
  const firstIP = value.split(',')[0].trim();
27
27
  if (firstIP) return firstIP;
@@ -1,16 +1,16 @@
1
1
  const compressImage = ({ img, type = 'image/webp' }: { img: HTMLImageElement; type?: string }) => {
2
- // 设置最大宽高
2
+ // Set maximum width and height
3
3
  const maxWidth = 2160;
4
4
  const maxHeight = 2160;
5
5
  let width = img.width;
6
6
  let height = img.height;
7
7
 
8
8
  if (width > height && width > maxWidth) {
9
- // 如果图片宽度大于高度且大于最大宽度限制
9
+ // If image width is greater than height and exceeds maximum width limit
10
10
  width = maxWidth;
11
11
  height = Math.round((maxWidth / img.width) * img.height);
12
12
  } else if (height > width && height > maxHeight) {
13
- // 如果图片高度大于宽度且大于最大高度限制
13
+ // If image height is greater than width and exceeds maximum height limit
14
14
  height = maxHeight;
15
15
  width = Math.round((maxHeight / img.height) * img.width);
16
16
  }
@@ -91,7 +91,7 @@ export interface FetchSSEOptions {
91
91
  responseAnimation?: ResponseAnimation;
92
92
  }
93
93
 
94
- const START_ANIMATION_SPEED = 10; // 默认起始速度
94
+ const START_ANIMATION_SPEED = 10; // Default starting speed
95
95
 
96
96
  const createSmoothMessage = (params: {
97
97
  onTextUpdate: (delta: string, text: string) => void;
@@ -106,7 +106,7 @@ const createSmoothMessage = (params: {
106
106
  let lastFrameTime = 0;
107
107
  let accumulatedTime = 0;
108
108
  let currentSpeed = startSpeed;
109
- let lastQueueLength = 0; // 记录上一帧的队列长度
109
+ let lastQueueLength = 0; // Record the queue length from the previous frame
110
110
 
111
111
  const stopAnimation = () => {
112
112
  isAnimationActive = false;
@@ -127,7 +127,7 @@ const createSmoothMessage = (params: {
127
127
  lastFrameTime = performance.now();
128
128
  accumulatedTime = 0;
129
129
  currentSpeed = speed;
130
- lastQueueLength = 0; // 重置上一帧队列长度
130
+ lastQueueLength = 0; // Reset previous frame queue length
131
131
 
132
132
  const updateText = (timestamp: number) => {
133
133
  if (!isAnimationActive) {
@@ -144,9 +144,9 @@ const createSmoothMessage = (params: {
144
144
 
145
145
  let charsToProcess = 0;
146
146
  if (outputQueue.length > 0) {
147
- // 更平滑的速度调整
147
+ // Smoother speed adjustment
148
148
  const targetSpeed = Math.max(speed, outputQueue.length);
149
- // 根据队列长度变化调整速度变化率
149
+ // Adjust speed change rate based on queue length changes
150
150
  const speedChangeRate = Math.abs(outputQueue.length - lastQueueLength) * 0.0008 + 0.005;
151
151
  currentSpeed += (targetSpeed - currentSpeed) * speedChangeRate;
152
152
 
@@ -157,7 +157,7 @@ const createSmoothMessage = (params: {
157
157
  accumulatedTime -= (charsToProcess * 1000) / currentSpeed;
158
158
 
159
159
  let actualChars = Math.min(charsToProcess, outputQueue.length);
160
- // actualChars = Math.min(speed, actualChars); // 速度上限
160
+ // actualChars = Math.min(speed, actualChars); // Speed upper limit
161
161
 
162
162
  // if (actualChars * 2 < outputQueue.length && /[\dA-Za-z]/.test(outputQueue[actualChars])) {
163
163
  // actualChars *= 2;
@@ -168,7 +168,7 @@ const createSmoothMessage = (params: {
168
168
  params.onTextUpdate(charsToAdd, buffer);
169
169
  }
170
170
 
171
- lastQueueLength = outputQueue.length; // 更新上一帧的队列长度
171
+ lastQueueLength = outputQueue.length; // Update previous frame queue length
172
172
 
173
173
  if (outputQueue.length > 0 && isAnimationActive) {
174
174
  animationFrameId = requestAnimationFrame(updateText);
@@ -219,7 +219,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
219
219
  const shouldSkipTextProcessing = text === 'none';
220
220
  const textSmoothing = text === 'smooth';
221
221
 
222
- // 添加文本buffer和计时器相关变量
222
+ // Add text buffer and timer related variables
223
223
  let textBuffer = '';
224
224
  let bufferTimer: ReturnType<typeof setTimeout> | null = null;
225
225
  const BUFFER_INTERVAL = 300; // 300ms
@@ -254,7 +254,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
254
254
  let thinkingBuffer = '';
255
255
  let thinkingBufferTimer: ReturnType<typeof setTimeout> | null = null;
256
256
 
257
- // 创建一个函数来处理buffer的刷新
257
+ // Create a function to handle buffer flushing
258
258
  const flushThinkingBuffer = () => {
259
259
  if (thinkingBuffer) {
260
260
  options.onMessageHandle?.({ text: thinkingBuffer, type: 'reasoning' });
@@ -349,10 +349,10 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
349
349
  } else {
350
350
  output += data;
351
351
 
352
- // 使用buffer机制
352
+ // Use buffer mechanism
353
353
  textBuffer += data;
354
354
 
355
- // 如果还没有设置计时器,创建一个
355
+ // If timer not set yet, create one
356
356
  if (!bufferTimer) {
357
357
  bufferTimer = setTimeout(() => {
358
358
  flushTextBuffer();
@@ -395,10 +395,10 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
395
395
  } else {
396
396
  thinking += data;
397
397
 
398
- // 使用buffer机制
398
+ // Use buffer mechanism
399
399
  thinkingBuffer += data;
400
400
 
401
- // 如果还没有设置计时器,创建一个
401
+ // If timer not set yet, create one
402
402
  if (!thinkingBufferTimer) {
403
403
  thinkingBufferTimer = setTimeout(() => {
404
404
  flushThinkingBuffer();
@@ -421,7 +421,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
421
421
  },
422
422
  onopen: async (res) => {
423
423
  response = res.clone();
424
- // 如果不 ok 说明有请求错误
424
+ // If not ok, it means there is a request error
425
425
  if (!response.ok) {
426
426
  throw await getMessageError(res);
427
427
  }
@@ -434,7 +434,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
434
434
  if (response) {
435
435
  textController.stopAnimation();
436
436
 
437
- // 确保所有缓冲区数据都被处理
437
+ // Ensure all buffered data is processed
438
438
  if (bufferTimer) {
439
439
  clearTimeout(bufferTimer);
440
440
  flushTextBuffer();
@@ -64,10 +64,10 @@ export const formatShortenNumber = (num: any) => {
64
64
  if (!num && num !== 0) return '--';
65
65
  if (!isNumber(num)) return num;
66
66
 
67
- // 使用Intl.NumberFormat来添加千分号
67
+ // Use Intl.NumberFormat to add thousand separators
68
68
  const formattedWithComma = new Intl.NumberFormat('en-US').format(num);
69
69
 
70
- // 格式化为 K M
70
+ // Format as K or M
71
71
  if (num >= 1_000_000) {
72
72
  return (num / 1_000_000).toFixed(1) + 'M';
73
73
  } else if (num >= 10_000) {
@@ -1,7 +1,7 @@
1
1
  import { merge as _merge, isEmpty, mergeWith } from 'lodash-es';
2
2
 
3
3
  /**
4
- * 用于合并对象,如果是数组则直接替换
4
+ * Merge objects, directly replace if it's an array
5
5
  * @param target
6
6
  * @param source
7
7
  */
@@ -24,7 +24,7 @@ export const mergeArrayById = <T extends MergeableItem>(defaultItems: T[], userI
24
24
  // Create a map of default items for faster lookup
25
25
  const defaultItemsMap = new Map(defaultItems.map((item) => [item.id, item]));
26
26
 
27
- // 使用 Map 存储合并结果,这样重复 ID 的后项会自然覆盖前项
27
+ // Use Map to store merged results, so duplicate IDs naturally overwrite previous entries
28
28
  const mergedItemsMap = new Map<string, T>();
29
29
 
30
30
  // Process user items with default metadata
@@ -51,7 +51,7 @@ export const mergeArrayById = <T extends MergeableItem>(defaultItems: T[], userI
51
51
  mergedItemsMap.set(userItem.id, mergedItem);
52
52
  });
53
53
 
54
- // 添加只在默认配置中存在的项
54
+ // Add items that only exist in default configuration
55
55
  defaultItems.forEach((item) => {
56
56
  if (!mergedItemsMap.has(item.id)) {
57
57
  mergedItemsMap.set(item.id, item);
@@ -138,16 +138,16 @@ export const transformToAiModelList = async ({
138
138
  const modelConfig = await parseModelString(providerId, modelString, withDeploymentName);
139
139
  let chatModels = modelConfig.removeAll ? [] : defaultModels;
140
140
 
141
- // 处理移除逻辑
141
+ // Handle removal logic
142
142
  if (!modelConfig.removeAll) {
143
143
  chatModels = chatModels.filter((m) => !modelConfig.removed.includes(m.id));
144
144
  }
145
145
 
146
- // 异步获取配置
146
+ // Asynchronously load configuration
147
147
  const { LOBE_DEFAULT_MODEL_LIST } = await import('model-bank');
148
148
 
149
149
  return produce(chatModels, (draft) => {
150
- // 处理添加或替换逻辑
150
+ // Handle add or replace logic
151
151
  for (const toAddModel of modelConfig.add) {
152
152
  // first try to find the model in LOBE_DEFAULT_MODEL_LIST to confirm if it is a known model
153
153
  let knownModel = LOBE_DEFAULT_MODEL_LIST.find(
@@ -3,12 +3,12 @@
3
3
  * @param str
4
4
  */
5
5
  export const sanitizeUTF8 = (str: string) => {
6
- // 移除替换字符 (0xFFFD) 和其他非法字符
6
+ // Remove replacement character (0xFFFD) and other illegal characters
7
7
  return (
8
8
  str
9
- .replaceAll('�', '') // 移除 Unicode 替换字符
9
+ .replaceAll('�', '') // Remove Unicode replacement character
10
10
  // eslint-disable-next-line no-control-regex
11
- .replaceAll(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '') // 移除控制字符
11
+ .replaceAll(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '') // Remove control characters
12
12
  .replaceAll(/[\uD800-\uDFFF]/g, '')
13
- ); // 移除未配对的代理项码点
13
+ ); // Remove unpaired surrogate code points
14
14
  };
@@ -4,7 +4,7 @@ import { LobeChatPluginManifest, pluginManifestSchema } from '@lobehub/chat-plug
4
4
  import { API_ENDPOINTS } from '@/services/_url';
5
5
 
6
6
  const fetchJSON = async <T = any>(url: string, proxy = false): Promise<T> => {
7
- // 2. 发送请求
7
+ // 2. Send request
8
8
  let res: Response;
9
9
  try {
10
10
  res = await (proxy ? fetch(API_ENDPOINTS.proxy, { body: url, method: 'POST' }) : fetch(url));
@@ -80,12 +80,12 @@ export const getToolManifest = async (
80
80
  url?: string,
81
81
  useProxy: boolean = false,
82
82
  ): Promise<LobeChatPluginManifest> => {
83
- // 1. valid plugin
83
+ // 1. Validate plugin
84
84
  if (!url) {
85
85
  throw new TypeError('noManifest');
86
86
  }
87
87
 
88
- // 2. 发送请求
88
+ // 2. Send request
89
89
  let data = await fetchJSON<LobeChatPluginManifest>(url, useProxy);
90
90
 
91
91
  // @ts-ignore
@@ -94,7 +94,7 @@ export const getToolManifest = async (
94
94
  if (data['description_for_model']) {
95
95
  data = convertOpenAIManifestToLobeManifest(data as any);
96
96
  }
97
- // 3. 校验插件文件格式规范
97
+ // 3. Validate plugin file format specification
98
98
  const parser = pluginManifestSchema.safeParse(data);
99
99
 
100
100
  if (!parser.success) {