dsh-qq-moony 0.8.1 → 0.9.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/client.js +47 -5
  3. package/index.js +104 -29
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.9.1 — 2026-10-25
4
+
5
+ - **每小时 / 每天的额度一律按「轮」算**,不再看拆条开关。一轮 = 一次回复(可能拆成几条),只占一份额度。
6
+ - 每分钟上限仍然按真实条数算,不受影响——它是防封兜底。
7
+ - 设置页文案同步:「每小时上限」→「一小时内最多回几轮」;状态栏改成"今天回了 N 轮(M 条)"。
8
+ ## 0.9.0 — 2026-10-25
9
+
10
+ 一句一句地说,额度按"轮"算。
11
+
12
+ - **拆条发送**:`qq_send` 收到一段话会按句末标点拆开、逐条发,中间隔 1–3 秒。太短的("哦。""!")会并进上一句;一轮最多 `maxMessagesPerRound` 条(默认 4),多的并到最后一条。
13
+ - **额度改按轮计**:`perHour` / `perDay` 现在数的是"轮",一轮只占一份——人本来也不是按条说话的。
14
+ - **技术限速不松**:`perMinute`(默认 6)永远按真实条数算,它是防封的兜底,不跟"轮"妥协。
15
+ - **主动说话改短**:提示词明写"一句就够,短到一个词、一个感叹号、一个颜文字都行",并且"别每次都同一句"。
16
+ - 设置页新增「一句一句说」卡片:开关 + 一轮最多几条 + 每分钟上限。
3
17
  ## 0.8.1 — 2026-10-25
4
18
 
5
19
  - **新增表情库上限** `stickerLimit`(默认 200,设置页可改)。超了淘汰最旧的记录,原图文件不删——库要干净,硬盘不用替它心疼。
