@openclaw/qqbot 2026.6.5-beta.2 → 2026.6.5-beta.3
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/dist/api.js +7 -657
- package/dist/{channel-BLMrGYfg.js → channel-CcN43bPL.js} +32 -287
- package/dist/channel-entry-api.js +2 -0
- package/dist/channel-entry-fUBLXKPr.js +143 -0
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-xBlmSU4X.js → channel.setup-CQ_DFfPx.js} +1 -1
- package/dist/config-ZEfgeoL4.js +257 -0
- package/dist/{config-schema-BI7AYP6Q.js → config-schema-BFLNZ8Nf.js} +17 -259
- package/dist/doctor-contract-BYTS8-ia.js +135 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/{gateway-iL9SWAED.js → gateway-DdM2k3ss.js} +23 -26
- package/dist/{handler-runtime-CjXwuNlq.js → handler-runtime-JKYDlRdq.js} +4 -3
- package/dist/index.js +8 -1
- package/dist/log-SDfMMBWe.js +107 -0
- package/dist/{outbound-CEqyriPT.js → outbound-C9wV892v.js} +3 -14
- package/dist/request-context-Bm7PTBD1.js +43 -0
- package/dist/runtime-B9UoQ5NI.js +18 -0
- package/dist/runtime-api.js +1 -1
- package/dist/{runtime-BSgtMZ6G.js → sender-DIMG7jHz.js} +36 -123
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{request-context-DsuA124s.js → slash-commands-impl-FRw-Dl44.js} +3 -45
- package/dist/target-parser-BdCUmxK7.js +285 -0
- package/dist/tools-2d9t-N1b.js +518 -0
- package/dist/tools-api.js +2 -0
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +162 -0
- package/package.json +4 -4
package/dist/api.js
CHANGED
|
@@ -1,658 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
9
|
-
//#region extensions/qqbot/src/engine/tools/channel-api.ts
|
|
10
|
-
/**
|
|
11
|
-
* QQ Channel API proxy tool core logic.
|
|
12
|
-
* QQ 频道 API 代理工具核心逻辑。
|
|
13
|
-
*
|
|
14
|
-
* Provides an authenticated HTTP proxy for the QQ Open Platform channel
|
|
15
|
-
* APIs. The caller (old tools/channel.ts shell) resolves the access
|
|
16
|
-
* token and passes it in; this module handles URL building, path
|
|
17
|
-
* validation, fetch, and structured response formatting.
|
|
18
|
-
*/
|
|
19
|
-
const API_BASE = "https://api.sgroup.qq.com";
|
|
20
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
21
|
-
/**
|
|
22
|
-
* JSON Schema for AI tool parameters (used by framework registration).
|
|
23
|
-
* AI Tool 参数的 JSON Schema 定义(供框架注册使用)。
|
|
24
|
-
*/
|
|
25
|
-
const ChannelApiSchema = {
|
|
26
|
-
type: "object",
|
|
27
|
-
properties: {
|
|
28
|
-
method: {
|
|
29
|
-
type: "string",
|
|
30
|
-
description: "HTTP method. Allowed values: GET, POST, PUT, PATCH, DELETE.",
|
|
31
|
-
enum: [
|
|
32
|
-
"GET",
|
|
33
|
-
"POST",
|
|
34
|
-
"PUT",
|
|
35
|
-
"PATCH",
|
|
36
|
-
"DELETE"
|
|
37
|
-
]
|
|
38
|
-
},
|
|
39
|
-
path: {
|
|
40
|
-
type: "string",
|
|
41
|
-
description: "API path without the host. Replace placeholders with concrete values. Examples: /users/@me/guilds, /guilds/{guild_id}/channels, /channels/{channel_id}."
|
|
42
|
-
},
|
|
43
|
-
body: {
|
|
44
|
-
type: "object",
|
|
45
|
-
description: "JSON request body for POST/PUT/PATCH requests. GET/DELETE usually do not need it."
|
|
46
|
-
},
|
|
47
|
-
query: {
|
|
48
|
-
type: "object",
|
|
49
|
-
description: "URL query parameters as key/value pairs appended to the path. For example, { \"limit\": \"100\", \"after\": \"0\" } becomes ?limit=100&after=0.",
|
|
50
|
-
additionalProperties: { type: "string" }
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
required: ["method", "path"]
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* Build the full API URL from base + path + query params.
|
|
57
|
-
* 拼接 API 基地址 + 路径 + 查询参数。
|
|
58
|
-
*/
|
|
59
|
-
function buildUrl(path, query) {
|
|
60
|
-
let url = `${API_BASE}${path}`;
|
|
61
|
-
if (query && Object.keys(query).length > 0) {
|
|
62
|
-
const params = new URLSearchParams();
|
|
63
|
-
for (const [key, value] of Object.entries(query)) if (value !== void 0 && value !== null && value !== "") params.set(key, value);
|
|
64
|
-
const qs = params.toString();
|
|
65
|
-
if (qs) url += `?${qs}`;
|
|
66
|
-
}
|
|
67
|
-
return url;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Validate API path format; returns an error string or null if valid.
|
|
71
|
-
* 校验 API 路径格式,返回错误描述或 null(合法)。
|
|
72
|
-
*/
|
|
73
|
-
function validatePath(path) {
|
|
74
|
-
if (!path.startsWith("/")) return "path must start with /";
|
|
75
|
-
if (path.includes("..") || path.includes("//")) return "path must not contain .. or //";
|
|
76
|
-
if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path) && path !== "/") return "path contains unsupported characters";
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
function json$1(data) {
|
|
80
|
-
return {
|
|
81
|
-
content: [{
|
|
82
|
-
type: "text",
|
|
83
|
-
text: JSON.stringify(data, null, 2)
|
|
84
|
-
}],
|
|
85
|
-
details: data
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Execute a channel API proxy request.
|
|
90
|
-
* 执行频道 API 代理请求。
|
|
91
|
-
*
|
|
92
|
-
* The caller provides the access token; this function handles
|
|
93
|
-
* URL building, path validation, HTTP fetch, and structured
|
|
94
|
-
* response formatting suitable for AI tool output.
|
|
95
|
-
*/
|
|
96
|
-
async function executeChannelApi(params, options) {
|
|
97
|
-
if (!params.method) return json$1({ error: "method is required" });
|
|
98
|
-
if (!params.path) return json$1({ error: "path is required" });
|
|
99
|
-
const method = params.method.toUpperCase();
|
|
100
|
-
if (![
|
|
101
|
-
"GET",
|
|
102
|
-
"POST",
|
|
103
|
-
"PUT",
|
|
104
|
-
"PATCH",
|
|
105
|
-
"DELETE"
|
|
106
|
-
].includes(method)) return json$1({ error: `Unsupported HTTP method: ${method}. Allowed values: GET, POST, PUT, PATCH, DELETE` });
|
|
107
|
-
const pathError = validatePath(params.path);
|
|
108
|
-
if (pathError) return json$1({ error: pathError });
|
|
109
|
-
if ((method === "GET" || method === "DELETE") && params.body && Object.keys(params.body).length > 0) debugLog(`[qqbot-channel-api] ${method} request with body, body will be ignored`);
|
|
110
|
-
try {
|
|
111
|
-
const url = buildUrl(params.path, params.query);
|
|
112
|
-
const headers = {
|
|
113
|
-
Authorization: `QQBot ${options.accessToken}`,
|
|
114
|
-
"Content-Type": "application/json"
|
|
115
|
-
};
|
|
116
|
-
const controller = new AbortController();
|
|
117
|
-
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
118
|
-
const fetchOptions = {
|
|
119
|
-
method,
|
|
120
|
-
headers,
|
|
121
|
-
signal: controller.signal
|
|
122
|
-
};
|
|
123
|
-
if (params.body && [
|
|
124
|
-
"POST",
|
|
125
|
-
"PUT",
|
|
126
|
-
"PATCH"
|
|
127
|
-
].includes(method)) fetchOptions.body = JSON.stringify(params.body);
|
|
128
|
-
debugLog(`[qqbot-channel-api] >>> ${method} ${url} (timeout: ${DEFAULT_TIMEOUT_MS}ms)`);
|
|
129
|
-
let res;
|
|
130
|
-
try {
|
|
131
|
-
res = await fetch(url, fetchOptions);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
clearTimeout(timeoutId);
|
|
134
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
135
|
-
debugError(`[qqbot-channel-api] <<< Request timeout after ${DEFAULT_TIMEOUT_MS}ms`);
|
|
136
|
-
return json$1({
|
|
137
|
-
error: `Request timed out after ${DEFAULT_TIMEOUT_MS}ms`,
|
|
138
|
-
path: params.path
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
debugError("[qqbot-channel-api] <<< Network error:", err);
|
|
142
|
-
return json$1({
|
|
143
|
-
error: `Network error: ${formatErrorMessage(err)}`,
|
|
144
|
-
path: params.path
|
|
145
|
-
});
|
|
146
|
-
} finally {
|
|
147
|
-
clearTimeout(timeoutId);
|
|
148
|
-
}
|
|
149
|
-
debugLog(`[qqbot-channel-api] <<< Status: ${res.status} ${res.statusText}`);
|
|
150
|
-
const rawBody = await res.text();
|
|
151
|
-
if (!rawBody || rawBody.trim() === "") {
|
|
152
|
-
if (res.ok) return json$1({
|
|
153
|
-
success: true,
|
|
154
|
-
status: res.status,
|
|
155
|
-
path: params.path
|
|
156
|
-
});
|
|
157
|
-
return json$1({
|
|
158
|
-
error: `API returned ${res.status} ${res.statusText}`,
|
|
159
|
-
status: res.status,
|
|
160
|
-
path: params.path
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
let parsed;
|
|
164
|
-
try {
|
|
165
|
-
parsed = JSON.parse(rawBody);
|
|
166
|
-
} catch {
|
|
167
|
-
parsed = rawBody;
|
|
168
|
-
}
|
|
169
|
-
if (!res.ok) {
|
|
170
|
-
const errMsg = typeof parsed === "object" && parsed && "message" in parsed ? String(parsed.message) : `${res.status} ${res.statusText}`;
|
|
171
|
-
debugError(`[qqbot-channel-api] Error [${method} ${params.path}]: ${errMsg}`);
|
|
172
|
-
return json$1({
|
|
173
|
-
error: errMsg,
|
|
174
|
-
status: res.status,
|
|
175
|
-
path: params.path,
|
|
176
|
-
details: parsed
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
return json$1({
|
|
180
|
-
success: true,
|
|
181
|
-
status: res.status,
|
|
182
|
-
path: params.path,
|
|
183
|
-
data: parsed
|
|
184
|
-
});
|
|
185
|
-
} catch (err) {
|
|
186
|
-
return json$1({
|
|
187
|
-
error: formatErrorMessage(err),
|
|
188
|
-
path: params.path
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
//#endregion
|
|
193
|
-
//#region extensions/qqbot/src/bridge/tools/channel.ts
|
|
194
|
-
/**
|
|
195
|
-
* Register the QQ channel API proxy tool.
|
|
196
|
-
*
|
|
197
|
-
* The tool acts as an authenticated HTTP proxy for the QQ Open Platform
|
|
198
|
-
* channel APIs. Agents learn endpoint details from the skill docs and
|
|
199
|
-
* send requests through this proxy.
|
|
200
|
-
*/
|
|
201
|
-
function registerChannelTool(api) {
|
|
202
|
-
const cfg = api.config;
|
|
203
|
-
if (!cfg) return;
|
|
204
|
-
const accountIds = listQQBotAccountIds(cfg);
|
|
205
|
-
if (accountIds.length === 0) return;
|
|
206
|
-
const firstAccountId = accountIds[0];
|
|
207
|
-
const account = resolveQQBotAccount(cfg, firstAccountId);
|
|
208
|
-
if (!account.appId || !account.clientSecret) return;
|
|
209
|
-
api.registerTool({
|
|
210
|
-
name: "qqbot_channel_api",
|
|
211
|
-
label: "QQBot Channel API",
|
|
212
|
-
description: "Authenticated HTTP proxy for QQ Open Platform channel APIs. Common endpoints: list guilds GET /users/@me/guilds | list channels GET /guilds/{guild_id}/channels | get channel GET /channels/{channel_id} | create channel POST /guilds/{guild_id}/channels | list members GET /guilds/{guild_id}/members?after=0&limit=100 | get member GET /guilds/{guild_id}/members/{user_id} | list threads GET /channels/{channel_id}/threads | create thread PUT /channels/{channel_id}/threads | create announce POST /guilds/{guild_id}/announces | create schedule POST /channels/{channel_id}/schedules. See the qqbot-channel skill for full endpoint details.",
|
|
213
|
-
parameters: ChannelApiSchema,
|
|
214
|
-
async execute(_toolCallId, params) {
|
|
215
|
-
return executeChannelApi(params, { accessToken: await getAccessToken(account.appId, account.clientSecret) });
|
|
216
|
-
}
|
|
217
|
-
}, { name: "qqbot_channel_api" });
|
|
218
|
-
}
|
|
219
|
-
//#endregion
|
|
220
|
-
//#region extensions/qqbot/src/engine/tools/remind-logic.ts
|
|
221
|
-
/**
|
|
222
|
-
* JSON Schema for AI tool parameters (used by framework registration).
|
|
223
|
-
* AI Tool 参数的 JSON Schema 定义(供框架注册使用)。
|
|
224
|
-
*/
|
|
225
|
-
const RemindSchema = {
|
|
226
|
-
type: "object",
|
|
227
|
-
properties: {
|
|
228
|
-
action: {
|
|
229
|
-
type: "string",
|
|
230
|
-
description: "Action type. add=create a reminder, list=show reminders, remove=delete a reminder.",
|
|
231
|
-
enum: [
|
|
232
|
-
"add",
|
|
233
|
-
"list",
|
|
234
|
-
"remove"
|
|
235
|
-
]
|
|
236
|
-
},
|
|
237
|
-
content: {
|
|
238
|
-
type: "string",
|
|
239
|
-
description: "Reminder content, for example \"drink water\" or \"join the meeting\". Required when action=add."
|
|
240
|
-
},
|
|
241
|
-
to: {
|
|
242
|
-
type: "string",
|
|
243
|
-
description: "Optional delivery target. The runtime automatically resolves the current conversation target, so you usually do not need to supply this. Direct-message format: qqbot:c2c:user_openid. Group format: qqbot:group:group_openid."
|
|
244
|
-
},
|
|
245
|
-
time: {
|
|
246
|
-
type: "string",
|
|
247
|
-
description: "Time description. Supported formats:\n1. Relative time, for example \"5m\", \"1h\", \"1h30m\", or \"2d\"\n2. Cron expression, for example \"0 8 * * *\" or \"0 9 * * 1-5\"\nValues containing spaces are treated as cron expressions; everything else is treated as a one-shot relative delay.\nRequired when action=add."
|
|
248
|
-
},
|
|
249
|
-
timezone: {
|
|
250
|
-
type: "string",
|
|
251
|
-
description: "Timezone used for cron reminders. Defaults to \"Asia/Shanghai\"."
|
|
252
|
-
},
|
|
253
|
-
name: {
|
|
254
|
-
type: "string",
|
|
255
|
-
description: "Optional reminder job name. Defaults to the first 20 characters of content."
|
|
256
|
-
},
|
|
257
|
-
jobId: {
|
|
258
|
-
type: "string",
|
|
259
|
-
description: "Job ID to remove. Required when action=remove; fetch it with list first."
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
required: ["action"]
|
|
263
|
-
};
|
|
264
|
-
/**
|
|
265
|
-
* Parse a relative time string into milliseconds.
|
|
266
|
-
* 解析相对时间字符串为毫秒数。
|
|
267
|
-
*
|
|
268
|
-
* Supports: "5m", "1h", "1h30m", "2d", "45s", plain number (as minutes).
|
|
269
|
-
*
|
|
270
|
-
* @returns Milliseconds or null if unparseable.
|
|
271
|
-
*/
|
|
272
|
-
function parseRelativeTime(timeStr) {
|
|
273
|
-
const s = timeStr.trim().toLowerCase();
|
|
274
|
-
if (/^\d+$/.test(s)) return Number.parseInt(s, 10) * 6e4;
|
|
275
|
-
let totalMs = 0;
|
|
276
|
-
let matched = false;
|
|
277
|
-
let consumed = 0;
|
|
278
|
-
const regex = /(\d+(?:\.\d+)?)\s*(d|h|m|s)\s*/g;
|
|
279
|
-
let match;
|
|
280
|
-
while ((match = regex.exec(s)) !== null) {
|
|
281
|
-
if (match.index !== consumed) return null;
|
|
282
|
-
matched = true;
|
|
283
|
-
consumed = regex.lastIndex;
|
|
284
|
-
const value = Number.parseFloat(match[1]);
|
|
285
|
-
switch (match[2]) {
|
|
286
|
-
case "d":
|
|
287
|
-
totalMs += value * 864e5;
|
|
288
|
-
break;
|
|
289
|
-
case "h":
|
|
290
|
-
totalMs += value * 36e5;
|
|
291
|
-
break;
|
|
292
|
-
case "m":
|
|
293
|
-
totalMs += value * 6e4;
|
|
294
|
-
break;
|
|
295
|
-
case "s":
|
|
296
|
-
totalMs += value * 1e3;
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return matched && consumed === s.length ? Math.round(totalMs) : null;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Check whether a time string is a cron expression (3–6 space-separated fields).
|
|
304
|
-
* 判断时间字符串是否为 cron 表达式。
|
|
305
|
-
*/
|
|
306
|
-
function isCronExpression(timeStr) {
|
|
307
|
-
const parts = timeStr.trim().split(/\s+/);
|
|
308
|
-
if (parts.length < 3 || parts.length > 6) return false;
|
|
309
|
-
return parts.every((p) => /^[0-9*?/,LW#-]/.test(p));
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Generate a cron job name from reminder content (first 20 chars).
|
|
313
|
-
* 根据提醒内容生成 cron job 名称。
|
|
314
|
-
*/
|
|
315
|
-
function generateJobName(content) {
|
|
316
|
-
const trimmed = content.trim();
|
|
317
|
-
return `Reminder: ${trimmed.length > 20 ? `${trimmed.slice(0, 20)}…` : trimmed}`;
|
|
318
|
-
}
|
|
319
|
-
/** Build the reminder system prompt sent to the AI. */
|
|
320
|
-
function buildReminderPrompt(content) {
|
|
321
|
-
return `You are a warm reminder assistant. Please remind the user about: ${content}. Requirements: (1) do not reply with HEARTBEAT_OK (2) do not explain who you are (3) output a direct and caring reminder message (4) you may add a short encouraging line (5) keep it within 2-3 sentences (6) use a small amount of emoji.`;
|
|
322
|
-
}
|
|
323
|
-
/** Build cron job params for a one-shot delayed reminder. */
|
|
324
|
-
function buildOnceJob(params, atMs, to, accountId) {
|
|
325
|
-
const content = params.content;
|
|
326
|
-
return {
|
|
327
|
-
action: "add",
|
|
328
|
-
job: {
|
|
329
|
-
name: params.name || generateJobName(content),
|
|
330
|
-
schedule: {
|
|
331
|
-
kind: "at",
|
|
332
|
-
atMs
|
|
333
|
-
},
|
|
334
|
-
sessionTarget: "isolated",
|
|
335
|
-
wakeMode: "now",
|
|
336
|
-
deleteAfterRun: true,
|
|
337
|
-
payload: {
|
|
338
|
-
kind: "agentTurn",
|
|
339
|
-
message: buildReminderPrompt(content)
|
|
340
|
-
},
|
|
341
|
-
delivery: {
|
|
342
|
-
mode: "announce",
|
|
343
|
-
channel: "qqbot",
|
|
344
|
-
to,
|
|
345
|
-
accountId
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
/** Build cron job params for a recurring cron reminder. */
|
|
351
|
-
function buildCronJob(params, to, accountId) {
|
|
352
|
-
const content = params.content;
|
|
353
|
-
const name = params.name || generateJobName(content);
|
|
354
|
-
const tz = params.timezone || "Asia/Shanghai";
|
|
355
|
-
return {
|
|
356
|
-
action: "add",
|
|
357
|
-
job: {
|
|
358
|
-
name,
|
|
359
|
-
schedule: {
|
|
360
|
-
kind: "cron",
|
|
361
|
-
expr: params.time.trim(),
|
|
362
|
-
tz
|
|
363
|
-
},
|
|
364
|
-
sessionTarget: "isolated",
|
|
365
|
-
wakeMode: "now",
|
|
366
|
-
payload: {
|
|
367
|
-
kind: "agentTurn",
|
|
368
|
-
message: buildReminderPrompt(content)
|
|
369
|
-
},
|
|
370
|
-
delivery: {
|
|
371
|
-
mode: "announce",
|
|
372
|
-
channel: "qqbot",
|
|
373
|
-
to,
|
|
374
|
-
accountId
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
/** Format a delay in milliseconds as a short string (e.g. "5m", "1h30m"). */
|
|
380
|
-
function formatDelay(ms) {
|
|
381
|
-
const totalSeconds = Math.round(ms / 1e3);
|
|
382
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
383
|
-
const totalMinutes = Math.round(ms / 6e4);
|
|
384
|
-
if (totalMinutes < 60) return `${totalMinutes}m`;
|
|
385
|
-
const hours = Math.floor(totalMinutes / 60);
|
|
386
|
-
const minutes = totalMinutes % 60;
|
|
387
|
-
if (minutes === 0) return `${hours}h`;
|
|
388
|
-
return `${hours}h${minutes}m`;
|
|
389
|
-
}
|
|
390
|
-
function json(data) {
|
|
391
|
-
return {
|
|
392
|
-
content: [{
|
|
393
|
-
type: "text",
|
|
394
|
-
text: JSON.stringify(data, null, 2)
|
|
395
|
-
}],
|
|
396
|
-
details: data
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
function formatSchedulerError(error) {
|
|
400
|
-
return error instanceof Error ? error.message : String(error);
|
|
401
|
-
}
|
|
402
|
-
function prepareRemindCronAction(params, ctx = {}) {
|
|
403
|
-
if (params.action === "list") return {
|
|
404
|
-
ok: true,
|
|
405
|
-
action: "list",
|
|
406
|
-
cronAction: { action: "list" }
|
|
407
|
-
};
|
|
408
|
-
if (params.action === "remove") {
|
|
409
|
-
if (!params.jobId) return {
|
|
410
|
-
ok: false,
|
|
411
|
-
error: "jobId is required when action=remove. Use action=list first."
|
|
412
|
-
};
|
|
413
|
-
return {
|
|
414
|
-
ok: true,
|
|
415
|
-
action: "remove",
|
|
416
|
-
cronAction: {
|
|
417
|
-
action: "remove",
|
|
418
|
-
jobId: params.jobId
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
if (!params.content) return {
|
|
423
|
-
ok: false,
|
|
424
|
-
error: "content is required when action=add"
|
|
425
|
-
};
|
|
426
|
-
const resolvedTo = params.to || ctx.fallbackTo;
|
|
427
|
-
if (!resolvedTo) return {
|
|
428
|
-
ok: false,
|
|
429
|
-
error: "Unable to determine delivery target for action=add. The reminder can only be scheduled from within an active conversation."
|
|
430
|
-
};
|
|
431
|
-
if (!params.time) return {
|
|
432
|
-
ok: false,
|
|
433
|
-
error: "time is required when action=add"
|
|
434
|
-
};
|
|
435
|
-
const resolvedAccountId = ctx.fallbackAccountId || "default";
|
|
436
|
-
if (isCronExpression(params.time)) return {
|
|
437
|
-
ok: true,
|
|
438
|
-
action: "add",
|
|
439
|
-
cronAction: buildCronJob(params, resolvedTo, resolvedAccountId),
|
|
440
|
-
summary: `⏰ Recurring reminder: "${params.content}" (${params.time}, tz=${params.timezone || "Asia/Shanghai"})`
|
|
441
|
-
};
|
|
442
|
-
const delayMs = parseRelativeTime(params.time);
|
|
443
|
-
if (delayMs == null) return {
|
|
444
|
-
ok: false,
|
|
445
|
-
error: `Could not parse time format: ${params.time}. Use values like 5m, 1h, 1h30m, or a cron expression.`
|
|
446
|
-
};
|
|
447
|
-
if (delayMs < 3e4) return {
|
|
448
|
-
ok: false,
|
|
449
|
-
error: "Reminder delay must be at least 30 seconds"
|
|
450
|
-
};
|
|
451
|
-
const atMs = resolveExpiresAtMsFromDurationMs(delayMs);
|
|
452
|
-
if (atMs === void 0) return {
|
|
453
|
-
ok: false,
|
|
454
|
-
error: "Reminder time is outside the supported Date range"
|
|
455
|
-
};
|
|
456
|
-
return {
|
|
457
|
-
ok: true,
|
|
458
|
-
action: "add",
|
|
459
|
-
cronAction: buildOnceJob(params, atMs, resolvedTo, resolvedAccountId),
|
|
460
|
-
summary: `⏰ Reminder in ${formatDelay(delayMs)}: "${params.content}"`
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
async function executeScheduledRemind(params, ctx, scheduler) {
|
|
464
|
-
const plan = prepareRemindCronAction(params, ctx);
|
|
465
|
-
if (!plan.ok) return json({ error: plan.error });
|
|
466
|
-
try {
|
|
467
|
-
const cronResult = await scheduler(plan.cronAction);
|
|
468
|
-
return json({
|
|
469
|
-
ok: true,
|
|
470
|
-
action: plan.action,
|
|
471
|
-
summary: plan.summary,
|
|
472
|
-
cronResult
|
|
473
|
-
});
|
|
474
|
-
} catch (error) {
|
|
475
|
-
return json({
|
|
476
|
-
error: `Failed to run Gateway cron action: ${formatSchedulerError(error)}`,
|
|
477
|
-
action: plan.action
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
//#endregion
|
|
482
|
-
//#region extensions/qqbot/src/bridge/tools/remind.ts
|
|
483
|
-
const DEFAULT_GATEWAY_TIMEOUT_MS = 6e4;
|
|
484
|
-
function unexpectedCronParams(params) {
|
|
485
|
-
throw new Error(`Unsupported reminder cron action: ${JSON.stringify(params)}`);
|
|
486
|
-
}
|
|
487
|
-
const defaultDeps = { callCron: async (params) => {
|
|
488
|
-
switch (params.action) {
|
|
489
|
-
case "list": return await callGatewayTool("cron.list", { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, {});
|
|
490
|
-
case "remove": return await callGatewayTool("cron.remove", { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, { jobId: params.jobId });
|
|
491
|
-
case "add": return await callGatewayTool("cron.add", { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, { job: params.job });
|
|
492
|
-
}
|
|
493
|
-
return unexpectedCronParams(params);
|
|
494
|
-
} };
|
|
495
|
-
function createRemindTool(toolContext = {}, deps = defaultDeps) {
|
|
496
|
-
return {
|
|
497
|
-
name: "qqbot_remind",
|
|
498
|
-
label: "QQBot Reminder",
|
|
499
|
-
description: "Create, list, and remove QQ reminders. This tool schedules Gateway cron jobs directly; do not call the cron tool after it succeeds.\nCreate: action=add, content=message, time=schedule (to is optional, resolved automatically from the current conversation)\nList: action=list\nRemove: action=remove, jobId=job id from list\nTime examples: \"5m\", \"1h\", \"0 8 * * *\"",
|
|
500
|
-
parameters: RemindSchema,
|
|
501
|
-
async execute(_toolCallId, params) {
|
|
502
|
-
const ctx = getRequestContext();
|
|
503
|
-
return await executeScheduledRemind(params, {
|
|
504
|
-
fallbackTo: ctx?.target ?? toolContext.deliveryContext?.to,
|
|
505
|
-
fallbackAccountId: ctx?.accountId ?? toolContext.deliveryContext?.accountId
|
|
506
|
-
}, deps.callCron);
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
function registerRemindTool(api) {
|
|
511
|
-
api.registerTool((ctx) => createRemindTool(ctx), { name: "qqbot_remind" });
|
|
512
|
-
}
|
|
513
|
-
//#endregion
|
|
514
|
-
//#region extensions/qqbot/src/bridge/tools/index.ts
|
|
515
|
-
function registerQQBotTools(api) {
|
|
516
|
-
registerChannelTool(api);
|
|
517
|
-
registerRemindTool(api);
|
|
518
|
-
}
|
|
519
|
-
//#endregion
|
|
520
|
-
//#region extensions/qqbot/src/bridge/commands/framework-context-adapter.ts
|
|
521
|
-
/**
|
|
522
|
-
* Default queue snapshot used for framework-registered commands.
|
|
523
|
-
*
|
|
524
|
-
* Framework-side command dispatch runs outside the per-sender queue, so
|
|
525
|
-
* handlers observe an empty snapshot by design.
|
|
526
|
-
*/
|
|
527
|
-
const DEFAULT_QUEUE_SNAPSHOT = {
|
|
528
|
-
totalPending: 0,
|
|
529
|
-
activeUsers: 0,
|
|
530
|
-
maxConcurrentUsers: 10,
|
|
531
|
-
senderPending: 0
|
|
532
|
-
};
|
|
533
|
-
function buildFrameworkSlashContext({ ctx, account, from, commandName }) {
|
|
534
|
-
const args = ctx.args ?? "";
|
|
535
|
-
const rawContent = args ? `/${commandName} ${args}` : `/${commandName}`;
|
|
536
|
-
return {
|
|
537
|
-
type: from.msgType,
|
|
538
|
-
senderId: ctx.senderId ?? "",
|
|
539
|
-
messageId: "",
|
|
540
|
-
eventTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
541
|
-
receivedAt: Date.now(),
|
|
542
|
-
rawContent,
|
|
543
|
-
args,
|
|
544
|
-
accountId: account.accountId,
|
|
545
|
-
appId: account.appId,
|
|
546
|
-
accountConfig: account.config,
|
|
547
|
-
commandAuthorized: ctx.isAuthorizedSender,
|
|
548
|
-
queueSnapshot: { ...DEFAULT_QUEUE_SNAPSHOT }
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
//#endregion
|
|
552
|
-
//#region extensions/qqbot/src/bridge/commands/from-parser.ts
|
|
553
|
-
const MSG_TYPE_MAP = {
|
|
554
|
-
c2c: "c2c",
|
|
555
|
-
dm: "dm",
|
|
556
|
-
group: "group",
|
|
557
|
-
channel: "guild"
|
|
558
|
-
};
|
|
559
|
-
const TARGET_TYPE_MAP = {
|
|
560
|
-
c2c: "c2c",
|
|
561
|
-
dm: "dm",
|
|
562
|
-
group: "group",
|
|
563
|
-
channel: "channel"
|
|
564
|
-
};
|
|
565
|
-
function isFromKind(value) {
|
|
566
|
-
return value === "c2c" || value === "dm" || value === "group" || value === "channel";
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Parse `ctx.from` into the structured fields the QQBot bridge expects.
|
|
570
|
-
*
|
|
571
|
-
* Unknown or missing prefixes fall back to c2c. The remainder after the first
|
|
572
|
-
* `:` is returned verbatim as the target id, matching what the previous inline
|
|
573
|
-
* implementation did.
|
|
574
|
-
*/
|
|
575
|
-
function parseQQBotFrom(from) {
|
|
576
|
-
const stripped = (from ?? "").replace(/^qqbot:/iu, "");
|
|
577
|
-
const colonIdx = stripped.indexOf(":");
|
|
578
|
-
const rawPrefix = colonIdx === -1 ? stripped : stripped.slice(0, colonIdx);
|
|
579
|
-
const targetId = colonIdx === -1 ? stripped : stripped.slice(colonIdx + 1);
|
|
580
|
-
const kind = isFromKind(rawPrefix) ? rawPrefix : "c2c";
|
|
581
|
-
return {
|
|
582
|
-
msgType: MSG_TYPE_MAP[kind],
|
|
583
|
-
targetType: TARGET_TYPE_MAP[kind],
|
|
584
|
-
targetId
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
//#endregion
|
|
588
|
-
//#region extensions/qqbot/src/bridge/commands/result-dispatcher.ts
|
|
589
|
-
const UNEXPECTED_RESULT_TEXT = "⚠️ 命令返回了意外结果。";
|
|
590
|
-
function hasFilePath(value) {
|
|
591
|
-
return typeof value === "object" && value !== null && "filePath" in value && typeof value.filePath === "string";
|
|
592
|
-
}
|
|
593
|
-
function buildMediaTarget$1(account, from) {
|
|
594
|
-
return {
|
|
595
|
-
targetType: from.targetType,
|
|
596
|
-
targetId: from.targetId,
|
|
597
|
-
account
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
async function dispatchFrameworkSlashResult({ result, account, from, logger }) {
|
|
601
|
-
if (typeof result === "string") return { text: result };
|
|
602
|
-
if (hasFilePath(result)) {
|
|
603
|
-
const mediaCtx = buildMediaTarget$1(account, from);
|
|
604
|
-
try {
|
|
605
|
-
await sendDocument(mediaCtx, result.filePath, { allowQQBotDataDownloads: true });
|
|
606
|
-
} catch (err) {
|
|
607
|
-
logger?.warn(`framework slash file send failed: ${String(err)}`);
|
|
608
|
-
}
|
|
609
|
-
return { text: result.text };
|
|
610
|
-
}
|
|
611
|
-
return { text: UNEXPECTED_RESULT_TEXT };
|
|
612
|
-
}
|
|
613
|
-
//#endregion
|
|
614
|
-
//#region extensions/qqbot/src/bridge/commands/framework-registration.ts
|
|
615
|
-
const PRIVATE_CHAT_ONLY_TEXT = "💡 请在私聊中使用此指令";
|
|
616
|
-
function isExplicitQQBotC2cFrom(from) {
|
|
617
|
-
const raw = (from ?? "").trim();
|
|
618
|
-
const stripped = raw.replace(/^qqbot:/iu, "");
|
|
619
|
-
const colonIdx = stripped.indexOf(":");
|
|
620
|
-
if (colonIdx === -1) return false;
|
|
621
|
-
const kind = stripped.slice(0, colonIdx).toLowerCase();
|
|
622
|
-
const targetId = stripped.slice(colonIdx + 1).trim();
|
|
623
|
-
return /^qqbot:/iu.test(raw) && kind === "c2c" && targetId.length > 0;
|
|
624
|
-
}
|
|
625
|
-
function registerQQBotFrameworkCommands(api) {
|
|
626
|
-
for (const cmd of getFrameworkCommands()) api.registerCommand({
|
|
627
|
-
name: cmd.name,
|
|
628
|
-
description: cmd.description,
|
|
629
|
-
channels: ["qqbot"],
|
|
630
|
-
requireAuth: true,
|
|
631
|
-
acceptsArgs: true,
|
|
632
|
-
handler: async (ctx) => {
|
|
633
|
-
if (cmd.c2cOnly && !isExplicitQQBotC2cFrom(ctx.from)) return { text: PRIVATE_CHAT_ONLY_TEXT };
|
|
634
|
-
const from = parseQQBotFrom(ctx.from);
|
|
635
|
-
const account = resolveQQBotAccount(ctx.config, ctx.accountId ?? void 0);
|
|
636
|
-
const slashCtx = buildFrameworkSlashContext({
|
|
637
|
-
ctx,
|
|
638
|
-
account,
|
|
639
|
-
from,
|
|
640
|
-
commandName: cmd.name
|
|
641
|
-
});
|
|
642
|
-
return await dispatchFrameworkSlashResult({
|
|
643
|
-
result: await cmd.handler(slashCtx),
|
|
644
|
-
account,
|
|
645
|
-
from,
|
|
646
|
-
logger: api.logger
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
//#endregion
|
|
652
|
-
//#region extensions/qqbot/src/bridge/channel-entry.ts
|
|
653
|
-
function registerQQBotFull(api) {
|
|
654
|
-
registerQQBotTools(api);
|
|
655
|
-
registerQQBotFrameworkCommands(api);
|
|
656
|
-
}
|
|
657
|
-
//#endregion
|
|
1
|
+
import { t as qqbotPlugin } from "./channel-CcN43bPL.js";
|
|
2
|
+
import { a as resolveQQBotAccount, i as resolveDefaultQQBotAccountId, n as applyQQBotAccountConfig, r as listQQBotAccountIds, t as DEFAULT_ACCOUNT_ID } from "./config-ZEfgeoL4.js";
|
|
3
|
+
import { t as qqbotSetupPlugin } from "./channel.setup-CQ_DFfPx.js";
|
|
4
|
+
import { t as getFrameworkCommands } from "./slash-commands-impl-FRw-Dl44.js";
|
|
5
|
+
import { n as registerRemindTool, r as registerChannelTool, t as registerQQBotTools } from "./tools-2d9t-N1b.js";
|
|
6
|
+
import { t as registerQQBotFull } from "./channel-entry-fUBLXKPr.js";
|
|
7
|
+
import { C as getMessageReplyStats, D as setOutboundAudioPort, E as OUTBOUND_ERROR_CODES, S as getMessageReplyConfig, T as DEFAULT_MEDIA_SEND_ERROR, _ as sendVideoMsg, a as sendText, b as MESSAGE_REPLY_LIMIT, f as buildMediaTarget, g as sendPhoto, h as sendDocument, i as sendProactiveMessage, m as resolveOutboundMediaPath, n as sendCronMessage, p as parseTarget, r as sendMedia, v as sendVoice, w as recordMessageReply, x as checkMessageReplyLimit, y as resolveUserFacingMediaError } from "./outbound-C9wV892v.js";
|
|
658
8
|
export { DEFAULT_ACCOUNT_ID, DEFAULT_MEDIA_SEND_ERROR, MESSAGE_REPLY_LIMIT, OUTBOUND_ERROR_CODES, applyQQBotAccountConfig, buildMediaTarget, checkMessageReplyLimit, getFrameworkCommands, getMessageReplyConfig, getMessageReplyStats, listQQBotAccountIds, parseTarget, qqbotPlugin, qqbotSetupPlugin, recordMessageReply, registerChannelTool, registerQQBotFull, registerQQBotTools, registerRemindTool, resolveDefaultQQBotAccountId, resolveOutboundMediaPath, resolveQQBotAccount, resolveUserFacingMediaError, sendCronMessage, sendDocument, sendMedia, sendPhoto, sendProactiveMessage, sendText, sendVideoMsg, sendVoice, setOutboundAudioPort };
|