从“搬砖”到“指挥机器人”:一个税务场景下的 RPA 登录实现方案

0 阅读13分钟

前言:什么是 RPA?

可以先想象一下,你身边有个“超级勤快、从不抱怨、不会摸鱼”的小同事:
每天帮你打开各种系统、点各种按钮、抄各种数据,干的都是最枯燥的流程性工作,你只负责告诉 TA 规则,然后坐在一旁看着进度条走就行了。

这就是 RPA(Robotic Process Automation,机器人流程自动化)要做的事——只不过这个“小同事”是软件机器人

  • 从本质上看:RPA 是一套用程序模拟人类操作各类系统(网页、客户端、Office 等)的技术;
  • 从使用方式看:它像搭“乐高积木”一样,把一个个固定、可复用的操作步骤拼成自动化流程;
  • 从价值上看:RPA 不会替你思考业务规则,但可以替你做所有“又机械又耗时间”的那部分。

和传统“改系统、写接口”的方式比,RPA 有三个很讨喜的特点:

  • 够轻:很多场景下不用大动干戈改系统,只要机器人会“点鼠标、敲键盘”就能上;
  • 够快:流程改动时,经常只是改脚本和配置,不需要再走一遍漫长的需求评审和上线流程;
  • 够接地气:做的就是一线同事每天在做的事,只是把“人肉流水线”换成了“机器人流水线”。

在税务、财务、人力、运营这些“规则多、节奏快、表格多”的领域,大量流程天然适合 RPA——
先把手从鼠标上解放出来,再慢慢把脑子从流程里解放出来。


一、背景:为什么要让“机器人”帮我们登录?

先从一个大家最熟的动作说起:登录

在一个典型的税务 / 财务业务场景里,一线人员每天可能要重复很多次类似的动作:

  • 打开客户端或浏览器;
  • 登录某个业务系统;
  • 等待系统加载完各种首页、公告、提示框;
  • 根据当前账号,确认绑定关系、权限信息,再决定能不能继续后续操作。

这些动作有什么特点?

  • 路径固定:每次点的地方都差不多;
  • 步骤稳定:顺序清晰,很少需要“灵机一动”;
  • 频率极高:一天要来回好几轮,甚至几百次调用;
  • 但又不能省略:不登录、不检查,就没法往后走。

这对人来说是“消耗心情”的重复劳动,对机器人来说却是“天然主场”。
所以本文就聚焦在 RPA 流程中的一个关键环节:“登录 + 前置校验”

我们会基于一个真实项目抽象出的架构,看看如何用前端页面 + 本地客户端函数 + 后端接口,让“登录”这件事从此变成机器人的一键小任务。


二、整体架构:从“浏览器里的脚本”到“桌面上的机器人”

结合整个前端项目和桌面端 RPA 服务,可以把这套方案抽成一个很好理解的“四层三明治”:

  • 第 1 层:浏览器端脚本层(Pages & Scripts)
    负责和网页打交道,分两类:
    • 页面脚本:负责渲染结果页、登录页等(比如登录结果展示页),像是“舞台布景”;
    • 功能脚本:负责自动登录、列表查询、任务跳转等,像是“在舞台上表演的演员”;
  • 第 2 层:客户端桥接层(Native Bridge)
    负责和本地客户端说话,通过类似 NativeBridge / NativeBridgeSync 的接口:
    • 读本地配置、账号信息;
    • 写日志文件;
    • 把结果回传给 RPA Worker;
  • 第 3 层:后端服务层(HTTP API)
    负责所有“问后端要答案”的动作,通过一个统一的 HTTP 封装:
    • 获取 RSA 公钥;
    • 获取登录 UUID;
    • 执行登录;
    • 查询默认身份、任务列表等;
  • 第 4 层:桌面 Worker 调度层(RpaWorker)
    负责“管机器人上班打卡”的那个人:
    • 通过类似 autoService 的配置,管理有哪些任务、多久跑一次;
    • 定时打开对应的 RPA 页面(登录、同步、审核、验证……);
    • 汇总每次执行结果,并决定下一步怎么跑。

