@cmach_socket/opencode-qq-bot 0.0.0-stage → 0.2.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/LICENSE +443 -0
- package/README.md +178 -2
- package/SOUL.txt +8 -0
- package/package.json +47 -4
- package/src/chat.ts +44 -0
- package/src/config.ts +444 -0
- package/src/index.ts +245 -0
- package/src/instance.ts +60 -0
- package/src/logger.ts +30 -0
- package/src/naplink/client.ts +118 -0
- package/src/naplink/media.ts +311 -0
- package/src/naplink/render.ts +440 -0
- package/src/session/manager.ts +628 -0
- package/src/session/proactive.ts +22 -0
- package/src/session/prompt.ts +85 -0
- package/src/tools/qq.ts +421 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
import { existsSync } from "node:fs"
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path"
|
|
4
|
+
|
|
5
|
+
/** 发消息用的工具名(命名空间 qq 下的 send_message)。 */
|
|
6
|
+
export const SEND_MESSAGE_TOOL = "qq_send_message"
|
|
7
|
+
|
|
8
|
+
const GUIDELINES = `
|
|
9
|
+
GUIDELINES:
|
|
10
|
+
你在 QQ 群聊或私聊里,你收到的消息不一定是发给你的,你需要根据上下文和语境来判断是否回复。私聊消息必须回复;群聊里只有被 @ 时才必须回复。
|
|
11
|
+
对于一条消息,<quote>...</quote> 里面的内容是引用的消息,通常是你之前发过的消息或者其他人的消息,你需要根据这些引用来判断当前消息的语境。<forward>...</forward> 里面是合并转发消息的内容,每条前面的名字是转发节点的发送者。user_name : message 是用户发给你的消息, 不同的user_name代表不同的用户,你需要根据这些消息来判断每个用户的意图和情感。
|
|
12
|
+
重要!!!发送消息时,**必须**调用发送类工具;任何直接输出的文字都会被**完全丢弃**,不会作为消息内容发送!!!
|
|
13
|
+
文本回复用 ${SEND_MESSAGE_TOOL};图片、语音、合并转发、戳一戳等用对应的 qq_* 工具,调用成功同样算作已回复。
|
|
14
|
+
每轮至少调用一个发送类工具;不想回复时把 ${SEND_MESSAGE_TOOL} 的 reply 设成 false(content 可留空),相当于已读不回。群聊里不需要每一条都回复,像人一样选择性回复就行。
|
|
15
|
+
不要输出markdown、代码块、表格、列表等格式化内容,直接输出纯文本即可。
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
/** 主动发起话题时追加的模式说明。 */
|
|
19
|
+
export const PROACTIVE_HINT = `
|
|
20
|
+
[模式] 主动发起话题。不一定与记忆相关,也不一定与当前聊天的最新消息相关。
|
|
21
|
+
可以是一个问题、一个建议、一个有趣的想法、一个冷知识、一个笑话等。
|
|
22
|
+
注意,不要再重复之前的话题,也不要重复之前的消息内容。
|
|
23
|
+
你需要调用 ${SEND_MESSAGE_TOOL} 工具把话题发到当前聊天。
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
/** 从模块所在目录向上查找 SOUL.txt,兼容 src/ 与 dist/ 两种布局。 */
|
|
27
|
+
export function findSoulFile(startDir: string, maxDepth = 5): string | undefined {
|
|
28
|
+
let current = resolve(startDir)
|
|
29
|
+
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
30
|
+
const candidate = join(current, "SOUL.txt")
|
|
31
|
+
if (existsSync(candidate)) return candidate
|
|
32
|
+
const parent = dirname(current)
|
|
33
|
+
if (parent === current) break
|
|
34
|
+
current = parent
|
|
35
|
+
}
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 定位角色设定文件:显式路径优先,否则用包内默认。 */
|
|
40
|
+
export function resolveSoulPath(
|
|
41
|
+
explicitPath: string | undefined,
|
|
42
|
+
moduleDir: string,
|
|
43
|
+
cwd: string,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
if (explicitPath) {
|
|
46
|
+
return isAbsolute(explicitPath) ? explicitPath : resolve(cwd, explicitPath)
|
|
47
|
+
}
|
|
48
|
+
return findSoulFile(moduleDir)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 读取角色设定,失败时返回空字符串。 */
|
|
52
|
+
export async function loadSoul(path: string | undefined): Promise<string> {
|
|
53
|
+
if (!path) return ""
|
|
54
|
+
try {
|
|
55
|
+
return await readFile(path, "utf-8")
|
|
56
|
+
} catch {
|
|
57
|
+
return ""
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 组装完整的系统提示词。 */
|
|
62
|
+
export function buildSystemPrompt(soul: string): string {
|
|
63
|
+
const base = soul.trim()
|
|
64
|
+
return base ? `${base}\n${GUIDELINES}` : GUIDELINES.trim()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const TIME_FORMAT = new Intl.DateTimeFormat("sv-SE", {
|
|
68
|
+
year: "numeric",
|
|
69
|
+
month: "2-digit",
|
|
70
|
+
day: "2-digit",
|
|
71
|
+
hour: "2-digit",
|
|
72
|
+
minute: "2-digit",
|
|
73
|
+
second: "2-digit",
|
|
74
|
+
hour12: false,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
/** 当前时间文本,注入到系统提示里让模型知道现在几点。 */
|
|
78
|
+
export function currentTimeText(now = new Date()): string {
|
|
79
|
+
return TIME_FORMAT.format(now)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 供测试与其它模块定位包内 SOUL.txt。 */
|
|
83
|
+
export function packageDirectory(): string {
|
|
84
|
+
return import.meta.dirname
|
|
85
|
+
}
|
package/src/tools/qq.ts
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin"
|
|
2
|
+
import { type ChatTarget, isTargetEnabled, targetLabel } from "../chat"
|
|
3
|
+
import type { QQBotConfig } from "../config"
|
|
4
|
+
import type { Logger } from "../logger"
|
|
5
|
+
import type { QQClient } from "../naplink/client"
|
|
6
|
+
import type { SessionManager } from "../session/manager"
|
|
7
|
+
|
|
8
|
+
/** 工具命名空间,最终工具名为 qq_send_message 等。 */
|
|
9
|
+
export const TOOL_NAMESPACE = "qq"
|
|
10
|
+
|
|
11
|
+
export interface ToolDeps {
|
|
12
|
+
ctx: Plugin.Context
|
|
13
|
+
config: QQBotConfig
|
|
14
|
+
logger: Logger
|
|
15
|
+
manager: SessionManager
|
|
16
|
+
client: QQClient
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SendMessageInput {
|
|
20
|
+
content?: string
|
|
21
|
+
reply?: boolean
|
|
22
|
+
unlimit_length?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface MediaInput {
|
|
26
|
+
file?: string
|
|
27
|
+
base64?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ForwardNodeInput {
|
|
31
|
+
user_id?: number | string
|
|
32
|
+
nickname?: string
|
|
33
|
+
content?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ForwardInput {
|
|
37
|
+
nodes?: ForwardNodeInput[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface UserInput {
|
|
41
|
+
user_id?: number | string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface MessageIdInput {
|
|
45
|
+
message_id?: number | string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** QQ 单条消息的保守长度上限,超过时按段发送。 */
|
|
49
|
+
export const MAX_MESSAGE_CHARS = 4000
|
|
50
|
+
|
|
51
|
+
function text(value: string) {
|
|
52
|
+
return { content: value }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function delay(ms: number): Promise<void> {
|
|
56
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 从 OneBot 发送结果里取出 message_id。 */
|
|
60
|
+
function messageIdOf(response: unknown): string | undefined {
|
|
61
|
+
if (!response || typeof response !== "object") return undefined
|
|
62
|
+
const record = response as { message_id?: unknown; data?: { message_id?: unknown } }
|
|
63
|
+
const value = record.message_id ?? record.data?.message_id
|
|
64
|
+
return value === undefined || value === null ? undefined : String(value)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 按长度切分消息,保留原有换行与空行。 */
|
|
68
|
+
export function splitMessage(content: string, max = MAX_MESSAGE_CHARS): string[] {
|
|
69
|
+
if (Array.from(content).length <= max) return [content]
|
|
70
|
+
const chunks: string[] = []
|
|
71
|
+
let current = ""
|
|
72
|
+
for (const line of content.split("\n")) {
|
|
73
|
+
const candidate = current ? `${current}\n${line}` : line
|
|
74
|
+
if (Array.from(candidate).length <= max) {
|
|
75
|
+
current = candidate
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (current) chunks.push(current)
|
|
79
|
+
let rest = line
|
|
80
|
+
while (Array.from(rest).length > max) {
|
|
81
|
+
const part = Array.from(rest).slice(0, max).join("")
|
|
82
|
+
chunks.push(part)
|
|
83
|
+
rest = Array.from(rest).slice(max).join("")
|
|
84
|
+
}
|
|
85
|
+
current = rest
|
|
86
|
+
}
|
|
87
|
+
if (current) chunks.push(current)
|
|
88
|
+
return chunks.length > 0 ? chunks : [content]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 注册 QQ 命名空间下的工具。
|
|
93
|
+
*
|
|
94
|
+
* 全部工具都是对 NapCat OneBot API 的透传,
|
|
95
|
+
* 只暴露消息收发与只读查询,不提供封禁/踢人等破坏性操作。
|
|
96
|
+
*/
|
|
97
|
+
export async function registerTools(deps: ToolDeps): Promise<{ dispose(): Promise<void> }> {
|
|
98
|
+
const { ctx, config, logger, manager, client } = deps
|
|
99
|
+
|
|
100
|
+
const requireTarget = (sessionID: string): ChatTarget | undefined => {
|
|
101
|
+
const target = manager.targetForSession(sessionID)
|
|
102
|
+
if (!target) return undefined
|
|
103
|
+
if (!isTargetEnabled(config, target)) {
|
|
104
|
+
logger.warn(`${targetLabel(target)} 已不在配置中,拒绝工具调用`)
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
if (!manager.hasActiveTurn(sessionID)) {
|
|
108
|
+
logger.warn(`会话 ${sessionID} 当前没有 QQ 消息轮次,拒绝工具调用`)
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
return target
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sendText = (target: ChatTarget, chunk: string) =>
|
|
115
|
+
target.kind === "group"
|
|
116
|
+
? client.api.sendGroupMessage(target.id, chunk)
|
|
117
|
+
: client.api.sendPrivateMessage(target.id, chunk)
|
|
118
|
+
|
|
119
|
+
const sendSegments = (target: ChatTarget, segments: unknown[]) =>
|
|
120
|
+
target.kind === "group"
|
|
121
|
+
? client.api.sendGroupMessage(target.id, segments)
|
|
122
|
+
: client.api.sendPrivateMessage(target.id, segments)
|
|
123
|
+
|
|
124
|
+
const sendForward = (target: ChatTarget, nodes: unknown[]) =>
|
|
125
|
+
target.kind === "group"
|
|
126
|
+
? client.api.sendGroupForwardMessage(target.id, nodes)
|
|
127
|
+
: client.api.sendPrivateForwardMessage({ user_id: target.id, messages: nodes })
|
|
128
|
+
|
|
129
|
+
const sendPoke = (target: ChatTarget, userId: string | number) =>
|
|
130
|
+
target.kind === "group"
|
|
131
|
+
? client.api.sendGroupPoke(target.id, userId)
|
|
132
|
+
: client.api.sendFriendPoke(userId)
|
|
133
|
+
|
|
134
|
+
return await ctx.tool.transform((editor) => {
|
|
135
|
+
editor.namespace({
|
|
136
|
+
name: TOOL_NAMESPACE,
|
|
137
|
+
description:
|
|
138
|
+
"QQ 聊天工具:发送消息、图片、语音、合并转发,以及查询用户信息。仅在 QQ 群聊或私聊会话里可用,其他会话调用会返回错误。",
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
editor.add({
|
|
142
|
+
name: "send_message",
|
|
143
|
+
description:
|
|
144
|
+
"向当前 QQ 群聊或私聊发送一条消息。每轮必须调用一个发送工具。content: 要发送的文本;reply: true 表示发送,false 表示选择不发送(已读不回);unlimit_length: 设为 true 可解除默认的长度告警限制。",
|
|
145
|
+
input: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
content: { type: "string", description: "要发送的文本内容;reply=false 时可省略" },
|
|
149
|
+
reply: { type: "boolean", description: "true 表示发送,false 表示本轮不回复" },
|
|
150
|
+
unlimit_length: { type: "boolean", description: "解除默认的消息长度限制" },
|
|
151
|
+
},
|
|
152
|
+
additionalProperties: false,
|
|
153
|
+
},
|
|
154
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
155
|
+
execute: async (raw, context) => {
|
|
156
|
+
const input = (raw ?? {}) as SendMessageInput
|
|
157
|
+
const target = requireTarget(context.sessionID)
|
|
158
|
+
if (!target) return text("error: 当前会话没有绑定 QQ 群聊或私聊")
|
|
159
|
+
const label = targetLabel(target)
|
|
160
|
+
|
|
161
|
+
if (input.reply === false) {
|
|
162
|
+
logger.info(`${label} 模型选择不回复`)
|
|
163
|
+
manager.markMessageSent(context.sessionID)
|
|
164
|
+
return text("skipped")
|
|
165
|
+
}
|
|
166
|
+
const content = (input.content ?? "").trim()
|
|
167
|
+
if (!content) {
|
|
168
|
+
logger.info(`${label} 回复内容为空`)
|
|
169
|
+
manager.markMessageSent(context.sessionID)
|
|
170
|
+
return text("skipped")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (content.length > config.warnLength && input.unlimit_length !== true) {
|
|
174
|
+
logger.warn(`send_message 内容长度 ${content.length} 超过 ${config.warnLength}`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const chunks = splitMessage(content)
|
|
178
|
+
let sent = 0
|
|
179
|
+
try {
|
|
180
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
181
|
+
const response = await sendText(target, chunk)
|
|
182
|
+
const messageId = messageIdOf(response)
|
|
183
|
+
if (messageId) manager.rememberOutgoing(target, messageId)
|
|
184
|
+
sent += 1
|
|
185
|
+
if (index < chunks.length - 1 && config.sendDelayMs > 0) {
|
|
186
|
+
await delay(config.sendDelayMs)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (sent > 0) manager.markMessageSent(context.sessionID)
|
|
191
|
+
logger.error(`${label} 发送消息失败,已发送 ${sent}/${chunks.length} 段`, error)
|
|
192
|
+
return text(`error: 消息只发送了 ${sent}/${chunks.length} 段`)
|
|
193
|
+
}
|
|
194
|
+
manager.markMessageSent(context.sessionID)
|
|
195
|
+
logger.info(`${label} 已发送 ${content.length} 字`)
|
|
196
|
+
return text("sent")
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
editor.add({
|
|
201
|
+
name: "send_image",
|
|
202
|
+
description:
|
|
203
|
+
"向当前 QQ 群聊或私聊发送一张图片。file 可以是本地路径或 http(s) 图片地址;base64 传 base64:// 形式的数据。",
|
|
204
|
+
input: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
file: { type: "string", description: "本地文件路径或图片 URL" },
|
|
208
|
+
base64: { type: "string", description: "base64 编码的图片数据" },
|
|
209
|
+
},
|
|
210
|
+
additionalProperties: false,
|
|
211
|
+
},
|
|
212
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
213
|
+
execute: async (raw, context) => {
|
|
214
|
+
const input = (raw ?? {}) as MediaInput
|
|
215
|
+
const target = requireTarget(context.sessionID)
|
|
216
|
+
if (!target) return text("error: 当前会话没有绑定 QQ 群聊或私聊")
|
|
217
|
+
const file = input.file ?? (input.base64 ? `base64://${input.base64}` : undefined)
|
|
218
|
+
if (!file) return text("error: 缺少 file 或 base64")
|
|
219
|
+
const response = await sendSegments(target, [{ type: "image", data: { file } }])
|
|
220
|
+
const messageId = messageIdOf(response)
|
|
221
|
+
if (messageId) manager.rememberOutgoing(target, messageId)
|
|
222
|
+
manager.markMessageSent(context.sessionID)
|
|
223
|
+
return text("sent")
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
editor.add({
|
|
228
|
+
name: "send_record",
|
|
229
|
+
description: "向当前 QQ 群聊或私聊发送一条语音。file 可以是本地路径或 http(s) 音频地址。",
|
|
230
|
+
input: {
|
|
231
|
+
type: "object",
|
|
232
|
+
properties: {
|
|
233
|
+
file: { type: "string", description: "本地文件路径或音频 URL" },
|
|
234
|
+
},
|
|
235
|
+
additionalProperties: false,
|
|
236
|
+
},
|
|
237
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
238
|
+
execute: async (raw, context) => {
|
|
239
|
+
const input = (raw ?? {}) as MediaInput
|
|
240
|
+
const target = requireTarget(context.sessionID)
|
|
241
|
+
if (!target) return text("error: 当前会话没有绑定 QQ 群聊或私聊")
|
|
242
|
+
if (!input.file) return text("error: 缺少 file")
|
|
243
|
+
const response = await sendSegments(target, [
|
|
244
|
+
{ type: "record", data: { file: input.file } },
|
|
245
|
+
])
|
|
246
|
+
const messageId = messageIdOf(response)
|
|
247
|
+
if (messageId) manager.rememberOutgoing(target, messageId)
|
|
248
|
+
manager.markMessageSent(context.sessionID)
|
|
249
|
+
return text("sent")
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
editor.add({
|
|
254
|
+
name: "send_forward",
|
|
255
|
+
description:
|
|
256
|
+
"向当前 QQ 群聊或私聊发送一条合并转发消息。nodes 为节点列表,每个节点包含 user_id、nickname 和纯文本 content。",
|
|
257
|
+
input: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
nodes: {
|
|
261
|
+
type: "array",
|
|
262
|
+
description: "合并转发的节点列表",
|
|
263
|
+
items: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
user_id: { type: "string", description: "发送者 QQ 号" },
|
|
267
|
+
nickname: { type: "string", description: "发送者昵称" },
|
|
268
|
+
content: { type: "string", description: "节点文本内容" },
|
|
269
|
+
},
|
|
270
|
+
required: ["content"],
|
|
271
|
+
additionalProperties: false,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
required: ["nodes"],
|
|
276
|
+
additionalProperties: false,
|
|
277
|
+
},
|
|
278
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
279
|
+
execute: async (raw, context) => {
|
|
280
|
+
const input = (raw ?? {}) as ForwardInput
|
|
281
|
+
const target = requireTarget(context.sessionID)
|
|
282
|
+
if (!target) return text("error: 当前会话没有绑定 QQ 群聊或私聊")
|
|
283
|
+
const nodes: {
|
|
284
|
+
user_id: number
|
|
285
|
+
nickname: string
|
|
286
|
+
content: { type: string; data: { text: string } }[]
|
|
287
|
+
}[] = []
|
|
288
|
+
for (const node of input.nodes ?? []) {
|
|
289
|
+
const userId = node.user_id === undefined ? 0 : Number(node.user_id)
|
|
290
|
+
if (!Number.isInteger(userId) || userId < 0) {
|
|
291
|
+
return text(`error: 非法 user_id:${String(node.user_id)}`)
|
|
292
|
+
}
|
|
293
|
+
nodes.push({
|
|
294
|
+
user_id: userId,
|
|
295
|
+
nickname: node.nickname?.trim() || "未知用户",
|
|
296
|
+
content: [{ type: "text", data: { text: node.content ?? "" } }],
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
if (nodes.length === 0) return text("error: nodes 为空")
|
|
300
|
+
const response = await sendForward(target, nodes)
|
|
301
|
+
const messageId = messageIdOf(response)
|
|
302
|
+
if (messageId) manager.rememberOutgoing(target, messageId)
|
|
303
|
+
manager.markMessageSent(context.sessionID)
|
|
304
|
+
return text("sent")
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
editor.add({
|
|
309
|
+
name: "send_poke",
|
|
310
|
+
description: "在当前 QQ 会话戳一下某个用户。",
|
|
311
|
+
input: {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {
|
|
314
|
+
user_id: { type: "string", description: "被戳的 QQ 号" },
|
|
315
|
+
},
|
|
316
|
+
required: ["user_id"],
|
|
317
|
+
additionalProperties: false,
|
|
318
|
+
},
|
|
319
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
320
|
+
execute: async (raw, context) => {
|
|
321
|
+
const input = (raw ?? {}) as UserInput
|
|
322
|
+
const target = requireTarget(context.sessionID)
|
|
323
|
+
if (!target) return text("error: 当前会话没有绑定 QQ 群聊或私聊")
|
|
324
|
+
if (!input.user_id) return text("error: 缺少 user_id")
|
|
325
|
+
await sendPoke(target, input.user_id)
|
|
326
|
+
manager.markMessageSent(context.sessionID)
|
|
327
|
+
return text("sent")
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
editor.add({
|
|
332
|
+
name: "delete_message",
|
|
333
|
+
description: "撤回一条机器人自己发过的消息。",
|
|
334
|
+
input: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: {
|
|
337
|
+
message_id: { type: "string", description: "要撤回的消息 ID" },
|
|
338
|
+
},
|
|
339
|
+
required: ["message_id"],
|
|
340
|
+
additionalProperties: false,
|
|
341
|
+
},
|
|
342
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
343
|
+
execute: async (raw, context) => {
|
|
344
|
+
const input = (raw ?? {}) as MessageIdInput
|
|
345
|
+
const target = requireTarget(context.sessionID)
|
|
346
|
+
if (!target) return text("error: 当前会话没有绑定 QQ 群聊或私聊")
|
|
347
|
+
if (input.message_id === undefined) return text("error: 缺少 message_id")
|
|
348
|
+
if (!manager.isOutgoing(target, String(input.message_id))) {
|
|
349
|
+
return text("error: 只能撤回机器人自己发送过的消息")
|
|
350
|
+
}
|
|
351
|
+
await client.api.deleteMessage(input.message_id)
|
|
352
|
+
return text("deleted")
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
editor.add({
|
|
357
|
+
name: "get_group_member_info",
|
|
358
|
+
description: "查询用户资料:群聊返回群名片与昵称,私聊返回 QQ 昵称。",
|
|
359
|
+
input: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
user_id: { type: "string", description: "QQ 号" },
|
|
363
|
+
},
|
|
364
|
+
required: ["user_id"],
|
|
365
|
+
additionalProperties: false,
|
|
366
|
+
},
|
|
367
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
368
|
+
execute: async (raw, context) => {
|
|
369
|
+
const input = (raw ?? {}) as UserInput
|
|
370
|
+
const target = requireTarget(context.sessionID)
|
|
371
|
+
if (!target) return text("error: 当前会话没有绑定 QQ 群聊或私聊")
|
|
372
|
+
if (!input.user_id) return text("error: 缺少 user_id")
|
|
373
|
+
const info =
|
|
374
|
+
target.kind === "group"
|
|
375
|
+
? await client.api.getGroupMemberInfo(target.id, input.user_id)
|
|
376
|
+
: await client.api.getStrangerInfo(input.user_id)
|
|
377
|
+
return text(JSON.stringify(info))
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
editor.add({
|
|
382
|
+
name: "fetch_ptt_text",
|
|
383
|
+
description: "把一条语音消息转写成文字。",
|
|
384
|
+
input: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
message_id: { type: "string", description: "语音消息的 ID" },
|
|
388
|
+
},
|
|
389
|
+
required: ["message_id"],
|
|
390
|
+
additionalProperties: false,
|
|
391
|
+
},
|
|
392
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
393
|
+
execute: async (raw, context) => {
|
|
394
|
+
const input = (raw ?? {}) as MessageIdInput
|
|
395
|
+
const target = requireTarget(context.sessionID)
|
|
396
|
+
if (!target) return text("error: 当前会话没有绑定 QQ 群聊或私聊")
|
|
397
|
+
if (input.message_id === undefined) return text("error: 缺少 message_id")
|
|
398
|
+
if (!manager.isKnownMessage(target, String(input.message_id))) {
|
|
399
|
+
return text("error: 只能转写当前会话最近收到的消息")
|
|
400
|
+
}
|
|
401
|
+
const result = await client.api.fetchPttText(input.message_id)
|
|
402
|
+
return text(result?.text ?? "")
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
editor.add({
|
|
407
|
+
name: "get_login_info",
|
|
408
|
+
description: "获取机器人自身的 QQ 号与昵称。",
|
|
409
|
+
input: {
|
|
410
|
+
type: "object",
|
|
411
|
+
properties: {},
|
|
412
|
+
additionalProperties: false,
|
|
413
|
+
},
|
|
414
|
+
options: { namespace: TOOL_NAMESPACE },
|
|
415
|
+
execute: async () => {
|
|
416
|
+
const info = await client.api.getLoginInfo()
|
|
417
|
+
return text(JSON.stringify(info))
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
}
|