Files
zn-ai/dist-electron/main/main.js
duanshuwen eb9acae071 feat(build): add icon generation and external binary bundling
- Add scripts to generate application icons in multiple formats (ICO, ICNS, PNG)
- Implement download scripts for uv and Node.js binaries for cross-platform support
- Update build configuration to use new icon resources and bundled binaries
- Remove old loading screen and unused build configurations
- Fix application icon path resolution to use app resources directory
2026-04-08 07:25:25 +08:00

1569 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
const electron = require("electron");
const OpenAI = require("openai");
const util = require("util");
const log = require("electron-log");
const path = require("path");
const fs = require("fs");
const jsBase64 = require("js-base64");
const path$1 = require("node:path");
const crypto = require("crypto");
const started = require("electron-squirrel-startup");
const net = require("net");
const http = require("http");
const child_process = require("child_process");
const events = require("events");
require("bytenode");
function _interopNamespaceDefault(e) {
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
if (e) {
for (const k in e) {
if (k !== "default") {
const d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: () => e[k]
});
}
}
}
n.default = e;
return Object.freeze(n);
}
const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
IPC_EVENTS2["EXTERNAL_OPEN"] = "external-open";
IPC_EVENTS2["WINDOW_MINIMIZE"] = "window-minimize";
IPC_EVENTS2["WINDOW_MAXIMIZE"] = "window-maximize";
IPC_EVENTS2["WINDOW_CLOSE"] = "window-close";
IPC_EVENTS2["IS_WINDOW_MAXIMIZED"] = "is-window-maximized";
IPC_EVENTS2["APP_SET_FRAMELESS"] = "app:set-frameless";
IPC_EVENTS2["APP_LOAD_PAGE"] = "app:load-page";
IPC_EVENTS2["TAB_CREATE"] = "tab:create";
IPC_EVENTS2["TAB_LIST"] = "tab:list";
IPC_EVENTS2["TAB_NAVIGATE"] = "tab:navigate";
IPC_EVENTS2["TAB_RELOAD"] = "tab:reload";
IPC_EVENTS2["TAB_BACK"] = "tab:back";
IPC_EVENTS2["TAB_FORWARD"] = "tab:forward";
IPC_EVENTS2["TAB_SWITCH"] = "tab:switch";
IPC_EVENTS2["TAB_CLOSE"] = "tab:close";
IPC_EVENTS2["LOG_TO_MAIN"] = "log-to-main";
IPC_EVENTS2["READ_FILE"] = "read-file";
IPC_EVENTS2["INVOKE"] = "ipc:invoke";
IPC_EVENTS2["INVOKE_ASYNC"] = "ipc:invokeAsync";
IPC_EVENTS2["APP_MINIMIZE"] = "app:minimize";
IPC_EVENTS2["APP_MAXIMIZE"] = "app:maximize";
IPC_EVENTS2["APP_QUIT"] = "app:quit";
IPC_EVENTS2["FILE_READ"] = "file:read";
IPC_EVENTS2["FILE_WRITE"] = "file:write";
IPC_EVENTS2["GET_WINDOW_ID"] = "get-window-id";
IPC_EVENTS2["CUSTOM_EVENT"] = "custom:event";
IPC_EVENTS2["TIME_UPDATE"] = "time:update";
IPC_EVENTS2["RENDERER_IS_READY"] = "renderer-ready";
IPC_EVENTS2["SHOW_CONTEXT_MENU"] = "show-context-menu";
IPC_EVENTS2["START_A_DIALOGUE"] = "start-a-dialogue";
IPC_EVENTS2["OPEN_WINDOW"] = "open-window";
IPC_EVENTS2["LOG_DEBUG"] = "log-debug";
IPC_EVENTS2["LOG_INFO"] = "log-info";
IPC_EVENTS2["LOG_WARN"] = "log-warn";
IPC_EVENTS2["LOG_ERROR"] = "log-error";
IPC_EVENTS2["CONFIG_UPDATED"] = "config-updated";
IPC_EVENTS2["SET_CONFIG"] = "set-config";
IPC_EVENTS2["GET_CONFIG"] = "get-config";
IPC_EVENTS2["UPDATE_CONFIG"] = "update-config";
IPC_EVENTS2["SET_THEME_MODE"] = "set-theme-mode";
IPC_EVENTS2["GET_THEME_MODE"] = "get-theme-mode";
IPC_EVENTS2["IS_DARK_THEME"] = "is-dark-theme";
IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated";
IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script";
IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel";
return IPC_EVENTS2;
})(IPC_EVENTS || {});
const MAIN_WIN_SIZE = {
width: 1440,
height: 900,
minWidth: 1440,
minHeight: 900
};
var WINDOW_NAMES = /* @__PURE__ */ ((WINDOW_NAMES2) => {
WINDOW_NAMES2["MAIN"] = "main";
WINDOW_NAMES2["SETTING"] = "setting";
WINDOW_NAMES2["DIALOG"] = "dialog";
WINDOW_NAMES2["LOADING"] = "loading";
return WINDOW_NAMES2;
})(WINDOW_NAMES || {});
var CONFIG_KEYS = /* @__PURE__ */ ((CONFIG_KEYS2) => {
CONFIG_KEYS2["THEME_MODE"] = "themeMode";
CONFIG_KEYS2["PRIMARY_COLOR"] = "primaryColor";
CONFIG_KEYS2["LANGUAGE"] = "language";
CONFIG_KEYS2["FONT_SIZE"] = "fontSize";
CONFIG_KEYS2["MINIMIZE_TO_TRAY"] = "minimizeToTray";
CONFIG_KEYS2["PROVIDER"] = "provider";
CONFIG_KEYS2["DEFAULT_MODEL"] = "defaultModel";
return CONFIG_KEYS2;
})(CONFIG_KEYS || {});
var MENU_IDS = /* @__PURE__ */ ((MENU_IDS2) => {
MENU_IDS2["CONVERSATION_ITEM"] = "conversation-item";
MENU_IDS2["CONVERSATION_LIST"] = "conversation-list";
MENU_IDS2["MESSAGE_ITEM"] = "message-item";
return MENU_IDS2;
})(MENU_IDS || {});
var CONVERSATION_ITEM_MENU_IDS = /* @__PURE__ */ ((CONVERSATION_ITEM_MENU_IDS2) => {
CONVERSATION_ITEM_MENU_IDS2["PIN"] = "pin";
CONVERSATION_ITEM_MENU_IDS2["RENAME"] = "rename";
CONVERSATION_ITEM_MENU_IDS2["DEL"] = "del";
return CONVERSATION_ITEM_MENU_IDS2;
})(CONVERSATION_ITEM_MENU_IDS || {});
var CONVERSATION_LIST_MENU_IDS = /* @__PURE__ */ ((CONVERSATION_LIST_MENU_IDS2) => {
CONVERSATION_LIST_MENU_IDS2["NEW_CONVERSATION"] = "newConversation";
CONVERSATION_LIST_MENU_IDS2["SORT_BY"] = "sortBy";
CONVERSATION_LIST_MENU_IDS2["SORT_BY_CREATE_TIME"] = "sortByCreateTime";
CONVERSATION_LIST_MENU_IDS2["SORT_BY_UPDATE_TIME"] = "sortByUpdateTime";
CONVERSATION_LIST_MENU_IDS2["SORT_BY_NAME"] = "sortByName";
CONVERSATION_LIST_MENU_IDS2["SORT_BY_MODEL"] = "sortByModel";
CONVERSATION_LIST_MENU_IDS2["SORT_ASCENDING"] = "sortAscending";
CONVERSATION_LIST_MENU_IDS2["SORT_DESCENDING"] = "sortDescending";
CONVERSATION_LIST_MENU_IDS2["BATCH_OPERATIONS"] = "batchOperations";
return CONVERSATION_LIST_MENU_IDS2;
})(CONVERSATION_LIST_MENU_IDS || {});
var MESSAGE_ITEM_MENU_IDS = /* @__PURE__ */ ((MESSAGE_ITEM_MENU_IDS2) => {
MESSAGE_ITEM_MENU_IDS2["COPY"] = "copy";
MESSAGE_ITEM_MENU_IDS2["DELETE"] = "delete";
MESSAGE_ITEM_MENU_IDS2["SELECT"] = "select";
return MESSAGE_ITEM_MENU_IDS2;
})(MESSAGE_ITEM_MENU_IDS || {});
class BaseProvider {
}
const readdirAsync = util.promisify(fs__namespace.readdir);
const statAsync = util.promisify(fs__namespace.stat);
const unlinkAsync = util.promisify(fs__namespace.unlink);
class LogService {
static _instance;
// 日志保留天数默认7天
LOG_RETENTION_DAYS = 7;
// 清理间隔默认24小时毫秒
CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
constructor() {
const logPath = path__namespace.join(electron.app.getPath("userData"), "logs");
try {
if (!fs__namespace.existsSync(logPath)) {
fs__namespace.mkdirSync(logPath, { recursive: true });
}
} catch (err) {
this.error("Failed to create log directory:", err);
}
log.transports.file.resolvePathFn = () => {
const today = /* @__PURE__ */ new Date();
const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
return path__namespace.join(logPath, `${formattedDate}.log`);
};
log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}";
log.transports.file.maxSize = 10 * 1024 * 1024;
log.transports.console.level = process.env.NODE_ENV === "development" ? "debug" : "info";
log.transports.file.level = "debug";
this._setupIpcEvents();
this._rewriteConsole();
this.info("LogService initialized successfully.");
this._cleanupOldLogs();
setInterval(() => this._cleanupOldLogs(), this.CLEANUP_INTERVAL_MS);
}
_setupIpcEvents() {
electron.ipcMain.on(IPC_EVENTS.LOG_DEBUG, (_e, message, ...meta) => this.debug(message, ...meta));
electron.ipcMain.on(IPC_EVENTS.LOG_INFO, (_e, message, ...meta) => this.info(message, ...meta));
electron.ipcMain.on(IPC_EVENTS.LOG_WARN, (_e, message, ...meta) => this.warn(message, ...meta));
electron.ipcMain.on(IPC_EVENTS.LOG_ERROR, (_e, message, ...meta) => this.error(message, ...meta));
}
_rewriteConsole() {
console.debug = log.debug;
console.log = log.info;
console.info = log.info;
console.warn = log.warn;
console.error = log.error;
}
async _cleanupOldLogs() {
try {
const logPath = path__namespace.join(electron.app.getPath("userData"), "logs");
if (!fs__namespace.existsSync(logPath)) return;
const now = /* @__PURE__ */ new Date();
const expirationDate = new Date(now.getTime() - this.LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3);
const files = await readdirAsync(logPath);
let deletedCount = 0;
for (const file of files) {
if (!file.endsWith(".log")) continue;
const filePath = path__namespace.join(logPath, file);
try {
const stats = await statAsync(filePath);
if (stats.isFile() && stats.birthtime < expirationDate) {
await unlinkAsync(filePath);
deletedCount++;
}
} catch (error) {
this.error(`Failed to delete old log file ${filePath}:`, error);
}
}
if (deletedCount > 0) {
this.info(`Successfully cleaned up ${deletedCount} old log files.`);
}
} catch (err) {
this.error("Failed to cleanup old logs:", err);
}
}
static getInstance() {
if (!this._instance) {
this._instance = new LogService();
}
return this._instance;
}
/**
* 记录调试信息
* @param {string} message - 日志消息
* @param {any[]} meta - 附加的元数据
*/
debug(message, ...meta) {
log.debug(message, ...meta);
}
/**
* 记录一般信息
* @param {string} message - 日志消息
* @param {any[]} meta - 附加的元数据
*/
info(message, ...meta) {
log.info(message, ...meta);
}
/**
* 记录警告信息
* @param {string} message - 日志消息
* @param {any[]} meta - 附加的元数据
*/
warn(message, ...meta) {
log.warn(message, ...meta);
}
/**
* 记录错误信息
* @param {string} message - 日志消息
* @param {any[]} meta - 附加的元数据,通常是错误对象
*/
error(message, ...meta) {
log.error(message, ...meta);
}
logApiRequest(endpoint, data = {}, method = "POST") {
this.info(`API Request: ${endpoint}, Method: ${method}, Request: ${JSON.stringify(data)}`);
}
logApiResponse(endpoint, response = {}, statusCode = 200, responseTime = 0) {
if (statusCode >= 400) {
this.error(`API Error Response: ${endpoint}, Status: ${statusCode}, Response Time: ${responseTime}ms, Response: ${JSON.stringify(response)}`);
} else {
this.debug(`API Response: ${endpoint}, Status: ${statusCode}, Response Time: ${responseTime}ms, Response: ${JSON.stringify(response)}`);
}
}
logUserOperation(operation, userId = "unknown", details = {}) {
this.info(`User Operation: ${operation} by ${userId}, Details: ${JSON.stringify(details)}`);
}
}
const logManager = LogService.getInstance();
function _transformChunk(chunk) {
const choice = chunk.choices[0];
return {
isEnd: choice?.finish_reason === "stop",
result: choice?.delta?.content ?? ""
};
}
class OpenAIProvider extends BaseProvider {
client;
constructor(apiKey, baseURL) {
super();
this.client = new OpenAI({ apiKey, baseURL });
}
async chat(messages2, model) {
const startTime = Date.now();
const lastMessage = messages2[messages2.length - 1];
logManager.logApiRequest("chat.completions.create", {
model,
lastMessage: lastMessage?.content?.substring(0, 100) + (lastMessage?.content?.length > 100 ? "..." : ""),
messageCount: messages2.length
}, "POST");
try {
const chunks = await this.client.chat.completions.create({
model,
messages: messages2,
stream: true
});
const responseTime = Date.now() - startTime;
logManager.logApiResponse("chat.completions.create", { success: true }, 200, responseTime);
return {
async *[Symbol.asyncIterator]() {
for await (const chunk of chunks) {
yield _transformChunk(chunk);
}
}
};
} catch (error) {
const responseTime = Date.now() - startTime;
logManager.logApiResponse("chat.completions.create", { error: error instanceof Error ? error.message : String(error) }, 500, responseTime);
throw error;
}
}
}
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
function cloneDeep(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => cloneDeep(item));
}
const clone = Object.assign({}, obj);
for (const key in clone) {
if (Object.prototype.hasOwnProperty.call(clone, key)) {
clone[key] = cloneDeep(clone[key]);
}
}
return clone;
}
function simpleCloneDeep(obj) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("simpleCloneDeep failed:", error);
return obj;
}
}
function parseOpenAISetting(setting) {
try {
return JSON.parse(jsBase64.decode(setting));
} catch (error) {
console.error("parseOpenAISetting failed:", error);
return {};
}
}
const DEFAULT_CONFIG = {
[CONFIG_KEYS.THEME_MODE]: "system",
[CONFIG_KEYS.PRIMARY_COLOR]: "#BB5BE7",
[CONFIG_KEYS.LANGUAGE]: "zh",
[CONFIG_KEYS.FONT_SIZE]: 14,
[CONFIG_KEYS.MINIMIZE_TO_TRAY]: false,
[CONFIG_KEYS.PROVIDER]: "",
[CONFIG_KEYS.DEFAULT_MODEL]: null
};
class ConfigService {
static _instance;
_config;
_configPath;
_defaultConfig = DEFAULT_CONFIG;
_listeners = [];
constructor() {
this._configPath = path__namespace.join(electron.app.getPath("userData"), "config.json");
this._config = this._loadConfig();
this._setupIpcEvents();
logManager.info("ConfigService initialized successfully.");
}
_setupIpcEvents() {
const duration = 200;
const handelUpdate = debounce((val) => this.update(val), duration);
electron.ipcMain.handle(IPC_EVENTS.GET_CONFIG, (_, key) => this.get(key));
electron.ipcMain.on(IPC_EVENTS.SET_CONFIG, (_, key, val) => this.set(key, val));
electron.ipcMain.on(IPC_EVENTS.UPDATE_CONFIG, (_, updates) => handelUpdate(updates));
}
static getInstance() {
if (!this._instance) {
this._instance = new ConfigService();
}
return this._instance;
}
_loadConfig() {
try {
if (fs__namespace.existsSync(this._configPath)) {
const configContent = fs__namespace.readFileSync(this._configPath, "utf-8");
const config = { ...this._defaultConfig, ...JSON.parse(configContent) };
logManager.info("Config loaded successfully from:", this._configPath);
return config;
}
} catch (error) {
logManager.error("Failed to load config:", error);
}
return { ...this._defaultConfig };
}
_saveConfig() {
try {
fs__namespace.mkdirSync(path__namespace.dirname(this._configPath), { recursive: true });
fs__namespace.writeFileSync(this._configPath, JSON.stringify(this._config, null, 2), "utf-8");
this._notifyListeners();
logManager.info("Config saved successfully to:", this._configPath);
} catch (error) {
logManager.error("Failed to save config:", error);
}
}
_notifyListeners() {
electron.BrowserWindow.getAllWindows().forEach((win) => win.webContents.send(IPC_EVENTS.CONFIG_UPDATED, this._config));
this._listeners.forEach((listener) => listener({ ...this._config }));
}
getConfig() {
return simpleCloneDeep(this._config);
}
get(key) {
return this._config[key];
}
set(key, value, autoSave = true) {
if (!(key in this._config)) return;
const oldValue = this._config[key];
if (oldValue === value) return;
this._config[key] = value;
logManager.debug(`Config set: ${key} = ${value}`);
autoSave && this._saveConfig();
}
update(updates, autoSave = true) {
this._config = { ...this._config, ...updates };
autoSave && this._saveConfig();
}
resetToDefault() {
this._config = { ...this._defaultConfig };
logManager.info("Config reset to default.");
this._saveConfig();
}
onConfigChange(listener) {
this._listeners.push(listener);
return () => this._listeners = this._listeners.filter((l) => l !== listener);
}
}
const configManager = ConfigService.getInstance();
[
{
id: 1,
name: "bigmodel",
title: "智谱AI",
models: ["glm-4.5-flash"],
openAISetting: {
baseURL: "https://open.bigmodel.cn/api/paas/v4",
apiKey: process.env.BIGMODEL_API_KEY || ""
},
createdAt: (/* @__PURE__ */ new Date()).getTime(),
updatedAt: (/* @__PURE__ */ new Date()).getTime()
},
{
id: 2,
name: "deepseek",
title: "深度求索 (DeepSeek)",
models: ["deepseek-chat"],
openAISetting: {
baseURL: "https://api.deepseek.com/v1",
apiKey: process.env.DEEPSEEK_API_KEY || ""
},
createdAt: (/* @__PURE__ */ new Date()).getTime(),
updatedAt: (/* @__PURE__ */ new Date()).getTime()
},
{
id: 3,
name: "siliconflow",
title: "硅基流动",
models: ["Qwen/Qwen3-8B", "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B"],
openAISetting: {
baseURL: "https://api.siliconflow.cn/v1",
apiKey: process.env.SILICONFLOW_API_KEY || ""
},
createdAt: (/* @__PURE__ */ new Date()).getTime(),
updatedAt: (/* @__PURE__ */ new Date()).getTime()
},
{
id: 4,
name: "qianfan",
title: "百度千帆",
models: ["ernie-speed-128k", "ernie-4.0-8k", "ernie-3.5-8k"],
openAISetting: {
baseURL: "https://qianfan.baidubce.com/v2",
apiKey: process.env.QIANFAN_API_KEY || ""
},
createdAt: (/* @__PURE__ */ new Date()).getTime(),
updatedAt: (/* @__PURE__ */ new Date()).getTime()
}
];
const _parseProvider = () => {
let result = [];
let isBase64Parsed = false;
const providerConfig = configManager.get(CONFIG_KEYS.PROVIDER);
const mapCallback = (provider) => ({
...provider,
openAISetting: typeof provider.openAISetting === "string" ? parseOpenAISetting(provider.openAISetting ?? "") : provider.openAISetting
});
try {
result = JSON.parse(jsBase64.decode(providerConfig));
isBase64Parsed = true;
} catch (error) {
logManager.error(`parse base64 provider failed: ${error}`);
}
if (!isBase64Parsed) try {
result = JSON.parse(providerConfig);
} catch (error) {
logManager.error(`parse provider failed: ${error}`);
}
if (!result.length) return;
return result.map(mapCallback);
};
const getProviderConfig = () => {
try {
return _parseProvider();
} catch (error) {
logManager.error(`get provider config failed: ${error}`);
return null;
}
};
function createProvider(name) {
const providers2 = getProviderConfig();
if (!providers2) {
throw new Error("provider config not found");
}
for (const provider of providers2) {
if (provider.name === name) {
if (!provider.openAISetting?.apiKey || !provider.openAISetting?.baseURL) {
throw new Error("apiKey or baseURL not found");
}
return new OpenAIProvider(provider.openAISetting.apiKey, provider.openAISetting.baseURL);
}
}
}
const window$1 = { "minimize": "Minimize", "maximize": "Maximize", "restore": "Restore", "close": "Close" };
const main$1 = { "welcome": { "helloMessage": "Hello, I'm Diona" }, "conversation": { "placeholder": "Type a message...", "newConversation": "New Conversation", "selectModel": "Please select model", "createConversation": "Create Conversation", "searchPlaceholder": "Search conversations...", "goSettings": "Go to", "settings": "Settings Window", "addModel": "to add a model", "dialog": { "title": "Confirm Deletion", "content": "Are you sure you want to delete this conversation?", "content_1": "Are you sure you want to delete the selected conversations? This action cannot be undone." }, "operations": { "pin": "Pin Selected", "del": "Delete Selected", "selectAll": "Select All", "cancel": "Cancel" } }, "sidebar": { "conversations": "Conversations", "settings": "Settings", "help": "Help" }, "message": { "dialog": { "title": "Confirm Deletion", "messageDelete": "Are you sure you want to delete this message?", "batchDelete": "Are you sure you want to delete the selected messages?", "copySuccess": "Copied successfully" }, "batchActions": { "deleteSelected": "Delete Selected" }, "rendering": "Thinking...", "stoppedGeneration": "(Stopped generating)", "sending": "Sending", "stopGeneration": "Stop generating", "send": "Send" } };
const dialog$1 = { "cancel": "Cancel", "confirm": "Confirm" };
const settings$1 = { "title": "Settings", "base": "Basic Settings", "provider": { "modelConfig": "Model Configuration" }, "theme": { "label": "Theme Settings", "dark": "Dark Theme", "light": "Light Theme", "system": "System Theme", "primaryColor": "Primary Color" }, "appearance": { "fontSize": "Font Size", "fontSizeOptions": { "10": "Tiny (10px)", "12": "Small (12px)", "14": "Normal (14px)", "16": "Medium (16px)", "18": "Large (18px)", "20": "Larger (20px)", "24": "Extra Large (24px)" } }, "behavior": { "minimizeToTray": "Minimize to tray when closed" }, "language": { "label": "Language" }, "providers": { "defaultModel": "Default Model", "apiKey": "API Key", "apiUrl": "API URL" } };
const menu$1 = { "conversation": { "newConversation": "New Conversation", "sortBy": "Sort By", "sortByCreateTime": "Sort by Creation Time", "sortByUpdateTime": "Sort by Update Time", "sortByName": "Sort by Name", "sortByModel": "Sort by Model", "sortAscending": "Ascending", "sortDescending": "Descending", "pinConversation": "Pin Conversation", "unpinConversation": "Unpin Conversation", "renameConversation": "Rename Conversation", "delConversation": "Delete Conversation", "batchOperations": "Batch Operations" }, "message": { "copyMessage": "Copy Message", "deleteMessage": "Delete Message", "selectMessage": "Select Message" } };
const tray$1 = { "tooltip": "Diona Application", "showWindow": "Show Window", "exit": "Exit" };
const timeAgo$1 = { "justNow": "Just now", "minutes": "{count} minutes ago", "hours": "{count} hours ago", "days": "{count} days ago", "months": "{count} months ago", "years": "{count} years ago", "weekday": { "sun": "Sunday", "mon": "Monday", "tue": "Tuesday", "wed": "Wednesday", "thu": "Thursday", "fri": "Friday", "sat": "Saturday" } };
const app$1 = { "title": "Diona Application" };
const en = {
window: window$1,
main: main$1,
dialog: dialog$1,
settings: settings$1,
menu: menu$1,
tray: tray$1,
timeAgo: timeAgo$1,
app: app$1
};
const window = { "minimize": "最小化", "maximize": "最大化", "restore": "还原", "close": "关闭" };
const main = { "welcome": { "helloMessage": "你好,我是迪奥娜" }, "conversation": { "placeholder": "输入消息...", "newConversation": "新对话", "selectModel": "请选择模型", "createConversation": "创建对话", "searchPlaceholder": "搜索对话...", "goSettings": "快去", "settings": "设置窗口", "addModel": "添加模型", "dialog": { "title": "确认删除", "content": "确定要删除这个对话吗?", "content_1": "确定要删除选中的对话吗?此操作不可撤销。" }, "operations": { "pin": "置顶所选", "del": "删除所选", "selectAll": "全选", "cancel": "取消" } }, "sidebar": { "conversations": "对话", "settings": "设置", "help": "帮助" }, "message": { "dialog": { "title": "确认删除", "messageDelete": "确认删除该条消息?", "batchDelete": "确认删除选中的消息?", "copySuccess": "复制成功" }, "batchActions": { "deleteSelected": "删除选中项" }, "rendering": "思考中...", "stoppedGeneration": "(已停止生成)", "sending": "发送中", "stopGeneration": "停止生成", "send": "发送" } };
const dialog = { "cancel": "取消", "confirm": "确认" };
const settings = { "title": "设置", "base": "基础设置", "provider": { "modelConfig": "模型配置" }, "providers": { "defaultModel": "默认模型", "apiKey": "API密钥", "apiUrl": "API地址" }, "theme": { "label": "主题设置", "dark": "深色主题", "light": "浅色主题", "system": "跟随系统", "primaryColor": "主题颜色" }, "appearance": { "fontSize": "字体大小", "fontSizeOptions": { "10": "极小 (10px)", "12": "小 (12px)", "14": "正常 (14px)", "16": "中 (16px)", "18": "大 (18px)", "20": "较大 (20px)", "24": "超大 (24px)" } }, "behavior": { "minimizeToTray": "关闭时最小化到托盘" }, "language": { "label": "语言设置" } };
const menu = { "conversation": { "newConversation": "新建对话", "sortBy": "排序方式", "sortByCreateTime": "按创建时间排序", "sortByUpdateTime": "按更新时间排序", "sortByName": "按名称排序", "sortByModel": "按模型排序", "sortAscending": "递增", "sortDescending": "递减", "pinConversation": "置顶对话", "unpinConversation": "取消置顶", "renameConversation": "重命名对话", "delConversation": "删除对话", "batchOperations": "批量操作" }, "message": { "copyMessage": "复制消息", "deleteMessage": "删除消息", "selectMessage": "选择消息" } };
const tray = { "tooltip": "迪奥娜", "showWindow": "显示窗口", "exit": "退出" };
const timeAgo = { "justNow": "刚刚", "minutes": "{count}分钟前", "hours": "{count}小时前", "days": "{count}天前", "months": "{count}个月前", "years": "{count}年前", "weekday": { "sun": "星期日", "mon": "星期一", "tue": "星期二", "wed": "星期三", "thu": "星期四", "fri": "星期五", "sat": "星期六" } };
const app = { "title": "迪奥娜" };
const zh = {
window,
main,
dialog,
settings,
menu,
tray,
timeAgo,
app
};
const messages = { en, zh };
function createTranslator() {
return (key) => {
if (!key) return void 0;
try {
const keys = key?.split(".");
let result = messages[configManager.get(CONFIG_KEYS.LANGUAGE)];
for (const _key of keys) {
result = result[_key];
}
return result;
} catch (e) {
logManager.error("failed to translate key:", key, e);
return key;
}
};
}
let logo = void 0;
function createLogo() {
if (logo != null) {
return logo;
}
const appPath = electron.app.getAppPath();
const iconPath = path$1.join(appPath, "resources", "icons", "icon.ico");
logo = iconPath;
return logo;
}
class ThemeService {
static _instance;
_isDark = electron.nativeTheme.shouldUseDarkColors;
constructor() {
const themeMode = configManager.get(CONFIG_KEYS.THEME_MODE);
if (themeMode) {
electron.nativeTheme.themeSource = themeMode;
this._isDark = electron.nativeTheme.shouldUseDarkColors;
}
this._setupIpcEvent();
logManager.info("ThemeService initialized successfully.");
}
_setupIpcEvent() {
electron.ipcMain.handle(IPC_EVENTS.SET_THEME_MODE, (_e, mode) => {
electron.nativeTheme.themeSource = mode;
configManager.set(CONFIG_KEYS.THEME_MODE, mode);
return electron.nativeTheme.shouldUseDarkColors;
});
electron.ipcMain.handle(IPC_EVENTS.GET_THEME_MODE, () => {
return electron.nativeTheme.themeSource;
});
electron.ipcMain.handle(IPC_EVENTS.IS_DARK_THEME, () => {
return electron.nativeTheme.shouldUseDarkColors;
});
electron.nativeTheme.on("updated", () => {
this._isDark = electron.nativeTheme.shouldUseDarkColors;
electron.BrowserWindow.getAllWindows().forEach(
(win) => win.webContents.send(IPC_EVENTS.THEME_MODE_UPDATED, this._isDark)
);
});
}
static getInstance() {
if (!this._instance) {
this._instance = new ThemeService();
}
return this._instance;
}
get isDark() {
return this._isDark;
}
get themeMode() {
return electron.nativeTheme.themeSource;
}
}
const themeManager = ThemeService.getInstance();
const SHARED_WINDOW_OPTIONS = {
frame: false,
titleBarStyle: "hidden",
trafficLightPosition: { x: -100, y: -100 },
show: false,
title: "NIANXX",
darkTheme: themeManager.isDark,
backgroundColor: themeManager.isDark ? "#2C2C2C" : "#FFFFFF",
webPreferences: {
nodeIntegration: false,
// 禁用 Node.js 集成,提高安全性
contextIsolation: true,
// 启用上下文隔离,防止渲染进程访问主进程 API
sandbox: true,
// 启用沙箱模式,进一步增强安全性
backgroundThrottling: false,
preload: path$1.join(process.cwd(), "dist-electron/preload/preload.js")
}
};
class WindowService {
static _instance;
_logo = createLogo();
isDev = true;
_winStates = {
main: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] },
setting: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] },
dialog: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] },
login: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] },
loading: { instance: void 0, isHidden: false, onCreate: [], onClosed: [] }
};
constructor() {
this._setupIpcEvents();
logManager.info("WindowService initialized successfully.");
}
_isReallyClose(windowName) {
if (windowName === WINDOW_NAMES.MAIN) return configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY) === false;
if (windowName === WINDOW_NAMES.SETTING) return false;
return true;
}
_setupIpcEvents() {
const handleCloseWindow = (e) => {
const target = electron.BrowserWindow.fromWebContents(e.sender);
const winName = this.getName(target);
this.close(target, this._isReallyClose(winName));
};
const handleMinimizeWindow = (e) => {
electron.BrowserWindow.fromWebContents(e.sender)?.minimize();
};
const handleMaximizeWindow = (e) => {
this.toggleMax(electron.BrowserWindow.fromWebContents(e.sender));
};
const handleIsWindowMaximized = (e) => {
return electron.BrowserWindow.fromWebContents(e.sender)?.isMaximized() ?? false;
};
electron.ipcMain.on(IPC_EVENTS.WINDOW_CLOSE, handleCloseWindow);
electron.ipcMain.on(IPC_EVENTS.WINDOW_MINIMIZE, handleMinimizeWindow);
electron.ipcMain.on(IPC_EVENTS.WINDOW_MAXIMIZE, handleMaximizeWindow);
electron.ipcMain.handle(IPC_EVENTS.IS_WINDOW_MAXIMIZED, handleIsWindowMaximized);
electron.ipcMain.handle(IPC_EVENTS.APP_LOAD_PAGE, (e, page) => {
const win = electron.BrowserWindow.fromWebContents(e.sender);
if (win) this._loadPage(win, page);
});
}
static getInstance() {
if (!this._instance) {
this._instance = new WindowService();
}
return this._instance;
}
create(name, size, moreOpts) {
if (this.get(name)) return;
const isHiddenWin = this._isHiddenWin(name);
let window2 = this._createWinInstance(name, { ...size, ...moreOpts });
if (this.isDev) window2.webContents.openDevTools();
!isHiddenWin && this._setupWinLifecycle(window2, name)._loadWindowTemplate(window2, name);
this._listenWinReady({
win: window2,
isHiddenWin,
size
});
if (!isHiddenWin) {
this._winStates[name].instance = window2;
this._winStates[name].onCreate.forEach((callback) => callback(window2));
}
if (isHiddenWin) {
this._winStates[name].isHidden = false;
logManager.info(`Hidden window show: ${name}`);
}
return window2;
}
_setupWinLifecycle(window2, name) {
const updateWinStatus = debounce(() => !window2?.isDestroyed() && window2?.webContents?.send(IPC_EVENTS.WINDOW_MAXIMIZE + "back", window2?.isMaximized()), 80);
window2.once("closed", () => {
this._winStates[name].onClosed.forEach((callback) => callback(window2));
window2?.destroy();
window2?.removeListener("resize", updateWinStatus);
this._winStates[name].instance = void 0;
this._winStates[name].isHidden = false;
logManager.info(`Window closed: ${name}`);
});
window2.on("resize", updateWinStatus);
return this;
}
_listenWinReady(params) {
const onReady = () => {
params.win?.once("show", () => setTimeout(() => this._applySizeConstraints(params.win, params.size), 2));
params.win?.show();
};
if (!params.isHiddenWin) {
const loadingHandler = this._addLoadingView(params.win, params.size);
loadingHandler?.(onReady);
} else {
onReady();
}
}
_addLoadingView(window2, size) {
let rendererIsReady = false;
const onRendererIsReady = (e) => {
if (e.sender !== window2?.webContents || rendererIsReady) return;
rendererIsReady = true;
electron.ipcMain.removeListener(IPC_EVENTS.RENDERER_IS_READY, onRendererIsReady);
};
electron.ipcMain.on(IPC_EVENTS.RENDERER_IS_READY, onRendererIsReady);
return (cb) => {
cb();
};
}
_applySizeConstraints(win, size) {
if (size.maxHeight && size.maxWidth) {
win.setMaximumSize(size.maxWidth, size.maxHeight);
}
if (size.minHeight && size.minWidth) {
win.setMinimumSize(size.minWidth, size.minHeight);
}
}
_loadPage(window2, pageName) {
{
return window2.loadURL(`${"http://localhost:5173"}/${pageName}.html`);
}
}
_loadWindowTemplate(window2, name) {
const page = "index";
this._loadPage(window2, page);
}
_handleCloseWindowState(target, really) {
const name = this.getName(target);
if (name) {
if (!really) this._winStates[name].isHidden = true;
else this._winStates[name].instance = void 0;
}
setTimeout(() => {
target[really ? "close" : "hide"]?.();
this._checkAndCloseAllWinodws();
}, 210);
}
_checkAndCloseAllWinodws() {
if (!this._winStates[WINDOW_NAMES.MAIN].instance || this._winStates[WINDOW_NAMES.MAIN].instance?.isDestroyed())
return Object.values(this._winStates).forEach((win) => win?.instance?.close());
const minimizeToTray = configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY);
if (!minimizeToTray && !this.get(WINDOW_NAMES.MAIN)?.isVisible())
return Object.values(this._winStates).forEach((win) => !win?.instance?.isVisible() && win?.instance?.close());
}
_isHiddenWin(name) {
return this._winStates[name] && this._winStates[name].isHidden;
}
_createWinInstance(name, opts) {
return this._isHiddenWin(name) ? this._winStates[name].instance : new electron.BrowserWindow({
...SHARED_WINDOW_OPTIONS,
icon: this._logo,
...opts
});
}
focus(target) {
if (!target) return;
const name = this.getName(target);
if (target?.isMaximized()) {
target?.restore();
logManager.debug(`Window ${name} restored and focused`);
} else {
logManager.debug(`Window ${name} focused`);
}
target?.focus();
}
close(target, really = true) {
if (!target) return;
const name = this.getName(target);
logManager.info(`Close window: ${name}, really: ${really}`);
this._handleCloseWindowState(target, really);
}
toggleMax(target) {
if (!target) return;
target.isMaximized() ? target.unmaximize() : target.maximize();
}
getName(target) {
if (!target) return;
for (const [name, win] of Object.entries(this._winStates)) {
if (win?.instance === target) return name;
}
}
get(name) {
if (this._winStates[name].isHidden) return void 0;
return this._winStates[name].instance;
}
onWindowCreate(name, callback) {
this._winStates[name].onCreate.push(callback);
}
onWindowClosed(name, callback) {
this._winStates[name].onClosed.push(callback);
}
}
const windowManager = WindowService.getInstance();
let t$1 = createTranslator();
class MenuService {
static _instance;
_menuTemplates = /* @__PURE__ */ new Map();
_currentMenu = void 0;
constructor() {
this._setupIpcListener();
this._setupLanguageChangeListener();
logManager.info("MenuService initialized successfully.");
}
_setupIpcListener() {
electron.ipcMain.handle(IPC_EVENTS.SHOW_CONTEXT_MENU, (_, menuId, dynamicOptions) => new Promise((resolve) => this.showMenu(menuId, () => resolve(true), dynamicOptions)));
}
_setupLanguageChangeListener() {
configManager.onConfigChange((config) => {
if (!config[CONFIG_KEYS.LANGUAGE]) return;
t$1 = createTranslator();
});
}
static getInstance() {
if (!this._instance)
this._instance = new MenuService();
return this._instance;
}
register(menuId, template) {
this._menuTemplates.set(menuId, template);
return menuId;
}
showMenu(menuId, onClose, dynamicOptions) {
if (this._currentMenu) return;
const template = cloneDeep(this._menuTemplates.get(menuId));
if (!template) {
logManager.warn(`Menu ${menuId} not found.`);
onClose?.();
return;
}
let _dynamicOptions = [];
try {
_dynamicOptions = Array.isArray(dynamicOptions) ? dynamicOptions : JSON.parse(dynamicOptions ?? "[]");
} catch (error) {
logManager.error(`Failed to parse dynamicOptions for menu ${menuId}: ${error}`);
}
const translationItem = (item) => {
if (item.submenu) {
return {
...item,
label: t$1(item?.label) ?? void 0,
submenu: item.submenu?.map((item2) => translationItem(item2))
};
}
return {
...item,
label: t$1(item?.label) ?? void 0
};
};
const localizedTemplate = template.map((item) => {
if (!Array.isArray(_dynamicOptions) || !_dynamicOptions.length) {
return translationItem(item);
}
const dynamicItem = _dynamicOptions.find((_item) => _item.id === item.id);
if (dynamicItem) {
const mergedItem = { ...item, ...dynamicItem };
return translationItem(mergedItem);
}
if (item.submenu) {
return translationItem({
...item,
submenu: item.submenu?.map((__item) => {
const dynamicItem2 = _dynamicOptions.find((_item) => _item.id === __item.id);
return { ...__item, ...dynamicItem2 };
})
});
}
return translationItem(item);
});
const menu2 = electron.Menu.buildFromTemplate(localizedTemplate);
this._currentMenu = menu2;
menu2.popup({
callback: () => {
this._currentMenu = void 0;
onClose?.();
}
});
}
destroyMenu(menuId) {
this._menuTemplates.delete(menuId);
}
destroyed() {
this._menuTemplates.clear();
this._currentMenu = void 0;
}
}
const menuManager = MenuService.getInstance();
let t = createTranslator();
class TrayService {
static _instance;
_tray = null;
_removeLanguageListener;
_setupLanguageChangeListener() {
this._removeLanguageListener = configManager.onConfigChange((config) => {
if (!config[CONFIG_KEYS.LANGUAGE]) return;
t = createTranslator();
if (this._tray) {
this._updateTray();
}
});
}
_updateTray() {
if (!this._tray) {
this._tray = new electron.Tray(createLogo());
}
const showWindow = () => {
const mainWindow = windowManager.get(WINDOW_NAMES.MAIN);
if (mainWindow && !mainWindow?.isDestroyed() && mainWindow?.isVisible() && !mainWindow?.isFocused()) {
return mainWindow.focus();
}
if (mainWindow?.isMinimized()) {
return mainWindow?.restore();
}
if (mainWindow?.isVisible() && mainWindow?.isFocused()) return;
windowManager.create(WINDOW_NAMES.MAIN, MAIN_WIN_SIZE);
};
this._tray.setToolTip(t("tray.tooltip") ?? "Diona Application");
this._tray.setContextMenu(electron.Menu.buildFromTemplate([
{ label: t("tray.showWindow"), accelerator: "CmdOrCtrl+N", click: showWindow },
{ type: "separator" },
{ label: t("settings.title"), click: () => electron.ipcMain.emit(`${IPC_EVENTS.OPEN_WINDOW}:${WINDOW_NAMES.SETTING}`) },
{ role: "quit", label: t("tray.exit") }
]));
this._tray.removeAllListeners("click");
this._tray.on("click", showWindow);
}
constructor() {
this._setupLanguageChangeListener();
logManager.info("TrayService initialized successfully.");
}
static getInstance() {
if (!this._instance) {
this._instance = new TrayService();
}
return this._instance;
}
create() {
if (this._tray) return;
this._updateTray();
electron.app.on("quit", () => {
this.destroy();
});
}
destroy() {
this._tray?.destroy();
this._tray = null;
if (this._removeLanguageListener) {
this._removeLanguageListener();
this._removeLanguageListener = void 0;
}
}
}
const trayManager = TrayService.getInstance();
class TabManager {
win;
views = /* @__PURE__ */ new Map();
activeId = null;
skipNextNavigate = /* @__PURE__ */ new Map();
enabled = false;
constructor(win) {
this.win = win;
this.win.on("resize", () => this.updateActiveBounds());
this._setupIpcEvents();
}
_setupIpcEvents() {
electron.ipcMain.handle(IPC_EVENTS.TAB_CREATE, (_e, url) => {
const info = this.create(url);
return info;
});
electron.ipcMain.handle(IPC_EVENTS.TAB_LIST, () => {
return this.list();
});
electron.ipcMain.handle(IPC_EVENTS.TAB_NAVIGATE, (_e, { tabId, url }) => {
this.navigate(tabId, url);
});
electron.ipcMain.handle(IPC_EVENTS.TAB_RELOAD, (_e, tabId) => {
this.reload(tabId);
});
electron.ipcMain.handle(IPC_EVENTS.TAB_BACK, (_e, tabId) => {
this.goBack(tabId);
});
electron.ipcMain.handle(IPC_EVENTS.TAB_FORWARD, (_e, tabId) => {
this.goForward(tabId);
});
electron.ipcMain.handle(IPC_EVENTS.TAB_SWITCH, (_e, tabId) => {
this.switch(tabId);
});
electron.ipcMain.handle(IPC_EVENTS.TAB_CLOSE, (_e, tabId) => {
this.close(tabId);
});
}
enable() {
this.enabled = true;
this.updateActiveBounds();
if (this.activeId) this.attach(this.activeId);
}
disable() {
this.enabled = false;
const view = this.activeId ? this.views.get(this.activeId) : null;
if (view) this.win.removeBrowserView(view);
}
destroy() {
this.disable();
this.views.forEach((view) => {
view.webContents.destroy();
});
this.views.clear();
electron.ipcMain.removeHandler(IPC_EVENTS.TAB_CREATE);
electron.ipcMain.removeHandler(IPC_EVENTS.TAB_LIST);
electron.ipcMain.removeHandler(IPC_EVENTS.TAB_NAVIGATE);
electron.ipcMain.removeHandler(IPC_EVENTS.TAB_RELOAD);
electron.ipcMain.removeHandler(IPC_EVENTS.TAB_BACK);
electron.ipcMain.removeHandler(IPC_EVENTS.TAB_FORWARD);
electron.ipcMain.removeHandler(IPC_EVENTS.TAB_SWITCH);
electron.ipcMain.removeHandler(IPC_EVENTS.TAB_CLOSE);
}
list() {
return Array.from(this.views.entries()).map(([id, view]) => this.info(id, view));
}
create(url, active = true) {
const id = crypto.randomUUID();
const view = new electron.BrowserView({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
preload: path$1.join(process.cwd(), "dist-electron/preload/preload.js")
}
});
this.views.set(id, view);
if (this.enabled && active) this.attach(id);
const target = url && url.length > 0 ? url : "about:blank";
view.webContents.loadURL(target);
this.bindEvents(id, view);
const info = this.info(id, view);
this.win.webContents.send("tab-created", info);
return info;
}
switch(tabId) {
if (!this.views.has(tabId)) return;
if (this.enabled) this.attach(tabId);
this.win.webContents.send("tab-switched", { tabId });
}
close(tabId) {
const view = this.views.get(tabId);
if (!view) return;
if (this.activeId === tabId) {
this.win.removeBrowserView(view);
this.activeId = null;
}
view.webContents.destroy();
this.views.delete(tabId);
this.win.webContents.send("tab-closed", { tabId });
const next = this.views.keys().next().value;
if (next) this.switch(next);
}
navigate(tabId, url) {
const view = this.views.get(tabId);
if (!view) return;
this.skipNextNavigate.set(tabId, true);
view.webContents.loadURL(url);
}
reload(tabId) {
const view = this.views.get(tabId);
if (!view) return;
view.webContents.reload();
}
goBack(tabId) {
const view = this.views.get(tabId);
if (!view) return;
if (view.webContents.canGoBack()) view.webContents.goBack();
}
goForward(tabId) {
const view = this.views.get(tabId);
if (!view) return;
if (view.webContents.canGoForward()) view.webContents.goForward();
}
attach(tabId) {
if (!this.enabled) return;
const view = this.views.get(tabId);
if (!view) return;
if (this.activeId && this.views.get(this.activeId)) {
const prev = this.views.get(this.activeId);
this.win.removeBrowserView(prev);
}
this.activeId = tabId;
this.win.addBrowserView(view);
this.updateActiveBounds();
}
updateActiveBounds() {
if (!this.enabled || !this.activeId) return;
const view = this.views.get(this.activeId);
if (!view) return;
const [winWidth, winHeight] = this.win.getContentSize();
const HEADER_HEIGHT = 88;
const PADDING = 8;
const RIGHT_PANEL_WIDTH = 392 + 80 + 8 + 8;
const x = PADDING;
const y = HEADER_HEIGHT + PADDING;
const width = winWidth - RIGHT_PANEL_WIDTH - PADDING;
const height = winHeight - HEADER_HEIGHT - PADDING * 2;
view.setBounds({
x,
y,
width: Math.max(0, width),
height: Math.max(0, height)
});
}
bindEvents(id, view) {
const send = () => this.win.webContents.send("tab-updated", this.info(id, view));
view.webContents.on("did-start-loading", send);
view.webContents.on("did-stop-loading", send);
view.webContents.on("did-finish-load", send);
view.webContents.on("page-title-updated", send);
view.webContents.on("did-navigate", send);
view.webContents.on("did-navigate-in-page", send);
view.webContents.on("will-navigate", (event, url) => {
if (this.skipNextNavigate.get(id)) {
this.skipNextNavigate.set(id, false);
return;
}
event.preventDefault();
this.create(url);
});
view.webContents.setWindowOpenHandler(({ url }) => {
this.create(url);
return { action: "deny" };
});
}
info(id, view) {
const wc = view.webContents;
return {
id,
url: wc.getURL(),
title: wc.getTitle(),
isLoading: wc.isLoading(),
canGoBack: wc.canGoBack(),
canGoForward: wc.canGoForward()
};
}
}
const handleTray = (minimizeToTray) => {
if (minimizeToTray) {
trayManager.create();
return;
}
trayManager.destroy();
};
const registerMenus = (window2) => {
const conversationItemMenuItemClick = (id) => {
logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_ITEM}-${id}`);
window2.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_ITEM}`, id);
};
menuManager.register(MENU_IDS.CONVERSATION_ITEM, [
{
id: CONVERSATION_ITEM_MENU_IDS.PIN,
label: "menu.conversation.pinConversation",
click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.PIN)
},
{
id: CONVERSATION_ITEM_MENU_IDS.RENAME,
label: "menu.conversation.renameConversation",
click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.RENAME)
},
{
id: CONVERSATION_ITEM_MENU_IDS.DEL,
label: "menu.conversation.delConversation",
click: () => conversationItemMenuItemClick(CONVERSATION_ITEM_MENU_IDS.DEL)
}
]);
const conversationListMenuItemClick = (id) => {
logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_LIST}-${id}`);
window2.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.CONVERSATION_LIST}`, id);
};
menuManager.register(MENU_IDS.CONVERSATION_LIST, [
{
id: CONVERSATION_LIST_MENU_IDS.NEW_CONVERSATION,
label: "menu.conversation.newConversation",
click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.NEW_CONVERSATION)
},
{ type: "separator" },
{
id: CONVERSATION_LIST_MENU_IDS.SORT_BY,
label: "menu.conversation.sortBy",
submenu: [
{ id: CONVERSATION_LIST_MENU_IDS.SORT_BY_CREATE_TIME, label: "menu.conversation.sortByCreateTime", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_CREATE_TIME) },
{ id: CONVERSATION_LIST_MENU_IDS.SORT_BY_UPDATE_TIME, label: "menu.conversation.sortByUpdateTime", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_UPDATE_TIME) },
{ id: CONVERSATION_LIST_MENU_IDS.SORT_BY_NAME, label: "menu.conversation.sortByName", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_NAME) },
{ id: CONVERSATION_LIST_MENU_IDS.SORT_BY_MODEL, label: "menu.conversation.sortByModel", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_BY_MODEL) },
{ type: "separator" },
{ id: CONVERSATION_LIST_MENU_IDS.SORT_ASCENDING, label: "menu.conversation.sortAscending", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_ASCENDING) },
{ id: CONVERSATION_LIST_MENU_IDS.SORT_DESCENDING, label: "menu.conversation.sortDescending", type: "radio", checked: false, click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.SORT_DESCENDING) }
]
},
{
id: CONVERSATION_LIST_MENU_IDS.BATCH_OPERATIONS,
label: "menu.conversation.batchOperations",
click: () => conversationListMenuItemClick(CONVERSATION_LIST_MENU_IDS.BATCH_OPERATIONS)
}
]);
const messageItemMenuItemClick = (id) => {
logManager.logUserOperation(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.MESSAGE_ITEM}-${id}`);
window2.webContents.send(`${IPC_EVENTS.SHOW_CONTEXT_MENU}:${MENU_IDS.MESSAGE_ITEM}`, id);
};
menuManager.register(MENU_IDS.MESSAGE_ITEM, [
{
id: MESSAGE_ITEM_MENU_IDS.COPY,
label: "menu.message.copyMessage",
click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.COPY)
},
{
id: MESSAGE_ITEM_MENU_IDS.SELECT,
label: "menu.message.selectMessage",
click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.SELECT)
},
{ type: "separator" },
{
id: MESSAGE_ITEM_MENU_IDS.DELETE,
label: "menu.message.deleteMessage",
click: () => messageItemMenuItemClick(MESSAGE_ITEM_MENU_IDS.DELETE)
}
]);
};
function setupMainWindow() {
windowManager.onWindowCreate(WINDOW_NAMES.MAIN, (mainWindow) => {
let minimizeToTray = configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY);
configManager.onConfigChange((config) => {
if (minimizeToTray === config[CONFIG_KEYS.MINIMIZE_TO_TRAY]) return;
minimizeToTray = config[CONFIG_KEYS.MINIMIZE_TO_TRAY];
handleTray(minimizeToTray);
});
handleTray(minimizeToTray);
registerMenus(mainWindow);
const tabManager = new TabManager(mainWindow);
tabManager.enable();
mainWindow.on("closed", () => {
tabManager.destroy();
});
});
windowManager.create(WINDOW_NAMES.MAIN, MAIN_WIN_SIZE);
electron.ipcMain.on(IPC_EVENTS.START_A_DIALOGUE, async (_event, props) => {
const { providerName, messages: messages2, messageId, selectedModel } = props;
const mainWindow = windowManager.get(WINDOW_NAMES.MAIN);
if (!mainWindow) {
throw new Error("mainWindow not found");
}
try {
const provider = createProvider(providerName);
const chunks = await provider?.chat(messages2, selectedModel);
if (!chunks) {
throw new Error("chunks or stream not found");
}
for await (const chunk of chunks) {
const chunkContent = {
messageId,
data: chunk
};
mainWindow.webContents.send(IPC_EVENTS.START_A_DIALOGUE + "back" + messageId, chunkContent);
}
} catch (error) {
const errorContent = {
messageId,
data: {
isEnd: true,
isError: true,
result: error instanceof Error ? error.message : String(error)
}
};
mainWindow.webContents.send(IPC_EVENTS.START_A_DIALOGUE + "back" + messageId, errorContent);
}
});
}
function getChromePath() {
if (process.platform === "win32") {
return "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
}
if (process.platform === "darwin") {
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
}
if (process.platform === "linux") {
return "google-chrome";
}
}
function getProfileDir(accountId) {
return path$1.join(electron.app.getPath("userData"), `profiles`, accountId);
}
function isPortInUse(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once("error", (err) => resolve(true));
server.once("listening", () => {
server.close();
resolve(false);
});
server.listen(port);
});
}
async function isChromeRunning() {
try {
return new Promise((resolve) => {
const req = http.get("http://127.0.0.1:9222/json/version", (res) => {
resolve(res.statusCode === 200);
});
req.on("error", () => resolve(false));
req.setTimeout(1e3, () => {
req.destroy();
resolve(false);
});
});
} catch (error) {
return false;
}
}
async function launchLocalChrome() {
const chromePath = getChromePath();
const profileDir = getProfileDir("default");
log.info(`Launching Chrome with user data dir: ${profileDir}`);
const portInUse = await isPortInUse(9222);
if (portInUse) {
log.info("Chrome already running on port 9222, skip launching.");
return;
}
if (await isChromeRunning()) {
log.info("Chrome already running, skip launching.");
return;
}
return new Promise((resolve, reject) => {
const chromeProcess = child_process.spawn(chromePath, [
"--remote-debugging-port=9222",
"--window-size=1920,1080",
"--window-position=0,0",
"--no-first-run",
`--user-data-dir=${profileDir}`,
"--no-default-browser-check",
"about:blank"
// '--window-maximized',
], {
detached: true,
stdio: "ignore"
});
chromeProcess.on("error", reject);
setTimeout(() => {
resolve(0);
}, 1e3);
});
}
class executeScriptService extends events.EventEmitter {
// 执行脚本
async executeScript(scriptPath, options) {
const MAX_TAIL = 32 * 1024;
const appendTail = (current, chunk) => {
const next = current + chunk;
return next.length > MAX_TAIL ? next.slice(next.length - MAX_TAIL) : next;
};
return await new Promise((resolve) => {
try {
const roomType = options?.roomType ?? "";
const startTime = options?.startTime ?? "";
const endTime = options?.endTime ?? "";
const operation = options?.operation ?? "";
const tabIndex = options?.tabIndex ?? "";
const channels = options?.channels ?? "";
const startTabIndex = options?.startTabIndex ?? "";
const child = electron.utilityProcess.fork(scriptPath, [], {
env: {
...process.env,
ROOM_TYPE: String(roomType),
START_DATE: String(startTime),
END_DATE: String(endTime),
OPERATION: String(operation),
TAB_INDEX: String(tabIndex),
CHANNELS: typeof channels === "string" ? channels : JSON.stringify(channels),
START_TAB_INDEX: String(startTabIndex)
},
stdio: "pipe"
});
let stdoutTail = "";
let stderrTail = "";
if (child.stdout) {
child.stdout.on("data", (data) => {
const text = data.toString();
stdoutTail = appendTail(stdoutTail, text);
log.info(`stdout: ${text}`);
});
}
if (child.stderr) {
child.stderr.on("data", (data) => {
const text = data.toString();
stderrTail = appendTail(stderrTail, text);
log.info(`stderr: ${text}`);
});
}
child.on("exit", (code) => {
log.info(`子进程退出,退出码 ${code}`);
resolve({
success: code === 0,
exitCode: code,
stdoutTail,
stderrTail,
...code === 0 ? {} : { error: `Script exited with code ${code}` }
});
});
} catch (error) {
resolve({
success: false,
exitCode: null,
stdoutTail: "",
stderrTail: "",
error: error?.message || "运行 Node 脚本时出错"
});
}
});
}
}
const openedTabIndexByChannelName = /* @__PURE__ */ new Map();
function getScriptsDir() {
return electron.app.isPackaged ? path.join(__dirname, "scripts") : path.join(process.cwd(), "electron/scripts");
}
function runTaskOperationService() {
const executeScriptServiceInstance = new executeScriptService();
electron.ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels) => {
try {
await launchLocalChrome();
const scriptsDir = getScriptsDir();
const scriptPath = path.join(scriptsDir, "open_all_channel.js");
openedTabIndexByChannelName.clear();
if (Array.isArray(channels)) {
for (let i = 0; i < channels.length; i++) {
const name = channels[i]?.channelName;
if (name) openedTabIndexByChannelName.set(String(name), i);
}
}
const result = await executeScriptServiceInstance.executeScript(scriptPath, { channels });
return { success: true, result };
} catch (error) {
return { success: false, error: error?.message || "open channel failed" };
}
});
electron.ipcMain.handle(IPC_EVENTS.EXECUTE_SCRIPT, async (_event, options) => {
try {
const roomType = options.roomList.find((item) => item.id === options.roomType);
const pairs = [
["fzName", "fg_trace.js"],
["mtName", "mt_trace.js"],
["dyHotelName", "dy_hotel_trace.js"],
["dyHotSpringName", "dy_hot_spring_trace.js"]
];
const scriptEntries = pairs.filter(([prop]) => roomType?.[prop]);
const scriptsDir = getScriptsDir();
const scriptPaths = scriptEntries.map(([channel, fileName]) => {
const p = path.join(scriptsDir, fileName);
if (!fs.existsSync(p)) {
throw new Error(`Script not found for channel ${channel}: ${p}`);
}
return { channel, scriptPath: p };
});
const results = [];
for (let i = 0; i < scriptPaths.length; i++) {
const item = scriptPaths[i];
const channelNameMap = {
fzName: "fliggy",
mtName: "meituan",
dyHotelName: "douyin",
dyHotSpringName: "douyin"
};
const defaultTabIndexMap = {
fliggy: 0,
meituan: 1,
douyin: 2
};
const mappedName = channelNameMap[item.channel];
const tabIndex = mappedName ? openedTabIndexByChannelName.get(mappedName) ?? defaultTabIndexMap[mappedName] ?? i : i;
log.info(`Launching script for channel ${item.channel}: ${item.scriptPath} (tabIndex: ${tabIndex})`);
const result = await executeScriptServiceInstance.executeScript(item.scriptPath, {
roomType: roomType[item.channel],
startTime: options.startTime,
endTime: options.endTime,
operation: options.operation,
tabIndex
});
results.push({
channel: item.channel,
scriptPath: item.scriptPath,
...result
});
}
return { success: true, result: results };
} catch (error) {
return { success: false, error: error.message };
}
});
}
if (started) {
electron.app.quit();
}
electron.app.whenReady().then(() => {
setupMainWindow();
runTaskOperationService();
});
electron.app.on("window-all-closed", () => {
if (process.platform !== "darwin" && !configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY)) {
log.info("app closing due to all windows being closed");
electron.app.quit();
}
});
electron.app.on("activate", () => {
if (electron.BrowserWindow.getAllWindows().length === 0) {
setupMainWindow();
}
});