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管理") +