@fengye_nobug/dsh-qq-bot 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # 更新日志
2
2
 
3
+ ## v0.2.7 — 2026-10-31
4
+
5
+ - **一键更新装完会自己重启 dsh-web**:安装成功后按面板原来的办法重启,不再把「请自己重启」当成正常结果。Linux 上如果是 systemd 拉起来的,会读本进程的 cgroup,大约 3 秒后只重启认出来的那个服务。服务在用户实例里时用 `systemctl --user`,不会重启整个登录。以前没把 cgroup 读进来,服务里一点更新就直接说面板没有重启。`systemd-run` 没权限或失败时,改为按当前的 node 命令行再拉起一次。两条都做不到,才用白话请你自己重启 dsh-web,并提醒刷新页面、登录口令可能会变。安装超过三分钟、但磁盘上已经是要装的版本时,同样会改包名并重启。进程还在,或没有确认装上时,仍然不会重启。
6
+ - **检查和安装的提示改成短句**:设置卡片不再把「优先从 npm…三分钟…」那段长说明当作主要提示。检查按钮旁边写「只检查,不安装。」有更新按钮时另写「装完会重启面板。请刷新;登录口令可能会变。不改 NapCat。」检查结果是「有新版本 x.y.z(现在是 a.b.c),在 npm 上。这里只检查,还没安装。」或「在 GitHub 上」。安装成功是「已经从 npm 装上 vX.Y.Z,面板即将重启。」或「已经从 GitHub 装上 vX.Y.Z,面板即将重启。」自动重启失败时才请自己重启,并写明这次是从 npm 还是从 GitHub 装上的。
7
+
3
8
  ## v0.2.6 — 2026-10-30
4
9
 
5
10
  - **一键更新之后,Cordis 按实际装上的包名挂 `qq-bridge`**:npm 包是 `@fengye_nobug/dsh-qq-bot`。v0.2.5 自带的 `cordis.patch.yml` 仍写 `name: dsh-qq-bot`。Cordis 按这个名字去加载,无作用域包并不存在,插件 id `qq-bridge` 不会挂上,`/api/qq-bridge/settings` 就是 HTTP 404,设置页显示本机接口不可用。现在插件自带的补丁把 `name` 写成 `@fengye_nobug/dsh-qq-bot`。安装成功后,还会看 profile 的 `cordis.patch.yml`、`$DSH_HOME/cordis.patch.yml`(这份文件里有 `qq-bridge` 才改),以及已经装进 profile 的插件补丁。这三处里 `qq-bridge` 的 `name` 如果还是 `dsh-qq-bot` 或 `dsh-qq-bridge`,就改成磁盘上 `package.json` 的包名。别的插件不动,`config` 里的设置不动。界面显示名和消息来源仍是 `dsh-qq-bot`,Cordis id 仍是 `qq-bridge`。从 0.2.5 点「更新」时,跑的仍是 0.2.5 里的安装程序,profile 里手写的旧 `name` 要先改成 `@fengye_nobug/dsh-qq-bot` 再重启;这一版自己跑一键更新时会改。
