├── 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"
--------------------------------------------------------------------------------