dsh-qq-moony 0.4.0 → 0.5.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,16 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.5.0 — 2026-10-25
4
+
5
+ 装完之后不再一片红:缺什么会自己说出来。
6
+
7
+ **新增**
8
+ - 体检:设置页顶部检查三件事——QQ NT 装没装、NapCat 找不找得到、能不能连上;缺哪样就列出来,写清去哪儿下、哪几步
9
+ - 状态接口多返回 `qqnt` / `napcat` 两个字段
10
+
11
+ **为什么不能一条命令全装**
12
+ QQ NT 是几百兆的客户端(要管理员权限),NapCat 要往 QQ 进程里注入(要匹配版本)——npm 包装不下这两样。插件能做的是:缺东西的时候说清楚缺什么,而不是让人装完盯着一片红发呆。
13
+
3
14
  ## 0.4.0 — 2026-10-25
4
15
 
5
16
  群聊从"只回 @"变成"会参与聊天"。
package/README.md CHANGED
@@ -64,7 +64,9 @@ NapCat 是接管整个 QQ 客户端的行为——消息收发、登录态、空
64
64
 
65
65
  ## 三、设置页(名字叫「QQ嗦嗨嗨」)
66
66
 
67
- 装完重启 DSH,设置里会多一项 **QQ嗦嗨嗨**:
67
+ 装完重启 DSH,设置里会多一项 **QQ嗦嗨嗨**。
68
+
69
+ **顶上一块是体检**:QQ NT 装没装、NapCat 找不找得到、能不能连上。缺哪样它就列哪样,写明去哪儿下、哪几步——不用盯着一片红猜。
68
70
 
69
71
  - **连接状态**:在线没有、登录的是谁、今天发了多少、上限多少
70
72
  - **主动说话**:开关(点一下即时保存)
