dsh-napcat-keeper 2.0.2 → 2.1.1

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.1
4
+
5
+ - 修:风险清单里的 **明文** 在设置页会原样显示成星号,改成「明文」。
6
+
7
+ ## 2.1.0 — 可选「记住密码」,并先把风险说清楚
8
+
9
+ - 新增 **`rememberPassword` + `quickPassword`**:勾上后令牌失效时用密码自动登录(NapCat 读环境变量 `NAPCAT_QUICK_PASSWORD` / `NAPCAT_QUICK_PASSWORD_MD5`)。默认关。
10
+ - 设置页勾选「记住密码」会先展开风险提示(明文落盘位置、谁能读走、为什么默认关、怎么收回),要点「我知道风险,仍然开启」才生效;入口是「自动登录(可不填)」那一组。
11
+ - 密码**不进命令行**(避免出现在进程列表 / 事件日志),只写进 `~/.dsh/napcat-keeper-env.bat` 并由计划任务 wrapper 调它;`/state` 只回 `hasPassword` / `passwordLength` / `passwordPath` / `passwordActive`,日志与工具输出都不打印密码。
12
+ - 新增动作 `action=clearPassword` + 页面按钮「清除已保存的密码」,删配置里的密码和那个 bat。
13
+ - `napcat_status` 多一段 `自动登录`:`方式`(password / token)、`密码已存`、`生效中`、`密码文件`,并给出「令牌失效只剩扫码」的提示。
14
+ - `saveAndApply`:`quickPassword` 传空 = 不改(不会被表单回显冲掉)。
15
+
3
16
  ## 2.0.2
4
17
 
5
18
  - `package.json` 补 `repository` / `homepage` / `bugs`,npm 页面能直接跳 GitHub 仓库。
package/README.md CHANGED
@@ -10,6 +10,9 @@
10
10
  > 常配套 `dsh-qq-bridge`(把 QQ 和 DSH 会话接通):那个负责「说话」,这个负责「别掉线」。
11
11
  > 也可以单独用——只要你想让 NapCat 一直活着。
12
12
 
13
+
14
+ 声明:基本纯让我家大肥鱼跑的,内容我只粗略检查了一下,本人也只是个半吊子学生,有隐性bug大概率也看不出来,这GitHub我也第一次用,之前光下别的大佬的好东西了,有大佬愿意的话欢迎随时指点我、提建议等等都行,爱你♥️
15
+
13
16
  ---
14
17
 
15
18
  ## 它到底管什么
@@ -78,6 +81,22 @@ cscript / cmd 直接拉 NapCat,会多出一个控制台窗口挂在桌面上
78
81
  不想要隐藏就把「拉起时不显示黑框」关掉,任务会退回直接跑 `cmd.exe`;已经跑起来的那条链想收窗口,
79
82
  点「收起黑框」(`action=hideConsole`)即可。
80
83
 
84
+ ## 记住密码:默认关,开了要自己认风险
85
+
86
+ NapCat 的**快速登录令牌会失效**(实测几小时到几天),失效后启动脚本只会停在扫码,QQ 就一直离线,
87
+ `dsh-qq-bridge` 那边则是刷屏的 `ECONNREFUSED 127.0.0.1:3000`。想让它自己能爬起来,就得给它密码。
88
+
89
+ 设置页「自动登录(可不填)」里勾 **记住密码** 时会先弹一段风险提示,要你自己点「我知道风险,仍然开启」才会生效。开启前请确认你接受这几条:
90
+
91
+ 1. 密码以**明文**存进 `~/.dsh/napcat-keeper-config.json` 和插件生成的 `napcat-keeper-env.bat`,不加密(MD5 那份是给 NapCat 兜底用的,等于同一个秘密)。
92
+ 2. 任何能读你用户目录的程序都能捡走:同步盘、备份工具、其它软件、远控、临时借电脑的人。
93
+ 3. QQ 密码往往和 QQ 邮箱、部分游戏、微信登录入口共用一套身份,泄漏面不止一个号。
94
+ 4. 插件不打印密码,`/state` 也只回「存没存 + 长度」,但那只是少开一个口子,不等于安全。
95
+ 5. 随时可以点「清除密码」收回,配置和 bat 里的密码会被删掉。
96
+
97
+ 密码不写成命令行参数(`NapCatWinBootMain.exe <号> <密码>` 那样会进进程列表和 Windows 事件日志),
98
+ 而是走环境变量 `NAPCAT_QUICK_PASSWORD` / `NAPCAT_QUICK_PASSWORD_MD5`,且只有本机 `127.0.0.1:3000` 回连时才注入。
99
+
81
100
  ## 计划任务为什么重要
