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 +9 -0
- package/client.js +44 -2
- package/index.js +101 -25
- package/package.json +1 -1
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
`【主动】现在没有人在找你。你可以主动给 ${目标}
|
|
703
|
+
`【主动】现在没有人在找你。你可以主动给 ${目标} 发一句话——**一句就够**,短到一个词、一个感叹号、一个颜文字都行,也可以配一张表情包。\n` +
|
|
696
704
|
`想发就用 qq_send;不想发就直接结束这一轮,不用解释。\n` +
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
992
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
}
|