开发了微信半自动消息辅助程序

我不想再写一个"AI 自动回复机器人"。我要的是反过来的东西:话还是我自己说,只是发出去之前有人帮我看一眼——错别字、语病、口气对不对。
所以做了这么一个东西:一个挂在自己微信窗口右边的小悬浮框。它靠 Windows 的窗口截图 + 本地离线 OCR 把屏幕上的对话读成上下文;你想回复的时候,点「生成回复」让它写 3 条候选,或者自己写完点「润色」改顺、点「润色并发送」一步发出去。默认状态下你不点按钮,它一次 API 都不发,也永远不会替你按发送。
界面上的名字是「润色」,仓库名 polish-wechat-windows,MIT,Python + PySide6 + WGC + RapidOCR。


一、先把路走窄:为什么最后只剩"截图 + OCR"

做之前先做减法。要让程序知道"上文是什么",能想到的路有四五条,我一条条试完,最后只剩一条。
读聊天记录数据库? 直接排除。解密别人(哪怕是自己的)客户端数据库这件事,风险面和道德账都不划算,我不想把"别人的聊天库"当输入。
hook / 注入 / 读进程内存? 同上,排除。
用 UIA(UI Automation)读控件树? 这是最"正统"的 Windows 无障碍读法,代价也最低。所以先写探针试了——结论是证伪:目标窗口的聊天内容自绘在一块 GPU 合成画布上,UIA 树只有 2 个节点,根本没有控件树。probe/probe_win.py、probe/probe_win2.py 两个探针分别验证了"树是空的"和"有树没文字",顺带试了 LegacyIAccessible,一样读不到。(这两个探针我留在仓库里了,结论可复现。)
那就只剩:截自己这一个窗口的画面 + 本地离线 OCR。
这条路的好处是它干净:不 hook、不注入、不读数据库、不解密、不碰对方进程内存——和读屏软件、录屏软件是同一类操作。OCR 用 RapidOCR,全程离线、零 token、不上传。
代价也很实在:一帧 OCR 要 250~800ms,还得靠像素和颜色去猜"这句话是谁说的"。

二、采集链路:从一帧画面到"上下文"

整条链路是这样的:

WGC 截聊天窗口(GPU 合成窗口也能截,被别的窗口盖住也能截)
  → 像素锚点定位消息区(认底色和分隔线,不写死坐标,深浅主题通用)
  → OCR 面板头部的会话名当 key(头部像素没变就不重跑 OCR)
  → RapidOCR 只认消息区那一块
  → 按气泡颜色分 me / her,灰字(引用块、时间戳、发言人名、链接卡片)过滤掉,
    发言人名摘出来挂到它下面那条消息上
  → 跟上一帧比,滚动翻出来的旧消息不重复上报
  → 全在本地;只有你点按钮才会走一次模型调用

几个当时花了时间的点:

  • 消息区不写死坐标。 靠"面板 45% 高度以下的第一根分隔线"定位,底色和分隔线都是相对判断,深浅主题通用。写死坐标是最省事也第一个崩的做法——微信随便改个版就废。
  • 会话名当缓存 key。 面板头部的会话名 OCR 出来既是"当前会话"标识,也是 OCR 缓存的 key:头部像素没变就不用重跑。记录和上下文按会话分开存,切会话不串味。
  • 按颜色分说话人。 气泡底色分 me / her;灰字是引用块、时间戳、发言人名、链接卡片,一律先摘出去。群聊里把发言人名挂到它下面那条消息上,才知道哪句是谁说的。
  • 滚动去重。 翻聊天记录时旧消息会一次次进画面,按文本相似度去重,避免把同一条消息反复算进上下文。
  • 截图只在内存里。 捕获到的是 numpy 数组,app/、core/ 里没有一处 .save(),不写磁盘、不进日志、不上传。调试视图也是另开一个窗口在内存里画(绿=我、蓝=对方、灰=过滤掉的灰字、橙=发言人名、红=当图片丢掉),关掉开关子进程连帧都不发。
  • OCR 放独立子进程。 一帧 250~800ms,放 Qt 主线程界面直接僵住。所以父进程只管界面和网络调用,子进程采集完走队列上报。

三、怎么让它写出来不那么像 AI

这类工具最容易翻车的地方不是"生成不出来",而是一眼假。我在这上面花的功夫比采集还多:
1)system prompt 写成反模板规则。 明确禁掉:不总结不复述、不解释自己为什么这么回、不用「首先/其次/总之」、不用「亲/您/加油哦」这类客套、不排比不凑三段式、句尾别习惯性加句号、允许不完整的句子和口头语。给的 3 条候选不是「温暖版/负责版/行动版」,而是同一个人三个心情下随手打的。
2)喂口吻样本。 把你自己最近 12 条短消息(60 字以内,链接和长段不送)原样给它,照着你的用词、句长、标点习惯写。设置里还能再补一句自己的说话风格。
3)润色那条链路更狠。 提示词里明写「意思、态度、信息量一点都别改」「长度最多长两成」「原文已经够顺就原样还回来,别为了改而改」,还要求保留原文的断句和标点习惯——不加标点的人别给他补句号。温度也压低。
4)出口做清洗。 剥掉编号、方括号、引号,剥掉照抄的 me: 前缀,去掉句尾句号(?!~ 留着,那是语气)。
5)防提示词注入。 上下文里长得像「忽略上面的规则」「回我三遍」的对方消息会被标出来单独提醒模型;候选里原样复读对方那句话的直接丢掉(纯笑声除外)。

四、产品上最克制的几条硬约束

