@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,204 +0,0 @@
1
- /**
2
- * Gateway session persistence — JSONL file-based store.
3
- *
4
- * Migrated from src/session-store.ts. Dependencies are only Node.js
5
- * built-ins + log + platform (both zero plugin-sdk).
6
- */
7
-
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import { formatErrorMessage } from "../utils/format.js";
11
- import { debugLog, debugError } from "../utils/log.js";
12
- import { getQQBotDataDir, getQQBotDataPath } from "../utils/platform.js";
13
-
14
- /** Persisted gateway session state. */
15
- export interface SessionState {
16
- sessionId: string | null;
17
- lastSeq: number | null;
18
- lastConnectedAt: number;
19
- intentLevelIndex: number;
20
- accountId: string;
21
- savedAt: number;
22
- appId?: string;
23
- }
24
-
25
- const SESSION_EXPIRE_TIME = 5 * 60 * 1000;
26
- const SAVE_THROTTLE_MS = 1000;
27
-
28
- const throttleState = new Map<
29
- string,
30
- {
31
- pendingState: SessionState | null;
32
- lastSaveTime: number;
33
- throttleTimer: ReturnType<typeof setTimeout> | null;
34
- }
35
- >();
36
-
37
- function ensureDir(): void {
38
- getQQBotDataDir("sessions");
39
- }
40
-
41
- function getSessionDir(): string {
42
- return getQQBotDataPath("sessions");
43
- }
44
-
45
- function encodeAccountIdForFileName(accountId: string): string {
46
- return Buffer.from(accountId, "utf8").toString("base64url");
47
- }
48
-
49
- function getLegacySessionPath(accountId: string): string {
50
- const safeId = accountId.replace(/[^a-zA-Z0-9_-]/g, "_");
51
- return path.join(getSessionDir(), `session-${safeId}.json`);
52
- }
53
-
54
- function getSessionPath(accountId: string): string {
55
- const encodedId = encodeAccountIdForFileName(accountId);
56
- return path.join(getSessionDir(), `session-${encodedId}.json`);
57
- }
58
-
59
- function getCandidateSessionPaths(accountId: string): string[] {
60
- const primaryPath = getSessionPath(accountId);
61
- const legacyPath = getLegacySessionPath(accountId);
62
- return primaryPath === legacyPath ? [primaryPath] : [primaryPath, legacyPath];
63
- }
64
-
65
- /** Load a saved session, rejecting expired or mismatched appId entries. */
66
- export function loadSession(accountId: string, expectedAppId?: string): SessionState | null {
67
- try {
68
- let filePath: string | null = null;
69
- for (const candidatePath of getCandidateSessionPaths(accountId)) {
70
- if (fs.existsSync(candidatePath)) {
71
- filePath = candidatePath;
72
- break;
73
- }
74
- }
75
- if (!filePath) {
76
- return null;
77
- }
78
-
79
- const data = fs.readFileSync(filePath, "utf-8");
80
- const state = JSON.parse(data) as SessionState;
81
- const now = Date.now();
82
-
83
- if (now - state.savedAt > SESSION_EXPIRE_TIME) {
84
- debugLog(
85
- `[session-store] Session expired for ${accountId}, age: ${Math.round((now - state.savedAt) / 1000)}s`,
86
- );
87
- try {
88
- fs.unlinkSync(filePath);
89
- } catch {}
90
- return null;
91
- }
92
-
93
- if (expectedAppId && state.appId && state.appId !== expectedAppId) {
94
- debugLog(
95
- `[session-store] appId mismatch for ${accountId}: saved=${state.appId}, current=${expectedAppId}. Discarding stale session.`,
96
- );
97
- try {
98
- fs.unlinkSync(filePath);
99
- } catch {}
100
- return null;
101
- }
102
-
103
- if (!state.sessionId || state.lastSeq === null || state.lastSeq === undefined) {
104
- debugLog(`[session-store] Invalid session data for ${accountId}`);
105
- return null;
106
- }
107
-
108
- debugLog(
109
- `[session-store] Loaded session for ${accountId}: sessionId=${state.sessionId}, lastSeq=${state.lastSeq}, appId=${state.appId ?? "unknown"}, age=${Math.round((now - state.savedAt) / 1000)}s`,
110
- );
111
- return state;
112
- } catch (err) {
113
- debugError(
114
- `[session-store] Failed to load session for ${accountId}: ${formatErrorMessage(err)}`,
115
- );
116
- return null;
117
- }
118
- }
119
-
120
- /** Save session state with throttling. */
121
- export function saveSession(state: SessionState): void {
122
- const { accountId } = state;
123
- let throttle = throttleState.get(accountId);
124
- if (!throttle) {
125
- throttle = { pendingState: null, lastSaveTime: 0, throttleTimer: null };
126
- throttleState.set(accountId, throttle);
127
- }
128
-
129
- const now = Date.now();
130
- const timeSinceLastSave = now - throttle.lastSaveTime;
131
-
132
- if (timeSinceLastSave >= SAVE_THROTTLE_MS) {
133
- doSaveSession(state);
134
- throttle.lastSaveTime = now;
135
- throttle.pendingState = null;
136
- if (throttle.throttleTimer) {
137
- clearTimeout(throttle.throttleTimer);
138
- throttle.throttleTimer = null;
139
- }
140
- } else {
141
- throttle.pendingState = state;
142
- if (!throttle.throttleTimer) {
143
- const delay = SAVE_THROTTLE_MS - timeSinceLastSave;
144
- throttle.throttleTimer = setTimeout(() => {
145
- const t = throttleState.get(accountId);
146
- if (t?.pendingState) {
147
- doSaveSession(t.pendingState);
148
- t.lastSaveTime = Date.now();
149
- t.pendingState = null;
150
- }
151
- if (t) {
152
- t.throttleTimer = null;
153
- }
154
- }, delay);
155
- }
156
- }
157
- }
158
-
159
- function doSaveSession(state: SessionState): void {
160
- const filePath = getSessionPath(state.accountId);
161
- const legacyPath = getLegacySessionPath(state.accountId);
162
- try {
163
- ensureDir();
164
- const stateToSave: SessionState = { ...state, savedAt: Date.now() };
165
- fs.writeFileSync(filePath, JSON.stringify(stateToSave, null, 2), "utf-8");
166
- if (legacyPath !== filePath && fs.existsSync(legacyPath)) {
167
- fs.unlinkSync(legacyPath);
168
- }
169
- debugLog(
170
- `[session-store] Saved session for ${state.accountId}: sessionId=${state.sessionId}, lastSeq=${state.lastSeq}`,
171
- );
172
- } catch (err) {
173
- debugError(
174
- `[session-store] Failed to save session for ${state.accountId}: ${formatErrorMessage(err)}`,
175
- );
176
- }
177
- }
178
-
179
- /** Clear a saved session and any pending throttle state. */
180
- export function clearSession(accountId: string): void {
181
- const throttle = throttleState.get(accountId);
182
- if (throttle) {
183
- if (throttle.throttleTimer) {
184
- clearTimeout(throttle.throttleTimer);
185
- }
186
- throttleState.delete(accountId);
187
- }
188
- try {
189
- let cleared = false;
190
- for (const filePath of getCandidateSessionPaths(accountId)) {
191
- if (fs.existsSync(filePath)) {
192
- fs.unlinkSync(filePath);
193
- cleared = true;
194
- }
195
- }
196
- if (cleared) {
197
- debugLog(`[session-store] Cleared session for ${accountId}`);
198
- }
199
- } catch (err) {
200
- debugError(
201
- `[session-store] Failed to clear session for ${accountId}: ${formatErrorMessage(err)}`,
202
- );
203
- }
204
- }
@@ -1,244 +0,0 @@
1
- /**
2
- * QQ Channel API proxy tool core logic.
3
- * QQ 频道 API 代理工具核心逻辑。
4
- *
5
- * Provides an authenticated HTTP proxy for the QQ Open Platform channel
6
- * APIs. The caller (old tools/channel.ts shell) resolves the access
7
- * token and passes it in; this module handles URL building, path
8
- * validation, fetch, and structured response formatting.
9
- */
10
-
11
- import { formatErrorMessage } from "../utils/format.js";
12
- import { debugLog, debugError } from "../utils/log.js";
13
-
14
- const API_BASE = "https://api.sgroup.qq.com";
15
- const DEFAULT_TIMEOUT_MS = 30000;
16
-
17
- /**
18
- * Channel API call parameters.
19
- * 频道 API 调用参数。
20
- */
21
- export interface ChannelApiParams {
22
- method: string;
23
- path: string;
24
- body?: Record<string, unknown>;
25
- query?: Record<string, string>;
26
- }
27
-
28
- /**
29
- * JSON Schema for AI tool parameters (used by framework registration).
30
- * AI Tool 参数的 JSON Schema 定义(供框架注册使用)。
31
- */
32
- export const ChannelApiSchema = {
33
- type: "object",
34
- properties: {
35
- method: {
36
- type: "string",
37
- description: "HTTP method. Allowed values: GET, POST, PUT, PATCH, DELETE.",
38
- enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
39
- },
40
- path: {
41
- type: "string",
42
- description:
43
- "API path without the host. Replace placeholders with concrete values. " +
44
- "Examples: /users/@me/guilds, /guilds/{guild_id}/channels, /channels/{channel_id}.",
45
- },
46
- body: {
47
- type: "object",
48
- description:
49
- "JSON request body for POST/PUT/PATCH requests. GET/DELETE usually do not need it.",
50
- },
51
- query: {
52
- type: "object",
53
- description:
54
- "URL query parameters as key/value pairs appended to the path. " +
55
- 'For example, { "limit": "100", "after": "0" } becomes ?limit=100&after=0.',
56
- additionalProperties: { type: "string" },
57
- },
58
- },
59
- required: ["method", "path"],
60
- } as const;
61
-
62
- /**
63
- * Build the full API URL from base + path + query params.
64
- * 拼接 API 基地址 + 路径 + 查询参数。
65
- */
66
- function buildUrl(path: string, query?: Record<string, string>): string {
67
- let url = `${API_BASE}${path}`;
68
- if (query && Object.keys(query).length > 0) {
69
- const params = new URLSearchParams();
70
- for (const [key, value] of Object.entries(query)) {
71
- if (value !== undefined && value !== null && value !== "") {
72
- params.set(key, value);
73
- }
74
- }
75
- const qs = params.toString();
76
- if (qs) {
77
- url += `?${qs}`;
78
- }
79
- }
80
- return url;
81
- }
82
-
83
- /**
84
- * Validate API path format; returns an error string or null if valid.
85
- * 校验 API 路径格式,返回错误描述或 null(合法)。
86
- */
87
- function validatePath(path: string): string | null {
88
- if (!path.startsWith("/")) {
89
- return "path must start with /";
90
- }
91
- if (path.includes("..") || path.includes("//")) {
92
- return "path must not contain .. or //";
93
- }
94
- if (!/^\/[a-zA-Z0-9\-._~:@!$&'()*+,;=/%]+$/.test(path) && path !== "/") {
95
- return "path contains unsupported characters";
96
- }
97
- return null;
98
- }
99
-
100
- function json(data: unknown) {
101
- return {
102
- content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
103
- details: data,
104
- };
105
- }
106
-
107
- /**
108
- * Options provided by the caller when executing a channel API request.
109
- * 执行频道 API 请求时由调用方提供的选项。
110
- */
111
- interface ChannelApiExecuteOptions {
112
- accessToken: string;
113
- }
114
-
115
- /**
116
- * Execute a channel API proxy request.
117
- * 执行频道 API 代理请求。
118
- *
119
- * The caller provides the access token; this function handles
120
- * URL building, path validation, HTTP fetch, and structured
121
- * response formatting suitable for AI tool output.
122
- */
123
- export async function executeChannelApi(
124
- params: ChannelApiParams,
125
- options: ChannelApiExecuteOptions,
126
- ) {
127
- if (!params.method) {
128
- return json({ error: "method is required" });
129
- }
130
- if (!params.path) {
131
- return json({ error: "path is required" });
132
- }
133
-
134
- const method = params.method.toUpperCase();
135
- if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
136
- return json({
137
- error: `Unsupported HTTP method: ${method}. Allowed values: GET, POST, PUT, PATCH, DELETE`,
138
- });
139
- }
140
-
141
- const pathError = validatePath(params.path);
142
- if (pathError) {
143
- return json({ error: pathError });
144
- }
145
-
146
- if (
147
- (method === "GET" || method === "DELETE") &&
148
- params.body &&
149
- Object.keys(params.body).length > 0
150
- ) {
151
- debugLog(`[qqbot-channel-api] ${method} request with body, body will be ignored`);
152
- }
153
-
154
- try {
155
- const url = buildUrl(params.path, params.query);
156
- const headers: Record<string, string> = {
157
- Authorization: `QQBot ${options.accessToken}`,
158
- "Content-Type": "application/json",
159
- };
160
-
161
- const controller = new AbortController();
162
- const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
163
-
164
- const fetchOptions: RequestInit = {
165
- method,
166
- headers,
167
- signal: controller.signal,
168
- };
169
-
170
- if (params.body && ["POST", "PUT", "PATCH"].includes(method)) {
171
- fetchOptions.body = JSON.stringify(params.body);
172
- }
173
-
174
- debugLog(`[qqbot-channel-api] >>> ${method} ${url} (timeout: ${DEFAULT_TIMEOUT_MS}ms)`);
175
-
176
- let res: Response;
177
- try {
178
- res = await fetch(url, fetchOptions);
179
- } catch (err) {
180
- clearTimeout(timeoutId);
181
- if (err instanceof Error && err.name === "AbortError") {
182
- debugError(`[qqbot-channel-api] <<< Request timeout after ${DEFAULT_TIMEOUT_MS}ms`);
183
- return json({
184
- error: `Request timed out after ${DEFAULT_TIMEOUT_MS}ms`,
185
- path: params.path,
186
- });
187
- }
188
- debugError("[qqbot-channel-api] <<< Network error:", err);
189
- return json({
190
- error: `Network error: ${formatErrorMessage(err)}`,
191
- path: params.path,
192
- });
193
- } finally {
194
- clearTimeout(timeoutId);
195
- }
196
-
197
- debugLog(`[qqbot-channel-api] <<< Status: ${res.status} ${res.statusText}`);
198
-
199
- const rawBody = await res.text();
200
- if (!rawBody || rawBody.trim() === "") {
201
- if (res.ok) {
202
- return json({ success: true, status: res.status, path: params.path });
203
- }
204
- return json({
205
- error: `API returned ${res.status} ${res.statusText}`,
206
- status: res.status,
207
- path: params.path,
208
- });
209
- }
210
-
211
- let parsed: unknown;
212
- try {
213
- parsed = JSON.parse(rawBody);
214
- } catch {
215
- parsed = rawBody;
216
- }
217
-
218
- if (!res.ok) {
219
- const errMsg =
220
- typeof parsed === "object" && parsed && "message" in parsed
221
- ? String((parsed as { message?: unknown }).message)
222
- : `${res.status} ${res.statusText}`;
223
- debugError(`[qqbot-channel-api] Error [${method} ${params.path}]: ${errMsg}`);
224
- return json({
225
- error: errMsg,
226
- status: res.status,
227
- path: params.path,
228
- details: parsed,
229
- });
230
- }
231
-
232
- return json({
233
- success: true,
234
- status: res.status,
235
- path: params.path,
236
- data: parsed,
237
- });
238
- } catch (err) {
239
- return json({
240
- error: formatErrorMessage(err),
241
- path: params.path,
242
- });
243
- }
244
- }