@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
@@ -1,226 +0,0 @@
1
- /**
2
- * QQBot inbound access decision.
3
- *
4
- * This module is the single place where the QQBot engine decides
5
- * whether an inbound message from a given sender is allowed to
6
- * proceed into the outbound pipeline. The implementation mirrors the
7
- * semantics of the framework-wide `resolveDmGroupAccessDecision`
8
- * (`src/security/dm-policy-shared.ts`) but is kept standalone so the
9
- * `engine/` layer does not pull in `openclaw/plugin-sdk/*` modules —
10
- * a hard constraint shared with the standalone `openclaw-qqbot` build.
11
- *
12
- * If in the future we lift the zero-dependency rule in the engine
13
- * layer, this file can be replaced by a thin adapter around the
14
- * framework API with identical semantics.
15
- */
16
-
17
- import { resolveQQBotEffectivePolicies, type EffectivePolicyInput } from "./resolve-policy.js";
18
- import { createQQBotSenderMatcher, normalizeQQBotAllowFrom } from "./sender-match.js";
19
- import {
20
- QQBOT_ACCESS_REASON,
21
- type QQBotAccessResult,
22
- type QQBotDmPolicy,
23
- type QQBotGroupPolicy,
24
- } from "./types.js";
25
-
26
- interface QQBotAccessInput extends EffectivePolicyInput {
27
- /** Whether the inbound originated in a group (or guild) chat. */
28
- isGroup: boolean;
29
- /** The raw inbound sender id as provided by the QQ event. */
30
- senderId: string;
31
- }
32
-
33
- /**
34
- * Evaluate the inbound access policy.
35
- *
36
- * Semantics (aligned with `resolveDmGroupAccessDecision`):
37
- * - Group message:
38
- * - `groupPolicy=disabled` → block
39
- * - `groupPolicy=open` → allow
40
- * - `groupPolicy=allowlist`:
41
- * - empty effectiveGroupAllowFrom → block (empty_allowlist)
42
- * - sender not in list → block (not_allowlisted)
43
- * - otherwise → allow
44
- * - Direct message:
45
- * - `dmPolicy=disabled` → block
46
- * - `dmPolicy=open` → allow wildcard, legacy empty allowFrom, or matching allowFrom
47
- * - `dmPolicy=allowlist`:
48
- * - empty effectiveAllowFrom → block (empty_allowlist)
49
- * - sender not in list → block (not_allowlisted)
50
- * - otherwise → allow
51
- *
52
- * The function never throws; callers can rely on the returned
53
- * `decision`/`reasonCode` pair for branching.
54
- */
55
- export function resolveQQBotAccess(input: QQBotAccessInput): QQBotAccessResult {
56
- const { dmPolicy, groupPolicy } = resolveQQBotEffectivePolicies(input);
57
-
58
- // Per framework convention: groupAllowFrom falls back to allowFrom
59
- // when not provided. We preserve that behaviour so a single
60
- // `allowFrom` entry locks down both DM and group.
61
- const rawGroupAllowFrom =
62
- input.groupAllowFrom && input.groupAllowFrom.length > 0
63
- ? input.groupAllowFrom
64
- : (input.allowFrom ?? []);
65
-
66
- const normalizedAllowFrom = normalizeQQBotAllowFrom(input.allowFrom);
67
- const effectiveAllowFrom =
68
- dmPolicy === "open" && normalizedAllowFrom.length === 0 ? ["*"] : normalizedAllowFrom;
69
- const effectiveGroupAllowFrom = normalizeQQBotAllowFrom(rawGroupAllowFrom);
70
-
71
- const isSenderAllowed = createQQBotSenderMatcher(input.senderId);
72
-
73
- if (input.isGroup) {
74
- return evaluateGroupDecision({
75
- groupPolicy,
76
- dmPolicy,
77
- effectiveAllowFrom,
78
- effectiveGroupAllowFrom,
79
- isSenderAllowed,
80
- });
81
- }
82
-
83
- return evaluateDmDecision({
84
- groupPolicy,
85
- dmPolicy,
86
- effectiveAllowFrom,
87
- effectiveGroupAllowFrom,
88
- isSenderAllowed,
89
- });
90
- }
91
-
92
- // ---- internal helpers ------------------------------------------------
93
-
94
- interface DecisionContext {
95
- dmPolicy: QQBotDmPolicy;
96
- groupPolicy: QQBotGroupPolicy;
97
- effectiveAllowFrom: string[];
98
- effectiveGroupAllowFrom: string[];
99
- isSenderAllowed: (allowFrom: string[]) => boolean;
100
- }
101
-
102
- function evaluateGroupDecision(ctx: DecisionContext): QQBotAccessResult {
103
- const base = buildResultBase(ctx);
104
-
105
- if (ctx.groupPolicy === "disabled") {
106
- return {
107
- ...base,
108
- decision: "block",
109
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_DISABLED,
110
- reason: "groupPolicy=disabled",
111
- };
112
- }
113
-
114
- if (ctx.groupPolicy === "open") {
115
- return {
116
- ...base,
117
- decision: "allow",
118
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_ALLOWED,
119
- reason: "groupPolicy=open",
120
- };
121
- }
122
-
123
- // groupPolicy === "allowlist"
124
- if (ctx.effectiveGroupAllowFrom.length === 0) {
125
- return {
126
- ...base,
127
- decision: "block",
128
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST,
129
- reason: "groupPolicy=allowlist (empty allowlist)",
130
- };
131
- }
132
-
133
- if (!ctx.isSenderAllowed(ctx.effectiveGroupAllowFrom)) {
134
- return {
135
- ...base,
136
- decision: "block",
137
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED,
138
- reason: "groupPolicy=allowlist (not allowlisted)",
139
- };
140
- }
141
-
142
- return {
143
- ...base,
144
- decision: "allow",
145
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_ALLOWED,
146
- reason: "groupPolicy=allowlist (allowlisted)",
147
- };
148
- }
149
-
150
- function evaluateDmDecision(ctx: DecisionContext): QQBotAccessResult {
151
- const base = buildResultBase(ctx);
152
-
153
- if (ctx.dmPolicy === "disabled") {
154
- return {
155
- ...base,
156
- decision: "block",
157
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_DISABLED,
158
- reason: "dmPolicy=disabled",
159
- };
160
- }
161
-
162
- if (ctx.dmPolicy === "open") {
163
- if (ctx.effectiveAllowFrom.includes("*")) {
164
- return {
165
- ...base,
166
- decision: "allow",
167
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_OPEN,
168
- reason: "dmPolicy=open",
169
- };
170
- }
171
- if (ctx.isSenderAllowed(ctx.effectiveAllowFrom)) {
172
- return {
173
- ...base,
174
- decision: "allow",
175
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_ALLOWLISTED,
176
- reason: "dmPolicy=open (allowlisted)",
177
- };
178
- }
179
- return {
180
- ...base,
181
- decision: "block",
182
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
183
- reason: "dmPolicy=open (not allowlisted)",
184
- };
185
- }
186
-
187
- // dmPolicy === "allowlist"
188
- if (ctx.effectiveAllowFrom.length === 0) {
189
- return {
190
- ...base,
191
- decision: "block",
192
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_EMPTY_ALLOWLIST,
193
- reason: "dmPolicy=allowlist (empty allowlist)",
194
- };
195
- }
196
-
197
- if (!ctx.isSenderAllowed(ctx.effectiveAllowFrom)) {
198
- return {
199
- ...base,
200
- decision: "block",
201
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
202
- reason: "dmPolicy=allowlist (not allowlisted)",
203
- };
204
- }
205
-
206
- return {
207
- ...base,
208
- decision: "allow",
209
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_ALLOWLISTED,
210
- reason: "dmPolicy=allowlist (allowlisted)",
211
- };
212
- }
213
-
214
- function buildResultBase(
215
- ctx: DecisionContext,
216
- ): Pick<
217
- QQBotAccessResult,
218
- "effectiveAllowFrom" | "effectiveGroupAllowFrom" | "dmPolicy" | "groupPolicy"
219
- > {
220
- return {
221
- effectiveAllowFrom: ctx.effectiveAllowFrom,
222
- effectiveGroupAllowFrom: ctx.effectiveGroupAllowFrom,
223
- dmPolicy: ctx.dmPolicy,
224
- groupPolicy: ctx.groupPolicy,
225
- };
226
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * QQBot inbound access control — public entry points.
3
- *
4
- * Consumers (inbound-pipeline and future adapters) should import from
5
- * this barrel to keep the internal module layout opaque.
6
- */
7
-
8
- export { resolveQQBotAccess } from "./access-control.js";
9
- export { createQQBotSenderMatcher, normalizeQQBotAllowFrom } from "./sender-match.js";
10
- export {
11
- type QQBotAccessDecision,
12
- type QQBotAccessReasonCode,
13
- type QQBotAccessResult,
14
- type QQBotDmPolicy,
15
- type QQBotGroupPolicy,
16
- } from "./types.js";
@@ -1,59 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { resolveQQBotEffectivePolicies } from "./resolve-policy.js";
3
-
4
- describe("resolveQQBotEffectivePolicies", () => {
5
- describe("backwards-compatible inference", () => {
6
- it("defaults to open when no allowFrom is configured", () => {
7
- expect(resolveQQBotEffectivePolicies({})).toEqual({
8
- dmPolicy: "open",
9
- groupPolicy: "open",
10
- });
11
- });
12
-
13
- it("defaults to open when allowFrom only contains wildcard", () => {
14
- expect(resolveQQBotEffectivePolicies({ allowFrom: ["*"] })).toEqual({
15
- dmPolicy: "open",
16
- groupPolicy: "open",
17
- });
18
- });
19
-
20
- it("infers allowlist when allowFrom has a concrete entry", () => {
21
- expect(resolveQQBotEffectivePolicies({ allowFrom: ["USER1"] })).toEqual({
22
- dmPolicy: "allowlist",
23
- groupPolicy: "allowlist",
24
- });
25
- });
26
-
27
- it("infers group=allowlist when only groupAllowFrom is restricted", () => {
28
- expect(
29
- resolveQQBotEffectivePolicies({ allowFrom: ["*"], groupAllowFrom: ["USER1"] }),
30
- ).toEqual({
31
- dmPolicy: "open",
32
- groupPolicy: "allowlist",
33
- });
34
- });
35
- });
36
-
37
- describe("explicit policy precedence", () => {
38
- it("honours explicit dmPolicy over inference", () => {
39
- expect(
40
- resolveQQBotEffectivePolicies({ allowFrom: ["USER1"], dmPolicy: "open" }),
41
- ).toMatchObject({ dmPolicy: "open" });
42
- });
43
-
44
- it("honours explicit groupPolicy over inference", () => {
45
- expect(
46
- resolveQQBotEffectivePolicies({
47
- allowFrom: ["USER1"],
48
- groupPolicy: "disabled",
49
- }),
50
- ).toMatchObject({ groupPolicy: "disabled" });
51
- });
52
-
53
- it("allows dmPolicy=disabled to cut off DM entirely", () => {
54
- expect(resolveQQBotEffectivePolicies({ dmPolicy: "disabled" })).toMatchObject({
55
- dmPolicy: "disabled",
56
- });
57
- });
58
- });
59
- });
@@ -1,57 +0,0 @@
1
- /**
2
- * Effective-policy resolver.
3
- *
4
- * Maps a raw `QQBotAccountConfig` to the concrete `dmPolicy`/`groupPolicy`
5
- * values that the access engine consumes. Provides backwards-compatible
6
- * defaults for accounts that only have the legacy `allowFrom` field:
7
- *
8
- * - Empty `allowFrom` or containing `"*"` → `"open"` (the historical
9
- * behaviour before P0/P1 landed).
10
- * - Non-empty `allowFrom` without `"*"` → `"allowlist"` (what a
11
- * security-conscious operator almost certainly meant).
12
- *
13
- * An explicit `dmPolicy`/`groupPolicy` always wins over the inference.
14
- */
15
-
16
- import type { QQBotDmPolicy, QQBotGroupPolicy } from "./types.js";
17
-
18
- /** Subset of the account config fields this resolver actually reads. */
19
- export interface EffectivePolicyInput {
20
- allowFrom?: Array<string | number> | null;
21
- groupAllowFrom?: Array<string | number> | null;
22
- dmPolicy?: QQBotDmPolicy | null;
23
- groupPolicy?: QQBotGroupPolicy | null;
24
- }
25
-
26
- function hasRealRestriction(list: Array<string | number> | null | undefined): boolean {
27
- if (!list || list.length === 0) {
28
- return false;
29
- }
30
- // A list that only contains `"*"` is logically equivalent to open.
31
- return !list.every((entry) => String(entry).trim() === "*");
32
- }
33
-
34
- /**
35
- * Derive the effective dmPolicy and groupPolicy applied at runtime.
36
- *
37
- * Caller should pass the raw `QQBotAccountConfig`. The resolver does
38
- * not look at `groups[id]` overrides — per-group overrides are layered
39
- * on top elsewhere (see `inbound-pipeline` mention gating).
40
- */
41
- export function resolveQQBotEffectivePolicies(input: EffectivePolicyInput): {
42
- dmPolicy: QQBotDmPolicy;
43
- groupPolicy: QQBotGroupPolicy;
44
- } {
45
- const allowFromRestricted = hasRealRestriction(input.allowFrom);
46
- const groupAllowFromRestricted = hasRealRestriction(input.groupAllowFrom);
47
-
48
- const dmPolicy: QQBotDmPolicy = input.dmPolicy ?? (allowFromRestricted ? "allowlist" : "open");
49
-
50
- // groupPolicy defaults: if an explicit groupAllowFrom is provided and
51
- // restricts, enforce allowlist. Otherwise fall back to the same rule
52
- // as DM (so a single `allowFrom` entry locks down both DM and group).
53
- const groupPolicy: QQBotGroupPolicy =
54
- input.groupPolicy ?? (groupAllowFromRestricted || allowFromRestricted ? "allowlist" : "open");
55
-
56
- return { dmPolicy, groupPolicy };
57
- }
@@ -1,60 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- createQQBotSenderMatcher,
4
- normalizeQQBotAllowFrom,
5
- normalizeQQBotSenderId,
6
- } from "./sender-match.js";
7
-
8
- describe("normalizeQQBotSenderId", () => {
9
- it("uppercases and strips qqbot: prefix", () => {
10
- expect(normalizeQQBotSenderId("qqbot:abc123")).toBe("ABC123");
11
- expect(normalizeQQBotSenderId("QQBot:abc123")).toBe("ABC123");
12
- });
13
-
14
- it("trims whitespace", () => {
15
- expect(normalizeQQBotSenderId(" USER1 ")).toBe("USER1");
16
- });
17
-
18
- it("returns empty string for non-string input", () => {
19
- expect(normalizeQQBotSenderId(undefined as unknown as string)).toBe("");
20
- expect(normalizeQQBotSenderId(null as unknown as string)).toBe("");
21
- expect(normalizeQQBotSenderId({} as unknown as string)).toBe("");
22
- });
23
-
24
- it("accepts numeric input", () => {
25
- expect(normalizeQQBotSenderId(42)).toBe("42");
26
- });
27
- });
28
-
29
- describe("normalizeQQBotAllowFrom", () => {
30
- it("normalizes all entries and drops empty ones", () => {
31
- expect(normalizeQQBotAllowFrom(["qqbot:user1", "USER2", "", " "])).toEqual(["USER1", "USER2"]);
32
- });
33
-
34
- it("returns empty array for undefined/null", () => {
35
- expect(normalizeQQBotAllowFrom(undefined)).toEqual([]);
36
- expect(normalizeQQBotAllowFrom(null)).toEqual([]);
37
- });
38
- });
39
-
40
- describe("createQQBotSenderMatcher", () => {
41
- it("matches wildcard regardless of sender", () => {
42
- expect(createQQBotSenderMatcher("USER1")(["*"])).toBe(true);
43
- expect(createQQBotSenderMatcher("")(["*"])).toBe(true);
44
- });
45
-
46
- it("matches case-insensitive with qqbot: prefix", () => {
47
- const match = createQQBotSenderMatcher("qqbot:USER1");
48
- expect(match(["qqbot:user1"])).toBe(true);
49
- expect(match(["USER1"])).toBe(true);
50
- expect(match(["USER2"])).toBe(false);
51
- });
52
-
53
- it("returns false on empty allowlist", () => {
54
- expect(createQQBotSenderMatcher("USER1")([])).toBe(false);
55
- });
56
-
57
- it("returns false for empty sender against non-wildcard list", () => {
58
- expect(createQQBotSenderMatcher("")(["USER1"])).toBe(false);
59
- });
60
- });
@@ -1,55 +0,0 @@
1
- /**
2
- * QQBot sender normalization and allowlist matching.
3
- *
4
- * Keeps QQ-specific quirks (the `qqbot:` prefix, uppercase-insensitive
5
- * comparison) localized to this module so the policy engine itself can
6
- * stay channel-agnostic.
7
- */
8
-
9
- /** Normalize a single entry (openid): strip `qqbot:` prefix, uppercase, trim. */
10
- export function normalizeQQBotSenderId(raw: unknown): string {
11
- if (typeof raw !== "string" && typeof raw !== "number") {
12
- return "";
13
- }
14
- return String(raw)
15
- .trim()
16
- .replace(/^qqbot:/i, "")
17
- .toUpperCase();
18
- }
19
-
20
- /** Normalize an entire allowFrom list, dropping empty entries. */
21
- export function normalizeQQBotAllowFrom(list: Array<string | number> | undefined | null): string[] {
22
- if (!list || list.length === 0) {
23
- return [];
24
- }
25
- const out: string[] = [];
26
- for (const entry of list) {
27
- const normalized = normalizeQQBotSenderId(entry);
28
- if (normalized) {
29
- out.push(normalized);
30
- }
31
- }
32
- return out;
33
- }
34
-
35
- /**
36
- * Build a matcher closure suitable for passing to the policy engine's
37
- * `isSenderAllowed` callback. The caller supplies the sender once, and
38
- * the returned function can be invoked against different allowlists
39
- * (DM allowlist vs group allowlist) without repeating normalization.
40
- */
41
- export function createQQBotSenderMatcher(senderId: string): (allowFrom: string[]) => boolean {
42
- const normalizedSender = normalizeQQBotSenderId(senderId);
43
- return (allowFrom: string[]) => {
44
- if (allowFrom.length === 0) {
45
- return false;
46
- }
47
- if (allowFrom.includes("*")) {
48
- return true;
49
- }
50
- if (!normalizedSender) {
51
- return false;
52
- }
53
- return allowFrom.some((entry) => normalizeQQBotSenderId(entry) === normalizedSender);
54
- };
55
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * QQBot access-control primitive types.
3
- *
4
- * Mirrors the semantics of the framework-shared `DmPolicy` and
5
- * `DmGroupAccessDecision` types while staying zero-dependency so the
6
- * engine layer remains portable across the built-in and standalone
7
- * plugin builds.
8
- *
9
- * The reason codes here intentionally match
10
- * `src/security/dm-policy-shared.ts::DM_GROUP_ACCESS_REASON` so metric
11
- * dashboards can treat QQBot decisions identically to WhatsApp /
12
- * Telegram / Discord decisions.
13
- */
14
-
15
- /** DM-level policy selecting between open / allowlist / disabled gating. */
16
- export type QQBotDmPolicy = "open" | "allowlist" | "disabled";
17
-
18
- /** Group-level policy selecting between open / allowlist / disabled gating. */
19
- export type QQBotGroupPolicy = "open" | "allowlist" | "disabled";
20
-
21
- /** High-level outcome returned by the access gate. */
22
- export type QQBotAccessDecision = "allow" | "block";
23
-
24
- /** Structured reason codes used in logs and metrics. */
25
- export const QQBOT_ACCESS_REASON = {
26
- DM_POLICY_OPEN: "dm_policy_open",
27
- DM_POLICY_DISABLED: "dm_policy_disabled",
28
- DM_POLICY_ALLOWLISTED: "dm_policy_allowlisted",
29
- DM_POLICY_NOT_ALLOWLISTED: "dm_policy_not_allowlisted",
30
- DM_POLICY_EMPTY_ALLOWLIST: "dm_policy_empty_allowlist",
31
- GROUP_POLICY_ALLOWED: "group_policy_allowed",
32
- GROUP_POLICY_DISABLED: "group_policy_disabled",
33
- GROUP_POLICY_EMPTY_ALLOWLIST: "group_policy_empty_allowlist",
34
- GROUP_POLICY_NOT_ALLOWLISTED: "group_policy_not_allowlisted",
35
- BOT_SELF_ECHO: "bot_self_echo",
36
- } as const;
37
-
38
- export type QQBotAccessReasonCode = (typeof QQBOT_ACCESS_REASON)[keyof typeof QQBOT_ACCESS_REASON];
39
-
40
- /** Result of the access gate evaluation. */
41
- export interface QQBotAccessResult {
42
- decision: QQBotAccessDecision;
43
- reasonCode: QQBotAccessReasonCode;
44
- /** Human-readable reason suitable for logging. */
45
- reason: string;
46
- /** The allowFrom list that was actually compared against. */
47
- effectiveAllowFrom: string[];
48
- /** The groupAllowFrom list that was actually compared against. */
49
- effectiveGroupAllowFrom: string[];
50
- /** The dm/group policies that were used (after defaults were applied). */
51
- dmPolicy: QQBotDmPolicy;
52
- groupPolicy: QQBotGroupPolicy;
53
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Audio port — abstracts inbound + outbound audio conversion operations.
3
- *
4
- * The engine defines this interface; the bridge layer provides an
5
- * implementation backed by `engine/utils/audio.js` functions.
6
- */
7
-
8
- /** Inbound audio conversion (SILK→WAV, voice detection, duration formatting). */
9
- export interface AudioConvertPort {
10
- convertSilkToWav(
11
- silkPath: string,
12
- outputDir: string,
13
- ): Promise<{ wavPath: string; duration: number } | null>;
14
- isVoiceAttachment(att: { content_type: string; filename?: string }): boolean;
15
- formatDuration(seconds: number): string;
16
- }
17
-
18
- /** Outbound audio conversion (WAV→SILK, audio detection, transcoding). */
19
- export interface OutboundAudioPort {
20
- audioFileToSilkBase64(
21
- audioPath: string,
22
- directUploadFormats?: string[],
23
- ): Promise<string | undefined>;
24
- isAudioFile(pathOrUrl: string, mimeType?: string): boolean;
25
- shouldTranscodeVoice(filePath: string): boolean;
26
- waitForFile(filePath: string, maxWaitMs?: number): Promise<number>;
27
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * Commands port — abstracts slash-command dependencies injected by the
3
- * bridge layer (version resolvers, approve runtime getter).
4
- *
5
- * Eliminates global `register*` singletons in `slash-commands-impl.ts`.
6
- */
7
-
8
- import type { PluginRuntime } from "openclaw/plugin-sdk/core";
9
-
10
- /** Runtime getter shape for the `/bot-approve` command. */
11
- export type ApproveRuntimeGetter = () => {
12
- config: Pick<PluginRuntime["config"], "current" | "replaceConfigFile">;
13
- };
14
-
15
- export interface CommandsPort {
16
- /** Resolve the framework runtime version string. */
17
- resolveVersion: () => string;
18
- /** Plugin version string (e.g. "1.2.3"). */
19
- pluginVersion: string;
20
- /** Runtime getter for `/bot-approve` config management. */
21
- approveRuntimeGetter?: ApproveRuntimeGetter;
22
- }
@@ -1,52 +0,0 @@
1
- /**
2
- * History port — abstracts the group history cache operations.
3
- *
4
- * The engine defines this interface; the bridge layer provides an
5
- * implementation backed by SDK `reply-history` functions. The engine's
6
- * built-in implementation in `group/history.ts` is used as the default
7
- * when no adapter is injected (standalone build).
8
- */
9
-
10
- /** Minimal history entry shape expected by the port. */
11
- export interface HistoryEntryLike {
12
- sender: string;
13
- body: string;
14
- timestamp?: number;
15
- messageId?: string;
16
- }
17
-
18
- export interface HistoryPort {
19
- /**
20
- * Record a non-@ message into the pending history buffer.
21
- * No-op when `limit <= 0` or `entry` is missing.
22
- */
23
- recordPendingHistoryEntry<T extends HistoryEntryLike>(params: {
24
- historyMap: Map<string, T[]>;
25
- historyKey: string;
26
- entry?: T | null;
27
- limit: number;
28
- }): T[];
29
-
30
- /**
31
- * Build the full user-message string prefixed with buffered history.
32
- * Returns `currentMessage` unchanged when no history exists.
33
- */
34
- buildPendingHistoryContext(params: {
35
- historyMap: Map<string, HistoryEntryLike[]>;
36
- historyKey: string;
37
- limit: number;
38
- currentMessage: string;
39
- formatEntry: (entry: HistoryEntryLike) => string;
40
- lineBreak?: string;
41
- }): string;
42
-
43
- /**
44
- * Clear a group's pending history buffer.
45
- * No-op when `limit <= 0`.
46
- */
47
- clearPendingHistory(params: {
48
- historyMap: Map<string, HistoryEntryLike[]>;
49
- historyKey: string;
50
- limit: number;
51
- }): void;
52
- }