@lobehub/lobehub 2.0.0-next.343 → 2.0.0-next.345

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 (169) hide show
  1. package/.cursor/rules/i18n.mdc +1 -1
  2. package/.cursor/rules/modal-imperative.mdc +162 -0
  3. package/.cursor/rules/rules-index.mdc +1 -0
  4. package/.env.example +0 -14
  5. package/.eslintrc.js +8 -1
  6. package/CHANGELOG.md +66 -0
  7. package/Dockerfile +3 -13
  8. package/README.md +3 -5
  9. package/README.zh-CN.md +3 -5
  10. package/changelog/v1.json +24 -0
  11. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.mdx +11 -42
  12. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.zh-CN.mdx +10 -41
  13. package/e2e/src/support/webServer.ts +2 -0
  14. package/locales/ar/error.json +0 -4
  15. package/locales/bg-BG/error.json +0 -4
  16. package/locales/de-DE/error.json +0 -4
  17. package/locales/en-US/error.json +0 -4
  18. package/locales/es-ES/error.json +0 -4
  19. package/locales/fa-IR/error.json +0 -4
  20. package/locales/fr-FR/error.json +0 -4
  21. package/locales/it-IT/error.json +0 -4
  22. package/locales/ja-JP/error.json +0 -4
  23. package/locales/ko-KR/error.json +0 -4
  24. package/locales/nl-NL/error.json +0 -4
  25. package/locales/pl-PL/error.json +0 -4
  26. package/locales/pt-BR/error.json +0 -4
  27. package/locales/ru-RU/error.json +0 -4
  28. package/locales/tr-TR/error.json +0 -4
  29. package/locales/vi-VN/error.json +0 -4
  30. package/locales/zh-CN/error.json +0 -4
  31. package/locales/zh-TW/error.json +0 -4
  32. package/package.json +7 -9
  33. package/packages/builtin-agents/package.json +2 -0
  34. package/packages/builtin-agents/src/agents/agent-builder/index.ts +4 -2
  35. package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +4 -2
  36. package/packages/builtin-agents/src/agents/page-agent/index.ts +5 -2
  37. package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +161 -12
  38. package/packages/context-engine/src/engine/messages/MessagesEngine.ts +9 -9
  39. package/packages/context-engine/src/providers/GroupContextInjector.ts +19 -33
  40. package/packages/context-engine/src/providers/__tests__/GroupContextInjector.test.ts +79 -43
  41. package/packages/context-engine/src/providers/__tests__/__snapshots__/GroupContextInjector.test.ts.snap +5 -15
  42. package/packages/database/src/repositories/userMemory/__tests__/UserMemoryTopicRepository.test.ts +24 -3
  43. package/packages/model-bank/src/modelProviders/comfyui.ts +0 -1
  44. package/packages/model-bank/src/modelProviders/fal.ts +0 -1
  45. package/packages/types/src/fetch.ts +1 -2
  46. package/packages/utils/src/server/__tests__/auth.test.ts +0 -47
  47. package/packages/utils/src/server/auth.ts +1 -9
  48. package/scripts/_shared/checkDeprecatedClerkEnv.js +42 -0
  49. package/scripts/changelogWorkflow/buildStaticChangelog.ts +2 -1
  50. package/scripts/clerk-to-betterauth/_internal/types.ts +53 -20
  51. package/scripts/clerk-to-betterauth/export-clerk-users-with-api.ts +43 -36
  52. package/scripts/countEnWord.ts +1 -1
  53. package/scripts/electronWorkflow/modifiers/appCode.mts +2 -131
  54. package/scripts/i18nWorkflow/protectedPatterns.ts +1 -2
  55. package/scripts/prebuild.mts +10 -8
  56. package/scripts/serverLauncher/startServer.js +23 -5
  57. package/src/app/(backend)/middleware/auth/index.test.ts +8 -4
  58. package/src/app/(backend)/middleware/auth/index.ts +0 -15
  59. package/src/app/(backend)/middleware/auth/utils.test.ts +0 -28
  60. package/src/app/(backend)/middleware/auth/utils.ts +2 -17
  61. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +3 -51
  62. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -4
  63. package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +7 -6
  64. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -16
  65. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/index.tsx +1 -1
  66. package/src/app/[variants]/(main)/home/features/InputArea/SkillInstallBanner.tsx +13 -13
  67. package/src/app/[variants]/(main)/home/features/RecentPage/Item.tsx +2 -2
  68. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +3 -21
  69. package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -2
  70. package/src/app/[variants]/(main)/settings/security/index.tsx +1 -22
  71. package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +12 -14
  72. package/src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx +8 -14
  73. package/src/app/[variants]/(main)/settings/skill/index.tsx +7 -5
  74. package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +2 -35
  75. package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +0 -20
  76. package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -2
  77. package/src/app/[variants]/(mobile)/me/profile/features/Category.tsx +3 -13
  78. package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +2 -3
  79. package/src/app/[variants]/share/t/[id]/_layout/index.tsx +1 -1
  80. package/src/app/[variants]/share/t/[id]/index.tsx +1 -1
  81. package/src/app/robots.tsx +1 -1
  82. package/src/envs/auth.ts +2 -27
  83. package/src/envs/llm.ts +2 -2
  84. package/src/features/AgentSetting/AgentPlugin/index.tsx +9 -12
  85. package/src/features/ChatInput/ActionBar/Tools/index.tsx +7 -5
  86. package/src/features/ChatMiniMap/utils.ts +1 -1
  87. package/src/features/CommandMenu/SearchResults.tsx +1 -1
  88. package/src/features/Conversation/ChatList/components/AutoScroll/DebugInspector.tsx +166 -0
  89. package/src/features/Conversation/ChatList/components/AutoScroll/index.tsx +86 -0
  90. package/src/features/Conversation/ChatList/components/VirtualizedList.tsx +11 -17
  91. package/src/features/Conversation/Messages/AgentCouncil/components/AutoScrollShadow.tsx +25 -14
  92. package/src/features/Conversation/Messages/AgentCouncil/components/CouncilMember.tsx +1 -1
  93. package/src/features/IntegrationDetailModal/IntegrationDetailContent.tsx +305 -0
  94. package/src/features/IntegrationDetailModal/index.tsx +21 -283
  95. package/src/features/MCPPluginDetail/Deployment/index.tsx +1 -1
  96. package/src/features/MCPPluginDetail/Schema/Prompts.tsx +1 -1
  97. package/src/features/MCPPluginDetail/Schema/Tools.tsx +1 -1
  98. package/src/features/ProfileEditor/AgentTool.tsx +14 -20
  99. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/NoteFileItem.tsx +1 -1
  100. package/src/features/SkillStore/LobeHubList/index.tsx +50 -87
  101. package/src/features/SkillStore/Search/index.tsx +1 -1
  102. package/src/features/SkillStore/{Content.tsx → SkillStoreContent.tsx} +3 -8
  103. package/src/features/SkillStore/index.tsx +15 -33
  104. package/src/features/User/UserPanel/PanelContent.tsx +0 -8
  105. package/src/features/User/__tests__/PanelContent.test.tsx +1 -35
  106. package/src/features/User/__tests__/UserAvatar.test.tsx +30 -57
  107. package/src/features/User/__tests__/useMenu.test.tsx +2 -43
  108. package/src/layout/AuthProvider/index.tsx +0 -5
  109. package/src/libs/next/config/define-config.ts +6 -0
  110. package/src/libs/next/proxy/createRouteMatcher.test.ts +121 -0
  111. package/src/libs/next/proxy/createRouteMatcher.ts +18 -0
  112. package/src/libs/next/proxy/define-config.ts +4 -53
  113. package/src/libs/next-auth/adapter/index.ts +1 -2
  114. package/src/libs/oidc-provider/provider.test.ts +5 -316
  115. package/src/libs/trpc/lambda/context.test.ts +0 -13
  116. package/src/libs/trpc/lambda/context.ts +3 -22
  117. package/src/libs/trpc/middleware/userAuth.ts +2 -4
  118. package/src/libs/trusted-client/getSessionUser.ts +2 -17
  119. package/src/locales/default/error.ts +0 -6
  120. package/src/locales/default/index.ts +0 -2
  121. package/src/proxy.ts +0 -1
  122. package/src/server/routers/lambda/__tests__/user.test.ts +0 -71
  123. package/src/server/routers/lambda/user.ts +6 -63
  124. package/src/server/services/changelog/index.test.ts +3 -2
  125. package/src/server/services/changelog/index.ts +1 -1
  126. package/src/server/services/user/index.ts +0 -83
  127. package/src/services/chat/index.ts +1 -2
  128. package/src/services/chat/mecha/agentConfigResolver.test.ts +43 -0
  129. package/src/services/chat/mecha/agentConfigResolver.ts +3 -1
  130. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +58 -14
  131. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +10 -2
  132. package/src/store/user/slices/auth/action.test.ts +1 -81
  133. package/src/store/user/slices/auth/action.ts +3 -28
  134. package/src/store/user/slices/auth/initialState.ts +1 -18
  135. package/src/store/user/slices/auth/selectors.test.ts +2 -127
  136. package/src/store/user/slices/auth/selectors.ts +1 -21
  137. package/src/utils/errorResponse.ts +1 -4
  138. package/src/utils/markdownToTxt.ts +20 -0
  139. package/locales/ar/clerk.json +0 -545
  140. package/locales/bg-BG/clerk.json +0 -545
  141. package/locales/de-DE/clerk.json +0 -545
  142. package/locales/en-US/clerk.json +0 -545
  143. package/locales/es-ES/clerk.json +0 -545
  144. package/locales/fa-IR/clerk.json +0 -545
  145. package/locales/fr-FR/clerk.json +0 -545
  146. package/locales/it-IT/clerk.json +0 -545
  147. package/locales/ja-JP/clerk.json +0 -545
  148. package/locales/ko-KR/clerk.json +0 -545
  149. package/locales/nl-NL/clerk.json +0 -545
  150. package/locales/pl-PL/clerk.json +0 -545
  151. package/locales/pt-BR/clerk.json +0 -545
  152. package/locales/ru-RU/clerk.json +0 -545
  153. package/locales/tr-TR/clerk.json +0 -545
  154. package/locales/vi-VN/clerk.json +0 -545
  155. package/locales/zh-CN/clerk.json +0 -545
  156. package/locales/zh-TW/clerk.json +0 -545
  157. package/src/app/(backend)/api/webhooks/clerk/__tests__/fixtures/createUser.json +0 -73
  158. package/src/app/(backend)/api/webhooks/clerk/route.ts +0 -95
  159. package/src/app/(backend)/api/webhooks/clerk/validateRequest.ts +0 -22
  160. package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +0 -27
  161. package/src/app/[variants]/(main)/settings/security/features/ClerkProfile.tsx +0 -67
  162. package/src/features/Conversation/ChatList/components/AutoScroll.tsx +0 -25
  163. package/src/layout/AuthProvider/Clerk/UserUpdater.tsx +0 -40
  164. package/src/layout/AuthProvider/Clerk/index.tsx +0 -54
  165. package/src/layout/AuthProvider/Clerk/useAppearance.ts +0 -133
  166. package/src/libs/clerk-auth/index.test.ts +0 -216
  167. package/src/libs/clerk-auth/index.ts +0 -80
  168. package/src/locales/default/clerk.ts +0 -677
  169. package/src/server/services/user/index.test.ts +0 -220
