dsh-napcat-keeper 2.6.0 → 2.7.0

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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.0 — 插件自己的文件收进 `~/.dsh/dsh-napcat-keeper/`
4
+ - **为什么要动这个**:以前插件产生的东西全都摊在 `~/.dsh` 根上(配置、日志、状态、二维码、生成的 bat/vbs、提权脚本),和 DSH 自己的 `AGENT.md`、`profiles/`、一堆别的插件的文件混在一起,压根看不出哪些是它写的。现在一个目录装完自己的全部家当。
5
+ - **新布局**(`~/.dsh/dsh-napcat-keeper/`):
6
+
7
+ | 文件 | 是什么 | 老名字 |
8
+ | --- | --- | --- |
9
+ | `config.json` | 设置页存下来的配置 | `napcat-keeper-config.json` |
10
+ | `keeper.log` | 看门狗自己的日志 | `napcat-keeper.log` |
11
+ | `launch.log` | 拉起 QQ 那一路的输出 | `napcat-keeper-launch.log` |
12
+ | `state.json` | 给人看的运行状态快照 | `napcat-keeper-state.json` |
13
+ | `lock.json` | 跨进程拉起锁 | `napcat-keeper.lock.json` |
14
+ | `qr.png` | 待扫二维码的副本 | `napcat-qr.png` |
15
+ | `env.bat` | 「记住密码」中间层(含密码明文) | `napcat-keeper-env.bat` |
16
+ | `launch.vbs` | 无窗口壳,计划任务调它 | `napcat-keeper-launch.vbs` |
17
+ | `autogen.bat` / `autogen-node.bat` | 自动生成的启动脚本 | `napcat-autogen*.bat` |
18
+ | `setup/` | 提权交接脚本(apply.ps1 等) | `napcat-keeper-setup/` |
19
+ | `legacy/` | 老版本遗留的配置备份等,不参与运行 | —(原来散在根上) |
20
+
21
+ - **自动搬迁**:`migrateLegacyFiles()` 在启动解析配置时跑一次,能 rename 就 rename(同卷原子),跨卷才退回复制再删;目标已存在时不动旧文件,旧文件自己会落到 `legacy/`。已经搬过就不再重复(按目录记住)。
22
+ - **计划任务跟着改**:任务动作里写死的是 `wscript.exe "<vbs>"` 这种绝对路径,文件搬家后指向不存在的脚本 = 开机静默失败。新增 `repairTaskActions()`:只在动作对不上时用 `schtasks /change /tr` 就地改一次,**不 `/end`**,正在跑的 QQ 不会被踢下线。
23
+ - **不搬的**:`qq-bridge-state.json` 属于 `dsh-qq-bridge`(我们只是读一下它的时间戳),`profiles/`、`package.json` 属于 DSH 自身,全都留在原地。
24
+ - **验证**:`npm run smoke:client` PASS、`selftest:scan` 15/15、`selftest:setup` 45 通过 0 失败、`npm run check`(清单 + 隐私)双绿;真实环境里跑一次搬迁,配置/日志/二维码/中间层全部落到新目录,`napcat_status` 仍在线。
25
+ - 版本 2.7.0。
26
+
3
27
  ## 2.6.0 — 清死代码 + README 与代码对齐
4
28
  - **删掉的死代码**(都确认过没有任何调用点,删了行为不变):
5
29
  - v2.2.2 起停用的目录自动探测三件套 `looksLikeShellDir()` / `findShellDirs()` / `detectShellDir()` —— 从那次起 `shellDir` 只认用户在设置页选的,这几个函数只被彼此和调试用的 `__internals` 引用;
package/README.md CHANGED
@@ -54,7 +54,7 @@ dsh plugin --profile <profile> add dsh-napcat-keeper
54
54
  | **要快登的 QQ 号** | 填了它,插件优先挑「填的这个号」的启动脚本;连脚本都没有时,自动生成一个直接调 `NapCatWinBootMain.exe <号>` 的脚本 |
55
55
  | **OneBot HTTP 端口** | 跟 NapCat 网络配置里的 httpServers 一致(默认 3000) |
56
56
 
57
- 点 **保存设置** → 存到 `~/.dsh/napcat-keeper-config.json`,立刻热生效。
57
+ 点 **保存设置** → 存到 `~/.dsh/dsh-napcat-keeper/config.json`,立刻热生效。
58
58
  回「状态」看:在线会显示昵称和 QQ 号;离线就点「拉起 NapCat」。
59
59
 
60
60
  ## 分盘扫描:装在哪都能找到
