@lobehub/lobehub 2.0.0-next.41 → 2.0.0-next.43

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 (27) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/database/src/models/__tests__/messages/message.create.test.ts +75 -18
  5. package/packages/database/src/models/__tests__/messages/message.query.test.ts +223 -6
  6. package/packages/database/src/models/__tests__/messages/message.stats.test.ts +56 -7
  7. package/packages/database/src/models/__tests__/messages/message.update.test.ts +45 -4
  8. package/packages/database/src/models/message.ts +3 -5
  9. package/packages/utils/src/clientIP.ts +6 -6
  10. package/packages/utils/src/compressImage.ts +3 -3
  11. package/packages/utils/src/fetch/fetchSSE.ts +15 -15
  12. package/packages/utils/src/format.ts +2 -2
  13. package/packages/utils/src/merge.ts +3 -3
  14. package/packages/utils/src/parseModels.ts +3 -3
  15. package/packages/utils/src/sanitizeUTF8.ts +4 -4
  16. package/packages/utils/src/toolManifest.ts +4 -4
  17. package/packages/utils/src/trace.test.ts +359 -0
  18. package/packages/utils/src/uriParser.ts +4 -4
  19. package/src/features/ChatItem/components/Title.tsx +20 -16
  20. package/src/features/Conversation/Messages/Assistant/index.tsx +3 -2
  21. package/src/features/Conversation/Messages/Group/index.tsx +10 -3
  22. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +29 -3
  23. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +8 -2
  24. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -4
  25. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +1 -1
  26. package/src/store/global/initialState.ts +5 -0
  27. package/src/store/global/selectors/systemStatus.ts +3 -0
@@ -5,20 +5,20 @@ interface UriParserResult {
5
5
  }
6
6
 
7
7
  export const parseDataUri = (dataUri: string): UriParserResult => {
8
- // 正则表达式匹配整个 Data URI 结构
8
+ // Regular expression to match the entire Data URI structure
9
9
  const dataUriMatch = dataUri.match(/^data:([^;]+);base64,(.+)$/);
10
10
 
11
11
  if (dataUriMatch) {
12
- // 如果是合法的 Data URI
12
+ // If it's a valid Data URI
13
13
  return { base64: dataUriMatch[2], mimeType: dataUriMatch[1], type: 'base64' };
14
14
  }
15
15
 
16
16
  try {
17
17
  new URL(dataUri);
18
- // 如果是合法的 URL
18
+ // If it's a valid URL
19
19
  return { base64: null, mimeType: null, type: 'url' };
20
20
  } catch {
21
- // 既不是 Data URI 也不是合法 URL
21
+ // Neither a Data URI nor a valid URL
22
22
  return { base64: null, mimeType: null, type: null };
23
23
  }
24
24
  };
@@ -1,5 +1,5 @@
1
1
  import dayjs from 'dayjs';
2
- import { memo } from 'react';
2
+ import { CSSProperties, memo } from 'react';
3
3
  import { Flexbox } from 'react-layout-kit';
4
4
 
5
5
  import { useStyles } from '../style';
@@ -10,6 +10,7 @@ export interface TitleProps {
10
10
  className?: string;
11
11
  placement?: ChatItemProps['placement'];
12
12
  showTitle?: ChatItemProps['showTitle'];
13
+ style?: CSSProperties;
13
14
  time?: ChatItemProps['time'];
14
15
  titleAddon?: ChatItemProps['titleAddon'];
15
16
  }
@@ -27,21 +28,24 @@ const formatTime = (time: number): string => {
27
28
  }
28
29
  };
29
30
 
30
- const Title = memo<TitleProps>(({ showTitle, placement, time, avatar, titleAddon, className }) => {
31
- const { styles, cx } = useStyles({ placement, showTitle, time });
31
+ const Title = memo<TitleProps>(
32
+ ({ showTitle, placement, time, avatar, titleAddon, className, style }) => {
33
+ const { styles, cx } = useStyles({ placement, showTitle, time });
32
34
 
33
- return (
34
- <Flexbox
35
- align={'center'}
36
- className={cx(styles.name, className)}
37
- direction={placement === 'left' ? 'horizontal' : 'horizontal-reverse'}
38
- gap={4}
39
- >
40
- {showTitle ? avatar.title || 'untitled' : undefined}
41
- {showTitle ? titleAddon : undefined}
42
- {time && <time>{formatTime(time)}</time>}
43
- </Flexbox>
44
- );
45
- });
35
+ return (
36
+ <Flexbox
37
+ align={'center'}
38
+ className={cx(styles.name, className)}
39
+ direction={placement === 'left' ? 'horizontal' : 'horizontal-reverse'}
40
+ gap={4}
41
+ style={style}
42
+ >
43
+ {showTitle ? avatar.title || 'untitled' : undefined}
44
+ {showTitle ? titleAddon : undefined}
45
+ {time && <time>{formatTime(time)}</time>}
46
+ </Flexbox>
47
+ );
48
+ },
49
+ );
46
50
 