如果把这四层串起来,一次完整的端到端 RPA 流程大概长这样:

  1. 桌面 Worker 按计划发起任务
    看配置文件,决定现在该跑哪个任务(比如“xx同步”、“xx更新”等),然后打开对应的 HTML 页面;
  2. 页面加载 & 登录脚本注入
    页面加载完后,由“注入脚本”清理掉不需要的脚本节点,再动态插入真正负责业务登录的脚本;
  3. 脚本桥接客户端 + 调后端接口
    • 从本地客户端拿到登录信息、账号标识、服务端baseURL;
    • 调用后端一系列接口(取公钥、取 UUID、执行登录、查默认身份、查任务列表等),直到登录彻底打通;
  4. 在浏览器里模拟人操作
    功能脚本通过 DOM 查询、iframe 访问、第三方 UI 组件等,执行“点击按钮、切 Tab、输入搜索条件、打开任务详情”等动作;
  5. 结果打包回传,等待下一轮调度
    本轮场景执行完毕后,把“成功 / 失败 + 消息 + 关键参数”打包,通过本地桥接回传给 RPA Worker,日志里也记上一笔,等下一个周期再来一轮。

后面几个小节会分别用“登录入口页”和“任务调度配置”这两个切片,拆给你看。


三、核心页面结构示例

登录场景的 RPA 入口页本质上是一个简单的 HTML 页面,分为三个展示区域:

  • 获取账号绑定信息的结果;
  • 系统登录结果;
  • RPA 脚本注册结果。

下面是一个脱敏后的结构示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>自动登录示例(RPA)</title>
</head>
<body>
  <div class="result">
    <div class="binding-info">
      <h2>获取账号绑定信息</h2>
      <div class="code">
        <span>成功状态:</span>
        <span id="binding-code"></span>
      </div>
      <div class="msg">
        <span>失败消息:</span>
        <span id="binding-msg"></span>
      </div>
    </div>
    <div class="login">
      <h2>系统登录结果</h2>
      <div class="code">
        <span>成功状态:</span>
        <span id="login-code"></span>
      </div>
      <div class="msg">
        <span>失败消息:</span>
        <span id="login-msg"></span>
      </div>
    </div>
    <div class="rpa-script">
      <h2>RPA 脚本注册</h2>
      <div class="code">
        <span>成功状态:</span>
        <span id="rpa-code"></span>
      </div>
      <div class="msg">
        <span>失败消息:</span>
        <span id="rpa-msg"></span>
      </div>
    </div>
  </div>

  <script type="text/javascript" src="./rpa-login.js"></script>
</body>
</html>

页面本身非常“傻”,主要由脚本来驱动逻辑,DOM 只承担结果展示的职责。


四、核心脚本逻辑:从页面加载到结果回传

你可以把 脚本 想象成一个“自启动的小机器人”,页面刚加载完,它就开始自言自语地说道:

  • “先记个日志,证明我真的开始干活啦”;
  • “去问问本地:我是谁,我在哪呢”;
  • “再去问问后端:我这个账号到底合不合规呀”;
  • “最后把结果打包丢回宿主系统,本次任务结束咯”。

1. 脚本整体结构

