build: fix ineffective dynamic imports with lazy boundaries (#33690)

Merged via squash.

Prepared head SHA: 38b3c23d6f
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana
2026-03-03 20:14:41 -05:00
committed by GitHub
parent a4850b1b8f
commit 21e8d88c1d
31 changed files with 330 additions and 153 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
- Gateway/security default response headers: add `Permissions-Policy: camera=(), microphone=(), geolocation=()` to baseline gateway HTTP security headers for all responses. (#30186) thanks @habakan.
- Plugins/startup loading: lazily initialize plugin runtime, split startup-critical plugin SDK imports into `openclaw/plugin-sdk/core` and `openclaw/plugin-sdk/telegram`, and preserve `api.runtime` reflection semantics for plugin compatibility. (#28620) thanks @hmemcpy.
- Build/lazy runtime boundaries: replace ineffective dynamic import sites with dedicated lazy runtime boundaries across Slack slash handling, Telegram audit, CLI send deps, memory fallback, and outbound delivery paths while preserving behavior. (#33690) thanks @gumadeiras.
- Security/auth labels: remove token and API-key snippets from user-facing auth status labels so `/status` and `/models` do not expose credential fragments. (#33262) thanks @cu1ch3n.
- Security/audit denyCommands guidance: suggest likely exact node command IDs for unknown `gateway.nodes.denyCommands` entries so ineffective denylist entries are easier to correct. (#29713) thanks @liquidhorizon88-bot.
- Docs/security hardening guidance: document Docker `DOCKER-USER` + UFW policy and add cross-linking from Docker install docs for VPS/public-host setups. (#27613) thanks @dorukardahan.

View File

@@ -0,0 +1 @@
export { pruneStaleCommandPolls } from "./command-poll-backoff.js";

View File

@@ -0,0 +1,7 @@
export { getDiagnosticSessionState } from "../logging/diagnostic-session-state.js";
export { logToolLoopAction } from "../logging/diagnostic.js";
export {
detectToolCallLoop,
recordToolCall,
recordToolCallOutcome,
} from "./tool-loop-detection.js";

View File

@@ -23,6 +23,14 @@ const adjustedParamsByToolCallId = new Map<string, unknown>();
const MAX_TRACKED_ADJUSTED_PARAMS = 1024;
const LOOP_WARNING_BUCKET_SIZE = 10;
const MAX_LOOP_WARNING_KEYS = 256;
let beforeToolCallRuntimePromise: Promise<
typeof import("./pi-tools.before-tool-call.runtime.js")
> | null = null;
function loadBeforeToolCallRuntime() {
beforeToolCallRuntimePromise ??= import("./pi-tools.before-tool-call.runtime.js");
return beforeToolCallRuntimePromise;
}
function buildAdjustedParamsKey(params: { runId?: string; toolCallId: string }): string {
if (params.runId && params.runId.trim()) {
@@ -62,8 +70,7 @@ async function recordLoopOutcome(args: {
return;
}
try {
const { getDiagnosticSessionState } = await import("../logging/diagnostic-session-state.js");
const { recordToolCallOutcome } = await import("./tool-loop-detection.js");
const { getDiagnosticSessionState, recordToolCallOutcome } = await loadBeforeToolCallRuntime();
const sessionState = getDiagnosticSessionState({
sessionKey: args.ctx.sessionKey,
sessionId: args.ctx?.agentId,
@@ -91,10 +98,8 @@ export async function runBeforeToolCallHook(args: {
const params = args.params;
if (args.ctx?.sessionKey) {
const { getDiagnosticSessionState } = await import("../logging/diagnostic-session-state.js");
const { logToolLoopAction } = await import("../logging/diagnostic.js");
const { detectToolCallLoop, recordToolCall } = await import("./tool-loop-detection.js");
const { getDiagnosticSessionState, logToolLoopAction, detectToolCallLoop, recordToolCall } =
await loadBeforeToolCallRuntime();
const sessionState = getDiagnosticSessionState({
sessionKey: args.ctx.sessionKey,
sessionId: args.ctx?.agentId,

View File

@@ -49,6 +49,15 @@ const FAST_TEST_RETRY_INTERVAL_MS = 8;
const FAST_TEST_REPLY_CHANGE_WAIT_MS = 20;
const DEFAULT_SUBAGENT_ANNOUNCE_TIMEOUT_MS = 60_000;
const MAX_TIMER_SAFE_TIMEOUT_MS = 2_147_000_000;
let subagentRegistryRuntimePromise: Promise<
typeof import("./subagent-registry-runtime.js")
> | null = null;
function loadSubagentRegistryRuntime() {
subagentRegistryRuntimePromise ??= import("./subagent-registry-runtime.js");
return subagentRegistryRuntimePromise;
}
const DIRECT_ANNOUNCE_TRANSIENT_RETRY_DELAYS_MS = FAST_TEST_MODE
? ([8, 16, 32] as const)
: ([5_000, 10_000, 20_000] as const);
@@ -773,12 +782,9 @@ async function sendSubagentAnnounceDirectly(params: {
if (!forceBoundSessionDirectDelivery) {
let pendingDescendantRuns = 0;
try {
const {
countPendingDescendantRuns,
countPendingDescendantRunsExcludingRun,
countActiveDescendantRuns,
} = await import("./subagent-registry.js");
if (params.currentRunId && typeof countPendingDescendantRunsExcludingRun === "function") {
const { countPendingDescendantRuns, countPendingDescendantRunsExcludingRun } =
await loadSubagentRegistryRuntime();
if (params.currentRunId) {
pendingDescendantRuns = Math.max(
0,
countPendingDescendantRunsExcludingRun(
@@ -789,9 +795,7 @@ async function sendSubagentAnnounceDirectly(params: {
} else {
pendingDescendantRuns = Math.max(
0,
typeof countPendingDescendantRuns === "function"
? countPendingDescendantRuns(canonicalRequesterSessionKey)
: countActiveDescendantRuns(canonicalRequesterSessionKey),
countPendingDescendantRuns(canonicalRequesterSessionKey),
);
}
} catch {
@@ -1224,14 +1228,8 @@ export async function runSubagentAnnounceFlow(params: {
let pendingChildDescendantRuns = 0;
try {
const { countPendingDescendantRuns, countActiveDescendantRuns } =
await import("./subagent-registry.js");
pendingChildDescendantRuns = Math.max(
0,
typeof countPendingDescendantRuns === "function"
? countPendingDescendantRuns(params.childSessionKey)
: countActiveDescendantRuns(params.childSessionKey),
);
const { countPendingDescendantRuns } = await loadSubagentRegistryRuntime();
pendingChildDescendantRuns = Math.max(0, countPendingDescendantRuns(params.childSessionKey));
} catch {
// Best-effort only; fall back to direct announce behavior when unavailable.
}
@@ -1281,7 +1279,7 @@ export async function runSubagentAnnounceFlow(params: {
// still receive the announce — injecting will start a new agent turn.
if (requesterIsSubagent) {
const { isSubagentSessionRunActive, resolveRequesterForChildSession } =
await import("./subagent-registry.js");
await loadSubagentRegistryRuntime();
if (!isSubagentSessionRunActive(targetRequesterSessionKey)) {
// Parent run has ended. Check if parent SESSION still exists.
// If it does, the parent may be waiting for child results — inject there.
@@ -1314,7 +1312,7 @@ export async function runSubagentAnnounceFlow(params: {
let remainingActiveSubagentRuns = 0;
try {
const { countActiveDescendantRuns } = await import("./subagent-registry.js");
const { countActiveDescendantRuns } = await loadSubagentRegistryRuntime();
remainingActiveSubagentRuns = Math.max(
0,
countActiveDescendantRuns(targetRequesterSessionKey),

View File

@@ -0,0 +1,7 @@
export {
countActiveDescendantRuns,
countPendingDescendantRuns,
countPendingDescendantRunsExcludingRun,
isSubagentSessionRunActive,
resolveRequesterForChildSession,
} from "./subagent-registry.js";

View File

@@ -18,6 +18,15 @@ import type { ReplyPayload } from "../types.js";
import { normalizeReplyPayload } from "./normalize-reply.js";
import { shouldSuppressReasoningPayload } from "./reply-payloads.js";
let deliverRuntimePromise: Promise<
typeof import("../../infra/outbound/deliver-runtime.js")
> | null = null;
function loadDeliverRuntime() {
deliverRuntimePromise ??= import("../../infra/outbound/deliver-runtime.js");
return deliverRuntimePromise;
}
export type RouteReplyParams = {
/** The reply payload to send. */
payload: ReplyPayload;
@@ -126,7 +135,7 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
try {
// Provider docking: this is an execution boundary (we're about to send).
// Keep the module cheap to import by loading outbound plumbing lazily.
const { deliverOutboundPayloads } = await import("../../infra/outbound/deliver.js");
const { deliverOutboundPayloads } = await loadDeliverRuntime();
const outboundSession = buildOutboundSessionContext({
cfg,
agentId: resolvedAgentId,

View File

@@ -0,0 +1 @@
export { sendMessageDiscord } from "../discord/send.js";

View File

@@ -0,0 +1 @@
export { sendMessageIMessage } from "../imessage/send.js";

View File

@@ -0,0 +1 @@
export { sendMessageSignal } from "../signal/send.js";

View File

@@ -0,0 +1 @@
export { sendMessageSlack } from "../slack/send.js";

View File

@@ -0,0 +1 @@
export { sendMessageTelegram } from "../telegram/send.js";

View File

@@ -0,0 +1 @@
export { sendMessageWhatsApp } from "../channels/web/index.js";

View File

@@ -16,30 +16,72 @@ export type CliDeps = {
sendMessageIMessage: typeof sendMessageIMessage;
};
let whatsappSenderRuntimePromise: Promise<typeof import("./deps-send-whatsapp.runtime.js")> | null =
null;
let telegramSenderRuntimePromise: Promise<typeof import("./deps-send-telegram.runtime.js")> | null =
null;
let discordSenderRuntimePromise: Promise<typeof import("./deps-send-discord.runtime.js")> | null =
null;
let slackSenderRuntimePromise: Promise<typeof import("./deps-send-slack.runtime.js")> | null = null;
let signalSenderRuntimePromise: Promise<typeof import("./deps-send-signal.runtime.js")> | null =
null;
let imessageSenderRuntimePromise: Promise<typeof import("./deps-send-imessage.runtime.js")> | null =
null;
function loadWhatsAppSenderRuntime() {
whatsappSenderRuntimePromise ??= import("./deps-send-whatsapp.runtime.js");
return whatsappSenderRuntimePromise;
}
function loadTelegramSenderRuntime() {
telegramSenderRuntimePromise ??= import("./deps-send-telegram.runtime.js");
return telegramSenderRuntimePromise;
}
function loadDiscordSenderRuntime() {
discordSenderRuntimePromise ??= import("./deps-send-discord.runtime.js");
return discordSenderRuntimePromise;
}
function loadSlackSenderRuntime() {
slackSenderRuntimePromise ??= import("./deps-send-slack.runtime.js");
return slackSenderRuntimePromise;
}
function loadSignalSenderRuntime() {
signalSenderRuntimePromise ??= import("./deps-send-signal.runtime.js");
return signalSenderRuntimePromise;
}
function loadIMessageSenderRuntime() {
imessageSenderRuntimePromise ??= import("./deps-send-imessage.runtime.js");
return imessageSenderRuntimePromise;
}
export function createDefaultDeps(): CliDeps {
return {
sendMessageWhatsApp: async (...args) => {
const { sendMessageWhatsApp } = await import("../channels/web/index.js");
const { sendMessageWhatsApp } = await loadWhatsAppSenderRuntime();
return await sendMessageWhatsApp(...args);
},
sendMessageTelegram: async (...args) => {
const { sendMessageTelegram } = await import("../telegram/send.js");
const { sendMessageTelegram } = await loadTelegramSenderRuntime();
return await sendMessageTelegram(...args);
},
sendMessageDiscord: async (...args) => {
const { sendMessageDiscord } = await import("../discord/send.js");
const { sendMessageDiscord } = await loadDiscordSenderRuntime();
return await sendMessageDiscord(...args);
},
sendMessageSlack: async (...args) => {
const { sendMessageSlack } = await import("../slack/send.js");
const { sendMessageSlack } = await loadSlackSenderRuntime();
return await sendMessageSlack(...args);
},
sendMessageSignal: async (...args) => {
const { sendMessageSignal } = await import("../signal/send.js");
const { sendMessageSignal } = await loadSignalSenderRuntime();
return await sendMessageSignal(...args);
},
sendMessageIMessage: async (...args) => {
const { sendMessageIMessage } = await import("../imessage/send.js");
const { sendMessageIMessage } = await loadIMessageSenderRuntime();
return await sendMessageIMessage(...args);
},
};

View File

@@ -0,0 +1 @@
export { deliverOutboundPayloads } from "./deliver.js";

View File

@@ -15,6 +15,12 @@ type WarningParams = {
const warnedContexts = new Map<string, string>();
const log = createSubsystemLogger("session-maintenance-warning");
let deliverRuntimePromise: Promise<typeof import("./outbound/deliver-runtime.js")> | null = null;
function loadDeliverRuntime() {
deliverRuntimePromise ??= import("./outbound/deliver-runtime.js");
return deliverRuntimePromise;
}
function shouldSendWarning(): boolean {
return !process.env.VITEST && process.env.NODE_ENV !== "test";
@@ -95,7 +101,7 @@ export async function deliverSessionMaintenanceWarning(params: WarningParams): P
}
try {
const { deliverOutboundPayloads } = await import("./outbound/deliver.js");
const { deliverOutboundPayloads } = await loadDeliverRuntime();
const outboundSession = buildOutboundSessionContext({
cfg: params.cfg,
sessionKey: params.sessionKey,

View File

@@ -25,6 +25,14 @@ let lastActivityAt = 0;
const DEFAULT_STUCK_SESSION_WARN_MS = 120_000;
const MIN_STUCK_SESSION_WARN_MS = 1_000;
const MAX_STUCK_SESSION_WARN_MS = 24 * 60 * 60 * 1000;
let commandPollBackoffRuntimePromise: Promise<
typeof import("../agents/command-poll-backoff.runtime.js")
> | null = null;
function loadCommandPollBackoffRuntime() {
commandPollBackoffRuntimePromise ??= import("../agents/command-poll-backoff.runtime.js");
return commandPollBackoffRuntimePromise;
}
function markActivity() {
lastActivityAt = Date.now();
@@ -376,7 +384,7 @@ export function startDiagnosticHeartbeat(config?: OpenClawConfig) {
queued: totalQueued,
});
import("../agents/command-poll-backoff.js")
void loadCommandPollBackoffRuntime()
.then(({ pruneStaleCommandPolls }) => {
for (const [, state] of diagnosticSessionStates) {
pruneStaleCommandPolls(state);

View File

@@ -3,6 +3,14 @@ import type { OpenClawConfig } from "../config/config.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import { isDeliverableMessageChannel } from "../utils/message-channel.js";
let deliverRuntimePromise: Promise<typeof import("../infra/outbound/deliver-runtime.js")> | null =
null;
function loadDeliverRuntime() {
deliverRuntimePromise ??= import("../infra/outbound/deliver-runtime.js");
return deliverRuntimePromise;
}
export const DEFAULT_ECHO_TRANSCRIPT_FORMAT = '📝 "{transcript}"';
function formatEchoTranscript(transcript: string, format: string): string {
@@ -43,7 +51,7 @@ export async function sendTranscriptEcho(params: {
const text = formatEchoTranscript(transcript, params.format ?? DEFAULT_ECHO_TRANSCRIPT_FORMAT);
try {
const { deliverOutboundPayloads } = await import("../infra/outbound/deliver.js");
const { deliverOutboundPayloads } = await loadDeliverRuntime();
await deliverOutboundPayloads({
cfg,
channel: normalizedChannel,

View File

@@ -0,0 +1 @@
export { describeImageWithModel } from "./image.js";

View File

@@ -0,0 +1 @@
export { MemoryIndexManager } from "./manager.js";

View File

@@ -10,6 +10,12 @@ import type {
const log = createSubsystemLogger("memory");
const QMD_MANAGER_CACHE = new Map<string, MemorySearchManager>();
let managerRuntimePromise: Promise<typeof import("./manager-runtime.js")> | null = null;
function loadManagerRuntime() {
managerRuntimePromise ??= import("./manager-runtime.js");
return managerRuntimePromise;
}
export type MemorySearchManagerResult = {
manager: MemorySearchManager | null;
@@ -48,7 +54,7 @@ export async function getMemorySearchManager(params: {
{
primary,
fallbackFactory: async () => {
const { MemoryIndexManager } = await import("./manager.js");
const { MemoryIndexManager } = await loadManagerRuntime();
return await MemoryIndexManager.get(params);
},
},
@@ -70,7 +76,7 @@ export async function getMemorySearchManager(params: {
}
try {
const { MemoryIndexManager } = await import("./manager.js");
const { MemoryIndexManager } = await loadManagerRuntime();
const manager = await MemoryIndexManager.get(params);
return { manager };
} catch (err) {

View File

@@ -0,0 +1 @@
export { loginWeb } from "../../web/login.js";

View File

@@ -0,0 +1 @@
export { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";

View File

@@ -55,21 +55,22 @@ const handleWhatsAppActionLazy: PluginRuntime["channel"]["whatsapp"]["handleWhat
return handleWhatsAppAction(...args);
};
let webOutboundPromise: Promise<typeof import("../../web/outbound.js")> | null = null;
let webLoginPromise: Promise<typeof import("../../web/login.js")> | null = null;
let webLoginQrPromise: Promise<typeof import("../../web/login-qr.js")> | null = null;
let webChannelPromise: Promise<typeof import("../../channels/web/index.js")> | null = null;
let webOutboundPromise: Promise<typeof import("./runtime-whatsapp-outbound.runtime.js")> | null =
null;
let webLoginPromise: Promise<typeof import("./runtime-whatsapp-login.runtime.js")> | null = null;
let whatsappActionsPromise: Promise<
typeof import("../../agents/tools/whatsapp-actions.js")
> | null = null;
function loadWebOutbound() {
webOutboundPromise ??= import("../../web/outbound.js");
webOutboundPromise ??= import("./runtime-whatsapp-outbound.runtime.js");
return webOutboundPromise;
}
function loadWebLogin() {
webLoginPromise ??= import("../../web/login.js");
webLoginPromise ??= import("./runtime-whatsapp-login.runtime.js");
return webLoginPromise;
}

View File

@@ -0,0 +1,7 @@
export {
buildCommandTextFromArgs,
findCommandByNativeName,
listNativeCommandSpecsForConfig,
parseCommandArgs,
resolveCommandArgMenu,
} from "../../auto-reply/commands-registry.js";

View File

@@ -0,0 +1,9 @@
export { resolveChunkMode } from "../../auto-reply/chunk.js";
export { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
export { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
export { resolveConversationLabel } from "../../channels/conversation-label.js";
export { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
export { recordInboundSessionMetaSafe } from "../../channels/session-meta.js";
export { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
export { resolveAgentRoute } from "../../routing/resolve-route.js";
export { deliverSlackSlashReplies } from "./replies.js";

View File

@@ -0,0 +1 @@
export { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js";

View File

@@ -1,5 +1,8 @@
import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt";
import type { ChatCommandDefinition, CommandArgs } from "../../auto-reply/commands-registry.js";
import {
type ChatCommandDefinition,
type CommandArgs,
} from "../../auto-reply/commands-registry.js";
import type { ReplyPayload } from "../../auto-reply/types.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js";
@@ -32,6 +35,28 @@ const SLACK_COMMAND_ARG_OVERFLOW_MAX = 5;
const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100;
const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75;
const SLACK_HEADER_TEXT_MAX = 150;
let slashCommandsRuntimePromise: Promise<typeof import("./slash-commands.runtime.js")> | null =
null;
let slashDispatchRuntimePromise: Promise<typeof import("./slash-dispatch.runtime.js")> | null =
null;
let slashSkillCommandsRuntimePromise: Promise<
typeof import("./slash-skill-commands.runtime.js")
> | null = null;
function loadSlashCommandsRuntime() {
slashCommandsRuntimePromise ??= import("./slash-commands.runtime.js");
return slashCommandsRuntimePromise;
}
function loadSlashDispatchRuntime() {
slashDispatchRuntimePromise ??= import("./slash-dispatch.runtime.js");
return slashDispatchRuntimePromise;
}
function loadSlashSkillCommandsRuntime() {
slashSkillCommandsRuntimePromise ??= import("./slash-skill-commands.runtime.js");
return slashSkillCommandsRuntimePromise;
}
type EncodedMenuChoice = SlackExternalArgMenuChoice;
const slackExternalArgMenuStore = createSlackExternalArgMenuStore();
@@ -75,15 +100,6 @@ function readSlackExternalArgMenuToken(raw: unknown): string | undefined {
return slackExternalArgMenuStore.readToken(raw);
}
type CommandsRegistry = typeof import("../../auto-reply/commands-registry.js");
let commandsRegistry: CommandsRegistry | undefined;
async function getCommandsRegistry(): Promise<CommandsRegistry> {
if (!commandsRegistry) {
commandsRegistry = await import("../../auto-reply/commands-registry.js");
}
return commandsRegistry;
}
function encodeSlackCommandArgValue(parts: {
command: string;
arg: string;
@@ -470,8 +486,8 @@ export async function registerSlackMonitorSlashCommands(params: {
}
if (commandDefinition && supportsInteractiveArgMenus) {
const reg = await getCommandsRegistry();
const menu = reg.resolveCommandArgMenu({
const { resolveCommandArgMenu } = await loadSlashCommandsRuntime();
const menu = resolveCommandArgMenu({
command: commandDefinition,
args: commandArgs,
cfg,
@@ -501,21 +517,17 @@ export async function registerSlackMonitorSlashCommands(params: {
const channelName = channelInfo?.name;
const roomLabel = channelName ? `#${channelName}` : `#${command.channel_id}`;
const [{ resolveAgentRoute }, { finalizeInboundContext }, { dispatchReplyWithDispatcher }] =
await Promise.all([
import("../../routing/resolve-route.js"),
import("../../auto-reply/reply/inbound-context.js"),
import("../../auto-reply/reply/provider-dispatcher.js"),
]);
const [
{ resolveConversationLabel },
{ createReplyPrefixOptions },
{ recordInboundSessionMetaSafe },
] = await Promise.all([
import("../../channels/conversation-label.js"),
import("../../channels/reply-prefix.js"),
import("../../channels/session-meta.js"),
]);
const {
createReplyPrefixOptions,
deliverSlackSlashReplies,
dispatchReplyWithDispatcher,
finalizeInboundContext,
recordInboundSessionMetaSafe,
resolveAgentRoute,
resolveChunkMode,
resolveConversationLabel,
resolveMarkdownTableMode,
} = await loadSlashDispatchRuntime();
const route = resolveAgentRoute({
cfg,
@@ -595,12 +607,6 @@ export async function registerSlackMonitorSlashCommands(params: {
});
const deliverSlashPayloads = async (replies: ReplyPayload[]) => {
const [{ deliverSlackSlashReplies }, { resolveChunkMode }, { resolveMarkdownTableMode }] =
await Promise.all([
import("./replies.js"),
import("../../auto-reply/chunk.js"),
import("../../config/markdown-tables.js"),
]);
await deliverSlackSlashReplies({
replies,
respond,
@@ -653,34 +659,39 @@ export async function registerSlackMonitorSlashCommands(params: {
globalSetting: cfg.commands?.nativeSkills,
});
let reg: CommandsRegistry | undefined;
let nativeCommands: Array<{ name: string }> = [];
let slashCommandsRuntime: typeof import("./slash-commands.runtime.js") | null = null;
if (nativeEnabled) {
reg = await getCommandsRegistry();
slashCommandsRuntime = await loadSlashCommandsRuntime();
const skillCommands = nativeSkillsEnabled
? (await import("../../auto-reply/skill-commands.js")).listSkillCommandsForAgents({ cfg })
? (await loadSlashSkillCommandsRuntime()).listSkillCommandsForAgents({ cfg })
: [];
nativeCommands = reg.listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "slack" });
nativeCommands = slashCommandsRuntime.listNativeCommandSpecsForConfig(cfg, {
skillCommands,
provider: "slack",
});
}
if (nativeCommands.length > 0) {
const registry = reg;
if (!registry) {
throw new Error("Missing commands registry for native Slack commands.");
if (!slashCommandsRuntime) {
throw new Error("Missing commands runtime for native Slack commands.");
}
for (const command of nativeCommands) {
ctx.app.command(
`/${command.name}`,
async ({ command: cmd, ack, respond, body }: SlackCommandMiddlewareArgs) => {
const commandDefinition = registry.findCommandByNativeName(command.name, "slack");
const commandDefinition = slashCommandsRuntime.findCommandByNativeName(
command.name,
"slack",
);
const rawText = cmd.text?.trim() ?? "";
const commandArgs = commandDefinition
? registry.parseCommandArgs(commandDefinition, rawText)
? slashCommandsRuntime.parseCommandArgs(commandDefinition, rawText)
: rawText
? ({ raw: rawText } satisfies CommandArgs)
: undefined;
const prompt = commandDefinition
? registry.buildCommandTextFromArgs(commandDefinition, commandArgs)
? slashCommandsRuntime.buildCommandTextFromArgs(commandDefinition, commandArgs)
: rawText
? `/${command.name} ${rawText}`
: `/${command.name}`;
@@ -824,13 +835,14 @@ export async function registerSlackMonitorSlashCommands(params: {
});
return;
}
const reg = await getCommandsRegistry();
const commandDefinition = reg.findCommandByNativeName(parsed.command, "slack");
const { buildCommandTextFromArgs, findCommandByNativeName } =
await loadSlashCommandsRuntime();
const commandDefinition = findCommandByNativeName(parsed.command, "slack");
const commandArgs: CommandArgs = {
values: { [parsed.arg]: parsed.value },
};
const prompt = commandDefinition
? reg.buildCommandTextFromArgs(commandDefinition, commandArgs)
? buildCommandTextFromArgs(commandDefinition, commandArgs)
: `/${parsed.command} ${parsed.value}`;
const user = body.user;
const userName =

View File

@@ -0,0 +1,74 @@
import { isRecord } from "../utils.js";
import { fetchWithTimeout } from "../utils/fetch-timeout.js";
import type {
AuditTelegramGroupMembershipParams,
TelegramGroupMembershipAudit,
TelegramGroupMembershipAuditEntry,
} from "./audit.js";
import { makeProxyFetch } from "./proxy.js";
const TELEGRAM_API_BASE = "https://api.telegram.org";
type TelegramApiOk<T> = { ok: true; result: T };
type TelegramApiErr = { ok: false; description?: string };
type TelegramGroupMembershipAuditData = Omit<TelegramGroupMembershipAudit, "elapsedMs">;
export async function auditTelegramGroupMembershipImpl(
params: AuditTelegramGroupMembershipParams,
): Promise<TelegramGroupMembershipAuditData> {
const fetcher = params.proxyUrl ? makeProxyFetch(params.proxyUrl) : fetch;
const base = `${TELEGRAM_API_BASE}/bot${params.token}`;
const groups: TelegramGroupMembershipAuditEntry[] = [];
for (const chatId of params.groupIds) {
try {
const url = `${base}/getChatMember?chat_id=${encodeURIComponent(chatId)}&user_id=${encodeURIComponent(String(params.botId))}`;
const res = await fetchWithTimeout(url, {}, params.timeoutMs, fetcher);
const json = (await res.json()) as TelegramApiOk<{ status?: string }> | TelegramApiErr;
if (!res.ok || !isRecord(json) || !json.ok) {
const desc =
isRecord(json) && !json.ok && typeof json.description === "string"
? json.description
: `getChatMember failed (${res.status})`;
groups.push({
chatId,
ok: false,
status: null,
error: desc,
matchKey: chatId,
matchSource: "id",
});
continue;
}
const status = isRecord((json as TelegramApiOk<unknown>).result)
? ((json as TelegramApiOk<{ status?: string }>).result.status ?? null)
: null;
const ok = status === "creator" || status === "administrator" || status === "member";
groups.push({
chatId,
ok,
status,
error: ok ? null : "bot not in group",
matchKey: chatId,
matchSource: "id",
});
} catch (err) {
groups.push({
chatId,
ok: false,
status: null,
error: err instanceof Error ? err.message : String(err),
matchKey: chatId,
matchSource: "id",
});
}
}
return {
ok: groups.every((g) => g.ok),
checkedGroups: groups.length,
unresolvedGroups: 0,
hasWildcardUnmentionedGroups: false,
groups,
};
}

View File

@@ -1,7 +1,4 @@
import type { TelegramGroupConfig } from "../config/types.js";
import { isRecord } from "../utils.js";
const TELEGRAM_API_BASE = "https://api.telegram.org";
export type TelegramGroupMembershipAuditEntry = {
chatId: string;
@@ -21,9 +18,6 @@ export type TelegramGroupMembershipAudit = {
elapsedMs: number;
};
type TelegramApiOk<T> = { ok: true; result: T };
type TelegramApiErr = { ok: false; description?: string };
export function collectTelegramUnmentionedGroupIds(
groups: Record<string, TelegramGroupConfig> | undefined,
) {
@@ -65,13 +59,25 @@ export function collectTelegramUnmentionedGroupIds(
return { groupIds, unresolvedGroups, hasWildcardUnmentionedGroups };
}
export async function auditTelegramGroupMembership(params: {
export type AuditTelegramGroupMembershipParams = {
token: string;
botId: number;
groupIds: string[];
proxyUrl?: string;
timeoutMs: number;
}): Promise<TelegramGroupMembershipAudit> {
};
let auditMembershipRuntimePromise: Promise<typeof import("./audit-membership-runtime.js")> | null =
null;
function loadAuditMembershipRuntime() {
auditMembershipRuntimePromise ??= import("./audit-membership-runtime.js");
return auditMembershipRuntimePromise;
}
export async function auditTelegramGroupMembership(
params: AuditTelegramGroupMembershipParams,
): Promise<TelegramGroupMembershipAudit> {
const started = Date.now();
const token = params.token?.trim() ?? "";
if (!token || params.groupIds.length === 0) {
@@ -87,63 +93,13 @@ export async function auditTelegramGroupMembership(params: {
// Lazy import to avoid pulling `undici` (ProxyAgent) into cold-path callers that only need
// `collectTelegramUnmentionedGroupIds` (e.g. config audits).
const fetcher = params.proxyUrl
? (await import("./proxy.js")).makeProxyFetch(params.proxyUrl)
: fetch;
const { fetchWithTimeout } = await import("../utils/fetch-timeout.js");
const base = `${TELEGRAM_API_BASE}/bot${token}`;
const groups: TelegramGroupMembershipAuditEntry[] = [];
for (const chatId of params.groupIds) {
try {
const url = `${base}/getChatMember?chat_id=${encodeURIComponent(chatId)}&user_id=${encodeURIComponent(String(params.botId))}`;
const res = await fetchWithTimeout(url, {}, params.timeoutMs, fetcher);
const json = (await res.json()) as TelegramApiOk<{ status?: string }> | TelegramApiErr;
if (!res.ok || !isRecord(json) || !json.ok) {
const desc =
isRecord(json) && !json.ok && typeof json.description === "string"
? json.description
: `getChatMember failed (${res.status})`;
groups.push({
chatId,
ok: false,
status: null,
error: desc,
matchKey: chatId,
matchSource: "id",
});
continue;
}
const status = isRecord((json as TelegramApiOk<unknown>).result)
? ((json as TelegramApiOk<{ status?: string }>).result.status ?? null)
: null;
const ok = status === "creator" || status === "administrator" || status === "member";
groups.push({
chatId,
ok,
status,
error: ok ? null : "bot not in group",
matchKey: chatId,
matchSource: "id",
});
} catch (err) {
groups.push({
chatId,
ok: false,
status: null,
error: err instanceof Error ? err.message : String(err),
matchKey: chatId,
matchSource: "id",
});
}
}
const { auditTelegramGroupMembershipImpl } = await loadAuditMembershipRuntime();
const result = await auditTelegramGroupMembershipImpl({
...params,
token,
});
return {
ok: groups.every((g) => g.ok),
checkedGroups: groups.length,
unresolvedGroups: 0,
hasWildcardUnmentionedGroups: false,
groups,
...result,
elapsedMs: Date.now() - started,
};
}

View File

@@ -143,6 +143,14 @@ export function getCacheStats(): { count: number; oldestAt?: string; newestAt?:
const STICKER_DESCRIPTION_PROMPT =
"Describe this sticker image in 1-2 sentences. Focus on what the sticker depicts (character, object, action, emotion). Be concise and objective.";
const VISION_PROVIDERS = ["openai", "anthropic", "google", "minimax"] as const;
let imageRuntimePromise: Promise<
typeof import("../media-understanding/providers/image-runtime.js")
> | null = null;
function loadImageRuntime() {
imageRuntimePromise ??= import("../media-understanding/providers/image-runtime.js");
return imageRuntimePromise;
}
export interface DescribeStickerParams {
imagePath: string;
@@ -242,8 +250,8 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi
try {
const buffer = await fs.readFile(imagePath);
// Dynamic import to avoid circular dependency
const { describeImageWithModel } = await import("../media-understanding/providers/image.js");
// Lazy import to avoid circular dependency
const { describeImageWithModel } = await loadImageRuntime();
const result = await describeImageWithModel({
buffer,
fileName: "sticker.webp",