@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.
Files changed (102) hide show
  1. package/.cursor/rules/desktop-local-tools-implement.mdc +80 -0
  2. package/.env.desktop +2 -1
  3. package/.github/scripts/pr-comment.js +4 -9
  4. package/CHANGELOG.md +51 -0
  5. package/changelog/v1.json +18 -0
  6. package/locales/ar/electron.json +38 -2
  7. package/locales/ar/plugin.json +51 -31
  8. package/locales/bg-BG/electron.json +38 -2
  9. package/locales/bg-BG/plugin.json +51 -31
  10. package/locales/de-DE/electron.json +38 -2
  11. package/locales/de-DE/plugin.json +29 -9
  12. package/locales/en-US/electron.json +38 -2
  13. package/locales/en-US/plugin.json +29 -9
  14. package/locales/es-ES/electron.json +38 -2
  15. package/locales/es-ES/plugin.json +51 -31
  16. package/locales/fa-IR/electron.json +38 -2
  17. package/locales/fa-IR/plugin.json +51 -31
  18. package/locales/fr-FR/electron.json +38 -2
  19. package/locales/fr-FR/plugin.json +51 -31
  20. package/locales/it-IT/electron.json +38 -2
  21. package/locales/it-IT/plugin.json +51 -31
  22. package/locales/ja-JP/electron.json +38 -2
  23. package/locales/ja-JP/plugin.json +51 -31
  24. package/locales/ko-KR/electron.json +38 -2
  25. package/locales/ko-KR/plugin.json +29 -9
  26. package/locales/nl-NL/electron.json +38 -2
  27. package/locales/nl-NL/plugin.json +51 -31
  28. package/locales/pl-PL/electron.json +38 -2
  29. package/locales/pl-PL/plugin.json +29 -9
  30. package/locales/pt-BR/electron.json +38 -2
  31. package/locales/pt-BR/plugin.json +51 -31
  32. package/locales/ru-RU/electron.json +38 -2
  33. package/locales/ru-RU/plugin.json +51 -31
  34. package/locales/tr-TR/electron.json +38 -2
  35. package/locales/tr-TR/plugin.json +51 -31
  36. package/locales/vi-VN/electron.json +38 -2
  37. package/locales/vi-VN/plugin.json +29 -9
  38. package/locales/zh-CN/electron.json +38 -2
  39. package/locales/zh-CN/plugin.json +30 -10
  40. package/locales/zh-TW/electron.json +38 -2
  41. package/locales/zh-TW/plugin.json +51 -31
  42. package/package.json +1 -1
  43. package/packages/electron-client-ipc/src/events/update.ts +3 -3
  44. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx +222 -0
  45. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Option.tsx +104 -0
  46. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx +42 -0
  47. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Waiting.tsx +203 -0
  48. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/index.tsx +57 -0
  49. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateModal.tsx +242 -0
  50. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateNotification.tsx +193 -0
  51. package/src/app/[variants]/(main)/_layout/Desktop/{Titlebar.tsx → ElectronTitlebar/index.tsx} +15 -1
  52. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/BottomActions.tsx +3 -2
  53. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
  54. package/src/app/[variants]/layout.tsx +2 -1
  55. package/src/config/aiModels/openrouter.ts +6 -6
  56. package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/LocalFile.tsx +65 -0
  57. package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +29 -0
  58. package/src/features/Conversation/components/MarkdownElements/LocalFile/index.ts +16 -0
  59. package/src/features/Conversation/components/MarkdownElements/index.ts +7 -1
  60. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +260 -0
  61. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +204 -0
  62. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +133 -0
  63. package/src/features/Conversation/components/MarkdownElements/type.ts +5 -1
  64. package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +20 -0
  65. package/src/features/PluginDevModal/MCPManifestForm/MCPTypeSelect.tsx +176 -0
  66. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +289 -0
  67. package/src/features/PluginDevModal/MCPManifestForm/utils.test.ts +262 -0
  68. package/src/features/PluginDevModal/MCPManifestForm/utils.ts +151 -0
  69. package/src/features/PluginDevModal/index.tsx +31 -22
  70. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -1
  71. package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +0 -56
  72. package/src/locales/default/electron.ts +38 -2
  73. package/src/locales/default/plugin.ts +28 -8
  74. package/src/server/modules/ElectronIPCClient/index.ts +36 -0
  75. package/src/server/routers/lambda/session.ts +2 -6
  76. package/src/server/routers/tools/mcp.ts +6 -0
  77. package/src/server/services/file/impls/index.ts +9 -1
  78. package/src/server/services/file/impls/local.test.ts +299 -0
  79. package/src/server/services/file/impls/local.ts +183 -0
  80. package/src/server/services/mcp/index.ts +26 -0
  81. package/src/services/aiModel/index.ts +5 -1
  82. package/src/services/aiProvider/index.ts +5 -1
  83. package/src/services/electron/autoUpdate.ts +4 -0
  84. package/src/services/file/index.ts +5 -1
  85. package/src/services/mcp.ts +13 -2
  86. package/src/services/message/index.ts +5 -1
  87. package/src/services/plugin/index.ts +5 -1
  88. package/src/services/session/index.ts +5 -1
  89. package/src/services/tableViewer/desktop.ts +15 -0
  90. package/src/services/tableViewer/index.ts +4 -1
  91. package/src/services/thread/index.ts +5 -1
  92. package/src/services/topic/index.ts +5 -1
  93. package/src/services/user/index.ts +5 -1
  94. package/src/store/electron/actions/app.ts +59 -0
  95. package/src/store/electron/actions/sync.ts +5 -1
  96. package/src/store/electron/initialState.ts +3 -1
  97. package/src/store/electron/store.ts +6 -1
  98. package/src/store/tool/slices/customPlugin/action.ts +16 -4
  99. package/src/utils/client/GlobalAgentContextManager.ts +85 -0
  100. package/src/utils/promptTemplate.test.ts +78 -0
  101. package/src/utils/promptTemplate.ts +17 -0
  102. package/src/features/PluginDevModal/MCPManifestForm.tsx +0 -164
