dsh-qq-moony 0.8.1 → 0.9.0

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,14 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.9.0 — 2026-10-25
4
+
5
+ 一句一句地说,额度按"轮"算。
6
+
7
+ - **拆条发送**:`qq_send` 收到一段话会按句末标点拆开、逐条发,中间隔 1–3 秒。太短的("哦。""!")会并进上一句;一轮最多 `maxMessagesPerRound` 条(默认 4),多的并到最后一条。
8
+ - **额度改按轮计**:`perHour` / `perDay` 现在数的是"轮",一轮只占一份——人本来也不是按条说话的。
9
+ - **技术限速不松**:`perMinute`(默认 6)永远按真实条数算,它是防封的兜底,不跟"轮"妥协。
10
+ - **主动说话改短**:提示词明写"一句就够,短到一个词、一个感叹号、一个颜文字都行",并且"别每次都同一句"。
11
+ - 设置页新增「一句一句说」卡片:开关 + 一轮最多几条 + 每分钟上限。
3
12
  ## 0.8.1 — 2026-10-25
4
13
 
5
14
  - **新增表情库上限** `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),
@@ -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,10 +798,13 @@ 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),
@@ -821,9 +832,12 @@ export function apply(ctx, 配置) {
821
832
  if (Number.isFinite(想要.everyMinMax)) patch.proactiveEveryMinMax = Math.max(5, Math.round(Number(想要.everyMinMax)));
822
833
  if (Number.isFinite(想要.perHour)) patch.perHour = Math.min(200, Math.max(1, Math.round(Number(想要.perHour))));
823
834
  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;
835
+ if (typeof 想要.limitsUnlimited === 'boolean') patch.limitsUnlimited = 想要.limitsUnlimited;
836
+ if (typeof 想要.stickerOnReply === 'boolean') patch.stickerOnReply = 想要.stickerOnReply;
837
+ if (typeof 想要.autoTagStickers === 'boolean') patch.autoTagStickers = 想要.autoTagStickers;
838
+ if (typeof 想要.splitReply === 'boolean') patch.splitReply = 想要.splitReply;
839
+ if (Number.isFinite(想要.maxPerRound)) patch.maxMessagesPerRound = Math.min(8, Math.max(1, Math.round(Number(想要.maxPerRound))));
840
+ if (Number.isFinite(想要.perMinute)) patch.perMinute = Math.min(60, Math.max(1, Math.round(Number(想要.perMinute))));
827
841
  if (Number.isFinite(想要.stickerLimit)) patch.stickerLimit = Math.min(2000, Math.max(1, Math.round(Number(想要.stickerLimit))));
828
842
  if (typeof 想要.persona === 'string') patch.persona = 想要.persona.slice(0, 2000);
829
843
  if (typeof 想要.qzoneAuto === 'boolean') patch.qzoneAuto = 想要.qzoneAuto;
@@ -905,13 +919,55 @@ export function apply(ctx, 配置) {
905
919
  if (c.limitsUnlimited === true) return '';
906
920
  const 现在 = Date.now() / 1000;
907
921
  const 记录 = 读账(账本);
922
+ const 一分钟 = 记录.filter((t) => 现在 - t < 60).length;
923
+ const 上限分钟 = Math.max(1, Number(c.perMinute ?? 6));
924
+ if (一分钟 >= 上限分钟) return `这一分钟已经发了 ${一分钟} 条,喘口气再说`;
908
925
  const 一小时 = 记录.filter((t) => 现在 - t < 3600).length;
909
926
  const 一天 = 记录.filter((t) => 现在 - t < 86400).length;
927
+ // 小时/天的额度按「轮」算(拆条模式下),分钟的硬上限永远按「条」算
928
+ const 轮记录 = 读账(轮次账);
929
+ const 轮小时 = 轮记录.filter((t) => 现在 - t < 3600).length;
930
+ const 轮一天 = 轮记录.filter((t) => 现在 - t < 86400).length;
931
+ if (c.splitReply !== false) {
932
+ if (轮小时 >= c.perHour) return `这一小时已经回了 ${轮小时} 轮,到顶了,等会儿再说`;
933
+ if (轮一天 >= c.perDay) return `今天已经回了 ${轮一天} 轮,到顶了,明天再聊`;
934
+ return '';
935
+ }
910
936
  if (一小时 >= c.perHour) return `这一小时已经发了 ${一小时} 条,到顶了,等会儿再说`;
911
937
  if (一天 >= c.perDay) return `今天已经发了 ${一天} 条,到顶了,明天再聊`;
912
938
  return '';
913
939
  }
914
940
 
941
+ /** 把一段话拆成该分开发的小句:标点断句、太短的并进上一句、最多 N 条。 */
942
+ function 拆成小段(整段) {
943
+ const 原文 = String(整段 ?? '').trim();
944
+ if (!原文) return [];
945
+ if (c.splitReply === false) return [原文];
946
+ const 最多 = Math.max(1, Math.min(8, Number(c.maxMessagesPerRound ?? 4)));
947
+ // 按句末标点或换行断
948
+ const 粗切 = 原文
949
+ .split(/(?<=[。!?!?…~\n])/u)
950
+ .map((x) => x.trim())
951
+ .filter(Boolean);
952
+ const 结果 = [];
953
+ for (const 句 of 粗切) {
954
+ // 太短的("哦。""!")并进上一句,除非上一句已经够长
955
+ if (结果.length > 0 && 句.length <= 2 && 结果[结果.length - 1].length < 30) {
956
+ 结果[结果.length - 1] += 句;
957
+ } else {
958
+ 结果.push(句);
959
+ }
960
+ }
961
+ // 超上限:把多出来的并到最后一条
962
+ if (结果.length > 最多) {
963
+ const 头 = 结果.slice(0, 最多 - 1);
964
+ const 尾 = 结果.slice(最多 - 1).join('');
965
+ 头.push(尾);
966
+ return 头;
967
+ }
968
+ return 结果;
969
+ }
970
+
915
971
  const 睡眠 = (ms) => new Promise((r) => setTimeout(r, ms));
916
972
 
917
973
  // ---------- qq_status ----------
@@ -982,22 +1038,42 @@ export function apply(ctx, 配置) {
982
1038
  if (!Number.isFinite(号) || 号 <= 0) return { ok: false, messageId: 0, reason: '目标不是有效号码' };
983
1039
 
984
1040
  const 消息 = [];
985
- if (正文) 消息.push({ type: 'text', data: { text: 正文 } });
1041
+ const 段们 = 正文 ? 拆成小段(正文) : [];
1042
+ if (段们.length === 0 && 正文) 段们.push(正文);
986
1043
  if (图) {
987
1044
  if (!existsSync(图)) return { ok: false, messageId: 0, reason: `找不到这张图:${图}` };
988
- 消息.push({ type: 'image', data: { file: 图.startsWith('file://') ? 图 : `file:///${图.replace(/\\/g, '/')}` } });
989
1045
  }
