@openclaw/qqbot 2026.6.11 → 2026.7.1-beta.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/dist/api.js +6 -6
- package/dist/{channel-DvWrqcZB.js → channel-BQM6pDlD.js} +71 -126
- package/dist/{channel-entry-BGXFRmbg.js → channel-entry-C18ypTaG.js} +3 -3
- package/dist/channel-entry-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-CldhUlTO.js → channel.setup-2ItDYKhz.js} +1 -1
- package/dist/{config-schema-gMW9zKvQ.js → config-schema-JZEf1dvB.js} +2 -5
- package/dist/doctor-contract-api.js +168 -2
- package/dist/{gateway-CCkrdgaK.js → gateway-CjdB3B6n.js} +284 -264
- package/dist/{group-CvVXke_k.js → group-BmS1Qwst.js} +2 -2
- package/dist/{handler-runtime--OTMkFWa.js → handler-runtime-rU03VA43.js} +3 -3
- package/dist/{outbound-BQcPCo-z.js → outbound-BZHwMFUX.js} +496 -44
- package/dist/{runtime-D1hn91-s.js → runtime-B4HWCOa-.js} +1 -1
- package/dist/runtime-api.js +1 -1
- package/dist/{sender-CRZ2af7P.js → sender-BfVLJJYp.js} +16 -14
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{doctor-contract-BYTS8-ia.js → state-keys-CQKlAFyo.js} +7 -1
- package/dist/target-parser-DagYRQkP.js +71 -0
- package/dist/{tools-CeUI9pG-.js → tools-C8kTMXKc.js} +58 -13
- package/dist/tools-api.js +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
- package/skills/qqbot-channel/SKILL.md +54 -41
- package/skills/qqbot-channel/references/api_references.md +16 -8
- package/skills/qqbot-media/SKILL.md +19 -13
- package/skills/qqbot-remind/SKILL.md +15 -14
- package/dist/target-parser-BdCUmxK7.js +0 -285
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { v as setOpenClawVersion } from "./sender-
|
|
1
|
+
import { v as setOpenClawVersion } from "./sender-BfVLJJYp.js";
|
|
2
2
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
3
3
|
//#region extensions/qqbot/src/bridge/runtime.ts
|
|
4
4
|
const { setRuntime: _setRuntime, clearRuntime: resetQQBotRuntimeForTest, getRuntime: getQQBotRuntime } = createPluginRuntimeStore({
|
package/dist/runtime-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-
|
|
1
|
+
import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-B4HWCOa-.js";
|
|
2
2
|
export { getQQBotRuntime, setQQBotRuntime };
|
|
@@ -2,10 +2,11 @@ import { c as getPlatformAdapter, i as normalizeOptionalString, s as sanitizeFil
|
|
|
2
2
|
import { a as formatErrorMessage, n as debugLog, r as debugWarn, t as debugError } from "./log-SDfMMBWe.js";
|
|
3
3
|
import * as fs$1 from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
5
|
+
import { readProviderTextResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
6
6
|
import { fetchWithSsrFGuard, isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
7
7
|
import * as crypto$1 from "node:crypto";
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
|
+
import { sleep } from "openclaw/plugin-sdk/runtime-env";
|
|
9
10
|
import { FsSafeError, openLocalFileSafely, readRegularFile, statRegularFileSync } from "openclaw/plugin-sdk/security-runtime";
|
|
10
11
|
import * as path$1 from "node:path";
|
|
11
12
|
import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
|
|
@@ -159,7 +160,7 @@ var ApiClient = class {
|
|
|
159
160
|
this.logger?.info?.(`[qqbot:api] <<< Status: ${res.status} ${res.statusText}${traceId ? ` | TraceId: ${traceId}` : ""}`);
|
|
160
161
|
const readBody = async (limitBytes) => {
|
|
161
162
|
try {
|
|
162
|
-
return limitBytes === void 0 ? await res
|
|
163
|
+
return limitBytes === void 0 ? await readProviderTextResponse(res, "QQBot API response") : await readResponseTextLimited(res, limitBytes);
|
|
163
164
|
} catch (err) {
|
|
164
165
|
throw new ApiError(`Failed to read response [${path}]: ${formatErrorMessage(err)}`, res.status, path);
|
|
165
166
|
}
|
|
@@ -462,6 +463,17 @@ async function normalizeSource(raw, opts = {}) {
|
|
|
462
463
|
//#endregion
|
|
463
464
|
//#region extensions/qqbot/src/engine/api/retry.ts
|
|
464
465
|
/**
|
|
466
|
+
* Generic retry engine for QQ Bot API requests.
|
|
467
|
+
*
|
|
468
|
+
* Replaces the three separate retry implementations in the old `api.ts`:
|
|
469
|
+
* - `apiRequestWithRetry` (upload retry with exponential backoff)
|
|
470
|
+
* - `partFinishWithRetry` (part-finish retry + persistent retry on specific biz codes)
|
|
471
|
+
* - `completeUploadWithRetry` (unconditional retry for complete-upload)
|
|
472
|
+
*
|
|
473
|
+
* All three patterns are expressed as a single `withRetry` function
|
|
474
|
+
* parameterized by `RetryPolicy` and optional `PersistentRetryPolicy`.
|
|
475
|
+
*/
|
|
476
|
+
/**
|
|
465
477
|
* Execute an async operation with configurable retry semantics.
|
|
466
478
|
*
|
|
467
479
|
* @param fn - The async operation to retry.
|
|
@@ -484,7 +496,7 @@ async function withRetry(fn, policy, persistentPolicy, logger) {
|
|
|
484
496
|
if (attempt < policy.maxRetries) {
|
|
485
497
|
const delay = policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** attempt : policy.baseDelayMs;
|
|
486
498
|
logger?.debug?.(`[qqbot:retry] Attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message.slice(0, 100)}`);
|
|
487
|
-
await sleep
|
|
499
|
+
await sleep(delay);
|
|
488
500
|
}
|
|
489
501
|
}
|
|
490
502
|
throw lastError;
|
|
@@ -514,16 +526,11 @@ async function persistentRetryLoop(fn, policy, logger) {
|
|
|
514
526
|
if (remaining <= 0) break;
|
|
515
527
|
const actualDelay = Math.min(policy.intervalMs, remaining);
|
|
516
528
|
(logger?.warn ?? logger?.error)?.(`[qqbot:retry] Persistent retry #${attempt}: retrying in ${actualDelay}ms (remaining=${Math.round(remaining / 1e3)}s)`);
|
|
517
|
-
await sleep
|
|
529
|
+
await sleep(actualDelay);
|
|
518
530
|
}
|
|
519
531
|
logger?.error?.(`[qqbot:retry] Persistent retry timed out after ${policy.timeoutMs / 1e3}s (${attempt} attempts)`);
|
|
520
532
|
throw lastError ?? /* @__PURE__ */ new Error(`Persistent retry timed out (${policy.timeoutMs / 1e3}s)`);
|
|
521
533
|
}
|
|
522
|
-
function sleep$1(ms) {
|
|
523
|
-
return new Promise((resolve) => {
|
|
524
|
-
setTimeout(resolve, ms);
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
534
|
/** Standard upload retry: exponential backoff, skip 400/401/timeout errors. */
|
|
528
535
|
const UPLOAD_RETRY_POLICY = {
|
|
529
536
|
maxRetries: 2,
|
|
@@ -967,11 +974,6 @@ async function runWithConcurrency(tasks, maxConcurrent) {
|
|
|
967
974
|
await Promise.all(batch.map((task) => task()));
|
|
968
975
|
}
|
|
969
976
|
}
|
|
970
|
-
function sleep(ms) {
|
|
971
|
-
return new Promise((resolve) => {
|
|
972
|
-
setTimeout(resolve, ms);
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
977
|
//#endregion
|
|
976
978
|
//#region extensions/qqbot/src/engine/api/token.ts
|
|
977
979
|
/**
|
package/dist/setup-plugin-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as qqbotSetupPlugin } from "./channel.setup-
|
|
1
|
+
import { t as qqbotSetupPlugin } from "./channel.setup-2ItDYKhz.js";
|
|
2
2
|
export { qqbotSetupPlugin };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import { asObjectRecord } from "openclaw/plugin-sdk/runtime-doctor";
|
|
2
3
|
//#region extensions/qqbot/src/doctor-contract.ts
|
|
3
4
|
const RESTRICTED_GROUP_TOOLS = { deny: [
|
|
@@ -132,4 +133,9 @@ function normalizeCompatibilityConfig({ cfg }) {
|
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
135
|
//#endregion
|
|
135
|
-
|
|
136
|
+
//#region extensions/qqbot/src/engine/utils/state-keys.ts
|
|
137
|
+
function buildQQBotStateKey(...parts) {
|
|
138
|
+
return crypto.createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { legacyConfigRules as n, normalizeCompatibilityConfig as r, buildQQBotStateKey as t };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region extensions/qqbot/src/engine/messaging/target-parser.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse a qqbot target string into a structured delivery target.
|
|
4
|
+
*
|
|
5
|
+
* Supported formats:
|
|
6
|
+
* - `qqbot:c2c:openid` → C2C direct message
|
|
7
|
+
* - `qqbot:group:groupid` → Group message
|
|
8
|
+
* - `qqbot:channel:channelid` → Channel message
|
|
9
|
+
* - `c2c:openid` → C2C (without qqbot: prefix)
|
|
10
|
+
* - `group:groupid` → Group (without qqbot: prefix)
|
|
11
|
+
* - `channel:channelid` → Channel (without qqbot: prefix)
|
|
12
|
+
* - `openid` → C2C (bare openid, default)
|
|
13
|
+
*
|
|
14
|
+
* @param to - Raw target string.
|
|
15
|
+
* @returns Parsed target with type and id.
|
|
16
|
+
* @throws {Error} When the target format is invalid.
|
|
17
|
+
*/
|
|
18
|
+
function parseTarget(to) {
|
|
19
|
+
const id = to.replace(/^qqbot:/i, "");
|
|
20
|
+
if (id.startsWith("c2c:")) {
|
|
21
|
+
const userId = id.slice(4);
|
|
22
|
+
if (!userId) throw new Error(`Invalid c2c target format: ${to} - missing user ID`);
|
|
23
|
+
return {
|
|
24
|
+
type: "c2c",
|
|
25
|
+
id: userId
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (id.startsWith("group:")) {
|
|
29
|
+
const groupId = id.slice(6);
|
|
30
|
+
if (!groupId) throw new Error(`Invalid group target format: ${to} - missing group ID`);
|
|
31
|
+
return {
|
|
32
|
+
type: "group",
|
|
33
|
+
id: groupId
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (id.startsWith("channel:")) {
|
|
37
|
+
const channelId = id.slice(8);
|
|
38
|
+
if (!channelId) throw new Error(`Invalid channel target format: ${to} - missing channel ID`);
|
|
39
|
+
return {
|
|
40
|
+
type: "channel",
|
|
41
|
+
id: channelId
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!id) throw new Error(`Invalid target format: ${to} - empty ID after removing qqbot: prefix`);
|
|
45
|
+
return {
|
|
46
|
+
type: "c2c",
|
|
47
|
+
id
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Normalize a QQ Bot target string into the canonical `qqbot:...` form.
|
|
52
|
+
*
|
|
53
|
+
* Returns `undefined` when the target does not look like a QQ Bot address.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeTarget(target) {
|
|
56
|
+
const id = target.replace(/^qqbot:/i, "");
|
|
57
|
+
if (id.startsWith("c2c:") || id.startsWith("group:") || id.startsWith("channel:")) return `qqbot:${id}`;
|
|
58
|
+
if (/^[0-9a-fA-F]{32}$/.test(id)) return `qqbot:c2c:${id}`;
|
|
59
|
+
if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id)) return `qqbot:c2c:${id}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Return true when the string looks like a QQ Bot target ID.
|
|
63
|
+
*/
|
|
64
|
+
function looksLikeQQBotTarget(id) {
|
|
65
|
+
if (/^qqbot:(c2c|group|channel):/i.test(id)) return true;
|
|
66
|
+
if (/^(c2c|group|channel):/i.test(id)) return true;
|
|
67
|
+
if (/^[0-9a-fA-F]{32}$/.test(id)) return true;
|
|
68
|
+
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id);
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { normalizeTarget as n, parseTarget as r, looksLikeQQBotTarget as t };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { a as resolveQQBotAccount, r as listQQBotAccountIds } from "./config-C1qZbh0K.js";
|
|
2
2
|
import { a as formatErrorMessage, n as debugLog, t as debugError } from "./log-SDfMMBWe.js";
|
|
3
3
|
import { t as getRequestContext } from "./request-context-Bm7PTBD1.js";
|
|
4
|
-
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
4
|
+
import { readProviderTextResponse, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
5
5
|
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
6
6
|
import { resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
7
7
|
import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
8
|
+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
8
9
|
//#region extensions/qqbot/src/engine/tools/channel-api.ts
|
|
9
10
|
/**
|
|
10
11
|
* QQ Channel API proxy tool core logic.
|
|
@@ -33,7 +34,7 @@ const ChannelApiSchema = {
|
|
|
33
34
|
properties: {
|
|
34
35
|
method: {
|
|
35
36
|
type: "string",
|
|
36
|
-
description: "HTTP method. Allowed values: GET, POST, PUT, PATCH, DELETE.",
|
|
37
|
+
description: "HTTP method. Allowed values: GET, POST, PUT, PATCH, DELETE. Use DELETE and other mutating methods only after explicit user intent and target confirmation.",
|
|
37
38
|
enum: [
|
|
38
39
|
"GET",
|
|
39
40
|
"POST",
|
|
@@ -48,12 +49,20 @@ const ChannelApiSchema = {
|
|
|
48
49
|
},
|
|
49
50
|
body: {
|
|
50
51
|
type: "object",
|
|
51
|
-
description: "JSON request body for POST/PUT/PATCH requests. GET/DELETE usually do not need it."
|
|
52
|
+
description: "JSON request body for POST/PUT/PATCH requests. GET/DELETE usually do not need it. For write requests, include only fields the user explicitly asked to change."
|
|
52
53
|
},
|
|
53
54
|
query: {
|
|
54
55
|
type: "object",
|
|
55
56
|
description: "URL query parameters as key/value pairs appended to the path. For example, { \"limit\": \"100\", \"after\": \"0\" } becomes ?limit=100&after=0.",
|
|
56
57
|
additionalProperties: { type: "string" }
|
|
58
|
+
},
|
|
59
|
+
confirmed: {
|
|
60
|
+
type: "boolean",
|
|
61
|
+
description: "Required true for DELETE requests after the user confirms the exact QQ resource to delete."
|
|
62
|
+
},
|
|
63
|
+
bulkConfirmed: {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
description: "Required true in addition to confirmed for bulk DELETE requests such as deleting all announcements."
|
|
57
66
|
}
|
|
58
67
|
},
|
|
59
68
|
required: ["method", "path"]
|
|
@@ -80,6 +89,33 @@ function validatePath(path) {
|
|
|
80
89
|
if (!path.startsWith("/")) return "path must start with /";
|
|
81
90
|
if (path.includes("..") || path.includes("//")) return "path must not contain .. or //";
|
|
82
91
|
if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path) && path !== "/") return "path contains unsupported characters";
|
|
92
|
+
for (const segment of path.split("/").slice(1)) {
|
|
93
|
+
let decodedSegment;
|
|
94
|
+
try {
|
|
95
|
+
decodedSegment = decodeURIComponent(segment);
|
|
96
|
+
} catch {
|
|
97
|
+
return "path contains invalid percent encoding";
|
|
98
|
+
}
|
|
99
|
+
if (decodedSegment.includes("/") || decodedSegment.includes("\\")) return "path contains encoded path separators";
|
|
100
|
+
if (decodedSegment === "." || decodedSegment === "..") return "path must not contain . or .. segments";
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function decodePathSegments(path) {
|
|
105
|
+
try {
|
|
106
|
+
return path.replace(/\/+$/, "").split("/").slice(1).map((segment) => decodeURIComponent(segment));
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function isBulkAnnouncementDeletePath(path) {
|
|
112
|
+
const segments = decodePathSegments(path);
|
|
113
|
+
return Boolean(segments && segments.length === 4 && segments[0]?.toLowerCase() === "guilds" && segments[2]?.toLowerCase() === "announces" && segments[3]?.toLowerCase() === "all");
|
|
114
|
+
}
|
|
115
|
+
function validateDeleteConfirmation(params) {
|
|
116
|
+
if (params.method.toUpperCase() !== "DELETE") return null;
|
|
117
|
+
if (!params.confirmed) return "DELETE requests require confirmed=true after the user confirms the exact QQ resource.";
|
|
118
|
+
if (isBulkAnnouncementDeletePath(params.path) && !params.bulkConfirmed) return "Deleting all announcements requires bulkConfirmed=true after a separate bulk-delete confirmation.";
|
|
83
119
|
return null;
|
|
84
120
|
}
|
|
85
121
|
function json$1(data) {
|
|
@@ -112,6 +148,14 @@ async function executeChannelApi(params, options) {
|
|
|
112
148
|
].includes(method)) return json$1({ error: `Unsupported HTTP method: ${method}. Allowed values: GET, POST, PUT, PATCH, DELETE` });
|
|
113
149
|
const pathError = validatePath(params.path);
|
|
114
150
|
if (pathError) return json$1({ error: pathError });
|
|
151
|
+
const confirmationError = validateDeleteConfirmation({
|
|
152
|
+
...params,
|
|
153
|
+
method
|
|
154
|
+
});
|
|
155
|
+
if (confirmationError) return json$1({
|
|
156
|
+
error: confirmationError,
|
|
157
|
+
path: params.path
|
|
158
|
+
});
|
|
115
159
|
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`);
|
|
116
160
|
try {
|
|
117
161
|
const url = buildUrl(params.path, params.query);
|
|
@@ -162,7 +206,7 @@ async function executeChannelApi(params, options) {
|
|
|
162
206
|
}
|
|
163
207
|
try {
|
|
164
208
|
debugLog(`[qqbot-channel-api] <<< Status: ${res.status} ${res.statusText}`);
|
|
165
|
-
const rawBody = res.ok ? await res
|
|
209
|
+
const rawBody = res.ok ? await readProviderTextResponse(res, "QQ channel API response") : await readResponseTextLimited(res, CHANNEL_API_ERROR_BODY_LIMIT_BYTES);
|
|
166
210
|
if (!rawBody || rawBody.trim() === "") {
|
|
167
211
|
if (res.ok) return json$1({
|
|
168
212
|
success: true,
|
|
@@ -227,16 +271,17 @@ function registerChannelTool(api) {
|
|
|
227
271
|
api.registerTool({
|
|
228
272
|
name: "qqbot_channel_api",
|
|
229
273
|
label: "QQBot Channel API",
|
|
230
|
-
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.",
|
|
274
|
+
description: "Authenticated HTTP proxy for QQ Open Platform channel APIs. Use write and delete endpoints only after explicit user intent; DELETE requires confirmed=true, and bulk deletes require bulkConfirmed=true after confirming the exact target. 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.",
|
|
231
275
|
parameters: ChannelApiSchema,
|
|
232
276
|
async execute(_toolCallId, params) {
|
|
233
|
-
const { getAccessToken } = await import("./sender-
|
|
277
|
+
const { getAccessToken } = await import("./sender-BfVLJJYp.js").then((n) => n._);
|
|
234
278
|
return executeChannelApi(params, { accessToken: await getAccessToken(account.appId, account.clientSecret) });
|
|
235
279
|
}
|
|
236
280
|
}, { name: "qqbot_channel_api" });
|
|
237
281
|
}
|
|
238
282
|
//#endregion
|
|
239
283
|
//#region extensions/qqbot/src/engine/tools/remind-logic.ts
|
|
284
|
+
const QQBOT_DEFAULT_REMINDER_TIMEZONE = "Asia/Shanghai";
|
|
240
285
|
/**
|
|
241
286
|
* JSON Schema for AI tool parameters (used by framework registration).
|
|
242
287
|
* AI Tool 参数的 JSON Schema 定义(供框架注册使用)。
|
|
@@ -246,7 +291,7 @@ const RemindSchema = {
|
|
|
246
291
|
properties: {
|
|
247
292
|
action: {
|
|
248
293
|
type: "string",
|
|
249
|
-
description: "Action type. add=create a reminder, list=show reminders, remove=delete a reminder.",
|
|
294
|
+
description: "Action type. add=create a reminder only after explicit user request, list=show reminders, remove=delete a reminder by confirmed job ID.",
|
|
250
295
|
enum: [
|
|
251
296
|
"add",
|
|
252
297
|
"list",
|
|
@@ -263,11 +308,11 @@ const RemindSchema = {
|
|
|
263
308
|
},
|
|
264
309
|
time: {
|
|
265
310
|
type: "string",
|
|
266
|
-
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."
|
|
311
|
+
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. Ask for clarification before scheduling if the time is ambiguous."
|
|
267
312
|
},
|
|
268
313
|
timezone: {
|
|
269
314
|
type: "string",
|
|
270
|
-
description: "
|
|
315
|
+
description: "Optional IANA timezone used for cron reminders. Include it when the user provides or confirms a timezone; if omitted, QQBot preserves its existing default timezone."
|
|
271
316
|
},
|
|
272
317
|
name: {
|
|
273
318
|
type: "string",
|
|
@@ -333,7 +378,7 @@ function isCronExpression(timeStr) {
|
|
|
333
378
|
*/
|
|
334
379
|
function generateJobName(content) {
|
|
335
380
|
const trimmed = content.trim();
|
|
336
|
-
return `Reminder: ${trimmed.length > 20 ? `${trimmed
|
|
381
|
+
return `Reminder: ${trimmed.length > 20 ? `${truncateUtf16Safe(trimmed, 20)}…` : trimmed}`;
|
|
337
382
|
}
|
|
338
383
|
/** Build the reminder system prompt sent to the AI. */
|
|
339
384
|
function buildReminderPrompt(content) {
|
|
@@ -370,7 +415,7 @@ function buildOnceJob(params, atMs, to, accountId) {
|
|
|
370
415
|
function buildCronJob(params, to, accountId) {
|
|
371
416
|
const content = params.content;
|
|
372
417
|
const name = params.name || generateJobName(content);
|
|
373
|
-
const tz = params.timezone ||
|
|
418
|
+
const tz = params.timezone || QQBOT_DEFAULT_REMINDER_TIMEZONE;
|
|
374
419
|
return {
|
|
375
420
|
action: "add",
|
|
376
421
|
job: {
|
|
@@ -456,7 +501,7 @@ function prepareRemindCronAction(params, ctx = {}) {
|
|
|
456
501
|
ok: true,
|
|
457
502
|
action: "add",
|
|
458
503
|
cronAction: buildCronJob(params, resolvedTo, resolvedAccountId),
|
|
459
|
-
summary: `⏰ Recurring reminder: "${params.content}" (${params.time}, tz=${params.timezone ||
|
|
504
|
+
summary: `⏰ Recurring reminder: "${params.content}" (${params.time}, tz=${params.timezone || QQBOT_DEFAULT_REMINDER_TIMEZONE})`
|
|
460
505
|
};
|
|
461
506
|
const delayMs = parseRelativeTime(params.time);
|
|
462
507
|
if (delayMs == null) return {
|
|
@@ -515,7 +560,7 @@ function createRemindTool(toolContext = {}, deps = defaultDeps) {
|
|
|
515
560
|
return {
|
|
516
561
|
name: "qqbot_remind",
|
|
517
562
|
label: "QQBot Reminder",
|
|
518
|
-
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 * * *\"",
|
|
563
|
+
description: "Create, list, and remove QQ reminders. Use only for explicit user requests, and ask when reminder content, schedule, or timezone is ambiguous. 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 * * *\"; include timezone for recurring cron reminders when known.",
|
|
519
564
|
parameters: RemindSchema,
|
|
520
565
|
async execute(_toolCallId, params) {
|
|
521
566
|
const ctx = getRequestContext();
|
package/dist/tools-api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as registerQQBotTools } from "./tools-
|
|
1
|
+
import { t as registerQQBotTools } from "./tools-C8kTMXKc.js";
|
|
2
2
|
export { registerQQBotTools };
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/qqbot",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.7.1-beta.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/qqbot",
|
|
9
|
-
"version": "2026.
|
|
9
|
+
"version": "2026.7.1-beta.2",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@tencent-connect/qqbot-connector": "1.1.0",
|
|
12
12
|
"mpg123-decoder": "1.0.3",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"zod": "4.4.3"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"openclaw": ">=2026.
|
|
18
|
+
"openclaw": ">=2026.7.1-beta.2"
|
|
19
19
|
},
|
|
20
20
|
"peerDependenciesMeta": {
|
|
21
21
|
"openclaw": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/qqbot",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.7.1-beta.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OpenClaw QQ Bot channel plugin for group and direct-message workflows.",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"zod": "4.4.3"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"openclaw": ">=2026.
|
|
19
|
+
"openclaw": ">=2026.7.1-beta.2"
|
|
20
20
|
},
|
|
21
21
|
"peerDependenciesMeta": {
|
|
22
22
|
"openclaw": {
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"minHostVersion": ">=2026.4.10"
|
|
46
46
|
},
|
|
47
47
|
"compat": {
|
|
48
|
-
"pluginApi": ">=2026.
|
|
48
|
+
"pluginApi": ">=2026.7.1-beta.2"
|
|
49
49
|
},
|
|
50
50
|
"build": {
|
|
51
|
-
"openclawVersion": "2026.
|
|
51
|
+
"openclawVersion": "2026.7.1-beta.2"
|
|
52
52
|
},
|
|
53
53
|
"release": {
|
|
54
54
|
"publishToClawHub": true,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qqbot-channel
|
|
3
|
-
description: QQ channel management skill. Use qqbot_channel_api
|
|
3
|
+
description: QQ channel management skill. Use qqbot_channel_api for explicit QQ channel-management requests; confirm write, delete, and bulk actions before calling authenticated QQ Open Platform endpoints.
|
|
4
4
|
metadata: { "openclaw": { "emoji": "📡", "requires": { "config": ["channels.qqbot"] } } }
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -18,15 +18,25 @@ metadata: { "openclaw": { "emoji": "📡", "requires": { "config": ["channels.qq
|
|
|
18
18
|
|
|
19
19
|
## 🔧 工具参数
|
|
20
20
|
|
|
21
|
-
| 参数
|
|
22
|
-
|
|
|
23
|
-
| `method`
|
|
24
|
-
| `path`
|
|
25
|
-
| `body`
|
|
26
|
-
| `query`
|
|
21
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
22
|
+
| --------------- | ------- | ---- | ---------------------------------------------------------------------------- |
|
|
23
|
+
| `method` | string | 是 | HTTP 方法:`GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
|
|
24
|
+
| `path` | string | 是 | API 路径(不含域名),如 `/guilds/{guild_id}/channels`,需替换占位符为实际值 |
|
|
25
|
+
| `body` | object | 否 | 请求体 JSON(POST/PUT/PATCH 使用) |
|
|
26
|
+
| `query` | object | 否 | URL 查询参数键值对,值为字符串类型 |
|
|
27
|
+
| `confirmed` | boolean | 否 | `DELETE` 必须传 `true`,表示用户已确认精确删除目标 |
|
|
28
|
+
| `bulkConfirmed` | boolean | 否 | 批量 `DELETE`(如删除全部公告)必须额外传 `true` |
|
|
27
29
|
|
|
28
30
|
> 基础 URL:`https://api.sgroup.qq.com`,鉴权头 `Authorization: QQBot {token}` 由工具自动填充。
|
|
29
31
|
|
|
32
|
+
## 🛡️ 安全边界
|
|
33
|
+
|
|
34
|
+
- 只在用户明确要求管理 QQ 频道、子频道、公告、论坛帖子或日程时调用写入接口。
|
|
35
|
+
- `POST`、`PUT`、`PATCH` 和 `DELETE` 会修改真实 QQ 资源。调用前先复述目标频道/子频道/帖子/日程和预期改动;删除、批量删除、公告覆盖等不可逆或大范围操作必须等用户确认后再执行。
|
|
36
|
+
- 删除前优先用 `GET`/列表接口查出候选项,让用户选择具体 ID;不要根据模糊名称猜测删除目标。
|
|
37
|
+
- `DELETE` 请求必须传 `confirmed: true`,否则工具会拒绝执行。`announces/all` 这样的批量操作还必须传 `bulkConfirmed: true`,只有在用户明确说要删除全部公告并再次确认后才可使用。
|
|
38
|
+
- 成员资料、头像 URL、频道图标等属于用户/群组资料。默认只总结必要字段;只有用户要求查看头像/图标或视觉比对时才内联展示图片,不要无关转发头像 URL。
|
|
39
|
+
|
|
30
40
|
---
|
|
31
41
|
|
|
32
42
|
## ⭐ 接口速查
|
|
@@ -40,13 +50,13 @@ metadata: { "openclaw": { "emoji": "📡", "requires": { "config": ["channels.qq
|
|
|
40
50
|
|
|
41
51
|
### 子频道(Channel)
|
|
42
52
|
|
|
43
|
-
| 操作 | 方法
|
|
44
|
-
| -------------- |
|
|
45
|
-
| 获取子频道列表 | `GET`
|
|
46
|
-
| 获取子频道详情 | `GET`
|
|
47
|
-
| 创建子频道 | `POST`
|
|
48
|
-
| 修改子频道 | `PATCH`
|
|
49
|
-
| 删除子频道 |
|
|
53
|
+
| 操作 | 方法 | 路径 | 参数说明 |
|
|
54
|
+
| -------------- | ------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| 获取子频道列表 | `GET` | `/guilds/{guild_id}/channels` | — |
|
|
56
|
+
| 获取子频道详情 | `GET` | `/channels/{channel_id}` | — |
|
|
57
|
+
| 创建子频道 | `POST` | `/guilds/{guild_id}/channels` | body: `name`\*, `type`\*, `position`\*, `sub_type`, `parent_id`, `private_type`, `private_user_ids`, `speak_permission`, `application_id` |
|
|
58
|
+
| 修改子频道 | `PATCH` | `/channels/{channel_id}` | body: `name`, `position`, `parent_id`, `private_type`, `speak_permission`(至少一个) |
|
|
59
|
+
| 删除子频道 | — | 见受确认保护的删除流程 | 破坏性操作;不要在未确认时调用 |
|
|
50
60
|
|
|
51
61
|
**子频道类型(type)**:`0`=文字, `2`=语音, `4`=分组(position≥2), `10005`=直播, `10006`=应用, `10007`=论坛
|
|
52
62
|
|
|
@@ -61,28 +71,28 @@ metadata: { "openclaw": { "emoji": "📡", "requires": { "config": ["channels.qq
|
|
|
61
71
|
|
|
62
72
|
### 公告(Announces)
|
|
63
73
|
|
|
64
|
-
| 操作 | 方法
|
|
65
|
-
| -------- |
|
|
66
|
-
| 创建公告 | `POST`
|
|
67
|
-
| 删除公告 |
|
|
74
|
+
| 操作 | 方法 | 路径 | 参数说明 |
|
|
75
|
+
| -------- | ------ | ------------------------------ | ------------------------------------------------------------------------------------------------ |
|
|
76
|
+
| 创建公告 | `POST` | `/guilds/{guild_id}/announces` | body: `message_id`, `channel_id`, `announces_type`(0=成员,1=欢迎), `recommend_channels`(最多3条) |
|
|
77
|
+
| 删除公告 | — | 见受确认保护的删除流程 | 破坏性操作;批量删除需二次确认 |
|
|
68
78
|
|
|
69
79
|
### 论坛(Forum)— 仅私域机器人
|
|
70
80
|
|
|
71
|
-
| 操作 | 方法
|
|
72
|
-
| ------------ |
|
|
73
|
-
| 获取帖子列表 | `GET`
|
|
74
|
-
| 获取帖子详情 | `GET`
|
|
75
|
-
| 发表帖子 | `PUT`
|
|
76
|
-
| 删除帖子 |
|
|
77
|
-
| 发表评论 | `POST`
|
|
81
|
+
| 操作 | 方法 | 路径 | 参数说明 |
|
|
82
|
+
| ------------ | ------ | ---------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
83
|
+
| 获取帖子列表 | `GET` | `/channels/{channel_id}/threads` | — |
|
|
84
|
+
| 获取帖子详情 | `GET` | `/channels/{channel_id}/threads/{thread_id}` | — |
|
|
85
|
+
| 发表帖子 | `PUT` | `/channels/{channel_id}/threads` | body: `title`\*, `content`\*, `format`(1=文本,2=HTML,3=Markdown,4=JSON,默认3) |
|
|
86
|
+
| 删除帖子 | — | 见受确认保护的删除流程 | 破坏性操作;不要在未确认时调用 |
|
|
87
|
+
| 发表评论 | `POST` | `/channels/{channel_id}/threads/{thread_id}/comment` | body: `thread_author`\*, `content`\*, `thread_create_time`, `image` |
|
|
78
88
|
|
|
79
89
|
### 日程(Schedule)
|
|
80
90
|
|
|
81
|
-
| 操作 | 方法
|
|
82
|
-
| -------- |
|
|
83
|
-
| 创建日程 | `POST`
|
|
84
|
-
| 修改日程 | `PATCH`
|
|
85
|
-
| 删除日程 |
|
|
91
|
+
| 操作 | 方法 | 路径 | 参数说明 |
|
|
92
|
+
| -------- | ------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
93
|
+
| 创建日程 | `POST` | `/channels/{channel_id}/schedules` | body: `{ schedule: { name*, start_timestamp*, end_timestamp*, jump_channel_id, remind_type } }` |
|
|
94
|
+
| 修改日程 | `PATCH` | `/channels/{channel_id}/schedules/{schedule_id}` | body: `{ schedule: { name*, start_timestamp*, end_timestamp*, jump_channel_id, remind_type } }` |
|
|
95
|
+
| 删除日程 | — | 见受确认保护的删除流程 | 破坏性操作;不要在未确认时调用 |
|
|
86
96
|
|
|
87
97
|
**提醒类型(remind_type)**:`"0"`=不提醒, `"1"`=开始时, `"2"`=5分钟前, `"3"`=15分钟前, `"4"`=30分钟前, `"5"`=60分钟前
|
|
88
98
|
|
|
@@ -180,14 +190,17 @@ metadata: { "openclaw": { "emoji": "📡", "requires": { "config": ["channels.qq
|
|
|
180
190
|
}
|
|
181
191
|
```
|
|
182
192
|
|
|
183
|
-
###
|
|
193
|
+
### 受确认保护的删除流程
|
|
184
194
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
195
|
+
删除类 QQ API 不作为普通速查示例暴露。若用户明确要求删除资源,先读取并复述目标对象,确认后再调用 `qqbot_channel_api`:`method` 设为 `"DELETE"`,`confirmed` 设为 `true`,`path` 使用已确认对象对应的资源路径。
|
|
196
|
+
|
|
197
|
+
| 删除对象 | 已确认后使用的 `path` | 额外要求 |
|
|
198
|
+
| -------- | ------------------------------------------------ | ---------------------------------------- |
|
|
199
|
+
| 子频道 | `/channels/{channel_id}` | 确认子频道 ID 和名称 |
|
|
200
|
+
| 单条公告 | `/guilds/{guild_id}/announces/{message_id}` | 确认公告 ID |
|
|
201
|
+
| 全部公告 | `/guilds/{guild_id}/announces/all` | 用户再次确认后再传 `bulkConfirmed: true` |
|
|
202
|
+
| 帖子 | `/channels/{channel_id}/threads/{thread_id}` | 确认帖子 ID、标题/作者 |
|
|
203
|
+
| 日程 | `/channels/{channel_id}/schedules/{schedule_id}` | 确认日程 ID、名称/时间 |
|
|
191
204
|
|
|
192
205
|
---
|
|
193
206
|
|
|
@@ -222,7 +235,7 @@ metadata: { "openclaw": { "emoji": "📡", "requires": { "config": ["channels.qq
|
|
|
222
235
|
|
|
223
236
|
### 展示成员头像
|
|
224
237
|
|
|
225
|
-
成员详情返回的 `user.avatar` 是头像 URL
|
|
238
|
+
成员详情返回的 `user.avatar` 是头像 URL。默认只展示昵称、ID、加入时间等必要字段;当用户明确要求查看头像/图标或头像是当前任务的必要依据时,再用 Markdown 图片语法内联展示:
|
|
226
239
|
|
|
227
240
|
```
|
|
228
241
|
成员信息:
|
|
@@ -231,7 +244,7 @@ metadata: { "openclaw": { "emoji": "📡", "requires": { "config": ["channels.qq
|
|
|
231
244
|

|
|
232
245
|
```
|
|
233
246
|
|
|
234
|
-
|
|
247
|
+
不要无关输出原始头像 URL 或把头像作为普通链接转发。频道的 `icon` 字段同理:仅在用户明确需要查看时展示。
|
|
235
248
|
|
|
236
249
|
---
|
|
237
250
|
|
|
@@ -255,8 +268,8 @@ metadata: { "openclaw": { "emoji": "📡", "requires": { "config": ["channels.qq
|
|
|
255
268
|
3. **成员列表翻页**时可能返回重复成员,需按 `user.id` 去重
|
|
256
269
|
4. **公告**的两种类型(消息公告和推荐子频道公告)会互相顶替
|
|
257
270
|
5. **日程**的时间戳为毫秒级字符串
|
|
258
|
-
6.
|
|
271
|
+
6. **删除操作不可逆**,必须先确认精确目标并传 `confirmed: true`;批量删除需二次确认并传 `bulkConfirmed: true`
|
|
259
272
|
7. **论坛操作**仅私域机器人可用
|
|
260
273
|
8. **子频道分组**(type=4)的 `position` 必须 >= 2
|
|
261
274
|
9. **日程操作**有频率限制:单个管理员每天 10 次,单个频道每天 100 次
|
|
262
|
-
10. **头像/图标展示**:成员 `user.avatar` 和频道 `icon` 等图片 URL
|
|
275
|
+
10. **头像/图标展示**:成员 `user.avatar` 和频道 `icon` 等图片 URL 属于资料信息;默认总结必要字段,只在用户明确需要查看图片时用 Markdown 图片语法 `` 展示
|