1. Win10 端口占用检测工具:为什么我要用 Trae 重写一遍
在 Win10 上跑 Python 项目,最烦的场景之一就是启动服务时弹出OSError: [WinError 10048] 通常每个套接字地址只允许使用一次,翻译成人话就是端口被占了。Linux 下有lsof -i:端口号一行命令搞定,Windows 下你得先netstat -ano | findstr 8080找到 PID,再tasklist /fi "pid eq 12345"查进程名,最后taskkill /F /PID 12345干掉它,三步下来命令行敲得手酸。
我这次用 Trae 配合 Python + wxPython 做了一个带界面的端口占用检测工具,输入端口号点一下就能看到占用进程的 PID、程序名、状态和可执行路径,选中后一键终止。但真正让我想写这篇文章的,不是界面本身,而是开发过程中踩到的一个坑:Trae 里同时挂了 Cline、CC Switch 好几个 AI 编码插件,每个都要填 API Key、Base URL、模型名,配置散落在不同插件的 settings.json 里,改一次要翻四五个文件。后来我把它们统一收敛到 TaoToken 的一套 Key 和 API 通道上,config.toml 写一份,各插件引用同一份配置,才算清爽。
这篇内容适合两类人:一是想在 Win10 上快速搭一个端口检测小工具的 Python 新手,二是同时用多个 AI 编码工具、被 Key 管理搞烦的开发者。下面我会先给 TaoToken 的统一配置骨架,再给完整的 wxPython 界面代码和 Nuitka 打包脚本,最后演示一次真实的端口扫描验证。
2. TaoToken 前置:一套 Key 打通 Trae 里的多个 AI 工具
Trae 本身是字节出的 AI IDE,内置了模型对话能力,但很多人会额外装 Cline 做 Agent 式编码、装 CC Switch 切换不同的模型通道。问题在于每个插件都有自己的配置入口,Key 填重复了不说,换模型时还得逐个改。
TaoToken 在这里的角色是一个统一的 API 通道:你只需要在官网注册后拿到一个 Key,然后在各个工具里把 Base URL 指向https://taotoken.net/api,模型名按需填,就能共用同一套凭证。这样做的好处是,Trae 内置对话、Cline、CC Switch 走的是同一个出口,额度、日志、模型切换都在一处管理,不用再记五六个不同的 Key。
具体操作路径我列一下,你按需点:
- 注册和拿 Key:进官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,登录后在控制台创建 API Key。
- 控制台管理:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- 创建 Key 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- 模型对话体验:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
注意:API 地址统一用
https://taotoken.net/api,不要在后面拼多余的路径,OpenAI 兼容协议下客户端会自动补/v1/chat/completions。
如果你主要是长期跑编码 Agent,比如让 Cline 连续改代码、跑测试,建议看一下 Coding Plan,额度模型更适合高频调用:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
3. 可复制配置:config.toml 与 settings.json 骨架
我习惯把公共配置抽到一个config.toml里,放在项目根目录,然后各个插件的settings.json引用它。这样换 Key 或换模型只改一处。
3.1 config.toml 统一配置骨架
# config.toml —— 放在项目根目录,供各 AI 插件读取 [taotoken] base_url = "https://taotoken.net/api" api_key = "sk-你的TaoToken密钥" default_model = "claude-sonnet-4-20250514" timeout = 60 [taotoken.models] chat = "claude-sonnet-4-20250514" coding = "claude-sonnet-4-20250514" fast = "gpt-4o-mini" [port_monitor] default_port = 8080 scan_timeout = 10Python 侧读取用标准库tomllib(Python 3.11+)或tomli:
import tomllib with open("config.toml", "rb") as f: cfg = tomllib.load(f) base_url = cfg["taotoken"]["base_url"] api_key = cfg["taotoken"]["api_key"]3.2 Cline 的 settings.json 接入
Cline 在 VS Code / Trae 里的配置存在扩展设置里,也可以直接改settings.json。关键字段是 API Provider 选 OpenAI Compatible,然后填 Base URL 和 Key:
{ "cline.apiProvider": "openai", "cline.openAiBaseUrl": "https://taotoken.net/api", "cline.openAiApiKey": "sk-你的TaoToken密钥", "cline.openAiModelId": "claude-sonnet-4-20250514", "cline.customInstructions": "回答用中文,代码注释用中文" }3.3 CC Switch 的 settings.json 接入
CC Switch 用来在多个模型通道间切换,配置结构类似,重点是baseUrl和apiKey指向同一套:
{ "providers": [ { "name": "taotoken", "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的TaoToken密钥", "models": ["claude-sonnet-4-20250514", "gpt-4o-mini"], "active": true } ] }提示:两个插件都指向同一个 Base URL 和 Key 后,你在 TaoToken 控制台看到的调用日志就是合并的,排查哪个插件在乱调模型会方便很多。
4. wxPython 界面骨架与端口扫描核心逻辑
配置搞定后,回到工具本身。整个程序分三块:界面布局、扫描线程、进程终止。我把它拆开讲,方便你对照改。
4.1 界面布局:输入框 + 两个按钮 + 结果列表
主窗口用wx.Frame,里面放一个垂直 BoxSizer,顶部是端口输入框,中间是两个按钮,底部是wx.ListCtrl表格,列分别是 PID、程序名称、状态、是否为主程序、程序路径。
import wx import subprocess import re import threading class PortMonitorFrame(wx.Frame): def __init__(self, parent, title): super().__init__(parent, title=title, size=(760, 460)) self.is_scanning = False self.InitUI() self.Centre() def InitUI(self): panel = wx.Panel(self) vbox = wx.BoxSizer(wx.VERTICAL) hbox1 = wx.BoxSizer(wx.HORIZONTAL) self.port_input = wx.TextCtrl(panel, value="8080") 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) for i, (col, w) in enumerate([("PID", 90), ("程序名称", 180), ("状态", 110), ("是否为主程序", 110), ("程序路径", 260)]): self.result_list.InsertColumn(i, col, width=w) vbox.Add(hbox1, flag=wx.EXPAND | wx.ALL, border=10) vbox.Add(hbox2, flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=10) vbox.Add(self.result_list, proportion=1, flag=wx.EXPAND | wx.ALL, border=10) panel.SetSizer(vbox)4.2 扫描线程:netstat 解析 + tasklist 查进程名
扫描必须放子线程,否则界面会卡死。核心是调netstat -ano拿全部连接,用正则匹配目标端口,再对每个 PID 调tasklist拿进程名,调wmic拿可执行路径。
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) threading.Thread(target=self.scan_task, args=(port,), daemon=True).start() def scan_task(self, port): try: si = subprocess.STARTUPINFO() si.dwFlags |= subprocess.STARTF_USESHOWWINDOW output = subprocess.check_output( ["netstat", "-ano"], text=True, stderr=subprocess.STDOUT, startupinfo=si ) pattern = re.compile(r"TCP\s+.*?:{}\s+.*?\s+(\d+)".format(port)) pids = set(pattern.findall(output)) if not pids: wx.CallAfter(wx.MessageBox, "端口未被占用", "提示", wx.ICON_INFORMATION) return for pid in pids: task_info = subprocess.check_output( ["tasklist", "/fi", f"pid eq {pid}"], text=True, stderr=subprocess.STDOUT, startupinfo=si ) m = re.search(r"(.+?)\s+\d+\s+Console", task_info) proc_name = m.group(1).strip() if m else "未知程序" is_listening = "LISTENING" in output.split(pid)[0][-80:] proc_path = self.get_proc_path(pid, si) 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) def get_proc_path(self, pid, si): try: out = subprocess.check_output( ["wmic", "process", "where", f"processid={pid}", "get", "executablepath"], text=True, stderr=subprocess.STDOUT, startupinfo=si ) m = re.search(r"ExecutablePath\s+(.+?)\s*$", out, re.MULTILINE) return m.group(1).strip() if m else "未知路径" except Exception: return "未知路径" def update_ui(self, pid, proc_name, is_listening, proc_path): idx = self.result_list.InsertItem(self.result_list.GetItemCount(), pid) self.result_list.SetItem(idx, 1, proc_name) self.result_list.SetItem(idx, 2, "运行中") self.result_list.SetItem(idx, 3, "是" if is_listening else "") self.result_list.SetItem(idx, 4, proc_path) if is_listening: self.result_list.SetItemTextColour(idx, wx.RED)4.3 终止进程:taskkill 一键清理
选中表格某一行,点终止按钮,调taskkill /F /PID强制结束,成功后从列表删掉那一行。
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: si = subprocess.STARTUPINFO() si.dwFlags |= subprocess.STARTF_USESHOWWINDOW subprocess.check_output( ["taskkill", "/F", "/PID", pid], text=True, stderr=subprocess.STDOUT, startupinfo=si ) 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()5. 验证请求:跑一次真实端口扫描
代码写完后,先直接跑 Python 脚本验证:
pip install wxpython python port_monitor.py窗口出来后,我故意先起一个占端口的服务来测:
python -m http.server 8080然后在工具里输入8080,点「检测端口占用」。实测下来,表格里会立刻出现一行:PID 是 python 进程号,程序名称python.exe,状态「运行中」,是否为主程序「是」,路径指向你的 Python 安装目录,整行标红。选中它点「终止占用进程」,弹窗提示成功,再点一次检测就显示「端口未被占用」。
如果你还想验证 TaoToken 那套配置是否生效,可以在 Trae 里打开 Cline,让它读一下config.toml并解释字段含义。如果 Cline 能正常返回中文解释,说明 Base URL 和 Key 都通了。想单独测模型通道,用模型对话页面发一条消息最快:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
5.1 Nuitka 打包成 exe
确认脚本能跑后,用 Nuitka 编译成单文件 exe。相比 PyInstaller,Nuitka 把 Python 编译成 C 再编译,启动更快,也不会在用户目录留一堆缓存。
pip install nuitka写一个build.bat:
@echo off nuitka --standalone --onefile --windows-console-mode=disable ^ --enable-plugin=wx ^ --output-dir=dist ^ port_monitor.py echo 打包完成,可执行文件在 dist 目录下 pause注意这里加了--enable-plugin=wx,wxPython 有动态库依赖,不加这个插件打包出来的 exe 运行时会报找不到 DLL。跑完build.bat,dist目录下就有port_monitor.exe,双击直接运行,不需要装 Python 环境。
6. 本篇常见错排查
报错一:ModuleNotFoundError: No module named 'wx'说明 wxPython 没装成功。Win10 上建议用pip install wxpython装预编译 wheel,如果卡在编译,检查 Python 版本是不是 3.12+,部分老版本 wxPython 对 3.12 支持滞后,换 3.11 更稳。
报错二:打包后 exe 双击闪退九成是没加--enable-plugin=wx,或者--windows-console-mode=disable把错误信息也吞了。排查时先改成--windows-console-mode=force重新打包,看控制台报什么错。
报错三:netstat输出解析不到 PIDWin10 中文系统下netstat -ano的表头是中文,但数据行格式不变,正则匹配TCP\s+.*?:端口\s+.*?\s+(\d+)依然有效。如果匹配不到,先手动跑一遍netstat -ano | findstr 8080看输出格式,确认端口确实处于 LISTENING 或 ESTABLISHED。
报错四:taskkill提示「拒绝访问」目标进程可能是系统服务或需要管理员权限。右键 exe 选「以管理员身份运行」,或者在代码里加 UAC 提权判断。普通用户进程一般不会遇到。
报错五:Cline 报 401 或 model not found先确认settings.json里 Base URL 是https://taotoken.net/api,没有多余斜杠;再确认 Key 没有前后空格。模型名要和控制台里可用的模型一致,写错了会返回 model not found。接入细节可以对照文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
报错六:扫描时界面卡死说明扫描逻辑跑在主线程了。检查on_check_port里是不是用threading.Thread起的子线程,所有 UI 更新是不是都包在wx.CallAfter里。wxPython 不允许子线程直接操作控件。
7. 把 Key 和工具都收进一套配置里
这个端口检测工具本身不复杂,真正省时间的是把 Trae 里多个 AI 插件的 Key 收敛到 TaoToken 一套通道上。config.toml 写一份,Cline 和 CC Switch 的 settings.json 都指向https://taotoken.net/api,以后换模型、查额度、看调用日志都在一个控制台里完成,不用再翻四五个配置文件。
如果你后面想让 Cline 帮你继续扩展这个工具,比如加个「扫描全部监听端口」按钮、加导出 CSV 功能,直接让它读config.toml和port_monitor.py就行,模型通道已经通了。长期跑编码 Agent 的话,Coding Plan 的额度模型更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
最后留一个我踩过的坑:Nuitka 打包时如果项目里引用了config.toml,记得用--include-data-files=config.toml=config.toml把它一起打进去,否则 exe 运行时读不到配置,会直接抛FileNotFoundError。这个报错不会在编译阶段出现,只有双击 exe 才暴露,第一次打包很容易漏。