├── LICENSE ├── README.md ├── nonebot_plugin_ncupdate ├── __init__.py ├── config.py ├── dialog.py ├── info.py ├── mode.json ├── notice.py ├── restart.py ├── restart_12.py ├── sysexec.py ├── unzip.py └── version.py └── pyproject.toml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 tianyisama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nonebot-plugin-ncupdate 2 | 管理nc的自动懒人插件,支持Windows与Linux,支持ws断连自动重启napcat 3 | 4 | # ⚠警告 5 | 本插件含有大量屎山代码 6 | 7 | 谨记非必要不更新的道理 8 | 9 | 建议都更新到2.4.6及以上的napcat版本,然后使用下方省流版安装 10 | 11 | Linux建议使用一键脚本,因为手动安装很麻烦,快速部署方式详见[Napcat官方文档](https://napcat.napneko.icu/guide/boot/Shell) 12 | 13 | 不想点进去?球球你了,去看看吧,雪雪真的很可爱~ 14 | 15 |
16 | 不看不看吧😭😭😭 17 | 使用以下代码安装napcat(Linux 一键脚本(适用于 Ubuntu 20+/Debian 10+/Centos 9)) 18 | 19 | curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && sudo bash napcat.sh 20 | 21 | 22 |
23 | 24 | linux断线重连仅支持xvfb法启动的、screen窗口名为napcat的方式 25 | 26 | 如果你不懂,请使用以下代码来启动napcat(其中123456789替换为你实际的机器人账号) 27 | 28 | `screen -dmS napcat bash -c "xvfb-run -a qq --no-sandbox -q 123456789"` 29 | 30 | ## 不想看一长串的省流版 31 |
32 | Windows 33 | 34 | - 不要使用server2012,换个新点的系统 35 | - 确保QQ版本大于等于[28060](https://dldir1.qq.com/qqfile/qq/QQNT/592d67a6/QQ9.9.16.29271_x64.exe) 36 | - 下载[napcat新版](https://github.com/NapNeko/NapCatQQ/releases/download/v3.6.5/NapCat.Shell.zip)到C盘(记得下载napcat.shell的版本) 37 | - 解压napcat压缩包,确保C:\NapCat.Shell下就是napcat的文件,比如含有napcat.mjs这个文件 38 | - 填写nb的.env.*文件(如果是win10,nc_restart_way值为5,win11就写6) 39 | ```ini 40 | base_path=C:\\ 41 | topfolder=NapCat.Shell 42 | napcat_mode=win 43 | nc_reconnect=true 44 | nc_restart_way=5 45 | ``` 46 | - 按照nb和nc的启动方式启动连接即可正常使用 47 | - 有更多需求,比如需要配置代理可以往后看配置项 48 |
49 | 50 |
51 | Linux 52 | 53 | - 不要使用centos7或8,换个新点的系统比如Ubuntu 54 | - 使用一键安装脚本`curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && sudo bash napcat.sh`是否使用shell安装选择是(y) 55 | - 填写nb的.env.*文件 56 | ```ini 57 | base_path=/opt/QQ/resources/app/app_launcher 58 | topfolder=napcat 59 | napcat_mode=linux 60 | nc_reconnect=true 61 | nc_restart_way=7 62 | ``` 63 | - 按照nb和nc的启动方式启动连接即可正常使用 64 | - 有更多需求,比如需要配置代理可以往后看配置项 65 |
66 | 67 |
68 | termux 69 | 70 | - 没试过不知道 71 | 72 |
73 | 74 | ## 常见问题 75 |
76 | 1.我用的Linux一键安装脚本,不知道napcat安装在哪里 77 | 78 | 一般来说在/opt/QQ/resources/app/app_launcher/napcat,如果是这样的话,base_path填写/opt/QQ/resources/app/app_launcher,topfolder填写napcat即可 79 | 80 |
81 | 82 |
83 | 2.我发指令没反应 84 | 85 | 请检查你是否在.env.*中填写了正确的superusers和command_start 86 | 87 |
88 | 89 |
90 | 3.指令太多记不住 91 | 92 | 发送nc帮助查看所有指令 93 | 94 |
95 | 96 | 97 | ## 更新 98 | ### 11.15 99 | - 适配了v4及其以后的版本(大概)~~怎么更新这么快啊~~ 100 | - 优化掉了两行答辩 101 | ### 10.19 102 | - 添加了v3的适配 103 | ### 10.8 104 | - 新增Linux断线重连 105 | - 增加了断线短暂等待,以防协议端抽风造成短时间内重连引起的死循环 106 | ### 10.6 107 | - Windows断线重连支持所有方式(除了1) 108 | - 重塑了部分史的形状 109 | ### 10.5 110 | - 添加了2.4.6及以上版本的登陆方式(launcher.bat登录法) 111 | ### 10.1 112 | - 添加了Linux查看qq版本(支持Ubuntu,Centos,Rocky,Debian) 113 | ### 9.16 114 | - 添加了2.x版本的适配 115 | - 移除了配置项`nc_http_port`,新增了配置项`nc_restart_way` `nc_self_qq_version` `nc_self_restart` 116 | - 新增了指令`nc检查更新` `查看qq/QQ版本` `柚子检查更新` `柚子查看qq版本`,指令`(柚子)更新nc`新增可指定版本,例如`(柚子)更新nc1.8.2` 117 | - 新增内置判断napcat是否与ntqq兼容,不兼容会终止更新 118 | - 新增多种可选的启动方式 119 | ### 7.29 120 | - 由于nb端改代码导致的reload也会触发重连,而且被硬控十秒(nb会在执行完代码后再关闭)。欸🤓👆,我有个好点子 121 | - 添加了自动重连的窗口,可选择是否立即重启或取消重启 122 | ### 7.14 123 | - 适配了9.9.12版本的ntqq 124 | ### 6.28 125 | - 修复了由于nc突然支持win32导致的win64位下载出错的问题. ~~说好的没有支持win32的打算呢(话说真的还有人在用win32吗)~~ 126 | - 新增win系统cmd闪退发起重连(手动关掉cmd也会触发重连请求) 127 | - 新增断线10s后再发起重连 128 | 129 | ## 安装 130 |
131 | 使用nb-cli安装(推荐) 132 | 133 | 134 | nb plugin install nonebot-plugin-ncupdate 135 | 136 | 137 |
138 | 139 |
140 | 使用PIP安装 141 | 142 | 143 | pip install nonebot-plugin-ncupdate 144 | 145 | 若安装了虚拟环境,请在虚拟环境中操作。安装完成后,请在你的`bot.py`文件中添加以下代码来导入插件: 146 | `nonebot.load_plugin("nonebot_plugin_ncupdate")` 147 |
148 | 149 | ## 说明 150 | 151 | 152 | 支持自身触发更新或重启,支持选择代理,支持获取QQ版本且自行判断是否适用新版napcat(目前判断到QQ版本【28060】附近) 153 | 154 | 支持断线重连(默认关闭,~~目前仅支持Windows,且只支持初版bat登录法和way03方法~~) 155 | 156 | ### 指令 157 | 158 | - 指令皆只有超级用户或自身可用 159 | 160 | - 更新nc 161 | 162 | - 重启nc 163 | 164 | - 查看qq(QQ)版本/qqv 165 | 166 | - nc检查更新 167 | 168 | - 柚子更新nc(自身作为bot触发的更新指令) 169 | 170 | - 柚子重启nc(自身作为bot触发的重启指令) 171 | 172 | 如果你需要使用`柚子更新nc`和`柚子更新nc`,则应当在nc的账号配置文件里打开自身消息上报(设置为true) 173 | ```json 174 | "reportSelfMessage": true, 175 | ``` 176 | ### 配置项 177 | 178 | > 以下配置项可在 `.env.*` 文件中设置,具体参考 [NoneBot 配置方式](https://nonebot.dev/docs/appendices/config) 179 | 180 | #### `nc_restart_way` (重要新增) 181 | 182 | - 默认:`1` 183 | - 说明:napcat触发更新或重启时的重启方式 184 | - 可选: 185 | 186 | 1.onebot接口的重启方式,部分napcat版本接口是坏的,~~Linux只可选用此方式(因为其他的没写)~~ 187 | 188 | 2.旧时代版本napcat-utf8.bat的启动方式,QQ版本9.12之后此方法已失效 189 | 190 | 3.way03:QQ.exe的启动方式,需要更改qq文件并配置补丁,具体参考 [way03启动方式](https://napneko.github.io/zh-CN/guide/boot/shell/BootWay03) 191 | 192 | 4.way05:ps1的启动方式,无需更改文件但需要替换补丁,具体参考 [way05启动方式](https://napneko.github.io/zh-CN/guide/boot/shell/BootWay05) 193 | 194 | 5.launcher-win10.bat:Napcat2.4.6版本及以上的Windows10(及以下)的登录方式 195 | 196 | 6.launcher.bat:Napcat2.4.6版本及以上的Windows11的登录方式 197 | 198 | 7.xvfb-run: Linux的启动方法,忘了是哪个版本开始支持的了 199 | 200 | - 必填:否 201 | - 警告:3和4的启动方式只可选择一个(因为启用了way05后,way03方法会失效) 202 | 203 | #### `base_path` 204 | 205 | - 默认:`C:\\napcat` 206 | - 说明:napcat运行目录的上级目录路径,例如原运行于`E:\111\NapCat.win32.x64`,则填写`E:\111` 207 | - 必填:否 208 | 209 | #### `topfolder` 210 | 211 | - 默认:`NapCat.win32.x64` 212 | - 说明:napcat运行目录的顶级目录名称,例如原运行于`E:\111\NapCat`,则填写`NapCat` 213 | - 必填:否 214 | 215 | #### `napcat_mode` 216 | 217 | - 默认:`win` 218 | - 说明:napcat的运行系统类型 219 | - 可选:win,win_32,linux,linux_arm 220 | - 必填:否 221 | 222 | #### `nc_proxy` 223 | 224 | - 默认:`false` 225 | - 说明:是否通过代理请求GitHub更新 226 | - 可选:true,false 227 | - 必填:否 228 | 229 | #### `nc_proxy_port` 230 | 231 | - 默认:`11451` 232 | - 说明:代理使用的端口 233 | - 必填:否 234 | 235 | #### `nc_self_update` 236 | 237 | - 默认:`"柚子更新nc"` 238 | - 说明:当bot是自己的时候触发的更新指令 239 | - 必填:否 240 | 241 | #### `nc_self_restart` 242 | 243 | - 默认:`"柚子重启nc"` 244 | - 说明:当bot是自己的时候触发的重启指令 245 | - 必填:否 246 | 247 | #### `nc_self_check_update` 248 | 249 | - 默认:`"柚子检查更新"` 250 | - 说明:当bot是自己的时候触发的检查更新 251 | - 必填:否 252 | 253 | #### `nc_self_qq_version` 254 | 255 | - 默认:`"柚子查看qq版本"` 256 | - 说明:当bot是自己的时候触发的查看qq版本 257 | - 必填:否 258 | 259 | #### `nc_reconnect` 260 | 261 | - 默认:`false` 262 | - 说明:是否开启napcat掉线重连(目前只支持Windows) 263 | - 可选:true,false 264 | - 必填:否 265 | 266 | ### 配置示例 267 | > 只有要用到的才填写,如果用不到或者不知道怎么设置,把你的napcat运行目录变成`C:\napcat\NapCat.win32.x64`就可以了 268 | #### Windows配置示例 269 | ```ini 270 | base_path=C:\\ 271 | topfolder=NapCat 272 | napcat_mode=win 273 | nc_proxy=true 274 | nc_proxy_port=11451 275 | nc_self_update="橘子更新nc" 276 | nc_self_restart="橘子重启nc" 277 | nc_reconnect=true 278 | nc_self_check_update="柚子检查更新" 279 | nc_self_qq_version="柚子查看qq版本" 280 | nc_restart_way=1 281 | ``` 282 | 如果你使用的是9.9.12版本的ntqq,那么他应该类似于这样 283 | ```ini 284 | base_path=D:\qqnt\resources\app\app_launcher 285 | topfolder=napcat 286 | ``` 287 | #### Linux配置示例 288 | ```ini 289 | base_path=/root 290 | topfolder=NapCat.linux.x64 291 | napcat_mode=linux 292 | nc_proxy=true 293 | nc_proxy_port=11451 294 | nc_self_update="橘子更新nc" 295 | nc_self_restart="橘子重启nc" 296 | nc_reconnect=false 297 | nc_self_check_update="柚子检查更新" 298 | nc_self_qq_version="柚子查看qq版本" 299 | nc_restart_way=1 300 | ``` 301 | ## 挖坑 302 | - ~~准备实现linux断线重连和相关功能~~ 303 | - 准备增加初始一键安装napcat 304 | - ~~准备将最新的启动方式加进去~~ 305 | - 准备增加qq指令更新 306 | 307 | ## 致谢 308 | 309 | 310 | - [Napcat](https://github.com/NapNeko/NapCatQQ) 311 | -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command, get_driver, on 2 | from nonebot.adapters.onebot.v11 import Bot, Event, MessageSegment, Message,GroupMessageEvent 3 | from nonebot.permission import SUPERUSER 4 | from nonebot.exception import FinishedException 5 | from nonebot.plugin import PluginMetadata 6 | from nonebot.params import CommandArg 7 | from .config import Config, config 8 | from .version import ciallo, get_qq_version_info, qq_version 9 | from .dialog import tkinter_dialog 10 | from .restart import BotRestarter 11 | from .unzip import unzip_v1, unzip_v2 12 | import httpx 13 | import aiofiles 14 | import os 15 | import nonebot 16 | import json 17 | import platform 18 | import asyncio 19 | 20 | __plugin_meta__ = PluginMetadata( 21 | name="指令更新NapCat", 22 | description="指令更新NapCat", 23 | usage="""更新nc: 更新napcat并自动重启 24 | 重启nc: 重新启动napcat 25 | 柚子更新nc: 自己作为机器人触发的更新 26 | 柚子重启nc: 自己作为机器人触发的重启""", 27 | type="application", 28 | homepage="https://github.com/tianyisama/nonebot-plugin-ncupdate", 29 | config=Config, 30 | supported_adapters={"~onebot.v11"}, 31 | ) 32 | driver = get_driver() 33 | base_path = config.base_path 34 | topfolder = config.topfolder 35 | napcat_mode = config.napcat_mode 36 | nc_proxy = config.nc_proxy 37 | nc_proxy_port = config.nc_proxy_port 38 | nc_self_update = config.nc_self_update 39 | nc_self_restart = config.nc_self_restart 40 | nc_reconnect = config.nc_reconnect 41 | nc_restart_way = config.nc_restart_way 42 | nc_self_check_update = config.nc_self_check_update 43 | nc_self_qq_version = config.nc_self_qq_version 44 | 45 | current_dir = os.path.dirname(os.path.abspath(__file__)) 46 | mode_file = os.path.join(current_dir, 'mode.json') 47 | update_nc = on_command("更新nc", priority=5, permission=SUPERUSER) 48 | restart = on_command("重启nc", priority=5, permission=SUPERUSER) 49 | help = on_command("nc帮助", priority=5, permission=SUPERUSER) 50 | update_info = on_command("nc检查更新", priority=5, permission=SUPERUSER) 51 | on_message_sent = on("message_sent", block=False) 52 | global bot_id, cnt 53 | 54 | async def create_client(): 55 | if nc_proxy: 56 | proxies = { 57 | "http://": f"http://127.0.0.1:{nc_proxy_port}", 58 | "https://": f"http://127.0.0.1:{nc_proxy_port}", 59 | } 60 | return httpx.AsyncClient(proxies=proxies, follow_redirects=True) 61 | else: 62 | return httpx.AsyncClient(follow_redirects=True) 63 | 64 | async def get_latest_release(napcat_mode, version_info, specific_version=None): 65 | asset_keyword = { 66 | "win": "win32.x64", 67 | "win_32": "win32.ia32", 68 | "linux": "linux.x64", 69 | "linux_arm": "linux.arm64" 70 | } 71 | release_url = ( 72 | f"https://api.github.com/repos/NapNeko/NapCatQQ/releases/tags/{specific_version}" 73 | if specific_version 74 | else "https://api.github.com/repos/NapNeko/NapCatQQ/releases/latest" 75 | ) 76 | async with await create_client() as client: 77 | resp = await client.get(release_url) 78 | if resp.status_code == 404: 79 | raise ValueError(f"指定的版本 {specific_version} 不存在。") 80 | resp.raise_for_status() 81 | release_data = resp.json() 82 | latest_version = release_data["tag_name"] 83 | current_version = f"v{version_info['app_version']}" 84 | 85 | if not latest_version.startswith("v1"): 86 | asset_keyword = "NapCat.Shell.zip" 87 | else: 88 | asset_keyword = asset_keyword.get(napcat_mode) 89 | 90 | if asset := next((asset for asset in release_data["assets"] if asset_keyword in asset["name"]), None): 91 | return asset, latest_version, current_version 92 | else: 93 | raise ValueError("未找到对应的release") 94 | 95 | async def download_file(download_url, filename): 96 | async with await create_client() as client: 97 | download_resp = await client.get(download_url) 98 | download_resp.raise_for_status() 99 | file_path = os.path.join(base_path, filename) 100 | os.makedirs(os.path.dirname(file_path), exist_ok=True) 101 | async with aiofiles.open(file_path, 'wb') as file: 102 | await file.write(download_resp.content) 103 | return file_path 104 | 105 | @help.handle() 106 | async def help_(): 107 | await help.send(f"Ciallo~(∠・ω<)⌒⚡\n" 108 | "本插件可自行判断napcat版本是否与ntqq兼容。\n" 109 | "可使用如下指令:\n" 110 | "(柚子)更新nc:更新Napcat最新版。若后接具体的版本号可指定具体的版本,例如:更新nc1.8.2\n" 111 | "(柚子)重启nc,重新启动Napcat\n" 112 | "(柚子)查看qq版本/qqv:查看当前的QQ版本号\n" 113 | "nc检查更新:检查最新的版本及其更新的内容和版本要求\n" 114 | "柚子检查更新:人机合一时检查更新") 115 | 116 | @update_nc.handle() 117 | async def handle_update_nc(bot: Bot, event: Event, args: Message = CommandArg()): 118 | specific_version = args.extract_plain_text().strip() 119 | try: 120 | version_info = await bot.get_version_info() 121 | latest_version = f"v{specific_version}" if specific_version else None 122 | asset, latest_version, current_version = await get_latest_release(napcat_mode, version_info, specific_version=latest_version) 123 | 124 | if latest_version == current_version: 125 | await update_nc.finish(f"已经是最新版了~\n当前版本:{current_version}") 126 | 127 | if platform.system().lower() == 'windows': 128 | odoo = await ciallo(latest_version) 129 | if not odoo: 130 | await update_nc.finish(f"警告: NTQQ版本与该版本NapCat不兼容。\n已取消本次更新") 131 | 132 | await update_nc.send("正在更新,请稍候") 133 | download_url = asset["browser_download_url"] 134 | file_path = await download_file(download_url, asset['name']) 135 | 136 | await update_nc.send("正在执行文件替换") 137 | if latest_version.startswith("v1"): 138 | await unzip_v1(file_path, base_path, topfolder) 139 | #elif latest_version.startswith(("v2", "v3")): 140 | else: 141 | await unzip_v2(file_path, base_path, topfolder) 142 | 143 | await handle_restart(bot, event) 144 | 145 | except FinishedException: 146 | pass 147 | except ValueError as e: 148 | await update_nc.finish(str(e)) 149 | except Exception as e: 150 | await update_nc.send(f"发生错误:{e}") 151 | 152 | 153 | @restart.handle() 154 | async def handle_restart(bot: Bot, event: Event): 155 | global bot_id 156 | restarter = BotRestarter(bot_id, base_path, topfolder, disconnect=False, bot=bot, event=event, send_message=True) 157 | await restarter.restart_bot(nc_restart_way) 158 | 159 | 160 | @update_info.handle() 161 | async def handle_update_info(bot: Bot): 162 | 163 | release_url = "https://api.github.com/repos/NapNeko/NapCatQQ/releases/latest" 164 | try: 165 | async with await create_client() as client: 166 | resp = await client.get(release_url) 167 | resp.raise_for_status() 168 | release_data = resp.json() 169 | tag_name = release_data["tag_name"] 170 | body = release_data["body"] 171 | qq_version = await get_qq_version_info() 172 | version_info = await bot.get_version_info() 173 | app_version = f"v{version_info['app_version']}" 174 | if tag_name == app_version: 175 | message = f"已是最新版本,无需更新~\n当前的QQ版本:{qq_version}\n当前的NapCat版本:{app_version}" 176 | else: 177 | message = f"最新版本: {tag_name}\n更新内容:\n{body}\n当前的QQ版本:{qq_version}\n当前的NapCat版本:{app_version}" 178 | 179 | await update_info.send(message) 180 | 181 | except httpx.HTTPStatusError as e: 182 | await update_info.send(f"获取最新版本信息失败,状态码:{e.response.status_code}") 183 | except Exception as e: 184 | await update_info.send(f"发生错误:{e}") 185 | 186 | 187 | 188 | @driver.on_bot_connect 189 | async def reconnected(bot: Bot): 190 | global cnt 191 | cnt = True 192 | version_info = await bot.get_version_info() 193 | appname = version_info["app_name"] 194 | version = version_info["app_version"] 195 | message = MessageSegment.text(f"操作完成!\n当前运行框架: {appname}\n当前版本:{version}") 196 | try: 197 | async with aiofiles.open(mode_file, 'r') as f: 198 | mode_data = json.loads(await f.read()) 199 | 200 | if not mode_data or 'type' not in mode_data: 201 | pass 202 | else: 203 | if mode_data["type"] == "group": 204 | await bot.send_group_msg(group_id=mode_data["id"], message=message) 205 | else: 206 | await bot.send_private_msg(user_id=mode_data["id"], message=message) 207 | 208 | async with aiofiles.open(mode_file, 'w') as f: 209 | await f.write(json.dumps({})) 210 | except FileNotFoundError: 211 | return 212 | except Exception as e: 213 | nonebot.logger.error(f"发送操作成功消息时发生错误:{e}") 214 | 215 | global bot_id 216 | bot_id = nonebot.get_bot().self_id 217 | 218 | @driver.on_bot_disconnect 219 | async def reconnect(): 220 | global bot_id, cnt 221 | cnt = False 222 | if not nc_reconnect: 223 | nonebot.logger.info("未开启断线重连,已跳过重连请求") 224 | return 225 | try: 226 | async with aiofiles.open(mode_file, 'r') as f: 227 | mode_data = await f.read() 228 | mode_data = json.loads(mode_data) 229 | if mode_data: 230 | nonebot.logger.info("检测到指令重启,跳过重连") 231 | return 232 | except FileNotFoundError: 233 | return 234 | except Exception as e: 235 | nonebot.logger.error(f"Error reading mode.json: {e}") 236 | return 237 | 238 | nonebot.logger.info('检测到连接已断开,将在10s后自动发起重连') 239 | await asyncio.sleep(2) 240 | if cnt: 241 | nonebot.logger.warning("又好了,不用重连啦") 242 | return 243 | dialog_result = await tkinter_dialog() 244 | if dialog_result == "restart": 245 | restarter = BotRestarter(bot_id, base_path, topfolder, disconnect=True, send_message=False) 246 | await restarter.restart_bot(nc_restart_way) 247 | elif dialog_result == "cancel": 248 | nonebot.logger.info('已取消本次重连') 249 | elif dialog_result == "tkinter not available": 250 | await asyncio.sleep(8) 251 | restarter = BotRestarter(bot_id, base_path, topfolder, disconnect=True, send_message=False) 252 | await restarter.restart_bot(nc_restart_way) 253 | return 254 | 255 | 256 | @on_message_sent.handle() 257 | async def handle_message_sent(bot: Bot, event: Event): 258 | if isinstance(event, Event): 259 | if nc_self_update in event.raw_message: 260 | args_index = event.raw_message.find(nc_self_update) + len(nc_self_update) 261 | args_str = event.raw_message[args_index:].strip() 262 | args_message = Message(args_str) 263 | await handle_update_nc(bot, event, args_message) 264 | elif nc_self_restart == event.raw_message: 265 | await handle_restart(bot, event) 266 | elif nc_self_check_update == event.raw_message: 267 | await handle_update_info(bot) 268 | elif nc_self_qq_version == event.raw_message: 269 | await qq_version() 270 | 271 | 272 | -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional 3 | from nonebot import get_plugin_config 4 | 5 | class Config(BaseModel): 6 | base_path: Optional[str] = "C:\\napcat" 7 | topfolder: Optional[str] = "NapCat.win32.x64" 8 | napcat_mode: Optional[str] = "win" 9 | nc_proxy: Optional[bool] = False 10 | nc_proxy_port: Optional[int] = 11451 11 | nc_self_update: Optional[str] = "柚子更新nc" 12 | nc_self_restart: Optional[str] = "柚子重启nc" 13 | nc_self_check_update: Optional[str] = "柚子检查更新" 14 | nc_self_qq_version: Optional[str] = "柚子查看qq版本" 15 | nc_reconnect: Optional[bool] = False 16 | nc_restart_way: Optional[int] = 1 17 | 18 | config = get_plugin_config(Config) -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/dialog.py: -------------------------------------------------------------------------------- 1 | # 真有系统不带tk库 2 | try: 3 | import tkinter as tk 4 | import tkinter.font as tkFont 5 | except ImportError: 6 | tk = None 7 | tkFont =None 8 | import asyncio 9 | import threading 10 | 11 | 12 | def run_tkinter_dialog(loop, future): 13 | if tk is None or tkFont is None: 14 | loop.call_soon_threadsafe(future.set_result, "tkinter not available") 15 | return 16 | 17 | def on_restart(): 18 | loop.call_soon_threadsafe(future.set_result, "restart") 19 | root.destroy() 20 | 21 | def on_cancel(): 22 | loop.call_soon_threadsafe(future.set_result, "cancel") 23 | root.destroy() 24 | 25 | root = tk.Tk() 26 | root.title("重启 Napcat") 27 | root.attributes('-topmost', True) 28 | # 设置字体 29 | label_font = tkFont.Font(family="Helvetica", size=12, weight="bold") 30 | button_font = tkFont.Font(family="Helvetica", size=10) 31 | # 设置窗口大小 32 | window_width = 400 33 | window_height = 200 34 | 35 | screen_width = root.winfo_screenwidth() 36 | screen_height = root.winfo_screenheight() 37 | 38 | x = (screen_width // 2) - (window_width // 2) 39 | y = (screen_height // 2) - (window_height // 2) 40 | 41 | root.geometry(f'{window_width}x{window_height}+{x}+{y}') 42 | tk.Label(root, text="将在10s后重启 Napcat", font=label_font).pack(pady=(20, 10)) 43 | tk.Button(root, text="立即重启", command=on_restart, font=button_font).pack(side=tk.LEFT, padx=(50, 10)) 44 | tk.Button(root, text="取消重启", command=on_cancel, font=button_font).pack(side=tk.RIGHT, padx=(10, 50)) 45 | root.after(10000, on_restart) 46 | root.mainloop() 47 | 48 | async def tkinter_dialog(): 49 | if tk is None or tkFont is None: 50 | return "tkinter not available" 51 | 52 | loop = asyncio.get_running_loop() 53 | future = loop.create_future() 54 | t = threading.Thread(target=run_tkinter_dialog, args=(loop, future)) 55 | t.start() 56 | result = await future 57 | t.join() 58 | return result -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/info.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import asyncio 3 | from typing import Dict 4 | try: 5 | import winreg 6 | except ImportError: 7 | winreg = None 8 | # 从注册表获取ntqq安装信息 9 | async def get_qq_registry_values_async() -> Dict[str, str]: 10 | if not winreg: 11 | raise OSError("winreg module is only available on Windows.") 12 | 13 | reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\QQ" 14 | access_flags = winreg.KEY_READ 15 | if platform.architecture()[0] == '64bit': 16 | reg_path = r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" 17 | access_flags |= winreg.KEY_WOW64_32KEY 18 | # 安装位置及版本号 19 | key_names = ["DisplayIcon", "DisplayVersion"] 20 | 21 | def get_values(): 22 | values = {} 23 | try: 24 | registry_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, access_flags) 25 | for name in key_names: 26 | try: 27 | value, regtype = winreg.QueryValueEx(registry_key, name) 28 | values[name] = value 29 | except WindowsError: 30 | values[name] = None 31 | winreg.CloseKey(registry_key) 32 | except WindowsError: 33 | pass 34 | return values 35 | 36 | loop = asyncio.get_running_loop() 37 | return await loop.run_in_executor(None, get_values) -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/mode.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/notice.py: -------------------------------------------------------------------------------- 1 | import json 2 | import aiofiles 3 | import os 4 | from nonebot.adapters.onebot.v11 import Bot, Event 5 | 6 | current_dir = os.path.dirname(os.path.abspath(__file__)) 7 | mode_file = os.path.join(current_dir, 'mode.json') 8 | 9 | def load_mode(): 10 | try: 11 | with open(mode_file, 'r') as f: 12 | return json.load(f) 13 | except (FileNotFoundError, json.JSONDecodeError): 14 | return 15 | # 记录事件以便回复 16 | async def notice(bot: Bot, event: Event): 17 | mode_data = {"type": "private", "id": event.user_id} 18 | if event.message_type == "group": 19 | mode_data = {"type": "group", "id": event.group_id} 20 | async with aiofiles.open(mode_file, 'w') as f: 21 | await f.write(json.dumps(mode_data)) -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/restart.py: -------------------------------------------------------------------------------- 1 | import os 2 | import aiofiles 3 | import json 4 | import platform 5 | import nonebot 6 | from packaging import version 7 | from .notice import notice 8 | from .version import is_qq_version_at_least_9_9_12 9 | from .sysexec import kill_cmd_process, kill_target_processes, start_powershell_script, start_script, start_program_async,kill_napcat_screens, start_napcat_screen 10 | 11 | current_dir = os.path.dirname(os.path.abspath(__file__)) 12 | mode_file = os.path.join(current_dir, 'mode.json') 13 | 14 | class BotRestarter: 15 | def __init__(self, bot_id, base_path, topfolder, disconnect, bot= None, event= None, send_message=True): 16 | self.bot = bot 17 | self.event = event 18 | self.bot_id = bot_id 19 | self.base_path = base_path 20 | self.topfolder = topfolder 21 | self.disconnect = disconnect 22 | self.send_message = send_message 23 | target_path = os.path.normcase(os.path.normpath(os.path.join(self.base_path, self.topfolder))) 24 | self.target_path = target_path 25 | async def send_restart_notice(self, message): 26 | if self.send_message: 27 | await self.bot.send(self.event, message) 28 | 29 | async def get_parsed_app_version(self): 30 | version_info = await self.bot.get_version_info() 31 | app_version = version_info['app_version'] 32 | return version.parse(app_version) 33 | 34 | async def restart_bot(self, nc_restart_way): 35 | 36 | try: 37 | await self.send_restart_notice("正在重启,请稍候") 38 | if nc_restart_way == 1: 39 | await self.restart_method_1(self.disconnect) 40 | elif nc_restart_way == 2: 41 | await self.restart_method_2(self.target_path,self.disconnect) 42 | elif nc_restart_way == 3: 43 | await self.restart_method_3(self.disconnect) 44 | elif nc_restart_way == 4: 45 | await self.restart_method_4(self.target_path,self.disconnect) 46 | elif nc_restart_way == 5: 47 | await self.restart_method_5(self.target_path,self.disconnect) 48 | elif nc_restart_way == 6: 49 | await self.restart_method_6(self.target_path,self.disconnect) 50 | elif nc_restart_way == 7: 51 | await self.restart_method_7(self.disconnect) 52 | except Exception as e: 53 | await self.send_restart_notice(f"发送重启请求时出现错误:{str(e)}") 54 | async with aiofiles.open(mode_file, 'w') as f: 55 | await f.write(json.dumps({})) 56 | async def restart_method_1(self, disconnect): 57 | if disconnect: 58 | await self.restart_method_6(self.target_path,self.disconnect) 59 | return 60 | await notice(self.bot, self.event) 61 | await self.bot.set_restart(delay=1000) 62 | 63 | async def restart_method_2(self, target_path, disconnect): 64 | if platform.system().lower() == 'windows': 65 | version_up = await is_qq_version_at_least_9_9_12() 66 | if version_up: 67 | await self.send_restart_notice("Baka!9.9.12以上不能用这个方法登录!") 68 | return 69 | if not disconnect: 70 | await notice(self.bot, self.event) 71 | found = await kill_cmd_process(target_path) 72 | if found: 73 | await start_script(target_path, self.bot_id, bat='napcat-utf8.bat', q_option=True) 74 | else: 75 | nonebot.logger.info('No matching CMD process found, starting script directly') 76 | await start_script(target_path, self.bot_id, bat='napcat-utf8.bat', q_option=True) 77 | else: 78 | await self.send_restart_notice("只有Windows才能用这个方法啦!") 79 | async def restart_method_3(self,disconnect): 80 | if platform.system().lower() == 'windows': 81 | version_up = await is_qq_version_at_least_9_9_12() 82 | if not disconnect: 83 | await notice(self.bot, self.event) 84 | if version_up: 85 | await start_program_async(self.bot_id) 86 | else: 87 | await self.send_restart_notice("只有Windows才能用这个方法啦!") 88 | async def restart_method_4(self, target_path, disconnect): 89 | if platform.system().lower() == 'windows': 90 | if not disconnect: 91 | app_version_parsed = await self.get_parsed_app_version() 92 | if app_version_parsed < version.parse("1.7.2"): 93 | await self.send_restart_notice("笨蛋!Napcat版本太低啦\n至少要为1.7.2!") 94 | return 95 | await notice(self.bot, self.event) 96 | found = await kill_target_processes('powershell.exe', target_path) 97 | if found: 98 | await start_powershell_script(target_path, self.bot_id) 99 | else: 100 | nonebot.logger.info('No matching PS process found, starting script directly') 101 | await start_powershell_script(target_path, self.bot_id) 102 | else: 103 | await self.send_restart_notice("只有Windows才能用这个方法啦!") 104 | async def restart_method_5(self, target_path, disconnect): 105 | if platform.system().lower() == 'windows': 106 | if not disconnect: 107 | app_version_parsed = await self.get_parsed_app_version() 108 | if app_version_parsed < version.parse("2.4.6"): 109 | await self.send_restart_notice("笨蛋!Napcat版本太低啦\n至少要为2.4.6!") 110 | return 111 | await notice(self.bot, self.event) 112 | found = await kill_target_processes('cmd.exe', target_path) 113 | if found: 114 | await start_script(target_path, self.bot_id, bat='launcher-win10', q_option=False) 115 | else: 116 | nonebot.logger.info('No matching CMD process found, starting script directly') 117 | await start_script(target_path, self.bot_id, bat='launcher-win10', q_option=False) 118 | else: 119 | await self.send_restart_notice("只有Windows才能用这个方法啦!") 120 | async def restart_method_6(self, target_path, disconnect): 121 | if platform.system().lower() == 'windows': 122 | if not disconnect: 123 | app_version_parsed = await self.get_parsed_app_version() 124 | if app_version_parsed < version.parse("2.4.6"): 125 | await self.send_restart_notice("笨蛋!Napcat版本太低啦\n至少要为2.4.6!") 126 | return 127 | await notice(self.bot, self.event) 128 | found = await kill_target_processes('cmd.exe', target_path) 129 | if found: 130 | await start_script(target_path, self.bot_id, bat='launcher.bat', q_option=False) 131 | else: 132 | nonebot.logger.info('No matching CMD process found, starting script directly') 133 | await start_script(target_path, self.bot_id, bat='launcher.bat', q_option=False) 134 | else: 135 | await self.send_restart_notice("只有Windows才能用这个方法啦!") 136 | async def restart_method_7(self, disconnect): 137 | if platform.system().lower() == 'linux': 138 | if not disconnect: 139 | await notice(self.bot, self.event) 140 | await kill_napcat_screens() 141 | await start_napcat_screen(self.bot_id) 142 | else: 143 | await self.send_restart_notice("只有linux才能用这个方法啦!") 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/restart_12.py: -------------------------------------------------------------------------------- 1 | import nonebot 2 | import asyncio 3 | import os 4 | import psutil 5 | from .info import get_qq_registry_values_async 6 | from .notice import notice 7 | # Kill掉NTQQ所在目录的cmd以防重复登陆 8 | async def kill_cmd_processes_at_path(exe_path): 9 | 10 | exe_dir = os.path.dirname(exe_path) 11 | normalized_exe_dir = os.path.normpath(exe_dir).lower() 12 | for proc in psutil.process_iter(['pid', 'name', 'exe', 'cmdline']): 13 | try: 14 | 15 | proc_cwd = proc.cwd().lower() 16 | normalized_proc_cwd = os.path.normpath(proc_cwd) 17 | 18 | if normalized_exe_dir == normalized_proc_cwd: 19 | nonebot.logger.info(f"Killing CMD process with PID {proc.info['pid']} at path {exe_dir}") 20 | proc.kill() 21 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 22 | continue 23 | 24 | # 9.9.12特有的启动方式 25 | async def start_program_async(bot_id=None): 26 | try: 27 | registry_values = await get_qq_registry_values_async() 28 | 29 | display_icon_value = registry_values.get("DisplayIcon") 30 | 31 | if display_icon_value: 32 | exe_path = display_icon_value.split(',')[0].strip("'").strip() 33 | nonebot.logger.info(f"获取到NTQQ安装位置: '{exe_path}'") 34 | 35 | exe_dir = os.path.dirname(exe_path) 36 | 37 | if not exe_dir or not os.path.exists(exe_dir): 38 | nonebot.logger.warning(f"Invalid executable directory: '{exe_dir}'") 39 | return 40 | 41 | args = f"--enable-logging -q {bot_id}" 42 | await kill_cmd_processes_at_path(exe_path) 43 | else: 44 | nonebot.logger.warning("Unable to read the registry value for DisplayIcon.") 45 | return 46 | 47 | if not os.path.exists(exe_path): 48 | nonebot.logger.warning(f"Executable not found: {exe_path}") 49 | return 50 | 51 | batch_command = f'chcp 65001>nul && "{exe_path}" {args}' 52 | command = f'start cmd.exe /k "cd /d "{exe_dir}" && {batch_command}"' 53 | nonebot.logger.info(f"Executing command: {command}") 54 | proc = await asyncio.create_subprocess_shell(command, cwd=exe_dir, shell=True) 55 | await proc.communicate() 56 | except Exception as e: 57 | nonebot.logger.error(f"An error occurred: {e}") 58 | -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/sysexec.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | import nonebot 3 | import os 4 | import asyncio 5 | import subprocess 6 | import shlex 7 | from datetime import datetime 8 | from .info import get_qq_registry_values_async 9 | 10 | # 干掉指定目录下的cmd 11 | async def kill_cmd_process(target_path): 12 | found = False 13 | for proc in psutil.process_iter(['pid', 'name', 'exe', 'cwd']): 14 | if proc.info['name'] == 'cmd.exe': 15 | pid = proc.info['pid'] 16 | cwd = proc.info['cwd'] 17 | normalized_cwd = os.path.normcase(os.path.normpath(cwd)) 18 | 19 | nonebot.logger.info(f'PID: {pid}, CWD: {normalized_cwd}') 20 | if normalized_cwd == target_path: 21 | proc.kill() 22 | nonebot.logger.info(f'Killed process with PID: {pid}') 23 | found = True 24 | break 25 | return found 26 | 27 | # 启动指定目录下的bat 28 | async def start_script(target_path, bot_id, bat=None, q_option=True): 29 | if bat is None: 30 | bat = 'napcat-utf8.bat' 31 | 32 | bat_path = os.path.join(target_path, bat) 33 | param = '-q' if q_option else '' 34 | command = f'cmd.exe /c start "" "{bat_path}" {param} {bot_id}' 35 | 36 | try: 37 | process = await asyncio.create_subprocess_shell( 38 | command, 39 | cwd=target_path 40 | ) 41 | nonebot.logger.info(f'已启动 {bat} 的新进程') 42 | except Exception as e: 43 | nonebot.logger.error(f'启动登录脚本失败: {e}') 44 | raise ValueError(f"脚本不存在") 45 | 46 | # 干掉指定目录下的进程 47 | async def kill_target_processes(target_name, target_path): 48 | def kill_related_processes(proc_name, proc_create_time): 49 | for proc in psutil.process_iter(['name', 'create_time']): 50 | if proc.info['name'].lower() == proc_name.lower(): 51 | create_time = datetime.fromtimestamp(proc.info['create_time']) 52 | if create_time > proc_create_time: 53 | proc.kill() 54 | proc.wait(timeout=3) 55 | nonebot.logger.info(f'Killed related process {proc_name} with PID: {proc.pid}') 56 | 57 | def kill_proc_and_children(proc): 58 | children = proc.children(recursive=True) 59 | for child in children: 60 | child.kill() 61 | child.wait(timeout=3) 62 | nonebot.logger.info(f'Killed child process with PID: {child.pid}') 63 | proc.kill() 64 | proc.wait(timeout=3) 65 | nonebot.logger.info(f'Killed {target_name} parent process with PID: {proc.pid}') 66 | 67 | found = False 68 | for proc in psutil.process_iter(['pid', 'name', 'exe', 'create_time']): 69 | try: 70 | if proc.info['name'].lower() == target_name.lower(): 71 | cwd = proc.cwd() 72 | normalized_cwd = os.path.normcase(os.path.normpath(cwd)) 73 | nonebot.logger.info(f'PID: {proc.info["pid"]}, CWD: {normalized_cwd}') 74 | if normalized_cwd == os.path.normcase(os.path.normpath(target_path)): 75 | found = True 76 | proc_create_time = datetime.fromtimestamp(proc.info['create_time']) 77 | nonebot.logger.info(f'{target_name} PID: {proc.info["pid"]} started at {proc_create_time}') 78 | kill_related_processes('QQ.exe', proc_create_time) 79 | kill_proc_and_children(proc) 80 | break 81 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 82 | pass 83 | return found 84 | 85 | # 启动指定目录下的ps 86 | async def start_powershell_script(target_path, bot_id): 87 | ps1_path = os.path.join(target_path, 'BootWay05.ps1') 88 | command = f'start powershell.exe -NoExit -ExecutionPolicy Bypass -File "{ps1_path}" -q {bot_id}' 89 | 90 | try: 91 | process = subprocess.Popen(command, cwd=target_path, shell=True) 92 | nonebot.logger.info(f'Started BootWay05.ps1 in a new window') 93 | except Exception as e: 94 | nonebot.logger.error(f'Failed to start the script: {e}') 95 | 96 | 97 | async def kill_cmd_processes_at_path(exe_path): 98 | 99 | exe_dir = os.path.dirname(exe_path) 100 | normalized_exe_dir = os.path.normpath(exe_dir).lower() 101 | for proc in psutil.process_iter(['pid', 'name', 'exe', 'cmdline']): 102 | try: 103 | 104 | proc_cwd = proc.cwd().lower() 105 | normalized_proc_cwd = os.path.normpath(proc_cwd) 106 | 107 | if normalized_exe_dir == normalized_proc_cwd: 108 | nonebot.logger.info(f"Killing CMD process with PID {proc.info['pid']} at path {exe_dir}") 109 | proc.kill() 110 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 111 | continue 112 | 113 | # 9.9.12特有的启动方式(exe启动方式/way03) 114 | async def start_program_async(bot_id=None): 115 | try: 116 | registry_values = await get_qq_registry_values_async() 117 | 118 | display_icon_value = registry_values.get("DisplayIcon") 119 | 120 | if display_icon_value: 121 | exe_path = display_icon_value.split(',')[0].strip("'").strip() 122 | nonebot.logger.info(f"获取到NTQQ安装位置: '{exe_path}'") 123 | 124 | exe_dir = os.path.dirname(exe_path) 125 | 126 | if not exe_dir or not os.path.exists(exe_dir): 127 | nonebot.logger.warning(f"Invalid executable directory: '{exe_dir}'") 128 | return 129 | 130 | args = f"--enable-logging -q {bot_id}" 131 | await kill_cmd_processes_at_path(exe_path) 132 | else: 133 | nonebot.logger.warning("Unable to read the registry value for DisplayIcon.") 134 | return 135 | 136 | if not os.path.exists(exe_path): 137 | nonebot.logger.warning(f"Executable not found: {exe_path}") 138 | return 139 | 140 | batch_command = f'chcp 65001>nul && "{exe_path}" {args}' 141 | command = f'start cmd.exe /k "cd /d "{exe_dir}" && {batch_command}"' 142 | nonebot.logger.info(f"Executing command: {command}") 143 | proc = await asyncio.create_subprocess_shell(command, cwd=exe_dir, shell=True) 144 | await proc.communicate() 145 | except Exception as e: 146 | nonebot.logger.error(f"An error occurred: {e}") 147 | 148 | async def kill_napcat_screens(): 149 | cmd_find = "screen -ls | grep 'napcat'" 150 | find_process = await asyncio.create_subprocess_shell( 151 | cmd_find, 152 | stdout=asyncio.subprocess.PIPE, 153 | stderr=asyncio.subprocess.PIPE 154 | ) 155 | stdout, stderr = await find_process.communicate() 156 | 157 | if find_process.returncode == 0: 158 | sessions = stdout.decode().strip().split('\n') 159 | for session in sessions: 160 | session_id = session.split()[0] 161 | if 'Dead' in session or 'dead' in session: 162 | cmd_wipe = f"screen -wipe {session_id}" 163 | await asyncio.create_subprocess_shell( 164 | cmd_wipe, 165 | stdout=asyncio.subprocess.PIPE, 166 | stderr=asyncio.subprocess.PIPE 167 | ) 168 | nonebot.logger.info(f"Wiped dead screen session {session_id}") 169 | else: 170 | cmd_kill = f"screen -S {session_id} -X quit" 171 | await asyncio.create_subprocess_shell( 172 | cmd_kill, 173 | stdout=asyncio.subprocess.PIPE, 174 | stderr=asyncio.subprocess.PIPE 175 | ) 176 | nonebot.logger.info(f"Killed screen session {session_id}") 177 | else: 178 | nonebot.logger.error(f"Error finding napcat screen sessions: {stderr.decode().strip()}") 179 | 180 | async def start_napcat_screen(bot_id): 181 | cmd_start = f'screen -dmS napcat bash -c "xvfb-run -a qq --no-sandbox -q {bot_id}"' 182 | process = await asyncio.create_subprocess_shell( 183 | cmd_start, 184 | stdout=asyncio.subprocess.PIPE, 185 | stderr=asyncio.subprocess.PIPE 186 | ) 187 | stdout, stderr = await process.communicate() 188 | if process.returncode == 0: 189 | nonebot.logger.info(f"Started napcat screen session with bot_id: {bot_id}") 190 | else: 191 | nonebot.logger.error(f"Failed to start napcat screen session with bot_id: {bot_id}. Error: {stderr.decode()}") 192 | -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/unzip.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import shutil 3 | import os 4 | 5 | # 1.x版本的解压方式 6 | async def unzip_v1(zip_file_path, base_path, topfolder): 7 | with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: 8 | for member in zip_ref.namelist(): 9 | try: 10 | relative_path = member.partition('/')[2] 11 | if relative_path: 12 | new_path = os.path.join(base_path, topfolder, relative_path) 13 | os.makedirs(os.path.dirname(new_path), exist_ok=True) 14 | with zip_ref.open(member, 'r') as source, open(new_path, 'wb') as target: 15 | shutil.copyfileobj(source, target) 16 | except zipfile.BadZipFile: 17 | continue 18 | except OSError: 19 | continue 20 | # 2.x版本的解压方式 21 | async def unzip_v2(zip_file_path, base_path, topfolder): 22 | with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: 23 | for member in zip_ref.namelist(): 24 | try: 25 | relative_path = member if member.endswith('/') else member 26 | if relative_path: 27 | new_path = os.path.join(base_path, topfolder, relative_path) 28 | os.makedirs(os.path.dirname(new_path), exist_ok=True) 29 | if not member.endswith('/'): 30 | with zip_ref.open(member, 'r') as source, open(new_path, 'wb') as target: 31 | shutil.copyfileobj(source, target) 32 | except zipfile.BadZipFile: 33 | continue 34 | except OSError: 35 | continue -------------------------------------------------------------------------------- /nonebot_plugin_ncupdate/version.py: -------------------------------------------------------------------------------- 1 | from packaging import version 2 | from .info import get_qq_registry_values_async 3 | from nonebot import on_command 4 | from nonebot.permission import SUPERUSER 5 | import nonebot 6 | import subprocess 7 | import asyncio 8 | import re 9 | import distro 10 | import platform 11 | qq_version_info = on_command("查看qq版本", aliases={"查看QQ版本", "qqv"},priority=5, permission=SUPERUSER) 12 | 13 | @qq_version_info.handle() 14 | async def qq_version(): 15 | version = await get_qq_version_info() 16 | if version == 0: 17 | await qq_version_info.send(f"未能获取到QQ版本,可能是不支持的系统") 18 | else: 19 | await qq_version_info.send(f"当前的QQ版本是:{version}") 20 | 21 | async def get_qq_version_info(): 22 | if platform.system().lower() == 'windows': 23 | qq_info = await get_qq_patch_number() 24 | else: 25 | if distro.id() in ["centos","rocky"]: 26 | qq_info = await get_qq_version_centos() 27 | elif distro.id() in ["debian","ubuntu"]: 28 | qq_info = await get_qq_version_debian() 29 | else: 30 | qq_info = 0 31 | return qq_info 32 | 33 | async def get_qq_patch_number() -> int: 34 | registry_values = await get_qq_registry_values_async() 35 | display_version_value = registry_values.get("DisplayVersion") 36 | if display_version_value: 37 | version_parts = display_version_value.split('.') 38 | return int(version_parts[3]) 39 | else: 40 | return 0 41 | 42 | async def get_qq_version_centos() -> int: 43 | try: 44 | process = await asyncio.create_subprocess_exec( 45 | "rpm", "-q", "--queryformat", "%{VERSION}", "linuxqq", 46 | stdout=subprocess.PIPE, 47 | stderr=subprocess.PIPE 48 | ) 49 | stdout, stderr = await process.communicate() 50 | stdout = stdout.decode('utf-8') 51 | if process.returncode == 0: 52 | version_match = re.search(r'_([0-9]+)$', stdout) 53 | if version_match: 54 | return int(version_match.group(1)) 55 | else: 56 | nonebot.logger.error(f"Error: {stderr.decode('utf-8')}") 57 | return 0 58 | except Exception as e: 59 | nonebot.logger.error(f"发生错误: {e}") 60 | return 0 61 | return 0 62 | 63 | async def get_qq_version_debian() -> int: 64 | try: 65 | process = await asyncio.create_subprocess_exec( 66 | "dpkg", "-l", "linuxqq", 67 | stdout=subprocess.PIPE, 68 | stderr=subprocess.PIPE 69 | ) 70 | stdout, stderr = await process.communicate() 71 | stdout = stdout.decode('utf-8') 72 | if process.returncode == 0: 73 | match = re.search(r'\bii\s+' + re.escape("linuxqq") + r'\s+(\S+)', stdout) 74 | if match: 75 | version_str = match.group(1) 76 | version_int_match = re.search(r'(\d+)$', version_str) 77 | if version_int_match: 78 | return int(version_int_match.group(1)) 79 | else: 80 | nonebot.logger.error(f"Error: {stderr}") 81 | return 0 82 | except Exception as e: 83 | nonebot.logger.error(f"发生错误: {e}") 84 | return 0 85 | return 0 86 | 87 | async def ciallo(latest_version: str) -> bool: 88 | 89 | latest_version = latest_version.lstrip('v') 90 | patch_number = await get_qq_version_info() 91 | 92 | 93 | if latest_version.startswith("1"): 94 | return patch_number <= 26702 95 | 96 | elif latest_version == "2.0.37": 97 | return 26702 <= patch_number <= 26909 98 | 99 | latest_version_parsed = version.parse(latest_version) 100 | 101 | if version.parse("2.1.0") <= latest_version_parsed <= version.parse("2.2.18"): 102 | return patch_number >= 27187 103 | 104 | elif version.parse("2.2.19") <= latest_version_parsed <= version.parse("2.2.29"): 105 | return patch_number >= 27254 106 | 107 | elif version.parse("2.2.30") <= latest_version_parsed <= version.parse("2.5.3"): 108 | return 27597 <= patch_number < 28060 109 | 110 | elif latest_version_parsed >= version.parse("2.5.4"): 111 | return patch_number >= 28060 112 | 113 | return False 114 | 115 | async def is_qq_version_at_least_9_9_12() -> bool: 116 | registry_values = await get_qq_registry_values_async() 117 | display_version_value = registry_values.get("DisplayVersion") 118 | target_version = "9.9.12" 119 | 120 | if display_version_value: 121 | # 如果版本号至少是9.9.12,返回True 122 | return version.parse(display_version_value) >= version.parse(target_version) 123 | else: 124 | return False 125 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot-plugin-ncupdate" 3 | version = "0.2.6" 4 | description = "A nonebot plugin for update NapCat" 5 | authors = ["tianyisama"] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/tianyisama" 9 | repository = "https://github.com/tianyisama/nonebot-plugin-ncupdate" 10 | [tool.poetry.dependencies] 11 | python = "^3.8" 12 | nonebot2 = "<3.0.0,>=2.2.0" 13 | nonebot-adapter-onebot = "^2.1.1" 14 | psutil = ">=5.9.0,<6.0.0" 15 | aiofiles = ">=23.2.1" 16 | httpx = ">=0.23.3" 17 | packaging = ">=24.1" 18 | distro = ">=1.5.0" 19 | [tool.poetry.dev-dependencies] 20 | pytest = "^5.2" 21 | 22 | [build-system] 23 | requires = ["poetry-core>=1.0.0"] 24 | build-backend = "poetry.core.masonry.api" --------------------------------------------------------------------------------