@openclaw/qqbot 2026.5.2 → 2026.5.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/dist/api.js +646 -0
  2. package/dist/approval-cg0SVahb.js +94 -0
  3. package/dist/channel-N6Y_Rcjp.js +551 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.setup-ByCRAe5H.js +25 -0
  6. package/dist/config-D6545NkC.js +94 -0
  7. package/dist/config-schema-DFcjQw73.js +309 -0
  8. package/dist/exec-approvals-COUsM6wZ.js +138 -0
  9. package/dist/gateway-CmSUJKSt.js +6726 -0
  10. package/dist/handler-runtime-Bqm6N0WG.js +120 -0
  11. package/dist/index.js +29 -0
  12. package/dist/narrowing-BoieBTIU.js +25 -0
  13. package/dist/outbound-BJfhwrPg.js +1357 -0
  14. package/dist/request-context-DXtpwWui.js +921 -0
  15. package/dist/resolve-D_06fV6-.js +284 -0
  16. package/dist/runtime-BJAS3eXW.js +18 -0
  17. package/dist/runtime-api.js +2 -0
  18. package/dist/secret-contract-api.js +56 -0
  19. package/dist/sender-p-B14eLG.js +1987 -0
  20. package/dist/setup-entry.js +15 -0
  21. package/dist/setup-plugin-api.js +2 -0
  22. package/dist/string-normalize-Ci6NM5DE.js +98 -0
  23. package/dist/target-parser-Y0prnrXD.js +245 -0
  24. package/package.json +15 -6
  25. package/api.ts +0 -56
  26. package/channel-plugin-api.ts +0 -1
  27. package/index.ts +0 -29
  28. package/runtime-api.ts +0 -9
  29. package/setup-entry.ts +0 -9
  30. package/setup-plugin-api.ts +0 -3
  31. package/src/bridge/approval/capability.ts +0 -237
  32. package/src/bridge/approval/handler-runtime.ts +0 -204
  33. package/src/bridge/bootstrap.ts +0 -135
  34. package/src/bridge/channel-entry.ts +0 -18
  35. package/src/bridge/commands/framework-context-adapter.ts +0 -60
  36. package/src/bridge/commands/framework-registration.ts +0 -47
  37. package/src/bridge/commands/from-parser.test.ts +0 -86
  38. package/src/bridge/commands/from-parser.ts +0 -60
  39. package/src/bridge/commands/result-dispatcher.ts +0 -76
  40. package/src/bridge/config-shared.ts +0 -132
  41. package/src/bridge/config.ts +0 -111
  42. package/src/bridge/gateway.ts +0 -174
  43. package/src/bridge/logger.ts +0 -31
  44. package/src/bridge/narrowing.ts +0 -31
  45. package/src/bridge/plugin-version.test.ts +0 -146
  46. package/src/bridge/plugin-version.ts +0 -102
  47. package/src/bridge/runtime.ts +0 -25
  48. package/src/bridge/sdk-adapter.ts +0 -131
  49. package/src/bridge/setup/finalize.ts +0 -144
  50. package/src/bridge/setup/surface.ts +0 -34
  51. package/src/bridge/tools/channel.ts +0 -58
  52. package/src/bridge/tools/index.ts +0 -15
  53. package/src/bridge/tools/remind.test.ts +0 -124
  54. package/src/bridge/tools/remind.ts +0 -91
  55. package/src/channel.setup.ts +0 -33
  56. package/src/channel.ts +0 -288
  57. package/src/command-auth.test.ts +0 -62
  58. package/src/config-schema.ts +0 -84
  59. package/src/config.test.ts +0 -364
  60. package/src/engine/access/access-control.test.ts +0 -198
  61. package/src/engine/access/access-control.ts +0 -226
  62. package/src/engine/access/index.ts +0 -16
  63. package/src/engine/access/resolve-policy.test.ts +0 -59
  64. package/src/engine/access/resolve-policy.ts +0 -57
  65. package/src/engine/access/sender-match.test.ts +0 -60
  66. package/src/engine/access/sender-match.ts +0 -55
  67. package/src/engine/access/types.ts +0 -53
  68. package/src/engine/adapter/audio.port.ts +0 -27
  69. package/src/engine/adapter/commands.port.ts +0 -22
  70. package/src/engine/adapter/history.port.ts +0 -52
  71. package/src/engine/adapter/index.ts +0 -139
  72. package/src/engine/adapter/mention-gate.port.ts +0 -50
  73. package/src/engine/adapter/types.ts +0 -38
  74. package/src/engine/api/api-client.ts +0 -212
  75. package/src/engine/api/media-chunked.test.ts +0 -336
  76. package/src/engine/api/media-chunked.ts +0 -622
  77. package/src/engine/api/media.ts +0 -218
  78. package/src/engine/api/messages.ts +0 -293
  79. package/src/engine/api/retry.ts +0 -217
  80. package/src/engine/api/routes.ts +0 -95
  81. package/src/engine/api/token.ts +0 -271
  82. package/src/engine/approval/index.test.ts +0 -22
  83. package/src/engine/approval/index.ts +0 -224
  84. package/src/engine/commands/builtin/log-helpers.ts +0 -319
  85. package/src/engine/commands/builtin/register-all.ts +0 -17
  86. package/src/engine/commands/builtin/register-approve.ts +0 -201
  87. package/src/engine/commands/builtin/register-basic.ts +0 -95
  88. package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
  89. package/src/engine/commands/builtin/register-logs.ts +0 -20
  90. package/src/engine/commands/builtin/register-streaming.ts +0 -137
  91. package/src/engine/commands/builtin/state.ts +0 -31
  92. package/src/engine/commands/slash-command-auth.ts +0 -48
  93. package/src/engine/commands/slash-command-handler.ts +0 -146
  94. package/src/engine/commands/slash-commands-impl.test.ts +0 -8
  95. package/src/engine/commands/slash-commands-impl.ts +0 -61
  96. package/src/engine/commands/slash-commands.ts +0 -199
  97. package/src/engine/config/credential-backup.test.ts +0 -88
  98. package/src/engine/config/credential-backup.ts +0 -107
  99. package/src/engine/config/credentials.ts +0 -76
  100. package/src/engine/config/group.test.ts +0 -234
  101. package/src/engine/config/group.ts +0 -299
  102. package/src/engine/config/resolve.test.ts +0 -152
  103. package/src/engine/config/resolve.ts +0 -283
  104. package/src/engine/config/setup-logic.ts +0 -84
  105. package/src/engine/engine-import-boundary.test.ts +0 -73
  106. package/src/engine/gateway/codec.ts +0 -47
  107. package/src/engine/gateway/constants.ts +0 -117
  108. package/src/engine/gateway/event-dispatcher.ts +0 -177
  109. package/src/engine/gateway/gateway-connection.ts +0 -371
  110. package/src/engine/gateway/gateway.ts +0 -291
  111. package/src/engine/gateway/inbound-attachments.test.ts +0 -126
  112. package/src/engine/gateway/inbound-attachments.ts +0 -360
  113. package/src/engine/gateway/inbound-context.ts +0 -195
  114. package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
  115. package/src/engine/gateway/inbound-pipeline.ts +0 -235
  116. package/src/engine/gateway/interaction-handler.ts +0 -220
  117. package/src/engine/gateway/message-queue.test.ts +0 -282
  118. package/src/engine/gateway/message-queue.ts +0 -499
  119. package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
  120. package/src/engine/gateway/outbound-dispatch.ts +0 -575
  121. package/src/engine/gateway/reconnect.ts +0 -199
  122. package/src/engine/gateway/stages/access-stage.ts +0 -132
  123. package/src/engine/gateway/stages/assembly-stage.ts +0 -156
  124. package/src/engine/gateway/stages/content-stage.test.ts +0 -77
  125. package/src/engine/gateway/stages/content-stage.ts +0 -77
  126. package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
  127. package/src/engine/gateway/stages/envelope-stage.ts +0 -144
  128. package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
  129. package/src/engine/gateway/stages/index.ts +0 -18
  130. package/src/engine/gateway/stages/quote-stage.ts +0 -113
  131. package/src/engine/gateway/stages/refidx-stage.ts +0 -62
  132. package/src/engine/gateway/stages/stub-contexts.ts +0 -116
  133. package/src/engine/gateway/types.ts +0 -264
  134. package/src/engine/gateway/typing-keepalive.ts +0 -79
  135. package/src/engine/group/activation.test.ts +0 -114
  136. package/src/engine/group/activation.ts +0 -147
  137. package/src/engine/group/history.test.ts +0 -314
  138. package/src/engine/group/history.ts +0 -321
  139. package/src/engine/group/mention.test.ts +0 -141
  140. package/src/engine/group/mention.ts +0 -197
  141. package/src/engine/group/message-gating.test.ts +0 -188
  142. package/src/engine/group/message-gating.ts +0 -216
  143. package/src/engine/messaging/decode-media-path.ts +0 -82
  144. package/src/engine/messaging/media-source.ts +0 -215
  145. package/src/engine/messaging/media-type-detect.ts +0 -37
  146. package/src/engine/messaging/outbound-audio-port.ts +0 -38
  147. package/src/engine/messaging/outbound-deliver.ts +0 -810
  148. package/src/engine/messaging/outbound-media-send.ts +0 -702
  149. package/src/engine/messaging/outbound-reply.ts +0 -27
  150. package/src/engine/messaging/outbound-result-helpers.ts +0 -54
  151. package/src/engine/messaging/outbound-types.ts +0 -45
  152. package/src/engine/messaging/outbound.ts +0 -485
  153. package/src/engine/messaging/reply-dispatcher.ts +0 -597
  154. package/src/engine/messaging/reply-limiter.ts +0 -164
  155. package/src/engine/messaging/sender.ts +0 -729
  156. package/src/engine/messaging/streaming-c2c.ts +0 -1192
  157. package/src/engine/messaging/streaming-media-send.ts +0 -544
  158. package/src/engine/messaging/target-parser.ts +0 -104
  159. package/src/engine/ref/format-message-ref.ts +0 -142
  160. package/src/engine/ref/format-ref-entry.test.ts +0 -60
  161. package/src/engine/ref/format-ref-entry.ts +0 -27
  162. package/src/engine/ref/store.ts +0 -208
  163. package/src/engine/ref/types.ts +0 -27
  164. package/src/engine/session/known-users.ts +0 -137
  165. package/src/engine/session/session-store.ts +0 -204
  166. package/src/engine/tools/channel-api.ts +0 -244
  167. package/src/engine/tools/remind-logic.test.ts +0 -280
  168. package/src/engine/tools/remind-logic.ts +0 -377
  169. package/src/engine/types.ts +0 -313
  170. package/src/engine/utils/attachment-tags.test.ts +0 -186
  171. package/src/engine/utils/attachment-tags.ts +0 -174
  172. package/src/engine/utils/audio.test.ts +0 -250
  173. package/src/engine/utils/audio.ts +0 -585
  174. package/src/engine/utils/data-paths.ts +0 -38
  175. package/src/engine/utils/diagnostics.ts +0 -109
  176. package/src/engine/utils/file-utils.test.ts +0 -72
  177. package/src/engine/utils/file-utils.ts +0 -225
  178. package/src/engine/utils/format.test.ts +0 -68
  179. package/src/engine/utils/format.ts +0 -70
  180. package/src/engine/utils/image-size.test.ts +0 -158
  181. package/src/engine/utils/image-size.ts +0 -249
  182. package/src/engine/utils/log.test.ts +0 -28
  183. package/src/engine/utils/log.ts +0 -61
  184. package/src/engine/utils/media-tags.test.ts +0 -32
  185. package/src/engine/utils/media-tags.ts +0 -177
  186. package/src/engine/utils/payload.test.ts +0 -68
  187. package/src/engine/utils/payload.ts +0 -145
  188. package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
  189. package/src/engine/utils/platform.test.ts +0 -148
  190. package/src/engine/utils/platform.ts +0 -343
  191. package/src/engine/utils/request-context.ts +0 -60
  192. package/src/engine/utils/string-normalize.ts +0 -91
  193. package/src/engine/utils/stt.test.ts +0 -104
  194. package/src/engine/utils/stt.ts +0 -100
  195. package/src/engine/utils/text-parsing.test.ts +0 -29
  196. package/src/engine/utils/text-parsing.ts +0 -155
  197. package/src/engine/utils/upload-cache.ts +0 -96
  198. package/src/engine/utils/voice-text.ts +0 -15
  199. package/src/exec-approvals.ts +0 -218
  200. package/src/manifest-schema.test.ts +0 -56
  201. package/src/qqbot-test-support.ts +0 -29
  202. package/src/types.ts +0 -210
  203. package/tsconfig.json +0 -16
