当AI学会操作你的桌面软件
——WorkBuddy桌面自动化实践与可行性分析
126条组合拆分单据,每条需要在思迅eStore10客户端里手动录入货号、数量、保存、新建下一张……一条平均30秒,全做完将近一小时。这不是什么高难度的技术活,但足以把一个人的耐心磨成粉末。
本文记录的是一次真实探索:如何让AI助手(WorkBuddy)通过Win32 API直接操控桌面应用,实现组合单据的批量自动录入。包括技术方案、踩过的坑、修复过程,以及与"数据库直连"方案的深度对比。
一、问题背景
零售行业会使用思迅eStore10管理门店进销存,速达5000做财务核算。两套系统之间存在着大量的数据转录需求。
其中一类典型操作是"组合拆分单据"——在思迅客户端的"组合拆分"页面,将成品拆分为原料或将原料组合为成品,用于库存调整和成本核算。每次盘点后或新品上架时,往往需要批量处理上百条组合单据。
传统流程:
| 合计 | 单条 | ~25-30秒 |
126条 × 30秒 = 63分钟。而且这是理想情况——实际操作中,疲劳导致的误录入、中断重来、数量录错等问题,至少还要再加50%的时间。
二、技术方案:Win32 API桌面自动化
2.1 为什么不用RPA工具?
市面上有不少RPA(机器人流程自动化)工具,如UiPath、影刀、八爪鱼等。但对于小型零售企业来说,它们存在几个问题:
成本高:商业RPA工具年费动辄上万 学习曲线陡:需要专门学习拖拽式编程 部署重:需要安装额外的客户端和运行时环境
而WorkBuddy的优势在于:它已经运行在我的电脑上,能直接编写和执行Python脚本,调用Windows API。不需要额外安装任何软件。
2.2 核心技术栈
import win32gui # 窗口查找、激活import win32api # 键盘、鼠标事件import win32con # 虚拟键码常量import ctypes # 线程级窗口控制from PIL import ImageGrab # 截图验证这套技术栈完全基于Python标准生态 + pywin32扩展,无需任何商业组件。
2.3 实现逻辑
整个自动化流程分为六个环节:
环节一:窗口查找与激活
思迅eStore10客户端是PowerBuilder开发的传统Windows桌面应用,窗口类名为FNWND390。通过EnumWindows遍历所有可见窗口,匹配类名和标题关键词即可找到:
deffind_sixun(): result = []defcb(hwnd, _):if win32gui.IsWindowVisible(hwnd): cls = win32gui.GetClassName(hwnd) title = win32gui.GetWindowText(hwnd)if cls == 'FNWND390'and'思迅'in title: result.append(hwnd) win32gui.EnumWindows(cb, None)return result[0] if result elseNone找到窗口后,需要将其置于前台。Windows对前台窗口有严格的权限控制,直接调用SetForegroundWindow往往失败。解决方案是通过AttachThreadInput将当前前台窗口的输入线程与目标窗口绑定:
deffocus(hwnd): win32gui.ShowWindow(hwnd, win32con.SW_MAXIMIZE) fg = win32gui.GetForegroundWindow() t1, t2 = ctypes.c_ulong(), ctypes.c_ulong() ctypes.windll.user32.GetWindowThreadProcessId(fg, ctypes.byref(t1)) ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(t2)) ctypes.windll.user32.AttachThreadInput(t1.value, t2.value, True) win32gui.SetForegroundWindow(hwnd) ctypes.windll.user32.AttachThreadInput(t1.value, t2.value, False)环节二:坐标定位
思迅客户端的界面是固定布局的,在1920×1080分辨率、窗口最大化的条件下,关键元素的屏幕坐标是固定的:
通过SetCursorPos + mouse_event模拟鼠标点击:
defclick(x, y): win32api.SetCursorPos((x, y)) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0) win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)环节三:键盘输入模拟
通过keybd_event发送虚拟键码,模拟键盘按下和释放:
defkey(vk): win32api.keybd_event(vk, 0, 0, 0) # 按下 win32api.keybd_event(vk, 0, KEYEVENTF_KEYUP, 0) # 释放文本输入则逐字符发送:
deftype_text(s):for ch instr(s): vk = ord(ch) win32api.keybd_event(vk, 0, 0, 0) win32api.keybd_event(vk, 0, KEYEVENTF_KEYUP, 0)环节四:单据处理流程
每条单据的处理流程被封装为一个函数:
defprocess_item(item_no, qty, sixun_hwnd, first=False):# 1. 首条选择"组合"方式if first: select_mode_combo()# 2. 录入货号 click(123, 251) # 点击货号字段 type_text(item_no) # 输入货号 key(win32con.VK_RETURN) # 确认# 3. 录入数量 click(427, 250) # 点击数量字段 clear_qty() # 清除默认值"1.00" type_text(qty_str) # 输入数量 key(win32con.VK_RETURN) # 确认# 4. 保存(不审核) key(win32con.VK_F3) # F3保存 key(win32con.VK_TAB) # Tab切到"否" key(win32con.VK_RETURN) # 确认不审核环节五:浮窗处理
保存后,思迅会弹出"未审核单据"浮窗显示单据号。需要关闭浮窗才能处理下一条。浮窗的关闭按钮位置不是固定的,需要动态获取浮窗窗口矩形,计算右上角关闭按钮的坐标:
defclose_panel(parent): h = find_panel(parent) # 查找标题含"未审核"的子窗口if h: r = win32gui.GetWindowRect(h) click(r[2]-20, r[1]+15) # 右上角偏移20,15环节六:新建下一条
按F1新建空白单据,系统会询问是否保存当前未保存的内容,用Tab+Enter选择"否":
defnew_sheet(sixun_hwnd): key(win32con.VK_F1) key(win32con.VK_TAB) key(win32con.VK_RETURN)2.4 批量执行
将126条数据分为两批执行。第一批10条用于验证流程,确认无误后再执行剩余114条:
# 第一批:10条python run_batch10.py batch_10.csv# 剩余:114条(可指定起止索引分批执行)python run_batch_remaining.py 050python run_batch_remaining.py 50114分批执行是"先测试后批量"操作哲学的体现——避免一次性执行出错导致大量错误数据需要回滚。
三、踩坑实录:一个小数点引发的"10倍事故"
3.1 发现问题
第一批10条执行后,在思迅客户端检查数据时发现:CSV中的数量25.0,实际录入变成了250。所有带小数点的数量都被放大了10倍。
3.2 根因分析
问题出在type_text函数中。原实现为每个字符直接使用ord(ch)作为虚拟键码:
# 原始代码(有Bug)deftype_text(s):for ch instr(s): vk = ord(ch) # 直接使用ASCII码作为虚拟键码 win32api.keybd_event(vk, 0, 0, 0)对于数字字符'0'~`'9',ord('0')=48恰好等于VK_0=48`,一切正常。
但对于小数点'.',ord('.')=46,而Windows虚拟键码46对应的是**VK_DELETE(删除键)**,不是小数点!
小数点的正确虚拟键码是VK_OEM_PERIOD = 0xBE = 190。
所以当输入"25.0"时:
'2'→ 正常输入2'5'→ 正常输入5'.'→ 发送了Delete键(光标在末尾,无效果)'0'→ 正常输入0实际结果: "250"
3.3 修复方案(双重防护)
防护一:输入前转换为整数字符串
组合单据的数量都是整数,直接在传入前去掉小数点:
qty_str = str(int(float(qty))) # "25.0" -> "25"type_text(qty_str)防护二:修正type_text函数
对小数点使用正确的虚拟键码,防止其他场景下再次出现同样问题:
deftype_text(s):for ch instr(s):if ch == '.': vk = 0xBE# VK_OEM_PERIODelse: vk = ord(ch) win32api.keybd_event(vk, 0, 0, 0) win32api.keybd_event(vk, 0, KEYEVENTF_KEYUP, 0)3.4 经验总结
这个Bug的教训是:keybd_event的参数是虚拟键码(Virtual Key Code),不是ASCII码。虽然数字和字母的虚拟键码恰好等于ASCII码,但标点符号完全不同。常见的坑:
. | |||
, | |||
- | |||
Enter |
凡是涉及标点符号的输入,都必须查虚拟键码表,不能想当然地用ord()。
四、另一条路线:数据库直连自动化
在桌面自动化之外,我们还有另一套更成熟的自动化方案——直接通过pyodbc连接数据库进行批量写入。
4.1 方案对比
以"思迅TG转货单转速达生产加工单/组装单"为例,这套方案完全不操作任何桌面应用,而是直接读写数据库:
思迅数据库(只读) → Python读取TG转货单 → 按备注分类 → 速达数据库(读写)批量INSERT| 操作对象 | ||
| 技术栈 | ||
| 速度 | ||
| 稳定性 | ||
| 安全性 | ||
| 前置条件 | ||
| 适用场景 | ||
| 维护成本 |
4.2 数据库直连的安全体系
数据库直连方案的三层安全保底是经过实战验证的:
Layer 1: 全量数据库备份(.bak文件,可RESTORE恢复)Layer 2: 数据快照(导出涉及表的实际数据到JSON)Layer 3: 元数据记录(count + MAX(billid),精确定位回滚点)写入时使用事务包裹,任一单据失败全部回滚:
with SudaDB() as db:with db.transaction() as cur:for order in orders: cur.execute(sql_header, params) # 头表for mat in materials: cur.execute(sql_detail, params) # 明细# 事务自动提交这套安全体系是桌面自动化方案所不具备的——桌面操作一旦执行就无法回滚,只能手动在客户端里删除错误单据。
4.3 数据库直连的"触发器陷阱"
直连数据库写入也不是没有风险。速达5000的表上挂着大量触发器,稍有不慎就会触发非预期行为:
mnf_manuorder_ai:仅在referbilltype=43/12时触发,独立单安全i_combin_au:审核时自动处理库存出入库 + 成本底稿mnf_manuorderdetail_ai:维护虚拟BOM表
这些都是通过大量测试才摸清的规则。直接写数据库的好处是快,但前提是你必须完全理解目标系统的触发器逻辑,否则可能写入"看起来正确但实际破坏了数据一致性"的数据。
五、可行性分析:什么场景该用什么方案
5.1 桌面自动化适用场景
5.2 数据库直连适用场景
5.3 混合策略:最佳实践
在实际业务中,最优解往往是两条路线的组合:
思迅数据库(只读查询) → Python处理 → 能直连速达数据库的,直接INSERT → 必须走UI的,用Win32自动化具体到用户的业务场景:
思迅→速达数据转录(MO配送单→PK/SS、MI退货→PK/SS、TG转货→加工单/组装单):全部走数据库直连,已实现每日定时自动执行 思迅客户端内操作(组合拆分单据录入):走桌面自动化,因为这是在思迅系统内部的操作,没有速达数据库的介入 速达客户端内操作(审核、反审核):走数据库直连(UPDATE billstate),触发器自动处理库存
六、反思与展望
6.1 桌面自动化的本质局限
桌面自动化解决的是"最后一公里"问题——当系统没有API、没有数据库权限、只能通过界面操作时,它是唯一的自动化途径。但它的局限也是显而易见的:
脆弱性:一次Windows更新、一次分辨率变化、一个弹窗位置偏移,整个流程就崩了 不可审计:无法像SQL事务那样回滚,出错了只能手动收拾 速度天花板:受限于UI响应速度,不可能像批量INSERT那样快 无人值守困难:执行过程中不能动鼠标键盘,否则按键发到错误窗口
6.2 AI助手的价值
在整个探索过程中,WorkBuddy(AI助手)扮演的角色不是"代替人操作",而是:
快速原型开发:从需求到可运行脚本,一次对话即可完成 Bug定位与修复:小数点Bug的根因分析和修复方案,AI在几秒内就给出了正确方向 经验沉淀:每次踩过的坑都会被记录到技能(Skill)文件中,下次不会再犯 方案对比:AI能同时分析多条技术路线的优劣,帮助做出最优决策
6.3 工具的边界
工具能做的事情有边界,但工具的边界不等于人的边界。
Win32 API能模拟键盘鼠标,但它不知道录入的数据对不对。pyodbc能批量INSERT,但它不理解触发器的业务含义。AI能编写脚本和定位Bug,但它不会替你判断"这批组合单据应该走生产加工单还是组装单"。
工具是手段,判断力才是核心竞争力。
知道什么时候该用桌面自动化、什么时候该用数据库直连、什么时候该手工操作——这个判断力,来自对业务逻辑的深度理解,是任何工具都无法替代的。
本文基于2026年7月的真实业务实践整理。涉及的技术方案已沉淀为WorkBuddy技能(estore10-combsplit-auto、suda-tg-batch等),可复用、可迭代。
关注「从心矩」,获取更多AI落地实践。
夜雨聆风