需要说明一下

  1. 最近钉钉升级,中间打卡部分Layout中的控件内容AutoJs捕获不到了;
  2. 可以通过另一种方式实现,就是AutoJs实现区间时段随机启动钉钉,在钉钉中打开自动打卡功能。

我们这里的钉钉自动打卡主要是使用AutoJs脚本实现的,但由于AutoJs受限于android版本,高版本的Android手机,可能无法实现完整功能,尽量使用Android10或一下版本的手机。

操作如下:

  1. 手机安装AutoJs;
  2. 开发机安装VSCode + Auto.js-Autox.js-VSCodeExt;
  3. 在VSCode中启动AutoJs服务监听
  4. 手机连接服务器
  5. 开发调试
  6. 打包应用

具体操作详情可以百度,这里就不展开细讲了,下面是主要脚本代码:

  1. main.ts

    import {Cron} from "croner";
    import Ding from "./ding";
    import {ZKER_CHECKIN_PACKAGENAME} from "./utils/constants";
    
    const checkin = () => {
      const timeout = random(1, 9)
      console.log(timeout + "秒后执行")
      setTimeout(() => {
        console.log("任务开始:", new Date().getTime())
        threads.start(() => new Ding().checkin())
      }, timeout * 1e3 * 60)
    }
    
    new Cron("0 25 08 ? * 1-5", checkin);
    new Cron("0 35 17 ? * 1-5", checkin);
    
    ui.layout(
        <>
          <frame w="*" h="*">
            <vertical layout_gravity="top|center" w="auto" h="auto">
              <text text="" textSize="8sp"/>
              <text text="打卡任务已在后台运行" textSize="11sp"/>
              <text text="保持程序正常运行即可" textSize="11sp" color="red"/>
            </vertical>
            <vertical layout_gravity="center" w="auto" h="auto">
              <button id="testRun" text="测试打卡"/>
              <button id="testWS" text="连接WS"/>
              <button id='exit' text="退出"/>
            </vertical>
            <vertical w="auto" h="auto" layout_gravity="bottom|center">
              <text text="钉钉自动打卡程序 PowerBy @ZKer" textSize="8sp"/>
              <text text="" textSize="8sp"/>
            </vertical>
          </frame>
        </>
    );
    
    ui.testRun.click(() => threads.start(() => new Ding().checkin()))
    
    ui.exit.click(() => app.exit())
    ui.testWS.click(() => {
      threads.start(() => {
        let nickName = "ZKer"
        try {
          nickName = new Ding().getName()
        } catch (e) {
          nickName = "心境"
        }
        app.launch(ZKER_CHECKIN_PACKAGENAME)
    
        importPackage(Packages["okhttp3"]); //导入包
        const client = new OkHttpClient.Builder().retryOnConnectionFailure(true).build();
        const request = new Request.Builder().url(`ws://www.kuaiyuai.top:59000/flamigo/ding/ws?name=${nickName}`).build(); //vscode  插件的ip地址,`
        client.dispatcher().cancelAll();//清理一次
    
        const myListener = {
          onOpen: function (webSocket, response) {
            print("onOpen");
            //打开链接后,想服务器端发送一条消息
            const json = {
              type: "device_info",
              data: {
                device_model: device.model,
                device_hardware: device.hardware,
                // device_imei: device.getIMEI(),
                device_serial: device.serial,
                android_version: device.release,
                baseOS: device.baseOS,
                app_version: app.versionName,
                app_version_code: app.versionCode
              }
            };
            const hello = JSON.stringify(json);
            webSocket.send(hello);
          },
          onMessage: function (webSocket, msg) { //msg可能是字符串,也可能是byte数组,取决于服务器送的内容
            print("onMessage:", msg);
            threads.start(() => new Ding().checkin());
          },
          onClosing: function (webSocket, code, response) {
            print("正在关闭");
          },
          onClosed: function (webSocket, code, response) {
            print("已关闭");
          },
          onFailure: function (webSocket, t, response) {
            print("错误", t);
            client.newWebSocket(request, new WebSocketListener(myListener)); //创建链接
          }
        }
    
        const webSocket = client.newWebSocket(request, new WebSocketListener(myListener)); //创建链接
      })
    })
    
  2. ding.ts

    import {SERVER_CHECKIN_URL, ZKER_CHECKIN_PACKAGENAME} from "./utils/constants";
    
    export default class {
        private appName: string = "钉钉"
        private packageName: string = "com.alibaba.android.rimet"
        private corporation: string = ""
        private nickName: string = ""
        private isRunning: boolean = false
    
        constructor() {
            this.checkStatus()
        }
    
        openAppSetting() {
            app.openAppSetting(this.packageName);
            text(this.appName).waitFor();
            sleep(500)
            console.log("判断控件是否已启用(想要关闭的app是否运行)")
            return textMatches(/(.*强.*|.*停.*|.*结.*|.*行.*)/).findOne();
        }
    
        checkStatus() {
            const is_sure = this.openAppSetting()
            this.isRunning = is_sure.enabled()
            return this.isRunning
        }
    
        kill() {
            const is_sure = this.openAppSetting()
            if (is_sure.enabled()) {
                console.log("结束应用的控件如果无法点击,需要在布局中找寻它的父控件,如果还无法点击,再上一级控件,本案例就是控件无法点击")
                is_sure.click();
                is_sure.parent().click(); // 确保保小米等收集在sure按钮层无法正确点击
                console.log("需找包含“确”,“定”的控件")
                textMatches(/(.*确.*|.*定.*)/).findOne().click();
                this.isRunning = false
                console.log(this.appName + "应用已被关闭");
                sleep(1000);
                back();
            } else {
                console.log(this.appName + "应用不能被正常关闭或不在后台运行");
                back();
            }
        }
    
        run() {
            if (this.isRunning) {
                this.kill()
            }
            console.log("打开应用")
            app.launch(this.packageName)
            this.isRunning = true
        }
    
        getName() {
            this.run()
    
            console.log("开始获取名字...")
            while (!click("我的")) ;
            this.nickName = id(`${this.packageName}:id/tv_biz_card_nick`).findOne().text();
            console.log("获取到名字:", this.nickName)
            return this.nickName
        }
    
        checkin() {
            this.getName()
    
            while (!click("工作台")) ;
    
            this.corporation = id(`${this.packageName}:id/tv_org_name`).findOne().text();
            console.log("获取到公司名:", this.corporation)
    
            let btn = text("考勤打卡")
            btn.waitFor();
            text("考勤打卡").findOne().click();
    
            btn = textEndsWith("班打卡")
            btn.waitFor();
            btn.findOne().click();
    
            sleep(8000)
    
            if (text("继续打卡").exists()) {
                text("继续打卡").findOne().click()
            }
    
            if (text("拍照备注并打卡").exists()) {
                console.log("找到【拍照备注并打卡】")
                text("拍照备注并打卡").findOne().click()
                id(`${this.packageName}:id/camera_takephoto`).findOne().click();
                // const pic_id = id(`${this.packageName}:id/picedit_usephoto`).findOne()
                text("确认").waitFor()
                text("确认").findOne().click()
            } else {
                console.log("没找到【拍照备注并打卡】")
            }
    
            const now = new Date()
            console.log("now", now.toLocaleDateString())
    
            textEndsWith("班打卡").waitFor();
            
            sleep(10000)
            const success = textEndsWith("班打卡成功").exists()
            back()
    
            const params = {
                success,
                name: this.nickName,
                corporation: this.corporation,
                timestamp: now.getTime().toString(),
            }
            console.log("打卡参数:", params)
            http.postJson(SERVER_CHECKIN_URL, params)
            console.log(`打卡${success ? "成功" : "失败"}!`)
            back()
    
            app.launch(ZKER_CHECKIN_PACKAGENAME)
        }
    }