dsh-napcat-keeper 2.7.1 → 2.7.3

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.3 — 修「设置页保存不进去」+ README 补「装不上」的两条排障
4
+ - **修了个真 bug,2.7.0 起就有**:设置页点「保存」、「用这个」、或者「清除密码」,值当场弹回原样,什么都没存下来。起因是 2.7.0 把插件自己的文件收进 `~/.dsh/dsh-napcat-keeper/` 时,只把**读取**改成了新目录,**写入**还留在老地方 `~/.dsh/config.json` —— 于是存进一个没人读的文件,界面重新渲染时拿到的还是旧值。2.7.0 / 2.7.1 / 2.7.2 都中招。
5
+ - 现在读和写认同一个目录(`saveAndApply` 与「清除密码」都改走 `keeperDir()`)。
6
+ - **老版本留下的那份被误写的配置会被自动认领**:启动时如果发现 `~/.dsh/config.json` 里有 `shellDir` / `onebotPort` 这类自家字段、而且比插件目录里的配置新,就把它的内容并进真配置(多半正是你当初点了保存却没生效的那次),原文归档进 `legacy/config.adopted-*.json`,认领前的旧配置备份成 `legacy/config.before-adopt.json`。不像插件配置的 `config.json` 一概不碰,也不会拿旧文件顶掉更新的配置。
7
+ - 补了 `pnpm selftest:save`(12 项):保存真的落到新目录、认领逻辑、keeper 更新时不认领、别人的 `config.json` 不动。
8
+ - **没发版,顺带补了 README 两条排障**:有人从 GitHub 装插件报 `'git' is not recognized as an internal or external command`,查下来是对方机器没装 Git —— `github:` 规格要 pnpm 调外部 `git ls-remote`,插件一个字节都还没下载,跟插件本身无关(npm 上的 2.7.2 一直正常)。
9
+ - 「安装」那节加了一句:**别加 `github:` 前缀**,用 npm 名装全程不碰 git。
10
+ - 「常见问题」补一条,三条路:npm 名装 / 装 Git 后**彻底重启 DSH**(子进程继承 DSH 启动时的 PATH,不重启照旧报错)/ 无 git 时走 `codeload` 的 tarball。
11
+ - 又有人报 `无法识别这个包名或地址:not a package name the registry accepts`:那是 DSH「设置 → 插件 → 添加插件」输入框的**规格校验**,跟插件和网络都无关 —— 他把整条命令(或 `add dsh-napcat-keeper` 这种)粘进了只认「包名 / 仓库地址 / 本地目录」的那一栏。桌面版安装目录里根本没有 `dsh` 命令,用户只能走界面,于是把命令行原样贴进去。
12
+ - README「安装」改成**先桌面版点界面、再命令行**两条路,并点明那一栏只填 `dsh-napcat-keeper`;「常见问题」再加一条,用表格列清楚哪些写法会被拒(整条命令 / 多出的 `add` / 大写 / 全角标点)。
13
+ - 已经在 2.7.0~2.7.2 上装过的,升级到 2.7.3 就行:配置会被自动认领回来,不用手改任何文件。
14
+
15
+ ## 2.7.2 — 根上一点不留
16
+ - **修的什么**:2.7.0 的搬迁兜底扫描会跳过「规范老名字」(`napcat-keeper-state.json` 这种已经安排好去处的),本意是别把刚搬完的东西又归一次档。但碰上**目标位已经有同名文件**的情况(新目录里先躺着一份 `state.json`),根上那份老的既搬不走、也不归档,就永远留在 `~/.dsh` 根上了 —— 正是这次要收拾的那种乱。
17
+ - 现在兜底扫描不再挑名字:只要还留在根上、名字对得上插件的老命名,一律归档进 `legacy/`;被别的进程攥着搬不动的,下次启动再搬。跑完根上一个不剩。
18
+ - 顺带删掉因此没用了的 `CANONICAL_LEGACY` 常量。升级不用改配置。
19
+
3
20
  ## 2.7.1 — 搬迁时不留半截副本
