dsh-qq-moony 0.9.2 → 0.9.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.
Files changed (3) hide show
  1. package/client.js +614 -605
  2. package/index.js +27 -0
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -1,611 +1,620 @@
1
- window.__ModuleLoader__.load({
2
- id: 'dsh-qq-moony',
3
- factory(require) {
4
- const React = require('react');
5
- const h = React.createElement;
6
-
7
- const 卡片 = {
8
- border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.25))',
9
- borderRadius: '12px',
10
- padding: '14px 16px',
11
- marginBottom: '12px',
12
- display: 'flex',
13
- alignItems: 'center',
14
- justifyContent: 'space-between',
15
- gap: '16px',
16
- };
17
- const 标题 = { fontSize: '14px', fontWeight: 600, marginBottom: '4px' };
18
- const 说明 = { fontSize: '12px', opacity: 0.65, lineHeight: '18px' };
19
- const 输入框 = {
20
- width: '90px',
21
- padding: '6px 8px',
22
- borderRadius: '8px',
23
- border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.35))',
24
- background: 'transparent',
25
- color: 'inherit',
26
- };
27
-
28
- function 开关({ 开, 点 }) {
29
- return h(
30
- 'button',
31
- {
32
- type: 'button',
33
- role: 'switch',
34
- 'aria-checked': Boolean(开),
35
- onClick: 点,
36
- style: {
37
- width: '44px',
38
- height: '24px',
39
- borderRadius: '999px',
40
- border: 'none',
41
- cursor: 'pointer',
42
- flexShrink: 0,
43
- background: 开
44
- ? 'var(--dsw-alias-accent-1, #4d8dff)'
45
- : 'var(--dsw-alias-bg-3, rgba(127,127,127,0.35))',
46
- position: 'relative',
47
- transition: 'background 120ms',
48
- },
49
- },
50
- h('span', {
51
- style: {
52
- position: 'absolute',
53
- top: '3px',
54
- left: 开 ? '23px' : '3px',
55
- width: '18px',
56
- height: '18px',
57
- borderRadius: '50%',
58
- background: '#fff',
59
- transition: 'left 120ms',
60
- },
61
- }),
62
- );
63
- }
64
-
65
- function 面板() {
66
- const [状态, 设置状态] = React.useState(null);
67
- const [错, 设置错] = React.useState('');
68
-
69
- const 刷新 = React.useCallback(() => {
70
- fetch('/api/qq-moony/state')
71
- .then((r) => r.json())
72
- .then((s) => 设置状态(s))
73
- .catch((e) => 设置错(String(e)));
74
- }, []);
75
-
76
- React.useEffect(() => {
77
- 刷新();
78
- const 定时 = setInterval(刷新, 15000);
79
- return () => clearInterval(定时);
80
- }, [刷新]);
81
-
82
- const 改 = async (patch) => {
83
- 设置状态((旧) => ({ ...(旧 ?? {}), ...patch }));
84
- try {
85
- const r = await fetch('/api/qq-moony/config', {
86
- method: 'POST',
87
- headers: { 'Content-Type': 'application/json' },
88
- body: JSON.stringify(patch),
89
- });
90
- const s = await r.json();
91
- if (!s.ok) 设置错(s.error || '保存失败');
92
- 刷新();
93
- } catch (e) {
94
- 设置错(String(e));
95
- 刷新();
96
- }
97
- };
98
-
99
- // 数字项走草稿:不立刻提交,等"保存更改"
100
- const [草稿, 设草稿] = React.useState(null);
101
- const [脏, 设脏] = React.useState(false);
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 ?? '',
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-qq-moony',
3
+ factory(require) {
4
+ const React = require('react');
5
+ const h = React.createElement;
6
+
7
+ const 卡片 = {
8
+ border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.25))',
9
+ borderRadius: '12px',
10
+ padding: '14px 16px',
11
+ marginBottom: '12px',
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ justifyContent: 'space-between',
15
+ gap: '16px',
16
+ };
17
+ const 标题 = { fontSize: '14px', fontWeight: 600, marginBottom: '4px' };
18
+ const 说明 = { fontSize: '12px', opacity: 0.65, lineHeight: '18px' };
19
+ const 输入框 = {
20
+ width: '90px',
21
+ padding: '6px 8px',
22
+ borderRadius: '8px',
23
+ border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.35))',
24
+ background: 'transparent',
25
+ color: 'inherit',
26
+ };
27
+
28
+ function 开关({ 开, 点 }) {
29
+ return h(
30
+ 'button',
31
+ {
32
+ type: 'button',
33
+ role: 'switch',
34
+ 'aria-checked': Boolean(开),
35
+ onClick: 点,
36
+ style: {
37
+ width: '44px',
38
+ height: '24px',
39
+ borderRadius: '999px',
40
+ border: 'none',
41
+ cursor: 'pointer',
42
+ flexShrink: 0,
43
+ background: 开
44
+ ? 'var(--dsw-alias-accent-1, #4d8dff)'
45
+ : 'var(--dsw-alias-bg-3, rgba(127,127,127,0.35))',
46
+ position: 'relative',
47
+ transition: 'background 120ms',
48
+ },
49
+ },
50
+ h('span', {
51
+ style: {
52
+ position: 'absolute',
53
+ top: '3px',
54
+ left: 开 ? '23px' : '3px',
55
+ width: '18px',
56
+ height: '18px',
57
+ borderRadius: '50%',
58
+ background: '#fff',
59
+ transition: 'left 120ms',
60
+ },
61
+ }),
62
+ );
63
+ }
64
+
65
+ function 面板() {
66
+ const [状态, 设置状态] = React.useState(null);
67
+ const [错, 设置错] = React.useState('');
68
+
69
+ const 刷新 = React.useCallback(() => {
70
+ fetch('/api/qq-moony/state')
71
+ .then((r) => r.json())
72
+ .then((s) => 设置状态(s))
73
+ .catch((e) => 设置错(String(e)));
74
+ }, []);
75
+
76
+ React.useEffect(() => {
77
+ 刷新();
78
+ fetch('/api/qq-moony/version').then((r) => r.json()).then(设版本).catch(() => {});
79
+ const 定时 = setInterval(刷新, 15000);
80
+ return () => clearInterval(定时);
81
+ }, [刷新]);
82
+
83
+ const 改 = async (patch) => {
84
+ 设置状态((旧) => ({ ...(旧 ?? {}), ...patch }));
85
+ try {
86
+ const r = await fetch('/api/qq-moony/config', {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify(patch),
90
+ });
91
+ const s = await r.json();
92
+ if (!s.ok) 设置错(s.error || '保存失败');
93
+ 刷新();
94
+ } catch (e) {
95
+ 设置错(String(e));
96
+ 刷新();
97
+ }
98
+ };
99
+
100
+ // 数字项走草稿:不立刻提交,等"保存更改"
101
+ const [草稿, 设草稿] = React.useState(null);
102
+ const [脏, 设脏] = React.useState(false);
103
+ const [版本, 设版本] = React.useState(null);
104
+ const [版本忽略, 设版本忽略] = React.useState(false);
105
+
106
+ // 草稿没建起来之前,先用状态里的值兜底——省得"保存"之后闪回空白
107
+ const 从状态取 = () => ({
108
+ everyMinMin: 状态?.everyMinMin ?? 45,
109
+ everyMinMax: 状态?.everyMinMax ?? 90,
110
+ perHour: 状态?.perHour ?? 20,
111
+ perDay: 状态?.perDay ?? 60,
112
+ qzoneMinMin: 状态?.qzoneMinMin ?? 120,
113
+ qzoneMinMax: 状态?.qzoneMinMax ?? 240,
114
+ qzonePrompt: 状态?.qzonePrompt ?? '',
112
115
  persona: 状态?.persona ?? '',
113
116
  stickerLimit: 状态?.stickerLimit ?? 200,
114
117
  maxPerRound: 状态?.maxPerRound ?? 4,
115
- perMinute: 状态?.perMinute ?? 6,
116
- groupWhitelist: (状态?.groupWhitelist ?? []).join(' '),
117
- groupContextSize: 状态?.groupContextSize ?? 30,
118
- groupEveryMinMin: 状态?.groupEveryMinMin ?? 8,
119
- groupEveryMinMax: 状态?.groupEveryMinMax ?? 20,
120
- groupActiveWindowMin: 状态?.groupActiveWindowMin ?? 5,
121
- groupCooldownMin: 状态?.groupCooldownMin ?? 10,
122
- groupPerDay: 状态?.groupPerDay ?? 20,
123
- });
124
-
125
- React.useEffect(() => {
126
- if (状态 && 草稿 === null) {
127
- 设草稿(从状态取());
128
- }
129
- }, [状态, 草稿]);
130
-
131
- const 数字 = (键, 新值) => {
132
- 设草稿((旧) => ({ ...(旧 ?? 从状态取()), [键]: 新值 }));
133
- 设脏(true);
134
- };
135
-
136
- const 保存 = async () => {
137
- const 值 = 草稿 ?? 从状态取();
138
- await 改({
139
- everyMinMin: Number(值.everyMinMin),
140
- everyMinMax: Number(值.everyMinMax),
141
- perHour: Number(值.perHour),
142
- perDay: Number(值.perDay),
143
- qzoneMinMin: Number(值.qzoneMinMin),
144
- qzoneMinMax: Number(值.qzoneMinMax),
145
- qzonePrompt: String(值.qzonePrompt ?? ''),
118
+ perMinute: 状态?.perMinute ?? 6,
119
+ groupWhitelist: (状态?.groupWhitelist ?? []).join(' '),
120
+ groupContextSize: 状态?.groupContextSize ?? 30,
121
+ groupEveryMinMin: 状态?.groupEveryMinMin ?? 8,
122
+ groupEveryMinMax: 状态?.groupEveryMinMax ?? 20,
123
+ groupActiveWindowMin: 状态?.groupActiveWindowMin ?? 5,
124
+ groupCooldownMin: 状态?.groupCooldownMin ?? 10,
125
+ groupPerDay: 状态?.groupPerDay ?? 20,
126
+ });
127
+
128
+ React.useEffect(() => {
129
+ if (状态 && 草稿 === null) {
130
+ 设草稿(从状态取());
131
+ }
132
+ }, [状态, 草稿]);
133
+
134
+ const 数字 = (键, 新值) => {
135
+ 设草稿((旧) => ({ ...(旧 ?? 从状态取()), [键]: 新值 }));
136
+ 设脏(true);
137
+ };
138
+
139
+ const 保存 = async () => {
140
+ const 值 = 草稿 ?? 从状态取();
141
+ await 改({
142
+ everyMinMin: Number(值.everyMinMin),
143
+ everyMinMax: Number(值.everyMinMax),
144
+ perHour: Number(值.perHour),
145
+ perDay: Number(值.perDay),
146
+ qzoneMinMin: Number(值.qzoneMinMin),
147
+ qzoneMinMax: Number(值.qzoneMinMax),
148
+ qzonePrompt: String(值.qzonePrompt ?? ''),
146
149
  persona: String(值.persona ?? ''),
147
150
  stickerLimit: Number(值.stickerLimit),
148
151
  maxPerRound: Number(值.maxPerRound),
149
- perMinute: Number(值.perMinute),
150
- groupWhitelist: String(值.groupWhitelist ?? ''),
151
- groupContextSize: Number(值.groupContextSize),
152
- groupEveryMinMin: Number(值.groupEveryMinMin),
153
- groupEveryMinMax: Number(值.groupEveryMinMax),
154
- groupActiveWindowMin: Number(值.groupActiveWindowMin),
155
- groupCooldownMin: Number(值.groupCooldownMin),
156
- groupPerDay: Number(值.groupPerDay),
157
- });
158
- 设脏(false);
159
- 设草稿(null);
160
- };
161
-
162
- if (状态 === null) {
163
- return h('div', { style: { padding: '8px 0', opacity: 0.6 } }, 错 || '读状态中…');
164
- }
165
-
166
- const 值 = 草稿 ?? 从状态取();
167
- const 缺口 = [];
168
- if (!状态.qqnt) 缺口.push('没找到 QQ NT —— 去 im.qq.com 装新版(9.9.x)。老版 9.7 注不进去,这是死条件。');
169
- if (!状态.napcat) 缺口.push('没找到 NapCat —— 去 github.com/NapNeko/NapCatQQ 下 Shell 版,解压后把 napCatDir 指过去。');
170
- if (状态.portState === 'foreign')
171
- 缺口.push('端口上坐着的不是 NapCat —— 3000 被别的服务占了。换个端口(改 baseUrl),或者把那个服务挪开。');
172
- else if (状态.qqnt && 状态.napcat && !状态.online)
173
- 缺口.push('QQ NT 和 NapCat 都在,但连不上 —— 检查 NapCat 启没启、3000 端口、token 三样。');
174
-
175
- return h(
176
- 'div',
177
- null,
178
- 缺口.length > 0
179
- ? h(
180
- 'div',
181
- {
182
- style: {
183
- ...卡片,
184
- flexDirection: 'column',
185
- alignItems: 'stretch',
186
- gap: '6px',
187
- borderColor: 'var(--dsw-alias-warning-1, #d99b2b)',
188
- },
189
- },
190
- h(
191
- 'div',
192
- { style: { ...标题, color: 'var(--dsw-alias-warning-1, #d99b2b)' } },
193
- '还差几步才能用',
194
- ),
195
- 缺口.map((行, i) => h('div', { key: i, style: 说明 }, `${i + 1}. ${行}`)),
196
- )
197
- : null,
198
- h(
199
- 'div',
200
- { style: { ...卡片 } },
201
- h(
202
- 'div',
203
- null,
204
- h('div', { style: 标题 }, 状态.online ? `已连接:${状态.nickname}` : '没连上 NapCat'),
205
- h(
206
- 'div',
207
- { style: 说明 },
208
- 状态.online
209
- ? `今天回了 ${状态.roundsToday ?? 0} 轮(${状态.sentToday ?? 0} 条),这一小时 ${状态.roundsThisHour ?? 0} 轮(上限 ${状态.perDay}/天、${状态.perHour}/时)`
210
- : '检查 NapCat 有没有启动、3000 端口通不通、token 对不对',
211
- ),
212
- ),
213
- ),
214
- h(
215
- 'div',
216
- { style: { ...卡片, alignItems: 'flex-start' } },
217
- h(
218
- 'div',
219
- { style: { flex: 1 } },
220
- h('div', { style: 标题 }, '人设(可选)'),
221
- h(
222
- 'div',
223
- { style: 说明 },
224
- '填了就把这段当作「你是谁」交给自动回复的会话;留空则用中性的“替账号主人收发消息”。不想让这个号变成别人的角色,就别填。',
225
- ),
226
- h('textarea', {
227
- rows: 3,
228
- value: 值.persona,
229
- placeholder: '例:你是这个账号主人在 DSH 里养的助手,说话短、口语,不自我介绍',
230
- onChange: (e) => 数字('persona', e.target.value),
231
- style: {
232
- width: '100%',
233
- marginTop: '8px',
234
- padding: '8px 10px',
235
- borderRadius: '10px',
236
- border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.35))',
237
- background: 'transparent',
238
- color: 'inherit',
239
- fontSize: '12px',
240
- lineHeight: '19px',
241
- resize: 'vertical',
242
- boxSizing: 'border-box',
243
- },
244
- }),
245
- ),
246
- ),
247
- h(
248
- 'div',
249
- { style: { ...卡片 } },
250
- h(
251
- 'div',
252
- null,
253
- h('div', { style: 标题 }, '主动说话'),
254
- h('div', { style: 说明 }, `开着它才会自己找你,大约每 ${值.everyMinMin}–${值.everyMinMax} 分钟有机会开口一次;关掉就只回不发`),
255
- ),
256
- h(开关, { 开: 状态.proactive, 点: () => 改({ proactive: !状态.proactive }) }),
257
- ),
258
- h(
259
- 'div',
260
- { style: { ...卡片 } },
261
- h(
262
- 'div',
263
- null,
264
- h('div', { style: 标题 }, '自动回复'),
265
- h('div', { style: 说明 }, '收到私聊就投进专用会话去回;群消息一律不接'),
266
- ),
267
- h(开关, { 开: 状态.autoReply, 点: () => 改({ autoReply: !状态.autoReply }) }),
268
- ),
269
- h(
270
- 'div',
271
- { style: { ...卡片 } },
272
- h(
273
- 'div',
274
- null,
275
- h('div', { style: 标题 }, '主动间隔'),
276
- h('div', { style: 说明 }, '每次从这段区间里随机取一个(分钟):最短 / 最长'),
277
- ),
278
- h(
279
- 'div',
280
- { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
281
- h('input', {
282
- type: 'number',
283
- min: 5,
284
- value: 值.everyMinMin,
285
- onChange: (e) => 数字('everyMinMin', Number(e.target.value)),
286
- style: { ...输入框, width: '78px' },
287
- }),
288
- h('span', { style: { opacity: 0.5 } }, '—'),
289
- h('input', {
290
- type: 'number',
291
- min: 5,
292
- value: 值.everyMinMax,
293
- onChange: (e) => 数字('everyMinMax', Number(e.target.value)),
294
- style: { ...输入框, width: '78px' },
295
- }),
296
- ),
297
- ),
298
- h(
299
- 'div',
300
- { style: { ...卡片 } },
301
- h(
302
- 'div',
303
- null,
304
- h('div', { style: 标题 }, '每小时上限'),
305
- h('div', { style: 说明 }, '一小时内最多回几轮(1–200),到顶直接拒发'),
306
- ),
307
- h('input', {
308
- type: 'number',
309
- min: 1,
310
- max: 200,
311
- value: 值.perHour,
312
- onChange: (e) => 数字('perHour', Number(e.target.value)),
313
- style: 输入框,
314
- }),
315
- ),
316
- h(
317
- 'div',
318
- { style: { ...卡片 } },
319
- h(
320
- 'div',
321
- null,
322
- h('div', { style: 标题 }, '每天上限'),
323
- h('div', { style: 说明 }, '一天内最多回几轮(1–1000),按自然日算'),
324
- ),
325
- h('input', {
326
- type: 'number',
327
- min: 1,
328
- max: 1000,
329
- value: 值.perDay,
330
- onChange: (e) => 数字('perDay', Number(e.target.value)),
331
- style: 输入框,
332
- }),
333
- ),
334
- h(
335
- 'div',
336
- { style: { ...卡片, alignItems: 'flex-start', flexDirection: 'column', gap: '10px' } },
337
- h(
338
- 'div',
339
- { style: { display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'space-between', gap: '16px' } },
340
- h(
341
- 'div',
342
- null,
343
- h('div', { style: 标题 }, '群聊'),
344
- h('div', { style: 说明 }, '白名单里的群才理;只接被 @ 的,或者按下面的节奏自己插一句'),
345
- ),
346
- h('div', { style: { display: 'flex', gap: '10px', alignItems: 'center' } },
347
- h('span', { style: 说明 }, '接消息'),
348
- h(开关, { 开: 状态.groupReply, 点: () => 改({ groupReply: !状态.groupReply }) }),
349
- h('span', { style: 说明 }, '参与聊天'),
350
- h(开关, { 开: 状态.groupChat, 点: () => 改({ groupChat: !状态.groupChat }) }),
351
- ),
352
- ),
353
- h('input', {
354
- type: 'text',
355
- value: 值.groupWhitelist,
356
- placeholder: '群号白名单,空格或逗号分隔;空着 = 群聊不生效',
357
- onChange: (e) => 数字('groupWhitelist', e.target.value),
358
- style: { ...输入框, width: '100%', boxSizing: 'border-box' },
359
- }),
360
- h(
361
- 'div',
362
- { style: { display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' } },
363
- h('span', { style: 说明 }, '记住最近'),
364
- h('input', {
365
- type: 'number',
366
- min: 5,
367
- max: 100,
368
- value: 值.groupContextSize,
369
- onChange: (e) => 数字('groupContextSize', Number(e.target.value)),
370
- style: { ...输入框, width: '70px' },
371
- }),
372
- h('span', { style: 说明 }, '条 · 每'),
373
- h('input', {
374
- type: 'number',
375
- min: 2,
376
- value: 值.groupEveryMinMin,
377
- onChange: (e) => 数字('groupEveryMinMin', Number(e.target.value)),
378
- style: { ...输入框, width: '70px' },
379
- }),
380
- h('span', { style: { opacity: 0.5 } }, '—'),
381
- h('input', {
382
- type: 'number',
383
- min: 2,
384
- value: 值.groupEveryMinMax,
385
- onChange: (e) => 数字('groupEveryMinMax', Number(e.target.value)),
386
- style: { ...输入框, width: '70px' },
387
- }),
388
- h('span', { style: 说明 }, '分钟给一次机会 · 冷场'),
389
- h('input', {
390
- type: 'number',
391
- min: 1,
392
- value: 值.groupActiveWindowMin,
393
- onChange: (e) => 数字('groupActiveWindowMin', Number(e.target.value)),
394
- style: { ...输入框, width: '70px' },
395
- }),
396
- h('span', { style: 说明 }, '分钟内没人才开口'),
397
- ),
398
- h(
399
- 'div',
400
- { style: { display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' } },
401
- h('span', { style: 说明 }, '同群冷却'),
402
- h('input', {
403
- type: 'number',
404
- min: 1,
405
- value: 值.groupCooldownMin,
406
- onChange: (e) => 数字('groupCooldownMin', Number(e.target.value)),
407
- style: { ...输入框, width: '70px' },
408
- }),
409
- h('span', { style: 说明 }, '分钟 · 群里一天最多'),
410
- h('input', {
411
- type: 'number',
412
- min: 1,
413
- value: 值.groupPerDay,
414
- onChange: (e) => 数字('groupPerDay', Number(e.target.value)),
415
- style: { ...输入框, width: '70px' },
416
- }),
417
- h('span', { style: 说明 }, '条'),
418
- ),
419
- h(
420
- 'div',
421
- { style: { ...说明, color: 'var(--dsw-alias-warning-1, #d99b2b)' } },
422
- '⚠️ 参与聊天很费 token:每给一次机会都等于跑一整轮会话,哪怕它最后决定不说话。间隔别设太短。',
423
- ),
424
- ),
425
- h(
426
- 'div',
427
- { style: { ...卡片, alignItems: 'flex-start' } },
428
- h(
429
- 'div',
430
- { style: { flex: 1 } },
431
- h('div', { style: 标题 }, 'QQ 空间自动发说说'),
432
- h(
433
- 'div',
434
- { style: 说明 },
435
- '开着才会自己去空间发;下面写要求(内容、时间、语气都行),到点把这句交给它判断',
436
- ),
437
- h('textarea', {
438
- rows: 3,
439
- value: 值.qzonePrompt,
440
- placeholder: '例:一周发一次,写点最近的感受,两三句,不要提具体的人和地点',
441
- onChange: (e) => 数字('qzonePrompt', e.target.value),
442
- style: {
443
- width: '100%',
444
- marginTop: '8px',
445
- padding: '8px 10px',
446
- borderRadius: '10px',
447
- border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.35))',
448
- background: 'transparent',
449
- color: 'inherit',
450
- fontSize: '12px',
451
- lineHeight: '19px',
452
- resize: 'vertical',
453
- boxSizing: 'border-box',
454
- },
455
- }),
456
- h(
457
- 'div',
458
- { style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '8px' } },
459
- h('span', { style: 说明 }, '每隔'),
460
- h('input', {
461
- type: 'number',
462
- min: 10,
463
- value: 值.qzoneMinMin,
464
- onChange: (e) => 数字('qzoneMinMin', Number(e.target.value)),
465
- style: { ...输入框, width: '78px' },
466
- }),
467
- h('span', { style: { opacity: 0.5 } }, '—'),
468
- h('input', {
469
- type: 'number',
470
- min: 10,
471
- value: 值.qzoneMinMax,
472
- onChange: (e) => 数字('qzoneMinMax', Number(e.target.value)),
473
- style: { ...输入框, width: '78px' },
474
- }),
475
- h('span', { style: 说明 }, '分钟随机一次'),
476
- ),
477
- ),
478
- h(开关, { 开: 状态.qzoneAuto, 点: () => 改({ qzoneAuto: !状态.qzoneAuto }) }),
479
- ),
480
- h(
481
- 'div',
482
- { style: { ...卡片, alignItems: 'flex-start', flexDirection: 'column', gap: '8px' } },
483
- h(
484
- 'div',
485
- { style: { display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'space-between', gap: '16px' } },
486
- h(
487
- 'div',
488
- null,
489
- h('div', { style: 标题 }, '一句一句说'),
490
- h('div', { style: 说明 }, '一段话按句拆成几条发,中间隔一两秒;一轮最多几条、每分钟硬上限都在下面'),
491
- ),
492
- h(开关, { 开: 状态.splitReply, 点: () => 改({ splitReply: !状态.splitReply }) }),
493
- ),
494
- h(
495
- 'div',
496
- { style: { display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' } },
497
- h('span', { style: 说明 }, '一轮最多'),
498
- h('input', {
499
- type: 'number',
500
- min: 1,
501
- max: 8,
502
- value: 值.maxPerRound,
503
- onChange: (e) => 数字('maxPerRound', Number(e.target.value)),
504
- style: { ...输入框, width: '70px' },
505
- }),
506
- h('span', { style: 说明 }, '条 · 每分钟最多'),
507
- h('input', {
508
- type: 'number',
509
- min: 1,
510
- max: 60,
511
- value: 值.perMinute,
512
- onChange: (e) => 数字('perMinute', Number(e.target.value)),
513
- style: { ...输入框, width: '70px' },
514
- }),
515
- h('span', { style: 说明 }, '条(防封兜底)'),
516
- ),
517
- ),
518
- h(
519
- 'div',
520
- { style: { ...卡片 } },
521
- h(
522
- 'div',
523
- null,
524
- h('div', { style: 标题 }, '不限条数'),
525
- h('div', { style: 说明 }, '关掉每小时/每天的上限。⚠️ 只在确实需要时开——额度是防封的第一道闸'),
526
- ),
527
- h(开关, { 开: 状态.limitsUnlimited, 点: () => 改({ limitsUnlimited: !状态.limitsUnlimited }) }),
528
- ),
529
- h(
530
- 'div',
531
- { style: { ...卡片 } },
532
- h(
533
- 'div',
534
- null,
535
- h('div', { style: 标题 }, '配表情包'),
536
- h('div', { style: 说明 }, '回话时如果合适,自己从库里挑一张一起发'),
537
- ),
538
- h(开关, { 开: 状态.stickerOnReply, 点: () => 改({ stickerOnReply: !状态.stickerOnReply }) }),
539
- ),
540
- h(
541
- 'div',
542
- { style: { ...卡片 } },
543
- h(
544
- 'div',
545
- null,
546
- h('div', { style: 标题 }, '自动收录表情包'),
547
- h('div', { style: 说明 }, '收到图会看一眼,值得收才收(重复、糊的、真人照片不收)'),
548
- ),
549
- h(开关, { 开: 状态.autoTagStickers, 点: () => 改({ autoTagStickers: !状态.autoTagStickers }) }),
550
- ),
551
- h(
552
- 'div',
553
- { style: { ...卡片 } },
554
- h(
555
- 'div',
556
- null,
557
- h('div', { style: 标题 }, '表情库上限'),
558
- h('div', { style: 说明 }, `现在存了 ${状态.stickerCount ?? 0} 张。超过上限时淘汰最旧的记录(原文件不删)`),
559
- ),
560
- h('input', {
561
- type: 'number',
562
- min: 1,
563
- max: 2000,
564
- value: 值.stickerLimit,
565
- onChange: (e) => 数字('stickerLimit', Number(e.target.value)),
566
- style: 输入框,
567
- }),
568
- ),
569
- h(
570
- 'div',
571
- { style: { display: 'flex', justifyContent: 'flex-end', gap: '10px', alignItems: 'center' } },
572
- 脏 ? h('span', { style: { ...说明, opacity: 0.7 } }, '还没保存') : null,
573
- h(
574
- 'button',
575
- {
576
- type: 'button',
577
- onClick: 保存,
578
- disabled: !脏,
579
- style: {
580
- padding: '8px 18px',
581
- borderRadius: '10px',
582
- border: 'none',
583
- cursor: 脏 ? 'pointer' : 'default',
584
- fontSize: '13px',
585
- fontWeight: 600,
586
- color: '#fff',
587
- background: 脏 ? 'var(--dsw-alias-accent-1, #4d8dff)' : 'var(--dsw-alias-bg-3, rgba(127,127,127,0.35))',
588
- },
589
- },
590
- '保存更改',
591
- ),
592
- ),
593
- 错
594
- ? h('div', { style: { ...说明, color: 'var(--dsw-alias-danger-1, #ff5d5d)' } }, 错)
595
- : null,
596
- );
597
- }
598
-
599
- return {
600
- inject: ['slots'],
601
- apply(ctx) {
602
- ctx.slots.inject('settings.section', () =>
603
- ctx.slots.register(
604
- { name: 'settings.section', id: 'qq-moony', order: 32, label: 'QQ嗦嗨嗨' },
605
- 面板,
606
- ),
607
- );
608
- },
609
- };
610
- },
611
- });
152
+ perMinute: Number(值.perMinute),
153
+ groupWhitelist: String(值.groupWhitelist ?? ''),
154
+ groupContextSize: Number(值.groupContextSize),
155
+ groupEveryMinMin: Number(值.groupEveryMinMin),
156
+ groupEveryMinMax: Number(值.groupEveryMinMax),
157
+ groupActiveWindowMin: Number(值.groupActiveWindowMin),
158
+ groupCooldownMin: Number(值.groupCooldownMin),
159
+ groupPerDay: Number(值.groupPerDay),
160
+ });
161
+ 设脏(false);
162
+ 设草稿(null);
163
+ };
164
+
165
+ if (状态 === null) {
166
+ return h('div', { style: { padding: '8px 0', opacity: 0.6 } }, 错 || '读状态中…');
167
+ }
168
+
169
+ const 值 = 草稿 ?? 从状态取();
170
+ const 缺口 = [];
171
+ if (!状态.qqnt) 缺口.push('没找到 QQ NT —— 去 im.qq.com 装新版(9.9.x)。老版 9.7 注不进去,这是死条件。');
172
+ if (!状态.napcat) 缺口.push('没找到 NapCat —— 去 github.com/NapNeko/NapCatQQ 下 Shell 版,解压后把 napCatDir 指过去。');
173
+ if (状态.portState === 'foreign')
174
+ 缺口.push('端口上坐着的不是 NapCat —— 3000 被别的服务占了。换个端口(改 baseUrl),或者把那个服务挪开。');
175
+ else if (状态.qqnt && 状态.napcat && !状态.online)
176
+ 缺口.push('QQ NT 和 NapCat 都在,但连不上 —— 检查 NapCat 启没启、3000 端口、token 三样。');
177
+
178
+ const 内容 = () => h(
179
+ 'div',
180
+ null,
181
+ 缺口.length > 0
182
+ ? h(
183
+ 'div',
184
+ {
185
+ style: {
186
+ ...卡片,
187
+ flexDirection: 'column',
188
+ alignItems: 'stretch',
189
+ gap: '6px',
190
+ borderColor: 'var(--dsw-alias-warning-1, #d99b2b)',
191
+ },
192
+ },
193
+ h(
194
+ 'div',
195
+ { style: { ...标题, color: 'var(--dsw-alias-warning-1, #d99b2b)' } },
196
+ '还差几步才能用',
197
+ ),
198
+ 缺口.map((行, i) => h('div', { key: i, style: 说明 }, `${i + 1}. ${行}`)),
199
+ )
200
+ : null,
201
+ h(
202
+ 'div',
203
+ { style: { ...卡片 } },
204
+ h(
205
+ 'div',
206
+ null,
207
+ h('div', { style: 标题 }, 状态.online ? `已连接:${状态.nickname}` : '没连上 NapCat'),
208
+ h(
209
+ 'div',
210
+ { style: 说明 },
211
+ 状态.online
212
+ ? `今天回了 ${状态.roundsToday ?? 0} 轮(${状态.sentToday ?? 0} 条),这一小时 ${状态.roundsThisHour ?? 0} 轮(上限 ${状态.perDay}/天、${状态.perHour}/时)`
213
+ : '检查 NapCat 有没有启动、3000 端口通不通、token 对不对',
214
+ ),
215
+ ),
216
+ ),
217
+ h(
218
+ 'div',
219
+ { style: { ...卡片, alignItems: 'flex-start' } },
220
+ h(
221
+ 'div',
222
+ { style: { flex: 1 } },
223
+ h('div', { style: 标题 }, '人设(可选)'),
224
+ h(
225
+ 'div',
226
+ { style: 说明 },
227
+ '填了就把这段当作「你是谁」交给自动回复的会话;留空则用中性的“替账号主人收发消息”。不想让这个号变成别人的角色,就别填。',
228
+ ),
229
+ h('textarea', {
230
+ rows: 3,
231
+ value: 值.persona,
232
+ placeholder: '例:你是这个账号主人在 DSH 里养的助手,说话短、口语,不自我介绍',
233
+ onChange: (e) => 数字('persona', e.target.value),
234
+ style: {
235
+ width: '100%',
236
+ marginTop: '8px',
237
+ padding: '8px 10px',
238
+ borderRadius: '10px',
239
+ border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.35))',
240
+ background: 'transparent',
241
+ color: 'inherit',
242
+ fontSize: '12px',
243
+ lineHeight: '19px',
244
+ resize: 'vertical',
245
+ boxSizing: 'border-box',
246
+ },
247
+ }),
248
+ ),
249
+ ),
250
+ h(
251
+ 'div',
252
+ { style: { ...卡片 } },
253
+ h(
254
+ 'div',
255
+ null,
256
+ h('div', { style: 标题 }, '主动说话'),
257
+ h('div', { style: 说明 }, `开着它才会自己找你,大约每 ${值.everyMinMin}–${值.everyMinMax} 分钟有机会开口一次;关掉就只回不发`),
258
+ ),
259
+ h(开关, { 开: 状态.proactive, 点: () => 改({ proactive: !状态.proactive }) }),
260
+ ),
261
+ h(
262
+ 'div',
263
+ { style: { ...卡片 } },
264
+ h(
265
+ 'div',
266
+ null,
267
+ h('div', { style: 标题 }, '自动回复'),
268
+ h('div', { style: 说明 }, '收到私聊就投进专用会话去回;群消息一律不接'),
269
+ ),
270
+ h(开关, { 开: 状态.autoReply, 点: () => 改({ autoReply: !状态.autoReply }) }),
271
+ ),
272
+ h(
273
+ 'div',
274
+ { style: { ...卡片 } },
275
+ h(
276
+ 'div',
277
+ null,
278
+ h('div', { style: 标题 }, '主动间隔'),
279
+ h('div', { style: 说明 }, '每次从这段区间里随机取一个(分钟):最短 / 最长'),
280
+ ),
281
+ h(
282
+ 'div',
283
+ { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
284
+ h('input', {
285
+ type: 'number',
286
+ min: 5,
287
+ value: 值.everyMinMin,
288
+ onChange: (e) => 数字('everyMinMin', Number(e.target.value)),
289
+ style: { ...输入框, width: '78px' },
290
+ }),
291
+ h('span', { style: { opacity: 0.5 } }, '—'),
292
+ h('input', {
293
+ type: 'number',
294
+ min: 5,
295
+ value: 值.everyMinMax,
296
+ onChange: (e) => 数字('everyMinMax', Number(e.target.value)),
297
+ style: { ...输入框, width: '78px' },
298
+ }),
299
+ ),
300
+ ),
301
+ h(
302
+ 'div',
303
+ { style: { ...卡片 } },
304
+ h(
305
+ 'div',
306
+ null,
307
+ h('div', { style: 标题 }, '每小时上限'),
308
+ h('div', { style: 说明 }, '一小时内最多回几轮(1–200),到顶直接拒发'),
309
+ ),
310
+ h('input', {
311
+ type: 'number',
312
+ min: 1,
313
+ max: 200,
314
+ value: 值.perHour,
315
+ onChange: (e) => 数字('perHour', Number(e.target.value)),
316
+ style: 输入框,
317
+ }),
318
+ ),
319
+ h(
320
+ 'div',
321
+ { style: { ...卡片 } },
322
+ h(
323
+ 'div',
324
+ null,
325
+ h('div', { style: 标题 }, '每天上限'),
326
+ h('div', { style: 说明 }, '一天内最多回几轮(1–1000),按自然日算'),
327
+ ),
328
+ h('input', {
329
+ type: 'number',
330
+ min: 1,
331
+ max: 1000,
332
+ value: 值.perDay,
333
+ onChange: (e) => 数字('perDay', Number(e.target.value)),
334
+ style: 输入框,
335
+ }),
336
+ ),
337
+ h(
338
+ 'div',
339
+ { style: { ...卡片, alignItems: 'flex-start', flexDirection: 'column', gap: '10px' } },
340
+ h(
341
+ 'div',
342
+ { style: { display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'space-between', gap: '16px' } },
343
+ h(
344
+ 'div',
345
+ null,
346
+ h('div', { style: 标题 }, '群聊'),
347
+ h('div', { style: 说明 }, '白名单里的群才理;只接被 @ 的,或者按下面的节奏自己插一句'),
348
+ ),
349
+ h('div', { style: { display: 'flex', gap: '10px', alignItems: 'center' } },
350
+ h('span', { style: 说明 }, '接消息'),
351
+ h(开关, { 开: 状态.groupReply, 点: () => 改({ groupReply: !状态.groupReply }) }),
352
+ h('span', { style: 说明 }, '参与聊天'),
353
+ h(开关, { 开: 状态.groupChat, 点: () => 改({ groupChat: !状态.groupChat }) }),
354
+ ),
355
+ ),
356
+ h('input', {
357
+ type: 'text',
358
+ value: 值.groupWhitelist,
359
+ placeholder: '群号白名单,空格或逗号分隔;空着 = 群聊不生效',
360
+ onChange: (e) => 数字('groupWhitelist', e.target.value),
361
+ style: { ...输入框, width: '100%', boxSizing: 'border-box' },
362
+ }),
363
+ h(
364
+ 'div',
365
+ { style: { display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' } },
366
+ h('span', { style: 说明 }, '记住最近'),
367
+ h('input', {
368
+ type: 'number',
369
+ min: 5,
370
+ max: 100,
371
+ value: 值.groupContextSize,
372
+ onChange: (e) => 数字('groupContextSize', Number(e.target.value)),
373
+ style: { ...输入框, width: '70px' },
374
+ }),
375
+ h('span', { style: 说明 }, '条 · 每'),
376
+ h('input', {
377
+ type: 'number',
378
+ min: 2,
379
+ value: 值.groupEveryMinMin,
380
+ onChange: (e) => 数字('groupEveryMinMin', Number(e.target.value)),
381
+ style: { ...输入框, width: '70px' },
382
+ }),
383
+ h('span', { style: { opacity: 0.5 } }, '—'),
384
+ h('input', {
385
+ type: 'number',
386
+ min: 2,
387
+ value: 值.groupEveryMinMax,
388
+ onChange: (e) => 数字('groupEveryMinMax', Number(e.target.value)),
389
+ style: { ...输入框, width: '70px' },
390
+ }),
391
+ h('span', { style: 说明 }, '分钟给一次机会 · 冷场'),
392
+ h('input', {
393
+ type: 'number',
394
+ min: 1,
395
+ value: 值.groupActiveWindowMin,
396
+ onChange: (e) => 数字('groupActiveWindowMin', Number(e.target.value)),
397
+ style: { ...输入框, width: '70px' },
398
+ }),
399
+ h('span', { style: 说明 }, '分钟内没人才开口'),
400
+ ),
401
+ h(
402
+ 'div',
403
+ { style: { display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' } },
404
+ h('span', { style: 说明 }, '同群冷却'),
405
+ h('input', {
406
+ type: 'number',
407
+ min: 1,
408
+ value: 值.groupCooldownMin,
409
+ onChange: (e) => 数字('groupCooldownMin', Number(e.target.value)),
410
+ style: { ...输入框, width: '70px' },
411
+ }),
412
+ h('span', { style: 说明 }, '分钟 · 群里一天最多'),
413
+ h('input', {
414
+ type: 'number',
415
+ min: 1,
416
+ value: 值.groupPerDay,
417
+ onChange: (e) => 数字('groupPerDay', Number(e.target.value)),
418
+ style: { ...输入框, width: '70px' },
419
+ }),
420
+ h('span', { style: 说明 }, '条'),
421
+ ),
422
+ h(
423
+ 'div',
424
+ { style: { ...说明, color: 'var(--dsw-alias-warning-1, #d99b2b)' } },
425
+ '⚠️ 参与聊天很费 token:每给一次机会都等于跑一整轮会话,哪怕它最后决定不说话。间隔别设太短。',
426
+ ),
427
+ ),
428
+ h(
429
+ 'div',
430
+ { style: { ...卡片, alignItems: 'flex-start' } },
431
+ h(
432
+ 'div',
433
+ { style: { flex: 1 } },
434
+ h('div', { style: 标题 }, 'QQ 空间自动发说说'),
435
+ h(
436
+ 'div',
437
+ { style: 说明 },
438
+ '开着才会自己去空间发;下面写要求(内容、时间、语气都行),到点把这句交给它判断',
439
+ ),
440
+ h('textarea', {
441
+ rows: 3,
442
+ value: 值.qzonePrompt,
443
+ placeholder: '例:一周发一次,写点最近的感受,两三句,不要提具体的人和地点',
444
+ onChange: (e) => 数字('qzonePrompt', e.target.value),
445
+ style: {
446
+ width: '100%',
447
+ marginTop: '8px',
448
+ padding: '8px 10px',
449
+ borderRadius: '10px',
450
+ border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.35))',
451
+ background: 'transparent',
452
+ color: 'inherit',
453
+ fontSize: '12px',
454
+ lineHeight: '19px',
455
+ resize: 'vertical',
456
+ boxSizing: 'border-box',
457
+ },
458
+ }),
459
+ h(
460
+ 'div',
461
+ { style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '8px' } },
462
+ h('span', { style: 说明 }, '每隔'),
463
+ h('input', {
464
+ type: 'number',
465
+ min: 10,
466
+ value: 值.qzoneMinMin,
467
+ onChange: (e) => 数字('qzoneMinMin', Number(e.target.value)),
468
+ style: { ...输入框, width: '78px' },
469
+ }),
470
+ h('span', { style: { opacity: 0.5 } }, '—'),
471
+ h('input', {
472
+ type: 'number',
473
+ min: 10,
474
+ value: 值.qzoneMinMax,
475
+ onChange: (e) => 数字('qzoneMinMax', Number(e.target.value)),
476
+ style: { ...输入框, width: '78px' },
477
+ }),
478
+ h('span', { style: 说明 }, '分钟随机一次'),
479
+ ),
480
+ ),
481
+ h(开关, { 开: 状态.qzoneAuto, 点: () => 改({ qzoneAuto: !状态.qzoneAuto }) }),
482
+ ),
483
+ h(
484
+ 'div',
485
+ { style: { ...卡片, alignItems: 'flex-start', flexDirection: 'column', gap: '8px' } },
486
+ h(
487
+ 'div',
488
+ { style: { display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'space-between', gap: '16px' } },
489
+ h(
490
+ 'div',
491
+ null,
492
+ h('div', { style: 标题 }, '一句一句说'),
493
+ h('div', { style: 说明 }, '一段话按句拆成几条发,中间隔一两秒;一轮最多几条、每分钟硬上限都在下面'),
494
+ ),
495
+ h(开关, { 开: 状态.splitReply, 点: () => 改({ splitReply: !状态.splitReply }) }),
496
+ ),
497
+ h(
498
+ 'div',
499
+ { style: { display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' } },
500
+ h('span', { style: 说明 }, '一轮最多'),
501
+ h('input', {
502
+ type: 'number',
503
+ min: 1,
504
+ max: 8,
505
+ value: 值.maxPerRound,
506
+ onChange: (e) => 数字('maxPerRound', Number(e.target.value)),
507
+ style: { ...输入框, width: '70px' },
508
+ }),
509
+ h('span', { style: 说明 }, '条 · 每分钟最多'),
510
+ h('input', {
511
+ type: 'number',
512
+ min: 1,
513
+ max: 60,
514
+ value: 值.perMinute,
515
+ onChange: (e) => 数字('perMinute', Number(e.target.value)),
516
+ style: { ...输入框, width: '70px' },
517
+ }),
518
+ h('span', { style: 说明 }, '条(防封兜底)'),
519
+ ),
520
+ ),
521
+ h(
522
+ 'div',
523
+ { style: { ...卡片 } },
524
+ h(
525
+ 'div',
526
+ null,
527
+ h('div', { style: 标题 }, '不限条数'),
528
+ h('div', { style: 说明 }, '关掉每小时/每天的上限。⚠️ 只在确实需要时开——额度是防封的第一道闸'),
529
+ ),
530
+ h(开关, { 开: 状态.limitsUnlimited, 点: () => 改({ limitsUnlimited: !状态.limitsUnlimited }) }),
531
+ ),
532
+ h(
533
+ 'div',
534
+ { style: { ...卡片 } },
535
+ h(
536
+ 'div',
537
+ null,
538
+ h('div', { style: 标题 }, '配表情包'),
539
+ h('div', { style: 说明 }, '回话时如果合适,自己从库里挑一张一起发'),
540
+ ),
541
+ h(开关, { 开: 状态.stickerOnReply, 点: () => 改({ stickerOnReply: !状态.stickerOnReply }) }),
542
+ ),
543
+ h(
544
+ 'div',
545
+ { style: { ...卡片 } },
546
+ h(
547
+ 'div',
548
+ null,
549
+ h('div', { style: 标题 }, '自动收录表情包'),
550
+ h('div', { style: 说明 }, '收到图会看一眼,值得收才收(重复、糊的、真人照片不收)'),
551
+ ),
552
+ h(开关, { 开: 状态.autoTagStickers, 点: () => 改({ autoTagStickers: !状态.autoTagStickers }) }),
553
+ ),
554
+ h(
555
+ 'div',
556
+ { style: { ...卡片 } },
557
+ h(
558
+ 'div',
559
+ null,
560
+ h('div', { style: 标题 }, '表情库上限'),
561
+ h('div', { style: 说明 }, `现在存了 ${状态.stickerCount ?? 0} 张。超过上限时淘汰最旧的记录(原文件不删)`),
562
+ ),
563
+ h('input', {
564
+ type: 'number',
565
+ min: 1,
566
+ max: 2000,
567
+ value: 值.stickerLimit,
568
+ onChange: (e) => 数字('stickerLimit', Number(e.target.value)),
569
+ style: 输入框,
570
+ }),
571
+ ),
572
+ h(
573
+ 'div',
574
+ { style: { display: 'flex', justifyContent: 'flex-end', gap: '10px', alignItems: 'center' } },
575
+ 脏 ? h('span', { style: { ...说明, opacity: 0.7 } }, '还没保存') : null,
576
+ h(
577
+ 'button',
578
+ {
579
+ type: 'button',
580
+ onClick: 保存,
581
+ disabled: !脏,
582
+ style: {
583
+ padding: '8px 18px',
584
+ borderRadius: '10px',
585
+ border: 'none',
586
+ cursor: 脏 ? 'pointer' : 'default',
587
+ fontSize: '13px',
588
+ fontWeight: 600,
589
+ color: '#fff',
590
+ background: 脏 ? 'var(--dsw-alias-accent-1, #4d8dff)' : 'var(--dsw-alias-bg-3, rgba(127,127,127,0.35))',
591
+ },
592
+ },
593
+ '保存更改',
594
+ ),
595
+ ),
596
+ 错
597
+ ? h('div', { style: { ...说明, color: 'var(--dsw-alias-danger-1, #ff5d5d)' } }, 错)
598
+ : null,
599
+ );
600
+
601
+ try {
602
+ return 内容();
603
+ } catch (e) {
604
+ return h('div', { style: { padding: '8px 0' } }, '面板渲染出错:' + ((e && e.message) || e));
605
+ }
606
+ }
607
+
608
+ return {
609
+ inject: ['slots'],
610
+ apply(ctx) {
611
+ ctx.slots.inject('settings.section', () =>
612
+ ctx.slots.register(
613
+ { name: 'settings.section', id: 'qq-moony', order: 32, label: 'QQ嗦嗨嗨' },
614
+ 面板,
615
+ ),
616
+ );
617
+ },
618
+ };
619
+ },
620
+ });
package/index.js CHANGED
@@ -817,6 +817,33 @@ export function apply(ctx, 配置) {
817
817
  return;
818
818
  }
