@openclaw/qqbot 2026.5.2 → 2026.5.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/dist/api.js +646 -0
  2. package/dist/approval-cg0SVahb.js +94 -0
  3. package/dist/channel-N6Y_Rcjp.js +551 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.setup-ByCRAe5H.js +25 -0
  6. package/dist/config-D6545NkC.js +94 -0
  7. package/dist/config-schema-DFcjQw73.js +309 -0
  8. package/dist/exec-approvals-COUsM6wZ.js +138 -0
  9. package/dist/gateway-CmSUJKSt.js +6726 -0
  10. package/dist/handler-runtime-Bqm6N0WG.js +120 -0
  11. package/dist/index.js +29 -0
  12. package/dist/narrowing-BoieBTIU.js +25 -0
  13. package/dist/outbound-BJfhwrPg.js +1357 -0
  14. package/dist/request-context-DXtpwWui.js +921 -0
  15. package/dist/resolve-D_06fV6-.js +284 -0
  16. package/dist/runtime-BJAS3eXW.js +18 -0
  17. package/dist/runtime-api.js +2 -0
  18. package/dist/secret-contract-api.js +56 -0
  19. package/dist/sender-p-B14eLG.js +1987 -0
  20. package/dist/setup-entry.js +15 -0
  21. package/dist/setup-plugin-api.js +2 -0
  22. package/dist/string-normalize-Ci6NM5DE.js +98 -0
  23. package/dist/target-parser-Y0prnrXD.js +245 -0
  24. package/package.json +15 -6
  25. package/api.ts +0 -56
  26. package/channel-plugin-api.ts +0 -1
  27. package/index.ts +0 -29
  28. package/runtime-api.ts +0 -9
  29. package/setup-entry.ts +0 -9
  30. package/setup-plugin-api.ts +0 -3
  31. package/src/bridge/approval/capability.ts +0 -237
  32. package/src/bridge/approval/handler-runtime.ts +0 -204
  33. package/src/bridge/bootstrap.ts +0 -135
  34. package/src/bridge/channel-entry.ts +0 -18
  35. package/src/bridge/commands/framework-context-adapter.ts +0 -60
  36. package/src/bridge/commands/framework-registration.ts +0 -47
  37. package/src/bridge/commands/from-parser.test.ts +0 -86
  38. package/src/bridge/commands/from-parser.ts +0 -60
  39. package/src/bridge/commands/result-dispatcher.ts +0 -76
  40. package/src/bridge/config-shared.ts +0 -132
  41. package/src/bridge/config.ts +0 -111
  42. package/src/bridge/gateway.ts +0 -174
  43. package/src/bridge/logger.ts +0 -31
  44. package/src/bridge/narrowing.ts +0 -31
  45. package/src/bridge/plugin-version.test.ts +0 -146
  46. package/src/bridge/plugin-version.ts +0 -102
  47. package/src/bridge/runtime.ts +0 -25
  48. package/src/bridge/sdk-adapter.ts +0 -131
  49. package/src/bridge/setup/finalize.ts +0 -144
  50. package/src/bridge/setup/surface.ts +0 -34
  51. package/src/bridge/tools/channel.ts +0 -58
  52. package/src/bridge/tools/index.ts +0 -15
  53. package/src/bridge/tools/remind.test.ts +0 -124
  54. package/src/bridge/tools/remind.ts +0 -91
  55. package/src/channel.setup.ts +0 -33
  56. package/src/channel.ts +0 -288
  57. package/src/command-auth.test.ts +0 -62
  58. package/src/config-schema.ts +0 -84
  59. package/src/config.test.ts +0 -364
  60. package/src/engine/access/access-control.test.ts +0 -198
  61. package/src/engine/access/access-control.ts +0 -226
  62. package/src/engine/access/index.ts +0 -16
  63. package/src/engine/access/resolve-policy.test.ts +0 -59
  64. package/src/engine/access/resolve-policy.ts +0 -57
  65. package/src/engine/access/sender-match.test.ts +0 -60
  66. package/src/engine/access/sender-match.ts +0 -55
  67. package/src/engine/access/types.ts +0 -53
  68. package/src/engine/adapter/audio.port.ts +0 -27
  69. package/src/engine/adapter/commands.port.ts +0 -22
  70. package/src/engine/adapter/history.port.ts +0 -52
  71. package/src/engine/adapter/index.ts +0 -139
  72. package/src/engine/adapter/mention-gate.port.ts +0 -50
  73. package/src/engine/adapter/types.ts +0 -38
  74. package/src/engine/api/api-client.ts +0 -212
  75. package/src/engine/api/media-chunked.test.ts +0 -336
  76. package/src/engine/api/media-chunked.ts +0 -622
  77. package/src/engine/api/media.ts +0 -218
  78. package/src/engine/api/messages.ts +0 -293
  79. package/src/engine/api/retry.ts +0 -217
  80. package/src/engine/api/routes.ts +0 -95
  81. package/src/engine/api/token.ts +0 -271
  82. package/src/engine/approval/index.test.ts +0 -22
  83. package/src/engine/approval/index.ts +0 -224
  84. package/src/engine/commands/builtin/log-helpers.ts +0 -319
  85. package/src/engine/commands/builtin/register-all.ts +0 -17
  86. package/src/engine/commands/builtin/register-approve.ts +0 -201
  87. package/src/engine/commands/builtin/register-basic.ts +0 -95
  88. package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
  89. package/src/engine/commands/builtin/register-logs.ts +0 -20
  90. package/src/engine/commands/builtin/register-streaming.ts +0 -137
  91. package/src/engine/commands/builtin/state.ts +0 -31
  92. package/src/engine/commands/slash-command-auth.ts +0 -48
  93. package/src/engine/commands/slash-command-handler.ts +0 -146
  94. package/src/engine/commands/slash-commands-impl.test.ts +0 -8
  95. package/src/engine/commands/slash-commands-impl.ts +0 -61
  96. package/src/engine/commands/slash-commands.ts +0 -199
  97. package/src/engine/config/credential-backup.test.ts +0 -88
  98. package/src/engine/config/credential-backup.ts +0 -107
  99. package/src/engine/config/credentials.ts +0 -76
  100. package/src/engine/config/group.test.ts +0 -234
  101. package/src/engine/config/group.ts +0 -299
  102. package/src/engine/config/resolve.test.ts +0 -152
  103. package/src/engine/config/resolve.ts +0 -283
  104. package/src/engine/config/setup-logic.ts +0 -84
  105. package/src/engine/engine-import-boundary.test.ts +0 -73
  106. package/src/engine/gateway/codec.ts +0 -47
  107. package/src/engine/gateway/constants.ts +0 -117
  108. package/src/engine/gateway/event-dispatcher.ts +0 -177
  109. package/src/engine/gateway/gateway-connection.ts +0 -371
  110. package/src/engine/gateway/gateway.ts +0 -291
  111. package/src/engine/gateway/inbound-attachments.test.ts +0 -126
  112. package/src/engine/gateway/inbound-attachments.ts +0 -360
  113. package/src/engine/gateway/inbound-context.ts +0 -195
  114. package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
  115. package/src/engine/gateway/inbound-pipeline.ts +0 -235
  116. package/src/engine/gateway/interaction-handler.ts +0 -220
  117. package/src/engine/gateway/message-queue.test.ts +0 -282
  118. package/src/engine/gateway/message-queue.ts +0 -499
  119. package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
  120. package/src/engine/gateway/outbound-dispatch.ts +0 -575
  121. package/src/engine/gateway/reconnect.ts +0 -199
  122. package/src/engine/gateway/stages/access-stage.ts +0 -132
  123. package/src/engine/gateway/stages/assembly-stage.ts +0 -156
  124. package/src/engine/gateway/stages/content-stage.test.ts +0 -77
  125. package/src/engine/gateway/stages/content-stage.ts +0 -77
  126. package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
  127. package/src/engine/gateway/stages/envelope-stage.ts +0 -144
  128. package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
  129. package/src/engine/gateway/stages/index.ts +0 -18
  130. package/src/engine/gateway/stages/quote-stage.ts +0 -113
  131. package/src/engine/gateway/stages/refidx-stage.ts +0 -62
  132. package/src/engine/gateway/stages/stub-contexts.ts +0 -116
  133. package/src/engine/gateway/types.ts +0 -264
  134. package/src/engine/gateway/typing-keepalive.ts +0 -79
  135. package/src/engine/group/activation.test.ts +0 -114
  136. package/src/engine/group/activation.ts +0 -147
  137. package/src/engine/group/history.test.ts +0 -314
  138. package/src/engine/group/history.ts +0 -321
  139. package/src/engine/group/mention.test.ts +0 -141
  140. package/src/engine/group/mention.ts +0 -197
  141. package/src/engine/group/message-gating.test.ts +0 -188
  142. package/src/engine/group/message-gating.ts +0 -216
  143. package/src/engine/messaging/decode-media-path.ts +0 -82
  144. package/src/engine/messaging/media-source.ts +0 -215
  145. package/src/engine/messaging/media-type-detect.ts +0 -37
  146. package/src/engine/messaging/outbound-audio-port.ts +0 -38
  147. package/src/engine/messaging/outbound-deliver.ts +0 -810
  148. package/src/engine/messaging/outbound-media-send.ts +0 -702
  149. package/src/engine/messaging/outbound-reply.ts +0 -27
  150. package/src/engine/messaging/outbound-result-helpers.ts +0 -54
  151. package/src/engine/messaging/outbound-types.ts +0 -45
  152. package/src/engine/messaging/outbound.ts +0 -485
  153. package/src/engine/messaging/reply-dispatcher.ts +0 -597
  154. package/src/engine/messaging/reply-limiter.ts +0 -164
  155. package/src/engine/messaging/sender.ts +0 -729
  156. package/src/engine/messaging/streaming-c2c.ts +0 -1192
  157. package/src/engine/messaging/streaming-media-send.ts +0 -544
  158. package/src/engine/messaging/target-parser.ts +0 -104
  159. package/src/engine/ref/format-message-ref.ts +0 -142
  160. package/src/engine/ref/format-ref-entry.test.ts +0 -60
  161. package/src/engine/ref/format-ref-entry.ts +0 -27
  162. package/src/engine/ref/store.ts +0 -208
  163. package/src/engine/ref/types.ts +0 -27
  164. package/src/engine/session/known-users.ts +0 -137
  165. package/src/engine/session/session-store.ts +0 -204
  166. package/src/engine/tools/channel-api.ts +0 -244
  167. package/src/engine/tools/remind-logic.test.ts +0 -280
  168. package/src/engine/tools/remind-logic.ts +0 -377
  169. package/src/engine/types.ts +0 -313
  170. package/src/engine/utils/attachment-tags.test.ts +0 -186
  171. package/src/engine/utils/attachment-tags.ts +0 -174
  172. package/src/engine/utils/audio.test.ts +0 -250
  173. package/src/engine/utils/audio.ts +0 -585
  174. package/src/engine/utils/data-paths.ts +0 -38
  175. package/src/engine/utils/diagnostics.ts +0 -109
  176. package/src/engine/utils/file-utils.test.ts +0 -72
  177. package/src/engine/utils/file-utils.ts +0 -225
  178. package/src/engine/utils/format.test.ts +0 -68
  179. package/src/engine/utils/format.ts +0 -70
  180. package/src/engine/utils/image-size.test.ts +0 -158
  181. package/src/engine/utils/image-size.ts +0 -249
  182. package/src/engine/utils/log.test.ts +0 -28
  183. package/src/engine/utils/log.ts +0 -61
  184. package/src/engine/utils/media-tags.test.ts +0 -32
  185. package/src/engine/utils/media-tags.ts +0 -177
  186. package/src/engine/utils/payload.test.ts +0 -68
  187. package/src/engine/utils/payload.ts +0 -145
  188. package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
  189. package/src/engine/utils/platform.test.ts +0 -148
  190. package/src/engine/utils/platform.ts +0 -343
  191. package/src/engine/utils/request-context.ts +0 -60
  192. package/src/engine/utils/string-normalize.ts +0 -91
  193. package/src/engine/utils/stt.test.ts +0 -104
  194. package/src/engine/utils/stt.ts +0 -100
  195. package/src/engine/utils/text-parsing.test.ts +0 -29
  196. package/src/engine/utils/text-parsing.ts +0 -155
  197. package/src/engine/utils/upload-cache.ts +0 -96
  198. package/src/engine/utils/voice-text.ts +0 -15
  199. package/src/exec-approvals.ts +0 -218
  200. package/src/manifest-schema.test.ts +0 -56
  201. package/src/qqbot-test-support.ts +0 -29
  202. package/src/types.ts +0 -210
  203. package/tsconfig.json +0 -16
