dsh-qq-tools 0.9.5 → 0.10.2
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/README.md +23 -11
- package/cordis.patch.yml +1 -1
- package/lib/config.js +96 -0
- package/lib/images.js +420 -0
- package/lib/index.js +2 -1
- package/lib/mount-all.js +59 -0
- package/lib/observation.js +24 -0
- package/lib/onebot.js +32 -0
- package/lib/prompts.js +56 -0
- package/lib/qq-state.js +51 -0
- package/lib/render.js +49 -0
- package/lib/session.js +201 -0
- package/lib/shared.js +26 -3377
- package/lib/tools/avatar.js +54 -0
- package/lib/tools/faces.js +296 -0
- package/lib/tools/friends.js +559 -0
- package/lib/tools/groups.js +220 -0
- package/lib/tools/poke.js +41 -0
- package/lib/tools/private.js +190 -0
- package/lib/tools/qzone.js +569 -0
- package/lib/utils.js +73 -0
- package/lib/watchers/mentions.js +167 -0
- package/lib/watchers/poke.js +87 -0
- package/lib/watchers/private.js +95 -0
- package/lib/watchers/ws.js +61 -0
- package/package.json +98 -103
- package/tools/qq-favorite-faces/index.js +11 -10
- package/tools/qq-favorite-faces/locale/en.json +1 -1
- package/tools/qq-favorite-faces/locale/zh.json +1 -1
- package/tools/qq-friend-management/index.js +28 -28
- package/tools/qq-get-group-history/index.js +10 -8
- package/tools/qq-get-group-members/index.js +9 -8
- package/tools/qq-list-groups/index.js +9 -8
- package/tools/qq-private-chat/index.js +16 -20
- package/tools/qq-qzone/index.js +22 -22
- package/tools/qq-send-face/index.js +11 -8
- package/tools/qq-send-face/locale/en.json +1 -1
- package/tools/qq-send-face/locale/zh.json +1 -1
- package/tools/qq-send-group-message/index.js +13 -10
- package/tools/qq-send-poke/index.js +13 -10
- package/tools/qq-set-avatar/index.js +9 -8
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// @ 提醒与群后续轮询。
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import { loadCfg, diag } from '../config.js'
|
|
5
|
+
import { onebot } from '../onebot.js'
|
|
6
|
+
import { clean, segText } from '../utils.js'
|
|
7
|
+
import { listOf } from '../render.js'
|
|
8
|
+
import { formatIncomingLine, favoriteHint, FACE_STYLE, CHAT_STYLE, lookHint } from '../prompts.js'
|
|
9
|
+
import { hasActiveTool, targetSessionId, findAgent } from '../session.js'
|
|
10
|
+
import { windowUntil, pruneExpiredWindows } from '../observation.js'
|
|
11
|
+
import { getSelfId, refreshNames, groupName, resetQqState } from '../qq-state.js'
|
|
12
|
+
|
|
13
|
+
// ---------- @ 提醒轮询(由「发群消息」组件启动) ----------
|
|
14
|
+
let lastSeen = new Map()
|
|
15
|
+
let seen = new Set()
|
|
16
|
+
let busy = false
|
|
17
|
+
let noAgentAt = 0
|
|
18
|
+
|
|
19
|
+
async function pollMentions(ctx) {
|
|
20
|
+
const cfg = loadCfg()
|
|
21
|
+
if (!cfg.mentionEnabled) return
|
|
22
|
+
if (busy) return
|
|
23
|
+
busy = true
|
|
24
|
+
try {
|
|
25
|
+
const target = targetSessionId()
|
|
26
|
+
const agent = findAgent(ctx.agents, target)
|
|
27
|
+
if (!agent) {
|
|
28
|
+
if (Date.now() - noAgentAt > 60000) {
|
|
29
|
+
noAgentAt = Date.now()
|
|
30
|
+
diag('@ 轮询:目标会话没有活着(' + (target || '未设置') + ')')
|
|
31
|
+
}
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const selfId = await getSelfId(cfg)
|
|
35
|
+
const now = Math.floor(Date.now() / 1000)
|
|
36
|
+
if (!lastSeen.size) {
|
|
37
|
+
for (const gid of cfg.groups) lastSeen.set(String(gid), now)
|
|
38
|
+
diag('@ 轮询初始化,从现在开始(' + now + ')')
|
|
39
|
+
}
|
|
40
|
+
await refreshNames(cfg)
|
|
41
|
+
const mentions = []
|
|
42
|
+
const followups = new Map()
|
|
43
|
+
for (const gid of cfg.groups) {
|
|
44
|
+
let messages = []
|
|
45
|
+
try {
|
|
46
|
+
messages = listOf(await onebot('get_group_msg_history', { group_id: Number(gid), count: 10 }, cfg), 'messages')
|
|
47
|
+
} catch (error) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
const prev = lastSeen.get(String(gid)) || 0
|
|
51
|
+
let maxTime = prev
|
|
52
|
+
for (const m of listOf(messages, 'messages').slice().sort((a, b) => (a.time || 0) - (b.time || 0))) {
|
|
53
|
+
const t = Number(m.time || 0)
|
|
54
|
+
if (t < prev) continue
|
|
55
|
+
if (t > maxTime) maxTime = t
|
|
56
|
+
const mid = m.message_id
|
|
57
|
+
if (mid != null) {
|
|
58
|
+
if (seen.has(mid)) continue
|
|
59
|
+
seen.add(mid)
|
|
60
|
+
}
|
|
61
|
+
const uid = Number((m.sender && m.sender.user_id) || m.user_id || 0)
|
|
62
|
+
if (selfId && uid === Number(selfId)) continue
|
|
63
|
+
const segs = Array.isArray(m.message) ? m.message : []
|
|
64
|
+
const atMe = segs.some(
|
|
65
|
+
(s) => s && s.type === 'at' && (String(s.data && s.data.qq) === selfId || String(s.data && s.data.qq) === 'all'),
|
|
66
|
+
)
|
|
67
|
+
const text = segText(segs) || '(没有文字)'
|
|
68
|
+
const name = clean((m.sender && (m.sender.card || m.sender.nickname)) || String(uid))
|
|
69
|
+
const senderQQ = String(uid || (m.sender && m.sender.user_id) || '?')
|
|
70
|
+
const winUntil = windowUntil(gid)
|
|
71
|
+
if (Date.now() < winUntil) {
|
|
72
|
+
const arr = followups.get(String(gid)) || []
|
|
73
|
+
arr.push({
|
|
74
|
+
gid: String(gid),
|
|
75
|
+
group: groupName(gid),
|
|
76
|
+
sender: name,
|
|
77
|
+
user_id: senderQQ,
|
|
78
|
+
message_id: mid,
|
|
79
|
+
text,
|
|
80
|
+
time: t,
|
|
81
|
+
})
|
|
82
|
+
followups.set(String(gid), arr)
|
|
83
|
+
} else if (atMe) {
|
|
84
|
+
mentions.push({
|
|
85
|
+
gid: String(gid),
|
|
86
|
+
group: groupName(gid),
|
|
87
|
+
sender: name,
|
|
88
|
+
user_id: senderQQ,
|
|
89
|
+
message_id: mid,
|
|
90
|
+
text,
|
|
91
|
+
time: t,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
lastSeen.set(String(gid), maxTime)
|
|
96
|
+
}
|
|
97
|
+
if (seen.size > 500) seen.clear()
|
|
98
|
+
pruneExpiredWindows()
|
|
99
|
+
if (!mentions.length && followups.size === 0) return
|
|
100
|
+
const fmtLine = (x) => formatIncomingLine(x.sender, x.user_id, x.text, x.message_id)
|
|
101
|
+
const lines = mentions.slice(-5).map((x) => `群「${x.group}」${fmtLine(x)}`)
|
|
102
|
+
const sendTools = []
|
|
103
|
+
if (hasActiveTool('qq_send_group_message')) sendTools.push('qq_send_group_message')
|
|
104
|
+
if (hasActiveTool('qq_send_face')) sendTools.push('qq_send_face')
|
|
105
|
+
const sendHint = sendTools.length ? '想说话可以用 ' + sendTools.join(' / ') + ' 回复;' : ''
|
|
106
|
+
const quoteHint = hasActiveTool('qq_send_group_message')
|
|
107
|
+
? '确实要指代某条消息时才传 reply_to=消息编号;必须提醒某个人时才传 at_user_ids=[QQ号];'
|
|
108
|
+
: ''
|
|
109
|
+
const favLine = [...mentions].reverse().find((x) => favoriteHint(x.text, x.message_id))
|
|
110
|
+
const favHint = favLine && hasActiveTool('qq_favorite_face')
|
|
111
|
+
? favoriteHint(favLine.text, favLine.message_id)
|
|
112
|
+
: ''
|
|
113
|
+
const faceHint = hasActiveTool('qq_send_face') ? FACE_STYLE : ''
|
|
114
|
+
const prompt =
|
|
115
|
+
'【群聊 @ 提醒】有人 @ 了你:\n' +
|
|
116
|
+
lines.join('\n') +
|
|
117
|
+
'\n' + lookHint() + '再决定要不要回复、回哪个群。' + sendHint + quoteHint + favHint + faceHint + '不想说话就安静待着;' +
|
|
118
|
+
'不要提工具和私聊内容。' + CHAT_STYLE
|
|
119
|
+
const message = {
|
|
120
|
+
id: randomUUID(),
|
|
121
|
+
role: 'user',
|
|
122
|
+
content: [{ type: 'text', text: prompt }],
|
|
123
|
+
source: { kind: 'qq-mention' },
|
|
124
|
+
}
|
|
125
|
+
if (mentions.length) {
|
|
126
|
+
await agent.followup(message)
|
|
127
|
+
diag('@ 提醒已送进会话:' + lines.join(' | ').slice(0, 300))
|
|
128
|
+
}
|
|
129
|
+
for (const [gid, list] of followups) {
|
|
130
|
+
const group = list[0] && list[0].group ? list[0].group : gid
|
|
131
|
+
const lines2 = list.slice(-10).map(fmtLine)
|
|
132
|
+
const favLine2 = [...list].reverse().find((x) => favoriteHint(x.text, x.message_id))
|
|
133
|
+
const favHint2 = favLine2 && hasActiveTool('qq_favorite_face')
|
|
134
|
+
? favoriteHint(favLine2.text, favLine2.message_id)
|
|
135
|
+
: ''
|
|
136
|
+
const prompt2 =
|
|
137
|
+
'【群聊后续】你刚才在群「' + group + '」里说过话,现在又有人说话:\n' +
|
|
138
|
+
lines2.join('\n') +
|
|
139
|
+
'\n想接就接;' +
|
|
140
|
+
(hasActiveTool('qq_send_group_message') ? '想说话用 qq_send_group_message 发到这个群,确实要指代某条才传 reply_to=消息编号,必须提醒某人才传 at_user_ids=[QQ号];' : '') +
|
|
141
|
+
favHint2 +
|
|
142
|
+
(hasActiveTool('qq_send_face') ? FACE_STYLE : '') +
|
|
143
|
+
'不想接就安静待着。' + CHAT_STYLE
|
|
144
|
+
await agent.followup({
|
|
145
|
+
id: randomUUID(),
|
|
146
|
+
role: 'user',
|
|
147
|
+
content: [{ type: 'text', text: prompt2 }],
|
|
148
|
+
source: { kind: 'qq-followup' },
|
|
149
|
+
})
|
|
150
|
+
diag('群后续 → 会话:群 ' + gid + ' ' + lines2.join(' | ').slice(0, 240))
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
diag('@ 轮询出错:' + String((error && error.message) || error))
|
|
154
|
+
} finally {
|
|
155
|
+
busy = false
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function startMentionPolling(ctx) {
|
|
160
|
+
// 组件重新加载时从头开始,避免把停用期间的旧 @ 一次性翻出来。
|
|
161
|
+
resetQqState()
|
|
162
|
+
lastSeen = new Map()
|
|
163
|
+
seen = new Set()
|
|
164
|
+
busy = false
|
|
165
|
+
noAgentAt = 0
|
|
166
|
+
ctx.effect(() => ctx.interval(() => pollMentions(ctx), loadCfg().pollMs))
|
|
167
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// 戳一戳事件接收。
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import { loadCfg, allowedPrivate, diag } from '../config.js'
|
|
5
|
+
import { onebot } from '../onebot.js'
|
|
6
|
+
import { clean } from '../utils.js'
|
|
7
|
+
import { hasActiveTool, targetSessionId, findAgent } from '../session.js'
|
|
8
|
+
import { FACE_STYLE, CHAT_STYLE } from '../prompts.js'
|
|
9
|
+
import { cachedSelfId, groupName as groupNameOf } from '../qq-state.js'
|
|
10
|
+
import { startWsWatcher } from './ws.js'
|
|
11
|
+
|
|
12
|
+
// ---------- 戳一戳(由「戳一戳」组件启动) ----------
|
|
13
|
+
const pokeSeen = new Set()
|
|
14
|
+
const memberNames = new Map()
|
|
15
|
+
|
|
16
|
+
async function memberName(groupId, userId) {
|
|
17
|
+
const key = String(groupId) + ':' + String(userId)
|
|
18
|
+
const hit = memberNames.get(key)
|
|
19
|
+
if (hit && Date.now() - hit.at < 300000) return hit.name
|
|
20
|
+
try {
|
|
21
|
+
const d = await onebot('get_group_member_info', { group_id: Number(groupId), user_id: Number(userId) }, loadCfg())
|
|
22
|
+
const name = clean((d && (d.card || d.nickname)) || userId)
|
|
23
|
+
memberNames.set(key, { name, at: Date.now() })
|
|
24
|
+
return name
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return String(userId)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function handlePoke(ctx, ev) {
|
|
31
|
+
const cfg = loadCfg()
|
|
32
|
+
if (!cfg.pokeEnabled) return
|
|
33
|
+
const self = String(ev.self_id || cachedSelfId() || '')
|
|
34
|
+
const sender = String(ev.user_id || ev.sender_id || '')
|
|
35
|
+
const target = String(ev.target_id || '')
|
|
36
|
+
if (!self || !sender || sender === self) return
|
|
37
|
+
if (target && target !== self) return
|
|
38
|
+
const groupId = ev.group_id ? String(ev.group_id) : ''
|
|
39
|
+
if (!groupId && !allowedPrivate(sender, cfg)) return
|
|
40
|
+
const key = [ev.time, groupId, sender, target].join('|')
|
|
41
|
+
if (pokeSeen.has(key)) return
|
|
42
|
+
pokeSeen.add(key)
|
|
43
|
+
if (pokeSeen.size > 300) pokeSeen.clear()
|
|
44
|
+
const agent = findAgent(ctx.agents, targetSessionId())
|
|
45
|
+
if (!agent) {
|
|
46
|
+
diag('戳一戳:目标会话没有活着')
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
const name = groupId ? await memberName(groupId, sender) : sender
|
|
50
|
+
const groupName = groupId ? groupNameOf(groupId) : ''
|
|
51
|
+
let prompt
|
|
52
|
+
if (groupId) {
|
|
53
|
+
const actions = []
|
|
54
|
+
if (hasActiveTool('qq_send_group_message')) actions.push('想回就用 qq_send_group_message 发到这个群')
|
|
55
|
+
if (hasActiveTool('qq_send_face')) actions.push('想回个表情用 qq_send_face(group_id=' + groupId + ')')
|
|
56
|
+
if (hasActiveTool('qq_send_poke')) actions.push(`想戳回去用 qq_send_poke(group_id=${groupId}, user_id=${sender})`)
|
|
57
|
+
prompt = `【戳一戳】群「${groupName}」里 ${name}(${sender}) 戳了你一下。`
|
|
58
|
+
+ (actions.length ? actions.join(';') + '。不想回就安静待着。' : '不用回复。')
|
|
59
|
+
+ (hasActiveTool('qq_send_face') ? FACE_STYLE : '')
|
|
60
|
+
+ CHAT_STYLE
|
|
61
|
+
} else {
|
|
62
|
+
const actions = []
|
|
63
|
+
if (hasActiveTool('qq_send_private_message')) actions.push(`想回就用 qq_send_private_message(user_id=${sender})`)
|
|
64
|
+
if (hasActiveTool('qq_send_face')) actions.push(`想回表情用 qq_send_face(user_id=${sender})`)
|
|
65
|
+
if (hasActiveTool('qq_send_poke')) actions.push(`想戳回去用 qq_send_poke(user_id=${sender})`)
|
|
66
|
+
prompt = `【戳一戳】${sender} 在私聊里戳了你一下。`
|
|
67
|
+
+ (actions.length ? actions.join(';') + '。不想回就安静待着。' : '不用回复。')
|
|
68
|
+
+ (hasActiveTool('qq_send_face') ? FACE_STYLE : '')
|
|
69
|
+
+ CHAT_STYLE
|
|
70
|
+
}
|
|
71
|
+
const sourceKind = groupId ? 'qq-poke' : 'qq-private'
|
|
72
|
+
await agent.followup({
|
|
73
|
+
id: randomUUID(),
|
|
74
|
+
role: 'user',
|
|
75
|
+
content: [{ type: 'text', text: prompt }],
|
|
76
|
+
source: { kind: sourceKind },
|
|
77
|
+
})
|
|
78
|
+
diag('戳一戳 → 会话:' + (groupId ? '群 ' + groupId : '私聊') + ' ' + sender)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function startPokeWatcher(ctx) {
|
|
82
|
+
startWsWatcher(ctx, '戳一戳', (cfg) => cfg.pokeEnabled, (ev) => {
|
|
83
|
+
if (ev.post_type === 'notice' && ev.notice_type === 'notify' && ev.sub_type === 'poke') {
|
|
84
|
+
return handlePoke(ctx, ev)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// 私聊消息唤醒。
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import { loadCfg, allowedPrivate, diag } from '../config.js'
|
|
5
|
+
import { onebot } from '../onebot.js'
|
|
6
|
+
import { clean, segText, animatedSeg } from '../utils.js'
|
|
7
|
+
import { hasActiveTool, targetSessionId, findAgent } from '../session.js'
|
|
8
|
+
import { formatIncomingLine, favoriteHint, FACE_STYLE, CHAT_STYLE } from '../prompts.js'
|
|
9
|
+
import { getSelfId } from '../qq-state.js'
|
|
10
|
+
import { startWsWatcher } from './ws.js'
|
|
11
|
+
|
|
12
|
+
const privateSeen = new Set()
|
|
13
|
+
|
|
14
|
+
async function handlePrivateMessage(ctx, ev, admitImages) {
|
|
15
|
+
const cfg = loadCfg()
|
|
16
|
+
if (!cfg.privateEnabled) return
|
|
17
|
+
const self = String(ev.self_id || await getSelfId(cfg) || '')
|
|
18
|
+
const userId = String(ev.user_id || (ev.sender && ev.sender.user_id) || '')
|
|
19
|
+
if (!userId) return
|
|
20
|
+
if (self && userId === self) return
|
|
21
|
+
if (!allowedPrivate(userId, cfg)) return
|
|
22
|
+
const key = String(ev.message_id != null ? ev.message_id : [ev.time, userId].join('|'))
|
|
23
|
+
if (privateSeen.has(key)) return
|
|
24
|
+
privateSeen.add(key)
|
|
25
|
+
if (privateSeen.size > 500) privateSeen.clear()
|
|
26
|
+
const agent = findAgent(ctx.agents, targetSessionId())
|
|
27
|
+
if (!agent) {
|
|
28
|
+
diag('私聊:目标会话没有活着')
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const name = clean((ev.sender && (ev.sender.card || ev.sender.nickname)) || userId)
|
|
32
|
+
const segs = Array.isArray(ev.message) ? ev.message : []
|
|
33
|
+
const text = segText(segs) || clean(ev.raw_message) || '(没有文字)'
|
|
34
|
+
const groupId = ev.group_id ? String(ev.group_id) : ''
|
|
35
|
+
const midHint = ev.message_id != null ? ',引用传 reply_to=' + ev.message_id : ''
|
|
36
|
+
const actions = []
|
|
37
|
+
if (hasActiveTool('qq_get_private_history')) {
|
|
38
|
+
actions.push('想看上下文用 qq_get_private_history(user_id=' + userId + ')')
|
|
39
|
+
}
|
|
40
|
+
if (hasActiveTool('qq_send_private_message')) {
|
|
41
|
+
actions.push('想回就用 qq_send_private_message(user_id=' + userId + (groupId ? ', group_id=' + groupId : '') + midHint + ')')
|
|
42
|
+
}
|
|
43
|
+
if (hasActiveTool('qq_send_face')) {
|
|
44
|
+
actions.push('想回表情用 qq_send_face(user_id=' + userId + (groupId ? ', group_id=' + groupId : '') + midHint + ')')
|
|
45
|
+
}
|
|
46
|
+
const where = groupId ? '群 ' + groupId + ' 的临时会话' : '私聊'
|
|
47
|
+
const favHint = hasActiveTool('qq_favorite_face') ? favoriteHint(text, ev.message_id) : ''
|
|
48
|
+
const faceHint = hasActiveTool('qq_send_face') ? FACE_STYLE : ''
|
|
49
|
+
const prompt =
|
|
50
|
+
'【QQ ' + where + '】' + formatIncomingLine(name, userId, text, ev.message_id) +
|
|
51
|
+
'\n' + (actions.length ? actions.join(';') + '。' : '') + favHint + faceHint +
|
|
52
|
+
'不想回就安静待着;不要提工具。' + CHAT_STYLE
|
|
53
|
+
const content = [{ type: 'text', text: prompt }]
|
|
54
|
+
if (typeof admitImages === 'function') {
|
|
55
|
+
// 和群/私聊记录一样:先保证最近两张动图能进来,剩下的名额给静图。
|
|
56
|
+
const anim = []
|
|
57
|
+
const stat = []
|
|
58
|
+
for (const seg of segs) {
|
|
59
|
+
if (!seg || (seg.type !== 'image' && seg.type !== 'mface') || !seg.data) continue
|
|
60
|
+
const item = { seg, animated: animatedSeg(seg) }
|
|
61
|
+
if (item.animated) anim.push(item)
|
|
62
|
+
else stat.push(item)
|
|
63
|
+
}
|
|
64
|
+
const animPick = anim.slice(-2)
|
|
65
|
+
const picked = [...animPick, ...stat.slice(-(4 - animPick.length))]
|
|
66
|
+
if (picked.length) {
|
|
67
|
+
const images = await admitImages(picked.map(({ seg, animated }, i) => ({
|
|
68
|
+
data: seg.data,
|
|
69
|
+
cfg,
|
|
70
|
+
animated,
|
|
71
|
+
name: (seg.data && seg.data.file) || `qq-private-${ev.message_id || 'x'}-${i}.jpg`,
|
|
72
|
+
caption: `私聊 ${name} 发的图片(消息 ${ev.message_id || '?'})`,
|
|
73
|
+
})))
|
|
74
|
+
for (const img of images) {
|
|
75
|
+
if (img && img.caption) content.push({ type: 'text', text: img.caption })
|
|
76
|
+
if (img && img.block) content.push(img.block)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await agent.followup({
|
|
81
|
+
id: randomUUID(),
|
|
82
|
+
role: 'user',
|
|
83
|
+
content,
|
|
84
|
+
source: { kind: 'qq-private' },
|
|
85
|
+
})
|
|
86
|
+
diag('私聊消息 → 会话:' + userId + ' ' + text.slice(0, 200))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function startPrivateWatcher(ctx, admitImages) {
|
|
90
|
+
startWsWatcher(ctx, '私聊', (cfg) => cfg.privateEnabled, (ev) => {
|
|
91
|
+
if (ev.post_type === 'message' && ev.message_type === 'private') {
|
|
92
|
+
return handlePrivateMessage(ctx, ev, admitImages)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// 通用 OneBot WebSocket 连接与重连。
|
|
2
|
+
|
|
3
|
+
import { loadCfg, diag } from '../config.js'
|
|
4
|
+
|
|
5
|
+
export function startWsWatcher(ctx, tag, enabled, onEvent) {
|
|
6
|
+
let ws = null
|
|
7
|
+
let stopped = false
|
|
8
|
+
let timer = null
|
|
9
|
+
|
|
10
|
+
function connectWs() {
|
|
11
|
+
if (stopped) return
|
|
12
|
+
const cfg = loadCfg()
|
|
13
|
+
if (!enabled(cfg)) return
|
|
14
|
+
const url = String(cfg.wsUrl || '')
|
|
15
|
+
if (!url) return
|
|
16
|
+
if (typeof WebSocket === 'undefined') {
|
|
17
|
+
diag(tag + ':WS 不可用,当前 Node 没有 WebSocket')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
let targetUrl = url
|
|
22
|
+
if (cfg.accessToken) {
|
|
23
|
+
const u = new URL(url)
|
|
24
|
+
u.searchParams.set('access_token', cfg.accessToken)
|
|
25
|
+
targetUrl = u.toString()
|
|
26
|
+
}
|
|
27
|
+
const socket = new WebSocket(targetUrl)
|
|
28
|
+
ws = socket
|
|
29
|
+
socket.onopen = () => diag(tag + ':WS 已连接')
|
|
30
|
+
socket.onmessage = (event) => {
|
|
31
|
+
try {
|
|
32
|
+
const ev = JSON.parse(typeof event.data === 'string' ? event.data : String(event.data))
|
|
33
|
+
if (ev) {
|
|
34
|
+
void Promise.resolve(onEvent(ev, cfg)).catch((error) =>
|
|
35
|
+
diag(tag + ':处理事件失败 ' + String((error && error.message) || error)),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {}
|
|
39
|
+
}
|
|
40
|
+
socket.onclose = () => {
|
|
41
|
+
if (ws === socket) ws = null
|
|
42
|
+
if (!stopped) timer = setTimeout(connectWs, 5000)
|
|
43
|
+
}
|
|
44
|
+
socket.onerror = () => {
|
|
45
|
+
try { socket.close() } catch (error) {}
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
diag(tag + ':WS 连接失败 ' + String((error && error.message) || error))
|
|
49
|
+
if (!stopped) timer = setTimeout(connectWs, 5000)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ctx.effect(() => {
|
|
54
|
+
connectWs()
|
|
55
|
+
return () => {
|
|
56
|
+
stopped = true
|
|
57
|
+
if (timer) clearTimeout(timer)
|
|
58
|
+
try { if (ws) ws.close() } catch (error) {}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,103 +1,98 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-qq-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "给大肥鱼会话用的 QQ
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"author": "cyhddyx",
|
|
9
|
-
"homepage": "https://github.com/cyhddyx/dsh-qq-tools#readme",
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/cyhddyx/dsh-qq-tools.git"
|
|
13
|
-
},
|
|
14
|
-
"bugs": {
|
|
15
|
-
"url": "https://github.com/cyhddyx/dsh-qq-tools/issues"
|
|
16
|
-
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"dsh",
|
|
19
|
-
"dsh-plugin",
|
|
20
|
-
"deepseek-harness",
|
|
21
|
-
"qq",
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"./
|
|
45
|
-
"./tools/qq-list-groups": "./tools/qq-list-groups/
|
|
46
|
-
"./tools/qq-
|
|
47
|
-
"./tools/qq-
|
|
48
|
-
"./tools/qq-get-group-history": "./tools/qq-get-group-history/
|
|
49
|
-
"./tools/qq-get-group-
|
|
50
|
-
"./tools/qq-get-group-
|
|
51
|
-
"./tools/qq-get-group-members": "./tools/qq-get-group-members/
|
|
52
|
-
"./tools/qq-
|
|
53
|
-
"./tools/qq-
|
|
54
|
-
"./tools/qq-send-group-message": "./tools/qq-send-group-message/
|
|
55
|
-
"./tools/qq-send-
|
|
56
|
-
"./tools/qq-send-
|
|
57
|
-
"./tools/qq-send-face": "./tools/qq-send-face/
|
|
58
|
-
"./tools/qq-
|
|
59
|
-
"./tools/qq-
|
|
60
|
-
"./tools/qq-favorite-faces": "./tools/qq-favorite-faces/
|
|
61
|
-
"./tools/qq-
|
|
62
|
-
"./tools/qq-
|
|
63
|
-
"./tools/qq-private-chat": "./tools/qq-private-chat/
|
|
64
|
-
"./tools/qq-
|
|
65
|
-
"./tools/qq-
|
|
66
|
-
"./tools/qq-send-poke": "./tools/qq-send-poke/
|
|
67
|
-
"./tools/qq-
|
|
68
|
-
"./tools/qq-
|
|
69
|
-
"./tools/qq-set-avatar": "./tools/qq-set-avatar/
|
|
70
|
-
"./tools/qq-
|
|
71
|
-
"./tools/qq-
|
|
72
|
-
"./tools/qq-friend-management": "./tools/qq-friend-management/
|
|
73
|
-
"./tools/qq-
|
|
74
|
-
"./tools/qq-
|
|
75
|
-
"./tools/qq-qzone": "./tools/qq-qzone/
|
|
76
|
-
"./
|
|
77
|
-
"./
|
|
78
|
-
"./
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"@deepseek-ai/dsh-client-
|
|
93
|
-
"@deepseek-ai/dsh-client-ui-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-qq-tools",
|
|
3
|
+
"version": "0.10.2",
|
|
4
|
+
"description": "给大肥鱼会话用的 QQ 群/私聊工具(只支持 SnowLuma,其他 OneBot 实现包括 NapCat 都不支持):列群、看群记录、发群消息(可引用/@)、发表情、收藏表情(SnowLuma 收藏夹)、好友/私聊记录、发私聊消息、私聊唤醒、群成员、戳一戳感知/戳回去、更改头像、好友管理(资料/备注/分组/删好友/名片赞/好友申请)、QQ 空间说说(列表/详情/发表/评论/点赞/权限)、挂载会话选择。11 个工具组加 1 个设置组件,可在插件页逐个开关。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "cyhddyx",
|
|
9
|
+
"homepage": "https://github.com/cyhddyx/dsh-qq-tools#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/cyhddyx/dsh-qq-tools.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/cyhddyx/dsh-qq-tools/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"dsh",
|
|
19
|
+
"dsh-plugin",
|
|
20
|
+
"deepseek-harness",
|
|
21
|
+
"qq",
|
|
22
|
+
"snowluma"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
"lib",
|
|
26
|
+
"tools",
|
|
27
|
+
"locale",
|
|
28
|
+
"cordis.patch.yml",
|
|
29
|
+
"README.md",
|
|
30
|
+
"SECURITY.md",
|
|
31
|
+
"docs/verification",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node test/smoke.mjs"
|
|
39
|
+
},
|
|
40
|
+
"exports": {
|
|
41
|
+
".": "./lib/index.js",
|
|
42
|
+
"./client": "./lib/client.js",
|
|
43
|
+
"./tools/qq-list-groups": "./tools/qq-list-groups/index.js",
|
|
44
|
+
"./tools/qq-list-groups/locale/en.json": "./tools/qq-list-groups/locale/en.json",
|
|
45
|
+
"./tools/qq-list-groups/locale/zh.json": "./tools/qq-list-groups/locale/zh.json",
|
|
46
|
+
"./tools/qq-get-group-history": "./tools/qq-get-group-history/index.js",
|
|
47
|
+
"./tools/qq-get-group-history/locale/en.json": "./tools/qq-get-group-history/locale/en.json",
|
|
48
|
+
"./tools/qq-get-group-history/locale/zh.json": "./tools/qq-get-group-history/locale/zh.json",
|
|
49
|
+
"./tools/qq-get-group-members": "./tools/qq-get-group-members/index.js",
|
|
50
|
+
"./tools/qq-get-group-members/locale/en.json": "./tools/qq-get-group-members/locale/en.json",
|
|
51
|
+
"./tools/qq-get-group-members/locale/zh.json": "./tools/qq-get-group-members/locale/zh.json",
|
|
52
|
+
"./tools/qq-send-group-message": "./tools/qq-send-group-message/index.js",
|
|
53
|
+
"./tools/qq-send-group-message/locale/en.json": "./tools/qq-send-group-message/locale/en.json",
|
|
54
|
+
"./tools/qq-send-group-message/locale/zh.json": "./tools/qq-send-group-message/locale/zh.json",
|
|
55
|
+
"./tools/qq-send-face": "./tools/qq-send-face/index.js",
|
|
56
|
+
"./tools/qq-send-face/locale/en.json": "./tools/qq-send-face/locale/en.json",
|
|
57
|
+
"./tools/qq-send-face/locale/zh.json": "./tools/qq-send-face/locale/zh.json",
|
|
58
|
+
"./tools/qq-favorite-faces": "./tools/qq-favorite-faces/index.js",
|
|
59
|
+
"./tools/qq-favorite-faces/locale/en.json": "./tools/qq-favorite-faces/locale/en.json",
|
|
60
|
+
"./tools/qq-favorite-faces/locale/zh.json": "./tools/qq-favorite-faces/locale/zh.json",
|
|
61
|
+
"./tools/qq-private-chat": "./tools/qq-private-chat/index.js",
|
|
62
|
+
"./tools/qq-private-chat/locale/en.json": "./tools/qq-private-chat/locale/en.json",
|
|
63
|
+
"./tools/qq-private-chat/locale/zh.json": "./tools/qq-private-chat/locale/zh.json",
|
|
64
|
+
"./tools/qq-send-poke": "./tools/qq-send-poke/index.js",
|
|
65
|
+
"./tools/qq-send-poke/locale/en.json": "./tools/qq-send-poke/locale/en.json",
|
|
66
|
+
"./tools/qq-send-poke/locale/zh.json": "./tools/qq-send-poke/locale/zh.json",
|
|
67
|
+
"./tools/qq-set-avatar": "./tools/qq-set-avatar/index.js",
|
|
68
|
+
"./tools/qq-set-avatar/locale/en.json": "./tools/qq-set-avatar/locale/en.json",
|
|
69
|
+
"./tools/qq-set-avatar/locale/zh.json": "./tools/qq-set-avatar/locale/zh.json",
|
|
70
|
+
"./tools/qq-friend-management": "./tools/qq-friend-management/index.js",
|
|
71
|
+
"./tools/qq-friend-management/locale/en.json": "./tools/qq-friend-management/locale/en.json",
|
|
72
|
+
"./tools/qq-friend-management/locale/zh.json": "./tools/qq-friend-management/locale/zh.json",
|
|
73
|
+
"./tools/qq-qzone": "./tools/qq-qzone/index.js",
|
|
74
|
+
"./tools/qq-qzone/locale/en.json": "./tools/qq-qzone/locale/en.json",
|
|
75
|
+
"./tools/qq-qzone/locale/zh.json": "./tools/qq-qzone/locale/zh.json",
|
|
76
|
+
"./locale/en.json": "./locale/en.json",
|
|
77
|
+
"./locale/zh.json": "./locale/zh.json",
|
|
78
|
+
"./package.json": "./package.json"
|
|
79
|
+
},
|
|
80
|
+
"dsh": {
|
|
81
|
+
"compatibility": {
|
|
82
|
+
"dsh": "0.2.x",
|
|
83
|
+
"node": ">=22"
|
|
84
|
+
},
|
|
85
|
+
"bundle": {
|
|
86
|
+
"patch": "./cordis.patch.yml"
|
|
87
|
+
},
|
|
88
|
+
"client": {
|
|
89
|
+
"inject": [
|
|
90
|
+
"@deepseek-ai/dsh-client-locale",
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
92
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
93
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
94
|
+
],
|
|
95
|
+
"platform": "web"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
}
|
|
1
|
+
import { makeListFacesTool, makeFavoriteFaceTool } from '../../lib/tools/faces.js'
|
|
2
|
+
import { mountOneTool } from '../../lib/session.js'
|
|
3
|
+
|
|
4
|
+
export const name = 'qq-tools-favorite-faces'
|
|
5
|
+
export const inject = ['agents', 'timer', 'tools']
|
|
6
|
+
|
|
7
|
+
export function apply(ctx) {
|
|
8
|
+
const list = mountOneTool(ctx, makeListFacesTool())
|
|
9
|
+
const fav = mountOneTool(ctx, makeFavoriteFaceTool())
|
|
10
|
+
return { tools: [list, fav] }
|
|
11
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Favorite faces",
|
|
4
|
-
"description": "Manage your QQ account's saved faces via SnowLuma: list, add from an image or message, remove, rename, and move to front."
|
|
4
|
+
"description": "Manage your QQ account's saved faces via SnowLuma: list emoji_id and text notes, add from an image or message, remove, rename, and move to front."
|
|
5
5
|
}
|
|
6
6
|
}
|