@openclaw/qqbot 2026.5.1-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 (187) hide show
  1. package/api.ts +56 -0
  2. package/channel-plugin-api.ts +1 -0
  3. package/dist/.boundary-tsc.stamp +1 -0
  4. package/dist/.boundary-tsc.tsbuildinfo +1 -0
  5. package/index.ts +29 -0
  6. package/openclaw.plugin.json +814 -0
  7. package/package.json +66 -0
  8. package/runtime-api.ts +9 -0
  9. package/setup-entry.ts +9 -0
  10. package/setup-plugin-api.ts +3 -0
  11. package/skills/qqbot-channel/SKILL.md +262 -0
  12. package/skills/qqbot-channel/references/api_references.md +521 -0
  13. package/skills/qqbot-media/SKILL.md +37 -0
  14. package/skills/qqbot-remind/SKILL.md +153 -0
  15. package/src/bridge/approval/capability.ts +237 -0
  16. package/src/bridge/approval/handler-runtime.ts +204 -0
  17. package/src/bridge/bootstrap.ts +135 -0
  18. package/src/bridge/channel-entry.ts +18 -0
  19. package/src/bridge/commands/framework-context-adapter.ts +60 -0
  20. package/src/bridge/commands/framework-registration.ts +47 -0
  21. package/src/bridge/commands/from-parser.test.ts +86 -0
  22. package/src/bridge/commands/from-parser.ts +60 -0
  23. package/src/bridge/commands/result-dispatcher.ts +76 -0
  24. package/src/bridge/config-shared.ts +132 -0
  25. package/src/bridge/config.ts +111 -0
  26. package/src/bridge/gateway.ts +174 -0
  27. package/src/bridge/logger.ts +31 -0
  28. package/src/bridge/narrowing.ts +31 -0
  29. package/src/bridge/plugin-version.test.ts +146 -0
  30. package/src/bridge/plugin-version.ts +102 -0
  31. package/src/bridge/runtime.ts +25 -0
  32. package/src/bridge/sdk-adapter.ts +131 -0
  33. package/src/bridge/setup/finalize.ts +144 -0
  34. package/src/bridge/setup/surface.ts +34 -0
  35. package/src/bridge/tools/channel.ts +58 -0
  36. package/src/bridge/tools/index.ts +15 -0
  37. package/src/bridge/tools/remind.test.ts +124 -0
  38. package/src/bridge/tools/remind.ts +91 -0
  39. package/src/channel.setup.ts +33 -0
  40. package/src/channel.ts +288 -0
  41. package/src/command-auth.test.ts +62 -0
  42. package/src/config-schema.ts +84 -0
  43. package/src/config.test.ts +364 -0
  44. package/src/engine/access/access-control.test.ts +198 -0
  45. package/src/engine/access/access-control.ts +226 -0
  46. package/src/engine/access/index.ts +16 -0
  47. package/src/engine/access/resolve-policy.test.ts +59 -0
  48. package/src/engine/access/resolve-policy.ts +57 -0
  49. package/src/engine/access/sender-match.test.ts +60 -0
  50. package/src/engine/access/sender-match.ts +55 -0
  51. package/src/engine/access/types.ts +53 -0
  52. package/src/engine/adapter/audio.port.ts +27 -0
  53. package/src/engine/adapter/commands.port.ts +22 -0
  54. package/src/engine/adapter/history.port.ts +52 -0
  55. package/src/engine/adapter/index.ts +139 -0
  56. package/src/engine/adapter/mention-gate.port.ts +50 -0
  57. package/src/engine/adapter/types.ts +38 -0
  58. package/src/engine/api/api-client.ts +212 -0
  59. package/src/engine/api/media-chunked.test.ts +336 -0
  60. package/src/engine/api/media-chunked.ts +622 -0
  61. package/src/engine/api/media.ts +218 -0
  62. package/src/engine/api/messages.ts +293 -0
  63. package/src/engine/api/retry.ts +217 -0
  64. package/src/engine/api/routes.ts +95 -0
  65. package/src/engine/api/token.ts +271 -0
  66. package/src/engine/approval/index.test.ts +22 -0
  67. package/src/engine/approval/index.ts +224 -0
  68. package/src/engine/commands/builtin/log-helpers.ts +319 -0
  69. package/src/engine/commands/builtin/register-all.ts +17 -0
  70. package/src/engine/commands/builtin/register-approve.ts +201 -0
  71. package/src/engine/commands/builtin/register-basic.ts +95 -0
  72. package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
  73. package/src/engine/commands/builtin/register-logs.ts +20 -0
  74. package/src/engine/commands/builtin/register-streaming.ts +137 -0
  75. package/src/engine/commands/builtin/state.ts +31 -0
  76. package/src/engine/commands/slash-command-auth.ts +48 -0
  77. package/src/engine/commands/slash-command-handler.ts +146 -0
  78. package/src/engine/commands/slash-commands-impl.test.ts +8 -0
  79. package/src/engine/commands/slash-commands-impl.ts +61 -0
  80. package/src/engine/commands/slash-commands.ts +199 -0
  81. package/src/engine/config/credential-backup.test.ts +88 -0
  82. package/src/engine/config/credential-backup.ts +107 -0
  83. package/src/engine/config/credentials.ts +76 -0
  84. package/src/engine/config/group.test.ts +234 -0
  85. package/src/engine/config/group.ts +299 -0
  86. package/src/engine/config/resolve.test.ts +152 -0
  87. package/src/engine/config/resolve.ts +283 -0
  88. package/src/engine/config/setup-logic.ts +84 -0
  89. package/src/engine/engine-import-boundary.test.ts +73 -0
  90. package/src/engine/gateway/codec.ts +47 -0
  91. package/src/engine/gateway/constants.ts +117 -0
  92. package/src/engine/gateway/event-dispatcher.ts +177 -0
  93. package/src/engine/gateway/gateway-connection.ts +371 -0
  94. package/src/engine/gateway/gateway.ts +291 -0
  95. package/src/engine/gateway/inbound-attachments.test.ts +126 -0
  96. package/src/engine/gateway/inbound-attachments.ts +360 -0
  97. package/src/engine/gateway/inbound-context.ts +195 -0
  98. package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +218 -0
  99. package/src/engine/gateway/inbound-pipeline.ts +235 -0
  100. package/src/engine/gateway/interaction-handler.ts +220 -0
  101. package/src/engine/gateway/message-queue.test.ts +282 -0
  102. package/src/engine/gateway/message-queue.ts +499 -0
  103. package/src/engine/gateway/outbound-dispatch.test.ts +231 -0
  104. package/src/engine/gateway/outbound-dispatch.ts +575 -0
  105. package/src/engine/gateway/reconnect.ts +199 -0
  106. package/src/engine/gateway/stages/access-stage.ts +132 -0
  107. package/src/engine/gateway/stages/assembly-stage.ts +156 -0
  108. package/src/engine/gateway/stages/content-stage.test.ts +77 -0
  109. package/src/engine/gateway/stages/content-stage.ts +77 -0
  110. package/src/engine/gateway/stages/envelope-stage.test.ts +152 -0
  111. package/src/engine/gateway/stages/envelope-stage.ts +144 -0
  112. package/src/engine/gateway/stages/group-gate-stage.ts +292 -0
  113. package/src/engine/gateway/stages/index.ts +18 -0
  114. package/src/engine/gateway/stages/quote-stage.ts +113 -0
  115. package/src/engine/gateway/stages/refidx-stage.ts +62 -0
  116. package/src/engine/gateway/stages/stub-contexts.ts +116 -0
  117. package/src/engine/gateway/types.ts +264 -0
  118. package/src/engine/gateway/typing-keepalive.ts +79 -0
  119. package/src/engine/group/activation.test.ts +114 -0
  120. package/src/engine/group/activation.ts +147 -0
  121. package/src/engine/group/history.test.ts +314 -0
  122. package/src/engine/group/history.ts +321 -0
  123. package/src/engine/group/mention.test.ts +141 -0
  124. package/src/engine/group/mention.ts +197 -0
  125. package/src/engine/group/message-gating.test.ts +188 -0
  126. package/src/engine/group/message-gating.ts +216 -0
  127. package/src/engine/messaging/decode-media-path.ts +82 -0
  128. package/src/engine/messaging/media-source.ts +215 -0
  129. package/src/engine/messaging/media-type-detect.ts +37 -0
  130. package/src/engine/messaging/outbound-audio-port.ts +38 -0
  131. package/src/engine/messaging/outbound-deliver.ts +810 -0
  132. package/src/engine/messaging/outbound-media-send.ts +702 -0
  133. package/src/engine/messaging/outbound-reply.ts +27 -0
  134. package/src/engine/messaging/outbound-result-helpers.ts +54 -0
  135. package/src/engine/messaging/outbound-types.ts +45 -0
  136. package/src/engine/messaging/outbound.ts +485 -0
  137. package/src/engine/messaging/reply-dispatcher.ts +597 -0
  138. package/src/engine/messaging/reply-limiter.ts +164 -0
  139. package/src/engine/messaging/sender.ts +729 -0
  140. package/src/engine/messaging/streaming-c2c.ts +1192 -0
  141. package/src/engine/messaging/streaming-media-send.ts +544 -0
  142. package/src/engine/messaging/target-parser.ts +104 -0
  143. package/src/engine/ref/format-message-ref.ts +142 -0
  144. package/src/engine/ref/format-ref-entry.test.ts +60 -0
  145. package/src/engine/ref/format-ref-entry.ts +27 -0
  146. package/src/engine/ref/store.ts +224 -0
  147. package/src/engine/ref/types.ts +27 -0
  148. package/src/engine/session/known-users.ts +254 -0
  149. package/src/engine/session/session-store.ts +284 -0
  150. package/src/engine/tools/channel-api.ts +244 -0
  151. package/src/engine/tools/remind-logic.test.ts +280 -0
  152. package/src/engine/tools/remind-logic.ts +377 -0
  153. package/src/engine/types.ts +313 -0
  154. package/src/engine/utils/attachment-tags.test.ts +186 -0
  155. package/src/engine/utils/attachment-tags.ts +174 -0
  156. package/src/engine/utils/audio.test.ts +250 -0
  157. package/src/engine/utils/audio.ts +585 -0
  158. package/src/engine/utils/data-paths.ts +38 -0
  159. package/src/engine/utils/diagnostics.ts +109 -0
  160. package/src/engine/utils/file-utils.test.ts +72 -0
  161. package/src/engine/utils/file-utils.ts +225 -0
  162. package/src/engine/utils/format.test.ts +68 -0
  163. package/src/engine/utils/format.ts +70 -0
  164. package/src/engine/utils/image-size.test.ts +158 -0
  165. package/src/engine/utils/image-size.ts +249 -0
  166. package/src/engine/utils/log.test.ts +28 -0
  167. package/src/engine/utils/log.ts +61 -0
  168. package/src/engine/utils/media-tags.test.ts +32 -0
  169. package/src/engine/utils/media-tags.ts +177 -0
  170. package/src/engine/utils/payload.test.ts +68 -0
  171. package/src/engine/utils/payload.ts +145 -0
  172. package/src/engine/utils/platform-storage-laziness.test.ts +65 -0
  173. package/src/engine/utils/platform.test.ts +148 -0
  174. package/src/engine/utils/platform.ts +343 -0
  175. package/src/engine/utils/request-context.ts +60 -0
  176. package/src/engine/utils/string-normalize.ts +91 -0
  177. package/src/engine/utils/stt.test.ts +104 -0
  178. package/src/engine/utils/stt.ts +100 -0
  179. package/src/engine/utils/text-parsing.test.ts +29 -0
  180. package/src/engine/utils/text-parsing.ts +155 -0
  181. package/src/engine/utils/upload-cache.ts +96 -0
  182. package/src/engine/utils/voice-text.ts +15 -0
  183. package/src/exec-approvals.ts +218 -0
  184. package/src/manifest-schema.test.ts +56 -0
  185. package/src/qqbot-test-support.ts +29 -0
  186. package/src/types.ts +210 -0
  187. package/tsconfig.json +16 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Register all `requireAuth: true` slash commands with the framework via
