牙叔教程 简单易懂
服务端负责什么
负责监听客户端发送的消息, 然后机器人发送关键词对应的答案
服务端怎么创建
用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官方文档
使用这个命令
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,
去限制可以看这个教程
如果不想去限制, 也可以通过图色, 或者用其他自动化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",
});
}
总结
以上是服务端代码的一些关键点, 具体的代码请看代码仓库
环境
设备: 小米11pro
Android版本: 12
Autojs版本: 9.3.9
名人名言
思路是最重要的, 其他的百度, bing, stackoverflow, github, 安卓文档, autojs文档, 最后才是群里问问 --- 牙叔教程
声明
部分内容来自网络 本教程仅用于学习, 禁止用于其他用途