@openclaw/qqbot 2026.5.9-beta.1 → 2026.5.10-beta.2

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.
@@ -1,12 +1,12 @@
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-CqRmSFJZ.js";
2
- import { c as getPlatformAdapter, i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, o as readStringField, s as sanitizeFileName, t as asOptionalObjectRecord } from "./string-normalize-Ci6NM5DE.js";
3
- import { d as setBridgeLogger, l as ensurePlatformAdapter, s as resolveAccountBase } from "./resolve-D_06fV6-.js";
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 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-CgdXGY2H.js";
7
- import { n as getQQBotRuntimeForEngine, t as getQQBotRuntime } from "./runtime-C3Qpr4Ph.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-DQ6J3Qe-.js";
9
- import { a as getPluginVersion, i as getFrameworkVersion, n as runWithRequestContext, o as initCommands, s as matchSlashCommand } from "./request-context-DVsN666m.js";
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";
@@ -21,33 +21,10 @@ import WebSocket from "ws";
21
21
  import { resolveAmbientNodeProxyAgent } from "openclaw/plugin-sdk/extension-shared";
22
22
  import { Buffer as Buffer$1 } from "node:buffer";
23
23
  import { fileURLToPath } from "node:url";
24
- import { implicitMentionKindWhen, resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-mention-gating";
24
+ import { createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
25
+ import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-mention-gating";
25
26
  import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
26
27
  //#region extensions/qqbot/src/engine/group/activation.ts
27
- /**
28
- * Group activation mode — how the bot decides whether to respond in a group.
29
- *
30
- * Resolution chain:
31
- * 1. session store override (`/activation` command writes per-session
32
- * `groupActivation` value) — highest priority
33
- * 2. per-group `requireMention` config
34
- * 3. `"mention"` default (require @-bot to respond)
35
- *
36
- * File I/O is isolated in the default node-based reader so the gating
37
- * logic itself stays a pure function, testable without touching disk.
38
- *
39
- * Note: the implicit-mention predicate (quoting a bot message counts as
40
- * @-ing the bot) lives in `./mention.ts` alongside the other mention
41
- * helpers — see `resolveImplicitMention` there.
42
- */
43
- /**
44
- * Resolve the effective activation mode for one inbound message.
45
- *
46
- * Order of precedence:
47
- * 1. `store[sessionKey].groupActivation` (read via the injected reader)
48
- * 2. config-level `requireMention` (maps to `"mention"` / `"always"`)
49
- * 3. `"mention"` (safe default)
50
- */
51
28
  function resolveGroupActivation(params) {
52
29
  const fallback = params.configRequireMention ? "mention" : "always";
53
30
  const store = params.sessionStoreReader?.read({
@@ -61,14 +38,6 @@ function resolveGroupActivation(params) {
61
38
  if (normalized === "mention" || normalized === "always") return normalized;
62
39
  return fallback;
63
40
  }
64
- /**
65
- * Resolve the on-disk path to the agent-sessions file.
66
- *
67
- * Priority:
68
- * 1. `cfg.session.store` (supports `{agentId}` placeholder and `~` expansion)
69
- * 2. `$OPENCLAW_STATE_DIR` / `$CLAWDBOT_STATE_DIR`
70
- * 3. `~/.openclaw/agents/{agentId}/sessions/sessions.json`
71
- */
72
41
  function resolveSessionStorePath(cfg, agentId) {
73
42
  const resolvedAgentId = agentId || "default";
74
43
  const session = typeof cfg.session === "object" && cfg.session !== null ? cfg.session : void 0;
@@ -85,16 +54,6 @@ function resolveSessionStorePath(cfg, agentId) {
85
54
  const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path.join(process.env.HOME || process.env.USERPROFILE || "", ".openclaw");
86
55
  return path.join(stateDir, "agents", resolvedAgentId, "sessions", "sessions.json");
87
56
  }
88
- /**
89
- * Create the default, production-ready session-store reader.
90
- *
91
- * Reads the file synchronously on every call. The overhead is acceptable
92
- * because activation mode is only resolved once per group message and
93
- * the sessions file is typically a handful of kilobytes.
94
- *
95
- * Any I/O or JSON error is swallowed and returned as `null` so the
96
- * gating pipeline falls back to the config default.
97
- */
98
57
  function createNodeSessionStoreReader() {
99
58
  return { read: ({ cfg, agentId }) => {
100
59
  try {
@@ -414,27 +373,6 @@ async function runDiagnostics() {
414
373
  return report;
415
374
  }
416
375
  //#endregion
417
- //#region extensions/qqbot/src/engine/access/resolve-policy.ts
418
- function hasRealRestriction(list) {
419
- if (!list || list.length === 0) return false;
420
- return !list.every((entry) => String(entry).trim() === "*");
421
- }
422
- /**
423
- * Derive the effective dmPolicy and groupPolicy applied at runtime.
424
- *
425
- * Caller should pass the raw `QQBotAccountConfig`. The resolver does
426
- * not look at `groups[id]` overrides — per-group overrides are layered
427
- * on top elsewhere (see `inbound-pipeline` mention gating).
428
- */
429
- function resolveQQBotEffectivePolicies(input) {
430
- const allowFromRestricted = hasRealRestriction(input.allowFrom);
431
- const groupAllowFromRestricted = hasRealRestriction(input.groupAllowFrom);
432
- return {
433
- dmPolicy: input.dmPolicy ?? (allowFromRestricted ? "allowlist" : "open"),
434
- groupPolicy: input.groupPolicy ?? (groupAllowFromRestricted || allowFromRestricted ? "allowlist" : "open")
435
- };
436
- }
437
- //#endregion
438
376
  //#region extensions/qqbot/src/engine/access/sender-match.ts
439
377
  /**
440
378
  * QQBot sender normalization and allowlist matching.
@@ -474,177 +412,13 @@ function createQQBotSenderMatcher(senderId) {
474
412
  };
475
413
  }
476
414
  //#endregion
477
- //#region extensions/qqbot/src/engine/access/types.ts
478
- /** Structured reason codes used in logs and metrics. */
479
- const QQBOT_ACCESS_REASON = {
480
- DM_POLICY_OPEN: "dm_policy_open",
481
- DM_POLICY_DISABLED: "dm_policy_disabled",
482
- DM_POLICY_ALLOWLISTED: "dm_policy_allowlisted",
483
- DM_POLICY_NOT_ALLOWLISTED: "dm_policy_not_allowlisted",
484
- DM_POLICY_EMPTY_ALLOWLIST: "dm_policy_empty_allowlist",
485
- GROUP_POLICY_ALLOWED: "group_policy_allowed",
486
- GROUP_POLICY_DISABLED: "group_policy_disabled",
487
- GROUP_POLICY_EMPTY_ALLOWLIST: "group_policy_empty_allowlist",
488
- GROUP_POLICY_NOT_ALLOWLISTED: "group_policy_not_allowlisted",
489
- BOT_SELF_ECHO: "bot_self_echo"
490
- };
491
- //#endregion
492
- //#region extensions/qqbot/src/engine/access/access-control.ts
493
- /**
494
- * QQBot inbound access decision.
495
- *
496
- * This module is the single place where the QQBot engine decides
497
- * whether an inbound message from a given sender is allowed to
498
- * proceed into the outbound pipeline. The implementation mirrors the
499
- * semantics of the framework-wide `resolveDmGroupAccessDecision`
500
- * (`src/security/dm-policy-shared.ts`) but is kept standalone so the
501
- * `engine/` layer does not pull in `openclaw/plugin-sdk/*` modules —
502
- * a hard constraint shared with the standalone `openclaw-qqbot` build.
503
- *
504
- * If in the future we lift the zero-dependency rule in the engine
505
- * layer, this file can be replaced by a thin adapter around the
506
- * framework API with identical semantics.
507
- */
508
- /**
509
- * Evaluate the inbound access policy.
510
- *
511
- * Semantics (aligned with `resolveDmGroupAccessDecision`):
512
- * - Group message:
513
- * - `groupPolicy=disabled` → block
514
- * - `groupPolicy=open` → allow
515
- * - `groupPolicy=allowlist`:
516
- * - empty effectiveGroupAllowFrom → block (empty_allowlist)
517
- * - sender not in list → block (not_allowlisted)
518
- * - otherwise → allow
519
- * - Direct message:
520
- * - `dmPolicy=disabled` → block
521
- * - `dmPolicy=open` → allow wildcard, legacy empty allowFrom, or matching allowFrom
522
- * - `dmPolicy=allowlist`:
523
- * - empty effectiveAllowFrom → block (empty_allowlist)
524
- * - sender not in list → block (not_allowlisted)
525
- * - otherwise → allow
526
- *
527
- * The function never throws; callers can rely on the returned
528
- * `decision`/`reasonCode` pair for branching.
529
- */
530
- function resolveQQBotAccess(input) {
531
- const { dmPolicy, groupPolicy } = resolveQQBotEffectivePolicies(input);
532
- const rawGroupAllowFrom = input.groupAllowFrom && input.groupAllowFrom.length > 0 ? input.groupAllowFrom : input.allowFrom ?? [];
533
- const normalizedAllowFrom = normalizeQQBotAllowFrom(input.allowFrom);
534
- const effectiveAllowFrom = dmPolicy === "open" && normalizedAllowFrom.length === 0 ? ["*"] : normalizedAllowFrom;
535
- const effectiveGroupAllowFrom = normalizeQQBotAllowFrom(rawGroupAllowFrom);
536
- const isSenderAllowed = createQQBotSenderMatcher(input.senderId);
537
- if (input.isGroup) return evaluateGroupDecision({
538
- groupPolicy,
539
- dmPolicy,
540
- effectiveAllowFrom,
541
- effectiveGroupAllowFrom,
542
- isSenderAllowed
543
- });
544
- return evaluateDmDecision({
545
- groupPolicy,
546
- dmPolicy,
547
- effectiveAllowFrom,
548
- effectiveGroupAllowFrom,
549
- isSenderAllowed
550
- });
551
- }
552
- function evaluateGroupDecision(ctx) {
553
- const base = buildResultBase(ctx);
554
- if (ctx.groupPolicy === "disabled") return {
555
- ...base,
556
- decision: "block",
557
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_DISABLED,
558
- reason: "groupPolicy=disabled"
559
- };
560
- if (ctx.groupPolicy === "open") return {
561
- ...base,
562
- decision: "allow",
563
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_ALLOWED,
564
- reason: "groupPolicy=open"
565
- };
566
- if (ctx.effectiveGroupAllowFrom.length === 0) return {
567
- ...base,
568
- decision: "block",
569
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_EMPTY_ALLOWLIST,
570
- reason: "groupPolicy=allowlist (empty allowlist)"
571
- };
572
- if (!ctx.isSenderAllowed(ctx.effectiveGroupAllowFrom)) return {
573
- ...base,
574
- decision: "block",
575
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_NOT_ALLOWLISTED,
576
- reason: "groupPolicy=allowlist (not allowlisted)"
577
- };
578
- return {
579
- ...base,
580
- decision: "allow",
581
- reasonCode: QQBOT_ACCESS_REASON.GROUP_POLICY_ALLOWED,
582
- reason: "groupPolicy=allowlist (allowlisted)"
583
- };
584
- }
585
- function evaluateDmDecision(ctx) {
586
- const base = buildResultBase(ctx);
587
- if (ctx.dmPolicy === "disabled") return {
588
- ...base,
589
- decision: "block",
590
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_DISABLED,
591
- reason: "dmPolicy=disabled"
592
- };
593
- if (ctx.dmPolicy === "open") {
594
- if (ctx.effectiveAllowFrom.includes("*")) return {
595
- ...base,
596
- decision: "allow",
597
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_OPEN,
598
- reason: "dmPolicy=open"
599
- };
600
- if (ctx.isSenderAllowed(ctx.effectiveAllowFrom)) return {
601
- ...base,
602
- decision: "allow",
603
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_ALLOWLISTED,
604
- reason: "dmPolicy=open (allowlisted)"
605
- };
606
- return {
607
- ...base,
608
- decision: "block",
609
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
610
- reason: "dmPolicy=open (not allowlisted)"
611
- };
612
- }
613
- if (ctx.effectiveAllowFrom.length === 0) return {
614
- ...base,
615
- decision: "block",
616
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_EMPTY_ALLOWLIST,
617
- reason: "dmPolicy=allowlist (empty allowlist)"
618
- };
619
- if (!ctx.isSenderAllowed(ctx.effectiveAllowFrom)) return {
620
- ...base,
621
- decision: "block",
622
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED,
623
- reason: "dmPolicy=allowlist (not allowlisted)"
624
- };
625
- return {
626
- ...base,
627
- decision: "allow",
628
- reasonCode: QQBOT_ACCESS_REASON.DM_POLICY_ALLOWLISTED,
629
- reason: "dmPolicy=allowlist (allowlisted)"
630
- };
631
- }
632
- function buildResultBase(ctx) {
633
- return {
634
- effectiveAllowFrom: ctx.effectiveAllowFrom,
635
- effectiveGroupAllowFrom: ctx.effectiveGroupAllowFrom,
636
- dmPolicy: ctx.dmPolicy,
637
- groupPolicy: ctx.groupPolicy
638
- };
639
- }
640
- //#endregion
641
415
  //#region extensions/qqbot/src/engine/commands/slash-command-auth.ts
642
416
  /**
643
417
  * Pre-dispatch authorization for requireAuth slash commands.
644
418
  *
645
- * Unlike the access-stage's `resolveCommandAuthorized` (which permits
646
- * `dm_policy_open` senders — i.e. anyone), this function requires the
647
- * sender to appear in an **explicit non-wildcard** allowFrom list.
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.
648
422
  *
649
423
  * Rationale: sensitive operations (log export, file deletion, approval
650
424
  * config changes) must be gated behind a deliberate operator decision.
@@ -713,6 +487,22 @@ async function trySlashCommand(msg, ctx) {
713
487
  }
714
488
  const receivedAt = Date.now();
715
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
+ });
716
506
  const cmdCtx = {
717
507
  type: msg.type,
718
508
  senderId: msg.senderId,
@@ -727,13 +517,7 @@ async function trySlashCommand(msg, ctx) {
727
517
  accountId: account.accountId,
728
518
  appId: account.appId,
729
519
  accountConfig: account.config,
730
- commandAuthorized: resolveSlashCommandAuth({
731
- senderId: msg.senderId,
732
- isGroup: msg.type === "group" || msg.type === "guild",
733
- allowFrom: account.config?.allowFrom,
734
- groupAllowFrom: account.config?.groupAllowFrom,
735
- commandsAllowFrom: resolveQQBotCommandsAllowFrom(ctx.cfg)
736
- }),
520
+ commandAuthorized,
737
521
  queueSnapshot: ctx.getQueueSnapshot(peerId)
738
522
  };
739
523
  try {
@@ -1345,43 +1129,16 @@ function buildGroupQueuedMessage(data, accountId, eventType) {
1345
1129
  }
1346
1130
  //#endregion
1347
1131
  //#region extensions/qqbot/src/engine/gateway/message-queue.ts
1348
- /**
1349
- * Per-user concurrent message queue.
1350
- *
1351
- * Messages are serialized per **peer** (one DM user, one group, one guild
1352
- * channel) and processed in parallel across peers up to
1353
- * {@link DEFAULT_MAX_CONCURRENT_USERS}.
1354
- *
1355
- * Group-specific enhancements (added when merging from the standalone build):
1356
- * - Group peers have a larger queue cap ({@link DEFAULT_GROUP_QUEUE_SIZE})
1357
- * because groups can burst more chatter than a single DM.
1358
- * - When a group's queue overflows, bot-authored messages are evicted
1359
- * preferentially so human messages don't get dropped.
1360
- * - When draining a group peer with more than one queued message, the
1361
- * non-command messages are **merged** into one logical turn (see
1362
- * {@link mergeGroupMessages}). Slash commands are always processed
1363
- * individually to avoid conflating a "/stop" with surrounding chatter.
1364
- *
1365
- * The module is self-contained: the only injected dependency is the
1366
- * logger / abort probe supplied via {@link MessageQueueContext}.
1367
- */
1368
- /** Global cap across all peers. */
1369
1132
  const DEFAULT_GLOBAL_QUEUE_SIZE = 1e3;
1370
- /** Per-DM / per-channel cap. */
1371
1133
  const DEFAULT_PER_PEER_QUEUE_SIZE = 20;
1372
- /** Per-group cap — larger because groups burst more. */
1373
1134
  const DEFAULT_GROUP_QUEUE_SIZE = 50;
1374
- /** Parallel fanout across peers. */
1375
1135
  const DEFAULT_MAX_CONCURRENT_USERS = 10;
1376
- /** Convenience predicate: is this a merged multi-message turn? */
1377
1136
  function isMergedTurn(msg) {
1378
1137
  return (msg.merge?.count ?? 0) > 1;
1379
1138
  }
1380
- /** Return true when the peer id refers to a group-like conversation. */
1381
1139
  function isGroupPeer(peerId) {
1382
1140
  return peerId.startsWith("group:") || peerId.startsWith("guild:");
1383
1141
  }
1384
- /** Slash-command test used by {@link drainGroupBatch}. */
1385
1142
  function isSlashCommand(msg) {
1386
1143
  return (msg.content ?? "").trim().startsWith("/");
1387
1144
  }
@@ -1448,9 +1205,6 @@ function mergeGroupMessages(batch) {
1448
1205
  }
1449
1206
  };
1450
1207
  }
1451
- /**
1452
- * Create a per-user concurrent queue with built-in group enhancements.
1453
- */
1454
1208
  function createMessageQueue(ctx) {
1455
1209
  const { accountId: _accountId, log } = ctx;
1456
1210
  const globalQueueSize = ctx.globalQueueSize ?? DEFAULT_GLOBAL_QUEUE_SIZE;
@@ -1466,13 +1220,6 @@ function createMessageQueue(ctx) {
1466
1220
  if (msg.type === "group") return `group:${msg.groupOpenid ?? "unknown"}`;
1467
1221
  return `dm:${msg.senderId}`;
1468
1222
  };
1469
- /**
1470
- * Evict one message from an over-full queue.
1471
- *
1472
- * For group peers we prefer to drop a bot-authored message so human
1473
- * input never gets lost. Falling back to dropping the oldest keeps the
1474
- * queue bounded when all members are bots.
1475
- */
1476
1223
  const evictOne = (queue, isGroup) => {
1477
1224
  if (isGroup) {
1478
1225
  const botIdx = queue.findIndex((m) => m.senderIsBot);
@@ -1480,7 +1227,6 @@ function createMessageQueue(ctx) {
1480
1227
  }
1481
1228
  return queue.shift();
1482
1229
  };
1483
- /** Run a single message, capturing errors in the log. */
1484
1230
  const processOne = async (msg, peerId, label) => {
1485
1231
  try {
1486
1232
  await handleMessageFnRef(msg);
@@ -1488,11 +1234,6 @@ function createMessageQueue(ctx) {
1488
1234
  log?.error(`${label} error for ${peerId}: ${formatErrorMessage(err)}`);
1489
1235
  }
1490
1236
  };
1491
- /**
1492
- * Drain a group's batch:
1493
- * - slash commands are processed one by one (order preserved);
1494
- * - the remaining messages are merged into a single turn.
1495
- */
1496
1237
  const drainGroupBatch = async (batch, peerId) => {
1497
1238
  const commands = [];
1498
1239
  const normal = [];
@@ -1508,7 +1249,6 @@ function createMessageQueue(ctx) {
1508
1249
  await processOne(merged, peerId, `Message processor (merged batch of ${normal.length})`);
1509
1250
  }
1510
1251
  };
1511
- /** Process one peer's queue serially. */
1512
1252
  const drainUserQueue = async (peerId) => {
1513
1253
  if (activeUsers.has(peerId)) return;
1514
1254
  if (activeUsers.size >= maxConcurrentUsers) {
@@ -1761,12 +1501,6 @@ async function createQQWSClient(options) {
1761
1501
  }
1762
1502
  //#endregion
1763
1503
  //#region extensions/qqbot/src/engine/gateway/gateway-connection.ts
1764
- /**
1765
- * GatewayConnection — WebSocket lifecycle, heartbeat, reconnect, and session persistence.
1766
- *
1767
- * Encapsulates all connection state as class fields (replaces 11 closure variables).
1768
- * Event handling and message processing are delegated to injected handlers.
1769
- */
1770
1504
  var GatewayConnection = class {
1771
1505
  constructor(ctx) {
1772
1506
  this.isAborted = false;
@@ -1785,7 +1519,6 @@ var GatewayConnection = class {
1785
1519
  isAborted: () => this.isAborted
1786
1520
  });
1787
1521
  }
1788
- /** Start the connection loop. Resolves when abortSignal fires. */
1789
1522
  async start() {
1790
1523
  this.restoreSession();
1791
1524
  this.registerAbortHandler();
@@ -1882,7 +1615,12 @@ var GatewayConnection = class {
1882
1615
  cfg: this.ctx.cfg,
1883
1616
  log,
1884
1617
  getMessagePeerId: (msg) => this.msgQueue.getMessagePeerId(msg),
1885
- 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
+ })
1886
1624
  };
1887
1625
  const trySlashCommandOrEnqueue = async (msg) => {
1888
1626
  const result = await trySlashCommand(msg, slashCtx);
@@ -2319,7 +2057,6 @@ async function processVoiceAttachment(localPath, audioPath, att, asrReferText, c
2319
2057
  }
2320
2058
  //#endregion
2321
2059
  //#region extensions/qqbot/src/engine/gateway/stages/stub-contexts.ts
2322
- /** Build an {@link InboundContext} with all non-routing fields cleared. */
2323
2060
  function emptyInboundContext(fields) {
2324
2061
  return {
2325
2062
  event: fields.event,
@@ -2328,30 +2065,12 @@ function emptyInboundContext(fields) {
2328
2065
  peerId: fields.peerId,
2329
2066
  qualifiedTarget: fields.qualifiedTarget,
2330
2067
  fromAddress: fields.fromAddress,
2331
- parsedContent: "",
2332
- userContent: "",
2333
- quotePart: "",
2334
- dynamicCtx: "",
2335
- userMessage: "",
2336
2068
  agentBody: "",
2337
2069
  body: "",
2338
- systemPrompts: [],
2339
2070
  groupSystemPrompt: void 0,
2340
- attachments: {
2341
- attachmentInfo: "",
2342
- imageUrls: [],
2343
- imageMediaTypes: [],
2344
- voiceAttachmentPaths: [],
2345
- voiceAttachmentUrls: [],
2346
- voiceAsrReferTexts: [],
2347
- voiceTranscripts: [],
2348
- voiceTranscriptSources: [],
2349
- attachmentLocalPaths: []
2350
- },
2351
2071
  localMediaPaths: [],
2352
2072
  localMediaTypes: [],
2353
2073
  remoteMediaUrls: [],
2354
- remoteMediaTypes: [],
2355
2074
  uniqueVoicePaths: [],
2356
2075
  uniqueVoiceUrls: [],
2357
2076
  uniqueVoiceAsrReferTexts: [],
@@ -2367,52 +2086,29 @@ function emptyInboundContext(fields) {
2367
2086
  inputNotifyRefIdx: void 0
2368
2087
  };
2369
2088
  }
2370
- /**
2371
- * Build an {@link InboundContext} that represents a message blocked by
2372
- * access control (policy denial, allowlist mismatch, etc.).
2373
- */
2374
2089
  function buildBlockedInboundContext(params) {
2375
2090
  return {
2376
2091
  ...emptyInboundContext(params),
2377
2092
  blocked: true,
2378
- blockReason: params.access.reason,
2379
- blockReasonCode: params.access.reasonCode,
2380
- accessDecision: params.access.decision
2093
+ blockReason: params.access.senderAccess.reasonCode,
2094
+ blockReasonCode: params.access.senderAccess.reasonCode,
2095
+ accessDecision: params.access.senderAccess.decision
2381
2096
  };
2382
2097
  }
2383
- /**
2384
- * Build an {@link InboundContext} that represents a message stopped by
2385
- * the group gate (drop_other_mention, block_unauthorized_command,
2386
- * skip_no_mention). Any history side-effects have already been applied
2387
- * by the gate stage.
2388
- */
2389
2098
  function buildSkippedInboundContext(params) {
2390
2099
  return {
2391
2100
  ...emptyInboundContext(params),
2392
2101
  group: params.group,
2393
2102
  skipped: true,
2394
2103
  skipReason: params.skipReason,
2395
- accessDecision: params.access.decision,
2104
+ accessDecision: params.access.senderAccess.decision,
2396
2105
  typing: params.typing,
2397
2106
  inputNotifyRefIdx: params.inputNotifyRefIdx
2398
2107
  };
2399
2108
  }
2400
2109
  //#endregion
2401
2110
  //#region extensions/qqbot/src/engine/gateway/stages/access-stage.ts
2402
- /**
2403
- * Access stage — resolves routing target + runs access control.
2404
- *
2405
- * Split from the pipeline so it is trivially unit-testable: given a raw
2406
- * event and the runtime's routing info, the stage returns either:
2407
- * - `{ kind: "allow", ... }` — proceed through the rest of the pipeline
2408
- * - `{ kind: "block", context }` — short-circuit; the caller returns
2409
- * `context` directly to its own caller.
2410
- */
2411
- /**
2412
- * Resolve the routing target, walk the access policy, and decide whether
2413
- * the inbound message should proceed to the rest of the pipeline.
2414
- */
2415
- function runAccessStage(event, deps) {
2111
+ async function runAccessStage(event, deps) {
2416
2112
  const { account, cfg, runtime, log } = deps;
2417
2113
  const isGroupChat = event.type === "guild" || event.type === "group";
2418
2114
  const peerId = resolvePeerId(event, isGroupChat);
@@ -2426,16 +2122,19 @@ function runAccessStage(event, deps) {
2426
2122
  id: peerId
2427
2123
  }
2428
2124
  });
2429
- const access = resolveQQBotAccess({
2125
+ const access = await deps.adapters.access.resolveInboundAccess({
2126
+ cfg,
2127
+ accountId: account.accountId,
2430
2128
  isGroup: isGroupChat,
2431
2129
  senderId: event.senderId,
2130
+ conversationId: peerId,
2432
2131
  allowFrom: account.config?.allowFrom,
2433
2132
  groupAllowFrom: account.config?.groupAllowFrom,
2434
2133
  dmPolicy: account.config?.dmPolicy,
2435
2134
  groupPolicy: account.config?.groupPolicy
2436
2135
  });
2437
- if (access.decision !== "allow") {
2438
- log?.info(`Blocked qqbot inbound: decision=${access.decision} reasonCode=${access.reasonCode} reason=${access.reason} senderId=${event.senderId} accountId=${account.accountId} isGroup=${isGroupChat}`);
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}`);
2439
2138
  return {
2440
2139
  kind: "block",
2441
2140
  context: buildBlockedInboundContext({
@@ -2469,28 +2168,9 @@ function buildQualifiedTarget(event, isGroupChat) {
2469
2168
  if (isGroupChat) return event.type === "guild" ? `qqbot:channel:${event.channelId}` : `qqbot:group:${event.groupOpenid}`;
2470
2169
  return event.type === "dm" ? `qqbot:dm:${event.guildId}` : `qqbot:c2c:${event.senderId}`;
2471
2170
  }
2472
- /**
2473
- * Decide whether the access decision permits running text-based control
2474
- * commands. Placed in the access stage because the rule is an
2475
- * access-policy derivative, not a gate derivative.
2476
- */
2477
- function resolveCommandAuthorized(access) {
2478
- return access.reasonCode === "dm_policy_open" || access.reasonCode === "dm_policy_allowlisted" || access.reasonCode === "group_policy_allowed" && access.effectiveGroupAllowFrom.length > 0 && access.groupPolicy === "allowlist";
2479
- }
2480
2171
  //#endregion
2481
2172
  //#region extensions/qqbot/src/engine/group/mention.ts
2482
- /** Regex detecting `<@openid>` / `<@!openid>` mention tags in raw content. */
2483
2173
  const MENTION_TAG_RE = /<@!?\w+>/;
2484
- /**
2485
- * Detect whether the inbound message explicitly targets the bot.
2486
- *
2487
- * Priority order:
2488
- * 1. `mentions[].is_you === true` (most reliable)
2489
- * 2. `eventType === "GROUP_AT_MESSAGE_CREATE"` (QQ-level @bot event)
2490
- * 3. regex match on any of `mentionPatterns` (fallback, e.g. "@bot-name")
2491
- *
2492
- * Returns `false` for direct messages or when no signal is found.
2493
- */
2494
2174
  function detectWasMentioned(input) {
2495
2175
  const { eventType, mentions, content, mentionPatterns } = input;
2496
2176
  if (mentions?.some((m) => m.is_you)) return true;
@@ -2503,30 +2183,11 @@ function detectWasMentioned(input) {
2503
2183
  }
2504
2184
  return false;
2505
2185
  }
2506
- /**
2507
- * Report whether the message contains **any** @mention (not necessarily @bot).
2508
- *
2509
- * Used by the gating layer to decide whether to bypass mention requirements
2510
- * for control commands. A control command like `/stop` that also @-s another
2511
- * user should NOT bypass the mention gate — the `@other-user` prefix is a
2512
- * strong signal that the command wasn't addressed to the bot.
2513
- */
2514
2186
  function hasAnyMention(input) {
2515
2187
  if (input.mentions && input.mentions.length > 0) return true;
2516
2188
  if (input.content && MENTION_TAG_RE.test(input.content)) return true;
2517
2189
  return false;
2518
2190
  }
2519
- /**
2520
- * Clean up `<@openid>` mention tags in raw QQ group content.
2521
- *
2522
- * - For the bot's own mention (`is_you === true`): the tag is removed
2523
- * outright so prompts don't contain visible `<@BOTID>` garbage.
2524
- * - For other mentioned users: the tag is replaced with `@nickname` (or
2525
- * `@username`) for readability. Entries without a display name are left
2526
- * as-is (rare in practice).
2527
- *
2528
- * Returns the original text unchanged when `text` or `mentions` is empty.
2529
- */
2530
2191
  function stripMentionText(text, mentions) {
2531
2192
  if (!text || !mentions?.length) return text;
2532
2193
  let cleaned = text;
@@ -2542,7 +2203,6 @@ function stripMentionText(text, mentions) {
2542
2203
  }
2543
2204
  return cleaned;
2544
2205
  }
2545
- /** Escape characters that carry regex meaning. */
2546
2206
  function escapeRegex$1(str) {
2547
2207
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2548
2208
  }
@@ -2852,7 +2512,6 @@ const DEFAULT_GROUP_CONFIG = {
2852
2512
  name: "",
2853
2513
  historyLimit: 50
2854
2514
  };
2855
- /** Read a named account's raw `groups` map from an OpenClawConfig. */
2856
2515
  function readGroupsMap(cfg, accountId) {
2857
2516
  const groups = asOptionalObjectRecord(resolveAccountBase(cfg, accountId).config.groups);
2858
2517
  if (!groups) return {};
@@ -2880,12 +2539,6 @@ function readHistoryLimit(obj, key) {
2880
2539
  if (typeof v !== "number" || !Number.isFinite(v)) return;
2881
2540
  return Math.max(0, Math.floor(v));
2882
2541
  }
2883
- /**
2884
- * Resolve per-group configuration with `specific > "*" > default` precedence.
2885
- *
2886
- * When `groupOpenid` is not provided, only the wildcard/default values are
2887
- * returned. This lets callers query the "default" behaviour for new groups.
2888
- */
2889
2542
  function resolveGroupConfig(cfg, groupOpenid, accountId) {
2890
2543
  const groups = readGroupsMap(cfg, accountId);
2891
2544
  const wildcard = groups["*"] ?? {};
@@ -2899,13 +2552,6 @@ function resolveGroupConfig(cfg, groupOpenid, accountId) {
2899
2552
  historyLimit: readHistoryLimit(specific, "historyLimit") ?? readHistoryLimit(wildcard, "historyLimit") ?? DEFAULT_GROUP_CONFIG.historyLimit
2900
2553
  };
2901
2554
  }
2902
- /**
2903
- * Resolve all per-inbound group-related settings in one pass.
2904
- *
2905
- * Prefer this over calling `resolveHistoryLimit` / `resolveRequireMention`
2906
- * / etc. individually in hot paths — each of those currently re-walks
2907
- * the config tree on its own.
2908
- */
2909
2555
  function resolveGroupSettings(params) {
2910
2556
  const config = resolveGroupConfig(params.cfg, params.groupOpenid, params.accountId);
2911
2557
  return {
@@ -2914,12 +2560,6 @@ function resolveGroupSettings(params) {
2914
2560
  mentionPatterns: resolveMentionPatterns(params.cfg, params.agentId)
2915
2561
  };
2916
2562
  }
2917
- /**
2918
- * Resolve mentionPatterns with `agent > global > []` precedence.
2919
- *
2920
- * Mirrors the framework's `messages.groupChat.mentionPatterns` / per-agent
2921
- * `agents.list[].groupChat.mentionPatterns` chain.
2922
- */
2923
2563
  function resolveMentionPatterns(cfg, agentId) {
2924
2564
  if (agentId) {
2925
2565
  const agents = asOptionalObjectRecord(cfg.agents);
@@ -2938,34 +2578,6 @@ function resolveMentionPatterns(cfg, agentId) {
2938
2578
  }
2939
2579
  //#endregion
2940
2580
  //#region extensions/qqbot/src/engine/gateway/stages/group-gate-stage.ts
2941
- /**
2942
- * Group-gate stage — for `type === "group"` inbound events, decide
2943
- * whether the message should pass to AI dispatch or be intercepted.
2944
- *
2945
- * Three possible outcomes:
2946
- * - `{ kind: "pass", groupInfo }` — continue the pipeline
2947
- * - `{ kind: "skip", groupInfo, skipReason }` — buffered to history
2948
- * (if applicable) and short-circuit
2949
- * - No group info at all — returned when the event isn't a group event
2950
- * (caller should treat as a straight pass-through)
2951
- *
2952
- * Consolidates the control-command auth check, session-store
2953
- * activation override, mention detection, and the unified
2954
- * {@link resolveGroupMessageGate} call. Delegates all pure logic to
2955
- * existing `engine/group/*` modules so this stage remains a thin
2956
- * orchestrator.
2957
- */
2958
- /**
2959
- * Run the group-gate stage.
2960
- *
2961
- * Precondition: `event.type === "group"` && `event.groupOpenid` is set.
2962
- * The caller (pipeline) enforces this; the stage doesn't re-check.
2963
- *
2964
- * On `skip` outcomes the stage records the message into the group's
2965
- * history buffer when the skip reason is one that should preserve
2966
- * context (drop / skip_no_mention), then returns. `block` skip
2967
- * reasons do NOT write history — they are silent rejects.
2968
- */
2969
2581
  function runGroupGateStage(input) {
2970
2582
  const { event, deps, accountId, agentId, sessionKey, userContent, processedAttachments } = input;
2971
2583
  const groupOpenid = event.groupOpenid;
@@ -3002,7 +2614,7 @@ function runGroupGateStage(input) {
3002
2614
  });
3003
2615
  const content = (event.content ?? "").trim();
3004
2616
  const isControlCommand = Boolean(deps.isControlCommand?.(content));
3005
- const commandAuthorized = deps.allowTextCommands !== false && isSenderAllowedForCommands(event.senderId, deps);
2617
+ const commandAuthorized = deps.allowTextCommands !== false && input.access.commandAccess.authorized;
3006
2618
  const gate = resolveGateWithPort({
3007
2619
  mentionGatePort: deps.adapters.mentionGate,
3008
2620
  ignoreOtherMentions,
@@ -3052,12 +2664,6 @@ function runGroupGateStage(input) {
3052
2664
  skipReason: gate.action
3053
2665
  };
3054
2666
  }
3055
- /**
3056
- * Resolve the gate using the SDK MentionGatePort adapter.
3057
- *
3058
- * Layer 1 (ignoreOtherMentions) is QQ-specific and handled here.
3059
- * Layers 2+3 delegate to the SDK's `resolveInboundMentionDecision`.
3060
- */
3061
2667
  function resolveGateWithPort(params) {
3062
2668
  if (params.ignoreOtherMentions && params.hasAnyMention && !params.wasMentioned && !params.implicitMention) return {
3063
2669
  action: "drop_other_mention",
@@ -3095,15 +2701,6 @@ function resolveGateWithPort(params) {
3095
2701
  shouldBypassMention: decision.shouldBypassMention
3096
2702
  };
3097
2703
  }
3098
- /**
3099
- * Test whether the sender is on the DM `allowFrom` list.
3100
- */
3101
- function isSenderAllowedForCommands(senderId, deps) {
3102
- const raw = deps.account.config?.allowFrom;
3103
- if (!Array.isArray(raw) || raw.length === 0) return true;
3104
- const normalized = normalizeQQBotAllowFrom(raw);
3105
- return createQQBotSenderMatcher(senderId)(normalized);
3106
- }
3107
2704
  function recordGroupHistory(params) {
3108
2705
  const { historyMap, groupOpenid, historyLimit, event, userContent, historyPort, localPaths } = params;
3109
2706
  if (!historyMap || historyLimit <= 0) return;
@@ -3271,15 +2868,9 @@ function writeRefIndex(params) {
3271
2868
  }
3272
2869
  //#endregion
3273
2870
  //#region extensions/qqbot/src/engine/gateway/inbound-pipeline.ts
3274
- /**
3275
- * Process a raw queued message through the full inbound pipeline.
3276
- *
3277
- * Returns an {@link InboundContext} with `blocked` / `skipped` set when
3278
- * the message should not reach the AI dispatcher.
3279
- */
3280
2871
  async function buildInboundContext(event, deps) {
3281
2872
  const { account, log } = deps;
3282
- const accessResult = runAccessStage(event, deps);
2873
+ const accessResult = await runAccessStage(event, deps);
3283
2874
  if (accessResult.kind === "block") return accessResult.context;
3284
2875
  const { isGroupChat, peerId, qualifiedTarget, fromAddress, route, access } = accessResult;
3285
2876
  const typingPromise = deps.startTyping(event);
@@ -3311,7 +2902,8 @@ async function buildInboundContext(event, deps) {
3311
2902
  agentId: route.agentId,
3312
2903
  sessionKey: route.sessionKey,
3313
2904
  userContent,
3314
- processedAttachments: processed
2905
+ processedAttachments: processed,
2906
+ access
3315
2907
  });
3316
2908
  if (gateOutcome.kind === "skip") {
3317
2909
  typingResult.keepAlive?.stop();
@@ -3346,26 +2938,6 @@ async function buildInboundContext(event, deps) {
3346
2938
  uniqueVoiceUrls: media.uniqueVoiceUrls,
3347
2939
  uniqueVoiceAsrReferTexts: media.uniqueVoiceAsrReferTexts
3348
2940
  });
3349
- const userMessage = buildUserMessage({
3350
- event,
3351
- userContent,
3352
- quotePart,
3353
- isGroupChat,
3354
- groupInfo
3355
- });
3356
- const agentBody = buildAgentBody({
3357
- event,
3358
- userContent,
3359
- userMessage,
3360
- dynamicCtx,
3361
- isGroupChat,
3362
- groupInfo,
3363
- deps
3364
- });
3365
- const systemPrompts = [];
3366
- if (account.systemPrompt) systemPrompts.push(account.systemPrompt);
3367
- const groupSystemPrompt = buildGroupSystemPrompt(systemPrompts.length > 0 ? systemPrompts.join("\n") : "", groupInfo);
3368
- const commandAuthorized = resolveCommandAuthorized(access);
3369
2941
  return {
3370
2942
  event,
3371
2943
  route,
@@ -3373,20 +2945,26 @@ async function buildInboundContext(event, deps) {
3373
2945
  peerId,
3374
2946
  qualifiedTarget,
3375
2947
  fromAddress,
3376
- parsedContent,
3377
- userContent,
3378
- quotePart,
3379
- dynamicCtx,
3380
- userMessage,
3381
- agentBody,
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
+ }),
3382
2963
  body,
3383
- systemPrompts,
3384
- groupSystemPrompt,
3385
- attachments: processed,
2964
+ groupSystemPrompt: buildGroupSystemPrompt(account.systemPrompt ?? "", groupInfo),
3386
2965
  localMediaPaths: media.localMediaPaths,
3387
2966
  localMediaTypes: media.localMediaTypes,
3388
2967
  remoteMediaUrls: media.remoteMediaUrls,
3389
- remoteMediaTypes: media.remoteMediaTypes,
3390
2968
  uniqueVoicePaths: media.uniqueVoicePaths,
3391
2969
  uniqueVoiceUrls: media.uniqueVoiceUrls,
3392
2970
  uniqueVoiceAsrReferTexts: media.uniqueVoiceAsrReferTexts,
@@ -3394,19 +2972,15 @@ async function buildInboundContext(event, deps) {
3394
2972
  hasAsrReferFallback: media.hasAsrReferFallback,
3395
2973
  voiceTranscriptSources: media.voiceTranscriptSources,
3396
2974
  replyTo,
3397
- commandAuthorized,
2975
+ commandAuthorized: access.commandAccess.authorized,
3398
2976
  group: groupInfo,
3399
2977
  blocked: false,
3400
2978
  skipped: false,
3401
- accessDecision: access.decision,
2979
+ accessDecision: access.senderAccess.decision,
3402
2980
  typing: { keepAlive: typingResult.keepAlive },
3403
2981
  inputNotifyRefIdx: typingResult.refIdx
3404
2982
  };
3405
2983
  }
3406
- /**
3407
- * Clear a group's pending history buffer. Exposed so the gateway can
3408
- * call it in its `finally` block after a reply attempt.
3409
- */
3410
2984
  function clearGroupPendingHistory(params) {
3411
2985
  if (!params.historyMap || !params.groupOpenid) return;
3412
2986
  params.historyPort.clearPendingHistory({
@@ -3416,6 +2990,20 @@ function clearGroupPendingHistory(params) {
3416
2990
  });
3417
2991
  }
3418
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
3419
3007
  //#region extensions/qqbot/src/engine/gateway/interaction-handler.ts
3420
3008
  /**
3421
3009
  * INTERACTION_CREATE event handler.
@@ -5942,7 +5530,7 @@ async function dispatchOutbound(inbound, deps) {
5942
5530
  };
5943
5531
  const sendWithRetry = (sendFn) => sendWithTokenRetry(account.appId, account.clientSecret, sendFn, log, account.accountId);
5944
5532
  const sendErrorMessage = (errorText) => sendErrorToTarget(replyCtx, errorText);
5945
- const ctxPayload = buildCtxPayload(inbound, runtime);
5533
+ const ctxPayload = buildCtxPayload(inbound, runtime, cfg);
5946
5534
  let hasResponse = false;
5947
5535
  let hasBlockResponse = false;
5948
5536
  let toolDeliverCount = 0;
@@ -6226,8 +5814,14 @@ async function dispatchOutbound(inbound, deps) {
6226
5814
  }
6227
5815
  }
6228
5816
  }
6229
- function buildCtxPayload(inbound, runtime) {
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) {
6230
5823
  const { event } = inbound;
5824
+ const commandSource = resolveCommandSource(inbound, runtime, cfg);
6231
5825
  return runtime.channel.reply.finalizeInboundContext({
6232
5826
  Body: inbound.body,
6233
5827
  BodyForAgent: inbound.agentBody,
@@ -6257,6 +5851,7 @@ function buildCtxPayload(inbound, runtime) {
6257
5851
  QQVoiceAsrReferTexts: inbound.uniqueVoiceAsrReferTexts,
6258
5852
  QQVoiceInputStrategy: "prefer_audio_stt_then_asr_fallback",
6259
5853
  CommandAuthorized: inbound.commandAuthorized,
5854
+ ...commandSource ? { CommandSource: commandSource } : {},
6260
5855
  ...inbound.voiceMediaTypes.length > 0 ? {
6261
5856
  MediaTypes: inbound.voiceMediaTypes,
6262
5857
  MediaType: inbound.voiceMediaTypes[0]
@@ -6337,22 +5932,6 @@ var TypingKeepAlive = class {
6337
5932
  };
6338
5933
  //#endregion
6339
5934
  //#region extensions/qqbot/src/engine/gateway/gateway.ts
6340
- /**
6341
- * Core gateway entry point — thin shell that wires together:
6342
- *
6343
- * - GatewayConnection: WebSocket lifecycle, heartbeat, reconnect
6344
- * - buildInboundContext: content building, attachments, quote resolution
6345
- * - dispatchOutbound: AI dispatch, deliver callbacks, timeouts
6346
- *
6347
- * The only responsibilities of this file are:
6348
- * 1. Initialize adapters from EngineAdapters
6349
- * 2. Initialize API config + refIdx cache hook
6350
- * 3. Create the message handler (inbound → outbound pipeline)
6351
- * 4. Start GatewayConnection
6352
- */
6353
- /**
6354
- * Start the Gateway WebSocket connection with automatic reconnect support.
6355
- */
6356
5935
  async function startGateway$1(ctx) {
6357
5936
  const { account, log, runtime, adapters } = ctx;
6358
5937
  setOutboundAudioPort(adapters.outboundAudio);
@@ -6475,6 +6054,7 @@ async function startGateway$1(ctx) {
6475
6054
  cfg: ctx.cfg,
6476
6055
  log,
6477
6056
  runtime,
6057
+ adapters,
6478
6058
  onReady: ctx.onReady,
6479
6059
  onResumed: ctx.onResumed,
6480
6060
  onError: ctx.onError,
@@ -6612,24 +6192,14 @@ function readQQBotVersionFromManifest(manifestPath) {
6612
6192
  }
6613
6193
  //#endregion
6614
6194
  //#region extensions/qqbot/src/bridge/sdk-adapter.ts
6615
- /**
6616
- * SDK adapter — binds engine port interfaces to the framework's shared
6617
- * SDK implementations.
6618
- *
6619
- * This file lives in bridge/ (not engine/) because it imports from
6620
- * `openclaw/plugin-sdk/*`. The engine layer stays zero-SDK-dependency;
6621
- * only the bridge layer couples to the framework.
6622
- */
6195
+ const qqbotIngressIdentity = defineStableChannelIngressIdentity({
6196
+ key: "sender-id",
6197
+ normalize: normalizeQQBotSenderId,
6198
+ isWildcardEntry: (entry) => normalizeQQBotSenderId(entry) === "*"
6199
+ });
6623
6200
  function asSdkMap(map) {
6624
6201
  return map;
6625
6202
  }
6626
- /**
6627
- * History adapter backed by SDK `reply-history`.
6628
- *
6629
- * Delegates record/build/clear to the SDK's shared implementation so
6630
- * the engine benefits from SDK improvements (e.g. future visibility
6631
- * filtering) without code duplication.
6632
- */
6633
6203
  function createSdkHistoryAdapter() {
6634
6204
  return {
6635
6205
  recordPendingHistoryEntry(params) {
@@ -6659,38 +6229,69 @@ function createSdkHistoryAdapter() {
6659
6229
  }
6660
6230
  };
6661
6231
  }
6662
- /**
6663
- * MentionGate adapter backed by SDK `channel-mention-gating`.
6664
- *
6665
- * Maps the engine's mention facts/policy to the SDK's
6666
- * `resolveInboundMentionDecision` call, normalizing the implicit
6667
- * mention boolean into the SDK's typed `ImplicitMentionKind[]`.
6668
- */
6669
6232
  function createSdkMentionGateAdapter() {
6670
6233
  return { resolveInboundMentionDecision(params) {
6671
- const result = resolveInboundMentionDecision({
6672
- facts: {
6673
- canDetectMention: params.facts.canDetectMention,
6674
- wasMentioned: params.facts.wasMentioned,
6675
- hasAnyMention: params.facts.hasAnyMention,
6676
- implicitMentionKinds: params.facts.implicitMentionKinds ?? implicitMentionKindWhen("reply_to_bot", false)
6677
- },
6678
- policy: {
6679
- isGroup: params.policy.isGroup,
6680
- requireMention: params.policy.requireMention,
6681
- allowTextCommands: params.policy.allowTextCommands,
6682
- hasControlCommand: params.policy.hasControlCommand,
6683
- commandAuthorized: params.policy.commandAuthorized
6684
- }
6685
- });
6686
- return {
6687
- effectiveWasMentioned: result.effectiveWasMentioned,
6688
- shouldSkip: result.shouldSkip,
6689
- shouldBypassMention: result.shouldBypassMention,
6690
- implicitMention: result.implicitMention
6691
- };
6234
+ return resolveInboundMentionDecision(params);
6692
6235
  } };
6693
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
+ }
6694
6295
  //#endregion
6695
6296
  //#region extensions/qqbot/src/bridge/gateway.ts
6696
6297
  /**
@@ -6716,6 +6317,7 @@ function createEngineAdapters(_runtime) {
6716
6317
  return {
6717
6318
  history: createSdkHistoryAdapter(),
6718
6319
  mentionGate: createSdkMentionGateAdapter(),
6320
+ access: createSdkAccessAdapter(),
6719
6321
  audioConvert: {
6720
6322
  convertSilkToWav,
6721
6323
  isVoiceAttachment,