@@ -0,0 +1,94 @@
1
+ //#region extensions/qqbot/src/engine/approval/index.ts
2
+ function buildExecApprovalText(request) {
3
+ const expiresIn = Math.max(0, Math.round((request.expiresAtMs - Date.now()) / 1e3));
4
+ const lines = ["🔐 命令执行审批", ""];
5
+ const cmd = request.request.commandPreview ?? request.request.command ?? "";
6
+ if (cmd) lines.push(`\`\`\`\n${cmd.slice(0, 300)}\n\`\`\``);
7
+ if (request.request.cwd) lines.push(`\u{1f4c1} \u76ee\u5f55: ${request.request.cwd}`);
8
+ if (request.request.agentId) lines.push(`\u{1f916} Agent: ${request.request.agentId}`);
9
+ lines.push("", `\u23f1\ufe0f \u8d85\u65f6: ${expiresIn} \u79d2`);
10
+ return lines.join("\n");
11
+ }
12
+ function buildPluginApprovalText(request) {
13
+ const timeoutSec = Math.round((request.request.timeoutMs ?? 12e4) / 1e3);
14
+ const lines = [`${request.request.severity === "critical" ? "🔴" : request.request.severity === "info" ? "🔵" : "🟡"} \u5ba1\u6279\u8bf7\u6c42`, ""];
15
+ lines.push(`\u{1f4cb} ${request.request.title}`);
16
+ if (request.request.description) lines.push(`\u{1f4dd} ${request.request.description}`);
17
+ if (request.request.toolName) lines.push(`\u{1f527} \u5de5\u5177: ${request.request.toolName}`);
18
+ if (request.request.pluginId) lines.push(`\u{1f50c} \u63d2\u4ef6: ${request.request.pluginId}`);
19
+ if (request.request.agentId) lines.push(`\u{1f916} Agent: ${request.request.agentId}`);
20
+ lines.push("", `\u23f1\ufe0f \u8d85\u65f6: ${timeoutSec} \u79d2`);
21
+ return lines.join("\n");
22
+ }
23
+ /**
24
+ * Build the three-button inline keyboard for approval messages.
25
+ *
26
+ * type=1 (Callback): click triggers INTERACTION_CREATE, button_data = data field.
27
+ * group_id "approval": clicking one button grays out the others (mutual exclusion).
28
+ * click_limit=1: each user can only click once.
29
+ * permission.type=2: all users can interact.
30
+ */
31
+ function buildApprovalKeyboard(approvalId, allowedDecisions = [
32
+ "allow-once",
33
+ "allow-always",
34
+ "deny"
35
+ ]) {
36
+ const makeBtn = (id, label, visitedLabel, data, style) => ({
37
+ id,
38
+ render_data: {
39
+ label,
40
+ visited_label: visitedLabel,
41
+ style
42
+ },
43
+ action: {
44
+ type: 1,
45
+ data,
46
+ permission: { type: 2 },
47
+ click_limit: 1
48
+ },
49
+ group_id: "approval"
50
+ });
51
+ const buttons = [];
52
+ if (allowedDecisions.includes("allow-once")) buttons.push(makeBtn("allow", "✅ 允许一次", "已允许", `approve:${approvalId}:allow-once`, 1));
53
+ if (allowedDecisions.includes("allow-always")) buttons.push(makeBtn("always", "⭐ 始终允许", "已始终允许", `approve:${approvalId}:allow-always`, 1));
54
+ if (allowedDecisions.includes("deny")) buttons.push(makeBtn("deny", "❌ 拒绝", "已拒绝", `approve:${approvalId}:deny`, 0));
55
+ return { content: { rows: [{ buttons }] } };
56
+ }
57
+ /**
58
+ * Extract the delivery target from a sessionKey or turnSourceTo string.
59
+ *
60
+ * Expected formats:
61
+ * agent:main:qqbot:direct:OPENID -> { type: "c2c", id: "OPENID" }
62
+ * agent:main:qqbot:c2c:OPENID -> { type: "c2c", id: "OPENID" }
63
+ * agent:main:qqbot:group:GROUPID -> { type: "group", id: "GROUPID" }
64
+ *
65
+ * Returns null if neither field matches the expected pattern.
66
+ */
67
+ function resolveApprovalTarget(sessionKey, turnSourceTo) {
68
+ const sk = sessionKey ?? turnSourceTo;
69
+ if (!sk) return null;
70
+ const m = sk.match(/qqbot:(c2c|direct|group):([A-F0-9]+)/i);
71
+ if (!m) return null;
72
+ return {
73
+ type: m[1].toLowerCase() === "group" ? "group" : "c2c",
74
+ id: m[2]
75
+ };
76
+ }
77
+ /**
78
+ * Parse the button_data string from an INTERACTION_CREATE event.
79
+ *
80
+ * Expected format: `approve:<approvalId>:<decision>`
81
+ * where approvalId may be prefixed with "exec:" or "plugin:".
82
+ *
83
+ * Returns null if the data does not match the approval button format.
84
+ */
85
+ function parseApprovalButtonData(buttonData) {
86
+ const m = buttonData.match(/^approve:((?:(?:exec|plugin):)?[0-9a-f-]+):(allow-once|allow-always|deny)$/i);
87
+ if (!m) return null;
88
+ return {
89
+ approvalId: m[1],
90
+ decision: m[2]
91
+ };
92
+ }
93
+ //#endregion
94
+ export { resolveApprovalTarget as a, parseApprovalButtonData as i, buildExecApprovalText as n, buildPluginApprovalText as r, buildApprovalKeyboard as t };
@@ -0,0 +1,551 @@
1
+ import { t as asOptionalObjectRecord } from "./string-normalize-Ci6NM5DE.js";
2
+ import { l as ensurePlatformAdapter, u as getBridgeLogger } from "./resolve-D_06fV6-.js";
3
+ import { a as resolveApprovalTarget } from "./approval-cg0SVahb.js";
4
+ import { a as resolveQQBotAccount, n as applyQQBotAccountConfig, t as DEFAULT_ACCOUNT_ID$1 } from "./config-D6545NkC.js";
5
+ import { a as resolveQQBotExecApprovalConfig, i as matchesQQBotApprovalAccount, n as isQQBotExecApprovalAuthorizedSender, o as shouldHandleQQBotExecApprovalRequest, r as isQQBotExecApprovalClientEnabled, t as isQQBotExecApprovalApprover } from "./exec-approvals-COUsM6wZ.js";
6
+ import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-DFcjQw73.js";
7
+ import { n as writeOpenClawConfigThroughRuntime, t as toGatewayAccount } from "./narrowing-BoieBTIU.js";
8
+ import { t as getQQBotRuntime } from "./runtime-BJAS3eXW.js";
9
+ import { n as normalizeTarget, s as getQQBotDataPath, t as looksLikeQQBotTarget } from "./target-parser-Y0prnrXD.js";
10
+ import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
11
+ import { createChannelApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime";
12
+ import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
13
+ import { resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime";
14
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ //#region extensions/qqbot/src/bridge/approval/capability.ts
18
+ /**
19
+ * QQ Bot Approval Capability — entry point.
20
+ *
21
+ * QQBot uses a simpler approval model than Telegram/Slack: any user who
22
+ * can see the inline-keyboard buttons can approve. No explicit approver
23
+ * list is required — the bot simply sends the approval message to the
24
+ * originating conversation and whoever clicks the button resolves it.
25
+ *
26
+ * When `execApprovals` IS configured, it gates which requests are
27
+ * handled natively and who is authorized. When it is NOT configured,
28
+ * QQBot falls back to "always handle, anyone can approve".
29
+ */
30
+ /**
31
+ * When `execApprovals` is configured, delegate to the profile-based
32
+ * check. Otherwise fall back to target-resolvability plus the shared
33
+ * per-account ownership rule in `matchesQQBotApprovalAccount` so that
34
+ * each QQBot account handler only delivers approvals that originated
35
+ * from its own account (openids are account-scoped — cross-account
36
+ * delivery fails with 500 on the QQ Bot API).
37
+ */
38
+ function shouldHandleRequest(params) {
39
+ if (hasExecApprovalConfig(params)) return shouldHandleQQBotExecApprovalRequest(params);
40
+ if (!canResolveTarget(params.request)) return false;
41
+ return matchesQQBotApprovalAccount({
42
+ cfg: params.cfg,
43
+ accountId: params.accountId,
44
+ request: params.request
45
+ });
46
+ }
47
+ function hasExecApprovalConfig(params) {
48
+ return resolveQQBotExecApprovalConfig(params) !== void 0;
49
+ }
50
+ function isNativeDeliveryEnabled(params) {
51
+ if (hasExecApprovalConfig(params)) return isQQBotExecApprovalClientEnabled(params);
52
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
53
+ return account.enabled && account.secretSource !== "none";
54
+ }
55
+ function canResolveTarget(request) {
56
+ if (resolveApprovalTarget(request.request.sessionKey ?? null, request.request.turnSourceTo ?? null)) return true;
57
+ return resolveApprovalRequestSessionConversation({
58
+ request,
59
+ channel: "qqbot",
60
+ bundledFallback: true
61
+ })?.id != null;
62
+ }
63
+ function createQQBotApprovalCapability() {
64
+ return createChannelApprovalCapability({
65
+ authorizeActorAction: ({ cfg, accountId, senderId, approvalKind }) => {
66
+ if (hasExecApprovalConfig({
67
+ cfg,
68
+ accountId
69
+ })) return (approvalKind === "plugin" ? isQQBotExecApprovalApprover({
70
+ cfg,
71
+ accountId,
72
+ senderId
73
+ }) : isQQBotExecApprovalAuthorizedSender({
74
+ cfg,
75
+ accountId,
76
+ senderId
77
+ })) ? { authorized: true } : {
78
+ authorized: false,
79
+ reason: "You are not authorized to approve this request."
80
+ };
81
+ return { authorized: true };
82
+ },
83
+ getActionAvailabilityState: ({ cfg, accountId }) => {
84
+ return isNativeDeliveryEnabled({
85
+ cfg,
86
+ accountId
87
+ }) ? { kind: "enabled" } : { kind: "disabled" };
88
+ },
89
+ getExecInitiatingSurfaceState: ({ cfg, accountId }) => {
90
+ return isNativeDeliveryEnabled({
91
+ cfg,
92
+ accountId
93
+ }) ? { kind: "enabled" } : { kind: "disabled" };
94
+ },
95
+ describeExecApprovalSetup: ({ accountId }) => {
96
+ return `QQBot native exec approvals are enabled by default. To restrict who can approve, configure \`${accountId && accountId !== "default" ? `channels.qqbot.accounts.${accountId}` : "channels.qqbot"}.execApprovals.approvers\` with QQ user OpenIDs.`;
97
+ },
98
+ delivery: {
99
+ hasConfiguredDmRoute: () => true,
100
+ shouldSuppressForwardingFallback: (input) => {
101
+ const channel = normalizeOptionalString(input.target?.channel);
102
+ if (channel !== "qqbot") return false;
103
+ const accountId = normalizeOptionalString(input.target?.accountId) ?? normalizeOptionalString(input.request?.request?.turnSourceAccountId);
104
+ const result = isNativeDeliveryEnabled({
105
+ cfg: input.cfg,
106
+ accountId
107
+ });
108
+ getBridgeLogger().debug?.(`[qqbot:approval] shouldSuppressForwardingFallback channel=${channel} accountId=${accountId} → ${result}`);
109
+ return result;
110
+ }
111
+ },
112
+ native: {
113
+ describeDeliveryCapabilities: ({ cfg, accountId }) => ({
114
+ enabled: isNativeDeliveryEnabled({
115
+ cfg,
116
+ accountId
117
+ }),
118
+ preferredSurface: "origin",
119
+ supportsOriginSurface: true,
120
+ supportsApproverDmSurface: false,
121
+ notifyOriginWhenDmOnly: false
122
+ }),
123
+ resolveOriginTarget: ({ request }) => {
124
+ const target = resolveApprovalTarget(request.request.sessionKey ?? null, request.request.turnSourceTo ?? null);
125
+ if (target) return { to: `${target.type}:${target.id}` };
126
+ const sessionConversation = resolveApprovalRequestSessionConversation({
127
+ request,
128
+ channel: "qqbot",
129
+ bundledFallback: true
130
+ });
131
+ if (sessionConversation?.id) return { to: `${sessionConversation.kind === "group" ? "group" : "c2c"}:${sessionConversation.id}` };
132
+ return null;
133
+ }
134
+ },
135
+ nativeRuntime: createLazyChannelApprovalNativeRuntimeAdapter({
136
+ eventKinds: ["exec", "plugin"],
137
+ isConfigured: ({ cfg, accountId }) => {
138
+ const result = isNativeDeliveryEnabled({
139
+ cfg,
140
+ accountId
141
+ });
142
+ getBridgeLogger().debug?.(`[qqbot:approval] nativeRuntime.isConfigured accountId=${accountId} → ${result}`);
143
+ return result;
144
+ },
145
+ shouldHandle: ({ cfg, accountId, request }) => {
146
+ const result = shouldHandleRequest({
147
+ cfg,
148
+ accountId,
149
+ request
150
+ });
151
+ getBridgeLogger().debug?.(`[qqbot:approval] nativeRuntime.shouldHandle accountId=${accountId} → ${result}`);
152
+ return result;
153
+ },
154
+ load: async () => {
155
+ ensurePlatformAdapter();
156
+ return (await import("./handler-runtime-Bqm6N0WG.js")).qqbotApprovalNativeRuntime;
157
+ }
158
+ })
159
+ });
160
+ }
161
+ const qqbotApprovalCapability = createQQBotApprovalCapability();
162
+ let _cachedCapability;
163
+ function getQQBotApprovalCapability() {
164
+ _cachedCapability ??= qqbotApprovalCapability;
165
+ return _cachedCapability;
166
+ }
167
+ //#endregion
168
+ //#region extensions/qqbot/src/engine/utils/data-paths.ts
169
+ /**
170
+ * Centralised filename helpers for persisted QQBot state.
171
+ *
172
+ * Every persistence module routes file paths through these helpers so the
173
+ * naming convention stays in sync and legacy migrations are handled
174
+ * consistently.
175
+ *
176
+ * Key design decisions:
177
+ * - Credential backup is keyed only by `accountId` because recovery runs
178
+ * exactly when the appId is missing from config.
179
+ */
180
+ /**
181
+ * Normalise an identifier so it is safe to embed in a filename.
182
+ * Keeps alphanumerics, dot, underscore, dash; everything else becomes `_`.
183
+ */
184
+ function safeName(id) {
185
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
186
+ }
187
+ /**
188
+ * Per-accountId credential backup file. Not keyed by appId because the
189
+ * whole point of this file is to recover credentials when appId is
190
+ * missing from the live config.
191
+ */
192
+ function getCredentialBackupFile(accountId) {
193
+ return path.join(getQQBotDataPath("data"), `credential-backup-${safeName(accountId)}.json`);
194
+ }
195
+ /** Legacy single-file credential backup (pre-multi-account-isolation). */
196
+ function getLegacyCredentialBackupFile() {
197
+ return path.join(getQQBotDataPath("data"), "credential-backup.json");
198
+ }
199
+ //#endregion
200
+ //#region extensions/qqbot/src/engine/config/credential-backup.ts
201
+ /**
202
+ * Credential backup & recovery.
203
+ * 凭证暂存与恢复。
204
+ *
205
+ * Solves the "hot-upgrade interrupted, appId/secret vanished from
206
+ * openclaw.json" failure mode.
207
+ *
208
+ * Mechanics:
209
+ * - After each successful gateway start we snapshot the currently
210
+ * resolved `appId` / `clientSecret` to a per-account backup file.
211
+ * - During plugin startup, if the live config has an empty appId or
212
+ * secret, the gateway consults the backup and restores the values
213
+ * via the config mutation API.
214
+ * - Backups live under `~/.openclaw/qqbot/data/` so they survive
215
+ * plugin directory replacement.
216
+ *
217
+ * Safety notes:
218
+ * - Only restore when credentials are **actually empty** — never
219
+ * overwrite a user's intentional config change.
220
+ * - Atomic write (temp file + rename) to avoid torn files.
221
+ * - Per-account file: `credential-backup-<accountId>.json`. We do
222
+ * **not** also key by appId because recovery happens precisely
223
+ * when appId is unknown.
224
+ * - Legacy single `credential-backup.json` is migrated automatically
225
+ * when the stored accountId matches the caller.
226
+ */
227
+ /** Persist a credential snapshot (called once gateway reaches READY). */
228
+ function saveCredentialBackup(accountId, appId, clientSecret) {
229
+ if (!appId || !clientSecret) return;
230
+ try {
231
+ const backupPath = getCredentialBackupFile(accountId);
232
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
233
+ const data = {
234
+ accountId,
235
+ appId,
236
+ clientSecret,
237
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
238
+ };
239
+ const tmpPath = `${backupPath}.tmp`;
240
+ fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
241
+ fs.renameSync(tmpPath, backupPath);
242
+ } catch {}
243
+ }
244
+ /**
245
+ * Load a credential snapshot for `accountId`.
246
+ *
247
+ * Consults the new per-account file first; falls back to the legacy
248
+ * global backup file and migrates it when the embedded `accountId`
249
+ * matches the request. Returns `null` when no usable backup exists.
250
+ */
251
+ function loadCredentialBackup(accountId) {
252
+ try {
253
+ if (accountId) {
254
+ const newPath = getCredentialBackupFile(accountId);
255
+ if (fs.existsSync(newPath)) {
256
+ const data = JSON.parse(fs.readFileSync(newPath, "utf8"));
257
+ if (data?.appId && data.clientSecret) return data;
258
+ }
259
+ }
260
+ const legacy = getLegacyCredentialBackupFile();
261
+ if (fs.existsSync(legacy)) {
262
+ const data = JSON.parse(fs.readFileSync(legacy, "utf8"));
263
+ if (!data?.appId || !data?.clientSecret) return null;
264
+ if (accountId && data.accountId !== accountId) return null;
265
+ if (data.accountId) try {
266
+ const backupPath = getCredentialBackupFile(data.accountId);
267
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
268
+ const tmpPath = `${backupPath}.tmp`;
269
+ fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
270
+ fs.renameSync(tmpPath, backupPath);
271
+ fs.unlinkSync(legacy);
272
+ } catch {}
273
+ return data;
274
+ }
275
+ } catch {}
276
+ return null;
277
+ }
278
+ //#endregion
279
+ //#region extensions/qqbot/src/engine/config/credentials.ts
280
+ /**
281
+ * QQBot credential management (pure logic layer).
282
+ * QQBot 凭证管理(纯逻辑层)。
283
+ *
284
+ * Credential clearing and field-level cleanup for logout and setup
285
+ * flows. All functions operate on plain objects (Record<string, unknown>)
286
+ * and stay framework-agnostic.
287
+ */
288
+ /**
289
+ * Remove clientSecret / clientSecretFile from a QQBot account config.
290
+ *
291
+ * Returns a shallow-cloned config with credentials removed, plus flags
292
+ * indicating whether anything actually changed.
293
+ */
294
+ function clearAccountCredentials(cfg, accountId) {
295
+ const nextCfg = { ...cfg };
296
+ const channels = asOptionalObjectRecord(cfg.channels);
297
+ const nextQQBot = channels?.qqbot ? { ...asOptionalObjectRecord(channels.qqbot) } : void 0;
298
+ let cleared = false;
299
+ let changed = false;
300
+ if (nextQQBot) {
301
+ const qqbot = nextQQBot;
302
+ if (accountId === "default") {
303
+ if (qqbot.clientSecret) {
304
+ delete qqbot.clientSecret;
305
+ cleared = true;
306
+ changed = true;
307
+ }
308
+ if (qqbot.clientSecretFile) {
309
+ delete qqbot.clientSecretFile;
310
+ cleared = true;
311
+ changed = true;
312
+ }
313
+ }
314
+ const accounts = qqbot.accounts;
315
+ if (accounts && accountId in accounts) {
316
+ const entry = accounts[accountId];
317
+ if (entry && "clientSecret" in entry) {
318
+ delete entry.clientSecret;
319
+ cleared = true;
320
+ changed = true;
321
+ }
322
+ if (entry && "clientSecretFile" in entry) {
323
+ delete entry.clientSecretFile;
324
+ cleared = true;
325
+ changed = true;
326
+ }
327
+ if (entry && Object.keys(entry).length === 0) {
328
+ delete accounts[accountId];
329
+ changed = true;
330
+ }
331
+ }
332
+ }
333
+ if (changed && nextQQBot) nextCfg.channels = {
334
+ ...channels,
335
+ qqbot: nextQQBot
336
+ };
337
+ return {
338
+ nextCfg,
339
+ cleared,
340
+ changed
341
+ };
342
+ }
343
+ //#endregion
344
+ //#region extensions/qqbot/src/channel.ts
345
+ let _gatewayModulePromise;
346
+ function loadGatewayModule() {
347
+ _gatewayModulePromise ??= import("./gateway-CmSUJKSt.js");
348
+ return _gatewayModulePromise;
349
+ }
350
+ const EXEC_APPROVAL_COMMAND_RE = /\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+(?:allow-once|allow-always|always|deny)\b/i;
351
+ function persistAccountCredentialSnapshot(account) {
352
+ if (account.appId && account.clientSecret) saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
353
+ }
354
+ function shouldSuppressLocalQQBotApprovalPrompt(params) {
355
+ if (params.hint?.kind !== "approval-pending" || params.hint.approvalKind !== "exec") return false;
356
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
357
+ if (!account.enabled || account.secretSource === "none") return false;
358
+ if (getExecApprovalReplyMetadata(params.payload)) return true;
359
+ const text = typeof params.payload.text === "string" ? params.payload.text : "";
360
+ return EXEC_APPROVAL_COMMAND_RE.test(text);
361
+ }
362
+ const qqbotPlugin = {
363
+ id: "qqbot",
364
+ setupWizard: qqbotSetupWizard,
365
+ meta: { ...qqbotMeta },
366
+ capabilities: {
367
+ chatTypes: ["direct", "group"],
368
+ media: true,
369
+ reactions: false,
370
+ threads: false,
371
+ blockStreaming: true
372
+ },
373
+ reload: { configPrefixes: ["channels.qqbot"] },
374
+ configSchema: qqbotChannelConfigSchema,
375
+ config: {
376
+ ...qqbotConfigAdapter,
377
+ /**
378
+ * Treat an account as configured when either the live config has
379
+ * credentials OR a recoverable credential backup exists. This mirrors
380
+ * the standalone plugin and lets the gateway survive a hot upgrade
381
+ * that wiped openclaw.json mid-flight.
382
+ */
383
+ isConfigured: (account) => {
384
+ if (qqbotConfigAdapter.isConfigured(account)) return true;
385
+ if (!account) return false;
386
+ const backup = loadCredentialBackup(account.accountId);
387
+ return Boolean(backup?.appId && backup?.clientSecret);
388
+ }
389
+ },
390
+ setup: { ...qqbotSetupAdapterShared },
391
+ approvalCapability: getQQBotApprovalCapability(),
392
+ messaging: {
393
+ targetPrefixes: ["qqbot"],
394
+ /** Normalize common QQ Bot target formats into the canonical qqbot:... form. */
395
+ normalizeTarget,
396
+ targetResolver: {
397
+ /** Return true when the id looks like a QQ Bot target. */
398
+ looksLikeId: looksLikeQQBotTarget,
399
+ hint: "QQ Bot target format: qqbot:c2c:openid (direct) or qqbot:group:groupid (group)"
400
+ }
401
+ },
402
+ outbound: {
403
+ deliveryMode: "direct",
404
+ chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
405
+ chunkerMode: "markdown",
406
+ textChunkLimit: 5e3,
407
+ shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) => shouldSuppressLocalQQBotApprovalPrompt({
408
+ cfg,
409
+ accountId,
410
+ payload,
411
+ hint
412
+ }),
413
+ sendText: async ({ to, text, accountId, replyToId, cfg }) => {
414
+ await loadGatewayModule();
415
+ const account = resolveQQBotAccount(cfg, accountId);
416
+ const { sendText } = await import("./outbound-BJfhwrPg.js").then((n) => n.t);
417
+ const result = await sendText({
418
+ to,
419
+ text,
420
+ accountId,
421
+ replyToId,
422
+ account: toGatewayAccount(account)
423
+ });
424
+ return {
425
+ channel: "qqbot",
426
+ messageId: result.messageId ?? "",
427
+ meta: result.error ? { error: result.error } : void 0
428
+ };
429
+ },
430
+ sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
431
+ await loadGatewayModule();
432
+ const account = resolveQQBotAccount(cfg, accountId);
433
+ const { sendMedia } = await import("./outbound-BJfhwrPg.js").then((n) => n.t);
434
+ const result = await sendMedia({
435
+ to,
436
+ text: text ?? "",
437
+ mediaUrl: mediaUrl ?? "",
438
+ accountId,
439
+ replyToId,
440
+ account: toGatewayAccount(account)
441
+ });
442
+ return {
443
+ channel: "qqbot",
444
+ messageId: result.messageId ?? "",
445
+ meta: result.error ? { error: result.error } : void 0
446
+ };
447
+ }
448
+ },
449
+ gateway: {
450
+ startAccount: async (ctx) => {
451
+ let { account, cfg } = ctx;
452
+ const { abortSignal, log } = ctx;
453
+ if (!account.appId || !account.clientSecret) {
454
+ const backup = loadCredentialBackup(account.accountId);
455
+ if (backup?.appId && backup?.clientSecret) try {
456
+ const nextCfg = applyQQBotAccountConfig(cfg, account.accountId, {
457
+ appId: backup.appId,
458
+ clientSecret: backup.clientSecret
459
+ });
460
+ await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg);
461
+ cfg = nextCfg;
462
+ account = resolveQQBotAccount(nextCfg, account.accountId);
463
+ log?.info(`[qqbot:${account.accountId}] Restored credentials from backup (appId=${account.appId})`);
464
+ } catch (err) {
465
+ log?.error(`[qqbot:${account.accountId}] Failed to restore credentials from backup: ${err instanceof Error ? err.message : String(err)}`);
466
+ }
467
+ }
468
+ const { startGateway } = await loadGatewayModule();
469
+ log?.info(`[qqbot:${account.accountId}] Starting gateway — appId=${account.appId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`);
470
+ await startGateway({
471
+ account,
472
+ abortSignal,
473
+ cfg,
474
+ log,
475
+ channelRuntime: ctx.channelRuntime,
476
+ onReady: () => {
477
+ log?.info(`[qqbot:${account.accountId}] Gateway ready`);
478
+ ctx.setStatus({
479
+ ...ctx.getStatus(),
480
+ running: true,
481
+ connected: true,
482
+ lastConnectedAt: Date.now()
483
+ });
484
+ persistAccountCredentialSnapshot(account);
485
+ },
486
+ onResumed: () => {
487
+ log?.info(`[qqbot:${account.accountId}] Gateway resumed`);
488
+ ctx.setStatus({
489
+ ...ctx.getStatus(),
490
+ running: true,
491
+ connected: true,
492
+ lastConnectedAt: Date.now()
493
+ });
494
+ persistAccountCredentialSnapshot(account);
495
+ },
496
+ onError: (error) => {
497
+ log?.error(`[qqbot:${account.accountId}] Gateway error: ${error.message}`);
498
+ ctx.setStatus({
499
+ ...ctx.getStatus(),
500
+ lastError: error.message
501
+ });
502
+ }
503
+ });
504
+ },
505
+ logoutAccount: async ({ accountId, cfg }) => {
506
+ const { nextCfg, cleared, changed } = clearAccountCredentials(cfg, accountId);
507
+ if (changed) await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg);
508
+ const loggedOut = resolveQQBotAccount(changed ? nextCfg : cfg, accountId).secretSource === "none";
509
+ return {
510
+ ok: true,
511
+ cleared,
512
+ envToken: Boolean(process.env.QQBOT_CLIENT_SECRET),
513
+ loggedOut
514
+ };
515
+ }
516
+ },
517
+ status: {
518
+ defaultRuntime: {
519
+ accountId: DEFAULT_ACCOUNT_ID$1,
520
+ running: false,
521
+ connected: false,
522
+ lastConnectedAt: null,
523
+ lastError: null,
524
+ lastInboundAt: null,
525
+ lastOutboundAt: null
526
+ },
527
+ buildChannelSummary: ({ snapshot }) => ({
528
+ configured: snapshot.configured ?? false,
529
+ tokenSource: snapshot.tokenSource ?? "none",
530
+ running: snapshot.running ?? false,
531
+ connected: snapshot.connected ?? false,
532
+ lastConnectedAt: snapshot.lastConnectedAt ?? null,
533
+ lastError: snapshot.lastError ?? null
534
+ }),
535
+ buildAccountSnapshot: ({ account, runtime }) => ({
536
+ accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID$1,
537
+ name: account?.name,
538
+ enabled: account?.enabled ?? false,
539
+ configured: Boolean(account?.appId && account?.clientSecret),
540
+ tokenSource: account?.secretSource,
541
+ running: runtime?.running ?? false,
542
+ connected: runtime?.connected ?? false,
543
+ lastConnectedAt: runtime?.lastConnectedAt ?? null,
544
+ lastError: runtime?.lastError ?? null,
545
+ lastInboundAt: runtime?.lastInboundAt ?? null,
546
+ lastOutboundAt: runtime?.lastOutboundAt ?? null
547
+ })
548
+ }
549
+ };
550
+ //#endregion
551
+ export { qqbotPlugin as t };
@@ -0,0 +1,2 @@
1
+ import { t as qqbotPlugin } from "./channel-N6Y_Rcjp.js";
2
+ export { qqbotPlugin };
@@ -0,0 +1,25 @@
1
+ import "./resolve-D_06fV6-.js";
2
+ import { a as qqbotSetupAdapterShared, i as qqbotMeta, n as qqbotSetupWizard, r as qqbotConfigAdapter, t as qqbotChannelConfigSchema } from "./config-schema-DFcjQw73.js";
3
+ //#region extensions/qqbot/src/channel.setup.ts
4
+ /**
5
+ * Setup-only QQBot plugin — lightweight subset used during `openclaw onboard`
6
+ * and `openclaw configure` without pulling the full runtime dependencies.
7
+ */
8
+ const qqbotSetupPlugin = {
9
+ id: "qqbot",
10
+ setupWizard: qqbotSetupWizard,
11
+ meta: { ...qqbotMeta },
12
+ capabilities: {
13
+ chatTypes: ["direct", "group"],
14
+ media: true,
15
+ reactions: false,
16
+ threads: false,
17
+ blockStreaming: true
18
+ },
19
+ reload: { configPrefixes: ["channels.qqbot"] },
20
+ configSchema: qqbotChannelConfigSchema,
21
+ config: { ...qqbotConfigAdapter },
22
+ setup: { ...qqbotSetupAdapterShared }
23
+ };
24
+ //#endregion
25
+ export { qqbotSetupPlugin as t };