乐于分享
好东西不私藏

当AI学会操作你的桌面软件——WorkBuddy桌面自动化的实践过程

当AI学会操作你的桌面软件——WorkBuddy桌面自动化的实践过程

当AI学会操作你的桌面软件

——WorkBuddy桌面自动化实践与可行性分析

126条组合拆分单据,每条需要在思迅eStore10客户端里手动录入货号、数量、保存、新建下一张……一条平均30秒,全做完将近一小时。这不是什么高难度的技术活,但足以把一个人的耐心磨成粉末。

本文记录的是一次真实探索:如何让AI助手(WorkBuddy)通过Win32 API直接操控桌面应用,实现组合单据的批量自动录入。包括技术方案、踩过的坑、修复过程,以及与"数据库直连"方案的深度对比。


一、问题背景

零售行业会使用思迅eStore10管理门店进销存,速达5000做财务核算。两套系统之间存在着大量的数据转录需求。

其中一类典型操作是"组合拆分单据"——在思迅客户端的"组合拆分"页面,将成品拆分为原料或将原料组合为成品,用于库存调整和成本核算。每次盘点后或新品上架时,往往需要批量处理上百条组合单据。

传统流程:

步骤
操作
耗时
1
打开组合拆分页面,选择"组合"方式
5秒
2
录入货号,按Enter确认
5秒
3
录入数量,按Enter确认
5秒
4
按F3保存
2秒
5
弹窗选择"否"(不审核)
3秒
6
关闭浮窗,新建下一张
5秒
合计单条~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分辨率、窗口最大化的条件下,关键元素的屏幕坐标是固定的:

界面元素
坐标
方式字段(组合/拆分下拉框)
(450, 202)
货号输入框
(123, 251)
数量输入框
(427, 250)

通过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码,但标点符号完全不同。常见的坑:

字符
ASCII码
对应VK
实际效果
.
46
VK_DELETE
删除键
,
44
无直接映射
可能无效
-
45
无直接映射
可能无效
Enter
13
VK_RETURN(0x0D)
恰好一致

凡是涉及标点符号的输入,都必须查虚拟键码表,不能想当然地用ord()。


四、另一条路线:数据库直连自动化

在桌面自动化之外,我们还有另一套更成熟的自动化方案——直接通过pyodbc连接数据库进行批量写入。

4.1 方案对比

以"思迅TG转货单转速达生产加工单/组装单"为例,这套方案完全不操作任何桌面应用,而是直接读写数据库:

思迅数据库(只读) → Python读取TG转货单 → 按备注分类 → 速达数据库(读写)批量INSERT
维度
桌面自动化(Win32 API)
数据库直连(pyodbc)
操作对象
思迅客户端GUI
速达SQL Server数据库
技术栈
pywin32 + 键盘鼠标模拟
pyodbc + SQL语句
速度
~5秒/条(受UI响应限制)
~0.1秒/条(批量INSERT)
稳定性
中(受窗口焦点、弹窗、坐标影响)
高(事务包裹,失败自动回滚)
安全性
低(无回滚机制,出错需手动处理)
高(三层安全保底:全量备份+数据快照+元数据)
前置条件
客户端已打开,页面正确
数据库连接参数
适用场景
目标系统无数据库访问权限
有数据库读写权限
维护成本
高(分辨率/窗口位置变化即失效)
低(SQL不依赖界面布局)

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 桌面自动化适用场景

条件
说明
目标系统无数据库访问权限
如SaaS应用、封闭式ERP客户端
操作必须经过UI业务逻辑校验
某些系统的触发器/存储过程只在UI操作时触发
数据量不大(<200条)
UI操作速度限制,大量数据不现实
操作流程固定且简单
步骤越多,出错概率越高
可接受不可回滚的风险
桌面操作无法事务回滚

5.2 数据库直连适用场景

条件
说明
有数据库读写权限
pyodbc/ODBC连接可用
数据量大(>50条)
批量INSERT效率远超UI操作
需要事务安全和回滚能力
生产环境必备
已摸清目标系统表结构和触发器
这是前提条件
需要自动化定时执行
可配合Windows任务计划/cron

5.3 混合策略:最佳实践

在实际业务中,最优解往往是两条路线的组合:

思迅数据库(只读查询) → Python处理 → 能直连速达数据库的,直接INSERT                                → 必须走UI的,用Win32自动化

具体到用户的业务场景:

  • 思迅→速达数据转录(MO配送单→PK/SS、MI退货→PK/SS、TG转货→加工单/组装单):全部走数据库直连,已实现每日定时自动执行
  • 思迅客户端内操作(组合拆分单据录入):走桌面自动化,因为这是在思迅系统内部的操作,没有速达数据库的介入
  • 速达客户端内操作(审核、反审核):走数据库直连(UPDATE billstate),触发器自动处理库存

六、反思与展望

6.1 桌面自动化的本质局限

桌面自动化解决的是"最后一公里"问题——当系统没有API、没有数据库权限、只能通过界面操作时,它是唯一的自动化途径。但它的局限也是显而易见的:

  1. 脆弱性:一次Windows更新、一次分辨率变化、一个弹窗位置偏移,整个流程就崩了
  2. 不可审计:无法像SQL事务那样回滚,出错了只能手动收拾
  3. 速度天花板:受限于UI响应速度,不可能像批量INSERT那样快
  4. 无人值守困难:执行过程中不能动鼠标键盘,否则按键发到错误窗口

6.2 AI助手的价值

在整个探索过程中,WorkBuddy(AI助手)扮演的角色不是"代替人操作",而是:

  1. 快速原型开发:从需求到可运行脚本,一次对话即可完成
  2. Bug定位与修复:小数点Bug的根因分析和修复方案,AI在几秒内就给出了正确方向
  3. 经验沉淀:每次踩过的坑都会被记录到技能(Skill)文件中,下次不会再犯
  4. 方案对比:AI能同时分析多条技术路线的优劣,帮助做出最优决策

6.3 工具的边界

工具能做的事情有边界,但工具的边界不等于人的边界。

Win32 API能模拟键盘鼠标,但它不知道录入的数据对不对。pyodbc能批量INSERT,但它不理解触发器的业务含义。AI能编写脚本和定位Bug,但它不会替你判断"这批组合单据应该走生产加工单还是组装单"。

工具是手段,判断力才是核心竞争力。

知道什么时候该用桌面自动化、什么时候该用数据库直连、什么时候该手工操作——这个判断力,来自对业务逻辑的深度理解,是任何工具都无法替代的。


本文基于2026年7月的真实业务实践整理。涉及的技术方案已沉淀为WorkBuddy技能(estore10-combsplit-auto、suda-tg-batch等),可复用、可迭代。

关注「从心矩」,获取更多AI落地实践。

相关学习资料