4
21
  - **修的什么**:2.7.0 的自动搬迁里,「rename 失败就复制再删」这两步原先裹在同一个 `try` 里。碰上源文件正被别的进程攥着(比如 QQ 那条 cmd 还在往 `launch.log` 里追加),`copyFileSync` 会成功、`unlinkSync` 会失败 —— 结果目标位落了个**残缺副本**,而且因为「目标已存在就跳过」,以后每次启动都不会再搬它,残缺文件就一直留在那儿。
5
22
  - 现在两步分开:删不掉源文件就把刚复制的撤掉,返回「没搬成」,下次启动再试一遍。真正搬不动的文件原地不动,不污染新目录。
package/README.md CHANGED
@@ -40,10 +40,26 @@
40
40
 
41
41
  ## 安装
42
42
 
43
+ **桌面版(点界面的)**:设置 → 插件 → **添加插件**,「包名或地址」那一栏只填包名 ——
44
+
45
+ ```
46
+ dsh-napcat-keeper
47
+ ```
48
+
49
+ > 只填包名,**别把整条命令粘进去**:那一栏是按「包名 / 仓库地址 / 本地目录」解析的,
50
+ > 多一个空格、多一个 `add`、多一个大写字母,就会报 `无法识别这个包名或地址:not a package name the registry accepts`。
51
+ > 顺带一提,桌面版装完之后 DSH 目录里并没有 `dsh` 命令,所以下面那条命令行不是人人都能跑 —— 用不了就走上面这条路。
52
+
53
+ **命令行**:
54
+
43
55
  ```bash
44
56
  dsh plugin --profile <profile> add dsh-napcat-keeper
45
57
  ```
46
58
 
59
+ > **别加 `github:` 前缀。** 写成 `add github:waning-sun/dsh-napcat-keeper` 走的是 git 通道,pnpm 会去调外部 `git ls-remote`;
60
+ > 机器上没装 Git(或者 Git 不在 PATH 里)就当场报 `'git' is not recognized as an internal or external command`。
61
+ > 上面这条是 npm 名安装,全程不碰 git。真踩到了看「常见问题」里 `'git' is not recognized` 那条。
62
+
47
63
  装完重启一次 DSH(第一次装载才生效),然后去 **设置 → napcat桥**。
48
64
 
49
65
  ## 首次配置:其实就三个空
