Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 41 additions & 22 deletions plugins/ztools-kill-process/README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,64 @@
# ⚡ 进程管理与结束工具 (Ztools / uTools 插件)

一款轻量、极速、高交互性的 Windows 进程管理与一键结束插件。专门为 Ztools / uTools 开发者及高效办公用户打造,提供秒级进程检索、内存监控、键盘优先操作与批量进程杀死功能。
# ⚡ 进程管理与结束工具
一款轻量、极速、高交互性的 Windows / 跨平台进程管理与一键结束插件,提供秒级进程检索、网络端口占用定位、内存监控、键盘优先操作与批量进程强制结束功能。

![插件界面预览](logo.png)

---

## ✨ 核心特性

- ⚡ **毫秒级极速响应**:基于底层 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`
- `进程`
Expand Down
137 changes: 118 additions & 19 deletions plugins/ztools-kill-process/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`
};
Expand Down Expand Up @@ -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();
Expand All @@ -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' }
];

Expand Down Expand Up @@ -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;
});
}

Expand All @@ -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();

Expand All @@ -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 => `<span class="port-badge" title="${fullTooltip}">:${p}</span>`).join(' ');
if (extraCount > 0) {
html += ` <span class="port-badge-more" title="${fullTooltip}">+${extraCount}</span>`;
}
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 => `<span class="port-badge port-badge-active" title="${fullTooltip}">:${p}</span>`).join(' ');
if (extraCount > 0) {
html += ` <span class="port-badge-more" title="${fullTooltip}">+${extraCount}</span>`;
}
return html;
}

return `<span class="no-port">-</span>`;
}

// Render Process Items
function renderList() {
processListEl.innerHTML = '';
Expand All @@ -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 = `
<div class="checkbox-cell">
<input type="checkbox" class="row-checkbox" data-index="${index}" ${isChecked ? 'checked' : ''}>
Expand All @@ -288,6 +373,7 @@ document.addEventListener('DOMContentLoaded', () => {
<span>${escapeHtml(proc.name)}</span>
</div>
<div class="pid-cell">${proc.pid}</div>
<div class="port-cell">${portHtml}</div>
<div class="mem-cell">${escapeHtml(proc.memoryStr)}</div>
<div class="action-cell">
<button type="button" class="btn-kill-row" data-action="kill" data-index="${index}">
Expand Down Expand Up @@ -435,6 +521,10 @@ document.addEventListener('DOMContentLoaded', () => {
<span class="info-label">进程 PID:</span>
<span class="info-value">${proc.pid}</span>
</div>
<div class="info-row">
<span class="info-label">占用端口:</span>
<span class="info-value" style="color: var(--accent-green); font-family: var(--font-mono);">${escapeHtml(proc.portsStr || '无')}</span>
</div>
<div class="info-row">
<span class="info-label">内存占用:</span>
<span class="info-value">${escapeHtml(proc.memoryStr)}</span>
Expand All @@ -459,7 +549,7 @@ document.addEventListener('DOMContentLoaded', () => {

let html = selectedProcs.map(p => `
<div class="modal-proc-tag">
<span><strong>${escapeHtml(p.name)}</strong> (PID: ${p.pid})</span>
<span><strong>${escapeHtml(p.name)}</strong> (PID: ${p.pid}${p.portsStr ? ' | ' + escapeHtml(p.portsStr) : ''})</span>
<span style="color: #38bdf8; font-family: var(--font-mono); font-size: 11px;">${p.memoryStr}</span>
</div>
`).join('');
Expand Down Expand Up @@ -647,12 +737,20 @@ document.addEventListener('DOMContentLoaded', () => {

// Search Input Event
searchInput.addEventListener('input', () => {
// 1. Instantly filter existing cached processes for fast response
applyFilterAndSort(false);

// 2. Debounce (300ms) background refresh from OS to catch newly launched processes/ports
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
searchDebounceTimer = setTimeout(() => {
loadProcesses(true, true);
}, 300);
});

searchClear.addEventListener('click', () => {
searchInput.value = '';
applyFilterAndSort(false);
loadProcesses(true, true);
searchInput.focus();
});

Expand All @@ -674,6 +772,7 @@ document.addEventListener('DOMContentLoaded', () => {

thName.addEventListener('click', () => toggleSort('name', false));
thPID.addEventListener('click', () => toggleSort('pid', false));
if (thPort) thPort.addEventListener('click', () => toggleSort('primaryPort', false));
thMem.addEventListener('click', () => toggleSort('memoryKB', true));

// Modal Mouse Click Handlers
Expand Down
Loading
Loading