autojs通知栏机器人-服务端

649 阅读5分钟

牙叔教程 简单易懂

服务端负责什么

负责监听客户端发送的消息, 然后机器人发送关键词对应的答案

服务端怎么创建

用autojs的nodejs引擎, 叫引擎合适吗? 反正就是autojs支持nodejs的意思.

服务端依赖如下

  "dependencies": {
    "@autojs/types-pro8": "^8.8.0",
    "archiver": "^5.3.1",
    "js-yaml": "^4.1.0",
    "koa": "^2.13.4",
    "koa-body": "^6.0.1",
    "koa-bodyparser": "^4.3.0",
    "koa-router": "^12.0.0",
    "node-schedule": "^2.1.0"
  }

我用的是koa, 为什么用koa呢?

为什么不用express, nest, egg, 等等其他的呢?

会哪个用哪个, 哪个方便用哪个, 哪个能用用哪个, 都可以用;

只要能实现你的目的, 解决你的问题

koa最短代码

"nodejs";
const Koa = require("koa");
const router = require("koa-router")();
var bodyParser = require("koa-bodyparser");
var app = new Koa();
app.use(bodyParser());
router.get("/hello", async (ctx) => {
  console.log("hello");
  ctx.body = "router: hello world";
});
router.post("/group-msg", async (ctx) => {
  return (ctx.body = {
    code: 200,
    message: "成功",
    data: data,
  });
});
// 调用router.routes()来组装匹配好的路由,返回一个合并好的中间件
// 调用router.allowedMethods()获得一个中间件,当发送了不符合的请求时,会返回 `405 Method Not Allowed` 或 `501 Not Implemented`
app.use(router.routes());
app.use(router.allowedMethods({}));
let localIp = getLocalIP();
app.listen(port, () => {
  console.log("koa is listening in " + localIp + ":" + port);
});

autojs怎么安装nodejs的模块依赖

请看autojs官方文档

pro.autojs.org/docs/zh/v9/…

使用这个命令

npm i --no-bin-links

在哪里执行这个命令呢?

在package.json这个文件夹下执行该命令, 点击文件名右侧的圆规, 就可以看到终端选项, 在终端中执行即可

文件同步

这里的同步指的是vscode中的文件, 同步到手机的 /sdcard/脚本/.remote/

node_modules 这个文件夹文件一般都特别多, 因此, 我们不要同步这个文件夹;

在vscode中, 创建文件 .autojs.sync.ignore, 里面写要忽略的文件;

凡在这个文件里写的文件, 都不会同步到手机中

/build
/node_modules
/references
/.git
/.gitignore
/.vscode
/.editorconfig
/.eslintrc.json
/牙叔教程q群.png
/牙叔教程公众号.jpg
/build.zip

有一点要明确, 手机上要安装node模块依赖, vscode中也要装node模块依赖,

但是我们不会同步 node_modules 这个文件夹

文件保存到手机

这里的保存指的是 vscode 中的文件, 保存到手机的 /sdcard/脚本/

project.json 中有一个字段

"ignore": [".vscode", "build", "node_modules", "references", "牙叔教程公众号.jpg", "牙叔教程q群.png", "build.zip"]

凡在这个字段中添加的文件, 都不会保存到手机中;

同步是同步, 保存是保存, 大家要区分这两个概念

服务端流程图

这里我用了两个引擎

  • Rhino
  • Nodejs

只用Node可以吗? 当然可以;

那为什么我要用两个呢?

因为一开始我就用的rhino, 认为rhino比较好写;

现在想想, 还是只用一个引擎比较简单, 就不用来回通信了;

那为什没改成一个Node引擎呢?

我的原则是能用就行, 如无必要, 我才不改

服务端的IP

有时候停电了, 手机IP就变了, 我还得翻阅手机看现在的IP是多少, 因此写了个获取本机IP

/**
 * 获取本机IP
 * @return {String} 返回本机IP
 */
function getLocalIP() {
  const ifaces = os.networkInterfaces();
  let localIp = "";
  for (let dev in ifaces) {
    for (let j = 0; j < ifaces[dev].length; j++) {
      if (ifaces[dev][j].family === "IPv4") {
        localIp = ifaces[dev][j].address;
        console.log("localIp: " + localIp);
        if (localIp === "127.0.0.1") {
          continue;
        } else {
          return localIp;
        }
      }
    }
  }
  if (!localIp) {
    throw new Error("无法获取本机IP");
  }
  return localIp;
}

