dsh-qq-moony 0.6.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 +20 -0
- package/README.md +13 -0
- package/client.js +68 -0
- package/cordis.patch.yml +5 -2
- package/index.js +163 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
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
|
+
**成本提醒**:自动收录要"看一张图",每条带图的来信都会多一次读图;配表情包也要多一轮工具调用。嫌贵就把这两个开关关掉。
|
|
14
|
+
## 0.7.0 — 2026-10-25
|
|
15
|
+
|
|
16
|
+
把"我自己的东西"从包里拿掉——别人装的是插件,不是我。
|
|
17
|
+
|
|
18
|
+
- **修:包里的 patch 不再写死我的 token / 小号号 / 人设。** 之前 `cordis.patch.yml` 里带着作者自己的 `token` 和 `uin`,别人装上会直接拿它去连——连不上还算好的,连上了更糟。现在一律留空,由各人在 `config.json` 里填。
|
|
19
|
+
- **新增:`persona`(人设)配置项,默认空。** 留空时,自动回复的会话只被告知"你替这个账号主人在 DSH 里收发消息"——中性的,不会变成别人的角色。填了才用你写的人设。设置页里也加了一块文本框。
|
|
20
|
+
- **措辞中性化**:提示词里原来写死的"你是溟月""主人"改成"账号主人",不再把我的身份塞给别人。
|
|
21
|
+
|
|
22
|
+
**升级提醒**:已经装过的人重新装一次即可;如果之前在 `config.json` 里填过端口和 token,这次会真正生效(0.6.0 修了优先级)。
|
|
3
23
|
## 0.6.0 — 2026-10-25
|
|
4
24
|
|
|
5
25
|
有人报告"装上了但一直连不上、还反复自己重启",根因是两个 bug 叠在一起:
|
package/README.md
CHANGED
|
@@ -82,6 +82,19 @@ NapCat 是接管整个 QQ 客户端的行为——消息收发、登录态、空
|
|
|
82
82
|
|
|
83
83
|
---
|
|
84
84
|
|
|
85
|
+
### 人设:默认是中性,不会变成作者的角色
|
|
86
|
+
|
|
87
|
+
包里**不带任何人的身份**。`persona` 留空时,自动回复的会话只被告知"你替这个账号主人在 DSH 里收发消息"——干净、中性,你说什么它就是什么。
|
|
88
|
+
|
|
89
|
+
想让它有自己的性格,就在设置页「人设」那一栏写几句,或者改 `config.json` 的 `persona`:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
"persona": "你是这个账号主人在 DSH 里养的助手,说话短、口语,不自我介绍"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
填了之后,这段话会跟着每条自动回复一起投给会话。
|
|
96
|
+
|
|
97
|
+
⚠️ 同理,**包里也不带作者的 token 和小号号**。`cordis.patch.yml` 里这几项都是空的,必须你自己在 `config.json` 填。
|
|
85
98
|
## 四、配置
|
|
86
99
|
|
|
87
100
|
优先级:**本地配置文件 > bundle patch 里的 config > 代码默认值**。
|
package/client.js
CHANGED
|
@@ -109,6 +109,7 @@ window.__ModuleLoader__.load({
|
|
|
109
109
|
qzoneMinMin: 状态?.qzoneMinMin ?? 120,
|
|
110
110
|
qzoneMinMax: 状态?.qzoneMinMax ?? 240,
|
|
111
111
|
qzonePrompt: 状态?.qzonePrompt ?? '',
|
|
112
|
+
persona: 状态?.persona ?? '',
|
|
112
113
|
groupWhitelist: (状态?.groupWhitelist ?? []).join(' '),
|
|
113
114
|
groupContextSize: 状态?.groupContextSize ?? 30,
|
|
114
115
|
groupEveryMinMin: 状态?.groupEveryMinMin ?? 8,
|
|
@@ -139,6 +140,7 @@ window.__ModuleLoader__.load({
|
|
|
139
140
|
qzoneMinMin: Number(值.qzoneMinMin),
|
|
140
141
|
qzoneMinMax: Number(值.qzoneMinMax),
|
|
141
142
|
qzonePrompt: String(值.qzonePrompt ?? ''),
|
|
143
|
+
persona: String(值.persona ?? ''),
|
|
142
144
|
groupWhitelist: String(值.groupWhitelist ?? ''),
|
|
143
145
|
groupContextSize: Number(值.groupContextSize),
|
|
144
146
|
groupEveryMinMin: Number(值.groupEveryMinMin),
|
|
@@ -203,6 +205,39 @@ window.__ModuleLoader__.load({
|
|
|
203
205
|
),
|
|
204
206
|
),
|
|
205
207
|
),
|
|
208
|
+
h(
|
|
209
|
+
'div',
|
|
210
|
+
{ style: { ...卡片, alignItems: 'flex-start' } },
|
|
211
|
+
h(
|
|
212
|
+
'div',
|
|
213
|
+
{ style: { flex: 1 } },
|
|
214
|
+
h('div', { style: 标题 }, '人设(可选)'),
|
|
215
|
+
h(
|
|
216
|
+
'div',
|
|
217
|
+
{ style: 说明 },
|
|
218
|
+
'填了就把这段当作「你是谁」交给自动回复的会话;留空则用中性的“替账号主人收发消息”。不想让这个号变成别人的角色,就别填。',
|
|
219
|
+
),
|
|
220
|
+
h('textarea', {
|
|
221
|
+
rows: 3,
|
|
222
|
+
value: 值.persona,
|
|
223
|
+
placeholder: '例:你是这个账号主人在 DSH 里养的助手,说话短、口语,不自我介绍',
|
|
224
|
+
onChange: (e) => 数字('persona', e.target.value),
|
|
225
|
+
style: {
|
|
226
|
+
width: '100%',
|
|
227
|
+
marginTop: '8px',
|
|
228
|
+
padding: '8px 10px',
|
|
229
|
+
borderRadius: '10px',
|
|
230
|
+
border: '1px solid var(--dsw-alias-border-2, rgba(127,127,127,0.35))',
|
|
231
|
+
background: 'transparent',
|
|
232
|
+
color: 'inherit',
|
|
233
|
+
fontSize: '12px',
|
|
234
|
+
lineHeight: '19px',
|
|
235
|
+
resize: 'vertical',
|
|
236
|
+
boxSizing: 'border-box',
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
),
|
|
240
|
+
),
|
|
206
241
|
h(
|
|
207
242
|
'div',
|
|
208
243
|
{ style: { ...卡片 } },
|
|
@@ -436,6 +471,39 @@ window.__ModuleLoader__.load({
|
|
|
436
471
|
),
|
|
437
472
|
h(开关, { 开: 状态.qzoneAuto, 点: () => 改({ qzoneAuto: !状态.qzoneAuto }) }),
|
|
438
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
|
+
),
|
|
439
507
|
h(
|
|
440
508
|
'div',
|
|
441
509
|
{ style: { display: 'flex', justifyContent: 'flex-end', gap: '10px', alignItems: 'center' } },
|
package/cordis.patch.yml
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# dsh-qq-moony bundle patch
|
|
2
2
|
# 把本包插进 profile 的层叠配置里。装好后 profile 只要在 bundles 里列出本包即生效。
|
|
3
|
+
#
|
|
4
|
+
# ⚠️ 这里只放「所有人都一样」的默认值。token / uin / 端口 / 人设这些属于各人自己的,
|
|
5
|
+
# 一律留空,由用户在 <DSH_HOME>/qq-moony/config.json 里填——本地文件优先级最高。
|
|
3
6
|
- insert:
|
|
4
7
|
- id: qq-moony
|
|
5
8
|
name: dsh-qq-moony
|
|
6
9
|
config:
|
|
7
10
|
enabled: true
|
|
8
11
|
baseUrl: 'http://127.0.0.1:3000'
|
|
9
|
-
token: '
|
|
10
|
-
uin:
|
|
12
|
+
token: ''
|
|
13
|
+
uin: 0
|
|
11
14
|
perHour: 20
|
|
12
15
|
perDay: 60
|
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,14 @@ 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,
|
|
70
|
+
// 人设:留空则用中性说明;填了就把这段当作"你是谁"交给会话
|
|
71
|
+
persona: '',
|
|
63
72
|
// DSH 启动时自动把 NapCat 拉起来(免扫码,用 -q 快速登录)
|
|
64
73
|
autoStartNapCat: true,
|
|
65
74
|
napCatDir: '',
|
|
@@ -101,6 +110,36 @@ function 记账(账本, 目标, 摘要) {
|
|
|
101
110
|
appendFileSync(账本, `${Date.now() / 1000}\t${目标}\t${String(摘要).slice(0, 60).replace(/\s+/g, ' ')}\n`, 'utf8');
|
|
102
111
|
}
|
|
103
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
|
+
|
|
104
143
|
const 工具输出 = (字段, 渲染) => ({
|
|
105
144
|
schema: {
|
|
106
145
|
type: 'object',
|
|
@@ -280,6 +319,28 @@ export function apply(ctx, 配置) {
|
|
|
280
319
|
|
|
281
320
|
const 群下次机会 = new Map();
|
|
282
321
|
|
|
322
|
+
/** 投递时给会话的"你是谁"说明:默认中性;填了 persona 才用主人自己写的人设。 */
|
|
323
|
+
function 身份说明() {
|
|
324
|
+
const 人设 = String(c.persona ?? '').trim();
|
|
325
|
+
if (!人设) {
|
|
326
|
+
return '这是你的 QQ 通道,你替这个账号的主人在 DSH 里收发消息。用账号主人平时的语气说话,别自我介绍。';
|
|
327
|
+
}
|
|
328
|
+
return `${人设}\n(同一个工作区、同一份记忆,memory/ 下的文件随时可看。)`;
|
|
329
|
+
}
|
|
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
|
+
|
|
283
344
|
/** 把来信投给 QQ 专用会话,让那边的 agent 去回。失败只记日志,不影响收消息。 */
|
|
284
345
|
function 自动回复(记录) {
|
|
285
346
|
记调试(
|
|
@@ -319,8 +380,11 @@ export function apply(ctx, 配置) {
|
|
|
319
380
|
const 回哪里 = 是群 ? `group:${记录.group}` : String(记录.from);
|
|
320
381
|
const 提示 =
|
|
321
382
|
`【QQ ${是群 ? `群 ${记录.group} 里被点名` : '来信'}】${记录.name}(${记录.from})在 ${记录.t} 说:${描述}\n\n` +
|
|
322
|
-
|
|
323
|
-
`用 qq_send 回给 ${回哪里}${是群 ? '(群里只回一句,别接着聊)' : ',一句就够'}
|
|
383
|
+
`${身份说明()}\n` +
|
|
384
|
+
`用 qq_send 回给 ${回哪里}${是群 ? '(群里只回一句,别接着聊)' : ',一句就够'},短,像平时说话那样,别解释你在做什么。回完就停。` +
|
|
385
|
+
(c.stickerOnReply === false
|
|
386
|
+
? ''
|
|
387
|
+
: `\n如果这句适合配一张图,可以先用 qq_pick_sticker 按情绪/用途挑一张;挑到了就用 qq_send 的 image 发出去,挑不到就别配——一张不配比配错好。`);
|
|
324
388
|
const 等待 = c.replyDelayMinMs + Math.random() * Math.max(0, c.replyDelayMaxMs - c.replyDelayMinMs);
|
|
325
389
|
const 记忆 = 读记忆片段();
|
|
326
390
|
记调试(`收到来信,${Math.round(等待)}ms 后投给 QQ 专用会话 ${c.sessionId}(记忆 ${记忆.length} 字)`);
|
|
@@ -517,6 +581,7 @@ export function apply(ctx, 配置) {
|
|
|
517
581
|
记一条(记录);
|
|
518
582
|
记群消息(记录);
|
|
519
583
|
自动回复(记录);
|
|
584
|
+
鉴定表情包(记录);
|
|
520
585
|
} catch (e) {
|
|
521
586
|
记调试(`处理消息时出错:${e?.message ?? e}`);
|
|
522
587
|
}
|
|
@@ -658,8 +723,8 @@ export function apply(ctx, 配置) {
|
|
|
658
723
|
记调试(`空间模式:给它一次发说说的机会(下一次排在 ${Math.round(下次.分钟)} 分钟后)`);
|
|
659
724
|
投递到会话(
|
|
660
725
|
`【QQ 空间】到了看看要不要发条说说的点。\n` +
|
|
661
|
-
(要求 ?
|
|
662
|
-
`想发就用 qq_qzone
|
|
726
|
+
(要求 ? `账号主人给的要求:${要求}\n` : '没有特别要求:想发就发一条轻的,不想发就结束这一轮。\n') +
|
|
727
|
+
`想发就用 qq_qzone;只发轻的,不点名、不写具体地点、不提账号主人。发完就停,别连着发第二条。${记忆 ? `\n\n${记忆}` : ''}`,
|
|
663
728
|
);
|
|
664
729
|
}, 60000);
|
|
665
730
|
|
|
@@ -709,6 +774,7 @@ export function apply(ctx, 配置) {
|
|
|
709
774
|
sessionId: c.sessionId,
|
|
710
775
|
everyMinMin: Number(c.proactiveEveryMinMin ?? 45),
|
|
711
776
|
everyMinMax: Number(c.proactiveEveryMinMax ?? c.proactiveEveryMin ?? 90),
|
|
777
|
+
persona: String(c.persona ?? ''),
|
|
712
778
|
qzoneAuto: Boolean(c.qzoneAuto),
|
|
713
779
|
qzonePrompt: String(c.qzonePrompt ?? ''),
|
|
714
780
|
qzoneMinMin: Number(c.qzoneEveryMinMin ?? 120),
|
|
@@ -722,6 +788,9 @@ export function apply(ctx, 配置) {
|
|
|
722
788
|
groupActiveWindowMin: Number(c.groupActiveWindowMin ?? 5),
|
|
723
789
|
groupCooldownMin: Number(c.groupCooldownMin ?? 10),
|
|
724
790
|
groupPerDay: Number(c.groupPerDay ?? 20),
|
|
791
|
+
limitsUnlimited: c.limitsUnlimited === true,
|
|
792
|
+
stickerOnReply: c.stickerOnReply !== false,
|
|
793
|
+
autoTagStickers: c.autoTagStickers !== false,
|
|
725
794
|
perHour: Number(c.perHour ?? 20),
|
|
726
795
|
perDay: Number(c.perDay ?? 60),
|
|
727
796
|
sentToday: 记录.filter((t) => 现在 - t < 86400).length,
|
|
@@ -748,6 +817,10 @@ export function apply(ctx, 配置) {
|
|
|
748
817
|
if (Number.isFinite(想要.everyMinMax)) patch.proactiveEveryMinMax = Math.max(5, Math.round(Number(想要.everyMinMax)));
|
|
749
818
|
if (Number.isFinite(想要.perHour)) patch.perHour = Math.min(200, Math.max(1, Math.round(Number(想要.perHour))));
|
|
750
819
|
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;
|
|
822
|
+
if (typeof 想要.autoTagStickers === 'boolean') patch.autoTagStickers = 想要.autoTagStickers;
|
|
823
|
+
if (typeof 想要.persona === 'string') patch.persona = 想要.persona.slice(0, 2000);
|
|
751
824
|
if (typeof 想要.qzoneAuto === 'boolean') patch.qzoneAuto = 想要.qzoneAuto;
|
|
752
825
|
if (typeof 想要.qzonePrompt === 'string') patch.qzonePrompt = 想要.qzonePrompt.slice(0, 2000);
|
|
753
826
|
if (Number.isFinite(想要.qzoneMinMin)) patch.qzoneEveryMinMin = Math.max(10, Math.round(Number(想要.qzoneMinMin)));
|
|
@@ -824,6 +897,7 @@ export function apply(ctx, 配置) {
|
|
|
824
897
|
}
|
|
825
898
|
|
|
826
899
|
function 查额度() {
|
|
900
|
+
if (c.limitsUnlimited === true) return '';
|
|
827
901
|
const 现在 = Date.now() / 1000;
|
|
828
902
|
const 记录 = 读账(账本);
|
|
829
903
|
const 一小时 = 记录.filter((t) => 现在 - t < 3600).length;
|
|
@@ -974,7 +1048,7 @@ export function apply(ctx, 配置) {
|
|
|
974
1048
|
ctx.tools.register({
|
|
975
1049
|
name: 'qq_stickers',
|
|
976
1050
|
description:
|
|
977
|
-
'
|
|
1051
|
+
'列出手里攒着的图,带标签(mood/use/note)。拿到路径就能用 qq_send 的 image 发出去。',
|
|
978
1052
|
parameters: {
|
|
979
1053
|
type: 'object',
|
|
980
1054
|
properties: {
|
|
@@ -990,6 +1064,15 @@ export function apply(ctx, 配置) {
|
|
|
990
1064
|
async execute(args) {
|
|
991
1065
|
const 数量 = Math.min(Math.max(Number(args?.count ?? 10), 1), 50);
|
|
992
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
|
+
// 还没打标签的,至少把文件列出来
|
|
993
1076
|
const 目录 = join(家目录(), 'media');
|
|
994
1077
|
if (!existsSync(目录)) return { ok: true, lines: [] };
|
|
995
1078
|
const 文件 = readdirSync(目录)
|
|
@@ -1009,6 +1092,81 @@ export function apply(ctx, 配置) {
|
|
|
1009
1092
|
}),
|
|
1010
1093
|
);
|
|
1011
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
|
+
|
|
1012
1170
|
// ---------- qq_qzone ----------
|
|
1013
1171
|
ctx.effect(() =>
|
|
1014
1172
|
ctx.tools.register({
|