82
101
 
83
102
  `napcat.bat` / `NapCatWinBootMain.exe` 拉起的进程链会跟着「拉起它的进程」一起死。
@@ -103,6 +122,8 @@ cscript / cmd 直接拉 NapCat,会多出一个控制台窗口挂在桌面上
103
122
  | `launcherScript` | 自动探测 | 启动脚本名或绝对路径 |
104
123
  | `launchAccount` | 空 | 想快登的 QQ 号 |
105
124
  | `launchArgs` | 空 | 追加给启动脚本的参数 |
125
+ | `rememberPassword` | `false` | 记住 QQ 密码、令牌失效时自动密码登录(**明文落盘,详见上节**) |
126
+ | `quickPassword` | 空 | QQ 密码;只在保存时写入,接口不回显、日志不打印 |
106
127
  | `onebotPort` | `3000` | OneBot 11 HTTP 端口 |
107
128
  | `accessToken` | 空 | OneBot access token |
108
129
  | `taskName` | `NapCat-Keeper-Launch` | 负责拉起的计划任务名,留空 = 不用 |
@@ -122,6 +143,7 @@ cscript / cmd 直接拉 NapCat,会多出一个控制台窗口挂在桌面上
122
143
  ~/.dsh/napcat-keeper.log 看门狗日志(超 1MB 自动滚动)
123
144
  ~/.dsh/napcat-keeper-launch.log 启动脚本的原始输出(扫码提示都在这)
124
145
  ~/.dsh/napcat-keeper-config.json 设置页保存的配置
146
+ ~/.dsh/napcat-keeper-env.bat 只在「记住密码」开启时生成:把密码塞进环境变量给启动脚本
125
147
  ~/.dsh/napcat-keeper-state.json 每轮 tick 的状态快照
126
148
  ~/.dsh/napcat-qr.png 最近一张二维码副本(扫码时会更新)
127
149
  ```
@@ -129,7 +151,7 @@ cscript / cmd 直接拉 NapCat,会多出一个控制台窗口挂在桌面上
129
151
  ## 常见问题
130
152
 
131
153
  **「离线」但进程明明还在?**
132
- 端口没通。看 `napcat-keeper-launch.log`——多半是卡在扫码,或者 QQ 弹了验证窗。
154
+ 端口没通。看 `napcat-keeper-launch.log`多半是卡在扫码,或者 QQ 弹了验证窗。
133
155
 
134
156
  **刚重启 DSH,QQ 就掉了?**
135
157
  说明这条链还是 DSH 拉起来的。填上「计划任务名」、点「建计划任务」,以后交给任务计划服务。
@@ -140,8 +162,15 @@ cscript / cmd 直接拉 NapCat,会多出一个控制台窗口挂在桌面上
140
162
  **换端口了?**
141
163
  改「OneBot HTTP 端口」保存即可;如果同时换了 Shell 目录,跨进程锁会自动清掉。
142
164
 
165
+ **老是掉线、每次都要扫码?**
166
+ 令牌过期了。要么每次手机扫,要么开「记住密码」让它自己用密码登(先看上上节的风险)。
167
+ `napcat_status` 会直接告诉你现在是 `password` 还是 `token` 在撑、以及密码文件在不在。
168
+
169
+ **密码改成带特殊字符的行不行?**
170
+ 行。`%` 会写成 `%%`(bat 里转义)、`"` 会被去掉,中文路径和中文密码也认。
171
+
143
172
  **能在 Linux / macOS 上用吗?**
144
- 看门狗本身是 node 实现,但「进程路径探测 / 计划任务 / cscript」这几处是为 Windows 写的;其它平台目前只保证「探端口 + 判活」这一条路。
173
+ 看门狗本身是 node 实现,但「进程路径探测 / 计划任务 / cscript」这几处是为 Windows 写的;其它平台目前只保证「探端口 + 判活」。
145
174
 
146
175
  ## License
147
176
 
package/lib/client.js CHANGED
@@ -135,6 +135,13 @@ window.__ModuleLoader__.load({
135
135
  ['launchGraceSec', '拉起后宽限期(秒)', '刚拉起时端口还没通,这期间不重复拉', 'number'],
136
136
  ],
137
137
  },
