@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
@@ -0,0 +1,1357 @@
1
+ import { i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, s as sanitizeFileName } from "./string-normalize-Ci6NM5DE.js";
2
+ import { A as fileExistsAsync, C as debugLog, D as UPLOAD_PREPARE_FALLBACK_CODE, F as readFileAsync, L as formatErrorMessage, M as getFileTypeName, N as getImageMimeType, O as checkFileSize, P as getMaxUploadSize, S as debugError, T as UploadDailyLimitExceededError, g as sendText$1, h as sendMedia$1, j as formatFileSize, k as downloadFile, t as accountToCreds, u as initApiConfig, w as debugWarn } from "./sender-p-B14eLG.js";
3
+ import { c as getQQBotMediaDir, d as isLocalPath, m as resolveQQBotPayloadLocalFilePath, o as getQQBotDataDir, p as normalizePath, r as parseTarget$1 } from "./target-parser-Y0prnrXD.js";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ //#region \0rolldown/runtime.js
7
+ var __defProp = Object.defineProperty;
8
+ var __exportAll = (all, no_symbols) => {
9
+ let target = {};
10
+ for (var name in all) __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true
13
+ });
14
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
15
+ return target;
16
+ };
17
+ //#endregion
18
+ //#region extensions/qqbot/src/engine/messaging/outbound-audio-port.ts
19
+ let _audioPort = null;
20
+ /**
21
+ * Initialize the outbound audio adapter. Called once by gateway startup
22
+ * via `adapters.outboundAudio`.
23
+ */
24
+ function setOutboundAudioPort(port) {
25
+ _audioPort = port;
26
+ }
27
+ function getAudio() {
28
+ if (!_audioPort) throw new Error("OutboundAudioPort not initialized — call setOutboundAudioPort first");
29
+ return _audioPort;
30
+ }
31
+ function audioFileToSilkBase64(p, f) {
32
+ return getAudio().audioFileToSilkBase64(p, f);
33
+ }
34
+ function isAudioFile(p, m) {
35
+ try {
36
+ return getAudio().isAudioFile(p, m);
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ function shouldTranscodeVoice(p) {
42
+ return getAudio().shouldTranscodeVoice(p);
43
+ }
44
+ function waitForFile(p, ms) {
45
+ return getAudio().waitForFile(p, ms);
46
+ }
47
+ //#endregion
48
+ //#region extensions/qqbot/src/engine/messaging/outbound-types.ts
49
+ /**
50
+ * Stable error codes for outbound media send results.
51
+ */
52
+ const OUTBOUND_ERROR_CODES = {
53
+ FILE_TOO_LARGE: "file_too_large",
54
+ UPLOAD_DAILY_LIMIT_EXCEEDED: "upload_daily_limit_exceeded"
55
+ };
56
+ const DEFAULT_MEDIA_SEND_ERROR = "发送失败,请稍后重试。";
57
+ //#endregion
58
+ //#region extensions/qqbot/src/engine/messaging/reply-limiter.ts
59
+ const DEFAULT_LIMIT = 4;
60
+ const DEFAULT_TTL_MS = 3600 * 1e3;
61
+ const DEFAULT_MAX_TRACKED = 1e4;
62
+ /**
63
+ * Per-account reply limiter with automatic eviction.
64
+ *
65
+ * Usage:
66
+ * ```ts
67
+ * const limiter = new ReplyLimiter({ limit: 4, ttlMs: 3600000 });
68
+ * const check = limiter.checkLimit(messageId);
69
+ * if (check.allowed) {
70
+ * await sendPassiveReply(...);
71
+ * limiter.record(messageId);
72
+ * } else if (check.shouldFallbackToProactive) {
73
+ * await sendProactiveMessage(...);
74
+ * }
75
+ * ```
76
+ */
77
+ var ReplyLimiter = class {
78
+ constructor(config) {
79
+ this.tracker = /* @__PURE__ */ new Map();
80
+ this.limit = config?.limit ?? DEFAULT_LIMIT;
81
+ this.ttlMs = config?.ttlMs ?? DEFAULT_TTL_MS;
82
+ this.maxTracked = config?.maxTrackedMessages ?? DEFAULT_MAX_TRACKED;
83
+ }
84
+ /** Check whether a passive reply is allowed for the given message. */
85
+ checkLimit(messageId) {
86
+ const now = Date.now();
87
+ this.evictIfNeeded(now);
88
+ const record = this.tracker.get(messageId);
89
+ if (!record) return {
90
+ allowed: true,
91
+ remaining: this.limit,
92
+ shouldFallbackToProactive: false
93
+ };
94
+ if (now - record.firstReplyAt > this.ttlMs) return {
95
+ allowed: false,
96
+ remaining: 0,
97
+ shouldFallbackToProactive: true,
98
+ fallbackReason: "expired",
99
+ message: `Message is older than ${this.ttlMs / (3600 * 1e3)}h; sending as a proactive message instead`
100
+ };
101
+ const remaining = this.limit - record.count;
102
+ if (remaining <= 0) return {
103
+ allowed: false,
104
+ remaining: 0,
105
+ shouldFallbackToProactive: true,
106
+ fallbackReason: "limit_exceeded",
107
+ message: `Passive reply limit reached (${this.limit} per hour); sending proactively instead`
108
+ };
109
+ return {
110
+ allowed: true,
111
+ remaining,
112
+ shouldFallbackToProactive: false
113
+ };
114
+ }
115
+ /** Record one passive reply against a message. */
116
+ record(messageId) {
117
+ const now = Date.now();
118
+ const existing = this.tracker.get(messageId);
119
+ if (!existing) this.tracker.set(messageId, {
120
+ count: 1,
121
+ firstReplyAt: now
122
+ });
123
+ else if (now - existing.firstReplyAt > this.ttlMs) this.tracker.set(messageId, {
124
+ count: 1,
125
+ firstReplyAt: now
126
+ });
127
+ else existing.count++;
128
+ }
129
+ /** Return diagnostic stats. */
130
+ getStats() {
131
+ let totalReplies = 0;
132
+ for (const record of this.tracker.values()) totalReplies += record.count;
133
+ return {
134
+ trackedMessages: this.tracker.size,
135
+ totalReplies
136
+ };
137
+ }
138
+ /** Return limiter configuration. */
139
+ getConfig() {
140
+ return {
141
+ limit: this.limit,
142
+ ttlMs: this.ttlMs,
143
+ ttlHours: this.ttlMs / (3600 * 1e3)
144
+ };
145
+ }
146
+ /** Clear all tracked records. */
147
+ clear() {
148
+ this.tracker.clear();
149
+ }
150
+ /** Opportunistically evict expired records to keep the tracker bounded. */
151
+ evictIfNeeded(now) {
152
+ if (this.tracker.size <= this.maxTracked) return;
153
+ for (const [id, rec] of this.tracker) if (now - rec.firstReplyAt > this.ttlMs) this.tracker.delete(id);
154
+ }
155
+ };
156
+ //#endregion
157
+ //#region extensions/qqbot/src/engine/messaging/outbound-reply.ts
158
+ const replyLimiter = new ReplyLimiter();
159
+ const MESSAGE_REPLY_LIMIT = 4;
160
+ function checkMessageReplyLimit(messageId) {
161
+ return replyLimiter.checkLimit(messageId);
162
+ }
163
+ function recordMessageReply(messageId) {
164
+ replyLimiter.record(messageId);
165
+ debugLog(`[qqbot] recordMessageReply: ${messageId}, count=${replyLimiter.getStats().totalReplies}`);
166
+ }
167
+ function getMessageReplyStats() {
168
+ return replyLimiter.getStats();
169
+ }
170
+ function getMessageReplyConfig() {
171
+ return replyLimiter.getConfig();
172
+ }
173
+ //#endregion
174
+ //#region extensions/qqbot/src/engine/messaging/outbound-result-helpers.ts
175
+ /**
176
+ * Convert a media send result into a user-facing message.
177
+ */
178
+ function resolveUserFacingMediaError(result) {
179
+ if (!result.error) return DEFAULT_MEDIA_SEND_ERROR;
180
+ if (result.qqBizCode === 40093002) return result.error;
181
+ switch (result.errorCode) {
182
+ case OUTBOUND_ERROR_CODES.FILE_TOO_LARGE:
183
+ case OUTBOUND_ERROR_CODES.UPLOAD_DAILY_LIMIT_EXCEEDED: return result.error;
184
+ default: return DEFAULT_MEDIA_SEND_ERROR;
185
+ }
186
+ }
187
+ function buildDailyLimitExceededResult(err) {
188
+ return {
189
+ channel: "qqbot",
190
+ error: `QQBot每天发送文件有累计2G的限制,如果着急的话,可以直接来我的主机copy下载,文件目录\`${path.dirname(err.filePath)}/${path.basename(err.filePath)}\`(${formatFileSize(err.fileSize)})`,
191
+ errorCode: OUTBOUND_ERROR_CODES.UPLOAD_DAILY_LIMIT_EXCEEDED,
192
+ qqBizCode: UPLOAD_PREPARE_FALLBACK_CODE
193
+ };
194
+ }
195
+ function buildFileTooLargeResult(fileType, fileSize) {
196
+ const typeName = getFileTypeName(fileType);
197
+ const limit = getMaxUploadSize(fileType);
198
+ const limitMB = Math.round(limit / (1024 * 1024));
199
+ return {
200
+ channel: "qqbot",
201
+ error: `${typeName}过大(${formatFileSize(fileSize)}),超过了${limitMB}M,暂时不能通过QQ直接发给你。`,
202
+ errorCode: OUTBOUND_ERROR_CODES.FILE_TOO_LARGE
203
+ };
204
+ }
205
+ //#endregion
206
+ //#region extensions/qqbot/src/engine/messaging/outbound-media-send.ts
207
+ /**
208
+ * Low-level outbound media sends (photo, voice, video, document) and path resolution.
209
+ */
210
+ /** Parse a qqbot target into a structured delivery target. */
211
+ function parseTarget(to) {
212
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
213
+ debugLog(`[${timestamp}] [qqbot] parseTarget: input=${to}`);
214
+ const parsed = parseTarget$1(to);
215
+ debugLog(`[${timestamp}] [qqbot] parseTarget: ${parsed.type} target, ID=${parsed.id}`);
216
+ return parsed;
217
+ }
218
+ /** Build a media target from a normal outbound context. */
219
+ function buildMediaTarget(ctx) {
220
+ const target = parseTarget(ctx.to);
221
+ return {
222
+ targetType: target.type,
223
+ targetId: target.id,
224
+ account: ctx.account,
225
+ replyToId: ctx.replyToId ?? void 0
226
+ };
227
+ }
228
+ /** Return true when public URLs should be passed through directly. */
229
+ function shouldDirectUploadUrl(account) {
230
+ return account.config?.urlDirectUpload !== false;
231
+ }
232
+ const qqBotMediaKindLabel = {
233
+ image: "Image",
234
+ voice: "Voice",
235
+ video: "Video",
236
+ file: "File",
237
+ media: "Media"
238
+ };
239
+ function isHttpOrDataSource(pathValue) {
240
+ return pathValue.startsWith("http://") || pathValue.startsWith("https://") || pathValue.startsWith("data:");
241
+ }
242
+ function isPathWithinRoot(candidate, root) {
243
+ const relative = path.relative(root, candidate);
244
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
245
+ }
246
+ function resolveMissingPathWithinMediaRoot(normalizedPath) {
247
+ const resolvedCandidate = path.resolve(normalizedPath);
248
+ if (fs.existsSync(resolvedCandidate)) return null;
249
+ const allowedRoot = path.resolve(getQQBotMediaDir());
250
+ let canonicalAllowedRoot;
251
+ try {
252
+ canonicalAllowedRoot = fs.realpathSync(allowedRoot);
253
+ } catch {
254
+ return null;
255
+ }
256
+ const missingSegments = [];
257
+ let cursor = resolvedCandidate;
258
+ while (!fs.existsSync(cursor)) {
259
+ const parent = path.dirname(cursor);
260
+ if (parent === cursor) break;
261
+ missingSegments.unshift(path.basename(cursor));
262
+ cursor = parent;
263
+ }
264
+ if (!fs.existsSync(cursor)) return null;
265
+ let canonicalCursor;
266
+ try {
267
+ canonicalCursor = fs.realpathSync(cursor);
268
+ } catch {
269
+ return null;
270
+ }
271
+ const canonicalCandidate = missingSegments.length > 0 ? path.join(canonicalCursor, ...missingSegments) : canonicalCursor;
272
+ return isPathWithinRoot(canonicalCandidate, canonicalAllowedRoot) ? canonicalCandidate : null;
273
+ }
274
+ function resolveExistingPathWithinRoots(normalizedPath, allowedRoots) {
275
+ const resolvedCandidate = path.resolve(normalizedPath);
276
+ if (!fs.existsSync(resolvedCandidate)) return null;
277
+ let canonicalCandidate;
278
+ try {
279
+ canonicalCandidate = fs.realpathSync(resolvedCandidate);
280
+ } catch {
281
+ return null;
282
+ }
283
+ for (const root of allowedRoots) {
284
+ const resolvedRoot = path.resolve(root);
285
+ const canonicalRoot = fs.existsSync(resolvedRoot) ? fs.realpathSync(resolvedRoot) : resolvedRoot;
286
+ if (isPathWithinRoot(canonicalCandidate, canonicalRoot)) return canonicalCandidate;
287
+ }
288
+ return null;
289
+ }
290
+ function resolveOutboundMediaPath(rawPath, mediaKind, options = {}) {
291
+ const normalizedPath = normalizePath(rawPath);
292
+ if (isHttpOrDataSource(normalizedPath)) return {
293
+ ok: true,
294
+ mediaPath: normalizedPath
295
+ };
296
+ const allowedPath = resolveQQBotPayloadLocalFilePath(normalizedPath);
297
+ if (allowedPath) return {
298
+ ok: true,
299
+ mediaPath: allowedPath
300
+ };
301
+ if (options.extraLocalRoots && options.extraLocalRoots.length > 0) {
302
+ const extraAllowedPath = resolveExistingPathWithinRoots(normalizedPath, options.extraLocalRoots);
303
+ if (extraAllowedPath) return {
304
+ ok: true,
305
+ mediaPath: extraAllowedPath
306
+ };
307
+ }
308
+ if (options.allowMissingLocalPath) {
309
+ const allowedMissingPath = resolveMissingPathWithinMediaRoot(normalizedPath);
310
+ if (allowedMissingPath) return {
311
+ ok: true,
312
+ mediaPath: allowedMissingPath
313
+ };
314
+ }
315
+ debugWarn(`blocked local ${mediaKind} path outside QQ Bot media storage`);
316
+ return {
317
+ ok: false,
318
+ error: `${qqBotMediaKindLabel[mediaKind]} path must be inside QQ Bot media storage`
319
+ };
320
+ }
321
+ /**
322
+ * Send a photo from a local file, public URL, or Base64 data URL.
323
+ */
324
+ async function sendPhoto(ctx, imagePath) {
325
+ const resolvedMediaPath = resolveOutboundMediaPath(imagePath, "image");
326
+ if (!resolvedMediaPath.ok) return {
327
+ channel: "qqbot",
328
+ error: resolvedMediaPath.error
329
+ };
330
+ const mediaPath = resolvedMediaPath.mediaPath;
331
+ const isLocal = isLocalPath(mediaPath);
332
+ const isHttp = mediaPath.startsWith("http://") || mediaPath.startsWith("https://");
333
+ const isData = mediaPath.startsWith("data:");
334
+ if (isHttp && !shouldDirectUploadUrl(ctx.account)) {
335
+ debugLog(`sendPhoto: urlDirectUpload=false, downloading URL first...`);
336
+ const localFile = await downloadToFallbackDir(mediaPath, "sendPhoto");
337
+ if (localFile) return await sendPhotoFromLocal(ctx, localFile);
338
+ return {
339
+ channel: "qqbot",
340
+ error: `Failed to download image: ${mediaPath.slice(0, 80)}`
341
+ };
342
+ }
343
+ if (isLocal) return await sendPhotoFromLocal(ctx, mediaPath);
344
+ if (!isHttp && !isData) return {
345
+ channel: "qqbot",
346
+ error: `Unsupported image source: ${mediaPath.slice(0, 50)}`
347
+ };
348
+ try {
349
+ const creds = accountToCreds(ctx.account);
350
+ const target = {
351
+ type: ctx.targetType,
352
+ id: ctx.targetId
353
+ };
354
+ if (target.type === "c2c" || target.type === "group") {
355
+ const r = await sendMedia$1({
356
+ target,
357
+ creds,
358
+ kind: "image",
359
+ source: { url: mediaPath },
360
+ msgId: ctx.replyToId
361
+ });
362
+ return {
363
+ channel: "qqbot",
364
+ messageId: r.id,
365
+ timestamp: r.timestamp
366
+ };
367
+ }
368
+ if (isHttp) {
369
+ const r = await sendText$1(target, `![](${mediaPath})`, creds, { msgId: ctx.replyToId });
370
+ return {
371
+ channel: "qqbot",
372
+ messageId: r.id,
373
+ timestamp: r.timestamp
374
+ };
375
+ }
376
+ debugLog(`sendPhoto: channel does not support local/Base64 images`);
377
+ return {
378
+ channel: "qqbot",
379
+ error: "Channel does not support local/Base64 images"
380
+ };
381
+ } catch (err) {
382
+ const msg = formatErrorMessage(err);
383
+ if (isHttp && !isData) {
384
+ debugWarn(`sendPhoto: URL direct upload failed (${msg}), downloading locally and retrying as Base64...`);
385
+ const localFile = await downloadToFallbackDir(mediaPath, "sendPhoto");
386
+ if (localFile) return await sendPhotoFromLocal(ctx, localFile);
387
+ }
388
+ debugError(`sendPhoto failed: ${msg}`);
389
+ return {
390
+ channel: "qqbot",
391
+ error: msg
392
+ };
393
+ }
394
+ }
395
+ /** Send a photo from a validated local file path. */
396
+ async function sendPhotoFromLocal(ctx, mediaPath) {
397
+ if (!await fileExistsAsync(mediaPath)) return {
398
+ channel: "qqbot",
399
+ error: "Image not found"
400
+ };
401
+ const sizeCheck = checkFileSize(mediaPath, getMaxUploadSize(1));
402
+ if (!sizeCheck.ok) return buildFileTooLargeResult(1, sizeCheck.size);
403
+ if (!getImageMimeType(mediaPath)) return {
404
+ channel: "qqbot",
405
+ error: `Unsupported image format: ${normalizeLowercaseStringOrEmpty(path.extname(mediaPath))}`
406
+ };
407
+ debugLog(`sendPhoto: local (${formatFileSize(sizeCheck.size)})`);
408
+ try {
409
+ const creds = accountToCreds(ctx.account);
410
+ const target = {
411
+ type: ctx.targetType,
412
+ id: ctx.targetId
413
+ };
414
+ if (target.type === "c2c" || target.type === "group") {
415
+ const r = await sendMedia$1({
416
+ target,
417
+ creds,
418
+ kind: "image",
419
+ source: { localPath: mediaPath },
420
+ msgId: ctx.replyToId,
421
+ localPathForMeta: mediaPath
422
+ });
423
+ return {
424
+ channel: "qqbot",
425
+ messageId: r.id,
426
+ timestamp: r.timestamp
427
+ };
428
+ }
429
+ debugLog(`sendPhoto: channel does not support local images`);
430
+ return {
431
+ channel: "qqbot",
432
+ error: "Channel does not support local/Base64 images"
433
+ };
434
+ } catch (err) {
435
+ if (err instanceof UploadDailyLimitExceededError) {
436
+ debugError(`sendPhoto (local): daily upload quota exceeded`);
437
+ return buildDailyLimitExceededResult(err);
438
+ }
439
+ const msg = formatErrorMessage(err);
440
+ debugError(`sendPhoto (local) failed: ${msg}`);
441
+ return {
442
+ channel: "qqbot",
443
+ error: msg
444
+ };
445
+ }
446
+ }
447
+ /**
448
+ * Send voice from either a local file or a public URL.
449
+ *
450
+ * URL handling respects `urlDirectUpload`, and local files are transcoded when needed.
451
+ */
452
+ async function sendVoice(ctx, voicePath, directUploadFormats, transcodeEnabled = true) {
453
+ const resolvedMediaPath = resolveOutboundMediaPath(voicePath, "voice", { allowMissingLocalPath: true });
454
+ if (!resolvedMediaPath.ok) return {
455
+ channel: "qqbot",
456
+ error: resolvedMediaPath.error
457
+ };
458
+ const mediaPath = resolvedMediaPath.mediaPath;
459
+ if (mediaPath.startsWith("http://") || mediaPath.startsWith("https://")) {
460
+ if (shouldDirectUploadUrl(ctx.account)) try {
461
+ const creds = accountToCreds(ctx.account);
462
+ const target = {
463
+ type: ctx.targetType,
464
+ id: ctx.targetId
465
+ };
466
+ if (target.type === "c2c" || target.type === "group") {
467
+ const r = await sendMedia$1({
468
+ target,
469
+ creds,
470
+ kind: "voice",
471
+ source: { url: mediaPath },
472
+ msgId: ctx.replyToId
473
+ });
474
+ return {
475
+ channel: "qqbot",
476
+ messageId: r.id,
477
+ timestamp: r.timestamp
478
+ };
479
+ }
480
+ debugLog(`sendVoice: voice not supported in channel`);
481
+ return {
482
+ channel: "qqbot",
483
+ error: "Voice not supported in channel"
484
+ };
485
+ } catch (err) {
486
+ debugWarn(`sendVoice: URL direct upload failed (${formatErrorMessage(err)}), downloading locally and retrying...`);
487
+ }
488
+ else debugLog(`sendVoice: urlDirectUpload=false, downloading URL first...`);
489
+ const localFile = await downloadToFallbackDir(mediaPath, "sendVoice");
490
+ if (localFile) return await sendVoiceFromLocal(ctx, localFile, directUploadFormats, transcodeEnabled);
491
+ return {
492
+ channel: "qqbot",
493
+ error: `Failed to download audio: ${mediaPath.slice(0, 80)}`
494
+ };
495
+ }
496
+ return await sendVoiceFromLocal(ctx, mediaPath, directUploadFormats, transcodeEnabled);
497
+ }
498
+ /** Send voice from a local file. */
499
+ async function sendVoiceFromLocal(ctx, mediaPath, directUploadFormats, transcodeEnabled) {
500
+ const fileSize = await waitForFile(mediaPath);
501
+ if (fileSize === 0) return {
502
+ channel: "qqbot",
503
+ error: "Voice generate failed"
504
+ };
505
+ if (fileSize > getMaxUploadSize(3)) return buildFileTooLargeResult(3, fileSize);
506
+ const safeMediaPath = resolveQQBotPayloadLocalFilePath(mediaPath);
507
+ if (!safeMediaPath) {
508
+ debugWarn(`sendVoice: blocked local voice path outside QQ Bot media storage`);
509
+ return {
510
+ channel: "qqbot",
511
+ error: "Voice path must be inside QQ Bot media storage"
512
+ };
513
+ }
514
+ if (shouldTranscodeVoice(safeMediaPath) && !transcodeEnabled) {
515
+ const ext = normalizeLowercaseStringOrEmpty(path.extname(safeMediaPath));
516
+ debugLog(`sendVoice: transcode disabled, format ${ext} needs transcode, returning error for fallback`);
517
+ return {
518
+ channel: "qqbot",
519
+ error: `Voice transcoding is disabled and format ${ext} cannot be uploaded directly`
520
+ };
521
+ }
522
+ try {
523
+ let uploadBase64 = await audioFileToSilkBase64(safeMediaPath, directUploadFormats);
524
+ if (!uploadBase64) {
525
+ const buf = await readFileAsync(safeMediaPath);
526
+ uploadBase64 = buf.toString("base64");
527
+ debugLog(`sendVoice: SILK conversion failed, uploading raw (${formatFileSize(buf.length)})`);
528
+ } else debugLog(`sendVoice: SILK ready (${fileSize} bytes)`);
529
+ const creds = accountToCreds(ctx.account);
530
+ const target = {
531
+ type: ctx.targetType,
532
+ id: ctx.targetId
533
+ };
534
+ if (target.type === "c2c" || target.type === "group") {
535
+ const r = await sendMedia$1({
536
+ target,
537
+ creds,
538
+ kind: "voice",
539
+ source: { base64: uploadBase64 },
540
+ msgId: ctx.replyToId,
541
+ localPathForMeta: safeMediaPath
542
+ });
543
+ return {
544
+ channel: "qqbot",
545
+ messageId: r.id,
546
+ timestamp: r.timestamp
547
+ };
548
+ }
549
+ debugLog(`sendVoice: voice not supported in channel`);
550
+ return {
551
+ channel: "qqbot",
552
+ error: "Voice not supported in channel"
553
+ };
554
+ } catch (err) {
555
+ if (err instanceof UploadDailyLimitExceededError) {
556
+ debugError(`sendVoice (local): daily upload quota exceeded`);
557
+ return buildDailyLimitExceededResult(err);
558
+ }
559
+ const msg = formatErrorMessage(err);
560
+ debugError(`sendVoice (local) failed: ${msg}`);
561
+ return {
562
+ channel: "qqbot",
563
+ error: msg
564
+ };
565
+ }
566
+ }
567
+ /** Send video from either a public URL or a local file. */
568
+ async function sendVideoMsg(ctx, videoPath) {
569
+ const resolvedMediaPath = resolveOutboundMediaPath(videoPath, "video");
570
+ if (!resolvedMediaPath.ok) return {
571
+ channel: "qqbot",
572
+ error: resolvedMediaPath.error
573
+ };
574
+ const mediaPath = resolvedMediaPath.mediaPath;
575
+ const isHttp = mediaPath.startsWith("http://") || mediaPath.startsWith("https://");
576
+ if (isHttp && !shouldDirectUploadUrl(ctx.account)) {
577
+ debugLog(`sendVideoMsg: urlDirectUpload=false, downloading URL first...`);
578
+ const localFile = await downloadToFallbackDir(mediaPath, "sendVideoMsg");
579
+ if (localFile) return await sendVideoFromLocal(ctx, localFile);
580
+ return {
581
+ channel: "qqbot",
582
+ error: `Failed to download video: ${mediaPath.slice(0, 80)}`
583
+ };
584
+ }
585
+ try {
586
+ if (isHttp) {
587
+ const creds = accountToCreds(ctx.account);
588
+ const target = {
589
+ type: ctx.targetType,
590
+ id: ctx.targetId
591
+ };
592
+ if (target.type === "c2c" || target.type === "group") {
593
+ const r = await sendMedia$1({
594
+ target,
595
+ creds,
596
+ kind: "video",
597
+ source: { url: mediaPath },
598
+ msgId: ctx.replyToId
599
+ });
600
+ return {
601
+ channel: "qqbot",
602
+ messageId: r.id,
603
+ timestamp: r.timestamp
604
+ };
605
+ }
606
+ debugLog(`sendVideoMsg: video not supported in channel`);
607
+ return {
608
+ channel: "qqbot",
609
+ error: "Video not supported in channel"
610
+ };
611
+ }
612
+ return await sendVideoFromLocal(ctx, mediaPath);
613
+ } catch (err) {
614
+ const msg = formatErrorMessage(err);
615
+ if (isHttp) {
616
+ debugWarn(`sendVideoMsg: URL direct upload failed (${msg}), downloading locally and retrying as Base64...`);
617
+ const localFile = await downloadToFallbackDir(mediaPath, "sendVideoMsg");
618
+ if (localFile) return await sendVideoFromLocal(ctx, localFile);
619
+ }
620
+ debugError(`sendVideoMsg failed: ${msg}`);
621
+ return {
622
+ channel: "qqbot",
623
+ error: msg
624
+ };
625
+ }
626
+ }
627
+ /** Send video from a local file. */
628
+ async function sendVideoFromLocal(ctx, mediaPath) {
629
+ if (!await fileExistsAsync(mediaPath)) return {
630
+ channel: "qqbot",
631
+ error: "Video not found"
632
+ };
633
+ const sizeCheck = checkFileSize(mediaPath, getMaxUploadSize(2));
634
+ if (!sizeCheck.ok) return buildFileTooLargeResult(2, sizeCheck.size);
635
+ debugLog(`sendVideoMsg: local video (${formatFileSize(sizeCheck.size)})`);
636
+ try {
637
+ const creds = accountToCreds(ctx.account);
638
+ const target = {
639
+ type: ctx.targetType,
640
+ id: ctx.targetId
641
+ };
642
+ if (target.type === "c2c" || target.type === "group") {
643
+ const r = await sendMedia$1({
644
+ target,
645
+ creds,
646
+ kind: "video",
647
+ source: { localPath: mediaPath },
648
+ msgId: ctx.replyToId,
649
+ localPathForMeta: mediaPath
650
+ });
651
+ return {
652
+ channel: "qqbot",
653
+ messageId: r.id,
654
+ timestamp: r.timestamp
655
+ };
656
+ }
657
+ debugLog(`sendVideoMsg: video not supported in channel`);
658
+ return {
659
+ channel: "qqbot",
660
+ error: "Video not supported in channel"
661
+ };
662
+ } catch (err) {
663
+ if (err instanceof UploadDailyLimitExceededError) {
664
+ debugError(`sendVideoMsg (local): daily upload quota exceeded`);
665
+ return buildDailyLimitExceededResult(err);
666
+ }
667
+ const msg = formatErrorMessage(err);
668
+ debugError(`sendVideoMsg (local) failed: ${msg}`);
669
+ return {
670
+ channel: "qqbot",
671
+ error: msg
672
+ };
673
+ }
674
+ }
675
+ /** Send a file from a local path or public URL. */
676
+ async function sendDocument(ctx, filePath, options = {}) {
677
+ const resolvedMediaPath = resolveOutboundMediaPath(filePath, "file", { extraLocalRoots: options.allowQQBotDataDownloads ? [getQQBotDataDir("downloads")] : void 0 });
678
+ if (!resolvedMediaPath.ok) return {
679
+ channel: "qqbot",
680
+ error: resolvedMediaPath.error
681
+ };
682
+ const mediaPath = resolvedMediaPath.mediaPath;
683
+ const isHttp = mediaPath.startsWith("http://") || mediaPath.startsWith("https://");
684
+ const fileName = sanitizeFileName(path.basename(mediaPath));
685
+ if (isHttp && !shouldDirectUploadUrl(ctx.account)) {
686
+ debugLog(`sendDocument: urlDirectUpload=false, downloading URL first...`);
687
+ const localFile = await downloadToFallbackDir(mediaPath, "sendDocument");
688
+ if (localFile) return await sendDocumentFromLocal(ctx, localFile);
689
+ return {
690
+ channel: "qqbot",
691
+ error: `Failed to download file: ${mediaPath.slice(0, 80)}`
692
+ };
693
+ }
694
+ try {
695
+ if (isHttp) {
696
+ const creds = accountToCreds(ctx.account);
697
+ const target = {
698
+ type: ctx.targetType,
699
+ id: ctx.targetId
700
+ };
701
+ if (target.type === "c2c" || target.type === "group") {
702
+ const r = await sendMedia$1({
703
+ target,
704
+ creds,
705
+ kind: "file",
706
+ source: { url: mediaPath },
707
+ msgId: ctx.replyToId,
708
+ fileName
709
+ });
710
+ return {
711
+ channel: "qqbot",
712
+ messageId: r.id,
713
+ timestamp: r.timestamp
714
+ };
715
+ }
716
+ debugLog(`sendDocument: file not supported in channel`);
717
+ return {
718
+ channel: "qqbot",
719
+ error: "File not supported in channel"
720
+ };
721
+ }
722
+ return await sendDocumentFromLocal(ctx, mediaPath);
723
+ } catch (err) {
724
+ const msg = formatErrorMessage(err);
725
+ if (isHttp) {
726
+ debugWarn(`sendDocument: URL direct upload failed (${msg}), downloading locally and retrying as Base64...`);
727
+ const localFile = await downloadToFallbackDir(mediaPath, "sendDocument");
728
+ if (localFile) return await sendDocumentFromLocal(ctx, localFile);
729
+ }
730
+ debugError(`sendDocument failed: ${msg}`);
731
+ return {
732
+ channel: "qqbot",
733
+ error: msg
734
+ };
735
+ }
736
+ }
737
+ /** Send a file from local storage. */
738
+ async function sendDocumentFromLocal(ctx, mediaPath) {
739
+ const fileName = sanitizeFileName(path.basename(mediaPath));
740
+ if (!await fileExistsAsync(mediaPath)) return {
741
+ channel: "qqbot",
742
+ error: "File not found"
743
+ };
744
+ const sizeCheck = checkFileSize(mediaPath, getMaxUploadSize(4));
745
+ if (!sizeCheck.ok) return buildFileTooLargeResult(4, sizeCheck.size);
746
+ if (sizeCheck.size === 0) return {
747
+ channel: "qqbot",
748
+ error: `File is empty: ${mediaPath}`
749
+ };
750
+ debugLog(`sendDocument: local file (${formatFileSize(sizeCheck.size)})`);
751
+ try {
752
+ const creds = accountToCreds(ctx.account);
753
+ const target = {
754
+ type: ctx.targetType,
755
+ id: ctx.targetId
756
+ };
757
+ if (target.type === "c2c" || target.type === "group") {
758
+ const r = await sendMedia$1({
759
+ target,
760
+ creds,
761
+ kind: "file",
762
+ source: { localPath: mediaPath },
763
+ msgId: ctx.replyToId,
764
+ fileName,
765
+ localPathForMeta: mediaPath
766
+ });
767
+ return {
768
+ channel: "qqbot",
769
+ messageId: r.id,
770
+ timestamp: r.timestamp
771
+ };
772
+ }
773
+ debugLog(`sendDocument: file not supported in channel`);
774
+ return {
775
+ channel: "qqbot",
776
+ error: "File not supported in channel"
777
+ };
778
+ } catch (err) {
779
+ if (err instanceof UploadDailyLimitExceededError) {
780
+ debugError(`sendDocument (local): daily upload quota exceeded`);
781
+ return buildDailyLimitExceededResult(err);
782
+ }
783
+ const msg = formatErrorMessage(err);
784
+ debugError(`sendDocument (local) failed: ${msg}`);
785
+ return {
786
+ channel: "qqbot",
787
+ error: msg
788
+ };
789
+ }
790
+ }
791
+ /** Download a remote file into the fallback media directory. */
792
+ async function downloadToFallbackDir(httpUrl, caller) {
793
+ try {
794
+ const localFile = await downloadFile(httpUrl, getQQBotMediaDir("downloads", "url-fallback"));
795
+ if (!localFile) {
796
+ debugError(`${caller} fallback: download also failed for ${httpUrl.slice(0, 80)}`);
797
+ return null;
798
+ }
799
+ debugLog(`${caller} fallback: downloaded → ${localFile}`);
800
+ return localFile;
801
+ } catch (err) {
802
+ debugError(`${caller} fallback download error:`, err);
803
+ return null;
804
+ }
805
+ }
806
+ //#endregion
807
+ //#region extensions/qqbot/src/engine/utils/media-tags.ts
808
+ /**
809
+ * Media tag normalization for QQ Bot messages.
810
+ *
811
+ * Normalizes malformed `<qqimg>`, `<qqvoice>`, etc. tags emitted by
812
+ * smaller models into canonical wrapped-tag format.
813
+ *
814
+ * Zero external dependencies.
815
+ */
816
+ /** Lowercase and trim a string, returning empty string for falsy input. */
817
+ function lc(s) {
818
+ return (s ?? "").toLowerCase().trim();
819
+ }
820
+ /** Expand `~` prefix to the process home directory. */
821
+ function expandTilde(p) {
822
+ if (!p) return p;
823
+ const home = typeof process !== "undefined" ? process.env.HOME ?? process.env.USERPROFILE : void 0;
824
+ if (!home) return p;
825
+ if (p === "~") return home;
826
+ if (p.startsWith("~/") || p.startsWith("~\\")) return `${home}/${p.slice(2)}`;
827
+ return p;
828
+ }
829
+ const VALID_TAGS = [
830
+ "qqimg",
831
+ "qqvoice",
832
+ "qqvideo",
833
+ "qqfile",
834
+ "qqmedia"
835
+ ];
836
+ const TAG_ALIASES = {
837
+ qq_img: "qqimg",
838
+ qqimage: "qqimg",
839
+ qq_image: "qqimg",
840
+ qqpic: "qqimg",
841
+ qq_pic: "qqimg",
842
+ qqpicture: "qqimg",
843
+ qq_picture: "qqimg",
844
+ qqphoto: "qqimg",
845
+ qq_photo: "qqimg",
846
+ img: "qqimg",
847
+ image: "qqimg",
848
+ pic: "qqimg",
849
+ picture: "qqimg",
850
+ photo: "qqimg",
851
+ qq_voice: "qqvoice",
852
+ qqaudio: "qqvoice",
853
+ qq_audio: "qqvoice",
854
+ voice: "qqvoice",
855
+ audio: "qqvoice",
856
+ qq_video: "qqvideo",
857
+ video: "qqvideo",
858
+ qq_file: "qqfile",
859
+ qqdoc: "qqfile",
860
+ qq_doc: "qqfile",
861
+ file: "qqfile",
862
+ doc: "qqfile",
863
+ document: "qqfile",
864
+ qq_media: "qqmedia",
865
+ media: "qqmedia",
866
+ attachment: "qqmedia",
867
+ attach: "qqmedia",
868
+ qqattachment: "qqmedia",
869
+ qq_attachment: "qqmedia",
870
+ qqsend: "qqmedia",
871
+ qq_send: "qqmedia",
872
+ send: "qqmedia"
873
+ };
874
+ const ALL_TAG_NAMES = [...VALID_TAGS, ...Object.keys(TAG_ALIASES)];
875
+ ALL_TAG_NAMES.sort((a, b) => b.length - a.length);
876
+ const TAG_NAME_PATTERN = ALL_TAG_NAMES.join("|");
877
+ const LEFT_BRACKET = "(?:[<<<]|&lt;)";
878
+ /** Match self-closing media-tag syntax with file/src/path/url attributes. */
879
+ const SELF_CLOSING_TAG_REGEX = new RegExp("`?" + LEFT_BRACKET + "\\s*(" + TAG_NAME_PATTERN + ")(?:\\s+(?!file|src|path|url)[a-z_-]+\\s*=\\s*[\"']?[^\"'\\s<<>>>]*?[\"']?)*\\s+(?:file|src|path|url)\\s*=\\s*[\"']?([^\"'\\s>>]+?)[\"']?(?:\\s+[a-z_-]+\\s*=\\s*[\"']?[^\"'\\s<<>>>]*?[\"']?)*\\s*/?\\s*(?:[>>>]|&gt;)`?", "gi");
880
+ /** Match malformed wrapped media tags that should be normalized. */
881
+ const FUZZY_MEDIA_TAG_REGEX = new RegExp("`?" + LEFT_BRACKET + "\\s*(" + TAG_NAME_PATTERN + ")\\s*(?:[>>>]|&gt;)[\"']?\\s*([^<<<>>\"'`]+?)\\s*[\"']?(?:[<<<]|&lt;)\\s*/?\\s*(?:" + TAG_NAME_PATTERN + ")\\s*(?:[>>>]|&gt;)`?", "gi");
882
+ /** Normalize a raw tag name into the canonical tag set. */
883
+ function resolveTagName(raw) {
884
+ const lower = lc(raw);
885
+ if (VALID_TAGS.includes(lower)) return lower;
886
+ return TAG_ALIASES[lower] ?? "qqimg";
887
+ }
888
+ /** Match wrapped tags whose bodies need newline and tab cleanup. */
889
+ const MULTILINE_TAG_CLEANUP = new RegExp("(" + LEFT_BRACKET + "\\s*(?:" + TAG_NAME_PATTERN + ")\\s*(?:[>>>]|&gt;))([\\s\\S]*?)((?:[<<<]|&lt;)\\s*/?\\s*(?:" + TAG_NAME_PATTERN + ")\\s*(?:[>>>]|&gt;))", "gi");
890
+ /** Normalize malformed media-tag output into canonical wrapped tags. */
891
+ function normalizeMediaTags(text) {
892
+ const normalizeWrappedTag = (_match, rawTag, content) => {
893
+ const tag = resolveTagName(rawTag);
894
+ const trimmed = content.trim();
895
+ if (!trimmed) return _match;
896
+ return `<${tag}>${expandTilde(trimmed)}</${tag}>`;
897
+ };
898
+ let cleaned = text.replace(SELF_CLOSING_TAG_REGEX, normalizeWrappedTag);
899
+ cleaned = cleaned.replace(MULTILINE_TAG_CLEANUP, (_m, open, body, close) => {
900
+ return open + body.replace(/[\r\n\t]+/g, " ").replace(/ {2,}/g, " ") + close;
901
+ });
902
+ return cleaned.replace(FUZZY_MEDIA_TAG_REGEX, normalizeWrappedTag);
903
+ }
904
+ //#endregion
905
+ //#region extensions/qqbot/src/engine/utils/payload.ts
906
+ const PAYLOAD_PREFIX = "QQBOT_PAYLOAD:";
907
+ const CRON_PREFIX = "QQBOT_CRON:";
908
+ function formatErr(e) {
909
+ return e instanceof Error ? e.message : String(e);
910
+ }
911
+ /** Parse model output that may start with the QQ Bot structured payload prefix. */
912
+ function parseQQBotPayload(text) {
913
+ const trimmedText = text.trim();
914
+ if (!trimmedText.startsWith(PAYLOAD_PREFIX)) return {
915
+ isPayload: false,
916
+ text
917
+ };
918
+ const jsonContent = trimmedText.slice(14).trim();
919
+ if (!jsonContent) return {
920
+ isPayload: true,
921
+ error: "Payload body is empty"
922
+ };
923
+ try {
924
+ const payload = JSON.parse(jsonContent);
925
+ if (!payload.type) return {
926
+ isPayload: true,
927
+ error: "Payload is missing the type field"
928
+ };
929
+ if (payload.type === "cron_reminder") {
930
+ if (!payload.content || !payload.targetType || !payload.targetAddress) return {
931
+ isPayload: true,
932
+ error: "cron_reminder payload is missing required fields (content, targetType, targetAddress)"
933
+ };
934
+ } else if (payload.type === "media") {
935
+ if (!payload.mediaType || !payload.source || !payload.path) return {
936
+ isPayload: true,
937
+ error: "media payload is missing required fields (mediaType, source, path)"
938
+ };
939
+ }
940
+ return {
941
+ isPayload: true,
942
+ payload
943
+ };
944
+ } catch (e) {
945
+ return {
946
+ isPayload: true,
947
+ error: `Failed to parse JSON: ${formatErr(e)}`
948
+ };
949
+ }
950
+ }
951
+ /** Encode a cron reminder payload into the stored cron-message format. */
952
+ function encodePayloadForCron(payload) {
953
+ const jsonString = JSON.stringify(payload);
954
+ return `${CRON_PREFIX}${Buffer.from(jsonString, "utf-8").toString("base64")}`;
955
+ }
956
+ /** Decode a stored cron payload. */
957
+ function decodeCronPayload(message) {
958
+ const trimmedMessage = message.trim();
959
+ if (!trimmedMessage.startsWith(CRON_PREFIX)) return { isCronPayload: false };
960
+ const base64Content = trimmedMessage.slice(11);
961
+ if (!base64Content) return {
962
+ isCronPayload: true,
963
+ error: "Cron payload body is empty"
964
+ };
965
+ try {
966
+ const jsonString = Buffer.from(base64Content, "base64").toString("utf-8");
967
+ const payload = JSON.parse(jsonString);
968
+ if (payload.type !== "cron_reminder") return {
969
+ isCronPayload: true,
970
+ error: `Expected type cron_reminder but got ${String(payload.type)}`
971
+ };
972
+ if (!payload.content || !payload.targetType || !payload.targetAddress) return {
973
+ isCronPayload: true,
974
+ error: "Cron payload is missing required fields"
975
+ };
976
+ return {
977
+ isCronPayload: true,
978
+ payload
979
+ };
980
+ } catch (e) {
981
+ return {
982
+ isCronPayload: true,
983
+ error: `Failed to decode cron payload: ${formatErr(e)}`
984
+ };
985
+ }
986
+ }
987
+ /** Type guard for cron reminder payloads. */
988
+ function isCronReminderPayload(payload) {
989
+ return payload.type === "cron_reminder";
990
+ }
991
+ /** Type guard for media payloads. */
992
+ function isMediaPayload(payload) {
993
+ return payload.type === "media";
994
+ }
995
+ //#endregion
996
+ //#region extensions/qqbot/src/engine/messaging/media-type-detect.ts
997
+ /**
998
+ * Media type detection — pure functions for classifying files by MIME or extension.
999
+ *
1000
+ * These replace the inline `isImageFile` and `isVideoFile` helpers scattered
1001
+ * across `outbound.ts`. Centralizing them here keeps detection consistent.
1002
+ */
1003
+ const IMAGE_EXTENSIONS = new Set([
1004
+ ".jpg",
1005
+ ".jpeg",
1006
+ ".png",
1007
+ ".gif",
1008
+ ".webp",
1009
+ ".bmp"
1010
+ ]);
1011
+ const VIDEO_EXTENSIONS = new Set([
1012
+ ".mp4",
1013
+ ".mov",
1014
+ ".avi",
1015
+ ".mkv",
1016
+ ".webm",
1017
+ ".flv",
1018
+ ".wmv"
1019
+ ]);
1020
+ /**
1021
+ * Extract a lowercase file extension from a path or URL, ignoring query and hash.
1022
+ */
1023
+ function getCleanExtension(filePath) {
1024
+ const cleanPath = filePath.split("?")[0].split("#")[0];
1025
+ const lastDot = cleanPath.lastIndexOf(".");
1026
+ if (lastDot < 0) return "";
1027
+ return cleanPath.slice(lastDot).toLowerCase();
1028
+ }
1029
+ /** Check whether a file is an image using MIME first and extension as fallback. */
1030
+ function isImageFile$1(filePath, mimeType) {
1031
+ if (mimeType?.startsWith("image/")) return true;
1032
+ return IMAGE_EXTENSIONS.has(getCleanExtension(filePath));
1033
+ }
1034
+ /** Check whether a file is a video using MIME first and extension as fallback. */
1035
+ function isVideoFile$1(filePath, mimeType) {
1036
+ if (mimeType?.startsWith("video/")) return true;
1037
+ return VIDEO_EXTENSIONS.has(getCleanExtension(filePath));
1038
+ }
1039
+ //#endregion
1040
+ //#region extensions/qqbot/src/engine/messaging/outbound.ts
1041
+ var outbound_exports = /* @__PURE__ */ __exportAll({
1042
+ DEFAULT_MEDIA_SEND_ERROR: () => DEFAULT_MEDIA_SEND_ERROR,
1043
+ MESSAGE_REPLY_LIMIT: () => 4,
1044
+ sendCronMessage: () => sendCronMessage,
1045
+ sendMedia: () => sendMedia,
1046
+ sendProactiveMessage: () => sendProactiveMessage,
1047
+ sendText: () => sendText
1048
+ });
1049
+ const isImageFile = isImageFile$1;
1050
+ const isVideoFile = isVideoFile$1;
1051
+ /**
1052
+ * Send text, optionally falling back from passive reply mode to proactive mode.
1053
+ *
1054
+ * Also supports inline media tags such as `<qqimg>...</qqimg>`.
1055
+ */
1056
+ async function sendText(ctx) {
1057
+ const { to, account } = ctx;
1058
+ let { text, replyToId } = ctx;
1059
+ let fallbackToProactive = false;
1060
+ initApiConfig(account.appId, { markdownSupport: account.markdownSupport });
1061
+ debugLog("[qqbot] sendText ctx:", JSON.stringify({
1062
+ to,
1063
+ text: text?.slice(0, 50),
1064
+ replyToId,
1065
+ accountId: account.accountId
1066
+ }, null, 2));
1067
+ if (replyToId) {
1068
+ const limitCheck = checkMessageReplyLimit(replyToId);
1069
+ if (!limitCheck.allowed) if (limitCheck.shouldFallbackToProactive) {
1070
+ debugWarn(`[qqbot] sendText: passive reply unavailable, falling back to proactive send - ${limitCheck.message}`);
1071
+ fallbackToProactive = true;
1072
+ replyToId = null;
1073
+ } else {
1074
+ debugError(`[qqbot] sendText: passive reply was blocked without a fallback path - ${limitCheck.message}`);
1075
+ return {
1076
+ channel: "qqbot",
1077
+ error: limitCheck.message
1078
+ };
1079
+ }
1080
+ else debugLog(`[qqbot] sendText: remaining passive replies for ${replyToId}: ${limitCheck.remaining}/4`);
1081
+ }
1082
+ text = normalizeMediaTags(text);
1083
+ const mediaTagMatches = text.match(/<(qqimg|qqvoice|qqvideo|qqfile|qqmedia)>([^<>]+)<\/(?:qqimg|qqvoice|qqvideo|qqfile|qqmedia|img)>/gi);
1084
+ if (mediaTagMatches && mediaTagMatches.length > 0) {
1085
+ debugLog(`[qqbot] sendText: Detected ${mediaTagMatches.length} media tag(s), processing...`);
1086
+ const sendQueue = [];
1087
+ let lastIndex = 0;
1088
+ const mediaTagRegexWithIndex = /<(qqimg|qqvoice|qqvideo|qqfile|qqmedia)>([^<>]+)<\/(?:qqimg|qqvoice|qqvideo|qqfile|qqmedia|img)>/gi;
1089
+ let match;
1090
+ while ((match = mediaTagRegexWithIndex.exec(text)) !== null) {
1091
+ const textBefore = text.slice(lastIndex, match.index).replace(/\n{3,}/g, "\n\n").trim();
1092
+ if (textBefore) sendQueue.push({
1093
+ type: "text",
1094
+ content: textBefore
1095
+ });
1096
+ const tagName = normalizeLowercaseStringOrEmpty(match[1]);
1097
+ let mediaPath = normalizeOptionalString(match[2]) ?? "";
1098
+ if (mediaPath.startsWith("MEDIA:")) mediaPath = mediaPath.slice(6);
1099
+ mediaPath = normalizePath(mediaPath);
1100
+ mediaPath = mediaPath.replace(/\\\\/g, "\\");
1101
+ const isWinLocal = /^[a-zA-Z]:[\\/]/.test(mediaPath) || mediaPath.startsWith("\\\\");
1102
+ try {
1103
+ const hasOctal = /\\[0-7]{1,3}/.test(mediaPath);
1104
+ const hasNonASCII = /[\u0080-\u00FF]/.test(mediaPath);
1105
+ if (!isWinLocal && (hasOctal || hasNonASCII)) {
1106
+ debugLog(`[qqbot] sendText: Decoding path with mixed encoding: ${mediaPath}`);
1107
+ let decoded = mediaPath.replace(/\\([0-7]{1,3})/g, (_, octal) => {
1108
+ return String.fromCharCode(Number.parseInt(octal, 8));
1109
+ });
1110
+ const bytes = [];
1111
+ for (let i = 0; i < decoded.length; i++) {
1112
+ const code = decoded.charCodeAt(i);
1113
+ if (code <= 255) bytes.push(code);
1114
+ else {
1115
+ const charBytes = Buffer.from(decoded[i], "utf8");
1116
+ bytes.push(...charBytes);
1117
+ }
1118
+ }
1119
+ const utf8Decoded = Buffer.from(bytes).toString("utf8");
1120
+ if (!utf8Decoded.includes("�") || utf8Decoded.length < decoded.length) {
1121
+ mediaPath = utf8Decoded;
1122
+ debugLog(`[qqbot] sendText: Successfully decoded path: ${mediaPath}`);
1123
+ }
1124
+ }
1125
+ } catch (decodeErr) {
1126
+ debugError(`[qqbot] sendText: Path decode error: ${decodeErr instanceof Error ? decodeErr.message : JSON.stringify(decodeErr)}`);
1127
+ }
1128
+ if (mediaPath) if (tagName === "qqmedia") {
1129
+ sendQueue.push({
1130
+ type: "media",
1131
+ content: mediaPath
1132
+ });
1133
+ debugLog(`[qqbot] sendText: Found auto-detect media in <qqmedia>: ${mediaPath}`);
1134
+ } else if (tagName === "qqvoice") {
1135
+ sendQueue.push({
1136
+ type: "voice",
1137
+ content: mediaPath
1138
+ });
1139
+ debugLog(`[qqbot] sendText: Found voice path in <qqvoice>: ${mediaPath}`);
1140
+ } else if (tagName === "qqvideo") {
1141
+ sendQueue.push({
1142
+ type: "video",
1143
+ content: mediaPath
1144
+ });
1145
+ debugLog(`[qqbot] sendText: Found video URL in <qqvideo>: ${mediaPath}`);
1146
+ } else if (tagName === "qqfile") {
1147
+ sendQueue.push({
1148
+ type: "file",
1149
+ content: mediaPath
1150
+ });
1151
+ debugLog(`[qqbot] sendText: Found file path in <qqfile>: ${mediaPath}`);
1152
+ } else {
1153
+ sendQueue.push({
1154
+ type: "image",
1155
+ content: mediaPath
1156
+ });
1157
+ debugLog(`[qqbot] sendText: Found image path in <qqimg>: ${mediaPath}`);
1158
+ }
1159
+ lastIndex = match.index + match[0].length;
1160
+ }
1161
+ const textAfter = text.slice(lastIndex).replace(/\n{3,}/g, "\n\n").trim();
1162
+ if (textAfter) sendQueue.push({
1163
+ type: "text",
1164
+ content: textAfter
1165
+ });
1166
+ debugLog(`[qqbot] sendText: Send queue: ${sendQueue.map((item) => item.type).join(" -> ")}`);
1167
+ const mediaTarget = buildMediaTarget({
1168
+ to,
1169
+ account,
1170
+ replyToId
1171
+ });
1172
+ let lastResult = { channel: "qqbot" };
1173
+ for (const item of sendQueue) try {
1174
+ if (item.type === "text") {
1175
+ const target = parseTarget(to);
1176
+ const creds = accountToCreds(account);
1177
+ const result = await sendText$1({
1178
+ type: target.type === "channel" ? "channel" : target.type,
1179
+ id: target.id
1180
+ }, item.content, creds, { msgId: replyToId ?? void 0 });
1181
+ if (replyToId) recordMessageReply(replyToId);
1182
+ lastResult = {
1183
+ channel: "qqbot",
1184
+ messageId: result.id,
1185
+ timestamp: result.timestamp,
1186
+ refIdx: result.ext_info?.ref_idx
1187
+ };
1188
+ debugLog(`[qqbot] sendText: Sent text part: ${item.content.slice(0, 30)}...`);
1189
+ } else if (item.type === "image") lastResult = await sendPhoto(mediaTarget, item.content);
1190
+ else if (item.type === "voice") lastResult = await sendVoice(mediaTarget, item.content, void 0, account.config?.audioFormatPolicy?.transcodeEnabled !== false);
1191
+ else if (item.type === "video") lastResult = await sendVideoMsg(mediaTarget, item.content);
1192
+ else if (item.type === "file") lastResult = await sendDocument(mediaTarget, item.content);
1193
+ else if (item.type === "media") lastResult = await sendMedia({
1194
+ to,
1195
+ text: "",
1196
+ mediaUrl: item.content,
1197
+ accountId: account.accountId,
1198
+ replyToId,
1199
+ account
1200
+ });
1201
+ } catch (err) {
1202
+ const errMsg = formatErrorMessage(err);
1203
+ debugError(`[qqbot] sendText: Failed to send ${item.type}: ${errMsg}`);
1204
+ lastResult = {
1205
+ channel: "qqbot",
1206
+ error: errMsg
1207
+ };
1208
+ }
1209
+ return lastResult;
1210
+ }
1211
+ if (!replyToId) {
1212
+ if (!text || text.trim().length === 0) {
1213
+ debugError("[qqbot] sendText error: proactive message content cannot be empty");
1214
+ return {
1215
+ channel: "qqbot",
1216
+ error: "Proactive messages require non-empty content (--message cannot be empty)"
1217
+ };
1218
+ }
1219
+ if (fallbackToProactive) debugLog(`[qqbot] sendText: [fallback] sending proactive message to ${to}, length=${text.length}`);
1220
+ else debugLog(`[qqbot] sendText: sending proactive message to ${to}, length=${text.length}`);
1221
+ }
1222
+ if (!account.appId || !account.clientSecret) return {
1223
+ channel: "qqbot",
1224
+ error: "QQBot not configured (missing appId or clientSecret)"
1225
+ };
1226
+ try {
1227
+ const target = parseTarget(to);
1228
+ const creds = accountToCreds(account);
1229
+ const deliveryTarget = {
1230
+ type: target.type === "channel" ? "channel" : target.type,
1231
+ id: target.id
1232
+ };
1233
+ debugLog("[qqbot] sendText target:", JSON.stringify(target));
1234
+ const result = await sendText$1(deliveryTarget, text, creds, { msgId: replyToId ?? void 0 });
1235
+ if (replyToId) recordMessageReply(replyToId);
1236
+ return {
1237
+ channel: "qqbot",
1238
+ messageId: result.id,
1239
+ timestamp: result.timestamp,
1240
+ refIdx: result.ext_info?.ref_idx
1241
+ };
1242
+ } catch (err) {
1243
+ return {
1244
+ channel: "qqbot",
1245
+ error: formatErrorMessage(err)
1246
+ };
1247
+ }
1248
+ }
1249
+ /** Send rich media, auto-routing by media type and source. */
1250
+ async function sendMedia(ctx) {
1251
+ const { to, text, replyToId, account, mimeType } = ctx;
1252
+ initApiConfig(account.appId, { markdownSupport: account.markdownSupport });
1253
+ if (!account.appId || !account.clientSecret) return {
1254
+ channel: "qqbot",
1255
+ error: "QQBot not configured (missing appId or clientSecret)"
1256
+ };
1257
+ if (!ctx.mediaUrl) return {
1258
+ channel: "qqbot",
1259
+ error: "mediaUrl is required for sendMedia"
1260
+ };
1261
+ const resolvedMediaPath = resolveOutboundMediaPath(ctx.mediaUrl, "media", { allowMissingLocalPath: true });
1262
+ if (!resolvedMediaPath.ok) return {
1263
+ channel: "qqbot",
1264
+ error: resolvedMediaPath.error
1265
+ };
1266
+ const mediaUrl = resolvedMediaPath.mediaPath;
1267
+ const target = buildMediaTarget({
1268
+ to,
1269
+ account,
1270
+ replyToId
1271
+ });
1272
+ if (isAudioFile(mediaUrl, mimeType)) {
1273
+ const result = await sendVoice(target, mediaUrl, account.config?.audioFormatPolicy?.uploadDirectFormats ?? account.config?.voiceDirectUploadFormats, account.config?.audioFormatPolicy?.transcodeEnabled !== false);
1274
+ if (!result.error) {
1275
+ if (text?.trim()) await sendTextAfterMedia(target, text);
1276
+ return result;
1277
+ }
1278
+ const voiceError = result.error;
1279
+ debugWarn(`[qqbot] sendMedia: sendVoice failed (${voiceError}), falling back to sendDocument`);
1280
+ const fallback = await sendDocument(target, mediaUrl);
1281
+ if (!fallback.error) {
1282
+ if (text?.trim()) await sendTextAfterMedia(target, text);
1283
+ return fallback;
1284
+ }
1285
+ return {
1286
+ channel: "qqbot",
1287
+ error: `voice: ${voiceError} | fallback file: ${fallback.error}`
1288
+ };
1289
+ }
1290
+ if (isVideoFile(mediaUrl, mimeType)) {
1291
+ const result = await sendVideoMsg(target, mediaUrl);
1292
+ if (!result.error && text?.trim()) await sendTextAfterMedia(target, text);
1293
+ return result;
1294
+ }
1295
+ if (!isImageFile(mediaUrl, mimeType) && !isAudioFile(mediaUrl, mimeType) && !isVideoFile(mediaUrl, mimeType)) {
1296
+ const result = await sendDocument(target, mediaUrl);
1297
+ if (!result.error && text?.trim()) await sendTextAfterMedia(target, text);
1298
+ return result;
1299
+ }
1300
+ const result = await sendPhoto(target, mediaUrl);
1301
+ if (!result.error && text?.trim()) await sendTextAfterMedia(target, text);
1302
+ return result;
1303
+ }
1304
+ async function sendTextAfterMedia(ctx, text) {
1305
+ try {
1306
+ const creds = accountToCreds(ctx.account);
1307
+ await sendText$1({
1308
+ type: ctx.targetType,
1309
+ id: ctx.targetId
1310
+ }, text, creds, { msgId: ctx.replyToId });
1311
+ } catch (err) {
1312
+ debugError(`[qqbot] sendTextAfterMedia failed: ${formatErrorMessage(err)}`);
1313
+ }
1314
+ }
1315
+ async function sendProactiveMessage(account, to, content) {
1316
+ return sendText({
1317
+ account,
1318
+ to,
1319
+ text: content
1320
+ });
1321
+ }
1322
+ async function sendCronMessage(account, to, message) {
1323
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1324
+ debugLog(`[${timestamp}] [qqbot] sendCronMessage: to=${to}, message length=${message.length}`);
1325
+ const cronResult = decodeCronPayload(message);
1326
+ if (cronResult.isCronPayload) {
1327
+ if (cronResult.error) {
1328
+ debugError(`[${timestamp}] [qqbot] sendCronMessage: cron payload decode error: ${cronResult.error}`);
1329
+ return {
1330
+ channel: "qqbot",
1331
+ error: `Failed to decode cron payload: ${cronResult.error}`
1332
+ };
1333
+ }
1334
+ if (cronResult.payload) {
1335
+ const payload = cronResult.payload;
1336
+ debugLog(`[${timestamp}] [qqbot] sendCronMessage: decoded cron payload, targetType=${payload.targetType}, targetAddress=${payload.targetAddress}, content length=${payload.content.length}`);
1337
+ const targetTo = payload.targetType === "group" ? `group:${payload.targetAddress}` : payload.targetAddress;
1338
+ debugLog(`[${timestamp}] [qqbot] sendCronMessage: sending proactive message to targetTo=${targetTo}`);
1339
+ const result = await sendText({
1340
+ account,
1341
+ to: targetTo,
1342
+ text: payload.content
1343
+ });
1344
+ if (result.error) debugError(`[${timestamp}] [qqbot] sendCronMessage: proactive message failed, error=${result.error}`);
1345
+ else debugLog(`[${timestamp}] [qqbot] sendCronMessage: proactive message sent successfully`);
1346
+ return result;
1347
+ }
1348
+ }
1349
+ debugLog(`[${timestamp}] [qqbot] sendCronMessage: plain text message, sending to ${to}`);
1350
+ return await sendText({
1351
+ account,
1352
+ to,
1353
+ text: message
1354
+ });
1355
+ }
1356
+ //#endregion
1357
+ export { recordMessageReply as C, setOutboundAudioPort as E, getMessageReplyStats as S, OUTBOUND_ERROR_CODES as T, sendVoice as _, sendText as a, checkMessageReplyLimit as b, isMediaPayload as c, buildMediaTarget as d, parseTarget as f, sendVideoMsg as g, sendPhoto as h, sendProactiveMessage as i, parseQQBotPayload as l, sendDocument as m, sendCronMessage as n, encodePayloadForCron as o, resolveOutboundMediaPath as p, sendMedia as r, isCronReminderPayload as s, outbound_exports as t, normalizeMediaTags as u, resolveUserFacingMediaError as v, DEFAULT_MEDIA_SEND_ERROR as w, getMessageReplyConfig as x, MESSAGE_REPLY_LIMIT as y };