package/client.js CHANGED
@@ -1,4 +1,4 @@
1
- window.__ModuleLoader__.load({
1
+ window.__ModuleLoader__.load({
2
2
  id: 'dsh-qq-moony',
3
3
  factory(require) {
4
4
  const React = require('react');
@@ -100,61 +100,91 @@
100
100
  const [草稿, 设草稿] = React.useState(null);
101
101
  const [脏, 设脏] = React.useState(false);
102
102
 
103
+ // 草稿没建起来之前,先用状态里的值兜底——省得"保存"之后闪回空白
104
+ const 从状态取 = () => ({
105
+ everyMinMin: 状态?.everyMinMin ?? 45,
106
+ everyMinMax: 状态?.everyMinMax ?? 90,
107
+ perHour: 状态?.perHour ?? 20,
108
+ perDay: 状态?.perDay ?? 60,
109
+ qzoneMinMin: 状态?.qzoneMinMin ?? 120,
110
+ qzoneMinMax: 状态?.qzoneMinMax ?? 240,
111
+ qzonePrompt: 状态?.qzonePrompt ?? '',
112
+ groupWhitelist: (状态?.groupWhitelist ?? []).join(' '),
113
+ groupContextSize: 状态?.groupContextSize ?? 30,
114
+ groupEveryMinMin: 状态?.groupEveryMinMin ?? 8,
115
+ groupEveryMinMax: 状态?.groupEveryMinMax ?? 20,
116
+ groupActiveWindowMin: 状态?.groupActiveWindowMin ?? 5,
117
+ groupCooldownMin: 状态?.groupCooldownMin ?? 10,
118
+ groupPerDay: 状态?.groupPerDay ?? 20,
119
+ });
120
+
103
121
  React.useEffect(() => {
104
122
  if (状态 && 草稿 === null) {
105
- 设草稿({
106
- everyMinMin: 状态.everyMinMin ?? 45,
107
- everyMinMax: 状态.everyMinMax ?? 90,
108
- perHour: 状态.perHour ?? 20,
109
- perDay: 状态.perDay ?? 60,
110
- qzoneMinMin: 状态.qzoneMinMin ?? 120,
111
- qzoneMinMax: 状态.qzoneMinMax ?? 240,
112
- qzonePrompt: 状态.qzonePrompt ?? '',
113
- groupWhitelist: (状态.groupWhitelist ?? []).join(' '),
114
- groupContextSize: 状态.groupContextSize ?? 30,
115
- groupEveryMinMin: 状态.groupEveryMinMin ?? 8,
116
- groupEveryMinMax: 状态.groupEveryMinMax ?? 20,
117
- groupActiveWindowMin: 状态.groupActiveWindowMin ?? 5,
118
- groupCooldownMin: 状态.groupCooldownMin ?? 10,
119
- groupPerDay: 状态.groupPerDay ?? 20,
120
- });
123
+ 设草稿(从状态取());
121
124
  }
122
125
  }, [状态, 草稿]);
123
126
 
124
- const 数字 = (键, 值) => {
125
- 设草稿((旧) => ({ ...(旧 ?? {}), [键]: 值 }));
127
+ const 数字 = (键, 新值) => {
128
+ 设草稿((旧) => ({ ...(旧 ?? 从状态取()), [键]: 新值 }));
126
129
  设脏(true);
127
130
  };
128
131
 
129
132
  const 保存 = async () => {
130
- if (!草稿) return;
133
+ const 值 = 草稿 ?? 从状态取();
131
134
  await 改({
132
- everyMinMin: Number(草稿.everyMinMin),
133
- everyMinMax: Number(草稿.everyMinMax),
134
- perHour: Number(草稿.perHour),
135
- perDay: Number(草稿.perDay),
136
- qzoneMinMin: Number(草稿.qzoneMinMin),
137
- qzoneMinMax: Number(草稿.qzoneMinMax),
138
- qzonePrompt: String(草稿.qzonePrompt ?? ''),
139
- groupWhitelist: String(草稿.groupWhitelist ?? ''),
140
- groupContextSize: Number(草稿.groupContextSize),
141
- groupEveryMinMin: Number(草稿.groupEveryMinMin),
142
- groupEveryMinMax: Number(草稿.groupEveryMinMax),
143
- groupActiveWindowMin: Number(草稿.groupActiveWindowMin),
144
- groupCooldownMin: Number(草稿.groupCooldownMin),
145
- groupPerDay: Number(草稿.groupPerDay),
135
+ everyMinMin: Number(值.everyMinMin),
136
+ everyMinMax: Number(值.everyMinMax),
137
+ perHour: Number(值.perHour),
138
+ perDay: Number(值.perDay),
139
+ qzoneMinMin: Number(值.qzoneMinMin),
140
+ qzoneMinMax: Number(值.qzoneMinMax),
141
+ qzonePrompt: String(值.qzonePrompt ?? ''),
142
+ groupWhitelist: String(值.groupWhitelist ?? ''),
143
+ groupContextSize: Number(值.groupContextSize),
144
+ groupEveryMinMin: Number(值.groupEveryMinMin),
145
+ groupEveryMinMax: Number(值.groupEveryMinMax),
146
+ groupActiveWindowMin: Number(值.groupActiveWindowMin),
147
+ groupCooldownMin: Number(值.groupCooldownMin),
148
+ groupPerDay: Number(值.groupPerDay),
146
149
  });
147
150
  设脏(false);
148
151
  设草稿(null);
149
152
  };
150
153
 
151
- if (状态 === null || 草稿 === null) {
154
+ if (状态 === null) {
152
155
  return h('div', { style: { padding: '8px 0', opacity: 0.6 } }, 错 || '读状态中…');
153
156
  }
154
157
 
158
+ const 值 = 草稿 ?? 从状态取();
159
+ const 缺口 = [];
160
+ if (!状态.qqnt) 缺口.push('没找到 QQ NT —— 去 im.qq.com 装新版(9.9.x)。老版 9.7 注不进去,这是死条件。');
161
+ if (!状态.napcat) 缺口.push('没找到 NapCat —— 去 github.com/NapNeko/NapCatQQ 下 Shell 版,解压后把 napCatDir 指过去。');
162
+ if (状态.qqnt && 状态.napcat && !状态.online)
163
+ 缺口.push('QQ NT 和 NapCat 都在,但连不上 —— 检查 NapCat 启没启、3000 端口、token 三样。');
164
+
155
165
  return h(
156
166
  'div',
157
167
  null,
168
+ 缺口.length > 0
169
+ ? h(
170
+ 'div',
171
+ {
172
+ style: {
173
+ ...卡片,
174
+ flexDirection: 'column',
175
+ alignItems: 'stretch',
176
+ gap: '6px',
177
+ borderColor: 'var(--dsw-alias-warning-1, #d99b2b)',
178
+ },
179
+ },
180
+ h(
181
+ 'div',
182
+ { style: { ...标题, color: 'var(--dsw-alias-warning-1, #d99b2b)' } },
183
+ '还差几步才能用',
184
+ ),
185
+ 缺口.map((行, i) => h('div', { key: i, style: 说明 }, `${i + 1}. ${行}`)),
186
+ )
187
+ : null,
158
188
  h(
159
189
  'div',
160
190
  { style: { ...卡片 } },
@@ -178,7 +208,7 @@
178
208
  'div',
179
209
  null,
180
210
  h('div', { style: 标题 }, '主动说话'),
181
- h('div', { style: 说明 }, `开着它才会自己找你,大约每 ${草稿.everyMinMin}–${草稿.everyMinMax} 分钟有机会开口一次;关掉就只回不发`),
211
+ h('div', { style: 说明 }, `开着它才会自己找你,大约每 ${值.everyMinMin}–${值.everyMinMax} 分钟有机会开口一次;关掉就只回不发`),
182
212
  ),
183
213
  h(开关, { 开: 状态.proactive, 点: () => 改({ proactive: !状态.proactive }) }),
184
214
  ),
@@ -208,7 +238,7 @@
208
238
  h('input', {
209
239
  type: 'number',
210
240
  min: 5,
211
- value: 草稿.everyMinMin,
241
+ value: 值.everyMinMin,
212
242
  onChange: (e) => 数字('everyMinMin', Number(e.target.value)),
213
243
  style: { ...输入框, width: '78px' },
214
244
  }),
@@ -216,7 +246,7 @@
216
246
  h('input', {
217
247
  type: 'number',
218
248
  min: 5,
219
- value: 草稿.everyMinMax,
249
+ value: 值.everyMinMax,
220
250
  onChange: (e) => 数字('everyMinMax', Number(e.target.value)),
221
251
  style: { ...输入框, width: '78px' },
222
252
  }),
@@ -235,7 +265,7 @@
235
265
  type: 'number',
236
266
  min: 1,
237
267
  max: 200,
238
- value: 草稿.perHour,
268
+ value: 值.perHour,
239
269
  onChange: (e) => 数字('perHour', Number(e.target.value)),
240
270
  style: 输入框,
241
271
  }),
@@ -253,7 +283,7 @@
253
283
  type: 'number',
254
284
  min: 1,
255
285
  max: 1000,
256
- value: 草稿.perDay,
286
+ value: 值.perDay,
257
287
  onChange: (e) => 数字('perDay', Number(e.target.value)),
258
288
  style: 输入框,
259
289
  }),
@@ -279,7 +309,7 @@
279
309
  ),
280
310
  h('input', {
281
311
  type: 'text',
282
- value: 草稿.groupWhitelist,
312
+ value: 值.groupWhitelist,
283
313
  placeholder: '群号白名单,空格或逗号分隔;空着 = 群聊不生效',
284
314
  onChange: (e) => 数字('groupWhitelist', e.target.value),
285
315
  style: { ...输入框, width: '100%', boxSizing: 'border-box' },
@@ -292,7 +322,7 @@
292
322
  type: 'number',
293
323
  min: 5,
294
324
  max: 100,
295
- value: 草稿.groupContextSize,
325
+ value: 值.groupContextSize,
296
326
  onChange: (e) => 数字('groupContextSize', Number(e.target.value)),
297
327
  style: { ...输入框, width: '70px' },
298
328
  }),
@@ -300,7 +330,7 @@
300
330
  h('input', {
301
331
  type: 'number',
302
332
  min: 2,
303
- value: 草稿.groupEveryMinMin,
333
+ value: 值.groupEveryMinMin,
304
334
  onChange: (e) => 数字('groupEveryMinMin', Number(e.target.value)),
305
335
  style: { ...输入框, width: '70px' },
306
336
  }),
@@ -308,7 +338,7 @@
308
338
  h('input', {
309
339
  type: 'number',
310
340
  min: 2,
311
- value: 草稿.groupEveryMinMax,
341
+ value: 值.groupEveryMinMax,
312
342
  onChange: (e) => 数字('groupEveryMinMax', Number(e.target.value)),
313
343
  style: { ...输入框, width: '70px' },
314
344
  }),
@@ -316,7 +346,7 @@
316
346
  h('input', {
317
347
  type: 'number',
318
348
  min: 1,
319
- value: 草稿.groupActiveWindowMin,
349
+ value: 值.groupActiveWindowMin,
320
350
  onChange: (e) => 数字('groupActiveWindowMin', Number(e.target.value)),
321
351
  style: { ...输入框, width: '70px' },
322
352
  }),
@@ -329,7 +359,7 @@
329
359
  h('input', {
330
360
  type: 'number',
331
361
  min: 1,
332
- value: 草稿.groupCooldownMin,
362
+ value: 值.groupCooldownMin,
333
363
  onChange: (e) => 数字('groupCooldownMin', Number(e.target.value)),
334
364
  style: { ...输入框, width: '70px' },
335
365
  }),
@@ -337,7 +367,7 @@
337
367
  h('input', {
338
368
  type: 'number',
339
369
  min: 1,
340
- value: 草稿.groupPerDay,
370
+ value: 值.groupPerDay,
341
371
  onChange: (e) => 数字('groupPerDay', Number(e.target.value)),
342
372
  style: { ...输入框, width: '70px' },
343
373
  }),
@@ -363,7 +393,7 @@
363
393
  ),
364
394
  h('textarea', {
365
395
  rows: 3,
366
- value: 草稿.qzonePrompt,
396
+ value: 值.qzonePrompt,
367
397
  placeholder: '例:一周发一次,写点最近的感受,两三句,不要提具体的人和地点',
368
398
  onChange: (e) => 数字('qzonePrompt', e.target.value),
369
399
  style: {
@@ -387,7 +417,7 @@
387
417
  h('input', {
388
418
  type: 'number',
389
419
  min: 10,
390
- value: 草稿.qzoneMinMin,
420
+ value: 值.qzoneMinMin,
391
421
  onChange: (e) => 数字('qzoneMinMin', Number(e.target.value)),
392
422
  style: { ...输入框, width: '78px' },
393
423
  }),
@@ -395,7 +425,7 @@
395
425
  h('input', {
396
426
  type: 'number',
397
427
  min: 10,
398
- value: 草稿.qzoneMinMax,
428
+ value: 值.qzoneMinMax,
399
429
  onChange: (e) => 数字('qzoneMinMax', Number(e.target.value)),
400
430
  style: { ...输入框, width: '78px' },
401
431
  }),
package/index.js CHANGED
@@ -120,6 +120,19 @@ export function apply(ctx, 配置) {
120
120
 
121
121
  ctx.logger?.info?.(`qq-moony: 接到 ${c.baseUrl}(限额 ${c.perHour}/时、${c.perDay}/天)`);
122
122
 
123
+ /** 体检:QQ NT 装没装、NapCat 在不在——缺东西的时候要能说清缺什么。 */
124
+ function 体检() {
125
+ const 候选 = [
126
+ 'C:/Program Files/Tencent/QQNT/QQ.exe',
127
+ 'C:/Program Files (x86)/Tencent/QQNT/QQ.exe',
128
+ join(homedir(), 'AppData/Local/Programs/QQNT/QQ.exe'),
129
+ 'D:/Tencent/QQNT/QQ.exe',
130
+ ];
131
+ const qqnt = 候选.some((p) => existsSync(p));
132
+ const napcat = Boolean(c.napCatDir) && existsSync(join(c.napCatDir, 'launcher-user.bat'));
133
+ return { qqnt, napcat };
134
+ }
135
+
123
136
  /** DSH 起来时顺手看一眼 NapCat:没跑就自己拉起来(免扫码,用 -q 快速登录)。 */
124
137
  async function 确认NapCat在跑() {
125
138
  if (c.autoStartNapCat === false) return;
@@ -658,6 +671,8 @@ export function apply(ctx, 配置) {
658
671
  ok: true,
659
672
  online: 在线,
660
673
  nickname: 昵称,
674
+ qqnt: 体检().qqnt,
675
+ napcat: 体检().napcat,
661
676
  uin: Number(c.uin ?? 0),
662
677
  proactive: Boolean(c.proactive),
663
678
  autoReply: Boolean(c.autoReply),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-qq-moony",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "把 QQ 小号接进 DeepSeek Harness:收发消息、发表情包、发 QQ 空间说说、参与群聊,能自己找话题,自带启动与限速防封。",
5
5
  "keywords": [
6
6
  "dsh",