diff --git a/py/jd_rabbit_账密.py b/py/jd_rabbit_账密.py
new file mode 100644
index 0000000..5ae5de7
--- /dev/null
+++ b/py/jd_rabbit_账密.py
@@ -0,0 +1,645 @@
+# [author: authook]
+# [title: jd_账密登陆]
+# [create_at: 2023-09-19 15:29:17]
+# [version: 2.3]
+# [pin: true]
+# [price: 0]
+# [rule: ^账密登录$]
+# [rule: ^账密登陆$]
+# [rule: ^账密刷新$]
+# [rule: ^账密强制刷新$]
+# [rule: ^账密管理$]
+# [priority: 6666666]优先级
+# [admin: false]
+# [service: authook]
+# [icon: https://bbs.autman.cn/assets/files/2024-05-16/1715842374-826456-2584e965be6ed12773865dbb8e29a449.png]
+# [description: jd_账密登陆插件 兼容兔子,pro等框架指令:账密登录,账密登录,账密管理 配置:在插件配参中配置,权限:需开启qls,jdNotify权限注意⚠️账密刷新请勿手动发送,否则会将ck绑定至管理员账号上!!!(插件已自动内置定时刷新账密)]
+
+# [param: {"required":true,"key":"jd.rabbit_host","placeholder":"http://192.168.1.1:81","name":"【rabbit】登陆地址:","desc":"登陆地址,例:http://192.168.3.22"}]
+# [param: {"required":true,"key":"jd.pro_host","placeholder":"http://192.168.1.1:81","name":"【pro】登陆地址:","desc":"pro登陆地址"}]
+# [param: {"required":true,"key":"jd.login_ql_name","placeholder":"ql容器名称","name":"【ql】容器名称","desc":"ql容器名称"}]
+# [param: {"required":true,"key":"jd.login_type","placeholder":"登陆框架","name":"登陆框架","desc":"pro框架请输入pro rabbit兔子框架请输入rabbit"}]
+
+# =======================
+# 项目:jd_账密登陆.py
+# 构建时间:2025-02-10 20:52:29
+import datetime
+from email import message
+import json
+import logging
+import math
+import os
+import random
+import re
+import time
+import traceback
+import inspect
+from base64 import b64encode
+from urllib.parse import quote, urlparse
+
+import colorlog
+import middleware
+import requests
+from Crypto.Cipher import AES
+from Crypto.Util.Padding import pad
+
+
+class TOOL:
+ def __init__(self):
+ self.plugin_pre = "rabbit_zm"
+ self.remark = ""
+ self.platform_arr = self.platformArr()
+ # 将日期和时间格式化为字符串
+ self.cut_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ self.logger = logging.getLogger()
+ self.color_formatter = colorlog.ColoredFormatter(
+ '%(log_color)s%(levelname)s: %(message)s',
+ log_colors={
+ 'DEBUG': 'cyan',
+ 'INFO': 'green',
+ 'WARNING': 'yellow',
+ 'ERROR': 'red',
+ 'CRITICAL': 'red,bg_white',
+ }
+ )
+ self.logger = self._configure_logger()
+
+ def _configure_logger(self):
+ """配置并返回一个带颜色的 logger 对象"""
+ logger = logging.getLogger(self.plugin_pre)
+ logger.setLevel(logging.DEBUG) # 设置日志默认最低级别
+
+ # 检查是否已添加过控制台处理器,避免重复添加
+ if not any(isinstance(handler, logging.StreamHandler) for handler in logger.handlers):
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.DEBUG) # 控制台日志最低级别
+ console_handler.setFormatter(self.color_formatter)
+ logger.addHandler(console_handler)
+
+ return logger
+
+ def log(self, level, message):
+ """通用日志方法"""
+ if level == logging.INFO:
+ self.logger.info(message)
+ elif level == logging.ERROR:
+ self.logger.error(message)
+ elif level == logging.WARNING:
+ self.logger.warning(message)
+ elif level == logging.DEBUG:
+ self.logger.debug(message)
+ elif level == logging.CRITICAL:
+ self.logger.critical(message)
+
+ def log_info(self, message):
+ self.log(logging.INFO, message)
+
+ def log_err(self, message):
+ self.log(logging.ERROR, message)
+
+ def log_warn(self, message):
+ self.log(logging.WARNING, message)
+
+ def platformArr(self):
+ plat_arr = []
+ for p in ["qq", "wx", "wb", "qb", "bt", "tg"]:
+ t = {"platform": f"{p}", "imType": p}
+ plat_arr.append(t)
+ return plat_arr
+
+ def pushMsg(self, userId, chatId, imType, title, content):
+ # 获取当前日期和时间
+ now = datetime.datetime.now()
+ # 将日期和时间格式化为字符串
+ formatted_datetime = now.strftime('%Y-%m-%d %H:%M:%S')
+ self.remark = ""
+ return middleware.push(imType, chatId, userId, title, f"{formatted_datetime}\n{content}")
+
+ def pushGroup(self, imtype, groupCode, content):
+ self.pushMsg(0, groupCode, imtype, "", content)
+
+ def autman_host(self):
+ file_path = inspect.getframeinfo(inspect.currentframe()).filename
+ dir_path = os.path.dirname(os.path.abspath(file_path))
+ setConfPath = dir_path.split("develop/scripts")[0] + "/middleware.py"
+ try:
+ os.setuid(0) # 强制转为root 前提是autman账户以root用户启用
+ with open(setConfPath, "r", encoding="utf-8") as f:
+ data = f.read()
+ lines = data.splitlines()
+ for line in lines:
+ if "url =" in line:
+ res = line.split('"')
+ url = res[1][0:-5]
+ self.log_info(url)
+ return url
+ except Exception as e:
+ self.log_info(f"❌autman_host error {e}")
+
+ def replyMsg(self, content):
+ return sender.reply(content)
+
+ def pushMaster(self, content):
+ arr = ["qq", "wx", "tg", "qb", 'wb']
+ for p in arr:
+ masters = sender.bucketGet(p, "masters")
+ if masters:
+ masters_arr = masters.split(",") if "," in masters else masters.split("&")
+ for m in masters_arr:
+ middleware.push(p, "", m, "", content)
+
+ def qls(self):
+ qls = []
+ ql_id_arr = sender.bucketAllKeys("qls")
+ if not ql_id_arr:
+ return []
+ for ql_id in ql_id_arr:
+ value = sender.bucketGet("qls", ql_id)
+ if value:
+ qls.append(json.loads(value))
+ return qls
+
+ def autman_host(self):
+ file_path = inspect.getframeinfo(inspect.currentframe()).filename
+ dir_path = os.path.dirname(os.path.abspath(file_path))
+ setConfPath = dir_path.split("develop/scripts")[0] + "/middleware.py"
+ try:
+ os.setuid(0) # 强制转为root 前提是autman账户以root用户启用
+ with open(setConfPath, "r", encoding="utf-8") as f:
+ data = f.read()
+ lines = data.splitlines()
+ for line in lines:
+ if "url =" in line:
+ res = line.split('"')
+ url = res[1][0:-5]
+ self.log_info(url)
+ return url
+ except Exception as e:
+ self.log_info(f"❌autman_host error {e}")
+
+
+class QL:
+ def __init__(self, ql):
+ self.host = ql["host"]
+ self.client_id = ql["client_id"]
+ self.client_secret = ql["client_secret"]
+ self.token = self.get_token()
+ self.name = ql['name']
+ self.content = ""
+ self.version = self.system()
+
+ def gettimestamp(self):
+ return str(int(time.time() * 1000))
+
+ def get_token(self):
+ url = f"{self.host}/open/auth/token?client_id={self.client_id}&client_secret={self.client_secret}"
+ print(url)
+ headers = {
+ 'Content-Type': 'application/json',
+ 'Accept': '*/*',
+ 'Cache-Control': 'no-cache',
+ 'Host': "",
+ 'Connection': 'keep-alive'
+ }
+ host = urlparse(url).netloc
+ if ":" in host:
+ host = host.split(":")[0]
+ headers['Host'] = host
+ try:
+ response = requests.get(url, timeout=10, headers=headers)
+ res = response.json()
+ if response.status_code == 200 and res["code"] == 200:
+ return res["data"]["token"]
+ else:
+ return None
+ except Exception as e:
+ return None
+
+ def Api(self, api, apd, method, payload):
+ url = f"{self.host}/open/{api}/{apd}"
+ headers = {
+ "Content-Type": "application/json;charset=UTF-8",
+ "Authorization": f"Bearer {self.token}",
+ }
+ try:
+ response = requests.request(method, url, headers=headers, data=payload, timeout=5)
+ return response.json()
+ except Exception as e:
+ return None
+
+ # 环境变量
+ def envGet(self, keyword):
+ return self.Api('envs', f"?searchValue={keyword}&t={self.gettimestamp()}", 'GET', {})
+
+ def envSet(self, env):
+ return self.Api('envs', f"?t={self.gettimestamp()}", 'POST', env)
+
+ def envUpdate(self, env):
+ return self.Api('envs', f"?t={self.gettimestamp()}", 'PUT', env)
+
+ def envEnable(self, ids):
+ return self.Api('envs', f"/enable?t={self.gettimestamp()}", 'PUT', json.dumps(ids))
+
+ def envDisable(self, ids):
+ return self.Api('envs', f"/disable?t={self.gettimestamp()}", 'PUT', json.dumps(ids))
+
+ def envDel(self, ids):
+ return self.Api('envs', f"?t={self.gettimestamp()}", 'delete', json.dumps(ids))
+
+ def envMove(self, id, data):
+ return self.Api('envs', f"/{id}/move?t={self.gettimestamp()}", 'PUT', json.dumps(data))
+
+ # 定时任务
+ def cronGet(self, keyword):
+ return self.Api('crons', f"?searchValue={keyword}&t={self.gettimestamp()}", 'GET', None)
+
+ def cronRun(self, ids):
+ return self.Api('crons', f"run?t={self.gettimestamp()}", 'PUT', ids)
+
+ def cronStop(self, ids):
+ return self.Api('crons', f"stop?t={self.gettimestamp()}", 'PUT', ids)
+
+ def cronEnable(self, ids):
+ return self.Api('crons', f"enable?t={self.gettimestamp()}", 'PUT', ids)
+
+ def cronDisable(self, ids):
+ return self.Api('crons', f"disable?t={self.gettimestamp()}", 'PUT', ids)
+
+ def cronDetail(self, cronId):
+ return self.Api('crons', f"{cronId}?t={self.gettimestamp()}", 'GET', None)
+
+ def cronLogs(self, cronId):
+ return self.Api('crons', f"{cronId}/log?t={self.gettimestamp()}", 'GET', None)
+
+ def cronLog(self, cronId):
+ return self.Api('crons', f"{cronId}/log?t={self.gettimestamp()}", 'GET', None)
+
+ # 配置文件
+ def configsGet(self, name):
+ if self.version < "2.17":
+ return self.Api('configs', f"{name}?t={self.gettimestamp()}", 'GET', None)
+ else:
+ return self.Api('configs/detail', f"?path={name}&t={self.gettimestamp()}", 'GET', None)
+
+ def configsSet(self, data):
+ return self.Api('configs', f"save?t={self.gettimestamp()}", 'POST', data)
+
+ # 订阅任务
+ def subsGet(self, keyword):
+ return self.Api('subscriptions', f"?searchValue={keyword}&t={self.gettimestamp()}", 'GET', None)
+
+ def subsSet(self, data):
+ return self.Api('subscriptions', f"save?t={self.gettimestamp()}", 'PUT', data)
+
+ def subsRun(self, ids):
+ return self.Api('subscriptions', f"run?t={self.gettimestamp()}", 'PUT', ids)
+
+ def subsEnable(self, ids):
+ return self.Api('subscriptions', f"enable?t={self.gettimestamp()}", 'PUT', ids)
+
+ def subsDisable(self, ids):
+ return self.Api('subscriptions', f"disable?t={self.gettimestamp()}", 'PUT', ids)
+
+ def subsLog(self, subId):
+ return self.Api('subscriptions', f"{subId}/log?t={self.gettimestamp()}", 'GET', None)
+
+ def system(self):
+ url = f"{self.host}/open/system?t={self.gettimestamp()}"
+ headers = {
+ "Content-Type": "application/json;charset=UTF-8",
+ }
+ try:
+ res = requests.get(url)
+ if res.status_code == 200:
+ rj = res.json()
+ return rj['data']['version']
+ except Exception as e:
+ return 2.16
+
+
+class ZM_RABBIT():
+ def __init__(self):
+ self.pins = self.get_pins()
+ self.conf = self.get_conf()
+ self.init()
+ self.s = requests.session()
+ pass
+
+ def hide_phone(self, text):
+ if not text:
+ return text
+ if len(text) != 11:
+ return text
+ return re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', text)
+
+ def check_ck_status(self, ck):
+ headers = {
+ "Cookie": ck,
+ "referer": "https://h5.m.jd.com/",
+ "User-Agent": "jdapp;iPhone;10.1.2;15.0;network/wifi;Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148;supportJDSHWK/1",
+ }
+
+ response = requests.get('https://plogin.m.jd.com/cgi-bin/ml/islogin', headers=headers)
+ data = response.json()
+ return data['islogin'] == "1" if 'islogin' in data else False
+
+ def en_pwd(self, pwd):
+ key = b"wn:_/imCQF;rBNS{V=|kML[(+$%Tjgy)"
+ iv = b"zA#f.x>jN!h-0[{$"
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ encrypted = cipher.encrypt(pad(pwd.encode(), AES.block_size))
+ return b64encode(encrypted).decode()
+
+ def get_conf(self):
+ self.conf = {
+ "paid": "n",
+ "fee": 0,
+ "qr_code": "",
+ "host": "",
+ "ql_name": None
+ }
+ if middleware.bucketGet("otto", "rabbit_pro_paid"):
+ self.conf["paid"] = middleware.bucketGet("otto", "rabbit_pro_paid")
+ if middleware.bucketGet("otto", "rabbit_pro_fee"):
+ self.conf['fee'] = middleware.bucketGet("otto", "rabbit_pro_fee")
+ if middleware.bucketGet("otto", "rabbit_pro_qr_code"):
+ self.conf['qr_code'] = middleware.bucketGet("otto", "rabbit_pro_qr_code")
+ if not middleware.bucketGet("otto", "rabbit_pro_host"):
+ tool.pushMaster("未配置rabbit登录地址,请先去【配参】配置")
+ exit()
+ self.conf['host'] = middleware.bucketGet("otto", "rabbit_pro_host")
+ if not middleware.bucketGet("otto", "rabbit_ql_name"):
+ tool.pushMaster("未配置rabbit登陆容器名称,请先去【配参】配置")
+ exit()
+ self.conf['ql_name'] = middleware.bucketGet("otto", "rabbit_ql_name")
+ return self.conf
+
+ def init(self):
+ res = requests.get(self.conf['host'] + "/api/Config")
+ tool.log_info(res.text)
+ if res.status_code == 200:
+ rj = res.json()
+ if rj['success']:
+ self.conf['container_id'] = rj['data']['list'][0]['container_id']
+ self.conf['container_name'] = rj['data']['list'][0]['container_name']
+ self.conf['autocount'] = rj['data']['autocount']
+ return self.conf
+ tool.replyMsg(rj['message'] + ",请完善rabbitPro配置")
+ exit()
+
+ def get_pins(self):
+ return sender.bucketKeys("pin" + imType.upper(), userId)
+
+ def account_list(self):
+ acc_list = []
+ acc_key = set()
+ if self.pins and len(self.pins) > 0:
+ for pin in self.pins:
+ acc_str = sender.bucketGet("", pin)
+ if acc_str:
+ acc = {
+ "username": acc_str.split("#")[0],
+ "pwd": acc_str.split("#")[1],
+ "ck": acc_str.split("#")[2],
+ }
+ if acc_str.split("#")[0] not in acc_key:
+ acc_list.append(acc)
+ acc_key.add(acc_str.split("#")[0])
+
+ return acc_list
+
+ def login_init(self, mobile):
+ headers = {
+ "Content-Type": "application/json",
+ "User_Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
+ }
+ data = {"account": mobile, "container_id": self.conf['container_id']}
+ url = self.conf['host'] + "/pwd/init"
+ res = self.s.post(url, headers=headers, data=json.dumps(data))
+ tool.log_info(f"{mobile}: login_init res {res.text}")
+ if res.status_code == 200:
+ rj = res.json()
+ if not rj['success']:
+ tool.replyMsg(f"[{self.hide_phone(mobile)}]登陆初始化失败:{rj['message']}")
+ return
+ if rj['data']['status'] == 505:
+ return True
+ else:
+ tool.replyMsg(f"f{rj['message']}")
+ return False
+
+ pass
+
+ def login(self, mobile, pwd):
+ headers = {
+ "Content-Type": "application/json",
+ "User_Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
+ }
+ data = {"account": mobile, "pwd": self.en_pwd(pwd), "container_id": self.conf['container_id']}
+ res = self.s.post(f"{self.conf['host']}/pwd/login", headers=headers, data=json.dumps(data))
+ tool.log_info(f"{mobile}: login res {res.text}")
+ if res.status_code == 200:
+ rj = res.json()
+ if rj['code'] == 601:
+ tool.replyMsg(f"[{self.hide_phone(mobile)}]账号需要点击验证之后,重新登陆\n{rj['RiskUrl']}")
+ return False, None
+ if rj['code'] == 200:
+ tool.replyMsg(f"【登陆账户】:{self.hide_phone(mobile)}\n【JD账户】:{rj['pin']}\n【登陆结果】:{rj['message']}")
+ value_json = {
+ "mobile": mobile,
+ "pwd": pwd
+ }
+ sender.bucketSet("rabbit_zm_acc", rj['pin'], json.dumps(value_json))
+ return True, rj['pin']
+ elif rj['code'] == 503:
+ tool.replyMsg(f"[{self.hide_phone(mobile)}]{rj['message']}")
+ return False, None
+ elif rj['code'] == 555:
+ # 需要验证
+ riskUrl = rj['RiskUrl']
+ tool.replyMsg(f"[{self.hide_phone(mobile)}]请手动点击验证链接,然后再次返回登陆:{riskUrl}")
+ return False, None
+ else:
+ tool.replyMsg(f"[{self.hide_phone(mobile)}]登陆异常:{rj['message']}")
+ return False, None
+
+ def after_login(self, pin):
+ time.sleep(random.randint(3,5))
+ sender.bucketSet('pin' + imType.upper(), pin, userId)
+ qls = tool.qls()
+ if not qls or len(qls) == 0:
+ tool.pushMaster("未配置青龙容器,无法同步更新容器ck")
+ exit()
+ default_ql = list(filter(lambda item: self.conf['ql_name'] in item['name'],qls))
+ if not default_ql:
+ tool.pushMaster(f"容器【{self.conf['ql_name']}】,不存在,请重新配置")
+ return
+ tql = QL(default_ql[0])
+ if not tql.token:
+ return
+ res = tql.envGet("JD_COOKIE")
+ if res['code'] != 200 or len(res['data']) == 0:
+ return
+ if len(res['data']) <= 0:
+ return
+ cookie = list(filter(lambda item: pin in item['value'], res['data']))
+ if not cookie:
+ return
+ if cookie[0]['status'] == 1:
+ return
+ if not self.check_ck_status(cookie[0]['value']):
+ return
+ ck = cookie[0]['value']
+ pt_key = re.findall(r"pt_key=(.*?);", ck)[0]
+ val = {
+ "ID": pin,
+ "Pet": False,
+ "Fruit": False,
+ "DreamFactory": False,
+ "Note": "",
+ "PtKey": pt_key,
+ "AssetCron": "",
+ "PushPlus": "",
+ "LoginedAt": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ "ClientID": tql.client_id
+ }
+ sender.bucketSet('jdNotify', pin, json.dumps(val))
+
+ for ql in qls:
+ if self.conf['ql_name'] in ql['name']:
+ continue
+ tql = QL(ql)
+ if not tql.token:
+ continue
+ res = tql.envGet("JD_COOKIE")
+ if res['code'] != 200 or len(res['data']) == 0:
+ continue
+ if len(res['data']) <= 0:
+ continue
+ cookie = list(filter(lambda item: pin in item['value'], res['data']))
+ if not cookie:
+ continue
+ ck = cookie[0]['value']
+ id = cookie[0].get('id', cookie[0].get("_id"))
+ remarks = cookie[0].get('remarks', pin)
+ if ck:
+ updateEnv = {
+ "name": "JD_COOKIE",
+ "value": ck,
+ "remarks": remarks,
+ "id": id,
+ }
+ res = tql.envUpdate(json.dumps(updateEnv))
+ tool.log_info(f"update res {res}")
+ res = tql.envEnable([id])
+ tool.log_info(f"envEnable res {res}")
+
+ def refresh_account(self):
+ need = 0
+ success = 0
+ failed = 0
+ failed_user = []
+ tool.log_info(f"[rabbit]账密刷新开始......")
+ acc_map = sender.bucketAll("rabbit_zm_acc")
+ if not acc_map:
+ sender.reply("暂无账密数据,已退出")
+ # 查询pin对应的ck是否有效
+ for pin, accStr in acc_map.items():
+ jdStr = sender.bucketGet("jdNotify", pin)
+ if not jdStr:
+ continue
+ jd_json = json.loads(jdStr)
+ ck = f"pt_pin={pin}; pt_key={jd_json['PtKey']}"
+ if self.check_ck_status(ck):
+ tool.log_info(f"账号【{pin}】有效✅,跳过刷新")
+ continue
+ need += 1
+ acc_json = json.loads(accStr)
+ refresh_res = self.run_login(acc_json['mobile'], acc_json['pwd'])
+ if not refresh_res:
+ failed += 1
+ failed_user.append(f"{quote(pin)}[{self.hide_phone(acc_json['mobile'])}]")
+ else:
+ success += 1
+ content = f"[rabbit]账密刷新结束"
+ if need > 0:
+ content += f"\n本次需要刷新:{need}个"
+ content += f"\n本次刷新成功:{success}个🎉"
+ if failed_user:
+ content += f"\n本次刷新失败:{failed}个😭"
+ s = "\n".join(failed_user)
+ content += f"\n刷新失败用户:{s}"
+ else:
+ content += f"\n本次所有账户有效,无需刷新🎉"
+ tool.pushMaster(content)
+
+ def account_manage(self):
+ acc_map = sender.bucketAll("rabbit_zm_acc")
+ if not acc_map:
+ tool.replyMsg("暂无账密记录,请先登录")
+ exit()
+ content = f"账密列表如下:"
+ no = 0
+ for pin,accStr in acc_map.items():
+ acc_json = json.loads(accStr)
+ no +=1
+ content += f"\n{no}、{quote(pin)}[{self.hide_phone(acc_json['mobile'])}]"
+ tool.replyMsg(content)
+
+
+ def run_login(self, mobile, pwd):
+ if not self.login_init(mobile):
+ return False
+ login_res, pin = self.login(mobile, pwd)
+ if not login_res:
+ return False
+ self.after_login(pin)
+ return True
+
+
+if __name__ == "__main__":
+ sender = middleware.Sender(middleware.getSenderID())
+ plugin_ver = sender.getPluginVersion()
+ plugin_name = sender.getPluginName()
+ msg = sender.getMessage().strip('"')
+ imType = "fake" if sender.getImtype() == "cron" else sender.getImtype()
+ chatId = sender.getChatID()
+ userId = sender.getUserID()
+ isAdmin = sender.isAdmin()
+ username = sender.getUserName()
+ tool = TOOL()
+ #### 定时任务 #####
+ cron = middleware.Cron(None)
+ crons = list(filter(lambda item: "兔子账密刷新" in item['cmd'],cron.getCrons()))
+ if not crons:
+ tool.log_info(f"【兔子账密刷新】定时任务不存在,重新添加")
+ id = cron.addCron("0 0,3,5 * * *","兔子账密刷新",True,False,"兔子账密刷新","","","")
+ tool.log_info(f"add cron id = {id}")
+ #### 定时任务 ####
+ tz = ZM_RABBIT()
+ if "登录" in msg or "登陆" in msg:
+ sms_content = "请在60秒内输入手机号(q:退出)"
+ tool.replyMsg(sms_content)
+ mobile = sender.listen(60000)
+ if not mobile or mobile == "q" or mobile == "error":
+ tool.replyMsg("已退出!")
+ exit()
+ pattern = r'^1[3-9]\d{9}$'
+ if not re.match(pattern, mobile):
+ tool.replyMsg("手机号格式错误,已退出!")
+ exit()
+ tool.replyMsg("请在60秒内输入登录密码(q:退出)")
+ pwd = sender.listen(60000)
+ if not pwd or pwd == "q" or pwd == "error":
+ tool.replyMsg("已退出!")
+ exit()
+ if len(pwd) < 8:
+ tool.replyMsg("密码格式错误,已退出!")
+ exit()
+ tz.run_login(mobile, pwd)
+ exit()
+ if "刷新" in msg:
+ tz.refresh_account()
+ exit()
+ if "管理" in msg:
+ tz.account_manage()
diff --git a/py/mi10rb-aut.py b/py/mi10rb-aut.py
new file mode 100644
index 0000000..e7a05e9
--- /dev/null
+++ b/py/mi10rb-aut.py
@@ -0,0 +1,296 @@
+#[title: mi10rb-aut]
+#[language: python]
+#[class: 工具类]
+#[service: 10010] ADB设备重启管理
+#[disable: false]
+#[admin: false]
+#[rule: ^重启Mi10$]
+#[priority: 999]
+#[platform: qq,qb,wx,tb,tg,web,wxmp]
+#[open_source: false]
+#[icon: 图标url]
+#[version: 1.0.0]
+#[public: false]
+#[price: 0.00]
+#[description: 通过ADB(TCP)连接到设备并执行重启;发送"重启Mi10"执行。
]
+
+# [param: {"required":true,"key":"mi10rb.address","placeholder":"127.0.0.1:5555","name":"ADB地址:","desc":"设备的 ADB 地址,如 192.168.1.100:5555"}]
+# [param: {"required":false,"key":"mi10rb.adb_path","placeholder":"adb","name":"adb路径:","desc":"adb 可执行文件路径,默认使用系统 adb"}]
+# [param: {"required":false,"key":"mi10rb.timeout","placeholder":"20","name":"超时:","desc":"单次命令超时时间(秒),默认20"}]
+
+import os
+import sys
+import shlex
+import logging
+import subprocess
+import time
+import colorlog
+import middleware
+import sender
+from typing import Tuple, Optional
+
+
+# 读取插件参数
+CONF_ADDRESS = middleware.bucketGet('mi10rb', 'address')
+CONF_ADB_PATH = middleware.bucketGet('mi10rb', 'adb_path') or 'adb'
+CONF_TIMEOUT = int(middleware.bucketGet('mi10rb', 'timeout') or '20')
+
+
+sender = middleware.Sender(middleware.getSenderID())
+
+
+class TOOL:
+ def __init__(self):
+ self.plugin_pre = "mi10rb"
+ self.color_formatter = colorlog.ColoredFormatter(
+ '%(log_color)s%(levelname)s: %(message)s',
+ log_colors={
+ 'DEBUG': 'cyan',
+ 'INFO': 'green',
+ 'WARNING': 'yellow',
+ 'ERROR': 'red',
+ 'CRITICAL': 'red,bg_white',
+ }
+ )
+ self.logger = self._configure_logger()
+
+ def _configure_logger(self):
+ logger_obj = logging.getLogger(self.plugin_pre)
+ logger_obj.setLevel(logging.DEBUG)
+ if not any(isinstance(h, logging.StreamHandler) for h in logger_obj.handlers):
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.DEBUG)
+ console_handler.setFormatter(self.color_formatter)
+ logger_obj.addHandler(console_handler)
+ return logger_obj
+
+ def log(self, level, message):
+ self.logger.log(level, message)
+
+ def reply(self, content):
+ return sender.reply(content)
+
+
+def run_command(command: str, timeout_seconds: int = 20) -> Tuple[int, str, str]:
+ """
+ 以非交互方式执行命令,返回 (exit_code, stdout, stderr)
+ """
+ try:
+ process = subprocess.Popen(
+ shlex.split(command),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+ stdout, stderr = process.communicate(timeout=timeout_seconds)
+ return process.returncode, (stdout or '').strip(), (stderr or '').strip()
+ except subprocess.TimeoutExpired:
+ try:
+ process.kill()
+ except Exception:
+ pass
+ return 124, "", f"Command timed out after {timeout_seconds}s: {command}"
+ except FileNotFoundError:
+ return 127, "", f"Command not found: {command.split()[0]}"
+
+
+class ADBRebootClient:
+ def __init__(self, address: str, adb_path: str = 'adb', timeout: int = 20, tool: Optional[TOOL] = None):
+ self.address = address
+ self.adb = adb_path or 'adb'
+ self.timeout = timeout
+ self.tool = tool
+
+ def _log(self, level, message: str):
+ if self.tool:
+ self.tool.log(level, message)
+
+ def disconnect(self) -> None:
+ # 断开指定地址,忽略错误
+ run_command(f"{self.adb} disconnect {self.address}", timeout_seconds=min(self.timeout, 5))
+
+ def kill_server(self) -> None:
+ # 尝试重启 adb 服务端,忽略错误
+ run_command(f"{self.adb} kill-server", timeout_seconds=min(self.timeout, 5))
+
+ def ensure_adb(self) -> bool:
+ code, out, err = run_command(f"{self.adb} version", timeout_seconds=self.timeout)
+ if code != 0:
+ self._log(logging.ERROR, "未找到可用的 adb,请安装或在参数中指定 adb 路径")
+ if err:
+ self._log(logging.ERROR, err)
+ return False
+ self._log(logging.INFO, (out or 'adb 可用'))
+ return True
+
+ def connect(self) -> bool:
+ # 先尝试断开可能残留的连接
+ self.disconnect()
+
+ # 首次连接
+ code, out, err = run_command(f"{self.adb} connect {self.address}", timeout_seconds=self.timeout)
+ msg = ((out or '') + ("\n" + err if err else '')).strip()
+ if code != 0:
+ self._log(logging.WARNING, f"adb connect 首次失败(code={code}),尝试重启 adb 并重试...\n{msg}")
+ # 重启 adb 后重试一次
+ self.kill_server()
+ self.disconnect()
+ code, out, err = run_command(f"{self.adb} connect {self.address}", timeout_seconds=self.timeout)
+ msg = ((out or '') + ("\n" + err if err else '')).strip()
+ if code != 0:
+ self._log(logging.ERROR, f"adb connect 重试失败(code={code}):\n{msg}")
+ return False
+
+ low = msg.lower()
+ if ("connected to" in low) or ("already connected to" in low):
+ self._log(logging.INFO, "✓ ADB 连接成功")
+ else:
+ self._log(logging.WARNING, "adb connect 返回0,但输出未包含已连接提示")
+ if msg:
+ self._log(logging.INFO, msg)
+
+ # 连接校验:确认设备在线
+ ok, detail = self.get_state()
+ if not ok:
+ self._log(logging.ERROR, f"连接校验失败:{detail}")
+ return False
+ self._log(logging.INFO, f"设备状态:{detail}")
+ return True
+
+ def get_state(self) -> Tuple[bool, str]:
+ code, out, err = run_command(f"{self.adb} -s {self.address} get-state", timeout_seconds=min(self.timeout, 10))
+ text = (out or err or '').strip()
+ if code == 0 and (out or '').strip() == 'device':
+ return True, 'device'
+ # 某些 ROM 会返回 unknown/unauthorized,这里给出更友好的提示
+ return False, (text or f"get-state 失败(code={code})")
+
+ def reboot(self) -> bool:
+ def looks_like_reboot_sent(message: str) -> bool:
+ low = (message or '').lower()
+ # 设备重启会导致连接被关闭,adb 可能返回如下错误,但指令已下发
+ hints = [
+ 'closed',
+ 'device offline',
+ 'no devices/emulators found',
+ 'cannot connect to daemon',
+ 'daemon not running',
+ 'failed to connect to',
+ 'connection refused',
+ 'connection reset',
+ 'transport endpoint is not connected',
+ 'broken pipe',
+ 'read failed',
+ 'i/o error',
+ 'more than one device/emulator',
+ ]
+ return any(h in low for h in hints)
+
+ def observe_post_reboot_disconnect(max_attempts: int = 8, interval_seconds: float = 0.5) -> bool:
+ # 在短时间内观察设备从在线到离线/不可达的过渡,以此判断重启已生效
+ for _ in range(max_attempts):
+ code_s, out_s, err_s = run_command(
+ f"{self.adb} -s {self.address} get-state",
+ timeout_seconds=min(self.timeout, 5)
+ )
+ text_s = ((out_s or '') + ('\n' + err_s if err_s else '')).strip().lower()
+ if code_s != 0:
+ # 命令失败,多半是设备掉线,视为重启进行中
+ return True
+ if (out_s or '').strip() != 'device':
+ # 非 device 状态(例如 offline/unknown),也视为重启进行中
+ return True
+ # 仍为在线,继续等待
+ time.sleep(interval_seconds)
+ return False
+
+ code, out, err = run_command(f"{self.adb} -s {self.address} reboot", timeout_seconds=self.timeout)
+ combined = ((out or '') + ("\n" + err if err else '')).strip()
+
+ if code == 0:
+ self._log(logging.INFO, "✓ 已发送重启指令")
+ return True
+
+ # 非零返回,但疑似已下发
+ if looks_like_reboot_sent(combined):
+ self._log(logging.WARNING, f"adb reboot 返回非零(code={code}),但检测到重启迹象,视为成功:\n{combined}")
+ return True
+
+ # 回退方案:走 shell reboot
+ self._log(logging.WARNING, f"adb reboot 失败(code={code}),尝试使用 shell reboot 回退...\n{combined}")
+ code2, out2, err2 = run_command(f"{self.adb} -s {self.address} shell reboot", timeout_seconds=self.timeout)
+ combined2 = ((out2 or '') + ("\n" + err2 if err2 else '')).strip()
+ if code2 == 0 or looks_like_reboot_sent(combined2):
+ self._log(logging.INFO, "✓ 已通过 shell reboot 发送重启指令")
+ return True
+
+ # 最终失败前,观察设备是否已开始断连(重启进行中)
+ if observe_post_reboot_disconnect():
+ self._log(logging.WARNING, "检测到重启后的断连迹象,尽管命令返回失败,仍视为已下发成功")
+ return True
+
+ self._log(logging.ERROR, f"shell reboot 仍失败(code={code2}):\n{combined2}")
+ return False
+
+
+def run_reboot_flow(tool: TOOL):
+ """执行 ADB 设备重启流程"""
+ tool.reply("开始执行 Mi10 ADB 重启流程...")
+
+ # 校验参数
+ if not CONF_ADDRESS:
+ tool.reply("❌ 配置参数不完整,请设置 mi10rb.address (如 192.168.1.100:5555)")
+ return
+
+ tool.log(logging.INFO, "配置信息:")
+ tool.log(logging.INFO, f" ADB地址: {CONF_ADDRESS}")
+ tool.log(logging.INFO, f" adb路径: {CONF_ADB_PATH}")
+ tool.log(logging.INFO, f" 超时: {CONF_TIMEOUT}秒")
+
+ client = ADBRebootClient(address=CONF_ADDRESS, adb_path=CONF_ADB_PATH, timeout=CONF_TIMEOUT, tool=tool)
+
+ try:
+ if not client.ensure_adb():
+ tool.reply("❌ 未检测到 adb,请安装或指定有效的 adb 路径")
+ return
+
+ tool.log(logging.INFO, "🚀 正在连接设备...")
+ if not client.connect():
+ tool.reply("❌ ADB 连接失败,请检查地址与网络")
+ return
+
+ tool.log(logging.INFO, "🔁 正在发送重启指令...")
+ if client.reboot():
+ tool.reply("✅ 重启指令已发送!设备即将重启")
+ else:
+ tool.reply("❌ 重启指令发送失败")
+ except Exception as exc:
+ tool.log(logging.ERROR, f"程序执行出错: {exc}")
+ tool.reply("❌ 程序执行出错,请检查配置与网络")
+
+
+def exit_with_msg(msg: str):
+ if msg:
+ sender.reply(msg)
+ sender.reply("已退出!")
+ sys.exit(0)
+
+
+def _safe_get_message():
+ try:
+ return sender.getMessage().strip('"')
+ except Exception:
+ return ""
+
+
+if __name__ == "__main__":
+ tool = TOOL()
+ msg = _safe_get_message()
+
+ if "重启Mi10" in msg:
+ run_reboot_flow(tool)
+ sys.exit(0)
+
+ sender.reply("指令不支持,请发送:重启Mi10")
+
+
diff --git a/py/nazha.py b/py/nazha.py
new file mode 100644
index 0000000..0dfe8e6
--- /dev/null
+++ b/py/nazha.py
@@ -0,0 +1,459 @@
+#[title: nazha]
+#[language: python]
+#[service: 10086] 售后联系方式
+#[disable: false] 禁用开关,true表示禁用,false表示可用
+#[admin: true] 是否为管理员指令
+#[rule: ^哪吒查询$]
+#[rule: ^哪吒管理$]
+#[cron: 0 7 * * *]
+#[priority: 300] 优先级,数字越大表示优先级越高
+#[platform: qq,wx,tg,tb] 适用的平台
+#[open_source: true]是否开源
+#[icon: 图标url]图标链接地址,请使用48像素的正方形图标,支持http和https
+#[version: 1.0.0]版本号
+#[public: false] 是否发布?值为true或false,不设置则上传aut云时会自动设置为true,false时上传后不显示在市场中,但是搜索能搜索到,方便开发者测试
+#[price: 0.01] 上架价格
+# [description: 【简介】:哪吒监控查询 支持多账号管理,可查询服务器状态、服务状态等]
+# [icon: https://cdn.jsdelivr.net/gh/naiba/nezha@master/resource/static/img/nezha.png]
+# =======================
+
+
+import importlib
+import inspect
+import json
+import logging
+import math
+from math import fabs
+import os
+import random
+import subprocess
+import sys
+import time
+from datetime import datetime, timedelta
+from multiprocessing import Pool
+from multiprocessing.pool import ThreadPool
+from urllib.parse import urlencode
+from wsgiref import headers
+
+import requests
+import aiohttp
+import asyncio
+
+import middleware
+
+plugin_pre = 'vhook_nezha_'
+
+class TOOL:
+ def __init__(self):
+ self.plat = ["qq", "wx", "wb", "qb", "bt", "tg"]
+ self.platform_arr = self.platformArr()
+ self.sender = middleware.Sender(middleware.getSenderID())
+ self.plugin_title = self.sender.getPluginName()
+ self.plugin_ver = self.sender.getPluginVersion()
+ self.plugin_name = self.sender.getPluginName()
+ self.msg = self.sender.getMessage().strip('"')
+ self.imType = "fake" if self.sender.getImtype() == "cron" else self.sender.getImtype()
+ self.chatId = self.sender.getChatID()
+ self.userId = self.sender.getUserID()
+ self.isAdmin = self.sender.isAdmin()
+ self.remark = ""
+ self.timeStr = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ # 安装必须依赖
+ self.dependencies = ['colorlog', "requests", "aiohttp"]
+ self.install_dependencies()
+ self.logger = logging.getLogger()
+ self.color_formatter = sys.modules['colorlog'].ColoredFormatter(
+ '%(log_color)s%(levelname)s: %(message)s',
+ log_colors={
+ 'DEBUG': 'cyan',
+ 'INFO': 'green',
+ 'WARNING': 'yellow',
+ 'ERROR': 'red',
+ 'CRITICAL': 'red,bg_white',
+ }
+ )
+
+ def install_dependencies(self):
+ for dependency in self.dependencies:
+ try:
+ importlib.import_module(dependency)
+ except ImportError as e:
+ self.pushErr2Master(f"【错误】:缺少【{dependency}】模块, 正在自动安装,请稍后....")
+ if self.check_pip_version("pip3"):
+ cmd = "pip3"
+ else:
+ cmd = "pip"
+ result = subprocess.run([cmd, 'install', dependency], capture_output=True, text=True)
+ if result.returncode == 0:
+ self.pushErr2Master(f"依赖【{dependency}】安装完成,请重新发送指令,运行插件")
+
+ def check_pip_version(self, pip_command):
+ try:
+ result = subprocess.run([pip_command, '--version'], capture_output=True, text=True)
+ if result.returncode == 0:
+ return True
+ except FileNotFoundError:
+ pass
+ return False
+
+ def get_log(self, level=logging.INFO):
+ self.logger.setLevel(level)
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(level)
+ console_handler.setFormatter(self.color_formatter)
+ for handler in self.logger.handlers:
+ self.logger.removeHandler(handler)
+ self.logger.addHandler(console_handler)
+
+ def log_info(self, msg):
+ self.get_log(logging.INFO)
+ return self.logger.info(f"{msg}")
+
+ def log_err(self, msg):
+ self.get_log(logging.ERROR)
+ return self.logger.error(f"{msg}")
+
+ def log_warn(self, msg):
+ self.get_log(logging.WARN)
+ return self.logger.warning(f"{msg}")
+
+ def platformArr(self):
+ plat_arr = []
+ for item in self.plat:
+ t = {"platform": f"{plugin_pre}{item}", "imType": item}
+ plat_arr.append(t)
+ return plat_arr
+
+ def pushMsg(self, userId, chatId, imType, title, content):
+ now = datetime.now()
+ formatted_datetime = now.strftime('%Y-%m-%d %H:%M:%S')
+ self.remark = ""
+ msg = f"*********{tool.plugin_title}*********"
+ msg += f'\n【任务时间】:{formatted_datetime}'
+ msg += f"\n{content}"
+ middleware.push(imType, chatId, userId, title, msg)
+ return
+
+ def pushGroup(self, imtype, groupCode, content):
+ self.pushMsg(0, groupCode, imtype, "", content)
+
+ def pushErr2Master(self, content):
+ middleware.notifyMasters(
+ f"【插件】:{self.plugin_title}\n {content}",
+ ['qq', 'tg', 'wx', 'wb', ])
+
+ def replyMsg(self, content):
+ return self.sender.reply(content)
+
+class ACCOUNT:
+ def __init__(self):
+ self.method = "account"
+ self.plugin_title = "哪吒监控"
+ self.attr_arr = [
+ {
+ "title": "备注",
+ "key": "name",
+ "timeOut": 60000
+ }, {
+ "title": "面板地址",
+ "key": "dashboard_url",
+ "timeOut": 60000
+ }, {
+ "title": "用户名",
+ "key": "username",
+ "timeOut": 60000
+ }, {
+ "title": "密码",
+ "key": "password",
+ "timeOut": 60000
+ }
+ ]
+
+ def setVal(self, item, title, key, timeOut):
+ tool.log_info(item)
+ tool.replyMsg(f"请输入{title}:")
+ value = tool.sender.listen(timeOut)
+ tool.log_info(f"{key}==={value}")
+
+ if value == "q" or value == "error":
+ tool.replyMsg("输入有误/超时,已退出!")
+ return False
+ item[key] = value
+ return True
+
+ def addCount(self):
+ tool.replyMsg("哪吒监控配置说明:\n1. 备注:用于区分不同账号\n2. 面板地址:哪吒面板的完整URL\n3. 用户名:面板登录用户名\n4. 密码:面板登录密码")
+ item = {}
+ go = True
+ for attr in self.attr_arr:
+ if not self.setVal(item, attr['title'], attr['key'], attr['timeOut']):
+ go = False
+ break
+ if not go:
+ tool.log_info("数据配置有误,已退出!")
+ return tool.replyMsg("数据配置有误,已退出!")
+ item_arr = []
+ item_str = middleware.bucketGet(f"{plugin_pre}{tool.imType}", tool.userId)
+ if item_str:
+ item_arr = json.loads(item_str)
+ item_arr.append(item)
+ middleware.bucketSet(f"{plugin_pre}{tool.imType}", tool.userId, json.dumps(item_arr))
+ tool.replyMsg("数据已配置完成,开始自动任务")
+ item['push_user_id'] = tool.userId
+ item['push_im_type'] = tool.imType
+ item['index'] = 1
+ item['total'] = 1
+ READ(item).run()
+
+ def editCount(self, item_arr, no):
+ item = item_arr[no]
+ content = f"请在【2分钟】内输入 序号,编辑对应的属性(q:退出)"
+ content += f"\n--------------------"
+ content += f"\n输入数字:0 删除此账号!"
+ content += f"\n--------------------"
+ for index, attr in enumerate(self.attr_arr):
+ if attr['key'] not in item:
+ item[attr['key']] = ""
+ content += f"\n{index + 1}.【{attr['title']}】:{item[attr['key']]}"
+ tool.replyMsg(content)
+ value = tool.sender.listen(120000)
+ if value == "q" or value == "error":
+ middleware.bucketSet(f"{plugin_pre}{tool.imType}", tool.userId, json.dumps(item_arr))
+ return tool.replyMsg("已退出!")
+ if not value.isdigit():
+ return self.editCount(item_arr, no)
+ if 0 < int(value) <= len(self.attr_arr):
+ attr = self.attr_arr[int(value) - 1]
+ if self.setVal(item, attr['title'], attr['key'], 12000):
+ item_arr[no] = item
+ self.editCount(item_arr, no)
+ elif value == "0":
+ item_arr.pop(no)
+ if len(item_arr) == 0:
+ middleware.bucketDel(f"{plugin_pre}{tool.imType}", tool.userId)
+ else:
+ middleware.bucketSet(f"{plugin_pre}{tool.imType}", tool.userId, json.dumps(item_arr))
+ return tool.replyMsg(f"已删除第{no + 1}个账号信息!请重新发送:哪吒管理 !")
+ else:
+ return self.editCount(item_arr, no)
+
+ def accoount_manager(self):
+ item_str = middleware.bucketGet(f"{plugin_pre}{tool.imType}", tool.userId)
+ if not item_str or item_str == "":
+ tool.replyMsg(f"未配置账号,请发送:哪吒查询")
+ exit(1)
+ item_arr = json.loads(item_str)
+ content = f"请选择要账号查看详情:(0增加, q退出)\n"
+ for index, item in enumerate(item_arr):
+ if not hasattr(item, ""):
+ item['disable'] = True
+ disable = item['disable']
+ status = '已启用' if disable else '已禁用'
+ content = "".join([content, f"\n{index + 1}、{item['name']}"])
+ tool.replyMsg(content)
+ value = tool.sender.listen(60000)
+ if value == "q" or value == "error":
+ tool.replyMsg("输入有误,已退出!")
+ elif value == "0":
+ self.addCount()
+ elif value.isdigit() and 0 < int(value) <= len(item_arr):
+ self.editCount(item_arr, int(value) - 1)
+ else:
+ tool.replyMsg(f"输入有误,请重新发送:哪吒管理,并输入正确的序号!")
+
+ def cron_account_arr(self):
+ account_arr = []
+ for plat in tool.platformArr():
+ p = plat["platform"]
+ user_id_arr = middleware.bucketAllKeys(p)
+ if not user_id_arr:
+ continue
+ for index, user_id in enumerate(user_id_arr):
+ user_data_str = middleware.bucketGet(p, user_id)
+ if not user_data_str or user_id == '':
+ continue
+ user_data_arr_temp = json.loads(user_data_str)
+ for n, account_data in enumerate(user_data_arr_temp):
+ account_data["push_user_id"] = user_id
+ account_data["push_im_type"] = plat["imType"]
+ account_data['index'] = n + 1
+ account_data['total'] = len(user_data_arr)
+ account_arr.append(account_data)
+ return account_arr
+
+ def account_task(self, item, no):
+ read = READ(item)
+ read.run()
+
+class READ:
+ def __init__(self, account):
+ self.name = account['name']
+ self.dashboard_url = account['dashboard_url']
+ self.username = account['username']
+ self.password = account['password']
+ self.index = account['index']
+ self.total = account['total']
+ self.push_user_id = account['push_user_id']
+ self.push_im_type = account['push_im_type']
+ self.msg = ''
+ self.dateStr = datetime.now().strftime('%Y-%m-%d')
+ self.timeStr = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ self.servers = [] # 初始化servers属性
+ self.chat_id = None # 初始化chat_id属性
+
+ def pushMsg(self, msg):
+ content = f"【账号备注】:{self.name}"
+ content += f'{msg}'
+ tool.pushMsg(self.push_user_id, None, self.push_im_type, "", content)
+
+ def format_bytes(self, size_in_bytes):
+ if size_in_bytes == 0:
+ return "0B"
+ units = ['B', 'KB', 'MB', 'GB', 'TB']
+ power = int(math.floor(math.log(size_in_bytes, 1024)))
+ power = min(power, len(units) - 1)
+ size = size_in_bytes / (1024 ** power)
+ formatted_size = f"{size:.2f}{units[power]}"
+ return formatted_size
+
+ async def get_server_info(self):
+ try:
+ async with aiohttp.ClientSession() as session:
+ # 登录获取token
+ login_url = f"{self.dashboard_url.rstrip('/')}/api/v1/login"
+ login_data = {
+ "username": self.username,
+ "password": self.password
+ }
+ async with session.post(login_url, json=login_data) as resp:
+ login_result = await resp.json()
+ if not login_result.get('success'):
+ return False, "登录失败,请检查用户名和密码"
+ token = login_result['data']['token']
+
+ # 获取服务器信息
+ headers = {"Authorization": f"Bearer {token}"}
+ server_url = f"{self.dashboard_url.rstrip('/')}/api/v1/server"
+ async with session.get(server_url, headers=headers) as resp:
+ server_result = await resp.json()
+ if not server_result.get('success'):
+ return False, "获取服务器信息失败"
+
+ self.servers = server_result['data'] # 填充servers属性
+ # 计算总体统计信息
+ online_count = sum(1 for server in self.servers if server.get('state', {}).get('last_active'))
+ total_mem = sum(server.get('host', {}).get('mem_total', 0) for server in self.servers)
+ used_mem = sum(server.get('state', {}).get('mem_used', 0) for server in self.servers)
+ total_disk = sum(server.get('host', {}).get('disk_total', 0) for server in self.servers)
+ used_disk = sum(server.get('state', {}).get('disk_used', 0) for server in self.servers)
+ total_swap = sum(server.get('host', {}).get('swap_total', 0) for server in self.servers)
+ used_swap = sum(server.get('state', {}).get('swap_used', 0) for server in self.servers)
+ total_net_in = sum(server.get('state', {}).get('net_in_transfer', 0) for server in self.servers)
+ total_net_out = sum(server.get('state', {}).get('net_out_transfer', 0) for server in self.servers)
+
+ # 返回总体统计信息
+ self.msg += f"\n【服务器数量】:{len(self.servers)}"
+ self.msg += f"\n【在线服务器】:{online_count}"
+ self.msg += f"\n【内存使用】:{self.format_bytes(used_mem)}/{self.format_bytes(total_mem)}"
+ self.msg += f"\n【磁盘使用】:{self.format_bytes(used_disk)}/{self.format_bytes(total_disk)}"
+ self.msg += f"\n【交换使用】:{self.format_bytes(used_swap)}/{self.format_bytes(total_swap)}"
+ self.msg += f"\n【总流量】:↓{self.format_bytes(total_net_in)} ↑{self.format_bytes(total_net_out)}"
+
+ return True, "获取成功"
+ except Exception as e:
+ return False, f"获取信息失败:{str(e)}"
+
+ def run(self):
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ success, message = loop.run_until_complete(self.get_server_info())
+ loop.close()
+
+ if not success:
+ self.msg += f"\n【错误】:{message}"
+ self.pushMsg(self.msg)
+ return
+
+ self.display_menu()
+
+ def display_menu(self):
+ # 只返回总体数据和选单
+ self.msg = f"\n【服务器数量】:{len(self.servers)}"
+ self.msg += f"\n【在线服务器】:{sum(1 for server in self.servers if server.get('state', {}).get('last_active'))}"
+ self.msg += f"\n【内存使用】:{self.format_bytes(sum(server.get('state', {}).get('mem_used', 0) for server in self.servers))}/{self.format_bytes(sum(server.get('host', {}).get('mem_total', 0) for server in self.servers))}"
+ self.msg += f"\n【磁盘使用】:{self.format_bytes(sum(server.get('state', {}).get('disk_used', 0) for server in self.servers))}/{self.format_bytes(sum(server.get('host', {}).get('disk_total', 0) for server in self.servers))}"
+ self.msg += f"\n【交换使用】:{self.format_bytes(sum(server.get('state', {}).get('swap_used', 0) for server in self.servers))}/{self.format_bytes(sum(server.get('host', {}).get('swap_total', 0) for server in self.servers))}"
+ self.msg += f"\n【总流量】:↓{self.format_bytes(sum(server.get('state', {}).get('net_in_transfer', 0) for server in self.servers))} ↑{self.format_bytes(sum(server.get('state', {}).get('net_out_transfer', 0) for server in self.servers))}"
+
+ self.msg += "\n\n【服务器编号选单】:"
+ for index, server in enumerate(self.servers):
+ server_name = server.get('name', '未知')
+ self.msg += f"\n{index + 1}. {server_name}"
+ self.msg += "\n0. 查询所有服务器信息"
+
+ self.pushMsg(self.msg)
+
+ # 等待用户输入
+ user_input = tool.sender.listen(30000) # 30秒超时
+ if user_input.lower() == 'q':
+ tool.replyMsg("对话已结束。")
+ elif user_input.isdigit():
+ index = int(user_input) - 1
+ if index == -1:
+ # 查询所有服务器信息
+ all_details = ""
+ for i in range(len(self.servers)):
+ all_details += self.query_server_by_index(i) + "\n"
+ tool.replyMsg(all_details)
+ #tool.replyMsg("查询结束。")
+ elif 0 <= index < len(self.servers):
+ details = self.query_server_by_index(index)
+ tool.replyMsg(details)
+ self.display_menu() # 返回到二级菜单
+ else:
+ tool.replyMsg("无效的编号,请重新输入。")
+ self.display_menu()
+ else:
+ tool.replyMsg("超时或无效输入,已自动退出。")
+
+ def query_server_by_index(self, index):
+ if index < 0 or index >= len(self.servers):
+ return "无效的服务器编号。"
+
+ server = self.servers[index]
+ server_name = server.get('name', '未知')
+ status = "在线" if server.get('state', {}).get('last_active') else ""
+ details = f"\n【{server_name}】 {status}"
+ details += f"\nCPU使用率:{server.get('state', {}).get('cpu', 0):.2f}%"
+ details += f"\n内存使用:{self.format_bytes(server.get('state', {}).get('mem_used', 0))}/{self.format_bytes(server.get('host', {}).get('mem_total', 0))}"
+ details += f"\n磁盘使用:{self.format_bytes(server.get('state', {}).get('disk_used', 0))}/{self.format_bytes(server.get('host', {}).get('disk_total', 0))}"
+ details += f"\n流量统计:↓{self.format_bytes(server.get('state', {}).get('net_in_transfer', 0))} ↑{self.format_bytes(server.get('state', {}).get('net_out_transfer', 0))}"
+ details += f"\n当前下载速率:↓{self.format_bytes(server.get('state', {}).get('net_in_speed', 0))}/s"
+ details += f"\n当前上传速率:↑{self.format_bytes(server.get('state', {}).get('net_out_speed', 0))}/s"
+
+ return details
+
+if __name__ == "__main__":
+ tool = TOOL()
+ account = ACCOUNT()
+ if tool.msg == '哪吒管理':
+ account.accoount_manager()
+ exit(1)
+ if tool.imType == 'fake':
+ user_data_arr = account.cron_account_arr()
+ else:
+ user_str = middleware.bucketGet(f"{plugin_pre}{tool.imType}", tool.userId)
+ if not user_str or user_str == "":
+ account.addCount()
+ exit(1)
+ user_data_arr = json.loads(user_str)
+ for i, user_data in enumerate(user_data_arr):
+ user_data["push_user_id"] = tool.userId
+ user_data["push_im_type"] = tool.imType
+ user_data['index'] = i + 1
+ user_data['total'] = len(user_data_arr)
+ if tool.sender.param(1):
+ user_data_arr = [user_data_arr[int(tool.sender.param(1))-1]]
+ for account_data in user_data_arr:
+ account.account_task(account_data, account_data['index'])
+ exit(1)
diff --git a/py/rb-aut.py b/py/rb-aut.py
new file mode 100644
index 0000000..951dff7
--- /dev/null
+++ b/py/rb-aut.py
@@ -0,0 +1,1172 @@
+#[title: rb-aut]
+#[language: python]
+#[class: 工具类]
+#[service: 10010] 设备重启管理(设备管理模式)
+#[disable: false]
+#[admin: false]
+#[rule: ^(rb-now|rb管理)$]
+#[priority: 999]
+#[platform: qq,qb,wx,tb,tg,web,wxmp]
+#[open_source: false]
+#[icon: 图标url]
+#[version: 1.0.0]
+#[public: false]
+#[price: 0.00]
+#[description: 设备重启管理,支持多设备管理。
指令:rb-now(设备选单重启)、rb管理(设备管理),adb方式重启需安装依赖adbtool,并且首次运行需在设备授权]
+
+import sys
+import shlex
+import time
+import json
+import logging
+import subprocess
+from typing import Tuple, Optional
+
+import colorlog
+import middleware
+import sender
+
+
+# 存储桶前缀
+BUCKET = 'rb-aut'
+
+
+sender = middleware.Sender(middleware.getSenderID())
+
+
+class TOOL:
+ def __init__(self):
+ self.plugin_pre = BUCKET
+ self.color_formatter = colorlog.ColoredFormatter(
+ '%(log_color)s%(levelname)s: %(message)s',
+ log_colors={
+ 'DEBUG': 'cyan',
+ 'INFO': 'green',
+ 'WARNING': 'yellow',
+ 'ERROR': 'red',
+ 'CRITICAL': 'red,bg_white',
+ }
+ )
+ self.logger = self._configure_logger()
+
+ def _configure_logger(self):
+ logger_obj = logging.getLogger(self.plugin_pre)
+ logger_obj.setLevel(logging.DEBUG)
+ if not any(isinstance(h, logging.StreamHandler) for h in logger_obj.handlers):
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.DEBUG)
+ console_handler.setFormatter(self.color_formatter)
+ logger_obj.addHandler(console_handler)
+ return logger_obj
+
+ def log(self, level, message):
+ self.logger.log(level, message)
+
+ def reply(self, content):
+ return sender.reply(content)
+
+
+def bucket_get(key: str, default: Optional[str] = None) -> Optional[str]:
+ val = middleware.bucketGet(BUCKET, key)
+ return val if val is not None and val != '' else default
+
+
+def bucket_set(key: str, value: str) -> None:
+ middleware.bucketSet(BUCKET, key, value)
+
+
+def get_all_devices() -> dict:
+ """获取所有设备配置"""
+ devices = {}
+ all_keys = middleware.bucketAllKeys(BUCKET)
+ for key in all_keys:
+ if key.startswith('device.'):
+ device_id = key[7:] # 去掉 'device.' 前缀
+ device_data = bucket_get(key)
+ if device_data:
+ try:
+ devices[device_id] = json.loads(device_data)
+ except Exception:
+ continue
+ return devices
+
+
+def save_device(device_id: str, device_config: dict) -> None:
+ """保存设备配置"""
+ bucket_set(f'device.{device_id}', json.dumps(device_config))
+
+
+def delete_device(device_id: str) -> None:
+ """删除设备配置"""
+ middleware.bucketDel(BUCKET, f'device.{device_id}')
+
+
+def has_device_permission(device_config: dict, user_id: str, is_admin: bool) -> bool:
+ """检查用户是否有设备权限
+ Args:
+ device_config: 设备配置字典
+ user_id: 用户ID
+ is_admin: 是否是管理员
+ Returns:
+ bool: 是否有权限
+ """
+ # 管理员有所有权限
+ if is_admin:
+ return True
+
+ # 检查设备权限配置
+ admin_only = device_config.get('admin_only', False)
+ if admin_only:
+ return False
+
+ # 检查允许的用户列表
+ allowed_users = device_config.get('allowed_users', [])
+ if isinstance(allowed_users, str):
+ # 兼容旧格式(逗号分隔的字符串)
+ allowed_users = [u.strip() for u in allowed_users.split(',') if u.strip()]
+ if not isinstance(allowed_users, list):
+ allowed_users = []
+
+ # 如果允许的用户列表为空,则默认所有用户都可以访问
+ if not allowed_users:
+ return True
+
+ return user_id in allowed_users
+
+
+def get_device_list(user_id: Optional[str] = None, is_admin: bool = False) -> list:
+ """获取设备列表,返回格式化的设备信息
+ Args:
+ user_id: 用户ID,如果提供则只返回该用户有权限的设备
+ is_admin: 是否是管理员,管理员可以看到所有设备
+ """
+ devices = get_all_devices()
+ device_list = []
+ for device_id, config in devices.items():
+ device_type = config.get('type', 'unknown')
+ name = config.get('name', device_id)
+
+ # 如果指定了用户ID且不是管理员,检查权限
+ if user_id and not is_admin:
+ if not has_device_permission(config, user_id, False):
+ continue
+
+ if device_type == 'adb':
+ device_list.append({
+ 'id': device_id,
+ 'name': name,
+ 'type': 'ADB',
+ 'info': name
+ })
+ elif device_type == 'ssh':
+ device_list.append({
+ 'id': device_id,
+ 'name': name,
+ 'type': 'SSH',
+ 'info': name
+ })
+ return device_list
+
+
+def run_command(command: str, timeout_seconds: int = 20) -> Tuple[int, str, str]:
+ try:
+ process = subprocess.Popen(
+ shlex.split(command),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+ stdout, stderr = process.communicate(timeout=timeout_seconds)
+ return process.returncode, (stdout or '').strip(), (stderr or '').strip()
+ except subprocess.TimeoutExpired:
+ try:
+ process.kill()
+ except Exception:
+ pass
+ return 124, '', f"Command timed out after {timeout_seconds}s: {command}"
+ except FileNotFoundError:
+ return 127, '', f"Command not found: {command.split()[0]}"
+
+
+class ADBRebootClient:
+ def __init__(self, address: str, adb_path: str = 'adb', timeout: int = 20, tool: Optional[TOOL] = None):
+ self.address = address
+ self.adb = adb_path or 'adb'
+ self.timeout = timeout
+ self.tool = tool
+
+ def _log(self, level, message: str):
+ if self.tool:
+ self.tool.log(level, message)
+
+ def disconnect(self) -> None:
+ run_command(f"{self.adb} disconnect {self.address}", timeout_seconds=min(self.timeout, 5))
+
+ def kill_server(self) -> None:
+ run_command(f"{self.adb} kill-server", timeout_seconds=min(self.timeout, 5))
+
+ def cleanup(self) -> None:
+ """清理ADB连接和服务"""
+ try:
+ # 先断开指定设备连接
+ self.disconnect()
+ # 等待一下确保断开完成
+ time.sleep(1)
+ # 然后kill整个adb服务
+ self.kill_server()
+ if self.tool:
+ self.tool.log(logging.INFO, "✓ ADB服务已清理完成")
+ except Exception as e:
+ if self.tool:
+ self.tool.log(logging.WARNING, f"ADB清理过程中出现异常: {e}")
+
+ def ensure_adb(self) -> bool:
+ code, out, err = run_command(f"{self.adb} version", timeout_seconds=self.timeout)
+ if code != 0:
+ self._log(logging.ERROR, "未找到可用的 adb,请安装或在参数中指定 adb 路径")
+ if err:
+ self._log(logging.ERROR, err)
+ return False
+ self._log(logging.INFO, (out or 'adb 可用'))
+ return True
+
+ def connect(self) -> bool:
+ self.disconnect()
+ code, out, err = run_command(f"{self.adb} connect {self.address}", timeout_seconds=self.timeout)
+ msg = ((out or '') + ("\n" + err if err else '')).strip()
+ if code != 0:
+ self._log(logging.WARNING, f"adb connect 首次失败(code={code}),尝试重启 adb 并重试...\n{msg}")
+ self.kill_server()
+ self.disconnect()
+ code, out, err = run_command(f"{self.adb} connect {self.address}", timeout_seconds=self.timeout)
+ msg = ((out or '') + ("\n" + err if err else '')).strip()
+ if code != 0:
+ self._log(logging.ERROR, f"adb connect 重试失败(code={code}):\n{msg}")
+ return False
+
+ low = msg.lower()
+ if ("connected to" in low) or ("already connected to" in low):
+ self._log(logging.INFO, "✓ ADB 连接成功")
+ else:
+ self._log(logging.WARNING, "adb connect 返回0,但输出未包含已连接提示")
+ if msg:
+ self._log(logging.INFO, msg)
+
+ ok, detail = self.get_state()
+ if not ok:
+ self._log(logging.ERROR, f"连接校验失败:{detail}")
+ return False
+ self._log(logging.INFO, f"设备状态:{detail}")
+ return True
+
+ def get_state(self) -> Tuple[bool, str]:
+ code, out, err = run_command(f"{self.adb} -s {self.address} get-state", timeout_seconds=min(self.timeout, 10))
+ text = (out or err or '').strip()
+ if code == 0 and (out or '').strip() == 'device':
+ return True, 'device'
+ return False, (text or f"get-state 失败(code={code})")
+
+ def reboot(self) -> bool:
+ def looks_like_reboot_sent(message: str) -> bool:
+ low = (message or '').lower()
+ hints = [
+ 'closed', 'device offline', 'no devices/emulators found', 'cannot connect to daemon',
+ 'daemon not running', 'failed to connect to', 'connection refused', 'connection reset',
+ 'transport endpoint is not connected', 'broken pipe', 'read failed', 'i/o error',
+ 'more than one device/emulator',
+ ]
+ return any(h in low for h in hints)
+
+ def observe_post_reboot_disconnect(max_attempts: int = 8, interval_seconds: float = 0.5) -> bool:
+ for _ in range(max_attempts):
+ code_s, out_s, err_s = run_command(
+ f"{self.adb} -s {self.address} get-state",
+ timeout_seconds=min(self.timeout, 5)
+ )
+ if code_s != 0:
+ return True
+ if (out_s or '').strip() != 'device':
+ return True
+ time.sleep(interval_seconds)
+ return False
+
+ code, out, err = run_command(f"{self.adb} -s {self.address} reboot", timeout_seconds=self.timeout)
+ combined = ((out or '') + ("\n" + err if err else '')).strip()
+ if code == 0:
+ self._log(logging.INFO, "✓ 已发送重启指令")
+ return True
+ if looks_like_reboot_sent(combined):
+ self._log(logging.WARNING, f"adb reboot 返回非零(code={code}),但检测到重启迹象,视为成功:\n{combined}")
+ return True
+ self._log(logging.WARNING, f"adb reboot 失败(code={code}),尝试使用 shell reboot 回退...\n{combined}")
+ code2, out2, err2 = run_command(f"{self.adb} -s {self.address} shell reboot", timeout_seconds=self.timeout)
+ combined2 = ((out2 or '') + ("\n" + err2 if err2 else '')).strip()
+ if code2 == 0 or looks_like_reboot_sent(combined2):
+ self._log(logging.INFO, "✓ 已通过 shell reboot 发送重启指令")
+ return True
+ if observe_post_reboot_disconnect():
+ self._log(logging.WARNING, "检测到重启后的断连迹象,尽管命令返回失败,仍视为已下发成功")
+ return True
+ self._log(logging.ERROR, f"shell reboot 仍失败(code={code2}):\n{combined2}")
+ return False
+
+
+class SSHRebootClient:
+ def __init__(self, hostname: str, username: str, password: str,
+ port: int = 5722, timeout: int = 30, tool: Optional[TOOL] = None):
+ self.hostname = hostname
+ self.username = username
+ self.password = password
+ self.port = port
+ self.timeout = timeout
+ self.tool = tool
+ self.client = None
+
+ def connect(self) -> bool:
+ try:
+ if self.tool:
+ self.tool.log(logging.INFO, f"正在连接到 {self.hostname}:{self.port}...")
+ import paramiko
+ self.client = paramiko.SSHClient()
+ self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self.client.connect(
+ hostname=self.hostname,
+ username=self.username,
+ password=self.password,
+ port=self.port,
+ timeout=self.timeout,
+ allow_agent=False,
+ look_for_keys=False,
+ )
+ if self.tool:
+ self.tool.log(logging.INFO, "✓ SSH连接建立成功!")
+ return True
+ except Exception as e:
+ if self.tool:
+ self.tool.log(logging.ERROR, f"✗ SSH连接失败: {e}")
+ return False
+
+ def _execute_reboot_command(self, command: str) -> bool:
+ reboot_commands = [
+ command,
+ f"sudo {command}",
+ f"nohup {command} > /dev/null 2>&1 &",
+ f"sudo nohup {command} > /dev/null 2>&1 &",
+ ]
+ for i, cmd in enumerate(reboot_commands):
+ try:
+ if self.tool:
+ self.tool.log(logging.INFO, f"尝试执行重启命令 ({i+1}/{len(reboot_commands)}): {cmd}")
+ stdin, stdout, stderr = self.client.exec_command(cmd, timeout=3)
+ try:
+ _ = stdout.read()
+ _ = stderr.read()
+ except Exception:
+ pass
+ time.sleep(1)
+ try:
+ t_in, t_out, t_err = self.client.exec_command("echo test", timeout=1)
+ _ = t_out.read()
+ except Exception:
+ if self.tool:
+ self.tool.log(logging.INFO, "✓ 检测到SSH连接断开,重启命令生效!")
+ self.tool.log(logging.INFO, "✓ 设备正在重启中...")
+ return True
+ if i < len(reboot_commands) - 1:
+ if self.tool:
+ self.tool.log(logging.INFO, "连接仍在,尝试下一种命令格式...")
+ time.sleep(1)
+ except Exception as e:
+ if self.tool:
+ self.tool.log(logging.WARNING, f"命令执行异常: {e}")
+ try:
+ _ = self.client.get_transport().is_active()
+ except Exception:
+ if self.tool:
+ self.tool.log(logging.INFO, "✓ 检测到SSH连接断开,重启命令生效!")
+ return True
+ if i < len(reboot_commands) - 1:
+ if self.tool:
+ self.tool.log(logging.INFO, "尝试下一种命令格式...")
+ time.sleep(1)
+ if self.tool:
+ self.tool.log(logging.WARNING, "⚠️ 所有重启命令格式都尝试过了;重启可能已发送")
+ return True
+
+ def send_reboot_command(self, command: str = 'reboot') -> bool:
+ if not self.client:
+ if self.tool:
+ self.tool.log(logging.ERROR, "✗ 未建立SSH连接")
+ return False
+ try:
+ if command.lower() in ['reboot', 'shutdown', 'poweroff']:
+ return self._execute_reboot_command(command)
+ stdin, stdout, stderr = self.client.exec_command(command, timeout=30)
+ exit_status = stdout.channel.recv_exit_status()
+ _ = stdout.read()
+ _ = stderr.read()
+ if exit_status == 0:
+ if self.tool:
+ self.tool.log(logging.INFO, "✓ 命令执行成功!")
+ return True
+ if self.tool:
+ self.tool.log(logging.ERROR, f"✗ 命令执行失败,退出码: {exit_status}")
+ return False
+ except Exception as e:
+ if self.tool:
+ self.tool.log(logging.ERROR, f"✗ 执行命令时出错: {e}")
+ return False
+
+ def is_connected(self) -> bool:
+ try:
+ if self.client and self.client.get_transport():
+ return self.client.get_transport().is_active()
+ return False
+ except Exception:
+ return False
+
+ def close(self) -> None:
+ if self.client:
+ try:
+ self.client.close()
+ if self.tool:
+ self.tool.log(logging.INFO, "SSH连接已关闭")
+ except Exception:
+ if self.tool:
+ self.tool.log(logging.INFO, "SSH连接已断开")
+
+
+def run_adb_reboot(tool: TOOL, device_config: dict) -> bool:
+ """执行ADB设备重启"""
+ addr = device_config.get('address', '')
+ name = device_config.get('name', '未知设备')
+
+ tool.reply(f"开始执行 {name} ADB 重启流程...")
+ if not addr:
+ tool.reply("❌ 设备配置不完整,缺少 address")
+ return
+
+ tool.log(logging.INFO, "配置信息:")
+ tool.log(logging.INFO, f" 设备名称: {name}")
+ tool.log(logging.INFO, f" ADB地址: {addr}")
+
+ client = ADBRebootClient(address=addr, adb_path='adb', timeout=20, tool=tool)
+ try:
+ if not client.ensure_adb():
+ tool.reply("❌ 未检测到 adb,请安装 adb")
+ return False
+ tool.log(logging.INFO, "🚀 正在连接设备...")
+ if not client.connect():
+ tool.reply("❌ ADB 连接失败,请检查地址与网络")
+ return False
+ tool.log(logging.INFO, "🔁 正在发送重启指令...")
+ if client.reboot():
+ tool.reply("✅ 重启指令已发送!设备即将重启")
+ # 等待一下让重启指令生效
+ time.sleep(2)
+ # 清理ADB连接和服务
+ tool.log(logging.INFO, "🔌 正在清理ADB连接...")
+ client.cleanup()
+ return True
+ else:
+ tool.reply("❌ 重启指令发送失败")
+ # 即使失败也清理连接
+ tool.log(logging.INFO, "🔌 正在清理ADB连接...")
+ client.cleanup()
+ return False
+ except Exception as exc:
+ tool.log(logging.ERROR, f"程序执行出错: {exc}")
+ tool.reply("❌ 程序执行出错,请检查配置与网络")
+ # 异常情况下也尝试清理
+ try:
+ tool.log(logging.INFO, "🔌 异常情况下清理ADB连接...")
+ client.cleanup()
+ except Exception:
+ pass
+ return False
+
+
+def run_ssh_reboot(tool: TOOL, device_config: dict) -> bool:
+ """执行SSH设备重启"""
+ host = device_config.get('hostname', '')
+ username = device_config.get('username', 'root')
+ password = device_config.get('password', '')
+ port = device_config.get('port', 22)
+ timeout = device_config.get('timeout', 30)
+ command = device_config.get('command', 'reboot')
+ name = device_config.get('name', '未知设备')
+
+ tool.reply(f"开始执行 {name} SSH 重启流程...")
+ if not host or not username or not password:
+ tool.reply("❌ 设备配置不完整,缺少 hostname/username/password")
+ return False
+
+ tool.log(logging.INFO, f"配置信息:")
+ tool.log(logging.INFO, f" 设备名称: {name}")
+ tool.log(logging.INFO, f" 主机: {host}")
+ tool.log(logging.INFO, f" 用户: {username}")
+ tool.log(logging.INFO, f" 端口: {port}")
+ tool.log(logging.INFO, f" 命令: {command}")
+ tool.log(logging.INFO, f" 超时: {timeout}秒")
+
+ client = SSHRebootClient(hostname=host, username=username, password=password,
+ port=port, timeout=timeout, tool=tool)
+ try:
+ if not client.connect():
+ tool.reply("❌ SSH连接失败,请检查设备地址和认证信息")
+ return False
+ tool.log(logging.INFO, "🚀 开始执行重启操作...")
+ result = client.send_reboot_command(command)
+ if result:
+ tool.reply("✅ 重启操作完成!")
+ tool.reply("📱 设备正在重启中,请稍后检查设备状态")
+ time.sleep(3)
+ if not client.is_connected():
+ tool.log(logging.INFO, "🔌 SSH连接已断开,确认设备正在重启")
+ else:
+ tool.log(logging.WARNING, "⚠️ SSH连接仍在,设备可能未重启成功")
+ return True
+ else:
+ tool.reply("❌ 重启命令执行失败!")
+ return False
+ except Exception as e:
+ tool.log(logging.ERROR, f"程序执行出错: {e}")
+ tool.reply("❌ 程序执行出错,请检查配置和网络连接")
+ return False
+ finally:
+ client.close()
+ tool.log(logging.INFO, "🏁 脚本执行完毕")
+
+
+def reboot_device(tool: TOOL, device_config: dict) -> bool:
+ """根据设备类型执行重启,返回是否成功"""
+ device_type = device_config.get('type')
+ if device_type == 'adb':
+ return run_adb_reboot(tool, device_config)
+ if device_type == 'ssh':
+ return run_ssh_reboot(tool, device_config)
+ tool.reply("❌ 未知设备类型")
+ return False
+
+
+def cmd_rb_now(tool: TOOL, user_id: str, is_admin: bool):
+ """rb-now 命令:显示设备选单并执行重启"""
+ device_list = get_device_list(user_id, is_admin)
+
+ if not device_list:
+ tool.reply("❌ 暂无可用设备,请联系管理员添加设备")
+ return
+
+ # 显示设备选单(只显示名称),0 为全部重启
+ menu_text = "请选择要重启的设备:\n0. 重启所有设备\n"
+ for i, device in enumerate(device_list, 1):
+ menu_text += f"{i}. {device['name']}\n"
+
+ tool.reply(menu_text)
+
+ # 等待用户选择
+ try:
+ choice = sender.listen(60000)
+ if not choice or choice == 'q' or choice == 'error':
+ tool.reply("已取消")
+ return
+
+ # 选择 0 重启所有设备
+ if choice == '0':
+ devices = get_all_devices()
+ total = len(device_list)
+ success = 0
+ fail = 0
+ for item in device_list:
+ cfg = devices.get(item['id'])
+ if not cfg:
+ fail += 1
+ continue
+ # 再次验证权限,确保安全
+ if not has_device_permission(cfg, user_id, is_admin):
+ fail += 1
+ continue
+ ok = reboot_device(tool, cfg)
+ if ok:
+ success += 1
+ else:
+ fail += 1
+ tool.reply(f"执行完成:成功 {success}/{total},失败 {fail}/{total}")
+ return
+
+ choice_num = int(choice)
+ if 1 <= choice_num <= len(device_list):
+ selected_device = device_list[choice_num - 1]
+ device_id = selected_device['id']
+ devices = get_all_devices()
+ device_config = devices.get(device_id)
+ if not device_config:
+ tool.reply("❌ 设备配置不存在")
+ return
+ # 再次验证权限,确保安全
+ if not has_device_permission(device_config, user_id, is_admin):
+ tool.reply("❌ 您没有权限重启此设备")
+ return
+ reboot_device(tool, device_config)
+ else:
+ tool.reply("❌ 无效选择")
+ except ValueError:
+ tool.reply("❌ 请输入有效数字")
+ except Exception as e:
+ tool.reply(f"❌ 选择设备时出错: {e}")
+
+
+def cmd_rb_manage(tool: TOOL, user_id: str, is_admin: bool):
+ """rb管理 命令:设备管理(仅管理员可用)"""
+ if not is_admin:
+ tool.reply("❌ 此命令仅管理员可使用")
+ return
+
+ device_list = get_device_list(user_id, is_admin)
+
+ if not device_list:
+ tool.reply("设备管理菜单:\n1. 添加设备\n0. 返回")
+ try:
+ choice = sender.listen(60000)
+ if not choice or choice == '0' or choice == 'q' or choice == 'error':
+ tool.reply("已返回")
+ return
+ if choice == '1':
+ add_device(tool)
+ else:
+ tool.reply("❌ 无效选择")
+ except Exception as e:
+ tool.reply(f"❌ 操作出错: {e}")
+ return
+
+ # 显示设备列表和管理选项(仅名称),提示 + 和 - 用法
+ menu_text = "设备管理菜单:\n"
+ for i, device in enumerate(device_list, 1):
+ menu_text += f"{i}. {device['name']}\n"
+ menu_text += "+ (添加设备)\n- 序号 (删除设备)\n0. 返回"
+
+ tool.reply(menu_text)
+
+ try:
+ choice = sender.listen(60000)
+ if not choice or choice == '0' or choice == 'q' or choice == 'error':
+ tool.reply("已返回")
+ return
+
+ if choice == '+':
+ add_device(tool)
+ elif choice.startswith('-'):
+ # 删除设备:-序号
+ try:
+ device_num = int(choice[1:])
+ if 1 <= device_num <= len(device_list):
+ delete_device_by_number(tool, device_list, device_num - 1)
+ else:
+ tool.reply("❌ 无效的设备序号")
+ except ValueError:
+ tool.reply("❌ 请输入正确的格式:-序号")
+ elif choice.isdigit():
+ # 查看/修改设备:序号
+ device_num = int(choice)
+ if 1 <= device_num <= len(device_list):
+ view_edit_device(tool, device_list, device_num - 1)
+ else:
+ tool.reply("❌ 无效的设备序号")
+ else:
+ tool.reply("❌ 无效选择")
+ except Exception as e:
+ tool.reply(f"❌ 操作出错: {e}")
+
+
+def add_device(tool: TOOL):
+ """添加设备"""
+ tool.reply("请选择设备类型:\n1. ADB设备\n2. SSH设备\n0. 取消")
+
+ try:
+ type_choice = sender.listen(60000)
+ if not type_choice or type_choice == '0' or type_choice == 'q' or type_choice == 'error':
+ tool.reply("已取消")
+ return
+
+ if type_choice == '1':
+ add_adb_device(tool)
+ elif type_choice == '2':
+ add_ssh_device(tool)
+ else:
+ tool.reply("❌ 无效选择")
+ except Exception as e:
+ tool.reply(f"❌ 添加设备出错: {e}")
+
+
+def add_adb_device(tool: TOOL):
+ """添加ADB设备"""
+ tool.reply("请输入设备名称(q:退出)")
+ name = sender.listen(60000)
+ if not name or name == 'q' or name == 'error':
+ tool.reply("已退出")
+ return
+
+ tool.reply("请输入ADB地址(示例:192.168.1.100:5555,q:退出)")
+ address = sender.listen(60000)
+ if not address or address == 'q' or address == 'error':
+ tool.reply("已退出")
+ return
+
+ # 设置设备权限
+ tool.reply("请设置设备权限:\n1. 仅管理员可用\n2. 所有用户可用\n3. 指定用户可用\n0. 取消")
+ perm_choice = sender.listen(60000)
+ if not perm_choice or perm_choice == '0' or perm_choice == 'q' or perm_choice == 'error':
+ tool.reply("已取消")
+ return
+
+ device_config = {
+ 'type': 'adb',
+ 'name': name,
+ 'address': address,
+ 'admin_only': False,
+ 'allowed_users': []
+ }
+
+ if perm_choice == '1':
+ device_config['admin_only'] = True
+ elif perm_choice == '2':
+ device_config['admin_only'] = False
+ device_config['allowed_users'] = [] # 空列表表示所有用户可用
+ elif perm_choice == '3':
+ tool.reply("请输入允许访问的用户ID(多个用户用逗号分隔,q:退出)")
+ users_input = sender.listen(60000)
+ if not users_input or users_input == 'q' or users_input == 'error':
+ tool.reply("已取消")
+ return
+ allowed_users = [u.strip() for u in users_input.split(',') if u.strip()]
+ device_config['admin_only'] = False
+ device_config['allowed_users'] = allowed_users
+ else:
+ tool.reply("❌ 无效选择,已取消")
+ return
+
+ # 生成设备ID
+ device_id = f"adb_{int(time.time())}"
+
+ save_device(device_id, device_config)
+ tool.reply(f"✅ ADB设备 '{name}' 添加成功!")
+
+
+def add_ssh_device(tool: TOOL):
+ """添加SSH设备"""
+ tool.reply("请输入设备名称(q:退出)")
+ name = sender.listen(60000)
+ if not name or name == 'q' or name == 'error':
+ tool.reply("已退出")
+ return
+
+ tool.reply("请输入SSH主机地址(示例:192.168.1.200,q:退出)")
+ hostname = sender.listen(60000)
+ if not hostname or hostname == 'q' or hostname == 'error':
+ tool.reply("已退出")
+ return
+
+ tool.reply("请输入SSH用户名(默认root,直接回车跳过)")
+ username = sender.listen(60000)
+ if not username or username == 'q' or username == 'error':
+ username = 'root'
+
+ tool.reply("请输入SSH密码(q:退出)")
+ password = sender.listen(60000)
+ if not password or password == 'q' or password == 'error':
+ tool.reply("已退出")
+ return
+
+ tool.reply("请输入SSH端口(默认22,直接回车跳过)")
+ port_str = sender.listen(60000)
+ port = 22
+ if port_str and port_str.isdigit():
+ port = int(port_str)
+
+ tool.reply("请输入重启命令(默认reboot,直接回车跳过)")
+ command = sender.listen(60000)
+ if not command or command == 'q' or command == 'error':
+ command = 'reboot'
+
+ tool.reply("请输入超时时间(默认30秒,直接回车跳过)")
+ timeout_str = sender.listen(60000)
+ timeout = 30
+ if timeout_str and timeout_str.isdigit():
+ timeout = int(timeout_str)
+
+ # 设置设备权限
+ tool.reply("请设置设备权限:\n1. 仅管理员可用\n2. 所有用户可用\n3. 指定用户可用\n0. 取消")
+ perm_choice = sender.listen(60000)
+ if not perm_choice or perm_choice == '0' or perm_choice == 'q' or perm_choice == 'error':
+ tool.reply("已取消")
+ return
+
+ device_config = {
+ 'type': 'ssh',
+ 'name': name,
+ 'hostname': hostname,
+ 'username': username,
+ 'password': password,
+ 'port': port,
+ 'command': command,
+ 'timeout': timeout,
+ 'admin_only': False,
+ 'allowed_users': []
+ }
+
+ if perm_choice == '1':
+ device_config['admin_only'] = True
+ elif perm_choice == '2':
+ device_config['admin_only'] = False
+ device_config['allowed_users'] = [] # 空列表表示所有用户可用
+ elif perm_choice == '3':
+ tool.reply("请输入允许访问的用户ID(多个用户用逗号分隔,q:退出)")
+ users_input = sender.listen(60000)
+ if not users_input or users_input == 'q' or users_input == 'error':
+ tool.reply("已取消")
+ return
+ allowed_users = [u.strip() for u in users_input.split(',') if u.strip()]
+ device_config['admin_only'] = False
+ device_config['allowed_users'] = allowed_users
+ else:
+ tool.reply("❌ 无效选择,已取消")
+ return
+
+ # 生成设备ID
+ device_id = f"ssh_{int(time.time())}"
+
+ save_device(device_id, device_config)
+ tool.reply(f"✅ SSH设备 '{name}' 添加成功!")
+
+
+
+
+def show_device_list(tool: TOOL):
+ """显示设备列表"""
+ device_list = get_device_list()
+
+ if not device_list:
+ tool.reply("❌ 暂无设备")
+ return
+
+ text = "当前设备列表:\n"
+ for i, device in enumerate(device_list, 1):
+ text += f"{i}. {device['name']}\n"
+
+ tool.reply(text)
+
+
+def view_edit_device(tool: TOOL, device_list: list, device_index: int):
+ """查看/编辑设备配置"""
+ selected_device = device_list[device_index]
+ device_id = selected_device['id']
+ device_name = selected_device['name']
+ device_type = selected_device['type']
+
+ # 获取设备详细配置
+ devices = get_all_devices()
+ device_config = devices.get(device_id)
+
+ if not device_config:
+ tool.reply("❌ 设备配置不存在")
+ return
+
+ # 显示设备配置
+ config_text = f"设备配置详情:\n"
+ config_text += f"设备名称: {device_config.get('name', '未知')}\n"
+ config_text += f"设备类型: {device_config.get('type', '未知')}\n"
+
+ # 显示权限信息
+ admin_only = device_config.get('admin_only', False)
+ allowed_users = device_config.get('allowed_users', [])
+ if isinstance(allowed_users, str):
+ allowed_users = [u.strip() for u in allowed_users.split(',') if u.strip()]
+ if not isinstance(allowed_users, list):
+ allowed_users = []
+
+ if admin_only:
+ config_text += f"权限设置: 仅管理员可用\n"
+ elif not allowed_users:
+ config_text += f"权限设置: 所有用户可用\n"
+ else:
+ config_text += f"权限设置: 指定用户可用 ({', '.join(allowed_users)})\n"
+
+ if device_type == 'ADB':
+ config_text += f"ADB地址: {device_config.get('address', '未设置')}\n"
+ elif device_type == 'SSH':
+ config_text += f"SSH主机: {device_config.get('hostname', '未设置')}\n"
+ config_text += f"SSH用户: {device_config.get('username', 'root')}\n"
+ config_text += f"SSH密码: {'******' if device_config.get('password') else '未设置'}\n"
+ config_text += f"SSH端口: {device_config.get('port', 22)}\n"
+ config_text += f"重启命令: {device_config.get('command', 'reboot')}\n"
+ config_text += f"超时时间: {device_config.get('timeout', 30)}秒\n"
+
+ config_text += "\n操作选项:\n1. 修改配置\n2. 删除设备\n0. 返回"
+
+ tool.reply(config_text)
+
+ try:
+ choice = sender.listen(60000)
+ if not choice or choice == '0' or choice == 'q' or choice == 'error':
+ tool.reply("已返回")
+ return
+
+ if choice == '1':
+ edit_device_config(tool, device_id, device_config)
+ elif choice == '2':
+ # 确认删除
+ tool.reply(f"确认删除设备 '{device_name}' 吗?(y/n)")
+ confirm = sender.listen(30000)
+ if confirm and confirm.lower() in ['y', 'yes', '是']:
+ delete_device(device_id)
+ tool.reply(f"✅ 设备 '{device_name}' 已删除")
+ else:
+ tool.reply("已取消删除")
+ else:
+ tool.reply("❌ 无效选择")
+ except Exception as e:
+ tool.reply(f"❌ 操作出错: {e}")
+
+
+def edit_device_config(tool: TOOL, device_id: str, device_config: dict):
+ """编辑设备配置"""
+ device_type = device_config.get('type')
+ device_name = device_config.get('name', '未知设备')
+
+ tool.reply(f"正在编辑设备 '{device_name}' 的配置...")
+
+ if device_type == 'adb':
+ edit_adb_device_config(tool, device_id, device_config)
+ elif device_type == 'ssh':
+ edit_ssh_device_config(tool, device_id, device_config)
+ else:
+ tool.reply("❌ 未知设备类型")
+
+
+def edit_adb_device_config(tool: TOOL, device_id: str, device_config: dict):
+ """编辑ADB设备配置"""
+ tool.reply("请选择要修改的配置项:\n1. 设备名称\n2. ADB地址\n3. 权限设置\n0. 返回")
+
+ try:
+ choice = sender.listen(60000)
+ if not choice or choice == '0' or choice == 'q' or choice == 'error':
+ tool.reply("已返回")
+ return
+
+ if choice == '1':
+ tool.reply("请输入新的设备名称(q:退出)")
+ new_name = sender.listen(60000)
+ if new_name and new_name != 'q' and new_name != 'error':
+ device_config['name'] = new_name
+ save_device(device_id, device_config)
+ tool.reply(f"✅ 设备名称已更新为: {new_name}")
+ else:
+ tool.reply("已取消")
+ elif choice == '2':
+ tool.reply("请输入新的ADB地址(示例:192.168.1.100:5555,q:退出)")
+ new_address = sender.listen(60000)
+ if new_address and new_address != 'q' and new_address != 'error':
+ device_config['address'] = new_address
+ save_device(device_id, device_config)
+ tool.reply(f"✅ ADB地址已更新为: {new_address}")
+ else:
+ tool.reply("已取消")
+ elif choice == '3':
+ tool.reply("请设置设备权限:\n1. 仅管理员可用\n2. 所有用户可用\n3. 指定用户可用\n0. 取消")
+ perm_choice = sender.listen(60000)
+ if not perm_choice or perm_choice == '0' or perm_choice == 'q' or perm_choice == 'error':
+ tool.reply("已取消")
+ return
+
+ if perm_choice == '1':
+ device_config['admin_only'] = True
+ device_config['allowed_users'] = []
+ save_device(device_id, device_config)
+ tool.reply("✅ 权限已设置为:仅管理员可用")
+ elif perm_choice == '2':
+ device_config['admin_only'] = False
+ device_config['allowed_users'] = []
+ save_device(device_id, device_config)
+ tool.reply("✅ 权限已设置为:所有用户可用")
+ elif perm_choice == '3':
+ tool.reply("请输入允许访问的用户ID(多个用户用逗号分隔,q:退出)")
+ users_input = sender.listen(60000)
+ if not users_input or users_input == 'q' or users_input == 'error':
+ tool.reply("已取消")
+ return
+ allowed_users = [u.strip() for u in users_input.split(',') if u.strip()]
+ device_config['admin_only'] = False
+ device_config['allowed_users'] = allowed_users
+ save_device(device_id, device_config)
+ tool.reply(f"✅ 权限已设置为:指定用户可用 ({', '.join(allowed_users)})")
+ else:
+ tool.reply("❌ 无效选择")
+ else:
+ tool.reply("❌ 无效选择")
+ except Exception as e:
+ tool.reply(f"❌ 操作出错: {e}")
+
+
+def edit_ssh_device_config(tool: TOOL, device_id: str, device_config: dict):
+ """编辑SSH设备配置"""
+ tool.reply("请选择要修改的配置项:\n1. 设备名称\n2. SSH主机\n3. SSH用户\n4. SSH密码\n5. SSH端口\n6. 重启命令\n7. 超时时间\n8. 权限设置\n0. 返回")
+
+ try:
+ choice = sender.listen(60000)
+ if not choice or choice == '0' or choice == 'q' or choice == 'error':
+ tool.reply("已返回")
+ return
+
+ if choice == '1':
+ tool.reply("请输入新的设备名称(q:退出)")
+ new_name = sender.listen(60000)
+ if new_name and new_name != 'q' and new_name != 'error':
+ device_config['name'] = new_name
+ save_device(device_id, device_config)
+ tool.reply(f"✅ 设备名称已更新为: {new_name}")
+ else:
+ tool.reply("已取消")
+ elif choice == '2':
+ tool.reply("请输入新的SSH主机地址(q:退出)")
+ new_hostname = sender.listen(60000)
+ if new_hostname and new_hostname != 'q' and new_hostname != 'error':
+ device_config['hostname'] = new_hostname
+ save_device(device_id, device_config)
+ tool.reply(f"✅ SSH主机已更新为: {new_hostname}")
+ else:
+ tool.reply("已取消")
+ elif choice == '3':
+ tool.reply("请输入新的SSH用户名(q:退出)")
+ new_username = sender.listen(60000)
+ if new_username and new_username != 'q' and new_username != 'error':
+ device_config['username'] = new_username
+ save_device(device_id, device_config)
+ tool.reply(f"✅ SSH用户已更新为: {new_username}")
+ else:
+ tool.reply("已取消")
+ elif choice == '4':
+ tool.reply("请输入新的SSH密码(q:退出)")
+ new_password = sender.listen(60000)
+ if new_password and new_password != 'q' and new_password != 'error':
+ device_config['password'] = new_password
+ save_device(device_id, device_config)
+ tool.reply("✅ SSH密码已更新")
+ else:
+ tool.reply("已取消")
+ elif choice == '5':
+ tool.reply("请输入新的SSH端口(q:退出)")
+ new_port = sender.listen(60000)
+ if new_port and new_port != 'q' and new_port != 'error':
+ if new_port.isdigit():
+ device_config['port'] = int(new_port)
+ save_device(device_id, device_config)
+ tool.reply(f"✅ SSH端口已更新为: {new_port}")
+ else:
+ tool.reply("❌ 端口必须是数字")
+ else:
+ tool.reply("已取消")
+ elif choice == '6':
+ tool.reply("请输入新的重启命令(q:退出)")
+ new_command = sender.listen(60000)
+ if new_command and new_command != 'q' and new_command != 'error':
+ device_config['command'] = new_command
+ save_device(device_id, device_config)
+ tool.reply(f"✅ 重启命令已更新为: {new_command}")
+ else:
+ tool.reply("已取消")
+ elif choice == '7':
+ tool.reply("请输入新的超时时间(秒,q:退出)")
+ new_timeout = sender.listen(60000)
+ if new_timeout and new_timeout != 'q' and new_timeout != 'error':
+ if new_timeout.isdigit():
+ device_config['timeout'] = int(new_timeout)
+ save_device(device_id, device_config)
+ tool.reply(f"✅ 超时时间已更新为: {new_timeout}秒")
+ else:
+ tool.reply("❌ 超时时间必须是数字")
+ else:
+ tool.reply("已取消")
+ elif choice == '8':
+ tool.reply("请设置设备权限:\n1. 仅管理员可用\n2. 所有用户可用\n3. 指定用户可用\n0. 取消")
+ perm_choice = sender.listen(60000)
+ if not perm_choice or perm_choice == '0' or perm_choice == 'q' or perm_choice == 'error':
+ tool.reply("已取消")
+ return
+
+ if perm_choice == '1':
+ device_config['admin_only'] = True
+ device_config['allowed_users'] = []
+ save_device(device_id, device_config)
+ tool.reply("✅ 权限已设置为:仅管理员可用")
+ elif perm_choice == '2':
+ device_config['admin_only'] = False
+ device_config['allowed_users'] = []
+ save_device(device_id, device_config)
+ tool.reply("✅ 权限已设置为:所有用户可用")
+ elif perm_choice == '3':
+ tool.reply("请输入允许访问的用户ID(多个用户用逗号分隔,q:退出)")
+ users_input = sender.listen(60000)
+ if not users_input or users_input == 'q' or users_input == 'error':
+ tool.reply("已取消")
+ return
+ allowed_users = [u.strip() for u in users_input.split(',') if u.strip()]
+ device_config['admin_only'] = False
+ device_config['allowed_users'] = allowed_users
+ save_device(device_id, device_config)
+ tool.reply(f"✅ 权限已设置为:指定用户可用 ({', '.join(allowed_users)})")
+ else:
+ tool.reply("❌ 无效选择")
+ else:
+ tool.reply("❌ 无效选择")
+ except Exception as e:
+ tool.reply(f"❌ 操作出错: {e}")
+
+
+def delete_device_by_number(tool: TOOL, device_list: list, device_index: int):
+ """按序号删除设备"""
+ selected_device = device_list[device_index]
+ device_id = selected_device['id']
+ device_name = selected_device['name']
+
+ # 确认删除
+ tool.reply(f"确认删除设备 '{device_name}' 吗?(y/n)")
+ try:
+ confirm = sender.listen(30000)
+ if confirm and confirm.lower() in ['y', 'yes', '是']:
+ delete_device(device_id)
+ tool.reply(f"✅ 设备 '{device_name}' 已删除")
+ else:
+ tool.reply("已取消删除")
+ except Exception as e:
+ tool.reply(f"❌ 删除设备时出错: {e}")
+
+
+def _safe_get_message():
+ try:
+ return sender.getMessage().strip('"')
+ except Exception:
+ return ""
+
+
+if __name__ == "__main__":
+ tool = TOOL()
+ msg = _safe_get_message()
+
+ # 获取用户信息
+ try:
+ user_id = sender.getUserID()
+ is_admin = sender.isAdmin()
+ except Exception:
+ user_id = ""
+ is_admin = False
+
+ if "rb-now" in msg:
+ cmd_rb_now(tool, user_id, is_admin)
+ sys.exit(0)
+
+ if "rb管理" in msg:
+ cmd_rb_manage(tool, user_id, is_admin)
+ sys.exit(0)
+
+ sender.reply("指令不支持,请发送:rb-now 或 rb管理")
+