@lobehub/chat 1.85.7 → 1.85.9
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/changelog/v1.json +21 -0
- package/docker-compose/local/init_data.json +2 -2
- package/package.json +2 -2
- package/packages/file-loaders/package.json +1 -1
- package/packages/file-loaders/test/setup.ts +1 -1
- package/src/components/Thinking/index.tsx +38 -22
- package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -1
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +3 -2
- package/src/features/Conversation/components/ChatItem/index.tsx +2 -1
- package/src/features/DataImporter/index.tsx +0 -1
- package/src/features/Portal/Artifacts/Body/index.tsx +4 -1
- package/src/features/Portal/Artifacts/Header.tsx +2 -6
- package/src/features/Portal/FilePreview/Header.tsx +1 -1
- package/src/features/Portal/Plugins/Header.tsx +2 -2
- package/src/features/Portal/components/Header.tsx +1 -0
- package/src/features/User/UserPanel/LangButton.tsx +3 -8
- package/src/features/User/UserPanel/PanelContent.tsx +1 -1
- package/src/features/User/UserPanel/ThemeButton.tsx +1 -7
- package/src/middleware.ts +20 -9
- package/src/server/routers/lambda/__tests__/importer.test.ts +3 -0
- package/src/server/routers/lambda/importer.ts +10 -2
- package/src/services/__tests__/upload.test.ts +4 -6
- package/src/services/import/server.ts +20 -71
- package/src/services/ragEval.ts +1 -1
- package/src/services/upload.ts +52 -18
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +14 -2
- package/src/store/chat/slices/builtinTool/actions/dalle.test.ts +2 -0
- package/src/store/chat/slices/message/selectors.ts +5 -0
- package/src/store/file/slices/upload/action.ts +14 -27
- package/src/utils/fetch/__tests__/fetchSSE.test.ts +5 -18
- package/src/utils/fetch/fetchSSE.ts +62 -3
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,64 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.85.9](https://github.com/lobehub/lobe-chat/compare/v1.85.8...v1.85.9)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-14**</sup>
|
8
|
+
|
9
|
+
#### 🐛 Bug Fixes
|
10
|
+
|
11
|
+
- **misc**: Redirect unauthorized next-auth user to signin page.
|
12
|
+
|
13
|
+
#### 💄 Styles
|
14
|
+
|
15
|
+
- **misc**: Improve smoothing on completion.
|
16
|
+
|
17
|
+
<br/>
|
18
|
+
|
19
|
+
<details>
|
20
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
21
|
+
|
22
|
+
#### What's fixed
|
23
|
+
|
24
|
+
- **misc**: Redirect unauthorized next-auth user to signin page, closes [#7813](https://github.com/lobehub/lobe-chat/issues/7813) ([6160784](https://github.com/lobehub/lobe-chat/commit/6160784))
|
25
|
+
|
26
|
+
#### Styles
|
27
|
+
|
28
|
+
- **misc**: Improve smoothing on completion, closes [#7833](https://github.com/lobehub/lobe-chat/issues/7833) ([6434686](https://github.com/lobehub/lobe-chat/commit/6434686))
|
29
|
+
|
30
|
+
</details>
|
31
|
+
|
32
|
+
<div align="right">
|
33
|
+
|
34
|
+
[](#readme-top)
|
35
|
+
|
36
|
+
</div>
|
37
|
+
|
38
|
+
### [Version 1.85.8](https://github.com/lobehub/lobe-chat/compare/v1.85.7...v1.85.8)
|
39
|
+
|
40
|
+
<sup>Released on **2025-05-11**</sup>
|
41
|
+
|
42
|
+
#### 🐛 Bug Fixes
|
43
|
+
|
44
|
+
- **misc**: Fix config import issue in the desktop version.
|
45
|
+
|
46
|
+
<br/>
|
47
|
+
|
48
|
+
<details>
|
49
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
50
|
+
|
51
|
+
#### What's fixed
|
52
|
+
|
53
|
+
- **misc**: Fix config import issue in the desktop version, closes [#7800](https://github.com/lobehub/lobe-chat/issues/7800) ([2cb8635](https://github.com/lobehub/lobe-chat/commit/2cb8635))
|
54
|
+
|
55
|
+
</details>
|
56
|
+
|
57
|
+
<div align="right">
|
58
|
+
|
59
|
+
[](#readme-top)
|
60
|
+
|
61
|
+
</div>
|
62
|
+
|
5
63
|
### [Version 1.85.7](https://github.com/lobehub/lobe-chat/compare/v1.85.6...v1.85.7)
|
6
64
|
|
7
65
|
<sup>Released on **2025-05-11**</sup>
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,25 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"fixes": [
|
5
|
+
"Redirect unauthorized next-auth user to signin page."
|
6
|
+
],
|
7
|
+
"improvements": [
|
8
|
+
"Improve smoothing on completion."
|
9
|
+
]
|
10
|
+
},
|
11
|
+
"date": "2025-05-14",
|
12
|
+
"version": "1.85.9"
|
13
|
+
},
|
14
|
+
{
|
15
|
+
"children": {
|
16
|
+
"fixes": [
|
17
|
+
"Fix config import issue in the desktop version."
|
18
|
+
]
|
19
|
+
},
|
20
|
+
"date": "2025-05-11",
|
21
|
+
"version": "1.85.8"
|
22
|
+
},
|
2
23
|
{
|
3
24
|
"children": {
|
4
25
|
"fixes": [
|
@@ -42,7 +42,7 @@
|
|
42
42
|
"cert": "cert-built-in",
|
43
43
|
"headerHtml": "",
|
44
44
|
"enablePassword": true,
|
45
|
-
"enableSignUp":
|
45
|
+
"enableSignUp": false,
|
46
46
|
"enableSigninSession": false,
|
47
47
|
"enableAutoSignin": false,
|
48
48
|
"enableCodeSignin": false,
|
@@ -1235,4 +1235,4 @@
|
|
1235
1235
|
}
|
1236
1236
|
],
|
1237
1237
|
"webhooks": []
|
1238
|
-
}
|
1238
|
+
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.85.
|
3
|
+
"version": "1.85.9",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -150,7 +150,7 @@
|
|
150
150
|
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
151
151
|
"@lobehub/icons": "^2.0.0",
|
152
152
|
"@lobehub/tts": "^2.0.0",
|
153
|
-
"@lobehub/ui": "^2.0.
|
153
|
+
"@lobehub/ui": "^2.0.13",
|
154
154
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
155
155
|
"@neondatabase/serverless": "^1.0.0",
|
156
156
|
"@next/third-parties": "^15.3.0",
|
@@ -26,6 +26,7 @@
|
|
26
26
|
"dependencies": {
|
27
27
|
"@langchain/community": "^0.3.41",
|
28
28
|
"@langchain/core": "^0.3.45",
|
29
|
+
"@napi-rs/canvas": "^0.1.70",
|
29
30
|
"@xmldom/xmldom": "^0.9.8",
|
30
31
|
"concat-stream": "^2.0.0",
|
31
32
|
"mammoth": "^1.8.0",
|
@@ -37,7 +38,6 @@
|
|
37
38
|
"devDependencies": {
|
38
39
|
"@types/concat-stream": "^2.0.3",
|
39
40
|
"@types/yauzl": "^2.10.3",
|
40
|
-
"canvas": "^3.1.0",
|
41
41
|
"typescript": "^5"
|
42
42
|
},
|
43
43
|
"peerDependencies": {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { CopyButton, Icon, Markdown } from '@lobehub/ui';
|
1
|
+
import { ActionIcon, CopyButton, Icon, Markdown } from '@lobehub/ui';
|
2
2
|
import { createStyles } from 'antd-style';
|
3
3
|
import { AnimatePresence, motion } from 'framer-motion';
|
4
4
|
import { AtomIcon, ChevronDown, ChevronRight } from 'lucide-react';
|
@@ -9,21 +9,32 @@ import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
|
10
10
|
import { CitationItem } from '@/types/message';
|
11
11
|
|
12
|
-
const useStyles = createStyles(({ css, token
|
12
|
+
const useStyles = createStyles(({ css, token }) => ({
|
13
13
|
container: css`
|
14
|
-
|
15
|
-
|
16
|
-
padding-inline: 8px;
|
17
|
-
border-radius: 6px;
|
18
|
-
|
14
|
+
overflow: hidden;
|
15
|
+
border-radius: ${token.borderRadius}px;
|
19
16
|
color: ${token.colorTextTertiary};
|
17
|
+
transition: all 0.2s ${token.motionEaseOut};
|
18
|
+
`,
|
19
|
+
expand: css`
|
20
|
+
color: ${token.colorTextSecondary};
|
21
|
+
background: ${token.colorFillTertiary};
|
22
|
+
`,
|
23
|
+
|
24
|
+
header: css`
|
25
|
+
padding-block: 4px;
|
26
|
+
padding-inline: 8px 4px;
|
27
|
+
transition: background 0.2s ${token.motionEaseOut};
|
28
|
+
transition: all 0.2s ${token.motionEaseOut};
|
20
29
|
|
21
30
|
&:hover {
|
22
|
-
background: ${
|
31
|
+
background: ${token.colorFillQuaternary};
|
23
32
|
}
|
24
33
|
`,
|
25
|
-
|
26
|
-
|
34
|
+
|
35
|
+
headerExpand: css`
|
36
|
+
color: ${token.colorTextSecondary};
|
37
|
+
background: ${token.colorFillQuaternary};
|
27
38
|
`,
|
28
39
|
shinyText: css`
|
29
40
|
color: ${rgba(token.colorText, 0.45)};
|
@@ -70,7 +81,7 @@ interface ThinkingProps {
|
|
70
81
|
|
71
82
|
const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style, citations }) => {
|
72
83
|
const { t } = useTranslation(['components', 'common']);
|
73
|
-
const { styles, cx } = useStyles();
|
84
|
+
const { styles, cx, theme } = useStyles();
|
74
85
|
|
75
86
|
const [showDetail, setShowDetail] = useState(false);
|
76
87
|
|
@@ -79,8 +90,13 @@ const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style, cita
|
|
79
90
|
}, [thinking]);
|
80
91
|
|
81
92
|
return (
|
82
|
-
<Flexbox
|
93
|
+
<Flexbox
|
94
|
+
className={cx(styles.container, showDetail && styles.expand)}
|
95
|
+
style={style}
|
96
|
+
width={'100%'}
|
97
|
+
>
|
83
98
|
<Flexbox
|
99
|
+
className={cx(styles.header, showDetail && styles.headerExpand)}
|
84
100
|
distribution={'space-between'}
|
85
101
|
flex={1}
|
86
102
|
gap={8}
|
@@ -89,17 +105,18 @@ const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style, cita
|
|
89
105
|
setShowDetail(!showDetail);
|
90
106
|
}}
|
91
107
|
style={{ cursor: 'pointer' }}
|
108
|
+
width={'100%'}
|
92
109
|
>
|
93
110
|
{thinking ? (
|
94
|
-
<Flexbox align={'center'} gap={8} horizontal>
|
95
|
-
<Icon icon={AtomIcon} />
|
111
|
+
<Flexbox align={'center'} gap={8} horizontal width={'100%'}>
|
112
|
+
<Icon color={theme.purple} icon={AtomIcon} />
|
96
113
|
<Flexbox className={styles.shinyText} horizontal>
|
97
114
|
{t('Thinking.thinking')}
|
98
115
|
</Flexbox>
|
99
116
|
</Flexbox>
|
100
117
|
) : (
|
101
|
-
<Flexbox align={'center'} gap={8} horizontal>
|
102
|
-
<Icon icon={AtomIcon} />
|
118
|
+
<Flexbox align={'center'} gap={8} horizontal width={'100%'}>
|
119
|
+
<Icon color={showDetail ? theme.purple : undefined} icon={AtomIcon} />
|
103
120
|
<Flexbox>
|
104
121
|
{!duration
|
105
122
|
? t('Thinking.thoughtWithDuration')
|
@@ -117,28 +134,27 @@ const Thinking = memo<ThinkingProps>(({ content, duration, thinking, style, cita
|
|
117
134
|
<CopyButton content={content} size={'small'} title={t('copy', { ns: 'common' })} />
|
118
135
|
</div>
|
119
136
|
)}
|
120
|
-
<
|
137
|
+
<ActionIcon icon={showDetail ? ChevronDown : ChevronRight} size={'small'} />
|
121
138
|
</Flexbox>
|
122
139
|
</Flexbox>
|
123
|
-
|
124
140
|
<AnimatePresence initial={false}>
|
125
141
|
{showDetail && (
|
126
142
|
<motion.div
|
127
143
|
animate="open"
|
128
144
|
exit="collapsed"
|
129
145
|
initial="collapsed"
|
130
|
-
style={{ overflow: 'hidden' }}
|
146
|
+
style={{ overflow: 'hidden', padding: 12 }}
|
131
147
|
transition={{
|
132
148
|
duration: 0.2,
|
133
149
|
ease: [0.4, 0, 0.2, 1], // 使用 ease-out 缓动函数
|
134
150
|
}}
|
135
151
|
variants={{
|
136
|
-
collapsed: {
|
137
|
-
open: {
|
152
|
+
collapsed: { opacity: 0, width: 'auto' },
|
153
|
+
open: { opacity: 1, width: 'auto' },
|
138
154
|
}}
|
139
155
|
>
|
140
156
|
{typeof content === 'string' ? (
|
141
|
-
<Markdown citations={citations} variant={'chat'}>
|
157
|
+
<Markdown animated={thinking} citations={citations} variant={'chat'}>
|
142
158
|
{content}
|
143
159
|
</Markdown>
|
144
160
|
) : (
|
@@ -31,9 +31,10 @@ const Tools = memo(() => {
|
|
31
31
|
<Suspense fallback={<Action disabled icon={Blocks} title={t('tools.title')} />}>
|
32
32
|
<Action
|
33
33
|
dropdown={{
|
34
|
-
|
34
|
+
maxHeight: 500,
|
35
|
+
maxWidth: 480,
|
35
36
|
menu: { items },
|
36
|
-
minWidth:
|
37
|
+
minWidth: 320,
|
37
38
|
}}
|
38
39
|
icon={Blocks}
|
39
40
|
loading={updating}
|
@@ -171,6 +171,7 @@ const Item = memo<ChatListItemProps>(
|
|
171
171
|
|
172
172
|
const markdownProps = useMemo(
|
173
173
|
() => ({
|
174
|
+
animated: generating,
|
174
175
|
citations: item?.role === 'user' ? undefined : item?.search?.citations,
|
175
176
|
components,
|
176
177
|
customRender: markdownCustomRender,
|
@@ -186,7 +187,7 @@ const Item = memo<ChatListItemProps>(
|
|
186
187
|
// if the citations's url and title are all the same, we should not show the citations
|
187
188
|
item?.search?.citations.every((item) => item.title !== item.url),
|
188
189
|
}),
|
189
|
-
[components, markdownCustomRender, item?.role, item?.search],
|
190
|
+
[generating, components, markdownCustomRender, item?.role, item?.search],
|
190
191
|
);
|
191
192
|
|
192
193
|
const onChange = useCallback((value: string) => updateMessageContent(id, value), [id]);
|
@@ -79,7 +79,10 @@ const ArtifactsUI = memo(() => {
|
|
79
79
|
style={{ overflow: 'hidden' }}
|
80
80
|
>
|
81
81
|
{showCode ? (
|
82
|
-
<Highlighter
|
82
|
+
<Highlighter
|
83
|
+
language={language || 'txt'}
|
84
|
+
style={{ fontSize: 12, height: '100%', overflow: 'auto' }}
|
85
|
+
>
|
83
86
|
{artifactContent}
|
84
87
|
</Highlighter>
|
85
88
|
) : (
|
@@ -33,12 +33,8 @@ const Header = () => {
|
|
33
33
|
return (
|
34
34
|
<Flexbox align={'center'} flex={1} gap={12} horizontal justify={'space-between'} width={'100%'}>
|
35
35
|
<Flexbox align={'center'} gap={4} horizontal>
|
36
|
-
<ActionIcon icon={ArrowLeft} onClick={() => closeArtifact()} />
|
37
|
-
<Typography.Text
|
38
|
-
className={cx(oneLineEllipsis)}
|
39
|
-
style={{ fontSize: 16 }}
|
40
|
-
type={'secondary'}
|
41
|
-
>
|
36
|
+
<ActionIcon icon={ArrowLeft} onClick={() => closeArtifact()} size={'small'} />
|
37
|
+
<Typography.Text className={cx(oneLineEllipsis)} type={'secondary'}>
|
42
38
|
{artifactTitle}
|
43
39
|
</Typography.Text>
|
44
40
|
</Flexbox>
|
@@ -20,7 +20,7 @@ const Header = () => {
|
|
20
20
|
|
21
21
|
return (
|
22
22
|
<Flexbox align={'center'} gap={4} horizontal>
|
23
|
-
<ActionIcon icon={ArrowLeft} onClick={() => closeFilePreview()} />
|
23
|
+
<ActionIcon icon={ArrowLeft} onClick={() => closeFilePreview()} size={'small'} />
|
24
24
|
|
25
25
|
{isLoading ? (
|
26
26
|
<Skeleton.Button active style={{ height: 28 }} />
|
@@ -25,7 +25,7 @@ const Header = () => {
|
|
25
25
|
if (toolUIIdentifier === WebBrowsingManifest.identifier) {
|
26
26
|
return (
|
27
27
|
<Flexbox align={'center'} gap={8} horizontal>
|
28
|
-
<ActionIcon icon={ArrowLeft} onClick={() => closeToolUI()} />
|
28
|
+
<ActionIcon icon={ArrowLeft} onClick={() => closeToolUI()} size={'small'} />
|
29
29
|
<Icon icon={Globe} size={16} />
|
30
30
|
<Typography.Text style={{ fontSize: 16 }} type={'secondary'}>
|
31
31
|
{t('search.title')}
|
@@ -35,7 +35,7 @@ const Header = () => {
|
|
35
35
|
}
|
36
36
|
return (
|
37
37
|
<Flexbox align={'center'} gap={4} horizontal>
|
38
|
-
<ActionIcon icon={ArrowLeft} onClick={() => closeToolUI()} />
|
38
|
+
<ActionIcon icon={ArrowLeft} onClick={() => closeToolUI()} size={'small'} />
|
39
39
|
<PluginAvatar identifier={toolUIIdentifier} size={28} />
|
40
40
|
<Typography.Text style={{ fontSize: 16 }} type={'secondary'}>
|
41
41
|
{pluginTitle}
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { ActionIcon } from '@lobehub/ui';
|
2
2
|
import { Popover, type PopoverProps } from 'antd';
|
3
|
-
import { useTheme } from 'antd-style';
|
4
3
|
import { Languages } from 'lucide-react';
|
5
4
|
import { memo, useMemo } from 'react';
|
6
5
|
import { useTranslation } from 'react-i18next';
|
@@ -12,8 +11,6 @@ import { globalGeneralSelectors } from '@/store/global/selectors';
|
|
12
11
|
import { LocaleMode } from '@/types/locale';
|
13
12
|
|
14
13
|
const LangButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement = 'right' }) => {
|
15
|
-
const theme = useTheme();
|
16
|
-
|
17
14
|
const [language, switchLocale] = useGlobalStore((s) => [
|
18
15
|
globalGeneralSelectors.language(s),
|
19
16
|
s.switchLocale,
|
@@ -48,16 +45,14 @@ const LangButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement
|
|
48
45
|
placement={placement}
|
49
46
|
styles={{
|
50
47
|
body: {
|
48
|
+
maxHeight: 360,
|
49
|
+
overflow: 'auto',
|
51
50
|
padding: 0,
|
52
51
|
},
|
53
52
|
}}
|
54
53
|
trigger={['click', 'hover']}
|
55
54
|
>
|
56
|
-
<ActionIcon
|
57
|
-
icon={Languages}
|
58
|
-
size={{ blockSize: 32, size: 16 }}
|
59
|
-
style={{ border: `1px solid ${theme.colorFillSecondary}` }}
|
60
|
-
/>
|
55
|
+
<ActionIcon icon={Languages} size={{ blockSize: 32, size: 16 }} />
|
61
56
|
</Popover>
|
62
57
|
);
|
63
58
|
});
|
@@ -63,7 +63,7 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
|
|
63
63
|
) : (
|
64
64
|
<BrandWatermark />
|
65
65
|
)}
|
66
|
-
<Flexbox align={'center'} flex={'none'} gap={
|
66
|
+
<Flexbox align={'center'} flex={'none'} gap={2} horizontal>
|
67
67
|
<LangButton />
|
68
68
|
<ThemeButton />
|
69
69
|
</Flexbox>
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { ActionIcon, Icon } from '@lobehub/ui';
|
2
2
|
import { Popover, type PopoverProps } from 'antd';
|
3
|
-
import { useTheme } from 'antd-style';
|
4
3
|
import { Monitor, Moon, Sun } from 'lucide-react';
|
5
4
|
import { memo, useMemo } from 'react';
|
6
5
|
import { useTranslation } from 'react-i18next';
|
@@ -16,7 +15,6 @@ const themeIcons = {
|
|
16
15
|
};
|
17
16
|
|
18
17
|
const ThemeButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement = 'right' }) => {
|
19
|
-
const theme = useTheme();
|
20
18
|
const [themeMode, switchThemeMode] = useGlobalStore((s) => [
|
21
19
|
systemStatusSelectors.themeMode(s),
|
22
20
|
s.switchThemeMode,
|
@@ -60,11 +58,7 @@ const ThemeButton = memo<{ placement?: PopoverProps['placement'] }>(({ placement
|
|
60
58
|
}}
|
61
59
|
trigger={['click', 'hover']}
|
62
60
|
>
|
63
|
-
<ActionIcon
|
64
|
-
icon={themeIcons[themeMode]}
|
65
|
-
size={{ blockSize: 32, size: 16 }}
|
66
|
-
style={{ border: `1px solid ${theme.colorFillSecondary}` }}
|
67
|
-
/>
|
61
|
+
<ActionIcon icon={themeIcons[themeMode]} size={{ blockSize: 32, size: 16 }} />
|
68
62
|
</Popover>
|
69
63
|
);
|
70
64
|
});
|
package/src/middleware.ts
CHANGED
@@ -134,12 +134,23 @@ const defaultMiddleware = (request: NextRequest) => {
|
|
134
134
|
return NextResponse.rewrite(url, { status: 200 });
|
135
135
|
};
|
136
136
|
|
137
|
+
const isProtectedRoute = createRouteMatcher([
|
138
|
+
'/settings(.*)',
|
139
|
+
'/files(.*)',
|
140
|
+
'/onboard(.*)',
|
141
|
+
'/oauth(.*)',
|
142
|
+
// ↓ cloud ↓
|
143
|
+
]);
|
144
|
+
|
137
145
|
// Initialize an Edge compatible NextAuth middleware
|
138
146
|
const nextAuthMiddleware = NextAuthEdge.auth((req) => {
|
139
147
|
logNextAuth('NextAuth middleware processing request: %s %s', req.method, req.url);
|
140
148
|
|
141
149
|
const response = defaultMiddleware(req);
|
142
150
|
|
151
|
+
const isProtected = isProtectedRoute(req);
|
152
|
+
logNextAuth('Route protection status: %s, %s', req.url, isProtected ? 'protected' : 'public');
|
153
|
+
|
143
154
|
// Just check if session exists
|
144
155
|
const session = req.auth;
|
145
156
|
|
@@ -165,20 +176,20 @@ const nextAuthMiddleware = NextAuthEdge.auth((req) => {
|
|
165
176
|
response.headers.set(OIDC_SESSION_HEADER, session.user.id);
|
166
177
|
}
|
167
178
|
} else {
|
168
|
-
|
179
|
+
// If request a protected route, redirect to sign-in page
|
180
|
+
// ref: https://authjs.dev/getting-started/session-management/protecting
|
181
|
+
if (isProtected) {
|
182
|
+
logNextAuth('Request a protected route, redirecting to sign-in page');
|
183
|
+
const nextLoginUrl = new URL('/next-auth/signin', req.nextUrl.origin);
|
184
|
+
nextLoginUrl.searchParams.set('callbackUrl', req.nextUrl.pathname);
|
185
|
+
return Response.redirect(nextLoginUrl);
|
186
|
+
}
|
187
|
+
logNextAuth('Request a free route but not login, allow visit without auth header');
|
169
188
|
}
|
170
189
|
|
171
190
|
return response;
|
172
191
|
});
|
173
192
|
|
174
|
-
const isProtectedRoute = createRouteMatcher([
|
175
|
-
'/settings(.*)',
|
176
|
-
'/files(.*)',
|
177
|
-
'/onboard(.*)',
|
178
|
-
'/oauth(.*)',
|
179
|
-
// ↓ cloud ↓
|
180
|
-
]);
|
181
|
-
|
182
193
|
const clerkAuthMiddleware = clerkMiddleware(
|
183
194
|
async (auth, req) => {
|
184
195
|
logClerk('Clerk middleware processing request: %s %s', req.method, req.url);
|
@@ -8,6 +8,7 @@ import { ImportResultData } from '@/types/importer';
|
|
8
8
|
import { importerRouter } from '../importer';
|
9
9
|
|
10
10
|
const mockGetFileContent = vi.fn();
|
11
|
+
const mockDeleteFile = vi.fn();
|
11
12
|
const mockImportData = vi.fn();
|
12
13
|
const mockImportPgData = vi.fn();
|
13
14
|
|
@@ -21,6 +22,7 @@ vi.mock('@/database/repositories/dataImporter', () => ({
|
|
21
22
|
vi.mock('@/server/services/file', () => ({
|
22
23
|
FileService: vi.fn().mockImplementation(() => ({
|
23
24
|
getFileContent: mockGetFileContent,
|
25
|
+
deleteFile: mockDeleteFile,
|
24
26
|
})),
|
25
27
|
}));
|
26
28
|
|
@@ -74,6 +76,7 @@ describe('importerRouter', () => {
|
|
74
76
|
expect(result).toEqual(mockImportResult);
|
75
77
|
expect(mockGetFileContent).toHaveBeenCalledWith('test.json');
|
76
78
|
expect(mockImportData).toHaveBeenCalledWith(JSON.parse(mockFileContent));
|
79
|
+
expect(mockDeleteFile).toHaveBeenCalledWith('test.json');
|
77
80
|
});
|
78
81
|
|
79
82
|
it('should handle PG data import', async () => {
|
@@ -39,11 +39,19 @@ export const importerRouter = router({
|
|
39
39
|
});
|
40
40
|
}
|
41
41
|
|
42
|
+
let result: ImportResultData;
|
42
43
|
if ('schemaHash' in data) {
|
43
|
-
|
44
|
+
result = await ctx.dataImporterService.importPgData(
|
45
|
+
data as unknown as ImportPgDataStructure,
|
46
|
+
);
|
47
|
+
} else {
|
48
|
+
result = await ctx.dataImporterService.importData(data);
|
44
49
|
}
|
45
50
|
|
46
|
-
|
51
|
+
// clean file after upload
|
52
|
+
await ctx.fileService.deleteFile(input.pathname);
|
53
|
+
|
54
|
+
return result;
|
47
55
|
}),
|
48
56
|
|
49
57
|
importByPost: importProcedure
|
@@ -69,7 +69,7 @@ describe('UploadService', () => {
|
|
69
69
|
}
|
70
70
|
});
|
71
71
|
|
72
|
-
const result = await uploadService.
|
72
|
+
const result = await uploadService.uploadToServerS3(mockFile, { onProgress });
|
73
73
|
|
74
74
|
expect(result).toEqual({
|
75
75
|
date: '1',
|
@@ -91,9 +91,7 @@ describe('UploadService', () => {
|
|
91
91
|
}
|
92
92
|
});
|
93
93
|
|
94
|
-
await expect(uploadService.
|
95
|
-
UPLOAD_NETWORK_ERROR,
|
96
|
-
);
|
94
|
+
await expect(uploadService.uploadToServerS3(mockFile, {})).rejects.toBe(UPLOAD_NETWORK_ERROR);
|
97
95
|
});
|
98
96
|
|
99
97
|
it('should handle upload error', async () => {
|
@@ -109,7 +107,7 @@ describe('UploadService', () => {
|
|
109
107
|
}
|
110
108
|
});
|
111
109
|
|
112
|
-
await expect(uploadService.
|
110
|
+
await expect(uploadService.uploadToServerS3(mockFile, {})).rejects.toBe('Bad Request');
|
113
111
|
});
|
114
112
|
});
|
115
113
|
|
@@ -125,7 +123,7 @@ describe('UploadService', () => {
|
|
125
123
|
|
126
124
|
(clientS3Storage.putObject as any).mockResolvedValue(undefined);
|
127
125
|
|
128
|
-
const result = await uploadService
|
126
|
+
const result = await uploadService['uploadToClientS3'](hash, mockFile);
|
129
127
|
|
130
128
|
expect(clientS3Storage.putObject).toHaveBeenCalledWith(hash, mockFile);
|
131
129
|
expect(result).toEqual(expectedResult);
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { DefaultErrorShape } from '@trpc/server/unstable-core-do-not-import';
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { lambdaClient } from '@/libs/trpc/client';
|
4
|
+
import { uploadService } from '@/services/upload';
|
4
5
|
import { useUserStore } from '@/store/user';
|
5
6
|
import { ImportPgDataStructure } from '@/types/export';
|
6
7
|
import { ImportStage, OnImportCallbacks } from '@/types/importer';
|
@@ -48,30 +49,7 @@ export class ServerService implements IImportService {
|
|
48
49
|
return;
|
49
50
|
}
|
50
51
|
|
51
|
-
|
52
|
-
const filename = `${uuid()}.json`;
|
53
|
-
|
54
|
-
const pathname = `import_config/${filename}`;
|
55
|
-
|
56
|
-
const url = await edgeClient.upload.createS3PreSignedUrl.mutate({ pathname });
|
57
|
-
|
58
|
-
try {
|
59
|
-
callbacks?.onStageChange?.(ImportStage.Uploading);
|
60
|
-
await this.uploadWithProgress(url, data, callbacks?.onFileUploading);
|
61
|
-
} catch {
|
62
|
-
throw new Error('Upload Error');
|
63
|
-
}
|
64
|
-
|
65
|
-
callbacks?.onStageChange?.(ImportStage.Importing);
|
66
|
-
const time = Date.now();
|
67
|
-
try {
|
68
|
-
const result = await lambdaClient.importer.importByFile.mutate({ pathname });
|
69
|
-
const duration = Date.now() - time;
|
70
|
-
callbacks?.onStageChange?.(ImportStage.Success);
|
71
|
-
callbacks?.onSuccess?.(result.results, duration);
|
72
|
-
} catch (e) {
|
73
|
-
handleError(e);
|
74
|
-
}
|
52
|
+
await this.uploadData(data, { callbacks, handleError });
|
75
53
|
};
|
76
54
|
|
77
55
|
importPgData: IImportService['importPgData'] = async (
|
@@ -115,16 +93,28 @@ export class ServerService implements IImportService {
|
|
115
93
|
return;
|
116
94
|
}
|
117
95
|
|
96
|
+
await this.uploadData(data, { callbacks, handleError });
|
97
|
+
};
|
98
|
+
|
99
|
+
private uploadData = async (
|
100
|
+
data: object,
|
101
|
+
{ callbacks, handleError }: { callbacks?: OnImportCallbacks; handleError: (e: unknown) => any },
|
102
|
+
) => {
|
118
103
|
// if the data is too large, upload it to S3 and upload by file
|
119
104
|
const filename = `${uuid()}.json`;
|
120
105
|
|
121
|
-
|
122
|
-
|
123
|
-
const url = await edgeClient.upload.createS3PreSignedUrl.mutate({ pathname });
|
124
|
-
|
106
|
+
let pathname;
|
125
107
|
try {
|
126
108
|
callbacks?.onStageChange?.(ImportStage.Uploading);
|
127
|
-
await
|
109
|
+
const result = await uploadService.uploadDataToS3(data, {
|
110
|
+
filename,
|
111
|
+
onProgress: (status, state) => {
|
112
|
+
callbacks?.onFileUploading?.(state);
|
113
|
+
},
|
114
|
+
pathname: `import_config/${filename}`,
|
115
|
+
});
|
116
|
+
pathname = result.data.path;
|
117
|
+
console.log(pathname);
|
128
118
|
} catch {
|
129
119
|
throw new Error('Upload Error');
|
130
120
|
}
|
@@ -140,45 +130,4 @@ export class ServerService implements IImportService {
|
|
140
130
|
handleError(e);
|
141
131
|
}
|
142
132
|
};
|
143
|
-
|
144
|
-
private uploadWithProgress = async (
|
145
|
-
url: string,
|
146
|
-
data: object,
|
147
|
-
onProgress: OnImportCallbacks['onFileUploading'],
|
148
|
-
) => {
|
149
|
-
const xhr = new XMLHttpRequest();
|
150
|
-
|
151
|
-
let startTime = Date.now();
|
152
|
-
xhr.upload.addEventListener('progress', (event) => {
|
153
|
-
if (event.lengthComputable) {
|
154
|
-
const progress = Number(((event.loaded / event.total) * 100).toFixed(1));
|
155
|
-
|
156
|
-
const speedInByte = event.loaded / ((Date.now() - startTime) / 1000);
|
157
|
-
|
158
|
-
onProgress?.({
|
159
|
-
// if the progress is 100, it means the file is uploaded
|
160
|
-
// but the server is still processing it
|
161
|
-
// so make it as 99.5 and let users think it's still uploading
|
162
|
-
progress: progress === 100 ? 99.5 : progress,
|
163
|
-
restTime: (event.total - event.loaded) / speedInByte,
|
164
|
-
speed: speedInByte / 1024,
|
165
|
-
});
|
166
|
-
}
|
167
|
-
});
|
168
|
-
|
169
|
-
xhr.open('PUT', url);
|
170
|
-
xhr.setRequestHeader('Content-Type', 'application/json');
|
171
|
-
|
172
|
-
return new Promise((resolve, reject) => {
|
173
|
-
xhr.addEventListener('load', () => {
|
174
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
175
|
-
resolve(xhr.response);
|
176
|
-
} else {
|
177
|
-
reject(xhr.statusText);
|
178
|
-
}
|
179
|
-
});
|
180
|
-
xhr.addEventListener('error', () => reject(xhr.statusText));
|
181
|
-
xhr.send(JSON.stringify(data));
|
182
|
-
});
|
183
|
-
};
|
184
133
|
}
|
package/src/services/ragEval.ts
CHANGED
@@ -40,7 +40,7 @@ class RAGEvalService {
|
|
40
40
|
};
|
41
41
|
|
42
42
|
importDatasetRecords = async (datasetId: number, file: File): Promise<void> => {
|
43
|
-
const { path } = await uploadService.
|
43
|
+
const { path } = await uploadService.uploadToServerS3(file, { directory: 'ragEval' });
|
44
44
|
|
45
45
|
await lambdaClient.ragEval.importDatasetRecords.mutate({ datasetId, pathname: path });
|
46
46
|
};
|
package/src/services/upload.ts
CHANGED
@@ -2,7 +2,7 @@ import dayjs from 'dayjs';
|
|
2
2
|
import { sha256 } from 'js-sha256';
|
3
3
|
|
4
4
|
import { fileEnv } from '@/config/file';
|
5
|
-
import { isServerMode } from '@/const/version';
|
5
|
+
import { isDesktop, isServerMode } from '@/const/version';
|
6
6
|
import { parseDataUri } from '@/libs/agent-runtime/utils/uriParser';
|
7
7
|
import { edgeClient } from '@/libs/trpc/client';
|
8
8
|
import { API_ENDPOINTS } from '@/services/_url';
|
@@ -16,7 +16,10 @@ export const UPLOAD_NETWORK_ERROR = 'NetWorkError';
|
|
16
16
|
interface UploadFileToS3Options {
|
17
17
|
directory?: string;
|
18
18
|
filename?: string;
|
19
|
+
onNotSupported?: () => void;
|
19
20
|
onProgress?: (status: FileUploadStatus, state: FileUploadState) => void;
|
21
|
+
pathname?: string;
|
22
|
+
skipCheckFileType?: boolean;
|
20
23
|
}
|
21
24
|
|
22
25
|
class UploadService {
|
@@ -25,20 +28,43 @@ class UploadService {
|
|
25
28
|
*/
|
26
29
|
uploadFileToS3 = async (
|
27
30
|
file: File,
|
28
|
-
|
29
|
-
): Promise<FileMetadata> => {
|
30
|
-
const {
|
31
|
+
{ onProgress, directory, skipCheckFileType, onNotSupported, pathname }: UploadFileToS3Options,
|
32
|
+
): Promise<{ data: FileMetadata; success: boolean }> => {
|
33
|
+
const { getElectronStoreState } = await import('@/store/electron');
|
34
|
+
const { electronSyncSelectors } = await import('@/store/electron/selectors');
|
35
|
+
// only if not enable sync
|
36
|
+
const state = getElectronStoreState();
|
37
|
+
const isSyncActive = electronSyncSelectors.isSyncActive(state);
|
38
|
+
|
39
|
+
// 桌面端上传逻辑(并且没开启 sync 同步)
|
40
|
+
if (isDesktop && !isSyncActive) {
|
41
|
+
const data = await this.uploadToDesktopS3(file);
|
42
|
+
return { data, success: true };
|
43
|
+
}
|
31
44
|
|
45
|
+
// 服务端上传逻辑
|
32
46
|
if (isServerMode) {
|
33
|
-
|
34
|
-
} else {
|
35
|
-
const fileArrayBuffer = await file.arrayBuffer();
|
47
|
+
// if is server mode, upload to server s3,
|
36
48
|
|
37
|
-
|
38
|
-
|
49
|
+
const data = await this.uploadToServerS3(file, { directory, onProgress, pathname });
|
50
|
+
return { data, success: true };
|
51
|
+
}
|
39
52
|
|
40
|
-
|
53
|
+
// upload to client s3
|
54
|
+
// 客户端上传逻辑
|
55
|
+
if (!skipCheckFileType && !file.type.startsWith('image')) {
|
56
|
+
onNotSupported?.();
|
57
|
+
return { data: undefined as unknown as FileMetadata, success: false };
|
41
58
|
}
|
59
|
+
|
60
|
+
const fileArrayBuffer = await file.arrayBuffer();
|
61
|
+
|
62
|
+
// 1. check file hash
|
63
|
+
const hash = sha256(fileArrayBuffer);
|
64
|
+
// Upload to the indexeddb in the browser
|
65
|
+
const data = await this.uploadToClientS3(hash, file);
|
66
|
+
|
67
|
+
return { data, success: true };
|
42
68
|
};
|
43
69
|
|
44
70
|
uploadBase64ToS3 = async (
|
@@ -79,7 +105,7 @@ class UploadService {
|
|
79
105
|
const file = new File([blob], fileName, { type: mimeType });
|
80
106
|
|
81
107
|
// 使用统一的上传方法
|
82
|
-
const metadata = await this.uploadFileToS3(file, options);
|
108
|
+
const { data: metadata } = await this.uploadFileToS3(file, options);
|
83
109
|
const hash = sha256(await file.arrayBuffer());
|
84
110
|
|
85
111
|
return {
|
@@ -90,19 +116,27 @@ class UploadService {
|
|
90
116
|
};
|
91
117
|
};
|
92
118
|
|
93
|
-
|
119
|
+
uploadDataToS3 = async (data: object, options: UploadFileToS3Options = {}) => {
|
120
|
+
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
|
121
|
+
const file = new File([blob], options.filename || 'data.json', { type: 'application/json' });
|
122
|
+
return await this.uploadFileToS3(file, options);
|
123
|
+
};
|
124
|
+
|
125
|
+
uploadToServerS3 = async (
|
94
126
|
file: File,
|
95
127
|
{
|
96
128
|
onProgress,
|
97
129
|
directory,
|
130
|
+
pathname,
|
98
131
|
}: {
|
99
132
|
directory?: string;
|
100
133
|
onProgress?: (status: FileUploadStatus, state: FileUploadState) => void;
|
134
|
+
pathname?: string;
|
101
135
|
},
|
102
136
|
): Promise<FileMetadata> => {
|
103
137
|
const xhr = new XMLHttpRequest();
|
104
138
|
|
105
|
-
const { preSignUrl, ...result } = await this.getSignedUploadUrl(file, directory);
|
139
|
+
const { preSignUrl, ...result } = await this.getSignedUploadUrl(file, { directory, pathname });
|
106
140
|
let startTime = Date.now();
|
107
141
|
xhr.upload.addEventListener('progress', (event) => {
|
108
142
|
if (event.lengthComputable) {
|
@@ -148,7 +182,7 @@ class UploadService {
|
|
148
182
|
return result;
|
149
183
|
};
|
150
184
|
|
151
|
-
|
185
|
+
private uploadToDesktopS3 = async (file: File) => {
|
152
186
|
const fileArrayBuffer = await file.arrayBuffer();
|
153
187
|
const hash = sha256(fileArrayBuffer);
|
154
188
|
|
@@ -157,7 +191,7 @@ class UploadService {
|
|
157
191
|
return metadata;
|
158
192
|
};
|
159
193
|
|
160
|
-
uploadToClientS3 = async (hash: string, file: File): Promise<FileMetadata> => {
|
194
|
+
private uploadToClientS3 = async (hash: string, file: File): Promise<FileMetadata> => {
|
161
195
|
await clientS3Storage.putObject(hash, file);
|
162
196
|
|
163
197
|
return {
|
@@ -183,7 +217,7 @@ class UploadService {
|
|
183
217
|
|
184
218
|
private getSignedUploadUrl = async (
|
185
219
|
file: File,
|
186
|
-
directory?: string,
|
220
|
+
options: { directory?: string; pathname?: string } = {},
|
187
221
|
): Promise<
|
188
222
|
FileMetadata & {
|
189
223
|
preSignUrl: string;
|
@@ -193,8 +227,8 @@ class UploadService {
|
|
193
227
|
|
194
228
|
// 精确到以 h 为单位的 path
|
195
229
|
const date = (Date.now() / 1000 / 60 / 60).toFixed(0);
|
196
|
-
const dirname = `${directory || fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/${date}`;
|
197
|
-
const pathname = `${dirname}/${filename}`;
|
230
|
+
const dirname = `${options.directory || fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/${date}`;
|
231
|
+
const pathname = options.pathname ?? `${dirname}/${filename}`;
|
198
232
|
|
199
233
|
const preSignUrl = await edgeClient.upload.createS3PreSignedUrl.mutate({ pathname });
|
200
234
|
|
@@ -677,7 +677,15 @@ export const generateAIChat: StateCreator<
|
|
677
677
|
// if there is no duration, it means the end of reasoning
|
678
678
|
if (!duration) {
|
679
679
|
duration = Date.now() - thinkingStartAt;
|
680
|
-
|
680
|
+
|
681
|
+
const isInChatReasoning = chatSelectors.isMessageInChatReasoning(messageId)(get());
|
682
|
+
if (isInChatReasoning) {
|
683
|
+
internal_toggleChatReasoning(
|
684
|
+
false,
|
685
|
+
messageId,
|
686
|
+
n('toggleChatReasoning/false') as string,
|
687
|
+
);
|
688
|
+
}
|
681
689
|
}
|
682
690
|
|
683
691
|
internal_dispatchMessage({
|
@@ -695,7 +703,11 @@ export const generateAIChat: StateCreator<
|
|
695
703
|
// if there is no thinkingStartAt, it means the start of reasoning
|
696
704
|
if (!thinkingStartAt) {
|
697
705
|
thinkingStartAt = Date.now();
|
698
|
-
internal_toggleChatReasoning(
|
706
|
+
internal_toggleChatReasoning(
|
707
|
+
true,
|
708
|
+
messageId,
|
709
|
+
n('toggleChatReasoning/true') as string,
|
710
|
+
);
|
699
711
|
}
|
700
712
|
|
701
713
|
thinking += chunk.text;
|
@@ -41,6 +41,7 @@ describe('chatToolSlice - dalle', () => {
|
|
41
41
|
vi.spyOn(uploadService, 'getImageFileByUrlWithCORS').mockResolvedValue(
|
42
42
|
new File(['1'], 'file.png', { type: 'image/png' }),
|
43
43
|
);
|
44
|
+
// @ts-ignore
|
44
45
|
vi.spyOn(uploadService, 'uploadToClientS3').mockResolvedValue({} as any);
|
45
46
|
vi.spyOn(ClientService.prototype, 'createFile').mockResolvedValue({
|
46
47
|
id: mockId,
|
@@ -56,6 +57,7 @@ describe('chatToolSlice - dalle', () => {
|
|
56
57
|
});
|
57
58
|
// For each prompt, loading is toggled on and then off
|
58
59
|
expect(imageGenerationService.generateImage).toHaveBeenCalledTimes(prompts.length);
|
60
|
+
// @ts-ignore
|
59
61
|
expect(uploadService.uploadToClientS3).toHaveBeenCalledTimes(prompts.length);
|
60
62
|
expect(result.current.toggleDallEImageLoading).toHaveBeenCalledTimes(prompts.length * 2);
|
61
63
|
});
|
@@ -157,6 +157,9 @@ const isMessageLoading = (id: string) => (s: ChatStoreState) => s.messageLoading
|
|
157
157
|
const isMessageGenerating = (id: string) => (s: ChatStoreState) => s.chatLoadingIds.includes(id);
|
158
158
|
const isMessageInRAGFlow = (id: string) => (s: ChatStoreState) =>
|
159
159
|
s.messageRAGLoadingIds.includes(id);
|
160
|
+
const isMessageInChatReasoning = (id: string) => (s: ChatStoreState) =>
|
161
|
+
s.reasoningLoadingIds.includes(id);
|
162
|
+
|
160
163
|
const isPluginApiInvoking = (id: string) => (s: ChatStoreState) =>
|
161
164
|
s.pluginApiLoadingIds.includes(id);
|
162
165
|
|
@@ -170,6 +173,7 @@ const isToolCallStreaming = (id: string, index: number) => (s: ChatStoreState) =
|
|
170
173
|
|
171
174
|
const isAIGenerating = (s: ChatStoreState) =>
|
172
175
|
s.chatLoadingIds.some((id) => mainDisplayChatIDs(s).includes(id));
|
176
|
+
|
173
177
|
const isInRAGFlow = (s: ChatStoreState) =>
|
174
178
|
s.messageRAGLoadingIds.some((id) => mainDisplayChatIDs(s).includes(id));
|
175
179
|
|
@@ -208,6 +212,7 @@ export const chatSelectors = {
|
|
208
212
|
isHasMessageLoading,
|
209
213
|
isMessageEditing,
|
210
214
|
isMessageGenerating,
|
215
|
+
isMessageInChatReasoning,
|
211
216
|
isMessageInRAGFlow,
|
212
217
|
isMessageLoading,
|
213
218
|
isPluginApiInvoking,
|
@@ -4,11 +4,8 @@ import { StateCreator } from 'zustand/vanilla';
|
|
4
4
|
|
5
5
|
import { message } from '@/components/AntdStaticMethods';
|
6
6
|
import { LOBE_CHAT_CLOUD } from '@/const/branding';
|
7
|
-
import { isDesktop, isServerMode } from '@/const/version';
|
8
7
|
import { fileService } from '@/services/file';
|
9
8
|
import { uploadService } from '@/services/upload';
|
10
|
-
import { getElectronStoreState } from '@/store/electron';
|
11
|
-
import { electronSyncSelectors } from '@/store/electron/selectors';
|
12
9
|
import { FileMetadata, UploadFileItem } from '@/types/files';
|
13
10
|
|
14
11
|
import { FileStore } from '../../store';
|
@@ -96,25 +93,8 @@ export const createFileUploadSlice: StateCreator<
|
|
96
93
|
}
|
97
94
|
// 2. if file don't exist, need upload files
|
98
95
|
else {
|
99
|
-
|
100
|
-
|
101
|
-
const isSyncActive = electronSyncSelectors.isSyncActive(state);
|
102
|
-
|
103
|
-
if (isDesktop && !isSyncActive) {
|
104
|
-
metadata = await uploadService.uploadToDesktop(file);
|
105
|
-
} else if (isServerMode) {
|
106
|
-
// if is server mode, upload to server s3, or upload to client s3
|
107
|
-
metadata = await uploadService.uploadWithProgress(file, {
|
108
|
-
onProgress: (status, upload) => {
|
109
|
-
onStatusUpdate?.({
|
110
|
-
id: file.name,
|
111
|
-
type: 'updateFile',
|
112
|
-
value: { status: status === 'success' ? 'processing' : status, uploadState: upload },
|
113
|
-
});
|
114
|
-
},
|
115
|
-
});
|
116
|
-
} else {
|
117
|
-
if (!skipCheckFileType && !file.type.startsWith('image')) {
|
96
|
+
const { data, success } = await uploadService.uploadFileToS3(file, {
|
97
|
+
onNotSupported: () => {
|
118
98
|
onStatusUpdate?.({ id: file.name, type: 'removeFile' });
|
119
99
|
message.info({
|
120
100
|
content: t('upload.fileOnlySupportInServerMode', {
|
@@ -124,12 +104,19 @@ export const createFileUploadSlice: StateCreator<
|
|
124
104
|
}),
|
125
105
|
duration: 5,
|
126
106
|
});
|
127
|
-
|
128
|
-
|
107
|
+
},
|
108
|
+
onProgress: (status, upload) => {
|
109
|
+
onStatusUpdate?.({
|
110
|
+
id: file.name,
|
111
|
+
type: 'updateFile',
|
112
|
+
value: { status: status === 'success' ? 'processing' : status, uploadState: upload },
|
113
|
+
});
|
114
|
+
},
|
115
|
+
skipCheckFileType,
|
116
|
+
});
|
117
|
+
if (!success) return;
|
129
118
|
|
130
|
-
|
131
|
-
metadata = await uploadService.uploadToClientS3(hash, file);
|
132
|
-
}
|
119
|
+
metadata = data;
|
133
120
|
}
|
134
121
|
|
135
122
|
// 3. use more powerful file type detector to get file type
|
@@ -41,8 +41,7 @@ describe('fetchSSE', () => {
|
|
41
41
|
smoothing: false,
|
42
42
|
});
|
43
43
|
|
44
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello', type: 'text' });
|
45
|
-
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: ' World', type: 'text' });
|
44
|
+
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello World', type: 'text' });
|
46
45
|
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
47
46
|
observationId: null,
|
48
47
|
toolCalls: undefined,
|
@@ -123,7 +122,7 @@ describe('fetchSSE', () => {
|
|
123
122
|
});
|
124
123
|
});
|
125
124
|
|
126
|
-
it('should handle text event with smoothing correctly', async () => {
|
125
|
+
it.skip('should handle text event with smoothing correctly', async () => {
|
127
126
|
const mockOnMessageHandle = vi.fn();
|
128
127
|
const mockOnFinish = vi.fn();
|
129
128
|
|
@@ -178,9 +177,9 @@ describe('fetchSSE', () => {
|
|
178
177
|
async (url: string, options: FetchEventSourceInit) => {
|
179
178
|
options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
|
180
179
|
options.onmessage!({ event: 'reasoning', data: JSON.stringify('Hello') } as any);
|
181
|
-
await sleep(
|
180
|
+
await sleep(400);
|
182
181
|
options.onmessage!({ event: 'reasoning', data: JSON.stringify(' World') } as any);
|
183
|
-
await sleep(
|
182
|
+
await sleep(400);
|
184
183
|
options.onmessage!({ event: 'text', data: JSON.stringify('hi') } as any);
|
185
184
|
},
|
186
185
|
);
|
@@ -321,19 +320,7 @@ describe('fetchSSE', () => {
|
|
321
320
|
smoothing: true,
|
322
321
|
});
|
323
322
|
|
324
|
-
const expectedMessages = [
|
325
|
-
{ text: 'H', type: 'text' },
|
326
|
-
{ text: 'e', type: 'text' },
|
327
|
-
{ text: 'l', type: 'text' },
|
328
|
-
{ text: 'l', type: 'text' },
|
329
|
-
{ text: 'o', type: 'text' },
|
330
|
-
{ text: ' ', type: 'text' },
|
331
|
-
{ text: 'W', type: 'text' },
|
332
|
-
{ text: 'o', type: 'text' },
|
333
|
-
{ text: 'r', type: 'text' },
|
334
|
-
{ text: 'l', type: 'text' },
|
335
|
-
{ text: 'd', type: 'text' },
|
336
|
-
];
|
323
|
+
const expectedMessages = [{ text: 'Hello World', type: 'text' }];
|
337
324
|
|
338
325
|
expectedMessages.forEach((message, index) => {
|
339
326
|
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(index + 1, message);
|
@@ -315,11 +315,27 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
315
315
|
|
316
316
|
const { smoothing } = options;
|
317
317
|
|
318
|
-
const textSmoothing =
|
318
|
+
const textSmoothing = false;
|
319
|
+
// TODO: 看下后面就是完全移除 smoothing 还是怎么说
|
320
|
+
// const textSmoothing = typeof smoothing === 'boolean' ? smoothing : (smoothing?.text ?? true);
|
319
321
|
const toolsCallingSmoothing =
|
320
322
|
typeof smoothing === 'boolean' ? smoothing : (smoothing?.toolsCalling ?? true);
|
323
|
+
|
321
324
|
const smoothingSpeed = isObject(smoothing) ? smoothing.speed : undefined;
|
322
325
|
|
326
|
+
// 添加文本buffer和计时器相关变量
|
327
|
+
let textBuffer = '';
|
328
|
+
// eslint-disable-next-line no-undef
|
329
|
+
let bufferTimer: NodeJS.Timeout | null = null;
|
330
|
+
const BUFFER_INTERVAL = 300; // 300ms
|
331
|
+
|
332
|
+
const flushTextBuffer = () => {
|
333
|
+
if (textBuffer) {
|
334
|
+
options.onMessageHandle?.({ text: textBuffer, type: 'text' });
|
335
|
+
textBuffer = '';
|
336
|
+
}
|
337
|
+
};
|
338
|
+
|
323
339
|
let output = '';
|
324
340
|
const textController = createSmoothMessage({
|
325
341
|
onTextUpdate: (delta, text) => {
|
@@ -340,6 +356,18 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
340
356
|
startSpeed: smoothingSpeed,
|
341
357
|
});
|
342
358
|
|
359
|
+
let thinkingBuffer = '';
|
360
|
+
// eslint-disable-next-line no-undef
|
361
|
+
let thinkingBufferTimer: NodeJS.Timeout | null = null;
|
362
|
+
|
363
|
+
// 创建一个函数来处理buffer的刷新
|
364
|
+
const flushThinkingBuffer = () => {
|
365
|
+
if (thinkingBuffer) {
|
366
|
+
options.onMessageHandle?.({ text: thinkingBuffer, type: 'reasoning' });
|
367
|
+
thinkingBuffer = '';
|
368
|
+
}
|
369
|
+
};
|
370
|
+
|
343
371
|
const toolCallsController = createSmoothToolCalls({
|
344
372
|
onToolCallsUpdate: (toolCalls, isAnimationActives) => {
|
345
373
|
options.onMessageHandle?.({ isAnimationActives, tool_calls: toolCalls, type: 'tool_calls' });
|
@@ -430,7 +458,17 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
430
458
|
if (!textController.isAnimationActive) textController.startAnimation();
|
431
459
|
} else {
|
432
460
|
output += data;
|
433
|
-
|
461
|
+
|
462
|
+
// 使用buffer机制
|
463
|
+
textBuffer += data;
|
464
|
+
|
465
|
+
// 如果还没有设置计时器,创建一个
|
466
|
+
if (!bufferTimer) {
|
467
|
+
bufferTimer = setTimeout(() => {
|
468
|
+
flushTextBuffer();
|
469
|
+
bufferTimer = null;
|
470
|
+
}, BUFFER_INTERVAL);
|
471
|
+
}
|
434
472
|
}
|
435
473
|
|
436
474
|
break;
|
@@ -466,7 +504,17 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
466
504
|
if (!thinkingController.isAnimationActive) thinkingController.startAnimation();
|
467
505
|
} else {
|
468
506
|
thinking += data;
|
469
|
-
|
507
|
+
|
508
|
+
// 使用buffer机制
|
509
|
+
thinkingBuffer += data;
|
510
|
+
|
511
|
+
// 如果还没有设置计时器,创建一个
|
512
|
+
if (!thinkingBufferTimer) {
|
513
|
+
thinkingBufferTimer = setTimeout(() => {
|
514
|
+
flushThinkingBuffer();
|
515
|
+
thinkingBufferTimer = null;
|
516
|
+
}, BUFFER_INTERVAL);
|
517
|
+
}
|
470
518
|
}
|
471
519
|
|
472
520
|
break;
|
@@ -509,6 +557,17 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
509
557
|
textController.stopAnimation();
|
510
558
|
toolCallsController.stopAnimations();
|
511
559
|
|
560
|
+
// 确保所有缓冲区数据都被处理
|
561
|
+
if (bufferTimer) {
|
562
|
+
clearTimeout(bufferTimer);
|
563
|
+
flushTextBuffer();
|
564
|
+
}
|
565
|
+
|
566
|
+
if (thinkingBufferTimer) {
|
567
|
+
clearTimeout(thinkingBufferTimer);
|
568
|
+
flushThinkingBuffer();
|
569
|
+
}
|
570
|
+
|
512
571
|
if (response.ok) {
|
513
572
|
// if there is no onMessageHandler, we should call onHandleMessage first
|
514
573
|
if (!triggerOnMessageHandler) {
|