3
+ * `api.registerCommand`.
4
+ *
5
+ * Routing through the framework lets `resolveCommandAuthorization()` apply
6
+ * `commands.allowFrom.qqbot` precedence and the `qqbot:` prefix normalization
7
+ * before any QQBot command handler runs.
8
+ *
9
+ * This module is intentionally thin: it wires the engine-side command
10
+ * registry (`getFrameworkCommands`) to the framework registration surface via
11
+ * the three single-responsibility helpers in this directory.
12
+ */
13
+
14
+ import type { OpenClawPluginApi, PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry";
15
+ import { getFrameworkCommands } from "../../engine/commands/slash-commands-impl.js";
16
+ import { resolveQQBotAccount } from "../config.js";
17
+ import { buildFrameworkSlashContext } from "./framework-context-adapter.js";
18
+ import { parseQQBotFrom } from "./from-parser.js";
19
+ import { dispatchFrameworkSlashResult } from "./result-dispatcher.js";
20
+
21
+ export function registerQQBotFrameworkCommands(api: OpenClawPluginApi): void {
22
+ for (const cmd of getFrameworkCommands()) {
23
+ api.registerCommand({
24
+ name: cmd.name,
25
+ description: cmd.description,
26
+ requireAuth: true,
27
+ acceptsArgs: true,
28
+ handler: async (ctx: PluginCommandContext) => {
29
+ const from = parseQQBotFrom(ctx.from);
30
+ const account = resolveQQBotAccount(ctx.config, ctx.accountId ?? undefined);
31
+ const slashCtx = buildFrameworkSlashContext({
32
+ ctx,
33
+ account,
34
+ from,
35
+ commandName: cmd.name,
36
+ });
37
+ const result = await cmd.handler(slashCtx);
38
+ return await dispatchFrameworkSlashResult({
39
+ result,
40
+ account,
41
+ from,
42
+ logger: api.logger,
43
+ });
44
+ },
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseQQBotFrom } from "./from-parser.js";
3
+
4
+ describe("parseQQBotFrom", () => {
5
+ it("parses a group from string", () => {
6
+ expect(parseQQBotFrom("qqbot:group:ABCDEF")).toEqual({
7
+ msgType: "group",
8
+ targetType: "group",
9
+ targetId: "ABCDEF",
10
+ });
11
+ });
12
+
13
+ it("parses a channel prefix into the guild msgType", () => {
14
+ expect(parseQQBotFrom("qqbot:channel:123")).toEqual({
15
+ msgType: "guild",
16
+ targetType: "channel",
17
+ targetId: "123",
18
+ });
19
+ });
20
+
21
+ it("parses a dm prefix", () => {
22
+ expect(parseQQBotFrom("qqbot:dm:456")).toEqual({
23
+ msgType: "dm",
24
+ targetType: "dm",
25
+ targetId: "456",
26
+ });
27
+ });
28
+
29
+ it("parses a c2c prefix", () => {
30
+ expect(parseQQBotFrom("qqbot:c2c:user-1")).toEqual({
31
+ msgType: "c2c",
32
+ targetType: "c2c",
33
+ targetId: "user-1",
34
+ });
35
+ });
36
+
37
+ it("is case-insensitive on the qqbot: prefix", () => {
38
+ expect(parseQQBotFrom("QQBOT:group:gid")).toEqual({
39
+ msgType: "group",
40
+ targetType: "group",
41
+ targetId: "gid",
42
+ });
43
+ });
44
+
45
+ it("handles target ids that contain a colon", () => {
46
+ expect(parseQQBotFrom("qqbot:group:GROUP:ID")).toEqual({
47
+ msgType: "group",
48
+ targetType: "group",
49
+ targetId: "GROUP:ID",
50
+ });
51
+ });
52
+
53
+ it("falls back to c2c for unknown prefixes", () => {
54
+ expect(parseQQBotFrom("qqbot:unknown:abc")).toEqual({
55
+ msgType: "c2c",
56
+ targetType: "c2c",
57
+ targetId: "abc",
58
+ });
59
+ });
60
+
61
+ it("falls back to c2c for missing from", () => {
62
+ expect(parseQQBotFrom(undefined)).toEqual({
63
+ msgType: "c2c",
64
+ targetType: "c2c",
65
+ targetId: "",
66
+ });
67
+ expect(parseQQBotFrom(null)).toEqual({
68
+ msgType: "c2c",
69
+ targetType: "c2c",
70
+ targetId: "",
71
+ });
72
+ expect(parseQQBotFrom("")).toEqual({
73
+ msgType: "c2c",
74
+ targetType: "c2c",
75
+ targetId: "",
76
+ });
77
+ });
78
+
79
+ it("treats a bare prefix (no colon) as c2c with that id", () => {
80
+ expect(parseQQBotFrom("qqbot:c2c")).toEqual({
81
+ msgType: "c2c",
82
+ targetType: "c2c",
83
+ targetId: "c2c",
84
+ });
85
+ });
86
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Parse the framework `PluginCommandContext.from` string into the QQBot
3
+ * message type and send target.
4
+ *
5
+ * The framework passes `from` in the form `qqbot:<kind>:<id>` (case-insensitive
6
+ * prefix). We split that string once and map `<kind>` into the engine-side
7
+ * `SlashCommandContext.type` enum and the outbound `MediaTargetContext.targetType`
8
+ * enum. Both enums diverge only for guild/channel, so we keep two lookup
9
+ * tables to avoid the nested ternary chain the previous implementation used.
10
+ */
11
+
12
+ export interface QQBotFromParseResult {
13
+ /** Message type consumed by SlashCommandContext.type. */
14
+ msgType: "c2c" | "guild" | "dm" | "group";
15
+ /** Target type consumed by MediaTargetContext.targetType. */
16
+ targetType: "c2c" | "group" | "channel" | "dm";
17
+ /** Raw target id (everything after the first `:`). */
18
+ targetId: string;
19
+ }
20
+
21
+ type FromKind = "c2c" | "group" | "channel" | "dm";
22
+
23
+ const MSG_TYPE_MAP: Record<FromKind, QQBotFromParseResult["msgType"]> = {
24
+ c2c: "c2c",
25
+ dm: "dm",
26
+ group: "group",
27
+ channel: "guild",
28
+ };
29
+
30
+ const TARGET_TYPE_MAP: Record<FromKind, QQBotFromParseResult["targetType"]> = {
31
+ c2c: "c2c",
32
+ dm: "dm",
33
+ group: "group",
34
+ channel: "channel",
35
+ };
36
+
37
+ function isFromKind(value: string): value is FromKind {
38
+ return value === "c2c" || value === "dm" || value === "group" || value === "channel";
39
+ }
40
+
41
+ /**
42
+ * Parse `ctx.from` into the structured fields the QQBot bridge expects.
43
+ *
44
+ * Unknown or missing prefixes fall back to c2c. The remainder after the first
45
+ * `:` is returned verbatim as the target id, matching what the previous inline
46
+ * implementation did.
47
+ */
48
+ export function parseQQBotFrom(from: string | undefined | null): QQBotFromParseResult {
49
+ const stripped = (from ?? "").replace(/^qqbot:/iu, "");
50
+ const colonIdx = stripped.indexOf(":");
51
+ const rawPrefix = colonIdx === -1 ? stripped : stripped.slice(0, colonIdx);
52
+ const targetId = colonIdx === -1 ? stripped : stripped.slice(colonIdx + 1);
53
+ const kind: FromKind = isFromKind(rawPrefix) ? rawPrefix : "c2c";
54
+
55
+ return {
56
+ msgType: MSG_TYPE_MAP[kind],
57
+ targetType: TARGET_TYPE_MAP[kind],
58
+ targetId,
59
+ };
60
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Dispatch a slash command result produced on the framework command surface.
3
+ *
4
+ * Slash command handlers return one of:
5
+ * 1. a plain string (text reply),
6
+ * 2. a `SlashCommandFileResult` (text plus a local file to upload), or
7
+ * 3. null / unexpected value (we surface a generic warning).
8
+ *
9
+ * This module isolates the text/file branching so the framework registration
10
+ * layer stays declarative and so the file-send side effect has a single
11
+ * location where logging and error handling live.
12
+ */
13
+
14
+ import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
15
+ import type { SlashCommandResult } from "../../engine/commands/slash-commands.js";
16
+ import { sendDocument, type MediaTargetContext } from "../../engine/messaging/outbound.js";
17
+ import type { ResolvedQQBotAccount } from "../../types.js";
18
+ import type { QQBotFromParseResult } from "./from-parser.js";
19
+
20
+ const UNEXPECTED_RESULT_TEXT = "⚠️ 命令返回了意外结果。";
21
+
22
+ interface FrameworkSlashReply {
23
+ text: string;
24
+ }
25
+
26
+ interface DispatchFrameworkSlashResultInput {
27
+ result: SlashCommandResult;
28
+ account: ResolvedQQBotAccount;
29
+ from: QQBotFromParseResult;
30
+ logger?: PluginLogger;
31
+ }
32
+
33
+ function hasFilePath(value: unknown): value is { text: string; filePath: string } {
34
+ return (
35
+ typeof value === "object" &&
36
+ value !== null &&
37
+ "filePath" in value &&
38
+ typeof (value as { filePath: unknown }).filePath === "string"
39
+ );
40
+ }
41
+
42
+ function buildMediaTarget(
43
+ account: ResolvedQQBotAccount,
44
+ from: QQBotFromParseResult,
45
+ ): MediaTargetContext {
46
+ return {
47
+ targetType: from.targetType,
48
+ targetId: from.targetId,
49
+ account: account as unknown as MediaTargetContext["account"],
50
+ };
51
+ }
52
+
53
+ export async function dispatchFrameworkSlashResult({
54
+ result,
55
+ account,
56
+ from,
57
+ logger,
58
+ }: DispatchFrameworkSlashResultInput): Promise<FrameworkSlashReply> {
59
+ if (typeof result === "string") {
60
+ return { text: result };
61
+ }
62
+
63
+ if (hasFilePath(result)) {
64
+ const mediaCtx = buildMediaTarget(account, from);
65
+ try {
66
+ await sendDocument(mediaCtx, result.filePath, {
67
+ allowQQBotDataDownloads: true,
68
+ });
69
+ } catch (err) {
70
+ logger?.warn(`framework slash file send failed: ${String(err)}`);
71
+ }
72
+ return { text: result.text };
73
+ }
74
+
75
+ return { text: UNEXPECTED_RESULT_TEXT };
76
+ }
@@ -0,0 +1,132 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2
+ import {
3
+ applyAccountNameToChannelSection,
4
+ deleteAccountFromConfigSection,
5
+ setAccountEnabledInConfigSection,
6
+ } from "openclaw/plugin-sdk/core";
7
+ import type { ChannelSetupInput } from "openclaw/plugin-sdk/setup";
8
+ import {
9
+ describeAccount as engineDescribeAccount,
10
+ formatAllowFrom as engineFormatAllowFrom,
11
+ isAccountConfigured as engineIsAccountConfigured,
12
+ } from "../engine/config/resolve.js";
13
+ import {
14
+ applySetupAccountConfig as engineApplySetupAccountConfig,
15
+ validateSetupInput as engineValidateSetupInput,
16
+ } from "../engine/config/setup-logic.js";
17
+ import { normalizeLowercaseStringOrEmpty } from "../engine/utils/string-normalize.js";
18
+ import type { ResolvedQQBotAccount } from "../types.js";
19
+ import {
20
+ listQQBotAccountIds,
21
+ resolveDefaultQQBotAccountId,
22
+ resolveQQBotAccount,
23
+ } from "./config.js";
24
+
25
+ export const qqbotMeta = {
26
+ id: "qqbot",
27
+ label: "QQ Bot",
28
+ selectionLabel: "QQ Bot (Bot API)",
29
+ docsPath: "/channels/qqbot",
30
+ blurb: "Connect to QQ via official QQ Bot API",
31
+ order: 50,
32
+ } as const;
33
+
34
+ function validateQQBotSetupInput(params: {
35
+ accountId: string;
36
+ input: ChannelSetupInput;
37
+ }): string | null {
38
+ return engineValidateSetupInput(params.accountId, params.input);
39
+ }
40
+
41
+ function applyQQBotSetupAccountConfig(params: {
42
+ cfg: OpenClawConfig;
43
+ accountId: string;
44
+ input: ChannelSetupInput;
45
+ }): OpenClawConfig {
46
+ return engineApplySetupAccountConfig(
47
+ params.cfg as unknown as Record<string, unknown>,
48
+ params.accountId,
49
+ params.input,
50
+ ) as OpenClawConfig;
51
+ }
52
+
53
+ function isQQBotConfigured(account: ResolvedQQBotAccount | undefined): boolean {
54
+ return engineIsAccountConfigured(account as never);
55
+ }
56
+
57
+ function describeQQBotAccount(account: ResolvedQQBotAccount | undefined) {
58
+ return engineDescribeAccount(account as never);
59
+ }
60
+
61
+ function formatQQBotAllowFrom(params: {
62
+ allowFrom: Array<string | number> | undefined | null;
63
+ }): string[] {
64
+ return engineFormatAllowFrom(params.allowFrom);
65
+ }
66
+
67
+ export const qqbotConfigAdapter = {
68
+ listAccountIds: (cfg: OpenClawConfig) => listQQBotAccountIds(cfg),
69
+ resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) =>
70
+ resolveQQBotAccount(cfg, accountId, { allowUnresolvedSecretRef: true }),
71
+ defaultAccountId: (cfg: OpenClawConfig) => resolveDefaultQQBotAccountId(cfg),
72
+ setAccountEnabled: ({
73
+ cfg,
74
+ accountId,
75
+ enabled,
76
+ }: {
77
+ cfg: OpenClawConfig;
78
+ accountId: string;
79
+ enabled: boolean;
80
+ }) =>
81
+ setAccountEnabledInConfigSection({
82
+ cfg,
83
+ sectionKey: "qqbot",
84
+ accountId,
85
+ enabled,
86
+ allowTopLevel: true,
87
+ }),
88
+ deleteAccount: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId: string }) =>
89
+ deleteAccountFromConfigSection({
90
+ cfg,
91
+ sectionKey: "qqbot",
92
+ accountId,
93
+ clearBaseFields: ["appId", "clientSecret", "clientSecretFile", "name"],
94
+ }),
95
+ isConfigured: isQQBotConfigured,
96
+ describeAccount: describeQQBotAccount,
97
+ resolveAllowFrom: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) =>
98
+ resolveQQBotAccount(cfg, accountId, { allowUnresolvedSecretRef: true }).config?.allowFrom,
99
+ formatAllowFrom: ({ allowFrom }: { allowFrom: Array<string | number> | undefined | null }) =>
100
+ formatQQBotAllowFrom({ allowFrom }),
101
+ };
102
+
103
+ export const qqbotSetupAdapterShared = {
104
+ resolveAccountId: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) =>
105
+ normalizeLowercaseStringOrEmpty(accountId) || resolveDefaultQQBotAccountId(cfg),
106
+ applyAccountName: ({
107
+ cfg,
108
+ accountId,
109
+ name,
110
+ }: {
111
+ cfg: OpenClawConfig;
112
+ accountId: string;
113
+ name?: string;
114
+ }) =>
115
+ applyAccountNameToChannelSection({
116
+ cfg,
117
+ channelKey: "qqbot",
118
+ accountId,
119
+ name,
120
+ }),
121
+ validateInput: ({ accountId, input }: { accountId: string; input: ChannelSetupInput }) =>
122
+ validateQQBotSetupInput({ accountId, input }),
123
+ applyAccountConfig: ({
124
+ cfg,
125
+ accountId,
126
+ input,
127
+ }: {
128
+ cfg: OpenClawConfig;
129
+ accountId: string;
130
+ input: ChannelSetupInput;
131
+ }) => applyQQBotSetupAccountConfig({ cfg, accountId, input }),
132
+ };
@@ -0,0 +1,111 @@
1
+ import fs from "node:fs";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
3
+ import { getPlatformAdapter } from "../engine/adapter/index.js";
4
+ import {
5
+ DEFAULT_ACCOUNT_ID as ENGINE_DEFAULT_ACCOUNT_ID,
6
+ applyAccountConfig,
7
+ listAccountIds,
8
+ resolveAccountBase,
9
+ resolveDefaultAccountId,
10
+ } from "../engine/config/resolve.js";
11
+ import type { ResolvedQQBotAccount, QQBotAccountConfig } from "../types.js";
12
+
13
+ export const DEFAULT_ACCOUNT_ID = ENGINE_DEFAULT_ACCOUNT_ID;
14
+
15
+ interface QQBotChannelConfig extends QQBotAccountConfig {
16
+ accounts?: Record<string, QQBotAccountConfig>;
17
+ defaultAccount?: string;
18
+ }
19
+
20
+ /** List all configured QQBot account IDs. */
21
+ export function listQQBotAccountIds(cfg: OpenClawConfig): string[] {
22
+ return listAccountIds(cfg as unknown as Record<string, unknown>);
23
+ }
24
+
25
+ /** Resolve the default QQBot account ID. */
26
+ export function resolveDefaultQQBotAccountId(cfg: OpenClawConfig): string {
27
+ return resolveDefaultAccountId(cfg as unknown as Record<string, unknown>);
28
+ }
29
+
30
+ /** Resolve QQBot account config for runtime or setup flows. */
31
+ export function resolveQQBotAccount(
32
+ cfg: OpenClawConfig,
33
+ accountId?: string | null,
34
+ opts?: { allowUnresolvedSecretRef?: boolean },
35
+ ): ResolvedQQBotAccount {
36
+ const raw = cfg as unknown as Record<string, unknown>;
37
+ const base = resolveAccountBase(raw, accountId);
38
+
39
+ const qqbot = cfg.channels?.qqbot as QQBotChannelConfig | undefined;
40
+ /**
41
+ * Legacy top-level account uses `channels.qqbot` as the base, but per-account
42
+ * fields (allowFrom, streaming, …) often live under `accounts.default`.
43
+ * Merge that slice so runtime sees `config.streaming` etc.
44
+ */
45
+ const accountConfig: QQBotAccountConfig =
46
+ base.accountId === DEFAULT_ACCOUNT_ID
47
+ ? {
48
+ ...qqbot,
49
+ ...qqbot?.accounts?.[DEFAULT_ACCOUNT_ID],
50
+ }
51
+ : (qqbot?.accounts?.[base.accountId] ?? {});
52
+
53
+ let clientSecret = "";
54
+ let secretSource: "config" | "file" | "env" | "none" = "none";
55
+
56
+ const clientSecretPath =
57
+ base.accountId === DEFAULT_ACCOUNT_ID
58
+ ? "channels.qqbot.clientSecret"
59
+ : `channels.qqbot.accounts.${base.accountId}.clientSecret`;
60
+
61
+ const adapter = getPlatformAdapter();
62
+ if (adapter.hasConfiguredSecret(accountConfig.clientSecret)) {
63
+ clientSecret = opts?.allowUnresolvedSecretRef
64
+ ? (adapter.normalizeSecretInputString(accountConfig.clientSecret) ?? "")
65
+ : (adapter.resolveSecretInputString({
66
+ value: accountConfig.clientSecret,
67
+ path: clientSecretPath,
68
+ }) ?? "");
69
+ secretSource = "config";
70
+ } else if (accountConfig.clientSecretFile) {
71
+ try {
72
+ clientSecret = fs.readFileSync(accountConfig.clientSecretFile, "utf8").trim();
73
+ secretSource = "file";
74
+ } catch {
75
+ secretSource = "none";
76
+ }
77
+ } else if (process.env.QQBOT_CLIENT_SECRET && base.accountId === DEFAULT_ACCOUNT_ID) {
78
+ clientSecret = process.env.QQBOT_CLIENT_SECRET;
79
+ secretSource = "env";
80
+ }
81
+
82
+ return {
83
+ accountId: base.accountId,
84
+ name: accountConfig.name,
85
+ enabled: base.enabled,
86
+ appId: base.appId,
87
+ clientSecret,
88
+ secretSource,
89
+ systemPrompt: base.systemPrompt,
90
+ markdownSupport: base.markdownSupport,
91
+ config: accountConfig,
92
+ };
93
+ }
94
+
95
+ /** Apply account config updates back into the OpenClaw config object. */
96
+ export function applyQQBotAccountConfig(
97
+ cfg: OpenClawConfig,
98
+ accountId: string,
99
+ input: {
100
+ appId?: string;
101
+ clientSecret?: string;
102
+ clientSecretFile?: string;
103
+ name?: string;
104
+ },
105
+ ): OpenClawConfig {
106
+ return applyAccountConfig(
107
+ cfg as unknown as Record<string, unknown>,
108
+ accountId,
109
+ input,
110
+ ) as OpenClawConfig;
111
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Gateway entry point — thin bridge shell that constructs
3
+ * {@link EngineAdapters} and passes them to the engine's
4
+ * `startGateway`.
5
+ *
6
+ * All adapter dependencies are assembled here in one place.
7
+ */
8
+
9
+ import { resolveRuntimeServiceVersion } from "openclaw/plugin-sdk/cli-runtime";
10
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
11
+ import type { EngineAdapters } from "../engine/adapter/index.js";
12
+ import {
13
+ startGateway as coreStartGateway,
14
+ type CoreGatewayContext,
15
+ } from "../engine/gateway/gateway.js";
16
+ import type { GatewayPluginRuntime } from "../engine/gateway/types.js";
17
+ import { initSender, registerAccount } from "../engine/messaging/sender.js";
18
+ import type { EngineLogger } from "../engine/types.js";
19
+ import * as _audioModule from "../engine/utils/audio.js";
20
+ import { formatDuration } from "../engine/utils/format.js";
21
+ import { debugLog, debugError } from "../engine/utils/log.js";
22
+ import type { ResolvedQQBotAccount } from "../types.js";
23
+ import { ensurePlatformAdapter } from "./bootstrap.js";
24
+ import { setBridgeLogger } from "./logger.js";
25
+ import { toGatewayAccount } from "./narrowing.js";
26
+ import { resolveQQBotPluginVersion } from "./plugin-version.js";
27
+ import { getQQBotRuntime, getQQBotRuntimeForEngine } from "./runtime.js";
28
+ import { createSdkHistoryAdapter, createSdkMentionGateAdapter } from "./sdk-adapter.js";
29
+
30
+ // ---- One-time startup initialization (module-level) ----
31
+
32
+ const _pluginVersion = resolveQQBotPluginVersion(import.meta.url);
33
+ initSender({
34
+ pluginVersion: _pluginVersion,
35
+ openclawVersion: resolveRuntimeServiceVersion(),
36
+ });
37
+
38
+ // ============ Public types ============
39
+
40
+ export interface GatewayContext {
41
+ account: ResolvedQQBotAccount;
42
+ abortSignal: AbortSignal;
43
+ cfg: OpenClawConfig;
44
+ onReady?: (data: unknown) => void;
45
+ onResumed?: (data: unknown) => void;
46
+ onError?: (error: Error) => void;
47
+ log?: {
48
+ info: (msg: string) => void;
49
+ error: (msg: string) => void;
50
+ debug?: (msg: string) => void;
51
+ };
52
+ channelRuntime?: {
53
+ runtimeContexts: {
54
+ register: (params: {
55
+ channelId: string;
56
+ accountId: string;
57
+ capability: string;
58
+ context: unknown;
59
+ abortSignal?: AbortSignal;
60
+ }) => { dispose: () => void };
61
+ };
62
+ };
63
+ }
64
+
65
+ // ============ Adapter factory ============
66
+
67
+ /**
68
+ * Create the full set of engine adapters from the bridge layer.
69
+ *
70
+ * This is the **single assembly point** — all SDK → engine binding
71
+ * happens here. The engine receives a fully-populated
72
+ * {@link EngineAdapters} object with zero global singletons.
73
+ */
74
+ function createEngineAdapters(_runtime: GatewayPluginRuntime): EngineAdapters {
75
+ return {
76
+ history: createSdkHistoryAdapter(),
77
+ mentionGate: createSdkMentionGateAdapter(),
78
+ audioConvert: {
79
+ convertSilkToWav: _audioModule.convertSilkToWav,
80
+ isVoiceAttachment: _audioModule.isVoiceAttachment,
81
+ formatDuration,
82
+ },
83
+ outboundAudio: {
84
+ audioFileToSilkBase64: async (p: string, f?: string[]) =>
85
+ (await _audioModule.audioFileToSilkBase64(p, f)) ?? undefined,
86
+ isAudioFile: (p: string, m?: string) => _audioModule.isAudioFile(p, m),
87
+ shouldTranscodeVoice: (p: string) => _audioModule.shouldTranscodeVoice(p),
88
+ waitForFile: (p: string, ms?: number) => _audioModule.waitForFile(p, ms),
89
+ },
90
+ commands: {
91
+ resolveVersion: resolveRuntimeServiceVersion,
92
+ pluginVersion: _pluginVersion,
93
+ approveRuntimeGetter: () => {
94
+ const rt = getQQBotRuntime();
95
+ return { config: rt.config };
96
+ },
97
+ },
98
+ };
99
+ }
100
+
101
+ // ============ startGateway ============
102
+
103
+ /**
104
+ * Start the Gateway WebSocket connection.
105
+ *
106
+ * Assembles all adapters and passes them to the engine's core gateway.
107
+ */
108
+ export async function startGateway(ctx: GatewayContext): Promise<void> {
109
+ ensurePlatformAdapter();
110
+
111
+ const runtime = getQQBotRuntimeForEngine();
112
+ const accountLogger = createAccountLogger(ctx.log, ctx.account.accountId);
113
+
114
+ // Per-account registration (still global — sender is a leaf utility).
115
+ registerAccount(ctx.account.appId, {
116
+ logger: accountLogger,
117
+ markdownSupport: ctx.account.markdownSupport,
118
+ });
119
+ setBridgeLogger(accountLogger);
120
+
121
+ if (ctx.channelRuntime) {
122
+ accountLogger.info("Registering approval.native runtime context");
123
+ const lease = ctx.channelRuntime.runtimeContexts.register({
124
+ channelId: "qqbot",
125
+ accountId: ctx.account.accountId,
126
+ capability: "approval.native",
127
+ context: { account: ctx.account },
128
+ abortSignal: ctx.abortSignal,
129
+ });
130
+ accountLogger.info(`approval.native context registered (lease=${!!lease})`);
131
+ } else {
132
+ accountLogger.info("No channelRuntime — skipping approval.native registration");
133
+ }
134
+
135
+ const coreCtx: CoreGatewayContext = {
136
+ account: toGatewayAccount(ctx.account),
137
+ abortSignal: ctx.abortSignal,
138
+ cfg: ctx.cfg,
139
+ onReady: ctx.onReady,
140
+ onResumed: ctx.onResumed,
141
+ onError: ctx.onError,
142
+ log: accountLogger,
143
+ runtime,
144
+ adapters: createEngineAdapters(runtime),
145
+ };
146
+
147
+ return coreStartGateway(coreCtx);
148
+ }
149
+
150
+ // ============ Per-account logger factory ============
151
+
152
+ function createAccountLogger(
153
+ raw: GatewayContext["log"] | undefined,
154
+ accountId: string,
155
+ ): EngineLogger {
156
+ const prefix = `[${accountId}]`;
157
+ const withMeta = (msg: string, meta?: Record<string, unknown>) =>
158
+ meta && Object.keys(meta).length > 0 ? `${msg} ${JSON.stringify(meta)}` : msg;
159
+
160
+ if (!raw) {
161
+ return {
162
+ info: (msg, meta) => debugLog(`${prefix} ${withMeta(msg, meta)}`),
163
+ error: (msg, meta) => debugError(`${prefix} ${withMeta(msg, meta)}`),
164
+ warn: (msg, meta) => debugError(`${prefix} ${withMeta(msg, meta)}`),
165
+ debug: (msg, meta) => debugLog(`${prefix} ${withMeta(msg, meta)}`),
166
+ };
167
+ }
168
+ return {
169
+ info: (msg, meta) => raw.info(`${prefix} ${withMeta(msg, meta)}`),
170
+ error: (msg, meta) => raw.error(`${prefix} ${withMeta(msg, meta)}`),
171
+ warn: (msg, meta) => raw.error(`${prefix} ${withMeta(msg, meta)}`),
172
+ debug: (msg, meta) => raw.debug?.(`${prefix} ${withMeta(msg, meta)}`),
173
+ };
174
+ }