显示消息队列

有时候想要知道目前队列中有哪些消息, 因此增加了一个悬浮窗, 显示当前的消息队列

function FloatingWindow(queue) {
  let that = this;
  threads.start(function () {
    that.window = floaty.rawWindow(
      <vertical bg="#99263238">
        <text id="queueContent" textColor="#ffffff"></text>
        <list id="list" h="300dp" alpha="1">
          <horizontal padding="16">
            <text text="{{this.groupName}} --- " textColor="#ffffff"></text>
            <text text="{{this.message}}" textColor="#ffffff"></text>
          </horizontal>
        </list>
      </vertical>
    );
    that.window.setTouchable(false);
    let listView = that.window.list;
    ui.run(function () {
      listView.setDataSource(queue);
    });
    events.on("exit", function () {
      if (that.window) {
        that.window.close();
        that.window = null;
        console.log("释放资源: window");
      }
    });
  });
}

更新代码

脚本打包以后, 如果要更改代码, 还要重新打包, 步骤麻烦, 因此写了热更新

读取文件和文件夹

// 这是nodejs代码
function walk(path) {
  let children = fs.readdirSync(path);
  let dirs = [];
  let files = [];
  children.forEach(function (item, index) {
    let fPath = path + "/" + item;
    let stat = fs.statSync(fPath);
    if (stat.isDirectory() === true) {
      dirs.push(fPath);
    }
    if (stat.isFile() === true) {
      files.push(fPath);
    }
  });
  dirs = dirs.map((item) => item.replace("./", ""));
  files = files.map((item) => item.replace("./", ""));
  dirs = dirs.filter((item) => config.ignore.indexOf(item) < 0);
  files = files.filter((item) => config.ignore.indexOf(item) < 0);
  return {
    dirs: dirs,
    files: files,
  };
}

压缩调试好的脚本, 这里用的是 archive

// 这是nodejs代码
const output = fs.createWriteStream("build.zip");
const archive = archiver("zip", { zlib: { level: 9 } });
archive.on("error", function (err) {
  throw err;
});

archive.pipe(output);
dirs.forEach((item) => {
  archive.directory(item, item);
});
files.forEach((item) => {
  archive.file(item, { name: item });
});
archive.finalize();

然后更新的时候就下载这个zip, 再解压文件

// 这是Rhino代码
function decompressFile(src, destination) {
  $files.remove(destination);
  files.createWithDirs(destination);
  $zip.unzip(src, destination);
}

来回倒腾引擎, 是不是有点晕呀

脚本更新链接

你下载文件得有地址吧, 我的链接放在了语雀, 语雀是一个写文档的地方;

文档里面就只写了一个链接

你如果下载不频繁, 就可以放语雀,

如果你下载的频繁, 就不可以, 因为语雀会拉黑你的IP

语雀内容

解析下载地址

function getDownloadLink() {
  let remoteUrl = "https://www.yuque.com/yashujs/di90k3/gp5w5cmfqd76pfy1";
  let r = http.get(remoteUrl, {
    headers: {
      authority: "<authority>",
      cookie: "<cookie>",
      "sec-fetch-user": "<sec-fetch-user>",
      "upgrade-insecure-requests": "<upgrade-insecure-requests>",
      "User-Agent": "apifox/1.0.0 (https://www.apifox.cn)",
    },
  });
  let data = r.body.string();
  let filterData = data.match(/-{9}.+?-{9}/)[0];
  filterData = filterData.replace(/-{9}/g, "");
  filterData = decodeURIComponent(filterData);
  return filterData;
}

上面这个代码我是用apifox生成的, 然后稍微修改了一下就能用了

下载文件

function downloadFile(url) {
  let r = http.post(url, {
    headers: {
      "Content-Type": "application/json",
    },
    fileName: "build.zip",
  });
  let data = r.body;
  let filePath = config.downloadFilePath;
  files.createWithDirs(filePath);
  files.writeBytes(filePath, data.bytes());
  return filePath;
}

