@openclaw/qqbot 2026.5.7 → 2026.5.10-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 +6 -6
- package/dist/{channel-CC2YO9fj.js → channel-Cl-NJpPD.js} +132 -62
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-ByCRAe5H.js → channel.setup-BDM8y3aD.js} +2 -2
- package/dist/{config-D6545NkC.js → config-BNAanXEH.js} +2 -2
- package/dist/{config-schema-DFcjQw73.js → config-schema-BTV9NZbt.js} +4 -4
- package/dist/{exec-approvals-COUsM6wZ.js → exec-approvals-DgO1msIB.js} +1 -1
- package/dist/{gateway-Cs3-_on9.js → gateway-OhbnW8Uq.js} +226 -584
- package/dist/{handler-runtime-Bqm6N0WG.js → handler-runtime-C9SrM2gR.js} +4 -4
- package/dist/{outbound-BJfhwrPg.js → outbound-DL154TS6.js} +19 -58
- package/dist/{request-context-Cjwx7OwW.js → request-context-Bn63CUkr.js} +26 -7
- package/dist/{resolve-D_06fV6-.js → resolve-Cm33TWZp.js} +1 -1
- package/dist/{runtime-BJAS3eXW.js → runtime-CHGblcVc.js} +1 -1
- package/dist/runtime-api.js +1 -1
- package/dist/{sender-p-B14eLG.js → sender-C-TmIFow.js} +241 -268
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{string-normalize-Ci6NM5DE.js → string-normalize-BzzzPOeb.js} +0 -16
- package/dist/{target-parser-Y0prnrXD.js → target-parser-CTNFSKb1.js} +2 -2
- package/package.json +5 -6
- package/skills/qqbot-channel/SKILL.md +1 -1
- package/skills/qqbot-media/SKILL.md +1 -1
- package/skills/qqbot-remind/SKILL.md +1 -1
|
@@ -1,49 +1,30 @@
|
|
|
1
|
-
import { E as setOutboundAudioPort, _ as sendVoice, c as isMediaPayload, g as sendVideoMsg, h as sendPhoto, l as parseQQBotPayload, m as sendDocument, o as encodePayloadForCron, r as sendMedia, s as isCronReminderPayload, u as normalizeMediaTags, v as resolveUserFacingMediaError, w as DEFAULT_MEDIA_SEND_ERROR } from "./outbound-
|
|
2
|
-
import { c as getPlatformAdapter, i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, o as readStringField, s as sanitizeFileName, t as asOptionalObjectRecord } from "./string-normalize-
|
|
3
|
-
import { d as setBridgeLogger, l as ensurePlatformAdapter, s as resolveAccountBase } from "./resolve-
|
|
1
|
+
import { E as setOutboundAudioPort, _ as sendVoice, c as isMediaPayload, g as sendVideoMsg, h as sendPhoto, l as parseQQBotPayload, m as sendDocument, o as encodePayloadForCron, r as sendMedia, s as isCronReminderPayload, u as normalizeMediaTags, v as resolveUserFacingMediaError, w as DEFAULT_MEDIA_SEND_ERROR } from "./outbound-DL154TS6.js";
|
|
2
|
+
import { c as getPlatformAdapter, i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, o as readStringField, s as sanitizeFileName, t as asOptionalObjectRecord } from "./string-normalize-BzzzPOeb.js";
|
|
3
|
+
import { d as setBridgeLogger, l as ensurePlatformAdapter, s as resolveAccountBase } from "./resolve-Cm33TWZp.js";
|
|
4
4
|
import { i as parseApprovalButtonData } from "./approval-cg0SVahb.js";
|
|
5
5
|
import { t as toGatewayAccount } from "./narrowing-BoieBTIU.js";
|
|
6
|
-
import { B as StreamInputState, C as
|
|
7
|
-
import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-
|
|
8
|
-
import { a as getHomeDir, c as getQQBotMediaDir, d as isLocalPath, f as isWindows, i as checkSilkWasmAvailable, m as resolveQQBotPayloadLocalFilePath, o as getQQBotDataDir, p as normalizePath$1, s as getQQBotDataPath, u as getTempDir } from "./target-parser-
|
|
9
|
-
import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-
|
|
6
|
+
import { B as StreamInputState, C as debugWarn, D as openLocalFile, I as formatDuration, L as formatErrorMessage, N as getImageMimeType, P as getMaxUploadSize, R as StreamContentType, S as debugLog, T as getNextMsgSeq, a as createRawInputNotifyFn, b as withTokenRetry, c as getMessageApi, d as initSender, f as onMessageSent, g as sendText, h as sendMedia$1, i as clearTokenCache, j as formatFileSize, k as downloadFile, l as getPluginUserAgent, m as sendInputNotify, n as acknowledgeInteraction, o as getAccessToken, p as registerAccount, r as buildDeliveryTarget, s as getGatewayUrl, t as accountToCreds, u as initApiConfig, v as startBackgroundTokenRefresh, x as debugError, y as stopBackgroundTokenRefresh, z as StreamInputMode } from "./sender-C-TmIFow.js";
|
|
7
|
+
import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-CHGblcVc.js";
|
|
8
|
+
import { a as getHomeDir, c as getQQBotMediaDir, d as isLocalPath, f as isWindows, i as checkSilkWasmAvailable, m as resolveQQBotPayloadLocalFilePath, o as getQQBotDataDir, p as normalizePath$1, s as getQQBotDataPath, u as getTempDir } from "./target-parser-CTNFSKb1.js";
|
|
9
|
+
import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-Bn63CUkr.js";
|
|
10
10
|
import * as fs$1 from "node:fs";
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import * as os$1 from "node:os";
|
|
13
13
|
import crypto from "node:crypto";
|
|
14
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
15
|
+
import { appendRegularFileSync, privateFileStoreSync, readRegularFileSync, replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
|
|
14
16
|
import * as path$1 from "node:path";
|
|
15
17
|
import path from "node:path";
|
|
18
|
+
import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
|
|
16
19
|
import { resolveRuntimeServiceVersion } from "openclaw/plugin-sdk/cli-runtime";
|
|
17
20
|
import WebSocket from "ws";
|
|
21
|
+
import { resolveAmbientNodeProxyAgent } from "openclaw/plugin-sdk/extension-shared";
|
|
18
22
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
19
23
|
import { fileURLToPath } from "node:url";
|
|
20
|
-
import {
|
|
24
|
+
import { createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
25
|
+
import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-mention-gating";
|
|
21
26
|
import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
|
|
22
27
|
//#region extensions/qqbot/src/engine/group/activation.ts
|
|
23
|
-
/**
|
|
24
|
-
* Group activation mode — how the bot decides whether to respond in a group.
|
|
25
|
-
*
|
|
26
|
-
* Resolution chain:
|
|
27
|
-
* 1. session store override (`/activation` command writes per-session
|
|
28
|
-
* `groupActivation` value) — highest priority
|
|
29
|
-
* 2. per-group `requireMention` config
|
|
30
|
-
* 3. `"mention"` default (require @-bot to respond)
|
|
31
|
-
*
|
|
32
|
-
* File I/O is isolated in the default node-based reader so the gating
|
|
33
|
-
* logic itself stays a pure function, testable without touching disk.
|
|
34
|
-
*
|
|
35
|
-
* Note: the implicit-mention predicate (quoting a bot message counts as
|
|
36
|
-
* @-ing the bot) lives in `./mention.ts` alongside the other mention
|
|
37
|
-
* helpers — see `resolveImplicitMention` there.
|
|
38
|
-
*/
|
|
39
|
-
/**
|
|
40
|
-
* Resolve the effective activation mode for one inbound message.
|
|
41
|
-
*
|
|
42
|
-
* Order of precedence:
|
|
43
|
-
* 1. `store[sessionKey].groupActivation` (read via the injected reader)
|
|
44
|
-
* 2. config-level `requireMention` (maps to `"mention"` / `"always"`)
|
|
45
|
-
* 3. `"mention"` (safe default)
|
|
46
|
-
*/
|
|
47
28
|
function resolveGroupActivation(params) {
|
|
48
29
|
const fallback = params.configRequireMention ? "mention" : "always";
|
|
49
30
|
const store = params.sessionStoreReader?.read({
|
|
@@ -57,14 +38,6 @@ function resolveGroupActivation(params) {
|
|
|
57
38
|
if (normalized === "mention" || normalized === "always") return normalized;
|
|
58
39
|
return fallback;
|
|
59
40
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Resolve the on-disk path to the agent-sessions file.
|
|
62
|
-
*
|
|
63
|
-
* Priority:
|
|
64
|
-
* 1. `cfg.session.store` (supports `{agentId}` placeholder and `~` expansion)
|
|
65
|
-
* 2. `$OPENCLAW_STATE_DIR` / `$CLAWDBOT_STATE_DIR`
|
|
66
|
-
* 3. `~/.openclaw/agents/{agentId}/sessions/sessions.json`
|
|
67
|
-
*/
|
|
68
41
|
function resolveSessionStorePath(cfg, agentId) {
|
|
69
42
|
const resolvedAgentId = agentId || "default";
|
|
70
43
|
const session = typeof cfg.session === "object" && cfg.session !== null ? cfg.session : void 0;
|
|
@@ -81,16 +54,6 @@ function resolveSessionStorePath(cfg, agentId) {
|
|
|
81
54
|
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path.join(process.env.HOME || process.env.USERPROFILE || "", ".openclaw");
|
|
82
55
|
return path.join(stateDir, "agents", resolvedAgentId, "sessions", "sessions.json");
|
|
83
56
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Create the default, production-ready session-store reader.
|
|
86
|
-
*
|
|
87
|
-
* Reads the file synchronously on every call. The overhead is acceptable
|
|
88
|
-
* because activation mode is only resolved once per group message and
|
|
89
|
-
* the sessions file is typically a handful of kilobytes.
|
|
90
|
-
*
|
|
91
|
-
* Any I/O or JSON error is swallowed and returned as `null` so the
|
|
92
|
-
* gating pipeline falls back to the config default.
|
|
93
|
-
*/
|
|
94
57
|
function createNodeSessionStoreReader() {
|
|
95
58
|
return { read: ({ cfg, agentId }) => {
|
|
96
59
|
try {
|
|
@@ -258,7 +221,10 @@ function ensureDir$2() {
|
|
|
258
221
|
function appendLine(line) {
|
|
259
222
|
try {
|
|
260
223
|
ensureDir$2();
|
|
261
|
-
|
|
224
|
+
appendRegularFileSync({
|
|
225
|
+
filePath: getRefIndexFile(),
|
|
226
|
+
content: JSON.stringify(line) + "\n"
|
|
227
|
+
});
|
|
262
228
|
totalLinesOnDisk++;
|
|
263
229
|
} catch (err) {
|
|
264
230
|
debugError(`[ref-index-store] Failed to append: ${formatErrorMessage(err)}`);
|
|
@@ -273,7 +239,6 @@ function compactFile() {
|
|
|
273
239
|
try {
|
|
274
240
|
ensureDir$2();
|
|
275
241
|
const refIndexFile = getRefIndexFile();
|
|
276
|
-
const tmpPath = refIndexFile + ".tmp";
|
|
277
242
|
const lines = [];
|
|
278
243
|
for (const [key, entry] of cache) lines.push(JSON.stringify({
|
|
279
244
|
k: key,
|
|
@@ -287,8 +252,11 @@ function compactFile() {
|
|
|
287
252
|
},
|
|
288
253
|
t: entry._createdAt
|
|
289
254
|
}));
|
|
290
|
-
|
|
291
|
-
|
|
255
|
+
replaceFileAtomicSync({
|
|
256
|
+
filePath: refIndexFile,
|
|
257
|
+
content: `${lines.join("\n")}\n`,
|
|
258
|
+
tempPrefix: ".qqbot-ref-index"
|
|
259
|
+
});
|
|
292
260
|
totalLinesOnDisk = cache.size;
|
|
293
261
|
debugLog(`[ref-index-store] Compacted: ${before} lines → ${totalLinesOnDisk} lines`);
|
|
294
262
|
} catch (err) {
|
|
@@ -405,27 +373,6 @@ async function runDiagnostics() {
|
|
|
405
373
|
return report;
|
|
406
374
|
}
|
|
407
375
|
//#endregion
|
|
408
|
-
//#region extensions/qqbot/src/engine/access/resolve-policy.ts
|
|
409
|
-
function hasRealRestriction(list) {
|
|
410
|
-
if (!list || list.length === 0) return false;
|
|
411
|
-
return !list.every((entry) => String(entry).trim() === "*");
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Derive the effective dmPolicy and groupPolicy applied at runtime.
|
|
415
|
-
*
|
|
416
|
-
* Caller should pass the raw `QQBotAccountConfig`. The resolver does
|
|
417
|
-
* not look at `groups[id]` overrides — per-group overrides are layered
|
|
418
|
-
* on top elsewhere (see `inbound-pipeline` mention gating).
|
|
419
|
-
*/
|
|
420
|
-
function resolveQQBotEffectivePolicies(input) {
|
|
421
|
-
const allowFromRestricted = hasRealRestriction(input.allowFrom);
|
|
422
|
-
const groupAllowFromRestricted = hasRealRestriction(input.groupAllowFrom);
|
|
423
|
-
return {
|
|
424
|
-
dmPolicy: input.dmPolicy ?? (allowFromRestricted ? "allowlist" : "open"),
|
|
425
|
-
groupPolicy: input.groupPolicy ?? (groupAllowFromRestricted || allowFromRestricted ? "allowlist" : "open")
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
//#endregion
|
|
429
376
|
//#region extensions/qqbot/src/engine/access/sender-match.ts
|
|
430
377
|
/**
|
|
431
378
|
* QQBot sender normalization and allowlist matching.
|
|
@@ -465,177 +412,13 @@ function createQQBotSenderMatcher(senderId) {
|
|
|
465
412
|
};
|
|
466
413
|
}
|
|
467
414
|
//#endregion
|
|
468
|
-
//#region extensions/qqbot/src/engine/access/types.ts
|
|
469
|
-
/** Structured reason codes used in logs and metrics. */
|
|
470
|
-
const QQBOT_ACCESS_REASON = {
|
|
471
|
-
DM_POLICY_OPEN: "dm_policy_open",
|
|
472
|
-
DM_POLICY_DISABLED: "dm_policy_disabled",
|
|
473
|
-
DM_POLICY_ALLOWLISTED: "dm_policy_allowlisted",
|
|
474
|
-
DM_POLICY_NOT_ALLOWLISTED: "dm_policy_not_allowlisted",
|
|
475
|
-
DM_POLICY_EMPTY_ALLOWLIST: "dm_policy_empty_allowlist",
|
|
476
|
-
GROUP_POLICY_ALLOWED: "group_policy_allowed",
|
|
477
|
-
GROUP_POLICY_DISABLED: "group_policy_disabled",
|
|
478
|
-
GROUP_POLICY_EMPTY_ALLOWLIST: "group_policy_empty_allowlist",
|
|
479
|
-
GROUP_POLICY_NOT_ALLOWLISTED: "group_policy_not_allowlisted",
|
|
480
|
-
BOT_SELF_ECHO: "bot_self_echo"
|
|
481
|
-
};
|
|
482
|
-
//#endregion
|
|
483
|
-
//#region extensions/qqbot/src/engine/access/access-control.ts
|
|
484
|
-
/**
|
|
485
|
-
* QQBot inbound access decision.
|
|
486
|
-
*
|
|
487
|
-
* This module is the single place where the QQBot engine decides
|
|
488
|
-
* whether an inbound message from a given sender is allowed to
|
|
489
|
-
* proceed into the outbound pipeline. The implementation mirrors the
|
|
490
|
-
* semantics of the framework-wide `resolveDmGroupAccessDecision`
|
|
491
|
-
* (`src/security/dm-policy-shared.ts`) but is kept standalone so the
|
|
492
|
-
* `engine/` layer does not pull in `openclaw/plugin-sdk/*` modules —
|
|
493
|
-
* a hard constraint shared with the standalone `openclaw-qqbot` build.
|
|
494
|
-
*
|
|
495
|
-
* If in the future we lift the zero-dependency rule in the engine
|
|
496
|
-
* layer, this file can be replaced by a thin adapter around the
|
|
497
|
-
* framework API with identical semantics.
|
|
498
|
-
*/
|
|
499
|
-
/**
|
|
500
|
-
* Evaluate the inbound access policy.
|
|
501
|
-
*
|
|
502
|
-
* Semantics (aligned with `resolveDmGroupAccessDecision`):
|
|
503
|
-
* - Group message:
|
|
504
|
-
* - `groupPolicy=disabled` → block
|
|
505
|
-
* - `groupPolicy=open` → allow
|
|
506
|
-
* - `groupPolicy=allowlist`:
|
|
507
|
-
* - empty effectiveGroupAllowFrom → block (empty_allowlist)
|
|
508
|
-
* - sender not in list → block (not_allowlisted)
|
|
509
|
-
* - otherwise → allow
|
|
510
|
-
* - Direct message:
|
|
511
|
-
* - `dmPolicy=disabled` → block
|
|
512
|
-
* - `dmPolicy=open` → allow wildcard, legacy empty allowFrom, or matching allowFrom
|
|
513
|
-
* - `dmPolicy=allowlist`:
|
|
514
|
-
* - empty effectiveAllowFrom → block (empty_allowlist)
|
|
515
|
-
* - sender not in list → block (not_allowlisted)
|
|
516
|
-
* - otherwise → allow
|
|
517
|
-
*
|
|
518
|
-
* The function never throws; callers can rely on the returned
|
|
519
|
-
* `decision`/`reasonCode` pair for branching.
|
|
520
|
-
*/
|
|
521
|
-
function resolveQQBotAccess(input) {
|
|
522
|
-
const { dmPolicy, groupPolicy } = resolveQQBotEffectivePolicies(input);
|
|
523
|
-
const rawGroupAllowFrom = input.groupAllowFrom && input.groupAllowFrom.length > 0 ? input.groupAllowFrom : input.allowFrom ?? [];
|
|
524
|
-
const normalizedAllowFrom = normalizeQQBotAllowFrom(input.allowFrom);
|
|
525
|
-
const effectiveAllowFrom = dmPolicy === "open" && normalizedAllowFrom.length === 0 ? ["*"] : normalizedAllowFrom;
|
|
526
|
-
const effectiveGroupAllowFrom = normalizeQQBotAllowFrom(rawGroupAllowFrom);
|
|
527
|
-
const isSenderAllowed = createQQBotSenderMatcher(input.senderId);
|
|
528
|
-
if (input.isGroup) return evaluateGroupDecision({
|
|
529
|
-
groupPolicy,
|
|
530
|
-
dmPolicy,
|
|
531
|
-
effectiveAllowFrom,
|
|
532
|
-
effectiveGroupAllowFrom,
|
|
533
|
-
isSenderAllowed
|
|
534
|
-
});
|
|
535
|
-
return evaluateDmDecision({
|
|
536
|
-
groupPolicy,
|
|
537
|
-
dmPolicy,
|
|
538
|
-
effectiveAllowFrom,
|
|
539
|
-
effectiveGroupAllowFrom,
|
|
540
|
-
isSenderAllowed
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
function evaluateGroupDecision(ctx) {
|
|
544
|
-
const base = buildResultBase(ctx);
|
|
545
|
-
if (ctx.groupPolicy === "disabled") return {
|
|
546
|
-
...base,
|
|
547
|
-
decision: "block",
|
|
548
|
-
reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_DISABLED,
|
|
549
|
-
reason: "groupPolicy=disabled"
|
|
550
|
-
};
|
|
551
|
-
if (ctx.groupPolicy === "open") return {
|
|
552
|
-
...base,
|
|
553
|
-
decision: "allow",
|
|
554
|
-
reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_ALLOWED,
|
|
555
|
-
reason: "groupPolicy=open"
|
|
556
|
-
};
|
|
557
|
-
if (ctx.effectiveGroupAllowFrom.length === 0) return {
|
|
558
|
-
...base,
|
|
559
|
-
decision: "block",
|
|
560
|
-
reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST,
|
|
561
|
-
reason: "groupPolicy=allowlist (empty allowlist)"
|
|
562
|
-
};
|
|
563
|
-
if (!ctx.isSenderAllowed(ctx.effectiveGroupAllowFrom)) return {
|
|
564
|
-
...base,
|
|
565
|
-
decision: "block",
|
|
566
|
-
reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED,
|
|
567
|
-
reason: "groupPolicy=allowlist (not allowlisted)"
|
|
568
|
-
};
|
|
569
|
-
return {
|
|
570
|
-
...base,
|
|
571
|
-
decision: "allow",
|
|
572
|
-
reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_ALLOWED,
|
|
573
|
-
reason: "groupPolicy=allowlist (allowlisted)"
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
function evaluateDmDecision(ctx) {
|
|
577
|
-
const base = buildResultBase(ctx);
|
|
578
|
-
if (ctx.dmPolicy === "disabled") return {
|
|
579
|
-
...base,
|
|
580
|
-
decision: "block",
|
|
581
|
-
reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_DISABLED,
|
|
582
|
-
reason: "dmPolicy=disabled"
|
|
583
|
-
};
|
|
584
|
-
if (ctx.dmPolicy === "open") {
|
|
585
|
-
if (ctx.effectiveAllowFrom.includes("*")) return {
|
|
586
|
-
...base,
|
|
587
|
-
decision: "allow",
|
|
588
|
-
reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_OPEN,
|
|
589
|
-
reason: "dmPolicy=open"
|
|
590
|
-
};
|
|
591
|
-
if (ctx.isSenderAllowed(ctx.effectiveAllowFrom)) return {
|
|
592
|
-
...base,
|
|
593
|
-
decision: "allow",
|
|
594
|
-
reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_ALLOWLISTED,
|
|
595
|
-
reason: "dmPolicy=open (allowlisted)"
|
|
596
|
-
};
|
|
597
|
-
return {
|
|
598
|
-
...base,
|
|
599
|
-
decision: "block",
|
|
600
|
-
reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
|
|
601
|
-
reason: "dmPolicy=open (not allowlisted)"
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
if (ctx.effectiveAllowFrom.length === 0) return {
|
|
605
|
-
...base,
|
|
606
|
-
decision: "block",
|
|
607
|
-
reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_EMPTY_ALLOWLIST,
|
|
608
|
-
reason: "dmPolicy=allowlist (empty allowlist)"
|
|
609
|
-
};
|
|
610
|
-
if (!ctx.isSenderAllowed(ctx.effectiveAllowFrom)) return {
|
|
611
|
-
...base,
|
|
612
|
-
decision: "block",
|
|
613
|
-
reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
|
|
614
|
-
reason: "dmPolicy=allowlist (not allowlisted)"
|
|
615
|
-
};
|
|
616
|
-
return {
|
|
617
|
-
...base,
|
|
618
|
-
decision: "allow",
|
|
619
|
-
reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_ALLOWLISTED,
|
|
620
|
-
reason: "dmPolicy=allowlist (allowlisted)"
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
function buildResultBase(ctx) {
|
|
624
|
-
return {
|
|
625
|
-
effectiveAllowFrom: ctx.effectiveAllowFrom,
|
|
626
|
-
effectiveGroupAllowFrom: ctx.effectiveGroupAllowFrom,
|
|
627
|
-
dmPolicy: ctx.dmPolicy,
|
|
628
|
-
groupPolicy: ctx.groupPolicy
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
//#endregion
|
|
632
415
|
//#region extensions/qqbot/src/engine/commands/slash-command-auth.ts
|
|
633
416
|
/**
|
|
634
417
|
* Pre-dispatch authorization for requireAuth slash commands.
|
|
635
418
|
*
|
|
636
|
-
* Unlike the
|
|
637
|
-
*
|
|
638
|
-
*
|
|
419
|
+
* Unlike the inbound message ingress command projection (which permits
|
|
420
|
+
* open-policy chat senders), this function requires the sender to appear in an
|
|
421
|
+
* **explicit non-wildcard** allowFrom list.
|
|
639
422
|
*
|
|
640
423
|
* Rationale: sensitive operations (log export, file deletion, approval
|
|
641
424
|
* config changes) must be gated behind a deliberate operator decision.
|
|
@@ -704,6 +487,22 @@ async function trySlashCommand(msg, ctx) {
|
|
|
704
487
|
}
|
|
705
488
|
const receivedAt = Date.now();
|
|
706
489
|
const peerId = ctx.getMessagePeerId(msg);
|
|
490
|
+
const isGroup = msg.type === "group" || msg.type === "guild";
|
|
491
|
+
const commandsAllowFrom = resolveQQBotCommandsAllowFrom(ctx.cfg);
|
|
492
|
+
const commandAuthorized = ctx.resolveCommandAuthorized ? await ctx.resolveCommandAuthorized({
|
|
493
|
+
isGroup,
|
|
494
|
+
senderId: msg.senderId,
|
|
495
|
+
conversationId: msg.groupOpenid ?? msg.channelId ?? msg.senderId,
|
|
496
|
+
allowFrom: account.config?.allowFrom,
|
|
497
|
+
groupAllowFrom: account.config?.groupAllowFrom,
|
|
498
|
+
commandsAllowFrom
|
|
499
|
+
}) : resolveSlashCommandAuth({
|
|
500
|
+
senderId: msg.senderId,
|
|
501
|
+
isGroup,
|
|
502
|
+
allowFrom: account.config?.allowFrom,
|
|
503
|
+
groupAllowFrom: account.config?.groupAllowFrom,
|
|
504
|
+
commandsAllowFrom
|
|
505
|
+
});
|
|
707
506
|
const cmdCtx = {
|
|
708
507
|
type: msg.type,
|
|
709
508
|
senderId: msg.senderId,
|
|
@@ -718,13 +517,7 @@ async function trySlashCommand(msg, ctx) {
|
|
|
718
517
|
accountId: account.accountId,
|
|
719
518
|
appId: account.appId,
|
|
720
519
|
accountConfig: account.config,
|
|
721
|
-
commandAuthorized
|
|
722
|
-
senderId: msg.senderId,
|
|
723
|
-
isGroup: msg.type === "group" || msg.type === "guild",
|
|
724
|
-
allowFrom: account.config?.allowFrom,
|
|
725
|
-
groupAllowFrom: account.config?.groupAllowFrom,
|
|
726
|
-
commandsAllowFrom: resolveQQBotCommandsAllowFrom(ctx.cfg)
|
|
727
|
-
}),
|
|
520
|
+
commandAuthorized,
|
|
728
521
|
queueSnapshot: ctx.getQueueSnapshot(peerId)
|
|
729
522
|
};
|
|
730
523
|
try {
|
|
@@ -778,9 +571,8 @@ function loadUsersFromFile() {
|
|
|
778
571
|
usersCache = /* @__PURE__ */ new Map();
|
|
779
572
|
try {
|
|
780
573
|
const knownUsersFile = getKnownUsersFile();
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const users = JSON.parse(data);
|
|
574
|
+
const users = privateFileStoreSync(path.dirname(knownUsersFile)).readJsonIfExists(path.basename(knownUsersFile));
|
|
575
|
+
if (users) {
|
|
784
576
|
for (const user of users) usersCache.set(makeUserKey(user), user);
|
|
785
577
|
debugLog(`[known-users] Loaded ${usersCache.size} users`);
|
|
786
578
|
}
|
|
@@ -801,7 +593,8 @@ function doSaveUsersToFile() {
|
|
|
801
593
|
if (!usersCache || !isDirty) return;
|
|
802
594
|
try {
|
|
803
595
|
ensureDir$1();
|
|
804
|
-
|
|
596
|
+
const filePath = getKnownUsersFile();
|
|
597
|
+
privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), Array.from(usersCache.values()));
|
|
805
598
|
isDirty = false;
|
|
806
599
|
} catch (err) {
|
|
807
600
|
debugError(`[known-users] Failed to save users: ${formatErrorMessage(err)}`);
|
|
@@ -878,13 +671,15 @@ function getCandidateSessionPaths(accountId) {
|
|
|
878
671
|
function loadSession(accountId, expectedAppId) {
|
|
879
672
|
try {
|
|
880
673
|
let filePath = null;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
674
|
+
let state = null;
|
|
675
|
+
for (const candidatePath of getCandidateSessionPaths(accountId)) {
|
|
676
|
+
state = privateFileStoreSync(path.dirname(candidatePath)).readJsonIfExists(path.basename(candidatePath));
|
|
677
|
+
if (state) {
|
|
678
|
+
filePath = candidatePath;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
884
681
|
}
|
|
885
|
-
if (!filePath) return null;
|
|
886
|
-
const data = fs.readFileSync(filePath, "utf-8");
|
|
887
|
-
const state = JSON.parse(data);
|
|
682
|
+
if (!filePath || !state) return null;
|
|
888
683
|
const now = Date.now();
|
|
889
684
|
if (now - state.savedAt > SESSION_EXPIRE_TIME) {
|
|
890
685
|
debugLog(`[session-store] Session expired for ${accountId}, age: ${Math.round((now - state.savedAt) / 1e3)}s`);
|
|
@@ -958,7 +753,7 @@ function doSaveSession(state) {
|
|
|
958
753
|
...state,
|
|
959
754
|
savedAt: Date.now()
|
|
960
755
|
};
|
|
961
|
-
|
|
756
|
+
privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), stateToSave);
|
|
962
757
|
if (legacyPath !== filePath && fs.existsSync(legacyPath)) fs.unlinkSync(legacyPath);
|
|
963
758
|
debugLog(`[session-store] Saved session for ${state.accountId}: sessionId=${state.sessionId}, lastSeq=${state.lastSeq}`);
|
|
964
759
|
} catch (err) {
|
|
@@ -1334,43 +1129,16 @@ function buildGroupQueuedMessage(data, accountId, eventType) {
|
|
|
1334
1129
|
}
|
|
1335
1130
|
//#endregion
|
|
1336
1131
|
//#region extensions/qqbot/src/engine/gateway/message-queue.ts
|
|
1337
|
-
/**
|
|
1338
|
-
* Per-user concurrent message queue.
|
|
1339
|
-
*
|
|
1340
|
-
* Messages are serialized per **peer** (one DM user, one group, one guild
|
|
1341
|
-
* channel) and processed in parallel across peers up to
|
|
1342
|
-
* {@link DEFAULT_MAX_CONCURRENT_USERS}.
|
|
1343
|
-
*
|
|
1344
|
-
* Group-specific enhancements (added when merging from the standalone build):
|
|
1345
|
-
* - Group peers have a larger queue cap ({@link DEFAULT_GROUP_QUEUE_SIZE})
|
|
1346
|
-
* because groups can burst more chatter than a single DM.
|
|
1347
|
-
* - When a group's queue overflows, bot-authored messages are evicted
|
|
1348
|
-
* preferentially so human messages don't get dropped.
|
|
1349
|
-
* - When draining a group peer with more than one queued message, the
|
|
1350
|
-
* non-command messages are **merged** into one logical turn (see
|
|
1351
|
-
* {@link mergeGroupMessages}). Slash commands are always processed
|
|
1352
|
-
* individually to avoid conflating a "/stop" with surrounding chatter.
|
|
1353
|
-
*
|
|
1354
|
-
* The module is self-contained: the only injected dependency is the
|
|
1355
|
-
* logger / abort probe supplied via {@link MessageQueueContext}.
|
|
1356
|
-
*/
|
|
1357
|
-
/** Global cap across all peers. */
|
|
1358
1132
|
const DEFAULT_GLOBAL_QUEUE_SIZE = 1e3;
|
|
1359
|
-
/** Per-DM / per-channel cap. */
|
|
1360
1133
|
const DEFAULT_PER_PEER_QUEUE_SIZE = 20;
|
|
1361
|
-
/** Per-group cap — larger because groups burst more. */
|
|
1362
1134
|
const DEFAULT_GROUP_QUEUE_SIZE = 50;
|
|
1363
|
-
/** Parallel fanout across peers. */
|
|
1364
1135
|
const DEFAULT_MAX_CONCURRENT_USERS = 10;
|
|
1365
|
-
/** Convenience predicate: is this a merged multi-message turn? */
|
|
1366
1136
|
function isMergedTurn(msg) {
|
|
1367
1137
|
return (msg.merge?.count ?? 0) > 1;
|
|
1368
1138
|
}
|
|
1369
|
-
/** Return true when the peer id refers to a group-like conversation. */
|
|
1370
1139
|
function isGroupPeer(peerId) {
|
|
1371
1140
|
return peerId.startsWith("group:") || peerId.startsWith("guild:");
|
|
1372
1141
|
}
|
|
1373
|
-
/** Slash-command test used by {@link drainGroupBatch}. */
|
|
1374
1142
|
function isSlashCommand(msg) {
|
|
1375
1143
|
return (msg.content ?? "").trim().startsWith("/");
|
|
1376
1144
|
}
|
|
@@ -1437,9 +1205,6 @@ function mergeGroupMessages(batch) {
|
|
|
1437
1205
|
}
|
|
1438
1206
|
};
|
|
1439
1207
|
}
|
|
1440
|
-
/**
|
|
1441
|
-
* Create a per-user concurrent queue with built-in group enhancements.
|
|
1442
|
-
*/
|
|
1443
1208
|
function createMessageQueue(ctx) {
|
|
1444
1209
|
const { accountId: _accountId, log } = ctx;
|
|
1445
1210
|
const globalQueueSize = ctx.globalQueueSize ?? DEFAULT_GLOBAL_QUEUE_SIZE;
|
|
@@ -1455,13 +1220,6 @@ function createMessageQueue(ctx) {
|
|
|
1455
1220
|
if (msg.type === "group") return `group:${msg.groupOpenid ?? "unknown"}`;
|
|
1456
1221
|
return `dm:${msg.senderId}`;
|
|
1457
1222
|
};
|
|
1458
|
-
/**
|
|
1459
|
-
* Evict one message from an over-full queue.
|
|
1460
|
-
*
|
|
1461
|
-
* For group peers we prefer to drop a bot-authored message so human
|
|
1462
|
-
* input never gets lost. Falling back to dropping the oldest keeps the
|
|
1463
|
-
* queue bounded when all members are bots.
|
|
1464
|
-
*/
|
|
1465
1223
|
const evictOne = (queue, isGroup) => {
|
|
1466
1224
|
if (isGroup) {
|
|
1467
1225
|
const botIdx = queue.findIndex((m) => m.senderIsBot);
|
|
@@ -1469,7 +1227,6 @@ function createMessageQueue(ctx) {
|
|
|
1469
1227
|
}
|
|
1470
1228
|
return queue.shift();
|
|
1471
1229
|
};
|
|
1472
|
-
/** Run a single message, capturing errors in the log. */
|
|
1473
1230
|
const processOne = async (msg, peerId, label) => {
|
|
1474
1231
|
try {
|
|
1475
1232
|
await handleMessageFnRef(msg);
|
|
@@ -1477,11 +1234,6 @@ function createMessageQueue(ctx) {
|
|
|
1477
1234
|
log?.error(`${label} error for ${peerId}: ${formatErrorMessage(err)}`);
|
|
1478
1235
|
}
|
|
1479
1236
|
};
|
|
1480
|
-
/**
|
|
1481
|
-
* Drain a group's batch:
|
|
1482
|
-
* - slash commands are processed one by one (order preserved);
|
|
1483
|
-
* - the remaining messages are merged into a single turn.
|
|
1484
|
-
*/
|
|
1485
1237
|
const drainGroupBatch = async (batch, peerId) => {
|
|
1486
1238
|
const commands = [];
|
|
1487
1239
|
const normal = [];
|
|
@@ -1497,7 +1249,6 @@ function createMessageQueue(ctx) {
|
|
|
1497
1249
|
await processOne(merged, peerId, `Message processor (merged batch of ${normal.length})`);
|
|
1498
1250
|
}
|
|
1499
1251
|
};
|
|
1500
|
-
/** Process one peer's queue serially. */
|
|
1501
1252
|
const drainUserQueue = async (peerId) => {
|
|
1502
1253
|
if (activeUsers.has(peerId)) return;
|
|
1503
1254
|
if (activeUsers.size >= maxConcurrentUsers) {
|
|
@@ -1740,13 +1491,16 @@ var ReconnectState = class {
|
|
|
1740
1491
|
}
|
|
1741
1492
|
};
|
|
1742
1493
|
//#endregion
|
|
1494
|
+
//#region extensions/qqbot/src/engine/gateway/ws-client.ts
|
|
1495
|
+
async function createQQWSClient(options) {
|
|
1496
|
+
const wsAgent = await resolveAmbientNodeProxyAgent();
|
|
1497
|
+
return new WebSocket(options.gatewayUrl, {
|
|
1498
|
+
headers: { "User-Agent": options.userAgent },
|
|
1499
|
+
...wsAgent ? { agent: wsAgent } : {}
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
//#endregion
|
|
1743
1503
|
//#region extensions/qqbot/src/engine/gateway/gateway-connection.ts
|
|
1744
|
-
/**
|
|
1745
|
-
* GatewayConnection — WebSocket lifecycle, heartbeat, reconnect, and session persistence.
|
|
1746
|
-
*
|
|
1747
|
-
* Encapsulates all connection state as class fields (replaces 11 closure variables).
|
|
1748
|
-
* Event handling and message processing are delegated to injected handlers.
|
|
1749
|
-
*/
|
|
1750
1504
|
var GatewayConnection = class {
|
|
1751
1505
|
constructor(ctx) {
|
|
1752
1506
|
this.isAborted = false;
|
|
@@ -1765,7 +1519,6 @@ var GatewayConnection = class {
|
|
|
1765
1519
|
isAborted: () => this.isAborted
|
|
1766
1520
|
});
|
|
1767
1521
|
}
|
|
1768
|
-
/** Start the connection loop. Resolves when abortSignal fires. */
|
|
1769
1522
|
async start() {
|
|
1770
1523
|
this.restoreSession();
|
|
1771
1524
|
this.registerAbortHandler();
|
|
@@ -1852,14 +1605,22 @@ var GatewayConnection = class {
|
|
|
1852
1605
|
log?.info(`✅ Access token obtained successfully`);
|
|
1853
1606
|
const gatewayUrl = await getGatewayUrl(accessToken, account.appId);
|
|
1854
1607
|
log?.info(`Connecting to ${gatewayUrl}`);
|
|
1855
|
-
const ws =
|
|
1608
|
+
const ws = await createQQWSClient({
|
|
1609
|
+
gatewayUrl,
|
|
1610
|
+
userAgent: getPluginUserAgent()
|
|
1611
|
+
});
|
|
1856
1612
|
this.currentWs = ws;
|
|
1857
1613
|
const slashCtx = {
|
|
1858
1614
|
account,
|
|
1859
1615
|
cfg: this.ctx.cfg,
|
|
1860
1616
|
log,
|
|
1861
1617
|
getMessagePeerId: (msg) => this.msgQueue.getMessagePeerId(msg),
|
|
1862
|
-
getQueueSnapshot: (peerId) => this.msgQueue.getSnapshot(peerId)
|
|
1618
|
+
getQueueSnapshot: (peerId) => this.msgQueue.getSnapshot(peerId),
|
|
1619
|
+
resolveCommandAuthorized: (params) => this.ctx.adapters.access.resolveSlashCommandAuthorization({
|
|
1620
|
+
cfg: this.ctx.cfg,
|
|
1621
|
+
accountId: account.accountId,
|
|
1622
|
+
...params
|
|
1623
|
+
})
|
|
1863
1624
|
};
|
|
1864
1625
|
const trySlashCommandOrEnqueue = async (msg) => {
|
|
1865
1626
|
const result = await trySlashCommand(msg, slashCtx);
|
|
@@ -2026,20 +1787,28 @@ async function transcribeAudio(audioPath, cfg) {
|
|
|
2026
1787
|
if (!sttCfg) return null;
|
|
2027
1788
|
const fileBuffer = fs$1.readFileSync(audioPath);
|
|
2028
1789
|
const fileName = sanitizeFileName(path.basename(audioPath));
|
|
2029
|
-
const mime =
|
|
1790
|
+
const mime = mimeTypeFromFilePath(fileName) ?? "application/octet-stream";
|
|
2030
1791
|
const form = new FormData();
|
|
2031
1792
|
form.append("file", new Blob([fileBuffer], { type: mime }), fileName);
|
|
2032
1793
|
form.append("model", sttCfg.model);
|
|
2033
|
-
const resp = await
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
1794
|
+
const { response: resp, release } = await fetchWithSsrFGuard({
|
|
1795
|
+
url: `${sttCfg.baseUrl}/audio/transcriptions`,
|
|
1796
|
+
auditContext: "qqbot-stt",
|
|
1797
|
+
init: {
|
|
1798
|
+
method: "POST",
|
|
1799
|
+
headers: { Authorization: `Bearer ${sttCfg.apiKey}` },
|
|
1800
|
+
body: form
|
|
1801
|
+
}
|
|
2037
1802
|
});
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
1803
|
+
try {
|
|
1804
|
+
if (!resp.ok) {
|
|
1805
|
+
const detail = await resp.text().catch(() => "");
|
|
1806
|
+
throw new Error(`STT failed (HTTP ${resp.status}): ${detail.slice(0, 300)}`);
|
|
1807
|
+
}
|
|
1808
|
+
return normalizeOptionalString((await resp.json()).text) ?? null;
|
|
1809
|
+
} finally {
|
|
1810
|
+
await release();
|
|
2041
1811
|
}
|
|
2042
|
-
return normalizeOptionalString((await resp.json()).text) ?? null;
|
|
2043
1812
|
}
|
|
2044
1813
|
//#endregion
|
|
2045
1814
|
//#region extensions/qqbot/src/engine/gateway/inbound-attachments.ts
|
|
@@ -2288,7 +2057,6 @@ async function processVoiceAttachment(localPath, audioPath, att, asrReferText, c
|
|
|
2288
2057
|
}
|
|
2289
2058
|
//#endregion
|
|
2290
2059
|
//#region extensions/qqbot/src/engine/gateway/stages/stub-contexts.ts
|
|
2291
|
-
/** Build an {@link InboundContext} with all non-routing fields cleared. */
|
|
2292
2060
|
function emptyInboundContext(fields) {
|
|
2293
2061
|
return {
|
|
2294
2062
|
event: fields.event,
|
|
@@ -2297,30 +2065,12 @@ function emptyInboundContext(fields) {
|
|
|
2297
2065
|
peerId: fields.peerId,
|
|
2298
2066
|
qualifiedTarget: fields.qualifiedTarget,
|
|
2299
2067
|
fromAddress: fields.fromAddress,
|
|
2300
|
-
parsedContent: "",
|
|
2301
|
-
userContent: "",
|
|
2302
|
-
quotePart: "",
|
|
2303
|
-
dynamicCtx: "",
|
|
2304
|
-
userMessage: "",
|
|
2305
2068
|
agentBody: "",
|
|
2306
2069
|
body: "",
|
|
2307
|
-
systemPrompts: [],
|
|
2308
2070
|
groupSystemPrompt: void 0,
|
|
2309
|
-
attachments: {
|
|
2310
|
-
attachmentInfo: "",
|
|
2311
|
-
imageUrls: [],
|
|
2312
|
-
imageMediaTypes: [],
|
|
2313
|
-
voiceAttachmentPaths: [],
|
|
2314
|
-
voiceAttachmentUrls: [],
|
|
2315
|
-
voiceAsrReferTexts: [],
|
|
2316
|
-
voiceTranscripts: [],
|
|
2317
|
-
voiceTranscriptSources: [],
|
|
2318
|
-
attachmentLocalPaths: []
|
|
2319
|
-
},
|
|
2320
2071
|
localMediaPaths: [],
|
|
2321
2072
|
localMediaTypes: [],
|
|
2322
2073
|
remoteMediaUrls: [],
|
|
2323
|
-
remoteMediaTypes: [],
|
|
2324
2074
|
uniqueVoicePaths: [],
|
|
2325
2075
|
uniqueVoiceUrls: [],
|
|
2326
2076
|
uniqueVoiceAsrReferTexts: [],
|
|
@@ -2336,52 +2086,29 @@ function emptyInboundContext(fields) {
|
|
|
2336
2086
|
inputNotifyRefIdx: void 0
|
|
2337
2087
|
};
|
|
2338
2088
|
}
|
|
2339
|
-
/**
|
|
2340
|
-
* Build an {@link InboundContext} that represents a message blocked by
|
|
2341
|
-
* access control (policy denial, allowlist mismatch, etc.).
|
|
2342
|
-
*/
|
|
2343
2089
|
function buildBlockedInboundContext(params) {
|
|
2344
2090
|
return {
|
|
2345
2091
|
...emptyInboundContext(params),
|
|
2346
2092
|
blocked: true,
|
|
2347
|
-
blockReason: params.access.
|
|
2348
|
-
blockReasonCode: params.access.reasonCode,
|
|
2349
|
-
accessDecision: params.access.decision
|
|
2093
|
+
blockReason: params.access.senderAccess.reasonCode,
|
|
2094
|
+
blockReasonCode: params.access.senderAccess.reasonCode,
|
|
2095
|
+
accessDecision: params.access.senderAccess.decision
|
|
2350
2096
|
};
|
|
2351
2097
|
}
|
|
2352
|
-
/**
|
|
2353
|
-
* Build an {@link InboundContext} that represents a message stopped by
|
|
2354
|
-
* the group gate (drop_other_mention, block_unauthorized_command,
|
|
2355
|
-
* skip_no_mention). Any history side-effects have already been applied
|
|
2356
|
-
* by the gate stage.
|
|
2357
|
-
*/
|
|
2358
2098
|
function buildSkippedInboundContext(params) {
|
|
2359
2099
|
return {
|
|
2360
2100
|
...emptyInboundContext(params),
|
|
2361
2101
|
group: params.group,
|
|
2362
2102
|
skipped: true,
|
|
2363
2103
|
skipReason: params.skipReason,
|
|
2364
|
-
accessDecision: params.access.decision,
|
|
2104
|
+
accessDecision: params.access.senderAccess.decision,
|
|
2365
2105
|
typing: params.typing,
|
|
2366
2106
|
inputNotifyRefIdx: params.inputNotifyRefIdx
|
|
2367
2107
|
};
|
|
2368
2108
|
}
|
|
2369
2109
|
//#endregion
|
|
2370
2110
|
//#region extensions/qqbot/src/engine/gateway/stages/access-stage.ts
|
|
2371
|
-
|
|
2372
|
-
* Access stage — resolves routing target + runs access control.
|
|
2373
|
-
*
|
|
2374
|
-
* Split from the pipeline so it is trivially unit-testable: given a raw
|
|
2375
|
-
* event and the runtime's routing info, the stage returns either:
|
|
2376
|
-
* - `{ kind: "allow", ... }` — proceed through the rest of the pipeline
|
|
2377
|
-
* - `{ kind: "block", context }` — short-circuit; the caller returns
|
|
2378
|
-
* `context` directly to its own caller.
|
|
2379
|
-
*/
|
|
2380
|
-
/**
|
|
2381
|
-
* Resolve the routing target, walk the access policy, and decide whether
|
|
2382
|
-
* the inbound message should proceed to the rest of the pipeline.
|
|
2383
|
-
*/
|
|
2384
|
-
function runAccessStage(event, deps) {
|
|
2111
|
+
async function runAccessStage(event, deps) {
|
|
2385
2112
|
const { account, cfg, runtime, log } = deps;
|
|
2386
2113
|
const isGroupChat = event.type === "guild" || event.type === "group";
|
|
2387
2114
|
const peerId = resolvePeerId(event, isGroupChat);
|
|
@@ -2395,16 +2122,19 @@ function runAccessStage(event, deps) {
|
|
|
2395
2122
|
id: peerId
|
|
2396
2123
|
}
|
|
2397
2124
|
});
|
|
2398
|
-
const access =
|
|
2125
|
+
const access = await deps.adapters.access.resolveInboundAccess({
|
|
2126
|
+
cfg,
|
|
2127
|
+
accountId: account.accountId,
|
|
2399
2128
|
isGroup: isGroupChat,
|
|
2400
2129
|
senderId: event.senderId,
|
|
2130
|
+
conversationId: peerId,
|
|
2401
2131
|
allowFrom: account.config?.allowFrom,
|
|
2402
2132
|
groupAllowFrom: account.config?.groupAllowFrom,
|
|
2403
2133
|
dmPolicy: account.config?.dmPolicy,
|
|
2404
2134
|
groupPolicy: account.config?.groupPolicy
|
|
2405
2135
|
});
|
|
2406
|
-
if (access.decision !== "allow") {
|
|
2407
|
-
log?.info(`Blocked qqbot inbound: decision=${access.decision} reasonCode=${access.reasonCode}
|
|
2136
|
+
if (access.senderAccess.decision !== "allow") {
|
|
2137
|
+
log?.info(`Blocked qqbot inbound: decision=${access.senderAccess.decision} reasonCode=${access.senderAccess.reasonCode} senderId=${event.senderId} accountId=${account.accountId} isGroup=${isGroupChat}`);
|
|
2408
2138
|
return {
|
|
2409
2139
|
kind: "block",
|
|
2410
2140
|
context: buildBlockedInboundContext({
|
|
@@ -2438,28 +2168,9 @@ function buildQualifiedTarget(event, isGroupChat) {
|
|
|
2438
2168
|
if (isGroupChat) return event.type === "guild" ? `qqbot:channel:${event.channelId}` : `qqbot:group:${event.groupOpenid}`;
|
|
2439
2169
|
return event.type === "dm" ? `qqbot:dm:${event.guildId}` : `qqbot:c2c:${event.senderId}`;
|
|
2440
2170
|
}
|
|
2441
|
-
/**
|
|
2442
|
-
* Decide whether the access decision permits running text-based control
|
|
2443
|
-
* commands. Placed in the access stage because the rule is an
|
|
2444
|
-
* access-policy derivative, not a gate derivative.
|
|
2445
|
-
*/
|
|
2446
|
-
function resolveCommandAuthorized(access) {
|
|
2447
|
-
return access.reasonCode === "dm_policy_open" || access.reasonCode === "dm_policy_allowlisted" || access.reasonCode === "group_policy_allowed" && access.effectiveGroupAllowFrom.length > 0 && access.groupPolicy === "allowlist";
|
|
2448
|
-
}
|
|
2449
2171
|
//#endregion
|
|
2450
2172
|
//#region extensions/qqbot/src/engine/group/mention.ts
|
|
2451
|
-
/** Regex detecting `<@openid>` / `<@!openid>` mention tags in raw content. */
|
|
2452
2173
|
const MENTION_TAG_RE = /<@!?\w+>/;
|
|
2453
|
-
/**
|
|
2454
|
-
* Detect whether the inbound message explicitly targets the bot.
|
|
2455
|
-
*
|
|
2456
|
-
* Priority order:
|
|
2457
|
-
* 1. `mentions[].is_you === true` (most reliable)
|
|
2458
|
-
* 2. `eventType === "GROUP_AT_MESSAGE_CREATE"` (QQ-level @bot event)
|
|
2459
|
-
* 3. regex match on any of `mentionPatterns` (fallback, e.g. "@bot-name")
|
|
2460
|
-
*
|
|
2461
|
-
* Returns `false` for direct messages or when no signal is found.
|
|
2462
|
-
*/
|
|
2463
2174
|
function detectWasMentioned(input) {
|
|
2464
2175
|
const { eventType, mentions, content, mentionPatterns } = input;
|
|
2465
2176
|
if (mentions?.some((m) => m.is_you)) return true;
|
|
@@ -2472,30 +2183,11 @@ function detectWasMentioned(input) {
|
|
|
2472
2183
|
}
|
|
2473
2184
|
return false;
|
|
2474
2185
|
}
|
|
2475
|
-
/**
|
|
2476
|
-
* Report whether the message contains **any** @mention (not necessarily @bot).
|
|
2477
|
-
*
|
|
2478
|
-
* Used by the gating layer to decide whether to bypass mention requirements
|
|
2479
|
-
* for control commands. A control command like `/stop` that also @-s another
|
|
2480
|
-
* user should NOT bypass the mention gate — the `@other-user` prefix is a
|
|
2481
|
-
* strong signal that the command wasn't addressed to the bot.
|
|
2482
|
-
*/
|
|
2483
2186
|
function hasAnyMention(input) {
|
|
2484
2187
|
if (input.mentions && input.mentions.length > 0) return true;
|
|
2485
2188
|
if (input.content && MENTION_TAG_RE.test(input.content)) return true;
|
|
2486
2189
|
return false;
|
|
2487
2190
|
}
|
|
2488
|
-
/**
|
|
2489
|
-
* Clean up `<@openid>` mention tags in raw QQ group content.
|
|
2490
|
-
*
|
|
2491
|
-
* - For the bot's own mention (`is_you === true`): the tag is removed
|
|
2492
|
-
* outright so prompts don't contain visible `<@BOTID>` garbage.
|
|
2493
|
-
* - For other mentioned users: the tag is replaced with `@nickname` (or
|
|
2494
|
-
* `@username`) for readability. Entries without a display name are left
|
|
2495
|
-
* as-is (rare in practice).
|
|
2496
|
-
*
|
|
2497
|
-
* Returns the original text unchanged when `text` or `mentions` is empty.
|
|
2498
|
-
*/
|
|
2499
2191
|
function stripMentionText(text, mentions) {
|
|
2500
2192
|
if (!text || !mentions?.length) return text;
|
|
2501
2193
|
let cleaned = text;
|
|
@@ -2511,7 +2203,6 @@ function stripMentionText(text, mentions) {
|
|
|
2511
2203
|
}
|
|
2512
2204
|
return cleaned;
|
|
2513
2205
|
}
|
|
2514
|
-
/** Escape characters that carry regex meaning. */
|
|
2515
2206
|
function escapeRegex$1(str) {
|
|
2516
2207
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2517
2208
|
}
|
|
@@ -2821,7 +2512,6 @@ const DEFAULT_GROUP_CONFIG = {
|
|
|
2821
2512
|
name: "",
|
|
2822
2513
|
historyLimit: 50
|
|
2823
2514
|
};
|
|
2824
|
-
/** Read a named account's raw `groups` map from an OpenClawConfig. */
|
|
2825
2515
|
function readGroupsMap(cfg, accountId) {
|
|
2826
2516
|
const groups = asOptionalObjectRecord(resolveAccountBase(cfg, accountId).config.groups);
|
|
2827
2517
|
if (!groups) return {};
|
|
@@ -2849,12 +2539,6 @@ function readHistoryLimit(obj, key) {
|
|
|
2849
2539
|
if (typeof v !== "number" || !Number.isFinite(v)) return;
|
|
2850
2540
|
return Math.max(0, Math.floor(v));
|
|
2851
2541
|
}
|
|
2852
|
-
/**
|
|
2853
|
-
* Resolve per-group configuration with `specific > "*" > default` precedence.
|
|
2854
|
-
*
|
|
2855
|
-
* When `groupOpenid` is not provided, only the wildcard/default values are
|
|
2856
|
-
* returned. This lets callers query the "default" behaviour for new groups.
|
|
2857
|
-
*/
|
|
2858
2542
|
function resolveGroupConfig(cfg, groupOpenid, accountId) {
|
|
2859
2543
|
const groups = readGroupsMap(cfg, accountId);
|
|
2860
2544
|
const wildcard = groups["*"] ?? {};
|
|
@@ -2868,13 +2552,6 @@ function resolveGroupConfig(cfg, groupOpenid, accountId) {
|
|
|
2868
2552
|
historyLimit: readHistoryLimit(specific, "historyLimit") ?? readHistoryLimit(wildcard, "historyLimit") ?? DEFAULT_GROUP_CONFIG.historyLimit
|
|
2869
2553
|
};
|
|
2870
2554
|
}
|
|
2871
|
-
/**
|
|
2872
|
-
* Resolve all per-inbound group-related settings in one pass.
|
|
2873
|
-
*
|
|
2874
|
-
* Prefer this over calling `resolveHistoryLimit` / `resolveRequireMention`
|
|
2875
|
-
* / etc. individually in hot paths — each of those currently re-walks
|
|
2876
|
-
* the config tree on its own.
|
|
2877
|
-
*/
|
|
2878
2555
|
function resolveGroupSettings(params) {
|
|
2879
2556
|
const config = resolveGroupConfig(params.cfg, params.groupOpenid, params.accountId);
|
|
2880
2557
|
return {
|
|
@@ -2883,12 +2560,6 @@ function resolveGroupSettings(params) {
|
|
|
2883
2560
|
mentionPatterns: resolveMentionPatterns(params.cfg, params.agentId)
|
|
2884
2561
|
};
|
|
2885
2562
|
}
|
|
2886
|
-
/**
|
|
2887
|
-
* Resolve mentionPatterns with `agent > global > []` precedence.
|
|
2888
|
-
*
|
|
2889
|
-
* Mirrors the framework's `messages.groupChat.mentionPatterns` / per-agent
|
|
2890
|
-
* `agents.list[].groupChat.mentionPatterns` chain.
|
|
2891
|
-
*/
|
|
2892
2563
|
function resolveMentionPatterns(cfg, agentId) {
|
|
2893
2564
|
if (agentId) {
|
|
2894
2565
|
const agents = asOptionalObjectRecord(cfg.agents);
|
|
@@ -2907,34 +2578,6 @@ function resolveMentionPatterns(cfg, agentId) {
|
|
|
2907
2578
|
}
|
|
2908
2579
|
//#endregion
|
|
2909
2580
|
//#region extensions/qqbot/src/engine/gateway/stages/group-gate-stage.ts
|
|
2910
|
-
/**
|
|
2911
|
-
* Group-gate stage — for `type === "group"` inbound events, decide
|
|
2912
|
-
* whether the message should pass to AI dispatch or be intercepted.
|
|
2913
|
-
*
|
|
2914
|
-
* Three possible outcomes:
|
|
2915
|
-
* - `{ kind: "pass", groupInfo }` — continue the pipeline
|
|
2916
|
-
* - `{ kind: "skip", groupInfo, skipReason }` — buffered to history
|
|
2917
|
-
* (if applicable) and short-circuit
|
|
2918
|
-
* - No group info at all — returned when the event isn't a group event
|
|
2919
|
-
* (caller should treat as a straight pass-through)
|
|
2920
|
-
*
|
|
2921
|
-
* Consolidates the control-command auth check, session-store
|
|
2922
|
-
* activation override, mention detection, and the unified
|
|
2923
|
-
* {@link resolveGroupMessageGate} call. Delegates all pure logic to
|
|
2924
|
-
* existing `engine/group/*` modules so this stage remains a thin
|
|
2925
|
-
* orchestrator.
|
|
2926
|
-
*/
|
|
2927
|
-
/**
|
|
2928
|
-
* Run the group-gate stage.
|
|
2929
|
-
*
|
|
2930
|
-
* Precondition: `event.type === "group"` && `event.groupOpenid` is set.
|
|
2931
|
-
* The caller (pipeline) enforces this; the stage doesn't re-check.
|
|
2932
|
-
*
|
|
2933
|
-
* On `skip` outcomes the stage records the message into the group's
|
|
2934
|
-
* history buffer when the skip reason is one that should preserve
|
|
2935
|
-
* context (drop / skip_no_mention), then returns. `block` skip
|
|
2936
|
-
* reasons do NOT write history — they are silent rejects.
|
|
2937
|
-
*/
|
|
2938
2581
|
function runGroupGateStage(input) {
|
|
2939
2582
|
const { event, deps, accountId, agentId, sessionKey, userContent, processedAttachments } = input;
|
|
2940
2583
|
const groupOpenid = event.groupOpenid;
|
|
@@ -2971,7 +2614,7 @@ function runGroupGateStage(input) {
|
|
|
2971
2614
|
});
|
|
2972
2615
|
const content = (event.content ?? "").trim();
|
|
2973
2616
|
const isControlCommand = Boolean(deps.isControlCommand?.(content));
|
|
2974
|
-
const commandAuthorized = deps.allowTextCommands !== false &&
|
|
2617
|
+
const commandAuthorized = deps.allowTextCommands !== false && input.access.commandAccess.authorized;
|
|
2975
2618
|
const gate = resolveGateWithPort({
|
|
2976
2619
|
mentionGatePort: deps.adapters.mentionGate,
|
|
2977
2620
|
ignoreOtherMentions,
|
|
@@ -3021,12 +2664,6 @@ function runGroupGateStage(input) {
|
|
|
3021
2664
|
skipReason: gate.action
|
|
3022
2665
|
};
|
|
3023
2666
|
}
|
|
3024
|
-
/**
|
|
3025
|
-
* Resolve the gate using the SDK MentionGatePort adapter.
|
|
3026
|
-
*
|
|
3027
|
-
* Layer 1 (ignoreOtherMentions) is QQ-specific and handled here.
|
|
3028
|
-
* Layers 2+3 delegate to the SDK's `resolveInboundMentionDecision`.
|
|
3029
|
-
*/
|
|
3030
2667
|
function resolveGateWithPort(params) {
|
|
3031
2668
|
if (params.ignoreOtherMentions && params.hasAnyMention && !params.wasMentioned && !params.implicitMention) return {
|
|
3032
2669
|
action: "drop_other_mention",
|
|
@@ -3064,15 +2701,6 @@ function resolveGateWithPort(params) {
|
|
|
3064
2701
|
shouldBypassMention: decision.shouldBypassMention
|
|
3065
2702
|
};
|
|
3066
2703
|
}
|
|
3067
|
-
/**
|
|
3068
|
-
* Test whether the sender is on the DM `allowFrom` list.
|
|
3069
|
-
*/
|
|
3070
|
-
function isSenderAllowedForCommands(senderId, deps) {
|
|
3071
|
-
const raw = deps.account.config?.allowFrom;
|
|
3072
|
-
if (!Array.isArray(raw) || raw.length === 0) return true;
|
|
3073
|
-
const normalized = normalizeQQBotAllowFrom(raw);
|
|
3074
|
-
return createQQBotSenderMatcher(senderId)(normalized);
|
|
3075
|
-
}
|
|
3076
2704
|
function recordGroupHistory(params) {
|
|
3077
2705
|
const { historyMap, groupOpenid, historyLimit, event, userContent, historyPort, localPaths } = params;
|
|
3078
2706
|
if (!historyMap || historyLimit <= 0) return;
|
|
@@ -3240,15 +2868,9 @@ function writeRefIndex(params) {
|
|
|
3240
2868
|
}
|
|
3241
2869
|
//#endregion
|
|
3242
2870
|
//#region extensions/qqbot/src/engine/gateway/inbound-pipeline.ts
|
|
3243
|
-
/**
|
|
3244
|
-
* Process a raw queued message through the full inbound pipeline.
|
|
3245
|
-
*
|
|
3246
|
-
* Returns an {@link InboundContext} with `blocked` / `skipped` set when
|
|
3247
|
-
* the message should not reach the AI dispatcher.
|
|
3248
|
-
*/
|
|
3249
2871
|
async function buildInboundContext(event, deps) {
|
|
3250
2872
|
const { account, log } = deps;
|
|
3251
|
-
const accessResult = runAccessStage(event, deps);
|
|
2873
|
+
const accessResult = await runAccessStage(event, deps);
|
|
3252
2874
|
if (accessResult.kind === "block") return accessResult.context;
|
|
3253
2875
|
const { isGroupChat, peerId, qualifiedTarget, fromAddress, route, access } = accessResult;
|
|
3254
2876
|
const typingPromise = deps.startTyping(event);
|
|
@@ -3280,7 +2902,8 @@ async function buildInboundContext(event, deps) {
|
|
|
3280
2902
|
agentId: route.agentId,
|
|
3281
2903
|
sessionKey: route.sessionKey,
|
|
3282
2904
|
userContent,
|
|
3283
|
-
processedAttachments: processed
|
|
2905
|
+
processedAttachments: processed,
|
|
2906
|
+
access
|
|
3284
2907
|
});
|
|
3285
2908
|
if (gateOutcome.kind === "skip") {
|
|
3286
2909
|
typingResult.keepAlive?.stop();
|
|
@@ -3315,26 +2938,6 @@ async function buildInboundContext(event, deps) {
|
|
|
3315
2938
|
uniqueVoiceUrls: media.uniqueVoiceUrls,
|
|
3316
2939
|
uniqueVoiceAsrReferTexts: media.uniqueVoiceAsrReferTexts
|
|
3317
2940
|
});
|
|
3318
|
-
const userMessage = buildUserMessage({
|
|
3319
|
-
event,
|
|
3320
|
-
userContent,
|
|
3321
|
-
quotePart,
|
|
3322
|
-
isGroupChat,
|
|
3323
|
-
groupInfo
|
|
3324
|
-
});
|
|
3325
|
-
const agentBody = buildAgentBody({
|
|
3326
|
-
event,
|
|
3327
|
-
userContent,
|
|
3328
|
-
userMessage,
|
|
3329
|
-
dynamicCtx,
|
|
3330
|
-
isGroupChat,
|
|
3331
|
-
groupInfo,
|
|
3332
|
-
deps
|
|
3333
|
-
});
|
|
3334
|
-
const systemPrompts = [];
|
|
3335
|
-
if (account.systemPrompt) systemPrompts.push(account.systemPrompt);
|
|
3336
|
-
const groupSystemPrompt = buildGroupSystemPrompt(systemPrompts.length > 0 ? systemPrompts.join("\n") : "", groupInfo);
|
|
3337
|
-
const commandAuthorized = resolveCommandAuthorized(access);
|
|
3338
2941
|
return {
|
|
3339
2942
|
event,
|
|
3340
2943
|
route,
|
|
@@ -3342,20 +2945,26 @@ async function buildInboundContext(event, deps) {
|
|
|
3342
2945
|
peerId,
|
|
3343
2946
|
qualifiedTarget,
|
|
3344
2947
|
fromAddress,
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
2948
|
+
agentBody: buildAgentBody({
|
|
2949
|
+
event,
|
|
2950
|
+
userContent,
|
|
2951
|
+
userMessage: buildUserMessage({
|
|
2952
|
+
event,
|
|
2953
|
+
userContent,
|
|
2954
|
+
quotePart,
|
|
2955
|
+
isGroupChat,
|
|
2956
|
+
groupInfo
|
|
2957
|
+
}),
|
|
2958
|
+
dynamicCtx,
|
|
2959
|
+
isGroupChat,
|
|
2960
|
+
groupInfo,
|
|
2961
|
+
deps
|
|
2962
|
+
}),
|
|
3351
2963
|
body,
|
|
3352
|
-
|
|
3353
|
-
groupSystemPrompt,
|
|
3354
|
-
attachments: processed,
|
|
2964
|
+
groupSystemPrompt: buildGroupSystemPrompt(account.systemPrompt ?? "", groupInfo),
|
|
3355
2965
|
localMediaPaths: media.localMediaPaths,
|
|
3356
2966
|
localMediaTypes: media.localMediaTypes,
|
|
3357
2967
|
remoteMediaUrls: media.remoteMediaUrls,
|
|
3358
|
-
remoteMediaTypes: media.remoteMediaTypes,
|
|
3359
2968
|
uniqueVoicePaths: media.uniqueVoicePaths,
|
|
3360
2969
|
uniqueVoiceUrls: media.uniqueVoiceUrls,
|
|
3361
2970
|
uniqueVoiceAsrReferTexts: media.uniqueVoiceAsrReferTexts,
|
|
@@ -3363,19 +2972,15 @@ async function buildInboundContext(event, deps) {
|
|
|
3363
2972
|
hasAsrReferFallback: media.hasAsrReferFallback,
|
|
3364
2973
|
voiceTranscriptSources: media.voiceTranscriptSources,
|
|
3365
2974
|
replyTo,
|
|
3366
|
-
commandAuthorized,
|
|
2975
|
+
commandAuthorized: access.commandAccess.authorized,
|
|
3367
2976
|
group: groupInfo,
|
|
3368
2977
|
blocked: false,
|
|
3369
2978
|
skipped: false,
|
|
3370
|
-
accessDecision: access.decision,
|
|
2979
|
+
accessDecision: access.senderAccess.decision,
|
|
3371
2980
|
typing: { keepAlive: typingResult.keepAlive },
|
|
3372
2981
|
inputNotifyRefIdx: typingResult.refIdx
|
|
3373
2982
|
};
|
|
3374
2983
|
}
|
|
3375
|
-
/**
|
|
3376
|
-
* Clear a group's pending history buffer. Exposed so the gateway can
|
|
3377
|
-
* call it in its `finally` block after a reply attempt.
|
|
3378
|
-
*/
|
|
3379
2984
|
function clearGroupPendingHistory(params) {
|
|
3380
2985
|
if (!params.historyMap || !params.groupOpenid) return;
|
|
3381
2986
|
params.historyPort.clearPendingHistory({
|
|
@@ -3385,6 +2990,20 @@ function clearGroupPendingHistory(params) {
|
|
|
3385
2990
|
});
|
|
3386
2991
|
}
|
|
3387
2992
|
//#endregion
|
|
2993
|
+
//#region extensions/qqbot/src/engine/access/resolve-policy.ts
|
|
2994
|
+
function hasRealRestriction(list) {
|
|
2995
|
+
if (!list || list.length === 0) return false;
|
|
2996
|
+
return !list.every((entry) => String(entry).trim() === "*");
|
|
2997
|
+
}
|
|
2998
|
+
function resolveQQBotEffectivePolicies(input) {
|
|
2999
|
+
const allowFromRestricted = hasRealRestriction(input.allowFrom);
|
|
3000
|
+
const groupAllowFromRestricted = hasRealRestriction(input.groupAllowFrom);
|
|
3001
|
+
return {
|
|
3002
|
+
dmPolicy: input.dmPolicy ?? (allowFromRestricted ? "allowlist" : "open"),
|
|
3003
|
+
groupPolicy: input.groupPolicy ?? (groupAllowFromRestricted || allowFromRestricted ? "allowlist" : "open")
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
//#endregion
|
|
3388
3007
|
//#region extensions/qqbot/src/engine/gateway/interaction-handler.ts
|
|
3389
3008
|
/**
|
|
3390
3009
|
* INTERACTION_CREATE event handler.
|
|
@@ -5552,8 +5171,13 @@ function stripAmrHeader(buf) {
|
|
|
5552
5171
|
}
|
|
5553
5172
|
/** Convert a SILK or AMR voice file to WAV format. */
|
|
5554
5173
|
async function convertSilkToWav(inputPath, outputDir) {
|
|
5555
|
-
|
|
5556
|
-
|
|
5174
|
+
let fileBuf;
|
|
5175
|
+
try {
|
|
5176
|
+
fileBuf = readRegularFileSync({ filePath: inputPath }).buffer;
|
|
5177
|
+
} catch {
|
|
5178
|
+
return null;
|
|
5179
|
+
}
|
|
5180
|
+
const strippedBuf = stripAmrHeader(fileBuf);
|
|
5557
5181
|
const rawData = new Uint8Array(strippedBuf.buffer, strippedBuf.byteOffset, strippedBuf.byteLength);
|
|
5558
5182
|
const silk = await loadSilkWasm();
|
|
5559
5183
|
if (!silk || !silk.isSilk(rawData)) return null;
|
|
@@ -5642,8 +5266,12 @@ function normalizeFormats(formats) {
|
|
|
5642
5266
|
* Attempts conversion via direct QQ-native upload → WASM decoders → null fallback chain.
|
|
5643
5267
|
*/
|
|
5644
5268
|
async function audioFileToSilkBase64(filePath, directUploadFormats) {
|
|
5645
|
-
|
|
5646
|
-
|
|
5269
|
+
let buf;
|
|
5270
|
+
try {
|
|
5271
|
+
buf = readRegularFileSync({ filePath }).buffer;
|
|
5272
|
+
} catch {
|
|
5273
|
+
return null;
|
|
5274
|
+
}
|
|
5647
5275
|
if (buf.length === 0) {
|
|
5648
5276
|
debugError(`[audio-convert] file is empty: ${filePath}`);
|
|
5649
5277
|
return null;
|
|
@@ -5902,7 +5530,7 @@ async function dispatchOutbound(inbound, deps) {
|
|
|
5902
5530
|
};
|
|
5903
5531
|
const sendWithRetry = (sendFn) => sendWithTokenRetry(account.appId, account.clientSecret, sendFn, log, account.accountId);
|
|
5904
5532
|
const sendErrorMessage = (errorText) => sendErrorToTarget(replyCtx, errorText);
|
|
5905
|
-
const ctxPayload = buildCtxPayload(inbound, runtime);
|
|
5533
|
+
const ctxPayload = buildCtxPayload(inbound, runtime, cfg);
|
|
5906
5534
|
let hasResponse = false;
|
|
5907
5535
|
let hasBlockResponse = false;
|
|
5908
5536
|
let toolDeliverCount = 0;
|
|
@@ -6186,8 +5814,14 @@ async function dispatchOutbound(inbound, deps) {
|
|
|
6186
5814
|
}
|
|
6187
5815
|
}
|
|
6188
5816
|
}
|
|
6189
|
-
function
|
|
5817
|
+
function resolveCommandSource(inbound, runtime, cfg) {
|
|
5818
|
+
const commandBody = inbound.event.content;
|
|
5819
|
+
if (!runtime.channel.commands?.isControlCommandMessage?.(commandBody, cfg)) return;
|
|
5820
|
+
return "text";
|
|
5821
|
+
}
|
|
5822
|
+
function buildCtxPayload(inbound, runtime, cfg) {
|
|
6190
5823
|
const { event } = inbound;
|
|
5824
|
+
const commandSource = resolveCommandSource(inbound, runtime, cfg);
|
|
6191
5825
|
return runtime.channel.reply.finalizeInboundContext({
|
|
6192
5826
|
Body: inbound.body,
|
|
6193
5827
|
BodyForAgent: inbound.agentBody,
|
|
@@ -6217,6 +5851,7 @@ function buildCtxPayload(inbound, runtime) {
|
|
|
6217
5851
|
QQVoiceAsrReferTexts: inbound.uniqueVoiceAsrReferTexts,
|
|
6218
5852
|
QQVoiceInputStrategy: "prefer_audio_stt_then_asr_fallback",
|
|
6219
5853
|
CommandAuthorized: inbound.commandAuthorized,
|
|
5854
|
+
...commandSource ? { CommandSource: commandSource } : {},
|
|
6220
5855
|
...inbound.voiceMediaTypes.length > 0 ? {
|
|
6221
5856
|
MediaTypes: inbound.voiceMediaTypes,
|
|
6222
5857
|
MediaType: inbound.voiceMediaTypes[0]
|
|
@@ -6297,22 +5932,6 @@ var TypingKeepAlive = class {
|
|
|
6297
5932
|
};
|
|
6298
5933
|
//#endregion
|
|
6299
5934
|
//#region extensions/qqbot/src/engine/gateway/gateway.ts
|
|
6300
|
-
/**
|
|
6301
|
-
* Core gateway entry point — thin shell that wires together:
|
|
6302
|
-
*
|
|
6303
|
-
* - GatewayConnection: WebSocket lifecycle, heartbeat, reconnect
|
|
6304
|
-
* - buildInboundContext: content building, attachments, quote resolution
|
|
6305
|
-
* - dispatchOutbound: AI dispatch, deliver callbacks, timeouts
|
|
6306
|
-
*
|
|
6307
|
-
* The only responsibilities of this file are:
|
|
6308
|
-
* 1. Initialize adapters from EngineAdapters
|
|
6309
|
-
* 2. Initialize API config + refIdx cache hook
|
|
6310
|
-
* 3. Create the message handler (inbound → outbound pipeline)
|
|
6311
|
-
* 4. Start GatewayConnection
|
|
6312
|
-
*/
|
|
6313
|
-
/**
|
|
6314
|
-
* Start the Gateway WebSocket connection with automatic reconnect support.
|
|
6315
|
-
*/
|
|
6316
5935
|
async function startGateway$1(ctx) {
|
|
6317
5936
|
const { account, log, runtime, adapters } = ctx;
|
|
6318
5937
|
setOutboundAudioPort(adapters.outboundAudio);
|
|
@@ -6435,6 +6054,7 @@ async function startGateway$1(ctx) {
|
|
|
6435
6054
|
cfg: ctx.cfg,
|
|
6436
6055
|
log,
|
|
6437
6056
|
runtime,
|
|
6057
|
+
adapters,
|
|
6438
6058
|
onReady: ctx.onReady,
|
|
6439
6059
|
onResumed: ctx.onResumed,
|
|
6440
6060
|
onError: ctx.onError,
|
|
@@ -6572,24 +6192,14 @@ function readQQBotVersionFromManifest(manifestPath) {
|
|
|
6572
6192
|
}
|
|
6573
6193
|
//#endregion
|
|
6574
6194
|
//#region extensions/qqbot/src/bridge/sdk-adapter.ts
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
*
|
|
6579
|
-
|
|
6580
|
-
* `openclaw/plugin-sdk/*`. The engine layer stays zero-SDK-dependency;
|
|
6581
|
-
* only the bridge layer couples to the framework.
|
|
6582
|
-
*/
|
|
6195
|
+
const qqbotIngressIdentity = defineStableChannelIngressIdentity({
|
|
6196
|
+
key: "sender-id",
|
|
6197
|
+
normalize: normalizeQQBotSenderId,
|
|
6198
|
+
isWildcardEntry: (entry) => normalizeQQBotSenderId(entry) === "*"
|
|
6199
|
+
});
|
|
6583
6200
|
function asSdkMap(map) {
|
|
6584
6201
|
return map;
|
|
6585
6202
|
}
|
|
6586
|
-
/**
|
|
6587
|
-
* History adapter backed by SDK `reply-history`.
|
|
6588
|
-
*
|
|
6589
|
-
* Delegates record/build/clear to the SDK's shared implementation so
|
|
6590
|
-
* the engine benefits from SDK improvements (e.g. future visibility
|
|
6591
|
-
* filtering) without code duplication.
|
|
6592
|
-
*/
|
|
6593
6203
|
function createSdkHistoryAdapter() {
|
|
6594
6204
|
return {
|
|
6595
6205
|
recordPendingHistoryEntry(params) {
|
|
@@ -6619,38 +6229,69 @@ function createSdkHistoryAdapter() {
|
|
|
6619
6229
|
}
|
|
6620
6230
|
};
|
|
6621
6231
|
}
|
|
6622
|
-
/**
|
|
6623
|
-
* MentionGate adapter backed by SDK `channel-mention-gating`.
|
|
6624
|
-
*
|
|
6625
|
-
* Maps the engine's mention facts/policy to the SDK's
|
|
6626
|
-
* `resolveInboundMentionDecision` call, normalizing the implicit
|
|
6627
|
-
* mention boolean into the SDK's typed `ImplicitMentionKind[]`.
|
|
6628
|
-
*/
|
|
6629
6232
|
function createSdkMentionGateAdapter() {
|
|
6630
6233
|
return { resolveInboundMentionDecision(params) {
|
|
6631
|
-
|
|
6632
|
-
facts: {
|
|
6633
|
-
canDetectMention: params.facts.canDetectMention,
|
|
6634
|
-
wasMentioned: params.facts.wasMentioned,
|
|
6635
|
-
hasAnyMention: params.facts.hasAnyMention,
|
|
6636
|
-
implicitMentionKinds: params.facts.implicitMentionKinds ?? implicitMentionKindWhen("reply_to_bot", false)
|
|
6637
|
-
},
|
|
6638
|
-
policy: {
|
|
6639
|
-
isGroup: params.policy.isGroup,
|
|
6640
|
-
requireMention: params.policy.requireMention,
|
|
6641
|
-
allowTextCommands: params.policy.allowTextCommands,
|
|
6642
|
-
hasControlCommand: params.policy.hasControlCommand,
|
|
6643
|
-
commandAuthorized: params.policy.commandAuthorized
|
|
6644
|
-
}
|
|
6645
|
-
});
|
|
6646
|
-
return {
|
|
6647
|
-
effectiveWasMentioned: result.effectiveWasMentioned,
|
|
6648
|
-
shouldSkip: result.shouldSkip,
|
|
6649
|
-
shouldBypassMention: result.shouldBypassMention,
|
|
6650
|
-
implicitMention: result.implicitMention
|
|
6651
|
-
};
|
|
6234
|
+
return resolveInboundMentionDecision(params);
|
|
6652
6235
|
} };
|
|
6653
6236
|
}
|
|
6237
|
+
function createSdkAccessAdapter() {
|
|
6238
|
+
return {
|
|
6239
|
+
async resolveInboundAccess(input) {
|
|
6240
|
+
const { dmPolicy, groupPolicy } = resolveQQBotEffectivePolicies(input);
|
|
6241
|
+
const rawGroupAllowFrom = input.groupAllowFrom && input.groupAllowFrom.length > 0 ? input.groupAllowFrom : input.allowFrom ?? [];
|
|
6242
|
+
const normalizedAllowFrom = normalizeQQBotAllowFrom(input.allowFrom);
|
|
6243
|
+
const dmAllowFromForIngress = dmPolicy === "open" && normalizedAllowFrom.length === 0 ? ["*"] : input.allowFrom ?? [];
|
|
6244
|
+
const commandOwnerAllowFrom = input.isGroup ? [] : input.allowFrom && input.allowFrom.length > 0 ? input.allowFrom : ["*"];
|
|
6245
|
+
return await createChannelIngressResolver({
|
|
6246
|
+
channelId: "qqbot",
|
|
6247
|
+
accountId: input.accountId,
|
|
6248
|
+
identity: qqbotIngressIdentity,
|
|
6249
|
+
cfg: input.cfg
|
|
6250
|
+
}).message({
|
|
6251
|
+
subject: { stableId: input.senderId },
|
|
6252
|
+
conversation: {
|
|
6253
|
+
kind: input.isGroup ? "group" : "direct",
|
|
6254
|
+
id: input.conversationId
|
|
6255
|
+
},
|
|
6256
|
+
event: { mayPair: false },
|
|
6257
|
+
dmPolicy,
|
|
6258
|
+
groupPolicy,
|
|
6259
|
+
policy: { groupAllowFromFallbackToAllowFrom: false },
|
|
6260
|
+
allowFrom: dmAllowFromForIngress,
|
|
6261
|
+
groupAllowFrom: rawGroupAllowFrom,
|
|
6262
|
+
command: { commandOwnerAllowFrom }
|
|
6263
|
+
});
|
|
6264
|
+
},
|
|
6265
|
+
async resolveSlashCommandAuthorization(input) {
|
|
6266
|
+
return await resolveQQBotSlashCommandAuthorized(input);
|
|
6267
|
+
}
|
|
6268
|
+
};
|
|
6269
|
+
}
|
|
6270
|
+
async function resolveQQBotSlashCommandAuthorized(params) {
|
|
6271
|
+
const explicitAllowFrom = normalizeQQBotAllowFrom(params.commandsAllowFrom ?? (params.isGroup && params.groupAllowFrom && params.groupAllowFrom.length > 0 ? params.groupAllowFrom : params.allowFrom)).filter((entry) => entry !== "*");
|
|
6272
|
+
if (explicitAllowFrom.length === 0) return false;
|
|
6273
|
+
return (await createChannelIngressResolver({
|
|
6274
|
+
channelId: "qqbot",
|
|
6275
|
+
accountId: params.accountId,
|
|
6276
|
+
identity: qqbotIngressIdentity,
|
|
6277
|
+
cfg: params.cfg
|
|
6278
|
+
}).message({
|
|
6279
|
+
subject: { stableId: params.senderId },
|
|
6280
|
+
conversation: {
|
|
6281
|
+
kind: params.isGroup ? "group" : "direct",
|
|
6282
|
+
id: params.conversationId
|
|
6283
|
+
},
|
|
6284
|
+
event: {
|
|
6285
|
+
kind: "slash-command",
|
|
6286
|
+
authMode: "none",
|
|
6287
|
+
mayPair: false
|
|
6288
|
+
},
|
|
6289
|
+
dmPolicy: "allowlist",
|
|
6290
|
+
groupPolicy: "open",
|
|
6291
|
+
allowFrom: explicitAllowFrom,
|
|
6292
|
+
command: { modeWhenAccessGroupsOff: "configured" }
|
|
6293
|
+
})).commandAccess.authorized;
|
|
6294
|
+
}
|
|
6654
6295
|
//#endregion
|
|
6655
6296
|
//#region extensions/qqbot/src/bridge/gateway.ts
|
|
6656
6297
|
/**
|
|
@@ -6676,6 +6317,7 @@ function createEngineAdapters(_runtime) {
|
|
|
6676
6317
|
return {
|
|
6677
6318
|
history: createSdkHistoryAdapter(),
|
|
6678
6319
|
mentionGate: createSdkMentionGateAdapter(),
|
|
6320
|
+
access: createSdkAccessAdapter(),
|
|
6679
6321
|
audioConvert: {
|
|
6680
6322
|
convertSilkToWav,
|
|
6681
6323
|
isVoiceAttachment,
|