mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
refactor(channels): centralize mention and group activation decisions (#109480)
* refactor(channels): adopt shared mention-gate decision; consolidate group activation * fix(matrix): carry effective mention decision * test(session): pin qqbot accessor migration
This commit is contained in:
@@ -884,6 +884,33 @@ describe("matrix monitor handler pairing account scope", () => {
|
||||
expect(recordInboundSession).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ body: "hello", isControlCommand: false, expectedDispatches: 0 },
|
||||
{ body: "/new", isControlCommand: true, expectedDispatches: 1 },
|
||||
])(
|
||||
"keeps require-mention decision for unmentioned room text $body",
|
||||
async ({ body, isControlCommand, expectedDispatches }) => {
|
||||
const { handler, finalizeInboundContext } = createMatrixHandlerTestHarness({
|
||||
cfg: { commands: { useAccessGroups: false } },
|
||||
isDirectMessage: false,
|
||||
mentionRegexes: [],
|
||||
shouldHandleTextCommands: () => true,
|
||||
hasControlCommand: (text?: string) => isControlCommand && text === body,
|
||||
getMemberDisplayName: async () => "sender",
|
||||
});
|
||||
|
||||
await handler(
|
||||
"!room:example.org",
|
||||
createMatrixTextMessageEvent({
|
||||
eventId: `$unmentioned-${isControlCommand ? "command" : "text"}`,
|
||||
body,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(finalizeInboundContext).toHaveBeenCalledTimes(expectedDispatches);
|
||||
},
|
||||
);
|
||||
|
||||
it("processes room messages mentioned via displayName in formatted_body", async () => {
|
||||
const recordInboundSession = vi.fn(async () => {});
|
||||
const { handler } = createMatrixHandlerTestHarness({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Matrix plugin module implements handler behavior.
|
||||
import {
|
||||
buildChannelInboundEventContext,
|
||||
resolveInboundMentionDecision,
|
||||
toInboundMediaFacts,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { hasFinalInboundReplyDispatch } from "openclaw/plugin-sdk/channel-inbound";
|
||||
@@ -1156,16 +1157,25 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
? roomConfig?.requireMention
|
||||
: true
|
||||
: false;
|
||||
const shouldBypassMention =
|
||||
allowTextCommands &&
|
||||
isRoom &&
|
||||
shouldRequireMention &&
|
||||
!wasMentioned &&
|
||||
!hasExplicitMention &&
|
||||
commandAuthorized &&
|
||||
hasControlCommandInMessage;
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
// Matrix native mention metadata lets us reliably decide absence even
|
||||
// when no custom mention regex is configured.
|
||||
canDetectMention: true,
|
||||
wasMentioned,
|
||||
hasAnyMention: hasExplicitMention,
|
||||
},
|
||||
policy: {
|
||||
isGroup: isRoom,
|
||||
requireMention: shouldRequireMention,
|
||||
allowTextCommands,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
const { effectiveWasMentioned, shouldBypassMention } = mentionDecision;
|
||||
const canDetectMention = agentMentionRegexes.length > 0 || hasExplicitMention;
|
||||
if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) {
|
||||
if (mentionDecision.shouldSkip) {
|
||||
const pendingHistoryBody = preflightAudioTranscript
|
||||
? formatMatrixAudioTranscript(preflightAudioTranscript)
|
||||
: pendingHistoryText || pendingHistoryPollText;
|
||||
@@ -1368,6 +1378,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
isRoom,
|
||||
shouldRequireMention,
|
||||
wasMentioned,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention,
|
||||
canDetectMention,
|
||||
commandAuthorized,
|
||||
@@ -1438,6 +1449,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
isRoom,
|
||||
shouldRequireMention,
|
||||
wasMentioned,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention,
|
||||
canDetectMention,
|
||||
commandAuthorized,
|
||||
@@ -1680,7 +1692,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
isMentionableGroup: isRoom,
|
||||
requireMention: shouldRequireMention,
|
||||
canDetectMention,
|
||||
effectiveWasMentioned: wasMentioned || shouldBypassMention,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from "../commands/command-visibility.js";
|
||||
import { initCommands } from "../commands/slash-commands-impl.js";
|
||||
import { resolveGroupCommandLevelFromAccountConfig } from "../config/group.js";
|
||||
import { createNodeSessionStoreReader } from "../group/activation.js";
|
||||
import type { HistoryEntry } from "../group/history.js";
|
||||
import { claimMessageReply } from "../messaging/outbound-reply.js";
|
||||
import { setOutboundAudioPort } from "../messaging/outbound.js";
|
||||
@@ -96,16 +95,11 @@ export async function startGateway(ctx: CoreGatewayContext): Promise<void> {
|
||||
allowTextCommands: ctx.group?.allowTextCommands,
|
||||
isControlCommand: ctx.group?.isControlCommand,
|
||||
resolveIntroHint: ctx.group?.resolveIntroHint,
|
||||
sessionStoreReader: ctx.group?.sessionStoreReader,
|
||||
};
|
||||
const groupChatEnabled = groupOpts.enabled;
|
||||
const groupHistories: Map<string, HistoryEntry[]> | undefined = groupChatEnabled
|
||||
? new Map()
|
||||
: undefined;
|
||||
const sessionStoreReader = groupChatEnabled
|
||||
? (groupOpts.sessionStoreReader ?? createNodeSessionStoreReader())
|
||||
: undefined;
|
||||
|
||||
// Live config provider: per-inbound lookup so binding edits applied
|
||||
// through the CLI take effect without a gateway restart (#69546).
|
||||
const activeCfgProvider = createActiveCfgProvider({ fallback: ctx.cfg });
|
||||
@@ -135,7 +129,6 @@ export async function startGateway(ctx: CoreGatewayContext): Promise<void> {
|
||||
runtime,
|
||||
startTyping: (ev) => startTypingForEvent(ev, account, log),
|
||||
groupHistories,
|
||||
sessionStoreReader,
|
||||
allowTextCommands: groupOpts.allowTextCommands,
|
||||
isControlCommand: groupOpts.isControlCommand,
|
||||
resolveGroupIntroHint: groupOpts.resolveIntroHint,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ChannelIngressDecision } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
||||
import type { EngineAdapters } from "../adapter/index.js";
|
||||
import type { QQBotGroupCommandLevel } from "../config/group.js";
|
||||
import type { GroupActivationMode, SessionStoreReader } from "../group/activation.js";
|
||||
import type { GroupActivationMode } from "../group/activation.js";
|
||||
import type { HistoryEntry } from "../group/history.js";
|
||||
import type { GroupMessageGateResult } from "../group/message-gating.js";
|
||||
import type { QueuedMessage } from "./message-queue.js";
|
||||
@@ -77,7 +77,6 @@ export interface InboundPipelineDeps {
|
||||
keepAlive: TypingKeepAlive | null;
|
||||
}>;
|
||||
groupHistories?: Map<string, HistoryEntry[]>;
|
||||
sessionStoreReader?: SessionStoreReader;
|
||||
allowTextCommands?: boolean;
|
||||
isControlCommand?: (content: string) => boolean;
|
||||
resolveGroupIntroHint?: (params: {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
// Qqbot tests cover group gate command-level enforcement.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/session-store-runtime", () => ({
|
||||
getSessionEntry: vi.fn(() => undefined),
|
||||
resolveStorePath: vi.fn(() => "/state/agents/default/openclaw-agent.sqlite"),
|
||||
}));
|
||||
|
||||
import type { QQBotInboundAccess } from "../../adapter/index.js";
|
||||
import type { InboundPipelineDeps } from "../inbound-context.js";
|
||||
import type { QueuedMessage } from "../message-queue.js";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Qqbot plugin module implements group gate stage behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { HistoryPort } from "../../adapter/history.port.js";
|
||||
import type { QQBotInboundAccess } from "../../adapter/index.js";
|
||||
import { classifyCoreCommandForGroup } from "../../commands/command-visibility.js";
|
||||
@@ -38,7 +39,7 @@ interface GroupGateStageInput {
|
||||
export function runGroupGateStage(input: GroupGateStageInput): GroupGateStageResult {
|
||||
const { event, deps, accountId, agentId, sessionKey, userContent, processedAttachments } = input;
|
||||
const groupOpenid = event.groupOpenid!;
|
||||
const cfg = (deps.cfg ?? {}) as Record<string, unknown>;
|
||||
const cfg = (deps.cfg ?? {}) as OpenClawConfig;
|
||||
|
||||
const settings = resolveGroupSettings({ cfg, groupOpenid, accountId, agentId });
|
||||
const { historyLimit, requireMention, ignoreOtherMentions } = settings.config;
|
||||
@@ -65,7 +66,6 @@ export function runGroupGateStage(input: GroupGateStageInput): GroupGateStageRes
|
||||
agentId: agentId ?? "default",
|
||||
sessionKey,
|
||||
configRequireMention: requireMention,
|
||||
sessionStoreReader: deps.sessionStoreReader,
|
||||
});
|
||||
|
||||
const content = (event.content ?? "").trim();
|
||||
|
||||
@@ -199,11 +199,6 @@ interface GatewayGroupOptions {
|
||||
accountId: string;
|
||||
groupId: string;
|
||||
}) => string | undefined;
|
||||
/**
|
||||
* Session-store reader for the `/activation` command override. When
|
||||
* omitted, the engine loads a default node-based reader lazily.
|
||||
*/
|
||||
sessionStoreReader?: import("../group/activation.js").SessionStoreReader;
|
||||
}
|
||||
|
||||
/** Full gateway startup context. */
|
||||
|
||||
@@ -1,115 +1,71 @@
|
||||
// Qqbot tests cover activation plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveGroupActivation, type SessionStoreReader } from "./activation.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sessionStoreMocks = vi.hoisted(() => ({
|
||||
getSessionEntry: vi.fn(),
|
||||
resolveStorePath: vi.fn(() => "/state/agents/main/openclaw-agent.sqlite"),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/session-store-runtime", () => sessionStoreMocks);
|
||||
|
||||
import { resolveGroupActivation } from "./activation.js";
|
||||
|
||||
describe("engine/group/activation", () => {
|
||||
describe("resolveGroupActivation — no reader", () => {
|
||||
it("maps configRequireMention=true → mention", () => {
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "s",
|
||||
configRequireMention: true,
|
||||
}),
|
||||
).toBe("mention");
|
||||
});
|
||||
beforeEach(() => {
|
||||
sessionStoreMocks.getSessionEntry.mockReset();
|
||||
sessionStoreMocks.resolveStorePath.mockClear();
|
||||
});
|
||||
|
||||
it("maps configRequireMention=false → always", () => {
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "s",
|
||||
configRequireMention: false,
|
||||
}),
|
||||
).toBe("always");
|
||||
it.each([
|
||||
{ configRequireMention: true, expected: "mention" },
|
||||
{ configRequireMention: false, expected: "always" },
|
||||
] as const)("falls back to $expected when no override exists", (testCase) => {
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "missing",
|
||||
configRequireMention: testCase.configRequireMention,
|
||||
}),
|
||||
).toBe(testCase.expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ raw: "mention", configRequireMention: false, expected: "mention" },
|
||||
{ raw: "always", configRequireMention: true, expected: "always" },
|
||||
{ raw: " Always ", configRequireMention: true, expected: "always" },
|
||||
{ raw: "weird-mode", configRequireMention: true, expected: "mention" },
|
||||
] as const)("resolves session activation $raw as $expected", (testCase) => {
|
||||
sessionStoreMocks.getSessionEntry.mockReturnValue({ groupActivation: testCase.raw });
|
||||
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "k1",
|
||||
configRequireMention: testCase.configRequireMention,
|
||||
}),
|
||||
).toBe(testCase.expected);
|
||||
expect(sessionStoreMocks.resolveStorePath).toHaveBeenCalledWith(undefined, { agentId: "main" });
|
||||
expect(sessionStoreMocks.getSessionEntry).toHaveBeenCalledWith({
|
||||
storePath: "/state/agents/main/openclaw-agent.sqlite",
|
||||
agentId: "main",
|
||||
sessionKey: "k1",
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGroupActivation — with reader", () => {
|
||||
const makeReader = (
|
||||
store: Record<string, { groupActivation?: string }> | null,
|
||||
): SessionStoreReader => ({
|
||||
read: () => store,
|
||||
it("falls back when the session accessor fails", () => {
|
||||
sessionStoreMocks.getSessionEntry.mockImplementation(() => {
|
||||
throw new Error("unavailable");
|
||||
});
|
||||
|
||||
it("honours explicit session-store override (mention)", () => {
|
||||
const reader = makeReader({ k1: { groupActivation: "mention" } });
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "k1",
|
||||
configRequireMention: false,
|
||||
sessionStoreReader: reader,
|
||||
}),
|
||||
).toBe("mention");
|
||||
});
|
||||
|
||||
it("honours explicit session-store override (always)", () => {
|
||||
const reader = makeReader({ k1: { groupActivation: "always" } });
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "k1",
|
||||
configRequireMention: true,
|
||||
sessionStoreReader: reader,
|
||||
}),
|
||||
).toBe("always");
|
||||
});
|
||||
|
||||
it("ignores override when the key is absent", () => {
|
||||
const reader = makeReader({});
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "MISSING",
|
||||
configRequireMention: true,
|
||||
sessionStoreReader: reader,
|
||||
}),
|
||||
).toBe("mention");
|
||||
});
|
||||
|
||||
it("ignores reader errors (null) and falls back", () => {
|
||||
const reader = makeReader(null);
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "k1",
|
||||
configRequireMention: false,
|
||||
sessionStoreReader: reader,
|
||||
}),
|
||||
).toBe("always");
|
||||
});
|
||||
|
||||
it("ignores invalid activation values", () => {
|
||||
const reader = makeReader({ k1: { groupActivation: "weird-mode" } });
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "k1",
|
||||
configRequireMention: true,
|
||||
sessionStoreReader: reader,
|
||||
}),
|
||||
).toBe("mention");
|
||||
});
|
||||
|
||||
it("normalizes whitespace / case", () => {
|
||||
const reader = makeReader({ k1: { groupActivation: " Always " } });
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "k1",
|
||||
configRequireMention: true,
|
||||
sessionStoreReader: reader,
|
||||
}),
|
||||
).toBe("always");
|
||||
});
|
||||
expect(
|
||||
resolveGroupActivation({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
sessionKey: "k1",
|
||||
configRequireMention: false,
|
||||
}),
|
||||
).toBe("always");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,89 +1,32 @@
|
||||
// Qqbot plugin module implements activation behavior.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
normalizeGroupActivation,
|
||||
type GroupActivationMode,
|
||||
} from "openclaw/plugin-sdk/group-activation";
|
||||
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
|
||||
export type GroupActivationMode = "mention" | "always";
|
||||
|
||||
export interface SessionStoreReader {
|
||||
read(params: {
|
||||
cfg: Record<string, unknown>;
|
||||
agentId: string;
|
||||
}): Record<string, { groupActivation?: string }> | null;
|
||||
}
|
||||
export type { GroupActivationMode } from "openclaw/plugin-sdk/group-activation";
|
||||
|
||||
export function resolveGroupActivation(params: {
|
||||
cfg: Record<string, unknown>;
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
sessionKey: string;
|
||||
configRequireMention: boolean;
|
||||
sessionStoreReader?: SessionStoreReader;
|
||||
}): GroupActivationMode {
|
||||
const fallback: GroupActivationMode = params.configRequireMention ? "mention" : "always";
|
||||
|
||||
const store = params.sessionStoreReader?.read({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
if (!store) {
|
||||
try {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.agentId });
|
||||
const activation = normalizeGroupActivation(
|
||||
getSessionEntry({
|
||||
storePath,
|
||||
agentId: params.agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
})?.groupActivation,
|
||||
);
|
||||
return activation ?? fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const entry = store[params.sessionKey];
|
||||
if (!entry?.groupActivation) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const normalized = entry.groupActivation.trim().toLowerCase();
|
||||
if (normalized === "mention" || normalized === "always") {
|
||||
return normalized;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function resolveSessionStorePath(
|
||||
cfg: Record<string, unknown>,
|
||||
agentId: string | undefined,
|
||||
): string {
|
||||
const resolvedAgentId = agentId || "default";
|
||||
|
||||
const session =
|
||||
typeof cfg.session === "object" && cfg.session !== null
|
||||
? (cfg.session as { store?: unknown })
|
||||
: undefined;
|
||||
const rawStore = typeof session?.store === "string" ? session.store : undefined;
|
||||
|
||||
if (rawStore) {
|
||||
let expanded = rawStore;
|
||||
if (expanded.includes("{agentId}")) {
|
||||
expanded = expanded.replaceAll("{agentId}", resolvedAgentId);
|
||||
}
|
||||
if (expanded.startsWith("~")) {
|
||||
const home = process.env.HOME || process.env.USERPROFILE || "";
|
||||
expanded = expanded.replace(/^~/, home);
|
||||
}
|
||||
return path.resolve(expanded);
|
||||
}
|
||||
|
||||
const stateDir =
|
||||
process.env.OPENCLAW_STATE_DIR?.trim() ||
|
||||
process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
||||
path.join(process.env.HOME || process.env.USERPROFILE || "", ".openclaw");
|
||||
return path.join(stateDir, "agents", resolvedAgentId, "sessions", "sessions.json");
|
||||
}
|
||||
|
||||
export function createNodeSessionStoreReader(): SessionStoreReader {
|
||||
return {
|
||||
read: ({ cfg, agentId }) => {
|
||||
try {
|
||||
const storePath = resolveSessionStorePath(cfg, agentId);
|
||||
if (!fs.existsSync(storePath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(storePath, "utf-8");
|
||||
return JSON.parse(raw) as Record<string, { groupActivation?: string }>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
resolveThreadBindingSpawnPolicy,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { formatErrorMessage, formatUncaughtError } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { normalizeGroupActivation } from "openclaw/plugin-sdk/group-activation";
|
||||
import {
|
||||
isNativeCommandsExplicitlyDisabled,
|
||||
resolveNativeCommandsEnabled,
|
||||
@@ -311,11 +312,15 @@ export function createTelegramBotCore(
|
||||
if (!getSessionEntry) {
|
||||
return undefined;
|
||||
}
|
||||
const entry = getSessionEntry({ storePath, sessionKey });
|
||||
if (entry?.groupActivation === "always") {
|
||||
const storedActivation = getSessionEntry({ storePath, sessionKey })?.groupActivation;
|
||||
const activation =
|
||||
storedActivation === "mention" || storedActivation === "always"
|
||||
? normalizeGroupActivation(storedActivation)
|
||||
: undefined;
|
||||
if (activation === "always") {
|
||||
return false;
|
||||
}
|
||||
if (entry?.groupActivation === "mention") {
|
||||
if (activation === "mention") {
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -161,6 +161,7 @@ export const migratedBundledPluginSessionAccessorFiles = new Set([
|
||||
"extensions/mattermost/src/mattermost/model-picker.ts",
|
||||
"extensions/matrix/src/matrix/monitor/handler.ts",
|
||||
"extensions/matrix/src/session-route.ts",
|
||||
"extensions/qqbot/src/engine/group/activation.ts",
|
||||
"extensions/slack/src/monitor/slash.ts",
|
||||
"extensions/telegram/src/bot-core.ts",
|
||||
"extensions/telegram/src/bot-handlers.runtime.ts",
|
||||
|
||||
@@ -111,6 +111,7 @@ describe("session accessor boundary guard", () => {
|
||||
"extensions/mattermost/src/mattermost/model-picker.ts",
|
||||
"extensions/matrix/src/matrix/monitor/handler.ts",
|
||||
"extensions/matrix/src/session-route.ts",
|
||||
"extensions/qqbot/src/engine/group/activation.ts",
|
||||
"extensions/slack/src/monitor/slash.ts",
|
||||
"extensions/telegram/src/bot-core.ts",
|
||||
"extensions/telegram/src/bot-handlers.runtime.ts",
|
||||
|
||||
Reference in New Issue
Block a user