1. 为什么 Win10 下还需要一个端口占用检测工具
在 Win10 上跑本地服务,最让人抓狂的场景之一就是启动项目时控制台甩出一句Address already in use或者10048 端口已被占用,然后你就得切到命令行,敲netstat -ano | findstr :8080,从一堆输出里找到那个 PID,再taskkill /F /PID xxxx。命令本身不难,但每次都要重复这套动作,尤其是同时开好几个微服务、前端 dev server、数据库端口的时候,效率非常低。
Linux 下有lsof -i:端口号一条命令直接定位,Win10 原生没有这么顺手的等价物。netstat能查,但输出格式对人不友好,taskkill又要手动复制 PID。对于刚接触 Windows 命令行的小白来说,这一套组合拳的门槛其实不低。
我试过在 Trae 里用 Python + wxPython 做一个带界面的端口占用检测工具,核心诉求就三个:输入端口号就能扫、扫描结果直接列出占用进程的 PID 和程序名、选中一行就能一键终止。Trae 作为编辑器负责代码补全和调试,Python 负责逻辑,wxPython 负责界面,nuitka 负责打包成 exe 发给同事直接用。
这篇文章会给出完整的依赖清单、可复制的核心检测代码、wxPython 界面配置,以及运行验证步骤。适合谁:在 Win10 上做本地开发、经常被端口冲突卡住、想用 Python 写个小工具但不想从零搭界面的同学。读完你能直接复现一个port_monitor.exe,双击就能用。
需要说明的是,这个工具只做本地端口扫描和进程管理,不涉及任何网络穿透或远程操作。如果你在团队里需要统一管理 API Key 和模型调用,可以顺带了解一下 TaoToken 的接入方式,后面会给出配置片段。
2. 前置准备:Trae 环境、Python 依赖与 TaoToken 接入配置
2.1 Trae 在 Win10 下的基础配置
Trae 是字节做的 AI 编辑器,Win10 下安装包直接双击下一步就行。装完后建议做两件事:一是把 Python 解释器路径配好,二是打开终端面板方便跑命令。Trae 的 AI 补全对 wxPython 这种 GUI 库挺友好,你写wx.Frame的时候它会提示构造函数参数,省去查文档的时间。
Python 版本建议 3.9 到 3.11,太新的版本某些 wxPython wheel 可能还没跟上。装完 Python 后确认python --version和pip --version都能正常输出。
2.2 依赖清单
这个工具只依赖两个东西:wxPython 和 nuitka。wxPython 负责界面,nuitka 负责打包。pip 安装命令如下:
pip install wxPython pip install nuitkawxPython 在 Win10 下安装时如果遇到编译错误,通常是因为缺少 Visual C++ 构建工具。最省事的办法是去 wxPython 官网下载对应 Python 版本的 whl 文件,然后pip install 文件名.whl本地安装。nuitka 打包时需要 C 编译器,Win10 下推荐装 Visual Studio Build Tools,勾选「使用 C++ 的桌面开发」即可。
2.3 TaoToken 接入配置(可选但推荐)
如果你在开发过程中需要调用大模型 API 来做代码补全、日志分析或者自动化脚本,TaoToken 提供了一个统一的接入入口。它的 Base URL 是https://taotoken.net/api,你需要在控制台生成 API Key,然后在代码或工具里配置。
以 Claude Code 或类似支持 Anthropic 协议的工具为例,配置文件通常长这样:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "claude-sonnet-4-20250514" }如果你用的是 Cline、CC Switch 这类工具,配置项一般拆成三件套:Base URL 填https://taotoken.net/api,API Key 填控制台生成的 Key,Model ID 填你要用的模型名。Codex 的auth.json也是类似结构,把 base URL 和 key 填进去就行。
需要提醒的是,TaoToken 只是模型调用的接入层,不替代编辑器本身。Trae 负责写代码,TaoToken 负责在你需要模型能力时提供 API。两者配合使用,开发体验会更顺。
控制台地址和 API Key 管理页面可以通过这个入口进入:https://taotoken.net/api-keys ,模型对话调试可以用 https://taotoken.net/model-chat ,接入文档在 https://taotoken.net/doc 。如果你长期做编码和 Agent 类任务,可以看看 Coding Plan:https://taotoken.net/coding-plan 。
3. 可复制配置:wxPython 界面与端口检测核心代码
3.1 项目结构
在 Trae 里新建一个文件夹,比如port_monitor,里面放三个文件:
port_monitor/ ├── port_monitor.py # 主程序 ├── build.bat # nuitka 打包脚本 └── requirements.txt # 依赖清单requirements.txt内容:
wxPython>=4.2.0 nuitka>=1.83.2 主程序 port_monitor.py
下面是完整的核心代码。它做了几件事:用 wxPython 搭一个窗口,输入端口号后开线程跑netstat -ano,解析出占用该端口的 PID,再用tasklist和wmic拿到程序名和路径,最后把结果填到 ListCtrl 里。选中某一行点「终止占用进程」就执行taskkill。
import wx import subprocess import re import threading class PortMonitorFrame(wx.Frame): def __init__(self, parent, title): super(PortMonitorFrame, self).__init__(parent, title=title, size=(760, 460)) self.InitUI() self.Centre() self.is_scanning = False def InitUI(self): panel = wx.Panel(self) vbox = wx.BoxSizer(wx.VERTICAL) hbox1 = wx.BoxSizer(wx.HORIZONTAL) self.port_input = wx.TextCtrl(panel) hbox1.Add(wx.StaticText(panel, label='端口号:'), flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, border=8) hbox1.Add(self.port_input, proportion=1) hbox2 = wx.BoxSizer(wx.HORIZONTAL) self.check_btn = wx.Button(panel, label='检测端口占用') self.kill_btn = wx.Button(panel, label='终止占用进程') self.check_btn.Bind(wx.EVT_BUTTON, self.on_check_port) self.kill_btn.Bind(wx.EVT_BUTTON, self.on_kill_process) hbox2.Add(self.check_btn, flag=wx.RIGHT, border=8) hbox2.Add(self.kill_btn) self.result_list = wx.ListCtrl(panel, style=wx.LC_REPORT) self.result_list.InsertColumn(0, 'PID', width=90) self.result_list.InsertColumn(1, '程序名称', width=180) self.result_list.InsertColumn(2, '状态', width=120) self.result_list.InsertColumn(3, '是否监听', width=90) self.result_list.InsertColumn(4, '程序路径', width=260) vbox.Add(hbox1, flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border=10) vbox.Add(hbox2, flag=wx.EXPAND | wx.ALL, border=10) vbox.Add(self.result_list, proportion=1, flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=10) panel.SetSizer(vbox) def _startupinfo(self): si = subprocess.STARTUPINFO() si.dwFlags |= subprocess.STARTF_USESHOWWINDOW return si def on_check_port(self, event): port = self.port_input.GetValue().strip() if not port.isdigit(): wx.MessageBox('请输入有效的端口号', '错误', wx.ICON_ERROR) return if self.is_scanning: wx.MessageBox('扫描正在进行中', '提示', wx.ICON_INFORMATION) return self.result_list.DeleteAllItems() self.is_scanning = True self.check_btn.Enable(False) def scan_task(): try: output = subprocess.check_output( ['netstat', '-ano'], text=True, stderr=subprocess.STDOUT, startupinfo=self._startupinfo() ) pattern = re.compile(r'TCP\s+.*?:{}\s+.*?\s+(\d+)'.format(port)) pids = pattern.findall(output) if not pids: wx.CallAfter(wx.MessageBox, '端口未被占用', '提示', wx.ICON_INFORMATION) return for pid in set(pids): if not self.is_scanning: break task_info = subprocess.check_output( ['tasklist', '/fi', f'pid eq {pid}'], text=True, stderr=subprocess.STDOUT, startupinfo=self._startupinfo() ) proc_match = re.search(r'(.+?)\s+\d+\s+Console', task_info) proc_name = proc_match.group(1).strip() if proc_match else '未知程序' detail = re.compile(r'TCP\s+.*?:{}\s+.*?\s+(\S+)\s+({})'.format(port, pid)) status_match = detail.search(output) is_listening = status_match.group(1) == 'LISTENING' if status_match else False try: path_out = subprocess.check_output( ['wmic', 'process', 'where', f'processid={pid}', 'get', 'executablepath'], text=True, stderr=subprocess.STDOUT, startupinfo=self._startupinfo() ) path_match = re.search(r'ExecutablePath\s+(.+?)\s*$', path_out, re.MULTILINE) proc_path = path_match.group(1).strip() if path_match else '未知路径' except Exception: proc_path = '未知路径' wx.CallAfter(self._update_ui, pid, proc_name, is_listening, proc_path) except subprocess.CalledProcessError as e: wx.CallAfter(wx.MessageBox, f'执行命令失败:{e.output}', '错误', wx.ICON_ERROR) finally: self.is_scanning = False wx.CallAfter(self.check_btn.Enable, True) threading.Thread(target=scan_task, daemon=True).start() def _update_ui(self, pid, proc_name, is_listening, proc_path): index = self.result_list.InsertItem(self.result_list.GetItemCount(), pid) self.result_list.SetItem(index, 1, proc_name) self.result_list.SetItem(index, 2, '运行中') self.result_list.SetItem(index, 3, '是' if is_listening else '') self.result_list.SetItem(index, 4, proc_path) if is_listening: self.result_list.SetItemTextColour(index, wx.RED) def on_kill_process(self, event): if self.is_scanning: wx.MessageBox('扫描未完成,请稍后', '提示', wx.ICON_INFORMATION) return selected = self.result_list.GetFirstSelected() if selected == -1: wx.MessageBox('请选择要终止的进程', '提示', wx.ICON_INFORMATION) return pid = self.result_list.GetItemText(selected) try: subprocess.check_output( ['taskkill', '/F', '/PID', pid], text=True, stderr=subprocess.STDOUT, startupinfo=self._startupinfo() ) self.result_list.DeleteItem(selected) wx.MessageBox('进程终止成功', '提示', wx.ICON_INFORMATION) except subprocess.CalledProcessError as e: wx.MessageBox(f'终止进程失败:{e.output}', '错误', wx.ICON_ERROR) if __name__ == '__main__': app = wx.App() frame = PortMonitorFrame(None, '端口监控工具') frame.Show() app.MainLoop()几个关键点解释一下。subprocess.STARTUPINFO配合STARTF_USESHOWWINDOW是为了在调用netstat、tasklist、wmic时不弹出黑色命令行窗口,否则每次扫描都会闪一下。wx.CallAfter是因为扫描跑在子线程里,更新 UI 必须回到主线程,否则 wxPython 会报错。is_listening判断的是连接状态是否为LISTENING,是的话那一行标红,方便你一眼看出哪个进程在监听。
3.3 打包脚本 build.bat
nuitka 打包比 pyinstaller 干净,不会在用户目录留缓存。在项目根目录建一个build.bat:
@echo off nuitka --standalone --onefile --windows-console-mode=disable port_monitor.py echo 打包完成,可执行文件在 dist 目录下 pause--standalone表示独立打包,--onefile表示合成单个 exe,--windows-console-mode=disable表示运行时不显示控制台窗口。双击build.bat等几分钟,dist目录下就会出现port_monitor.exe。
4. 运行验证:从 python 脚本到 exe 的完整测试
4.1 直接跑 Python 脚本
在 Trae 的终端里执行:
python port_monitor.py窗口出来后,输入一个你确定被占用的端口,比如你本地起了个服务在 8080,就输入8080,点「检测端口占用」。正常情况下 ListCtrl 里会出现一行或多行记录,PID、程序名称、状态、是否监听、程序路径都有。如果该端口是 LISTENING 状态,那一行会标红。
如果输入一个没被占用的端口,比如59999,会弹窗提示「端口未被占用」。
4.2 测试终止进程
选中一行,点「终止占用进程」。如果权限足够,会弹「进程终止成功」,同时那一行从列表里消失。如果进程属于系统服务或者需要管理员权限,会弹「终止进程失败」,这时候你需要以管理员身份重新运行工具。
4.3 打包成 exe 再验证
双击build.bat,等它跑完。然后进dist目录,双击port_monitor.exe。第一次启动可能会慢几秒,因为 nuitka 打包的程序需要解压运行时。之后的操作和 Python 脚本完全一致。
实测下来,打包后的 exe 在 Win10 上可以直接发给同事,不需要他们装 Python 和 wxPython。文件体积大概在 30 到 50 MB 之间,取决于 wxPython 的版本。
4.4 验证成功的结果
一个完整的成功流程是这样的:打开 exe,输入3306(假设你本地有 MySQL),点检测,列表出现mysqld.exe,状态显示运行中,是否监听显示「是」,路径显示 MySQL 的 bin 目录。选中它点终止,弹窗成功,列表清空。再点一次检测,提示端口未被占用。
如果你在开发过程中需要调用模型 API 来做一些自动化处理,比如把扫描结果发给模型分析,可以在代码里加一段 HTTP 请求,Base URL 用https://taotoken.net/api,Key 从控制台拿。模型对话调试入口在 https://taotoken.net/model-chat ,接入文档在 https://taotoken.net/doc 。
5. 常见报错排查:401、local proxy failed、reading choices 与 OAuth
5.1 401 错误
如果你在工具里配置了 TaoToken 的 API Key,但请求返回 401,通常是 Key 没填对或者没带上。检查三件套:Base URL 是不是https://taotoken.net/api,Key 是不是从 https://taotoken.net/api-keys 生成的,Model ID 是不是写对了。有时候复制 Key 会多带空格,用strip()处理一下。
5.2 local proxy failed
这个报错一般出现在你本地配了代理,但代理没启动或者端口不对。Win10 下检查「设置 -> 网络和 Internet -> 代理」,确认没有开启手动代理,或者代理地址和端口是正确的。如果你在用 Cline、CC Switch 这类工具,检查它们的网络配置里有没有指向一个不存在的本地端口。
5.3 reading choices 报错
这个通常出现在调用模型 API 时,返回的 JSON 结构里没有choices字段。原因可能是 Base URL 填错了,比如填成了https://taotoken.net而不是https://taotoken.net/api,导致请求打到了网页而不是 API 端点。确认 URL 路径完整。
5.4 OAuth 相关报错
如果你用的是 Claude Code 或者类似需要 OAuth 的工具,报 OAuth 错误通常是 token 过期或者回调地址不对。检查auth.json或配置文件里的 token 是否还有效,必要时重新生成。Codex 的auth.json里 base URL 和 key 要对应上,不要混用不同环境的配置。
5.5 端口扫描本身的问题
如果netstat -ano返回空或者解析不到 PID,可能是权限不够。Win10 下普通用户跑netstat一般没问题,但wmic查进程路径可能需要管理员权限。解决办法是右键 exe 选择「以管理员身份运行」。另外,如果端口号输入了非数字,代码里会弹「请输入有效的端口号」,这是预期行为。
5.6 nuitka 打包报错
如果build.bat跑的时候报缺少 C 编译器,装 Visual Studio Build Tools,勾选「使用 C++ 的桌面开发」。如果报 wxPython 相关模块找不到,试试先pip install --upgrade wxPython,或者用--include-package=wx参数显式包含。
6. 从工具到工作流:把端口检测接入你的日常开发
这个工具本身不复杂,但它的价值在于把「查端口 -> 找 PID -> 杀进程」这三步压缩成一次点击。你可以把它固定在任务栏,遇到端口冲突时直接点开,输入端口号,两秒内看到结果。
如果你想让它在团队里更好用,可以加几个小功能:一是支持多个端口批量扫描,用逗号分隔;二是把扫描历史存到本地 SQLite,方便回溯;三是加一个「复制 PID」按钮,方便你在其他工具里用。这些改动在 Trae 里让 AI 补全帮你写,效率很高。
对于需要调用模型能力的场景,比如自动分析端口占用日志、生成释放建议,可以在工具里加一个「AI 分析」按钮,把扫描结果 POST 到https://taotoken.net/api的对话端点,让模型给出处理建议。配置还是那三件套:Base URL、API Key、Model ID。控制台在 https://taotoken.net/console ,API Key 管理在 https://taotoken.net/api-keys ,文档在 https://taotoken.net/doc 。如果你长期做编码和 Agent 任务,Coding Plan 的入口在 https://taotoken.net/coding-plan 。
最后说一个实际踩过的坑:nuitka 打包时如果用了--onefile,exe 启动会比--standalone慢,因为每次都要解压。如果你追求启动速度,可以去掉--onefile,直接发整个dist文件夹。另外,wxPython 的界面在高分屏上可能有点糊,可以在wx.App初始化前加一行 DPI 感知设置,具体代码在 wxPython 文档里有。