mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
Gate QQBot streaming command auth [AI] (#76375)
* fix: gate QQBot streaming command * addressing codex review * addressing review-skill * addressing review-skill * addressing codex review * addressing claude review * docs: add changelog entry for PR merge
This commit is contained in:
committed by
GitHub
parent
5d9752ba18
commit
1f724bc50b
@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gate QQBot streaming command auth [AI]. (#76375) Thanks @pgondhi987.
|
||||
- Plugins/release: make the published npm runtime verifier reject blank `openclaw.runtimeExtensions` entries instead of treating them as absent and passing via inferred outputs. Thanks @vincentkoc.
|
||||
- Web fetch: scope provider fallback cache entries by the selected fetch provider so config reloads cannot reuse another provider's cached fallback payload. Thanks @vincentkoc.
|
||||
- Web search: honor late-bound `tools.web.search.enabled: false` during tool execution so config reloads cannot leave an already-created `web_search` tool runnable. Thanks @vincentkoc.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
OpenClawPluginCommandDefinition,
|
||||
PluginCommandContext,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getWrittenQQBotConfig,
|
||||
installCommandRuntime,
|
||||
} from "../../engine/commands/slash-command-test-support.js";
|
||||
import { ensurePlatformAdapter } from "../bootstrap.js";
|
||||
import { registerQQBotFrameworkCommands } from "./framework-registration.js";
|
||||
|
||||
function createConfig(): OpenClawConfig {
|
||||
return {
|
||||
channels: {
|
||||
qqbot: {
|
||||
appId: "app",
|
||||
allowFrom: ["TRUSTED_OPENID"],
|
||||
streaming: false,
|
||||
accounts: {
|
||||
default: {
|
||||
allowFrom: ["TRUSTED_OPENID"],
|
||||
streaming: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function registerCommands(): OpenClawPluginCommandDefinition[] {
|
||||
ensurePlatformAdapter();
|
||||
const commands: OpenClawPluginCommandDefinition[] = [];
|
||||
const api = {
|
||||
logger: {},
|
||||
registerCommand: (command: OpenClawPluginCommandDefinition) => {
|
||||
commands.push(command);
|
||||
},
|
||||
} as unknown as OpenClawPluginApi;
|
||||
|
||||
registerQQBotFrameworkCommands(api);
|
||||
return commands;
|
||||
}
|
||||
|
||||
function findCommand(
|
||||
commands: OpenClawPluginCommandDefinition[],
|
||||
name: string,
|
||||
): OpenClawPluginCommandDefinition {
|
||||
const command = commands.find((entry) => entry.name === name);
|
||||
expect(command).toBeDefined();
|
||||
return command as OpenClawPluginCommandDefinition;
|
||||
}
|
||||
|
||||
function createCommandContext(
|
||||
config: OpenClawConfig,
|
||||
from: string | undefined,
|
||||
): PluginCommandContext {
|
||||
return {
|
||||
senderId: "TRUSTED_OPENID",
|
||||
channel: "qqbot",
|
||||
isAuthorizedSender: true,
|
||||
args: "on",
|
||||
commandBody: "/bot-streaming on",
|
||||
config,
|
||||
from,
|
||||
requestConversationBinding: async () => undefined,
|
||||
detachConversationBinding: async () => ({ removed: false }),
|
||||
getCurrentConversationBinding: async () => null,
|
||||
} as unknown as PluginCommandContext;
|
||||
}
|
||||
|
||||
describe("registerQQBotFrameworkCommands", () => {
|
||||
it("registers bot-streaming as an auth-gated framework command", () => {
|
||||
const command = findCommand(registerCommands(), "bot-streaming");
|
||||
|
||||
expect(command.requireAuth).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves the private-chat guard for bot-streaming on generic framework calls", async () => {
|
||||
const config = createConfig();
|
||||
const writes: OpenClawConfig[] = [];
|
||||
installCommandRuntime(config, writes);
|
||||
const command = findCommand(registerCommands(), "bot-streaming");
|
||||
|
||||
const missingFromResult = await command.handler(createCommandContext(config, undefined));
|
||||
const nonQQBotResult = await command.handler(createCommandContext(config, "generic:dm:user"));
|
||||
const groupResult = await command.handler(
|
||||
createCommandContext(config, "qqbot:group:GROUP_OPENID"),
|
||||
);
|
||||
|
||||
expect(missingFromResult).toEqual({ text: "💡 请在私聊中使用此指令" });
|
||||
expect(nonQQBotResult).toEqual({ text: "💡 请在私聊中使用此指令" });
|
||||
expect(groupResult).toEqual({ text: "💡 请在私聊中使用此指令" });
|
||||
expect(writes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("allows bot-streaming on explicit QQBot private-chat framework calls", async () => {
|
||||
const config = createConfig();
|
||||
const writes: OpenClawConfig[] = [];
|
||||
installCommandRuntime(config, writes);
|
||||
const command = findCommand(registerCommands(), "bot-streaming");
|
||||
|
||||
const result = await command.handler(createCommandContext(config, "qqbot:c2c:TRUSTED_OPENID"));
|
||||
|
||||
const qqbot = getWrittenQQBotConfig(writes[0]);
|
||||
expect(result).toMatchObject({ text: expect.stringContaining("已开启") });
|
||||
expect(writes).toHaveLength(1);
|
||||
expect(qqbot?.streaming).toBe(true);
|
||||
expect(qqbot?.accounts?.default?.streaming).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,20 @@ import { buildFrameworkSlashContext } from "./framework-context-adapter.js";
|
||||
import { parseQQBotFrom } from "./from-parser.js";
|
||||
import { dispatchFrameworkSlashResult } from "./result-dispatcher.js";
|
||||
|
||||
const PRIVATE_CHAT_ONLY_TEXT = "💡 请在私聊中使用此指令";
|
||||
|
||||
function isExplicitQQBotC2cFrom(from: string | undefined | null): boolean {
|
||||
const raw = (from ?? "").trim();
|
||||
const stripped = raw.replace(/^qqbot:/iu, "");
|
||||
const colonIdx = stripped.indexOf(":");
|
||||
if (colonIdx === -1) {
|
||||
return false;
|
||||
}
|
||||
const kind = stripped.slice(0, colonIdx).toLowerCase();
|
||||
const targetId = stripped.slice(colonIdx + 1).trim();
|
||||
return /^qqbot:/iu.test(raw) && kind === "c2c" && targetId.length > 0;
|
||||
}
|
||||
|
||||
export function registerQQBotFrameworkCommands(api: OpenClawPluginApi): void {
|
||||
for (const cmd of getFrameworkCommands()) {
|
||||
api.registerCommand({
|
||||
@@ -26,6 +40,10 @@ export function registerQQBotFrameworkCommands(api: OpenClawPluginApi): void {
|
||||
requireAuth: true,
|
||||
acceptsArgs: true,
|
||||
handler: async (ctx: PluginCommandContext) => {
|
||||
if (cmd.c2cOnly && !isExplicitQQBotC2cFrom(ctx.from)) {
|
||||
return { text: PRIVATE_CHAT_ONLY_TEXT };
|
||||
}
|
||||
|
||||
const from = parseQQBotFrom(ctx.from);
|
||||
const account = resolveQQBotAccount(ctx.config, ctx.accountId ?? undefined);
|
||||
const slashCtx = buildFrameworkSlashContext({
|
||||
|
||||
@@ -30,6 +30,7 @@ export function registerStreamingCommands(registry: SlashCommandRegistry): void
|
||||
registry.register({
|
||||
name: "bot-streaming",
|
||||
description: "一键开关流式消息",
|
||||
requireAuth: true,
|
||||
c2cOnly: true,
|
||||
usage: [
|
||||
`/bot-streaming on 开启流式消息`,
|
||||
|
||||
@@ -13,15 +13,53 @@
|
||||
|
||||
import { createQQBotSenderMatcher, normalizeQQBotAllowFrom } from "../access/index.js";
|
||||
|
||||
type SlashCommandAuthEntry = string | number;
|
||||
|
||||
function isSlashCommandAuthEntry(value: unknown): value is SlashCommandAuthEntry {
|
||||
return typeof value === "string" || typeof value === "number";
|
||||
}
|
||||
|
||||
function readSlashCommandAuthList(value: unknown): SlashCommandAuthEntry[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return value.filter(isSlashCommandAuthEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the command-specific QQBot allowlist from the root OpenClaw config.
|
||||
*
|
||||
* `commands.allowFrom.qqbot` takes precedence over the global
|
||||
* `commands.allowFrom["*"]`, matching the framework command authorization
|
||||
* contract used by registered plugin commands.
|
||||
*/
|
||||
export function resolveQQBotCommandsAllowFrom(cfg: unknown): SlashCommandAuthEntry[] | undefined {
|
||||
if (!cfg || typeof cfg !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const commands = (cfg as { commands?: unknown }).commands;
|
||||
if (!commands || typeof commands !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const allowFrom = (commands as { allowFrom?: unknown }).allowFrom;
|
||||
if (!allowFrom || typeof allowFrom !== "object" || Array.isArray(allowFrom)) {
|
||||
return undefined;
|
||||
}
|
||||
const byProvider = allowFrom as Record<string, unknown>;
|
||||
return readSlashCommandAuthList(byProvider.qqbot) ?? readSlashCommandAuthList(byProvider["*"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether `senderId` is authorized to execute `requireAuth`
|
||||
* slash commands for the given account configuration.
|
||||
*
|
||||
* Authorization rules:
|
||||
* - `commands.allowFrom.qqbot` / `commands.allowFrom["*"]` configured →
|
||||
* use that command-specific list instead of channel allowFrom
|
||||
* - `allowFrom` not configured / empty / only `["*"]` → **false**
|
||||
* (wildcard means "open to everyone", not explicit authorization)
|
||||
* - `allowFrom` contains at least one concrete entry AND sender
|
||||
* matches → **true**
|
||||
* matches a concrete entry → **true**
|
||||
* - Group messages use `groupAllowFrom` when present, falling back
|
||||
* to `allowFrom`.
|
||||
*/
|
||||
@@ -30,19 +68,21 @@ export function resolveSlashCommandAuth(params: {
|
||||
isGroup: boolean;
|
||||
allowFrom?: Array<string | number>;
|
||||
groupAllowFrom?: Array<string | number>;
|
||||
commandsAllowFrom?: Array<string | number>;
|
||||
}): boolean {
|
||||
const rawList =
|
||||
params.isGroup && params.groupAllowFrom && params.groupAllowFrom.length > 0
|
||||
params.commandsAllowFrom ??
|
||||
(params.isGroup && params.groupAllowFrom && params.groupAllowFrom.length > 0
|
||||
? params.groupAllowFrom
|
||||
: params.allowFrom;
|
||||
: params.allowFrom);
|
||||
|
||||
const normalized = normalizeQQBotAllowFrom(rawList);
|
||||
|
||||
// Require at least one explicit (non-wildcard) entry.
|
||||
const hasExplicitEntry = normalized.some((entry) => entry !== "*");
|
||||
if (!hasExplicitEntry) {
|
||||
// Require and match only explicit (non-wildcard) entries.
|
||||
const explicitEntries = normalized.filter((entry) => entry !== "*");
|
||||
if (explicitEntries.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return createQQBotSenderMatcher(params.senderId)(normalized);
|
||||
return createQQBotSenderMatcher(params.senderId)(explicitEntries);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { QueuedMessage } from "../gateway/message-queue.js";
|
||||
import type { GatewayAccount } from "../gateway/types.js";
|
||||
import { sendText } from "../messaging/sender.js";
|
||||
import { trySlashCommand } from "./slash-command-handler.js";
|
||||
import { getWrittenQQBotConfig, installCommandRuntime } from "./slash-command-test-support.js";
|
||||
|
||||
vi.mock("../messaging/outbound.js", () => ({
|
||||
sendDocument: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../messaging/sender.js", () => ({
|
||||
accountToCreds: vi.fn(() => ({ appId: "app", clientSecret: "" })),
|
||||
buildDeliveryTarget: vi.fn(() => ({ targetType: "c2c", targetId: "TRUSTED_OPENID" })),
|
||||
sendText: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
function createStreamingMessage(): QueuedMessage {
|
||||
return {
|
||||
type: "c2c",
|
||||
senderId: "TRUSTED_OPENID",
|
||||
content: "/bot-streaming on",
|
||||
messageId: "msg-1",
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function createAccount(): GatewayAccount {
|
||||
return {
|
||||
accountId: "default",
|
||||
appId: "app",
|
||||
clientSecret: "",
|
||||
markdownSupport: true,
|
||||
config: {
|
||||
allowFrom: ["*"],
|
||||
streaming: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("trySlashCommand", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(sendText).mockClear();
|
||||
});
|
||||
|
||||
it("honors commands.allowFrom for pre-dispatch bot-streaming in open DM configs", async () => {
|
||||
const writes: OpenClawConfig[] = [];
|
||||
const config: OpenClawConfig = {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
qqbot: ["TRUSTED_OPENID"],
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
qqbot: {
|
||||
allowFrom: ["*"],
|
||||
streaming: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
installCommandRuntime(config, writes);
|
||||
|
||||
const result = await trySlashCommand(createStreamingMessage(), {
|
||||
account: createAccount(),
|
||||
cfg: config,
|
||||
getMessagePeerId: () => "c2c:TRUSTED_OPENID",
|
||||
getQueueSnapshot: () => ({
|
||||
totalPending: 0,
|
||||
activeUsers: 0,
|
||||
maxConcurrentUsers: 1,
|
||||
senderPending: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
const qqbot = getWrittenQQBotConfig(writes[0]);
|
||||
expect(result).toBe("handled");
|
||||
expect(writes).toHaveLength(1);
|
||||
expect(qqbot?.streaming).toBe(true);
|
||||
expect(vi.mocked(sendText).mock.calls[0]?.[1]).toContain("已开启");
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
buildDeliveryTarget,
|
||||
accountToCreds,
|
||||
} from "../messaging/sender.js";
|
||||
import { resolveSlashCommandAuth } from "./slash-command-auth.js";
|
||||
import { resolveQQBotCommandsAllowFrom, resolveSlashCommandAuth } from "./slash-command-auth.js";
|
||||
import { matchSlashCommand } from "./slash-commands-impl.js";
|
||||
import type { SlashCommandContext, QueueSnapshot } from "./slash-commands.js";
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { SlashCommandContext, QueueSnapshot } from "./slash-commands.js";
|
||||
|
||||
export interface SlashCommandHandlerContext {
|
||||
account: GatewayAccount;
|
||||
cfg?: unknown;
|
||||
log?: EngineLogger;
|
||||
getMessagePeerId: (msg: QueuedMessage) => string;
|
||||
getQueueSnapshot: (peerId: string) => QueueSnapshot;
|
||||
@@ -81,6 +82,7 @@ export async function trySlashCommand(
|
||||
isGroup: msg.type === "group" || msg.type === "guild",
|
||||
allowFrom: account.config?.allowFrom,
|
||||
groupAllowFrom: account.config?.groupAllowFrom,
|
||||
commandsAllowFrom: resolveQQBotCommandsAllowFrom(ctx.cfg),
|
||||
}),
|
||||
queueSnapshot: ctx.getQueueSnapshot(peerId),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import type { CommandsPort } from "../adapter/commands.port.js";
|
||||
import { initCommands } from "./slash-commands-impl.js";
|
||||
|
||||
type RuntimeConfigApi = ReturnType<NonNullable<CommandsPort["approveRuntimeGetter"]>>["config"];
|
||||
type ReplaceConfigFile = RuntimeConfigApi["replaceConfigFile"];
|
||||
type ReplaceConfigFileResult = Awaited<ReturnType<ReplaceConfigFile>>;
|
||||
|
||||
export type WrittenQQBotConfig = {
|
||||
streaming?: unknown;
|
||||
accounts?: { default?: { streaming?: unknown } };
|
||||
};
|
||||
|
||||
export function installCommandRuntime(
|
||||
currentConfig: OpenClawConfig,
|
||||
writes: OpenClawConfig[],
|
||||
): void {
|
||||
const replaceConfigFile: ReplaceConfigFile = async (params) => {
|
||||
writes.push(params.nextConfig);
|
||||
return undefined as unknown as ReplaceConfigFileResult;
|
||||
};
|
||||
|
||||
initCommands({
|
||||
resolveVersion: () => "test",
|
||||
pluginVersion: "0.0.0-test",
|
||||
approveRuntimeGetter: () => ({
|
||||
config: {
|
||||
current: () => currentConfig,
|
||||
replaceConfigFile,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function getWrittenQQBotConfig(
|
||||
write: OpenClawConfig | undefined,
|
||||
): WrittenQQBotConfig | undefined {
|
||||
return write?.channels?.qqbot as WrittenQQBotConfig | undefined;
|
||||
}
|
||||
@@ -1,8 +1,179 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getFrameworkCommands } from "./slash-commands-impl.js";
|
||||
import { resolveQQBotCommandsAllowFrom, resolveSlashCommandAuth } from "./slash-command-auth.js";
|
||||
import { getWrittenQQBotConfig, installCommandRuntime } from "./slash-command-test-support.js";
|
||||
import { getFrameworkCommands, matchSlashCommand } from "./slash-commands-impl.js";
|
||||
import type { SlashCommandContext } from "./slash-commands.js";
|
||||
|
||||
function createStreamingContext(overrides: Partial<SlashCommandContext> = {}): SlashCommandContext {
|
||||
return {
|
||||
type: "c2c",
|
||||
senderId: "UNTRUSTED_OPENID",
|
||||
messageId: "msg-1",
|
||||
eventTimestamp: "2026-01-01T00:00:00.000Z",
|
||||
receivedAt: 1,
|
||||
rawContent: "/bot-streaming on",
|
||||
args: "",
|
||||
accountId: "default",
|
||||
appId: "app",
|
||||
accountConfig: { allowFrom: ["*"], streaming: false },
|
||||
commandAuthorized: false,
|
||||
queueSnapshot: {
|
||||
totalPending: 0,
|
||||
activeUsers: 0,
|
||||
maxConcurrentUsers: 1,
|
||||
senderPending: 0,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("QQBot framework slash commands", () => {
|
||||
it("routes bot-approve through the auth-gated framework registry", () => {
|
||||
expect(getFrameworkCommands().map((command) => command.name)).toContain("bot-approve");
|
||||
});
|
||||
|
||||
it("routes bot-streaming through the auth-gated framework registry", () => {
|
||||
expect(getFrameworkCommands().map((command) => command.name)).toContain("bot-streaming");
|
||||
});
|
||||
|
||||
it("does not write streaming config when the sender is not command-authorized", async () => {
|
||||
const writes: OpenClawConfig[] = [];
|
||||
installCommandRuntime(
|
||||
{
|
||||
channels: {
|
||||
qqbot: {
|
||||
allowFrom: ["*"],
|
||||
streaming: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
writes,
|
||||
);
|
||||
|
||||
const result = await matchSlashCommand(createStreamingContext());
|
||||
|
||||
expect(result).toContain("权限不足");
|
||||
expect(writes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("does not write streaming config when allowFrom mixes wildcard with another sender", async () => {
|
||||
const writes: OpenClawConfig[] = [];
|
||||
const allowFrom = ["*", "TRUSTED_OPENID"];
|
||||
installCommandRuntime(
|
||||
{
|
||||
channels: {
|
||||
qqbot: {
|
||||
allowFrom,
|
||||
streaming: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
writes,
|
||||
);
|
||||
|
||||
const commandAuthorized = resolveSlashCommandAuth({
|
||||
senderId: "UNTRUSTED_OPENID",
|
||||
isGroup: false,
|
||||
allowFrom,
|
||||
});
|
||||
const result = await matchSlashCommand(
|
||||
createStreamingContext({
|
||||
accountConfig: { allowFrom, streaming: false },
|
||||
commandAuthorized,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(commandAuthorized).toBe(false);
|
||||
expect(result).toContain("权限不足");
|
||||
expect(writes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("writes streaming config when commands.allowFrom grants the sender in open DM configs", async () => {
|
||||
const writes: OpenClawConfig[] = [];
|
||||
installCommandRuntime(
|
||||
{
|
||||
commands: {
|
||||
allowFrom: {
|
||||
qqbot: ["TRUSTED_OPENID"],
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
qqbot: {
|
||||
allowFrom: ["*"],
|
||||
streaming: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
writes,
|
||||
);
|
||||
|
||||
const commandAuthorized = resolveSlashCommandAuth({
|
||||
senderId: "TRUSTED_OPENID",
|
||||
isGroup: false,
|
||||
allowFrom: ["*"],
|
||||
commandsAllowFrom: resolveQQBotCommandsAllowFrom({
|
||||
commands: {
|
||||
allowFrom: {
|
||||
qqbot: ["TRUSTED_OPENID"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
const result = await matchSlashCommand(
|
||||
createStreamingContext({
|
||||
senderId: "TRUSTED_OPENID",
|
||||
accountConfig: { allowFrom: ["*"], streaming: false },
|
||||
commandAuthorized,
|
||||
}),
|
||||
);
|
||||
|
||||
const qqbot = getWrittenQQBotConfig(writes[0]);
|
||||
expect(commandAuthorized).toBe(true);
|
||||
expect(result).toContain("已开启");
|
||||
expect(writes).toHaveLength(1);
|
||||
expect(qqbot?.streaming).toBe(true);
|
||||
});
|
||||
|
||||
it("writes streaming config when the sender is command-authorized", async () => {
|
||||
const writes: OpenClawConfig[] = [];
|
||||
const allowFrom = ["*", "TRUSTED_OPENID"];
|
||||
installCommandRuntime(
|
||||
{
|
||||
channels: {
|
||||
qqbot: {
|
||||
allowFrom,
|
||||
streaming: false,
|
||||
accounts: {
|
||||
default: {
|
||||
allowFrom,
|
||||
streaming: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
writes,
|
||||
);
|
||||
|
||||
const commandAuthorized = resolveSlashCommandAuth({
|
||||
senderId: "TRUSTED_OPENID",
|
||||
isGroup: false,
|
||||
allowFrom,
|
||||
});
|
||||
const result = await matchSlashCommand(
|
||||
createStreamingContext({
|
||||
senderId: "TRUSTED_OPENID",
|
||||
accountConfig: { allowFrom, streaming: false },
|
||||
commandAuthorized,
|
||||
}),
|
||||
);
|
||||
|
||||
const qqbot = getWrittenQQBotConfig(writes[0]);
|
||||
expect(commandAuthorized).toBe(true);
|
||||
expect(result).toContain("已开启");
|
||||
expect(writes).toHaveLength(1);
|
||||
expect(qqbot?.streaming).toBe(true);
|
||||
expect(qqbot?.accounts?.default?.streaming).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface QQBotFrameworkCommand {
|
||||
name: string;
|
||||
description: string;
|
||||
usage?: string;
|
||||
c2cOnly?: boolean;
|
||||
handler: (ctx: SlashCommandContext) => SlashCommandResult | Promise<SlashCommandResult>;
|
||||
}
|
||||
|
||||
@@ -125,6 +126,7 @@ export class SlashCommandRegistry {
|
||||
name: cmd.name,
|
||||
description: cmd.description,
|
||||
usage: cmd.usage,
|
||||
c2cOnly: cmd.c2cOnly,
|
||||
handler: cmd.handler,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ export class GatewayConnection {
|
||||
// ---- Slash command interception ----
|
||||
const slashCtx: SlashCommandHandlerContext = {
|
||||
account,
|
||||
cfg: this.ctx.cfg,
|
||||
log,
|
||||
getMessagePeerId: (msg) => this.msgQueue.getMessagePeerId(msg),
|
||||
getQueueSnapshot: (peerId) => this.msgQueue.getSnapshot(peerId),
|
||||
|
||||
Reference in New Issue
Block a user