@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,231 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import type { InboundContext } from "./inbound-context.js";
3
+ import { dispatchOutbound } from "./outbound-dispatch.js";
4
+ import type { GatewayAccount, GatewayPluginRuntime } from "./types.js";
5
+
6
+ const sendVoiceMessageMock = vi.hoisted(() =>
7
+ vi.fn(async () => ({ id: "voice-1", timestamp: "2026-04-25T00:00:00.000Z" })),
8
+ );
9
+ const sendMediaMock = vi.hoisted(() =>
10
+ vi.fn(async () => ({ id: "media-1", timestamp: "2026-04-25T00:00:00.000Z" })),
11
+ );
12
+ const sendTextMock = vi.hoisted(() =>
13
+ vi.fn(async () => ({ id: "text-1", timestamp: "2026-04-25T00:00:00.000Z" })),
14
+ );
15
+ const audioFileToSilkBase64Mock = vi.hoisted(() => vi.fn(async () => "silk-base64"));
16
+
17
+ vi.mock("../messaging/sender.js", () => ({
18
+ accountToCreds: (account: GatewayAccount) => ({
19
+ appId: account.appId,
20
+ clientSecret: account.clientSecret,
21
+ }),
22
+ buildDeliveryTarget: (target: { type: string; senderId: string; groupOpenid?: string }) => ({
23
+ type: target.type === "group" ? "group" : target.type === "c2c" ? "c2c" : target.type,
24
+ id: target.type === "group" ? target.groupOpenid : target.senderId,
25
+ }),
26
+ initApiConfig: vi.fn(),
27
+ sendFileMessage: vi.fn(),
28
+ sendImage: vi.fn(),
29
+ sendText: sendTextMock,
30
+ sendVideoMessage: vi.fn(),
31
+ sendVoiceMessage: sendVoiceMessageMock,
32
+ sendMedia: sendMediaMock,
33
+ withTokenRetry: async (_creds: unknown, fn: () => Promise<unknown>) => await fn(),
34
+ }));
35
+
36
+ vi.mock("../utils/audio.js", () => ({
37
+ audioFileToSilkBase64: audioFileToSilkBase64Mock,
38
+ }));
39
+
40
+ const account: GatewayAccount = {
41
+ accountId: "qq-main",
42
+ appId: "app",
43
+ clientSecret: "secret",
44
+ markdownSupport: false,
45
+ config: {},
46
+ };
47
+
48
+ function makeInbound(overrides: Partial<InboundContext> = {}): InboundContext {
49
+ return {
50
+ event: {
51
+ type: "c2c",
52
+ senderId: "user-openid",
53
+ messageId: "msg-1",
54
+ content: "voice",
55
+ timestamp: "2026-04-25T00:00:00.000Z",
56
+ },
57
+ route: { sessionKey: "qqbot:c2c:user-openid", accountId: "qq-main" },
58
+ isGroupChat: false,
59
+ peerId: "user-openid",
60
+ qualifiedTarget: "qqbot:c2c:user-openid",
61
+ fromAddress: "qqbot:c2c:user-openid",
62
+ parsedContent: "voice",
63
+ userContent: "voice",
64
+ quotePart: "",
65
+ dynamicCtx: "",
66
+ userMessage: "voice",
67
+ agentBody: "voice",
68
+ body: "voice",
69
+ systemPrompts: [],
70
+ attachments: {
71
+ attachmentInfo: "",
72
+ imageUrls: [],
73
+ imageMediaTypes: [],
74
+ voiceAttachmentPaths: [],
75
+ voiceAttachmentUrls: [],
76
+ voiceAsrReferTexts: [],
77
+ voiceTranscripts: [],
78
+ voiceTranscriptSources: [],
79
+ attachmentLocalPaths: [],
80
+ },
81
+ localMediaPaths: [],
82
+ localMediaTypes: [],
83
+ remoteMediaUrls: [],
84
+ remoteMediaTypes: [],
85
+ uniqueVoicePaths: [],
86
+ uniqueVoiceUrls: [],
87
+ uniqueVoiceAsrReferTexts: [],
88
+ voiceMediaTypes: [],
89
+ hasAsrReferFallback: false,
90
+ voiceTranscriptSources: [],
91
+ commandAuthorized: false,
92
+ blocked: false,
93
+ skipped: false,
94
+ typing: { keepAlive: null },
95
+ ...overrides,
96
+ };
97
+ }
98
+
99
+ function makeRuntime(params: {
100
+ onFinalize?: (ctx: Record<string, unknown>) => void;
101
+ onDeliver?: (
102
+ deliver: (
103
+ payload: { text?: string; audioAsVoice?: boolean },
104
+ info: { kind: string },
105
+ ) => Promise<void>,
106
+ ) => Promise<void>;
107
+ }): GatewayPluginRuntime {
108
+ return {
109
+ channel: {
110
+ activity: { record: vi.fn() },
111
+ routing: {
112
+ resolveAgentRoute: vi.fn(() => ({
113
+ sessionKey: "qqbot:c2c:user-openid",
114
+ accountId: "qq-main",
115
+ })),
116
+ },
117
+ reply: {
118
+ dispatchReplyWithBufferedBlockDispatcher: vi.fn(async (rawParams: unknown) => {
119
+ const deliver = (
120
+ rawParams as {
121
+ dispatcherOptions: {
122
+ deliver: (
123
+ payload: { text?: string; audioAsVoice?: boolean },
124
+ info: { kind: string },
125
+ ) => Promise<void>;
126
+ };
127
+ }
128
+ ).dispatcherOptions.deliver;
129
+ await params.onDeliver?.(deliver);
130
+ }),
131
+ finalizeInboundContext: vi.fn((rawCtx: Record<string, unknown>) => {
132
+ params.onFinalize?.(rawCtx);
133
+ return rawCtx;
134
+ }),
135
+ formatInboundEnvelope: vi.fn(() => "voice"),
136
+ resolveEffectiveMessagesConfig: vi.fn(() => ({})),
137
+ resolveEnvelopeFormatOptions: vi.fn(() => ({})),
138
+ },
139
+ session: {
140
+ resolveStorePath: vi.fn(() => "/tmp/openclaw/qqbot-sessions.json"),
141
+ recordInboundSession: vi.fn(async () => undefined),
142
+ },
143
+ turn: {
144
+ run: vi.fn(async (rawParams: unknown) => {
145
+ const params = rawParams as {
146
+ raw: unknown;
147
+ adapter: {
148
+ ingest: (raw: unknown) => unknown;
149
+ resolveTurn: (...args: unknown[]) => unknown;
150
+ };
151
+ };
152
+ const input = await params.adapter.ingest(params.raw);
153
+ const turn = (await params.adapter.resolveTurn(
154
+ input,
155
+ {
156
+ kind: "message",
157
+ canStartAgentTurn: true,
158
+ },
159
+ {},
160
+ )) as { runDispatch: () => Promise<unknown> };
161
+ return { dispatchResult: await turn.runDispatch() };
162
+ }),
163
+ },
164
+ text: {
165
+ chunkMarkdownText: (text: string) => [text],
166
+ },
167
+ },
168
+ tts: {
169
+ textToSpeech: vi.fn(async () => ({
170
+ success: true,
171
+ audioPath: "/tmp/openclaw-qqbot/tts.wav",
172
+ provider: "test-tts",
173
+ outputFormat: "wav",
174
+ })),
175
+ },
176
+ };
177
+ }
178
+
179
+ describe("dispatchOutbound", () => {
180
+ beforeEach(() => {
181
+ vi.clearAllMocks();
182
+ });
183
+
184
+ it("marks voice-only inbound as audio without adding voice paths to MediaPaths", async () => {
185
+ let finalized: Record<string, unknown> | undefined;
186
+ const runtime = makeRuntime({ onFinalize: (ctx) => (finalized = ctx) });
187
+
188
+ await dispatchOutbound(
189
+ makeInbound({
190
+ uniqueVoicePaths: ["/tmp/qqbot/voice.wav"],
191
+ voiceMediaTypes: ["audio/wav"],
192
+ }),
193
+ { runtime, cfg: {}, account },
194
+ );
195
+
196
+ expect(finalized).toMatchObject({
197
+ MediaType: "audio/wav",
198
+ MediaTypes: ["audio/wav"],
199
+ QQVoiceAttachmentPaths: ["/tmp/qqbot/voice.wav"],
200
+ });
201
+ expect(finalized).not.toHaveProperty("MediaPath");
202
+ expect(finalized).not.toHaveProperty("MediaPaths");
203
+ });
204
+
205
+ it("synthesizes plain audioAsVoice text as a QQ voice reply", async () => {
206
+ const runtime = makeRuntime({
207
+ onDeliver: async (deliver) => {
208
+ await deliver({ text: "read this aloud", audioAsVoice: true }, { kind: "block" });
209
+ },
210
+ });
211
+
212
+ await dispatchOutbound(makeInbound(), { runtime, cfg: {}, account });
213
+
214
+ expect(runtime.tts.textToSpeech).toHaveBeenCalledWith({
215
+ text: "read this aloud",
216
+ cfg: {},
217
+ channel: "qqbot",
218
+ accountId: "qq-main",
219
+ });
220
+ expect(audioFileToSilkBase64Mock).toHaveBeenCalledWith("/tmp/openclaw-qqbot/tts.wav");
221
+ expect(sendMediaMock).toHaveBeenCalledWith(
222
+ expect.objectContaining({
223
+ kind: "voice",
224
+ source: { base64: "silk-base64" },
225
+ msgId: "msg-1",
226
+ ttsText: "read this aloud",
227
+ }),
228
+ );
229
+ expect(sendTextMock).not.toHaveBeenCalled();
230
+ });
231
+ });