package/README.md CHANGED
@@ -23,7 +23,7 @@ dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot#v0.2.5
23
23
  在 GitHub 上 Watch → Custom → **Releases**,比盯 main 稳。环境、第一次怎么开白名单、怎么升级、和官方插件为什么不能一起挂,写在[安装](#安装)。
24
24
 
25
25
  - 设计:[`DESIGN.md`](./DESIGN.md) | 里程碑与真机实录:[`HANDOVER.md`](./HANDOVER.md) | 变更:[`CHANGELOG.md`](./CHANGELOG.md)
26
- - 源码版本 **0.2.6**(一键更新会把 Cordis 的包名改成实际装上的 `@fengye_nobug/dsh-qq-bot`)。已经发布的是 **v0.2.5**,npm 上对应 `@fengye_nobug/dsh-qq-bot@0.2.5`。源码这一版还没打 tag,也还没 `npm publish`。v0.2.5 自带补丁里的 `name` 仍是无作用域的 `dsh-qq-bot`,装完若设置页是 HTTP 404,先把 `qq-bridge` 的 `name` 改成 `@fengye_nobug/dsh-qq-bot` 再重启。见 [`CHANGELOG.md`](./CHANGELOG.md)。
26
+ - 源码版本 **0.2.7**(一键更新装完会自己重启 dsh-web;Cordis 的包名会改成实际装上的 `@fengye_nobug/dsh-qq-bot`)。已经发布的是 **v0.2.5**,npm 上对应 `@fengye_nobug/dsh-qq-bot@0.2.5`。源码这一版还没打 tag,也还没 `npm publish`。v0.2.5 自带补丁里的 `name` 仍是无作用域的 `dsh-qq-bot`,装完若设置页是 HTTP 404,先把 `qq-bridge` 的 `name` 改成 `@fengye_nobug/dsh-qq-bot` 再重启。见 [`CHANGELOG.md`](./CHANGELOG.md)。
27
27
  - **M1–M8 全部完成**,已在**真实 NapCat + 真实 QQ** 上端到端跑通(Windows 真机)
28
28
 
29
29
  ## 目录
@@ -49,7 +49,7 @@ dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot#v0.2.5
49
49
  也认 `[表情182]` 和 `[CQ:face,id=182]`。详见[出站](#出站三种模式--来源闸门)。
50
50
  - **有设置卡片**:名称是白话(哪些群可以叫醒、群里发出去的是哪一段、忙的时候怎么插话等),
51
51
  配置键仍是英文。在 `设置 → 插件 → 插件配置` 里修改,保存即生效。
52
- - **可以一键更新**:先点「检查更新」。有新正式版时再点「更新到 vX.Y.Z」,面板会装上并重启。优先从 npm 安装 `@fengye_nobug/dsh-qq-bot@版本`;这一版还没发到 npm 时,改从 GitHub 下载。装完把 Cordis 里 `qq-bridge` 的 `name` 改成实际装上的包名。超过三分钟没有结束就停掉卡住的安装进程;要装的版本已经在磁盘上时仍改包名,并请你自己重启 dsh-web。启动时检查不会自动安装。
52
+ - **可以一键更新**:先点「检查更新」。有新正式版时再点「更新到 vX.Y.Z」,面板会装上并自己重启 dsh-web。优先从 npm 安装 `@fengye_nobug/dsh-qq-bot@版本`;这一版还没发到 npm 时,改从 GitHub 下载。装完把 Cordis 里 `qq-bridge` 的 `name` 改成实际装上的包名。超过三分钟没有结束就停掉卡住的安装进程;要装的版本已经在磁盘上时仍改包名,并自动重启。只有自动重启没有安排上,才提示自己重启。启动时检查不会自动安装。
53
53
  - **NapCat 自助托管**:插件可以按需下载、校验、解包、写配置、启停 NapCat。
54
54
 
55
55
  **不做的三件事**:
@@ -60,7 +60,7 @@ dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot#v0.2.5
60
60
 
61
61
  ## 安装
62
62
 
63
- 日常安装用 npm 上已经发出的 **`@fengye_nobug/dsh-qq-bot@0.2.5`**。无作用域的 `dsh-qq-bot@版本` 装不上:npm 不接受这个名字,它和已有的 `dsh-qqbot` 太像。GitHub [Releases](https://github.com/yunfengnobug/dsh-qq-bot/releases) 上已经发出的是 **v0.2.5**。仓库源码如果已经是 0.2.6,那是还没打 tag、也还没发到 npm 的版本,日常不要装 main。某一版还没出现在 registry 里时,一键更新会改从 GitHub 下载。v0.2.5 的补丁仍把 Cordis 的 `name` 写成 `dsh-qq-bot`。装上这一版后,一键更新会在安装成功时把 profile、home(写了 `qq-bridge` 才改)和插件补丁里的 `name` 改成磁盘上的包名。
63
+ 日常安装用 npm 上已经发出的 **`@fengye_nobug/dsh-qq-bot@0.2.5`**。无作用域的 `dsh-qq-bot@版本` 装不上:npm 不接受这个名字,它和已有的 `dsh-qqbot` 太像。GitHub [Releases](https://github.com/yunfengnobug/dsh-qq-bot/releases) 上已经发出的是 **v0.2.5**。仓库源码如果已经是 0.2.7,那是还没打 tag、也还没发到 npm 的版本,日常不要装 main。某一版还没出现在 registry 里时,一键更新会改从 GitHub 下载。v0.2.5 的补丁仍把 Cordis 的 `name` 写成 `dsh-qq-bot`。装上这一版后,一键更新会在安装成功时把 profile、home(写了 `qq-bridge` 才改)和插件补丁里的 `name` 改成磁盘上的包名。
64
64
 
65
65
  ```sh
66
66
  dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@0.2.5
@@ -105,13 +105,13 @@ dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot
105
105
 
106
106
  无作用域名字发不出去。第 1 步如果发现 registry 里没有 `@fengye_nobug/dsh-qq-bot` 的这一版,会改从 GitHub 下载该标签的官方源码包:先 `github.com`,连不上再试同一标签的 `codeload.github.com`,然后交给 `dsh plugin add`。v0.2.4 及更早标签里的 `package.json` 仍是无作用域名,这种源码包仍然能装,Cordis 的 `name` 也保持 `dsh-qq-bot`。v0.2.5 的 `package.json` 已是作用域包,但补丁里的 `name` 还是旧的,所以会 404。插件不写死第三方镜像。registry 里已经有比当前新的正式版时,检查和安装不再访问 GitHub。
107
107
 
108
- 从 0.2.5 点「更新」时,执行的还是那一版里的安装程序,不会改 profile。若设置页变成 HTTP 404,把 profile 和插件目录里 `qq-bridge` 的 `name` 改成 `@fengye_nobug/dsh-qq-bot`,再重启 dsh-web。这一版(0.2.6)自己跑一键更新时会改。
108
+ 从 0.2.5 点「更新」时,执行的还是那一版里的安装程序,不会改 profile,也不会自己重启。若设置页变成 HTTP 404,把 profile 和插件目录里 `qq-bridge` 的 `name` 改成 `@fengye_nobug/dsh-qq-bot`,再重启 dsh-web。这一版(0.2.7)自己跑一键更新时会改包名,并在装完后自动重启。
109
109
 
110
- 「检查更新」和「启动时检查一次」只询问,不会安装。已经是最新正式版时不出现更新按钮。不会装比当前更旧的版本,也不装草稿和预发布。连不上、安装失败、面板没能重启,都用白话说明。安装已经成功但没能安排重启时,新版本在磁盘上,需要自己重启 dsh-web。包名已经改好的话,重启后设置页会回来。
110
+ 「检查更新」和「启动时检查一次」只询问,不会安装。已经是最新正式版时不出现更新按钮。不会装比当前更旧的版本,也不装草稿和预发布。连不上、安装失败、面板没能重启,都用白话说明。安装已经成功但自动重启没有安排上时,新版本在磁盘上,需要自己重启 dsh-web。包名已经改好的话,重启后设置页会回来。
111
111
 
112
- 安装超过三分钟没有结束时,会停掉卡住的 dsh 和它带起来的 pnpm,并用白话说明。profile 里的 `.plugin-manager/run.json` 只在记录的进程已经不在、或记录读不懂时删掉;进程还在就不会再开一次安装。超时后如果要装的版本已经在磁盘上,包名仍会改成实际安装的包,但不会自动重启,需要自己重启 dsh-web。没有确认装上时,可以再点一次「更新」。
112
+ 安装超过三分钟没有结束时,会停掉卡住的 dsh 和它带起来的 pnpm,并用白话说明。profile 里的 `.plugin-manager/run.json` 只在记录的进程已经不在、或记录读不懂时删掉;进程还在就不会再开一次安装。超时后如果要装的版本已经在磁盘上,包名仍会改成实际安装的包,然后自动重启 dsh-web。没有确认装上时,可以再点一次「更新」,这时不会重启。
113
113
 
114
- 在 Linux 上,进程由 systemd 拉起并且能认出具体服务时,大约 3 秒后只重启那个服务。只看到用户会话、认不出单独服务时,请自己重启 dsh-web。没有被 systemd 拉起时,响应发出后按当前的 node 命令行再拉起一次。这次更新不改 NapCat 的配置,也不去停它。插件自己拉起的 NapCat 会随 dsh-web 退出而结束;已经在外面跑着的 NapCat 继续运行。
114
+ 在 Linux 上,进程由 systemd 拉起并且能认出具体服务时,大约 3 秒后只重启那个服务。服务挂在用户实例(`user@`)下面时,用 `systemctl --user`,不会重启整个登录。认不出单独服务,或 `systemd-run` 没有权限、执行失败时,改为按当前的 node 命令行再拉起一次。两条都做不到,才提示自己重启 dsh-web。没有被 systemd 拉起时,响应发出后按当前命令行再拉起一次。这次更新不改 NapCat 的配置,也不去停它。插件自己拉起的 NapCat 会随 dsh-web 退出而结束;已经在外面跑着的 NapCat 继续运行。
115
115
 
116
116
  想让 npm 走镜像,把 npm / pnpm 的 registry 配成你自己的镜像。插件只在没读到配置时用 `https://registry.npmjs.org/`。
117
117
 
@@ -170,7 +170,7 @@ dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@0.2.5
170
170
 
171
171
  无作用域的 `dsh-qq-bot` 发不出去,npm 认为它和已有的 `dsh-qqbot` 太像。不要写成 `dsh plugin add dsh-qq-bot@版本`。
172
172
 
173
- registry 里还没有要装的那一版时(源码 0.2.6 尚未发布),改用 GitHub:
173
+ registry 里还没有要装的那一版时(源码 0.2.7 尚未发布),改用 GitHub:
174
174
 
175
175
  ```sh
176
176
  dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot#v0.2.5
@@ -475,9 +475,9 @@ QQ 的“拍一拍”在 OneBot 里走的是 **notice 事件**,不是消息段
475
475
  > 宿主 `requestRejection()` 返回空、`undefined` 或 `0` **不算**已登录。已登录只认宿主明确给出的结果:`isAuthenticated`、浏览器会话,或 `admit()` 交回的身份。没有这个结果的外网请求是 403。来源是别的网站、和 `Host` 对不上的请求也是 403,就算带着 cookie 也不行。接口只写 schema 白名单里的键。
476
476
  > 判断面板监听器是不是 HTTPS 时,只通过 `ctx.get('webServer')` 读协议,不读 `ctx.webServer`。插件没有把 `webServer` 放进 inject。直接读这个属性会抛错,宿主会把这次请求收成**没有正文的 HTTP 400**,卡片诊断就显示本机接口不可用,即使浏览器里已经登录。读不到这项服务、或读取失败时,按 HTTP 继续做登录判定。
477
477
 
478
- 卡片上方显示**当前版本**,并有 **检查更新**。接口是 `GET/POST /api/qq-bridge/update`,准入和设置接口相同。GET 只读当前版本,不去 npm,也不去 GitHub。POST 不带 `action` 才对比版本:先问 npm registry 里的 `@fengye_nobug/dsh-qq-bot` 正式版。有比当前新的版本时,写出版本号和 `dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@x.y.z`,并且不再访问 GitHub。npm 上还没有更新、但 GitHub Release 上有更新时,才改问 GitHub,并说明一键更新会从 GitHub 下载。没有新版、连不上、被限流,都用白话说明。检查本身不会下载或安装。
478
+ 卡片上方显示**当前版本**,并有 **检查更新**。按钮旁边只写「只检查,不安装。」接口是 `GET/POST /api/qq-bridge/update`,准入和设置接口相同。GET 只读当前版本,不去 npm,也不去 GitHub。POST 不带 `action` 才对比版本:先问 npm registry 里的 `@fengye_nobug/dsh-qq-bot` 正式版。有比当前新的版本时,卡片上写短句「有新版本 x.y.z(现在是 a.b.c),在 npm 上。这里只检查,还没安装。」并且不再访问 GitHub。npm 上还没有更新、但 GitHub Release 上有更新时,改问 GitHub,短句里写「在 GitHub 上」。安装命令和说明摘要仍在接口字段里,不铺在这句主提示上。没有新版、连不上、被限流,都用白话说明。检查本身不会下载或安装。
479
479
 
480
- 有新版时出现 **更新到 vX.Y.Z**。点下去才发送 `POST { "action": "apply", "version": "x.y.z" }`。优先 `dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@x.y.z`。registry 里没有这个版本时,改从 GitHub 的官方源码包装上。安装成功后,把 profile、`$DSH_HOME/cordis.patch.yml`(有 `qq-bridge` 这一行才改)和已安装插件补丁里的 `name` 改成磁盘上 `package.json` 的包名,然后重启面板。只改我们自己的旧包名(`dsh-qq-bot`、`dsh-qq-bridge`),不改 `config`,也不改别的插件。按钮下面写着:设置还在,不改 NapCat;重启后请刷新,登录口令可能会变;安装如果超过三分钟没有动静,会停掉卡住的进程,并用白话说明。进行中显示「正在更新…」。已经是最新正式版时没有这个按钮。超过三分钟还没装完时停掉这次的 dsh 和 pnpm。`.plugin-manager/run.json` 里的进程还在就不删,也不再开第二个安装;进程已经不在或记录读不懂才删。超时后如果磁盘上已经是要装的版本,仍改包名,请自己重启 dsh-web,不会自动重启。
480
+ 有新版时出现 **更新到 vX.Y.Z**。旁边只加一句「装完会重启面板。请刷新;登录口令可能会变。不改 NapCat。」点下去才发送 `POST { "action": "apply", "version": "x.y.z" }`。优先 `dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@x.y.z`。registry 里没有这个版本时,改从 GitHub 的官方源码包装上。安装成功后,把 profile、`$DSH_HOME/cordis.patch.yml`(有 `qq-bridge` 这一行才改)和已安装插件补丁里的 `name` 改成磁盘上 `package.json` 的包名,然后重启面板。只改我们自己的旧包名(`dsh-qq-bot`、`dsh-qq-bridge`),不改 `config`,也不改别的插件。成功时主提示是「已经从 npm 装上 vX.Y.Z,面板即将重启。」或「已经从 GitHub 装上 vX.Y.Z,面板即将重启。」进行中显示「正在更新…」。已经是最新正式版时没有这个按钮。超过三分钟还没装完时停掉这次的 dsh 和 pnpm。`.plugin-manager/run.json` 里的进程还在就不删,也不再开第二个安装;进程已经不在或记录读不懂才删。超时后如果磁盘上已经是要装的版本,仍改包名,并自动重启 dsh-web。自动重启没有安排上时,才提示自己重启,并写明这次是从 npm 还是从 GitHub 装上的。
481
481
 
482
482
  「启动时检查一次」默认关。打开并保存后,下次启动 dsh-web 才问一次,结果只写在日志里,不会安装。
483
483
 
@@ -530,7 +530,7 @@ QQ 的“拍一拍”在 OneBot 里走的是 **notice 事件**,不是消息段
530
530
  | `downloadProxy` | `''` | 下载时走的代理;空 = 先看 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY`。NapCat 下载,以及一键更新改从 GitHub 下载时,都用它。从 npm 安装时走 pnpm 自己的 registry,不读这个框。界面上不显示代理地址 |
531
531
  | `onebotPort` | `3001` | OneBot 端口。自己另外部署的 NapCat 要和这边一致 |
532
532
  | `qqNumber` | `''` | 快速登录用的 QQ 号(需先手动登录过一次) |
533
- | `checkUpdateOnStart` | `false` | 启动时检查一次。默认关。打开后,下次启动先问 npm 上的 `@fengye_nobug/dsh-qq-bot`;npm 上还没有更新时再问 GitHub。只写日志,不自动安装。要装用卡片上的「更新到 vX.Y.Z」 |
533
+ | `checkUpdateOnStart` | `false` | 启动时检查一次。默认关。打开后,下次启动只检查有没有新版本,写在日志里,不自动安装。要装用卡片上的「更新到 vX.Y.Z」 |
534
534
 
535
535
  这些值优先从设置界面 / `settings.yaml` 取,未设置时回落到 `cordis.patch.yml` 里的安装配置(composition base)。
536
536
 
@@ -628,7 +628,7 @@ QQ 侧没有人能点击“同意”,因此:
628
628
 
629
629
  改用法、设置或运行行为时,同一次提交里更新本文件。约定见 [`AGENTS.md`](./AGENTS.md)。
630
630
 
631
- 5 个套件都是**零依赖**(测试自己造 zip、自己起一个最小 OneBot WS 服务器;检查更新不连真 npm、也不连真 GitHub),共 **778 条断言**:
631
+ 5 个套件都是**零依赖**(测试自己造 zip、自己起一个最小 OneBot WS 服务器;检查更新不连真 npm、也不连真 GitHub),共 **812 条断言**:
632
632
 
633
633
  ```powershell
634
634
  node test-smoke.mjs # 策略分支 + 出站闸门 + 描述缓存命中 + 表情包工具(含 mface)+ 系统表情分段 + 设置卡片字段 + 检查更新和一键安装接口(假 ctx,纯离线)
package/client/client.js CHANGED
@@ -38,7 +38,7 @@ window.__ModuleLoader__.load({
38
38
  key: 'checkUpdateOnStart',
39
39
  label: '启动时检查一次',
40
40
  kind: 'boolean',
41
- hint: '默认关。打开并保存后,下次启动 dsh-web 会先问 npm 上的 @fengye_nobug/dsh-qq-bot 有没有新版本;npm 上还没有更新时再看 GitHub。只写在日志里,不会自动安装。要装新版本,用检查之后出现的更新按钮。',
41
+ hint: '默认关。打开并保存后,下次启动只检查有没有新版本,写在日志里,不会自动安装。',
42
42
  },
43
43
  {
44
44
  key: 'sessionScope',
@@ -860,9 +860,9 @@ window.__ModuleLoader__.load({
860
860
  )
861
861
  : null,
862
862
  ),
863
- h('div', { style: S.hint }, '先问 npm 上有没有新版本。这里只检查,不会安装。'),
863
+ h('div', { style: S.hint }, '只检查,不安装。'),
864
864
  updateOffer
865
- ? h('div', { style: S.hint }, '优先从 npm 安装 @fengye_nobug/dsh-qq-bot 的这一版并重启面板。这一版还没发到 npm 时,会改从 GitHub 下载。设置还在,不改 NapCat。重启后请刷新,登录口令可能会变。安装如果超过三分钟没有动静,会停掉卡住的进程,并用白话说明。')
865
+ ? h('div', { style: S.hint }, '装完会重启面板。请刷新;登录口令可能会变。不改 NapCat。')
866
866
  : null,
867
867
  updateNote
868
868
  ? h('div', { style: { ...(updateIsError ? S.err : S.note), whiteSpace: 'pre-wrap' } }, updateNote)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fengye_nobug/dsh-qq-bot",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "description": "把 DSH 会话接到 QQ(OneBot/NapCat)的插件。白名单默认全关;被 @、叫到昵称或被戳一定叫醒,其余消息按可调概率抽样。Fork 自 sj244/dsh-qq-bridge(MIT)。",
package/updates.js CHANGED
@@ -10,7 +10,11 @@
10
10
  // 安装成功后,若 profile、home 或插件自己的 cordis.patch.yml 里 qq-bridge 的 name
11
11
  // 仍是旧的无作用域名,会改成磁盘上 package.json 里的包名。不改 config,不改设置,不改 NapCat。
12
12
  // 安装超过三分钟还没结束时,停掉这次拉起的进程组,并清掉已经退出的安装记录。
13
- // 超时后若磁盘上已是目标版本,仍改包名,但不自动重启。
13
+ // 超时后若磁盘上已是目标版本,仍改包名,并安排重启。
14
+ // 重启会读本进程的 cgroup:认得出具体服务就用 systemd-run 只重启那个服务
15
+ // (挂在 user@ 下面的用 systemctl --user)。systemd-run 失败,或只看见用户会话、
16
+ // 认不出单独服务时,不重启整个登录,改为按当前命令行再拉起一次。
17
+ // 两条都做不到,才提示自己重启。
14
18
 
15
19
  import { spawn } from 'node:child_process'
16
20
  import { chmodSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
@@ -237,7 +241,7 @@ export function assessUpdate({ current, releases, repo = UPDATE_REPO, channel =
237
241
  kind: 'no-release',
238
242
  current: parsedCurrent.raw,
239
243
  updateAvailable: false,
240
- message: 'GitHub 上还没有可用的正式版,没法对比新版本。',
244
+ message: channel === 'npm' ? 'npm 上还没有可用的正式版,没法对比新版本。' : 'GitHub 上还没有可用的正式版,没法对比新版本。',
241
245
  }
242
246
  }
243
247
  stable.sort((left, right) => compareSemver(right.parsed, left.parsed) ?? 0)
@@ -259,21 +263,21 @@ export function assessUpdate({ current, releases, repo = UPDATE_REPO, channel =
259
263
  url,
260
264
  }
261
265
  if (compared > 0) {
262
- const lines = [`有新版本 ${latest.parsed.raw}(你现在是 ${parsedCurrent.raw})。`]
263
- if (summary) lines.push(`说明:${summary}`)
264
- lines.push(`升级命令:${command}`)
265
- lines.push('装完请重启 dsh-web。')
266
- if (useNpm) lines.push(`也可以在设置卡片里点「更新到 v${latest.parsed.raw}」,面板会从 npm 装上并重启。`)
267
- else lines.push(`也可以在设置卡片里点「更新到 v${latest.parsed.raw}」,面板会自己装并重启。`)
268
- if (!useNpm) lines.push(`发布页:${url}`)
269
- return { ...base, kind: 'update', updateAvailable: true, message: lines.join('\n') }
266
+ const where = useNpm ? 'npm' : 'GitHub'
267
+ return {
268
+ ...base,
269
+ kind: 'update',
270
+ updateAvailable: true,
271
+ message: `有新版本 ${latest.parsed.raw}(现在是 ${parsedCurrent.raw}),在 ${where} 上。这里只检查,还没安装。`,
272
+ }
270
273
  }
271
274
  if (compared < 0) {
275
+ const where = useNpm ? 'npm' : 'GitHub'
272
276
  return {
273
277
  ...base,
274
278
  kind: 'ahead',
275
279
  updateAvailable: false,
276
- message: `你现在的 ${parsedCurrent.raw} 比 GitHub 上最新正式版 ${latest.parsed.raw} 还新,不用按 Release 往回装。`,
280
+ message: `你现在的 ${parsedCurrent.raw} 比 ${where} 上最新正式版 ${latest.parsed.raw} 还新,不用往回装。`,
277
281
  }
278
282
  }
279
283
  return {
@@ -439,19 +443,6 @@ export async function queryNpmVersions(input = {}) {
439
443
  return { ok: false, code: 'network' }
440
444
  }
441
445
 
442
- function noteGithubFallback(assessed) {
443
- if (!assessed || assessed.updateAvailable !== true) return assessed
444
- const message = String(assessed.message ?? '')
445
- if (message.includes('还没发到 npm') || !message.includes('也可以在设置卡片里点')) return assessed
446
- return {
447
- ...assessed,
448
- message: message.replace(
449
- '也可以在设置卡片里点',
450
- '这一版还没发到 npm,一键更新会改从 GitHub 下载。\n也可以在设置卡片里点',
451
- ),
452
- }
453
- }
454
-
455
446
  function lookupFailure(npmCode, githubError, applying) {
456
447
  const raw = String(githubError ?? '')
457
448
  if (npmCode === 'network' && /连不上 GitHub/.test(raw)) {
@@ -490,7 +481,7 @@ async function findUpdate(input) {
490
481
  if (wanted !== '') return { ok: false, error: '这次检查到的正式版里没有这个版本,没有安装。' }
491
482
  return { ok: true, channel: 'npm', releases, assessed }
492
483
  }
493
- const ghAssessed = noteGithubFallback(assessUpdate({ current: input.current, releases: gh.releases, repo, channel: 'github' }))
484
+ const ghAssessed = assessUpdate({ current: input.current, releases: gh.releases, repo, channel: 'github' })
494
485
  if (ghAssessed.ok === false) return { ok: false, error: ghAssessed.message }
495
486
  const npmLatest = assessed.latest || '0.0.0'
496
487
  const githubNewer = ghAssessed.updateAvailable === true && (compareSemver(ghAssessed.latest, npmLatest) ?? 0) > 0
@@ -506,7 +497,7 @@ async function findUpdate(input) {
506
497
  }
507
498
  const gh = await queryGithubReleases(input.fetchImpl, { repo, timeoutMs: input.timeoutMs })
508
499
  if (!gh.ok) return { ok: false, error: lookupFailure(npm.code, gh.error, input.applying === true) }
509
- const assessed = noteGithubFallback(assessUpdate({ current: input.current, releases: gh.releases, repo, channel: 'github' }))
500
+ const assessed = assessUpdate({ current: input.current, releases: gh.releases, repo, channel: 'github' })
510
501
  if (assessed.ok === false) return { ok: false, error: assessed.message }
511
502
  return { ok: true, channel: 'github', releases: gh.releases, assessed }
512
503
  }
@@ -627,9 +618,6 @@ function installError(code, command, extra = {}) {
627
618
  const which = Number.isSafeInteger(pid) && pid > 1 ? `(进程 ${pid})` : ''
628
619
  return `已经有一个安装进程还在跑${which},这次没有再启动。等它结束再点「更新」。如果确认它已经卡死,先停掉那个进程,再点一次。`
629
620
  }
630
- if (code === 'timeout' && extra.packageName) {
631
- return `安装超时,已经停掉卡住的进程。新版本的文件已经在磁盘上,qq-bridge 的包名也改成了 ${extra.packageName}。请重启 dsh-web,设置页才会出来。`
632
- }
633
621
  if (code === 'timeout' && extra.leftover) {
634
622
  return `安装超时,卡住的安装进程还没停干净。请先停掉 dsh 和 pnpm,再点一次「更新」。${hint}`
635
623
  }
@@ -639,13 +627,12 @@ function installError(code, command, extra = {}) {
639
627
  if (code === 'wrong-name') return '下到的安装包不是这个插件,没有安装。'
640
628
  if (code === 'wrong-version') return '下到的版本和发布标签对不上,没有安装。'
641
629
  if (code === 'bad-package') return '下到的安装包读不懂,没有安装。'
642
- const base = `安装没有成功。${hint}`
643
- if (extra.packageName) return `${base}Cordis 里 qq-bridge 的包名已经改成实际安装的 ${extra.packageName}。请重启 dsh-web,设置页才会出来。`
644
- return base
630
+ return `安装没有成功。${hint}`
645
631
  }
646
632
 
647
- function restartError() {
648
- return '新版本已经装上,但面板没有重启。请自己重启 dsh-web,装好的才会生效。重启后请刷新页面,登录口令可能会变。'
633
+ function restartError(channel) {
634
+ const via = channel === 'npm' ? '从 npm ' : channel === 'github' ? '从 GitHub ' : ''
635
+ return `新版本已经${via}装上,但没能自动重启面板。请自己重启 dsh-web,然后刷新页面。登录口令可能会变。`
649
636
  }
650
637
 
651
638
  function headerValue(response, name) {
@@ -1538,46 +1525,75 @@ setTimeout(() => {
1538
1525
  }, delay)
1539
1526
  `.trim()
1540
1527
 
1528
+ /** 具体服务挂在 user@UID.service 下面时,重启要走用户实例,不能重启整个登录会话。 */
1529
+ function systemdUserManager(text) {
1530
+ return /\/user@\d+\.service(?:\/|$)/.test(String(text ?? ''))
1531
+ }
1532
+
1533
+ function systemdRunArgs(unit, user) {
1534
+ const scope = user ? ['--user'] : []
1535
+ return [
1536
+ ...scope,
1537
+ '--quiet',
1538
+ '--collect',
1539
+ '--on-active=3s',
1540
+ '--timer-property=AccuracySec=1s',
1541
+ '--',
1542
+ 'systemctl',
1543
+ ...scope,
1544
+ 'restart',
1545
+ unit,
1546
+ ]
1547
+ }
1548
+
1549
+ function cgroupText(input) {
1550
+ if (input.cgroup != null) return String(input.cgroup)
1551
+ const read = typeof input.readCgroup === 'function' ? input.readCgroup : readOwnCgroup
1552
+ try {
1553
+ return String(read() ?? '')
1554
+ } catch {
1555
+ return ''
1556
+ }
1557
+ }
1558
+
1559
+ function reexecPlan(input) {
1560
+ return {
1561
+ mode: 'reexec',
1562
+ execPath: input.execPath || process.execPath,
1563
+ argv: Array.isArray(input.argv) ? input.argv : process.argv.slice(1),
1564
+ pid: input.pid || process.pid,
1565
+ cwd: input.cwd || process.cwd(),
1566
+ delayMs: RESTART_DELAY_MS,
1567
+ }
1568
+ }
1569
+
1541
1570
  /**
1542
1571
  * 重启计划。systemd 里只重启 cgroup 里那个具体服务,用 systemd-run 把动作放到服务外面。
1543
- * 认不出单独服务时不重启整个登录会话。其余情况等响应发出后,按当前进程的命令行再拉起一次。
1572
+ * 没传入 cgroup 时读本进程的 /proc/self/cgroup。用户服务加 --user。
1573
+ * 认不出单独服务时计划是 manual:不重启整个登录会话。其余情况按当前进程的命令行再拉起一次。
1544
1574
  */
1545
1575
  export function planPanelRestart(input = {}) {
1546
1576
  const platform = input.platform || process.platform
1547
1577
  const env = input.env || process.env
1548
1578
  const invocation = platform !== 'win32' && Boolean(env && (env.INVOCATION_ID || env.NOTIFY_SOCKET))
1549
- const unit = invocation ? systemdUnitFromCgroup(input.cgroup ?? '') : null
1550
- if (invocation && !unit) return { mode: 'manual' }
1579
+ const cgroup = invocation ? cgroupText(input) : ''
1580
+ const unit = invocation ? systemdUnitFromCgroup(cgroup) : null
1551
1581
  if (unit) {
1582
+ const user = systemdUserManager(cgroup)
1552
1583
  return {
1553
1584
  mode: 'systemd',
1554
1585
  command: 'systemd-run',
1555
- args: ['--quiet', '--collect', '--on-active=3s', '--timer-property=AccuracySec=1s', '--', 'systemctl', 'restart', unit],
1586
+ args: systemdRunArgs(unit, user),
1556
1587
  unit,
1588
+ scope: user ? 'user' : 'system',
1557
1589
  }
1558
1590
  }
1559
- return {
1560
- mode: 'reexec',
1561
- execPath: input.execPath || process.execPath,
1562
- argv: Array.isArray(input.argv) ? input.argv : process.argv.slice(1),
1563
- pid: input.pid || process.pid,
1564
- cwd: input.cwd || process.cwd(),
1565
- delayMs: RESTART_DELAY_MS,
1566
- }
1591
+ if (invocation) return { mode: 'manual' }
1592
+ return reexecPlan(input)
1567
1593
  }
1568
1594
 
1569
- /**
1570
- * @param {{ plan?: ReturnType<typeof planPanelRestart>, spawnImpl?: typeof spawn, env?: NodeJS.ProcessEnv, platform?: string, cgroup?: string, execPath?: string, argv?: string[], pid?: number, cwd?: string }} input
1571
- */
1572
- export async function schedulePanelRestart(input = {}) {
1573
- const plan = input.plan || planPanelRestart(input)
1574
- if (plan.mode === 'manual') return { ok: false, mode: 'manual', error: restartError() }
1595
+ async function spawnReexec(plan, input) {
1575
1596
  const spawnImpl = input.spawnImpl || spawn
1576
- if (plan.mode === 'systemd') {
1577
- const ran = await runChild(spawnImpl, plan.command, plan.args, { timeoutMs: 20000, stdio: 'ignore' })
1578
- if (!ran.ok) return { ok: false, mode: 'systemd', error: restartError() }
1579
- return { ok: true, mode: 'systemd' }
1580
- }
1581
1597
  const payload = JSON.stringify({
1582
1598
  pid: plan.pid,
1583
1599
  cwd: plan.cwd,
@@ -1607,6 +1623,29 @@ export async function schedulePanelRestart(input = {}) {
1607
1623
  return { ok: true, mode: 'reexec' }
1608
1624
  }
1609
1625
 
1626
+ /**
1627
+ * 先按计划重启。systemd-run 失败,或认不出单独服务时,改为重启当前进程,不重启整个登录。
1628
+ * 两条都做不到才返回失败。
1629
+ * @param {{ plan?: ReturnType<typeof planPanelRestart>, spawnImpl?: typeof spawn, env?: NodeJS.ProcessEnv, platform?: string, cgroup?: string, readCgroup?: () => string, execPath?: string, argv?: string[], pid?: number, cwd?: string }} input
1630
+ */
1631
+ export async function schedulePanelRestart(input = {}) {
1632
+ const plan = input.plan || planPanelRestart(input)
1633
+ const spawnImpl = input.spawnImpl || spawn
1634
+ if (plan.mode === 'systemd') {
1635
+ const ran = await runChild(spawnImpl, plan.command, plan.args, { timeoutMs: 20000, stdio: 'ignore' })
1636
+ if (ran.ok) return { ok: true, mode: 'systemd', unit: plan.unit, scope: plan.scope }
1637
+ const fallback = await spawnReexec(reexecPlan(input), { ...input, spawnImpl })
1638
+ if (fallback.ok) return fallback
1639
+ return { ok: false, mode: 'manual', error: restartError() }
1640
+ }
1641
+ if (plan.mode === 'manual') {
1642
+ const fallback = await spawnReexec(reexecPlan(input), { ...input, spawnImpl })
1643
+ if (fallback.ok) return fallback
1644
+ return { ok: false, mode: 'manual', error: restartError() }
1645
+ }
1646
+ return spawnReexec(plan, { ...input, spawnImpl })
1647
+ }
1648
+
1610
1649
  const CORDIS_ID = 'qq-bridge'
1611
1650
 
1612
1651
  function isWritablePackageName(name) {
@@ -1783,10 +1822,8 @@ function alignNote(input, expectedVersion) {
1783
1822
  }
1784
1823
 
1785
1824
  function successMessage(tag, channel, note = '') {
1786
- const via = channel === 'npm'
1787
- ? '这次是从 npm 装的。'
1788
- : '这一版在 npm 上还没有,所以从 GitHub 装上。'
1789
- const base = `已经装上 ${tag},面板即将重启。请刷新页面;登录口令可能会变,用新的面板地址再打开。${via}现有设置还在,没有改动 NapCat。`
1825
+ const via = channel === 'npm' ? 'npm' : 'GitHub'
1826
+ const base = `已经从 ${via} 装上 ${tag},面板即将重启。`
1790
1827
  return note ? `${base}${note}` : base
1791
1828
  }
1792
1829
 
@@ -1810,25 +1847,31 @@ function installRunSpec(input, extra) {
1810
1847
  }
1811
1848
  }
1812
1849
 
1813
- /** 安装失败时,只有目标版本已经在磁盘上、并且进程已经停干净,才改包名。不在这里重启。 */
1814
- function failInstall(input, decision, ran, command, install) {
1850
+ /** 磁盘上的已安装包是不是这次要装的版本。只读 package.json,不改文件。 */
1851
+ function installedMatches(input, version) {
1852
+ const profileDir = profileDirOf(input)
1853
+ if (profileDir === '' || !version) return false
1854
+ const scoped = readInstalledManifest(join(profileDir, 'node_modules', '@fengye_nobug', 'dsh-qq-bot'))
1855
+ const legacy = readInstalledManifest(join(profileDir, 'node_modules', 'dsh-qq-bot'))
1856
+ return [scoped, legacy].some((item) => item && compareSemver(item.version, version) === 0)
1857
+ }
1858
+
1859
+ /**
1860
+ * 安装命令没返回成功时:进程已经停干净,并且磁盘上已经是目标版本,就改包名并安排重启。
1861
+ * 进程还在,或版本对不上,不重启。
1862
+ */
1863
+ async function failInstall(input, decision, ran, command, install, channel) {
1815
1864
  const code = ran?.code
1816
1865
  const leftover = ran?.leftover === true
1817
- let packageName = ''
1818
- const profileDir = profileDirOf(input)
1819
- if (!leftover && (code === 'timeout' || code === 'exit' || code === 'error') && profileDir !== '') {
1820
- try {
1821
- const aligned = alignCordisNames({
1822
- profileDir,
1823
- home: typeof input.home === 'string' ? input.home : '',
1824
- expectedVersion: decision.version,
1825
- })
1826
- if (aligned.changed.length > 0) packageName = aligned.name
1827
- } catch {
1828
- // 对不齐就只说明安装失败。
1829
- }
1830
- }
1831
- const message = installError(code, command, { packageName, leftover, pid: ran?.pid })
1866
+ const via = channel === 'npm' || channel === 'github' ? channel : decision.channel
1867
+ if (!leftover && (code === 'timeout' || code === 'exit' || code === 'error') && installedMatches(input, decision.version)) {
1868
+ return finishApply(
1869
+ input,
1870
+ { ...decision, command: command || decision.command, install: install || decision.install },
1871
+ via === 'npm' ? 'npm' : 'github',
1872
+ )
1873
+ }
1874
+ const message = installError(code, command, { leftover, pid: ran?.pid })
1832
1875
  return {
1833
1876
  ok: false,
1834
1877
  stage: 'install',
@@ -1891,7 +1934,7 @@ export async function applyPluginUpdate(input = {}) {
1891
1934
  ran = { ok: false, code: 'error' }
1892
1935
  }
1893
1936
  if (ran?.ok === true) return finishApply(input, decision, 'npm')
1894
- if (ran?.code !== 'unpublished') return failInstall(input, decision, ran, decision.command, decision.install)
1937
+ if (ran?.code !== 'unpublished') return failInstall(input, decision, ran, decision.command, decision.install, 'npm')
1895
1938
  channel = 'github'
1896
1939
  }
1897
1940
  const shown = shownInstall(channel, decision.version, repo)
@@ -1933,7 +1976,7 @@ export async function applyPluginUpdate(input = {}) {
1933
1976
  rmSync(dir, { recursive: true, force: true })
1934
1977
  }
1935
1978
  if (!ran || ran.ok !== true) {
1936
- return failInstall(input, { ...decision, install: shown.install, command: shown.command }, ran, shown.command, shown.install)
1979
+ return failInstall(input, { ...decision, install: shown.install, command: shown.command }, ran, shown.command, shown.install, 'github')
1937
1980
  }
1938
1981
  return finishApply(input, { ...decision, install: shown.install, command: shown.command }, 'github')
1939
1982
  }
@@ -1943,12 +1986,22 @@ async function finishApply(input, decision, channel) {
1943
1986
  let restart
1944
1987
  try {
1945
1988
  const schedule = input.scheduleRestart || schedulePanelRestart
1946
- restart = await schedule({ spawnImpl: input.spawnImpl, platform: input.platform })
1989
+ restart = await schedule({
1990
+ spawnImpl: input.spawnImpl,
1991
+ platform: input.platform,
1992
+ env: input.env,
1993
+ cgroup: input.cgroup,
1994
+ readCgroup: input.readCgroup,
1995
+ execPath: input.execPath,
1996
+ argv: input.argv,
1997
+ pid: input.pid,
1998
+ cwd: input.cwd,
1999
+ })
1947
2000
  } catch {
1948
2001
  restart = { ok: false, error: restartError() }
1949
2002
  }
1950
2003
  if (!restart || restart.ok !== true) {
1951
- const message = `${restart?.error || restartError()}${note}`
2004
+ const message = `${restartError(channel)}${note}`
1952
2005
  return {
1953
2006
  ok: false,
1954
2007
  stage: 'restart',