@@ -239,7 +255,7 @@ QQ 那条进程链挂在本 DSH 实例下面——DSH 开着它就在,DSH 关
239
255
  ```
240
256
 
241
257
  - `env.bat` 只在「记住密码」开启时才有;两份 `autogen*.bat` 只在需要时出现(每次拉起前按当前配置重写,删掉也会自己长回来)。整个目录删掉 = 插件回到没配置过的状态。
242
- - **从老版本升上来(< 2.7.0)**:第一次启动会自动把原来散在 `~/.dsh` 根上的文件搬进这个目录(`napcat-keeper-config.json` → `config.json` 这种),改名一次就固定;计划任务里写死的旧路径也会一并改指新位置 —— 那一步用 `schtasks /change` 就地改,不结束正在跑的 QQ。老备份之类的零碎挪进 `legacy/`。
258
+ - **从老版本升上来(< 2.7.0)**:第一次启动会自动把原来散在 `~/.dsh` 根上的文件搬进这个目录(`napcat-keeper-config.json` → `config.json` 这种),改名一次就固定;计划任务里写死的旧路径也会一并改指新位置 —— 那一步用 `schtasks /change` 就地改,不结束正在跑的 QQ。老备份、以及搬不动的老文件(目标位已经有同名的那份、或者正被别的进程占着)都归到 `legacy/`,`~/.dsh` 根上不会留东西;实在被占着搬不动的,下次启动再搬一次。
243
259
  - `~/.dsh/qq-bridge-state.json` **不在**这个目录里,也不归这个插件:它由 `dsh-qq-bridge` 写,插件只是读一下它的时间戳,判断那边轮询还活着没有。
244
260
 
245
261
  ## 出事了先跑体检:`npm run check`
@@ -293,6 +309,36 @@ npm run check:privacy # 只查隐私(发版前必跑)
293
309
  2.2.1 之前是 `settings.section` 钩子签名对不上新版 DSH(抛 `Cannot add property N, object is not extensible`),异常冒到设置面板层,整个面板一起白。升级后重开 DSH 即可。
294
310
  另外设置页会轮询 `/state`,2.2.1 起热路径有 300 秒进程快照缓存,不再每次都新起 PowerShell;如果你自己写脚本调这个接口,加 `?light=1` 只读缓存、不扫描进程。
295
311
 
312
+ **装的时候报 `'git' is not recognized as an internal or external command`?**
313
+ 不是插件的问题。那句是 pnpm 在解析 **git 规格的依赖**(`github:owner/repo`、`git+https://…`)时调的外部 `git`;
314
+ DSH 自带的 pnpm 不捎带 git,Windows 上又默认没装,于是就卡在「拉代码」这一步,插件一个字节都还没下载。
315
+ 包里的 `lib/` 本来就是入库的、没有构建步骤,所以跟「源码要编译」也没关系。三条路,越靠前越省事:
316
+
317
+ 1. **用 npm 名装** —— 就是上面「安装」那节那条命令,全程不碰 git。
318
+ 2. **装 Git**:`winget install --id Git.Git -e`,或者从 [git-scm.com](https://git-scm.com/download/win) 下安装包。
319
+ 装完 **必须彻底退出 DSH 再重开**:pnpm 是 DSH 的子进程,继承的是 DSH 启动那一刻的 PATH,
320
+ 不重启会继续报一模一样的错(很多人以为白装了)。重开后在新的终端里 `git --version` 有输出,再去装插件。
321
+ 3. **不装 git、又非要从 GitHub 拉**:改用源码 tarball,pnpm 走 HTTPS 下载、不调 git ——
322
+ `dsh plugin --profile <profile> add https://codeload.github.com/waning-sun/dsh-napcat-keeper/tar.gz/refs/heads/main`
323
+ 仓库目前没打 tag,所以只能指 `refs/heads/main`;缺点是版本号只能读包里的 `package.json`,也不跟着发版更新。
324
+
325
+ **装的时候报 `无法识别这个包名或地址:not a package name the registry accepts`?**
326
+ 这句是 DSH 在**解析你填的那串字**,还没到下载那一步 —— 跟插件、跟网络都没关系。它是「设置 → 插件 → 添加插件」
327
+ 那个输入框的校验:那一栏认三种东西,**npm 包名**(`dsh-napcat-keeper`、`@作者/名字`)、**仓库地址**(`https://github.com/…`)、
328
+ **本地目录**。包名只允许小写字母、数字和 `. _ ~ -`,所以下面这些写法一律被拒:
329
+
330
+ | 填进去的 | 结果 |
331
+ | --- | --- |
332
+ | `dsh-napcat-keeper` | ✅ |
333
+ | `dsh-napcat-keeper@2.7.3` | ✅(指定版本) |
334
+ | `dsh plugin --profile desktop add dsh-napcat-keeper` | ❌ 整条命令 |
335
+ | `add dsh-napcat-keeper` | ❌ 多了 `add` |
336
+ | `Dsh-napcat-keeper` | ❌ 大写 |
337
+ | `dsh-napcat-keeper。`、`dsh-napcat-keeper;` | ❌ 全角标点 |
338
+
339
+ 改法就一句:**那一栏只填 `dsh-napcat-keeper`**(要指定版本就 `dsh-napcat-keeper@2.7.3`)。想跑命令行的话,得在终端里跑,
340
+ 不是在输入框里跑。
341
+
296
342
  ## License
297
343
 
298
344
  MIT
package/lib/index.js CHANGED
@@ -1,9 +1,20 @@
1
1
  /**
2
- * dsh-napcat-keeper —— NapCat 看门狗 v2.7.1(可发布通用版)
2
+ * dsh-napcat-keeper —— NapCat 看门狗 v2.7.3(可发布通用版)
3
3
  *
4
4
  * 解决的问题:DSH 是桌面应用,重启 DSH 时 NapCat(QQ 协议端) 不会跟着起来,
5
5
  * 于是 dsh-qq-bridge 一直轮询一个死端口,你在 QQ 里说的话就石沉大海。
6
6
  *
7
+ * v2.7.3:设置页保存不再空转
8
+ * - 2.7.0 把配置文件收进 keeperDir 时只改了「读」,saveAndApply / clearPassword
9
+ * 还在写 ~/.dsh/config.json,于是点保存 / 点「用这个」之后值立刻弹回原样;
10
+ * - 现在读写都走 keeperDir;启动时还会把那份误写的 ~/.dsh/config.json 认领回来
11
+ * (带自家字段且比 keeper 里的新才认领,原文归档进 legacy/)。
12
+ *
13
+ * v2.7.2:根上一点不留
14
+ * - 搬迁的兜底扫描原先跳过「规范老名字」,于是目标位已经有同名文件时(比如新目录里
15
+ * 已经躺着一份 state.json),根上那份老文件既搬不走、也不归档,永远留在那儿;
16
+ * - 现在只要名字对得上 LEGACY_LEFTOVER 就归档进 legacy/,搬不动的下次启动再来。
17
+ *
7
18
  * v2.7.1:搬迁时别留半截副本
8
19
  * - moveOne() 的复制兜底原来一把 try 包着「复制 + 删源」,源文件被别的进程攥着时
9
20
  * 会出现「复制成功、删源失败」——目标位落了个残缺文件,之后每次搬迁都因为
@@ -127,7 +138,7 @@ import { homedir, tmpdir } from 'node:os'
127
138
  import path from 'node:path'
128
139
 
129
140
  export const name = 'napcat-keeper'
130
- export const version = '2.7.1'
141
+ export const version = '2.7.3'
131
142
 
132
143
  /** tools 用来注册 napcat_* 工具;webServer 用来挂设置界面接口。拿不到也不至于把 DSH 拖崩。 */
133
144
  export const inject = ['tools', 'webServer']
@@ -271,9 +282,8 @@ const LEGACY_FILES = [
271
282
  ['napcat-autogen.bat', 'autogen.bat'],
272
283
  ]
273
284
  const LEGACY_DIRS = [['napcat-keeper-setup', 'setup']]
274
- /** 老版本留下的零碎(配置备份、bom-backup 之类):扫一遍挪进 legacy/,不参与运行 */
285
+ /** 老版本留在 ~/.dsh 根上的东西(没搬走的、配置备份、bom-backup 之类):统一归档进 legacy/ */
275
286
  const LEGACY_LEFTOVER = /^(napcat-keeper[.-]|napcat-qr\.|napcat-autogen)/
276
- const CANONICAL_LEGACY = new Set([...LEGACY_FILES, ...LEGACY_DIRS].map(([oldName]) => oldName))
277
287
 
278
288
  let migratedFor = ''
279
289
  /** 上一次搬迁搬了几个(给 apply 写一行日志用) */
@@ -305,6 +315,59 @@ function moveOne(from, to) {
305
315
  }
306
316
  }
