@openclaw/qqbot 2026.5.1-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/api.ts +56 -0
- package/channel-plugin-api.ts +1 -0
- package/dist/.boundary-tsc.stamp +1 -0
- package/dist/.boundary-tsc.tsbuildinfo +1 -0
- package/index.ts +29 -0
- package/openclaw.plugin.json +814 -0
- package/package.json +66 -0
- package/runtime-api.ts +9 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/skills/qqbot-channel/SKILL.md +262 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +37 -0
- package/skills/qqbot-remind/SKILL.md +153 -0
- package/src/bridge/approval/capability.ts +237 -0
- package/src/bridge/approval/handler-runtime.ts +204 -0
- package/src/bridge/bootstrap.ts +135 -0
- package/src/bridge/channel-entry.ts +18 -0
- package/src/bridge/commands/framework-context-adapter.ts +60 -0
- package/src/bridge/commands/framework-registration.ts +47 -0
- package/src/bridge/commands/from-parser.test.ts +86 -0
- package/src/bridge/commands/from-parser.ts +60 -0
- package/src/bridge/commands/result-dispatcher.ts +76 -0
- package/src/bridge/config-shared.ts +132 -0
- package/src/bridge/config.ts +111 -0
- package/src/bridge/gateway.ts +174 -0
- package/src/bridge/logger.ts +31 -0
- package/src/bridge/narrowing.ts +31 -0
- package/src/bridge/plugin-version.test.ts +146 -0
- package/src/bridge/plugin-version.ts +102 -0
- package/src/bridge/runtime.ts +25 -0
- package/src/bridge/sdk-adapter.ts +131 -0
- package/src/bridge/setup/finalize.ts +144 -0
- package/src/bridge/setup/surface.ts +34 -0
- package/src/bridge/tools/channel.ts +58 -0
- package/src/bridge/tools/index.ts +15 -0
- package/src/bridge/tools/remind.test.ts +124 -0
- package/src/bridge/tools/remind.ts +91 -0
- package/src/channel.setup.ts +33 -0
- package/src/channel.ts +288 -0
- package/src/command-auth.test.ts +62 -0
- package/src/config-schema.ts +84 -0
- package/src/config.test.ts +364 -0
- package/src/engine/access/access-control.test.ts +198 -0
- package/src/engine/access/access-control.ts +226 -0
- package/src/engine/access/index.ts +16 -0
- package/src/engine/access/resolve-policy.test.ts +59 -0
- package/src/engine/access/resolve-policy.ts +57 -0
- package/src/engine/access/sender-match.test.ts +60 -0
- package/src/engine/access/sender-match.ts +55 -0
- package/src/engine/access/types.ts +53 -0
- package/src/engine/adapter/audio.port.ts +27 -0
- package/src/engine/adapter/commands.port.ts +22 -0
- package/src/engine/adapter/history.port.ts +52 -0
- package/src/engine/adapter/index.ts +139 -0
- package/src/engine/adapter/mention-gate.port.ts +50 -0
- package/src/engine/adapter/types.ts +38 -0
- package/src/engine/api/api-client.ts +212 -0
- package/src/engine/api/media-chunked.test.ts +336 -0
- package/src/engine/api/media-chunked.ts +622 -0
- package/src/engine/api/media.ts +218 -0
- package/src/engine/api/messages.ts +293 -0
- package/src/engine/api/retry.ts +217 -0
- package/src/engine/api/routes.ts +95 -0
- package/src/engine/api/token.ts +271 -0
- package/src/engine/approval/index.test.ts +22 -0
- package/src/engine/approval/index.ts +224 -0
- package/src/engine/commands/builtin/log-helpers.ts +319 -0
- package/src/engine/commands/builtin/register-all.ts +17 -0
- package/src/engine/commands/builtin/register-approve.ts +201 -0
- package/src/engine/commands/builtin/register-basic.ts +95 -0
- package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
- package/src/engine/commands/builtin/register-logs.ts +20 -0
- package/src/engine/commands/builtin/register-streaming.ts +137 -0
- package/src/engine/commands/builtin/state.ts +31 -0
- package/src/engine/commands/slash-command-auth.ts +48 -0
- package/src/engine/commands/slash-command-handler.ts +146 -0
- package/src/engine/commands/slash-commands-impl.test.ts +8 -0
- package/src/engine/commands/slash-commands-impl.ts +61 -0
- package/src/engine/commands/slash-commands.ts +199 -0
- package/src/engine/config/credential-backup.test.ts +88 -0
- package/src/engine/config/credential-backup.ts +107 -0
- package/src/engine/config/credentials.ts +76 -0
- package/src/engine/config/group.test.ts +234 -0
- package/src/engine/config/group.ts +299 -0
- package/src/engine/config/resolve.test.ts +152 -0
- package/src/engine/config/resolve.ts +283 -0
- package/src/engine/config/setup-logic.ts +84 -0
- package/src/engine/engine-import-boundary.test.ts +73 -0
- package/src/engine/gateway/codec.ts +47 -0
- package/src/engine/gateway/constants.ts +117 -0
- package/src/engine/gateway/event-dispatcher.ts +177 -0
- package/src/engine/gateway/gateway-connection.ts +371 -0
- package/src/engine/gateway/gateway.ts +291 -0
- package/src/engine/gateway/inbound-attachments.test.ts +126 -0
- package/src/engine/gateway/inbound-attachments.ts +360 -0
- package/src/engine/gateway/inbound-context.ts +195 -0
- package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +218 -0
- package/src/engine/gateway/inbound-pipeline.ts +235 -0
- package/src/engine/gateway/interaction-handler.ts +220 -0
- package/src/engine/gateway/message-queue.test.ts +282 -0
- package/src/engine/gateway/message-queue.ts +499 -0
- package/src/engine/gateway/outbound-dispatch.test.ts +231 -0
- package/src/engine/gateway/outbound-dispatch.ts +575 -0
- package/src/engine/gateway/reconnect.ts +199 -0
- package/src/engine/gateway/stages/access-stage.ts +132 -0
- package/src/engine/gateway/stages/assembly-stage.ts +156 -0
- package/src/engine/gateway/stages/content-stage.test.ts +77 -0
- package/src/engine/gateway/stages/content-stage.ts +77 -0
- package/src/engine/gateway/stages/envelope-stage.test.ts +152 -0
- package/src/engine/gateway/stages/envelope-stage.ts +144 -0
- package/src/engine/gateway/stages/group-gate-stage.ts +292 -0
- package/src/engine/gateway/stages/index.ts +18 -0
- package/src/engine/gateway/stages/quote-stage.ts +113 -0
- package/src/engine/gateway/stages/refidx-stage.ts +62 -0
- package/src/engine/gateway/stages/stub-contexts.ts +116 -0
- package/src/engine/gateway/types.ts +264 -0
- package/src/engine/gateway/typing-keepalive.ts +79 -0
- package/src/engine/group/activation.test.ts +114 -0
- package/src/engine/group/activation.ts +147 -0
- package/src/engine/group/history.test.ts +314 -0
- package/src/engine/group/history.ts +321 -0
- package/src/engine/group/mention.test.ts +141 -0
- package/src/engine/group/mention.ts +197 -0
- package/src/engine/group/message-gating.test.ts +188 -0
- package/src/engine/group/message-gating.ts +216 -0
- package/src/engine/messaging/decode-media-path.ts +82 -0
- package/src/engine/messaging/media-source.ts +215 -0
- package/src/engine/messaging/media-type-detect.ts +37 -0
- package/src/engine/messaging/outbound-audio-port.ts +38 -0
- package/src/engine/messaging/outbound-deliver.ts +810 -0
- package/src/engine/messaging/outbound-media-send.ts +702 -0
- package/src/engine/messaging/outbound-reply.ts +27 -0
- package/src/engine/messaging/outbound-result-helpers.ts +54 -0
- package/src/engine/messaging/outbound-types.ts +45 -0
- package/src/engine/messaging/outbound.ts +485 -0
- package/src/engine/messaging/reply-dispatcher.ts +597 -0
- package/src/engine/messaging/reply-limiter.ts +164 -0
- package/src/engine/messaging/sender.ts +729 -0
- package/src/engine/messaging/streaming-c2c.ts +1192 -0
- package/src/engine/messaging/streaming-media-send.ts +544 -0
- package/src/engine/messaging/target-parser.ts +104 -0
- package/src/engine/ref/format-message-ref.ts +142 -0
- package/src/engine/ref/format-ref-entry.test.ts +60 -0
- package/src/engine/ref/format-ref-entry.ts +27 -0
- package/src/engine/ref/store.ts +224 -0
- package/src/engine/ref/types.ts +27 -0
- package/src/engine/session/known-users.ts +254 -0
- package/src/engine/session/session-store.ts +284 -0
- package/src/engine/tools/channel-api.ts +244 -0
- package/src/engine/tools/remind-logic.test.ts +280 -0
- package/src/engine/tools/remind-logic.ts +377 -0
- package/src/engine/types.ts +313 -0
- package/src/engine/utils/attachment-tags.test.ts +186 -0
- package/src/engine/utils/attachment-tags.ts +174 -0
- package/src/engine/utils/audio.test.ts +250 -0
- package/src/engine/utils/audio.ts +585 -0
- package/src/engine/utils/data-paths.ts +38 -0
- package/src/engine/utils/diagnostics.ts +109 -0
- package/src/engine/utils/file-utils.test.ts +72 -0
- package/src/engine/utils/file-utils.ts +225 -0
- package/src/engine/utils/format.test.ts +68 -0
- package/src/engine/utils/format.ts +70 -0
- package/src/engine/utils/image-size.test.ts +158 -0
- package/src/engine/utils/image-size.ts +249 -0
- package/src/engine/utils/log.test.ts +28 -0
- package/src/engine/utils/log.ts +61 -0
- package/src/engine/utils/media-tags.test.ts +32 -0
- package/src/engine/utils/media-tags.ts +177 -0
- package/src/engine/utils/payload.test.ts +68 -0
- package/src/engine/utils/payload.ts +145 -0
- package/src/engine/utils/platform-storage-laziness.test.ts +65 -0
- package/src/engine/utils/platform.test.ts +148 -0
- package/src/engine/utils/platform.ts +343 -0
- package/src/engine/utils/request-context.ts +60 -0
- package/src/engine/utils/string-normalize.ts +91 -0
- package/src/engine/utils/stt.test.ts +104 -0
- package/src/engine/utils/stt.ts +100 -0
- package/src/engine/utils/text-parsing.test.ts +29 -0
- package/src/engine/utils/text-parsing.ts +155 -0
- package/src/engine/utils/upload-cache.ts +96 -0
- package/src/engine/utils/voice-text.ts +15 -0
- package/src/exec-approvals.ts +218 -0
- package/src/manifest-schema.test.ts +56 -0
- package/src/qqbot-test-support.ts +29 -0
- package/src/types.ts +210 -0
- package/tsconfig.json +16 -0
|
@@ -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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `InboundContext` stub builders for early-return paths.
|
|
3
|
+
*
|
|
4
|
+
* Both the access-control "blocked" path and the group-gate "skipped"
|
|
5
|
+
* path need to return a fully populated {@link InboundContext} that the
|
|
6
|
+
* upstream handler can inspect without crashing on undefined fields.
|
|
7
|
+
* Centralising the stubs here prevents the two paths from drifting.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { QQBotAccessResult } from "../../access/index.js";
|
|
11
|
+
import type { InboundContext, InboundGroupInfo } from "../inbound-context.js";
|
|
12
|
+
import type { QueuedMessage } from "../message-queue.js";
|
|
13
|
+
import type { TypingKeepAlive } from "../typing-keepalive.js";
|
|
14
|
+
|
|
15
|
+
/** Shared fields every stub context needs. */
|
|
16
|
+
interface BaseStubFields {
|
|
17
|
+
event: QueuedMessage;
|
|
18
|
+
route: { sessionKey: string; accountId: string; agentId?: string };
|
|
19
|
+
isGroupChat: boolean;
|
|
20
|
+
peerId: string;
|
|
21
|
+
qualifiedTarget: string;
|
|
22
|
+
fromAddress: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Build an {@link InboundContext} with all non-routing fields cleared. */
|
|
26
|
+
function emptyInboundContext(fields: BaseStubFields): InboundContext {
|
|
27
|
+
return {
|
|
28
|
+
event: fields.event,
|
|
29
|
+
route: fields.route,
|
|
30
|
+
isGroupChat: fields.isGroupChat,
|
|
31
|
+
peerId: fields.peerId,
|
|
32
|
+
qualifiedTarget: fields.qualifiedTarget,
|
|
33
|
+
fromAddress: fields.fromAddress,
|
|
34
|
+
parsedContent: "",
|
|
35
|
+
userContent: "",
|
|
36
|
+
quotePart: "",
|
|
37
|
+
dynamicCtx: "",
|
|
38
|
+
userMessage: "",
|
|
39
|
+
agentBody: "",
|
|
40
|
+
body: "",
|
|
41
|
+
systemPrompts: [],
|
|
42
|
+
groupSystemPrompt: undefined,
|
|
43
|
+
attachments: {
|
|
44
|
+
attachmentInfo: "",
|
|
45
|
+
imageUrls: [],
|
|
46
|
+
imageMediaTypes: [],
|
|
47
|
+
voiceAttachmentPaths: [],
|
|
48
|
+
voiceAttachmentUrls: [],
|
|
49
|
+
voiceAsrReferTexts: [],
|
|
50
|
+
voiceTranscripts: [],
|
|
51
|
+
voiceTranscriptSources: [],
|
|
52
|
+
attachmentLocalPaths: [],
|
|
53
|
+
},
|
|
54
|
+
localMediaPaths: [],
|
|
55
|
+
localMediaTypes: [],
|
|
56
|
+
remoteMediaUrls: [],
|
|
57
|
+
remoteMediaTypes: [],
|
|
58
|
+
uniqueVoicePaths: [],
|
|
59
|
+
uniqueVoiceUrls: [],
|
|
60
|
+
uniqueVoiceAsrReferTexts: [],
|
|
61
|
+
voiceMediaTypes: [],
|
|
62
|
+
hasAsrReferFallback: false,
|
|
63
|
+
voiceTranscriptSources: [],
|
|
64
|
+
replyTo: undefined,
|
|
65
|
+
commandAuthorized: false,
|
|
66
|
+
group: undefined,
|
|
67
|
+
blocked: false,
|
|
68
|
+
skipped: false,
|
|
69
|
+
typing: { keepAlive: null },
|
|
70
|
+
inputNotifyRefIdx: undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build an {@link InboundContext} that represents a message blocked by
|
|
76
|
+
* access control (policy denial, allowlist mismatch, etc.).
|
|
77
|
+
*/
|
|
78
|
+
export function buildBlockedInboundContext(
|
|
79
|
+
params: BaseStubFields & {
|
|
80
|
+
access: QQBotAccessResult;
|
|
81
|
+
},
|
|
82
|
+
): InboundContext {
|
|
83
|
+
return {
|
|
84
|
+
...emptyInboundContext(params),
|
|
85
|
+
blocked: true,
|
|
86
|
+
blockReason: params.access.reason,
|
|
87
|
+
blockReasonCode: params.access.reasonCode,
|
|
88
|
+
accessDecision: params.access.decision,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build an {@link InboundContext} that represents a message stopped by
|
|
94
|
+
* the group gate (drop_other_mention, block_unauthorized_command,
|
|
95
|
+
* skip_no_mention). Any history side-effects have already been applied
|
|
96
|
+
* by the gate stage.
|
|
97
|
+
*/
|
|
98
|
+
export function buildSkippedInboundContext(
|
|
99
|
+
params: BaseStubFields & {
|
|
100
|
+
group: InboundGroupInfo;
|
|
101
|
+
skipReason: NonNullable<InboundContext["skipReason"]>;
|
|
102
|
+
access: QQBotAccessResult;
|
|
103
|
+
typing: { keepAlive: TypingKeepAlive | null };
|
|
104
|
+
inputNotifyRefIdx?: string;
|
|
105
|
+
},
|
|
106
|
+
): InboundContext {
|
|
107
|
+
return {
|
|
108
|
+
...emptyInboundContext(params),
|
|
109
|
+
group: params.group,
|
|
110
|
+
skipped: true,
|
|
111
|
+
skipReason: params.skipReason,
|
|
112
|
+
accessDecision: params.access.decision,
|
|
113
|
+
typing: params.typing,
|
|
114
|
+
inputNotifyRefIdx: params.inputNotifyRefIdx,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway types.
|
|
3
|
+
*
|
|
4
|
+
* core/gateway/gateway.ts now imports all dependencies directly (both
|
|
5
|
+
* core/ modules and upper-layer files). The only injected dependency
|
|
6
|
+
* is `runtime` (PluginRuntime), which is a framework-provided object.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ============ Logger ============
|
|
10
|
+
import type { EngineLogger } from "../types.js";
|
|
11
|
+
export type { EngineLogger };
|
|
12
|
+
|
|
13
|
+
// ============ Account ============
|
|
14
|
+
|
|
15
|
+
/** Re-export GatewayAccount from engine/types.ts (single source of truth). */
|
|
16
|
+
import type { GatewayAccount as _GatewayAccount } from "../types.js";
|
|
17
|
+
export type GatewayAccount = _GatewayAccount;
|
|
18
|
+
|
|
19
|
+
// ============ PluginRuntime subset ============
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Subset of PluginRuntime used by the gateway.
|
|
23
|
+
*
|
|
24
|
+
* This is NOT a custom adapter — it's the exact same object shape that
|
|
25
|
+
* the framework injects. We define it here so core/ doesn't need to
|
|
26
|
+
* depend on the plugin-sdk root barrel.
|
|
27
|
+
*/
|
|
28
|
+
export interface GatewayPluginRuntime {
|
|
29
|
+
channel: {
|
|
30
|
+
activity: {
|
|
31
|
+
record: (params: {
|
|
32
|
+
channel: string;
|
|
33
|
+
accountId: string;
|
|
34
|
+
direction: "inbound" | "outbound";
|
|
35
|
+
}) => void;
|
|
36
|
+
};
|
|
37
|
+
routing: {
|
|
38
|
+
resolveAgentRoute: (params: {
|
|
39
|
+
cfg: unknown;
|
|
40
|
+
channel: string;
|
|
41
|
+
accountId: string;
|
|
42
|
+
peer: { kind: "group" | "direct"; id: string };
|
|
43
|
+
}) => { sessionKey: string; accountId: string; agentId?: string };
|
|
44
|
+
};
|
|
45
|
+
reply: {
|
|
46
|
+
dispatchReplyWithBufferedBlockDispatcher: (params: unknown) => Promise<unknown>;
|
|
47
|
+
resolveEffectiveMessagesConfig: (
|
|
48
|
+
cfg: unknown,
|
|
49
|
+
agentId?: string,
|
|
50
|
+
) => { responsePrefix?: string };
|
|
51
|
+
finalizeInboundContext: (fields: Record<string, unknown>) => unknown;
|
|
52
|
+
formatInboundEnvelope: (params: unknown) => string;
|
|
53
|
+
resolveEnvelopeFormatOptions: (cfg: unknown) => unknown;
|
|
54
|
+
};
|
|
55
|
+
session: {
|
|
56
|
+
resolveStorePath: (store: unknown, params: { agentId: string }) => string;
|
|
57
|
+
recordInboundSession: (params: unknown) => Promise<unknown>;
|
|
58
|
+
};
|
|
59
|
+
turn: {
|
|
60
|
+
run: (params: unknown) => Promise<unknown>;
|
|
61
|
+
};
|
|
62
|
+
text: {
|
|
63
|
+
chunkMarkdownText: (text: string, limit: number) => string[];
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
tts: {
|
|
67
|
+
textToSpeech: (params: {
|
|
68
|
+
text: string;
|
|
69
|
+
cfg: unknown;
|
|
70
|
+
channel: string;
|
|
71
|
+
accountId?: string;
|
|
72
|
+
}) => Promise<{
|
|
73
|
+
success: boolean;
|
|
74
|
+
audioPath?: string;
|
|
75
|
+
provider?: string;
|
|
76
|
+
outputFormat?: string;
|
|
77
|
+
error?: string;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Config API for reading/writing the framework configuration.
|
|
82
|
+
*
|
|
83
|
+
* Used by the interaction handler (config query/update) directly
|
|
84
|
+
* within the engine layer. Optional because not all runtime
|
|
85
|
+
* environments provide config write capability.
|
|
86
|
+
*/
|
|
87
|
+
config?: {
|
|
88
|
+
current: () => Record<string, unknown>;
|
|
89
|
+
replaceConfigFile: (params: {
|
|
90
|
+
nextConfig: unknown;
|
|
91
|
+
afterWrite: { mode: "auto" };
|
|
92
|
+
}) => Promise<unknown>;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============ Shared result types ============
|
|
97
|
+
|
|
98
|
+
/** Re-export ProcessedAttachments from inbound-attachments (single source of truth). */
|
|
99
|
+
export type { ProcessedAttachments } from "./inbound-attachments.js";
|
|
100
|
+
|
|
101
|
+
/** Outbound result from media sends. */
|
|
102
|
+
export interface OutboundResult {
|
|
103
|
+
channel: string;
|
|
104
|
+
messageId?: string;
|
|
105
|
+
timestamp?: string | number;
|
|
106
|
+
error?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Re-export RefAttachmentSummary for convenience. */
|
|
110
|
+
export type { RefAttachmentSummary } from "../ref/types.js";
|
|
111
|
+
|
|
112
|
+
// ============ WebSocket Event Types ============
|
|
113
|
+
|
|
114
|
+
/** Raw WebSocket payload structure. */
|
|
115
|
+
export interface WSPayload {
|
|
116
|
+
op: number;
|
|
117
|
+
d: unknown;
|
|
118
|
+
s?: number;
|
|
119
|
+
t?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Attachment shape shared by all message event types. */
|
|
123
|
+
interface RawMessageAttachment {
|
|
124
|
+
content_type: string;
|
|
125
|
+
url: string;
|
|
126
|
+
filename?: string;
|
|
127
|
+
voice_wav_url?: string;
|
|
128
|
+
asr_refer_text?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Referenced message element (used for quote messages). */
|
|
132
|
+
interface RawMsgElement {
|
|
133
|
+
msg_idx?: string;
|
|
134
|
+
content?: string;
|
|
135
|
+
attachments?: Array<
|
|
136
|
+
RawMessageAttachment & {
|
|
137
|
+
height?: number;
|
|
138
|
+
width?: number;
|
|
139
|
+
size?: number;
|
|
140
|
+
}
|
|
141
|
+
>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface C2CMessageEvent {
|
|
145
|
+
id: string;
|
|
146
|
+
content: string;
|
|
147
|
+
timestamp: string;
|
|
148
|
+
author: { user_openid: string };
|
|
149
|
+
attachments?: RawMessageAttachment[];
|
|
150
|
+
message_scene?: { ext?: string[] };
|
|
151
|
+
message_type?: number;
|
|
152
|
+
msg_elements?: RawMsgElement[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface GuildMessageEvent {
|
|
156
|
+
id: string;
|
|
157
|
+
content: string;
|
|
158
|
+
timestamp: string;
|
|
159
|
+
author: { id: string; username?: string };
|
|
160
|
+
channel_id: string;
|
|
161
|
+
guild_id: string;
|
|
162
|
+
attachments?: RawMessageAttachment[];
|
|
163
|
+
message_scene?: { ext?: string[] };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface GroupMessageEvent {
|
|
167
|
+
id: string;
|
|
168
|
+
content: string;
|
|
169
|
+
timestamp: string;
|
|
170
|
+
author: {
|
|
171
|
+
member_openid: string;
|
|
172
|
+
username?: string;
|
|
173
|
+
/** True when the sender is itself a bot. */
|
|
174
|
+
bot?: boolean;
|
|
175
|
+
};
|
|
176
|
+
group_openid: string;
|
|
177
|
+
attachments?: RawMessageAttachment[];
|
|
178
|
+
/** Optional @mentions list with per-entry is_you / member_openid / nickname. */
|
|
179
|
+
mentions?: Array<{
|
|
180
|
+
scope?: "all" | "single";
|
|
181
|
+
id?: string;
|
|
182
|
+
user_openid?: string;
|
|
183
|
+
member_openid?: string;
|
|
184
|
+
nickname?: string;
|
|
185
|
+
username?: string;
|
|
186
|
+
bot?: boolean;
|
|
187
|
+
/** `true` when this mention targets the bot itself. */
|
|
188
|
+
is_you?: boolean;
|
|
189
|
+
}>;
|
|
190
|
+
message_scene?: { source?: string; ext?: string[] };
|
|
191
|
+
message_type?: number;
|
|
192
|
+
msg_elements?: RawMsgElement[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ============ Gateway Context ============
|
|
196
|
+
|
|
197
|
+
import type { EngineAdapters } from "../adapter/index.js";
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Group-chat behaviour options.
|
|
201
|
+
*
|
|
202
|
+
* Grouped under a dedicated sub-object on {@link CoreGatewayContext} so
|
|
203
|
+
* future additions (admin lookup, proactive push, per-group toggles)
|
|
204
|
+
* don't keep polluting the top-level context type.
|
|
205
|
+
*/
|
|
206
|
+
interface GatewayGroupOptions {
|
|
207
|
+
/**
|
|
208
|
+
* Whether group-chat gating is enabled. Defaults to `true`; set to
|
|
209
|
+
* `false` to disable all group processing (e.g. for a DM-only smoke
|
|
210
|
+
* test). When disabled, the engine does not allocate a history
|
|
211
|
+
* buffer and does not instantiate the session-store reader.
|
|
212
|
+
*/
|
|
213
|
+
enabled?: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Whether the framework has text-based control commands enabled. When
|
|
216
|
+
* `false`, the group gate skips the "unauthorized command" check and
|
|
217
|
+
* the command-bypass path.
|
|
218
|
+
*/
|
|
219
|
+
allowTextCommands?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Optional probe that returns true when `content` is a recognised
|
|
222
|
+
* control command. Injected to avoid hard-coding a command list in
|
|
223
|
+
* the engine. When omitted, no message is treated as a control
|
|
224
|
+
* command and the bypass path never activates.
|
|
225
|
+
*/
|
|
226
|
+
isControlCommand?: (content: string) => boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Platform hook that contributes a channel-level group intro hint
|
|
229
|
+
* (e.g. "当前群: 开发讨论组"). Invoked per-group when building the
|
|
230
|
+
* system prompt.
|
|
231
|
+
*/
|
|
232
|
+
resolveIntroHint?: (params: {
|
|
233
|
+
cfg: unknown;
|
|
234
|
+
accountId: string;
|
|
235
|
+
groupId: string;
|
|
236
|
+
}) => string | undefined;
|
|
237
|
+
/**
|
|
238
|
+
* Session-store reader for the `/activation` command override. When
|
|
239
|
+
* omitted, the engine loads a default node-based reader lazily.
|
|
240
|
+
*/
|
|
241
|
+
sessionStoreReader?: import("../group/activation.js").SessionStoreReader;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Full gateway startup context. */
|
|
245
|
+
export interface CoreGatewayContext {
|
|
246
|
+
account: GatewayAccount;
|
|
247
|
+
abortSignal: AbortSignal;
|
|
248
|
+
cfg: unknown;
|
|
249
|
+
onReady?: (data: unknown) => void;
|
|
250
|
+
/**
|
|
251
|
+
* Invoked when a RESUMED event is received after reconnect.
|
|
252
|
+
* Falls back to `onReady` when not provided so existing callers
|
|
253
|
+
* keep their current behaviour.
|
|
254
|
+
*/
|
|
255
|
+
onResumed?: (data: unknown) => void;
|
|
256
|
+
onError?: (error: Error) => void;
|
|
257
|
+
log?: EngineLogger;
|
|
258
|
+
/** PluginRuntime injected by the framework — same object in both versions. */
|
|
259
|
+
runtime: GatewayPluginRuntime;
|
|
260
|
+
/** Group-chat tuning options. */
|
|
261
|
+
group?: GatewayGroupOptions;
|
|
262
|
+
/** Adapter ports — delegates audio, history, mention gating, commands to bridge implementations. */
|
|
263
|
+
adapters: EngineAdapters;
|
|
264
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Periodically refresh C2C typing state while a response is in progress.
|
|
3
|
+
*
|
|
4
|
+
* All I/O operations are injected via constructor parameters so this
|
|
5
|
+
* module has zero external dependencies and can run in both plugin versions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { formatErrorMessage } from "../utils/format.js";
|
|
9
|
+
|
|
10
|
+
/** Function that sends a typing indicator to one user. */
|
|
11
|
+
type SendInputNotifyFn = (
|
|
12
|
+
token: string,
|
|
13
|
+
openid: string,
|
|
14
|
+
msgId: string | undefined,
|
|
15
|
+
inputSecond: number,
|
|
16
|
+
) => Promise<unknown>;
|
|
17
|
+
|
|
18
|
+
/** Refresh every 50s for the QQ API's 60s input-notify window. */
|
|
19
|
+
const TYPING_INTERVAL_MS = 50_000;
|
|
20
|
+
export const TYPING_INPUT_SECOND = 60;
|
|
21
|
+
|
|
22
|
+
export class TypingKeepAlive {
|
|
23
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
24
|
+
private stopped = false;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly getToken: () => Promise<string>,
|
|
28
|
+
private readonly clearCache: () => void,
|
|
29
|
+
private readonly sendInputNotify: SendInputNotifyFn,
|
|
30
|
+
private readonly openid: string,
|
|
31
|
+
private readonly msgId: string | undefined,
|
|
32
|
+
private readonly log?: {
|
|
33
|
+
info: (msg: string) => void;
|
|
34
|
+
error: (msg: string) => void;
|
|
35
|
+
debug?: (msg: string) => void;
|
|
36
|
+
},
|
|
37
|
+
) {}
|
|
38
|
+
|
|
39
|
+
/** Start periodic keep-alive sends. */
|
|
40
|
+
start(): void {
|
|
41
|
+
if (this.stopped) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.timer = setInterval(() => {
|
|
45
|
+
if (this.stopped) {
|
|
46
|
+
this.stop();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.send().catch(() => {});
|
|
50
|
+
}, TYPING_INTERVAL_MS);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Stop periodic keep-alive sends. */
|
|
54
|
+
stop(): void {
|
|
55
|
+
this.stopped = true;
|
|
56
|
+
if (this.timer) {
|
|
57
|
+
clearInterval(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async send(): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
const token = await this.getToken();
|
|
65
|
+
await this.sendInputNotify(token, this.openid, this.msgId, TYPING_INPUT_SECOND);
|
|
66
|
+
this.log?.debug?.(`Typing keep-alive sent to ${this.openid}`);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
try {
|
|
69
|
+
this.clearCache();
|
|
70
|
+
const token = await this.getToken();
|
|
71
|
+
await this.sendInputNotify(token, this.openid, this.msgId, TYPING_INPUT_SECOND);
|
|
72
|
+
} catch {
|
|
73
|
+
this.log?.debug?.(
|
|
74
|
+
`Typing keep-alive failed for ${this.openid}: ${formatErrorMessage(err)}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|