@@ -0,0 +1,166 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+
6
+ import { messageStateSelectors, useConversationStore, virtuaListSelectors } from '../../../store';
7
+
8
+ /**
9
+ * 判断是否在底部的阈值(单位:px)
10
+ * 当距离底部小于等于此值时,认为在底部
11
+ */
12
+ export const AT_BOTTOM_THRESHOLD = 300;
13
+
14
+ /**
15
+ * 是否开启调试面板
16
+ * 设为 true 可以显示滚动位置调试信息
17
+ */
18
+ export const OPEN_DEV_INSPECTOR = false;
19
+
20
+ const DebugInspector = memo(() => {
21
+ const atBottom = useConversationStore(virtuaListSelectors.atBottom);
22
+ const isScrolling = useConversationStore(virtuaListSelectors.isScrolling);
23
+ const isGenerating = useConversationStore(messageStateSelectors.isAIGenerating);
24
+ const virtuaScrollMethods = useConversationStore((s) => s.virtuaScrollMethods);
25
+
26
+ const shouldAutoScroll = atBottom && isGenerating && !isScrolling;
27
+ const scrollOffset = virtuaScrollMethods?.getScrollOffset?.() ?? 0;
28
+ const scrollSize = virtuaScrollMethods?.getScrollSize?.() ?? 0;
29
+ const viewportSize = virtuaScrollMethods?.getViewportSize?.() ?? 0;
30
+ const distanceToBottom = scrollSize - scrollOffset - viewportSize;
31
+ // 可视化计算
32
+ const visualHeight = 120;
33
+ const scale = scrollSize > 0 ? visualHeight / scrollSize : 0;
34
+ const viewportVisualHeight = Math.max(viewportSize * scale, 10);
35
+ const scrollVisualOffset = scrollOffset * scale;
36
+ const thresholdVisualHeight = Math.min(AT_BOTTOM_THRESHOLD * scale, visualHeight * 0.3);
37
+
38
+ const panel = (
39
+ <div
40
+ style={{
41
+ background: 'rgba(0,0,0,0.9)',
42
+ borderRadius: 8,
43
+ bottom: 80,
44
+ display: 'flex',
45
+ fontFamily: 'monospace',
46
+ fontSize: 11,
47
+ gap: 16,
48
+ left: 12,
49
+ padding: '10px 14px',
50
+ position: 'fixed',
51
+ zIndex: 9999,
52
+ }}
53
+ >
54
+ {/* 滚动条可视化 */}
55
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
56
+ <div style={{ color: '#9ca3af', fontSize: 10 }}>Scroll Position</div>
57
+ <div
58
+ style={{
59
+ background: '#374151',
60
+ borderRadius: 3,
61
+ height: visualHeight,
62
+ position: 'relative',
63
+ width: 24,
64
+ }}
65
+ >
66
+ {/* threshold 区域 (底部 200px) */}
67
+ <div
68
+ style={{
69
+ background: atBottom ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)',
70
+ borderRadius: '0 0 3px 3px',
71
+ bottom: 0,
72
+ height: thresholdVisualHeight,
73
+ left: 0,
74
+ position: 'absolute',
75
+ right: 0,
76
+ }}
77
+ />
78
+ {/* 当前视口位置 */}
79
+ <div
80
+ style={{
81
+ background: atBottom ? '#22c55e' : '#3b82f6',
82
+ borderRadius: 2,
83
+ height: viewportVisualHeight,
84
+ left: 2,
85
+ position: 'absolute',
86
+ right: 2,
87
+ top: scrollVisualOffset,
88
+ transition: 'top 0.1s',
89
+ }}
90
+ />
91
+ {/* threshold 线 */}
92
+ <div
93
+ style={{
94
+ background: '#f59e0b',
95
+ bottom: thresholdVisualHeight,
96
+ height: 1,
97
+ left: 0,
98
+ position: 'absolute',
99
+ right: 0,
100
+ }}
101
+ />
102
+ </div>
103
+ <div style={{ color: '#f59e0b', fontSize: 9, textAlign: 'center' }}>
104
+ {AT_BOTTOM_THRESHOLD}px
105
+ </div>
106
+ </div>
107
+
108
+ {/* 数值信息 */}
109
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
110
+ <div style={{ color: '#9ca3af', fontSize: 10 }}>
111
+ scrollSize: <span style={{ color: 'white' }}>{Math.round(scrollSize)}px</span>
112
+ </div>
113
+ <div style={{ color: '#9ca3af', fontSize: 10 }}>
114
+ viewport: <span style={{ color: 'white' }}>{Math.round(viewportSize)}px</span>
115
+ </div>
116
+ <div style={{ color: '#9ca3af', fontSize: 10 }}>
117
+ offset: <span style={{ color: 'white' }}>{Math.round(scrollOffset)}px</span>
118
+ </div>
119
+ <div
120
+ style={{
121
+ color: atBottom ? '#22c55e' : '#ef4444',
122
+ fontSize: 10,
123
+ fontWeight: 'bold',
124
+ }}
125
+ >
126
+ toBottom: {Math.round(distanceToBottom)}px
127
+ {distanceToBottom <= AT_BOTTOM_THRESHOLD ? ' ≤' : ' >'} {AT_BOTTOM_THRESHOLD}
128
+ </div>
129
+
130
+ <div style={{ borderTop: '1px solid #374151', marginTop: 4, paddingTop: 4 }}>
131
+ <div style={{ color: atBottom ? '#22c55e' : '#ef4444', fontSize: 10 }}>
132
+ atBottom: {atBottom ? 'YES' : 'NO'}
133
+ </div>
134
+ <div style={{ color: isGenerating ? '#3b82f6' : '#6b7280', fontSize: 10 }}>
135
+ generating: {isGenerating ? 'YES' : 'NO'}
136
+ </div>
137
+ <div style={{ color: isScrolling ? '#f59e0b' : '#6b7280', fontSize: 10 }}>
138
+ scrolling: {isScrolling ? 'YES' : 'NO'}
139
+ </div>
140
+ </div>
141
+
142
+ <div
143
+ style={{
144
+ background: shouldAutoScroll ? '#22c55e' : '#ef4444',
145
+ borderRadius: 3,
146
+ color: 'white',
147
+ fontSize: 10,
148
+ marginTop: 4,
149
+ padding: '2px 6px',
150
+ textAlign: 'center',
151
+ }}
152
+ >
153
+ autoScroll: {shouldAutoScroll ? 'YES' : 'NO'}
154
+ </div>
155
+ </div>
156
+ </div>
157
+ );
158
+
159
+ if (typeof document === 'undefined') return null;
160
+
161
+ return createPortal(panel, document.body);
162
+ });
163
+
164
+ DebugInspector.displayName = 'DebugInspector';
165
+
166
+ export default DebugInspector;
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import { memo, useEffect } from 'react';
4
+
5
+ import {
6
+ dataSelectors,
7
+ messageStateSelectors,
8
+ useConversationStore,
9
+ virtuaListSelectors,
10
+ } from '../../../store';
11
+ import BackBottom from '../BackBottom';
12
+ import { AT_BOTTOM_THRESHOLD, OPEN_DEV_INSPECTOR } from './DebugInspector';
13
+
14
+ const AutoScroll = memo(() => {
15
+ const atBottom = useConversationStore(virtuaListSelectors.atBottom);
16
+ const isScrolling = useConversationStore(virtuaListSelectors.isScrolling);
17
+ const isGenerating = useConversationStore(messageStateSelectors.isAIGenerating);
18
+ const scrollToBottom = useConversationStore((s) => s.scrollToBottom);
19
+ const dbMessages = useConversationStore(dataSelectors.dbMessages);
20
+
21
+ const shouldAutoScroll = atBottom && isGenerating && !isScrolling;
22
+
23
+ // 获取最后一条消息的 content 长度,用于监听流式输出
24
+ const lastMessage = dbMessages.at(-1);
25
+ const lastMessageContentLength =
26
+ typeof lastMessage?.content === 'string' ? lastMessage.content.length : 0;
27
+
28
+ useEffect(() => {
29
+ if (shouldAutoScroll) {
30
+ scrollToBottom(false);
31
+ }
32
+ }, [shouldAutoScroll, scrollToBottom, dbMessages.length, lastMessageContentLength]);
33
+
34
+ return (
35
+ <div style={{ position: 'relative', width: '100%' }}>
36
+ {OPEN_DEV_INSPECTOR && (
37
+ <>
38
+ {/* Threshold 区域顶部边界线 */}
39
+ <div
40
+ style={{
41
+ background: atBottom ? '#22c55e' : '#ef4444',
42
+ height: 2,
43
+ left: 0,
44
+ opacity: 0.5,
45
+ pointerEvents: 'none',
46
+ position: 'absolute',
47
+ right: 0,
48
+ top: -AT_BOTTOM_THRESHOLD,
49
+ }}
50
+ />
51
+
52
+ {/* Threshold 区域 mask - 显示在指示线上方 */}
53
+ <div
54
+ style={{
55
+ background: atBottom
56
+ ? 'linear-gradient(to top, rgba(34, 197, 94, 0.15), transparent)'
57
+ : 'linear-gradient(to top, rgba(239, 68, 68, 0.1), transparent)',
58
+ height: AT_BOTTOM_THRESHOLD,
59
+ left: 0,
60
+ pointerEvents: 'none',
61
+ position: 'absolute',
62
+ right: 0,
63
+ top: -AT_BOTTOM_THRESHOLD,
64
+ }}
65
+ />
66
+
67
+ {/* AutoScroll 位置指示线(底部) */}
68
+ <div
69
+ style={{
70
+ background: atBottom ? '#22c55e' : '#ef4444',
71
+ height: 2,
72
+ position: 'relative',
73
+ width: '100%',
74
+ }}
75
+ />
76
+ </>
77
+ )}
78
+
79
+ <BackBottom onScrollToBottom={() => scrollToBottom(true)} visible={!atBottom} />
80
+ </div>
81
+ );
82
+ });
83
+
84
+ AutoScroll.displayName = 'ConversationAutoScroll';
85
+
86
+ export default AutoScroll;
@@ -7,6 +7,10 @@ import { VList, type VListHandle } from 'virtua';
7
7
  import WideScreenContainer from '../../../WideScreenContainer';
