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.
Files changed (41) hide show
  1. package/README.md +23 -11
  2. package/cordis.patch.yml +1 -1
  3. package/lib/config.js +96 -0
  4. package/lib/images.js +420 -0
  5. package/lib/index.js +2 -1
  6. package/lib/mount-all.js +59 -0
  7. package/lib/observation.js +24 -0
  8. package/lib/onebot.js +32 -0
  9. package/lib/prompts.js +56 -0
  10. package/lib/qq-state.js +51 -0
  11. package/lib/render.js +49 -0
  12. package/lib/session.js +201 -0
  13. package/lib/shared.js +26 -3377
  14. package/lib/tools/avatar.js +54 -0
  15. package/lib/tools/faces.js +296 -0
  16. package/lib/tools/friends.js +559 -0
  17. package/lib/tools/groups.js +220 -0
  18. package/lib/tools/poke.js +41 -0
  19. package/lib/tools/private.js +190 -0
  20. package/lib/tools/qzone.js +569 -0
  21. package/lib/utils.js +73 -0
  22. package/lib/watchers/mentions.js +167 -0
  23. package/lib/watchers/poke.js +87 -0
  24. package/lib/watchers/private.js +95 -0
  25. package/lib/watchers/ws.js +61 -0
  26. package/package.json +98 -103
  27. package/tools/qq-favorite-faces/index.js +11 -10
  28. package/tools/qq-favorite-faces/locale/en.json +1 -1
  29. package/tools/qq-favorite-faces/locale/zh.json +1 -1
  30. package/tools/qq-friend-management/index.js +28 -28
  31. package/tools/qq-get-group-history/index.js +10 -8
  32. package/tools/qq-get-group-members/index.js +9 -8
  33. package/tools/qq-list-groups/index.js +9 -8
  34. package/tools/qq-private-chat/index.js +16 -20
  35. package/tools/qq-qzone/index.js +22 -22
  36. package/tools/qq-send-face/index.js +11 -8
  37. package/tools/qq-send-face/locale/en.json +1 -1
  38. package/tools/qq-send-face/locale/zh.json +1 -1
  39. package/tools/qq-send-group-message/index.js +13 -10
  40. package/tools/qq-send-poke/index.js +13 -10
  41. package/tools/qq-set-avatar/index.js +9 -8
