2555 lines
94 KiB
JavaScript
2555 lines
94 KiB
JavaScript
"use strict";
|
||
var __create = Object.create;
|
||
var __defProp = Object.defineProperty;
|
||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
var __getProtoOf = Object.getPrototypeOf;
|
||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||
var __copyProps = (to, from, except, desc) => {
|
||
if (from && typeof from === "object" || typeof from === "function") {
|
||
for (let key of __getOwnPropNames(from))
|
||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||
}
|
||
return to;
|
||
};
|
||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||
// If the importer is in node compatibility mode or this is not an ESM
|
||
// file that has been converted to a CommonJS file using a Babel-
|
||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||
mod
|
||
));
|
||
const electron = require("electron");
|
||
require("js-base64");
|
||
const util = require("util");
|
||
const log = require("electron-log");
|
||
const path = require("path");
|
||
const fs = require("fs");
|
||
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");
|
||
const electronUpdater = require("electron-updater");
|
||
const axios = require("axios");
|
||
const OpenAI = require("openai");
|
||
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";
|
||
IPC_EVENTS2["SCRIPT_LIST"] = "script:list";
|
||
IPC_EVENTS2["SCRIPT_GET"] = "script:get";
|
||
IPC_EVENTS2["SCRIPT_SAVE"] = "script:save";
|
||
IPC_EVENTS2["SCRIPT_DELETE"] = "script:delete";
|
||
IPC_EVENTS2["SCRIPT_TOGGLE"] = "script:toggle";
|
||
IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
|
||
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
|
||
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
|
||
IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen";
|
||
IPC_EVENTS2["GATEWAY_RPC"] = "gateway:rpc";
|
||
IPC_EVENTS2["GATEWAY_EVENT"] = "gateway:event";
|
||
IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
|
||
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
|
||
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
|
||
IPC_EVENTS2["UPDATE_VERSION"] = "update:version";
|
||
IPC_EVENTS2["UPDATE_STATUS_CHANGED"] = "update:status-changed";
|
||
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";
|
||
CONFIG_KEYS2["AUTO_CHECK_UPDATE"] = "autoCheckUpdate";
|
||
CONFIG_KEYS2["AUTO_DOWNLOAD_UPDATE"] = "autoDownloadUpdate";
|
||
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 || {});
|
||
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;
|
||
}
|
||
}
|
||
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();
|
||
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();
|
||
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);
|
||
}
|
||
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 META_FILENAME = "scripts.meta.json";
|
||
function getScriptsDir$1() {
|
||
return electron.app.isPackaged ? path.join(__dirname, "scripts") : path.join(process.cwd(), "electron/scripts");
|
||
}
|
||
function ensureScriptsDir() {
|
||
const dir = getScriptsDir$1();
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
}
|
||
function getMetaPath() {
|
||
return path.join(getScriptsDir$1(), META_FILENAME);
|
||
}
|
||
function readMeta() {
|
||
const metaPath = getMetaPath();
|
||
if (!fs.existsSync(metaPath)) {
|
||
return { scripts: [] };
|
||
}
|
||
try {
|
||
const raw = fs.readFileSync(metaPath, "utf-8");
|
||
const parsed = JSON.parse(raw);
|
||
if (parsed && Array.isArray(parsed.scripts)) {
|
||
return parsed;
|
||
}
|
||
} catch (err) {
|
||
log.warn("[script-store-service] Failed to read meta:", err);
|
||
}
|
||
return { scripts: [] };
|
||
}
|
||
function writeMeta(meta) {
|
||
ensureScriptsDir();
|
||
const metaPath = getMetaPath();
|
||
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
||
}
|
||
function sanitizeFilename(name) {
|
||
return name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-+|-+$/g, "") || "script";
|
||
}
|
||
function generateUniqueFilename(name, existingNames) {
|
||
const base = sanitizeFilename(name);
|
||
let filename = `${base}.mjs`;
|
||
let counter = 1;
|
||
while (existingNames.has(filename)) {
|
||
filename = `${base}-${counter}.mjs`;
|
||
counter++;
|
||
}
|
||
return filename;
|
||
}
|
||
function seedScripts() {
|
||
const scriptsDir = getScriptsDir$1();
|
||
const metaPath = getMetaPath();
|
||
if (fs.existsSync(metaPath)) {
|
||
return;
|
||
}
|
||
if (!fs.existsSync(scriptsDir)) {
|
||
log.info("[script-store-service] Scripts directory does not exist, skipping seed.");
|
||
return;
|
||
}
|
||
const meta = { scripts: [] };
|
||
const scriptFiles = fs.readdirSync(scriptsDir).filter((f) => f.endsWith(".mjs"));
|
||
for (const file of scriptFiles) {
|
||
try {
|
||
const name = file.replace(/\.mjs$/, "");
|
||
const now = (/* @__PURE__ */ new Date()).toISOString();
|
||
meta.scripts.push({
|
||
id: `seed-${name}`,
|
||
name,
|
||
description: "",
|
||
filename: file,
|
||
enabled: true,
|
||
channel: "",
|
||
createdAt: now,
|
||
updatedAt: now
|
||
});
|
||
} catch (err) {
|
||
log.warn("[script-store-service] Failed to seed script", file, err);
|
||
}
|
||
}
|
||
writeMeta(meta);
|
||
log.info("[script-store-service] Seeded scripts:", meta.scripts.length);
|
||
}
|
||
function initScriptStoreService() {
|
||
ensureScriptsDir();
|
||
seedScripts();
|
||
}
|
||
function listScripts() {
|
||
const meta = readMeta();
|
||
return meta.scripts.map((item) => enrichWithCode(item)).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||
}
|
||
function getScript(id) {
|
||
const meta = readMeta();
|
||
const item = meta.scripts.find((s) => s.id === id);
|
||
if (!item) return null;
|
||
return enrichWithCode(item);
|
||
}
|
||
function getScriptPathById(id) {
|
||
const meta = readMeta();
|
||
const item = meta.scripts.find((s) => s.id === id);
|
||
if (!item) return null;
|
||
return path.join(getScriptsDir$1(), item.filename);
|
||
}
|
||
function saveScript(input) {
|
||
const meta = readMeta();
|
||
const scriptsDir = getScriptsDir$1();
|
||
const existingNames = new Set(meta.scripts.map((s) => s.filename));
|
||
const now = (/* @__PURE__ */ new Date()).toISOString();
|
||
if (input.id) {
|
||
const index = meta.scripts.findIndex((s) => s.id === input.id);
|
||
if (index >= 0) {
|
||
const existing = meta.scripts[index];
|
||
const filePath2 = path.join(scriptsDir, existing.filename);
|
||
fs.writeFileSync(filePath2, input.code, "utf-8");
|
||
meta.scripts[index] = {
|
||
...existing,
|
||
name: input.name,
|
||
description: input.description,
|
||
channel: input.channel,
|
||
enabled: input.enabled,
|
||
updatedAt: now
|
||
};
|
||
writeMeta(meta);
|
||
return enrichWithCode(meta.scripts[index]);
|
||
}
|
||
}
|
||
const filename = generateUniqueFilename(input.name, existingNames);
|
||
const filePath = path.join(scriptsDir, filename);
|
||
fs.writeFileSync(filePath, input.code, "utf-8");
|
||
const id = `script-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||
const item = {
|
||
id,
|
||
name: input.name,
|
||
description: input.description,
|
||
filename,
|
||
enabled: input.enabled,
|
||
channel: input.channel,
|
||
createdAt: now,
|
||
updatedAt: now
|
||
};
|
||
meta.scripts.push(item);
|
||
writeMeta(meta);
|
||
return enrichWithCode(item);
|
||
}
|
||
function deleteScript(id) {
|
||
const meta = readMeta();
|
||
const index = meta.scripts.findIndex((s) => s.id === id);
|
||
if (index === -1) return false;
|
||
const item = meta.scripts[index];
|
||
const filePath = path.join(getScriptsDir$1(), item.filename);
|
||
if (fs.existsSync(filePath)) {
|
||
try {
|
||
fs.unlinkSync(filePath);
|
||
} catch (err) {
|
||
log.warn("[script-store-service] Failed to delete script file:", err);
|
||
}
|
||
}
|
||
meta.scripts.splice(index, 1);
|
||
writeMeta(meta);
|
||
return true;
|
||
}
|
||
function toggleScript(id, enabled) {
|
||
const meta = readMeta();
|
||
const index = meta.scripts.findIndex((s) => s.id === id);
|
||
if (index === -1) return false;
|
||
meta.scripts[index].enabled = enabled;
|
||
meta.scripts[index].updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||
writeMeta(meta);
|
||
return true;
|
||
}
|
||
function updateLastRun(id, lastRun) {
|
||
const meta = readMeta();
|
||
const index = meta.scripts.findIndex((s) => s.id === id);
|
||
if (index === -1) return false;
|
||
meta.scripts[index].lastRun = lastRun;
|
||
meta.scripts[index].updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||
writeMeta(meta);
|
||
return true;
|
||
}
|
||
function enrichWithCode(item) {
|
||
const scriptsDir = getScriptsDir$1();
|
||
const filePath = path.join(scriptsDir, item.filename);
|
||
let code = "";
|
||
try {
|
||
if (fs.existsSync(filePath)) {
|
||
code = fs.readFileSync(filePath, "utf-8");
|
||
}
|
||
} catch (err) {
|
||
log.warn("[script-store-service] Failed to read script file:", err);
|
||
}
|
||
return {
|
||
...item,
|
||
code
|
||
};
|
||
}
|
||
const executor = new executeScriptService();
|
||
async function runScriptById(id, channel) {
|
||
const scriptPath = getScriptPathById(id);
|
||
if (!scriptPath) {
|
||
return {
|
||
success: false,
|
||
exitCode: null,
|
||
stdoutTail: "",
|
||
stderrTail: "",
|
||
error: "Script not found"
|
||
};
|
||
}
|
||
const result = await executor.executeScript(scriptPath, {
|
||
SCRIPT_ID: id,
|
||
CHANNEL: channel || ""
|
||
});
|
||
updateLastRun(id, {
|
||
time: (/* @__PURE__ */ new Date()).toISOString(),
|
||
success: result.success,
|
||
error: result.error
|
||
});
|
||
return result;
|
||
}
|
||
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();
|
||
const playwrightCoreDir = path.dirname(require.resolve("playwright-core"));
|
||
const cliPath = path.join(playwrightCoreDir, "cli.js");
|
||
let recorderProc = null;
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_LIST, async () => {
|
||
try {
|
||
return listScripts();
|
||
} catch (error) {
|
||
log.error("[SCRIPT_LIST] error:", error);
|
||
throw error;
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_GET, async (_event, id) => {
|
||
try {
|
||
return getScript(id);
|
||
} catch (error) {
|
||
log.error("[SCRIPT_GET] error:", error);
|
||
throw error;
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_SAVE, async (_event, input) => {
|
||
try {
|
||
return saveScript(input);
|
||
} catch (error) {
|
||
log.error("[SCRIPT_SAVE] error:", error);
|
||
throw error;
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_DELETE, async (_event, id) => {
|
||
try {
|
||
return deleteScript(id);
|
||
} catch (error) {
|
||
log.error("[SCRIPT_DELETE] error:", error);
|
||
throw error;
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_TOGGLE, async (_event, id, enabled) => {
|
||
try {
|
||
return toggleScript(id, enabled);
|
||
} catch (error) {
|
||
log.error("[SCRIPT_TOGGLE] error:", error);
|
||
throw error;
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RUN, async (_event, id) => {
|
||
try {
|
||
const script = getScript(id);
|
||
return await runScriptById(id, script?.channel);
|
||
} catch (error) {
|
||
log.error("[SCRIPT_RUN] error:", error);
|
||
return { success: false, exitCode: null, stdoutTail: "", stderrTail: "", error: error?.message || "Run failed" };
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_START, async (_event, url) => {
|
||
try {
|
||
if (recorderProc) {
|
||
recorderProc.kill("SIGINT");
|
||
recorderProc = null;
|
||
}
|
||
const targetUrl = url || "about:blank";
|
||
recorderProc = child_process.spawn(process.execPath, [cliPath, "codegen", "--target", "javascript", "--channel", "chrome", "--viewport-size", "1920,1080", "--color-scheme", "light", targetUrl], {
|
||
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
||
stdio: "pipe"
|
||
});
|
||
recorderProc.on("error", (err) => {
|
||
log.error("[SCRIPT_RECORD_START] Failed to start codegen process:", err);
|
||
});
|
||
recorderProc.on("exit", (code, signal) => {
|
||
log.info(`[SCRIPT_RECORD_START] Process exited code=${code} signal=${signal}`);
|
||
recorderProc = null;
|
||
});
|
||
recorderProc.stdout?.on("data", (data) => {
|
||
log.info(`[SCRIPT_RECORD_START] stdout: ${data.toString()}`);
|
||
});
|
||
recorderProc.stderr?.on("data", (data) => {
|
||
log.error(`[SCRIPT_RECORD_START] stderr: ${data.toString()}`);
|
||
});
|
||
return { success: true };
|
||
} catch (error) {
|
||
log.error("[SCRIPT_RECORD_START] error:", error);
|
||
return { success: false, error: error?.message || "Recording start failed" };
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_RECORD_STOP, async () => {
|
||
try {
|
||
if (recorderProc) {
|
||
recorderProc.kill("SIGINT");
|
||
recorderProc = null;
|
||
}
|
||
return { success: true, code: "" };
|
||
} catch (error) {
|
||
log.error("[SCRIPT_RECORD_STOP] error:", error);
|
||
return { success: false, error: error?.message || "Recording stop failed" };
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.SCRIPT_CODEGEN, async (_event, id, url) => {
|
||
try {
|
||
const script = getScript(id);
|
||
if (!script) {
|
||
return { success: false, error: "Script not found" };
|
||
}
|
||
const scriptsDir = getScriptsDir();
|
||
const scriptPath = path.join(scriptsDir, script.filename);
|
||
const targetUrl = url || "about:blank";
|
||
log.info(`[SCRIPT_CODEGEN] Starting codegen for script ${id} at ${scriptPath} with url ${targetUrl}`);
|
||
return await new Promise((resolve) => {
|
||
const proc = child_process.spawn(process.execPath, [cliPath, "codegen", "--target", "javascript", "--channel", "chrome", "-o", scriptPath, targetUrl], {
|
||
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
||
stdio: "pipe"
|
||
});
|
||
proc.on("exit", () => {
|
||
try {
|
||
let generatedCode = fs.readFileSync(scriptPath, "utf-8");
|
||
if (generatedCode.includes("require('playwright')") && !generatedCode.includes("createRequire")) {
|
||
generatedCode = `import { createRequire } from 'node:module';
|
||
const require = createRequire(import.meta.url);
|
||
|
||
${generatedCode}`;
|
||
}
|
||
fs.writeFileSync(scriptPath, generatedCode, "utf-8");
|
||
saveScript({
|
||
id,
|
||
name: script.name,
|
||
description: script.description,
|
||
code: generatedCode,
|
||
channel: script.channel,
|
||
enabled: script.enabled
|
||
});
|
||
resolve({ success: true, code: generatedCode });
|
||
} catch (err) {
|
||
log.error("[SCRIPT_CODEGEN] Failed to process generated code:", err);
|
||
resolve({ success: false, error: err?.message || "Failed to process generated code" });
|
||
}
|
||
});
|
||
proc.on("error", (err) => {
|
||
log.error("[SCRIPT_CODEGEN] Failed to start codegen:", err);
|
||
resolve({ success: false, error: err.message });
|
||
});
|
||
});
|
||
} catch (error) {
|
||
log.error("[SCRIPT_CODEGEN] error:", error);
|
||
return { success: false, error: error?.message || "Codegen failed" };
|
||
}
|
||
});
|
||
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 };
|
||
}
|
||
});
|
||
}
|
||
class AppUpdater {
|
||
mainWindow = null;
|
||
static _instance;
|
||
_initialized = false;
|
||
constructor() {
|
||
electronUpdater.autoUpdater.autoDownload = false;
|
||
}
|
||
init() {
|
||
if (this._initialized) return;
|
||
this._initialized = true;
|
||
this.setupListeners();
|
||
this.registerHandlers();
|
||
}
|
||
static getInstance() {
|
||
if (!this._instance) {
|
||
this._instance = new AppUpdater();
|
||
}
|
||
return this._instance;
|
||
}
|
||
setMainWindow(window2) {
|
||
this.mainWindow = window2;
|
||
}
|
||
setupListeners() {
|
||
electronUpdater.autoUpdater.on("checking-for-update", () => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "checking" }));
|
||
electronUpdater.autoUpdater.on("update-available", (info) => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "available", info }));
|
||
electronUpdater.autoUpdater.on("update-not-available", () => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "not-available" }));
|
||
electronUpdater.autoUpdater.on("download-progress", (progress) => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "downloading", progress }));
|
||
electronUpdater.autoUpdater.on("update-downloaded", (info) => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "downloaded", info }));
|
||
electronUpdater.autoUpdater.on("error", (error) => this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "error", error: error.message }));
|
||
}
|
||
sendToRenderer(channel, data) {
|
||
electron.BrowserWindow.getAllWindows().forEach((win) => {
|
||
if (!win.isDestroyed()) {
|
||
win.webContents.send(channel, data);
|
||
}
|
||
});
|
||
}
|
||
registerHandlers() {
|
||
electron.ipcMain.handle(IPC_EVENTS.UPDATE_CHECK, () => {
|
||
if (electron.app.isPackaged) {
|
||
return electronUpdater.autoUpdater.checkForUpdates();
|
||
} else {
|
||
this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "checking" });
|
||
setTimeout(() => {
|
||
this.sendToRenderer(IPC_EVENTS.UPDATE_STATUS_CHANGED, { status: "not-available" });
|
||
}, 1500);
|
||
return null;
|
||
}
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.UPDATE_DOWNLOAD, () => {
|
||
if (electron.app.isPackaged) {
|
||
return electronUpdater.autoUpdater.downloadUpdate();
|
||
}
|
||
return null;
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.UPDATE_INSTALL, () => {
|
||
if (electron.app.isPackaged) {
|
||
return electronUpdater.autoUpdater.quitAndInstall();
|
||
}
|
||
return null;
|
||
});
|
||
electron.ipcMain.handle(IPC_EVENTS.UPDATE_VERSION, () => {
|
||
return electron.app.getVersion();
|
||
});
|
||
}
|
||
}
|
||
const appUpdater = AppUpdater.getInstance();
|
||
const PROVIDER_TYPE_INFO = [
|
||
{
|
||
id: "anthropic",
|
||
name: "Anthropic",
|
||
icon: "🤖",
|
||
placeholder: "sk-ant-api03-...",
|
||
model: "Claude",
|
||
requiresApiKey: true,
|
||
docsUrl: "https://platform.claude.com/docs/en/api/overview"
|
||
},
|
||
{
|
||
id: "openai",
|
||
name: "OpenAI",
|
||
icon: "💚",
|
||
placeholder: "sk-proj-...",
|
||
model: "GPT",
|
||
requiresApiKey: true,
|
||
isOAuth: true,
|
||
supportsApiKey: true,
|
||
defaultModelId: "gpt-5.4",
|
||
showModelId: true,
|
||
showModelIdInDevModeOnly: true,
|
||
modelIdPlaceholder: "gpt-5.4",
|
||
apiKeyUrl: "https://platform.openai.com/api-keys"
|
||
},
|
||
{
|
||
id: "google",
|
||
name: "Google",
|
||
icon: "🔷",
|
||
placeholder: "AIza...",
|
||
model: "Gemini",
|
||
requiresApiKey: true,
|
||
isOAuth: true,
|
||
supportsApiKey: true,
|
||
defaultModelId: "gemini-3-pro-preview",
|
||
showModelId: true,
|
||
showModelIdInDevModeOnly: true,
|
||
modelIdPlaceholder: "gemini-3-pro-preview",
|
||
apiKeyUrl: "https://aistudio.google.com/app/apikey"
|
||
},
|
||
{ id: "openrouter", name: "OpenRouter", icon: "🌐", placeholder: "sk-or-v1-...", model: "Multi-Model", requiresApiKey: true, showModelId: true, modelIdPlaceholder: "openai/gpt-5.4", defaultModelId: "openai/gpt-5.4", docsUrl: "https://openrouter.ai/models" },
|
||
{ id: "minimax-portal-cn", name: "MiniMax (CN)", icon: "☁️", placeholder: "sk-...", model: "MiniMax", requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: "MiniMax-M2.7", showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: "MiniMax-M2.7", apiKeyUrl: "https://platform.minimaxi.com/" },
|
||
{ id: "moonshot", name: "Moonshot (CN)", icon: "🌙", placeholder: "sk-...", model: "Kimi", requiresApiKey: true, defaultBaseUrl: "https://api.moonshot.cn/v1", defaultModelId: "kimi-k2.5", docsUrl: "https://platform.moonshot.cn/" },
|
||
{ id: "siliconflow", name: "SiliconFlow (CN)", icon: "🌊", placeholder: "sk-...", model: "Multi-Model", requiresApiKey: true, defaultBaseUrl: "https://api.siliconflow.cn/v1", showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: "deepseek-ai/DeepSeek-V3", defaultModelId: "deepseek-ai/DeepSeek-V3", docsUrl: "https://docs.siliconflow.cn/cn/userguide/introduction" },
|
||
{ id: "deepseek", name: "DeepSeek", icon: "🐋", placeholder: "sk-...", model: "DeepSeek", requiresApiKey: true, defaultBaseUrl: "https://api.deepseek.com/v1", showModelId: true, modelIdPlaceholder: "deepseek-chat", defaultModelId: "deepseek-chat", apiKeyUrl: "https://platform.deepseek.com/api_keys", docsUrl: "https://api-docs.deepseek.com/" },
|
||
{ id: "minimax-portal", name: "MiniMax (Global)", icon: "☁️", placeholder: "sk-...", model: "MiniMax", requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: "MiniMax-M2.7", showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: "MiniMax-M2.7", apiKeyUrl: "https://platform.minimax.io" },
|
||
{ id: "modelstudio", name: "Model Studio", icon: "☁️", placeholder: "sk-...", model: "Qwen", requiresApiKey: true, defaultBaseUrl: "https://coding.dashscope.aliyuncs.com/v1", showBaseUrl: true, defaultModelId: "qwen3.5-plus", showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: "qwen3.5-plus", apiKeyUrl: "https://bailian.console.aliyun.com/", hidden: true },
|
||
{ id: "ark", name: "ByteDance Ark", icon: "A", placeholder: "your-ark-api-key", model: "Doubao", requiresApiKey: true, defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3", showBaseUrl: true, showModelId: true, modelIdPlaceholder: "ep-20260228000000-xxxxx", docsUrl: "https://www.volcengine.com/", codePlanPresetBaseUrl: "https://ark.cn-beijing.volces.com/api/coding/v3", codePlanPresetModelId: "ark-code-latest", codePlanDocsUrl: "https://www.volcengine.com/docs/82379/1928261?lang=zh" },
|
||
{ id: "ollama", name: "Ollama", icon: "🦙", placeholder: "Not required", requiresApiKey: false, defaultBaseUrl: "http://localhost:11434/v1", showBaseUrl: true, showModelId: true, modelIdPlaceholder: "qwen3:latest" },
|
||
{
|
||
id: "custom",
|
||
name: "Custom",
|
||
icon: "⚙️",
|
||
placeholder: "API key...",
|
||
requiresApiKey: true,
|
||
showBaseUrl: true,
|
||
showModelId: true,
|
||
modelIdPlaceholder: "your-provider/model-id",
|
||
docsUrl: "https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth",
|
||
docsUrlZh: "https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh"
|
||
}
|
||
];
|
||
function getProviderTypeInfo(type) {
|
||
return PROVIDER_TYPE_INFO.find((t2) => t2.id === type);
|
||
}
|
||
const defaultStore = {
|
||
accounts: [],
|
||
defaultAccountId: null
|
||
};
|
||
const storePath = path__namespace.join(electron.app.getPath("userData"), "provider-accounts.json");
|
||
const keysPath = path__namespace.join(electron.app.getPath("userData"), "provider-keys.json");
|
||
function readJson(filePath, defaultValue) {
|
||
try {
|
||
if (fs__namespace.existsSync(filePath)) {
|
||
return JSON.parse(fs__namespace.readFileSync(filePath, "utf-8"));
|
||
}
|
||
} catch (e) {
|
||
logManager.error(`Failed to read ${filePath}:`, e);
|
||
}
|
||
return defaultValue;
|
||
}
|
||
function writeJson(filePath, data) {
|
||
try {
|
||
fs__namespace.mkdirSync(path__namespace.dirname(filePath), { recursive: true });
|
||
fs__namespace.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
||
} catch (e) {
|
||
logManager.error(`Failed to write ${filePath}:`, e);
|
||
}
|
||
}
|
||
function getStore() {
|
||
return readJson(storePath, defaultStore);
|
||
}
|
||
function saveStore(store) {
|
||
writeJson(storePath, store);
|
||
}
|
||
function getKeys() {
|
||
return readJson(keysPath, {});
|
||
}
|
||
function saveKeys(keys) {
|
||
writeJson(keysPath, keys);
|
||
}
|
||
function mapToProviderWithKeyInfo(account) {
|
||
const keys = getKeys();
|
||
const hasKey = !!keys[account.id];
|
||
return {
|
||
id: account.id,
|
||
name: account.label,
|
||
type: account.vendorId,
|
||
baseUrl: account.baseUrl,
|
||
apiProtocol: account.apiProtocol,
|
||
headers: account.headers,
|
||
model: account.model,
|
||
fallbackModels: account.fallbackModels,
|
||
fallbackProviderIds: account.fallbackAccountIds,
|
||
enabled: account.enabled,
|
||
createdAt: account.createdAt,
|
||
updatedAt: account.updatedAt,
|
||
hasKey,
|
||
keyMasked: hasKey ? "••••••••" : null
|
||
};
|
||
}
|
||
const listeners = [];
|
||
function onProviderChange(listener) {
|
||
listeners.push(listener);
|
||
return () => {
|
||
const idx = listeners.indexOf(listener);
|
||
if (idx > -1) listeners.splice(idx, 1);
|
||
};
|
||
}
|
||
function notifyChange() {
|
||
listeners.forEach((l) => l());
|
||
}
|
||
function mapToVendorInfo(info) {
|
||
return {
|
||
...info,
|
||
category: info.id === "ollama" ? "local" : info.id === "custom" ? "custom" : "compatible",
|
||
supportedAuthModes: info.requiresApiKey ? info.isOAuth ? ["api_key", "oauth_browser"] : ["api_key"] : info.isOAuth ? ["local", "oauth_browser"] : ["local"],
|
||
defaultAuthMode: info.requiresApiKey ? "api_key" : "local",
|
||
supportsMultipleAccounts: true
|
||
};
|
||
}
|
||
function sanitizeAccount(account) {
|
||
let model = account.model;
|
||
if (model) {
|
||
if (model === "deepseek-chat/deepseek-reasoner" || model.startsWith("deepseek-chat/")) {
|
||
model = "deepseek-chat";
|
||
} else if (model.startsWith("deepseek-reasoner/")) {
|
||
model = "deepseek-reasoner";
|
||
}
|
||
}
|
||
if (model !== account.model) {
|
||
return { ...account, model };
|
||
}
|
||
return account;
|
||
}
|
||
const providerApiService = {
|
||
getVendors() {
|
||
return PROVIDER_TYPE_INFO.map(mapToVendorInfo);
|
||
},
|
||
getAccounts() {
|
||
return getStore().accounts.map(sanitizeAccount);
|
||
},
|
||
getProviders() {
|
||
return getStore().accounts.map(sanitizeAccount).map(mapToProviderWithKeyInfo);
|
||
},
|
||
getDefault() {
|
||
return { accountId: getStore().defaultAccountId };
|
||
},
|
||
createAccount(body) {
|
||
const store = getStore();
|
||
const account = { ...body.account, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
||
store.accounts.push(account);
|
||
if (body.apiKey) {
|
||
const keys = getKeys();
|
||
keys[account.id] = body.apiKey;
|
||
saveKeys(keys);
|
||
}
|
||
saveStore(store);
|
||
notifyChange();
|
||
return { success: true };
|
||
},
|
||
updateAccount(accountId, body) {
|
||
const store = getStore();
|
||
const idx = store.accounts.findIndex((a) => a.id === accountId);
|
||
if (idx === -1) return { success: false, error: "Account not found" };
|
||
store.accounts[idx] = {
|
||
...store.accounts[idx],
|
||
...body.updates,
|
||
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
||
};
|
||
if (body.apiKey) {
|
||
const keys = getKeys();
|
||
keys[accountId] = body.apiKey;
|
||
saveKeys(keys);
|
||
}
|
||
saveStore(store);
|
||
notifyChange();
|
||
return { success: true };
|
||
},
|
||
deleteAccount(accountId) {
|
||
const store = getStore();
|
||
store.accounts = store.accounts.filter((a) => a.id !== accountId);
|
||
if (store.defaultAccountId === accountId) store.defaultAccountId = null;
|
||
saveStore(store);
|
||
const keys = getKeys();
|
||
delete keys[accountId];
|
||
saveKeys(keys);
|
||
notifyChange();
|
||
return { success: true };
|
||
},
|
||
setDefault(body) {
|
||
const store = getStore();
|
||
const accountExists = store.accounts.some((a) => a.id === body.accountId);
|
||
if (!accountExists) {
|
||
return { success: false, error: "Account not found" };
|
||
}
|
||
store.defaultAccountId = body.accountId;
|
||
saveStore(store);
|
||
notifyChange();
|
||
return { success: true };
|
||
},
|
||
validateApiKey(body) {
|
||
if (!body.apiKey || body.apiKey.trim().length === 0) {
|
||
return { valid: false, error: "API key is required" };
|
||
}
|
||
return { valid: true };
|
||
},
|
||
getApiKey(providerId) {
|
||
const keys = getKeys();
|
||
return { apiKey: keys[providerId] || null };
|
||
},
|
||
deleteApiKey(accountId) {
|
||
const keys = getKeys();
|
||
delete keys[accountId];
|
||
saveKeys(keys);
|
||
notifyChange();
|
||
return { success: true };
|
||
},
|
||
getUsageHistory() {
|
||
return [];
|
||
}
|
||
};
|
||
class BaseProvider {
|
||
}
|
||
function _transformChunk(chunk) {
|
||
const choice = chunk.choices[0];
|
||
return {
|
||
isEnd: choice?.finish_reason != null,
|
||
result: choice?.delta?.content ?? ""
|
||
};
|
||
}
|
||
class OpenAIProvider extends BaseProvider {
|
||
client;
|
||
constructor(apiKey, baseURL, headers) {
|
||
super();
|
||
this.client = new OpenAI({ apiKey, baseURL, defaultHeaders: headers });
|
||
}
|
||
async chat(messages2, model, options) {
|
||
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
|
||
}, {
|
||
signal: options?.signal
|
||
});
|
||
return {
|
||
async *[Symbol.asyncIterator]() {
|
||
try {
|
||
for await (const chunk of chunks) {
|
||
if (options?.signal?.aborted) break;
|
||
yield _transformChunk(chunk);
|
||
}
|
||
const responseTime = Date.now() - startTime;
|
||
logManager.logApiResponse("chat.completions.create", { success: true }, 200, responseTime);
|
||
} catch (error) {
|
||
const responseTime = Date.now() - startTime;
|
||
logManager.logApiResponse("chat.completions.create", { error: error instanceof Error ? error.message : String(error) }, 500, responseTime);
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
} 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 createProvider(accountId) {
|
||
const account = providerApiService.getAccounts().find((a) => a.id === accountId);
|
||
if (!account) {
|
||
throw new Error(`Provider account ${accountId} not found`);
|
||
}
|
||
const apiKeyResult = providerApiService.getApiKey(accountId);
|
||
const apiKey = apiKeyResult.apiKey;
|
||
if (!apiKey) {
|
||
throw new Error(`API key for account ${accountId} not found`);
|
||
}
|
||
const baseURL = account.baseUrl || getProviderTypeInfo(account.vendorId)?.defaultBaseUrl;
|
||
if (!baseURL) {
|
||
throw new Error(`Base URL for account ${accountId} not found`);
|
||
}
|
||
switch (account.apiProtocol) {
|
||
case "anthropic-messages":
|
||
throw new Error("Anthropic provider not yet implemented");
|
||
case "openai-completions":
|
||
case "openai-responses":
|
||
default:
|
||
return new OpenAIProvider(apiKey, baseURL, account.headers);
|
||
}
|
||
}
|
||
let sessionsFilePath = null;
|
||
function getSessionsFilePath() {
|
||
if (!sessionsFilePath) {
|
||
sessionsFilePath = path__namespace.join(electron.app.getPath("userData"), "chat-sessions.json");
|
||
}
|
||
return sessionsFilePath;
|
||
}
|
||
class SessionStore {
|
||
sessions = /* @__PURE__ */ new Map();
|
||
loaded = false;
|
||
ensureLoaded() {
|
||
if (this.loaded) return;
|
||
this.loaded = true;
|
||
this.loadFromDisk();
|
||
}
|
||
loadFromDisk() {
|
||
try {
|
||
const filePath = getSessionsFilePath();
|
||
if (fs__namespace.existsSync(filePath)) {
|
||
const data = JSON.parse(fs__namespace.readFileSync(filePath, "utf-8"));
|
||
for (const [key, entry] of Object.entries(data)) {
|
||
this.sessions.set(key, {
|
||
...entry,
|
||
activeRun: void 0
|
||
});
|
||
}
|
||
}
|
||
} catch (e) {
|
||
logManager.error("Failed to load sessions from disk:", e);
|
||
}
|
||
}
|
||
saveToDisk() {
|
||
try {
|
||
const filePath = getSessionsFilePath();
|
||
const data = {};
|
||
for (const [key, entry] of this.sessions) {
|
||
data[key] = {
|
||
key: entry.key,
|
||
messages: entry.messages,
|
||
updatedAt: entry.updatedAt
|
||
};
|
||
}
|
||
fs__namespace.mkdirSync(path__namespace.dirname(filePath), { recursive: true });
|
||
fs__namespace.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
||
} catch (e) {
|
||
logManager.error("Failed to save sessions to disk:", e);
|
||
}
|
||
}
|
||
getOrCreate(key) {
|
||
this.ensureLoaded();
|
||
let session = this.sessions.get(key);
|
||
if (!session) {
|
||
session = {
|
||
key,
|
||
messages: [],
|
||
updatedAt: Date.now()
|
||
};
|
||
this.sessions.set(key, session);
|
||
}
|
||
return session;
|
||
}
|
||
get(key) {
|
||
this.ensureLoaded();
|
||
return this.sessions.get(key);
|
||
}
|
||
getAllKeys() {
|
||
this.ensureLoaded();
|
||
return Array.from(this.sessions.keys());
|
||
}
|
||
appendMessage(key, message) {
|
||
const session = this.getOrCreate(key);
|
||
session.messages.push(message);
|
||
session.updatedAt = Date.now();
|
||
this.saveToDisk();
|
||
}
|
||
getMessages(key, limit = 50) {
|
||
const session = this.get(key);
|
||
if (!session) return [];
|
||
return session.messages.slice(-limit);
|
||
}
|
||
setActiveRun(key, runId, abortController) {
|
||
const session = this.getOrCreate(key);
|
||
session.activeRun = { runId, abortController };
|
||
}
|
||
clearActiveRun(key) {
|
||
const session = this.sessions.get(key);
|
||
if (session) {
|
||
session.activeRun = void 0;
|
||
}
|
||
}
|
||
getActiveRun(key) {
|
||
return this.sessions.get(key)?.activeRun;
|
||
}
|
||
deleteSession(key) {
|
||
this.sessions.delete(key);
|
||
this.saveToDisk();
|
||
}
|
||
}
|
||
const sessionStore = new SessionStore();
|
||
function buildChatMessages(sessionMessages) {
|
||
return sessionMessages.map((msg) => {
|
||
if (!msg.role || !msg.content) return null;
|
||
const role = msg.role;
|
||
if (role === "user" || role === "assistant" || role === "system") {
|
||
return {
|
||
role,
|
||
content: typeof msg.content === "string" ? msg.content : ""
|
||
};
|
||
}
|
||
return null;
|
||
}).filter((m) => m !== null);
|
||
}
|
||
async function processChatStream(sessionKey, runId, provider, model, messages2, signal, broadcast) {
|
||
let assistantContent = "";
|
||
try {
|
||
const chunks = await provider.chat(messages2, model, { signal });
|
||
for await (const chunk of chunks) {
|
||
if (signal.aborted) break;
|
||
if (chunk.result) {
|
||
assistantContent += chunk.result;
|
||
broadcast({
|
||
type: "chat:delta",
|
||
sessionKey,
|
||
runId,
|
||
delta: chunk.result
|
||
});
|
||
}
|
||
if (chunk.isEnd) {
|
||
break;
|
||
}
|
||
}
|
||
if (!signal.aborted) {
|
||
const finalMessage = {
|
||
role: "assistant",
|
||
content: assistantContent,
|
||
timestamp: Date.now()
|
||
};
|
||
sessionStore.appendMessage(sessionKey, finalMessage);
|
||
sessionStore.clearActiveRun(sessionKey);
|
||
broadcast({
|
||
type: "chat:final",
|
||
sessionKey,
|
||
runId,
|
||
message: finalMessage
|
||
});
|
||
}
|
||
} catch (error) {
|
||
sessionStore.clearActiveRun(sessionKey);
|
||
broadcast({
|
||
type: "chat:error",
|
||
sessionKey,
|
||
runId,
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
}
|
||
}
|
||
function handleChatSend(params, broadcast) {
|
||
const { sessionKey, message, options } = params;
|
||
const runId = crypto.randomUUID();
|
||
sessionStore.appendMessage(sessionKey, {
|
||
...message,
|
||
timestamp: message.timestamp || Date.now()
|
||
});
|
||
const accountId = options?.providerAccountId || providerApiService.getDefault().accountId;
|
||
if (!accountId) {
|
||
throw new Error("No provider account selected");
|
||
}
|
||
const account = providerApiService.getAccounts().find((a) => a.id === accountId);
|
||
if (!account) {
|
||
throw new Error(`Provider account ${accountId} not found`);
|
||
}
|
||
const model = account.model;
|
||
if (!model) {
|
||
throw new Error(`Provider account ${accountId} has no model configured`);
|
||
}
|
||
const session = sessionStore.getOrCreate(sessionKey);
|
||
const messages2 = buildChatMessages(session.messages);
|
||
const abortController = new AbortController();
|
||
sessionStore.setActiveRun(sessionKey, runId, abortController);
|
||
const provider = createProvider(accountId);
|
||
processChatStream(sessionKey, runId, provider, model, messages2, abortController.signal, broadcast).catch(
|
||
(err) => {
|
||
logManager.error("Unexpected error in processChatStream:", err);
|
||
sessionStore.clearActiveRun(sessionKey);
|
||
broadcast({
|
||
type: "chat:error",
|
||
sessionKey,
|
||
runId,
|
||
error: err instanceof Error ? err.message : String(err)
|
||
});
|
||
}
|
||
);
|
||
return { runId };
|
||
}
|
||
function handleChatHistory(params) {
|
||
return sessionStore.getMessages(params.sessionKey, params.limit ?? 50);
|
||
}
|
||
function handleChatAbort(params, broadcast) {
|
||
const activeRun = sessionStore.getActiveRun(params.sessionKey);
|
||
if (activeRun) {
|
||
activeRun.abortController.abort();
|
||
sessionStore.clearActiveRun(params.sessionKey);
|
||
broadcast({
|
||
type: "chat:aborted",
|
||
sessionKey: params.sessionKey,
|
||
runId: activeRun.runId
|
||
});
|
||
}
|
||
}
|
||
function handleSessionList() {
|
||
return sessionStore.getAllKeys();
|
||
}
|
||
function handleProviderList() {
|
||
return {
|
||
accounts: providerApiService.getAccounts(),
|
||
defaultAccountId: providerApiService.getDefault().accountId
|
||
};
|
||
}
|
||
function handleProviderGetDefault() {
|
||
return providerApiService.getDefault();
|
||
}
|
||
class GatewayManager {
|
||
initialized = false;
|
||
async init() {
|
||
if (this.initialized) return;
|
||
this.initialized = true;
|
||
logManager.info("GatewayManager initialized");
|
||
this.broadcast({ type: "gateway:status", status: "connected" });
|
||
}
|
||
async rpc(method, params) {
|
||
if (!this.initialized) {
|
||
await this.init();
|
||
}
|
||
logManager.info(`Gateway RPC: ${method}`, params);
|
||
switch (method) {
|
||
case "chat.send":
|
||
return handleChatSend(params, (event) => this.broadcast(event));
|
||
case "chat.history":
|
||
return handleChatHistory(params);
|
||
case "chat.abort":
|
||
return handleChatAbort(params, (event) => this.broadcast(event));
|
||
case "session.list":
|
||
return handleSessionList();
|
||
case "provider.list":
|
||
return handleProviderList();
|
||
case "provider.getDefault":
|
||
return handleProviderGetDefault();
|
||
default:
|
||
throw new Error(`Unknown gateway RPC method: ${method}`);
|
||
}
|
||
}
|
||
broadcast(event) {
|
||
const mainWindow = electron.BrowserWindow.getAllWindows().find(
|
||
(win) => windowManager.getName(win) === "main"
|
||
);
|
||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||
mainWindow.webContents.send("gateway:event", event);
|
||
}
|
||
}
|
||
reloadProviders() {
|
||
logManager.info("GatewayManager reloading providers");
|
||
}
|
||
}
|
||
const gatewayManager = new GatewayManager();
|
||
appUpdater.init();
|
||
const HOST_API_BASE_URL = "http://8.138.234.141/ingress";
|
||
async function handleLocalProviderApi(path2, method, body) {
|
||
const parsedBody = typeof body === "string" && body ? JSON.parse(body) : body;
|
||
if (path2 === "/api/provider-vendors" && method === "GET") {
|
||
return { success: true, ok: true, json: providerApiService.getVendors(), data: providerApiService.getVendors() };
|
||
}
|
||
if (path2 === "/api/provider-accounts" && method === "GET") {
|
||
return { success: true, ok: true, json: providerApiService.getAccounts(), data: providerApiService.getAccounts() };
|
||
}
|
||
if (path2 === "/api/providers" && method === "GET") {
|
||
return { success: true, ok: true, json: providerApiService.getProviders(), data: providerApiService.getProviders() };
|
||
}
|
||
if (path2 === "/api/provider-accounts/default" && method === "GET") {
|
||
return { success: true, ok: true, json: providerApiService.getDefault(), data: providerApiService.getDefault() };
|
||
}
|
||
if (path2 === "/api/provider-accounts" && method === "POST") {
|
||
const result = providerApiService.createAccount(parsedBody || {});
|
||
return { success: true, ok: true, json: result, data: result };
|
||
}
|
||
if (path2 === "/api/provider-accounts/default" && method === "PUT") {
|
||
const result = providerApiService.setDefault(parsedBody || {});
|
||
return { success: result.success, ok: result.success, json: result, data: result };
|
||
}
|
||
if (path2.startsWith("/api/provider-accounts/") && method === "PUT") {
|
||
const id = decodeURIComponent(path2.replace("/api/provider-accounts/", ""));
|
||
const result = providerApiService.updateAccount(id, parsedBody || {});
|
||
return { success: result.success, ok: result.success, json: result, data: result };
|
||
}
|
||
if (path2.startsWith("/api/provider-accounts/") && method === "DELETE") {
|
||
const id = decodeURIComponent(path2.replace("/api/provider-accounts/", ""));
|
||
const result = providerApiService.deleteAccount(id);
|
||
return { success: result.success, ok: result.success, json: result, data: result };
|
||
}
|
||
if (path2 === "/api/providers/default" && method === "PUT") {
|
||
const result = providerApiService.setDefault({ accountId: parsedBody?.providerId });
|
||
return { success: result.success, ok: result.success, json: result, data: result };
|
||
}
|
||
if (path2.startsWith("/api/providers/") && path2.endsWith("/api-key") && method === "GET") {
|
||
const id = decodeURIComponent(path2.replace("/api/providers/", "").replace("/api-key", ""));
|
||
const result = providerApiService.getApiKey(id);
|
||
return { success: true, ok: true, json: result, data: result };
|
||
}
|
||
if (path2.startsWith("/api/providers/") && method === "PUT") {
|
||
const id = decodeURIComponent(path2.replace("/api/providers/", ""));
|
||
const result = providerApiService.updateAccount(id, parsedBody || {});
|
||
return { success: result.success, ok: result.success, json: result, data: result };
|
||
}
|
||
if (path2.startsWith("/api/providers/") && method === "DELETE") {
|
||
const [rawId, query] = path2.replace("/api/providers/", "").split("?");
|
||
const id = decodeURIComponent(rawId);
|
||
if (query && query.includes("apiKeyOnly=1")) {
|
||
const result2 = providerApiService.deleteApiKey(id);
|
||
return { success: result2.success, ok: result2.success, json: result2, data: result2 };
|
||
}
|
||
const result = providerApiService.deleteAccount(id);
|
||
return { success: result.success, ok: result.success, json: result, data: result };
|
||
}
|
||
if (path2 === "/api/providers/validate" && method === "POST") {
|
||
const result = await providerApiService.validateApiKey(parsedBody || {});
|
||
return { success: true, ok: true, json: result, data: result };
|
||
}
|
||
if (path2 === "/api/usage/recent-token-history" && method === "GET") {
|
||
return { success: true, ok: true, json: providerApiService.getUsageHistory(), data: providerApiService.getUsageHistory() };
|
||
}
|
||
return null;
|
||
}
|
||
electron.ipcMain.handle("hostapi:fetch", async (_event, { path: path2, method, headers, body }) => {
|
||
const localResult = await handleLocalProviderApi(path2, method || "GET", body);
|
||
if (localResult) return localResult;
|
||
const url = `${HOST_API_BASE_URL}${path2}`;
|
||
try {
|
||
const response = await axios({
|
||
url,
|
||
method: method || "GET",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
...headers
|
||
},
|
||
data: body ?? void 0,
|
||
timeout: 3e4
|
||
});
|
||
return {
|
||
success: true,
|
||
ok: true,
|
||
json: response.data,
|
||
data: response.data
|
||
};
|
||
} catch (error) {
|
||
if (error.response) {
|
||
return {
|
||
success: false,
|
||
ok: false,
|
||
status: error.response.status,
|
||
error: error.response.data?.message || error.message,
|
||
text: error.response.statusText,
|
||
data: error.response.data
|
||
};
|
||
}
|
||
return {
|
||
success: false,
|
||
ok: false,
|
||
error: error.message || "Unknown error"
|
||
};
|
||
}
|
||
});
|
||
electron.ipcMain.handle("gateway:rpc", async (_event, method, params) => {
|
||
return gatewayManager.rpc(method, params);
|
||
});
|
||
if (started) {
|
||
electron.app.quit();
|
||
}
|
||
electron.app.whenReady().then(() => {
|
||
gatewayManager.init();
|
||
onProviderChange(() => {
|
||
gatewayManager.reloadProviders();
|
||
});
|
||
setupMainWindow();
|
||
initScriptStoreService();
|
||
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();
|
||
}
|
||
});
|