news 2026/10/23 3:06:03

Easy-Tools v0.3.0:一个用 ctypes + PySide6 写的 Windows 桌面效率工具(中键动作面板 / 事件链 / 本地 OCR)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Easy-Tools v0.3.0:一个用 ctypes + PySide6 写的 Windows 桌面效率工具(中键动作面板 / 事件链 / 本地 OCR)

Easy-Tools v0.3.0:一个用 ctypes + PySide6 写的 Windows 桌面效率工具(中键动作面板 / 事件链 / 本地 OCR)

源码:https://github.com/FortC/Easy-Tools | License:Apache-2.0 | 版本:v0.3.0
技术栈:Python 3 + PySide6 + 纯 ctypes Win32 + llama.cpp | 代码量约 11.5k 行 / 26 个测试文件


下载地址

一、写在前面:为什么又造一个轮子

QWin、Quicker、AutoHotkey、Power Automate 都已经是成熟方案,这个项目并不比它们功能多。本文写给想自己实现一个桌面动作面板,或者想看一个完整桌面工具从 PRD 到发布的全过程的开发者。

Easy-Tools 的定位很窄,但目标明确:

设计约束具体做法
触发要零成本鼠标中键/侧键全局钩子,短按呼出、拖动保持原生滚动
面板要认上下文两级识别:进程名/标题正则(L1,零成本)+ 本地 OCR(L2,带防抖缓存)
能力要可编排事件链 =触发器 → 条件 → 动作序列的 JSON 流水线
数据要留在本机内置 GGUF 模型本地推理;OCR 可选依赖;云端调用需显式选择
依赖要少Win32 接入层不用 pywin32 / pynput,全部用标准库 ctypes

二、目录结构

easy-tools/ ├── main.py # 入口:托盘、单实例、初始化顺序 ├── easy_tools/ │ ├── core/ │ │ ├── app.py # 应用生命周期、托盘、GUI 线程队列 │ │ ├── chain.py # 事件链引擎(节点调度、变量作用域) │ │ ├── context.py # 上下文识别:L1 进程/标题 + L2 OCR 融合 │ │ ├── ocr.py # OCR 服务(后台线程、防抖、结果缓存) │ │ ├── config_store.py # 配置读写、保存即生效 │ │ ├── question_bank.py # Excel 题库解析、列映射、检索索引 │ │ ├── builtin_model.py # 内置 GGUF 模型封装 │ │ ├── ccswitch.py # 外部模型配置导入 │ │ ├── update_client.py # 更新检查 │ │ └── actions/ │ │ ├── base.py # 动作基类 + 注册机制 │ │ ├── openers.py # 打开应用/文件/URL/命令 │ │ ├── text_input.py # 文本投递:粘贴 / 逐字键入 │ │ ├── clipboard.py # 剪贴板动作 │ │ └── processors.py # 40+ 可复用处理器(同时是链步骤) │ ├── win32/ # ★ 纯 ctypes 接入层 │ │ ├── mouse.py # WH_MOUSE_LL 低级钩子、按键判定 │ │ ├── input.py # SendInput 合成按键/文本 │ │ ├── window.py # 前台窗口、进程名、标题 │ │ ├── clipboard.py # 剪贴板读写 │ │ ├── gdi.py # 屏幕/窗口位图抓取 │ │ └── api.py # 结构体声明、错误码翻译 │ ├── ui/ │ │ ├── panel.py # 动作面板(搜索/Tab/分页/定位) │ │ ├── settings.py # 配置中心(14 页侧边导航) │ │ ├── chain_editor.py # 事件链可视化编辑器 │ │ ├── question_bank_page.py │ │ ├── capture_overlay.py # 框选遮罩层 │ │ ├── widgets.py # 自绘控件 │ │ ├── theme.py # 主题与 QSS │ │ └── icons/ # Lucide SVG 图标集(50 个) │ ├── config/ │ │ ├── schema.py # 配置结构定义 │ │ └── defaults.json # 默认动作 / 规则 / 事件链 │ └── diagnostics/ │ └── capture_workflow.py # 截图 → OCR → 提问 全流程 ├── installer/ # C# 编译的安装器(.NET Framework) ├── tests/ # 26 个测试文件 ├── docs/ # 设计方案 / 隐私设计 / 开发与发布规范 └── _doc/prd/ # 6 份需求文档(PRD)