@@ -0,0 +1,220 @@
1
+ // 群相关工具:列群、看群记录、看群成员、发群消息。
2
+
3
+ import { loadCfg, allowedGroup } from '../config.js'
4
+ import { onebot } from '../onebot.js'
5
+ import { clean, segText, animatedSeg, fmtTime, senderName, senderId, pickChatImages, CHAT_IMAGE_LIMIT } from '../utils.js'
6
+ import { listOf, renderTool } from '../render.js'
7
+ import { CHAT_STYLE, chatText } from '../prompts.js'
8
+
9
+ export function makeListTool() {
10
+ return {
11
+ name: 'qq_list_groups',
12
+ description:
13
+ '看看你所在的 QQ 群。返回允许你查看/发言的群号、群名、人数。' +
14
+ '群聊里想说话时先用这个挑群;群号可以直接传给 qq_get_group_history / qq_send_group_message。',
15
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
16
+ output: { schema: { type: 'object', additionalProperties: true }, render: renderTool },
17
+ async execute() {
18
+ const cfg = loadCfg()
19
+ try {
20
+ const data = await onebot('get_group_list', {}, cfg)
21
+ const groups = (Array.isArray(data) ? data : [])
22
+ .map((g) => ({
23
+ id: Number(g.group_id),
24
+ name: clean(g.group_name || g.group_id || ''),
25
+ member_count: Number(g.member_count || 0),
26
+ }))
27
+ .filter((g) => Number.isFinite(g.id) && allowedGroup(g.id, cfg))
28
+ return { ok: true, groups }
29
+ } catch (error) {
30
+ return { ok: false, error: String((error && error.message) || error) }
31
+ }
32
+ },
33
+ render(_args, v) {
34
+ if (!v || !v.ok) return [{ type: 'text', text: '看群列表失败:' + String((v && v.error) || '未知错误') }]
35
+ if (!v.groups.length) return [{ type: 'text', text: '现在没有允许查看的群。' }]
36
+ return [{
37
+ type: 'text',
38
+ text: '可以去的群:\n' + v.groups.map((g) => `- ${g.name || g.id}(群号 ${g.id},${g.member_count} 人)`).join('\n'),
39
+ }]
40
+ },
41
+ }
42
+ }
43
+
44
+ export function makeHistoryTool(admitImages) {
45
+ return {
46
+ name: 'qq_get_group_history',
47
+ description:
48
+ '看某个群最近的聊天记录(只读)。默认最近 30 条,最多 50 条。' +
49
+ '有人 @ 你时,可以用该工具看个@你的群,再进行回复。' +
50
+ '群号从 qq_list_groups 或 @ 提醒里拿。消息前面的 #编号 是 message_id,' +
51
+ '可以用 qq_send_face 回复,或用 qq_favorite_face 收藏消息里的图片。',
52
+ parameters: {
53
+ type: 'object',
54
+ properties: {
55
+ group_id: { type: 'number', description: '群号' },
56
+ count: { type: 'number', description: '看最近多少条,默认 30,最多 50' },
57
+ include_images: { type: 'boolean', description: '要不要把最近的图片也给你看(最多 ' + CHAT_IMAGE_LIMIT + ' 张,默认 true)' },
58
+ },
59
+ required: ['group_id'],
60
+ additionalProperties: false,
61
+ },
62
+ output: { schema: { type: 'object', additionalProperties: true }, render: renderTool },
63
+ async execute(args) {
64
+ const cfg = loadCfg()
65
+ const groupId = Number(args && args.group_id)
66
+ if (!Number.isFinite(groupId)) return { ok: false, error: 'group_id 不是有效群号' }
67
+ if (!allowedGroup(groupId, cfg)) return { ok: false, error: '这个群不在允许列表里' }
68
+ const count = Math.max(1, Math.min(Number(args && args.count) || cfg.historyCount, cfg.maxHistoryCount))
69
+ try {
70
+ const data = await onebot('get_group_msg_history', { group_id: groupId, count }, cfg)
71
+ const raw = listOf(data, 'messages')
72
+ const messages = raw
73
+ .map((m) => ({
74
+ time: Number(m.time || 0),
75
+ t: fmtTime(m.time),
76
+ sender: senderName(m),
77
+ user_id: senderId(m),
78
+ text: segText(m.message),
79
+ message_id: m.message_id,
80
+ }))
81
+ .sort((a, b) => a.time - b.time)
82
+ let images = []
83
+ if (args && args.include_images !== false) {
84
+ const picked = []
85
+ for (const m of raw.slice().sort((a, b) => (a.time || 0) - (b.time || 0))) {
86
+ const segs = Array.isArray(m.message) ? m.message : []
87
+ for (const seg of segs) {
88
+ if (!seg || (seg.type !== 'image' && seg.type !== 'mface') || !seg.data) continue
89
+ picked.push({ m, seg, animated: animatedSeg(seg) })
90
+ }
91
+ }
92
+ const last = pickChatImages(picked)
93
+ if (last.length && typeof admitImages === 'function') {
94
+ images = await admitImages(last.map(({ m, seg, animated }, i) => ({
95
+ data: seg.data,
96
+ cfg,
97
+ animated,
98
+ name: (seg.data && seg.data.file) || `qq-${m.message_id || 'x'}-${i}.jpg`,
99
+ caption: `群 ${groupId} ${senderName(m)} 发的图片(消息 ${m.message_id || '?'})`,
100
+ })))
101
+ }
102
+ }
103
+ return { ok: true, group_id: groupId, count: messages.length, messages, images }
104
+ } catch (error) {
105
+ return { ok: false, error: String((error && error.message) || error) }
106
+ }
107
+ },
108
+ render(_args, v) {
109
+ if (!v || !v.ok) return [{ type: 'text', text: '看群记录失败:' + String((v && v.error) || '未知错误') }]
110
+ if (!v.messages.length) return [{ type: 'text', text: `群 ${v.group_id} 最近没有消息。` }]
111
+ const lines = v.messages.map((m) => `${m.t || ''} [#${m.message_id || '?'}] ${m.sender}(${m.user_id}):${m.text || '(空)'}`)
112
+ return [{ type: 'text', text: `群 ${v.group_id} 最近 ${v.count} 条:\n` + lines.join('\n') }]
113
+ },
114
+ }
115
+ }
116
+
117
+ export function makeMembersTool() {
118
+ return {
119
+ name: 'qq_get_group_members',
120
+ description: '看看某个群的成员(昵称、群名片、QQ 号、身份)。需要知道谁是谁时用。',
121
+ parameters: {
122
+ type: 'object',
123
+ properties: {
124
+ group_id: { type: 'number', description: '群号' },
125
+ },
126
+ required: ['group_id'],
127
+ additionalProperties: false,
128
+ },
129
+ output: { schema: { type: 'object', additionalProperties: true }, render: renderTool },
130
+ async execute(args) {
131
+ const cfg = loadCfg()
132
+ const groupId = Number(args && args.group_id)
133
+ if (!Number.isFinite(groupId)) return { ok: false, error: 'group_id 不是有效群号' }
134
+ if (!allowedGroup(groupId, cfg)) return { ok: false, error: '这个群不在允许列表里' }
135
+ try {
136
+ const data = await onebot('get_group_member_list', { group_id: groupId }, cfg)
137
+ const members = listOf(data, 'members').slice(0, 100).map((m) => ({
138
+ user_id: String(m.user_id || ''),
139
+ nickname: String(m.nickname || ''),
140
+ card: String(m.card || ''),
141
+ role: String(m.role || ''),
142
+ }))
143
+ return { ok: true, group_id: groupId, members }
144
+ } catch (error) {
145
+ return { ok: false, error: String((error && error.message) || error) }
146
+ }
147
+ },
148
+ render(_args, v) {
149
+ if (!v || !v.ok) return [{ type: 'text', text: '看群成员失败:' + String((v && v.error) || '未知错误') }]
150
+ const lines = v.members.map((m) => `- ${m.card || m.nickname || m.user_id}(QQ ${m.user_id},${m.role || 'member'})`)
151
+ return [{ type: 'text', text: `群 ${v.group_id} 成员:\n` + (lines.join('\n') || '(空)') }]
152
+ },
153
+ }
154
+ }
155
+
156
+ export function makeSendTool(onSent) {
157
+ return {
158
+ name: 'qq_send_group_message',
159
+ description:
160
+ '在指定群里发一条消息。发之前先确认群号;只发到允许的群。想分几条说就多次调用本工具,一次一条。' +
161
+ 'reply_to 传消息前面的 #编号 可以引用那条消息;at_user_ids 传 QQ 号可以 @ 人,at_all=true 是 @全体成员。' +
162
+ '如果有用的表情可以表达自己的想法的话,也可以发表情加句子会更加生动,表情可以调用qq_send_face发送'+
163
+ CHAT_STYLE +
164
+ '不想说话就不要调用这个工具;调用后不要再重复说自己发了什么。',
165
+ parameters: {
166
+ type: 'object',
167
+ properties: {
168
+ group_id: { type: 'number', description: '群号' },
169
+ message: { type: 'string', description: '这一条的文字,一句短话;只 @ 不说话时可以留空' },
170
+ reply_to: { type: 'string', description: '要引用的消息 ID(消息前面的 #编号),可选', },
171
+ at_user_ids: { type: 'array', items: { type: 'number' }, description: '要 @ 的 QQ 号列表,可选', },
172
+ at_all: { type: 'boolean', description: 'true = @全体成员,可选' },
173
+ },
174
+ required: ['group_id'],
175
+ additionalProperties: false,
176
+ },
177
+ output: { schema: { type: 'object', additionalProperties: true }, render: renderTool },
178
+ async execute(args) {
179
+ const cfg = loadCfg()
180
+ const groupId = Number(args && args.group_id)
181
+ const text = chatText(args)
182
+ const atIds = [...new Set(
183
+ (Array.isArray(args && args.at_user_ids) ? args.at_user_ids : [])
184
+ .map((x) => Number(x)).filter((x) => Number.isInteger(x) && x > 0),
185
+ )].slice(0, 20)
186
+ const atAll = args && args.at_all === true
187
+ if (!Number.isFinite(groupId)) return { ok: false, error: 'group_id 不是有效群号' }
188
+ if (!text && !atIds.length && !atAll) return { ok: false, error: '消息是空的' }
189
+ if (!allowedGroup(groupId, cfg)) return { ok: false, error: '这个群不在允许列表里' }
190
+ try {
191
+ const replyTo = args && args.reply_to
192
+ const message = []
193
+ if (replyTo != null && replyTo !== '') message.push({ type: 'reply', data: { id: String(replyTo) } })
194
+ if (atAll) message.push({ type: 'at', data: { qq: 'all' } })
195
+ else for (const qq of atIds) message.push({ type: 'at', data: { qq: String(qq) } })
196
+ if (text) message.push({ type: 'text', data: { text } })
197
+ const data = await onebot('send_group_msg', { group_id: groupId, message }, cfg)
198
+ try { if (typeof onSent === 'function') onSent(groupId, '发群消息') } catch (error) {}
199
+ return {
200
+ ok: true,
201
+ group_id: groupId,
202
+ message_id: data && data.message_id,
203
+ reply_to: replyTo != null && replyTo !== '' ? String(replyTo) : '',
204
+ at_user_ids: atAll ? [] : atIds,
205
+ at_all: atAll,
206
+ }
207
+ } catch (error) {
208
+ return { ok: false, error: String((error && error.message) || error) }
209
+ }
210
+ },
211
+ render(_args, v) {
212
+ if (!v || !v.ok) return [{ type: 'text', text: '发群消息失败:' + String((v && v.error) || '未知错误') }]
213
+ const extra = []
214
+ if (v.reply_to) extra.push('引用了 #' + v.reply_to)
215
+ if (v.at_all) extra.push('@全体成员')
216
+ else if (Array.isArray(v.at_user_ids) && v.at_user_ids.length) extra.push('@了 ' + v.at_user_ids.join('、'))
217
+ return [{ type: 'text', text: `已发到群 ${v.group_id}(消息 ${v.message_id || '?'}${extra.length ? ',' + extra.join(',') : ''})。` }]
218
+ },
219
+ }
220
+ }
@@ -0,0 +1,41 @@
1
+ // 戳一戳工具。
2
+
3
+ import { loadCfg, allowedGroup, allowedPrivate } from '../config.js'
4
+ import { onebot } from '../onebot.js'
5
+ import { renderTool } from '../render.js'
6
+
7
+ export function makePokeTool(onSent) {
8
+ return {
9
+ name: 'qq_send_poke',
10
+ description:
11
+ '戳一戳某人。群聊传 group_id + user_id;私聊只传 user_id。' +
12
+ '别人戳你时可以戳回去,但别频繁;戳完不用再解释。',
13
+ parameters: {
14
+ type: 'object',
15
+ properties: {
16
+ user_id: { type: 'number', description: '对方的 QQ 号' },
17
+ group_id: { type: 'number', description: '群号;私聊不传' },
18
+ },
19
+ required: ['user_id'],
20
+ additionalProperties: false,
21
+ },
22
+ output: { schema: { type: 'object', additionalProperties: true }, render: renderTool },
23
+ async execute(args) {
24
+ const cfg = loadCfg()
25
+ const userId = Number(args && args.user_id)
26
+ const groupId = args && args.group_id != null ? Number(args.group_id) : 0
27
+ if (!Number.isFinite(userId)) return { ok: false, error: 'user_id 不是有效 QQ 号' }
28
+ if (groupId && !allowedGroup(groupId, cfg)) return { ok: false, error: '这个群不在允许列表里' }
29
+ if (!groupId && !allowedPrivate(userId, cfg)) return { ok: false, error: '这个 QQ 不在允许列表里' }
30
+ try {
31
+ const body = { user_id: userId }
32
+ if (groupId) body.group_id = groupId
33
+ await onebot('send_poke', body, cfg)
34
+ try { if (groupId && typeof onSent === 'function') onSent(groupId, '戳回去') } catch (error) {}
35
+ return { ok: true, poke: true, user_id: userId, group_id: groupId || null }
36
+ } catch (error) {
37
+ return { ok: false, error: String((error && error.message) || error) }
38
+ }
39
+ },
40
+ }
41
+ }
@@ -0,0 +1,190 @@
1
+ // 私聊工具:好友列表、私聊记录、发私聊消息。
2
+
3
+ import { loadCfg, allowedPrivate } from '../config.js'
4
+ import { onebot } from '../onebot.js'
5
+ import { clean, segText, animatedSeg, fmtTime, senderName, senderId, pickChatImages, CHAT_IMAGE_LIMIT } from '../utils.js'
6
+ import { listOf, renderTool } from '../render.js'
7
+ import { CHAT_STYLE, chatText } from '../prompts.js'
8
+ import { getSelfId } from '../qq-state.js'
9
+
10
+ // ---------- 私聊:好友列表 / 私聊记录 / 发私聊消息 ----------
11
+ export function makeListFriendsTool() {
12
+ return {
13
+ name: 'qq_list_friends',
14
+ description:
15
+ '看看你的 QQ 好友列表。想私聊谁时先用这个拿 QQ 号;可以按昵称、备注或 QQ 号搜。' +
16
+ '私聊消息唤醒里也会直接给出发送者的 QQ 号。',
17
+ parameters: {
18
+ type: 'object',
19
+ properties: {
20
+ query: { type: 'string', description: '按昵称、备注或 QQ 号模糊找;不传就列全部' },
21
+ count: { type: 'number', description: '最多列多少个,默认 100,最多 500' },
22
+ },
23
+ additionalProperties: false,
24
+ },
25
+ output: { schema: { type: 'object', additionalProperties: true }, render: renderTool },
26
+ async execute(args) {
27
+ const cfg = loadCfg()
28
+ const q = String((args && args.query) || '').trim().toLowerCase()
29
+ const count = Math.max(1, Math.min(Number(args && args.count) || 100, 500))
30
+ try {
31
+ const data = await onebot('get_friend_list', {}, cfg)
32
+ const friends = (Array.isArray(data) ? data : [])
33
+ .map((f) => ({
34
+ user_id: String(f.user_id || ''),
35
+ nickname: clean(f.nickname),
36
+ remark: clean(f.remark),
37
+ }))
38
+ .filter((f) => f.user_id && allowedPrivate(f.user_id, cfg))
39
+ .filter((f) => !q || (f.nickname + ' ' + f.remark + ' ' + f.user_id).toLowerCase().includes(q))
40
+ return { ok: true, query: q, total: friends.length, friends: friends.slice(0, count) }
41
+ } catch (error) {
42
+ return { ok: false, error: String((error && error.message) || error) }
43
+ }
44
+ },
45
+ render(_args, v) {
46
+ if (!v || !v.ok) return [{ type: 'text', text: '看好友列表失败:' + String((v && v.error) || '未知错误') }]
47
+ if (!v.friends.length) return [{ type: 'text', text: v.query ? '没有匹配的好友。' : '好友列表是空的。' }]
48
+ const lines = v.friends.map((f) => `- ${f.remark || f.nickname || f.user_id}(${f.nickname || ''},QQ ${f.user_id})`)
49
+ const more = v.total > v.friends.length ? `,只列了前 ${v.friends.length} 个` : ''
50
+ return [{ type: 'text', text: `你的好友(${v.total} 个${more}):\n` + lines.join('\n') }]
51
+ },
52
+ }
53
+ }
54
+
55
+ export function makePrivateHistoryTool(admitImages) {
56
+ return {
57
+ name: 'qq_get_private_history',
58
+ description:
59
+ '看和某个 QQ 好友的私聊记录(只读)。默认最近 30 条,最多 50 条。' +
60
+ '消息前面的 #编号 是 message_id,可以用于回复或收藏图片。' +
61
+ 'user_id 从 qq_list_friends 或私聊唤醒提示里拿。',
62
+ parameters: {
63
+ type: 'object',
64
+ properties: {
65
+ user_id: { type: 'number', description: '对方的 QQ 号' },
66
+ count: { type: 'number', description: '看最近多少条,默认 30,最多 50' },
67
+ include_images: { type: 'boolean', description: '要不要把最近的图片也给你看(最多 ' + CHAT_IMAGE_LIMIT + ' 张,默认 true)' },
68
+ },
69
+ required: ['user_id'],
70
+ additionalProperties: false,
71
+ },
72
+ output: { schema: { type: 'object', additionalProperties: true }, render: renderTool },
73
+ async execute(args) {
74
+ const cfg = loadCfg()
75
+ const userId = Number(args && args.user_id)
76
+ if (!Number.isFinite(userId)) return { ok: false, error: 'user_id 不是有效 QQ 号' }
77
+ if (!allowedPrivate(userId, cfg)) return { ok: false, error: '这个 QQ 不在允许列表里' }
78
+ const count = Math.max(1, Math.min(Number(args && args.count) || cfg.historyCount, cfg.maxHistoryCount))
79
+ try {
80
+ const data = await onebot('get_friend_msg_history', { user_id: userId, count }, cfg)
81
+ const raw = listOf(data, 'messages')
82
+ const selfId = await getSelfId(cfg)
83
+ const messages = raw
84
+ .map((m) => {
85
+ const uid = senderId(m)
86
+ const isSelf = !!selfId && uid === selfId
87
+ return {
88
+ time: Number(m.time || 0),
89
+ t: fmtTime(m.time),
90
+ sender: isSelf ? '我' : senderName(m),
91
+ user_id: uid,
92
+ self: isSelf,
93
+ text: segText(m.message),
94
+ message_id: m.message_id,
95
+ }
96
+ })
97
+ .sort((a, b) => a.time - b.time)
98
+ let images = []
99
+ if (args && args.include_images !== false) {
100
+ const picked = []
101
+ for (const m of raw.slice().sort((a, b) => (a.time || 0) - (b.time || 0))) {
102
+ const segs = Array.isArray(m.message) ? m.message : []
103
+ for (const seg of segs) {
104
+ if (!seg || (seg.type !== 'image' && seg.type !== 'mface') || !seg.data) continue
105
+ picked.push({ m, seg, animated: animatedSeg(seg) })
106
+ }
107
+ }
108
+ const last = pickChatImages(picked)
109
+ if (last.length && typeof admitImages === 'function') {
110
+ images = await admitImages(last.map(({ m, seg, animated }, i) => ({
111
+ data: seg.data,
112
+ cfg,
113
+ animated,
114
+ name: (seg.data && seg.data.file) || `qq-private-${m.message_id || 'x'}-${i}.jpg`,
115
+ caption: `私聊 ${senderName(m)} 发的图片(消息 ${m.message_id || '?'})`,
116
+ })))
117
+ }
118
+ }
119
+ return { ok: true, user_id: userId, count: messages.length, messages, images }
120
+ } catch (error) {
121
+ return { ok: false, error: String((error && error.message) || error) }
122
+ }
123
+ },
124
+ render(_args, v) {
125
+ if (!v || !v.ok) return [{ type: 'text', text: '看私聊记录失败:' + String((v && v.error) || '未知错误') }]
126
+ if (!v.messages.length) return [{ type: 'text', text: `和 QQ ${v.user_id} 最近没有消息。` }]
127
+ const lines = v.messages.map((m) => `${m.t || ''} [#${m.message_id || '?'}] ${m.sender}(${m.user_id}):${m.text || '(空)'}`)
128
+ const blocks = [{ type: 'text', text: `和 QQ ${v.user_id} 最近 ${v.count} 条:\n` + lines.join('\n') }]
129
+ for (const img of Array.isArray(v.images) ? v.images : []) {
130
+ if (img && img.caption) blocks.push({ type: 'text', text: img.caption })
131
+ if (img && img.block) blocks.push(img.block)
132
+ }
133
+ return blocks
134
+ },
135
+ }
136
+ }
137
+
138
+ export function makeSendPrivateTool() {
139
+ return {
140
+ name: 'qq_send_private_message',
141
+ description:
142
+ '给某个 QQ 好友发一条私聊消息。user_id 从 qq_list_friends 或私聊唤醒提示里拿。想分几条说就多次调用本工具,一次一条。' +
143
+ 'reply_to 传消息前面的 #编号 可以引用那条消息。' +
144
+ CHAT_STYLE +
145
+ '不想说话就不要调用;调用后不要再重复说自己发了什么。',
146
+ parameters: {
147
+ type: 'object',
148
+ properties: {
149
+ user_id: { type: 'number', description: '对方的 QQ 号' },
150
+ message: { type: 'string', description: '这一条的文字,一句短话' },
151
+ reply_to: { type: 'string', description: '要引用的消息 ID(消息前面的 #编号),可选' },
152
+ group_id: { type: 'number', description: '临时会话的来源群号,可选;普通好友私聊不用传' },
153
+ },
154
+ required: ['user_id', 'message'],
155
+ additionalProperties: false,
156
+ },
157
+ output: { schema: { type: 'object', additionalProperties: true }, render: renderTool },
158
+ async execute(args) {
159
+ const cfg = loadCfg()
160
+ const userId = Number(args && args.user_id)
161
+ const text = chatText(args)
162
+ const groupId = args && args.group_id != null ? Number(args.group_id) : 0
163
+ if (!Number.isFinite(userId)) return { ok: false, error: 'user_id 不是有效 QQ 号' }
164
+ if (!text) return { ok: false, error: '消息是空的' }
165
+ if (!allowedPrivate(userId, cfg)) return { ok: false, error: '这个 QQ 不在允许列表里' }
166
+ try {
167
+ const replyTo = args && args.reply_to
168
+ const message = []
169
+ if (replyTo != null && replyTo !== '') message.push({ type: 'reply', data: { id: String(replyTo) } })
170
+ message.push({ type: 'text', data: { text } })
171
+ const body = { user_id: userId, message }
172
+ if (groupId) body.group_id = groupId
173
+ const data = await onebot('send_private_msg', body, cfg)
174
+ return {
175
+ ok: true,
176
+ user_id: userId,
177
+ message_id: data && data.message_id,
178
+ reply_to: replyTo != null && replyTo !== '' ? String(replyTo) : '',
179
+ }
180
+ } catch (error) {
181
+ return { ok: false, error: String((error && error.message) || error) }
182
+ }
183
+ },
184
+ render(_args, v) {
185
+ if (!v || !v.ok) return [{ type: 'text', text: '发私聊消息失败:' + String((v && v.error) || '未知错误') }]
186
+ const quote = v.reply_to ? ',引用了 #' + v.reply_to : ''
187
+ return [{ type: 'text', text: `已发给 QQ ${v.user_id}(消息 ${v.message_id || '?'}${quote})。` }]
188
+ },
189
+ }
190
+ }