const { app, BrowserWindow, ipcMain, clipboard, dialog, shell, session } = require('electron'); const path = require('path'); const fs = require('fs'); const DL_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'; function filenameFromResponse(res, fallback) { const cd = res.headers.get('content-disposition') || ''; let m = cd.match(/filename\*=(?:UTF-8'')?([^;]+)/i) || cd.match(/filename="?([^";]+)"?/i); if (m) { try { return decodeURIComponent(m[1].trim()); } catch (e) { return m[1].trim(); } } try { const u = new URL(res.url); const base = path.basename(u.pathname); if (base && /\.[a-z0-9]{2,5}$/i.test(base)) return decodeURIComponent(base); } catch (e) { /* ignore */ } return fallback || 'download.bin'; } app.setName('PeopleLib'); // 全局忽略证书错误:访问 Z-Library / LibGen 等使用泛域名证书或经代理的站点时必需 app.commandLine.appendSwitch('ignore-certificate-errors'); const userDataDir = app.isPackaged ? path.join(path.dirname(app.getPath('exe')), 'data') : path.join(app.getPath('appData'), 'PeopleLib'); app.setPath('userData', userDataDir); const sources = require('./src/sources'); const library = require('./src/library/store'); const zlibAuth = require('./src/sources/zlib-auth'); const settings = require('./src/settings'); const { setProxy, getProxy, fetchWithProxy } = require('./src/sources/http'); zlibAuth.init(userDataDir); settings.init(userDataDir); // 启动时从持久化设置恢复代理 setProxy(settings.get('proxy', '')); let mainWindow; function createWindow() { mainWindow = new BrowserWindow({ width: 1240, height: 840, minWidth: 940, minHeight: 620, frame: false, backgroundColor: '#141414', title: 'PeopleLib 文献库', webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false } }); mainWindow.loadFile(path.join(__dirname, 'src', 'ui', 'index.html')); } library.setChangeListener(() => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('library:changed'); } }); app.whenReady().then(() => { // 应用代理到 Chromium defaultSession(影响 net.fetch、窗口加载、所有请求) const p = getProxy(); if (p) { session.defaultSession.setProxy({ proxyRules: p }).catch(() => {}); } // 允许证书错误的请求继续(net.fetch / 渲染进程 fetch 都会触发) app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { event.preventDefault(); callback(true); }); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); function wrap(promise) { return promise .then((data) => ({ ok: true, data })) .catch((err) => ({ ok: false, error: err.message || String(err) })); } // 数据源 ipcMain.handle('sources:list', () => ({ ok: true, data: sources.listSources() })); ipcMain.handle('source:list', (_e, sourceId, page) => wrap(sources.getSource(sourceId).list(page))); ipcMain.handle('source:search', (_e, sourceId, keyword, page) => wrap(sources.getSource(sourceId).search(keyword, page))); ipcMain.handle('source:detail', (_e, sourceId, postId) => wrap(sources.getSource(sourceId).detail(postId))); ipcMain.handle('source:download', (_e, sourceId, postId) => wrap(sources.getSource(sourceId).download(postId))); // 代理配置:全局生效,影响所有数据源的 HTTP 请求与文件下载 ipcMain.handle('proxy:get', () => ({ ok: true, data: getProxy() })); ipcMain.handle('proxy:set', (_e, url) => { const u = String(url || '').trim(); setProxy(u); settings.set('proxy', u); session.defaultSession.setProxy({ proxyRules: u || 'direct://' }).catch(() => {}); return { ok: true }; }); // Z-Library 凭据 ipcMain.handle('zlib:hasCreds', () => ({ ok: true, data: zlibAuth.hasCreds() })); ipcMain.handle('zlib:login', (_e, email, password) => wrap(sources.getSource('zlib').login(email, password))); ipcMain.handle('zlib:logout', () => wrap(sources.getSource('zlib').logout())); // 本地书库 ipcMain.handle('library:list', () => wrap(Promise.resolve(library.list()))); ipcMain.handle('library:get', (_e, id) => wrap(Promise.resolve(library.get(id)))); ipcMain.handle('library:findBySource', (_e, sourceId, postId) => wrap(Promise.resolve(library.findBySource(sourceId, postId)))); ipcMain.handle('library:add', (_e, item) => wrap(Promise.resolve(library.add(item)))); ipcMain.handle('library:update', (_e, id, patch) => wrap(Promise.resolve(library.update(id, patch)))); ipcMain.handle('library:remove', (_e, id, deleteFiles) => wrap(Promise.resolve(library.remove(id, deleteFiles)))); // 下载文件:直接保存到书库下载目录或弹保存框,支持自定义 headers ipcMain.handle('download:file', async (_e, url, suggestName, entryId, extraHeaders) => { try { const headers = { 'User-Agent': DL_UA, ...(extraHeaders || {}) }; try { headers['Referer'] = new URL(url).origin + '/'; } catch (e) { /* ignore */ } const res = await fetchWithProxy(String(url || ''), { redirect: 'follow', headers }); if (!res.ok) throw new Error(`下载失败: ${res.status}`); const respName = filenameFromResponse(res, suggestName); const hasExt = suggestName && /\.[a-z0-9]{2,5}$/i.test(suggestName); const defaultName = (hasExt ? suggestName : respName).replace(/[\\/:*?"<>|]/g, '_'); const save = await dialog.showSaveDialog(mainWindow, { title: '保存文件', defaultPath: defaultName }); if (save.canceled || !save.filePath) return { ok: true, data: { canceled: true } }; const buf = Buffer.from(await res.arrayBuffer()); fs.writeFileSync(save.filePath, buf); if (entryId) library.attachFile(entryId, save.filePath); shell.showItemInFolder(save.filePath); return { ok: true, data: { path: save.filePath, name: path.basename(save.filePath) } }; } catch (e) { return { ok: false, error: e.message || String(e) }; } }); ipcMain.handle('shell:openPath', async (_e, p) => { const err = await shell.openPath(p || ''); return err ? { ok: false, error: err } : { ok: true }; }); ipcMain.handle('shell:showItem', (_e, p) => { shell.showItemInFolder(p || ''); return { ok: true }; }); ipcMain.handle('shell:openExternal', async (_e, url) => { try { await shell.openExternal(String(url || '')); return { ok: true }; } catch (e) { return { ok: false, error: e.message || String(e) }; } }); ipcMain.handle('dialog:pickFile', async () => { const r = await dialog.showOpenDialog(mainWindow, { title: '选择本地文献文件', properties: ['openFile'], filters: [{ name: '文献', extensions: ['pdf', 'epub', 'mobi', 'txt', 'azw3'] }, { name: '所有文件', extensions: ['*'] }] }); if (r.canceled || !r.filePaths.length) return { ok: true, data: null }; const p = r.filePaths[0]; return { ok: true, data: { path: p, name: path.basename(p, path.extname(p)) } }; }); ipcMain.handle('app:version', () => ({ ok: true, data: app.getVersion() })); ipcMain.handle('copy', (_e, text) => { clipboard.writeText(String(text || '')); return { ok: true }; }); ipcMain.on('win:minimize', () => mainWindow && mainWindow.minimize()); ipcMain.on('win:maximize', () => { if (!mainWindow) return; if (mainWindow.isMaximized()) mainWindow.unmaximize(); else mainWindow.maximize(); }); ipcMain.on('win:close', () => mainWindow && mainWindow.close());