三、Win32 接入层:为什么不用 pywin32

这是整个项目最有立场的一个技术决策。

3.1 依赖问题

requirements.txt里刻意没有pywin32和pynput:

# 刻意不列 pywin32 / pynput:Win32 接入层用纯标准库 ctypes 实现# (见 easy_tools/win32/),理由见 easy_tools/win32/__init__.py。# 少两个原生依赖,就少两类"换 Python 版本装不上"的问题。

实际收益:原生扩展(.pyd)是 Python 小版本绑定的。用户在 Python 3.11 / 3.12 / 3.13 之间切换时,pywin32 常常要重新编译或找不到匹配 wheel。ctypes 是标准库,跟着解释器走,不存在这个问题。整个 Win32 层(鼠标钩子、SendInput、窗口枚举、剪贴板、GDI 抓图)不到 1400 行,全是结构体声明 + 函数指针。

3.2 全局鼠标钩子

核心是WH_MOUSE_LL低级钩子:

SetWindowsHookExW(WH_MOUSE_LL,hook_proc,hmod,0)

hook_proc必须是可持久化的函数指针,不能是临时 lambda——否则回调触发时已被回收,直接崩。项目里用一个模块级函数 +CFUNCTYPE持有引用:

HOOKPROC=WINFUNCTYPE(c_longlong,c_int,UINT,WPARAM,LPARAM)_proc=HOOKPROC(_mouse_proc)# 关键:模块级持有,防止 GC_hhk=user32.SetWindowsHookExW(WH_MOUSE_LL,_proc,hmod,0)

3.3 短按 vs 拖动的判定

中键在浏览器/编辑器里是滚动,直接劫持会毁掉用户习惯。判定逻辑:

按下 → 记录 (x, y, timestamp) ├── 位移 < drag_threshold_px 且 释放时间 - 按下时间 < tap_timeout │ → 判定为「呼出面板」,并按配置决定是否 swallow(吞掉这次点击) └── 否则 → 完全交给系统,触发原生中键滚动

drag_threshold_px和消费模式(native_passthrough/swallow_tap)都在配置中心可配。这个阈值是 v0.3.0 统一到所有触发键的——中键、X1、X2 走同一套判定逻辑。

对应的回归测试在tests/test_mouse_hook.py,钩子结构体声明有tests/test_win32_decls.py守着。

3.4 文本投递的两种实现

粘贴模式(兼容性最好):

backup=get_clipboard()# 1. 备份set_clipboard(text)# 2. 写入send_keys(Ctrl+V)# 3. 模拟粘贴QTimer.singleShot(delay,lambda:set_clipboard(backup))# 4. 延迟恢复

第 4 步的延迟恢复是关键——立刻恢复会让目标应用还没读完就丢数据。

逐字键入模式:SendInput逐字符构造KEYEVENTF_UNICODE,天然支持中英文数字混排和换行(\r→ 回车)。速度可配基础间隔 + 随机抖动,按Esc随时中断。


四、上下文识别:两级融合

4.1 L1 — 进程名 + 标题正则(零成本)

fg=get_foreground_window()proc=get_process_name(fg)# → "WINWORD.EXE"title=get_window_title(fg)# → "报销单2026.docx - Word"

直接读,耗时在微秒级。规则匹配支持进程名、标题正则、OCR 关键词三个条件。

4.2 L2 — 本地 OCR(带防抖和缓存)

# 窗口切换后防抖 ~300ms 再触发识别# 结果按 (hwnd, size, mtime) 缓存# 后台线程执行,面板弹出不等它

关键设计:面板弹出不等 OCR。先按 L1 规则把面板渲染出来,L2 结果到达后再动态补充分组。所以识别再慢也不影响弹窗手感。