package/client.js CHANGED
@@ -110,7 +110,9 @@ window.__ModuleLoader__.load({
110
110
  qzoneMinMax: 状态?.qzoneMinMax ?? 240,
111
111
  qzonePrompt: 状态?.qzonePrompt ?? '',
112
112
  persona: 状态?.persona ?? '',
113
- stickerLimit: 状态?.stickerLimit ?? 200,
113
+ stickerLimit: 状态?.stickerLimit ?? 200,
114
+ maxPerRound: 状态?.maxPerRound ?? 4,
115
+ perMinute: 状态?.perMinute ?? 6,
114
116
  groupWhitelist: (状态?.groupWhitelist ?? []).join(' '),
115
117
  groupContextSize: 状态?.groupContextSize ?? 30,
116
118
  groupEveryMinMin: 状态?.groupEveryMinMin ?? 8,
@@ -142,7 +144,9 @@ window.__ModuleLoader__.load({
142
144
  qzoneMinMax: Number(值.qzoneMinMax),
143
145
  qzonePrompt: String(值.qzonePrompt ?? ''),
144
146
  persona: String(值.persona ?? ''),
145
- stickerLimit: Number(值.stickerLimit),
147
+ stickerLimit: Number(值.stickerLimit),
148
+ maxPerRound: Number(值.maxPerRound),
149
+ perMinute: Number(值.perMinute),
146
150
  groupWhitelist: String(值.groupWhitelist ?? ''),
147
151
  groupContextSize: Number(值.groupContextSize),
148
152
  groupEveryMinMin: Number(值.groupEveryMinMin),
@@ -202,7 +206,7 @@ window.__ModuleLoader__.load({
202
206
  'div',
203
207
  { style: 说明 },
204
208
  状态.online
205
- ? `今天发了 ${状态.sentToday} 条,这一小时 ${状态.sentThisHour} 条(上限 ${状态.perDay}/天、${状态.perHour}/时)`
209
+ ? `今天回了 ${状态.roundsToday ?? 0} 轮(${状态.sentToday ?? 0} 条),这一小时 ${状态.roundsThisHour ?? 0} 轮(上限 ${状态.perDay}/天、${状态.perHour}/时)`
206
210
  : '检查 NapCat 有没有启动、3000 端口通不通、token 对不对',
207
211
  ),
208
212
  ),
@@ -298,7 +302,7 @@ window.__ModuleLoader__.load({
298
302
  'div',
299
303
  null,
300
304
  h('div', { style: 标题 }, '每小时上限'),
301
- h('div', { style: 说明 }, '一小时内最多发几条(1–200),到顶直接拒发'),
305
+ h('div', { style: 说明 }, '一小时内最多回几轮(1–200),到顶直接拒发'),
302
306
  ),
303
307
  h('input', {
304
308
  type: 'number',
@@ -316,7 +320,7 @@ window.__ModuleLoader__.load({
316
320
  'div',
317
321
  null,
318
322
  h('div', { style: 标题 }, '每天上限'),
319
- h('div', { style: 说明 }, '一天内最多发几条(1–1000),按自然日算'),
323
+ h('div', { style: 说明 }, '一天内最多回几轮(1–1000),按自然日算'),
320
324
  ),
321
325
  h('input', {
322
326
  type: 'number',
@@ -473,6 +477,44 @@ window.__ModuleLoader__.load({
473
477
  ),
474
478
  h(开关, { 开: 状态.qzoneAuto, 点: () => 改({ qzoneAuto: !状态.qzoneAuto }) }),
475
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
+ ),
476
518
  h(
477
519
  'div',
478
520
  { style: { ...卡片 } },
package/index.js CHANGED
@@ -61,12 +61,19 @@ const 默认配置 = {
61
61
  qzonePrompt: '',
62
62
  qzoneEveryMinMin: 120,
63
63
  qzoneEveryMinMax: 240,
64
- // 发完消息要不要考虑配表情包
65
- stickerOnReply: true,
66
- // 表情库最多存几张(超了淘汰最旧的记录,不动原文件)
67
- stickerLimit: 200,
68
- // 收到图要不要鉴定一下、收进表情库
69
- autoTagStickers: true,
64
+ // 一轮回复拆成几条发(像人一句一句说)
65
+ splitReply: true,
66
+ maxMessagesPerRound: 4,
67
+ splitGapMinMs: 1000,
68
+ splitGapMaxMs: 3000,
69
+ // 技术限速:不管几轮,每分钟最多几条(防封兜底)
70
+ perMinute: 6,
71
+ // 发完消息要不要考虑配表情包
72
+ stickerOnReply: true,
73
+ // 表情库最多存几张(超了淘汰最旧的记录,不动原文件)
74
+ stickerLimit: 200,
75
+ // 收到图要不要鉴定一下、收进表情库
76
+ autoTagStickers: true,
70
77
  // 上限开关:true = 不限条数(有些场景确实需要)
71
78
  limitsUnlimited: false,
72
79
  // 人设:留空则用中性说明;填了就把这段当作"你是谁"交给会话
@@ -158,6 +165,7 @@ export function apply(ctx, 配置) {
158
165
  const c = { ...默认配置, ...(配置 ?? {}), ...读取本地配置() };
159
166
  const 账本 = join(家目录(), 'sent.log');
160
167
  const 收件箱 = join(家目录(), 'inbox.jsonl');
168
+ const 轮次账 = join(家目录(), 'rounds.log');
161
169
 
162
170
  ctx.logger?.info?.(`qq-moony: 接到 ${c.baseUrl}(限额 ${c.perHour}/时、${c.perDay}/天)`);
163
171
 
@@ -692,9 +700,9 @@ export function apply(ctx, 配置) {
692
700
  const 记忆 = 读记忆片段();
693
701
  记调试(`主动模式:给它一次说话的机会(下一次排在 ${Math.round(下次.分钟)} 分钟后)`);
694
702
  投递到会话(
695
- `【主动】现在没有人在找你。你可以主动给 ${目标} 发一句话,也可以什么都不发。\n` +
703
+ `【主动】现在没有人在找你。你可以主动给 ${目标} 发一句话——**一句就够**,短到一个词、一个感叹号、一个颜文字都行,也可以配一张表情包。\n` +
696
704
  `想发就用 qq_send;不想发就直接结束这一轮,不用解释。\n` +
697
- `别每次都发,也别发天气预报那种东西。${记忆 ? `\n\n${记忆}` : ''}`,
705
+ `别每次都同一句,也别发天气预报那种东西。${记忆 ? `\n\n${记忆}` : ''}`,
698
706
  );
699
707
  }, 60000);
700
708
 
@@ -790,14 +798,19 @@ export function apply(ctx, 配置) {
790
798
  groupActiveWindowMin: Number(c.groupActiveWindowMin ?? 5),
791
799
  groupCooldownMin: Number(c.groupCooldownMin ?? 10),
792
800
  groupPerDay: Number(c.groupPerDay ?? 20),
793
- limitsUnlimited: c.limitsUnlimited === true,
794
- stickerOnReply: c.stickerOnReply !== false,
795
- autoTagStickers: c.autoTagStickers !== false,
796
- stickerLimit: Number(c.stickerLimit ?? 200),
801
+ limitsUnlimited: c.limitsUnlimited === true,
802
+ stickerOnReply: c.stickerOnReply !== false,
803
+ autoTagStickers: c.autoTagStickers !== false,
804
+ splitReply: c.splitReply !== false,
805
+ maxPerRound: Number(c.maxMessagesPerRound ?? 4),
806
+ perMinute: Number(c.perMinute ?? 6),
807
+ stickerLimit: Number(c.stickerLimit ?? 200),
797
808
  stickerCount: 读表情库().length,
798
809
  perHour: Number(c.perHour ?? 20),
799
810
  perDay: Number(c.perDay ?? 60),
800
- sentToday: 记录.filter((t) => 现在 - t < 86400).length,
811
+ sentToday: 记录.filter((t) => 现在 - t < 86400).length,
812
+ roundsToday: 读账(轮次账).filter((t) => 现在 - t < 86400).length,
813
+ roundsThisHour: 读账(轮次账).filter((t) => 现在 - t < 3600).length,
801
814
  sentThisHour: 记录.filter((t) => 现在 - t < 3600).length,
802
815
  });
803
816
  return;
@@ -821,9 +834,12 @@ export function apply(ctx, 配置) {
821
834
  if (Number.isFinite(想要.everyMinMax)) patch.proactiveEveryMinMax = Math.max(5, Math.round(Number(想要.everyMinMax)));
822
835
  if (Number.isFinite(想要.perHour)) patch.perHour = Math.min(200, Math.max(1, Math.round(Number(想要.perHour))));
823
836
  if (Number.isFinite(想要.perDay)) patch.perDay = Math.min(1000, Math.max(1, Math.round(Number(想要.perDay))));
824
- if (typeof 想要.limitsUnlimited === 'boolean') patch.limitsUnlimited = 想要.limitsUnlimited;
825
- if (typeof 想要.stickerOnReply === 'boolean') patch.stickerOnReply = 想要.stickerOnReply;
826
- if (typeof 想要.autoTagStickers === 'boolean') patch.autoTagStickers = 想要.autoTagStickers;
837
+ if (typeof 想要.limitsUnlimited === 'boolean') patch.limitsUnlimited = 想要.limitsUnlimited;
838
+ if (typeof 想要.stickerOnReply === 'boolean') patch.stickerOnReply = 想要.stickerOnReply;
839
+ if (typeof 想要.autoTagStickers === 'boolean') patch.autoTagStickers = 想要.autoTagStickers;
840
+ if (typeof 想要.splitReply === 'boolean') patch.splitReply = 想要.splitReply;
841
+ if (Number.isFinite(想要.maxPerRound)) patch.maxMessagesPerRound = Math.min(8, Math.max(1, Math.round(Number(想要.maxPerRound))));
842
+ if (Number.isFinite(想要.perMinute)) patch.perMinute = Math.min(60, Math.max(1, Math.round(Number(想要.perMinute))));
827
843
  if (Number.isFinite(想要.stickerLimit)) patch.stickerLimit = Math.min(2000, Math.max(1, Math.round(Number(想要.stickerLimit))));
828
844
  if (typeof 想要.persona === 'string') patch.persona = 想要.persona.slice(0, 2000);
829
845
  if (typeof 想要.qzoneAuto === 'boolean') patch.qzoneAuto = 想要.qzoneAuto;
@@ -905,13 +921,50 @@ export function apply(ctx, 配置) {
905
921
  if (c.limitsUnlimited === true) return '';
906
922
  const 现在 = Date.now() / 1000;
907
923
  const 记录 = 读账(账本);
924
+ const 一分钟 = 记录.filter((t) => 现在 - t < 60).length;
925
+ const 上限分钟 = Math.max(1, Number(c.perMinute ?? 6));
926
+ if (一分钟 >= 上限分钟) return `这一分钟已经发了 ${一分钟} 条,喘口气再说`;
908
927
  const 一小时 = 记录.filter((t) => 现在 - t < 3600).length;
909
928
  const 一天 = 记录.filter((t) => 现在 - t < 86400).length;
910
- if (一小时 >= c.perHour) return `这一小时已经发了 ${一小时} 条,到顶了,等会儿再说`;
911
- if (一天 >= c.perDay) return `今天已经发了 ${一天} 条,到顶了,明天再聊`;
929
+ // 小时/天的额度一律按「轮」算;每分钟的硬上限永远按「条」算
930
+ const 轮记录 = 读账(轮次账);
931
+ const 轮小时 = 轮记录.filter((t) => 现在 - t < 3600).length;
932
+ const 轮一天 = 轮记录.filter((t) => 现在 - t < 86400).length;
933
+ if (轮小时 >= c.perHour) return `这一小时已经回了 ${轮小时} 轮,到顶了,等会儿再说`;
934
+ if (轮一天 >= c.perDay) return `今天已经回了 ${轮一天} 轮,到顶了,明天再聊`;
912
935
  return '';
913
936
  }
914
937
 
938
+ /** 把一段话拆成该分开发的小句:标点断句、太短的并进上一句、最多 N 条。 */
939
+ function 拆成小段(整段) {
940
+ const 原文 = String(整段 ?? '').trim();
941
+ if (!原文) return [];
942
+ if (c.splitReply === false) return [原文];
943
+ const 最多 = Math.max(1, Math.min(8, Number(c.maxMessagesPerRound ?? 4)));
944
+ // 按句末标点或换行断
945
+ const 粗切 = 原文
946
+ .split(/(?<=[。!?!?…~\n])/u)
947
+ .map((x) => x.trim())
948
+ .filter(Boolean);
949
+ const 结果 = [];
950
+ for (const 句 of 粗切) {
951
+ // 太短的("哦。""!")并进上一句,除非上一句已经够长
952
+ if (结果.length > 0 && 句.length <= 2 && 结果[结果.length - 1].length < 30) {
953
+ 结果[结果.length - 1] += 句;
954
+ } else {
955
+ 结果.push(句);
956
+ }
957
+ }
958
+ // 超上限:把多出来的并到最后一条
959
+ if (结果.length > 最多) {
960
+ const 头 = 结果.slice(0, 最多 - 1);
961
+ const 尾 = 结果.slice(最多 - 1).join('');
962
+ 头.push(尾);
963
+ return 头;
964
+ }
965
+ return 结果;
966
+ }
967
+
915
968
  const 睡眠 = (ms) => new Promise((r) => setTimeout(r, ms));
916
969
 
917
970
  // ---------- qq_status ----------
@@ -939,7 +992,9 @@ export function apply(ctx, 配置) {
939
992
  ok: true,
940
993
  uin: Number(r.data?.user_id ?? 0),
941
994
  nickname: String(r.data?.nickname ?? ''),
942
- sentToday: 记录.filter((t) => 现在 - t < 86400).length,
995
+ sentToday: 记录.filter((t) => 现在 - t < 86400).length,
996
+ roundsToday: 读账(轮次账).filter((t) => 现在 - t < 86400).length,
997
+ roundsThisHour: 读账(轮次账).filter((t) => 现在 - t < 3600).length,
943
998
  sentThisHour: 记录.filter((t) => 现在 - t < 3600).length,
944
999
  };
945
1000
  } catch (e) {
@@ -982,22 +1037,42 @@ export function apply(ctx, 配置) {
982
1037
  if (!Number.isFinite(号) || 号 <= 0) return { ok: false, messageId: 0, reason: '目标不是有效号码' };
983
1038
 
984
1039
  const 消息 = [];
985
- if (正文) 消息.push({ type: 'text', data: { text: 正文 } });
1040
+ const 段们 = 正文 ? 拆成小段(正文) : [];
1041
+ if (段们.length === 0 && 正文) 段们.push(正文);
986
1042
  if (图) {
987
1043
  if (!existsSync(图)) return { ok: false, messageId: 0, reason: `找不到这张图:${图}` };
988
- 消息.push({ type: 'image', data: { file: 图.startsWith('file://') ? 图 : `file:///${图.replace(/\\/g, '/')}` } });
989
1044
  }
990
1045
 
991
- const 等待 = c.minDelayMs + Math.random() * Math.max(0, c.maxDelayMs - c.minDelayMs);
992
- await 睡眠(等待);
1046
+ const 重试等待 = () => c.minDelayMs + Math.random() * Math.max(0, c.maxDelayMs - c.minDelayMs);
1047
+ const 段间隔 = () =>
1048
+ Math.max(0, Number(c.splitGapMinMs ?? 1000)) +
1049
+ Math.random() * Math.max(0, Number(c.splitGapMaxMs ?? 3000) - Number(c.splitGapMinMs ?? 1000));
993
1050
 
1051
+ await 睡眠(重试等待());
1052
+ let 最后id = 0;
994
1053
  try {
995
- const r = await 调(是群 ? 'send_group_msg' : 'send_private_msg', {
996
- [是群 ? 'group_id' : 'user_id']: 号,
997
- message: 消息,
998
- });
999
- 记账(账本, 号, 正文 || `[图片]${图}`);
1000
- return { ok: true, messageId: Number(r.data?.message_id ?? 0) };
1054
+ // 一句一句发,像人说话
1055
+ for (let i = 0; i < 段们.length; i += 1) {
1056
+ if (i > 0) await 睡眠(段间隔());
1057
+ const r = await 调(是群 ? 'send_group_msg' : 'send_private_msg', {
1058
+ [是群 ? 'group_id' : 'user_id']: 号,
1059
+ message: [{ type: 'text', data: { text: 段们[i] } }],
1060
+ });
1061
+ 最后id = Number(r.data?.message_id ?? 0);
1062
+ 记账(账本, 号, 段们[i]);
1063
+ }
1064
+ if (图) {
1065
+ if (段们.length > 0) await 睡眠(段间隔());
1066
+ const r = await 调(是群 ? 'send_group_msg' : 'send_private_msg', {
1067
+ [是群 ? 'group_id' : 'user_id']: 号,
1068
+ message: [{ type: 'image', data: { file: 图.startsWith('file://') ? 图 : `file:///${图.replace(/\\/g, '/')}` } }],
1069
+ });
1070
+ 最后id = Number(r.data?.message_id ?? 0);
1071
+ 记账(账本, 号, `[图片]${图}`);
1072
+ }
1073
+ // 一轮只记一份轮次额度
1074
+ 记账(轮次账, 号, 段们.join('') || '[图片]');
1075
+ return { ok: true, messageId: 最后id, parts: 段们.length + (图 ? 1 : 0) };
1001
1076
  } catch (e) {
1002
1077
  return { ok: false, messageId: 0, reason: e.message };
1003
1078
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-qq-moony",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "把 QQ 小号接进 DeepSeek Harness:收发消息、发表情包、发 QQ 空间说说、参与群聊,能自己找话题,自带启动与限速防封。",
5
5
  "keywords": [
6
6
  "dsh",