@@ -7,6 +7,7 @@
7
7
  "payload": "插件载荷",
8
8
  "pluginState": "插件 State",
9
9
  "response": "返回结果",
10
+ "title": "插件详情",
10
11
  "tool_call": "工具调用请求"
11
12
  },
12
13
  "detailModal": {
@@ -47,15 +48,16 @@
47
48
  },
48
49
  "mcp": {
49
50
  "args": {
50
- "desc": "传递给 STDIO 命令的参数列表",
51
+ "desc": "传递给 STDIO 命令的参数列表,一般在这里输入 MCP 服务器名称",
51
52
  "label": "命令参数",
52
- "placeholder": "例如:--port 8080 --debug",
53
- "tooltip": "输入参数后按回车或使用逗号/空格分隔"
53
+ "placeholder": "例如:mcp-hello-world",
54
+ "required": "请输入启动参数"
54
55
  },
55
56
  "command": {
56
- "desc": "用于启动 MCP STDIO 插件的可执行文件或脚本",
57
+ "desc": "用于启动 MCP STDIO Server 的可执行文件或脚本",
57
58
  "label": "命令",
58
- "placeholder": "例如:python main.py /path/to/executable"
59
+ "placeholder": "例如:npx / uv / docker 等",
60
+ "required": "请输入启动命令"
59
61
  },
60
62
  "endpoint": {
61
63
  "desc": "输入你的 MCP Streamable HTTP Server 的地址",
@@ -63,17 +65,31 @@
63
65
  },
64
66
  "identifier": {
65
67
  "desc": "为你的 MCP 插件指定一个名称,需要使用英文字符",
66
- "invalid": "只能输入英文字符、数字 、- 和_ 这两个符号",
68
+ "invalid": "标识符只能包含字母、数字、连字符和下划线",
67
69
  "label": "MCP 插件名称",
68
- "placeholder": "例如:my-mcp-plugin"
70
+ "placeholder": "例如:my-mcp-plugin",
71
+ "required": "请输入 MCP 服务标识符"
69
72
  },
73
+ "previewManifest": "预览插件描述文件",
74
+ "testConnection": "测试连接",
75
+ "testConnectionTip": "测试连接成功后 MCP 插件才可以被正常使用",
70
76
  "type": {
71
77
  "desc": "选择 MCP 插件的通信方式,网页版只支持 Streamable HTTP",
72
- "label": "MCP 插件类型"
78
+ "httpFeature1": "兼容网页版与桌面端",
79
+ "httpFeature2": "连接远程 MCP 服务器, 无需额外安装配置",
80
+ "httpShortDesc": "基于流式 HTTP 的通信协议",
81
+ "label": "MCP 插件类型",
82
+ "stdioFeature1": "更低的通信延迟, 适合本地执行",
83
+ "stdioFeature2": "需在本地安装运行 MCP 服务器",
84
+ "stdioNotAvailable": "STDIO 模式仅在桌面版可用",
85
+ "stdioShortDesc": "基于标准输入输出的通信协议",
86
+ "title": "MCP 插件类型"
73
87
  },
74
88
  "url": {
75
- "desc": "输入你的 MCP HTTP 插件的 Endpoint 地址",
76
- "label": "HTTP Endpoint URL"
89
+ "desc": "输入你的 MCP Server Streamable HTTP 地址,不会以 /sse 结尾",
90
+ "invalid": "请输入有效的 URL 地址",
91
+ "label": "Streamable HTTP Endpoint URL",
92
+ "required": "请输入 MCP 服务 URL"
77
93
  }
78
94
  },
