日报自动化实战:告别手动复制粘贴

书接上回,上回我们聊了处理重复任务的自动化思维。

其中,我举了用工具自动化公司日报的例子。

今天,我就来详细说说,我到底是怎么做的,以及过程中遇到了哪些问题和挑战。

背景

我们公司使用某第三方系统有一个自定义的数据看板,每天需要向群里发送日报。之前,这项工作由团队成员轮流手动完成:从系统的一个自定义看板复制数据到 Excel,再将表格转为图片,发到群里。

轮到我负责的那一周,我左手边电脑打开系统,右手边打开 Excel,一个个数据复制过去,3.4%、-10%......为避免出错,还要逐一核对。整个过程每天耗时大约 7 到 10 分钟,繁琐又枯燥。

我开始思考:这种重复性工作能不能自动化?

于是,我在群里向大佬们请教,提出了这个问题:

结果,消息已读,没有一个人回复。

那一刻,我暗下决心:我要自己解决这个问题!

初探

于是乎我打开了改系统,开始研究。

该系统大概长这样, 这是一个自定义看板,后台自定义配置出来的,数据是根据配置的规则算出来的,有十几项目,我们是需要从每项取3个数据。加起来复制30-40次。

  • 手动复制效率低下。
  • 浪费时间。
  • 容易出错,粘错位置了,又得一个个重新对一遍。

所以我第一步是需要把手动复制拿数据的这个过程,利用脚本自动化了。

流程与任务拆解

我们的思路是这样,先脑子里过一下原来的流程,然后一步步自动化原来的流程。

1、原来手动的流程

  1. 手动登录系统
  2. 点击对应面板,一个个复制数据。
  3. 复制为图片
  4. 发送到群里。

2、脚本任务拆解

  1. js逆向登录加密方法,自动化登录,拿到token。
  2. 利用爬虫抓取数据,拿到我需要的。
  3. 利用canvas将数据画成表格,然后转成图片。
  4. 图片传到oss,调用钉钉webhook接口,定时发送到群里

以上我们已经将,手动的流程的任务与自动化需要做的任务一一对应了。

现在我们思路清晰了。

然后我们要做的就是把每个任务逐个攻克即可。

任务分步实现

你不觉得我应该先完成第一个任务------JS 逆向登录加密方法,实现自动化登录并获取 token 吗?

这确实是全自动流程中最核心的一环:没有自动登录获取凭证,后续的数据抓取和操作根本无从谈起。

不过,我初步分析了登录接口,发现参数加密逻辑较复杂,短时间内难以破解。

于是我选择暂时跳过,先手动复制登录凭证,确保后续流程全部打通后再回过头补全自动化登录部分。

1、利用爬虫抓取数据,拿到需要的。

首先看板这是个列表,有很多项目,首先看这个列表怎么来的,服务端渲染还是,调的接口。 然后看能不能完全拿到,还是说,每一项会再单独掉接口去拿。

1.1内容搜索大法

众多接口,我们怎么找到我要的数据在哪,于是我们利用调试工具,搜索响应内容关键字

例如搜页面中显示的这个标题
通过内容再network搜索内容 找到了列表接口

点开看,确实,里边就是这个列表的数据。

但是没有具体是环比、同比,我要的数字。

再次寻找每一项具体数据的获取接口。

再次通过搜索大法找到了通过每项id和过滤条件去获取具体数据的接口

1.2 接口找齐,开始编码

整体逻辑是,先获取面板列表,然后循环列表的每一项,拿着参数去调详情。

面板的数据列表获取
js 复制代码
/**
 * 获取重点功能监控面板列表及详情数据
 * @returns {Promise<*[]>}
 */
