更新压缩机制,提高前缀缓存命中率,优化UI渲染机制,提高帧数

This commit is contained in:
minorQAQ committed 2026-08-25 10:13:50 +08:00
1 parent 6131c2563e
commit 0ce72f74f1
175 files changed
+7621 -2611

No files matched your search

+80 -17
View File
@@ -60,6 +60,25 @@ let mainWindow = null;
let pythonProcess = null;
let isQuitting = false;
let isLaunching = false; // 防止 setup 窗口关闭时触发 app.quit
let _closeConfirmed = false; // 后台任务确认后放行关闭
/** 查询后端运行中的后台终端任务数(退出确认用;后端不可用时视为 0) */
function checkRunningTasks() {
return new Promise((resolve) => {
const req = http.get(`${BASE_URL}/api/terminal/running-count`, { timeout: 2000 }, (res) => {
let body = '';
res.on('data', (d) => (body += d));
res.on('end', () => {
try {
const n = Number(JSON.parse(body).running) || 0;
resolve(n);
} catch { resolve(0); }
});
});
req.on('error', () => resolve(0));
req.on('timeout', () => { req.destroy(); resolve(0); });
});
}
// ==================== 端口清理 ====================
function killPortProcess(port) {
@@ -343,6 +362,28 @@ async function createMainWindow(themeMode) {
mainWindow.on('closed', () => { mainWindow = null; });
// 关闭拦截:若仍有后台终端任务在运行,先弹确认("终止并关闭 / 取消")
mainWindow.on('close', (e) => {
if (isQuitting || _closeConfirmed) return;
e.preventDefault();
checkRunningTasks().then(async (n) => {
if (n > 0) {
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'warning',
buttons: ['终止并关闭', '取消'],
defaultId: 1,
cancelId: 1,
title: '后台终端仍在运行',
message: `仍有 ${n} 个后台终端任务在运行`,
detail: '终止这些任务并关闭应用?(若不终止,进程将与应用一并被清理)',
});
if (response !== 0) return;
}
_closeConfirmed = true;
mainWindow.close();
});
});
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
require('electron').shell.openExternal(url).catch(() => {});
return { action: 'deny' };
@@ -971,11 +1012,29 @@ app.on('before-quit', () => {
stopFastAPI();
});
// ==================== 内置 PowerShell 终端 ====================
// 每个标签对应一个 powershell 子进程;面板折叠不 kill,仅标签 x / 右上角 x / 退出时清理。
// ==================== 内置 PowerShell 终端(ConPTY 真终端) ====================
// 每个标签对应一个经 node-pty (Windows ConPTY) 启动的真实 PowerShell 会话,
// PSReadLine / Tab 补全 / 历史 / 全屏应用由真实控制台提供,前端 xterm.js 渲染。
// 面板折叠不 kill(仅隐藏);标签 x 单个 kill;右上角 x 确认后 kill 全部。
const _terminals = new Map(); // id -> { proc, sender }
let _terminalSeq = 0;
// 优先 PowerShell 7(truecolor / PSReadLine 更强),回退 Windows PowerShell 5.1
let _shellPathCache = '';
function resolveShellPath() {
if (_shellPathCache) return _shellPathCache;
try {
execSync('where pwsh.exe', { stdio: 'ignore', shell: true });
_shellPathCache = 'pwsh.exe';
} catch (e) {
_shellPathCache = path.join(
process.env.SystemRoot || 'C:\\Windows',
'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'
);
}
return _shellPathCache;
}
function forceKill(proc) {
try { proc.kill(); } catch (e) { /* ignore */ }
// Windows 兜底:taskkill 结束整个进程树(处理 Ctrl 信号不响应/子进程残留)
@@ -991,30 +1050,28 @@ function killAllTerminals() {
_terminals.clear();
}
ipcMain.handle('terminal:spawn', (event, { cwd } = {}) => {
ipcMain.handle('terminal:spawn', (event, { cwd, cols, rows } = {}) => {
const id = ++_terminalSeq;
let proc;
try {
proc = spawn('powershell.exe', ['-NoLogo'], {
// ConPTY 需真实控制台;懒加载,模块缺失时不影响应用其余功能
const pty = require('@homebridge/node-pty-prebuilt-multiarch');
proc = pty.spawn(resolveShellPath(), ['-NoLogo'], {
cwd: (cwd && fs.existsSync(cwd)) ? cwd : undefined,
windowsHide: true,
cols: (cols | 0) || 80,
rows: (rows | 0) || 24,
env: { ...process.env, TERM: 'xterm-256color' },
});
} catch (e) {
return { id, ok: false, error: String(e && e.message || e) };
return { id, ok: false, error: String((e && e.message) || e) };
}
_terminals.set(id, { proc, sender: event.sender });
proc.stdout.on('data', (chunk) => {
if (!event.sender.isDestroyed()) event.sender.send('terminal:data', { id, chunk: chunk.toString('utf8') });
proc.onData((chunk) => {
if (!event.sender.isDestroyed()) event.sender.send('terminal:data', { id, chunk });
});
proc.stderr.on('data', (chunk) => {
if (!event.sender.isDestroyed()) event.sender.send('terminal:data', { id, chunk: chunk.toString('utf8') });
});
proc.on('exit', (code) => {
proc.onExit(({ exitCode }) => {
_terminals.delete(id);
if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { id, code });
});
proc.on('error', (err) => {
if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { id, code: -1, error: String(err.message || err) });
if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { id, code: exitCode });
});
return { id, ok: true };
});
@@ -1022,7 +1079,13 @@ ipcMain.handle('terminal:spawn', (event, { cwd } = {}) => {
ipcMain.on('terminal:input', (_event, { id, data } = {}) => {
const t = _terminals.get(id);
if (!t || !data) return;
try { t.proc.stdin.write(data); } catch (e) { /* ignore */ }
try { t.proc.write(data); } catch (e) { /* ignore */ }
});
ipcMain.on('terminal:resize', (_event, { id, cols, rows } = {}) => {
const t = _terminals.get(id);
if (!t) return;
try { t.proc.resize((cols | 0) || 80, (rows | 0) || 24); } catch (e) { /* ignore */ }
});
ipcMain.on('terminal:kill', (_event, { id } = {}) => {
+2 -1
View File
@@ -29,8 +29,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
openInExplorer: (filePath) => ipcRenderer.invoke('shell:openInExplorer', filePath),
// ===== 内置 PowerShell 终端 =====
spawnTerminal: (cwd) => ipcRenderer.invoke('terminal:spawn', { cwd }),
spawnTerminal: (cwd, cols, rows) => ipcRenderer.invoke('terminal:spawn', { cwd, cols, rows }),
sendTerminalInput: (id, data) => ipcRenderer.send('terminal:input', { id, data }),
resizeTerminal: (id, cols, rows) => ipcRenderer.send('terminal:resize', { id, cols, rows }),
killTerminal: (id) => ipcRenderer.send('terminal:kill', { id }),
killAllTerminals: () => ipcRenderer.send('terminal:killAll'),
onTerminalData: (cb) => {