@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,152 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- DEFAULT_ACCOUNT_ID,
4
- listAccountIds,
5
- resolveDefaultAccountId,
6
- resolveAccountBase,
7
- } from "./resolve.js";
8
-
9
- describe("engine/config/resolve", () => {
10
- it("returns empty list when no accounts configured", () => {
11
- expect(listAccountIds({})).toEqual([]);
12
- });
13
-
14
- it("returns default when top-level appId is set", () => {
15
- const cfg = {
16
- channels: {
17
- qqbot: { appId: "123456" },
18
- },
19
- };
20
- expect(listAccountIds(cfg)).toEqual([DEFAULT_ACCOUNT_ID]);
21
- });
22
-
23
- it("lists named accounts", () => {
24
- const cfg = {
25
- channels: {
26
- qqbot: {
27
- accounts: {
28
- bot2: { appId: "654321" },
29
- bot3: { appId: "111222" },
30
- },
31
- },
32
- },
33
- };
34
- const ids = listAccountIds(cfg);
35
- expect(ids).toContain("bot2");
36
- expect(ids).toContain("bot3");
37
- });
38
-
39
- it("resolves default account id to 'default' when top-level appId exists", () => {
40
- const cfg = {
41
- channels: {
42
- qqbot: { appId: "123456" },
43
- },
44
- };
45
- expect(resolveDefaultAccountId(cfg)).toBe(DEFAULT_ACCOUNT_ID);
46
- });
47
-
48
- it("honors configured defaultAccount", () => {
49
- const cfg = {
50
- channels: {
51
- qqbot: {
52
- defaultAccount: "bot2",
53
- accounts: {
54
- bot2: { appId: "654321" },
55
- },
56
- },
57
- },
58
- };
59
- expect(resolveDefaultAccountId(cfg)).toBe("bot2");
60
- });
61
-
62
- it("falls back to first named account when no default configured", () => {
63
- const cfg = {
64
- channels: {
65
- qqbot: {
66
- accounts: {
67
- mybot: { appId: "999999" },
68
- },
69
- },
70
- },
71
- };
72
- expect(resolveDefaultAccountId(cfg)).toBe("mybot");
73
- });
74
-
75
- it("resolves base account info for default account", () => {
76
- const cfg = {
77
- channels: {
78
- qqbot: {
79
- appId: "123456",
80
- name: "Test Bot",
81
- systemPrompt: "You are helpful.",
82
- markdownSupport: true,
83
- },
84
- },
85
- };
86
- const base = resolveAccountBase(cfg, DEFAULT_ACCOUNT_ID);
87
- expect(base.accountId).toBe(DEFAULT_ACCOUNT_ID);
88
- expect(base.appId).toBe("123456");
89
- expect(base.name).toBe("Test Bot");
90
- expect(base.systemPrompt).toBe("You are helpful.");
91
- expect(base.markdownSupport).toBe(true);
92
- expect(base.enabled).toBe(true);
93
- });
94
-
95
- it("resolves base account info for named account", () => {
96
- const cfg = {
97
- channels: {
98
- qqbot: {
99
- accounts: {
100
- bot2: {
101
- appId: "654321",
102
- name: "Bot Two",
103
- enabled: false,
104
- },
105
- },
106
- },
107
- },
108
- };
109
- const base = resolveAccountBase(cfg, "bot2");
110
- expect(base.accountId).toBe("bot2");
111
- expect(base.appId).toBe("654321");
112
- expect(base.name).toBe("Bot Two");
113
- expect(base.enabled).toBe(false);
114
- });
115
-
116
- it("uses configured defaultAccount when accountId is omitted", () => {
117
- const cfg = {
118
- channels: {
119
- qqbot: {
120
- defaultAccount: "bot2",
121
- accounts: {
122
- bot2: { appId: "654321" },
123
- },
124
- },
125
- },
126
- };
127
- const base = resolveAccountBase(cfg);
128
- expect(base.accountId).toBe("bot2");
129
- expect(base.appId).toBe("654321");
130
- });
131
-
132
- it("preserves audioFormatPolicy on the config object", () => {
133
- const cfg = {
134
- channels: {
135
- qqbot: {
136
- appId: "123456",
137
- audioFormatPolicy: {
138
- sttDirectFormats: [".wav"],
139
- uploadDirectFormats: [".mp3"],
140
- transcodeEnabled: false,
141
- },
142
- },
143
- },
144
- };
145
- const base = resolveAccountBase(cfg, DEFAULT_ACCOUNT_ID);
146
- expect(base.config.audioFormatPolicy).toEqual({
147
- sttDirectFormats: [".wav"],
148
- uploadDirectFormats: [".mp3"],
149
- transcodeEnabled: false,
150
- });
151
- });
152
- });
@@ -1,283 +0,0 @@
1
- /**
2
- * QQBot config resolution (pure logic layer).
3
- * QQBot 配置解析(纯逻辑层)。
4
- *
5
- * Resolves account IDs, default account selection, and base account
6
- * info from raw config objects. Secret/credential resolution is
7
- * intentionally left to the outer layer (src/bridge/config.ts) so that
8
- * this module stays framework-agnostic and self-contained.
9
- */
10
-
11
- import { getPlatformAdapter } from "../adapter/index.js";
12
- import {
13
- asOptionalObjectRecord as asRecord,
14
- normalizeOptionalLowercaseString,
15
- normalizeStringifiedOptionalString,
16
- readStringField as readString,
17
- } from "../utils/string-normalize.js";
18
-
19
- /**
20
- * Default account ID, used for the unnamed top-level account.
21
- * 默认账号 ID,用于顶层配置中未命名的账号。
22
- */
23
- export const DEFAULT_ACCOUNT_ID = "default";
24
-
25
- /**
26
- * Internal shape of the channels.qqbot config section.
27
- * channels.qqbot 配置节的内部结构。
28
- */
29
- interface QQBotChannelConfig {
30
- appId?: unknown;
31
- clientSecret?: unknown;
32
- clientSecretFile?: string;
33
- accounts?: Record<string, Record<string, unknown>>;
34
- defaultAccount?: unknown;
35
- [key: string]: unknown;
36
- }
37
-
38
- /**
39
- * Base account resolution result (without credentials).
40
- * 账号基础解析结果(不含凭证信息)。
41
- *
42
- * The outer config.ts layer extends this with clientSecret / secretSource.
43
- */
44
- interface ResolvedAccountBase {
45
- accountId: string;
46
- name?: string;
47
- enabled: boolean;
48
- appId: string;
49
- systemPrompt?: string;
50
- markdownSupport: boolean;
51
- config: Record<string, unknown>;
52
- }
53
-
54
- function normalizeAppId(raw: unknown): string {
55
- if (typeof raw === "string") {
56
- return raw.trim();
57
- }
58
- if (typeof raw === "number") {
59
- return String(raw);
60
- }
61
- return "";
62
- }
63
-
64
- function normalizeAccountConfig(
65
- account: Record<string, unknown> | undefined,
66
- ): Record<string, unknown> {
67
- if (!account) {
68
- return {};
69
- }
70
- const audioPolicy = asRecord(account.audioFormatPolicy);
71
- return {
72
- ...account,
73
- ...(audioPolicy ? { audioFormatPolicy: { ...audioPolicy } } : {}),
74
- };
75
- }
76
-
77
- function readQQBotSection(cfg: Record<string, unknown>): QQBotChannelConfig | undefined {
78
- const channels = asRecord(cfg.channels);
79
- return asRecord(channels?.qqbot) as QQBotChannelConfig | undefined;
80
- }
81
-
82
- /**
83
- * List all configured QQBot account IDs.
84
- * 列出所有已配置的 QQBot 账号 ID。
85
- */
86
- export function listAccountIds(cfg: Record<string, unknown>): string[] {
87
- const ids = new Set<string>();
88
- const qqbot = readQQBotSection(cfg);
89
-
90
- if (qqbot?.appId || process.env.QQBOT_APP_ID) {
91
- ids.add(DEFAULT_ACCOUNT_ID);
92
- }
93
-
94
- if (qqbot?.accounts) {
95
- for (const accountId of Object.keys(qqbot.accounts)) {
96
- if (qqbot.accounts[accountId]?.appId) {
97
- ids.add(accountId);
98
- }
99
- }
100
- }
101
-
102
- return Array.from(ids);
103
- }
104
-
105
- /**
106
- * Resolve the default QQBot account ID.
107
- * 解析默认 QQBot 账号 ID(优先级:defaultAccount > 顶层 appId > 第一个命名账号)。
108
- */
109
- export function resolveDefaultAccountId(cfg: Record<string, unknown>): string {
110
- const qqbot = readQQBotSection(cfg);
111
- const configuredDefaultAccountId = normalizeOptionalLowercaseString(qqbot?.defaultAccount);
112
- if (
113
- configuredDefaultAccountId &&
114
- (configuredDefaultAccountId === DEFAULT_ACCOUNT_ID ||
115
- Boolean(qqbot?.accounts?.[configuredDefaultAccountId]?.appId))
116
- ) {
117
- return configuredDefaultAccountId;
118
- }
119
- if (qqbot?.appId || process.env.QQBOT_APP_ID) {
120
- return DEFAULT_ACCOUNT_ID;
121
- }
122
- if (qqbot?.accounts) {
123
- const ids = Object.keys(qqbot.accounts);
124
- if (ids.length > 0) {
125
- return ids[0];
126
- }
127
- }
128
- return DEFAULT_ACCOUNT_ID;
129
- }
130
-
131
- /**
132
- * Resolve base account info (without credentials).
133
- * 解析账号基础信息(不含凭证)。
134
- *
135
- * Resolves everything except Secret/credential fields. The outer
136
- * config.ts layer calls this and adds Secret handling on top.
137
- */
138
- export function resolveAccountBase(
139
- cfg: Record<string, unknown>,
140
- accountId?: string | null,
141
- ): ResolvedAccountBase {
142
- const resolvedAccountId = accountId ?? resolveDefaultAccountId(cfg);
143
- const qqbot = readQQBotSection(cfg);
144
-
145
- let accountConfig: Record<string, unknown> = {};
146
- let appId = "";
147
-
148
- if (resolvedAccountId === DEFAULT_ACCOUNT_ID) {
149
- accountConfig = normalizeAccountConfig(asRecord(qqbot));
150
- appId = normalizeAppId(qqbot?.appId);
151
- } else {
152
- const account = qqbot?.accounts?.[resolvedAccountId];
153
- accountConfig = normalizeAccountConfig(asRecord(account));
154
- appId = normalizeAppId(asRecord(account)?.appId);
155
- }
156
-
157
- if (!appId && process.env.QQBOT_APP_ID && resolvedAccountId === DEFAULT_ACCOUNT_ID) {
158
- appId = normalizeAppId(process.env.QQBOT_APP_ID);
159
- }
160
-
161
- return {
162
- accountId: resolvedAccountId,
163
- name: readString(accountConfig, "name"),
164
- enabled: accountConfig.enabled !== false,
165
- appId,
166
- systemPrompt: readString(accountConfig, "systemPrompt"),
167
- markdownSupport: accountConfig.markdownSupport !== false,
168
- config: accountConfig,
169
- };
170
- }
171
-
172
- // ---- Account config apply ----
173
-
174
- interface ApplyAccountInput {
175
- appId?: string;
176
- clientSecret?: string;
177
- clientSecretFile?: string;
178
- name?: string;
179
- }
180
-
181
- /** Apply account config updates into a raw config object. */
182
- export function applyAccountConfig(
183
- cfg: Record<string, unknown>,
184
- accountId: string,
185
- input: ApplyAccountInput,
186
- ): Record<string, unknown> {
187
- const next = { ...cfg };
188
- const channels = asRecord(cfg.channels) ?? {};
189
- const existingQQBot = asRecord(channels.qqbot) ?? {};
190
-
191
- if (accountId === DEFAULT_ACCOUNT_ID) {
192
- const allowFrom = (existingQQBot.allowFrom as unknown[]) ?? ["*"];
193
- next.channels = {
194
- ...channels,
195
- qqbot: {
196
- ...existingQQBot,
197
- enabled: true,
198
- allowFrom,
199
- ...(input.appId ? { appId: input.appId } : {}),
200
- ...(input.clientSecret
201
- ? { clientSecret: input.clientSecret, clientSecretFile: undefined }
202
- : input.clientSecretFile
203
- ? { clientSecretFile: input.clientSecretFile, clientSecret: undefined }
204
- : {}),
205
- ...(input.name ? { name: input.name } : {}),
206
- },
207
- };
208
- } else {
209
- const accounts = (existingQQBot.accounts ?? {}) as Record<string, Record<string, unknown>>;
210
- const existingAccount = accounts[accountId] ?? {};
211
- const allowFrom = (existingAccount.allowFrom as unknown[]) ?? ["*"];
212
- next.channels = {
213
- ...channels,
214
- qqbot: {
215
- ...existingQQBot,
216
- enabled: true,
217
- accounts: {
218
- ...accounts,
219
- [accountId]: {
220
- ...existingAccount,
221
- enabled: true,
222
- allowFrom,
223
- ...(input.appId ? { appId: input.appId } : {}),
224
- ...(input.clientSecret
225
- ? { clientSecret: input.clientSecret, clientSecretFile: undefined }
226
- : input.clientSecretFile
227
- ? { clientSecretFile: input.clientSecretFile, clientSecret: undefined }
228
- : {}),
229
- ...(input.name ? { name: input.name } : {}),
230
- },
231
- },
232
- },
233
- };
234
- }
235
-
236
- return next;
237
- }
238
-
239
- // ---- Account status helpers ----
240
-
241
- /** Resolved account shape expected by isAccountConfigured / describeAccount. */
242
- interface AccountSnapshot {
243
- accountId: string;
244
- name?: string;
245
- enabled: boolean;
246
- appId: string;
247
- clientSecret?: string;
248
- secretSource?: string;
249
- config: Record<string, unknown> & {
250
- clientSecret?: unknown;
251
- clientSecretFile?: string;
252
- };
253
- }
254
-
255
- /** Check whether a QQBot account has been fully configured. */
256
- export function isAccountConfigured(account: AccountSnapshot | undefined): boolean {
257
- return Boolean(
258
- account?.appId &&
259
- (Boolean(account?.clientSecret) ||
260
- getPlatformAdapter().hasConfiguredSecret(account?.config?.clientSecret) ||
261
- Boolean(account?.config?.clientSecretFile?.trim())),
262
- );
263
- }
264
-
265
- /** Build a summary description of an account. */
266
- export function describeAccount(account: AccountSnapshot | undefined) {
267
- return {
268
- accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
269
- name: account?.name,
270
- enabled: account?.enabled ?? false,
271
- configured: isAccountConfigured(account),
272
- tokenSource: account?.secretSource,
273
- };
274
- }
275
-
276
- /** Normalize allowFrom entries into uppercase strings without the qqbot: prefix. */
277
- export function formatAllowFrom(allowFrom: Array<string | number> | undefined | null): string[] {
278
- return (allowFrom ?? [])
279
- .map((entry) => normalizeStringifiedOptionalString(entry))
280
- .filter((entry): entry is string => Boolean(entry))
281
- .map((entry) => entry.replace(/^qqbot:/i, ""))
282
- .map((entry) => entry.toUpperCase());
283
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * QQBot setup business logic (pure layer).
3
- * QQBot setup 相关纯业务逻辑。
4
- *
5
- * Token parsing, input validation, and setup config application.
6
- * All functions are framework-agnostic and operate on plain objects.
7
- */
8
-
9
- import { applyAccountConfig } from "./resolve.js";
10
- import { DEFAULT_ACCOUNT_ID } from "./resolve.js";
11
-
12
- /** Parse an inline "appId:clientSecret" token string. */
13
- function parseInlineToken(token: string): { appId: string; clientSecret: string } | null {
14
- const colonIdx = token.indexOf(":");
15
- if (colonIdx <= 0 || colonIdx === token.length - 1) {
16
- return null;
17
- }
18
-
19
- const appId = token.slice(0, colonIdx).trim();
20
- const clientSecret = token.slice(colonIdx + 1).trim();
21
- if (!appId || !clientSecret) {
22
- return null;
23
- }
24
-
25
- return { appId, clientSecret };
26
- }
27
-
28
- interface SetupInput {
29
- token?: string;
30
- tokenFile?: string;
31
- useEnv?: boolean;
32
- name?: string;
33
- }
34
-
35
- /** Validate setup input for a QQBot account. Returns an error string or null. */
36
- export function validateSetupInput(accountId: string, input: SetupInput): string | null {
37
- if (!input.token && !input.tokenFile && !input.useEnv) {
38
- return "QQBot requires --token (format: appId:clientSecret) or --use-env";
39
- }
40
-
41
- if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
42
- return "QQBot --use-env only supports the default account";
43
- }
44
-
45
- if (input.token && !parseInlineToken(input.token)) {
46
- return "QQBot --token must be in appId:clientSecret format";
47
- }
48
-
49
- return null;
50
- }
51
-
52
- /** Apply setup input to account config. Returns updated config. */
53
- export function applySetupAccountConfig(
54
- cfg: Record<string, unknown>,
55
- accountId: string,
56
- input: SetupInput,
57
- ): Record<string, unknown> {
58
- if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
59
- return cfg;
60
- }
61
-
62
- let appId = "";
63
- let clientSecret = "";
64
-
65
- if (input.token) {
66
- const parsed = parseInlineToken(input.token);
67
- if (!parsed) {
68
- return cfg;
69
- }
70
- appId = parsed.appId;
71
- clientSecret = parsed.clientSecret;
72
- }
73
-
74
- if (!appId && !input.tokenFile && !input.useEnv) {
75
- return cfg;
76
- }
77
-
78
- return applyAccountConfig(cfg, accountId, {
79
- appId,
80
- clientSecret,
81
- clientSecretFile: input.tokenFile,
82
- name: input.name,
83
- });
84
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Engine import boundary test.
3
- *
4
- * Ensures that engine/ sources only import from `openclaw/plugin-sdk/*`
5
- * and never reach into other openclaw internals directly.
6
- */
7
-
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import { describe, expect, it } from "vitest";
11
-
12
- const ENGINE_DIR = path.resolve(import.meta.dirname);
13
-
14
- /** Recursively collect all non-test .ts files under a directory. */
15
- function walkSourceFiles(dir: string, files: string[] = []): string[] {
16
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
17
- const fullPath = path.join(dir, entry.name);
18
- if (entry.isDirectory()) {
19
- if (entry.name === "node_modules" || entry.name === "dist") {
20
- continue;
21
- }
22
- walkSourceFiles(fullPath, files);
23
- continue;
24
- }
25
- if (
26
- entry.name.endsWith(".ts") &&
27
- !entry.name.endsWith(".test.ts") &&
28
- !entry.name.endsWith(".spec.ts")
29
- ) {
30
- files.push(fullPath);
31
- }
32
- }
33
- return files;
34
- }
35
-
36
- /**
37
- * Extract all `openclaw/...` import specifiers from source text.
38
- * Matches: import ... from "openclaw/...", import("openclaw/...")
39
- */
40
- function findOpenclawImports(source: string): string[] {
41
- return [
42
- ...source.matchAll(/from\s+["'](openclaw\/[^"']+)["']/g),
43
- ...source.matchAll(/import\(\s*["'](openclaw\/[^"']+)["']\s*\)/g),
44
- ].map((match) => match[1]);
45
- }
46
-
47
- /** Check if an import specifier is an allowed openclaw/plugin-sdk subpath. */
48
- const ALLOWED_PREFIX = ["openclaw", "plugin-sdk"].join("/");
49
- function isAllowedImport(specifier: string): boolean {
50
- return specifier.startsWith(ALLOWED_PREFIX);
51
- }
52
-
53
- describe("engine import boundary", () => {
54
- it("only imports from openclaw/plugin-sdk, never from other openclaw internals", () => {
55
- const sourceFiles = walkSourceFiles(ENGINE_DIR);
56
- const offenders: Array<{ file: string; imports: string[] }> = [];
57
-
58
- for (const file of sourceFiles) {
59
- const source = fs.readFileSync(file, "utf8");
60
- const openclawImports = findOpenclawImports(source);
61
- const forbidden = openclawImports.filter((specifier) => !isAllowedImport(specifier));
62
-
63
- if (forbidden.length > 0) {
64
- offenders.push({
65
- file: path.relative(ENGINE_DIR, file),
66
- imports: forbidden,
67
- });
68
- }
69
- }
70
-
71
- expect(offenders).toEqual([]);
72
- });
73
- });
@@ -1,47 +0,0 @@
1
- /**
2
- * Gateway message decoding utilities.
3
- *
4
- * Extracted from `gateway.ts` — handles the various data formats that
5
- * the QQ Bot WebSocket can deliver (string, Buffer, Buffer[], ArrayBuffer).
6
- *
7
- * Zero external dependencies beyond Node.js built-ins.
8
- */
9
-
10
- /**
11
- * Decode raw WebSocket `data` into a UTF-8 string.
12
- *
13
- * The QQ Bot gateway can send data as a plain string, a single Buffer,
14
- * an array of Buffer chunks, an ArrayBuffer, or a typed array view.
15
- */
16
- export function decodeGatewayMessageData(data: unknown): string {
17
- if (typeof data === "string") {
18
- return data;
19
- }
20
- if (Buffer.isBuffer(data)) {
21
- return data.toString("utf8");
22
- }
23
- if (Array.isArray(data) && data.every((chunk) => Buffer.isBuffer(chunk))) {
24
- return Buffer.concat(data).toString("utf8");
25
- }
26
- if (data instanceof ArrayBuffer) {
27
- return Buffer.from(data).toString("utf8");
28
- }
29
- if (ArrayBuffer.isView(data)) {
30
- return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
31
- }
32
- return "";
33
- }
34
-
35
- /**
36
- * Read the optional `message_scene.ext` array from an event payload.
37
- *
38
- * Guild, C2C, and Group events may carry a `message_scene` object
39
- * with an `ext` string array used for ref-index parsing.
40
- */
41
- export function readOptionalMessageSceneExt(event: Record<string, unknown>): string[] | undefined {
42
- if (!("message_scene" in event)) {
43
- return undefined;
44
- }
45
- const scene = event.message_scene as { ext?: string[] } | undefined;
46
- return scene?.ext;
47
- }