配置文件

代码有很多可以配置的, 我们把它抽离到config.js中,

这个config.js文件, 有时候是Rhino加载, 有时候是Node加载, 因此我们要做环境判断;

比如判断 files 是否未定义, 未定义就是Node环境, 定义了就是Rhino环境

if (typeof files == "undefined") {
  var files = {
    exists: function (path) {
      var fs = require("fs");
      return fs.existsSync && fs.existsSync(path); // 用于判断文件是否存在
    },
  };
  remoteUpdate = files.exists("/sdcard/脚本/remoteUpdate.txt");
} else {
  console.log("files已定义");
  remoteUpdate = files.exists("/sdcard/脚本/remoteUpdate.txt");
}

我只抽离了一部分配置, 还有别的也可以抽离, 我没动, 能不动就不动

定时发广告

这里的定时器一开始使用的是 $work_manager,

但是他不太准, 因此改成了增加一个多线程, 来把消息推入到队列中

threads.start(function () {
  // 定时发广告
  advertise();
});

advertise

function advertise() {
  let 限制群名列表 = config.限制群名列表;
  let groupIds = config.groupIds;
  let advertising = config.advertising;
  let clocksWithFlag = config.clocks.map((clock) => {
    return { hour: clock, hasPushed: false };
  });
  setInterval(function () {
    let nowHour = new Date().getHours();
    log("nowHour: " + nowHour);
    let reset = false;
    if (nowHour == 2 && !reset) {
      clocksWithFlag.map((clock) => {
        clock.hasPushed = false;
      });
      reset = true;
    }

    var len = clocksWithFlag.length;
    for (var i = 0; i < len; i++) {
      var clock = clocksWithFlag[i];
      if (clock.hour == nowHour && !clock.hasPushed) {
        var groupIdsLen = groupIds.length;
        for (var j = 0; j < groupIdsLen; j++) {
          let groupId = groupIds[j];
          let groupName = 限制群名列表[j];
          let message = advertising;
          queue.push({
            groupId: groupId,
            groupName: groupName,
            message: message,
          });
        }
        clock.hasPushed = true;
      }
    }
  }, config.advertisingIntervalCheckTime);
}

群聊肯定更要限制, 你的机器人只能在自己的群发言, 去别的群发言, 除非你的机器人对别的群有益处, 否则就等着挨踢, 在自己的群, 机器人也不能发言太频繁, 我看到某些群机器人发言, 我就屏蔽了, 我不喜欢机器人在群里频繁发言

QQ发消息

function sendGroupMsg(data, floatingWindow) {
  if (!data.groupId || !data.groupName || !data.message) {
    log("参数异常" + JSON.stringify(data));
    return;
  }
  结束运行("QQ");
  openQqGroup(data.groupId);
  click发消息按钮(floatingWindow);
  enterMsg(data.message, floatingWindow);
  clickSendButton(floatingWindow);
  back();
  sleep(300);
  back();
  sleep(300);
}

这里用了无障碍来操作QQ,

去限制可以看这个教程

mp.weixin.qq.com/s/C05dH-PPm…

如果不想去限制, 也可以通过图色, 或者用其他自动化app代替

打开QQ群

在QQ发消息的代码中, 有一个 结束运行QQ, 为什么要结束他呢?

因为我要用 startActivity 打开QQ群页面, 如果打开的次数多了, 就打不开了;

只有停止了QQ, 才能再次打开QQ群

function openQqGroup(groupId) {
  let data = "mqqapi://card/show_pslcard?src_type=internal&version=1&card_type=group&uin=" + groupId;
  app.startActivity({
    action: "android.intent.action.VIEW",
    data: data,
    packageName: "com.tencent.mobileqq",
  });
}

总结

以上是服务端代码的一些关键点, 具体的代码请看代码仓库

gitee.com/yashujs/aut…

环境

设备: 小米11pro
Android版本: 12
Autojs版本: 9.3.9

名人名言

思路是最重要的, 其他的百度, bing, stackoverflow, github, 安卓文档, autojs文档, 最后才是群里问问 --- 牙叔教程

声明

部分内容来自网络 本教程仅用于学习, 禁止用于其他用途