dsh-qq-moony 0.8.0 → 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 +12 -0
- package/client.js +64 -2
- package/index.js +106 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
- 设置页新增「一句一句说」卡片:开关 + 一轮最多几条 + 每分钟上限。
|
|
12
|
+
## 0.8.1 — 2026-10-25
|
|
13
|
+
|
|
14
|
+
- **新增表情库上限** `stickerLimit`(默认 200,设置页可改)。超了淘汰最旧的记录,原图文件不删——库要干净,硬盘不用替它心疼。
|
|
3
15
|
## 0.8.0 — 2026-10-25
|
|
4
16
|
|
|
5
17
|
表情包会自己攒、自己配了;上限多了一个"不限"。
|
package/client.js
CHANGED
|
@@ -109,7 +109,10 @@ window.__ModuleLoader__.load({
|
|
|
109
109
|
qzoneMinMin: 状态?.qzoneMinMin ?? 120,
|
|
110
110
|
qzoneMinMax: 状态?.qzoneMinMax ?? 240,
|
|
111
111
|
qzonePrompt: 状态?.qzonePrompt ?? '',
|
|
112
|
-
persona: 状态?.persona ?? '',
|
|
112
|
+
persona: 状态?.persona ?? '',
|
|
113
|
+
stickerLimit: 状态?.stickerLimit ?? 200,
|
|
114
|
+
maxPerRound: 状态?.maxPerRound ?? 4,
|
|
115
|
+
perMinute: 状态?.perMinute ?? 6,
|
|
113
116
|
groupWhitelist: (状态?.groupWhitelist ?? []).join(' '),
|
|
114
117
|
groupContextSize: 状态?.groupContextSize ?? 30,
|
|
115
118
|
groupEveryMinMin: 状态?.groupEveryMinMin ?? 8,
|
|
@@ -140,7 +143,10 @@ window.__ModuleLoader__.load({
|
|
|
140
143
|
qzoneMinMin: Number(值.qzoneMinMin),
|
|
141
144
|
qzoneMinMax: Number(值.qzoneMinMax),
|
|
142
145
|
qzonePrompt: String(值.qzonePrompt ?? ''),
|
|
143
|
-
persona: String(值.persona ?? ''),
|
|
146
|
+
persona: String(值.persona ?? ''),
|
|
147
|
+
stickerLimit: Number(值.stickerLimit),
|
|
148
|
+
maxPerRound: Number(值.maxPerRound),
|
|
149
|
+
perMinute: Number(值.perMinute),
|
|
144
150
|
groupWhitelist: String(值.groupWhitelist ?? ''),
|
|
145
151
|
groupContextSize: Number(值.groupContextSize),
|
|
146
152
|
groupEveryMinMin: Number(值.groupEveryMinMin),
|
|
@@ -471,6 +477,44 @@ window.__ModuleLoader__.load({
|
|
|
471
477
|
),
|
|
472
478
|
h(开关, { 开: 状态.qzoneAuto, 点: () => 改({ qzoneAuto: !状态.qzoneAuto }) }),
|
|
473
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
|
+
),
|
|
474
518
|
h(
|
|
475
519
|
'div',
|
|
476
520
|
{ style: { ...卡片 } },
|
|
@@ -504,6 +548,24 @@ window.__ModuleLoader__.load({
|
|
|
504
548
|
),
|
|
505
549
|
h(开关, { 开: 状态.autoTagStickers, 点: () => 改({ autoTagStickers: !状态.autoTagStickers }) }),
|
|
506
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
|
+
),
|
|
507
569
|
h(
|
|
508
570
|
'div',
|
|
509
571
|
{ style: { display: 'flex', justifyContent: 'flex-end', gap: '10px', alignItems: 'center' } },
|
package/index.js
CHANGED
|
@@ -61,10 +61,19 @@ const 默认配置 = {
|
|
|
61
61
|
qzonePrompt: '',
|
|
62
62
|
qzoneEveryMinMin: 120,
|
|
63
63
|
qzoneEveryMinMax: 240,
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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,
|
|
68
77
|
// 上限开关:true = 不限条数(有些场景确实需要)
|
|
69
78
|
limitsUnlimited: false,
|
|
70
79
|
// 人设:留空则用中性说明;填了就把这段当作"你是谁"交给会话
|
|
@@ -156,6 +165,7 @@ export function apply(ctx, 配置) {
|
|
|
156
165
|
const c = { ...默认配置, ...(配置 ?? {}), ...读取本地配置() };
|
|
157
166
|
const 账本 = join(家目录(), 'sent.log');
|
|
158
167
|
const 收件箱 = join(家目录(), 'inbox.jsonl');
|
|
168
|
+
const 轮次账 = join(家目录(), 'rounds.log');
|
|
159
169
|
|
|
160
170
|
ctx.logger?.info?.(`qq-moony: 接到 ${c.baseUrl}(限额 ${c.perHour}/时、${c.perDay}/天)`);
|
|
161
171
|
|
|
@@ -690,9 +700,9 @@ export function apply(ctx, 配置) {
|
|
|
690
700
|
const 记忆 = 读记忆片段();
|
|
691
701
|
记调试(`主动模式:给它一次说话的机会(下一次排在 ${Math.round(下次.分钟)} 分钟后)`);
|
|
692
702
|
投递到会话(
|
|
693
|
-
`【主动】现在没有人在找你。你可以主动给 ${目标}
|
|
703
|
+
`【主动】现在没有人在找你。你可以主动给 ${目标} 发一句话——**一句就够**,短到一个词、一个感叹号、一个颜文字都行,也可以配一张表情包。\n` +
|
|
694
704
|
`想发就用 qq_send;不想发就直接结束这一轮,不用解释。\n` +
|
|
695
|
-
|
|
705
|
+
`别每次都同一句,也别发天气预报那种东西。${记忆 ? `\n\n${记忆}` : ''}`,
|
|
696
706
|
);
|
|
697
707
|
}, 60000);
|
|
698
708
|
|
|
@@ -788,9 +798,14 @@ export function apply(ctx, 配置) {
|
|
|
788
798
|
groupActiveWindowMin: Number(c.groupActiveWindowMin ?? 5),
|
|
789
799
|
groupCooldownMin: Number(c.groupCooldownMin ?? 10),
|
|
790
800
|
groupPerDay: Number(c.groupPerDay ?? 20),
|
|
791
|
-
limitsUnlimited: c.limitsUnlimited === true,
|
|
792
|
-
stickerOnReply: c.stickerOnReply !== false,
|
|
801
|
+
limitsUnlimited: c.limitsUnlimited === true,
|
|
802
|
+
stickerOnReply: c.stickerOnReply !== false,
|
|
793
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),
|
|
808
|
+
stickerCount: 读表情库().length,
|
|
794
809
|
perHour: Number(c.perHour ?? 20),
|
|
795
810
|
perDay: Number(c.perDay ?? 60),
|
|
796
811
|
sentToday: 记录.filter((t) => 现在 - t < 86400).length,
|
|
@@ -817,9 +832,13 @@ export function apply(ctx, 配置) {
|
|
|
817
832
|
if (Number.isFinite(想要.everyMinMax)) patch.proactiveEveryMinMax = Math.max(5, Math.round(Number(想要.everyMinMax)));
|
|
818
833
|
if (Number.isFinite(想要.perHour)) patch.perHour = Math.min(200, Math.max(1, Math.round(Number(想要.perHour))));
|
|
819
834
|
if (Number.isFinite(想要.perDay)) patch.perDay = Math.min(1000, Math.max(1, Math.round(Number(想要.perDay))));
|
|
820
|
-
if (typeof 想要.limitsUnlimited === 'boolean') patch.limitsUnlimited = 想要.limitsUnlimited;
|
|
821
|
-
if (typeof 想要.stickerOnReply === 'boolean') patch.stickerOnReply = 想要.stickerOnReply;
|
|
835
|
+
if (typeof 想要.limitsUnlimited === 'boolean') patch.limitsUnlimited = 想要.limitsUnlimited;
|
|
836
|
+
if (typeof 想要.stickerOnReply === 'boolean') patch.stickerOnReply = 想要.stickerOnReply;
|
|
822
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))));
|
|
841
|
+
if (Number.isFinite(想要.stickerLimit)) patch.stickerLimit = Math.min(2000, Math.max(1, Math.round(Number(想要.stickerLimit))));
|
|
823
842
|
if (typeof 想要.persona === 'string') patch.persona = 想要.persona.slice(0, 2000);
|
|
824
843
|
if (typeof 想要.qzoneAuto === 'boolean') patch.qzoneAuto = 想要.qzoneAuto;
|
|
825
844
|
if (typeof 想要.qzonePrompt === 'string') patch.qzonePrompt = 想要.qzonePrompt.slice(0, 2000);
|
|
@@ -900,13 +919,55 @@ export function apply(ctx, 配置) {
|
|
|
900
919
|
if (c.limitsUnlimited === true) return '';
|
|
901
920
|
const 现在 = Date.now() / 1000;
|
|
902
921
|
const 记录 = 读账(账本);
|
|
922
|
+
const 一分钟 = 记录.filter((t) => 现在 - t < 60).length;
|
|
923
|
+
const 上限分钟 = Math.max(1, Number(c.perMinute ?? 6));
|
|
924
|
+
if (一分钟 >= 上限分钟) return `这一分钟已经发了 ${一分钟} 条,喘口气再说`;
|
|
903
925
|
const 一小时 = 记录.filter((t) => 现在 - t < 3600).length;
|
|
904
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
|
+
}
|
|
905
936
|
if (一小时 >= c.perHour) return `这一小时已经发了 ${一小时} 条,到顶了,等会儿再说`;
|
|
906
937
|
if (一天 >= c.perDay) return `今天已经发了 ${一天} 条,到顶了,明天再聊`;
|
|
907
938
|
return '';
|
|
908
939
|
}
|
|
909
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
|
+
|
|
910
971
|
const 睡眠 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
911
972
|
|
|
912
973
|
// ---------- qq_status ----------
|
|
@@ -977,22 +1038,42 @@ export function apply(ctx, 配置) {
|
|
|
977
1038
|
if (!Number.isFinite(号) || 号 <= 0) return { ok: false, messageId: 0, reason: '目标不是有效号码' };
|
|
978
1039
|
|
|
979
1040
|
const 消息 = [];
|
|
980
|
-
|
|
1041
|
+
const 段们 = 正文 ? 拆成小段(正文) : [];
|
|
1042
|
+
if (段们.length === 0 && 正文) 段们.push(正文);
|
|
981
1043
|
if (图) {
|
|
982
1044
|
if (!existsSync(图)) return { ok: false, messageId: 0, reason: `找不到这张图:${图}` };
|
|
983
|
-
消息.push({ type: 'image', data: { file: 图.startsWith('file://') ? 图 : `file:///${图.replace(/\\/g, '/')}` } });
|
|
984
1045
|
}
|
|
985
1046
|
|
|
986
|
-
const
|
|
987
|
-
|
|
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));
|
|
988
1051
|
|
|
1052
|
+
await 睡眠(重试等待());
|
|
1053
|
+
let 最后id = 0;
|
|
989
1054
|
try {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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) };
|
|
996
1077
|
} catch (e) {
|
|
997
1078
|
return { ok: false, messageId: 0, reason: e.message };
|
|
998
1079
|
}
|
|
@@ -1118,6 +1199,11 @@ export function apply(ctx, 配置) {
|
|
|
1118
1199
|
const 指纹 = 文件指纹(路径);
|
|
1119
1200
|
const 库 = 读表情库();
|
|
1120
1201
|
if (指纹 && 库.some((x) => x.hash === 指纹)) return { ok: true, added: false };
|
|
1202
|
+
const 上限 = Math.max(1, Number(c.stickerLimit ?? 200));
|
|
1203
|
+
while (库.length >= 上限) {
|
|
1204
|
+
const 淘汰 = 库.shift();
|
|
1205
|
+
记调试(`表情库满(${上限}),淘汰最旧的:${淘汰?.path ?? '?'}`);
|
|
1206
|
+
}
|
|
1121
1207
|
const 条目 = {
|
|
1122
1208
|
path: 路径,
|
|
1123
1209
|
hash: 指纹,
|