4.3 融合策略

{"processes":["WINWORD.EXE","wps.exe"],"match":{"titleRegex":"\\.docx?$","ocrKeywords":["插入","页面布局"]},"action_ids":["word.cleanup","word.trim","word.md_table"]}

匹配到的动作组会排在公共动作之前——用户在 Excel 里,90% 的意图是处理表格。


五、事件链引擎

5.1 数据结构

{"id":"chain_trim_transpose","name":"整理选中材料","trigger":{"type":"manual"},"steps":[{"type":"action","id":"clipboard.copy","output":"sel"},{"type":"action","id":"text.drop_blank_lines","input":"{sel}"},{"type":"condition","expr":"has_table"},{"type":"action","id":"table.transpose","input":"{step2.output}"},{"type":"action","id":"input.paste","input":"{step4.output}"}]}

步骤类型:action/condition/loop/wait/python/clipboard(处理器)。

变量系统:{clipboard}、{selection}、{ocr_text}、{stepN.output}、{var.name}在步骤间传递。

5.2 处理器复用

设计上的一个巧思:剪贴板处理器和事件链步骤是同一套东西。40+ 个处理器(大小写转换、日期格式化、千分位、去重、排序、表格转 CSV、Markdown 表格互转……)既能在面板里当按钮点,也能直接拖进链里当步骤。

# easy_tools/core/actions/processors.py# 注册的处理器同时是链的可执行节点case.upper/case.lower/case.title/case.camel/case.snake/case.kebab date.format/date.now/date.weekday number.thousands_sep/number.fixed_decimals/number.sum/number.strip_sep data.dedupe/data.sort_lines/data.padding/data.normalize_width/data.add_numbers table.transpose/table.csv/...

5.3 Python 节点的隔离设计

Python 节点在独立进程中运行,有超时保护,卡死不会拖住主界面。

但必须说清楚:这不是沙箱。获授权的脚本拥有当前 Windows 账户的全部权限——能读写文件、访问网络、自己调PrintWindow截屏,甚至能绕过 Easy-Tools 对自身窗口设置的SetWindowDisplayAffinity排除。

所以 UI 上必须手动勾选信任:

运行授权 ☐ 我信任此脚本,并接受它使用当前 Windows 账户权限

代码里对应的也是老老实实的双重检查(UI 状态 + 运行前再校验),不是装饰。

5.4 v0.3.0 修的事件链 Bug

这个 bug 挺典型,值得单独说:

  1. 链卡片根本没接点击/选中事件,所以「编辑选中 / 删除选中 / 立即运行选中」永远提示"请先选中"
  2. 编辑对话框关闭时引用了QDialogButtonBox.Accepted——Qt 里根本没有这个常量(只有Accepted),直接抛AttributeError崩溃

修法:卡片单击选中(高亮)、双击直接进编辑;常量改正。

教训:PySide6 里枚举是Qt.ItemDataRole.UserRole这种形式,不是QDialogButtonBox.Accepted。这类错误 IDE 不会提示,只能靠 review 和测试兜住。


六、隐私设计:写成硬约束,不是口头承诺

docs/privacy-design.md里明确写了适用规则,且规定新增功能不得绕过:

6.1 窗口捕获排除

开启隐私模式后,所有顶层窗口继承CapturePrivacyFilter的捕获排除状态(基于SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE))。

关键约束(也是大多数工具做不到的):

Easy-Tools 发起截图、OCR、Canvas 等内部流程时,不得临时移除自身窗口的捕获排除属性。

很多工具的"隐私模式"是靠截图时临时取消排除实现的,然后忘记加回去——结果就是自己截到了自己的面板。这里明确禁止了这种做法。

新建对话框必须由 Qt 应用持有,让全局过滤器能观察到显示事件,不允许创建绕过过滤器的独立原生窗口。

6.2 能力边界写清楚

