本帖最后由 tfsn20 于 2025-4-9 20:43 编辑
前几天我的手机不小心丢失了,为了能在其他设备上追踪手机位置,我写了一个 auto.js 脚本,并结合高德定位 SDK 和 MQTT 服务实现了一个简单的定位方案。以下是实现过程和具体步骤,供大家参考。
前置条件与手机设置
手机设置:
GPS 必须始终开启。
获取定位时手机需要保持联网状态(移动数据或 Wi-Fi),虽然脚本支持断网重连。
AutoX.js v7 权限配置:
开启无障碍服务。
在设置中关闭省电策略,确保脚本后台运行。
允许始终精准定位。
授予联网控制权限。
技术选型与配置
高德定位 SDK:
使用最新版 AMap_Location_V6.4.9_20241226。
可参考牙叔的文章自行编译:https://blog.csdn.net/snailuncle2/article/details/115270156。
注意:编译时 Java 版本需小于等于 8。
编译完成后放到手机文件夹一个位置,代码里是/storage/emulated/0/脚本/tools/AMap_Location_V6.4.9_20241226.dex。
高德 API:
登录高德开放平台(https://lbs.amap.com/),创建两个应用:
一个绑定 Android 平台,用于手机端定位。
另一个绑定 Web 服务,用于逆地理编码(将经纬度转换为地址)。
获取对应的 API Key 并在脚本中配置。
MQTT 服务:
使用 HiveMQ 官网提供的免费服务器(https://www.hivemq.com/),每月提供 10GB 免费流量。
注意:注册时 QQ 邮箱无法使用,建议更换其他邮箱;连接时使用 MQTT URL(避免使用 WSS 端口)。

由于 HiveMQ 官网加载较慢且登录不便,可选注册 https://www.lddgo.net/network/mqtt 进行在线管理。

脚本特色
脚本运行后,手机会持续监听 MQTT 的指定主题(gaode)。当浏览器发送触发消息时,脚本启动定位并将结果(经纬度及地址)发布到 gaode/location 主题。如果手机断网或关机,MQTT 会发送遗嘱消息到 gaode/will 主题提示状态。脚本支持网络自动重连,确保可靠性。
使用方法
打开浏览器登陆lddgo.net(HiveMQ 也可以)。
连接 HiveMQ 的 WSS 端口,配置连接信息,连接信息可以同步到云端保存,由于主题不支持同步,所以可以订阅 # 主题(全部主题)方便查看消息。
发送 gaode 主题消息触发定位。 注意发送QoS为1的消息,这样autojs至少能在断网重连后接收到一次



代码
// 定位配置
let apiKey = "8xxxxxx9"; // 替换为你的高德 API Key
let webApiKey = "0xxxxxxxxxd"; // 替换为你的高德 Web 服务 API Key
let timeoutLocation = 20 //获取定位超时时间/秒
// MQTT 配置
let mqttHost = "5xxxxxxxb84.s1.eu.hivemq.cloud";
let mqttPort = xxxx;
let clientId = "autojs-client"; // 确保 clientId 唯一
let mqttUrl = "ssl://" + mqttHost + ":" + mqttPort;
let username = "admin"; // 替换为你的 HiveMQ 用户名
let password = "1xxx"; // 替换为你的 HiveMQ 密码
let receiveTopic = "gaode" //接受来自MQTT服务器gaode主题的消息
let sendTopic = "gaode/location"; //向MQTT服务器发送主题为gaode/location的消息
let willTopic = "gaode/will"; // 遗嘱消息主题
// 其他配置
let keepAlive = 120 // 保持活跃间隔/秒
let maxReconnectAttempts = 6; //网络重连次数
let reconnectInterval = 1000 * 10; // 网络重连次数间隔 10秒间隔
// 加载高德地图 Dex 文件
let dexPath = "/storage/emulated/0/脚本/tools/AMap_Location_V6.4.9_20241226.dex";
if (files.exists(dexPath)) {
runtime.loadDex(dexPath);
log("Dex 文件加载成功");
} else {
log("Dex 文件不存在: " + dexPath);
exit();
}
// 引入高德地图相关类
importClass(com.amap.api.location.AMapLocationClient);
importClass(com.amap.api.location.AMapLocationClientOption);
importClass(com.amap.api.location.AMapLocationListener);
// 引入 MQTT 相关类
importClass(org.eclipse.paho.client.mqttv3.MqttClient);
importClass(org.eclipse.paho.client.mqttv3.MqttConnectOptions);
importClass(org.eclipse.paho.client.mqttv3.IMqttMessageListener);
importClass(org.eclipse.paho.client.mqttv3.MqttMessage);
// 引入 Java 时间格式化类
importClass(java.text.SimpleDateFormat);
// 设置高德地图隐私合规和 API Key
try {
AMapLocationClient.updatePrivacyShow(context, true, true);
AMapLocationClient.updatePrivacyAgree(context, true);
log("隐私合规设置完成");
} catch (e) {
log("隐私合规设置失败: " + e);
exit();
}
try {
AMapLocationClient.setApiKey(apiKey);
log("API Key 设置成功");
} catch (e) {
log("API Key 设置失败: " + e);
exit();
}
// 创建 MQTT 客户端
let mqttClient;
try {
mqttClient = new MqttClient(mqttUrl, clientId, null);
let options = new MqttConnectOptions();
options.setUserName(username);
options.setPassword(new java.lang.String(password).toCharArray());
options.setCleanSession(true); // 确保每次连接是新的会话
options.setKeepAliveInterval(keepAlive); // 设置 Keep-Alive 为 keepAlive 秒
// 创建当地时间格式化器
let sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); // ISO 8601 格式,带时区偏移
sdf.setTimeZone(java.util.TimeZone.getDefault()); // 使用设备默认时区
let localTime = sdf.format(new java.util.Date()); // 获取当地时间字符串
// 设置遗嘱消息
let willMessageData = {
clientId: clientId,
status: "offline",
timestamp: localTime
};
let willMessageString = JSON.stringify(willMessageData);
let willPayload = new java.lang.String(willMessageString).getBytes("UTF-8");
options.setWill(willTopic, willPayload, 1, true);
// 设置 MQTT 回调
mqttClient.setCallback({
connectionLost: function (cause) {
log("MQTT 连接断开: " + cause);
attemptReconnect(options);
},
messageArrived: function (topic, message) {
// 此处可以留空,因为我们已经在 subscribe 中处理了消息
},
deliveryComplete: function (token) {
log("消息发送完成: " + token);
}
});
mqttClient.connect(options);
log("MQTT 连接成功");
// 订阅主题并设置消息监听
mqttClient.subscribe(receiveTopic, new IMqttMessageListener({
messageArrived: function (topic, message) {
log("收到消息: " + message.toString());
if (isLocating) {
log("定位进行中,忽略本次请求");
return;
}
// 触发定位
startLocation();
}
}));
} catch (e) {
log("MQTT 连接失败: " + e);
exit();
}
// 自动重连函数
function attemptReconnect(options) {
let reconnectAttempts = 0;
function tryReconnect() {
// 等待一段时间后再次尝试
java.lang.Thread.sleep(reconnectInterval);
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
log(`尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})`);
try {
mqttClient.connect(options);
log("重连成功");
// 重连成功后重新订阅
mqttClient.subscribe(receiveTopic, new IMqttMessageListener({
messageArrived: function (topic, message) {
log("收到消息: " + message.toString());
if (isLocating) {
log("定位进行中,忽略本次请求");
return;
}
startLocation();
}
}));
} catch (e) {
log("重连失败: " + e);
tryReconnect();
}
} else {
log("达到最大重连次数,放弃重连");
exit();
}
}
// 开始第一次重连尝试
tryReconnect();
}
// 定义定位客户端和状态变量
let mLocationClient;
let locationReceived = false;
let timeoutId;
let locationData = null;
let isLocating = false; // 添加定位状态锁
// 初始化定位客户端
function initLocationClient() {
try {
mLocationClient = new AMapLocationClient(context);
mLocationClient.setLocationListener(getAMapLocationListener());
let mLocationOption = new AMapLocationClientOption();
mLocationOption.setLocationPurpose(AMapLocationClientOption.AMapLocationPurpose.SignIn);
mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
mLocationOption.setOnceLocation(true);
mLocationOption.setNeedAddress(true);
mLocationClient.setLocationOption(mLocationOption);
log("定位客户端初始化成功");
} catch (e) {
log("定位客户端初始化失败: " + e);
}
}
// 定位回调监听器
function getAMapLocationListener() {
return new AMapLocationListener({
onLocationChanged: function (location) {
log("回调定位触发");
if (location != null) {
if (location.getErrorCode() == 0) {
log("定位成功");
log("纬度: " + location.getLatitude());
log("经度: " + location.getLongitude());
log("精度: " + location.getAccuracy());
log("速度: " + location.getSpeed());
log("提供者: " + location.getProvider());
locationData = {
纬度: location.getLatitude(),
经度: location.getLongitude(),
准确度: location.getAccuracy(),
速度: location.getSpeed(),
提供商: location.getProvider()
};
// 发送定位信息到 MQTT
sendLocationToMQTT(JSON.stringify(locationData), sendTopic);
} else {
log("定位失败: " + location.getErrorCode() + " - " + location.getErrorInfo());
sendLocationToMQTT("定位失败: " + location.getErrorCode() + " - " + location.getErrorInfo(), sendTopic);
}
} else {
log("Location is null");
}
locationReceived = true; // 标记定位已完成
isLocating = false; // 定位完成后释放锁
if (timeoutId) {
clearTimeout(timeoutId); // 清除超时计时器
}
}
});
}
// 发送定位信息到 MQTT
function sendLocationToMQTT(messages, topic) {
try {
if (mqttClient && mqttClient.isConnected()) {
let message = new MqttMessage(new java.lang.String(messages).getBytes("UTF-8"));
message.setQos(1);
mqttClient.publish(topic, message);
log("信息已发送到 MQTT");
} else {
log("MQTT 客户端未连接,无法发送定位信息");
}
} catch (e) {
log("发送信息失败: " + e);
}
}
// 启动定位函数
function startLocation() {
if (!mLocationClient) {
initLocationClient();
}
if (isLocating) {
log("已在定位中,请稍候");
return;
}
locationReceived = false;
try {
mLocationClient.stopLocation();
mLocationClient.startLocation();
isLocating = true;
log("开始定位");
// 设置超时
timeoutId = setTimeout(() => {
if (!locationReceived) {
log(`定位超时:${timeoutLocation}秒内未获取到结果`);
mLocationClient.stopLocation();
isLocating = false; // 超时后释放锁
}
}, 1000 * timeoutLocation);
// 等待定位结果
function waitForLocation() {
if (locationReceived) {
// 在此处添加对定位数据的进一步处理
// 给定的经纬度
let longitude = locationData.经度;
let latitude = locationData.纬度;
// 构造逆地理编码 API 请求 URL
let url = "https://restapi.amap.com/v3/geocode/regeo?key=" + webApiKey +
"&location=" + longitude + "," + latitude +
"&extensions=all"; // extensions=all 返回详细地址信息
// 发送 HTTP 请求并获取地址信息
try {
let response = http.get(url);
if (response.statusCode == 200) {
let result = response.body.json();
if (result.status == "1") {
// 提取逆地理编码结果
let regeocode = result.regeocode;
let address = regeocode.formatted_address; // 完整地址
let addressComponent = regeocode.addressComponent;
let streetInfo = `${addressComponent.streetNumber.street}${addressComponent.streetNumber.number}位于定位的${addressComponent.streetNumber.direction}方向${addressComponent.streetNumber.distance}米远`;
// 输出详细信息
log("逆地理编码成功:");
log("完整地址: " + address);
log(`街道信息:${streetInfo}`);
sendLocationToMQTT(`完整地址: ${address}\n街道信息: ${streetInfo}`, sendTopic)
} else {
log("逆地理编码失败: " + result.info);
}
} else {
log("请求失败,状态码: " + response.statusCode);
}
} catch (e) {
log("请求出错: " + e);
}
log("定位完成");
} else {
setTimeout(waitForLocation, 1000);
}
}
setTimeout(waitForLocation, 1000);
} catch (e) {
log("定位启动失败: " + e);
isLocating = false; // 定位启动失败释放锁
}
}
// 初始化定位客户端
initLocationClient();
// 脚本退出时清理资源
events.on("exit", () => {
log("脚本退出");
if (mLocationClient) {
mLocationClient.stopLocation();
mLocationClient.onDestroy();
}
if (mqttClient && mqttClient.isConnected()) {
mqttClient.disconnect();
}
log("清理完成")
});
// 保持脚本运行
setInterval(() => { }, 1000);