@lobehub/chat 1.82.0 → 1.82.2
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/.cursor/rules/desktop-local-tools-implement.mdc +80 -0
- package/.env.desktop +2 -1
- package/.github/scripts/pr-comment.js +4 -9
- package/CHANGELOG.md +51 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/electron.json +38 -2
- package/locales/ar/plugin.json +51 -31
- package/locales/bg-BG/electron.json +38 -2
- package/locales/bg-BG/plugin.json +51 -31
- package/locales/de-DE/electron.json +38 -2
- package/locales/de-DE/plugin.json +29 -9
- package/locales/en-US/electron.json +38 -2
- package/locales/en-US/plugin.json +29 -9
- package/locales/es-ES/electron.json +38 -2
- package/locales/es-ES/plugin.json +51 -31
- package/locales/fa-IR/electron.json +38 -2
- package/locales/fa-IR/plugin.json +51 -31
- package/locales/fr-FR/electron.json +38 -2
- package/locales/fr-FR/plugin.json +51 -31
- package/locales/it-IT/electron.json +38 -2
- package/locales/it-IT/plugin.json +51 -31
- package/locales/ja-JP/electron.json +38 -2
- package/locales/ja-JP/plugin.json +51 -31
- package/locales/ko-KR/electron.json +38 -2
- package/locales/ko-KR/plugin.json +29 -9
- package/locales/nl-NL/electron.json +38 -2
- package/locales/nl-NL/plugin.json +51 -31
- package/locales/pl-PL/electron.json +38 -2
- package/locales/pl-PL/plugin.json +29 -9
- package/locales/pt-BR/electron.json +38 -2
- package/locales/pt-BR/plugin.json +51 -31
- package/locales/ru-RU/electron.json +38 -2
- package/locales/ru-RU/plugin.json +51 -31
- package/locales/tr-TR/electron.json +38 -2
- package/locales/tr-TR/plugin.json +51 -31
- package/locales/vi-VN/electron.json +38 -2
- package/locales/vi-VN/plugin.json +29 -9
- package/locales/zh-CN/electron.json +38 -2
- package/locales/zh-CN/plugin.json +30 -10
- package/locales/zh-TW/electron.json +38 -2
- package/locales/zh-TW/plugin.json +51 -31
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/update.ts +3 -3
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx +222 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Option.tsx +104 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx +42 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Waiting.tsx +203 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/index.tsx +57 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateModal.tsx +242 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateNotification.tsx +193 -0
- package/src/app/[variants]/(main)/_layout/Desktop/{Titlebar.tsx → ElectronTitlebar/index.tsx} +15 -1
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/BottomActions.tsx +3 -2
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
- package/src/app/[variants]/layout.tsx +2 -1
- package/src/config/aiModels/openrouter.ts +6 -6
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/LocalFile.tsx +65 -0
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +29 -0
- package/src/features/Conversation/components/MarkdownElements/LocalFile/index.ts +16 -0
- package/src/features/Conversation/components/MarkdownElements/index.ts +7 -1
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +260 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +204 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +133 -0
- package/src/features/Conversation/components/MarkdownElements/type.ts +5 -1
- package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +20 -0
- package/src/features/PluginDevModal/MCPManifestForm/MCPTypeSelect.tsx +176 -0
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +289 -0
- package/src/features/PluginDevModal/MCPManifestForm/utils.test.ts +262 -0
- package/src/features/PluginDevModal/MCPManifestForm/utils.ts +151 -0
- package/src/features/PluginDevModal/index.tsx +31 -22
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -1
- package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +0 -56
- package/src/locales/default/electron.ts +38 -2
- package/src/locales/default/plugin.ts +28 -8
- package/src/server/modules/ElectronIPCClient/index.ts +36 -0
- package/src/server/routers/lambda/session.ts +2 -6
- package/src/server/routers/tools/mcp.ts +6 -0
- package/src/server/services/file/impls/index.ts +9 -1
- package/src/server/services/file/impls/local.test.ts +299 -0
- package/src/server/services/file/impls/local.ts +183 -0
- package/src/server/services/mcp/index.ts +26 -0
- package/src/services/aiModel/index.ts +5 -1
- package/src/services/aiProvider/index.ts +5 -1
- package/src/services/electron/autoUpdate.ts +4 -0
- package/src/services/file/index.ts +5 -1
- package/src/services/mcp.ts +13 -2
- package/src/services/message/index.ts +5 -1
- package/src/services/plugin/index.ts +5 -1
- package/src/services/session/index.ts +5 -1
- package/src/services/tableViewer/desktop.ts +15 -0
- package/src/services/tableViewer/index.ts +4 -1
- package/src/services/thread/index.ts +5 -1
- package/src/services/topic/index.ts +5 -1
- package/src/services/user/index.ts +5 -1
- package/src/store/electron/actions/app.ts +59 -0
- package/src/store/electron/actions/sync.ts +5 -1
- package/src/store/electron/initialState.ts +3 -1
- package/src/store/electron/store.ts +6 -1
- package/src/store/tool/slices/customPlugin/action.ts +16 -4
- package/src/utils/client/GlobalAgentContextManager.ts +85 -0
- package/src/utils/promptTemplate.test.ts +78 -0
- package/src/utils/promptTemplate.ts +17 -0
- package/src/features/PluginDevModal/MCPManifestForm.tsx +0 -164
@@ -0,0 +1,203 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
|
4
|
+
import { Icon } from '@lobehub/ui';
|
5
|
+
import { Button, Typography } from 'antd';
|
6
|
+
import { createStyles, cx, keyframes } from 'antd-style';
|
7
|
+
import { WifiIcon } from 'lucide-react';
|
8
|
+
import { memo } from 'react';
|
9
|
+
import { useTranslation } from 'react-i18next';
|
10
|
+
|
11
|
+
import { useElectronStore } from '@/store/electron';
|
12
|
+
|
13
|
+
const { Text, Title } = Typography;
|
14
|
+
|
15
|
+
const airdropPulse = keyframes`
|
16
|
+
0% {
|
17
|
+
transform: translate(-50%, -50%) scale(0.8);
|
18
|
+
opacity: 0.5;
|
19
|
+
}
|
20
|
+
100% {
|
21
|
+
transform: translate(-50%, -50%) scale(2.5);
|
22
|
+
opacity: 0;
|
23
|
+
}
|
24
|
+
`;
|
25
|
+
|
26
|
+
const useStyles = createStyles(({ css, token }) => ({
|
27
|
+
container: css`
|
28
|
+
overflow: hidden;
|
29
|
+
display: flex;
|
30
|
+
flex-direction: column;
|
31
|
+
align-items: center;
|
32
|
+
justify-content: center;
|
33
|
+
|
34
|
+
min-height: 100vh;
|
35
|
+
|
36
|
+
color: ${token.colorTextBase};
|
37
|
+
|
38
|
+
background-color: ${token.colorBgContainer};
|
39
|
+
`,
|
40
|
+
|
41
|
+
content: css`
|
42
|
+
z-index: 10;
|
43
|
+
display: flex;
|
44
|
+
flex-direction: column;
|
45
|
+
align-items: center;
|
46
|
+
`,
|
47
|
+
|
48
|
+
description: css`
|
49
|
+
margin-block-end: ${token.marginXL}px !important;
|
50
|
+
color: ${token.colorTextSecondary} !important;
|
51
|
+
`,
|
52
|
+
|
53
|
+
helpLink: css`
|
54
|
+
margin-inline-start: ${token.marginXXS}px;
|
55
|
+
color: ${token.colorTextSecondary};
|
56
|
+
text-decoration: underline;
|
57
|
+
text-underline-offset: 2px;
|
58
|
+
|
59
|
+
&:hover {
|
60
|
+
color: ${token.colorText};
|
61
|
+
}
|
62
|
+
`,
|
63
|
+
|
64
|
+
helpText: css`
|
65
|
+
margin-block-start: ${token.marginLG}px;
|
66
|
+
font-size: ${token.fontSizeSM}px;
|
67
|
+
color: ${token.colorTextTertiary};
|
68
|
+
`,
|
69
|
+
// 新增:图标和脉冲动画的容器
|
70
|
+
iconContainer: css`
|
71
|
+
position: relative;
|
72
|
+
|
73
|
+
display: flex;
|
74
|
+
align-items: center;
|
75
|
+
justify-content: center;
|
76
|
+
|
77
|
+
width: 160px;
|
78
|
+
height: 160px;
|
79
|
+
margin-block-end: ${token.marginXL}px;
|
80
|
+
`,
|
81
|
+
|
82
|
+
// 新增:不同延迟的脉冲动画
|
83
|
+
pulse1: css`
|
84
|
+
animation: ${airdropPulse} 3s ease-out infinite;
|
85
|
+
`,
|
86
|
+
|
87
|
+
pulse2: css`
|
88
|
+
animation: ${airdropPulse} 3s ease-out 1.2s infinite;
|
89
|
+
`,
|
90
|
+
|
91
|
+
pulse3: css`
|
92
|
+
animation: ${airdropPulse} 3s ease-out 1.8s infinite;
|
93
|
+
`,
|
94
|
+
// 新增:基础脉冲样式
|
95
|
+
pulseBase: css`
|
96
|
+
pointer-events: none;
|
97
|
+
content: '';
|
98
|
+
|
99
|
+
position: absolute;
|
100
|
+
inset-block-start: 50%;
|
101
|
+
inset-inline-start: 50%;
|
102
|
+
transform: translate(-50%, -50%);
|
103
|
+
|
104
|
+
width: 100px;
|
105
|
+
height: 100px;
|
106
|
+
border-radius: 50%;
|
107
|
+
|
108
|
+
opacity: 0;
|
109
|
+
background-color: ${token.colorPrimaryBgHover};
|
110
|
+
`,
|
111
|
+
|
112
|
+
// 新增:Radar 图标样式
|
113
|
+
radarIcon: css`
|
114
|
+
z-index: 1;
|
115
|
+
color: ${token.colorPrimary};
|
116
|
+
`,
|
117
|
+
|
118
|
+
ring1: css`
|
119
|
+
width: 80px;
|
120
|
+
height: 80px;
|
121
|
+
border: 1px solid ${token.colorText};
|
122
|
+
`,
|
123
|
+
|
124
|
+
ring2: css`
|
125
|
+
width: 120px;
|
126
|
+
height: 120px;
|
127
|
+
border: 1px solid ${token.colorTextQuaternary};
|
128
|
+
`,
|
129
|
+
|
130
|
+
ring3: css`
|
131
|
+
width: 160px;
|
132
|
+
height: 160px;
|
133
|
+
border: 1px solid ${token.colorFillSecondary};
|
134
|
+
`,
|
135
|
+
|
136
|
+
// 新增:星环基础样式
|
137
|
+
ringBase: css`
|
138
|
+
pointer-events: none;
|
139
|
+
|
140
|
+
position: absolute;
|
141
|
+
inset-block-start: 50%;
|
142
|
+
inset-inline-start: 50%;
|
143
|
+
transform: translate(-50%, -50%);
|
144
|
+
|
145
|
+
border-radius: 50%;
|
146
|
+
`,
|
147
|
+
title: css`
|
148
|
+
margin-block-end: ${token.marginSM}px !important;
|
149
|
+
color: ${token.colorText} !important;
|
150
|
+
`,
|
151
|
+
}));
|
152
|
+
|
153
|
+
interface WaitingOAuthProps {
|
154
|
+
setIsOpen: (open: boolean) => void;
|
155
|
+
setWaiting: (waiting: boolean) => void;
|
156
|
+
}
|
157
|
+
const WaitingOAuth = memo<WaitingOAuthProps>(({ setWaiting, setIsOpen }) => {
|
158
|
+
const { styles } = useStyles();
|
159
|
+
const { t } = useTranslation('electron'); // 指定 namespace 为 electron
|
160
|
+
const [disconnect, refreshServerConfig] = useElectronStore((s) => [
|
161
|
+
s.disconnectRemoteServer,
|
162
|
+
s.refreshServerConfig,
|
163
|
+
]);
|
164
|
+
|
165
|
+
const handleCancel = async () => {
|
166
|
+
await disconnect();
|
167
|
+
setWaiting(false);
|
168
|
+
};
|
169
|
+
|
170
|
+
useWatchBroadcast('authorizationSuccessful', async () => {
|
171
|
+
setIsOpen(false);
|
172
|
+
setWaiting(false);
|
173
|
+
await refreshServerConfig();
|
174
|
+
});
|
175
|
+
|
176
|
+
return (
|
177
|
+
<div className={styles.container}>
|
178
|
+
<div className={styles.content}>
|
179
|
+
{/* 更新为新的图标和脉冲动画结构 */}
|
180
|
+
<div className={styles.iconContainer}>
|
181
|
+
{/* 新增:星环 */}
|
182
|
+
<div className={cx(styles.ringBase, styles.ring1)} />
|
183
|
+
<div className={cx(styles.ringBase, styles.ring2)} />
|
184
|
+
<div className={cx(styles.ringBase, styles.ring3)} />
|
185
|
+
{/* 脉冲 */}
|
186
|
+
<div className={cx(styles.pulseBase, styles.pulse1)} />
|
187
|
+
<div className={cx(styles.pulseBase, styles.pulse2)} />
|
188
|
+
<div className={cx(styles.pulseBase, styles.pulse3)} />
|
189
|
+
|
190
|
+
<Icon className={styles.radarIcon} icon={WifiIcon} size={{ fontSize: 40 }} />
|
191
|
+
</div>
|
192
|
+
<Title className={styles.title} level={4}>
|
193
|
+
{t('waitingOAuth.title')}
|
194
|
+
</Title>
|
195
|
+
<Text className={styles.description}>{t('waitingOAuth.description')}</Text>
|
196
|
+
<Button onClick={handleCancel}>{t('waitingOAuth.cancel')}</Button>{' '}
|
197
|
+
<Text className={styles.helpText}>{t('waitingOAuth.helpText')}</Text>
|
198
|
+
</div>
|
199
|
+
</div>
|
200
|
+
);
|
201
|
+
});
|
202
|
+
|
203
|
+
export default WaitingOAuth;
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import { Drawer } from 'antd';
|
2
|
+
import { createStyles } from 'antd-style';
|
3
|
+
import { useState } from 'react';
|
4
|
+
|
5
|
+
import Mode from './Mode';
|
6
|
+
import Sync from './Sync';
|
7
|
+
import WaitingOAuth from './Waiting';
|
8
|
+
|
9
|
+
const useStyles = createStyles(({ css }) => {
|
10
|
+
return {
|
11
|
+
modal: css`
|
12
|
+
.ant-drawer-close {
|
13
|
+
position: absolute;
|
14
|
+
inset-block-start: 8px;
|
15
|
+
inset-inline-end: 0;
|
16
|
+
}
|
17
|
+
`,
|
18
|
+
};
|
19
|
+
});
|
20
|
+
|
21
|
+
const Connection = () => {
|
22
|
+
const { styles, theme } = useStyles();
|
23
|
+
|
24
|
+
const [isOpen, setIsOpen] = useState(false);
|
25
|
+
const [isWaiting, setWaiting] = useState(false);
|
26
|
+
|
27
|
+
return (
|
28
|
+
<>
|
29
|
+
<Sync
|
30
|
+
onClick={() => {
|
31
|
+
setIsOpen(true);
|
32
|
+
}}
|
33
|
+
/>
|
34
|
+
<Drawer
|
35
|
+
classNames={{ header: styles.modal }}
|
36
|
+
height={'100vh'}
|
37
|
+
onClose={() => {
|
38
|
+
setIsOpen(false);
|
39
|
+
}}
|
40
|
+
open={isOpen}
|
41
|
+
placement={'top'}
|
42
|
+
style={{
|
43
|
+
background: theme.colorBgLayout,
|
44
|
+
}}
|
45
|
+
styles={{ body: { padding: 0 }, header: { padding: 0 } }}
|
46
|
+
>
|
47
|
+
{isWaiting ? (
|
48
|
+
<WaitingOAuth setIsOpen={setIsOpen} setWaiting={setWaiting} />
|
49
|
+
) : (
|
50
|
+
<Mode setIsOpen={setIsOpen} setWaiting={setWaiting} />
|
51
|
+
)}
|
52
|
+
</Drawer>
|
53
|
+
</>
|
54
|
+
);
|
55
|
+
};
|
56
|
+
|
57
|
+
export default Connection;
|
@@ -0,0 +1,242 @@
|
|
1
|
+
import { ProgressInfo, UpdateInfo, useWatchBroadcast } from '@lobechat/electron-client-ipc';
|
2
|
+
import { App, Button, Modal, Progress, Spin } from 'antd';
|
3
|
+
import React, { memo, useState } from 'react';
|
4
|
+
import { useTranslation } from 'react-i18next';
|
5
|
+
|
6
|
+
import { autoUpdateService } from '@/services/electron/autoUpdate';
|
7
|
+
|
8
|
+
export const UpdateModal = memo(() => {
|
9
|
+
const { t } = useTranslation(['electron', 'common']);
|
10
|
+
|
11
|
+
const [isChecking, setIsChecking] = useState(false);
|
12
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
13
|
+
const [updateAvailableInfo, setUpdateAvailableInfo] = useState<UpdateInfo | null>(null);
|
14
|
+
const [downloadedInfo, setDownloadedInfo] = useState<UpdateInfo | null>(null);
|
15
|
+
const [progress, setProgress] = useState<ProgressInfo | null>(null);
|
16
|
+
const [latestVersionInfo, setLatestVersionInfo] = useState<UpdateInfo | null>(null); // State for latest version modal
|
17
|
+
const { modal } = App.useApp();
|
18
|
+
// --- Event Listeners ---
|
19
|
+
|
20
|
+
useWatchBroadcast('manualUpdateCheckStart', () => {
|
21
|
+
console.log('[Manual Update] Check Start');
|
22
|
+
setIsChecking(true);
|
23
|
+
setUpdateAvailableInfo(null);
|
24
|
+
setDownloadedInfo(null);
|
25
|
+
setProgress(null);
|
26
|
+
setLatestVersionInfo(null); // Reset latest version info
|
27
|
+
// Optional: Show a brief notification that check has started
|
28
|
+
// notification.info({ message: t('updater.checking') });
|
29
|
+
});
|
30
|
+
|
31
|
+
useWatchBroadcast('manualUpdateAvailable', (info: UpdateInfo) => {
|
32
|
+
console.log('[Manual Update] Available:', info);
|
33
|
+
// Only react if it's part of a manual check flow (i.e., isChecking was true)
|
34
|
+
// No need to check isChecking here as this event is specific
|
35
|
+
setIsChecking(false);
|
36
|
+
setUpdateAvailableInfo(info);
|
37
|
+
});
|
38
|
+
|
39
|
+
useWatchBroadcast('manualUpdateNotAvailable', (info) => {
|
40
|
+
console.log('[Manual Update] Not Available:', info);
|
41
|
+
// Only react if it's part of a manual check flow
|
42
|
+
// No need to check isChecking here as this event is specific
|
43
|
+
setIsChecking(false);
|
44
|
+
setLatestVersionInfo(info); // Set info for the modal
|
45
|
+
// notification.success({
|
46
|
+
// description: t('updater.isLatestVersionDesc', { version: info.version }),
|
47
|
+
// message: t('updater.isLatestVersion'),
|
48
|
+
// });
|
49
|
+
});
|
50
|
+
|
51
|
+
useWatchBroadcast('updateError', (message: string) => {
|
52
|
+
console.log('[Manual Update] Error:', message);
|
53
|
+
// Only react if it's part of a manual check/download flow
|
54
|
+
if (isChecking || isDownloading) {
|
55
|
+
setIsChecking(false);
|
56
|
+
setIsDownloading(false);
|
57
|
+
// Show error modal or notification
|
58
|
+
modal.error({ content: message, title: t('updater.updateError') });
|
59
|
+
setLatestVersionInfo(null); // Ensure other modals are closed on error
|
60
|
+
setUpdateAvailableInfo(null);
|
61
|
+
setDownloadedInfo(null);
|
62
|
+
}
|
63
|
+
});
|
64
|
+
|
65
|
+
useWatchBroadcast('updateDownloadStart', () => {
|
66
|
+
console.log('[Manual Update] Download Start');
|
67
|
+
// This event implies a manual download was triggered (likely from the 'updateAvailable' modal)
|
68
|
+
setIsDownloading(true);
|
69
|
+
setUpdateAvailableInfo(null); // Hide the 'download' button modal
|
70
|
+
setProgress({ bytesPerSecond: 0, percent: 0, total: 0, transferred: 0 }); // Reset progress
|
71
|
+
setLatestVersionInfo(null); // Ensure other modals are closed
|
72
|
+
// Optional: Show notification that download started
|
73
|
+
// notification.info({ message: t('updater.downloadingUpdate') });
|
74
|
+
});
|
75
|
+
|
76
|
+
useWatchBroadcast('updateDownloadProgress', (progressInfo: ProgressInfo) => {
|
77
|
+
console.log('[Manual Update] Progress:', progressInfo);
|
78
|
+
// Only update progress if we are in the manual download state
|
79
|
+
setProgress(progressInfo);
|
80
|
+
});
|
81
|
+
|
82
|
+
useWatchBroadcast('updateDownloaded', (info: UpdateInfo) => {
|
83
|
+
console.log('[Manual Update] Downloaded:', info);
|
84
|
+
// This event implies a download finished, likely the one we started manually
|
85
|
+
setIsChecking(false);
|
86
|
+
setIsDownloading(false);
|
87
|
+
setDownloadedInfo(info);
|
88
|
+
setProgress(null); // Clear progress
|
89
|
+
setLatestVersionInfo(null); // Ensure other modals are closed
|
90
|
+
setUpdateAvailableInfo(null);
|
91
|
+
});
|
92
|
+
|
93
|
+
// --- Render Logic ---
|
94
|
+
|
95
|
+
const handleDownload = () => {
|
96
|
+
if (!updateAvailableInfo) return;
|
97
|
+
// No need to set states here, 'updateDownloadStart' will handle it
|
98
|
+
autoUpdateService.downloadUpdate();
|
99
|
+
};
|
100
|
+
|
101
|
+
const handleInstallNow = () => {
|
102
|
+
setDownloadedInfo(null); // Close modal immediately
|
103
|
+
autoUpdateService.installNow();
|
104
|
+
};
|
105
|
+
|
106
|
+
const handleInstallLater = () => {
|
107
|
+
// No need to set state here, 'updateWillInstallLater' handles it
|
108
|
+
autoUpdateService.installLater();
|
109
|
+
setDownloadedInfo(null); // Close the modal after clicking
|
110
|
+
};
|
111
|
+
|
112
|
+
const closeAvailableModal = () => setUpdateAvailableInfo(null);
|
113
|
+
const closeDownloadedModal = () => setDownloadedInfo(null);
|
114
|
+
const closeLatestVersionModal = () => setLatestVersionInfo(null);
|
115
|
+
|
116
|
+
const renderCheckingModal = () => (
|
117
|
+
<Modal
|
118
|
+
closable={false}
|
119
|
+
footer={null}
|
120
|
+
maskClosable={false}
|
121
|
+
open={isChecking}
|
122
|
+
title={t('updater.checkingUpdate')}
|
123
|
+
>
|
124
|
+
<Spin spinning={true}>
|
125
|
+
<div style={{ padding: '20px', textAlign: 'center' }}>
|
126
|
+
{t('updater.checkingUpdateDesc')}
|
127
|
+
</div>
|
128
|
+
</Spin>
|
129
|
+
</Modal>
|
130
|
+
);
|
131
|
+
|
132
|
+
const renderAvailableModal = () => (
|
133
|
+
<Modal
|
134
|
+
footer={[
|
135
|
+
<Button key="cancel" onClick={closeAvailableModal}>
|
136
|
+
{t('cancel', { ns: 'common' })}
|
137
|
+
</Button>,
|
138
|
+
<Button key="download" onClick={handleDownload} type="primary">
|
139
|
+
{t('updater.downloadNewVersion')}
|
140
|
+
</Button>,
|
141
|
+
]}
|
142
|
+
onCancel={closeAvailableModal}
|
143
|
+
open={!!updateAvailableInfo}
|
144
|
+
title={t('updater.newVersionAvailable')}
|
145
|
+
>
|
146
|
+
<h4>{t('updater.newVersionAvailableDesc', { version: updateAvailableInfo?.version })}</h4>
|
147
|
+
{updateAvailableInfo?.releaseNotes && (
|
148
|
+
<div
|
149
|
+
dangerouslySetInnerHTML={{ __html: updateAvailableInfo.releaseNotes as string }}
|
150
|
+
style={{
|
151
|
+
// background:theme
|
152
|
+
borderRadius: 4,
|
153
|
+
marginTop: 8,
|
154
|
+
maxHeight: 300,
|
155
|
+
overflow: 'auto',
|
156
|
+
padding: '8px 12px',
|
157
|
+
}}
|
158
|
+
/>
|
159
|
+
)}
|
160
|
+
</Modal>
|
161
|
+
);
|
162
|
+
|
163
|
+
const renderDownloadingModal = () => {
|
164
|
+
const percent = progress ? Math.round(progress.percent) : 0;
|
165
|
+
return (
|
166
|
+
<Modal
|
167
|
+
closable={false}
|
168
|
+
footer={null}
|
169
|
+
maskClosable={false}
|
170
|
+
open={isDownloading && !downloadedInfo}
|
171
|
+
title={t('updater.downloadingUpdate')}
|
172
|
+
>
|
173
|
+
<div style={{ padding: '20px 0' }}>
|
174
|
+
<Progress percent={percent} status="active" />
|
175
|
+
<div style={{ fontSize: 12, marginTop: 8, textAlign: 'center' }}>
|
176
|
+
{t('updater.downloadingUpdateDesc', { percent })}
|
177
|
+
{progress && progress.bytesPerSecond > 0 && (
|
178
|
+
<span> ({(progress.bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s)</span>
|
179
|
+
)}
|
180
|
+
</div>
|
181
|
+
</div>
|
182
|
+
</Modal>
|
183
|
+
);
|
184
|
+
};
|
185
|
+
|
186
|
+
const renderDownloadedModal = () => (
|
187
|
+
<Modal
|
188
|
+
footer={[
|
189
|
+
<Button key="later" onClick={handleInstallLater}>
|
190
|
+
{t('updater.installLater')}
|
191
|
+
</Button>,
|
192
|
+
<Button key="now" onClick={handleInstallNow} type="primary">
|
193
|
+
{t('updater.restartAndInstall')}
|
194
|
+
</Button>,
|
195
|
+
]}
|
196
|
+
onCancel={closeDownloadedModal} // Allow closing if they don't want to decide now
|
197
|
+
open={!!downloadedInfo}
|
198
|
+
title={t('updater.updateReady')}
|
199
|
+
>
|
200
|
+
<h4>{t('updater.updateReadyDesc', { version: downloadedInfo?.version })}</h4>
|
201
|
+
{downloadedInfo?.releaseNotes && (
|
202
|
+
<div
|
203
|
+
dangerouslySetInnerHTML={{ __html: downloadedInfo.releaseNotes as string }}
|
204
|
+
style={{
|
205
|
+
borderRadius: 4,
|
206
|
+
marginTop: 8,
|
207
|
+
maxHeight: 300,
|
208
|
+
overflow: 'auto',
|
209
|
+
padding: '8px 12px',
|
210
|
+
}}
|
211
|
+
/>
|
212
|
+
)}
|
213
|
+
</Modal>
|
214
|
+
);
|
215
|
+
|
216
|
+
// New modal for "latest version"
|
217
|
+
const renderLatestVersionModal = () => (
|
218
|
+
<Modal
|
219
|
+
footer={[
|
220
|
+
<Button key="ok" onClick={closeLatestVersionModal} type="primary">
|
221
|
+
{t('ok', { ns: 'common' })}
|
222
|
+
</Button>,
|
223
|
+
]}
|
224
|
+
onCancel={closeLatestVersionModal}
|
225
|
+
open={!!latestVersionInfo}
|
226
|
+
title={t('updater.isLatestVersion')}
|
227
|
+
>
|
228
|
+
<p>{t('updater.isLatestVersionDesc', { version: latestVersionInfo?.version })}</p>
|
229
|
+
</Modal>
|
230
|
+
);
|
231
|
+
|
232
|
+
return (
|
233
|
+
<>
|
234
|
+
{renderCheckingModal()}
|
235
|
+
{renderAvailableModal()}
|
236
|
+
{renderDownloadingModal()}
|
237
|
+
{renderDownloadedModal()}
|
238
|
+
{renderLatestVersionModal()}
|
239
|
+
{/* Error state is handled by Modal.error currently */}
|
240
|
+
</>
|
241
|
+
);
|
242
|
+
});
|
@@ -0,0 +1,193 @@
|
|
1
|
+
import { DownloadOutlined } from '@ant-design/icons';
|
2
|
+
import { UpdateInfo, useWatchBroadcast } from '@lobechat/electron-client-ipc';
|
3
|
+
import { Icon } from '@lobehub/ui';
|
4
|
+
import { Badge, Button, Popover, Progress, Tooltip, theme } from 'antd';
|
5
|
+
import { createStyles } from 'antd-style';
|
6
|
+
import { Download } from 'lucide-react';
|
7
|
+
import React, { useState } from 'react';
|
8
|
+
import { useTranslation } from 'react-i18next';
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
10
|
+
|
11
|
+
import { autoUpdateService } from '@/services/electron/autoUpdate';
|
12
|
+
|
13
|
+
const useStyles = createStyles(({ css, token }) => ({
|
14
|
+
container: css`
|
15
|
+
cursor: pointer;
|
16
|
+
|
17
|
+
height: 24px;
|
18
|
+
padding-inline: 8px;
|
19
|
+
border: 1px solid ${token.green7A};
|
20
|
+
border-radius: 24px;
|
21
|
+
|
22
|
+
font-size: 12px;
|
23
|
+
line-height: 22px;
|
24
|
+
color: ${token.green11A};
|
25
|
+
|
26
|
+
background: ${token.green2A};
|
27
|
+
`,
|
28
|
+
|
29
|
+
releaseNote: css`
|
30
|
+
overflow: scroll;
|
31
|
+
|
32
|
+
max-height: 300px;
|
33
|
+
padding: 8px;
|
34
|
+
border-radius: 8px;
|
35
|
+
|
36
|
+
background: ${token.colorFillQuaternary};
|
37
|
+
`,
|
38
|
+
}));
|
39
|
+
|
40
|
+
export const UpdateNotification: React.FC = () => {
|
41
|
+
const { t } = useTranslation('electron');
|
42
|
+
const { styles } = useStyles();
|
43
|
+
const { token } = theme.useToken();
|
44
|
+
const [updateAvailable, setUpdateAvailable] = useState(false);
|
45
|
+
const [updateDownloaded, setUpdateDownloaded] = useState(false);
|
46
|
+
const [downloadProgress, setDownloadProgress] = useState(0);
|
47
|
+
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
48
|
+
const [willInstallLater, setWillInstallLater] = useState(false);
|
49
|
+
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
|
50
|
+
|
51
|
+
useWatchBroadcast('updateDownloadProgress', (progress: { percent: number }) => {
|
52
|
+
setDownloadProgress(progress.percent);
|
53
|
+
});
|
54
|
+
|
55
|
+
useWatchBroadcast('updateDownloaded', (info: UpdateInfo) => {
|
56
|
+
setUpdateInfo(info);
|
57
|
+
setUpdateDownloaded(true);
|
58
|
+
setUpdateAvailable(false);
|
59
|
+
});
|
60
|
+
|
61
|
+
useWatchBroadcast('updateWillInstallLater', () => {
|
62
|
+
setWillInstallLater(true);
|
63
|
+
setTimeout(() => setWillInstallLater(false), 5000); // 5秒后自动隐藏提示
|
64
|
+
});
|
65
|
+
|
66
|
+
// 没有更新或正在下载时不显示任何内容
|
67
|
+
if ((!updateAvailable && !updateDownloaded) || (downloadProgress > 0 && downloadProgress < 100)) {
|
68
|
+
return null;
|
69
|
+
}
|
70
|
+
|
71
|
+
// 如果正在下载,显示下载进度
|
72
|
+
if (downloadProgress > 0 && downloadProgress < 100) {
|
73
|
+
return (
|
74
|
+
<div
|
75
|
+
style={{
|
76
|
+
position: 'fixed',
|
77
|
+
right: 12,
|
78
|
+
top: 12,
|
79
|
+
zIndex: 1000,
|
80
|
+
}}
|
81
|
+
>
|
82
|
+
<Tooltip title={t('updater.downloadingUpdateDesc', '正在下载更新...')}>
|
83
|
+
<Badge
|
84
|
+
count={<DownloadOutlined style={{ color: token.colorPrimary }} />}
|
85
|
+
offset={[-4, 4]}
|
86
|
+
>
|
87
|
+
<div
|
88
|
+
style={{
|
89
|
+
alignItems: 'center',
|
90
|
+
background: token.colorBgElevated,
|
91
|
+
borderRadius: '50%',
|
92
|
+
boxShadow: token.boxShadow,
|
93
|
+
display: 'flex',
|
94
|
+
height: 32,
|
95
|
+
justifyContent: 'center',
|
96
|
+
position: 'relative',
|
97
|
+
width: 32,
|
98
|
+
}}
|
99
|
+
>
|
100
|
+
<Progress
|
101
|
+
percent={Math.round(downloadProgress)}
|
102
|
+
showInfo={false}
|
103
|
+
strokeWidth={12}
|
104
|
+
type="circle"
|
105
|
+
width={30}
|
106
|
+
/>
|
107
|
+
<span
|
108
|
+
style={{
|
109
|
+
fontSize: 10,
|
110
|
+
fontWeight: 'bold',
|
111
|
+
position: 'absolute',
|
112
|
+
}}
|
113
|
+
>
|
114
|
+
{Math.round(downloadProgress)}%
|
115
|
+
</span>
|
116
|
+
</div>
|
117
|
+
</Badge>
|
118
|
+
</Tooltip>
|
119
|
+
</div>
|
120
|
+
);
|
121
|
+
}
|
122
|
+
|
123
|
+
return (
|
124
|
+
<Flexbox>
|
125
|
+
<Popover
|
126
|
+
arrow={false}
|
127
|
+
content={
|
128
|
+
<Flexbox gap={8} style={{ maxWidth: 380 }}>
|
129
|
+
<div>
|
130
|
+
<h3 style={{ margin: 0 }}>{t('updater.updateReady')}</h3>
|
131
|
+
<div style={{ color: token.colorTextSecondary, fontSize: 12 }}>
|
132
|
+
{updateInfo?.version}
|
133
|
+
</div>
|
134
|
+
</div>
|
135
|
+
|
136
|
+
{updateInfo?.releaseNotes && (
|
137
|
+
<div
|
138
|
+
className={styles.releaseNote}
|
139
|
+
dangerouslySetInnerHTML={{ __html: updateInfo.releaseNotes }}
|
140
|
+
style={{ maxHeight: 300, overflow: 'scroll' }}
|
141
|
+
/>
|
142
|
+
)}
|
143
|
+
|
144
|
+
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
145
|
+
<Button
|
146
|
+
onClick={() => {
|
147
|
+
autoUpdateService.installNow();
|
148
|
+
}}
|
149
|
+
size="small"
|
150
|
+
type="primary"
|
151
|
+
>
|
152
|
+
{t('updater.upgradeNow')}
|
153
|
+
</Button>
|
154
|
+
</div>
|
155
|
+
</Flexbox>
|
156
|
+
}
|
157
|
+
onOpenChange={setIsPopoverVisible}
|
158
|
+
open={isPopoverVisible}
|
159
|
+
placement="bottomRight"
|
160
|
+
title={null}
|
161
|
+
trigger="hover"
|
162
|
+
>
|
163
|
+
<Flexbox
|
164
|
+
align={'center'}
|
165
|
+
className={styles.container}
|
166
|
+
gap={4}
|
167
|
+
horizontal
|
168
|
+
onClick={() => setIsPopoverVisible(true)}
|
169
|
+
>
|
170
|
+
<Icon icon={Download} style={{ fontSize: 14 }} /> 已有可用更新
|
171
|
+
</Flexbox>
|
172
|
+
</Popover>
|
173
|
+
{/* 下次启动时更新提示 */}
|
174
|
+
{willInstallLater && (
|
175
|
+
<div
|
176
|
+
style={{
|
177
|
+
backgroundColor: token.colorBgElevated,
|
178
|
+
borderRadius: token.borderRadius,
|
179
|
+
bottom: 20,
|
180
|
+
boxShadow: token.boxShadow,
|
181
|
+
color: token.colorText,
|
182
|
+
padding: '10px 16px',
|
183
|
+
position: 'fixed',
|
184
|
+
right: 20,
|
185
|
+
zIndex: 1000,
|
186
|
+
}}
|
187
|
+
>
|
188
|
+
{t('updater.willInstallLater', '更新将在下次启动时安装')}
|
189
|
+
</div>
|
190
|
+
)}
|
191
|
+
</Flexbox>
|
192
|
+
);
|
193
|
+
};
|