微信自动发送(消息 · 图片 · 批量)
> 【使用前必读】请先在 SkillHub 下载并安装本技能,装完才能在对话里触发:
> 搜索「微信自动发送」点「安装」,或执行 skillhub install wechat-one-click-send
点一下,几十家门店的账单和凭证自动发到各自的微信群——不用切会话、不用复制粘贴、不用守着屏幕。
适用什么场景
- 每月要往微信发对账单、通知单、结算凭证的人(结算 / 对账 / 财务)
- 手上几十个客户、门店或代理,要把同一类单据一次性发出去
- 电商、门店连锁、代理体系里,单据格式统一、只是收件人不同
- 正在做结算 / 对账 / CRM 系统,想加一个「自动把单据发到微信」的能力
解决什么问题
手工发账单,真正怕的不是慢,是出事:
- 发错群:A 店的账发进 B 店的群,群里几十号人看见,只能挨个撤回道歉
- 发出旧金额:改了补贴忘了重新结算,钱错了,当场没人发现
- 重复发送:发到中途卡住,重来一遍,对方收到两遍
快,只是顺手捡的;防事故才是核心价值。
优势点
| 对比项 | 手工发送 | 本技能 |
|---|---|---|
| 20 家门店耗时 | 20 分钟以上,全程盯屏 | 点一下,自动跑完 |
| 发错群 | 全靠人不出错 | 会话名对不上就中止 |
| 发出旧金额 | 当场没人会发现 | 和结算结果逐分比对,对不上就拒发 |
| 中途失败 | 从头再来 | 只补没发的,不重复 |
| 弹窗打扰 | 手动一个个关 | 自动清走遮挡,不误伤在写的文档 |
三条最容易出事的路径全部堵死:
- 不会发错群 —— 发送前核对「当前打开的会话名」,和目标不一致立即停下,绝不将错就错
- 不会发旧金额 —— 账单金额和最后一次结算结果逐分比对,对不上直接拒发(核对在程序里做,改页面绕不过)
- 不会重复发 —— 每家门店记住进度(文字发没发、图片发到第几张),失败重试只补没发出去的部分
怎么调用 / 触发
第一步:先拿到它
在 SkillHub 搜「微信自动发送」,点「安装」;或命令行:
skillhub install wechat-one-click-send
装完之后它常驻在你的技能目录,以后在对话里说话就能触发,不用每次重装。
第二步:在对话里直接说
- 「帮我把这个月的账单发到各门店微信群」
- 「批量发,发一半断了能接着发吗」
- 「给我们的结算系统加个自动发微信的功能」
AI 会加载本技能,按这套流程走:找出真微信窗口 → 关掉挡在前面的窗口 → 切会话并用 OCR 核对是不是目标对象 → 发文字 → 逐张发凭证图。每一步都记进度,中途失败只补没发完的那部分。
第三步:装了和没装差在哪
不装,AI 也能给你写个「点按钮、发消息」的脚本——但发错群、发旧金额、重复发这三条路它不会主动帮你堵,你得自己踩一遍才知道。
装了,直接拿走已经验证过的方案:六大坑的完整解法、发送前的三重金额校验、断点续发的进度模型,以及一个不点击不发送的上线自检脚本。
> 另一条路径:如果你已经在用配套的结算系统,不用装技能。
> 系统界面上本来就有按钮:
> 1. 给每家门店填「微信群名 / 微信名」(填群名或备注名,别填昵称,昵称会变)
> 2. 结算完成后点「一键发送」,核对弹窗里的账单全文 + 本月应补金额 + 发送目标
> 3. 确认后自动跑完;批量点「全部发送」,有失败的点「只补发缺失部分」
>
> 本技能是给「想自己做出这套能力」的人看的——怎么做的、坑在哪。
正式发之前,先跑一次自检(只查谁挡在微信前面,不点击、不发送):
python occlusion_selftest.py
边界
- 只在 Windows 上跑(依赖系统接口和 Windows 11 自带 OCR)
- 微信大改版可能影响窗口定位(靠「窗口类名 + 标题」认出真微信)
- 任务栏、开始菜单、输入法等系统窗口不做处理(设计如此)
- 发送期间机器不要锁屏或休眠
- 最终发出去的内容仍需人确认——金额核对防的是「算错没重算」,不替代你对账单内容的判断
技术实现:六大坑与完整解法
全链路架构
浏览器页面 后端服务 自动化子进程
┌─────────────┐ POST ┌──────────────────┐ stdin ┌─────────────────┐
│ 确认框(账单全文)│ ───────→ │ ①有结算结果? │ ─JSON──→ │ 找微信窗口→杀冒名 │
│ 含金额+目标 │ │ ②账单金额==结算金额?│ │ →清遮挡→置前 │
│ [确认发送] │ ←──────── │ ③有发送目标? │ ←标记── │ →切会话→OCR校验 │
│ 进度日志/续发 │ partial │ (任一不过→拒发) │ [OK_*] │ →发文字→逐张发图 │
└─────────────┘ └──────────────────┘ └─────────────────┘
关键设计:三重校验放在后端(前端可被绕过);子进程用 JSON 进、带机器可读标记出,失败时已发出去多少也要回传。
坑 1:子进程 stdin/stdout 必须显式 utf-8
子进程默认继承控制台 GBK 编码,父进程传的 utf-8 JSON 会把「文件传输助手」读成乱码,按乱码搜索会话必然失败:
import io
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
症状识别:报错里的中文是 GBK 风格乱码(特征字:鑷/浼/犺/鍔)→ 先查编码,别去查窗口逻辑。
坑 2:Win32 API 先确认在哪个 DLL
ctypes.windll 上取不存在的函数不报 AttributeError,而是 function 'X' not found。用前冒烟一遍——比如 GetCurrentThreadId 在 kernel32 不在 user32:
u = ctypes.windll.user32
k32 = ctypes.windll.kernel32
GetCurrentThreadId = k32.GetCurrentThreadId
坑 3:窗口置前不能只试一次
Windows 不允许后台进程直接 SetForegroundWindow。组合三招 + 多次重试:
# ① 模拟按一下 ALT —— 解除系统的前台锁
u.keybd_event(0x12, 0, 0, 0); u.keybd_event(0x12, 0, 2, 0)
# ② AttachThreadInput —— 借用当前前台线程的输入权限
u.AttachThreadInput(cur_thread, fg_thread, True)
try:
u.BringWindowToTop(h)
u.SetWindowPos(h, -1, 0,0,0,0, FLAGS) # TOPMOST
u.SetWindowPos(h, -2, 0,0,0,0, FLAGS) # NOTOPMOST(闪一下,避免常驻最前)
u.SetForegroundWindow(h)
finally:
u.AttachThreadInput(cur_thread, fg_thread, False)
# ③ 重试 8 次,等待逐次放宽;焦点校验留 2 秒缓冲(瞬时闪动会误判)
找真微信窗口:按窗口类名(微信 4.x 的类名以 Qt 开头)+ 标题「微信」定位;同时关闭冒名的「微信」窗口(title 相同但类名不对的,比如某些 shell 窗口),否则后面校验全是空转。
坑 4:微信 4.x(Qt)关闭了无障碍接口
实测:UIA 元素树是空的(descendants() → 0),MSAA 只有根节点,窗口标题恒为「微信」不随会话变化。任何读 UI 元素的校验方案都不可行。
确认方式(只读不点击):
from pywinauto import Application
w = Application(backend="uia").connect(handle=h).window(handle=h)
print(len(w.descendants())) # 微信 4.x → 0
解法:截图 + OCR 校验当前会话名。Windows 11 自带 OCR(WinRT,无需安装),经 PowerShell 调用:
- 截聊天头部横条并放大 2 倍再识别(小字识别率骤降)
- OCR 会乱插空格:比对前
"".join(text.split()) - PowerShell 输出中文必须
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - 会话名与目标不一致 → 立即中止,绝不「将错就错发出去」
坑 5:批量必须串行 + 失败后断点续发
微信自动化一次只能操作一个会话,批量逐家串行、每家之间歇 3 秒。但「失败即停」是错的——卡在一家导致后面全没发。正确做法:每家记进度,只补缺失部分。
进度模型(前端维护)
progress = { text: false, imgs: 0 } // 文字是否已发、凭证已发出几张
每次请求只带「还没发出去的」
const pendingIds = imgs.slice(progress.imgs).map(it => it.id); // 第 N 张之后的
await api("/api/send-bill", {
month, store, target,
text: buildBillText(store), // 文字每次都带上(见下方裁决规则)
image_ids: pendingIds,
skip_text: progress.text, // 已发过就告诉后端别再发
});
后端失败时也要回传「已发出去的部分」
子进程边发边打机器可读标记,父进程用正则抠出来随错误一起返回:
# 子进程输出(每成功一步打一条)
print(" [OK_TEXT] 账单文字已发送")
print(" [OK_IMG %d] 凭证图 %d 已发送" % (i, i))
# 服务端解析
_OK_TEXT_RE = re.compile(r"\[OK_TEXT\]")
_OK_IMG_RE = re.compile(r"\[OK_IMG\s+(\d+)\]")
def parse_partial(out):
return {"text": bool(_OK_TEXT_RE.search(out)), "images": max([*map(int, _OK_IMG_RE.findall(out))], default=0)}
# 接口 400 时
payload = {"error": str(e)}
if getattr(e, "partial", None): payload["partial"] = e.partial
# → {"error": "...", "partial": {"text": true, "images": 1}}
三条铁律
skip_text必须由后端裁决,不能「前端没传 text 就默认跳过校验」——否则前端传个空 text 就绕过了金额核对。后端规则:text永远要求传(校验用),skip_text=true只控制「发不发」。- 进度只认最大值:
imgs = Math.max(prev, reported)。网络报错顺序不保证,取小会重复发。 - 图片顺序固定:按数组顺序发,第 N 张失败就补 N 之后的——不能用 id 集合(顺序乱了对方看到的凭证排列就乱了)。
批量循环骨架
for (const s of stores) {
try {
const r = await _sendOne(s, target(s), imgs(s), progress[s]); // 内部自动重试 2 次
progress[s] = r;
} catch (e) {
doneParts[s] = e.progress; // 记录断点
failures.push(s); // 失败继续下一家,不中断批次
}
await sleep(3000);
}
// 批次结束:全成功 → 报告;有失败 → 给「🔄 只补发缺失部分(N 家)」按钮,携带 parts 重入
坑 6:窗口被遮挡 → 点击落到别的程序上
症状:明明已切到目标会话,文字却发不出去/发到别处。根因:目标窗口被盖住,click(x, y) 的坐标落在遮挡窗口上。
陷阱 A:EnumWindows 返回「所有」窗口,不是「压在上面的」
直接「有重叠就关」会把一堆在目标下面的窗口也关掉。必须按 Z 序——EnumWindows 的返回顺序就是 Z 序:
seq = zorder() # EnumWindows 顺序 = Z 序(顶→底)
i = seq.index(target)
for h in seq[:i]: # 只有排在 target 之前 = 真正压在它上面
if cover_ratio(rect(h), rect(target)) > 0.02:
... # 这才算遮挡
陷阱 B:有些程序绝不能关
用户可能正在写文档/改代码。维护保护名单,命中只报告、绝不关闭:
_PROTECT_PROCS = {"winword.exe","excel.exe","wps.exe","et.exe","code.exe",
"notepad.exe","explorer.exe","cmd.exe","powershell.exe",
"wechat.exe","weixin.exe"} # 微信自身也绝不关
# PostMessageW(WM_CLOSE) 前先按 PID 查进程名(QueryFullProcessImageNameW)
默认动作选「最小化」而不是关闭——最小化无破坏性且同样解除遮挡。
处理顺序与自愈
先关遮挡 → 再置前 → 置前成功后再复核一遍(防止又有窗口弹上来)→ 最多 3 轮(逐轮提高激进程度)
关不掉的(受保护)→ 把微信挪到屏幕空位避让
焦点守卫自愈:发现前台不是目标 → 先自动清遮挡+重新置前 → 成功就继续发,不要立刻中止
发送前三重校验(后端,一行都不能省)
- 该期必须有结算结果
- 账单文字里抠出的金额必须与最后一次结算结果完全一致(差 >0.005 拒发)——防止「改了补贴没重算就发出去了」。这是资金安全线,跳过它的系统迟早发错账
- 必须有发送目标(该门店的微信群/微信名已填)
前端确认框还要展示账单全文 + 本月应补金额 + 发送目标,让用户在点「确认发送」前肉眼核一遍。
凭证图发送:剪贴板 CF_DIB
微信桌面版没有拖放/文件接口可自动化,发图走剪贴板:把图片文件转成 DIB 格式放进剪贴板,再模拟 Ctrl+V:
import win32clipboard, io
def put_image(path):
from PIL import Image
img = Image.open(path)
with io.BytesIO() as buf:
img.save(buf, "BMP")
data = buf.getvalue()[14:] # 去掉 BMP 文件头 14 字节 = DIB
win32clipboard.OpenClipboard()
try:
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(win32con.CF_DIB, data)
finally:
win32clipboard.CloseClipboard()
# 然后对输入框 click() + Ctrl+V + Enter
要点:逐张「放剪贴板 → 粘贴 → 确认已发出(打 [OK_IMG n])→ 下一张」,中途失败就能从第 n+1 张续发。
上线前自检(不点击不发送)
scripts/occlusion_selftest.py:列出当前谁挡在微信上面、哪些命中保护名单——只检测不动作,上线前/发送异常时先跑它:
echo '{"selftest": true}' | python occlusion_selftest.py
# 或直接 python occlusion_selftest.py
输出示例:
微信窗口: 0x1a0c2e (rect 100,200 1200x800)
压在微信上面的窗口:
- 0x2b01a4 CalculatorFrame 计算器 [可处理] 遮挡比例 31.2%
- 0x1c00d8 Notepad 记事本* [受保护] 遮挡比例 58.0% → 建议挪微信避让
无遮挡。
常见故障速查
| 症状 | 先查什么 |
|---|---|
| 报错中文是乱码(鑷/浼/犺) | 坑 1 编码 |
function 'X' not found |
坑 2 DLL 归属 |
| 怎么都置不了前 | 坑 3 三招是否齐全、重试次数 |
| 文字发到了别的会话 | 坑 6 遮挡 + 坑 4 OCR 校验是否在跑 |
| 重发后对方收到两遍 | 坑 5 进度取最大值 / skip_text 是否后端裁决 |
| 发出旧金额 | 三重校验第 2 条是否被绕过 |