document.addEventListener('DOMContentLoaded', function () {
  try {
    // 1. 记录开始日志(通过本地客户端桥接)
    window.NativeBridge('LogInfo', JSON.stringify({
      fileName: 'rpa-login.log',
      msg: '开始执行自动登录流程'
    }), '');

    // 2. 获取当前账号
    var accountId = window.NativeBridgeSync('GetLocalAccount');
    if (!accountId) {
      handleScene('fail', '本地账号为空,无法继续处理', null);
      return;
    }

    // 3. 获取服务端基础地址
    var serviceBaseUrl = window.NativeBridgeSync('GetServiceBaseURL');
    if (!serviceBaseUrl) {
      handleScene('fail', '服务端基础地址为空,无法继续处理', null);
      return;
    }

    // 4. 向后端请求账号绑定信息
    fetch(serviceBaseUrl + '/api/binding/query', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ accountId: accountId })
    })
      .then(function (response) {
        return response.text();
      })
      .then(function (resText) {
        if (!resText) {
          handleScene('fail', '接口返回为空', null);
          return;
        }

        var res = JSON.parse(resText);
        if (res.code !== 'SUCCESS') {
          handleScene('fail', '查询绑定信息接口调用失败', null);
          return;
        }

        if (!res.data) {
          handleScene('fail', '查询绑定信息返回数据为空', null);
          return;
        }

        // 假设后端在 data 中返回认证结果码
        if (res.data.authResultCode === '00000') {
          // 将关键数据编码后通过参数回传给宿主 RPA
          var encodedParams = window.btoa(
            encodeURIComponent(JSON.stringify(res.data))
          );
          handleScene('success', '获取绑定信息并校验通过', encodedParams);
        } else {
          handleScene(
            'fail',
            '认证未通过:' + res.data.authResultMessage + '(' + res.data.authResultCode + ')',
            null
          );
        }
      })
      .catch(function (error) {
        handleScene('fail', '调用接口异常:' + error.toString(), null);
      });
  } catch (e) {
    console.error(e);
    handleScene('fail', '自动登录流程执行异常', null);
  }

  /**
   * 统一场景处理函数
   * @param {'success' | 'fail'} code
   * @param {string | null} message
   * @param {string | null} parameters 已编码的参数
   */
  function handleScene(code, message, parameters) {
    // 1. 页面展示
    document.getElementById('binding-code').innerText = code;
    document.getElementById('binding-msg').innerText = message || '';

    // 2. 本地日志
    window.NativeBridge('LogInfo', JSON.stringify({
      fileName: 'rpa-login.log',
      msg: message
    }), '');

    // 3. 将结果结构化回传给宿主 RPA 框架
    window.NativeBridge('InitParametersResult', JSON.stringify({
      code: code,
      msg: message,
      parameters: parameters
    }), '');
  }

  /**
   * 用于展示后续步骤(如真正的登录、脚本注册)的结果
   * @param {'login' | 'rpa'} type
   * @param {'success' | 'fail'} code
   * @param {string} message
   */
  window.showResult = function (type, code, message) {
    if (type === 'login') {
      document.getElementById('login-code').innerText = code;
      document.getElementById('login-msg').innerText = message;
    }
    if (type === 'rpa') {
      document.getElementById('rpa-code').innerText = code;
      document.getElementById('rpa-msg').innerText = message;
    }
  };
});

五、设计要点拆解

1. 本地桥接:让前端“说得懂”客户端

在很多桌面型场景中,前端页面并不是跑在“浏览器地址栏”里,而是嵌在某种 WebView / CEF 容器中。
此时,页面就像是住进了客户端的一间“房间”,需要通过事先约定好的“对讲机”(比如 window.NativeBridge / window.NativeBridgeSync)跟外界交流:

  • 写入本地日志文件,便于现场排查;
  • 获取当前操作员或账号 ID;
  • 读取本地配置(比如服务端地址、代理配置等)。

这样一来,RPA 逻辑就可以复用客户端现有能力,而无需在脚本中硬编码环境信息啦。

2. 接口调用:让业务判断尽量留在后端

登录这件事,一旦牵涉到风控、权限、认证结果,就需要跟后端打很多交道。
在这套实现里,前端脚本的姿态非常简单粗暴,只做这几件事:

  • 组织好请求参数(如账号 ID);
  • 调用后端统一入口(/api/binding/query 一类);
  • 解析结果并根据成功 / 失败走不同分支。

业务规则尽量放在后端,例如:

  • 账号与业务系统之间的绑定关系;
  • 是否已通过某种实名认证;
  • 返回给前端的业务结果码和中文提示。

前端只负责“选择哪条路走”,而不负责“规则本身是什么”。
这样,规则变了动后端,流程变了动前端,分工足够清晰。

3. 统一场景处理:handleScene 的价值

handleScene 可以理解为一个“统一出口的值班前台”,所有情况最后都由它来接待:

  1. 更新页面展示,方便人工观察;
  2. 记录本地日志,便于运维与排查;
  3. 通过统一结构把结果回传给宿主 RPA 引擎。

这样,无论在哪个环节翻车(本地信息为空、接口失败、业务未通过、脚本异常),
最终都会“汇聚”到这一个出口,而不是在代码各个角落散落一地 的alertconsole.log 和“随手一抛的错误”。

4. 结果透传:为后续 RPA 步骤提供“燃料”

当认证通过时,示例中会把 res.data 编码后放到 parameters 字段中回传。
在实际项目中,你可以根据需要:

  • 只回传最关键的几个字段;
  • 或者回传整个数据结构,在后续流程中进行二次拆解和使用。

关键在于:RPA 的每个子步骤都要有清晰的输入和输出
只有这样,才能像搭积木一样,把多个小机器人串起来,变成一条真正可靠的自动化生产线。