8
8
  import { useConversationStore, virtuaListSelectors } from '../../store';
9
9
  import AutoScroll from './AutoScroll';
10
+ import DebugInspector, {
11
+ AT_BOTTOM_THRESHOLD,
12
+ OPEN_DEV_INSPECTOR,
13
+ } from './AutoScroll/DebugInspector';
10
14
 
11
15
  interface VirtualizedListProps {
12
16
  dataSource: string[];
@@ -23,15 +27,11 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
23
27
  const prevDataLengthRef = useRef(dataSource.length);
24
28
  const scrollEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
25
29
 
26
- const atBottomThreshold = 200;
27
-
28
30
  // Store actions
29
31
  const registerVirtuaScrollMethods = useConversationStore((s) => s.registerVirtuaScrollMethods);
30
32
  const setScrollState = useConversationStore((s) => s.setScrollState);
31
33
  const resetVisibleItems = useConversationStore((s) => s.resetVisibleItems);
32
- const scrollToBottom = useConversationStore((s) => s.scrollToBottom);
33
34
  const setActiveIndex = useConversationStore((s) => s.setActiveIndex);
34
- const atBottom = useConversationStore(virtuaListSelectors.atBottom);
35
35
  const activeIndex = useConversationStore(virtuaListSelectors.activeIndex);
36
36
 
37
37
  // Check if at bottom based on scroll position
@@ -43,8 +43,8 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
43
43
  const scrollSize = ref.scrollSize;
44
44
  const viewportSize = ref.viewportSize;
45
45
 
46
- return scrollSize - scrollOffset - viewportSize <= atBottomThreshold;
47
- }, [atBottomThreshold]);
46
+ return scrollSize - scrollOffset - viewportSize <= AT_BOTTOM_THRESHOLD;
47
+ }, [AT_BOTTOM_THRESHOLD]);
48
48
 
