@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.
- package/dist/api.js +646 -0
- package/dist/approval-cg0SVahb.js +94 -0
- package/dist/channel-N6Y_Rcjp.js +551 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.setup-ByCRAe5H.js +25 -0
- package/dist/config-D6545NkC.js +94 -0
- package/dist/config-schema-DFcjQw73.js +309 -0
- package/dist/exec-approvals-COUsM6wZ.js +138 -0
- package/dist/gateway-CmSUJKSt.js +6726 -0
- package/dist/handler-runtime-Bqm6N0WG.js +120 -0
- package/dist/index.js +29 -0
- package/dist/narrowing-BoieBTIU.js +25 -0
- package/dist/outbound-BJfhwrPg.js +1357 -0
- package/dist/request-context-DXtpwWui.js +921 -0
- package/dist/resolve-D_06fV6-.js +284 -0
- package/dist/runtime-BJAS3eXW.js +18 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-api.js +56 -0
- package/dist/sender-p-B14eLG.js +1987 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/string-normalize-Ci6NM5DE.js +98 -0
- package/dist/target-parser-Y0prnrXD.js +245 -0
- package/package.json +15 -6
- package/api.ts +0 -56
- package/channel-plugin-api.ts +0 -1
- package/index.ts +0 -29
- package/runtime-api.ts +0 -9
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -3
- package/src/bridge/approval/capability.ts +0 -237
- package/src/bridge/approval/handler-runtime.ts +0 -204
- package/src/bridge/bootstrap.ts +0 -135
- package/src/bridge/channel-entry.ts +0 -18
- package/src/bridge/commands/framework-context-adapter.ts +0 -60
- package/src/bridge/commands/framework-registration.ts +0 -47
- package/src/bridge/commands/from-parser.test.ts +0 -86
- package/src/bridge/commands/from-parser.ts +0 -60
- package/src/bridge/commands/result-dispatcher.ts +0 -76
- package/src/bridge/config-shared.ts +0 -132
- package/src/bridge/config.ts +0 -111
- package/src/bridge/gateway.ts +0 -174
- package/src/bridge/logger.ts +0 -31
- package/src/bridge/narrowing.ts +0 -31
- package/src/bridge/plugin-version.test.ts +0 -146
- package/src/bridge/plugin-version.ts +0 -102
- package/src/bridge/runtime.ts +0 -25
- package/src/bridge/sdk-adapter.ts +0 -131
- package/src/bridge/setup/finalize.ts +0 -144
- package/src/bridge/setup/surface.ts +0 -34
- package/src/bridge/tools/channel.ts +0 -58
- package/src/bridge/tools/index.ts +0 -15
- package/src/bridge/tools/remind.test.ts +0 -124
- package/src/bridge/tools/remind.ts +0 -91
- package/src/channel.setup.ts +0 -33
- package/src/channel.ts +0 -288
- package/src/command-auth.test.ts +0 -62
- package/src/config-schema.ts +0 -84
- package/src/config.test.ts +0 -364
- package/src/engine/access/access-control.test.ts +0 -198
- package/src/engine/access/access-control.ts +0 -226
- package/src/engine/access/index.ts +0 -16
- package/src/engine/access/resolve-policy.test.ts +0 -59
- package/src/engine/access/resolve-policy.ts +0 -57
- package/src/engine/access/sender-match.test.ts +0 -60
- package/src/engine/access/sender-match.ts +0 -55
- package/src/engine/access/types.ts +0 -53
- package/src/engine/adapter/audio.port.ts +0 -27
- package/src/engine/adapter/commands.port.ts +0 -22
- package/src/engine/adapter/history.port.ts +0 -52
- package/src/engine/adapter/index.ts +0 -139
- package/src/engine/adapter/mention-gate.port.ts +0 -50
- package/src/engine/adapter/types.ts +0 -38
- package/src/engine/api/api-client.ts +0 -212
- package/src/engine/api/media-chunked.test.ts +0 -336
- package/src/engine/api/media-chunked.ts +0 -622
- package/src/engine/api/media.ts +0 -218
- package/src/engine/api/messages.ts +0 -293
- package/src/engine/api/retry.ts +0 -217
- package/src/engine/api/routes.ts +0 -95
- package/src/engine/api/token.ts +0 -271
- package/src/engine/approval/index.test.ts +0 -22
- package/src/engine/approval/index.ts +0 -224
- package/src/engine/commands/builtin/log-helpers.ts +0 -319
- package/src/engine/commands/builtin/register-all.ts +0 -17
- package/src/engine/commands/builtin/register-approve.ts +0 -201
- package/src/engine/commands/builtin/register-basic.ts +0 -95
- package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
- package/src/engine/commands/builtin/register-logs.ts +0 -20
- package/src/engine/commands/builtin/register-streaming.ts +0 -137
- package/src/engine/commands/builtin/state.ts +0 -31
- package/src/engine/commands/slash-command-auth.ts +0 -48
- package/src/engine/commands/slash-command-handler.ts +0 -146
- package/src/engine/commands/slash-commands-impl.test.ts +0 -8
- package/src/engine/commands/slash-commands-impl.ts +0 -61
- package/src/engine/commands/slash-commands.ts +0 -199
- package/src/engine/config/credential-backup.test.ts +0 -88
- package/src/engine/config/credential-backup.ts +0 -107
- package/src/engine/config/credentials.ts +0 -76
- package/src/engine/config/group.test.ts +0 -234
- package/src/engine/config/group.ts +0 -299
- package/src/engine/config/resolve.test.ts +0 -152
- package/src/engine/config/resolve.ts +0 -283
- package/src/engine/config/setup-logic.ts +0 -84
- package/src/engine/engine-import-boundary.test.ts +0 -73
- package/src/engine/gateway/codec.ts +0 -47
- package/src/engine/gateway/constants.ts +0 -117
- package/src/engine/gateway/event-dispatcher.ts +0 -177
- package/src/engine/gateway/gateway-connection.ts +0 -371
- package/src/engine/gateway/gateway.ts +0 -291
- package/src/engine/gateway/inbound-attachments.test.ts +0 -126
- package/src/engine/gateway/inbound-attachments.ts +0 -360
- package/src/engine/gateway/inbound-context.ts +0 -195
- package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
- package/src/engine/gateway/inbound-pipeline.ts +0 -235
- package/src/engine/gateway/interaction-handler.ts +0 -220
- package/src/engine/gateway/message-queue.test.ts +0 -282
- package/src/engine/gateway/message-queue.ts +0 -499
- package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
- package/src/engine/gateway/outbound-dispatch.ts +0 -575
- package/src/engine/gateway/reconnect.ts +0 -199
- package/src/engine/gateway/stages/access-stage.ts +0 -132
- package/src/engine/gateway/stages/assembly-stage.ts +0 -156
- package/src/engine/gateway/stages/content-stage.test.ts +0 -77
- package/src/engine/gateway/stages/content-stage.ts +0 -77
- package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
- package/src/engine/gateway/stages/envelope-stage.ts +0 -144
- package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
- package/src/engine/gateway/stages/index.ts +0 -18
- package/src/engine/gateway/stages/quote-stage.ts +0 -113
- package/src/engine/gateway/stages/refidx-stage.ts +0 -62
- package/src/engine/gateway/stages/stub-contexts.ts +0 -116
- package/src/engine/gateway/types.ts +0 -264
- package/src/engine/gateway/typing-keepalive.ts +0 -79
- package/src/engine/group/activation.test.ts +0 -114
- package/src/engine/group/activation.ts +0 -147
- package/src/engine/group/history.test.ts +0 -314
- package/src/engine/group/history.ts +0 -321
- package/src/engine/group/mention.test.ts +0 -141
- package/src/engine/group/mention.ts +0 -197
- package/src/engine/group/message-gating.test.ts +0 -188
- package/src/engine/group/message-gating.ts +0 -216
- package/src/engine/messaging/decode-media-path.ts +0 -82
- package/src/engine/messaging/media-source.ts +0 -215
- package/src/engine/messaging/media-type-detect.ts +0 -37
- package/src/engine/messaging/outbound-audio-port.ts +0 -38
- package/src/engine/messaging/outbound-deliver.ts +0 -810
- package/src/engine/messaging/outbound-media-send.ts +0 -702
- package/src/engine/messaging/outbound-reply.ts +0 -27
- package/src/engine/messaging/outbound-result-helpers.ts +0 -54
- package/src/engine/messaging/outbound-types.ts +0 -45
- package/src/engine/messaging/outbound.ts +0 -485
- package/src/engine/messaging/reply-dispatcher.ts +0 -597
- package/src/engine/messaging/reply-limiter.ts +0 -164
- package/src/engine/messaging/sender.ts +0 -729
- package/src/engine/messaging/streaming-c2c.ts +0 -1192
- package/src/engine/messaging/streaming-media-send.ts +0 -544
- package/src/engine/messaging/target-parser.ts +0 -104
- package/src/engine/ref/format-message-ref.ts +0 -142
- package/src/engine/ref/format-ref-entry.test.ts +0 -60
- package/src/engine/ref/format-ref-entry.ts +0 -27
- package/src/engine/ref/store.ts +0 -208
- package/src/engine/ref/types.ts +0 -27
- package/src/engine/session/known-users.ts +0 -137
- package/src/engine/session/session-store.ts +0 -204
- package/src/engine/tools/channel-api.ts +0 -244
- package/src/engine/tools/remind-logic.test.ts +0 -280
- package/src/engine/tools/remind-logic.ts +0 -377
- package/src/engine/types.ts +0 -313
- package/src/engine/utils/attachment-tags.test.ts +0 -186
- package/src/engine/utils/attachment-tags.ts +0 -174
- package/src/engine/utils/audio.test.ts +0 -250
- package/src/engine/utils/audio.ts +0 -585
- package/src/engine/utils/data-paths.ts +0 -38
- package/src/engine/utils/diagnostics.ts +0 -109
- package/src/engine/utils/file-utils.test.ts +0 -72
- package/src/engine/utils/file-utils.ts +0 -225
- package/src/engine/utils/format.test.ts +0 -68
- package/src/engine/utils/format.ts +0 -70
- package/src/engine/utils/image-size.test.ts +0 -158
- package/src/engine/utils/image-size.ts +0 -249
- package/src/engine/utils/log.test.ts +0 -28
- package/src/engine/utils/log.ts +0 -61
- package/src/engine/utils/media-tags.test.ts +0 -32
- package/src/engine/utils/media-tags.ts +0 -177
- package/src/engine/utils/payload.test.ts +0 -68
- package/src/engine/utils/payload.ts +0 -145
- package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
- package/src/engine/utils/platform.test.ts +0 -148
- package/src/engine/utils/platform.ts +0 -343
- package/src/engine/utils/request-context.ts +0 -60
- package/src/engine/utils/string-normalize.ts +0 -91
- package/src/engine/utils/stt.test.ts +0 -104
- package/src/engine/utils/stt.ts +0 -100
- package/src/engine/utils/text-parsing.test.ts +0 -29
- package/src/engine/utils/text-parsing.ts +0 -155
- package/src/engine/utils/upload-cache.ts +0 -96
- package/src/engine/utils/voice-text.ts +0 -15
- package/src/exec-approvals.ts +0 -218
- package/src/manifest-schema.test.ts +0 -56
- package/src/qqbot-test-support.ts +0 -29
- package/src/types.ts +0 -210
- 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";
|