diff --git a/plugins/ztools-kill-process/README.md b/plugins/ztools-kill-process/README.md index 5b480b7fc..430c6ef28 100644 --- a/plugins/ztools-kill-process/README.md +++ b/plugins/ztools-kill-process/README.md @@ -1,6 +1,5 @@ -# ⚡ 进程管理与结束工具 (Ztools / uTools 插件) - -一款轻量、极速、高交互性的 Windows 进程管理与一键结束插件。专门为 Ztools / uTools 开发者及高效办公用户打造,提供秒级进程检索、内存监控、键盘优先操作与批量进程杀死功能。 +# ⚡ 进程管理与结束工具 +一款轻量、极速、高交互性的 Windows / 跨平台进程管理与一键结束插件,提供秒级进程检索、网络端口占用定位、内存监控、键盘优先操作与批量进程强制结束功能。  @@ -8,38 +7,58 @@ ## ✨ 核心特性 -- ⚡ **毫秒级极速响应**:基于底层 PowerShell 结构化指令优化,单次枚举进程仅需不到 100ms,绝不卡顿。 -- 🎨 **主题自动跟随系统**:原生支持 Windows / OS 系统深色(Dark)与浅色(Light)模式,支持宿主软件主题无缝自动切换。 -- 📦 **自动内存格式化**:智能换算进程内存占用(KB / MB / GB),数据直观易读。 -- 🎯 **智能过滤与安全防护**:自动过滤 `Memory Compression` 及 `Idle` 等系统内核级别进程,防止误杀导致系统蓝屏。 -- 🔍 **实时搜索与点击排序**:支持按进程名称(如 `chrome`)或 `PID` 实时搜索;点击表头可自由按名称、PID、内存大小进行升序/降序排列。 -- 📑 **多选与批量结束**: +- ⚡ **毫秒级极速响应**:基于底层 PowerShell 结构化指令与系统原生 API 优化,单次枚举数百个进程仅需不到 100ms,卡顿全无。 +- 🌐 **网络端口占用检索**: + - 自动抓取并关联进程占用的 TCP/UDP 端口(支持区分**监听端口**与**活跃连接**)。 + - 支持直接搜索端口号(如 `8080`、`:3000` 或 `port:5432`),秒级找出占用端口的后端或前端开发服务。 + - 悬浮可查看完整端口列表,点击表头可按「占用端口」进行升降序排列。 +- 🎨 **主题自动跟随系统**:原生支持 Windows / macOS 系统深色(Dark)与浅色(Light)模式,无缝匹配宿主软件主题。 +- 📦 **自动内存格式化**:智能换算进程内存占用(KB / MB / GB),实时可视化对比高占用进程。 +- 🎯 **智能过滤与安全防护**:自动过滤 `Memory Compression` 及 `Idle` 等内核级系统进程,防止误杀导致系统蓝屏崩溃。 +- 🔍 **多维实时搜索与表头排序**: + - 支持按 **进程名称**(如 `chrome`)、**PID** 或 **占用端口** 实时模糊匹配。 + - 点击表头自由按进程名称、PID、占用端口、内存大小进行升序/降序排列。 +- 📑 **灵活多选与批量结束**: - 支持复选框勾选、`Shift` 范围框选、`Ctrl` 点选及 `Ctrl+A` 全选。 - - 提供安全的批量确认弹窗与清单明细展示,支持一键批量强制结束(Taskkill /F)。 + - 提供安全确认弹窗与清单明细展示(包含 PID、端口及内存信息),支持一键批量强制结束(Taskkill /F)。 - ⌨️ **键盘优先高效交互**: - - 打开即自动聚焦搜索框,输入随查随选。 - - 支持键盘上下方向键选择、`Enter` 触发杀死弹窗、`Esc` 快速撤销/清空。 + - 打开插件自动聚焦搜索框,随输随查。 + - 支持全键盘方向键导航、空格键勾选、回车确认结束、Esc 清空/取消。 --- -## ⌨️ 快捷键指南 +## ⌨️ 快捷键与交互指南 -| 快捷键 | 功能说明 | +| 操作 / 快捷键 | 功能说明 | | :--- | :--- | | `↑` / `↓` | 在进程列表中上下切换当前选中行 | -| `Space` (空格) | 快速勾选/取消勾选当前选中的进程 | +| `Space` (空格) | 快速勾选 / 取消勾选当前选中的进程 | | `Shift` + 鼠标点击 | 快速进行范围多选 | -| `Ctrl` / `Cmd` + 鼠标点击 | 自由多选/取消多选指定的进程行 | -| `Ctrl + A` | 一键全选当前搜索过滤结果中的所有进程 | -| `Enter` | 触发单进程结束或批量结束确认弹窗(弹窗内按 `Enter` 确认杀死) | -| `Esc` | 弹窗中取消关闭 / 输入框中清空搜索 / 列表中取消全选 | -| `F5` / `Ctrl + R` | 强制刷新系统进程列表 | +| `Ctrl` / `Cmd` + 鼠标点击 | 自由多选 / 取消多选指定的进程行 | +| `Ctrl + A` | 一键全选当前过滤结果中的所有进程 | +| `鼠标双击` / `Enter` | 触发单进程结束或批量结束确认弹窗(弹窗内按 `Enter` 确认杀死) | +| `Esc` | 弹窗中取消关闭 / 输入框中清空搜索 / 列表中取消勾选全选 | +| `F5` / `Ctrl + R` | 强制刷新系统进程列表与端口占用 | + +--- + +## 💡 典型使用场景 + +### 1. 解决开发端口冲突 (`EADDRINUSE`) +当启动 Node.js, Spring Boot, Vue / React 等项目提示 `Port 8080 is already in use` 时: +1. 输入 `kill` 或 `8080` 唤起插件; +2. 输入 `:8080` 或 `8080`,瞬间定位占用该端口的进程; +3. 按 `Enter` 键直接强制杀死进程,快速释放端口。 + +### 2. 批量清理高内存占用进程 +1. 点击表头 **内存占用** 排序,找到高资源占用进程; +2. 按 `Ctrl` 依次选中多个无需使用的子进程或浏览器进程; +3. 点击 **批量结束** 或按 `Enter` 一键批量释放系统内存。 --- -## 🚀 安装与使用 +## 🚀 唤起关键词 -### 触发关键词 在 Ztools / uTools 搜索框中输入以下任意关键词即可唤起: - `kill` - `进程` diff --git a/plugins/ztools-kill-process/app.js b/plugins/ztools-kill-process/app.js index 8dbe3ead7..d873ee7fe 100644 --- a/plugins/ztools-kill-process/app.js +++ b/plugins/ztools-kill-process/app.js @@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { // Header click sort handlers const thName = document.getElementById('thName'); const thPID = document.getElementById('thPID'); + const thPort = document.getElementById('thPort'); const thMem = document.getElementById('thMem'); // Modal Elements @@ -88,11 +89,11 @@ document.addEventListener('DOMContentLoaded', () => { // Resolution of process service from preload const processService = window.services || window.processService || { getProcesses: async () => [ - { name: 'chrome.exe', pid: 12100, memoryStr: '132.3 MB', memoryKB: 135480 }, - { name: 'Antigravity.exe', pid: 65176, memoryStr: '252.4 MB', memoryKB: 258488 }, - { name: 'ZTools.exe', pid: 56044, memoryStr: '91.9 MB', memoryKB: 94092 }, - { name: 'WeChatAppEx.exe', pid: 54964, memoryStr: '133.1 MB', memoryKB: 136308 }, - { name: 'Unity.exe', pid: 28976, memoryStr: '0.96 GB', memoryKB: 1002752 } + { name: 'chrome.exe', pid: 12100, memoryStr: '132.3 MB', memoryKB: 135480, listeningPorts: [8080, 8081], allPorts: [8080, 8081], primaryPort: 8080, portsStr: ':8080, :8081' }, + { name: 'Antigravity.exe', pid: 65176, memoryStr: '252.4 MB', memoryKB: 258488, listeningPorts: [], allPorts: [54321], primaryPort: 54321, portsStr: ':54321' }, + { name: 'ZTools.exe', pid: 56044, memoryStr: '91.9 MB', memoryKB: 94092, listeningPorts: [], allPorts: [], primaryPort: Infinity, portsStr: '' }, + { name: 'WeChatAppEx.exe', pid: 54964, memoryStr: '133.1 MB', memoryKB: 136308, listeningPorts: [], allPorts: [], primaryPort: Infinity, portsStr: '' }, + { name: 'Unity.exe', pid: 28976, memoryStr: '0.96 GB', memoryKB: 1002752, listeningPorts: [3000], allPorts: [3000], primaryPort: 3000, portsStr: ':3000' } ], killProcess: async (pid) => `Mock killed process PID ${pid}` }; @@ -120,12 +121,20 @@ document.addEventListener('DOMContentLoaded', () => { }, 3000); } + let isFetchingProcesses = false; + let searchDebounceTimer = null; + // Load and refresh process list - async function loadProcesses(keepIndex = true) { + async function loadProcesses(keepIndex = true, silent = false) { + if (isFetchingProcesses) return; + isFetchingProcesses = true; + adjustPluginHeight(580); - loadingState.style.display = 'flex'; - emptyState.style.display = 'none'; - processListEl.style.display = 'none'; + if (!silent) { + loadingState.style.display = 'flex'; + emptyState.style.display = 'none'; + processListEl.style.display = 'none'; + } try { allProcesses = await processService.getProcesses(); @@ -141,21 +150,44 @@ document.addEventListener('DOMContentLoaded', () => { applyFilterAndSort(keepIndex); } catch (err) { console.error('Failed to load processes:', err); - showToast('获取进程列表失败: ' + (err.message || '未知错误'), 'error'); + if (!silent) { + showToast('获取进程列表失败: ' + (err.message || '未知错误'), 'error'); + } } finally { - loadingState.style.display = 'none'; - processListEl.style.display = 'block'; - if (searchInput && !isModalOpen) { + isFetchingProcesses = false; + if (!silent) { + loadingState.style.display = 'none'; + processListEl.style.display = 'block'; + } + if (searchInput && !isModalOpen && document.activeElement !== searchInput) { searchInput.focus(); } } } + // Window Focus & Plugin Enter Lifecycle Events for Auto-Refresh + window.onPluginEnter = function(action) { + loadProcesses(true, allProcesses.length > 0); + }; + + window.addEventListener('focus', () => { + if (!isModalOpen && !isFetchingProcesses) { + loadProcesses(true, true); + } + }); + + document.addEventListener('visibilitychange', () => { + if (!document.hidden && !isModalOpen && !isFetchingProcesses) { + loadProcesses(true, true); + } + }); + // Update header sort direction icons function updateHeaderSortIcons() { const headers = [ { el: thName, field: 'name' }, { el: thPID, field: 'pid' }, + { el: thPort, field: 'primaryPort' }, { el: thMem, field: 'memoryKB' } ]; @@ -209,18 +241,36 @@ document.addEventListener('DOMContentLoaded', () => { // Filter and Sort logic function applyFilterAndSort(keepIndex = false) { - const query = searchInput.value.trim().toLowerCase(); + const rawQuery = searchInput.value.trim().toLowerCase(); // Filter - if (!query) { + if (!rawQuery) { filteredProcesses = [...allProcesses]; searchClear.style.display = 'none'; } else { searchClear.style.display = 'flex'; + + // Clean query for port matching (e.g. ":8080", "port:8080", "port=8080" -> "8080") + const cleanPortQuery = rawQuery.replace(/^port\s*[:=]?\s*/i, '').replace(/^[:]/, '').trim(); + filteredProcesses = allProcesses.filter(p => { - const nameMatch = p.name.toLowerCase().includes(query); - const pidMatch = p.pid.toString().includes(query); - return nameMatch || pidMatch; + const nameMatch = p.name.toLowerCase().includes(rawQuery); + const pidMatch = p.pid.toString().includes(rawQuery); + + let portMatch = false; + if (cleanPortQuery) { + const listeningPorts = p.listeningPorts || []; + const allPorts = p.allPorts || []; + const portsStr = p.portsStr || ''; + + const hasListeningMatch = listeningPorts.some(port => port.toString().includes(cleanPortQuery)); + const hasAllMatch = allPorts.some(port => port.toString().includes(cleanPortQuery)); + const hasStrMatch = portsStr.toLowerCase().includes(rawQuery) || portsStr.toLowerCase().includes(cleanPortQuery); + + portMatch = hasListeningMatch || hasAllMatch || hasStrMatch; + } + + return nameMatch || pidMatch || portMatch; }); } @@ -229,6 +279,9 @@ document.addEventListener('DOMContentLoaded', () => { let valA = a[currentSort.field]; let valB = b[currentSort.field]; + if (valA === undefined || valA === null) valA = currentSort.field === 'primaryPort' ? Infinity : ''; + if (valB === undefined || valB === null) valB = currentSort.field === 'primaryPort' ? Infinity : ''; + if (typeof valA === 'string') valA = valA.toLowerCase(); if (typeof valB === 'string') valB = valB.toLowerCase(); @@ -251,6 +304,36 @@ document.addEventListener('DOMContentLoaded', () => { updateBatchKillState(); } + // Render Port Badges Helper + function renderPortBadges(proc) { + const listening = proc.listeningPorts || []; + const all = proc.allPorts || []; + + if (listening.length > 0) { + const showPorts = listening.slice(0, 2); + const extraCount = listening.length - showPorts.length; + const fullTooltip = `监听端口: ${listening.map(p => ':' + p).join(', ')}`; + + let html = showPorts.map(p => `:${p}`).join(' '); + if (extraCount > 0) { + html += ` +${extraCount}`; + } + return html; + } else if (all.length > 0) { + const showPorts = all.slice(0, 2); + const extraCount = all.length - showPorts.length; + const fullTooltip = `活跃连接端口: ${all.map(p => ':' + p).join(', ')}`; + + let html = showPorts.map(p => `:${p}`).join(' '); + if (extraCount > 0) { + html += ` +${extraCount}`; + } + return html; + } + + return `-`; + } + // Render Process Items function renderList() { processListEl.innerHTML = ''; @@ -268,6 +351,8 @@ document.addEventListener('DOMContentLoaded', () => { item.className = `process-item ${index === selectedIndex ? 'selected' : ''} ${isChecked ? 'checked-row' : ''}`; item.setAttribute('data-index', index); + const portHtml = renderPortBadges(proc); + item.innerHTML = `