138
+ {
139
+ group: '自动登录(可不填)',
140
+ note: 'NapCat 的快速登录令牌会失效,失效后只剩手机扫码。勾上「记住密码」它就会用密码自己重登;'
141
+ + '代价是密码要留在本机,所以勾选前请先看完下面那段风险。',
142
+ custom: 'password',
143
+ items: [],
144
+ },
138
145
  {
139
146
  group: '高级',
140
147
  note: '一般不用动。',
@@ -148,7 +155,7 @@ window.__ModuleLoader__.load({
148
155
  },
149
156
  ]
150
157
 
151
- const 全部键 = 字段.reduce((acc, g) => acc.concat(g.items.map((i) => i[0])), [])
158
+ const 全部键 = 字段.reduce((acc, g) => acc.concat(g.items.map((i) => i[0])), ['rememberPassword', 'quickPassword'])
152
159
 
153
160
  function 取(obj, key) {
154
161
  return obj && Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : ''
@@ -193,6 +200,7 @@ window.__ModuleLoader__.load({
193
200
  const [忙, set忙] = React.useState(false)
194
201
  const [说, set说] = React.useState('')
195
202
  const [开日志, set开日志] = React.useState(false)
203
+ const [风险开, set风险开] = React.useState(false)
196
204
 
197
205
  const 装 = (data) => {
198
206
  set态(data)
@@ -247,6 +255,92 @@ window.__ModuleLoader__.load({
247
255
 
248
256
  const 改 = (k, v) => set草((prev) => ({ ...(prev || {}), [k]: v }))
249
257
 
258
+ /* ---------- 「记住密码」那一块:先讲风险,再让人自己决定 ---------- */
259
+ const 风险条目 = [
260
+ '密码会以明文写进本机两个地方:~/.dsh/napcat-keeper-config.json,以及插件生成的 napcat-keeper-env.bat。不加密、不做任何变换(MD5 那份是给 NapCat 兜底用的,同样等于密码)。',
261
+ '任何能读你这个用户目录的东西都能直接捡走:同步盘、备份工具、其它软件、远控木马、临时借你电脑的人。',
262
+ 'QQ 密码通常和 QQ 邮箱、部分游戏、微信的登录入口共用一套身份,泄漏的影响面不止这一个号。',
263
+ '插件本身不打印密码、/state 接口也只回「存没存」和长度,但那只是少开一个泄漏口,不等于安全。',
264
+ '不开的代价:快速登录令牌会失效(实测几小时到几天),失效后就只能手机扫码,否则 QQ 离线,dsh-qq-bridge 会一路报 ECONNREFUSED。',
265
+ '想收回:随时点下面「清除密码」,配置文件和这个 bat 里的密码都会被删掉;已经跑起来的进程环境只活到那次进程结束。',
266
+ ]
267
+
268
+ function 密码区() {
269
+ const 记着 = 取(草, 'rememberPassword') === true || String(取(草, 'rememberPassword')) === 'true'
270
+ const 有密码 = Boolean(取(态.config, 'hasPassword'))
271
+ const 密码位 = 取(态.config, 'passwordLength')
272
+ const 账号 = String(取(草, 'launchAccount') || '')
273
+ return h('div', null,
274
+ h('div', { style: S.h }, '自动登录(可不填)'),
275
+ h('div', { style: { ...S.dim, marginBottom: 8 } },
276
+ 'NapCat 的快速登录令牌会失效,失效后只剩手机扫码。勾上「记住密码」它就会用密码自己重登;'
277
+ + '代价是密码要留在本机,所以勾之前先看完下面那段风险。'),
278
+
279
+ h('div', { style: S.card },
280
+ h('label', { style: { display: 'flex', gap: 8, alignItems: 'center', fontSize: 12.5 } },
281
+ h('input', {
282
+ type: 'checkbox',
283
+ checked: 记着,
284
+ onChange: (e) => {
285
+ if (e.target.checked) set风险开(true)
286
+ else { set风险开(false); 改('rememberPassword', false) }
287
+ },
288
+ }),
289
+ h('span', null, '记住 QQ 密码:令牌失效时用密码自动登录'),
290
+ h('span', { style: 记着 ? S.warn : S.dim }, 记着 ? '(已开启)' : '(默认关)'),
291
+ ),
292
+
293
+ h('div', { style: { ...S.row, marginTop: 10 } },
294
+ h('div', { style: S.lab },
295
+ h('div', null, 'QQ 密码'),
296
+ h('div', { style: S.hint },
297
+ 有密码 ? '已保存 ' + String(密码位) + ' 位;直接留空 = 不改' : '还没保存过;这里填的密码只发给本机 NapCat')),
298
+ h('div', { style: S.ctl },
299
+ h('input', {
300
+ style: S.input,
301
+ type: 'password',
302
+ autoComplete: 'new-password',
303
+ spellCheck: false,
304
+ value: String(取(草, 'quickPassword') || ''),
305
+ placeholder: 有密码 ? '留空 = 不改' : '填 QQ 密码',
306
+ disabled: !记着,
307
+ onChange: (e) => 改('quickPassword', e.target.value),
308
+ }),
309
+ !账号 ? h('div', { style: { ...S.bad, marginTop: 6 } }, '先去上面「连接」里把 QQ 号填了,否则密码用不上') : null,
310
+ ),
311
+ ),
312
+
313
+ 风险开 ? h('div', { style: { marginTop: 10, padding: '10px 12px', borderRadius: 8, border: '1px solid #c0392b', background: 'rgba(192,57,43,.06)' } },
314
+ h('div', { style: { ...S.bad, marginBottom: 6 } }, '⚠ 开启前请确认你接受这些风险'),
315
+ h('ul', { style: { margin: '0 0 8px 18px', padding: 0, fontSize: 12.5, lineHeight: 1.75 } },
316
+ 风险条目.map((t, i) => h('li', { key: i, style: { marginBottom: 4 } }, t))),
317
+ h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
318
+ h('button', {
319
+ type: 'button',
320
+ style: S.btnOn,
321
+ disabled: 忙,
322
+ onClick: () => { 改('rememberPassword', true); set风险开(false); set说('已勾选「记住密码」——记得点下面「保存设置」才会真正落盘') },
323
+ }, '我知道风险,仍然开启'),
324
+ h('button', {
325
+ type: 'button',
326
+ style: S.btn,
327
+ disabled: 忙,
328
+ onClick: () => { set风险开(false); 改('rememberPassword', false) },
329
+ }, '算了,不用'),
330
+ ),
331
+ ) : null,
332
+
333
+ 有密码 ? h('div', { style: { marginTop: 10, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
334
+ h('button', { type: 'button', style: S.btn, disabled: 忙, onClick: () => 动('clearPassword') }, '清除已保存的密码'),
335
+ h('span', { style: S.dim },
336
+ 取(态.config, 'passwordActive')
337
+ ? '当前生效中:拉起时会带环境变量;文件 ' + String(取(态.config, 'passwordPath') || '')
338
+ : '已保存但未生效(开关关着,或 QQ 号没填)'),
339
+ ) : null,
340
+ ),
341
+ )
342
+ }
343
+
250
344
  const 渲染项 = (item) => {
251
345
  const [key, label, hint, type] = item
252
346
  const val = 取(草, key)
@@ -299,11 +393,13 @@ window.__ModuleLoader__.load({
299
393
  ),
300
394
  说 ? h('div', { style: { ...S.dim, marginBottom: 6 } }, 说) : null,
301
395
 
302
- 字段.map((g) => h('div', { key: g.group },
303
- h('div', { style: S.h }, g.group),
304
- g.note ? h('div', { style: { ...S.dim, marginBottom: 8 } }, g.note) : null,
305
- g.items.map(渲染项),
306
- )),
396
+ 字段.map((g) => g.custom === 'password'
397
+ ? h('div', { key: g.group }, 密码区())
398
+ : h('div', { key: g.group },
399
+ h('div', { style: S.h }, g.group),
400
+ g.note ? h('div', { style: { ...S.dim, marginBottom: 8 } }, g.note) : null,
401
+ g.items.map(渲染项),
402
+ )),
307
403
 
308
404
  h('div', { style: S.h }, '日志'),
309
405
  h('div', { style: { display: 'flex', gap: 8, marginBottom: 6 } },
package/lib/index.js CHANGED
@@ -1,9 +1,17 @@
1
1
  /**
2
- * dsh-napcat-keeper —— NapCat 看门狗 v2.0.0(可发布通用版)
2
+ * dsh-napcat-keeper —— NapCat 看门狗 v2.1.0(可发布通用版)
3
3
  *
4
4
  * 解决的问题:DSH 是桌面应用,重启 DSH 时 NapCat(QQ 协议端) 不会跟着起来,
5
5
  * 于是 dsh-qq-bridge 一直轮询一个死端口,你在 QQ 里说的话就石沉大海。
6
6
  *
7
+ * v2.1.0:可选「记住密码」
8
+ * - NapCat 的快速登录 token 会失效(实测几小时就掉),失效后只剩手机扫码一条路;
9
+ * - 打开 rememberPassword 后,拉起时会通过环境变量 ACCOUNT / NAPCAT_QUICK_PASSWORD
10
+ * (外加 MD5 兜底)交给 NapCat,token 一失效它自己用密码重新登录,不用人管;
11
+ * - 密码只落在本机两处:~/.dsh/napcat-keeper-config.json 与生成的 napcat-keeper-env.bat,
12
+ * 所以设置页打开它之前会先摆出风险说明,要人自己点确认;关掉或点「清除密码」就删干净。
13
+ * - 密码绝不进日志、绝不进 /state 返回值(只回「已保存」和长度)。
14
+ *
7
15
  * v2.0.0:从「一台机器专用」改成通用款
8
16
  * - 所有机器相关的路径都不再写死,改为「设置 → napcat桥」里填,或自动探测;
9
17
  * - 新增设置界面(lib/client.js):状态 / 配置 / 一键动作 / 日志,保存即热生效;
@@ -19,6 +27,7 @@
19
27
  * 只依赖 node 内置模块,不需要构建。看门狗自己绝不能把 DSH 启动搞崩。
20
28
  */
21
29
 
30
+ import { createHash } from 'node:crypto'
22
31
  import { execFileSync, spawn } from 'node:child_process'
23
32
  import {
24
33
  appendFileSync,
@@ -37,7 +46,7 @@ import { homedir, tmpdir } from 'node:os'
37
46
  import path from 'node:path'
38
47
 
39
48
  export const name = 'napcat-keeper'
40
- export const version = '2.0.1'
49
+ export const version = '2.1.0'
41
50
 
42
51
  /** tools 用来注册 napcat_* 工具;webServer 用来挂设置界面接口。拿不到也不至于把 DSH 拖崩。 */
43
52
  export const inject = ['tools', 'webServer']
@@ -52,6 +61,13 @@ const DEFAULTS = {
52
61
  launchAccount: '',
53
62
  /** 追加给启动脚本的参数(空格分隔),一般不用填 */
54
63
  launchArgs: '',
64
+ /**
65
+ * 记住 QQ 密码:token 失效时 NapCat 用密码自己重新登录,不用手机扫码。
66
+ * 默认关;打开后密码会以明文存在本机配置和一个生成的 bat 里,设置页会先摆风险说明。
67
+ */
68
+ rememberPassword: false,
69
+ /** QQ 密码(明文,只存在本机);rememberPassword 打开、且 launchAccount 有值时才生效 */
70
+ quickPassword: '',
55
71
  /** OneBot 11 HTTP 端口 */
56
72
  onebotPort: 3000,
57
73
  /** OneBot access token,没有就留空 */
@@ -286,6 +302,11 @@ function normalizeConfig(cfg) {
286
302
  cfg.maxQqPerChain = clampNum(cfg.maxQqPerChain, 1, 40, 4)
287
303
  cfg.bridgeStaleSec = clampNum(cfg.bridgeStaleSec, 30, 86400, 180)
288
304
  cfg.hideConsole = cfg.hideConsole !== false && cfg.hideConsole !== 'false' && cfg.hideConsole !== 0
305
+ cfg.rememberPassword = cfg.rememberPassword === true || cfg.rememberPassword === 'true' || cfg.rememberPassword === 1
306
+ // 密码原样保留(空格也可能属于密码),只把换行去掉——换行会破坏生成的 bat
307
+ cfg.quickPassword = String(cfg.quickPassword == null ? '' : cfg.quickPassword).replace(/[\r\n]/g, '')
308
+ // 没有密码、没有账号,「记住密码」就等于没打开
309
+ if (!cfg.quickPassword || !cfg.launchAccount) cfg.rememberPassword = false
289
310
  return cfg
290
311
  }
291
312
 
@@ -674,10 +695,77 @@ function taskWrapperPath(cfg) {
674
695
  return path.join(dir, 'napcat-keeper-launch.vbs')
675
696
  }
676
697
 
698
+ /** 「记住密码」时生成的那个 bat:设好环境变量,再 call 真正的启动脚本 */
699
+ function envBatPath(cfg) {
700
+ const dir = path.dirname(cfg.launcherLog || path.join(dshHome(), 'napcat-keeper-launch.log'))
701
+ return path.join(dir, 'napcat-keeper-env.bat')
702
+ }
703
+
704
+ /** 密码是否真的在生效(三个条件缺一不可) */
705
+ function passwordActive(cfg) {
706
+ return Boolean(cfg && cfg.rememberPassword && cfg.quickPassword && cfg.launchAccount)
707
+ }
708
+
709
+ function md5Hex(text) {
710
+ return createHash('md5').update(String(text), 'utf8').digest('hex')
711
+ }
712
+
713
+ /**
714
+ * bat 里 `set "K=V"` 的转义:引号在 set 语法里没法安全出现,直接剔除;
715
+ * `%` 会被 cmd 展开,双写;`^` 在引号内不特殊,剩下的都靠外层引号包住。
716
+ */
717
+ function batValue(value) {
718
+ return String(value == null ? '' : value)
719
+ .replace(/[\r\n]/g, '')
720
+ .replace(/"/g, '')
721
+ .replace(/%/g, '%%')
722
+ }
723
+
724
+ /**
725
+ * 写「带密码」的启动壳。
726
+ * 为什么不直接改用户的 napcat.quick2.bat:那是人家自己的文件,插件只生成自己的中间层,
727
+ * 关掉「记住密码」时把这个文件删掉就干净了;任务计划里也只出现这个中间层的路径,不出现密码。
728
+ */
729
+ function writeEnvBat(cfg, bat) {
730
+ const target = bat || resolveLauncherPath(cfg)
731
+ if (!passwordActive(cfg) || !target) return ''
732
+ const file = envBatPath(cfg)
733
+ const lines = [
734
+ '@echo off',
735
+ 'rem dsh-napcat-keeper: 自动生成,改了也会被覆盖;关掉「记住密码」这个文件会被删掉',
736
+ `set "ACCOUNT=${batValue(cfg.launchAccount)}"`,
737
+ `set "NAPCAT_QUICK_PASSWORD=${batValue(cfg.quickPassword)}"`,
738
+ `set "NAPCAT_QUICK_PASSWORD_MD5=${md5Hex(cfg.quickPassword)}"`,
739
+ `call "${target}"${cfg.launchArgs ? ` ${cfg.launchArgs}` : ''}`,
740
+ '',
741
+ ]
742
+ try {
743
+ mkdirSync(path.dirname(file), { recursive: true })
744
+ writeFileSync(file, lines.join('\r\n'), 'utf8')
745
+ return file
746
+ } catch (error) {
747
+ log(cfg, `env bat 写不出来(${String((error && error.message) || error)}),这次退回不带密码的启动`)
748
+ return ''
749
+ }
750
+ }
751
+
752
+ function removeEnvBat(cfg) {
753
+ const file = envBatPath(cfg)
754
+ try {
755
+ if (existsSync(file)) {
756
+ unlinkSync(file)
757
+ log(cfg, `env bat removed: ${file}`)
758
+ }
759
+ } catch {}
760
+ }
761
+
677
762
  /** 真正要跑的那条 cmd 命令 */
678
763
  function taskCommandLine(cfg, bat) {
764
+ // 记住密码时改走中间层(set 环境变量 → call 原脚本),其余情况原样直跑
765
+ const viaEnv = passwordActive(cfg) ? writeEnvBat(cfg, bat) : ''
766
+ const target = viaEnv || bat
679
767
  return (
680
- `cmd.exe /c chcp 65001 >nul & cd /d "${cfg.shellDir}" & call "${bat}" ` +
768
+ `cmd.exe /c chcp 65001 >nul & cd /d "${cfg.shellDir}" & call "${target}" ` +
681
769
  `>> "${cfg.launcherLog}" 2>&1`
682
770
  )
683
771
  }
@@ -709,6 +797,8 @@ function ensureTask(cfg) {
709
797
  if (!cfg.taskName) return { ok: false, error: 'taskName 为空' }
710
798
  const bat = resolveLauncherPath(cfg)
711
799
  if (!bat) return { ok: false, error: '找不到启动脚本(先在设置里填 NapCat 目录)' }
800
+ // 关掉「记住密码」时清掉旧的中间层,免得密码继续留在磁盘上
801
+ if (!passwordActive(cfg)) removeEnvBat(cfg)
712
802
  let wrapper = ''
713
803
  let inner = ''
714
804
  if (cfg.hideConsole !== false) {
@@ -745,6 +835,12 @@ function ensureTask(cfg) {
745
835
  }
746
836
  state.taskReady = undefined
747
837
  log(cfg, `task "${cfg.taskName}" created → ${cfg.shellDir}`)
838
+ if (passwordActive(cfg)) {
839
+ log(
840
+ cfg,
841
+ `自动登录已武装:账号 ${cfg.launchAccount},token 失效时 NapCat 会用密码重新登录(密码不进日志)`,
842
+ )
843
+ }
748
844
  const ps =
749
845
  `$ErrorActionPreference='SilentlyContinue';` +
750
846
  `$t=Get-ScheduledTask -TaskName '${cfg.taskName.replace(/'/g, "''")}';` +
@@ -833,13 +929,16 @@ function launchNapcat(cfg, reason = '') {
833
929
  const stamp = Date.now()
834
930
  const cmdFile = path.join(tmpdir(), `dsh-napcat-keeper-${stamp}.cmd`)
835
931
  const vbsFile = path.join(tmpdir(), `dsh-napcat-keeper-${stamp}.vbs`)
932
+ // cscript 这条退路也要认「记住密码」:走中间层,否则密码白存
933
+ const viaEnv = passwordActive(cfg) ? writeEnvBat(cfg, batPath) : ''
934
+ const runBat = viaEnv || batPath
836
935
  try {
837
936
  writeFileSync(
838
937
  cmdFile,
839
938
  '@echo off\r\n' +
840
939
  'chcp 65001 >nul\r\n' +
841
940
  `cd /d "${cfg.shellDir}"\r\n` +
842
- `call "${batPath}" >> "${cfg.launcherLog}" 2>&1\r\n`,
941
+ `call "${runBat}" >> "${cfg.launcherLog}" 2>&1\r\n`,
843
942
  'utf8',
844
943
  )
845
944
  // VBScript 字符串里 "" 表示一个引号;sh.Run 的第三个参数 False = 不等待返回
@@ -874,7 +973,7 @@ function launchNapcat(cfg, reason = '') {
874
973
  state.phase = 'launching'
875
974
  state.scanSince = 0
876
975
  state.lastError = ''
877
- log(cfg, `LAUNCH ok: ${batPath}${reason ? ` (${reason})` : ''} (launch #${state.launchCount})`)
976
+ log(cfg, `LAUNCH ok: ${runBat}${reason ? ` (${reason})` : ''} (launch #${state.launchCount})`)
878
977
  return { ok: true, cmdFile, vbsFile }
879
978
  }
880
979
 
@@ -1211,6 +1310,12 @@ function registerTools(ctx, cfg) {
1211
1310
  procs,
1212
1311
  qr,
1213
1312
  bridge: bridgeAge(cfg),
1313
+ autoLogin: {
1314
+ rememberPassword: cfg.rememberPassword,
1315
+ passwordActive: passwordActive(cfg),
1316
+ account: cfg.launchAccount,
1317
+ envBat: passwordActive(cfg) ? envBatPath(cfg) : '',
1318
+ },
1214
1319
  launches: state.launchCount,
1215
1320
  dedupes: state.dedupeCount,
1216
1321
  scanRetries: state.scanRetries,
@@ -1228,7 +1333,9 @@ function registerTools(ctx, cfg) {
1228
1333
  note: health.online
1229
1334
  ? '在线。若 QQ 里还是没回音,问题在 dsh-qq-bridge 那一侧(绑定/试运行开关)。'
1230
1335
  : qr && qr.ageSec <= cfg.qrFreshSec
1231
- ? '离线,且二维码是新的:小号需要手机扫码,文件在 qrCopyPath。'
1336
+ ? passwordActive(cfg)
1337
+ ? '离线,且在等扫码——但「记住密码」是开着的:看一眼日志里“快速登录错误”后面是不是仍回退到二维码(密码登录可能被腾讯要求验证)。'
1338
+ : '离线,且二维码是新的:需要手机扫码(或在设置里打开「记住密码」让 NapCat 自己用密码登录)。文件在 qrCopyPath。'
1232
1339
  : '离线。看门狗会自动拉起;若 procs.dup 为 true 说明被重复拉起过,已自动去重。',
1233
1340
  },
1234
1341
  null,
@@ -1399,6 +1506,12 @@ function publicConfig(cfg) {
1399
1506
  launcherScript: cfg.launcherScript,
1400
1507
  launchAccount: cfg.launchAccount,
1401
1508
  launchArgs: cfg.launchArgs,
1509
+ rememberPassword: cfg.rememberPassword,
1510
+ /** 只回「存没存、几位」,明文密码永远不出这个函数 */
1511
+ hasPassword: Boolean(cfg.quickPassword),
1512
+ passwordLength: cfg.quickPassword ? String(cfg.quickPassword).length : 0,
1513
+ passwordPath: passwordActive(cfg) ? envBatPath(cfg) : '',
1514
+ passwordActive: passwordActive(cfg),
1402
1515
  onebotPort: cfg.onebotPort,
1403
1516
  accessToken: cfg.accessToken,
1404
1517
  procPathMatch: cfg.procPathMatch,
@@ -1470,6 +1583,16 @@ function saveAndApply(ctx, patch) {
1470
1583
  const fileCfg = { ...readConfigFile(home) }
1471
1584
  for (const [key, value] of Object.entries(patch || {})) {
1472
1585
  if (!Object.prototype.hasOwnProperty.call(DEFAULTS, key)) continue
1586
+ if (key === 'quickPassword') {
1587
+ // 设置页不回显密码:空字符串 = 别动它(要清空走 clearPassword 动作)
1588
+ const text = String(value == null ? '' : value).replace(/[\r\n]/g, '')
1589
+ if (text) fileCfg.quickPassword = text
1590
+ continue
1591
+ }
1592
+ if (key === 'rememberPassword') {
1593
+ fileCfg.rememberPassword = value !== false && value !== 'false'
1594
+ continue
1595
+ }
1473
1596
  if (key === 'shellDir' || key === 'procPathMatch') {
1474
1597
  fileCfg[key] = String(value == null ? '' : value).trim().replace(/"/g, '').replace(/\\/g, '/')
1475
1598
  continue
@@ -1502,6 +1625,27 @@ function saveAndApply(ctx, patch) {
1502
1625
  `config saved → ${written} (enabled=${next.enabled}, shell=${next.shellDir || '(未探测到)'}, ` +
1503
1626
  `port=${next.onebotPort}, task=${next.taskName || '(不用)'})`,
1504
1627
  )
1628
+ // 密码相关的改动要落到「计划任务 / 中间层 bat」上,否则只是改了配置文件
1629
+ const passwordChanged =
1630
+ prev &&
1631
+ (prev.rememberPassword !== next.rememberPassword ||
1632
+ prev.quickPassword !== next.quickPassword ||
1633
+ prev.launchAccount !== next.launchAccount)
1634
+ if (passwordChanged) {
1635
+ if (passwordActive(next)) {
1636
+ const made = next.taskName ? ensureTask(next) : { ok: true }
1637
+ log(
1638
+ next,
1639
+ made.ok
1640
+ ? '记住密码已开启:下次拉起会带 ACCOUNT / NAPCAT_QUICK_PASSWORD,token 失效自动用密码登录'
1641
+ : `记住密码已写入配置,但任务重建失败(${made.error || ''}),下次拉起可能仍走扫码`,
1642
+ )
1643
+ } else {
1644
+ removeEnvBat(next)
1645
+ if (next.taskName) ensureTask(next)
1646
+ log(next, '记住密码已关闭:中间层 bat 已删除,任务回到不带密码的启动方式')
1647
+ }
1648
+ }
1505
1649
  armTimer(next)
1506
1650
  if (next.enabled) {
1507
1651
  tick(next).catch((error) => log(next, `tick after save error: ${String((error && error.message) || error)}`))
@@ -1554,6 +1698,24 @@ async function doAction(ctx, cfg, action) {
1554
1698
  hidden: r.hidden,
1555
1699
  }
1556
1700
  }
1701
+ case 'clearPassword': {
1702
+ // 把密码从本机彻底删掉:配置文件两条 + 中间层 bat,再让任务回到不带密码的样子
1703
+ const home = dshHome()
1704
+ const fileCfg = { ...readConfigFile(home) }
1705
+ fileCfg.rememberPassword = false
1706
+ fileCfg.quickPassword = ''
1707
+ writeConfigFile(home, fileCfg)
1708
+ const next = resolveConfig(state.patchConfig || {})
1709
+ state.cfg = next
1710
+ removeEnvBat(next)
1711
+ const made = next.taskName ? ensureTask(next) : { ok: true }
1712
+ await tick(next)
1713
+ return {
1714
+ said: made.ok
1715
+ ? '密码已从本机配置和启动脚本里删掉;之后 token 失效就得手机扫码'
1716
+ : `密码已删,但任务重建失败:${made.error || ''}`,
1717
+ }
1718
+ }
1557
1719
  case 'detect': {
1558
1720
  const found = detectShellDir()
1559
1721
  return { said: found ? `探测到:${found}` : '常见位置没找到 NapCat Shell 目录', shellDir: found }
@@ -1656,5 +1818,10 @@ export const __internals = {
1656
1818
  killNapcat,
1657
1819
  tick,
1658
1820
  lockBlocked,
1821
+ passwordActive,
1822
+ writeEnvBat,
1823
+ removeEnvBat,
1824
+ envBatPath,
1825
+ md5Hex,
1659
1826
  state,
1660
1827
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-napcat-keeper",
3
- "version": "2.0.2",
3
+ "version": "2.1.1",
4
4
  "description": "发现没有能够随着dsh打开自动启动napcat的插件,遂使用大肥鱼制作此插件,建议搭配Yuuyuko-uu的dsh-qq-bridge使用~",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",