不吹牛的部分:

  • 只覆盖受支持的 Windows/DWM 捕获 API
  • 不能防止直接读进程内存、辅助功能接口读取、外部摄像设备拍摄
  • Easy-Tools 自己发起的截图仍可处理目标应用内容

6.3 数据流

数据默认去向
OCR 文本、选区、剪贴板、知识库内容仅本机
云端模型调用用户显式选择后,问题/OCR 内容/检索片段离开本机,界面明确提示
CC Switch 凭据仅调用时只读,不写入配置 / 常规日志 / 错误弹窗
调试日志不记录完整问题、OCR 内容、剪贴板内容、文件内容、令牌

七、UI 工程:PySide6 踩过的坑

7.1 GUI 主线程绝不阻塞

这是ET-PERF-001的核心验收点,起因是用户反馈"偶发无响应"。排查后定的规矩:

  • 题库导入、表头读取、配置确认 → 移到有上限的后台任务
  • GUI 列表刷新不重新读 Excel(只读内存中的已解析数据)
  • OCR 和内置模型限制 CPU 并发,给界面留资源
  • 模型启动/停止不在 GUI 线程等长锁
  • 重复提问/截图任务不无限积压,显示"处理中"状态

7.2 托盘启动的初始化顺序

PRD 里记了一条很具体的约束:

启动时托盘通知槽必须先定义后连接,回调再次检查隐私通知开关;最终 EXE 的正常启动路径能进入托盘就绪阶段,不出现未定义回调异常。

这种 bug 只在特定时序下出现,测试很难覆盖,所以直接写进验收点。

7.3 图标系统

v0.2.1 把全站 emoji 换成了 Lucide SVG:

  • 50 个矢量图标(ISC 许可,免费可商用)
  • 动作面板 27 个默认动作、面板顶部设置/关闭按钮、复选框勾选样式全部替换
  • 配置中心 14 个页面各配语义对应图标
  • 高 DPI 屏按 2 倍像素渲染(QIcon+QPixmap手动setDevicePixelRatio)

图标加载做了优雅降级——图标文件缺失时条目照常创建,只是没图标,不阻塞启动:

mark=self._MARKS.get(title)ifmarkandicons.has_icon(mark):item.setIcon(icons.icon(mark,"#475569",18))

7.4 显示层和存储层分离

easy_tools/ui/labels.py是个很典型的设计。原始配置中心直接把内部代码甩给用户:

类型 chain.run 参数 {'snippet_id': 'snip_a', 'mode': ''}

这个模块专门做「翻译」:

defkind_label(kind):# chain.run → "运行事件链"defprocessor_label(pid):# text.strip → "去除首尾空白"defmode_label(mode):# "type" → "逐字键入"

边界写得很清楚:只做翻译,不改存储格式。用户看到的中文绝不会被写回配置。找不到映射时如实显示原文,绝不假装认识——宁可漏翻,不能错翻。


八、题库检索的一个设计细节

知识库导入 Excel 后,需要配置列映射:题目、题型、正确答案、答案内容、选项列。

其中「仅显示答案内容」这个选项值得单独说。

很多题库的选项顺序是随机的(同一道题,A/B/C/D 的内容每次都不一样)。如果按B去匹配答案,命中率会随机下降。

勾上这个选项后,工具直接显示正确答案对应的那段内容(即答案内容列的值),而不是字母。选项打乱也不影响。

这个功能明显是实际用过题库才加得出来的——它是业务知识的产物,不是技术推导的产物。

性能上做了预计算:题干规范化 + 词项索引在导入时算好,配置或文件变化后重建索引,移除题库时释放缓存。避免每次查询都遍历 Excel。


九、安装器:.NET FrameworkC# 单文件

installer/下是一个用 C# 写的安装器(csc.exe编译),不走 NSIS/InnoSetup。