async function queryReportList(dashboard) {
    const { id: dashboard_id, common_event_filter } = dashboard

    const data = await fetch(
        `https://xxx/api/v2/sa/dashboards/${dashboard_id}?is_visit_record=true`,
        {
            credentials: "include",
            headers: {
                "User-Agent":
                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
                Cookie: Cookie
            },
            referrer:
                `https://xxx/dashboard/?dash_type=lego&id=${dashboard_id}&project=fotorglobalproduct&product=sensors_analysis`,
            method: "GET",
            mode: "cors"
        }
    )
        .then(res => res.json())

    const result = [];
    // 获取控面板的前12个监控项的监控数据。
    for (const item of data.items.slice(0, 13)) {
        if (item.bookmark) {
            // 这里解出来, 调下一个接口要用到。
            const data = JSON.parse(item.bookmark.data);
            const res = await queryReportByTool({
                bookmarkid: item.bookmark.id,
                measures: data.measures,
                dashboard_id: dashboard_id,
                common_event_filter: common_event_filter
            });
            result.push({
                ...res,
                name: item.bookmark.name
            });

            console.log(
                {
                    name: item.bookmark.name,
                    base_number: res.base_number /= 100,
                    day: res.month_on_month /= 100,
                    week: res.year_on_year /= 100
                }
            )
        }
    }

    return result
}
获取每一项具体数据
js 复制代码
/**
 * 报告列表的报告id去获取具体数据
 * @param params
 * @returns {Promise<T|*|undefined>}
 */
async function queryReportByTool(params) {
    const requestId = Date.now() + ":803371";
    const body = {
        measures: params.measures,
        unit: "day",
        by_fields: [],
        sampling_factor: null,
        from_date: dayjs()
            .subtract(14, "day")
            .format("YYYY-MM-DD"),
        // from_date:  "2025-02-28",
        to_date: getYesterDay(),
        // to_date: "2025-03-13",
        detail_and_rollup: true,
        enable_detail_follow_rollup_by_values_rank: true,
        sub_task_type: "SEGMENTATION",
        time_zone_mode: "",
        server_time_zone: "",
        include_today: true,
        bucket_params: {},
        chartType: "line",
        rangeText: "14 day",
        bookmarkid: params.bookmarkid,
        rollup_date: false,
        mixed_response: true,
        subjectIdCnameText: "用户",
        subjectIdUnit: "人",
        dashboard_id: params.dashboard_id,
        use_cache: true,
        show_mom_ratio: true,
        show_yoy_ratio: true,
        yoy_rule: "yoy_week",
        request_id: requestId,
        common_event_filter: params.common_event_filter
    };
    try {
        const data = await fetch(
            `https://xxxx/api/events/compare/report/?bookmarkId=${
                params.bookmarkid
            }&async=true&timeout=10&request_id=${requestId}`,
            {
                credentials: "include",
                headers: {
                    "User-Agent":
                        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
                    ...
                    Cookie
                },
                referrer:
                    "https://xxxx/dashboard/?dash_type=lego&id=692&project=fotorglobalproduct&product=sensors_analysis",
                body: JSON.stringify(body),
                method: "POST",
                mode: "cors",
                timeout: 10000
            }
        ).then(res => res.json());
        if (!data || data.isDone === false) {
            return await queryReportByTool(params);
        } else {
            return data;
        }
    } catch (e) {
        return await queryReportByTool(params);
    }
}

1.3 数据拿到

执行一下,数据拿到了,也拿出来了我要的几个字段

js 复制代码
PS D:\project2\report> node .\index.js
{
  name: 'xxx生成失败率',   
  base_number: 0.0103,      
  day: -0.3602,
  week: -0.16260000000000002
}
...
{
  name: 'xxxx生成失败率',
  base_number: 0.017,
  day: 0,
  week: 0.0241
}
2025-03-18.xlsx文件已保存!
default: 27.917s

2、生成图片

node-cavas 生成图片

3、图片传到oss,调用钉钉webhook接口,定时发送到群里

传图

js 复制代码
const filePath = `/custom/999/${dashboard.worksheetName}-${dayjs().format('YYYYMMDD')}.jpeg`
const uploadRes = await tencentCos.upload(imageBuffer, filePath, true)

发钉钉群