79
95
  "meta": {
@@ -101,12 +117,14 @@
101
117
  "label": "标识符",
102
118
  "pattenErrorMessage": "只能输入英文字符、数字 、- 和_ 这两个符号"
103
119
  },
120
+ "lobe": "{{appName}} 插件",
104
121
  "manifest": {
105
122
  "desc": "{{appName}}将会通过该链接安装插件",
106
123
  "label": "插件描述文件 (Manifest) URL",
107
124
  "preview": "预览 Manifest",
108
125
  "refresh": "刷新"
109
126
  },
127
+ "openai": "OpenAI 插件",
110
128
  "title": {
111
129
  "desc": "插件标题",
112
130
  "label": "标题",
@@ -148,6 +166,7 @@
148
166
  "noManifest": "描述文件不存在",
149
167
  "openAPIInvalid": "OpenAPI 解析失败,错误: \n\n {{error}}",
150
168
  "reinstallError": "插件 {{name}} 刷新失败",
169
+ "testConnectionFailed": "获取 Manifest 失败: {{error}}",
151
170
  "urlError": "该链接没有返回 JSON 格式的内容, 请确保是有效的链接"
152
171
  },
153
172
  "inspector": {
@@ -226,5 +245,6 @@
226
245
  },
227
246
  "title": "插件商店"
228
247
  },
248
+ "unknownError": "未知错误",
229
249
  "unknownPlugin": "未知插件"
230
250
  }
@@ -17,16 +17,52 @@
17
17
  "statusDisconnected": "未連接",
18
18
  "urlRequired": "請輸入伺服器地址"
19
19
  },
20
+ "sync": {
21
+ "continue": "繼續",
22
+ "inCloud": "當前使用雲端同步",
23
+ "inLocalStorage": "當前使用本地儲存",
24
+ "isIniting": "正在初始化...",
25
+ "lobehubCloud": {
26
+ "description": "官方提供的雲版本",
27
+ "title": "LobeHub Cloud"
28
+ },
29
+ "local": {
30
+ "description": "使用本地資料庫,完全離線可用",
31
+ "title": "本地資料庫"
32
+ },
33
+ "mode": {
34
+ "cloudSync": "雲端同步",
35
+ "localStorage": "本地儲存",
36
+ "title": "選擇你的連接模式",
37
+ "useSelfHosted": "使用自托管實例?"
38
+ },
39
+ "selfHosted": {
40
+ "description": "自行部署的社區版本",
41
+ "title": "自托管實例"
42
+ }
43
+ },
20
44
  "updater": {
45
+ "checkingUpdate": "檢查新版本",
46
+ "checkingUpdateDesc": "正在獲取版本資訊...",
47
+ "downloadNewVersion": "下載新版本",
21
48
  "downloadingUpdate": "正在下載更新",
22
49
  "downloadingUpdateDesc": "更新正在下載中,請稍候...",
50
+ "installLater": "下次啟動時更新",
51
+ "isLatestVersion": "當前已是最新版本",
52
+ "isLatestVersionDesc": "非常棒,使用的版本 {{version}} 已是最前沿的版本。",
23
53
  "later": "稍後更新",
24
54
  "newVersionAvailable": "新版本可用",
25
55
  "newVersionAvailableDesc": "發現新版本 {{version}},是否立即下載?",
26
- "restartAndInstall": "重啟並安裝",
56
+ "restartAndInstall": "安裝更新並重啟",
27
57
  "updateError": "更新錯誤",
28
58
  "updateReady": "更新已就緒",
29
- "updateReadyDesc": "Lobe Chat {{version}} 已下載完成,重啟應用後即可完成安裝。",
59
+ "updateReadyDesc": "新版本 {{version}} 已下載完成,重啟應用後即可完成安裝。",
30
60
  "upgradeNow": "立即更新"
61
+ },
62
+ "waitingOAuth": {
63
+ "cancel": "取消",
64
+ "description": "瀏覽器已打開授權頁面,請在瀏覽器中完成授權",
65
+ "helpText": "如果瀏覽器沒有自動打開,請點擊取消後重新嘗試",
66
+ "title": "等待授權連接"
31
67
  }