@@ -115,7 +115,7 @@ DSH 自己是普通权限。插件要碰「装系统的位置」(往防火墙
115
115
  | 事 | 会做什么 | 要不要管理员 |
116
116
  | --- | --- | --- |
117
117
  | 自动启动脚本 | 在 `shellDir` 里补一个 `napcat.quick.auto.bat`(写死账号和 `NapCatWinBootMain.exe` 路径) | 看那个目录你能不能写,不能写才需要 |
118
- | 中间层 | 写 `napcat-keeper-env.bat`:把密码塞进环境变量再 call 启动脚本 | 不需要 |
118
+ | 中间层 | 写 `env.bat`:把密码塞进环境变量再 call 启动脚本 | 不需要 |
119
119
  | 计划任务 | 用 `schtasks` 建用户级任务(要不要隐藏黑框跟着设置走) | 不需要 |
120
120
  | 防火墙放行 | `New-NetFirewallRule` 放行 OneBot 端口入站 | 需要 |
121
121
 
@@ -126,7 +126,7 @@ DSH 自己是普通权限。插件要碰「装系统的位置」(往防火墙
126
126
  ## 桌面上那个黑框
127
127
 
128
128
  cscript / cmd 直接拉 NapCat,会多出一个控制台窗口挂在桌面上。默认行为是**不显示**:
129
- 计划任务的动作写的是 `wscript.exe "<DSH 家目录>\napcat-keeper-launch.vbs"`,这个壳里用
129
+ 计划任务的动作写的是 `wscript.exe "<DSH 家目录>\dsh-napcat-keeper\launch.vbs"`(绝对路径),这个壳里用
130
130
  `sh.Run cmd, 0, False` 把真正的 cmd 以隐藏窗口启动(`0` = SW_HIDE)。壳文件用 UTF-16LE 存,中文路径也认。
131
131
 
132
132
  不想要隐藏就把「拉起时不显示黑框」关掉,任务会退回直接跑 `cmd.exe`;已经跑起来的那条链想收窗口,
@@ -139,7 +139,7 @@ NapCat 的**快速登录令牌会失效**(实测几小时左右),失效后
139
139
 
140
140
  设置页「自动登录(可不填)」里勾 **记住密码** 时会先弹一段风险提示,要你自己点「我知道风险,仍然开启」才会生效。开启前请确认你接受这几条:
141
141
 
142
- 1. 密码以**明文**存进 `~/.dsh/napcat-keeper-config.json` 和插件生成的 `napcat-keeper-env.bat`,不加密。
142
+ 1. 密码以**明文**存进 `~/.dsh/dsh-napcat-keeper/config.json` 和同目录下插件生成的 `env.bat`,不加密。
143
143
  2. 任何能读你用户目录的程序都能捡走:同步盘、备份工具、其它软件、远控、临时借电脑的人。
144
144
  3. QQ 密码往往和 QQ 邮箱、部分游戏、微信登录入口共用一套身份,泄漏面不止一个号。
145
145
  4. 插件不打印密码,`/state` 也只回「存没存 + 长度」,但那只是少开一个口子,不等于安全。
@@ -172,7 +172,7 @@ QQ 那条进程链挂在本 DSH 实例下面——DSH 开着它就在,DSH 关
172
172
 
173
173
  两种 NapCat 装法的区别:
174
174
 
175
- - **Shell 型**(官方 `NapCat.Shell` 包、目录**顶层**有 `napcat.mjs`):插件会用上面这份 Node 生成 `~/.dsh/napcat-autogen-node.bat` 去跑 `node napcat.mjs` —— 这是 Node 运行时真正派上用场的地方;
175
+ - **Shell 型**(官方 `NapCat.Shell` 包、目录**顶层**有 `napcat.mjs`):插件会用上面这份 Node 生成 `~/.dsh/dsh-napcat-keeper/autogen-node.bat` 去跑 `node napcat.mjs` —— 这是 Node 运行时真正派上用场的地方;
176
176
  - **注入型**(OneKey 那种):NapCat 跑在 `QQ.exe` 自己的 Node service 里(`--type=utility --utility-sub-type=node.mojom.NodeService`),磁盘上根本没有 `node.exe`,外面的 Node 也替不了它。这条链路插件只负责调 `NapCatWinBootMain.exe`,跟 Node 无关。
177
177
 
178
178
  > 顺带一提:`versions\*\resources\app\napcat\napcat.mjs` 那份**不会**被当成入口——它是等 QQ 注入用的,拿普通 Node 直接跑必崩。
@@ -215,24 +215,32 @@ QQ 那条进程链挂在本 DSH 实例下面——DSH 开着它就在,DSH 关
215
215
  | `bridgeStaleSec` | `180` | `dsh-qq-bridge` 的 state 多久没更新就警告 |
216
216
  | `maxLogBytes` | `1048576` | 日志超这么多字节就滚动一次 |
217
217
 
218
- 剩下几个是路径类字段(`configPath` / `logPath` / `launcherLog` / `lockPath` / `statePath` / `qrCopyPath` / `bridgeStatePath`),默认都落在 `~/.dsh/` 下,一般不用碰。
218
+ 剩下几个是路径类字段(`configPath` / `logPath` / `launcherLog` / `lockPath` / `statePath` / `qrCopyPath` / `bridgeStatePath`),默认都落在 `~/.dsh/dsh-napcat-keeper/` 下(`bridgeStatePath` 例外,那是 `dsh-qq-bridge` 的文件,留在 `~/.dsh/` 根上),一般不用碰。
219
219
 
220
220
  ## 文件都在哪
221
221
 
222
+ 插件产生的东西全在 `~/.dsh/dsh-napcat-keeper/` 一个目录里,DSH 自己的目录不会被它塞满:
223
+
222
224
  ```
223
- ~/.dsh/napcat-keeper.log 看门狗日志(超 1MB 自动滚动)
224
- ~/.dsh/napcat-keeper-launch.log 启动脚本的原始输出(扫码提示都在这)
225
- ~/.dsh/napcat-keeper-config.json 设置页保存的配置
226
- ~/.dsh/napcat-keeper-env.bat 只在「记住密码」开启时生成:把密码塞进环境变量给启动脚本
227
- ~/.dsh/napcat-keeper-state.json 每轮 tick 的状态快照
228
- ~/.dsh/napcat-qr.png 最近一张二维码副本(扫码时会更新)
229
- ~/.dsh/napcat-autogen-node.bat Shell 型专用:拿 DSH 自带的 node 跑 napcat.mjs
230
- ~/.dsh/napcat-autogen.bat 没现成启动脚本时生成:直接调 NapCatWinBootMain.exe
231
- ~/.dsh/napcat-keeper-setup/ 提权交接目录:plan.json / apply.ps1 / apply-admin.cmd
232
- / undo-admin.cmd / result.json / run.log / README.txt
225
+ ~/.dsh/dsh-napcat-keeper/
226
+ ├── config.json 设置页保存的配置
227
+ ├── keeper.log 看门狗日志(超 1MB 自动滚动)
228
+ ├── launch.log 启动脚本的原始输出(扫码提示都在这)
229
+ ├── state.json 每轮 tick 的状态快照
230
+ ├── lock.json 跨进程拉起锁(防两个 DSH 同时拉)
231
+ ├── qr.png 最近一张二维码副本(扫码时更新)
232
+ ├── env.bat 只在「记住密码」开启时生成:把密码塞进环境变量给启动脚本
233
+ ├── launch.vbs 「自动创建计划任务」时生成的无窗口壳,计划任务调它
234
+ ├── autogen-node.bat Shell 型专用:拿 DSH 自带的 node 跑 napcat.mjs
235
+ ├── autogen.bat 没现成启动脚本时生成:直接调 NapCatWinBootMain.exe
236
+ ├── setup/ 提权交接目录:plan.json / apply.ps1 / apply-admin.cmd
237
+ │ / undo-admin.cmd / result.json / run.log / README.txt
238
+ └── legacy/ 老版本遗留的配置备份等,不参与运行,整个删掉也没事
233
239
  ```
234
240
 
235
- 那两份 `napcat-autogen*.bat` 只在需要时出现(每次拉起前按当前配置重写,删掉也会自己长回来)。
241
+ - `env.bat` 只在「记住密码」开启时才有;两份 `autogen*.bat` 只在需要时出现(每次拉起前按当前配置重写,删掉也会自己长回来)。整个目录删掉 = 插件回到没配置过的状态。
242
+ - **从老版本升上来(< 2.7.0)**:第一次启动会自动把原来散在 `~/.dsh` 根上的文件搬进这个目录(`napcat-keeper-config.json` → `config.json` 这种),改名一次就固定;计划任务里写死的旧路径也会一并改指新位置 —— 那一步用 `schtasks /change` 就地改,不结束正在跑的 QQ。老备份之类的零碎挪进 `legacy/`。
243
+ - `~/.dsh/qq-bridge-state.json` **不在**这个目录里,也不归这个插件:它由 `dsh-qq-bridge` 写,插件只是读一下它的时间戳,判断那边轮询还活着没有。
236
244
 
237
245
  ## 出事了先跑体检:`npm run check`
238
246
 
@@ -259,7 +267,7 @@ npm run check:privacy # 只查隐私(发版前必跑)
259
267
  ## 常见问题
260
268
 
261
269
  **「离线」但进程明明还在?**
262
- 端口没通。看 `napcat-keeper-launch.log`,多半是卡在扫码,或者 QQ 弹了验证窗。
270
+ 端口没通。看 `~/.dsh/dsh-napcat-keeper/launch.log`,多半是卡在扫码,或者 QQ 弹了验证窗。
263
271
 
264
272
  **刚关掉 DSH,QQ 就跟着退了?**
265
273
  正常,默认就是这样:QQ 挂在 DSH 下面,DSH 一走它就走。下次开 DSH 会重新拉一条。
package/lib/client.js CHANGED
@@ -253,7 +253,7 @@ window.__ModuleLoader__.load({
253
253
  setTimeout(拉, 900)
254
254
  return
255
255
  }
256
- set说('接口没响应,读不到状态(日志见 ~/.dsh/napcat-keeper.log)')
256
+ set说('接口没响应,读不到状态(日志见 ~/.dsh/dsh-napcat-keeper/keeper.log)')
257
257
  set态({ ok: false, config: null, runtime: {}, scan: {}, drives: [], log: [] })
258
258
  }
259
259
  拉()
@@ -576,7 +576,7 @@ window.__ModuleLoader__.load({
576
576
 
577
577
  /* ---------- 「记住密码」那一块:先讲风险,再让人自己决定 ---------- */
578
578
  const 风险条目 = [
579
- '密码会以明文写进本机两个地方:~/.dsh/napcat-keeper-config.json,以及插件生成的 napcat-keeper-env.bat。不加密、不做任何变换(MD5 那份是给 NapCat 兜底用的,同样等于密码)。',
579
+ '密码会以明文写进本机两个地方:~/.dsh/dsh-napcat-keeper/config.json,以及同目录下插件生成的 env.bat。不加密、不做任何变换(MD5 那份是给 NapCat 兜底用的,同样等于密码)。',
580
580
  '任何能读你这个用户目录的东西都能直接捡走:同步盘、备份工具、其它软件、远控木马、临时借你电脑的人。',
581
581
  'QQ 密码通常和 QQ 邮箱、部分游戏、微信的登录入口共用一套身份,泄漏的影响面不止这一个号。',
582
582
  '插件本身不打印密码、/state 接口也只回「存没存」和长度,但那只是少开一个泄漏口,不等于安全。',
package/lib/index.js CHANGED
@@ -1,9 +1,20 @@
1
1
  /**
2
- * dsh-napcat-keeper —— NapCat 看门狗 v2.6.0(可发布通用版)
2
+ * dsh-napcat-keeper —— NapCat 看门狗 v2.7.0(可发布通用版)
3
3
  *
4
4
  * 解决的问题:DSH 是桌面应用,重启 DSH 时 NapCat(QQ 协议端) 不会跟着起来,
5
5
  * 于是 dsh-qq-bridge 一直轮询一个死端口,你在 QQ 里说的话就石沉大海。
6
6
  *
7
+ * v2.7.0:自己的东西收进自己的目录(~/.dsh/dsh-napcat-keeper/)
8
+ * - 以前配置、日志、状态、二维码、生成的 bat/vbs、提权脚本全散在 ~/.dsh 根上,
9
+ * 和 DSH 自己的文件混在一起,一眼看不出哪些是插件产生的;
10
+ * - 现在统一落在 ~/.dsh/dsh-napcat-keeper/ 一个目录里,文件名去掉重复前缀:
11
+ * config.json / keeper.log / launch.log / state.json / lock.json / qr.png /
12
+ * env.bat / launch.vbs / autogen.bat / autogen-node.bat / setup/;
13
+ * - 老版本升级时 migrateLegacyFiles() 自动搬一次(能 rename 就 rename),
14
+ * 遗留的 *.bak、旧备份目录挪进 legacy/ 留档;计划任务里写死的旧路径由
15
+ * repairTaskActions() 用 /change 改指新位置,不 /end、不打扰正在跑的 QQ;
16
+ * - qq-bridge-state.json 不搬:那是 dsh-qq-bridge 的文件,只是被读一下。
17
+ *
7
18
  * v2.6.0:清死代码 + README 与代码对齐(行为一字没变)
8
19
  * - 删掉 v2.2.2 起就没人再调用的目录自动探测三件套(looksLikeShellDir / findShellDirs /
9
20
  * detectShellDir)和只被写过一次的 napcatRoot():配置不再有「悄悄替你填」的暗路,
@@ -45,7 +56,7 @@
45
56
  *
46
57
  * v2.3.0:提权交接 —— 需要管理员的活,交给「你点一下」的脚本
47
58
  * - 插件不再自己尝试要管理员(它本来也要不到):把要做的事摊成步骤写进
48
- * ~/.dsh/napcat-keeper-setup/plan.json,生成 apply.ps1 + apply-admin.cmd + undo-admin.cmd;
59
+ * ~/.dsh/dsh-napcat-keeper/setup/plan.json,生成 apply.ps1 + apply-admin.cmd + undo-admin.cmd;
49
60
  * - 脚本先数一遍有没有真需要管理员的步骤:没有就直接干;有才 Start-Process -Verb RunAs
50
61
  * 自我提权 —— UAC 那个「是」就是用户亲手按的确认键,插件全程普通权限;
51
62
  * - 每步结果写 result.json,设置页「读结果」说清楚哪条成了、哪条没成、有没有拿到管理员;
@@ -67,7 +78,7 @@
67
78
  * - NapCat 的快速登录 token 会失效(实测几小时就掉),失效后只剩手机扫码一条路;
68
79
  * - 打开 rememberPassword 后,拉起时会通过环境变量 ACCOUNT / NAPCAT_QUICK_PASSWORD
69
80
  * (外加 MD5 兜底)交给 NapCat,token 一失效它自己用密码重新登录,不用人管;
70
- * - 密码只落在本机两处:~/.dsh/napcat-keeper-config.json 与生成的 napcat-keeper-env.bat,
81
+ * - 密码只落在本机两处:~/.dsh/dsh-napcat-keeper/config.json 与生成的 env.bat,
71
82
  * 所以设置页打开它之前会先摆出风险说明,要人自己点确认;关掉或点「清除密码」就删干净。
72
83
  * - 密码绝不进日志、绝不进 /state 返回值(只回「已保存」和长度)。
73
84
  *
@@ -100,6 +111,7 @@ import {
100
111
  readFileSync,
101
112
  readSync,
102
113
  readdirSync,
114
+ renameSync,
103
115
  statSync,
104
116
  unlinkSync,
105
117
  writeFileSync,
@@ -110,7 +122,7 @@ import { homedir, tmpdir } from 'node:os'
110
122
  import path from 'node:path'
111
123
 
112
124
  export const name = 'napcat-keeper'
113
- export const version = '2.6.0'
125
+ export const version = '2.7.0'
114
126
 
115
127
  /** tools 用来注册 napcat_* 工具;webServer 用来挂设置界面接口。拿不到也不至于把 DSH 拖崩。 */
116
128
  export const inject = ['tools', 'webServer']
@@ -227,15 +239,102 @@ function dshHome() {
227
239
  return process.env.DSH_HOME || path.join(homedir(), '.dsh')
228
240
  }
229
241
 
230
- /** 设置界面写的那份配置(JSON,无 BOM) */
231
- function configFilePath(cfgOrHome) {
232
- const home = typeof cfgOrHome === 'string' ? cfgOrHome : dshHome()
233
- if (cfgOrHome && typeof cfgOrHome === 'object' && cfgOrHome.configPath) return cfgOrHome.configPath
234
- return path.join(home, 'napcat-keeper-config.json')
242
+ /**
243
+ * 插件自己的家:~/.dsh/dsh-napcat-keeper。
244
+ * 配置、日志、状态、二维码、生成的 bat / vbs、提权脚本全在这一个目录里 ——
245
+ * 老版本是直接摊在 ~/.dsh 根上的,2.7.0 起收拢,升级时 migrateLegacyFiles() 自动搬。
246
+ */
247
+ function keeperDir() {
248
+ const dir = path.join(dshHome(), 'dsh-napcat-keeper')
249
+ try {
250
+ mkdirSync(dir, { recursive: true })
251
+ } catch {}
252
+ return dir
253
+ }
254
+
255
+ /* 老名字 → 新名字。搬过一次就不再动(同目录 rename 是原子的)。 */
256
+ const LEGACY_FILES = [
257
+ ['napcat-keeper-config.json', 'config.json'],
258
+ ['napcat-keeper.log', 'keeper.log'],
259
+ ['napcat-keeper-launch.log', 'launch.log'],
260
+ ['napcat-keeper.lock.json', 'lock.json'],
261
+ ['napcat-keeper-state.json', 'state.json'],
262
+ ['napcat-qr.png', 'qr.png'],
263
+ ['napcat-keeper-env.bat', 'env.bat'],
264
+ ['napcat-keeper-launch.vbs', 'launch.vbs'],
265
+ ['napcat-autogen-node.bat', 'autogen-node.bat'],
266
+ ['napcat-autogen.bat', 'autogen.bat'],
267
+ ]
268
+ const LEGACY_DIRS = [['napcat-keeper-setup', 'setup']]
269
+ /** 老版本留下的零碎(配置备份、bom-backup 之类):扫一遍挪进 legacy/,不参与运行 */
270
+ const LEGACY_LEFTOVER = /^(napcat-keeper[.-]|napcat-qr\.|napcat-autogen)/
271
+ const CANONICAL_LEGACY = new Set([...LEGACY_FILES, ...LEGACY_DIRS].map(([oldName]) => oldName))
272
+
273
+ let migratedFor = ''
274
+ /** 上一次搬迁搬了几个(给 apply 写一行日志用) */
275
+ let lastMigrated = 0
276
+
277
+ function moveOne(from, to) {
278
+ try {
279
+ if (existsSync(to)) return false
280
+ renameSync(from, to)
281
+ return true
282
+ } catch {
283
+ try {
284
+ copyFileSync(from, to)
285
+ unlinkSync(from)
286
+ return true
287
+ } catch {
288
+ return false
289
+ }
290
+ }
235
291
  }
236
292
 
237
- function readConfigFile(home) {
238
- const file = configFilePath(home)
293
+ /** 把老版本散在 ~/.dsh 根上的文件搬进 keeperDir;重复调用是空操作,返回搬了几个 */
294
+ function migrateLegacyFiles(dir) {
295
+ const target = dir || keeperDir()
296
+ if (migratedFor === target) return 0
297
+ migratedFor = target
298
+ const home = dshHome()
299
+ let moved = 0
300
+ for (const [oldName, newName] of LEGACY_FILES) {
301
+ try {
302
+ if (!existsSync(path.join(home, oldName))) continue
303
+ } catch {
304
+ continue
305
+ }
306
+ if (moveOne(path.join(home, oldName), path.join(target, newName))) moved += 1
307
+ }
308
+ for (const [oldName, newName] of LEGACY_DIRS) {
309
+ try {
310
+ const from = path.join(home, oldName)
311
+ if (existsSync(from) && !existsSync(path.join(target, newName)) && moveOne(from, path.join(target, newName))) {
312
+ moved += 1
313
+ }
314
+ } catch {}
315
+ }
316
+ try {
317
+ for (const entry of readdirSync(home)) {
318
+ if (CANONICAL_LEGACY.has(entry) || !LEGACY_LEFTOVER.test(entry)) continue
319
+ try {
320
+ mkdirSync(path.join(target, 'legacy'), { recursive: true })
321
+ if (moveOne(path.join(home, entry), path.join(target, 'legacy', entry))) moved += 1
322
+ } catch {}
323
+ }
324
+ } catch {}
325
+ lastMigrated = moved
326
+ return moved
327
+ }
328
+
329
+ /** 设置界面写的那份配置(JSON,无 BOM):<keeperDir>/config.json */
330
+ function configFilePath(cfgOrDir) {
331
+ if (cfgOrDir && typeof cfgOrDir === 'object' && cfgOrDir.configPath) return cfgOrDir.configPath
332
+ const dir = typeof cfgOrDir === 'string' ? cfgOrDir : keeperDir()
333
+ return path.join(dir, 'config.json')
334
+ }
335
+
336
+ function readConfigFile(dir) {
337
+ const file = configFilePath(dir)
239
338
  try {
240
339
  if (!existsSync(file)) return {}
241
340
  } catch {
@@ -607,18 +706,22 @@ function normalizeConfig(cfg) {
607
706
 
608
707
  function resolveConfig(raw) {
609
708
  const home = dshHome()
709
+ const dir = keeperDir()
710
+ // 老版本的东西摊在 ~/.dsh 根上,先搬进 dir 再读配置(否则读的是搬走前的那份)
711
+ migrateLegacyFiles(dir)
610
712
  // 优先级:内置默认 < cordis 里的 config < 设置界面存下来的 json
611
- const cfg = { ...DEFAULTS, ...(raw || {}), ...readConfigFile(home) }
713
+ const cfg = { ...DEFAULTS, ...(raw || {}), ...readConfigFile(dir) }
612
714
  normalizeConfig(cfg)
613
715
  // v2.2.2:shellDir 不再自动探测/自动填,空着就等用户在设置页里扫盘选或者手填
614
716
  if (!cfg.launcherScript) cfg.launcherScript = detectLauncher(cfg.shellDir, cfg.launchAccount)
615
- if (!cfg.logPath) cfg.logPath = path.join(home, 'napcat-keeper.log')
616
- if (!cfg.launcherLog) cfg.launcherLog = path.join(home, 'napcat-keeper-launch.log')
617
- if (!cfg.lockPath) cfg.lockPath = path.join(home, 'napcat-keeper.lock.json')
618
- if (!cfg.statePath) cfg.statePath = path.join(home, 'napcat-keeper-state.json')
619
- if (!cfg.qrCopyPath) cfg.qrCopyPath = path.join(home, 'napcat-qr.png')
717
+ if (!cfg.logPath) cfg.logPath = path.join(dir, 'keeper.log')
718
+ if (!cfg.launcherLog) cfg.launcherLog = path.join(dir, 'launch.log')
719
+ if (!cfg.lockPath) cfg.lockPath = path.join(dir, 'lock.json')
720
+ if (!cfg.statePath) cfg.statePath = path.join(dir, 'state.json')
721
+ if (!cfg.qrCopyPath) cfg.qrCopyPath = path.join(dir, 'qr.png')
722
+ // qq-bridge-state.json 是 dsh-qq-bridge 写的,不归我们管,留在 ~/.dsh 根上
620
723
  if (!cfg.bridgeStatePath) cfg.bridgeStatePath = path.join(home, 'qq-bridge-state.json')
621
- if (!cfg.configPath) cfg.configPath = configFilePath(home)
724
+ if (!cfg.configPath) cfg.configPath = configFilePath(dir)
622
725
  return cfg
623
726
  }
624
727
 
@@ -963,7 +1066,7 @@ function resolveLauncherPath(cfg) {
963
1066
  if (standaloneMjs) {
964
1067
  const node = resolveNode(cfg)
965
1068
  if (node.path) {
966
- const file = path.join(dshHome(), 'napcat-autogen-node.bat')
1069
+ const file = path.join(keeperDir(), 'autogen-node.bat')
967
1070
  const args = String(cfg.launchArgs || '').trim()
968
1071
  const body =
969
1072
  '@echo off\r\n' +
@@ -991,7 +1094,7 @@ function resolveLauncherPath(cfg) {
991
1094
  // 用户只填了 QQ 号:直接调 NapCatWinBootMain.exe,不依赖别人的 quick bat
992
1095
  const boot = findBootMain(cfg.shellDir)
993
1096
  if (boot) {
994
- const file = path.join(dshHome(), 'napcat-autogen.bat')
1097
+ const file = path.join(keeperDir(), 'autogen.bat')
995
1098
  const body =
996
1099
  '@echo off\r\n' +
997
1100
  'chcp 65001 >nul\r\n' +
@@ -1193,16 +1296,16 @@ function findStandaloneNapcatMjs(dir) {
1193
1296
  return ''
1194
1297
  }
1195
1298
 
1196
- /** 无窗口壳 .vbs 的路径(跟日志放一起) */
1299
+ /** 无窗口壳 .vbs 的路径(跟日志放同一个目录) */
1197
1300
  function taskWrapperPath(cfg) {
1198
- const dir = path.dirname(cfg.launcherLog || path.join(dshHome(), 'napcat-keeper-launch.log'))
1199
- return path.join(dir, 'napcat-keeper-launch.vbs')
1301
+ const dir = path.dirname(cfg.launcherLog || path.join(keeperDir(), 'launch.log'))
1302
+ return path.join(dir, 'launch.vbs')
1200
1303
  }
1201
1304
 
1202
1305
  /** 「记住密码」时生成的那个 bat:设好环境变量,再 call 真正的启动脚本 */
1203
1306
  function envBatPath(cfg) {
1204
- const dir = path.dirname(cfg.launcherLog || path.join(dshHome(), 'napcat-keeper-launch.log'))
1205
- return path.join(dir, 'napcat-keeper-env.bat')
1307
+ const dir = path.dirname(cfg.launcherLog || path.join(keeperDir(), 'launch.log'))
1308
+ return path.join(dir, 'env.bat')
1206
1309
  }
1207
1310
 
1208
1311
  /** 密码是否真的在生效(三个条件缺一不可) */
@@ -1359,6 +1462,51 @@ function ensureTask(cfg) {
1359
1462
  return { ok: true, created: true }
1360
1463
  }
1361
1464
 
1465
+ /**
1466
+ * 计划任务的动作是写死的绝对路径(wscript.exe "<vbs>")。文件搬了家、或者日志目录换了,
1467
+ * 任务就指向一个不存在的脚本 —— 开机静默失败,人还以为是别的原因。
1468
+ *
1469
+ * 这里只在「动作里的路径已经不是当前该用的那个」时改一次,用 /change 就地改:
1470
+ * 不动 /end,正在跑的那条 QQ 进程链不会被踢下来。
1471
+ */
1472
+ function repairTaskActions(cfg) {
1473
+ if (!cfg.taskName) return { ok: true, skipped: true }
1474
+ if (state.taskActionsChecked) return { ok: true, skipped: true }
1475
+ let xml = ''
1476
+ try {
1477
+ xml = captureSync('schtasks.exe', ['/query', '/tn', cfg.taskName, '/xml'], 8000)
1478
+ } catch {
1479
+ xml = ''
1480
+ }
1481
+ // 任务不存在(或查不动):不用管,真要建是 ensureTask 的活
1482
+ if (!xml || !xml.trim()) return { ok: true, skipped: true }
1483
+ const bat = resolveLauncherPath(cfg)
1484
+ if (!bat) return { ok: true, skipped: true }
1485
+ const wanted = cfg.hideConsole !== false ? taskWrapperPath(cfg) : passwordActive(cfg) ? envBatPath(cfg) : bat
1486
+ state.taskActionsChecked = true
1487
+ if (xml.includes(wanted)) return { ok: true, skipped: true }
1488
+ try {
1489
+ let inner = ''
1490
+ if (cfg.hideConsole !== false) {
1491
+ const vbs = writeTaskWrapper(cfg, bat, true)
1492
+ inner = `wscript.exe "${vbs}"`
1493
+ } else {
1494
+ inner = taskCommandLine(cfg, bat)
1495
+ }
1496
+ execFileSync('schtasks.exe', ['/change', '/tn', cfg.taskName, '/tr', inner], {
1497
+ stdio: 'ignore',
1498
+ windowsHide: true,
1499
+ timeout: 15000,
1500
+ })
1501
+ state.taskReady = undefined
1502
+ log(cfg, `task "${cfg.taskName}" 动作已改指到 ${wanted}`)
1503
+ return { ok: true, repaired: true }
1504
+ } catch (error) {
1505
+ state.taskActionsChecked = false
1506
+ return { ok: false, error: String((error && error.message) || error) }
1507
+ }
1508
+ }
1509
+
1362
1510
  /* ── 提权交接:插件不自己去要管理员,改成给你一个双击就行的脚本 ──────────────
1363
1511
  *
1364
1512
  * 为什么要这样:DSH 自己是普通权限进程。插件偶尔要做几件「可能要管理员」的事
@@ -1374,7 +1522,7 @@ function ensureTask(cfg) {
1374
1522
  * 4. 每步结果写 result.json,设置页读回来说清楚哪条成了、哪条没成;
1375
1523
  * 5. 另外给一个 undo-admin.cmd,反着把插件建过的东西删掉。
1376
1524
  *
1377
- * 所有产物只在 ~/.dsh/napcat-keeper-setup 一个目录里,整个删掉就等于撤回。
1525
+ * 所有产物只在 ~/.dsh/dsh-napcat-keeper/setup 一个目录里,整个删掉就等于撤回。
1378
1526
  */
1379
1527
 
1380
1528
  const SETUP_NEEDS = {
@@ -1397,7 +1545,8 @@ const SETUP_NEEDS = {
1397
1545
  }
1398
1546
 
1399
1547
  function setupPaths() {
1400
- const dir = path.join(dshHome(), 'napcat-keeper-setup')
1548
+ const dir = path.join(keeperDir(), 'setup')
1549
+ migrateLegacyFiles()
1401
1550
  return {
1402
1551
  dir,
1403
1552
  ps1: path.join(dir, 'apply.ps1'),
@@ -1574,7 +1723,7 @@ function buildSetupSteps(cfg, wants) {
1574
1723
  ].join('\r\n')
1575
1724
  steps.push({
1576
1725
  id: 'task-wrapper',
1577
- label: '写无窗口壳 napcat-keeper-launch.vbs',
1726
+ label: '写无窗口壳 launch.vbs',
1578
1727
  kind: 'writeFile',
1579
1728
  path: vbs,
1580
1729
  admin: !dirWritable(path.dirname(vbs)),
@@ -3205,6 +3354,13 @@ export function apply(ctx, config) {
3205
3354
  `scanTimeout=${cfg.scanTimeoutSec}s, shell=${cfg.shellDir || '(未配置)'}, task=${cfg.taskName || '(不用)'}, ` +
3206
3355
  `node=${node.path ? `${node.version || '?'}(${node.source})` : '(没找到)'})`,
3207
3356
  )
3357
+ if (lastMigrated) {
3358
+ log(cfg, `已把 ${lastMigrated} 个文件从 ~/.dsh 根上搬进 ${keeperDir()}(老版本的散装布局)`)
3359
+ }
3360
+ if (cfg.taskName) {
3361
+ const repaired = repairTaskActions(cfg)
3362
+ if (repaired.ok && repaired.repaired) log(cfg, '计划任务已跟着文件位置改好')
3363
+ }
3208
3364
  registerTools(ctx, cfg)
3209
3365
  registerRoutes(ctx)
3210
3366
  armTimer(cfg)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-napcat-keeper",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "发现没有能够随着dsh打开自动启动napcat的插件,遂使用大肥鱼制作此插件,建议搭配Yuuyuko-uu的dsh-qq-bridge使用~",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -24,7 +24,7 @@ writeFileSync(path.join(fakeShell, 'NapCatWinBootMain.exe'), '', 'utf8')
24
24
  const cfg = internals.resolveConfig({})
25
25
  cfg.shellDir = fakeShell
26
26
  cfg.launcherScript = 'napcat.quick2.bat'
27
- cfg.launcherLog = path.join(fakeLogDir, 'napcat-keeper-launch.log')
27
+ cfg.launcherLog = path.join(fakeLogDir, 'launch.log')
28
28
  cfg.taskName = 'NapCat-Keeper-Selftest'
29
29
  cfg.launchAccount = '10001'
30
30
  cfg.launchArgs = ''
@@ -139,10 +139,10 @@ if (first) {
139
139
  ok(first.results.length === 4, '4 条结果')
140
140
  }
141
141
  ok(existsSync(path.join(fakeShell, 'napcat.quick.auto.bat')), 'launcher: 启动脚本写出来了')
142
- ok(existsSync(path.join(fakeLogDir, 'napcat-keeper-env.bat')), 'envbat: 中间层写出来了')
143
- ok(existsSync(path.join(fakeLogDir, 'napcat-keeper-launch.vbs')), 'task-wrapper: vbs 写出来了')
142
+ ok(existsSync(path.join(fakeLogDir, 'env.bat')), 'envbat: 中间层写出来了')
143
+ ok(existsSync(path.join(fakeLogDir, 'launch.vbs')), 'task-wrapper: vbs 写出来了')
144
144
 
145
- const envBat = readFileSync(path.join(fakeLogDir, 'napcat-keeper-env.bat'), 'utf8')
145
+ const envBat = readFileSync(path.join(fakeLogDir, 'env.bat'), 'utf8')
146
146
  ok(envBat.includes('NAPCAT_QUICK_PASSWORD=selftest-pass-123'), 'env bat 里带密码(这是它的用途)')
147
147
  ok(envBat.includes('call "'), 'env bat 会 call 真启动脚本')
148
148
 
@@ -186,8 +186,8 @@ const undone = internals.readSetupResult()
186
186
  ok(undone && undone.mode === 'undo', 'result 里 mode = undo')
187
187
  ok(undone && undone.ok === true, '撤回每步都成', JSON.stringify(undone && undone.results.filter((r) => !r.ok)))
188
188
  ok(!existsSync(path.join(fakeShell, 'napcat.quick.auto.bat')), '启动脚本删掉了')
189
- ok(!existsSync(path.join(fakeLogDir, 'napcat-keeper-env.bat')), '中间层删掉了(密码不留在磁盘)')
190
- ok(!existsSync(path.join(fakeLogDir, 'napcat-keeper-launch.vbs')), 'vbs 删掉了')
189
+ ok(!existsSync(path.join(fakeLogDir, 'env.bat')), '中间层删掉了(密码不留在磁盘)')
190
+ ok(!existsSync(path.join(fakeLogDir, 'launch.vbs')), 'vbs 删掉了')
191
191
  try {
192
192
  execFileSync('schtasks.exe', ['/query', '/tn', cfg.taskName], { stdio: 'ignore', windowsHide: true })
193
193
  ok(false, '计划任务还在(应该删掉)')
@@ -60,9 +60,9 @@ function collect(tree) {
60
60
  render(); // 第一次:态=null 的 loading 分支
61
61
  states[0] = {
62
62
  ok: true,
63
- version: '2.2.2',
63
+ version: '2.7.0',
64
64
  config: {
65
- configPath: 'C:/x/napcat-keeper-config.json', logPath: 'C:/x/napcat-keeper.log',
65
+ configPath: 'C:/Users/x/.dsh/dsh-napcat-keeper/config.json', logPath: 'C:/Users/x/.dsh/dsh-napcat-keeper/keeper.log',
66
66
  shellDir: 'C:/Users/x/NapCat/OneKey/NapCat.52230.Shell', launcherScript: 'napcat.quick2.bat',
67
67
  launchAccount: '1000000000', launchArgs: '', onebotPort: 3000, accessToken: '',
68
68
  taskName: 'NapCat-Keeper-Launch', rememberPassword: true, autoCreateTask: true,
@@ -78,10 +78,10 @@ states[0] = {
78
78
  scan: { running: false, found: [], drives: ['C:'], visited: 0, elapsedMs: 0, limitHit: false, cancelled: false },
79
79
  drives: ['C:', 'D:', 'E:'],
80
80
  setup: {
81
- dir: 'C:/Users/x/.dsh/napcat-keeper-setup',
82
- cmdPath: 'C:/Users/x/.dsh/napcat-keeper-setup/apply-admin.cmd',
83
- ps1Path: 'C:/Users/x/.dsh/napcat-keeper-setup/apply.ps1',
84
- undoCmdPath: 'C:/Users/x/.dsh/napcat-keeper-setup/undo-admin.cmd',
81
+ dir: 'C:/Users/x/.dsh/dsh-napcat-keeper/setup',
82
+ cmdPath: 'C:/Users/x/.dsh/dsh-napcat-keeper/setup/apply-admin.cmd',
83
+ ps1Path: 'C:/Users/x/.dsh/dsh-napcat-keeper/setup/apply.ps1',
84
+ undoCmdPath: 'C:/Users/x/.dsh/dsh-napcat-keeper/setup/undo-admin.cmd',
85
85
  exists: true,
86
86
  needs: [
87
87
  { id: 'launcher', label: '自动启动脚本', hint: '补一个 napcat.quick.auto.bat', ready: true, why: '', needAdmin: false },
@@ -179,7 +179,7 @@ check('显示上次生成 2 步', /上次生成:.*2 步/.test(setupOut.text));
179
179
  check('显示上次执行 1/2 步成', /执行:1\/2 步成/.test(setupOut.text));
180
180
  check('显示用了管理员', setupOut.text.includes('用了管理员'));
181
181
  check('失败项有 ✗', setupOut.text.includes('✗ 放行防火墙:没权限'));
182
- check('显示脚本目录', setupOut.text.includes('napcat-keeper-setup'));
182
+ check('显示脚本目录', setupOut.text.includes('dsh-napcat-keeper'));
183
183
 
184
184
  // 没生成过 / 还没跑过
185
185
  const keepSetup = states[0].setup;