六、工程化实践:不仅是一段“脚本”,而是一个“项目”

在工程层面,这类 RPA 脚本完全可以、也应该享受现代前端工程体系的加成,而不是停留在“桌面上一堆 .js 文件”的野生时代:

  • 使用构建工具(如 webpack)进行多入口打包;
  • 使用 Babel 处理兼容性问题;
  • 使用 ESLint 等工具统一代码风格;
  • 使用 NPM 脚本统一构建命令,例如:
npm run build

这样做的收益是:

  • 可维护:接口变更、字段调整时,能快速定位并修改;
  • 可扩展:在现有框架上增加新的 RPA 场景页面,不断扩展自动化版图;
  • 可协作:多开发者可以在统一规范下并行推进,而不是各写各的“个人脚本”。

七、任务调度与多场景编排:从“一个脚本”到“一个机器人工人”

在真实桌面环境里,几乎不会只有一个“登录机器人”。
更多时候,你需要的是一整套“机器人工人队伍”(可以理解为一个 RpaWorker 集群),在不同时间点,自动去执行不同的业务脚本。

一个很实用的做法,是用一个独立的配置文件(例如某个 autoService.json)来描述所有可调度的任务。
对于每个任务,你只需要关心几件事:

  • 任务编码code,用于唯一标识某个自动化任务;
  • 任务名称name,用于给业务人员看的中文描述;
  • 命令编号commandId,通常与客户端或调度器内部的指令一一对应;
  • 页面路径URLPath,指向对应的 RPA 页面,例如某个同步、更新或交互场景;
  • 启动延迟DelayTime,单位秒,表示客户端启动后多久开始首次执行该任务;
  • 默认间隔DefaultInterval,单位秒,表示两次自动执行之间的时间间隔。

一个经过脱敏的任务配置示例如下:

[
  {
    "code": "T01",
    "name": "同步某类业务",
    "commandId": "1",
    "URLPath": "syncTask.html?type=001",
    "DelayTime": "1800",
    "DefaultInterval": "7200"
  },
  {
    "code": "T02",
    "name": "更新某类业务",
    "commandId": "2",
    "URLPath": "updateTask.html?type=002",
    "DelayTime": "1800",
    "DefaultInterval": "7200"
  },
  {
    "code": "T03",
    "name": "创建xx任务",
    "commandId": "3",
    "URLPath": "create.html",
    "DelayTime": "1800",
    "DefaultInterval": "7200"
  }
]

在实际运行时,桌面端会启动一个常驻的“RPA 工人”进程,像一个守在机房里的调度小哥,小哥会执行这样的任务:

  • 读取上述配置,按 DelayTimeDefaultInterval 定时打开对应的 RPA 页面;
  • 每个页面内部用类似前文的方式(本地桥接 + 后端接口)完成一次完整的自动化流程;
  • 执行完毕后,将结果回传给宿主并写入日志,等待下一次调度。

这种“配置驱动 + 多场景页面 + 统一脚本框架”的方式,有几个非常接地气的好处:

  • 扩展新场景很简单:大部分时候只是新增一个页面 + 一条配置;
  • 部署与灰度方便:可以在不同环境/机构下,通过不同的配置组合启用不同的任务;
  • 业务可见性更强:业务人员可以通过任务列表直观看到当前有哪些机器人任务在运行。

从工程视角看,RPA 页面 + Worker 调度 + autoService 配置 组合起来,
就不再是“某个开发偷偷写的小脚本”,而是一套真正可运营、可演进的机器人体系!


八、结语:从一个登录页面开始的 RPA 之路

一个看似不起眼的“自动登录 + 绑定校验”页面,
其实已经悄悄具备了一个成熟 RPA 体系所需的很多关键要素:

  • 与本地客户端的能力集成;
  • 与后端业务的接口协同;
  • 统一的结果处理与日志体系;
  • 工程化的构建与部署方式。

从这样一个小小的登录场景做起,你可以逐步:

  • 把更多重复流程交给机器人;
  • 把更多时间还给真正需要人思考的工作;
  • 让自己从“流程执行者”变成“流程指挥者”。

如果你正准备在税务 / 财务 / 政务等领域实践 RPA,
希望我深夜码的文章能成为你搭建“第一支机器人小队”的起点。