@gakr-gakr/qqbot 0.1.0

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 (149) hide show
  1. package/api.ts +56 -0
  2. package/autobot.plugin.json +167 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +33 -0
  5. package/package.json +64 -0
  6. package/runtime-api.ts +9 -0
  7. package/secret-contract-api.ts +5 -0
  8. package/setup-entry.ts +13 -0
  9. package/setup-plugin-api.ts +3 -0
  10. package/skills/qqbot-channel/SKILL.md +262 -0
  11. package/skills/qqbot-channel/references/api_references.md +521 -0
  12. package/skills/qqbot-media/SKILL.md +37 -0
  13. package/skills/qqbot-remind/SKILL.md +153 -0
  14. package/src/bridge/approval/capability.ts +225 -0
  15. package/src/bridge/approval/handler-runtime.ts +204 -0
  16. package/src/bridge/bootstrap.ts +135 -0
  17. package/src/bridge/channel-entry.ts +18 -0
  18. package/src/bridge/commands/framework-context-adapter.ts +60 -0
  19. package/src/bridge/commands/framework-registration.ts +66 -0
  20. package/src/bridge/commands/from-parser.ts +60 -0
  21. package/src/bridge/commands/result-dispatcher.ts +76 -0
  22. package/src/bridge/config-shared.ts +132 -0
  23. package/src/bridge/config.ts +176 -0
  24. package/src/bridge/gateway.ts +178 -0
  25. package/src/bridge/logger.ts +31 -0
  26. package/src/bridge/narrowing.ts +31 -0
  27. package/src/bridge/plugin-version.ts +102 -0
  28. package/src/bridge/runtime.ts +25 -0
  29. package/src/bridge/sdk-adapter.ts +164 -0
  30. package/src/bridge/setup/finalize.ts +144 -0
  31. package/src/bridge/setup/surface.ts +34 -0
  32. package/src/bridge/tools/channel.ts +58 -0
  33. package/src/bridge/tools/index.ts +15 -0
  34. package/src/bridge/tools/remind.ts +91 -0
  35. package/src/channel.setup.ts +33 -0
  36. package/src/channel.ts +399 -0
  37. package/src/config-schema.ts +84 -0
  38. package/src/engine/access/index.ts +2 -0
  39. package/src/engine/access/resolve-policy.ts +30 -0
  40. package/src/engine/access/sender-match.ts +55 -0
  41. package/src/engine/access/types.ts +2 -0
  42. package/src/engine/adapter/audio.port.ts +27 -0
  43. package/src/engine/adapter/commands.port.ts +22 -0
  44. package/src/engine/adapter/history.port.ts +52 -0
  45. package/src/engine/adapter/index.ts +76 -0
  46. package/src/engine/adapter/mention-gate.port.ts +50 -0
  47. package/src/engine/adapter/types.ts +38 -0
  48. package/src/engine/api/api-client.ts +212 -0
  49. package/src/engine/api/media-chunked.ts +644 -0
  50. package/src/engine/api/media.ts +218 -0
  51. package/src/engine/api/messages.ts +293 -0
  52. package/src/engine/api/retry.ts +217 -0
  53. package/src/engine/api/routes.ts +95 -0
  54. package/src/engine/api/token.ts +277 -0
  55. package/src/engine/approval/index.ts +224 -0
  56. package/src/engine/commands/builtin/log-helpers.ts +341 -0
  57. package/src/engine/commands/builtin/register-all.ts +17 -0
  58. package/src/engine/commands/builtin/register-approve.ts +201 -0
  59. package/src/engine/commands/builtin/register-basic.ts +95 -0
  60. package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
  61. package/src/engine/commands/builtin/register-logs.ts +20 -0
  62. package/src/engine/commands/builtin/register-streaming.ts +138 -0
  63. package/src/engine/commands/builtin/state.ts +31 -0
  64. package/src/engine/commands/slash-command-auth.ts +88 -0
  65. package/src/engine/commands/slash-command-handler.ts +168 -0
  66. package/src/engine/commands/slash-command-test-support.ts +39 -0
  67. package/src/engine/commands/slash-commands-impl.ts +61 -0
  68. package/src/engine/commands/slash-commands.ts +202 -0
  69. package/src/engine/config/credential-backup.ts +108 -0
  70. package/src/engine/config/credentials.ts +76 -0
  71. package/src/engine/config/group.ts +227 -0
  72. package/src/engine/config/resolve.ts +283 -0
  73. package/src/engine/config/setup-logic.ts +84 -0
  74. package/src/engine/gateway/active-cfg.ts +52 -0
  75. package/src/engine/gateway/codec.ts +47 -0
  76. package/src/engine/gateway/constants.ts +117 -0
  77. package/src/engine/gateway/event-dispatcher.ts +177 -0
  78. package/src/engine/gateway/gateway-connection.ts +356 -0
  79. package/src/engine/gateway/gateway.ts +267 -0
  80. package/src/engine/gateway/inbound-attachments.ts +360 -0
  81. package/src/engine/gateway/inbound-context.ts +82 -0
  82. package/src/engine/gateway/inbound-pipeline.ts +171 -0
  83. package/src/engine/gateway/interaction-handler.ts +345 -0
  84. package/src/engine/gateway/message-queue.ts +404 -0
  85. package/src/engine/gateway/outbound-dispatch.ts +590 -0
  86. package/src/engine/gateway/reconnect.ts +199 -0
  87. package/src/engine/gateway/stages/access-stage.ts +99 -0
  88. package/src/engine/gateway/stages/assembly-stage.ts +156 -0
  89. package/src/engine/gateway/stages/content-stage.ts +77 -0
  90. package/src/engine/gateway/stages/envelope-stage.ts +144 -0
  91. package/src/engine/gateway/stages/group-gate-stage.ts +223 -0
  92. package/src/engine/gateway/stages/index.ts +18 -0
  93. package/src/engine/gateway/stages/quote-stage.ts +113 -0
  94. package/src/engine/gateway/stages/refidx-stage.ts +62 -0
  95. package/src/engine/gateway/stages/stub-contexts.ts +77 -0
  96. package/src/engine/gateway/types.ts +230 -0
  97. package/src/engine/gateway/typing-keepalive.ts +102 -0
  98. package/src/engine/gateway/ws-client.ts +16 -0
  99. package/src/engine/group/activation.ts +88 -0
  100. package/src/engine/group/history.ts +321 -0
  101. package/src/engine/group/mention.ts +114 -0
  102. package/src/engine/group/message-gating.ts +108 -0
  103. package/src/engine/messaging/decode-media-path.ts +82 -0
  104. package/src/engine/messaging/media-source.ts +210 -0
  105. package/src/engine/messaging/media-type-detect.ts +27 -0
  106. package/src/engine/messaging/outbound-audio-port.ts +38 -0
  107. package/src/engine/messaging/outbound-deliver.ts +810 -0
  108. package/src/engine/messaging/outbound-media-send.ts +658 -0
  109. package/src/engine/messaging/outbound-reply.ts +27 -0
  110. package/src/engine/messaging/outbound-result-helpers.ts +54 -0
  111. package/src/engine/messaging/outbound-types.ts +47 -0
  112. package/src/engine/messaging/outbound.ts +485 -0
  113. package/src/engine/messaging/reply-dispatcher.ts +597 -0
  114. package/src/engine/messaging/reply-limiter.ts +164 -0
  115. package/src/engine/messaging/sender.ts +741 -0
  116. package/src/engine/messaging/streaming-c2c.ts +1192 -0
  117. package/src/engine/messaging/streaming-media-send.ts +544 -0
  118. package/src/engine/messaging/target-parser.ts +104 -0
  119. package/src/engine/ref/format-message-ref.ts +142 -0
  120. package/src/engine/ref/format-ref-entry.ts +27 -0
  121. package/src/engine/ref/store.ts +211 -0
  122. package/src/engine/ref/types.ts +27 -0
  123. package/src/engine/session/known-users.ts +138 -0
  124. package/src/engine/session/session-store.ts +207 -0
  125. package/src/engine/tools/channel-api.ts +244 -0
  126. package/src/engine/tools/remind-logic.ts +377 -0
  127. package/src/engine/types.ts +313 -0
  128. package/src/engine/utils/attachment-tags.ts +174 -0
  129. package/src/engine/utils/audio.ts +525 -0
  130. package/src/engine/utils/data-paths.ts +38 -0
  131. package/src/engine/utils/diagnostics.ts +93 -0
  132. package/src/engine/utils/file-utils.ts +215 -0
  133. package/src/engine/utils/format.ts +70 -0
  134. package/src/engine/utils/image-size.ts +249 -0
  135. package/src/engine/utils/log.ts +77 -0
  136. package/src/engine/utils/media-tags.ts +177 -0
  137. package/src/engine/utils/payload.ts +157 -0
  138. package/src/engine/utils/platform.ts +265 -0
  139. package/src/engine/utils/request-context.ts +60 -0
  140. package/src/engine/utils/string-normalize.ts +91 -0
  141. package/src/engine/utils/stt.ts +103 -0
  142. package/src/engine/utils/text-parsing.ts +155 -0
  143. package/src/engine/utils/upload-cache.ts +96 -0
  144. package/src/engine/utils/voice-text.ts +15 -0
  145. package/src/exec-approvals.ts +237 -0
  146. package/src/qqbot-test-support.ts +29 -0
  147. package/src/secret-contract.ts +82 -0
  148. package/src/types.ts +210 -0
  149. package/tsconfig.json +16 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * QQ Bot Approval Capability — entry point.