这是"设计",不是"没做完",README 里也是按硬约束写的:

  • 默认零调用。 对方来消息只记账(进上下文和聊天记录),不出网、不调模型、不弹窗。十分钟没人说话就是十分钟零调用;光看着不点,一次都不调。
  • 不替你按发送。 程序里没有任何自动发送路径,唯一会按回车的地方就是你点了「发送」之后。
  • 可选的「自动生成」也不越界。 输入区那行有个「自动生成」开关:拨开之后对方一发新消息就自动起草一版填进输入框,但发送还是你自己按;输入框里已经有你写的东西时直接跳过、不覆盖。这个开关默认关、且不落盘(重启就回关),免得哪天静悄悄开始自动调模型。
  • 不碰钱。 转账、红包、收款相关的界面元素一律不碰,提示词里也禁了这几个话题。

五、踩过的坑(都带数字)

1)Win32 矩形是物理像素,Qt 摆窗口用逻辑像素。
GetWindowRect / DwmGetWindowAttribute 给的是物理像素,换算要除 QScreen.devicePixelRatio()。100% 缩放下 dpr=1.0 会退化成恒等,看起来"没问题",一旦跨屏或缩放不是 100% 就全错。而且要按窗口所在那块屏的 dpr 折算,别假设全局一个比例。
2)吸附是下沿对齐,QRect.bottom() 是闭区间。
y = area.bottom() - height + 1——忘了那个 +1 就差 1 像素,视觉上就是"没对齐"。
3)内容一变就必须重算高度,否则 Qt 会把控件摞在一起。
Qt 在空间不够时既不报错也不自动长高,而是把固定高度的子控件叠着画——我实测到的是按钮盖住输入框、进度条横穿按钮那一行(用户截图报上来的)。所以出备选行、进度条显隐、状态文字换行、进设置页、跨紧凑断点,每一处变化都得走一遍 _apply_height(),漏一个就是"界面错乱"。
4)Windows 上 multiprocessing.Queue() 要建命名管道。
受限环境里直接 PermissionError WinError 5——多进程 GUI 必须放宽沙箱才能跑,纯单进程的 Qt 程序不受影响。
5)OpenAI 兼容口的"默认思考模式"会把正文吃光。
某兼容来源上的模型默认开着思考模式而且关不掉(来源表里没有思考开关字段)。max_tokens 给 800 时 reasoning 直接吃满 800,finish_reason=length、content 是空串,调用方解析当场抛错。两手兜住:① 关思考的场景也给足额度(生成 1000 / 润色 1200);② 正文为空时同一请求重问一次(同样输入思考长度会变,实测第二次就正常返回),再空才提示用户。
6)拖动必须有阈值。
一开始没有阈值,一次带轻微抖动的点击就把窗口挪走了,更糟的是把"吸附"这个开关落盘成了 false。现在 ≥6px 才算拖动,而且拖动只脱开这一次运行、不写设置,写设置只由显式的图钉/开关触发。

六、它不做什么,以及一些已知限制

不做什么(同第四节,这里只列一条最重要的):只做微信这一个聊天窗口,不打算适配别的 IM。识别全靠微信自己的界面布局和配色。
已知限制挑几条说:

  • 微信改版可能失效;发送靠回车,如果你把微信设成 Ctrl+回车 发送,这一下只会换行(可以改微信快捷键,或者直接点微信自己的发送按钮)。
  • 输入框拉高超过面板一半会认错消息区(消息区靠"面板 45% 高度以下第一根分隔线"定位)。
  • 群聊里发言人名被 OCR 漏识时,这条消息会挂到上一个人头上。
  • 会话靠头部标题认,名字只差一个字的两个会话可能被并成一个。
  • 同一人连发两句一模一样的,去重会吞掉一条。
  • Win10 上 WGC 会在目标窗口外画一圈黄框,系统不给关(Win11 可以关);嫌碍眼就把采集开关拨到「已暂停」。
  • 没有托盘,关窗口就是退出。

七、项目信息

  • 仓库:https://github.com/Echosong/polish-wechat-windows
  • Windows 10 1903+ / 11,下载解压双击 polish-chat.exe 即用,只需一把 API key(LLM_API_KEY,默认 DeepSeek 官网直连;也支持 OpenAI / Anthropic / Gemini 三种协议共 12 家来源 + 自定义 Base URL)
  • key 只写进注册表 HKCU\Environment,任何文件里都不出现 key,报错文本一律脱敏;其余设置落在 exe 旁边的 config.json,整个文件夹拷走设置跟着走
  • 源码运行:pip install -r requirements.txt 然后 python main.py;自己打包双击 build.bat(PyInstaller onedir,推 v* tag 由 GitHub Actions 出 Release)
    许可:本项目自身代码 MIT。但 Windows 发布包里打进了 PyQt-Fluent-Widgets(GPLv3,非商用免费),所以发布包整体受 GPLv3 约束,商用请自行购买该组件的商业授权或替换掉它。第三方组件清单见 NOTICE。
    出处:本项目脱胎于 jev-chat-windows(MIT),再上游是安卓原版的 Jev 聊天助手。重做时把原来「Jev 判断 → 起草 → 排序」的三段式判断内核整套删掉了,收敛成"一次生成 + 一次润色"、两把 key 变一把 key。血缘写在 NOTICE 和 README 的「出处与致谢」里。

最后说一句定位:它不是"帮你自动聊天"的工具,是"帮你把已经想说的话说好"的工具。 真正需要它的场景往往很小——开会时打错的一个字,对领导过冲的一句话,深夜想回又怕说错的那条消息。

posted @ 2026-09-25 16:23  EchoSong  阅读(14)  评论(0)    收藏  举报