@openclaw/qqbot 2026.5.2-beta.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,76 +0,0 @@
1
- /**
2
- * QQBot credential management (pure logic layer).
3
- * QQBot 凭证管理(纯逻辑层)。
4
- *
5
- * Credential clearing and field-level cleanup for logout and setup
6
- * flows. All functions operate on plain objects (Record<string, unknown>)
7
- * and stay framework-agnostic.
8
- */
9
-
10
- import { asOptionalObjectRecord as asRecord } from "../utils/string-normalize.js";
11
- import { DEFAULT_ACCOUNT_ID } from "./resolve.js";
12
-
13
- // ---- Logout: clear all credential fields for an account ----
14
-
15
- interface ClearCredentialsResult {
16
- nextCfg: Record<string, unknown>;
17
- cleared: boolean;
18
- changed: boolean;
19
- }
20
-
21
- /**
22
- * Remove clientSecret / clientSecretFile from a QQBot account config.
23
- *
24
- * Returns a shallow-cloned config with credentials removed, plus flags
25
- * indicating whether anything actually changed.
26
- */
27
- export function clearAccountCredentials(
28
- cfg: Record<string, unknown>,
29
- accountId: string,
30
- ): ClearCredentialsResult {
31
- const nextCfg = { ...cfg };
32
- const channels = asRecord(cfg.channels);
33
- const nextQQBot = channels?.qqbot ? { ...asRecord(channels.qqbot) } : undefined;
34
- let cleared = false;
35
- let changed = false;
36
-
37
- if (nextQQBot) {
38
- const qqbot = nextQQBot as Record<string, unknown>;
39
- if (accountId === DEFAULT_ACCOUNT_ID) {
40
- if (qqbot.clientSecret) {
41
- delete qqbot.clientSecret;
42
- cleared = true;
43
- changed = true;
44
- }
45
- if (qqbot.clientSecretFile) {
46
- delete qqbot.clientSecretFile;
47
- cleared = true;
48
- changed = true;
49
- }
50
- }
51
- const accounts = qqbot.accounts as Record<string, Record<string, unknown>> | undefined;
52
- if (accounts && accountId in accounts) {
53
- const entry = accounts[accountId] as Record<string, unknown> | undefined;
54
- if (entry && "clientSecret" in entry) {
55
- delete entry.clientSecret;
56
- cleared = true;
57
- changed = true;
58
- }
59
- if (entry && "clientSecretFile" in entry) {
60
- delete entry.clientSecretFile;
61
- cleared = true;
62
- changed = true;
63
- }
64
- if (entry && Object.keys(entry).length === 0) {
65
- delete accounts[accountId];
66
- changed = true;
67
- }
68
- }
69
- }
70
-
71
- if (changed && nextQQBot) {
72
- nextCfg.channels = { ...channels, qqbot: nextQQBot };
73
- }
74
-
75
- return { nextCfg, cleared, changed };
76
- }
@@ -1,234 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- DEFAULT_GROUP_HISTORY_LIMIT,
4
- resolveGroupConfig,
5
- resolveGroupName,
6
- resolveGroupPrompt,
7
- resolveGroupSettings,
8
- resolveGroupToolPolicy,
9
- resolveHistoryLimit,
10
- resolveIgnoreOtherMentions,
11
- resolveMentionPatterns,
12
- resolveRequireMention,
13
- } from "./group.js";
14
-
15
- describe("engine/config/group", () => {
16
- describe("resolveGroupConfig precedence", () => {
17
- it("returns defaults when no config exists", () => {
18
- const cfg = resolveGroupConfig({}, "G1");
19
- expect(cfg).toMatchObject({
20
- requireMention: true,
21
- ignoreOtherMentions: false,
22
- toolPolicy: "restricted",
23
- name: "",
24
- historyLimit: DEFAULT_GROUP_HISTORY_LIMIT,
25
- });
26
- expect(cfg.prompt).toBeUndefined();
27
- });
28
-
29
- it("falls back to wildcard when specific is missing", () => {
30
- const cfg = {
31
- channels: {
32
- qqbot: {
33
- appId: "1",
34
- groups: {
35
- "*": {
36
- requireMention: false,
37
- toolPolicy: "full",
38
- historyLimit: 20,
39
- name: "wild",
40
- },
41
- },
42
- },
43
- },
44
- };
45
- const resolved = resolveGroupConfig(cfg, "G1");
46
- expect(resolved.requireMention).toBe(false);
47
- expect(resolved.toolPolicy).toBe("full");
48
- expect(resolved.historyLimit).toBe(20);
49
- expect(resolved.name).toBe("wild");
50
- });
51
-
52
- it("specific overrides wildcard and defaults", () => {
53
- const cfg = {
54
- channels: {
55
- qqbot: {
56
- appId: "1",
57
- groups: {
58
- "*": { requireMention: true, toolPolicy: "restricted", historyLimit: 20 },
59
- GROUPA: { requireMention: false, toolPolicy: "none", historyLimit: 5, name: "A" },
60
- },
61
- },
62
- },
63
- };
64
- const resolved = resolveGroupConfig(cfg, "GROUPA");
65
- expect(resolved.requireMention).toBe(false);
66
- expect(resolved.toolPolicy).toBe("none");
67
- expect(resolved.historyLimit).toBe(5);
68
- expect(resolved.name).toBe("A");
69
- });
70
-
71
- it("historyLimit is clamped to >= 0 and floored", () => {
72
- const cfg = {
73
- channels: {
74
- qqbot: { appId: "1", groups: { "*": { historyLimit: -3.7 } } },
75
- },
76
- };
77
- expect(resolveHistoryLimit(cfg, "G")).toBe(0);
78
- });
79
-
80
- it("non-finite historyLimit falls back to default", () => {
81
- const cfg = {
82
- channels: {
83
- qqbot: { appId: "1", groups: { "*": { historyLimit: "not a number" } } },
84
- },
85
- };
86
- expect(resolveHistoryLimit(cfg, "G")).toBe(DEFAULT_GROUP_HISTORY_LIMIT);
87
- });
88
-
89
- it("invalid toolPolicy values are ignored", () => {
90
- const cfg = {
91
- channels: {
92
- qqbot: { appId: "1", groups: { "*": { toolPolicy: "invalid" } } },
93
- },
94
- };
95
- expect(resolveGroupToolPolicy(cfg, "G")).toBe("restricted");
96
- });
97
- });
98
-
99
- describe("named accounts", () => {
100
- it("reads groups from the named-account scope", () => {
101
- const cfg = {
102
- channels: {
103
- qqbot: {
104
- accounts: {
105
- bot2: {
106
- appId: "9",
107
- groups: { "*": { requireMention: false, historyLimit: 7 } },
108
- },
109
- },
110
- },
111
- },
112
- };
113
- expect(resolveRequireMention(cfg, "G", "bot2")).toBe(false);
114
- expect(resolveHistoryLimit(cfg, "G", "bot2")).toBe(7);
115
- });
116
- });
117
-
118
- describe("resolveGroupName", () => {
119
- it("uses the first 8 chars of openid when name is unset", () => {
120
- expect(resolveGroupName({}, "ABCDEFGH1234")).toBe("ABCDEFGH");
121
- });
122
-
123
- it("prefers the configured name", () => {
124
- const cfg = {
125
- channels: { qqbot: { appId: "1", groups: { ABCDEFGH1234: { name: "Foo" } } } },
126
- };
127
- expect(resolveGroupName(cfg, "ABCDEFGH1234")).toBe("Foo");
128
- });
129
- });
130
-
131
- describe("resolveGroupPrompt", () => {
132
- it("returns the default prompt when nothing configured", () => {
133
- expect(resolveGroupPrompt({}, "G")).toContain("bot");
134
- });
135
-
136
- it("prefers specific over wildcard", () => {
137
- const cfg = {
138
- channels: {
139
- qqbot: {
140
- appId: "1",
141
- groups: { "*": { prompt: "WILD" }, G1: { prompt: "SPEC" } },
142
- },
143
- },
144
- };
145
- expect(resolveGroupPrompt(cfg, "G1")).toBe("SPEC");
146
- expect(resolveGroupPrompt(cfg, "G2")).toBe("WILD");
147
- });
148
- });
149
-
150
- describe("resolveIgnoreOtherMentions", () => {
151
- it("defaults to false", () => {
152
- expect(resolveIgnoreOtherMentions({}, "G")).toBe(false);
153
- });
154
-
155
- it("honours wildcard override", () => {
156
- const cfg = {
157
- channels: { qqbot: { appId: "1", groups: { "*": { ignoreOtherMentions: true } } } },
158
- };
159
- expect(resolveIgnoreOtherMentions(cfg, "G")).toBe(true);
160
- });
161
- });
162
-
163
- describe("resolveMentionPatterns", () => {
164
- it("returns [] when nothing configured", () => {
165
- expect(resolveMentionPatterns({})).toEqual([]);
166
- });
167
-
168
- it("reads global patterns", () => {
169
- const cfg = { messages: { groupChat: { mentionPatterns: ["/^hey/"] } } };
170
- expect(resolveMentionPatterns(cfg)).toEqual(["/^hey/"]);
171
- });
172
-
173
- it("agent-level overrides global", () => {
174
- const cfg = {
175
- messages: { groupChat: { mentionPatterns: ["g"] } },
176
- agents: {
177
- list: [{ id: "main", groupChat: { mentionPatterns: ["a", "b"] } }],
178
- },
179
- };
180
- expect(resolveMentionPatterns(cfg, "main")).toEqual(["a", "b"]);
181
- expect(resolveMentionPatterns(cfg, "OTHER")).toEqual(["g"]);
182
- });
183
-
184
- it("filters non-string entries", () => {
185
- const cfg = { messages: { groupChat: { mentionPatterns: ["ok", 42, null] } } };
186
- expect(resolveMentionPatterns(cfg)).toEqual(["ok"]);
187
- });
188
- });
189
-
190
- describe("resolveGroupSettings (aggregate)", () => {
191
- it("returns merged config + name + mentionPatterns in one call", () => {
192
- const cfg = {
193
- channels: {
194
- qqbot: {
195
- appId: "1",
196
- groups: {
197
- G1: { requireMention: false, name: "Dev" },
198
- "*": { historyLimit: 10 },
199
- },
200
- },
201
- },
202
- messages: { groupChat: { mentionPatterns: ["@bot"] } },
203
- };
204
- const settings = resolveGroupSettings({ cfg, groupOpenid: "G1" });
205
- expect(settings.config.requireMention).toBe(false);
206
- expect(settings.config.historyLimit).toBe(10);
207
- expect(settings.name).toBe("Dev");
208
- expect(settings.mentionPatterns).toEqual(["@bot"]);
209
- });
210
-
211
- it("falls back to the first 8 chars of the openid for name", () => {
212
- const settings = resolveGroupSettings({
213
- cfg: {},
214
- groupOpenid: "ABCDEFGHIJKLMNOP",
215
- });
216
- expect(settings.name).toBe("ABCDEFGH");
217
- });
218
-
219
- it("applies agent-level mentionPatterns over global", () => {
220
- const cfg = {
221
- agents: {
222
- list: [{ id: "custom", groupChat: { mentionPatterns: ["@agent"] } }],
223
- },
224
- messages: { groupChat: { mentionPatterns: ["@global"] } },
225
- };
226
- const settings = resolveGroupSettings({
227
- cfg,
228
- groupOpenid: "G1",
229
- agentId: "custom",
230
- });
231
- expect(settings.mentionPatterns).toEqual(["@agent"]);
232
- });
233
- });
234
- });
@@ -1,299 +0,0 @@
1
- /**
2
- * QQBot group configuration resolution (pure logic).
3
- * QQBot 群配置解析(纯逻辑层)。
4
- *
5
- * Resolves per-group settings that the inbound pipeline needs to decide how
6
- * to gate and contextualize group messages. Reads from a raw config object
7
- * produced by the framework's config loader, with a `specific > wildcard
8
- * ("*") > default` precedence chain.
9
- *
10
- * All functions are **pure** (no I/O, no external state) — making them
11
- * portable to the standalone plugin build and trivially unit-testable.
12
- */
13
-
14
- import { asOptionalObjectRecord as asRecord } from "../utils/string-normalize.js";
15
- import { resolveAccountBase } from "./resolve.js";
16
-
17
- // ============ Types ============
18
-
19
- /**
20
- * Tool policy — which tool palette an agent should use in a given group.
21
- *
22
- * - `full`: default allow everything (no engine-side restriction).
23
- * - `restricted`: engine returns an empty allowlist so the framework falls
24
- * back to its built-in restricted palette.
25
- * - `none`: deny all tools.
26
- */
27
- type GroupToolPolicy = "full" | "restricted" | "none";
28
-
29
- /** Per-group configuration — everything that may be overridden per group. */
30
- interface GroupConfig {
31
- /** Whether the bot requires @mention to respond. Defaults to true. */
32
- requireMention: boolean;
33
- /**
34
- * When true, group messages that @other users (but not the bot) are
35
- * dropped silently without reaching the AI pipeline.
36
- */
37
- ignoreOtherMentions: boolean;
38
- /** Tool palette policy. Defaults to "restricted". */
39
- toolPolicy: GroupToolPolicy;
40
- /** Human-readable group name. Empty string if not configured. */
41
- name: string;
42
- /** Per-group behaviour prompt appended to the system prompt. */
43
- prompt?: string;
44
- /**
45
- * Number of non-@ history messages buffered per group. Clamped to 0 when
46
- * disabled. The default matches the standalone build's `50`.
47
- */
48
- historyLimit: number;
49
- }
50
-
51
- // ============ Defaults ============
52
-
53
- /** Default history limit — matches the standalone build. */
54
- export const DEFAULT_GROUP_HISTORY_LIMIT = 50;
55
-
56
- /** Default group behaviour prompt. Exported so the gating stage can use
57
- * the same fallback when no per-group `prompt` is configured. */
58
- export const DEFAULT_GROUP_PROMPT =
59
- "If the sender is a bot, respond only when they explicitly @mention you to ask a question or request assistance with a specific task; keep your replies concise and clear, avoiding the urge to race other bots to answer or engage in lengthy, unproductive exchanges. In group chats, prioritize responding to messages from human users; bots should maintain a collaborative rather than competitive dynamic to ensure the conversation remains orderly and does not result in message flooding.";
60
-
61
- const DEFAULT_GROUP_CONFIG: Readonly<Omit<GroupConfig, "prompt">> = {
62
- requireMention: true,
63
- ignoreOtherMentions: false,
64
- toolPolicy: "restricted",
65
- name: "",
66
- historyLimit: DEFAULT_GROUP_HISTORY_LIMIT,
67
- };
68
-
69
- // ============ Helpers ============
70
-
71
- /** Read a named account's raw `groups` map from an OpenClawConfig. */
72
- function readGroupsMap(
73
- cfg: Record<string, unknown>,
74
- accountId?: string | null,
75
- ): Record<string, Record<string, unknown>> {
76
- const account = resolveAccountBase(cfg, accountId);
77
- const groups = asRecord(account.config.groups);
78
- if (!groups) {
79
- return {};
80
- }
81
- // Only keep sub-objects; skip scalars produced by user mistakes.
82
- const normalized: Record<string, Record<string, unknown>> = {};
83
- for (const [key, value] of Object.entries(groups)) {
84
- const sub = asRecord(value);
85
- if (sub) {
86
- normalized[key] = sub;
87
- }
88
- }
89
- return normalized;
90
- }
91
-
92
- function readBoolean(obj: Record<string, unknown>, key: string): boolean | undefined {
93
- const v = obj[key];
94
- return typeof v === "boolean" ? v : undefined;
95
- }
96
-
97
- function readString(obj: Record<string, unknown>, key: string): string | undefined {
98
- const v = obj[key];
99
- return typeof v === "string" && v.length > 0 ? v : undefined;
100
- }
101
-
102
- function readToolPolicy(obj: Record<string, unknown>, key: string): GroupToolPolicy | undefined {
103
- const v = obj[key];
104
- return v === "full" || v === "restricted" || v === "none" ? v : undefined;
105
- }
106
-
107
- function readHistoryLimit(obj: Record<string, unknown>, key: string): number | undefined {
108
- const v = obj[key];
109
- if (typeof v !== "number" || !Number.isFinite(v)) {
110
- return undefined;
111
- }
112
- return Math.max(0, Math.floor(v));
113
- }
114
-
115
- // ============ Public API ============
116
-
117
- /**
118
- * Resolve per-group configuration with `specific > "*" > default` precedence.
119
- *
120
- * When `groupOpenid` is not provided, only the wildcard/default values are
121
- * returned. This lets callers query the "default" behaviour for new groups.
122
- */
123
- export function resolveGroupConfig(
124
- cfg: Record<string, unknown>,
125
- groupOpenid?: string | null,
126
- accountId?: string | null,
127
- ): GroupConfig {
128
- const groups = readGroupsMap(cfg, accountId);
129
- const wildcard = groups["*"] ?? {};
130
- const specific = groupOpenid ? (groups[groupOpenid] ?? {}) : {};
131
-
132
- return {
133
- requireMention:
134
- readBoolean(specific, "requireMention") ??
135
- readBoolean(wildcard, "requireMention") ??
136
- DEFAULT_GROUP_CONFIG.requireMention,
137
- ignoreOtherMentions:
138
- readBoolean(specific, "ignoreOtherMentions") ??
139
- readBoolean(wildcard, "ignoreOtherMentions") ??
140
- DEFAULT_GROUP_CONFIG.ignoreOtherMentions,
141
- toolPolicy:
142
- readToolPolicy(specific, "toolPolicy") ??
143
- readToolPolicy(wildcard, "toolPolicy") ??
144
- DEFAULT_GROUP_CONFIG.toolPolicy,
145
- name: readString(specific, "name") ?? readString(wildcard, "name") ?? DEFAULT_GROUP_CONFIG.name,
146
- prompt: readString(specific, "prompt") ?? readString(wildcard, "prompt"),
147
- historyLimit:
148
- readHistoryLimit(specific, "historyLimit") ??
149
- readHistoryLimit(wildcard, "historyLimit") ??
150
- DEFAULT_GROUP_CONFIG.historyLimit,
151
- };
152
- }
153
-
154
- /** Resolve the effective `historyLimit` (>= 0) for a given group. */
155
- export function resolveHistoryLimit(
156
- cfg: Record<string, unknown>,
157
- groupOpenid?: string | null,
158
- accountId?: string | null,
159
- ): number {
160
- return resolveGroupConfig(cfg, groupOpenid, accountId).historyLimit;
161
- }
162
-
163
- /** Resolve `requireMention` for a given group. */
164
- export function resolveRequireMention(
165
- cfg: Record<string, unknown>,
166
- groupOpenid?: string | null,
167
- accountId?: string | null,
168
- ): boolean {
169
- return resolveGroupConfig(cfg, groupOpenid, accountId).requireMention;
170
- }
171
-
172
- /** Resolve `ignoreOtherMentions` for a given group. */
173
- export function resolveIgnoreOtherMentions(
174
- cfg: Record<string, unknown>,
175
- groupOpenid?: string | null,
176
- accountId?: string | null,
177
- ): boolean {
178
- return resolveGroupConfig(cfg, groupOpenid, accountId).ignoreOtherMentions;
179
- }
180
-
181
- /** Resolve tool policy for a given group. */
182
- export function resolveGroupToolPolicy(
183
- cfg: Record<string, unknown>,
184
- groupOpenid?: string | null,
185
- accountId?: string | null,
186
- ): GroupToolPolicy {
187
- return resolveGroupConfig(cfg, groupOpenid, accountId).toolPolicy;
188
- }
189
-
190
- /**
191
- * Resolve the behaviour prompt (PE) for a group. Falls back to the built-in
192
- * default when neither specific nor wildcard configuration provides one.
193
- */
194
- export function resolveGroupPrompt(
195
- cfg: Record<string, unknown>,
196
- groupOpenid?: string | null,
197
- accountId?: string | null,
198
- ): string {
199
- return resolveGroupConfig(cfg, groupOpenid, accountId).prompt ?? DEFAULT_GROUP_PROMPT;
200
- }
201
-
202
- /**
203
- * Resolve the display name for a group.
204
- *
205
- * When no name is configured, the first 8 characters of the openid are used
206
- * as a short identifier so log lines stay compact.
207
- */
208
- export function resolveGroupName(
209
- cfg: Record<string, unknown>,
210
- groupOpenid: string,
211
- accountId?: string | null,
212
- ): string {
213
- const name = resolveGroupConfig(cfg, groupOpenid, accountId).name;
214
- return name || groupOpenid.slice(0, 8);
215
- }
216
-
217
- // ============ GroupSettings (aggregate) ============
218
-
219
- /**
220
- * Per-inbound aggregate of everything the pipeline needs about a group.
221
- *
222
- * Built once at the top of the group-gate stage so downstream consumers
223
- * don't repeatedly re-parse the same `cfg` tree. Superset of
224
- * {@link GroupConfig}: also includes the effective `mentionPatterns`
225
- * (which depend on `agentId`, not on the group itself) and a
226
- * pre-computed display name for logging.
227
- */
228
- interface GroupSettings {
229
- /** Merged group config (specific > wildcard > defaults). */
230
- config: GroupConfig;
231
- /** Display name — `config.name` or the first 8 chars of the openid. */
232
- name: string;
233
- /** Raw mentionPatterns (agent > global > []). */
234
- mentionPatterns: string[];
235
- }
236
-
237
- /**
238
- * Resolve all per-inbound group-related settings in one pass.
239
- *
240
- * Prefer this over calling `resolveHistoryLimit` / `resolveRequireMention`
241
- * / etc. individually in hot paths — each of those currently re-walks
242
- * the config tree on its own.
243
- */
244
- export function resolveGroupSettings(params: {
245
- cfg: Record<string, unknown>;
246
- groupOpenid: string;
247
- accountId?: string | null;
248
- agentId?: string | null;
249
- }): GroupSettings {
250
- const config = resolveGroupConfig(params.cfg, params.groupOpenid, params.accountId);
251
- const name = config.name || params.groupOpenid.slice(0, 8);
252
- const mentionPatterns = resolveMentionPatterns(params.cfg, params.agentId);
253
- return { config, name, mentionPatterns };
254
- }
255
-
256
- interface AgentEntry {
257
- id?: unknown;
258
- groupChat?: { mentionPatterns?: unknown };
259
- }
260
-
261
- /**
262
- * Resolve mentionPatterns with `agent > global > []` precedence.
263
- *
264
- * Mirrors the framework's `messages.groupChat.mentionPatterns` / per-agent
265
- * `agents.list[].groupChat.mentionPatterns` chain.
266
- */
267
- export function resolveMentionPatterns(
268
- cfg: Record<string, unknown>,
269
- agentId?: string | null,
270
- ): string[] {
271
- // ---- 1. Agent-level ----
272
- if (agentId) {
273
- const agents = asRecord(cfg.agents);
274
- const list = Array.isArray(agents?.list) ? (agents?.list as AgentEntry[]) : [];
275
- const entry = list.find(
276
- (a) => typeof a.id === "string" && a.id.trim().toLowerCase() === agentId.trim().toLowerCase(),
277
- );
278
- const agentGroupChat = entry?.groupChat;
279
- if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
280
- const patterns = agentGroupChat.mentionPatterns;
281
- return Array.isArray(patterns)
282
- ? patterns.filter((p): p is string => typeof p === "string")
283
- : [];
284
- }
285
- }
286
-
287
- // ---- 2. Global level ----
288
- const messages = asRecord(cfg.messages);
289
- const globalGroupChat = asRecord(messages?.groupChat);
290
- if (globalGroupChat && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
291
- const patterns = globalGroupChat.mentionPatterns;
292
- return Array.isArray(patterns)
293
- ? patterns.filter((p): p is string => typeof p === "string")
294
- : [];
295
- }
296
-
297
- // ---- 3. Default ----
298
- return [];
299
- }