49
49
  // Handle scroll events
50
50
  const handleScroll = useCallback(() => {
@@ -131,6 +131,8 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
131
131
 
132
132
  return (
133
133
  <>
134
+ {/* Debug Inspector - 放在 VList 外面,不会被虚拟列表回收 */}
135
+ {OPEN_DEV_INSPECTOR && <DebugInspector />}
134
136
  <VList
135
137
  bufferSize={typeof window !== 'undefined' ? window.innerHeight : 0}
136
138
  data={dataSource}
@@ -142,12 +144,14 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
142
144
  {(messageId, index): ReactElement => {
143
145
  const isAgentCouncil = messageId.includes('agentCouncil');
144
146
  const content = itemContent(index, messageId);
147
+ const isLast = index === dataSource.length - 1;
145
148
 
146
149
  if (isAgentCouncil) {
147
150
  // AgentCouncil needs full width for horizontal scroll
148
151
  return (
149
152
  <div key={messageId} style={{ position: 'relative', width: '100%' }}>
150
153
  {content}
154
+ {isLast && <AutoScroll />}
151
155
  </div>
152
156
  );
153
157
  }
@@ -155,21 +159,11 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent })
155
159
  return (
156
160
  <WideScreenContainer key={messageId} style={{ position: 'relative' }}>
157
161
  {content}
162
+ {isLast && <AutoScroll />}
158
163
  </WideScreenContainer>
159
164
  );
160
165
  }}
161
166
  </VList>
162
- <WideScreenContainer
163
- onChange={() => {
164
- if (!atBottom) return;
165
- setTimeout(() => scrollToBottom(true), 100);
166
- }}
167
- style={{
168
- position: 'relative',
169
- }}
170
- >
171
- <AutoScroll />
172
- </WideScreenContainer>
173
167
  </>
174
168
  );