307
317
 
318
+ /**
319
+ * 认领 2.7.0~2.7.2 因保存 bug 误写到 ~/.dsh/config.json 的那份配置。
320
+ *
321
+ * 那三个版本的 saveAndApply 写的是 dshHome()/config.json,而所有读取都走 keeperDir(),
322
+ * 于是用户每次「保存设置」只是往一个没人读的文件里抄了一遍 —— 值当场弹回旧的样子。
323
+ * 升级到修好的版本后,把那份(可能是用户最后一次真正想保存的内容)并进真配置里,原文归档进 legacy/。
324
+ *
325
+ * 只在不含糊的前提下动手:根上得有 config.json、得能解析成对象、得带至少两个自家字段
326
+ * (别把别的程序放在 ~/.dsh 的 config.json 抢走),而且它得比 keeper 里的新 ——
327
+ * 修好以后再保存就会写 keeper,mtime 立刻反超,这条认领自然不会再触发。
328
+ */
329
+ function adoptStrayConfig(dir) {
330
+ const home = dshHome()
331
+ const stray = path.join(home, 'config.json')
332
+ const target = configFilePath(dir)
333
+ let strayAt
334
+ try {
335
+ strayAt = statSync(stray).mtimeMs
336
+ } catch {
337
+ return 0
338
+ }
339
+ let mine = false
340
+ let parsed = null
341
+ try {
342
+ parsed = JSON.parse(readFileSync(stray, 'utf8').replace(/^\uFEFF/, ''))
343
+ } catch {
344
+ return 0
345
+ }
346
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return 0
347
+ const marks = ['shellDir', 'onebotPort', 'launchAccount', 'taskName', 'procPathMatch', 'scanTimeoutSec']
348
+ if (marks.filter((k) => Object.prototype.hasOwnProperty.call(parsed, k)).length < 2) return 0
349
+ try {
350
+ mine = statSync(target).mtimeMs < strayAt
351
+ } catch {
352
+ mine = true // keeper 里还没有配置,那它就是唯一的一份
353
+ }
354
+ if (!mine) return 0
355
+ const keep = readConfigFile(dir)
356
+ const merged = { ...keep, ...parsed }
357
+ try {
358
+ mkdirSync(path.join(dir, 'legacy'), { recursive: true })
359
+ if (existsSync(target)) {
360
+ copyFileSync(target, path.join(dir, 'legacy', 'config.before-adopt.json'))
361
+ }
362
+ writeConfigFile(dir, merged)
363
+ // 归档原文,不留一份会被下次认领的副本
364
+ moveOne(stray, path.join(dir, 'legacy', `config.adopted-${new Date().toISOString().replace(/[:.]/g, '-')}.json`))
365
+ return 1
366
+ } catch {
367
+ return 0
368
+ }
369
+ }
370
+
308
371
  /** 把老版本散在 ~/.dsh 根上的文件搬进 keeperDir;重复调用是空操作,返回搬了几个 */
