@openclaw/qqbot 2026.5.2 → 2026.5.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/dist/api.js +646 -0
  2. package/dist/approval-cg0SVahb.js +94 -0
  3. package/dist/channel-N6Y_Rcjp.js +551 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.setup-ByCRAe5H.js +25 -0
  6. package/dist/config-D6545NkC.js +94 -0
  7. package/dist/config-schema-DFcjQw73.js +309 -0
  8. package/dist/exec-approvals-COUsM6wZ.js +138 -0
  9. package/dist/gateway-CmSUJKSt.js +6726 -0
  10. package/dist/handler-runtime-Bqm6N0WG.js +120 -0
  11. package/dist/index.js +29 -0
  12. package/dist/narrowing-BoieBTIU.js +25 -0
  13. package/dist/outbound-BJfhwrPg.js +1357 -0
  14. package/dist/request-context-DXtpwWui.js +921 -0
  15. package/dist/resolve-D_06fV6-.js +284 -0
  16. package/dist/runtime-BJAS3eXW.js +18 -0
  17. package/dist/runtime-api.js +2 -0
  18. package/dist/secret-contract-api.js +56 -0
  19. package/dist/sender-p-B14eLG.js +1987 -0
  20. package/dist/setup-entry.js +15 -0
  21. package/dist/setup-plugin-api.js +2 -0
  22. package/dist/string-normalize-Ci6NM5DE.js +98 -0
  23. package/dist/target-parser-Y0prnrXD.js +245 -0
  24. package/package.json +15 -6
  25. package/api.ts +0 -56
  26. package/channel-plugin-api.ts +0 -1
  27. package/index.ts +0 -29
  28. package/runtime-api.ts +0 -9
  29. package/setup-entry.ts +0 -9
  30. package/setup-plugin-api.ts +0 -3
  31. package/src/bridge/approval/capability.ts +0 -237
  32. package/src/bridge/approval/handler-runtime.ts +0 -204
  33. package/src/bridge/bootstrap.ts +0 -135
  34. package/src/bridge/channel-entry.ts +0 -18
  35. package/src/bridge/commands/framework-context-adapter.ts +0 -60
  36. package/src/bridge/commands/framework-registration.ts +0 -47
  37. package/src/bridge/commands/from-parser.test.ts +0 -86
  38. package/src/bridge/commands/from-parser.ts +0 -60
  39. package/src/bridge/commands/result-dispatcher.ts +0 -76
  40. package/src/bridge/config-shared.ts +0 -132
  41. package/src/bridge/config.ts +0 -111
  42. package/src/bridge/gateway.ts +0 -174
  43. package/src/bridge/logger.ts +0 -31
  44. package/src/bridge/narrowing.ts +0 -31
  45. package/src/bridge/plugin-version.test.ts +0 -146
  46. package/src/bridge/plugin-version.ts +0 -102
  47. package/src/bridge/runtime.ts +0 -25
  48. package/src/bridge/sdk-adapter.ts +0 -131
  49. package/src/bridge/setup/finalize.ts +0 -144
  50. package/src/bridge/setup/surface.ts +0 -34
  51. package/src/bridge/tools/channel.ts +0 -58
  52. package/src/bridge/tools/index.ts +0 -15
  53. package/src/bridge/tools/remind.test.ts +0 -124
  54. package/src/bridge/tools/remind.ts +0 -91
  55. package/src/channel.setup.ts +0 -33
  56. package/src/channel.ts +0 -288
  57. package/src/command-auth.test.ts +0 -62
  58. package/src/config-schema.ts +0 -84
  59. package/src/config.test.ts +0 -364
  60. package/src/engine/access/access-control.test.ts +0 -198
  61. package/src/engine/access/access-control.ts +0 -226
  62. package/src/engine/access/index.ts +0 -16
  63. package/src/engine/access/resolve-policy.test.ts +0 -59
  64. package/src/engine/access/resolve-policy.ts +0 -57
  65. package/src/engine/access/sender-match.test.ts +0 -60
  66. package/src/engine/access/sender-match.ts +0 -55
  67. package/src/engine/access/types.ts +0 -53
  68. package/src/engine/adapter/audio.port.ts +0 -27
  69. package/src/engine/adapter/commands.port.ts +0 -22
  70. package/src/engine/adapter/history.port.ts +0 -52
  71. package/src/engine/adapter/index.ts +0 -139
  72. package/src/engine/adapter/mention-gate.port.ts +0 -50
  73. package/src/engine/adapter/types.ts +0 -38
  74. package/src/engine/api/api-client.ts +0 -212
  75. package/src/engine/api/media-chunked.test.ts +0 -336
  76. package/src/engine/api/media-chunked.ts +0 -622
  77. package/src/engine/api/media.ts +0 -218
  78. package/src/engine/api/messages.ts +0 -293
  79. package/src/engine/api/retry.ts +0 -217
  80. package/src/engine/api/routes.ts +0 -95
  81. package/src/engine/api/token.ts +0 -271
  82. package/src/engine/approval/index.test.ts +0 -22
  83. package/src/engine/approval/index.ts +0 -224
  84. package/src/engine/commands/builtin/log-helpers.ts +0 -319
  85. package/src/engine/commands/builtin/register-all.ts +0 -17
  86. package/src/engine/commands/builtin/register-approve.ts +0 -201
  87. package/src/engine/commands/builtin/register-basic.ts +0 -95
  88. package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
  89. package/src/engine/commands/builtin/register-logs.ts +0 -20
  90. package/src/engine/commands/builtin/register-streaming.ts +0 -137
  91. package/src/engine/commands/builtin/state.ts +0 -31
  92. package/src/engine/commands/slash-command-auth.ts +0 -48
  93. package/src/engine/commands/slash-command-handler.ts +0 -146
  94. package/src/engine/commands/slash-commands-impl.test.ts +0 -8
  95. package/src/engine/commands/slash-commands-impl.ts +0 -61
  96. package/src/engine/commands/slash-commands.ts +0 -199
  97. package/src/engine/config/credential-backup.test.ts +0 -88
  98. package/src/engine/config/credential-backup.ts +0 -107
  99. package/src/engine/config/credentials.ts +0 -76
  100. package/src/engine/config/group.test.ts +0 -234
  101. package/src/engine/config/group.ts +0 -299
  102. package/src/engine/config/resolve.test.ts +0 -152
  103. package/src/engine/config/resolve.ts +0 -283
  104. package/src/engine/config/setup-logic.ts +0 -84
  105. package/src/engine/engine-import-boundary.test.ts +0 -73
  106. package/src/engine/gateway/codec.ts +0 -47
  107. package/src/engine/gateway/constants.ts +0 -117
  108. package/src/engine/gateway/event-dispatcher.ts +0 -177
  109. package/src/engine/gateway/gateway-connection.ts +0 -371
  110. package/src/engine/gateway/gateway.ts +0 -291
  111. package/src/engine/gateway/inbound-attachments.test.ts +0 -126
  112. package/src/engine/gateway/inbound-attachments.ts +0 -360
  113. package/src/engine/gateway/inbound-context.ts +0 -195
  114. package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
  115. package/src/engine/gateway/inbound-pipeline.ts +0 -235
  116. package/src/engine/gateway/interaction-handler.ts +0 -220
  117. package/src/engine/gateway/message-queue.test.ts +0 -282
  118. package/src/engine/gateway/message-queue.ts +0 -499
  119. package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
  120. package/src/engine/gateway/outbound-dispatch.ts +0 -575
  121. package/src/engine/gateway/reconnect.ts +0 -199
  122. package/src/engine/gateway/stages/access-stage.ts +0 -132
  123. package/src/engine/gateway/stages/assembly-stage.ts +0 -156
  124. package/src/engine/gateway/stages/content-stage.test.ts +0 -77
  125. package/src/engine/gateway/stages/content-stage.ts +0 -77
  126. package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
  127. package/src/engine/gateway/stages/envelope-stage.ts +0 -144
  128. package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
  129. package/src/engine/gateway/stages/index.ts +0 -18
  130. package/src/engine/gateway/stages/quote-stage.ts +0 -113
  131. package/src/engine/gateway/stages/refidx-stage.ts +0 -62
  132. package/src/engine/gateway/stages/stub-contexts.ts +0 -116
  133. package/src/engine/gateway/types.ts +0 -264
  134. package/src/engine/gateway/typing-keepalive.ts +0 -79
  135. package/src/engine/group/activation.test.ts +0 -114
  136. package/src/engine/group/activation.ts +0 -147
  137. package/src/engine/group/history.test.ts +0 -314
  138. package/src/engine/group/history.ts +0 -321
  139. package/src/engine/group/mention.test.ts +0 -141
  140. package/src/engine/group/mention.ts +0 -197
  141. package/src/engine/group/message-gating.test.ts +0 -188
  142. package/src/engine/group/message-gating.ts +0 -216
  143. package/src/engine/messaging/decode-media-path.ts +0 -82
  144. package/src/engine/messaging/media-source.ts +0 -215
  145. package/src/engine/messaging/media-type-detect.ts +0 -37
  146. package/src/engine/messaging/outbound-audio-port.ts +0 -38
  147. package/src/engine/messaging/outbound-deliver.ts +0 -810
  148. package/src/engine/messaging/outbound-media-send.ts +0 -702
  149. package/src/engine/messaging/outbound-reply.ts +0 -27
  150. package/src/engine/messaging/outbound-result-helpers.ts +0 -54
  151. package/src/engine/messaging/outbound-types.ts +0 -45
  152. package/src/engine/messaging/outbound.ts +0 -485
  153. package/src/engine/messaging/reply-dispatcher.ts +0 -597
  154. package/src/engine/messaging/reply-limiter.ts +0 -164
  155. package/src/engine/messaging/sender.ts +0 -729
  156. package/src/engine/messaging/streaming-c2c.ts +0 -1192
  157. package/src/engine/messaging/streaming-media-send.ts +0 -544
  158. package/src/engine/messaging/target-parser.ts +0 -104
  159. package/src/engine/ref/format-message-ref.ts +0 -142
  160. package/src/engine/ref/format-ref-entry.test.ts +0 -60
  161. package/src/engine/ref/format-ref-entry.ts +0 -27
  162. package/src/engine/ref/store.ts +0 -208
  163. package/src/engine/ref/types.ts +0 -27
  164. package/src/engine/session/known-users.ts +0 -137
  165. package/src/engine/session/session-store.ts +0 -204
  166. package/src/engine/tools/channel-api.ts +0 -244
  167. package/src/engine/tools/remind-logic.test.ts +0 -280
  168. package/src/engine/tools/remind-logic.ts +0 -377
  169. package/src/engine/types.ts +0 -313
  170. package/src/engine/utils/attachment-tags.test.ts +0 -186
  171. package/src/engine/utils/attachment-tags.ts +0 -174
  172. package/src/engine/utils/audio.test.ts +0 -250
  173. package/src/engine/utils/audio.ts +0 -585
  174. package/src/engine/utils/data-paths.ts +0 -38
  175. package/src/engine/utils/diagnostics.ts +0 -109
  176. package/src/engine/utils/file-utils.test.ts +0 -72
  177. package/src/engine/utils/file-utils.ts +0 -225
  178. package/src/engine/utils/format.test.ts +0 -68
  179. package/src/engine/utils/format.ts +0 -70
  180. package/src/engine/utils/image-size.test.ts +0 -158
  181. package/src/engine/utils/image-size.ts +0 -249
  182. package/src/engine/utils/log.test.ts +0 -28
  183. package/src/engine/utils/log.ts +0 -61
  184. package/src/engine/utils/media-tags.test.ts +0 -32
  185. package/src/engine/utils/media-tags.ts +0 -177
  186. package/src/engine/utils/payload.test.ts +0 -68
  187. package/src/engine/utils/payload.ts +0 -145
  188. package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
  189. package/src/engine/utils/platform.test.ts +0 -148
  190. package/src/engine/utils/platform.ts +0 -343
  191. package/src/engine/utils/request-context.ts +0 -60
  192. package/src/engine/utils/string-normalize.ts +0 -91
  193. package/src/engine/utils/stt.test.ts +0 -104
  194. package/src/engine/utils/stt.ts +0 -100
  195. package/src/engine/utils/text-parsing.test.ts +0 -29
  196. package/src/engine/utils/text-parsing.ts +0 -155
  197. package/src/engine/utils/upload-cache.ts +0 -96
  198. package/src/engine/utils/voice-text.ts +0 -15
  199. package/src/exec-approvals.ts +0 -218
  200. package/src/manifest-schema.test.ts +0 -56
  201. package/src/qqbot-test-support.ts +0 -29
  202. package/src/types.ts +0 -210
  203. package/tsconfig.json +0 -16