3
+ *
4
+ * QQBot uses a simpler approval model than Telegram/Slack: when no
5
+ * approver list is configured, the bot sends the approval message to the
6
+ * originating conversation and any participant can approve from there.
7
+ *
8
+ * When `execApprovals` IS configured, it gates which requests are
9
+ * handled natively and who is authorized. When it is NOT configured,
10
+ * QQBot falls back to "always handle, anyone can approve".
11
+ */
12
+
13
+ import { createChannelApprovalCapability } from "autobot/plugin-sdk/approval-delivery-runtime";
14
+ import { createLazyChannelApprovalNativeRuntimeAdapter } from "autobot/plugin-sdk/approval-handler-adapter-runtime";
15
+ import type { ChannelApprovalNativeRuntimeAdapter } from "autobot/plugin-sdk/approval-handler-runtime";
16
+ import { resolveApprovalRequestSessionConversation } from "autobot/plugin-sdk/approval-native-runtime";
17
+ import type { ChannelApprovalCapability } from "autobot/plugin-sdk/channel-contract";
18
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
19
+ import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
20
+ import { resolveApprovalTarget } from "../../engine/approval/index.js";
21
+ import {
22
+ isQQBotExecApprovalClientEnabled,
23
+ matchesQQBotApprovalAccount,
24
+ shouldHandleQQBotExecApprovalRequest,
25
+ resolveQQBotExecApprovalConfig,
26
+ authorizeQQBotApprovalAction,
27
+ } from "../../exec-approvals.js";
28
+ import { ensurePlatformAdapter } from "../bootstrap.js";
29
+ import { resolveQQBotAccount } from "../config.js";
30
+ import { getBridgeLogger } from "../logger.js";
31
+
32
+ /**
33
+ * When `execApprovals` is configured, delegate to the profile-based
34
+ * check. Otherwise fall back to target-resolvability plus the shared
35
+ * per-account ownership rule in `matchesQQBotApprovalAccount` so that
36
+ * each QQBot account handler only delivers approvals that originated
37
+ * from its own account (openids are account-scoped — cross-account
38
+ * delivery fails with 500 on the QQ Bot API).
39
+ */
40
+ function shouldHandleRequest(params: {
41
+ cfg: AutoBotConfig;
42
+ accountId?: string | null;
43
+ request: {
44
+ request: {
45
+ sessionKey?: string | null;
46
+ turnSourceTo?: string | null;
47
+ turnSourceChannel?: string | null;
48
+ turnSourceAccountId?: string | null;
49
+ };
50
+ };
51
+ }): boolean {
52
+ if (hasExecApprovalConfig(params)) {
53
+ return shouldHandleQQBotExecApprovalRequest(params as never);
54
+ }
55
+ if (!canResolveTarget(params.request)) {
56
+ return false;
57
+ }
58
+ return matchesQQBotApprovalAccount({
59
+ cfg: params.cfg,
60
+ accountId: params.accountId,
61
+ request: params.request as never,
62
+ });
63
+ }
64
+
65
+ function hasExecApprovalConfig(params: {
66
+ cfg: AutoBotConfig;
67
+ accountId?: string | null;
68
+ }): boolean {
69
+ return resolveQQBotExecApprovalConfig(params) !== undefined;
70
+ }
71
+
72
+ function isNativeDeliveryEnabled(params: {
73
+ cfg: AutoBotConfig;
74
+ accountId?: string | null;
75
+ }): boolean {
76
+ if (hasExecApprovalConfig(params)) {
77
+ return isQQBotExecApprovalClientEnabled(params);
78
+ }
79
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
80
+ return account.enabled && account.secretSource !== "none";
81
+ }
82
+
83
+ function canResolveTarget(request: {
84
+ request: { sessionKey?: string | null; turnSourceTo?: string | null };
85
+ }): boolean {
86
+ const sessionKey = request.request.sessionKey ?? null;
87
+ const turnSourceTo = request.request.turnSourceTo ?? null;
88
+
89
+ const target = resolveApprovalTarget(sessionKey, turnSourceTo);
90
+ if (target) {
91
+ return true;
92
+ }
93
+
94
+ const sessionConversation = resolveApprovalRequestSessionConversation({
95
+ request: request as never,
96
+ channel: "qqbot",
97
+ bundledFallback: true,
98
+ });
99
+ return sessionConversation?.id != null;
100
+ }
101
+
102
+ function createQQBotApprovalCapability(): ChannelApprovalCapability {
103
+ return createChannelApprovalCapability({
104
+ authorizeActorAction: ({ cfg, accountId, senderId, approvalKind }) =>
105
+ authorizeQQBotApprovalAction({ cfg, accountId, senderId, approvalKind }),
106
+
107
+ getActionAvailabilityState: ({
108
+ cfg,
109
+ accountId,
110
+ }: {
111
+ cfg: AutoBotConfig;
112
+ accountId?: string | null;
113
+ action: "approve";
114
+ }) => {
115
+ const enabled = isNativeDeliveryEnabled({ cfg, accountId });
116
+ return enabled ? { kind: "enabled" } : { kind: "disabled" };
117
+ },
118
+
119
+ getExecInitiatingSurfaceState: ({
120
+ cfg,
121
+ accountId,
122
+ }: {
123
+ cfg: AutoBotConfig;
124
+ accountId?: string | null;
125
+ action: "approve";
126
+ }) => {
127
+ const enabled = isNativeDeliveryEnabled({ cfg, accountId });
128
+ return enabled ? { kind: "enabled" } : { kind: "disabled" };
129
+ },
130
+
131
+ describeExecApprovalSetup: ({ accountId }: { accountId?: string | null }) => {
132
+ const prefix =
133
+ accountId && accountId !== "default"
134
+ ? `channels.qqbot.accounts.${accountId}`
135
+ : "channels.qqbot";
136
+ return `QQBot native exec approvals are enabled by default. To restrict who can approve, configure \`${prefix}.execApprovals.approvers\` with QQ user OpenIDs.`;
137
+ },
138
+
139
+ delivery: {
140
+ hasConfiguredDmRoute: () => true,
141
+ shouldSuppressForwardingFallback: (input) => {
142
+ const channel = normalizeOptionalString(input.target?.channel);
143
+ if (channel !== "qqbot") {
144
+ return false;
145
+ }
146
+ const accountId =
147
+ normalizeOptionalString(input.target?.accountId) ??
148
+ normalizeOptionalString(input.request?.request?.turnSourceAccountId);
149
+ const result = isNativeDeliveryEnabled({ cfg: input.cfg, accountId });
150
+ getBridgeLogger().debug?.(
151
+ `[qqbot:approval] shouldSuppressForwardingFallback channel=${channel} accountId=${accountId} → ${result}`,
152
+ );
153
+ return result;
154
+ },
155
+ },
156
+
157
+ native: {
158
+ describeDeliveryCapabilities: ({ cfg, accountId }) => ({
159
+ enabled: isNativeDeliveryEnabled({ cfg, accountId }),
160
+ preferredSurface: "origin" as const,
161
+ supportsOriginSurface: true,
162
+ supportsApproverDmSurface: false,
163
+ notifyOriginWhenDmOnly: false,
164
+ }),
165
+ resolveOriginTarget: ({ request }) => {
166
+ const sessionKey = request.request.sessionKey ?? null;
167
+ const turnSourceTo = request.request.turnSourceTo ?? null;
168
+ const target = resolveApprovalTarget(sessionKey, turnSourceTo);
169
+ if (target) {
170
+ return { to: `${target.type}:${target.id}` };
171
+ }
172
+ const sessionConversation = resolveApprovalRequestSessionConversation({
173
+ request: request as never,
174
+ channel: "qqbot",
175
+ bundledFallback: true,
176
+ });
177
+ if (sessionConversation?.id) {
178
+ const kind = sessionConversation.kind === "group" ? "group" : "c2c";
179
+ return { to: `${kind}:${sessionConversation.id}` };
180
+ }
181
+ return null;
182
+ },
183
+ },
184
+
185
+ nativeRuntime: createLazyChannelApprovalNativeRuntimeAdapter({
186
+ eventKinds: ["exec", "plugin"],
187
+ isConfigured: ({ cfg, accountId }) => {
188
+ const result = isNativeDeliveryEnabled({ cfg, accountId });
189
+ getBridgeLogger().debug?.(
190
+ `[qqbot:approval] nativeRuntime.isConfigured accountId=${accountId} → ${result}`,
191
+ );
192
+ return result;
193
+ },
194
+ shouldHandle: ({ cfg, accountId, request }) => {
195
+ const result = shouldHandleRequest({
196
+ cfg,
197
+ accountId,
198
+ request: request as never,
199
+ });
200
+ getBridgeLogger().debug?.(
201
+ `[qqbot:approval] nativeRuntime.shouldHandle accountId=${accountId} → ${result}`,
202
+ );
203
+ return result;
204
+ },
205
+ load: async () => {
206
+ // Ensure PlatformAdapter is registered before handler-runtime uses
207
+ // getPlatformAdapter(). When the framework spawns the approval handler
208
+ // outside the qqbot gateway startAccount context, channel.ts's
209
+ // side-effect `import "./bridge/bootstrap.js"` may not have run yet.
210
+ ensurePlatformAdapter();
211
+ return (await import("./handler-runtime.js"))
212
+ .qqbotApprovalNativeRuntime as unknown as ChannelApprovalNativeRuntimeAdapter;
213
+ },
214
+ }),
215
+ });
216
+ }
217
+
218
+ const qqbotApprovalCapability = createQQBotApprovalCapability();
219
+
220
+ let cachedCapability: ChannelApprovalCapability | undefined;
221
+
222
+ export function getQQBotApprovalCapability(): ChannelApprovalCapability {
223
+ cachedCapability ??= qqbotApprovalCapability;
224
+ return cachedCapability;
225
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * QQ Bot Native Approval Runtime Adapter.
3
+ *
4
+ * Implements the framework's ChannelApprovalNativeRuntimeSpec to deliver
5
+ * approval requests as QQ messages with inline keyboard buttons and handle
6
+ * resolved/expired lifecycle events.
7
+ *
8
+ * This file is lazily imported by capability.ts to avoid loading
9
+ * heavy dependencies on the critical startup path.
10
+ */
11
+
12
+ import type { ChannelApprovalNativeRuntimeSpec } from "autobot/plugin-sdk/approval-handler-runtime";
13
+ import { createChannelApprovalNativeRuntimeAdapter } from "autobot/plugin-sdk/approval-handler-runtime";
14
+ import type { ChannelApprovalNativeRuntimeAdapter } from "autobot/plugin-sdk/approval-handler-runtime";
15
+ import { resolveApprovalRequestSessionConversation } from "autobot/plugin-sdk/approval-native-runtime";
16
+ import {
17
+ buildExecApprovalText,
18
+ buildPluginApprovalText,
19
+ buildApprovalKeyboard,
20
+ resolveApprovalTarget,
21
+ type ExecApprovalRequest,
22
+ type PluginApprovalRequest,
23
+ } from "../../engine/approval/index.js";
24
+ import { getMessageApi, accountToCreds } from "../../engine/messaging/sender.js";
25
+ import type { ChatScope, InlineKeyboard, MessageResponse } from "../../engine/types.js";
26
+ import {
27
+ matchesQQBotApprovalAccount,
28
+ resolveQQBotExecApprovalConfig,
29
+ isQQBotExecApprovalClientEnabled,
30
+ shouldHandleQQBotExecApprovalRequest,
31
+ } from "../../exec-approvals.js";
32
+ import { ensurePlatformAdapter } from "../bootstrap.js";
33
+ import { resolveQQBotAccount } from "../config.js";
34
+ import { getBridgeLogger } from "../logger.js";
35
+
36
+ type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
37
+
38
+ type QQBotPendingEntry = {
39
+ messageId?: string;
40
+ targetType: ChatScope;
41
+ targetId: string;
42
+ };
43
+
44
+ type QQBotPendingPayload = {
45
+ text: string;
46
+ keyboard: InlineKeyboard;
47
+ };
48
+
49
+ function isExecRequest(request: ApprovalRequest): request is ExecApprovalRequest {
50
+ return "expiresAtMs" in request;
51
+ }
52
+
53
+ function resolveQQTarget(request: ApprovalRequest): { type: ChatScope; id: string } | null {
54
+ const sessionConversation = resolveApprovalRequestSessionConversation({
55
+ request: request as never,
56
+ channel: "qqbot",
57
+ bundledFallback: true,
58
+ });
59
+
60
+ const sessionKey = request.request.sessionKey ?? null;
61
+ const turnSourceTo = request.request.turnSourceTo ?? null;
62
+
63
+ const target = resolveApprovalTarget(sessionKey, turnSourceTo);
64
+ if (target) {
65
+ return target;
66
+ }
67
+
68
+ if (sessionConversation?.id) {
69
+ const kind = sessionConversation.kind;
70
+ const chatScope: ChatScope = kind === "group" ? "group" : "c2c";
71
+ return { type: chatScope, id: sessionConversation.id };
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ type QQBotPreparedTarget = { type: ChatScope; id: string };
78
+
79
+ const qqbotApprovalRuntimeSpec: ChannelApprovalNativeRuntimeSpec<
80
+ QQBotPendingPayload,
81
+ QQBotPreparedTarget,
82
+ QQBotPendingEntry
83
+ > = {
84
+ eventKinds: ["exec", "plugin"],
85
+
86
+ availability: {
87
+ isConfigured: ({ cfg, accountId }) => {
88
+ if (resolveQQBotExecApprovalConfig({ cfg, accountId }) !== undefined) {
89
+ const result = isQQBotExecApprovalClientEnabled({ cfg, accountId });
90
+ getBridgeLogger().debug?.(
91
+ `[qqbot:approval-runtime] isConfigured(profile) accountId=${accountId} → ${result}`,
92
+ );
93
+ return result;
94
+ }
95
+ const account = resolveQQBotAccount(cfg, accountId ?? undefined);
96
+ const result = account.enabled && account.secretSource !== "none";
97
+ getBridgeLogger().debug?.(
98
+ `[qqbot:approval-runtime] isConfigured(fallback) accountId=${accountId} enabled=${account.enabled} secretSource=${account.secretSource} → ${result}`,
99
+ );
100
+ return result;
101
+ },
102
+ shouldHandle: ({ cfg, accountId, request }) => {
103
+ if (resolveQQBotExecApprovalConfig({ cfg, accountId }) !== undefined) {
104
+ const result = shouldHandleQQBotExecApprovalRequest({ cfg, accountId, request });
105
+ getBridgeLogger().debug?.(
106
+ `[qqbot:approval-runtime] shouldHandle(profile) accountId=${accountId} → ${result}`,
107
+ );
108
+ return result;
109
+ }
110
+ const target = resolveQQTarget(request as ApprovalRequest);
111
+ if (target === null) {
112
+ getBridgeLogger().debug?.(
113
+ `[qqbot:approval-runtime] shouldHandle(fallback) accountId=${accountId} target=null → false`,
114
+ );
115
+ return false;
116
+ }
117
+ const accountMatches = matchesQQBotApprovalAccount({
118
+ cfg,
119
+ accountId,
120
+ request: request as ApprovalRequest,
121
+ });
122
+ getBridgeLogger().debug?.(
123
+ `[qqbot:approval-runtime] shouldHandle(fallback) accountId=${accountId} target=${JSON.stringify(
124
+ target,
125
+ )} accountMatches=${accountMatches} → ${accountMatches}`,
126
+ );
127
+ return accountMatches;
128
+ },
129
+ },
130
+
131
+ presentation: {
132
+ buildPendingPayload: ({ request, view }) => {
133
+ const req = request as ApprovalRequest;
134
+ const text = isExecRequest(req) ? buildExecApprovalText(req) : buildPluginApprovalText(req);
135
+ const keyboard = buildApprovalKeyboard(
136
+ req.id,
137
+ view.actions.map((action) => action.decision),
138
+ );
139
+ getBridgeLogger().debug?.(
140
+ `[qqbot:approval-runtime] buildPendingPayload requestId=${req.id} kind=${
141
+ isExecRequest(req) ? "exec" : "plugin"
142
+ }`,
143
+ );
144
+ return { text, keyboard };
145
+ },
146
+ buildResolvedResult: () => ({ kind: "leave" }),
147
+ buildExpiredResult: () => ({ kind: "leave" }),
148
+ },
149
+
150
+ transport: {
151
+ prepareTarget: ({ request }) => {
152
+ const target = resolveQQTarget(request as ApprovalRequest);
153
+ getBridgeLogger().debug?.(
154
+ `[qqbot:approval-runtime] prepareTarget requestId=${request.id} target=${JSON.stringify(target)}`,
155
+ );
156
+ if (!target) {
157
+ return null;
158
+ }
159
+ return { target, dedupeKey: `${target.type}:${target.id}` };
160
+ },
161
+
162
+ deliverPending: async ({ cfg, accountId, preparedTarget, pendingPayload }) => {
163
+ // Ensure the PlatformAdapter is registered — resolveQQBotAccount below
164
+ // calls getPlatformAdapter() to resolve secret inputs.
165
+ ensurePlatformAdapter();
166
+ const account = resolveQQBotAccount(cfg, accountId ?? undefined);
167
+ const creds = accountToCreds(account);
168
+ const messageApi = getMessageApi(account.appId);
169
+
170
+ let result: MessageResponse;
171
+ try {
172
+ getBridgeLogger().debug?.(
173
+ `[qqbot:approval-runtime] deliverPending accountId=${accountId} target=${preparedTarget.type}:${preparedTarget.id}`,
174
+ );
175
+ result = await messageApi.sendMessage(
176
+ preparedTarget.type,
177
+ preparedTarget.id,
178
+ pendingPayload.text,
179
+ creds,
180
+ { inlineKeyboard: pendingPayload.keyboard },
181
+ );
182
+ } catch (err) {
183
+ const msg = err instanceof Error ? err.message : String(err);
184
+ throw new Error(
185
+ `Failed to send approval message to ${preparedTarget.type}:${preparedTarget.id}: ${msg}`,
186
+ { cause: err },
187
+ );
188
+ }
189
+
190
+ getBridgeLogger().debug?.(
191
+ `[qqbot:approval-runtime] deliverPending success accountId=${accountId} messageId=${result.id ?? ""}`,
192
+ );
193
+ return {
194
+ messageId: result.id,
195
+ targetType: preparedTarget.type,
196
+ targetId: preparedTarget.id,
197
+ };
198
+ },
199
+ },
200
+ };
201
+
202
+ export const qqbotApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter(
203
+ qqbotApprovalRuntimeSpec,
204
+ ) as unknown as ChannelApprovalNativeRuntimeAdapter;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Bootstrap the PlatformAdapter for the built-in version.
3
+ *
4
+ * ## Design
5
+ *
6
+ * The adapter is registered via two complementary mechanisms:
7
+ *
8
+ * 1. **Factory registration** (`registerPlatformAdapterFactory`) — a lightweight
9
+ * callback stored in `adapter/index.ts` that is invoked lazily by
10
+ * `getPlatformAdapter()` on first access. This guarantees the adapter is
11
+ * available regardless of module evaluation order or bundler chunk splitting.
12
+ *
13
+ * 2. **Eager side-effect** (`ensurePlatformAdapter()`) — called at module
14
+ * evaluation time when `channel.ts` imports this file. Provides the adapter
15
+ * immediately for code that runs synchronously during startup.
16
+ *
17
+ * Heavy async-only dependencies (`media-runtime`, `config-runtime`,
18
+ * `approval-gateway-runtime`) are lazy-imported inside each async method body
19
+ * so that this module evaluates with minimal overhead.
20
+ *
21
+ * Synchronous dependencies (`secret-input`, `temp-path`) are imported
22
+ * statically at the top level so they work reliably in both production and
23
+ * vitest (which resolves bare specifiers via `resolve.alias`, not Node CJS).
24
+ */
25
+
26
+ import {
27
+ hasConfiguredSecretInput,
28
+ normalizeResolvedSecretInputString,
29
+ normalizeSecretInputString,
30
+ } from "autobot/plugin-sdk/secret-input";
31
+ import { resolvePreferredAutoBotTmpDir } from "autobot/plugin-sdk/temp-path";
32
+ import {
33
+ registerPlatformAdapter,
34
+ registerPlatformAdapterFactory,
35
+ hasPlatformAdapter,
36
+ type PlatformAdapter,
37
+ } from "../engine/adapter/index.js";
38
+ import type { FetchMediaOptions, FetchMediaResult } from "../engine/adapter/types.js";
39
+ import { getBridgeLogger } from "./logger.js";
40
+
41
+ function createBuiltinAdapter(): PlatformAdapter {
42
+ return {
43
+ async validateRemoteUrl(_url: string, _options?: { allowPrivate?: boolean }): Promise<void> {
44
+ // Built-in version delegates SSRF validation to readRemoteMediaBuffer's ssrfPolicy.
45
+ },
46
+
47
+ async resolveSecret(value): Promise<string | undefined> {
48
+ if (typeof value === "string") {
49
+ return value || undefined;
50
+ }
51
+ return undefined;
52
+ },
53
+
54
+ async downloadFile(url: string, destDir: string, filename?: string): Promise<string> {
55
+ const { readRemoteMediaBuffer } = await import("autobot/plugin-sdk/media-runtime");
56
+ const result = await readRemoteMediaBuffer({ url, filePathHint: filename });
57
+ const fs = await import("node:fs");
58
+ const path = await import("node:path");
59
+ if (!fs.existsSync(destDir)) {
60
+ fs.mkdirSync(destDir, { recursive: true });
61
+ }
62
+ const destPath = path.join(destDir, filename ?? "download");
63
+ fs.writeFileSync(destPath, result.buffer);
64
+ return destPath;
65
+ },
66
+
67
+ async fetchMedia(options: FetchMediaOptions): Promise<FetchMediaResult> {
68
+ const { readRemoteMediaBuffer } = await import("autobot/plugin-sdk/media-runtime");
69
+ const result = await readRemoteMediaBuffer({
70
+ url: options.url,
71
+ filePathHint: options.filePathHint,
72
+ maxBytes: options.maxBytes,
73
+ maxRedirects: options.maxRedirects,
74
+ ssrfPolicy: options.ssrfPolicy,
75
+ requestInit: options.requestInit,
76
+ });
77
+ return { buffer: result.buffer, fileName: result.fileName };
78
+ },
79
+
80
+ getTempDir(): string {
81
+ return resolvePreferredAutoBotTmpDir();
82
+ },
83
+
84
+ hasConfiguredSecret(value: unknown): boolean {
85
+ return hasConfiguredSecretInput(value);
86
+ },
87
+
88
+ normalizeSecretInputString(value: unknown): string | undefined {
89
+ return normalizeSecretInputString(value) ?? undefined;
90
+ },
91
+
92
+ resolveSecretInputString(params: { value: unknown; path: string }): string | undefined {
93
+ return normalizeResolvedSecretInputString(params) ?? undefined;
94
+ },
95
+
96
+ async resolveApproval(approvalId: string, decision: string): Promise<boolean> {
97
+ try {
98
+ const { getRuntimeConfig } = await import("autobot/plugin-sdk/runtime-config-snapshot");
99
+ const { resolveApprovalOverGateway } =
100
+ await import("autobot/plugin-sdk/approval-gateway-runtime");
101
+ const cfg = getRuntimeConfig();
102
+ await resolveApprovalOverGateway({
103
+ cfg,
104
+ approvalId,
105
+ decision: decision as "allow-once" | "allow-always" | "deny",
106
+ clientDisplayName: "QQBot Approval Handler",
107
+ });
108
+ return true;
109
+ } catch (err) {
110
+ getBridgeLogger().error(`[qqbot] resolveApproval failed: ${String(err)}`);
111
+ return false;
112
+ }
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Ensure the built-in PlatformAdapter is registered.
119
+ *
120
+ * Safe to call multiple times — only registers on the first invocation.
121
+ * Exported for backward compatibility with code that calls it explicitly.
122
+ */
123
+ export function ensurePlatformAdapter(): void {
124
+ if (!hasPlatformAdapter()) {
125
+ registerPlatformAdapter(createBuiltinAdapter());
126
+ }
127
+ }
128
+
129
+ // Register the adapter factory so getPlatformAdapter() can lazy-init even when
130
+ // this module's side-effect import hasn't executed yet (bundler reordering,
131
+ // framework-spawned approval handlers, etc.).
132
+ registerPlatformAdapterFactory(createBuiltinAdapter);
133
+
134
+ // Also eagerly register for the normal startup path (imported by channel.ts).
135
+ ensurePlatformAdapter();
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Orchestrator for the QQBot `registerFull` hook.
3
+ *
4
+ * Keeping this function in `src/bridge/` (rather than inline in the
5
+ * `extensions/qqbot/index.ts` channel-entry contract) lets the composition
6
+ * be unit-tested and aligns with the layering described in the double-repo
7
+ * migration spec, where bridge-layer composition code is expected to live
8
+ * under `src/bridge/` (or `src/bootstrap/` in the standalone variant).
9
+ */
10
+
11
+ import type { AutoBotPluginApi } from "autobot/plugin-sdk/plugin-entry";
12
+ import { registerQQBotFrameworkCommands } from "./commands/framework-registration.js";
13
+ import { registerQQBotTools } from "./tools/index.js";
14
+
15
+ export function registerQQBotFull(api: AutoBotPluginApi): void {
16
+ registerQQBotTools(api);
17
+ registerQQBotFrameworkCommands(api);
18
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Adapter that builds a `SlashCommandContext` from a framework
3
+ * `PluginCommandContext`.
4
+ *
5
+ * Framework-registered commands enter the plugin through
6
+ * `api.registerCommand`, which surfaces a `PluginCommandContext` shape. Our
7
+ * engine-side command registry, however, is driven by `SlashCommandContext`.
8
+ * This adapter bridges the two so handlers authored against the engine
9
+ * registry can be reused unchanged on the framework command surface.
10
+ */
11
+
12
+ import type { PluginCommandContext } from "autobot/plugin-sdk/plugin-entry";
13
+ import type { SlashCommandContext } from "../../engine/commands/slash-commands.js";
14
+ import type { ResolvedQQBotAccount } from "../../types.js";
15
+ import type { QQBotFromParseResult } from "./from-parser.js";
16
+
17
+ /**
18
+ * Default queue snapshot used for framework-registered commands.
19
+ *
20
+ * Framework-side command dispatch runs outside the per-sender queue, so
21
+ * handlers observe an empty snapshot by design.
22
+ */
23
+ const DEFAULT_QUEUE_SNAPSHOT = {
24
+ totalPending: 0,
25
+ activeUsers: 0,
26
+ maxConcurrentUsers: 10,
27
+ senderPending: 0,
28
+ } as const;
29
+
30
+ interface BuildFrameworkSlashContextInput {
31
+ ctx: PluginCommandContext;
32
+ account: ResolvedQQBotAccount;
33
+ from: QQBotFromParseResult;
34
+ commandName: string;
35
+ }
36
+
37
+ export function buildFrameworkSlashContext({
38
+ ctx,
39
+ account,
40
+ from,
41
+ commandName,
42
+ }: BuildFrameworkSlashContextInput): SlashCommandContext {
43
+ const args = ctx.args ?? "";
44
+ const rawContent = args ? `/${commandName} ${args}` : `/${commandName}`;
45
+
46
+ return {
47
+ type: from.msgType,
48
+ senderId: ctx.senderId ?? "",
49
+ messageId: "",
50
+ eventTimestamp: new Date().toISOString(),
51
+ receivedAt: Date.now(),
52
+ rawContent,
53
+ args,
54
+ accountId: account.accountId,
55
+ appId: account.appId,
56
+ accountConfig: account.config as unknown as Record<string, unknown>,
57
+ commandAuthorized: ctx.isAuthorizedSender,
58
+ queueSnapshot: { ...DEFAULT_QUEUE_SNAPSHOT },
59
+ };
60
+ }