309
372
  function migrateLegacyFiles(dir) {
310
373
  const target = dir || keeperDir()
@@ -328,15 +391,19 @@ function migrateLegacyFiles(dir) {
328
391
  }
329
392
  } catch {}
330
393
  }
394
+ // 兜底:还留在根上的(目标位已有同名文件、或者正被别的进程占用搬不动)一律归档进 legacy/,
395
+ // 别让它们继续躺在 ~/.dsh 根上冒充「插件还在用老布局」。这一趟搬不动的,下次启动再来。
331
396
  try {
332
397
  for (const entry of readdirSync(home)) {
333
- if (CANONICAL_LEGACY.has(entry) || !LEGACY_LEFTOVER.test(entry)) continue
398
+ if (!LEGACY_LEFTOVER.test(entry)) continue
334
399
  try {
335
400
  mkdirSync(path.join(target, 'legacy'), { recursive: true })
336
401
  if (moveOne(path.join(home, entry), path.join(target, 'legacy', entry))) moved += 1
337
402
  } catch {}
338
403
  }
339
404
  } catch {}
405
+ // 根上那份 config.json 是 2.7.0~2.7.2 的保存 bug 留下的,认领回来
406
+ if (adoptStrayConfig(target)) moved += 1
340
407
  lastMigrated = moved
341
408
  return moved
342
409
  }