47
51
  export default Title;
@@ -4,6 +4,7 @@ import { LOADING_FLAT } from '@lobechat/const';
4
4
  import { UIChatMessage } from '@lobechat/types';
5
5
  import { Tag } from '@lobehub/ui';
6
6
  import { useResponsive } from 'antd-style';
7
+ import isEqual from 'fast-deep-equal';
7
8
  import { ReactNode, memo, useCallback, useMemo } from 'react';
8
9
  import { useTranslation } from 'react-i18next';
9
10
  import { Flexbox } from 'react-layout-kit';
@@ -213,12 +214,12 @@ const AssistantMessage = memo<AssistantMessageProps>((props) => {
213
214
  onClick={onAvatarClick}
214
215
  placement={placement}
215
216
  size={MOBILE_AVATAR_SIZE}
216
- style={{ marginTop: 6 }}
217
217
  />
218
218
  <Title
219
219
  avatar={avatar}
220
220
  placement={placement}
221
221
  showTitle
222
+ style={{ marginBlockEnd: 0 }}
222
223
  time={createdAt}
223
224
  titleAddon={dmIndicator}
224
225
  />
@@ -274,6 +275,6 @@ const AssistantMessage = memo<AssistantMessageProps>((props) => {
274
275
  {mobile && <BorderSpacing borderSpacing={MOBILE_AVATAR_SIZE} />}
275
276
  </Flexbox>
276
277
  );
277
- });
278
+ }, isEqual);
278
279
 
279
280
  export default AssistantMessage;
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { UIChatMessage } from '@lobechat/types';
4
4
  import { useResponsive } from 'antd-style';
5
+ import isEqual from 'fast-deep-equal';
5
6
  import { memo, useCallback } from 'react';
6
7
  import { Flexbox } from 'react-layout-kit';
7
8
 
