Deploy: exdev sync 2026-08-03 13:59:26
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
const { app, BrowserWindow, shell, Menu, session } = require("electron");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
// Gatekeeper / ad-hoc 서명 환경에서 GPU Helper SIGTRAP 완화
|
||||
app.disableHardwareAcceleration();
|
||||
app.commandLine.appendSwitch("disable-gpu");
|
||||
app.commandLine.appendSwitch("disable-gpu-compositing");
|
||||
app.commandLine.appendSwitch("disable-software-rasterizer");
|
||||
app.commandLine.appendSwitch("no-sandbox");
|
||||
app.commandLine.appendSwitch("disable-dev-shm-usage");
|
||||
|
||||
const WINDOW_WIDTH = 390;
|
||||
const WINDOW_HEIGHT = 720;
|
||||
|
||||
const DEFAULT_URL =
|
||||
process.env.CHATBOT_APP_URL ||
|
||||
"https://chatbot.exdev.co.kr/chatbot/ai/?desktop=1";
|
||||
|
||||
const logFile = path.join(app.getPath("userData"), "ex-ai-chatbot.log");
|
||||
|
||||
function log(msg) {
|
||||
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
||||
try {
|
||||
fs.appendFileSync(logFile, line);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
/** 챗봇 주소에서 온 마이크 요청은 매번 묻지 않고 허용한다. (음성인식 반복 권한창 방지) */
|
||||
function allowMicrophoneForChatbot() {
|
||||
const allowed = new Set(["media", "audioCapture", "mediaKeySystem"]);
|
||||
const appOrigin = (() => {
|
||||
try {
|
||||
return new URL(DEFAULT_URL).origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const isChatbotOrigin = (url) => {
|
||||
if (!appOrigin || !url) return false;
|
||||
try {
|
||||
return new URL(url).origin === appOrigin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ses = session.defaultSession;
|
||||
|
||||
ses.setPermissionRequestHandler((webContents, permission, callback, details) => {
|
||||
const url = details?.requestingUrl || webContents?.getURL();
|
||||
callback(allowed.has(permission) && isChatbotOrigin(url));
|
||||
});
|
||||
|
||||
ses.setPermissionCheckHandler((_webContents, permission, requestingOrigin, details) => {
|
||||
const url = requestingOrigin || details?.requestingUrl;
|
||||
return allowed.has(permission) && isChatbotOrigin(url);
|
||||
});
|
||||
|
||||
if (typeof ses.setDevicePermissionHandler === "function") {
|
||||
ses.setDevicePermissionHandler((details) => isChatbotOrigin(details?.origin));
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const iconPath = path.join(__dirname, "build", process.platform === "win32" ? "icon.ico" : "icon.png");
|
||||
const winOpts = {
|
||||
width: WINDOW_WIDTH,
|
||||
height: WINDOW_HEIGHT,
|
||||
minWidth: 360,
|
||||
minHeight: 520,
|
||||
title: "한국도로공사 AI 챗봇",
|
||||
backgroundColor: "#ffffff",
|
||||
autoHideMenuBar: true,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
webSecurity: true,
|
||||
},
|
||||
};
|
||||
if (fs.existsSync(iconPath)) {
|
||||
winOpts.icon = iconPath;
|
||||
}
|
||||
const win = new BrowserWindow(winOpts);
|
||||
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
win.once("ready-to-show", () => {
|
||||
win.show();
|
||||
if (process.platform === "darwin" && app.dock) {
|
||||
const dockIcon = path.join(__dirname, "build", "icon.png");
|
||||
if (fs.existsSync(dockIcon)) {
|
||||
try {
|
||||
app.dock.setIcon(dockIcon);
|
||||
} catch (e) {
|
||||
log(`dock.setIcon failed: ${e}`);
|
||||
}
|
||||
}
|
||||
app.dock.show();
|
||||
}
|
||||
});
|
||||
|
||||
win.webContents.on("did-fail-load", (_e, code, desc, url) => {
|
||||
log(`did-fail-load code=${code} desc=${desc} url=${url}`);
|
||||
// 네트워크 실패 시에도 빈 창으로 종료되지 않도록 안내 페이지
|
||||
const html = `<!doctype html><meta charset="utf-8"/>
|
||||
<body style="font:14px -apple-system,sans-serif;padding:40px;line-height:1.6">
|
||||
<h2>연결 실패</h2>
|
||||
<p>챗봇 서버에 연결하지 못했습니다.</p>
|
||||
<p style="color:#666">${code}: ${desc}</p>
|
||||
<p><a href="${DEFAULT_URL}">다시 시도</a></p>
|
||||
</body>`;
|
||||
win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
||||
});
|
||||
|
||||
win.webContents.on("render-process-gone", (_e, details) => {
|
||||
log(`render-process-gone ${JSON.stringify(details)}`);
|
||||
});
|
||||
|
||||
win.on("unresponsive", () => log("window unresponsive"));
|
||||
|
||||
log(`loadURL ${DEFAULT_URL}`);
|
||||
win.loadURL(DEFAULT_URL).catch((err) => log(`loadURL error: ${err}`));
|
||||
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
const wins = BrowserWindow.getAllWindows();
|
||||
if (wins.length > 0) {
|
||||
if (wins[0].isMinimized()) wins[0].restore();
|
||||
wins[0].focus();
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
app.setName("EX AI Chatbot");
|
||||
log(`ready userData=${app.getPath("userData")}`);
|
||||
allowMicrophoneForChatbot();
|
||||
createWindow();
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (err) => {
|
||||
log(`uncaughtException ${err?.stack || err}`);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user