dsh-qq-moony 0.7.0 → 0.8.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 +11 -0
- package/client.js +33 -0
- package/index.js +149 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
表情包会自己攒、自己配了;上限多了一个"不限"。
|
|
6
|
+
|
|
7
|
+
- **新增 `qq_tag_sticker` / `qq_pick_sticker`**:收到图会先看一眼,值得收才收进库,打两个标签(mood 情绪 / use 用途 / note 一句描述);回话时按情绪用途挑一张,挑不到就不配。
|
|
8
|
+
- **去重**:按文件 sha1 指纹判断,同一张图转十遍只存一份。
|
|
9
|
+
- **配表情包开关**:`stickerOnReply`(默认开)。关掉就只发字。
|
|
10
|
+
- **自动收录开关**:`autoTagStickers`(默认开)。关掉就不看图、不花那份 token。
|
|
11
|
+
- **不限条数开关**:`limitsUnlimited`(默认关)。开了就绕过每小时/每天上限——给"确实需要"的人留的口子,页面上写着警告。
|
|
12
|
+
|
|
13
|
+
**成本提醒**:自动收录要"看一张图",每条带图的来信都会多一次读图;配表情包也要多一轮工具调用。嫌贵就把这两个开关关掉。
|
|
3
14
|
## 0.7.0 — 2026-10-25
|
|
4
15
|
|
|
5
16
|
把"我自己的东西"从包里拿掉——别人装的是插件,不是我。
|
package/client.js
CHANGED
|
@@ -471,6 +471,39 @@ window.__ModuleLoader__.load({
|
|
|
471
471
|
),
|
|
472
472
|
h(开关, { 开: 状态.qzoneAuto, 点: () => 改({ qzoneAuto: !状态.qzoneAuto }) }),
|
|
473
473
|
),
|
|
474
|
+
h(
|
|
475
|
+
'div',
|
|
476
|
+
{ style: { ...卡片 } },
|
|
477
|
+
h(
|
|
478
|
+
'div',
|
|
479
|
+
null,
|
|
480
|
+
h('div', { style: 标题 }, '不限条数'),
|
|
481
|
+
h('div', { style: 说明 }, '关掉每小时/每天的上限。⚠️ 只在确实需要时开——额度是防封的第一道闸'),
|
|
482
|
+
),
|
|
483
|
+
h(开关, { 开: 状态.limitsUnlimited, 点: () => 改({ limitsUnlimited: !状态.limitsUnlimited }) }),
|
|
484
|
+
),
|
|
485
|
+
h(
|
|
486
|
+
'div',
|
|
487
|
+
{ style: { ...卡片 } },
|
|
488
|
+
h(
|
|
489
|
+
'div',
|
|
490
|
+
null,
|
|
491
|
+
h('div', { style: 标题 }, '配表情包'),
|
|
492
|
+
h('div', { style: 说明 }, '回话时如果合适,自己从库里挑一张一起发'),
|
|
493
|
+
),
|
|
494
|
+
h(开关, { 开: 状态.stickerOnReply, 点: () => 改({ stickerOnReply: !状态.stickerOnReply }) }),
|
|
495
|
+
),
|
|
496
|
+
h(
|
|
497
|
+
'div',
|
|
498
|
+
{ style: { ...卡片 } },
|
|
499
|
+
h(
|
|
500
|
+
'div',
|
|
501
|
+
null,
|
|
502
|
+
h('div', { style: 标题 }, '自动收录表情包'),
|
|
503
|
+
h('div', { style: 说明 }, '收到图会看一眼,值得收才收(重复、糊的、真人照片不收)'),
|
|
504
|
+
),
|
|
505
|
+
h(开关, { 开: 状态.autoTagStickers, 点: () => 改({ autoTagStickers: !状态.autoTagStickers }) }),
|
|
506
|
+
),
|
|
474
507
|
h(
|
|
475
508
|
'div',
|
|
476
509
|
{ style: { display: 'flex', justifyContent: 'flex-end', gap: '10px', alignItems: 'center' } },
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* 本插件只当“手”,不当“腿”。
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
13
14
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
14
15
|
import { homedir } from 'node:os';
|
|
15
16
|
import { join } from 'node:path';
|
|
@@ -60,6 +61,12 @@ const 默认配置 = {
|
|
|
60
61
|
qzonePrompt: '',
|
|
61
62
|
qzoneEveryMinMin: 120,
|
|
62
63
|
qzoneEveryMinMax: 240,
|
|
64
|
+
// 发完消息要不要考虑配表情包
|
|
65
|
+
stickerOnReply: true,
|
|
66
|
+
// 收到图要不要鉴定一下、收进表情库
|
|
67
|
+
autoTagStickers: true,
|
|
68
|
+
// 上限开关:true = 不限条数(有些场景确实需要)
|
|
69
|
+
limitsUnlimited: false,
|
|
63
70
|
// 人设:留空则用中性说明;填了就把这段当作"你是谁"交给会话
|
|
64
71
|
persona: '',
|
|
65
72
|
// DSH 启动时自动把 NapCat 拉起来(免扫码,用 -q 快速登录)
|
|
@@ -103,6 +110,36 @@ function 记账(账本, 目标, 摘要) {
|
|
|
103
110
|
appendFileSync(账本, `${Date.now() / 1000}\t${目标}\t${String(摘要).slice(0, 60).replace(/\s+/g, ' ')}\n`, 'utf8');
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
/** 表情包库:<home>/stickers.json,按标签存,按标签取。 */
|
|
114
|
+
function 表情库文件() {
|
|
115
|
+
return join(家目录(), 'stickers.json');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function 读表情库() {
|
|
119
|
+
try {
|
|
120
|
+
const 文件 = 表情库文件();
|
|
121
|
+
if (!existsSync(文件)) return [];
|
|
122
|
+
const 体 = JSON.parse(readFileSync(文件, 'utf8').replace(/^\uFEFF/, ''));
|
|
123
|
+
return Array.isArray(体) ? 体 : [];
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function 写表情库(列表) {
|
|
130
|
+
mkdirSync(家目录(), { recursive: true });
|
|
131
|
+
writeFileSync(表情库文件(), JSON.stringify(列表, null, 2), 'utf8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function 文件指纹(路径) {
|
|
135
|
+
try {
|
|
136
|
+
const 数据 = readFileSync(路径);
|
|
137
|
+
return createHash('sha1').update(数据).digest('hex').slice(0, 16);
|
|
138
|
+
} catch {
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
const 工具输出 = (字段, 渲染) => ({
|
|
107
144
|
schema: {
|
|
108
145
|
type: 'object',
|
|
@@ -291,6 +328,19 @@ export function apply(ctx, 配置) {
|
|
|
291
328
|
return `${人设}\n(同一个工作区、同一份记忆,memory/ 下的文件随时可看。)`;
|
|
292
329
|
}
|
|
293
330
|
|
|
331
|
+
/** 收到图之后:值不值得收进表情库,让它自己判断。 */
|
|
332
|
+
function 鉴定表情包(记录) {
|
|
333
|
+
if (c.autoTagStickers === false) return;
|
|
334
|
+
const 图 = (记录.attachments ?? []).filter((a) => a.path).map((a) => a.path);
|
|
335
|
+
if (图.length === 0) return;
|
|
336
|
+
const 清单 = 图.map((p) => `- ${p}`).join('\n');
|
|
337
|
+
投递到会话(
|
|
338
|
+
`【表情包鉴定】${记录.name} 刚发来 ${图.length} 张图:\n${清单}\n\n` +
|
|
339
|
+
`用 read_image 看一眼。**值得收**就用 qq_tag_sticker 打两个标签(mood 一个词:笑/无语/惊讶/得意…;use 一个词:接梗/收尾/耍赖/安慰…),note 写一句"这图长什么样",方便以后匹配。\n` +
|
|
340
|
+
`不值得(重复的、糊的、没意思的、涉及真人的照片)就什么都不做,直接结束。别为了凑数收。`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
294
344
|
/** 把来信投给 QQ 专用会话,让那边的 agent 去回。失败只记日志,不影响收消息。 */
|
|
295
345
|
function 自动回复(记录) {
|
|
296
346
|
记调试(
|
|
@@ -331,7 +381,10 @@ export function apply(ctx, 配置) {
|
|
|
331
381
|
const 提示 =
|
|
332
382
|
`【QQ ${是群 ? `群 ${记录.group} 里被点名` : '来信'}】${记录.name}(${记录.from})在 ${记录.t} 说:${描述}\n\n` +
|
|
333
383
|
`${身份说明()}\n` +
|
|
334
|
-
`用 qq_send 回给 ${回哪里}${是群 ? '(群里只回一句,别接着聊)' : ',一句就够'}
|
|
384
|
+
`用 qq_send 回给 ${回哪里}${是群 ? '(群里只回一句,别接着聊)' : ',一句就够'},短,像平时说话那样,别解释你在做什么。回完就停。` +
|
|
385
|
+
(c.stickerOnReply === false
|
|
386
|
+
? ''
|
|
387
|
+
: `\n如果这句适合配一张图,可以先用 qq_pick_sticker 按情绪/用途挑一张;挑到了就用 qq_send 的 image 发出去,挑不到就别配——一张不配比配错好。`);
|
|
335
388
|
const 等待 = c.replyDelayMinMs + Math.random() * Math.max(0, c.replyDelayMaxMs - c.replyDelayMinMs);
|
|
336
389
|
const 记忆 = 读记忆片段();
|
|
337
390
|
记调试(`收到来信,${Math.round(等待)}ms 后投给 QQ 专用会话 ${c.sessionId}(记忆 ${记忆.length} 字)`);
|
|
@@ -528,6 +581,7 @@ export function apply(ctx, 配置) {
|
|
|
528
581
|
记一条(记录);
|
|
529
582
|
记群消息(记录);
|
|
530
583
|
自动回复(记录);
|
|
584
|
+
鉴定表情包(记录);
|
|
531
585
|
} catch (e) {
|
|
532
586
|
记调试(`处理消息时出错:${e?.message ?? e}`);
|
|
533
587
|
}
|
|
@@ -720,7 +774,7 @@ export function apply(ctx, 配置) {
|
|
|
720
774
|
sessionId: c.sessionId,
|
|
721
775
|
everyMinMin: Number(c.proactiveEveryMinMin ?? 45),
|
|
722
776
|
everyMinMax: Number(c.proactiveEveryMinMax ?? c.proactiveEveryMin ?? 90),
|
|
723
|
-
persona: String(c.persona ?? ''),
|
|
777
|
+
persona: String(c.persona ?? ''),
|
|
724
778
|
qzoneAuto: Boolean(c.qzoneAuto),
|
|
725
779
|
qzonePrompt: String(c.qzonePrompt ?? ''),
|
|
726
780
|
qzoneMinMin: Number(c.qzoneEveryMinMin ?? 120),
|
|
@@ -734,6 +788,9 @@ export function apply(ctx, 配置) {
|
|
|
734
788
|
groupActiveWindowMin: Number(c.groupActiveWindowMin ?? 5),
|
|
735
789
|
groupCooldownMin: Number(c.groupCooldownMin ?? 10),
|
|
736
790
|
groupPerDay: Number(c.groupPerDay ?? 20),
|
|
791
|
+
limitsUnlimited: c.limitsUnlimited === true,
|
|
792
|
+
stickerOnReply: c.stickerOnReply !== false,
|
|
793
|
+
autoTagStickers: c.autoTagStickers !== false,
|
|
737
794
|
perHour: Number(c.perHour ?? 20),
|
|
738
795
|
perDay: Number(c.perDay ?? 60),
|
|
739
796
|
sentToday: 记录.filter((t) => 现在 - t < 86400).length,
|
|
@@ -760,7 +817,10 @@ export function apply(ctx, 配置) {
|
|
|
760
817
|
if (Number.isFinite(想要.everyMinMax)) patch.proactiveEveryMinMax = Math.max(5, Math.round(Number(想要.everyMinMax)));
|
|
761
818
|
if (Number.isFinite(想要.perHour)) patch.perHour = Math.min(200, Math.max(1, Math.round(Number(想要.perHour))));
|
|
762
819
|
if (Number.isFinite(想要.perDay)) patch.perDay = Math.min(1000, Math.max(1, Math.round(Number(想要.perDay))));
|
|
763
|
-
if (typeof 想要.
|
|
820
|
+
if (typeof 想要.limitsUnlimited === 'boolean') patch.limitsUnlimited = 想要.limitsUnlimited;
|
|
821
|
+
if (typeof 想要.stickerOnReply === 'boolean') patch.stickerOnReply = 想要.stickerOnReply;
|
|
822
|
+
if (typeof 想要.autoTagStickers === 'boolean') patch.autoTagStickers = 想要.autoTagStickers;
|
|
823
|
+
if (typeof 想要.persona === 'string') patch.persona = 想要.persona.slice(0, 2000);
|
|
764
824
|
if (typeof 想要.qzoneAuto === 'boolean') patch.qzoneAuto = 想要.qzoneAuto;
|
|
765
825
|
if (typeof 想要.qzonePrompt === 'string') patch.qzonePrompt = 想要.qzonePrompt.slice(0, 2000);
|
|
766
826
|
if (Number.isFinite(想要.qzoneMinMin)) patch.qzoneEveryMinMin = Math.max(10, Math.round(Number(想要.qzoneMinMin)));
|
|
@@ -837,6 +897,7 @@ export function apply(ctx, 配置) {
|
|
|
837
897
|
}
|
|
838
898
|
|
|
839
899
|
function 查额度() {
|
|
900
|
+
if (c.limitsUnlimited === true) return '';
|
|
840
901
|
const 现在 = Date.now() / 1000;
|
|
841
902
|
const 记录 = 读账(账本);
|
|
842
903
|
const 一小时 = 记录.filter((t) => 现在 - t < 3600).length;
|
|
@@ -987,7 +1048,7 @@ export function apply(ctx, 配置) {
|
|
|
987
1048
|
ctx.tools.register({
|
|
988
1049
|
name: 'qq_stickers',
|
|
989
1050
|
description:
|
|
990
|
-
'
|
|
1051
|
+
'列出手里攒着的图,带标签(mood/use/note)。拿到路径就能用 qq_send 的 image 发出去。',
|
|
991
1052
|
parameters: {
|
|
992
1053
|
type: 'object',
|
|
993
1054
|
properties: {
|
|
@@ -1003,6 +1064,15 @@ export function apply(ctx, 配置) {
|
|
|
1003
1064
|
async execute(args) {
|
|
1004
1065
|
const 数量 = Math.min(Math.max(Number(args?.count ?? 10), 1), 50);
|
|
1005
1066
|
try {
|
|
1067
|
+
const 库 = 读表情库();
|
|
1068
|
+
if (库.length > 0) {
|
|
1069
|
+
const 行 = 库
|
|
1070
|
+
.slice(-数量)
|
|
1071
|
+
.reverse()
|
|
1072
|
+
.map((x) => `[${x.mood ?? '?'}/${x.use ?? '?'}] ${x.note ? x.note + '|' : ''}${x.path}`);
|
|
1073
|
+
return { ok: true, lines: 行 };
|
|
1074
|
+
}
|
|
1075
|
+
// 还没打标签的,至少把文件列出来
|
|
1006
1076
|
const 目录 = join(家目录(), 'media');
|
|
1007
1077
|
if (!existsSync(目录)) return { ok: true, lines: [] };
|
|
1008
1078
|
const 文件 = readdirSync(目录)
|
|
@@ -1022,6 +1092,81 @@ export function apply(ctx, 配置) {
|
|
|
1022
1092
|
}),
|
|
1023
1093
|
);
|
|
1024
1094
|
|
|
1095
|
+
// ---------- qq_tag_sticker ----------
|
|
1096
|
+
ctx.effect(() =>
|
|
1097
|
+
ctx.tools.register({
|
|
1098
|
+
name: 'qq_tag_sticker',
|
|
1099
|
+
description:
|
|
1100
|
+
'把一张图收进自己的表情包库并打标签。值得收才收——重复的、没意思的、看不清的就别收。',
|
|
1101
|
+
parameters: {
|
|
1102
|
+
type: 'object',
|
|
1103
|
+
properties: {
|
|
1104
|
+
path: { type: 'string', description: '图片本地路径' },
|
|
1105
|
+
mood: { type: 'string', description: '情绪:笑 / 敷衍 / 惊讶 / 无语 / 得意 …' },
|
|
1106
|
+
use: { type: 'string', description: '用在哪儿:接梗 / 收尾 / 耍赖 / 安慰 …' },
|
|
1107
|
+
note: { type: 'string', description: '一句话说明这张图长什么样,方便以后匹配' },
|
|
1108
|
+
},
|
|
1109
|
+
required: ['path', 'mood'],
|
|
1110
|
+
additionalProperties: false,
|
|
1111
|
+
},
|
|
1112
|
+
output: 工具输出({ added: { type: 'boolean' } }, (v) =>
|
|
1113
|
+
v.ok ? (v.added ? '收进库了。' : '库里已经有这张,没重复加。') : `没存成:${v.reason ?? '未知原因'}`,
|
|
1114
|
+
),
|
|
1115
|
+
async execute(args) {
|
|
1116
|
+
const 路径 = String(args?.path ?? '').trim();
|
|
1117
|
+
if (!路径 || !existsSync(路径)) return { ok: false, added: false, reason: '找不到这张图' };
|
|
1118
|
+
const 指纹 = 文件指纹(路径);
|
|
1119
|
+
const 库 = 读表情库();
|
|
1120
|
+
if (指纹 && 库.some((x) => x.hash === 指纹)) return { ok: true, added: false };
|
|
1121
|
+
const 条目 = {
|
|
1122
|
+
path: 路径,
|
|
1123
|
+
hash: 指纹,
|
|
1124
|
+
mood: String(args?.mood ?? '').slice(0, 20),
|
|
1125
|
+
use: String(args?.use ?? '').slice(0, 20),
|
|
1126
|
+
note: String(args?.note ?? '').slice(0, 80),
|
|
1127
|
+
addedAt: new Date().toISOString(),
|
|
1128
|
+
};
|
|
1129
|
+
库.push(条目);
|
|
1130
|
+
写表情库(库);
|
|
1131
|
+
记调试(`表情入库:${条目.mood}/${条目.use} ${路径}`);
|
|
1132
|
+
return { ok: true, added: true };
|
|
1133
|
+
},
|
|
1134
|
+
}),
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
// ---------- qq_pick_sticker ----------
|
|
1138
|
+
ctx.effect(() =>
|
|
1139
|
+
ctx.tools.register({
|
|
1140
|
+
name: 'qq_pick_sticker',
|
|
1141
|
+
description:
|
|
1142
|
+
'从表情包库里挑一张合适的(按情绪和用途匹配)。没有合适的会明说,别硬凑。',
|
|
1143
|
+
parameters: {
|
|
1144
|
+
type: 'object',
|
|
1145
|
+
properties: {
|
|
1146
|
+
mood: { type: 'string', description: '想要的情绪,比如 笑 / 无语 / 得意' },
|
|
1147
|
+
use: { type: 'string', description: '用在哪,比如 接梗 / 收尾 / 耍赖' },
|
|
1148
|
+
},
|
|
1149
|
+
additionalProperties: false,
|
|
1150
|
+
},
|
|
1151
|
+
output: 工具输出({ path: { type: 'string' } }, (v) =>
|
|
1152
|
+
v.ok ? (v.path ? `挑到这张:${v.path}` : `没挑到合适的:${v.reason ?? '库里没有匹配的'}`) : `挑不出来:${v.reason ?? '未知原因'}`,
|
|
1153
|
+
),
|
|
1154
|
+
async execute(args) {
|
|
1155
|
+
const 库 = 读表情库();
|
|
1156
|
+
if (库.length === 0) return { ok: false, path: '', reason: '表情库还是空的' };
|
|
1157
|
+
const 情绪 = String(args?.mood ?? '').trim();
|
|
1158
|
+
const 用途 = String(args?.use ?? '').trim();
|
|
1159
|
+
const 命中 = 库.filter(
|
|
1160
|
+
(x) => (!情绪 || x.mood === 情绪) && (!用途 || x.use === 用途),
|
|
1161
|
+
);
|
|
1162
|
+
const 池 = 命中.length > 0 ? 命中 : [];
|
|
1163
|
+
if (池.length === 0) return { ok: true, path: '', reason: '库里没有这个组合' };
|
|
1164
|
+
const 选 = 池[Math.floor(Math.random() * 池.length)];
|
|
1165
|
+
return { ok: true, path: 选.path };
|
|
1166
|
+
},
|
|
1167
|
+
}),
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1025
1170
|
// ---------- qq_qzone ----------
|
|
1026
1171
|
ctx.effect(() =>
|
|
1027
1172
|
ctx.tools.register({
|