@openclaw/qqbot 2026.5.2 → 2026.5.3-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.
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
@@ -1,84 +0,0 @@
1
- import {
2
- AllowFromListSchema,
3
- buildChannelConfigSchema,
4
- } from "openclaw/plugin-sdk/channel-config-schema";
5
- import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
6
- import { z } from "zod";
7
-
8
- const AudioFormatPolicySchema = z
9
- .object({
10
- sttDirectFormats: z.array(z.string()).optional(),
11
- uploadDirectFormats: z.array(z.string()).optional(),
12
- transcodeEnabled: z.boolean().optional(),
13
- })
14
- .optional();
15
-
16
- const QQBotSttSchema = z
17
- .object({
18
- enabled: z.boolean().optional(),
19
- provider: z.string().optional(),
20
- baseUrl: z.string().optional(),
21
- apiKey: z.string().optional(),
22
- model: z.string().optional(),
23
- })
24
- .strict()
25
- .optional();
26
-
27
- /** When `true`, same as `mode: "partial"` and `c2cStreamApi: true` for C2C. Object form kept for legacy configs. */
28
- const QQBotStreamingSchema = z
29
- .union([
30
- z.boolean(),
31
- z
32
- .object({
33
- /** "partial" (default) enables block streaming; "off" disables it. */
34
- mode: z.enum(["off", "partial"]).default("partial"),
35
- /** @deprecated Prefer `streaming: true`. */
36
- c2cStreamApi: z.boolean().optional(),
37
- })
38
- .passthrough(),
39
- ])
40
- .optional();
41
-
42
- const QQBotExecApprovalsSchema = z
43
- .object({
44
- enabled: z.union([z.boolean(), z.literal("auto")]).optional(),
45
- approvers: z.array(z.string()).optional(),
46
- agentFilter: z.array(z.string()).optional(),
47
- sessionFilter: z.array(z.string()).optional(),
48
- target: z.enum(["dm", "channel", "both"]).optional(),
49
- })
50
- .strict()
51
- .optional();
52
-
53
- const QQBotDmPolicySchema = z.enum(["open", "allowlist", "disabled"]).optional();
54
- const QQBotGroupPolicySchema = z.enum(["open", "allowlist", "disabled"]).optional();
55
-
56
- const QQBotAccountSchema = z
57
- .object({
58
- enabled: z.boolean().optional(),
59
- name: z.string().optional(),
60
- appId: z.string().optional(),
61
- clientSecret: buildSecretInputSchema().optional(),
62
- clientSecretFile: z.string().optional(),
63
- allowFrom: AllowFromListSchema,
64
- groupAllowFrom: AllowFromListSchema,
65
- dmPolicy: QQBotDmPolicySchema,
66
- groupPolicy: QQBotGroupPolicySchema,
67
- systemPrompt: z.string().optional(),
68
- markdownSupport: z.boolean().optional(),
69
- voiceDirectUploadFormats: z.array(z.string()).optional(),
70
- audioFormatPolicy: AudioFormatPolicySchema,
71
- urlDirectUpload: z.boolean().optional(),
72
- upgradeUrl: z.string().optional(),
73
- upgradeMode: z.enum(["doc", "hot-reload"]).optional(),
74
- streaming: QQBotStreamingSchema,
75
- execApprovals: QQBotExecApprovalsSchema,
76
- })
77
- .passthrough();
78
-
79
- export const QQBotConfigSchema = QQBotAccountSchema.extend({
80
- stt: QQBotSttSchema,
81
- accounts: z.object({}).catchall(QQBotAccountSchema.passthrough()).optional(),
82
- defaultAccount: z.string().optional(),
83
- }).passthrough();
84
- export const qqbotChannelConfigSchema = buildChannelConfigSchema(QQBotConfigSchema);
@@ -1,364 +0,0 @@
1
- import fs from "node:fs";
2
- import { type JsonSchemaObject, validateJsonSchemaValue } from "openclaw/plugin-sdk/config-schema";
3
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
4
- import { describe, expect, it } from "vitest";
5
- import { qqbotSetupAdapterShared } from "./bridge/config-shared.js";
6
- import {
7
- DEFAULT_ACCOUNT_ID,
8
- resolveDefaultQQBotAccountId,
9
- resolveQQBotAccount,
10
- } from "./bridge/config.js";
11
- import { qqbotSetupPlugin } from "./channel.setup.js";
12
- import { QQBotConfigSchema } from "./config-schema.js";
13
- import { makeQqbotDefaultAccountConfig, makeQqbotSecretRefConfig } from "./qqbot-test-support.js";
14
-
15
- describe("qqbot config", () => {
16
- it("accepts top-level speech overrides in the manifest schema", () => {
17
- const manifest = JSON.parse(
18
- fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
19
- ) as { configSchema: JsonSchemaObject };
20
-
21
- const result = validateJsonSchemaValue({
22
- schema: manifest.configSchema,
23
- cacheKey: "qqbot.manifest.speech-overrides",
24
- value: {
25
- stt: {
26
- provider: "openai",
27
- baseUrl: "https://example.com/v1",
28
- apiKey: "stt-key",
29
- model: "whisper-1",
30
- },
31
- },
32
- });
33
-
34
- expect(result.ok).toBe(true);
35
- });
36
-
37
- it("accepts defaultAccount in the manifest schema", () => {
38
- const manifest = JSON.parse(
39
- fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
40
- ) as { configSchema: JsonSchemaObject };
41
-
42
- const result = validateJsonSchemaValue({
43
- schema: manifest.configSchema,
44
- cacheKey: "qqbot.manifest.default-account",
45
- value: {
46
- defaultAccount: "bot2",
47
- accounts: {
48
- bot2: {
49
- appId: "654321",
50
- },
51
- },
52
- },
53
- });
54
-
55
- expect(result.ok).toBe(true);
56
- });
57
-
58
- it("honors configured defaultAccount when resolving the default QQ Bot account id", () => {
59
- const cfg = {
60
- channels: {
61
- qqbot: {
62
- defaultAccount: "bot2",
63
- accounts: {
64
- bot2: {
65
- appId: "654321",
66
- },
67
- },
68
- },
69
- },
70
- } as OpenClawConfig;
71
-
72
- expect(resolveDefaultQQBotAccountId(cfg)).toBe("bot2");
73
- });
74
-
75
- it("accepts SecretRef-backed credentials in the runtime schema", () => {
76
- const parsed = QQBotConfigSchema.safeParse({
77
- defaultAccount: "bot2",
78
- appId: "123456",
79
- clientSecret: {
80
- source: "env",
81
- provider: "default",
82
- id: "QQBOT_CLIENT_SECRET",
83
- },
84
- allowFrom: ["*"],
85
- audioFormatPolicy: {
86
- sttDirectFormats: [".wav"],
87
- uploadDirectFormats: [".mp3"],
88
- transcodeEnabled: false,
89
- },
90
- urlDirectUpload: false,
91
- upgradeUrl: "https://docs.openclaw.ai/channels/qqbot",
92
- upgradeMode: "doc",
93
- accounts: {
94
- bot2: {
95
- appId: "654321",
96
- clientSecret: {
97
- source: "env",
98
- provider: "default",
99
- id: "QQBOT_CLIENT_SECRET_BOT2",
100
- },
101
- allowFrom: ["user-1"],
102
- },
103
- },
104
- });
105
-
106
- expect(parsed.success).toBe(true);
107
- });
108
-
109
- it("accepts account-level speech overrides as forward-compatible config", () => {
110
- const parsed = QQBotConfigSchema.safeParse({
111
- accounts: {
112
- bot2: {
113
- appId: "654321",
114
- stt: {
115
- provider: "openai",
116
- },
117
- },
118
- },
119
- });
120
-
121
- expect(parsed.success).toBe(true);
122
- });
123
-
124
- it("preserves top-level media and upgrade config on the default account", () => {
125
- const cfg = {
126
- channels: {
127
- qqbot: {
128
- appId: "123456",
129
- clientSecret: "secret-value",
130
- audioFormatPolicy: {
131
- sttDirectFormats: [".wav"],
132
- uploadDirectFormats: [".mp3"],
133
- transcodeEnabled: false,
134
- },
135
- urlDirectUpload: false,
136
- upgradeUrl: "https://docs.openclaw.ai/channels/qqbot",
137
- upgradeMode: "hot-reload",
138
- },
139
- },
140
- } as OpenClawConfig;
141
-
142
- const resolved = resolveQQBotAccount(cfg, DEFAULT_ACCOUNT_ID);
143
-
144
- expect(resolved.clientSecret).toBe("secret-value");
145
- expect(resolved.config.audioFormatPolicy).toEqual({
146
- sttDirectFormats: [".wav"],
147
- uploadDirectFormats: [".mp3"],
148
- transcodeEnabled: false,
149
- });
150
- expect(resolved.config.urlDirectUpload).toBe(false);
151
- expect(resolved.config.upgradeUrl).toBe("https://docs.openclaw.ai/channels/qqbot");
152
- expect(resolved.config.upgradeMode).toBe("hot-reload");
153
- });
154
-
155
- it("uses configured defaultAccount when accountId is omitted", () => {
156
- const cfg = {
157
- channels: {
158
- qqbot: {
159
- defaultAccount: "bot2",
160
- accounts: {
161
- bot2: {
162
- appId: "654321",
163
- clientSecret: "secret-value",
164
- name: "Bot Two",
165
- },
166
- },
167
- },
168
- },
169
- } as OpenClawConfig;
170
-
171
- const resolved = resolveQQBotAccount(cfg);
172
-
173
- expect(resolved.accountId).toBe("bot2");
174
- expect(resolved.appId).toBe("654321");
175
- expect(resolved.clientSecret).toBe("secret-value");
176
- expect(resolved.name).toBe("Bot Two");
177
- });
178
-
179
- it("rejects unresolved SecretRefs on runtime resolution", () => {
180
- const cfg = makeQqbotSecretRefConfig();
181
-
182
- expect(() => resolveQQBotAccount(cfg, DEFAULT_ACCOUNT_ID)).toThrow(
183
- 'channels.qqbot.clientSecret: unresolved SecretRef "env:default:QQBOT_CLIENT_SECRET"',
184
- );
185
- });
186
-
187
- it("allows unresolved SecretRefs for setup/status flows", () => {
188
- const cfg = makeQqbotSecretRefConfig();
189
-
190
- const resolved = resolveQQBotAccount(cfg, DEFAULT_ACCOUNT_ID, {
191
- allowUnresolvedSecretRef: true,
192
- });
193
-
194
- expect(resolved.clientSecret).toBe("");
195
- expect(resolved.secretSource).toBe("config");
196
- expect(qqbotSetupPlugin.config.isConfigured?.(resolved, cfg)).toBe(true);
197
- expect(qqbotSetupPlugin.config.describeAccount?.(resolved, cfg)?.configured).toBe(true);
198
- });
199
-
200
- it.each([
201
- {
202
- accountId: DEFAULT_ACCOUNT_ID,
203
- inputAccountId: DEFAULT_ACCOUNT_ID,
204
- expectedPath: ["channels", "qqbot"],
205
- },
206
- {
207
- accountId: "bot2",
208
- inputAccountId: "bot2",
209
- expectedPath: ["channels", "qqbot", "accounts", "bot2"],
210
- },
211
- ])("splits --token on the first colon for $accountId", ({ inputAccountId, expectedPath }) => {
212
- const setup = qqbotSetupPlugin.setup;
213
- expect(setup).toBeDefined();
214
-
215
- const next = setup!.applyAccountConfig?.({
216
- cfg: {} as OpenClawConfig,
217
- accountId: inputAccountId,
218
- input: {
219
- token: "102905186:Oi2Mg1Mh2Ni3:Pl7TpBXuHe1OmAYwKi7W",
220
- },
221
- }) as Record<string, unknown>;
222
-
223
- const accountConfig = expectedPath.reduce<unknown>((value, key) => {
224
- if (!value || typeof value !== "object") {
225
- return undefined;
226
- }
227
- return (value as Record<string, unknown>)[key];
228
- }, next) as Record<string, unknown> | undefined;
229
-
230
- expect(accountConfig).toMatchObject({
231
- enabled: true,
232
- appId: "102905186",
233
- clientSecret: "Oi2Mg1Mh2Ni3:Pl7TpBXuHe1OmAYwKi7W",
234
- });
235
- });
236
-
237
- it("rejects malformed --token consistently across setup paths", () => {
238
- const runtimeSetup = qqbotSetupAdapterShared;
239
- const lightweightSetup = qqbotSetupPlugin.setup;
240
- expect(runtimeSetup).toBeDefined();
241
- expect(lightweightSetup).toBeDefined();
242
-
243
- const input = { token: "broken", name: "Bad" };
244
-
245
- expect(
246
- runtimeSetup.validateInput?.({
247
- cfg: {} as OpenClawConfig,
248
- accountId: DEFAULT_ACCOUNT_ID,
249
- input,
250
- } as never),
251
- ).toBe("QQBot --token must be in appId:clientSecret format");
252
- expect(
253
- lightweightSetup!.validateInput?.({
254
- cfg: {} as OpenClawConfig,
255
- accountId: DEFAULT_ACCOUNT_ID,
256
- input,
257
- } as never),
258
- ).toBe("QQBot --token must be in appId:clientSecret format");
259
- expect(
260
- runtimeSetup.applyAccountConfig?.({
261
- cfg: {} as OpenClawConfig,
262
- accountId: DEFAULT_ACCOUNT_ID,
263
- input,
264
- } as never),
265
- ).toEqual({});
266
- expect(
267
- lightweightSetup!.applyAccountConfig?.({
268
- cfg: {} as OpenClawConfig,
269
- accountId: DEFAULT_ACCOUNT_ID,
270
- input,
271
- } as never),
272
- ).toEqual({});
273
- });
274
-
275
- it("preserves the --use-env add flow across setup paths", () => {
276
- const runtimeSetup = qqbotSetupAdapterShared;
277
- const lightweightSetup = qqbotSetupPlugin.setup;
278
- expect(runtimeSetup).toBeDefined();
279
- expect(lightweightSetup).toBeDefined();
280
-
281
- const input = { useEnv: true, name: "Env Bot" };
282
-
283
- expect(
284
- runtimeSetup.applyAccountConfig?.({
285
- cfg: {} as OpenClawConfig,
286
- accountId: DEFAULT_ACCOUNT_ID,
287
- input,
288
- } as never),
289
- ).toMatchObject({
290
- channels: {
291
- qqbot: {
292
- enabled: true,
293
- allowFrom: ["*"],
294
- name: "Env Bot",
295
- },
296
- },
297
- });
298
- expect(
299
- lightweightSetup!.applyAccountConfig?.({
300
- cfg: {} as OpenClawConfig,
301
- accountId: DEFAULT_ACCOUNT_ID,
302
- input,
303
- } as never),
304
- ).toMatchObject({
305
- channels: {
306
- qqbot: {
307
- enabled: true,
308
- allowFrom: ["*"],
309
- name: "Env Bot",
310
- },
311
- },
312
- });
313
- });
314
-
315
- it("uses configured defaultAccount when runtime setup accountId is omitted", () => {
316
- const runtimeSetup = qqbotSetupAdapterShared;
317
- expect(runtimeSetup).toBeDefined();
318
-
319
- expect(
320
- runtimeSetup.resolveAccountId?.({
321
- cfg: makeQqbotDefaultAccountConfig(),
322
- accountId: undefined,
323
- } as never),
324
- ).toBe("bot2");
325
- });
326
-
327
- it("rejects --use-env for named accounts across setup paths", () => {
328
- const runtimeSetup = qqbotSetupAdapterShared;
329
- const lightweightSetup = qqbotSetupPlugin.setup;
330
- expect(runtimeSetup).toBeDefined();
331
- expect(lightweightSetup).toBeDefined();
332
-
333
- const input = { useEnv: true, name: "Env Bot" };
334
-
335
- expect(
336
- runtimeSetup.validateInput?.({
337
- cfg: {} as OpenClawConfig,
338
- accountId: "bot2",
339
- input,
340
- } as never),
341
- ).toBe("QQBot --use-env only supports the default account");
342
- expect(
343
- lightweightSetup!.validateInput?.({
344
- cfg: {} as OpenClawConfig,
345
- accountId: "bot2",
346
- input,
347
- } as never),
348
- ).toBe("QQBot --use-env only supports the default account");
349
- expect(
350
- runtimeSetup.applyAccountConfig?.({
351
- cfg: {} as OpenClawConfig,
352
- accountId: "bot2",
353
- input,
354
- } as never),
355
- ).toEqual({});
356
- expect(
357
- lightweightSetup!.applyAccountConfig?.({
358
- cfg: {} as OpenClawConfig,
359
- accountId: "bot2",
360
- input,
361
- } as never),
362
- ).toEqual({});
363
- });
364
- });
@@ -1,198 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { resolveQQBotAccess } from "./access-control.js";
3
- import { QQBOT_ACCESS_REASON } from "./types.js";
4
-
5
- describe("resolveQQBotAccess", () => {
6
- describe("DM scenarios", () => {
7
- it("allows default-open DMs when allowFrom is omitted", () => {
8
- const result = resolveQQBotAccess({ isGroup: false, senderId: "USER1" });
9
- expect(result).toMatchObject({
10
- decision: "allow",
11
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_OPEN,
12
- dmPolicy: "open",
13
- effectiveAllowFrom: ["*"],
14
- });
15
- });
16
-
17
- it("allows default-open DMs when allowFrom is explicitly empty", () => {
18
- const result = resolveQQBotAccess({
19
- isGroup: false,
20
- senderId: "USER1",
21
- allowFrom: [],
22
- });
23
- expect(result).toMatchObject({
24
- decision: "allow",
25
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_OPEN,
26
- dmPolicy: "open",
27
- effectiveAllowFrom: ["*"],
28
- });
29
- });
30
-
31
- it("allows everyone with wildcard allowFrom", () => {
32
- const result = resolveQQBotAccess({
33
- isGroup: false,
34
- senderId: "USER1",
35
- allowFrom: ["*"],
36
- });
37
- expect(result.decision).toBe("allow");
38
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.DM_POLICY_OPEN);
39
- });
40
-
41
- it("allows sender matching the allowlist", () => {
42
- const result = resolveQQBotAccess({
43
- isGroup: false,
44
- senderId: "USER1",
45
- allowFrom: ["USER1"],
46
- });
47
- expect(result.decision).toBe("allow");
48
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.DM_POLICY_ALLOWLISTED);
49
- expect(result.dmPolicy).toBe("allowlist");
50
- });
51
-
52
- it("allows open mode when sender matches restrictive allowFrom", () => {
53
- const result = resolveQQBotAccess({
54
- isGroup: false,
55
- senderId: "USER1",
56
- allowFrom: ["USER1"],
57
- dmPolicy: "open",
58
- });
59
- expect(result.decision).toBe("allow");
60
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.DM_POLICY_ALLOWLISTED);
61
- expect(result.reason).toBe("dmPolicy=open (allowlisted)");
62
- });
63
-
64
- it("blocks sender not in allowlist", () => {
65
- const result = resolveQQBotAccess({
66
- isGroup: false,
67
- senderId: "USER2",
68
- allowFrom: ["USER1"],
69
- });
70
- expect(result.decision).toBe("block");
71
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED);
72
- });
73
-
74
- it("blocks DM when dmPolicy=disabled (even with wildcard)", () => {
75
- const result = resolveQQBotAccess({
76
- isGroup: false,
77
- senderId: "USER1",
78
- allowFrom: ["*"],
79
- dmPolicy: "disabled",
80
- });
81
- expect(result.decision).toBe("block");
82
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.DM_POLICY_DISABLED);
83
- });
84
-
85
- it("blocks DM with allowlist policy but empty allowlist", () => {
86
- const result = resolveQQBotAccess({
87
- isGroup: false,
88
- senderId: "USER1",
89
- dmPolicy: "allowlist",
90
- });
91
- expect(result.decision).toBe("block");
92
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.DM_POLICY_EMPTY_ALLOWLIST);
93
- });
94
-
95
- it("normalizes qqbot: prefix and case when matching", () => {
96
- const result = resolveQQBotAccess({
97
- isGroup: false,
98
- senderId: "qqbot:user1",
99
- allowFrom: ["QQBot:USER1"],
100
- });
101
- expect(result.decision).toBe("allow");
102
- });
103
- });
104
-
105
- describe("group scenarios", () => {
106
- it("inherits allowFrom for group access when no groupAllowFrom is set", () => {
107
- const allowed = resolveQQBotAccess({
108
- isGroup: true,
109
- senderId: "USER1",
110
- allowFrom: ["USER1"],
111
- });
112
- expect(allowed.decision).toBe("allow");
113
- expect(allowed.groupPolicy).toBe("allowlist");
114
-
115
- const blocked = resolveQQBotAccess({
116
- isGroup: true,
117
- senderId: "USER2",
118
- allowFrom: ["USER1"],
119
- });
120
- expect(blocked.decision).toBe("block");
121
- expect(blocked.reasonCode).toBe(QQBOT_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED);
122
- });
123
-
124
- it("uses groupAllowFrom when explicitly provided", () => {
125
- const result = resolveQQBotAccess({
126
- isGroup: true,
127
- senderId: "USER2",
128
- allowFrom: ["USER1"],
129
- groupAllowFrom: ["USER2"],
130
- });
131
- expect(result.decision).toBe("allow");
132
- });
133
-
134
- it("blocks when groupPolicy=disabled", () => {
135
- const result = resolveQQBotAccess({
136
- isGroup: true,
137
- senderId: "USER1",
138
- allowFrom: ["*"],
139
- groupPolicy: "disabled",
140
- });
141
- expect(result.decision).toBe("block");
142
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.GROUP_POLICY_DISABLED);
143
- });
144
-
145
- it("allows anyone when groupPolicy=open", () => {
146
- const result = resolveQQBotAccess({
147
- isGroup: true,
148
- senderId: "RANDOM_USER",
149
- allowFrom: ["USER1"],
150
- groupPolicy: "open",
151
- });
152
- expect(result.decision).toBe("allow");
153
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.GROUP_POLICY_ALLOWED);
154
- });
155
-
156
- it("blocks when groupPolicy=allowlist but list is empty", () => {
157
- const result = resolveQQBotAccess({
158
- isGroup: true,
159
- senderId: "USER1",
160
- groupPolicy: "allowlist",
161
- });
162
- expect(result.decision).toBe("block");
163
- expect(result.reasonCode).toBe(QQBOT_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST);
164
- });
165
- });
166
-
167
- describe("backwards compatibility (legacy allowFrom-only configs)", () => {
168
- it("legacy allowFrom=['*'] stays fully open for both DM and group", () => {
169
- const dm = resolveQQBotAccess({
170
- isGroup: false,
171
- senderId: "RANDOM",
172
- allowFrom: ["*"],
173
- });
174
- const group = resolveQQBotAccess({
175
- isGroup: true,
176
- senderId: "RANDOM",
177
- allowFrom: ["*"],
178
- });
179
- expect(dm.decision).toBe("allow");
180
- expect(group.decision).toBe("allow");
181
- });
182
-
183
- it("legacy allowFrom=['USER1'] locks down both DM and group to USER1", () => {
184
- const allowedDm = resolveQQBotAccess({
185
- isGroup: false,
186
- senderId: "USER1",
187
- allowFrom: ["USER1"],
188
- });
189
- const blockedGroup = resolveQQBotAccess({
190
- isGroup: true,
191
- senderId: "INTRUDER",
192
- allowFrom: ["USER1"],
193
- });
194
- expect(allowedDm.decision).toBe("allow");
195
- expect(blockedGroup.decision).toBe("block");
196
- });
197
- });
198
- });