@lobehub/chat 1.82.10 → 1.83.1
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/.env.desktop +1 -2
- package/.github/workflows/{release-desktop.yml → desktop-pr-build.yml} +59 -137
- package/.github/workflows/release-desktop-beta.yml +194 -0
- package/CHANGELOG.md +42 -0
- package/apps/desktop/.i18nrc.js +31 -0
- package/apps/desktop/Development.md +47 -0
- package/apps/desktop/README.md +6 -0
- package/apps/desktop/build/Icon-beta.icns +0 -0
- package/apps/desktop/build/Icon-nightly.icns +0 -0
- package/apps/desktop/build/Icon.icns +0 -0
- package/apps/desktop/build/entitlements.mac.plist +12 -0
- package/apps/desktop/build/favicon.ico +0 -0
- package/apps/desktop/build/icon-beta.png +0 -0
- package/apps/desktop/build/icon-dev.png +0 -0
- package/apps/desktop/build/icon-nightly.ico +0 -0
- package/apps/desktop/build/icon-nightly.png +0 -0
- package/apps/desktop/build/icon.ico +0 -0
- package/apps/desktop/build/icon.png +0 -0
- package/apps/desktop/dev-app-update.yml +6 -0
- package/apps/desktop/electron-builder.js +92 -0
- package/apps/desktop/electron.vite.config.ts +40 -0
- package/apps/desktop/package.json +72 -0
- package/apps/desktop/pnpm-workspace.yaml +5 -0
- package/apps/desktop/resources/error.html +136 -0
- package/apps/desktop/resources/locales/ar/common.json +32 -0
- package/apps/desktop/resources/locales/ar/dialog.json +31 -0
- package/apps/desktop/resources/locales/ar/menu.json +70 -0
- package/apps/desktop/resources/locales/bg-BG/common.json +32 -0
- package/apps/desktop/resources/locales/bg-BG/dialog.json +31 -0
- package/apps/desktop/resources/locales/bg-BG/menu.json +70 -0
- package/apps/desktop/resources/locales/de-DE/common.json +32 -0
- package/apps/desktop/resources/locales/de-DE/dialog.json +31 -0
- package/apps/desktop/resources/locales/de-DE/menu.json +70 -0
- package/apps/desktop/resources/locales/en-US/common.json +32 -0
- package/apps/desktop/resources/locales/en-US/dialog.json +31 -0
- package/apps/desktop/resources/locales/en-US/menu.json +70 -0
- package/apps/desktop/resources/locales/es-ES/common.json +32 -0
- package/apps/desktop/resources/locales/es-ES/dialog.json +31 -0
- package/apps/desktop/resources/locales/es-ES/menu.json +70 -0
- package/apps/desktop/resources/locales/fa-IR/common.json +32 -0
- package/apps/desktop/resources/locales/fa-IR/dialog.json +31 -0
- package/apps/desktop/resources/locales/fa-IR/menu.json +70 -0
- package/apps/desktop/resources/locales/fr-FR/common.json +32 -0
- package/apps/desktop/resources/locales/fr-FR/dialog.json +31 -0
- package/apps/desktop/resources/locales/fr-FR/menu.json +70 -0
- package/apps/desktop/resources/locales/it-IT/common.json +32 -0
- package/apps/desktop/resources/locales/it-IT/dialog.json +31 -0
- package/apps/desktop/resources/locales/it-IT/menu.json +70 -0
- package/apps/desktop/resources/locales/ja-JP/common.json +32 -0
- package/apps/desktop/resources/locales/ja-JP/dialog.json +31 -0
- package/apps/desktop/resources/locales/ja-JP/menu.json +70 -0
- package/apps/desktop/resources/locales/ko-KR/common.json +32 -0
- package/apps/desktop/resources/locales/ko-KR/dialog.json +31 -0
- package/apps/desktop/resources/locales/ko-KR/menu.json +70 -0
- package/apps/desktop/resources/locales/nl-NL/common.json +32 -0
- package/apps/desktop/resources/locales/nl-NL/dialog.json +31 -0
- package/apps/desktop/resources/locales/nl-NL/menu.json +70 -0
- package/apps/desktop/resources/locales/pl-PL/common.json +32 -0
- package/apps/desktop/resources/locales/pl-PL/dialog.json +31 -0
- package/apps/desktop/resources/locales/pl-PL/menu.json +70 -0
- package/apps/desktop/resources/locales/pt-BR/common.json +32 -0
- package/apps/desktop/resources/locales/pt-BR/dialog.json +31 -0
- package/apps/desktop/resources/locales/pt-BR/menu.json +70 -0
- package/apps/desktop/resources/locales/ru-RU/common.json +32 -0
- package/apps/desktop/resources/locales/ru-RU/dialog.json +31 -0
- package/apps/desktop/resources/locales/ru-RU/menu.json +70 -0
- package/apps/desktop/resources/locales/tr-TR/common.json +32 -0
- package/apps/desktop/resources/locales/tr-TR/dialog.json +31 -0
- package/apps/desktop/resources/locales/tr-TR/menu.json +70 -0
- package/apps/desktop/resources/locales/vi-VN/common.json +32 -0
- package/apps/desktop/resources/locales/vi-VN/dialog.json +31 -0
- package/apps/desktop/resources/locales/vi-VN/menu.json +70 -0
- package/apps/desktop/resources/locales/zh-CN/common.json +32 -0
- package/apps/desktop/resources/locales/zh-CN/dialog.json +31 -0
- package/apps/desktop/resources/locales/zh-CN/menu.json +70 -0
- package/apps/desktop/resources/locales/zh-TW/common.json +32 -0
- package/apps/desktop/resources/locales/zh-TW/dialog.json +31 -0
- package/apps/desktop/resources/locales/zh-TW/menu.json +70 -0
- package/apps/desktop/resources/splash.html +88 -0
- package/apps/desktop/scripts/i18nWorkflow/const.ts +18 -0
- package/apps/desktop/scripts/i18nWorkflow/genDefaultLocale.ts +35 -0
- package/apps/desktop/scripts/i18nWorkflow/genDiff.ts +57 -0
- package/apps/desktop/scripts/i18nWorkflow/index.ts +35 -0
- package/apps/desktop/scripts/i18nWorkflow/utils.ts +54 -0
- package/apps/desktop/scripts/pglite-server.ts +14 -0
- package/apps/desktop/src/common/routes.ts +78 -0
- package/apps/desktop/src/main/appBrowsers.ts +47 -0
- package/apps/desktop/src/main/const/dir.ts +29 -0
- package/apps/desktop/src/main/const/env.ts +3 -0
- package/apps/desktop/src/main/const/store.ts +22 -0
- package/apps/desktop/src/main/controllers/AuthCtr.ts +390 -0
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +95 -0
- package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +9 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +380 -0
- package/apps/desktop/src/main/controllers/MenuCtr.ts +29 -0
- package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +335 -0
- package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +321 -0
- package/apps/desktop/src/main/controllers/ShortcutCtr.ts +19 -0
- package/apps/desktop/src/main/controllers/SystemCtr.ts +93 -0
- package/apps/desktop/src/main/controllers/UpdaterCtr.ts +43 -0
- package/apps/desktop/src/main/controllers/UploadFileCtr.ts +34 -0
- package/apps/desktop/src/main/controllers/_template.ts +9 -0
- package/apps/desktop/src/main/controllers/index.ts +58 -0
- package/apps/desktop/src/main/core/App.ts +370 -0
- package/apps/desktop/src/main/core/Browser.ts +345 -0
- package/apps/desktop/src/main/core/BrowserManager.ts +154 -0
- package/apps/desktop/src/main/core/I18nManager.ts +185 -0
- package/apps/desktop/src/main/core/IoCContainer.ts +12 -0
- package/apps/desktop/src/main/core/MenuManager.ts +64 -0
- package/apps/desktop/src/main/core/ShortcutManager.ts +173 -0
- package/apps/desktop/src/main/core/StoreManager.ts +89 -0
- package/apps/desktop/src/main/core/UpdaterManager.ts +321 -0
- package/apps/desktop/src/main/index.ts +5 -0
- package/apps/desktop/src/main/locales/default/common.ts +34 -0
- package/apps/desktop/src/main/locales/default/dialog.ts +33 -0
- package/apps/desktop/src/main/locales/default/index.ts +11 -0
- package/apps/desktop/src/main/locales/default/menu.ts +72 -0
- package/apps/desktop/src/main/locales/resources.ts +35 -0
- package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.ts +10 -0
- package/apps/desktop/src/main/menus/impls/linux.ts +243 -0
- package/apps/desktop/src/main/menus/impls/macOS.ts +360 -0
- package/apps/desktop/src/main/menus/impls/windows.ts +226 -0
- package/apps/desktop/src/main/menus/index.ts +34 -0
- package/apps/desktop/src/main/menus/types.ts +28 -0
- package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +577 -0
- package/apps/desktop/src/main/modules/fileSearch/index.ts +23 -0
- package/apps/desktop/src/main/modules/fileSearch/type.ts +27 -0
- package/apps/desktop/src/main/modules/updater/configs.ts +22 -0
- package/apps/desktop/src/main/modules/updater/utils.ts +33 -0
- package/apps/desktop/src/main/services/fileSearchSrv.ts +35 -0
- package/apps/desktop/src/main/services/fileSrv.ts +255 -0
- package/apps/desktop/src/main/services/index.ts +9 -0
- package/apps/desktop/src/main/shortcuts/config.ts +18 -0
- package/apps/desktop/src/main/shortcuts/index.ts +1 -0
- package/apps/desktop/src/main/types/fileSearch.ts +51 -0
- package/apps/desktop/src/main/types/store.ts +14 -0
- package/apps/desktop/src/main/utils/file-system.ts +15 -0
- package/apps/desktop/src/main/utils/logger.ts +44 -0
- package/apps/desktop/src/main/utils/next-electron-rsc.ts +383 -0
- package/apps/desktop/src/preload/electronApi.ts +18 -0
- package/apps/desktop/src/preload/index.ts +14 -0
- package/apps/desktop/src/preload/invoke.ts +10 -0
- package/apps/desktop/src/preload/routeInterceptor.ts +162 -0
- package/apps/desktop/tsconfig.json +21 -0
- package/changelog/v1.json +14 -0
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/remoteServer.ts +11 -4
- package/packages/electron-client-ipc/src/types/dataSync.ts +15 -0
- package/packages/electron-client-ipc/src/types/index.ts +2 -1
- package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +21 -0
- package/packages/electron-server-ipc/src/const.ts +3 -3
- package/packages/electron-server-ipc/src/ipcClient.test.ts +7 -6
- package/packages/electron-server-ipc/src/ipcClient.ts +17 -8
- package/packages/electron-server-ipc/src/ipcServer.ts +7 -3
- package/scripts/electronWorkflow/setDesktopVersion.ts +60 -43
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
- package/src/components/Analytics/Desktop.tsx +19 -0
- package/src/components/Analytics/index.tsx +3 -0
- package/src/database/core/db-adaptor.ts +4 -1
- package/src/database/core/electron.ts +317 -0
- package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
- package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
- package/src/libs/trpc/client/async.ts +6 -0
- package/src/libs/trpc/client/edge.ts +6 -0
- package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
- package/src/libs/trpc/client/index.ts +1 -0
- package/src/libs/trpc/client/lambda.ts +10 -1
- package/src/libs/trpc/client/tools.ts +6 -0
- package/src/server/globalConfig/index.ts +0 -3
- package/src/server/modules/ElectronIPCClient/index.ts +3 -1
- package/src/server/routers/desktop/index.ts +2 -0
- package/src/server/routers/desktop/mcp.ts +47 -0
- package/src/server/routers/lambda/user.ts +38 -23
- package/src/server/routers/tools/mcp.ts +0 -6
- package/src/services/electron/remoteServer.ts +4 -4
- package/src/services/mcp.ts +17 -7
- package/src/services/upload.ts +9 -0
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
- package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
- package/src/store/electron/actions/sync.ts +20 -19
- package/src/store/electron/initialState.ts +3 -3
- package/src/store/electron/selectors/sync.ts +6 -3
- package/src/store/electron/store.ts +2 -0
- package/src/store/file/slices/upload/action.ts +11 -3
- package/src/store/tool/selectors/tool.ts +10 -1
- package/src/utils/fetch/headers.ts +27 -0
- package/src/utils/fetch/index.ts +2 -0
- package/src/utils/fetch/request.ts +28 -0
- package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
- /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
- /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
- /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/index.tsx +0 -0
@@ -0,0 +1,317 @@
|
|
1
|
+
import { PGlite } from '@electric-sql/pglite';
|
2
|
+
import { vector } from '@electric-sql/pglite/vector';
|
3
|
+
import { drizzle as pgliteDrizzle } from 'drizzle-orm/pglite';
|
4
|
+
import fs from 'node:fs';
|
5
|
+
import { Md5 } from 'ts-md5';
|
6
|
+
|
7
|
+
import { DrizzleMigrationModel } from '@/database/models/drizzleMigration';
|
8
|
+
import * as schema from '@/database/schemas';
|
9
|
+
import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
|
10
|
+
import { MigrationTableItem } from '@/types/clientDB';
|
11
|
+
|
12
|
+
import migrations from '../client/migrations.json';
|
13
|
+
import { LobeChatDatabase } from '../type';
|
14
|
+
|
15
|
+
// 用于实例管理的全局对象
|
16
|
+
interface LobeGlobal {
|
17
|
+
pgDB?: LobeChatDatabase;
|
18
|
+
pgDBInitPromise?: Promise<LobeChatDatabase>;
|
19
|
+
pgDBLock?: {
|
20
|
+
acquired: boolean;
|
21
|
+
lockPath: string;
|
22
|
+
};
|
23
|
+
}
|
24
|
+
|
25
|
+
// 确保 globalThis 有我们的命名空间
|
26
|
+
declare global {
|
27
|
+
// eslint-disable-next-line no-var
|
28
|
+
var __LOBE__: LobeGlobal;
|
29
|
+
}
|
30
|
+
|
31
|
+
if (!globalThis.__LOBE__) {
|
32
|
+
globalThis.__LOBE__ = {};
|
33
|
+
}
|
34
|
+
|
35
|
+
/**
|
36
|
+
* 尝试创建一个文件锁来确保单例模式
|
37
|
+
* 返回 true 表示成功获取锁,false 表示已有其他实例正在运行
|
38
|
+
*/
|
39
|
+
const acquireLock = async (dbPath: string): Promise<boolean> => {
|
40
|
+
try {
|
41
|
+
// 数据库锁文件路径
|
42
|
+
const lockPath = `${dbPath}.lock`;
|
43
|
+
|
44
|
+
// 尝试创建锁文件
|
45
|
+
if (!fs.existsSync(lockPath)) {
|
46
|
+
// 创建锁文件并写入当前进程 ID
|
47
|
+
fs.writeFileSync(lockPath, process.pid.toString(), 'utf8');
|
48
|
+
|
49
|
+
// 保存锁信息到全局对象
|
50
|
+
if (!globalThis.__LOBE__.pgDBLock) {
|
51
|
+
globalThis.__LOBE__.pgDBLock = {
|
52
|
+
acquired: true,
|
53
|
+
lockPath,
|
54
|
+
};
|
55
|
+
}
|
56
|
+
|
57
|
+
console.log(`✅ Successfully acquired database lock: ${lockPath}`);
|
58
|
+
return true;
|
59
|
+
}
|
60
|
+
|
61
|
+
// 检查锁文件是否过期(超过5分钟未更新)
|
62
|
+
const stats = fs.statSync(lockPath);
|
63
|
+
const currentTime = Date.now();
|
64
|
+
const modifiedTime = stats.mtime.getTime();
|
65
|
+
|
66
|
+
// 如果锁文件超过5分钟未更新,视为过期锁
|
67
|
+
if (currentTime - modifiedTime > 5 * 60 * 1000) {
|
68
|
+
// 删除过期锁文件
|
69
|
+
fs.unlinkSync(lockPath);
|
70
|
+
// 重新创建锁文件
|
71
|
+
fs.writeFileSync(lockPath, process.pid.toString(), 'utf8');
|
72
|
+
|
73
|
+
// 保存锁信息到全局对象
|
74
|
+
if (!globalThis.__LOBE__.pgDBLock) {
|
75
|
+
globalThis.__LOBE__.pgDBLock = {
|
76
|
+
acquired: true,
|
77
|
+
lockPath,
|
78
|
+
};
|
79
|
+
}
|
80
|
+
|
81
|
+
console.log(`✅ Removed stale lock and acquired new lock: ${lockPath}`);
|
82
|
+
return true;
|
83
|
+
}
|
84
|
+
|
85
|
+
console.warn(`⚠️ Another process has already locked the database: ${lockPath}`);
|
86
|
+
return false;
|
87
|
+
} catch (error) {
|
88
|
+
console.error('❌ Failed to acquire database lock:', error);
|
89
|
+
return false;
|
90
|
+
}
|
91
|
+
};
|
92
|
+
|
93
|
+
/**
|
94
|
+
* 释放文件锁
|
95
|
+
*/
|
96
|
+
const releaseLock = () => {
|
97
|
+
if (globalThis.__LOBE__.pgDBLock?.acquired && globalThis.__LOBE__.pgDBLock.lockPath) {
|
98
|
+
try {
|
99
|
+
fs.unlinkSync(globalThis.__LOBE__.pgDBLock.lockPath);
|
100
|
+
globalThis.__LOBE__.pgDBLock.acquired = false;
|
101
|
+
console.log(`✅ Released database lock: ${globalThis.__LOBE__.pgDBLock.lockPath}`);
|
102
|
+
} catch (error) {
|
103
|
+
console.error('❌ Failed to release database lock:', error);
|
104
|
+
}
|
105
|
+
}
|
106
|
+
};
|
107
|
+
|
108
|
+
// 在进程退出时释放锁
|
109
|
+
process.on('exit', releaseLock);
|
110
|
+
process.on('SIGINT', () => {
|
111
|
+
releaseLock();
|
112
|
+
process.exit(0);
|
113
|
+
});
|
114
|
+
|
115
|
+
process.on('uncaughtException', (error) => {
|
116
|
+
// ignore ECONNRESET error
|
117
|
+
if ((error as any).code === 'ECONNRESET') return;
|
118
|
+
|
119
|
+
console.error('Uncaught exception:', error);
|
120
|
+
releaseLock();
|
121
|
+
});
|
122
|
+
|
123
|
+
const migrateDatabase = async (db: LobeChatDatabase): Promise<void> => {
|
124
|
+
try {
|
125
|
+
let hash: string | undefined;
|
126
|
+
const cacheHash = await electronIpcClient.getDatabaseSchemaHash();
|
127
|
+
|
128
|
+
hash = Md5.hashStr(JSON.stringify(migrations));
|
129
|
+
|
130
|
+
console.log('schemaHash:', hash);
|
131
|
+
|
132
|
+
// 如果哈希值相同,看下表是否全了
|
133
|
+
if (hash === cacheHash) {
|
134
|
+
try {
|
135
|
+
const drizzleMigration = new DrizzleMigrationModel(db);
|
136
|
+
|
137
|
+
// 检查数据库中是否存在表
|
138
|
+
const tableCount = await drizzleMigration.getTableCounts();
|
139
|
+
|
140
|
+
// 如果表数量大于0,则认为数据库已正确初始化
|
141
|
+
if (tableCount > 0) {
|
142
|
+
console.log('✅ Electron DB schema already synced');
|
143
|
+
return;
|
144
|
+
}
|
145
|
+
} catch (error) {
|
146
|
+
console.warn('Error checking table existence, proceeding with migration:');
|
147
|
+
console.warn(error);
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
const start = Date.now();
|
152
|
+
console.log('🚀 Starting Electron DB migration...');
|
153
|
+
|
154
|
+
try {
|
155
|
+
// 执行迁移
|
156
|
+
// @ts-expect-error
|
157
|
+
await db.dialect.migrate(migrations, db.session, {});
|
158
|
+
|
159
|
+
await electronIpcClient.setDatabaseSchemaHash(hash);
|
160
|
+
|
161
|
+
console.info(`✅ Electron DB migration success, took ${Date.now() - start}ms`);
|
162
|
+
} catch (error) {
|
163
|
+
console.error('❌ Electron database schema migration failed', error);
|
164
|
+
|
165
|
+
// 尝试查询迁移表数据
|
166
|
+
let migrationsTableData: MigrationTableItem[] = [];
|
167
|
+
try {
|
168
|
+
// 尝试查询迁移表
|
169
|
+
const drizzleMigration = new DrizzleMigrationModel(db);
|
170
|
+
migrationsTableData = await drizzleMigration.getMigrationList();
|
171
|
+
} catch (queryError) {
|
172
|
+
console.error('Failed to query migrations table:', queryError);
|
173
|
+
}
|
174
|
+
|
175
|
+
throw {
|
176
|
+
error: error as Error,
|
177
|
+
migrationTableItems: migrationsTableData,
|
178
|
+
};
|
179
|
+
}
|
180
|
+
} catch (error) {
|
181
|
+
console.error('❌ Electron database migration failed:', error);
|
182
|
+
throw error;
|
183
|
+
}
|
184
|
+
};
|
185
|
+
|
186
|
+
/**
|
187
|
+
* 检查当前是否有活跃的数据库实例,如果有则尝试关闭它
|
188
|
+
*/
|
189
|
+
const checkAndCleanupExistingInstance = async () => {
|
190
|
+
if (globalThis.__LOBE__.pgDB) {
|
191
|
+
try {
|
192
|
+
// 尝试关闭现有的 PGlite 实例 (如果客户端有 close 方法)
|
193
|
+
// @ts-expect-error
|
194
|
+
const client = globalThis.__LOBE__.pgDB?.dialect?.client;
|
195
|
+
|
196
|
+
if (client && typeof client.close === 'function') {
|
197
|
+
await client.close();
|
198
|
+
console.log('✅ Successfully closed previous PGlite instance');
|
199
|
+
}
|
200
|
+
|
201
|
+
// 重置全局引用
|
202
|
+
globalThis.__LOBE__.pgDB = undefined;
|
203
|
+
} catch (error) {
|
204
|
+
console.error('❌ Failed to close previous PGlite instance:', error);
|
205
|
+
// 继续执行,创建新实例
|
206
|
+
}
|
207
|
+
}
|
208
|
+
};
|
209
|
+
|
210
|
+
let isInitializing = false;
|
211
|
+
|
212
|
+
export const getPgliteInstance = async (): Promise<LobeChatDatabase> => {
|
213
|
+
try {
|
214
|
+
console.log(
|
215
|
+
'Getting PGlite instance, state:',
|
216
|
+
JSON.stringify({
|
217
|
+
hasExistingDB: !!globalThis.__LOBE__.pgDB,
|
218
|
+
hasPromise: !!globalThis.__LOBE__.pgDBInitPromise,
|
219
|
+
isInitializing,
|
220
|
+
}),
|
221
|
+
);
|
222
|
+
|
223
|
+
// 已经初始化完成,直接返回实例
|
224
|
+
if (globalThis.__LOBE__.pgDB) return globalThis.__LOBE__.pgDB;
|
225
|
+
|
226
|
+
// 有初始化进行中的Promise,等待它完成
|
227
|
+
if (globalThis.__LOBE__.pgDBInitPromise) {
|
228
|
+
console.log('Waiting for existing initialization promise to complete');
|
229
|
+
return globalThis.__LOBE__.pgDBInitPromise;
|
230
|
+
}
|
231
|
+
|
232
|
+
// 防止多次调用引起的竞态条件
|
233
|
+
if (isInitializing) {
|
234
|
+
console.log('Already initializing, waiting for result');
|
235
|
+
// 创建新的 Promise 等待初始化完成
|
236
|
+
return new Promise((resolve, reject) => {
|
237
|
+
const checkInterval = setInterval(() => {
|
238
|
+
if (globalThis.__LOBE__.pgDB) {
|
239
|
+
clearInterval(checkInterval);
|
240
|
+
resolve(globalThis.__LOBE__.pgDB);
|
241
|
+
} else if (!isInitializing) {
|
242
|
+
clearInterval(checkInterval);
|
243
|
+
reject(new Error('Initialization failed or was canceled'));
|
244
|
+
}
|
245
|
+
}, 100);
|
246
|
+
});
|
247
|
+
}
|
248
|
+
|
249
|
+
isInitializing = true;
|
250
|
+
|
251
|
+
// 创建初始化Promise并保存
|
252
|
+
globalThis.__LOBE__.pgDBInitPromise = (async () => {
|
253
|
+
// 再次检查,以防在等待过程中已有其他调用初始化成功
|
254
|
+
if (globalThis.__LOBE__.pgDB) return globalThis.__LOBE__.pgDB;
|
255
|
+
|
256
|
+
// 先获取数据库路径
|
257
|
+
let dbPath: string = '';
|
258
|
+
try {
|
259
|
+
dbPath = await electronIpcClient.getDatabasePath();
|
260
|
+
} catch {
|
261
|
+
/* empty */
|
262
|
+
}
|
263
|
+
|
264
|
+
console.log('Database path:', dbPath);
|
265
|
+
try {
|
266
|
+
// 尝试获取数据库锁
|
267
|
+
const lockAcquired = await acquireLock(dbPath);
|
268
|
+
if (!lockAcquired) {
|
269
|
+
throw new Error('Cannot acquire database lock. Another instance might be using it.');
|
270
|
+
}
|
271
|
+
|
272
|
+
// 检查并清理可能存在的旧实例
|
273
|
+
await checkAndCleanupExistingInstance();
|
274
|
+
|
275
|
+
// 创建新的 PGlite 实例
|
276
|
+
console.log('Creating new PGlite instance');
|
277
|
+
const client = new PGlite(dbPath, {
|
278
|
+
extensions: { vector },
|
279
|
+
// 增加选项以提高稳定性
|
280
|
+
relaxedDurability: true,
|
281
|
+
});
|
282
|
+
|
283
|
+
// 等待数据库就绪
|
284
|
+
await client.waitReady;
|
285
|
+
console.log('PGlite state:', client.ready);
|
286
|
+
|
287
|
+
// 创建 Drizzle 数据库实例
|
288
|
+
const db = pgliteDrizzle({ client, schema }) as unknown as LobeChatDatabase;
|
289
|
+
|
290
|
+
// 执行迁移
|
291
|
+
await migrateDatabase(db);
|
292
|
+
|
293
|
+
// 保存实例引用
|
294
|
+
globalThis.__LOBE__.pgDB = db;
|
295
|
+
|
296
|
+
console.log('✅ PGlite instance successfully initialized');
|
297
|
+
|
298
|
+
return db;
|
299
|
+
} catch (error) {
|
300
|
+
console.error('❌ Failed to initialize PGlite instance:', error);
|
301
|
+
// 清空初始化Promise,允许下次重试
|
302
|
+
globalThis.__LOBE__.pgDBInitPromise = undefined;
|
303
|
+
// 释放可能已获取的锁
|
304
|
+
releaseLock();
|
305
|
+
throw error;
|
306
|
+
} finally {
|
307
|
+
isInitializing = false;
|
308
|
+
}
|
309
|
+
})();
|
310
|
+
|
311
|
+
return globalThis.__LOBE__.pgDBInitPromise;
|
312
|
+
} catch (error) {
|
313
|
+
console.error('❌ Unexpected error in getPgliteInstance:', error);
|
314
|
+
isInitializing = false;
|
315
|
+
throw error;
|
316
|
+
}
|
317
|
+
};
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { StorageMode, StorageModeEnum } from '@lobechat/electron-client-ipc';
|
1
2
|
import { Input } from '@lobehub/ui';
|
2
3
|
import { LobeHub } from '@lobehub/ui/brand';
|
3
4
|
import { Button } from 'antd';
|
@@ -8,8 +9,9 @@ import { useTranslation } from 'react-i18next';
|
|
8
9
|
import { Center, Flexbox } from 'react-layout-kit';
|
9
10
|
|
10
11
|
import { useElectronStore } from '@/store/electron';
|
12
|
+
import { electronSyncSelectors } from '@/store/electron/selectors';
|
11
13
|
|
12
|
-
import {
|
14
|
+
import { Option } from './Option';
|
13
15
|
|
14
16
|
const useStyles = createStyles(({ token, css }) => {
|
15
17
|
return {
|
@@ -78,12 +80,15 @@ interface ConnectionModeProps {
|
|
78
80
|
const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) => {
|
79
81
|
const { styles } = useStyles();
|
80
82
|
const { t } = useTranslation(['electron', 'common']);
|
81
|
-
const [selectedOption, setSelectedOption] = useState<AccessOption>();
|
82
|
-
const [selfHostedUrl, setSelfHostedUrl] = useState('');
|
83
83
|
const [urlError, setUrlError] = useState<string | undefined>();
|
84
84
|
|
85
85
|
const connect = useElectronStore((s) => s.connectRemoteServer);
|
86
86
|
const disconnect = useElectronStore((s) => s.disconnectRemoteServer);
|
87
|
+
const storageMode = useElectronStore(electronSyncSelectors.storageMode);
|
88
|
+
const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
|
89
|
+
|
90
|
+
const [selectedOption, setSelectedOption] = useState<StorageMode>(storageMode);
|
91
|
+
const [selfHostedUrl, setSelfHostedUrl] = useState(remoteServerUrl);
|
87
92
|
|
88
93
|
const validateUrl = useCallback((url: string) => {
|
89
94
|
if (!url) {
|
@@ -100,9 +105,9 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) =>
|
|
100
105
|
}
|
101
106
|
}, []);
|
102
107
|
|
103
|
-
const handleSelectOption = (option:
|
108
|
+
const handleSelectOption = (option: StorageMode) => {
|
104
109
|
setSelectedOption(option);
|
105
|
-
if (option !==
|
110
|
+
if (option !== StorageModeEnum.SelfHost) {
|
106
111
|
setUrlError(undefined);
|
107
112
|
} else {
|
108
113
|
setUrlError(validateUrl(selfHostedUrl));
|
@@ -110,7 +115,7 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) =>
|
|
110
115
|
};
|
111
116
|
|
112
117
|
const handleContinue = async () => {
|
113
|
-
if (selectedOption ===
|
118
|
+
if (selectedOption === StorageModeEnum.SelfHost) {
|
114
119
|
const error = validateUrl(selfHostedUrl);
|
115
120
|
setUrlError(error);
|
116
121
|
if (error) {
|
@@ -118,7 +123,7 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) =>
|
|
118
123
|
}
|
119
124
|
}
|
120
125
|
|
121
|
-
if (selectedOption ===
|
126
|
+
if (selectedOption === StorageModeEnum.Local) {
|
122
127
|
await disconnect();
|
123
128
|
setIsOpen(false);
|
124
129
|
return;
|
@@ -126,11 +131,8 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) =>
|
|
126
131
|
|
127
132
|
// try to connect
|
128
133
|
setWaiting(true);
|
129
|
-
|
130
|
-
|
131
|
-
? { isSelfHosted: true, serverUrl: selfHostedUrl }
|
132
|
-
: { isSelfHosted: false },
|
133
|
-
);
|
134
|
+
console.log('selectedOption:', selectedOption);
|
135
|
+
await connect({ remoteServerUrl: selfHostedUrl, storageMode: selectedOption });
|
134
136
|
};
|
135
137
|
|
136
138
|
return (
|
@@ -145,7 +147,7 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) =>
|
|
145
147
|
<div className={styles.groupTitle}>{t('sync.mode.cloudSync')}</div>
|
146
148
|
<div
|
147
149
|
className={styles.selfHostedText}
|
148
|
-
onClick={() => handleSelectOption(
|
150
|
+
onClick={() => handleSelectOption(StorageModeEnum.SelfHost)}
|
149
151
|
>
|
150
152
|
{t('sync.mode.useSelfHosted')}
|
151
153
|
</div>
|
@@ -156,18 +158,18 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) =>
|
|
156
158
|
isSelected={selectedOption === 'cloud'}
|
157
159
|
label={t('sync.lobehubCloud.title')}
|
158
160
|
onClick={handleSelectOption}
|
159
|
-
value=
|
161
|
+
value={StorageModeEnum.Cloud}
|
160
162
|
/>
|
161
|
-
{selectedOption ===
|
163
|
+
{selectedOption === StorageModeEnum.SelfHost && (
|
162
164
|
<Option
|
163
165
|
description={t('sync.selfHosted.description')}
|
164
166
|
icon={Server}
|
165
|
-
isSelected={selectedOption ===
|
167
|
+
isSelected={selectedOption === StorageModeEnum.SelfHost}
|
166
168
|
label={t('sync.selfHosted.title')}
|
167
169
|
onClick={handleSelectOption}
|
168
|
-
value=
|
170
|
+
value={StorageModeEnum.SelfHost}
|
169
171
|
>
|
170
|
-
{selectedOption ===
|
172
|
+
{selectedOption === StorageModeEnum.SelfHost && (
|
171
173
|
<>
|
172
174
|
<Input
|
173
175
|
autoFocus
|
@@ -195,10 +197,10 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) =>
|
|
195
197
|
<Option
|
196
198
|
description={t('sync.local.description')}
|
197
199
|
icon={ComputerIcon}
|
198
|
-
isSelected={selectedOption ===
|
200
|
+
isSelected={selectedOption === StorageModeEnum.Local}
|
199
201
|
label={t('sync.local.title')}
|
200
202
|
onClick={handleSelectOption}
|
201
|
-
value=
|
203
|
+
value={StorageModeEnum.Local}
|
202
204
|
/>
|
203
205
|
</Flexbox>
|
204
206
|
</Flexbox>
|
@@ -206,7 +208,8 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setIsOpen, setWaiting }) =>
|
|
206
208
|
<Button
|
207
209
|
className={styles.continueButton}
|
208
210
|
disabled={
|
209
|
-
!selectedOption ||
|
211
|
+
!selectedOption ||
|
212
|
+
(selectedOption === StorageModeEnum.SelfHost && (!!urlError || !selfHostedUrl))
|
210
213
|
}
|
211
214
|
onClick={handleContinue}
|
212
215
|
size="large"
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { CheckCircleFilled } from '@ant-design/icons';
|
2
|
+
import { StorageModeEnum } from '@lobechat/electron-client-ipc';
|
2
3
|
import { createStyles } from 'antd-style';
|
3
4
|
import { ComponentType, ReactNode } from 'react';
|
4
5
|
import { Center, Flexbox } from 'react-layout-kit';
|
@@ -55,17 +56,14 @@ const useStyles = createStyles(({ token, css }) => ({
|
|
55
56
|
`,
|
56
57
|
}));
|
57
58
|
|
58
|
-
// 定义选项类型
|
59
|
-
export type AccessOption = 'cloud' | 'self-hosted' | 'local';
|
60
|
-
|
61
59
|
export interface OptionProps {
|
62
60
|
children?: ReactNode;
|
63
61
|
description: string;
|
64
62
|
icon: ComponentType<any>;
|
65
63
|
isSelected: boolean;
|
66
64
|
label: string;
|
67
|
-
onClick: (value:
|
68
|
-
value:
|
65
|
+
onClick: (value: StorageModeEnum) => void;
|
66
|
+
value: StorageModeEnum; // For self-hosted input
|
69
67
|
}
|
70
68
|
|
71
69
|
export const Option = ({
|
@@ -9,17 +9,20 @@ import { electronSyncSelectors } from '@/store/electron/selectors';
|
|
9
9
|
interface SyncProps {
|
10
10
|
onClick: () => void;
|
11
11
|
}
|
12
|
-
const
|
12
|
+
const RemoteStatus = memo<SyncProps>(({ onClick }) => {
|
13
13
|
const { t } = useTranslation('electron');
|
14
14
|
|
15
|
-
const [isIniting, isSyncActive, useRemoteServerConfig] =
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
const [isIniting, isSyncActive, useRemoteServerConfig, useRefreshDataWhenActive] =
|
16
|
+
useElectronStore((s) => [
|
17
|
+
!s.isInitRemoteServerConfig,
|
18
|
+
electronSyncSelectors.isSyncActive(s),
|
19
|
+
s.useDataSyncConfig,
|
20
|
+
s.useRefreshDataWhenActive,
|
21
|
+
]);
|
20
22
|
|
21
23
|
// 使用useSWR获取远程服务器配置
|
22
24
|
useRemoteServerConfig();
|
25
|
+
useRefreshDataWhenActive(isSyncActive);
|
23
26
|
|
24
27
|
return (
|
25
28
|
<ActionIcon
|
@@ -39,4 +42,4 @@ const Sync = memo<SyncProps>(({ onClick }) => {
|
|
39
42
|
);
|
40
43
|
});
|
41
44
|
|
42
|
-
export default
|
45
|
+
export default RemoteStatus;
|
package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx
RENAMED
@@ -2,8 +2,8 @@ import { Drawer } from 'antd';
|
|
2
2
|
import { createStyles } from 'antd-style';
|
3
3
|
import { useState } from 'react';
|
4
4
|
|
5
|
-
import
|
6
|
-
import
|
5
|
+
import ConnectionMode from './ConnectionMode';
|
6
|
+
import RemoteStatus from './RemoteStatus';
|
7
7
|
import WaitingOAuth from './Waiting';
|
8
8
|
|
9
9
|
const useStyles = createStyles(({ css }) => {
|
@@ -26,7 +26,7 @@ const Connection = () => {
|
|
26
26
|
|
27
27
|
return (
|
28
28
|
<>
|
29
|
-
<
|
29
|
+
<RemoteStatus
|
30
30
|
onClick={() => {
|
31
31
|
setIsOpen(true);
|
32
32
|
}}
|
@@ -47,7 +47,7 @@ const Connection = () => {
|
|
47
47
|
{isWaiting ? (
|
48
48
|
<WaitingOAuth setIsOpen={setIsOpen} setWaiting={setWaiting} />
|
49
49
|
) : (
|
50
|
-
<
|
50
|
+
<ConnectionMode setIsOpen={setIsOpen} setWaiting={setWaiting} />
|
51
51
|
)}
|
52
52
|
</Drawer>
|
53
53
|
</>
|
package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx
RENAMED
@@ -4,6 +4,7 @@ import React, { memo, useState } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
5
5
|
|
6
6
|
import { autoUpdateService } from '@/services/electron/autoUpdate';
|
7
|
+
import { formatSpeed } from '@/utils/format';
|
7
8
|
|
8
9
|
export const UpdateModal = memo(() => {
|
9
10
|
const { t } = useTranslation(['electron', 'common']);
|
@@ -175,7 +176,7 @@ export const UpdateModal = memo(() => {
|
|
175
176
|
<div style={{ fontSize: 12, marginTop: 8, textAlign: 'center' }}>
|
176
177
|
{t('updater.downloadingUpdateDesc', { percent })}
|
177
178
|
{progress && progress.bytesPerSecond > 0 && (
|
178
|
-
<span>
|
179
|
+
<span>{formatSpeed(progress.bytesPerSecond)}</span>
|
179
180
|
)}
|
180
181
|
</div>
|
181
182
|
</div>
|
@@ -1,11 +1,17 @@
|
|
1
1
|
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
2
2
|
import superjson from 'superjson';
|
3
3
|
|
4
|
+
import { isDesktop } from '@/const/version';
|
4
5
|
import { AsyncRouter } from '@/server/routers/async';
|
5
6
|
|
7
|
+
import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
|
8
|
+
|
6
9
|
export const asyncClient = createTRPCClient<AsyncRouter>({
|
7
10
|
links: [
|
8
11
|
httpBatchLink({
|
12
|
+
fetch: isDesktop
|
13
|
+
? (input, init) => fetchWithDesktopRemoteRPC(input as string, init)
|
14
|
+
: undefined,
|
9
15
|
maxURLLength: 2083,
|
10
16
|
transformer: superjson,
|
11
17
|
url: '/trpc/async',
|
@@ -1,12 +1,18 @@
|
|
1
1
|
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
2
2
|
import superjson from 'superjson';
|
3
3
|
|
4
|
+
import { isDesktop } from '@/const/version';
|
4
5
|
import type { EdgeRouter } from '@/server/routers/edge';
|
5
6
|
import { withBasePath } from '@/utils/basePath';
|
6
7
|
|
8
|
+
import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
|
9
|
+
|
7
10
|
export const edgeClient = createTRPCClient<EdgeRouter>({
|
8
11
|
links: [
|
9
12
|
httpBatchLink({
|
13
|
+
fetch: isDesktop
|
14
|
+
? (input, init) => fetchWithDesktopRemoteRPC(input as string, init)
|
15
|
+
: undefined,
|
10
16
|
headers: async () => {
|
11
17
|
// dynamic import to avoid circular dependency
|
12
18
|
const { createHeaderWithAuth } = await import('@/services/_auth');
|
@@ -0,0 +1,72 @@
|
|
1
|
+
import { ProxyTRPCRequestParams, dispatch } from '@lobechat/electron-client-ipc';
|
2
|
+
import debug from 'debug';
|
3
|
+
|
4
|
+
import { isDesktop } from '@/const/version';
|
5
|
+
import { getElectronStoreState } from '@/store/electron';
|
6
|
+
import { electronSyncSelectors } from '@/store/electron/selectors';
|
7
|
+
import { getRequestBody, headersToRecord } from '@/utils/fetch';
|
8
|
+
|
9
|
+
const log = debug('lobe-lambda:desktopRemoteRPCFetch');
|
10
|
+
|
11
|
+
// eslint-disable-next-line no-undef
|
12
|
+
export const desktopRemoteRPCFetch = async (input: string, init?: RequestInit) => {
|
13
|
+
const isSyncActive = electronSyncSelectors.isSyncActive(getElectronStoreState());
|
14
|
+
log('isDesktop:', isDesktop, 'isSyncActive:', isSyncActive);
|
15
|
+
|
16
|
+
if (isSyncActive) {
|
17
|
+
log('Using IPC proxy for tRPC request');
|
18
|
+
try {
|
19
|
+
const url = input as string;
|
20
|
+
const parsedUrl = new URL(url, window.location.origin);
|
21
|
+
const urlPath = parsedUrl.pathname + parsedUrl.search;
|
22
|
+
const method = init?.method?.toUpperCase() || 'GET';
|
23
|
+
const headers = headersToRecord(init?.headers);
|
24
|
+
const body = await getRequestBody(init?.body);
|
25
|
+
|
26
|
+
const params: ProxyTRPCRequestParams = {
|
27
|
+
body,
|
28
|
+
headers,
|
29
|
+
method,
|
30
|
+
urlPath,
|
31
|
+
};
|
32
|
+
|
33
|
+
const ipcResult = await dispatch('proxyTRPCRequest', params);
|
34
|
+
|
35
|
+
log('Received IPC proxy response:', { status: ipcResult.status });
|
36
|
+
const response = new Response(ipcResult.body, {
|
37
|
+
headers: ipcResult.headers,
|
38
|
+
status: ipcResult.status,
|
39
|
+
statusText: ipcResult.statusText,
|
40
|
+
});
|
41
|
+
|
42
|
+
if (!response.ok) {
|
43
|
+
console.warn(
|
44
|
+
'[lambda] IPC proxy response indicates an error:',
|
45
|
+
response.status,
|
46
|
+
response.statusText,
|
47
|
+
);
|
48
|
+
}
|
49
|
+
|
50
|
+
return response;
|
51
|
+
} catch (error) {
|
52
|
+
console.error('[lambda] Error during IPC proxy call:', error);
|
53
|
+
return new Response(
|
54
|
+
`IPC Proxy Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
55
|
+
{
|
56
|
+
status: 500,
|
57
|
+
statusText: 'IPC Proxy Error',
|
58
|
+
},
|
59
|
+
);
|
60
|
+
}
|
61
|
+
}
|
62
|
+
};
|
63
|
+
|
64
|
+
// eslint-disable-next-line no-undef
|
65
|
+
export const fetchWithDesktopRemoteRPC = async (input: string, init?: RequestInit) => {
|
66
|
+
if (isDesktop) {
|
67
|
+
const res = await desktopRemoteRPCFetch(input as string, init);
|
68
|
+
if (res) return res;
|
69
|
+
}
|
70
|
+
|
71
|
+
return fetch(input, init);
|
72
|
+
};
|