@@ -3063,8 +3130,12 @@ function uiStateFull(cfg) {
3063
3130
 
3064
3131
  /** 保存配置:写文件 → 重新解析 → 热应用(不用重启 DSH) */
3065
3132
  function saveAndApply(ctx, patch) {
3066
- const home = dshHome()
3067
- const fileCfg = { ...readConfigFile(home) }
3133
+ // ⚠ 必须读写的都是 keeperDir():resolveConfig 读的是 <keeperDir>/config.json。
3134
+ // 2.7.0 把配置收进 keeperDir 时只改了「读」,这两行还留着 dshHome(),
3135
+ // 于是 2.7.0~2.7.2 的「保存设置」全写进了 ~/.dsh/config.json —— 谁都不读它。
3136
+ // 表现:设置页点保存/点「用这个」之后,值立刻弹回空。
3137
+ const dir = keeperDir()
3138
+ const fileCfg = { ...readConfigFile(dir) }
3068
3139
  for (const [key, value] of Object.entries(patch || {})) {
3069
3140
  if (!Object.prototype.hasOwnProperty.call(DEFAULTS, key)) continue
3070
3141
  if (key === 'quickPassword') {
@@ -3092,7 +3163,7 @@ function saveAndApply(ctx, patch) {
3092
3163
  }
3093
3164
  fileCfg[key] = value == null ? '' : String(value).trim()
3094
3165
  }
3095
- const written = writeConfigFile(home, fileCfg)
3166
+ const written = writeConfigFile(dir, fileCfg)
3096
3167
  const prev = state.cfg
3097
3168
  const next = resolveConfig(state.patchConfig || {})
3098
3169
  state.cfg = next
@@ -3184,11 +3255,12 @@ async function doAction(ctx, cfg, action, payload) {
3184
3255
  }
3185
3256
  case 'clearPassword': {
3186
3257
  // 把密码从本机彻底删掉:配置文件两条 + 中间层 bat,再让任务回到不带密码的样子
3187
- const home = dshHome()
3188
- const fileCfg = { ...readConfigFile(home) }
3258
+ // 同样必须落在 keeperDir() 的 config.json 上,否则删的是一份没人读的文件(见 saveAndApply 注释)
3259
+ const dir = keeperDir()
3260
+ const fileCfg = { ...readConfigFile(dir) }
3189
3261
  fileCfg.rememberPassword = false
3190
3262
  fileCfg.quickPassword = ''
3191
- writeConfigFile(home, fileCfg)
3263
+ writeConfigFile(dir, fileCfg)
3192
3264
  const next = resolveConfig(state.patchConfig || {})
3193
3265
  state.cfg = next
3194
3266
  removeEnvBat(next)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-napcat-keeper",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
4
  "description": "发现没有能够随着dsh打开自动启动napcat的插件,遂使用大肥鱼制作此插件,建议搭配Yuuyuko-uu的dsh-qq-bridge使用~",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -24,6 +24,7 @@
24
24
  "check:fix": "node scripts/check-manifest.mjs --fix",
25
25
  "smoke:client": "node scripts/smoke-client.mjs",
26
26
  "selftest:scan": "node scripts/selftest-scan.mjs",
27
+ "selftest:save": "node scripts/selftest-save.mjs",
27
28
  "selftest:setup": "node scripts/selftest-setup.mjs"
28
29
  },
29
30
  "keywords": [
@@ -0,0 +1,100 @@
1
+ /**
2
+ * 「保存设置」回归自检。
3
+ *
4
+ * 守的是 2.7.0 引入、2.7.3 修掉的那个 bug:saveAndApply 写的是 $DSH_HOME/config.json,
5
+ * 而所有读取都走 <keeperDir>/config.json,于是设置页点保存/点「用这个」之后值立刻弹回空。
6
+ *
7
+ * 全程在临时 DSH_HOME 里跑,不碰真实配置。
8
+ * node scripts/selftest-save.mjs
9
+ */
10
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, utimesSync, writeFileSync, mkdirSync } from 'node:fs'
11
+ import { tmpdir } from 'node:os'
12
+ import path from 'node:path'
13
+
14
+ let pass = 0
15
+ let fail = 0
16
+ const ok = (cond, name, extra = '') => {
17
+ if (cond) {
18
+ pass += 1
19
+ console.log(` ✓ ${name}`)
20
+ } else {
21
+ fail += 1
22
+ console.log(` ✗ ${name}${extra ? ' — ' + extra : ''}`)
23
+ }
24
+ }
25
+
26
+ const freshHome = (tag) => {
27
+ const home = mkdtempSync(path.join(tmpdir(), `npk-save-${tag}-`))
28
+ process.env.DSH_HOME = home
29
+ return { home, dir: path.join(home, 'dsh-napcat-keeper') }
30
+ }
31
+
32
+ const loadInternals = async () => (await import('../lib/index.js?t=' + Date.now())).__internals
33
+
34
+ const fakeCtx = { effect: (fn) => fn(), logger: null, tools: { register() {} } }
35
+ const readJson = (file) => JSON.parse(readFileSync(file, 'utf8').replace(/^\uFEFF/, ''))
36
+
37
+ /* 1. 保存必须落到 keeperDir/config.json,回读一致 --------------------------- */
38
+ console.log('1) saveAndApply 真的写进 keeperDir 了吗')
39
+ {
40
+ const { home, dir } = freshHome('write')
41
+ const { resolveConfig, saveAndApply } = await loadInternals()
42
+ const target = 'D:/fake/NapCat.Shell'
43
+ const next = saveAndApply(fakeCtx, { shellDir: target, onebotPort: 3011 })
44
+ ok(next.shellDir === target, 'saveAndApply 返回值里就有新值', JSON.stringify(next.shellDir))
45
+ ok(existsSync(path.join(dir, 'config.json')), 'keeperDir/config.json 被写出来了')
46
+ ok(readJson(path.join(dir, 'config.json')).shellDir === target, '落盘内容对得上')
47
+ ok(!existsSync(path.join(home, 'config.json')), '根上没再冒出 config.json')
48
+ ok(resolveConfig({}).shellDir === target, '重新 resolveConfig 读回新值(重启 DSH 后也生效)')
49
+ }
50
+
51
+ /* 2. 老版本误写的那份要被认领回来 ------------------------------------------- */
52
+ console.log('\n2) 认领 2.7.0~2.7.2 误写到根上的 config.json')
53
+ {
54
+ const { home, dir } = freshHome('adopt')
55
+ mkdirSync(dir, { recursive: true })
56
+ writeFileSync(path.join(dir, 'config.json'), JSON.stringify({ shellDir: '', onebotPort: 3000 }, null, 2) + '\n')
57
+ // 根上那份是「用户最后一次点保存」的内容,比 keeper 的新
58
+ const stray = path.join(home, 'config.json')
59
+ writeFileSync(stray, JSON.stringify({ shellDir: 'E:/NapCat.Shell', onebotPort: 3000, launchAccount: '10001' }, null, 2) + '\n')
60
+ const old = (Date.now() - 60000) / 1000
61
+ utimesSync(path.join(dir, 'config.json'), old, old)
62
+
63
+ const { resolveConfig } = await loadInternals()
64
+ ok(resolveConfig({}).shellDir === 'E:/NapCat.Shell', '认领后读到的就是用户当初想存的那份')
65
+ ok(!existsSync(stray), '根上那份已归档,不会下次又被认领一遍')
66
+ const leg = readdirSync(path.join(dir, 'legacy'))
67
+ ok(leg.some((f) => f.startsWith('config.adopted-')), '原文留在 legacy/ 里(可回滚)', leg.join(','))
68
+ }
69
+
70
+ /* 3. keeper 更新时不许被根上那份覆盖 ---------------------------------------- */
71
+ console.log('\n3) keeper 比根上新时不认领(别把新配置写旧)')
72
+ {
73
+ const { home, dir } = freshHome('noadopt')
74
+ mkdirSync(dir, { recursive: true })
75
+ writeFileSync(path.join(dir, 'config.json'), JSON.stringify({ shellDir: 'F:/good', onebotPort: 3000, launchAccount: '1' }, null, 2) + '\n')
76
+ const stray = path.join(home, 'config.json')
77
+ writeFileSync(stray, JSON.stringify({ shellDir: 'G:/stale', onebotPort: 3000, launchAccount: '1' }, null, 2) + '\n')
78
+ const old = (Date.now() - 120000) / 1000
79
+ utimesSync(stray, old, old)
80
+
81
+ const { resolveConfig } = await loadInternals()
82
+ ok(resolveConfig({}).shellDir === 'F:/good', 'keeper 里的值没被旧文件顶掉')
83
+ ok(existsSync(stray), '不认领就不动它')
84
+ }
85
+
86
+ /* 4. 不是自家的 config.json 不许抢 ----------------------------------------- */
87
+ console.log('\n4) 根上的 config.json 若不像插件的,一概不碰')
88
+ {
89
+ const { home, dir } = freshHome('foreign')
90
+ mkdirSync(dir, { recursive: true })
91
+ const stray = path.join(home, 'config.json')
92
+ writeFileSync(stray, JSON.stringify({ theme: 'dark', language: 'zh-CN' }, null, 2) + '\n')
93
+ const { resolveConfig } = await loadInternals()
94
+ resolveConfig({})
95
+ ok(existsSync(stray), '别人的 config.json 原样留着')
96
+ ok(!existsSync(path.join(dir, 'config.json')), '也没凭空造一份 keeper 配置')
97
+ }
98
+
99
+ console.log(`\n结果:${pass} 通过,${fail} 失败`)
100
+ process.exit(fail ? 1 : 0)