@@ -1,58 +0,0 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
- import { getAccessToken } from "../../engine/messaging/sender.js";
3
- import { ChannelApiSchema, executeChannelApi } from "../../engine/tools/channel-api.js";
4
- import type { ChannelApiParams } from "../../engine/tools/channel-api.js";
5
- import { listQQBotAccountIds, resolveQQBotAccount } from "../config.js";
6
-
7
- /**
8
- * Register the QQ channel API proxy tool.
9
- *
10
- * The tool acts as an authenticated HTTP proxy for the QQ Open Platform
11
- * channel APIs. Agents learn endpoint details from the skill docs and
12
- * send requests through this proxy.
13
- */
14
- export function registerChannelTool(api: OpenClawPluginApi): void {
15
- const cfg = api.config;
16
- if (!cfg) {
17
- return;
18
- }
19
-
20
- const accountIds = listQQBotAccountIds(cfg);
21
- if (accountIds.length === 0) {
22
- return;
23
- }
24
-
25
- const firstAccountId = accountIds[0];
26
- const account = resolveQQBotAccount(cfg, firstAccountId);
27
-
28
- if (!account.appId || !account.clientSecret) {
29
- return;
30
- }
31
-
32
- api.registerTool(
33
- {
34
- name: "qqbot_channel_api",
35
- label: "QQBot Channel API",
36
- description:
37
- "Authenticated HTTP proxy for QQ Open Platform channel APIs. " +
38
- "Common endpoints: " +
39
- "list guilds GET /users/@me/guilds | " +
40
- "list channels GET /guilds/{guild_id}/channels | " +
41
- "get channel GET /channels/{channel_id} | " +
42
- "create channel POST /guilds/{guild_id}/channels | " +
43
- "list members GET /guilds/{guild_id}/members?after=0&limit=100 | " +
44
- "get member GET /guilds/{guild_id}/members/{user_id} | " +
45
- "list threads GET /channels/{channel_id}/threads | " +
46
- "create thread PUT /channels/{channel_id}/threads | " +
47
- "create announce POST /guilds/{guild_id}/announces | " +
48
- "create schedule POST /channels/{channel_id}/schedules. " +
49
- "See the qqbot-channel skill for full endpoint details.",
50
- parameters: ChannelApiSchema,
51
- async execute(_toolCallId, params) {
52
- const accessToken = await getAccessToken(account.appId, account.clientSecret);
53
- return executeChannelApi(params as ChannelApiParams, { accessToken });
54
- },
55
- },
56
- { name: "qqbot_channel_api" },
57
- );
58
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Aggregate QQBot plugin tool registrations.
3
- *
4
- * New tools should be added here rather than in the channel-entry contract
5
- * file so that the plugin-level `index.ts` stays a pure declaration.
6
- */
7
-
8
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
9
- import { registerChannelTool } from "./channel.js";
10
- import { registerRemindTool } from "./remind.js";
11
-
12
- export function registerQQBotTools(api: OpenClawPluginApi): void {
13
- registerChannelTool(api);
14
- registerRemindTool(api);
15
- }
@@ -1,124 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const { callGatewayToolMock } = vi.hoisted(() => ({
4
- callGatewayToolMock: vi.fn(),
5
- }));
6
-
7
- vi.mock("openclaw/plugin-sdk/agent-harness-runtime", () => ({
8
- callGatewayTool: callGatewayToolMock,
9
- }));
10
-
11
- import { createRemindTool } from "./remind.js";
12
-
13
- describe("bridge/tools/remind", () => {
14
- beforeEach(() => {
15
- callGatewayToolMock.mockReset();
16
- callGatewayToolMock.mockResolvedValue({ ok: true });
17
- });
18
-
19
- it("marks qqbot_remind as owner-only", () => {
20
- const tool = createRemindTool();
21
- expect(tool.ownerOnly).toBe(true);
22
- });
23
-
24
- it("schedules reminders directly through Gateway cron with ambient QQ delivery context", async () => {
25
- callGatewayToolMock.mockResolvedValue({ id: "job-1" });
26
- const tool = createRemindTool({
27
- senderIsOwner: true,
28
- deliveryContext: { to: "qqbot:c2c:user-openid", accountId: "bot2" },
29
- });
30
-
31
- const result = await tool.execute("tool-call-1", {
32
- action: "add",
33
- content: "drink water",
34
- time: "5m",
35
- });
36
-
37
- expect(callGatewayToolMock).toHaveBeenCalledWith(
38
- "cron.add",
39
- { timeoutMs: 60_000 },
40
- {
41
- job: expect.objectContaining({
42
- sessionTarget: "isolated",
43
- payload: { kind: "agentTurn", message: expect.stringContaining("drink water") },
44
- delivery: {
45
- mode: "announce",
46
- channel: "qqbot",
47
- to: "qqbot:c2c:user-openid",
48
- accountId: "bot2",
49
- },
50
- }),
51
- },
52
- );
53
- expect(result.details).toEqual({
54
- ok: true,
55
- action: "add",
56
- summary: '⏰ Reminder in 5m: "drink water"',
57
- cronResult: { id: "job-1" },
58
- });
59
- });
60
-
61
- it("routes list and remove through Gateway cron without exposing generic cron to the model", async () => {
62
- const tool = createRemindTool({ senderIsOwner: true });
63
-
64
- await tool.execute("tool-call-1", { action: "list" });
65
- await tool.execute("tool-call-2", { action: "remove", jobId: "job-1" });
66
-
67
- expect(callGatewayToolMock).toHaveBeenNthCalledWith(1, "cron.list", { timeoutMs: 60_000 }, {});
68
- expect(callGatewayToolMock).toHaveBeenNthCalledWith(
69
- 2,
70
- "cron.remove",
71
- { timeoutMs: 60_000 },
72
- { jobId: "job-1" },
73
- );
74
- });
75
-
76
- it("supports injected cron scheduler dependencies for engine-level tests", async () => {
77
- const callCron = vi.fn(async () => ({ id: "job-1" }));
78
- const tool = createRemindTool(
79
- {
80
- senderIsOwner: true,
81
- deliveryContext: { to: "qqbot:c2c:user-openid", accountId: "bot2" },
82
- },
83
- { callCron },
84
- );
85
-
86
- await tool.execute("tool-call-1", {
87
- action: "add",
88
- content: "drink water",
89
- time: "5m",
90
- });
91
-
92
- expect(callCron).toHaveBeenCalledWith(
93
- expect.objectContaining({
94
- action: "add",
95
- job: expect.objectContaining({
96
- delivery: expect.objectContaining({ to: "qqbot:c2c:user-openid", accountId: "bot2" }),
97
- }),
98
- }),
99
- );
100
- expect(callGatewayToolMock).not.toHaveBeenCalled();
101
- });
102
-
103
- it("does not schedule when sender ownership is missing", async () => {
104
- const callCron = vi.fn(async () => ({ id: "job-1" }));
105
- const tool = createRemindTool(
106
- {
107
- deliveryContext: { to: "qqbot:c2c:user-openid", accountId: "bot2" },
108
- },
109
- { callCron },
110
- );
111
-
112
- const result = await tool.execute("tool-call-1", {
113
- action: "add",
114
- content: "drink water",
115
- time: "5m",
116
- });
117
-
118
- expect(callCron).not.toHaveBeenCalled();
119
- expect(callGatewayToolMock).not.toHaveBeenCalled();
120
- expect(result.details).toEqual({
121
- error: "QQ reminders require an owner-authorized sender.",
122
- });
123
- });
124
- });
@@ -1,91 +0,0 @@
1
- import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
2
- import type {
3
- AnyAgentTool,
4
- OpenClawPluginApi,
5
- OpenClawPluginToolContext,
6
- } from "openclaw/plugin-sdk/core";
7
- import { RemindSchema, executeScheduledRemind } from "../../engine/tools/remind-logic.js";
8
- import type { RemindCronAction, RemindParams } from "../../engine/tools/remind-logic.js";
9
- import { getRequestContext } from "../../engine/utils/request-context.js";
10
-
11
- type CronGatewayCaller = (params: RemindCronAction) => Promise<unknown>;
12
-
13
- type RemindToolDeps = {
14
- callCron: CronGatewayCaller;
15
- };
16
-
17
- const DEFAULT_GATEWAY_TIMEOUT_MS = 60_000;
18
-
19
- function unexpectedCronParams(params: never): never {
20
- throw new Error(`Unsupported reminder cron action: ${JSON.stringify(params)}`);
21
- }
22
-
23
- const defaultDeps: RemindToolDeps = {
24
- callCron: async (params) => {
25
- switch (params.action) {
26
- case "list":
27
- return await callGatewayTool("cron.list", { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, {});
28
- case "remove":
29
- return await callGatewayTool(
30
- "cron.remove",
31
- { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS },
32
- { jobId: params.jobId },
33
- );
34
- case "add":
35
- return await callGatewayTool(
36
- "cron.add",
37
- { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS },
38
- { job: params.job },
39
- );
40
- }
41
- return unexpectedCronParams(params);
42
- },
43
- };
44
-
45
- export function createRemindTool(
46
- toolContext: OpenClawPluginToolContext = {},
47
- deps: RemindToolDeps = defaultDeps,
48
- ): AnyAgentTool {
49
- return {
50
- name: "qqbot_remind",
51
- label: "QQBot Reminder",
52
- ownerOnly: true,
53
- description:
54
- "Create, list, and remove QQ reminders. " +
55
- "This tool schedules Gateway cron jobs directly; do not call the cron tool after it succeeds.\n" +
56
- "Create: action=add, content=message, time=schedule (to is optional, " +
57
- "resolved automatically from the current conversation)\n" +
58
- "List: action=list\n" +
59
- "Remove: action=remove, jobId=job id from list\n" +
60
- 'Time examples: "5m", "1h", "0 8 * * *"',
61
- parameters: RemindSchema,
62
- async execute(_toolCallId, params) {
63
- if (toolContext.senderIsOwner !== true) {
64
- return {
65
- content: [
66
- {
67
- type: "text" as const,
68
- text: JSON.stringify({
69
- error: "QQ reminders require an owner-authorized sender.",
70
- }),
71
- },
72
- ],
73
- details: { error: "QQ reminders require an owner-authorized sender." },
74
- };
75
- }
76
- const ctx = getRequestContext();
77
- return await executeScheduledRemind(
78
- params as RemindParams,
79
- {
80
- fallbackTo: ctx?.target ?? toolContext.deliveryContext?.to,
81
- fallbackAccountId: ctx?.accountId ?? toolContext.deliveryContext?.accountId,
82
- },
83
- deps.callCron,
84
- );
85
- },
86
- };
87
- }
88
-
89
- export function registerRemindTool(api: OpenClawPluginApi): void {
90
- api.registerTool((ctx) => createRemindTool(ctx), { name: "qqbot_remind" });
91
- }
@@ -1,33 +0,0 @@
1
- import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
2
- import "./bridge/bootstrap.js";
3
- import { qqbotConfigAdapter, qqbotMeta, qqbotSetupAdapterShared } from "./bridge/config-shared.js";
4
- import { qqbotSetupWizard } from "./bridge/setup/surface.js";
5
- import { qqbotChannelConfigSchema } from "./config-schema.js";
6
- import type { ResolvedQQBotAccount } from "./types.js";
7
-
8
- /**
9
- * Setup-only QQBot plugin — lightweight subset used during `openclaw onboard`
10
- * and `openclaw configure` without pulling the full runtime dependencies.
11
- */
12
- export const qqbotSetupPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
13
- id: "qqbot",
14
- setupWizard: qqbotSetupWizard,
15
- meta: {
16
- ...qqbotMeta,
17
- },
18
- capabilities: {
19
- chatTypes: ["direct", "group"],
20
- media: true,
21
- reactions: false,
22
- threads: false,
23
- blockStreaming: true,
24
- },
25
- reload: { configPrefixes: ["channels.qqbot"] },
26
- configSchema: qqbotChannelConfigSchema,
27
- config: {
28
- ...qqbotConfigAdapter,
29
- },
30
- setup: {
31
- ...qqbotSetupAdapterShared,
32
- },
33
- };
package/src/channel.ts DELETED
@@ -1,288 +0,0 @@
1
- import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
2
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
3
- import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
4
- // Register the PlatformAdapter before any core/ module is used.
5
- import "./bridge/bootstrap.js";
6
- import { getQQBotApprovalCapability } from "./bridge/approval/capability.js";
7
- import { qqbotConfigAdapter, qqbotMeta, qqbotSetupAdapterShared } from "./bridge/config-shared.js";
8
- import {
9
- applyQQBotAccountConfig,
10
- DEFAULT_ACCOUNT_ID,
11
- resolveQQBotAccount,
12
- } from "./bridge/config.js";
13
- import type { GatewayContext } from "./bridge/gateway.js";
14
- import { toGatewayAccount, writeOpenClawConfigThroughRuntime } from "./bridge/narrowing.js";
15
- import { getQQBotRuntime } from "./bridge/runtime.js";
16
- import { qqbotSetupWizard } from "./bridge/setup/surface.js";
17
- import { qqbotChannelConfigSchema } from "./config-schema.js";
18
- import { loadCredentialBackup, saveCredentialBackup } from "./engine/config/credential-backup.js";
19
- import { clearAccountCredentials } from "./engine/config/credentials.js";
20
- import {
21
- normalizeTarget as coreNormalizeTarget,
22
- looksLikeQQBotTarget,
23
- } from "./engine/messaging/target-parser.js";
24
- import type { ResolvedQQBotAccount } from "./types.js";
25
-
26
- // Shared promise so concurrent multi-account startups serialize the dynamic
27
- // import of the gateway module, avoiding an ESM circular-dependency race.
28
- let _gatewayModulePromise: Promise<typeof import("./bridge/gateway.js")> | undefined;
29
- function loadGatewayModule(): Promise<typeof import("./bridge/gateway.js")> {
30
- _gatewayModulePromise ??= import("./bridge/gateway.js");
31
- return _gatewayModulePromise;
32
- }
33
-
34
- const EXEC_APPROVAL_COMMAND_RE =
35
- /\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+(?:allow-once|allow-always|always|deny)\b/i;
36
-
37
- function persistAccountCredentialSnapshot(account: ResolvedQQBotAccount): void {
38
- if (account.appId && account.clientSecret) {
39
- saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
40
- }
41
- }
42
-
43
- function shouldSuppressLocalQQBotApprovalPrompt(params: {
44
- cfg: OpenClawConfig;
45
- accountId?: string | null;
46
- payload: { text?: string; channelData?: unknown };
47
- hint?: { kind: "approval-pending" | "approval-resolved"; approvalKind: "exec" | "plugin" };
48
- }): boolean {
49
- if (params.hint?.kind !== "approval-pending" || params.hint.approvalKind !== "exec") {
50
- return false;
51
- }
52
- const account = resolveQQBotAccount(params.cfg, params.accountId);
53
- if (!account.enabled || account.secretSource === "none") {
54
- return false;
55
- }
56
- if (getExecApprovalReplyMetadata(params.payload as never)) {
57
- return true;
58
- }
59
- const text = typeof params.payload.text === "string" ? params.payload.text : "";
60
- return EXEC_APPROVAL_COMMAND_RE.test(text);
61
- }
62
-
63
- export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
64
- id: "qqbot",
65
- setupWizard: qqbotSetupWizard,
66
- meta: {
67
- ...qqbotMeta,
68
- },
69
- capabilities: {
70
- chatTypes: ["direct", "group"],
71
- media: true,
72
- reactions: false,
73
- threads: false,
74
- blockStreaming: true,
75
- },
76
- reload: { configPrefixes: ["channels.qqbot"] },
77
- configSchema: qqbotChannelConfigSchema,
78
- config: {
79
- ...qqbotConfigAdapter,
80
- /**
81
- * Treat an account as configured when either the live config has
82
- * credentials OR a recoverable credential backup exists. This mirrors
83
- * the standalone plugin and lets the gateway survive a hot upgrade
84
- * that wiped openclaw.json mid-flight.
85
- */
86
- isConfigured: (account: ResolvedQQBotAccount | undefined) => {
87
- if (qqbotConfigAdapter.isConfigured(account)) {
88
- return true;
89
- }
90
- if (!account) {
91
- return false;
92
- }
93
- const backup = loadCredentialBackup(account.accountId);
94
- return Boolean(backup?.appId && backup?.clientSecret);
95
- },
96
- },
97
- setup: {
98
- ...qqbotSetupAdapterShared,
99
- },
100
- approvalCapability: getQQBotApprovalCapability(),
101
- messaging: {
102
- targetPrefixes: ["qqbot"],
103
- /** Normalize common QQ Bot target formats into the canonical qqbot:... form. */
104
- normalizeTarget: coreNormalizeTarget,
105
- targetResolver: {
106
- /** Return true when the id looks like a QQ Bot target. */
107
- looksLikeId: looksLikeQQBotTarget,
108
- hint: "QQ Bot target format: qqbot:c2c:openid (direct) or qqbot:group:groupid (group)",
109
- },
110
- },
111
- outbound: {
112
- deliveryMode: "direct",
113
- chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
114
- chunkerMode: "markdown",
115
- textChunkLimit: 5000,
116
- shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) =>
117
- shouldSuppressLocalQQBotApprovalPrompt({
118
- cfg,
119
- accountId,
120
- payload,
121
- hint,
122
- }),
123
- sendText: async ({ to, text, accountId, replyToId, cfg }) => {
124
- // Ensure bridge/gateway.ts module-level registrations (audio adapter factory,
125
- // platform adapter, etc.) have executed before engine code runs.
126
- await loadGatewayModule();
127
- const account = resolveQQBotAccount(cfg, accountId);
128
- const { sendText } = await import("./engine/messaging/outbound.js");
129
- const result = await sendText({
130
- to,
131
- text,
132
- accountId,
133
- replyToId,
134
- account: toGatewayAccount(account),
135
- });
136
- return {
137
- channel: "qqbot" as const,
138
- messageId: result.messageId ?? "",
139
- meta: result.error ? { error: result.error } : undefined,
140
- };
141
- },
142
- sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
143
- // Same guard as sendText — ensure adapters are registered.
144
- await loadGatewayModule();
145
- const account = resolveQQBotAccount(cfg, accountId);
146
- const { sendMedia } = await import("./engine/messaging/outbound.js");
147
- const result = await sendMedia({
148
- to,
149
- text: text ?? "",
150
- mediaUrl: mediaUrl ?? "",
151
- accountId,
152
- replyToId,
153
- account: toGatewayAccount(account),
154
- });
155
- return {
156
- channel: "qqbot" as const,
157
- messageId: result.messageId ?? "",
158
- meta: result.error ? { error: result.error } : undefined,
159
- };
160
- },
161
- },
162
- gateway: {
163
- startAccount: async (ctx) => {
164
- let { account, cfg } = ctx;
165
- const { abortSignal, log } = ctx;
166
-
167
- // Recover credentials from the per-account backup if the live
168
- // config is missing appId/secret (e.g. a hot-upgrade wiped
169
- // openclaw.json). We only restore when both fields are empty so a
170
- // user's intentional clear isn't silently undone.
171
- if (!account.appId || !account.clientSecret) {
172
- const backup = loadCredentialBackup(account.accountId);
173
- if (backup?.appId && backup?.clientSecret) {
174
- try {
175
- const nextCfg = applyQQBotAccountConfig(cfg, account.accountId, {
176
- appId: backup.appId,
177
- clientSecret: backup.clientSecret,
178
- });
179
- await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg);
180
- cfg = nextCfg;
181
- account = resolveQQBotAccount(nextCfg, account.accountId);
182
- log?.info(
183
- `[qqbot:${account.accountId}] Restored credentials from backup (appId=${account.appId})`,
184
- );
185
- } catch (err) {
186
- log?.error(
187
- `[qqbot:${account.accountId}] Failed to restore credentials from backup: ${err instanceof Error ? err.message : String(err)}`,
188
- );
189
- }
190
- }
191
- }
192
-
193
- // Serialize the dynamic import so concurrent multi-account startups
194
- // do not hit an ESM circular-dependency race where the gateway chunk's
195
- // transitive imports have not finished evaluating yet.
196
- const { startGateway } = await loadGatewayModule();
197
-
198
- log?.info(
199
- `[qqbot:${account.accountId}] Starting gateway — appId=${account.appId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`,
200
- );
201
-
202
- await startGateway({
203
- account,
204
- abortSignal,
205
- cfg,
206
- log,
207
- channelRuntime: ctx.channelRuntime as GatewayContext["channelRuntime"],
208
- onReady: () => {
209
- log?.info(`[qqbot:${account.accountId}] Gateway ready`);
210
- ctx.setStatus({
211
- ...ctx.getStatus(),
212
- running: true,
213
- connected: true,
214
- lastConnectedAt: Date.now(),
215
- });
216
- // Snapshot credentials so we can recover from the next hot
217
- // upgrade that might wipe openclaw.json mid-flight.
218
- persistAccountCredentialSnapshot(account);
219
- },
220
- onResumed: () => {
221
- log?.info(`[qqbot:${account.accountId}] Gateway resumed`);
222
- ctx.setStatus({
223
- ...ctx.getStatus(),
224
- running: true,
225
- connected: true,
226
- lastConnectedAt: Date.now(),
227
- });
228
- persistAccountCredentialSnapshot(account);
229
- },
230
- onError: (error) => {
231
- log?.error(`[qqbot:${account.accountId}] Gateway error: ${error.message}`);
232
- ctx.setStatus({
233
- ...ctx.getStatus(),
234
- lastError: error.message,
235
- });
236
- },
237
- });
238
- },
239
- logoutAccount: async ({ accountId, cfg }) => {
240
- const { nextCfg, cleared, changed } = clearAccountCredentials(
241
- cfg as unknown as Record<string, unknown>,
242
- accountId,
243
- );
244
-
245
- if (changed) {
246
- await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg as OpenClawConfig);
247
- }
248
-
249
- const resolved = resolveQQBotAccount((changed ? nextCfg : cfg) as OpenClawConfig, accountId);
250
- const loggedOut = resolved.secretSource === "none";
251
- const envToken = Boolean(process.env.QQBOT_CLIENT_SECRET);
252
-
253
- return { ok: true, cleared, envToken, loggedOut };
254
- },
255
- },
256
- status: {
257
- defaultRuntime: {
258
- accountId: DEFAULT_ACCOUNT_ID,
259
- running: false,
260
- connected: false,
261
- lastConnectedAt: null,
262
- lastError: null,
263
- lastInboundAt: null,
264
- lastOutboundAt: null,
265
- },
266
- buildChannelSummary: ({ snapshot }) => ({
267
- configured: snapshot.configured ?? false,
268
- tokenSource: snapshot.tokenSource ?? "none",
269
- running: snapshot.running ?? false,
270
- connected: snapshot.connected ?? false,
271
- lastConnectedAt: snapshot.lastConnectedAt ?? null,
272
- lastError: snapshot.lastError ?? null,
273
- }),
274
- buildAccountSnapshot: ({ account, runtime }) => ({
275
- accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
276
- name: account?.name,
277
- enabled: account?.enabled ?? false,
278
- configured: Boolean(account?.appId && account?.clientSecret),
279
- tokenSource: account?.secretSource,
280
- running: runtime?.running ?? false,
281
- connected: runtime?.connected ?? false,
282
- lastConnectedAt: runtime?.lastConnectedAt ?? null,
283
- lastError: runtime?.lastError ?? null,
284
- lastInboundAt: runtime?.lastInboundAt ?? null,
285
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
286
- }),
287
- },
288
- };
@@ -1,62 +0,0 @@
1
- /**
2
- * Regression tests for QQBot command authorization alignment with the shared
3
- * command-auth model.
4
- *
5
- * Covers the regression identified in the code review:
6
- *
7
- * allowFrom entries with the qqbot: prefix must normalize correctly so that
8
- * "qqbot:<id>" in channel.allowFrom matches the inbound event.senderId "<id>".
9
- * Verified against the normalization logic in the gateway.ts inbound path.
10
- *
11
- * Note: commands.allowFrom.qqbot precedence over channel allowFrom is enforced
12
- * by the framework's resolveCommandAuthorization(). QQBot routes requireAuth:true
13
- * commands through the framework (api.registerCommand), so that behavior is
14
- * covered by the framework's own tests rather than duplicated here.
15
- */
16
-
17
- import { describe, expect, it } from "vitest";
18
- import { qqbotPlugin } from "./channel.js";
19
-
20
- // ---------------------------------------------------------------------------
21
- // qqbot: prefix normalization for inbound commandAuthorized
22
- //
23
- // Uses qqbotPlugin.config.formatAllowFrom directly — the same function the
24
- // fixed gateway.ts inbound path calls — so the test stays in sync with the
25
- // actual implementation without duplicating the logic.
26
- // ---------------------------------------------------------------------------
27
-
28
- describe("qqbot: prefix normalization for inbound commandAuthorized", () => {
29
- const formatAllowFrom = qqbotPlugin.config.formatAllowFrom!;
30
-
31
- /** Mirrors the fixed gateway.ts inbound commandAuthorized computation. */
32
- function resolveInboundCommandAuthorized(rawAllowFrom: string[], senderId: string): boolean {
33
- const normalizedAllowFrom = formatAllowFrom({
34
- cfg: {} as never,
35
- accountId: null,
36
- allowFrom: rawAllowFrom,
37
- });
38
- const normalizedSenderId = senderId.replace(/^qqbot:/i, "").toUpperCase();
39
- const allowAll = normalizedAllowFrom.length === 0 || normalizedAllowFrom.some((e) => e === "*");
40
- return allowAll || normalizedAllowFrom.includes(normalizedSenderId);
41
- }
42
-
43
- it("authorizes when allowFrom uses qqbot: prefix and senderId is the bare id", () => {
44
- expect(resolveInboundCommandAuthorized(["qqbot:USER123"], "USER123")).toBe(true);
45
- });
46
-
47
- it("authorizes when qqbot: prefix is mixed case", () => {
48
- expect(resolveInboundCommandAuthorized(["QQBot:user123"], "USER123")).toBe(true);
49
- });
50
-
51
- it("denies a sender not in the qqbot:-prefixed allowFrom list", () => {
52
- expect(resolveInboundCommandAuthorized(["qqbot:USER123"], "OTHER")).toBe(false);
53
- });
54
-
55
- it("authorizes any sender when allowFrom is empty (open)", () => {
56
- expect(resolveInboundCommandAuthorized([], "ANYONE")).toBe(true);
57
- });
58
-
59
- it("authorizes any sender when allowFrom contains wildcard *", () => {
60
- expect(resolveInboundCommandAuthorized(["*"], "ANYONE")).toBe(true);
61
- });
62
- });