refactor: route channel runtime via plugin api
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { hasControlCommand, hasInlineCommandTokens } from "./command-detection.js";
|
||||
import { listChatCommands } from "./commands-registry.js";
|
||||
import { parseActivationCommand } from "./group-activation.js";
|
||||
import { parseSendPolicyCommand } from "./send-policy.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
describe("control command parsing", () => {
|
||||
it("requires slash for send policy", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { listChannelDocks } from "../channels/dock.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { listThinkingLevels } from "./thinking.js";
|
||||
import { COMMAND_ARG_FORMATTERS } from "./commands-args.js";
|
||||
import type { ChatCommandDefinition, CommandScope } from "./commands-registry.types.js";
|
||||
@@ -111,7 +112,12 @@ function assertCommandRegistry(commands: ChatCommandDefinition[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
||||
let cachedCommands: ChatCommandDefinition[] | null = null;
|
||||
let cachedRegistry: ReturnType<typeof getActivePluginRegistry> | null = null;
|
||||
let cachedNativeCommandSurfaces: Set<string> | null = null;
|
||||
let cachedNativeRegistry: ReturnType<typeof getActivePluginRegistry> | null = null;
|
||||
|
||||
function buildChatCommands(): ChatCommandDefinition[] {
|
||||
const commands: ChatCommandDefinition[] = [
|
||||
defineChatCommand({
|
||||
key: "help",
|
||||
@@ -454,17 +460,28 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
||||
|
||||
assertCommandRegistry(commands);
|
||||
return commands;
|
||||
})();
|
||||
}
|
||||
|
||||
let cachedNativeCommandSurfaces: Set<string> | null = null;
|
||||
export function getChatCommands(): ChatCommandDefinition[] {
|
||||
const registry = getActivePluginRegistry();
|
||||
if (cachedCommands && registry === cachedRegistry) return cachedCommands;
|
||||
const commands = buildChatCommands();
|
||||
cachedCommands = commands;
|
||||
cachedRegistry = registry;
|
||||
cachedNativeCommandSurfaces = null;
|
||||
return commands;
|
||||
}
|
||||
|
||||
export const getNativeCommandSurfaces = (): Set<string> => {
|
||||
if (!cachedNativeCommandSurfaces) {
|
||||
cachedNativeCommandSurfaces = new Set(
|
||||
listChannelDocks()
|
||||
.filter((dock) => dock.capabilities.nativeCommands)
|
||||
.map((dock) => dock.id),
|
||||
);
|
||||
export function getNativeCommandSurfaces(): Set<string> {
|
||||
const registry = getActivePluginRegistry();
|
||||
if (cachedNativeCommandSurfaces && registry === cachedNativeRegistry) {
|
||||
return cachedNativeCommandSurfaces;
|
||||
}
|
||||
cachedNativeCommandSurfaces = new Set(
|
||||
listChannelDocks()
|
||||
.filter((dock) => dock.capabilities.nativeCommands)
|
||||
.map((dock) => dock.id),
|
||||
);
|
||||
cachedNativeRegistry = registry;
|
||||
return cachedNativeCommandSurfaces;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildCommandText,
|
||||
@@ -10,6 +10,16 @@ import {
|
||||
normalizeCommandBody,
|
||||
shouldHandleTextCommands,
|
||||
} from "./commands-registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
describe("commands registry", () => {
|
||||
it("builds command text with args", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||
import { CHAT_COMMANDS, getNativeCommandSurfaces } from "./commands-registry.data.js";
|
||||
import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import type {
|
||||
@@ -16,7 +16,6 @@ import type {
|
||||
ShouldHandleTextCommandsParams,
|
||||
} from "./commands-registry.types.js";
|
||||
|
||||
export { CHAT_COMMANDS } from "./commands-registry.data.js";
|
||||
export type {
|
||||
ChatCommandDefinition,
|
||||
CommandArgChoiceContext,
|
||||
@@ -37,9 +36,16 @@ type TextAliasSpec = {
|
||||
acceptsArgs: boolean;
|
||||
};
|
||||
|
||||
const TEXT_ALIAS_MAP: Map<string, TextAliasSpec> = (() => {
|
||||
let cachedTextAliasMap: Map<string, TextAliasSpec> | null = null;
|
||||
let cachedTextAliasCommands: ChatCommandDefinition[] | null = null;
|
||||
let cachedDetection: CommandDetection | undefined;
|
||||
let cachedDetectionCommands: ChatCommandDefinition[] | null = null;
|
||||
|
||||
function getTextAliasMap(): Map<string, TextAliasSpec> {
|
||||
const commands = getChatCommands();
|
||||
if (cachedTextAliasMap && cachedTextAliasCommands === commands) return cachedTextAliasMap;
|
||||
const map = new Map<string, TextAliasSpec>();
|
||||
for (const command of CHAT_COMMANDS) {
|
||||
for (const command of commands) {
|
||||
// Canonicalize to the *primary* text alias, not `/${key}`. Some command keys are
|
||||
// internal identifiers (e.g. `dock:telegram`) while the public text command is
|
||||
// the alias (e.g. `/dock-telegram`).
|
||||
@@ -53,10 +59,10 @@ const TEXT_ALIAS_MAP: Map<string, TextAliasSpec> = (() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedTextAliasMap = map;
|
||||
cachedTextAliasCommands = commands;
|
||||
return map;
|
||||
})();
|
||||
|
||||
let cachedDetection: CommandDetection | undefined;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
@@ -78,8 +84,9 @@ function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatC
|
||||
export function listChatCommands(params?: {
|
||||
skillCommands?: SkillCommandSpec[];
|
||||
}): ChatCommandDefinition[] {
|
||||
if (!params?.skillCommands?.length) return [...CHAT_COMMANDS];
|
||||
return [...CHAT_COMMANDS, ...buildSkillCommandDefinitions(params.skillCommands)];
|
||||
const commands = getChatCommands();
|
||||
if (!params?.skillCommands?.length) return [...commands];
|
||||
return [...commands, ...buildSkillCommandDefinitions(params.skillCommands)];
|
||||
}
|
||||
|
||||
export function isCommandEnabled(cfg: ClawdbotConfig, commandKey: string): boolean {
|
||||
@@ -93,7 +100,7 @@ export function listChatCommandsForConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
params?: { skillCommands?: SkillCommandSpec[] },
|
||||
): ChatCommandDefinition[] {
|
||||
const base = CHAT_COMMANDS.filter((command) => isCommandEnabled(cfg, command.key));
|
||||
const base = getChatCommands().filter((command) => isCommandEnabled(cfg, command.key));
|
||||
if (!params?.skillCommands?.length) return base;
|
||||
return [...base, ...buildSkillCommandDefinitions(params.skillCommands)];
|
||||
}
|
||||
@@ -127,7 +134,7 @@ export function listNativeCommandSpecsForConfig(
|
||||
|
||||
export function findCommandByNativeName(name: string): ChatCommandDefinition | undefined {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
return CHAT_COMMANDS.find(
|
||||
return getChatCommands().find(
|
||||
(command) => command.scope !== "text" && command.nativeName?.toLowerCase() === normalized,
|
||||
);
|
||||
}
|
||||
@@ -299,14 +306,15 @@ export function normalizeCommandBody(raw: string, options?: CommandNormalizeOpti
|
||||
: normalized;
|
||||
|
||||
const lowered = commandBody.toLowerCase();
|
||||
const exact = TEXT_ALIAS_MAP.get(lowered);
|
||||
const textAliasMap = getTextAliasMap();
|
||||
const exact = textAliasMap.get(lowered);
|
||||
if (exact) return exact.canonical;
|
||||
|
||||
const tokenMatch = commandBody.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!tokenMatch) return commandBody;
|
||||
const [, token, rest] = tokenMatch;
|
||||
const tokenKey = `/${token.toLowerCase()}`;
|
||||
const tokenSpec = TEXT_ALIAS_MAP.get(tokenKey);
|
||||
const tokenSpec = textAliasMap.get(tokenKey);
|
||||
if (!tokenSpec) return commandBody;
|
||||
if (rest && !tokenSpec.acceptsArgs) return commandBody;
|
||||
const normalizedRest = rest?.trimStart();
|
||||
@@ -319,10 +327,11 @@ export function isCommandMessage(raw: string): boolean {
|
||||
}
|
||||
|
||||
export function getCommandDetection(_cfg?: ClawdbotConfig): CommandDetection {
|
||||
if (cachedDetection) return cachedDetection;
|
||||
const commands = getChatCommands();
|
||||
if (cachedDetection && cachedDetectionCommands === commands) return cachedDetection;
|
||||
const exact = new Set<string>();
|
||||
const patterns: string[] = [];
|
||||
for (const cmd of CHAT_COMMANDS) {
|
||||
for (const cmd of commands) {
|
||||
for (const alias of cmd.textAliases) {
|
||||
const normalized = alias.trim().toLowerCase();
|
||||
if (!normalized) continue;
|
||||
@@ -340,6 +349,7 @@ export function getCommandDetection(_cfg?: ClawdbotConfig): CommandDetection {
|
||||
exact,
|
||||
regex: patterns.length ? new RegExp(`^(?:${patterns.join("|")})$`, "i") : /$^/,
|
||||
};
|
||||
cachedDetectionCommands = commands;
|
||||
return cachedDetection;
|
||||
}
|
||||
|
||||
@@ -353,7 +363,7 @@ export function maybeResolveTextAlias(raw: string, cfg?: ClawdbotConfig) {
|
||||
const tokenMatch = normalized.match(/^\/([^\s:]+)(?:\s|$)/);
|
||||
if (!tokenMatch) return null;
|
||||
const tokenKey = `/${tokenMatch[1]}`;
|
||||
return TEXT_ALIAS_MAP.has(tokenKey) ? tokenKey : null;
|
||||
return getTextAliasMap().has(tokenKey) ? tokenKey : null;
|
||||
}
|
||||
|
||||
export function resolveTextCommand(
|
||||
@@ -366,9 +376,9 @@ export function resolveTextCommand(
|
||||
const trimmed = normalizeCommandBody(raw).trim();
|
||||
const alias = maybeResolveTextAlias(trimmed, cfg);
|
||||
if (!alias) return null;
|
||||
const spec = TEXT_ALIAS_MAP.get(alias);
|
||||
const spec = getTextAliasMap().get(alias);
|
||||
if (!spec) return null;
|
||||
const command = CHAT_COMMANDS.find((entry) => entry.key === spec.key);
|
||||
const command = getChatCommands().find((entry) => entry.key === spec.key);
|
||||
if (!command) return null;
|
||||
if (!spec.acceptsArgs) return { command };
|
||||
const args = trimmed.slice(alias.length).trim();
|
||||
|
||||
@@ -48,6 +48,7 @@ vi.mock("../../telegram/send.js", () => ({
|
||||
}));
|
||||
vi.mock("../../web/outbound.js", () => ({
|
||||
sendMessageWhatsApp: mocks.sendMessageWhatsApp,
|
||||
sendPollWhatsApp: mocks.sendMessageWhatsApp,
|
||||
}));
|
||||
vi.mock("../../infra/outbound/deliver.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../infra/outbound/deliver.js")>(
|
||||
|
||||
Reference in New Issue
Block a user