32
68
  }
@@ -7,6 +7,7 @@
7
7
  "payload": "插件載荷",
8
8
  "pluginState": "插件狀態",
9
9
  "response": "回應",
10
+ "title": "插件詳情",
10
11
  "tool_call": "工具呼叫"
11
12
  },
12
13
  "detailModal": {
@@ -34,37 +35,6 @@
34
35
  "desc": "外掛的唯一識別碼",
35
36
  "label": "識別碼"
36
37
  },
37
- "mcp": {
38
- "args": {
39
- "desc": "傳遞給 STDIO 命令的參數列表",
40
- "label": "命令參數",
41
- "placeholder": "例如:--port 8080 --debug",
42
- "tooltip": "輸入參數後按回車或使用逗號/空格分隔"
43
- },
44
- "command": {
45
- "desc": "用於啟動 MCP STDIO 插件的可執行文件或腳本",
46
- "label": "命令",
47
- "placeholder": "例如:python main.py 或 /path/to/executable"
48
- },
49
- "endpoint": {
50
- "desc": "輸入你的 MCP Streamable HTTP Server 的地址",
51
- "label": "MCP Endpoint URL"
52
- },
53
- "identifier": {
54
- "desc": "為你的 MCP 插件指定一個名稱,需要使用英文字符",
55
- "invalid": "只能輸入英文字符、數字、- 和_ 這兩個符號",
56
- "label": "MCP 插件名稱",
57
- "placeholder": "例如:my-mcp-plugin"
58
- },
59
- "type": {
60
- "desc": "選擇 MCP 插件的通信方式,網頁版只支持 Streamable HTTP",
61
- "label": "MCP 插件類型"
62
- },
63
- "url": {
64
- "desc": "輸入你的 MCP HTTP 插件的 Endpoint 地址",
65
- "label": "HTTP Endpoint URL"
66
- }
67
- },
68
38
  "mode": {
69
39
  "mcp": "MCP 插件",
70
40
  "mcpExp": "實驗性",
@@ -76,6 +46,52 @@
76
46
  "placeholder": "搜尋引擎"
77
47
  }
78
48
  },