js 复制代码
async function sendDingTalkMessage(text) {
    // const today = dayjs()
    //     .format("YYYY-MM-DD")
    const token = '1a6e1111111' // 大群机器人
    const result = await fetch(`https://oapi.dingtalk.com/robot/send?access_token=${token}`, {
        method: 'post',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            "msgtype": "markdown",
            "markdown": {
                "title": "监控日报",
                // "text": `#### ${title}-${today} \n  > ![screenshot](${imageUrl}) \n`
                "text": text
            },
            "at": {
                "isAtAll": true
            }
        })
    }).then(res => res.json())
    console.log(result)
    if (result.errcode === 0) {
        console.log('发送成功')
        return true
    }
}

4、js逆向登录加密方法,自动化登录。

就剩下自动登录了。

4.1 为什么要自动化登录?

因为这个系统登录凭证在一定时间内会过期,且不是明文登录的,登录接口参数加密了的。

看到这你就得去研究他的加密规则了

或者止步于此,手动复制登录凭证,本地执行脚本也是可以。

我如果要在服务器上自动化整个流程,必须得让他自动登录拿到登录凭证。

4.2 逆向步骤

4.2.1找到登录接口

先点页面的登录,找到登录接口,在请求调用栈中随便找个位置先打个断点,然后刷新页面,再次点击登录,嘿,您猜怎么着,断住了!!!

4.2.2 顺着调用栈找逻辑

顺着调栈给上找逻辑。所有在前端加密的一定是可以模拟的。

找到了调登录的方法。 看了下这就是调store里的方法passport/login。

从这再往下就比较不容易了,因为你会发现,就有点乱了。进到的都是混淆的一些abcdefg名字的方法。

但是咱们明确目标就是要找到调用的方法 passport/login的位置。

我尝试了如下

  • 搜索passport/login关键字
  • 搜索接口路径

找了一辈子, 终于找到了调接口的地方

看到这个Me方法,传递了一个 isEncrypted 我猜测就是 是否要加密参数的意思吧。

别搁Me方法外面蹭了,赶紧进去看看。

你就给我看这个? 这里面又调了另一个

咱们接着进到xt.request。

您猜怎么着,还没到,这里又进行了一顿操作之后,调了一个名为P的方法。

好好好,继续继续。

4.2.3 找到了加密的位置

到了P方法,终于是没给我玩套娃了啊。

在这一步终于是看到了关键字 isEncrypted

看了代码确实是判断isEncrypted加密的。

看了后发现这是一个RSA+AES结合的加密方式

RAS加密密钥, AES加密登录数据。

加密流程总结:

  1. RSA保护AES密钥的安全传输
  2. AES保护实际登录数据的机密性
  3. 双重加密确保登录信息在传输过程中的安全性
为何不用单一的加密方式?

那么你有没有这样的疑问呢?为什么不单独用rsa直接加密数据呢?岂不简单。

当然不行,是有原因的!

RSA长度限制 RSA加密算法对明文长度有严格限制,具体取决于密钥长度和填充方式‌。以下是不同密钥长度下的最大明文长度(以字节为单位):

  • 1024位密钥 ‌:最大明文长度约为 ‌117字节‌‌
  • 2048位密钥 ‌:最大明文长度约为 ‌245字节‌‌
  • 4096位密钥 ‌:最大明文长度约为 ‌512字节‌‌

所以RSA加密超出长度的会报错的。

所以先生成短密钥,再使用RSA加密AES对称算法的密钥,再用对称密钥加密实际数据‌。这是实际应用中的常见做法,兼顾安全性和效率‌

4.2.4 模拟他的加密过程
大致流程

所以你需要怎么做?

  1. 把加密逻辑copy啊。
  2. 补环境。
  3. 不断尝试直到通过后端校验。
理解后端如何解密和校验

前面我们说到

RAS加密密钥, AES加密登录数据。

那么后端的校验流程就是:

  1. 私钥解出密钥
  2. 密钥配和iv、salt等再解出被AES加密的账号密码信息.

知道了这些,那么我们需要做的就是正确加密和传递相关信息,如果校验失败,我们就要来回对比差异,找到问题,不断尝试。

遇到的问题
  • 加密的包的版本跟目标网站用的不一样导致校验失败,后经过漫长的查找找到了一样的版本。
  • 还要注意header里带的字段,都要模拟他加密后的带过去。例如这几个。