它解决的问题比"拷贝文件"多:

  1. 升级识别:同时查注册表记录 + 常规旧版本目录(默认目录、临时目录)
  2. 精准结束进程:只结束该安装目录下的进程,不误杀同名程序
  3. 配置保留:覆盖升级时保留用户配置(APPDATA)和非程序文件
  4. 旧文件清理:对比安装清单,清理新版本已移除的旧文件
  5. 卸载安全:按清单删除 +校验绝对路径边界;保留清单外文件和用户配置;注册表和快捷方式只清理归属当前安装目录的项
  6. 不使用递归 CMD/PowerShell,不保留后台重启循环,清理重试有上限

打包产物:build.py(PyInstaller,目录模式,~451MB 含模型)+build_installer.py(单文件安装器,~408MB)。模型权重models/*.gguf因超过 GitHub 单文件限制不入库。


十、开发流程:PRD 驱动

_doc/prd/下 6 份需求文档,每份都有编号和验收点:

编号主题
ET-UX-001动作面板与文本输入
ET-EVENT-001事件链构建器
ET-QA-001截图题库问答
ET-PRIV-001窗口捕获隐私
ET-PERF-001卡顿排查与稳定性
ET-UPDATE-001灰度发布

docs/开发与发布规范.md定义了完整的版本发布和 GitHub Releases + DownloadCenter 灰度流程。

有意思的一条约定:PRD 与记录不由 AI 标记为完成。工具能写代码能改文档,但"需求是否真的验收通过"得人来判。


十一、快速上手(开发者)

git clone https://github.com/FortC/Easy-Tools.git cd Easy-Tools python-m pip install-r requirements.txt# 可选:OCR 后端(不装照常运行,配置中心会显示"不可用 + 原因")python-m pip install rapidocr_onnxruntime python main.py

打包(Windows,需 PyInstaller + .NET Framework C# 编译器 + 模型文件):

python build.py# 目录程序python build_installer.py# 单文件安装器

跑测试:

python-m pytest tests/-q

十二、小结

Easy-Tools 不是功能最多的桌面效率工具,但有几个实现值得抄:

  1. 纯 ctypes 消灭原生依赖问题——1400 行换来跨 Python 版本零摩擦
  2. 短按/拖动判定——劫持全局按键时保住原生行为的正确做法
  3. 两级上下文识别 + 面板不等 OCR——识别精度和弹窗速度可以兼得
  4. 处理器与链步骤同构——一份实现,两个入口
  5. Python 节点进程隔离 + 显式信任勾选——诚实地说明边界,而不是假装沙箱
  6. 隐私写成硬约束文档——禁止"截图时临时取消自身排除"这类偷懒实现

如果你也在写 Windows 桌面工具,这几个坑可以避开。


联系方式:lamthebest@foxmail.com | 欢迎提 Issue 和 PR

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 3:05:59

怎么用 AI 做一两分钟的小故事视频?从想法到成片

本文用跨三天的自拟小故事&#xff0c;说明从想法、故事稿、时间变化、画面清单到逐段生成、选片剪辑的完整做法&#xff0c;示范怎样在一两分钟里保留关键事件、带过重复劳动。 怎么用 AI 做一两分钟的小故事视频&#xff1f;先写清人物想完成什么&#xff0c;再把阻碍、改变办…

作者头像 李华
网站建设 2026/10/23 3:05:33

agent学习笔记4

首先感谢datawhale教程地址&#xff1a;https://github.com/bojieli/ai-agent-book/tree/main概念学习目标 1&#xff5c;Tools 是 Agent 与外部世界交互的"手脚"为什么必须有工具&#xff1a;模型本身只有"大脑"。它能推理&#xff0c;但不知道今天天气、…

作者头像 李华
网站建设 2026/10/23 3:04:57

【人工智能方向 | 湖北大学支持 | EI Compendex、知网、谷歌学术检索 | 广州举办】第三届人工智能与数字图书馆国际学术会议(AIDL 2026)

第三届人工智能与数字图书馆国际学术会议&#xff08;AIDL 2026&#xff09; 2026 3rd International Conference on Artificial Intelligence and Digital Libraries 第三届人工智能与数字图书馆国际学术会议&#xff08;AIDL 2026&#xff09;是一个致力于探讨和展示人工智…

作者头像 李华