49
+ "mcp": {
50
+ "args": {
51
+ "desc": "傳遞給 STDIO 命令的參數列表",
52
+ "label": "命令參數",
53
+ "placeholder": "例如:--port 8080 --debug",
54
+ "required": "請輸入啟動參數"
55
+ },
56
+ "command": {
57
+ "desc": "用於啟動 MCP STDIO 插件的可執行文件或腳本",
58
+ "label": "命令",
59
+ "placeholder": "例如:python main.py 或 /path/to/executable",
60
+ "required": "請輸入啟動命令"
61
+ },
62
+ "endpoint": {
63
+ "desc": "輸入你的 MCP Streamable HTTP Server 的地址",
64
+ "label": "MCP Endpoint URL"
65
+ },
66
+ "identifier": {
67
+ "desc": "為你的 MCP 插件指定一個名稱,需要使用英文字符",
68
+ "invalid": "只能輸入英文字符、數字、- 和_ 這兩個符號",
69
+ "label": "MCP 插件名稱",
70
+ "placeholder": "例如:my-mcp-plugin",
71
+ "required": "請輸入 MCP 服務標識符"
72
+ },
73
+ "previewManifest": "預覽插件描述文件",
74
+ "testConnection": "測試連接",
75
+ "testConnectionTip": "測試連接成功後 MCP 插件才可以被正常使用",
76
+ "type": {
77
+ "desc": "選擇 MCP 插件的通信方式,網頁版只支持 Streamable HTTP",
78
+ "httpFeature1": "兼容網頁版與桌面端",
79
+ "httpFeature2": "連接遠端 MCP 伺服器, 無需額外安裝配置",
80
+ "httpShortDesc": "基於串流 HTTP 的通訊協議",
81
+ "label": "MCP 插件類型",
82
+ "stdioFeature1": "更低的通訊延遲, 適合本地執行",
83
+ "stdioFeature2": "需在本地安裝運行 MCP 伺服器",
84
+ "stdioNotAvailable": "STDIO 模式僅在桌面版可用",
85
+ "stdioShortDesc": "基於標準輸入輸出的通訊協議",
86
+ "title": "MCP 插件類型"
87
+ },
88
+ "url": {
89
+ "desc": "輸入你的 MCP Server Streamable HTTP 地址,結尾不會有 /sse",
90
+ "invalid": "請輸入有效的 URL 地址",
91
+ "label": "HTTP Endpoint URL",
92
+ "required": "請輸入 MCP 服務 URL"
93
+ }
94
+ },
79
95
  "meta": {
80
96
  "author": {
81
97
  "desc": "外掛的作者",
@@ -101,12 +117,14 @@
101
117
  "label": "識別碼",
102
118
  "pattenErrorMessage": "只允許英數字元、連字號 -,以及底線 _"
103
119
  },
120
+ "lobe": "{{appName}} 插件",
104
121
  "manifest": {
105
122
  "desc": "{{appName}}將會透過該連結安裝插件",
106
123
  "label": "插件描述檔 (Manifest) URL",
107
124
  "preview": "預覽 Manifest",
108
125
  "refresh": "重新載入"
109
126
  },
127
+ "openai": "OpenAI 插件",
110
128
  "title": {
111
129
  "desc": "外掛的名稱",
112
130
  "label": "名稱",
@@ -148,6 +166,7 @@
148
166
  "noManifest": "描述檔不存在",
149
167
  "openAPIInvalid": "OpenAPI 解析失敗,錯誤: \n\n {{error}}",
150
168
  "reinstallError": "插件 {{name}} 刷新失敗",
169
+ "testConnectionFailed": "獲取 Manifest 失敗: {{error}}",
151
170
  "urlError": "該連結沒有返回 JSON 格式的內容, 請確保是有效的連結"
152
171
  },
153
172
  "inspector": {
@@ -226,5 +245,6 @@
226
245
  },
227
246
  "title": "插件商店"
228
247
  },
248
+ "unknownError": "未知錯誤",
229
249
  "unknownPlugin": "未知插件"
230
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.82.0",
3
+ "version": "1.82.2",
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",
@@ -9,12 +9,12 @@ export interface AutoUpdateDispatchEvents {
9
9
  }
10
10
 
11
11
  export interface AutoUpdateBroadcastEvents {
12
- updateAvailable: (info: UpdateInfo) => void;
13
- updateCheckStart: () => void;
12
+ manualUpdateAvailable: (info: UpdateInfo) => void;
13
+ manualUpdateCheckStart: () => void;
14
+ manualUpdateNotAvailable: (info: UpdateInfo) => void;
14
15
  updateDownloadProgress: (progress: ProgressInfo) => void;
15
16
  updateDownloadStart: () => void;
16
17
  updateDownloaded: (info: UpdateInfo) => void;
17
18
  updateError: (message: string) => void;
18
- updateNotAvailable: (info: UpdateInfo) => void;
19
19
  updateWillInstallLater: () => void;
20
20
  }
