@openclaw/qqbot 2026.5.2-beta.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,27 +0,0 @@
1
- import { debugLog } from "../utils/log.js";
2
- import { ReplyLimiter, type ReplyLimitResult } from "./reply-limiter.js";
3
-
4
- const replyLimiter = new ReplyLimiter();
5
-
6
- export type { ReplyLimitResult };
7
-
8
- export const MESSAGE_REPLY_LIMIT = 4;
9
-
10
- export function checkMessageReplyLimit(messageId: string): ReplyLimitResult {
11
- return replyLimiter.checkLimit(messageId);
12
- }
13
-
14
- export function recordMessageReply(messageId: string): void {
15
- replyLimiter.record(messageId);
16
- debugLog(
17
- `[qqbot] recordMessageReply: ${messageId}, count=${replyLimiter.getStats().totalReplies}`,
18
- );
19
- }
20
-
21
- export function getMessageReplyStats(): { trackedMessages: number; totalReplies: number } {
22
- return replyLimiter.getStats();
23
- }
24
-
25
- export function getMessageReplyConfig(): { limit: number; ttlMs: number; ttlHours: number } {
26
- return replyLimiter.getConfig();
27
- }
@@ -1,54 +0,0 @@
1
- import path from "node:path";
2
- import { UPLOAD_PREPARE_FALLBACK_CODE } from "../api/retry.js";
3
- import { MediaFileType } from "../types.js";
4
- import { formatFileSize, getFileTypeName, getMaxUploadSize } from "../utils/file-utils.js";
5
- import {
6
- DEFAULT_MEDIA_SEND_ERROR,
7
- OUTBOUND_ERROR_CODES,
8
- type OutboundResult,
9
- } from "./outbound-types.js";
10
- import { UploadDailyLimitExceededError } from "./sender.js";
11
-
12
- /**
13
- * Convert a media send result into a user-facing message.
14
- */
15
- export function resolveUserFacingMediaError(
16
- result: Pick<OutboundResult, "error" | "errorCode" | "qqBizCode">,
17
- ): string {
18
- if (!result.error) {
19
- return DEFAULT_MEDIA_SEND_ERROR;
20
- }
21
- if (result.qqBizCode === UPLOAD_PREPARE_FALLBACK_CODE) {
22
- return result.error;
23
- }
24
- switch (result.errorCode) {
25
- case OUTBOUND_ERROR_CODES.FILE_TOO_LARGE:
26
- case OUTBOUND_ERROR_CODES.UPLOAD_DAILY_LIMIT_EXCEEDED:
27
- return result.error;
28
- default:
29
- return DEFAULT_MEDIA_SEND_ERROR;
30
- }
31
- }
32
-
33
- export function buildDailyLimitExceededResult(err: UploadDailyLimitExceededError): OutboundResult {
34
- const dir = path.dirname(err.filePath);
35
- const name = path.basename(err.filePath);
36
- const size = formatFileSize(err.fileSize);
37
- return {
38
- channel: "qqbot",
39
- error: `QQBot每天发送文件有累计2G的限制,如果着急的话,可以直接来我的主机copy下载,文件目录\`${dir}/${name}\`(${size})`,
40
- errorCode: OUTBOUND_ERROR_CODES.UPLOAD_DAILY_LIMIT_EXCEEDED,
41
- qqBizCode: UPLOAD_PREPARE_FALLBACK_CODE,
42
- };
43
- }
44
-
45
- export function buildFileTooLargeResult(fileType: MediaFileType, fileSize: number): OutboundResult {
46
- const typeName = getFileTypeName(fileType);
47
- const limit = getMaxUploadSize(fileType);
48
- const limitMB = Math.round(limit / (1024 * 1024));
49
- return {
50
- channel: "qqbot",
51
- error: `${typeName}过大(${formatFileSize(fileSize)}),超过了${limitMB}M,暂时不能通过QQ直接发给你。`,
52
- errorCode: OUTBOUND_ERROR_CODES.FILE_TOO_LARGE,
53
- };
54
- }
@@ -1,45 +0,0 @@
1
- import type { GatewayAccount } from "../types.js";
2
-
3
- export interface OutboundContext {
4
- to: string;
5
- text: string;
6
- accountId?: string | null;
7
- replyToId?: string | null;
8
- account: GatewayAccount;
9
- }
10
-
11
- export interface MediaOutboundContext extends OutboundContext {
12
- mediaUrl: string;
13
- mimeType?: string;
14
- }
15
-
16
- /**
17
- * Stable error codes for outbound media send results.
18
- */
19
- export const OUTBOUND_ERROR_CODES = {
20
- FILE_TOO_LARGE: "file_too_large",
21
- UPLOAD_DAILY_LIMIT_EXCEEDED: "upload_daily_limit_exceeded",
22
- } as const;
23
-
24
- export type OutboundErrorCode = (typeof OUTBOUND_ERROR_CODES)[keyof typeof OUTBOUND_ERROR_CODES];
25
-
26
- export const DEFAULT_MEDIA_SEND_ERROR = "发送失败,请稍后重试。";
27
-
28
- export interface OutboundResult {
29
- channel: string;
30
- messageId?: string;
31
- timestamp?: string | number;
32
- error?: string;
33
- errorCode?: OutboundErrorCode;
34
- qqBizCode?: number;
35
- refIdx?: string;
36
- }
37
-
38
- /** Normalized target information for media sends. */
39
- export interface MediaTargetContext {
40
- targetType: "c2c" | "group" | "channel" | "dm";
41
- targetId: string;
42
- account: GatewayAccount;
43
- replyToId?: string;
44
- logPrefix?: string;
45
- }
@@ -1,485 +0,0 @@
1
- /**
2
- * Outbound messaging — aggregates reply limits, audio port, media sends, and text orchestration.
3
- */
4
-
5
- export { setOutboundAudioPort } from "./outbound-audio-port.js";
6
- export type {
7
- OutboundContext,
8
- MediaOutboundContext,
9
- OutboundResult,
10
- OutboundErrorCode,
11
- MediaTargetContext,
12
- } from "./outbound-types.js";
13
- export { OUTBOUND_ERROR_CODES, DEFAULT_MEDIA_SEND_ERROR } from "./outbound-types.js";
14
-
15
- export {
16
- checkMessageReplyLimit,
17
- recordMessageReply,
18
- getMessageReplyStats,
19
- getMessageReplyConfig,
20
- MESSAGE_REPLY_LIMIT,
21
- } from "./outbound-reply.js";
22
- export type { ReplyLimitResult } from "./outbound-reply.js";
23
-
24
- export { resolveUserFacingMediaError } from "./outbound-result-helpers.js";
25
-
26
- export {
27
- buildMediaTarget,
28
- parseTarget,
29
- resolveOutboundMediaPath,
30
- sendDocument,
31
- sendPhoto,
32
- sendVideoMsg,
33
- sendVoice,
34
- } from "./outbound-media-send.js";
35
-
36
- import type { GatewayAccount } from "../types.js";
37
- import { formatErrorMessage } from "../utils/format.js";
38
- import { debugError, debugLog, debugWarn } from "../utils/log.js";
39
- import { normalizeMediaTags } from "../utils/media-tags.js";
40
- import { decodeCronPayload } from "../utils/payload.js";
41
- import { normalizePath } from "../utils/platform.js";
42
- import {
43
- normalizeLowercaseStringOrEmpty,
44
- normalizeOptionalString,
45
- } from "../utils/string-normalize.js";
46
- import {
47
- isImageFile as coreIsImageFile,
48
- isVideoFile as coreIsVideoFile,
49
- } from "./media-type-detect.js";
50
- import { isAudioFile } from "./outbound-audio-port.js";
51
- import {
52
- buildMediaTarget,
53
- parseTarget,
54
- resolveOutboundMediaPath,
55
- sendDocument,
56
- sendPhoto,
57
- sendVideoMsg,
58
- sendVoice,
59
- } from "./outbound-media-send.js";
60
- import {
61
- checkMessageReplyLimit,
62
- MESSAGE_REPLY_LIMIT,
63
- recordMessageReply,
64
- } from "./outbound-reply.js";
65
- import type {
66
- MediaOutboundContext,
67
- MediaTargetContext,
68
- OutboundContext,
69
- OutboundResult,
70
- } from "./outbound-types.js";
71
- import {
72
- initApiConfig,
73
- accountToCreds,
74
- sendText as senderSendText,
75
- type DeliveryTarget,
76
- } from "./sender.js";
77
-
78
- const isImageFile = coreIsImageFile;
79
- const isVideoFile = coreIsVideoFile;
80
-
81
- /**
82
- * Send text, optionally falling back from passive reply mode to proactive mode.
83
- *
84
- * Also supports inline media tags such as `<qqimg>...</qqimg>`.
85
- */
86
- export async function sendText(ctx: OutboundContext): Promise<OutboundResult> {
87
- const { to, account } = ctx;
88
- let { text, replyToId } = ctx;
89
- let fallbackToProactive = false;
90
-
91
- initApiConfig(account.appId, { markdownSupport: account.markdownSupport });
92
-
93
- debugLog(
94
- "[qqbot] sendText ctx:",
95
- JSON.stringify(
96
- { to, text: text?.slice(0, 50), replyToId, accountId: account.accountId },
97
- null,
98
- 2,
99
- ),
100
- );
101
-
102
- if (replyToId) {
103
- const limitCheck = checkMessageReplyLimit(replyToId);
104
-
105
- if (!limitCheck.allowed) {
106
- if (limitCheck.shouldFallbackToProactive) {
107
- debugWarn(
108
- `[qqbot] sendText: passive reply unavailable, falling back to proactive send - ${limitCheck.message}`,
109
- );
110
- fallbackToProactive = true;
111
- replyToId = null;
112
- } else {
113
- debugError(
114
- `[qqbot] sendText: passive reply was blocked without a fallback path - ${limitCheck.message}`,
115
- );
116
- return {
117
- channel: "qqbot",
118
- error: limitCheck.message,
119
- };
120
- }
121
- } else {
122
- debugLog(
123
- `[qqbot] sendText: remaining passive replies for ${replyToId}: ${limitCheck.remaining}/${MESSAGE_REPLY_LIMIT}`,
124
- );
125
- }
126
- }
127
-
128
- text = normalizeMediaTags(text);
129
-
130
- const mediaTagRegex =
131
- /<(qqimg|qqvoice|qqvideo|qqfile|qqmedia)>([^<>]+)<\/(?:qqimg|qqvoice|qqvideo|qqfile|qqmedia|img)>/gi;
132
- const mediaTagMatches = text.match(mediaTagRegex);
133
-
134
- if (mediaTagMatches && mediaTagMatches.length > 0) {
135
- debugLog(`[qqbot] sendText: Detected ${mediaTagMatches.length} media tag(s), processing...`);
136
-
137
- const sendQueue: Array<{
138
- type: "text" | "image" | "voice" | "video" | "file" | "media";
139
- content: string;
140
- }> = [];
141
-
142
- let lastIndex = 0;
143
- const mediaTagRegexWithIndex =
144
- /<(qqimg|qqvoice|qqvideo|qqfile|qqmedia)>([^<>]+)<\/(?:qqimg|qqvoice|qqvideo|qqfile|qqmedia|img)>/gi;
145
- let match;
146
-
147
- while ((match = mediaTagRegexWithIndex.exec(text)) !== null) {
148
- const textBefore = text
149
- .slice(lastIndex, match.index)
150
- .replace(/\n{3,}/g, "\n\n")
151
- .trim();
152
- if (textBefore) {
153
- sendQueue.push({ type: "text", content: textBefore });
154
- }
155
-
156
- const tagName = normalizeLowercaseStringOrEmpty(match[1]);
157
-
158
- let mediaPath = normalizeOptionalString(match[2]) ?? "";
159
- if (mediaPath.startsWith("MEDIA:")) {
160
- mediaPath = mediaPath.slice("MEDIA:".length);
161
- }
162
- mediaPath = normalizePath(mediaPath);
163
-
164
- mediaPath = mediaPath.replace(/\\\\/g, "\\");
165
-
166
- const isWinLocal = /^[a-zA-Z]:[\\/]/.test(mediaPath) || mediaPath.startsWith("\\\\");
167
- try {
168
- const hasOctal = /\\[0-7]{1,3}/.test(mediaPath);
169
- const hasNonASCII = /[\u0080-\u00FF]/.test(mediaPath);
170
-
171
- if (!isWinLocal && (hasOctal || hasNonASCII)) {
172
- debugLog(`[qqbot] sendText: Decoding path with mixed encoding: ${mediaPath}`);
173
-
174
- let decoded = mediaPath.replace(/\\([0-7]{1,3})/g, (_: string, octal: string) => {
175
- return String.fromCharCode(Number.parseInt(octal, 8));
176
- });
177
-
178
- const bytes: number[] = [];
179
- for (let i = 0; i < decoded.length; i++) {
180
- const code = decoded.charCodeAt(i);
181
- if (code <= 0xff) {
182
- bytes.push(code);
183
- } else {
184
- const charBytes = Buffer.from(decoded[i], "utf8");
185
- bytes.push(...charBytes);
186
- }
187
- }
188
-
189
- const buffer = Buffer.from(bytes);
190
- const utf8Decoded = buffer.toString("utf8");
191
-
192
- if (!utf8Decoded.includes("\uFFFD") || utf8Decoded.length < decoded.length) {
193
- mediaPath = utf8Decoded;
194
- debugLog(`[qqbot] sendText: Successfully decoded path: ${mediaPath}`);
195
- }
196
- }
197
- } catch (decodeErr) {
198
- debugError(
199
- `[qqbot] sendText: Path decode error: ${
200
- decodeErr instanceof Error ? decodeErr.message : JSON.stringify(decodeErr)
201
- }`,
202
- );
203
- }
204
-
205
- if (mediaPath) {
206
- if (tagName === "qqmedia") {
207
- sendQueue.push({ type: "media", content: mediaPath });
208
- debugLog(`[qqbot] sendText: Found auto-detect media in <qqmedia>: ${mediaPath}`);
209
- } else if (tagName === "qqvoice") {
210
- sendQueue.push({ type: "voice", content: mediaPath });
211
- debugLog(`[qqbot] sendText: Found voice path in <qqvoice>: ${mediaPath}`);
212
- } else if (tagName === "qqvideo") {
213
- sendQueue.push({ type: "video", content: mediaPath });
214
- debugLog(`[qqbot] sendText: Found video URL in <qqvideo>: ${mediaPath}`);
215
- } else if (tagName === "qqfile") {
216
- sendQueue.push({ type: "file", content: mediaPath });
217
- debugLog(`[qqbot] sendText: Found file path in <qqfile>: ${mediaPath}`);
218
- } else {
219
- sendQueue.push({ type: "image", content: mediaPath });
220
- debugLog(`[qqbot] sendText: Found image path in <qqimg>: ${mediaPath}`);
221
- }
222
- }
223
-
224
- lastIndex = match.index + match[0].length;
225
- }
226
-
227
- const textAfter = text
228
- .slice(lastIndex)
229
- .replace(/\n{3,}/g, "\n\n")
230
- .trim();
231
- if (textAfter) {
232
- sendQueue.push({ type: "text", content: textAfter });
233
- }
234
-
235
- debugLog(`[qqbot] sendText: Send queue: ${sendQueue.map((item) => item.type).join(" -> ")}`);
236
-
237
- const mediaTarget = buildMediaTarget({ to, account, replyToId });
238
- let lastResult: OutboundResult = { channel: "qqbot" };
239
-
240
- for (const item of sendQueue) {
241
- try {
242
- if (item.type === "text") {
243
- const target = parseTarget(to);
244
- const creds = accountToCreds(account);
245
- const deliveryTarget: DeliveryTarget = {
246
- type: target.type === "channel" ? "channel" : target.type,
247
- id: target.id,
248
- };
249
- const result = await senderSendText(deliveryTarget, item.content, creds, {
250
- msgId: replyToId ?? undefined,
251
- });
252
- if (replyToId) {
253
- recordMessageReply(replyToId);
254
- }
255
- lastResult = {
256
- channel: "qqbot",
257
- messageId: result.id,
258
- timestamp: result.timestamp,
259
- refIdx: result.ext_info?.ref_idx,
260
- };
261
- debugLog(`[qqbot] sendText: Sent text part: ${item.content.slice(0, 30)}...`);
262
- } else if (item.type === "image") {
263
- lastResult = await sendPhoto(mediaTarget, item.content);
264
- } else if (item.type === "voice") {
265
- lastResult = await sendVoice(
266
- mediaTarget,
267
- item.content,
268
- undefined,
269
- account.config?.audioFormatPolicy?.transcodeEnabled !== false,
270
- );
271
- } else if (item.type === "video") {
272
- lastResult = await sendVideoMsg(mediaTarget, item.content);
273
- } else if (item.type === "file") {
274
- lastResult = await sendDocument(mediaTarget, item.content);
275
- } else if (item.type === "media") {
276
- lastResult = await sendMedia({
277
- to,
278
- text: "",
279
- mediaUrl: item.content,
280
- accountId: account.accountId,
281
- replyToId,
282
- account,
283
- });
284
- }
285
- } catch (err) {
286
- const errMsg = formatErrorMessage(err);
287
- debugError(`[qqbot] sendText: Failed to send ${item.type}: ${errMsg}`);
288
- lastResult = { channel: "qqbot", error: errMsg };
289
- }
290
- }
291
-
292
- return lastResult;
293
- }
294
-
295
- if (!replyToId) {
296
- if (!text || text.trim().length === 0) {
297
- debugError("[qqbot] sendText error: proactive message content cannot be empty");
298
- return {
299
- channel: "qqbot",
300
- error: "Proactive messages require non-empty content (--message cannot be empty)",
301
- };
302
- }
303
- if (fallbackToProactive) {
304
- debugLog(
305
- `[qqbot] sendText: [fallback] sending proactive message to ${to}, length=${text.length}`,
306
- );
307
- } else {
308
- debugLog(`[qqbot] sendText: sending proactive message to ${to}, length=${text.length}`);
309
- }
310
- }
311
-
312
- if (!account.appId || !account.clientSecret) {
313
- return { channel: "qqbot", error: "QQBot not configured (missing appId or clientSecret)" };
314
- }
315
-
316
- try {
317
- const target = parseTarget(to);
318
- const creds = accountToCreds(account);
319
- const deliveryTarget: DeliveryTarget = {
320
- type: target.type === "channel" ? "channel" : target.type,
321
- id: target.id,
322
- };
323
- debugLog("[qqbot] sendText target:", JSON.stringify(target));
324
-
325
- const result = await senderSendText(deliveryTarget, text, creds, {
326
- msgId: replyToId ?? undefined,
327
- });
328
- if (replyToId) {
329
- recordMessageReply(replyToId);
330
- }
331
- return {
332
- channel: "qqbot",
333
- messageId: result.id,
334
- timestamp: result.timestamp,
335
- refIdx: result.ext_info?.ref_idx,
336
- };
337
- } catch (err) {
338
- const message = formatErrorMessage(err);
339
- return { channel: "qqbot", error: message };
340
- }
341
- }
342
-
343
- /** Send rich media, auto-routing by media type and source. */
344
- export async function sendMedia(ctx: MediaOutboundContext): Promise<OutboundResult> {
345
- const { to, text, replyToId, account, mimeType } = ctx;
346
-
347
- initApiConfig(account.appId, { markdownSupport: account.markdownSupport });
348
-
349
- if (!account.appId || !account.clientSecret) {
350
- return { channel: "qqbot", error: "QQBot not configured (missing appId or clientSecret)" };
351
- }
352
- if (!ctx.mediaUrl) {
353
- return { channel: "qqbot", error: "mediaUrl is required for sendMedia" };
354
- }
355
-
356
- const resolvedMediaPath = resolveOutboundMediaPath(ctx.mediaUrl, "media", {
357
- allowMissingLocalPath: true,
358
- });
359
- if (!resolvedMediaPath.ok) {
360
- return { channel: "qqbot", error: resolvedMediaPath.error };
361
- }
362
- const mediaUrl = resolvedMediaPath.mediaPath;
363
-
364
- const target = buildMediaTarget({ to, account, replyToId });
365
-
366
- if (isAudioFile(mediaUrl, mimeType)) {
367
- const formats =
368
- account.config?.audioFormatPolicy?.uploadDirectFormats ??
369
- account.config?.voiceDirectUploadFormats;
370
- const transcodeEnabled = account.config?.audioFormatPolicy?.transcodeEnabled !== false;
371
- const result = await sendVoice(target, mediaUrl, formats, transcodeEnabled);
372
- if (!result.error) {
373
- if (text?.trim()) {
374
- await sendTextAfterMedia(target, text);
375
- }
376
- return result;
377
- }
378
- const voiceError = result.error;
379
- debugWarn(`[qqbot] sendMedia: sendVoice failed (${voiceError}), falling back to sendDocument`);
380
- const fallback = await sendDocument(target, mediaUrl);
381
- if (!fallback.error) {
382
- if (text?.trim()) {
383
- await sendTextAfterMedia(target, text);
384
- }
385
- return fallback;
386
- }
387
- return { channel: "qqbot", error: `voice: ${voiceError} | fallback file: ${fallback.error}` };
388
- }
389
-
390
- if (isVideoFile(mediaUrl, mimeType)) {
391
- const result = await sendVideoMsg(target, mediaUrl);
392
- if (!result.error && text?.trim()) {
393
- await sendTextAfterMedia(target, text);
394
- }
395
- return result;
396
- }
397
-
398
- if (
399
- !isImageFile(mediaUrl, mimeType) &&
400
- !isAudioFile(mediaUrl, mimeType) &&
401
- !isVideoFile(mediaUrl, mimeType)
402
- ) {
403
- const result = await sendDocument(target, mediaUrl);
404
- if (!result.error && text?.trim()) {
405
- await sendTextAfterMedia(target, text);
406
- }
407
- return result;
408
- }
409
-
410
- const result = await sendPhoto(target, mediaUrl);
411
- if (!result.error && text?.trim()) {
412
- await sendTextAfterMedia(target, text);
413
- }
414
- return result;
415
- }
416
-
417
- async function sendTextAfterMedia(ctx: MediaTargetContext, text: string): Promise<void> {
418
- try {
419
- const creds = accountToCreds(ctx.account);
420
- const target: DeliveryTarget = { type: ctx.targetType, id: ctx.targetId };
421
- await senderSendText(target, text, creds, { msgId: ctx.replyToId });
422
- } catch (err) {
423
- debugError(`[qqbot] sendTextAfterMedia failed: ${formatErrorMessage(err)}`);
424
- }
425
- }
426
-
427
- export async function sendProactiveMessage(
428
- account: GatewayAccount,
429
- to: string,
430
- content: string,
431
- ): Promise<OutboundResult> {
432
- return sendText({ account, to, text: content });
433
- }
434
-
435
- export async function sendCronMessage(
436
- account: GatewayAccount,
437
- to: string,
438
- message: string,
439
- ): Promise<OutboundResult> {
440
- const timestamp = new Date().toISOString();
441
- debugLog(`[${timestamp}] [qqbot] sendCronMessage: to=${to}, message length=${message.length}`);
442
-
443
- const cronResult = decodeCronPayload(message);
444
-
445
- if (cronResult.isCronPayload) {
446
- if (cronResult.error) {
447
- debugError(
448
- `[${timestamp}] [qqbot] sendCronMessage: cron payload decode error: ${cronResult.error}`,
449
- );
450
- return {
451
- channel: "qqbot",
452
- error: `Failed to decode cron payload: ${cronResult.error}`,
453
- };
454
- }
455
-
456
- if (cronResult.payload) {
457
- const payload = cronResult.payload;
458
- debugLog(
459
- `[${timestamp}] [qqbot] sendCronMessage: decoded cron payload, targetType=${payload.targetType}, targetAddress=${payload.targetAddress}, content length=${payload.content.length}`,
460
- );
461
-
462
- const targetTo =
463
- payload.targetType === "group" ? `group:${payload.targetAddress}` : payload.targetAddress;
464
-
465
- debugLog(
466
- `[${timestamp}] [qqbot] sendCronMessage: sending proactive message to targetTo=${targetTo}`,
467
- );
468
-
469
- const result = await sendText({ account, to: targetTo, text: payload.content });
470
-
471
- if (result.error) {
472
- debugError(
473
- `[${timestamp}] [qqbot] sendCronMessage: proactive message failed, error=${result.error}`,
474
- );
475
- } else {
476
- debugLog(`[${timestamp}] [qqbot] sendCronMessage: proactive message sent successfully`);
477
- }
478
-
479
- return result;
480
- }
481
- }
482
-
483
- debugLog(`[${timestamp}] [qqbot] sendCronMessage: plain text message, sending to ${to}`);
484
- return await sendText({ account, to, text: message });
485
- }