@@ -1,152 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { GroupMessageGateResult } from "../../group/message-gating.js";
3
- import type { ProcessedAttachments } from "../inbound-attachments.js";
4
- import type { InboundGroupInfo } from "../inbound-context.js";
5
- import {
6
- buildDynamicCtx,
7
- buildGroupSystemPrompt,
8
- buildQuotePart,
9
- classifyMedia,
10
- } from "./envelope-stage.js";
11
-
12
- function makeGate(): GroupMessageGateResult {
13
- return { action: "pass", effectiveWasMentioned: true, shouldBypassMention: false };
14
- }
15
-
16
- function makeGroupInfo(partial: Partial<InboundGroupInfo["display"]> = {}): InboundGroupInfo {
17
- return {
18
- gate: makeGate(),
19
- activation: "mention",
20
- historyLimit: 50,
21
- isMerged: false,
22
- display: {
23
- groupName: "G",
24
- senderLabel: "S",
25
- ...partial,
26
- },
27
- };
28
- }
29
-
30
- describe("envelope-stage", () => {
31
- describe("buildQuotePart", () => {
32
- it("returns empty string when no replyTo", () => {
33
- expect(buildQuotePart(undefined)).toBe("");
34
- });
35
-
36
- it("wraps a quoted body in begin/end tags", () => {
37
- const out = buildQuotePart({ id: "R1", body: "hello", isQuote: true });
38
- expect(out).toContain("[Quoted message begins]");
39
- expect(out).toContain("hello");
40
- expect(out).toContain("[Quoted message ends]");
41
- });
42
-
43
- it("uses a fallback line when body is missing", () => {
44
- const out = buildQuotePart({ id: "R1", isQuote: true });
45
- expect(out).toContain("Original content unavailable");
46
- });
47
- });
48
-
49
- describe("buildDynamicCtx", () => {
50
- it("returns empty string when every list is empty", () => {
51
- expect(
52
- buildDynamicCtx({
53
- imageUrls: [],
54
- uniqueVoicePaths: [],
55
- uniqueVoiceUrls: [],
56
- uniqueVoiceAsrReferTexts: [],
57
- }),
58
- ).toBe("");
59
- });
60
-
61
- it("renders images / voice / asr when present", () => {
62
- const out = buildDynamicCtx({
63
- imageUrls: ["https://x/a.png", "https://x/b.png"],
64
- uniqueVoicePaths: ["/tmp/v.wav"],
65
- uniqueVoiceUrls: ["https://x/v.wav"],
66
- uniqueVoiceAsrReferTexts: ["hi", "there"],
67
- });
68
- expect(out).toContain("- Images: https://x/a.png, https://x/b.png");
69
- expect(out).toContain("- Voice: /tmp/v.wav, https://x/v.wav");
70
- expect(out).toContain("- ASR: hi | there");
71
- // Trailing blank line.
72
- expect(out.endsWith("\n\n")).toBe(true);
73
- });
74
- });
75
-
76
- describe("buildGroupSystemPrompt", () => {
77
- it("returns undefined when no prompts exist", () => {
78
- expect(buildGroupSystemPrompt("", undefined)).toBeUndefined();
79
- });
80
-
81
- it("joins accountSystemInstruction + introHint + behaviorPrompt", () => {
82
- const out = buildGroupSystemPrompt(
83
- "ACCOUNT",
84
- makeGroupInfo({ introHint: "INTRO", behaviorPrompt: "BEHAVIOR" }),
85
- );
86
- expect(out).toBe("ACCOUNT\nINTRO\nBEHAVIOR");
87
- });
88
-
89
- it("skips undefined parts cleanly", () => {
90
- const out = buildGroupSystemPrompt("", makeGroupInfo({ behaviorPrompt: "B" }));
91
- expect(out).toBe("B");
92
- });
93
- });
94
-
95
- describe("classifyMedia", () => {
96
- const emptyProcessed: ProcessedAttachments = {
97
- attachmentInfo: "",
98
- imageUrls: [],
99
- imageMediaTypes: [],
100
- voiceAttachmentPaths: [],
101
- voiceAttachmentUrls: [],
102
- voiceAsrReferTexts: [],
103
- voiceTranscripts: [],
104
- voiceTranscriptSources: [],
105
- attachmentLocalPaths: [],
106
- };
107
-
108
- it("separates local from remote image URLs", () => {
109
- const out = classifyMedia({
110
- ...emptyProcessed,
111
- imageUrls: ["/tmp/a.png", "https://x/b.png", "http://x/c.png"],
112
- imageMediaTypes: ["image/png", "image/jpeg", "image/gif"],
113
- });
114
- expect(out.localMediaPaths).toEqual(["/tmp/a.png"]);
115
- expect(out.remoteMediaUrls).toEqual(["https://x/b.png", "http://x/c.png"]);
116
- expect(out.remoteMediaTypes).toEqual(["image/jpeg", "image/gif"]);
117
- });
118
-
119
- it("defaults missing media type to image/png", () => {
120
- // When `imageMediaTypes[i]` is undefined (shorter than imageUrls),
121
- // the classifier substitutes a default.
122
- const out = classifyMedia({
123
- ...emptyProcessed,
124
- imageUrls: ["https://x/a.png"],
125
- imageMediaTypes: [],
126
- });
127
- expect(out.remoteMediaTypes).toEqual(["image/png"]);
128
- });
129
-
130
- it("dedupes voice paths and URLs", () => {
131
- const out = classifyMedia({
132
- ...emptyProcessed,
133
- voiceAttachmentPaths: ["/a", "/a", "/b"],
134
- voiceAttachmentUrls: ["u1", "u1"],
135
- voiceAsrReferTexts: ["x", "", "x"],
136
- });
137
- expect(out.uniqueVoicePaths).toEqual(["/a", "/b"]);
138
- expect(out.uniqueVoiceUrls).toEqual(["u1"]);
139
- expect(out.uniqueVoiceAsrReferTexts).toEqual(["x"]);
140
- });
141
-
142
- it("flags ASR fallback when transcriptSources contains 'asr'", () => {
143
- expect(
144
- classifyMedia({ ...emptyProcessed, voiceTranscriptSources: ["stt", "asr"] })
145
- .hasAsrReferFallback,
146
- ).toBe(true);
147
- expect(
148
- classifyMedia({ ...emptyProcessed, voiceTranscriptSources: ["stt"] }).hasAsrReferFallback,
149
- ).toBe(false);
150
- });
151
- });
152
- });
@@ -1,144 +0,0 @@
1
- /**
2
- * Envelope stage — render the Web UI body, the dynamic-context block,
3
- * the final group system prompt, and the media classification arrays.
4
- *
5
- * All logic here is presentation-layer glue: it combines fields built by
6
- * earlier stages into the display-friendly strings the outbound
7
- * dispatcher needs. No decisions / gating.
8
- */
9
-
10
- import type { ProcessedAttachments } from "../inbound-attachments.js";
11
- import type { InboundGroupInfo, InboundPipelineDeps, ReplyToInfo } from "../inbound-context.js";
12
- import type { QueuedMessage } from "../message-queue.js";
13
-
14
- // ─────────────────────────── Envelope body ───────────────────────────
15
-
16
- interface BuildBodyInput {
17
- event: QueuedMessage;
18
- deps: InboundPipelineDeps;
19
- userContent: string;
20
- isGroupChat: boolean;
21
- imageUrls: string[];
22
- }
23
-
24
- /** Format the inbound envelope (Web UI body). */
25
- export function buildBody(input: BuildBodyInput): string {
26
- const { event, deps, userContent, isGroupChat, imageUrls } = input;
27
- const envelopeOptions = deps.runtime.channel.reply.resolveEnvelopeFormatOptions(deps.cfg);
28
- return deps.runtime.channel.reply.formatInboundEnvelope({
29
- channel: "qqbot",
30
- from: event.senderName ?? event.senderId,
31
- timestamp: new Date(event.timestamp).getTime(),
32
- body: userContent,
33
- chatType: isGroupChat ? "group" : "direct",
34
- sender: { id: event.senderId, name: event.senderName },
35
- envelope: envelopeOptions,
36
- ...(imageUrls.length > 0 ? { imageUrls } : {}),
37
- });
38
- }
39
-
40
- // ─────────────────────────── Quote / dynamic ctx ───────────────────────────
41
-
42
- /** Render the `[Quoted message begins]...[ends]` block (empty if no reply-to). */
43
- export function buildQuotePart(replyTo?: ReplyToInfo): string {
44
- if (!replyTo) {
45
- return "";
46
- }
47
- return replyTo.body
48
- ? `[Quoted message begins]\n${replyTo.body}\n[Quoted message ends]\n`
49
- : `[Quoted message begins]\nOriginal content unavailable\n[Quoted message ends]\n`;
50
- }
51
-
52
- interface BuildDynamicCtxInput {
53
- imageUrls: string[];
54
- uniqueVoicePaths: string[];
55
- uniqueVoiceUrls: string[];
56
- uniqueVoiceAsrReferTexts: string[];
57
- }
58
-
59
- /** Render the per-message dynamic metadata block (images / voice / ASR). */
60
- export function buildDynamicCtx(input: BuildDynamicCtxInput): string {
61
- const lines: string[] = [];
62
- if (input.imageUrls.length > 0) {
63
- lines.push(`- Images: ${input.imageUrls.join(", ")}`);
64
- }
65
- if (input.uniqueVoicePaths.length > 0 || input.uniqueVoiceUrls.length > 0) {
66
- lines.push(`- Voice: ${[...input.uniqueVoicePaths, ...input.uniqueVoiceUrls].join(", ")}`);
67
- }
68
- if (input.uniqueVoiceAsrReferTexts.length > 0) {
69
- lines.push(`- ASR: ${input.uniqueVoiceAsrReferTexts.join(" | ")}`);
70
- }
71
- return lines.length > 0 ? lines.join("\n") + "\n\n" : "";
72
- }
73
-
74
- // ─────────────────────────── System prompt ───────────────────────────
75
-
76
- /** Combine account-level system prompt with group-specific prompts. */
77
- export function buildGroupSystemPrompt(
78
- accountSystemInstruction: string,
79
- groupInfo: InboundGroupInfo | undefined,
80
- ): string | undefined {
81
- const parts: string[] = [];
82
- if (accountSystemInstruction) {
83
- parts.push(accountSystemInstruction);
84
- }
85
- if (groupInfo?.display.introHint) {
86
- parts.push(groupInfo.display.introHint);
87
- }
88
- if (groupInfo?.display.behaviorPrompt) {
89
- parts.push(groupInfo.display.behaviorPrompt);
90
- }
91
- const combined = parts.filter(Boolean).join("\n");
92
- return combined || undefined;
93
- }
94
-
95
- // ─────────────────────────── Media classification ───────────────────────────
96
-
97
- interface MediaClassification {
98
- localMediaPaths: string[];
99
- localMediaTypes: string[];
100
- remoteMediaUrls: string[];
101
- remoteMediaTypes: string[];
102
- uniqueVoicePaths: string[];
103
- uniqueVoiceUrls: string[];
104
- uniqueVoiceAsrReferTexts: string[];
105
- voiceMediaTypes: string[];
106
- hasAsrReferFallback: boolean;
107
- voiceTranscriptSources: string[];
108
- }
109
-
110
- /** Classify image URLs into local vs remote and de-duplicate voice arrays. */
111
- export function classifyMedia(processed: ProcessedAttachments): MediaClassification {
112
- const localMediaPaths: string[] = [];
113
- const localMediaTypes: string[] = [];
114
- const remoteMediaUrls: string[] = [];
115
- const remoteMediaTypes: string[] = [];
116
- for (let i = 0; i < processed.imageUrls.length; i++) {
117
- const u = processed.imageUrls[i];
118
- const t = processed.imageMediaTypes[i] ?? "image/png";
119
- if (u.startsWith("http://") || u.startsWith("https://")) {
120
- remoteMediaUrls.push(u);
121
- remoteMediaTypes.push(t);
122
- } else {
123
- localMediaPaths.push(u);
124
- localMediaTypes.push(t);
125
- }
126
- }
127
-
128
- const uniqueVoicePaths = [...new Set(processed.voiceAttachmentPaths)];
129
- const uniqueVoiceUrls = [...new Set(processed.voiceAttachmentUrls)];
130
- const voiceMediaTypes = [...uniqueVoicePaths, ...uniqueVoiceUrls].map(() => "audio/wav");
131
-
132
- return {
133
- localMediaPaths,
134
- localMediaTypes,
135
- remoteMediaUrls,
136
- remoteMediaTypes,
137
- uniqueVoicePaths,
138
- uniqueVoiceUrls,
139
- uniqueVoiceAsrReferTexts: [...new Set(processed.voiceAsrReferTexts)].filter(Boolean),
140
- voiceMediaTypes,
141
- hasAsrReferFallback: processed.voiceTranscriptSources.includes("asr"),
142
- voiceTranscriptSources: processed.voiceTranscriptSources,
143
- };
144
- }
@@ -1,292 +0,0 @@
1
- /**
2
- * Group-gate stage — for `type === "group"` inbound events, decide
3
- * whether the message should pass to AI dispatch or be intercepted.
4
- *
5
- * Three possible outcomes:
6
- * - `{ kind: "pass", groupInfo }` — continue the pipeline
7
- * - `{ kind: "skip", groupInfo, skipReason }` — buffered to history
8
- * (if applicable) and short-circuit
9
- * - No group info at all — returned when the event isn't a group event
10
- * (caller should treat as a straight pass-through)
11
- *
12
- * Consolidates the control-command auth check, session-store
13
- * activation override, mention detection, and the unified
14
- * {@link resolveGroupMessageGate} call. Delegates all pure logic to
15
- * existing `engine/group/*` modules so this stage remains a thin
16
- * orchestrator.
17
- */
18
-
19
- import { createQQBotSenderMatcher, normalizeQQBotAllowFrom } from "../../access/index.js";
20
- import { DEFAULT_GROUP_PROMPT, resolveGroupSettings } from "../../config/group.js";
21
- import { resolveGroupActivation } from "../../group/activation.js";
22
- import { toAttachmentSummaries, type HistoryEntry } from "../../group/history.js";
23
- import { detectWasMentioned, hasAnyMention, resolveImplicitMention } from "../../group/mention.js";
24
- import { getRefIndex } from "../../ref/store.js";
25
- import type { InboundGroupInfo, InboundPipelineDeps } from "../inbound-context.js";
26
- import { isMergedTurn, type QueuedMessage } from "../message-queue.js";
27
-
28
- // ─────────────────────────── Types ───────────────────────────
29
-
30
- interface GroupGatePass {
31
- kind: "pass";
32
- groupInfo: InboundGroupInfo;
33
- }
34
-
35
- interface GroupGateSkip {
36
- kind: "skip";
37
- groupInfo: InboundGroupInfo;
38
- skipReason: NonNullable<import("../inbound-context.js").InboundContext["skipReason"]>;
39
- }
40
-
41
- type GroupGateStageResult = GroupGatePass | GroupGateSkip;
42
-
43
- interface GroupGateStageInput {
44
- event: QueuedMessage;
45
- deps: InboundPipelineDeps;
46
- accountId: string;
47
- agentId?: string;
48
- sessionKey: string;
49
- /** User-visible content (post-emoji-parse, post-mention-strip). */
50
- userContent: string;
51
- /** Already-processed attachments (downloaded). Available for history recording. */
52
- processedAttachments?: import("../inbound-attachments.js").ProcessedAttachments;
53
- }
54
-
55
- // ─────────────────────────── Stage ───────────────────────────
56
-
57
- /**
58
- * Run the group-gate stage.
59
- *
60
- * Precondition: `event.type === "group"` && `event.groupOpenid` is set.
61
- * The caller (pipeline) enforces this; the stage doesn't re-check.
62
- *
63
- * On `skip` outcomes the stage records the message into the group's
64
- * history buffer when the skip reason is one that should preserve
65
- * context (drop / skip_no_mention), then returns. `block` skip
66
- * reasons do NOT write history — they are silent rejects.
67
- */
68
- export function runGroupGateStage(input: GroupGateStageInput): GroupGateStageResult {
69
- const { event, deps, accountId, agentId, sessionKey, userContent, processedAttachments } = input;
70
- const groupOpenid = event.groupOpenid!;
71
- const cfg = (deps.cfg ?? {}) as Record<string, unknown>;
72
-
73
- // ---- 1. One-pass config resolution ----
74
- const settings = resolveGroupSettings({ cfg, groupOpenid, accountId, agentId });
75
- const { historyLimit, requireMention, ignoreOtherMentions } = settings.config;
76
- const behaviorPrompt = settings.config.prompt ?? DEFAULT_GROUP_PROMPT;
77
- const groupName = settings.name;
78
-
79
- // ---- 2. Mention detection (QQ-specific) ----
80
- const explicitWasMentioned = detectWasMentioned({
81
- eventType: event.eventType,
82
- mentions: event.mentions as never,
83
- content: event.content,
84
- mentionPatterns: settings.mentionPatterns,
85
- });
86
- const anyMention = hasAnyMention({
87
- mentions: event.mentions as never,
88
- content: event.content,
89
- });
90
- const implicitMention = resolveImplicitMention({
91
- refMsgIdx: event.refMsgIdx,
92
- getRefEntry: (idx) => getRefIndex(idx) ?? null,
93
- });
94
-
95
- // ---- 3. Activation mode (session store > cfg) ----
96
- const activation = resolveGroupActivation({
97
- cfg,
98
- agentId: agentId ?? "default",
99
- sessionKey,
100
- configRequireMention: requireMention,
101
- sessionStoreReader: deps.sessionStoreReader,
102
- });
103
-
104
- // ---- 4. Command authorization (for bypass) ----
105
- const content = (event.content ?? "").trim();
106
- const isControlCommand = Boolean(deps.isControlCommand?.(content));
107
- const commandAuthorized =
108
- deps.allowTextCommands !== false && isSenderAllowedForCommands(event.senderId, deps);
109
-
110
- // ---- 5. Gate evaluation ----
111
- // Layer 1 (ignoreOtherMentions) is QQ-specific and handled by
112
- // resolveGateWithPort. Layers 2+3 delegate to the SDK adapter.
113
- const gate = resolveGateWithPort({
114
- mentionGatePort: deps.adapters.mentionGate,
115
- ignoreOtherMentions,
116
- hasAnyMention: anyMention,
117
- wasMentioned: explicitWasMentioned,
118
- implicitMention,
119
- allowTextCommands: deps.allowTextCommands !== false,
120
- isControlCommand,
121
- commandAuthorized,
122
- requireMention: activation === "mention",
123
- });
124
-
125
- // ---- 6. Build InboundGroupInfo (shared by pass / skip paths) ----
126
- const introHint = deps.resolveGroupIntroHint?.({
127
- cfg,
128
- accountId,
129
- groupId: groupOpenid,
130
- });
131
- const senderLabel = event.senderName ? `${event.senderName} (${event.senderId})` : event.senderId;
132
-
133
- const groupInfo: InboundGroupInfo = {
134
- gate,
135
- activation,
136
- historyLimit,
137
- isMerged: isMergedTurn(event),
138
- mergedMessages: event.merge?.messages,
139
- display: {
140
- groupName,
141
- senderLabel,
142
- introHint,
143
- behaviorPrompt,
144
- },
145
- };
146
-
147
- // ---- 7. Decide pass vs skip ----
148
- if (gate.action === "pass") {
149
- return { kind: "pass", groupInfo };
150
- }
151
-
152
- // Skip path: record history for drop / skip_no_mention, silent for block.
153
- if (gate.action === "drop_other_mention" || gate.action === "skip_no_mention") {
154
- recordGroupHistory({
155
- historyMap: deps.groupHistories,
156
- groupOpenid,
157
- historyLimit,
158
- event,
159
- userContent,
160
- historyPort: deps.adapters.history,
161
- localPaths: processedAttachments?.attachmentLocalPaths,
162
- });
163
- }
164
-
165
- return { kind: "skip", groupInfo, skipReason: gate.action };
166
- }
167
-
168
- // ─────────────────────────── Internal helpers ───────────────────────────
169
-
170
- import type { HistoryPort } from "../../adapter/history.port.js";
171
- import type { MentionGatePort } from "../../adapter/mention-gate.port.js";
172
- import type { GroupMessageGateResult } from "../../group/message-gating.js";
173
-
174
- /**
175
- * Resolve the gate using the SDK MentionGatePort adapter.
176
- *
177
- * Layer 1 (ignoreOtherMentions) is QQ-specific and handled here.
178
- * Layers 2+3 delegate to the SDK's `resolveInboundMentionDecision`.
179
- */
180
- function resolveGateWithPort(params: {
181
- mentionGatePort: MentionGatePort;
182
- ignoreOtherMentions: boolean;
183
- hasAnyMention: boolean;
184
- wasMentioned: boolean;
185
- implicitMention: boolean;
186
- allowTextCommands: boolean;
187
- isControlCommand: boolean;
188
- commandAuthorized: boolean;
189
- requireMention: boolean;
190
- }): GroupMessageGateResult {
191
- // Layer 1: QQ-specific ignoreOtherMentions
192
- if (
193
- params.ignoreOtherMentions &&
194
- params.hasAnyMention &&
195
- !params.wasMentioned &&
196
- !params.implicitMention
197
- ) {
198
- return {
199
- action: "drop_other_mention",
200
- effectiveWasMentioned: false,
201
- shouldBypassMention: false,
202
- };
203
- }
204
-
205
- // Layer 2+3: delegate to SDK mention gate (includes command bypass)
206
- const decision = params.mentionGatePort.resolveInboundMentionDecision({
207
- facts: {
208
- canDetectMention: true,
209
- wasMentioned: params.wasMentioned,
210
- hasAnyMention: params.hasAnyMention,
211
- implicitMentionKinds: params.implicitMention ? ["reply_to_bot"] : [],
212
- },
213
- policy: {
214
- isGroup: true,
215
- requireMention: params.requireMention,
216
- allowTextCommands: params.allowTextCommands,
217
- hasControlCommand: params.isControlCommand,
218
- commandAuthorized: params.commandAuthorized,
219
- },
220
- });
221
-
222
- // Map SDK's shouldBlock (unauthorized command) to our action
223
- if (params.allowTextCommands && params.isControlCommand && !params.commandAuthorized) {
224
- return {
225
- action: "block_unauthorized_command",
226
- effectiveWasMentioned: false,
227
- shouldBypassMention: false,
228
- };
229
- }
230
-
231
- if (decision.shouldSkip) {
232
- return {
233
- action: "skip_no_mention",
234
- effectiveWasMentioned: decision.effectiveWasMentioned,
235
- shouldBypassMention: decision.shouldBypassMention,
236
- };
237
- }
238
-
239
- return {
240
- action: "pass",
241
- effectiveWasMentioned: decision.effectiveWasMentioned,
242
- shouldBypassMention: decision.shouldBypassMention,
243
- };
244
- }
245
-
246
- /**
247
- * Test whether the sender is on the DM `allowFrom` list.
248
- */
249
- function isSenderAllowedForCommands(senderId: string, deps: InboundPipelineDeps): boolean {
250
- const raw = deps.account.config?.allowFrom;
251
- if (!Array.isArray(raw) || raw.length === 0) {
252
- return true;
253
- }
254
- const normalized = normalizeQQBotAllowFrom(raw);
255
- return createQQBotSenderMatcher(senderId)(normalized);
256
- }
257
-
258
- function recordGroupHistory(params: {
259
- historyMap: Map<string, HistoryEntry[]> | undefined;
260
- groupOpenid: string;
261
- historyLimit: number;
262
- event: QueuedMessage;
263
- userContent: string;
264
- historyPort: HistoryPort;
265
- /** Local paths from processAttachments — enriches history with downloaded file paths. */
266
- localPaths?: Array<string | null>;
267
- }): void {
268
- const { historyMap, groupOpenid, historyLimit, event, userContent, historyPort, localPaths } =
269
- params;
270
- if (!historyMap || historyLimit <= 0) {
271
- return;
272
- }
273
-
274
- const senderForHistory = event.senderName
275
- ? `${event.senderName} (${event.senderId})`
276
- : event.senderId;
277
-
278
- const entry: HistoryEntry = {
279
- sender: senderForHistory,
280
- body: userContent,
281
- timestamp: new Date(event.timestamp).getTime(),
282
- messageId: event.messageId,
283
- attachments: toAttachmentSummaries(event.attachments, localPaths),
284
- };
285
-
286
- historyPort.recordPendingHistoryEntry({
287
- historyMap,
288
- historyKey: groupOpenid,
289
- limit: historyLimit,
290
- entry,
291
- });
292
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * Inbound pipeline stages — each stage is a pure(-ish) function that
3
- * transforms a subset of the pipeline's state. The main `inbound-pipeline`
4
- * module composes them in order.
5
- *
6
- * Keeping every stage in its own file makes the pipeline's control flow
7
- * obvious and lets each piece be unit-tested against tiny input fixtures
8
- * without spinning up the full gateway.
9
- */
10
-
11
- export * from "./access-stage.js";
12
- export * from "./assembly-stage.js";
13
- export * from "./content-stage.js";
14
- export * from "./envelope-stage.js";
15
- export * from "./group-gate-stage.js";
16
- export * from "./quote-stage.js";
17
- export * from "./refidx-stage.js";
18
- export { buildSkippedInboundContext } from "./stub-contexts.js";