|
|
@@ -2,6 +2,8 @@
|
|
|
const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage, webContents } = require('electron');
|
|
|
const { join } = require('path');
|
|
|
const fs = require('fs');
|
|
|
+const http = require('http');
|
|
|
+const https = require('https');
|
|
|
|
|
|
let mainWindow: typeof BrowserWindow.prototype | null = null;
|
|
|
let tray: typeof Tray.prototype | null = null;
|
|
|
@@ -9,6 +11,97 @@ let isQuitting = false;
|
|
|
|
|
|
const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
|
|
|
|
|
|
+function setupCertificateBypass() {
|
|
|
+ app.on('certificate-error', (event: Event, _webContents: typeof webContents.prototype, _url: string, _error: string, _certificate: unknown, callback: (isTrusted: boolean) => void) => {
|
|
|
+ event.preventDefault();
|
|
|
+ callback(true);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function setupCorsBypassForApiRequests() {
|
|
|
+ const ses = session.defaultSession;
|
|
|
+ if (!ses) return;
|
|
|
+
|
|
|
+ ses.webRequest.onHeadersReceived((details: { url: string; responseHeaders?: Record<string, string[] | string> }, callback: (response: { responseHeaders: Record<string, string[] | string> }) => void) => {
|
|
|
+ const url = String(details.url || '');
|
|
|
+ const isHttp = url.startsWith('http://') || url.startsWith('https://');
|
|
|
+ const isApiLike = url.includes('/api/') || url.includes('/uploads/');
|
|
|
+ if (!isHttp || !isApiLike) {
|
|
|
+ callback({ responseHeaders: details.responseHeaders || {} });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const responseHeaders = { ...(details.responseHeaders || {}) };
|
|
|
+ // 移除服务端已有的 CORS 头,避免与下面设置的值合并成 "origin1, *" 导致违规
|
|
|
+ const corsKeys = ['access-control-allow-origin', 'Access-Control-Allow-Origin'];
|
|
|
+ corsKeys.forEach((k) => delete responseHeaders[k]);
|
|
|
+ responseHeaders['access-control-allow-origin'] = ['*'];
|
|
|
+ responseHeaders['access-control-allow-methods'] = ['GET,POST,PUT,PATCH,DELETE,OPTIONS'];
|
|
|
+ responseHeaders['access-control-allow-headers'] = ['Authorization,Content-Type,X-Requested-With'];
|
|
|
+ responseHeaders['access-control-expose-headers'] = ['Content-Disposition,Content-Type'];
|
|
|
+
|
|
|
+ callback({ responseHeaders });
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeBaseUrl(url: string): string {
|
|
|
+ const raw = String(url || '').trim();
|
|
|
+ if (!raw) return '';
|
|
|
+ try {
|
|
|
+ const u = new URL(raw);
|
|
|
+ return `${u.protocol}//${u.host}`.replace(/\/$/, '');
|
|
|
+ } catch {
|
|
|
+ return raw.replace(/\/$/, '');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function requestJson(url: string, timeoutMs: number): Promise<{ ok: boolean; status?: number; data?: any; error?: string }> {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const u = new URL(url);
|
|
|
+ const isHttps = u.protocol === 'https:';
|
|
|
+ const lib = isHttps ? https : http;
|
|
|
+ // Windows 上 localhost 常被解析为 ::1,而后端仅监听 127.0.0.1,导致 ECONNREFUSED
|
|
|
+ const hostname = (u.hostname === 'localhost' || u.hostname === '::1') ? '127.0.0.1' : u.hostname;
|
|
|
+ const req = lib.request({
|
|
|
+ method: 'GET',
|
|
|
+ protocol: u.protocol,
|
|
|
+ hostname,
|
|
|
+ port: u.port || (isHttps ? 443 : 80),
|
|
|
+ path: `${u.pathname}${u.search}`,
|
|
|
+ headers: {
|
|
|
+ Accept: 'application/json',
|
|
|
+ },
|
|
|
+ timeout: timeoutMs,
|
|
|
+ rejectUnauthorized: false,
|
|
|
+ }, (res: any) => {
|
|
|
+ const chunks: Buffer[] = [];
|
|
|
+ res.on('data', (c: Buffer) => chunks.push(c));
|
|
|
+ res.on('end', () => {
|
|
|
+ const status = Number(res.statusCode || 0);
|
|
|
+ const rawText = Buffer.concat(chunks).toString('utf-8');
|
|
|
+ if (status < 200 || status >= 300) {
|
|
|
+ resolve({ ok: false, status, error: `HTTP ${status}` });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const json = rawText ? JSON.parse(rawText) : null;
|
|
|
+ resolve({ ok: true, status, data: json });
|
|
|
+ } catch {
|
|
|
+ resolve({ ok: false, status, error: '响应不是 JSON' });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ req.on('timeout', () => {
|
|
|
+ req.destroy(new Error('timeout'));
|
|
|
+ });
|
|
|
+ req.on('error', (err: any) => {
|
|
|
+ resolve({ ok: false, error: err?.message || '网络错误' });
|
|
|
+ });
|
|
|
+ req.end();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
// 获取图标路径
|
|
|
function getIconPath() {
|
|
|
return VITE_DEV_SERVER_URL
|
|
|
@@ -166,6 +259,8 @@ if (!gotTheLock) {
|
|
|
|
|
|
// 配置 webview session,允许第三方 cookies 和跨域请求
|
|
|
setupWebviewSessions();
|
|
|
+ setupCertificateBypass();
|
|
|
+ setupCorsBypassForApiRequests();
|
|
|
|
|
|
app.on('activate', () => {
|
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
|
@@ -258,6 +353,19 @@ app.on('quit', () => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+ipcMain.handle('test-server-connection', async (_event: unknown, args: { url: string }) => {
|
|
|
+ try {
|
|
|
+ const baseUrl = normalizeBaseUrl(args?.url);
|
|
|
+ if (!baseUrl) return { ok: false, error: '未填写服务器地址' };
|
|
|
+ const result = await requestJson(`${baseUrl}/api/health`, 5000);
|
|
|
+ if (!result.ok) return { ok: false, error: result.error || '连接失败' };
|
|
|
+ if (result.data?.status === 'ok') return { ok: true };
|
|
|
+ return { ok: false, error: '服务器响应异常' };
|
|
|
+ } catch (e: any) {
|
|
|
+ return { ok: false, error: e?.message || '连接失败' };
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
// IPC 处理
|
|
|
ipcMain.handle('get-app-version', () => {
|
|
|
return app.getVersion();
|