175
169
  }, isEqual);
@@ -1,27 +1,38 @@
1
1
  import { ScrollShadow } from '@lobehub/ui';
2
- import { type PropsWithChildren, type RefObject, memo, useEffect, useRef } from 'react';
2
+ import { type PropsWithChildren, type RefObject, memo, useEffect } from 'react';
3
3
 
4
- const AutoScrollShadow = memo<PropsWithChildren>(({ children }) => {
5
- const contentRef = useRef<HTMLDivElement | null>(null);
4
+ import { useAutoScroll } from '@/hooks/useAutoScroll';
6
5
 
7
- useEffect(() => {
8
- const container = contentRef.current;
9
- if (!container) return;
10
- const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
11
- const isNearBottom = distanceToBottom < 120;
6
+ interface AutoScrollShadowProps extends PropsWithChildren {
7
+ /**
8
+ * Content string to track for auto-scrolling
9
+ */
10
+ content?: string;
11
+ /**
12
+ * Whether the content is currently streaming/generating
13
+ */
14
+ streaming?: boolean;
15
+ }
16
+
17
+ const AutoScrollShadow = memo<AutoScrollShadowProps>(({ children, content, streaming }) => {
18
+ const { ref, handleScroll, resetScrollLock } = useAutoScroll<HTMLDivElement>({
19
+ deps: [content],
20
+ enabled: streaming,
21
+ });
12
22
 
13
- if (isNearBottom) {
14
- requestAnimationFrame(() => {
15
- container.scrollTop = container.scrollHeight;
16
- });
23
+ // Reset scroll lock when content is cleared (new stream starts)
24
+ useEffect(() => {
25
+ if (!content) {
26
+ resetScrollLock();
17
27
  }
18
- }, []);
28
+ }, [content, resetScrollLock]);
19
29
 
20
30
  return (
21
31
  <ScrollShadow
22
32
  height={'max(33vh, 480px)'}
23
33
  hideScrollBar
24
- ref={contentRef as unknown as RefObject<HTMLDivElement>}
34
+ onScroll={handleScroll}
35
+ ref={ref as RefObject<HTMLDivElement>}
25
36
  size={16}
26
37
  >
27
38
  {children}
@@ -92,7 +92,7 @@ const CouncilMember = memo<CouncilMemberProps>(({ item, index }) => {
92
92
  showTitle
93
93
  time={createdAt}
94
94
  >
95
- <AutoScrollShadow>
95
+ <AutoScrollShadow content={content} streaming={generating}>
96
96
  <MessageContent {...item} />
97
97
  </AutoScrollShadow>
98
98
  </ChatItem>