@lobehub/chat 1.82.10 → 1.83.0
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 +196 -0
- package/CHANGELOG.md +25 -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 +9 -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,383 @@
|
|
1
|
+
// copy from https://github.com/kirill-konshin/next-electron-rsc
|
2
|
+
import { serialize as serializeCookie } from 'cookie';
|
3
|
+
import type { Protocol, Session } from 'electron';
|
4
|
+
import type { NextConfig } from 'next';
|
5
|
+
import type NextNodeServer from 'next/dist/server/next-server';
|
6
|
+
import assert from 'node:assert';
|
7
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
8
|
+
import { Socket } from 'node:net';
|
9
|
+
import path from 'node:path';
|
10
|
+
import { parse } from 'node:url';
|
11
|
+
import resolve from 'resolve';
|
12
|
+
import { parse as parseCookie, splitCookiesString } from 'set-cookie-parser';
|
13
|
+
|
14
|
+
import { isDev } from '@/const/env';
|
15
|
+
import { createLogger } from '@/utils/logger';
|
16
|
+
|
17
|
+
// 创建日志记录器
|
18
|
+
const logger = createLogger('utils:next-electron-rsc');
|
19
|
+
|
20
|
+
// 定义自定义处理器类型
|
21
|
+
export type CustomRequestHandler = (request: Request) => Promise<Response | null | undefined>;
|
22
|
+
|
23
|
+
export const createRequest = async ({
|
24
|
+
socket,
|
25
|
+
request,
|
26
|
+
session,
|
27
|
+
}: {
|
28
|
+
request: Request;
|
29
|
+
session: Session;
|
30
|
+
socket: Socket;
|
31
|
+
}): Promise<IncomingMessage> => {
|
32
|
+
const req = new IncomingMessage(socket);
|
33
|
+
|
34
|
+
const url = new URL(request.url);
|
35
|
+
|
36
|
+
// Normal Next.js URL does not contain schema and host/port, otherwise endless loops due to butchering of schema by normalizeRepeatedSlashes in resolve-routes
|
37
|
+
req.url = url.pathname + (url.search || '');
|
38
|
+
req.method = request.method;
|
39
|
+
|
40
|
+
request.headers.forEach((value, key) => {
|
41
|
+
req.headers[key] = value;
|
42
|
+
});
|
43
|
+
|
44
|
+
try {
|
45
|
+
// @see https://github.com/electron/electron/issues/39525#issue-1852825052
|
46
|
+
const cookies = await session.cookies.get({
|
47
|
+
url: request.url,
|
48
|
+
// domain: url.hostname,
|
49
|
+
// path: url.pathname,
|
50
|
+
// `secure: true` Cookies should not be sent via http
|
51
|
+
// secure: url.protocol === 'http:' ? false : undefined,
|
52
|
+
// theoretically not possible to implement sameSite because we don't know the url
|
53
|
+
// of the website that is requesting the resource
|
54
|
+
});
|
55
|
+
|
56
|
+
if (cookies.length) {
|
57
|
+
const cookiesHeader = [];
|
58
|
+
|
59
|
+
for (const cookie of cookies) {
|
60
|
+
const { name, value } = cookie;
|
61
|
+
cookiesHeader.push(serializeCookie(name, value)); // ...(options as any)?
|
62
|
+
}
|
63
|
+
|
64
|
+
req.headers.cookie = cookiesHeader.join('; ');
|
65
|
+
}
|
66
|
+
} catch (e) {
|
67
|
+
throw new Error('Failed to parse cookies', { cause: e });
|
68
|
+
}
|
69
|
+
|
70
|
+
if (request.body) {
|
71
|
+
req.push(Buffer.from(await request.arrayBuffer()));
|
72
|
+
}
|
73
|
+
|
74
|
+
req.push(null);
|
75
|
+
req.complete = true;
|
76
|
+
|
77
|
+
return req;
|
78
|
+
};
|
79
|
+
|
80
|
+
export class ReadableServerResponse extends ServerResponse {
|
81
|
+
private responsePromise: Promise<Response>;
|
82
|
+
|
83
|
+
constructor(req: IncomingMessage) {
|
84
|
+
super(req);
|
85
|
+
|
86
|
+
this.responsePromise = new Promise<Response>((resolve) => {
|
87
|
+
const readableStream = new ReadableStream({
|
88
|
+
cancel: () => {},
|
89
|
+
pull: () => {
|
90
|
+
this.emit('drain');
|
91
|
+
},
|
92
|
+
start: (controller) => {
|
93
|
+
let onData;
|
94
|
+
|
95
|
+
this.on(
|
96
|
+
'data',
|
97
|
+
(onData = (chunk) => {
|
98
|
+
controller.enqueue(chunk);
|
99
|
+
}),
|
100
|
+
);
|
101
|
+
|
102
|
+
this.once('end', (chunk) => {
|
103
|
+
controller.enqueue(chunk);
|
104
|
+
controller.close();
|
105
|
+
this.off('data', onData);
|
106
|
+
});
|
107
|
+
},
|
108
|
+
});
|
109
|
+
|
110
|
+
this.once('writeHead', (statusCode) => {
|
111
|
+
resolve(
|
112
|
+
new Response(readableStream, {
|
113
|
+
headers: this.getHeaders() as any,
|
114
|
+
status: statusCode,
|
115
|
+
statusText: this.statusMessage,
|
116
|
+
}),
|
117
|
+
);
|
118
|
+
});
|
119
|
+
});
|
120
|
+
}
|
121
|
+
|
122
|
+
write(chunk: any, ...args): boolean {
|
123
|
+
this.emit('data', chunk);
|
124
|
+
return super.write(chunk, ...args);
|
125
|
+
}
|
126
|
+
|
127
|
+
end(chunk: any, ...args): this {
|
128
|
+
this.emit('end', chunk);
|
129
|
+
return super.end(chunk, ...args);
|
130
|
+
}
|
131
|
+
|
132
|
+
writeHead(statusCode: number, ...args: any): this {
|
133
|
+
this.emit('writeHead', statusCode);
|
134
|
+
return super.writeHead(statusCode, ...args);
|
135
|
+
}
|
136
|
+
|
137
|
+
getResponse() {
|
138
|
+
return this.responsePromise;
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
/**
|
143
|
+
* https://nextjs.org/docs/pages/building-your-application/configuring/custom-server
|
144
|
+
* https://github.com/vercel/next.js/pull/68167/files#diff-d0d8b7158bcb066cdbbeb548a29909fe8dc4e98f682a6d88654b1684e523edac
|
145
|
+
* https://github.com/vercel/next.js/blob/canary/examples/custom-server/server.ts
|
146
|
+
*
|
147
|
+
* @param {string} standaloneDir
|
148
|
+
* @param {string} localhostUrl
|
149
|
+
* @param {import('electron').Protocol} protocol
|
150
|
+
* @param {boolean} debug
|
151
|
+
*/
|
152
|
+
export function createHandler({
|
153
|
+
standaloneDir,
|
154
|
+
localhostUrl,
|
155
|
+
protocol,
|
156
|
+
debug = false,
|
157
|
+
}: {
|
158
|
+
debug?: boolean;
|
159
|
+
localhostUrl: string;
|
160
|
+
protocol: Protocol;
|
161
|
+
standaloneDir: string;
|
162
|
+
}) {
|
163
|
+
assert(standaloneDir, 'standaloneDir is required');
|
164
|
+
assert(protocol, 'protocol is required');
|
165
|
+
|
166
|
+
// 存储自定义请求处理器的数组
|
167
|
+
const customHandlers: CustomRequestHandler[] = [];
|
168
|
+
|
169
|
+
// 注册自定义请求处理器的方法 - 在开发和生产环境中都提供此功能
|
170
|
+
function registerCustomHandler(handler: CustomRequestHandler) {
|
171
|
+
logger.debug('Registering custom request handler');
|
172
|
+
customHandlers.push(handler);
|
173
|
+
return () => {
|
174
|
+
const index = customHandlers.indexOf(handler);
|
175
|
+
if (index !== -1) {
|
176
|
+
logger.debug('Unregistering custom request handler');
|
177
|
+
customHandlers.splice(index, 1);
|
178
|
+
}
|
179
|
+
};
|
180
|
+
}
|
181
|
+
let registerProtocolHandle = false;
|
182
|
+
|
183
|
+
protocol.registerSchemesAsPrivileged([
|
184
|
+
{
|
185
|
+
privileges: {
|
186
|
+
secure: true,
|
187
|
+
standard: true,
|
188
|
+
supportFetchAPI: true,
|
189
|
+
},
|
190
|
+
scheme: 'http',
|
191
|
+
},
|
192
|
+
]);
|
193
|
+
logger.debug('Registered HTTP scheme as privileged');
|
194
|
+
|
195
|
+
// 初始化 Next.js 应用(仅在生产环境中使用)
|
196
|
+
let app: NextNodeServer | null = null;
|
197
|
+
let handler: any = null;
|
198
|
+
let preparePromise: Promise<void> | null = null;
|
199
|
+
|
200
|
+
if (!isDev) {
|
201
|
+
logger.info('Initializing Next.js app for production');
|
202
|
+
const next = require(resolve.sync('next', { basedir: standaloneDir }));
|
203
|
+
|
204
|
+
// @see https://github.com/vercel/next.js/issues/64031#issuecomment-2078708340
|
205
|
+
const config = require(path.join(standaloneDir, '.next', 'required-server-files.json'))
|
206
|
+
.config as NextConfig;
|
207
|
+
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config);
|
208
|
+
|
209
|
+
app = next({
|
210
|
+
dev: false,
|
211
|
+
dir: standaloneDir,
|
212
|
+
}) as NextNodeServer;
|
213
|
+
|
214
|
+
handler = app.getRequestHandler();
|
215
|
+
preparePromise = app.prepare();
|
216
|
+
} else {
|
217
|
+
logger.debug('Starting in development mode');
|
218
|
+
}
|
219
|
+
|
220
|
+
// 通用的请求处理函数 - 开发和生产环境共用
|
221
|
+
const handleRequest = async (
|
222
|
+
request: Request,
|
223
|
+
session: Session,
|
224
|
+
socket: Socket,
|
225
|
+
): Promise<Response> => {
|
226
|
+
try {
|
227
|
+
// 先尝试使用自定义处理器处理请求
|
228
|
+
for (const customHandler of customHandlers) {
|
229
|
+
try {
|
230
|
+
const response = await customHandler(request);
|
231
|
+
if (response) {
|
232
|
+
if (debug) logger.debug(`Custom handler processed: ${request.url}`);
|
233
|
+
return response;
|
234
|
+
}
|
235
|
+
} catch (error) {
|
236
|
+
if (debug) logger.error(`Custom handler error: ${error}`);
|
237
|
+
// 继续尝试下一个处理器
|
238
|
+
}
|
239
|
+
}
|
240
|
+
|
241
|
+
// 创建 Node.js 请求对象
|
242
|
+
const req = await createRequest({ request, session, socket });
|
243
|
+
// 创建可读取响应的 Response 对象
|
244
|
+
const res = new ReadableServerResponse(req);
|
245
|
+
|
246
|
+
if (isDev) {
|
247
|
+
// 开发环境:转发请求到开发服务器
|
248
|
+
if (debug) logger.debug(`Forwarding request to dev server: ${request.url}`);
|
249
|
+
|
250
|
+
// 修改 URL 以指向开发服务器
|
251
|
+
const devUrl = new URL(req.url, localhostUrl);
|
252
|
+
|
253
|
+
// 使用 node:http 模块发送请求到开发服务器
|
254
|
+
const http = require('node:http');
|
255
|
+
const devReq = http.request(
|
256
|
+
{
|
257
|
+
headers: req.headers,
|
258
|
+
hostname: devUrl.hostname,
|
259
|
+
method: req.method,
|
260
|
+
path: devUrl.pathname + (devUrl.search || ''),
|
261
|
+
port: devUrl.port,
|
262
|
+
},
|
263
|
+
(devRes) => {
|
264
|
+
// 设置响应状态码和头部
|
265
|
+
res.statusCode = devRes.statusCode;
|
266
|
+
res.statusMessage = devRes.statusMessage;
|
267
|
+
|
268
|
+
// 复制响应头
|
269
|
+
Object.keys(devRes.headers).forEach((key) => {
|
270
|
+
res.setHeader(key, devRes.headers[key]);
|
271
|
+
});
|
272
|
+
|
273
|
+
// 流式传输响应内容
|
274
|
+
devRes.pipe(res);
|
275
|
+
},
|
276
|
+
);
|
277
|
+
|
278
|
+
// 处理错误
|
279
|
+
devReq.on('error', (err) => {
|
280
|
+
if (debug) logger.error(`Error forwarding request: ${err}`);
|
281
|
+
});
|
282
|
+
|
283
|
+
// 传输请求体
|
284
|
+
req.pipe(devReq);
|
285
|
+
} else {
|
286
|
+
// 生产环境:使用 Next.js 处理请求
|
287
|
+
if (debug) logger.debug(`Processing with Next.js handler: ${request.url}`);
|
288
|
+
|
289
|
+
// 确保 Next.js 已准备就绪
|
290
|
+
if (preparePromise) await preparePromise;
|
291
|
+
|
292
|
+
const url = parse(req.url, true);
|
293
|
+
handler(req, res, url);
|
294
|
+
}
|
295
|
+
|
296
|
+
// 获取 Response 对象
|
297
|
+
const response = await res.getResponse();
|
298
|
+
|
299
|
+
// 处理 cookies(两种环境通用处理)
|
300
|
+
try {
|
301
|
+
const cookies = parseCookie(
|
302
|
+
response.headers.getSetCookie().reduce((r, c) => {
|
303
|
+
return [...r, ...splitCookiesString(c)];
|
304
|
+
}, []),
|
305
|
+
);
|
306
|
+
|
307
|
+
for (const cookie of cookies) {
|
308
|
+
const expires = cookie.expires
|
309
|
+
? cookie.expires.getTime()
|
310
|
+
: cookie.maxAge
|
311
|
+
? Date.now() + cookie.maxAge * 1000
|
312
|
+
: undefined;
|
313
|
+
|
314
|
+
if (expires && expires < Date.now()) {
|
315
|
+
await session.cookies.remove(request.url, cookie.name);
|
316
|
+
continue;
|
317
|
+
}
|
318
|
+
|
319
|
+
await session.cookies.set({
|
320
|
+
domain: cookie.domain,
|
321
|
+
expirationDate: expires,
|
322
|
+
httpOnly: cookie.httpOnly,
|
323
|
+
name: cookie.name,
|
324
|
+
path: cookie.path,
|
325
|
+
secure: cookie.secure,
|
326
|
+
url: request.url,
|
327
|
+
value: cookie.value,
|
328
|
+
} as any);
|
329
|
+
}
|
330
|
+
} catch (e) {
|
331
|
+
logger.error('Failed to set cookies', e);
|
332
|
+
}
|
333
|
+
|
334
|
+
if (debug) logger.debug(`Request processed: ${request.url}, status: ${response.status}`);
|
335
|
+
return response;
|
336
|
+
} catch (e) {
|
337
|
+
if (debug) logger.error(`Error handling request: ${e}`);
|
338
|
+
return new Response(e.message, { status: 500 });
|
339
|
+
}
|
340
|
+
};
|
341
|
+
|
342
|
+
// 创建拦截器函数
|
343
|
+
const createInterceptor = ({ session }: { session: Session }) => {
|
344
|
+
assert(session, 'Session is required');
|
345
|
+
logger.debug(
|
346
|
+
`Creating interceptor with session in ${isDev ? 'development' : 'production'} mode`,
|
347
|
+
);
|
348
|
+
|
349
|
+
const socket = new Socket();
|
350
|
+
|
351
|
+
const closeSocket = () => socket.end();
|
352
|
+
|
353
|
+
process.on('SIGTERM', () => closeSocket);
|
354
|
+
process.on('SIGINT', () => closeSocket);
|
355
|
+
|
356
|
+
if (!isDev && !registerProtocolHandle) {
|
357
|
+
logger.debug(
|
358
|
+
`Registering HTTP protocol handler in ${isDev ? 'development' : 'production'} mode`,
|
359
|
+
);
|
360
|
+
protocol.handle('http', async (request) => {
|
361
|
+
if (!isDev) {
|
362
|
+
assert(request.url.startsWith(localhostUrl), 'External HTTP not supported, use HTTPS');
|
363
|
+
}
|
364
|
+
|
365
|
+
return handleRequest(request, session, socket);
|
366
|
+
});
|
367
|
+
registerProtocolHandle = true;
|
368
|
+
}
|
369
|
+
|
370
|
+
return function stopIntercept() {
|
371
|
+
if (registerProtocolHandle) {
|
372
|
+
logger.debug('Unregistering HTTP protocol handler');
|
373
|
+
protocol.unhandle('http');
|
374
|
+
registerProtocolHandle = false;
|
375
|
+
}
|
376
|
+
process.off('SIGTERM', () => closeSocket);
|
377
|
+
process.off('SIGINT', () => closeSocket);
|
378
|
+
closeSocket();
|
379
|
+
};
|
380
|
+
};
|
381
|
+
|
382
|
+
return { createInterceptor, registerCustomHandler };
|
383
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { electronAPI } from '@electron-toolkit/preload';
|
2
|
+
import { contextBridge } from 'electron';
|
3
|
+
|
4
|
+
import { invoke } from './invoke';
|
5
|
+
|
6
|
+
export const setupElectronApi = () => {
|
7
|
+
// Use `contextBridge` APIs to expose Electron APIs to
|
8
|
+
// renderer only if context isolation is enabled, otherwise
|
9
|
+
// just add to the DOM global.
|
10
|
+
|
11
|
+
try {
|
12
|
+
contextBridge.exposeInMainWorld('electron', electronAPI);
|
13
|
+
} catch (error) {
|
14
|
+
console.error(error);
|
15
|
+
}
|
16
|
+
|
17
|
+
contextBridge.exposeInMainWorld('electronAPI', { invoke });
|
18
|
+
};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { setupElectronApi } from './electronApi';
|
2
|
+
import { setupRouteInterceptors } from './routeInterceptor';
|
3
|
+
|
4
|
+
const setupPreload = () => {
|
5
|
+
setupElectronApi();
|
6
|
+
|
7
|
+
// 设置路由拦截逻辑
|
8
|
+
window.addEventListener('DOMContentLoaded', () => {
|
9
|
+
// 设置客户端路由拦截器
|
10
|
+
setupRouteInterceptors();
|
11
|
+
});
|
12
|
+
};
|
13
|
+
|
14
|
+
setupPreload();
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { ClientDispatchEventKey, DispatchInvoke } from '@lobechat/electron-client-ipc';
|
2
|
+
import { ipcRenderer } from 'electron';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* client 端请求 electron main 端方法
|
6
|
+
*/
|
7
|
+
export const invoke: DispatchInvoke = async <T extends ClientDispatchEventKey>(
|
8
|
+
event: T,
|
9
|
+
...data: any[]
|
10
|
+
) => ipcRenderer.invoke(event, ...data);
|
@@ -0,0 +1,162 @@
|
|
1
|
+
import { findMatchingRoute } from '~common/routes';
|
2
|
+
|
3
|
+
import { invoke } from './invoke';
|
4
|
+
|
5
|
+
const interceptRoute = async (
|
6
|
+
path: string,
|
7
|
+
source: 'link-click' | 'push-state' | 'replace-state',
|
8
|
+
url: string,
|
9
|
+
) => {
|
10
|
+
console.log(`[preload] Intercepted ${source} and prevented default behavior:`, path);
|
11
|
+
|
12
|
+
// 使用electron-client-ipc的dispatch方法
|
13
|
+
try {
|
14
|
+
await invoke('interceptRoute', { path, source, url });
|
15
|
+
} catch (e) {
|
16
|
+
console.error(`[preload] Route interception (${source}) call failed`, e);
|
17
|
+
}
|
18
|
+
};
|
19
|
+
/**
|
20
|
+
* 路由拦截器 - 负责捕获和拦截客户端路由导航
|
21
|
+
*/
|
22
|
+
export const setupRouteInterceptors = function () {
|
23
|
+
console.log('[preload] Setting up route interceptors');
|
24
|
+
|
25
|
+
// 存储被阻止的路径,避免pushState重复触发
|
26
|
+
const preventedPaths = new Set<string>();
|
27
|
+
|
28
|
+
// 拦截所有a标签的点击事件 - 针对Next.js的Link组件
|
29
|
+
document.addEventListener(
|
30
|
+
'click',
|
31
|
+
async (e) => {
|
32
|
+
const link = (e.target as HTMLElement).closest('a');
|
33
|
+
if (link && link.href) {
|
34
|
+
try {
|
35
|
+
const url = new URL(link.href);
|
36
|
+
|
37
|
+
// 使用共享配置检查是否需要拦截
|
38
|
+
const matchedRoute = findMatchingRoute(url.pathname);
|
39
|
+
|
40
|
+
// 如果是需要拦截的路径,立即阻止默认行为
|
41
|
+
if (matchedRoute) {
|
42
|
+
// 检查当前页面是否已经在目标路径下,如果是则不拦截
|
43
|
+
const currentPath = window.location.pathname;
|
44
|
+
const isAlreadyInTargetPage = currentPath.startsWith(matchedRoute.pathPrefix);
|
45
|
+
|
46
|
+
// 如果已经在目标页面下,则不拦截,让默认导航继续
|
47
|
+
if (isAlreadyInTargetPage) return;
|
48
|
+
|
49
|
+
// 立即阻止默认行为,避免Next.js接管路由
|
50
|
+
e.preventDefault();
|
51
|
+
e.stopPropagation();
|
52
|
+
|
53
|
+
await interceptRoute(url.pathname, 'link-click', link.href);
|
54
|
+
|
55
|
+
return false;
|
56
|
+
}
|
57
|
+
} catch (err) {
|
58
|
+
console.error('[preload] Link interception error:', err);
|
59
|
+
}
|
60
|
+
}
|
61
|
+
},
|
62
|
+
true,
|
63
|
+
);
|
64
|
+
|
65
|
+
// 拦截 history API (用于捕获Next.js的useRouter().push/replace等)
|
66
|
+
const originalPushState = history.pushState;
|
67
|
+
const originalReplaceState = history.replaceState;
|
68
|
+
|
69
|
+
// 重写pushState
|
70
|
+
history.pushState = function () {
|
71
|
+
const url = arguments[2];
|
72
|
+
if (typeof url === 'string') {
|
73
|
+
try {
|
74
|
+
// 只处理相对路径或当前域的URL
|
75
|
+
const parsedUrl = new URL(url, window.location.origin);
|
76
|
+
|
77
|
+
// 使用共享配置检查是否需要拦截
|
78
|
+
const matchedRoute = findMatchingRoute(parsedUrl.pathname);
|
79
|
+
|
80
|
+
// 检查是否需要拦截这个导航
|
81
|
+
if (matchedRoute) {
|
82
|
+
// 检查当前页面是否已经在目标路径下,如果是则不拦截
|
83
|
+
const currentPath = window.location.pathname;
|
84
|
+
const isAlreadyInTargetPage = currentPath.startsWith(matchedRoute.pathPrefix);
|
85
|
+
|
86
|
+
// 如果已经在目标页面下,则不拦截,让默认导航继续
|
87
|
+
if (isAlreadyInTargetPage) {
|
88
|
+
console.log(
|
89
|
+
`[preload] Skip pushState interception for ${parsedUrl.pathname} because already in target page ${matchedRoute.pathPrefix}`,
|
90
|
+
);
|
91
|
+
return Reflect.apply(originalPushState, this, arguments);
|
92
|
+
}
|
93
|
+
|
94
|
+
// 将此路径添加到已阻止集合中
|
95
|
+
preventedPaths.add(parsedUrl.pathname);
|
96
|
+
|
97
|
+
interceptRoute(parsedUrl.pathname, 'push-state', parsedUrl.href);
|
98
|
+
|
99
|
+
// 不执行原始的pushState操作,阻止导航发生
|
100
|
+
// 但返回undefined以避免错误
|
101
|
+
return;
|
102
|
+
}
|
103
|
+
} catch (err) {
|
104
|
+
console.error('[preload] pushState interception error:', err);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
return Reflect.apply(originalPushState, this, arguments);
|
108
|
+
};
|
109
|
+
|
110
|
+
// 重写replaceState
|
111
|
+
history.replaceState = function () {
|
112
|
+
const url = arguments[2];
|
113
|
+
if (typeof url === 'string') {
|
114
|
+
try {
|
115
|
+
const parsedUrl = new URL(url, window.location.origin);
|
116
|
+
|
117
|
+
// 使用共享配置检查是否需要拦截
|
118
|
+
const matchedRoute = findMatchingRoute(parsedUrl.pathname);
|
119
|
+
|
120
|
+
// 检查是否需要拦截这个导航
|
121
|
+
if (matchedRoute) {
|
122
|
+
// 检查当前页面是否已经在目标路径下,如果是则不拦截
|
123
|
+
const currentPath = window.location.pathname;
|
124
|
+
const isAlreadyInTargetPage = currentPath.startsWith(matchedRoute.pathPrefix);
|
125
|
+
|
126
|
+
// 如果已经在目标页面下,则不拦截,让默认导航继续
|
127
|
+
if (isAlreadyInTargetPage) {
|
128
|
+
console.log(
|
129
|
+
`[preload] Skip replaceState interception for ${parsedUrl.pathname} because already in target page ${matchedRoute.pathPrefix}`,
|
130
|
+
);
|
131
|
+
return Reflect.apply(originalReplaceState, this, arguments);
|
132
|
+
}
|
133
|
+
|
134
|
+
// 添加到已阻止集合
|
135
|
+
preventedPaths.add(parsedUrl.pathname);
|
136
|
+
|
137
|
+
interceptRoute(parsedUrl.pathname, 'replace-state', parsedUrl.href);
|
138
|
+
|
139
|
+
// 阻止导航
|
140
|
+
return;
|
141
|
+
}
|
142
|
+
} catch (err) {
|
143
|
+
console.error('[preload] replaceState interception error:', err);
|
144
|
+
}
|
145
|
+
}
|
146
|
+
return Reflect.apply(originalReplaceState, this, arguments);
|
147
|
+
};
|
148
|
+
|
149
|
+
// 监听并拦截路由错误 - 有时Next.js会在路由错误时尝试恢复导航
|
150
|
+
window.addEventListener(
|
151
|
+
'error',
|
152
|
+
function (e) {
|
153
|
+
if (e.message && e.message.includes('navigation') && preventedPaths.size > 0) {
|
154
|
+
console.log('[preload] Captured possible routing error, preventing default behavior');
|
155
|
+
e.preventDefault();
|
156
|
+
}
|
157
|
+
},
|
158
|
+
true,
|
159
|
+
);
|
160
|
+
|
161
|
+
console.log('[preload] Route interceptors setup completed');
|
162
|
+
};
|
@@ -0,0 +1,21 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"allowJs": true,
|
4
|
+
"skipLibCheck": true,
|
5
|
+
"forceConsistentCasingInFileNames": true,
|
6
|
+
"noEmit": true,
|
7
|
+
"target": "ESNext",
|
8
|
+
"esModuleInterop": true,
|
9
|
+
"emitDecoratorMetadata": true,
|
10
|
+
"experimentalDecorators": true,
|
11
|
+
"module": "esnext",
|
12
|
+
"moduleResolution": "bundler",
|
13
|
+
"resolveJsonModule": true,
|
14
|
+
"baseUrl": ".",
|
15
|
+
"paths": {
|
16
|
+
"@/*": ["src/main/*"],
|
17
|
+
"~common/*": ["src/common/*"]
|
18
|
+
}
|
19
|
+
},
|
20
|
+
"include": ["src/main/**/*", "src/preload/**/*", "electron-builder.js"]
|
21
|
+
}
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.83.0",
|
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",
|
@@ -1,20 +1,27 @@
|
|
1
|
-
import {
|
1
|
+
import { DataSyncConfig } from '../types/dataSync';
|
2
|
+
import { ProxyTRPCRequestParams, ProxyTRPCRequestResult } from '../types/proxyTRPCRequest';
|
2
3
|
|
3
4
|
/**
|
4
5
|
* 远程服务器配置相关的事件
|
5
6
|
*/
|
6
7
|
export interface RemoteServerDispatchEvents {
|
7
8
|
clearRemoteServerConfig: () => boolean;
|
8
|
-
getRemoteServerConfig: () =>
|
9
|
+
getRemoteServerConfig: () => DataSyncConfig;
|
10
|
+
/**
|
11
|
+
* Proxy a tRPC request to the remote server.
|
12
|
+
* @param args - Request arguments.
|
13
|
+
* @returns Promise resolving with the response details.
|
14
|
+
*/
|
15
|
+
proxyTRPCRequest: (args: ProxyTRPCRequestParams) => ProxyTRPCRequestResult;
|
9
16
|
refreshAccessToken: () => {
|
10
17
|
error?: string;
|
11
18
|
success: boolean;
|
12
19
|
};
|
13
|
-
requestAuthorization: (
|
20
|
+
requestAuthorization: (config: DataSyncConfig) => {
|
14
21
|
error?: string;
|
15
22
|
success: boolean;
|
16
23
|
};
|
17
|
-
setRemoteServerConfig: (config:
|
24
|
+
setRemoteServerConfig: (config: DataSyncConfig) => boolean;
|
18
25
|
}
|
19
26
|
|
20
27
|
/**
|
@@ -0,0 +1,15 @@
|
|
1
|
+
export type StorageMode = 'local' | 'cloud' | 'selfHost';
|
2
|
+
export enum StorageModeEnum {
|
3
|
+
Cloud = 'cloud',
|
4
|
+
Local = 'local',
|
5
|
+
SelfHost = 'selfHost',
|
6
|
+
}
|
7
|
+
|
8
|
+
/**
|
9
|
+
* 远程服务器配置相关的事件
|
10
|
+
*/
|
11
|
+
export interface DataSyncConfig {
|
12
|
+
active?: boolean;
|
13
|
+
remoteServerUrl?: string;
|
14
|
+
storageMode: StorageMode;
|
15
|
+
}
|