@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,139 +0,0 @@
1
- /**
2
- * Engine adapter layer — all external dependency interfaces unified here.
3
- *
4
- * This directory is the **single source of truth** for every interface
5
- * the engine uses to talk to the outside world.
6
- *
7
- * ## Two-layer DI architecture
8
- *
9
- * ### Layer 1: EngineAdapters (构造参数注入 — preferred)
10
- *
11
- * Used for capabilities consumed within the pipeline call stack.
12
- * Injected once via {@link CoreGatewayContext.adapters}, threaded
13
- * through {@link InboundPipelineDeps.adapters}, consumed by stages.
14
- *
15
- * - {@link HistoryPort} — group history record/build/clear
16
- * - {@link MentionGatePort} — mention + command gate evaluation
17
- * - {@link AudioConvertPort} — inbound SILK→WAV conversion
18
- * - {@link OutboundAudioPort} — outbound WAV→SILK conversion
19
- * - {@link CommandsPort} — slash-command version/approve dependencies
20
- *
21
- * ### Layer 2: PlatformAdapter (global singleton — leaf utilities)
22
- *
23
- * Used by leaf utility functions (`file-utils`, `image-size`,
24
- * `platform`, `config/resolve`) that sit outside the pipeline and
25
- * cannot receive a `deps` parameter. Registered once at startup.
26
- *
27
- * - {@link PlatformAdapter} — SSRF, secrets, media fetch, temp dir
28
- */
29
-
30
- import type { FetchMediaOptions, FetchMediaResult, SecretInputRef } from "./types.js";
31
-
32
- // ============ EngineAdapters (aggregated port injection) ============
33
-
34
- /**
35
- * Aggregated adapter ports injected via `CoreGatewayContext.adapters`.
36
- *
37
- * All fields are required — the bridge layer must provide every adapter.
38
- * The engine no longer falls back to built-in implementations.
39
- */
40
- export interface EngineAdapters {
41
- /** Group history record/build/clear — backed by SDK `reply-history`. */
42
- history: import("./history.port.js").HistoryPort;
43
- /** Mention + command gate evaluation — backed by SDK `channel-mention-gating`. */
44
- mentionGate: import("./mention-gate.port.js").MentionGatePort;
45
- /** Inbound audio conversion (SILK→WAV, voice detection). */
46
- audioConvert: import("./audio.port.js").AudioConvertPort;
47
- /** Outbound audio conversion (WAV→SILK, audio detection). */
48
- outboundAudio: import("./audio.port.js").OutboundAudioPort;
49
- /** Slash-command dependencies (version, approve runtime). */
50
- commands: import("./commands.port.js").CommandsPort;
51
- }
52
-
53
- // ============ PlatformAdapter (global singleton — leaf utilities) ============
54
-
55
- /** Platform adapter that leaf utilities use for framework-specific operations. */
56
- export interface PlatformAdapter {
57
- /** Validate that a remote URL is safe to fetch (SSRF protection). */
58
- validateRemoteUrl(url: string, options?: { allowPrivate?: boolean }): Promise<void>;
59
-
60
- /** Resolve a secret value (SecretInput or plain string) to a plain string. */
61
- resolveSecret(value: string | SecretInputRef | undefined): Promise<string | undefined>;
62
-
63
- /** Download a remote file to a local directory. Returns the local file path. */
64
- downloadFile(url: string, destDir: string, filename?: string): Promise<string>;
65
-
66
- /**
67
- * Fetch remote media with SSRF protection.
68
- * Replaces direct usage of `fetchRemoteMedia` from `plugin-sdk/media-runtime`.
69
- */
70
- fetchMedia(options: FetchMediaOptions): Promise<FetchMediaResult>;
71
-
72
- /** Return the preferred temporary directory for the platform. */
73
- getTempDir(): string;
74
-
75
- /** Check whether a secret input value has been configured (non-empty). */
76
- hasConfiguredSecret(value: unknown): boolean;
77
-
78
- /**
79
- * Normalize a raw SecretInput value into a plain string.
80
- * For unresolved references (e.g. `$secret:xxx`), returns the raw reference string.
81
- */
82
- normalizeSecretInputString(value: unknown): string | undefined;
83
-
84
- /**
85
- * Resolve a SecretInput value into the final plain-text secret.
86
- * For secret references, resolves them to actual values via the platform's secret store.
87
- */
88
- resolveSecretInputString(params: { value: unknown; path: string }): string | undefined;
89
-
90
- /**
91
- * Submit an approval decision to the framework's approval gateway.
92
- * Optional — only available when the framework supports approvals.
93
- * Returns true if the decision was submitted successfully.
94
- */
95
- resolveApproval?(approvalId: string, decision: string): Promise<boolean>;
96
- }
97
-
98
- let _adapter: PlatformAdapter | null = null;
99
- let _adapterFactory: (() => PlatformAdapter) | null = null;
100
-
101
- /** Register the platform adapter. Called once during startup. */
102
- export function registerPlatformAdapter(adapter: PlatformAdapter): void {
103
- _adapter = adapter;
104
- }
105
-
106
- /**
107
- * Register a factory that creates the PlatformAdapter on first access.
108
- *
109
- * This decouples adapter availability from side-effect import ordering.
110
- * The factory is invoked at most once — on the first `getPlatformAdapter()`
111
- * call when no adapter has been explicitly registered yet.
112
- */
113
- export function registerPlatformAdapterFactory(factory: () => PlatformAdapter): void {
114
- _adapterFactory = factory;
115
- }
116
-
117
- /**
118
- * Get the registered platform adapter.
119
- *
120
- * If no adapter has been explicitly registered yet but a factory was provided
121
- * via `registerPlatformAdapterFactory()`, the factory is invoked to create
122
- * and register the adapter automatically.
123
- */
124
- export function getPlatformAdapter(): PlatformAdapter {
125
- if (!_adapter && _adapterFactory) {
126
- _adapter = _adapterFactory();
127
- }
128
- if (!_adapter) {
129
- throw new Error(
130
- "PlatformAdapter not registered. Call registerPlatformAdapter() during bootstrap.",
131
- );
132
- }
133
- return _adapter;
134
- }
135
-
136
- /** Check whether a platform adapter has been registered (or can be created from a factory). */
137
- export function hasPlatformAdapter(): boolean {
138
- return _adapter !== null || _adapterFactory !== null;
139
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * Mention gate port — abstracts the SDK's `resolveInboundMentionDecision`
3
- * + `resolveControlCommandGate` into a single interface.
4
- *
5
- * The engine's `resolveGroupMessageGate` (Layer 1: ignoreOtherMentions)
6
- * is QQ-specific and stays in `group/message-gating.ts`. Layer 2+3
7
- * (command gating + mention gating + command bypass) delegate to this port.
8
- */
9
-
10
- /** Implicit mention kind aligned with SDK's `InboundImplicitMentionKind`. */
11
- type ImplicitMentionKind = "reply_to_bot" | "quoted_bot" | "bot_thread_participant" | "native";
12
-
13
- /** Facts about the current message's mention state. */
14
- export interface MentionFacts {
15
- canDetectMention: boolean;
16
- wasMentioned: boolean;
17
- hasAnyMention?: boolean;
18
- implicitMentionKinds?: readonly ImplicitMentionKind[];
19
- }
20
-
21
- /** Policy configuration for the mention gate. */
22
- export interface MentionPolicy {
23
- isGroup: boolean;
24
- requireMention: boolean;
25
- allowTextCommands: boolean;
26
- hasControlCommand: boolean;
27
- commandAuthorized: boolean;
28
- }
29
-
30
- /** Result of the mention gate evaluation. */
31
- export interface MentionGateDecision {
32
- effectiveWasMentioned: boolean;
33
- shouldSkip: boolean;
34
- shouldBypassMention: boolean;
35
- implicitMention: boolean;
36
- }
37
-
38
- export interface MentionGatePort {
39
- /**
40
- * Evaluate whether the message should be skipped based on mention
41
- * policy, command bypass, and implicit mention rules.
42
- *
43
- * Equivalent to SDK's `resolveInboundMentionDecision` with the
44
- * command-bypass logic folded in.
45
- */
46
- resolveInboundMentionDecision(params: {
47
- facts: MentionFacts;
48
- policy: MentionPolicy;
49
- }): MentionGateDecision;
50
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * Shared types used by the PlatformAdapter interface.
3
- */
4
-
5
- /** Reference to a secret stored in the platform's secret management system. */
6
- export interface SecretInputRef {
7
- source: "env" | "file" | "config";
8
- id: string;
9
- }
10
-
11
- /** Options for fetching remote media through the platform adapter. */
12
- export interface FetchMediaOptions {
13
- url: string;
14
- /** Hint for the local filename when saving. */
15
- filePathHint?: string;
16
- /** Maximum bytes to download. */
17
- maxBytes?: number;
18
- /** Maximum redirects to follow. */
19
- maxRedirects?: number;
20
- /** SSRF policy configuration. */
21
- ssrfPolicy?: SsrfPolicyConfig;
22
- /** Extra fetch() RequestInit options. */
23
- requestInit?: RequestInit;
24
- }
25
-
26
- /** Result of a remote media fetch operation. */
27
- export interface FetchMediaResult {
28
- buffer: Buffer;
29
- fileName?: string;
30
- }
31
-
32
- /** SSRF policy configuration — platform-agnostic subset. */
33
- export interface SsrfPolicyConfig {
34
- /** Hostnames that are always allowed (supports `*.example.com` wildcards). */
35
- hostnameAllowlist?: string[];
36
- /** Whether to allow RFC 2544 benchmark ranges (198.18.0.0/15). */
37
- allowRfc2544BenchmarkRange?: boolean;
38
- }
@@ -1,212 +0,0 @@
1
- /**
2
- * Core HTTP client for the QQ Open Platform REST API.
3
- *
4
- * Key improvements over the old `src/api.ts#apiRequest`:
5
- * - `ApiClient` is an **instance** — config (baseUrl, timeout, logger, UA)
6
- * is injected via the constructor, eliminating module-level globals.
7
- * - Throws structured `ApiError` with httpStatus, bizCode, and path fields.
8
- * - Detects HTML error pages from CDN/gateway and returns user-friendly messages.
9
- * - `redactBodyKeys` replaces the hardcoded `file_data` redaction.
10
- */
11
-
12
- import { ApiError, type ApiClientConfig, type EngineLogger } from "../types.js";
13
- import { formatErrorMessage } from "../utils/format.js";
14
-
15
- const DEFAULT_BASE_URL = "https://api.sgroup.qq.com";
16
- const DEFAULT_TIMEOUT_MS = 30_000;
17
- const FILE_UPLOAD_TIMEOUT_MS = 120_000;
18
-
19
- interface RequestOptions {
20
- /** Request timeout override in milliseconds. */
21
- timeoutMs?: number;
22
- /** Body keys to redact in debug logs (e.g. `['file_data']`). */
23
- redactBodyKeys?: string[];
24
- /**
25
- * Mark the request as a file-upload call.
26
- *
27
- * Triggers the longer `fileUploadTimeoutMs` (default 120s) instead of the
28
- * standard `defaultTimeoutMs` (default 30s). Prefer this flag over
29
- * inspecting the request path; it keeps the timeout policy independent of
30
- * route naming conventions.
31
- */
32
- uploadRequest?: boolean;
33
- }
34
-
35
- /**
36
- * Stateful HTTP client for the QQ Open Platform.
37
- *
38
- * Usage:
39
- * ```ts
40
- * const client = new ApiClient({ logger, userAgent: 'QQBotPlugin/1.0' });
41
- * const data = await client.request<{ url: string }>(token, 'GET', '/gateway');
42
- * ```
43
- */
44
- export class ApiClient {
45
- private readonly baseUrl: string;
46
- private readonly defaultTimeoutMs: number;
47
- private readonly fileUploadTimeoutMs: number;
48
- private readonly logger?: EngineLogger;
49
- private readonly resolveUserAgent: () => string;
50
-
51
- constructor(config: ApiClientConfig = {}) {
52
- this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
53
- this.defaultTimeoutMs = config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
54
- this.fileUploadTimeoutMs = config.fileUploadTimeoutMs ?? FILE_UPLOAD_TIMEOUT_MS;
55
- this.logger = config.logger;
56
- const ua = config.userAgent ?? "QQBotPlugin/unknown";
57
- this.resolveUserAgent = typeof ua === "function" ? ua : () => ua;
58
- }
59
-
60
- /**
61
- * Send an authenticated JSON request to the QQ Open Platform.
62
- *
63
- * @param accessToken - Bearer token (`QQBot {token}`).
64
- * @param method - HTTP method.
65
- * @param path - API path (appended to baseUrl).
66
- * @param body - Optional JSON body.
67
- * @param options - Optional request overrides.
68
- * @returns Parsed JSON response.
69
- * @throws {ApiError} On HTTP or parse errors.
70
- */
71
- async request<T = unknown>(
72
- accessToken: string,
73
- method: string,
74
- path: string,
75
- body?: unknown,
76
- options?: RequestOptions,
77
- ): Promise<T> {
78
- const url = `${this.baseUrl}${path}`;
79
-
80
- const headers: Record<string, string> = {
81
- Authorization: `QQBot ${accessToken}`,
82
- "Content-Type": "application/json",
83
- "User-Agent": this.resolveUserAgent(),
84
- };
85
-
86
- const isFileUpload =
87
- options?.uploadRequest === true ||
88
- // Back-compat: legacy callers that predate the explicit `uploadRequest`
89
- // flag still get the long timeout when hitting file endpoints. New
90
- // code should always pass `uploadRequest: true` explicitly.
91
- path.includes("/files") ||
92
- path.includes("/upload_prepare") ||
93
- path.includes("/upload_part_finish");
94
- const timeout =
95
- options?.timeoutMs ?? (isFileUpload ? this.fileUploadTimeoutMs : this.defaultTimeoutMs);
96
-
97
- const controller = new AbortController();
98
- const timeoutId = setTimeout(() => controller.abort(), timeout);
99
-
100
- const fetchInit: RequestInit = {
101
- method,
102
- headers,
103
- signal: controller.signal,
104
- };
105
-
106
- if (body) {
107
- fetchInit.body = JSON.stringify(body);
108
- }
109
-
110
- // Debug logging with optional body redaction.
111
- this.logger?.debug?.(`[qqbot:api] >>> ${method} ${url} (timeout: ${timeout}ms)`);
112
- if (body && this.logger?.debug) {
113
- const logBody = { ...(body as Record<string, unknown>) };
114
- for (const key of options?.redactBodyKeys ?? ["file_data"]) {
115
- if (typeof logBody[key] === "string") {
116
- logBody[key] = `<redacted ${logBody[key].length} chars>`;
117
- }
118
- }
119
- this.logger.debug(`[qqbot:api] >>> Body: ${JSON.stringify(logBody)}`);
120
- }
121
-
122
- let res: Response;
123
- try {
124
- res = await fetch(url, fetchInit);
125
- } catch (err) {
126
- clearTimeout(timeoutId);
127
- if (err instanceof Error && err.name === "AbortError") {
128
- this.logger?.error?.(`[qqbot:api] <<< Timeout after ${timeout}ms`);
129
- throw new ApiError(`Request timeout [${path}]: exceeded ${timeout}ms`, 0, path);
130
- }
131
- this.logger?.error?.(`[qqbot:api] <<< Network error: ${formatErrorMessage(err)}`);
132
- throw new ApiError(`Network error [${path}]: ${formatErrorMessage(err)}`, 0, path);
133
- } finally {
134
- clearTimeout(timeoutId);
135
- }
136
-
137
- // Log response status and trace ID.
138
- const traceId = res.headers.get("x-tps-trace-id") ?? "";
139
- this.logger?.info?.(
140
- `[qqbot:api] <<< Status: ${res.status} ${res.statusText}${traceId ? ` | TraceId: ${traceId}` : ""}`,
141
- );
142
-
143
- let rawBody: string;
144
- try {
145
- rawBody = await res.text();
146
- } catch (err) {
147
- throw new ApiError(
148
- `Failed to read response [${path}]: ${formatErrorMessage(err)}`,
149
- res.status,
150
- path,
151
- );
152
- }
153
- this.logger?.debug?.(`[qqbot:api] <<< Body: ${rawBody}`);
154
-
155
- // Detect non-JSON responses (HTML gateway errors, CDN rate-limit pages).
156
- const contentType = res.headers.get("content-type") ?? "";
157
- const isHtmlResponse = contentType.includes("text/html") || rawBody.trimStart().startsWith("<");
158
-
159
- if (!res.ok) {
160
- if (isHtmlResponse) {
161
- const statusHint =
162
- res.status === 502 || res.status === 503 || res.status === 504
163
- ? "调用发生异常,请稍候重试"
164
- : res.status === 429
165
- ? "请求过于频繁,已被限流"
166
- : `开放平台返回 HTTP ${res.status}`;
167
- throw new ApiError(`${statusHint}(${path}),请稍后重试`, res.status, path);
168
- }
169
-
170
- // JSON error response.
171
- try {
172
- const error = JSON.parse(rawBody) as {
173
- message?: string;
174
- code?: number;
175
- err_code?: number;
176
- };
177
- const bizCode = error.code ?? error.err_code;
178
- throw new ApiError(
179
- `API Error [${path}]: ${error.message ?? rawBody}`,
180
- res.status,
181
- path,
182
- bizCode,
183
- error.message,
184
- );
185
- } catch (parseErr) {
186
- if (parseErr instanceof ApiError) {
187
- throw parseErr;
188
- }
189
- throw new ApiError(
190
- `API Error [${path}] HTTP ${res.status}: ${rawBody.slice(0, 200)}`,
191
- res.status,
192
- path,
193
- );
194
- }
195
- }
196
-
197
- // Successful response but not JSON (extreme edge case).
198
- if (isHtmlResponse) {
199
- throw new ApiError(
200
- `QQ 服务端返回了非 JSON 响应(${path}),可能是临时故障,请稍后重试`,
201
- res.status,
202
- path,
203
- );
204
- }
205
-
206
- try {
207
- return JSON.parse(rawBody) as T;
208
- } catch {
209
- throw new ApiError(`开放平台响应格式异常(${path}),请稍后重试`, res.status, path);
210
- }
211
- }
212
- }