@gakr-gakr/qqbot 0.1.0

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 (149) hide show
  1. package/api.ts +56 -0
  2. package/autobot.plugin.json +167 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +33 -0
  5. package/package.json +64 -0
  6. package/runtime-api.ts +9 -0
  7. package/secret-contract-api.ts +5 -0
  8. package/setup-entry.ts +13 -0
  9. package/setup-plugin-api.ts +3 -0
  10. package/skills/qqbot-channel/SKILL.md +262 -0
  11. package/skills/qqbot-channel/references/api_references.md +521 -0
  12. package/skills/qqbot-media/SKILL.md +37 -0
  13. package/skills/qqbot-remind/SKILL.md +153 -0
  14. package/src/bridge/approval/capability.ts +225 -0
  15. package/src/bridge/approval/handler-runtime.ts +204 -0
  16. package/src/bridge/bootstrap.ts +135 -0
  17. package/src/bridge/channel-entry.ts +18 -0
  18. package/src/bridge/commands/framework-context-adapter.ts +60 -0
  19. package/src/bridge/commands/framework-registration.ts +66 -0
  20. package/src/bridge/commands/from-parser.ts +60 -0
  21. package/src/bridge/commands/result-dispatcher.ts +76 -0
  22. package/src/bridge/config-shared.ts +132 -0
  23. package/src/bridge/config.ts +176 -0
  24. package/src/bridge/gateway.ts +178 -0
  25. package/src/bridge/logger.ts +31 -0
  26. package/src/bridge/narrowing.ts +31 -0
  27. package/src/bridge/plugin-version.ts +102 -0
  28. package/src/bridge/runtime.ts +25 -0
  29. package/src/bridge/sdk-adapter.ts +164 -0
  30. package/src/bridge/setup/finalize.ts +144 -0
  31. package/src/bridge/setup/surface.ts +34 -0
  32. package/src/bridge/tools/channel.ts +58 -0
  33. package/src/bridge/tools/index.ts +15 -0
  34. package/src/bridge/tools/remind.ts +91 -0
  35. package/src/channel.setup.ts +33 -0
  36. package/src/channel.ts +399 -0
  37. package/src/config-schema.ts +84 -0
  38. package/src/engine/access/index.ts +2 -0
  39. package/src/engine/access/resolve-policy.ts +30 -0
  40. package/src/engine/access/sender-match.ts +55 -0
  41. package/src/engine/access/types.ts +2 -0
  42. package/src/engine/adapter/audio.port.ts +27 -0
  43. package/src/engine/adapter/commands.port.ts +22 -0
  44. package/src/engine/adapter/history.port.ts +52 -0
  45. package/src/engine/adapter/index.ts +76 -0
  46. package/src/engine/adapter/mention-gate.port.ts +50 -0
  47. package/src/engine/adapter/types.ts +38 -0
  48. package/src/engine/api/api-client.ts +212 -0
  49. package/src/engine/api/media-chunked.ts +644 -0
  50. package/src/engine/api/media.ts +218 -0
  51. package/src/engine/api/messages.ts +293 -0
  52. package/src/engine/api/retry.ts +217 -0
  53. package/src/engine/api/routes.ts +95 -0
  54. package/src/engine/api/token.ts +277 -0
  55. package/src/engine/approval/index.ts +224 -0
  56. package/src/engine/commands/builtin/log-helpers.ts +341 -0
  57. package/src/engine/commands/builtin/register-all.ts +17 -0
  58. package/src/engine/commands/builtin/register-approve.ts +201 -0
  59. package/src/engine/commands/builtin/register-basic.ts +95 -0
  60. package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
  61. package/src/engine/commands/builtin/register-logs.ts +20 -0
  62. package/src/engine/commands/builtin/register-streaming.ts +138 -0
  63. package/src/engine/commands/builtin/state.ts +31 -0
  64. package/src/engine/commands/slash-command-auth.ts +88 -0
  65. package/src/engine/commands/slash-command-handler.ts +168 -0
  66. package/src/engine/commands/slash-command-test-support.ts +39 -0
  67. package/src/engine/commands/slash-commands-impl.ts +61 -0
  68. package/src/engine/commands/slash-commands.ts +202 -0
  69. package/src/engine/config/credential-backup.ts +108 -0
  70. package/src/engine/config/credentials.ts +76 -0
  71. package/src/engine/config/group.ts +227 -0
  72. package/src/engine/config/resolve.ts +283 -0
  73. package/src/engine/config/setup-logic.ts +84 -0
  74. package/src/engine/gateway/active-cfg.ts +52 -0
  75. package/src/engine/gateway/codec.ts +47 -0
  76. package/src/engine/gateway/constants.ts +117 -0
  77. package/src/engine/gateway/event-dispatcher.ts +177 -0
  78. package/src/engine/gateway/gateway-connection.ts +356 -0
  79. package/src/engine/gateway/gateway.ts +267 -0
  80. package/src/engine/gateway/inbound-attachments.ts +360 -0
  81. package/src/engine/gateway/inbound-context.ts +82 -0
  82. package/src/engine/gateway/inbound-pipeline.ts +171 -0
  83. package/src/engine/gateway/interaction-handler.ts +345 -0
  84. package/src/engine/gateway/message-queue.ts +404 -0
  85. package/src/engine/gateway/outbound-dispatch.ts +590 -0
  86. package/src/engine/gateway/reconnect.ts +199 -0
  87. package/src/engine/gateway/stages/access-stage.ts +99 -0
  88. package/src/engine/gateway/stages/assembly-stage.ts +156 -0
  89. package/src/engine/gateway/stages/content-stage.ts +77 -0
  90. package/src/engine/gateway/stages/envelope-stage.ts +144 -0
  91. package/src/engine/gateway/stages/group-gate-stage.ts +223 -0
  92. package/src/engine/gateway/stages/index.ts +18 -0
  93. package/src/engine/gateway/stages/quote-stage.ts +113 -0
  94. package/src/engine/gateway/stages/refidx-stage.ts +62 -0
  95. package/src/engine/gateway/stages/stub-contexts.ts +77 -0
  96. package/src/engine/gateway/types.ts +230 -0
  97. package/src/engine/gateway/typing-keepalive.ts +102 -0
  98. package/src/engine/gateway/ws-client.ts +16 -0
  99. package/src/engine/group/activation.ts +88 -0
  100. package/src/engine/group/history.ts +321 -0
  101. package/src/engine/group/mention.ts +114 -0
  102. package/src/engine/group/message-gating.ts +108 -0
  103. package/src/engine/messaging/decode-media-path.ts +82 -0
  104. package/src/engine/messaging/media-source.ts +210 -0
  105. package/src/engine/messaging/media-type-detect.ts +27 -0
  106. package/src/engine/messaging/outbound-audio-port.ts +38 -0
  107. package/src/engine/messaging/outbound-deliver.ts +810 -0
  108. package/src/engine/messaging/outbound-media-send.ts +658 -0
  109. package/src/engine/messaging/outbound-reply.ts +27 -0
  110. package/src/engine/messaging/outbound-result-helpers.ts +54 -0
  111. package/src/engine/messaging/outbound-types.ts +47 -0
  112. package/src/engine/messaging/outbound.ts +485 -0
  113. package/src/engine/messaging/reply-dispatcher.ts +597 -0
  114. package/src/engine/messaging/reply-limiter.ts +164 -0
  115. package/src/engine/messaging/sender.ts +741 -0
  116. package/src/engine/messaging/streaming-c2c.ts +1192 -0
  117. package/src/engine/messaging/streaming-media-send.ts +544 -0
  118. package/src/engine/messaging/target-parser.ts +104 -0
  119. package/src/engine/ref/format-message-ref.ts +142 -0
  120. package/src/engine/ref/format-ref-entry.ts +27 -0
  121. package/src/engine/ref/store.ts +211 -0
  122. package/src/engine/ref/types.ts +27 -0
  123. package/src/engine/session/known-users.ts +138 -0
  124. package/src/engine/session/session-store.ts +207 -0
  125. package/src/engine/tools/channel-api.ts +244 -0
  126. package/src/engine/tools/remind-logic.ts +377 -0
  127. package/src/engine/types.ts +313 -0
  128. package/src/engine/utils/attachment-tags.ts +174 -0
  129. package/src/engine/utils/audio.ts +525 -0
  130. package/src/engine/utils/data-paths.ts +38 -0
  131. package/src/engine/utils/diagnostics.ts +93 -0
  132. package/src/engine/utils/file-utils.ts +215 -0
  133. package/src/engine/utils/format.ts +70 -0
  134. package/src/engine/utils/image-size.ts +249 -0
  135. package/src/engine/utils/log.ts +77 -0
  136. package/src/engine/utils/media-tags.ts +177 -0
  137. package/src/engine/utils/payload.ts +157 -0
  138. package/src/engine/utils/platform.ts +265 -0
  139. package/src/engine/utils/request-context.ts +60 -0
  140. package/src/engine/utils/string-normalize.ts +91 -0
  141. package/src/engine/utils/stt.ts +103 -0
  142. package/src/engine/utils/text-parsing.ts +155 -0
  143. package/src/engine/utils/upload-cache.ts +96 -0
  144. package/src/engine/utils/voice-text.ts +15 -0
  145. package/src/exec-approvals.ts +237 -0
  146. package/src/qqbot-test-support.ts +29 -0
  147. package/src/secret-contract.ts +82 -0
  148. package/src/types.ts +210 -0
  149. package/tsconfig.json +16 -0