@@ -49,6 +50,7 @@ const GroupMessage = memo<GroupMessageProps>((props) => {
49
50
  provider,
50
51
  } = props;
51
52
  const avatar = meta;
53
+ console.log('render');
52
54
  const { mobile } = useResponsive();
53
55
  const placement = 'left';
54
56
  const type = useAgentStore(agentChatConfigSelectors.displayMode);
@@ -96,9 +98,14 @@ const GroupMessage = memo<GroupMessageProps>((props) => {
96
98
  onClick={onAvatarClick}
97
99
  placement={placement}
98
100
  size={MOBILE_AVATAR_SIZE}
99
- style={{ marginTop: 6 }}
100
101
  />
101
- <Title avatar={avatar} placement={placement} showTitle time={createdAt} />
102
+ <Title
103
+ avatar={avatar}
104
+ placement={placement}
105
+ showTitle
106
+ style={{ marginBlockEnd: 0 }}
107
+ time={createdAt}
108
+ />
102
109
  </Flexbox>
103
110
  {isEditing && contentId ? (
104
111
  <EditState content={lastAssistantMsg?.content} id={contentId} />
@@ -141,6 +148,6 @@ const GroupMessage = memo<GroupMessageProps>((props) => {
141
148
  {mobile && <BorderSpacing borderSpacing={MOBILE_AVATAR_SIZE} />}
142
149
  </Flexbox>
143
150
  );
144
- });
151
+ }, isEqual);
145
152
 
146
153
  export default GroupMessage;
@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next';
8
8
  import { Center, Flexbox } from 'react-layout-kit';
9
9
 
10
10
  import InfoTooltip from '@/components/InfoTooltip';
11
+ import { useIsMobile } from '@/hooks/useIsMobile';
11
12
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
12
13
  import { useGlobalStore } from '@/store/global';
13
14
  import { systemStatusSelectors } from '@/store/global/selectors';
@@ -27,6 +28,11 @@ interface TokenDetailProps {
27
28
  const TokenDetail = memo<TokenDetailProps>(({ meta, model, provider }) => {
28
29
  const { t } = useTranslation('chat');
29
30
  const theme = useTheme();
31
+ const isMobile = useIsMobile();
32
+
33
+ // 使用 systemStatus 管理短格式显示状态
34
+ const isShortFormat = useGlobalStore(systemStatusSelectors.tokenDisplayFormatShort);
35
+ const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
30
36
 
31
37
  const modelCard = useAiInfraStore(aiModelSelectors.getModelCard(model, provider));
32
38
  const isShowCredit = useGlobalStore(systemStatusSelectors.isShowCredit) && !!modelCard?.pricing;
@@ -211,12 +217,32 @@ const TokenDetail = memo<TokenDetailProps>(({ meta, model, provider }) => {
211
217
  </Flexbox>
212
218
  }
213
219
  placement={'top'}
214
- trigger={['hover', 'click']}
220
+ trigger={isMobile ? ['click'] : ['hover']}
215
221
  >
216
- <Center gap={2} horizontal style={{ cursor: 'default' }}>
222
+ <Center
223
+ gap={2}
224
+ horizontal
225
+ onClick={(e) => {
226
+ // 移动端:让 Popover 处理点击事件
227
+ if (isMobile) return;
228
+
229
+ // 桌面端:阻止 Popover 并切换格式
230
+ e.preventDefault();
231
+ e.stopPropagation();
232
+ updateSystemStatus({ tokenDisplayFormatShort: !isShortFormat });
233
+ }}
234
+ style={{ cursor: isMobile ? 'default' : 'pointer' }}
235
+ >
217
236
  <Icon icon={isShowCredit ? BadgeCent : CoinsIcon} />
218
237
  <AnimatedNumber
219
- formatter={(value) => (formatShortenNumber(value) as string).toLowerCase?.()}
238
+ duration={1500}
239
+ formatter={(value) => {
240
+ const roundedValue = Math.round(value);
241
+ if (isShortFormat) {
242
+ return (formatShortenNumber(roundedValue) as string).toLowerCase?.();
243
+ }
244
+ return new Intl.NumberFormat('en-US').format(roundedValue);
245
+ }}
220
246
  // Force remount when switching between token/credit to prevent unwanted animation
221
247
  // See: https://github.com/lobehub/lobe-chat/pull/10098
222
248
  key={isShowCredit ? 'credit' : 'token'}
@@ -166,7 +166,11 @@ export const conversationLifecycle: StateCreator<
166
166
  topicId = data.topicId;
167
167
  }
168
168
 
169
- get().replaceMessages(data.messages, { sessionId: activeId, topicId: topicId });
169
+ get().replaceMessages(data.messages, {
170
+ sessionId: activeId,
171
+ topicId: topicId,
172
+ action: 'sendMessage/serverResponse',
173
+ });
170
174
 
171
175
  if (data.isCreateNewTopic && data.topicId) {
172
176
  await get().switchTopic(data.topicId, true);
@@ -250,7 +254,9 @@ export const conversationLifecycle: StateCreator<
250
254
  .map((f) => f?.id)
251
255
  .filter(Boolean) as string[];
252
256
 
253
- await getAgentStoreState().addFilesToAgent(userFiles, false);
257
+ if (userFiles.length > 0) {
258
+ await getAgentStoreState().addFilesToAgent(userFiles, false);
259
+ }
254
260
  } catch (e) {
255
261
  console.error(e);
256
262
  } finally {
@@ -521,10 +521,7 @@ export const streamingExecutor: StateCreator<
521
521
  // Handle completion and error events
522
522
  for (const event of result.events) {
523
523
  if (event.type === 'done') {
524
- log('[internal_execAgentRuntime] Received done event, syncing to database');
525
- // Sync final state to database
526
- const finalMessages = get().messagesMap[messageKey] || [];
527
- get().replaceMessages(finalMessages);
524
+ log('[internal_execAgentRuntime] Received done event');
528
525
  }
529
526
 
530
527
  if (event.type === 'error') {
@@ -195,7 +195,7 @@ export const messageOptimisticUpdate: StateCreator<
195
195
  );
196
196
 
197
197
  if (result && result.success && result.messages) {
198
- replaceMessages(result.messages);
198
+ replaceMessages(result.messages, { action: 'optimisticUpdateMessageContent' });
199
199
  } else {
200
200
  await refreshMessages();
201
201
  }
@@ -99,6 +99,10 @@ export interface SystemStatus {
99
99
  * theme mode
100
100
  */
101
101
  themeMode?: ThemeMode;
102
+ /**
103
+ * 是否使用短格式显示 token
104
+ */
105
+ tokenDisplayFormatShort?: boolean;
102
106
  zenMode?: boolean;
103
107
  }
104
108
 
@@ -151,6 +155,7 @@ export const INITIAL_STATUS = {
151
155
  showSystemRole: false,
152
156
  systemRoleExpandedMap: {},
153
157
  themeMode: 'auto',
158
+ tokenDisplayFormatShort: true,
154
159
  zenMode: false,
155
160
  } satisfies SystemStatus;
156
161
 
@@ -66,6 +66,8 @@ const getAgentSystemRoleExpanded =
66
66
  const disabledModelProvidersSortType = (s: GlobalState) =>
67
67
  s.status.disabledModelProvidersSortType || 'default';
68
68
  const disabledModelsSortType = (s: GlobalState) => s.status.disabledModelsSortType || 'default';
69
+ const tokenDisplayFormatShort = (s: GlobalState) =>
70
+ s.status.tokenDisplayFormatShort !== undefined ? s.status.tokenDisplayFormatShort : true;
69
71
 
70
72
  export const systemStatusSelectors = {
71
73
  chatInputHeight,
@@ -99,5 +101,6 @@ export const systemStatusSelectors = {
99
101
  showSystemRole,
100
102
  systemStatus,
101
103
  themeMode,
104
+ tokenDisplayFormatShort,
102
105
  wideScreen,
103
106
  };