819
819
 
820
+ if (路径 === '/api/qq-moony/version') {
821
+ let 本版 = '';
822
+ try {
823
+ 本版 = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version ?? '';
824
+ } catch {
825
+ /* 读不到就空着 */
826
+ }
827
+ let 最新 = '';
828
+ let 说明 = '';
829
+ try {
830
+ const r = await fetch('https://registry.npmmirror.com/dsh-qq-moony/latest', {
831
+ signal: AbortSignal.timeout(6000),
832
+ });
833
+ const 体 = await r.json();
834
+ 最新 = String(体?.version ?? '');
835
+ } catch (e) {
836
+ 说明 = `查不到最新版:${e?.message ?? e}`;
837
+ }
838
+ 发json(res, 200, {
839
+ ok: true,
840
+ installed: 本版,
841
+ latest: 最新,
842
+ hasUpdate: Boolean(本版 && 最新 && 最新 !== 本版),
843
+ note: 说明,
844
+ });
845
+ return;
846
+ }
820
847
  if (路径 === '/api/qq-moony/config' && req.method === 'POST') {
821
848
  let 体 = '';
822
849
  for await (const 块 of req) 体 += 块;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-qq-moony",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "把 QQ 小号接进 DeepSeek Harness:收发消息、发表情包、发 QQ 空间说说、参与群聊,能自己找话题,自带启动与限速防封。",
5
5
  "keywords": [
6
6
  "dsh",