@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.
@@ -1,4 +1,4 @@
1
- import { v as setOpenClawVersion } from "./sender-CRZ2af7P.js";
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({
@@ -1,2 +1,2 @@
1
- import { r as setQQBotRuntime, t as getQQBotRuntime } from "./runtime-D1hn91-s.js";
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.text() : await readResponseTextLimited(res, limitBytes);
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$1(delay);
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$1(actualDelay);
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
  /**
@@ -1,2 +1,2 @@
1
- import { t as qqbotSetupPlugin } from "./channel.setup-CldhUlTO.js";
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
- export { normalizeCompatibilityConfig as n, legacyConfigRules as t };
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.text() : await readResponseTextLimited(res, CHANNEL_API_ERROR_BODY_LIMIT_BYTES);
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-CRZ2af7P.js").then((n) => n._);
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: "Timezone used for cron reminders. Defaults to \"Asia/Shanghai\"."
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.slice(0, 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 || "Asia/Shanghai";
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 || "Asia/Shanghai"})`
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-CeUI9pG-.js";
1
+ import { t as registerQQBotTools } from "./tools-C8kTMXKc.js";
2
2
  export { registerQQBotTools };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/qqbot",
3
- "version": "2026.6.11",
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.6.11",
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.6.11"
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.6.11",
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.6.11"
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.6.11"
48
+ "pluginApi": ">=2026.7.1-beta.2"
49
49
  },
50
50
  "build": {
51
- "openclawVersion": "2026.6.11"
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 to list guilds and channels, inspect members, publish posts, manage announcements, and work with schedules through the QQ Open Platform HTTP API with automatic token authentication.
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` | 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 查询参数键值对,值为字符串类型 |
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` | `/guilds/{guild_id}/channels` | — |
46
- | 获取子频道详情 | `GET` | `/channels/{channel_id}` | — |
47
- | 创建子频道 | `POST` | `/guilds/{guild_id}/channels` | body: `name`\*, `type`\*, `position`\*, `sub_type`, `parent_id`, `private_type`, `private_user_ids`, `speak_permission`, `application_id` |
48
- | 修改子频道 | `PATCH` | `/channels/{channel_id}` | body: `name`, `position`, `parent_id`, `private_type`, `speak_permission`(至少一个) |
49
- | 删除子频道 | `DELETE` | `/channels/{channel_id}` | ⚠️ 不可逆 |
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` | `/guilds/{guild_id}/announces` | body: `message_id`, `channel_id`, `announces_type`(0=成员,1=欢迎), `recommend_channels`(最多3条) |
67
- | 删除公告 | `DELETE` | `/guilds/{guild_id}/announces/{message_id}` | message_id 设 `all` 删除所有 |
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` | `/channels/{channel_id}/threads` | — |
74
- | 获取帖子详情 | `GET` | `/channels/{channel_id}/threads/{thread_id}` | — |
75
- | 发表帖子 | `PUT` | `/channels/{channel_id}/threads` | body: `title`\*, `content`\*, `format`(1=文本,2=HTML,3=Markdown,4=JSON,默认3) |
76
- | 删除帖子 | `DELETE` | `/channels/{channel_id}/threads/{thread_id}` | ⚠️ 不可逆 |
77
- | 发表评论 | `POST` | `/channels/{channel_id}/threads/{thread_id}/comment` | body: `thread_author`\*, `content`\*, `thread_create_time`, `image` |
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` | `/channels/{channel_id}/schedules` | body: `{ schedule: { name*, start_timestamp*, end_timestamp*, jump_channel_id, remind_type } }` |
84
- | 修改日程 | `PATCH` | `/channels/{channel_id}/schedules/{schedule_id}` | body: `{ schedule: { name*, start_timestamp*, end_timestamp*, jump_channel_id, remind_type } }` |
85
- | 删除日程 | `DELETE` | `/channels/{channel_id}/schedules/{schedule_id}` | ⚠️ 不可逆 |
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
- ```json
186
- {
187
- "method": "DELETE",
188
- "path": "/guilds/123456/announces/all"
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,**必须使用 Markdown 图片语法展示**,让用户直接看到头像图片,而非纯文本链接:
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
  ![头像]({user.avatar})
232
245
  ```
233
246
 
234
- > **禁止**将头像 URL 作为纯文本或超链接展示(如 `查看头像`),必须用 `![描述](URL)` 语法内联显示。频道的 `icon` 字段同理。
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 必须使用 Markdown 图片语法 `![描述](URL)` 展示,禁止作为纯文本或超链接展示
275
+ 10. **头像/图标展示**:成员 `user.avatar` 和频道 `icon` 等图片 URL 属于资料信息;默认总结必要字段,只在用户明确需要查看图片时用 Markdown 图片语法 `![描述](URL)` 展示