990
1046
 
991
- const 等待 = c.minDelayMs + Math.random() * Math.max(0, c.maxDelayMs - c.minDelayMs);
992
- await 睡眠(等待);
1047
+ const 重试等待 = () => c.minDelayMs + Math.random() * Math.max(0, c.maxDelayMs - c.minDelayMs);
1048
+ const 段间隔 = () =>
1049
+ Math.max(0, Number(c.splitGapMinMs ?? 1000)) +
1050
+ Math.random() * Math.max(0, Number(c.splitGapMaxMs ?? 3000) - Number(c.splitGapMinMs ?? 1000));
993
1051
 
1052
+ await 睡眠(重试等待());
1053
+ let 最后id = 0;
994
1054
  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) };
1055
+ // 一句一句发,像人说话
1056
+ for (let i = 0; i < 段们.length; i += 1) {
1057
+ if (i > 0) await 睡眠(段间隔());
1058
+ const r = await 调(是群 ? 'send_group_msg' : 'send_private_msg', {
1059
+ [是群 ? 'group_id' : 'user_id']: 号,
1060
+ message: [{ type: 'text', data: { text: 段们[i] } }],
1061
+ });
1062
+ 最后id = Number(r.data?.message_id ?? 0);
1063
+ 记账(账本, 号, 段们[i]);
1064
+ }
1065
+ if (图) {
1066
+ if (段们.length > 0) await 睡眠(段间隔());
1067
+ const r = await 调(是群 ? 'send_group_msg' : 'send_private_msg', {
1068
+ [是群 ? 'group_id' : 'user_id']: 号,
1069
+ message: [{ type: 'image', data: { file: 图.startsWith('file://') ? 图 : `file:///${图.replace(/\\/g, '/')}` } }],
1070
+ });
1071
+ 最后id = Number(r.data?.message_id ?? 0);
1072
+ 记账(账本, 号, `[图片]${图}`);
1073
+ }
1074
+ // 一轮只记一份轮次额度
1075
+ 记账(轮次账, 号, 段们.join('') || '[图片]');
1076
+ return { ok: true, messageId: 最后id, parts: 段们.length + (图 ? 1 : 0) };
1001
1077
  } catch (e) {
1002
1078
  return { ok: false, messageId: 0, reason: e.message };
1003
1079
  }
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.0",
4
4
  "description": "把 QQ 小号接进 DeepSeek Harness:收发消息、发表情包、发 QQ 空间说说、参与群聊,能自己找话题,自带启动与限速防封。",
5
5
  "keywords": [
6
6
  "dsh",