真的是一个不断尝试,不断解决问题的过程。

最终我抽出来的方法

js 复制代码
var b = require("crypto-js");
var jsencrypt = require("nodejs-jsencrypt/bin/jsencrypt").default;

/**
 * js逆向回来的方法,模拟xx登录对参数加密
 * @param body xx登录参数
 * @param public 公钥
 * @returns {{headers: {"aes-salt": string, "aes-iv": string, "aes-passphrase": *, "X-Request-Timestamp": string, "X-Request-Id": string, "X-Request-Sign": *}, body: string}}
 */
function encryptLogin(body, public) {
    const W = new jsencrypt();
    W.setPublicKey(public)

    q = b.enc.Utf8.parse(Math.floor(Math.random() * 1e6) + Date.now()).toString();
    // q = "31373432353237383135363835";
    var re = W.encrypt(q)
        , ie = b.lib.WordArray.random(128 / 8)
        , fe = b.lib.WordArray.random(128 / 8)
        , ue = b.PBKDF2(q, ie, {
        keySize: 128 / 32,
        iterations: 100
    })
        , ye = b.AES.encrypt(JSON.stringify(body), ue, {
        iv: fe,
        mode: b.mode.CBC,
        padding: b.pad.Pkcs7
    });

    const j = "/api/v2/auth/login?is_global=true"
    const Ee = parseInt(Date.now() / 1000).toString()
    const he = Ee
    const Fe = ye.toString()

    var bt = "".concat(Ee, "_").concat(he, "_").concat(j, "_").concat(Fe, "_14skjh");

    const res = {
        headers: {
            "aes-salt": ie.toString(),
            "aes-iv": fe.toString(),
            "aes-passphrase": re,
            "X-Request-Timestamp": Ee,
            "X-Request-Sign": b.MD5(bt).toString(),
            "X-Request-Id": he,
        },
        body: ye.toString()
    }

    return res
}

任务分步都实现了(自动化了)。

串联起来这四步,整体就实现了。

效果展示

总结

  • 先通后补:登录逆向卡壳,先手动Cookie跑通全链,再回填自动化。

  • 逆向不怕乱:混淆代码里断点+全局搜索(接口路径/关键字),总能定位加密点。

  • 加密常RSA+AES:RSA只加密短密钥,AES加密长数据,补环境+对齐Header字段是关键。

  • 贵在坚持:第一天研究无果别灰心,第二天重新上手,灵感与进展常不期而至。

虽然文章写得像一帆风顺,但实则磕磕绊绊------在层层混淆的代码里翻找,第一天方法不对,左冲右突脑壳嗡嗡作响。幸好第二天没放弃,沉下心继续深挖,一步步试错、迭代,终于攻克所有难题。

如果有小伙伴想跟我探讨细节,欢迎联系!

喜欢的话,点点关注。

相关推荐
晴殇i4 小时前
JavaScript还能这样写?!ES2025新语法让代码优雅到极致
前端·javascript·程序员
浏览器API调用工程师_Taylor4 小时前
我是如何将手动的日报自动化的☺️☺️☺️
前端·javascript·爬虫
东哥很忙XH5 小时前
flutter开发的音乐搜索app
android·javascript·flutter
前端Hardy5 小时前
HTML&CSS&JS:抖音爆火的满屏“关心弹幕”酷炫卡片,已经帮你打包好了,快来体验吧!
前端·javascript·css
江城开朗的豌豆5 小时前
我的Vue项目胖成球了!用Webpack给它狠狠瘦个身
前端·javascript
WebInfra5 小时前
Rspack 1.6 发布:让打包产物更小、更纯净
前端·javascript·前端框架
Mintopia5 小时前
⚙️ Next.js 接口限流与审计全攻略 —— 用 @upstash/ratelimit 打造优雅“闸门”
前端·javascript·全栈
Mintopia5 小时前
🌐 实时翻译 + AIGC:Web跨语言内容生成的技术闭环
前端·javascript·aigc
Cache技术分享5 小时前
225. Java 集合 - List接口 —— 记住顺序的集合
前端·后端