@@ -0,0 +1,223 @@
1
+ import type { HistoryPort } from "../../adapter/history.port.js";
2
+ import type { QQBotInboundAccess } from "../../adapter/index.js";
3
+ import type { MentionGatePort } from "../../adapter/mention-gate.port.js";
4
+ import { DEFAULT_GROUP_PROMPT, resolveGroupSettings } from "../../config/group.js";
5
+ import { resolveGroupActivation } from "../../group/activation.js";
6
+ import { toAttachmentSummaries, type HistoryEntry } from "../../group/history.js";
7
+ import { detectWasMentioned, hasAnyMention, resolveImplicitMention } from "../../group/mention.js";
8
+ import type { GroupMessageGateResult } from "../../group/message-gating.js";
9
+ import { getRefIndex } from "../../ref/store.js";
10
+ import type { InboundContext, InboundGroupInfo, InboundPipelineDeps } from "../inbound-context.js";
11
+ import { isMergedTurn, type QueuedMessage } from "../message-queue.js";
12
+
13
+ interface GroupGatePass {
14
+ kind: "pass";
15
+ groupInfo: InboundGroupInfo;
16
+ }
17
+
18
+ interface GroupGateSkip {
19
+ kind: "skip";
20
+ groupInfo: InboundGroupInfo;
21
+ skipReason: NonNullable<InboundContext["skipReason"]>;
22
+ }
23
+
24
+ type GroupGateStageResult = GroupGatePass | GroupGateSkip;
25
+
26
+ interface GroupGateStageInput {
27
+ event: QueuedMessage;
28
+ deps: InboundPipelineDeps;
29
+ accountId: string;
30
+ agentId?: string;
31
+ sessionKey: string;
32
+ userContent: string;
33
+ processedAttachments?: import("../inbound-attachments.js").ProcessedAttachments;
34
+ access: QQBotInboundAccess;
35
+ }
36
+
37
+ export function runGroupGateStage(input: GroupGateStageInput): GroupGateStageResult {
38
+ const { event, deps, accountId, agentId, sessionKey, userContent, processedAttachments } = input;
39
+ const groupOpenid = event.groupOpenid!;
40
+ const cfg = (deps.cfg ?? {}) as Record<string, unknown>;
41
+
42
+ const settings = resolveGroupSettings({ cfg, groupOpenid, accountId, agentId });
43
+ const { historyLimit, requireMention, ignoreOtherMentions } = settings.config;
44
+ const behaviorPrompt = settings.config.prompt ?? DEFAULT_GROUP_PROMPT;
45
+ const groupName = settings.name;
46
+
47
+ const explicitWasMentioned = detectWasMentioned({
48
+ eventType: event.eventType,
49
+ mentions: event.mentions as never,
50
+ content: event.content,
51
+ mentionPatterns: settings.mentionPatterns,
52
+ });
53
+ const anyMention = hasAnyMention({
54
+ mentions: event.mentions as never,
55
+ content: event.content,
56
+ });
57
+ const implicitMention = resolveImplicitMention({
58
+ refMsgIdx: event.refMsgIdx,
59
+ getRefEntry: (idx) => getRefIndex(idx) ?? null,
60
+ });
61
+
62
+ const activation = resolveGroupActivation({
63
+ cfg,
64
+ agentId: agentId ?? "default",
65
+ sessionKey,
66
+ configRequireMention: requireMention,
67
+ sessionStoreReader: deps.sessionStoreReader,
68
+ });
69
+
70
+ const content = (event.content ?? "").trim();
71
+ const isControlCommand = Boolean(deps.isControlCommand?.(content));
72
+ const commandAuthorized =
73
+ deps.allowTextCommands !== false && input.access.commandAccess.authorized;
74
+
75
+ const gate = resolveGateWithPort({
76
+ mentionGatePort: deps.adapters.mentionGate,
77
+ ignoreOtherMentions,
78
+ hasAnyMention: anyMention,
79
+ wasMentioned: explicitWasMentioned,
80
+ implicitMention,
81
+ allowTextCommands: deps.allowTextCommands !== false,
82
+ isControlCommand,
83
+ commandAuthorized,
84
+ requireMention: activation === "mention",
85
+ });
86
+
87
+ const introHint = deps.resolveGroupIntroHint?.({
88
+ cfg,
89
+ accountId,
90
+ groupId: groupOpenid,
91
+ });
92
+ const senderLabel = event.senderName ? `${event.senderName} (${event.senderId})` : event.senderId;
93
+
94
+ const groupInfo: InboundGroupInfo = {
95
+ gate,
96
+ activation,
97
+ historyLimit,
98
+ isMerged: isMergedTurn(event),
99
+ mergedMessages: event.merge?.messages,
100
+ display: {
101
+ groupName,
102
+ senderLabel,
103
+ introHint,
104
+ behaviorPrompt,
105
+ },
106
+ };
107
+
108
+ if (gate.action === "pass") {
109
+ return { kind: "pass", groupInfo };
110
+ }
111
+
112
+ if (gate.action === "drop_other_mention" || gate.action === "skip_no_mention") {
113
+ recordGroupHistory({
114
+ historyMap: deps.groupHistories,
115
+ groupOpenid,
116
+ historyLimit,
117
+ event,
118
+ userContent,
119
+ historyPort: deps.adapters.history,
120
+ localPaths: processedAttachments?.attachmentLocalPaths,
121
+ });
122
+ }
123
+
124
+ return { kind: "skip", groupInfo, skipReason: gate.action };
125
+ }
126
+
127
+ function resolveGateWithPort(params: {
128
+ mentionGatePort: MentionGatePort;
129
+ ignoreOtherMentions: boolean;
130
+ hasAnyMention: boolean;
131
+ wasMentioned: boolean;
132
+ implicitMention: boolean;
133
+ allowTextCommands: boolean;
134
+ isControlCommand: boolean;
135
+ commandAuthorized: boolean;
136
+ requireMention: boolean;
137
+ }): GroupMessageGateResult {
138
+ if (
139
+ params.ignoreOtherMentions &&
140
+ params.hasAnyMention &&
141
+ !params.wasMentioned &&
142
+ !params.implicitMention
143
+ ) {
144
+ return {
145
+ action: "drop_other_mention",
146
+ effectiveWasMentioned: false,
147
+ shouldBypassMention: false,
148
+ };
149
+ }
150
+
151
+ const decision = params.mentionGatePort.resolveInboundMentionDecision({
152
+ facts: {
153
+ canDetectMention: true,
154
+ wasMentioned: params.wasMentioned,
155
+ hasAnyMention: params.hasAnyMention,
156
+ implicitMentionKinds: params.implicitMention ? ["reply_to_bot"] : [],
157
+ },
158
+ policy: {
159
+ isGroup: true,
160
+ requireMention: params.requireMention,
161
+ allowTextCommands: params.allowTextCommands,
162
+ hasControlCommand: params.isControlCommand,
163
+ commandAuthorized: params.commandAuthorized,
164
+ },
165
+ });
166
+
167
+ if (params.allowTextCommands && params.isControlCommand && !params.commandAuthorized) {
168
+ return {
169
+ action: "block_unauthorized_command",
170
+ effectiveWasMentioned: false,
171
+ shouldBypassMention: false,
172
+ };
173
+ }
174
+
175
+ if (decision.shouldSkip) {
176
+ return {
177
+ action: "skip_no_mention",
178
+ effectiveWasMentioned: decision.effectiveWasMentioned,
179
+ shouldBypassMention: decision.shouldBypassMention,
180
+ };
181
+ }
182
+
183
+ return {
184
+ action: "pass",
185
+ effectiveWasMentioned: decision.effectiveWasMentioned,
186
+ shouldBypassMention: decision.shouldBypassMention,
187
+ };
188
+ }
189
+
190
+ function recordGroupHistory(params: {
191
+ historyMap: Map<string, HistoryEntry[]> | undefined;
192
+ groupOpenid: string;
193
+ historyLimit: number;
194
+ event: QueuedMessage;
195
+ userContent: string;
196
+ historyPort: HistoryPort;
197
+ localPaths?: Array<string | null>;
198
+ }): void {
199
+ const { historyMap, groupOpenid, historyLimit, event, userContent, historyPort, localPaths } =
200
+ params;
201
+ if (!historyMap || historyLimit <= 0) {
202
+ return;
203
+ }
204
+
205
+ const senderForHistory = event.senderName
206
+ ? `${event.senderName} (${event.senderId})`
207
+ : event.senderId;
208
+
209
+ const entry: HistoryEntry = {
210
+ sender: senderForHistory,
211
+ body: userContent,
212
+ timestamp: new Date(event.timestamp).getTime(),
213
+ messageId: event.messageId,
214
+ attachments: toAttachmentSummaries(event.attachments, localPaths),
215
+ };
216
+
217
+ historyPort.recordPendingHistoryEntry({
218
+ historyMap,
219
+ historyKey: groupOpenid,
220
+ limit: historyLimit,
221
+ entry,
222
+ });
223
+ }
@@ -0,0 +1,18 @@
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";
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Quote stage — resolve the quoted-reply (`refMsgIdx`) if any.
3
+ *
4
+ * Three-level fallback mirrors the standalone build:
5
+ * 1. RefIndex cache hit → rich ReplyToInfo
6
+ * 2. `msg_elements[0]` present → re-process the quoted body
7
+ * 3. Otherwise → id-only placeholder so the pipeline still knows it's a reply
8
+ */
9
+
10
+ import {
11
+ formatMessageReferenceForAgent,
12
+ type AttachmentProcessor,
13
+ } from "../../ref/format-message-ref.js";
14
+ import { formatRefEntryForAgent, getRefIndex } from "../../ref/store.js";
15
+ import { MSG_TYPE_QUOTE } from "../../utils/text-parsing.js";
16
+ import { formatVoiceText } from "../../utils/voice-text.js";
17
+ import { processAttachments } from "../inbound-attachments.js";
18
+ import type { InboundPipelineDeps, ReplyToInfo } from "../inbound-context.js";
19
+ import type { QueuedMessage } from "../message-queue.js";
20
+
21
+ /**
22
+ * Resolve the quote metadata for an inbound event.
23
+ *
24
+ * Returns `undefined` when the event is not a reply at all.
25
+ */
26
+ export async function resolveQuote(
27
+ event: QueuedMessage,
28
+ deps: InboundPipelineDeps,
29
+ ): Promise<ReplyToInfo | undefined> {
30
+ if (!event.refMsgIdx) {
31
+ return undefined;
32
+ }
33
+
34
+ const { account, log } = deps;
35
+
36
+ // ---- Layer 1: cache hit ----
37
+ const refEntry = getRefIndex(event.refMsgIdx);
38
+ if (refEntry) {
39
+ log?.debug?.(
40
+ `Quote detected via refMsgIdx cache: refMsgIdx=${event.refMsgIdx}, sender=${refEntry.senderName ?? refEntry.senderId}`,
41
+ );
42
+ return {
43
+ id: event.refMsgIdx,
44
+ body: formatRefEntryForAgent(refEntry),
45
+ sender: refEntry.senderName ?? refEntry.senderId,
46
+ isQuote: true,
47
+ };
48
+ }
49
+
50
+ // ---- Layer 2: fall back to msg_elements[0] if this is a quote type ----
51
+ if (event.msgType === MSG_TYPE_QUOTE && event.msgElements?.[0]) {
52
+ try {
53
+ const refElement = event.msgElements[0];
54
+ const refData = {
55
+ content: refElement.content ?? "",
56
+ attachments: refElement.attachments,
57
+ };
58
+ const attachmentProcessor: AttachmentProcessor = {
59
+ processAttachments: async (atts, refCtx) => {
60
+ const result = await processAttachments(
61
+ atts as Array<{
62
+ content_type: string;
63
+ url: string;
64
+ filename?: string;
65
+ voice_wav_url?: string;
66
+ asr_refer_text?: string;
67
+ }>,
68
+ {
69
+ accountId: account.accountId,
70
+ cfg: refCtx.cfg,
71
+ audioConvert: deps.adapters.audioConvert,
72
+ log: refCtx.log,
73
+ },
74
+ );
75
+ return {
76
+ attachmentInfo: result.attachmentInfo,
77
+ voiceTranscripts: result.voiceTranscripts,
78
+ voiceTranscriptSources: result.voiceTranscriptSources,
79
+ attachmentLocalPaths: result.attachmentLocalPaths,
80
+ };
81
+ },
82
+ formatVoiceText: (transcripts) => formatVoiceText(transcripts),
83
+ };
84
+ const refPeerId =
85
+ event.type === "group" && event.groupOpenid ? event.groupOpenid : event.senderId;
86
+ const refBody = await formatMessageReferenceForAgent(
87
+ refData,
88
+ { appId: account.appId, peerId: refPeerId, cfg: account.config, log },
89
+ attachmentProcessor,
90
+ );
91
+ log?.debug?.(
92
+ `Quote detected via msg_elements[0] (cache miss): id=${event.refMsgIdx}, content="${(refBody ?? "").slice(0, 80)}..."`,
93
+ );
94
+ return {
95
+ id: event.refMsgIdx,
96
+ body: refBody || undefined,
97
+ isQuote: true,
98
+ };
99
+ } catch (refErr) {
100
+ log?.error(`Failed to format quoted message from msg_elements: ${String(refErr)}`);
101
+ }
102
+ } else {
103
+ log?.debug?.(
104
+ `Quote detected but no cache and msgType=${event.msgType}: refMsgIdx=${event.refMsgIdx}`,
105
+ );
106
+ }
107
+
108
+ // ---- Layer 3: id-only placeholder ----
109
+ return {
110
+ id: event.refMsgIdx,
111
+ isQuote: true,
112
+ };
113
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * RefIdx persistence stage — writes the current message into the shared
3
+ * `refIndex` cache so future quote resolutions can find it.
4
+ *
5
+ * The stage also attaches voice transcripts (and their source) onto the
6
+ * cached attachment summaries so replies-to-this-message can render the
7
+ * original audio content inline instead of just a file handle.
8
+ *
9
+ * Pure data pipeline (no network I/O). Sync return value.
10
+ */
11
+
12
+ import { setRefIndex } from "../../ref/store.js";
13
+ import { buildAttachmentSummaries } from "../../utils/text-parsing.js";
14
+ import type { ProcessedAttachments } from "../inbound-attachments.js";
15
+ import type { QueuedMessage } from "../message-queue.js";
16
+
17
+ /**
18
+ * Cache the current message under `msgIdx` (or the fallback `refIdx`
19
+ * returned by the typing-indicator call) so later quotes resolve.
20
+ *
21
+ * No-op when neither id is available.
22
+ */
23
+ export function writeRefIndex(params: {
24
+ event: QueuedMessage;
25
+ parsedContent: string;
26
+ processed: ProcessedAttachments;
27
+ /** Optional refIdx returned by `InputNotify` — used when `msgIdx` is missing. */
28
+ inputNotifyRefIdx?: string;
29
+ }): void {
30
+ const { event, parsedContent, processed, inputNotifyRefIdx } = params;
31
+
32
+ const currentMsgIdx = event.msgIdx ?? inputNotifyRefIdx;
33
+ if (!currentMsgIdx) {
34
+ return;
35
+ }
36
+
37
+ const attSummaries = buildAttachmentSummaries(event.attachments, processed.attachmentLocalPaths);
38
+ if (attSummaries && processed.voiceTranscripts.length > 0) {
39
+ let voiceIdx = 0;
40
+ for (const att of attSummaries) {
41
+ if (att.type === "voice" && voiceIdx < processed.voiceTranscripts.length) {
42
+ att.transcript = processed.voiceTranscripts[voiceIdx];
43
+ if (voiceIdx < processed.voiceTranscriptSources.length) {
44
+ att.transcriptSource = processed.voiceTranscriptSources[voiceIdx] as
45
+ | "stt"
46
+ | "asr"
47
+ | "tts"
48
+ | "fallback";
49
+ }
50
+ voiceIdx++;
51
+ }
52
+ }
53
+ }
54
+
55
+ setRefIndex(currentMsgIdx, {
56
+ content: parsedContent,
57
+ senderId: event.senderId,
58
+ senderName: event.senderName,
59
+ timestamp: new Date(event.timestamp).getTime(),
60
+ attachments: attSummaries,
61
+ });
62
+ }
@@ -0,0 +1,77 @@
1
+ import type { QQBotInboundAccess } from "../../adapter/index.js";
2
+ import type { InboundContext, InboundGroupInfo } from "../inbound-context.js";
3
+ import type { QueuedMessage } from "../message-queue.js";
4
+ import type { TypingKeepAlive } from "../typing-keepalive.js";
5
+
6
+ interface BaseStubFields {
7
+ event: QueuedMessage;
8
+ route: { sessionKey: string; accountId: string; agentId?: string };
9
+ isGroupChat: boolean;
10
+ peerId: string;
11
+ qualifiedTarget: string;
12
+ fromAddress: string;
13
+ }
14
+
15
+ function emptyInboundContext(fields: BaseStubFields): InboundContext {
16
+ return {
17
+ event: fields.event,
18
+ route: fields.route,
19
+ isGroupChat: fields.isGroupChat,
20
+ peerId: fields.peerId,
21
+ qualifiedTarget: fields.qualifiedTarget,
22
+ fromAddress: fields.fromAddress,
23
+ agentBody: "",
24
+ body: "",
25
+ groupSystemPrompt: undefined,
26
+ localMediaPaths: [],
27
+ localMediaTypes: [],
28
+ remoteMediaUrls: [],
29
+ uniqueVoicePaths: [],
30
+ uniqueVoiceUrls: [],
31
+ uniqueVoiceAsrReferTexts: [],
32
+ voiceMediaTypes: [],
33
+ hasAsrReferFallback: false,
34
+ voiceTranscriptSources: [],
35
+ replyTo: undefined,
36
+ commandAuthorized: false,
37
+ group: undefined,
38
+ blocked: false,
39
+ skipped: false,
40
+ typing: { keepAlive: null },
41
+ inputNotifyRefIdx: undefined,
42
+ };
43
+ }
44
+
45
+ export function buildBlockedInboundContext(
46
+ params: BaseStubFields & {
47
+ access: QQBotInboundAccess;
48
+ },
49
+ ): InboundContext {
50
+ return {
51
+ ...emptyInboundContext(params),
52
+ blocked: true,
53
+ blockReason: params.access.senderAccess.reasonCode,
54
+ blockReasonCode: params.access.senderAccess.reasonCode,
55
+ accessDecision: params.access.senderAccess.decision,
56
+ };
57
+ }
58
+
59
+ export function buildSkippedInboundContext(
60
+ params: BaseStubFields & {
61
+ group: InboundGroupInfo;
62
+ skipReason: NonNullable<InboundContext["skipReason"]>;
63
+ access: QQBotInboundAccess;
64
+ typing: { keepAlive: TypingKeepAlive | null };
65
+ inputNotifyRefIdx?: string;
66
+ },
67
+ ): InboundContext {
68
+ return {
69
+ ...emptyInboundContext(params),
70
+ group: params.group,
71
+ skipped: true,
72
+ skipReason: params.skipReason,
73
+ accessDecision: params.access.senderAccess.decision,
74
+ typing: params.typing,
75
+ inputNotifyRefIdx: params.inputNotifyRefIdx,
76
+ };
77
+ }