@@ -0,0 +1,222 @@
1
+ import { Input } from '@lobehub/ui';
2
+ import { LobeHub } from '@lobehub/ui/brand';
3
+ import { Button } from 'antd';
4
+ import { createStyles } from 'antd-style';
5
+ import { ComputerIcon, Server } from 'lucide-react';
6
+ import { memo, useCallback, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Center, Flexbox } from 'react-layout-kit';
9
+
10
+ import { useElectronStore } from '@/store/electron';
11
+
12
+ import { AccessOption, Option } from './Option';
13
+
14
+ const useStyles = createStyles(({ token, css }) => {
15
+ return {
16
+ cardGroup: css`
17
+ width: 400px; /* Increased width */
18
+ `,
19
+ container: css`
20
+ overflow-y: auto;
21
+
22
+ width: 100%;
23
+ height: 100%;
24
+ padding-block: 0 40px;
25
+ padding-inline: 24px; /* Increased top padding */
26
+ `,
27
+ continueButton: css`
28
+ width: 100%;
29
+ margin-block-start: 40px;
30
+ `,
31
+ groupTitle: css`
32
+ padding-inline-start: 4px; /* Align with card padding */
33
+ font-size: 16px;
34
+ font-weight: 500;
35
+ color: ${token.colorTextSecondary};
36
+ `,
37
+ header: css`
38
+ text-align: center;
39
+ `,
40
+ inputError: css`
41
+ margin-block-start: 8px;
42
+ font-size: 12px;
43
+ color: ${token.colorError};
44
+ `,
45
+ modal: css`
46
+ .ant-drawer-close {
47
+ position: absolute;
48
+ inset-block-start: 8px;
49
+ inset-inline-end: 0;
50
+ }
51
+ `,
52
+ selfHostedInput: css`
53
+ margin-block-start: 12px;
54
+ `,
55
+ selfHostedText: css`
56
+ cursor: pointer;
57
+ font-size: 14px;
58
+ color: ${token.colorTextTertiary};
59
+
60
+ :hover {
61
+ color: ${token.colorTextSecondary};
62
+ }
63
+ `,
64
+ title: css`
65
+ margin-block: 16px 48px; /* Increased Spacing below title */
66
+ font-size: 24px; /* Increased font size */
67
+ font-weight: 600;
68
+ color: ${token.colorTextHeading};
69
+ `,
70
+ };
71
+ });
72
+
73
+ interface ConnectionModeProps {
74
+ setIsOpen: (open: boolean) => void;
75
+ setWaiting: (waiting: boolean) => void;
76
+ }
77
+
78
+ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) => {
79
+ const { styles } = useStyles();
80
+ const { t } = useTranslation(['electron', 'common']);
81
+ const [selectedOption, setSelectedOption] = useState<AccessOption>();
82
+ const [selfHostedUrl, setSelfHostedUrl] = useState('');
83
+ const [urlError, setUrlError] = useState<string | undefined>();
84
+
85
+ const connect = useElectronStore((s) => s.connectRemoteServer);
86
+ const disconnect = useElectronStore((s) => s.disconnectRemoteServer);
87
+
88
+ const validateUrl = useCallback((url: string) => {
89
+ if (!url) {
90
+ return t('remoteServer.urlRequired');
91
+ }
92
+ try {
93
+ new URL(url);
94
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
95
+ throw new Error('Invalid protocol');
96
+ }
97
+ return undefined;
98
+ } catch {
99
+ return t('remoteServer.invalidUrl');
100
+ }
101
+ }, []);
102
+
103
+ const handleSelectOption = (option: AccessOption) => {
104
+ setSelectedOption(option);
105
+ if (option !== 'self-hosted') {
106
+ setUrlError(undefined);
107
+ } else {
108
+ setUrlError(validateUrl(selfHostedUrl));
109
+ }
110
+ };
111
+
112
+ const handleContinue = async () => {
113
+ if (selectedOption === 'self-hosted') {
114
+ const error = validateUrl(selfHostedUrl);
115
+ setUrlError(error);
116
+ if (error) {
117
+ return;
118
+ }
119
+ }
120
+
121
+ if (selectedOption === 'local') {
122
+ await disconnect();
123
+ setIsOpen(false);
124
+ return;
125
+ }
126
+
127
+ // try to connect
128
+ setWaiting(true);
129
+ await connect(
130
+ selectedOption === 'self-hosted'
131
+ ? { isSelfHosted: true, serverUrl: selfHostedUrl }
132
+ : { isSelfHosted: false },
133
+ );
134
+ };
135
+
136
+ return (
137
+ <Center className={styles.container}>
138
+ <Flexbox align={'center'} gap={0}>
139
+ <h1 className={styles.title}>{t('sync.mode.title')}</h1>
140
+ </Flexbox>
141
+
142
+ <Flexbox className={styles.cardGroup} gap={24}>
143
+ <Flexbox gap={16}>
144
+ <Flexbox align="center" horizontal justify="space-between">
145
+ <div className={styles.groupTitle}>{t('sync.mode.cloudSync')}</div>
146
+ <div
147
+ className={styles.selfHostedText}
148
+ onClick={() => handleSelectOption('self-hosted')}
149
+ >
150
+ {t('sync.mode.useSelfHosted')}
151
+ </div>
152
+ </Flexbox>
153
+ <Option
154
+ description={t('sync.lobehubCloud.description')}
155
+ icon={LobeHub}
156
+ isSelected={selectedOption === 'cloud'}
157
+ label={t('sync.lobehubCloud.title')}
158
+ onClick={handleSelectOption}
159
+ value="cloud"
160
+ />
161
+ {selectedOption === 'self-hosted' && (
162
+ <Option
163
+ description={t('sync.selfHosted.description')}
164
+ icon={Server}
165
+ isSelected={selectedOption === 'self-hosted'}
166
+ label={t('sync.selfHosted.title')}
167
+ onClick={handleSelectOption}
168
+ value="self-hosted"
169
+ >
170
+ {selectedOption === 'self-hosted' && (
171
+ <>
172
+ <Input
173
+ autoFocus
174
+ className={styles.selfHostedInput}
175
+ onChange={(e) => {
176
+ const newUrl = e.target.value;
177
+ setSelfHostedUrl(newUrl);
178
+ setUrlError(validateUrl(newUrl));
179
+ }}
180
+ onClick={(e) => e.stopPropagation()}
181
+ placeholder="https://your-lobechat.com"
182
+ status={urlError ? 'error' : undefined}
183
+ value={selfHostedUrl}
184
+ />
185
+ {urlError && <div className={styles.inputError}>{urlError}</div>}
186
+ </>
187
+ )}
188
+ </Option>
189
+ )}
190
+ </Flexbox>
191
+ <Flexbox>
192
+ <div className={styles.groupTitle} style={{ marginBottom: 12 }}>
193
+ {t('sync.mode.localStorage')}
194
+ </div>
195
+ <Option
196
+ description={t('sync.local.description')}
197
+ icon={ComputerIcon}
198
+ isSelected={selectedOption === 'local'}
199
+ label={t('sync.local.title')}
200
+ onClick={handleSelectOption}
201
+ value="local"
202
+ />
203
+ </Flexbox>
204
+ </Flexbox>
205
+
206
+ <Button
207
+ className={styles.continueButton}
208
+ disabled={
209
+ !selectedOption || (selectedOption === 'self-hosted' && (!!urlError || !selfHostedUrl))
210
+ }
211
+ onClick={handleContinue}
212
+ size="large"
213
+ style={{ maxWidth: 400 }}
214
+ type="primary"
215
+ >
216
+ {selectedOption === 'local' ? t('save', { ns: 'common' }) : t('sync.continue')}
217
+ </Button>
218
+ </Center>
219
+ );
220
+ });
221
+
222
+ export default ConnectionMode;
@@ -0,0 +1,104 @@
1
+ import { CheckCircleFilled } from '@ant-design/icons';
2
+ import { createStyles } from 'antd-style';
3
+ import { ComponentType, ReactNode } from 'react';
4
+ import { Center, Flexbox } from 'react-layout-kit';
5
+
6
+ const useStyles = createStyles(({ token, css }) => ({
7
+ checked: css`
8
+ position: relative;
9
+ border: 1px solid ${token.colorPrimary};
10
+ `,
11
+ description: css`
12
+ margin-block-start: 4px; /* Adjust spacing */
13
+ font-size: 13px; /* Slightly larger description */
14
+ color: ${token.colorTextSecondary};
15
+ `,
16
+ iconWrapper: css`
17
+ margin-block-start: 2px;
18
+ padding: 0;
19
+ color: ${token.colorTextSecondary};
20
+
21
+ svg {
22
+ display: block;
23
+ font-size: 24px; /* Increased icon size */
24
+ stroke-width: 2; /* Ensure lucide icons look bolder */
25
+ }
26
+ `,
27
+ label: css`
28
+ font-size: 16px;
29
+ font-weight: 600; /* Bolder label */
30
+ color: ${token.colorText};
31
+ `,
32
+ optionCard: css`
33
+ cursor: pointer;
34
+
35
+ width: 100%;
36
+ padding: 16px;
37
+ border: 1px solid ${token.colorBorderSecondary}; /* Use secondary border */
38
+ border-radius: ${token.borderRadiusLG}px;
39
+
40
+ color: ${token.colorText};
41
+
42
+ background-color: ${token.colorBgContainer};
43
+
44
+ transition: all 0.2s ${token.motionEaseInOut};
45
+
46
+ :hover {
47
+ border-color: ${token.colorPrimary};
48
+ }
49
+ `,
50
+ optionInner: css`
51
+ display: flex;
52
+ gap: 16px;
53
+ align-items: flex-start;
54
+ justify-content: space-between;
55
+ `,
56
+ }));
57
+
58
+ // 定义选项类型
59
+ export type AccessOption = 'cloud' | 'self-hosted' | 'local';
60
+
61
+ export interface OptionProps {
62
+ children?: ReactNode;
63
+ description: string;
64
+ icon: ComponentType<any>;
65
+ isSelected: boolean;
66
+ label: string;
67
+ onClick: (value: AccessOption) => void;
68
+ value: AccessOption; // For self-hosted input
69
+ }
70
+
71
+ export const Option = ({
72
+ description,
73
+ icon: PrefixIcon,
74
+ label,
75
+ value,
76
+ isSelected,
77
+ onClick,
78
+ children,
79
+ }: OptionProps) => {
80
+ const { styles, cx } = useStyles();
81
+
82
+ return (
83
+ <Flexbox
84
+ className={cx(styles.optionCard, isSelected && styles.checked)}
85
+ direction="vertical"
86
+ key={value}
87
+ onClick={() => onClick(value)}
88
+ >
89
+ <div className={styles.optionInner}>
90
+ <Flexbox gap={16} horizontal>
91
+ <Center className={styles.iconWrapper}>
92
+ <PrefixIcon />
93
+ </Center>
94
+ <Flexbox gap={8}>
95
+ <div className={styles.label}>{label}</div>
96
+ <div className={styles.description}>{description}</div>
97
+ </Flexbox>
98
+ </Flexbox>
99
+ {isSelected && <CheckCircleFilled style={{ fontSize: 16 }} />}
100
+ </div>
101
+ {children}
102
+ </Flexbox>
103
+ );
104
+ };
@@ -0,0 +1,42 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Loader, Wifi, WifiOffIcon } from 'lucide-react';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { useElectronStore } from '@/store/electron';
7
+ import { electronSyncSelectors } from '@/store/electron/selectors';
8
+
9
+ interface SyncProps {
10
+ onClick: () => void;
11
+ }
12
+ const Sync = memo<SyncProps>(({ onClick }) => {
13
+ const { t } = useTranslation('electron');
14
+
15
+ const [isIniting, isSyncActive, useRemoteServerConfig] = useElectronStore((s) => [
16
+ !s.isInitRemoteServerConfig,
17
+ electronSyncSelectors.isSyncActive(s),
18
+ s.useRemoteServerConfig,
19
+ ]);
20
+
21
+ // 使用useSWR获取远程服务器配置
22
+ useRemoteServerConfig();
23
+
24
+ return (
25
+ <ActionIcon
26
+ icon={isIniting ? Loader : isSyncActive ? Wifi : WifiOffIcon}
27
+ loading={isIniting}
28
+ onClick={onClick}
29
+ placement={'bottomRight'}
30
+ size="small"
31
+ title={
32
+ isIniting
33
+ ? t('sync.isIniting')
34
+ : isSyncActive
35
+ ? t('sync.inCloud')
36
+ : t('sync.inLocalStorage')
37
+ }
38
+ />
39
+ );
40
+ });
41
+
42
+ export default Sync;